From f307cde4ac1126e38c5595ce61a26d50cdd7ad4a Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 1 Feb 2026 11:10:35 +0100 Subject: feat: Implement hybrid rendering with SDF primitives - Added SDF logic for Sphere, Box, and Torus in WGSL. - Implemented hybrid normal calculation (analytical for sphere, numerical fallback). - Updated Renderer3D to dispatch object types to shader. - Updated test_3d_render to display mixed SDF shapes (Sphere, Torus, Box). - Added BOX to ObjectType enum. --- src/3d/object.h | 3 +- src/3d/renderer.cc | 159 +++++++++++++++++++++++++++++++++++++------- src/tests/test_3d_render.cc | 18 +++-- 3 files changed, 150 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/3d/object.h b/src/3d/object.h index f4215aa..ccbb1e1 100644 --- a/src/3d/object.h +++ b/src/3d/object.h @@ -10,7 +10,8 @@ enum class ObjectType { CUBE, SPHERE, PLANE, - TORUS + TORUS, + BOX // Add more SDF types here }; diff --git a/src/3d/renderer.cc b/src/3d/renderer.cc index 1745a97..2e08b4e 100644 --- a/src/3d/renderer.cc +++ b/src/3d/renderer.cc @@ -76,19 +76,14 @@ struct VertexOutput { @builtin(position) position: vec4, @location(0) local_pos: vec3, @location(1) color: vec4, + @location(2) @interpolate(flat) instance_index: u32, + @location(3) world_pos: vec3, }; @vertex fn vs_main(@builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32) -> VertexOutput { - // Hardcoded cube vertices (similar to C++ array but in shader for simplicity if desired, - // but here we might assume a vertex buffer or just generate logic. - // For this demo, let's use the buffer-less approach for vertices if we want to save space, - // but we have a C++ array. Let's just generate a cube on the fly from index?) - // Actually, let's map the C++ kCubeVertices to a vertex buffer or use a hardcoded array here. - // For 64k size, hardcoded in shader is good. - var pos = array, 36>( vec3(-1.0, -1.0, 1.0), vec3( 1.0, -1.0, 1.0), vec3( 1.0, 1.0, 1.0), vec3(-1.0, -1.0, 1.0), vec3( 1.0, 1.0, 1.0), vec3(-1.0, 1.0, 1.0), @@ -113,31 +108,140 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, var out: VertexOutput; out.position = clip_pos; - out.local_pos = p; + out.local_pos = p; // Proxy geometry local coords (-1 to 1) out.color = obj.color; + out.instance_index = instance_index; + out.world_pos = world_pos.xyz; return out; } +// --- SDF Primitives --- +// All primitives are centered at 0,0,0 + +fn sdSphere(p: vec3, r: f32) -> f32 { + return length(p) - r; +} + +fn sdBox(p: vec3, b: vec3) -> f32 { + let q = abs(p) - b; + return length(max(q, vec3(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); +} + +fn sdTorus(p: vec3, t: vec2) -> f32 { + let q = vec2(length(p.xz) - t.x, p.y); + return length(q) - t.y; +} + +// --- Dispatchers --- + +// Type IDs: 0=Cube(Wireframe proxy), 1=Sphere, 2=Box, 3=Torus +fn get_dist(p: vec3, type: f32) -> f32 { + if (type == 1.0) { return sdSphere(p, 0.9); } + if (type == 2.0) { return sdBox(p, vec3(0.7)); } + if (type == 3.0) { return sdTorus(p, vec2(0.6, 0.25)); } + return 100.0; +} + +// Analytical normals where possible, fallback to Numerical +fn get_normal(p: vec3, type: f32) -> vec3 { + if (type == 1.0) { // Sphere + return normalize(p); // Center is 0,0,0 + } + + // Finite Difference for others + let e = vec2(0.001, 0.0); + return normalize(vec3( + get_dist(p + e.xyy, type) - get_dist(p - e.xyy, type), + get_dist(p + e.yxy, type) - get_dist(p - e.yxy, type), + get_dist(p + e.yyx, type) - get_dist(p - e.yyx, type) + )); +} + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - // Simple wireframe-ish effect using barycentric coords logic? - // Or just check proximity to edge of local cube? - let d = abs(in.local_pos); - let edge_dist = max(max(d.x, d.y), d.z); + let obj = object_data.objects[in.instance_index]; + let type = obj.params.x; + + // Case 0: The central cube (Wireframe/Solid Box logic) - Proxy only + if (type == 0.0) { + let d = abs(in.local_pos); + let edge_dist = max(max(d.x, d.y), d.z); + + var col = in.color.rgb; + if (edge_dist > 0.95) { + col = vec3(1.0, 1.0, 1.0); // White edges + } else { + // Simple face shading + let normal = normalize(cross(dpdx(in.local_pos), dpdy(in.local_pos))); + let light = normalize(vec3(0.5, 1.0, 0.5)); + let diff = max(dot(normal, light), 0.2); + col = col * diff; + } + return vec4(col, 1.0); + } + + // Case 1+: Raymarching inside the proxy box + let center = vec3(obj.model[3].x, obj.model[3].y, obj.model[3].z); + + // Scale: Assume uniform scale from model matrix + 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); - // Mix object color with edge highlight - var col = in.color.rgb; - if (edge_dist > 0.95) { - col = vec3(1.0, 1.0, 1.0); // White edges - } else { - // Simple shading - let normal = normalize(cross(dpdx(in.local_pos), dpdy(in.local_pos))); - let light = normalize(vec3(0.5, 1.0, 0.5)); - let diff = max(dot(normal, light), 0.2); - col = col * diff; + // Start marching at proxy surface + var t = length(in.world_pos - ro); + var p = ro + rd * t; + + // Extract rotation (Normalized columns of model matrix) + let mat3 = mat3x3( + obj.model[0].xyz / scale, + obj.model[1].xyz / scale, + obj.model[2].xyz / scale + ); + + var hit = false; + // Raymarch Loop + for (var i = 0; i < 40; i++) { + // Transform p to local unscaled space for SDF eval + // q = inv(R) * (p - center) / scale + let q = transpose(mat3) * (p - center) / scale; + + let d_local = get_dist(q, type); + let d_world = d_local * scale; + + if (d_world < 0.001) { + hit = true; + break; + } + if (d_world > 3.0 * scale) { + break; + } + p = p + rd * d_world; } - return vec4(col, 1.0); + if (!hit) { + discard; + } + + // Shading + // Recompute local pos at hit + let q_hit = transpose(mat3) * (p - center) / scale; + + // Normal calculation: + // Calculate normal in local space, then rotate to world. + let n_local = get_normal(q_hit, type); + let n_world = mat3 * n_local; + + let normal = normalize(n_world); + let light_dir = normalize(vec3(1.0, 1.0, 1.0)); + + let diff = max(dot(normal, light_dir), 0.0); + let amb = 0.1; + + let lighting = diff + amb; + + return vec4(in.color.rgb * lighting, 1.0); } )"; @@ -328,7 +432,14 @@ void Renderer3D::update_uniforms(const Scene& scene, const Camera& camera, float ObjectData data; data.model = obj.get_model_matrix(); data.color = obj.color; - // data.params = ... + // Map ObjectType enum to float ID + float type_id = 0.0f; + if (obj.type == ObjectType::SPHERE) type_id = 1.0f; + else if (obj.type == ObjectType::CUBE) type_id = 0.0f; + else if (obj.type == ObjectType::TORUS) type_id = 3.0f; + else if (obj.type == ObjectType::BOX) type_id = 2.0f; + + data.params = vec4(type_id, 0, 0, 0); obj_data.push_back(data); if (obj_data.size() >= kMaxObjects) break; } diff --git a/src/tests/test_3d_render.cc b/src/tests/test_3d_render.cc index 41bffe6..4be7153 100644 --- a/src/tests/test_3d_render.cc +++ b/src/tests/test_3d_render.cc @@ -140,19 +140,27 @@ void init_wgpu() { void setup_scene() { g_scene.clear(); - // Center Red Cube - Object3D center; + // Center Red Cube (Wireframe Proxy) + Object3D center(ObjectType::CUBE); center.position = vec3(0, 0, 0); center.color = vec4(1, 0, 0, 1); g_scene.add_object(center); - // Orbiting Green Cubes + // Orbiting Objects for (int i = 0; i < 8; ++i) { - Object3D obj; + ObjectType type = ObjectType::SPHERE; + if (i % 3 == 1) type = ObjectType::TORUS; + if (i % 3 == 2) type = ObjectType::BOX; + + Object3D obj(type); float angle = (i / 8.0f) * 6.28318f; obj.position = vec3(std::cos(angle) * 4.0f, 0, std::sin(angle) * 4.0f); obj.scale = vec3(0.5f, 0.5f, 0.5f); - obj.color = vec4(0, 1, 0, 1); + + if (type == ObjectType::SPHERE) obj.color = vec4(0, 1, 0, 1); + else if (type == ObjectType::TORUS) obj.color = vec4(0, 0.5, 1, 1); + else obj.color = vec4(1, 1, 0, 1); + g_scene.add_object(obj); } } -- cgit v1.2.3