阴影体素化 Sink(rendering/voxelization)
基本信息
| 属性 | 值 |
|---|---|
| 包 | com.gtnewhorizons.angelica.rendering.voxelization |
| 本条目覆盖 | 1 个文件:SdlShadowVoxelizationSink.java(92 行) |
| 目录内另 1 个 | ShadowVoxelizer.java —— 已被其它条目提及(见 Celeritas) |
| 可见性 | public final implements ShadowVoxelizer.Sink |
| 依赖 | 内嵌 Iris(1 类)+ 内嵌 Embeddium/Sodium(3 类)+ GLSM(3 类) |
⚠️ rendering/voxelization/ 只有 2 个文件,其中 ShadowVoxelizer(接口的持有方)已在上轮被覆盖,故本条目只写实现方。
它解决的问题
ShadowVoxelizer.Sink 是一个回调接口,本类实现它,把「地形顶点缓冲」喂给 GLSM SDL 端的体素化后端。
体素化(voxelization) 是现代阴影技术:把场景几何编码进 3D 体素纹理,让阴影贴图获得高精度遮挡。1.7.10 没有这个机制,着色器包(Iris)期待它存在。本类是 Angelica 补的适配层。
SSBO 绑定槽来自 Iris(:17)
private static final int SSBO_BINDING = RwImageStoreExtractor.VG_VBUF_SSBO_BINDING;
⚠️ ⚠️ 绑定槽号不是字面量,而是引用 net.coderbot.iris.pipeline.transform.RwImageStoreExtractor(内嵌 Iris)的静态常量(:6 import)。
这是本文件最重要的一行。 它意味着:
| 事实 | 含义 |
|---|---|
| 槽号由内嵌 Iris 决定 | Iris 改这个常量,Angelica 跟随 —— 编译期一致,语义由 Iris 定 |
用 RwImageStoreExtractor 的槽 |
说明这个槽原本是给 Iris 的「渲染目标顶点缓冲 SSBO」用的,体素化复用同一槽 |
| Iris 若移除该常量 | 编译失败(好过静默用错槽) |
⚠️ 「复用 Iris 的 SSBO 槽」意味着槽位冲突风险 —— 若 Iris 同时也在用 VG_VBUF_SSBO_BINDING,两者的绑定会互相覆盖。⚠️ finish()(:85-91)只调 endVoxelizationBatch,不调 glBindBufferBase(..., 0) 解绑 —— 解绑责任在 GLSM 的 endVoxelizationBatch 内(GLSM 子项目,本条目不可判定它是否解绑)。这是本文件最需要确认的一点。
5 个「一次性告警」标志与 3 个静态计数器
| 字段 | 行 | 类型 | 语义 |
|---|---|---|---|
LOG |
:16 |
Logger |
LogManager.getLogger("Angelica") |
encodersThisFrame |
:19 |
static int |
本帧成功创建的 encoder 数 |
dispatchesThisFrame |
:20 |
static int |
本帧的 dispatch 次数 |
regionsThisFrame |
:21 |
static int |
本帧的 region 数 |
warnedBindFailure |
:23 |
boolean(实例) |
|
warnedBeginFailure |
:24 |
boolean(实例) |
|
pass |
:25 |
long |
当前 encoder pass 句柄 |
pendingVertexBuffer |
:26 |
int |
待绑定的 GL buffer handle |
pendingX/Y/Z |
:27-29 |
float |
region 偏移 |
⚠️ 两个告警标志是实例字段(private boolean),3 个计数器是静态字段 —— 混用。⚠️ 告警「一次性」的语义是「每个 sink 实例每种失败只警告一次」 —— 若 ShadowVoxelizer 每帧新建 sink,告警会每帧打。⚠️ sink 的创建/复用策略本条目不可判定(ShadowVoxelizer 已在上轮覆盖,但创建点未核实)。
⚠️ 两个告警的文案都是「不带修复指引」的(:56、:68):
LOG.warn("shadow voxelization: could not bind region vertex buffer {} at SSBO slot {}", ...); // :56
LOG.warn("shadow voxelization: compute encoder refused; no voxel writes this pass"); // :68
⚠️ :56 有两个 {} 占位符与两个实参(正确),:68 是纯字符串无占位符。⚠️ 两条都不含「请检查 X」的建议,与 RenderPassIndex.indexOf 那条含修复指引的异常消息(见 GPU 视锥剔除)形成对比。
⚠️ encodersThisFrame / dispatchesThisFrame / regionsThisFrame 是 static int 非 volatile(:19-21)—— 与 DroppedItemInstancer 的 11 个静态可变字段同类问题(见 掉落物品实例化)。
两个 Sink 回调方法
region(:35-47)—— 登记 region,不立即绑定
final RenderRegion.DeviceResources resources = region.getResources(); // :37
if (resources == null) return false; // :38
final GlBuffer vertexBuffer = resources.getVertexBuffer(); // :39
if (vertexBuffer == null || vertexBuffer.handle() == 0) return false; // :40
pendingVertexBuffer = vertexBuffer.handle(); // :41
pendingX = offsetX; pendingY = offsetY; pendingZ = offsetZ; // :42-44
regionsThisFrame++; // :45
return true;
5 个检查点(:38、:40 的两个),只存不绑 —— 实际绑定推迟到 range(:51-61)。
⚠️ vertexBuffer.handle() == 0 的检查(:40)—— handle() 返回 int 的 GL buffer name,0 是默认/无效值。⚠️ GL 没有「buffer 0 无效」的标准 —— glBindBufferBase 绑 0 是合法的(读零长度)。这里把 0 当无效是本实现的选择,若上游某天用 buffer 0 做真实缓冲,会被误拒。
⚠️ region 只设置 pending*,不验证多个 region 的连续性 —— 若调用方连续 region(A)、region(A)、region(B),A 的登记被覆盖。⚠️ 接口契约要求 region 后紧跟 range,本文件无断言。这是隐式跨文件契约。
⚠️ regionsThisFrame++ 在 return true 之前(:45)—— 计数的是「成功登记」而非「成功编码」。
range(:49-76)—— 两阶段:先绑 SSBO,再 dispatch
第一阶段:绑定 region 顶点缓冲(:51-61)
if (pendingVertexBuffer != 0) {
GLStateManager.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, SSBO_BINDING, pendingVertexBuffer); // :52
if (!sdl().bindVoxelizationRegion(SSBO_BINDING, pass, pendingX, pendingY, pendingZ)) { // :53
if (!warnedBindFailure) { warnedBindFailure = true; LOG.warn(...); } // :54-57
return false; // :58
}
pendingVertexBuffer = 0; // :60
}
⚠️ glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ...) 用的是 GL43(:12 import)—— 需要 GL 4.3。无版本检查 —— 在不支持 SSBO 的上下文里会 GL_INVALID_OPERATION(不崩,静默失败),随后 bindVoxelizationRegion 大概返回 false,触发一次性告警。这实际上是一个「优雅降级」。
⚠️ ⚠️ pass 在第一阶段被用作 bindVoxelizationRegion 的参数(:53),但 pass 此时可能还是 0(首次调用时第二阶段还没执行 :63)。⚠️ 即「首次 range 调用时,绑定用的 pass 是 0」。这要么是 GLSM 侧容忍 pass == 0(把绑定登记下来,等 beginVoxelizationBatch 返回真实 pass 后再用),要么是顺序缺陷。本条目无法判定 GLSM 侧行为 —— 但这是本文件最需要向 GLSM 侧确认的一行。
⚠️ 绑定失败时 pendingVertexBuffer 不清零(:58 直接 return,:60 未执行)—— 下一次 range 会重试绑定。这是正确的重试语义(因为失败可能是暂时的)。
第二阶段:懒创建 encoder pass(:62-72)
if (pass == 0) {
pass = sdl().beginVoxelizationBatch(SSBO_BINDING); // :63
if (pass != 0) encodersThisFrame++; // :64
if (pass == 0) {
if (!warnedBeginFailure) { warnedBeginFailure = true; LOG.warn(...); } // :66-69
return false; // :70
}
}
⚠️ pass 是 long(不是 int)(:25、:63)—— GLSM 的 pass 句柄是 64 位,与 RenderPassIndex 的 int 索引(见 GPU 视锥剔除)是两种不同的「pass」概念,不要混用。
⚠️ beginVoxelizationBatch 返回 0 表示失败(:65 的 if (pass == 0))—— 0 是失败哨兵。⚠️ 与 pendingVertexBuffer 的 0 哨兵(:40、:51、:60)不同语义但同样用 0。
⚠️ encodersThisFrame++ 只在 pass != 0 时(:64)—— 统计「成功创建的 encoder 数」,且因为 pass 一旦非 0 就走 if (pass == 0) 的假分支,每个 sink 实例每帧只创建 1 个 encoder(直到 finish() 清零)。⚠️ 但 encodersThisFrame 是静态的 —— 多个 sink 实例共享计数。
第三阶段:dispatch(:73-75)
sdl().voxelizeRange(pass, vertexOffset, vertexCount); // :73
dispatchesThisFrame++; // :74
return true;
⚠️ voxelizeRange 的返回值被丢弃(:73)—— 若它返回失败状态,无检查。⚠️ 而 bindVoxelizationRegion / beginVoxelizationBatch 的返回值都检查了(:53、:65)。三个 GLSM 调用里唯一一个不检查返回值 —— 这是本文件最可能出静默失败的一行。
finish 与三个 takeXxx(:78-91)
public static int takeEncoders() { final int n = encodersThisFrame; encodersThisFrame = 0; return n; } // :78
public static int takeDispatches(){ final int n = dispatchesThisFrame; dispatchesThisFrame = 0; return n; } // :80
public static int takeRegions() { final int n = regionsThisFrame; regionsThisFrame = 0; return n; } // :82
@Override public void finish() { // :85
if (pass != 0) { sdl().endVoxelizationBatch(pass); pass = 0; } // :86-89
pendingVertexBuffer = 0; // :90
}
3 个 takeXxx 是「读并清零」(read-and-reset),典型的帧统计模式。
⚠️ 三个 takeXxx 各自独立清零自己的字段 —— 若调用方只取一个,另两个会累积到下一帧。⚠️ regionsThisFrame 只在 region() 成功时累加,takeRegions 不检查是否有对应 dispatch —— 「登记了 region 但 range 全失败」时 takeRegions > 0 而 takeEncoders == 0。这三个计数器不保证自洽。
⚠️ ⚠️ takeEncoders 等三个是 public static 且直接读写非 volatile 的静态字段(:19-21、:78-82)—— 跨线程读/清零无 happens-before。⚠️ 若 Tracy 线程调用 takeXxx 而渲染线程累加,会丢计数。这三个是本文件最可能被外部线程调用的 public API。
⚠️ finish() 不调 takeXxx —— 结束体素化批次与取统计是分离的。⚠️ 若调用方忘记 finish(),pass 非 0 保持到下次 range,而 pendingVertexBuffer 已清零(:90) → 下次 range 走 if (pass == 0) 假分支,用旧 pass dispatch 新 region 的数据。⚠️ 这是真实的状态泄漏路径(接口契约要求 finish() 必被调用,本文件无 AutoCloseable 保障)。
⚠️ finish() 无 try/finally —— endVoxelizationBatch 抛异常则 pass 不清零(下次继续用坏 pass)。
依赖清单(10 个 import)
| 包 | 类 | 行 |
|---|---|---|
com.gtnewhorizons.angelica.glsm |
GLStateManager |
:3 |
com.gtnewhorizons.angelica.glsm.backend |
BackendManager、RenderBackend |
:4-5 |
net.coderbot.iris.pipeline.transform |
RwImageStoreExtractor |
:6 |
org.embeddedt.embeddium.impl.gl.attribute |
GlVertexFormat |
:7 |
org.embeddedt.embeddium.impl.gl.buffer |
GlBuffer |
:8 |
org.apache.logging.log4j |
LogManager、Logger |
:9-10 |
org.embeddedt.embeddium.impl.render.chunk.region |
RenderRegion |
:11 |
org.lwjgl.opengl |
GL43 |
:12 |
⚠️ GL43(1.7.10 时代不存在的 GL 版本常量)出现在一个为 1.7.10 服务的 mod 里 —— LWJGL 2.9 提供 org.lwjgl.opengl.GL43 的常量定义(只是常量,不需要运行时的 GL 4.3 上下文)。⚠️ 常量可用 ≠ 功能可用 —— 实际 SSBO 支持取决于 GLSM 所用的 GL 后端(SDL/GLX/WGL)。无版本查询代码。
⚠️ sdl() 静态辅助(:31-33)只是 BackendManager.RENDER_BACKEND 的 getter —— 名字叫 sdl() 但返回的是抽象 RenderBackend(可能不是 SDL 后端)。⚠️ 类名 SdlShadowVoxelizationSink 与方法名 sdl() 暗示 SDL 专用,实际是通用后端。这是命名与实现的偏差。
⚠️ GLSM 的 4 个体素化方法(bindVoxelizationRegion、beginVoxelizationBatch、voxelizeRange、endVoxelizationBatch)是 GLSM 扩展的标准 GL 之外的方法,定义在 RenderBackend(GLSM 子项目)。本条目不可判定它们的实现。
已知问题 / 风险
- SSBO 槽号复用内嵌 Iris 的
RwImageStoreExtractor.VG_VBUF_SSBO_BINDING(:17)—— 槽位冲突风险取决于 Iris 何时用该槽。 finish()不解绑 SSBO(:85-91),解绑责任在 GLSM 的endVoxelizationBatch内(本条目不可判定)。- ⚠️ 首次
range调用时bindVoxelizationRegion收到的pass是 0(:53vs:63的顺序)—— 是 GLSM 侧容忍还是顺序缺陷,本条目无法判定。这是最需要向 GLSM 侧确认的一行。 - ⚠️
voxelizeRange的返回值被丢弃(:73)—— 三个 GLSM 调用里唯一不检查返回值的,静默失败风险最高。 glBindBufferBase用GL43.GL_SHADER_STORAGE_BUFFER,无 GL 版本检查(:52)—— 不支持时 GL_INVALID_OPERATION(静默),靠后续bindVoxelizationRegion返回 false 降级。vertexBuffer.handle() == 0被当作无效(:40)—— GL 语义上 buffer 0 可用,是本实现的约定。region只设置pending*不验证连续性(:41-44),多次region不跟range会静默丢弃前面的登记 —— 接口契约无断言。- 两个告警标志是实例字段、3 个计数器是静态字段(
:19-24)—— 混用;若 sink 每帧新建,告警每帧打。 - 两条告警消息都不含修复建议(
:56、:68),对比RenderPassIndex含指引的风格不一致。 - 3 个静态计数器非 volatile 且由
public static takeXxx()直接读写(:19-21、:78-82)—— 跨线程取统计会丢计数。 - 3 个计数器不保证自洽(
takeRegions不检查是否有对应 dispatch)—— 「登记了但全失败」时regions > 0而encoders == 0。 finish()被漏调用会导致pass残留(:86-89)—— 下次range用旧 pass dispatch 新数据。无AutoCloseable保障。finish()无try/finally(:86-89)——endVoxelizationBatch抛异常则pass不清零。- 类名
Sdl*与sdl()方法暗示 SDL 专用,实际用通用RenderBackend(:31-33)—— 命名与实现偏差。 GL43常量在 1.7.10 语境下的可用性依赖 LWJGL 2.9,且常量可用 ≠ 功能可用,无版本查询。
相关条目
- GPU 视锥剔除 -
pass概念的对照(int 索引 vs long 句柄) - Celeritas(内嵌地形渲染引擎) -
ShadowVoxelizer(接口持有方)与RenderRegion的数据来源 - Iris(内嵌) -
RwImageStoreExtractor的定义 - Subprojects(内嵌子项目) - GLSM 的
RenderBackend/BackendManager/ 体素化方法 - 调试叠加层 - 同属调试/诊断风格的 LOGGER 用法
- 帧节流内核 -
encodersThisFrame式的帧统计模式可对照PacerCore.summary