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

  1. Recompute shTextureCount as Math.ceil((((shDegree + 1) * (shDegree + 1) - 1) * 3) / _GaussianSplattingBytesPerShTexel) before calling reserveStreamingPart
  2. Pass shTextureCount of 0 together with shDegree 0 if the part has no SH data
  3. 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

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


AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30). Data as JSON: /api/errors/fbd42575c452aeb4. Report an issue: GitHub.