【Three.js 与 Shader】编写你的第一个自定义着色器,让模型拥有灵魂

前言

以前我觉得 Shader 是神仙才能看懂的东西,直到我发现它其实就是告诉 GPU"怎么画"的说明书。

两年前我第一次接触 Shader,看了一篇教程,开头就是 gl_FragColorvaryinguniform 这些天书一样的词。我心想:这玩意儿是人写的吗?

后来项目里有个需求:让模型边缘发蓝光。网上搜了一堆方案,最后发现除了自己写 Shader 别无他法。硬着头皮啃了一周,终于把第一个能跑起来的着色器怼出来了。运行起来的那一刻,模型边缘真的泛着蓝光,我当时激动得差点原地蹦起来。

原来 Shader 没那么可怕,它就像一本固定的菜谱,你告诉 GPU"颜色怎么混合、顶点怎么移动",它就能画出你想要的效果。

今天我就用最笨的方式,带你写三个最简单的自定义着色器。不扯虚的,代码直接跑,效果直接看。


一、Shader 是啥?

通俗点说,渲染一个 3D 模型要经过两个阶段:

  • 顶点着色器(Vertex Shader):负责处理每个顶点的位置、变换。好比整容医生,决定骨架长啥样。
  • 片元着色器(Fragment Shader):负责计算每个像素的颜色。好比化妆师,给每个点涂上什么颜色。

Three.js 默认的材质(比如 MeshStandardMaterial)内部已经有一套写好的着色器。我们用自定义着色器,就是替换掉默认的,自己控制这两个阶段。


二、Three.js 里的自定义材质

Three.js 提供了两种方式来写自定义着色器:

  • ShaderMaterial:自动帮你补全一些默认的 uniform 和 attribute,适合初学者。
  • RawShaderMaterial:完全自己控制,什么都不帮你补,适合进阶。

我们先从 ShaderMaterial 开始,省事。

基础结构

javascript 复制代码
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 },
    color: { value: new THREE.Color(0xffaa00) }
  },
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 color;
    void main() {
      gl_FragColor = vec4(color, 1.0);
    }
  `
});
  • uniforms:可以从 JavaScript 传给着色器的变量,每帧可以更新。
  • vertexShader:顶点着色器代码,字符串形式。
  • fragmentShader:片元着色器代码。

gl_Position 是顶点着色器必须输出的最终位置,gl_FragColor 是片元着色器必须输出的最终颜色。


三、第一个例子:让模型颜色随时间变化

我们用上面的基础结构,加一个 time uniform,让颜色在红色和蓝色之间循环。

javascript 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(2, 2, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

new OrbitControls(camera, renderer.domElement);

// 创建一个立方体
const geometry = new THREE.BoxGeometry(2, 2, 2);

// 自定义着色器材质
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float time;
    void main() {
      // 让红色分量在 0.5 到 1.0 之间变化,蓝色分量反向变化
      float r = 0.6 + 0.4 * sin(time);
      float b = 0.6 + 0.4 * cos(time);
      gl_FragColor = vec4(r, 0.2, b, 1.0);
    }
  `
});

const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

function animate() {
  requestAnimationFrame(animate);

  // 更新 uniform 中的时间
  material.uniforms.time.value += 0.05;

  renderer.render(scene, camera);
}
animate();

把这段代码贴进一个 HTML 文件里运行,你会看到一个立方体颜色在紫色系里渐变。


四、第二个例子:让顶点动起来(波浪效果)

现在我们来动顶点。让立方体的顶点按照正弦波上下浮动,像一块果冻。

javascript 复制代码
const geometry = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32); // 增加细分段数,让波浪更平滑

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    uniform float time;
    void main() {
      // 根据顶点原来的位置计算偏移量
      float offsetX = sin(position.y * 2.0 + time * 3.0) * 0.2;
      float offsetZ = cos(position.y * 2.0 + time * 2.0) * 0.2;
      vec3 newPosition = position + vec3(offsetX, 0.0, offsetZ);
      
      gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
    }
  `,
  fragmentShader: `
    void main() {
      gl_FragColor = vec4(0.6, 0.8, 1.0, 1.0);
    }
  `
});

这里的关键是:我们在顶点着色器里修改了 position,然后再进行 MVP 变换。注意 position 是模型局部坐标,计算时要小心不要破坏模型结构。

运行后,立方体的侧面会像波浪一样起伏。


五、第三个例子:简单边缘光

边缘光(Fresnel Effect)是让模型边缘发光的常见效果。原理是视线方向与法线方向越垂直(边缘),光越强。

我们需要在顶点着色器里把法线和视线方向传给片元着色器。

javascript 复制代码
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    varying vec3 vNormal;
    varying vec3 vViewPosition;
    
    void main() {
      vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
      vViewPosition = -mvPosition.xyz; // 指向相机的方向
      vNormal = normalize(normalMatrix * normal); // 将法线转换到视图空间
      
      gl_Position = projectionMatrix * mvPosition;
    }
  `,
  fragmentShader: `
    varying vec3 vNormal;
    varying vec3 vViewPosition;
    
    void main() {
      vec3 normal = normalize(vNormal);
      vec3 viewDir = normalize(vViewPosition);
      
      // 计算视线与法线的点积,越接近0(垂直)强度越大
      float fresnel = 1.0 - abs(dot(normal, viewDir));
      fresnel = pow(fresnel, 2.0); // 增强对比
      
      vec3 baseColor = vec3(0.3, 0.6, 1.0);
      vec3 edgeColor = vec3(0.8, 0.9, 1.0);
      
      vec3 finalColor = mix(baseColor, edgeColor, fresnel);
      
      gl_FragColor = vec4(finalColor, 1.0);
    }
  `
});

这个效果在球体上最明显,模型边缘会有一圈亮光,非常有科技感。


六、坑点总结

  1. 矩阵顺序projectionMatrix * modelViewMatrix * vec4(position, 1.0) 顺序不能错,Three.js 的矩阵是右乘,坐标从右向左变换。
  2. 法线变换 :不能用 modelViewMatrix 直接乘法线,要用 normalMatrix(它是 modelViewMatrix 的逆转置的左上3x3)。
  3. 变量精度 :移动设备上可能需要指定精度,比如 precision highp float; 放在着色器开头。
  4. uniform 更新 :记得在动画循环里更新 material.uniforms.xxx.value
  5. 调试技巧 :可以用 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 先确认片元着色器是否运行,用 gl_Position = vec4(position, 1.0);(跳过 MVP)看顶点原始位置。

七、进阶方向

这三个例子只是冰山一角。有了自定义着色器,你还可以做:

  • 流光效果
  • 溶解消失
  • 噪声纹理生成地形
  • 后处理滤镜

Three.js 官方提供了很多现成的着色器例子,在 examples/jsm/shaders/ 目录下。有空可以翻翻源码,看看大佬们怎么写。


互动

你写过最得意的 Shader 效果是啥?或者你在学习 Shader 时遇到过什么坑?评论区分享出来,咱们一起讨论 😏

下篇预告:【Three.js 后期处理进阶】用 Shader 实现自己的滤镜,让画面拥有电影质感

相关推荐
逻极5 小时前
Windows平台Ollama AMD GPU编译全攻略:基于ROCm 6.2的实战指南(附构建脚本)
人工智能·windows·gpu·amd·ollama
三维搬砖者8 小时前
AI 解密大厂 Three.js 三维引擎开发 03|从经纬度到三维世界的坐标解码
webgl·three.js
threelab4 天前
引擎案例分析 02|GeoLayer 大厂地理可视化方案深度拆解
javascript·3d·webgl
山海鲸可视化4 天前
【山海鲸功能演示】如何设置选中按钮的时候其他按钮切换为默认样式?
webgl·可视化·数据可视化·数据表格·搜索框
kadog4 天前
GraphX:基于 WebGL 区间算术的 GPU 加速隐函数绘图器
前端·javascript·数学建模·webgl
ct9785 天前
Cesium的Primitive API
gis·webgl·cesium
HyperAI超神经5 天前
在线教程丨华中科大与小红书 hi lab开源dots.mocr,SOTA级OCR模型完美还原文档结构,图形也能转 SVG
人工智能·深度学习·学习·机器学习·gpu·orc·vllm
d1z8887 天前
NCCL 测试完全指南:从概念到性能调优
gpu·nvidia·nccl
一马平川的大草原8 天前
基于Vue+Three.js实现三维油藏模型解析与可视化交互切割操作
vue.js·three.js·三维油藏模型
sin°θ_陈8 天前
前馈式3D Gaussian Splatting 研究地图(路线二):几何优先的前馈式 3DGS——前馈式 3DGS 如何重新拥抱多视图几何
深度学习·3d·webgl·三维重建·空间计算·3dgs·空间智能