Wireframe cube
Twelve edges, twenty-four vertices, one draw call with line-list
topology: every pair of vertices becomes one line, and the vertex shader
does the rotation and a hand-written perspective divide. Lines further from
the camera are dimmer. There is no depth buffer and no culling; a wireframe
does not need either.
; Wireframe cube: a rotating cube drawn as 12 line segments with `line-list`; topology; brightness falls off with depth. Exercises a float32 vertex buffer; consumed as line pairs and a pngine-inputs uniform.
(shader-module :name shader :code """ struct Uniforms { time: f32, width: f32, height: f32, aspect: f32, } @group(0) @binding(0) var<uniform> u: Uniforms;
struct VertexOutput { @builtin(position) pos: vec4f, @location(0) depth: f32, }
fn rotateY(p: vec3f, angle: f32) -> vec3f { let c = cos(angle); let s = sin(angle); return vec3f(p.x * c + p.z * s, p.y, -p.x * s + p.z * c); }
fn rotateX(p: vec3f, angle: f32) -> vec3f { let c = cos(angle); let s = sin(angle); return vec3f(p.x, p.y * c - p.z * s, p.y * s + p.z * c); }
@vertex fn vs(@location(0) pos: vec3f) -> VertexOutput { let t = u.time;
// Scale and rotate var p = pos * 0.3; p = rotateY(p, t * 0.5); p = rotateX(p, t * 0.3);
// Simple perspective let z = p.z + 2.5; let projX = p.x / z / u.aspect; let projY = p.y / z;
var out: VertexOutput; out.pos = vec4f(projX, projY, 0.5, 1.0); out.depth = z; return out; }
@fragment fn fs(in: VertexOutput) -> @location(0) vec4f { // Depth-based brightness let brightness = 1.0 - (in.depth - 2.0) * 0.3; let color = vec3f(0.2, 0.8, 1.0) * brightness; return vec4f(color, 1.0); } """)
; Cube edges as line segments (12 edges × 2 verts = 24 vertices, stride 12).(data :name lineVertices :float32 [ -1 -1 -1 1 -1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 -1 1 -1 -1 -1 -1 1 -1 1 1 -1 1 1 -1 1 1 1 1 1 1 -1 1 1 -1 1 1 -1 1 -1 -1 -1 -1 -1 1 -1 1 -1 -1 1 1 -1 1 -1 1 1 1 1 -1 -1 1 -1 1 1])
(buffer :name vertexBuffer :usage [vertex] :data lineVertices)
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeUniforms (write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(render-pipeline :name pipeline :layout auto (vertex :module shader :entry vs (vertex-buffer :array-stride 12 (attribute :shader-location 0 :offset 0 :format float32x3))) (fragment :module shader :entry fs (target :format preferred-canvas-format)) (primitive :topology line-list))
(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.05 0.05 0.1 1] :load-op clear :store-op store) :pipeline pipeline :vertex-buffers [vertexBuffer] :bind-groups [uniformsBindGroup] (draw :vertex-count 24))
(frame :name main :perform [writeUniforms mainPass])examples/samples/05_wireframe_cube.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.
The edge list
Section titled “The edge list”(data :name lineVertices :float32 [ … ]) lists the cube’s twelve edges as
pairs of endpoints, x y z each, in the [-1, 1] cube: four edges of the
bottom face, four of the top, four verticals. (buffer :name vertexBuffer :usage [vertex] :data lineVertices)
uploads them at creation, :data giving the buffer both its contents and its
size, and the pipeline’s (vertex-buffer :array-stride 12 (attribute :shader-location 0 :offset 0 :format float32x3))
reads one vec3f per vertex.
Line-list topology
Section titled “Line-list topology”(primitive :topology line-list) is the one line that changes the
picture: with it, (draw :vertex-count 24) assembles vertices
(0,1) (2,3) … (22,23) into twelve independent lines instead of eight
triangles. WebGPU rasterizes lines one pixel wide; there is no line width
setting in the API, which is why the wireframe stays thin at any
resolution.
Rotation and projection by hand
Section titled “Rotation and projection by hand”vs scales the cube to 0.3, rotates it about y then x with the time
uniform, and projects with a manual perspective: z + 2.5 moves the cube in
front of the camera, x / z / aspect and y / z divide by depth. Writing
aspect into the divide is what keeps the cube square on the wide canvas
above. pos.z is written as a constant 0.5, which is fine because nothing
depth-tests. The pre-projection depth travels to fs as @location(0)
and drives brightness = 1 - (depth - 2) * 0.3, so the far edges fade.
The rest
Section titled “The rest”The uniform buffer refilled every frame from pngine-inputs (the built-in
time/width/height/aspect source), the bind group, the clear-and-draw
pass and the frame are the scaffold from
Gradient background.
In the specifications
Section titled “In the specifications”| What the sample uses | WebGPU | WGSL |
|---|---|---|
| Line-list topology | primitive state, "line-list", primitive assembly, line rasterization |
|
Vertex buffer with one float32x3 attribute |
GPUVertexBufferLayout, "float32x3", mappedAtCreation |
@location inputs |
| Drawing 24 vertices | draw(), setVertexBuffer() |
|
| Clip-space output and the perspective divide | clip space coordinates, coordinate systems | position (vertex output) |
| Depth-based shading | @location outputs, interpolation, sin / cos |
Related
Section titled “Related”- Simple lighting draws a solid cube with a real depth buffer; Particle fountain uses the other non-triangle topology,
point-list. - Upstream: the WebGPU Samples wireframe sample shows two heavier techniques (line expansion in the vertex shader, barycentric discard); this is the plain line-list approach.
- Forms:
(data …),(render-pipeline …)((primitive …)),(render-pass …).