npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

luxscura

v0.2.0

Published

A TypeGPU ray marching renderer

Readme

Luxscura

A TypeGPU ray marching renderer for custom WebGPU applications. Supply a camera, geometry as a signed-distance function, and an appearance; Luxscura handles ray marching, surface normals, depth, and drawing. Use it alongside your other rendering pipelines, with your application's TypeGPU root and render attachments.

See the website and interactive examples.

⚠️ Luxscura is in early development; its public API may change.

Setup

npm install luxscura typegpu
npm install --save-dev unplugin-typegpu

Configure TypeGPU's build plugin so GPU callbacks can compile from TypeScript: TypeGPU setup · Build plugin. Configuring tsover is highly recommended for operator overloading on vectors and matrices in GPU callbacks, as used in the examples below. Follow the TypeGPU setup guide for configuration.

Your application needs a browser with WebGPU support.

Quick start

A program combines a camera callback, a surface, and an appearance. The surface's sd callback returns the signed distance at a world-space point; its bounds callback provides an axis-aligned bounding box enclosing the surface. Marching stops at the box's exit by default. GPU callbacks use 'use gpu'.

This example uses @typegpu/sdf for a sphere. createCamera belongs to your application; see Camera for an example.

import { sdSphere } from '@typegpu/sdf'
import {
  AABB,
  createRaymarchConstantLighting,
  createRaymarchProgram,
  createRaymarchRenderer,
  RaymarchCamera,
} from 'luxscura'
import { createMatteAppearance } from 'luxscura/matte'
import { vec3f, vec4f } from 'typegpu/data'

// A canvas with its drawing-buffer width and height already set.
const camera = createCamera(canvas)
const lighting = createRaymarchConstantLighting({
  ambient: vec3f(0.2),
  directionalLights: [{
    direction: vec3f(-1, 1, -1),
    color: vec3f(1, 0.9, 0.75),
    intensity: 1,
  }],
})

const program = createRaymarchProgram({
  options: { epsilon: 0.001 },
  camera: () => {
    'use gpu'
    return RaymarchCamera({
      position: camera.position,
      viewProjectionMatrix: camera.viewProjectionMatrix,
    })
  },
  surface: {
    bounds: () => {
      'use gpu'
      return AABB({ min: vec3f(-1.01), max: vec3f(1.01) })
    },
    sd: (point) => {
      'use gpu'
      return sdSphere(point, 1)
    },
  },
  appearance: createMatteAppearance({
    color: () => {
      'use gpu'
      return vec4f(0.15, 0.5, 0.85, 1)
    },
    lighting,
  }),
})

// Use your application's existing TypeGPU root.
const render = createRaymarchRenderer({ root, program })
render({
  colorAttachment: { view: canvasContext },
  depthStencilAttachment: { view: depthTexture },
})

The application owns render targets and scheduling. Call render from your render loop when needed. A depth attachment is required, with format depth24plus by default. Use renderTarget when creating the renderer to specify a different color format, depth format, or sample count matching your attachments.

Run this before the quick start, with a sized <canvas id="canvas">:

import { tgpu } from 'typegpu'

const canvas = document.getElementById('canvas') as HTMLCanvasElement
const root = await tgpu.init()
const canvasContext = root.configureContext({ canvas })
const depthTexture = root
  .createTexture({
    size: [canvas.width, canvas.height],
    format: 'depth24plus',
  })
  .$usage('render')

When resizing, recreate the depth texture to match the canvas and update the camera's aspect ratio. When disposing the application, stop its render loop and release its resources (root.destroy() when the root is no longer needed).

Camera

Your application owns the camera, its controls, and its matrices. Luxscura only needs a GPU callback returning its world-space position and viewProjectionMatrix, using WebGPU's depth range of 0–1. Adapt your existing perspective camera to those two fields. For a moving camera, the callback can read a TypeGPU buffer that your application updates, without rebuilding the rendering pipeline.

Here is one way to create a fixed perspective camera using wgpu-matrix:

import { mat4x4f, vec3f } from 'typegpu/data'
import { mat4 } from 'wgpu-matrix'

function createCamera({ width, height }: HTMLCanvasElement) {
  const position = vec3f(0, -3, 0)
  const projection = mat4.perspective(Math.PI / 4, width / height, 0.1, 10)
  const view = mat4.lookAt(position, vec3f(0), vec3f(0, 0, 1))
  const viewProjectionMatrix = mat4x4f()
  mat4.multiply(projection, view, viewProjectionMatrix)
  return { position, viewProjectionMatrix }
}

Appearances

Custom appearances

Supply an appearance callback directly to control surface shading. It receives a RaymarchResult, including world-space position, normal, rayDirection, and instanceIndex, so appearance can vary across a surface:

import type { RaymarchAppearance } from 'luxscura'

const appearance: RaymarchAppearance = (result) => {
  'use gpu'
  return vec4f(result.normal * 0.5 + vec3f(0.5), 1)
}

A custom appearance returns unpremultiplied linear RGB and opacity (alpha in 0–1). Appearance and material/color sampling callbacks run on both hits and misses; the renderer only uses color on hits. Keep derivative-dependent sampling outside hit-dependent branches. Surface hits write depth by default, even with zero opacity; use depthWriteEnabled: false when appropriate for your rendering setup.

Built-in appearances

Built-in appearances provide color or material callbacks that receive the same RaymarchResult. They share ambient and directional lighting. Light direction is the world-space direction the light travels. All color and lighting values use linear RGB.

| Appearance | Import | Inputs | | --- | --- | --- | | Matte — diffuse lighting | luxscura/matte | Color callback and lighting | | Glossy — adjustable shiny highlights | luxscura/glossy | Material callback and lighting | | PBR (experimental) — metallic/roughness shading and environment reflections | luxscura/pbr | Material callback, lighting, and environment callback |

The quick start uses matte. To use glossy, replace its appearance with:

import { createGlossyAppearance, GlossyMaterial } from 'luxscura/glossy'

const appearance = createGlossyAppearance({
  material: (result) => {
    'use gpu'
    return GlossyMaterial({
      baseColor: vec3f(0.15, 0.5, 0.85),
      specular: vec3f(1),
      shininess: 128,
      emission: vec3f(0),
      opacity: 1,
    })
  },
  lighting,
})

specular sets highlight color and strength; shininess sets highlight tightness (at least 1). emission adds color independently of lighting.

PBR (experimental)

PBR's API and visual results are still being refined. Its metallic and roughness values range from 0 to 1. The required environment callback supplies incoming linear RGB for a reflection direction and roughness; it can return a procedural color or sample an environment texture. The caller should use roughness to blur the reflection, choosing a filtering method appropriate for their environment. This approach is still experimental and somewhat awkward; it is likely to change in future releases.

import { createPbrAppearance, PbrMaterial } from 'luxscura/pbr'

const appearance = createPbrAppearance({
  material: (result) => {
    'use gpu'
    return PbrMaterial({
      baseColor: vec3f(0.85, 0.9, 1),
      metallic: 0.5,
      roughness: 0.2,
      emission: vec3f(0),
      opacity: 1,
    })
  },
  lighting,
  environment: (direction, roughness) => {
    'use gpu'
    return vec3f(0.12, 0.35, 0.65)
  },
})

API

Program — createRaymarchProgram

A program describes what to render. createRaymarchProgram stores the GPU callbacks and compilation options in a versioned RaymarchProgramDefinition; it does not allocate GPU resources. Pass that definition to a renderer.

Using named GPU callbacks keeps the call small (their implementations follow the same pattern as the quick start):

const program = createRaymarchProgram({
  camera: getCamera,
  surface: {
    bounds: getBounds,
    sd: distanceToSurface,
  },
  appearance: shadeSurface,
  options: {
    epsilon: 0.001,
    maxSteps: 150,
    stepScale: 0.8,
  },
})

The property paths below follow the nesting in the call above. Callback types include argument and return types; v3f and v4f are TypeGPU float vector types from typegpu/data. Values in options are used when compiling the pipeline.

camera

| Type | Default | | --- | --- | | () => RaymarchCamera | Required |

Returns the current RaymarchCamera: world-space position and view-projection matrix.

surface

| Type | Default | | --- | --- | | RaymarchSurface | Required |

Supplies geometry, bounds, and optional per-fragment initialization and visibility.

surface.bounds

| Type | Default | | --- | --- | | (instanceIndex: number) => AABB | Required |

Returns the instance's world-space AABB, which determines the area rasterized and normally limits marching.

surface.sd

| Type | Default | | --- | --- | | (point: v3f, instanceIndex: number, bounds: AABB) => number | Required |

Returns the signed distance to the surface from a world-space point. Also used to estimate normals.

surface.init

| Type | Default | | --- | --- | | (instanceIndex: number) => void | None |

Initializes per-fragment state before visibility testing and marching. If omitted, no initialization callback runs.

surface.isRayVisible

| Type | Default | | --- | --- | | (point: v3f, instanceIndex: number, bounds: AABB) => boolean | None |

Returns false to discard a fragment before marching. The point is on the rasterized bounding box. If omitted, all rasterized fragments are eligible for marching.

appearance

| Type | Default | | --- | --- | | (result: RaymarchResult) => v4f | Required |

Returns linear RGBA after marching. See Appearances.

options

| Type | Default | | --- | --- | | RaymarchProgramOptions | Required |

Controls marching and depth behavior.

options.label

| Type | Default | | --- | --- | | string | None |

Diagnostic metadata for your application; unused by the renderer. If omitted, the program has no label.

options.epsilon

| Type | Default | | --- | --- | | number | Required |

Surface hit tolerance in world units. Smaller values give a tighter tolerance but may require more steps.

options.epsilonNormal

| Type | Default | | --- | --- | | number | None |

Offset in world units used to sample the distance function for surface normals. Adjust separately when normal detail needs a different scale. If omitted, normal sampling uses options.epsilon.

options.maxSteps

| Type | Default | | --- | --- | | number | 100 |

Maximum marching iterations per ray. Marching stops without a hit if this limit is reached.

options.stepScale

| Type | Default | | --- | --- | | number | 1 |

Multiplies each distance-based step. A value between 0 and 1 may be necessary when your distance function overestimates the distance to the surface, for example after coordinate-space warping or when using an approximate SDF. Smaller steps help avoid stepping past the surface, at the cost of more marching iterations.

options.maxDistance

| Type | Default | | --- | --- | | number | None |

If present, applies an additional maximum marching distance from the bounding-box entry point, in world units. If absent, rays stop marching at the bounds. With options.marchBeyondBounds: true, this value is required and replaces the bounds exit limit.

options.marchBeyondBounds

| Type | Default | | --- | --- | | boolean | false |

Allows marching past the box exit; requires an explicit options.maxDistance when true. This extends the march along existing rays; the bounding box still controls which pixels are rasterized.

options.depthWriteEnabled

| Type | Default | | --- | --- | | boolean | true |

Writes surface hit depth, allowing other pipelines to share depth with raymarched geometry. Disable when your rendering setup needs depth testing without writes.

options.depthCompare

| Type | Default | | --- | --- | | GPUCompareFunction | 'less' |

WebGPU depth comparison used to test surface hits against the depth attachment. Match your application's depth convention.

Renderer — createRaymarchRenderer

A renderer compiles a program into a GPU pipeline and returns a draw function. Its configuration connects that program to your application's GPU resources.

Pass the program from above here. This example specifies the render target settings; the preparation hooks simply return their inputs to show where they belong:

const render = createRaymarchRenderer({
  root,
  program,
  renderTarget: {
    colorFormat: navigator.gpu.getPreferredCanvasFormat(),
    depthStencilFormat: 'depth24plus',
    sampleCount: 1,
  },
  prepare: (body) => body, // Optionally adapt the program callbacks.
  preparePipeline: (pipeline) => pipeline, // Optionally attach performance monitoring.
})

root

| Type | Default | | --- | --- | | TgpuRoot | Required |

Your application's TypeGPU root, used to create the pipeline.

program

| Type | Default | | --- | --- | | Readonly<RaymarchProgramDefinition> | Required |

The definition returned by createRaymarchProgram.

renderTarget

| Type | Default | | --- | --- | | Readonly<RaymarchRenderTargetOptions> | None |

Attachment formats and sample count used for pipeline compatibility. They must match the attachments used when drawing. If omitted, the pipeline uses the preferred canvas color format, 'depth24plus', and one sample per pixel.

renderTarget.colorFormat

| Type | Default | | --- | --- | | GPUTextureFormat | navigator.gpu.getPreferredCanvasFormat() |

Color attachment format. Set explicitly for targets using another format.

renderTarget.depthStencilFormat

| Type | Default | | --- | --- | | Extract<GPUTextureFormat, `depth${string}`> | 'depth24plus' |

Depth/stencil attachment format. A depth attachment is always required.

renderTarget.sampleCount

| Type | Default | | --- | --- | | 1 \| 4 | 1 |

Samples per pixel: 1 or 4. Use 4 with matching multisampled attachments and a color resolve target.

prepare

| Type | Default | | --- | --- | | (program: RaymarchProgram) => RaymarchProgram | None |

Transforms the camera, surface, or appearance callbacks before compilation. Return the resulting RaymarchProgram, for example to adapt callbacks to application resources.

preparePipeline

| Type | Default | | --- | --- | | (pipeline: TgpuRenderPipeline<{ color: Vec4f }>) => TgpuRenderPipeline<{ color: Vec4f }> | None |

Transforms the created TypeGPU pipeline before it is captured for drawing. Return the resulting pipeline. For example, attach GPU performance monitoring with pipeline.withPerformanceCallback(...). This requires enabling the timestamp-query device feature; see TypeGPU's Timing Your Pipelines guide.

TgpuRenderPipeline comes from typegpu; Vec4f is the vector schema type from typegpu/data. Both preparation hooks run during initial creation and again when a program version change triggers recompilation. They do not run on every frame.

Draw — render(options)

Call the returned function whenever your application needs to submit a raymarch draw. It uses TypeGPU attachment descriptors, so you can configure clearing, loading, and storage to fit the surrounding rendering passes.

Call render from above in your application's render loop, supplying attachments that match its renderTarget configuration:

render({
  colorAttachment: { view: colorTexture }, // Uses the preferred canvas format.
  depthStencilAttachment: { view: depthTexture }, // depth24plus.
  instances: 5,
})

colorAttachment

| Type | Default | | --- | --- | | ColorAttachment | Required |

TypeGPU color attachment descriptor: the target view and its load/store settings.

depthStencilAttachment

| Type | Default | | --- | --- | | DepthStencilAttachment | Required |

TypeGPU depth/stencil attachment descriptor: the depth view and its load/store settings. Required because Luxscura is designed to work alongside other pipelines in a TypeGPU application. Sharing a depth buffer lets raymarched surfaces and other rendered objects occlude one another according to their depth.

instances

| Type | Default | | --- | --- | | number | 1 |

Number of bounds instances to draw. The instance index lets your callbacks look up per-instance values in a GPU buffer to change how the program renders that instance.

Hot module reloading

If shader edits cause a full page reload or do not update the renderer, enable Luxscura's hot reload support. With Vite, pass import.meta.hot as the second argument to createRaymarchProgram and call import.meta.hot?.accept():

const program = createRaymarchProgram({
  options: { epsilon: 0.001 },
  camera,
  surface,
  appearance,
}, import.meta.hot)

import.meta.hot?.accept()

On a module update, Luxscura preserves the program definition and increments its version. The existing renderer rebuilds its pipeline on the next draw using the updated callbacks and options.

Currently, only one HMR-backed program can be defined per module.