@aethervtt/shard-render 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/assets.d.ts +104 -0
  4. package/dist/assets.js +213 -0
  5. package/dist/atmosphere-model.d.ts +96 -0
  6. package/dist/atmosphere-model.js +370 -0
  7. package/dist/atmosphere-nodes.d.ts +55 -0
  8. package/dist/atmosphere-nodes.js +780 -0
  9. package/dist/atmosphere-plugin.d.ts +5 -0
  10. package/dist/atmosphere-plugin.js +42 -0
  11. package/dist/atmosphere-shaders.d.ts +7 -0
  12. package/dist/atmosphere-shaders.js +595 -0
  13. package/dist/atmosphere-state.d.ts +72 -0
  14. package/dist/atmosphere-state.js +18 -0
  15. package/dist/atmosphere.d.ts +191 -0
  16. package/dist/atmosphere.js +950 -0
  17. package/dist/camera.d.ts +83 -0
  18. package/dist/camera.js +163 -0
  19. package/dist/clusters.d.ts +68 -0
  20. package/dist/clusters.js +288 -0
  21. package/dist/culling-shaders.d.ts +2 -0
  22. package/dist/culling-shaders.js +203 -0
  23. package/dist/culling.d.ts +84 -0
  24. package/dist/culling.js +498 -0
  25. package/dist/debug-shaders.d.ts +3 -0
  26. package/dist/debug-shaders.js +207 -0
  27. package/dist/debug-views.d.ts +60 -0
  28. package/dist/debug-views.js +197 -0
  29. package/dist/deferred-plugin.d.ts +5 -0
  30. package/dist/deferred-plugin.js +20 -0
  31. package/dist/deferred-shaders.d.ts +2 -0
  32. package/dist/deferred-shaders.js +151 -0
  33. package/dist/deferred.d.ts +22 -0
  34. package/dist/deferred.js +296 -0
  35. package/dist/deform.d.ts +80 -0
  36. package/dist/deform.js +213 -0
  37. package/dist/describe-plugin.d.ts +5 -0
  38. package/dist/describe-plugin.js +19 -0
  39. package/dist/display-nodes.d.ts +5 -0
  40. package/dist/display-nodes.js +189 -0
  41. package/dist/dynamic-resolution.d.ts +47 -0
  42. package/dist/dynamic-resolution.js +191 -0
  43. package/dist/environment-plugin.d.ts +6 -0
  44. package/dist/environment-plugin.js +44 -0
  45. package/dist/environment-shaders.d.ts +2 -0
  46. package/dist/environment-shaders.js +260 -0
  47. package/dist/environment-state.d.ts +71 -0
  48. package/dist/environment-state.js +144 -0
  49. package/dist/environment.d.ts +70 -0
  50. package/dist/environment.js +529 -0
  51. package/dist/forward.d.ts +123 -0
  52. package/dist/forward.js +1216 -0
  53. package/dist/fxaa.d.ts +2 -0
  54. package/dist/fxaa.js +65 -0
  55. package/dist/gizmo-font.d.ts +21 -0
  56. package/dist/gizmo-font.js +132 -0
  57. package/dist/gizmos-plugin.d.ts +5 -0
  58. package/dist/gizmos-plugin.js +34 -0
  59. package/dist/gizmos.d.ts +156 -0
  60. package/dist/gizmos.js +772 -0
  61. package/dist/gpu-assets.d.ts +114 -0
  62. package/dist/gpu-assets.js +494 -0
  63. package/dist/graph.d.ts +204 -0
  64. package/dist/graph.js +417 -0
  65. package/dist/index.d.ts +53 -0
  66. package/dist/index.js +540 -0
  67. package/dist/instances.d.ts +349 -0
  68. package/dist/instances.js +1335 -0
  69. package/dist/large-world.d.ts +13 -0
  70. package/dist/large-world.js +20 -0
  71. package/dist/lights.d.ts +160 -0
  72. package/dist/lights.js +468 -0
  73. package/dist/material-noise.d.ts +19 -0
  74. package/dist/material-noise.js +7 -0
  75. package/dist/material-pipelines.d.ts +46 -0
  76. package/dist/material-pipelines.js +166 -0
  77. package/dist/materials.d.ts +108 -0
  78. package/dist/materials.js +219 -0
  79. package/dist/noise.d.ts +76 -0
  80. package/dist/noise.js +286 -0
  81. package/dist/overlay-registry.d.ts +18 -0
  82. package/dist/overlay-registry.js +30 -0
  83. package/dist/overlays.d.ts +35 -0
  84. package/dist/overlays.js +344 -0
  85. package/dist/paths.d.ts +20 -0
  86. package/dist/paths.js +46 -0
  87. package/dist/picking-plugin.d.ts +2 -0
  88. package/dist/picking-plugin.js +20 -0
  89. package/dist/picking.d.ts +94 -0
  90. package/dist/picking.js +456 -0
  91. package/dist/pixel-perfect-plugin.d.ts +9 -0
  92. package/dist/pixel-perfect-plugin.js +135 -0
  93. package/dist/pixel-perfect.d.ts +27 -0
  94. package/dist/pixel-perfect.js +70 -0
  95. package/dist/plugin.d.ts +111 -0
  96. package/dist/plugin.js +182 -0
  97. package/dist/pool.d.ts +19 -0
  98. package/dist/pool.js +43 -0
  99. package/dist/post-common.d.ts +51 -0
  100. package/dist/post-common.js +148 -0
  101. package/dist/post-nodes.d.ts +39 -0
  102. package/dist/post-nodes.js +849 -0
  103. package/dist/post-plugin.d.ts +5 -0
  104. package/dist/post-plugin.js +26 -0
  105. package/dist/post-shaders.d.ts +8 -0
  106. package/dist/post-shaders.js +698 -0
  107. package/dist/post.d.ts +175 -0
  108. package/dist/post.js +453 -0
  109. package/dist/readback.d.ts +4 -0
  110. package/dist/readback.js +95 -0
  111. package/dist/render-scale.d.ts +53 -0
  112. package/dist/render-scale.js +63 -0
  113. package/dist/shaders.d.ts +122 -0
  114. package/dist/shaders.js +1168 -0
  115. package/dist/shadows.d.ts +119 -0
  116. package/dist/shadows.js +562 -0
  117. package/dist/skin-asset.d.ts +26 -0
  118. package/dist/skin-asset.js +25 -0
  119. package/dist/skinning-plugin.d.ts +5 -0
  120. package/dist/skinning-plugin.js +16 -0
  121. package/dist/skinning.d.ts +19 -0
  122. package/dist/skinning.js +343 -0
  123. package/dist/standard-fields.d.ts +67 -0
  124. package/dist/standard-fields.js +73 -0
  125. package/dist/standard.d.ts +7 -0
  126. package/dist/standard.js +37 -0
  127. package/dist/stats.d.ts +25 -0
  128. package/dist/stats.js +18 -0
  129. package/dist/target.d.ts +55 -0
  130. package/dist/target.js +70 -0
  131. package/dist/testing.d.ts +34 -0
  132. package/dist/testing.js +126 -0
  133. package/dist/timer.d.ts +28 -0
  134. package/dist/timer.js +136 -0
  135. package/dist/view.d.ts +117 -0
  136. package/dist/view.js +389 -0
  137. package/dist/visibility.d.ts +10 -0
  138. package/dist/visibility.js +84 -0
  139. package/package.json +49 -0
@@ -0,0 +1,13 @@
1
+ import type { World } from '@aethervtt/shard-core';
2
+ /**
3
+ * Moves everything the renderer keeps from earlier frames into the new origin frame (spec 0040):
4
+ * previous instance transforms, each camera's last view-projection, and retained gizmos. TAA history
5
+ * textures hold colors, not positions, so they stay valid once reprojection lines up. Shadow
6
+ * cascades and light records are rebuilt from `GlobalTransform` every frame.
7
+ */
8
+ export declare function shiftRenderHistory(world: World, x: number, y: number, z: number): void;
9
+ /**
10
+ * Follows `OriginShift` synchronously: it fires during transform propagation (PostUpdate), after
11
+ * this frame's gameplay drew in the old frame and before extraction reads the new one.
12
+ */
13
+ export declare function observeOriginShifts(world: World): () => void;
@@ -0,0 +1,20 @@
1
+ import { OriginShift } from "@aethervtt/shard-transform";
2
+ import { Instances } from "./instances.js";
3
+ import { RenderCounters } from "./stats.js";
4
+ import { Cameras, shiftCameraHistory } from "./view.js";
5
+ function shiftRenderHistory(world, x, y, z) {
6
+ world.initResource(RenderCounters).originShifts++;
7
+ if (x === 0 && y === 0 && z === 0) return;
8
+ world.tryResource(Instances)?.shiftOrigin(x, y, z);
9
+ const cameras = world.tryResource(Cameras);
10
+ if (cameras) for (const cam of cameras.values()) shiftCameraHistory(cam, x, y, z);
11
+ }
12
+ function observeOriginShifts(world) {
13
+ return world.observe(OriginShift, ({ world: w, data }) => {
14
+ shiftRenderHistory(w, data.offset[0], data.offset[1], data.offset[2]);
15
+ });
16
+ }
17
+ export {
18
+ observeOriginShifts,
19
+ shiftRenderHistory
20
+ };
@@ -0,0 +1,160 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
+ /** Luminous power presets in lumens, for point and spot lights. */
4
+ export declare const LuminousPowerPresets: {
5
+ readonly candle: 12;
6
+ readonly 'bulb-40w': 450;
7
+ readonly bulb: 800;
8
+ readonly floodlight: 20000;
9
+ };
10
+ export type LuminousPowerPreset = keyof typeof LuminousPowerPresets;
11
+ /** Lumens for a preset name or a number. */
12
+ export declare const lumens: (value: LuminousPowerPreset | number) => number;
13
+ export declare const CascadeSettings: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
14
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
15
+ maxDistance: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
+ splitLambda: import("@aethervtt/shard-core").FieldType<number, "f32">;
17
+ }>, "object">;
18
+ export declare const DirectionalLight: import("@aethervtt/shard-core").ComponentDef<{
19
+ readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
20
+ readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
21
+ readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
22
+ readonly shadowSoftness: import("@aethervtt/shard-core").FieldType<number, "f32">;
23
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
24
+ readonly illuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
25
+ readonly cascades: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
26
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
27
+ maxDistance: import("@aethervtt/shard-core").FieldType<number, "f32">;
28
+ splitLambda: import("@aethervtt/shard-core").FieldType<number, "f32">;
29
+ }>, "object">;
30
+ readonly angularDiameter: import("@aethervtt/shard-core").FieldType<number, "f32">;
31
+ }>;
32
+ export declare const PointLight: import("@aethervtt/shard-core").ComponentDef<{
33
+ readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
34
+ readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
35
+ readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
36
+ readonly shadowSoftness: import("@aethervtt/shard-core").FieldType<number, "f32">;
37
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
38
+ readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
39
+ readonly range: import("@aethervtt/shard-core").FieldType<number, "f32">;
40
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
41
+ }>;
42
+ export declare const SpotLight: import("@aethervtt/shard-core").ComponentDef<{
43
+ readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
44
+ readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
45
+ readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
46
+ readonly shadowSoftness: import("@aethervtt/shard-core").FieldType<number, "f32">;
47
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
48
+ readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
49
+ readonly range: import("@aethervtt/shard-core").FieldType<number, "f32">;
50
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
51
+ readonly innerAngle: import("@aethervtt/shard-core").FieldType<number, "f32">;
52
+ readonly outerAngle: import("@aethervtt/shard-core").FieldType<number, "f32">;
53
+ }>;
54
+ export interface AmbientLightValue {
55
+ /** Linear color. */
56
+ color: [number, number, number];
57
+ /** Sky luminance in cd/m². A useful fill is roughly illuminance / 10 of the main light. */
58
+ brightness: number;
59
+ }
60
+ export declare const AmbientLight: import("@aethervtt/shard-core").ResourceDef<AmbientLightValue>;
61
+ export interface LightingSettingsValue {
62
+ /** Point and spot lights the light buffer holds. */
63
+ maxLights: number;
64
+ /** Where the last cluster slice ends (m). 0: the camera's far plane, or 1 km. */
65
+ clusterFar: number;
66
+ cascadeMapSize: number;
67
+ /** Spot and point shadow map size (per face). */
68
+ shadowMapSize: number;
69
+ maxShadowedPoints: number;
70
+ maxShadowedSpots: number;
71
+ }
72
+ export declare const LightingSettings: import("@aethervtt/shard-core").ResourceDef<LightingSettingsValue>;
73
+ /** Floats per light record in the light buffer. */
74
+ export declare const LIGHT_FLOATS = 20;
75
+ export declare const LIGHT_KIND: {
76
+ readonly point: 0;
77
+ readonly spot: 1;
78
+ };
79
+ export declare const MAX_DIRECTIONAL = 4;
80
+ /** A clustered light (point or spot) as the renderer tracks it. */
81
+ export interface LightRecord {
82
+ entity: Entity;
83
+ kind: 0 | 1;
84
+ /** Slot in the light buffer. */
85
+ slot: number;
86
+ /** World position and range; spot axis (unit, direction light travels) and cos(outer). */
87
+ x: number;
88
+ y: number;
89
+ z: number;
90
+ range: number;
91
+ dx: number;
92
+ dy: number;
93
+ dz: number;
94
+ cosOuter: number;
95
+ /** Luminous power (lm), for describe and shadow ranking. */
96
+ intensity: number;
97
+ shadows: boolean;
98
+ /** Shadow map index this frame (spot layer, or point cube index), or -1. */
99
+ shadowIndex: number;
100
+ outerAngle: number;
101
+ radius: number;
102
+ alive: boolean;
103
+ }
104
+ /**
105
+ * Point and spot lights in a storage buffer (`array<Light>`). Each light owns a slot; only lights
106
+ * whose data changed this frame are uploaded (coalesced runs), and the counters say how many.
107
+ */
108
+ export declare class LightStore {
109
+ readonly records: LightRecord[];
110
+ readonly byEntity: Map<number, LightRecord>;
111
+ data: Float32Array;
112
+ u32: Uint32Array;
113
+ private dirty;
114
+ private readonly free;
115
+ high: number;
116
+ /** Lights uploaded in the last frame, and bytes. */
117
+ uploadedLights: number;
118
+ uploadedBytes: number;
119
+ readonly buffer: GpuBuffer;
120
+ /** Directional lights: count + up to 4 × (direction, shadow cascade flag, color × lux, disk radius). */
121
+ readonly directional: GpuBuffer;
122
+ readonly directionalData: Float32Array<ArrayBuffer>;
123
+ readonly directionalU32: Uint32Array<ArrayBuffer>;
124
+ /** The first directional light with shadows: entity, and its settings. */
125
+ shadowSun: {
126
+ entity: Entity;
127
+ direction: Float32Array;
128
+ count: number;
129
+ maxDistance: number;
130
+ splitLambda: number;
131
+ bias: number;
132
+ normalBias: number;
133
+ softness: number;
134
+ } | null;
135
+ directionalCount: number;
136
+ private generation;
137
+ private readonly gpu;
138
+ constructor(gpu: GpuContext, capacity: number);
139
+ get capacity(): number;
140
+ /** The live records (dense list; order follows slots). */
141
+ live(): LightRecord[];
142
+ record(entity: Entity, kind: 0 | 1): LightRecord | undefined;
143
+ remove(entity: Entity): void;
144
+ /** Writes a float into a light's record, marking it dirty only when the value changes. */
145
+ set(slot: number, index: number, value: number): void;
146
+ setU32(slot: number, index: number, value: number): void;
147
+ upload(): void;
148
+ }
149
+ export declare const Lights: import("@aethervtt/shard-core").ResourceDef<LightStore>;
150
+ /** Reads lights into the light store. Only lights whose values changed are re-uploaded later. */
151
+ export declare const extractLights: import("@aethervtt/shard-core").SystemDef<{
152
+ directional: import("@aethervtt/shard-core").Query;
153
+ points: import("@aethervtt/shard-core").Query;
154
+ spots: import("@aethervtt/shard-core").Query;
155
+ seen: Set<number>;
156
+ }>;
157
+ /** Frees a light's slot when its component goes away (including despawn). */
158
+ export declare function observeLightRemovals(world: World): void;
159
+ /** Sets a light's shadow index (marks it dirty if it changed). */
160
+ export declare function setShadowIndex(store: LightStore, r: LightRecord, index: number): void;
package/dist/lights.js ADDED
@@ -0,0 +1,468 @@
1
+ import {
2
+ defineComponent,
3
+ defineResource,
4
+ defineSystem,
5
+ onRemove,
6
+ t
7
+ } from "@aethervtt/shard-core";
8
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
9
+ import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
10
+ import { LightPresets } from "./camera.js";
11
+ const LuminousPowerPresets = {
12
+ candle: 12,
13
+ "bulb-40w": 450,
14
+ bulb: 800,
15
+ floodlight: 2e4
16
+ };
17
+ const lumens = (value) => typeof value === "number" ? value : LuminousPowerPresets[value];
18
+ const shadowFields = (defaults) => ({
19
+ shadows: t.bool({ description: "Casts shadows (within the shadow budget)." }),
20
+ shadowBias: t.f32({
21
+ default: defaults.bias,
22
+ min: 0,
23
+ unit: "m",
24
+ description: "Moves receivers toward the light before the depth test. Fixes acne."
25
+ }),
26
+ shadowNormalBias: t.f32({
27
+ default: defaults.normalBias,
28
+ min: 0,
29
+ description: "Moves receivers along their normal, in shadow-map texels. Fixes acne at grazing angles."
30
+ }),
31
+ shadowSoftness: t.f32({
32
+ min: 0,
33
+ description: "PCF filter radius in texels beyond the base 3x3 (0 = 3x3). Softer edges."
34
+ })
35
+ });
36
+ const CascadeSettings = t.struct(
37
+ {
38
+ count: t.u8({ default: 4, min: 1, max: 4, description: "Number of cascades." }),
39
+ maxDistance: t.f32({
40
+ default: 150,
41
+ min: 0.01,
42
+ unit: "m",
43
+ description: "Shadows end this far from the camera."
44
+ }),
45
+ splitLambda: t.f32({
46
+ default: 0.8,
47
+ min: 0,
48
+ max: 1,
49
+ description: "Blend of logarithmic (1) and uniform (0) cascade splits."
50
+ })
51
+ },
52
+ { description: "How cascaded shadow maps divide the view." }
53
+ );
54
+ const DirectionalLight = defineComponent(
55
+ "render/DirectionalLight",
56
+ {
57
+ color: t.color({ default: [1, 1, 1, 1], description: "Light color (linear)." }),
58
+ illuminance: t.f32({
59
+ default: LightPresets.daylight,
60
+ min: 0,
61
+ unit: "lux",
62
+ presets: LightPresets,
63
+ description: "Presets: direct-sun 100000, daylight 10000, overcast 1000, indoor 400, twilight 10, moonlight 0.3."
64
+ }),
65
+ ...shadowFields({ bias: 0.02, normalBias: 1.5 }),
66
+ cascades: CascadeSettings,
67
+ angularDiameter: t.f32({
68
+ default: 0.53,
69
+ min: 0,
70
+ max: 30,
71
+ unit: "deg",
72
+ description: "The sun disk's size in an atmosphere's sky (the Sun 0.53\xB0). 0: no disk. Illuminance is at the top of the atmosphere."
73
+ })
74
+ },
75
+ {
76
+ description: "Sun-like light shining along its -Z axis. Up to 4 are lit; the first with shadows gets cascaded shadow maps. Inside an Atmosphere its light is dimmed and reddened by the air toward it, and the brightest two draw sun disks.",
77
+ requires: [Transform]
78
+ }
79
+ );
80
+ const pointFields = {
81
+ color: t.color({ default: [1, 1, 1, 1], description: "Light color (linear)." }),
82
+ intensity: t.f32({
83
+ default: LuminousPowerPresets.bulb,
84
+ min: 0,
85
+ unit: "lm",
86
+ presets: LuminousPowerPresets,
87
+ description: "Luminous power. Presets: candle 12, bulb-40w 450, bulb 800, floodlight 20000."
88
+ }),
89
+ range: t.f32({
90
+ default: 20,
91
+ min: 0.01,
92
+ unit: "m",
93
+ description: "Where the light's contribution is windowed to zero (and where culling stops)."
94
+ }),
95
+ radius: t.f32({
96
+ min: 0,
97
+ unit: "m",
98
+ description: "Emitter size: softens specular highlights."
99
+ })
100
+ };
101
+ const PointLight = defineComponent(
102
+ "render/PointLight",
103
+ { ...pointFields, ...shadowFields({ bias: 0.02, normalBias: 1 }) },
104
+ {
105
+ description: "Light emitted equally in all directions, in lumens (intensity lm / 4\u03C0 candela), with physical inverse-square falloff windowed to zero at range.",
106
+ requires: [Transform]
107
+ }
108
+ );
109
+ const SpotLight = defineComponent(
110
+ "render/SpotLight",
111
+ {
112
+ ...pointFields,
113
+ ...shadowFields({ bias: 0.02, normalBias: 1 }),
114
+ innerAngle: t.f32({
115
+ default: 30,
116
+ min: 0,
117
+ max: 89,
118
+ unit: "deg",
119
+ description: "Full brightness inside this angle from the axis."
120
+ }),
121
+ outerAngle: t.f32({
122
+ default: 45,
123
+ min: 0.1,
124
+ max: 89.9,
125
+ unit: "deg",
126
+ description: "No light outside this angle from the axis."
127
+ })
128
+ },
129
+ {
130
+ description: "A cone of light along -Z. Brightness is lm / 4\u03C0 candela regardless of the cone's width.",
131
+ requires: [Transform]
132
+ }
133
+ );
134
+ const AmbientLight = defineResource("render/AmbientLight", {
135
+ description: "Uniform fill light, in cd/m\xB2. Used when no environment map lights the view.",
136
+ init: () => ({ color: [1, 1, 1], brightness: 0 })
137
+ });
138
+ const LightingSettings = defineResource("render/LightingSettings", {
139
+ description: "Light and shadow budgets: light buffer size, cluster range, shadow map sizes.",
140
+ init: () => ({
141
+ maxLights: 1024,
142
+ clusterFar: 0,
143
+ cascadeMapSize: 2048,
144
+ shadowMapSize: 1024,
145
+ maxShadowedPoints: 4,
146
+ maxShadowedSpots: 8
147
+ })
148
+ });
149
+ const LIGHT_FLOATS = 20;
150
+ const LIGHT_KIND = { point: 0, spot: 1 };
151
+ const MAX_DIRECTIONAL = 4;
152
+ class LightStore {
153
+ records = [];
154
+ byEntity = /* @__PURE__ */ new Map();
155
+ data;
156
+ u32;
157
+ dirty;
158
+ free = [];
159
+ high = 0;
160
+ /** Lights uploaded in the last frame, and bytes. */
161
+ uploadedLights = 0;
162
+ uploadedBytes = 0;
163
+ buffer;
164
+ /** Directional lights: count + up to 4 × (direction, shadow cascade flag, color × lux, disk radius). */
165
+ directional;
166
+ directionalData = new Float32Array(4 + MAX_DIRECTIONAL * 8);
167
+ directionalU32 = new Uint32Array(this.directionalData.buffer);
168
+ /** The first directional light with shadows: entity, and its settings. */
169
+ shadowSun = null;
170
+ directionalCount = 0;
171
+ generation;
172
+ gpu;
173
+ constructor(gpu, capacity) {
174
+ this.gpu = gpu;
175
+ this.generation = gpu.generation;
176
+ this.data = new Float32Array(capacity * LIGHT_FLOATS);
177
+ this.u32 = new Uint32Array(this.data.buffer);
178
+ this.dirty = new Uint8Array(capacity);
179
+ this.buffer = new GpuBuffer(gpu, {
180
+ label: "lights",
181
+ usage: GPUBufferUsage.STORAGE,
182
+ size: capacity * LIGHT_FLOATS * 4
183
+ });
184
+ this.directional = new GpuBuffer(gpu, {
185
+ label: "lights/directional",
186
+ usage: GPUBufferUsage.STORAGE,
187
+ size: this.directionalData.byteLength
188
+ });
189
+ }
190
+ get capacity() {
191
+ return this.dirty.length;
192
+ }
193
+ /** The live records (dense list; order follows slots). */
194
+ live() {
195
+ return this.records.filter((r) => r.alive);
196
+ }
197
+ record(entity, kind) {
198
+ let r = this.byEntity.get(entity);
199
+ if (r && r.kind === kind) return r;
200
+ if (r) this.remove(entity);
201
+ const slot = this.free.pop() ?? (this.high < this.capacity ? this.high++ : -1);
202
+ if (slot < 0) return void 0;
203
+ r = {
204
+ entity,
205
+ kind,
206
+ slot,
207
+ x: 0,
208
+ y: 0,
209
+ z: 0,
210
+ range: 0,
211
+ dx: 0,
212
+ dy: 0,
213
+ dz: -1,
214
+ cosOuter: -2,
215
+ intensity: 0,
216
+ shadows: false,
217
+ shadowIndex: -1,
218
+ outerAngle: 0,
219
+ radius: 0,
220
+ alive: true
221
+ };
222
+ this.records[slot] = r;
223
+ this.byEntity.set(entity, r);
224
+ return r;
225
+ }
226
+ remove(entity) {
227
+ const r = this.byEntity.get(entity);
228
+ if (!r) return;
229
+ this.byEntity.delete(entity);
230
+ r.alive = false;
231
+ this.data.fill(0, r.slot * LIGHT_FLOATS, (r.slot + 1) * LIGHT_FLOATS);
232
+ this.dirty[r.slot] = 1;
233
+ this.free.push(r.slot);
234
+ }
235
+ /** Writes a float into a light's record, marking it dirty only when the value changes. */
236
+ set(slot, index, value) {
237
+ const o = slot * LIGHT_FLOATS + index;
238
+ const d = this.data;
239
+ const before = d[o];
240
+ d[o] = value;
241
+ if (d[o] !== before && !(Number.isNaN(before) && Number.isNaN(d[o]))) this.dirty[slot] = 1;
242
+ }
243
+ setU32(slot, index, value) {
244
+ const o = slot * LIGHT_FLOATS + index;
245
+ if (this.u32[o] !== value >>> 0) {
246
+ this.u32[o] = value >>> 0;
247
+ this.dirty[slot] = 1;
248
+ }
249
+ }
250
+ upload() {
251
+ if (this.generation !== this.gpu.generation) {
252
+ this.generation = this.gpu.generation;
253
+ this.dirty.fill(1, 0, this.high);
254
+ }
255
+ let lights = 0;
256
+ let runStart = -1;
257
+ for (let s = 0; s <= this.high; s++) {
258
+ const d = s < this.high && this.dirty[s] === 1;
259
+ if (d) {
260
+ this.dirty[s] = 0;
261
+ lights++;
262
+ if (runStart < 0) runStart = s;
263
+ } else if (runStart >= 0) {
264
+ this.buffer.write(
265
+ this.data,
266
+ runStart * LIGHT_FLOATS * 4,
267
+ runStart * LIGHT_FLOATS,
268
+ (s - runStart) * LIGHT_FLOATS
269
+ );
270
+ runStart = -1;
271
+ }
272
+ }
273
+ this.uploadedLights = lights;
274
+ this.uploadedBytes = lights * LIGHT_FLOATS * 4;
275
+ this.directional.write(this.directionalData);
276
+ }
277
+ }
278
+ const Lights = defineResource("render/Lights", {
279
+ description: "Point and spot lights on the GPU, plus the directional lights."
280
+ });
281
+ const POINT_KIND = LIGHT_KIND.point;
282
+ const SPOT_KIND = LIGHT_KIND.spot;
283
+ const INV_4PI = 1 / (4 * Math.PI);
284
+ function writeLight(store, r, g, o, color, c, intensity, range, radius, bias, normalBias, softness, inner, outer) {
285
+ const s = r.slot;
286
+ r.x = g[o + 3];
287
+ r.y = g[o + 7];
288
+ r.z = g[o + 11];
289
+ r.range = range;
290
+ r.intensity = intensity;
291
+ r.radius = radius;
292
+ const dx = -g[o + 2];
293
+ const dy = -g[o + 6];
294
+ const dz = -g[o + 10];
295
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
296
+ r.dx = dx / len;
297
+ r.dy = dy / len;
298
+ r.dz = dz / len;
299
+ const candela = intensity * INV_4PI;
300
+ store.set(s, 0, r.x);
301
+ store.set(s, 1, r.y);
302
+ store.set(s, 2, r.z);
303
+ store.set(s, 3, range);
304
+ store.set(s, 4, color[c] * candela);
305
+ store.set(s, 5, color[c + 1] * candela);
306
+ store.set(s, 6, color[c + 2] * candela);
307
+ store.set(s, 7, radius);
308
+ store.set(s, 8, r.dx);
309
+ store.set(s, 9, r.dy);
310
+ store.set(s, 10, r.dz);
311
+ store.setU32(s, 11, r.kind);
312
+ if (r.kind === SPOT_KIND) {
313
+ const cosOuter = Math.cos(outer * Math.PI / 180);
314
+ const cosInner = Math.cos(Math.min(inner, outer - 0.01) * Math.PI / 180);
315
+ const scale = 1 / Math.max(cosInner - cosOuter, 1e-4);
316
+ store.set(s, 12, scale);
317
+ store.set(s, 13, -cosOuter * scale);
318
+ r.cosOuter = cosOuter;
319
+ r.outerAngle = outer;
320
+ } else {
321
+ store.set(s, 12, 0);
322
+ store.set(s, 13, 1);
323
+ r.cosOuter = -2;
324
+ }
325
+ store.setU32(s, 14, r.shadowIndex < 0 ? 4294967295 : r.shadowIndex);
326
+ store.set(s, 15, bias);
327
+ store.set(s, 16, normalBias);
328
+ store.set(s, 17, softness);
329
+ }
330
+ const extractLights = defineSystem({
331
+ name: "render/extract-lights",
332
+ description: "Reads directional, point, and spot lights into GPU light buffers.",
333
+ setup: (world) => ({
334
+ directional: world.query({ with: [DirectionalLight, GlobalTransform] }),
335
+ points: world.query({ with: [PointLight, GlobalTransform] }),
336
+ spots: world.query({ with: [SpotLight, GlobalTransform] }),
337
+ seen: /* @__PURE__ */ new Set()
338
+ }),
339
+ run: ({ directional, points, spots, seen }, world, ctx) => {
340
+ const store = world.resource(Lights);
341
+ const since = ctx.lastRunTick;
342
+ seen.clear();
343
+ for (const [def, kind, q] of [
344
+ [PointLight, POINT_KIND, points],
345
+ [SpotLight, SPOT_KIND, spots]
346
+ ]) {
347
+ for (const table of q.tables) {
348
+ const n = table.count;
349
+ if (n === 0) continue;
350
+ const g = table.column(GlobalTransform, "matrix");
351
+ const gChanged = table.changedTicks(GlobalTransform);
352
+ const changed = table.changedTicks(def);
353
+ const color = table.column(def, "color");
354
+ const intensity = table.column(def, "intensity");
355
+ const range = table.column(def, "range");
356
+ const radius = table.column(def, "radius");
357
+ const shadows = table.column(def, "shadows");
358
+ const bias = table.column(def, "shadowBias");
359
+ const normalBias = table.column(def, "shadowNormalBias");
360
+ const softness = table.column(def, "shadowSoftness");
361
+ const inner = kind === SPOT_KIND ? table.column(SpotLight, "innerAngle") : void 0;
362
+ const outer = kind === SPOT_KIND ? table.column(SpotLight, "outerAngle") : void 0;
363
+ for (let i = 0; i < n; i++) {
364
+ const entity = table.entities[i];
365
+ seen.add(entity);
366
+ const existing = store.byEntity.get(entity);
367
+ const r = store.record(entity, kind);
368
+ if (!r) continue;
369
+ r.shadows = shadows[i] !== 0;
370
+ if (existing === r && gChanged[i] <= since && changed[i] <= since) continue;
371
+ writeLight(
372
+ store,
373
+ r,
374
+ g,
375
+ i * 12,
376
+ color,
377
+ i * 4,
378
+ intensity[i],
379
+ range[i],
380
+ radius[i],
381
+ bias[i],
382
+ normalBias[i],
383
+ softness[i],
384
+ inner ? inner[i] : 0,
385
+ outer ? outer[i] : 90
386
+ );
387
+ }
388
+ }
389
+ }
390
+ if (seen.size !== store.byEntity.size) {
391
+ for (const entity of [...store.byEntity.keys()]) if (!seen.has(entity)) store.remove(entity);
392
+ }
393
+ const d = store.directionalData;
394
+ const du = store.directionalU32;
395
+ let count = 0;
396
+ store.shadowSun = null;
397
+ for (const table of directional.tables) {
398
+ const g = table.column(GlobalTransform, "matrix");
399
+ const color = table.column(DirectionalLight, "color");
400
+ const lux = table.column(DirectionalLight, "illuminance");
401
+ const shadows = table.column(DirectionalLight, "shadows");
402
+ const cascades = table.column(DirectionalLight, "cascades");
403
+ const angular = table.column(DirectionalLight, "angularDiameter");
404
+ for (let i = 0; i < table.count && count < MAX_DIRECTIONAL; i++) {
405
+ const o = 4 + count * 8;
406
+ const x = g[i * 12 + 2];
407
+ const y = g[i * 12 + 6];
408
+ const z = g[i * 12 + 10];
409
+ const len = Math.sqrt(x * x + y * y + z * z) || 1;
410
+ d[o] = x / len;
411
+ d[o + 1] = y / len;
412
+ d[o + 2] = z / len;
413
+ const shadowed = shadows[i] !== 0 && store.shadowSun === null;
414
+ du[o + 3] = shadowed ? 1 : 0;
415
+ d[o + 4] = color[i * 4] * lux[i];
416
+ d[o + 5] = color[i * 4 + 1] * lux[i];
417
+ d[o + 6] = color[i * 4 + 2] * lux[i];
418
+ d[o + 7] = angular[i] * Math.PI / 360;
419
+ if (shadowed) {
420
+ const c = cascades[i];
421
+ store.shadowSun = {
422
+ entity: table.entities[i],
423
+ direction: new Float32Array([x / len, y / len, z / len]),
424
+ count: c.count,
425
+ maxDistance: c.maxDistance,
426
+ splitLambda: c.splitLambda,
427
+ bias: table.column(DirectionalLight, "shadowBias")[i],
428
+ normalBias: table.column(DirectionalLight, "shadowNormalBias")[i],
429
+ softness: table.column(DirectionalLight, "shadowSoftness")[i]
430
+ };
431
+ }
432
+ count++;
433
+ }
434
+ }
435
+ du[0] = count;
436
+ store.directionalCount = count;
437
+ }
438
+ });
439
+ function observeLightRemovals(world) {
440
+ for (const def of [PointLight, SpotLight]) {
441
+ world.observe(onRemove(def), ({ entity, world: world2 }) => {
442
+ world2.tryResource(Lights)?.remove(entity);
443
+ });
444
+ }
445
+ }
446
+ function setShadowIndex(store, r, index) {
447
+ if (r.shadowIndex === index) return;
448
+ r.shadowIndex = index;
449
+ store.setU32(r.slot, 14, index < 0 ? 4294967295 : index);
450
+ }
451
+ export {
452
+ AmbientLight,
453
+ CascadeSettings,
454
+ DirectionalLight,
455
+ LIGHT_FLOATS,
456
+ LIGHT_KIND,
457
+ LightStore,
458
+ LightingSettings,
459
+ Lights,
460
+ LuminousPowerPresets,
461
+ MAX_DIRECTIONAL,
462
+ PointLight,
463
+ SpotLight,
464
+ extractLights,
465
+ lumens,
466
+ observeLightRemovals,
467
+ setShadowIndex
468
+ };
@@ -0,0 +1,19 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { ShaderLibrary } from '@aethervtt/shard-shader';
3
+ import type { MaterialType } from './materials.js';
4
+ /**
5
+ * Noise slots in materials (`noise: { detail: 'rock.noise.json' }`), supplied by
6
+ * `materialNoisePlugin` from `@aethervtt/shard-render/noise`. Core render only asks for it when a
7
+ * material type has noise slots, so apps without such materials carry no noise code.
8
+ */
9
+ export interface MaterialNoiseSupport {
10
+ /**
11
+ * The WGSL a type's noise slots add to its module, and a key that changes with its graphs.
12
+ * Undefined while a graph is still loading (this starts the load).
13
+ */
14
+ wrappers(world: World, library: ShaderLibrary, type: MaterialType): {
15
+ source: string;
16
+ key: string;
17
+ } | undefined;
18
+ }
19
+ export declare const MaterialNoise: import("@aethervtt/shard-core").ResourceDef<MaterialNoiseSupport>;
@@ -0,0 +1,7 @@
1
+ import { defineResource } from "@aethervtt/shard-core";
2
+ const MaterialNoise = defineResource("render/MaterialNoise", {
3
+ description: "Noise slots in materials, when materialNoisePlugin is installed."
4
+ });
5
+ export {
6
+ MaterialNoise
7
+ };