我们要理解Threejs材质的架构逻辑,可以先通过一个简单的材质示例来理解。
1-创建材质
1-1-示例简介
创建MeshMiniMaterial 材质,只要能渲染一个纯色三角形即可,其余全不考虑,比如深度检测、贴图、灯光等。

1-2-meshmini.glsl.js
直接下载three.js 源码,在里面写代码。
新建文件:src/renderers/shaders/ShaderLib/meshmini.glsl.js
ini
export const vertex = /* glsl */`
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
export const fragment = /* glsl */`
uniform vec3 diffuse;
void main() {
gl_FragColor = vec4( diffuse, 1.0 );
}
`;
vertex 是顶点着色器;fragment 是片元着色器。
ShaderLib 文件夹中存储的是着色器文件。
1-3-接入 ShaderLib
1.在ShaderChunk.js 中注册meshmini 的vertex 和fragment 。
javascript
import * as meshmini from './ShaderLib/meshmini.glsl.js';
export const ShaderChunk = {
...
meshmini_vert: meshmini.vertex,
meshmini_frag: meshmini.fragment,
};
ShaderChunk 是 Three.js 着色器代码片段的中央注册表。
2.在ShaderLib.js 中增加条目。
css
const ShaderLib = {
...
mini: {
uniforms: {
diffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) }
},
vertexShader: ShaderChunk.meshmini_vert,
fragmentShader: ShaderChunk.meshmini_frag
},
}
ShaderLib 是着色器库,其中的键,如mini,对应了Material.type。
ShaderLib 的uniforms中定义的是uniform 变量,MeshMiniMaterial 只用到了diffuse。
3.在WebGLPrograms.js 的 shaderIDs 映射中添加MeshMiniMaterial。
ini
const shaderIDs = {
...
MeshMiniMaterial: 'mini',
};
MeshMiniMaterial 对应材质类型Material.type,所以通过Material.type可以从shaderIDs 中获取mini,然后再通过mini 从ShaderLib 中获取相应的着色器。
1-4-创建MeshMiniMaterial
新建 src/materials/MeshMiniMaterial.js
kotlin
import { Material } from './Material.js';
import { Color } from '../math/Color.js';
/**
* A minimal material.
* Not affected by lights, fog, or textures.
*
* @augments Material
*/
class MeshMiniMaterial extends Material {
constructor( parameters ) {
super();
this.isMeshMiniMaterial = true;
this.type = 'MeshMiniMaterial';
/**
* Color of the material. Maps to the `diffuse` uniform in meshmini.glsl.js.
*
* @type {Color}
* @default (1,1,1)
*/
this.color = new Color( 0xffffff );
/**
* MeshMiniMaterial has no fog support in its shader.
*
* @type {boolean}
* @default false
*/
this.fog = false;
this.setValues( parameters );
}
copy( source ) {
super.copy( source );
this.color.copy( source.color );
this.fog = source.fog;
return this;
}
}
export { MeshMiniMaterial };
1-5-更新WebGLMaterials的uniform
在WebGLMaterials.js 中的refreshMaterialUniforms 方法中,根据MeshMiniMaterial材质更新相应的uniform 变量。
scss
function refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {
if ( material.isNodeMaterial ) {
...
} else if ( material.isMeshMiniMaterial ) {
uniforms.diffuse.value.copy( material.color );
}
}
1-6- JSON 转Material
在src/loaders/MaterialLoader.js 中添加MeshMinMaterial 。
scala
import { MeshMinMaterial } from '../materials/MeshMinMaterial.js';
class MaterialLoader extends Loader {
...
static createMaterialFromType( type ) {
const materialLib = {
...
MeshMiniMaterial
};
return new materialLib[ type ]();
}
}
MaterialLoader.js 是 Three.js 的材质反序列化加载器,负责把 JSON 格式的材质数据还原成运行时 Material 对象。它与 Material.toJSON() 配对,是场景/资源导入链路中的一环。
1-7-实例化MeshMiniMaterial
新建 examples/webgl_material_mini.html 文件。
ini
scene = new THREE.Scene();
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array( [
- 1.0, - 1.0, 0.0,
1.0, - 1.0, 0.0,
0.0, 1.0, 0.0
] );
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
const material = new THREE.MeshMiniMaterial( { color: 0x0000ff } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
配置examples/files.json
json
{
"webgl": [
...
"webgl_material_mini"
]
}
运行项目
arduino
npm ci
nom run start
打开webgl_material_mini.html,效果如下:

1-8-总结
meshmini.glsl.js 中定义了vertex 和fragment 。
在ShaderChunk 中集中注册着色器。
在ShaderLib 中合成具体材质所需的着色器,并定义fragment 所需的uniform变量。
MeshMiniMaterial 通过WebGLPrograms.js 中的shaderIDs 与着色器相关联。
2-chunk
chunk 是着色器的代码块。
three.js 中有很多代码是共用的,就像vue里的公共组件一样,所以就有了chunk。
chunk 可以被three.js 拼合成着色器代码。
我们以上节课的MeshMiniMaterial 材质为例,举个chunk 的例子。
2-1-chunk 示例
在src/renderers/shaders/ShaderLib/meshmini.glsl.js 的fragment 中引入一个chunk。
arduino
export const fragment = /* glsl */`
uniform vec3 diffuse;
void main() {
gl_FragColor = vec4( diffuse, 1.0 );
#include <colorspace_fragment>
}
`;
#include <> 是three.js 自定义的着色器编写规则,它可以引入外部定义的chunk。
#include <colorspace_fragment> 中的colorspace_fragment 就是一个chunk。
<colorspace_fragment> 的作用是把 gl_FragColor 从线性色域转换到渲染器输出色域(通常是 sRGB)。
<colorspace_fragment> 对应colorspace_fragment.glsl.js 文件。
ini
export default /* glsl */`
gl_FragColor = linearToOutputTexel( gl_FragColor );
`;
linearToOutputTexel 是由 WebGLProgram.js 在编译前动态注入到片元 shader 前缀里的。
scss
ShaderChunk[ 'colorspace_pars_fragment' ],
getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),
最终,效果如下:

此三角形比上节课的三角形的颜色浅,这就是colorspace_fragment 的作用。
在当前示例中有2个重点:
- #include 语法
- shader 前缀
接下来我们对其进行详解。
2-2-#include 语法
WebGL 的GLSL 无法从外部引入GLSL ,所以Three.js 在字符串中用#include 模拟像C 语言那样的#include 预处理能力
在WebGLProgram.js 中可以看到#include 的解析方法。
scss
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
//...
fragmentShader = resolveIncludes( fragmentShader );
}
php
// Resolve Includes
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
const shaderChunkMap = new Map();
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ];
if ( string === undefined ) {
const newInclude = shaderChunkMap.get( include );
if ( newInclude !== undefined ) {
string = ShaderChunk[ newInclude ];
warn( 'WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.', include, newInclude );
} else {
throw new Error( 'Can not resolve #include <' + include + '>' );
}
}
return resolveIncludes( string );
}
正则 includePattern
arduino
/^[ \t]*#include +<([\w\d./]+)>/gm
| 部分 | 含义 |
|---|---|
^[ \t]* |
行首,允许前导空格/Tab |
#include + |
字面量 #include 加至少一个空格 |
<([\w\d./]+)> |
尖括号内的 chunk 名,捕获为 include |
g |
全局替换 |
m |
多行模式,使 ^ 匹配每一行行首 |
能匹配例如:
arduino
#include <common>
#include <uv_pars_vertex>
不能匹配 #include "common"(Three.js 只用尖括号形式)。
resolveIncludes(string)
return string.replace( includePattern, includeReplacer );
对整段 shader 做一次扫描,每个 #include 都交给 includeReplacer 处理。
includeReplacer(match, include)
根据include,从ShaderChunk中取chunk,即ShaderChunk include
ShaderChunk 在 src/renderers/shaders/ShaderChunk.js,汇总了所有chunk。
关键:return resolveIncludes( string ) --- 对被插入的 chunk 再递归 做 include 解析
fragment
在上例中,#include <colorspace_fragment> 被替换后,fragment 如下:
ini
uniform vec3 diffuse;
void main() {
gl_FragColor = vec4( diffuse, 1.0 );
gl_FragColor = linearToOutputTexel( gl_FragColor );
}
在当前的fragment 中并没有linearToOutputTexel 方法,这是因为fragment 还会引入shader 前缀。
2-3-shader 前缀
对于非 RawShaderMaterial 的着色器,会将版本信息、shader 前缀与着色器拼在一起。
在WebGLProgram.js 中可以看到相关代码。
ini
prefixFragment = [
...
ShaderChunk[ 'colorspace_pars_fragment' ], // this code is required here because it is used by the various encoding/decoding function defined below
getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),
].filter( filterEmptyLine ).join( '\n' );
...
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;
ShaderChunk 'colorspace_pars_fragment' 对应colorspace_pars_fragment.glsl.js 文件
scss
export default /* glsl */`
vec4 LinearTransferOETF( in vec4 value ) {
return value;
}
vec4 sRGBTransferEOTF( in vec4 value ) {
return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );
}
vec4 sRGBTransferOETF( in vec4 value ) {
return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
}
`;
getTexelEncodingFunction 会根据 renderer.outputColorSpace 生成特定的着色代码。
javascript
function getTexelEncodingFunction( functionName, colorSpace ) {
const components = getEncodingComponents( colorSpace );
return [
`vec4 ${functionName}( vec4 value ) {`,
` return ${components[ 1 ]}( vec4( value.rgb * ${components[ 0 ]}, value.a ) );`,
'}',
].join( '\n' );
}
当renderer.outputColorSpace = THREE.SRGBColorSpace 时,会生成类似代码:
scss
vec4 linearToOutputTexel( vec4 value ) {
return sRGBTransferOETF( vec4( value.rgb * mat3(...), value.a ) );
}
总结
three.js中的着色器是通过#include <> 规则,将多个chunk 拼合而成的。
顶点着色器和片元着色器的构成如下:
ini
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;
这一章我们主要说的chunk在Material 中的架构原理,我们并没有细说着色器的内容,这个我们会作为另一快内容详解。