Three.js 实战:手把手教你用ShaderMaterial打造一个会呼吸的熔岩星球特效
在数字艺术与交互式3D内容创作领域,Three.js已成为连接创意与技术的桥梁。而ShaderMaterial则是这座桥梁上最耀眼的明珠,它让我们能够直接对话GPU,用数学和算法编织视觉魔法。本文将带你深入ShaderMaterial的核心,从零构建一个充满生命力的熔岩星球——不是简单调用现成材质,而是亲手编写着色器代码,让每一行GLSL都成为创造力的延伸。
1. 环境准备与基础架构
在开始着色器编程之前,我们需要搭建一个标准的Three.js开发环境。推荐使用Vite作为构建工具,它能完美支持GLSL代码的模块化导入:
npm create vite@latest lava-planet --template vanilla cd lava-planet npm install three @types/three创建基础场景时,有几个关键配置需要注意:
- 启用WebGLRenderer的
antialias属性以获得更平滑的边缘 - 设置合理的相机位置和视野角度
- 添加轨道控制器(OrbitControls)方便调试
import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 5; const controls = new OrbitControls(camera, renderer.domElement);2. 熔岩核心:顶点与片元着色器基础
ShaderMaterial的核心在于两个着色器程序:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。我们先创建一个最基础的着色器材质:
const material = new THREE.ShaderMaterial({ vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec2 vUv; void main() { gl_FragColor = vec4(vUv, 0.0, 1.0); } ` });这个最简单的着色器已经包含了几个关键概念:
uv坐标:每个顶点在纹理空间的位置(0到1)varying变量:在顶点和片元着色器之间传递数据- 矩阵变换:将模型坐标转换为屏幕坐标
提示:在开发过程中,可以通过显示UV坐标来快速验证着色器的基础功能是否正常。
3. 动态熔岩纹理的实现
要让熔岩看起来有流动感,我们需要在着色器中实现UV动画。这里采用噪声函数结合时间变量的方案:
// 片元着色器中添加噪声函数 float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); } // 在uniforms中添加时间变量 uniforms: { time: { value: 0 } } // 动画循环中更新uniform function animate() { requestAnimationFrame(animate); material.uniforms.time.value += 0.01; renderer.render(scene, camera); }完整的熔岩效果需要多层噪声叠加:
float lavaPattern(vec2 uv) { float n1 = random(uv * 10.0 + time); float n2 = random(uv * 20.0 - time * 1.5); float n3 = random(uv * 5.0 + time * 0.7); return (n1 * 0.6 + n2 * 0.3 + n3 * 0.1); } void main() { float pattern = lavaPattern(vUv); vec3 color = mix(vec3(0.8, 0.3, 0.1), vec3(1.0, 0.6, 0.2), pattern); gl_FragColor = vec4(color, 1.0); }4. 内部发光效果的数学原理
内部发光效果的核心是计算表面法线与视线方向的夹角。当表面正对相机时(夹角为0),透明度最高;当表面与视线垂直时(夹角90度),完全不透明。
在顶点着色器中添加:
varying vec3 vNormal; varying vec3 vViewDir; void main() { vNormal = normalize(normalMatrix * normal); vec4 worldPosition = modelMatrix * vec4(position, 1.0); vViewDir = normalize(cameraPosition - worldPosition.xyz); // ...原有代码 }在片元着色器中计算菲涅尔效应:
float fresnel = pow(1.0 - dot(vNormal, vViewDir), 2.0); vec3 glow = vec3(0.9, 0.4, 0.1) * fresnel * 2.0; gl_FragColor.rgb += glow;注意:菲涅尔效应的强度可以通过调整幂指数来控制,值越大边缘发光越锐利。
5. 耀斑特效的环形几何
蓝色耀斑效果需要特殊的几何结构。我们使用RingGeometry并自定义顶点属性:
const geometry = new THREE.RingGeometry(0.5, 1, 32); const positionAttribute = geometry.attributes.position; const count = positionAttribute.count; // 添加自定义属性:顶点到中心的距离 const radiusArray = new Float32Array(count); for (let i = 0; i < count; i++) { const x = positionAttribute.getX(i); const y = positionAttribute.getY(i); radiusArray[i] = sqrt(x * x + y * y); } geometry.setAttribute('radius', new THREE.BufferAttribute(radiusArray, 1));在着色器中利用这个属性创建渐变透明效果:
attribute float radius; varying float vRadius; void main() { vRadius = radius; // ...原有顶点着色器代码 } // 片元着色器 float edge = smoothstep(0.4, 0.5, vRadius) * (1.0 - smoothstep(0.9, 1.0, vRadius)); float alpha = edge * sin(time + vRadius * 10.0) * 0.8; vec3 blueFlare = vec3(0.2, 0.5, 1.0) * alpha * 2.0;6. 性能优化与调试技巧
着色器开发中常见的性能陷阱和解决方案:
| 问题类型 | 表现症状 | 优化方案 |
|---|---|---|
| 精度过高 | 移动端帧率骤降 | 使用precision mediump float |
| 复杂循环 | 着色器编译超时 | 展开循环或使用纹理查找 |
| 冗余计算 | 帧率波动大 | 将计算移到顶点着色器 |
调试着色器的实用技巧:
- 使用
gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);可视化法线 - 通过
console.log(gl.getShaderInfoLog(shader))获取编译错误 - 逐步注释代码块定位问题区域
// 在init函数中添加错误检查 renderer.debug.checkShaderErrors = true;7. 完整效果集成与参数调节
将所有效果层叠加时,需要注意渲染顺序和混合模式:
// 熔岩球体 const lavaSphere = new THREE.Mesh(sphereGeometry, lavaMaterial); scene.add(lavaSphere); // 耀斑系统 const flareGroup = new THREE.Group(); for (let i = 0; i < 8; i++) { const flare = new THREE.Mesh(flareGeometry, flareMaterial); flare.rotation.z = Math.PI * 2 * (i / 8); flareGroup.add(flare); } scene.add(flareGroup); // 外层辉光 const glowMaterial = new THREE.SpriteMaterial({ map: glowTexture, blending: THREE.AdditiveBlending, opacity: 0.8 }); const glow = new THREE.Sprite(glowMaterial); glow.scale.set(5, 5, 1); scene.add(glow);关键参数调节建议:
- 熔岩流动速度:调整time变量的增量
- 发光强度:修改菲涅尔效应的乘数
- 耀斑密度:改变sin函数的频率参数
- 整体色调:调整mix函数的颜色参数
在项目实践中,我发现最耗时的部分往往是参数的微调过程。建议为每个主要效果创建dat.GUI控制器,实时观察参数变化的影响:
const params = { lavaSpeed: 0.01, glowIntensity: 2.0, flareDensity: 10.0 }; const gui = new GUI(); gui.add(params, 'lavaSpeed', 0, 0.1); gui.add(params, 'glowIntensity', 0, 5); gui.add(params, 'flareDensity', 1, 20);