From 15c4c0cf02d2b4ec8b3446bbc1805651ab312889 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 2 Feb 2026 13:34:36 +0100 Subject: feat(3d): Implement scene query shadows (POC) - Updated Renderer3D shader to include 'map_scene' and 'calc_shadow' functions. - Shader now iterates over all objects (up to kMaxObjects) to evaluate the scene SDF globally. - Implemented hard/soft shadows from a fixed directional light. - Updated GlobalUniforms struct with proper packing/padding for WebGPU compatibility. - Captured 'GPU BVH & Shadows' task in PROJECT_CONTEXT.md for future optimization. --- src/3d/renderer.cc | 98 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 47 deletions(-) (limited to 'src/3d/renderer.cc') diff --git a/src/3d/renderer.cc b/src/3d/renderer.cc index adc9a5f..031895d 100644 --- a/src/3d/renderer.cc +++ b/src/3d/renderer.cc @@ -14,8 +14,8 @@ bool Renderer3D::s_debug_enabled_ = false; static const char* kShaderCode = R"( struct GlobalUniforms { view_proj: mat4x4, - camera_pos: vec3, - time: f32, + camera_pos_time: vec4, + params: vec4, }; struct ObjectData { @@ -95,6 +95,44 @@ fn get_dist(p: vec3, obj_type: f32) -> f32 { return 100.0; } +fn map_scene(p: vec3) -> f32 { + var d = 1000.0; + let count = u32(globals.params.x); + + // Brute force loop over all objects + for (var i = 0u; i < count; i = i + 1u) { + let obj = object_data.objects[i]; + let obj_type = obj.params.x; + if (obj_type <= 0.0) { continue; } // Skip non-sdf objects + + // Transform world p to local q + // Assuming uniform scale + let center = vec3(obj.model[3].x, obj.model[3].y, obj.model[3].z); + let scale = length(vec3(obj.model[0].x, obj.model[0].y, obj.model[0].z)); + let mat3 = mat3x3(obj.model[0].xyz/scale, obj.model[1].xyz/scale, obj.model[2].xyz/scale); + let q = transpose(mat3) * (p - center) / scale; + + let dist = get_dist(q, obj_type) * scale; + d = min(d, dist); + } + return d; +} + +fn calc_shadow(ro: vec3, rd: vec3, tmin: f32, tmax: f32) -> f32 { + var t = tmin; + var res = 1.0; + for (var i = 0; i < 30; i++) { + let h = map_scene(ro + rd * t); + if (h < 0.001) { + return 0.0; // Hard shadow hit + } + res = min(res, 8.0 * h / t); // Soft shadow k=8 + t = t + h; + if (t > tmax) { break; } + } + return res; +} + fn get_normal(p: vec3, obj_type: f32) -> vec3 { if (obj_type == 1.0) { return normalize(p); } let e = vec2(0.001, 0.0); @@ -127,8 +165,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let center = vec3(obj.model[3].x, obj.model[3].y, obj.model[3].z); let scale = length(vec3(obj.model[0].x, obj.model[0].y, obj.model[0].z)); - let ro = globals.camera_pos; - let rd = normalize(in.world_pos - globals.camera_pos); + let ro = globals.camera_pos_time.xyz; + let rd = normalize(in.world_pos - ro); var t = length(in.world_pos - ro); var p = ro + rd * t; @@ -146,47 +184,10 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (!hit) { discard; } // Shading - // Recompute local pos at hit let q_hit = transpose(mat3) * (p - center) / scale; - // Calculate normal with bump mapping (Displacement method) - // N = normalize(gradient( dist(p) - displacement(p) )) - // We do finite difference on the combined field. - - let e = vec2(0.005, 0.0); // Slightly larger epsilon for texture smoothness - - // Helper to get displaced distance - // Note: We only displace for normal calc, not for the raymarch hit (surface detail only) - // or we could refine the hit. For now, just lighting. - - // Inline helper for displacement - // We need UVs for any point q - // UV Mapping: Spherical - - var n_local = vec3(0.0); - - // Base normal - let n_base = get_normal(q_hit, obj_type); - - // Sample noise at center - let uv_c = vec2(atan2(q_hit.x, q_hit.z) / 6.28 + 0.5, acos(clamp(q_hit.y / length(q_hit), -1.0, 1.0)) / 3.14); - let h_c = textureSample(noise_tex, noise_sampler, uv_c).r; - - // Evaluate noise gradient via finite difference on UVs? - // Or just 3D finite difference on pos? - // 3D FD is generic but requires 6 texture samples (or 4 tetra). - // Let's try a cheaper trick: Gradient of texture in UV space? - // textureSampleGrad? No, we want world space normal perturbation. - - // Standard tri-planar or 3D noise is better for SDFs, but we have 2D texture. - // Let's stick to the "Gradient by 2D finite difference on UVs" or simply perturb n_base with derivatives. - // simpler: - // float h = texture(...); - // vec3 bump = vec3(dFdx(h), dFdy(h), 0.0); // Screen space derivative? No. - - // Let's go with the robust 3D FD on the displacement field. - // dist_disp(q) = get_dist(q) - 0.02 * noise(q) - + // Calculate normal with bump mapping + let e = vec2(0.005, 0.0); let disp_strength = 0.05; let q_x1 = q_hit + e.xyy; @@ -219,13 +220,15 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let h_z2 = textureSample(noise_tex, noise_sampler, uv_z2).r; let d_z2 = get_dist(q_z2, obj_type) - disp_strength * h_z2; - n_local = normalize(vec3(d_x1 - d_x2, d_y1 - d_y2, d_z1 - d_z2)); + let n_local = normalize(vec3(d_x1 - d_x2, d_y1 - d_y2, d_z1 - d_z2)); let n_world = mat3 * n_local; let normal = normalize(n_world); let light_dir = normalize(vec3(1.0, 1.0, 1.0)); - let lighting = max(dot(normal, light_dir), 0.0) + 0.1; + let shadow = calc_shadow(p + normal * 0.05, light_dir, 0.0, 20.0); + + let lighting = (max(dot(normal, light_dir), 0.0) * shadow) + 0.1; return vec4(in.color.rgb * lighting, 1.0); } )"; @@ -413,8 +416,9 @@ 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.camera_pos = camera.position; - globals.time = time; + globals.camera_pos_time = vec4(camera.position.x, camera.position.y, camera.position.z, time); + globals.params = vec4((float)std::min((size_t)kMaxObjects, scene.objects.size()), 0.0f, 0.0f, 0.0f); + wgpuQueueWriteBuffer(queue_, global_uniform_buffer_, 0, &globals, sizeof(GlobalUniforms)); -- cgit v1.2.3