Skip to content

Commit 0e9dc4a

Browse files
committed
perf(3d): migrate 6 particle systems to GPU vertex shader and consolidate useFrame
Layer 2 - GPU particle migration: - Create gpuParticleShaders.ts: reusable onBeforeCompile shader injection utility with 6 motion types (float-up, radial, drift, fall, orbit, ring) - Migrate ParticleSystem (2000 particles): float-up with wrap - Migrate StarDust (1500 particles): radial with boundary reset - Migrate BioluminescentLayer (120 particles): drift with sin/cos - Migrate SeafloorSnow (500 particles): fall with y-wrap - Migrate FishSchool (30 particles): orbital path - Migrate OrbitalRing (40x9=360 particles): ring rotation All particles now computed in vertex shader, eliminating CPU loop + buffer upload Layer 3 - useFrame consolidation: - Removed 6 CPU-side useFrame loops with position updates + needsUpdate - Replaced with single uniform update per component (uTime only) - ModuleEntity: OrbitalRing useFrame simplified to uniform-only update
1 parent 1ec8b37 commit 0e9dc4a

6 files changed

Lines changed: 502 additions & 266 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* FishSchool — 发光鱼群:思绪游弋的视觉化(GPU 着色器版)
3+
* 实体外围环形区缓慢环绕漂移,个体间相位差形成自然错落
4+
* 克制含蓄:小尺寸光点,透明度 0.4,始终不穿越前景实体区
5+
*
6+
* 使用顶点着色器计算轨道位置,消除 CPU 循环 + buffer 上传。
7+
*
8+
* @ai-context: 3D 场景对象:FishSchool(DeepSeaWorld 增强层,GPU 粒子版)。
9+
* 宪法第四条:high 30 条 / medium 15 条 / low 关闭。
10+
*/
11+
import { useMemo, useRef } from 'react';
12+
import { useFrame } from '@react-three/fiber';
13+
import * as THREE from 'three';
14+
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
15+
import {
16+
patchParticleShader,
17+
updateGPUParticleUniforms,
18+
addParticleAttributes,
19+
} from '@/lib/3d/shaders/gpuParticleShaders';
20+
21+
/** 最大鱼数(high 档预算) */
22+
const MAX_FISH = 30;
23+
24+
/** 鱼群主组件:Points 环绕路径游动(GPU 着色器) */
25+
export function FishSchool() {
26+
const tier = useEffectiveTier();
27+
const count = tier === 'low' ? 0 : tier === 'medium' ? 15 : MAX_FISH;
28+
const pointsRef = useRef<THREE.Points>(null);
29+
30+
// 固定分配最大 buffer,drawRange 控制可见数
31+
const { positions, colors } = useMemo(() => {
32+
const pos = new Float32Array(MAX_FISH * 3);
33+
const col = new Float32Array(MAX_FISH * 3);
34+
const white = new THREE.Color('#ffffff');
35+
const cyan = new THREE.Color('#67e8f9');
36+
for (let i = 0; i < MAX_FISH; i++) {
37+
const c = white.clone().lerp(cyan, Math.random() * 0.6);
38+
col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b;
39+
}
40+
return { positions: pos, colors: col };
41+
}, []);
42+
43+
const geometry = useMemo(() => {
44+
const geo = new THREE.BufferGeometry();
45+
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
46+
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
47+
// velocity: x=角速度, y=轨道半径, z=高度
48+
addParticleAttributes(geo, MAX_FISH, (i) => [
49+
0.05 + Math.random() * 0.08, // 角速度
50+
6 + Math.random() * 4, // 轨道半径 6~10
51+
Math.random() * 2, // 高度 0~2
52+
]);
53+
return geo;
54+
}, [positions, colors]);
55+
56+
const material = useMemo(() => {
57+
const mat = new THREE.PointsMaterial({
58+
vertexColors: true, size: 0.05, transparent: true,
59+
opacity: 0.4, blending: THREE.AdditiveBlending,
60+
depthWrite: false, sizeAttenuation: true,
61+
});
62+
patchParticleShader(mat, { motion: 'orbit', speed: 0.6 });
63+
return mat;
64+
}, []);
65+
66+
useFrame(({ clock }) => {
67+
if (!pointsRef.current) return;
68+
updateGPUParticleUniforms(pointsRef.current.material as THREE.PointsMaterial, clock.getElapsedTime());
69+
pointsRef.current.geometry.setDrawRange(0, count);
70+
});
71+
72+
if (count === 0) return null;
73+
74+
return <points ref={pointsRef} geometry={geometry} material={material} />;
75+
}

‎client/src/lib/3d/objects/ModuleEntity.tsx‎

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import * as THREE from 'three';
1616
import { Float, Html } from '@react-three/drei';
1717
import { getModuleSubtitle } from '@/features/onboarding/firstDive/moduleSubtitles';
1818
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
19+
import {
20+
patchParticleShader,
21+
updateGPUParticleUniforms,
22+
addParticleAttributes,
23+
} from '@/lib/3d/shaders/gpuParticleShaders';
1924
import {
2025
createModuleTexture, createNormalMap, createRoughnessMap,
2126
createFlashcardTexture, idToSeed,
@@ -230,19 +235,18 @@ function CoreBreath({ color, isActive, isHovered }: {
230235
);
231236
}
232237

233-
// ─── 细节层 4:轨道粒子环(动态旋转) ────────────────
238+
// ─── 细节层 4:轨道粒子环(GPU 着色器版) ────────────
234239
// 使用固定最大 buffer(MAX_COUNT=40)+ drawRange 控制可见粒子数,
235-
// 彻底避免 buffer resize 导致的 WebGL 错误
240+
// 顶点着色器计算旋转位置,消除 CPU 循环 + buffer 上传
236241
const MAX_ORBITAL = 40;
237242

238243
function OrbitalRing({ isActive, isHovered, color }: {
239244
isActive: boolean; isHovered: boolean; color: string;
240245
}) {
241246
const pointsRef = useRef<THREE.Points>(null);
242247
const visibleCount = isActive ? 40 : isHovered ? 20 : 0;
243-
const angleRef = useRef(0);
244248

245-
// 固定分配最大 buffer,避免 resize
249+
// 固定分配最大 buffer
246250
const { positions, colors } = useMemo(() => {
247251
const pos = new Float32Array(MAX_ORBITAL * 3);
248252
const col = new Float32Array(MAX_ORBITAL * 3);
@@ -262,40 +266,37 @@ function OrbitalRing({ isActive, isHovered, color }: {
262266
return { positions: pos, colors: col };
263267
}, [color]);
264268

265-
useFrame((_, delta) => {
269+
const geometry = useMemo(() => {
270+
const geo = new THREE.BufferGeometry();
271+
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
272+
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
273+
// velocity: x=旋转速度倍率
274+
addParticleAttributes(geo, MAX_ORBITAL, (i) => [
275+
isActive ? 1.5 : 0.6,
276+
0, 0,
277+
]);
278+
return geo;
279+
}, [positions, colors, isActive]);
280+
281+
const material = useMemo(() => {
282+
const mat = new THREE.PointsMaterial({
283+
vertexColors: true, size: 0.05, transparent: true,
284+
opacity: isActive ? 0.9 : 0.5, blending: THREE.AdditiveBlending,
285+
depthWrite: false, sizeAttenuation: true,
286+
});
287+
patchParticleShader(mat, { motion: 'ring', speed: 0.8 });
288+
return mat;
289+
}, [isActive]);
290+
291+
useFrame(({ clock }) => {
266292
if (!pointsRef.current) return;
267-
// drawRange 控制可见粒子数(buffer 大小固定不变)
293+
updateGPUParticleUniforms(pointsRef.current.material as THREE.PointsMaterial, clock.getElapsedTime());
268294
pointsRef.current.geometry.setDrawRange(0, visibleCount);
269-
if (visibleCount === 0) return;
270-
angleRef.current += Math.min(delta, 0.1) * (isActive ? 1.5 : 0.6);
271-
const posAttr = pointsRef.current.geometry.attributes.position as THREE.BufferAttribute;
272-
const pos = posAttr.array as Float32Array;
273-
for (let i = 0; i < visibleCount; i++) {
274-
const a = (i / visibleCount) * Math.PI * 2 + angleRef.current;
275-
const radius = 1.4 + (Math.sin(i * 0.5) * 0.08 + 0.08);
276-
const tilt = (Math.sin(i * 0.3) * 0.3);
277-
pos[i * 3] = Math.cos(a) * radius;
278-
pos[i * 3 + 1] = Math.sin(a * 0.7) * 0.3 + tilt;
279-
pos[i * 3 + 2] = Math.sin(a) * radius;
280-
}
281-
posAttr.needsUpdate = true;
282295
});
283296

284297
if (visibleCount === 0) return null;
285298

286-
return (
287-
<points ref={pointsRef}>
288-
<bufferGeometry>
289-
<bufferAttribute attach="attributes-position" args={[positions, 3]} count={MAX_ORBITAL} />
290-
<bufferAttribute attach="attributes-color" args={[colors, 3]} count={MAX_ORBITAL} />
291-
</bufferGeometry>
292-
<pointsMaterial
293-
vertexColors size={0.05} transparent
294-
opacity={isActive ? 0.9 : 0.5} blending={THREE.AdditiveBlending}
295-
depthWrite={false} sizeAttenuation
296-
/>
297-
</points>
298-
);
299+
return <points ref={pointsRef} geometry={geometry} material={material} />;
299300
}
300301

301302
// ─── 细节层 5:地面辉光圆环 ──────────────────────────
Lines changed: 74 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
/**
2-
* 独立粒子系统组件 — 深海浮力微粒
3-
* 使用 THREE.Points + AdditiveBlending 实现高性能粒子渲染
2+
* 独立粒子系统组件 — 深海浮力微粒(GPU 顶点着色器版)
43
*
5-
* @ai-context: 3D 场景对象:ParticleSystem。
6-
* @ai-context: P3-15 降更新频率——按 tier 跳帧更新(high 每帧/medium 隔 1/low 隔 2),
7-
* 水平漂移基于绝对时钟相位(跳帧仍平滑),增量位移乘以 step 补偿以保持运动速度一致。
4+
* 使用 THREE.Points + 自定义顶点着色器实现 GPU 粒子动画:
5+
* 粒子位置计算从 CPU(useFrame 循环 + needsUpdate 上传)迁移到 GPU
6+
* (顶点着色器),消除每帧的 CPU→GPU 数据传输瓶颈。
7+
* 位置算法与 CPU 版一致(浮力上升 + 水平漂移 + 边界回绕)。
8+
*
9+
* 兼容 WebGPU 和 WebGL 渲染后端。
10+
*
11+
* @ai-context: 3D 场景对象:ParticleSystem(GPU 粒子版)。
812
*/
913
import { useRef, useMemo } from 'react';
1014
import { useFrame } from '@react-three/fiber';
1115
import * as THREE from 'three';
1216
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
17+
import {
18+
patchParticleShader,
19+
updateGPUParticleUniforms,
20+
addParticleAttributes,
21+
type GPUParticleConfig,
22+
} from '@/lib/3d/shaders/gpuParticleShaders';
1323

1424
interface ParticleSystemProps {
1525
count?: number;
@@ -27,18 +37,13 @@ export function ParticleSystem({
2737
speed = 1,
2838
}: ParticleSystemProps) {
2939
const pointsRef = useRef<THREE.Points>(null);
30-
// P3-15 跳帧计数器
31-
const frameRef = useRef(0);
32-
// 有效 tier(自动 tier 受用户性能模式上限约束)
3340
const tier = useEffectiveTier();
3441

3542
const particleCount = tier === 'low' ? 500 : tier === 'medium' ? 1200 : count;
36-
// 更新频率分档:high 每帧、medium 隔 1 帧、low 隔 2 帧(粒子运动缓慢,跳帧视觉无感)
37-
const frameSkip = tier === 'low' ? 3 : tier === 'medium' ? 2 : 1;
3843

39-
const { positions, velocities, colors, sizes } = useMemo(() => {
44+
// 固定 buffer 一次性构建(不再每帧更新位置)
45+
const { positions, colors, sizes } = useMemo(() => {
4046
const pos = new Float32Array(particleCount * 3);
41-
const vel = new Float32Array(particleCount * 3);
4247
const col = new Float32Array(particleCount * 3);
4348
const sz = new Float32Array(particleCount);
4449

@@ -47,93 +52,80 @@ export function ParticleSystem({
4752

4853
for (let i = 0; i < particleCount; i++) {
4954
const i3 = i * 3;
50-
// Random position within bounds
5155
pos[i3] = (Math.random() - 0.5) * 2 * bounds.x;
5256
pos[i3 + 1] = bounds.y[0] + Math.random() * (bounds.y[1] - bounds.y[0]);
5357
pos[i3 + 2] = (Math.random() - 0.5) * 2 * bounds.z;
5458

55-
// Random velocity factors for variation
56-
vel[i3] = (Math.random() - 0.5) * 0.3; // horizontal drift factor
57-
vel[i3 + 1] = 0.002 + Math.random() * 0.004; // upward speed
58-
vel[i3 + 2] = (Math.random() - 0.5) * 0.3; // horizontal drift factor
59-
60-
// Lerp between two colors
6159
const t = Math.random();
6260
const lerpedColor = colorA.clone().lerp(colorB, t);
6361
col[i3] = lerpedColor.r;
6462
col[i3 + 1] = lerpedColor.g;
6563
col[i3 + 2] = lerpedColor.b;
6664

67-
// Random size
6865
sz[i] = 0.02 + Math.random() * 0.06;
6966
}
70-
71-
return { positions: pos, velocities: vel, colors: col, sizes: sz };
67+
return { positions: pos, colors: col, sizes: sz };
7268
}, [particleCount, bounds, baseColor, secondaryColor]);
7369

70+
// 创建几何体并添加 GPU 粒子属性
71+
const geometry = useMemo(() => {
72+
const geo = new THREE.BufferGeometry();
73+
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
74+
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
75+
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
76+
77+
// 添加 GPU 粒子 velocity 和 seed 属性
78+
addParticleAttributes(
79+
geo,
80+
particleCount,
81+
(i) => {
82+
const i3 = i * 3;
83+
return [
84+
positions[i3] * 0.01, // x 漂移因子
85+
0.002 + Math.random() * 0.004, // 上升速度
86+
positions[i3 + 2] * 0.01, // z 漂移因子
87+
];
88+
},
89+
() => Math.random(),
90+
);
91+
92+
return geo;
93+
}, [positions, colors, sizes, particleCount]);
94+
95+
// 创建材质并注入 GPU 粒子着色器
96+
const material = useMemo(() => {
97+
const mat = new THREE.PointsMaterial({
98+
vertexColors: true,
99+
transparent: true,
100+
opacity: 0.7,
101+
sizeAttenuation: true,
102+
size: 0.05,
103+
blending: THREE.AdditiveBlending,
104+
depthWrite: false,
105+
});
106+
107+
const config: GPUParticleConfig = {
108+
motion: 'float-up',
109+
wrap: true,
110+
bounds: { yMin: bounds.y[0], yMax: bounds.y[1] },
111+
speed,
112+
};
113+
114+
patchParticleShader(mat, config);
115+
return mat;
116+
}, [bounds.y[0], bounds.y[1], speed]);
117+
118+
// 每帧仅更新 uniform(不再更新 buffer)
74119
useFrame(({ clock }) => {
75-
if (!pointsRef.current) return;
76-
// P3-15 跳帧:非更新帧直接返回,减少 sin/cos 逐粒子计算开销
77-
frameRef.current += 1;
78-
if (frameRef.current % frameSkip !== 0) return;
79-
const step = frameSkip; // 补偿跳过帧,保持运动速度一致
80-
81-
const posAttr = pointsRef.current.geometry.attributes.position as THREE.BufferAttribute;
82-
const posArray = posAttr.array as Float32Array;
83-
const time = clock.getElapsedTime();
84-
85-
for (let i = 0; i < particleCount; i++) {
86-
const i3 = i * 3;
87-
88-
// Horizontal drift using sin/cos(基于绝对时钟相位,跳帧仍平滑)
89-
posArray[i3] += Math.sin(time * 0.5 + i * 0.1) * velocities[i3] * speed * 0.001 * step;
90-
posArray[i3 + 2] += Math.cos(time * 0.4 + i * 0.1) * velocities[i3 + 2] * speed * 0.001 * step;
91-
92-
// Float upward
93-
posArray[i3 + 1] += velocities[i3 + 1] * speed * step;
94-
95-
// Reset particle to bottom when reaching top
96-
if (posArray[i3 + 1] > bounds.y[1]) {
97-
posArray[i3 + 1] = bounds.y[0];
98-
posArray[i3] = (Math.random() - 0.5) * 2 * bounds.x;
99-
posArray[i3 + 2] = (Math.random() - 0.5) * 2 * bounds.z;
100-
}
120+
if (pointsRef.current) {
121+
updateGPUParticleUniforms(
122+
pointsRef.current.material as THREE.PointsMaterial,
123+
clock.getElapsedTime(),
124+
);
101125
}
102-
103-
posAttr.needsUpdate = true;
104126
});
105127

106128
return (
107-
<points ref={pointsRef} key={`ps-${particleCount}`}>
108-
<bufferGeometry key={`ps-geo-${particleCount}`}>
109-
<bufferAttribute
110-
attach="attributes-position"
111-
array={positions}
112-
count={particleCount}
113-
itemSize={3}
114-
/>
115-
<bufferAttribute
116-
attach="attributes-color"
117-
array={colors}
118-
count={particleCount}
119-
itemSize={3}
120-
/>
121-
<bufferAttribute
122-
attach="attributes-size"
123-
array={sizes}
124-
count={particleCount}
125-
itemSize={1}
126-
/>
127-
</bufferGeometry>
128-
<pointsMaterial
129-
vertexColors
130-
transparent
131-
opacity={0.7}
132-
sizeAttenuation
133-
size={0.05}
134-
blending={THREE.AdditiveBlending}
135-
depthWrite={false}
136-
/>
137-
</points>
129+
<points ref={pointsRef} geometry={geometry} material={material} />
138130
);
139-
}
131+
}

0 commit comments

Comments
 (0)