diff --git a/CPU-Software-Renderer/CONVENTIONS.md b/CPU-Software-Renderer/CONVENTIONS.md new file mode 100644 index 0000000..f4f26ab --- /dev/null +++ b/CPU-Software-Renderer/CONVENTIONS.md @@ -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 级别的近似方案,不是完整的隐藏线消除算法。 + +后续如果项目要加入更严格的裁剪、剔除或可见性规则,应当以代码实现为准,并同步更新本文档。 diff --git a/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj b/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj index 83aed61..32da52f 100644 --- a/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj +++ b/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj @@ -157,7 +157,7 @@ - + diff --git a/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj.filters b/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj.filters index c4e8875..3614f5c 100644 --- a/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj.filters +++ b/CPU-Software-Renderer/CPU-Software-Renderer.vcxproj.filters @@ -77,11 +77,11 @@ - - 资源文件 - + + 资源文件 + diff --git a/CPU-Software-Renderer/Core/DepthBuffer.cpp b/CPU-Software-Renderer/Core/DepthBuffer.cpp index e69de29..63ea6bd 100644 --- a/CPU-Software-Renderer/Core/DepthBuffer.cpp +++ b/CPU-Software-Renderer/Core/DepthBuffer.cpp @@ -0,0 +1,23 @@ +#include "DepthBuffer.h" +#include +#include + +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(y) * width + x; + buffer.at(index) = depth; + } +} diff --git a/CPU-Software-Renderer/Core/DepthBuffer.h b/CPU-Software-Renderer/Core/DepthBuffer.h index 4e113e6..ca508d5 100644 --- a/CPU-Software-Renderer/Core/DepthBuffer.h +++ b/CPU-Software-Renderer/Core/DepthBuffer.h @@ -1,7 +1,36 @@ -#pragma once - -namespace Core -{ - class DepthBuffer - {}; -} \ No newline at end of file +#pragma once +#include +#include +#include "Color.h" +#include "Vector2.h" + +namespace Core +{ + class DepthBuffer + { + private: + int32_t width; + int32_t height; + std::vector 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(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); + }; +}; diff --git a/CPU-Software-Renderer/Core/FrameBuffer.cpp b/CPU-Software-Renderer/Core/FrameBuffer.cpp index 9792785..a7e85f1 100644 --- a/CPU-Software-Renderer/Core/FrameBuffer.cpp +++ b/CPU-Software-Renderer/Core/FrameBuffer.cpp @@ -1,8 +1,6 @@ #include "FrameBuffer.h" #include -#include #include -using namespace Math; namespace Core { diff --git a/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.cpp b/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.cpp index 294704f..65bea00 100644 --- a/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.cpp +++ b/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.cpp @@ -1,31 +1,31 @@ -#include "TriangleRasterizer.h" -#include -#include -#include "BoundingBox.h" -#include -#include -#include - -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 +#include +#include "BoundingBox.h" +#include +#include +#include + +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()); } } } - } -} \ No newline at end of file + } +} diff --git a/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.h b/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.h index bd51c20..9863337 100644 --- a/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.h +++ b/CPU-Software-Renderer/Rasterizer/TriangleRasterizer.h @@ -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); + }; } \ No newline at end of file diff --git a/CPU-Software-Renderer/RenderData/Triangle.h b/CPU-Software-Renderer/RenderData/Triangle.h index dcf359f..0edab97 100644 --- a/CPU-Software-Renderer/RenderData/Triangle.h +++ b/CPU-Software-Renderer/RenderData/Triangle.h @@ -1,56 +1,59 @@ -#pragma once -#include "Vector2.h" -#include "BoundingBox.h" -#include -#include -#include - -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(std::floor(std::min({ v0.x, v1.x, v2.x }))); - int32_t maxX = static_cast(std::ceil(std::max({ v0.x, v1.x, v2.x }))); - int32_t minY = static_cast(std::floor(std::min({ v0.y, v1.y, v2.y }))); - int32_t maxY = static_cast(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 +#include +#include +#include + +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(std::floor(std::min({ v0.position.x, v1.position.x, v2.position.x }))); + int32_t maxX = static_cast(std::ceil(std::max({ v0.position.x, v1.position.x, v2.position.x }))); + int32_t minY = static_cast(std::floor(std::min({ v0.position.y, v1.position.y, v2.position.y }))); + int32_t maxY = static_cast(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); + } + }; } \ No newline at end of file diff --git a/CPU-Software-Renderer/main.cpp b/CPU-Software-Renderer/main.cpp index 7e423d5..5c3dbd3 100644 --- a/CPU-Software-Renderer/main.cpp +++ b/CPU-Software-Renderer/main.cpp @@ -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 +#include +#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 vertices; - std::array edges; +}; + +struct CubeTriangle +{ + std::array 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& viewSpaceVertices) @@ -174,6 +185,19 @@ static bool IsFaceVisible(const CubeFace& face, const std::array 0.0f; } +static bool IsTriangleVisible(const CubeTriangle& triangle, const std::array& 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, 12> cubeEdges = { - std::pair(0, 1), std::pair(1, 2), std::pair(2, 3), std::pair(3, 0), - std::pair(4, 5), std::pair(5, 6), std::pair(6, 7), std::pair(7, 4), - std::pair(0, 4), std::pair(1, 5), std::pair(2, 6), std::pair(3, 7) - }; const std::array 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 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 visibleEdges = {}; - for (const CubeFace& face : cubeFaces) + std::array 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 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));