文章目录
- ShaderMaterial
-
- 基本用法
- [ShaderMaterial 渲染流程](#ShaderMaterial 渲染流程)
- RawShaderMaterial
- onBeforeCompile
- [GLSL 常用内置变量](#GLSL 常用内置变量)
-
- 顶点着色器和片元着色器的区别
-
- [顶点着色器 vertexShader](#顶点着色器 vertexShader)
- [片元着色器 fragmentShader](#片元着色器 fragmentShader)
- 为什么叫片元着色器,而不是颜色着色器
-
- 片元是什么
- [为什么不直接叫 pixel](#为什么不直接叫 pixel)
- 为什么不叫颜色着色器
- 更准确的理解
- 顶点着色器里的内置输入
- 顶点着色器里的内置输出
- 片元着色器里的内置输入
- 片元着色器里的内置输出
- 片元着色器里的特殊指令
- [Three.js 中最常见的内置变量组合](#Three.js 中最常见的内置变量组合)
-
- [普通 Mesh](#普通 Mesh)
- [Points 粒子](#Points 粒子)
- 最常用内置变量速查
Three.js 里和着色器相关的内容,主要围绕这几类:
ShaderMaterialRawShaderMaterialonBeforeCompileuniformsattributesvarying- shader chunks
- 后期处理 shader pass
- WebGLRenderTarget
ShaderMaterial
ShaderMaterial 是 Three.js 里"自己写 GPU 渲染逻辑"的材质。
普通材质,比如:
MeshBasicMaterialMeshStandardMaterialPointsMaterial
都是 Three.js 帮你写好了 shader。
而 ShaderMaterial 是你自己提供:
vertexShaderfragmentShaderuniforms- 透明、混合、双面渲染等渲染配置
基本用法
javascript
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x7df7ff) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
`,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending,
});
uniforms
uniforms 是 THREE.ShaderMaterial 里用来把 JavaScript 侧的数据传进 GLSL 着色器的参数。
简单说:它是 CPU 传给 GPU shader 的"外部变量表"。
- uTime:可以每帧更新,用来做动画
- uColor:传颜色给片元着色器
- uTexture:传贴图给 shader
javascript
uniforms: {
uTime: { value: 0 },
uInnerColor: { value: new THREE.Color(0x7df7ff) },
}
shader 里接收:
javascript
uniform float uTime;
uniform vec3 uInnerColor;
每帧更新:
javascript
material.uniforms.uTime.value = seconds;
常见用途:
- 时间动画
- 颜色
- 纹理
- 鼠标位置
- 分辨率
- 速度、强度、透明度等参数
vertexShader
vertexShader 是顶点着色器。
它会对几何体的每个顶点执行一次。
主要负责:
- 计算顶点最终位置
- 修改顶点动画
- 设置
gl_Position - 把数据传给
fragmentShader
常见写法:
javascript
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
几个关键变量:
position
几何体自带的顶点位置。
uv
几何体自带的纹理坐标,通常范围是 0 ~ 1。
modelViewMatrix
模型矩阵和相机视图矩阵的组合。
projectionMatrix
相机投影矩阵。
gl_Position
顶点最终在屏幕裁剪空间中的位置,必须赋值。
fragmentShader
fragmentShader 是片元着色器。
可以简单理解成:它决定每个像素最终显示的颜色。
常见写法:
javascript
varying vec2 vUv;
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
输出颜色:
javascript
gl_FragColor = vec4(r, g, b, a);
其中:
rgba
范围通常是 0.0 ~ 1.0。
例如:
javascript
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
表示不透明红色。
varying
varying 用来把 vertexShader 里的数据传给 fragmentShader。是自定义的中转变量
顶点着色器里:
javascript
varying vec2 vUv;
void main() {
vUv = uv;
}
片元着色器里:
javascript
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv.x, vUv.y, 1.0, 1.0);
}
vUv 会在三角形表面自动插值。
也就是说,fragment shader 得到的不是某个单独顶点的 uv,而是当前像素位置对应的插值 uv。
transparent
javascript
transparent: true
开启透明渲染。
如果你的 fragmentShader 里输出了 alpha:
javascript
gl_FragColor = vec4(color, alpha);
那通常需要设置:
javascript
transparent: true
否则透明度可能不会按预期显示。
适合:
- 光环
- 粒子
- 能量面
- 玻璃
- 半透明 UI 面板
depthWrite
javascript
depthWrite: false
表示这个材质不写入深度缓冲。
适合半透明和发光材质。
如果透明物体写入深度,可能出现:
- 前面的透明面挡住后面的透明面
- 粒子出现黑块
- 发光层次断裂
- 后方透明物体不显示
所以粒子、光环、传送门常用:
javascript
depthWrite: false
side
javascript
side: THREE.DoubleSide
表示正反两面都渲染。
常见取值:
javascript
THREE.FrontSide
只渲染正面,默认值。
javascript
THREE.BackSide
只渲染背面。
javascript
THREE.DoubleSide
正反面都渲染。
传送门的圆面、能量面、平面特效通常会用:
javascript
side: THREE.DoubleSide
因为从背后或侧面看时,也希望它可见。
blending
javascript
blending: THREE.AdditiveBlending
设置颜色混合方式。
AdditiveBlending 是加法混合。
效果是:颜色叠加后会越来越亮。
适合:
- 发光
- 火焰
- 粒子
- 星星
- 能量环
- 激光
- 魔法特效
普通透明混合更像"半透明塑料片"。
加法混合更像"光"。
attributes
attribute 是每个顶点自己的数据。
Three.js 内置常见 attribute:
positionnormaluvcolor
自定义 attribute:
javascript
const count = 1000;
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
sizes[i] = Math.random() * 10;
}
geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
shader 中使用:
javascript
attribute float aSize;
void main() {
gl_PointSize = aSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
适合:
- 每个粒子的尺寸
- 每个顶点的随机值
- 每个点的速度
- 每个点的相位
- 每个实例的参数
注意:
attribute 在 vertexShader 中使用,不能直接在 fragmentShader 中使用。
如果片元阶段也需要它,要通过 varying 传过去。
ShaderMaterial 渲染流程
javascript
JavaScript
创建 geometry
创建 ShaderMaterial
设置 uniforms
每帧更新 uniforms
vertexShader
每个顶点执行一次
计算顶点位置
传递 varying 数据
fragmentShader
每个像素执行一次
根据 uv、颜色、时间等计算最终颜色
renderer
根据 transparent / depthWrite / side / blending 等配置
把结果画到 canvas
ShaderMaterial 是 Three.js 里自定义视觉效果的入口。
它让你自己决定:
- 顶点怎么动
- 像素怎么上色
- 透明怎么处理
- 发光怎么叠加
- 动画参数怎么传入 GPU
普通材质适合常规物体。
ShaderMaterial 适合:
- 粒子
- 波纹
- 能量环
- 水面
- 火焰
- 扫描线
- 传送门
- 自定义后期视觉效果
RawShaderMaterial
RawShaderMaterial 比 ShaderMaterial 更底层。
区别是:
ShaderMaterial 会帮你注入 Three.js 的内置变量、宏和 shader chunk。
RawShaderMaterial 不会自动注入太多东西,你需要自己写得更完整。
基本写法:
javascript
const material = new THREE.RawShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0xff0000) },
},
vertexShader: `
precision mediump float;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
precision mediump float;
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
`,
});
适合:
- 更接近原生 WebGL 的写法
- 想完全控制 shader 输入
- 学习底层 WebGL shader
- 避免 Three.js 自动注入内容
一般项目里优先用 ShaderMaterial,除非你明确需要更底层控制。
onBeforeCompile
onBeforeCompile 可以修改 Three.js 内置材质的 shader。
它适合这种场景:
你想保留 MeshStandardMaterial 的光照、阴影、PBR、贴图能力,但又想插入一点自定义 shader 逻辑。
例如给标准材质加一个时间 uniform:
javascript
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.4,
metalness: 0.2,
});
material.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 };
shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
`
#include <common>
uniform float uTime;
`,
);
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
transformed.y += sin(position.x * 4.0 + uTime) * 0.2;
`,
);
material.userData.shader = shader;
};
每帧更新:
javascript
if (material.userData.shader) {
material.userData.shader.uniforms.uTime.value = elapsedTime;
}
适合:
- 基于标准材质做轻微变形
- 保留灯光和阴影
- 给 PBR 材质加扫描线、波动、溶解等效果
- 不想从零写完整 shader
GLSL 常用内置变量
顶点着色器和片元着色器的区别
在 Three.js / WebGL 里,一个物体最终显示到屏幕上,大致会经过两个核心阶段:
- 顶点着色器 vertexShader
- 片元着色器 fragmentShader
它们运行的位置、次数、职责都不一样。
顶点着色器 vertexShader
顶点着色器是处理"顶点"的。
几何体里有多少个顶点,顶点着色器就大致执行多少次。
比如一个平面有 4 个顶点:
javascript
const geometry = new THREE.PlaneGeometry(1, 1);
顶点着色器主要处理这 4 个顶点的位置。
顶点着色器最重要的任务是输出:
javascript
gl_Position
也就是当前顶点最终在屏幕上的位置。
典型写法:
javascript
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
顶点着色器适合做:
- 顶点位置变形
- 波浪起伏
- 粒子位置变化
- 点粒子大小
- 把
uv / color / normal等数据传给片元着色器
例如让平面上下起伏:
javascript
uniform float uTime;
void main() {
vec3 transformed = position;
transformed.y += sin(position.x * 4.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
片元着色器 fragmentShader
片元着色器是处理"片元"的。
可以先粗略理解成:处理屏幕上的每个像素。
一个三角形被投影到屏幕后,会覆盖很多像素区域。
这些像素区域里的每个片元,都会执行片元着色器。
片元着色器最重要的任务是输出颜色:
javascript
gl_FragColor
典型写法:
javascript
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
表示输出红色。
片元着色器适合做:
- 颜色
- 透明度
- 贴图采样
- 渐变
- 波纹
- 噪声
- 扫描线
- 发光边缘
- 溶解效果
- 圆形粒子裁剪
例如根据 uv 做渐变:
javascript
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv.x, vUv.y, 1.0, 1.0);
}
总结:
顶点着色器:
bash
处理顶点
决定位置
决定形状怎么变
执行次数相对少
必须输出 gl_Position
片元着色器:
bash
处理片元/像素
决定颜色
决定透明、贴图、图案
执行次数通常很多
输出 gl_FragColor
为什么叫片元着色器,而不是颜色着色器
片元是什么
片元不是最终像素,但很接近像素。
渲染流程大概是:
javascript
顶点
↓
三角形
↓
光栅化
↓
片元 fragments
↓
片元着色器
↓
深度测试 / 模板测试 / 混合
↓
最终像素
光栅化阶段会把三角形覆盖到屏幕上的区域切成很多小候选单位。
这些候选单位就叫:
javascript
fragment / 片元
为什么不直接叫 pixel
因为片元还不一定会成为最终像素。
一个片元后面还可能经历:
- 深度测试:被前面的物体挡住,就不会显示
- 模板测试:不符合 stencil 条件就丢弃
discard:shader 内主动丢弃- blending:和已有颜色混合
- MSAA 多重采样:一个像素里可能有多个采样片元
所以片元着色器输出的是:
javascript
这个候选片元应该是什么颜色、透明度、深度等
但它还不是屏幕最终结果。
为什么不叫颜色着色器
因为 fragment shader 不只决定颜色,还可以处理:
- 透明度
- 丢弃片元
discard - 贴图采样
- 法线贴图
- 阴影
- 光照结果
- 雾效
- 深度相关效果
- 屏幕空间坐标
- 多渲染目标输出
- 后期 pass 中的整屏像素处理
比如:
javascript
if (alpha < 0.01) {
discard;
}
这不是"上色",而是直接让这个片元不存在。
再比如:
javascript
vec4 texColor = texture2D(uTexture, vUv);
gl_FragColor = texColor;
它是在决定这个片元从纹理哪里采样、透明度是多少、最后怎么参与混合。
更准确的理解
javascript
fragment shader = 片元着色器
它处理的是"光栅化后生成的片元"。
颜色只是它最常见、最直观的输出之一。
顶点着色器里的内置输入
内置变量的特点是:
它们不是你自己定义的变量名,而是 GLSL/WebGL 已经规定好的名字。
gl_VertexID
WebGL2 中可用。
表示当前顶点的编号。
javascript
int id = gl_VertexID;
用途:
- 根据顶点编号生成图案
- 不依赖 attribute 生成简单几何逻辑
WebGL1 中通常不能用。
顶点着色器里的内置输出
gl_Position
顶点着色器最重要的内置输出变量。
它表示当前顶点最终在裁剪空间中的位置。
javascript
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
必须赋值。
如果不赋值,GPU 不知道这个顶点应该画到哪里。
类型:
javascript
vec4
含义:
javascript
gl_Position = vec4(x, y, z, w);
gl_PointSize
只在渲染点粒子时常用。
它控制当前点的屏幕尺寸。
javascript
gl_PointSize = 8.0;
用于:
THREE.Points- 粒子
- 星点
- 火花
- 雪花
类型:
javascript
float
如果你渲染的是 Mesh,通常用不到它。
片元着色器里的内置输入
gl_FragCoord
表示当前片元在屏幕上的坐标。
javascript
vec2 screenUv = gl_FragCoord.xy / uResolution;
类型:
javascript
vec4
常用:
javascript
gl_FragCoord.x
gl_FragCoord.y
gl_FragCoord.z
gl_FragCoord.w
常见用途:
- 屏幕空间效果
- 后期处理
- 像素扫描线
- 根据屏幕坐标生成图案
gl_PointCoord
只在绘制 Points 时有意义。
表示当前片元在点精灵内部的位置。
范围通常是:
javascript
0.0 ~ 1.0
比如把方形点裁成圆形:
javascript
float d = distance(gl_PointCoord, vec2(0.5));
if (d > 0.5) {
discard;
}
gl_FragColor = vec4(1.0);
类型:
javascript
vec2
用于:
- 圆形粒子
- 粒子贴图
- 星点光斑
- 火花效果
gl_FrontFacing
表示当前片元来自正面还是背面。
类型:
javascript
bool
示例:
javascript
if (gl_FrontFacing) {
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
} else {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
用途:
- 正反面不同颜色
- 调试法线方向
- 双面材质效果
片元着色器里的内置输出
gl_FragColor
WebGL1 常用。
表示当前片元最终输出颜色。
javascript
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
类型:
javascript
vec4
四个分量:
javascript
vec4(r, g, b, a)
范围通常是:
javascript
0.0 ~ 1.0
注意:
在 WebGL2 / GLSL 300 es 中,通常不用 gl_FragColor,而是自定义输出变量。
自定义片元输出变量
WebGL2 / GLSL 300 es 写法。
javascript
out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
Three.js 默认很多示例仍然是 WebGL1 风格,所以经常看到:
javascript
gl_FragColor = vec4(...);
片元着色器里的特殊指令
discard
discard 不是变量,是内置指令。
它表示丢弃当前片元,不绘制这个像素。
javascript
if (alpha < 0.01) {
discard;
}
用途:
- 裁剪透明边缘
- 做圆形粒子
- 溶解效果
- mask 效果
Three.js 中最常见的内置变量组合
普通 Mesh
顶点着色器:
javascript
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
片元着色器:
javascript
void main() {
gl_FragColor = vec4(1.0);
}
核心内置变量:
gl_Positiongl_FragColor
Points 粒子
顶点着色器:
javascript
void main() {
vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = 8.0;
}
片元着色器:
javascript
void main() {
float d = distance(gl_PointCoord, vec2(0.5));
if (d > 0.5) {
discard;
}
gl_FragColor = vec4(1.0);
}
核心内置变量:
gl_Positiongl_PointSizegl_PointCoordgl_FragColordiscard
最常用内置变量速查
| 名称 | 阶段 | 类型 | 作用 |
|---|---|---|---|
gl_Position |
vertexShader | vec4 |
当前顶点最终位置 |
gl_PointSize |
vertexShader | float |
点粒子的屏幕尺寸 |
gl_FragCoord |
fragmentShader | vec4 |
当前片元的屏幕坐标 |
gl_PointCoord |
fragmentShader | vec2 |
点粒子内部坐标 |
gl_FrontFacing |
fragmentShader | bool |
当前片元是否来自正面 |
gl_FragColor |
fragmentShader | vec4 |
当前片元输出颜色 |
discard |
fragmentShader | 指令 | 丢弃当前片元 |