glMatrix中的转换顺序被颠倒了吗?

14

enter image description here

在这两个场景中,转换必须在glMatrix中交换。

即在glMatrix中实现1):

mat4.translate(modelViewMatrix, modelViewMatrix, [0.6, 0.0, 0.0]);
mat4.rotateZ(modelViewMatrix, modelViewMatrix, degToRad(45));
为什么转换顺序被倒转了?
3个回答

16
这不仅适用于WebGL,而且适用于OpenGL总体。确实,这可能令人困惑:转换应用的顺序与它们在源代码中出现的顺序相反。
以下是您提供的代码简化/缩短的“伪代码”版本:
M = identity();
M = M * T; // Where T = Translation
M = M * R; // Where R = Rotation

一种更简短的写法是:
M = T * R;

现在想象一下你使用这个矩阵来转换一个顶点 - 这可以写成:
transformedVertex = M * vertex

回想一下 M = T * R,这与以下表达式相同

transformedVertex = T * R * vertex

你也可以将其写成
transformedVertex = T * (R * vertex)

或者,为了更加明显:

rotatedVertex = R * vertex
transformedVertex = T * rotatedVertex

首先旋转顶点。(然后,将旋转后的顶点平移)


当然,你基本上可以颠倒事物。在OpenGL中矩阵乘法的常规方式是“后乘法”或“右乘法”,形式如下:
newMatrix = oldMatrix * additionalTransformation

(就像您在代码中所做的那样)。另一种选择是编写

newMatrix = additionalTransformation * oldMatrix

这有时被称为“预乘法”或“左乘法”。因此,您也可以写成

M = identity();
M = T * M; // Where T = Translation
M = R * M; // Where R = Rotation

最终,

M = R * T

在这种情况下,翻译出现在源代码中旋转之前,并且翻译也将被应用于旋转之前

但在OpenGL的上下文中,这是相当不寻常的。 (同时混合两种方式会非常令人困惑 - 我不建议这样做)。


一则旁注:当 glPushMatrixglPopMatrix 仍然是 OpenGL API 的一部分时,所有这些可能会更有意义。这种思考方式类似于遍历场景图。您首先应用“全局”变换,然后再应用“本地”变换。

更新:

针对评论,我将尝试写几句话来解释一些概念。在这里总结有点困难,我会尽量简化,并省略一些细节,因为这些细节可能超出了单个答案的范围。这里提到的一些事情是关于OpenGL早期版本中如何完成的,现在已经有了不同的解决方法 - 尽管许多概念仍然相同!

场景图的形式表示3D场景并不罕见。这是场景的分层结构表示,通常以树的形式呈现:

             root
            /    \
       nodeA      nodeB
      /  \           \
nodeA0    nodeA1    nodeB0
object    object    object

节点包含变换矩阵(如旋转或平移)。3D对象附加到这些节点上。在渲染期间,遍历此图形:访问每个节点,并将其对象渲染。这是递归完成的,从根节点开始,访问所有子节点,直至叶子节点。例如,渲染器可以按以下顺序访问上述节点:

root 
  nodeA
    nodeA0
    nodeA1
  nodeB
    nodeB0

在此遍历过程中,渲染器维护着一个“矩阵堆栈”。在早期的OpenGL版本中,有专门的方法来维护这个堆栈。例如,glPushMatrix用于将当前“顶部”矩阵的一份副本推入堆栈,glPopMatrix用于从堆栈中移除最顶端的矩阵。或者使用 glMultMatrix 将堆栈中当前“顶部”矩阵与另一个矩阵相乘。
当一个对象被渲染时,它总是使用位于此堆栈顶部的矩阵进行渲染。(那时还没有着色器和 mat4 uniform …)
因此,渲染器可以使用简单的递归方法来渲染场景图,如下所示(伪代码):
void render(Node node) {

    glPushMatrix();
    glMultMatrix(node.matrix);

    renderObject(node.object);

    foreach (child in node.children) {
        render(child);
    }

    glPopMatrix();
}

通过将渲染过程封装在glPushMatrix/glPopMatrix对中,渲染器可以始终维护正在访问的节点的正确当前矩阵。现在,渲染器访问这些节点,并维护矩阵堆栈:
Node:           Matrix Stack:
-----------------------------
root            identity 
  nodeA         identity * nodeA.matrix 
    nodeA0      identity * nodeA.matrix * nodeA0.matrix
    nodeA1      identity * nodeA.matrix * nodeA1.matrix
  nodeB         identity * nodeB.matrix
    nodeB0      identity * nodeB.matrix * nodeB0.matrix

可以看到,在节点中用于渲染对象的矩阵是由从根节点到相应节点路径上所有矩阵的乘积给出的。

当考虑一个“大型”场景图时,这些概念的可能性能优势和优雅性可能会更加明显:

root
  nodeA
    nodeB
      nodeC
        nodeD0
        nodeD1
        nodeD2
        ...
        nodeD1000

我们可以计算乘积

nodeA.matrix * nodeB.matrix * nodeC.matrix

一次,然后用这个矩阵一直乘以nodeD0...nodeD1000的矩阵。相反地,如果想要改变乘法顺序,就必须计算

nodeD0.matrix * nodeC.matrix * nodeB.matrix * nodeA.matrix
nodeD1.matrix * nodeC.matrix * nodeB.matrix * nodeA.matrix
...
nodeD1000.matrix * nodeC.matrix * nodeB.matrix * nodeA.matrix

浪费大量资源进行矩阵乘法计算。(虽然可以用其他方法避免这些冗余计算,但这些方法不会像这样优雅和简单。)

1
我很好奇为什么有人会点击“此答案没有帮助”。是否有什么遗漏或不清楚的地方? - Marco13
@Maro13 实际上,它非常详细和全面。但有一件事我不明白:为什么OpenGL有那个变换顺序?如果在变换操作中交换乘法顺序,这将节省我们很多麻烦。 - piegames
1
当你写T * R * vector时,"数学定义"(不考虑OpenGL),直观地说,顶点首先被旋转,然后再进行平移。正如已经提到的:你可以稍微改变代码来改变这个顺序,但即使你写成M = Identity; M = T * M; M = R * T;,结果仍然会是M = R * T,这仍然是"先平移再旋转"。(抱歉,我不确定如何以一种听起来有说服力的方式描述这个问题。我试图为场景图遍历绘制一个图像,但这看起来很混乱...) - Marco13
我(认为我)理解了顺序被颠倒的原则以及它是如何工作的。但在文档:www.opengl.org/sdk/docs/man2/xhtml/glMultMatrix.xml中,它说“使用参数m调用glMultMatrix = [...]将当前变换替换为C×M×v[...]”。此时顺序是任意的,他们也可以选择M x C x v。但为什么要这样呢?当我编写转换时,按照我想要应用它们的顺序调用变换操作是很直观的。即使这违反了使用变换的数学方式,但没有人会以数学符号来编程。 - piegames
从某种角度来看,数学和编程是非常密切相关的。使用+-*/运算符进行算术运算,使用命题演算进行条件语句,使用代数进行面向对象编程...这在计算机图形学中尤为明显,它总是与矩阵、向量空间等相关。然而,我对答案进行了更新,希望不会太令人困惑。 - Marco13
谢谢。将矩阵堆栈视为场景图确实有助于思考(因为我的场景总是太小而无法使用场景图)。 - piegames

1

我不太确定这个glMatrix是反向的。

例如观看这些视频,似乎标准做法是

m1 * m2 * m3 * vector

而根据视频中显示的顺序,这将对应于

gl_Position = projection * view * world * position;

它完全匹配GL和GLSL。

它也与glMatrix相匹配。

var m = mat4.create();
mat4.projection(m, fov, aspect, zNear, zFar);
mat4.multiply(m, m, view);
mat4.translate(m, m, [x, y, z]);
mat4.rotateY(m, m, someAngle);
mat4.scale(m, m, [sx, sy, sz]);

准确对应于

m = projection * 
    view * 
    translation * 
    rotation *
    scale;

对我来说似乎是向前的。

var vs = `
uniform mat4 u_worldViewProjection;

attribute vec4 position;
attribute vec2 texcoord;

varying vec2 v_texCoord;

void main() {
  v_texCoord = texcoord;
  gl_Position = u_worldViewProjection * position;
}
`;
var fs = `
precision mediump float;

varying vec2 v_texCoord;
uniform sampler2D u_diffuse;

void main() {
  gl_FragColor = texture2D(u_diffuse, v_texCoord);
}
`;

"use strict";
var gl = document.querySelector("canvas").getContext("webgl");
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);

var arrays = {
  position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1],
  normal:   [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1],
  texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1],
  indices:  [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23],
};
var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);

var tex = twgl.createTexture(gl, {
  min: gl.NEAREST,
  mag: gl.NEAREST,
  src: [
    255, 0, 0, 255,
    192, 192, 192, 255,
    0, 0, 192, 255,
    255, 0, 255, 255,
  ],
    });

    var uniforms = {
    u_lightWorldPos: [1, 8, -10],
  u_lightColor: [1, 0.8, 0.8, 1],
  u_ambient: [0, 0, 0, 1],
  u_specular: [1, 1, 1, 1],
  u_shininess: 50,
  u_specularFactor: 1,
  u_diffuse: tex,
};

function render(time) {
  time *= 0.001;
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  var eye = [1, 4, -6];
  var target = [0, 0, 0];
  var up = [0, 1, 0];

  var view = mat4.create();
  var camera = mat4.create();
  
  // glMatrix's lookAt is arguably backward.
  // It's making an inverse lookAt which is far less useful.
  // There's one camera in the scene but hundreds of other
  // objects that might want to use a lookAt to you know, look at things.
  mat4.lookAt(view, eye, target, up);
  //mat4.lookAt(camera, eye, target, up);
  //mat4.invert(view, camera);
    
  var m = mat4.create();

  var fov = 30 * Math.PI / 180;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.5;
  var zFar = 10;
  mat4.perspective(m, fov, aspect, zNear, zFar);
  mat4.multiply(m, m, view);
  mat4.translate(m, m, [1, 0, 0]);
  mat4.rotateY(m, m, time);
  mat4.scale(m, m, [1, 0.5, 0.7]);
  
  uniforms.u_worldViewProjection = m;
  
  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, uniforms);
  twgl.drawBufferInfo(gl, gl.TRIANGLES, bufferInfo);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display block; }
<script src="https://twgljs.org/dist/twgl-full.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.2/gl-matrix-min.js"></script>
<canvas></canvas>


1
谢谢提供链接,这个系列真的很棒! - piegames

0

我知道你需要什么,看一下:

http://nidza.html-5.me/zlatnaspirala2/project/index.html

源代码:

https://github.com/zlatnaspirala/zlatnaspirala2 https://github.com/zlatnaspirala/zlatnaspirala2/blob/master/project/zlatnaspirala/zlatnaspirala.js

魔法是:

mat4.translate(mvMatrix, [0.0, 0.0, 0.0]);
        xRot = YY;
        yRot = alfa + XX;
        mat4.rotate(mvMatrix, degToRad(xRot), [1, 0, 0]);
        mat4.rotate(mvMatrix,  degToRad(yRot), [0, 1, 0]);
        mat4.translate(mvMatrix, [transX +TX,transY + TY,transZ +TZ]);

1) 将其翻译为零

2) 旋转

3) 在三维世界中将其翻译为最后或当前位置。


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