@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,27 @@
1
+ /**
2
+ * Bone-mask utilities for layered animation. A {@link BoneMask} selects a
3
+ * subset of an armature's bones by index; the {@link Armature} layer system
4
+ * uses it to gate an overlay animation to (e.g.) just the upper body while a
5
+ * base full-body clip drives the rest.
6
+ *
7
+ * Built-in regions ({@link UPPER_BODY_BONE_NAMES} / {@link LOWER_BODY_BONE_NAMES})
8
+ * follow the canonical 86-bone rig naming. The split bone is the pelvis:
9
+ * - UPPER covers everything from `spine_01` up (spine 01-05, neck, head,
10
+ * clavicles, arms, hands, all fingers) so the overlay fully replaces
11
+ * the torso without leak from the base's spine rotation.
12
+ * - LOWER covers `pelvis` plus the leg chain (thighs, calves, calf
13
+ * twists, feet, ball, all toes). Including pelvis means an overlay's
14
+ * pelvis Y motion drives the rig and routes through `rootMotionMode`,
15
+ * so the lower-body overlay owns root motion when active.
16
+ *
17
+ * @group Characters & Animation
18
+ */
19
+ export declare const UPPER_BODY_BONE_NAMES: readonly ["spine_01", "spine_02", "spine_03", "spine_04", "spine_05", "neck_01", "neck_02", "head", "clavicle_l", "clavicle_r", "upperarm_l", "upperarm_r", "lowerarm_l", "lowerarm_r", "hand_l", "hand_r", "thumb_01_l", "thumb_02_l", "thumb_03_l", "index_metacarpal_l", "index_01_l", "index_02_l", "index_03_l", "middle_metacarpal_l", "middle_01_l", "middle_02_l", "middle_03_l", "ring_metacarpal_l", "ring_01_l", "ring_02_l", "ring_03_l", "pinky_metacarpal_l", "pinky_01_l", "pinky_02_l", "pinky_03_l", "thumb_01_r", "thumb_02_r", "thumb_03_r", "index_metacarpal_r", "index_01_r", "index_02_r", "index_03_r", "middle_metacarpal_r", "middle_01_r", "middle_02_r", "middle_03_r", "ring_metacarpal_r", "ring_01_r", "ring_02_r", "ring_03_r", "pinky_metacarpal_r", "pinky_01_r", "pinky_02_r", "pinky_03_r"];
20
+ export declare const LOWER_BODY_BONE_NAMES: readonly ["pelvis", "thigh_l", "thigh_r", "calf_l", "calf_r", "calf_twist_01_l", "calf_twist_01_r", "foot_l", "foot_r", "ball_l", "ball_r", "bigtoe_01_l", "bigtoe_02_l", "indextoe_01_l", "indextoe_02_l", "middletoe_01_l", "middletoe_02_l", "ringtoe_01_l", "ringtoe_02_l", "littletoe_01_l", "littletoe_02_l", "bigtoe_01_r", "bigtoe_02_r", "indextoe_01_r", "indextoe_02_r", "middletoe_01_r", "middletoe_02_r", "ringtoe_01_r", "ringtoe_02_r", "littletoe_01_r", "littletoe_02_r"];
21
+ export interface BoneMask {
22
+ name: string;
23
+ boneIndices: Int32Array;
24
+ }
25
+ export declare function resolveBoneIndices(allBoneNames: readonly string[], maskNames: readonly string[]): Int32Array;
26
+ export type BodyRegion = 'upperBody' | 'lowerBody';
27
+ export declare function makeRegionMask(region: BodyRegion, allBoneNames: readonly string[]): BoneMask;
@@ -0,0 +1,65 @@
1
+ import { Mat4 } from '../math';
2
+ import type { Skeleton } from './skeleton';
3
+ /**
4
+ * Per-axis scale for a single bone, in a frame aligned to the bone:
5
+ * - `along` - scale down the bone's own axis (lengthen / shorten),
6
+ * - `width` / `thickness` - scale across the bone (the two cross-section axes;
7
+ * their world orientation is bone-dependent, so treat them as "the two
8
+ * perpendicular directions" rather than a fixed front/side).
9
+ *
10
+ * Each defaults to `1`. A bare `number` is shorthand for a uniform scale on all
11
+ * three axes.
12
+ *
13
+ * @group Characters & Animation
14
+ */
15
+ export interface BoneScale {
16
+ along: number;
17
+ width: number;
18
+ thickness: number;
19
+ }
20
+ /** A bone scale, or a number meaning uniform scale on all three axes. */
21
+ export type BoneScaleInput = Partial<BoneScale> | number;
22
+ /**
23
+ * Per-bone data that realizes a set of bone scales. Produced by
24
+ * {@link computeBoneStretch}, consumed by {@link Skeleton} (lengthening) and
25
+ * {@link SplatSkinning} (flesh/covariance stretch).
26
+ *
27
+ * @group Characters & Animation
28
+ */
29
+ export interface BoneStretch {
30
+ /**
31
+ * Per-bone multiplier on the local rest translation. A bone's local
32
+ * translation is its offset from its parent (the parent segment's vector),
33
+ * so scaling it lengthens that segment by the parent's `along` factor.
34
+ * Translation-only → never inflates covariance, never compounds down a
35
+ * chain. Indexed by bone; `1` = unchanged.
36
+ */
37
+ lengthScale: Float32Array;
38
+ /**
39
+ * Per-bone local stretch matrix (`F·diag(along,width,thickness)·Fᵀ` in the
40
+ * bone-aligned frame `F`), folded into the skinning palette so the bone's
41
+ * Gaussians scale on all three axes and their covariances follow. Identity
42
+ * for unscaled bones. Indexed by bone.
43
+ */
44
+ covStretch: Mat4[];
45
+ /** True when at least one bone is actually scaled. */
46
+ any: boolean;
47
+ }
48
+ /**
49
+ * Translate a map of per-bone scales into the {@link BoneStretch} arrays.
50
+ * `factors[boneName]` scales that bone's flesh + segment (e.g.
51
+ * `{ thigh_l: { along: 1.3 }, calf_l: { along: 1.3 } }` → a 1.3× longer left
52
+ * leg; `{ upperarm_l: { width: 1.4, thickness: 1.4 } }` → a beefier arm).
53
+ * Bones not listed stay at `1`.
54
+ *
55
+ * Correct for arbitrary bone sets (limbs, spine, …) with no boundary
56
+ * compensation: lengthening lives in per-segment translations (no cumulative
57
+ * scale), and the flesh scale is per-bone in the palette (non-propagating).
58
+ * Each bone scales in ITS OWN frame, so bones at any orientation scale
59
+ * correctly.
60
+ *
61
+ * @param skeleton Rig to read the rest pose / hierarchy from.
62
+ * @param boneNames Bone names in index order (from the armature).
63
+ * @param factors Per-bone scales keyed by bone name.
64
+ */
65
+ export declare function computeBoneStretch(skeleton: Skeleton, boneNames: readonly string[], factors: Record<string, BoneScaleInput>): BoneStretch;
@@ -0,0 +1,27 @@
1
+ import { Quat, Vec3 } from '../math';
2
+ /**
3
+ * Per-bone local TRS for a whole rig. The seam between
4
+ * {@link AnimationPlayer} (which writes pose values) and
5
+ * {@link Skeleton} (which composes them into world transforms).
6
+ *
7
+ * Three parallel arrays of length `numBones`. Values live in armature-local
8
+ * bone space - absolute, not rest-relative. Allocation happens once;
9
+ * AnimationPlayer mutates entries in place each frame.
10
+ *
11
+ * @group Characters & Animation
12
+ */
13
+ export declare class Pose {
14
+ readonly positions: Vec3[];
15
+ readonly rotations: Quat[];
16
+ readonly scales: Vec3[];
17
+ constructor(numBones: number);
18
+ get numBones(): number;
19
+ /** Copy every bone from `other` into this pose. */
20
+ copyFrom(other: Pose): void;
21
+ /**
22
+ * Reset every bone to the bind-pose values held in `bindT` (flat
23
+ * `[x,y,z]*numBones`) and `bindR` (flat `[x,y,z,w]*numBones`). Scales reset
24
+ * to unit.
25
+ */
26
+ setToBind(bindT: Float32Array, bindR: Float32Array): void;
27
+ }
@@ -0,0 +1,57 @@
1
+ import { Mat4 } from '../math';
2
+ import type { RestPose } from '../anim/rest-relative';
3
+ import type { ArmatureData } from '../types';
4
+ import type { Pose } from './pose';
5
+ /**
6
+ * Rig data and skeletal math for one character. Owns the bone hierarchy, the
7
+ * rest pose, and the (lazy) inverse-bind pose. Composes per-bone local
8
+ * transforms into world-space matrices.
9
+ *
10
+ * Pure data + math - no three.js object, no `SplatEngine`, no `Splat`. Safe to
11
+ * construct in tests with a fixture rig.
12
+ *
13
+ * @group Characters & Animation
14
+ */
15
+ export declare class Skeleton {
16
+ readonly numBones: number;
17
+ /** Parent bone index per bone. `-1` for root bones. */
18
+ readonly parents: Int32Array;
19
+ /** Flat bind-pose translations: `[x,y,z]*numBones`. */
20
+ readonly bindT: Float32Array;
21
+ /** Flat bind-pose rotations (quaternions): `[x,y,z,w]*numBones`. */
22
+ readonly bindR: Float32Array;
23
+ /** Rest pose as parallel `Vec3`/`Quat` arrays. */
24
+ readonly restPose: RestPose;
25
+ /**
26
+ * Optional per-bone multiplier on the local rest translation, applied during
27
+ * FK. A bone's local translation is its offset from its parent, so scaling
28
+ * it lengthens that segment - this is the lengthening half of bone-length
29
+ * scaling (see {@link computeBoneStretch}). It deliberately affects only the
30
+ * translation, not the bone's orientation/scale, so it never inflates
31
+ * Gaussian covariance and never compounds down a chain. `null` = unscaled
32
+ * (the normal path). The bind/inverse-bind are intentionally NOT rescaled,
33
+ * so the palette carries the standard→stretched delta.
34
+ */
35
+ lengthScale: Float32Array | null;
36
+ private readonly _children;
37
+ private readonly _rootBones;
38
+ private _inverseBindPose;
39
+ private readonly _tmpLocalMat;
40
+ private readonly _tmpPos;
41
+ private readonly _kfStack;
42
+ constructor(armatureData: ArmatureData);
43
+ /**
44
+ * World-space inverse-bind pose, one matrix per bone. Lazy - bind pose is
45
+ * only needed once splat skinning starts pushing transforms.
46
+ */
47
+ get inverseBindPose(): Mat4[];
48
+ /**
49
+ * Compose local-space `pose` into world-space matrices, prefixed by
50
+ * `entityWorldTransform`. Walks the hierarchy parent-first so child world
51
+ * transforms read the freshly-written parent.
52
+ *
53
+ * `out` is reused - entries are created lazily if missing, but the caller
54
+ * owns the buffer (no per-frame allocation).
55
+ */
56
+ applyLocal(pose: Pose, entityWorldTransform: Mat4, out: Mat4[]): void;
57
+ }
@@ -0,0 +1,69 @@
1
+ import type { Frustum } from 'three';
2
+ import { Mat4 } from '../math';
3
+ import type { Splat } from '../splat/splat';
4
+ import type { SkeletonLibrary } from '../types';
5
+ import type { Skeleton } from './skeleton';
6
+ /**
7
+ * Per-character splat skinning coordinator. Owns the set of {@link Splat}s
8
+ * driven by an armature, their palette-slot maps, and the GPU push that
9
+ * uploads bone matrices each frame.
10
+ *
11
+ * Also owns the frustum-visibility gate: the armature can skip the expensive
12
+ * pose-eval + skinning path when no linked splat is on-screen.
13
+ *
14
+ * @group Characters & Animation
15
+ */
16
+ export declare class SplatSkinning {
17
+ /** Splats currently driven by this skinning unit. */
18
+ readonly linkedSplats: Set<Splat>;
19
+ /**
20
+ * Optional per-bone local stretch matrices (the covariance half of
21
+ * bone-length scaling - see {@link computeBoneStretch}). When set, each
22
+ * bone's palette entry gets this stretch folded in, the per-bone scale is
23
+ * NOT normalized out of the palette, and linked splats render with the
24
+ * `USE_NONRIGID_COV` shader path so Gaussian covariances follow the stretch.
25
+ * `null` = rigid skinning (the normal path). Set via {@link setCovStretch}.
26
+ */
27
+ covStretch: Mat4[] | null;
28
+ private readonly _skeleton;
29
+ private readonly _skeletonLibrary;
30
+ private readonly _bonePaletteFirstIdx;
31
+ private readonly _boneIndexLists;
32
+ private readonly _finalBoneTransforms;
33
+ private readonly _armatureWorldToLocal;
34
+ constructor(skeleton: Skeleton, skeletonLibrary: SkeletonLibrary | undefined);
35
+ link(splat: Splat): void;
36
+ unlink(splat: Splat): void;
37
+ /**
38
+ * Set (or clear with `null`) the per-bone covariance-stretch matrices for
39
+ * bone-length scaling. Also flips the `USE_NONRIGID_COV` shader path on every
40
+ * linked splat so covariances follow the stretch. Pass `null` to return to
41
+ * rigid skinning.
42
+ */
43
+ setCovStretch(mats: Mat4[] | null): void;
44
+ /**
45
+ * Returns `true` when at least one linked splat's bounding sphere intersects
46
+ * the camera frustum. Used by the armature to short-circuit the per-frame
47
+ * skinning math for off-screen characters.
48
+ *
49
+ * When `frustum` is missing (e.g. before the first `engine.update()`) returns `true` -
50
+ * the conservative choice is to do the extra work rather than freeze a
51
+ * character's pose.
52
+ */
53
+ anyVisible(frustum: Frustum | undefined): boolean;
54
+ /**
55
+ * Compose `worldTransforms` (one per bone) with the pivot's inverse world
56
+ * transform and the inverse-bind pose, then upload the resulting palette to
57
+ * every linked splat. `worldTransforms[i]` is the i-th bone's matrix in
58
+ * world space, as produced by {@link Skeleton.applyLocal}.
59
+ *
60
+ * Strips scale from the per-bone linear part before composing with the
61
+ * inverse bind. Without this, any net scale (from `splat.object3D` world scale
62
+ * or animated bone scale) inflates Gaussian sigmas on the GPU via the
63
+ * palette's column lengths - visible as radial streaks on hands/fists, and
64
+ * desyncs CPU-computed centers from GPU-rendered positions.
65
+ */
66
+ pushTransforms(worldTransforms: Mat4[], entityWorldTransform: Mat4, skipPositions?: boolean): void;
67
+ /** Request a re-sort on every linked splat (e.g. after a transform change). */
68
+ requestSortAll(): void;
69
+ }
@@ -0,0 +1,57 @@
1
+ import type { BufferGeometry, Camera, Material, Mesh } from 'three';
2
+ import type { SplatEngine } from '../engine';
3
+ import type { Splat } from './splat';
4
+ /**
5
+ * The renderer-specific half of a {@link SplatMesh}: the geometry and material
6
+ * one kind of three.js renderer needs, plus the per-frame work to keep them
7
+ * current.
8
+ *
9
+ * Everything a splat is - its packed data, weights, palette, tint - lives on
10
+ * the `Splat`. An implementation only turns that into something one renderer
11
+ * can draw, and owns whatever that takes (sort state, GPU buffers).
12
+ *
13
+ * @internal
14
+ */
15
+ export interface SplatRenderImpl {
16
+ readonly geometry: BufferGeometry;
17
+ readonly material: Material;
18
+ /**
19
+ * Whether the splat's centres must be re-skinned on the CPU every frame.
20
+ * True when the depth sort runs on the CPU; false when the GPU skins for its
21
+ * own sort, in which case that pass - the dominant per-character CPU cost -
22
+ * is skipped entirely.
23
+ */
24
+ readonly needsCpuCenters: boolean;
25
+ /** True while the drawn order is stale or absent because a sort is outstanding. */
26
+ readonly sortPending: boolean;
27
+ /** Force a re-sort on the next render even if nothing appears to have changed. */
28
+ requestSort(): void;
29
+ /** Rebuild the shader variant after a feature toggle on the splat. */
30
+ syncDefines(): void;
31
+ /** Refresh culling bounds from the splat's current local bounds. */
32
+ syncBounds(): void;
33
+ /** Re-upload bone indices/weights after the splat's arrays were edited. */
34
+ syncBoneData(): void;
35
+ /** Re-upload per-splat group ids after {@link Splat.setSplatGroups}. */
36
+ syncVariantGroups(): void;
37
+ /** Called from the host mesh's `onBeforeRender`, once per draw. */
38
+ beforeRender(renderer: unknown, camera: Camera): void;
39
+ /** Release GPU resources. The splat's own textures are not this object's to free. */
40
+ dispose(): void;
41
+ }
42
+ /**
43
+ * Creates {@link SplatRenderImpl}s for one kind of three.js renderer.
44
+ *
45
+ * You rarely need to name one: a {@link SplatMesh} picks the backend that
46
+ * matches whichever renderer first draws it.
47
+ *
48
+ * @group Engine
49
+ */
50
+ export interface SplatBackend {
51
+ /**
52
+ * @param host - The mesh in the scene graph the implementation draws through.
53
+ * @param asyncSort - The splat is static, so a sort that lags the camera by
54
+ * a frame or two is acceptable. Backends that sort on the GPU ignore this.
55
+ */
56
+ createImpl(host: Mesh, splat: Splat, engine: SplatEngine, asyncSort: boolean): SplatRenderImpl;
57
+ }
@@ -0,0 +1,38 @@
1
+ import type { SkeletonLibrary } from '../types';
2
+ import type { Splat } from './splat';
3
+ /**
4
+ * Build the source→target bone remap when the splat's `.bin` bundle was
5
+ * authored against a wider skeleton than the armature uses (e.g. a 1185-bone
6
+ * source on the 86-bone std-male rig, or a 441-bone full body+facial source
7
+ * on the 86-bone body rig).
8
+ *
9
+ * Returns `null` when no remap is needed (source and target bone counts
10
+ * match) or when the skeleton library doesn't carry enough naming metadata to
11
+ * resolve the mapping. Unmapped bones in the result are `-1`, except facial
12
+ * bones (`FACIAL_*`), which fall back to the `head` bone so the splats track
13
+ * head motion rather than snapping to root.
14
+ */
15
+ export declare function buildSourceBoneRemap(splat: Splat, numBones: number, skeletonLibrary: SkeletonLibrary | undefined): Int32Array | null;
16
+ /**
17
+ * Allocate the splat's slice of the shared transform palette and rewrite its
18
+ * bone-index texture so each splat's K influences point at the palette slots
19
+ * the armature will write into.
20
+ *
21
+ * Splats with no resolvable target bone (typically facial influences on a
22
+ * body-only rig that can't fall back to `head`) get weight 0 - they stay at
23
+ * rest instead of snapping to bone 0.
24
+ *
25
+ * Returns the first palette index allocated for this splat's bone block.
26
+ * Palette slot for bone `i` is always `firstIdx + i`.
27
+ */
28
+ export declare function initializeSplatBoneMapping(splat: Splat, numBones: number, skeletonLibrary: SkeletonLibrary | undefined): number;
29
+ /**
30
+ * Walk the splat's per-influence bone-index buffer and return the sorted list
31
+ * of distinct bone indices it actually references. Used to skip palette writes
32
+ * for bones no splat influences (e.g. a 441-bone rig where ~half the bones are
33
+ * facial and the splat has no facial weights).
34
+ *
35
+ * Returns an empty `Int32Array` when the splat has no weights data - callers
36
+ * treat that as "fall back to walking every bone."
37
+ */
38
+ export declare function buildSplatBoneIndexList(splat: Splat, numBones: number, firstPaletteIdx: number): Int32Array;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Back-to-front depth ordering for splats: a counting sort over quantised
3
+ * distance along the view direction.
4
+ *
5
+ * The same routine runs in two places. Skinned characters are small and move
6
+ * every frame, so they sort synchronously on the main thread and never show a
7
+ * stale order. Environments are large and only need a new order when the camera
8
+ * moves, so they sort in a worker ({@link AsyncDepthSorter}) and keep drawing
9
+ * the previous order until the new one lands.
10
+ */
11
+ /**
12
+ * Range of `dot(p, dir)` over an axis-aligned box - the projection of the box
13
+ * onto the view direction.
14
+ */
15
+ export declare function projectBoundsOnDirection(min: ArrayLike<number>, max: ArrayLike<number>, dx: number, dy: number, dz: number): {
16
+ depthMin: number;
17
+ depthMax: number;
18
+ };
19
+ /** Synchronous sorter with its own scratch buffers. One per splat. */
20
+ export declare class DepthSorter {
21
+ private readonly _scratch;
22
+ /**
23
+ * @param count - How many splats take part. With `subset`, the length of
24
+ * the prefix of it to use; without, the splats are `[0, count)`.
25
+ * @param subset - Splat indices to sort, for a count LOD. See
26
+ * {@link Splat.setCountLod}.
27
+ */
28
+ sort(centers: Float32Array, count: number, dx: number, dy: number, dz: number, depthMin: number, depthMax: number, order: Uint32Array, subset?: Uint32Array | null): void;
29
+ }
30
+ export interface SortRequest {
31
+ dx: number;
32
+ dy: number;
33
+ dz: number;
34
+ depthMin: number;
35
+ depthMax: number;
36
+ /**
37
+ * How many splats to order, for a count LOD. Defaults to all of them; with
38
+ * a subset set, the length of the prefix of it to use.
39
+ */
40
+ count?: number;
41
+ }
42
+ /**
43
+ * Worker-backed sorter for static splats.
44
+ *
45
+ * At most one sort is in flight. A request made while the worker is busy
46
+ * replaces any earlier pending one, so a fast-moving camera never queues work
47
+ * it no longer wants. A single order buffer is transferred to the worker and
48
+ * back, never copied.
49
+ *
50
+ * The worker is built from a Blob of this module's own sort routine, so there
51
+ * is no second file for a bundler to lose.
52
+ */
53
+ export declare class AsyncDepthSorter {
54
+ private _worker;
55
+ private _pending;
56
+ /** The order buffer, when it is on this side of the worker boundary. */
57
+ private _order;
58
+ /**
59
+ * @param centers - Flat `[x,y,z] * count`. Copied into the worker once.
60
+ * @param onSorted - Called with each finished order and how many of its
61
+ * entries are current. The array is only valid for the duration of the call
62
+ * - it goes back to the worker on the next request - so copy out of it
63
+ * before returning.
64
+ */
65
+ constructor(centers: Float32Array, count: number, onSorted: (order: Uint32Array, count: number) => void);
66
+ /**
67
+ * Give the worker the importance ordering a count LOD selects from. Sent
68
+ * once and kept there, like the centres; after this a request carries only
69
+ * how many of its entries to use.
70
+ */
71
+ setSubset(subset: Uint32Array): void;
72
+ /** True while a sort is running in the worker. */
73
+ get busy(): boolean;
74
+ request(req: SortRequest): void;
75
+ dispose(): void;
76
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Ranking splats by how much they contribute, and the spatial bucketing both
3
+ * users of that ranking share.
4
+ *
5
+ * Two features are built on it. {@link pruneSplats} throws the weakest splats
6
+ * away at load; the count LOD ({@link Splat.setCountLod}) keeps them all and
7
+ * processes a prefix of the ranking that shrinks with distance. The ranking
8
+ * has to be the same one in both cases, which is why it lives here rather than
9
+ * in either of them.
10
+ */
11
+ import type { PackedSplats } from './packed-splats';
12
+ /**
13
+ * Group indices by spatial cell without a comparison sort: hash the cells to
14
+ * dense ids, count, prefix-sum, scatter.
15
+ *
16
+ * O(n) with one map probe per splat, against the O(n log n) a sort over a
17
+ * composite key would cost - which measured 11-16 ms of a ~40 ms character
18
+ * load in the Unity build this is ported from.
19
+ */
20
+ export declare function bucketByCell(cellKey: Float64Array, n: number): {
21
+ order: Uint32Array;
22
+ start: Uint32Array;
23
+ cells: number;
24
+ };
25
+ /**
26
+ * One splat's cell key on a `1 / voxelSize` grid whose origin is `min`.
27
+ *
28
+ * The one definition of the grid. Both users of it have to agree - a splat
29
+ * pruned by one rule and ranked by another would be thinned twice in the same
30
+ * place - and they walk different layouts, so what they share is this rather
31
+ * than a loop.
32
+ */
33
+ export declare function cellKeyOf(x: number, y: number, z: number, minX: number, minY: number, minZ: number, invVoxel: number): number;
34
+ /**
35
+ * Cell keys for `centers`, with the grid origin at the actual minimum of the
36
+ * positions rather than at any declared bounds, which can carry a margin the
37
+ * grid should not inherit.
38
+ */
39
+ export declare function cellKeysFor(centers: Float32Array, count: number, voxelSize: number): Float64Array;
40
+ /**
41
+ * Splat indices ordered so that any prefix of the result is a good-looking
42
+ * subset of the whole: the most important splat of every cell first, then the
43
+ * second of every cell, and so on.
44
+ *
45
+ * Importance within a cell is opacity times the two largest gaussian axes -
46
+ * the area the splat actually paints, which the Unity build measured as
47
+ * ranking 0.80 with the ink each splat deposits, at no cost to compute.
48
+ *
49
+ * The round-robin across cells is what makes this usable as a prefix, and it
50
+ * is the same decision the load-time prune makes for the same reason: ranked
51
+ * globally, a prefix would be every strong splat on the jacket and none of the
52
+ * many individually-faint ones that make up a face, so the face would be the
53
+ * first thing to go. Taken a rank at a time across cells, every region thins
54
+ * by the same proportion at every prefix length, so a character at 30% looks
55
+ * like a sparser version of itself rather than a damaged one.
56
+ *
57
+ * Costs one pass over the splats plus a small sort inside each cell, and is
58
+ * built once, the first time a count LOD asks for it.
59
+ */
60
+ export declare function buildImportanceOrder(packed: PackedSplats, voxelSize: number): Uint32Array;
@@ -0,0 +1,36 @@
1
+ import type { SplatAttributes } from '../loader/character/types';
2
+ /**
3
+ * Splat attributes packed into the three texel streams the shaders read.
4
+ *
5
+ * Renderer-agnostic on purpose: these are plain typed arrays laid out as
6
+ * `width x height` RGBA texels, so the WebGL and WebGPU backends wrap the same
7
+ * bytes in their own texture objects.
8
+ *
9
+ * The layout is 32 bytes per splat:
10
+ *
11
+ * - `color` RGBA16F - `rgb` display-referred colour, `a` opacity (post-sigmoid).
12
+ * - `transformA` RGBA32UI - `xyz` the centre as raw float32 bits, `w` two packed
13
+ * half floats: rotation `x` (low 16) and `y` (high 16).
14
+ * - `transformB` RGBA16F - `xyz` linear scale, `w` rotation `z`.
15
+ *
16
+ * Rotation `w` is not stored: quaternions are normalised and flipped to
17
+ * `w >= 0` here, so the shader recovers it as `sqrt(1 - |xyz|^2)`.
18
+ */
19
+ export interface PackedSplats {
20
+ numSplats: number;
21
+ width: number;
22
+ height: number;
23
+ color: Uint16Array;
24
+ transformA: Uint32Array;
25
+ transformB: Uint16Array;
26
+ /** Rest-pose centres, flat `[x,y,z] * numSplats`. The depth sort reads these. */
27
+ centers: Float32Array;
28
+ boundsMin: [number, number, number];
29
+ boundsMax: [number, number, number];
30
+ }
31
+ /** Texture dimensions for `count` texels: near-square, wide enough to hold every row. */
32
+ export declare function textureSizeFor(count: number): {
33
+ width: number;
34
+ height: number;
35
+ };
36
+ export declare function packSplats(numSplats: number, s: SplatAttributes): PackedSplats;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Load-time splat removal: the two passes that make a character cheaper by
3
+ * containing fewer splats, rather than by drawing each one faster.
4
+ *
5
+ * Both run on the decoded attribute columns, before {@link packSplats} turns
6
+ * them into texels, so a removed splat costs nothing anywhere afterwards -
7
+ * no texel, no CPU skinning slot, no sort slot, no quad. That is what
8
+ * separates these from {@link SplatQuality.quadAlphaCutoff}, which shrinks
9
+ * quads and saves rasterisation alone.
10
+ *
11
+ * Ported from the Unity build's `SplatCharacterBinData.CullByOpacity` and
12
+ * `PruneByImportance`, including the two decisions in them that are easy to
13
+ * get wrong: ranking inside spatial cells rather than globally, and selecting
14
+ * rather than sorting within a cell.
15
+ */
16
+ import type { SplatAttributes } from '../loader/character/types';
17
+ import type { SplatWeights } from '../types';
18
+ /** What {@link pruneSplats} did, for a caller that wants to log or report it. */
19
+ export interface PruneReport {
20
+ /** Splats the decode handed in. */
21
+ numSplatsIn: number;
22
+ /** Splats left after both passes. This is what every consumer should read. */
23
+ numSplatsOut: number;
24
+ /** Removed by the opacity floor. */
25
+ droppedByOpacity: number;
26
+ /** Removed by the per-voxel importance quota, on top of the opacity floor. */
27
+ droppedByImportance: number;
28
+ /**
29
+ * The skinning influences, re-viewed onto the compacted prefix when anything
30
+ * was removed, or the caller's own object when nothing was. `undefined` for
31
+ * a splat with no skinning.
32
+ *
33
+ * Handed back rather than mutated in place because the arrays' *length* is
34
+ * what says how many influences each splat has: `Splat` divides it by the
35
+ * splat count to recover `K`, and leaving a pruned set at its original
36
+ * length would make that division report a `K` several times too large.
37
+ */
38
+ weights?: SplatWeights;
39
+ }
40
+ export interface PruneOptions {
41
+ /** Defaults to {@link SplatQuality.minOpacity}. */
42
+ minOpacity?: number;
43
+ /** Defaults to {@link SplatQuality.pruneRatio}. */
44
+ pruneRatio?: number;
45
+ /** Defaults to {@link SplatQuality.pruneVoxelSize}. */
46
+ voxelSize?: number;
47
+ }
48
+ /**
49
+ * Remove splats the current quality settings say are not worth carrying,
50
+ * compacting every column in place.
51
+ *
52
+ * `attributes` and `weights` are mutated: their arrays keep their allocated
53
+ * length and the returned count is what every consumer should read, which is
54
+ * exactly how {@link packSplats} already uses `numSplats`. Order is preserved,
55
+ * so whatever spatial coherence the file had survives.
56
+ *
57
+ * Returns the input count unchanged, having touched nothing, when both knobs
58
+ * are off - the default - so this sits on the load path for free.
59
+ */
60
+ export declare function pruneSplats(numSplats: number, attributes: SplatAttributes, weights?: SplatWeights, opts?: PruneOptions): PruneReport;