@viggle/splat-engine-three 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +450 -2
  3. package/dist/chunks/chunk-WN5WBKM4.js +8606 -0
  4. package/dist/chunks/chunk-WN5WBKM4.js.map +7 -0
  5. package/dist/index.js +56 -0
  6. package/dist/index.js.map +7 -0
  7. package/dist/types/anim/animation-conversion.d.ts +25 -0
  8. package/dist/types/anim/bind-pose.d.ts +15 -0
  9. package/dist/types/anim/keyframe-splines.d.ts +59 -0
  10. package/dist/types/anim/rest-relative.d.ts +32 -0
  11. package/dist/types/anim/spline.d.ts +14 -0
  12. package/dist/types/animation.d.ts +216 -0
  13. package/dist/types/armature.d.ts +524 -0
  14. package/dist/types/character.d.ts +582 -0
  15. package/dist/types/engine.d.ts +166 -0
  16. package/dist/types/environment.d.ts +53 -0
  17. package/dist/types/index.d.ts +40 -0
  18. package/dist/types/lighting/color.d.ts +13 -0
  19. package/dist/types/lighting/point-light.d.ts +33 -0
  20. package/dist/types/lighting/sky-light.d.ts +22 -0
  21. package/dist/types/lighting/sun-light.d.ts +34 -0
  22. package/dist/types/loader/animation/animation-glb.d.ts +117 -0
  23. package/dist/types/loader/character/index.d.ts +33 -0
  24. package/dist/types/loader/character/legacy/index.d.ts +34 -0
  25. package/dist/types/loader/character/legacy/interleaved-splat.d.ts +101 -0
  26. package/dist/types/loader/character/legacy/planar-splat.d.ts +139 -0
  27. package/dist/types/loader/character/load.d.ts +33 -0
  28. package/dist/types/loader/character/types.d.ts +129 -0
  29. package/dist/types/loader/character/v1/decode.d.ts +32 -0
  30. package/dist/types/loader/environment/ply.d.ts +16 -0
  31. package/dist/types/loader/shared/bin-cache.d.ts +19 -0
  32. package/dist/types/loader/skeleton/index.d.ts +19 -0
  33. package/dist/types/loader/skeleton/skeleton-data-441.d.ts +5 -0
  34. package/dist/types/loader/skeleton/skeleton-data.d.ts +6 -0
  35. package/dist/types/math.d.ts +82 -0
  36. package/dist/types/skeletal/animation-player.d.ts +185 -0
  37. package/dist/types/skeletal/bone-mask.d.ts +27 -0
  38. package/dist/types/skeletal/bone-stretch.d.ts +65 -0
  39. package/dist/types/skeletal/pose.d.ts +27 -0
  40. package/dist/types/skeletal/skeleton.d.ts +57 -0
  41. package/dist/types/skeletal/splat-skinning.d.ts +69 -0
  42. package/dist/types/splat/backend.d.ts +57 -0
  43. package/dist/types/splat/bone-binding.d.ts +38 -0
  44. package/dist/types/splat/depth-sort.d.ts +76 -0
  45. package/dist/types/splat/importance.d.ts +60 -0
  46. package/dist/types/splat/packed-splats.d.ts +36 -0
  47. package/dist/types/splat/prune.d.ts +60 -0
  48. package/dist/types/splat/quality.d.ts +175 -0
  49. package/dist/types/splat/splat-mesh.d.ts +71 -0
  50. package/dist/types/splat/splat.d.ts +252 -0
  51. package/dist/types/splat/transform-palette.d.ts +42 -0
  52. package/dist/types/splat/webgl/backend.d.ts +17 -0
  53. package/dist/types/splat/webgl/shaders.d.ts +2 -0
  54. package/dist/types/splat/webgl/splat-mesh.d.ts +54 -0
  55. package/dist/types/splat/webgpu/backend.d.ts +25 -0
  56. package/dist/types/splat/webgpu/lighting.d.ts +34 -0
  57. package/dist/types/splat/webgpu/radix-sort.d.ts +46 -0
  58. package/dist/types/splat/webgpu/splat-mesh.d.ts +73 -0
  59. package/dist/types/three/loaders.d.ts +77 -0
  60. package/dist/types/three/splat-character.d.ts +220 -0
  61. package/dist/types/types.d.ts +30 -0
  62. package/dist/types/webgpu.d.ts +5 -0
  63. package/dist/webgpu.js +843 -0
  64. package/dist/webgpu.js.map +7 -0
  65. package/package.json +46 -4
@@ -0,0 +1,175 @@
1
+ export declare const SplatQuality: {
2
+ /**
3
+ * Alpha at which a splat's quad is allowed to end, so each one is sized to
4
+ * the radius its own opacity actually reaches instead of to the worst case.
5
+ * **The first knob to reach for on a weak GPU.**
6
+ *
7
+ * The fragment stage multiplies a gaussian falloff by the splat's opacity
8
+ * and discards anything below 1/255, so a quad drawn out to where that
9
+ * product is already below the cutoff spends fragments on a ring that
10
+ * cannot put ink on the screen. Raising the cutoff shrinks every quad. What
11
+ * it costs is a harder edge on each splat, which shows first on soft things
12
+ * - hair, fabric, anything semi-transparent.
13
+ *
14
+ * Takes effect on the next frame; nothing is reloaded and nothing is lost,
15
+ * so it is safe to drive from a frame-rate governor.
16
+ *
17
+ * | Value | What it is for |
18
+ * |---|---|
19
+ * | `1 / 255` | Exactly the behaviour before this knob existed. Changes nothing at all: below it a fragment is discarded anyway, so the quad it trims was drawing nothing. |
20
+ * | `8 / 255` | What the Unity build ships. The conservative end. |
21
+ * | `12 / 255` | **The default here.** |
22
+ * | `16 / 255` | **Low-end phones, and the floor.** Still 43.7 dB overall and 34.8 dB on the face, which the Unity build calls invisible at arm's length. |
23
+ * | above `16 / 255` | Not on this project. The Unity build measures a further -7% at 24/255, but the edges are visibly harder by then; a device that needs more should spend it on {@link SplatQuality.pruneRatio}, the renderer's pixel ratio, or a distance LOD, none of which harden every silhouette in the frame. |
24
+ *
25
+ * Measured on a Mali-G57 with three characters, against 8/255: 12/255 -7%,
26
+ * 16/255 -12%, 24/255 -19%. Measured here on an Apple GPU where fragments
27
+ * are the bottleneck, against 1/255: 8/255 -29%, 16/255 -42%, 24/255 -50%.
28
+ * Where the main thread is the bottleneck instead - the WebGL backend with
29
+ * animated characters - it is free but buys nothing; see the README's
30
+ * Tuning section for which case a scene is in.
31
+ */
32
+ quadAlphaCutoff: number;
33
+ /**
34
+ * Drop splats whose projected size falls below this radius in pixels, for
35
+ * every splat at once. `0` (the default) draws everything.
36
+ *
37
+ * The same test as the per-splat {@link Splat.minPixelSize}; the larger of
38
+ * the two wins, so a global floor cannot un-cull a splat an application
39
+ * already asked to cull. Removes distant crowd splats that cannot contribute
40
+ * more than a fraction of a pixel.
41
+ */
42
+ screenSizeCull: number;
43
+ /**
44
+ * Peak opacity a splat must reach to be kept when a character or scene is
45
+ * loaded. `0` keeps everything.
46
+ *
47
+ * A splat's alpha at its centre IS its opacity - the gaussian only falls
48
+ * off from there - so the faintest splats in a bundle are ones that paint
49
+ * almost nothing from any camera while still costing memory, a skinning
50
+ * pass, a sort slot and a quad. This removes them outright, which is what
51
+ * separates it from {@link SplatQuality.quadAlphaCutoff}: the cutoff
52
+ * shrinks a splat's quad and saves rasterisation alone, this saves
53
+ * everything, but it cannot half-remove one.
54
+ *
55
+ * **Applied during the decode of a CHARACTER bundle**, so it only affects
56
+ * what is loaded afterwards - set it before the first `load()`, and changing
57
+ * it later does nothing to characters already in the scene. It does not
58
+ * touch `.ply` scenes, matching the Unity build, which applies it in the
59
+ * skinned renderer and nowhere else: a scene's opacity distribution is a
60
+ * different thing, and 0.05 measured as 8.7% of a character against 41.8%
61
+ * of a 1M-splat room. {@link pruneSplats} is exported for an application
62
+ * that wants to make the trade on a scene of its own. It also changes
63
+ * {@link Splat.numSplats}, so an application that hands back a per-splat
64
+ * array ({@link Splat.setSplatGroups}) must size that array from
65
+ * `numSplats` after loading rather than from the file.
66
+ *
67
+ * | Value | What it is for |
68
+ * |---|---|
69
+ * | `0` | Keep every splat. The behaviour before this knob existed. |
70
+ * | `1 / 255` | Free in the strict sense: provably pixel-identical, because a splat under the fragment shader's own discard cannot produce a pixel. Worth about 2% of a character. |
71
+ * | `0.05` | **The default here, and what the Unity build ships.** Removes ~13% of a typical character for a difference of at most 11/255 on 1.3% of it - 58-62 dB, which does not read as a quality loss. |
72
+ * | above `0.05` | Untested. Prefer {@link SplatQuality.pruneRatio} to go further: it is measured, and it thins every region by the same share instead of taking the faintest splats wherever they happen to be - which is disproportionately the face. |
73
+ */
74
+ minOpacity: number;
75
+ /**
76
+ * Fraction of splats removed at load by voxel-local importance, on top of
77
+ * {@link SplatQuality.minOpacity}. `0` (the default) keeps everything, `0.5`
78
+ * drops the least-important half of every cell.
79
+ *
80
+ * Importance is opacity times a splat's two largest axes - the area it
81
+ * actually paints - ranked separately inside each spatial cell so a dense
82
+ * region loses the same share as a sparse one rather than being deleted
83
+ * wholesale. That per-cell quota is what keeps the face: a global ranking
84
+ * puts the many faint facial splats at the bottom and smears it. Measured on
85
+ * the Unity build's reference character at 0.5: 25,826 of 50,267 splats
86
+ * remain, the frame falls 24.5% on a Mali-G57, and the face holds 43.5 dB
87
+ * where a global rank gives 37.6.
88
+ *
89
+ * Applied during the decode of a character bundle, like
90
+ * {@link SplatQuality.minOpacity}, with the same scope and the same reason
91
+ * for being off by default.
92
+ */
93
+ pruneRatio: number;
94
+ /**
95
+ * Cell size, in metres, for {@link SplatQuality.pruneRatio}'s per-voxel
96
+ * quota.
97
+ *
98
+ * 4 cm gives ~19 splats per occupied cell on the Unity build's reference
99
+ * character - small enough that the face and the jacket are ranked apart,
100
+ * large enough that a cell has a population to rank. A tuning constant
101
+ * rather than a per-title knob, exposed because a character authored at a
102
+ * different scale needs a different one.
103
+ */
104
+ pruneVoxelSize: number;
105
+ /**
106
+ * Test each splat against the depth buffer, so opaque geometry hides the
107
+ * splats behind it. On by default.
108
+ *
109
+ * Unlike the Unity build, where turning this off was the single largest
110
+ * mobile saving, this is a correctness switch here and not a performance
111
+ * one: that build renders splats into an offscreen target and pays for the
112
+ * test by sampling a depth texture per fragment, where this package draws
113
+ * straight into the camera's target and gets the hardware depth test for
114
+ * free. Turning it off makes characters draw through walls and saves
115
+ * nothing. It is here because the behaviour is occasionally what an
116
+ * application wants - a character deliberately drawn over the scene.
117
+ */
118
+ depthOcclusion: boolean;
119
+ /**
120
+ * Collect {@link SplatQuality.timings}. Off by default.
121
+ *
122
+ * The CPU stages are timed with `performance.now()`, which is cheap but not
123
+ * free at a few calls per splat per frame, and a shipping application that
124
+ * never reads the numbers should not pay for them.
125
+ */
126
+ passTimings: boolean;
127
+ /**
128
+ * Per-stage cost of the last frame in which {@link SplatQuality.passTimings}
129
+ * was on, in milliseconds, summed over every splat that ran that stage.
130
+ *
131
+ * These are main-thread times, which is the measurement that matters for
132
+ * this package: under `WebGLRenderer` the per-character CPU cost - skinning
133
+ * every centre to feed the sort, then the sort - is what limits how many
134
+ * characters a scene can carry, and neither stage exists on the GPU
135
+ * timeline. `skin` and `sort` are zero under `WebGPURenderer`, where both
136
+ * happen in compute; `splatsDrawn` and `splatsTotal` are meaningful on both,
137
+ * and differ when {@link SplatQuality.screenSizeCull} or a count LOD is
138
+ * reducing what is submitted.
139
+ *
140
+ * These describe the most recent pass in which any splat drew, so read them
141
+ * after your render call. A pass that drew no splats leaves the previous
142
+ * numbers in place rather than zeroing them - a zero and "nothing happened"
143
+ * would otherwise read the same, which is the mistake the Unity build's
144
+ * equivalent made and spent a long time chasing.
145
+ */
146
+ timings: {
147
+ /** CPU skinning of splat centres, i.e. `Splat.ensureCenters`. */
148
+ skin: number;
149
+ /** The depth sort, where it runs on the main thread. */
150
+ sort: number;
151
+ /** Uniform and buffer updates in `beforeRender`, excluding skin and sort. */
152
+ prepare: number;
153
+ /** Splat instances submitted to the GPU this frame. */
154
+ splatsDrawn: number;
155
+ /** Splat instances those draws could have covered at full detail. */
156
+ splatsTotal: number;
157
+ };
158
+ /** Restore every knob to its default. Call when tearing the engine down. */
159
+ resetStatics(): void;
160
+ };
161
+ /** The effective sub-pixel cull for one splat: its own floor or the global one. */
162
+ export declare function effectiveMinPixelSize(splatMinPixelSize: number): number;
163
+ /**
164
+ * Add a CPU skinning measurement for the frame now being prepared.
165
+ * @internal
166
+ */
167
+ export declare function addSkinTime(ms: number): void;
168
+ /**
169
+ * Open this pass's timing accumulation, clearing it when `source` - the splat
170
+ * reporting - has already contributed, which means a new pass has begun.
171
+ * Called by every backend before it adds to {@link SplatQuality.timings}.
172
+ *
173
+ * @internal
174
+ */
175
+ export declare function beginFrameTimings(source: object): void;
@@ -0,0 +1,71 @@
1
+ import { Mesh } from 'three';
2
+ import type { Camera } from 'three';
3
+ import type { SplatEngine } from '../engine';
4
+ import type { SplatBackend } from './backend';
5
+ import type { Splat } from './splat';
6
+ /**
7
+ * Make the WebGPU backend available to every {@link SplatMesh}. Called by the
8
+ * `@viggle/splat-engine-three/webgpu` entry point when it is imported; the
9
+ * default entry point leaves it out so WebGL applications never load
10
+ * `three/webgpu`.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare function registerWebGPUBackend(backend: SplatBackend): void;
15
+ /**
16
+ * The object that draws one {@link Splat}: an ordinary `THREE.Mesh` in your
17
+ * scene graph, so `visible`, `renderOrder`, `layers` and parent transforms all
18
+ * work as they do for any mesh.
19
+ *
20
+ * It works with both `THREE.WebGLRenderer` and `WebGPURenderer`. Which one is
21
+ * not known until something renders it, so the renderer-specific geometry and
22
+ * material are attached the first time it is drawn; the mesh shows from the
23
+ * following frame.
24
+ *
25
+ * @group Splats
26
+ */
27
+ export declare class SplatMesh extends Mesh {
28
+ /** Type flag, in the three.js manner. */
29
+ readonly isSplatMesh = true;
30
+ /** The splat this mesh draws: its data, tint, grade and render options. */
31
+ readonly splat: Splat;
32
+ private readonly _engine;
33
+ private readonly _asyncSort;
34
+ private _impl;
35
+ /**
36
+ * @param asyncSort - See {@link SplatBackend.createImpl}.
37
+ * @internal Construct a {@link Splat} instead; it creates its own mesh.
38
+ */
39
+ constructor(splat: Splat, engine: SplatEngine, asyncSort: boolean);
40
+ private _attach;
41
+ /** See {@link SplatRenderImpl.needsCpuCenters}. False until a renderer has drawn this mesh. */
42
+ get needsCpuCenters(): boolean;
43
+ /** True while the drawn order is stale or absent, including before the first draw. */
44
+ get sortPending(): boolean;
45
+ requestSort(): void;
46
+ syncDefines(): void;
47
+ syncBounds(): void;
48
+ syncBoneData(): void;
49
+ syncVariantGroups(): void;
50
+ onBeforeRender(renderer: unknown, _scene: unknown, camera: Camera): void;
51
+ /**
52
+ * A second mesh drawing the same splat data, at the same transform. The
53
+ * decoded data is shared; the GPU copy of it is not.
54
+ *
55
+ * Only for static splats. A mesh that belongs to a character is driven by
56
+ * that character's rig and cannot stand alone - clone the `SplatCharacter`.
57
+ */
58
+ clone(): this;
59
+ /**
60
+ * Splats are not pickable. The inherited implementation would test the ray
61
+ * against this mesh's own geometry, which is a single proxy quad at the
62
+ * local origin - every instance is positioned in the shader - and report
63
+ * hits that have nothing to do with where the splats are drawn.
64
+ */
65
+ raycast(): void;
66
+ /**
67
+ * Release every GPU resource behind this mesh - the splat's data textures as
68
+ * well as the renderer-specific buffers - and remove it from its parent.
69
+ */
70
+ dispose(): void;
71
+ }
@@ -0,0 +1,252 @@
1
+ import { Box3, DataTexture, Vector2, Vector4 } from 'three';
2
+ import type { Frustum } from 'three';
3
+ import type { SplatEngine } from '../engine';
4
+ import type { Mat4 } from '../math';
5
+ import type { SplatWeights } from '../types';
6
+ import type { PackedSplats } from './packed-splats';
7
+ import { SplatMesh } from './splat-mesh';
8
+ import { TransformPalette } from './transform-palette';
9
+ /** What a {@link Splat} is built from: packed gaussians, plus skinning data for characters. */
10
+ export interface SplatSource {
11
+ packed: PackedSplats;
12
+ /** Present for skinned splats. Any `K` influences per splat; the top four are used. */
13
+ splatWeights?: SplatWeights;
14
+ /** Bone count of the rig the weights index into, when it differs from the armature's. */
15
+ sourceBoneCount?: number;
16
+ }
17
+ /** @group Splats */
18
+ export interface SplatOptions {
19
+ /**
20
+ * Depth-sort in a worker instead of synchronously. Default: `true` for
21
+ * unskinned splats, `false` for skinned ones (whose order must track the pose).
22
+ */
23
+ asyncSort?: boolean;
24
+ }
25
+ /**
26
+ * Gaussian splat renderer.
27
+ *
28
+ * Owns the splat data textures, the bone index/weight textures used for
29
+ * skeletal deformation, the transform palette written to by the linked
30
+ * {@link Armature}, and the three.js mesh that draws it all. Skinning runs on
31
+ * the GPU when the splat has weights; unskinned splats (environments) take the
32
+ * same path with the identity transform.
33
+ *
34
+ * {@link Character.load} and {@link Environment.load} construct this
35
+ * internally. Direct construction is only needed for custom loading pipelines.
36
+ *
37
+ * @group Splats
38
+ */
39
+ export declare class Splat {
40
+ readonly numSplats: number;
41
+ /** The packed gaussian data backing the textures. */
42
+ readonly packed: PackedSplats;
43
+ /** The three.js object that draws this splat. Parent it; do not replace it. */
44
+ readonly object3D: SplatMesh;
45
+ readonly colorTexture: DataTexture;
46
+ readonly transformATexture: DataTexture;
47
+ readonly transformBTexture: DataTexture;
48
+ /** Per-splat palette slot indices, four per splat. Rewritten when an armature binds. */
49
+ boneIndicesData?: Float32Array;
50
+ boneWeightsData?: Float32Array;
51
+ boneIndicesTexture?: DataTexture;
52
+ boneWeightsTexture?: DataTexture;
53
+ groupIdsTexture?: DataTexture;
54
+ variantMaskTexture?: DataTexture;
55
+ variantMask?: Float32Array;
56
+ groupIds?: Uint8Array;
57
+ /** Bumped whenever the group ids or the variant mask change. */
58
+ variantMaskVersion: number;
59
+ transformPalette: TransformPalette;
60
+ /** rgb tint colour, w tint amount. Shared with the material by reference. */
61
+ readonly tint: Vector4;
62
+ /** x contrast, y saturation. Shared with the material by reference. */
63
+ readonly grade: Vector2;
64
+ linkedArmature: any;
65
+ /** Skinning influences normalised to exactly four slots per splat. */
66
+ splatWeights?: SplatWeights;
67
+ sourceBoneCount?: number;
68
+ /**
69
+ * Splat centres in local space: the rest pose until the first skinning
70
+ * update, the skinned pose after.
71
+ *
72
+ * Only kept current when the renderer sorts on the CPU (WebGL). Under WebGPU
73
+ * this lags the pose; call {@link Splat.ensureCenters} before reading it.
74
+ */
75
+ centers: Float32Array;
76
+ /** Bumped whenever {@link Splat.centers} changes. */
77
+ centersVersion: number;
78
+ /**
79
+ * Local-space bounds used for culling and visibility tests. With CPU
80
+ * centres these track the pose exactly; without, they are the rest bounds
81
+ * padded generously enough to contain any pose.
82
+ */
83
+ readonly localBounds: Box3;
84
+ /** Discard splats smaller than this many pixels on screen. `0` draws everything. */
85
+ minPixelSize: number;
86
+ private _activeSplatCount;
87
+ private _importanceOrder;
88
+ /** `_importanceOrder` inverted: each splat's place in it. @internal */
89
+ private _importanceRank;
90
+ private readonly _mesh;
91
+ private _cpuInfluences;
92
+ private _pendingBoneTransforms;
93
+ private _flatBones;
94
+ private _variantMaskEnabled;
95
+ private _nonRigidCov;
96
+ private _antiAlias;
97
+ private _destroyed;
98
+ private readonly _worldBounds;
99
+ private readonly _worldSphere;
100
+ private readonly _groundHist;
101
+ constructor(engine: SplatEngine, source: SplatSource, opts?: SplatOptions);
102
+ /**
103
+ * A new, independent splat over the same packed data, carrying this one's
104
+ * render options. For unskinned splats only.
105
+ * @internal
106
+ */
107
+ cloneStatic(engine: SplatEngine, asyncSort: boolean): Splat;
108
+ /** Top-4 influences per splat, zero-padded. The shader and the CPU path both assume four. */
109
+ private _normalizeWeights;
110
+ /**
111
+ * How many splats are skinned, sorted and drawn this frame: the whole set,
112
+ * or the first `n` of {@link Splat.importanceOrder} after
113
+ * {@link Splat.setCountLod}.
114
+ */
115
+ get activeSplatCount(): number;
116
+ /** Whether a count LOD is currently reducing this splat. */
117
+ get countLodActive(): boolean;
118
+ /**
119
+ * Process only the `n` most important splats this frame, or the whole set
120
+ * with `null`.
121
+ *
122
+ * The dynamic counterpart of {@link SplatQuality.pruneRatio}: the prune
123
+ * decides once, at load, that some splats are not worth carrying at all,
124
+ * while this decides per frame how many of them are worth the work right
125
+ * now - which is the trade a character makes as it walks away from the
126
+ * camera. Nothing is freed, so it reverses the moment `n` goes back up.
127
+ *
128
+ * Which splats the first `n` are is {@link Splat.importanceOrder}'s answer:
129
+ * the strongest splat of every 4 cm cell, then the second of every cell, and
130
+ * so on, so the character thins evenly rather than losing a region.
131
+ *
132
+ * `WebGLRenderer` only, where it is worth the most: that backend skins every
133
+ * splat centre on the CPU each frame to feed its sort, and this cuts that
134
+ * pass, the sort and the draw together. Under `WebGPURenderer` all of that
135
+ * already happens in compute, and the call is accepted and ignored rather
136
+ * than silently changing which splats a GPU sort can see.
137
+ *
138
+ * {@link Character.setSplatCountLod} drives this from camera distance; call
139
+ * it directly for a policy of your own.
140
+ */
141
+ setCountLod(n: number | null): void;
142
+ /**
143
+ * Build {@link Splat.importanceOrder} now, if it does not exist yet.
144
+ *
145
+ * One pass over the splats plus a small sort inside each 4 cm cell - single
146
+ * digit milliseconds for a character, but milliseconds that would otherwise
147
+ * land on the frame where the LOD first engages, which is a frame where the
148
+ * camera is already moving. {@link Character.setSplatCountLod} calls this
149
+ * when the LOD is configured, so the cost lands there instead.
150
+ */
151
+ prepareCountLod(): void;
152
+ /**
153
+ * Each splat's place in {@link Splat.importanceOrder}, which is how the
154
+ * per-frame passes test membership of the active prefix. `null` until
155
+ * {@link Splat.prepareCountLod}.
156
+ * @internal
157
+ */
158
+ get importanceRank(): Uint32Array | null;
159
+ /**
160
+ * Splat indices ordered by importance for {@link Splat.setCountLod}. `null`
161
+ * until {@link Splat.prepareCountLod} or a count LOD has asked for one.
162
+ */
163
+ get importanceOrder(): Uint32Array | null;
164
+ /** Re-upload the bone index/weight textures after editing their data arrays. */
165
+ markBoneTexturesDirty(): void;
166
+ /** Keep CPU-skinned centres (sorting and ground probes) on the same bone
167
+ * mapping as the GPU shader. Called once when an armature binds this splat. */
168
+ syncMappedCpuInfluences(firstPaletteIdx: number): void;
169
+ get variantMaskEnabled(): boolean;
170
+ get nonRigidCovariance(): boolean;
171
+ /**
172
+ * Toggle the non-rigid covariance shader path. When on, the palette's
173
+ * per-axis scale stretches each gaussian's covariance; the palette must then
174
+ * actually carry that scale (see `SplatSkinning.covStretch`). Off is the
175
+ * rigid path, where palette scale is treated as unit. Switching recompiles
176
+ * the shader, so toggle it rarely.
177
+ */
178
+ setNonRigidCovariance(on: boolean): void;
179
+ /**
180
+ * Compensate each splat's opacity for the screen-space dilation applied to
181
+ * keep sub-pixel splats visible. Off by default; turn on for assets trained
182
+ * with anti-aliasing (e.g. Mip-Splatting).
183
+ */
184
+ get antiAlias(): boolean;
185
+ set antiAlias(on: boolean);
186
+ /**
187
+ * Blend every splat's colour toward `color` by `intensity` (0..1).
188
+ * `intensity = 0` restores the authored colour.
189
+ */
190
+ setTint(intensity: number, color?: {
191
+ r: number;
192
+ g: number;
193
+ b: number;
194
+ }): void;
195
+ /** Per-splat colour grade. `(1, 1)` is a true no-op. */
196
+ setGrade(contrast: number, saturation: number): void;
197
+ /**
198
+ * Assign each splat to a group and enable per-group visibility. All groups
199
+ * start visible; toggle them with {@link Splat.setVariantMask}.
200
+ *
201
+ * `groupIds` is indexed by splat, so it has to be built against
202
+ * {@link Splat.numSplats} as loaded - which {@link SplatQuality.minOpacity}
203
+ * makes smaller than the count in the file. An array authored offline
204
+ * against the file cannot simply be shortened: see the error this throws.
205
+ */
206
+ setSplatGroups(groupIds: Uint8Array, numGroups: number): void;
207
+ /** Per-group visibility, one entry per group: `>= 0.5` shows the group. */
208
+ setVariantMask(mask: ArrayLike<number>): void;
209
+ /** Whether the splat's world-space bounding sphere touches `frustum`. */
210
+ isInFrustum(frustum: Frustum): boolean;
211
+ /** World-space bounds of the splat in its current pose. */
212
+ getWorldAabb(out?: Box3): Box3;
213
+ /** World-space Y of the lowest corner of the current bounds. */
214
+ getWorldMinY(): number;
215
+ /**
216
+ * World-space Y of the splat's ground contact: the height below which only
217
+ * `quantile` of the splats lie. More robust than the bounds minimum, which a
218
+ * single stray gaussian can drag down.
219
+ */
220
+ getGroundWorldY(quantile?: number): number;
221
+ /**
222
+ * Flatten rest centres and influences into one interleaved buffer for the
223
+ * per-frame CPU skinning pass: `[x, y, z, w0, b0, w1, b1, w2, b2, w3, b3]`.
224
+ */
225
+ private _bakeInfluences;
226
+ /**
227
+ * Tell the splat its bone matrices changed. When the renderer sorts on the
228
+ * CPU the centres are re-skinned immediately; otherwise the work is deferred
229
+ * until something asks for them ({@link Splat.ensureCenters}).
230
+ */
231
+ updatePositions(boneTransforms: Mat4[]): void;
232
+ /** Bring {@link Splat.centers} up to date with the latest bone matrices. */
233
+ ensureCenters(): void;
234
+ /**
235
+ * Skin the splat centres on the CPU with the same matrices the GPU uses.
236
+ * This is the dominant per-character CPU cost, which is why it only runs
237
+ * when something needs the result.
238
+ */
239
+ private _skinCenters;
240
+ /** Force a re-sort on the next render. */
241
+ requestSort(): void;
242
+ /** True while an asynchronous sort is outstanding, i.e. the drawn order is stale or absent. */
243
+ get sortPending(): boolean;
244
+ /** Show or hide the splat without disturbing its bindings. */
245
+ setRenderingEnabled(enabled: boolean): void;
246
+ get renderingEnabled(): boolean;
247
+ get destroyed(): boolean;
248
+ /** Remove from the scene graph and release every GPU resource. */
249
+ destroy(): void;
250
+ /** @internal */
251
+ static _liveInstances: Set<Splat>;
252
+ }
@@ -0,0 +1,42 @@
1
+ import { DataTexture } from 'three';
2
+ import type { Mat4 } from '../math';
3
+ /** Transforms per texture row. The shader's palette lookup hard-codes this. */
4
+ export declare const PALETTE_ROW_SLOTS = 512;
5
+ /**
6
+ * The per-splat table of skinning matrices, stored as a float texture: three
7
+ * RGBA texels per transform, {@link PALETTE_ROW_SLOTS} transforms per row.
8
+ *
9
+ * Slot 0 is reserved for identity and is what unmapped influences point at.
10
+ */
11
+ declare class TransformPalette {
12
+ private _texture;
13
+ private _data;
14
+ private _dirty;
15
+ private _nextIdx;
16
+ private readonly _identity;
17
+ constructor(initialSize?: number);
18
+ /**
19
+ * The backing texture. Replaced when the palette grows, so read it each
20
+ * frame rather than holding on to it.
21
+ */
22
+ get texture(): DataTexture;
23
+ /**
24
+ * The backing array: twelve floats per slot, three row-major vec4 rows.
25
+ * Replaced when the palette grows, like {@link TransformPalette.texture}.
26
+ */
27
+ get data(): Float32Array;
28
+ /** Incremented every time {@link TransformPalette.upload} reports a change. */
29
+ version: number;
30
+ /** Slots the palette can hold before it has to grow. */
31
+ get capacity(): number;
32
+ private _realloc;
33
+ getTransform(index: number, transform: Mat4): void;
34
+ setTransform(index: number, transform: Mat4): void;
35
+ /** Flag the texture for re-upload if anything changed. Returns whether it did. */
36
+ upload(): boolean;
37
+ /** Reserve `num` consecutive slots and return the index of the first. */
38
+ alloc(num?: number): number;
39
+ free(num?: number): void;
40
+ dispose(): void;
41
+ }
42
+ export { TransformPalette };
@@ -0,0 +1,17 @@
1
+ import type { Mesh } from 'three';
2
+ import type { SplatEngine } from '../../engine';
3
+ import type { SplatBackend, SplatRenderImpl } from '../backend';
4
+ import type { Splat } from '../splat';
5
+ /**
6
+ * Splat rendering for `THREE.WebGLRenderer`: GLSL ES 3.00, WebGL2.
7
+ *
8
+ * Splats are skinned and projected in the vertex stage. The depth sort runs on
9
+ * the CPU - synchronously for skinned characters, in a worker for static
10
+ * environments - which is what makes this backend's cost scale with the
11
+ * number of animated characters.
12
+ *
13
+ * @group Engine
14
+ */
15
+ export declare class WebGLSplatBackend implements SplatBackend {
16
+ createImpl(host: Mesh, splat: Splat, engine: SplatEngine, asyncSort: boolean): SplatRenderImpl;
17
+ }
@@ -0,0 +1,2 @@
1
+ export declare const SPLAT_VERTEX_GLSL = "\nprecision highp float;\nprecision highp int;\nprecision highp usampler2D;\n\n// Quad corner in [-1, 1].\nin vec3 position;\n// Index of the splat this instance draws; instances arrive back-to-front.\nin uint splatOrder;\n\nuniform mat4 modelMatrix;\nuniform mat4 viewMatrix;\nuniform mat4 projectionMatrix;\n\nuniform highp usampler2D tTransformA;\nuniform highp sampler2D tTransformB;\nuniform highp sampler2D tColor;\nuniform int uTexWidth;\n\n// Drawing-buffer size in pixels.\nuniform vec2 uViewport;\nuniform float uMinPixelSize;\n// 1 / the alpha a splat's quad is allowed to end at. Carried as the reciprocal\n// so the historic 1/255 cutoff arrives as exactly 255.0 and the two\n// expressions below reduce, bit for bit, to what they were before the cutoff\n// was a setting at all. See SplatQuality.quadAlphaCutoff.\nuniform float uAlphaCutoffScale;\n// World rotation (xyzw) and scale of the splat object.\nuniform vec4 uModelRotation;\nuniform vec3 uModelScale;\n// rgb tint colour, a tint amount.\nuniform vec4 uTint;\n// x contrast, y saturation.\nuniform vec2 uGrade;\n// 1 when the render target expects linear colour, 0 when it is display-referred.\nuniform float uOutputLinear;\n\nout mediump vec2 gaussianUV;\nout mediump vec4 gaussianColor;\n\n\nuniform vec3 uSunColor;\nuniform float uSunIntensity;\nuniform vec3 uAmbientColor;\nuniform int uPointLightCount;\nuniform highp sampler2D uPointLightTex;\n\n// Baked splat colour is multiplied by this. With no lights it is exactly 1.\nvec3 computeLitFactor(vec3 worldCenter) {\n vec3 lit = uAmbientColor + uSunColor * uSunIntensity;\n for (int i = 0; i < uPointLightCount; i++) {\n vec4 posRange = texelFetch(uPointLightTex, ivec2(i, 0), 0);\n vec4 colorInt = texelFetch(uPointLightTex, ivec2(i, 1), 0);\n vec3 toLight = posRange.xyz - worldCenter;\n float dist = length(toLight);\n float range = posRange.w;\n if (dist >= range) continue;\n float t = dist / range;\n float atten = 1.0 - t * t;\n atten = atten * atten;\n lit += colorInt.rgb * colorInt.w * atten;\n }\n return lit;\n}\n\n\nuniform highp sampler2D transformPalette;\n\n#ifdef USE_BONE_BLENDING\nuniform highp sampler2D boneIndices;\nuniform highp sampler2D boneWeights;\n#endif\n\n#ifdef USE_VARIANT_MASK\nuniform highp sampler2D splatGroupIds;\nuniform highp sampler2D splatVariantMask;\n\nbool isSplatHiddenByVariantMask(ivec2 splatUV) {\n float gidF = texelFetch(splatGroupIds, splatUV, 0).r;\n int gid = int(gidF + 0.5);\n float bit = texelFetch(splatVariantMask, ivec2(gid, 0), 0).r;\n return bit < 0.5;\n}\n#endif\n\nbool isReasonablePaletteTransform(mat4 t) {\n mat3 lin = mat3(t);\n vec3 s = vec3(length(lin[0]), length(lin[1]), length(lin[2]));\n // Reject only when EVERY column is near-zero (a truly empty palette slot).\n // Linear-blend skinning of two very different bone rotations produces a\n // candy-wrapper matrix with one short column; that is still a valid\n // mid-rotation transform. Requiring all columns to pass would collapse\n // joint-spanning splats (notably the neck) back to the bind pose.\n return any(greaterThan(s, vec3(1e-4)));\n}\n\nmat4 readTransformFromPalette(uint paletteIndex) {\n if (paletteIndex == 0u) {\n return mat4(1.0);\n }\n\n int u = int(paletteIndex % 512u) * 3;\n int v = int(paletteIndex / 512u);\n\n mat4 t;\n t[0] = texelFetch(transformPalette, ivec2(u, v), 0);\n t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);\n t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);\n t[3] = vec4(0.0, 0.0, 0.0, 1.0);\n\n mat4 result = transpose(t);\n if (!isReasonablePaletteTransform(result)) {\n return mat4(1.0);\n }\n return result;\n}\n\nmat4 getSplatPaletteTransform(ivec2 splatUV) {\n #ifdef USE_BONE_BLENDING\n vec4 boneIdxVec = texelFetch(boneIndices, splatUV, 0);\n vec4 boneWeightVec = texelFetch(boneWeights, splatUV, 0);\n float totalWeight = dot(boneWeightVec, vec4(1.0));\n if (totalWeight > 0.001) {\n mat4 blended = mat4(0.0);\n for (int i = 0; i < 4; i++) {\n float w = boneWeightVec[i];\n if (w > 0.001) {\n uint idx = uint(boneIdxVec[i] + 0.5);\n blended += readTransformFromPalette(idx) * w;\n }\n }\n mat4 result = blended / totalWeight;\n if (isReasonablePaletteTransform(result)) {\n return result;\n }\n }\n #endif\n\n return mat4(1.0);\n}\n\nvec4 mat3ToQuatXYZW(mat3 R) {\n vec4 q;\n float trace = R[0][0] + R[1][1] + R[2][2];\n if (trace > 0.0) {\n float s = sqrt(trace + 1.0) * 2.0;\n q.w = 0.25 * s;\n q.x = (R[1][2] - R[2][1]) / s;\n q.y = (R[2][0] - R[0][2]) / s;\n q.z = (R[0][1] - R[1][0]) / s;\n } else if (R[0][0] > R[1][1] && R[0][0] > R[2][2]) {\n float s = sqrt(1.0 + R[0][0] - R[1][1] - R[2][2]) * 2.0;\n q.w = (R[1][2] - R[2][1]) / s;\n q.x = 0.25 * s;\n q.y = (R[1][0] + R[0][1]) / s;\n q.z = (R[2][0] + R[0][2]) / s;\n } else if (R[1][1] > R[2][2]) {\n float s = sqrt(1.0 + R[1][1] - R[0][0] - R[2][2]) * 2.0;\n q.w = (R[2][0] - R[0][2]) / s;\n q.x = (R[1][0] + R[0][1]) / s;\n q.y = 0.25 * s;\n q.z = (R[2][1] + R[1][2]) / s;\n } else {\n float s = sqrt(1.0 + R[2][2] - R[0][0] - R[1][1]) * 2.0;\n q.w = (R[0][1] - R[1][0]) / s;\n q.x = (R[2][0] + R[0][2]) / s;\n q.y = (R[2][1] + R[1][2]) / s;\n q.z = 0.25 * s;\n }\n return q;\n}\n\n\nconst vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0);\n\nmat3 quatToMat3(vec4 R) {\n vec4 R2 = R + R;\n float X = R2.x * R.w;\n vec4 Y = R2.y * R;\n vec4 Z = R2.z * R;\n float W = R2.w * R.w;\n return mat3(\n 1.0 - Z.z - W, Y.z + X, Y.w - Z.x,\n Y.z - X, 1.0 - Y.y - W, Z.w + Y.x,\n Y.w + Z.x, Z.w - Y.x, 1.0 - Y.y - Z.z\n );\n}\n\nvec4 quatMul(vec4 a, vec4 b) {\n return vec4(\n a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,\n a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,\n a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,\n a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z\n );\n}\n\nvoid computeCovariance(vec4 rotation, vec3 scale, out vec3 covA, out vec3 covB) {\n mat3 rot = quatToMat3(rotation);\n mat3 M = transpose(mat3(\n scale.x * rot[0],\n scale.y * rot[1],\n scale.z * rot[2]\n ));\n covA = vec3(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2]));\n covB = vec3(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2]));\n}\n\nvec3 sRGBToLinear(vec3 c) {\n return mix(\n pow(c * 0.9478672986 + vec3(0.0521327014), vec3(2.4)),\n c * 0.0773993808,\n vec3(lessThanEqual(c, vec3(0.04045)))\n );\n}\n\nvoid main(void) {\n ivec2 splatUV = ivec2(int(splatOrder % uint(uTexWidth)), int(splatOrder / uint(uTexWidth)));\n uvec4 tA = texelFetch(tTransformA, splatUV, 0);\n vec4 tB = texelFetch(tTransformB, splatUV, 0);\n\n vec3 modelCenter = uintBitsToFloat(tA.xyz);\n vec3 packedRot = vec3(unpackHalf2x16(tA.w), tB.w);\n // xyzw, with w recovered: quaternions are stored on the w >= 0 hemisphere.\n vec4 srcRotation = vec4(packedRot, sqrt(max(0.0, 1.0 - dot(packedRot, packedRot))));\n vec3 srcScale = tB.xyz;\n\n // ---- Skin, then lift to world space --------------------------------\n mat4 palette = getSplatPaletteTransform(splatUV);\n vec3 modelDeformed = (palette * vec4(modelCenter, 1.0)).xyz;\n vec3 worldCenter = (modelMatrix * vec4(modelDeformed, 1.0)).xyz;\n\n vec4 worldRotation = quatMul(uModelRotation, srcRotation);\n if (worldRotation.w < 0.0) worldRotation = -worldRotation;\n vec3 worldScale = uModelScale * srcScale;\n\n #ifdef USE_BONE_BLENDING\n {\n // The full world-space skinning rotation comes from modelMatrix * palette;\n // the palette's own scale is deliberately dropped (rigid skinning).\n mat3 lin = mat3(modelMatrix * palette);\n vec3 s = vec3(length(lin[0]), length(lin[1]), length(lin[2]));\n if (s.x > 1e-5 && s.y > 1e-5 && s.z > 1e-5) {\n mat3 rot = mat3(lin[0] / s.x, lin[1] / s.y, lin[2] / s.z);\n vec4 composed = normalize(quatMul(mat3ToQuatXYZW(rot), srcRotation));\n if (composed.w < 0.0) composed = -composed;\n worldRotation = composed;\n }\n #ifdef USE_NONRIGID_COV\n // Bone-length scaling: the palette keeps its per-axis scale and the\n // gaussian stretches with the bone.\n worldScale *= s;\n #endif\n }\n #endif\n\n #ifdef USE_VARIANT_MASK\n if (isSplatHiddenByVariantMask(splatUV)) {\n gl_Position = discardVec;\n return;\n }\n #endif\n\n // ---- Project the centre --------------------------------------------\n bool ortho = projectionMatrix[3][3] == 1.0;\n vec4 centerView = viewMatrix * vec4(worldCenter, 1.0);\n if (!ortho && centerView.z > 0.0) {\n gl_Position = discardVec;\n return;\n }\n vec4 centerProj = projectionMatrix * centerView;\n centerProj.z = clamp(centerProj.z, -abs(centerProj.w), abs(centerProj.w));\n\n // ---- Project the covariance to a screen-space ellipse ----------------\n vec3 covA, covB;\n computeCovariance(worldRotation.wxyz, worldScale, covA, covB);\n mat3 Vrk = mat3(\n covA.x, covA.y, covA.z,\n covA.y, covB.x, covB.y,\n covA.z, covB.y, covB.z\n );\n\n float focal = uViewport.x * projectionMatrix[0][0];\n vec3 v = centerView.xyz;\n vec3 vp = ortho ? vec3(0.0, 0.0, 1.0) : v;\n float J1 = focal / vp.z;\n vec2 J2 = -J1 / vp.z * vp.xy;\n mat3 J = mat3(\n J1, 0.0, J2.x,\n 0.0, J1, J2.y,\n 0.0, 0.0, 0.0\n );\n mat3 W = transpose(mat3(viewMatrix));\n mat3 T = W * J;\n mat3 cov = transpose(T) * Vrk * T;\n\n #ifdef USE_ANTIALIAS\n float detOrig = cov[0][0] * cov[1][1] - cov[0][1] * cov[0][1];\n float detBlur = (cov[0][0] + 0.3) * (cov[1][1] + 0.3) - cov[0][1] * cov[0][1];\n float aaFactor = sqrt(max(detOrig / detBlur, 0.0));\n #endif\n\n float diagonal1 = cov[0][0] + 0.3;\n float offDiagonal = cov[0][1];\n float diagonal2 = cov[1][1] + 0.3;\n float mid = 0.5 * (diagonal1 + diagonal2);\n float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal));\n float lambda1 = mid + radius;\n float lambda2 = max(mid - radius, 0.1);\n\n float vmin = min(1024.0, min(uViewport.x, uViewport.y));\n float l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin);\n float l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin);\n if (max(l1, l2) < uMinPixelSize) {\n gl_Position = discardVec;\n return;\n }\n\n // Cull splats whose ellipse lies wholly off-screen.\n vec2 c = centerProj.ww / uViewport;\n if (any(greaterThan(abs(centerProj.xy) - vec2(max(l1, l2)) * c, centerProj.ww))) {\n gl_Position = discardVec;\n return;\n }\n\n vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1));\n vec2 v1 = l1 * diagonalVector;\n vec2 v2 = l2 * vec2(diagonalVector.y, -diagonalVector.x);\n vec2 cornerUV = position.xy;\n vec2 offset = (cornerUV.x * v1 + cornerUV.y * v2) * c;\n\n // ---- Colour -----------------------------------------------------------\n vec4 clr = texelFetch(tColor, splatUV, 0);\n #ifdef USE_ANTIALIAS\n clr.a *= aaFactor;\n #endif\n\n clr.rgb *= computeLitFactor(worldCenter);\n clr.rgb = mix(clr.rgb, uTint.rgb, clamp(uTint.a, 0.0, 1.0));\n // Identity grade is a true no-op: skipping it keeps HDR splat colours unclamped.\n if (uGrade.x != 1.0 || uGrade.y != 1.0) {\n clr.rgb = (clr.rgb - 0.5) * uGrade.x + 0.5;\n float luma = dot(clr.rgb, vec3(0.2126, 0.7152, 0.0722));\n clr.rgb = mix(vec3(luma), clr.rgb, uGrade.y);\n clr.rgb = clamp(clr.rgb, 0.0, 1.0);\n }\n\n // A splat whose peak alpha is already under the cutoff cannot put a\n // fragment on screen: its own quad would be empty.\n float alphaOverCutoff = clr.a * uAlphaCutoffScale;\n if (alphaOverCutoff <= 1.0) {\n gl_Position = discardVec;\n return;\n }\n\n // Shrink the quad to where the gaussian is still above the cutoff: fewer\n // fragments. At a cutoff of 1/255 the pixels are identical, since below\n // that frag() discards anyway; above it, this is the trade the setting\n // exists to make.\n float clip = min(1.0, sqrt(log(alphaOverCutoff)) * 0.5);\n offset *= clip;\n\n gl_Position = centerProj + vec4(offset, 0.0, 0.0);\n gaussianUV = cornerUV * clip;\n\n vec3 rgb = max(clr.rgb, 0.0);\n if (uOutputLinear > 0.5) rgb = sRGBToLinear(rgb);\n gaussianColor = vec4(rgb, clr.a);\n}\n";
2
+ export declare const SPLAT_FRAGMENT_GLSL = "\nprecision highp float;\n\nin mediump vec2 gaussianUV;\nin mediump vec4 gaussianColor;\n\nlayout(location = 0) out highp vec4 fragColor;\n\nconst float EXP4 = exp(-4.0);\nconst float INV_EXP4 = 1.0 / (1.0 - EXP4);\n\n// exp(-4x) rescaled to hit exactly 0 at the quad edge (x = 1).\nfloat normExp(float x) {\n return (exp(x * -4.0) - EXP4) * INV_EXP4;\n}\n\nvoid main(void) {\n mediump float A = dot(gaussianUV, gaussianUV);\n if (A > 1.0) {\n discard;\n }\n mediump float alpha = normExp(A) * gaussianColor.a;\n if (alpha < 1.0 / 255.0) {\n discard;\n }\n // Premultiplied.\n fragColor = vec4(gaussianColor.rgb * alpha, alpha);\n}\n";
@@ -0,0 +1,54 @@
1
+ import { InstancedBufferGeometry, RawShaderMaterial } from 'three';
2
+ import type { Camera, Mesh } from 'three';
3
+ import type { SplatEngine } from '../../engine';
4
+ import type { SplatRenderImpl } from '../backend';
5
+ import type { Splat } from '../splat';
6
+ /**
7
+ * How one {@link Splat} is drawn under `WebGLRenderer`: one instanced quad per
8
+ * gaussian, drawn back-to-front with premultiplied alpha.
9
+ *
10
+ * The instance attribute holds splat indices in draw order, so sorting only
11
+ * ever rewrites that one buffer; all splat data stays put in textures.
12
+ *
13
+ * @internal
14
+ */
15
+ export declare class WebGLSplatImpl implements SplatRenderImpl {
16
+ readonly geometry: InstancedBufferGeometry;
17
+ readonly material: RawShaderMaterial;
18
+ /** The depth sort runs on the CPU, so it needs CPU-side centres. */
19
+ readonly needsCpuCenters = true;
20
+ private readonly _host;
21
+ private readonly _splat;
22
+ private readonly _engine;
23
+ private readonly _order;
24
+ private readonly _syncSorter;
25
+ private readonly _asyncSorter;
26
+ private _sortedVersion;
27
+ private _sortRequested;
28
+ private _lastDx;
29
+ private _lastDy;
30
+ private _lastDz;
31
+ /** The importance ordering the worker already holds, if any. */
32
+ private _sentSubset;
33
+ /**
34
+ * @param asyncSort - Sort in a worker and keep drawing the previous order
35
+ * meanwhile. Right for large static splats; wrong for skinned ones, whose
36
+ * order would lag the pose.
37
+ */
38
+ constructor(host: Mesh, splat: Splat, engine: SplatEngine, asyncSort: boolean);
39
+ /** True while a worker sort is in flight (always false for synchronous sorting). */
40
+ get sortPending(): boolean;
41
+ /** Force a re-sort on the next render even if the view direction is unchanged. */
42
+ requestSort(): void;
43
+ /** Rebuild the shader variant after a feature toggle on the splat. */
44
+ syncDefines(): void;
45
+ /** The bone textures are the splat's own; flagging them is all there is to do. */
46
+ syncBoneData(): void;
47
+ /** Group ids live in the splat's own texture, which it flags itself. */
48
+ syncVariantGroups(): void;
49
+ /** Refresh the culling sphere from the splat's current local bounds. */
50
+ syncBounds(): void;
51
+ beforeRender(renderer: unknown, camera: Camera): void;
52
+ private _sortFor;
53
+ dispose(): void;
54
+ }