# CPU 软件渲染器项目约定 本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。 > 文档分工:坐标、矩阵、深度等数学语义记录在本文档;IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_CORE_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。 ## 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. 三角形绕序与正面约定 当前项目将三角形正面统一约定为顺时针 `CW` 绕序。 这里的“顺时针”按当前渲染流程解释为: - 三角形经过 view / projection / viewport 变换后 - 从屏幕上观察其顶点顺序时,正面三角形按顺时针排列 与这套约定配套的实现规则是: - 背面剔除当前在 view space 中完成 - 法线方向使用 `faceNormal = (v1 - v0).cross(v2 - v0)` 计算 - 当前 demo 中,`faceNormal.dot(faceCenter) > 0` 被视为正面 这意味着: - 所有手写或导入的三角形索引都必须保持一致绕序 - 如果未来改成逆时针 `CCW` 为正面,那么剔除判定符号也必须同步调整 - `main.cpp` 里的 `cubeTriangles` 和 `cubeFaces` 当前应继续保持与这套约定一致,不要单独翻转其中一部分 ## 7. 投影与 NDC 当前透视投影相关约定如下: - 规范化设备坐标(NDC)可见范围为 `[-1, 1]` - `x`、`y`、`z` 都会在映射到 viewport 之前做范围检查 - 当前测试代码里,如果点超出 NDC,可能会直接判为不可见,而不是继续做线段裁剪 这意味着: - 当前 demo 还没有实现完整的视锥裁剪 - **2D 屏幕空间线段裁剪已实现**:`Rasterizer::DrawLine` 使用 Cohen-Sutherland 算法对屏幕边界做裁剪,部分在屏幕内的线段可以正确绘制,不再整条丢弃 - 3D 视锥裁剪(齐次裁剪空间)仍未实现 ## 8. 屏幕与像素坐标 屏幕/像素坐标使用左上角为原点的约定: - 原点在左上角 - `x` 向右增大 - `y` 向下增大 `Core::FrameBuffer` 当前行为如下: - 有效像素范围:`x in [0, width)` - 有效像素范围:`y in [0, height)` - 越界写入会被直接忽略 - 像素缓冲按 row-major 排列 - 内存中的第一行对应 `y = 0` `Camera::get_viewport_matrix()` 里也对 `Y` 做了翻转,因此 NDC 的“向上”为正,最终会映射成屏幕坐标“向下”为正。 ## 9. 深度缓冲约定 当前项目已经接入 `Core::DepthBuffer`,并采用以下规则: - `DepthBuffer` 存储类型为 `float` - 每帧开始时必须调用 `depthBuffer->clear()`,默认清为 `INFINITY` - 当前约定为“深度值越小,离相机越近” - 深度测试通过后,必须同时更新 `DepthBuffer` 和 `FrameBuffer` - `DepthBuffer` 只负责存储和读取深度,不负责决定颜色写入逻辑;是否写颜色由光栅化阶段决定 当前三角形光栅化里的深度流程为: - 在屏幕空间遍历三角形包围盒 - 以像素中心 `x + 0.5, y + 0.5` 作为采样点 - 用屏幕空间 `x/y` 计算重心坐标 - 用同一组重心坐标判断点是否在三角形内,并插值顶点 `z` - 若新深度更近,则写入 `DepthBuffer` 和 `FrameBuffer` 也就是说: - 重心坐标的计算是二维问题,只使用屏幕空间 `x/y` - 顶点 `z` 的插值使用这组重心权重完成 - 当前实现是屏幕空间线性插值,后续如果引入纹理、法线或更严格的属性插值,需要进一步考虑透视校正插值 ## 10. Demo 中的可见性规则 `main.cpp` 里的旋转立方体示例,目前采用以下可见性与遮挡规则: - 三角形是否参与光栅化,仍由投影合法性检查和背面剔除决定 - 三角形之间的遮挡,不再依赖按平均深度排序,而是由 `DepthBuffer` 逐像素决定 - 当前 demo 仍保留按面筛选后的轮廓线绘制,用于显示黑色边框 这意味着: - `DepthBuffer` 不能替代投影合法性检查或背面剔除 - `DepthBuffer` 负责的是像素级遮挡,而不是顶点级或三角形级是否进入渲染流程 后续如果项目要加入更严格的裁剪、剔除、透视校正插值或隐藏线规则,应当以代码实现为准,并同步更新本文档。