three.js 纹理数据源详解:TextureSource(Source)类的设计动机、上传机制与序列化
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本文基于 three.js 仓库中的 Source API 文档 及其源码实现,系统讲解纹理数据源类(自 r186 起更名为TextureSource)的核心设计:它如何将纹理"数据定义"与"纹理定义"解耦,data、dataReady、needsUpdate、version各属性如何驱动 WebGL 纹理上传流程,以及getSize与toJSON两种方法的具体行为。读完本文,你将能够独立使用TextureSource让多个Texture实例共享同一份图像数据,并理解Texture#needsUpdate触发 GPU 上传的完整调用链。
1. 什么是纹理数据源:数据与纹理解耦
Source(现名TextureSource)表示一张纹理的数据源(data source)。其核心目的,用 API 文档 的原话说:
The main purpose of this class is to decouple the data definition from the texture definition so the same data can be used with multiple texture instances.
即把"数据定义"与"纹理定义"解耦,使同一份数据可以被多个纹理实例复用。这在实践中非常常见:同一张法线图可能被两个材质以不同wrapS、repeat、magFilter使用;若各自持有独立数据副本,不仅浪费内存,数据更新时还需同步多次。通过共享一个TextureSource,GPU 端只需维护一份纹理对象,多个Texture实例只是挂在同一数据源上的不同"视图配置"。
从源码结构看,Texture与TextureSource的关系在 src/textures/Texture.js 中直接体现:
this.source = new TextureSource( image );new Texture( image )时,构造函数会把传入的图像封装成一个TextureSource作为texture.source。Texture的image属性实际上是一个代理属性,读写都转发到 source:
// src/textures/Texture.js get image() { return this.source.data; } set image( value ) { this.source.data = value; }因此texture.image = newImage与texture.source.data = newImage等价;而texture.image只是读取source.data。
2. 命名沿革:Source 更名为 TextureSource
当前版本中,实际使用的类是TextureSource。原Source类自 r186 起被标记为弃用(deprecated),保留为TextureSource的子类用于向后兼容。在 src/textures/TextureSource.js 中可以看到这一设计:
/** * @deprecated since r186. Use {@link TextureSource} instead. `Source` has been renamed to `TextureSource`. */ class Source extends TextureSource { constructor( data = null ) { warnOnce( 'Source: "Source" has been renamed to "TextureSource". Please update your code to use "THREE.TextureSource" instead.' ); // @deprecated, r186 super( data ); this.isSource = true; } }两点迁移要点:
- 新代码应使用
TextureSource;使用Source会触发一次warnOnce控制台警告(仅首次),实例上仍保留isSource标志供旧代码做类型测试; - 类型测试标志从
isSource更名为isTextureSource,新代码应检查source.isTextureSource === true。
3. 构造函数
new TextureSource( data : any )
构造一个新的纹理数据源。
参数:
- data:纹理的数据定义,可以是
HTMLImageElement、HTMLCanvasElement、HTMLVideoElement、ImageBitmap、VideoFrame、带width/height的普通对象,或立方体贴图所需的图像数组。默认为null。
构造函数实现位于 src/textures/TextureSource.js,逐字段初始化如下(与 API 文档的属性清单一一对应):
constructor( data = null ) { // 类型测试标志 this.isTextureSource = true; // 自增 ID(模块级计数器 _sourceId) Object.defineProperty( this, 'id', { value: _sourceId ++ } ); // 全局唯一 UUID this.uuid = generateUUID(); // 纹理数据定义 this.data = data; // 数据是否已就绪(默认 true) this.dataReady = true; // 版本号(默认 0) this.version = 0; }注意id是通过Object.defineProperty以只读方式挂载的模块级自增计数(_sourceId从 0 开始),主要用于内部对象属性映射与调试标识。
4. 属性详解
.data : any
纹理的数据定义。渲染器最终上传到 GPU 的内容就是source.data。在 src/renderers/webgl/WebGLTextures.js 的uploadTexture流程中,上传前会先对texture.image(即source.data)执行resizeImage与verifyColorSpace预处理。
.dataReady : boolean
默认true。该属性仅在needsUpdate被设为true时才有意义,用于精细控制纹理数据的处理方式:
当
dataReady为false时,引擎执行内存分配(如必要),但不把数据实际传输到 GPU 内存。
源码级证据在 src/renderers/webgl/WebGLTextures.js:
const allocateMemory = ( sourceProperties.__version === undefined ) || ( forceUpload === true ); const dataReady = source.dataReady;随后在各级 mipmap 上传分支中(例如 src/renderers/webgl/WebGLTextures.js):
if ( useTexStorage ) { if ( dataReady ) { state.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data ); } } else { state.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data ); }典型场景是流式纹理或渐进加载:先置dataReady = false让引擎按数据尺寸分配 GPU 显存(texStorage2D),待真实像素数据(如网络加载的 mipmap 层)就位后再置回true并触发上传,避免渲染出空白纹理。
.id : number (readonly)
数据源 ID,模块级自增计数器,只读。
.isSource : boolean (readonly)/.isTextureSource : boolean (readonly)
类型测试标志,均为true。当前版本的主标志是isTextureSource;isSource仅由弃用的Source子类设置(见第 2 节)。
.needsUpdate : boolean
这是一个只写 setter(src/textures/TextureSource.js):
set needsUpdate( value ) { if ( value === true ) this.version ++; }设为true时引擎为纹理分配内存(如必要),并在该 source 下次被使用时触发实际的 GPU 上传;设为false不会递减version。它本身不存储布尔值,而是通过递增version传递"需要更新"的信号。
.uuid : string (readonly)
数据源的 UUID,由generateUUID()生成。序列化(第 7 节)与ObjectLoader反序列化都以此为图像缓存的键。
.version : number (readonly)
从0开始,记录needsUpdate被设为true的次数。渲染器正是靠比较version与上次记录的版本来决定是否重新上传,见 src/renderers/webgl/WebGLTextures.js:
if ( source.version !== sourceProperties.__version || forceUpload === true ) { // 执行纹理上传流程 }上传完成后(src/renderers/webgl/WebGLTextures.js):
sourceProperties.__version = source.version;由此形成"设置 → 检测差异 → 上传 → 记录版本"的幂等更新闭环。
5. 与 Texture 的联动:Texture#needsUpdate的传播
Texture自己也有一个needsUpdatesetter(src/textures/Texture.js):
set needsUpdate( value ) { if ( value === true ) { this.version ++; this.source.needsUpdate = true; } }即texture.needsUpdate = true同时做两件事:递增texture.version,并级联触发source.needsUpdate = true(进而递增source.version)。由于多个Texture可以共享同一个source,任一纹理声明更新,所有共享该数据源的纹理在下一帧渲染时都会看到更新后的 GPU 内容——这正是数据解耦带来的复用收益。Texture的width/height/depth访问器同样直接委托给source.getSize(src/textures/Texture.js)。
6. 方法:getSize( target )
.getSize( target : Vector2 | Vector3 ) : Vector2 | Vector3
将数据源尺寸写入并返回给定的目标向量。实现位于 src/textures/TextureSource.js,按数据类型分支:
getSize( target ) { const data = this.data; if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( data instanceof HTMLVideoElement ) ) { target.set( data.videoWidth, data.videoHeight, 0 ); } else if ( ( typeof VideoFrame !== 'undefined' ) && ( data instanceof VideoFrame ) ) { target.set( data.displayWidth, data.displayHeight, 0 ); } else if ( data !== null ) { target.set( data.width, data.height, data.depth || 0 ); } else { target.set( 0, 0, 0 ); } return target; }要点:
- 视频帧源(
HTMLVideoElement/VideoFrame)使用videoWidth/videoHeight(或displayWidth/displayHeight),深度恒为 0; - 普通数据源直接读取
width/height/depth(无depth时取 0); data为null时返回(0, 0, 0);- 结果写入调用方提供的
target(复用向量,避免频繁分配),并返回同一对象,支持链式调用。
7. 方法:toJSON( meta )序列化
.toJSON( meta : Object | string ) : Object
将数据源序列化为 JSON。meta是可选的序列化元信息对象(由Object3D.toJSON在序列化场景时传递);当meta为undefined或字符串时,视为序列化根对象(root object)。
实现位于 src/textures/TextureSource.js,行为可归纳为三步:
- 去重:非根对象时,若
meta.images[ this.uuid ]已存在则直接返回缓存项——同一TextureSource被多个纹理引用时,images表中只会出现一份记录; - 输出结构:
{ uuid, url },其中url由serializeImage填充:data为数组(立方体贴图)时,url是逐项序列化后的数组;- 否则
url为单个序列化结果; data为null时url保持空字符串;
- 缓存登记:非根对象时把输出登记进
meta.images[ this.uuid ],供后续纹理的toJSON引用(Texture#toJSON输出中即包含image: this.source.toJSON( meta ).uuid,见 src/textures/Texture.js)。
serializeImage函数(src/textures/TextureSource.js)区分三类输入:
| 数据形式 | 序列化结果 |
|---|---|
HTMLImageElement/HTMLCanvasElement/ImageBitmap | ImageUtils.getDataURL( image ),即 data URL 字符串 |
DataTexture的图像对象(带.data) | { data: Array.from( image.data ), width, height, type: image.data.constructor.name } |
| 其他不可识别数据 | 输出警告Texture: Unable to serialize Texture.并返回空对象{} |
反序列化端在 src/loaders/ObjectLoader.js 中消费这些记录:立方体贴图分支执行images[ image.uuid ] = new TextureSource( imageArray ),普通图像分支执行images[ image.uuid ] = new TextureSource( deserializedImage ),纹理对象随后通过 uuid 找回对应 source。API 文档中toJSON的 "See: ObjectLoader#parse" 即指这一对序列化/反序列化闭环。
8. 实战示例:多纹理共享同一数据源
import * as THREE from 'three'; const loader = new THREE.TextureLoader(); loader.load( 'textures/brick-wall.jpg', ( image ) => { // 手动构造数据源,供多个纹理共享 const source = new THREE.TextureSource( image ); const diffuseMap = new THREE.Texture( source ); diffuseMap.colorSpace = THREE.SRGBColorSpace; diffuseMap.wrapS = diffuseMap.wrapT = THREE.RepeatWrapping; diffuseMap.repeat.set( 4, 2 ); // 同一份 GPU 数据,不同采样参数 const normalMap = new THREE.Texture( source ); normalMap.wrapS = normalMap.wrapT = THREE.RepeatWrapping; normalMap.repeat.set( 4, 2 ); normalMap.magFilter = THREE.NearestFilter; material.map = diffuseMap; material.normalMap = normalMap; // 数据更新后只需触发一次,共享 source 的所有纹理同步生效 // (Texture#needsUpdate 会级联到 source.needsUpdate) diffuseMap.needsUpdate = true; } );注意两个 API 细节:
new THREE.Texture( source )传入的 source 若是已有实例,Texture构造函数会直接复用(this.source = source.source,见 src/textures/Texture.js 附近逻辑),不会二次封装;- 使用旧类名
new THREE.Source( image )虽仍可用,但会打印一次弃用警告,新代码请一律使用TextureSource。
9. 关键文件索引
| 内容 | 路径 |
|---|---|
TextureSource/ 弃用Source实现 | src/textures/TextureSource.js |
Texture与 source 的绑定、needsUpdate传播 | src/textures/Texture.js |
GPU 上传主流程(version对比、dataReady控制) | src/renderers/webgl/WebGLTextures.js |
反序列化重建TextureSource | src/loaders/ObjectLoader.js |
| 原始 API 文档 | docs/pages/Source.html.md |
10. 小结
TextureSource是纹理数据与纹理定义的解耦层:一个 source 可被多个Texture共享,GPU 只上传一次;needsUpdate不存值,只通过递增version向渲染器发信号;渲染器以source.version !== sourceProperties.__version判断是否需要重新上传;dataReady = false允许"只分配显存、不传数据",是流式/渐进纹理加载的关键开关;toJSON/ObjectLoader以uuid为键完成 source 的去重序列化与重建,保证场景序列化时共享关系不丢失;- r186 起请使用
TextureSource与isTextureSource,旧名Source/isSource仅为兼容保留。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考