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

useProgram、bindTexture、bindFramebuffer 等都是这个思路。

总结

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

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

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

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

相关推荐
wangruofeng4 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng4 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈8 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技8 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569158 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马10 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng11 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding12 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天12 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo14 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构