文章目录
- [1. 什么是渲染?](#1. 什么是渲染?)
- [2. Rasterization(光栅化)与 Ray Tracing(光线追踪)](#2. Rasterization(光栅化)与 Ray Tracing(光线追踪))
-
- [2.1 Rasterization(光栅化)](#2.1 Rasterization(光栅化))
-
- [2.1.1 3D Graphics Pipeline(3D 图形管线)](#2.1.1 3D Graphics Pipeline(3D 图形管线))
- [2.1.2 3D 图元(3D Primitives)](#2.1.2 3D 图元(3D Primitives))
- [2.1.3 渲染管线(Rendering Pipeline)](#2.1.3 渲染管线(Rendering Pipeline))
- [2.1.4 图形管线(Graphics Pipeline)](#2.1.4 图形管线(Graphics Pipeline))
-
- [2.1.4.1 Modeling Transformations(建模变换)](#2.1.4.1 Modeling Transformations(建模变换))
- [2.1.4.2 Illumination(光照) / Shading(着色)](#2.1.4.2 Illumination(光照) / Shading(着色))
- [2.1.4.3 Viewing Transformation(视图变换 / 相机变换)](#2.1.4.3 Viewing Transformation(视图变换 / 相机变换))
- [2.1.4.4 Clipping(裁剪)](#2.1.4.4 Clipping(裁剪))
- [2.1.4.5 Projection(投影)](#2.1.4.5 Projection(投影))
- [2.1.4.6 Rasterization(光栅化 / 扫描转换)](#2.1.4.6 Rasterization(光栅化 / 扫描转换))
- [2.1.4.7 Visibility / Display(可见性 / 显示)](#2.1.4.7 Visibility / Display(可见性 / 显示))
- [2.1.4.8 Unity中的渲染](#2.1.4.8 Unity中的渲染)
- [2.1.4.9 Common Coordinate Systems(常见坐标系)](#2.1.4.9 Common Coordinate Systems(常见坐标系))
- [2.1.4.10 Common Space(常见空间)](#2.1.4.10 Common Space(常见空间))
- [2.2 Ray Tracing(光线追踪)](#2.2 Ray Tracing(光线追踪))
- [3. 变换(Transformation)](#3. 变换(Transformation))
-
- [3.1 四种基本变换](#3.1 四种基本变换)
- [3.2 Rigid-Body / Euclidean Transforms(刚体变换 / 欧几里得变换)](#3.2 Rigid-Body / Euclidean Transforms(刚体变换 / 欧几里得变换))
- [3.3 相似变换(Similarity Transformations)](#3.3 相似变换(Similarity Transformations))
- [3.4 线性变换(Linear Transformation)](#3.4 线性变换(Linear Transformation))
- [3.5 仿射变换(Affine Transformation)](#3.5 仿射变换(Affine Transformation))
- [3.6 投影变换(Projective Transformation)](#3.6 投影变换(Projective Transformation))
- [3.7 透视投影(Perspective Projection)](#3.7 透视投影(Perspective Projection))
- [4. 变换(Transformation)的表示](#4. 变换(Transformation)的表示)
-
- [4.1 齐次坐标(Homogeneous Coordinates)](#4.1 齐次坐标(Homogeneous Coordinates))
-
- [4.1.1 齐次坐标把平移变成矩阵乘法](#4.1.1 齐次坐标把平移变成矩阵乘法)
- [4.1.2 w w w的意义](#4.1.2 w w w的意义)
- [4.2 平移(Translation)](#4.2 平移(Translation))
- [4.3 缩放(Scale)](#4.3 缩放(Scale))
- [4.4 旋转(Rotation)](#4.4 旋转(Rotation))
-
- [4.4.1 绕任意轴旋转(Rodrigues 公式)](#4.4.1 绕任意轴旋转(Rodrigues 公式))
- [4.5 变换组合](#4.5 变换组合)
- [5. 变换的意义](#5. 变换的意义)
- [6. Unity中的Transformation(变换)](#6. Unity中的Transformation(变换))
- [7. 世界、视图、投影变换](#7. 世界、视图、投影变换)
-
- [7.1 世界、视图、投影矩阵](#7.1 世界、视图、投影矩阵)
- [7.2 World Transformation(世界变换)](#7.2 World Transformation(世界变换))
-
- [7.2.1 Unity中如何实现变换矩阵(Transformations)](#7.2.1 Unity中如何实现变换矩阵(Transformations))
- [7.3 View Transformation(视图变换)](#7.3 View Transformation(视图变换))
- [7.4 Projection Transformation(投影变换)](#7.4 Projection Transformation(投影变换))
-
- [7.4.1 Orthographic Projection(正交投影)](#7.4.1 Orthographic Projection(正交投影))
- [7.4.2 Viewing Volume Clipping(视体裁剪)](#7.4.2 Viewing Volume Clipping(视体裁剪))
- [7.4.3 Unity里如何控制Projection Matrix](#7.4.3 Unity里如何控制Projection Matrix)
- [8. Unity实践](#8. Unity实践)
1. 什么是渲染?
计算机图形学的目标是 把三维场景(3D scene)生成二维图像(2D image)。
例如在游戏或电影里,虚拟世界是三维的,但显示器只能显示二维画面,所以需要把三维信息"投影"成二维图像。
输入是场景描述。这包括物体的形状、材质、位置,光源的位置和强度,摄像机位置等。
输出结果就是一张二维图像,可以显示在屏幕上。
一种方法是用计算机模拟摄像机或人眼。
也就是说,把三维世界看作是一个真实世界,计算机像相机一样拍照,把 3D 场景"投影"到二维平面上。
一个场景中主要有三个元素:
- Objects(物体):三维模型,比如球、立方体、角色。
- Lights(光源):照亮物体的光,包括点光源、平行光等。
- Viewer(观察者):观察图像的人或虚拟摄像机的位置。
那我们回到渲染上。
渲染就是自动把模型生成图像的过程,可以是:
Photorealistic(逼真图像):尽量接近真实世界的效果。
Non-photorealistic(非逼真图像):卡通风格、漫画风格、技术图纸等。
用刚刚的表达那就是:
输入:2D 或 3D 模型
输出:图像(2D)
渲染方法一般有两类主要方法:
-
Rasterization(光栅化)
特点:把三维场景投影到二维屏幕上,然后按像素绘制。
应用:主要用于 实时图形(Real-time Graphics),比如游戏、VR、交互式应用。
优点:速度快,适合实时渲染。
缺点:光影效果、反射、折射、全局光照等复杂效果难以完全逼真。
-
Ray Tracing(光线追踪)
特点:模拟光线从摄像机出发,与场景中的物体相交,计算光照、反射、折射。
应用:传统上用于 离线渲染(Offline Rendering),比如电影特效。
优点:图像逼真,可模拟复杂光照和反射。
缺点:计算量大,渲染慢。
趋势:随着 GPU 加速和实时光线追踪技术(RTX)发展,现在实时应用也可以部分使用 Ray Tracing
当然我们有可以把 Rasterization + Ray Tracing 结合使用:
- Rasterization 处理大部分几何和像素
- Ray Tracing 处理反射、阴影、折射等高质量效果
还有其他渲染方法这里不再详细叙述,比如Radiosity(辐射度)用于模拟全局光照(Global Illumination),尤其是光线在墙面、地面等表面多次反射后的光照效果。
2. Rasterization(光栅化)与 Ray Tracing(光线追踪)
2.1 Rasterization(光栅化)
步骤如下:
- 场景中的几何图元(如三角形、立方体、模型等)被投影到二维图像平面上。
- 光栅化器(Rasterizer)决定每个像素是否被某个图元覆盖。

特点:速度快,适合实时渲染(游戏、VR),但光照效果只是近似。

Rasterization(光栅化) 把三维场景投影到二维图像平面,然后决定每个像素的颜色。
左边的兔子模型是一个由三角形网格组成的三维对象。
三角形网格是光栅化的基本单位(primitive)。
虚拟摄像机或观察者看到模型时,光栅化器会把三角形投影到二维屏幕(image plane)。
图中用虚线表示从三维模型到屏幕的投影。
对于每个投影到屏幕上的三角形,光栅化器计算每个像素是否被覆盖(填充)。对每个像素计算深度(Depth),保留最靠近摄像机的像素值(z-buffer,确保前面的物体挡住后面的物体)。
伪代码如下。
for (each triangle)
for (each pixel)
if (triangle covers pixel)
keep closest hit
图片右上角显示了填充后的像素结果(灰度图或 RGB 通道分离示意)。
然后每个三角形被投影之前,会先计算照明(Compute Illumination),即判断颜色、阴影、反射等近似光照效果。
右侧三个彩色方框(红、绿、蓝)可能是示意光栅化对每个颜色通道的处理。
2.1.1 3D Graphics Pipeline(3D 图形管线)

GPU 渲染三维模型到二维图像的整个流程如下:
- Vertex Processing(顶点处理)
输入:3D 模型的顶点信息(位置、法线、纹理坐标等)。
操作:将顶点从模型空间变换到世界空间,再变换到摄像机视图空间。
计算光照(可选)。
输出顶点坐标,用于后续光栅化。
图示:左边的兔子模型 → 投影到屏幕前的轮廓。 - Rasterization(光栅化)
功能:把三角形投影到二维屏幕平面,生成 fragments(片元)。
Fragments:类似像素,但尚未最终确定颜色。
每个 fragment 有二维位置和颜色信息。
图示:投影后的兔子轮廓 → 网格状片元。 - Fragment Processing(片元处理)
功能:对每个 fragment 进行处理,最终生成屏幕上的像素。
操作:隐藏面消除(Hidden Surface Removal):只保留最靠近摄像机的 fragment。
颜色计算与合成(Compositing):考虑透明度、光照、纹理等,得到最终像素颜色。
输出:最终二维图像,显示在屏幕上。
2.1.2 3D 图元(3D Primitives)
这一部分都可以参考 CPT205 知识点 CPT205 Pt.1。

这里介绍6钟基本图片。
- Point Lists(点列表)
只有点,没有线、没有面
图中就是一堆独立的绿色点
每个点互不连接
用途:粒子效果(雪花、星星、火花) - Line Lists(线段列表)
每两个点组成一条线
(A,B)、(C,D)、(E,F) 各自独立
线与线之间不连接
用途:绘制独立线段(比如辅助线) - Line Strips(连续线)
一条"连续折线"
点按顺序连接:A → B → C → D
不会断开
用途:路径、曲线、轨迹 - Triangle Lists(三角形列表)
每3个点组成一个独立三角形
(A,B,C)、(D,E,F)
每个三角形互不共享点
用途:最常见!构建3D模型的基础 - Triangle Strips(三角形带)
连续生成三角形(省点)
前3个点 = 第一个三角形
每增加一个点,就多一个三角形
例如:A,B,C → 三角形1
B,C,D → 三角形2
C,D,E → 三角形3
优点:更高效(减少重复顶点) - Triangle Fans(三角形扇)
所有三角形围绕一个中心点
中心点固定(图中底部那个点)
其他点围绕它形成"扇形"
用途:圆形、扇形结构(比如雷达、光束)
因此3D图元是由一组"顶点(vertices)"组成的一个完整3D对象。
最简单的图元是3D坐标系中的一组点,这叫做"点列表(point list)"。
通常,3D图元是多边形。多边形是由至少三个顶点围成的一个封闭3D图形。最简单的多边形是三角形。
图形API通常使用三角形来构建所有多边形,因为三角形的三个顶点一定在同一个平面上。
那如何做个立方体(cube)呢?
其实还是用三角形拼出来。一个立方体有6个面。
顶部用三角形带制作两个三角形拼成正方形,侧面用三角形带一次制作四个正方形,底部与顶部同理。

那我们在Unity里怎么实现呢?
Unity里用Mesh(网格)来实现。
你看到的是一个普通的 Cube,但你能发现每个面都有对角线,是因为每个面其实被拆成了两个三角形。
所以Unity 也是用三角形来构建立方体的。
我们可以看右边 Inspector 面板信息。
在 Unity Inspector 里显示:Vertices: 24(顶点)、Triangles: 12(三角形)。
数学上的立方体确实只有 8 个角点,但:在图形学里是 24 个顶点,因为每个面有自己的法线(方向),每个面可能有不同纹理(UV)。所以一个角点会被拆成多个"独立顶点",我们可以简单理解为 一个角 = 3个面 也就是 3个顶点。
12 个三角形很好理解,因为每个面是 2 个三角形。
相关的代码如下。
csharp
Mesh myMesh = GetComponent<MeshFilter>().mesh;
Vector3[] vertices = myMesh.vertices;
int[] triangles = myMesh.triangles;
这里的代码可以拿到相关网格的所有顶点,但是这里的 triangles 是三角形的索引而不是坐标,它是顶点的编号。
2.1.3 渲染管线(Rendering Pipeline)
我们现在解释一下渲染管线(Rendering Pipeline)在 GPU 里是怎么一步步工作的。

- Input Assembly(输入组装)
把来自内存(Memory)数据准备好:
顶点数据(vertex buffer)
索引数据(index buffer)
组合成:
点 / 线 / 三角形 - Vertex Shading(顶点着色器)
处理每一个顶点
计算位置(模型 → 屏幕)
可以做动画、变形 - Rasterization(光栅化)
把三角形变成像素
判断哪些像素被三角形覆盖
生成"片段(fragment)" - Early Depth Test(提前深度测试)
提前剔除看不见的像素
被挡住的直接丢掉
提高性能 - Pixel Shading(像素着色器)
给每个像素上色
计算颜色
加光照、阴影、纹理 - Depth Test(深度测试)
再次确认前后关系
哪个像素在前面?
哪个被遮挡? - Render Target Output(输出)
最终写入屏幕
2.1.4 图形管线(Graphics Pipeline)
这一部分在 CPT205 中也很详细 相关知识点。

先是MC(Model Coordinate,模型坐标系)通过Modelling Transformation(模型变换)变换到WC(World Coordinate,世界坐标系),再通过Viewing Transformation(视图变换)变换到VC(Viewing Coordinate/Cammera Coordinate,相机坐标系),再通过Projection Transformation/Perspective Distortion(投影变换)变换到Perspective Coordinate(或者Clip Coordinates,裁剪坐标),再通过Clipping(裁剪)获得的是Still Clip Coordinate,再经过Homogeneous Divide(齐次除法)变换到NC(Normalization Coordinate,规范化坐标系,或者叫Window Coordinates),最后经过Window / Viewport Transform得到屏幕坐标(Screen Coordinates,或者叫Viewport Coordinates)。

下面介绍一个更加详细的理论版本:

这里输入是Geometric model(几何模型)、Lighting model(光照模型)、Camera(相机)、Raster viewport(屏幕)。
输出是每个像素的颜色值RGB(24-bit颜色)。
渲染管线会经过一系列阶段处理图元(Primitives),每个阶段都会把结果传递给下一个阶段。
我们需要注意的是管线可以用不同方式表示和实现,不同系统/引擎实现流程不完全一样,有些阶段在硬件中执行,有些在软件中执行。例如GPU:光栅化、像素着色。CPU:准备数据、逻辑控制。某些阶段可以进行优化,并支持编程扩展。
2.1.4.1 Modeling Transformations(建模变换)
3D模型是在它"自己的坐标系"中定义的(对象空间 / Object Space),建模变换把模型放到一个统一的坐标系中(世界空间)。

2.1.4.2 Illumination(光照) / Shading(着色)
这一步决定物体看起来是什么颜色、亮不亮、有没有高光。
顶点的光照(着色)取决于:
- 材质(material)
- 表面属性(法线 normal)
- 光源(light sources)
还可以使用局部光照模型(比如 Diffuse、Ambient、Phong 等),常见的模型有:
- Ambient(环境光)
基础亮度
不管有没有光,都有一点亮
防止全黑 - Diffuse(漫反射)
最真实的基础光
光照角度越正 → 越亮
常见的"自然光效果" - Phong(高光)
反光点
金属、塑料那种"闪光点"

2.1.4.3 Viewing Transformation(视图变换 / 相机变换)
把世界空间(World Space)映射到眼睛空间(Eye Space).
具体操作是把相机位置移动到原点,并让视线方向对齐某个轴(通常是 z 轴)。
例如你相机在 (10, 0, 0),看向原点。系统会把所有物体往左移动 10,让相机变成在 (0,0,0)。

2.1.4.4 Clipping(裁剪)
这一步是将上一步结果转换到标准化设备坐标(NDC)

物体在视锥体(view frustum)之外的部分会被移除。

2.1.4.5 Projection(投影)
物体被投影到二维图像平面(屏幕空间)。
2.1.4.6 Rasterization(光栅化 / 扫描转换)
把物体光栅化为像素,在过程中进行插值(颜色、深度等)。

可以分成 3 步:
- 覆盖测试(Coverage)
判断:
哪个像素在三角形里面?
在 → 生成 fragment(片段)
不在 → 忽略 - 插值(Interpolation)
从顶点"推算"每个像素的数据:
比如:
颜色(Color)
深度(Depth)
法线(Normal)
纹理坐标(UV)
例如:
三角形三个顶点是红、绿、蓝。那么 中间的像素 = 混合颜色(渐变) - 生成 Fragment(片元)
每个像素候选点叫Fragment(片段)
注意:Fragment ≠ 最终像素,还要经过后面的测试(深度测试等)。
2.1.4.7 Visibility / Display(可见性 / 显示)
每个像素会记录"离相机最近的物体",这里依靠的机制是 Z-buffer。
渲染时:新像素更近 → 覆盖
更远 → 丢弃
图形管线几乎每一步都在改变坐标系,变换是理解3D图形的核心。
你看到的一切操作:移动(Position)、旋转(Rotation)、缩放(Scale)、相机视角。本质都是矩阵变换(Matrix Transform)。
2.1.4.8 Unity中的渲染
Unity帮你封装了整个Graphics Pipeline,我们主要控制"模型 + 材质 + Shader"。
Mesh Renderer 负责提交 Draw Call(绘制调用)。

Mesh Renderer负责告诉GPU画哪个模型、用什么材质、在哪里画。
渲染状态(如裁剪、深度测试)和光照由材质和Shader控制。
2.1.4.9 Common Coordinate Systems(常见坐标系)
我们现在总结一下这里遇到的坐标系。
- Object Space(物体坐标)
每个物体自己的坐标系。 - World Space(世界坐标)
所有物体共享的坐标系。 - Eye Space / Camera Space(相机坐标)
基于相机视锥体的坐标系。 - Clip Space / NDC(标准化设备坐标)
坐标被压缩到 [-1,1] 范围。 - Screen Space(屏幕坐标)
根据硬件(分辨率)定义的坐标。
2.1.4.10 Common Space(常见空间)
这个和前面近似。
- Object Space(物体空间)
每个模型自己的坐标
比如一个立方体以自己中心为原点 - World Space(世界空间)
所有物体统一放在同一个场景里。 - Eye Space / Camera Space(观察空间)
对应步骤:Viewing Transformation(视图变换)。把"世界"转换成"摄像机视角"。 - Clip Space(裁剪空间)
对应步骤:Projection + Clipping。把3D视锥(camera frustum)变成一个标准盒子。 - Screen Space(屏幕空间)
对应步骤:Rasterization(光栅化)。把标准坐标映射到屏幕像素。 - Visibility / Display(最终显示)
使用Depth Buffer(深度缓冲)。每个像素只显示离摄像机最近"的物体。

2.2 Ray Tracing(光线追踪)
步骤如下:
- 从摄像机或观察者的每个像素出发,发射一条采样光线(View Ray)。
- 光线穿过像素平面,检测与场景中物体的交点。
- 计算光照、反射、折射、阴影等效果(包括 Shadow Ray)。

特点:更逼真,可以模拟真实光的物理传播,但计算量大,传统上用于离线渲染。
3. 变换(Transformation)
Transformation(变换)让你可以控制物体在哪、怎么看、怎么显示。
变换可以把模型旋转成任何方向,可以控制"相机在哪看",还可以把3D世界变成2D屏幕。
我们可以组合多个变换,模拟复杂运动。
这一部分也可以参考 CPT205知识。
我们从最简单的二维开始理解。变换就是把一个点从一个坐标系"变换"到另一个坐标系。
原来的点 ( x , y ) (x, y) (x,y),变换之后变为 ( x ′ , y ′ ) (x', y') (x′,y′)。
我们可以用一个通用变换公式去概括。
x ′ = a x + b y + c x' = ax + by + c x′=ax+by+c
y ′ = d x + e y + f y' = dx + ey + f y′=dx+ey+f
a, b, d, e控制旋转(Rotate)、缩放(Scale)、拉伸(Shear)。
c, f控制平移(Translation)
3.1 四种基本变换

- Identity(恒等变换)
什么都不做。 - Translation(平移)
把物体移动位置。 - Rotation(旋转)
围绕某个点旋转。 - Scaling(缩放)
改变大小。
这些操作可以组合,而且都可以还原,但是除了把物体缩放成0以外。
3.2 Rigid-Body / Euclidean Transforms(刚体变换 / 欧几里得变换)
这是不改变形状和大小的变换。
具有两个关键性质:
- Preserves distances(保持距离):点与点之间的距离不变。
- Preserves angles(保持角度):所有角度都不变。
所以刚刚提到的四种中只有 Scaling 不包含,其余的都是刚体变换。
3.3 相似变换(Similarity Transformations)
相似变换指的是保持角度不变,长度可以改变(因为可以缩放)。所以就是形状不变,但大小可以变。
因此 相似变换 = 刚体变换 + 等比缩放。
3.4 线性变换(Linear Transformation)
线性变换 = 不包含平移的变换
也就是说原点 (0,0) 一定还是在原点。
常见的线性变换有:
- 缩放(Scaling),放大或缩小物体,可以是等比(uniform)也可以是不等比(non-uniform)。
- 反射(Reflection),镜像翻转(比如左右翻转)。
- 剪切(Shear),把物体"推斜"。

线性变化符合下面两个性质。 - L ( p + q ) = L ( p ) + L ( q ) L(p+q)=L(p)+L(q) L(p+q)=L(p)+L(q),两个点一起变换,等于分别变换再合起来。
- L ( a p ) = a L ( p ) L(ap)=aL(p) L(ap)=aL(p),先放大再变换 = 先变换再放大。
这两个性质是判断是否是线性变换的参考依据,因此如果有平移,那就一定不是线性变换。
3.5 仿射变换(Affine Transformation)
仿射变换 = 线性变换 + 平移.
其的核心是preserves parallel lines(保持平行线)。
原来是平行的线 → 变换后还是平行。
因此仿射变换包含了刚刚提到的一切变换。

3.6 投影变换(Projective Transformation)
投影变换 = 模拟"透视效果"的变换.
也就是现实中的近大远小,或者平行线的尽头会相交(铁轨)。

它又包含前面提到的所有,所以这些变换的关系图如下。

3.7 透视投影(Perspective Projection)
透视投影就是用"人的眼睛"来看世界,把3D变成2D。

4. 变换(Transformation)的表示
这里也和CPT205知识点一致。
在图形学中用矩阵进行表示。
最基础的:
x ′ = a x + b y + c x' = ax + by + c x′=ax+by+c
y ′ = d x + e y + f y' = dx + ey + f y′=dx+ey+f
所以用矩阵表示就如下所示:
x ′ y ′ \] = \[ a b d e \] \[ x y \] + \[ c f \] \\begin{bmatrix} x' \\\\ y'\\end{bmatrix}= \\begin{bmatrix} a \& b \\\\ d \& e \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} + \\begin{bmatrix} c \\\\ f \\end{bmatrix} \[x′y′\]=\[adbe\]\[xy\]+\[cf
简写为:
p ′ = M p + t p' = M p + t p′=Mp+t

这里 M M M是变换矩阵(改变形状), t t t是平移向量(改变位置)。
4.1 齐次坐标(Homogeneous Coordinates)
我们给坐标多加一维( w w w),让所有变换都可以用"矩阵乘法"统一表示。
正如前面所说,这里还有一个平移向量,因此无法用矩阵乘法统一表示。
所以我们现在加一个维度( w w w)。
所以现在矩阵表示为。
x ′ y ′ z ′ w ′ \] = \[ a b c d e f g h i j k l m n o p \] \[ x y z w \] \\begin{bmatrix} x' \\\\ y' \\\\ z' \\\\ w' \\end{bmatrix}= \\begin{bmatrix} a \& b \& c \& d \\\\ e \& f \& g \& h \\\\ i \& j \& k \& l \\\\ m \& n \& o \& p \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ z \\\\ w \\end{bmatrix} x′y′z′w′ = aeimbfjncgkodhlp xyzw 简写为 p ′ = M p p' = M p p′=Mp  #### 4.1.1 齐次坐标把平移变成矩阵乘法 x ′ = a x + b y + c x' = ax + by + c x′=ax+by+c y ′ = d x + e y + f y' = dx + ey + f y′=dx+ey+f 原来的写法如下: \[ x ′ y ′ \] = \[ a b d e \] \[ x y \] + \[ c f \] \\begin{bmatrix} x' \\\\ y' \\end{bmatrix}= \\begin{bmatrix} a \& b \\\\ d \& e \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} + \\begin{bmatrix} c \\\\ f \\end{bmatrix} \[x′y′\]=\[adbe\]\[xy\]+\[cf
现在我们改成齐次坐标就可以转化为:
x ′ y ′ 1 \] = \[ a b c d e f 0 0 1 \] \[ x y 1 \] \\begin{bmatrix} x' \\\\ y' \\\\ 1 \\end{bmatrix}= \\begin{bmatrix} a \& b \& c \\\\ d \& e \& f \\\\ 0 \& 0 \& 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} x′y′1 = ad0be0cf1 xy1 这就完成了从 p ′ = M p + t p' = M p + t p′=Mp+t到 p ′ = M p p' = M p p′=Mp的转化。 #### 4.1.2 w w w的意义 由于通常 w = 1 w=1 w=1,所以我们可以忽视它。 \[ x ′ y ′ z ′ 1 \] = \[ a b c d e f g h i j k l 0 0 0 1 \] \[ x y z 1 \] \\begin{bmatrix} x' \\\\ y' \\\\ z' \\\\ 1 \\end{bmatrix}= \\begin{bmatrix} a \& b \& c \& d \\\\ e \& f \& g \& h \\\\ i \& j \& k \& l \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ z \\\\ 1 \\end{bmatrix} x′y′z′1 = aei0bfj0cgk0dhl1 xyz1 如果我们使用仿射变换矩阵(Affine matrix)进行变换,那么就不会改变 w w w,也就是如果平移、旋转、缩放、剪切,都不会改变 w w w ,但是如果进行投射变换(Perspective),那么 w w w就会变。 我们可以将 w w w作为一个缩放因子。齐次坐标最终要除以 w w w才能得到真实坐标。这一步叫做归一化/齐次化(Homogenization)。  如图所示,这里 ( 8 , 10 , 2 ) → ( 8 / 2 , 10 / 2 , 2 / 2 ) = ( 4 , 5 , 1 ) (8,10,2)→(8/2,10/2,2/2)=(4,5,1) (8,10,2)→(8/2,10/2,2/2)=(4,5,1), ( x , y , w ) (x, y, w) (x,y,w)和 ( k x , k y , k w ) (kx, ky, kw) (kx,ky,kw)表示同一个点。 所以在在透视投影里远的点 → w 变大,然后除以 w → 变小,从而产生近大远小的效果。 这里的特殊情况是 w = 0 w=0 w=0,这里代表无穷远点(方向)。 ### 4.2 平移(Translation) 我们现在看在齐次坐标下怎么表示这些变换操作,我们从平移开始。   t x t_x tx表示沿 x x x轴移动多少。 t y t_y ty表示沿 y y y轴移动多少。 t z t_z tz表示沿 z z z轴移动多少。 ### 4.3 缩放(Scale) 缩放与平移类似。   s x s_x sx表示沿 x x x轴移动多少。 s y s_y sy表示沿 y y y轴移动多少。 s z s_z sz表示沿 z z z轴移动多少。 ### 4.4 旋转(Rotation) 计算机图形学是右手坐标系,所以逆时针是正方向。   这里图片展示的是绕z轴,绕不同轴的结果如下。  #### 4.4.1 绕任意轴旋转(Rodrigues 公式)  我们现在定义一个方向向量(单位向量) ( k x , k y , k z ) (kx, ky, kz) (kx,ky,kz),它是旋转轴。 例如: k = ( 0 , 1 , 0 ) k = (0, 1, 0) k=(0,1,0)就是绕 y y y轴旋转。  ### 4.5 变换组合 我们现在知道了如何进行单个变换操作,当然我们可以组合这些操作。 如下图所示。  我们先缩放 Scale(2,2),再平移 Translate(3,1)。 所以原始点 ( 1 , 1 ) (1,1) (1,1)就到了 ( 5 , 3 ) (5,3) (5,3)。 公式为 p ′ = T ( S p ) p' = T ( S p ) p′=T(Sp) 注意这里是矩阵,所以右边的先作用,因此 p ′ = T ( S ( p ) ) p' = T(S(p)) p′=T(S(p)),变为矩阵就是 T ⋅ S T · S T⋅S 平移矩阵: T = \[ 1 0 3 0 1 1 0 0 1 \] T = \\begin{bmatrix} 1 \& 0 \& 3 \\\\ 0 \& 1 \& 1 \\\\ 0 \& 0 \& 1 \\end{bmatrix} T= 100010311 缩放矩阵: S = \[ 2 0 0 0 2 0 0 0 1 \] S = \\begin{bmatrix} 2 \& 0 \& 0 \\\\ 0 \& 2 \& 0 \\\\ 0 \& 0 \& 1 \\end{bmatrix} S= 200020001 所以组合在一起那就是: T S = \[ 1 0 3 0 1 1 0 0 1 \] \[ 2 0 0 0 2 0 0 0 1 \] = \[ 2 0 3 0 2 1 0 0 1 \] TS =\\begin{bmatrix} 1 \& 0 \& 3 \\\\ 0 \& 1 \& 1 \\\\ 0 \& 0 \& 1 \\end{bmatrix}\\begin{bmatrix} 2 \& 0 \& 0 \\\\ 0 \& 2 \& 0 \\\\ 0 \& 0 \& 1 \\end{bmatrix}= \\begin{bmatrix} 2 \& 0 \& 3 \\\\ 0 \& 2 \& 1 \\\\ 0 \& 0 \& 1 \\end{bmatrix} TS= 100010311 200020001 = 200020311 既然是矩阵,所以下一个要注意的点是,矩阵乘法不满足交换律,也就是 T S ≠ S T TS ≠ ST TS=ST,这也是我们前一点强调的顺序问题。 比如我们前面的例子中,如果先平移再缩放的结果那就是 ( 1 , 1 ) → ( 4 , 2 ) → ( 8 , 4 ) (1,1) → (4,2) → (8,4) (1,1)→(4,2)→(8,4),和我们之前的得到的 ( 5 , 3 ) (5,3) (5,3)完全不一致。   我们在Unity里当然也符合这个规律, 模型矩阵 = T ⋅ R ⋅ S 模型矩阵 = T · R · S 模型矩阵=T⋅R⋅S,先缩放、再旋转、再平移。 ## 5. 变换的意义 变换有什么作用呢?变换在计算机图形学中无处不在。 把物体放到场景中的某个位置用的是平移(Translation)。 改变物体的形状用的是缩放(Scaling)。 复制多个物体用的是对同一个模型做不同变换(平移/旋转)。 我们将3D场景能变成屏幕画面用的是透视投影(Perspective Projection), 而我们的动画也是对模型里的人的身体的各部分不断做变换(旋转 + 平移)。  ## 6. Unity中的Transformation(变换)  Unity中有专门的Transform组件控制物体的变换中的平移、旋转、缩放。  也有专门的Camera组件修改Camera的各个属性。 Transform 组件本质上是一个 Model Matrix(模型矩阵),这个组件本质上维护了一个 4×4 变换矩阵(T · R · S)。 我们可以通过下面的代码获得其局部坐标。 ```csharp transform.localPosition ``` 通过下面的代码获得其在整个世界中的绝对位置。 ```csharp transform.position ``` 去获得真正的模型矩阵可以通过下面的代码。 ```csharp transform.localToWorldMatrix ``` 想获得视图矩阵可以通过下面的代码。 ```csharp Camera.main.worldToCameraMatrix ``` 想获得Projection Matrix(投影矩阵)可以通过下面的代码。 ```csharp Camera.main.projectionMatrix ``` 所以Unity的整个流程是local → world → camera → screen。对应的矩阵是Projection · View · Model · p。 因此Unity的渲染 = P ⋅ V ⋅ M ⋅ p = P · V · M · p =P⋅V⋅M⋅p。 ## 7. 世界、视图、投影变换 物体一开始是在自己的坐标系里。 我们可以使用世界变化从而实现移动、旋转和缩放操作,这时候物体就在世界坐标系里。 然后我们确定摄像机位置、摄像机方向从而通过 View Matrix(视图矩阵)来使用另一个变换来定位和旋转我们的视角。这样一来,物体就被转换到了 View Space(相机空间)。 最后一个变换是投影变换,它把 3D 场景投影到 2D 屏幕上。  ### 7.1 世界、视图、投影矩阵 不同矩阵分别在变换什么。 世界矩阵可以用来平移、旋转、缩放物体。 视图矩阵就是"相机"。 投影矩阵就是"相机镜头"。  ### 7.2 World Transformation(世界变换) World Transformation(世界变换)把物体从"自己的坐标系"放到"世界坐标系"。  例如一个立方体中心在 (0,0,0),顶点在 (-1,1,-1) 这些坐标只和这个模型有关。 而世界坐标系就是将整个场景的统一坐标系(包括地面、玩家、敌人)都放进一个坐标系里。 再进行世界变换后,其就是在世界坐标系里,这里的所有点都是相对于"世界原点"的。 回到刚刚的例子中,这个模型没有任何修改,我们加上世界变换移动到 (10,0,5),那么它就进入了世界坐标系,如上图所示。 #### 7.2.1 Unity中如何实现变换矩阵(Transformations) 我们先回忆一下我们前面说的,Unity的标准顺序是先缩放 → 再旋转 → 再平移。 下面的代码进行了一个示范。 ```csharp Matrix4x4 T = Matrix4x4.Translate(new Vector3(3, 1, 0)); Matrix4x4 S = Matrix4x4.Scale(new Vector3(2, 2, 1)); Matrix4x4 TS = T * S; Matrix4x4 ST = S * T; Graphics.DrawMesh(mesh, TS, material, 0); ``` 这里前面创建了一个平移矩阵,将物体沿着x轴移动3,y轴移动1。 然后创建了一个缩放矩阵,x轴和y轴都放大2倍。 这里第三行是正确的操作,先进行缩放然后再平移(矩阵计算从右向左)。 而第四行的顺序就是先平移再缩放。 我们可以用最后一行去绘制模型。 这里依然强调一遍,矩阵乘法的交换律不成立,因此变换的顺序不同可能会得到完全不同的结果。  ### 7.3 View Transformation(视图变换) 视图变换在世界中定义相机的位置和方向,将所有的点从世界坐标系带到相机坐标系中。 在相机空间里相机永远在 (0,0,0),而且看向z轴方向。我们再强调一遍计算机图形学使用的是左手坐标系。 因此现在是世界在动而不是相机在动,因此现在不是移动相机而是将整个世界反向移动。  这里图片黑色坐标系是世界空间(世界坐标系),而蓝色坐标系是相机空间(相机坐标系),目前是变换前的情况,当视图变换应用后,整个世界会移动旋转从而让相机在(0,0,0)。 ### 7.4 Projection Transformation(投影变换) 投影变换让原来的3D世界变为2D的照片。 所以这一步类似给相机选镜头。 影响投影的因素有: 1. 视野范围(FOV) 大 → 广角(wide-angle) 小 → 长焦(telephoto) 2. 近裁剪面(near plane) 离相机太近的东西会被裁掉。 3. 远裁剪面(far plane) 太远的东西也不显示。  DirectX 写法: ```cpp XMMatrixPerspectiveFovLH( XM_PI/4, // FOV 1.25f, // aspect 1.0f, // near 2000.0f // far ); ``` OpenGL 写法: ```cpp gluPerspective(fov, aspect, near, far); ``` 这些都是用代码实现投影变换的示例。  #### 7.4.1 Orthographic Projection(正交投影) 这个与刚刚的透视投影相对立。 透视投影的效果是近大远小,正交投影的效果是远近一样大。  如图所示,这里不是锥体,而是一个长方体,因此是平行的没有缩放变化。 实现的代码如下: DirectX版: ```cpp XMMatrixOrthographicLH(width, height, nearZ, farZ) ``` OpenGL版: ```cpp glOrtho(left, right, bottom, top, near, far) ``` #### 7.4.2 Viewing Volume Clipping(视体裁剪) 当我们确定了我们的视体,不在视体的部分就会被裁剪,无论我们的视体的形状,我们都可以用六个裁剪面去确定。  #### 7.4.3 Unity里如何控制Projection Matrix  我们使用Unity里的Camera模块去控制Projection Matrix。 这里的参数都是前面所说的那些。 ## 8. Unity实践 我们现在如何在Unity中实践这些变换矩阵呢? 分四个步骤: 1. 定义物体。 2. 创建变换矩阵。 3. 矩阵相乘(把所有变换合成一个)。 4. 应用变换(将矩阵用在点上)。  我们先创建一个c#脚本,然后在Update()函数里输入代码。 ```csharp // -------------------------------------------------------- // STEP 1: Create the individual transformation matrices // -------------------------------------------------------- // Translation Matrix (T) Matrix4x4 T = Matrix4x4.Translate(translation); // Rotation Matrix (R) - Unity uses Quaternions to avoid Gimbal Lock, // but it mathematically constructs the 4x4 rotation matrix under the hood. Quaternion rot = Quaternion.Euler(rotationAngles); Matrix4x4 R = Matrix4x4.Rotate(rot); // Scale Matrix (S) Matrix4x4 S = Matrix4x4.Scale(scale); // -------------------------------------------------------- // STEP 2: Combine matrices into a single Model Matrix (M) // -------------------------------------------------------- // Standard order: Scale -> Rotate -> Translate // Note: In column-major math (like Unity/OpenGL), matrix multiplication is read right-to-left. // Formula: v' = T * R * S * v modelMatrix = T * R * S; // -------------------------------------------------------- // STEP 3: Apply the Model Matrix to every vertex // (Simulating the Application Stage passing data to the Geometry Stage) // -------------------------------------------------------- for (int i = 0; i < originalVertices.Length; i++) { // Convert Vector3 (x, y, z) to a homogeneous coordinate Vector4 (x, y, z, 1) // as explained in PDF Page 40. The multiplyPoint3x4 handles the w=1 implicitly. transformedVertices[i] = modelMatrix.MultiplyPoint3x4(originalVertices[i]); } // -------------------------------------------------------- // STEP 4: Assign the transformed vertices back to the mesh // -------------------------------------------------------- mesh.vertices = transformedVertices; ``` 这样我们就可以获得一个可以控制平移、旋转、缩放的组件了。  然后我们像前面说的一样,第一步定义物体。  我们已经完成了第二步和第三步,然后我们选择这个刚刚添加的Cube,点击右边的Add Component以添加组件然后找到我们刚刚编辑的脚本,或者我们直接拖拽脚本到这个新添加的Cube上,这样我们就可以将这个组件加到这个Cube上,这样就完成了应用。