diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-01 10:51:15 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-01 10:51:15 +0100 |
| commit | 8bdc4754647c9c6691130fa91d51fee93c5fc88f (patch) | |
| tree | 2cfd7f72a21541c488ea48629eef47a6774fc2c4 /src/3d/renderer.cc | |
| parent | 7905abd9f7ad35231289e729b42e3ad57a943ff5 (diff) | |
feat: Implement 3D system and procedural texture manager
- Extended mini_math.h with mat4 multiplication and affine transforms.
- Implemented TextureManager for runtime procedural texture generation and GPU upload.
- Added 3D system components: Camera, Object, Scene, and Renderer3D.
- Created test_3d_render mini-demo for interactive 3D verification.
- Fixed WebGPU validation errors regarding depthSlice and unimplemented WaitAny.
Diffstat (limited to 'src/3d/renderer.cc')
| -rw-r--r-- | src/3d/renderer.cc | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/src/3d/renderer.cc b/src/3d/renderer.cc new file mode 100644 index 0000000..1745a97 --- /dev/null +++ b/src/3d/renderer.cc @@ -0,0 +1,383 @@ +// This file is part of the 64k demo project. +// It implements the Renderer3D class. + +#include "3d/renderer.h" +#include <iostream> +#include <cstring> + +// Simple Cube Geometry (Triangle list) +// 36 vertices +static const float kCubeVertices[] = { + // Front face + -1.0, -1.0, 1.0, + 1.0, -1.0, 1.0, + 1.0, 1.0, 1.0, + -1.0, -1.0, 1.0, + 1.0, 1.0, 1.0, + -1.0, 1.0, 1.0, + // Back face + -1.0, -1.0, -1.0, + -1.0, 1.0, -1.0, + 1.0, 1.0, -1.0, + -1.0, -1.0, -1.0, + 1.0, 1.0, -1.0, + 1.0, -1.0, -1.0, + // Top face + -1.0, 1.0, -1.0, + -1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, + -1.0, 1.0, -1.0, + 1.0, 1.0, 1.0, + 1.0, 1.0, -1.0, + // Bottom face + -1.0, -1.0, -1.0, + 1.0, -1.0, -1.0, + 1.0, -1.0, 1.0, + -1.0, -1.0, -1.0, + 1.0, -1.0, 1.0, + -1.0, -1.0, 1.0, + // Right face + 1.0, -1.0, -1.0, + 1.0, 1.0, -1.0, + 1.0, 1.0, 1.0, + 1.0, -1.0, -1.0, + 1.0, 1.0, 1.0, + 1.0, -1.0, 1.0, + // Left face + -1.0, -1.0, -1.0, + -1.0, -1.0, 1.0, + -1.0, 1.0, 1.0, + -1.0, -1.0, -1.0, + -1.0, 1.0, 1.0, + -1.0, 1.0, -1.0, +}; + +static const char* kShaderCode = R"( +struct GlobalUniforms { + view_proj: mat4x4<f32>, + camera_pos: vec3<f32>, + time: f32, +}; + +struct ObjectData { + model: mat4x4<f32>, + color: vec4<f32>, + params: vec4<f32>, +}; + +struct ObjectsBuffer { + objects: array<ObjectData>, +}; + +@group(0) @binding(0) var<uniform> globals: GlobalUniforms; +@group(0) @binding(1) var<storage, read> object_data: ObjectsBuffer; + +struct VertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) local_pos: vec3<f32>, + @location(1) color: vec4<f32>, +}; + +@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<vec3<f32>, 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), + 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), + 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), + 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), + 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), + 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) + ); + + let p = pos[vertex_index]; + let obj = object_data.objects[instance_index]; + + // Model -> World -> Clip + let world_pos = obj.model * vec4<f32>(p, 1.0); + let clip_pos = globals.view_proj * world_pos; + + var out: VertexOutput; + out.position = clip_pos; + out.local_pos = p; + out.color = obj.color; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + // 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); + + // Mix object color with edge highlight + var col = in.color.rgb; + if (edge_dist > 0.95) { + col = vec3<f32>(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<f32>(0.5, 1.0, 0.5)); + let diff = max(dot(normal, light), 0.2); + col = col * diff; + } + + return vec4<f32>(col, 1.0); +} +)"; + +void Renderer3D::init(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format) { + device_ = device; + queue_ = queue; + format_ = format; + + create_default_resources(); + create_pipeline(); +} + +void Renderer3D::shutdown() { + if (pipeline_) wgpuRenderPipelineRelease(pipeline_); + if (bind_group_) wgpuBindGroupRelease(bind_group_); + if (global_uniform_buffer_) wgpuBufferRelease(global_uniform_buffer_); + if (object_storage_buffer_) wgpuBufferRelease(object_storage_buffer_); + if (depth_view_) wgpuTextureViewRelease(depth_view_); + if (depth_texture_) wgpuTextureRelease(depth_texture_); +} + +void Renderer3D::resize(int width, int height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + if (depth_view_) wgpuTextureViewRelease(depth_view_); + if (depth_texture_) wgpuTextureRelease(depth_texture_); + + WGPUTextureDescriptor desc = {}; + desc.usage = WGPUTextureUsage_RenderAttachment; + desc.dimension = WGPUTextureDimension_2D; + desc.size = {(uint32_t)width, (uint32_t)height, 1}; + desc.format = WGPUTextureFormat_Depth24Plus; // Common depth format + desc.mipLevelCount = 1; + desc.sampleCount = 1; + + depth_texture_ = wgpuDeviceCreateTexture(device_, &desc); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = WGPUTextureFormat_Depth24Plus; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.aspect = WGPUTextureAspect_DepthOnly; + view_desc.arrayLayerCount = 1; + view_desc.mipLevelCount = 1; + + depth_view_ = wgpuTextureCreateView(depth_texture_, &view_desc); +} + +void Renderer3D::create_default_resources() { + // Uniform Buffer + global_uniform_buffer_ = gpu_create_buffer(device_, sizeof(GlobalUniforms), + WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr).buffer; + + // Storage Buffer + size_t storage_size = sizeof(ObjectData) * kMaxObjects; + object_storage_buffer_ = gpu_create_buffer(device_, storage_size, + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, nullptr).buffer; +} + +void Renderer3D::create_pipeline() { + // Bind Group Layout + WGPUBindGroupLayoutEntry entries[2] = {}; + + // Binding 0: Globals (Uniform) + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.minBindingSize = sizeof(GlobalUniforms); + + // Binding 1: Object Data (Storage) + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + entries[1].buffer.minBindingSize = sizeof(ObjectData) * kMaxObjects; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = entries; + WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + // Bind Group + WGPUBindGroupEntry bg_entries[2] = {}; + bg_entries[0].binding = 0; + bg_entries[0].buffer = global_uniform_buffer_; + bg_entries[0].size = sizeof(GlobalUniforms); + + bg_entries[1].binding = 1; + bg_entries[1].buffer = object_storage_buffer_; + bg_entries[1].size = sizeof(ObjectData) * kMaxObjects; + + WGPUBindGroupDescriptor bg_desc = {}; + bg_desc.layout = bgl; + bg_desc.entryCount = 2; + bg_desc.entries = bg_entries; + bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); + + // Pipeline Layout + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bgl; + WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + + // Shader Code + const char* shader_source = kShaderCode; + + // Shader Module +#if defined(DEMO_CROSS_COMPILE_WIN32) + WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; + wgsl_desc.code = shader_source; + + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#else + WGPUShaderSourceWGSL wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_desc.code = {shader_source, strlen(shader_source)}; + + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#endif + + WGPUShaderModule shader_module = wgpuDeviceCreateShaderModule(device_, &shader_desc); + + // Depth Stencil State + WGPUDepthStencilState depth_stencil = {}; + depth_stencil.format = WGPUTextureFormat_Depth24Plus; + depth_stencil.depthWriteEnabled = WGPUOptionalBool_True; + depth_stencil.depthCompare = WGPUCompareFunction_Less; + + // Render Pipeline + WGPURenderPipelineDescriptor desc = {}; + desc.layout = pipeline_layout; + + // Vertex + desc.vertex.module = shader_module; +#if defined(DEMO_CROSS_COMPILE_WIN32) + desc.vertex.entryPoint = "vs_main"; +#else + desc.vertex.entryPoint = {"vs_main", 7}; +#endif + + // Fragment + WGPUColorTargetState color_target = {}; + color_target.format = format_; + color_target.writeMask = WGPUColorWriteMask_All; + + WGPUFragmentState fragment = {}; + fragment.module = shader_module; +#if defined(DEMO_CROSS_COMPILE_WIN32) + fragment.entryPoint = "fs_main"; +#else + fragment.entryPoint = {"fs_main", 7}; +#endif + fragment.targetCount = 1; + fragment.targets = &color_target; + desc.fragment = &fragment; + + desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + desc.primitive.cullMode = WGPUCullMode_Back; + desc.primitive.frontFace = WGPUFrontFace_CCW; + + desc.depthStencil = &depth_stencil; + desc.multisample.count = 1; + desc.multisample.mask = 0xFFFFFFFF; + + pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &desc); + + wgpuBindGroupLayoutRelease(bgl); + wgpuPipelineLayoutRelease(pipeline_layout); + wgpuShaderModuleRelease(shader_module); +} + +void Renderer3D::update_uniforms(const Scene& scene, const Camera& camera, float time) { + // Update Globals + GlobalUniforms globals; + globals.view_proj = camera.get_projection_matrix() * camera.get_view_matrix(); + globals.camera_pos = camera.position; + globals.time = time; + wgpuQueueWriteBuffer(queue_, global_uniform_buffer_, 0, &globals, sizeof(GlobalUniforms)); + + // Update Objects + std::vector<ObjectData> obj_data; + obj_data.reserve(scene.objects.size()); + for (const auto& obj : scene.objects) { + ObjectData data; + data.model = obj.get_model_matrix(); + data.color = obj.color; + // data.params = ... + obj_data.push_back(data); + if (obj_data.size() >= kMaxObjects) break; + } + + if (!obj_data.empty()) { + wgpuQueueWriteBuffer(queue_, object_storage_buffer_, 0, obj_data.data(), obj_data.size() * sizeof(ObjectData)); + } +} + +void Renderer3D::render(const Scene& scene, const Camera& camera, float time, + WGPUTextureView target_view, WGPUTextureView depth_view_opt) { + update_uniforms(scene, camera, time); + + WGPUTextureView depth_view = depth_view_opt ? depth_view_opt : depth_view_; + if (!depth_view) return; // Should have been created by resize + + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, target_view); + color_attachment.clearValue = {0.05, 0.05, 0.1, 1.0}; // Dark blue-ish background + + WGPURenderPassDepthStencilAttachment depth_attachment = {}; + depth_attachment.view = depth_view; + depth_attachment.depthLoadOp = WGPULoadOp_Clear; + depth_attachment.depthStoreOp = WGPUStoreOp_Store; + depth_attachment.depthClearValue = 1.0f; + + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + pass_desc.depthStencilAttachment = &depth_attachment; + + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device_, nullptr); + WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + + // Draw all objects (Instance Count = object count) + // Vertex Count = 36 (Cube) + uint32_t instance_count = (uint32_t)std::min((size_t)kMaxObjects, scene.objects.size()); + if (instance_count > 0) { + wgpuRenderPassEncoderDraw(pass, 36, instance_count, 0, 0); + } + + wgpuRenderPassEncoderEnd(pass); + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); + wgpuQueueSubmit(queue_, 1, &commands); + + wgpuRenderPassEncoderRelease(pass); + wgpuCommandBufferRelease(commands); + wgpuCommandEncoderRelease(encoder); +} |
