参赛作品
在我看来,我们需要在计算机图形学中使用较少的三角函数。 使用三角函数时,对投影,反射和矢量运算(如矢量的标量(点)和矢量(叉)的真实含义)的良好理解通常会带来越来越多的焦虑感。 更确切地说,我相信三角法非常适合将数据输入算法(对于角度的概念,这是一种测量方向的直观方法),当我看到三角法位于某些3D渲染算法的深度中时,我感到有些错误。 实际上,我认为当三角函数潜入其中时,小猫会在某处死亡。 我并不担心速度或准确性,但我认为它具有概念上的优雅……现在我将解释。
在其他地方,我已经讨论过,对于这两个“矩形”运算-角度的正弦和余弦,矢量的标量和矢量积包含旋转的所有必要信息。 在许多地方,此信息等效于正弦和余弦,似乎您可以简单地使用向量的乘积,而摆脱三角函数和角度。 实际上,您可以通过保留普通的欧几里得向量来完成此操作,而无需三角函数。 这使我们感到奇怪:“我们不是在做多余的事情吗?” 似乎正在做。 但是,不幸的是,即使是经验丰富的专业人士也容易滥用三角函数,并使事情变得非常复杂,繁琐且并非最简洁。 甚至可能是“错误的”。
让我们停止使文章更加抽象。 让我们想象一下用向量乘积代替三角公式的一种情况,看看我刚才说的是什么。
旋转空间或对象的选项错误
让我们有一个函数来计算围绕标准化向量的向量的旋转矩阵
在拐角处
。 在任何3D引擎或实时数学库中,都会有一个这样的功能很可能是从另一个引擎,维基百科或OpenGL教程中盲目复制的...(是的,这时您必须承认,并且根据您的心情,可能需要担心因此)。
该函数将如下所示:
mat3x3 rotationAxisAngle( const vec3 & v, float a ) { const float si = sinf( a ); const float co = cosf( a ); const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
想象一下,您正在挖掘演示或游戏的内部,可能正在完成某种动画模块,并且需要沿给定方向旋转对象。 您想旋转它,使其一个轴,例如一个轴
与特定向量重合
(例如,与动画路径相切)。 当然,您决定创建一个矩阵,其中将包含使用
rotationAxisAngle()
转换。 因此,您首先需要测量轴之间的角度
您的对象和所需的方向向量。 因为您是图形程序员,所以您知道可以使用标量积来完成此操作,然后使用
acos()
提取角度。
另外,您知道有时,如果标量积超出范围[-1; 1],然后决定更改其值以使其落入该范围内(
大约每钳位)(在这一点上,您甚至可以责怪计算机的精度,因为归一化向量的长度不完全是1)。 此时,一只小猫死了。 但是,直到您了解为止,您仍将继续编写代码。 接下来,您计算旋转轴,并且知道这是向量的向量积
您的对象和选择的方向
,以防万一……(小猫被复活并再次被杀死),对象中的所有点都将在与这两个向量所定义的平面平行的平面内旋转。 结果,代码看起来像这样:
const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const mat3x3 rot = rotationAxisAngle( axi, ang );
要理解为什么这样做有效,但仍然会出错,我们将打开所有的
rotationAxisAngle()
代码,然后看看实际发生了什么:
const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const float co = cosf( ang ); const float si = sinf( ang ); const float ic = 1.0f - co; const mat3x3 rot = mat3x3( axi.x*axi.x*ic + co, axi.y*axi.x*ic - si*axi.z, axi.z*axi.x*ic + si*axi.y, axi.x*axi.y*ic + si*axi.z, axi.y*axi.y*ic + co, axi.z*axi.y*ic - si*axi.x, axi.x*axi.z*ic - si*axi.y, axi.y*axi.z*ic + si*axi.x, axi.z*axi.z*ic + co);
您可能已经注意到,我们正在通过计算返回值的余弦来进行相当不准确且昂贵的acos调用以立即将其取消。 然后出现第一个问题:“为什么不跳过
acos()
--->
cos()
调用链并节省CPU时间?” 此外,这是否不告诉我们我们做错了什么并且非常复杂,并且通过简化表达式来向我们展示一些简单的数学原理?
您可以辩称无法简化,因为您将需要一个角度来计算正弦。 但是,事实并非如此。 如果您熟悉向量的向量积,那么您就知道就像标量积包含余弦一样,向量也包含正弦。 大多数图形程序员理解为什么需要矢量的标量积,但并不是每个人都理解为什么需要矢量积(并且仅将其用于读取法线和旋转轴)。 基本上,可以帮助我们摆脱cos / acos对的数学原理还告诉我们,在有标量积的地方,可能有矢量乘积报告缺少的信息(垂直部分,正弦)。
旋转空间或物体的正确方法
现在我们可以提取之间的夹角的正弦
和
只是看他们向量乘积的长度...-记住
和
标准化! 这意味着我们可以(我们必须!)以这种方式重写该函数:
const vec3 axi = cross( z, d ); const float si = length( axi ); const float co = dot( z, d ); const mat3x3 rot = rotationAxisCosSin( axi/si, co, si );
并确保我们新的旋转矩阵构造函数
rotationAxisCosSin()
不会在任何地方计算正弦和余弦,而是将其作为参数:
mat3x3 rotationAxisCosSin( const vec3 & v, const float co, const float si ) { const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
要摆脱标准化和平方根,还可以做另一件事-将整个逻辑封装在一个新函数中,然后将
1/si
传递给矩阵:
mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = (1.0fc)/(1.0fc*c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
后来,佐尔坦·弗拉纳(Zoltan Vrana)注意到
k
可以简化为
k = 1/(1+c)
,这不仅在数学上看起来更优雅,而且还将两个特征移到k上,因此将整个函数移到了k(
和
并行)合而为一(当
和
在这种情况下一致,则没有明显的旋转)。 最终代码如下所示:
mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = 1.0f/(1.0f+c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
我们不仅摆脱了三个三角函数,摆脱了丑陋的钳制(和归一化!),而且从概念上简化了我们的3D数学。 没有超越功能;这里仅使用向量。 向量创建可修改其他向量的矩阵。 这很重要,因为3D引擎中的三角函数越少,它不仅变得更快,更清晰,而且首先在数学上更加优雅(更正确!)。