@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.
- package/README.md +79 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/archetype.d.ts +2 -2
- package/dist/assets/asset-loader.d.ts +28 -5
- package/dist/assets/asset-loader.js +63 -7
- package/dist/assets/model-backend.d.ts +21 -0
- package/dist/assets/model-backend.js +21 -0
- package/dist/assets/model-cache.d.ts +47 -0
- package/dist/assets/model-cache.js +117 -0
- package/dist/audio/audio-subsystem.d.ts +6 -1
- package/dist/audio/audio-subsystem.js +8 -15
- package/dist/audio/placement.d.ts +41 -0
- package/dist/audio/placement.js +21 -0
- package/dist/audio/spatial.d.ts +20 -0
- package/dist/audio/spatial.js +26 -0
- package/dist/audio/types.d.ts +7 -1
- package/dist/camera-projection.d.ts +38 -0
- package/dist/camera-projection.js +44 -0
- package/dist/camera-rig.d.ts +48 -0
- package/dist/camera-rig.js +98 -0
- package/dist/camera.d.ts +20 -3
- package/dist/camera.js +3 -1
- package/dist/component.d.ts +38 -2
- package/dist/component.js +3 -0
- package/dist/components/animated-sprite.d.ts +8 -1
- package/dist/components/animated-sprite.js +16 -1
- package/dist/components/collider.d.ts +89 -0
- package/dist/components/collider.js +57 -0
- package/dist/components/dynamic-body.d.ts +2 -1
- package/dist/components/dynamic-body.js +1 -0
- package/dist/components/hitbox.d.ts +2 -1
- package/dist/components/hitbox.js +1 -0
- package/dist/components/light.d.ts +69 -0
- package/dist/components/light.js +60 -0
- package/dist/components/model.d.ts +63 -0
- package/dist/components/model.js +117 -0
- package/dist/components/particle-emitter.d.ts +8 -1
- package/dist/components/particle-emitter.js +14 -0
- package/dist/components/point-light.d.ts +75 -0
- package/dist/components/point-light.js +102 -0
- package/dist/components/rigid-body.d.ts +114 -0
- package/dist/components/rigid-body.js +83 -0
- package/dist/components/solid.d.ts +2 -1
- package/dist/components/solid.js +1 -0
- package/dist/components/sprite.d.ts +8 -1
- package/dist/components/sprite.js +16 -1
- package/dist/components/sun.d.ts +46 -0
- package/dist/components/sun.js +74 -0
- package/dist/components/tilemap.d.ts +2 -1
- package/dist/components/tilemap.js +7 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +59 -15
- package/dist/game.js +100 -75
- package/dist/index.d.ts +39 -4
- package/dist/index.js +18 -1
- package/dist/light-field.d.ts +78 -0
- package/dist/light-field.js +145 -0
- package/dist/light-map-material.d.ts +40 -0
- package/dist/light-map-material.js +203 -0
- package/dist/light-map.d.ts +46 -0
- package/dist/light-map.js +132 -0
- package/dist/lit-frame.d.ts +48 -0
- package/dist/lit-frame.js +174 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.js +3 -2
- package/dist/physics-3d/body-contacts.d.ts +10 -0
- package/dist/physics-3d/body-contacts.js +59 -0
- package/dist/physics-3d/body-sync.d.ts +52 -0
- package/dist/physics-3d/body-sync.js +153 -0
- package/dist/physics-3d/character-controller.d.ts +44 -0
- package/dist/physics-3d/character-controller.js +126 -0
- package/dist/physics-3d/physics-host.d.ts +86 -0
- package/dist/physics-3d/physics-host.js +174 -0
- package/dist/physics-3d/physics-world.d.ts +65 -0
- package/dist/physics-3d/physics-world.js +158 -0
- package/dist/physics-3d/rapier-module.d.ts +17 -0
- package/dist/physics-3d/rapier-module.js +19 -0
- package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
- package/dist/physics-3d/sensor-dispatch.js +77 -0
- package/dist/physics-3d/world-queries.d.ts +39 -0
- package/dist/physics-3d/world-queries.js +63 -0
- package/dist/pointer-raycast.d.ts +17 -0
- package/dist/pointer-raycast.js +48 -0
- package/dist/pointer.d.ts +27 -6
- package/dist/pointer.js +43 -17
- package/dist/post-effects.d.ts +33 -0
- package/dist/post-effects.js +48 -0
- package/dist/post-pass.d.ts +23 -0
- package/dist/post-pass.js +93 -0
- package/dist/render-layers.d.ts +18 -0
- package/dist/render-layers.js +19 -0
- package/dist/render-output.d.ts +7 -0
- package/dist/render-output.js +17 -1
- package/dist/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +14 -0
- package/dist/runtime-inspection.js +12 -2
- package/dist/runtime-lighting-snapshot.d.ts +67 -0
- package/dist/runtime-lighting-snapshot.js +77 -0
- package/dist/runtime-physics-snapshot.d.ts +41 -0
- package/dist/runtime-physics-snapshot.js +45 -0
- package/dist/runtime-view-snapshot.d.ts +25 -0
- package/dist/runtime-view-snapshot.js +30 -0
- package/dist/scene-camera-3d.d.ts +66 -0
- package/dist/scene-camera-3d.js +117 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene-simulation.d.ts +22 -0
- package/dist/scene-simulation.js +29 -0
- package/dist/scene-space.d.ts +50 -0
- package/dist/scene-space.js +103 -0
- package/dist/scene.d.ts +41 -2
- package/dist/scene.js +11 -4
- package/dist/simulation-phases.d.ts +9 -0
- package/dist/simulation-phases.js +19 -0
- package/dist/spatial-query-3d.d.ts +48 -0
- package/dist/spatial-query-3d.js +96 -0
- package/dist/spatial-query.d.ts +18 -4
- package/dist/spatial-query.js +57 -1
- package/dist/sprite-batch.js +5 -1
- 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
|
+
}
|