对矩阵和顶点(骨骼旋转)应用权重

24

我正在为一个低多边形3D人物模型旋转骨骼内的网格。在顶点着色器中应用如下。


glsl:
    vec4 vert1 = (bone_matrix[index1]*vertex_in)*weight;
    vec4 vert2 = (bone_matrix[index2]*vertex_in)*(1-weight);
    gl_Position =  vert1+vert2;

bone_matrix[index1] 是一个骨骼的矩阵,而 bone_matrix[index2] 是另一个骨骼的矩阵。 weight 指定了 vertex_in 对这些骨骼的成员身份。问题在于当应用旋转时权重越接近0.5, 肘部直径就会缩小得越多。我通过测试大约有10,000个顶点的圆柱体(具有梯度权重)来验证了这一点。结果看起来像是弯曲一个花园水管。

我从以下来源获得了我的加权方法。实际上这是我能找到的唯一方法:
http://www.opengl.org/wiki/Skeletal_Animation
http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html
http://blenderecia.orgfree.com/blender/skinning_proposal.pdf

initial_ugly_good

左边是形状开始的样子,中间是上述方程旋转它的样子,右边是我的目标。中间点的权重为 0.5。弯曲得越多,情况就会变得更糟糕,在180度时直径为零。

  • 我尝试在着色器上组装矩阵,以便将权重应用于旋转而不是最终顶点。它看起来像右边图片中的完美效果,但需要为每个单独的顶点组装矩阵(代价高昂)
  • 我研究过四元数,但是GLSL不支持它们(正确我如果我错了)并且它们很令人困惑。这是我需要做的吗?
  • 我考虑每个关节使用三个骨骼,并在每个骨骼之间添加一个“膝盖”。这不能消除问题,但可以减轻问题。
  • 我正在考虑在旋转后投影顶点到其距离轴线的原始距离。这在180度处会失败,但代价(相对)较小。

因此,考虑到选项或我可能没有考虑到的其他选项,其他人如何避免这种夹紧效应?

编辑:我已经通过使用四元数使 SLERP 工作,但我选择不使用它,因为GLSL不支持它。我无法像Tom所描述的那样让几何SLERP起作用。我在前90度中让NLERP起作用,所以我在每个关节之间添加了一个额外的“骨头”。因此,为了弯曲前臂40度,我将肘部和前臂各弯曲20度。这消除了夹紧效应,但代价是骨骼数量增加了一倍,这不是理想的解决方案。


3
我喜欢你的编辑。当我阅读这个问题时,感觉能够理解它(除了某些领域特定术语)。有些人可能认为这个问题太宽泛了,但在我看来,主要问题只是这个话题没有太多了解的 Stack Overflow 读者。(如果有人想知道我为什么会突然发表这个评论,那是因为我在 meta 上发现了这个问题。) - John Y
我很感谢meta给我的所有关注和支持。在编辑后,我故意将问题保持宽泛,因为我对任何没有这个问题并已在其他地方成功应用的备选解决方案都很感兴趣。 - gunfulker
7
这里的社群非常棒,我们非常感谢这个网站的成员。 - JonH
3个回答

9

免责声明:我不是3D方面的专家,因此我只能为您提供一个可能有用的数学方法。

首先,让我们放上这个小图,这样我们就可以确定我们都在谈论同一件事:

enter image description here

蓝色和绿色的图形是原始骨骼,完全由bone_matrix [index1]bone_matrix [index2]旋转。红点是旋转中心,橙色的图形是你想要的,黑色的是你已经有的。

因此,作为蓝色和绿色的加权平均值构建的模型会像这样缩小,这一点通过灰色线条可以看出。

你需要以某种方式弥补这种缩小,我建议从旋转中心缩放点,我们需要在骨骼连接处进行2倍的缩放,并且在末端进行1倍的缩放。

scale_matrix成为预先计算的矩阵:以你的旋转中心(红点)为中心的振幅为2的缩放。

下面是你需要的着色器:

vec4 vert1 = (bone_matrix[index1]*vertex_in)*weight;
vec4 vert2 = (bone_matrix[index2]*vertex_in)*(1-weight);
vec4 inter =  vert1+vert2;
vec4 scaled1 = inter*(1-2*min(weight, 1-weight));
vec4 scaled2 = (scale_matrix*inter)*(2*min(weight, 1-weight));
gl_Position =  scaled1+scaled2;

很抱歉我现在不能测试它(我对GLSL不是很了解),但如果有不适配的地方,我认为你可以将其调整适应你的情况。


今晚我会试一下,看起来很有前途。 - gunfulker
我已经实现了它......显然有什么问题。晚饭后,我会仔细检查我是否忠实地实现了你的建议,并上传一个.gif链接,展示之前和现在的效果。 - gunfulker
好的,这是原始的样子: http://youtu.be/tPXU5WI4Bvk 这是我尝试使用您的建议的结果: http://youtu.be/62i7MZaiHPw 我发现它太大了,所以我去掉了矩阵缩放,得到了这个:<br> http://youtu.be/HyuldtPdkdQ 请注意,暗部分代表三角形的背面可见(片段着色器效果)。另外,我确认(上面的视频中没有显示)即使在没有缩放的情况下,它仍然完全收缩,这应该是可以预料的。 - gunfulker
所以要么你的方法有问题,要么我在错误地相乘矩阵。我有一个骨骼类,它递归地相乘骨骼的矩阵,我模仿了这个过程,使得方向矩阵相乘以确保在正确的坐标上进行缩放。假设我有一个肩膀、肘部和手腕矩阵。身体保持不动,所以矩阵从身体中传播出去。在发送到着色器之前,你的手腕的scale_matrix会被乘以什么,以及什么顺序?我猜是肩膀肘部scale_matrix_original。对吗? - gunfulker
在我的想法中,你首先将整个身体部位转化为一个单一的骨骼(因此肘部和手腕的重量是0或1个常数,具体取决于你的实现),然后相对于肩膀同时转换肘部和手腕,就好像它们是一个单一的部位等等...但是我意识到用OpenGL着色器可能不会这么容易。说实话,我之前没有考虑过这个问题。 - Levans
使用OpenGL着色器并不难,你可以通过计算肩部肘部手腕矩阵一次性将其发送到顶点着色器,而不是每个顶点都重新计算手腕矩阵并将其发送到顶点着色器。否则,你需要为每个顶点重新计算一次。如果在发送到着色器之前不进行相应调整,那么他推荐的以关节为中心的缩放矩阵在骨架姿势发生变化时就会出现错误。我想是这样。 - gunfulker

7

问题

你所看到的问题的原因在Levans answer中有所说明。然而,要理解发生了什么,请考虑执行代码时正在发生的事情:

如果第一个点vert1具有坐标(p, 0),则vert2的坐标将为(p cos(α), p sin(α)),其中α是两个骨头之间的角度(通过适当的坐标变换始终可以实现)。使用适当的权重w1-w将它们相加,我们得到以下坐标:

x = w p + (1-w) p cos(α)
y = (1-w) p sin(α)

这个向量的长度是:
length^2 = x^2 + y^2
         = (w p + (1-w) p cos(α))^2 + (1-w)^2 p^2 sin(α)^2
         = p^2 [w^2 + 2 w (1-w) cos(α) + (1-w)^2 cos(α)^2 + (1-w)^2 sin(α)^2]
         = p^2 [w^2 + (1-w)^2 + 2 w (1-w) cos(α)]

作为一个例子,当 w = 1/2 时,这简化为:
length^2 = p^2 (1/2 + 1/2 cos(α)) = p^2 cos(α/2)^2

length = p |cos(α/2)|,原始向量的长度为p(请参见 graph)。新向量的长度变小了,这就是您感知到的收缩效应。原因在于我们实际上是沿着一条直线插值两个顶点。如果我们想保持相同的长度p,我们实际上需要沿着绕旋转中心的圆圈插值。一种可能的方法是重新归一化结果向量,保留接头处的宽度。

这意味着我们需要将结果顶点坐标除以|cos(α/2)|(或任意权重的更普遍结果)。当角度恰好为180°时,它自然会产生一个除以零的副作用(出于与您技术相同的原因,接头处的宽度为零)。

我不是骨骼动画专家,但在我看来,您描述的原始解决方案是处理小骨角度的近似方法(其中收缩效应最小)。

替代方案

一种不同的方法是对旋转进行插值,而不是对顶点进行插值。例如,请参见slerp wiki pagethis paper

SLERP

slerp技术类似于我上面描述的技术,因为它也保留了关节处的宽度,但是它直接沿着关节周围的圆形路径进行插值。其通用公式为:

gl_Position = [sin((1-w)α)*vert1 + sin(wα)*vert2]/sin(α)

给定上述点 vert1 = (p, 0)vert2 = (p cos(α), p sin(α)),应用 SLERP 公式得到的结果为 result = (x, y),其中:

x = p [sin((1-w)α) + sin(wα) cos(α)]/sin(α)
y = p sin(wα) sin(α)/sin(α) = p sin(wα)

计算vert1result之间夹角的余弦值cos θ

cos(θ) = vert1*result/(|vert1| |result|) = vert1*result/p^2
       = p^2 [sin(wα) + sin((1-w)α) cos(α)]/sin(α)/p^2
       = [sin(α) cos((1-w)α) - cos(α) sin((1-w)α) + sin((1-w)α) cos(α)]/sin(α)
       = cos((1-w)α)

vert2result之间的角度为:

cos(φ) = vert2*result/p^2
       = [sin(wα) cos(α) + sin((1-w)α) cos(α)^2 + sin((1-w)α) sin(α)^2]/sin(α)
       = [sin(wα) cos(α) + sin((1-w)α) cos(α)]/sin(α)
       = [sin(wα) cos(α) + sin(α) cos(wα) - cos(α) sin(wα)]/sin(α)
       = cos(wα)

这意味着θ/φ = (1-w)/w,表明SLERP插值具有恒定的径向速度。在使用3D旋转矩阵时,我们可以将将把vert1转换为vert2的旋转表示为M = inverse(A)*B = transpose(A)*B,因此我们可以将旋转角度α表示为:
cos(α) = (tr(M) - 1)/2 = (tr(transpose(A)*B) - 1)/2
       = (A[0][0]*B[0][0] + A[0][1]*B[1][0] + A[0][2]*B[2][0] + 
          A[1][0]*B[0][1] + A[1][1]*B[1][1] + A[1][2]*B[2][1] + 
          A[2][0]*B[0][2] + A[2][1]*B[1][2] + A[2][2]*B[2][2] - 1)/2

四元数线性插值(LERP)

在使用四元数时,一种较好的逼近SLERP的方法是直接进行线性插值,然后对结果进行重新归一化。这样可以得到与SLERP相同的插值曲线,但插值不会以恒定的径向速度发生。

如果您真的想完全避免这些问题,您可以始终在关节处分割网格并单独旋转它们。


你所描述的 |cos α/2| 是我在问题的第4个要点中提到的,我会尝试一下。分割网格不是一个选项,但我考虑过使用中间的“膝盖”类型骨骼来避免接近180度。在尝试slerp之前,我应该将结束点发送到顶点着色器还是在顶点着色器上乘以矩阵? - gunfulker
你可以在着色器中保留矩阵乘法,只需要将使用的权重调整为维基页面中提到的权重即可。请记住,slerp技术在180度时也存在问题。 - Tom De Caluwé
在评论中发布代码是无效的。这是顶点着色器上的几何球面线性插值,我使用gl_Position = Slerp(vert1, vert2, weight)进行调用,但仍然存在捏合现象。http://pastebin.com/vZieSqhU 我做错了吗? - gunfulker
以上内容是基于http://keithmaggio.wordpress.com/2011/02/15/math-magician-lerp-slerp-and-nlerp/的模型。 - gunfulker
1
@gunfulker - 你的问题得到了回答吗?如果是这样,公平起见,应该给Tom加上赏金。请告诉我,因为我开了赏金。 - JonH
显示剩余9条评论

5

根据您的实际应用程序,您可能会喜欢这种变体:您可以在部分之间添加额外的带状物,如下所示:

enter image description here

权重以绿色/青色显示。然而,这需要一些骨骼技巧,因此当您向右弯曲时,您使用右侧骨骼并将旋转中心设置为右侧,而向左时则使用左侧骨骼和左侧旋转中心。


抱歉,我不能接受这个。它和第一个一样有同样的问题,当弯曲角度大于90度时,从侧面看会显得非常扭曲。此外,它需要额外的骨骼选择逻辑。不过,它确实给了我一些灵感,所以谢谢! - gunfulker
1
没关系。也许你的想法会奏效,然后你也会在这里发布它 :) - Kromster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接