本文介绍下THREE.js里面和geometry相关的morphTargets。

THREE.js有两种基本的geometry:Geometry和BufferGeometry。这两种类型创建morphTargets的方式不一样,所以会分别进行讲述。因此,本文包括以下三个部分:

  1. morphTargets是啥;
  2. 给Geometry添加morphTargets;
  3. 给BufferGeomtry添加morphAttributes;

本文示例基于THREE.js的124版本,可以通过THREE.REVISION属性获取THREE.js的版本。

morphTargets是啥

morph是图像变换、变形的意思。那么,一个物体的几何形态是如何表示的呢?
顶点位置。

THREE.js中用于表示顶点位置的数据包括Geometry的vertices属性,以及,BufferGeometry的attributes属性的position属性。那么,如何表示变形后的物体呢?

THREE.js采用的是通过变形的顶点来定义变形物体。这里,原物体的顶点和变形后物体的顶点是一一对应关系,包括以下特点:

  1. 数量相同;
  2. 顺序一致;

前面提到Geometry和BufferGeometry是通过不同的属性来存储顶点信息的,所以导致它们存储变形顶点的属性也是不一样的。所以,下文会针对这两种类型分别进行介绍。

给Geometry添加morphTargets

首先,创建一个长宽高都是2,材质是线框的红色立方体:

const boxGeometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
const mesh = new THREE.Mesh(boxGeometry, material)

设置Geometry变形后的形态。因为变形后的顶点信息是和Geometry的vertices属性相对应的,所以我们先看下原vertices属性是咋样的:

console.log(boxGeometry.vertices)

THREE.js morphTargets介绍_THREE.js
因为是要把图形缩小一半,所以顶点信息的单位长度变为一半就行,我们通过一个循环实现:

const morphVertices = boxGeometry.vertices.map(vector => {
    return vector.clone().multiplyScalar(0.5)
})
console.log(morphVertices)

THREE.js morphTargets介绍_THREE.js_02
给Geometry添加morphTargets属性:

boxGeometry.morphTargets.push({
    target: 'halfBox', // 名字随便设置,目前还没有发现有啥用
    vertices: morphVertices
})

添加之后,发现并没有生效,经过查找资料,发现除了设置Geometry之外,还需要Material和Mesh的配合:

  1. 初始化Material的时候,设置morphTargets属性为true;
  2. 给Mesh添加morphTargetInfluences属性,属性值可以是0-1之间,表示应用变形的程度;
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true, morphTargets: true })
mesh.morphTargetInfluences = [1]

截图如下,蓝色线框是原始长宽高为2的立方体,此处用来对比:
morphTargetInfluence: 1
THREE.js morphTargets介绍_THREE.js_03
morphTargetInfluence: 0.5
THREE.js morphTargets介绍_THREE.js_04

此时,遇到了一些疑问:

  1. 如果influence的值介于0到1之间,计算实际顶点位置的算法是啥;
  2. 查看Geometry的morphTargets属性会发现该属性是一个数组,同样Mesh的morphTargetInfluences属性也是一个数组,所以这是不是表示我们可以添加多个morphTargets;
  3. 如果第2项的答案是可以添加多个,那么,多个morphTargets如何同时作用呢;

我们可以在上面的基础上再添加一个morphTargets,这个morphTargets把原立方体放大2倍:

const morphVertices2 = boxGeometry.vertices.map(vector => {
    return vector.clone().multiplyScalar(2)
})
boxGeometry.morphTargets.push({
    target: 'doubleBox',
    vertices: morphVertices2
})

然后重新设置morphTargetInfluences:

mesh.morphTargetInfluences = [1, 0.8]

那么,最后的效果图立方体的尺寸是多少呢?

通过查看源码,发现文件src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js中有这样的注释:

// morphTargetBaseInfluence is set based on BufferGeometry.morphTargetsRelative value:
// When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in position = sum((target - base) * influence)
// When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
transformed *= morphTargetBaseInfluence;
transformed += morphTarget0 * morphTargetInfluences[ 0 ];
transformed += morphTarget1 * morphTargetInfluences[ 1 ];
transformed += morphTarget2 * morphTargetInfluences[ 2 ];
transformed += morphTarget3 * morphTargetInfluences[ 3 ];
// ...

也就是

  1. 当BufferGeometry.morphTargetsRelative是false的时候,计算方式为:base + sum((target - base) * influence),或者按照上述部分的代码逻辑:base * (1 - sum(influences)) + sum(target * influence),这两个计算方式是等价的。
  2. 当BufferGeometry.morphTargetsRelative是true的时候,计算方式是:base + sum(target * influence)。

上述中,base指原始顶点的值,target指每个变形定义的顶点的值,influence是每个target对应的影响值。

还存在一个问题,计算方式是和BufferGeometry.morphTargetsRelative的值相关的,但是我们用的是Geometry,并没有morphTargetsRelative属性。又经过一番查找,发现Geometry是有一个对应的BufferGeometry的,挂在_bufferGeometry属性下面。

需要注意的是,我们创建完Geometry,在首次渲染之前,THREE.js并不会给Geometry创建_bufferGeometry,那么如何捕捉这个设置morphTargetsRelative属性的时机呢?我使用的是Mesh.onBeforeRender回调:

mesh.onBeforeRender = function () {
    boxGeometry._bufferGeometry.morphTargetsRelative = true // 默认是false
}

当morphTargetsRelative是false的时候,立方体的长宽高是2 + (2 * 0.5 - 2) * 1 + (2 * 2 - 2) * 0.8 = 2.6,我是通过设置上面那个蓝色的线框立方体为2.6,进行比对来验证的。

当morphTargetsRelative是true的时候,立方体的长宽高是2 + 2 * 0.5 * 1 + 2 * 2 * 0.8 = 6.2。

更多morphTargets同时作用的细节可以参见src/renderers/webgl/WebGLMorphtargets.js文件,比如当morphTargets的个数超过8个的时候。

给BufferGeomtry添加morphAttributes

与Geometry的morphTargets对应的是BufferGeometry的morphAttributes。

同样,首先创建一个长宽高都是2,材质是线框的红色立方体:

const boxGeometry = new THREE.BoxBufferGeometry(2, 2, 2)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true, morphTargets: true })
const mesh = new THREE.Mesh(boxGeometry, material)
scene.add(mesh)

此时,顶点数据存储在BufferGeometry.attributes.position里面,所以,遍历这个数据,生成一个新的morphAttribute,然后添加在morphAttributes属性上:

const morphPositions = []
const positions = boxGeometry.attributes.position.array
for (let i = 0; i < positions.length; i++) {
  morphPositions.push(positions[i] * 0.5)
}
const morphAttribute = new THREE.BufferAttribute(Float32Array.from(morphPositions), 3)
morphAttribute.name = 'halfBox'
boxGeometry.morphAttributes.position = [ // 注意,我们这里修改的是position属性,对应attributes.position
  morphAttribute
]

Material和Mesh的修改与前面一样。
同样,我们可以添加多个morphAttribute,对应上面的例子:

const morphPositions2 = []
const positions2 = boxGeometry.attributes.position.array
for (let i = 0; i < positions.length; i++) {
  morphPositions2.push(positions2[i] * 2)
}
const morphAttribute2 = new THREE.BufferAttribute(Float32Array.from(morphPositions2), 3)
morphAttribute2.name = 'doubleBox'
boxGeometry.morphAttributes.position.push(morphAttribute2)

boxGeometry.morphTargetsRelative = true // 比geometry设置morphTargetsRelative的方式要简单

总结

本文通过Geometry和BufferGeometry介绍了morphTargets是啥,以及多个morphTargets同时存在的时候,最终顶点信息的计算方法,希望大家有所收获。

如有错误,欢迎留言评论。