- wgpu: Graphics API.
- winit: Window creation and event loop.
- glam: Linear algebra (math).
- rayon: Data parallelism (multithreading).
- bytemuck: Casting raw bytes for buffer uploads.
- log / env_logger: Logging.
What you will learn:
- Rust Workspaces: Managing multiple crates in one project.
- WGPU Concepts: Adapters, Devices, Queues, Surfaces, and Swapchains.
- The Pipeline: Unlike OpenGL, you cannot change render state on the fly. You will learn to build Pipeline State Objects (PSOs).
-
Initialize Workspace: Create a root
Cargo.toml. Define the following members:voxel-engine(Binary, the entry point).voxel-render(Library, handles wgpu interaction).
-
The Render Context (
voxel-render): Create a structRenderState.- Initialize
winitwindow. - Request a
wgpu::Instanceandwgpu::Surface. - Request an
Adapter(physical GPU) andDevice/Queue(logical connection). - Configure the Swapchain (handling VSync and format).
- Initialize
-
The Event Loop (
voxel-engine):- Set up the
winitevent loop. - Create a clean separate of logic:
update()andrender(). - Goal: Get a window to open and clear to a specific color (e.g., Cornflower Blue).
- Set up the
What you will learn:
- Uniform Buffers: How to send global data (MVP matrices) to shaders.
- Bind Groups: The wgpu equivalent of "Texture Slots" and "UBO bindings," but strictly validated.
- Coordinate Systems: Moving from OpenGL's right-handed (Y-up) to wgpu’s coordinate system (Z is 0 to 1, unlike GL's -1 to 1).
-
Create Crate:
voxel-core:- Add
glamas a dependency. - Create a
Camerastruct with position, yaw, and pitch. - Implement a method to generate the View-Projection Matrix.
- Add
-
Shader Implementation:
- Write a basic WGSL shader (
shader.wgsl). wgpu uses WGSL, not GLSL. - Define a struct for the
CameraUniformin both Rust and WGSL. Important: Learn about#[repr(C)]and padding/alignment requirements in Rust.
- Write a basic WGSL shader (
-
The Binding Layout:
- In
voxel-render, create aBindGroupLayout(describes the inputs to shaders). - Create a
Bufferfor the camera uniform. - Create a
BindGroup(actual data linked to the layout).
- In
-
Integration:
- Update the camera position based on keyboard input in
voxel-engine. - Write the new matrix to the buffer every frame.
- Update the camera position based on keyboard input in
What you will learn:
- Memory Layout: How to store voxel data efficiently (flat arrays vs. vectors).
- Rust Traits: Defining common behavior for blocks.
- Coordinate Mapping: Converting (x, y, z) to array indices.
-
Create Crate:
voxel-world:- Define a
BlockID(u8 or u16). - Define a constant
CHUNK_SIZE(e.g., 32x32x32).
- Define a
-
The Chunk Struct:
- Implement
Chunk. Instead ofVec<Block>, useBox<[Block; CHUNK_SIZE^3]>. This keeps chunks on the heap but avoids vector resizing overhead. - Implement helper functions:
get_block(x,y,z)andset_block(x,y,z).
- Implement
-
World Management:
- Create a
Worldstruct containing aHashMap<(i32, i32, i32), Chunk>. - Implement simple noise generation (use the
noise-rscrate) to fill chunks with terrain data.
- Create a
What you will learn:
- Vertex Formats: Defining memory layouts manually (
wgpu::VertexBufferLayout). - Face Culling: The logic of only drawing sides of a block touching air.
- Index Buffers: Reducing vertex count by reusing vertices.
-
Create Crate:
voxel-mesh:- Define a
Vertexstruct:[position: vec3, tex_coords: vec2, normal: vec3]. - Implement
bytemuck::PodandZeroablefor the vertex to safely cast it to bytes for GPU upload.
- Define a
-
Naive Meshing:
- Iterate through every block in a
Chunk. - For every block, generate 6 faces (2 triangles each).
- Result: A very heavy mesh.
- Iterate through every block in a
-
Face Culling (Optimization 1):
- Update the loop: Check the neighbor block.
- If the neighbor is solid, do not generate geometry for that face.
- Note: You will need to handle chunk boundaries (checking neighbors in adjacent chunks).
-
Render the Mesh:
- Pass the generated
Vec<Vertex>andVec<u32>(indices) tovoxel-render. - Create buffers:
BufferUsage::VERTEXandBufferUsage::INDEX. - Issue the draw call:
render_pass.draw_indexed(...).
- Pass the generated
What you will learn:
- Texture Arrays: Storing all block textures in a single array texture to avoid switching bind groups.
- Depth Stencil State: Ensuring close objects cover far objects.
- Z-Fighting mitigation: Precision issues.
-
Texture Loading:
- Load an image using the
imagecrate. - Create a
wgpu::Texturewith multiple layers (Texture Array). layer 0 = dirt, layer 1 = grass, etc.
- Load an image using the
-
Sampler & Bind Group:
- Create a
Sampler(nearest neighbor for that retro voxel look). - Update the
BindGroupinvoxel-renderto include the texture view and sampler.
- Create a
-
Depth Buffer:
- Create a depth texture (Format:
Depth32Float). - Attach it to the
RenderPassin thedepth_stencil_attachmentfield. - Observation: Cubes now look like cubes, not weirdly overlapping shapes.
- Create a depth texture (Format:
What you will learn:
- Async/Await vs Thread Pools: When to use which.
- Arc & Mutex/RwLock: Shared memory safety.
- Channels: Communicating between threads without locking the render loop.
-
Architecture Change:
- Mesh generation is slow. If done in the main loop, the game freezes while loading chunks.
- We need a Task System.
-
The Thread Pool (
voxel-world):- Integrate
rayon. - Create a struct
ChunkTask. - Use
crossbeam_channelorflume.
- Integrate
-
The Flow:
- Main Thread: "I need chunk at (0,0,0)." -> Send request to channel.
- Worker Thread: Receives request -> Generates Noise -> Generates Mesh -> Sends
Meshdata back to Main Thread via result channel. - Main Thread (Update Loop): Check result channel. If mesh acts, upload to GPU.
-
Synchronization:
- Wrap the
Worlddata inArc<RwLock<World>>if multiple threads need to read block data to generate meshes (for neighbor checking).
- Wrap the
What you will learn:
- Algorithmic Optimization: Reducing VRAM usage and Vertex Shader load.
- Data Compression: merging adjacent faces.
- Refine
voxel-mesh:- Implement Greedy Meshing.
- Instead of 1 block = 1 face, merge adjacent matching blocks into one large rectangle.
- Result: Massive reduction in triangle count (often 80%+ reduction for flat terrain).
What you will learn:
- Frustum Culling: Don't process chunks behind the player.
- Dynamic Loading/Unloading: Managing memory.
-
Chunk Loading Radius:
- In
voxel-engine, track player position. - Calculate which chunk coordinates are within radius
$R$ . - Queue load requests for new chunks.
- Queue drop requests for far chunks (free memory and destroy GPU buffers).
- In
-
Frustum Culling:
- Extract the frustum planes from the Camera's View-Projection matrix.
- Check AABB (Axis Aligned Bounding Box) of chunks against planes.
- Filter out chunks from the
render()list if they aren't visible.
/voxel_workspace
├── Cargo.toml
├── /voxel-engine (bin)
│ └── Main loop, Input handling, Glue code
├── /voxel-core (lib)
│ └── Camera, Transform, Math utils
├── /voxel-world (lib)
│ └── Chunk storage, Generation logic, Multithreading logic
├── /voxel-mesh (lib)
│ └── Vertex definitions, Greedy meshing algorithms
└── /voxel-render (lib)
└── wgpu instance, pipelines, buffer management, texture loading
- No Global State: You must pass the
DeviceandQueueinto every function that needs to modify GPU memory. - Promises: In wgpu, when you ask to map a buffer to read it back, it is async.
- Validation: wgpu crashes with helpful messages if your shader bindings don't match your Rust struct layout exactly.