Vertex colors
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.
; 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])examples/samples/04_vertex_colors.sjon in the pngine repository.
How it works
Section titled “How it works”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 into a buffer
Section titled “Data into a buffer”(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.
Describing the layout
Section titled “Describing the layout”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.
Rotation and interpolation
Section titled “Rotation and interpolation”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 rest
Section titled “The rest”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.
In the specifications
Section titled “In the specifications”| 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 |
Related
Section titled “Related”- Wireframe cube is the same buffer idea with a different topology; Multiple triangles adds a second, per-instance buffer.
- Forms:
(data …),(buffer …),(render-pipeline …)(the(vertex-buffer …)child),(render-pass …).