@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,83 @@
1
+ import { type Infer } from '@aethervtt/shard-core';
2
+ /** Light presets in lux, paired with the exposure presets below. */
3
+ export declare const LightPresets: {
4
+ readonly 'direct-sun': 100000;
5
+ readonly daylight: 10000;
6
+ readonly overcast: 1000;
7
+ readonly indoor: 400;
8
+ readonly twilight: 10;
9
+ readonly moonlight: 0.3;
10
+ };
11
+ export type LightPreset = keyof typeof LightPresets;
12
+ /** EV100 from scene illuminance (incident-light meter, ISO 100): EV100 = log2(lux / 2.5). */
13
+ export declare function ev100FromIlluminance(lux: number): number;
14
+ /** Exposure presets in EV100, matched to the light presets. */
15
+ export declare const ExposurePresets: {
16
+ readonly sunny: number;
17
+ readonly daylight: number;
18
+ readonly overcast: number;
19
+ readonly indoor: number;
20
+ readonly twilight: number;
21
+ };
22
+ export type ExposurePreset = keyof typeof ExposurePresets;
23
+ /** Illuminance for a preset name or a number (lux). */
24
+ export declare const lux: (value: LightPreset | number) => number;
25
+ /** EV100 for a preset name or a number. */
26
+ export declare const ev100: (value: ExposurePreset | number) => number;
27
+ /** The multiplier from scene luminance to display, for an EV100 (Lagarde & de Rousiers, 2014). */
28
+ export declare function exposureScale(ev: number): number;
29
+ /** EV100 from photographic settings: log2(N² / t · 100 / S). */
30
+ export declare function ev100FromCamera(aperture: number, shutterSpeed: number, iso: number): number;
31
+ export declare const Exposure: import("@aethervtt/shard-core").ComponentDef<{
32
+ readonly ev100: import("@aethervtt/shard-core").FieldType<number, "f32">;
33
+ }>;
34
+ export declare const Camera3d: import("@aethervtt/shard-core").ComponentDef<{
35
+ readonly projection: import("@aethervtt/shard-core").FieldType<"orthographic" | "perspective", "u8">;
36
+ readonly fovY: import("@aethervtt/shard-core").FieldType<number, "f32">;
37
+ readonly near: import("@aethervtt/shard-core").FieldType<number, "f32">;
38
+ readonly orthoHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
39
+ readonly far: import("@aethervtt/shard-core").FieldType<number, "f32">;
40
+ readonly order: import("@aethervtt/shard-core").FieldType<number, "i32">;
41
+ readonly clearColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
42
+ readonly target: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"RenderTarget"> | null, "object">;
43
+ readonly active: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
44
+ }>;
45
+ export type Camera3dValue = Infer<typeof Camera3d>;
46
+ export declare const PhysicalCamera: import("@aethervtt/shard-core").ComponentDef<{
47
+ readonly aperture: import("@aethervtt/shard-core").FieldType<number, "f32">;
48
+ readonly shutterSpeed: import("@aethervtt/shard-core").FieldType<number, "f32">;
49
+ readonly iso: import("@aethervtt/shard-core").FieldType<number, "f32">;
50
+ readonly sensorHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
51
+ readonly focalLength: import("@aethervtt/shard-core").FieldType<number, "f32">;
52
+ }>;
53
+ /** A 2D camera: orthographic, looking down -Z. Use as `world.spawn(...camera2d({ height: 20 }))`. */
54
+ export declare function camera2d(options?: {
55
+ height?: number;
56
+ order?: number;
57
+ clearColor?: [number, number, number, number];
58
+ }): readonly [readonly [import("@aethervtt/shard-core").ComponentDef<{
59
+ readonly projection: import("@aethervtt/shard-core").FieldType<"orthographic" | "perspective", "u8">;
60
+ readonly fovY: import("@aethervtt/shard-core").FieldType<number, "f32">;
61
+ readonly near: import("@aethervtt/shard-core").FieldType<number, "f32">;
62
+ readonly orthoHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
63
+ readonly far: import("@aethervtt/shard-core").FieldType<number, "f32">;
64
+ readonly order: import("@aethervtt/shard-core").FieldType<number, "i32">;
65
+ readonly clearColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
66
+ readonly target: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"RenderTarget"> | null, "object">;
67
+ readonly active: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
68
+ }>, {
69
+ readonly projection: 'orthographic';
70
+ readonly orthoHeight: number;
71
+ readonly order: number;
72
+ readonly clearColor?: [number, number, number, number] | undefined;
73
+ }], readonly [import("@aethervtt/shard-core").ComponentDef<{
74
+ readonly translation: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
75
+ readonly rotation: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Quat, "f32">;
76
+ readonly scale: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
77
+ }>, {
78
+ readonly translation: [number, number, number];
79
+ }]];
80
+ /** PhysicalCamera → Exposure (and fovY when a focal length is set). */
81
+ export declare const applyPhysicalCameras: import("@aethervtt/shard-core").SystemDef<{
82
+ q: import("@aethervtt/shard-core").Query;
83
+ }>;
package/dist/camera.js ADDED
@@ -0,0 +1,163 @@
1
+ import { defineComponent, defineSystem, t } from "@aethervtt/shard-core";
2
+ import { Transform } from "@aethervtt/shard-transform";
3
+ const LightPresets = {
4
+ "direct-sun": 1e5,
5
+ daylight: 1e4,
6
+ overcast: 1e3,
7
+ indoor: 400,
8
+ twilight: 10,
9
+ moonlight: 0.3
10
+ };
11
+ function ev100FromIlluminance(lux2) {
12
+ return Math.log2(lux2 / 2.5);
13
+ }
14
+ const ExposurePresets = {
15
+ sunny: ev100FromIlluminance(LightPresets["direct-sun"]),
16
+ daylight: ev100FromIlluminance(LightPresets.daylight),
17
+ overcast: ev100FromIlluminance(LightPresets.overcast),
18
+ indoor: ev100FromIlluminance(LightPresets.indoor),
19
+ twilight: ev100FromIlluminance(LightPresets.twilight)
20
+ };
21
+ const lux = (value) => typeof value === "number" ? value : LightPresets[value];
22
+ const ev100 = (value) => typeof value === "number" ? value : ExposurePresets[value];
23
+ function exposureScale(ev) {
24
+ return 1 / (1.2 * 2 ** ev);
25
+ }
26
+ function ev100FromCamera(aperture, shutterSpeed, iso) {
27
+ return Math.log2(aperture * aperture / shutterSpeed * (100 / iso));
28
+ }
29
+ const Exposure = defineComponent(
30
+ "render/Exposure",
31
+ {
32
+ ev100: t.f32({
33
+ default: ExposurePresets.daylight,
34
+ presets: ExposurePresets,
35
+ description: "How bright the scene may be. Match it to the light: EV100 = log2(lux / 2.5)."
36
+ })
37
+ },
38
+ {
39
+ description: "Camera exposure in EV100. Presets: sunny 15.3, daylight 12, overcast 8.6, indoor 7.3, twilight 2."
40
+ }
41
+ );
42
+ const Camera3d = defineComponent(
43
+ "render/Camera3d",
44
+ {
45
+ projection: t.enum(["perspective", "orthographic"]),
46
+ fovY: t.f32({
47
+ default: 60,
48
+ min: 1,
49
+ max: 179,
50
+ unit: "deg",
51
+ description: "Vertical field of view."
52
+ }),
53
+ near: t.f32({
54
+ default: 0.1,
55
+ min: 1e-4,
56
+ unit: "m",
57
+ description: "Near plane. Far is infinite."
58
+ }),
59
+ orthoHeight: t.f32({
60
+ default: 10,
61
+ min: 1e-4,
62
+ unit: "m",
63
+ description: "View height for orthographic."
64
+ }),
65
+ far: t.f32({
66
+ default: 1e3,
67
+ min: 1e-3,
68
+ unit: "m",
69
+ description: "Far plane, orthographic only."
70
+ }),
71
+ order: t.i32({ description: "Lower renders first." }),
72
+ clearColor: t.color({
73
+ default: [56e-4, 65e-4, 91e-4, 1],
74
+ description: "Background (linear)."
75
+ }),
76
+ target: t.handle("RenderTarget", { description: "Offscreen target, or null for the window." }),
77
+ active: t.bool({
78
+ default: true,
79
+ description: "Renders when true. An inactive camera keeps its settings and transform but draws nothing."
80
+ })
81
+ },
82
+ {
83
+ description: "Renders the scene from this entity, looking down its -Z axis.",
84
+ requires: [Transform, Exposure]
85
+ }
86
+ );
87
+ const PhysicalCamera = defineComponent(
88
+ "render/PhysicalCamera",
89
+ {
90
+ aperture: t.f32({ default: 4, min: 0.5, description: "f-stop." }),
91
+ shutterSpeed: t.f32({
92
+ default: 1 / 125,
93
+ min: 1e-5,
94
+ unit: "s",
95
+ description: "Exposure time."
96
+ }),
97
+ iso: t.f32({ default: 100, min: 1, description: "Sensor sensitivity." }),
98
+ sensorHeight: t.f32({
99
+ default: 24,
100
+ min: 1,
101
+ unit: "mm",
102
+ description: "Sensor height (35mm film: 24)."
103
+ }),
104
+ focalLength: t.f32({
105
+ min: 0,
106
+ unit: "mm",
107
+ description: "Lens focal length. When > 0, sets Camera3d.fovY from the sensor height."
108
+ })
109
+ },
110
+ {
111
+ description: "Photographic camera settings. When present, they set Exposure (and fovY).",
112
+ requires: [Camera3d]
113
+ }
114
+ );
115
+ function camera2d(options = {}) {
116
+ return [
117
+ [
118
+ Camera3d,
119
+ {
120
+ projection: "orthographic",
121
+ orthoHeight: options.height ?? 10,
122
+ order: options.order ?? 0,
123
+ ...options.clearColor ? { clearColor: options.clearColor } : {}
124
+ }
125
+ ],
126
+ [Transform, { translation: [0, 0, 100] }]
127
+ ];
128
+ }
129
+ const applyPhysicalCameras = defineSystem({
130
+ name: "render/physical-camera",
131
+ description: "Sets Exposure and fovY from PhysicalCamera settings.",
132
+ setup: (world) => ({ q: world.query({ with: [PhysicalCamera, Camera3d, Exposure] }) }),
133
+ run: ({ q }) => {
134
+ for (const table of q.tables) {
135
+ const aperture = table.column(PhysicalCamera, "aperture");
136
+ const shutter = table.column(PhysicalCamera, "shutterSpeed");
137
+ const iso = table.column(PhysicalCamera, "iso");
138
+ const sensor = table.column(PhysicalCamera, "sensorHeight");
139
+ const focal = table.column(PhysicalCamera, "focalLength");
140
+ const ev = table.column(Exposure, "ev100");
141
+ const fov = table.column(Camera3d, "fovY");
142
+ for (let i = 0; i < table.count; i++) {
143
+ ev[i] = ev100FromCamera(aperture[i], shutter[i], iso[i]);
144
+ if (focal[i] > 0) fov[i] = 2 * Math.atan(sensor[i] / (2 * focal[i])) * 180 / Math.PI;
145
+ }
146
+ table.markChanged(Exposure);
147
+ }
148
+ }
149
+ });
150
+ export {
151
+ Camera3d,
152
+ Exposure,
153
+ ExposurePresets,
154
+ LightPresets,
155
+ PhysicalCamera,
156
+ applyPhysicalCameras,
157
+ camera2d,
158
+ ev100,
159
+ ev100FromCamera,
160
+ ev100FromIlluminance,
161
+ exposureScale,
162
+ lux
163
+ };
@@ -0,0 +1,68 @@
1
+ import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
2
+ import type { LightRecord } from './lights.js';
3
+ import type { CameraData } from './view.js';
4
+ /** Cluster grid: 16×9 screen tiles × 24 exponential depth slices. */
5
+ export declare const CLUSTER_X = 16;
6
+ export declare const CLUSTER_Y = 9;
7
+ export declare const CLUSTER_Z = 24;
8
+ export declare const CLUSTER_COUNT: number;
9
+ export declare const MAX_LIGHTS_PER_CLUSTER = 128;
10
+ /** Floats per entry in a view's light list: view-space position + range, axis + cos(outer). */
11
+ export declare const VIEW_LIGHT_FLOATS = 12;
12
+ /** Cluster slice range for a camera: near to `clusterFar` (default: far plane, or 1 km). */
13
+ export declare function clusterRange(cam: CameraData, clusterFar: number): [number, number];
14
+ /**
15
+ * A view's clustering inputs: the visible lights in view space. Built on the CPU each frame from the
16
+ * light store (frustum-tested), read by the GPU cluster pass and by `clusterLightsCpu`.
17
+ */
18
+ export declare class ViewLightList {
19
+ count: number;
20
+ data: Float32Array<ArrayBuffer>;
21
+ u32: Uint32Array<ArrayBuffer>;
22
+ /** The records of the visible lights, in list order. */
23
+ readonly records: LightRecord[];
24
+ /** Fills the list with lights whose influence sphere intersects the camera frustum. */
25
+ build(cam: CameraData, lights: readonly (LightRecord | undefined)[], high: number): void;
26
+ }
27
+ /**
28
+ * The view-space AABB of cluster (x, y, z), written as [minX, minY, minZ, maxX, maxY, maxZ].
29
+ * Mirrors `cluster_aabb` in `shard::lighting::cluster` operation for operation, in f32.
30
+ */
31
+ export declare function clusterAabb(out: Float32Array, x: number, y: number, z: number, near: number, far: number, proj: ArrayLike<number>, orthographic: boolean): Float32Array;
32
+ /** Whether a view-space light (sphere, or cone for spots) touches a cluster AABB. Mirrors WGSL. */
33
+ export declare function lightTouchesCluster(box: Float32Array, light: Float32Array, o: number): boolean;
34
+ /** Every cluster's view-space AABB, 8 floats each (min.xyz, 0, max.xyz, 0), for a camera. */
35
+ export declare function clusterAabbs(out: Float32Array, cam: CameraData, clusterFar: number): Float32Array;
36
+ /** A key for what the cluster AABBs depend on. */
37
+ export declare function clusterKey(cam: CameraData, clusterFar: number): string;
38
+ /** CPU clustering with the same results as the GPU pass: per cluster, the light slots. */
39
+ export declare function clusterLightsCpu(list: ViewLightList, cam: CameraData, clusterFar: number): {
40
+ counts: Uint32Array;
41
+ indices: Uint32Array;
42
+ maxCount: number;
43
+ overflows: number;
44
+ };
45
+ /** Cluster statistics from the GPU, one or two frames late. */
46
+ export interface ClusterStats {
47
+ maxLightsPerCluster: number;
48
+ overflows: number;
49
+ }
50
+ /** A view's GPU cluster buffers: light list in, counts + indices out, stats read back. */
51
+ export declare class ClusterBuffers {
52
+ readonly lightList: GpuBuffer;
53
+ /** counts[CLUSTER_COUNT], then indices[CLUSTER_COUNT × MAX_LIGHTS_PER_CLUSTER]. */
54
+ readonly clusters: GpuBuffer;
55
+ readonly stats: GpuBuffer;
56
+ readonly aabbs: GpuBuffer;
57
+ private aabbKey;
58
+ private aabbGeneration;
59
+ private readonly aabbData;
60
+ readonly stats0: Uint32Array<ArrayBuffer>;
61
+ latest: ClusterStats;
62
+ private readonly readbacks;
63
+ private readonly gpu;
64
+ constructor(gpu: GpuContext, label: string);
65
+ upload(list: ViewLightList, cam: CameraData, clusterFar: number): void;
66
+ /** Copies stats to a readback buffer; resolves into `latest` when mapped. */
67
+ readback(encoder: GPUCommandEncoder): (() => void) | undefined;
68
+ }
@@ -0,0 +1,288 @@
1
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
2
+ const CLUSTER_X = 16;
3
+ const CLUSTER_Y = 9;
4
+ const CLUSTER_Z = 24;
5
+ const CLUSTER_COUNT = CLUSTER_X * CLUSTER_Y * CLUSTER_Z;
6
+ const MAX_LIGHTS_PER_CLUSTER = 128;
7
+ const VIEW_LIGHT_FLOATS = 12;
8
+ function clusterRange(cam, clusterFar) {
9
+ const near = Math.max(cam.near, 1e-4);
10
+ const far = clusterFar > 0 ? clusterFar : cam.orthographic && cam.far > 0 ? cam.far : 1e3;
11
+ return [near, Math.max(far, near * 1.01)];
12
+ }
13
+ class ViewLightList {
14
+ count = 0;
15
+ data = new Float32Array(4 + 64 * VIEW_LIGHT_FLOATS);
16
+ u32 = new Uint32Array(this.data.buffer);
17
+ /** The records of the visible lights, in list order. */
18
+ records = [];
19
+ /** Fills the list with lights whose influence sphere intersects the camera frustum. */
20
+ build(cam, lights, high) {
21
+ const f = cam.frustum;
22
+ const v = cam.view;
23
+ this.records.length = 0;
24
+ let n = 0;
25
+ for (let s = 0; s < high; s++) {
26
+ const r = lights[s];
27
+ if (!r?.alive || r.range <= 0) continue;
28
+ let inside = true;
29
+ for (let p = 0; p < 24; p += 4) {
30
+ if (f[p] * r.x + f[p + 1] * r.y + f[p + 2] * r.z + f[p + 3] < -r.range) {
31
+ inside = false;
32
+ break;
33
+ }
34
+ }
35
+ if (!inside) continue;
36
+ const o = 4 + n * VIEW_LIGHT_FLOATS;
37
+ if (o + VIEW_LIGHT_FLOATS > this.data.length) {
38
+ const grown = new Float32Array(this.data.length * 2);
39
+ grown.set(this.data);
40
+ this.data = grown;
41
+ this.u32 = new Uint32Array(grown.buffer);
42
+ }
43
+ const d = this.data;
44
+ d[o] = v[0] * r.x + v[4] * r.y + v[8] * r.z + v[12];
45
+ d[o + 1] = v[1] * r.x + v[5] * r.y + v[9] * r.z + v[13];
46
+ d[o + 2] = v[2] * r.x + v[6] * r.y + v[10] * r.z + v[14];
47
+ d[o + 3] = r.range;
48
+ d[o + 4] = v[0] * r.dx + v[4] * r.dy + v[8] * r.dz;
49
+ d[o + 5] = v[1] * r.dx + v[5] * r.dy + v[9] * r.dz;
50
+ d[o + 6] = v[2] * r.dx + v[6] * r.dy + v[10] * r.dz;
51
+ d[o + 7] = r.cosOuter;
52
+ this.u32[o + 8] = r.slot;
53
+ this.u32[o + 9] = 0;
54
+ this.u32[o + 10] = 0;
55
+ this.u32[o + 11] = 0;
56
+ this.records.push(r);
57
+ n++;
58
+ }
59
+ this.count = n;
60
+ this.u32[0] = n;
61
+ }
62
+ }
63
+ const f32 = Math.fround;
64
+ function clusterAabb(out, x, y, z, near, far, proj, orthographic) {
65
+ const ratio = f32(far / near);
66
+ const d0 = f32(near * f32(ratio ** f32(z / CLUSTER_Z)));
67
+ const d1 = f32(near * f32(ratio ** f32((z + 1) / CLUSTER_Z)));
68
+ const x0 = f32(-1 + f32(f32(2 * x) / CLUSTER_X));
69
+ const x1 = f32(-1 + f32(f32(2 * (x + 1)) / CLUSTER_X));
70
+ const y1 = f32(1 - f32(f32(2 * y) / CLUSTER_Y));
71
+ const y0 = f32(1 - f32(f32(2 * (y + 1)) / CLUSTER_Y));
72
+ const p00 = proj[0];
73
+ const p11 = proj[5];
74
+ let minX;
75
+ let maxX;
76
+ let minY;
77
+ let maxY;
78
+ if (orthographic) {
79
+ minX = f32(f32(x0 - proj[12]) / p00);
80
+ maxX = f32(f32(x1 - proj[12]) / p00);
81
+ minY = f32(f32(y0 - proj[13]) / p11);
82
+ maxY = f32(f32(y1 - proj[13]) / p11);
83
+ } else {
84
+ const ax = f32(f32(x0 * d0) / p00);
85
+ const bx = f32(f32(x0 * d1) / p00);
86
+ const cx = f32(f32(x1 * d0) / p00);
87
+ const dx = f32(f32(x1 * d1) / p00);
88
+ minX = Math.min(ax, bx, cx, dx);
89
+ maxX = Math.max(ax, bx, cx, dx);
90
+ const ay = f32(f32(y0 * d0) / p11);
91
+ const by = f32(f32(y0 * d1) / p11);
92
+ const cy = f32(f32(y1 * d0) / p11);
93
+ const dy = f32(f32(y1 * d1) / p11);
94
+ minY = Math.min(ay, by, cy, dy);
95
+ maxY = Math.max(ay, by, cy, dy);
96
+ }
97
+ out[0] = minX;
98
+ out[1] = minY;
99
+ out[2] = -d1;
100
+ out[3] = maxX;
101
+ out[4] = maxY;
102
+ out[5] = -d0;
103
+ return out;
104
+ }
105
+ function lightTouchesCluster(box, light, o) {
106
+ const px = light[o];
107
+ const py = light[o + 1];
108
+ const pz = light[o + 2];
109
+ const range = light[o + 3];
110
+ const cx = Math.min(Math.max(px, box[0]), box[3]);
111
+ const cy = Math.min(Math.max(py, box[1]), box[4]);
112
+ const cz = Math.min(Math.max(pz, box[2]), box[5]);
113
+ const ex = f32(cx - px);
114
+ const ey = f32(cy - py);
115
+ const ez = f32(cz - pz);
116
+ if (f32(f32(f32(ex * ex) + f32(ey * ey)) + f32(ez * ez)) > f32(range * range)) return false;
117
+ const cosOuter = light[o + 7];
118
+ if (cosOuter < -1) return true;
119
+ const sx = f32(f32(f32(box[0] + box[3]) * 0.5) - px);
120
+ const sy = f32(f32(f32(box[1] + box[4]) * 0.5) - py);
121
+ const sz = f32(f32(f32(box[2] + box[5]) * 0.5) - pz);
122
+ const hx = f32(f32(box[3] - box[0]) * 0.5);
123
+ const hy = f32(f32(box[4] - box[1]) * 0.5);
124
+ const hz = f32(f32(box[5] - box[2]) * 0.5);
125
+ const radius = f32(Math.sqrt(f32(f32(f32(hx * hx) + f32(hy * hy)) + f32(hz * hz))));
126
+ const lenSq = f32(f32(f32(sx * sx) + f32(sy * sy)) + f32(sz * sz));
127
+ const v1 = f32(f32(f32(sx * light[o + 4]) + f32(sy * light[o + 5])) + f32(sz * light[o + 6]));
128
+ const sinOuter = f32(Math.sqrt(Math.max(0, f32(1 - f32(cosOuter * cosOuter)))));
129
+ const closest = f32(
130
+ f32(cosOuter * f32(Math.sqrt(Math.max(0, f32(lenSq - f32(v1 * v1)))))) - f32(v1 * sinOuter)
131
+ );
132
+ if (closest > radius) return false;
133
+ if (v1 > f32(radius + range)) return false;
134
+ if (v1 < -radius) return false;
135
+ return true;
136
+ }
137
+ function clusterAabbs(out, cam, clusterFar) {
138
+ const [near, far] = clusterRange(cam, clusterFar);
139
+ const box = new Float32Array(6);
140
+ for (let c = 0; c < CLUSTER_COUNT; c++) {
141
+ const x = c % CLUSTER_X;
142
+ const y = Math.floor(c / CLUSTER_X) % CLUSTER_Y;
143
+ const z = Math.floor(c / (CLUSTER_X * CLUSTER_Y));
144
+ clusterAabb(box, x, y, z, near, far, cam.proj, cam.orthographic);
145
+ out[c * 8] = box[0];
146
+ out[c * 8 + 1] = box[1];
147
+ out[c * 8 + 2] = box[2];
148
+ out[c * 8 + 4] = box[3];
149
+ out[c * 8 + 5] = box[4];
150
+ out[c * 8 + 6] = box[5];
151
+ }
152
+ return out;
153
+ }
154
+ function clusterKey(cam, clusterFar) {
155
+ const [near, far] = clusterRange(cam, clusterFar);
156
+ const p = cam.proj;
157
+ return `${near}|${far}|${p[0]}|${p[5]}|${p[12]}|${p[13]}|${cam.orthographic}`;
158
+ }
159
+ function clusterLightsCpu(list, cam, clusterFar) {
160
+ const aabbs = clusterAabbs(new Float32Array(CLUSTER_COUNT * 8), cam, clusterFar);
161
+ const counts = new Uint32Array(CLUSTER_COUNT);
162
+ const indices = new Uint32Array(CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER);
163
+ const box = new Float32Array(6);
164
+ const light = new Float32Array(list.data.buffer);
165
+ const u32 = list.u32;
166
+ let maxCount = 0;
167
+ let overflows = 0;
168
+ for (let c = 0; c < CLUSTER_COUNT; c++) {
169
+ box[0] = aabbs[c * 8];
170
+ box[1] = aabbs[c * 8 + 1];
171
+ box[2] = aabbs[c * 8 + 2];
172
+ box[3] = aabbs[c * 8 + 4];
173
+ box[4] = aabbs[c * 8 + 5];
174
+ box[5] = aabbs[c * 8 + 6];
175
+ let n = 0;
176
+ for (let i = 0; i < list.count; i++) {
177
+ const o = 4 + i * VIEW_LIGHT_FLOATS;
178
+ if (!lightTouchesCluster(box, light, o)) continue;
179
+ if (n < MAX_LIGHTS_PER_CLUSTER) indices[c * MAX_LIGHTS_PER_CLUSTER + n] = u32[o + 8];
180
+ n++;
181
+ }
182
+ counts[c] = Math.min(n, MAX_LIGHTS_PER_CLUSTER);
183
+ if (n > maxCount) maxCount = n;
184
+ if (n > MAX_LIGHTS_PER_CLUSTER) overflows++;
185
+ }
186
+ return { counts, indices, maxCount, overflows };
187
+ }
188
+ class ClusterBuffers {
189
+ lightList;
190
+ /** counts[CLUSTER_COUNT], then indices[CLUSTER_COUNT × MAX_LIGHTS_PER_CLUSTER]. */
191
+ clusters;
192
+ stats;
193
+ aabbs;
194
+ aabbKey = "";
195
+ aabbGeneration = -1;
196
+ aabbData = new Float32Array(CLUSTER_COUNT * 8);
197
+ stats0 = new Uint32Array(4);
198
+ latest = { maxLightsPerCluster: 0, overflows: 0 };
199
+ readbacks = [];
200
+ gpu;
201
+ constructor(gpu, label) {
202
+ this.gpu = gpu;
203
+ this.lightList = new GpuBuffer(gpu, {
204
+ label: `${label}/light-list`,
205
+ usage: GPUBufferUsage.STORAGE,
206
+ size: (4 + 64 * VIEW_LIGHT_FLOATS) * 4
207
+ });
208
+ this.clusters = new GpuBuffer(gpu, {
209
+ label: `${label}/clusters`,
210
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
211
+ size: CLUSTER_COUNT * (1 + MAX_LIGHTS_PER_CLUSTER) * 4
212
+ });
213
+ this.aabbs = new GpuBuffer(gpu, {
214
+ label: `${label}/cluster-aabbs`,
215
+ usage: GPUBufferUsage.STORAGE,
216
+ size: CLUSTER_COUNT * 8 * 4
217
+ });
218
+ this.stats = new GpuBuffer(gpu, {
219
+ label: `${label}/cluster-stats`,
220
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
221
+ size: 16
222
+ });
223
+ }
224
+ upload(list, cam, clusterFar) {
225
+ const key = clusterKey(cam, clusterFar);
226
+ if (key !== this.aabbKey || this.aabbGeneration !== this.gpu.generation) {
227
+ this.aabbKey = key;
228
+ this.aabbGeneration = this.gpu.generation;
229
+ this.aabbs.write(clusterAabbs(this.aabbData, cam, clusterFar));
230
+ }
231
+ this.lightList.write(list.data, 0, 0, 4 + Math.max(1, list.count) * VIEW_LIGHT_FLOATS);
232
+ this.stats.write(this.stats0);
233
+ }
234
+ /** Copies stats to a readback buffer; resolves into `latest` when mapped. */
235
+ readback(encoder) {
236
+ const gen = this.gpu.generation;
237
+ let rb = this.readbacks.find((r) => !r.busy && r.generation === gen);
238
+ if (!rb && this.readbacks.length < 3) {
239
+ rb = {
240
+ buffer: this.gpu.device.createBuffer({
241
+ label: "cluster-stats/readback",
242
+ size: 16,
243
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
244
+ }),
245
+ busy: false,
246
+ generation: gen
247
+ };
248
+ this.readbacks.push(rb);
249
+ }
250
+ if (!rb) {
251
+ for (let i = this.readbacks.length - 1; i >= 0; i--)
252
+ if (this.readbacks[i].generation !== gen) this.readbacks.splice(i, 1);
253
+ return void 0;
254
+ }
255
+ encoder.copyBufferToBuffer(this.stats.buffer, 0, rb.buffer, 0, 16);
256
+ rb.busy = true;
257
+ const target = rb;
258
+ return () => {
259
+ target.buffer.mapAsync(GPUMapMode.READ).then(
260
+ () => {
261
+ const v = new Uint32Array(target.buffer.getMappedRange().slice(0));
262
+ target.buffer.unmap();
263
+ target.busy = false;
264
+ this.latest = { maxLightsPerCluster: v[0], overflows: v[1] };
265
+ },
266
+ () => {
267
+ target.busy = false;
268
+ }
269
+ );
270
+ };
271
+ }
272
+ }
273
+ export {
274
+ CLUSTER_COUNT,
275
+ CLUSTER_X,
276
+ CLUSTER_Y,
277
+ CLUSTER_Z,
278
+ ClusterBuffers,
279
+ MAX_LIGHTS_PER_CLUSTER,
280
+ VIEW_LIGHT_FLOATS,
281
+ ViewLightList,
282
+ clusterAabb,
283
+ clusterAabbs,
284
+ clusterKey,
285
+ clusterLightsCpu,
286
+ clusterRange,
287
+ lightTouchesCluster
288
+ };
@@ -0,0 +1,2 @@
1
+ /** WGSL for GPU culling (see culling.ts; the CPU path in instances.ts mirrors every test). */
2
+ export declare const CULLING_SHADERS: Record<string, string>;