Skip to main content

Hello Triangle

This tutorial demonstrates how to draw a triangle using luma.gl's cross-platform rendering APIs.

It is assumed you've set up your development environment as described in Setup.

We create a Model to render the triangle. This will be a recurring theme in all our tutorials. A Model can be thought of as gathering all the WebGL/WebGPU pieces necessary for a single draw call: render pipelines (shader programs), attribute buffers, uniforms, texture bindings etc.

The program uses a tiny vertex shader that relies on the built-in vertex_index to look up clip-space positions for the three vertices. A matching fragment shader fills the triangle with a solid color. Both WGSL and GLSL versions are provided so the example runs on WebGPU and WebGL without changes.

The complete source for this example is shown below. It creates a Model with both WGSL and GLSL shaders and renders it inside an AnimationLoopTemplate. The animation loop simply opens a render pass, draws the model and ends the pass each frame.

import {AnimationLoopTemplate, AnimationProps, makeAnimationLoop, Model} from '@luma.gl/engine';
import {webgl2Adapter} from '@luma.gl/webgl';
import {webgpuAdapter} from '@luma.gl/webgpu';

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);
}
`;

const VS_GLSL = /* glsl */ `
#version 300 es
const vec2 pos[3] = vec2[3](vec2(0.0f, 0.5f), vec2(-0.5f, -0.5f), vec2(0.5f, -0.5f));
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);
}
`;

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 onFinalize() {
this.model.destroy();
}

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

const animationLoop = makeAnimationLoop(AppAnimationLoopTemplate, {
adapters: [webgpuAdapter, webgl2Adapter]
});
animationLoop.start();

The vertex shader uses built-in indices, so no vertex buffers are needed for this minimal example. The render loop clears the canvas and draws the model every frame.

When the application runs you should see a red triangle rendered on a white background.