three.js CanvasTarget 深度解析:渲染器最终输出目标的统一抽象
CanvasTarget 是 three.js 中负责"渲染结果最终写到哪里去"的核心抽象。它代表了渲染器的默认输出目标(Default Framebuffer),统一管理画布尺寸、像素比(pixel ratio)、视口(viewport)与剪裁矩形(scissor),并通过事件机制把 resize 等状态变化同步给渲染器与后端(Backend)。读完全文,你将掌握 CanvasTarget 的完整 API 语义、它在 Renderer 与 WebGPUBackend 内部的真实调用链,以及如何在自定义画布、OffscreenCanvas 和多画布场景中正确使用它。
类定义与继承关系
文档中的定义非常明确:
CanvasTarget is a class that represents the final output destination of the renderer.(CanvasTarget 是一个表示渲染器最终输出目标的类。)
继承链为 EventDispatcher → CanvasTarget,源码位于 src/renderers/common/CanvasTarget.js:
class CanvasTarget extends EventDispatcher {
constructor( domElement ) {
super();
this.domElement = domElement;
this._pixelRatio = 1;
this._width = this.domElement.width;
this._height = this.domElement.height;
this._viewport = new Vector4( 0, 0, this._width, this._height );
this._scissor = new Vector4( 0, 0, this._width, this._height );
this._scissorTest = false;
this.colorTexture = new FramebufferTexture();
this.depthTexture = new DepthTexture();
}
}
从源码结构看,有几个值得注意的细节:
- 构造时
_width/_height直接取自domElement.width/height,即画布属性值(物理像素)而非 CSS 尺寸,这是后续所有尺寸计算的基准; _viewport与_scissor初始值都是覆盖整个画布的Vector4;_scissorTest默认为false,即剪裁测试默认关闭。
该类通过两个入口导出:src/Three.WebGPU.js 与 src/Three.WebGPU.Nodes.js 中均有 export { default as CanvasTarget } from './renderers/common/CanvasTarget.js'。这意味着 CanvasTarget 是 WebGPU 渲染栈(及 TSL 节点模块)的公共 API,而不是内部私有类。
构造函数
new CanvasTarget( domElement : HTMLCanvasElement | OffscreenCanvas )
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
domElement |
HTMLCanvasElement | OffscreenCanvas |
要渲染到的画布元素。既支持普通页面中的 <canvas>,也支持 Worker 环境下的 OffscreenCanvas。 |
OffscreenCanvas 的支持让 three.js 可以在 Web Worker 中执行 WebGPU 渲染(仓库示例 examples/webgl_worker_offscreencanvas.html 即围绕该场景构建)。
属性(Properties)
.domElement : HTMLCanvasElement | OffscreenCanvas
渲染器正在绘制的画布元素引用。文档特别指出:"This value of this property will automatically be created by the renderer." —— 当通过渲染器参数 canvas 创建渲染器但未显式传入时,画布由渲染器自动创建;若显式传入,则 CanvasTarget 直接持有该引用。
.colorTexture : FramebufferTexture
默认帧缓冲(default framebuffer)的颜色纹理。在 src/renderers/common/CanvasTarget.js 构造时初始化为 new FramebufferTexture()。它把"屏幕输出"抽象成与普通 FramebufferTexture 一致的纹理对象,使 ViewportTextureNode 等 TSL 节点能够像采样普通纹理一样采样当前画布内容——src/nodes/display/ViewportTextureNode.js 内部正是通过 renderer.getCanvasTarget() 获取该目标,再取 colorTexture 进行绑定。
.depthTexture : DepthTexture
默认帧缓冲的深度纹理,构造时初始化为 new DepthTexture()。与 colorTexture 配合,使视口相关的深度采样节点(如 ViewportDepthTextureNode,见 test/unit/src/nodes/display/ViewportDepthTextureNode.tests.js)可以读取当前帧的深度缓冲。
尺寸与像素比 API:逻辑像素 vs 物理像素
CanvasTarget 中最容易混淆的语义在于逻辑像素(logical pixels)与物理像素(physical pixels)的区分。所有 getter/setter 都严格遵循这一约定。
.getPixelRatio() : number
返回当前像素比。内部字段 _pixelRatio 默认为 1。
.getDrawingBufferSize( target : Vector2 ) : Vector2
返回物理像素下的绘制缓冲尺寸,即已经乘过像素比:
getDrawingBufferSize( target ) {
return target.set( this._width * this._pixelRatio, this._height * this._pixelRatio ).floor();
}
注意末尾的 .floor()——结果会被向下取整,保证返回整数像素。
.getSize( target : Vector2 ) : Vector2
返回逻辑像素下的渲染器尺寸,不乘像素比:
getSize( target ) {
return target.set( this._width, this._height );
}
.setPixelRatio( value : number )
设置像素比,并在必要时重设画布尺寸。默认值 1。源码实现揭示了一个易被忽略的行为——setPixelRatio 内部直接复用了 setSize:
setPixelRatio( value = 1 ) {
if ( this._pixelRatio === value ) return; // 值未变化时提前返回,避免无谓重建
this._pixelRatio = value;
this.setSize( this._width, this._height, false ); // 注意 updateStyle 传了 false
}
setSize( width, height, false ) 意味着:改变像素比时只修改 domElement.width/height(物理缓冲尺寸),不改动 CSS style 属性。这正是"同一逻辑尺寸、更高物理分辨率"的典型语义。
.setDrawingBufferSize( width : number, height : number, pixelRatio : number )
一次性指定宽度、高度与像素比,绘制缓冲尺寸的计算公式为:
size.x = width * pixelRatio;
size.y = height * pixelRatio;
源码实现(src/renderers/common/CanvasTarget.js):
setDrawingBufferSize( width, height, pixelRatio ) {
// Renderer can't be resized while presenting in XR.
if ( this.xr && this.xr.isPresenting ) return;
this._width = width;
this._height = height;
this._pixelRatio = pixelRatio;
this.domElement.width = Math.floor( width * pixelRatio );
this.domElement.height = Math.floor( height * pixelRatio );
this.setViewport( 0, 0, width, height ); // 尺寸变化后视口复位为全屏
this._dispatchResize();
}
两个关键细节:
- XR 保护:
this.xr.isPresenting为真时直接返回,XR 呈现期间禁止调整尺寸(WebXR 设备有自己的输出尺寸管理逻辑); - 视口自动复位:修改尺寸后立即
setViewport( 0, 0, width, height ),避免视口残留旧坐标导致画面错位。
.setSize( width : number, height : number, updateStyle : boolean )
设置渲染器尺寸,updateStyle 默认为 true:
setSize( width, height, updateStyle = true ) {
if ( this.xr && this.xr.isPresenting ) return;
this._width = width;
this._height = height;
this.domElement.width = Math.floor( width * this._pixelRatio );
this.domElement.height = Math.floor( height * this._pixelRatio );
if ( updateStyle === true ) {
this.domElement.style.width = width + 'px';
this.domElement.style.height = height + 'px';
}
this.setViewport( 0, 0, width, height );
this._dispatchResize();
}
updateStyle 决定了是否同步写入 CSS style.width/height。当你用 CSS 媒体查询/flex 自行控制画布显示尺寸时,应传 false,让 setSize 只负责物理缓冲,避免两套尺寸逻辑互相覆盖。
Scissor API
.setScissor( x : number | Vector4, y : number, width : number, height : number )
定义剪裁矩形,坐标与宽高均以逻辑像素为单位,原点在矩形左下角。支持两种调用形式:四个标量参数,或直接传入一个 Vector4(源码通过 x.isVector4 判别后走 scissor.copy( x ) 分支)。
.getScissor( target : Vector4 ) : Vector4
返回当前剪裁矩形,结果写入调用方提供的 target 对象(零分配写法,适合逐帧调用)。
.setScissorTest( boolean : boolean ) / .getScissorTest() : boolean
开启/关闭剪裁测试。开启后,超出 scissor 矩形的片元将被丢弃。从源码看,CanvasTarget 本身只记录状态(this._scissorTest = boolean),真正下发到 GPU 的动作发生在上层:Renderer.setScissorTest 在转发给 CanvasTarget 后还会调用 this.backend.setScissorTest( boolean )(见 src/renderers/common/Renderer.js)。这一设计让 CanvasTarget 保持"纯状态容器",渲染状态的下发由 Backend 负责。
Viewport API
.setViewport( x : number | Vector4, y : number, width : number, height : number, minDepth : number, maxDepth : number )
定义视口。参数语义:
| 参数 | 说明 | 默认值 |
|---|---|---|
x / y |
视口左下角坐标(逻辑像素),也可传入单个 Vector4 |
— |
width / height |
视口宽高(逻辑像素) | — |
minDepth |
视口最小深度值。仅 WebGPU 生效 | 0 |
maxDepth |
视口最大深度值。仅 WebGPU 生效 | 1 |
源码实现中,minDepth/maxDepth 被直接挂到内部 Vector4 实例上:
setViewport( x, y, width, height, minDepth = 0, maxDepth = 1 ) {
const viewport = this._viewport;
if ( x.isVector4 ) {
viewport.copy( x );
} else {
viewport.set( x, y, width, height );
}
viewport.minDepth = minDepth;
viewport.maxDepth = maxDepth;
}
minDepth/maxDepth 是 WebGPU 渲染管线中的合法深度钳制范围(WebGL 的 gl.viewport 没有对应概念),这也是文档标注 "WebGPU only" 的原因。
.getViewport( target : Vector4 ) : Vector4
返回当前视口定义,写入调用方提供的 target。
事件与销毁
.dispose()
释放该实例分配的 GPU 相关资源,并触发 dispose 事件:
dispose() {
this.dispatchEvent( { type: 'dispose' } );
}
注意 dispose() 的职责是派发事件,由监听该事件的系统(Backend 的纹理缓存等)回收资源。另外源码中还有一个文档未列出的私有方法 _dispatchResize(),在 setSize / setDrawingBufferSize 后派发 resize 事件——这正是 CanvasTarget 与渲染器联动的主通道。
与 Renderer 的集成:谁创建了 CanvasTarget?
文档说画布"will automatically be created by the renderer",其落地过程可以在 src/renderers/common/Renderer.js 中找到:
this._canvasTarget = new CanvasTarget( backend.getDomElement() );
this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize );
this._canvasTarget.isDefaultCanvasTarget = true;
三个要点:
- 画布来自 Backend:
backend.getDomElement()返回渲染器配置中传入(或自动创建)的画布,CanvasTarget 在 Renderer 构造期即建立; - resize 事件绑定:Renderer 监听
resize事件,回调_onCanvasTargetResize(src/renderers/common/Renderer.js)在渲染器已初始化时调用this.backend.updateSize(),把逻辑尺寸变化传导到 GPU 侧; - 默认目标标记:
isDefaultCanvasTarget = true供后端识别"主画布"。在 src/renderers/webgpu/WebGPUBackend.js 的context访问器中,只有isDefaultCanvasTarget为真且参数里显式提供了context时,才复用外部传入的GPUCanvasContext;否则调用canvasTarget.domElement.getContext( 'webgpu' )获取上下文,并按alpha/outputType参数配置alphaMode(premultiplied/opaque)与toneMapping.mode(standard/extended)。
Renderer 上所有与尺寸/视口/剪裁相关的公开 API 都直接委托给 CanvasTarget,例如 renderer.getPixelRatio() → this._canvasTarget.getPixelRatio()、renderer.setViewport() → this._canvasTarget.setViewport()(见 src/renderers/common/Renderer.js)。因此在日常开发中,你通常不直接接触 CanvasTarget,而是调用 Renderer 的同名方法;理解 CanvasTarget 的价值在于弄清这些方法背后的状态归属与语义边界。
自定义画布:setCanvasTarget / getCanvasTarget
Renderer 还提供了替换输出目标的接口(src/renderers/common/Renderer.js):
setCanvasTarget( canvasTarget ) {
this._canvasTarget.removeEventListener( 'resize', this._onCanvasTargetResize );
this._canvasTarget = canvasTarget;
this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize );
}
getCanvasTarget() {
return this._canvasTarget;
}
典型用法——将渲染输出切换到另一个画布(例如多屏输出、或把主渲染结果接到 OffscreenCanvas 做二次处理):
import { WebGPURenderer, CanvasTarget } from 'three/webgpu';
const offscreenCanvas = document.querySelector( '#output' ).transferControlToOffscreen();
const renderer = new WebGPURenderer();
await renderer.init();
renderer.setCanvasTarget( new CanvasTarget( offscreenCanvas ) );
替换时会自动解绑旧的 resize 监听并绑定新目标的 resize 监听,保证尺寸同步链不断裂。
典型实战场景
1. 高分屏适配(Retina/高 DPR 屏幕)
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
setPixelRatio 不改变 CSS 尺寸,setSize 按 width * pixelRatio 重设物理缓冲——两者配合实现"逻辑尺寸不变、物理分辨率提升"的标准写法。
2. 窗口 resize 联动
window.addEventListener( 'resize', () => {
renderer.setSize( window.innerWidth, window.innerHeight );
// 画布尺寸变化会经 CanvasTarget 派发 resize 事件,
// Renderer 内部随即调用 backend.updateSize() 同步 GPU 尺寸
} );
3. 子区域渲染(scissor)
renderer.setScissor( true );
renderer.setScissor( offsetX, offsetY, panelWidth, panelHeight );
renderer.setViewport( offsetX, offsetY, panelWidth, panelHeight );
renderer.render( scene, camera );
注意 scissor 与 viewport 语义不同:viewport 改变 NDC 到屏幕坐标的映射(画面"装"在哪个区域),scissor 只是丢弃区域外的片元(画面"遮"掉哪些像素)。多面板布局需要同时设置两者。
4. 视口深度范围(WebGPU)
// 仅 WebGPU 生效的 minDepth / maxDepth
renderer.setViewport( 0, 0, width, height, 0, 0.5 );
在 WebGPU 管线中可限制实际写入的深度范围,WebGL 侧该参数无对应硬件语义。
与 XR / 后处理管线的交互
从源码结构看,CanvasTarget 还承担两个文档未展开的职责:
- XR 尺寸保护:
setSize/setDrawingBufferSize中均有if ( this.xr && this.xr.isPresenting ) return;守卫。XR 会话期间输出尺寸由 WebXR 系统接管,应用侧调用会被静默忽略; - 作为纹理源参与节点渲染:
colorTexture/depthTexture使画布输出可以被 TSL 节点采样。ViewportTextureNode在 src/nodes/display/ViewportTextureNode.js 中通过renderer.getCanvasTarget()获取当前输出目标并读取其纹理,单元测试 test/unit/src/nodes/display/ViewportTextureNode.tests.js 以CanvasTarget为测试对象验证了这条链路。
小结
CanvasTarget 把"渲染到屏幕"这件事从 Renderer 的杂项状态中剥离出来,形成一个内聚的、可替换的状态对象:它以逻辑像素为统一坐标系,通过 setDrawingBufferSize / setSize / setPixelRatio 管理物理缓冲,通过 viewport/scissor 四元组管理输出映射与剪裁,通过 resize / dispose 事件驱动后端同步,并以 colorTexture / depthTexture 让屏幕输出可被节点系统当作普通纹理消费。理解这个类,就理解了 three.js 渲染器尺寸语义(逻辑像素 vs 物理像素)与 WebGPU 后端画布上下文配置(src/renderers/webgpu/WebGPUBackend.js)之间的完整衔接关系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00