一.前言
已经是三部曲中最后一个章节,这个章节结束之后将会完成这个SoftwareRender。而这章也是内容最多,最不容易理解的章节。
二.缓冲区
在写流水线之前我们要先实现缓冲区,因为流水线上的读写操作都是在缓冲区中实现的,一般来说缓冲区有颜色缓冲区、深度缓冲区、模板缓冲区。在这里为了简单,我们只实现模板缓冲区和深度缓冲区。对于缓冲区而言,我们就简单的提供三个接口:初始化、读、写。
class Device
{
public:
Device(int width, int height);
~Device();
void DrawPixel(int x, int y, easym::Vector4 color);
real GetZ(int x, int y) const;
void SetZ(int x, int y, real z);
inline UINT*& GetFrameBuffer() { return m_pFrameBuffer; }
inline int GetClientWidth() { return m_width; }
inline int getClientHeight() { return m_height; }
void ClearBuffer(Vector3 color);
private:
int m_width, m_height;
UINT* m_pFrameBuffer;
real **m_zBuffer;
};
三.流水线
你可以把流水线理解为从模型、贴图等等各种数据到最后像素颜色的过程。在这个系列中,我们只是实现一个简易的SoftwareRender。所以这里实现的也是简易的一个流水线。
- 顶点着色器
一开始,我们将输入的所有顶点数据传入至顶点着色器,利用顶点着色器的功能将顶点从物体坐标转化为投影坐标,同时也将顶点中的其他数据一起输出,供流水线后面的工作使用。
VertexOut DeviceContext::TransofrmToProjection(const VertexIn& v)
{
assert(m_pShader);
VertexOut out = m_pShader->VS(v);
//由于1/z和x,y成线性关系
//这里将需要插值的信息都乘以1/z 得到 s/z和t/z等,方便光栅化阶段进行插值
out.oneDivZ = 1 / out.posH.w;
out.posTrans *= out.oneDivZ;
out.color *= out.oneDivZ;
out.tex *= out.oneDivZ;
return out;
}
- 组装图元
我们按照图元的类别,按序将顶点组装成图元,在这个Software中,我们只提供了三角形这一类图元,那么其实在这里就只是按序将每三个顶点数据组成一个组。
for (UINT i = startIndexLocation; i < indexCount / 3; ++i)
{
VertexIn p1 = m_vertices[startVertexLocation + m_indices[3 * i]];
VertexIn p2 = m_vertices[startVertexLocation + m_indices[3 * i + 1]];
VertexIn p3 = m_vertices[startVertexLocation + m_indices[3 * i + 2]];
...
}
- 背面剔除(背面消隐,Face Culling)
如果对背面这个概念不是很熟悉的朋友可以先看看wiki Back-face Culling
当顶点着色器之后,所有顶点坐标都是在裁剪空间下,而且照相机(人眼)的位置为(0,0,0)。所以我们只需要用三个点组成的两个向量的叉乘的Z轴方向即可判断该三角形图元是背面还是正面。
bool DeviceContext::BackFaceCulling(const VertexIn & v1, const VertexIn & v2, const VertexIn & v3)
{
//线框模式不进行背面消隐
if (m_renderMode == FILL_WIREFRAME)
{
return true;
}
else
{
Vector3 vector1 = v2.pos - v1.pos;
Vector3 vector2 = v3.pos - v2.pos;
Vector3 normal = Cross(vector1, vector2);
Vector3 viewDir = v1.pos;
if (Dot(normal, viewDir) > 0)
{
return true;
}
return false;
}
}
for (UINT i = startIndexLocation; i < indexCount / 3; ++i)
{
...
if (BackFaceCulling(p1, p2, p3) == false)
{
continue;
}
...
}
- 裁剪
很显然,对于三个顶点都在视野外的图元,我们是没必要去光栅化的,那么如何判断顶点坐标是否在视野外呢?我们知道在透视投影中,我们做了两个事情,一是将3维空间的点投影到近投影面上,二是将点CVV化。不过,在透视投影阶段我们是没有做透视除法的。所以实际上在视野内的三个维度的范围应该是[-w,w],[-w,w],[0,w]。所以我们只需要判断点的坐标是不是在这个范围就知道顶点坐标是否在视野内。
bool DeviceContext::Clip(const VertexOut& v)
{
//cvv为 x-1,1 y-1,1 z0,1
if (v.posH.x >= -v.posH.w && v.posH.x <= v.posH.w &&
v.posH.y >= -v.posH.w && v.posH.y <= v.posH.w &&
v.posH.z >= 0.f && v.posH.z <= v.posH.w)
{
return true;
}
return false;
}
- 透视除法
将齐次坐标转化为非齐次坐标,也是透视投影中最核心的一步,也正是因为透视除法使得给人以近大远小的感觉
/* 透视除法 */
void DeviceContext::ToCVV(VertexOut & v)
{
v.posH.x /= v.posH.w;
v.posH.y /= v.posH.w;
v.posH.z /= v.posH.w;
v.posH.w = 1;
}
- 视口变换
根据ViewPort视口,进行视口变换,利用视口的选取,我们很轻松就可以实现画中画的效果。
void DeviceContext::TransformToScreen(const Matrix & m, VertexOut & v)
{
v.posH = v.posH * m;
}
- 光栅化三角形
最复杂的一步就是光栅化了,我们光栅化一个三角形,是根据三角形的顶点位置分为平顶三角形,平底三角形和普通三角形。对于普通三角形可以通过分割的方式变为一个平底三角形和一个平顶三角形,剩下的就是做线性插值。
void DeviceContext::TriangleRasterization(const VertexOut & v1, const VertexOut & v2, const VertexOut & v3)
{
if (v1.posH.y == v2.posH.y)
{
if (v1.posH.y < v3.posH.y)
{
DrawTriangleTop(v1, v2, v3);
}
else
{
DrawTriangleBottom(v3, v1, v2);
}
}
else if (v1.posH.y == v3.posH.y)
{
if (v1.posH.y < v2.posH.y)
{
DrawTriangleTop(v1, v3, v2);
}
else
{
DrawTriangleBottom(v2, v1, v3);
}
}
else if (v2.posH.y == v3.posH.y)
{
if (v2.posH.y < v1.posH.y)
{
DrawTriangleTop(v2, v3, v1);
}
else
{
DrawTriangleBottom(v1, v2, v3);
}
}
else
{
std::vector<VertexOut> vertices{ v1,v2,v3 };
std::sort(vertices.begin(), vertices.end(), [](VertexOut v1, VertexOut v2) {
return v1.posH.y < v2.posH.y; });
VertexOut top = vertices[0];
VertexOut middle = vertices[1];
VertexOut bottom = vertices[2];
real t = (middle.posH.y - top.posH.y) / (bottom.posH.y - top.posH.y);
VertexOut newMiddle = Lerp(top, bottom, t);
DrawTriangleTop(middle, newMiddle, bottom);
DrawTriangleBottom(top, middle, newMiddle);
}
}
void DeviceContext::DrawTriangleTop(const VertexOut & v1, const VertexOut & v2, const VertexOut & v3)
{
assert(m_pDevice);
real dy = 0;
int height = m_pDevice->getClientHeight();
for (real y = v1.posH.y; y <= v3.posH.y; y += 1)
{
int yIndex = static_cast<int>(y + static_cast<real>(0.5));
if (yIndex >= 0 && yIndex < height)
{
real t = 0;
if (!equal(v3.posH.y,v1.posH.y))
{
t = dy / (v3.posH.y - v1.posH.y);
}
VertexOut new1 = Lerp(v1, v3, t);
VertexOut new2 = Lerp(v2, v3, t);
dy += static_cast<real>(1);
if (new1.posH.x <= new2.posH.x)
{
ScanlineFill(new1, new2, yIndex);
}
else
{
ScanlineFill(new2, new1, yIndex);
}
}
}
}
void DeviceContext::DrawTriangleBottom(const VertexOut & v1, const VertexOut & v2, const VertexOut & v3)
{
assert(m_pDevice);
real dy = 0;
int height = m_pDevice->getClientHeight();
for (real y = v1.posH.y; y <= v2.posH.y; y += 1)
{
int yIndex = static_cast<int>(y + static_cast<real>(0.5));
if (yIndex >= 0 && yIndex < height)
{
real t = 0;
if (!equal(v2.posH.y, v1.posH.y))
{
t = dy / (v2.posH.y - v1.posH.y);
}
VertexOut new1 = Lerp(v1, v2, t);
VertexOut new2 = Lerp(v1, v3, t);
dy += static_cast<real>(1);
if (new1.posH.x <= new2.posH.x)
{
ScanlineFill(new1, new2, yIndex);
}
else
{
ScanlineFill(new2, new1, yIndex);
}
}
}
}
- 深度检测
深度检测是进行检测深度值得手段,也就是我们所说的前面的物体总是会’盖住’后面的物体,这里的前就是指深度值较小的,后面的物体是指深度值较大的。我们在渲染的时候是没办法保证从’最近’或者’最远’的顶点开始画;所以我们需要提供深度检测这样的手段,保证当已经渲染的较近的像素时,就不必计算较远的。在运行片元着色器之前,我们可以提前进行深度检测,这样可以避免对深度检测失败的点进行片元着色器的开销。
real oneDivZ = Lerp(left.oneDivZ, right.oneDivZ, lerpFactor);
//depth test
if (oneDivZ >= m_pDevice->GetZ(xIndex, yIndex))
{
...
}
- 片元着色器
光栅化最后,实际上是在两点之间进行插值填充,也是在这个时候,我们将插值得到的顶点数据传递给片元着色器,通过计算得到实际的像素颜色。
void DeviceContext::ScanlineFill(const VertexOut & left, const VertexOut & right, int yIndex)
{
assert(m_pDevice);
assert(m_pShader);
real dx = right.posH.x - left.posH.x;
int width = m_pDevice->GetClientWidth();
for (real x = left.posH.x; x <= right.posH.x; x += 1)
{
int xIndex = static_cast<int>(x + static_cast<real>(0.5));
if (xIndex >= 0 && xIndex < width)
{
real lerpFactor = 0;
if (!equal(dx, 0))
{
lerpFactor = (x - left.posH.x) / dx;
}
real oneDivZ = Lerp(left.oneDivZ, right.oneDivZ, lerpFactor);
//depth test
if (oneDivZ >= m_pDevice->GetZ(xIndex, yIndex))
{
m_pDevice->SetZ(xIndex, yIndex, oneDivZ);
real w = 1 / oneDivZ;
VertexOut out = Lerp(left, right, lerpFactor);
out.posH.x = xIndex;
out.posH.y = yIndex;
out.posTrans *= w;
out.tex *= w;
out.color *= w;
m_pDevice->DrawPixel(xIndex, yIndex, m_pShader->PS(out));
}
}
}
}
四.后言
本节完成之后,也表示这个SoftwareRender已经做完了。不过此时应该还看不到效果,因为现在只有一个渲染框架。我们还需要添加上层的逻辑代码才能使得渲染出东西,具体有兴趣大家可以去Github上看完整工程。
本节相关代码:Device.h,Device.cpp,DeviceContext.h,DeviceContext.cpp