3D骨骼模型的底层原理

3D骨骼模型在计算机图形学的某些应用上有很重要的地位,尤其是需要用到动画或者变形的3D模型。我们平时能接触到的各种图形引擎都实现了对3D骨骼模型的支持。本文以three.js为例,介绍一下3D骨骼模型实现的底层原理。

为了实现骨骼3D模型的系统,three.js提供了3个对象来达到目的 ———SkinnedMesh, Bone, Skeleton。对于整个骨骼体系而言,这3个对象并不是平等的关系,他们是从属关系。其中Skeleton在整个3D骨骼体系中起到一个总体的架构作用,无论是SkinnedMesh还是Bone都可以看作是Skeleton的组成部分。在three.js中,他们以不同的形式来实现与Skeleton的关系。

Skeleton定义了一个模型的所有关节结构。每一个skeleton的关节可以理解为一个点,这个点拥有记录空间位姿的所有数据,此外这个关节还拥有一个parent和children的拓扑结点的结构。大体来说,在three.js中,这个关节点基本上等价于一个Object3D。而这样一个关节点的载体就是Bone。因此,我们可以将Skeleton理解为Bone的容器,它管理着这些Bone的结构并且记录它们的初始位姿。

这里,很有意思的一点。Skeleton是通过记录那些Bone的初始矩阵的逆来记录它们的初始位置。为何如此?假设每个Bone的初始矩阵为m0,m0的逆为m0-1,Bone在经过一系列变化之后的矩阵为m1,而这一系列的变化矩阵为mt,则: mt*m0 = m1,那么mt = m1 * m0-1。如此我们调整完每一个Bone时,都能轻松的通过m0-1和矩阵的当前的数据m1来求得Bone的转移矩阵。

那么,拿到了转移矩阵后要怎么办?这就要提到SkinnedMesh了。不同于一般的Mesh,最重要的一点,SkinnedMesh的geometry中除了position, normal 等这些常见属性外,它还有skinIndex和skinWeight这两个属性。

每一个SkinnedMesh都可以与Skeleton绑定,而SkinnedMesh中geometry的skinIndex是一个4元数组,其中每一个元素是指向4个bone的指引。然后skinWeight也是一个4元数组,不过这里面每一个元素是指每一个相对应bone的影响权重。

这样skinIndex和skinWeight就建立了Skeleton中bone对geometry中position的影响。请看下面一段来自three.js中glsl的代码片段:

export default /* glsl */`     
#ifdef USE_SKINNING
                     
  vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );
                            
  vec4 skinned = vec4( 0.0 );
  skinned += boneMatX * skinVertex * skinWeight.x;
  skinned += boneMatY * skinVertex * skinWeight.y;
  skinned += boneMatZ * skinVertex * skinWeight.z;
  skinned += boneMatW * skinVertex * skinWeight.w;
                                                        
  transformed = ( bindMatrixInverse * skinned ).xyz;
                                                                               
#endif
`;


export default /* glsl */`     
#ifdef USE_SKINNING
                     
  mat4 boneMatX = getBoneMatrix( skinIndex.x );            
  mat4 boneMatY = getBoneMatrix( skinIndex.y );
  mat4 boneMatZ = getBoneMatrix( skinIndex.z );
  mat4 boneMatW = getBoneMatrix( skinIndex.w );    
                                                   
#endif                                             
`;     

代码中,transformed最后用来计算gl_Position的值。我们可以清晰的看到skinIndex和skinWeight是如何影响transformed的值。getBoneMatrix函数是通过指引来读取与SkinnedMesh所绑定的Skeleton中Bone的相对转移矩阵。

SkinnedMesh是最后被显卡渲染的对象,整个骨骼体系,本质上可以这么理解:3D骨骼体系建立Bone对SkinnedMesh中几何顶点的权值影响关系。而由于这层关系是通过显卡应用建立起来的,因此Bone的位姿值在变化后,实现的几何变形会非常的快速。

如果,放到three.js这里,这句话可以更精确点说成:threejs的3D骨骼体系建立了最多支持4个骨骼位姿数据对SkinnedMesh中几何顶点的权值影响关系。

通过上面的解释,我们也能理解,这个关系是通过Skeleton建立起来的。

发表评论

邮箱地址不会被公开。 必填项已用*标注