luxscura 0.1.0 → 0.3.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,684 @@
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'
141
+
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 }
149
+ }
150
+ ```
151
+
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` accepts either GPU
250
+ callbacks directly or a factory that creates them from typed dependencies. It
251
+ stores a factory and compilation options in a versioned `RaymarchProgramDefinition`;
252
+ the direct form is wrapped in a factory automatically. Defining a program does
253
+ not invoke the factory or allocate GPU resources. Pass the definition to a renderer.
254
+
255
+ Using named GPU callbacks keeps the call small (their implementations follow the
256
+ same pattern as the quick start):
257
+
258
+ ```ts
259
+ const program = createRaymarchProgram({
260
+ camera: getCamera,
261
+ surface: {
262
+ bounds: getBounds,
263
+ sd: distanceToSurface,
264
+ },
265
+ appearance: shadeSurface,
266
+ options: {
267
+ epsilon: 0.001,
268
+ maxSteps: 150,
269
+ stepScale: 0.8,
270
+ },
271
+ })
272
+ ```
85
273
 
86
- // Render loop
87
- function renderFrame() {
88
- render({
89
- colorAttachment: { view: canvasContext },
90
- depthStencilAttachment: { view: depthTexture },
91
- })
92
- requestAnimationFrame(renderFrame)
274
+ When callbacks need resources supplied later, pass compilation options as the
275
+ first argument and a factory as the second:
276
+
277
+ ```ts
278
+ import type { TgpuUniform } from 'typegpu'
279
+ import { f32, vec3f, vec4f } from 'typegpu/data'
280
+ import { length } from 'typegpu/std'
281
+ import { AABB, createRaymarchProgram, type RaymarchCamera } from 'luxscura'
282
+
283
+ type SphereContext = {
284
+ camera: () => RaymarchCamera
285
+ radius: TgpuUniform<typeof f32>
93
286
  }
94
- requestAnimationFrame(renderFrame)
287
+
288
+ const sphereProgram = createRaymarchProgram(
289
+ { epsilon: 0.001 },
290
+ ({ camera, radius }: SphereContext) => ({
291
+ camera,
292
+ surface: {
293
+ bounds: () => {
294
+ 'use gpu'
295
+ return AABB({ min: vec3f(-radius.$), max: vec3f(radius.$) })
296
+ },
297
+ sd: (point) => {
298
+ 'use gpu'
299
+ return length(point) - radius.$
300
+ },
301
+ },
302
+ appearance: () => {
303
+ 'use gpu'
304
+ return vec4f(1, 0.2, 0.1, 1)
305
+ },
306
+ }),
307
+ )
308
+ ```
309
+
310
+ Create the resources in application setup and supply them to the renderer:
311
+
312
+ ```ts
313
+ const radius = root.createUniform(f32, 0.5)
314
+ const renderSphere = createRaymarchRenderer({
315
+ root,
316
+ program: sphereProgram,
317
+ context: { camera: getCamera, radius },
318
+ })
319
+ ```
320
+
321
+ The program determines the required context type. Missing or incompatible
322
+ context is a type error. Direct programs and zero-argument factories do not
323
+ require a `context` argument.
324
+
325
+ The factory is a synchronous JavaScript setup function; the callbacks it returns
326
+ are GPU functions. Luxscura invokes it during renderer creation and again when
327
+ the program version changes, before running `prepare` and compiling the pipeline.
328
+ It does not run on ordinary draws. Updating `radius` with `radius.write(...)`
329
+ changes what the shaders read without rebuilding the program.
330
+
331
+ Context belongs to each renderer, so several renderers can share one program
332
+ with different resources. Luxscura retains and passes the supplied context; it
333
+ does not create or destroy its resources. Allocate caller-owned GPU resources
334
+ outside the factory so shader rebuilds can reuse them.
335
+
336
+ The property paths below follow the nesting in the call above. Callback types
337
+ include argument and return types; `v3f` and `v4f` are TypeGPU float vector types
338
+ from `typegpu/data`. Values in `options` are used when compiling the pipeline.
339
+
340
+ #### `camera`
341
+
342
+ | Type | Default |
343
+ | --- | --- |
344
+ | `() => RaymarchCamera` | Required |
345
+
346
+ Returns the current `RaymarchCamera`: world-space position and view-projection matrix.
347
+
348
+ #### `surface`
349
+
350
+ | Type | Default |
351
+ | --- | --- |
352
+ | `RaymarchSurface` | Required |
353
+
354
+ Supplies geometry, bounds, and optional per-fragment initialization and visibility.
355
+
356
+ #### `surface.bounds`
357
+
358
+ | Type | Default |
359
+ | --- | --- |
360
+ | `(instanceIndex: number) => AABB` | Required |
361
+
362
+ Returns the instance's world-space `AABB`, which determines the area rasterized and
363
+ normally limits marching.
364
+
365
+ #### `surface.sd`
366
+
367
+ | Type | Default |
368
+ | --- | --- |
369
+ | `(point: v3f, instanceIndex: number, bounds: AABB) => number` | Required |
370
+
371
+ Returns the signed distance to the surface from a world-space point. Also used to
372
+ estimate normals.
373
+
374
+ #### `surface.init`
375
+
376
+ | Type | Default |
377
+ | --- | --- |
378
+ | `(instanceIndex: number) => void` | None |
379
+
380
+ Initializes per-fragment state before visibility testing and marching.
381
+ If omitted, no initialization callback runs.
382
+
383
+ #### `surface.isRayVisible`
384
+
385
+ | Type | Default |
386
+ | --- | --- |
387
+ | `(point: v3f, instanceIndex: number, bounds: AABB) => boolean` | None |
388
+
389
+ Returns false to discard a fragment before marching. The point is on the rasterized
390
+ bounding box.
391
+ If omitted, all rasterized fragments are eligible for marching.
392
+
393
+ #### `appearance`
394
+
395
+ | Type | Default |
396
+ | --- | --- |
397
+ | `(result: RaymarchResult) => v4f` | Required |
398
+
399
+ Returns linear RGBA after marching. See [Appearances](#appearances).
400
+
401
+ #### `options`
402
+
403
+ | Type | Default |
404
+ | --- | --- |
405
+ | `RaymarchProgramOptions` | Required |
406
+
407
+ Controls marching and depth behavior.
408
+
409
+ #### `options.label`
410
+
411
+ | Type | Default |
412
+ | --- | --- |
413
+ | `string` | None |
414
+
415
+ Diagnostic metadata for your application; unused by the renderer.
416
+ If omitted, the program has no label.
417
+
418
+ #### `options.epsilon`
419
+
420
+ | Type | Default |
421
+ | --- | --- |
422
+ | `number` | Required |
423
+
424
+ Surface hit tolerance in world units. Smaller values give a tighter tolerance but may
425
+ require more steps.
426
+
427
+ #### `options.epsilonNormal`
428
+
429
+ | Type | Default |
430
+ | --- | --- |
431
+ | `number` | None |
432
+
433
+ Offset in world units used to sample the distance function for surface normals. Adjust
434
+ separately when normal detail needs a different scale.
435
+ If omitted, normal sampling uses `options.epsilon`.
436
+
437
+ #### `options.maxSteps`
438
+
439
+ | Type | Default |
440
+ | --- | --- |
441
+ | `number` | `100` |
442
+
443
+ Maximum marching iterations per ray. Marching stops without a hit if this limit is reached.
444
+
445
+ #### `options.stepScale`
446
+
447
+ | Type | Default |
448
+ | --- | --- |
449
+ | `number` | `1` |
450
+
451
+ Multiplies each distance-based step. A value between 0 and 1 may be necessary when
452
+ your distance function overestimates the distance to the surface, for example after
453
+ coordinate-space warping or when using an approximate SDF. Smaller steps help avoid
454
+ stepping past the surface, at the cost of more marching iterations.
455
+
456
+ #### `options.maxDistance`
457
+
458
+ | Type | Default |
459
+ | --- | --- |
460
+ | `number` | None |
461
+
462
+ If present, applies an additional maximum marching distance from the bounding-box
463
+ entry point, in world units. If absent, rays stop marching at the bounds. With
464
+ `options.marchBeyondBounds: true`, this value is required and replaces the bounds
465
+ exit limit.
466
+
467
+ #### `options.marchBeyondBounds`
468
+
469
+ | Type | Default |
470
+ | --- | --- |
471
+ | `boolean` | `false` |
472
+
473
+ Allows marching past the box exit; requires an explicit `options.maxDistance` when true.
474
+ This extends the march along existing rays; the bounding box still controls which pixels
475
+ are rasterized.
476
+
477
+ #### `options.depthWriteEnabled`
478
+
479
+ | Type | Default |
480
+ | --- | --- |
481
+ | `boolean` | `true` |
482
+
483
+ Writes surface hit depth, allowing other pipelines to share depth with raymarched
484
+ geometry. Disable when your rendering setup needs depth testing without writes.
485
+
486
+ #### `options.depthCompare`
487
+
488
+ | Type | Default |
489
+ | --- | --- |
490
+ | `GPUCompareFunction` | `'less'` |
491
+
492
+ WebGPU depth comparison used to test surface hits against the depth attachment. Match
493
+ your application's depth convention.
494
+
495
+ ### Renderer — `createRaymarchRenderer`
496
+
497
+ A renderer compiles a program into a GPU pipeline and returns a draw function.
498
+ Its configuration connects that program to your application's GPU resources.
499
+
500
+ Pass the `program` from above here. This example specifies the render target settings;
501
+ the preparation hooks simply return their inputs to show where they belong:
502
+
503
+ ```ts
504
+ const render = createRaymarchRenderer({
505
+ root,
506
+ program,
507
+ renderTarget: {
508
+ colorFormat: navigator.gpu.getPreferredCanvasFormat(),
509
+ depthStencilFormat: 'depth24plus',
510
+ sampleCount: 1,
511
+ },
512
+ prepare: (body) => body, // Optionally adapt the program callbacks.
513
+ preparePipeline: (pipeline) => pipeline, // Optionally attach performance monitoring.
514
+ })
515
+ ```
516
+
517
+ #### `root`
518
+
519
+ | Type | Default |
520
+ | --- | --- |
521
+ | `TgpuRoot` | Required |
522
+
523
+ Your application's TypeGPU root, used to create the pipeline.
524
+
525
+ #### `program`
526
+
527
+ | Type | Default |
528
+ | --- | --- |
529
+ | `Readonly<RaymarchProgramDefinition<TContext>>` | Required |
530
+
531
+ The definition returned by `createRaymarchProgram`.
532
+
533
+ #### `context`
534
+
535
+ | Type | Default |
536
+ | --- | --- |
537
+ | `TContext` (inferred from `program`) | Required unless the type accepts `undefined` |
538
+
539
+ Setup-time dependencies passed to the program factory. The same context is used
540
+ when this renderer rebuilds after a program update. Changing the context object
541
+ itself does not trigger recompilation; use buffers or uniforms for changing GPU
542
+ data. If an edit adds required dependencies, recreate the renderer with a context
543
+ that supplies them.
544
+
545
+ #### `renderTarget`
546
+
547
+ | Type | Default |
548
+ | --- | --- |
549
+ | `Readonly<RaymarchRenderTargetOptions>` | None |
550
+
551
+ Attachment formats and sample count used for pipeline compatibility. They must match the
552
+ attachments used when drawing.
553
+ If omitted, the pipeline uses the preferred canvas color format, `'depth24plus'`,
554
+ and one sample per pixel.
555
+
556
+ #### `renderTarget.colorFormat`
557
+
558
+ | Type | Default |
559
+ | --- | --- |
560
+ | `GPUTextureFormat` | `navigator.gpu.getPreferredCanvasFormat()` |
561
+
562
+ Color attachment format. Set explicitly for targets using another format.
563
+
564
+ #### `renderTarget.depthStencilFormat`
565
+
566
+ | Type | Default |
567
+ | --- | --- |
568
+ | ``Extract<GPUTextureFormat, `depth${string}`>`` | `'depth24plus'` |
569
+
570
+ Depth/stencil attachment format. A depth attachment is always required.
571
+
572
+ #### `renderTarget.sampleCount`
573
+
574
+ | Type | Default |
575
+ | --- | --- |
576
+ | `1 \| 4` | `1` |
577
+
578
+ Samples per pixel: `1` or `4`. Use `4` with matching multisampled attachments and a
579
+ color resolve target.
580
+
581
+ #### `prepare`
582
+
583
+ | Type | Default |
584
+ | --- | --- |
585
+ | `(program: RaymarchProgram) => RaymarchProgram` | None |
586
+
587
+ Transforms the camera, surface, or appearance callbacks after the factory runs
588
+ and before compilation. Return the resulting `RaymarchProgram`, for example to
589
+ decorate its appearance or adapt its geometry.
590
+
591
+ #### `preparePipeline`
592
+
593
+ | Type | Default |
594
+ | --- | --- |
595
+ | `(pipeline: TgpuRenderPipeline<{ color: Vec4f }>) => TgpuRenderPipeline<{ color: Vec4f }>` | None |
596
+
597
+ Transforms the created TypeGPU pipeline before it is captured for drawing. Return the
598
+ resulting pipeline. For example, attach GPU performance monitoring with
599
+ `pipeline.withPerformanceCallback(...)`. This requires enabling the `timestamp-query`
600
+ device feature; see TypeGPU's [Timing Your Pipelines guide](https://docs.swmansion.com/TypeGPU/advanced/timestamp-queries/).
601
+
602
+ `TgpuRenderPipeline` comes from `typegpu`; `Vec4f` is the vector schema type from
603
+ `typegpu/data`. Both preparation hooks run during initial creation and again when
604
+ a program version change triggers recompilation. They do not run on every frame.
605
+
606
+ ### Draw — `render(options)`
607
+
608
+ Call the returned function whenever your application needs to submit a raymarch
609
+ draw. It uses TypeGPU attachment descriptors, so you can configure clearing,
610
+ loading, and storage to fit the surrounding rendering passes.
611
+
612
+ Call `render` from above in your application's render loop, supplying attachments
613
+ that match its `renderTarget` configuration:
614
+
615
+ ```ts
616
+ render({
617
+ colorAttachment: { view: colorTexture }, // Uses the preferred canvas format.
618
+ depthStencilAttachment: { view: depthTexture }, // depth24plus.
619
+ instances: 5,
620
+ })
621
+ ```
622
+
623
+ #### `colorAttachment`
624
+
625
+ | Type | Default |
626
+ | --- | --- |
627
+ | `ColorAttachment` | Required |
628
+
629
+ TypeGPU color attachment descriptor: the target view and its load/store settings.
630
+
631
+ #### `depthStencilAttachment`
632
+
633
+ | Type | Default |
634
+ | --- | --- |
635
+ | `DepthStencilAttachment` | Required |
636
+
637
+ TypeGPU depth/stencil attachment descriptor: the depth view and its load/store settings.
638
+ Required because Luxscura is designed to work alongside other pipelines in a TypeGPU
639
+ application. Sharing a depth buffer lets raymarched surfaces and other rendered objects
640
+ occlude one another according to their depth.
641
+
642
+ #### `instances`
643
+
644
+ | Type | Default |
645
+ | --- | --- |
646
+ | `number` | `1` |
647
+
648
+ Number of bounds instances to draw. The instance index lets your callbacks look up
649
+ per-instance values in a GPU buffer to change how the program renders that instance.
650
+
651
+ ## Hot module reloading
652
+
653
+ If shader edits cause a full page reload or do not update the renderer, enable
654
+ Luxscura's hot reload support. With Vite, pass `import.meta.hot` as the final
655
+ argument to `createRaymarchProgram` and call `import.meta.hot?.accept()`. It is
656
+ the second argument for the direct form:
657
+
658
+ ```ts
659
+ const program = createRaymarchProgram({
660
+ options: { epsilon: 0.001 },
661
+ camera,
662
+ surface,
663
+ appearance,
664
+ }, import.meta.hot)
665
+
666
+ import.meta.hot?.accept()
667
+ ```
668
+
669
+ For the factory form, it is the third argument:
670
+
671
+ ```ts
672
+ const program = createRaymarchProgram(options, createProgram, import.meta.hot)
673
+
674
+ import.meta.hot?.accept()
95
675
  ```
96
676
 
97
- ## More documentation coming soon
677
+ On a module update, Luxscura preserves the program definition and increments its
678
+ version. Each existing renderer invokes the updated factory with its original
679
+ context and rebuilds its pipeline on its next draw, using the updated options.
680
+ Both preparation hooks run again; caller-owned buffers and uniforms remain in
681
+ place. Changing the required context shape may require recreating the renderer
682
+ or reloading the application.
98
683
 
99
- See [the examples on the website](https://alexjwayne.github.io/luxscura/).
684
+ Currently, only one HMR-backed program can be defined per module.