| title | document_id | status | created | last_updated | version | engine_workspace_version | wgpu_version | shader_backend_default | winit_version | repo_commit | owners | reviewers | tags | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Basic Triangle: Vertex‑Only Draw |
basic-triangle-tutorial-2025-12-16 |
draft |
2025-12-16T00:00:00Z |
2026-02-07T00:00:00Z |
0.2.4 |
2023.1.30 |
28.0.0 |
naga |
0.29.10 |
544444652b4dc3639f8b3e297e56c302183a7a0b |
|
|
|
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: demos/render/src/bin/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 minimal demo runs:
cargo run -p lambda-demos-minimal --bin 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 optional 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.
Implement resize handling using event_mask() and on_window_event.
use lambda::events::{EventMask, WindowEvent};
// Inside `impl Component<ComponentResult, String> for DemoComponent`.
fn event_mask(&self) -> EventMask {
return EventMask::WINDOW;
}
fn on_window_event(&mut self, event: &WindowEvent) -> Result<(), String> {
if let WindowEvent::Resize { width, height } = event {
self.width = *width;
self.height = *height;
}
return Ok(());
}This setup ensures the runtime only dispatches window events to the component, and the component keeps a current width/height for viewport creation.
- Build:
cargo build --workspace - Run:
cargo run -p lambda-demos-render --bin 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 immediates
- Add an immediate data block for color and position and port the shader interface to
match
demos/render/src/bin/triangles.rs.
- Add an immediate data block for 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.2.4 (2026-02-05): Update demo commands and reference paths for
demos/. - 0.2.3 (2026-01-16): Normalize event handler terminology.
- 0.2.2 (2026-01-16): Add
event_mask()andon_window_eventresize example. - 0.2.1 (2026-01-16): Replace deprecated
on_eventreferences with per-category handlers. - 0.2.0 (2026-01-05): Update for wgpu v28; rename push constants to immediates in exercises.
- 0.1.0 (2025-12-16): Initial draft aligned with
demos/render/src/bin/triangle.rs.