首页
/ three.js CanvasTarget 深度解析:渲染器最终输出目标的统一抽象

three.js CanvasTarget 深度解析:渲染器最终输出目标的统一抽象

2026-09-06 13:30:40作者:冯爽妲Honey

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.jssrc/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();
}

两个关键细节:

  1. XR 保护this.xr.isPresenting 为真时直接返回,XR 呈现期间禁止调整尺寸(WebXR 设备有自己的输出尺寸管理逻辑);
  2. 视口自动复位:修改尺寸后立即 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;

三个要点:

  1. 画布来自 Backendbackend.getDomElement() 返回渲染器配置中传入(或自动创建)的画布,CanvasTarget 在 Renderer 构造期即建立;
  2. resize 事件绑定:Renderer 监听 resize 事件,回调 _onCanvasTargetResizesrc/renderers/common/Renderer.js)在渲染器已初始化时调用 this.backend.updateSize(),把逻辑尺寸变化传导到 GPU 侧;
  3. 默认目标标记isDefaultCanvasTarget = true 供后端识别"主画布"。在 src/renderers/webgpu/WebGPUBackend.jscontext 访问器中,只有 isDefaultCanvasTarget 为真且参数里显式提供了 context 时,才复用外部传入的 GPUCanvasContext;否则调用 canvasTarget.domElement.getContext( 'webgpu' ) 获取上下文,并按 alpha / outputType 参数配置 alphaModepremultiplied/opaque)与 toneMapping.modestandard/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 尺寸,setSizewidth * 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 节点采样。ViewportTextureNodesrc/nodes/display/ViewportTextureNode.js 中通过 renderer.getCanvasTarget() 获取当前输出目标并读取其纹理,单元测试 test/unit/src/nodes/display/ViewportTextureNode.tests.jsCanvasTarget 为测试对象验证了这条链路。

小结

CanvasTarget 把"渲染到屏幕"这件事从 Renderer 的杂项状态中剥离出来,形成一个内聚的、可替换的状态对象:它以逻辑像素为统一坐标系,通过 setDrawingBufferSize / setSize / setPixelRatio 管理物理缓冲,通过 viewport/scissor 四元组管理输出映射与剪裁,通过 resize / dispose 事件驱动后端同步,并以 colorTexture / depthTexture 让屏幕输出可被节点系统当作普通纹理消费。理解这个类,就理解了 three.js 渲染器尺寸语义(逻辑像素 vs 物理像素)与 WebGPU 后端画布上下文配置(src/renderers/webgpu/WebGPUBackend.js)之间的完整衔接关系。

登录后查看全文
热门项目推荐
相关项目推荐