一、GLSL 着色器入门
1.1 简历描述模板
项目经验:自定义着色器特效开发 - WebGL 编程
负责开发多个自定义着色器特效,实现了水波纹、全息投影、粒子爆炸等 20+ 视觉效果。通过 GLSL 编程和 GPU 计算,实现了 CPU 无法达到的实时特效性能。核心工作包括:
- 开发了完整的着色器材质库,封装了顶点着色器和片元着色器的常用功能模块
- 实现了水面反射、折射效果,通过实时计算法线贴图和菲涅尔反射,达到真实的水体效果
- 开发了粒子系统着色器,支持 100 万+ 粒子的实时渲染,通过 GPU Instancing 优化性能
- 实现了后期处理特效管线,包括 Bloom、景深、色调映射等 15+ 后期效果
- 优化着色器性能,通过减少纹理采样、优化分支逻辑,提升渲染效率 50%
1.2 SOP 标准回答
面试官:你对 GLSL 着色器编程有什么了解?
我在做 3D 项目时,经常需要实现一些特殊的视觉效果,Three.js 自带的材质满足不了需求,就需要自己写着色器。
GLSL 是 OpenGL 的着色器语言,语法类似 C 语言。在 WebGL 中,着色器分为顶点着色器和片元着色器两部分。顶点着色器负责处理每个顶点的位置、法线等信息,输出到裁剪空间;片元着色器负责计算每个像素的颜色。
我举个例子,之前做过一个水波纹效果。顶点着色器里,我根据时间变量和顶点位置计算正弦波,让水面产生波动。具体的公式是 position.y += sin(position.x * frequency + time) * amplitude。然后在片元着色器里,根据法线方向和视角计算反射和折射,加上一点菲涅尔效应,就能得到比较真实的水面效果。
着色器编程的难点在于调试。因为着色器在 GPU 上运行,不能像 JavaScript 那样打 console.log 调试。我一般的做法是把中间结果输出为颜色,通过颜色判断计算是否正确。比如要调试法线,就把法线的 xyz 映射到 rgb 输出,看颜色是否符合预期。
性能优化也很重要。着色器里的每个操作都会被执行成千上万次,所以要特别注意效率。比如避免在片元着色器里做复杂计算,能在顶点着色器做的就在顶点着色器做;减少纹理采样次数;少用分支语句,因为 GPU 对分支不友好。
我还学会了一些技巧,比如用 smoothstep 做平滑过渡,用 mix 做插值,用 fract 做循环动画等。这些 GLSL 内置函数都是经过优化的,比自己写要快。
面试官:你是如何将自定义着色器集成到 Three.js 中的?
Three.js 提供了 ShaderMaterial,可以很方便地使用自定义着色器。
首先我会把着色器代码写在单独的文件里,比如 water.vert 和 water.frag。在 Vue 组件里,我用 import 导入这些文件,或者直接写成模板字符串。
然后创建 ShaderMaterial,传入顶点着色器和片元着色器代码,还有 uniforms(着色器的输入变量)。uniforms 可以传递时间、鼠标位置、纹理等数据给着色器。
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
time: { value: 0 },
resolution: { value: new THREE.Vector2(800, 600) },
texture1: { value: texture }
}
})
在渲染循环里,我会更新 uniforms 的值,比如每帧更新时间变量,这样着色器里的动画就会运行。
有个要注意的点是,Three.js 会自动注入一些变量到着色器里,比如模型矩阵、视图矩阵、投影矩阵等。我们可以直接使用这些变量,不需要自己传。还有 Three.js 的光照信息也可以在着色器里获取,前提是在 ShaderMaterial 里设置 lights: true。
如果要实现更复杂的效果,可以继承 Three.js 的 ShaderMaterial,重写 onBeforeCompile 方法,在编译前修改着色器代码。这样可以在不完全自定义的情况下,扩展 Three.js 自带材质的功能。
1.3 难点与亮点分析
难点 1:着色器调试困难
问题:着色器在 GPU 上运行,无法使用传统的 console.log 调试,出错时只显示编译失败。
解决方案:
- 使用可视化调试方法,将中间变量映射为颜色输出
- 分段测试,逐步添加功能代码,定位问题位置
- 使用 Shader Toy 等在线工具快速原型验证
- 集成 glslify,使用模块化开发和调试
技术亮点:
- 开发了着色器调试工具,可以实时修改 uniforms 查看效果
- 实现了着色器热重载,修改代码后自动编译
- 建立了着色器代码库,复用常用功能
难点 2:性能优化
问题:复杂的着色器计算导致帧率下降,特别是在移动端。
解决方案:
- 将复杂计算移到顶点着色器,利用硬件插值
- 使用查找表(LUT)替代实时计算
- 减少纹理采样,合并纹理通道
- 避免分支语句,使用 step、mix 等函数
技术亮点:
- 实现了着色器性能分析工具
- 针对移动端做了降级方案
- 使用预计算和缓存策略
难点 3:跨平台兼容性
问题:不同设备和浏览器对 GLSL 的支持程度不同,同样的代码可能在某些设备上无法运行。
解决方案:
- 使用 GLSL 1.0 语法,兼容性最好
- 避免使用高级特性,如纹理数组、循环展开
- 提供多个精度版本(highp、mediump、lowp)
- 检测设备能力,动态加载对应版本
技术亮点:
- 实现了着色器能力检测系统
- 提供优雅降级方案
- 自动选择最优精度
1.4 完整技术实现
基础着色器组件
<!-- BasicShader.vue -->
<template>
<div class="basic-shader">
<div ref="containerRef" class="shader-container"></div>
<div class="controls">
<h3>着色器参数</h3>
<div class="control-group">
<label>时间速度: {{ timeSpeed }}</label>
<input
type="range"
min="0"
max="2"
step="0.1"
v-model.number="timeSpeed"
/>
</div>
<div class="control-group">
<label>波浪频率: {{ frequency }}</label>
<input
type="range"
min="1"
max="10"
step="0.5"
v-model.number="frequency"
/>
</div>
<div class="control-group">
<label>波浪振幅: {{ amplitude }}</label>
<input
type="range"
min="0"
max="2"
step="0.1"
v-model.number="amplitude"
/>
</div>
<div class="control-group">
<label>颜色</label>
<input type="color" v-model="color" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const containerRef = ref(null)
// Three.js 核心对象
let scene, camera, renderer, controls
let mesh, material
let animationId
// 着色器参数
const timeSpeed = ref(1)
const frequency = ref(5)
const amplitude = ref(1)
const color = ref('#00ffff')
const time = ref(0)
// 顶点着色器
const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;
varying vec3 vNormal;
uniform float uTime;
uniform float uFrequency;
uniform float uAmplitude;
void main() {
vUv = uv;
vPosition = position;
vNormal = normal;
// 创建波浪效果
vec3 pos = position;
float wave = sin(pos.x * uFrequency + uTime) * uAmplitude;
wave += sin(pos.z * uFrequency * 0.8 + uTime * 1.3) * uAmplitude * 0.5;
pos.y += wave * 0.1;
// 计算新的法线
vec3 tangent = vec3(1.0, cos(pos.x * uFrequency + uTime) * uAmplitude * uFrequency * 0.1, 0.0);
vec3 bitangent = vec3(0.0, cos(pos.z * uFrequency * 0.8 + uTime * 1.3) * uAmplitude * uFrequency * 0.08, 1.0);
vNormal = normalize(cross(tangent, bitangent));
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`
// 片元着色器
const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;
varying vec3 vNormal;
uniform float uTime;
uniform vec3 uColor;
void main() {
// 基础颜色
vec3 color = uColor;
// 根据法线添加光照效果
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diffuse = max(dot(vNormal, lightDir), 0.0);
// 添加一些变化
float pattern = sin(vUv.x * 10.0 + uTime) * sin(vUv.y * 10.0 + uTime);
pattern = pattern * 0.5 + 0.5;
// 混合颜色
color = mix(color, color * 1.5, pattern);
color *= (diffuse * 0.5 + 0.5);
// 添加边缘高光
vec3 viewDir = normalize(cameraPosition - vPosition);
float fresnel = pow(1.0 - max(dot(viewDir, vNormal), 0.0), 3.0);
color += vec3(fresnel * 0.5);
gl_FragColor = vec4(color, 1.0);
}
`
// 初始化场景
const initScene = () => {
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0x111111)
// 创建相机
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 3, 5)
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// 创建着色器材质
material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uFrequency: { value: frequency.value },
uAmplitude: { value: amplitude.value },
uColor: { value: new THREE.Color(color.value) }
},
side: THREE.DoubleSide
})
// 创建几何体
const geometry = new THREE.PlaneGeometry(5, 5, 64, 64)
mesh = new THREE.Mesh(geometry, material)
mesh.rotation.x = -Math.PI / 4
scene.add(mesh)
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(2)
scene.add(axesHelper)
}
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
// 更新时间
time.value += 0.016 * timeSpeed.value
material.uniforms.uTime.value = time.value
// 更新控制器
controls.update()
// 渲染场景
renderer.render(scene, camera)
}
// 处理窗口大小变化
const handleResize = () => {
if (!containerRef.value) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
// 监听参数变化
watch(frequency, (value) => {
if (material) {
material.uniforms.uFrequency.value = value
}
})
watch(amplitude, (value) => {
if (material) {
material.uniforms.uAmplitude.value = value
}
})
watch(color, (value) => {
if (material) {
material.uniforms.uColor.value = new THREE.Color(value)
}
})
// 清理
const dispose = () => {
if (animationId) {
cancelAnimationFrame(animationId)
}
if (controls) {
controls.dispose()
}
if (mesh) {
mesh.geometry.dispose()
mesh.material.dispose()
}
if (renderer) {
renderer.dispose()
if (renderer.domElement.parentElement) {
renderer.domElement.parentElement.removeChild(renderer.domElement)
}
}
}
// 生命周期
onMounted(() => {
initScene()
animate()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
dispose()
})
</script>
<style scoped>
.basic-shader {
position: relative;
width: 100%;
height: 100vh;
background: #000;
}
.shader-container {
width: 100%;
height: 100%;
}
.controls {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 8px;
min-width: 250px;
}
h3 {
margin: 0 0 16px 0;
font-size: 14px;
color: #0ff;
text-transform: uppercase;
}
.control-group {
margin-bottom: 16px;
}
.control-group label {
display: block;
margin-bottom: 8px;
font-size: 12px;
color: #ccc;
}
.control-group input[type="range"] {
width: 100%;
}
.control-group input[type="color"] {
width: 100%;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
二、自定义材质效果
2.1 水面材质实现
<!-- WaterMaterial.vue -->
<template>
<div class="water-material">
<div ref="containerRef" class="scene-container"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const containerRef = ref(null)
let scene, camera, renderer, controls
let waterMesh, skybox
let animationId
// 水面顶点着色器
const waterVertexShader = `
varying vec2 vUv;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float uTime;
uniform float uWaveHeight;
uniform float uWaveFrequency;
// 噪声函数
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x * 34.0) + 1.0) * x);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}
void main() {
vUv = uv;
vec3 pos = position;
// 使用多层噪声创建波浪
float wave1 = snoise(vec3(pos.x * uWaveFrequency, pos.z * uWaveFrequency, uTime * 0.5));
float wave2 = snoise(vec3(pos.x * uWaveFrequency * 2.0, pos.z * uWaveFrequency * 2.0, uTime * 0.8));
float wave3 = snoise(vec3(pos.x * uWaveFrequency * 4.0, pos.z * uWaveFrequency * 4.0, uTime * 1.2));
pos.y += (wave1 * 0.5 + wave2 * 0.25 + wave3 * 0.125) * uWaveHeight;
vPosition = pos;
// 计算法线(用于反射和折射)
float delta = 0.01;
float dx = snoise(vec3((pos.x + delta) * uWaveFrequency, pos.z * uWaveFrequency, uTime * 0.5))
- snoise(vec3((pos.x - delta) * uWaveFrequency, pos.z * uWaveFrequency, uTime * 0.5));
float dz = snoise(vec3(pos.x * uWaveFrequency, (pos.z + delta) * uWaveFrequency, uTime * 0.5))
- snoise(vec3(pos.x * uWaveFrequency, (pos.z - delta) * uWaveFrequency, uTime * 0.5));
vNormal = normalize(vec3(-dx, 1.0, -dz));
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`
// 水面片元着色器
const waterFragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float uTime;
uniform vec3 uWaterColor;
uniform vec3 uFoamColor;
uniform samplerCube uEnvMap;
uniform float uReflectivity;
uniform float uTransparency;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// 计算反射方向
vec3 reflectDir = reflect(-viewDir, normal);
// 环境反射
vec3 envColor = textureCube(uEnvMap, reflectDir).rgb;
// 菲涅尔效应
float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), 3.0);
fresnel = clamp(fresnel, 0.0, 1.0);
// 基础水面颜色
vec3 waterColor = uWaterColor;
// 混合反射和水面颜色
vec3 color = mix(waterColor, envColor, uReflectivity * fresnel);
// 添加一些波光
float sparkle = pow(max(dot(normal, normalize(vec3(1.0, 1.0, 0.5))), 0.0), 32.0);
color += vec3(sparkle * 0.5);
// 根据深度添加透明度
float alpha = mix(uTransparency, 1.0, fresnel);
gl_FragColor = vec4(color, alpha);
}
`
// 初始化场景
const initScene = () => {
scene = new THREE.Scene()
// 创建相机
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 10)
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.maxPolarAngle = Math.PI / 2.5
// 创建天空盒
const cubeTextureLoader = new THREE.CubeTextureLoader()
// 实际项目中需要加载真实的天空盒纹理
const envMap = cubeTextureLoader.load([
'/textures/skybox/px.jpg',
'/textures/skybox/nx.jpg',
'/textures/skybox/py.jpg',
'/textures/skybox/ny.jpg',
'/textures/skybox/pz.jpg',
'/textures/skybox/nz.jpg'
])
scene.background = envMap
// 创建水面材质
const waterMaterial = new THREE.ShaderMaterial({
vertexShader: waterVertexShader,
fragmentShader: waterFragmentShader,
uniforms: {
uTime: { value: 0 },
uWaveHeight: { value: 0.3 },
uWaveFrequency: { value: 1.0 },
uWaterColor: { value: new THREE.Color(0x1e3a5f) },
uFoamColor: { value: new THREE.Color(0xffffff) },
uEnvMap: { value: envMap },
uReflectivity: { value: 0.8 },
uTransparency: { value: 0.7 }
},
transparent: true,
side: THREE.DoubleSide
})
// 创建水面几何体
const waterGeometry = new THREE.PlaneGeometry(20, 20, 128, 128)
waterMesh = new THREE.Mesh(waterGeometry, waterMaterial)
waterMesh.rotation.x = -Math.PI / 2
scene.add(waterMesh)
// 添加一些水下物体
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff6b6b })
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
sphere.position.set(0, -2, 0)
scene.add(sphere)
// 添加光照
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 10, 5)
scene.add(directionalLight)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
}
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
// 更新时间
if (waterMesh) {
waterMesh.material.uniforms.uTime.value += 0.016
}
controls.update()
renderer.render(scene, camera)
}
// 清理
const dispose = () => {
if (animationId) {
cancelAnimationFrame(animationId)
}
if (controls) controls.dispose()
if (waterMesh) {
waterMesh.geometry.dispose()
waterMesh.material.dispose()
}
if (renderer) {
renderer.dispose()
if (renderer.domElement.parentElement) {
renderer.domElement.parentElement.removeChild(renderer.domElement)
}
}
}
onMounted(() => {
initScene()
animate()
})
onUnmounted(() => {
dispose()
})
</script>
<style scoped>
.water-material {
width: 100%;
height: 100vh;
background: linear-gradient(to bottom, #87CEEB, #1e3a5f);
}
.scene-container {
width: 100%;
height: 100%;
}
</style>
三、后期处理特效
3.1 后期处理管线
<!-- PostProcessing.vue -->
<template>
<div class="post-processing">
<div ref="containerRef" class="scene-container"></div>
<div class="effects-panel">
<h3>后期处理效果</h3>
<div class="effect-control">
<label>
<input type="checkbox" v-model="effects.bloom" />
Bloom 辉光
</label>
<input
v-if="effects.bloom"
type="range"
min="0"
max="3"
step="0.1"
v-model.number="bloomStrength"
/>
</div>
<div class="effect-control">
<label>
<input type="checkbox" v-model="effects.vignette" />
Vignette 暗角
</label>
</div>
<div class="effect-control">
<label>
<input type="checkbox" v-model="effects.chromaticAberration" />
色差
</label>
</div>
<div class="effect-control">
<label>
<input type="checkbox" v-model="effects.filmGrain" />
胶片颗粒
</label>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'
const containerRef = ref(null)
let scene, camera, renderer, controls
let composer
let animationId
const effects = ref({
bloom: true,
vignette: true,
chromaticAberration: false,
filmGrain: false
})
const bloomStrength = ref(1.5)
// 自定义后期处理着色器 - 暗角效果
const VignetteShader = {
uniforms: {
tDiffuse: { value: null },
offset: { value: 1.0 },
darkness: { value: 1.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float offset;
uniform float darkness;
varying vec2 vUv;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
vec2 uv = (vUv - vec2(0.5)) * vec2(offset);
float vignette = 1.0 - dot(uv, uv);
vignette = pow(vignette, darkness);
texel.rgb *= vignette;
gl_FragColor = texel;
}
`
}
// 色差效果
const ChromaticAberrationShader = {
uniforms: {
tDiffuse: { value: null },
amount: { value: 0.005 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float amount;
varying vec2 vUv;
void main() {
vec2 offset = vec2(amount, 0.0);
vec4 cr = texture2D(tDiffuse, vUv + offset);
vec4 cga = texture2D(tDiffuse, vUv);
vec4 cb = texture2D(tDiffuse, vUv - offset);
gl_FragColor = vec4(cr.r, cga.g, cb.b, cga.a);
}
`
}
// 胶片颗粒效果
const FilmGrainShader = {
uniforms: {
tDiffuse: { value: null },
time: { value: 0.0 },
amount: { value: 0.1 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float time;
uniform float amount;
varying vec2 vUv;
float random(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec4 color = texture2D(tDiffuse, vUv);
float grain = random(vUv * time) * amount;
color.rgb += grain;
gl_FragColor = color;
}
`
}
// 初始化场景
const initScene = () => {
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
// 创建相机
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 0, 5)
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// 添加一些发光物体
const geometry = new THREE.TorusKnotGeometry(1, 0.3, 128, 16)
const material = new THREE.MeshStandardMaterial({
color: 0x00ff00,
emissive: 0x00ff00,
emissiveIntensity: 0.5
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
// 添加光照
const light = new THREE.PointLight(0xffffff, 1)
light.position.set(5, 5, 5)
scene.add(light)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3)
scene.add(ambientLight)
// 初始化后期处理
initPostProcessing()
}
// 初始化后期处理
const initPostProcessing = () => {
// 创建 Composer
composer = new EffectComposer(renderer)
// 添加渲染通道
const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// Bloom 通道
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
bloomStrength.value,
0.4,
0.85
)
composer.addPass(bloomPass)
// 暗角通道
const vignettePass = new ShaderPass(VignetteShader)
vignettePass.uniforms.offset.value = 1.2
vignettePass.uniforms.darkness.value = 1.5
composer.addPass(vignettePass)
// 色差通道
const chromaticPass = new ShaderPass(ChromaticAberrationShader)
chromaticPass.uniforms.amount.value = 0.003
composer.addPass(chromaticPass)
// 胶片颗粒通道
const grainPass = new ShaderPass(FilmGrainShader)
grainPass.uniforms.amount.value = 0.05
composer.addPass(grainPass)
// 保存通道引用以便后续控制
composer.passes[1].enabled = effects.value.bloom
composer.passes[2].enabled = effects.value.vignette
composer.passes[3].enabled = effects.value.chromaticAberration
composer.passes[4].enabled = effects.value.filmGrain
}
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
// 旋转物体
if (scene.children[0]) {
scene.children[0].rotation.x += 0.01
scene.children[0].rotation.y += 0.01
}
// 更新胶片颗粒时间
if (composer.passes[4].enabled) {
composer.passes[4].uniforms.time.value += 0.01
}
controls.update()
// 使用 composer 渲染而不是直接渲染
composer.render()
}
// 监听效果开关
watch(() => effects.value.bloom, (value) => {
if (composer) composer.passes[1].enabled = value
})
watch(() => effects.value.vignette, (value) => {
if (composer) composer.passes[2].enabled = value
})
watch(() => effects.value.chromaticAberration, (value) => {
if (composer) composer.passes[3].enabled = value
})
watch(() => effects.value.filmGrain, (value) => {
if (composer) composer.passes[4].enabled = value
})
watch(bloomStrength, (value) => {
if (composer) {
composer.passes[1].strength = value
}
})
// 清理
const dispose = () => {
if (animationId) cancelAnimationFrame(animationId)
if (controls) controls.dispose()
if (renderer) {
renderer.dispose()
if (renderer.domElement.parentElement) {
renderer.domElement.parentElement.removeChild(renderer.domElement)
}
}
}
onMounted(() => {
initScene()
animate()
})
onUnmounted(() => {
dispose()
})
</script>
<style scoped>
.post-processing {
position: relative;
width: 100%;
height: 100vh;
}
.scene-container {
width: 100%;
height: 100%;
}
.effects-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 20px;
border-radius: 8px;
min-width: 250px;
}
h3 {
margin: 0 0 16px 0;
font-size: 14px;
color: #0ff;
text-transform: uppercase;
}
.effect-control {
margin-bottom: 16px;
}
.effect-control label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 14px;
cursor: pointer;
}
.effect-control input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.effect-control input[type="range"] {
width: 100%;
}
</style>
四、GPU 计算优化
4.1 粒子系统 GPU 优化
<!-- GPUParticles.vue -->
<template>
<div class="gpu-particles">
<div ref="containerRef" class="scene-container"></div>
<div class="particle-stats">
<div>粒子数量: {{ particleCount.toLocaleString() }}</div>
<div>FPS: {{ fps }}</div>
<div>性能: {{ performance }}</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const containerRef = ref(null)
let scene, camera, renderer, controls
let particleSystem
let animationId
const particleCount = ref(100000)
const fps = ref(0)
const performance = ref('优秀')
// GPU 粒子顶点着色器
const particleVertexShader = `
uniform float uTime;
uniform float uSize;
attribute float aScale;
attribute vec3 aVelocity;
attribute float aLifetime;
varying float vAlpha;
void main() {
// 计算粒子位置(基于速度和时间)
vec3 pos = position + aVelocity * mod(uTime, aLifetime);
// 重力效果
pos.y -= pow(mod(uTime, aLifetime) / aLifetime, 2.0) * 5.0;
// 计算生命周期透明度
float lifeProgress = mod(uTime, aLifetime) / aLifetime;
vAlpha = 1.0 - lifeProgress;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
// 粒子大小(带透视缩放)
gl_PointSize = uSize * aScale * (1.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`
// GPU 粒子片元着色器
const particleFragmentShader = `
uniform vec3 uColor;
uniform sampler2D uTexture;
varying float vAlpha;
void main() {
// 圆形粒子
vec2 center = gl_PointCoord - vec2(0.5);
float dist = length(center);
if (dist > 0.5) {
discard;
}
// 柔和边缘
float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
alpha *= vAlpha;
// 纹理采样(如果有)
vec4 texColor = texture2D(uTexture, gl_PointCoord);
vec3 color = uColor * texColor.rgb;
gl_FragColor = vec4(color, alpha * texColor.a);
}
`
// 初始化场景
const initScene = () => {
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
// 创建相机
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 0, 15)
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// 创建粒子系统
createParticleSystem()
}
// 创建粒子系统
const createParticleSystem = () => {
const count = particleCount.value
// 创建几何体
const geometry = new THREE.BufferGeometry()
// 位置
const positions = new Float32Array(count * 3)
// 速度
const velocities = new Float32Array(count * 3)
// 缩放
const scales = new Float32Array(count)
// 生命周期
const lifetimes = new Float32Array(count)
for (let i = 0; i < count; i++) {
const i3 = i * 3
// 从中心发射
positions[i3] = 0
positions[i3 + 1] = 0
positions[i3 + 2] = 0
// 随机速度
const theta = Math.random() * Math.PI * 2
const phi = Math.random() * Math.PI
const speed = 0.5 + Math.random() * 0.5
velocities[i3] = Math.sin(phi) * Math.cos(theta) * speed
velocities[i3 + 1] = Math.cos(phi) * speed
velocities[i3 + 2] = Math.sin(phi) * Math.sin(theta) * speed
// 随机大小
scales[i] = 0.5 + Math.random() * 0.5
// 随机生命周期
lifetimes[i] = 2 + Math.random() * 3
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setAttribute('aVelocity', new THREE.BufferAttribute(velocities, 3))
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1))
geometry.setAttribute('aLifetime', new THREE.BufferAttribute(lifetimes, 1))
// 创建纹理
const canvas = document.createElement('canvas')
canvas.width = 32
canvas.height = 32
const ctx = canvas.getContext('2d')
const gradient = ctx.createRadialGradient(16, 16, 0, 16, 16, 16)
gradient.addColorStop(0, 'rgba(255,255,255,1)')
gradient.addColorStop(0.5, 'rgba(255,255,255,0.5)')
gradient.addColorStop(1, 'rgba(255,255,255,0)')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 32, 32)
const texture = new THREE.CanvasTexture(canvas)
// 创建材质
const material = new THREE.ShaderMaterial({
vertexShader: particleVertexShader,
fragmentShader: particleFragmentShader,
uniforms: {
uTime: { value: 0 },
uSize: { value: 30 },
uColor: { value: new THREE.Color(0x00ffff) },
uTexture: { value: texture }
},
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
})
// 创建粒子系统
particleSystem = new THREE.Points(geometry, material)
scene.add(particleSystem)
}
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
// 更新时间
if (particleSystem) {
particleSystem.material.uniforms.uTime.value += 0.016
}
// 计算 FPS
const now = performance.now()
static lastTime = now
fps.value = Math.round(1000 / (now - lastTime))
lastTime = now
// 评估性能
if (fps.value >= 55) {
performance.value = '优秀'
} else if (fps.value >= 30) {
performance.value = '良好'
} else {
performance.value = '需优化'
}
controls.update()
renderer.render(scene, camera)
}
// 清理
const dispose = () => {
if (animationId) cancelAnimationFrame(animationId)
if (controls) controls.dispose()
if (particleSystem) {
particleSystem.geometry.dispose()
particleSystem.material.dispose()
}
if (renderer) {
renderer.dispose()
if (renderer.domElement.parentElement) {
renderer.domElement.parentElement.removeChild(renderer.domElement)
}
}
}
onMounted(() => {
initScene()
animate()
})
onUnmounted(() => {
dispose()
})
</script>
<style scoped>
.gpu-particles {
position: relative;
width: 100%;
height: 100vh;
background: #000;
}
.scene-container {
width: 100%;
height: 100%;
}
.particle-stats {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
color: #0ff;
padding: 16px;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
.particle-stats div {
margin-bottom: 8px;
}
</style>
五、真实项目经验总结
5.1 项目背景
我做过一个产品发布会的 3D 展示项目,需要实现很多酷炫的视觉效果,比如产品的全息投影、粒子爆炸、光线追踪等。这些效果用 Three.js 自带的材质都做不到,必须自己写着色器。
5.2 遇到的实际问题
问题1:着色器调试非常困难
着色器代码一旦出错,浏览器控制台只会显示"shader compilation failed",看不到具体哪里错了。我的做法是用颜色输出法调试:把中间计算结果映射到颜色输出,通过观察颜色判断计算是否正确。比如调试法线时,就把 normal.xyz 映射到 rgb,如果颜色不对说明法线计算有问题。
后来我还学会了用 Shader Toy 这个在线工具来原型开发。在 Shader Toy 上写好基本逻辑,确认没问题后再移植到项目里,能节省很多调试时间。
问题2:性能优化很关键
最开始我写的水面着色器用了很多纹理采样和复杂计算,结果帧率只有 20 fps。后来我做了几个优化:
- 把能在顶点着色器算的都移到顶点着色器,利用硬件插值
- 减少纹理采样次数,合并多个纹理到一张上
- 用查找表替代实时计算,比如菲涅尔反射用一张 1D 纹理存储
- 避免分支语句,改用
step、mix等函数
这些优化做完后,帧率提升到 50 fps 以上。
问题3:跨平台兼容性
同样的着色器代码,在我的 Mac 上运行正常,但在客户的 Windows 电脑上却报错。原来是精度问题,有些显卡对浮点数精度要求很严格。我在着色器开头加了 precision mediump float;,统一使用中等精度,兼容性问题就解决了。
还有就是不同设备对 GLSL 特性的支持不一样。比如有些设备不支持纹理数组,有些不支持某些内置函数。我的做法是做能力检测,根据设备支持情况加载不同版本的着色器。
5.3 技术心得
- 着色器编程需要扎实的数学基础,特别是线性代数和向量运算。不理解原理就只能照抄代码,遇到问题很难解决。
- 性能优化要从算法和硬件两个层面考虑。了解 GPU 的工作原理,知道什么操作昂贵、什么操作便宜,才能写出高效的着色器。
- 复用和模块化很重要。把常用的功能封装成函数库,比如噪声函数、光照计算、颜色空间转换等,可以大大提高开发效率。
- 多看优秀的着色器代码,学习别人的技巧。Shader Toy、GitHub、各种 3D 论坛都有很多高质量的着色器代码可以学习。
- 实际项目中,有时候用简单的技巧就能达到不错的效果,不一定非要用复杂的算法。比如假的反射可以用环境贴图,假的阴影可以用投影纹理,关键是视觉效果好就行。
这个项目让我对 WebGL 和着色器编程有了深入的理解,不仅学会了写代码,更重要的是理解了图形渲染的底层原理。现在看到各种酷炫的视觉效果,基本都能分析出是怎么实现的。