【问题标题】:Applying weights to matrixes and vertices (bone rotation)将权重应用于矩阵和顶点(骨骼旋转)
【发布时间】:2014-08-25 07:41:46
【问题描述】:

我正在为低多边形 3D 图形旋转网格内的骨架骨骼。在顶点着色器上它是这样应用的。
gsl:

    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

左边是形状如何开始,中间是上面的方程如何旋转它,右边是我的目标。中点加权0.5。它只会越弯曲越严重,在 180 度时它的直径为零。

  • 我尝试在着色器上组装矩阵,这样我就可以将权重应用于旋转而不是生成的顶点。它看起来像右图一样完美,但它需要为每个顶点组装矩阵(昂贵)
  • 我研究过四元数,但 glsl 本身并不支持它们(如果我错了,请纠正我),而且它们令人困惑。这是我需要做的吗?
  • 我考虑过每个关节有三块骨头,并在每块骨头之间添加一个“膝盖骨”。这不会消除问题,但会减轻问题。
  • 我正在考虑将顶点投影到旋转后与轴的原始距离。这会在 180 度时失败,但会(相对)便宜。

所以考虑到这些选项,或者我可能没有考虑过的其他选项,其他人如何避免这种挤压效应?

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

【问题讨论】:

  • 我喜欢你的编辑。当我阅读这个问题时,我觉得我可以遵循它(模域特定行话)。有些人可能会认为这个问题太宽泛了,但在我看来,主要问题是这个主题的知识渊博的 Stack Overflow 读者太少了。 (如果有人想知道我为什么发表这个看似出乎意料的评论,那是因为我在meta 上发现了这个问题。)
  • 感谢 meta 给予我的所有兴趣和支持。编辑后我故意将问题保持广泛,因为我对没有此问题且已在其他地方成功使用的任何替代解决方案感兴趣。
  • 这里的社区非常棒,我们非常感谢这个网站的成员。

标签: matrix glsl skeletal-animation


【解决方案1】:

免责声明:我不是一个 3D 专家,所以我只是向您推荐一种可能对您有所帮助的数学方法。

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

蓝色和绿色的数字是原始骨骼,使用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 了解不多),但我认为如果有什么不合适的地方,你可以根据自己的情况进行调整。

【讨论】:

  • 我今晚试试。看起来很有希望。
  • 我已经实现了......显然有问题。晚饭后,我会仔细检查我是否忠实地实施了您的建议,并上传一个 .gif 链接,说明它以前的样子和现在的情况。
  • 好吧,这就是它最初的样子:youtu.be/tPXU5WI4Bvk 这是我尝试使用你的建议:youtu.be/62i7MZaiHPw 我觉得它太大了,所以我取出矩阵缩放并得到这个:
    youtu.be/HyuldtPdkdQ 请注意,黑色部分表示可见的三角形的背面(片段着色器效果)。我还确认(未在上面的视频中显示)它在未缩放时仍然完全挤压,我想这是可以预料的。
  • 所以要么你的方法是错误的,要么我把我的矩阵相乘错了。我有一个骨架类,它递归地乘以骨骼的矩阵,我模仿了它,因此将方向矩阵相乘以确保它在正确的坐标上将缩放居中。假设我有一个肩膀、肘部和手腕矩阵。身体保持静止,因此矩阵从身体传播出去。在发送到着色器之前,手腕的 scale_matrix 会乘以什么,以及什么顺序?我猜是肩scale_matrix_original。对吗?
  • 在我看来,您首先将整个成员转换为单个骨骼(因此肘部和手腕的重量是 0 或 1 常量,具体取决于您的实现),然后您同时转换肘部和手腕相对肩,好像他们是一个单独的成员,等等......但我意识到使用 OpenGL 着色器可能并不那么容易。老实说,我事先没有考虑过这个问题。
【解决方案2】:

问题

Levansanswer 中的图说明了您看到的原因。但是,要了解发生了什么,请考虑执行代码时发生的情况:

如果第一个点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 之前,我是将终点发送到顶点着色器还是将其乘以顶点着色器上的矩阵?
  • 您可以将矩阵乘法保留在着色器中,您只需将使用的权重调整为维基页面中提到的权重。请记住,slerp 技术在 180° 时也存在问题。
  • 我可以忍受 170° 或更高的强制回避。是在接近 180° 时逐渐变差,还是某种被零除,因为它不知道“从北极到南极”该往哪个方向走?
  • 对于 slerp,它是除以零,因为正弦函数在 0° 和 180° 处为零。对于重整化技术,它是除以零,因为线性插值通过原点(您可能确实会说它不知道如何从北极到南极)。要从旋转矩阵中获取角度,请记住 cos α = (tr(M)-1)/2 其中tr 是跟踪函数,M 是 3D 矩阵。
  • @gunfulker - 你的问题得到了回答 - 如果是这样的话,向汤姆申请赏金是公平的。请让我知道,因为我打开了赏金。
【解决方案3】:

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

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

【讨论】:

  • 不能接受这个抱歉,它和第一个有同样的故障,当它弯曲超过 90 度时,从侧面看它开始看起来很猥琐。此外,它需要额外的骨骼选择逻辑。不过它确实给了我一些想法,所以谢谢!
  • 不用担心。也许你的想法会成功,你也会在这里发布:)
猜你喜欢
  • 1970-01-01
  • 2017-05-05
  • 2018-11-27
  • 2017-12-21
  • 2021-01-20
  • 1970-01-01
  • 2014-09-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多