IMX6U-Game/docs/CONVENTIONS.md

6.0 KiB
Raw Blame History

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 里的 cubeTrianglescubeFaces 当前应继续保持与这套约定一致,不要单独翻转其中一部分

7. 投影与 NDC

当前透视投影相关约定如下:

  • 规范化设备坐标NDC可见范围为 [-1, 1]
  • xyz 都会在映射到 viewport 之前做范围检查
  • 当前测试代码里,如果点超出 NDC可能会直接判为不可见而不是继续做线段裁剪

这意味着:

  • 当前 demo 还没有实现完整的视锥裁剪
  • 如果一条线段只有一部分还在屏幕内,但端点已经越出 NDC整条线仍可能被直接丢弃

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
  • 当前约定为“深度值越小,离相机越近”
  • 深度测试通过后,必须同时更新 DepthBufferFrameBuffer
  • DepthBuffer 只负责存储和读取深度,不负责决定颜色写入逻辑;是否写颜色由光栅化阶段决定

当前三角形光栅化里的深度流程为:

  • 在屏幕空间遍历三角形包围盒
  • 以像素中心 x + 0.5, y + 0.5 作为采样点
  • 用屏幕空间 x/y 计算重心坐标
  • 用同一组重心坐标判断点是否在三角形内,并插值顶点 z
  • 若新深度更近,则写入 DepthBufferFrameBuffer

也就是说:

  • 重心坐标的计算是二维问题,只使用屏幕空间 x/y
  • 顶点 z 的插值使用这组重心权重完成
  • 当前实现是屏幕空间线性插值,后续如果引入纹理、法线或更严格的属性插值,需要进一步考虑透视校正插值

10. Demo 中的可见性规则

main.cpp 里的旋转立方体示例,目前采用以下可见性与遮挡规则:

  • 三角形是否参与光栅化,仍由投影合法性检查和背面剔除决定
  • 三角形之间的遮挡,不再依赖按平均深度排序,而是由 DepthBuffer 逐像素决定
  • 当前 demo 仍保留按面筛选后的轮廓线绘制,用于显示黑色边框

这意味着:

  • DepthBuffer 不能替代投影合法性检查或背面剔除
  • DepthBuffer 负责的是像素级遮挡,而不是顶点级或三角形级是否进入渲染流程

后续如果项目要加入更严格的裁剪、剔除、透视校正插值或隐藏线规则,应当以代码实现为准,并同步更新本文档。