每一个游戏可以呈现炫丽效果的背后,需要进行一系列的复杂计算,同时也伴随着各种各样的顶点空间变换。渲染游戏的过程可以理解成是把一个个顶点经过层层处理最终转化到屏幕上的过程,本文就旨在说明,顶点是经过了哪些坐标空间后,最终被画在了我们的屏幕上。

空间变换的原理

首先,我们来看一个简单的问题:当给定一个坐标空间以及其中一点(a, b, c)时,我们是如何知道该点的位置的呢?

  1. 从坐标空间的原点开始
  2. 向x轴方向移动a个单位
  3. 向y轴方向移动b个单位
  4. 向z轴方向移动c个单位

坐标空间的变换就蕴含在上面的4个步骤中。现在,我们已知坐标空间C的3个坐标轴在坐标空间P下的表示Xc, Yc, Zc,以及其原点位置Oc。当给定坐标空间C中的一点Ac = (a, b, c),我们同样可以依照上面4个步骤来确定其在坐标空间P下的位置Ap

  1. 从坐标空间的原点开始,即 Oc
  2. 向x轴方向移动a个单位,即 Oc + aXc
  3. 向y轴方向移动b个单位,即 Oc + aXc + bYc
  4. 向z轴方向移动c个单位,即 Oc + aXc + bYc + cZc

对得到的表达式做如下变换,其中“|”符号表示按列展开

继续对其中的加法表达式做变换,即扩展到齐次坐标空间做平移变换

现在,我们得到了坐标空间C到坐标空间P的变换矩阵Mc->p

可以看出Mc->p实际上是通过坐标空间C在坐标空间P中的原点和坐标轴的矢量表示构建出来的:把3个坐标轴依次放入矩阵的前3列,把原点矢量放到最后一列,再用0和1填充最后一行即可。

我们可以利用反向思维,从这个变换矩阵中提取出坐标空间C的原点和坐标轴在坐标空间P的表示。例如,当我们已知从模型空间到世界空间的4×4变换矩阵,我们可以提取出它的第一列,再进行归一化(为了消除缩放的影响)来得到模型空间的x轴在世界空间下的单位矢量表示。同样的方法可以提取y轴和z轴。

当对方向矢量进行坐标空间变换时,由于矢量是没有位置的,因此坐标空间的原点变换是可以忽略的。那么对方向矢量的坐标空间变换就可以使用3×3的矩阵来表示,即

在Shader中,我们常常看到截取变换矩阵的前3行前3列来对法线方向,光照方向进行空间变化,这正是原因所在。

一旦求出来Mc->p,Mp->c就可以通过求逆矩阵的方式求出来,因为从坐标空间C变换到坐标空间P与从坐标空间P变换到坐标空间C是互逆的两个过程。当Mc->p是一个正交矩阵时,Mc->p的逆矩阵就等于它的转置矩阵,即

此时,我们还可以通过Mc->p反推出坐标空间P的坐标轴在坐标空间C中的表示Xp, Yp, Zp,这些坐标轴对应的就是Mc->p的每一行。

模型空间

模型空间,是和某个模型或者说是对象有关的,模型空间也被称为对象空间或局部空间。

每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着移动和旋转。

Unity在模型空间中使用的是左手坐标系,因此在模型空间中,+x轴,+y轴,+z轴分别对应的是模型的右,上,前向。

模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的。当导入到Unity中后,我们可以在顶点着色器中访问到模型的顶点信息,其中就包含了每个顶点的坐标。这些坐标都是相对于模型空间中的原点(通常位于模型的重心)定义的。

世界空间

世界空间是一个特殊的坐标系,因为它建立了我们所关心的最大空间,即整个游戏空间

在Unity中,世界空间同样使用了左手坐标系。它的x轴,y轴,z轴是固定不变的。

顶点变换的第一步,就是将顶点坐标从模型空间转换到世界空间中,这个变换通常叫做模型变换

在Unity中,我们可以通过Transform组件中的值得知模型做了哪些变换。这个值是根据Transform的父节点的模型坐标空间中的原点定义的,如果这个Transform没有任何父节点,那么这个值就是相对于世界坐标空间定义的。

要将模型空间中的一点转换到其父空间中,需要获取M子->父,这个矩阵可以通过模型的Transform值得到。Transform中包含了旋转,缩放和平移值,则M子->父 = Mtranslation Mrotate Mscale。而从模型空间转换到世界空间的变换矩阵M模型->世界可以通过子空间到父空间变换矩阵,父空间到爷爷空间变换矩阵,连乘,直到世界空间为止得到。

观察空间

观察空间也被称为摄像机空间,可以认为是模型空间的一个特例,即摄像机的模型空间。

在Unity中,观察空间使用的是右手坐标系,即+x轴指向右方,+y轴指向上方,+z轴指向摄像机后方

顶点变换的第二步就是将顶点坐标从世界空间变换到观察空间中。这个变换通常叫做观察变换

从观察空间到世界空间的变换矩阵我们同样可以通过Transform中的值得到,再对该矩阵求逆得到从世界空间到观察空间的变换矩阵。我们还可以使用另一种方法,对Transform组件中的值直接取反(做逆向变换),然后得到从世界空间到观察空间的变换矩阵。注意,由于观察空间使用的是右手坐标系,因此还需要对变换矩阵的z分量进行取反操作。

裁剪空间

顶点接下来要从观察空间转换到裁剪空间中,这个变换可以被称为投影变换。这个用于变换的矩阵叫做裁剪矩阵或是投影矩阵

裁剪空间的目的是能够方便地对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,与这块空间边界相交的图元就会被裁剪。而这块空间就是由视椎体来决定的。

视椎体有两种类型,分别对应两种投影类型:透视投影(下图左)和正交投影(下图右)。透视投影模拟了人眼看世界的方式,而正交投影则完全保留了物体的距离和角度。

投影矩阵虽然叫做投影矩阵,但并没有真正进行投影,而是为投影做准备。目的是对x,y,z分量进行缩放,经过投影矩阵的缩放后,我们可以直接使用w分量作为范围值,只有x,y,z分量都位于这个范围内的顶点才认为是在裁剪空间内。并且w分量在真正的投影时也会用到。

透视投影和正交投影分别对应了不同的投影矩阵。还需要注意的是投影矩阵会改变空间的旋向性:空间从右手坐标系变换到了左手坐标系

透视投影

其中FOV表示视椎体垂直方向的张开角度,而Near和Far分别控制了近裁剪平面和远裁剪平面距离摄像机的远近。这样我们可以求出近裁剪平面的高度,如下所示。远裁剪平面类似。

而根据摄像机的横纵比信息,我么就可以得到近裁剪平面的宽度

一个顶点和透视投影的投影矩阵相乘后得到的结果如下

视椎体的变化如下所示

此时我们就可以按如下不等式来判断一个变换后的顶点是否位于视椎体内

正交投影

其中Size表示视椎体竖直方向上高度的一半,而Near和Far同样分别控制了近裁剪平面和远裁剪距离摄像机的远近。则近裁剪平面的高度如下所示。远裁剪平面类似。

近裁剪平面的高度同样可以通过摄像机的纵横比得到

一个顶点和正交投影的投影投影矩阵相乘后得到的结果如下

视椎体的变化如下所示

判断一个变换后的顶点是否位于视椎体内使用的不等式和透视投影中的一样,这种通用性也是为什么要使用投影矩阵的原因之一。

屏幕空间

当完成了所有的裁剪工作后,就需要进行真正的投影了,即把视椎体投影到屏幕空间中,这个过程可以被称为屏幕映射。经过这一步变换,我们会得到真正的像素位置,对应的2D坐标,而不是虚拟的三维坐标。这个过程可以理解成有两步:

  1. 进行标准齐次除法,也被称为透视除法。就是把齐次坐标系的x,y,z分量都除以w分量。在OpenGL中把这一步得到的坐标叫做归一化的设备坐标(NDC)。经过透视投影变换后的裁剪空间会变换到一个立方体内,而正交投影的裁剪空间本身就是一个立方体(它的w分量是1,齐次除法不会对它产生影响)。在Unity中这个立方体的x,y,z分量的范围都是[-1, 1],和OpenGL保持一致。

  2. 经过齐次除法后,透视投影和正交投影的视椎体都变换到一个相同的立方体内。现在,我们可以根据变换后的x,y坐标来映射输出窗口对应的像素坐标。

在Unity中,屏幕空间左下角的像素是(0, 0),右上角的像素坐标是(pixelWidth, pixelHeight)。齐次除法和屏幕映射的过程可以使用下面的公式来表示

\[screen_x = \frac{clip_x * pixelWidth}{2 * clip_w} + \frac{pixelWidth}{2}
\]

\[screen_y = \frac{clip_y * pixelHeight}{2 * clip_w} + \frac{pixelHeight}{2}
\]

在Unity中,从裁剪空间到屏幕空间的转换是由底层帮我们完成的。我们的顶点着色器只需要把顶点转换到裁剪空间即可(模型空间-世界空间-观察空间-裁剪空间,对应的矩阵通常会串联成一个MVP矩阵)。

法线变换

最后,我们再来看一种特殊的变换:法线变换。在游戏中,模型的一个顶点往往会携带额外的信息,而顶点法线和切线就是其中的两种信息,切线和法线是互相垂直的。

由于切线是由两个顶点之间的差值计算得到的,因此我们可以直接使用变换顶点的矩阵MA->B来变换切线。但如果直接使用MA->B来变换法线,得到的新法线可能就不会和切线垂直了。例如下图所示.

那么应该使用哪个矩阵来变换法线呢?我们可以通过数学约束条件推出这个矩阵。由于顶点法线NA和切线TA垂直,则TANA = 0。给定变换矩阵MA->B,我们已知TB = MA->BTA。现在我们要找到一个矩阵G来变换法线NA,使得变换后的法线仍然与切线垂直,即

通过一些推导可得

由于TANA = 0,因此可得



这说明使用原变换矩阵的逆转置矩阵来变换法线就可以得到正确的结果。

值得注意的是,如果矩阵MA->B是正交矩阵,则我们可以直接使用原变换矩阵作为法线的变换矩阵。如果变换只包含旋转和统一缩放,我们可以利用统一缩放系数k来得到变换矩阵MA->B的逆转置矩阵,这样可以避免计算逆矩阵的过程。

视口空间

视口空间中的坐标被称为视口坐标,就是把屏幕归一化,这样屏幕左下角就是(0, 0),右上角就是(1, 1)。如果已知屏幕坐标的话,我们只需要把x,y分量除以屏幕分辨率即可得到视口坐标。如果已知裁剪空间中的坐标,可以通过以下公式得到视口坐标

3D游戏中各种空间变换到底是怎么回事的更多相关文章

  1. OpenGL中的空间变换

    OpenGL中的空间变换          在使用OpenGL的三维虚拟程序中.当我们指定了模型的顶点之后.在屏幕上显示它们之前,一共会发生3种类型的变换:视图变换.模型变换.投影变换.        ...

  2. 3D游戏中的画质与效率适配

      哪里来的需求? 众所周知,由于不同的设备配置不同.导致其CPU和GPU处理能力有高有低.同样的游戏想要在所有设备上运行流畅且画面精美,是不可能的.这就需要我们针对不同的设备能力进行画质调节,以保证 ...

  3. 3D游戏中人物换装解决方案

    换装基本上是每个网游都必须有的一个功能,每种网游的做法都各有不同,有些是换掉整个模型,有些则是通过可以换掉模型的一个部分完成.前者属于整体换,相对简单些:后者则是通过部分替换实现,目前用的比较多,本文 ...

  4. 转载:[转]如何学好3D游戏引擎编程

      [转]如何学好3D游戏引擎编程 Albert 本帖被 gamengines 从 游戏引擎(Game Engine) 此文为转载,但是值得一看. 此篇文章献给那些为了游戏编程不怕困难的热血青年,它的 ...

  5. [转]显卡帝揭秘3D游戏画质特效

    显卡帝揭秘3D游戏画质特效 近几年来,大量采用最新技术制作的大型3D游戏让大部分玩家都享受到了前所未有的游戏画质体验,同时在显卡硬件方面的技术革新也日新月异.对于经常玩游戏的玩家来说,可能对游戏画质提 ...

  6. 3D游戏与计算机图形学中的数学方法-变换

    1变换 在3D游戏的整个开发过程中,通常需要以某种方式对一系列的向量进行变换.通常用到的变换包括平移,缩放和旋转. 1.1通用变换 通常可将n x n可逆矩阵M看成是一个从坐标系到另一个坐标系的变换矩 ...

  7. OpenGL 的空间变换(上):矩阵在空间几何中的应用

    在使用 OpenGL 的应用程序中,当我们指定了模型的顶点后,顶点依次会变换到不同的 OpenGL 空间中,最后才会被显示到屏幕上.在变换的过程中,通过使用矩阵,我们更高效地来完成这些变换工作. 本篇 ...

  8. 图形与游戏中3D数学基础的说明

    1.左手坐标系与右手坐标系没有好坏之分,不同的研究领域和不同的背景下,选择不同的坐标系:传统计算机图形学采用“左手坐标系”,线性代数则倾向于使用右手坐标系 坐标系由坐标轴与坐标原点组成.原点定义坐标系 ...

  9. 3D游戏引擎中常见的三维场景管理方法

    对于一个有很多物体的3D场景来说,渲染这个场景最简单的方式就是用一个List将这些物体进行存储,并送入GPU进行渲染.当然,这种做法在效率上来说是相当低下的,因为真正需要渲染的物体应该是视椎体内的物体 ...

随机推荐

  1. 对Vue为什么不支持IE8的解释之一

    在JavaScript对象中有一个Object.defineProperties(obj, props)方法 该方法主要用来给指定对象添加自定义属性 可以接收两个参数: 第一个参数 要定义或者修改属性 ...

  2. 编程语言十万个为什么之java web的基础概念

    1.什么是JAVA Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由SunMicrosystems公司于1995年5月推出的Java程序设计语言和Java平台(即JavaSE, Ja ...

  3. 分析Java中的length和length()

    在不适用任何带有自动补全功能的IDE的情况下,我们怎么获取一个数组的长度?如何获取字符串的长度? 这里我们先举用实例去分析一下:int[] arr=new int[3]:System.out.prin ...

  4. linux下大文件处理

    linux下采用先分割后合并的策略处理大文件 第一步:分割文件 split split 参数:-a, --suffix-length=N     指定输出文件名的后缀,默认为2个-b, --bytes ...

  5. C++走向远洋——42(项目二,我的数组类,深复制,构造函数)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  6. Centos +Docker 安装及仓库使用概述

    ​1. Linux 系统学习Docker安装篇 这里我使用的Centos系统 安装Docker yum命令说明 即Yellowdog Update Modifier,是一种基于rpm的包管理工具 yu ...

  7. JavaScript(5)--- 面向对象 + 原型

    面向对象 + 原型 面向对象这个概念并不陌生,如 C++.Java 都是面向对象语言.面向对象而言都会现有一个类的概念 ,先有类再有对象.类是实例的类型模板. 比如人类 是一个类 张三 李四 就是一个 ...

  8. 手摸手教你在vue-cli里面使用vuex,以及vuex简介

    写在前面: 这篇文章是在vue-cli里面使用vuex的一个极简demo,附带一些vuex的简单介绍.有需要的朋友可以做一下参考,喜欢的可以点波赞,或者关注一下,希望可以帮到大家. 本文首发于我的个人 ...

  9. 用ABAP 生成二维码 QR Code

    除了使用我的这篇blogStep by step to create QRCode in ABAP Webdynpro提到的使用ABAP webdynpro生成二维码之外,也可以通过使用二维码在线生成 ...

  10. [LeetCode] 994. Rotting Oranges 腐烂的橘子

    题目: 思路: 每个腐烂的橘子都能将自己上下左右的新鲜橘子传染,像极了现在的肺炎... 如果格子中只有一个腐烂的橘子,那么这便是一个典型的层次遍历,第一个传染多个,称为第二层,第二层传染第三层 但这里 ...