three.js CanvasTexture 深度解析:从 HTML Canvas 创建动态 3D 纹理
本篇指南以 three.js 官方文档 CanvasTexture 页面为核心,系统讲解该类的继承关系、完整构造参数、needsUpdate 自动上传机制,并结合仓库源码 src/textures/CanvasTexture.js 与官方示例 examples/webgl_materials_texture_canvas.html,带你掌握如何在 Web 场景中把可交互的 2D 画布实时变为 3D 物体的纹理。读完后,你将能够独立创建动态 Canvas 纹理、正确驱动 GPU 更新,并理解其底层实现。
CanvasTexture 是什么:继承关系与定位
CanvasTexture 用于从 HTML canvas 元素创建纹理。它与所有纹理共用的基类 Texture 几乎一致,唯一的区别是:由于 canvas 元素可以直接被 WebGL 用作渲染源,构造函数会立即将 Texture#needsUpdate 置为 true,触发一次 GPU 纹理上传。
继承链如下(来自官方文档 docs/pages/CanvasTexture.html.md):
EventDispatcher → Texture → CanvasTexture
从源码 src/textures/CanvasTexture.js 可以看到,整个类只有 45 行,实现极其精简:
class CanvasTexture extends Texture {
constructor( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
super( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isCanvasTexture = true;
this.needsUpdate = true;
}
}
也就是说,CanvasTexture 的全部"魔法"只有两点:一是把 canvas 透传给 Texture 基类的 image 参数;二是在构造时强制标记纹理为待更新状态,使首帧渲染前纹理数据就能同步到 GPU。单元测试 test/unit/src/textures/CanvasTexture.tests.js 也验证了 CanvasTexture extends from Texture 与 isCanvasTexture 恒为 true 这两条继承契约。
构造参数详解
官方文档定义的构造函数签名为:
new CanvasTexture(
canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy
)
以下参数说明完整继承自官方文档,并结合 src/textures/Texture.js 基类构造函数中的实际默认值进行了核对:
| 参数 | 含义 | 默认值 | 说明 |
|---|---|---|---|
| canvas | HTML canvas 元素 | 无默认值 | 纹理数据来源,直接作为 WebGL 的纹理上传源 |
| mapping | 纹理映射方式 | Texture.DEFAULT_MAPPING(即 UVMapping) |
静态常量定义于 Texture.js#L801 |
| wrapS | U 方向(水平)环绕方式 | ClampToEdgeWrapping |
可选 RepeatWrapping / MirroredRepeatWrapping |
| wrapT | V 方向(垂直)环绕方式 | ClampToEdgeWrapping |
同上 |
| magFilter | 放大过滤方式(texel 覆盖多于一个像素时) | LinearFilter |
可选 NearestFilter 等 |
| minFilter | 缩小过滤方式(texel 覆盖少于一个像素时) | LinearMipmapLinearFilter |
依赖 mipmap |
| format | 像素格式 | RGBAFormat |
— |
| type | 数据类型 | UnsignedByteType |
— |
| anisotropy | 各向异性过滤采样数 | Texture.DEFAULT_ANISOTROPY(即 1) |
静态常量定义于 Texture.js#L810 |
这些常量(ClampToEdgeWrapping、LinearFilter、RGBAFormat 等)均定义在 src/constants.js 中,导入后直接作为枚举值传入即可。
几个值得注意的默认值语义:
ClampToEdgeWrapping:UV 超出[0,1]范围时钳制到边缘像素,而不会平铺。若想让 Canvas 纹理平铺,需显式设置RepeatWrapping并配合repeat属性。LinearMipmapLinearFilter:默认最小化过滤使用三线性 mipmap,因此默认generateMipmaps = true,渲染器会在上传时自动生成 mipmap 链;要求 canvas 宽高为 2 的幂以获得最佳兼容性(该约定是 WebGL1 时代的经验,仓库文档 src/textures/Texture.js 中对anisotropy的注释提到:值越大,倾斜视角下比基础 mipmap 更清晰,代价是更多纹理采样)。anisotropy = 1:即关闭各向异性增强。对斜视角下的 Canvas 纹理(如文字、线稿),可从renderer.capabilities.getMaxAnisotropy()取得设备上限后调高该值。
isCanvasTexture 属性
文档列出的唯一专属属性:
.isCanvasTexture : boolean (readonly)—— 用于运行时类型判断的标志位,默认true。
其用途典型场景是:在遍历材质中混用的多种纹理时做类型分派,例如:
if ( material.map && material.map.isCanvasTexture ) {
// 只有 Canvas 纹理才需要在 2D 上下书画完后重传
}
与基类的 isTexture 标志形成两级判断:isCanvasTexture 为 true 必然意味着 isTexture 也为 true,反之不然。
needsUpdate 机制:CanvasTexture 的核心工作流
理解 CanvasTexture 的关键,在于理解 needsUpdate。基类 src/textures/Texture.js#L754-L763 中它被实现为只写的 setter:
set needsUpdate( value ) {
if ( value === true ) {
this.version ++;
this.source.needsUpdate = true;
}
}
其注释明确写道:将其设为 true 表示"引擎在下一次渲染时必须更新该纹理",这会触发纹理上传到 GPU,并确保纹理参数配置正确。两个动作的含义是:
this.version ++:纹理版本号递增,渲染器检测到版本号变化即判定纹理数据已变更;this.source.needsUpdate = true:底层TextureSource(封装实际的图像数据)同样被标记为待上传。
这解释了 CanvasTexture 的完整生命周期:
- 构造时:
this.needsUpdate = true自动触发首次上传,无需手动干预——这正是它与new Texture(canvas)的本质区别; - 运行时修改画布:Canvas 是"活"的 2D 上下文,用
canvas.getContext('2d')重新绘制后,GPU 上的旧纹理不会自动同步,必须再次设置texture.needsUpdate = true; - 数据源绑定:
Texture的image属性通过 getter/setter 委托给this.source.data(见 Texture.js#L414-L424),因此你拿到的就是同一个 canvas 引用,后续在 2D 上下文里的任何绘制都直接作用于纹理源。
实战:官方交互绘图示例逐行剖析
仓库自带一个非常直观的官方示例 examples/webgl_materials_texture_canvas.html:页面上叠加一个 128×128 的 <canvas>,用户在上面用指针画线,笔迹实时贴到一个旋转立方体的六个面上。
1. 页面结构
<canvas id="drawing-canvas" height="128" width="128"></canvas>
2. 初始化场景与材质
material = new THREE.MeshBasicMaterial();
mesh = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), material );
3. 建立画布并把 Canvas 纹理挂到材质
function setupCanvasDrawing() {
// get canvas and context
const drawingCanvas = document.getElementById( 'drawing-canvas' );
const drawingContext = drawingCanvas.getContext( '2d' );
// draw white background
drawingContext.fillStyle = '#FFFFFF';
drawingContext.fillRect( 0, 0, 128, 128 );
// set canvas as material.map (this could be done to any map, bump, displacement etc.)
material.map = new THREE.CanvasTexture( drawingCanvas );
// ... 添加 pointerdown / pointermove / pointerup / pointerleave 监听
}
注意源码注释中的关键提示:"this could be done to any map, bump, displacement etc." —— CanvasTexture 不仅可用作 material.map,同样可以作为 bumpMap、displacementMap 等任意纹理槽的数据源。
4. 绘图并触发纹理重传(核心中的核心)
function draw( drawContext, x, y ) {
drawContext.moveTo( drawStartPos.x, drawStartPos.y );
drawContext.strokeStyle = '#000000';
drawContext.lineTo( x, y );
drawContext.stroke();
// reset drawing start position to current position.
drawStartPos.set( x, y );
// need to flag the map as needing updating.
material.map.needsUpdate = true;
}
每一步 stroke() 之后立即执行 material.map.needsUpdate = true,对应前文所述的 version++ 与 source.needsUpdate = true 流程,下一次 renderer.render() 就会把最新的画布位图重新上传到 GPU。如果遗漏这一步,你会看到笔迹"画了但不上屏"这一最常见的 CanvasTexture 踩坑现象。
5. 渲染循环
function animate() {
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
renderer.render( scene, camera );
}
该示例采用 importmap 方式引入 three 模块(../build/three.module.js),可以直接复制改造为自己的工程起点。
进阶要点与注意事项
- 纹理尺寸/格式不可变:
Texture基类注释(Texture.js#L26-L28)明确说明,纹理首次上传后,其尺寸、格式、类型不能更改;如需变更,应调用dispose()释放并新建一个纹理实例。对于 Canvas 纹理,这意味着动态修改 canvas 的width/height前需要先dispose()。 - 资源释放:
dispose()(Texture.js#L647-L657)会派发dispose事件,渲染器据此释放 GPU 资源;离开页面或移除材质时应调用。 - UV 变换:
offset/repeat/rotation/center等 UV 变换属性全部继承自Texture,作用于 Canvas 纹理同样生效;transformUv()中对ClampToEdgeWrapping的钳位逻辑(Texture.js#L680-L683)解释了默认包裹行为。 - 与 TextureLoader 的分工:
CanvasTexture面向"程序生成/实时绘制"的场景(文字贴图、动态图表、粒子纹理、Lottie 动画等);而 examples/webgl_loader_texture_lottie.html 一类示例展示了把动画帧画到 canvas 再经纹理输出的典型用法模式。仓库中共有 30 余个示例使用了CanvasTexture,覆盖地形着色(webgl_geometry_terrain.html)、混合模式(webgl_materials_blending.html)、体积云(webgl_volume_cloud.html)等,可作为进阶参考。
小结
CanvasTexture 虽然源码不到 50 行,却把 two 个 2D 世界与 WebGL 3D 世界打通:构造函数中的 this.needsUpdate = true 负责首次自动上传,而每次修改画布后手动置位 needsUpdate 则负责后续增量同步。结合 src/textures/Texture.js 的默认参数、官方文档页 的 API 约定与 examples/webgl_materials_texture_canvas.html 的完整交互实现,你可以快速在任何 three.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 StartedRust0627
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
