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

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -2
  3. package/dist/chunks/chunk-MM6DJU6S.js +7801 -0
  4. package/dist/chunks/chunk-MM6DJU6S.js.map +7 -0
  5. package/dist/index.js +52 -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 +540 -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 +37 -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 +58 -0
  45. package/dist/types/splat/packed-splats.d.ts +36 -0
  46. package/dist/types/splat/splat-mesh.d.ts +71 -0
  47. package/dist/types/splat/splat.d.ts +186 -0
  48. package/dist/types/splat/transform-palette.d.ts +42 -0
  49. package/dist/types/splat/webgl/backend.d.ts +17 -0
  50. package/dist/types/splat/webgl/shaders.d.ts +2 -0
  51. package/dist/types/splat/webgl/splat-mesh.d.ts +52 -0
  52. package/dist/types/splat/webgpu/backend.d.ts +25 -0
  53. package/dist/types/splat/webgpu/lighting.d.ts +34 -0
  54. package/dist/types/splat/webgpu/radix-sort.d.ts +46 -0
  55. package/dist/types/splat/webgpu/splat-mesh.d.ts +71 -0
  56. package/dist/types/three/loaders.d.ts +77 -0
  57. package/dist/types/three/splat-character.d.ts +192 -0
  58. package/dist/types/types.d.ts +30 -0
  59. package/dist/types/webgpu.d.ts +5 -0
  60. package/dist/webgpu.js +813 -0
  61. package/dist/webgpu.js.map +7 -0
  62. package/package.json +46 -4
@@ -0,0 +1,166 @@
1
+ import { DataTexture, Frustum, Group, Vector3 } from 'three';
2
+ import type { Camera, IUniform, Object3D } from 'three';
3
+ import type { Animation } from './animation';
4
+ import type { AnimLodPreset } from './armature';
5
+ import type { PointLight } from './lighting/point-light';
6
+ import type { SkyLight } from './lighting/sky-light';
7
+ import type { SunLight } from './lighting/sun-light';
8
+ import type { SkeletonLibrary } from './loader/skeleton';
9
+ import type { SplatBackend } from './splat/backend';
10
+ /**
11
+ * Lights that splats respond to. Splats carry baked colour, so this is a
12
+ * multiplier on that colour rather than a shading model: ambient plus sun plus
13
+ * distance-attenuated point lights, evaluated at each splat's centre.
14
+ *
15
+ * With no sun and no sky the factor is exactly 1 and splats render as authored.
16
+ *
17
+ * @group Lighting
18
+ */
19
+ export interface SceneLighting {
20
+ sun: SunLight | null;
21
+ sky: SkyLight | null;
22
+ points: PointLight[];
23
+ }
24
+ /** @group Engine */
25
+ export interface SplatEngineOptions {
26
+ /**
27
+ * Parent for everything the engine creates. When given, the engine's
28
+ * {@link SplatEngine.contentRoot} is added to it, so loaded characters and
29
+ * environments appear without a further `scene.add`.
30
+ */
31
+ scene?: Object3D;
32
+ /** Camera used for visibility gating and distance LOD. Can also be passed to {@link SplatEngine.update}. */
33
+ camera?: Camera;
34
+ /**
35
+ * Force a particular splat backend. Leave unset: each splat then picks the
36
+ * backend matching the renderer that first draws it.
37
+ */
38
+ backend?: SplatBackend;
39
+ }
40
+ /**
41
+ * Shared state for a set of splat characters and environments: the skeleton
42
+ * library, the registry of named animation clips, the lights, and the
43
+ * per-frame update that advances every character.
44
+ *
45
+ * This is the three.js counterpart of the PlayCanvas build's `Scene`, minus
46
+ * everything three.js already owns. It creates no renderer, canvas or camera:
47
+ * you keep your own `WebGLRenderer`, `Scene` and render loop, and call
48
+ * {@link SplatEngine.update} once per frame before rendering.
49
+ *
50
+ * ```ts
51
+ * const engine = new SplatEngine({ scene, camera });
52
+ * const character = await Character.load(engine, '/assets/hero.vsplat');
53
+ * await Animation.loadGlb(engine, '/assets/idle.glb', 'idle');
54
+ * character.playAnimation('idle', { loop: true });
55
+ *
56
+ * renderer.setAnimationLoop(() => {
57
+ * engine.update(clock.getDelta());
58
+ * renderer.render(scene, camera);
59
+ * });
60
+ * ```
61
+ *
62
+ * @group Engine
63
+ */
64
+ export declare class SplatEngine {
65
+ /** Parent of every character and environment loaded through this engine. */
66
+ readonly contentRoot: Group;
67
+ /** A forced splat backend, or `null` to pick per renderer (the default). */
68
+ readonly backend: SplatBackend | null;
69
+ readonly skeletonLibrary: SkeletonLibrary;
70
+ /** Clips by name. Populated by {@link Animation.register} and the `Animation.load*` helpers. */
71
+ readonly skeletalAnimationLibrary: Map<string, Animation>;
72
+ readonly lighting: SceneLighting;
73
+ /** Camera used for visibility gating and distance LOD. `null` disables both. */
74
+ camera: Camera | null;
75
+ /** Default animation-LOD bands for `Armature.setAnimationLod()` called with no arguments. */
76
+ animLodPreset?: AnimLodPreset;
77
+ /** Number of {@link SplatEngine.update} calls so far. */
78
+ frame: number;
79
+ /** {@link SplatEngine.frame} of the most recent character pose evaluation. */
80
+ characterPoseUpdatedFrame: number;
81
+ /**
82
+ * Lighting uniforms shared by every splat material this engine creates.
83
+ * Refreshed by {@link SplatEngine.syncLighting}.
84
+ * @internal
85
+ */
86
+ readonly lightingUniforms: {
87
+ uSunColor: IUniform<Vector3>;
88
+ uSunIntensity: IUniform<number>;
89
+ uAmbientColor: IUniform<Vector3>;
90
+ uPointLightCount: IUniform<number>;
91
+ uPointLightTex: IUniform<DataTexture>;
92
+ };
93
+ private readonly _updaters;
94
+ private readonly _frustum;
95
+ private readonly _projScreen;
96
+ private readonly _cameraPosition;
97
+ private _frustumValid;
98
+ private _pointLightCapacity;
99
+ private _destroyed;
100
+ constructor(opts?: SplatEngineOptions);
101
+ get destroyed(): boolean;
102
+ private static _shared;
103
+ /**
104
+ * The engine the loaders use when none is given: one shared skeleton
105
+ * library, animation registry and set of lights for the whole page. Created
106
+ * on first use, attached to no scene.
107
+ */
108
+ static get shared(): SplatEngine;
109
+ /**
110
+ * Advance every auto-updating character by `dt` seconds. Call once per
111
+ * frame, before rendering.
112
+ *
113
+ * @param camera - Optional; replaces {@link SplatEngine.camera} when given.
114
+ */
115
+ update(dt: number, camera?: Camera): void;
116
+ /**
117
+ * Register a per-frame callback run by {@link SplatEngine.update}.
118
+ * @returns A function that removes it.
119
+ * @internal
120
+ */
121
+ addUpdater(fn: (dt: number) => void): () => void;
122
+ /** @internal */
123
+ removeUpdater(fn: (dt: number) => void): void;
124
+ /**
125
+ * The camera frustum as of the last {@link SplatEngine.update}, or
126
+ * `undefined` when no camera is set - callers treat that as "visible".
127
+ * @internal
128
+ */
129
+ get cameraFrustum(): Frustum | undefined;
130
+ /**
131
+ * World-space camera position as of the last {@link SplatEngine.update}, or
132
+ * `null` when no camera is set.
133
+ * @internal
134
+ */
135
+ get cameraWorldPosition(): Vector3 | null;
136
+ /**
137
+ * Incremented whenever {@link SplatEngine.syncLighting} finds the lights
138
+ * changed. Lets a backend skip work that depends on lighting.
139
+ * @internal
140
+ */
141
+ lightingVersion: number;
142
+ /**
143
+ * Copy {@link SplatEngine.lighting} into the shared uniforms. Called per
144
+ * draw, so it only compares numbers and writes on change.
145
+ * @internal
146
+ */
147
+ syncLighting(): void;
148
+ /**
149
+ * Detach the engine's content from the scene and stop updating. Characters
150
+ * and environments are not destroyed for you - destroy them first if you
151
+ * want their GPU resources released.
152
+ */
153
+ dispose(): void;
154
+ }
155
+ /**
156
+ * Thrown when an asset finishes loading onto an engine that has already been
157
+ * disposed.
158
+ *
159
+ * This is a CANCELLATION, not a failure: the bytes were fine and the engine is
160
+ * simply gone. It exists as its own type so a caller can tell the two apart.
161
+ *
162
+ * @group Engine
163
+ */
164
+ export declare class EngineDisposedError extends Error {
165
+ constructor(what: string);
166
+ }
@@ -0,0 +1,53 @@
1
+ import type { Object3D } from 'three';
2
+ import type { SplatEngine } from './engine';
3
+ import { Splat } from './splat/splat';
4
+ /** @group Environment */
5
+ export interface LoadEnvironmentOptions {
6
+ /** Object name. Default: basename of the URL. */
7
+ name?: string;
8
+ /** World position. Default: origin. */
9
+ position?: {
10
+ x: number;
11
+ y: number;
12
+ z: number;
13
+ };
14
+ /** Euler angles in degrees. Default: no rotation. */
15
+ rotation?: {
16
+ x: number;
17
+ y: number;
18
+ z: number;
19
+ };
20
+ /** Uniform scale. Default: 1. */
21
+ scale?: number;
22
+ /** Explicit filename for extension detection on URLs without one. */
23
+ filename?: string;
24
+ }
25
+ /**
26
+ * Gaussian-splat environment loaded into an engine.
27
+ *
28
+ * A static, unskinned splat that responds to the engine's lighting registry
29
+ * (sun + sky + point lights). Its depth sort runs in a worker and only when
30
+ * the view direction changes, so a large scene costs nothing while the camera
31
+ * is still.
32
+ *
33
+ * Construct via {@link Environment.load} - the constructor is internal.
34
+ *
35
+ * @group Environment
36
+ */
37
+ export declare class Environment {
38
+ /** The splat that draws the environment. */
39
+ readonly splat: Splat;
40
+ private constructor();
41
+ /** The three.js object for the environment. Transform it like any other. */
42
+ get object3D(): Object3D;
43
+ /**
44
+ * Fetch, decode and add an environment splat.
45
+ *
46
+ * Reads uncompressed 3D Gaussian Splatting `.ply` files.
47
+ *
48
+ * @param url - Location of the splat file.
49
+ */
50
+ static load(engine: SplatEngine, url: string, opts?: LoadEnvironmentOptions): Promise<Environment>;
51
+ /** Remove the environment and release its GPU resources. */
52
+ destroy(): void;
53
+ }
@@ -0,0 +1,37 @@
1
+ export { SplatCharacter } from './three/splat-character';
2
+ export type { SplatCharacterEventMap, SplatCrossFadeOptions, SplatLayerOptions, SplatPlayOptions, SplatRootMotion, } from './three/splat-character';
3
+ export { SplatClipLoader, SplatPLYLoader, VSplatLoader } from './three/loaders';
4
+ export { Animation as SplatClip } from './animation';
5
+ export { SplatEngine, EngineDisposedError } from './engine';
6
+ export type { SceneLighting, SplatEngineOptions } from './engine';
7
+ export { Character } from './character';
8
+ export type { LoadCharacterOptions } from './character';
9
+ export { Animation } from './animation';
10
+ export type { AnimationEvent } from './animation';
11
+ export { Armature } from './armature';
12
+ export type { AnimLodPreset, PoseProvider, RootMotionMode } from './armature';
13
+ export { Environment } from './environment';
14
+ export type { LoadEnvironmentOptions } from './environment';
15
+ export { Splat } from './splat/splat';
16
+ export { SplatMesh } from './splat/splat-mesh';
17
+ export { WebGLSplatBackend } from './splat/webgl/backend';
18
+ export type { SplatBackend } from './splat/backend';
19
+ export type { SplatOptions, SplatSource } from './splat/splat';
20
+ export { packSplats } from './splat/packed-splats';
21
+ export type { PackedSplats } from './splat/packed-splats';
22
+ export { SunLight } from './lighting/sun-light';
23
+ export type { CreateSunLightOptions } from './lighting/sun-light';
24
+ export { SkyLight } from './lighting/sky-light';
25
+ export type { CreateSkyLightOptions } from './lighting/sky-light';
26
+ export { PointLight } from './lighting/point-light';
27
+ export type { CreatePointLightOptions } from './lighting/point-light';
28
+ export { makeRegionMask } from './skeletal/bone-mask';
29
+ export type { BodyRegion, BoneMask } from './skeletal/bone-mask';
30
+ export type { AnimEventCallback, CrossfadeOptions } from './skeletal/animation-player';
31
+ export type { BoneScaleInput } from './skeletal/bone-stretch';
32
+ export { decodeCharacterBundle, loadCharacterBundle, loadCharacterBundleFromBytes } from './loader/character/load';
33
+ export type { CharacterBundle } from './loader/character/load';
34
+ export { CharacterBundleError } from './loader/character/types';
35
+ export { parseGaussianPly } from './loader/environment/ply';
36
+ export type { SkeletonLibrary } from './loader/skeleton';
37
+ export type { ArmatureData, SplatWeights } from './types';
@@ -0,0 +1,13 @@
1
+ import type { Color } from 'three';
2
+ /** Parse `#rrggbb` into raw 0..1 components. */
3
+ export declare function parseHex(css: string): [number, number, number];
4
+ /**
5
+ * Write a CSS hex colour into `color` as raw component values.
6
+ *
7
+ * Deliberately not `Color.set(hex)`: light colours here multiply baked,
8
+ * display-referred splat colour, so they are used as written. three.js colour
9
+ * management would convert the hex to linear and darken every light.
10
+ */
11
+ export declare function applyHexTo(color: Color, css: string): void;
12
+ /** A `Color` holding exactly the given component values, with no colour-space conversion. */
13
+ export declare function rawColor(color: Color, r: number, g: number, b: number): Color;
@@ -0,0 +1,33 @@
1
+ import { Color, Vector3 } from 'three';
2
+ import type { SplatEngine } from '../engine';
3
+ /** @group Lighting */
4
+ export interface CreatePointLightOptions {
5
+ position?: {
6
+ x: number;
7
+ y: number;
8
+ z: number;
9
+ };
10
+ /** Light colour as CSS hex string. */
11
+ color?: string;
12
+ intensity?: number;
13
+ /** Hard cutoff distance in world units. Contribution is 0 at and beyond this. */
14
+ range?: number;
15
+ enabled?: boolean;
16
+ }
17
+ /** @group Lighting */
18
+ export declare class PointLight {
19
+ /** World-space position. */
20
+ position: Vector3;
21
+ /** Raw component values; not colour-managed. */
22
+ color: Color;
23
+ intensity: number;
24
+ range: number;
25
+ enabled: boolean;
26
+ private _engine;
27
+ /** @internal */
28
+ _attach(engine: SplatEngine): void;
29
+ /** Remove this light from its engine. After calling, the light stops contributing. */
30
+ destroy(): void;
31
+ /** Create a point light and register it on the engine's lighting registry. */
32
+ static create(engine: SplatEngine, opts?: CreatePointLightOptions): PointLight;
33
+ }
@@ -0,0 +1,22 @@
1
+ import { Color } from 'three';
2
+ import type { SplatEngine } from '../engine';
3
+ /** @group Lighting */
4
+ export interface CreateSkyLightOptions {
5
+ /** Sky colour as CSS hex string. */
6
+ color?: string;
7
+ intensity?: number;
8
+ enabled?: boolean;
9
+ }
10
+ /**
11
+ * Ambient light for splats: a constant `color * intensity` added to every splat.
12
+ *
13
+ * @group Lighting
14
+ */
15
+ export declare class SkyLight {
16
+ /** Raw component values; not colour-managed. */
17
+ color: Color;
18
+ intensity: number;
19
+ enabled: boolean;
20
+ /** Create a sky light and make it the engine's sky. */
21
+ static create(engine: SplatEngine, opts?: CreateSkyLightOptions): SkyLight;
22
+ }
@@ -0,0 +1,34 @@
1
+ import { Color, Vector3 } from 'three';
2
+ import type { SplatEngine } from '../engine';
3
+ /** @group Lighting */
4
+ export interface CreateSunLightOptions {
5
+ /** Azimuth in degrees. 0 = +Z, 90 = +X. */
6
+ azimuth?: number;
7
+ /** Elevation in degrees. 0 = horizon, 90 = directly overhead. */
8
+ elevation?: number;
9
+ /** Sun colour as CSS hex string, e.g. '#fff4e0'. */
10
+ color?: string;
11
+ /** Brightness multiplier applied to `color` in the shader. */
12
+ intensity?: number;
13
+ /** Start disabled. Lights default to enabled. */
14
+ enabled?: boolean;
15
+ }
16
+ /**
17
+ * Directional light for splats. Splats have no normals, so the sun is a flat
18
+ * brightness term: every splat receives `color * intensity`, regardless of
19
+ * which way it faces.
20
+ *
21
+ * @group Lighting
22
+ */
23
+ export declare class SunLight {
24
+ azimuth: number;
25
+ elevation: number;
26
+ /** Raw component values; not colour-managed. */
27
+ color: Color;
28
+ intensity: number;
29
+ enabled: boolean;
30
+ /** Unit vector pointing from the scene toward the sun. */
31
+ lightDir(out?: Vector3): Vector3;
32
+ /** Create a sun and make it the engine's sun. */
33
+ static create(engine: SplatEngine, opts?: CreateSunLightOptions): SunLight;
34
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Decoded contents of an *armature GLB* - a glTF 2.0 binary carrying a bone
3
+ * hierarchy, a skin, and one animation clip. Produced by the offline MetaHuman
4
+ * GLB exporter, and readable back after a Blender round-trip.
5
+ *
6
+ * @internal
7
+ */
8
+ export interface ArmatureGlbAnimation {
9
+ /** Bone matrices in {@link Animation.data} layout: `numFrames * numBones * 16`. */
10
+ data: Float32Array;
11
+ numFrames: number;
12
+ numBones: number;
13
+ /** Bone names in bone-index order (canonical rig order when recognised). */
14
+ boneNames: string[];
15
+ /**
16
+ * Per bone: `1` if the clip drives that bone's translation, `0` if the bone
17
+ * must inherit the armature's rest translation. `undefined` when every bone
18
+ * is driven - which is what a `.anim` file means, since that format cannot
19
+ * say anything else.
20
+ *
21
+ * Derived from the DATA, not from which channels exist: a translation
22
+ * channel whose keys are all equal declares a rest pose, and a channel that
23
+ * varies is motion. See {@link parseArmatureGlbAnimation}.
24
+ */
25
+ translationMask?: Uint8Array;
26
+ /** Source frame rate derived from the main timestamp accessor. */
27
+ frameRate: number;
28
+ /** Clip name as stored in the GLB. */
29
+ animationName: string;
30
+ }
31
+ /**
32
+ * How far a translation channel may wander and still count as a declaration of
33
+ * rest rather than motion: 10 µm.
34
+ *
35
+ * The distinction cannot be drawn from channel *presence*, because a
36
+ * conformant writer emits a channel for every bone and Blender re-exports
37
+ * dense channels for everything it imports. It is drawn from the values, which
38
+ * survive that round trip: a constant track stays constant however densely it
39
+ * is written.
40
+ *
41
+ * The bound is empirical and has room on both sides. Below it: a Blender
42
+ * 4.2.18 import→export of our own files re-derives every bone through its own
43
+ * matrix pipeline and perturbs a constant translation by at most 7.96e-7 m,
44
+ * 12x under this. Above it: the smallest genuinely moving channel in the same
45
+ * files drifts 3.06e-4 m, 30x over. The writer's own collapse threshold
46
+ * (the exporter's own `TRANSLATION_EPS`, 1e-6 m) sits inside
47
+ * that gap too, so everything it calls constant still reads as constant here,
48
+ * before and after a round trip.
49
+ *
50
+ * Shared with the `.anim` reader ({@link Animation.parse}), which asks the same
51
+ * question of the same quantity, so one number keeps the two formats from ever
52
+ * disagreeing about what "constant" means. It has even more room there: a v3
53
+ * `.anim` stores float16, whose decode is deterministic, so a constant track
54
+ * comes back bit-identical (deviation exactly 0) while the smallest moving bone
55
+ * across `test_full` / `test_stripped` / `test_blend_*` drifts 2.69e-4 m. Motion
56
+ * finer than this bound is not representable at bone-offset magnitudes in
57
+ * float16 anyway (one ulp at 0.4 m is 2.4e-4 m), so it has already been
58
+ * quantized flat before the test sees it.
59
+ *
60
+ * @internal
61
+ */
62
+ export declare const CONSTANT_TRANSLATION_EPS = 0.00001;
63
+ /**
64
+ * Parse an armature GLB into the flat bone-matrix form that {@link Animation}
65
+ * holds after {@link Animation.parse}.
66
+ *
67
+ * **Bones are resolved by NAME, never by node index.** The joint set comes from
68
+ * `skins[0].joints` when a skin is present (files we write, and anything
69
+ * Blender exports), falling back to "every node is a bone" for older files
70
+ * that carried no skin. If the joint names match one of the engine's known rigs the bones are
71
+ * re-ordered into that rig's canonical order; otherwise the file's joint order
72
+ * stands. Nodes that are not joints - the armature parent, mesh nodes, cameras,
73
+ * anything a DCC tool adds - are ignored, and so are channels targeting them.
74
+ *
75
+ * What it accepts:
76
+ *
77
+ * - `translation`, `rotation` and `scale` channels; `weights` is ignored,
78
+ * there being no morph targets on an armature.
79
+ * - Rotations as FLOAT or `normalized: true` SHORT (dequantized + renormalized).
80
+ * - `LINEAR`, `STEP` and `CUBICSPLINE` samplers. Other modes throw by name.
81
+ * - Per-channel timelines: each sampler rides either the main timeline - the
82
+ * longest, so frame `f` is the same instant for every bone - or a one-key
83
+ * timeline of its own, meaning a constant channel held for the whole clip.
84
+ * - Accessors sharing one bufferView via `byteOffset`, and absent `min`/`max`.
85
+ *
86
+ * What it requires: exactly one rotation channel per bone, timestamps on a
87
+ * uniform grid (the engine indexes animations by frame, so key `i` must be
88
+ * frame `i`), and no duplicate channels.
89
+ *
90
+ * **Constant translation is a rest declaration, not motion.** A translation
91
+ * channel whose keys are all equal (within {@link CONSTANT_TRANSLATION_EPS})
92
+ * states the rig's own bone length; a channel that varies is real motion -
93
+ * root motion at the pelvis, and the face, which is translation-driven. The
94
+ * former is reported through {@link ArmatureGlbAnimation.translationMask} as a
95
+ * `0`, so playback keeps the TARGET character's rest translation and its limbs
96
+ * do not resize mid-clip. On our own clips that lands on 85 of 441 bones
97
+ * without a single bone name being mentioned anywhere.
98
+ *
99
+ * The test reads the data rather than the channel list on purpose. Absence
100
+ * could carry the same message, but only in a file nobody has touched: a
101
+ * conformant reader is entitled to use `nodes[b].translation` when a channel
102
+ * is missing, and Blender re-exports dense channels for every bone, which
103
+ * erases the distinction. Constancy survives both. Files that genuinely omit
104
+ * the channel - everything written before 2026-08 - are still read the old
105
+ * way, as `0`.
106
+ *
107
+ * Scale gets the same treatment, and resolves at parse time: a constant scale
108
+ * is a rest declaration, and every rig this engine builds rests at scale
109
+ * `1,1,1`, so a constant channel simply resolves to identity here and needs no
110
+ * mask of its own. A varying scale is honoured. Our writers emit no scale
111
+ * channels at all, so this only ever fires for a bone somebody scaled by hand
112
+ * in a DCC tool.
113
+ *
114
+ * @throws If the container, the layout, or an accessor is invalid.
115
+ * @internal
116
+ */
117
+ export declare function parseArmatureGlbAnimation(buffer: ArrayBuffer, animationName?: string): ArmatureGlbAnimation;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Character-bundle reader: the dispatch table and the public entry point.
3
+ *
4
+ * ONE top-level branch, and legacy behind one door:
5
+ *
6
+ * magic == VIGGCHAR -> look `version` up in VERSION_HANDLERS
7
+ * otherwise -> legacy/
8
+ *
9
+ * That is the whole structure, and it is the point of it: adding version N+1
10
+ * costs one module under `vN/` and one line in the table below, and removing a
11
+ * version costs deleting exactly those two things. If a change to support a new
12
+ * version has to touch an existing handler, the decomposition is not real.
13
+ * (Character-bundle format spec, "Reader architecture", R1-R5.)
14
+ *
15
+ * The only bytes read before dispatch are the 16 container bytes. Nothing here
16
+ * sniffs the payload, peeks at the header, tries a parse, or consults the
17
+ * filename, the extension, the URL or the MIME type - the `.vsplat` extension
18
+ * is advisory and says nothing this reader may act on.
19
+ */
20
+ import { CharacterBundleError } from './types';
21
+ import type { DecodeResult } from './types';
22
+ /**
23
+ * Read a character bundle of any container this reader implements.
24
+ *
25
+ * @param file - the whole file as a read-only byte buffer.
26
+ * @returns the decoded bundle in the version-independent output type, plus the
27
+ * buffers a worker may transfer rather than copy. A caller cannot tell from the
28
+ * result which handler ran.
29
+ * @throws {CharacterBundleError} carrying the spec's error category.
30
+ */
31
+ declare function parseCharacterBundle(file: ArrayBuffer): Promise<DecodeResult>;
32
+ export { parseCharacterBundle, CharacterBundleError };
33
+ export type { DecodeResult };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The legacy door.
3
+ *
4
+ * Everything whose first 8 bytes are not `VIGGCHAR` arrives here, and nothing
5
+ * else does. Behind it are the two container generations that shipped before
6
+ * the format was respecified:
7
+ *
8
+ * `VFCHARB1` -> interleaved per-splat records (v1 fp32, v2 fp16)
9
+ * `VFCHARB2` -> planar byte planes (v3; fp16 or q23 precision)
10
+ * `VSPLAT01` -> the pre-release prototype of the planar layout
11
+ *
12
+ * These readers are PERMANENT, not migration scaffolding. Files already in
13
+ * storage keep their old magic forever - there is no backfill - so this module
14
+ * is kept indefinitely and its decoders are moved, never improved. What it is
15
+ * NOT is entangled: it is one branch of one top-level `if`, it imports nothing
16
+ * from `../v1/`, and `../v1/` imports nothing from it.
17
+ *
18
+ * These containers spell `version` as one global lineage across two magics,
19
+ * which is precisely the accretion `VIGGCHAR` exists to end. Nothing about that
20
+ * lineage leaks past this file: the dispatcher upstream never reads it, and the
21
+ * bundle handed back is the same version-independent shape `v1/` returns.
22
+ */
23
+ import type { DecodeResult } from '../types';
24
+ /**
25
+ * Decode a bundle in one of the pre-`VIGGCHAR` containers.
26
+ *
27
+ * The container is identified by SNIFFING THE 8-BYTE MAGIC and never by the
28
+ * filename or a header key - both generations arrive through the same URLs,
29
+ * with no out-of-band type information, so the bytes are the only honest
30
+ * answer. Anything this door does not recognise throws with the magic it
31
+ * actually found.
32
+ */
33
+ declare function decodeLegacyBundle(file: ArrayBuffer): Promise<DecodeResult>;
34
+ export { decodeLegacyBundle };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Interleaved character-splat decoder - the payload half of `VFCHARB1`.
3
+ *
4
+ * MOVED, NOT REWRITTEN. This is `binary-gsplat-worker.ts`'s `parseBinaryGsplat`
5
+ * verbatim, lifted out of the worker glue so the legacy readers sit behind one
6
+ * door (`../index.ts`). It decodes files that are already in storage and will
7
+ * stay there - there is no backfill - so any behaviour change here is a
8
+ * regression, and it keeps its own vocabulary: this container talks about
9
+ * CHUNKS, where the planar one talks about PLANES and `VIGGCHAR` talks about
10
+ * BLOCKS.
11
+ *
12
+ * `version` here numbers one global bundle lineage across both legacy magics
13
+ * (1 = fp32 interleaved, 2 = fp16 interleaved, 3 = planar), which is exactly the
14
+ * accretion `VIGGCHAR` was shaped to end.
15
+ */
16
+ /** The interleaved container: per-splat records, v1 (fp32) and v2 (fp16). */
17
+ declare const CHARACTER_BUNDLE_MAGIC = "VFCHARB1";
18
+ interface BundleChunk {
19
+ offset: number;
20
+ length: number;
21
+ stride?: number;
22
+ }
23
+ interface AssetHeader {
24
+ format: string;
25
+ version: number;
26
+ numSplats: number;
27
+ numBones: number;
28
+ bounds: {
29
+ min: number[];
30
+ max: number[];
31
+ };
32
+ encoding?: {
33
+ /**
34
+ * Bundle-wide field precision. `'fp16'` means pos/scale/rot/opacity and
35
+ * the weights chunk's per-splat weights are stored as IEEE 754 half-floats
36
+ * (read 2 bytes / value, expanded to Float32 on parse). `'fp32'` or
37
+ * missing = legacy raw float32 (the v1 layout). RGB stays uint8 and bone
38
+ * indices stay uint16 either way. Bumps the file size by ~42%; expanding
39
+ * to Float32 here means GPU memory is unchanged.
40
+ */
41
+ precision?: 'fp16' | 'fp32';
42
+ weights?: {
43
+ topK: number;
44
+ indexType: string;
45
+ weightType: string;
46
+ stride: number;
47
+ };
48
+ color?: {
49
+ format: string;
50
+ levels: number;
51
+ };
52
+ };
53
+ chunks: {
54
+ splats: BundleChunk;
55
+ weights?: BundleChunk;
56
+ joints?: BundleChunk;
57
+ animation?: BundleChunk;
58
+ };
59
+ animation?: {
60
+ format: string;
61
+ numFrames: number;
62
+ numBones: number;
63
+ shape: number[];
64
+ stride: number;
65
+ };
66
+ joints?: {
67
+ count: number;
68
+ format: string;
69
+ stride: number;
70
+ };
71
+ }
72
+ declare function parseBinaryGsplat(bundleBuffer: ArrayBuffer): {
73
+ result: {
74
+ header: AssetHeader;
75
+ numSplats: number;
76
+ splats: {
77
+ x: Float32Array<ArrayBuffer>;
78
+ y: Float32Array<ArrayBuffer>;
79
+ z: Float32Array<ArrayBuffer>;
80
+ scale_0: Float32Array<ArrayBuffer>;
81
+ scale_1: Float32Array<ArrayBuffer>;
82
+ scale_2: Float32Array<ArrayBuffer>;
83
+ rot_0: Float32Array<ArrayBuffer>;
84
+ rot_1: Float32Array<ArrayBuffer>;
85
+ rot_2: Float32Array<ArrayBuffer>;
86
+ rot_3: Float32Array<ArrayBuffer>;
87
+ f_dc_0: Float32Array<ArrayBuffer>;
88
+ f_dc_1: Float32Array<ArrayBuffer>;
89
+ f_dc_2: Float32Array<ArrayBuffer>;
90
+ opacity: Float32Array<ArrayBuffer>;
91
+ };
92
+ boneIndices: Uint16Array<ArrayBufferLike> | null;
93
+ boneWeights: Float32Array<ArrayBufferLike> | null;
94
+ animationArray: Float32Array<ArrayBufferLike> | null;
95
+ animNumFrames: number;
96
+ animNumBones: number;
97
+ jointsArray: Float32Array<ArrayBufferLike> | null;
98
+ };
99
+ transferables: ArrayBuffer[];
100
+ };
101
+ export { parseBinaryGsplat, CHARACTER_BUNDLE_MAGIC };