lecodes-sdk 2.0.12 → 2.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.
package/package.json CHANGED
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "bun run build-types.ts",
30
- "test": "bun test",
30
+ "test": "bun test --randomize",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "docs:check": "node docs/check.mjs",
33
33
  "prompts:build": "bun prompts/compose.ts",
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.14.1",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.0.12",
48
+ "version": "2.1.0",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
@@ -5,8 +5,11 @@
5
5
  new Scene(options?: {
6
6
  ibl?: boolean // image-based ambient light; default TRUE — every scene has ambient
7
7
  environmentIntensity?: number // IBL strength; default 20000
8
- bloom?: boolean // default false
9
- bloomIntensity?: number // default 0.2 (only with bloom: true)
8
+ postProcessing?: { // the look: tone mapping, grading, a .cube LUT, bloom, vignette, AO, reflections, DoF
9
+ toneMapping?: 'aces' | 'neutral' | 'linear' | 'filmic', exposure?: number, contrast?: number, saturation?: number,
10
+ lut?: string, bloom?: boolean | { intensity?: number }, vignette?: boolean | { feather?: number },
11
+ ambientOcclusion?: boolean | { radius?: number }, screenSpaceReflections?: boolean, depthOfField?: boolean | { focusDistance?: number, aperture?: number }
12
+ } // live: scene.postProcessing.contrast = 1.1; .apply({ ... }) = the named fields only; .reset(); never assigned
10
13
  skybox?: ColorInput // solid background / clear color
11
14
  antialias?: boolean // 4× MSAA; default false
12
15
  })
@@ -11,7 +11,7 @@
11
11
  // `?` on a method = an optional slot: a host that leaves it null gets no such JS method, so the SDK's
12
12
  // `typeof` feature-detects stay honest. The `HostApp` interface below adds the slots the runtime
13
13
  // calls that JS never sees (notifications to the host).
14
- import type { bytes } from "./types"
14
+ import type { bytes, i32 } from "./types"
15
15
 
16
16
  declare global {
17
17
  // ---- World control (run/restart/quit — the platform's `location` analog) ----------------------
@@ -73,6 +73,23 @@ declare global {
73
73
  * synchronous top level and the launcher always comes back unlocked. Optional — hosts
74
74
  * without a rotatable screen (desktop, headless, macOS) omit it. Backs app.setOrientation. */
75
75
  setOrientation?(mode: string): void
76
+
77
+ // ---- Versions: the installed app (the HOST) vs the bundle running in it (OTA may part them) -----
78
+ /** The installed app's version string — Android versionName, iOS CFBundleShortVersionString.
79
+ * Hosts that are not an app (desktop, headless, the web) omit it. Backs app.host.version. */
80
+ appVersion?(): string
81
+ /** The installed app's build number — Android versionCode, iOS CFBundleVersion read as an
82
+ * integer (0 when it is not one). Backs app.host.build. */
83
+ appBuild?(): i32
84
+ /** The SDK version embedded in this host (CREATOR_PKG_SDK_VERSION, the runtime set's version — a
85
+ * shell's lecodes-ios-sdk / lecodes-android-sdk release). The bundle's own is `// sdk:` of its
86
+ * header (app.sdkVersion); the two differ once an OTA bundle runs on an older install. Backs
87
+ * app.host.sdk. @custom */
88
+ runtimeVersion(): string
89
+ /** The `// last-updated:` line of the bundle the live world runs (its compile time, ISO 8601) or
90
+ * null for a bundle without the line. The updater's downgrade rule reads the same line. Backs
91
+ * app.bundle.updatedAt. @custom */
92
+ bundleUpdatedAt(): string | null
76
93
  }
77
94
  }
78
95
 
@@ -113,8 +113,35 @@ declare global {
113
113
  * ktx every call, so it can never carry a slider. No-op until the scene has an IBL. Optional:
114
114
  * hosts that predate it keep whatever `setDefaultIbl` set at open. */
115
115
  setEnvironmentIntensity?(sceneId: u32, intensity: f32): void
116
- setBloomOptions(sceneId: u32, enabled: boolean, strength: f32, quality: u8): void
117
- setToneMapping?(sceneId: u32, mode: u8): void
116
+ /** Bloom, every knob in one call (`scene.postProcessing.bloom`): `strength` 0..1 of the blurred
117
+ * highlights added back, `threshold` = only what is brighter than 1.0 after exposure blooms,
118
+ * `highlight` clamps one pixel's contribution (1000), `levels` 1..12 = how far the glow spreads,
119
+ * `quality` 0..3, `lensFlare` = ghosts + halo, `dirtTextureId` (0xFFFFFFFF = none) a texture
120
+ * multiplied into the bloom at `dirtStrength`. */
121
+ setBloomOptions(sceneId: u32, enabled: boolean, strength: f32, threshold: boolean, highlight: f32, levels: u8, quality: u8, lensFlare: boolean, dirtTextureId: u32, dirtStrength: f32): void
122
+ /** Vignette, a uniform of the grading pass (free to animate): `midPoint` 0..1 where the darkening
123
+ * starts, `roundness` 0 (the aspect) .. 1 (a circle), `feather` 0..1, `color` 0xRRGGBBAA read as
124
+ * linear. Optional: a host that predates it draws none. */
125
+ setVignetteOptions?(sceneId: u32, enabled: boolean, midPoint: f32, roundness: f32, feather: f32, color: u32): void
126
+ /** Colour grading: the operator and every adjustment baked into the view's ONE 3D LUT. `params` is
127
+ * the fixed layout of `sdk/src/gl/postProcessing.ts` (`CG_*`, mirrored by creator.h's
128
+ * ColorGradingParam; 39 floats, the last one = apply the LUT of setColorLut). A change re-bakes a 32³ table on the CPU, so the SDK coalesces
129
+ * its writes per tick and quantises; an identical array is a no-op in the engine. Optional: a
130
+ * host that predates it renders filament's default (ACES legacy). */
131
+ setColorGrading?(sceneId: u32, params: Float32Array): void
132
+ /** A custom 3D LUT after the operator, in display-referred sRGB: the TEXT of a `.cube` file
133
+ * (`LUT_3D_SIZE n` + n³ `r g b` lines, red fastest), parsed by the engine once and applied while
134
+ * the `CG_LUT` flag of setColorGrading is 1 (a LUT is removed by the flag, never by an empty
135
+ * buffer: the bridge passes none). Baked into the same table. Optional. */
136
+ setColorLut?(sceneId: u32, cubeFetchId: bytes): void
137
+ /** Screen-space reflections (`scene.postProcessing.screenSpaceReflections`): what is on screen
138
+ * reflects, by a ray march through the depth buffer — `maxDistance` metres, `thickness` metres a
139
+ * surface extends behind its depth, `stride` pixels per step. Optional. */
140
+ setScreenSpaceReflections?(sceneId: u32, enabled: boolean, maxDistance: f32, thickness: f32, stride: f32): void
141
+ /** Depth of field: the camera focuses at `focusDistance` metres and the view blurs as if the lens
142
+ * were at f/`aperture`, without touching the exposure (0 = the camera's own f-number), times
143
+ * `strength` (1 = physical; a game's wide lens blurs little). Optional. */
144
+ setDepthOfField?(sceneId: u32, enabled: boolean, focusDistance: f32, aperture: f32, strength: f32): void
118
145
  /** Exposure of the scene's camera — filament's physical model: `aperture` f-stops,
119
146
  * `shutterSpeed` seconds, `sensitivity` ISO (default f/16, 1/125 s, ISO 100 = EV100 15, bright
120
147
  * sunlight). ISO ×2 = one stop brighter. Scene-referred light follows it; particle emission is
@@ -742,26 +769,32 @@ declare global {
742
769
  /** Distance fade for every instance of the entity's ASSET (present and future): the cards thin out between `start`
743
770
  * and `end` metres from the camera and past `end` the LOD pass takes the instance out of the scene. end 0 = none. */
744
771
  foliageSetFade?(entityId: u32, start: f32, end: f32): void
772
+ /** @ifdef CREATOR_GL_PHYSICS */
745
773
  physicsConfigure(gx: f32, gy: f32, gz: f32, maxBodies: u32): void
746
- /** @c setPhysicsInterpolation */
774
+ /** @c setPhysicsInterpolation @ifdef CREATOR_GL_PHYSICS */
747
775
  setInterpolation(enabled: boolean): void
748
776
  // Shape/Physics/Trigger aspects: build a shape once, create bodies from it.
777
+ /** @ifdef CREATOR_GL_PHYSICS */
749
778
  physicsBuildBox(hx: f32, hy: f32, hz: f32): u32
779
+ /** @ifdef CREATOR_GL_PHYSICS */
750
780
  physicsBuildSphere(radius: f32): u32
781
+ /** @ifdef CREATOR_GL_PHYSICS */
751
782
  physicsBuildCylinder(halfHeight: f32, radius: f32): u32
783
+ /** @ifdef CREATOR_GL_PHYSICS */
752
784
  physicsBuildCapsule(halfHeight: f32, radius: f32): u32
753
785
  /** Mesh shape from node-local triangles. convex=false → triangle mesh (static/kinematic/pick/character
754
786
  * only; physicsCreateBody returns 0 for a dynamic one), convex=true → convex hull (any motion).
755
- * (sx,sy,sz) = world scale, applied natively. Returns 0 if the shape can't be built. @count vertexCount=vertices/3 */
787
+ * (sx,sy,sz) = world scale, applied natively. Returns 0 if the shape can't be built. @count vertexCount=vertices/3 @ifdef CREATOR_GL_PHYSICS */
756
788
  physicsBuildMesh(vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: f32, sy: f32, sz: f32): u32
757
- /** Same from a loaded GLB root (non-skinned primitives, bind pose, baked sub-node transforms; cached per asset). */
789
+ /** Same from a loaded GLB root (non-skinned primitives, bind pose, baked sub-node transforms; cached per asset). @ifdef CREATOR_GL_PHYSICS */
758
790
  physicsBuildMeshFromEntity(entityId: u32, convex: boolean, sx: f32, sy: f32, sz: f32): u32
759
791
  /** Terrain collider (Shape { heightfield: true }, docs/terrain-plan.md §1.4): a Jolt HeightFieldShape over
760
792
  * the grid `terrainCreate` draws (same arrays, same hole rule). Static / kinematic / pick / character
761
793
  * ground only. (sx,sy,sz) = world scale. `physicsUpdateHeightField` rewrites a sample rectangle in
762
794
  * place from the FULL arrays (live bodies keep the shape); heights beyond the range chosen at build
763
- * time (25 % headroom) clamp — the SDK rebuilds the shape when an edit leaves that range. Optional. */
795
+ * time (25 % headroom) clamp — the SDK rebuilds the shape when an edit leaves that range. Optional. @ifdef CREATOR_GL_PHYSICS */
764
796
  physicsBuildHeightField?(heights: Float32Array, sizeX: u32, sizeZ: u32, cellSize: f32, holes: Uint8Array | null, sx: f32, sy: f32, sz: f32): u32
797
+ /** @ifdef CREATOR_GL_PHYSICS */
765
798
  physicsUpdateHeightField?(shapeId: u32, heights: Float32Array, sizeX: u32, sizeZ: u32, x0: u32, z0: u32, w: u32, h: u32, holes: Uint8Array | null): void
766
799
  /** A loaded GLB root's triangle soup — the one `physicsBuildMeshFromEntity` collides — as 9 floats per
767
800
  * triangle in the asset root's space (empty when the entity is not a GLB). `Terrain.conform` stamps a
@@ -769,56 +802,63 @@ declare global {
769
802
  glbTriangles?(entityId: u32): Float32Array
770
803
  /** Offset a built shape's centre from the node's origin (Shape `origin`) — world units in the
771
804
  * body's rotated, UNSCALED frame, sitting outside a mesh shape's scale wrapper. Sets rather
772
- * than accumulates; (0,0,0) clears it. Optional: an older host just centres on the node. */
805
+ * than accumulates; (0,0,0) clears it. Optional: an older host just centres on the node. @ifdef CREATOR_GL_PHYSICS */
773
806
  physicsSetShapeOrigin?(shapeId: u32, x: f32, y: f32, z: f32): void
774
807
  /** Swap a live body's shape, keeping its id, velocity and transform (Shape.fit / a re-attach).
775
- * updateMass recomputes the inertia tensor. Refuses a triangle mesh on a dynamic body. */
808
+ * updateMass recomputes the inertia tensor. Refuses a triangle mesh on a dynamic body. @ifdef CREATOR_GL_PHYSICS */
776
809
  physicsSetBodyShape?(bodyId: u32, shapeId: u32, updateMass: boolean): void
810
+ /** @ifdef CREATOR_GL_PHYSICS */
777
811
  physicsDestroyShape(shapeId: u32): void
778
- /** sensor = trigger (overlap events, no response); pickOnly = raycast-only, non-colliding body. */
812
+ /** sensor = trigger (overlap events, no response); pickOnly = raycast-only, non-colliding body. @ifdef CREATOR_GL_PHYSICS */
779
813
  physicsCreateBody(entityId: u32, shapeId: u32, motion: i32, mass: f32, sensor: boolean, pickOnly: boolean): u32
814
+ /** @ifdef CREATOR_GL_PHYSICS */
780
815
  physicsSetPickable(bodyId: u32, pickable: boolean): void
781
816
  /** Surface friction of one body (0 = ice, ~1 = grippy asphalt). Values COMBINE as sqrt(a * b), so
782
817
  * a low value on either side dominates. New bodies start at 0.6 — a neutral solid surface.
783
- * The vehicle wheel cast reads the GROUND body's value — this is what caps a car's cornering. */
818
+ * The vehicle wheel cast reads the GROUND body's value — this is what caps a car's cornering. @ifdef CREATOR_GL_PHYSICS */
784
819
  physicsSetFriction(bodyId: u32, friction: f32): void
820
+ /** @ifdef CREATOR_GL_PHYSICS */
785
821
  physicsGetFriction(bodyId: u32): f64
786
- /** Ray vs pickable bodies → hit entity id (0 = miss); fills `out` = [px,py,pz,nx,ny,nz,fraction]. @into out 7 */
822
+ /** Ray vs pickable bodies → hit entity id (0 = miss); fills `out` = [px,py,pz,nx,ny,nz,fraction]. @into out 7 @ifdef CREATOR_GL_PHYSICS */
787
823
  physicsRaycast(ox: f32, oy: f32, oz: f32, dx: f32, dy: f32, dz: f32, maxDist: f32, out?: Float32Array): u32
788
824
  /** Dev-time dump of the static collision geometry (docs/navmesh-plan.md §4) — the input of
789
825
  * `lecodes navmesh bake`: every STATIC solid body's triangles in world space, as an NGEO file at
790
826
  * `outPath`. `entityIds`/`areas` are per-entity overrides (area 0..15, 255 unwalkable, 254 skip).
791
- * Returns the triangle count (-1 = failed). Desktop hosts with physics only. */
827
+ * Returns the triangle count (-1 = failed). Desktop hosts with physics only. @ifdef CREATOR_GL_PHYSICS */
792
828
  physicsStaticGeometry?(outPath: string, entityIds: Uint32Array, areas: Uint8Array): i32
793
829
  // CharacterController (Jolt CharacterVirtual). The character steps on the engine's FIXED clock like
794
830
  // every body (frame-rate independent) and is render-interpolated; the engine owns its gravity, so
795
831
  // the SDK never ticks it. Both velocity halves are LATCHED STATE, never one-shot events — JS runs
796
832
  // once per frame while the sim runs 0..4 sub-steps, so a one-shot would double-apply or vanish.
797
833
  // groundState: 0 OnGround / 1 OnSteepGround / 2 NotSupported / 3 InAir.
834
+ /** @ifdef CREATOR_GL_PHYSICS */
798
835
  characterCreate(entityId: u32, shapeId: u32, maxSlopeDeg: f32): u32
836
+ /** @ifdef CREATOR_GL_PHYSICS */
799
837
  characterDestroy(charId: u32): void
800
838
  /** This frame's HORIZONTAL command (world units/s), cleared once a step consumes it — no command
801
839
  * means standing still, not coasting. Held across the frame's sub-steps, and it takes the axis
802
- * back from a latched velocity: the last writer owns X/Z. */
840
+ * back from a latched velocity: the last writer owns X/Z. @ifdef CREATOR_GL_PHYSICS */
803
841
  characterMove(charId: u32, x: f32, z: f32): void
804
- /** The same command including the vertical — free mode (gravityScale 0): swimming / flying. */
842
+ /** The same command including the vertical — free mode (gravityScale 0): swimming / flying. @ifdef CREATOR_GL_PHYSICS */
805
843
  characterMoveFree(charId: u32, x: f32, y: f32, z: f32): void
806
- /** Seed the LATCHED ballistic vertical (jump / dash). No ground check. */
844
+ /** Seed the LATCHED ballistic vertical (jump / dash). No ground check. @ifdef CREATOR_GL_PHYSICS */
807
845
  characterSetVerticalVelocity(charId: u32, vy: f32): void
808
846
  /** Latch the whole velocity — it persists until a characterMove takes the axis back (knockback,
809
- * wall jump, launch pad, weightless flight). Gravity still acts on the vertical. */
847
+ * wall jump, launch pad, weightless flight). Gravity still acts on the vertical. @ifdef CREATOR_GL_PHYSICS */
810
848
  characterSetVelocity(charId: u32, x: f32, y: f32, z: f32): void
811
- /** Multiplier over the world gravity; 0 = free mode, which ALSO disables stick-to-floor + stairs. */
849
+ /** Multiplier over the world gravity; 0 = free mode, which ALSO disables stick-to-floor + stairs. @ifdef CREATOR_GL_PHYSICS */
812
850
  characterSetGravityScale(charId: u32, scale: f32): void
851
+ /** @ifdef CREATOR_GL_PHYSICS */
813
852
  characterSetMaxSlope(charId: u32, maxSlopeDeg: f32): void
814
853
  /** Swap the collider live (crouch / stand up), keeping the FEET planted. Returns false when the new
815
854
  * shape doesn't fit where the character stands — nothing changed, so the caller retries later and
816
- * that retry is an exact headroom test. Optional: a host without it can't resize a character. */
855
+ * that retry is an exact headroom test. Optional: a host without it can't resize a character. @ifdef CREATOR_GL_PHYSICS */
817
856
  characterSetShape?(charId: u32, shapeId: u32): boolean
818
- /** The velocity the solver ENDED UP with after the last step (post-collision), not the command. @into out 3 */
857
+ /** The velocity the solver ENDED UP with after the last step (post-collision), not the command. @into out 3 @ifdef CREATOR_GL_PHYSICS */
819
858
  characterGetVelocity(charId: u32, out: Float32Array): void
859
+ /** @ifdef CREATOR_GL_PHYSICS */
820
860
  characterGetGroundState(charId: u32): i32
821
- /** Discontinuous move (spawn / respawn / teleport) — also resets the interpolation pair. */
861
+ /** Discontinuous move (spawn / respawn / teleport) — also resets the interpolation pair. @ifdef CREATOR_GL_PHYSICS */
822
862
  characterSetPosition(charId: u32, x: f32, y: f32, z: f32): void
823
863
  // Vehicle (Jolt VehicleConstraint + WheeledVehicleController). One settings BLOB, so tuning knobs
824
864
  // never grow this ABI — layout (floats):
@@ -873,7 +913,9 @@ declare global {
873
913
  // wheel models itself from the state. Chassis space is forward -Z / up +Y (matching node.forward);
874
914
  // wheel positions are suspension attachment points in unscaled chassis space; `axle` pairs wheels
875
915
  // for the differentials + anti-roll bars.
916
+ /** @ifdef CREATOR_GL_PHYSICS */
876
917
  vehicleCreate(entityId: u32, shapeId: u32, settings: Float32Array): u32
918
+ /** @ifdef CREATOR_GL_PHYSICS */
877
919
  vehicleDestroy(vehicleId: u32): void
878
920
  /** ONE input vector, latched and applied once per fixed step: [throttle 0..1, steer −1..1 (a
879
921
  * fraction of the wheels' maxSteerDeg, applied as is; with a column, where the hands aim),
@@ -882,12 +924,12 @@ declare global {
882
924
  * neutral, negative = reverse — the engine never sees a gear list), clutch 0..1 (scales the clutch
883
925
  * in the coupled engine/wheel solve), brake_0 … brake_n (N·m of brake torque per wheel, this
884
926
  * step)]. A shorter vector leaves the rest as it was. Sticky; a non-zero throttle, steer or brake
885
- * wakes a sleeping car. */
927
+ * wakes a sleeping car. @ifdef CREATOR_GL_PHYSICS */
886
928
  vehicleSetInput(vehicleId: u32, input: Float32Array): void
887
929
  /** Re-apply the TUNABLE half of the blob (same layout) to a live car — differential, per-wheel tire
888
930
  * curves / traction / circle, engine torque/RPM/curve, clutch strength + capacity, steer lock, the
889
931
  * column, anti-roll stiffness, tilt limit. Structural values (mass, centre of mass, wheel geometry,
890
- * the driven shares, suspension, whether an axle has a bar) are ignored: those need a re-create. */
932
+ * the driven shares, suspension, whether an axle has a bar) are ignored: those need a re-create. @ifdef CREATOR_GL_PHYSICS */
891
933
  vehicleSetTuning?(vehicleId: u32, settings: Float32Array): void
892
934
  /** out = [speed, rpm, wheelsInContact, vx, vy, vz, wx, wy, wz, steerDeg, steerTorque] (w = chassis
893
935
  * angular velocity, rad/s; steerDeg = where the road wheels are, right-positive; steerTorque = the
@@ -898,12 +940,12 @@ declare global {
898
940
  * patch's sliding speeds, m/s, + = to the right / tread spinning up — force × speed is heat; load =
899
941
  * the suspension's force on the wheel, N). slipLong is SIGNED (+ spinning up, − locking) and so is
900
942
  * slipAngleDeg (+ the patch sliding to the tire's right). suspensionLength, steerDeg and spin are
901
- * what a wheel model's pose is built from — by the game, the SDK only exposes them. */
943
+ * what a wheel model's pose is built from — by the game, the SDK only exposes them. @ifdef CREATOR_GL_PHYSICS */
902
944
  vehicleGetState(vehicleId: u32, out: Float32Array): void
903
945
  /** Teleport upright and clear all motion (velocities, engine RPM, wheel spin); the input vector is
904
- * zeroed — the SDK re-sends its own on the next early pass. */
946
+ * zeroed — the SDK re-sends its own on the next early pass. @ifdef CREATOR_GL_PHYSICS */
905
947
  vehicleReset(vehicleId: u32, x: f32, y: f32, z: f32, qx: f32, qy: f32, qz: f32, qw: f32): void
906
- /** The chassis rigid body, for the plain body calls (physicsApplyImpulse, …). 0 if unknown. */
948
+ /** The chassis rigid body, for the plain body calls (physicsApplyImpulse, …). 0 if unknown. @ifdef CREATOR_GL_PHYSICS */
907
949
  vehicleBodyId(vehicleId: u32): u32
908
950
  // Ragdoll (Jolt Ragdoll): a Model's skeleton handed to physics — one dynamic body per listed bone
909
951
  // (a capsule from the bone's origin to the next joint) joined to its parent part by a swing-twist
@@ -949,24 +991,26 @@ declare global {
949
991
  // half-angle), twistDeg (half-angle), hinge (0/1), hingeAxisX/Y/Z (in the MODEL
950
992
  // node's space), hingeMinDeg, hingeMaxDeg — a hinge is a swing-twist whose cone is
951
993
  // flat (2°) across the axis and [min, max] about it (a knee, an elbow)
994
+ /** @ifdef CREATOR_GL_PHYSICS */
952
995
  ragdollCreate?(rootEntityId: u32, bones: Uint32Array, settings: Float32Array): u32
996
+ /** @ifdef CREATOR_GL_PHYSICS */
953
997
  ragdollDestroy?(ragdollId: u32): void
954
998
  /** Pose the bodies from the bones' current transforms and add them to the world, each moving as
955
999
  * its bone was measured over the last two frames, plus the launch (vx, vy, vz) on every body.
956
- * Already active = re-wake + the launch. false = unknown id / no world. */
1000
+ * Already active = re-wake + the launch. false = unknown id / no world. @ifdef CREATOR_GL_PHYSICS */
957
1001
  ragdollActivate?(ragdollId: u32, vx: f32, vy: f32, vz: f32): boolean
958
1002
  /** Take the bodies out of the world. The bones get the bodies' last pose written against the
959
1003
  * parents' CURRENT worlds (move the model node under the hips first), and with `blend` > 0 the
960
1004
  * model's animator is seeded with it: whatever plays next transitions from the fallen pose over
961
- * `blend` seconds. */
1005
+ * `blend` seconds. @ifdef CREATOR_GL_PHYSICS */
962
1006
  ragdollDeactivate?(ragdollId: u32, blend: f32): void
963
- /** In the world AND at least one body still awake (a settled ragdoll reads false). */
1007
+ /** In the world AND at least one body still awake (a settled ragdoll reads false). @ifdef CREATOR_GL_PHYSICS */
964
1008
  ragdollActive?(ragdollId: u32): boolean
965
- /** The rigid body of part `index` (for physicsApplyImpulseAt / velocities). 0 if unknown. */
1009
+ /** The rigid body of part `index` (for physicsApplyImpulseAt / velocities). 0 if unknown. @ifdef CREATOR_GL_PHYSICS */
966
1010
  ragdollBodyId?(ragdollId: u32, index: u32): u32
967
1011
  /** The joint motors' strength (0..1 = the share of driveTorque; 0 = off = limp) and the root
968
1012
  * anchor (the hips body kinematic on the animated hips). Kept across activations; a driven or
969
- * anchored body never freezes. */
1013
+ * anchored body never freezes. @ifdef CREATOR_GL_PHYSICS */
970
1014
  ragdollSetDrive?(ragdollId: u32, strength: f32, anchor: boolean): void
971
1015
  // Dynamic bones (the SDK's DynamicBone; creator-anim canimDyn* behind creator-gl): secondary motion
972
1016
  // for tails, ears, hair and cloaks — a bone tree under one root simulated as Verlet particle chains
@@ -1027,23 +1071,31 @@ declare global {
1027
1071
  dynamicBoneColliderSet?(colliderId: u32, ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32, radius: f32, sided: f32): void
1028
1072
  dynamicBoneColliderDestroy?(colliderId: u32): void
1029
1073
  // Legacy coupled shape+body (still used by the worker RigidBody).
1074
+ /** @ifdef CREATOR_GL_PHYSICS */
1030
1075
  physicsCreateBox(entityId: u32, hx: f32, hy: f32, hz: f32, motionType: i32, mass: f32): u32
1076
+ /** @ifdef CREATOR_GL_PHYSICS */
1031
1077
  physicsCreateSphere(entityId: u32, radius: f32, motionType: i32, mass: f32): u32
1078
+ /** @ifdef CREATOR_GL_PHYSICS */
1032
1079
  physicsCreateCylinder(entityId: u32, halfHeight: f32, radius: f32, motionType: i32, mass: f32): u32
1080
+ /** @ifdef CREATOR_GL_PHYSICS */
1033
1081
  physicsSetLinearVelocity(bodyId: u32, x: f32, y: f32, z: f32): void
1034
- /** @into out 3 */
1082
+ /** @into out 3 @ifdef CREATOR_GL_PHYSICS */
1035
1083
  physicsGetLinearVelocity(bodyId: u32, out: Float32Array): void
1036
1084
  /** Angular velocity about each world axis, RADIANS/second — Jolt's unit; the SDK exposes degrees.
1037
- * The only way to stop a spin: a position write leaves both velocities untouched. */
1085
+ * The only way to stop a spin: a position write leaves both velocities untouched. @ifdef CREATOR_GL_PHYSICS */
1038
1086
  physicsSetAngularVelocity?(bodyId: u32, x: f32, y: f32, z: f32): void
1039
- /** @into out 3 */
1087
+ /** @into out 3 @ifdef CREATOR_GL_PHYSICS */
1040
1088
  physicsGetAngularVelocity?(bodyId: u32, out: Float32Array): void
1089
+ /** @ifdef CREATOR_GL_PHYSICS */
1041
1090
  physicsApplyImpulse(bodyId: u32, x: f32, y: f32, z: f32): void
1091
+ /** @ifdef CREATOR_GL_PHYSICS */
1042
1092
  physicsApplyImpulseAt?(bodyId: u32, x: f32, y: f32, z: f32, px: f32, py: f32, pz: f32): void
1093
+ /** @ifdef CREATOR_GL_PHYSICS */
1043
1094
  physicsSetBodyPosition(bodyId: u32, x: f32, y: f32, z: f32): void
1044
1095
  /** The rotation twin (normalized host-side). Both snap the body's render-interpolation pair, so a
1045
- * discontinuous move is drawn as one rather than as a one-frame slide/spin across the gap. */
1096
+ * discontinuous move is drawn as one rather than as a one-frame slide/spin across the gap. @ifdef CREATOR_GL_PHYSICS */
1046
1097
  physicsSetBodyRotation?(bodyId: u32, qx: f32, qy: f32, qz: f32, qw: f32): void
1098
+ /** @ifdef CREATOR_GL_PHYSICS */
1047
1099
  physicsRemoveBody(bodyId: u32): void
1048
1100
 
1049
1101
  /** @custom */
package/src/gl/Scene.ts CHANGED
@@ -19,22 +19,12 @@ import { Camera } from "./Camera"
19
19
  import { SceneAudio } from "./audio/SceneAudio"
20
20
  import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
21
21
  import { Material } from "./Material"
22
+ import { PostProcessing, type PostProcessingOptions } from "./postProcessing"
22
23
  import { Texture } from "./Texture"
23
24
  import { Node, nodes } from "./Node"
24
25
  import { glState } from "./state"
25
26
  import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
26
27
 
27
- export type AmbientOcclusionOptions = {
28
- /** Strength of the darkening (default 1). */
29
- intensity?: number
30
- /** How far the occlusion reaches, in metres (default 0.3). */
31
- radius?: number
32
- /** Falloff contrast; >1 tightens it into the crease (default 1). */
33
- power?: number
34
- /** Sample count + filtering (default 'medium'). Not the buffer resolution — that stays half. */
35
- quality?: "low" | "medium" | "high" | "ultra"
36
- }
37
-
38
28
  /** `SceneOptions.taa` / `scene.setAntialias("taa", 4, taa)`: the temporal anti-aliasing knobs. */
39
29
  export type TaaOptions = {
40
30
  /** Render the 3D at this fraction (0.5–1) and TAA-upscale it to the viewport; 1 = none. */
@@ -92,14 +82,10 @@ export type SceneOptions = {
92
82
  * rather than inflating the lights: scaling lamps past what they physically emit gives bright
93
83
  * fixtures in a black room, because it changes the RATIO, not the level. */
94
84
  exposureCompensation?: number
95
- /** Bloom post-processing. */
96
- bloom?: boolean
97
- bloomIntensity?: number
98
- /** Tone mapping operator. `'aces'` (default, filament's ACES legacy) desaturates bright colours
99
- * towards white — HDR fire reads pale; `'neutral'` (Khronos PBR Neutral) keeps hue and
100
- * saturation until very bright; `'linear'` clips each channel (what an engine without a
101
- * tonemapper shows — saturated, Unity-without-post-processing look); `'filmic'` (Uncharted). */
102
- toneMapping?: "aces" | "neutral" | "linear" | "filmic"
85
+ /** The look: tone mapping, colour grading, a `.cube` LUT, bloom, vignette, ambient occlusion,
86
+ * screen-space reflections, depth of field — as one object (`scene.postProcessing` after the
87
+ * fact; every field is live, postProcessing.ts says which are free to animate). */
88
+ postProcessing?: PostProcessingOptions
103
89
  /** The sky. Three forms:
104
90
  * - a colour — a flat clear colour;
105
91
  * - `{ texture }` — a KTX1 **cubemap**, the sharp `<name>_skybox.ktx` that filament's `cmgen`
@@ -112,12 +98,6 @@ export type SceneOptions = {
112
98
  * space after the opaque queue. No geometry, no meridian seam, no pole distortion — do NOT build
113
99
  * a sky dome or a fullscreen equirect material by hand, both are strictly worse. */
114
100
  skybox?: ColorInput | "environment" | { texture: string }
115
- /** Screen-space ambient occlusion — the contact darkening in creases and where props meet the
116
- * ground. Without it an IBL lights a crease exactly as brightly as an open face, so everything
117
- * reads as pasted onto the floor rather than standing on it. `true` takes defaults tuned for
118
- * human-scale props; `radius` is world-space metres and is the one knob that must follow the
119
- * scene's scale (~0.3 for objects on a table, ~0.6-1 for a yard of crates and containers). */
120
- ambientOcclusion?: boolean | AmbientOcclusionOptions
121
101
  /** Distance fog / aerial perspective: distant geometry loses contrast so the eye reads depth, and
122
102
  * the hard edge where a finite level ends against the skybox goes away. By default the fog applies
123
103
  * at every distance — the skybox included — so the sky itself takes the fog colour and the horizon
@@ -215,6 +195,14 @@ export class Scene implements Presentable {
215
195
  readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node | null>) => void> = []
216
196
 
217
197
  private _material?: Material
198
+ private readonly _postProcessing: PostProcessing
199
+ /** The scene's post-processing (postProcessing.ts): a live object — write a field
200
+ * (`scene.postProcessing.contrast = 1.1`), or `apply({ ... })` several at once (named fields
201
+ * only), `reset()` for the defaults. Read-only: there is nothing to assign. An overlay's grading
202
+ * is its parent's; its bloom, vignette and the rest are its own. */
203
+ get postProcessing(): PostProcessing { return this._postProcessing }
204
+ /** An explicit throw, not a bare getter: a bundle evaluated in sloppy mode would swallow the assignment silently. */
205
+ set postProcessing(_: never) { throw new Error("scene.postProcessing is read-only: write a field, apply({ ... }) or reset()") }
218
206
 
219
207
  private static _active: Scene | null = null
220
208
  static get active(): Scene | null { return Scene._active }
@@ -245,12 +233,9 @@ export class Scene implements Presentable {
245
233
  }
246
234
  this._environmentIntensity = options.ibl === false ? 0 : (options.environmentIntensity ?? 20000)
247
235
  if (options.exposureCompensation !== undefined) this.camera.exposureCompensation = options.exposureCompensation
248
- if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
249
- if (options.toneMapping !== undefined && options.toneMapping !== "aces") {
250
- _creator.setToneMapping?.(this._id, { neutral: 1, linear: 2, filmic: 3 }[options.toneMapping] ?? 0)
251
- }
236
+ this._postProcessing = new PostProcessing(this._id)
237
+ if (options.postProcessing) this._postProcessing.apply(options.postProcessing)
252
238
  if (options.skybox !== undefined) this.skybox = options.skybox
253
- if (options.ambientOcclusion) this.setAmbientOcclusion(options.ambientOcclusion)
254
239
  if (options.fog) this.setFog(options.fog)
255
240
  if (options.antialias !== undefined) {
256
241
  const a = options.antialias
@@ -302,12 +287,6 @@ export class Scene implements Presentable {
302
287
  _creator.setSkybox(this._id, Color.toPackedRgb(sky))
303
288
  }
304
289
 
305
- /** Runtime form of `ambientOcclusion` (a graphics-settings menu). `false` turns it off. */
306
- setAmbientOcclusion(options: boolean | AmbientOcclusionOptions): void {
307
- const o: AmbientOcclusionOptions = typeof options === "object" ? options : {}
308
- const quality = { low: 0, medium: 1, high: 2, ultra: 3 }[o.quality ?? "medium"] ?? 1
309
- _creator.setAmbientOcclusionOptions?.(this._id, options !== false, o.intensity ?? 1, o.radius ?? 0.3, o.power ?? 1, quality)
310
- }
311
290
 
312
291
  /** Runtime form of `fog` (weather, entering a building). `false` turns it off. */
313
292
  setFog(options: FogOptions | false): void {
@@ -349,10 +328,6 @@ export class Scene implements Presentable {
349
328
  setLodBias(bias: number): void {
350
329
  _creator.setLodBias?.(bias)
351
330
  }
352
- /** Runtime form of `bloom` / `bloomIntensity` (a graphics-settings menu). */
353
- setBloom(enabled: boolean, intensity = 0.2): void {
354
- _creator.setBloomOptions(this._id, enabled, intensity, 1)
355
- }
356
331
  /**
357
332
  * How bright the environment (IBL) lights the scene, in lux — `SceneOptions.environmentIntensity`
358
333
  * after the fact. Live: it changes the probe's intensity, not the probe, so it costs nothing and