Skip to content

Latest commit

 

History

History
236 lines (183 loc) · 7.62 KB

File metadata and controls

236 lines (183 loc) · 7.62 KB
title Basic Triangle: Vertex‑Only Draw
document_id basic-triangle-tutorial-2025-12-16
status draft
created 2025-12-16T00:00:00Z
last_updated 2025-12-16T00:00:00Z
version 0.1.0
engine_workspace_version 2023.1.30
wgpu_version 26.0.1
shader_backend_default naga
winit_version 0.29.10
repo_commit 797047468a927f1e4ba111b43381a607ac53c0d1
owners
lambda-sh
reviewers
engine
rendering
tags
tutorial
graphics
triangle
rust
wgpu

Overview

This tutorial renders a single 2D triangle using a vertex shader that derives positions from gl_VertexIndex. The implementation uses no vertex buffers and demonstrates the minimal render pass, pipeline, and command sequence in lambda-rs.

Reference implementation: crates/lambda-rs/examples/triangle.rs.

Table of Contents

Goals

  • Render a triangle with a vertex shader driven by gl_VertexIndex.
  • Learn the minimal RenderCommand sequence for a draw.
  • Construct a RenderPass and RenderPipeline using builder APIs.

Prerequisites

  • The workspace builds: cargo build --workspace.
  • The lambda-rs crate examples run: cargo run -p lambda-rs --example minimal.

Requirements and Constraints

  • Rendering commands MUST be issued inside an active render pass (RenderCommand::BeginRenderPass ... RenderCommand::EndRenderPass).
  • The pipeline MUST be set before draw commands (RenderCommand::SetPipeline).
  • The shader interface MUST match the pipeline configuration (no vertex buffers are declared for this example).
  • Back-face culling MUST be disabled or the triangle winding MUST be adjusted. Rationale: the example’s vertex positions are defined in clockwise order.

Data Flow

  • CPU builds shaders and pipeline once in on_attach.
  • CPU emits render commands each frame in on_render.
  • The GPU generates vertex positions from gl_VertexIndex (no vertex buffers).

ASCII diagram

Component::on_attach
  ├─ ShaderBuilder → Shader modules
  ├─ RenderPassBuilder → RenderPass
  └─ RenderPipelineBuilder → RenderPipeline

Component::on_render (each frame)
  BeginRenderPass → SetPipeline → SetViewports/Scissors → Draw → EndRenderPass

Implementation Steps

Step 1 — Runtime and Component Skeleton

Create an ApplicationRuntime and register a Component that receives on_attach, on_render, and on_event callbacks.

fn main() {
  let runtime = ApplicationRuntimeBuilder::new("2D Triangle Demo")
    .with_window_configured_as(|window_builder| {
      return window_builder
        .with_dimensions(1200, 600)
        .with_name("2D Triangle Window");
    })
    .with_component(|runtime, demo: DemoComponent| {
      return (runtime, demo);
    })
    .build();

  start_runtime(runtime);
}

The runtime drives component lifecycle and calls on_render on each frame.

Step 2 — Vertex and Fragment Shaders

The vertex shader generates positions from gl_VertexIndex so the draw call only needs a vertex count of 3.

vec2 positions[3];
positions[0] = vec2(0.0, -0.5);
positions[1] = vec2(-0.5, 0.5);
positions[2] = vec2(0.5, 0.5);

gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);

The fragment shader outputs a constant color.

Step 3 — Compile Shaders with ShaderBuilder

Load shader sources from crates/lambda-rs/assets/shaders/ and compile them using ShaderBuilder.

let triangle_vertex = VirtualShader::Source {
  source: include_str!("../assets/shaders/triangle.vert").to_string(),
  kind: ShaderKind::Vertex,
  name: String::from("triangle"),
  entry_point: String::from("main"),
};

The compiled Shader objects are stored in component state and passed to the pipeline builder during on_attach.

Step 4 — Build Render Pass and Pipeline

Construct a RenderPass targeting the surface format, then build a pipeline. Disable culling to ensure the triangle is visible regardless of winding.

let render_pass = render_pass::RenderPassBuilder::new().build(
  render_context.gpu(),
  render_context.surface_format(),
  render_context.depth_format(),
);

let pipeline = pipeline::RenderPipelineBuilder::new()
  .with_culling(pipeline::CullingMode::None)
  .build(
    render_context.gpu(),
    render_context.surface_format(),
    render_context.depth_format(),
    &render_pass,
    &self.vertex_shader,
    Some(&self.fragment_shader),
  );

Attach the created resources to the RenderContext and store their IDs.

Step 5 — Issue Render Commands

Emit a pass begin, bind the pipeline, set viewport/scissor, and issue a draw.

RenderCommand::Draw {
  vertices: 0..3,
  instances: 0..1,
}

This produces one triangle using three implicit vertices.

Step 6 — Handle Window Resize

Track WindowEvent::Resize and rebuild the Viewport each frame using the stored dimensions.

The viewport and scissor MUST match the surface dimensions to avoid clipping or undefined behavior when the window resizes.

Validation

  • Build: cargo build --workspace
  • Run: cargo run -p lambda-rs --example triangle
  • Expected behavior: a window opens and shows a solid-color triangle.

Notes

  • Culling and winding
    • This tutorial disables culling via .with_culling(CullingMode::None).
    • If culling is enabled, the vertex order in crates/lambda-rs/assets/shaders/triangle.vert SHOULD be updated to counter-clockwise winding for a default front_face = CCW pipeline.
  • Debugging
    • If the window is blank, verify that the pipeline is set inside the render pass and the draw uses 0..3 vertices.

Conclusion

This tutorial demonstrates the minimal lambda-rs rendering path: compile shaders, build a render pass and pipeline, and issue a draw using RenderCommands.

Exercises

  • Exercise 1: Change the triangle color
    • Modify crates/lambda-rs/assets/shaders/triangle.frag to output a different constant color.
  • Exercise 2: Enable back-face culling
    • Set .with_culling(CullingMode::Back) and update the vertex order in crates/lambda-rs/assets/shaders/triangle.vert to counter-clockwise.
  • Exercise 3: Add a second triangle
    • Issue a second Draw and offset positions in the shader for one of the triangles.
  • Exercise 4: Introduce push constants
    • Add a push constant color and position and port the shader interface to match crates/lambda-rs/examples/triangles.rs.
  • Exercise 5: Replace gl_VertexIndex with a vertex buffer
    • Create a vertex buffer for positions and update the pipeline and shader inputs accordingly.

Changelog

  • 0.1.0 (2025-12-16): Initial draft aligned with crates/lambda-rs/examples/triangle.rs.