本文介绍下THREE.js里面和geometry相关的morphTargets。
THREE.js有两种基本的geometry:Geometry和BufferGeometry。这两种类型创建morphTargets的方式不一样,所以会分别进行讲述。因此,本文包括以下三个部分:
- morphTargets是啥;
- 给Geometry添加morphTargets;
- 给BufferGeomtry添加morphAttributes;
本文示例基于THREE.js的124版本,可以通过THREE.REVISION
属性获取THREE.js的版本。
morphTargets是啥
morph是图像变换、变形的意思。那么,一个物体的几何形态是如何表示的呢?
顶点位置。
THREE.js中用于表示顶点位置的数据包括Geometry的vertices属性,以及,BufferGeometry的attributes属性的position属性。那么,如何表示变形后的物体呢?
THREE.js采用的是通过变形的顶点来定义变形物体。这里,原物体的顶点和变形后物体的顶点是一一对应关系,包括以下特点:
- 数量相同;
- 顺序一致;
前面提到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)
因为是要把图形缩小一半,所以顶点信息的单位长度变为一半就行,我们通过一个循环实现:
const morphVertices = boxGeometry.vertices.map(vector => {
return vector.clone().multiplyScalar(0.5)
})
console.log(morphVertices)
给Geometry添加morphTargets属性:
boxGeometry.morphTargets.push({
target: 'halfBox', // 名字随便设置,目前还没有发现有啥用
vertices: morphVertices
})
添加之后,发现并没有生效,经过查找资料,发现除了设置Geometry之外,还需要Material和Mesh的配合:
- 初始化Material的时候,设置morphTargets属性为true;
- 给Mesh添加morphTargetInfluences属性,属性值可以是0-1之间,表示应用变形的程度;
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true, morphTargets: true })
mesh.morphTargetInfluences = [1]
截图如下,蓝色线框是原始长宽高为2的立方体,此处用来对比:
morphTargetInfluence: 1
morphTargetInfluence: 0.5
此时,遇到了一些疑问:
- 如果influence的值介于0到1之间,计算实际顶点位置的算法是啥;
- 查看Geometry的morphTargets属性会发现该属性是一个数组,同样Mesh的morphTargetInfluences属性也是一个数组,所以这是不是表示我们可以添加多个morphTargets;
- 如果第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 ];
// ...
也就是
- 当BufferGeometry.morphTargetsRelative是false的时候,计算方式为:
base + sum((target - base) * influence)
,或者按照上述部分的代码逻辑:base * (1 - sum(influences)) + sum(target * influence)
,这两个计算方式是等价的。 - 当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同时存在的时候,最终顶点信息的计算方法,希望大家有所收获。
如有错误,欢迎留言评论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。