CSharpGL(27)讲讲清楚OpenGL坐标变换 

在理解OpenGL的坐标变换问题的路上,有好几个难点和易错点。且OpenGL秉持着程序难以调试、难点互相纠缠的特色,更让人迷惑。本文依序整理出关于OpenGL坐标变换的各个知识点、隐藏规则、诀窍和注意事项。

+BIT祝威+悄悄在此留下版了个权的信息说:

OpenGL用4x4矩阵进行坐标变换。

OpenGL的4x4矩阵是按排列的。

CSharpGL(27)讲讲清楚OpenGL坐标变换

忘记glRotatef(),glScalef(),glTranslatef()什么的吧,那都属于legacy opengl,不久会被彻底淘汰。在modern opengl中有其他方式代替他们。

+BIT祝威+悄悄在此留下版了个权的信息说:

Model Space

为了描述3D世界,首先要设计一些三维模型出来。

设计三维模型的时候用的坐标系就是Model Coordinate System。

CSharpGL(27)讲讲清楚OpenGL坐标变换

只有1个模型

此时你所见的这个空间就是Model Space。Model Space里只负责描述一个模型。

有人可能会说,此图只设计了一个茶壶,如果我设计的是一套茶具(茶壶+几个茶杯),那不就是多个模型了吗?答:还真不是,此时应该把这套茶具视作一个整体,视为一个模型。回忆一下中学学的"确定研究对象"、"将XXX视作一个整体",就是这个意思。

围绕原点

在Model Space设计模型的时候,要注意使模型的包围盒的中心位于原点(0, 0, 0)。    

包围盒就是能够把模型包围的最小的长方体。

CSharpGL(27)讲讲清楚OpenGL坐标变换

为什么要围绕原点?因为这样才能在下文所述的World Space里"正常地"旋转、缩放和平移模型。

+BIT祝威+悄悄在此留下版了个权的信息说:

World Space

为何围绕原点

继续解释上面的问题。假设我们设计了一个立方体模型,它是关于原点(0, 0, 0)对称的。我们就这样让它降生到世界上。为了叙述方便,我们称其为Center。如下图所示。

CSharpGL(27)讲讲清楚OpenGL坐标变换

(再换个角度看)

CSharpGL(27)讲讲清楚OpenGL坐标变换

现在,我们再设计一个小一点的立方体模型,但这个立方体模型的中心不在原点(0, 0, 0)。为了叙述方便,我们称其为Corner。我们把这个Corner也放进来。

CSharpGL(27)讲讲清楚OpenGL坐标变换

(再换个角度看)

CSharpGL(27)讲讲清楚OpenGL坐标变换

现在,我们分别把CenterCorner缩小为原来的一半。我们希望的情形是这样的:

CSharpGL(27)讲讲清楚OpenGL坐标变换

(再换个角度看)

CSharpGL(27)讲讲清楚OpenGL坐标变换

为了看得清楚,我们把Center再扩大到原来的大小:

CSharpGL(27)讲讲清楚OpenGL坐标变换

(再换个角度看)

CSharpGL(27)讲讲清楚OpenGL坐标变换

可以看到Corner原来的位置上缩小了一半。这符合我们的预期。

但是,残酷的现实并非如此,当你把CenterCorner同时缩小一半时,你看到的情形会是这样:

CSharpGL(27)讲讲清楚OpenGL坐标变换

(再换个角度看)

CSharpGL(27)讲讲清楚OpenGL坐标变换

也就是说,一个缩放操作不仅改变了Corner的大小,还改变了它的位置。如果你在缩放之前把Camera对准了Corner,那么缩放之后Corner的位置发生了巨变,Camera很可能就看不到Corner了。

总结提升

如果一个模型的包围盒A在Model Space的中心不是(0, 0, 0),那么你可以想象有一个虚拟的包围盒B,B的中心是(0, 0, 0),且恰好能包围住A。然后,同时缩放A和B。由于B的中心是(0, 0, 0),缩放前后不会改变;而A的中心实际上是B内部一侧的一点,它是必然移动了的,即缩放操作改变了A的位置。

上述例子描述的是缩放操作,对于旋转操作,道理相同。

这就是保持模型的包围盒中心在原点(0, 0, 0)的好处。你可以随意旋转(rotate)、缩放(scale)模型,之后再移动(translate)到任意位置(此位置即模型在World Space里的位置)。无论你如何旋转、缩放此模型,它在移动(translate)之后的位置都是一样的。

CSharpGL(27)讲讲清楚OpenGL坐标变换

如上图所示,一个立方体向右移动4个单位,并进行了旋转和缩放操作。无论旋转角度、缩放比例是多少,其移动距离始终是4个单位。

Model Matrix

Model Matrix负责将模型从Model Space变换到World Space。

变换操作有三种:旋转(rotation)、缩放(scale)和平移(translate)。可以按字母表的顺序来记(Rotation, Scale, Translate)。

变换的顺序应当是:1旋转,2缩放,3平移。

设模型在Model Space里的任意一个顶点坐标为(x, y, z),我们想把模型放到World Space里的(tx, ty, tz)处,且绕y轴旋转r°,缩放为原来的s倍。那么:

平移矩阵为 mat4 translate = glm.translate(mat4.identity(), new vec3(tx, ty, tz)); ;

缩放矩阵为 mat4 scale = glm.scale(mat4.identity(), new vec3(s, s, s)); ;

旋转矩阵为 mat4 rotation = glm.rotate(mat4.identity(), (float)(r * Math.PI / 180.0), new vec3(0, 1, 0)); ;

总的Model Matrix为 mat4 modelMatrix = translate * scale * rotation; 。

为了获取(x, y, z)变换到World Space上的位置,首先将其扩充为四元向量(x, y, z, 1)。(不用管为什么不是(x, y, z, 0)),然后可得:vec4 worldPos = modelMatrix * new vec4(x, y, z, 1); 

性质

旋转、缩放操作都是关于原点(0, 0, 0)对称的。把模型的包围盒中心置于原点,会有难以言喻的好处。

 (worldPos.x, worldPos,y, worldPos.z) 就是 (x, y, z) 变换到World Space之后的位置。

+BIT祝威+悄悄在此留下版了个权的信息说:

 worldPos.w 必然是1。

对模型的操作顺序应当为rotation -> scale -> translate。

View/Eye/Camera Space

这三个名称是指同一个Space。

在World Space,各个模型都摆放好了位置和角度,之后就该从某个位置用Eye/Camera去看这个World。Camera有三个属性:eye/Position描述其位置,center/Target是朝向,Up是头顶。

Camera的Position是World Space里的一个点(Position.x, Position.y, Position.z),Target和Up是World Space里的2个向量。就是说,Camera.Position/Target/Up都是在World Space里定义的。

view matrix

Camera的参数(Position, Target, Up)决定了view matrix。模型在World Space里的位置,经过view matrix的变换,就变成了在View Space里的位置。

根据camera的Position, Target, Up求view matrix的过程就是著名的lookAt()函数。

 1         /// <summary>
 2         /// Build a look at view matrix.
 3         /// transform object's coordinate from world's space to camera's space.
 4         /// </summary>
 5         /// <param name="eye">The eye.</param>
 6         /// <param name="center">The center.</param>
 7         /// <param name="up">Up.</param>
 8         /// <returns></returns>
 9         public static mat4 lookAt(vec3 eye, vec3 center, vec3 upVector)
10         {
11             // camera's back in world space coordinate system
12             vec3 back = (eye - center).normalize();
13             // camera's right in world space coordinate system
14             vec3 right = upVector.cross(back).normalize();
15             // camera's up in world space coordinate system
16             vec3 up = back.cross(right);
17 
18             mat4 viewMatrix = new mat4(1);
19             viewMatrix.col0.x = right.x;
20             viewMatrix.col1.x = right.y;
21             viewMatrix.col2.x = right.z;
22             viewMatrix.col0.y = up.x;
23             viewMatrix.col1.y = up.y;
24             viewMatrix.col2.y = up.z;
25             viewMatrix.col0.z = back.x;
26             viewMatrix.col1.z = back.y;
27             viewMatrix.col2.z = back.z;
28 
29             // Translation in world space coordinate system
30             viewMatrix.col3.x = -eye.dot(right);
31             viewMatrix.col3.y = -eye.dot(up);
32             viewMatrix.col3.z = -eye.dot(back);
33 
34             return viewMatrix;
35         }
 上述函数中的right/up/back指的就是Camera的右侧、上方、后面,如下图所示。right/up/back是三个互相垂直的向量(构成一个右手系),且是在World Space中描述的。

CSharpGL(27)讲讲清楚OpenGL坐标变换

上述函数得到的结果 viewMatrix 可以用下图描述。[right/up/back]构成了旋转和缩放的部分,-[right/up/back]*eye构成了平移的部分。right/up/back分别描述了Camera坐标系的X/Y/Z轴,且在 viewMatrix 里也依次位于第0/1/2行。

CSharpGL(27)讲讲清楚OpenGL坐标变换

+BIT祝威+悄悄在此留下版了个权的信息说:

Clip Space

Camera摆好之后,要实现透视投影或正交投影。经过投影之后的坐标就是在Clip Space里的坐标。

透视投影

透视投影的效果就是近大远小:

CSharpGL(27)讲讲清楚OpenGL坐标变换

透视矩阵的作用就是设定下图所示的一个棱台范围,将Camera Space里的顶点位置变换一下。变换效果就是远处的点比变换之前更加靠近彼此,越远就靠近的越多。想象一下把这个棱台的Far面缓缓缩小到与Near面相同的大小,这一过程中,越远的顶点,被挤压的程度越大。

CSharpGL(27)讲讲清楚OpenGL坐标变换

根据棱台参数计算透视投影矩阵的函数就是著名的perspective()函数。

 1         /// <summary>
 2         /// Creates a perspective transformation matrix.
 3         /// </summary>
 4         /// <param name="fovy">The field of view angle, in radians.</param>
 5         /// <param name="aspect">The aspect ratio.</param>
 6         /// <param name="zNear">The near depth clipping plane.</param>
 7         /// <param name="zFar">The far depth clipping plane.</param>
 8         /// <returns>A <see cref="mat4"/> that contains the projection matrix for the perspective transformation.</returns>
 9         public static mat4 perspective(float fovy, float aspect, float zNear, float zFar)
10         {
11             float tangent = (float)Math.Tan(fovy / 2.0f);
12             float height = zNear * tangent;
13             float width = height * aspect;
14 
15             float left = -width, right = width, bottom = -height, top = height, near = zNear, far = zFar;
16 
17             mat4 result = frustum(left, right, bottom, top, near, far);
18 
19             return result;
20         }
21         /// <summary>
22         /// Creates a frustrum projection matrix.
23         /// </summary>
24         /// <param name="left">The left.</param>
25         /// <param name="right">The right.</param>
26         /// <param name="bottom">The bottom.</param>
27         /// <param name="top">The top.</param>
28         /// <param name="nearVal">The near val.</param>
29         /// <param name="farVal">The far val.</param>
30         /// <returns></returns>
31         public static mat4 frustum(float left, float right, float bottom, float top, float nearVal, float farVal)
32         {
33             var result = mat4.identity();
34 
35             result[0, 0] = (2.0f * nearVal) / (right - left);
36             result[1, 1] = (2.0f * nearVal) / (top - bottom);
37             result[2, 0] = (right + left) / (right - left);
38             result[2, 1] = (top + bottom) / (top - bottom);
39             result[2, 2] = -(farVal + nearVal) / (farVal - nearVal);
40             result[2, 3] = -1.0f;
41             result[3, 2] = -(2.0f * farVal * nearVal) / (farVal - nearVal);
42             result[3, 3] = 0.0f;
43 
44             return result;
45         }
perspective

相关文章:

  • 2021-09-16
  • 2021-10-28
  • 2022-12-23
  • 2021-10-31
  • 2021-08-05
  • 2022-12-23
猜你喜欢
  • 2022-12-23
  • 2021-05-17
  • 2021-12-28
  • 2021-05-16
  • 2021-10-28
  • 2022-12-23
相关资源
相似解决方案