# 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 级别的近似方案，不是完整的隐藏线消除算法。

后续如果项目要加入更严格的裁剪、剔除或可见性规则，应当以代码实现为准，并同步更新本文档。
