点火放孔明灯 · 模拟器
by HyperMaker20478 lines17.6 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>点火放孔明灯 · 模拟器</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Microsoft YaHei', sans-serif;
}
#info {
position: absolute;
top: 20px;
left: 20px;
color: rgba(255, 255, 255, 0.9);
background: rgba(0, 0, 0, 0.5);
padding: 12px 24px;
border-radius: 40px;
backdrop-filter: blur(8px);
pointer-events: none;
z-index: 10;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
border-left: 4px solid #ffaa00;
}
#info h1 {
margin: 0;
font-size: 1.2rem;
font-weight: 400;
letter-spacing: 2px;
}
#info p {
margin: 5px 0 0;
font-size: 0.8rem;
opacity: 0.8;
}
#ignite-btn {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
padding: 16px 48px;
font-size: 1.4rem;
font-weight: bold;
color: #fff;
background: linear-gradient(45deg, #ff6a00, #ee3e00);
border: none;
border-radius: 60px;
cursor: pointer;
z-index: 20;
box-shadow: 0 0 25px rgba(255, 100, 0, 0.8), 0 10px 20px rgba(0,0,0,0.3);
letter-spacing: 4px;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 200, 0.6);
text-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
#ignite-btn:hover {
transform: translateX(-50%) scale(1.05);
box-shadow: 0 0 40px rgba(255, 150, 0, 1), 0 15px 25px rgba(0,0,0,0.4);
background: linear-gradient(45deg, #ff7b1c, #ff4d00);
}
#ignite-btn:active {
transform: translateX(-50%) scale(0.95);
}
#ignite-btn:disabled {
opacity: 0.5;
pointer-events: none;
filter: grayscale(0.6);
box-shadow: 0 0 10px rgba(255, 100, 0, 0.3);
}
#status {
position: absolute;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
color: #ffdd99;
background: rgba(20, 20, 30, 0.7);
padding: 8px 24px;
border-radius: 30px;
font-size: 0.95rem;
letter-spacing: 2px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 200, 100, 0.4);
transition: opacity 0.5s;
z-index: 15;
pointer-events: none;
white-space: nowrap;
}
.instruction {
position: absolute;
bottom: 20px;
right: 20px;
color: rgba(255, 255, 255, 0.4);
font-size: 0.7rem;
z-index: 5;
}
</style>
</head>
<body>
<div id="info">
<h1>✨ 孔明灯 · 许愿 ✨</h1>
<p>点击下方按钮点火 · 让它升空</p>
</div>
<div id="status">🏮 等待点火...</div>
<button id="ignite-btn">点 火</button>
<div class="instruction">拖动视角观看 | 模拟效果</div>
<!-- 引入 Three.js 和 OrbitControls -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.128.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- 初始化场景、相机、渲染器 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a); // 深蓝色夜空
// 添加一点渐变效果:用雾来模拟
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.002);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(8, 6, 15);
camera.lookAt(0, 3, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // 虽然用不上但无妨
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// --- 轨道控制器 ---
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = false;
controls.enableZoom = true;
controls.target.set(0, 3, 0);
controls.maxPolarAngle = Math.PI / 2.2; // 限制角度,不要看到地底
controls.minDistance = 5;
controls.maxDistance = 30;
// --- 灯光 ---
// 环境光
const ambientLight = new THREE.AmbientLight(0x404060);
scene.add(ambientLight);
// 主光源 - 偏蓝色的月光
const moonLight = new THREE.DirectionalLight(0xaaccff, 0.8);
moonLight.position.set(-10, 20, 10);
scene.add(moonLight);
// 辅助背光暖色,让孔明灯更有氛围
const backLight = new THREE.PointLight(0xffaa55, 0.5, 40);
backLight.position.set(5, 5, -10);
scene.add(backLight);
// 底部补光
const fillLight = new THREE.PointLight(0x4466aa, 0.4);
fillLight.position.set(0, 2, 8);
scene.add(fillLight);
// --- 创建星空背景效果(粒子系统)---
const starsGeometry = new THREE.BufferGeometry();
const starsCount = 2000;
const starsPositions = new Float32Array(starsCount * 3);
for (let i = 0; i < starsCount * 3; i += 3) {
// 随机分布在很大的球壳内
const r = 80 + Math.random() * 60;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
starsPositions[i] = Math.sin(phi) * Math.cos(theta) * r;
starsPositions[i+1] = Math.sin(phi) * Math.sin(theta) * r * 0.6 + 30; // 让星星偏上
starsPositions[i+2] = Math.cos(phi) * r;
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starsPositions, 3));
const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.25, transparent: true, opacity: 0.9 });
const stars = new THREE.Points(starsGeometry, starsMaterial);
scene.add(stars);
// 再加一些小而亮的星星
const brightStarsGeo = new THREE.BufferGeometry();
const brightCount = 300;
const brightPositions = new Float32Array(brightCount * 3);
for (let i = 0; i < brightCount * 3; i += 3) {
const r = 120 + Math.random() * 80;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
brightPositions[i] = Math.sin(phi) * Math.cos(theta) * r;
brightPositions[i+1] = Math.sin(phi) * Math.sin(theta) * r * 0.7 + 40;
brightPositions[i+2] = Math.cos(phi) * r;
}
brightStarsGeo.setAttribute('position', new THREE.BufferAttribute(brightPositions, 3));
const brightStarsMat = new THREE.PointsMaterial({ color: 0xffeedd, size: 0.4, transparent: true, opacity: 1 });
const brightStars = new THREE.Points(brightStarsGeo, brightStarsMat);
scene.add(brightStars);
// --- 地面 (微弱的草地/土地效果,简单平面) ---
const groundGeometry = new THREE.CircleGeometry(40, 32);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x1a2a1a,
roughness: 0.9,
metalness: 0.1,
emissive: new THREE.Color(0x0a1a0a),
side: THREE.DoubleSide
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.05;
scene.add(ground);
// 地面上的小点装饰(模拟草地/石子)
const grassGeo = new THREE.BufferGeometry();
const grassCount = 800;
const grassPositions = new Float32Array(grassCount * 3);
for (let i = 0; i < grassCount * 3; i += 3) {
const angle = Math.random() * Math.PI * 2;
const rad = 3 + Math.random() * 15;
grassPositions[i] = Math.cos(angle) * rad;
grassPositions[i+1] = 0.02;
grassPositions[i+2] = Math.sin(angle) * rad;
}
grassGeo.setAttribute('position', new THREE.BufferAttribute(grassPositions, 3));
const grassMat = new THREE.PointsMaterial({ color: 0x3a5a3a, size: 0.08 });
const grass = new THREE.Points(grassGeo, grassMat);
scene.add(grass);
// --- 构建孔明灯 ---
const lanternGroup = new THREE.Group();
lanternGroup.position.set(0, 0.8, 0); // 初始在地面上
// 1. 灯体(主体):使用圆柱体+圆锥组合成经典孔明灯形状,带半透明纸张质感
const bodyMaterial = new THREE.MeshStandardMaterial({
color: 0xffdd99,
emissive: new THREE.Color(0xffaa33),
emissiveIntensity: 0.1, // 未点燃时很微弱
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide,
roughness: 0.4,
metalness: 0.1
});
// 中间圆柱部分
const cylinder = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.9, 1.6, 16), bodyMaterial);
cylinder.position.y = 0.8;
cylinder.castShadow = true;
cylinder.receiveShadow = false;
lanternGroup.add(cylinder);
// 上部圆锥顶
const topCone = new THREE.Mesh(new THREE.ConeGeometry(0.9, 0.8, 16), bodyMaterial);
topCone.position.y = 1.6 + 0.4; // 圆柱顶部在1.6,锥高0.8,中心在1.6+0.4
topCone.castShadow = true;
lanternGroup.add(topCone);
// 底部开口的环(竹圈)
const ringMaterial = new THREE.MeshStandardMaterial({ color: 0xcc9966, roughness: 0.7, metalness: 0.2 });
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.06, 8, 24), ringMaterial);
ring.rotation.x = Math.PI / 2;
ring.position.y = 0.0;
lanternGroup.add(ring);
// 上部装饰环
const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.05, 8, 24), ringMaterial);
ring2.rotation.x = Math.PI / 2;
ring2.position.y = 1.6;
lanternGroup.add(ring2);
// 2. 内部的火焰/热源(用于点燃后发光)
const flameMaterial = new THREE.MeshStandardMaterial({
color: 0xff6600,
emissive: new THREE.Color(0xff4400),
emissiveIntensity: 0.0, // 初始不发光
transparent: true,
opacity: 0.0
});
const flame = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), flameMaterial);
flame.position.y = 0.4;
flame.scale.set(1, 0.8, 1);
lanternGroup.add(flame);
// 加一个小火苗的粒子效果(稍后动态控制)
const fireParticlesGeo = new THREE.BufferGeometry();
const particleCount = 30;
const particlePositions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i += 3) {
particlePositions[i] = (Math.random() - 0.5) * 0.6;
particlePositions[i+1] = Math.random() * 0.8 + 0.2;
particlePositions[i+2] = (Math.random() - 0.5) * 0.6;
}
fireParticlesGeo.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
const fireParticlesMat = new THREE.PointsMaterial({
color: 0xffaa00,
size: 0.1,
transparent: true,
opacity: 0,
blending: THREE.AdditiveBlending
});
const fireParticles = new THREE.Points(fireParticlesGeo, fireParticlesMat);
fireParticles.position.y = 0.4;
lanternGroup.add(fireParticles);
// 3. 点缀一些飘带/绳子 (简单装饰)
const ropeMaterial = new THREE.LineBasicMaterial({ color: 0xccaa88 });
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2;
const x = Math.cos(angle) * 0.85;
const z = Math.sin(angle) * 0.85;
const points = [
new THREE.Vector3(x, 0.0, z),
new THREE.Vector3(x * 1.1, -0.4, z * 1.1)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, ropeMaterial);
lanternGroup.add(line);
}
scene.add(lanternGroup);
// 在灯下加一个微弱的点光源,点燃后增强
const lanternLight = new THREE.PointLight(0xff6600, 0, 10);
lanternLight.position.set(0, 0.8, 0);
lanternGroup.add(lanternLight);
// --- 状态管理 ---
let isIgnited = false; // 是否已经点火
let isFlying = false; // 是否在升空
let flySpeed = 0.0; // 升空速度
let time = 0; // 时间累积,用于动画
// 获取按钮和状态显示元素
const igniteBtn = document.getElementById('ignite-btn');
const statusDiv = document.getElementById('status');
// 点燃函数
function ignite() {
if (isIgnited) return;
isIgnited = true;
isFlying = true;
flySpeed = 0.0; // 初始速度
// 更新按钮和状态
igniteBtn.disabled = true;
igniteBtn.textContent = '🔥 已点燃';
statusDiv.textContent = '🏮 孔明灯正在升空... 许个愿吧 ✨';
// 改变材质发光效果
bodyMaterial.emissiveIntensity = 0.8;
bodyMaterial.opacity = 0.95;
flameMaterial.emissiveIntensity = 1.5;
flameMaterial.opacity = 0.9;
// 粒子效果显现
fireParticlesMat.opacity = 0.9;
// 灯光加强
lanternLight.intensity = 2.5;
lanternLight.distance = 15;
// 改变火焰颜色稍微亮一点
flameMaterial.color.setHex(0xffaa00);
flameMaterial.emissive.setHex(0xff8800);
}
igniteBtn.addEventListener('click', ignite);
// 重置/重新开始功能?这里不需要,但可以允许刷新页面重新来。为了友好,加一个隐藏的重置(通过双击按钮?不,保持简单)
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
const delta = 0.016; // 近似60fps
time += delta;
// 1. 如果已经点燃,孔明灯上升
if (isFlying) {
// 加速上升:速度逐渐增加
flySpeed += 0.002;
// 限制最大速度
if (flySpeed > 0.08) flySpeed = 0.08;
// 向上移动
lanternGroup.position.y += flySpeed;
// 轻微左右飘动(模拟风)
lanternGroup.position.x += Math.sin(time * 1.5) * 0.002;
lanternGroup.position.z += Math.cos(time * 1.8) * 0.002;
// 一点旋转晃动
lanternGroup.rotation.z = Math.sin(time * 2) * 0.03;
lanternGroup.rotation.x = Math.cos(time * 2.3) * 0.03;
lanternGroup.rotation.y += 0.001;
// 如果飞得太高,消失并重置?为了模拟器体验,让它一直飞,但可能飞出视野。不过相机可以跟着?不强制。
// 如果高度超过25,稍微降低一点透明度或者让它继续(让用户还能看到)
if (lanternGroup.position.y > 22) {
// 保持可见,但可以慢慢淡出?为了不丢失,我们让相机可自由观看,不重置。
// 但为了防止无限上升,设置一个上限,然后悬停?还是让它飞走再重置?干脆让它继续,用户想看就跟着。
// 为了更好的体验,在高度超过25后,加速上升直至消失?但用户可能想一直看。我们让它超过30后停止上升,悬停在高空?不好,还是让它可以一直飞,但最大速度限制,同时相机可以跟随。
// 为了演示方便,我们到30高度后就停止上升,让灯悬停在高空,但有点奇怪,还是让它继续,但用户想看可以调视角。
// 这里我们保持继续上升,但为了不飞出太远,让速度在高度高时降低?不,保持原样。
}
}
// 2. 火焰/粒子动态闪烁 (无论是否点燃,但点燃后更明显)
if (isIgnited) {
// 火焰跳动
const scale = 1 + Math.sin(time * 20) * 0.15;
flame.scale.set(scale, 0.8 * scale, scale);
// 粒子闪烁
fireParticlesMat.size = 0.12 + Math.sin(time * 30) * 0.04;
// 灯光强度微变
lanternLight.intensity = 2.2 + Math.sin(time * 25) * 0.8;
// 灯体发光强度微变
bodyMaterial.emissiveIntensity = 0.7 + Math.sin(time * 15) * 0.2;
} else {
// 未点燃时,只有极其微弱的呼吸效果(模拟等待)
bodyMaterial.emissiveIntensity = 0.08 + Math.sin(time * 3) * 0.03;
}
// 3. 星星缓慢旋转(增加动感)
stars.rotation.y += 0.0001;
brightStars.rotation.y -= 0.00015;
// 4. 更新控制器
controls.update();
// 5. 让相机轻微自动跟随上升的灯?不强制,但可以稍微调整 controls.target 跟随?
// 为了视角更好,如果灯上升超过一定高度,让 controls.target 慢慢跟随灯的位置,但保持相机相对。
// 这样用户不用手动调整也能看到灯。但为了自由性,还是让用户控制。这里可选:温和跟随。
if (isFlying && lanternGroup.position.y > 5) {
// 让目标点平滑跟随灯的位置(但保留y轴偏移,使视角舒适)
const targetY = lanternGroup.position.y + 1.5;
controls.target.y += (targetY - controls.target.y) * 0.02;
// 同时让相机也稍微提升,但保持相对角度?可以稍微向上移动相机
// 这里不强制移动相机位置,因为OrbitControls会处理
} else if (!isFlying && lanternGroup.position.y <= 1) {
controls.target.y += (3 - controls.target.y) * 0.05; // 重置目标高度
}
// 渲染
renderer.render(scene, camera);
}
animate();
// --- 窗口大小自适应 ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 一些小提示:初始状态显示
setTimeout(() => {
statusDiv.style.opacity = '1';
}, 100);
// 禁用按钮双击选中文本等
igniteBtn.addEventListener('mousedown', (e) => e.preventDefault());
// 添加一点初始化时的飘动(未点燃时的小晃动?不必要)
</script>
</body>
</html>Game Source: 点火放孔明灯 · 模拟器
Creator: HyperMaker20
Libraries: three
Complexity: complex (478 lines, 17.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-hypermaker20-muhq3cx5" to link back to the original. Then publish at arcadelab.ai/publish.