Comfy-Org/ComfyUI · error · ValueError

SplatToFile3D: gaussian is empty

Error message

SplatToFile3D: gaussian is empty

What it means

Raised by _gaussian_ply_bytes when serializing a gaussian splat to the 3DGS .ply format and the positions tensor of the selected batch item has zero rows (n == 0). SplatToFile3D.execute slices item 0 up to _real_len(splat, 0) before calling this writer, so the error means the first (and only supported) batch item contains no gaussians. The check exists because an empty vertex list would produce a structurally invalid .ply with zero vertices that downstream viewers cannot open.

Source

Thrown at comfy_extras/nodes_gaussian_splat.py:64

def _quantile(x, q):
    # torch.quantile errors above 2**24 elements; stride-subsample large inputs for the estimate.
    lim = 1 << 24
    if x.numel() > lim:
        x = x[:: x.numel() // lim + 1]
    return torch.quantile(x, q)


def _gaussian_ply_bytes(positions, scales, rotations, opacities, sh) -> bytes:
    """Serialize render-ready gaussian tensors as a binary 3DGS .ply.

    positions (N,3) world; scales (N,3) linear; rotations (N,4) quat wxyz; opacities (N,1) in [0,1];
    sh (N,K,3) SH coefficients. Activated values are inverted to the standard 3D gaussian splat storage convention
    (log scale, logit opacity).
    """
    xyz = positions.cpu().numpy().astype(np.float32)
    n = xyz.shape[0]
    if n == 0:
        raise ValueError("SplatToFile3D: gaussian is empty")
    normals = np.zeros_like(xyz)
    f = sh.cpu().numpy().astype(np.float32)                  # (N, K, 3)
    f_dc = f[:, 0, :]                                        # (N, 3)
    f_rest = f[:, 1:, :].transpose(0, 2, 1).reshape(n, -1)   # (N, 3*(K-1)) channel-major
    op = opacities.cpu().numpy().astype(np.float32).reshape(n, 1).clip(1e-6, 1 - 1e-6)
    op = np.log(op / (1.0 - op))                             # inverse sigmoid (logit)
    scale = np.log(scales.cpu().numpy().astype(np.float32).clip(min=1e-8))
    rot = rotations.cpu().numpy().astype(np.float32)         # (N, 4)

    attrs = (['x', 'y', 'z', 'nx', 'ny', 'nz']
             + [f'f_dc_{i}' for i in range(3)]
             + [f'f_rest_{i}' for i in range(f_rest.shape[1])]
             + ['opacity'] + [f'scale_{i}' for i in range(3)] + [f'rot_{i}' for i in range(4)])
    elements = np.empty(n, dtype=[(a, 'f4') for a in attrs])
    elements[:] = list(map(tuple, np.concatenate([xyz, normals, f_dc, f_rest, op, scale, rot], axis=1)))

    header = "ply\nformat binary_little_endian 1.0\n" + f"element vertex {n}\n"
    header += "".join(f"property float {a}\n" for a in attrs) + "end_header\n"

View on GitHub (pinned to 1c6d8d45b3)

Solutions

  1. Inspect the upstream node that produced the SPLAT and confirm it actually generated gaussians (check its length/real-length output or log).
  2. If the splat is batched and only a non-first item is populated, re-batch so the non-empty item is item 0 (SplatToFile3D only writes item 0).
  3. Relax any culling/densification threshold upstream that can drop all gaussians for an item.
  4. Guard in the workflow: skip the save node when the item length is 0 instead of letting it raise.

Example fix

// before
splat = some_empty_splat
file = SplatToFile3D().execute(splat, format="ply")

// after
end = _real_len(splat, 0)
if end == 0:
    raise ValueError("upstream produced 0 gaussians for item 0")
file = SplatToFile3D().execute(splat, format="ply")
Defensive patterns

Strategy: validation

Validate before calling

from comfy_extras.nodes_gaussian_splat import _real_len

end = _real_len(splat, 0)
if end == 0:
    raise ValueError("upstream splat item 0 is empty; fix generation before export")

Type guard

def has_splat_item(splat, i: int) -> bool:
    return splat is not None and splat.positions.shape[0] > i and _real_len(splat, i) > 0

Try / catch

try:
    out = SplatToFile3D().execute(splat, format="ply")
except ValueError as e:
    if "gaussian is empty" in str(e):
        logging.warning("skipping empty splat export")
    else:
        raise

Prevention

When it happens

Trigger: Calling SplatToFile3D with a SPLAT whose item 0 has length 0: an upstream decode/splat-produce step yielded zero gaussians, or the lengths/real-length bookkeeping says 0 even though the padded tensors are non-empty. Also happens when a batch has >1 items and only a later item is non-empty (the node warns it uses the first item, then fails because item 0 is empty).

Common situations: An upstream gaussian-splat generation node returned an empty result (e.g., a threshold/culling step removed every splat, or a decoder produced nothing for that latent); connecting a MergeSplat output where item 0 was empty; testing the node with placeholder/zero-length data.

Related errors


AI-assisted analysis of Comfy-Org/ComfyUI@1c6d8d45b3 (2026-08-14). Data as JSON: /api/errors/382deb23373cc3ce. Report an issue: GitHub.