@waica/engine 0.24.0 → 0.26.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 (129) hide show
  1. package/README.md +79 -1
  2. package/dist/anchored-pieces.d.ts +3 -2
  3. package/dist/anchored-pieces.js +12 -8
  4. package/dist/archetype.d.ts +2 -2
  5. package/dist/assets/asset-loader.d.ts +28 -5
  6. package/dist/assets/asset-loader.js +63 -7
  7. package/dist/assets/model-backend.d.ts +21 -0
  8. package/dist/assets/model-backend.js +21 -0
  9. package/dist/assets/model-cache.d.ts +47 -0
  10. package/dist/assets/model-cache.js +117 -0
  11. package/dist/audio/audio-subsystem.d.ts +6 -1
  12. package/dist/audio/audio-subsystem.js +8 -15
  13. package/dist/audio/placement.d.ts +41 -0
  14. package/dist/audio/placement.js +21 -0
  15. package/dist/audio/spatial.d.ts +20 -0
  16. package/dist/audio/spatial.js +26 -0
  17. package/dist/audio/types.d.ts +7 -1
  18. package/dist/camera-projection.d.ts +38 -0
  19. package/dist/camera-projection.js +44 -0
  20. package/dist/camera-rig.d.ts +48 -0
  21. package/dist/camera-rig.js +98 -0
  22. package/dist/camera.d.ts +20 -3
  23. package/dist/camera.js +3 -1
  24. package/dist/component.d.ts +38 -2
  25. package/dist/component.js +3 -0
  26. package/dist/components/animated-sprite.d.ts +8 -1
  27. package/dist/components/animated-sprite.js +16 -1
  28. package/dist/components/collider.d.ts +89 -0
  29. package/dist/components/collider.js +57 -0
  30. package/dist/components/dynamic-body.d.ts +2 -1
  31. package/dist/components/dynamic-body.js +1 -0
  32. package/dist/components/hitbox.d.ts +2 -1
  33. package/dist/components/hitbox.js +1 -0
  34. package/dist/components/light.d.ts +69 -0
  35. package/dist/components/light.js +60 -0
  36. package/dist/components/model.d.ts +63 -0
  37. package/dist/components/model.js +117 -0
  38. package/dist/components/particle-emitter.d.ts +8 -1
  39. package/dist/components/particle-emitter.js +14 -0
  40. package/dist/components/point-light.d.ts +75 -0
  41. package/dist/components/point-light.js +102 -0
  42. package/dist/components/rigid-body.d.ts +114 -0
  43. package/dist/components/rigid-body.js +83 -0
  44. package/dist/components/solid.d.ts +2 -1
  45. package/dist/components/solid.js +1 -0
  46. package/dist/components/sprite.d.ts +8 -1
  47. package/dist/components/sprite.js +16 -1
  48. package/dist/components/sun.d.ts +46 -0
  49. package/dist/components/sun.js +74 -0
  50. package/dist/components/tilemap.d.ts +2 -1
  51. package/dist/components/tilemap.js +7 -0
  52. package/dist/entity-transform.d.ts +18 -0
  53. package/dist/entity-transform.js +17 -0
  54. package/dist/frame-quad.d.ts +21 -0
  55. package/dist/frame-quad.js +50 -0
  56. package/dist/game.d.ts +59 -15
  57. package/dist/game.js +100 -75
  58. package/dist/index.d.ts +39 -4
  59. package/dist/index.js +18 -1
  60. package/dist/light-field.d.ts +78 -0
  61. package/dist/light-field.js +145 -0
  62. package/dist/light-map-material.d.ts +40 -0
  63. package/dist/light-map-material.js +203 -0
  64. package/dist/light-map.d.ts +46 -0
  65. package/dist/light-map.js +132 -0
  66. package/dist/lit-frame.d.ts +48 -0
  67. package/dist/lit-frame.js +174 -0
  68. package/dist/occluder-grid.d.ts +19 -0
  69. package/dist/occluder-grid.js +50 -0
  70. package/dist/particle-batch.js +3 -2
  71. package/dist/physics-3d/body-contacts.d.ts +10 -0
  72. package/dist/physics-3d/body-contacts.js +59 -0
  73. package/dist/physics-3d/body-sync.d.ts +52 -0
  74. package/dist/physics-3d/body-sync.js +153 -0
  75. package/dist/physics-3d/character-controller.d.ts +44 -0
  76. package/dist/physics-3d/character-controller.js +126 -0
  77. package/dist/physics-3d/physics-host.d.ts +86 -0
  78. package/dist/physics-3d/physics-host.js +174 -0
  79. package/dist/physics-3d/physics-world.d.ts +65 -0
  80. package/dist/physics-3d/physics-world.js +158 -0
  81. package/dist/physics-3d/rapier-module.d.ts +17 -0
  82. package/dist/physics-3d/rapier-module.js +19 -0
  83. package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
  84. package/dist/physics-3d/sensor-dispatch.js +77 -0
  85. package/dist/physics-3d/world-queries.d.ts +39 -0
  86. package/dist/physics-3d/world-queries.js +63 -0
  87. package/dist/pointer-raycast.d.ts +17 -0
  88. package/dist/pointer-raycast.js +48 -0
  89. package/dist/pointer.d.ts +27 -6
  90. package/dist/pointer.js +43 -17
  91. package/dist/post-effects.d.ts +33 -0
  92. package/dist/post-effects.js +48 -0
  93. package/dist/post-pass.d.ts +23 -0
  94. package/dist/post-pass.js +93 -0
  95. package/dist/render-layers.d.ts +18 -0
  96. package/dist/render-layers.js +19 -0
  97. package/dist/render-output.d.ts +7 -0
  98. package/dist/render-output.js +17 -1
  99. package/dist/runtime-bridge.d.ts +1 -1
  100. package/dist/runtime-inspection.d.ts +14 -0
  101. package/dist/runtime-inspection.js +12 -2
  102. package/dist/runtime-lighting-snapshot.d.ts +67 -0
  103. package/dist/runtime-lighting-snapshot.js +77 -0
  104. package/dist/runtime-physics-snapshot.d.ts +41 -0
  105. package/dist/runtime-physics-snapshot.js +45 -0
  106. package/dist/runtime-view-snapshot.d.ts +25 -0
  107. package/dist/runtime-view-snapshot.js +30 -0
  108. package/dist/scene-camera-3d.d.ts +66 -0
  109. package/dist/scene-camera-3d.js +117 -0
  110. package/dist/scene-lighting.d.ts +52 -0
  111. package/dist/scene-lighting.js +107 -0
  112. package/dist/scene-lights-3d.d.ts +17 -0
  113. package/dist/scene-lights-3d.js +27 -0
  114. package/dist/scene-render-options.d.ts +51 -0
  115. package/dist/scene-render-options.js +126 -0
  116. package/dist/scene-simulation.d.ts +22 -0
  117. package/dist/scene-simulation.js +29 -0
  118. package/dist/scene-space.d.ts +50 -0
  119. package/dist/scene-space.js +103 -0
  120. package/dist/scene.d.ts +41 -2
  121. package/dist/scene.js +11 -4
  122. package/dist/simulation-phases.d.ts +9 -0
  123. package/dist/simulation-phases.js +19 -0
  124. package/dist/spatial-query-3d.d.ts +48 -0
  125. package/dist/spatial-query-3d.js +96 -0
  126. package/dist/spatial-query.d.ts +18 -4
  127. package/dist/spatial-query.js +57 -1
  128. package/dist/sprite-batch.js +5 -1
  129. package/package.json +2 -1
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export type { GameOptions, GameResolution, SceneCatalog, SpawnPrefabOptions, Upd
3
3
  export { AssetLoader } from './assets/asset-loader.js';
4
4
  export type { AssetStatus } from './assets/asset-loader.js';
5
5
  export type { TextureBackend } from './assets/texture-backend.js';
6
+ export { GltfModelBackend } from './assets/model-backend.js';
7
+ export type { LoadedModel, ModelBackend } from './assets/model-backend.js';
8
+ export type { ModelHandle, ModelOutcome } from './assets/model-cache.js';
6
9
  export { AudioSubsystem } from './audio/audio-subsystem.js';
7
10
  export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
8
11
  export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
@@ -21,11 +24,20 @@ export { CameraEffects } from './camera-effects.js';
21
24
  export type { CameraEffectColor, CameraEffectHandle, CameraEffectsOptions, CameraEffectsState, FadeOptions, FlashOptions, ShakeOptions, } from './camera-effects.js';
22
25
  export { GameTime, advanceGameTime } from './game-time.js';
23
26
  export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game-time.js';
24
- export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
27
+ export type { SceneCameraJson, OrthographicSceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
28
+ export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
29
+ export type { PerspectiveSceneCameraJson, ResolvedPerspectiveCamera, SceneFieldIssue, Vec3Json, } from './scene-camera-3d.js';
30
+ export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
31
+ export type { GameCamera, NormalizedPoint, ViewCamera, WorldPoint } from './camera-projection.js';
32
+ export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
33
+ export type { SceneSpace, SpaceMarked } from './scene-space.js';
34
+ export type { RuntimeSnapshotView } from './runtime-view-snapshot.js';
35
+ export { applyTransformJson } from './entity-transform.js';
36
+ export type { PositionJson, TransformJson } from './entity-transform.js';
25
37
  export { Entity } from './entity.js';
26
- export { Component } from './component.js';
38
+ export { COMPONENT_SPACES, Component } from './component.js';
27
39
  export { authoringDefaults } from './authoring-defaults.js';
28
- export type { ComponentClass, ContactNormal, ParamSpec, SolidContact, } from './component.js';
40
+ export type { BodyContact, ComponentClass, ComponentSpace, ContactNormal, ParamSpec, SolidContact, } from './component.js';
29
41
  export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
30
42
  export type { ComponentModule } from './component-registry.js';
31
43
  export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
@@ -40,6 +52,7 @@ export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_B
40
52
  export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
41
53
  export type { RenderBackend } from './render-readiness.js';
42
54
  export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
55
+ export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
43
56
  export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
44
57
  export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
45
58
  export { Stats } from './stats.js';
@@ -47,6 +60,27 @@ export type { StatValue } from './stats.js';
47
60
  export { GameUi } from './ui.js';
48
61
  export type { AnchoredPieceHandle, AttachOptions } from './anchored-pieces.js';
49
62
  export { Sprite } from './components/sprite.js';
63
+ export { Light } from './components/light.js';
64
+ export { Model } from './components/model.js';
65
+ export type { ModelShape } from './components/model.js';
66
+ export { Sun } from './components/sun.js';
67
+ export { Collider, COLLIDER_SHAPES } from './components/collider.js';
68
+ export type { ColliderShape } from './components/collider.js';
69
+ export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
70
+ export type { BodyVector, RigidBodyType } from './components/rigid-body.js';
71
+ export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
72
+ export type { ResolvedSceneSimulation, SceneSimulationJson } from './scene-simulation.js';
73
+ export type { PhysicsBackend, RapierModule } from './physics-3d/rapier-module.js';
74
+ export type { RuntimeSnapshotBody, RuntimeSnapshotPhysics } from './runtime-physics-snapshot.js';
75
+ export { PointLight } from './components/point-light.js';
76
+ export { GameLighting } from './scene-lighting.js';
77
+ export type { AmbientLightInput } from './scene-lighting.js';
78
+ export { GamePost } from './post-effects.js';
79
+ export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
80
+ export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
81
+ export type { LightField, OccluderGrid, Rgb } from './light-field.js';
82
+ export { sceneRenderIssues } from './scene-render-options.js';
83
+ export type { AmbientLight, ColorGradeEffect, PostEffectsState, SceneRenderIssue, VignetteEffect, } from './scene-render-options.js';
50
84
  export { ParticleEmitter } from './components/particle-emitter.js';
51
85
  export type { ParticleBlend, ParticleDestroyMode, ParticleOverflow, ParticleSpace, ParticleVector, } from './components/particle-emitter.js';
52
86
  export { Solid } from './components/solid.js';
@@ -64,9 +98,10 @@ export type { TilemapCell, TilemapCellBounds, TilemapGridSpec, } from './tilemap
64
98
  export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
65
99
  export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
66
100
  export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit, SpatialQuery, SpatialQueryFilter, } from './spatial-query.js';
101
+ export type { Point3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
67
102
  export { Emitter } from './events.js';
68
103
  export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
69
- export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, PrefabJson, } from './scene.js';
104
+ export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
70
105
  export { ClipPlayer } from './animation/clip-player.js';
71
106
  export type { ClipDef } from './animation/clip-player.js';
72
107
  export { sheetCell, sheetFrameCount, locateFrame } from './animation/sheet.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Game } from './game.js';
2
2
  export { AssetLoader } from './assets/asset-loader.js';
3
+ export { GltfModelBackend } from './assets/model-backend.js';
3
4
  export { AudioSubsystem } from './audio/audio-subsystem.js';
4
5
  export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
5
6
  export { isYSortBatchParticipant, isYSortParticipant, ySortZ } from './render-sort.js';
@@ -9,8 +10,12 @@ export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepScen
9
10
  export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
10
11
  export { CameraEffects } from './camera-effects.js';
11
12
  export { GameTime, advanceGameTime } from './game-time.js';
13
+ export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
14
+ export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
15
+ export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
16
+ export { applyTransformJson } from './entity-transform.js';
12
17
  export { Entity } from './entity.js';
13
- export { Component } from './component.js';
18
+ export { COMPONENT_SPACES, Component } from './component.js';
14
19
  export { authoringDefaults } from './authoring-defaults.js';
15
20
  export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
16
21
  export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
@@ -22,6 +27,18 @@ export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
22
27
  export { Stats } from './stats.js';
23
28
  export { GameUi } from './ui.js';
24
29
  export { Sprite } from './components/sprite.js';
30
+ export { Light } from './components/light.js';
31
+ export { Model } from './components/model.js';
32
+ export { Sun } from './components/sun.js';
33
+ export { Collider, COLLIDER_SHAPES } from './components/collider.js';
34
+ export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
35
+ export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
36
+ export { PointLight } from './components/point-light.js';
37
+ export { GameLighting } from './scene-lighting.js';
38
+ export { GamePost } from './post-effects.js';
39
+ export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
40
+ export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
41
+ export { sceneRenderIssues } from './scene-render-options.js';
25
42
  export { ParticleEmitter } from './components/particle-emitter.js';
26
43
  export { Solid } from './components/solid.js';
27
44
  export { Hitbox } from './components/hitbox.js';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The CPU reference of a light-map texel (issue #78 CA-5..CA-7): what the
3
+ * light shader computes per texel, written once in plain TypeScript so it
4
+ * can be tested. The shader in `light-map-material.ts` follows it step by
5
+ * step; this file is the specification both are read against.
6
+ */
7
+ /** A light-map value or a light color: three multipliers in 0..1 over the sRGB frame. */
8
+ export type Rgb = [number, number, number];
9
+ /** One Light as the light-map sees it: logical position and radius, its look, its shadow. */
10
+ export interface LightField {
11
+ x: number;
12
+ y: number;
13
+ /** Logical units: the same distance in every grid direction, also on an isometric screen. */
14
+ radius: number;
15
+ color: Rgb;
16
+ intensity: number;
17
+ /** 0 = smooth falloff; N > 0 = N equal steps. */
18
+ bands: number;
19
+ /** 0 = hard shadow edge; up to 1 = the widest penumbra. */
20
+ softness: number;
21
+ castShadows: boolean;
22
+ }
23
+ /**
24
+ * Every Tilemap's solid tiles rasterized onto one logical grid: `solid` holds
25
+ * one byte per cell, row-major, row 0 at `originY` (rows grow with y).
26
+ */
27
+ export interface OccluderGrid {
28
+ originX: number;
29
+ originY: number;
30
+ cellSize: number;
31
+ columns: number;
32
+ rows: number;
33
+ solid: Uint8Array;
34
+ }
35
+ /** A logical point. */
36
+ export interface LogicalPoint {
37
+ x: number;
38
+ y: number;
39
+ }
40
+ /** What every light-map texel of a frame is computed from. */
41
+ export interface LightScene {
42
+ ambient: Rgb;
43
+ lights: readonly LightField[];
44
+ grid: OccluderGrid | null;
45
+ }
46
+ /**
47
+ * Brightness at `t` = distance / radius: 1 at the centre, a smooth ease to 0
48
+ * at the radius and nothing beyond. With `bands` > 0 every lit point takes
49
+ * the top of its step, so the outermost band still reaches the radius.
50
+ */
51
+ export declare function lightFalloff(t: number, bands: number): number;
52
+ /** The render-space box a light covers: a circle's square, or the 2:1 ellipse's box on an isometric screen. */
53
+ export declare function lightFootprint(radius: number, projection: 'isometric' | null): {
54
+ width: number;
55
+ height: number;
56
+ };
57
+ /**
58
+ * Whether the segment from a texel to a light crosses no solid cell other
59
+ * than the texel's own (the lit face of a wall, CA-7). A grid walk
60
+ * (Amanatides–Woo) over every cell the segment enters, the light's included.
61
+ */
62
+ export declare function segmentClear(grid: OccluderGrid, from: LogicalPoint, to: LogicalPoint): boolean;
63
+ /**
64
+ * How far the segment from a texel to a light travels inside solid cells,
65
+ * in logical units, never counting the texel's own cell (its lit face): the
66
+ * same grid walk as `segmentClear`, measuring instead of stopping.
67
+ */
68
+ export declare function segmentPenetration(grid: OccluderGrid, from: LogicalPoint, to: LogicalPoint): number;
69
+ /**
70
+ * How much of a light reaches a texel. Hard (softness 0): 1 or 0. Soft: one
71
+ * walk measures how deep the ray runs through solid cells, and the light
72
+ * fades over the first `softness` cells of it — a ray grazing a wall's
73
+ * corner keeps most of its light, one through the wall none — so the edge
74
+ * is a single transition that widens away from the occluder (PR #150 review).
75
+ */
76
+ export declare function lightVisibility(grid: OccluderGrid | null, light: LightField, point: LogicalPoint): number;
77
+ /** A light-map texel at a logical point: the Ambient Light plus every light's term, clamped per channel. */
78
+ export declare function lightMapValue({ ambient, lights, grid }: LightScene, point: LogicalPoint): Rgb;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The CPU reference of a light-map texel (issue #78 CA-5..CA-7): what the
3
+ * light shader computes per texel, written once in plain TypeScript so it
4
+ * can be tested. The shader in `light-map-material.ts` follows it step by
5
+ * step; this file is the specification both are read against.
6
+ */
7
+ /**
8
+ * Brightness at `t` = distance / radius: 1 at the centre, a smooth ease to 0
9
+ * at the radius and nothing beyond. With `bands` > 0 every lit point takes
10
+ * the top of its step, so the outermost band still reaches the radius.
11
+ */
12
+ export function lightFalloff(t, bands) {
13
+ if (!(t < 1))
14
+ return 0;
15
+ const clamped = Math.max(0, t);
16
+ const smooth = 1 - clamped * clamped * (3 - 2 * clamped);
17
+ if (bands <= 0)
18
+ return smooth;
19
+ return Math.ceil(smooth * bands) / bands;
20
+ }
21
+ /** The render-space box a light covers: a circle's square, or the 2:1 ellipse's box on an isometric screen. */
22
+ export function lightFootprint(radius, projection) {
23
+ if (projection === 'isometric')
24
+ return { width: 2 * Math.SQRT2 * radius, height: Math.SQRT2 * radius };
25
+ return { width: 2 * radius, height: 2 * radius };
26
+ }
27
+ function solidAt(grid, column, row) {
28
+ if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows)
29
+ return false;
30
+ return grid.solid[row * grid.columns + column] === 1;
31
+ }
32
+ /** The ray parameter of the first cell boundary on one axis, and how much it grows per cell. */
33
+ function axisCrossing(start, delta, cell) {
34
+ if (delta === 0)
35
+ return { next: Number.POSITIVE_INFINITY, step: Number.POSITIVE_INFINITY };
36
+ const step = Math.abs(1 / delta);
37
+ const toBoundary = delta > 0 ? cell + 1 - start : start - cell;
38
+ return { next: toBoundary * step, step };
39
+ }
40
+ /**
41
+ * Whether the segment from a texel to a light crosses no solid cell other
42
+ * than the texel's own (the lit face of a wall, CA-7). A grid walk
43
+ * (Amanatides–Woo) over every cell the segment enters, the light's included.
44
+ */
45
+ export function segmentClear(grid, from, to) {
46
+ const ax = (from.x - grid.originX) / grid.cellSize;
47
+ const ay = (from.y - grid.originY) / grid.cellSize;
48
+ const bx = (to.x - grid.originX) / grid.cellSize;
49
+ const by = (to.y - grid.originY) / grid.cellSize;
50
+ const endColumn = Math.floor(bx);
51
+ const endRow = Math.floor(by);
52
+ const walk = { column: Math.floor(ax), row: Math.floor(ay), nextX: 0, nextY: 0 };
53
+ const x = axisCrossing(ax, bx - ax, walk.column);
54
+ const y = axisCrossing(ay, by - ay, walk.row);
55
+ walk.nextX = x.next;
56
+ walk.nextY = y.next;
57
+ const stepColumn = Math.sign(bx - ax);
58
+ const stepRow = Math.sign(by - ay);
59
+ const cells = Math.abs(endColumn - walk.column) + Math.abs(endRow - walk.row);
60
+ for (let index = 0; index < cells; index += 1) {
61
+ if (walk.nextX < walk.nextY) {
62
+ walk.column += stepColumn;
63
+ walk.nextX += x.step;
64
+ }
65
+ else {
66
+ walk.row += stepRow;
67
+ walk.nextY += y.step;
68
+ }
69
+ if (solidAt(grid, walk.column, walk.row))
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ /**
75
+ * How far the segment from a texel to a light travels inside solid cells,
76
+ * in logical units, never counting the texel's own cell (its lit face): the
77
+ * same grid walk as `segmentClear`, measuring instead of stopping.
78
+ */
79
+ export function segmentPenetration(grid, from, to) {
80
+ const ax = (from.x - grid.originX) / grid.cellSize;
81
+ const ay = (from.y - grid.originY) / grid.cellSize;
82
+ const bx = (to.x - grid.originX) / grid.cellSize;
83
+ const by = (to.y - grid.originY) / grid.cellSize;
84
+ const walk = { column: Math.floor(ax), row: Math.floor(ay), nextX: 0, nextY: 0 };
85
+ const x = axisCrossing(ax, bx - ax, walk.column);
86
+ const y = axisCrossing(ay, by - ay, walk.row);
87
+ walk.nextX = x.next;
88
+ walk.nextY = y.next;
89
+ const cells = Math.abs(Math.floor(bx) - walk.column) + Math.abs(Math.floor(by) - walk.row);
90
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
91
+ let inside = 0;
92
+ for (let index = 0; index < cells; index += 1) {
93
+ let entry;
94
+ if (walk.nextX < walk.nextY) {
95
+ entry = walk.nextX;
96
+ walk.column += Math.sign(bx - ax);
97
+ walk.nextX += x.step;
98
+ }
99
+ else {
100
+ entry = walk.nextY;
101
+ walk.row += Math.sign(by - ay);
102
+ walk.nextY += y.step;
103
+ }
104
+ if (solidAt(grid, walk.column, walk.row))
105
+ inside += (Math.min(walk.nextX, walk.nextY, 1) - entry) * length;
106
+ }
107
+ return inside;
108
+ }
109
+ /**
110
+ * How much of a light reaches a texel. Hard (softness 0): 1 or 0. Soft: one
111
+ * walk measures how deep the ray runs through solid cells, and the light
112
+ * fades over the first `softness` cells of it — a ray grazing a wall's
113
+ * corner keeps most of its light, one through the wall none — so the edge
114
+ * is a single transition that widens away from the occluder (PR #150 review).
115
+ */
116
+ export function lightVisibility(grid, light, point) {
117
+ if (!grid || !light.castShadows)
118
+ return 1;
119
+ if (light.softness <= 0)
120
+ return segmentClear(grid, point, light) ? 1 : 0;
121
+ const depth = Math.min(1, segmentPenetration(grid, point, light) / (light.softness * grid.cellSize));
122
+ return 1 - depth * depth * (3 - 2 * depth);
123
+ }
124
+ /** One light's own term at a logical point: color × intensity × falloff × visibility, never negative. */
125
+ function lightTerm(light, grid, point) {
126
+ const strength = Math.max(0, light.intensity);
127
+ const distance = Math.hypot(point.x - light.x, point.y - light.y);
128
+ const reach = light.radius > 0 ? lightFalloff(distance / light.radius, light.bands) : 0;
129
+ if (strength === 0 || reach === 0)
130
+ return [0, 0, 0];
131
+ const amount = strength * reach * lightVisibility(grid, light, point);
132
+ return [Math.max(0, light.color[0]) * amount, Math.max(0, light.color[1]) * amount, Math.max(0, light.color[2]) * amount];
133
+ }
134
+ const clampUnit = (value) => Math.min(1, Math.max(0, value));
135
+ /** A light-map texel at a logical point: the Ambient Light plus every light's term, clamped per channel. */
136
+ export function lightMapValue({ ambient, lights, grid }, point) {
137
+ const total = [ambient[0], ambient[1], ambient[2]];
138
+ for (const light of lights) {
139
+ const [r, g, b] = lightTerm(light, grid, point);
140
+ total[0] += r;
141
+ total[1] += g;
142
+ total[2] += b;
143
+ }
144
+ return [clampUnit(total[0]), clampUnit(total[1]), clampUnit(total[2])];
145
+ }
@@ -0,0 +1,40 @@
1
+ import * as THREE from 'three/webgpu';
2
+ /** What one light-map mesh draws: its light, in logical units, with its look. */
3
+ export interface LightDraw {
4
+ readonly position: THREE.Vector2;
5
+ /** color × intensity, the term the light adds at its centre. */
6
+ readonly color: THREE.Vector3;
7
+ radius: number;
8
+ bands: number;
9
+ softness: number;
10
+ /** 1 casts shadows, 0 does not. */
11
+ castShadows: number;
12
+ }
13
+ /** Registers what a light-map mesh draws; the shared material reads it per object. */
14
+ export declare function setLightDraw(mesh: THREE.Object3D, draw: LightDraw): void;
15
+ /** The light-map's per-frame inputs every light reads: the projection and the occluder grid. */
16
+ export interface LightMapUniforms {
17
+ /** 1 for an isometric scene: render space is unprojected to logical space per texel. */
18
+ readonly isometric: THREE.UniformNode<'float', number>;
19
+ /** originX, originY, cellSize and 1 when a grid exists (0 casts no shadow at all). */
20
+ readonly grid: THREE.UniformNode<'vec4', THREE.Vector4>;
21
+ /** The grid's columns and rows. */
22
+ readonly gridSize: THREE.UniformNode<'vec2', THREE.Vector2>;
23
+ /** One byte per cell, row 0 first: 255 solid, 0 open. */
24
+ readonly occluders: THREE.TextureNode;
25
+ }
26
+ /** The light-map's shared uniforms, with an empty occluder grid. */
27
+ export declare function createLightMapUniforms(empty: THREE.Texture): LightMapUniforms;
28
+ /**
29
+ * The material every light-map mesh shares (issue #78 CA-5..CA-7): per texel,
30
+ * `color × intensity × falloff(d / radius) × visibility` with `d` measured in
31
+ * logical space (ADR 0009), added onto the Ambient Light the target was
32
+ * cleared to. light-field.ts is its specification.
33
+ */
34
+ export declare function createLightMaterial(uniforms: LightMapUniforms): THREE.MeshBasicNodeMaterial;
35
+ /**
36
+ * The quad that multiplies the light-map over the frame already drawn
37
+ * (ADR 0026): `frame × light`, alpha kept, on the sRGB values as stored —
38
+ * on the canvas, or in a Post Effect's target, which stores them alike.
39
+ */
40
+ export declare function createMultiplyMaterial(light: THREE.Texture): THREE.MeshBasicNodeMaterial;
@@ -0,0 +1,203 @@
1
+ import { abs, Break, ceil, clamp, float, floor, Fn, If, int, ivec2, length, Loop, max, min, mix, positionWorld, select, sign, texture, uniform, uv, vec2, vec4, } from 'three/tsl';
2
+ import * as THREE from 'three/webgpu';
3
+ const draws = new WeakMap();
4
+ /** Registers what a light-map mesh draws; the shared material reads it per object. */
5
+ export function setLightDraw(mesh, draw) {
6
+ draws.set(mesh, draw);
7
+ }
8
+ function drawOf(object) {
9
+ return object ? draws.get(object) : undefined;
10
+ }
11
+ /** The light-map's shared uniforms, with an empty occluder grid. */
12
+ export function createLightMapUniforms(empty) {
13
+ return {
14
+ isometric: uniform(0),
15
+ grid: uniform(new THREE.Vector4(0, 0, 1, 0)),
16
+ gridSize: uniform(new THREE.Vector2(1, 1)),
17
+ // An explicit UV: mapUvPerVertex (render-output.ts) leaves every other
18
+ // texture a per-vertex UV varying, one per load() here, past WebGPU's
19
+ // limit of 16 inter-stage variables. The grid is only ever load()ed.
20
+ occluders: texture(empty, vec2(0, 0)),
21
+ };
22
+ }
23
+ /** 1 when the grid cell holds a solid tile, 0 when it is open or outside the grid. */
24
+ function solidAtNode(uniforms, cell) {
25
+ const size = uniforms.gridSize;
26
+ const inside = cell.x.greaterThanEqual(0).and(cell.y.greaterThanEqual(0))
27
+ .and(cell.x.lessThan(size.x)).and(cell.y.lessThan(size.y));
28
+ const clamped = clamp(cell, vec2(0, 0), size.sub(1));
29
+ // load() follows the uniform's texture, so a rebuilt grid reaches the shader.
30
+ const value = uniforms.occluders.load(ivec2(int(clamped.x), int(clamped.y))).r;
31
+ return select(inside, value, float(0));
32
+ }
33
+ /** light-field.ts `axisCrossing`: the first boundary's ray parameter and its growth per cell. */
34
+ function axisCrossingNode(start, delta, cell) {
35
+ const flat = abs(delta).lessThan(1e-9);
36
+ const step = select(flat, float(1e30), float(1).div(max(abs(delta), 1e-9)));
37
+ const toBoundary = select(delta.greaterThan(0), cell.add(1).sub(start), start.sub(cell));
38
+ return { next: select(flat, float(1e30), toBoundary.mul(step)), step };
39
+ }
40
+ /**
41
+ * `segmentClear` of light-field.ts in TSL: walks the grid cells from the
42
+ * texel's to the light's (Amanatides–Woo) and returns 0 at the first solid
43
+ * one, never testing the texel's own cell — the lit face of a wall.
44
+ */
45
+ function segmentClearNode(uniforms) {
46
+ const walk = Fn(([from, to]) => {
47
+ const grid = uniforms.grid;
48
+ const a = from.sub(grid.xy).div(grid.z).toVar();
49
+ const b = to.sub(grid.xy).div(grid.z).toVar();
50
+ const cell = floor(a).toVar();
51
+ const end = floor(b);
52
+ const delta = b.sub(a).toVar();
53
+ const x = axisCrossingNode(a.x, delta.x, cell.x);
54
+ const y = axisCrossingNode(a.y, delta.y, cell.y);
55
+ const nextX = x.next.toVar();
56
+ const nextY = y.next.toVar();
57
+ const cells = min(abs(end.x.sub(cell.x)).add(abs(end.y.sub(cell.y))), 4096);
58
+ const clear = float(1).toVar();
59
+ Loop({ start: int(0), end: int(cells), type: 'int', condition: '<' }, () => {
60
+ If(nextX.lessThan(nextY), () => {
61
+ cell.x.addAssign(sign(delta.x));
62
+ nextX.addAssign(x.step);
63
+ }).Else(() => {
64
+ cell.y.addAssign(sign(delta.y));
65
+ nextY.addAssign(y.step);
66
+ });
67
+ If(solidAtNode(uniforms, cell).greaterThan(0.5), () => {
68
+ clear.assign(0);
69
+ Break();
70
+ });
71
+ });
72
+ return clear;
73
+ });
74
+ return (from, to) => walk(from, to);
75
+ }
76
+ /**
77
+ * `segmentPenetration` of light-field.ts in TSL: the same walk, summing in
78
+ * logical units how far the ray runs through solid cells, the texel's own
79
+ * cell never counted.
80
+ */
81
+ function segmentPenetrationNode(uniforms) {
82
+ const walk = Fn(([from, to]) => {
83
+ const grid = uniforms.grid;
84
+ const a = from.sub(grid.xy).div(grid.z).toVar();
85
+ const b = to.sub(grid.xy).div(grid.z).toVar();
86
+ const cell = floor(a).toVar();
87
+ const end = floor(b);
88
+ const delta = b.sub(a).toVar();
89
+ const x = axisCrossingNode(a.x, delta.x, cell.x);
90
+ const y = axisCrossingNode(a.y, delta.y, cell.y);
91
+ const nextX = x.next.toVar();
92
+ const nextY = y.next.toVar();
93
+ const entry = float(0).toVar();
94
+ const cells = min(abs(end.x.sub(cell.x)).add(abs(end.y.sub(cell.y))), 4096);
95
+ const length = to.sub(from).length();
96
+ const inside = float(0).toVar();
97
+ Loop({ start: int(0), end: int(cells), type: 'int', condition: '<' }, () => {
98
+ If(nextX.lessThan(nextY), () => {
99
+ entry.assign(nextX);
100
+ cell.x.addAssign(sign(delta.x));
101
+ nextX.addAssign(x.step);
102
+ }).Else(() => {
103
+ entry.assign(nextY);
104
+ cell.y.addAssign(sign(delta.y));
105
+ nextY.addAssign(y.step);
106
+ });
107
+ const exit = min(min(nextX, nextY), 1);
108
+ inside.addAssign(solidAtNode(uniforms, cell).mul(exit.sub(entry)).mul(length));
109
+ });
110
+ return inside;
111
+ });
112
+ return (from, to) => walk(from, to);
113
+ }
114
+ /** One light's per-object uniforms, read from the mesh being drawn. */
115
+ function lightUniforms() {
116
+ return {
117
+ position: uniform(new THREE.Vector2()).onObjectUpdate(({ object }) => drawOf(object)?.position),
118
+ color: uniform(new THREE.Vector3()).onObjectUpdate(({ object }) => drawOf(object)?.color),
119
+ radius: uniform(1).onObjectUpdate(({ object }) => drawOf(object)?.radius),
120
+ bands: uniform(0).onObjectUpdate(({ object }) => drawOf(object)?.bands),
121
+ softness: uniform(0).onObjectUpdate(({ object }) => drawOf(object)?.softness),
122
+ castShadows: uniform(1).onObjectUpdate(({ object }) => drawOf(object)?.castShadows),
123
+ };
124
+ }
125
+ /** The texel's logical position: render space as is, or unprojected from the 2:1 lattice (ADR 0009). */
126
+ function logicalPositionNode(isometric) {
127
+ const world = positionWorld.xy;
128
+ const unprojected = vec2(world.x.div(2).sub(world.y), world.x.div(2).negate().sub(world.y));
129
+ return mix(world, unprojected, isometric);
130
+ }
131
+ /** light-field.ts `lightFalloff` at `t` = d / radius: smooth, or the top of its band. */
132
+ function falloffNode(t, bands) {
133
+ const c = clamp(t, 0, 1);
134
+ const smooth = float(1).sub(c.mul(c).mul(float(3).sub(c.mul(2))));
135
+ const banded = ceil(smooth.mul(bands)).div(max(bands, 1));
136
+ return select(t.lessThan(1), select(bands.greaterThan(0.5), banded, smooth), float(0));
137
+ }
138
+ /** light-field.ts `lightVisibility`: hard, the clear walk; soft, a smoothstep over how deep the ray runs through walls. */
139
+ function visibilityNode(uniforms, light, logical) {
140
+ const visibility = float(1).toVar();
141
+ If(light.castShadows.greaterThan(0.5).and(uniforms.grid.w.greaterThan(0.5)), () => {
142
+ If(light.softness.greaterThan(0), () => {
143
+ const penetration = segmentPenetrationNode(uniforms)(logical, light.position);
144
+ const depth = clamp(penetration.div(light.softness.mul(uniforms.grid.z)), 0, 1);
145
+ visibility.assign(float(1).sub(depth.mul(depth).mul(float(3).sub(depth.mul(2)))));
146
+ }).Else(() => {
147
+ visibility.assign(segmentClearNode(uniforms)(logical, light.position));
148
+ });
149
+ });
150
+ return visibility;
151
+ }
152
+ /** Adds the light onto the cleared target: ONE + ONE, which the 8-bit target clamps at 1. */
153
+ function addOnto(material) {
154
+ material.transparent = true;
155
+ material.depthTest = false;
156
+ material.depthWrite = false;
157
+ material.blending = THREE.CustomBlending;
158
+ material.blendEquation = THREE.AddEquation;
159
+ material.blendSrc = THREE.OneFactor;
160
+ material.blendDst = THREE.OneFactor;
161
+ material.blendSrcAlpha = THREE.OneFactor;
162
+ material.blendDstAlpha = THREE.OneFactor;
163
+ }
164
+ /**
165
+ * The material every light-map mesh shares (issue #78 CA-5..CA-7): per texel,
166
+ * `color × intensity × falloff(d / radius) × visibility` with `d` measured in
167
+ * logical space (ADR 0009), added onto the Ambient Light the target was
168
+ * cleared to. light-field.ts is its specification.
169
+ */
170
+ export function createLightMaterial(uniforms) {
171
+ const light = lightUniforms();
172
+ const material = new THREE.MeshBasicNodeMaterial();
173
+ material.fragmentNode = Fn(() => {
174
+ const logical = logicalPositionNode(uniforms.isometric).toVar();
175
+ const reach = falloffNode(length(logical.sub(light.position)).div(max(light.radius, 1e-6)), light.bands).toVar();
176
+ const visibility = float(0).toVar();
177
+ If(reach.greaterThan(0), () => {
178
+ visibility.assign(visibilityNode(uniforms, light, logical));
179
+ });
180
+ return vec4(light.color.mul(reach).mul(visibility), 1);
181
+ })();
182
+ addOnto(material);
183
+ return material;
184
+ }
185
+ /**
186
+ * The quad that multiplies the light-map over the frame already drawn
187
+ * (ADR 0026): `frame × light`, alpha kept, on the sRGB values as stored —
188
+ * on the canvas, or in a Post Effect's target, which stores them alike.
189
+ */
190
+ export function createMultiplyMaterial(light) {
191
+ const material = new THREE.MeshBasicNodeMaterial();
192
+ material.fragmentNode = vec4(texture(light, uv()).rgb, 1);
193
+ material.transparent = true;
194
+ material.depthTest = false;
195
+ material.depthWrite = false;
196
+ material.blending = THREE.CustomBlending;
197
+ material.blendEquation = THREE.AddEquation;
198
+ material.blendSrc = THREE.ZeroFactor;
199
+ material.blendDst = THREE.SrcColorFactor;
200
+ material.blendSrcAlpha = THREE.ZeroFactor;
201
+ material.blendDstAlpha = THREE.OneFactor;
202
+ return material;
203
+ }
@@ -0,0 +1,46 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import type { Light } from './components/light.js';
3
+ import { type OccluderGrid, type Rgb } from './light-field.js';
4
+ /** What one frame's light-map is drawn from. */
5
+ export interface LightMapFrame {
6
+ width: number;
7
+ height: number;
8
+ /** The Ambient Light the target is cleared to, as multipliers. */
9
+ ambient: Rgb;
10
+ lights: readonly Light[];
11
+ projection: 'isometric' | null;
12
+ /** Changes whenever the solid tiles or a Tilemap's origin change; the grid is rebuilt only then. */
13
+ occluderRevision: string;
14
+ occluders: () => OccluderGrid | null;
15
+ }
16
+ /**
17
+ * The scene's light-map (issue #78, ADR 0026): an 8-bit target (inference
18
+ * 16) the size of the internal resolution — or of the drawing buffer — with
19
+ * nearest upscale, cleared to the Ambient Light, onto which each live Light
20
+ * adds its term through one mesh over its footprint. Then multiplied over
21
+ * the frame by one quad.
22
+ */
23
+ export declare class LightMap {
24
+ readonly target: THREE.RenderTarget<THREE.Texture<unknown, THREE.TextureEventMap>, THREE.RenderTargetEventMap>;
25
+ private readonly scene;
26
+ private readonly geometry;
27
+ private readonly empty;
28
+ private readonly uniforms;
29
+ private readonly material;
30
+ private readonly meshes;
31
+ private readonly multiply;
32
+ private occluderTexture;
33
+ private builtRevision;
34
+ private readonly clear;
35
+ private readonly savedClear;
36
+ /** Draws this frame's light-map into its target and leaves the renderer bound to `previous`. */
37
+ render(renderer: THREE.WebGPURenderer, camera: THREE.Camera, frame: LightMapFrame): void;
38
+ /** Multiplies the light-map over what is bound now: the canvas, or a Post Effect's canvas-like target. */
39
+ multiplyOver(renderer: THREE.WebGPURenderer): void;
40
+ dispose(): void;
41
+ private syncOccluders;
42
+ /** One mesh per live Light, placed over its footprint; a destroyed Light's mesh leaves now. */
43
+ private syncLights;
44
+ private place;
45
+ private createMesh;
46
+ }