From 470feea65fff2072934ea3b7677456c5aac077f8 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Wed, 24 Dec 2025 02:21:24 +0000 Subject: [PATCH 1/2] Sync with main (docs cleanup) --- .github/workflows/opencode.yml | 4 +- docs/AGENTS.md | 19 - docs/ROADMAP.md | 171 --------- docs/ROADMAPv2.md | 478 ------------------------ docs/ROADMAPv3.md | 324 ----------------- docs/SOLID-REFACTOR.md | 151 -------- docs/atmosphere-lighting.md | 352 ------------------ docs/audit-5.md | 356 ------------------ docs/biomes.md | 486 ------------------------- docs/cave-system.md | 292 --------------- docs/clouds.md | 343 ----------------- docs/coastlines.md | 391 -------------------- docs/decouple.md | 78 ---- docs/feedback.md | 483 ------------------------ docs/mesh.md | 327 ----------------- docs/render-stability-investigation.md | 116 ------ docs/shadows.md | 283 -------------- docs/worldgen-luanti-style.md | 278 -------------- docs/worldgen-revamp.md | 377 ------------------- docs/worldgen-spec2.md | 272 -------------- 20 files changed, 2 insertions(+), 5579 deletions(-) delete mode 100644 docs/AGENTS.md delete mode 100644 docs/ROADMAP.md delete mode 100644 docs/ROADMAPv2.md delete mode 100644 docs/ROADMAPv3.md delete mode 100644 docs/SOLID-REFACTOR.md delete mode 100644 docs/atmosphere-lighting.md delete mode 100644 docs/audit-5.md delete mode 100644 docs/biomes.md delete mode 100644 docs/cave-system.md delete mode 100644 docs/clouds.md delete mode 100644 docs/coastlines.md delete mode 100644 docs/decouple.md delete mode 100644 docs/feedback.md delete mode 100644 docs/mesh.md delete mode 100644 docs/render-stability-investigation.md delete mode 100644 docs/shadows.md delete mode 100644 docs/worldgen-luanti-style.md delete mode 100644 docs/worldgen-revamp.md delete mode 100644 docs/worldgen-spec2.md diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 316b49fd..6577696d 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -26,6 +26,6 @@ jobs: - name: Run opencode uses: sst/opencode/github@latest env: - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} with: - model: openrouter/minimax/minimax-m2.1 + model: zhipuai-coding-plan/glm-4.7 diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 9ff1a13e..00000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,19 +0,0 @@ -# AGENTS.md - Zig OpenGL Voxel Engine - -## Build Commands -```bash -nix develop # Enter dev shell (provides zig, SDL3, GLEW, OpenGL) -zig build # Build the project -zig build run # Build and run -nix build # Production build (outputs to ./result/bin/) -``` -No tests - this is a graphics/game project. Verify changes by running `zig build run`. - -## Code Style -- **Zig 0.14** (master/nightly), uses SDL3 + GLEW + OpenGL 3.3 -- **Imports**: `@import("std")` first, then local modules, then `c.zig` for C bindings -- **Naming**: `snake_case` for vars/functions, `PascalCase` for types/structs -- **Errors**: Return error unions (`!void`), propagate with `try`, use `defer` for cleanup -- **C Interop**: Access C via `c.` prefix (see `src/c.zig`), explicit C types (c.GLuint) -- **Constants**: Prefer `const`, use `\\` for multiline GLSL shader strings -- **Structure**: Engine code in `src/engine/`, world/voxel code in `src/world/` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index 49c475fb..00000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,171 +0,0 @@ -# OpenGL Engine Roadmap - -This roadmap is derived from The Cherno's OpenGL series and translated into **engine-level milestones**. -Use this as a checklist and progression guide while building your engine. - ---- - -## Phase 0 — Foundations -**Goal:** Window + context + sanity - -- [x] Window creation abstraction (GLFW / SDL) -- [x] OpenGL context creation (core profile) -- [x] Swap buffers -- [x] VSync enable / disable -- [x] OpenGL loader (GLAD / GLEW) -- [x] Runtime OpenGL version & capability checks - ---- - -## Phase 1 — Modern OpenGL Basics -**Goal:** Draw *something* correctly, the modern way - -- [x] Core-profile OpenGL only (no fixed pipeline) -- [x] Vertex Buffer (VBO) abstraction -- [ ] Index Buffer (EBO / IBO) abstraction -- [x] Vertex Array Object (VAO) abstraction -- [x] Vertex attribute specification -- [x] Interleaved vertex layouts -- [x] Static vs dynamic buffer usage - ---- - -## Phase 2 — Shaders -**Goal:** Full control of the GPU pipeline - -- [x] Shader compilation system -- [x] Shader linking & validation -- [x] Error reporting for shaders -- [x] Shader abstraction class -- [x] Uniform upload API -- [ ] Uniform location caching -- [ ] Shader source hot-reloading -- [ ] Central shader library / registry - ---- - -## Phase 3 — Error Handling & Debugging -**Goal:** Fail loudly, debug easily - -- [ ] OpenGL debug context -- [ ] KHR_debug callback -- [ ] GL call error macros -- [ ] Assertions around GPU calls -- [x] Engine-level logging system - ---- - -## Phase 4 — Renderer Architecture -**Goal:** Hide OpenGL behind a clean engine API - -- [x] Renderer API layer -- [ ] Render command abstraction -- [x] Draw call encapsulation -- [x] Renderer statistics (draw calls, vertices) -- [x] Render state isolation -- [x] Multiple object rendering - ---- - -## Phase 5 — Textures & Materials -**Goal:** Real assets, not hardcoded colors - -- [x] Texture loading system -- [x] Texture abstraction class -- [x] Texture parameter configuration -- [x] Texture unit / slot management -- [ ] Multi-texture rendering -- [x] Texture atlases -- [ ] Material system (shader + textures + params) - ---- - -## Phase 6 — Blending & Transparency -**Goal:** UI, sprites, and transparency - -- [x] Alpha blending -- [x] Blend mode abstraction -- [ ] Premultiplied alpha support -- [ ] Transparent object ordering (basic) - ---- - -## Phase 7 — Math & Transforms -**Goal:** Cameras, movement, real scenes - -- [x] Math library (vec2/3/4, mat4) -- [ ] Transform component -- [x] Projection matrices (ortho & perspective) -- [x] View matrices (camera) -- [x] Model matrices -- [x] MVP pipeline -- [x] Camera abstraction -- [x] Frustum culling - ---- - -## Phase 8 — Batch Rendering (Performance) -**Goal:** Reduce draw calls, scale scenes - -- [ ] Batch renderer architecture -- [ ] Batched colored geometry -- [ ] Batched textured geometry -- [x] Texture slot management -- [ ] Dynamic geometry batching -- [x] Draw-call minimisation strategy (frustum culling) - ---- - -## Phase 9 — Uniform Optimisation -**Goal:** Stop hammering the driver - -- [ ] Uniform Buffer Objects (UBOs) -- [ ] Frame-level uniform buffers -- [ ] Per-object vs per-frame separation -- [ ] Persistent mapped buffers (optional) - ---- - -## Phase 10 — Tooling & Engine UX -**Goal:** Developer-friendly engine - -- [ ] ImGui integration -- [ ] Debug panels -- [x] Renderer stats overlay -- [ ] Live shader reload toggle -- [x] Runtime render mode toggles (wireframe, etc.) - ---- - -## Phase 11 — Testing Framework -**Goal:** Don't break rendering accidentally - -- [ ] Render test framework -- [ ] Isolated render tests -- [ ] Texture rendering tests -- [ ] Regression test scenes - ---- - -## Engine v1 "Done" Definition -You can call this a **real engine** when you have: - -- [x] Clean renderer API -- [ ] Shader + material system -- [x] Texture & asset loading -- [x] Camera & transform system -- [ ] Batch renderer -- [ ] Debug UI -- [x] Measured performance metrics - ---- - -## Optional Future Directions -- Vulkan backend -- Deferred rendering -- ECS integration -- Scene graph -- Asset pipeline -- Editor tooling - ---- diff --git a/docs/ROADMAPv2.md b/docs/ROADMAPv2.md deleted file mode 100644 index 90784a78..00000000 --- a/docs/ROADMAPv2.md +++ /dev/null @@ -1,478 +0,0 @@ - -# Voxel Game Spec: Blocks + Worldgen + UI (Seeded) - -This document specs the **basic building blocks** of a voxel sandbox game and a **seeded procedural world generator** with biomes, mountains, cliffs, oceans, rivers, oases, etc., plus a **home screen UI** with seed input and reproducibility. - ---- - -## 1) Core Requirements - -### 1.1 Goals -- Deterministic world generation: **same seed => same world** (across machines). -- Infinite or very large worlds via **chunked generation**. -- Multiple biomes and large-scale features: - - oceans, beaches, rivers, lakes - - plains/forests/deserts/snow biomes - - mountain ranges, cliffs/plateaus - - caves and ore distribution - - oases in deserts (rare, seeded) - -### 1.2 Non-goals (for v1) -- Complex climate simulation -- Realistic erosion simulation -- Full story/progression systems -These can be added later. - ---- - -## 2) World Structure - -### 2.1 Coordinate System -- World coordinates: integer (x, y, z) -- y is vertical, y=0 is sea level reference (can be offset). -- Use 32-bit int for block coords; 64-bit for derived hashes. - -### 2.2 Chunking -- Chunk size: **16 x 16 x 256** (x,z,y) or configurable. -- Vertical sections recommended (e.g., 16x16x16 subchunks) for memory efficiency. -- Each chunk stores: - - block IDs - - lighting (optional v1) - - metadata (optional) -- Generation happens in phases (heightmap first, then features). - -### 2.3 World Layers -A clean approach is to treat generation as layered fields: -- **Continentalness** (land vs ocean) -- **Erosion/roughness** (cliffs vs smooth hills) -- **Temperature** -- **Humidity** -- **Height (base terrain)** -- **Local modifiers** (mountain mask, river carving, etc.) - ---- - -## 3) Seed System - -### 3.1 Seed Input -- Accept: - - string seed (e.g. `"my cool world"`) - - numeric seed (e.g. `123456789`) -- Convert string seed to 64-bit integer via stable hash (e.g., FNV-1a 64-bit). -- Use a stable PRNG for deterministic randomness (e.g., PCG32 / splitmix64). - -### 3.2 Deterministic Noise -Use deterministic noise functions where: -- Input: (x, z) or (x, y, z) in world coords -- Output: float in [-1, 1] or [0, 1] -- Ensure floating-point determinism by: - - using integer-based hashing noise where possible - - or keeping same implementation + precision everywhere - ---- - -## 4) Block System - -### 4.1 Block Data Model -Each block has: -- `id` (uint16 or uint32) -- `name` -- `is_solid` -- `is_transparent` -- `emits_light` (optional) -- `light_absorption` (optional) -- `texture_index` per face (or material key) -- `break_time` (optional) -- `drops` (optional) -- `tags` (e.g., `ground`, `stone`, `wood`, `leaf`, `fluid`) - -### 4.2 Basic Block Set (v1) -Minimum set for a complete world loop (terrain + resources + building): - -#### Air / Fluids -- Air -- Water (source) -- Water (flowing) (optional v1; can fake as same block with level metadata) -- Lava (optional v1) -- Ice (cold biomes) -- Snow layer (thin overlay) (optional v1) - -#### Terrain: Surface -- Grass -- Dirt -- Sand -- Red sand (optional) -- Gravel -- Clay (optional) - -#### Terrain: Subsurface / Rock -- Stone -- Cobblestone (player-made, optional) -- Deepslate / Basalt (optional depth variation) -- Bedrock (bottom boundary) - -#### Biome-specific -- Podzol / forest floor (optional) -- Mossy dirt / moss block (optional) -- Silt / mud (swamp-like, optional) -- Limestone / sandstone (optional for deserts) -- Snow block - -#### Plants / Natural Blocks -- Short grass (decor) -- Tall grass (optional) -- Flowers (2–4 variants, optional) -- Cactus -- Dead bush (optional) -- Sugar cane / reeds (water edges) -- Logs (wood trunk) -- Leaves -- Sapling (optional) - -#### Ores (basic progression) -- Coal ore -- Iron ore -- Copper ore (optional) -- Gold ore (optional) -- Diamond-like rare ore (optional) -- Redstone-like ore (optional) - -#### Utility / Crafting (optional v1) -- Planks -- Crafting table -- Furnace -- Torch (light emitting) - -### 4.3 Metadata (Optional v1) -If not doing full blockstate system, allow minimal metadata per block: -- water level (0..7) -- orientation (for logs) -- growth stage (for saplings, crops later) - ---- - -## 5) Biome System - -### 5.1 Biome Definition -A biome is defined by: -- `id`, `name` -- climate: temperature range, humidity range -- surface blocks: - - top block (e.g., grass/sand/snow) - - filler block (e.g., dirt/sand) - - stone type overrides (optional) -- vegetation rules (density + types) -- terrain modifiers: - - base height offset - - hilliness - - cliffiness bias -- water color / fog (optional) -- spawn rules (optional) - -### 5.2 Biomes (v1 list) -- Ocean -- Beach -- Plains -- Forest -- Taiga (conifer + colder forest) -- Desert -- Savanna (optional) -- Tundra / Snow -- Mountains (high elevation biome) -- Badlands / Mesa (optional) -- Swamp (optional) - -### 5.3 Climate Map -Compute 2D climate maps from noise: -- Temperature noise `T(x,z)` in [0..1] -- Humidity noise `H(x,z)` in [0..1] -Biome selection uses: -- altitude influence (high => colder) -- proximity to ocean influence (optional) - ---- - -## 6) Terrain Generation Pipeline (Deterministic) - -### 6.1 Overview -Worldgen runs in deterministic steps: - -1. **Global maps (2D)** - Compute base fields per column (x,z): - - continentalness C(x,z) - - erosion E(x,z) - - temperature T(x,z) - - humidity H(x,z) - - mountain mask M(x,z) - - river mask R(x,z) -2. **Base height** from continentalness + mountain mask -3. **Cliffs** from slope + erosion -4. **Carving** rivers/coasts -5. **3D density field** for caves (optional v1) -6. **Material assignment** (stone/dirt/sand/snow) -7. **Features** (trees, cacti, ores, structures, oases) - -### 6.2 Sea Level -- Define `SEA_LEVEL = 64` (config). -- Any column where surface height < SEA_LEVEL becomes ocean/lake fill. - -### 6.3 Continentalness: Land vs Ocean -Use low-frequency noise to shape continents: -- `C(x,z)` in [0..1] -- thresholds: - - `C < 0.35` => deep ocean - - `0.35..0.45` => shallow ocean / coasts - - `> 0.45` => land - -This makes big oceans/continents instead of noisy puddles. - -### 6.4 Base Height Function -Compute a base height: -- `base = SEA_LEVEL + landLift(C)` -- `landLift(C)`: - - deep ocean => negative - - coast => near SEA_LEVEL - - inland => positive - -Example conceptual mapping: -- `landLift = lerp(-40, +60, smoothstep(0.35, 0.75, C))` - -### 6.5 Mountains (Ranges) -Use a mountain mask `M(x,z)`: -- low-frequency ridge noise or combined FBM -- threshold to form ranges: - - `M > 0.6` => mountain region -Mountains add height: -- `mountAdd = pow(remap(M, 0.6..1.0), 2.0) * mountainAmplitude` -- amplitude: 60–140 blocks depending on desired scale - -### 6.6 Hills / Local Variation -Use mid-frequency noise `Hn(x,z)`: -- adds small-to-medium variation (5–25 blocks) - -### 6.7 Cliffs / Plateaus -Cliffs should appear where: -- slope is high OR erosion is low (meaning sharp terrain) -Compute slope using sampled heights: -- `slope = max(|h(x+1)-h(x)|, |h(z+1)-h(z)|)` -Cliffiness: -- `cliff = smoothstep(slopeLow, slopeHigh, slope) * (1 - E)` -Apply cliff shaping: -- Increase verticality by compressing heights into plateau steps or steep ramps. -Material rules: -- cliffs expose stone more (thin topsoil). - -### 6.8 Oceans, Beaches, Shores -- If surface height < SEA_LEVEL: - - fill with water up to SEA_LEVEL - - seabed is sand/gravel/clay mix -- Beaches: - - within N blocks of coastline AND height near SEA_LEVEL => sand - -### 6.9 Rivers (Carving) -Use a river mask `R(x,z)`: -- generate a low-frequency "flow field" + noise threshold lines OR use “distance to river spline” style. -Simpler deterministic method: -- `R = abs(noise_river(x,z))` -- if `R < riverWidthThreshold`, this column is in a river corridor. -Carve height towards a river bed level: -- `riverDepth = remap(R, 0..threshold)` (deeper at center) -- `h = min(h, SEA_LEVEL - 2 - riverDepth)` -Fill with water where below SEA_LEVEL. - -### 6.10 Lakes (Optional) -Lakes can be placed as rare features: -- pick candidate points by hashed grid -- if local basin exists, fill to lake level -Keep it deterministic by hashing region coords. - ---- - -## 7) Material Assignment (Surface + Subsurface) - -For each (x,z): -1. Determine final height `h`. -2. Determine biome based on (T, H, altitude, ocean distance). -3. Assign column materials: - - y == h => top block (grass/sand/snow) - - next `fillerDepth` (3–6) => filler (dirt/sand) - - below => stone -4. Add bedrock at bottom: - - y=0..4 => bedrock noise threshold - -Biome-specific rules: -- Desert: top sand, filler sand/sandstone -- Snow: top snow block or snow layer + dirt -- Mountains: top stone/snow depending on temp/altitude - ---- - -## 8) Caves & Ores (v1-friendly) - -### 8.1 Caves -Option A (simple): 3D noise threshold carving. -- `density = noise3d(x,y,z)` + vertical bias -- if density > threshold => carve to air -Add cave rarity by using lower frequency + threshold tuning. - -Option B (better later): worm/tunnel carving via random walk seeded per region. - -### 8.2 Ores -Run ore passes after stone placement: -- For each ore type: - - vertical range (minY..maxY) - - vein size - - vein count per chunk -- Deterministic placements using: - - per-chunk PRNG seeded by (worldSeed, chunkX, chunkZ, oreType) - ---- - -## 9) Features: Trees, Cacti, Vegetation, Oases - -### 9.1 Feature Placement Strategy -For each chunk: -- Seed PRNG with `(worldSeed, chunkX, chunkZ, featurePassId)`. -- Decide a number of attempts based on biome. -- For each attempt: - - pick (x,z) in chunk - - find surface y - - validate placement rules - - place blocks - -### 9.2 Trees -- Forest/taiga: more frequent -- Plains: rare lone trees -Tree shapes (v1): -- simple trunk height 4–7 -- leaf blob radius 2–3 -Use biome-specific block types (log/leaves). - -### 9.3 Cacti -- Desert only -- height 2–5 -- must be on sand - -### 9.4 Oases (Desert Feature) -Goal: rare pockets of water + palms/trees in deserts. -Deterministic placement: -- Divide world into large regions (e.g., 256x256 blocks) -- For each region: - - use hashed RNG to decide if an oasis exists (e.g., 5–10% chance) - - if yes, pick a center point in the region -Placement rules: -- biome at center must be desert -- must be inland enough (not right on coast) -Build steps: -- carve a shallow basin -- fill with water (small lake) -- place sand around edges -- add reeds + a few trees + grass patches nearby - ---- - -## 10) Home Screen UI Spec (Seed + World Creation) - -### 10.1 Home Screen Layout -Required elements: -- Title: game name -- Primary actions: - - `Singleplayer` (opens world create/load) - - `Settings` - - `Quit` -Optional: -- `Continue` (last played world) -- `Credits` - -### 10.2 Singleplayer Screen -Two sections: -- **World List** - - world name - - last played date - - seed (hidden behind “details”) - - buttons: Play / Delete / Rename (delete requires confirm) -- **Create World** - - World Name (text) - - Seed (text input) - - placeholder: “Leave blank for random” - - Random seed button (generates a seed string or number) - - World options (v1 minimal): - - World Size: Infinite (default) / Limited (optional) - - Starting biome bias: None (default) (optional) - - Create button - -### 10.3 Seed Behavior -- If seed input is empty: - - generate a random 64-bit seed and display it after creation -- If seed input is provided: - - store original string plus hashed numeric seed -Reproducibility: -- World folder stores: - - `seed_string` (optional) - - `seed_u64` - - `worldgen_version` - -### 10.4 Worldgen Versioning -Store a `worldgen_version` integer. -If you change generation later: -- new worlds get new version -- old worlds keep their version for deterministic chunk regen - ---- - -## 11) Data Storage Spec - -### 11.1 World Save Folder -Example structure: -- `worlds//` - - `world.json` (metadata) - - `region/` (chunk storage) - - `player/` (player state) - -### 11.2 `world.json` -Fields: -- `world_name` -- `seed_u64` -- `seed_string` (optional) -- `worldgen_version` -- `created_at` -- `last_played_at` -- `settings`: - - `sea_level` - - `chunk_size` - - `enabled_features` (optional) - ---- - -## 12) Implementation Roadmap (Suggested Order) - -1. Seed system + deterministic PRNG -2. Chunk system + storage + basic meshing -3. Heightmap terrain: continentalness -> land/ocean -4. Biome selection via temp/humidity -5. Surface materials (grass/sand/snow) -6. Mountains + cliffs -7. Rivers + beaches -8. Caves (optional) -9. Ores -10. Vegetation (trees/cacti/reeds) -11. Oases -12. Home screen + world create/load with seed -13. Worldgen versioning + save format stabilization - ---- - -## 13) Acceptance Criteria (v1) - -- Creating a world with a seed reproduces the same terrain layout. -- Oceans/continents are large-scale and readable. -- At least 5 biomes appear in a typical exploration. -- Mountains and cliffs visibly exist (not just bumpy hills). -- Rivers exist and flow through land into oceans (even if simplified). -- Desert oases exist rarely and are deterministic. -- Home screen allows: - - create world (name + seed) - - load existing world - - random seed generation - ---- diff --git a/docs/ROADMAPv3.md b/docs/ROADMAPv3.md deleted file mode 100644 index fab5d0ec..00000000 --- a/docs/ROADMAPv3.md +++ /dev/null @@ -1,324 +0,0 @@ -# Chunk Streaming Spec: Loading, Meshing, Rendering, and Unloading (Smooth View Distance) - -This document specifies a **chunk streaming system** for a voxel engine that: -- Loads chunks around the player smoothly based on **view distance** and **settings** -- Generates + meshes chunks asynchronously -- Prioritizes nearby chunks -- Unloads far chunks safely -- Avoids frame spikes via budgets and staged pipelines - ---- - -## 1) Goals - -- Smooth gameplay while moving: no long stalls. -- Deterministic chunk generation (seeded). -- Configurable: - - `viewDistanceChunks` (radius in chunks) - - `maxLoadedChunks` (memory cap) - - `meshDistanceChunks` (optional separate radius for rendering meshes) - - per-frame budgets (generation, meshing, uploads) -- Correctness: - - No rendering holes caused by missing neighbors (or handled gracefully). - - Unloading never races with jobs still using chunk data. - ---- - -## 2) Terminology & Definitions - -- **Chunk coords**: `(cx, cz)` in 2D, optional `(cy)` if vertical chunking. -- **Chunk size**: e.g. `16x16x256`. -- **World position to chunk**: - - `cx = floor(x / CHUNK_SIZE_X)` - - `cz = floor(z / CHUNK_SIZE_Z)` -- **Chunk radius**: - - view distance radius `R = viewDistanceChunks` - - region of interest = all chunks with `dx*dx + dz*dz <= R*R` (circle) OR square if simpler. -- **Load distance** vs **render distance**: - - `loadDistance` determines which chunks must exist in memory. - - `meshDistance` determines which chunks must have a mesh uploaded and rendered. - - Often: `meshDistance <= loadDistance` for perf. - ---- - -## 3) Chunk States and Lifecycle - -### 3.1 Chunk State Machine -A chunk should progress through explicit states: - -- `Missing` (not in memory) -- `QueuedForLoad` -- `LoadingFromDisk` -- `Generating` (procedural) -- `Generated` (blocks available) -- `QueuedForMesh` -- `Meshing` (CPU mesh build) -- `MeshReadyCPU` -- `UploadingGPU` -- `Renderable` (GPU buffers ready) -- `Unloading` (release resources) -- `Unloaded` (removed from map) - -### 3.2 Chunk Object Contents -Store: -- coords `(cx, cz[, cy])` -- block storage pointer / compressed array -- flags: - - `dirtyBlocks` (needs remesh) - - `needsNeighborRemesh` (when neighbors arrive) -- mesh handles: - - opaque mesh GPU buffers - - transparent mesh GPU buffers (optional) -- job handles / refcounts: - - `generationJobId` - - `meshJobId` -- last used timestamp (for LRU unloading) -- `pinCount` (prevent unloading while referenced) - ---- - -## 4) Settings - -### 4.1 User Settings -- `viewDistanceChunks` (int) - Example defaults: 8–12 -- `loadDistanceChunks` (int) - Usually `viewDistance + 2` (preload ring) -- `meshDistanceChunks` (int) - Usually equal to viewDistance; can be smaller. -- `maxLoadedChunks` (int) - Hard cap to avoid memory blowups, e.g. 2048 -- `maxMeshedChunks` (int) - Cap how many chunks may keep GPU meshes (optional) -- `chunkUploadBudgetPerFrame` (int) - e.g. 1–4 chunk meshes per frame -- `meshBuildBudgetPerFrameMs` (float) - e.g. 2–6 ms (or N tasks) -- `generationBudgetPerFrameMs` (float) -- `threads_generation` / `threads_meshing` - -### 4.2 Derived Distances -- `preloadRadius = loadDistanceChunks` -- `renderRadius = meshDistanceChunks` -- `keepAliveRadius = preloadRadius + 1` (optional ring to prevent thrash) - ---- - -## 5) Core Streaming Algorithm - -### 5.1 High-level Update Loop (per frame) -Inputs: -- player position -- camera view (optional frustum) -- settings - -Steps: -1. Determine `playerChunk = (pcx, pcz)`. -2. Build the **target set** of chunks to load (within `preloadRadius`). -3. Build the **target set** of chunks to mesh/render (within `renderRadius`). -4. Enqueue missing chunks for load/generation. -5. Prioritize and run jobs within budgets: - - disk load/generate tasks - - mesh build tasks - - GPU uploads -6. Unload chunks outside `keepAliveRadius` and/or past caps. - -### 5.2 Target Set Computation -Prefer circle (less total chunks than square for same radius): - -For `dx in [-R..R]`, `dz in [-R..R]`: -- if `dx*dx + dz*dz <= R*R`, include `(pcx+dx, pcz+dz)`. - -Optionally order by distance for priority queue. - -### 5.3 Prioritization -Use priority key: -1. smaller `dist2` first -2. within camera forward cone first (optional) -3. within frustum first (optional) - -This ensures nearby chunks appear first. - ---- - -## 6) Asynchronous Pipeline (Jobs) - -### 6.1 Worker Threads -Recommended separation: -- **Generation thread pool**: noise + block fill (CPU heavy) -- **Meshing thread pool**: greedy meshing/culled meshing (CPU heavy) -- **Main thread**: OpenGL calls only (upload buffers, create VAOs, etc.) - -### 6.2 Job Types -- `Job_LoadOrGenerateChunk(cx,cz)` - - if chunk exists on disk -> load - - else -> generate deterministically - - output: block data + metadata -- `Job_BuildChunkMesh(cx,cz)` - - needs chunk + neighbors (at least for face culling) - - output: CPU vertex/index buffers (opaque & transparent) -- `Job_UploadChunkMesh(cx,cz)` (main thread) - - create/update VBO/IBO/VAO - - swap mesh handles atomically - -### 6.3 Neighbor Dependency -Meshing typically needs neighbor blocks to cull faces at boundaries. -Options: - -**Option A (strict)**: only mesh when all 4 neighbors exist (N/E/S/W) (and vertical neighbors if applicable). -- Pros: no seams / no missing faces. -- Cons: slower visible appearance. - -**Option B (optimistic)**: mesh immediately with whatever neighbors exist; when a missing neighbor arrives, mark edges dirty and remesh. -- Pros: chunks appear quickly. -- Cons: extra remesh work. - -Recommended for smoothness: **Option B**. - -Implementation detail: -- Meshing treats missing neighbor as "air" for boundary culling. -- When neighbor loads, both chunks mark `dirtyBlocks=true` for boundary remesh. - ---- - -## 7) Smoothness Budgets (Avoid Frame Spikes) - -### 7.1 Budgets to Apply -Per frame, cap: -- number of generation completions applied -- number of mesh builds started / completed -- number of GPU uploads - -Suggested defaults: -- generate: up to 1–2 chunks/frame (or 2–4ms) -- mesh build: up to 1–2 chunks/frame (or 2–6ms) -- upload: up to 1 chunk/frame (more if small meshes) - -### 7.2 Work Queues -Maintain queues: -- `genQueue`: prioritized by dist2 -- `meshQueue`: prioritized by dist2 (and only if generated) -- `uploadQueue`: FIFO or prioritized by dist2 - -Each queue holds chunk coords + priority. Use a heap. - ---- - -## 8) Caching & Unloading - -### 8.1 Unload Rules -A chunk is a candidate for unloading if: -- outside `keepAliveRadius` -- not pinned (`pinCount==0`) -- no active jobs (or jobs can be canceled safely) -- not in a “grace period” (optional) - -### 8.2 LRU / Memory Cap -Maintain: -- `loadedChunksCount` -- if `loadedChunksCount > maxLoadedChunks`: - - unload farthest or least-recently-used chunks first (prefer farthest). - -### 8.3 Safe Unload with Jobs -You need job-safe ownership: -- chunks have a `generationVersion` or `jobToken`. -- when a job is queued, it captures the token. -- if the chunk is unloaded/recycled, token changes, job result is discarded. - -This prevents writing results into freed memory. - ---- - -## 9) Rendering Integration - -### 9.1 Render List -Each frame: -- build a list of chunks in `Renderable` state within `renderRadius`. -Optional: -- frustum cull chunk AABBs. -- sort by distance for transparency pass. - -### 9.2 Opaque vs Transparent Pass -Recommended: -- Render opaque chunk meshes front-to-back (better depth rejection). -- Render transparent chunk meshes back-to-front. - -### 9.3 Chunk Boundary Pop-in Mitigation -Techniques: -- Preload ring: `loadDistance = viewDistance + 2` -- Mesh ring: build mesh slightly beyond viewDistance (optional) -- Fade-in (advanced): per-chunk alpha ramp after upload (requires shader support) - ---- - -## 10) Disk IO (Optional v1, but recommended) - -### 10.1 Save Strategy -- Save modified chunks asynchronously. -- Use a region file system (like Minecraft) or per-chunk files: - - `chunks/cx_cz.bin` -- On load: - - schedule disk read; if missing -> generate. - -### 10.2 Throttling Disk -- Limit concurrent IO tasks. -- Avoid blocking the main thread. - ---- - -## 11) Debug/Developer Tools - -- [ ] Show current `(cx,cz)` in HUD -- [ ] Show loaded chunk count -- [ ] Show queued gen/mesh/upload counts -- [ ] Render chunk borders (wireframe) -- [ ] Toggle viewDistance live (rebuild target set) -- [ ] Visualize “priority rings” (optional) - ---- - -## 12) Suggested Data Structures - -### 12.1 Chunk Map -- `unordered_map loadedChunks` -- `ChunkKey` packs `(cx,cz[,cy])` into 64-bit key. - -### 12.2 Priority Queues -- `genQueue: min-heap by dist2` -- `meshQueue: min-heap by dist2` -- `uploadQueue: queue/heap` - -### 12.3 State Tracking -- Bitsets or flags for: - - inTargetLoadSet - - inTargetMeshSet - - queuedForGen - - queuedForMesh - ---- - -## 13) Acceptance Criteria (v1) - -- Moving quickly across terrain does not freeze the game. -- Chunks load nearest-first, then outward. -- View distance is respected: - - beyond `viewDistanceChunks`, chunks do not render -- Changing view distance in settings smoothly updates loaded/meshed sets. -- Chunks outside keepAlive/unload radius are eventually unloaded. -- No crashes or corruption when unloading while jobs are running. - ---- - -## 14) Implementation Order (Recommended) - -1. Chunk coordinate conversion + target set -2. Chunk state machine + chunk map -3. Generation job queue + worker threads + apply results -4. Meshing job queue + apply CPU meshes -5. GPU upload queue + per-frame upload budget -6. Unloading + LRU + safe job token discard -7. Frustum culling + opaque/transparent passes -8. Debug overlay + live settings changes - ---- - diff --git a/docs/SOLID-REFACTOR.md b/docs/SOLID-REFACTOR.md deleted file mode 100644 index 35508d93..00000000 --- a/docs/SOLID-REFACTOR.md +++ /dev/null @@ -1,151 +0,0 @@ -# SOLID Refactoring Plan - -This document tracks the refactoring effort to improve SOLID compliance in the Zig Voxel Engine. - -## Summary - -| Principle | Before | Target | -|-----------|--------|--------| -| Single Responsibility | C+ | B+ | -| Open/Closed | B+ | A- | -| Liskov Substitution | B+ | A | -| Interface Segregation | B | B+ | -| Dependency Inversion | C | B+ | - ---- - -## High Priority - -### 1. Extract UI from main.zig -**Problem**: `main.zig` contains ~300 lines of UI rendering code (fonts, buttons, text input) - -**Files to create**: -- [x] `src/engine/ui/font.zig` - Bitmap font rendering (drawText, drawGlyph, etc.) -- [x] `src/engine/ui/widgets.zig` - Button, text input helpers -- [ ] `src/game/menu.zig` - Menu screen logic - -**Status**: Partial (Font/Widgets extracted) - ---- - -### 2. Remove Renderer struct -**Problem**: `Renderer` duplicates RHI functionality with direct OpenGL calls - -**Changes**: -- [x] Migrate `Renderer.beginFrame()` callers to use RHI -- [x] Migrate `Renderer.setViewport()` to RHI (add method if needed) -- [x] Migrate `Renderer.setClearColor()` to RHI -- [x] Remove `src/engine/graphics/renderer.zig` (Struct removed, kept helpers) -- [x] Update `main.zig` to remove Renderer usage - -**Status**: Completed - ---- - -### 3. Fix World's dual shader dependency -**Problem**: `World.render()` accepts `?*const Shader` which couples it to OpenGL - -**Current signature**: -```zig -pub fn render(self: *World, shader: ?*const Shader, view_proj: Mat4, camera_pos: Vec3) void -``` - -**Target signature**: -```zig -pub fn render(self: *World, view_proj: Mat4, camera_pos: Vec3) void -``` - -**Changes**: -- [x] Remove `shader` parameter from `World.render()` -- [x] Remove `shader` parameter from `World.renderShadowPass()` -- [x] Update all call sites in `main.zig` -- [x] Ensure RHI handles all uniform updates (Implemented setModelMatrix/updateGlobalUniforms in rhi_opengl.zig) - -**Status**: Completed - ---- - -### 4. Move embedded shaders to files -**Problem**: ~230 lines of GLSL embedded in `main.zig` - -**Changes**: -- [ ] Verify `assets/shaders/terrain.vert` and `terrain.frag` exist and are up-to-date -- [ ] Remove embedded `vertex_shader_src` and `fragment_shader_src` from main.zig -- [ ] Ensure `Shader.initFromFile()` is used consistently - -**Status**: Not Started - ---- - -## Medium Priority - -### 5. Split World struct -**Problem**: World handles chunk storage, job dispatch, and rendering - -**Target structure**: -``` -World (facade) -├── ChunkManager - chunk loading/unloading/storage -├── ChunkJobDispatcher - async generation/meshing -└── (rendering stays in World for now, uses RHI) -``` - -**Status**: Not Started - ---- - -### 6. Implement or remove interfaces.zig -**Problem**: `IUpdatable`, `IRenderable`, `IChunkProvider`, `IMeshBuilder` are defined but never used - -**Decision**: Remove unused interfaces, keep as documentation for future extension - -**Status**: Not Started - ---- - -### 7. Separate Atmosphere concerns -**Problem**: `Atmosphere` handles time simulation AND sky rendering - -**Target**: -- `DayNightCycle` - time of day, sun/moon positions, light intensities -- `SkyRenderer` - sky mesh, shaders, rendering (or use RHI.drawSky) - -**Status**: Not Started - ---- - -## Low Priority - -### 8. Consider splitting RHI.VTable -**Problem**: 27 methods in single interface - -**Potential split**: -- `IRHICore` - lifecycle, buffers, textures, frame management -- `IRHIShadows` - shadow pass methods (optional capability) -- `IRHIUI` - UI quad rendering (optional capability) - -**Decision**: Defer - current design works, split only if backends diverge significantly - -**Status**: Deferred - ---- - -### 9. Unify Atmosphere rendering -**Problem**: OpenGL path uses direct GL calls, Vulkan uses RHI - -**Changes**: -- [ ] Remove `Atmosphere.renderSky()` OpenGL implementation -- [ ] Ensure all paths use `rhi.drawSky()` -- [ ] Remove sky VAO/VBO from Atmosphere - -**Status**: Not Started - ---- - -## Progress Log - -| Date | Change | Files Modified | -|------|--------|----------------| -| 2024-12-23 | Created refactoring plan | SOLID-REFACTOR.md | -| 2024-12-23 | Extract font rendering to font.zig | src/engine/ui/font.zig, main.zig | - diff --git a/docs/atmosphere-lighting.md b/docs/atmosphere-lighting.md deleted file mode 100644 index bc3885ef..00000000 --- a/docs/atmosphere-lighting.md +++ /dev/null @@ -1,352 +0,0 @@ -# atmosphere-lighting.md — Atmosphere, Sun/Moon, Day–Night Cycle, Dynamic Lighting (Voxel Engine) - -This spec defines a complete “v1 atmosphere” system: -- Day/night cycle with sun + moon -- Sky rendering (simple → scalable) -- Dynamic lighting system for voxels (sunlight + block lights) -- Time/seed determinism and save format -- Practical performance strategy for 16×256×16 chunks - ---- - -## 1) Goals - -- Visually readable day/night cycle: dawn → day → dusk → night. -- Sun + moon directions affect: - - sky color - - ambient intensity - - directional light (shadows optional later) -- World lighting: - - **Sunlight** propagates from sky downward and into caves - - **Block light** (torches, lava, etc.) propagates outward -- Lighting updates: - - incremental, chunk-local, smooth streaming - - no full-world recomputes -- Deterministic with seed + stored time. - -Non-goals (v1): -- Real volumetric clouds -- Cascaded shadow maps -- Full physically-based scattering -- Global illumination - ---- - -## 2) Time System - -### 2.1 World Time Model -Store time as a continuous value: -- `timeOfDay` in `[0, 1)` where: - - 0.00 = midnight - - 0.25 = sunrise - - 0.50 = noon - - 0.75 = sunset - -Or store `worldTicks`: -- `ticksPerDay = 24000` (Minecraft-like) (any consistent value ok) -- `timeOfDay = (worldTicks % ticksPerDay) / ticksPerDay` - -### 2.2 Save Fields -In `world.json`: -- `world_time_ticks` -- `time_scale` (optional; 1.0 default) - -### 2.3 Determinism -- World time is not derived from real clock; it advances by `deltaTime * timeScale`. -- When loaded, resume from saved ticks. - ---- - -## 3) Sun & Moon - -### 3.1 Directions -Compute a unit direction for the sun: -- `sunAngle = timeOfDay * 2π` -- Use a tilted orbit (more natural): - - tilt around world X axis (e.g. 15–25 degrees) -- `sunDir` points from world towards sun (directional light direction is `-sunDir`) - -Moon is opposite: -- `moonDir = -sunDir` - -### 3.2 Colors/Intensity Curves -Define curves (can be simple lerps): -- `sunIntensity(timeOfDay)`: - - 0 at night - - ramp up at dawn - - peak at noon - - ramp down at dusk -- `moonIntensity(timeOfDay)`: - - strongest at night - - 0 at day - -Recommended approach: -- Use smoothstep around sunrise/sunset: - - dawn window: `0.22..0.28` - - dusk window: `0.72..0.78` - -### 3.3 Sun/Moon Rendering -Options: -- Billboard quad in sky dome -- Analytical disc in fragment shader (cheap) -- Textured sprites (later) - -v1 requirement: -- Sun disc visible during day -- Moon disc visible at night - ---- - -## 4) Sky Rendering - -### 4.1 V1 Sky (Simple, Good Looking) -Use a fullscreen triangle/quad sky shader: -Inputs: -- camera direction -- `sunDir`, `moonDir` -- `sunIntensity`, `moonIntensity` -- color presets - -Compute: -- sky gradient (horizon → zenith) -- sun glow near sunDir -- dusk/dawn tint near horizon - -Stars: -- Render procedural starfield at night: - - hash(viewDir) based stars - - fade by `(1 - sunIntensity)` - -Clouds (optional v1): -- 2D scrolling noise layer projected onto sky. - -### 4.2 Fog (Strongly recommended) -Fog improves depth perception and hides chunk pop: -- Color matches sky/horizon -- Exponential fog: - - `fogFactor = 1 - exp(-distance * fogDensity)` -- Increase fog at night slightly. - ---- - -## 5) World Lighting Overview - -You need two independent voxel light channels: - -### 5.1 Light Types -- **Sunlight** (a.k.a. skylight) - - Range: 0..15 (u4) - - Source: sky exposure - - Directional-ish: strongest downward, but spreads into caves -- **Block light** - - Range: 0..15 (u4) - - Source: emissive blocks (torch=14, lava=15, etc.) - - Spreads in all directions - -Store them separately: -- `skyLight` and `blockLight` -Final light at a voxel: -- `L = max(skyLight, blockLight)` for brightness -- (optional later) use both for color grading - ---- - -## 6) Light Storage Layout - -### 6.1 Per Block -Minimum: -- 4 bits skylight -- 4 bits blocklight -Pack into one byte: -- `uint8 light = (sky << 4) | block` - -### 6.2 Per Subchunk -Because you already mesh in 16×16×16: -- store light arrays per subchunk too (cache-friendly) - ---- - -## 7) Skylight Computation - -### 7.1 Initial Skylight for a Column -For each (x,z) column: -- Start from `y=255` downwards -- Keep a “sunlight value” initially 15 -- For each y: - - if block is fully opaque: sunlight becomes 0 below - - else set `skyLight(x,y,z)=currentSun` - -This produces: -- outdoor light = 15 -- caves under overhangs become dark - -### 7.2 Skylight Flood Fill (Spread into caves) -After vertical pass, propagate skylight sideways/down into openings: -- BFS flood from all voxels with skylight > 0 -- Spread to neighbors with decay: - - `next = cur - 1` (or no decay for vertical-down in some engines, but decay is simpler) -- Only propagate through non-opaque blocks. - -Performance: -- Do this per chunk (and across chunk borders using neighbor queues) - -### 7.3 Incremental Updates -When blocks change: -- If removing an opaque block: - - skylight can increase below → “light add” BFS -- If placing an opaque block: - - skylight can decrease → “light remove” BFS + re-add from other sources - -(Use the standard “remove then add” algorithm used by voxel engines.) - ---- - -## 8) Block Light Propagation - -### 8.1 Emissive Blocks -Define emissive levels: -- Torch: 14 -- Lava: 15 -- Glowstone (if any): 15 -- Lantern: 13, etc. - -### 8.2 Flood Fill -For each source voxel with `blockLight = N`: -- BFS outward: - - neighbor gets `max(existing, N-1)` if transparent -- stops at 0 - -### 8.3 Incremental Updates -On block changes: -- If a light source removed: - - run “remove light” BFS (tracking old levels) - - then re-add from remaining sources -- If added: - - add BFS only - ---- - -## 9) Cross-Chunk Lighting - -### 9.1 Border Exchanges -Lighting must be consistent across chunk edges. - -Rules: -- When a chunk loads or updates lighting, it must: - - push border light changes to neighbors - - or request neighbor border values during BFS - -Implementation options: -- Option A: keep a 1-block “light padding” border cache per chunk -- Option B: BFS queries neighbor chunk live via accessor - -V1 recommendation: -- Query neighbors live and queue work if neighbor missing. - -### 9.2 When Neighbor Missing -Treat missing neighbor as: -- “opaque” for propagation? (prevents light leaking) -- OR “air” for propagation? (causes popping) -Recommended: -- Treat missing as opaque for lighting to avoid fake leaks. -- When neighbor arrives, recompute border propagation for both. - ---- - -## 10) Rendering the Lighting - -### 10.1 V1 Lighting Model -In chunk mesh vertex data, include a packed light value per vertex: -- simplest: per-face/per-vertex light = sample from the adjacent voxel -- For each quad vertex, sample light from the block “in front” of the face. - -Shader: -- `brightness = light / 15.0` -- `color = textureColor * (ambient + brightness * directionalFactor)` - -### 10.2 Day/Night Integration -Do NOT recompute skylight values each timeOfDay. -Instead: -- Skylight values represent “full sun” exposure (0..15). -- Apply time-of-day as a global multiplier: - - `skyFactor = sunIntensity(timeOfDay)` - - final brightness uses: - - `skyLight * skyFactor` (scaled) - - blockLight unaffected (or slightly affected by exposure) -This gives: -- Day: bright outdoors -- Night: outdoors dim, but torches still bright - -### 10.3 Ambient Light -At night, avoid fully black outdoors: -- `ambient = lerp(nightAmbient, dayAmbient, sunIntensity)` -Example: -- nightAmbient: 0.05..0.12 -- dayAmbient: 0.20..0.35 - ---- - -## 11) Smooth Lighting (Optional v1, Recommended) - -“Minecraft-style” smooth lighting uses neighbor samples to create gradients across faces. - -Simpler approach: -- Per-vertex brightness = average of 4 nearby voxels adjacent to that vertex. -- Works well with greedy meshing. - -If you do this, merging faces must ensure the corner samples remain valid. - ---- - -## 12) Moonlight (Optional v1) - -Two approaches: - -### 12.1 Simple -- Moon only affects sky color -- World lighting at night is ambient + block lights -(v1 acceptable) - -### 12.2 Better -- Add `moonFactor` as part of `skyFactor` at night: - - `skyFactor = max(sunIntensity, moonIntensity * moonScale)` -- `moonScale` small (e.g. 0.10..0.25) - ---- - -## 13) Required Debug Tools - -- Toggle: show skylight as colors -- Toggle: show block light as colors -- Show current `timeOfDay`, `sunIntensity`, `moonIntensity` -- Force time presets: midnight/noon/sunrise/sunset -- Visualize light BFS queue sizes (optional) - ---- - -## 14) Implementation Order - -1. World time + sun/moon direction + sky shader -2. Fog matching time-of-day -3. Light storage per voxel (packed) -4. Skylight vertical pass per chunk -5. Block light BFS (add only) -6. Incremental light updates (remove+add) -7. Cross-chunk lighting propagation -8. Per-vertex light sampling and shader application -9. Optional smooth lighting - ---- - -## 15) Acceptance Criteria - -- Sun rises/sets; moon visible at night. -- Sky colors and fog shift naturally through the day. -- Outdoor areas brighten/dim with time-of-day without re-lighting the world. -- Caves are dark unless opened to the surface or lit by torches. -- Placing a torch lights nearby blocks smoothly. -- Lighting does not “leak” through solid terrain across chunk borders. -- Lighting updates are incremental (no full world rebuild). - ---- - diff --git a/docs/audit-5.md b/docs/audit-5.md deleted file mode 100644 index 1c84f52a..00000000 --- a/docs/audit-5.md +++ /dev/null @@ -1,356 +0,0 @@ -🔍 Comprehensive Code Audit Report -Executive Summary - -This Zig OpenGL/Vulkan voxel engine demonstrates solid architecture fundamentals with a well-designed RHI abstraction, clear layer separation, and sophisticated features (CSM shadows, multi-threaded chunk loading, procedural generation). However, significant technical debt has accumulated, particularly around code organization, error handling, and performance optimization. - -Overall Assessment: 6.5/10 -1. Architecture & Design Patterns -✅ Strengths - -RHI Abstraction (Excellent) - - src/engine/graphics/rhi.zig:170-364 - Clean vtable-based polymorphism - Supports both OpenGL 3.3+ and Vulkan backends - Dependency inversion achieved via interface contracts - No backend-specific code leaks into core engine - -Layer Separation (Good) - -src/engine/core/ - Core interfaces, job system, time -src/engine/graphics/ - Rendering, RHI, shaders -src/engine/math/ - Matrices, vectors, frustum -src/world/ - World, chunks, generation -src/game/ - Application, menus, state - -❌ Issues - -Monolithic App Object (CRITICAL) - - src/game/app.zig:31-586 - 586 lines with 25+ fields - Responsibilities: input, UI, rendering, world management, state machine, debug rendering, map editing - Violates Single Responsibility Principle severely - -Interface Underutilization (HIGH) - - src/engine/core/interfaces.zig defines IUpdatable, IRenderable, IWidget - But Camera, World, Chunk don't implement them - Only used for polymorphic storage, not actual behavior abstraction - -Direct Backend Coupling (MEDIUM) - - src/game/app.zig:458-466 - Direct OpenGL calls for debug shadows - src/game/app.zig:351-457 - Backends have different code paths - Shadow rendering logic differs significantly between backends - -2. Code Quality & Maintainability -✅ Strengths - - Consistent Zig naming (snake_case vars, PascalCase types) - Good use of packed structs for data compression (PackedLight) - Clean file organization following logical boundaries - -❌ Issues - -Large Functions (HIGH) - -// src/game/app.zig:143-584 - 440 line run() function -// src/world/worldgen/generator.zig:176-368 - 190 line generate() -// Nested conditionals 6-8 levels deep - -Code Duplication (MEDIUM) - - Surface calculation code duplicated in TerrainGenerator.generate() (lines 193-294, 370-422) - Vertex attribute setup repeated across backends - Similar biome lookups in multiple places - -Inconsistent Error Handling (HIGH) - -// src/engine/graphics/shader.zig:84-121 - initFromFile handles errors -// src/engine/graphics/rhi_opengl.zig:284-351 - createBuffer returns 0 on error, no logging -// src/world/world.zig:283-291 - getOrCreateChunk has error handling -// src/engine/core/job_system.zig:97-113 - updatePlayerPos silently drops jobs on OOM - -Magic Numbers (MEDIUM) - - src/world/world.zig:29 - 80 (HashMap capacity) - src/world/chunk_mesh.zig:20 - SUBCHUNK_SIZE = 16 - src/game/app.zig:399 - max_uploads: usize = 4 (no explanation) - -3. Performance & Optimization -✅ Strengths - -Chunk System (Good) - - Subchunking for efficient frustum culling (chunk_mesh.zig:21) - Greedy meshing reduces triangle count by 30-50% - Pinning system prevents race conditions during async operations - Packed light storage (8 bits instead of 16) - -Job System (Good) - - Priority queue for distance-based job ordering - Separate pools for generation (4 threads) and meshing (3 threads) - Efficient async chunk loading - -❌ Issues - -Memory Management (HIGH) - -// src/world/chunk_mesh.zig:234-239, 248-253 -// Buffer destroyed and recreated on every mesh upload: -if (self.subchunks[si].solid_handle != 0) { - rhi.destroyBuffer(self.subchunks[si].solid_handle); -} -// Should: Ring buffer or buffer orphaning - -GPU Resource Issues (HIGH) - - Vulkan: Uses host-visible coherent memory everywhere (slow) - rhi_vulkan.zig:267 - Should use staging + device-local - Uniforms: Recreated per-frame, should use ring buffer - Texture Atlas: Regenerated unnecessarily (16×256×256 = 4MB/pixel) - -Rendering Inefficiencies (HIGH) - -// src/world/world.zig:441-498 -// Linear iteration over all chunks, no spatial partition -var iter = self.chunks.iterator(); -while (iter.next()) |entry| { - // Each chunk sets model matrix and issues draw - // No draw call batching -} - -Shadow Mapping (MEDIUM) - - Separate FBOs/textures per cascade is fine - But drawShadowPass re-iterates all chunks per cascade - -Terrain Generation (MEDIUM) - - Shore distance calculation O(n²) with nested loops - Noise calculations could be memoized - -Missing Optimizations (HIGH) - - No occlusion culling beyond frustum - No instanced rendering for repeated geometry - No texture compression - No vertex buffer streaming with glMapBufferRange - -4. Graphics Engine Specific -✅ Strengths - -RHI Design (Excellent) - - Clean vtable abstraction - Backend-agnostic types (BufferHandle, TextureHandle, etc.) - Good separation of concerns - -❌ Issues - -Resource Lifecycle (MEDIUM) - -// No explicit state tracking for resources -// Manual cleanup required, easy to leak -// src/game/app.zig:130-140 -pub fn deinit(self: *App) void { - if (self.world_map) |*m| m.deinit(); - if (self.world) |w| w.deinit(); - // Manual ordering matters -} - -Shader Management (LOW) - - Embedded GLSL strings (acceptable for single-file) - No hot-reloading capability - Uniform lookups not cached (shader.zig:134-137) - -Vulkan-Specific Issues (HIGH) - -// src/engine/graphics/rhi_vulkan.zig:276-279 -fn init(ctx_ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!void { - _ = ctx_ptr; - _ = allocator; -} -// NEVER CALLS createRHI! Actual init is in createRHI function - -Shadow Pass Discrepancy (MEDIUM) - - OpenGL: External FBOs per cascade - Vulkan: Internal render passes - Different shadow map layouts - -5. Error Handling & Robustness -❌ Issues - -Inconsistent Error Propagation (CRITICAL) -Location Issue -rhi_opengl.zig:284-351 createBuffer returns 0 on failure, no error info -shader.zig:58-81 initSimple returns LinkFailed with no log -job_system.zig:97-113 updatePlayerPos silently drops jobs on OOM -app.zig:160 World creation failure sets state to .home but doesn't log error details -generator.zig:297-299 worm_carve_map error caught, logs but continues - -Missing Validation (MEDIUM) - - No GL error checking (glGetError()) after GL calls - Only shader compilation logs errors - No bounds checking on some array accesses - -Resource Leak Risks (HIGH) - - Panic in texture_atlas.zig:135 on OOM - no cleanup - Some paths in deinit() may skip cleanup - -6. Testing & Coverage -❌ Issues - -No Test Infrastructure (CRITICAL) - -build.zig - No test step defined -src/ - No test files (test_*.zig or *_test.zig) -.github/ - No test workflow - -Areas Requiring Tests - - Noise functions - critical for worldgen determinism - Block occlusion logic - Coordinate transformations (world↔chunk↔local) - Frustum culling - Light propagation - RHI backend implementations - -7. Build & Tooling -✅ Strengths - - Simple build.zig - Nix flake for reproducible dev environment - CI workflow exists (.github/workflows/opencode.yml) - -❌ Issues - -Missing Tooling (HIGH) - -Static Analysis: None (zig fmt exists but not enforced) -Testing: No test framework integration -Profiling: No Tracy/Valgrind integration -Benchmarking: No performance metrics - -Version Dependency (MEDIUM) - - Uses Zig nightly/master features - shader.zig:108 - @enumFromInt(1024 * 1024) for std.io.Limit - May break with Zig updates - -8. Documentation -✅ Strengths - - Well-commented shader strings - Good architecture docs (AGENTS.md) - Feature documentation files (shadows.md, clouds.md, etc.) - -❌ Issues - -Missing API Documentation (HIGH) - - No doc comments on most public functions - No explanation of file format for blocks - No contribution guide - -Architecture Gaps (MEDIUM) - - No threading model documentation - No state machine diagram for AppState - No data flow diagram - -Priority Action Items -🔴 CRITICAL (Immediate Action Required) -Priority Issue Location Action Est. Effort -P0 Memory leak on OOM texture_atlas.zig:135 Replace @panic with error return 2 hrs -P0 Broken Vulkan init rhi_vulkan.zig:276-279 Fix/merge init with createRHI 4 hrs -P0 No error info on buffer fail rhi_opengl.zig:351 Return error union with message 3 hrs -P1 Monolithic App struct app.zig:31-126 Extract Systems (InputSystem, RenderingSystem) 2 days -P1 Silent job drops job_system.zig:97-113 Log warning, retry or queue rebuild 4 hrs -🟠 HIGH (Next Sprint) -Priority Issue Location Action Est. Effort -P2 Buffer recreation on mesh chunk_mesh.zig:236-239 Implement ring buffer strategy 8 hrs -P2 Host-visible memory rhi_vulkan.zig:267 Add staging buffers + device-local 12 hrs -P2 No testing build.zig Add unit tests for math, worldgen 1 week -P2 GL error checking rhi_opengl.zig Add glGetError() after GL calls 6 hrs -P2 Inefficient chunk rendering world.zig:441-498 Add spatial partition (chunk grid) 16 hrs -🟡 MEDIUM (Technical Debt) -Priority Issue Location Action Est. Effort -P3 Duplication in generator generator.zig:193-427 Extract common surface calculation 4 hrs -P3 No uniform caching shader.zig:134-137 Add StringHashMap cache 2 hrs -P3 Shore distance O(n²) generator.zig:252-294 Use BFS/floodfill 6 hrs -P3 Missing API docs All files Add doc comments to public APIs 1 week -P3 No draw call batching world.zig:441-498 Batch by shader/state 12 hrs -🟢 LOW (Nice to Have) -Priority Issue Location Action Est. Effort -P4 No occlusion culling world.zig:441 Add HZB/octree culling 2 weeks -P4 No texture compression texture_atlas.zig Add BCn compression 1 week -P4 No shader hot-reload app.zig Implement file watching 8 hrs -P4 Add Tracy profiler Multiple Instrument key paths 3 days -SOLID Principles Assessment -Principle Score Notes -S - Single Responsibility 3/10 App, World have too many responsibilities -O - Open/Closed 6/10 RHI is extensible, but BlockType enum is closed -L - Liskov Substitution 8/10 Interface-based types work well -I - Interface Segregation 4/10 Interfaces exist but are too broad/not used -D - Dependency Inversion 7/10 RHI abstraction is excellent, but app depends on concretes - -Average SOLID Score: 5.6/10 -Performance Profile - -Identified Bottlenecks: - - Chunk iteration - O(n) linear scan every frame (~10K ops at r=16) - Buffer recreation - GPU sync on every mesh update - Host-visible memory - CPU→GPU bandwidth bottleneck (Vulkan) - Draw calls - No batching, 1000+ calls per frame - Shadow rendering - 3× chunk iteration per frame - -Estimated Improvement Potential: - - Ring buffers: +20-30% mesh upload speed - Spatial partition: +50-100% culling efficiency - Uniform caching: -10% uniform lookup overhead - Draw batching: +30-50% GPU throughput - -Refactoring Roadmap -Phase 1: Critical Fixes (1-2 weeks) - - Fix OOM handling in texture atlas - Fix Vulkan initialization - Improve error reporting - Add basic unit tests - -Phase 2: Architecture (2-3 weeks) - - Extract systems from App - Implement proper error handling - Add GL error checking - Document core APIs - -Phase 3: Performance (3-4 weeks) - - Implement ring buffers - Add spatial partition - Optimize Vulkan memory usage - Implement draw call batching - -Phase 4: Polish (1-2 weeks) - - Add profiling - Improve shader management - Hot-reloading - Documentation completion - -Total Estimated Effort: 7-11 weeks for one developer -Recommended Tools -Purpose Tool Priority -Profiling Tracy Profiler HIGH -Memory Valgrind/ASan HIGH -GPU RenderDoc/Nsight MEDIUM -Static Analysis zig fmt, zig ast-check MEDIUM -Testing zig test HIGH diff --git a/docs/biomes.md b/docs/biomes.md deleted file mode 100644 index b0f94358..00000000 --- a/docs/biomes.md +++ /dev/null @@ -1,486 +0,0 @@ -````md -# biomes.md — Extensible Biome System & Variety Expansion (Voxel Engine) - -This spec defines a **biome system** that: -- Produces **much more variety** (deserts, swamps, forests, etc.) -- Avoids “everything looks the same” -- Is **data-driven and extensible** -- Allows adding new biomes later without rewriting worldgen -- Integrates cleanly with existing terrain, caves, lighting, and rendering - -This is intentionally aligned with **Minecraft-style multi-parameter biome selection**, but simplified and engine-friendly. - ---- - -## 1) Goals - -- Large-scale biome regions (readable from high altitude) -- Smooth biome transitions (no hard borders) -- Biomes affect: - - surface blocks - - vegetation - - terrain shape bias - - colors (grass/water tint) -- Easy to add new biomes later -- Deterministic per seed - -Non-goals (v1): -- Full climate simulation -- Seasonal biome shifts -- Weather systems (rain/snow handled later) - ---- - -## 2) Core Concept: Biomes Are Chosen in “Climate Space” - -Biomes are **not chosen by a single noise**. - -Each biome is selected by evaluating **multiple continuous parameters**: - -Primary biome axes: -- **Temperature** (cold → hot) -- **Humidity** (dry → wet) -- **Elevation** (low → high) -- **Continentalness** (ocean → inland) -- **Ruggedness** (smooth → mountainous) - -This prevents repetition and allows meaningful combinations. - ---- - -## 3) Global Biome Parameter Fields - -These are computed **per (x,z)** column and reused everywhere. - -### 3.1 Temperature (T) -Controls cold vs hot biomes. - -```text -T = fbm2(seed+TEMP, X*sT, Z*sT, oct=3) → [0..1] -```` - -Adjust for altitude: - -```text -T_adj = clamp01(T - altitude * lapseRate) -``` - -Suggested: - -* `sT = 1/4000 .. 1/6000` -* `lapseRate = 0.25 .. 0.35` - ---- - -### 3.2 Humidity (H) - -Controls dry vs wet biomes. - -```text -H = fbm2(seed+HUM, X*sH, Z*sH, oct=3) → [0..1] -``` - -Suggested: - -* `sH = 1/3000 .. 1/5000` - ---- - -### 3.3 Continentalness (C) - -Already used in terrain: - -* ocean vs coast vs inland - -Reuse existing field: - -* deep ocean -* shallow ocean -* coast -* land -* deep inland - ---- - -### 3.4 Elevation (E) - -Normalized surface height: - -```text -E = clamp01((height - seaLevel) / elevationRange) -``` - -Used to separate: - -* beaches -* plains -* hills -* mountains -* alpine zones - ---- - -### 3.5 Ruggedness / Erosion (R) - -Already computed for cliffs/mountains. - -Reuse: - -* low R → smooth (plains, deserts) -* high R → rough (mountains, badlands) - ---- - -## 4) Biome Definition (Data-Driven) - -Each biome is defined by **constraints + weights**, not hard rules. - -### 4.1 Biome Struct - -```text -Biome { - id - name - - temperatureRange [min,max] - humidityRange [min,max] - elevationRange [min,max] - continentalRange [min,max] - ruggednessRange [min,max] - - priority (int) - blendWeight (float) - - surfaceBlocks { - top - filler - depthRange - } - - vegetationProfile - terrainModifiers - colorTints -} -``` - ---- - -## 5) Biome Selection Algorithm - -For each (x,z): - -1. Compute climate parameters: - - * T_adj, H, C, E, R -2. Evaluate **all biomes**: - - * If parameters fall outside biome ranges → score = 0 - * Otherwise compute normalized score based on distance to ideal center -3. Pick: - - * Highest score biome (v1) - * Or top 2 biomes for blending (optional v2) - -This makes biomes: - -* Predictable -* Tunable -* Expandable - ---- - -## 6) Core Biomes (v1) - -### 6.1 Ocean Biomes - -* Deep Ocean -* Ocean -* Shallow Ocean -* Beach - -Ocean biomes depend mostly on: - -* continentalness -* elevation - ---- - -### 6.2 Plains - -* Temp: temperate -* Humidity: low–medium -* Elevation: low -* Ruggedness: low - -Surface: - -* grass -* dirt filler - Vegetation: -* sparse trees -* grass - ---- - -### 6.3 Forest - -* Temp: temperate -* Humidity: medium–high -* Elevation: low–medium -* Ruggedness: low–medium - -Vegetation: - -* dense trees -* bushes -* tall grass - ---- - -### 6.4 Desert - -* Temp: high -* Humidity: very low -* Elevation: low–medium -* Ruggedness: low - -Surface: - -* sand -* sandstone filler - Vegetation: -* cactus -* dead bushes - -Terrain: - -* flatter, smoother -* fewer hills - ---- - -### 6.5 Swamp - -* Temp: warm -* Humidity: very high -* Elevation: near sea level -* Continentalness: inland - -Surface: - -* grass/mud -* shallow water pools - Vegetation: -* swamp trees -* reeds - -Special: - -* waterlogged terrain -* darker grass/water tint - ---- - -### 6.6 Mountains - -* Elevation: high -* Ruggedness: high - -Sub-variants by temperature: - -* Cold mountains → snow -* Warm mountains → bare stone - -Surface: - -* stone -* thin soil - Vegetation: -* sparse or none - ---- - -### 6.7 Snow / Tundra - -* Temp: very low -* Elevation: low–medium - -Surface: - -* snow -* frozen water - Vegetation: -* minimal - ---- - -## 7) Biome Influence on Terrain Shape - -Biomes should not only change blocks, but also **bias terrain**. - -Examples: - -* Desert: - - * reduce hill amplitude - * smooth noise -* Swamp: - - * clamp elevation near sea level - * add micro-depressions -* Mountains: - - * amplify peaks - * increase cliff chance -* Plains: - - * reduce ruggedness - -Apply these as **local modifiers** on top of base terrain. - ---- - -## 8) Vegetation System (Biome-Driven) - -Each biome has a vegetation profile: - -```text -VegetationProfile { - treeTypes - treeDensity - bushDensity - grassDensity - specialFeatures -} -``` - -Placement rules: - -* Deterministic per chunk -* Biome controls density and type -* Terrain slope limits placement - -This keeps forests dense and deserts sparse automatically. - ---- - -## 9) Biome Blending (v1 Simple, v2 Advanced) - -### v1 (Simple) - -* Single biome per column -* Hard switch at boundaries -* Acceptable initially - -### v2 (Recommended) - -* Pick top 2 biome scores -* Blend: - - * surface blocks (probabilistic) - * colors - * vegetation density -* Produces smooth transitions: - - * forest → plains - * desert → savanna - * swamp → forest - ---- - -## 10) Visual Biome Identity - -Each biome defines: - -* grass tint -* foliage tint -* water tint -* fog color bias (optional) - -These are applied in shaders via biome ID or biome color lookup. - ---- - -## 11) Adding New Biomes Later (Key Requirement) - -To add a new biome later: - -1. Define parameter ranges -2. Define surface blocks -3. Define vegetation profile -4. Register biome - -NO changes needed to: - -* core terrain generator -* cave system -* lighting -* chunk system - -Examples of easy future biomes: - -* Savanna -* Badlands -* Jungle -* Mangrove swamp -* Volcanic -* Mushroom fields - ---- - -## 12) Debug & Tooling (Strongly Recommended) - -* Biome visualization mode (color by biome) -* Climate visualization: - - * temperature map - * humidity map -* Show biome scores under cursor -* Toggle biome blending on/off - -These are essential for tuning. - ---- - -## 13) Acceptance Criteria - -* World contains clearly distinct regions: - - * deserts - * forests - * swamps - * mountains -* Biomes are large-scale and readable from above -* No checkerboard or noisy biome distribution -* Terrain shape changes with biome -* Adding a new biome requires only data changes -* Different seeds produce dramatically different biome layouts - ---- - -## 14) Implementation Order - -1. Implement temperature + humidity maps -2. Convert biomes to data-driven definitions -3. Single-biome selection -4. Surface + vegetation per biome -5. Terrain shape modifiers per biome -6. Visual tints -7. Debug visualizers -8. Optional biome blending - ---- - -End of spec. - -``` - -If you want next: -- **Biome blending implementation details** -- **Vegetation placement rules** -- **Biome-aware caves & ores** -- **Biome-specific ambient sounds** - -Say the word. -``` - diff --git a/docs/cave-system.md b/docs/cave-system.md deleted file mode 100644 index 6a91f085..00000000 --- a/docs/cave-system.md +++ /dev/null @@ -1,292 +0,0 @@ -Below is a **clean, engine-ready cave system spec** you can hand to your agent. -It is designed to add **interesting caves without ruining the surface** and avoids the “too many holes” problem you hit earlier. - ---- - -````md -# cave-system.md — Controlled, Natural Cave Generation (Voxel Engine) - -This spec defines a **multi-style cave system** inspired by modern Minecraft + Minetest concepts, but simplified and controllable. - -Goals: -- Large, readable cave networks -- Minimal surface perforation -- Deterministic, seeded generation -- No “swiss cheese” terrain -- Easy to tune density, rarity, and depth - ---- - -## 1) Design Principles - -1. **Caves are volumetric, not heightmap-based** -2. **Surface protection is mandatory** -3. **Caves appear in regions, not everywhere** -4. **Multiple cave types create variety** -5. **Rarity > density** - ---- - -## 2) Cave Types (v1) - -Implement **two cave systems**, layered: - -### A) Worm / Tunnel Caves (Primary) -- Long, winding tunnels -- Large connected networks -- Main exploration caves - -### B) Noise Cavities (Secondary) -- Small chambers -- Occasional bubbles / pockets -- Adds texture, not structure - -(Do NOT start with ravines or mega-caverns yet.) - ---- - -## 3) Global Cave Mask (Stops “Too Many Holes”) - -Before carving ANY caves, compute a **2D cave region mask**. - -### 3.1 Cave Region Noise (2D) -```text -C2D(x,z) = fbm2(seed+C2D, x*s, z*s, oct=3) → [0..1] -```` - -Suggested params: - -* `s = 1/900 .. 1/1500` -* Region threshold: - - * `C2D < 0.55` → NO caves - * `C2D >= 0.55` → caves allowed - -This ensures: - -* Entire regions with caves -* Entire regions with none - ---- - -## 4) Surface Protection (Critical) - -Never carve caves too close to the surface. - -### Rule - -```text -if (surfaceHeight(x,z) - y < minSurfaceDepth) → DO NOT carve -``` - -Suggested: - -* `minSurfaceDepth = 8 .. 14` - -This single rule removes: - -* Holes everywhere -* Collapsing hills -* Ugly exposed cave ceilings - ---- - -## 5) Worm / Tunnel Caves (Main System) - -### 5.1 Seeded Cave Worms - -For each chunk: - -* Seed RNG with `(worldSeed, chunkX, chunkZ, CAVE_WORM)` -* Spawn `N` worms: - - * `N = 0..2` (biased low) - -### 5.2 Worm Parameters - -Each worm has: - -* start position `(x,y,z)` -* direction vector `dir` -* radius `r` -* length `L` - -Suggested ranges: - -* `y`: 20..120 -* `r`: 2..5 -* `L`: 40..120 blocks - -### 5.3 Worm Step Algorithm - -For each step: - -1. Carve a sphere at current position -2. Move forward -3. Slightly rotate direction using noise -4. Occasionally: - - * branch (rare) - * change radius slightly - -Pseudo: - -```cpp -for i in 0..L: - carveSphere(pos, r) - dir += noiseVec3(pos) * turnStrength - dir = normalize(dir) - pos += dir * stepSize -``` - -### 5.4 Carve Rule - -For each voxel in sphere: - -* Only carve if: - - * cave mask allows - * surface protection allows - ---- - -## 6) Noise Cavities (Secondary System) - -Used for: - -* Small pockets -* Side chambers -* Irregular cave shapes - -### 6.1 3D Density Noise - -```text -D(x,y,z) = fbm3(seed+C3D, x*s, y*sY, z*s, oct=4) -``` - -Suggested: - -* `s = 1/48 .. 1/70` -* `sY = same or slightly lower` -* `threshold = 0.65 .. 0.75` - -### 6.2 Vertical Bias - -Restrict cavities to depth bands: - -```text -band = smoothstep(20, 50, y) * (1 - smoothstep(120, 160, y)) -``` - -### 6.3 Final Carve Condition - -```text -if C2D >= 0.55 -and band > 0 -and D > threshold -and surfaceDepth >= minSurfaceDepth -→ carve -``` - ---- - -## 7) Connectivity Rules (Important) - -* Worm caves define the main network -* Noise cavities must: - - * either intersect a worm cave - * or be extremely rare - -This prevents: - -* Isolated 1×1 holes -* Random floating cavities - ---- - -## 8) Performance & Chunk Safety - -### 8.1 Chunk Boundary Safety - -* Worms may cross chunk borders -* Use world-space carving, not chunk-local -* Ensure neighbor chunks mark themselves dirty - -### 8.2 Determinism - -* All cave decisions must be: - - * seed-based - * chunk-coordinate based -* No frame-based randomness - ---- - -## 9) Debug Toggles (Highly Recommended) - -* `show_cave_mask` (visualize regions) -* `disable_noise_caves` -* `disable_worm_caves` -* `draw_cave_bounds` - -These dramatically speed tuning. - ---- - -## 10) Tuning Guide (Quick Fixes) - -If caves are too common: - -* Increase `C2D threshold` -* Increase `minSurfaceDepth` -* Increase 3D noise threshold - -If caves feel boring: - -* Increase worm length -* Increase branch chance slightly -* Increase turn strength - -If caves break the surface: - -* Increase `minSurfaceDepth` -* Lower worm start Y max - ---- - -## 11) Acceptance Criteria - -* Large cave networks exist underground -* Surface terrain remains intact -* Not every mountain has holes -* Caves are discoverable but not everywhere -* Different seeds produce different cave regions - ---- - -## 12) Future Extensions (Not v1) - -* Ravines (vertical worms) -* Mega caverns -* Aquifers / flooded caves -* Biome-specific cave materials -* Stalactites / stalagmites - ---- - -End of spec. - -``` - ---- - -If you want next: -- **Ore distribution tied to cave proximity** -- **Lighting rules for caves** -- **Water/lava cave interactions** -- **Debug visualisation shaders** - -Say which one. -``` - diff --git a/docs/clouds.md b/docs/clouds.md deleted file mode 100644 index c0ae6013..00000000 --- a/docs/clouds.md +++ /dev/null @@ -1,343 +0,0 @@ -````md -# clouds.md — Cloud System Specification (Voxel Engine) - -This document defines a **v1 cloud system** that: -- Looks good from ground and high altitude -- Moves naturally with time and wind -- Integrates with sun/moon lighting -- Works with large render distances -- Avoids heavy volumetric cost (but leaves a path to v2) - -The design is **tiered**: -- v1: 2D/2.5D clouds (cheap, stable, Minecraft-like) -- v2: optional volumetric upgrade later - ---- - -## 1) Goals - -- Visually readable clouds at all altitudes -- Clouds move consistently with wind -- Clouds react to time-of-day (lighting + color) -- Minimal shimmer or popping -- No coupling to terrain/worldgen logic - -Non-goals (v1): -- True volumetric scattering -- Cloud self-shadowing on other clouds -- Weather simulation (rain, storms) - ---- - -## 2) Cloud Types (v1) - -### 2.1 Primary: Layered 2D Clouds (Recommended) -- Single horizontal cloud layer at fixed altitude -- Rendered as a large projected plane or sky-domain sampling -- Noise-based coverage and shape - -This is: -- Cheap -- Stable -- Easy to tune -- Matches voxel aesthetic well - -### 2.2 Optional Secondary: Low Fog/Cloud Mist -- Very low-opacity fog band near cloud height -- Enhances depth and scale -- Optional, can be skipped in v1 - ---- - -## 3) Cloud Coordinate Space (Critical) - -Clouds must be: -- **Camera-relative** -- Independent of world origin -- Sampled in **world XZ**, but rendered relative to camera - -Rule: -```text -cloudSamplePos = (worldXZ + windOffset) -renderPos = cameraRelative -```` - -This prevents: - -* Precision shimmer -* “Sliding” when far from origin - ---- - -## 4) Cloud Layer Parameters - -### 4.1 Base Settings - -* `cloudHeight` (world Y): e.g. 140–180 -* `cloudThickness`: e.g. 8–20 units -* `cloudCoverage`: 0..1 (global density) -* `cloudScale`: noise scale (controls cloud size) - -Suggested defaults: - -* height: 160 -* thickness: 12 -* scale: 1 / 800 .. 1 / 1200 - -### 4.2 Wind - -* Wind direction: normalized vec2 (XZ) -* Wind speed: units per second (e.g. 0.5..3.0) - -Maintain: - -* `windOffset += windDir * windSpeed * deltaTime` - ---- - -## 5) Noise Model (Key to “not samey”) - -### 5.1 Base Shape Noise - -Use 2D noise (OpenSimplex / Perlin): - -```text -N1 = fbm2(seed + C1, (x+wind)*s1, (z+wind)*s1, oct=4) -``` - -Low frequency, large shapes. - -### 5.2 Detail Noise - -Add higher-frequency breakup: - -```text -N2 = fbm2(seed + C2, (x+wind)*s2, (z+wind)*s2, oct=3) -``` - -### 5.3 Final Coverage - -```text -cloudValue = N1 * 0.7 + N2 * 0.3 -cloudMask = smoothstep(thresholdLow, thresholdHigh, cloudValue) -``` - -Adjust thresholds using `cloudCoverage`. - ---- - -## 6) Rendering Approaches - -### 6.1 Option A — Projected Cloud Plane (Recommended v1) - -Render a large quad at `cloudHeight` centered on camera. - -Vertex shader: - -* Quad in local space -* Offset to camera XZ -* Fixed Y = cloudHeight - -Fragment shader: - -* Sample noise using world XZ -* Alpha = cloudMask -* Apply lighting - -Pros: - -* Very simple -* Stable -* Works with shadows/fog easily - -Cons: - -* Clouds always flat (acceptable for v1) - ---- - -### 6.2 Option B — Sky-Space Raymarch (Optional) - -Sample clouds in sky shader using view ray intersection with cloud slab. - -More complex, but: - -* No geometry -* Natural horizon blending - -Not required for v1. - ---- - -## 7) Lighting & Time-of-Day Integration - -### 7.1 Sun Lighting - -Cloud brightness depends on sun angle: - -* `lightFactor = clamp(dot(sunDir, up), 0..1)` -* Brightest at noon -* Dimmer at sunrise/sunset - -Apply: - -```text -cloudColor = baseCloudColor * mix(nightTint, dayTint, sunIntensity) -``` - -### 7.2 Sunset / Sunrise Tint - -Near horizon: - -* Add warm tint when sun is low -* Blend based on sun elevation - -This gives: - -* Orange/pink clouds at dusk/dawn -* White clouds at noon - -### 7.3 Moon Lighting (Optional v1) - -At night: - -* Very subtle moonlight contribution -* Cool blue tint -* Low intensity - ---- - -## 8) Shadows (v1 Simple, v2 Optional) - -### 8.1 v1: Fake Cloud Shadows (Cheap & Effective) - -Project cloud noise onto terrain: - -* Sample same cloud noise in terrain fragment shader -* Offset by sun direction -* Darken terrain slightly where cloudMask > threshold - -This gives: - -* Moving cloud shadows -* Zero shadow-map cost - -Control strength: - -* `cloudShadowStrength = 0.05 .. 0.15` - -### 8.2 v2: Real Shadow Maps (Not required) - -* Clouds rendered into shadow map -* Expensive, complex -* Skip for now - ---- - -## 9) Fog & Depth Integration - -Clouds should blend with fog: - -* Clouds fade into horizon fog -* At high altitude, clouds below camera fade smoothly - -Rules: - -* If camera Y > cloudHeight: - - * fade cloud opacity as camera rises above layer -* If camera Y < cloudHeight: - - * clouds appear overhead only - ---- - -## 10) Performance Considerations - -* One draw call for clouds -* No per-chunk work -* No lighting recompute -* Noise computed per fragment (cheap) - -Avoid: - -* Per-voxel clouds -* 3D raymarching in v1 -* Cloud geometry tied to chunks - ---- - -## 11) Debug & Tuning Tools - -Required: - -* Toggle clouds on/off -* Sliders: - - * coverage - * scale - * speed - * height -* Visualize cloudMask (grayscale) -* Freeze wind (for stability testing) - ---- - -## 12) Failure Modes & Fixes - -### Clouds shimmer at distance - -* Ensure camera-relative rendering -* Avoid world-space vertex positions -* Clamp noise precision - -### Clouds slide incorrectly with camera - -* Ensure sampling uses world XZ + wind, not view-space - -### Clouds look tiled/repeating - -* Increase noise scale -* Add domain warp (small) -* Blend two noise layers with different scales - ---- - -## 13) Implementation Order - -1. Time-of-day + sun direction hookup -2. Single cloud quad rendered above world -3. Noise-based alpha mask -4. Wind movement -5. Day/night color blending -6. Fake cloud shadows on terrain -7. Fog/horizon blending -8. Debug UI - ---- - -## 14) Acceptance Criteria - -* Clouds move smoothly across the sky -* Clouds respond to time-of-day -* No jitter when flying far or rotating camera -* Terrain subtly darkens under clouds -* Performance impact negligible - ---- - -## 15) Future Extensions (v2+) - -* Volumetric clouds (raymarching) -* Weather systems (rain, storms) -* Thunderhead clouds -* Cloud self-shadowing -* Lightning flashes - ---- - -End of spec. - -``` -``` - diff --git a/docs/coastlines.md b/docs/coastlines.md deleted file mode 100644 index 999ee5b2..00000000 --- a/docs/coastlines.md +++ /dev/null @@ -1,391 +0,0 @@ -````md -# coastlines.md — Natural Beaches, Shores, and Coastal Transitions (Voxel Engine) - -This spec fixes “too much sand between sea and trees” and improves coastline quality to feel closer to Minecraft/Minetest. - -Key idea: -- **Beaches are not a biome paint.** -- Beaches are a **conditional shoreline rule** based on: - - sea level proximity - - slope - - ocean exposure (ocean vs lake/river) - - local coastal width variation -- Forests need a **coastal transition band** (reduced trees) so you don’t get “forest meets sand”. - ---- - -## 1) Goals - -- Narrow, believable beaches (typical 2–5 blocks). -- Wider beaches only in exposed areas (rare). -- Steep coasts become cliffs (little/no sand). -- Forests do not touch sand directly; include a transition band. -- Rivers/lakes do not create massive beaches. -- Data-driven/tunable; works with future biomes. - -Non-goals (v1): -- Real dune simulation -- Wave erosion -- Tidal effects - ---- - -## 2) Definitions & Inputs - -World constants: -- `seaLevel` (e.g. 64) -- `waterBlockId` -- `airBlockId` - -Per-column values (computed for each XZ): -- `h = surfaceHeight(x,z)` (top solid block height) -- `depth = h - seaLevel` -- `slope = maxAbsNeighborDelta(h, x,z)` (see §3) -- `continentalness C(x,z)` (0..1) -- `isOcean(x,z)` derived from continentalness (see §4) - -Optional but recommended: -- `shoreDistOcean(x,z)` distance (blocks) from column to nearest **ocean water** (see §5) - ---- - -## 3) Slope Metric (Cheap and Effective) - -Compute a simple 2D gradient from neighbor heights: - -### 3.1 4-neighbor slope -```text -slope4 = max( - abs(h - h(x+1,z)), - abs(h - h(x-1,z)), - abs(h - h(x,z+1)), - abs(h - h(x,z-1)) -) -```` - -Optional 8-neighbor (stronger): - -* include diagonals as well - -Recommended: - -* use 4-neighbor for speed -* optionally clamp to a reasonable range - -Why: - -* Beaches form on gentle slopes. -* Steep slopes should become cliffs/rock. - ---- - -## 4) Ocean vs Lake/River (Critical) - -The main reason you have too much sand is usually: - -* treating ANY nearby water as “coast”. - -We must distinguish **ocean shoreline** from inland water. - -### 4.1 Ocean classification using continentalness - -Example thresholds (tune to your generator): - -* `C < 0.35` => deep ocean -* `0.35..0.45` => ocean -* `0.45..0.52` => coast band -* `> 0.52` => inland - -Define: - -```text -isOceanWater(x,z) = (column is water) AND (C(x,z) < oceanThreshold) -``` - -Define: - -```text -isOceanLand(x,z) = (column is land) AND (C(x,z) < inlandCutoff) -``` - -Where: - -* `oceanThreshold ~ 0.45` -* `inlandCutoff ~ 0.55` - -This prevents: - -* huge “beaches” around lakes -* sand banding around rivers - ---- - -## 5) Shore Distance to Ocean (Two Options) - -Beaches need a distance-from-shore measurement. - -### Option A (v1, simple): Local radius search (fast, approximate) - -For a land column, search within radius R (e.g. 12) for any `isOceanWater`. - -Return: - -* `shoreDistOcean = min manhattan/chebyshev distance to found ocean water` -* If none found: `shoreDistOcean = INF` - -This is easy and good enough for v1. - -### Option B (v2, best): BFS distance field (accurate) - -For a region (e.g. chunk + padding), BFS from all `isOceanWater` cells to compute distance for all land cells. - -Store per chunk: - -* `shoreDistOcean` array (16×16) - -Recommended later when you want perfect coast control. - ---- - -## 6) Beach Width Field (No More Uniform Bands) - -Beaches should vary width based on “exposure”. - -Compute an exposure noise: - -```text -exposure = fbm2(seed+EXPOSE, x*sE, z*sE, oct=2..3) -> [0..1] -``` - -Suggested: - -* `sE = 1/1500 .. 1/2500` - -Then: - -```text -baseBeachWidth = lerp(2, 7, exposure) -``` - -Now incorporate slope: - -```text -slopeFactor = 1 - smoothstep(slopeMin, slopeMax, slope) -beachWidth = baseBeachWidth * slopeFactor -``` - -Suggested: - -* `slopeMin = 1` -* `slopeMax = 4` - -Interpretation: - -* gentle coast (slope ~0..1): width stays near base width -* steep coast (slope >= 4): width collapses toward 0 - -Finally clamp: - -```text -beachWidth = clamp(beachWidth, 0, 10) -``` - ---- - -## 7) Beach Eligibility Rules (When to Place Sand) - -A land column becomes “beach sand” only if: - -### 7.1 Near sea level - -Beaches are near sea level, not up on mountains. - -```text -depthOK = (depth >= 0) AND (depth <= beachMaxDepth) -``` - -Suggested: - -* `beachMaxDepth = 6` (0..6 blocks above sea level) - -### 7.2 Ocean shoreline only - -```text -oceanOK = (shoreDistOcean != INF) -``` - -### 7.3 Within computed beach width - -```text -widthOK = (shoreDistOcean <= beachWidth) -``` - -### 7.4 Gentle slope only - -```text -slopeOK = (slope <= beachSlopeMax) -``` - -Suggested: - -* `beachSlopeMax = 2` (tune 1..3) - -### 7.5 Final condition - -```text -isBeach = depthOK && oceanOK && widthOK && slopeOK -``` - -Result: - -* Typical beaches: 2–5 blocks -* Wide beaches: rare and only exposed shores -* Cliff shores: almost no sand - ---- - -## 8) Cliff Shores (Rock meets sea) - -When slope is steep near sea level, we want rock/cliff not sand. - -Define: - -```text -isCliffCoast = depthOK && oceanOK && (slope >= cliffSlopeMin) -``` - -Suggested: - -* `cliffSlopeMin = 4` - -If `isCliffCoast`: - -* top block becomes stone/rock (or biome rock) -* optionally place gravel at waterline - ---- - -## 9) Coastal Transition Band (Fix “trees touch beach”) - -Even with good beaches, forests shouldn’t start immediately behind sand. - -Define a coastal vegetation suppression band: - -```text -noTreeDist = lerp(noTreeMin, noTreeMax, exposure) -``` - -Suggested: - -* `noTreeMin = 6` -* `noTreeMax = 18` - -Rule: - -```text -if shoreDistOcean <= noTreeDist: - suppress trees (tree density = 0 or near 0) - allow shrubs/grass -``` - -This creates: - -* beach → grassy/shrubby band → forest - ---- - -## 10) Coastal Micro-Biomes (Optional, High Impact) - -Instead of abrupt biome adjacency, introduce a “CoastalPlains” transition for forests: - -Rule: - -```text -if biome == Forest && shoreDistOcean <= coastalBand && !isBeach: - biome = CoastalPlains -``` - -Suggested: - -* `coastalBand = 12..24` - -CoastalPlains characteristics: - -* same climate as forest, but: - - * tree density reduced (e.g. 20–40% of forest) - * more grass and shrubs - * occasional sand patches near beach edge - -This is how you get Minecraft-like “soft” coasts. - ---- - -## 11) Sand Placement Scope (Important) - -Sand should be applied as a *surface override* only: - -* do not change underlying height -* do not force large dunes everywhere in non-desert biomes - -Surface layering rule: - -* If `isBeach`: top = sand, filler = sand/sandstone (few layers) -* Else: biome decides top block normally - -For deserts: - -* desert biome still uses sand inland -* but coastline rules still control shore realism - ---- - -## 12) Implementation Order - -1. Compute `slope(x,z)` from neighbor heights. -2. Implement `isOceanWater/isOceanLand` using continentalness thresholds. -3. Implement `shoreDistOcean` (Option A radius search first). -4. Implement exposure noise and `beachWidth`. -5. Implement `isBeach` and `isCliffCoast` surface overrides. -6. Add `noTreeDist` suppression band for vegetation placement. -7. Add optional `CoastalPlains` micro-biome rule. -8. Add debug visualizers. - ---- - -## 13) Debug Visualizers (Required) - -* Color by `shoreDistOcean` (gradient) -* Show `beachWidth` map -* Highlight `isBeach` cells -* Highlight `isCliffCoast` cells -* Show vegetation suppression band - -These are critical for tuning. - ---- - -## 14) Tuning Targets (What “good” looks like) - -* Typical beach width: 2–5 blocks -* Wide beaches: 6–10 blocks only in exposed areas -* Cliff shores: 0–2 blocks of sand (or none) -* Trees start: 8–20 blocks inland (varies by exposure/biome) - ---- - -## 15) Acceptance Criteria - -* No massive uniform sand bands around oceans. -* Forests do not touch sand immediately; transition band exists. -* Steep mountain coasts become cliffs rather than beaches. -* Lakes and rivers do not generate huge beaches. -* Coastline varies naturally (bays, coves, exposed shores). - ---- - -End of spec. - -``` -``` - diff --git a/docs/decouple.md b/docs/decouple.md deleted file mode 100644 index 5193dd6f..00000000 --- a/docs/decouple.md +++ /dev/null @@ -1,78 +0,0 @@ -Here is a comprehensive `decouple.md` file designed for an AI agent or a developer. It outlines the architectural shift from a hard-coded OpenGL renderer to an API-agnostic **Render Hardware Interface (RHI)** while preserving your existing greedy meshing and game logic. - -*** - -# Technical Specification: Decoupling Renderer for Vulkan/OpenGL RHI - -## 1. Objective -The goal is to move from a direct OpenGL implementation to a **Render Hardware Interface (RHI)**. This allows the engine to support multiple backends (Vulkan via `mach-gpu` and a legacy OpenGL fallback) while keeping the game logic, world generation, and greedy meshing code 100% agnostic of the graphics API. - -## 2. Core Architecture: The "Frontend/Backend" Split -We will divide the engine into two distinct layers: -1. **Frontend (Game Logic):** Manages the world, chunk data, greedy meshing, and camera. It produces "Render Commands" and "Vertex Data." -2. **Backend (RHI):** Consumes data and commands to interface with the GPU (Vulkan/OpenGL). - -## 3. The RHI Interface -Create a Zig `Interface` or a `struct` with function pointers to abstract the following operations: - -```zig -const RHI = struct { - // Lifecycle - init: *const fn (allocator: Allocator) anyerror!void, - deinit: *const fn () void, - - // Resource Management - createBuffer: *const fn (data: []const u8, usage: BufferUsage) BufferHandle, - destroyBuffer: *const fn (handle: BufferHandle) void, - - // Command Recording - beginFrame: *const fn () void, - endFrame: *const fn () void, - - // Draw Calls - drawMesh: *const fn (handle: BufferHandle, count: u32, camera: CameraUniform) void, -}; -``` - -## 4. Migration Steps - -### Step A: Isolate the Vertex Format -Currently, your vertices are likely uploaded directly. We must define a fixed, byte-compatible layout. -- **Action:** Define a `Vertex` struct in a shared module. -- **Action:** Ensure the Greedy Mesher outputs a `std.ArrayList(Vertex)` or a raw `[]u8` buffer. -- **Constraint:** The Mesher must NOT call `glBufferData`. It must return the data to a "Renderer Manager." - -### Step B: The "Buffer Handle" System -Vulkan and OpenGL handle IDs differently (pointers vs. integers). -- **Action:** Implement a `Handle` system (integers or UUIDs) to reference GPU resources. -- The Game Logic holds a `ChunkMeshHandle`. The RHI maps that handle to either a `GLuint` (VAO/VBO) or a `VkBuffer`. - -### Step C: Decouple Shaders (SPIR-V Pipeline) -Vulkan uses SPIR-V; OpenGL uses GLSL. -- **Action:** Move shaders to external files. -- **Action:** Use `glslangValidator` to compile GLSL to SPIR-V for the Vulkan backend. -- **Optimization:** Use `#version 450` in GLSL and ensure `layout(set=..., binding=...)` is used, as Vulkan requires explicit descriptor sets. - -### Step D: The Upload Queue (Multiplayer Optimization) -To prevent the "stuttering" during high-view-distance chunk loading: -- **Action:** Create a `TransferQueue`. -- When a chunk is meshed, the Frontend calls `RHI.uploadAsync(mesh_data)`. -- **OpenGL Backend:** Will likely execute this on the main thread (driver limitation). -- **Vulkan Backend:** Will use a dedicated `VkTransferQueue` and Fences to upload in the background without dropping frames. - -## 5. View Distance & Performance Targets -- **Indirect Drawing:** The RHI should support "Draw Indirect." This allows the CPU to send a list of 1,000 chunk handles, and the GPU culls them. -- **Uniform Management:** Replace `glUniformMatrix4fv` with a "Global Uniform Buffer" that is updated once per frame. - -## 6. Implementation Notes for Agent -1. **Memory:** Use `Zig` allocators for all CPU-side staging buffers. -2. **Threading:** The Greedy Mesher should run on a thread pool (e.g., `zig-threadpool`). -3. **Stability:** On NixOS, ensure the RHI backend looks for `vulkan-loader` and `libX11` via the environment variables defined in the project's `flake.nix` or `shell.nix`. -4. **Fallback:** If `RHI.init(.vulkan)` fails (e.g., old drivers/NixOS config issues), the engine must automatically attempt `RHI.init(.opengl)`. - -## 7. Data Flow Diagram -`World Data` -> `Greedy Mesher` -> `Raw Vertex Buffer` -> `RHI Upload` -> `GPU Memory` -> `RHI Draw Call` - -*** - -**Next Action:** Begin by refactoring the `Chunk` struct to remove all `gl` prefixed calls, replacing them with `BufferHandle`. diff --git a/docs/feedback.md b/docs/feedback.md deleted file mode 100644 index 87d6421a..00000000 --- a/docs/feedback.md +++ /dev/null @@ -1,483 +0,0 @@ -Code Review: SOLID Refactor PR -1. Vulkan/OpenGL Parity -✅ Well-Aligned Areas - - Both backends implement all RHI vtable methods - updateGlobalUniforms, setModelMatrix, beginUI/endUI, drawUIQuad all provide functional equivalents - Shadow pass handling (beginShadowPass/endShadowPass) is conceptually aligned - -⚠️ Parity Issues - -rhi_vulkan.zig:1234-1238 - setViewport is a no-op: - -fn setViewport(ctx_ptr: *anyopaque, width: u32, height: u32) void { - _ = ctx_ptr; - _ = width; - _ = height; - // Vulkan handles viewport dynamically in render passes -} - - Issue: Comment says "dynamically in render passes" but OpenGL explicitly calls glViewport - Impact: Any code expecting setViewport to work may behave differently between backends - Fix: Either document this is a no-op for Vulkan, or implement explicit viewport tracking - -rhi_opengl.zig:672-680 vs rhi_vulkan.zig:1246-1253 - setWireframe timing: - -// OpenGL: Immediate state change -fn setWireframe(ctx_ptr: *anyopaque, enabled: bool) void { - _ = ctx_ptr; - if (enabled) { - c.glPolygonMode(c.GL_FRONT_AND_BACK, c.GL_LINE); - } else { - c.glPolygonMode(c.GL_FRONT_AND_BACK, c.GL_FILL); - } -} - -// Vulkan: Deferred state flag, only affects next pipeline bind -fn setWireframe(ctx_ptr: *anyopaque, enabled: bool) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - if (ctx.wireframe_enabled != enabled) { - ctx.wireframe_enabled = enabled; - // Force pipeline rebind next draw - ctx.terrain_pipeline_bound = false; - } -} - - Issue: OpenGL changes immediately; Vulkan requires a draw call to rebind pipeline - Impact: Different latency for wireframe toggle - Fix: Document this difference or force immediate rebind in Vulkan - -rhi_opengl.zig:830-835 vs rhi_vulkan.zig:954-958 - drawClouds both no-op: - -// OpenGL -fn drawClouds(ctx_ptr: *anyopaque, params: rhi.CloudParams) void { - _ = ctx_ptr; - _ = params; - // OpenGL path currently still uses Clouds struct directly from main.zig, - // but we can proxy it here if needed. -} - -// Vulkan -fn drawClouds(ctx_ptr: *anyopaque, params: rhi.CloudParams) void { - _ = ctx_ptr; - _ = params; - // TODO: Implement Vulkan cloud plane rendering -} - - Issue: Both stubbed, but main.zig still uses Clouds directly for OpenGL path (line 764) - Impact: Inconsistent abstraction - clouds not going through RHI - Fix: Either implement in both RHI backends or remove from RHI interface - -rhi_opengl.zig:672-687 - setTexturesEnabled no-op: - -fn setTexturesEnabled(ctx_ptr: *anyopaque, enabled: bool) void { - _ = ctx_ptr; - _ = enabled; - // OpenGL texture toggle is handled via shader uniform in renderer.zig - // This is a no-op here since the old code path handles it -} - - Issue: Comment references old renderer.zig code path, but shader binding happens in main.zig:657 - Impact: Confusing, suggests incomplete refactor - Recommendation: Either implement proper state tracking or remove the method - -rhi_opengl.zig:666-680 - drawUITexturedQuad state restoration issue: - -// Temporarily reconfigure vertex attributes for textured quad -const stride: c.GLsizei = 4 * @sizeOf(f32); -c.glVertexAttribPointer().?(0, 2, c.GL_FLOAT, c.GL_FALSE, stride, null); -c.glVertexAttribPointer().?(1, 2, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(2 * @sizeOf(f32))); -// ... draw ... -// Restore colored quad vertex format -const color_stride: c.GLsizei = 6 * @sizeOf(f32); -c.glVertexAttribPointer().?(0, 2, c.GL_FLOAT, c.GL_FALSE, color_stride, null); -c.glVertexAttribPointer().?(1, 4, c.GL_FLOAT, c.GL_FALSE, color_stride, @ptrFromInt(2 * @sizeOf(f32))); - -// Switch back to color shader -if (ctx.ui_shader) |*shader| { - shader.use(); -} - - Issue: This switches back to ui_shader, but if drawUITexturedQuad was called multiple times, the pipeline state ping-pongs - Vulkan equivalent (rhi_vulkan.zig:1581-1582): Also switches back to ui_pipeline - Fix: Consider separate VAOs for textured vs untextured quads instead of reconfiguring attributes - -2. SOLID Principles -✅ Single Responsibility (SRP) - GOOD - -UI extraction is well done: - - src/engine/ui/font.zig: Only handles bitmap font rendering - src/engine/ui/widgets.zig: Only handles button/text input widgets - Previously in main.zig, now properly separated - -World.render decoupling: - - src/world/world.zig:441-498: Now uses rhi.setModelMatrix instead of Shader - Removed hard dependency on Shader class - -⚠️ SRP Violations - -main.zig is still too monolithic: - - 1056 lines, handles game loop, UI, input, world management, both rendering paths - Contains conditional logic everywhere for Vulkan vs OpenGL paths - Functions like main() span lines 291-989 (698 lines) - -Suggested extraction: - -src/ - game/ - game_state.zig - AppState management, world lifecycle - app.zig - Main application struct with init/update/deinit - ui/ - menus.zig - Home, settings, singleplayer screens - -✅ Open/Closed (OCP) - GOOD - -RHI Interface: - - rhi.zig:170-228: VTable interface allows extending to new backends without modifying existing code - Adding Metal or DirectX would only require new rhi_metal.zig/rhi_directx.zig - -UI Widget extensibility: - - widgets.zig has simple, composable draw functions - Adding new widgets doesn't require modifying existing ones - -⚠️ Dependency Inversion (DIP) - PARTIAL - -Good: - - World depends on RHI abstraction, not concrete implementations - world.zig:86: rhi: RHI field - -Issues: - -main.zig still has concrete backend knowledge: - -// main.zig:397-428 -var shader: ?Shader = if (!is_vulkan) try Shader.initFromFile(...) else null; -var shadow_map: ?ShadowMap = if (!is_vulkan) ShadowMap.init(...) else null; -var atmosphere: ?Atmosphere = if (is_vulkan) Atmosphere.initNoGL() else Atmosphere.init(); -var clouds: ?Clouds = if (is_vulkan) Clouds.initNoGL() else try Clouds.init(); - - Issue: Conditional initialization creates tight coupling - Fix: Use factory pattern: - - const BackendFactory = struct { - fn createRenderer(allocator: Allocator, rhi: RHI, config: Config) !RendererInterface { ... } - fn createAtmosphere(allocator: Allocator, is_vulkan: bool) AtmosphereInterface { ... } - }; - -main.zig:653-763 - Conditional rendering paths: - -if (!is_vulkan) { - rhi.beginMainPass(); - if (atmosphere) |*a| a.renderSky(...); -} -// ... later ... -if (shader) |*s| { - s.use(); - // ... uniforms ... - active_world.render(view_proj_cull, camera.position); -} else if (is_vulkan) { - // ... completely different code path ... -} - - Issue: Two nearly separate rendering pipelines in one function - Fix: Extract to renderFrame.zig with backend-specific implementations - -❌ Interface Segregation (ISP) - POOR - -RHI has massive VTable (rhi.zig:174-228): - -pub const VTable = struct { - init: *const fn (ctx: *anyopaque, allocator: Allocator) anyerror!void, - deinit: *const fn (ctx: *anyopaque) void, - createBuffer: *const fn (ctx: *anyopaque, size: usize, usage: BufferUsage) BufferHandle, - // ... 20+ more functions ... - drawClouds: *const fn (ctx: *anyopaque, params: CloudParams) void, -}; - - Issue: Not all clients need all methods - World rendering only needs: setModelMatrix, draw - UI only needs: beginUI, endUI, drawUIQuad, drawUITexturedQuad - Main pass needs: beginMainPass, endMainPass, setClearColor, etc. - -Suggested split: - -pub const CoreRHI = struct { - init, deinit, createBuffer, destroyBuffer, uploadBuffer, // ... -}; - -pub const PassRHI = struct { - beginFrame, endFrame, beginMainPass, endMainPass, // ... -}; - -pub const DrawRHI = struct { - setModelMatrix, draw, drawSky, drawClouds, // ... -}; - -pub const UIRHI = struct { - beginUI, endUI, drawUIQuad, drawUITexturedQuad, // ... -}; - -Or use tagged unions/comptime to generate specialized interfaces. -✅ Liskov Substitution (LSP) - GOOD - -RHI backends can be swapped: - - main.zig:358-377: Falls back from Vulkan to OpenGL on error - Both backends implement the same VTable - -3. Memory Management -✅ Good Practices - -Proper RAII-like cleanup in Vulkan: - -// rhi_vulkan.zig:281-375 -fn deinit(ctx_ptr: *anyopaque) void { - // Comprehensive cleanup of all Vulkan objects in reverse order - if (ctx.device != null) { - _ = c.vkDeviceWaitIdle(ctx.device); - // ... cleanup all resources ... - } - ctx.allocator.destroy(ctx); -} - -Proper mutex protection: - - rhi_opengl.zig:31: mutex: std.Thread.Mutex for buffer lists - rhi_vulkan.zig:158: mutex: std.Thread.Mutex for buffer/texture maps - -Free list pattern for OpenGL buffers: - -// rhi_opengl.zig:337-351 -if (ctx.free_indices.items.len > 0) { - const new_len = ctx.free_indices.items.len - 1; - const idx = ctx.free_indices.items[new_len]; - ctx.free_indices.items.len = new_len; - ctx.buffers.items[idx] = .{ .vao = vao, .vbo = vbo }; - return @intCast(idx + 1); -} - -⚠️ Memory Issues - -rhi_opengl.zig:274-281 - Potential use-after-free in deinit: - -fn deinit(ctx_ptr: *anyopaque) void { - const ctx: *OpenGLContext = @ptrCast(@alignCast(ctx_ptr)); - { - ctx.mutex.lock(); - defer ctx.mutex.unlock(); - - // ... cleanup buffers ... - ctx.buffers.deinit(ctx.allocator); - ctx.free_indices.deinit(ctx.allocator); - } - - // ... cleanup UI resources ... - - ctx.allocator.destroy(ctx); // <-- Destroy context here -} - - After ctx.allocator.destroy(ctx), any deferred cleanup that hasn't run yet would be invalid - In this case, defer blocks execute in reverse order, so mutex.unlock() runs BEFORE destroy(ctx), which is correct ✅ - -rhi_vulkan.zig:282-375 - No error checking on destroy: - -fn deinit(ctx_ptr: *anyopaque) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - if (ctx.device != null) { - _ = c.vkDeviceWaitIdle(ctx.device); - - // ... many cleanup calls without checking for null ... - if (ctx.ui_pipeline != null) c.vkDestroyPipeline(ctx.device, ctx.ui_pipeline, null); - // ... - } - // ... -} - - Good: Checks for null before destroy - Issue: Some cleanup happens before checking ctx.device != null but still uses it - Line 287-295: Cleanup of UI resources assumes ctx.device is valid (protected by outer if) - -rhi_vulkan.zig:402-412 - Memory leak on buffer upload error: - -fn uploadBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, data: []const u8) void { - // ... - if (c.vkMapMemory(ctx.device, buf.memory, 0, @intCast(data.len), 0, &map_ptr) == c.VK_SUCCESS) { - @memcpy(@as([*]u8, @ptrCast(map_ptr))[0..data.len], data); - c.vkUnmapMemory(ctx.device, buf.memory); - } - // Issue: If map fails, data is not uploaded but no error is reported -} - - Issue: Silent failure if vkMapMemory fails - Fix: Should at least log an error, and ideally return a result type - -rhi_vulkan.zig:1162-1232 - Staging buffer allocation in updateTexture: - -fn updateTexture(ctx_ptr: *anyopaque, handle: rhi.TextureHandle, data: []const u8) void { - // ... - const staging_buffer = createVulkanBuffer(ctx, data.len, c.VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - defer { - c.vkDestroyBuffer(ctx.device, staging_buffer.buffer, null); - c.vkFreeMemory(ctx.device, staging_buffer.memory, null); - } - // ... -} - - Good: Uses defer for cleanup - Issue: createVulkanBuffer returns VulkanBuffer but doesn't indicate allocation failure (returns default-initialized struct) - Risk: If allocation fails, staging_buffer.buffer/memory might be 0/null, but still passed to Destroy/Free - Fix: createVulkanBuffer should return !VulkanBuffer - -rhi_vulkan.zig:830-835 - createTexture null pointer risk: - -fn createTexture(...) rhi.TextureHandle { - // ... - if (c.vkCreateImage(ctx.device, &image_info, null, &image) != c.VK_SUCCESS) return 0; - // ... - if (c.vkAllocateMemory(ctx.device, &alloc_info, null, &memory) != c.VK_SUCCESS) { - c.vkDestroyImage(ctx.device, image, null); // <-- Good: cleanup image - return 0; - } - if (c.vkBindImageMemory(ctx.device, image, memory, 0) != c.VK_SUCCESS) { - c.vkFreeMemory(ctx.device, memory, null); // <-- Good: cleanup memory - c.vkDestroyImage(ctx.device, image, null); // <-- Good: cleanup image - return 0; - } -} - - Good: Proper cleanup on failure - Minor: Could return an error union to distinguish between different failure modes - -world.zig:89-119 - World.init allocates queues but error handling is deferred: - -pub fn init(...) !*World { - const world = try allocator.create(World); - - const gen_queue = try allocator.create(JobQueue); - gen_queue.* = JobQueue.init(allocator); - - const mesh_queue = try allocator.create(JobQueue); - mesh_queue.* = JobQueue.init(allocator); - - world.* = .{ - // ... - .gen_queue = gen_queue, - .mesh_queue = mesh_queue, - // ... - }; - - world.gen_pool = try WorkerPool.init(allocator, 4, gen_queue, world, processGenJob); - world.mesh_pool = try WorkerPool.init(allocator, 3, mesh_queue, world, processMeshJob); - - Issue: If WorkerPool.init fails after gen_queue/mesh_queue creation, those queues are leaked - Fix: Use errdefer or initialize in order with cleanup on failure - -world.zig:122-144 - deinit calls rhi.waitIdle(): - -pub fn deinit(self: *World) void { - self.rhi.waitIdle(); // <-- Good: ensure GPU is done - // ... -} - - Good: Ensures GPU resources aren't in use before cleanup - -main.zig:458-474 - Safe deferred cleanup: - -while (!input.should_quit) { - // Safe deferred world management OUTSIDE of frame window - if (pending_world_cleanup or pending_new_world_seed != null) { - rhi.waitIdle(); // <-- Wait before cleanup - if (world) |w| { - w.deinit(); - world = null; - } - pending_world_cleanup = false; - } - // ... - rhi.beginFrame(); // Frame starts after cleanup - - Excellent: Clean separation of cleanup vs frame lifecycle - -⚠️ Potential Race Conditions - -world.zig:372-397 - Chunk state machine with mutex gaps: - -self.chunks_mutex.lock(); -var mesh_iter = self.chunks.iterator(); -while (mesh_iter.next()) |entry| { - const data = entry.value_ptr.*; - if (data.chunk.state == .generated) { - // ... calculate dist ... - data.chunk.state = .meshing; // <-- State change under mutex - try self.mesh_queue.push(...); // <-- Push might fail - } - // ... more state changes ... -} -self.chunks_mutex.unlock(); - - Issue: If try self.mesh_queue.push fails, the chunk is left in .meshing state but never queued - Fix: Queue push should happen before state change, or handle error properly - -4. Other Issues -renderer.zig (rhi_opengl.zig:673-687) - -fn setTexturesEnabled(ctx_ptr: *anyopaque, enabled: bool) void { - _ = ctx_ptr; - _ = enabled; - // OpenGL texture toggle is handled via shader uniform in renderer.zig - // This is a no-op here since the old code path handles it -} - - Issue: Comment references "old code path" but renderer.zig was emptied (now only has setVSync and utility functions) - Fix: Update comment or implement actual functionality - -main.zig:389 - Conditional VSync - -var time = Time.init(); -if (!is_vulkan) setVSync(settings.vsync); - - Issue: VSync not set for Vulkan at init - Fix: Should also call rhi.setVSync(settings.vsync) unconditionally since RHI handles it - -Missing error handling in RHI functions - -Most RHI functions return void or simple handles (u32), making error handling difficult: - -// rhi.zig -pub const VTable = struct { - createBuffer: *const fn (ctx: *anyopaque, size: usize, usage: BufferUsage) BufferHandle, - // Returns 0 on error (InvalidBufferHandle), but caller doesn't know WHY it failed -}; - - Fix: Consider returning error unions for critical operations - -Summary -✅ Strengths - - UI extraction is clean and follows SRP well - RHI abstraction allows backend swapping - Vulkan backend has comprehensive resource management - World rendering properly decoupled from Shader class - -⚠️ Issues to Address - - Parity: setViewport no-op in Vulkan, inconsistent wireframe toggle timing - ISP violation: RHI vtable too large, should be split - DIP incomplete: main.zig still has heavy conditional backend logic - main.zig monolith: 1000+ lines, should be split into separate modules - Memory: Some functions silently fail (uploadBuffer), createTexture returns 0 on all errors - Race condition: Chunk state updates not atomic with queue operations - -🔴 High Priority - - Implement proper error propagation in Vulkan buffer/texture creation - Fix state machine race condition in World.update - Remove stub drawClouds or implement it properly - Split main.zig into smaller, focused modules - diff --git a/docs/mesh.md b/docs/mesh.md deleted file mode 100644 index f585d4bf..00000000 --- a/docs/mesh.md +++ /dev/null @@ -1,327 +0,0 @@ -# meshing.md — Chunk Meshing (16×256×16) with Face Culling + Greedy Meshing - -This document specifies the meshing system for a voxel engine with chunks sized **16 (X) × 256 (Y) × 16 (Z)**. -It covers: -- Face visibility (culling) -- Greedy meshing (rectangle merging) -- Subchunk strategy (16×16×16) for smooth updates -- Opaque vs transparent passes -- Chunk-boundary neighbor handling -- Data structures, state, and rebuild triggers - ---- - -## 1) Goals - -- Minimize triangles/draw calls via: - - **Face culling** (don’t emit internal faces) - - **Greedy meshing** (merge adjacent coplanar faces into large quads) -- Support smooth streaming and edits: - - Mesh rebuilds should be limited to affected regions, not entire 256-high chunks. -- Deterministic output given same chunk/block data. - ---- - -## 2) Chunk Layout & Subchunks - -### 2.1 Storage -- Chunk dimensions: `CX=16`, `CY=256`, `CZ=16` -- Total blocks: 65,536 -- Block storage can remain as a single array: - - index: `idx = x + z*CX + y*CX*CZ` - - memory order: X fastest, then Z, then Y - -### 2.2 Meshing granularity: subchunks -Mesh in vertical sections: -- Subchunk size: `16×16×16` -- Subchunk count: `CY / 16 = 16` - -Benefits: -- Block edits rebuild only 1–2 subchunks. -- Streaming can cull and upload smaller pieces. - -### 2.3 Rendering options -- Option A (recommended): draw per subchunk (opaque + transparent) - - Pros: simple, good rebuild granularity, good culling. - - Cons: more draw calls (up to 16 per chunk per pass). -- Option B: merge subchunk meshes into one chunk mesh (optional later). - ---- - -## 3) Mesh Data Model - -### 3.1 Passes -Maintain separate meshes: -- **Opaque mesh**: solid blocks, depth-write on -- **Transparent mesh**: water/glass/leaves if needed, depth-write off (typical) - -Do not mix opaque and transparent in the same mesh. - -### 3.2 Vertex format (minimal v1) -Per vertex: -- `vec3 position` -- `vec3 normal` (or packed normal) -- `vec2 uv` -Optional later: -- AO/light (packed u8), biome tint, etc. - -### 3.3 GPU resources -Per subchunk per pass: -- VBO + IBO (+ VAO) -- Or a single VBO with interleaved + glDrawElements - -Upload budget is managed elsewhere (see chunk streaming spec). - ---- - -## 4) Face Visibility (Culling) - -A face is visible if: -- The current block is renderable for this pass, and -- The neighbor block in that face direction is NOT occluding this pass. - -Definitions: -- `isOpaque(id)` — true for solid blocks -- `isTransparent(id)` — true for blocks rendered in transparent pass -- `occludesOpaque(neighbor)` — neighbor blocks that hide opaque faces (typically opaque blocks) -- `occludesTransparent(neighbor)` — neighbor blocks that hide transparent faces (often anything non-air, depends on your water/glass rules) - -### 4.1 Opaque pass visibility rule (recommended) -Emit face if: -- `isOpaque(cur) == true` -- `isOpaque(nei) == false` (treat air, water, etc. as non-opaque) - -### 4.2 Transparent pass visibility rule (simple v1) -Emit face if: -- `isTransparent(cur) == true` -- `nei` is air OR `nei` is not the same transparent “fluid group” - - For water: don’t render faces between adjacent water blocks. - - For glass: often don’t render internal glass-to-glass faces either. - ---- - -## 5) Neighbor Sampling (Chunk Borders) - -Meshing requires neighbor blocks for boundary faces: -- If neighbor chunk exists: sample real neighbor block. -- If neighbor chunk missing: treat neighbor as air, emit faces. - - When neighbor later loads, mark border subchunks dirty and remesh. - -### 5.1 Border invalidation rules -When a chunk at `(cx,cz)` loads or changes: -- It must notify its 4 neighbors (N/E/S/W) to remesh the touching border subchunks: - - Example: if east neighbor loads, current chunk’s `x=15` border subchunks become dirty. -- If you have vertical subchunks: only mark those overlapping the changed y-range. - ---- - -## 6) Greedy Meshing Overview - -Greedy meshing merges many 1×1 quads into fewer large rectangles. - -You run greedy meshing for each of the 3 axes: -- Faces perpendicular to X: ±X -- Faces perpendicular to Y: ±Y -- Faces perpendicular to Z: ±Z - -Greedy meshing operates on a 2D “mask” per slice boundary. - -### 6.1 Face Material Key -To merge, faces must match a key: -- `key = (blockId, faceDir, passType[, textureId])` -If texture differs per face, include faceDir or faceTextureId. - -If lighting/AO differs per vertex, merging may need to be limited (v1 can ignore). - ---- - -## 7) Per-Subchunk Meshing Procedure - -Given a subchunk: -- X range: `[0..15]` -- Z range: `[0..15]` -- Y range: `[y0..y0+15]` where `y0 = subchunkIndex * 16` - -For each pass (Opaque then Transparent): - -1. Clear mesh builders (CPU vertex/index arrays). -2. Run greedy for X faces (±X) for boundaries inside the subchunk and across borders. -3. Run greedy for Y faces (±Y). -4. Run greedy for Z faces (±Z). -5. Output CPU mesh buffers. -6. Queue GPU upload (main thread). - ---- - -## 8) Greedy Meshing Details (per axis) - -This section defines the exact masks and loops for each axis. - -### 8.1 Common concepts -- A “slice boundary” is between two adjacent blocks. -- For each boundary, build a 2D mask of faces to emit. -- Merge rectangles of identical face keys. - -Mask cells store either: -- Empty -- `FaceCell { key, direction }` - -### 8.2 Axis X (faces perpendicular to X) -For X boundaries, the 2D mask is over **(Y,Z)**. - -Loop: -- `xBoundary` in `[0..16]` (inclusive; boundaries count is 17) -- mask size: `H = 16` for Y within the subchunk, `W = 16` for Z - -At boundary `xBoundary`, for each `(y,z)` in the subchunk: -- `left = block(xBoundary - 1, y, z)` (if xBoundary==0 -> neighbor chunk west) -- `right = block(xBoundary, y, z)` (if xBoundary==16 -> neighbor chunk east) - -Decide faces: -- If `left` is renderable for pass and `right` occludes == false => emit **+X face** for `left` -- If `right` is renderable for pass and `left` occludes == false => emit **-X face** for `right` - -Store the chosen face (if any) in mask cell at (y,z). - -Then greedy-merge rectangles in the (Y,Z) mask. - -### 8.3 Axis Y (faces perpendicular to Y) -For Y boundaries, the 2D mask is over **(X,Z)**. - -Loop: -- `yBoundary` in `[y0..y0+16]` -- mask size: X=16, Z=16 - -At boundary `yBoundary`, for each `(x,z)`: -- `below = block(x, yBoundary - 1, z)` (if yBoundary==0 -> treat as solid bedrock or air per world rules) -- `above = block(x, yBoundary, z)` (if yBoundary==256 -> air) - -Decide faces: -- If `below` renderable and `above` not occluding => emit **+Y face** for `below` -- If `above` renderable and `below` not occluding => emit **-Y face** for `above` - -Greedy-merge rectangles in (X,Z). - -### 8.4 Axis Z (faces perpendicular to Z) -For Z boundaries, the 2D mask is over **(X,Y)**. - -Loop: -- `zBoundary` in `[0..16]` -- mask size: X=16, Y=16 (within subchunk) - -At boundary `zBoundary`, for each `(x,y)`: -- `back = block(x, y, zBoundary - 1)` (if zBoundary==0 -> neighbor chunk north) -- `front = block(x, y, zBoundary)` (if zBoundary==16 -> neighbor chunk south) - -Decide faces: -- If `back` renderable and `front` not occluding => emit **+Z face** for `back` -- If `front` renderable and `back` not occluding => emit **-Z face** for `front` - -Greedy-merge rectangles in (X,Y). - ---- - -## 9) Rectangle Merge Algorithm (Greedy Step) - -Given a 2D mask `mask[u][v]` with dimensions `U×V`: - -1. Scan cells in a fixed order (u then v). -2. When a non-empty cell is found at `(u0,v0)`: - - Let `k = mask[u0][v0].key`. -3. Find max width: - - `w` = largest such that for all `du in [0..w-1]`, `mask[u0+du][v0]` has key `k`. -4. Find max height: - - `h` = largest such that for all `dv in [0..h-1]` and all `du in [0..w-1]`, - `mask[u0+du][v0+dv]` has key `k`. -5. Emit one quad for the rectangle (size w×h). -6. Clear those cells to empty. -7. Continue scanning. - -Merging requirements: -- keys must match exactly, including direction and texture/material. - ---- - -## 10) Quad Emission Rules - -### 10.1 Vertex positions -Each rectangle produces one quad (4 vertices, 6 indices). - -You compute quad corners based on: -- axis (X/Y/Z) -- boundary coordinate (xBoundary, yBoundary, zBoundary) -- rectangle extents in the mask dimensions - -Example: for X faces, rectangle spans: -- y range: `[yStart .. yStart + h]` -- z range: `[zStart .. zStart + w]` -- x constant: `xBoundary` (for -X or +X depends on which block is emitting) - -### 10.2 Normals -- +X, -X, +Y, -Y, +Z, -Z are constant normals. - -### 10.3 UVs -Two common approaches: - -**Tiled UVs (recommended for block textures)** -- u spans `[0..w]`, v spans `[0..h]` -- In shader, sample atlas using block face texture + fractional part if you want repeats. - -**Atlas-per-face UVs** -- For each block face texture: - - base UV rect in atlas - - scale by w/h if repeating - - or keep fixed and accept stretching (not recommended) - -Pick one and ensure it is consistent across all faces. - ---- - -## 11) Dirty Flags & Remeshing - -### 11.1 When to mark a subchunk dirty -- Any block change within its y-range. -- Any block change in a neighboring chunk that touches one of its faces: - - x=0 or x=15 border - - z=0 or z=15 border -- For Y boundaries: - - if your world supports stacked chunks, handle vertical neighbors similarly. - -### 11.2 Remesh scheduling -- Dirty subchunks are queued for meshing. -- Queue priority can be based on distance to player. - -### 11.3 Cancelling / invalidating jobs -Use a `meshVersion` or `jobToken` per subchunk: -- Increment token when: - - the subchunk is dirtied again - - the subchunk is unloaded -- Worker jobs capture token; results are discarded if token mismatches. - ---- - -## 12) Performance Notes (for 16×256×16) - -- Reuse mask buffers to avoid allocations: - - For X and Z masks: 16×16 - - For Y masks: 16×16 -- Use compact keys (32-bit): - - `key = blockId | (faceDir<<16) | (pass<<20) | (texId<<22)` -- Separate opaque and transparent meshes to simplify ordering and reduce overdraw. -- For v1, greedy meshing on opaque is the biggest win. - - Transparent can be naive first, then greedy later. - ---- - -## 13) Acceptance Criteria - -- Adjacent solid blocks do not produce internal faces. -- A 2×2 flat area of visible identical faces produces **2 triangles** (one quad), not 8 triangles. -- Chunk borders render correctly: - - If neighbor missing: faces visible. - - When neighbor loads: border subchunks remesh and internal faces disappear. -- Editing one block only remeshes the affected subchunk(s), not the entire 256 height. -- Opaque and transparent geometry are not mixed in one draw call/mesh. - ---- - diff --git a/docs/render-stability-investigation.md b/docs/render-stability-investigation.md deleted file mode 100644 index c2470864..00000000 --- a/docs/render-stability-investigation.md +++ /dev/null @@ -1,116 +0,0 @@ -# render-stability-investigation.md -## Terrain Shimmering / Morphing at High Altitude & Large Render Distance - -This document is a **handoff spec for investigation and fixes** related to terrain appearing to *morph, shimmer, crawl, or lose smoothness* when flying high and increasing render distance. - -This is **not a worldgen logic bug**. It is almost certainly a **rendering precision + depth issue**, possibly compounded by meshing or shading choices. - -The goal is to **identify the exact cause(s)** and **implement industry-standard fixes** used by voxel engines (Minecraft, Minetest, etc.). - ---- - -## 1) Observed Symptoms - -- Terrain appears to subtly move or shimmer as the camera moves. -- Effect increases: - - with higher altitude - - with larger render distance / far plane -- Most visible on: - - large flat areas - - sloped terrain - - distant mountains -- Looks like “shader movement”, but geometry is static. - ---- - -## 2) Primary Root Causes (Ranked by Likelihood) - -### 2.1 Floating-Point Precision Loss (Very Likely) -**Problem** -- Rendering uses large absolute world-space coordinates. -- GPU uses 32-bit floats. -- Precision drops as values grow larger. -- Small vertex differences become unstable frame-to-frame. - -**Symptoms** -- Shimmering terrain -- “Crawling” edges -- Motion that looks like shader artifacts - -**Industry solution** -➡ **Floating Origin / Camera-relative rendering** - ---- - -### 2.2 Depth Buffer Precision Collapse (Very Likely) -**Problem** -- Large far plane (e.g. 20k–100k+ units) -- Standard depth buffer is non-linear -- Precision concentrated near camera -- Far geometry loses depth resolution - -**Symptoms** -- Z-fighting-like shimmer -- Surfaces flicker or lose smoothness -- Artifacts worsen as render distance increases - -**Industry solution** -➡ **Reverse-Z + floating-point depth buffer + sane near plane** - ---- - -### 2.3 Shader-side Noise or Continuous LOD (Possible) -**Problem** -- Terrain noise or displacement sampled in shaders -- Or continuous LOD morphing without snapping -- Small camera movements alter sampled values - -**Symptoms** -- Terrain shape subtly changes as camera moves -- Adjacent chunks disagree slightly - -**Rule** -➡ Terrain noise must be **CPU-only**, baked into meshes. - ---- - -### 2.4 Normal / Lighting Instability (Possible) -**Problem** -- Greedy meshing + averaged normals -- Or normals reconstructed in shader -- Interpolation causes lighting shifts - -**Symptoms** -- Brightness changes with camera movement -- Looks like surface “rippling” - -**Fix** -➡ Flat shading or strict per-face normals. - ---- - -### 2.5 Aggressive Frustum Culling (Lower probability) -**Problem** -- Precision errors near frustum edges -- Chunks popping in/out rapidly - -**Fix** -➡ Conservative chunk AABBs, chunk-level culling only. - ---- - -## 3) Mandatory Fixes to Implement - -### 3.1 Floating Origin (Required) - -**Rule** -- Never send large absolute world coordinates to the GPU. - -**Implementation** -- Keep camera near `(0,0,0)` -- All chunk/world positions are computed relative to camera - -**Example** -```cpp -vec3 relativePos = worldPos - cameraWorldPos; - diff --git a/docs/shadows.md b/docs/shadows.md deleted file mode 100644 index 84b9d112..00000000 --- a/docs/shadows.md +++ /dev/null @@ -1,283 +0,0 @@ -# shadows.md — Shadow System for Voxel Engine (OpenGL) - -This spec defines a practical shadow system for a voxel engine with: -- Sun (directional light) shadows -- Optional moon shadows (v2) -- Chunked world, large render distances -- Performance constraints typical of voxel terrain - -Primary approach: **Cascaded Shadow Maps (CSM)** for the sun. - ---- - -## 1) Goals - -- Stable sun shadows across large outdoor scenes. -- Reasonable performance with configurable quality. -- Minimal shimmering (“shadow swimming”) while camera moves. -- Works with chunk streaming and camera-relative rendering. - -Non-goals (v1): -- Perfect contact-hardening / soft shadows -- Ray-traced GI -- Voxel cone tracing - ---- - -## 2) Shadowing Approach - -### 2.1 Directional Light => Cascaded Shadow Maps (CSM) -Directional light (sun) requires shadowing over large distances. -CSM splits the camera frustum into multiple ranges (cascades), each with its own shadow map. - -Default: -- 3 cascades (good) -Optional: -- 4 cascades (better) - ---- - -## 3) Settings - -Expose these in graphics settings: - -- `shadows_enabled` (bool) -- `shadow_map_resolution` (1024 / 2048 / 4096) -- `shadow_cascades` (2 / 3 / 4) -- `shadow_distance` (e.g. 80m / 150m / 250m in world units) -- `shadow_bias` (float) -- `shadow_normal_bias` (float) -- `pcf_kernel` (1 / 2 / 3) (filter radius) -- `cascade_split_lambda` (0..1) (split distribution) - ---- - -## 4) Pipeline Overview - -Per frame: -1. Compute `sunDir` from time-of-day. -2. Compute camera frustum splits for cascades. -3. For each cascade: - - compute light-space ortho projection covering that frustum slice - - render shadow caster geometry into shadow map (depth-only) -4. Render main scene: - - sample correct cascade shadow map per fragment - - apply PCF filtering - - apply shadow factor to sun lighting term only - ---- - -## 5) Cascade Splits - -Let: -- camera near = `n` -- camera farShadow = `f` (shadow_distance, not camera far plane) -- cascades = `C` - -Compute split distances using a blend of: -- linear splits -- logarithmic splits - -Standard formula: -- `split_i = lerp( n + (f-n) * (i/C), - n * pow(f/n, i/C), - lambda )` -Where `lambda` controls distribution: -- 0.0 = linear -- 1.0 = logarithmic -Default: -- `lambda = 0.6` - -Store: -- `cascadeSplits[i]` in view-space depth - ---- - -## 6) Light-space Matrix for Each Cascade - -### 6.1 Compute Frustum Corners for Cascade Slice -- Take the 8 corners of the camera frustum slice between split_i and split_{i+1} -- Convert to world-space (camera-relative world, consistent with floating origin) - -### 6.2 Create Light View Matrix -Directional light view: -- `lightPos = cameraPos - sunDir * lightDistance` -- `lightView = lookAt(lightPos, cameraPos, worldUp)` -Note: position is arbitrary for directional lights, but needed for matrix. - -### 6.3 Fit Orthographic Projection -Transform frustum corners into light space. -Compute AABB bounds: -- `minX..maxX`, `minY..maxY`, `minZ..maxZ` -Build ortho projection: -- `lightOrtho = ortho(minX, maxX, minY, maxY, -maxZ - margin, -minZ + margin)` -(Ensure correct handedness conventions for your math lib.) - -### 6.4 Stabilize to Prevent Shadow Shimmer (Mandatory) -Shimmering occurs when the ortho projection “slides” with camera movement. - -Fix: **texel snapping** -- Compute world units per texel: - - `texelSizeX = (maxX - minX) / shadowRes` - - `texelSizeY = (maxY - minY) / shadowRes` -- Snap the ortho bounds (or light-space origin) to texel grid: - - `minX = floor(minX / texelSizeX) * texelSizeX` - - `minY = floor(minY / texelSizeY) * texelSizeY` - - recompute max from snapped min + extent -This makes shadows stable as camera moves. - ---- - -## 7) Shadow Map Rendering - -### 7.1 Depth-only Pass -For each cascade: -- bind shadow FBO with depth texture -- set viewport to shadow resolution -- clear depth -- render only shadow casters - -Use a minimal vertex shader that outputs `lightSpaceMatrix * worldPos`. -Fragment shader can be empty (depth only). - -### 7.2 What Geometry to Render -Render: -- Opaque chunk meshes only -- Do not render transparent blocks into shadow maps (v1) - -Optional v2: -- alpha-tested foliage (leaves) as caster (requires alpha test in shadow pass) - -### 7.3 Culling for Performance -For each cascade: -- render only chunks within shadow distance AND intersecting cascade frustum slice -- chunk-level culling is enough - ---- - -## 8) Sampling Shadows in Main Render - -### 8.1 Cascade Selection -In main fragment shader: -- Compute fragment view-space depth -- Select cascade index where depth < cascadeSplit[i] -- Use that cascade’s lightSpaceMatrix and depth texture - -### 8.2 Shadow Test -- Transform world position into light clip space -- Project to UV -- Sample shadow depth -- Compare with current depth (with bias) - -### 8.3 Bias (Fixes Shadow Acne) -Use slope-scaled bias: -- `bias = max(shadow_bias * (1 - dot(normal, lightDir)), shadow_min_bias)` -Plus optional normal offset: -- offset position along normal by `shadow_normal_bias` - -Expose both to settings. - -### 8.4 PCF Filtering (v1) -Use a small PCF kernel (3×3 or 5×5): -- Sample neighbor texels around UV -- Average comparisons -Configurable radius. - ---- - -## 9) Integration with Day/Night - -### 9.1 Sun Shadows -Only apply when `sunIntensity > threshold` (e.g. 0.05) -At night: -- skip shadow rendering entirely (big perf win) - -### 9.2 Moon Shadows (optional v2) -- Usually very subtle -- Could reuse same CSM pipeline at lower resolution -Not required for v1. - ---- - -## 10) Interaction with Floating Origin - -Rule: -- All world positions used in shadow matrices must be in the same coordinate space as the main render. -Recommended: -- Use **camera-relative** world positions for both: - - shadow caster rendering - - main scene rendering -This prevents precision issues. - ---- - -## 11) Debug Tools (Required) - -- Toggle: show cascade boundaries overlay -- Toggle: visualize shadow map depth for each cascade -- Toggle: freeze cascades (stability testing) -- Sliders: bias, normalBias, lambda, shadowDistance -- Display: current cascade index under crosshair - ---- - -## 12) Performance Targets - -Defaults: -- 3 cascades -- 2048 shadow maps -- render shadow pass only during day -- chunk-level culling per cascade - -Expected: -- Shadow pass cost proportional to visible chunks + cascades - ---- - -## 13) Known Issues & Fixes - -### 13.1 Shadow shimmering -Fix: -- texel snapping (mandatory) -- stable cascade splits (don’t change shadowDistance every frame) - -### 13.2 Peter panning (detached shadows) -Fix: -- reduce bias -- reduce normalBias -- increase resolution or improve PCF - -### 13.3 Shadow acne -Fix: -- increase bias or slope-scale bias -- ensure normals are correct (flat shading helps) - -### 13.4 Swimming with greedy meshing -Usually caused by unstable world coords: -- ensure floating origin + camera-relative rendering is applied - ---- - -## 14) Implementation Order - -1. Single shadow map (no cascades) to validate pipeline -2. Add cascade splits + multiple depth textures -3. Cascade selection in shader -4. Bias controls + PCF -5. Texel snapping stabilization -6. Chunk culling per cascade -7. Day-only rendering optimisation -8. Debug views and tuning UI - ---- - -## 15) Acceptance Criteria - -- Sun casts stable shadows during day. -- Shadows do not noticeably shimmer when camera moves. -- Bias is tunable; acne and peter panning can be balanced. -- Shadow rendering is skipped at night. -- Performance remains acceptable at target render distance. - ---- - diff --git a/docs/worldgen-luanti-style.md b/docs/worldgen-luanti-style.md deleted file mode 100644 index 2dd8e798..00000000 --- a/docs/worldgen-luanti-style.md +++ /dev/null @@ -1,278 +0,0 @@ -# worldgen-luanti-style.md — Revamp Worldgen to “Luanti/Minetest-like” Pipeline - -Objective: -Rebuild your worldgen pipeline so it behaves like Luanti’s mapgen approach: coherent large-scale terrain, clean surface layering, predictable chunk boundaries, and a clear separation between terrain shape, biomes, surface rules, caves, and decorations. - -This is specifically designed to fix your current problems: -- Artificial/predictable patterns -- Hard biome blobs and disconnected regions -- Too-wide, uniform sand bands around coasts -- Height discontinuities (sand higher than forest) -- Over-dramatic cliffs/walls and chaotic high terrain -- Features (trees) popping in unnatural patches - ---- - -## 1) Match Luanti’s Core Strategy: “Generate Bigger Than You Store” - -Luanti generates in a larger working volume (mapchunk) to keep features consistent across boundaries, then “commits” a subset. - -### Your equivalent (recommended) -Keep your existing storage chunk size: -- **Chunk storage**: 16 × 256 × 16 (X,Y,Z) - -But generate using a larger “gen region”: -- **GenRegion**: 80 × 256 × 80 (X,Z) = 5×5 chunks horizontally -- That’s the direct equivalent of Luanti’s 80×80×80 (but you’re full height, so 80×256×80) - -Why: -- Mountains, coastlines, caves, and biome transitions need neighborhood context. -- If you compute everything per chunk in isolation, you get seams, blobs, and “painted” transitions. - -### Implementation rule -When a chunk is needed: -- Determine its GenRegion origin (aligned to 5×5 chunk grid). -- Generate the entire GenRegion in one pass. -- Fill/cache the 25 chunks from that result. - ---- - -## 2) Luanti-Style Generation Pipeline (Phases) - -You must implement these phases in order, and keep them cleanly separated. - -### Phase A: Terrain Shape (Stone + Water Only) -Output: -- a solid/empty decision for every voxel (stone vs air) -- water filling under sea level -- **no dirt, no sand, no grass, no trees** - -Inputs: -- seed -- continuous fields (noise) - -Recommended approach: -- Use 2D fields for “macro shape”: - - continentalness (ocean/land) - - peaks / mountain mask - - erosion (ruggedness limiter) -- Optional 3D density for overhangs: - - density(x,y,z) threshold => stone/air - -Hard rule: -- Terrain shape is **biome-agnostic**. - -Deliverable: -- `stoneMask[x][y][z]` -- `heightmap[x][z]` (top solid y) -- `oceanMask[x][z]` (ocean vs inland classification) -- `slope[x][z]` (computed from heightmap) - -### Phase B: Biome Calculation (Climate Space) -Output: -- biome ownership per (x,z) column -- BUT as weights (top2/top3) not a single hard biome - -Inputs (computed in Phase A and from climate noise): -- temperature T(x,z) -- humidity H(x,z) -- continentalness C(x,z) -- elevation normalized E01(x,z) from heightmap -- ruggedness/erosion R(x,z) - -Rule: -- Determine `biomeA`, `biomeB`, blend `t` per column. - -Deliverable: -- `biomeAId[x][z]` -- `biomeBId[x][z]` -- `blendT[x][z]` - -### Phase C: Surface “Dusting” (Top/Filler Replacement) -Output: -- replace the top layers of stone with biome-appropriate layers: - - top node (1 block) - - filler (3–5 blocks) - - optional biome stone variants (sandstone, etc.) - -Inputs: -- heightmap + slope -- biome blend (A/B/t) -- sea level + ocean shoreline distance - -Hard rules: -- Surface rules **never change terrain height**. -- Beaches are not “biome = sand”; beaches are a shoreline rule (see §4). - -Deliverable: -- final terrain surface blocks (stone/dirt/grass/sand/etc.) -- still no trees/ores yet - -### Phase D: Caves / Caverns / Dungeons (Carving + Structures) -Output: -- carve stone into air using controlled cave logic -- optionally place dungeon rooms/halls later - -Inputs: -- stoneMask (pre-surface or post-surface depending on your approach) -- cave region mask -- 3D noise / worm tunnels -- surface protection depth - -Rule: -- Apply cave carving BEFORE final surface painting if you want correct cave mouths and dirt ceilings, OR carve after and then fix up ceilings—pick one and keep it consistent. - -Recommendation: -- Carve after Phase A, then recompute heightmap, then do Phase C dusting. - -Deliverable: -- carved volume -- updated heightmap - -### Phase E: Decorations and Ores (Deterministic Feature Placement) -Output: -- trees, plants, shrubs, boulders, ores - -Inputs: -- biome blend (A/B/t) -- slope and elevation constraints -- coastline buffers - -Hard rule: -- Feature placement must be deterministic per region and must obey spacing rules. -- Features should not define terrain shape. - -Deliverable: -- final blocks (including trees and ores) - ---- - -## 3) Data Structures and Caching (Required to Feel “Coherent”) - -### 3.1 Region cache -Maintain an LRU cache keyed by GenRegion coords: -- `GenRegionKey = (regionX, regionZ)` -- store: - - heightmap 80×80 - - slope 80×80 - - climate fields 80×80 (T/H/C/R/P/etc.) - - biome blend 80×80 (A/B/t) - - optionally stoneMask if memory allows (or regenerate in steps) - -### 3.2 Deterministic random -For features, never use global RNG state. Use: -- `hash(seed, worldX, worldZ, featureSalt)` as randomness source - -This prevents tree blobs that change depending on generation order. - ---- - -## 4) Coastlines (Stop the Sand Bands Permanently) - -Beaches must be handled in Phase C (surface rules) and must be conditional. - -Required rules: -- Beaches apply only: - - near sea level (0..6 blocks above sea) - - gentle slope only (<=2) - - ocean shore only (not lakes/rivers) - - variable width based on “exposure” -- Tree placement suppressed within a coastal band (6–18 blocks inland, varying by exposure) - -You already have `coastlines.md`. Integrate it strictly as: -- Phase C (surface) -- Phase E (tree suppression) - -If sand still forms huge bands, it means: -- you’re using “any water” instead of “ocean water” -- or you’re applying sand beyond the near-sea band -- or you’re letting deserts override shoreline logic globally - ---- - -## 5) Fix “Disconnected Pieces” (Height Discontinuities + Biome Blobs) - -These are always caused by mixing responsibilities. - -### 5.1 Height discontinuities (sand above forest) -Cause: -- biome or surface is altering height -Fix: -- Height is Phase A only. -- Biome terrain modifiers (if any) are tiny and blended, never hard-switched. - -### 5.2 Predictable blobs (big forest blob, big mountain blob) -Cause: -- single-noise classification -- no domain warp -- no multi-field selection -Fix: -- use climate space (T/H/C/E/R) -- return top2 + blend -- optional domain warp for the *inputs* (not for final biome id) - -### 5.3 Mountains as giant walls -Cause: -- ridged noise driving height directly -- missing erosion limiter -Fix: -- mountain mask (inland * peaks * (1-erosion)) -- capped mountain lift -- optional slope limiter on heightmap - ---- - -## 6) Concrete Implementation Plan (What to build next) - -### Step 1 — Add GenRegion generation (80×256×80) -- Align regions to 5×5 chunks. -- Generate and cache 25 chunks per region. - -### Step 2 — Refactor into Phase functions -Create strict functions: -- `phaseA_generateTerrainStoneWater(region)` -- `phaseB_computeBiomeBlend(region)` -- `phaseC_applySurfaceRules(region)` -- `phaseD_carveCaves(region)` -- `phaseE_placeFeatures(region)` - -### Step 3 — Recompute heightmap after carving -If caves can open to surface: -- carve first -- recompute heightmap -- then apply surface dusting - -### Step 4 — Add debugging overlays (mandatory) -You cannot tune without these: -- height grayscale -- slope heatmap -- ocean classification -- shoreline distance -- biome A/B/t visualization -- mountain mask visualization - ---- - -## 7) Success Criteria (“Now it feels like Luanti/Minecraft”) - -- No visible seams or discontinuities at chunk borders. -- Mountains form coherent ranges with foothills and calmer peaks. -- Forests taper naturally; no hard blobs. -- Beaches are narrow and varied; forests don’t touch sand. -- Sand is never “floating above” dirt/forest due to disconnected height rules. -- Different seeds produce strongly distinct worlds without obvious repetition. - ---- - -## 8) Notes on Matching Luanti Behavior in Your Constraints - -Luanti’s default generation unit is large (80³), and that is a major reason its worlds feel coherent. -Your vertical axis is fixed at 256, so your best equivalent is: -- **80×256×80 generation regions** with strict phase separation. - -This alone will remove a huge amount of “artificial / painted” look because your algorithms stop fighting chunk boundaries and stop fighting each other. - ---- - -End of spec. - diff --git a/docs/worldgen-revamp.md b/docs/worldgen-revamp.md deleted file mode 100644 index b526a343..00000000 --- a/docs/worldgen-revamp.md +++ /dev/null @@ -1,377 +0,0 @@ -````md -# worldgen-revamp.md — Minecraft/Minetest-Quality Worldgen Revamp Spec - -This spec is a full revamp plan to move from “procedural paint / artificial blobs” to a layered, stable, believable worldgen pipeline closer to Minecraft (1.18+) / Minetest quality. - -It targets the current issues: -- Coastlines: too much sand, uniform bands, forests touching beaches -- Terrain: abrupt height jumps, “walls”, overly dramatic slopes -- Biomes: hard blobs, sharp boundaries, predictable patterns -- Consistency: sand sometimes above forest, mismatched height/surface rules -- Overall feel: disconnected systems that don’t blend - ---- - -## 0) Design Principle: Separate the Layers (Hard Rule) - -We enforce a strict pipeline separation: - -1) **Terrain (world shape)** - - continents/oceans, mountains, valleys, base height, cliffs, caves density - - NO biome block painting - - NO vegetation - -2) **Climate + Biome selection** - - temperature/humidity/etc. => biome weights (not single biome) - - biome selection does NOT define terrain height; only small *blended* modifiers - -3) **Surface rules** - - decide top/filler blocks using (biome weights + slope + sea proximity + masks) - - beaches and cliff shores belong here - -4) **Features** - - trees, plants, boulders, ores, structures - - placed deterministically after surface is set - - obey coast buffers and transition bands - -If any layer does another layer’s job, you get exactly the artifacts you’re seeing. - ---- - -## 1) Target Output Qualities (Acceptance Targets) - -### 1.1 Coastline targets -- Typical beach width: 2–5 blocks -- Wide beaches: 6–10 blocks only in exposed zones -- Steep coasts: 0–2 blocks of sand (cliff/rock meets sea) -- Forest tree line begins: 8–20 blocks inland (biome dependent) - -### 1.2 Terrain targets -- No long near-vertical walls unless explicitly a “cliff biome” / special feature -- High elevation silhouettes are calm/broad; micro-noise reduced at peaks -- Height transitions between regions are continuous (no plateaus “pasted on”) - -### 1.3 Biome targets -- Biomes form large readable regions, but borders are blended -- Transition zones exist (forest→plains→beach, desert→savanna→plains) -- Vegetation density ramps; no instant 0→100% jumps - ---- - -## 2) Proposed New Architecture - -### 2.1 Data produced per (x,z) column -Compute these once and reuse: -- `continentalness C` (0..1) : ocean→inland -- `peaks P` (0..1) : mountain-likelihood mask (ridged recommended) -- `erosion E` (0..1) : ruggedness limiter -- `weirdness W` (0..1) : variation / ridge-vs-valley signal -- `temperature T` (0..1) -- `humidity H` (0..1) - -Optional: -- `exposure X` (0..1) : coastline beach width variation -- `riverMask Rm` later - -### 2.2 Data produced per (x,y,z) -- `density(x,y,z)` for caves/overhangs (v2; keep separate) - ---- - -## 3) Terrain Generator Revamp (World Shape Only) - -### 3.1 Base height from continentalness -Use continentalness to drive a smooth ocean→land curve: -- deep ocean basin -- continental shelf -- coastal rise -- inland plateau - -Example conceptual mapping (tune): -- `C < 0.35` => deep ocean -- `0.35..0.45` => shallow ocean -- `0.45..0.55` => coast band -- `> 0.55` => inland - -Height should be continuous through these bands. - -### 3.2 Mountain system = mask + capped lift (fixes “walls”) -Do NOT do “ridgedNoise * hugeAmp directly into height”. - -Instead: -1) Compute mountain mask: -```text -inland = smoothstep(0.48, 0.70, C) -peakMask = smoothstep(0.60, 0.90, P) -ruggedMask = 1.0 - smoothstep(0.45, 0.85, E) -mountMask = inland * peakMask * ruggedMask -```` - -2. Compute mountain lift (use smooth noise, then cap): - -```text -liftNoise = fbm2(seed+LIFT, x*sL, z*sL) -> [0..1] -mountLiftRaw = mountMask * liftNoise * mountAmp -mountLift = mountLiftRaw / (1 + mountLiftRaw / mountCap) -``` - -This prevents runaway cliffs and creates ranges, not walls. - -### 3.3 Elevation-dependent detail attenuation (fixes “busy peaks”) - -Detail noise must fade with elevation: - -```text -elev01 = clamp01((height - seaLevel) / highlandRange) -detailAtten = 1 - smoothstep(0.3, 0.85, elev01) -height += detailNoise * detailAmp * detailAtten -``` - -### 3.4 Slope limiter (optional but very effective) - -After generating a local heightmap (chunk + border), run 3–6 relaxation passes: - -* enforce `maxDelta` between neighbors (suggest 2) - This kills giant vertical sheets while preserving mountains. - ---- - -## 4) Climate → Biomes (Fixes blobs, predictability) - -### 4.1 Biome selection returns weights, not a single biome - -For each (x,z), compute scores for all biomes in climate space: - -* temperature -* humidity -* continentalness -* erosion/ruggedness -* elevation band - -Pick top 2 (optionally 3): - -* `biomeA`, `biomeB` -* `blend t = scoreA / (scoreA + scoreB)` - -### 4.2 Blend everything using `t` - -This is non-negotiable: - -* surface blocks -* vegetation density -* color tints -* *small* terrain modifiers (never large plateaus) - -This is how you avoid “big blobs” and hard borders. - -### 4.3 Add transition micro-biomes - -For harsh pairs, define explicit transitions: - -* Desert ↔ Forest => Savanna / Dry Plains -* Forest ↔ Swamp => Marsh -* Plains ↔ Mountains => Foothills - Use these only near 50/50 blends. - ---- - -## 5) Surface Rules (Fixes sand bands + sand above trees) - -Surface rules are a separate step that takes: - -* final terrain height `h` -* slope `slope(x,z)` -* sea proximity -* biome weights - -### 5.1 Compute slope for surface rules - -Use max neighbor delta of heightmap. - -### 5.2 Coastlines: beaches are conditional (ocean-only + gentle slope) - -Use the `coastlines.md` approach: - -* distinguish ocean water via continentalness -* compute `shoreDistOcean` -* compute `beachWidth` from exposure + slope -* only place sand where: - - * near sea level (0..6 above sea) - * gentle slope (<=2) - * within beach width - * ocean-only (not lakes) - -### 5.3 Coastal transition band (prevents forest touching sand) - -In vegetation pass: - -* suppress trees for 6–18 blocks inland (varies by exposure) - Optionally replace forest with CoastalPlains micro-biome for that band. - -### 5.4 Prevent “sand above trees” - -Rule: beach sand must never be applied outside the near-sea band. - -* sand inland is controlled by desert biome, not “near water”. - -Additionally: - -* surface rules must not alter height. - If you have any “raise/lower for biome” logic, remove or blend and keep amplitude small. - ---- - -## 6) Features (Trees, Plants) (Fixes tree blobs and harsh edges) - -### 6.1 Use density fields, not binary placement - -For each column compute: - -* `treeDensity = lerp(densityB, densityA, t)` - Then place trees using probability based on density. - -This creates natural tapering. - -### 6.2 Add spacing rules - -Use a deterministic hash + spacing radius: - -* avoid trees every 1–2 blocks -* enforce minimum distance between trunks - -### 6.3 Biome-aware coastal suppression - -If `shoreDistOcean <= noTreeDist`: - -* set `treeDensity = 0` -* allow shrubs/grass - ---- - -## 7) Debugging: Add the “Minecraft tools” you’re missing - -You cannot tune worldgen without visibility. - -Required debug views: - -* show height as grayscale -* show slope heatmap -* show continentalness -* show mountain mask -* show temperature/humidity -* show biome weights (A/B with blend value) -* show shoreline distance + beach eligibility - -Also: - -* print under cursor: - - * h, slope, C, P, E, biomeA/B, t, shoreDistOcean, beachWidth - -These debug tools are a major reason Minecraft-like pipelines converge fast. - ---- - -## 8) Implementation Plan (Incremental, No Rewrite Cliff) - -### Phase 1 — Stabilize terrain - -1. Refactor: isolate “terrain height function” (no blocks/biomes inside). -2. Implement mountain mask + capped lift. -3. Add elevation-dependent detail attenuation. -4. Add optional slope limiter. - -Exit criteria: - -* no vertical sheets/walls -* highlands feel calmer - -### Phase 2 — Fix biomes (no blobs) - -1. Implement biome weights (top2 + blend t). -2. Blend surface blocks (probabilistic). -3. Blend vegetation density. - -Exit criteria: - -* no hard circular islands -* transitions feel gradual - -### Phase 3 — Fix coastlines (sand problem) - -1. Implement ocean-only shoreline distance. -2. Implement slope+sea-level constrained beaches. -3. Add coastal no-tree band. - -Exit criteria: - -* beaches 2–5 blocks typical -* forests no longer touch sand - -### Phase 4 — Polish - -1. Add transition micro-biomes (savanna, foothills, marsh). -2. Improve vegetation spacing and variety. -3. Tune constants using debug views. - -Exit criteria: - -* “looks believable at distance” -* “looks cohesive on the ground” - ---- - -## 9) Known Failure Modes & Direct Fixes - -### Massive sand bands - -Cause: - -* beach rule uses any water; no slope/sea constraints - Fix: -* ocean-only + slope + sea band + variable width - -### Sand above trees / height discontinuities - -Cause: - -* biome is modifying height or surface logic is inconsistent - Fix: -* terrain height independent; biome modifiers blended and small; surface rules don’t change height - -### Big blobs of forest / big blobs of mountain - -Cause: - -* single-noise biome classification; no blending; mountain mask too broad - Fix: -* climate-space selection + blend; mountain mask inland+peaks+erosion - -### Artificial predictability - -Cause: - -* too few independent fields; same noise scale reused everywhere - Fix: -* separate scales for C/P/E/T/H/exposure; add domain warp sparingly - ---- - -## 10) Acceptance Criteria (Final) - -* Coastlines look natural; beaches narrow and variable. -* Forests transition through a coastal band; no “forest meets sand”. -* Mountains appear as ranges, not walls; peaks are calmer. -* Biomes blend and taper; no hard blobs. -* Terrain, biomes, surface, and features feel cohesive and connected. -* New biomes can be added by data/config, not code rewrites. - ---- - -End of spec. - -``` -::contentReference[oaicite:0]{index=0} -``` - diff --git a/docs/worldgen-spec2.md b/docs/worldgen-spec2.md deleted file mode 100644 index 30bb849f..00000000 --- a/docs/worldgen-spec2.md +++ /dev/null @@ -1,272 +0,0 @@ -This spec replaces the earlier heightmap-only approach with a **layered noise stack** closer in spirit to modern Minecraft-style generation: multiple large-scale fields (continentalness, erosion, peaks/valleys) plus climate-driven biome placement, separate ocean shaping, and controlled 3D carving to avoid “too many holes”. - -It does **not** claim Mojang’s exact implementation (that changes over versions and is complex), but it **does** mirror the key ideas Minecraft exposes via its multi-noise biome parameters and noise settings pipeline. :contentReference[oaicite:0]{index=0} - ---- - -## 0) The real problem you’re seeing (and the fixes) - -### Symptoms -- “Worlds look samey” → too few distinct low-frequency controls; no domain warping; biome transitions too uniform. -- “Oceans too flat / fake” → using one height function for everything; seabed not varied; coastlines too smooth. -- “Too many holes” → 3D density threshold carving without constraints; caves breaking the surface too often; no cave masking near surface. - -### Fixes (high level) -1. Use **separate fields** for continents vs mountains vs erosion (not just one fBm height). -2. Use **domain warping** so patterns aren’t obviously “noise bands”. -3. Give oceans their own treatment: **coastline shaping + seabed noise**, not “sea level clamp”. -4. Make caves controlled: **cave mask** + **surface protection** + **rarity**. - ---- - -## 1) Determinism & Seeds - -- Accept `seed_string` or `seed_u64`. -- Convert string → u64 using stable hash (FNV-1a 64-bit is fine). -- Use deterministic PRNG (SplitMix64/PCG32). -- All noise samplers are seeded from `(seed_u64, salt)`. - ---- - -## 2) Chunk Inputs/Outputs - -- Terrain is defined per (x,z) column + 3D density for caves. -- Chunk generation outputs: - - block IDs - - biome ID (per column, or per 4×4 cell like MC-style) - - optional: heightmap cache - ---- - -## 3) Noise types to implement - -### 3.1 Primary noise (recommended) -- **OpenSimplex2** (2D + 3D) or classic Perlin/Simplex. -- Build **fBm** (octaves), **ridged** variant, and **domain warp** utility. - -### 3.2 Why this matches Minecraft/Minetest style -- Modern Minecraft uses multiple “multi-noise” parameters for biome decisions (temperature, humidity, continentalness, erosion, weirdness, etc.). :contentReference[oaicite:1]{index=1} -- Noise settings are configurable in datapacks; these parameters primarily drive biome placement and tie into terrain/aquifer logic in that pipeline. :contentReference[oaicite:2]{index=2} -- Minetest mapgen v7 uses a combination of 2D and 3D Perlin noise and is notable for large rivers and cave differences (useful inspiration for “less flat” water + controlled caves). :contentReference[oaicite:3]{index=3} - ---- - -## 4) Core 2D Fields (computed per column) - -All fields are sampled in **world-space** with domain warping applied first. - -### 4.1 Domain warping (anti-samey) -Compute a warp offset from low-frequency noise: -- `warp = vec2( noise2(seed+W0, x*sW, z*sW), noise2(seed+W1, x*sW, z*sW) ) * warpAmp` -- Use warped coords for subsequent sampling: -- `Xw = x + warp.x`, `Zw = z + warp.y` - -Suggested: -- `sW = 1/900` to `1/1400` -- `warpAmp = 30` to `80` blocks - -### 4.2 Continentalness C (landmass) -Purpose: big continents + ocean basins. -- `C = fbm2(seed+C0, Xw*sC, Zw*sC, oct=4)` -- Normalize to [0..1]. - -Suggested: -- `sC = 1/2200` to `1/3200` -- thresholds: - - `C < 0.35` deep ocean - - `0.35..0.46` coast / shelf - - `> 0.46` land - -### 4.3 Erosion E (cliffs vs rolling) -Purpose: places where terrain should be “sharper” vs “soft”. -- `E = fbm2(seed+E0, Xw*sE, Zw*sE, oct=4)` → [0..1] - -Suggested: -- `sE = 1/900` to `1/1400` - -Interpretation: -- low E → sharp, rugged, cliff-prone -- high E → smooth hills/plains - -### 4.4 Peaks & Valleys / Weirdness P (mountain rhythm) -Purpose: repeated large-scale mountain range rhythm but warped. -- Use ridged noise: - - `P = ridged2(seed+P0, Xw*sP, Zw*sP, oct=5)` → [0..1] - -Suggested: -- `sP = 1/700` to `1/1100` - -### 4.5 Climate: Temperature T and Humidity H -Purpose: biome variety independent of elevation bands. -- `T = fbm2(seed+T0, Xw*sT, Zw*sT, oct=3)` → [0..1] -- `H = fbm2(seed+H0, Xw*sH, Zw*sH, oct=3)` → [0..1] - -Suggested: -- `sT = 1/4000` to `1/6000` -- `sH = 1/3000` to `1/5000` - -Altitude adjustment: -- `T_adj = clamp01(T - (height / 512.0)*tempLapse)` -- `tempLapse = 0.20..0.35` - ---- - -## 5) Height Function (less flat, more structure) - -Let: -- `SEA = 64` - -### 5.1 Base land height from continentalness -Map C to a base elevation: -- `land = smoothstep(0.35, 0.75, C)` -- `baseHeight = lerp(SEA - 55, SEA + 70, land)` - -This creates: -- deep oceans -- broad continental plates -- varied inland elevation - -### 5.2 Mountains from Peaks/Valleys + low erosion -Mountains should occur where: -- peaks are high (P) AND erosion is low (rugged zones) - -Define mountain mask: -- `mMask = smoothstep(0.55, 0.85, P) * (1.0 - smoothstep(0.45, 0.80, E))` - -Mountain lift: -- `mount = pow(mMask, 1.7) * mountAmp` -- `mountAmp = 60..170` - -### 5.3 Hills / local detail -Add smaller variation: -- `detail = fbm2(seed+D0, Xw*sD, Zw*sD, oct=5) * detailAmp` -- `sD = 1/180..1/260` -- `detailAmp = 6..18` - -### 5.4 Final surface height (pre carving) -- `h0 = baseHeight + mount + detail` - -### 5.5 Cliff shaping (reduces “rounded noise blobs”) -Compute slope from sampled heights (or gradient of a noise field): -- `slope = max(|h0(x+1)-h0(x)|, |h0(z+1)-h0(z)|)` -Cliff factor: -- `cliff = smoothstep(3, 10, slope) * (1.0 - E)` -Apply: -- reduce topsoil thickness when `cliff` high -- optionally snap/terrace heights slightly in cliff regions: - - `h = mix(h0, round(h0 / step) * step, cliff * terraceStrength)` - - `step=3..6`, `terraceStrength=0.2..0.5` - ---- - -## 6) Oceans that don’t look fake - -### 6.1 Coastline roughness (prevents perfect curves) -Use a dedicated coastal noise: -- `coastJitter = fbm2(seed+OJ0, Xw*sOJ, Zw*sOJ, oct=3) * 0.05` -- Apply to the “ocean threshold”: - - effectively shift `C` by jitter near coasts -This makes shorelines irregular. - -Suggested: -- `sOJ = 1/500..1/800` - -### 6.2 Seabed / ocean floor variation -If column is ocean (final height below SEA): -- seabed height: - - `seabed = SEA - 18 - deepFactor(C)*35 + fbm2(seed+OF0, Xw*sOF, Zw*sOF, oct=5)*seabedAmp` - - `sOF=1/220..1/360`, `seabedAmp=3..10` -Where `deepFactor(C)` increases as C decreases (deep ocean basins). - -### 6.3 Waves are NOT geometry -Do not try to add “wave noise” to water surface blocks. -Keep water plane flat at SEA; make the seabed interesting. - ---- - -## 7) Rivers and Lakes (fewer “random holes”, more readable water) - -### 7.1 River mask (2D) -Use a ridged or “valley” field: -- `R = ridged2(seed+R0, Xw*sR, Zw*sR, oct=4)` → [0..1] -Rivers occur where ridges are LOW (valley lines). Convert: -- `river = 1.0 - R` -- `riverMask = smoothstep(riverMin, riverMax, river)` -Suggested: -- `sR=1/900..1/1500` -- `riverMin=0.72`, `riverMax=0.86` - -### 7.2 Carve rivers into terrain -Let `riverDepth = riverMask * riverDepthMax` -- `riverDepthMax = 6..18` -Carve: -- `h = min(h, h0 - riverDepth)` -Fill with water if `h < SEA-1`. - ---- - -## 8) Biomes (Minecraft-like multi-noise selection concept) - -Use (T_adj, H, C, E, P, altitude) to choose biome. -Minecraft exposes these types of parameters to place biomes in a “multi-noise” space. :contentReference[oaicite:4]{index=4} - -### 8.1 Biome set (v1) -- Deep Ocean, Ocean, Beach -- Plains, Forest -- Taiga (cold forest) -- Desert -- Snow/Tundra -- Mountains (high elevation + rugged) - -### 8.2 Simple decision approach (works well) -1. If `C < 0.35` → Deep Ocean -2. Else if `C < 0.46` and `abs(h-SEA) < 4` → Beach -3. Else land: - - if `altitude > SEA+95` or `mMask > 0.6` → Mountains (snow if cold) - - else pick by T/H: - - hot + dry → Desert - - temperate + humid → Forest - - temperate + dry → Plains - - cold → Taiga / Snow - ---- - -## 9) Materials & Surface Layers - -### 9.1 Top/filler logic -- Determine `topBlock` by biome. -- `fillerDepth` varies by erosion and detail: - - `fillerDepth = 3 + floor(fbm2(seed+FD0, Xw*sFD, Zw*sFD, oct=2) * 2)` -- On cliffs (`cliff > 0.6`) reduce filler to 0–1 and expose stone. - -### 9.2 Ocean floor materials -- Shallow: sand + gravel patches -- Deep: gravel + clay/silt (if you have it) - ---- - -## 10) Caves without “too many holes” - -If your current caves are “too holey”, it’s usually because: -- density threshold is too permissive -- caves are allowed near the surface -- cave noise frequency is too high -- no rarity gating - -### 10.1 Cave mask (rare + deeper) -Make a 2D cave “probability mask”: -- `Cave2 = fbm2(seed+CV2, Xw*sCV2, Zw*sCV2, oct=3)` → [0..1] -- `caveAllowed = smoothstep(0.58, 0.80, Cave2)` -This makes caves appear in regions, not everywhere. - -Suggested: -- `sCV2=1/900..1/1500` - -### 10.2 3D density field (carving) -Compute density: -- `n = fbm3(seed+CV3, x*sCV3, y*sY, z*sCV3, oct=4)` -- Add vertical bias so caves prefer certain bands: - - `band = smoothstep(12, 60, y) * (1.0 - smoothstep(120, 180, y))` -- Final carve condition: - - if `caveAllowed > 0` AND `band > 0` AN - From 6c39091beaa42e66e231c02be68d59bc171a5bb1 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Wed, 24 Dec 2025 02:27:32 +0000 Subject: [PATCH 2/2] Fix compilation error: update VulkanBuffer initializers in createRHI --- src/engine/graphics/rhi_vulkan.zig | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index 031fdded..334540e3 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -81,6 +81,7 @@ const VulkanBuffer = struct { buffer: c.VkBuffer, memory: c.VkDeviceMemory, size: c.VkDeviceSize, + is_host_visible: bool, }; /// Vulkan texture with image, view, and sampler. @@ -271,11 +272,12 @@ fn createVulkanBuffer(ctx: *VulkanContext, size: usize, usage: c.VkBufferUsageFl // Existing code ignored errors here mostly. Ideally we check result. if (c.vkAllocateMemory(ctx.device, &alloc_info, null, &memory) != c.VK_SUCCESS) { c.vkDestroyBuffer(ctx.device, buffer, null); - return .{ .buffer = null, .memory = null, .size = 0 }; + return .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; } _ = c.vkBindBufferMemory(ctx.device, buffer, memory, 0); - return .{ .buffer = buffer, .memory = memory, .size = mem_reqs.size }; + const is_host_visible = (properties & c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0; + return .{ .buffer = buffer, .memory = memory, .size = mem_reqs.size, .is_host_visible = is_host_visible }; } fn init(ctx_ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!void { @@ -1480,12 +1482,14 @@ fn uploadBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, data: []const u8) if (buf_opt) |buf| { // Try mapping directly first (for HOST_VISIBLE buffers like UBOs) - var map_ptr: ?*anyopaque = null; - const result = c.vkMapMemory(ctx.device, buf.memory, 0, @intCast(data.len), 0, &map_ptr); - if (result == c.VK_SUCCESS) { - @memcpy(@as([*]u8, @ptrCast(map_ptr))[0..data.len], data); - c.vkUnmapMemory(ctx.device, buf.memory); - return; + if (buf.is_host_visible) { + var map_ptr: ?*anyopaque = null; + const result = c.vkMapMemory(ctx.device, buf.memory, 0, @intCast(data.len), 0, &map_ptr); + if (result == c.VK_SUCCESS) { + @memcpy(@as([*]u8, @ptrCast(map_ptr))[0..data.len], data); + c.vkUnmapMemory(ctx.device, buf.memory); + return; + } } // If mapping failed, assume DEVICE_LOCAL and use staging buffer @@ -1501,6 +1505,7 @@ fn uploadBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, data: []const u8) } // Copy to staging + var map_ptr: ?*anyopaque = null; if (c.vkMapMemory(ctx.device, staging.memory, 0, @intCast(data.len), 0, &map_ptr) == c.VK_SUCCESS) { @memcpy(@as([*]u8, @ptrCast(map_ptr))[0..data.len], data); c.vkUnmapMemory(ctx.device, staging.memory); @@ -2920,12 +2925,12 @@ pub fn createRHI(allocator: std.mem.Allocator, window: *c.SDL_Window) !rhi.RHI { ctx.image_available_semaphores[i] = null; ctx.render_finished_semaphores[i] = null; ctx.in_flight_fences[i] = null; - ctx.global_ubos[i] = .{ .buffer = null, .memory = null, .size = 0 }; - ctx.shadow_ubos[i] = .{ .buffer = null, .memory = null, .size = 0 }; - ctx.ui_vbos[i] = .{ .buffer = null, .memory = null, .size = 0 }; + ctx.global_ubos[i] = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; + ctx.shadow_ubos[i] = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; + ctx.ui_vbos[i] = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; ctx.descriptor_sets[i] = null; } - ctx.model_ubo = .{ .buffer = null, .memory = null, .size = 0 }; + ctx.model_ubo = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; return rhi.RHI{ .ptr = ctx,