使用 Three.js 的 Shader

2025-12-19
使用 Three.js 的 ShaderMaterial 和 7万个粒子创建一个金黄色发光星系
思考过程
**Analyzing Galaxy Design** I'm currently focused on the visual aspects of the galaxy. I've been experimenting with `ShaderMaterial` and about 70,000 particles to create that golden, glowing spiral effect. Performance is a key factor, of course, and I'm looking at ways to optimize, particularly by using points, while balancing the aesthetics of the glitter and glow. The math of the spiral itself is next. **Formulating the Spiral's Algorithm** I'm now delving into the mathematical underpinnings of the spiral shape, exploring formulas for controllable arms, radius, and twist. To start with, I'm leaning towards the use of `THREE.Points` and creating the golden-glowing spiral effect using both vertex and fragment shaders. I'm focusing on defining core attributes like position, color, size, and angle. Also, I am looking to establish a golden core with a higher density of white-gold and the spiral arms using a mixture of gold and dark orange. **Refining Shader Application** I'm now zeroing in on the shader specifics. Building on the core spiral math and particle positions, I'm defining the vertex shader's key role in positioning and size attenuation based on distance. The fragment shader is shaping up to create a soft, circular glow using the distance from the point's center. I'm aiming for an additive blending effect with a palette of gold, orange, and white to enhance the visual impact.
创建一个包含 7 万个粒子的金黄色发光星系,最有效的方法是使用 `THREE.BufferGeometry` 结合自定义的 `ShaderMaterial`。 为了实现“金黄色发光”效果,我们需要在 Shader 中处理粒子的颜色渐变、光晕效果以及加法混合(Additive Blending)。 以下是完整的代码实现: ### 1. 核心思路 - **几何体**:使用 `BufferGeometry` 存储 70,000 个粒子的位置、颜色和大小。 - **数学模型**:使用阿基米德螺旋线(Spiral formula)来分布粒子。 - **顶点着色器 (Vertex Shader)**:计算粒子的位置,并根据距离相机的远近缩放粒子大小。 - **片元着色器 (Fragment Shader)**:绘制圆形的、具有径向渐变的光效,模拟发光。 - **混合模式**:使用 `THREE.AdditiveBlending` 让粒子重叠处变得更亮。 ### 2. 完整代码示例 你可以直接将以下代码复制到 HTML 文件中运行(需联网加载 Three.js): ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Three.js Golden Galaxy - 70k Particles</title> <style> body { margin: 0; background-color: #000; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // --- 初始化场景 --- const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 3, 5); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // --- 星系配置 --- const parameters = { count: 70000, size: 0.015, radius: 5, branches: 3, spin: 1.5, randomness: 0.2, randomnessPower: 3, insideColor: "#ffdd66", // 中心金黄色 outsideColor: "#ff7700" // 边缘橙红色 }; // --- 几何体生成 --- const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(parameters.count * 3); const colors = new Float32Array(parameters.count * 3); const scales = new Float32Array(parameters.count); const colorInside = new THREE.Color(parameters.insideColor); const colorOutside = new THREE.Color(parameters.outsideColor); for (let i = 0; i < parameters.count; i++) { const i3 = i * 3; // 位置计算 const radius = Math.random() * parameters.radius; const spinAngle = radius * parameters.spin; const branchAngle = ((i % parameters.branches) / parameters.branches) * Math.PI * 2; const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius; const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius; const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius; positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX; positions[i3 + 1] = randomY; positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ; // 颜色混合 const mixedColor = colorInside.clone(); mixedColor.lerp(colorOutside, radius / parameters.radius); colors[i3] = mixedColor.r; colors[i3 + 1] = mixedColor.g; colors[i3 + 2] = mixedColor.b; // 随机大小系数 scales[i] = Math.random(); } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1)); // --- Shader Material --- const shaderMaterial = new THREE.ShaderMaterial({ depthWrite: false, blending: THREE.AdditiveBlending, vertexColors: true, uniforms: { uTime: { value: 0 }, uSize: { value: 30.0 * renderer.getPixelRatio() } }, vertexShader: ` uniform float uTime; uniform float uSize; attribute float aScale; varying vec3 vColor; void main() { // 基础位置 vec4 modelPosition = modelMatrix * vec4(position, 1.0); // 简单的旋转动画 float angle = atan(modelPosition.x, modelPosition.z); float distance = length(modelPosition.xz); float angleOffset = (1.0 / distance) * uTime * 0.2; angle += angleOffset; modelPosition.x = cos(angle) * distance; modelPosition.z = sin(angle) * distance; vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition; gl_Position = projectionPosition; // 粒子大小计算:基础大小 * 随机系数 * 距离衰减 gl_PointSize = uSize * aScale; gl_PointSize *= (1.0 / - viewPosition.z); vColor = color; } `, fragmentShader: ` varying vec3 vColor; void main() { // 制作圆形发光点 float strength = distance(gl_PointCoord, vec2(0.5)); strength = 1.0 - strength; strength = pow(strength, 3.0); // 增强边缘羽化 // 最终颜色:粒子颜色 * 强度 vec3 finalColor = mix(vec3(0.0), vColor, strength); gl_FragColor = vec4(finalColor, strength); } ` }); const points = new THREE.Points(geometry, shaderMaterial); scene.add(points); // --- 动画循环 --- const clock = new THREE.Clock(); function animate() { const elapsedTime = clock.getElapsedTime(); // 更新 Shader 中的时间 shaderMaterial.uniforms.uTime.value = elapsedTime; controls.update(); renderer.render(scene, camera); requestAnimationFrame(animate); } animate(); // 窗口自适应 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); }); </script> </body> </html> ``` ### 3. 关键点解释 #### 1. 螺旋线生成逻辑 我们通过以下公式计算每个粒子的位置: - `branchAngle`: 确定粒子属于哪一条旋臂。 - `spinAngle`: `radius * parameters.spin`。距离中心越远,旋转角度越大,形成螺旋效果。 - `randomness`: 使用 `Math.pow(..., power)` 让大部分粒子聚集在旋臂中心,少数散开,这样看起来更自然。 #### 2. Shader 动画 在 `Vertex Shader` 中,我们并没有直接在 JavaScript 里更新 7 万个粒子的位置(那会非常卡顿),而是根据 `uTime` 在 GPU 中实时计算旋转。 ```glsl float angleOffset = (1.0 / distance) * uTime * 0.2; ``` 这让内圈转得比外圈快,符合星系的物理视觉感。 #### 3. 发光效果 (Glow Effect) - **Additive Blending**:`blending: THREE.AdditiveBlending` 是关键。它会让重叠的粒子颜色相加。当几千个金色的点重叠在中心时,会变成亮白色,产生强烈的发光感。 - **Fragment Shader 径向渐变**: ```glsl float strength = distance(gl_PointCoord, vec2(0.5)); strength = pow(1.0 - strength, 3.0); ``` 这行代码将原本的正方形粒子贴图变成了一个中心亮、边缘模糊的圆形光斑。 #### 4. 性能优化 - **70,000 个粒子**:对于现代显卡,在 `THREE.Points` 模式下是非常轻松的。 - **BufferAttribute**:数据直接上传到显存,避免了每一帧的 CPU-GPU 通信。 - **depthWrite: false**:对于半透明粒子混合,关闭深度写入可以解决粒子重叠时的黑色方块遮挡问题。 ### 4. 如何调整? - **改变颜色**:修改 `parameters.insideColor`(中心)和 `outsideColor`(外围)。 - **改变密度**:修改 `parameters.count`。 - **改变形状**:调整 `parameters.branches`(旋臂数量)和 `parameters.spin`(卷曲程度)。