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 | |
| 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')
| -rw-r--r-- | src/3d/camera.h | 39 | ||||
| -rw-r--r-- | src/3d/object.h | 51 | ||||
| -rw-r--r-- | src/3d/renderer.cc | 383 | ||||
| -rw-r--r-- | src/3d/renderer.h | 60 | ||||
| -rw-r--r-- | src/3d/scene.h | 22 |
5 files changed, 555 insertions, 0 deletions
diff --git a/src/3d/camera.h b/src/3d/camera.h new file mode 100644 index 0000000..23e26d6 --- /dev/null +++ b/src/3d/camera.h @@ -0,0 +1,39 @@ +// This file is part of the 64k demo project. +// It defines the Camera class for 3D navigation and rendering. +// Handles view and projection matrix generation. + +#pragma once + +#include "util/mini_math.h" + +class Camera { + public: + vec3 position; + vec3 target; + vec3 up; + + float fov_y_rad; + float aspect_ratio; + float near_plane; + float far_plane; + + Camera() + : position(0, 0, 5), target(0, 0, 0), up(0, 1, 0), fov_y_rad(0.785398f), + aspect_ratio(1.777f), near_plane(0.1f), far_plane(100.0f) { + } + + mat4 get_view_matrix() const { + return mat4::look_at(position, target, up); + } + + mat4 get_projection_matrix() const { + return mat4::perspective(fov_y_rad, aspect_ratio, near_plane, far_plane); + } + + // Helper to move camera + void set_look_at(vec3 pos, vec3 tgt, vec3 up_vec = vec3(0, 1, 0)) { + position = pos; + target = tgt; + up = up_vec; + } +}; diff --git a/src/3d/object.h b/src/3d/object.h new file mode 100644 index 0000000..f4215aa --- /dev/null +++ b/src/3d/object.h @@ -0,0 +1,51 @@ +// This file is part of the 64k demo project. +// It defines the base 3D Object structure. +// Handles transforms and bounding volumes for hybrid rendering. + +#pragma once + +#include "util/mini_math.h" + +enum class ObjectType { + CUBE, + SPHERE, + PLANE, + TORUS + // Add more SDF types here +}; + +struct BoundingVolume { + vec3 min; + vec3 max; +}; + +class Object3D { + public: + vec3 position; + quat rotation; + vec3 scale; + + ObjectType type; + // Material parameters could go here (color, roughness, etc.) + vec4 color; + + Object3D(ObjectType t = ObjectType::CUBE) + : position(0, 0, 0), rotation(0, 0, 0, 1), scale(1, 1, 1), type(t), + color(1, 1, 1, 1) { + } + + mat4 get_model_matrix() const { + mat4 T = mat4::translate(position); + mat4 R = rotation.to_mat(); + mat4 S = mat4::scale(scale); + // M = T * R * S + return T * (R * S); + } + + // Returns the local-space AABB of the primitive (before transform) + // Used to generate the proxy geometry for rasterization. + BoundingVolume get_local_bounds() const { + // Simple defaults for unit primitives + return {{-1, -1, -1}, {1, 1, 1}}; + } +}; 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); +} diff --git a/src/3d/renderer.h b/src/3d/renderer.h new file mode 100644 index 0000000..0dadc32 --- /dev/null +++ b/src/3d/renderer.h @@ -0,0 +1,60 @@ +// This file is part of the 64k demo project. +// It defines the Renderer3D class. +// Handles WebGPU pipeline creation and execution for 3D scenes. + +#pragma once + +#include "3d/camera.h" +#include "3d/scene.h" +#include "gpu/gpu.h" +#include <vector> + +// Matches the GPU struct layout +struct GlobalUniforms { + mat4 view_proj; + vec3 camera_pos; + float time; +}; + +// Matches the GPU struct layout +struct ObjectData { + mat4 model; + vec4 color; + vec4 params; // Type, etc. +}; + +class Renderer3D { + public: + void init(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format); + void shutdown(); + + // Renders the scene to the given texture view + void render(const Scene& scene, const Camera& camera, float time, + WGPUTextureView target_view, WGPUTextureView depth_view_opt = nullptr); + + // Resize handler (if needed for internal buffers) + void resize(int width, int height); + + private: + void create_pipeline(); + void create_default_resources(); + void update_uniforms(const Scene& scene, const Camera& camera, float time); + + WGPUDevice device_ = nullptr; + WGPUQueue queue_ = nullptr; + WGPUTextureFormat format_ = WGPUTextureFormat_Undefined; + + WGPURenderPipeline pipeline_ = nullptr; + WGPUBindGroup bind_group_ = nullptr; + WGPUBuffer global_uniform_buffer_ = nullptr; + WGPUBuffer object_storage_buffer_ = nullptr; + + // Depth buffer management + WGPUTexture depth_texture_ = nullptr; + WGPUTextureView depth_view_ = nullptr; + int width_ = 0; + int height_ = 0; + + // Max objects capacity + static const int kMaxObjects = 100; +}; diff --git a/src/3d/scene.h b/src/3d/scene.h new file mode 100644 index 0000000..6793975 --- /dev/null +++ b/src/3d/scene.h @@ -0,0 +1,22 @@ +// This file is part of the 64k demo project. +// It defines the Scene container. +// Manages a collection of objects and lights. + +#pragma once + +#include "3d/object.h" +#include <vector> + +class Scene { + public: + std::vector<Object3D> objects; + // std::vector<Light> lights; // Future + + void add_object(const Object3D& obj) { + objects.push_back(obj); + } + + void clear() { + objects.clear(); + } +}; |
