要理解three.js的架构思路,需要先知道webgl 对象与three.js 对象的对应关系。
1-webgl 中的对象
以webgl 绘制纯色三角形为例。
ini
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - pure WebGL triangle </title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script id="vertexShader" type="x-shader/x-vertex">
#version 300 es
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
in vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
#version 300 es
layout(location = 0) out highp vec4 pc_fragColor;
precision highp float;
uniform vec3 diffuse;
void main() {
pc_fragColor = vec4( diffuse, 1.0 );
}
</script>
<script type="module">
import * as THREE from 'three';
// 设置绘制缓冲区尺寸与 CSS 显示尺寸
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
// WebGL 上下文
const gl = canvas.getContext('webgl2');
// 从 <script> 标签读取 GLSL 源码;trim() 去掉首尾空白,确保 #version 在第一行
const vsSource = document.getElementById('vertexShader').innerText.trim();
const fsSource = document.getElementById('fragmentShader').innerText.trim();
// 声明投影矩阵和模型视图矩阵
const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.z = 4;
camera.updateMatrixWorld();
camera.updateProjectionMatrix();
const projectionMatrix = camera.projectionMatrix.clone();
const modelMatrix = new THREE.Matrix4(); // 单位矩阵,三角形位于世界原点
const modelViewMatrix = camera.matrixWorldInverse.clone().multiply( modelMatrix );
// 创建Program 对象,关联 vertexShader 和 fragmentShader,并链接和使用Program对象
const program = gl.createProgram();
const vertexShader = compileShader( gl, gl.VERTEX_SHADER, vsSource );
const fragmentShader = compileShader( gl, gl.FRAGMENT_SHADER, fsSource );
gl.attachShader( program, vertexShader );
gl.attachShader( program, fragmentShader );
gl.linkProgram( program );
gl.useProgram( program );
function compileShader( gl, type, source ) {
const shader = gl.createShader( type );
gl.shaderSource( shader, source );
gl.compileShader( shader );
return shader;
}
// 视口映射到整个 canvas(坐标系左下角为原点)
gl.viewport( 0, 0, canvas.width, canvas.height );
// 创建 VBO(Vertex Buffer Object),在已绑定的 VAO 上配置 position 属性
const vertices = new Float32Array( [
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
0.0, 1.0, 0.0
] );
const vertexBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertexBuffer );
gl.bufferData( gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW );
// 创建 VAO(Vertex Array Object):缓存「position 怎么读」的配置,绘制时 bind 即可恢复
const vao = gl.createVertexArray();
gl.bindVertexArray( vao );
// 查询 attribute 在 program 中的 location(与 shader 中 in vec3 position 对应)
const a_Position = gl.getAttribLocation( program, 'position' );
// vertexAttribPointer(index, size, type, normalized, stride, offset)
// size=3:每顶点读 x/y/z;stride=3*4:每顶点占 12 字节;offset=0:从 buffer 开头读
// 以下 enable / pointer 调用会写入当前 VAO,而非全局默认状态
gl.vertexAttribPointer( a_Position, 3, gl.FLOAT, false, 3 * 4, 0 );
gl.enableVertexAttribArray( a_Position );
// 解绑 VBO(可选);避免后续 bufferData 误操作当前 buffer
gl.bindBuffer( gl.ARRAY_BUFFER, null );
// 解绑 VAO(可选);避免后续 bindVertexArray 误操作当前 vao
gl.bindVertexArray( null );
// 向着色器传入模型视图矩阵和投影矩阵
const u_modelViewMatrix = gl.getUniformLocation( program, 'modelViewMatrix' );
gl.uniformMatrix4fv( u_modelViewMatrix, false, modelViewMatrix.elements );
const u_projectionMatrix = gl.getUniformLocation( program, 'projectionMatrix' );
gl.uniformMatrix4fv( u_projectionMatrix, false, projectionMatrix.elements );
// 向着色器传入三角形颜色
const u_diffuse = gl.getUniformLocation( program, 'diffuse' );
gl.uniform3fv( u_diffuse, new Float32Array( [ 0.0, 0.0, 1.0 ] ) ); // 蓝色
// 绘制背景色
gl.clearColor( 1.0, 1.0, 0.0, 1.0 ); // 黄色
gl.clear( gl.COLOR_BUFFER_BIT ); // 清屏
// 绘制三角形:先 bind VAO,一次性恢复顶点属性配置,再 draw
gl.bindVertexArray( vao );
gl.drawArrays( gl.TRIANGLES, 0, 3 ); // 从第 0 个顶点起,绘制 3 个顶点(1 个三角形)
gl.bindVertexArray( null );
// 画完后就释放 GPU 资源(对应 Three.js WebGLBindingStates 中的 deleteVertexArray)
gl.deleteVertexArray( vao );
gl.deleteBuffer( vertexBuffer );
gl.deleteProgram( program );
gl.deleteShader( vertexShader );
gl.deleteShader( fragmentShader );
</script>
</body>
</html>
vertexShader 源文件
ini
#version 300 es
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
in vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
fragmentShader 源文件
ini
#version 300 es
layout(location = 0) out highp vec4 pc_fragColor;
precision highp float;
uniform vec3 diffuse;
void main() {
pc_fragColor = vec4( diffuse, 1.0 );
}
vertex shader
scss
gl.createShader( gl.VERTEX_SHADER)
fragment shader
scss
gl.createShader( gl.FRAGMENT_SHADER)
shader program
ini
const program = gl.createProgram();
VBO(Vertex Buffer Object)
在GPU中存储Vertex 数据的buffer。
ini
const vertexBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertexBuffer );
gl.bufferData( gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW );
VAO(Vertex Array Object)
program 中的attribute 变量如何从GPU的buffer 中读取Vertex 数据。
ini
const vao = gl.createVertexArray();
gl.bindVertexArray( vao );
const a_Position = gl.getAttribLocation( program, 'position' );
gl.vertexAttribPointer( a_Position, 3, gl.FLOAT, false, 3 * 4, 0 );
gl.enableVertexAttribArray( a_Position );
AttribLocation
attribute在 GPU 上的"位置句柄",用于后续从VBO中,根据VAO,传入vertex 数据。
scss
gl.getAttribLocation( program, name )
uniformLocation
uniform 在 GPU 上的"位置句柄",用于后续上传uniform值
ini
gl.getUniformLocation( program, name );
2-three.js 对象与Webgl 的对应关系
用three.js 绘制三角形的代码如下:
ini
import * as THREE from 'three';
const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.z = 4;
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xffff00 );
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.MeshBasicMaterial( { color: 0x00acec, side: THREE.DoubleSide } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.render( scene, camera );
document.body.appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize );
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
PerspectiveCamera
具有相机相关的uniform 数据,如视图矩阵、投影矩阵。
Scene
包含Mesh,需遍历子元素,从Mesh 中寻找着色相关的数据。
Mesh
具有模型矩阵相关的uniform 数据。包含BufferGeometry 和Material。
BufferGeometry
具有attribute 相关的数据。
Material:存储uniform 相关的数据,是创建shader program的重要依据。
Material与program 的对应关系:
- Material不同则program 不同。
- 一个Material,因为参数不同,可能对应多个program。比如
transparent=true/false会对应两份独立的 Program。
WebGLRenderer
统筹管理整体的渲染流程。
看懂WebGLRenderer,可以让大家对three.js 的架构原理有一个更深入的理解。
3-关于shader program 的关键模块
我们可以以WebGLRenderer 为切入点,围绕shader program,循着webgl 的渲染流程,查看three.js中与之相对于的关键模块。
ShaderLib
定义了多种着色器的预定义文件。
css
const ShaderLib = {
basic: {
uniforms: /*@__PURE__*/ mergeUniforms( [
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.fog
] ),
vertexShader: ShaderChunk.meshbasic_vert,
fragmentShader: ShaderChunk.meshbasic_frag
},
....
WebGLProgram 会根据此文件创建shader program。
WebGLProgram
1.构造函数
创建并缓存 shader program
ini
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
const program = gl.createProgram();
...
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;
const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
gl.linkProgram( program );
2.onFirstUse
创建并缓存program中的所有attribLocation和uniformLocation
ini
function onFirstUse( self ) {
...
gl.deleteShader( glVertexShader );
gl.deleteShader( glFragmentShader );
cachedUniforms = new WebGLUniforms( gl, program );
cachedAttributes = fetchAttributeLocations( gl, program );
}
function fetchAttributeLocations( gl, program ) {
const attributes = {};
const n = gl.getProgramParameter( program, gl.ACTIVE_ATTRIBUTES );
for ( let i = 0; i < n; i ++ ) {
const info = gl.getActiveAttrib( program, i );
const name = info.name;
let locationSize = 1;
if ( info.type === gl.FLOAT_MAT2 ) locationSize = 2;
if ( info.type === gl.FLOAT_MAT3 ) locationSize = 3;
if ( info.type === gl.FLOAT_MAT4 ) locationSize = 4;
attributes[ name ] = {
type: info.type,
location: gl.getAttribLocation( program, name ),
locationSize: locationSize
};
}
return attributes;
}
3.WebGLUniforms
WebGLUniforms 的构造函数中内置了获取并缓存program中的所有uniformLocation 方法。
ini
class WebGLUniforms {
constructor( gl, program ) {
this.seq = [];
this.map = {};
const n = gl.getProgramParameter( program, gl.ACTIVE_UNIFORMS );
for ( let i = 0; i < n; ++ i ) {
const info = gl.getActiveUniform( program, i ),
addr = gl.getUniformLocation( program, info.name );
parseUniform( info, addr, this );
}
WebGLUniforms 中还有其它关于uniform 的方法,稍后详解。
WebGLAttributes
1.createBuffer
创建并缓冲VBO
ini
function WebGLAttributes( gl ) {
const buffers = new WeakMap();
function createBuffer( attribute, bufferType ) {
const array = attribute.array;
const usage = attribute.usage;
const size = array.byteLength;
const buffer = gl.createBuffer();
gl.bindBuffer( bufferType, buffer );
gl.bufferData( bufferType, array, usage );
...
return {
buffer: buffer,
type: type,
bytesPerElement: array.BYTES_PER_ELEMENT,
version: attribute.version,
size: size
};
}
2.updateBuffer
更新VBO
ini
function updateBuffer( buffer, attribute, bufferType ) {
const array = attribute.array;
const updateRanges = attribute.updateRanges;
gl.bindBuffer( bufferType, buffer );
if ( updateRanges.length === 0 ) {
// Not using update ranges
gl.bufferSubData( bufferType, 0, array );
}
WebGLBindingStates
1.getBindingState
创建并缓存 VAO
ini
function getBindingState( object, geometry, program, material ) {
const wireframe = ( material.wireframe === true );
let objectMap = bindingStates[ geometry.id ];
if ( objectMap === undefined ) {
objectMap = {};
bindingStates[ geometry.id ] = objectMap;
}
// Each InstancedMesh requires unique binding states because it contains instanced attributes.
const objectId = ( object.isInstancedMesh === true ) ? object.id : 0;
let programMap = objectMap[ objectId ];
if ( programMap === undefined ) {
programMap = {};
objectMap[ objectId ] = programMap;
}
let stateMap = programMap[ program.id ];
if ( stateMap === undefined ) {
stateMap = {};
programMap[ program.id ] = stateMap;
}
let state = stateMap[ wireframe ];
if ( state === undefined ) {
state = createBindingState( createVertexArrayObject() );
stateMap[ wireframe ] = state;
}
return state;
}
function createVertexArrayObject() {
return gl.createVertexArray();
}
function createBindingState( vao ) {
const newAttributes = [];
const enabledAttributes = [];
const attributeDivisors = [];
for ( let i = 0; i < maxVertexAttributes; i ++ ) {
newAttributes[ i ] = 0;
enabledAttributes[ i ] = 0;
attributeDivisors[ i ] = 0;
}
return {
// for backward compatibility on non-VAO support browser
geometry: null,
program: null,
wireframe: false,
newAttributes: newAttributes,
enabledAttributes: enabledAttributes,
attributeDivisors: attributeDivisors,
object: vao,
attributes: {},
index: null
};
}
2.saveCache
根据shader program 中的attribute 变量名,从BufferGeometry.attributes 中获取的attribute 数据。
ini
function saveCache( object, geometry, program, index ) {
const cache = {};
const attributes = geometry.attributes;
let attributesNum = 0;
const programAttributes = program.getAttributes();
for ( const name in programAttributes ) {
const programAttribute = programAttributes[ name ];
if ( programAttribute.location >= 0 ) {
let attribute = attributes[ name ];
...
}
3.setupVertexAttributes
将BufferGeometry 中的attribute 数据更新到VBO。
css
function setupVertexAttributes( object, material, program, geometry ) {
...
gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
for ( let i = 0; i < programAttribute.locationSize; i ++ ) {
vertexAttribPointer(
programAttribute.location + i,
size / programAttribute.locationSize,
type,
normalized,
size * bytesPerElement,
( size / programAttribute.locationSize ) * i * bytesPerElement,
integer
);
}
WebGLUniforms
管理Uniform 数据,提供与之相关的操作方法。
Uniform 数据的来源有多种,比如相机视图投影矩阵、物体的模型矩阵、材质、灯光等。
1.构造函数
内置了获取并缓存program中的所有uniformLocation 方法。
ini
class WebGLUniforms {
constructor( gl, program ) {
this.seq = [];
this.map = {};
const n = gl.getProgramParameter( program, gl.ACTIVE_UNIFORMS );
for ( let i = 0; i < n; ++ i ) {
const info = gl.getActiveUniform( program, i ),
addr = gl.getUniformLocation( program, info.name );
parseUniform( info, addr, this );
}
2.setValue
根据uniform 变量的名称,将RAM中的uniform 数据传到GPU的shader program 中。
javascript
setValue( gl, name, value, textures ) {
const u = this.map[ name ];
if ( u !== undefined ) u.setValue( gl, value, textures );
}
SingleUniform
对每个uniform 相关的数据进行了封装。
ini
class SingleUniform {
constructor( id, activeInfo, addr ) {
this.id = id;
this.addr = addr;
this.cache = [];
this.type = activeInfo.type;
this.setValue = getSingularSetter( activeInfo.type );
// this.path = activeInfo.name; // DEBUG
}
}
setValue 方法可以根据不同的uniform 数据,使用不同的方法,将数据传递到GPU的shader program 中。
小结
上面的这些模块都是与以下对象的创建、缓存、更新等操作相关的:
- vertex shader
- fragment shader
- shader program
- VBO
- VBO
- uniform
接下来我们说一下其它的一些周边模块。
4-WebGLRenderer 中的其它模块
WebGLPrograms
shader program 管理器,根据「材质 + 几何体 + 场景 + 灯光」决定用哪个program 。
主要运行流程:
scss
材质/物体/场景/灯光
↓
getParameters() ← 收集所有影响 shader 的条件
↓
getProgramCacheKey() ← 生成唯一缓存键
↓
acquireProgram() ← 命中缓存则复用,否则 new WebGLProgram
WebGLGeometries
WebGLGeometries 负责管理 BufferGeometry 的生命周期,并通过依赖注入调用WebGLAttributes。
WebGLAttributes 管理的是底层 WebGL buffer。
渲染时的调用链:
scss
WebGLRenderer.render()
└── WebGLObjects.update(object)
├── geometries.get(object, geometry) // 注册 + 监听 dispose
└── geometries.update(geometry) // 每帧一次
└── attributes.update(attr, ARRAY_BUFFER) ← 委托
└── WebGLBindingStates.setup(...)
├── attributes.update(index, ELEMENT_ARRAY_BUFFER)
└── attributes.get(attr).buffer // 绑定 VAO / 顶点指针
由上可知:渲染时,WebGLGeometries 会先更新attributes,attributes 会再将attribute 数据更新到VBO。
WebGLObjects.update() 方法会调用WebGLGeometries.update() 方法。
csharp
function update( object ) {
const buffergeometry = geometries.get( object, geometry );
if ( updateMap.get( buffergeometry ) !== frame ) {
geometries.update( buffergeometry );
updateMap.set( buffergeometry, frame );
}
...
WebGLMaterials
主要负责把Material 中的uniform 数据同步到ShaderLib中的uniforms 中。
scss
function refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {
if ( material.isNodeMaterial ) {
material.uniformsNeedUpdate = false;
} else if ( material.isMeshBasicMaterial ) {
refreshUniformsCommon( uniforms, material );
}
...
}
function refreshUniformsCommon( uniforms, material ) {
uniforms.opacity.value = material.opacity;
if ( material.color ) {
uniforms.diffuse.value.copy( material.color );
}
...
}
WebGLBufferRenderer
无索引几何体的绘制。
ini
function WebGLBufferRenderer( gl, extensions, info ) {
let mode;
function setMode( value ) {
mode = value;
}
function render( start, count ) {
gl.drawArrays( mode, start, count );
info.update( count, mode, 1 );
}
function renderInstances( start, count, primcount ) {
if ( primcount === 0 ) return;
gl.drawArraysInstanced( mode, start, count, primcount );
info.update( count, mode, primcount );
}
function renderMultiDraw( starts, counts, drawCount ) {
// ... WEBGL_multi_draw 扩展
}
this.setMode = setMode;
this.render = render;
this.renderInstances = renderInstances;
this.renderMultiDraw = renderMultiDraw;
}
WebGLIndexedBufferRenderer
有索引几何体的绘制。
scss
function WebGLIndexedBufferRenderer( gl, extensions, info ) {
let mode;
// ...
let type, bytesPerElement;
function setIndex( value ) {
type = value.type;
bytesPerElement = value.bytesPerElement;
}
function render( start, count ) {
gl.drawElements( mode, count, type, start * bytesPerElement );
info.update( count, mode, 1 );
}
function renderInstances( start, count, primcount ) {
gl.drawElementsInstanced( mode, count, type, start * bytesPerElement, primcount );
// ...
}
function renderMultiDraw( starts, counts, drawCount ) {
extension.multiDrawElementsWEBGL( mode, counts, 0, type, starts, 0, drawCount );
// ...
}
}
WebGLBackground
设置背景色,可以纯色或图片。
会根据WebGLState中设置的初始状态,调用WebGLRenderer.clear() 方法清屏。
src\renderers\WebGLRenderer.js
scss
function render( scene ) {
...
state.buffers.color.setClear( clearColor.r, clearColor.g, clearColor.b );
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
src\renderers\webgl\WebGLState.js
css
setClear: function ( r, g, b, a, premultipliedAlpha ) {
...
gl.clearColor( r, g, b, a );
src\renderers\WebGLRenderer.js
ini
this.clear = function () {
_gl.clear( _gl.COLOR_BUFFER_BIT );
};
WebGLState
WebGL 渲染器的状态管理器。
WebGL 是有状态的:每次 gl.enable()、gl.blendFunc()、gl.bindTexture() 都会改 GPU 状态。渲染一帧里可能切换成百上千次材质/纹理,如果每次都直接调 GL,开销很大。WebGLState 用本地变量缓存"当前值",避免重复设置。
核心思路:
bash
function enable( id ) {
if ( enabledCapabilities[ id ] !== true ) {
gl.enable( id );
enabledCapabilities[ id ] = true;
}
}
function disable( id ) {
if ( enabledCapabilities[ id ] !== false ) {
gl.disable( id );
enabledCapabilities[ id ] = false;
}
}
典型模式:先比较 → 不同才调用 GL → 更新缓存。
useProgram、bindTexture、bindFramebuffer 等都是这个思路。
总结
这一章我们说了webgl 和three.js 中的对象的对应关系。
围绕shader program,循着webgl 的渲染流程,说了three.js中的关键模块和其它相关模块。
当然,我说的这些并不全,但基本可以让我们看到three.js 在架构中要考虑的问题。
下一章,我们会概览three.js的渲染流程。