Repository navigation
SOG v3 and opacity above 1 - #370
Merged
Merged
Conversation
- Decimators record the opacity above 1 of merged gaussians as a PLY opacity_excess column (A = sigmoid(opacity) + opacity_excess); sources, readers and writers carry it. - SOG v3 is the default output (--sog-version 2 keeps v2): an opacity codebook that can exceed 1, 24-bit exp-map rotations in the alpha channels (no quats.webp), an optional 4th SH band, and per-gaussian blind data (--sog-blind). HTML output still embeds v2. - extras in meta.json and lod-meta.json (--sog-extras), kept when combined inputs agree. - The renderer draws opacity above 1 with the plateau profile; --no-compensation draws plain gaussians. - 4th SH band support in PLY, SPZ, SH rotation and rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scale codebook gave the large splats that cover most of the frame only a few entries (3-4 for 8-17K large splats on a sky scene, none above log scale 1 on an indoor scene), and where those entries landed depended on the single largest scale, so render quality varied by about 3 dB between splat sets of the same size. quantize1dColumns takes an importance exponent, weighting each bin by exp(gamma * value), and SOG scales use 0.25. Encoding loss on random subsets of a sky scene improves 0.9-2.8 dB at every count from 64K to 4096K at the same file size, and the spread between subsets of the same size narrows from 3.0 to 1.6 dB at 1024K. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- SOG v3 SH labels are 24-bit: shN_labels.b holds bits 16-23 (v2 still writes 0). The reader decodes them for v3. - --sh-palette-size (sogPaletteSize) sets the palette for .sog / meta.json output: up to 262144 entries in v3 (4096 palette rows, the WebGL2 minimum texture size) and 65536 in v2. The default stays one entry per 1024 gaussians, at most 65536; LOD output keeps it. - shN.count records the entries actually written, fewer than requested when the scene has fewer gaussians. - GPU k-means convert/divide kernels use a 2D dispatch, so palettes whose size times coefficients exceed 65535 workgroups (e.g. 262144 entries with 4 SH bands) no longer fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cap only steers merge selection; merges keep the excess as opacity_excess. Removing it was A/B tested on five scenes and was worse with compensation on, so note that at the four cap sites, which must stay in step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
meta.blind is now a list of blind textures, each a set of fields packed into its R, G, B, A bytes. A field stores a source column as an unsigned integer (u8, u16, u24, u32), float bits (f16, f32) or a quantized float (unorm8, unorm16, over [0, 1] or an explicit [min, max] recorded in meta). The CLI's --sog-blind takes one texture per flag, e.g. --sog-blind a:unorm8,b:unorm8[0.5,40],id, and defaults a field to the fewest-byte unsigned integer that holds it. The reader turns each field back into a column; the LOD writer keeps only the fields the environment scene has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take main's removal of the legacy DataTable rasterizer (playcanvas#369): drop sog-v3's SH band 4 edits to gpu-splat-rasterizer.ts and the preprocess helpers it used; the scene rasterizer already carries band 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One opaque gaussian viewed from 32 directions on every scene tier; the centre colour must match an independent CPU evaluation of the real spherical harmonics (associated Legendre, Condon-Shortley phase) to within 1/255. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed byte Opacity above ~1.6 comes from merges over-counting overlapping children: on five scenes at deep LOD levels the render improves as it is capped, down to 1.6, and capping it inside the merge leaves the cascade unchanged. Drawing the plateau at exactly its mass (no 0.4 shrink) then scores 10.1% lower two-sided gap than no compensation, against 6.3% before, at the same fill. - SOG v3 opacity is a fixed byte instead of a codebook: b <= 224 holds A = (b/224)^2, b > 224 holds (1, 1.6] linearly. The square root beats v2's linear 255 levels on 13 Inria scenes (+2.3 dB) and a low-opacity object scan (+6.2 dB). meta.opacity.max is written only when some opacity exceeds 1. - The renderer clamps A to 1.6 and reads the exact-mass shape from a 65-entry table, replacing the per-gaussian bisection and PLATEAU_SHRINK. - Decimation is unchanged; PLY keeps the full opacity_excess. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One formula with no branch, for SOG decoders outside PlayCanvas: A = (b/204)^2 = (1.25 * b/255)^2. The square root keeps fine steps at low opacity, byte 204 is exactly 1 (14% of trained gaussians are saturated, so 1 must sit on a code), and the top is exactly 1.5625, which the cap study scores the same as 1.6. On 13 trained Inria scenes and a low-opacity object scan this is +1.9 dB / +5.7 dB over v2's linear byte, within 0.5 dB of the two-segment encoding it replaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utoff The plateau profile is evaluated at r * e(D), with e(D) = ((D - 1) + cutoff / sqrt(D)) / cutoff, so it reaches the gaussian floor at SIGMA_CUTOFF like a plain gaussian and keeps its footprint. plateauShape now solves I(D) / e(D)^2 = A so the mass stays exact (D up to ~1.84). This matches the engine, whose culls and extents no longer depend on the plateau. Quality-neutral on the alpha study: 14-cell mean gap 17.917% -> 17.911%, at most 0.09 points apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plateauShape preserved the mass of an untruncated profile, but the rasterizer subtracts GAUSSIAN_FLOOR so each splat reaches 0 at SIGMA_CUTOFF, which drew plateaus a few percent heavier than their opacity. It now solves against plateauDrawnMass, the closed-form mass of the profile as drawn with the floor subtracted, relative to a plain gaussian's (D up to ~1.73 at 3 sigma). A CPU test integrates the rasterizer's profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gaussian edge The squeezed plateau is the plain gaussian with its radius remapped past an opaque core of radius u0 (a fraction of the cutoff), and its drawn mass is quadratic in u0, so u0 has a closed form in the opacity for any cutoff. This replaces the shape D, its bisection solve, the 65-entry table in the project shader and the long drawn-mass formula with plateauCoreConstants/plateauCore, matching the engine's gsplatPlateauCore (whose constants it reproduces at 2√2σ). Renders match the previous build to 94-99 dB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blind fields are packed in the order listed, so meta no longer records each field's offset, and a unorm field's custom range is stored as range: [min, max] instead of min / max (default [0, 1], omitted). The writer also rejects field names whose shader identifiers collide or are empty, since the engine reads each field through getBlind_<id>() and trusts the writer to have checked. Matches playcanvas/engine#9960. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scenes trained with anti-aliasing (model 'antialiased') now have each splat's opacity scaled by sqrt(det Σ / det(Σ + 0.3 px²)), so it draws the undilated mass, as the engine's aaFactor does. The factor is applied before the over-unity profile, in the engine's order, so an over-unity splat it brings to 1 or below draws plain. A new test checks the drawn mass ratio on every scene tier for plain, sub-pixel, half-opaque and over-unity splats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Postshot (v1.0.110 to v1.1.69) writes `comment postshot.anti_aliasing=1` and omits the line without anti-aliasing; the reader looked for `antialiased 1`, which no producer writes, so Postshot anti-aliased scenes read as default. The tests now use the comment lines of a real Postshot export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scenes trained with anti-aliasing are now filtered with a 0.1 px² screen-space filter instead of the 0.3 px² dilation, with their opacity scaled by sqrt(det Σ / det(Σ + filter)), as the engine does with its antiAliasFilterSize. 0.1 is Mip-Splatting's value, which Brush, LichtFeld Studio and Spirula train with, and the engine's new default. Other scenes keep the 0.3 px² dilation. The filter can be overridden with writeImage's antiAliasFilterSize (renderAntiAliasFilterSize in Options) or --aa-filter-size <px²> on the CLI, for scenes trained with another filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PLY's header comments (e.g. `Postshot v1.1.69`, `postshot.color.eotf=sRGB`, `Offset: ...`) are kept as the scene's `comments`, minus the model tags and splat-transform's own generator line, which are written afresh. PLY and compressed PLY write them back as `comment` lines (a line break becomes a space); SOG meta.json and lod-meta.json store them as `comments`, once per streamed scene, and the readers read them back. They survive decimation and DataTable actions, and combining inputs whose comments differ warns and drops them. Comments are not interpreted, so coordinate ones are not updated by transforms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`comments` is a SOG v3 addition: v2 meta.json leaves it out, and so does the lod-meta.json of a streamed scene written with v2 chunks, so v2 output stays as it was. Readers still accept it wherever it appears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's option list was missing --decimate-adaptive, --gpu-backend, --lod-file-count, --sog-version, --sog-blind, --sog-extras and --no-compensation; each now uses the CLI help text and sits where the help lists it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`extras` is new in SOG v3, like `comments`: v2 meta.json carries neither, and lod-meta.json writes them only with v3 chunks, so v2 output stays exactly as released. Readers still accept `extras` wherever it appears, and --sog-extras is documented as SOG v3 only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PLY column now holds the true (linear) opacity A for every row, above 1 for merged gaussians, and always wins over the opacity logit when present. Writers still set opacity = logit(min(A, 1)) for tools that don't know the column. Sources without the column read as sigmoid(opacity) when combined with ones that have it, and --no-compensation clamps the column to 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Give every unit of a streamed scene opacity_linear when any selected unit has it, so a level with opacity above 1 keeps it when the first level does not. - Read opacity_linear after the blind fields, where withOpacityLinear appends it, so SOG sources with and without it combine. - Quantize only lod-meta.json's own asset and tree floats, writing extras exactly as given. - Drop model tags and generator lines from comments written to PLY, so comments from elsewhere cannot override the scene's model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly formats - Fill opacity_linear from sigmoid(opacity) for CLI inputs without it before combining, so the table combine no longer zero-fills it and makes those inputs transparent. - Bake opacity_linear into the opacity logit for compressed PLY, .splat, SPZ, GLB and voxel output, which store only the logit; the column wins over the logit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…taTable writers - Give every LCC2 chunk opacity_linear when any selected chunk has it (streamed reader), and fill the rows of chunks without it from sigmoid(opacity) instead of zero (eager reader), sharing the unit check with the streamed SOG reader. - Reduce SH4 tables to 3 bands inside the exported writeCompressedPly and writeGlb, keeping each channel's coefficient offsets, so direct calls no longer crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roperty per PLY A source's geometric opacity slot now holds true (linear) opacity instead of the PLY logit, so there is no opacity_linear column inside the pipeline. Readers convert on the way in and writers on the way out; the DataTable bridge converts both ways. This removes the "opacity_linear always wins" rule every consumer had to apply, and the layout special cases it forced on concat, stack-LODs, the LOD and LCC2 containers, the CLI combine and the decimators. - PLY files carry one opacity property: opacity_linear when the scene may exceed 1 (decimated output, SOG v3 with opacity above 1), else the standard opacity logit. A file carrying both reads opacity_linear. - --ply-logit-opacity (Options.plyLogitOpacity) forces the standard logit, clamping opacity above 1 with a warning. - ChunkSourceMetadata.opacityAboveOne tells the streaming PLY writer which property to write; concat and stackLods OR it, the LOD and LCC2 readers set it from their units' meta.json. - Opacity 1 and 0 write as +Infinity and -Infinity logits, so infinite logits round-trip. - Decimators, filters, stats and the renderer read true opacity directly; opacity filters compare it (opacity_raw compares the logit) and --no-compensation clamps it in the shader. - combine() fills a missing opacity_linear from opacity; the DataTable filterNaN also keeps a -Infinity opacity logit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment opacity above 1 - writeCompressedPly, writeGlb, writeSpz and writeVoxel take the opacity logit from a table's opacity_linear when it has one (withOpacityLogit), so a stale logit cannot win. - README: explain the two kinds of PLY (opacity or opacity_linear), --ply-logit-opacity, and the updated filter descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orm endpoints - processDataTable's opacity filter compares true opacity (opacity_linear, which wins, else the f32 sigmoid of the logit) and accepts any threshold, as the source filters do. - opacity_raw compares each gaussian's exported logit with the threshold rounded the same way, so opacity above 1 compares as its clamped logit and a read logit compares equal to itself. - SOG blind unorm encoding accepts values stored as f32 just outside the range's endpoints, clamping them before quantizing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rounding the threshold through f32 opacity collapsed thresholds near saturation (16, 16.2, the clamped 16.118) onto ~15.94, so opacity 1.5 passed opacity_raw > 16.2 and failed equality with its own stored logit. Each gaussian now compares its exact stored logit, counting as equal only to a threshold that reads as exactly its opacity, which keeps a read logit equal to itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SOG reader tags each blind column with its texture, encoding and unorm range, and the SOG writer stores tagged columns the same way when no blind option is given. An empty blind list (--sog-blind none) stores none. Concat and stackLods widen unorm ranges and keep the first input's encoding on conflict, with a warning; DataTable islands re-attach the tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd data - Streamed SOG and LCC2 containers combine the blind origins of every selected unit (summarizeUnits, replacing anyUnitOpacityAboveOne), so an inherited unorm range covers all units' values. - Combining inputs widens a column's encoding to hold every input: the wider unsigned integer, unorm16 over the union of the ranges for two unorms, else f32. A texture the widening overfills continues in the next, and the conflict warning is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Layout-changing merges and PLY-spilled decimation lose blind-column metadata, causing subsequent default SOG writes to silently omit blind data.
5 open issues
Preserve blind origins when rebuilding sources with different layouts · New Retain blind metadata across PLY-spilled generations · New Retain blind metadata across PLY-spilled generations · New Include untagged inputs when selecting safe column encoding · New Avoid clamping valid opacities below 1 during logit conversion · New
What changed in this PR
This PR introduces SOG v3 and carries linear opacity above 1 through splat-transform’s conversion, decimation, filtering, and rendering pipeline, supporting the format proposed in #362.
Changes:
- Adds SOG version controls, blind-data and metadata handling, and compatibility options for older formats.
- Preserves merged opacity above 1 and adds corresponding rendering and anti-aliasing behavior.
- Extends spherical harmonics to four bands, updates GPU processing, and expands regression coverage and CLI documentation.
| File | Description |
|---|---|
test/stats.test.mjs |
Updates statistics expectations for linear opacity. |
test/source.test.mjs |
Allows opacity conversion rounding. |
test/rotate-sh.test.mjs |
Tests fourth-band rotation correctness. |
test/render-sh.test.mjs |
Checks SH rendering across all tiers. |
test/read-lcc2.test.mjs |
Tests mixed-opacity container chunks. |
test/read-lcc.test.mjs |
Updates linear-opacity reader assertions. |
test/quantize-1d.test.mjs |
Tests importance-weighted quantization. |
test/process.test.mjs |
Accepts boundary opacity thresholds. |
test/process-source.test.mjs |
Updates infinite-logit filtering expectations. |
test/ply-streaming.test.mjs |
Allows opacity round-trip rounding. |
test/ops.test.mjs |
Updates source-operation opacity assertions. |
test/moment-match.test.mjs |
Checks merges using linear opacity. |
test/helpers/test-utils.mjs |
Adds opacity-aware comparison helpers. |
test/helpers/synthetic-source.mjs |
Generates linear-opacity source data. |
test/gpu-recost.test.mjs |
Uses linear opacity in GPU fixtures. |
test/formats.test.mjs |
Updates opacity checks and fourth-band SPZ coverage. |
test/decimate-source.test.mjs |
Checks uncapped opacity and metadata. |
test/decimate-recost.test.mjs |
Uses linear opacity in cost fixtures. |
test/decimate-opacity.test.mjs |
Tests mass preservation across generations. |
test/combinators.test.mjs |
Uses opacity-aware row comparisons. |
test/cli.test.mjs |
Tests new options and metadata round-trips. |
src/lib/writers/write-voxel.ts |
Converts linear opacity for voxel output. |
src/lib/writers/write-spz.ts |
Converts linear opacity for SPZ output. |
src/lib/writers/write-splat-streaming.ts |
Encodes linear opacity directly. |
src/lib/writers/write-ply-streaming.ts |
Selects opacity property and preserves comments. |
src/lib/writers/write-lod.ts |
Passes SOG options and scene metadata. |
src/lib/writers/write-image.ts |
Exposes opacity and anti-aliasing controls. |
src/lib/writers/write-html.ts |
Keeps embedded SOG output at v2. |
src/lib/writers/write-glb.ts |
Adapts opacity and limits SH bands. |
src/lib/writers/write-compressed-ply.ts |
Adapts opacity, bands, and comments. |
src/lib/writers/utils.ts |
Formats preserved PLY comments. |
src/lib/write.ts |
Routes new writer options and compatibility reductions. |
src/lib/workers/tasks.ts |
Passes quantization importance to worker tasks. |
src/lib/workers/index.ts |
Exposes quantization importance through worker dispatch. |
src/lib/value-transforms.ts |
Updates display and raw-opacity conversions. |
src/lib/utils/rotate-sh.ts |
Implements fourth-band SH rotation. |
src/lib/utils/quat-expmap.ts |
Adds SOG v3 quaternion encoding. |
src/lib/utils/opacity.ts |
Adds opacity encoding and rendering helpers. |
src/lib/types.ts |
Defines new format and rendering options. |
src/lib/spatial/quantize-1d-core.ts |
Adds importance-weighted quantization. |
src/lib/render/scene-renderer.ts |
Configures opacity and anti-aliasing behavior. |
src/lib/render/config.ts |
Defines the anti-aliasing filter default. |
src/lib/readers/read-spz.ts |
Reads linear opacity and fourth-band SH. |
src/lib/readers/read-splat.ts |
Decodes source opacity into linear values. |
src/lib/readers/read-ply.ts |
Reads linear opacity, comments, and four SH bands. |
src/lib/readers/read-lod.ts |
Collects unit metadata and scene annotations. |
src/lib/readers/read-lcc2.ts |
Handles SOG v3 and mixed-opacity chunks. |
src/lib/readers/read-lcc.ts |
Decodes linear source opacity. |
src/lib/readers/container-source.ts |
Summarizes opacity and blind-field metadata. |
src/lib/process.ts |
Updates opacity filters and SH-band limits. |
src/lib/process-source.ts |
Restores metadata after DataTable processing. |
src/lib/ops/stack-lods.ts |
Combines scene and blind-field metadata. |
src/lib/ops/reduce-bands-source.ts |
Documents four-band input support. |
src/lib/ops/filter-mask.ts |
Filters linear opacity and reconstructed logits. |
src/lib/ops/concat-source.ts |
Combines opacity hints and metadata. |
src/lib/ops/bake-transform.ts |
Supports fourth-band coefficient layouts. |
src/lib/index.ts |
Exports blind-data option types. |
src/lib/gpu/shaders/rasterize-binned.ts |
Draws the opaque-core opacity profile. |
src/lib/gpu/shaders/project.ts |
Projects linear opacity, anti-aliasing, and fourth-band SH. |
src/lib/gpu/shaders/chunks/sh-band-4.ts |
Evaluates fourth-band SH coefficients. |
src/lib/gpu/gpu-scene-rasterizer.ts |
Configures new shader variants and layouts. |
src/lib/gpu/gpu-recost.ts |
Documents deliberate cost-model opacity capping. |
src/lib/gpu/gpu-kmeans.ts |
Supports larger palettes and wider SH records. |
src/lib/gpu/gpu-edge-cost.ts |
Clarifies deliberate merged-opacity capping. |
src/lib/extras.ts |
Adds scene-extras propagation helpers. |
src/lib/decimate/recost-core.ts |
Documents deliberate cost-model opacity capping. |
src/lib/decimate/moment-match.ts |
Preserves uncapped merged opacity. |
src/lib/decimate/edge-cost-cpu.ts |
Separates true mass from capped scoring opacity. |
src/lib/decimate/decimate-source.ts |
Marks merged opacity and preserves scene metadata. |
src/lib/decimate-uniform/priority.ts |
Packs linear opacity and wider appearance records. |
src/lib/decimate-uniform/gpu-edge-cost.ts |
Sizes appearance chunks for four SH bands. |
src/lib/decimate-uniform/edge-cost-cpu.ts |
Uses linear opacity in cost calculations. |
src/lib/decimate-uniform/decimate-source.ts |
Updates buffer sizing and output metadata. |
src/lib/data-table/transform.ts |
Transforms fourth-band SH coefficients. |
src/lib/data-table/sh-bands.ts |
Detects and reduces four-band SH data. |
src/lib/data-table/opacity.ts |
Adapts linear opacity for logit-only writers. |
src/lib/data-table/index.ts |
Exports opacity and SH-reduction helpers. |
src/lib/data-table/combine.ts |
Fills missing linear-opacity values. |
src/lib/compat/data-table.ts |
Bridges linear source opacity and table logits. |
src/lib/comments.ts |
Adds producer-comment propagation helpers. |
src/lib/chunk/source.ts |
Defines opacity hints and scene metadata. |
src/lib/chunk/layout.ts |
Extends SH layouts and blind-field metadata. |
src/lib/chunk/index.ts |
Exports the linear-opacity column constant. |
src/lib/chunk/in-memory.ts |
Carries the opacity-above-one hint. |
src/cli/index.ts |
Parses and routes new format and rendering options. |
README.md |
Documents compatibility changes and CLI controls. |
Review effort: Balanced
…spills, and write opacity below 1 at its own logit - Combining inputs widens a blind column that some input leaves untagged (or lacks) to u32 / f32, which holds any value of its type, so a SOG id:u8 combined with a PLY id of 1000 writes. - The CLI's mixed-layout combine merges the inputs' blind origins by column name after the DataTable round-trip. - Decimation carries the input's blind origins across generations that spill through PLY. - opacityToLogit clamps only opacity above 1, to the largest f32 below 1 (logit ~16.64), so opacity just below 1 keeps its own logit and opacity above 1 never logs lower than it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The container can silently corrupt output by reading mismatched SOG fields using the first unit’s layout and retaining stale values for units without blind fields.
1 open issue
5 resolved since last review
Review effort: Balanced
Every unit of a streamed SOG or LCC2 container fills the first unit's layout, so a unit with blind fields in another order, or none, would misplace values or leave stale ones. summarizeUnits now throws when units' extra columns differ instead of skipping them, and each unit is checked again when it is decoded, alongside the SH band check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 9, 2026
mvaligursky
added a commit
to playcanvas/engine
that referenced
this pull request
Nov 9, 2026
…er (#9979) Rewrite examples/assets/splats/playcanvas-logo-v3 with splat-transform main (the merge of playcanvas/splat-transform#370). The logo and edge blind data is carried over unchanged, and meta.json no longer has the offset keys that the pre-release writer added to blind fields. Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Adds SOG v3, the format proposed in #362, and carries opacity above 1 through the whole pipeline: decimation, PLY, SOG, filters and the renderer. Engine support: playcanvas/engine#9873 and playcanvas/engine#9960 (merged), playcanvas/engine#9972 (PLY
opacity_linear). Spec and CLI docs: playcanvas/developer-site#1336.SOG v3
SOG v3 is now the default output.
--sog-version 2writes v2 for older viewers; it drops opacity above 1, blind data and a 4th SH band, with a warning. HTML output still embeds v2.A = (b / 204)², capped at 1.5625. No codebook. On 13 trained Inria scenes it scores +1.9 dB over v2's linear byte, and +5.7 dB on a low-opacity object scan.meta.opacity.maxis written only when some opacity exceeds 1.quats.webp.--sh-palette-size).u8–u32,f16,f32,unorm8/unorm16with an optional range), packed into blind textures with--sog-blind. A SOG input's blind data is kept by default, with the same encodings, ranges and grouping.--sog-blind nonedrops it.extras(--sog-extras, application-defined JSON as in glTF) andcomments(PLY header comments) inmeta.jsonandlod-meta.json.Opacity above 1
Merging gaussians conserves mass, so merged opacity can exceed 1.
opacity_linearwhen the scene may exceed 1: decimated output, or SOG v3 with opacity above 1.opacitylogit.opacity_linear.--ply-logit-opacityforces the standard logit, clamping opacity above 1 with a warning..splat, SPZ, GLB, voxel) clamp to 1.--no-compensationclamps it to 1.opacitycompares true opacity, and any threshold is allowed.opacity_rawcompares the logit a standard PLY would store.Anti-aliased scenes
sqrt(det Σ / det(Σ + filter)), as the engine does.--aa-filter-sizeoverrides it.postshot.anti_aliasing=1comment.Other changes
Breaking changes
.sog/meta.jsonoutput is v3, which needs a viewer with v3 support. Use--sog-version 2for older viewers.opacity_linearinstead ofopacity, so tools that don't know the property won't find an opacity. Use--ply-logit-opacityfor them.ChunkSource's geometric opacity slot holds linear opacity, not the logit. DataTables still carry theopacitylogit, plusopacity_linearwhen some opacity exceeds 1.