前言
大部分人会有一种感觉threejs学习更多的在学api,原理也只是略懂,为了深入学习,接下来我们将深入Threejs的core目录下的BufferAttribute类看看它做了什么?
core
在threejs的core核心文件目录内,定义了缓冲区类、图层类、事件类、优化的实例类、射线类等。在仔细阅读threejs源码后你会发现只不过做了不同层面的封装降低了门槛,并不涉及很复杂的继承关系。
缓冲类 BUfferAttribute
该类主要封装了缓冲buffer的定义,主要拓展了矩阵的应用、更多获取坐标方法、更多自定义拷贝方法。
首先他会校验是否是类型化数组,如果是普通数组那么会报错,在构造函数中他会定义初始值,除了一些自身的特殊定义外,大部分都是webgl的vertexttribPointer的函数传参,itemSize为顶点属性由几个值组成,type用于定义顶点的类型,normalize用于将整型转为浮点数,stride就是顶点数据的间隔一般为0,offset就是偏移的字节数,如果一个缓冲区应用了顶点、法线、纹理等,那么stride和offset是很有用的。
类型化数组
WebGL中有多种类型化数组,包括:
- Int8Array:8位有符号整型数组
- Uint8Array:8位无符号整型数组
- Int16Array:16位有符号整型数组
- Uint16Array:16位无符号整型数组
- Int32Array:32位有符号整型数组
- Uint32Array:32位无符号整型数组
- Float32Array:32位浮点型数组
- Float64Array:64位浮点型数组
这里注意下无符号相比有符号的正数范围会大一半,类型化数组你可以简单理解为操作buffer二进制的数据结构,同时你可以控制内存空间,相比普通数组能有更快的性能。
js
var myArray = [1.0, 2.0, 3.0, 4.0];
var myTypedArray = new Float32Array(myArray);
gl.bufferData(gl.ARRAY_BUFFER, myTypedArray, gl.STATIC_DRAW);
usage的静态绘制和动态绘制
StaticDrawUsage
其实代表buffer只会写入一次,但是是被缓存下来可以多次渲染的,目的是为了提高复用及性能考虑。当然如果你需要动态修改buffer,那么你可以使用DynamicDrawUsage
动态绘制
拓展的通用方法
在threejs源码中,定义了非常多的通用方法,比如copy、copyat、clone、toJSON, copy方法传入bufferAttribute实例对象source然后返回更新后的实例。copyat只不过自定义获取buffer的范围。clone的区别也只不过是不用传入实例对象,而toJSON也只不过转成对象,方便你序列化json。
js
copy( source ) {
this.name = source.name;
this.array = new source.array.constructor( source.array );
this.itemSize = source.itemSize;
this.count = source.count;
this.normalized = source.normalized;
this.usage = source.usage;
this.gpuType = source.gpuType;
return this;
}
copyAt( index1, attribute, index2 ) {
index1 *= this.itemSize;
index2 *= attribute.itemSize;
for ( let i = 0, l = this.itemSize; i < l; i ++ ) {
this.array[ index1 + i ] = attribute.array[ index2 + i ];
}
return this;
}
clone() {
return new this.constructor( this.array, this.itemSize ).copy( this );
}
toJSON() {
const data = {
itemSize: this.itemSize,
type: this.array.constructor.name,
array: Array.from( this.array ),
normalized: this.normalized
};
if ( this.name !== '' ) data.name = this.name;
if ( this.usage !== StaticDrawUsage ) data.usage = this.usage;
if ( this.updateRange.offset !== 0 || this.updateRange.count !== - 1 ) data.updateRange = this.updateRange;
return data;
}
纯函数
这里简单说明下/*@__PURE*/
是为了在webpack的treesharking中表明是无副作用的,那么在打包构建的时候如果没有使用到这些向量就会自动删除无用代码。
矩阵的应用
这块就是简单的对向量和矩阵的相乘,根据顶点的渲染个数分开处理最后赋值。在4维矩阵也是同理,但是当转换方向的时候我们可以看到这段源码。
TransformDirection
transformDirection(m)
是 Three.js 中 THREE.Vector3
类的一个方法,用于将该 vector(作为一个方向向量)随矩阵 m
进行方向转换。其实这里也只是做了叉乘或者叫向量积,你可以通过m来让点实现偏移、缩放等。接下来我们看下normalize方法。
normalize
normalize其实是一个很重要的api,我们通常说他为归一化,就是将向量的长度转为1,注意这里是向量到原点的距离是1,并不代表xyz坐标为0或1,这样我们只需要考虑方向即可,我们可以应用在方向的计算上,比如光照中的法线。源码如下
js
length() {
return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
}
normalize() {
return this.divideScalar( this.length() || 1 );
}
divideScalar( scalar ) {
return this.multiplyScalar( 1 / scalar );
}
multiplyScalar( scalar ) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
return this;
}
欧几里得范数(Euclidean norm)
上面源码的理解困难主要在length这个数学方法,其实它是欧几里得范数(Euclidean norm),也称为L2范数(L2 norm)或向量的模(magnitude),用于求出向量的长度。
欧几里得范数可以通过以下公式计算:||v|| = √(x² + y² + z²)
,其中v是当前向量。因此,length()方法计算当前向量的每个分量的平方之和的平方根,以确定它的长度。
那么我们可以假设长度为0.5,那么multiplyScalar中传入的scalar就为2,对向量相乘也就是将原有向量的长度增加了一倍,那么就是1的长度。
InstancedBufferAttribute
InstancedBufferAttribute其实也就是BufferAttribute一个继承关系,但是为什么要多此一举呢?
InstancedBufferAttribute更偏向于组的概念,我们可以看到它有一个核心参数也就是meshPerAttribute,你可以理解为一组可复用的网格实例的数量。假设你的模型是10个学生为一组,每组需要配一个老师,每个学校要有一个校长,每个学校要配置多个老师也就是多个组,他的作用主要用于方便你管理不同实例的父子关系。
所以他的主要应用就是网格模型,比如一个平面为InstancedBufferAttribute,他的网格面会有多个InstancedBufferAttribute, 那么就可以做更好的性能优化来复用节点。比如下面截取的源码告诉了我们meshPerAttribute在做什么:
总结
从BufferAttribute,我们学习了类型化数组、通用方法来方便我们开发、代码的构建优化、归一化算法、同时也理解了为什么需要InstancedBufferAttribute来优化。如果你有webgl的基础,那么这些对你来说是小菜一碟了,如果你阅读吃力可以看看我之前的webgl的文章。
后续我会再写几篇threejs的源码分析,有兴趣的同学可以关注关注我,或者关注我的公众号谦宇的编程世界, 文章会同步更新。