好的,我已经将“使用 InstancedBufferAttribute 存储 AO”这一关键技术决策补充到了设计文档中。
这一修改确保了 AO 数据(光影)与 instanceColor(材质/生物群系染色)完全解耦,为后续草地变色等功能预留了完美的兼容性。
以下是更新后的完整 plan.md 内容:
# 使用极简 AO(Top-Column AO)方案设计文档
## 1. 背景与问题
当前体素引擎使用基于 0fps 的 **逐面 / 逐顶点 Ambient Occlusion** 计算方式:
- 每个方块计算 6 个面
- 每个面计算 4 个顶点
- 每个顶点查询 3 个邻居
该方案在视觉质量上表现优秀,但在 **Web / Three.js / Chunk 实时生成** 场景下存在明显问题:
- CPU 开销极高
- Chunk 生成时产生明显卡顿
- AO 逻辑复杂,维护成本高
- 对 chunk 边界和跨 chunk 访问要求高
因此需要一种 **更简单、可扩展、性能友好** 的 AO 方案。
------------------------------------------------------------------------
## 2. 设计目标
本方案的目标是:
- 将 AO 计算复杂度降低一个数量级
- 每个体素仅计算 **一个 AO 值**
- AO 不再区分面或顶点
- AO 结果稳定、可缓存、适合 chunk 系统
- 在牺牲少量细节的前提下,保留足够的空间层次感
------------------------------------------------------------------------
## 3. 极简 AO 核心思想
### 3.1 基本原则
- **每个体素视为一个整体**
- AO 仅由该体素 **正上方一层(Y+1)** 的遮挡决定
- 忽略侧向和斜向的逐面遮挡
- AO 值只反映"顶部环境遮蔽(Top Occlusion)"
该方法在工程上等价于:
> **Column-based AO / Height-based AO**
------------------------------------------------------------------------
### 3.2 遮蔽规则定义(新版:轴向优先)
采用 **“轴向优先 + 角落补充”** 的策略,进一步减少判断次数。
对于体素 `(x, y, z)`,检查其上方 `y+1` 层的遮挡情况:
1. **优先检查十字轴向**:上、下、左、右(4 个邻居)。
2. **条件补充角落**:仅当轴向遮挡不足 3 时,才检查对角方向(4 个角落)。
3. **阈值截断**:AO 最大值为 3。
示意(Y+1 层俯视):
C A C
A X A → A (Axis) 优先检查
C A C → C (Corner) 仅在需要时补充
------------------------------------------------------------------------
### 3.3 AO 数值范围
遮蔽方块数 AO 等级 说明
------------ --------- ----------
0 0 完全明亮
1 1 轻微遮蔽
2 2 中等遮蔽
≥3 3 最强遮蔽
最终映射到渲染使用的 0--255 区间,或直接在 Shader 中作为 0.0-3.0 的浮点数使用。
------------------------------------------------------------------------
## 4. 算法流程
### 4.1 单个方块 AO 计算流程
1. 若方块不可见 / 透明 → 不计算 AO
2. 初始化遮蔽计数 `occ = 0`
3. **阶段一(轴向)**:检查 `(x±1, y+1, z)` 和 `(x, y+1, z±1)`
* 每发现一个实体,`occ++`
4. **阶段二(角落)**:若 `occ < 3`,继续检查对角 `(x±1, y+1, z±1)`
* 每发现一个实体,`occ++`
* 一旦 `occ >= 3`,立即停止
5. 返回 AO 值 `min(3, occ)`
------------------------------------------------------------------------
### 4.2 推荐伪代码
```js
function computeBlockAO(container, x, y, z) {
let occ = 0
const oy = y + 1
// 1. 轴向优先 (Axis First) - 命中率最高,大概率直接填满
if (container.isSolidFast(x + 1, oy, z)) occ++
if (container.isSolidFast(x - 1, oy, z)) occ++
if (container.isSolidFast(x, oy, z + 1)) occ++
if (container.isSolidFast(x, oy, z - 1)) occ++
// 2. 角落补充 (Corner Supplement) - 仅在遮挡不足时计算
// 这种写法避免了不必要的内存访问
if (occ < 3) {
if (container.isSolidFast(x + 1, oy, z + 1)) occ++
if (container.isSolidFast(x - 1, oy, z + 1)) occ++
if (container.isSolidFast(x + 1, oy, z - 1)) occ++
if (container.isSolidFast(x - 1, oy, z - 1)) occ++
}
return Math.min(3, occ)
}为了在 InstancedMesh 中高效传递 AO 数据,且不干扰植物/树叶的染色逻辑,必须使用自定义 Attribute。
- Attribute 名称:
aAo - 数据类型:
Float32Array(每个实例 1 个 float) - 绑定方式:
geometry.setAttribute('aAo', new THREE.InstancedBufferAttribute(aoArray, 1))
- 冲突避免:
instanceColor已被用于植物(草、树叶)的生物群系染色(如0x5B8731)。若将 AO 混合进去,会导致逻辑耦合,难以独立调整光影强度。 - 动态性分离:AO 是基于地形动态计算的(0-3),而颜色往往是静态配置或基于 Perlin Noise 的,两者分离更利于维护。
- Vertex Shader: 接收
attribute float aAo,将其通过varying float vAo传递给 Fragment Shader。 - Fragment Shader: 读取
vAo,计算遮蔽因子occlusion = 1.0 - (vAo * strength),并以此调整csm_DiffuseColor。
- 分支预测友好:大部分地形(平地)在轴向判断阶段就会结束。
- 内存优化:每个方块仅增加 4 字节显存开销。
- 批处理友好:可在 Chunk 生成 Mesh 的阶段一次性计算并填入 Buffer。
- Web / Three.js 体素引擎
- 多 biome / 大世界
- Chunk 实时加载
- 非写实风格体素世界
- 风格化强烈:这种计算方式产生的阴影更接近早期 Minecraft 的经典感觉。
- 地表层次感明显。
- 计算开销几乎可以忽略不计。
- 洞穴侧壁 AO 减弱。
- 对于复杂的非封闭结构,阴影可能比 9 宫格循环检测略少,但在视觉上很难察觉差异。
- 地表使用极简 AO,洞穴使用旧 AO
- AO 与高度图结合(shader 计算)
- AO 结果缓存到 height-column 级别
极简 AO(轴向优先版)配合 InstancedBufferAttribute 是一种 极致压榨性能且架构清晰 的方案:
- 逻辑极简:甚至不需要 for 循环
- 架构解耦:光影与材质颜色互不干扰
- 性能极高:平均内存访问次数减半,显存占用极低
推荐在当前项目中作为默认 AO 实现。