| 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 |
|
|||||
| reviewers |
|
|||||
| tags |
|
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.
- Overview
- Goals
- Prerequisites
- Requirements and Constraints
- Data Flow
- Implementation Steps
- Validation
- Notes
- Conclusion
- Exercises
- Changelog
- Render a triangle with a vertex shader driven by
gl_VertexIndex. - Learn the minimal
RenderCommandsequence for a draw. - Construct a
RenderPassandRenderPipelineusing builder APIs.
- The workspace builds:
cargo build --workspace. - The
lambda-rscrate examples run:cargo run -p lambda-rs --example minimal.
- 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.
- 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
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.
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.
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.
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.
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.
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.
- Build:
cargo build --workspace - Run:
cargo run -p lambda-rs --example triangle - Expected behavior: a window opens and shows a solid-color triangle.
- 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.vertSHOULD be updated to counter-clockwise winding for a defaultfront_face = CCWpipeline.
- This tutorial disables culling via
- Debugging
- If the window is blank, verify that the pipeline is set inside the render
pass and the draw uses
0..3vertices.
- If the window is blank, verify that the pipeline is set inside the render
pass and the draw uses
This tutorial demonstrates the minimal lambda-rs rendering path: compile
shaders, build a render pass and pipeline, and issue a draw using
RenderCommands.
- Exercise 1: Change the triangle color
- Modify
crates/lambda-rs/assets/shaders/triangle.fragto output a different constant color.
- Modify
- Exercise 2: Enable back-face culling
- Set
.with_culling(CullingMode::Back)and update the vertex order incrates/lambda-rs/assets/shaders/triangle.vertto counter-clockwise.
- Set
- Exercise 3: Add a second triangle
- Issue a second
Drawand offset positions in the shader for one of the triangles.
- Issue a second
- 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.
- Add a push constant color and position and port the shader interface to
match
- Exercise 5: Replace
gl_VertexIndexwith a vertex buffer- Create a vertex buffer for positions and update the pipeline and shader inputs accordingly.
- 0.1.0 (2025-12-16): Initial draft aligned with
crates/lambda-rs/examples/triangle.rs.