变换
前言 在计算机图形学中,变换(transformation)是对点、向量的坐标进行修改的操作。渲染管线的各个环节(比如摆放物体、设置摄像机、把三维场景投影到屏幕上等)本质上都离不开变换。本节从缩放、旋转、平移这三种基础变换入手,介绍它们对应的矩阵,最后把二维的结论推广到三维空间。 在此先行强调一下:我们的变换通过矩阵乘法实现,即让在一个 矩阵/向量 上左乘一个变换矩阵,实现坐标的修改。 一些事实 在推导变换矩阵之前,先列出几个关于向量与矩阵的基本事实,后面的内容会反复用到: 一个 nnn 维向量左乘 nnn 阶单位矩阵(identity matrix)InI_nIn,得到它本身: (1001)(xy)=(xy)\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} x \\ y \end{pmatrix} (1001)(xy)=(xy) 若矩阵 AAA 可逆,则它与逆矩阵(inverse matrix...
模型、视图、投影(MVP)变换
需要预先了解: 变换 模型变换(Model Transformation) 模型变换(model transformation)把顶点从模型空间(model space,也称局部空间 local space)变换到世界空间(world space),对应的矩阵称为模型矩阵(model matrix),一般用符号 MMM 来表示。 计算机图形学中,我们经常会提到“空间”的概念,这里可以把空间理解为一个坐标系,用于描述物体的位置和方向。 具体地讲,通常一个模型在建模软件里被创造出来,里面需要存储顶点之类的模型的数据,建模的过程其实就是在模型空间中,原点通常落在模型的中心或脚底,轴向由建模习惯决定(Blender 默认 zzz 轴向上,glTF 和多数引擎默认 yyy 轴向上)。这些数字只有在"这个模型自己"的语境下才有意义。世界空间则是整个场景(scene)共享的坐标系,物体的摆放、光源与摄像机的位置都在这里描述,后续会讲到的视图变换与光照计算也都在这个空间里进行。 模型矩阵的组成 模型矩阵一般由平移(translation)、旋转(rotat...
顶点、图元与屏幕映射
需要预先了解: 模型、视图、投影(MVP)变换 模型的表示 我们想要在电脑里创建一个几何体,就是要想办法去存储一个模型。首先,不免要考虑一个模型应当用什么数据表示。表示方式决定了存储、处理和绘制的方方面面。 几何表示分两大类: 隐式表示(implicit representation):不直接给出表面上的点,而是给出点满足的关系。比如单位球面 x2+y2+z2=1x^2 + y^2 + z^2 = 1x2+y2+z2=1,以及距离场、水平集、CSG 等 显式表示(explicit representation):直接给出点,或者给出从参数到三维点的映射。比如点云、贝塞尔曲面、多边形网格。 两类表示正好互补: 表示方式 典型例子 判断点在内外 找到表面上的点 隐式 代数曲面、距离场 容易,代入函数看符号 困难,需求解方程 显式 点云、贝塞尔曲面、网格 困难 容易,直接给出或按参数计算 我们在实时渲染中更多地会使用显式表示,尤其是网格。因为显式表示可以直接送进 GPU 渲染管线,而隐式表示通常需要先离散化成显式表示才能渲染。 下面介绍几种常见...
光栅化
需要预先学习: 顶点、图元与屏幕映射 光栅化 光栅化(rasterization)是把 图元 转换为 片元(fragment)的过程。 光栅化是渲染管线中从"几何"跨越到"图像"的一步:之前的模型变换、视图变换、投影变换处理的都是连续的几何数据,而屏幕是离散的像素网格,光栅化负责决定每个像素显示成什么颜色。 本笔记的光栅化只考虑三角形图元,不对其他图元(线段、点、曲面)做讨论。 那么,光栅化一个三角形的核心问题只有一个:哪些像素被这个三角形覆盖? 三角形设置 三角形设置(triangle setup)在硬件里由光栅器的固定功能单元完成,对每个三角形执行一次。我们会在此阶段提前算出后面三角形遍历阶段要反复用的全部常量,给后面遍历做预处理,避免每个像素重复算一遍。 输入是三个屏幕空间的顶点(已经过了 视口变换),尽量做到把能提前算的都提前算好,追求在逐像素阶段就只剩加法与比较。 有向面积的计算 我们将三角形顶点记为 P0=(x0,y0)P_0 = (x_0, y_0)P0...
片元着色
需要预先学习: 算了别看了,前面的都得学 片元着色 我们在 光栅化 阶段得到了 片元,片元本身会携带顶点经过插值后的属性,但是这些属性距离最终显示的像素颜色还有一段距离。片元着色器(fragment shader)就是在这个阶段对片元进行处理,计算出最终的结果(与 顶点着色器 一样,我们需要的结果并不一定是颜色)。 也有人将片元着色器(fragment shader)称为像素着色器(pixel shader),但是严格来说片元与像素并不等价(详见 片元)。笔者认为"片元着色器"这个叫法更为恰当,因此本博客统一使用"片元着色器"。 前面的几篇处理的对象都不太一样:变换、投影处理的是顶点的坐标,图元装配与裁剪处理的是三角形本身,光栅化处理的是"哪些像素被覆盖"。到了片元着色,处理的对象变成每个被覆盖像素上的一个片元,它不再改变几何的形状,也不改变覆盖关系,只负责给这个片元算出一组值。 那么,片元着色的核心问题有两个:每个片元要算出什么值,以及这些值最终...
深度缓冲
需要预先学习: 光栅化 深度缓冲(Depth Buffer 或 Z-Buffer) 光栅化之后,屏幕上每个像素可能对应多个片元——多个三角形在投影后会互相重叠,同一个像素可能被多个三角形覆盖。最终该显示哪个片元,需要判断谁离摄像机最近。深度缓冲(depth buffer)就是为此设计的存储结构,也是深度测试(depth test)的基础。 帧缓冲区与深度缓冲 先分清两个容易混淆的缓冲。 帧缓冲区(frame buffer,这里特指颜色缓冲 color buffer)保存每个像素最终显示的颜色;深度缓冲(depth buffer,也称 Z-Buffer,因为存的是 z 坐标)保存每个像素当前最近的深度值。 缓冲 存储内容 每个像素存什么 用途 帧缓冲区(frame buffer) 颜色 一个颜色值(RGB) 最终显示 深度缓冲(depth buffer / z-buffer) 深度 一个深度值(z) 可见性判断 二者有两个重要的关系: 并列结构:帧缓冲与深度缓冲是两块并列的存储,尺寸与屏幕分辨率相同、逐像素一一对应——第 (x,y)(x, ...
着色与渲染管线
需要预先学习: 光栅化 深度缓冲 着色(Shading) 着色(shading)指对物体表面应用材质(material)的过程——计算表面每个点呈现的颜色。在渲染管线中,光栅化产生片元之后,由着色阶段决定片元的颜色。 着色的输入包括:光源的位置与强度、观察方向、表面法线(normal,表面在该点的朝向)、材质属性(颜色、光泽度等)。 着色是局部的:每个点独立计算颜色,只考虑光源与表面本身,不考虑其他物体。物体之间的遮挡(阴影)与光线的多次反弹属于全局光照(global illumination),由光线追踪等更复杂的算法处理,不在本节范围。 Blinn-Phong 反射模型 Blinn-Phong 反射模型是最常用的局部光照模型(local illumination model)之一,它把表面颜色分解为三部分之和:环境光(ambient)、漫反射(diffuse)、高光(specular)。 基本约定 在着色点(shading point,表面上的一个点)处定义以下向量: 符号 含义 n\mathbf{n}n 表面法线,单位向量 l\m...
纹理映射
需要预先学习: 着色与渲染管线 光栅化 插值 在渲染管线中,光栅化阶段(见 着色与渲染管线)需要为三角形内部的每个片元确定颜色、法线、纹理坐标等属性,而顶点只提供了属性在三个顶点处的值。三角形内部的属性要靠插值(interpolation)推算:已知离散的顶点值,估计连续区域内的值。 线性插值 线性插值(linear interpolation)是最基本的插值方式:在两个已知值之间按比例取中间值。一维情况下,在 aaa、bbb 之间按参数 t∈[0,1]t \in [0, 1]t∈[0,1] 插值: lerp(a,b,t)=(1−t)⋅a+t⋅blerp(a, b, t) = (1 - t) \cdot a + t \cdot b lerp(a,b,t)=(1−t)⋅a+t⋅b t=0t = 0t=0 得到 aaa,t=1t = 1t=1 得到 bbb,t=0.5t = 0.5t=0.5 得到中点。这个式子对二维点、三维向量同样成立,逐分量计算即可。 重心坐标 三角形内部任意一点的位置可以用重心坐标(barycentric coordinates...
几何体
注:本文内容已全部移动到 顶点、图元与屏幕映射 的「模型的表示」与「网格几何体的操作」两节,此文件待删除。
阴影
需要预先学习: 深度缓冲 ShadowMapping(阴影贴图) 着色与渲染管线 里提过,着色是局部的:每个点只根据光源与自身材质计算颜色,完全不知道其他物体的存在。所以 Blinn-Phong 管线本身画不出阴影——被遮挡的点和直接被照亮的点会得到相同的着色结果。ShadowMapping(阴影贴图)为光栅化管线补上这块能力,出发点是一个关键观察: 一个点不在阴影中,当且仅当光源能直接看到这个点。 "从某个视点看,谁能被看见"正是 深度缓冲 解决的问题。ShadowMapping 的思路,就是把深度测试搬到光源的位置再做一遍。 算法流程 ShadowMapping 分两遍(two-pass)渲染。 Pass 1:从光源渲染场景,只记深度。把光源当作一台摄像机——平行光配正交投影,点光源配透视投影(见 视图(摄像机)与投影)——渲染整个场景,但不存颜色,只把深度缓冲存下来。这张深度图就是阴影贴图(shadow map):它记录了从光源看过去,每个方向上最近的表面有多远。 **Pass 2:从摄像机正常渲染场景。**着...


