Getting started
Draw a red triangle using one Model and a render pass. The example supplies WGSL for
WebGPU and GLSL ES 3.00 for WebGL 2, sharing the application lifecycle across both backends.
You need basic TypeScript knowledge and a browser supporting at least one backend.
Create a project
npm create vite@latest luma-demo -- --template vanilla-ts
cd luma-demo
npm install @luma.gl/core @luma.gl/engine
Use matching versions of the luma.gl packages. See Installing for other package managers and smaller installations.
Render the triangle
Replace src/main.ts with:
import {luma} from '@luma.gl/core';
import {AnimationLoopTemplate, type AnimationProps, makeAnimationLoop, Model} from '@luma.gl/engine';
import {webgpuAdapter} from '@luma.gl/core/webgpu';
import {webgl2Adapter} from '@luma.gl/core/webgl';
const VS_GLSL = /* glsl */ `#version 300 es
const vec2 pos[3] = vec2[3](vec2(0.0, 0.5), vec2(-0.5, -0.5), vec2(0.5, -0.5));
void main() {
gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0);
}
`;
const FS_GLSL = /* glsl */ `\
#version 300 es
precision highp float;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
const WGSL_SHADER = /* wgsl */ `\
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4<f32> {
var positions = array<vec2<f32>, 3>(vec2(0.0, 0.5), vec2(-0.5, -0.5), vec2(0.5, -0.5));
return vec4<f32>(positions[vertexIndex], 0.0, 1.0);
}
@fragment
fn fragmentMain() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
`;
class AppAnimationLoopTemplate extends AnimationLoopTemplate {
model: Model;
constructor({device}: AnimationProps) {
super();
this.model = new Model(device, {
source: WGSL_SHADER,
vs: VS_GLSL,
fs: FS_GLSL,
topology: 'triangle-list',
vertexCount: 3,
shaderLayout: {
attributes: [],
bindings: []
},
parameters: {
depthFormat: 'depth24plus'
}
});
}
override onRender({device}: AnimationProps): void {
const renderPass = device.beginRenderPass({
clearColor: [0.961, 0.961, 0.961, 1]
});
this.model.draw(renderPass);
renderPass.end();
}
override onFinalize(): void {
this.model.destroy();
}
}
const container = document.getElementById('app')!;
const device = await luma.createDevice({
adapters: [webgpuAdapter, webgl2Adapter],
createCanvasContext: {
container,
width: 500,
height: 500
}
});
makeAnimationLoop(AppAnimationLoopTemplate, {
device
}).start();
Keep Vite's index.html with its <div id="app"></div> and
<script type="module" src="/src/main.ts"></script>. Then start the server:
npm run dev
Follow the frame
luma.createDevice()selects a supported backend and creates the canvas context.- The model selects backend-appropriate shaders. Its vertex shader uses the vertex index, so this triangle needs no vertex buffer.
onRenderbegins a pass, draws the model, and ends the pass.AnimationLoopsubmits work afteronRenderreturns.onFinalizedestroys the model when the loop is destroyed.
type: 'best-available' is the default: supported WebGPU is preferred over WebGL 2.
Adapter array order does not set preference. If device creation fails after selection, handle
the error explicitly; supplying both adapters does not retry a failed WebGPU creation.
Troubleshoot
- Blank page: check the browser console, the
appelement, and the module script path. - Shader error: check the source for the selected backend; portability requires both shader languages.
- WebGPU unavailable: serve over HTTPS or localhost and check browser support. WebGL 2 can render this example when it is the selected backend.
Next steps
Browse the packages to choose additional tools and integrations.
- Hello Cube adds geometry, uniforms, and depth testing.
- Choosing an API layer explains Engine, Core, and Shaders.
- API reference provides exact class and resource contracts.
- Examples show larger rendering and compute workflows.