03-threejs架构原理浅析

源码:github.com/buglas/thre...

要理解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 → 更新缓存。

useProgrambindTexturebindFramebuffer 等都是这个思路。

总结

这一章我们说了webgl 和three.js 中的对象的对应关系。

围绕shader program,循着webgl 的渲染流程,说了three.js中的关键模块和其它相关模块。

当然,我说的这些并不全,但基本可以让我们看到three.js 在架构中要考虑的问题。

下一章,我们会概览three.js的渲染流程。

相关推荐
学以智用1 小时前
HTTP 请求头 Headers 完整详解
前端·后端
cidy_981 小时前
Codex、Hermes、Claude 使用共享知识库的对比
前端
牧艺1 小时前
浏览器 3D 交互实战:射线拾取、描边高亮与业务面板联动
前端·three.js·数据可视化
叉歪1 小时前
为 UnoCSS 扩展无限色阶与主题换色
前端·css
jiayong231 小时前
第 44 课:任务详情抽屉里的评论输入与操作记录联动
前端
ndsc_d1 小时前
前端实战复盘:用AI直接生成响应式官网落地页与React源码
前端·人工智能·react.js·ui·前端框架·aigc·paico
大家的林语冰2 小时前
✌️ Deno 2.9 来了,ESM 直接导入 CSS,甚至能开发桌面应用?!
前端·javascript·node.js
李顿波3 小时前
前端样式技术发展进程和解析(长期更新版)
前端
不简说3 小时前
JS 代码技巧 vol.1 — 10 个让代码少写 30% 的小套路
前端·javascript·面试