|
| 1 | +--- |
| 2 | +title: "Basic Triangle: Vertex‑Only Draw" |
| 3 | +document_id: "basic-triangle-tutorial-2025-12-16" |
| 4 | +status: "draft" |
| 5 | +created: "2025-12-16T00:00:00Z" |
| 6 | +last_updated: "2025-12-16T00:00:00Z" |
| 7 | +version: "0.1.0" |
| 8 | +engine_workspace_version: "2023.1.30" |
| 9 | +wgpu_version: "26.0.1" |
| 10 | +shader_backend_default: "naga" |
| 11 | +winit_version: "0.29.10" |
| 12 | +repo_commit: "797047468a927f1e4ba111b43381a607ac53c0d1" |
| 13 | +owners: ["lambda-sh"] |
| 14 | +reviewers: ["engine", "rendering"] |
| 15 | +tags: ["tutorial", "graphics", "triangle", "rust", "wgpu"] |
| 16 | +--- |
| 17 | + |
| 18 | +## Overview <a name="overview"></a> |
| 19 | + |
| 20 | +This tutorial renders a single 2D triangle using a vertex shader that derives |
| 21 | +positions from `gl_VertexIndex`. The implementation uses no vertex buffers and |
| 22 | +demonstrates the minimal render pass, pipeline, and command sequence in |
| 23 | +`lambda-rs`. |
| 24 | + |
| 25 | +Reference implementation: `crates/lambda-rs/examples/triangle.rs`. |
| 26 | + |
| 27 | +## Table of Contents |
| 28 | + |
| 29 | +- [Overview](#overview) |
| 30 | +- [Goals](#goals) |
| 31 | +- [Prerequisites](#prerequisites) |
| 32 | +- [Requirements and Constraints](#requirements-and-constraints) |
| 33 | +- [Data Flow](#data-flow) |
| 34 | +- [Implementation Steps](#implementation-steps) |
| 35 | + - [Step 1 — Runtime and Component Skeleton](#step-1) |
| 36 | + - [Step 2 — Vertex and Fragment Shaders](#step-2) |
| 37 | + - [Step 3 — Compile Shaders with `ShaderBuilder`](#step-3) |
| 38 | + - [Step 4 — Build Render Pass and Pipeline](#step-4) |
| 39 | + - [Step 5 — Issue Render Commands](#step-5) |
| 40 | + - [Step 6 — Handle Window Resize](#step-6) |
| 41 | +- [Validation](#validation) |
| 42 | +- [Notes](#notes) |
| 43 | +- [Conclusion](#conclusion) |
| 44 | +- [Exercises](#exercises) |
| 45 | +- [Changelog](#changelog) |
| 46 | + |
| 47 | +## Goals <a name="goals"></a> |
| 48 | + |
| 49 | +- Render a triangle with a vertex shader driven by `gl_VertexIndex`. |
| 50 | +- Learn the minimal `RenderCommand` sequence for a draw. |
| 51 | +- Construct a `RenderPass` and `RenderPipeline` using builder APIs. |
| 52 | + |
| 53 | +## Prerequisites <a name="prerequisites"></a> |
| 54 | + |
| 55 | +- The workspace builds: `cargo build --workspace`. |
| 56 | +- The `lambda-rs` crate examples run: `cargo run -p lambda-rs --example minimal`. |
| 57 | + |
| 58 | +## Requirements and Constraints <a name="requirements-and-constraints"></a> |
| 59 | + |
| 60 | +- Rendering commands MUST be issued inside an active render pass |
| 61 | + (`RenderCommand::BeginRenderPass` ... `RenderCommand::EndRenderPass`). |
| 62 | +- The pipeline MUST be set before draw commands (`RenderCommand::SetPipeline`). |
| 63 | +- The shader interface MUST match the pipeline configuration (no vertex buffers |
| 64 | + are declared for this example). |
| 65 | +- Back-face culling MUST be disabled or the triangle winding MUST be adjusted. |
| 66 | + Rationale: the example’s vertex positions are defined in clockwise order. |
| 67 | + |
| 68 | +## Data Flow <a name="data-flow"></a> |
| 69 | + |
| 70 | +- CPU builds shaders and pipeline once in `on_attach`. |
| 71 | +- CPU emits render commands each frame in `on_render`. |
| 72 | +- The GPU generates vertex positions from `gl_VertexIndex` (no vertex buffers). |
| 73 | + |
| 74 | +ASCII diagram |
| 75 | + |
| 76 | +``` |
| 77 | +Component::on_attach |
| 78 | + ├─ ShaderBuilder → Shader modules |
| 79 | + ├─ RenderPassBuilder → RenderPass |
| 80 | + └─ RenderPipelineBuilder → RenderPipeline |
| 81 | +
|
| 82 | +Component::on_render (each frame) |
| 83 | + BeginRenderPass → SetPipeline → SetViewports/Scissors → Draw → EndRenderPass |
| 84 | +``` |
| 85 | + |
| 86 | +## Implementation Steps <a name="implementation-steps"></a> |
| 87 | + |
| 88 | +### Step 1 — Runtime and Component Skeleton <a name="step-1"></a> |
| 89 | + |
| 90 | +Create an `ApplicationRuntime` and register a `Component` that receives |
| 91 | +`on_attach`, `on_render`, and `on_event` callbacks. |
| 92 | + |
| 93 | +```rust |
| 94 | +fn main() { |
| 95 | + let runtime = ApplicationRuntimeBuilder::new("2D Triangle Demo") |
| 96 | + .with_window_configured_as(|window_builder| { |
| 97 | + return window_builder |
| 98 | + .with_dimensions(1200, 600) |
| 99 | + .with_name("2D Triangle Window"); |
| 100 | + }) |
| 101 | + .with_component(|runtime, demo: DemoComponent| { |
| 102 | + return (runtime, demo); |
| 103 | + }) |
| 104 | + .build(); |
| 105 | + |
| 106 | + start_runtime(runtime); |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +The runtime drives component lifecycle and calls `on_render` on each frame. |
| 111 | + |
| 112 | +### Step 2 — Vertex and Fragment Shaders <a name="step-2"></a> |
| 113 | + |
| 114 | +The vertex shader generates positions from `gl_VertexIndex` so the draw call |
| 115 | +only needs a vertex count of `3`. |
| 116 | + |
| 117 | +```glsl |
| 118 | +vec2 positions[3]; |
| 119 | +positions[0] = vec2(0.0, -0.5); |
| 120 | +positions[1] = vec2(-0.5, 0.5); |
| 121 | +positions[2] = vec2(0.5, 0.5); |
| 122 | +
|
| 123 | +gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); |
| 124 | +``` |
| 125 | + |
| 126 | +The fragment shader outputs a constant color. |
| 127 | + |
| 128 | +### Step 3 — Compile Shaders with `ShaderBuilder` <a name="step-3"></a> |
| 129 | + |
| 130 | +Load shader sources from `crates/lambda-rs/assets/shaders/` and compile them |
| 131 | +using `ShaderBuilder`. |
| 132 | + |
| 133 | +```rust |
| 134 | +let triangle_vertex = VirtualShader::Source { |
| 135 | + source: include_str!("../assets/shaders/triangle.vert").to_string(), |
| 136 | + kind: ShaderKind::Vertex, |
| 137 | + name: String::from("triangle"), |
| 138 | + entry_point: String::from("main"), |
| 139 | +}; |
| 140 | +``` |
| 141 | + |
| 142 | +The compiled `Shader` objects are stored in component state and passed to the |
| 143 | +pipeline builder during `on_attach`. |
| 144 | + |
| 145 | +### Step 4 — Build Render Pass and Pipeline <a name="step-4"></a> |
| 146 | + |
| 147 | +Construct a `RenderPass` targeting the surface format, then build a pipeline. |
| 148 | +Disable culling to ensure the triangle is visible regardless of winding. |
| 149 | + |
| 150 | +```rust |
| 151 | +let render_pass = render_pass::RenderPassBuilder::new().build( |
| 152 | + render_context.gpu(), |
| 153 | + render_context.surface_format(), |
| 154 | + render_context.depth_format(), |
| 155 | +); |
| 156 | + |
| 157 | +let pipeline = pipeline::RenderPipelineBuilder::new() |
| 158 | + .with_culling(pipeline::CullingMode::None) |
| 159 | + .build( |
| 160 | + render_context.gpu(), |
| 161 | + render_context.surface_format(), |
| 162 | + render_context.depth_format(), |
| 163 | + &render_pass, |
| 164 | + &self.vertex_shader, |
| 165 | + Some(&self.fragment_shader), |
| 166 | + ); |
| 167 | +``` |
| 168 | + |
| 169 | +Attach the created resources to the `RenderContext` and store their IDs. |
| 170 | + |
| 171 | +### Step 5 — Issue Render Commands <a name="step-5"></a> |
| 172 | + |
| 173 | +Emit a pass begin, bind the pipeline, set viewport/scissor, and issue a draw. |
| 174 | + |
| 175 | +```rust |
| 176 | +RenderCommand::Draw { |
| 177 | + vertices: 0..3, |
| 178 | + instances: 0..1, |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +This produces one triangle using three implicit vertices. |
| 183 | + |
| 184 | +### Step 6 — Handle Window Resize <a name="step-6"></a> |
| 185 | + |
| 186 | +Track `WindowEvent::Resize` and rebuild the `Viewport` each frame using the |
| 187 | +stored dimensions. |
| 188 | + |
| 189 | +The viewport and scissor MUST match the surface dimensions to avoid clipping or |
| 190 | +undefined behavior when the window resizes. |
| 191 | + |
| 192 | +## Validation <a name="validation"></a> |
| 193 | + |
| 194 | +- Build: `cargo build --workspace` |
| 195 | +- Run: `cargo run -p lambda-rs --example triangle` |
| 196 | +- Expected behavior: a window opens and shows a solid-color triangle. |
| 197 | + |
| 198 | +## Notes <a name="notes"></a> |
| 199 | + |
| 200 | +- Culling and winding |
| 201 | + - This tutorial disables culling via `.with_culling(CullingMode::None)`. |
| 202 | + - If culling is enabled, the vertex order in |
| 203 | + `crates/lambda-rs/assets/shaders/triangle.vert` SHOULD be updated to |
| 204 | + counter-clockwise winding for a default `front_face = CCW` pipeline. |
| 205 | +- Debugging |
| 206 | + - If the window is blank, verify that the pipeline is set inside the render |
| 207 | + pass and the draw uses `0..3` vertices. |
| 208 | + |
| 209 | +## Conclusion <a name="conclusion"></a> |
| 210 | + |
| 211 | +This tutorial demonstrates the minimal `lambda-rs` rendering path: compile |
| 212 | +shaders, build a render pass and pipeline, and issue a draw using |
| 213 | +`RenderCommand`s. |
| 214 | + |
| 215 | +## Exercises <a name="exercises"></a> |
| 216 | + |
| 217 | +- Exercise 1: Change the triangle color |
| 218 | + - Modify `crates/lambda-rs/assets/shaders/triangle.frag` to output a different |
| 219 | + constant color. |
| 220 | +- Exercise 2: Enable back-face culling |
| 221 | + - Set `.with_culling(CullingMode::Back)` and update the vertex order in |
| 222 | + `crates/lambda-rs/assets/shaders/triangle.vert` to counter-clockwise. |
| 223 | +- Exercise 3: Add a second triangle |
| 224 | + - Issue a second `Draw` and offset positions in the shader for one of the |
| 225 | + triangles. |
| 226 | +- Exercise 4: Introduce push constants |
| 227 | + - Add a push constant color and position and port the shader interface to |
| 228 | + match `crates/lambda-rs/examples/triangles.rs`. |
| 229 | +- Exercise 5: Replace `gl_VertexIndex` with a vertex buffer |
| 230 | + - Create a vertex buffer for positions and update the pipeline and shader |
| 231 | + inputs accordingly. |
| 232 | + |
| 233 | +## Changelog <a name="changelog"></a> |
| 234 | + |
| 235 | +- 0.1.0 (2025-12-16): Initial draft aligned with |
| 236 | + `crates/lambda-rs/examples/triangle.rs`. |
0 commit comments