Skip to main content

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
0s

Follow the frame​

  1. luma.createDevice() selects a supported backend and creates the canvas context.
  2. The model selects backend-appropriate shaders. Its vertex shader uses the vertex index, so this triangle needs no vertex buffer.
  3. onRender begins a pass, draws the model, and ends the pass.
  4. AnimationLoop submits work after onRender returns.
  5. onFinalize destroys 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 app element, 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.