铺垫 DepthBuffer

- 将 Triangle2D 扩展到 Triangle,能支持三维的坐标
- 简单填充了深度缓冲的类实现
- 将 main 中进行测试的正方体的面打散成三角形
This commit is contained in:
SepComet 2026-03-19 21:00:38 +08:00
parent b5f81958df
commit cc203c40f3
10 changed files with 378 additions and 143 deletions

View File

@ -0,0 +1,110 @@
# CPU 软件渲染器项目约定
本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。
## 1. 通用约定
- 项目使用右手坐标系。
- `Vector3::cross(a, b)` 遵循右手定则。
- 除非特别说明,向量按列向量理解。
- 变换写法按 `M * v` 解释。
## 2. 世界空间与局部空间
世界空间与物体局部空间目前使用同一套方向命名:
- `+X`:右
- `+Y`:上
- `+Z`:前
`Scene::Transform` 也遵循这套约定:
- `get_right()`:旋转后的局部右方向
- `get_up()`:旋转后的局部上方向
- `get_forward()`:旋转后的局部前方向
- `get_left()`、`get_down()`、`get_back()`:分别为对应反方向
也就是说,`Transform` 中的 `forward` 明确定义为 `+Z`
## 3. 旋转约定
- 欧拉角使用弧度制。
- `rotation.x`:绕 `X` 轴旋转
- `rotation.y`:绕 `Y` 轴旋转
- `rotation.z`:绕 `Z` 轴旋转
- 组合旋转顺序为 `Rz * Ry * Rx`
当前项目里,方向向量的旋转方式是先构造旋转矩阵,再去变换基础方向轴。
## 4. 矩阵约定
`Math::Matrix4x4` 当前采用以下规则:
- 语义上按列向量使用,写法为 `M * v`
- 元素访问方式为 `matrix[row][col]`
- `data()` 暴露的是连续的 row-major 内存
- 平移分量存放在最后一列
因此,常见的组合变换阅读顺序是从右往左:
- `worldPosition = Translation * Rotation * Scale * localPosition`
## 5. 相机与视图空间
相机目前由 `Scene::Camera::transform` 驱动。
相机自身局部方向定义为:
- 相机右方向:`transform.get_right()`
- 相机上方向:`transform.get_up()`
- 相机前方向:`transform.get_forward()`
但进入视图空间后,当前项目采用的是常见的相机空间约定:
- 位于相机前方的点,其 view-space `z` 为负值
- 视图矩阵第三行存的是相机 backward而不是 forward
这和当前透视投影矩阵实现是一致的,因为那里对应的是 `clip.w = -viewZ` 这套约定。
## 6. 投影与 NDC
当前透视投影相关约定如下:
- 规范化设备坐标NDC可见范围为 `[-1, 1]`
- `x`、`y`、`z` 都会在映射到 viewport 之前做范围检查
- 当前测试代码里,如果点超出 NDC可能会直接判为不可见而不是继续做线段裁剪
这意味着:
- 当前 demo 还没有实现完整的视锥裁剪
- 如果一条线段只有一部分还在屏幕内,但端点已经越出 NDC整条线仍可能被直接丢弃
## 7. 屏幕与像素坐标
屏幕/像素坐标使用左上角为原点的约定:
- 原点在左上角
- `x` 向右增大
- `y` 向下增大
`Core::FrameBuffer` 当前行为如下:
- 有效像素范围:`x in [0, width)`
- 有效像素范围:`y in [0, height)`
- 越界写入会被直接忽略
- 像素缓冲按 row-major 排列
- 内存中的第一行对应 `y = 0`
`Camera::get_viewport_matrix()` 里也对 `Y` 做了翻转,因此 NDC 的“向上”为正,最终会映射成屏幕坐标“向下”为正。
## 8. Demo 中的可见性规则
`main.cpp` 里的旋转立方体示例,目前采用的是一个简化版隐藏线规则:
- 先在 view space 中判断哪些面朝向相机
- 再把这些可见面的边标记为可绘制
- 最后只绘制被标记出来的边
这只是一个 demo 级别的近似方案,不是完整的隐藏线消除算法。
后续如果项目要加入更严格的裁剪、剔除或可见性规则,应当以代码实现为准,并同步更新本文档。

View File

@ -157,7 +157,7 @@
<ItemGroup>
<None Include="..\README.md" />
<None Include="..\TODO.md" />
<None Include="TODO.md" />
<None Include="CONVENTIONS.md" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Asset\ObjLoader.h" />

View File

@ -77,11 +77,11 @@
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="TODO.md">
<Filter>资源文件</Filter>
</None>
<None Include="..\README.md" />
<None Include="..\TODO.md" />
<None Include="CONVENTIONS.md">
<Filter>资源文件</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Core\DepthBuffer.h">

View File

@ -0,0 +1,23 @@
#include "DepthBuffer.h"
#include <cstdint>
#include <algorithm>
namespace Core
{
void DepthBuffer::clear(const uint8_t depth)
{
std::fill(buffer.begin(), buffer.end(), depth);
}
void DepthBuffer::set_pixel(const int32_t x, const int32_t y, const uint8_t depth)
{
if (x < 0 || x >= width || y < 0 || y >= height)
{
return;
}
// Row-major layout with y = 0 on the first row, matching a top-left screen origin.
size_t index = static_cast<size_t>(y) * width + x;
buffer.at(index) = depth;
}
}

View File

@ -1,7 +1,36 @@
#pragma once
namespace Core
{
class DepthBuffer
{};
}
#pragma once
#include <cstdint>
#include <vector>
#include "Color.h"
#include "Vector2.h"
namespace Core
{
class DepthBuffer
{
private:
int32_t width;
int32_t height;
std::vector<uint8_t> buffer;
public:
int32_t get_width() const { return width; }
int32_t get_height() const { return height; }
size_t total_pixels() const { return buffer.size(); }
void* get_buffer() const { return (void*)buffer.data(); }
DepthBuffer(int32_t width, int32_t height) :width(width), height(height), buffer(std::vector<uint8_t>(width* height, 0)) {}
void clear(const uint8_t depth);
void set_pixel(const Math::Vector2Int position, const uint8_t depth)
{
set_pixel(position.x, position.y, depth);
}
void set_pixel(const int32_t x, const int32_t y, const uint8_t depth);
};
};

View File

@ -1,8 +1,6 @@
#include "FrameBuffer.h"
#include <cstdint>
#include <iostream>
#include <algorithm>
using namespace Math;
namespace Core
{

View File

@ -1,31 +1,31 @@
#include "TriangleRasterizer.h"
#include <Triangle.h>
#include <Color.h>
#include "BoundingBox.h"
#include <Vector2.h>
#include <algorithm>
#include <cstdint>
namespace Rasterizer
{
void TriangleRasterizer::DrawTriangle2D(const RenderData::Triangle2D triangle, const RenderData::Color color)
{
auto boundingBox = triangle.get_boundingBox();
int32_t minX = std::max(0, boundingBox.min.x);
int32_t maxX = std::min(frameBuffer.get_width() - 1, boundingBox.max.x);
int32_t minY = std::max(0, boundingBox.min.y);
int32_t maxY = std::min(frameBuffer.get_height() - 1, boundingBox.max.y);
#include "TriangleRasterizer.h"
#include <Triangle.h>
#include <Color.h>
#include "BoundingBox.h"
#include <Vector2.h>
#include <algorithm>
#include <cstdint>
namespace Rasterizer
{
void TriangleRasterizer::DrawTriangle2D(const RenderData::Triangle& triangle, const RenderData::Color color)
{
auto boundingBox = triangle.get_boundingBox();
int32_t minX = std::max(0, boundingBox.min.x);
int32_t maxX = std::min(frameBuffer.get_width() - 1, boundingBox.max.x);
int32_t minY = std::max(0, boundingBox.min.y);
int32_t maxY = std::min(frameBuffer.get_height() - 1, boundingBox.max.y);
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
if (triangle.ContainsPixel(Math::Vector2(x, y)))
if (triangle.ContainsPixel(Math::Vector2Int(x, y)))
{
frameBuffer.set_pixel(Math::Vector2Int(x, y), color.to_rgba());
}
}
}
}
}
}
}

View File

@ -1,19 +1,19 @@
#pragma once
#include "Triangle.h"
#include "Color.h"
#include "FrameBuffer.h"
namespace Rasterizer
{
class TriangleRasterizer
{
private:
Core::FrameBuffer& frameBuffer;
public:
explicit TriangleRasterizer(Core::FrameBuffer& frameBuffer) :frameBuffer(frameBuffer) {};
void DrawTriangle2D(const RenderData::Triangle2D triangle, const RenderData::Color color);
};
#pragma once
#include "Triangle.h"
#include "Color.h"
#include "FrameBuffer.h"
namespace Rasterizer
{
class TriangleRasterizer
{
private:
Core::FrameBuffer& frameBuffer;
public:
explicit TriangleRasterizer(Core::FrameBuffer& frameBuffer) :frameBuffer(frameBuffer) {};
void DrawTriangle2D(const RenderData::Triangle& triangle, const RenderData::Color color);
};
}

View File

@ -1,56 +1,59 @@
#pragma once
#include "Vector2.h"
#include "BoundingBox.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace RenderData
{
struct Triangle2D
{
Math::Vector2 v0;
Math::Vector2 v1;
Math::Vector2 v2;
Triangle2D() : v0(), v1(), v2() {}
Triangle2D(const Math::Vector2 a, const Math::Vector2 b, const Math::Vector2 c) : v0(a), v1(b), v2(c) {}
BoundingBox2D get_boundingBox() const
{
using namespace Math;
int32_t minX = static_cast<int32_t>(std::floor(std::min({ v0.x, v1.x, v2.x })));
int32_t maxX = static_cast<int32_t>(std::ceil(std::max({ v0.x, v1.x, v2.x })));
int32_t minY = static_cast<int32_t>(std::floor(std::min({ v0.y, v1.y, v2.y })));
int32_t maxY = static_cast<int32_t>(std::ceil(std::max({ v0.y, v1.y, v2.y })));
Vector2Int min(minX, minY);
Vector2Int max(maxX, maxY);
return BoundingBox2D(min, max);
}
bool ContainsPixel(const Math::Vector2 point) const
{
using namespace Math;
auto cross = [](const Vector2& p1, const Vector2& p2, const Vector2& p3) -> float
{
const float x1 = p2.x - p1.x;
const float y1 = p2.y - p1.y;
const float x2 = p3.x - p1.x;
const float y2 = p3.y - p1.y;
return x1 * y2 - y1 * x2;
};
const float c0 = cross(v0, v1, point);
const float c1 = cross(v1, v2, point);
const float c2 = cross(v2, v0, point);
const bool hasNeg = (c0 < 0) || (c1 < 0) || (c2 < 0);
const bool hasPos = (c0 > 0) || (c1 > 0) || (c2 > 0);
return !(hasNeg && hasPos);
}
};
#pragma once
#include "Vector2.h"
#include "BoundingBox.h"
#include "Vertex.h"
#include <cstdint>
#include <algorithm>
#include <cmath>
#include <Vector3.h>
namespace RenderData
{
struct Triangle
{
Scene::Vertex v0;
Scene::Vertex v1;
Scene::Vertex v2;
Triangle() : v0(), v1(), v2() {}
Triangle(const Scene::Vertex a, const Scene::Vertex b, const Scene::Vertex c) : v0(a), v1(b), v2(c) {}
BoundingBox2D get_boundingBox() const
{
using namespace Math;
int32_t minX = static_cast<int32_t>(std::floor(std::min({ v0.position.x, v1.position.x, v2.position.x })));
int32_t maxX = static_cast<int32_t>(std::ceil(std::max({ v0.position.x, v1.position.x, v2.position.x })));
int32_t minY = static_cast<int32_t>(std::floor(std::min({ v0.position.y, v1.position.y, v2.position.y })));
int32_t maxY = static_cast<int32_t>(std::ceil(std::max({ v0.position.y, v1.position.y, v2.position.y })));
Vector2Int min(minX, minY);
Vector2Int max(maxX, maxY);
return BoundingBox2D(min, max);
}
bool ContainsPixel(const Math::Vector2Int point) const
{
using namespace Scene;
auto cross = [](const Vertex& v1, const Vertex& v2, const Math::Vector2Int& point) -> float
{
Scene::Vertex v3(Math::Vector3(point.x, point.y, 0));
const float x1 = v2.position.x - v1.position.x;
const float y1 = v2.position.y - v1.position.y;
const float x2 = v3.position.x - v1.position.x;
const float y2 = v3.position.y - v1.position.y;
return x1 * y2 - y1 * x2;
};
const float c0 = cross(v0, v1, point);
const float c1 = cross(v1, v2, point);
const float c2 = cross(v2, v0, point);
const bool hasNeg = (c0 < 0) || (c1 < 0) || (c2 < 0);
const bool hasPos = (c0 > 0) || (c1 > 0) || (c2 > 0);
return !(hasNeg && hasPos);
}
};
}

View File

@ -14,11 +14,15 @@
#include "Color.h"
#include "FrameBuffer.h"
#include "Rasterizer.h"
#include "TriangleRasterizer.h"
#include "Triangle.h"
#include "Camera.h"
#include "SDL_events.h"
#include "SDL_keycode.h"
#include "SDL_timer.h"
#include <cstdlib>
#include <algorithm>
#include "Vertex.h"
const uint32_t SDL_INIT_FLAGS = SDL_INIT_VIDEO;
const int32_t width = 800;
@ -117,14 +121,24 @@ static bool EnsureTexture()
struct ProjectedVertex
{
Math::Vector2Int screen;
Math::Vector3 screen;
bool visible = false;
};
struct CubeFace
{
std::array<int, 4> vertices;
std::array<int, 4> edges;
};
struct CubeTriangle
{
std::array<int, 3> vertices;
};
struct TriangleDrawCommand
{
RenderData::Triangle triangle;
float averageViewSpaceZ = 0.0f;
};
static ProjectedVertex ProjectToScreen(
@ -151,10 +165,7 @@ static ProjectedVertex ProjectToScreen(
}
const Vector4 screen = viewport * Vector4(ndcX, ndcY, ndcZ, 1.0f);
const float screenX = screen.x;
const float screenY = screen.y;
return { Vector2(screenX, screenY).to_vector2Int(), true };
return { Math::Vector3(screen.x, screen.y, screen.z), true };
}
static bool IsFaceVisible(const CubeFace& face, const std::array<Math::Vector3, 8>& viewSpaceVertices)
@ -174,6 +185,19 @@ static bool IsFaceVisible(const CubeFace& face, const std::array<Math::Vector3,
return faceNormal.dot(faceCenter) > 0.0f;
}
static bool IsTriangleVisible(const CubeTriangle& triangle, const std::array<Math::Vector3, 8>& viewSpaceVertices)
{
using namespace Math;
const Vector3& v0 = viewSpaceVertices[triangle.vertices[0]];
const Vector3& v1 = viewSpaceVertices[triangle.vertices[1]];
const Vector3& v2 = viewSpaceVertices[triangle.vertices[2]];
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
const Vector3 faceCenter = (v0 + v1 + v2) / 3.0f;
return faceNormal.dot(faceCenter) > 0.0f;
}
int main(int argc, char* argv[])
{
if (!EnsureSDLInitialized()) return -1;
@ -186,6 +210,7 @@ int main(int argc, char* argv[])
Core::FrameBuffer frameBuffer(width, height);
Rasterizer::Rasterizer rasterizer(frameBuffer);
Rasterizer::TriangleRasterizer triangleRasterizer(frameBuffer);
Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
@ -202,18 +227,21 @@ int main(int argc, char* argv[])
Math::Vector3(-0.5f, 0.5f, 0.5f)
};
const std::array<std::pair<int, int>, 12> cubeEdges = {
std::pair<int, int>(0, 1), std::pair<int, int>(1, 2), std::pair<int, int>(2, 3), std::pair<int, int>(3, 0),
std::pair<int, int>(4, 5), std::pair<int, int>(5, 6), std::pair<int, int>(6, 7), std::pair<int, int>(7, 4),
std::pair<int, int>(0, 4), std::pair<int, int>(1, 5), std::pair<int, int>(2, 6), std::pair<int, int>(3, 7)
};
const std::array<CubeFace, 6> cubeFaces = {
CubeFace{ { 0, 3, 2, 1 }, { 0, 1, 2, 3 } },
CubeFace{ { 4, 5, 6, 7 }, { 4, 5, 6, 7 } },
CubeFace{ { 0, 4, 7, 3 }, { 8, 7, 11, 3 } },
CubeFace{ { 1, 2, 6, 5 }, { 1, 10, 5, 9 } },
CubeFace{ { 0, 1, 5, 4 }, { 0, 9, 4, 8 } },
CubeFace{ { 3, 7, 6, 2 }, { 11, 6, 10, 2 } }
CubeFace{ { 0, 3, 2, 1 } },
CubeFace{ { 4, 5, 6, 7 } },
CubeFace{ { 0, 4, 7, 3 } },
CubeFace{ { 1, 2, 6, 5 } },
CubeFace{ { 0, 1, 5, 4 } },
CubeFace{ { 3, 7, 6, 2 } }
};
const std::array<CubeTriangle, 12> cubeTriangles = {
CubeTriangle{ { 0, 3, 2 } }, CubeTriangle{ { 0, 2, 1 } },
CubeTriangle{ { 4, 5, 6 } }, CubeTriangle{ { 4, 6, 7 } },
CubeTriangle{ { 0, 4, 7 } }, CubeTriangle{ { 0, 7, 3 } },
CubeTriangle{ { 1, 2, 6 } }, CubeTriangle{ { 1, 6, 5 } },
CubeTriangle{ { 0, 1, 5 } }, CubeTriangle{ { 0, 5, 4 } },
CubeTriangle{ { 3, 7, 6 } }, CubeTriangle{ { 3, 6, 2 } }
};
const RenderData::Color clearColor(18, 18, 24, 255);
@ -257,36 +285,80 @@ int main(int argc, char* argv[])
projectedVertices[i] = ProjectToScreen(cubeVertices[i], mvp, viewport);
}
std::array<bool, 12> visibleEdges = {};
for (const CubeFace& face : cubeFaces)
std::array<bool, 6> visibleFaces = {};
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
if (!IsFaceVisible(face, viewSpaceVertices))
{
continue;
}
for (const int edgeIndex : face.edges)
{
visibleEdges[edgeIndex] = true;
}
visibleFaces[faceIndex] = IsFaceVisible(cubeFaces[faceIndex], viewSpaceVertices);
}
for (size_t edgeIndex = 0; edgeIndex < cubeEdges.size(); ++edgeIndex)
std::array<TriangleDrawCommand, 12> drawCommands;
size_t drawCommandCount = 0;
for (const CubeTriangle& cubeTriangle : cubeTriangles)
{
if (!visibleEdges[edgeIndex])
if (!IsTriangleVisible(cubeTriangle, viewSpaceVertices))
{
continue;
}
const auto& edge = cubeEdges[edgeIndex];
const ProjectedVertex& start = projectedVertices[edge.first];
const ProjectedVertex& end = projectedVertices[edge.second];
if (!start.visible || !end.visible)
const ProjectedVertex& v0 = projectedVertices[cubeTriangle.vertices[0]];
const ProjectedVertex& v1 = projectedVertices[cubeTriangle.vertices[1]];
const ProjectedVertex& v2 = projectedVertices[cubeTriangle.vertices[2]];
if (!v0.visible || !v1.visible || !v2.visible)
{
continue;
}
rasterizer.DrawLine(start.screen, end.screen, cubeColor);
const Math::Vector3& viewV0 = viewSpaceVertices[cubeTriangle.vertices[0]];
const Math::Vector3& viewV1 = viewSpaceVertices[cubeTriangle.vertices[1]];
const Math::Vector3& viewV2 = viewSpaceVertices[cubeTriangle.vertices[2]];
drawCommands[drawCommandCount++] = TriangleDrawCommand{
RenderData::Triangle(
Scene::Vertex(v0.screen),
Scene::Vertex(v1.screen),
Scene::Vertex(v2.screen)
),
(viewV0.z + viewV1.z + viewV2.z) / 3.0f
};
}
std::sort(
drawCommands.begin(),
drawCommands.begin() + drawCommandCount,
[](const TriangleDrawCommand& a, const TriangleDrawCommand& b)
{
return a.averageViewSpaceZ < b.averageViewSpaceZ;
});
for (size_t i = 0; i < drawCommandCount; ++i)
{
triangleRasterizer.DrawTriangle2D(drawCommands[i].triangle, cubeColor);
}
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
if (!visibleFaces[faceIndex])
{
continue;
}
const CubeFace& face = cubeFaces[faceIndex];
for (size_t edgeOffset = 0; edgeOffset < face.vertices.size(); ++edgeOffset)
{
const int startIndex = face.vertices[edgeOffset];
const int endIndex = face.vertices[(edgeOffset + 1) % face.vertices.size()];
const ProjectedVertex& start = projectedVertices[startIndex];
const ProjectedVertex& end = projectedVertices[endIndex];
if (!start.visible || !end.visible)
{
continue;
}
rasterizer.DrawLine(
Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(),
Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(),
clearColor);
}
}
SDL_UpdateTexture(texture, nullptr, frameBuffer.get_buffer(), width * sizeof(uint32_t));