luxscura 0.1.0 → 0.2.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 CHANGED
@@ -1,99 +1,595 @@
1
1
  # Luxscura
2
2
 
3
- ## A TypeGPU Raymarching Renderer
3
+ A TypeGPU ray marching renderer for custom WebGPU applications. Supply a camera,
4
+ geometry as a signed-distance function, and an appearance; Luxscura handles
5
+ ray marching, surface normals, depth, and drawing. Use it alongside your other
6
+ rendering pipelines, with your application's TypeGPU root and render attachments.
4
7
 
5
- Luxscura is currently in early development. Its package structure and public API
6
- may change.
8
+ See the [website and interactive examples](https://alexjwayne.github.io/luxscura/).
7
9
 
8
- ## TypeGPU setup
10
+ ⚠️ Luxscura is in early development; its public API may change.
9
11
 
10
- Luxscura is build on top of TypeGPU. You will want to set it up in your project as well as the the build plugin so your shaders can get compiled from TypeScript.
12
+ ## Setup
11
13
 
12
- - https://docs.swmansion.com/TypeGPU/getting-started/
13
- - https://docs.swmansion.com/TypeGPU/tooling/unplugin-typegpu/
14
+ ```sh
15
+ npm install luxscura typegpu
16
+ npm install --save-dev unplugin-typegpu
17
+ ```
18
+
19
+ Configure TypeGPU's build plugin so GPU callbacks can compile from TypeScript:
20
+ [TypeGPU setup](https://docs.swmansion.com/TypeGPU/getting-started/) ·
21
+ [Build plugin](https://docs.swmansion.com/TypeGPU/tooling/unplugin-typegpu/).
22
+ Configuring `tsover` is highly recommended for operator overloading on vectors
23
+ and matrices in GPU callbacks, as used in the examples below. Follow the
24
+ [TypeGPU setup guide](https://docs.swmansion.com/TypeGPU/getting-started/) for configuration.
25
+
26
+ Your application needs a browser with WebGPU support.
14
27
 
15
- ## Usage
28
+ ## Quick start
16
29
 
17
- 1. Create a raymarching program:
30
+ A program combines a camera callback, a surface, and an appearance. The surface's
31
+ `sd` callback returns the signed distance at a world-space point; its `bounds`
32
+ callback provides an axis-aligned bounding box enclosing the surface. Marching
33
+ stops at the box's exit by default. GPU callbacks use `'use gpu'`.
34
+
35
+ This example uses `@typegpu/sdf` for a sphere.
36
+ `createCamera` belongs to your application; see [Camera](#camera) for an example.
18
37
 
19
38
  ```ts
20
- import { createRaymarchProgram } from 'luxscura'
21
-
22
- // This is a camera position and view projection matrix
23
- const camera = createCamera()
24
-
25
- const program = createRaymarchProgram({ epsilon: 0.001 }, () => {
26
- return {
27
- camera: () => {
28
- 'use gpu'
29
- return RaymarchCamera({
30
- position: camera.cameraPosition,
31
- viewProjectionMatrix: camera.viewProjectionMatrix,
32
- })
33
- },
34
-
35
- surface: {
36
- bounds: () => {
37
- 'use gpu'
38
- return AABB({ min: vec3f(-1), max: vec3f(1) })
39
- },
40
- sd: (point) => {
41
- 'use gpu'
42
- return sdSphere(point, 1)
43
- },
44
- },
45
-
46
- appearance: createMatteAppearance({
47
- color: () => {
48
- 'use gpu'
49
- return vec3f(0.15, 0.5, 0.85)
50
- },
51
- lighting: createRaymarchConstantLighting({
52
- ambient: vec3f(0.2, 0.05, 0.05),
53
- directionalLights: [
54
- {
55
- direction: vec3f(-1, 1, -1),
56
- color: vec3f(1, 0.9, 0.75),
57
- intensity: 1,
58
- },
59
- ],
60
- }),
61
- }),
62
- }
39
+ import { sdSphere } from '@typegpu/sdf'
40
+ import {
41
+ AABB,
42
+ createRaymarchConstantLighting,
43
+ createRaymarchProgram,
44
+ createRaymarchRenderer,
45
+ RaymarchCamera,
46
+ } from 'luxscura'
47
+ import { createMatteAppearance } from 'luxscura/matte'
48
+ import { vec3f, vec4f } from 'typegpu/data'
49
+
50
+ // A canvas with its drawing-buffer width and height already set.
51
+ const camera = createCamera(canvas)
52
+ const lighting = createRaymarchConstantLighting({
53
+ ambient: vec3f(0.2),
54
+ directionalLights: [{
55
+ direction: vec3f(-1, 1, -1),
56
+ color: vec3f(1, 0.9, 0.75),
57
+ intensity: 1,
58
+ }],
59
+ })
60
+
61
+ const program = createRaymarchProgram({
62
+ options: { epsilon: 0.001 },
63
+ camera: () => {
64
+ 'use gpu'
65
+ return RaymarchCamera({
66
+ position: camera.position,
67
+ viewProjectionMatrix: camera.viewProjectionMatrix,
68
+ })
69
+ },
70
+ surface: {
71
+ bounds: () => {
72
+ 'use gpu'
73
+ return AABB({ min: vec3f(-1.01), max: vec3f(1.01) })
74
+ },
75
+ sd: (point) => {
76
+ 'use gpu'
77
+ return sdSphere(point, 1)
78
+ },
79
+ },
80
+ appearance: createMatteAppearance({
81
+ color: () => {
82
+ 'use gpu'
83
+ return vec4f(0.15, 0.5, 0.85, 1)
84
+ },
85
+ lighting,
86
+ }),
87
+ })
88
+
89
+ // Use your application's existing TypeGPU root.
90
+ const render = createRaymarchRenderer({ root, program })
91
+ render({
92
+ colorAttachment: { view: canvasContext },
93
+ depthStencilAttachment: { view: depthTexture },
63
94
  })
64
95
  ```
65
96
 
66
- 2. And then create a renderer
97
+ The application owns render targets and scheduling. Call `render` from your render
98
+ loop when needed. A depth attachment is required, with format `depth24plus` by
99
+ default. Use `renderTarget` when creating the renderer to specify a different
100
+ color format, depth format, or sample count matching your attachments.
101
+
102
+ <details>
103
+ <summary>Standalone root and attachment setup</summary>
104
+
105
+ Run this before the quick start, with a sized `<canvas id="canvas">`:
67
106
 
68
107
  ```ts
69
- import { createRaymarchRenderer } from 'luxscura'
108
+ import { tgpu } from 'typegpu'
70
109
 
71
110
  const canvas = document.getElementById('canvas') as HTMLCanvasElement
72
111
  const root = await tgpu.init()
73
112
  const canvasContext = root.configureContext({ canvas })
74
-
75
- // A depth buffer is currently required
76
113
  const depthTexture = root
77
- .createTexture({
78
- size: [canvas.width, canvas.height],
79
- format: 'depth24plus',
80
- })
81
- .$usage('render')
114
+ .createTexture({
115
+ size: [canvas.width, canvas.height],
116
+ format: 'depth24plus',
117
+ })
118
+ .$usage('render')
119
+ ```
82
120
 
83
- // This function renders the scene
84
- const render = createRaymarchRenderer({ root, program })
121
+ When resizing, recreate the depth texture to match the canvas and update the
122
+ camera's aspect ratio. When disposing the application, stop its render loop and
123
+ release its resources (`root.destroy()` when the root is no longer needed).
124
+
125
+ </details>
126
+
127
+ ## Camera
128
+
129
+ Your application owns the camera, its controls, and its matrices. Luxscura only
130
+ needs a GPU callback returning its world-space `position` and
131
+ `viewProjectionMatrix`, using WebGPU's depth range of 0–1. Adapt your existing
132
+ perspective camera to those two fields. For a moving camera, the callback can read
133
+ a TypeGPU buffer that your application updates, without rebuilding the rendering
134
+ pipeline.
135
+
136
+ Here is one way to create a fixed perspective camera using `wgpu-matrix`:
137
+
138
+ ```ts
139
+ import { mat4x4f, vec3f } from 'typegpu/data'
140
+ import { mat4 } from 'wgpu-matrix'
85
141
 
86
- // Render loop
87
- function renderFrame() {
88
- render({
89
- colorAttachment: { view: canvasContext },
90
- depthStencilAttachment: { view: depthTexture },
91
- })
92
- requestAnimationFrame(renderFrame)
142
+ function createCamera({ width, height }: HTMLCanvasElement) {
143
+ const position = vec3f(0, -3, 0)
144
+ const projection = mat4.perspective(Math.PI / 4, width / height, 0.1, 10)
145
+ const view = mat4.lookAt(position, vec3f(0), vec3f(0, 0, 1))
146
+ const viewProjectionMatrix = mat4x4f()
147
+ mat4.multiply(projection, view, viewProjectionMatrix)
148
+ return { position, viewProjectionMatrix }
93
149
  }
94
- requestAnimationFrame(renderFrame)
95
150
  ```
96
151
 
97
- ## More documentation coming soon
152
+ ## Appearances
153
+
154
+ ### Custom appearances
155
+
156
+ Supply an appearance callback directly to control surface shading. It receives
157
+ a `RaymarchResult`, including world-space `position`, `normal`, `rayDirection`,
158
+ and `instanceIndex`, so appearance can vary across a surface:
159
+
160
+ ```ts
161
+ import type { RaymarchAppearance } from 'luxscura'
162
+
163
+ const appearance: RaymarchAppearance = (result) => {
164
+ 'use gpu'
165
+ return vec4f(result.normal * 0.5 + vec3f(0.5), 1)
166
+ }
167
+ ```
168
+
169
+ A custom appearance returns unpremultiplied linear RGB and opacity (alpha in
170
+ 0–1). Appearance and material/color sampling callbacks run on both hits and
171
+ misses; the renderer only uses color on hits. Keep derivative-dependent sampling
172
+ outside hit-dependent branches. Surface hits write depth by default, even with
173
+ zero opacity; use `depthWriteEnabled: false` when appropriate for your rendering
174
+ setup.
175
+
176
+ ### Built-in appearances
177
+
178
+ Built-in appearances provide color or material callbacks that receive the same
179
+ `RaymarchResult`. They share ambient and directional lighting. Light `direction`
180
+ is the world-space direction the light travels. All color and lighting values
181
+ use linear RGB.
182
+
183
+ | Appearance | Import | Inputs |
184
+ | --- | --- | --- |
185
+ | Matte — diffuse lighting | `luxscura/matte` | Color callback and lighting |
186
+ | Glossy — adjustable shiny highlights | `luxscura/glossy` | Material callback and lighting |
187
+ | PBR (experimental) — metallic/roughness shading and environment reflections | `luxscura/pbr` | Material callback, lighting, and environment callback |
188
+
189
+ The quick start uses matte. To use glossy, replace its `appearance` with:
190
+
191
+ ```ts
192
+ import { createGlossyAppearance, GlossyMaterial } from 'luxscura/glossy'
193
+
194
+ const appearance = createGlossyAppearance({
195
+ material: (result) => {
196
+ 'use gpu'
197
+ return GlossyMaterial({
198
+ baseColor: vec3f(0.15, 0.5, 0.85),
199
+ specular: vec3f(1),
200
+ shininess: 128,
201
+ emission: vec3f(0),
202
+ opacity: 1,
203
+ })
204
+ },
205
+ lighting,
206
+ })
207
+ ```
208
+
209
+ `specular` sets highlight color and strength; `shininess` sets highlight tightness
210
+ (at least 1). `emission` adds color independently of lighting.
211
+
212
+ ### PBR (experimental)
213
+
214
+ PBR's API and visual results are still being refined. Its `metallic` and
215
+ `roughness` values range from 0 to 1. The required `environment` callback supplies
216
+ incoming linear RGB for a reflection direction and roughness; it can return a
217
+ procedural color or sample an environment texture.
218
+ The caller should use `roughness` to blur the reflection, choosing a filtering
219
+ method appropriate for their environment.
220
+ This approach is still experimental and somewhat awkward; it is likely to
221
+ change in future releases.
222
+
223
+ ```ts
224
+ import { createPbrAppearance, PbrMaterial } from 'luxscura/pbr'
225
+
226
+ const appearance = createPbrAppearance({
227
+ material: (result) => {
228
+ 'use gpu'
229
+ return PbrMaterial({
230
+ baseColor: vec3f(0.85, 0.9, 1),
231
+ metallic: 0.5,
232
+ roughness: 0.2,
233
+ emission: vec3f(0),
234
+ opacity: 1,
235
+ })
236
+ },
237
+ lighting,
238
+ environment: (direction, roughness) => {
239
+ 'use gpu'
240
+ return vec3f(0.12, 0.35, 0.65)
241
+ },
242
+ })
243
+ ```
244
+
245
+ ## API
246
+
247
+ ### Program — `createRaymarchProgram`
248
+
249
+ A program describes what to render. `createRaymarchProgram` stores the GPU
250
+ callbacks and compilation options in a versioned `RaymarchProgramDefinition`;
251
+ it does not allocate GPU resources. Pass that definition to a renderer.
252
+
253
+ Using named GPU callbacks keeps the call small (their implementations follow the
254
+ same pattern as the quick start):
255
+
256
+ ```ts
257
+ const program = createRaymarchProgram({
258
+ camera: getCamera,
259
+ surface: {
260
+ bounds: getBounds,
261
+ sd: distanceToSurface,
262
+ },
263
+ appearance: shadeSurface,
264
+ options: {
265
+ epsilon: 0.001,
266
+ maxSteps: 150,
267
+ stepScale: 0.8,
268
+ },
269
+ })
270
+ ```
271
+
272
+ The property paths below follow the nesting in the call above. Callback types
273
+ include argument and return types; `v3f` and `v4f` are TypeGPU float vector types
274
+ from `typegpu/data`. Values in `options` are used when compiling the pipeline.
275
+
276
+ #### `camera`
277
+
278
+ | Type | Default |
279
+ | --- | --- |
280
+ | `() => RaymarchCamera` | Required |
281
+
282
+ Returns the current `RaymarchCamera`: world-space position and view-projection matrix.
283
+
284
+ #### `surface`
285
+
286
+ | Type | Default |
287
+ | --- | --- |
288
+ | `RaymarchSurface` | Required |
289
+
290
+ Supplies geometry, bounds, and optional per-fragment initialization and visibility.
291
+
292
+ #### `surface.bounds`
293
+
294
+ | Type | Default |
295
+ | --- | --- |
296
+ | `(instanceIndex: number) => AABB` | Required |
297
+
298
+ Returns the instance's world-space `AABB`, which determines the area rasterized and
299
+ normally limits marching.
300
+
301
+ #### `surface.sd`
302
+
303
+ | Type | Default |
304
+ | --- | --- |
305
+ | `(point: v3f, instanceIndex: number, bounds: AABB) => number` | Required |
306
+
307
+ Returns the signed distance to the surface from a world-space point. Also used to
308
+ estimate normals.
309
+
310
+ #### `surface.init`
311
+
312
+ | Type | Default |
313
+ | --- | --- |
314
+ | `(instanceIndex: number) => void` | None |
315
+
316
+ Initializes per-fragment state before visibility testing and marching.
317
+ If omitted, no initialization callback runs.
318
+
319
+ #### `surface.isRayVisible`
320
+
321
+ | Type | Default |
322
+ | --- | --- |
323
+ | `(point: v3f, instanceIndex: number, bounds: AABB) => boolean` | None |
324
+
325
+ Returns false to discard a fragment before marching. The point is on the rasterized
326
+ bounding box.
327
+ If omitted, all rasterized fragments are eligible for marching.
328
+
329
+ #### `appearance`
330
+
331
+ | Type | Default |
332
+ | --- | --- |
333
+ | `(result: RaymarchResult) => v4f` | Required |
334
+
335
+ Returns linear RGBA after marching. See [Appearances](#appearances).
336
+
337
+ #### `options`
338
+
339
+ | Type | Default |
340
+ | --- | --- |
341
+ | `RaymarchProgramOptions` | Required |
342
+
343
+ Controls marching and depth behavior.
344
+
345
+ #### `options.label`
346
+
347
+ | Type | Default |
348
+ | --- | --- |
349
+ | `string` | None |
350
+
351
+ Diagnostic metadata for your application; unused by the renderer.
352
+ If omitted, the program has no label.
353
+
354
+ #### `options.epsilon`
355
+
356
+ | Type | Default |
357
+ | --- | --- |
358
+ | `number` | Required |
359
+
360
+ Surface hit tolerance in world units. Smaller values give a tighter tolerance but may
361
+ require more steps.
362
+
363
+ #### `options.epsilonNormal`
364
+
365
+ | Type | Default |
366
+ | --- | --- |
367
+ | `number` | None |
368
+
369
+ Offset in world units used to sample the distance function for surface normals. Adjust
370
+ separately when normal detail needs a different scale.
371
+ If omitted, normal sampling uses `options.epsilon`.
372
+
373
+ #### `options.maxSteps`
374
+
375
+ | Type | Default |
376
+ | --- | --- |
377
+ | `number` | `100` |
378
+
379
+ Maximum marching iterations per ray. Marching stops without a hit if this limit is reached.
380
+
381
+ #### `options.stepScale`
382
+
383
+ | Type | Default |
384
+ | --- | --- |
385
+ | `number` | `1` |
386
+
387
+ Multiplies each distance-based step. A value between 0 and 1 may be necessary when
388
+ your distance function overestimates the distance to the surface, for example after
389
+ coordinate-space warping or when using an approximate SDF. Smaller steps help avoid
390
+ stepping past the surface, at the cost of more marching iterations.
391
+
392
+ #### `options.maxDistance`
393
+
394
+ | Type | Default |
395
+ | --- | --- |
396
+ | `number` | None |
397
+
398
+ If present, applies an additional maximum marching distance from the bounding-box
399
+ entry point, in world units. If absent, rays stop marching at the bounds. With
400
+ `options.marchBeyondBounds: true`, this value is required and replaces the bounds
401
+ exit limit.
402
+
403
+ #### `options.marchBeyondBounds`
404
+
405
+ | Type | Default |
406
+ | --- | --- |
407
+ | `boolean` | `false` |
408
+
409
+ Allows marching past the box exit; requires an explicit `options.maxDistance` when true.
410
+ This extends the march along existing rays; the bounding box still controls which pixels
411
+ are rasterized.
412
+
413
+ #### `options.depthWriteEnabled`
414
+
415
+ | Type | Default |
416
+ | --- | --- |
417
+ | `boolean` | `true` |
418
+
419
+ Writes surface hit depth, allowing other pipelines to share depth with raymarched
420
+ geometry. Disable when your rendering setup needs depth testing without writes.
421
+
422
+ #### `options.depthCompare`
423
+
424
+ | Type | Default |
425
+ | --- | --- |
426
+ | `GPUCompareFunction` | `'less'` |
427
+
428
+ WebGPU depth comparison used to test surface hits against the depth attachment. Match
429
+ your application's depth convention.
430
+
431
+ ### Renderer — `createRaymarchRenderer`
432
+
433
+ A renderer compiles a program into a GPU pipeline and returns a draw function.
434
+ Its configuration connects that program to your application's GPU resources.
435
+
436
+ Pass the `program` from above here. This example specifies the render target settings;
437
+ the preparation hooks simply return their inputs to show where they belong:
438
+
439
+ ```ts
440
+ const render = createRaymarchRenderer({
441
+ root,
442
+ program,
443
+ renderTarget: {
444
+ colorFormat: navigator.gpu.getPreferredCanvasFormat(),
445
+ depthStencilFormat: 'depth24plus',
446
+ sampleCount: 1,
447
+ },
448
+ prepare: (body) => body, // Optionally adapt the program callbacks.
449
+ preparePipeline: (pipeline) => pipeline, // Optionally attach performance monitoring.
450
+ })
451
+ ```
452
+
453
+ #### `root`
454
+
455
+ | Type | Default |
456
+ | --- | --- |
457
+ | `TgpuRoot` | Required |
458
+
459
+ Your application's TypeGPU root, used to create the pipeline.
460
+
461
+ #### `program`
462
+
463
+ | Type | Default |
464
+ | --- | --- |
465
+ | `Readonly<RaymarchProgramDefinition>` | Required |
466
+
467
+ The definition returned by `createRaymarchProgram`.
468
+
469
+ #### `renderTarget`
470
+
471
+ | Type | Default |
472
+ | --- | --- |
473
+ | `Readonly<RaymarchRenderTargetOptions>` | None |
474
+
475
+ Attachment formats and sample count used for pipeline compatibility. They must match the
476
+ attachments used when drawing.
477
+ If omitted, the pipeline uses the preferred canvas color format, `'depth24plus'`,
478
+ and one sample per pixel.
479
+
480
+ #### `renderTarget.colorFormat`
481
+
482
+ | Type | Default |
483
+ | --- | --- |
484
+ | `GPUTextureFormat` | `navigator.gpu.getPreferredCanvasFormat()` |
485
+
486
+ Color attachment format. Set explicitly for targets using another format.
487
+
488
+ #### `renderTarget.depthStencilFormat`
489
+
490
+ | Type | Default |
491
+ | --- | --- |
492
+ | ``Extract<GPUTextureFormat, `depth${string}`>`` | `'depth24plus'` |
493
+
494
+ Depth/stencil attachment format. A depth attachment is always required.
495
+
496
+ #### `renderTarget.sampleCount`
497
+
498
+ | Type | Default |
499
+ | --- | --- |
500
+ | `1 \| 4` | `1` |
501
+
502
+ Samples per pixel: `1` or `4`. Use `4` with matching multisampled attachments and a
503
+ color resolve target.
504
+
505
+ #### `prepare`
506
+
507
+ | Type | Default |
508
+ | --- | --- |
509
+ | `(program: RaymarchProgram) => RaymarchProgram` | None |
510
+
511
+ Transforms the camera, surface, or appearance callbacks before compilation. Return the
512
+ resulting `RaymarchProgram`, for example to adapt callbacks to application resources.
513
+
514
+ #### `preparePipeline`
515
+
516
+ | Type | Default |
517
+ | --- | --- |
518
+ | `(pipeline: TgpuRenderPipeline<{ color: Vec4f }>) => TgpuRenderPipeline<{ color: Vec4f }>` | None |
519
+
520
+ Transforms the created TypeGPU pipeline before it is captured for drawing. Return the
521
+ resulting pipeline. For example, attach GPU performance monitoring with
522
+ `pipeline.withPerformanceCallback(...)`. This requires enabling the `timestamp-query`
523
+ device feature; see TypeGPU's [Timing Your Pipelines guide](https://docs.swmansion.com/TypeGPU/advanced/timestamp-queries/).
524
+
525
+ `TgpuRenderPipeline` comes from `typegpu`; `Vec4f` is the vector schema type from
526
+ `typegpu/data`. Both preparation hooks run during initial creation and again when
527
+ a program version change triggers recompilation. They do not run on every frame.
528
+
529
+ ### Draw — `render(options)`
530
+
531
+ Call the returned function whenever your application needs to submit a raymarch
532
+ draw. It uses TypeGPU attachment descriptors, so you can configure clearing,
533
+ loading, and storage to fit the surrounding rendering passes.
534
+
535
+ Call `render` from above in your application's render loop, supplying attachments
536
+ that match its `renderTarget` configuration:
537
+
538
+ ```ts
539
+ render({
540
+ colorAttachment: { view: colorTexture }, // Uses the preferred canvas format.
541
+ depthStencilAttachment: { view: depthTexture }, // depth24plus.
542
+ instances: 5,
543
+ })
544
+ ```
545
+
546
+ #### `colorAttachment`
547
+
548
+ | Type | Default |
549
+ | --- | --- |
550
+ | `ColorAttachment` | Required |
551
+
552
+ TypeGPU color attachment descriptor: the target view and its load/store settings.
553
+
554
+ #### `depthStencilAttachment`
555
+
556
+ | Type | Default |
557
+ | --- | --- |
558
+ | `DepthStencilAttachment` | Required |
559
+
560
+ TypeGPU depth/stencil attachment descriptor: the depth view and its load/store settings.
561
+ Required because Luxscura is designed to work alongside other pipelines in a TypeGPU
562
+ application. Sharing a depth buffer lets raymarched surfaces and other rendered objects
563
+ occlude one another according to their depth.
564
+
565
+ #### `instances`
566
+
567
+ | Type | Default |
568
+ | --- | --- |
569
+ | `number` | `1` |
570
+
571
+ Number of bounds instances to draw. The instance index lets your callbacks look up
572
+ per-instance values in a GPU buffer to change how the program renders that instance.
573
+
574
+ ## Hot module reloading
575
+
576
+ If shader edits cause a full page reload or do not update the renderer, enable
577
+ Luxscura's hot reload support. With Vite, pass `import.meta.hot` as the second
578
+ argument to `createRaymarchProgram` and call `import.meta.hot?.accept()`:
579
+
580
+ ```ts
581
+ const program = createRaymarchProgram({
582
+ options: { epsilon: 0.001 },
583
+ camera,
584
+ surface,
585
+ appearance,
586
+ }, import.meta.hot)
587
+
588
+ import.meta.hot?.accept()
589
+ ```
590
+
591
+ On a module update, Luxscura preserves the program definition and increments its
592
+ version. The existing renderer rebuilds its pipeline on the next draw using the
593
+ updated callbacks and options.
98
594
 
99
- See [the examples on the website](https://alexjwayne.github.io/luxscura/).
595
+ Currently, only one HMR-backed program can be defined per module.
@@ -6,6 +6,7 @@ export type GlossyMaterialSampler = (result: RaymarchResult) => GlossyMaterial;
6
6
  /**
7
7
  * Creates a glossy appearance using diffuse lighting and Blinn-Phong highlights.
8
8
  * Ambient fill affects the diffuse color; emission is added independently.
9
+ * Material opacity applies to the entire shaded result during blending.
9
10
  *
10
11
  * Material sampling runs for hits and misses so derivatives remain available.
11
12
  * Lighting runs only for hits. Colors and lighting values are linear RGB.
@@ -1,2 +1,2 @@
1
1
  export { createGlossyAppearance, type GlossyMaterialSampler, } from './appearance';
2
- export { GlossyMaterial } from './material';
2
+ export { GlossyMaterial, mixGlossyMaterials } from './material';
@@ -1,6 +1,6 @@
1
1
  import { type Infer } from 'typegpu/data';
2
2
  /**
3
- * GPU material schema for an opaque surface with adjustable shiny highlights.
3
+ * GPU material schema for a surface with adjustable shiny highlights and opacity.
4
4
  * Colors are linear RGB. Callers must supply finite values within the documented
5
5
  * ranges; constructing this struct does not clamp or validate material values.
6
6
  */
@@ -16,5 +16,9 @@ export declare const GlossyMaterial: import("typegpu/data").WgslStruct<{
16
16
  shininess: import("typegpu/data").F32;
17
17
  /** Nonnegative outgoing linear RGB added independently of illumination; may exceed 1. */
18
18
  emission: import("typegpu/data").Vec3f;
19
+ /** Opacity of the entire shaded surface in [0, 1]: 0 is transparent, 1 is opaque. */
20
+ opacity: import("typegpu/data").F32;
19
21
  }>;
20
22
  export type GlossyMaterial = Infer<typeof GlossyMaterial>;
23
+ /** Linearly blends all material properties. Amount is 0 for a, 1 for b, and is not clamped. */
24
+ export declare function mixGlossyMaterials(a: GlossyMaterial, b: GlossyMaterial, amount: number): GlossyMaterial;