@plasius/gpu-lighting 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +203 -0
  3. package/README.md +101 -0
  4. package/dist/index.cjs +318 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.js +275 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/techniques/hdri/brdf-lut.job.wgsl +3 -0
  9. package/dist/techniques/hdri/irradiance-convolution.job.wgsl +3 -0
  10. package/dist/techniques/hdri/prelude.wgsl +10 -0
  11. package/dist/techniques/hdri/specular-prefilter.job.wgsl +3 -0
  12. package/dist/techniques/hybrid/direct-lighting.job.wgsl +3 -0
  13. package/dist/techniques/hybrid/final-gather.job.wgsl +3 -0
  14. package/dist/techniques/hybrid/prelude.wgsl +15 -0
  15. package/dist/techniques/hybrid/radiance-cache.job.wgsl +3 -0
  16. package/dist/techniques/hybrid/reflection-resolve.job.wgsl +3 -0
  17. package/dist/techniques/hybrid/screen-trace.job.wgsl +3 -0
  18. package/dist/techniques/pathtracer/accumulate.job.wgsl +3 -0
  19. package/dist/techniques/pathtracer/denoise.job.wgsl +3 -0
  20. package/dist/techniques/pathtracer/pathtrace.job.wgsl +3 -0
  21. package/dist/techniques/pathtracer/prelude.wgsl +15 -0
  22. package/dist/techniques/volumetrics/froxel-integrate.job.wgsl +3 -0
  23. package/dist/techniques/volumetrics/prelude.wgsl +15 -0
  24. package/dist/techniques/volumetrics/volumetric-shadow.job.wgsl +3 -0
  25. package/legal/CLA-REGISTRY.csv +2 -0
  26. package/legal/CLA.md +22 -0
  27. package/legal/CORPORATE_CLA.md +57 -0
  28. package/legal/INDIVIDUAL_CLA.md +91 -0
  29. package/package.json +78 -0
  30. package/src/index.js +296 -0
  31. package/src/techniques/hdri/brdf-lut.job.wgsl +3 -0
  32. package/src/techniques/hdri/irradiance-convolution.job.wgsl +3 -0
  33. package/src/techniques/hdri/prelude.wgsl +10 -0
  34. package/src/techniques/hdri/specular-prefilter.job.wgsl +3 -0
  35. package/src/techniques/hybrid/direct-lighting.job.wgsl +3 -0
  36. package/src/techniques/hybrid/final-gather.job.wgsl +3 -0
  37. package/src/techniques/hybrid/prelude.wgsl +15 -0
  38. package/src/techniques/hybrid/radiance-cache.job.wgsl +3 -0
  39. package/src/techniques/hybrid/reflection-resolve.job.wgsl +3 -0
  40. package/src/techniques/hybrid/screen-trace.job.wgsl +3 -0
  41. package/src/techniques/pathtracer/accumulate.job.wgsl +3 -0
  42. package/src/techniques/pathtracer/denoise.job.wgsl +3 -0
  43. package/src/techniques/pathtracer/pathtrace.job.wgsl +3 -0
  44. package/src/techniques/pathtracer/prelude.wgsl +15 -0
  45. package/src/techniques/volumetrics/froxel-integrate.job.wgsl +3 -0
  46. package/src/techniques/volumetrics/prelude.wgsl +15 -0
  47. package/src/techniques/volumetrics/volumetric-shadow.job.wgsl +3 -0
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@plasius/gpu-lighting",
3
+ "version": "0.1.0",
4
+ "description": "Advanced lighting WGSL modules and planning profiles for @plasius/gpu-worker.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "private": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "src",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "LICENSE",
16
+ "legal"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ },
23
+ "./techniques/*": "./dist/techniques/*",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup && cp -R src/techniques dist/techniques",
28
+ "demo": "python3 -m http.server --directory ..",
29
+ "test": "npm run test:unit",
30
+ "test:unit": "node --test",
31
+ "test:e2e": "npx playwright install chromium && playwright test",
32
+ "test:coverage": "c8 --reporter=lcov --reporter=text node --test"
33
+ },
34
+ "keywords": [
35
+ "webgpu",
36
+ "lighting",
37
+ "global-illumination",
38
+ "lumen",
39
+ "path-tracing",
40
+ "volumetrics",
41
+ "wgsl",
42
+ "gpu",
43
+ "compute",
44
+ "plasius"
45
+ ],
46
+ "author": "Plasius LTD <development@plasius.co.uk>",
47
+ "license": "Apache-2.0",
48
+ "devDependencies": {
49
+ "@playwright/test": "^1.57.0",
50
+ "c8": "^10.1.3",
51
+ "tsup": "^8.5.0",
52
+ "typescript": "^5.9.3"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/Plasius-LTD/gpu-lighting.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/Plasius-LTD/gpu-lighting/issues"
60
+ },
61
+ "homepage": "https://github.com/Plasius-LTD/gpu-lighting#readme",
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "funding": [
66
+ {
67
+ "type": "patreon",
68
+ "url": "https://www.patreon.com/c/plasiusltd/membership"
69
+ },
70
+ {
71
+ "type": "github",
72
+ "url": "https://github.com/sponsors/Plasius-LTD"
73
+ }
74
+ ],
75
+ "dependencies": {
76
+ "@plasius/gpu-worker": "^0.1.2"
77
+ }
78
+ }
package/src/index.js ADDED
@@ -0,0 +1,296 @@
1
+ const baseUrl = (() => {
2
+ if (typeof __IMPORT_META_URL__ !== "undefined") {
3
+ return new URL("./index.js", __IMPORT_META_URL__);
4
+ }
5
+ if (typeof __filename !== "undefined" && typeof require !== "undefined") {
6
+ const { pathToFileURL } = require("node:url");
7
+ return pathToFileURL(__filename);
8
+ }
9
+ const base =
10
+ typeof process !== "undefined" && process.cwd
11
+ ? `file://${process.cwd()}/`
12
+ : "file:///";
13
+ return new URL("./index.js", base);
14
+ })();
15
+
16
+ const techniqueSpecs = {
17
+ hybrid: {
18
+ description:
19
+ "Lumen-inspired hybrid realtime GI and reflections with radiance cache final gather.",
20
+ prelude: "prelude.wgsl",
21
+ jobs: {
22
+ directLighting: "direct-lighting.job.wgsl",
23
+ screenTrace: "screen-trace.job.wgsl",
24
+ radianceCache: "radiance-cache.job.wgsl",
25
+ finalGather: "final-gather.job.wgsl",
26
+ reflectionResolve: "reflection-resolve.job.wgsl",
27
+ },
28
+ },
29
+ pathtracer: {
30
+ description:
31
+ "Monte Carlo path-traced reference mode with progressive accumulation and denoise stage.",
32
+ prelude: "prelude.wgsl",
33
+ jobs: {
34
+ pathTrace: "pathtrace.job.wgsl",
35
+ accumulate: "accumulate.job.wgsl",
36
+ denoise: "denoise.job.wgsl",
37
+ },
38
+ },
39
+ volumetrics: {
40
+ description:
41
+ "Froxel volumetric lighting for fog, shafts, and participating media shadows.",
42
+ prelude: "prelude.wgsl",
43
+ jobs: {
44
+ froxelIntegrate: "froxel-integrate.job.wgsl",
45
+ volumetricShadow: "volumetric-shadow.job.wgsl",
46
+ },
47
+ },
48
+ hdri: {
49
+ description:
50
+ "HDRI and IBL precompute passes including irradiance, specular prefilter, and BRDF LUT.",
51
+ prelude: "prelude.wgsl",
52
+ jobs: {
53
+ irradianceConvolution: "irradiance-convolution.job.wgsl",
54
+ specularPrefilter: "specular-prefilter.job.wgsl",
55
+ brdfLut: "brdf-lut.job.wgsl",
56
+ },
57
+ },
58
+ };
59
+
60
+ function buildTechnique(name, spec) {
61
+ const preludeUrl = new URL(`./techniques/${name}/${spec.prelude}`, baseUrl);
62
+ const jobs = Object.entries(spec.jobs).map(([key, file]) => {
63
+ const label = `lighting.${name}.${key}`;
64
+ return {
65
+ key,
66
+ label,
67
+ url: new URL(`./techniques/${name}/${file}`, baseUrl),
68
+ sourceName: label,
69
+ };
70
+ });
71
+ return {
72
+ name,
73
+ description: spec.description,
74
+ preludeUrl,
75
+ jobs,
76
+ };
77
+ }
78
+
79
+ export const lightingTechniques = Object.freeze(
80
+ Object.fromEntries(
81
+ Object.entries(techniqueSpecs).map(([name, spec]) => [
82
+ name,
83
+ buildTechnique(name, spec),
84
+ ])
85
+ )
86
+ );
87
+
88
+ export const lightingTechniqueNames = Object.freeze(Object.keys(lightingTechniques));
89
+
90
+ export const defaultLightingTechnique = "hybrid";
91
+
92
+ const profileSpecs = {
93
+ realtime: {
94
+ description:
95
+ "Primary runtime profile: hybrid GI/reflections with volumetrics and HDRI/IBL.",
96
+ techniques: ["hybrid", "volumetrics", "hdri"],
97
+ },
98
+ hybrid: {
99
+ description:
100
+ "Hybrid-focused profile for direct tuning of Lumen-inspired realtime passes.",
101
+ techniques: ["hybrid", "hdri"],
102
+ },
103
+ reference: {
104
+ description:
105
+ "Reference quality profile: path tracing plus volumetrics and HDRI/IBL validation.",
106
+ techniques: ["pathtracer", "volumetrics", "hdri"],
107
+ },
108
+ };
109
+
110
+ function buildProfile(name, spec) {
111
+ return {
112
+ name,
113
+ description: spec.description,
114
+ techniques: [...spec.techniques],
115
+ };
116
+ }
117
+
118
+ export const lightingProfiles = Object.freeze(
119
+ Object.fromEntries(
120
+ Object.entries(profileSpecs).map(([name, spec]) => [
121
+ name,
122
+ buildProfile(name, spec),
123
+ ])
124
+ )
125
+ );
126
+
127
+ export const lightingProfileNames = Object.freeze(Object.keys(lightingProfiles));
128
+
129
+ export const defaultLightingProfile = "realtime";
130
+
131
+ function getTechniqueJob(technique, key) {
132
+ const job = technique.jobs.find((entry) => entry.key === key);
133
+ if (!job) {
134
+ const available = technique.jobs.map((entry) => entry.key).join(", ");
135
+ throw new Error(
136
+ `Unknown job "${key}" for technique "${technique.name}". ` +
137
+ `Available: ${available}.`
138
+ );
139
+ }
140
+ return job;
141
+ }
142
+
143
+ export function getLightingTechnique(name = defaultLightingTechnique) {
144
+ const technique = lightingTechniques[name];
145
+ if (!technique) {
146
+ const available = lightingTechniqueNames.join(", ");
147
+ throw new Error(`Unknown lighting technique "${name}". Available: ${available}.`);
148
+ }
149
+ return technique;
150
+ }
151
+
152
+ export function getLightingProfile(name = defaultLightingProfile) {
153
+ const profile = lightingProfiles[name];
154
+ if (!profile) {
155
+ const available = lightingProfileNames.join(", ");
156
+ throw new Error(`Unknown lighting profile "${name}". Available: ${available}.`);
157
+ }
158
+ return profile;
159
+ }
160
+
161
+ const defaultTechnique = getLightingTechnique(defaultLightingTechnique);
162
+
163
+ export const lightingPreludeWgslUrl = defaultTechnique.preludeUrl;
164
+
165
+ export const lightingJobLabels = Object.freeze(
166
+ Object.fromEntries(defaultTechnique.jobs.map((job) => [job.key, job.label]))
167
+ );
168
+
169
+ export const lightingJobs = defaultTechnique.jobs.map((job) => ({
170
+ label: job.label,
171
+ url: job.url,
172
+ sourceName: job.sourceName,
173
+ }));
174
+
175
+ function assertNotHtmlWgsl(source, context) {
176
+ const sample = source.slice(0, 200).toLowerCase();
177
+ if (
178
+ sample.includes("<!doctype") ||
179
+ sample.includes("<html") ||
180
+ sample.includes("<meta")
181
+ ) {
182
+ const label = context ? ` for ${context}` : "";
183
+ throw new Error(
184
+ `Expected WGSL${label} but received HTML. Check the URL or server root.`
185
+ );
186
+ }
187
+ }
188
+
189
+ async function loadWgslSource(options = {}) {
190
+ const { wgsl, url, fetcher = globalThis.fetch, base } = options ?? {};
191
+ if (typeof wgsl === "string") {
192
+ assertNotHtmlWgsl(wgsl, "inline WGSL");
193
+ return wgsl;
194
+ }
195
+ if (!url) {
196
+ return null;
197
+ }
198
+ const resolved = url instanceof URL ? url : new URL(url, base ?? baseUrl);
199
+ if (!fetcher || resolved.protocol === "file:") {
200
+ const { readFile } = await import("node:fs/promises");
201
+ const { fileURLToPath } = await import("node:url");
202
+ const source = await readFile(fileURLToPath(resolved), "utf8");
203
+ assertNotHtmlWgsl(source, resolved.href);
204
+ return source;
205
+ }
206
+ const response = await fetcher(resolved);
207
+ if (!response.ok) {
208
+ const status = "status" in response ? response.status : "unknown";
209
+ const statusText = "statusText" in response ? response.statusText : "";
210
+ const detail = statusText ? `${status} ${statusText}` : `${status}`;
211
+ throw new Error(`Failed to load WGSL (${detail})`);
212
+ }
213
+ const source = await response.text();
214
+ assertNotHtmlWgsl(source, resolved.href);
215
+ return source;
216
+ }
217
+
218
+ async function loadTechniquePrelude(technique, fetcher) {
219
+ const source = await loadWgslSource({ url: technique.preludeUrl, fetcher });
220
+ if (typeof source !== "string") {
221
+ throw new Error(`Failed to load ${technique.name} prelude WGSL source.`);
222
+ }
223
+ return source;
224
+ }
225
+
226
+ async function loadTechniqueJob(technique, job, fetcher) {
227
+ const source = await loadWgslSource({ url: job.url, fetcher });
228
+ if (typeof source !== "string") {
229
+ throw new Error(
230
+ `Failed to load ${technique.name} job "${job.key}" WGSL source.`
231
+ );
232
+ }
233
+ return source;
234
+ }
235
+
236
+ export async function loadLightingTechniquePreludeWgsl(
237
+ techniqueName,
238
+ options = {}
239
+ ) {
240
+ const { fetcher } = options ?? {};
241
+ const technique = getLightingTechnique(techniqueName);
242
+ return loadTechniquePrelude(technique, fetcher);
243
+ }
244
+
245
+ export async function loadLightingTechniqueJobWgsl(
246
+ techniqueName,
247
+ jobKey,
248
+ options = {}
249
+ ) {
250
+ const { fetcher } = options ?? {};
251
+ const technique = getLightingTechnique(techniqueName);
252
+ const job = getTechniqueJob(technique, jobKey);
253
+ return loadTechniqueJob(technique, job, fetcher);
254
+ }
255
+
256
+ export async function loadLightingTechniqueJobs(techniqueName, options = {}) {
257
+ const { fetcher } = options ?? {};
258
+ const technique = getLightingTechnique(techniqueName);
259
+ const preludeWgsl = await loadTechniquePrelude(technique, fetcher);
260
+ const jobSources = await Promise.all(
261
+ technique.jobs.map((job) => loadTechniqueJob(technique, job, fetcher))
262
+ );
263
+ const jobs = technique.jobs.map((job, index) => ({
264
+ wgsl: jobSources[index],
265
+ label: job.label,
266
+ sourceName: job.sourceName,
267
+ }));
268
+ return { preludeWgsl, jobs };
269
+ }
270
+
271
+ export async function loadLightingPreludeWgsl(options = {}) {
272
+ const { fetcher } = options ?? {};
273
+ return loadTechniquePrelude(defaultTechnique, fetcher);
274
+ }
275
+
276
+ export async function loadLightingJobs(options = {}) {
277
+ return loadLightingTechniqueJobs(defaultLightingTechnique, options);
278
+ }
279
+
280
+ export async function loadLightingProfile(profileName, options = {}) {
281
+ const profile = getLightingProfile(profileName);
282
+ const techniques = await Promise.all(
283
+ profile.techniques.map(async (techniqueName) => {
284
+ const { preludeWgsl, jobs } = await loadLightingTechniqueJobs(
285
+ techniqueName,
286
+ options
287
+ );
288
+ return {
289
+ technique: techniqueName,
290
+ preludeWgsl,
291
+ jobs,
292
+ };
293
+ })
294
+ );
295
+ return { profile, techniques };
296
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder BRDF LUT generation stage.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder irradiance convolution stage.
3
+ }
@@ -0,0 +1,10 @@
1
+ struct IblPrecomputeParams {
2
+ mip_level: u32,
3
+ sample_count: u32,
4
+ roughness: f32,
5
+ exposure_bias: f32,
6
+ };
7
+
8
+ fn clamp_roughness(value: f32) -> f32 {
9
+ return clamp(value, 0.0, 1.0);
10
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder specular prefilter stage.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder direct lighting resolve for the hybrid technique.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder final gather stage that combines cache data and traces.
3
+ }
@@ -0,0 +1,15 @@
1
+ struct HybridFrameParams {
2
+ frame_index: u32,
3
+ max_trace_steps: u32,
4
+ history_weight: f32,
5
+ exposure: f32,
6
+ };
7
+
8
+ struct HybridHit {
9
+ radiance: vec3<f32>,
10
+ hit_distance: f32,
11
+ };
12
+
13
+ fn encode_history_weight(value: f32) -> f32 {
14
+ return clamp(value, 0.0, 1.0);
15
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder radiance cache update stage for indirect lighting reuse.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder reflection resolve stage for rough/specular response.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder screen-space tracing stage for first-hit reuse.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder progressive accumulation stage.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder denoise stage for interactive reference previews.
3
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder path tracing kernel for reference rendering.
3
+ }
@@ -0,0 +1,15 @@
1
+ struct PathTracerParams {
2
+ frame_index: u32,
3
+ max_bounces: u32,
4
+ samples_per_pixel: u32,
5
+ enable_next_event_estimation: u32,
6
+ };
7
+
8
+ struct PathSample {
9
+ radiance: vec3<f32>,
10
+ throughput: vec3<f32>,
11
+ };
12
+
13
+ fn luminance(value: vec3<f32>) -> f32 {
14
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
15
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder froxel integration stage for volumetric lighting.
3
+ }
@@ -0,0 +1,15 @@
1
+ struct FroxelGridParams {
2
+ grid_width: u32,
3
+ grid_height: u32,
4
+ grid_depth: u32,
5
+ scattering_strength: f32,
6
+ };
7
+
8
+ struct MediumSample {
9
+ extinction: vec3<f32>,
10
+ inscattering: vec3<f32>,
11
+ };
12
+
13
+ fn saturate(value: f32) -> f32 {
14
+ return clamp(value, 0.0, 1.0);
15
+ }
@@ -0,0 +1,3 @@
1
+ fn process_job() {
2
+ // Placeholder volumetric shadow resolve stage.
3
+ }