console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover" />
<title>Paper Station · 花开的这一站</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400&display=swap');
html, body { margin: 0; height: 100%; overflow: hidden; background: #2b2b36; -webkit-tap-highlight-color: transparent; touch-action: none; font-family: 'Noto Serif SC', "Songti SC", serif; }
canvas { display: block; }
/* 全局暗角与纱幕 */
#vignette { position: fixed; inset: 0; z-index: 10; pointer-events: none; background: radial-gradient(ellipse at 50% 50%, rgba(0,0,0,0) 40%, rgba(20,20,35,0.7) 100%); }
#veil { position: fixed; inset: 0; z-index: 11; pointer-events: none; background: linear-gradient(to bottom, rgba(160,140,210,0.1), transparent); }
/* 左侧排版 */
#poem { position: fixed; left: 6vw; top: 15vh; z-index: 20; pointer-events: none; color: #e8e4f0; text-shadow: 0 2px 10px rgba(0,0,0,0.5); }
#poem .title { font-size: 24px; letter-spacing: 0.2em; line-height: 1.5; margin-bottom: 10px; font-weight: 300; }
#poem .sub { font-size: 9px; letter-spacing: 0.3em; opacity: 0.6; margin-bottom: 40px; font-family: monospace; }
#poem p { margin: 0 0 8px 0; font-size: 11px; letter-spacing: 0.15em; opacity: 0.7; line-height: 2; }
#poem .stage { margin-top: 40px; font-size: 9px; opacity: 0.4; font-family: monospace; letter-spacing: 0.2em; }
/* 底部控制台 */
#bottomConsole { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 20; display: flex; flex-direction: column; align-items: center; gap: 12px; }
#toggles { display: flex; gap: 8px; background: rgba(50, 45, 65, 0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 20px; padding: 4px; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
#toggles button { border: 0; background: transparent; padding: 6px 16px; border-radius: 16px; font-family: inherit; font-size: 11px; color: #a090b0; cursor: pointer; transition: all .3s; }
#toggles button.active { background: rgba(255,255,255,0.15); color: #fff; }
.btns { display: flex; gap: 12px; }
.roundBtn { height: 38px; border-radius: 19px; border: 1px solid rgba(255,255,255,0.1); background: rgba(50, 45, 65, 0.6); backdrop-filter: blur(10px); color: #ddd; font-family: inherit; font-size: 12px; letter-spacing: 0.1em; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0 24px; }
#resetBtn { width: 38px; padding: 0; font-size: 16px; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.161.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.161.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="vignette"></div>
<div id="veil"></div>
<div id="poem">
<div class="title">花开的<br>这一站</div>
<div class="sub">FLOWERS OF THIS STATION</div>
<p>风会记得,</p>
<p>那场被黄昏拉长的等待。</p>
<div class="stage">UNFINISHED STATION · STAGE 31</div>
</div>
<div id="bottomConsole">
<div id="toggles">
<button class="active">花夜</button>
<button>晚霞</button>
<button>晴日</button>
</div>
<div class="btns">
<button id="ticketBtn" class="roundBtn">领取车票</button>
<button id="resetBtn" class="roundBtn">↺</button>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ============================================================
0. 基础设置
============================================================ */
const BASE_L = 4.0, BASE_W = 2.6, BASE_H = 0.3;
let _seed = 88888;
function rand() { _seed = (_seed * 1664525 + 1013904223) % 4294967296; return _seed / 4294967296; }
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
// 贴近参考图的蓝紫渐变背景
scene.background = new THREE.Color(0x8a88a8);
const camera = new THREE.PerspectiveCamera(32, window.innerWidth / window.innerHeight, 0.1, 100);
// 微缩沙盘正侧微俯视
camera.position.set(0, 2.2, 5.5);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0.4, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enablePan = false;
controls.minDistance = 3.0;
controls.maxDistance = 7.5;
controls.minPolarAngle = 0.9;
controls.maxPolarAngle = 1.3;
controls.update();
/* ============================================================
1. 光照系统 (营造黄昏/夜景的对比)
============================================================ */
const hemiLight = new THREE.HemisphereLight(0xb0b8e0, 0x2a2035, 1.2);
scene.add(hemiLight);
const keyLight = new THREE.DirectionalLight(0xffe0f0, 2.0); // 偏暖的受光
keyLight.position.set(3, 5, 4);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
keyLight.shadow.camera.left = -5;
keyLight.shadow.camera.right = 5;
keyLight.shadow.camera.top = 5;
keyLight.shadow.camera.bottom = -5;
keyLight.shadow.bias = -0.001;
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0x9ab0f0, 1.0); // 冷色补光
fillLight.position.set(-4, 3, -2);
scene.add(fillLight);
/* ============================================================
2. 纹理工厂
============================================================ */
// 生成渐变光晕 (用于灯光和发光面)
function createGlowTexture() {
const c = document.createElement('canvas'); c.width = 128; c.height = 128;
const ctx = c.getContext('2d');
const grad = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
grad.addColorStop(0, 'rgba(255,255,255,1)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.6)');
grad.addColorStop(0.7, 'rgba(255,255,255,0.1)');
grad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 128, 128);
return new THREE.CanvasTexture(c);
}
const glowTex = createGlowTexture();
// 生成车内发光贴图 (参考图里的暖色内景)
function createBusInteriorTex() {
const c = document.createElement('canvas'); c.width = 512; c.height = 128;
const ctx = c.getContext('2d');
// 暖光背景
const grad = ctx.createLinearGradient(0, 0, 0, 128);
grad.addColorStop(0, '#ffeedd');
grad.addColorStop(1, '#ffbb77');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 512, 128);
// 绘制深色座椅与拉手
ctx.fillStyle = '#664433';
for(let i=20; i<512; i+=60) {
ctx.fillRect(i, 60, 30, 68); // 座椅
}
ctx.strokeStyle = '#ffddaa';
ctx.lineWidth = 2;
for(let i=40; i<512; i+=80) {
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, 30); ctx.stroke();
ctx.beginPath(); ctx.arc(i, 35, 5, 0, Math.PI*2); ctx.stroke();
}
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const busInteriorTex = createBusInteriorTex();
// 生成广告牌贴图
function createPosterTex() {
const c = document.createElement('canvas'); c.width = 256; c.height = 384;
const ctx = c.getContext('2d');
ctx.fillStyle = '#f5f0f8'; ctx.fillRect(0, 0, 256, 384);
// 画点紫色的树
ctx.fillStyle = '#9a7ec4';
for(let i=0; i<50; i++) {
ctx.beginPath();
ctx.arc(128 + (rand()-0.5)*150, 150 + (rand()-0.5)*150, 10 + rand()*20, 0, Math.PI*2);
ctx.fill();
}
ctx.fillStyle = '#6a5a7a'; ctx.font = 'bold 24px serif'; ctx.textAlign = 'center';
ctx.fillText('花开的这一站', 128, 320);
ctx.font = '14px monospace'; ctx.fillText('FLOWERS OF THIS STATION', 128, 350);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const posterTex = createPosterTex();
/* ============================================================
3. 底座与地面
============================================================ */
const matBase = new THREE.MeshStandardMaterial({ color: 0x2b2b36, roughness: 0.8, metalness: 0.2 });
const matTile = new THREE.MeshStandardMaterial({ color: 0x3a3845, roughness: 0.7 }); // 人行道
const matRoad = new THREE.MeshStandardMaterial({ color: 0x1c1a24, roughness: 0.9 }); // 柏油路
const matLine = new THREE.MeshStandardMaterial({ color: 0x5a5a6a, roughness: 0.5 });
// 主底座 (带有厚度)
const base = new THREE.Mesh(new THREE.BoxGeometry(BASE_L, BASE_H, BASE_W), matBase);
base.position.set(0, -BASE_H/2, 0);
base.castShadow = true; base.receiveShadow = true;
scene.add(base);
// 人行道 (占后方 60%)
const sidewalk = new THREE.Mesh(new THREE.BoxGeometry(BASE_L, 0.12, BASE_W * 0.6), matTile);
sidewalk.position.set(0, 0.06, -BASE_W * 0.2);
sidewalk.castShadow = true; sidewalk.receiveShadow = true;
scene.add(sidewalk);
// 马路 (占前方 40%)
const road = new THREE.Mesh(new THREE.PlaneGeometry(BASE_L, BASE_W * 0.4), matRoad);
road.rotation.x = -Math.PI/2;
road.position.set(0, 0.002, BASE_W * 0.3);
road.receiveShadow = true;
scene.add(road);
// 路边停车线
for(let i=-1.5; i<=1.5; i+=0.6) {
const line = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 0.02), matLine);
line.rotation.x = -Math.PI/2;
line.position.set(i, 0.004, BASE_W * 0.3);
scene.add(line);
}
/* ============================================================
4. 双层观光大巴 (核心复刻)
============================================================ */
const busGroup = new THREE.Group();
busGroup.position.set(0.5, 0, 0.3);
// 车头朝左 (-X方向),不需要旋转
const matBusBody = new THREE.MeshStandardMaterial({ color: 0x8a7e9c, roughness: 0.4, metalness: 0.3 }); // 淡紫色车漆
const matBusDark = new THREE.MeshStandardMaterial({ color: 0x4a3e5c, roughness: 0.6 });
const matBusGold = new THREE.MeshStandardMaterial({ color: 0xd4af37, roughness: 0.3, metalness: 0.8 }); // 金色栏杆
const matTire = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 });
const matHub = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.2 });
const matHeadlight = new THREE.MeshBasicMaterial({ color: 0xffffff });
const matTaillight = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
// 下层发光玻璃材质
const matGlassLower = new THREE.MeshStandardMaterial({
map: busInteriorTex, emissive: 0xffffff, emissiveMap: busInteriorTex, emissiveIntensity: 0.8,
roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.9
});
const bL = 2.8; // 车长
const bW = 0.9; // 车宽
const hLower = 0.6; // 下层高
const hUpper = 0.4; // 上层高
const yLower = 0.18; // 底盘高度
// --- 下层车身 ---
const lowerBody = new THREE.Mesh(new THREE.BoxGeometry(bL, hLower, bW), matBusBody);
lowerBody.position.set(0, yLower + hLower/2, 0);
lowerBody.castShadow = true; lowerBody.receiveShadow = true;
busGroup.add(lowerBody);
// 下层车窗 (左右)
const winLowerGeo = new THREE.PlaneGeometry(bL - 0.2, hLower * 0.6);
const winL = new THREE.Mesh(winLowerGeo, matGlassLower);
winL.position.set(0, yLower + hLower*0.6, bW/2 + 0.005);
busGroup.add(winL);
const winR = new THREE.Mesh(winLowerGeo, matGlassLower);
winR.position.set(0, yLower + hLower*0.6, -bW/2 - 0.005);
winR.rotation.y = Math.PI;
busGroup.add(winR);
// 窗框立柱
for(let i=0; i<6; i++) {
const x = -bL/2 + 0.2 + i * ((bL-0.4)/5);
for(let z of [bW/2+0.01, -bW/2-0.01]) {
const p = new THREE.Mesh(new THREE.BoxGeometry(0.03, hLower*0.6+0.05, 0.02), matBusDark);
p.position.set(x, yLower + hLower*0.6, z);
busGroup.add(p);
}
}
// 车门
const door = new THREE.Mesh(new THREE.PlaneGeometry(0.3, hLower*0.8), matGlassLower);
door.position.set(-bL/2 + 0.3, yLower + hLower*0.5, bW/2 + 0.01);
busGroup.add(door);
const doorFrame = new THREE.Mesh(new THREE.BoxGeometry(0.32, hLower*0.8+0.04, 0.02), matBusGold);
doorFrame.position.set(-bL/2 + 0.3, yLower + hLower*0.5, bW/2 + 0.005);
busGroup.add(doorFrame);
// --- 中部腰线 ---
const belt = new THREE.Mesh(new THREE.BoxGeometry(bL+0.04, 0.04, bW+0.04), matBusGold);
belt.position.set(0, yLower + hLower, 0);
busGroup.add(belt);
// --- 上层敞篷区 ---
// 地板
const upperFloor = new THREE.Mesh(new THREE.BoxGeometry(bL, 0.05, bW), matBusDark);
upperFloor.position.set(0, yLower + hLower + 0.025, 0);
upperFloor.castShadow = true;
busGroup.add(upperFloor);
// 上层座椅 (从外面能看见一点)
const seatGeo = new THREE.BoxGeometry(0.2, 0.15, 0.2);
const matSeat = new THREE.MeshStandardMaterial({ color: 0x4a3e5c });
for(let x=-bL/2+0.4; x<=bL/2-0.4; x+=0.4) {
for(let z of [-0.2, 0.2]) {
const s = new THREE.Mesh(seatGeo, matSeat);
s.position.set(x, yLower + hLower + 0.125, z);
busGroup.add(s);
}
}
// 上层金色栏杆
const railY = yLower + hLower + hUpper;
for(let z of [-bW/2+0.02, bW/2-0.02]) {
const topRail = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, bL, 8), matBusGold);
topRail.rotation.z = Math.PI/2;
topRail.position.set(0, railY, z);
busGroup.add(topRail);
// 立柱
for(let x=-bL/2+0.1; x<=bL/2-0.1; x+=0.2) {
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, hUpper, 6), matBusGold);
post.position.set(x, yLower + hLower + hUpper/2, z);
busGroup.add(post);
}
}
// --- 车轮 ---
const tireGeo = new THREE.CylinderGeometry(0.15, 0.15, 0.1, 24).rotateX(Math.PI/2);
const hubGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.11, 12).rotateX(Math.PI/2);
for(let x of [-0.8, 0.8]) for(let z of [-bW/2+0.05, bW/2-0.05]) {
const t = new THREE.Mesh(tireGeo, matTire); t.position.set(x, 0.15, z); t.castShadow = true; busGroup.add(t);
const h = new THREE.Mesh(hubGeo, matHub); h.position.set(x, 0.15, z); busGroup.add(h);
}
// 车头细节
const frontGlass = new THREE.Mesh(new THREE.PlaneGeometry(bW-0.1, hLower*0.6), matGlassLower);
frontGlass.rotation.y = -Math.PI/2;
frontGlass.position.set(-bL/2 - 0.005, yLower + hLower*0.6, 0);
busGroup.add(frontGlass);
// 车灯
for(let z of [-0.3, 0.3]) {
const hl = new THREE.Mesh(new THREE.CircleGeometry(0.05, 12), matHeadlight);
hl.rotation.y = -Math.PI/2; hl.position.set(-bL/2 - 0.02, yLower+0.2, z); busGroup.add(hl);
const tl = new THREE.Mesh(new THREE.CircleGeometry(0.04, 12), matTaillight);
tl.rotation.y = Math.PI/2; tl.position.set(bL/2 + 0.02, yLower+0.2, z); busGroup.add(tl);
}
scene.add(busGroup);
/* ============================================================
5. 候车亭 (左侧)
============================================================ */
const shelterGroup = new THREE.Group();
shelterGroup.position.set(-1.3, 0.12, -0.6);
const matWhite = new THREE.MeshStandardMaterial({ color: 0xe0d8d0, roughness: 0.5 });
const matWood = new THREE.MeshStandardMaterial({ color: 0x8a5a3a, roughness: 0.7 });
const matPoster = new THREE.MeshStandardMaterial({ map: posterTex, emissive: 0xffffff, emissiveMap: posterTex, emissiveIntensity: 0.3 });
// 细立柱
for(let x of [-0.5, 0.5]) for(let z of [-0.25, 0.25]) {
const p = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.6, 8), matWhite);
p.position.set(x, 0.3, z);
p.castShadow = true;
shelterGroup.add(p);
}
// 平顶
const roof = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.04, 0.6), matWhite);
roof.position.set(0, 0.62, 0);
roof.castShadow = true;
shelterGroup.add(roof);
// 广告背板
const backPanel = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.45, 0.02), matBusDark);
backPanel.position.set(0, 0.35, -0.26);
shelterGroup.add(backPanel);
// 海报
const poster = new THREE.Mesh(new THREE.PlaneGeometry(0.4, 0.35), matPoster);
poster.position.set(0, 0.35, -0.24);
shelterGroup.add(poster);
// 长椅
const bench = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.04, 0.15), matWood);
bench.position.set(0, 0.2, -0.05);
bench.castShadow = true;
shelterGroup.add(bench);
for(let x of [-0.3, 0.3]) {
shelterGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.2, 0.15), matWood).translateX(x).translateY(0.1).translateZ(-0.05));
}
scene.add(shelterGroup);
/* ============================================================
6. 蓝花楹树 (向心交织)
============================================================ */
const matTrunk = new THREE.MeshStandardMaterial({ color: 0x2a1e1c, roughness: 0.95 });
const branchGeos = [];
function addBranch(start, end, r1, r2) {
const len = start.distanceTo(end);
const geo = new THREE.CylinderGeometry(r2, r1, len, 6);
const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), end.clone().sub(start).normalize());
geo.applyMatrix4(new THREE.Matrix4().compose(start.clone().add(end).multiplyScalar(0.5), q, new THREE.Vector3(1,1,1)));
branchGeos.push(geo);
}
// 递归生成树枝
function growBranch(start, dir, len, rad, depth) {
const end = start.clone().addScaledVector(dir, len);
addBranch(start, end, rad, rad*0.7);
if(depth > 0) {
const n = 2 + Math.floor(rand()*2);
for(let i=0; i<n; i++) {
const newDir = dir.clone();
// 向中心靠拢 (x趋向0)
newDir.x += (0 - start.x) * 0.2 + (rand()-0.5)*0.8;
newDir.y += (rand()-0.5)*0.5 + 0.2;
newDir.z += (rand()-0.5)*0.8;
newDir.normalize();
growBranch(end, newDir, len*0.65, rad*0.6, depth-1);
}
} else {
// 末梢生成花团
for(let k=0; k<15; k++) {
bloomData.push({
x: end.x + (rand()-0.5)*0.4,
y: end.y + (rand()-0.5)*0.3,
z: end.z + (rand()-0.5)*0.4,
s: 0.05 + rand()*0.05
});
}
}
}
const bloomData = [];
// 左树
growBranch(new THREE.Vector3(-1.2, 0.12, -0.8), new THREE.Vector3(0.5, 1, 0.2), 0.6, 0.06, 3);
// 右树
growBranch(new THREE.Vector3(1.6, 0.12, -0.6), new THREE.Vector3(-0.5, 1, 0.2), 0.6, 0.06, 3);
const treeMesh = new THREE.Mesh(mergeGeometries(branchGeos), matTrunk);
treeMesh.castShadow = true;
scene.add(treeMesh);
// 花冠 (极其密集的紫色小球)
const bloomGeo = new THREE.IcosahedronGeometry(1, 1);
const bloomMat = new THREE.MeshStandardMaterial({ color: 0x8a6ab5, roughness: 0.6, metalness: 0.1, flatShading: true });
const bloomInstances = new THREE.InstancedMesh(bloomGeo, bloomMat, bloomData.length);
const _m = new THREE.Matrix4(), _p = new THREE.Vector3(), _q = new THREE.Quaternion(), _s = new THREE.Vector3();
const bloomColors = ['#8a6ab5', '#9c7ec8', '#7a5ca5', '#b090d8'].map(c => new THREE.Color(c));
for(let i=0; i<bloomData.length; i++) {
const b = bloomData[i];
_p.set(b.x, b.y, b.z);
_q.setFromEuler(new THREE.Euler(rand()*3, rand()*3, rand()*3));
_s.set(b.s, b.s, b.s);
_m.compose(_p, _q, _s);
bloomInstances.setMatrixAt(i, _m);
bloomInstances.setColorAt(i, bloomColors[Math.floor(rand()*bloomColors.length)]);
}
bloomInstances.instanceColor.needsUpdate = true;
bloomInstances.castShadow = true;
scene.add(bloomInstances);
// 花冠上的紫色花篮 (装饰在上层栏杆)
const railFlowerGeo = new THREE.IcosahedronGeometry(0.05, 1);
const railFlowerMat = new THREE.MeshStandardMaterial({ color: 0x9a7ec4, roughness: 0.7 });
for(let x=-bL/2+0.2; x<=bL/2-0.2; x+=0.3) {
const f = new THREE.Mesh(railFlowerGeo, railFlowerMat);
f.position.set(x + busGroup.position.x, railY + 0.05, busGroup.position.z + bW/2);
f.scale.set(1, 1.5, 1);
scene.add(f);
}
/* ============================================================
7. 飘落花瓣
============================================================ */
const petalShape = new THREE.Shape();
petalShape.moveTo(0, -0.05);
petalShape.bezierCurveTo(0.03, -0.02, 0.04, 0.03, 0, 0.05);
petalShape.bezierCurveTo(-0.04, 0.03, -0.03, -0.02, 0, -0.05);
const airGeo = new THREE.ShapeGeometry(petalShape);
const airMat = new THREE.MeshStandardMaterial({ color: 0xc0a0e0, side: THREE.DoubleSide, roughness: 0.8 });
const airCount = 400;
const airMesh = new THREE.InstancedMesh(airGeo, airMat, airCount);
airMesh.frustumCulled = false;
scene.add(airMesh);
const airData = [];
for(let i=0; i<airCount; i++) {
airData.push({
x: (rand()-0.5)*4, y: rand()*3, z: (rand()-0.5)*3,
vy: -0.05 - rand()*0.1, phase: rand()*Math.PI*2, rot: rand()*Math.PI*2
});
}
function updateAir(dt) {
for(let i=0; i<airCount; i++) {
const p = airData[i];
p.phase += dt;
p.y += p.vy * dt;
p.x += Math.sin(p.phase) * 0.1 * dt;
p.rot += dt;
if(p.y < 0) { p.y = 3; p.x = (rand()-0.5)*4; p.z = (rand()-0.5)*3; }
_p.set(p.x, p.y, p.z);
_q.setFromEuler(new THREE.Euler(p.rot*1.2, p.rot*0.8, p.rot*1.5));
_s.set(1, 1, 1);
_m.compose(_p, _q, _s);
airMesh.setMatrixAt(i, _m);
}
airMesh.instanceMatrix.needsUpdate = true;
}
/* ============================================================
8. 环境补光 (为了让车体和树有立体感)
============================================================ */
// 模拟背景纱幕透出的微光
const envLight = new THREE.PointLight(0xaab0ff, 1.0, 5);
envLight.position.set(0, 2, -2);
scene.add(envLight);
/* ============================================================
9. 循环
============================================================ */
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
controls.update();
updateAir(dt);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>