@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.
- package/LICENSE +21 -0
- package/README.md +450 -2
- package/dist/chunks/chunk-WN5WBKM4.js +8606 -0
- package/dist/chunks/chunk-WN5WBKM4.js.map +7 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +7 -0
- package/dist/types/anim/animation-conversion.d.ts +25 -0
- package/dist/types/anim/bind-pose.d.ts +15 -0
- package/dist/types/anim/keyframe-splines.d.ts +59 -0
- package/dist/types/anim/rest-relative.d.ts +32 -0
- package/dist/types/anim/spline.d.ts +14 -0
- package/dist/types/animation.d.ts +216 -0
- package/dist/types/armature.d.ts +524 -0
- package/dist/types/character.d.ts +582 -0
- package/dist/types/engine.d.ts +166 -0
- package/dist/types/environment.d.ts +53 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/lighting/color.d.ts +13 -0
- package/dist/types/lighting/point-light.d.ts +33 -0
- package/dist/types/lighting/sky-light.d.ts +22 -0
- package/dist/types/lighting/sun-light.d.ts +34 -0
- package/dist/types/loader/animation/animation-glb.d.ts +117 -0
- package/dist/types/loader/character/index.d.ts +33 -0
- package/dist/types/loader/character/legacy/index.d.ts +34 -0
- package/dist/types/loader/character/legacy/interleaved-splat.d.ts +101 -0
- package/dist/types/loader/character/legacy/planar-splat.d.ts +139 -0
- package/dist/types/loader/character/load.d.ts +33 -0
- package/dist/types/loader/character/types.d.ts +129 -0
- package/dist/types/loader/character/v1/decode.d.ts +32 -0
- package/dist/types/loader/environment/ply.d.ts +16 -0
- package/dist/types/loader/shared/bin-cache.d.ts +19 -0
- package/dist/types/loader/skeleton/index.d.ts +19 -0
- package/dist/types/loader/skeleton/skeleton-data-441.d.ts +5 -0
- package/dist/types/loader/skeleton/skeleton-data.d.ts +6 -0
- package/dist/types/math.d.ts +82 -0
- package/dist/types/skeletal/animation-player.d.ts +185 -0
- package/dist/types/skeletal/bone-mask.d.ts +27 -0
- package/dist/types/skeletal/bone-stretch.d.ts +65 -0
- package/dist/types/skeletal/pose.d.ts +27 -0
- package/dist/types/skeletal/skeleton.d.ts +57 -0
- package/dist/types/skeletal/splat-skinning.d.ts +69 -0
- package/dist/types/splat/backend.d.ts +57 -0
- package/dist/types/splat/bone-binding.d.ts +38 -0
- package/dist/types/splat/depth-sort.d.ts +76 -0
- package/dist/types/splat/importance.d.ts +60 -0
- package/dist/types/splat/packed-splats.d.ts +36 -0
- package/dist/types/splat/prune.d.ts +60 -0
- package/dist/types/splat/quality.d.ts +175 -0
- package/dist/types/splat/splat-mesh.d.ts +71 -0
- package/dist/types/splat/splat.d.ts +252 -0
- package/dist/types/splat/transform-palette.d.ts +42 -0
- package/dist/types/splat/webgl/backend.d.ts +17 -0
- package/dist/types/splat/webgl/shaders.d.ts +2 -0
- package/dist/types/splat/webgl/splat-mesh.d.ts +54 -0
- package/dist/types/splat/webgpu/backend.d.ts +25 -0
- package/dist/types/splat/webgpu/lighting.d.ts +34 -0
- package/dist/types/splat/webgpu/radix-sort.d.ts +46 -0
- package/dist/types/splat/webgpu/splat-mesh.d.ts +73 -0
- package/dist/types/three/loaders.d.ts +77 -0
- package/dist/types/three/splat-character.d.ts +220 -0
- package/dist/types/types.d.ts +30 -0
- package/dist/types/webgpu.d.ts +5 -0
- package/dist/webgpu.js +843 -0
- package/dist/webgpu.js.map +7 -0
- 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
|
+
}
|