{"record":{"id":"382deb23373cc3ce","repo":"Comfy-Org/ComfyUI","slug":"splattofile3d-gaussian-is-empty","errorCode":null,"errorMessage":"SplatToFile3D: gaussian is empty","messagePattern":"SplatToFile3D: gaussian is empty","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"comfy_extras/nodes_gaussian_splat.py","lineNumber":64,"sourceCode":"def _quantile(x, q):\n    # torch.quantile errors above 2**24 elements; stride-subsample large inputs for the estimate.\n    lim = 1 << 24\n    if x.numel() > lim:\n        x = x[:: x.numel() // lim + 1]\n    return torch.quantile(x, q)\n\n\ndef _gaussian_ply_bytes(positions, scales, rotations, opacities, sh) -> bytes:\n    \"\"\"Serialize render-ready gaussian tensors as a binary 3DGS .ply.\n\n    positions (N,3) world; scales (N,3) linear; rotations (N,4) quat wxyz; opacities (N,1) in [0,1];\n    sh (N,K,3) SH coefficients. Activated values are inverted to the standard 3D gaussian splat storage convention\n    (log scale, logit opacity).\n    \"\"\"\n    xyz = positions.cpu().numpy().astype(np.float32)\n    n = xyz.shape[0]\n    if n == 0:\n        raise ValueError(\"SplatToFile3D: gaussian is empty\")\n    normals = np.zeros_like(xyz)\n    f = sh.cpu().numpy().astype(np.float32)                  # (N, K, 3)\n    f_dc = f[:, 0, :]                                        # (N, 3)\n    f_rest = f[:, 1:, :].transpose(0, 2, 1).reshape(n, -1)   # (N, 3*(K-1)) channel-major\n    op = opacities.cpu().numpy().astype(np.float32).reshape(n, 1).clip(1e-6, 1 - 1e-6)\n    op = np.log(op / (1.0 - op))                             # inverse sigmoid (logit)\n    scale = np.log(scales.cpu().numpy().astype(np.float32).clip(min=1e-8))\n    rot = rotations.cpu().numpy().astype(np.float32)         # (N, 4)\n\n    attrs = (['x', 'y', 'z', 'nx', 'ny', 'nz']\n             + [f'f_dc_{i}' for i in range(3)]\n             + [f'f_rest_{i}' for i in range(f_rest.shape[1])]\n             + ['opacity'] + [f'scale_{i}' for i in range(3)] + [f'rot_{i}' for i in range(4)])\n    elements = np.empty(n, dtype=[(a, 'f4') for a in attrs])\n    elements[:] = list(map(tuple, np.concatenate([xyz, normals, f_dc, f_rest, op, scale, rot], axis=1)))\n\n    header = \"ply\\nformat binary_little_endian 1.0\\n\" + f\"element vertex {n}\\n\"\n    header += \"\".join(f\"property float {a}\\n\" for a in attrs) + \"end_header\\n\"","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/Comfy-Org/ComfyUI/blob/1c6d8d45b3693bfbb32385b410d813a7fd6be216/comfy_extras/nodes_gaussian_splat.py#L46-L82","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Inspect the upstream node that produced the SPLAT and confirm it actually generated gaussians (check its length/real-length output or log).","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).","Relax any culling/densification threshold upstream that can drop all gaussians for an item.","Guard in the workflow: skip the save node when the item length is 0 instead of letting it raise."],"exampleFix":"// before\nsplat = some_empty_splat\nfile = SplatToFile3D().execute(splat, format=\"ply\")\n\n// after\nend = _real_len(splat, 0)\nif end == 0:\n    raise ValueError(\"upstream produced 0 gaussians for item 0\")\nfile = SplatToFile3D().execute(splat, format=\"ply\")","handlingStrategy":"validation","validationCode":"from comfy_extras.nodes_gaussian_splat import _real_len\n\nend = _real_len(splat, 0)\nif end == 0:\n    raise ValueError(\"upstream splat item 0 is empty; fix generation before export\")","typeGuard":"def has_splat_item(splat, i: int) -> bool:\n    return splat is not None and splat.positions.shape[0] > i and _real_len(splat, i) > 0","tryCatchPattern":"try:\n    out = SplatToFile3D().execute(splat, format=\"ply\")\nexcept ValueError as e:\n    if \"gaussian is empty\" in str(e):\n        logging.warning(\"skipping empty splat export\")\n    else:\n        raise","preventionTips":["Check the real length of batch item 0 before wiring SplatToFile3D.","Ensure upstream splat generation cannot silently return zero gaussians (assert on counts).","When batching, put the populated item at index 0 since the node only exports item 0."],"tags":["gaussian-splatting","serialization","empty-input","validation"],"backgroundTag":null,"analyzedSha":"1c6d8d45b3693bfbb32385b410d813a7fd6be216","analyzedAt":"2026-08-14T19:37:18.893Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}