Skip to main content

Engine programming

Outcome

Engine packages the most common rendering lifecycle into reusable objects. The central idea is simple: a Model is a durable draw unit. It connects geometry, shader code, shader inputs, bindings, and pipeline state so the application can update values and draw without rebuilding the underlying Core objects each time.

Start with Geometry + Model. Add ShaderInputs, dynamic resources, picking, scenegraphs, animation, or compute helpers only when the application needs them. This keeps simple renderers simple while leaving a path to richer scenes.

Mental model

Engine sits above Core; it does not replace it:

Engine conceptWhat it decidesCore work it manages
Geometry or GPUGeometryWhich vertex/index data a model drawsBuffers and vertex layouts
ShaderInputsWhich module props and resources shaders consumeUniform buffers and bindings
ModelWhich geometry, shaders, parameters, and instance count form one draw unitShaders, render pipeline, binding state, and draw calls
AnimationLoopWhen application callbacks run and a frame is presentedDevice frame lifecycle and submission
needsRedraw()Whether changed state can affect the next imageAn application-readable invalidation signal

The application still owns the render pass, frame policy, and destruction of objects it creates. Engine reduces repeated setup; it does not make GPU lifetime or submission implicit.

Geometry ───────┐
ShaderInputs ───┼→ Model → draw into caller-owned RenderPass
Bindings ───────┘ ↑
updates mark redraw

Complete workflow

1. Create durable scene data

Use Geometry for CPU-provided vertex and index data, or GPUGeometry when buffers already live on the GPU. Preserve the source attribute semantics and provide shader-facing layouts at the model boundary.

2. Create one model per reusable draw unit

Give Model its geometry, portable WGSL/GLSL shaders where required, topology, parameters, and bindings. Construct the model once; pipeline and shader factories reuse compatible Core resources behind the scenes.

3. Update values instead of rebuilding

Use ShaderInputs for shader-module props, setBindings() for resource changes, and dynamic resources for frequently changing data. Recreate a model only when its structural contract changes.

4. Draw into an application-owned pass

Create a render pass through Core, call model.draw(renderPass), then end and submit the pass. One pass can contain many model draws. The application decides their order and presentation.

5. Render only when the image can change

Treat needsRedraw() as an invalidation signal. State changes set it; reading it clears it. The application or AnimationLoop decides whether to request and render a frame. See redraw detection.

6. Add higher-level systems selectively

  • Add interaction for orbit controls, GPU picking, and hover/selection feedback.
  • Add a scenegraph when parent/child transforms and traversal are genuinely useful.
  • Add animation for clips, tracks, blending, and morphs.
  • Add compute helpers for a bounded compute or transform workflow; use GPU scheduling for a multi-stage scheduled dataflow.

A minimal rendering shape

import {Geometry, Model} from '@luma.gl/engine';

const geometry = new Geometry({
topology: 'triangle-list',
attributes: {
POSITION: {size: 2, value: new Float32Array([-1, -1, 1, -1, 0, 1])}
}
});

const model = new Model(device, {
id: 'triangle',
geometry,
vs: vertexShader,
fs: fragmentShader
});

const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1]});
model.draw(renderPass);
renderPass.end();

model.destroy();

The exact shader source differs between WebGPU and WebGL 2. The lifecycle does not: create a durable model, update it, draw it into a pass, and destroy it when its owner is finished.

Choose the next page

If you need to…Continue with…Conclusion you should reach
Copy a complete small taskEngine cookbookBegin with the smallest complete lifecycle.
Pass module props to shadersShader inputsKeep typed module values separate from resource bindings.
Avoid idle renderingRedraw detectionInvalidation is a signal; frame scheduling remains yours.
Add camera or object interactionInteractivityRoute events first, then derive controls or picking.
Organize hierarchical transformsScenegraphsUse hierarchy for relationships, not merely as a container.
Play and blend authored motionAnimationAdvance animation state before deciding to redraw.
Run a bounded GPU transformCompute helpersSelect by backend and data shape.

Decisions and tradeoffs

  • One model or a scenegraph? Prefer a direct list of models until hierarchy, transform propagation, or traversal adds value.
  • Continuous loop or on-demand frames? Render continuously only for active animation or streaming. Otherwise schedule a frame when invalidated.
  • Engine helper or Core? Drop to Core when exact pass, resource, or synchronization control is the point of the work.
  • Engine helper or GPU scheduling? A helper suits one bounded operation. A graph suits several operations with shared resources, dependencies, indirect counts, or multi-frame budgets.

Common mistakes

  • Recreating Model for changes that belong in shader inputs, bindings, or dynamic resources.
  • Starting an unconditional animation loop for a mostly static view.
  • Reading needsRedraw() in several places even though reading clears the reason.
  • Introducing a scenegraph before the scene has meaningful hierarchy.
  • Treating a Material as a scenegraph node rather than reusable surface state.
  • Forgetting that Engine objects own Core resources and must be destroyed by their owner.

Next steps