v2 重做:C1连续速度剖面+弧长参数化+视线兴趣点阻尼;Organimo浓郁度美术(深靛蓝/彩色星云/雕塑月面/强轮廓光)

This commit is contained in:
Kimi
2026-08-06 04:54:13 +00:00
parent 1956a995dd
commit b9fdf6644d
6 changed files with 4647 additions and 4427 deletions
-4280
View File
File diff suppressed because one or more lines are too long
+4280
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -10,7 +10,7 @@
href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;600&family=JetBrains+Mono:wght@400&display=swap"
rel="stylesheet"
/>
<script type="module" crossorigin src="./assets/index-D34tEj7q.js"></script>
<script type="module" crossorigin src="./assets/index-DHgtFENf.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CLQdFjpd.css">
</head>
<body>
+33 -9
View File
@@ -3,15 +3,20 @@ import * as THREE from 'three'
// ---------- 色板(STYLE.md ----------
export const C = {
space: '#060B14',
moonLit: '#4A5A6E',
fracture: '#1E2836',
fog: '#16222E',
textMain: '#CFE8F0',
space: '#04060E', // 深靛蓝太空(v2:更黑更纯)
moonLit: '#5D6F8C', // 月表亮部(v2:提高明度与蓝倾向)
fracture: '#212B3D',
fog: '#0E1826', // 有色雾(v2:深青蓝,不再灰洗)
textMain: '#D8EFF7',
relic: '#7FE7FF',
dawn: '#E8C97A',
dawn: '#F0C06A', // 终章暖阳(v2:更金)
glitchRed: '#FF4A5E',
textDim: '#8FA3B5',
textDim: '#93A9BC',
// v2 浓郁度扩展色
nebulaTeal: '#1E8C96',
nebulaViolet: '#5B4396',
moonHi: '#7E8FAA', // 月表高地高光
gapGlow: '#4FD8F0', // 缺口辉光
} as const
// ---------- 世界布局 ----------
@@ -74,8 +79,8 @@ export const POSES: Pose[] = [
{ t: 0.0, pos: [-7, 4, 46], look: [-4, 2, 0] }, // W0 舷窗(月球居画面右下)
{ t: 0.22, pos: [-6.4, 3.7, 44.2], look: [-4, 2, 0] }, // W1 末(极慢推进)
{ t: 0.27, pos: [10, 7, 37], look: [13, -5, 24] }, // T1 弧线中点
{ t: 0.32, pos: [18, -6.4, 25], look: [27, -5.9, 25] }, // W2 贴地·山脊
{ t: 0.46, pos: [18.1, -6.5, 24.8], look: [27, -6.0, 24.9] }, // W2 末
{ t: 0.32, pos: [14.5, -6.2, 25], look: [28.4, 1.2, 25] }, // W2 山足仰壁(天/壁/地三层纵深)
{ t: 0.46, pos: [14.6, -6.3, 24.8], look: [28.4, 1.2, 25] }, // W2 末
{ t: 0.5, pos: [17, 0, 31], look: [8, -2, 8] }, // T2 拉远中点
{ t: 0.54, pos: [13.5, 3.2, 34.6], look: [4, -3, 5] }, // W3 观测(近 20%,转 30°)
{ t: 0.62, pos: [13.2, 3.3, 34.1], look: [4, -3, 5] }, // W3 末
@@ -90,6 +95,25 @@ export const POSES: Pose[] = [
{ t: 1.0, pos: [30, 10, 66], look: [-2, 2, 0] }, // W7 终章全景(缺口一侧)
]
// ---------- 视线兴趣点(运动系统 v2:视线只在这些点间长阻尼滑动) ----------
export const INTEREST: Record<string, THREE.Vector3> = {
W0: new THREE.Vector3(-4, 2, 0), // 月球全景(居画面右侧构图)
W1: new THREE.Vector3(-4, 2, 0),
W2: new THREE.Vector3(28.4, 1.2, 25), // 岩壁(仰视,留天空)
W3: new THREE.Vector3(4.5, -3, 6), // 断裂缺口
W4: new THREE.Vector3(34, 11, -14), // 遗迹构件
W5: new THREE.Vector3(-4, 2, 0),
W6: new THREE.Vector3(-26, 15, -18), // 同源碎片
W7: new THREE.Vector3(0, 1, 0), // 全景
}
export const SEG_INTEREST: Record<string, [string, string]> = {
W0: ['W0', 'W0'], W1: ['W1', 'W1'], T1: ['W1', 'W2'],
W2: ['W2', 'W2'], T2: ['W2', 'W3'], W3: ['W3', 'W3'],
T3: ['W3', 'W4'], W4: ['W4', 'W4'], T4: ['W4', 'W5'],
W5: ['W5', 'W5'], T5: ['W5', 'W6'], W6: ['W6', 'W6'],
T6: ['W6', 'W7'], W7: ['W7', 'W7'],
}
// ---------- 文案(STORYBOARD.md 占位文案,定案使用) ----------
export const COPY = {
W0: {
+2 -2
View File
@@ -137,8 +137,8 @@ class Rig {
if (s.reduced && s.content >= 1) sun = 1 // 降级:静态终章
s.sunrise = Math.min(1, sun)
// 指针阻尼(比滚动更慢 0.04"重量感"+ 入场收敛 0.5s
const pk = 1 - Math.pow(1 - 0.04, dt * 60)
// 指针阻尼(v20.03 更重,"惯性云台"感+ 入场收敛 0.5s
const pk = 1 - Math.pow(1 - 0.03, dt * 60)
s.pointerDamped.x += (s.pointer.x - s.pointerDamped.x) * pk
s.pointerDamped.y += (s.pointer.y - s.pointerDamped.y) * pk
s.pointerActive = Math.min(1, s.pointerActive + dt / 0.5)
+331 -135
View File
@@ -1,4 +1,8 @@
// world.ts — 单一 WebGL 场景:破碎月球 + 碎片群 + 样条摄影机 + 日出
// world.ts v2 — 单一 WebGL 场景:破碎月球 + 碎片群 + 丝滑摄影机 + 日出
// 运动 v2 三原则:
// 1) 弧长参数化:相机在路径上的空间速度由"距离/时间"决定,不再受样条参数密度影响
// 2) 全旅程 C1 连续速度剖面:段内 Hermite,段界速度取相邻段调和平均——全程无硬加速/急停
// 3) 视线无独立样条:只在少数"兴趣点"之间长阻尼滑动,行进中段轻微前引
import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
@@ -6,6 +10,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
import { rig } from './rig'
import {
C, MOON_R, GAP_DIR, FRAG_A, FRAG_B, FRAG_C, SUN_POS, POSES, SEGMENTS,
INTEREST, SEG_INTEREST,
} from '../config/stage'
// ---------- 工具:确定性伪随机 + 值噪声 ----------
@@ -34,6 +39,17 @@ function fbm(x: number, y: number, z: number, oct = 3) {
}
return a
}
// 脊状噪声:山脊/皱褶质感(雕塑级月面的关键)
function ridged(x: number, y: number, z: number, oct = 3) {
let a = 0, amp = 0.55, f = 1
for (let i = 0; i < oct; i++) {
const n = vnoise(x * f, y * f, z * f)
a += (1 - Math.abs(2 * n - 1)) * amp
amp *= 0.5
f *= 2.3
}
return a
}
function mulberry32(seed: number) {
return () => {
seed |= 0; seed = (seed + 0x6d2b79f5) | 0
@@ -46,16 +62,14 @@ const smoothstep = (a: number, b: number, x: number) => {
const t = Math.min(1, Math.max(0, (x - a) / (b - a)))
return t * t * (3 - 2 * t)
}
const sstep01 = (x: number) => x * x * (3 - 2 * x)
// ---------- 机位样条 ----------
// ---------- 相机路径:位置样条 + 弧长重参数化 + C1 速度剖面 ----------
const N = POSES.length
const posCurve = new THREE.CatmullRomCurve3(
POSES.map((p) => new THREE.Vector3(...p.pos)), false, 'centripetal',
)
const lookCurve = new THREE.CatmullRomCurve3(
POSES.map((p) => new THREE.Vector3(...p.look)), false, 'centripetal',
)
// content t → 样条参数 u(命中机位点)
// content t → 样条参数(命中机位点)
function tToU(t: number) {
if (t <= POSES[0].t) return 0
for (let i = 0; i < N - 1; i++) {
@@ -66,26 +80,76 @@ function tToU(t: number) {
}
return 1
}
// 全局进度 → u(按段落性格施加缓动)
function contentToU(t: number) {
for (const s of SEGMENTS) {
// 弧长查找表
const POS_SAMPLES = 1600
const cumLen = new Float32Array(POS_SAMPLES + 1)
{
let prev = posCurve.getPoint(0)
for (let i = 1; i <= POS_SAMPLES; i++) {
const pt = posCurve.getPoint(i / POS_SAMPLES)
cumLen[i] = cumLen[i - 1] + pt.distanceTo(prev)
prev = pt
}
}
const totalLen = cumLen[POS_SAMPLES]
function paramToArc(p: number) {
const f = Math.min(1, Math.max(0, p)) * POS_SAMPLES
const i = Math.min(POS_SAMPLES - 1, Math.floor(f))
const fr = f - i
return (cumLen[i] + (cumLen[i + 1] - cumLen[i]) * fr) / totalLen
}
function arcToParam(a: number) {
const target = Math.min(1, Math.max(0, a)) * totalLen
let lo = 0, hi = POS_SAMPLES
while (lo < hi) {
const mid = (lo + hi) >> 1
if (cumLen[mid] < target) lo = mid + 1; else hi = mid
}
const i = Math.max(1, lo)
const l0 = cumLen[i - 1], l1 = cumLen[i]
const fr = l1 > l0 ? (target - l0) / (l1 - l0) : 0
return (i - 1 + fr) / POS_SAMPLES
}
// 段落边界 → 弧长分数;边界速度 = 相邻段平均速度的调和平均(全程 C1 连续、永不反向)
const segArc = SEGMENTS.map((s) => ({ a0: paramToArc(tToU(s.t0)), a1: paramToArc(tToU(s.t1)) }))
const segSpeed = SEGMENTS.map((s, i) => (segArc[i].a1 - segArc[i].a0) / (s.t1 - s.t0 || 1))
const vBound = new Array<number>(SEGMENTS.length + 1).fill(0)
for (let i = 1; i < SEGMENTS.length; i++) {
const w0 = segSpeed[i - 1], w1 = segSpeed[i]
vBound[i] = w0 + w1 > 1e-8 ? (2 * w0 * w1) / (w0 + w1) : 0
}
// 全局进度 → 弧长分数(段内 Hermite,端点斜率 = 边界速度,单调性保护)
function contentToArc(t: number) {
if (t <= 0) return 0
if (t >= 1) return 1
for (let i = 0; i < SEGMENTS.length; i++) {
const s = SEGMENTS[i]
if (t >= s.t0 && t <= s.t1) {
const lt = (t - s.t0) / (s.t1 - s.t0 || 1)
const u0 = tToU(s.t0), u1 = tToU(s.t1)
return u0 + (u1 - u0) * s.ease(Math.min(1, Math.max(0, lt)))
const x = Math.min(1, Math.max(0, (t - s.t0) / (s.t1 - s.t0 || 1)))
const { a0, a1 } = segArc[i]
const da = a1 - a0
const w = segSpeed[i]
let m0 = Math.abs(w) > 1e-8 ? vBound[i] / w : 0
let m1 = Math.abs(w) > 1e-8 ? vBound[i + 1] / w : 0
m0 = Math.max(0, m0); m1 = Math.max(0, m1)
const sum = m0 + m1
if (sum > 2.9) { const k = 2.9 / sum; m0 *= k; m1 *= k }
const x2 = x * x, x3 = x2 * x
const H = (x3 - 2 * x2 + x) * m0 + (-2 * x3 + 3 * x2) + (x3 - x2) * m1
return a0 + da * H
}
}
return t >= 1 ? 1 : 0
return 1
}
// ---------- 材质辅助 ----------
function rockMaterial(color: string, vertexColors = false) {
return new THREE.MeshStandardMaterial({
color, roughness: 0.96, metalness: 0.04, flatShading: false, vertexColors,
color, roughness: 0.94, metalness: 0.05, flatShading: false, vertexColors,
})
}
// 太阳 / 光轴贴图(canvas 程序生成
// 径向光斑贴图(太阳 / 缺口辉光 / 星云
function radialTexture(inner: string, outer: string) {
const cv = document.createElement('canvas')
cv.width = cv.height = 256
@@ -96,21 +160,23 @@ function radialTexture(inner: string, outer: string) {
grad.addColorStop(1, 'rgba(0,0,0,0)')
g.fillStyle = grad
g.fillRect(0, 0, 256, 256)
const tx = new THREE.CanvasTexture(cv)
return tx
return new THREE.CanvasTexture(cv)
}
function hexA(hex: string, a: number) {
const c = new THREE.Color(hex)
return `rgba(${Math.round(c.r * 255)},${Math.round(c.g * 255)},${Math.round(c.b * 255)},${a})`
}
function shaftTexture() {
const cv = document.createElement('canvas')
cv.width = 256; cv.height = 64
const g = cv.getContext('2d')!
const grad = g.createLinearGradient(0, 0, 256, 0)
grad.addColorStop(0, 'rgba(232,201,122,0)')
grad.addColorStop(0.25, 'rgba(232,201,122,0.5)')
grad.addColorStop(0.75, 'rgba(232,201,122,0.35)')
grad.addColorStop(1, 'rgba(232,201,122,0)')
grad.addColorStop(0, 'rgba(240,192,106,0)')
grad.addColorStop(0.25, 'rgba(240,192,106,0.55)')
grad.addColorStop(0.75, 'rgba(240,192,106,0.38)')
grad.addColorStop(1, 'rgba(240,192,106,0)')
g.fillStyle = grad
g.fillRect(0, 0, 256, 64)
// 纵向羽化
const im = g.getImageData(0, 0, 256, 64)
for (let y = 0; y < 64; y++) {
const f = 1 - Math.abs(y - 32) / 32
@@ -128,7 +194,7 @@ export function initWorld(canvas: HTMLCanvasElement) {
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5))
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.05
renderer.toneMappingExposure = 1.15
const scene = new THREE.Scene()
scene.background = new THREE.Color(C.space)
@@ -138,105 +204,167 @@ export function initWorld(canvas: HTMLCanvasElement) {
45, window.innerWidth / window.innerHeight, 0.1, 600,
)
camera.position.set(-7, 4, 46)
camera.lookAt(3, -3, 0)
camera.lookAt(INTEREST.W0)
// ---------- 灯光(全片冷色纪律 ----------
scene.add(new THREE.AmbientLight(0x33465e, 1.1))
const keyLight = new THREE.DirectionalLight(0xcfe8f0, 1.6)
// ---------- 灯光 v2:强对比 + 青色轮廓光 + 紫色补光(Organimo 浓郁度 ----------
scene.add(new THREE.AmbientLight(0x2e4058, 0.85))
const keyLight = new THREE.DirectionalLight(0xdceef5, 1.9)
keyLight.position.set(30, 25, 40)
scene.add(keyLight)
const rimLight = new THREE.DirectionalLight(0x9fd8e8, 1.0)
// 强青色轮廓光(破碎月球的"刀锋边")
const rimLight = new THREE.DirectionalLight(0x7fe7ff, 1.8)
rimLight.position.set(-25, -8, -45)
scene.add(rimLight)
// 紫色低位补光(暗部不死黑,带色彩层次)
const fillLight = new THREE.DirectionalLight(0x5b4396, 0.7)
fillLight.position.set(25, -35, -15)
scene.add(fillLight)
// 终章暖阳(日出驱动)
const sunLight = new THREE.DirectionalLight(C.dawn, 0)
sunLight.position.copy(SUN_POS)
scene.add(sunLight)
// W2 山地局部冷光(让岩壁在雾中可读,不影响太空机位)
const groundLight = new THREE.PointLight(0xd8ecf4, 16, 40, 1.6)
groundLight.position.set(26, -5.5, 28)
const groundLight = new THREE.PointLight(0xd8ecf4, 10, 40, 1.6)
groundLight.position.set(24, -8, 31) // 低位掠射,扫出岩壁肌理
scene.add(groundLight)
// ---------- 星空 ----------
{
const n = 1400
// ---------- 星空 v2:双层 + 色彩变化 ----------
function starLayer(n: number, rMin: number, rMax: number, size: number, opacity: number, seed: number) {
const pos = new Float32Array(n * 3)
const rnd = mulberry32(7)
const col = new Float32Array(n * 3)
const rnd = mulberry32(seed)
const cWhite = new THREE.Color('#EAF6FB')
const cCyan = new THREE.Color('#9FD8E8')
const cWarm = new THREE.Color('#F0DFC0')
for (let i = 0; i < n; i++) {
const r = 220 + rnd() * 160
const r = rMin + rnd() * (rMax - rMin)
const th = rnd() * Math.PI * 2, ph = Math.acos(2 * rnd() - 1)
pos[i * 3] = r * Math.sin(ph) * Math.cos(th)
pos[i * 3 + 1] = r * Math.cos(ph) * 0.6
pos[i * 3 + 2] = r * Math.sin(ph) * Math.sin(th)
const pick = rnd()
const cc = pick < 0.68 ? cWhite : pick < 0.88 ? cCyan : cWarm
const dim = 0.45 + rnd() * 0.55
col[i * 3] = cc.r * dim; col[i * 3 + 1] = cc.g * dim; col[i * 3 + 2] = cc.b * dim
}
const g = new THREE.BufferGeometry()
g.setAttribute('position', new THREE.BufferAttribute(pos, 3))
g.setAttribute('color', new THREE.BufferAttribute(col, 3))
const m = new THREE.PointsMaterial({
color: 0xcfe8f0, size: 1.1, sizeAttenuation: false,
transparent: true, opacity: 0.65, fog: false,
size, sizeAttenuation: false, vertexColors: true,
transparent: true, opacity, fog: false, depthWrite: false,
})
scene.add(new THREE.Points(g, m))
}
// 微弱星云
starLayer(1700, 240, 420, 1.0, 0.7, 7) // 远层密而暗
starLayer(260, 170, 260, 1.9, 0.95, 8) // 近层疏而亮
// ---------- 星云 v2:青 / 紫 / 蓝三色交织( additive ----------
{
const tx = radialTexture('rgba(70,110,140,0.35)', 'rgba(40,70,100,0.12)')
const texTeal = radialTexture(hexA(C.nebulaTeal, 0.5), hexA(C.nebulaTeal, 0.14))
const texViolet = radialTexture(hexA(C.nebulaViolet, 0.5), hexA(C.nebulaViolet, 0.13))
const texBlue = radialTexture('rgba(58,92,150,0.45)', 'rgba(58,92,150,0.12)')
const texs = [texTeal, texViolet, texBlue]
const rnd = mulberry32(11)
for (let i = 0; i < 4; i++) {
for (let i = 0; i < 7; i++) {
const sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: tx, transparent: true, opacity: 0.35, depthWrite: false, fog: false,
map: texs[i % 3], transparent: true, opacity: 0.3 + rnd() * 0.14,
depthWrite: false, fog: false, blending: THREE.AdditiveBlending,
}))
sp.position.set((rnd() - 0.5) * 300, (rnd() - 0.5) * 160, -180 - rnd() * 120)
sp.scale.setScalar(180 + rnd() * 160)
const th = rnd() * Math.PI * 2
sp.position.set(
Math.cos(th) * (140 + rnd() * 160),
(rnd() - 0.5) * 180,
Math.sin(th) * (140 + rnd() * 160) - 60,
)
sp.scale.setScalar(150 + rnd() * 190)
scene.add(sp)
}
}
// ---------- 月球(破碎——全片最重要的资产 ----------
// ---------- 月球 v2(雕塑级:大陆起伏 + 脊状山脊 + 环形山 + 发光缺口 ----------
const moon = new THREE.Group()
{
const geo = new THREE.IcosahedronGeometry(MOON_R, 32)
const geo = new THREE.IcosahedronGeometry(MOON_R, 48)
const p = geo.attributes.position
const colors = new Float32Array(p.count * 3)
const lit = new THREE.Color(C.moonLit)
const hi = new THREE.Color(C.moonHi)
const maria = new THREE.Color('#39445A')
const frac = new THREE.Color(C.fracture)
const fracGlow = new THREE.Color(C.gapGlow)
const tmp = new THREE.Vector3()
const cosGap = Math.cos(0.72) // 缺口半角 ~41°
// 环形山(确定性分布,避开缺口区)
const craters: { dir: THREE.Vector3; r: number; depth: number }[] = []
{
const rnd = mulberry32(97)
for (let i = 0; i < 18; i++) {
const d = new THREE.Vector3(rnd() * 2 - 1, rnd() * 2 - 1, rnd() * 2 - 1)
if (d.lengthSq() < 1e-4) continue
d.normalize()
if (d.dot(GAP_DIR) > 0.1) continue // 缺口附近不放
craters.push({ dir: d, r: 0.10 + rnd() * 0.22, depth: 0.22 + rnd() * 0.5 })
}
}
for (let i = 0; i < p.count; i++) {
tmp.fromBufferAttribute(p, i)
const dir = tmp.clone().normalize()
// 基础岩石起伏
let r = MOON_R + (fbm(dir.x * 2.3 + 9, dir.y * 2.3, dir.z * 2.3) - 0.5) * 0.9
// 缺口:一侧整体缺失,顶点位向内扣
// 三层地形:大陆 / 脊状山脊 / 细颗粒
const continent = fbm(dir.x * 1.6 + 9, dir.y * 1.6, dir.z * 1.6, 4)
const ridge = ridged(dir.x * 3.4 + 21, dir.y * 3.4, dir.z * 3.4, 4)
let r = MOON_R
+ (continent - 0.5) * 1.5
+ (ridge - 0.55) * 0.8
+ (fbm(dir.x * 9 + 33, dir.y * 9, dir.z * 9) - 0.5) * 0.22
// 环形山:碗形凹陷 + 环形山脊
for (const c of craters) {
const dd = dir.distanceTo(c.dir)
if (dd < c.r * 1.6) {
const bowl = dd < c.r ? (Math.cos((Math.PI * dd) / c.r) * 0.5 + 0.5) : 0
const rimD = (dd - c.r) / (c.r * 0.3)
const rim = Math.exp(-rimD * rimD) * 0.4
r += -c.depth * bowl + c.depth * rim
}
}
// 缺口:一侧整体缺失,锯齿状内扣
const d = dir.dot(GAP_DIR)
let depth = 0
if (d > cosGap) {
depth = smoothstep(cosGap, 1, d)
const jag = (fbm(dir.x * 6 + 40, dir.y * 6, dir.z * 6) - 0.5) * 2.2
r -= depth * (7.2 + jag)
const jag = (ridged(dir.x * 6 + 40, dir.y * 6, dir.z * 6) - 0.5) * 2.6
r -= depth * (7.4 + jag)
}
tmp.copy(dir).multiplyScalar(r)
p.setXYZ(i, tmp.x, tmp.y, tmp.z)
// 顶点色:断面深 + 月表灰蓝
const cc = depth > 0.02
? frac.clone().lerp(lit, Math.max(0, 0.25 - depth * 0.25))
: lit.clone().multiplyScalar(0.75 + fbm(dir.x * 4, dir.y * 4 + 3, dir.z * 4) * 0.5)
// 顶点色:高地亮 / 月海暗 / 断面深 + 缺口内缘微青
let cc: THREE.Color
if (depth > 0.02) {
cc = frac.clone().lerp(lit, Math.max(0, 0.22 - depth * 0.22))
cc.lerp(fracGlow, Math.max(0, depth - 0.55) * 0.35) // 缺口深处透出青光
} else {
cc = lit.clone()
cc.lerp(maria, smoothstep(0.5, 0.28, continent) * 0.7) // 低地是月海
cc.lerp(hi, smoothstep(0.55, 0.8, ridge) * 0.55) // 山脊提亮
cc.multiplyScalar(0.82 + fbm(dir.x * 4, dir.y * 4 + 3, dir.z * 4) * 0.36)
}
colors[i * 3] = cc.r; colors[i * 3 + 1] = cc.g; colors[i * 3 + 2] = cc.b
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3))
geo.computeVertexNormals()
moon.add(new THREE.Mesh(geo, rockMaterial('#ffffff', true)))
}
// 断裂环(锯齿状碎边轮廓)
// 断裂环(锯齿状碎边轮廓,微微透光
{
const ringR = Math.sin(0.72) * MOON_R * 0.96
const center = GAP_DIR.clone().multiplyScalar(MOON_R - 5.6)
const u = new THREE.Vector3(0, 1, 0).cross(GAP_DIR).normalize()
const v = GAP_DIR.clone().cross(u).normalize()
const n = 46
const inst = new THREE.InstancedMesh(
new THREE.TetrahedronGeometry(0.85),
rockMaterial(C.fracture), n,
)
const n = 52
const mat = rockMaterial(C.fracture)
mat.emissive = new THREE.Color(C.gapGlow)
mat.emissiveIntensity = 0.08
const inst = new THREE.InstancedMesh(new THREE.TetrahedronGeometry(0.85), mat, n)
const rnd = mulberry32(23)
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e = new THREE.Euler()
for (let i = 0; i < n; i++) {
@@ -253,10 +381,21 @@ export function initWorld(canvas: HTMLCanvasElement) {
}
moon.add(inst)
}
// 缺口内部尘埃(叙事焦点的生命力
// 缺口辉光(叙事焦点的"呼吸灯"
{
const glow = new THREE.Sprite(new THREE.SpriteMaterial({
map: radialTexture(hexA(C.gapGlow, 0.85), hexA(C.gapGlow, 0.22)),
transparent: true, opacity: 0.3, depthWrite: false,
blending: THREE.AdditiveBlending,
}))
glow.position.copy(GAP_DIR).multiplyScalar(MOON_R - 3.2)
glow.scale.setScalar(15)
moon.add(glow)
}
// 缺口内部尘埃
let gapDust: THREE.Points
{
const n = 180
const n = 240
const pos = new Float32Array(n * 3)
const rnd = mulberry32(31)
const u = new THREE.Vector3(0, 1, 0).cross(GAP_DIR).normalize()
@@ -272,19 +411,19 @@ export function initWorld(canvas: HTMLCanvasElement) {
const g = new THREE.BufferGeometry()
g.setAttribute('position', new THREE.BufferAttribute(pos, 3))
gapDust = new THREE.Points(g, new THREE.PointsMaterial({
color: 0x7fe7ff, size: 0.12, transparent: true, opacity: 0.5,
color: C.gapGlow, size: 0.14, transparent: true, opacity: 0.65,
blending: THREE.AdditiveBlending, depthWrite: false,
}))
moon.add(gapDust)
}
scene.add(moon)
// ---------- 远景碎片群(InstancedMesh + 着色器失重悬浮) ----------
// ---------- 远景碎片群(InstancedMesh + 着色器失重悬浮 + 逐实例色变 ----------
const debrisTime = { value: 0 }
{
const n = 56
const n = 64
const geo = new THREE.IcosahedronGeometry(1, 1)
const mat = rockMaterial(C.moonLit)
const mat = rockMaterial('#ffffff')
mat.flatShading = true
mat.onBeforeCompile = (sh) => {
sh.uniforms.uTime = debrisTime as never
@@ -325,16 +464,31 @@ export function initWorld(canvas: HTMLCanvasElement) {
const axis = new Float32Array(n * 3), rotP = new Float32Array(n)
const bobA = new Float32Array(n), bobP = new Float32Array(n)
const phase = new Float32Array(n), drift = new Float32Array(n * 3)
const cLit = new THREE.Color(C.moonLit)
const cFrac = new THREE.Color(C.fracture)
const cRelic = new THREE.Color(C.gapGlow)
for (let i = 0; i < n; i++) {
const dist = 14 + Math.pow(rnd(), 1.4) * 70
const spread = 3 + dist * 0.35
const pt = GAP_DIR.clone().multiplyScalar(dist)
let pt = GAP_DIR.clone().multiplyScalar(dist)
.addScaledVector(u, (rnd() - 0.5) * spread)
.addScaledVector(v, (rnd() - 0.5) * spread)
// 避开 T1 着陆走廊 / W2 机位(防近景碎片糊镜头)
if (pt.distanceTo(new THREE.Vector3(16, -4, 25)) < 10) {
pt = pt.add(new THREE.Vector3(0, 12, -6))
}
const s = 0.35 + rnd() * rnd() * 2.0
q.identity()
m4.compose(pt, q, new THREE.Vector3(s, s * (0.7 + rnd() * 0.6), s))
inst.setMatrixAt(i, m4)
// 逐实例色:多数月表灰蓝,少数断面深色,个别透青光
const pick = rnd()
const cc = pick < 0.62
? cLit.clone().multiplyScalar(0.7 + rnd() * 0.5)
: pick < 0.9
? cFrac.clone().multiplyScalar(0.9 + rnd() * 0.4)
: cFrac.clone().lerp(cRelic, 0.45)
inst.setColorAt(i, cc)
const ax = new THREE.Vector3(rnd() - 0.5, rnd() - 0.5, rnd() - 0.5).normalize()
axis[i * 3] = ax.x; axis[i * 3 + 1] = ax.y; axis[i * 3 + 2] = ax.z
rotP[i] = 20 + rnd() * 20
@@ -344,6 +498,7 @@ export function initWorld(canvas: HTMLCanvasElement) {
const dv = GAP_DIR.clone().multiplyScalar(0.5 + rnd() * 1.5)
drift[i * 3] = dv.x; drift[i * 3 + 1] = dv.y; drift[i * 3 + 2] = dv.z
}
if (inst.instanceColor) inst.instanceColor.needsUpdate = true
geo.setAttribute('aAxis', new THREE.InstancedBufferAttribute(axis, 3))
geo.setAttribute('aRotP', new THREE.InstancedBufferAttribute(rotP, 1))
geo.setAttribute('aBobA', new THREE.InstancedBufferAttribute(bobA, 1))
@@ -362,7 +517,9 @@ export function initWorld(canvas: HTMLCanvasElement) {
for (let i = 0; i < p.count; i++) {
tmp.fromBufferAttribute(p, i)
const dir = tmp.clone().normalize()
const r = 4.5 + (fbm(dir.x * 3 + 60, dir.y * 3, dir.z * 3) - 0.5) * 1.6
const r = 4.5
+ (fbm(dir.x * 3 + 60, dir.y * 3, dir.z * 3) - 0.5) * 1.6
+ (ridged(dir.x * 5 + 61, dir.y * 5, dir.z * 5) - 0.55) * 0.7
p.setXYZ(i, dir.x * r, dir.y * r, dir.z * r)
}
geo.computeVertexNormals()
@@ -379,45 +536,45 @@ export function initWorld(canvas: HTMLCanvasElement) {
for (let i = 0; i < p.count; i++) {
const x = p.getX(i), z = p.getZ(i)
const h = (fbm(x * 0.12 + 80, 0, z * 0.12, 4) - 0.5) * 3.2
+ (ridged(x * 0.3 + 100, 0, z * 0.3, 3) - 0.55) * 1.4
+ (fbm(x * 0.8 + 120, 0, z * 0.8, 3) - 0.5) * 0.7
p.setY(i, h)
}
geo.computeVertexNormals()
const ground = new THREE.Mesh(geo, rockMaterial('#3d4c5e'))
ground.position.set(30, -8.3, 25)
ground.visible = false // 尺度戏法:仅 W2 附近可见(远看隐藏,防穿帮)
scene.add(ground)
groundMesh = ground
groundMesh = new THREE.Mesh(geo, rockMaterial('#46566C'))
groundMesh.position.set(30, -8.3, 25)
groundMesh.visible = false // 尺度戏法:仅 W2 附近可见
scene.add(groundMesh)
}
// 岩壁(古文锚点所在,预留较平整区域朝向镜头)
let cliffAnchor: THREE.Object3D
{
const geo = new THREE.PlaneGeometry(40, 20, 90, 45)
const geo = new THREE.PlaneGeometry(34, 13, 90, 40)
const p = geo.attributes.position
for (let i = 0; i < p.count; i++) {
const x = p.getX(i), y = p.getY(i)
// 中央区(古文区)平整化
const flat = smoothstep(4, 9, Math.abs(x)) * smoothstep(2.5, 6, Math.abs(y + 1))
const h = (fbm(x * 0.3 + 200, y * 0.3, 0, 4) - 0.5) * 2.6 * (0.3 + 0.7 * flat)
const flat = smoothstep(3.5, 8, Math.abs(x)) * smoothstep(1.8, 4.5, Math.abs(y))
const h = (fbm(x * 0.3 + 200, y * 0.3, 0, 4) - 0.5) * 2.9 * (0.3 + 0.7 * flat)
+ (ridged(x * 0.5 + 210, y * 0.5, 0, 3) - 0.55) * 1.2 * flat
+ (ridged(x * 1.6 + 230, y * 1.6, 0, 2) - 0.55) * 0.65
+ (fbm(x * 3.2 + 250, y * 3.2, 0, 2) - 0.5) * 0.3
p.setZ(i, h)
}
geo.computeVertexNormals()
const cliff = new THREE.Mesh(geo, rockMaterial('#4e6178'))
cliff.rotation.y = -Math.PI / 2 // 法线朝 -X,面向 W2 机位
cliff.position.set(28.6, -1.8, 25)
cliff.visible = false
cliffMesh = cliff
scene.add(cliff)
cliffMesh = cliff
cliffMesh = new THREE.Mesh(geo, rockMaterial('#4E6178'))
cliffMesh.rotation.y = -Math.PI / 2 // 法线朝 -X,面向 W2 机位
cliffMesh.position.set(28.6, 0.8, 25)
cliffMesh.visible = false
scene.add(cliffMesh)
cliffAnchor = new THREE.Object3D()
cliffAnchor.position.set(28.2, -4.1, 25)
cliffAnchor.position.set(28.2, 0.2, 25)
scene.add(cliffAnchor)
}
// 近景砾石(W2 接近时淡入)
{
const n = rig.state.isMobile ? 90 : 200
gravel = new THREE.InstancedMesh(
new THREE.DodecahedronGeometry(0.14), rockMaterial('#4a5a6e'), n,
new THREE.DodecahedronGeometry(0.14), rockMaterial(C.moonLit), n,
)
const rnd = mulberry32(71)
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e = new THREE.Euler()
@@ -445,7 +602,9 @@ export function initWorld(canvas: HTMLCanvasElement) {
for (let i = 0; i < p.count; i++) {
tmp.fromBufferAttribute(p, i)
const dir = tmp.clone().normalize()
const r = 3.5 + (fbm(dir.x * 3 + 90, dir.y * 3, dir.z * 3) - 0.5) * 1.2
const r = 3.5
+ (fbm(dir.x * 3 + 90, dir.y * 3, dir.z * 3) - 0.5) * 1.2
+ (ridged(dir.x * 5 + 91, dir.y * 5, dir.z * 5) - 0.55) * 0.5
p.setXYZ(i, dir.x * r, dir.y * r, dir.z * r)
}
geo.computeVertexNormals()
@@ -454,29 +613,32 @@ export function initWorld(canvas: HTMLCanvasElement) {
rockMaterial(C.moonLit),
)
fragB.add(rock)
// 半埋环形构件:硬边、金属、发光纹路
relicMat = new THREE.MeshStandardMaterial({
color: 0x2b3644, roughness: 0.3, metalness: 0.9,
color: 0x2b3644, roughness: 0.28, metalness: 0.92,
emissive: new THREE.Color(C.relic), emissiveIntensity: 0,
})
const torus = new THREE.Mesh(new THREE.TorusGeometry(3.0, 0.28, 10, 56, Math.PI * 1.4), relicMat)
torus.rotation.set(1.25, 0.35, 0.5)
torus.position.set(0.4, 1.1, 0.2)
// 遗迹构件朝向 W4 机位来向半埋在岩面(v2:确保任何时刻可读)
const dirToCamB = new THREE.Vector3(24.5 - FRAG_B.x, 14.5 - FRAG_B.y, -2.5 - FRAG_B.z).normalize()
const qFace = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), dirToCamB)
const torus = new THREE.Mesh(new THREE.TorusGeometry(1.7, 0.2, 12, 56, Math.PI * 1.6), relicMat)
torus.quaternion.copy(qFace)
torus.rotateZ(Math.PI * 0.9) // 开口朝下埋入岩中
torus.position.copy(dirToCamB).multiplyScalar(3.3)
fragB.add(torus)
const poly = new THREE.Mesh(new THREE.IcosahedronGeometry(1.05, 0), relicMat)
poly.position.set(0.4, 1.3, 0.2)
const poly = new THREE.Mesh(new THREE.IcosahedronGeometry(0.78, 0), relicMat)
poly.position.copy(dirToCamB).multiplyScalar(4.2)
fragB.add(poly)
relicLines = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.6, 0)),
new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.22, 0)),
new THREE.LineBasicMaterial({ color: C.relic, transparent: true, opacity: 0 }),
)
relicLines.position.set(0.4, 1.3, 0.2)
relicLines.position.copy(dirToCamB).multiplyScalar(4.2)
fragB.add(relicLines)
fragB.position.copy(FRAG_B)
scene.add(fragB)
}
const anchorB = new THREE.Object3D()
anchorB.position.copy(FRAG_B).add(new THREE.Vector3(0.4, 2.3, 0.2))
anchorB.position.copy(FRAG_B).add(new THREE.Vector3(-2.4, 3.4, 3.2))
scene.add(anchorB)
// ---------- 碎片 C · 同源(岩壁刻痕与发光构件各半) ----------
@@ -489,7 +651,6 @@ export function initWorld(canvas: HTMLCanvasElement) {
for (let i = 0; i < p.count; i++) {
tmp.fromBufferAttribute(p, i)
const dir = tmp.clone().normalize()
// 只有 x<0 半侧做有机起伏;x>0 半侧保持硬边(机械感)
const organic = dir.x < 0 ? (fbm(dir.x * 3 + 140, dir.y * 3, dir.z * 3) - 0.5) * 1.3 : 0.1
const r = 3.4 + organic - (dir.x < 0 ? 0 : 0.35)
p.setXYZ(i, dir.x * r, dir.y * r, dir.z * r)
@@ -497,38 +658,42 @@ export function initWorld(canvas: HTMLCanvasElement) {
geo.computeVertexNormals()
{ const m = rockMaterial(C.moonLit); m.flatShading = true; fragC.add(new THREE.Mesh(geo, m)) }
convMat = new THREE.MeshStandardMaterial({
color: 0x2b3644, roughness: 0.3, metalness: 0.9,
color: 0x2b3644, roughness: 0.28, metalness: 0.92,
emissive: new THREE.Color(C.relic), emissiveIntensity: 0.05,
})
// 构件朝向 W6 机位来向(v2:有界摆动下永远可读)
const dirToCamC = new THREE.Vector3(-22.5 - FRAG_C.x, 15.5 - FRAG_C.y, -4 - FRAG_C.z).normalize()
const qFaceC = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), dirToCamC)
const ring = new THREE.Mesh(new THREE.TorusGeometry(1.9, 0.16, 8, 40, Math.PI), convMat)
ring.position.set(3.0, 0.5, 0)
ring.rotation.set(0.2, Math.PI / 2, 0.75)
ring.quaternion.copy(qFaceC)
ring.rotateZ(Math.PI * 0.15)
ring.position.copy(dirToCamC).multiplyScalar(2.6)
fragC.add(ring)
const shard = new THREE.Mesh(new THREE.OctahedronGeometry(0.8, 0), convMat)
shard.position.set(3.1, 1.6, 0.5)
shard.position.copy(dirToCamC).multiplyScalar(3.4).add(new THREE.Vector3(0, 0.5, 0))
fragC.add(shard)
fragC.position.copy(FRAG_C)
scene.add(fragC)
}
const anchorC = new THREE.Object3D()
anchorC.position.copy(FRAG_C).add(new THREE.Vector3(0.5, 2.2, 0.5))
anchorC.position.copy(FRAG_C).add(new THREE.Vector3(1.0, 1.4, 4.2))
scene.add(anchorC)
// ---------- 太阳与光轴(终章) ----------
const sunSprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: radialTexture('rgba(255,244,214,1)', 'rgba(232,201,122,0.55)'),
map: radialTexture('rgba(255,246,220,1)', hexA(C.dawn, 0.55)),
transparent: true, opacity: 0, depthWrite: false, fog: false,
blending: THREE.AdditiveBlending,
}))
sunSprite.scale.setScalar(34)
scene.add(sunSprite)
const sunStart = SUN_POS.clone().add(new THREE.Vector3(-26, 18, 0)) // 画面左上外缘
const sunStart = SUN_POS.clone().add(new THREE.Vector3(-26, 18, 0))
const shaftCount = rig.state.isMobile ? 3 : 5
const shafts: THREE.Mesh[] = []
{
const tx = shaftTexture()
const d = SUN_POS.clone().negate().normalize() // 光轴行进方向
const d = SUN_POS.clone().negate().normalize()
for (let i = 0; i < shaftCount; i++) {
const m = new THREE.Mesh(
new THREE.PlaneGeometry(180, 5.5),
@@ -549,17 +714,21 @@ export function initWorld(canvas: HTMLCanvasElement) {
}
}
// ---------- 后处理:Bloom常态克制,终章渐强 ----------
// ---------- 后处理:Bloomv2:更强发光分级 ----------
const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
const bloom = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight), 0.32, 0.55, 0.85,
new THREE.Vector2(window.innerWidth, window.innerHeight), 0.45, 0.7, 0.78,
)
composer.addPass(bloom)
// ---------- 每帧更新 ----------
const tmpV = new THREE.Vector3()
const tmpLook = new THREE.Vector3()
const tmpTan = new THREE.Vector3()
const lookTarget = new THREE.Vector3()
const lookLead = new THREE.Vector3()
const lookSmooth = INTEREST.W0.clone()
const camRight = new THREE.Vector3(), camUp = new THREE.Vector3()
const qYaw = new THREE.Quaternion(), qPitch = new THREE.Quaternion()
const AXIS_Y = new THREE.Vector3(0, 1, 0), AXIS_X = new THREE.Vector3(1, 0, 0)
const clock = new THREE.Clock()
@@ -578,21 +747,22 @@ export function initWorld(canvas: HTMLCanvasElement) {
rig.update(dt, time)
debrisTime.value = time
// --- 1. 滚动层:样条机位 ---
const u = contentToU(s.content)
posCurve.getPoint(u, tmpV)
lookCurve.getPoint(u, tmpLook)
// --- 1. 滚动层:弧长参数化机位(C1 连续速度剖面,全程无急停) ---
const a = contentToArc(s.content)
const p = arcToParam(a)
posCurve.getPoint(p, tmpV)
posCurve.getTangent(p, tmpTan)
const inDwell = s.seg.type === 'dwell'
// idle 呼吸浮动(驻留段,±0.05,周期 ~5.5s)
if (!s.reduced) {
const idleAmp = inDwell ? 0.055 : 0.02
// idle 呼吸浮动(驻留段)
const idleAmp = inDwell ? 0.045 : 0.015
tmpV.y += Math.sin(time * (Math.PI * 2 / 5.5)) * idleAmp
tmpV.x += Math.sin(time * (Math.PI * 2 / 7.3) + 1.7) * idleAmp * 0.6
// 蛇形接近(T3/T5
if (s.seg.sway) {
const sway = Math.sin(s.segT * Math.PI * 2.2) * 0.7
const side = new THREE.Vector3().subVectors(tmpV, tmpLook)
const side = new THREE.Vector3().subVectors(tmpV, lookSmooth)
.cross(AXIS_Y).normalize()
tmpV.addScaledVector(side, sway)
}
@@ -605,41 +775,61 @@ export function initWorld(canvas: HTMLCanvasElement) {
}
}
camera.position.copy(tmpV)
camera.lookAt(tmpLook)
// --- 2. 时间层:天体失重悬浮 ---
// --- 视线 v2:兴趣点长阻尼滑动 + 行进中段切线前引 ---
{
const pair = SEG_INTEREST[s.seg.id] || ['W7', 'W7']
lookTarget.lerpVectors(INTEREST[pair[0]], INTEREST[pair[1]], sstep01(s.segT))
if (s.seg.type === 'travel') {
// 中段 22% 权重瞥向前进方向(速度感),两端归零
const lead = Math.sin(Math.PI * s.segT) * 0.22
lookLead.copy(tmpV).addScaledVector(tmpTan, 9)
lookTarget.lerp(lookLead, lead)
}
const lk = s.reduced ? 1 : 1 - Math.pow(1 - 0.055, dt * 60)
lookSmooth.lerp(lookTarget, lk)
camera.lookAt(lookSmooth)
}
// --- 2. 时间层:天体失重悬浮(v2:全部有界摆动,舞台朝向永不漂移) ---
if (!s.reduced) {
moon.rotation.y += dt * (Math.PI * 2 / 240)
moon.rotation.y = Math.sin(time * (Math.PI * 2 / 240)) * 0.5
moon.rotation.z = 0.04
gapDust.rotation.z += dt * 0.05
// 叙事碎片:多轴翻滚(主副周期不取整数比)
const tA = time * (Math.PI * 2 / 58)
fragA.rotation.set(Math.sin(tA * 0.45) * 0.1, tA * 0.12, Math.sin(tA / 1.9) * 0.08)
fragA.rotation.set(Math.sin(tA * 0.45) * 0.1, Math.sin(tA * 0.11) * 0.12, Math.sin(tA / 1.9) * 0.08)
fragA.position.y = FRAG_A.y + Math.sin(time * (Math.PI * 2 / 47) + 1.2) * 0.09
const tB = time * (Math.PI * 2 / 64)
fragB.rotation.set(Math.sin(tB * 0.4) * 0.12, tB * 0.1, Math.sin(tB / 2.1) * 0.1)
fragB.rotation.set(Math.sin(tB * 0.4) * 0.1, Math.sin(tB * 0.13) * 0.12, Math.sin(tB / 2.1) * 0.09)
fragB.position.y = FRAG_B.y + Math.sin(time * (Math.PI * 2 / 41) + 3.1) * 0.08
const tC = time * (Math.PI * 2 / 52)
fragC.rotation.set(Math.sin(tC * 0.5) * 0.11, -tC * 0.11, Math.sin(tC / 1.7) * 0.09)
fragC.rotation.set(Math.sin(tC * 0.5) * 0.1, Math.sin(tC * 0.12) * 0.11, Math.sin(tC / 1.7) * 0.08)
fragC.position.y = FRAG_C.y + Math.sin(time * (Math.PI * 2 / 44) + 5.0) * 0.08
}
// --- 3. 指针层:相机姿态 × 指针偏移(以旋转为主 ---
// --- 3. 指针层 v2:姿态微转 + 位置视差(近景位移大 → 纵深,kpr 手感 ---
if (!s.reduced && !s.isMobile) {
let amp = inDwell ? 1 : 0.4
let amp = inDwell ? 1 : 0.35
if (s.seg.id === 'W1' && s.glitchActive) amp = 0.15
if (s.seg.id === 'W7') amp = 0.6
if (s.seg.id === 'W7') amp = 0.55
amp *= s.pointerActive
const yaw = -s.pointerDamped.x * THREE.MathUtils.degToRad(2.5) * amp
const pitch = -s.pointerDamped.y * THREE.MathUtils.degToRad(1.5) * amp
// 姿态:轻(v2 减弱 40%
const yaw = -s.pointerDamped.x * THREE.MathUtils.degToRad(1.5) * amp
const pitch = -s.pointerDamped.y * THREE.MathUtils.degToRad(0.9) * amp
qYaw.setFromAxisAngle(AXIS_Y, yaw)
qPitch.setFromAxisAngle(AXIS_X, pitch)
camera.quaternion.multiply(qYaw).multiply(qPitch)
// 位置:沿相机右/上轴平移(前景与背景产生位移差)
const pAmp = 0.55 * amp
camRight.setFromMatrixColumn(camera.matrix, 0)
camUp.setFromMatrixColumn(camera.matrix, 1)
camera.position.addScaledVector(camRight, s.pointerDamped.x * pAmp)
camera.position.addScaledVector(camUp, -s.pointerDamped.y * pAmp)
}
// --- 雾密度分段驱动 ---
const segId = s.seg.id
fogTarget = segId === 'W2' ? 0.02
fogTarget = segId === 'W2' ? 0.017
: segId === 'W4' || segId === 'W6' ? 0.011
: segId === 'W7' ? 0.006 : 0.008
const fog = scene.fog as THREE.FogExp2
@@ -653,8 +843,8 @@ export function initWorld(canvas: HTMLCanvasElement) {
// --- W4 遗迹发光(滚动驱动 0→1) ---
const w4Glow = segId === 'W4' ? smoothstep(0.08, 0.55, s.segT) : segId === 'T4' || segId === 'W5' ? 1 : 0
relicMat.emissiveIntensity = w4Glow * 0.85
;(relicLines.material as THREE.LineBasicMaterial).opacity = w4Glow * 0.9
relicMat.emissiveIntensity = w4Glow * 0.3
;(relicLines.material as THREE.LineBasicMaterial).opacity = w4Glow * 0.45
convMat.emissiveIntensity = 0.05 + (segId === 'W6' ? smoothstep(0.1, 0.6, s.segT) * 0.8 : 0)
// --- 终章日出 ---
@@ -663,13 +853,12 @@ export function initWorld(canvas: HTMLCanvasElement) {
sunSprite.material.opacity = Math.min(1, sun * 1.6)
sunLight.intensity = sun * 2.3
for (let i = 0; i < shafts.length; i++) {
// 逐束点亮:第 1 束 30%,之后每 +15% 一束
const o = smoothstep(0.3 + i * 0.15, 0.3 + i * 0.15 + 0.12, sun)
;(shafts[i].material as THREE.MeshBasicMaterial).opacity = o * 0.4
}
bloom.strength = 0.32 + sun * 0.55
bloom.threshold = 0.85 - sun * 0.16
renderer.toneMappingExposure = 1.05 + sun * 0.12
bloom.strength = 0.45 + sun * 0.55
bloom.threshold = 0.78 - sun * 0.14
renderer.toneMappingExposure = 1.15 + sun * 0.12
// --- 3D 锚点 → 屏幕投影(DOM 文字层跟随) ---
for (const [key, obj] of Object.entries(anchorsMap)) {
@@ -683,6 +872,13 @@ export function initWorld(canvas: HTMLCanvasElement) {
}
composer.render()
// 调试探针(验收截图用)
;(window as unknown as { __dbg: unknown }).__dbg = {
content: s.content.toFixed(3), seg: s.seg.id, segT: s.segT.toFixed(2),
cam: camera.position.toArray().map((v) => +v.toFixed(1)),
look: lookSmooth.toArray().map((v) => +v.toFixed(1)),
fps: (1 / Math.max(dt, 1e-4)).toFixed(1),
}
}
frame()