Skip to content

修复 3DGS 的 SH 1-3 阶系数从未上传到 SSBO-4(INSTANCING 方法下视角相关色丢失) - #30

Merged
xarray merged 1 commit into
xarray:masterfrom
HIFsun:fix/gaussian-sh-ssbo-upload
Sep 8, 2026
Merged

修复 3DGS 的 SH 1-3 阶系数从未上传到 SSBO-4(INSTANCING 方法下视角相关色丢失)#30
xarray merged 1 commit into
xarray:masterfrom
HIFsun:fix/gaussian-sh-ssbo-upload

Conversation

@HIFsun

@HIFsun HIFsun commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

问题

GaussianGeometry::INSTANCING(默认渲染方法)加载带球谐的 3DGS 数据时,
读取器解出来的 SH 1-3 阶系数(PLY 的 f_rest_*、SOG 的 shN从来没有被上传到 GPU
表现是模型只剩 DC 项的固定颜色,视角相关的高光/色彩变化整个消失;
而 shader 那边 FULL_SH 宏仍然是打开的,采样的是一个未绑定的 SSBO——
在没有 robust buffer access 的驱动上会读到垃圾数据。

根因

modeling/GaussianGeometry.cppGaussianGeometry::finalize()(当前 master 第 398-400 行):

size_t shDataSize = _preDataMap2.size();
bool noSH = (_method == INSTANCING_TEXTURE || _method == INSTANCING_TEX2D);
if (_degrees > 0 && shDataSize > 10 && noSH)   // <-- 条件反了

noSH 的语义是「这个方法不带 SH」,但上传块的门却建在 noSH == true 上。
_shcoefBuffer 只在构造函数里给 INSTANCING 分配(第 156-161 行):

if (_method == INSTANCING)
{
    _coreBuffer = new osg::FloatArray;
    _coreAttrBuffer = new osg::UShortArray;
    _shcoefBuffer = new osg::UShortArray;
}

两边一叠加,结果是没有任何一条路径能真正上传 SH

RenderMethod noSH _shcoefBuffer 结果
INSTANCING false 门不进去,SH 永不上传 ← 受影响
INSTANCING_TEXTURE / INSTANCING_TEX2D true 进了门但缓冲为空,什么也没写
GEOMETRY_SHADER 走顶点属性通道(setShRed/Green/Blue 的 else 分支),不受影响

与此同时 checkShaderFlag()(第 203-206 行)对 INSTANCING 照样定义 FULL_SH

if (_degrees > 0 && _method != INSTANCING_TEXTURE && _method != INSTANCING_TEX2D)
    getOrCreateStateSet()->setDefine("FULL_SH");

于是 gaussian_splatting.vert.glslFULL_SH 分支会去读 binding=4 这个从未绑定的 SSBO。

复现

任意一个带 SH 的 .ply(含 f_rest_*)或 .sog(含 shN),用默认的 INSTANCING
方法加载并旋转视角:颜色完全不随视角变化。加断点在上面那个 if 上,可以看到
INSTANCINGnoSH == false 直接跳过整块。

修改

把门反过来,并显式要求缓冲存在:

--- a/modeling/GaussianGeometry.cpp
+++ b/modeling/GaussianGeometry.cpp
@@ -396,8 +396,13 @@
 
         // Apply shcoef attributes
         size_t shDataSize = _preDataMap2.size();
+        // SH 的 SSBO(binding 4)只有 INSTANCING 方法才分配(见构造函数),
+        // 两个纹理方法本来就不携带 SH。只要读取器给了任意阶数就上传
+        // (3 层 = 1 阶,7 层 = 2 阶,15 层 = 3 阶):缓冲是零填充的,
+        // 缺的高阶在 shader 固定的 15 槽布局里读出来就是 0。
         bool noSH = (_method == INSTANCING_TEXTURE || _method == INSTANCING_TEX2D);
-        if (_degrees > 0 && shDataSize > 10 && noSH)
+        if (_degrees > 0 && shDataSize >= 3 && !noSH && _shcoefBuffer.valid())
         {

顺带把 shDataSize > 10 放宽成 >= 3:原来的阈值只接受 3 阶(15 层)数据,
1 阶(3 层)和 2 阶(7 层)的数据会被静默丢掉。_shcoefBuffer 上传前
resize(..., 0) 是零填充的,shader 又按固定 15 槽读,所以低阶数据缺的那几档
天然算成 0,不需要额外分支。

GEOMETRY_SHADER 路径完全不经过这段代码,行为不变;两个纹理方法因为
!noSH 依然进不去,也和改前一致(它们本来就没有 SH 缓冲、也不定义 FULL_SH)。
即只有原本就坏掉的 INSTANCING 路径的行为发生变化。

验证

  • Windows / MSVC 2022 / OSG 3.6.5,INSTANCING 方法。
  • 合成数据(band-1 SH 让 +x 面偏红、+y 偏绿、+z 偏蓝的球壳):改前三个方向同色,
    改后随视角正确变色。
  • 真实 .ply / .sog 成果:视角相关高光恢复。

附注(不在本 PR 范围内,供参考)

pipeline/ShaderLibrary.cppprocessIncludes() 在处理
#pragma import_defines(...) 时会把这一行整个 erase 掉,只把当次传入的
defines 列表里命中的键重新插成 #define。这意味着 shader 一旦经过
ShaderLibrary 预处理,之后再通过 StateSet::setDefine() 设置的宏(例如
checkShaderFlag() 设的 FULL_SH)就到不了 program 了——因为 OSG 注入
stateset defines 依赖编译时源码里那行 pragma 还在。

readerwriter/UtilitiesEx.cpp:996tests/gaussian_splatting_test.cpp:185
里那两条 // FIXME: it seems import_defines failed in GLCore/GLES mode?
注释很可能就是这个现象。我们目前是在应用侧手工 setShaderDefines() 补回来绕过的,
没有把它做进这个 PR;如果确认是个 bug,我可以另开一个。


🤖 Generated with Claude Code

https://claude.ai/code/session_013yKGp8faVmYP2Ao6NGQnxj

GaussianGeometry::finalize() gates the SSBO-4 upload on noSH, i.e. on the
INSTANCING_TEXTURE / INSTANCING_TEX2D methods, but _shcoefBuffer is only
allocated for INSTANCING (see the constructor). So under INSTANCING the
block is skipped and under the texture methods the buffer is null: the SH
bands 1-3 the reader decoded (PLY f_rest_*, SOG shN) never reach the GPU,
while checkShaderFlag() still defines FULL_SH for INSTANCING whenever the
degree is > 0 -- the shader then samples an unbound buffer. Symptom: the
view-dependent colour is lost (DC-only look) or, on drivers without robust
buffer access, garbage.

Invert the gate and require the buffer. Also accept degree 1 / 2 data
(3 / 7 layers) instead of only degree 3 (> 10): the buffer is zero-filled,
so missing higher bands evaluate to 0 against the shader's fixed 15-entry
layout.

GEOMETRY_SHADER passes SH through vertex attributes and never enters this
code path; the texture methods still do not enter it. Only the INSTANCING
path -- the broken one -- changes behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKGp8faVmYP2Ao6NGQnxj
@xarray
xarray merged commit 2a27dac into xarray:master Sep 8, 2026
@xarray

xarray commented Sep 8, 2026

Copy link
Copy Markdown
Owner

非常有道理,感谢您和Claude~如果有别的问题也欢迎随时提交

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants