图形流水线中光栅化原理与实现
光栅化主要解决的问题
在传统图形学流水线,技术难点可以分为两大类:
- 可见性(visibility)
- 着色(shading)
文章图形流水线中坐标变换详解:模型矩阵、视角矩阵、投影矩阵中的坐标系变换和本文介绍的光栅化就是解决 可见性(visibility) 的关键技术。
着色(shading) 涉及到后续局部光照模型以及全局光照模型。这类知识点在以后我学明白后再写文章介绍给大家。
光栅化原理
-
那什么是可见性问题呢?简单来说就是处在后面的物体应该被前面的物体所遮挡,从而在渲染结果上表现为不可见(对于不透明物体来说是这样,对于透明物体来说是颜色混合)。
-
结合文章(图形流水线中坐标变换详解:模型矩阵、视角矩阵、投影矩阵),所有三角形点在经过模型矩阵变换、视角矩阵变换、投影矩阵变化以及透视除法后,坐标都变换到NDC坐标系下(x,y,z∈【-1, 1】)。而在知道输出屏幕大小的情况下,通过窗口变换可将x/y变换到窗口大小下(x∈【0,width】 y∈【0,height】z不变)。至此我们即将所有三角形投射到raster_space中。
-
光栅化的作用是判断哪些像素点与三角形“干涉”(在三角形内部),并插值出三角形内部点的属性值(Z值、颜色、法向、纹理坐标等)。
-
光栅化解决可见性(visibility) 思想:借助Z-Buffer, FrameBuffer
- Z-Buffer是一个与光栅图像大小一致的二维数组,记录着每个像素点的深度值。
- Z-Buffer中所有值初始化为inifity,当三角形上点P的Z值小于Z-Buffer中记录的值时,Z-Buffer中相应位置深度值更新为P点的Z值。否则不进行更新。
- FrameBuffer也是一个与光栅图像大小一致的二维数据,记录着每个像素点的颜色值
- 当Z-Buffer深度值更新时,FrameBuffer对应处颜色值也进行更新。
-
光栅化流程:
//遍历所有三角形
FOR 每个已经转换到窗口坐标系(raster space)下的三角形:
//内部两重循环 遍历所有像素点
FOR row in raster space:
FOR column in raster space:
pixelCoord = vec2(row, column)
IF pixelCoord 在三角形内部:
插值pixelCoord 的Z值
用Z值与Z-Buffer做深度测试
IF 通过深度测试:
更新Z-Buffer对应处的Z值
插值pixelCoord的其他顶点属性(color, normal, uv)
将像素点处的颜色值写入FrameBuffer
根据光栅化步骤,我们可以提出两个关键点:
- 如何判断某个像素在三角形内部
- 如何正确插值顶点属性
判断像素在三角形内部or外部
前提条件:三角形都已投影到raster space空间中
- 该问题可以简化为,在二维平面中,如何判断一个点在三角形内部。如果你学过计算几何中凸包求解问题。你可能记得里面有一个TO-LEFT测试:一条向量无限延长后可以将平面分成两部分,分别称为其左侧和右侧
给定三角形旋向,三角形三条边即可以确定三条向量,如果对于一个点P,对这三条向量来说,它都处于同一侧(都在左侧(或者边上)、都在右侧(或者边上)),那个我们可以称点P它在三角形内部。具体情况如下图所示:
- 用向量叉乘判断点P在向量的左侧或右侧或在直线上
因此只需要判断三次叉乘结果是否同号(或者=0)即可判断点是否在上三角形上 - 判断像素在三角形内部or外部 伪代码
//edge Funciton
bool edgeFunction(vec2 p, vec2 a, vec2 b, vec2 c){
//三角形三个点abc构成三个向量ab bc ca
vec2 ab = b - a;
vec2 bc = c - b;
vec2 ca = a - c;//待判断点p分别于abc构成三个向量 ap, bp, cp
vec2 ap = p - a;
vec2 bp = p - b;
vec2 cp = p - c;
//得到三次叉乘结果
result1 = ab * ap;
result2 = bc * bp;
result3 = ca * cp;
//判断是否同号
float threshold = 1e-5;
if(result1 > -threshold && result2 > -threshold && result3 > -threshold)//都为非负数
return true;
if(result1 < threshold && result2 < threshold && result3 ><threshold)//都为非正数
return true;
return false;
}
顶点属性插值
流水线中我们只会给三角形顶点赋值某些顶点属性,光栅化需要根据三个顶点信息插值三角形内部点的顶点属性。
重心坐标系
对于三角形内部点P来说,它可以由顶点V0/V1/V2唯一表示:
p = λ0 * V0 + λ1 * V1 + λ2 * V2 且 λ0 >=0 λ1 >=0 λ2 >=0; λ0 + λ1 + λ2 = 1
(λ0, λ1, λ2)即V0V1V2组成的三角形内部点P的重心坐标
同理我们可以用重心坐标来插值顶点属性
P_attribute = λ0 * V0_attribute + λ1 * V1_attribute + λ2 * V2_attribute
- 既然可以用重心坐标系来插值所有顶点属性信息,那么又如何计算重心坐标系呢?
观察上图,你可以从中看出,重心坐标系值与V0V1P/V1V2P/V2V0P这三个三角形的面积有关。
由向量叉乘的集合意义可知: 叉乘得到的行列式的值,即两个向量所围成的平行四边形面积
结合判断点P是否在三角形内部的函数。我们可以在判断点P是否在三角形内部时,同时计算出P点的重心坐标系值。这是不是非常巧妙!
//改进 edge Funciton, 判断点p是否在三角形abc上时,
//同时返回其重心坐标系值
bool edgeFunction(vec2 p, vec2 a, vec2 b, vec2 c, vector<< float>& barycentricCoord ){
//三角形三个点abc构成三个向量ab bc ca
vec2 ab = b - a;
vec2 bc = c - b;
vec2 ca = a - c;
//三角形面积的两倍
float triangleArea = abs(ab * bc);
//待判断点p分别于abc构成三个向量 ap, bp, cp
vec2 ap = p - a;
vec2 bp = p - b;
vec2 cp = p - c;
//得到三次叉乘结果
result1 = ab * ap;
result2 = bc * bp;
result3 = ca * cp;
//计算重心坐标系
barycentricCoord.push_back(abs(result2) / triangleArea);
barycentricCoord.push_back(abs(result3) / triangleArea);
barycentricCoord.push_back(abs(result1) / triangleArea);
//判断是否同号
float threshold = 1e-5;
if(result1 > -threshold && result2 > -threshold && result3 > -threshold)//都为非负数
return true;
if(result1 < threshold && result2 < threshold && result3 ><threshold)//都为非正数
return true;
return false;
}
插值深度
直接插值Z值的问题
根据上文结论可得,对于三角形内部点p,其Z值可以由此插值:
P_z = λ0 * V0_z + λ1 * V1_z + λ2 * V2_z
但实际上直接使用此式子是错误的,因为经过投影变换后,Z值不再满足线性变化。下图可以清楚的展现这种错误:
在投影前,P的Z值为:
P.z = V0.z * (1 - 0.666) + V1.z * 0.666 = 4.001;
投影后,P‘的Z值为
P’.z = V0.z * (1 - 0.8333) + V1.z * 0.8333 = 4.499;
正确的深度插值公式推导
因此正确的深度插值公式为:
1 / P_z = λ0 * 1 / V0_z + λ1 * 1 / V1_z + λ2 * 1 / V2_z
透视校正
同理在插值其他顶点属性(如颜色、法相、纹理坐标)时,如果直接用重心坐标系,也不正确。
因此正确的方法是借助已经正确插值的Z值,属性与Z值满足线性变化关系。
因此正确的顶点属性插值公式为:
Attr = Z * [Attr0 / V0_z * λ0 +Attr1 / V1_z * λ1 + Attr2 / V2_z * λ2 ]
下图展示了使用正确的属性插值与不正确的属性插值的颜色误差。
代码实现
vector2.h
//vector2.h
#pragma once
#include<iostream>
using namespace std;
template <typename T>
class Vector2 {
public:
T x, y;
float z;
public:
//Vector(){}
~Vector2() {
}
Vector2(T xx = 0, T yy = 0, float zz = 0) :x(xx), y(yy), z(zz) {
}
Vector2(Vector2& t) {
x = t.x; y = t.y; z = t.z; }
//标量乘除
Vector2 multiplayByScalar(float a, Vector2<T>& result) {
result.x = x * a;
result.y = y * a;
return result;
}
Vector2 divideByScalar(float a, Vector2<T>& result) {
result.x = x / a;
result.y = y / a;
return result;
}
//矢量运算
Vector2<T> add(Vector2& t, Vector2<T>& result) {
result.x = x + t.x;
result.y = y + t.y;
return result;
}
Vector2<T> divide(Vector2& t, Vector2<T>& result) {
result.x = x - t.x;
result.y = y - t.y;
return result;
}
float cross(Vector2& t) {
return y * t.x - x * t.y;
}
//模场
float length() {
return sqrt(x * x + y * y);
}
//归一化
void normalize() {
float l = this->length();
if (l < 1e-5) {
cout << "向量模长为0.0,不能归一化" << endl;
return;
}
x /= l;
y /= l;
}
//流运算
friend ostream& operator<<(ostream& os, const Vector2<T>& t) {
os << "(x, y, z):" << "(" << t.x << ", " << t.y << ", " << t.z << ")" << endl;
return os;
}
};
Vector3.h
//Vector3
#pragma once
#include<iostream>
using namespace std;
template <typename T>
class Vector3 {
public:
T x, y, z;
float w;
public:
//Vector(){}
~Vector3() {
}
Vector3(T xx = 0, T yy = 0, T zz = 0, float ww = 1.0) :x(xx), y(yy), z(zz), w(ww) {
}
Vector3(Vector3& t) {
x = t.x; y = t.y; z = t.z; w = t.w; }
//标量乘除
Vector3 multiplayByScalar(float a, Vector3<T>& result) {
result.x = x * a;
result.y = y * a;
result.z = z * a;
return result;
}
Vector3 divideByScalar(float a, Vector3<T>& result) {
result.x = x / a;
result.y = y / a;
result.z = z / a;
return result;
}
//矢量运算
Vector3<T> add(Vector3& t, Vector3<T>& result) {
result.x = x + t.x;
result.y = y + t.y;
result.z = z + t.z;
return result;
}
Vector3<T> divide(Vector3& t, Vector3<T>& result) {
result.x = x - t.x;
result.y = y - t.y;
result.z = z - t.z;
return result;
}
Vector3<T> cross(Vector3& t, Vector3<T>& result) {
result.x = y * t.z - z * t.y;
result.y = z * t.x - x * t.z;
result.z = x * t.y - y * t.x;
return result;
}
//比较运算
bool equal(Vector3<T>& t) {
float threshold = 1e-5;
if (abs(x - t.x) < threshold && abs(y - t.y) < threshold && abs(z - t.z) < threshold)
return true;
return false;
}
//透视除法
void perspectiveDivision() {
x = x / T(w);
y = y / T(w);
z = z / T(w);
w = 1.0f;
}
//模场
float length() {
return sqrt(x * x + y * y + z * z);
}
//归一化
void normalize() {
float l = this->length();
if (l < 1e-5) {
cout << "向量模长为0.0,不能归一化" << endl;
return ;
}
x /= l;
y /= l;
z /= l;
}
//流运算
friend ostream& operator<<(ostream& os, const Vector3<T>& t) {
os << "(x, y, z, w):" << "(" << t.x << ", " << t.y << ", " << t.z << ", " << t.w << ")" << endl;
return os;
}
};
Matrix4.h
//Matrix4.h
#pragma once
#include "Vector3.h"
class Matrix4
{
public:
Matrix4();
~Matrix4();
Matrix4(float a0, float a1, float a2, float a3,
float a4, float a5, float a6, float a7,
float a8, float a9, float a10, float a11,
float a12, float a13, float a14, float a15);
Matrix4(Matrix4& t);
//重置为单位矩阵
void setIdentityMatrix();
//求矩阵的行列式的值
float getDeterminant();
//求矩阵的逆矩阵
Matrix4 invert();
//矩阵与向量/点相乘
Vector3<float> multiplyByVector(Vector3<float> v);
//矩阵与矩阵相乘
Matrix4 multiplyByMatrix4(Matrix4& m);
//交换矩阵的两行
void swapRow(int row1, int row2);
//输出矩阵
friend ostream& operator<<(ostream& os, Matrix4& t);
public:
float m[16];
};
Matrix4.cpp
//Matrix4.cpp
#include "Matrix4.h"
Matrix4::Matrix4()
{
m[0] = m[5] = m[10] = m[15] = 1.0f;
m[1] = m[2] = m[3] = m[4] = m[6] = m[7] = m[8] = m[9] = m[11] = m