v2 重做:C1连续速度剖面+弧长参数化+视线兴趣点阻尼;Organimo浓郁度美术(深靛蓝/彩色星云/雕塑月面/强轮廓光)
This commit is contained in:
Vendored
-4280
File diff suppressed because one or more lines are too long
Vendored
+4280
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||
// 指针阻尼(v2:0.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
@@ -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(常态克制,终章渐强) ----------
|
||||
// ---------- 后处理:Bloom(v2:更强发光分级) ----------
|
||||
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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user