【THREE.js源码】Geometry和Attribute

  • BufferAttribute
  • BufferGeometry
  • InstancedBufferAttribute
  • InstancedBufferGeometry
  • InstancedInterleavedBuffer
  • InterleavedBuffer
  • InterleavedBufferAttribute

THREE.js中有一个重要的类,Mesh,即网格体对象。这个网格体对象在构造的时候需要传入两个变量,geometry和material,geometry指的是这个网格体的顶点属性信息,以及索引信息,而material中包含的是网格体的材质信息。

从webgl的底层来看,geometry可以视为绘制时的VAO/VBO/EBO等buffer的信息,material则可以视为存储uniform变量的对象。在geometry中,通常会有多个attribute属性信息,记录每个顶点的属性,以及index属性,记录EBO信息。每个attribute中存储了一个数组,包含对应顶点的属性信息,以及itemSize,记录每个属性包含的元素个数,以及normalized,元素是否需要归一化等。

THREE.js中有很多种类的geometry,比如常用的长方体几何体BoxGeometry,球几何体SphereGeometry,平面几何体PlaneGeometry,以及各种奇奇怪怪形状的几何体。但是这些几何体都有一个共同的基类,BufferGeometry。

BufferGeometry,故名思意,缓冲区几何体,内存存储的是要绘制的物体的缓冲区数据。使用webgl时我们知道,在定义好顶点数据后,需要一系列的gl.createBuffer()、gl.bindBuffer()、gl.getAttributeLocation()、gl.vertexAttribPointer()、gl.enableVertexAttribArray()等一系列操作来绑定和操作VAO/VBO/EBO。虽然BufferGeometry并没有这些步骤,而是在后续render的时候才会进行VAO/VBO/EBO的操作,但是BufferGeometry将所需要的数据进行了存储,后续能够很方便得处理数据。

在使用webgl绘制物体的时候,我们会在glsl中使用attribute变量来定义顶点属性。如,我们定义一个"attribute vec3 aPosition"来记录物体的顶点的坐标信息。在准备好数据后,我们使用gl.getAttributeLocation()来获取aPosition变量的地址,然后使用gl.vertexAttribPointer()将我们的数据送入变量,每次渲染GPU会从buffer中读取数据,组成顶点的位置,进行后续的处理。

BufferGeometry中类似的过程是使用一个BufferAttribute类型的变量来实现的。接口为BufferGeometry.prototype.setAttribute(name: string, attribute: BufferAttribute)
BufferAttribute变量内部包含了以下数据:

  • array:buffer的结构化数组数据
  • itemSize:每一个属性使用到的数据元素个数
  • normalized:是否进行归一化
  • count:数据数量,使用array.length / itemSize计算
    在BufferGeometry内部以{ attributeName: BufferAttribute }键值对来保存顶点属性名称及其内容的。

在使用gl.vertexAttribPointer时,传入的参数有以下内容

  • index:指定顶点属性的索引值,就是通过getAttributeLocation获取到的地址,这个可以通过对应的attributeName来获取
  • size:每个顶点属性所使用的元素的个数,对应itemSize
  • type:顶点属性的数据类型,three.js中全部默认为float类型
  • normalized:是否归一化,对应normalized
  • stride:步长,
  • offset:偏移量
    在BufferGeometry中,传入的结构化数组的值必须全部都是同一个顶点属性的数据,即不支持在一个结构化数组中传入两种及以上个顶点属性的值。渲染时,stride和itemSize*元素字节大小一样,offset为0。如果必须使用一个包含了多个顶点属性值的结构化数组,则需要使用InterleavedBuffer来构造带stride步长的buffer,stride即表示每一组数据包含多少个元素,如,我们在一个结构化数组中包含了3个position的XYZ,4个color的RGBA值,以及2个UV值,则stride为3+4+2=9。同时,还要使用lnterleavedBufferAttribute来构建attribute,需要传入一个包含顶点属性数据的InterleavedBuffer,顶点属性的itemSize,以及便宜量offset和normalized。如,上述的结构化数组中,每段步长内,前三个值为position的值,因此,使用方式为
new InterleavedBufferAttribute(interleavedBuffer, 3, 0, false);

而对于中间四个RGBA值,由于前面有三个position值,因此,offset为3

new InterleavedBufferAttribute(interleavedBuffer, 4, 3, false);

同理,对于UV,offset为3+4=7,itemSize为2。

一个完整的示例:

const vertices = new Float32Array([    // 定义顶点数据,三个顶点,每个顶点XYZ三个值
  0.0, 0.5, 0.0,
  -0.5, 0.0, 0.0,
  0.5, 0.0, 0.0
])

const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
  "position", // attribute属性名,position,会在shader中找名为position的attribute变量 
  new THREE.BufferAttribute(
    vertices, // 数据,一个结构化数组,保存所有的顶点值
    3,        // 每个顶点使用的元素个数,每个顶点包含XYZ三个值
    false     // 是否进行normalize
  )
);

如果使用InterleavedBuffer

const buffer = new THREE.InterleavedBuffer(
  new Float32Array([
    // 顶点位置       // 顶点颜色    
    0.0, 0.5, 0.0,   1.0, 0.0, 0.0,
    -0.5, 0.0, 0.0,  0.0, 1.0, 0.0,
    0.5, 0.0, 0.0,   0.0, 0.0, 1.0
  ]),
  6 // stride,每个顶点有position和color两个属性,各有XYZ和RGB三个值
);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
  "position",
  new THREE.InterleavedBufferAttribute(
    buffer,    // buffer数据
    3,         // position属性有三个值
    0,         // position的值在一个stride中的偏移值为0
    false      // 是否normalized
  )
);

geometry.setAttribute(
  "color",
  new THREE.InterleavedBufferAttribute(
    buffer,
    3,
    3,
    false
  )
);

理解了这一点后,有关BufferGeometry和BufferAttribute的其它方法并没有什么难的。但是值得注意的是,BufferGeometry中默认顶点坐标的attribute名为position,法向为normal,切线为tangent,同时,有computeVertexNormal和computeTangents方法可以自动根据顶点坐标计算法向和切线。同时会计算顶点包围球和轴向包围盒。

另一个比较好玩的东西是BufferGeometry中的groups属性和drawRange属性,groups可以将顶点进行分组,并对分组的部分设置一个新的材质。也就是说,mesh的material部分是可以是一个数组的,我们可以通过addGroup方法对BufferGeometry内的顶点进行分组,然后每一组对应一个material,这样就可以实现同一个mesh的不同部分有着不同的样式。而drawRange是用于控制绘制多少个顶点的。在webgl中,gl.drawElements方法有两个参数,count和offset,可以控制绘制图元的数量和数据的便宜量,而drawRange中的start和count正是对应了这两个属性。

BufferGeometry中其他的一些变换方法,如旋转,平移、缩放、使用矩阵等,其实也都是对应position\normal和tangent来进行变换的(如果有的话)。但是和mesh的变换不同的是,mesh的变换是作用在mesh自身的matrix上的,而BufferGeometry的变换是真的作用在buffer数据中的。

而对于BufferAttribute,它是buffer数据本身,所以并不会考虑自己是否是position之类的属性,所有的变换方法都是针对自身的buffer数据来进行的。因此,如果要对非position/normal/tangent数据进行矩阵变换,则可以获取到BufferAttribute后进行变换。

对于InterleavedBufferAttribute,原理是一样的,但是要注意的是,这种attribute使用到的数据只是buffer的一部分,所以每次获取原数据的时候要考虑一下buffer本身的stride和attibute的offset,获取attribute使用的那一部分数据。

THREE.js中还有另一类mesh,InstancedMesh,即实例对象mesh。实例对象和普通的对象的区别是,实例对象可以一次性绘制多个同样的物体,这些物体使用同样的geometry、material,也共享InstanceMesh对象的matrix和matrixWorld,但是每一个物体会有一个自身的instanceMatrix,用于调整自身的位置。WebGL2支持了实例化对象的绘制,使用gl.drawArraysInstanced和gl.drawElementsInstanced,webgl中,则要使用ANGLE_instanced_arrays扩展。

创建InstancedMesh时,除了geometry和material,还需要传入一个count,表示实例对象的个数,然后threejs会根据实例个数初始化每个对象的instanceMatrix。在shader中,对于非实例对象而言,其世界坐标为modelMatrix * worldPosition,而对于实例对象,则为modelMatrix * instanceMatrix * worldPosition。

同样的,使用InstancedMesh构建实例对象时,使用的geometry的类型为InstancedBufferGeometry,属性也是对应的InstancedBufferAttribute,对于InterleavedBufferAttribute,使用的buffer类型也为InstancedInterleavedBuffer。

InstancedBufferGeometry是BufferGeometry的子类,唯一的变动是,多了一个instanceCount属性,用于保存实例的个数。

InstancedBufferAttribute和InstancedInterleavedBuffer也是对应的BufferAttribute和InterleavedBuffer的子类,唯一的不同是,多了一个属性,meshPerAttribute,即每一个属性对应多少个实例对象,默认为1,表示一个实例属性值被一个实例所用,如果是2,则表示一个实例属性被两个连续的实例所用,以此类推。

有关geometry和attribute的重点内容就这么多.

posted @ 2024-04-16 12:12  李煎饼_GISer  阅读(98)  评论(0编辑  收藏  举报