Skip to content

Vertex colors

Download PNG

The hello-triangle of vertex buffers: three vertices, each with a position and a colour, interleaved in one buffer that is filled at creation from data written in the document. The vertex shader rotates the positions with the time uniform, and the rasterizer interpolates the colours across the face.

examples/samples/04_vertex_colors.sjon
; Vertex colors: one triangle rotating about the origin, each vertex carrying
; its own colour, interpolated across the face. Exercises a `:data` fill
; vertex buffer with an interleaved pos2+color3 layout (array-stride 20) next
; to a pngine-inputs uniform. No `(primitive …)` form: the defaults
; (triangle-list, ccw, no culling) are what it needs.
(data :name vertexData :float32 [
0.0 0.5 1.0 0.0 0.0
-0.5 -0.5 0.0 1.0 0.0
0.5 -0.5 0.0 0.0 1.0
])
(buffer :name vertexBuffer :usage [vertex] :data vertexData)
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeUniforms
(write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(shader-module :name shader :code """
struct Uniforms {
time: f32,
width: f32,
height: f32,
aspect: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VertexInput {
@location(0) pos: vec2f,
@location(1) color: vec3f,
}
struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) color: vec3f,
}
@vertex
fn vs(in: VertexInput) -> VertexOutput {
// Rotate based on time
let angle = u.time * 0.5;
let c = cos(angle);
let s = sin(angle);
let rotated = vec2f(
in.pos.x * c - in.pos.y * s,
in.pos.x * s + in.pos.y * c
);
var out: VertexOutput;
out.pos = vec4f(rotated, 0.0, 1.0);
out.color = in.color;
return out;
}
@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
return vec4f(in.color, 1.0);
}
""")
(render-pipeline :name pipeline
:layout auto
(vertex :module shader :entry vs
(vertex-buffer :array-stride 20
(attribute :shader-location 0 :offset 0 :format float32x2)
(attribute :shader-location 1 :offset 8 :format float32x3)))
(fragment :module shader :entry fs
(target :format preferred-canvas-format)))
(bind-group :name uniformsBindGroup :layout pipeline :group 0
(entry :binding 0 :buffer uniforms))
(render-pass :name mainPass
(color-attachment :view context-current-texture :clear-value [0.1 0.1 0.1 1] :load-op clear :store-op store)
:pipeline pipeline
:vertex-buffers [vertexBuffer]
:bind-groups [uniformsBindGroup]
(draw :vertex-count 3))
(frame :name main :perform [writeUniforms mainPass])

The document is SJON, the S-expression format pngine compiles: each form is one WebGPU resource or operation, and the shader text inside (shader-module …) is plain WGSL, WebGPU’s shading language.

(data :name vertexData :float32 [ … ]) embeds fifteen floats in the compiled payload: three rows of x y r g b. Then (buffer :name vertexBuffer :usage [vertex] :data vertexData) creates a VERTEX buffer from it. :data both fills and sizes the buffer, so no :size is needed: 60 bytes, written through mappedAtCreation before the first frame. No queue write, no staging: the buffer is born full.

The pipeline’s (vertex … (vertex-buffer :array-stride 20 (attribute …) (attribute …))) tells WebGPU how to read it: 20 bytes per vertex, float32x2 at offset 0 into @location(0), float32x3 at offset 8 into @location(1). The shader declares the same two locations in its VertexInput struct. Nothing states the vertex count in the pipeline; (draw :vertex-count 3) in the pass does. The pass names the buffer with :vertex-buffers [vertexBuffer], slot 0.

No (primitive …) form appears, so the defaults apply: triangle-list, counter-clockwise front faces, no culling. The rotation is in the plane, so the winding never changes and culling would make no difference here anyway.

vs rotates pos by time * 0.5 with a 2D rotation matrix written out as four multiplies and passes the colour through. Between vs and fs the colour is an @location(0) output, so it is perspective-interpolated across the triangle: red at the top, green and blue at the base, every mix in between.

The uniform buffer, its (queue …) writer (pngine-inputs, the built-in time/width/height/aspect source), the bind group, the pass and the frame are the fullscreen scaffold from Gradient background.

What the sample uses WebGPU WGSL
A vertex buffer filled at creation mappedAtCreation, GPUBufferUsage.VERTEX, buffer creation
Vertex buffer layout and attributes vertex state, GPUVertexBufferLayout, GPUVertexAttribute, GPUVertexFormat @location inputs, structure types
Binding the buffer and drawing setVertexBuffer(), draw()
Primitive defaults primitive state, GPUPrimitiveTopology, GPUCullMode
Colour interpolation rasterization interpolation
Uniform time writeBuffer() uniform address space, sin / cos