diff options
| -rw-r--r-- | PROJECT_CONTEXT.md | 1 | ||||
| -rw-r--r-- | TODO.md | 7 | ||||
| -rw-r--r-- | assets/final/shaders/common_uniforms.wgsl | 3 | ||||
| -rw-r--r-- | assets/final/shaders/skybox.wgsl | 22 | ||||
| -rw-r--r-- | src/3d/renderer.cc | 19 | ||||
| -rw-r--r-- | src/3d/renderer.h | 1 | ||||
| -rw-r--r-- | src/procedural/generator.cc | 36 | ||||
| -rw-r--r-- | src/tests/test_3d_render.cc | 7 |
8 files changed, 55 insertions, 41 deletions
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index cbaeb9b..af87827 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -28,6 +28,7 @@ Style: ## Project Roadmap ### Recently Completed +- **Skybox & Two-pass Rendering Stability**: Resolved "black screen" and validation errors by implementing a robust two-pass rendering architecture (Pass 1: Skybox/Clear, Pass 2: Scene Objects). Implemented a rotating skybox using world-space ray unprojection (`inv_view_proj`) and a multi-octave procedural noise generator. - **Task #20: Platform & Code Hygiene**: Consolidated platform-specific shims and WebGPU headers into `platform.h`. Refactored `platform_init` and `platform_poll` for better abstraction. Removed STL containers from initial hot paths (`AssetManager`, `procedural`). Full STL removal for CRT replacement is deferred to the final optimization phase. - **Task #26: Shader Asset Testing & Validation**: Developed comprehensive tests for `ShaderComposer` and WGSL asset loading/composition. Added a shader validation test to ensure production assets are valid. - **Asset Pipeline Improvement**: Created a robust `gen_spectrograms.sh` script to automate the conversion of `.wav` and `.aif` files to `.spec` format, replacing the old, fragile script. Added 13 new drum and bass samples to the project. @@ -2,6 +2,13 @@ This file tracks prioritized tasks with detailed attack plans. +## Recently Completed (February 4, 2026) +- [x] **Skybox & Two-pass Rendering Stability**: + - [x] **Fixed Two-pass Rendering:** Implemented mandatory clear operations for color and depth when the skybox is absent, preventing black screens and depth validation errors. + - [x] **Implemented Rotating Skybox:** Added `inv_view_proj` to `GlobalUniforms` and updated the skybox shader to perform world-space ray unprojection, enabling correct rotation with the camera. + - [x] **Enhanced Procedural Noise:** Implemented a multi-octave Value Noise generator for higher-quality skybox textures. + - [x] **Scene Integrity:** Restored proper object indexing and removed redundant geometry, ensuring the floor grid and objects render correctly. + ## Recently Completed (February 3, 2026) - [x] **Task #20: Platform & Code Hygiene**: - [x] **Header Consolidation:** Moved all `#ifdef` logic for WebGPU headers and platform-specific shims into `src/platform.h`. diff --git a/assets/final/shaders/common_uniforms.wgsl b/assets/final/shaders/common_uniforms.wgsl index cefa3b2..4b5cf33 100644 --- a/assets/final/shaders/common_uniforms.wgsl +++ b/assets/final/shaders/common_uniforms.wgsl @@ -1,5 +1,6 @@ struct GlobalUniforms { view_proj: mat4x4<f32>, + inv_view_proj: mat4x4<f32>, camera_pos_time: vec4<f32>, params: vec4<f32>, resolution: vec2<f32>, @@ -12,4 +13,4 @@ struct ObjectData { }; struct ObjectsBuffer { objects: array<ObjectData>, -}; +};
\ No newline at end of file diff --git a/assets/final/shaders/skybox.wgsl b/assets/final/shaders/skybox.wgsl index 8347a5a..6becc1a 100644 --- a/assets/final/shaders/skybox.wgsl +++ b/assets/final/shaders/skybox.wgsl @@ -22,6 +22,22 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { } @fragment -fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> { - return textureSample(sky_tex, sky_sampler, frag_pos.xy / globals.resolution); -} +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + // Convert UV to NDC + let ndc_x = in.uv.x * 2.0 - 1.0; + let ndc_y = (1.0 - in.uv.y) * 2.0 - 1.0; // Un-flip Y for NDC (Y-up) + + // Unproject to find world direction + // We want the direction from camera to the far plane at this pixel + let clip_pos = vec4<f32>(ndc_x, ndc_y, 1.0, 1.0); + let world_pos_h = globals.inv_view_proj * clip_pos; + let world_pos = world_pos_h.xyz / world_pos_h.w; + + let ray_dir = normalize(world_pos - globals.camera_pos_time.xyz); + + // Spherical Mapping + let u = atan2(ray_dir.z, ray_dir.x) / 6.28318 + 0.5; + let v = asin(clamp(ray_dir.y, -1.0, 1.0)) / 3.14159 + 0.5; + + return textureSample(sky_tex, sky_sampler, vec2<f32>(u, v)); +}
\ No newline at end of file diff --git a/src/3d/renderer.cc b/src/3d/renderer.cc index 45caf14..778509f 100644 --- a/src/3d/renderer.cc +++ b/src/3d/renderer.cc @@ -111,7 +111,12 @@ void Renderer3D::create_skybox_pipeline() { desc.primitive.frontFace = WGPUFrontFace_CCW; desc.multisample.count = 1; desc.multisample.mask = 0xFFFFFFFF; - desc.depthStencil = nullptr; // Explicitly no depth for skybox + + WGPUDepthStencilState depth_stencil = {}; + depth_stencil.format = WGPUTextureFormat_Depth24Plus; + depth_stencil.depthWriteEnabled = WGPUOptionalBool_False; + depth_stencil.depthCompare = WGPUCompareFunction_Always; + desc.depthStencil = &depth_stencil; skybox_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &desc); wgpuBindGroupLayoutRelease(bgl); @@ -295,6 +300,7 @@ void Renderer3D::update_uniforms(const Scene& scene, const Camera& camera, float time) { GlobalUniforms globals; globals.view_proj = camera.get_projection_matrix() * camera.get_view_matrix(); + globals.inv_view_proj = globals.view_proj.inverse(); globals.camera_pos_time = vec4(camera.position.x, camera.position.y, camera.position.z, time); globals.params = @@ -416,6 +422,7 @@ void Renderer3D::render(const Scene& scene, const Camera& camera, float time, WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device_, nullptr); // --- Pass 1: Render Skybox (Clears color, clears and stores depth) --- + bool skybox_rendered = false; if (sky_texture_view_ && skybox_pipeline_) { WGPURenderPassColorAttachment sky_color_attachment = {}; gpu_init_color_attachment(sky_color_attachment, target_view); @@ -464,20 +471,24 @@ void Renderer3D::render(const Scene& scene, const Camera& camera, float time, wgpuRenderPassEncoderDraw(sky_pass, 3, 1, 0, 0); // Draw a full-screen quad wgpuRenderPassEncoderEnd(sky_pass); wgpuRenderPassEncoderRelease(sky_pass); + skybox_rendered = true; } // --- Pass 2: Render Scene Objects (Loads depth, writes depth) --- WGPURenderPassColorAttachment obj_color_attachment = {}; gpu_init_color_attachment(obj_color_attachment, target_view); - obj_color_attachment.loadOp = WGPULoadOp_Load; // Load skybox color + obj_color_attachment.loadOp = + skybox_rendered ? WGPULoadOp_Load : WGPULoadOp_Clear; // Load or Clear obj_color_attachment.storeOp = WGPUStoreOp_Store; + obj_color_attachment.clearValue = {0.05f, 0.05f, 0.05f, + 1.0f}; // Dark gray if no sky WGPURenderPassDepthStencilAttachment obj_depth_attachment = {}; obj_depth_attachment.view = depth_view; obj_depth_attachment.depthLoadOp = - WGPULoadOp_Load; // Load cleared depth from skybox pass + skybox_rendered ? WGPULoadOp_Load : WGPULoadOp_Clear; // Load or Clear obj_depth_attachment.depthStoreOp = WGPUStoreOp_Store; // Store object depth - obj_depth_attachment.depthClearValue = 1.0f; // Not used when loadOp is Load + obj_depth_attachment.depthClearValue = 1.0f; WGPURenderPassDescriptor obj_pass_desc = {}; obj_pass_desc.colorAttachmentCount = 1; diff --git a/src/3d/renderer.h b/src/3d/renderer.h index 148a521..57ad671 100644 --- a/src/3d/renderer.h +++ b/src/3d/renderer.h @@ -16,6 +16,7 @@ // Matches the GPU struct layout struct GlobalUniforms { mat4 view_proj; + mat4 inv_view_proj; // Added for skybox/raymarching vec4 camera_pos_time; // xyz = camera_pos, w = time vec4 params; // x = num_objects, yzw = padding vec2 resolution; diff --git a/src/procedural/generator.cc b/src/procedural/generator.cc index 0eb8b03..f6d4e02 100644 --- a/src/procedural/generator.cc +++ b/src/procedural/generator.cc @@ -33,7 +33,8 @@ bool gen_perlin(uint8_t* buffer, int w, int h, const float* params, // Pre-allocate temporary float buffer for accumulating noise float* accum = (float*)calloc((size_t)w * h, sizeof(float)); - if (!accum) return false; + if (!accum) + return false; float current_freq = base_freq; float current_amp = base_amp; @@ -42,7 +43,8 @@ bool gen_perlin(uint8_t* buffer, int w, int h, const float* params, for (int o = 0; o < octaves; ++o) { const int lattice_w = (int)ceil(current_freq) + 1; const int lattice_h = (int)ceil(current_freq) + 1; - float* lattice = (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); + float* lattice = + (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); if (!lattice) { free(accum); return false; @@ -110,43 +112,33 @@ bool gen_perlin(uint8_t* buffer, int w, int h, const float* params, bool gen_noise(uint8_t* buffer, int w, int h, const float* params, int num_params) { - float freq = (num_params > 1) ? params[1] : 4.0f; if (num_params > 0 && params[0] != 0) { - srand((unsigned int)params[0]); - } - - // Create a small lattice of random values const int lattice_w = (int)ceil(freq); const int lattice_h = (int)ceil(freq); - float* lattice = (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); - - if (!lattice) return false; - + float* lattice = + (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); + if (!lattice) + return false; for (int i = 0; i < lattice_w * lattice_h; ++i) { - lattice[i] = (float)rand() / RAND_MAX; - } const float scale_u = 1.f * (lattice_w - 1) / w; const float scale_v = 1.f * (lattice_h - 1) / h; - - for (int y = 0; y < h; ++y) { - const float v = scale_v * y; const int ly = (int)floor(v); @@ -162,7 +154,6 @@ bool gen_noise(uint8_t* buffer, int w, int h, const float* params, uint8_t* const dst = &buffer[y * w * 4]; for (int x = 0; x < w; ++x) { - float u = scale_u * x; const int lx = (int)floor(u); @@ -171,8 +162,6 @@ bool gen_noise(uint8_t* buffer, int w, int h, const float* params, float fu = smooth(u - lx); - - float n00 = n0[lx]; float n10 = n0[lx_next]; @@ -181,12 +170,8 @@ bool gen_noise(uint8_t* buffer, int w, int h, const float* params, float n11 = n1[lx_next]; - - const float noise = mix(mix(n00, n10, fu), mix(n01, n11, fu), fv); - - const uint8_t val = (uint8_t)(noise * 255.0f); dst[4 * x + 0] = val; // R @@ -196,19 +181,14 @@ bool gen_noise(uint8_t* buffer, int w, int h, const float* params, dst[4 * x + 2] = val; // B dst[4 * x + 3] = 255; // A - } - } free(lattice); return true; - } - - // Simple grid generator // Params[0]: Grid Size (pixels) // Params[1]: Line Thickness (pixels) diff --git a/src/tests/test_3d_render.cc b/src/tests/test_3d_render.cc index 5ae8b3a..2e9b663 100644 --- a/src/tests/test_3d_render.cc +++ b/src/tests/test_3d_render.cc @@ -120,11 +120,6 @@ void setup_scene() { g_scene.clear(); srand(12345); // Fixed seed - // Skybox object (always drawn first by renderer) - Object3D skybox(ObjectType::SKYBOX); - skybox.scale = vec3(1000.0f, 1000.0f, 1000.0f); // Large sphere - g_scene.add_object(skybox); - // Large floor, use BOX type (SDF) at index 0 Object3D floor(ObjectType::BOX); floor.position = vec3(0, -2.0f, 0); @@ -222,6 +217,8 @@ int main(int argc, char** argv) { sky_def.params = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; g_textures.create_procedural_texture("sky", sky_def); + g_renderer.set_sky_texture(g_textures.get_texture_view("sky")); + setup_scene(); g_camera.position = vec3(0, 5, 10); |
