BabylonJS/Babylon.js · error
reserveStreamingPart: shTextureCount ${shTextureCount} does
Error message
reserveStreamingPart: shTextureCount ${shTextureCount} does not match shDegree ${shDegree} (expected ${expectedShTextureCount}) What it means
GaussianSplattingMesh.reserveStreamingPart validates that the number of SH texels reserved for a streaming part is consistent with the requested spherical-harmonics degree. For degree d, each splat needs ceil((((d+1)^2 - 1) * 3) / bytesPerShTexel) texels, and the library throws when shTextureCount differs from that derived value. This prevents corrupt SH texture layouts in the splat atlas.
Source
Thrown at packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.ts:2074
}
// Validate the SH sizing parameters — they drive the SH_DEGREE define and the SH texture allocation, so a
// fractional/negative/huge value would give an invalid layout or an unbounded allocation. The draw path
// supports degree 0..4 (shTexture0..4); SH is all-or-nothing; the texture count must match the degree.
const maxSupportedShDegree = 4;
if (!Number.isSafeInteger(shDegree) || shDegree < 0 || shDegree > maxSupportedShDegree) {
throw new Error(`reserveStreamingPart: shDegree must be an integer in [0, ${maxSupportedShDegree}]`);
}
if (!Number.isSafeInteger(shTextureCount) || shTextureCount < 0) {
throw new Error("reserveStreamingPart: shTextureCount must be a non-negative integer");
}
if (shDegree > 0 !== shTextureCount > 0) {
throw new Error("reserveStreamingPart: shDegree and shTextureCount must both be positive (SH) or both zero (no SH)");
}
if (shDegree > 0) {
const expectedShTextureCount = Math.ceil((((shDegree + 1) * (shDegree + 1) - 1) * 3) / _GaussianSplattingBytesPerShTexel);
if (shTextureCount !== expectedShTextureCount) {
throw new Error(`reserveStreamingPart: shTextureCount ${shTextureCount} does not match shDegree ${shDegree} (expected ${expectedShTextureCount})`);
}
}
const maxTextureSize = this._scene.getEngine().getCaps().maxTextureSize;
const maxCapacity = maxTextureSize * maxTextureSize;
// Row-align the region so a later GPU relayout (defrag under a memory budget) can be scoped to whole
// atlas rows via scissor without ever touching a preceding part that shares a row.
// - Front alignment: start the usable region on the next row boundary. This only consumes the
// preceding parts' already-allocated last-row tail padding, so it costs no extra memory.
// - Capacity alignment: pad the region up to a whole number of rows so its end is a row boundary too
// (needed when another part follows, e.g. multiple streaming parts).
const atlasWidth = this._getTextureSize(1).x;
const startOffset = this._vertexCount; // first atlas index the reserved part occupies
const alignedBase = Math.ceil(startOffset / atlasWidth) * atlasWidth;
const frontPad = alignedBase - startOffset; // invisible padding that fills the preceding row
const alignedCapacity = Math.ceil(capacity / atlasWidth) * atlasWidth;
const regionSplats = frontPad + alignedCapacity;
View on GitHub (pinned to 0592b347b8)
Solutions
- Recompute shTextureCount as Math.ceil((((shDegree + 1) * (shDegree + 1) - 1) * 3) / _GaussianSplattingBytesPerShTexel) before calling reserveStreamingPart
- Pass shTextureCount of 0 together with shDegree 0 if the part has no SH data
- Log both values at the call site and compare against the expected count in the message
Example fix
// before
mesh.reserveStreamingPart(count, { shDegree: 2, shTextureCount: 45 });
// after
const expected = Math.ceil((((2 + 1) * (2 + 1) - 1) * 3) / bytesPerShTexel);
mesh.reserveStreamingPart(count, { shDegree: 2, shTextureCount: expected }); Defensive patterns
Strategy: validation
Validate before calling
const expected = Math.ceil((((shDegree + 1) * (shDegree + 1) - 1) * 3) / bytesPerShTexel);
if (shDegree > 0 !== shTextureCount > 0 || (shDegree > 0 && shTextureCount !== expected)) {
throw new RangeError(`shTextureCount ${shTextureCount} invalid for shDegree ${shDegree}; expected ${expected}`);
} Type guard
const isValidShPair = (d: number, t: number, b: number) => d > 0 ? t === Math.ceil((((d + 1) * (d + 1) - 1) * 3) / b) : t === 0;
Try / catch
try {
mesh.reserveStreamingPart(count, opts);
} catch (e) {
if (String(e).includes('shTextureCount')) { opts.shTextureCount = recompute(opts.shDegree); retry(); }
else throw e;
} Prevention
- Derive shTextureCount from shDegree with the library's formula, never hardcode it
- Keep the SH packing constant in one shared module
- Test reserve calls for degrees 0-3 in unit tests
When it happens
Trigger: Calling reserveStreamingPart with an shTextureCount computed with a different formula, hardcoded count, or one derived from a different bytes-per-texel constant than the one used internally.
Common situations: Custom splat converters that precompute SH texture sizes for SH degree 1-3, or code copied from an older Babylon version where the packing constant differed.
Related errors
- GaussianSplattingDebugger.addMesh: mesh must have a Gaussian
- To call addPart()/addParts(), each source mesh must not be a
- ${method}: local range [${offset}, ${offset + count}) is out
- Gaussian splat data byte length (${bytes.byteLength}) is not
- _writeStreamingSplats: range [${globalOffset}, ${globalOffse
AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30).
Data as JSON: /api/errors/fbd42575c452aeb4.
Report an issue: GitHub.