summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PROJECT_CONTEXT.md1
-rw-r--r--TODO.md7
-rw-r--r--assets/final/shaders/common_uniforms.wgsl3
-rw-r--r--assets/final/shaders/skybox.wgsl22
-rw-r--r--src/3d/renderer.cc19
-rw-r--r--src/3d/renderer.h1
-rw-r--r--src/procedural/generator.cc36
-rw-r--r--src/tests/test_3d_render.cc7
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.
diff --git a/TODO.md b/TODO.md
index 70154da..6fc6e1e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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);