Skip to main content

Hello Triangle

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

caution

Tutorials are maintained on a best-effort basis and may not be fully up to date (contributions welcome).

Hello Triangle

Have to start somewhere...

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(AnimationLoopTemplate, {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.