返回笔记首页

WebGL 着色器编程完整指南

主题配置

一、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.vertwater.frag。在 Vue 组件里,我用 import 导入这些文件,或者直接写成模板字符串。

然后创建 ShaderMaterial,传入顶点着色器和片元着色器代码,还有 uniforms(着色器的输入变量)。uniforms 可以传递时间、鼠标位置、纹理等数据给着色器。

javascript
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 完整技术实现

基础着色器组件

vue
<!-- 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 水面材质实现

vue
<!-- 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 后期处理管线

vue
<!-- 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 优化

vue
<!-- 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。后来我做了几个优化:

  1. 把能在顶点着色器算的都移到顶点着色器,利用硬件插值
  2. 减少纹理采样次数,合并多个纹理到一张上
  3. 用查找表替代实时计算,比如菲涅尔反射用一张 1D 纹理存储
  4. 避免分支语句,改用 stepmix 等函数

这些优化做完后,帧率提升到 50 fps 以上。

问题3:跨平台兼容性

同样的着色器代码,在我的 Mac 上运行正常,但在客户的 Windows 电脑上却报错。原来是精度问题,有些显卡对浮点数精度要求很严格。我在着色器开头加了 precision mediump float;,统一使用中等精度,兼容性问题就解决了。

还有就是不同设备对 GLSL 特性的支持不一样。比如有些设备不支持纹理数组,有些不支持某些内置函数。我的做法是做能力检测,根据设备支持情况加载不同版本的着色器。

5.3 技术心得

  1. 着色器编程需要扎实的数学基础,特别是线性代数和向量运算。不理解原理就只能照抄代码,遇到问题很难解决。
  2. 性能优化要从算法和硬件两个层面考虑。了解 GPU 的工作原理,知道什么操作昂贵、什么操作便宜,才能写出高效的着色器。
  3. 复用和模块化很重要。把常用的功能封装成函数库,比如噪声函数、光照计算、颜色空间转换等,可以大大提高开发效率。
  4. 多看优秀的着色器代码,学习别人的技巧。Shader Toy、GitHub、各种 3D 论坛都有很多高质量的着色器代码可以学习。
  5. 实际项目中,有时候用简单的技巧就能达到不错的效果,不一定非要用复杂的算法。比如假的反射可以用环境贴图,假的阴影可以用投影纹理,关键是视觉效果好就行。

这个项目让我对 WebGL 和着色器编程有了深入的理解,不仅学会了写代码,更重要的是理解了图形渲染的底层原理。现在看到各种酷炫的视觉效果,基本都能分析出是怎么实现的。