diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index dc2aa0739..9413e016d 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -104,6 +104,8 @@ find_package(EnTT CONFIG REQUIRED) find_package(assimp CONFIG REQUIRED) +find_package(libzip CONFIG REQUIRED) + # Build render stack library group set(SDL3CPP_RENDER_STACK_LIBS EnTT::EnTT) @@ -281,6 +283,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/geometry/workflow_geometry_create_cube_step.cpp src/services/impl/workflow/geometry/workflow_geometry_create_plane_step.cpp src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp + src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp src/services/impl/workflow/rendering/workflow_draw_map_step.cpp src/services/impl/workflow/rendering/workflow_draw_textured_box_step.cpp src/services/impl/workflow/rendering/workflow_draw_textured_step.cpp @@ -370,6 +373,7 @@ if(BUILD_SDL3_APP) glm::glm stb::stb EnTT::EnTT + libzip::zip assimp::assimp ) endif() diff --git a/gameengine/cmake_config.json b/gameengine/cmake_config.json index 1eb252bef..3c8ac6816 100644 --- a/gameengine/cmake_config.json +++ b/gameengine/cmake_config.json @@ -51,7 +51,8 @@ "glm", "stb", "EnTT", - "assimp" + "assimp", + "libzip" ] }, "source_exclusions": [ @@ -128,6 +129,7 @@ "glm::glm", "stb::stb", "EnTT::EnTT", + "libzip::zip", "assimp::assimp" ], "compile_definitions": [], diff --git a/gameengine/packages/quake3/package.json b/gameengine/packages/quake3/package.json index b973961cb..441212de1 100644 --- a/gameengine/packages/quake3/package.json +++ b/gameengine/packages/quake3/package.json @@ -2,11 +2,11 @@ "name": "Quake 3 Support", "version": "1.0.0", "id": "quake3", - "type": "game-format-support", - "category": "asset-loaders", - "description": "Load Quake 3 BSP maps with automatic unit conversion and Q3-correct camera/physics settings", + "type": "game", + "category": "demo", + "description": "Quake 3 BSP map viewer with FPS controls", - "defaultWorkflow": "workflows/load_map_with_unit_conversion.json", + "defaultWorkflow": "workflows/q3_game.json", "workflows": [ "workflows/load_map_with_unit_conversion.json" diff --git a/gameengine/packages/quake3/workflows/q3_frame.json b/gameengine/packages/quake3/workflows/q3_frame.json new file mode 100644 index 000000000..17d4ecd67 --- /dev/null +++ b/gameengine/packages/quake3/workflows/q3_frame.json @@ -0,0 +1,129 @@ +{ + "name": "Q3 Frame Tick", + "description": "Per-frame: poll input, FPS move, render BSP map with post-FX.", + "nodes": [ + { + "id": "input_poll", + "type": "input.poll", + "typeVersion": 1, + "position": [0, 0] + }, + { + "id": "physics_move", + "type": "physics.fps.move", + "typeVersion": 1, + "position": [200, 0], + "parameters": { + "move_speed": 8.0, + "sprint_multiplier": 2.0, + "crouch_multiplier": 0.4, + "jump_height": 2.0, + "jump_duration": 0.4, + "air_control": 0.5, + "gravity_scale": 0.3, + "crouch_height": 0.5, + "stand_height": 1.4 + } + }, + { + "id": "physics_step", + "type": "physics.step", + "typeVersion": 1, + "position": [400, 0] + }, + { + "id": "sync_transforms", + "type": "physics.sync_transforms", + "typeVersion": 1, + "position": [500, 0] + }, + { + "id": "camera_update", + "type": "camera.fps.update", + "typeVersion": 1, + "position": [600, 0], + "parameters": { + "sensitivity": 0.003, + "eye_height": 1.4, + "fov": 90.0, + "near": 0.05, + "far": 1000.0 + } + }, + { + "id": "render_prepare", + "type": "render.prepare", + "typeVersion": 1, + "position": [750, 0] + }, + { + "id": "frame_begin", + "type": "frame.gpu.begin", + "typeVersion": 1, + "position": [800, 0], + "parameters": { + "clear_r": 0.3, + "clear_g": 0.5, + "clear_b": 0.8 + } + }, + { + "id": "draw_map", + "type": "draw.map", + "typeVersion": 1, + "position": [1000, 0], + "parameters": { + "default_texture": "walls_texture", + "roughness": 0.7, + "metallic": 0.0 + } + }, + { + "id": "end_scene", + "type": "frame.gpu.end_scene", + "typeVersion": 1, + "position": [1200, 0] + }, + { + "id": "postfx_taa", + "type": "postfx.taa", + "typeVersion": 1, + "position": [1250, 0], + "parameters": { "blend_factor": 0.05 } + }, + { + "id": "postfx_ssao", + "type": "postfx.ssao", + "typeVersion": 1, + "position": [1300, 0] + }, + { + "id": "bloom_extract", + "type": "postfx.bloom_extract", + "typeVersion": 1, + "position": [1400, 0] + }, + { + "id": "bloom_blur", + "type": "postfx.bloom_blur", + "typeVersion": 1, + "position": [1500, 0] + }, + { + "id": "postfx_composite", + "type": "postfx.composite", + "typeVersion": 1, + "position": [1600, 0] + } + ], + "connections": { + "input_poll": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } }, + "physics_move": { "main": { "0": [{ "node": "physics_step", "type": "main", "index": 0 }] } }, + "physics_step": { "main": { "0": [{ "node": "sync_transforms", "type": "main", "index": 0 }] } }, + "sync_transforms": { "main": { "0": [{ "node": "camera_update", "type": "main", "index": 0 }] } }, + "camera_update": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } }, + "render_prepare": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } }, + "frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } }, + "draw_map": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } } + } +} diff --git a/gameengine/packages/quake3/workflows/q3_game.json b/gameengine/packages/quake3/workflows/q3_game.json new file mode 100644 index 000000000..af6b9633b --- /dev/null +++ b/gameengine/packages/quake3/workflows/q3_game.json @@ -0,0 +1,68 @@ +{ + "name": "Quake 3 Map Viewer", + "active": true, + "settings": { "executionTimeout": 0 }, + "variables": { + "window_width": { "name": "window_width", "type": "number", "defaultValue": 1280 }, + "window_height": { "name": "window_height", "type": "number", "defaultValue": 960 }, + "window_title": { "name": "window_title", "type": "string", "defaultValue": "Quake 3 - Map Viewer" }, + "renderer_type": { "name": "renderer_type", "type": "string", "defaultValue": "auto" }, + "shader_vertex_path": { "name": "shader_vertex_path", "type": "string", "defaultValue": "packages/seed/shaders/msl/constant_color.vert.metal" }, + "shader_fragment_path": { "name": "shader_fragment_path", "type": "string", "defaultValue": "packages/seed/shaders/msl/constant_color.frag.metal" }, + "shader_textured_vert_path": { "name": "shader_textured_vert_path", "type": "string", "defaultValue": "packages/seed/shaders/msl/textured.vert.metal" }, + "shader_textured_frag_path": { "name": "shader_textured_frag_path", "type": "string", "defaultValue": "packages/seed/shaders/msl/textured.frag.metal" }, + "tex_walls_path": { "name": "tex_walls_path", "type": "string", "defaultValue": "packages/seed/assets/textures/walls/Bricks058_1K-JPG_Color.jpg" } + }, + "nodes": [ + { "id": "sdl_init", "type": "sdl.init", "typeVersion": 1, "position": [0, 0] }, + { "id": "sdl_window", "type": "sdl.window.create", "typeVersion": 1, "position": [200, 0] }, + { "id": "gpu_init_viewport", "type": "graphics.gpu.init_viewport", "typeVersion": 1, "position": [400, 0], + "parameters": { "inputs": { "width": "window_width", "height": "window_height" }, "outputs": { "viewport_config": "viewport_config" } } }, + { "id": "gpu_init_renderer", "type": "graphics.gpu.init_renderer", "typeVersion": 1, "position": [600, 0], + "parameters": { "inputs": { "renderer_type": "renderer_type" }, "outputs": { "selected_renderer": "selected_renderer" } } }, + { "id": "gpu_init", "type": "graphics.gpu.init", "typeVersion": 1, "position": [800, 0], + "parameters": { "inputs": { "viewport_config": "viewport_config", "selected_renderer": "selected_renderer" }, "outputs": { "gpu_handle": "gpu_handle" } } }, + { "id": "compile_tex_vert", "type": "graphics.gpu.shader.compile", "typeVersion": 1, "position": [1000, 0], + "parameters": { "stage": "vertex", "output_key": "textured_vertex_shader", "num_uniform_buffers": 1, "num_samplers": 0 }, + "inputs": { "shader_path": "shader_textured_vert_path" } }, + { "id": "compile_tex_frag", "type": "graphics.gpu.shader.compile", "typeVersion": 1, "position": [1100, 0], + "parameters": { "stage": "fragment", "output_key": "textured_fragment_shader", "num_uniform_buffers": 1, "num_samplers": 2 }, + "inputs": { "shader_path": "shader_textured_frag_path" } }, + { "id": "create_tex_pipeline", "type": "graphics.gpu.pipeline.create", "typeVersion": 1, "position": [1200, 0], + "parameters": { "vertex_shader_key": "textured_vertex_shader", "fragment_shader_key": "textured_fragment_shader", "vertex_format": "position_uv", "pipeline_key": "gpu_pipeline_textured" } }, + { "id": "tex_walls", "name": "Load Texture", "type": "texture.load", "typeVersion": 1, "position": [1300, 0], + "parameters": { "inputs": { "image_path": "tex_walls_path" }, "outputs": { "texture": "walls_texture" } } }, + { "id": "load_bsp", "name": "Load Q3 BSP", "type": "bsp.load", "typeVersion": 1, "position": [1400, 0], + "parameters": { "pk3_path": "C:/baseq3/pak0.pk3", "map_name": "q3dm17", "scale": 0.03125 } }, + { "id": "physics_world", "type": "physics.world.create", "typeVersion": 1, "position": [0, 200] }, + { "id": "player", "type": "physics.body.add", "typeVersion": 1, "position": [200, 200], + "parameters": { "name": "player", "shape": "capsule", "mass": 80, "pos_x": 0, "pos_y": 5, "pos_z": 0, "radius": 0.3, "height": 1.0, "lock_rotation": 1, "is_player": 1 } }, + { "id": "camera_setup", "type": "camera.setup", "typeVersion": 1, "position": [400, 200], + "parameters": { "outputs": { "camera_state": "camera.state" } } }, + { "id": "lighting", "type": "lighting.setup", "typeVersion": 1, "position": [600, 200], + "parameters": { "light_dir_x": -0.5, "light_dir_y": -0.8, "light_dir_z": -0.3, "light_intensity": 2.0, "ambient_r": 0.2, "ambient_g": 0.2, "ambient_b": 0.25, "ambient_intensity": 1.5, "exposure": 1.0 } }, + { "id": "set_running", "type": "value.literal", "typeVersion": 1, "position": [800, 200], + "parameters": { "value": true, "outputs": { "value": "game_running" } } }, + { "id": "game_loop", "type": "control.loop.while", "typeVersion": 1, "position": [1000, 200], + "parameters": { "condition_key": "game_running", "package": "quake3", "workflow": "q3_frame" } }, + { "id": "exit", "type": "system.exit", "typeVersion": 1, "position": [1200, 200] } + ], + "connections": { + "sdl_init": { "main": { "0": [{ "node": "sdl_window", "type": "main", "index": 0 }] } }, + "sdl_window": { "main": { "0": [{ "node": "gpu_init_viewport", "type": "main", "index": 0 }] } }, + "gpu_init_viewport": { "main": { "0": [{ "node": "gpu_init_renderer", "type": "main", "index": 0 }] } }, + "gpu_init_renderer": { "main": { "0": [{ "node": "gpu_init", "type": "main", "index": 0 }] } }, + "gpu_init": { "main": { "0": [{ "node": "compile_tex_vert", "type": "main", "index": 0 }] } }, + "compile_tex_vert": { "main": { "0": [{ "node": "compile_tex_frag", "type": "main", "index": 0 }] } }, + "compile_tex_frag": { "main": { "0": [{ "node": "create_tex_pipeline", "type": "main", "index": 0 }] } }, + "create_tex_pipeline": { "main": { "0": [{ "node": "tex_walls", "type": "main", "index": 0 }] } }, + "tex_walls": { "main": { "0": [{ "node": "physics_world", "type": "main", "index": 0 }] } }, + "physics_world": { "main": { "0": [{ "node": "load_bsp", "type": "main", "index": 0 }] } }, + "load_bsp": { "main": { "0": [{ "node": "player", "type": "main", "index": 0 }] } }, + "player": { "main": { "0": [{ "node": "camera_setup", "type": "main", "index": 0 }] } }, + "camera_setup": { "main": { "0": [{ "node": "lighting", "type": "main", "index": 0 }] } }, + "lighting": { "main": { "0": [{ "node": "set_running", "type": "main", "index": 0 }] } }, + "set_running": { "main": { "0": [{ "node": "game_loop", "type": "main", "index": 0 }] } }, + "game_loop": { "main": { "0": [{ "node": "exit", "type": "main", "index": 0 }] } } + } +} diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp new file mode 100644 index 000000000..5dade6e70 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp @@ -0,0 +1,334 @@ +#include "services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +// Q3 BSP file format structures +#pragma pack(push, 1) + +struct BspHeader { + char magic[4]; // "IBSP" + int32_t version; // 0x2E (46) for Q3 +}; + +struct BspLump { + int32_t offset; + int32_t length; +}; + +// Lump indices +enum { + LUMP_ENTITIES = 0, + LUMP_TEXTURES = 1, + LUMP_PLANES = 2, + LUMP_NODES = 3, + LUMP_LEAFS = 4, + LUMP_LEAFFACES = 5, + LUMP_LEAFBRUSHES = 6, + LUMP_MODELS = 7, + LUMP_BRUSHES = 8, + LUMP_BRUSHSIDES = 9, + LUMP_VERTICES = 10, + LUMP_MESHVERTS = 11, + LUMP_EFFECTS = 12, + LUMP_FACES = 13, + LUMP_LIGHTMAPS = 14, + LUMP_LIGHTVOLS = 15, + LUMP_VISDATA = 16, + NUM_LUMPS = 17 +}; + +struct BspVertex { + float position[3]; + float texcoord[2][2]; // [0]=surface, [1]=lightmap + float normal[3]; + uint8_t color[4]; +}; + +struct BspFace { + int32_t texture; + int32_t effect; + int32_t type; // 1=polygon, 2=patch, 3=mesh, 4=billboard + int32_t vertex; + int32_t n_vertices; + int32_t meshvert; + int32_t n_meshverts; + int32_t lm_index; + int32_t lm_start[2]; + int32_t lm_size[2]; + float lm_origin[3]; + float lm_vecs[2][3]; + float normal[3]; + int32_t size[2]; // patch dimensions +}; + +struct BspTexture { + char name[64]; + int32_t flags; + int32_t contents; +}; + +#pragma pack(pop) + +WorkflowBspLoadStep::WorkflowBspLoadStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowBspLoadStep::GetPluginId() const { + return "bsp.load"; +} + +void WorkflowBspLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + WorkflowStepParameterResolver params; + + auto getStr = [&](const char* name, const std::string& def) -> std::string { + const auto* p = params.FindParameter(step, name); + if (p && p->type == WorkflowParameterValue::Type::String) return p->stringValue; + auto it = step.inputs.find(name); + if (it != step.inputs.end()) { + const auto* ctx = context.TryGet(it->second); + if (ctx) return *ctx; + } + return def; + }; + auto getNum = [&](const char* name, float def) -> float { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + + const std::string pk3_path = getStr("pk3_path", ""); + const std::string map_name = getStr("map_name", "q3dm17"); + const float scale = getNum("scale", 1.0f / 32.0f); // Q3 units to meters (32 units = 1m) + + if (pk3_path.empty()) { + throw std::runtime_error("bsp.load: 'pk3_path' parameter required"); + } + + SDL_GPUDevice* device = context.Get("gpu_device", nullptr); + if (!device) throw std::runtime_error("bsp.load: GPU device not found"); + + // Open pk3 (zip) and extract BSP + int zip_err = 0; + zip_t* archive = zip_open(pk3_path.c_str(), ZIP_RDONLY, &zip_err); + if (!archive) { + throw std::runtime_error("bsp.load: Failed to open pk3: " + pk3_path); + } + + std::string bsp_entry = "maps/" + map_name + ".bsp"; + zip_stat_t st; + if (zip_stat(archive, bsp_entry.c_str(), 0, &st) != 0) { + zip_close(archive); + throw std::runtime_error("bsp.load: Map '" + bsp_entry + "' not found in " + pk3_path); + } + + std::vector bspData(st.size); + zip_file_t* zf = zip_fopen(archive, bsp_entry.c_str(), 0); + if (!zf) { + zip_close(archive); + throw std::runtime_error("bsp.load: Failed to open " + bsp_entry); + } + zip_fread(zf, bspData.data(), st.size); + zip_fclose(zf); + zip_close(archive); + + if (logger_) { + logger_->Info("bsp.load: Read " + bsp_entry + " (" + std::to_string(bspData.size()) + " bytes)"); + } + + // Parse BSP header + if (bspData.size() < sizeof(BspHeader) + sizeof(BspLump) * NUM_LUMPS) { + throw std::runtime_error("bsp.load: BSP file too small"); + } + + auto* header = reinterpret_cast(bspData.data()); + if (std::memcmp(header->magic, "IBSP", 4) != 0 || header->version != 46) { + throw std::runtime_error("bsp.load: Not a valid Q3 BSP (magic/version mismatch)"); + } + + auto* lumps = reinterpret_cast(bspData.data() + sizeof(BspHeader)); + + // Extract vertices + const auto& vtxLump = lumps[LUMP_VERTICES]; + int numVertices = vtxLump.length / static_cast(sizeof(BspVertex)); + auto* bspVertices = reinterpret_cast(bspData.data() + vtxLump.offset); + + // Extract faces + const auto& faceLump = lumps[LUMP_FACES]; + int numFaces = faceLump.length / static_cast(sizeof(BspFace)); + auto* bspFaces = reinterpret_cast(bspData.data() + faceLump.offset); + + // Extract mesh vertices (index offsets) + const auto& mvLump = lumps[LUMP_MESHVERTS]; + int numMeshVerts = mvLump.length / 4; + auto* meshVerts = reinterpret_cast(bspData.data() + mvLump.offset); + + // Extract textures + const auto& texLump = lumps[LUMP_TEXTURES]; + int numTextures = texLump.length / static_cast(sizeof(BspTexture)); + auto* bspTextures = reinterpret_cast(bspData.data() + texLump.offset); + + // Convert to our vertex format: float3 pos + float2 uv = 20 bytes + struct PosUvVertex { float x, y, z, u, v; }; + + std::vector vertices; + std::vector indices; + vertices.reserve(numVertices); + + // Convert Q3 vertices (swap Y/Z for Q3→OpenGL coordinate system) + for (int i = 0; i < numVertices; ++i) { + PosUvVertex v; + v.x = bspVertices[i].position[0] * scale; + v.y = bspVertices[i].position[2] * scale; // Q3 Z-up → Y-up + v.z = -bspVertices[i].position[1] * scale; // Q3 Y → -Z + v.u = bspVertices[i].texcoord[0][0]; + v.v = bspVertices[i].texcoord[0][1]; + vertices.push_back(v); + } + + // Build indices from faces (type 1=polygon, type 3=mesh - both use meshverts) + int skippedPatches = 0; + for (int f = 0; f < numFaces; ++f) { + const auto& face = bspFaces[f]; + + // Skip sky, trigger, clip textures + if (face.texture >= 0 && face.texture < numTextures) { + const auto& tex = bspTextures[face.texture]; + if (tex.contents & 0x200000) continue; // CONTENTS_TRANSLUCENT + std::string texName(tex.name); + if (texName.find("sky") != std::string::npos) continue; + if (texName.find("clip") != std::string::npos) continue; + if (texName.find("trigger") != std::string::npos) continue; + if (texName.find("hint") != std::string::npos) continue; + if (texName.find("caulk") != std::string::npos) continue; + } + + if (face.type == 1 || face.type == 3) { + // Polygon or mesh face - use meshvert indices + for (int mv = 0; mv < face.n_meshverts; ++mv) { + int idx = face.vertex + meshVerts[face.meshvert + mv]; + if (idx >= 0 && idx < static_cast(vertices.size()) && idx <= 65535) { + indices.push_back(static_cast(idx)); + } + } + } else if (face.type == 2) { + skippedPatches++; + // TODO: tessellate bezier patches + } + } + + if (logger_) { + logger_->Info("bsp.load: " + std::to_string(numVertices) + " vertices, " + + std::to_string(indices.size()) + " indices, " + + std::to_string(numFaces) + " faces (" + + std::to_string(skippedPatches) + " patches skipped)"); + } + + if (vertices.empty() || indices.empty()) { + throw std::runtime_error("bsp.load: No renderable geometry found"); + } + + // Upload to GPU as a single mesh + uint32_t vtxSize = static_cast(vertices.size() * sizeof(PosUvVertex)); + uint32_t idxSize = static_cast(indices.size() * sizeof(uint16_t)); + + SDL_GPUBufferCreateInfo vbInfo = {}; + vbInfo.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + vbInfo.size = vtxSize; + SDL_GPUBuffer* vb = SDL_CreateGPUBuffer(device, &vbInfo); + + SDL_GPUBufferCreateInfo ibInfo = {}; + ibInfo.usage = SDL_GPU_BUFFERUSAGE_INDEX; + ibInfo.size = idxSize; + SDL_GPUBuffer* ib = SDL_CreateGPUBuffer(device, &ibInfo); + + SDL_GPUTransferBufferCreateInfo tbInfo = {}; + tbInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbInfo.size = vtxSize + idxSize; + SDL_GPUTransferBuffer* tb = SDL_CreateGPUTransferBuffer(device, &tbInfo); + + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(device, tb, false)); + std::memcpy(mapped, vertices.data(), vtxSize); + std::memcpy(mapped + vtxSize, indices.data(), idxSize); + SDL_UnmapGPUTransferBuffer(device, tb); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device); + SDL_GPUCopyPass* cp = SDL_BeginGPUCopyPass(cmd); + + SDL_GPUTransferBufferLocation srcV = {}; srcV.transfer_buffer = tb; + SDL_GPUBufferRegion dstV = {}; dstV.buffer = vb; dstV.size = vtxSize; + SDL_UploadToGPUBuffer(cp, &srcV, &dstV, false); + + SDL_GPUTransferBufferLocation srcI = {}; srcI.transfer_buffer = tb; srcI.offset = vtxSize; + SDL_GPUBufferRegion dstI = {}; dstI.buffer = ib; dstI.size = idxSize; + SDL_UploadToGPUBuffer(cp, &srcI, &dstI, false); + + SDL_EndGPUCopyPass(cp); + SDL_SubmitGPUCommandBuffer(cmd); + SDL_ReleaseGPUTransferBuffer(device, tb); + + // Store as map mesh (compatible with draw.map) + std::string meshName = "bsp_" + map_name; + context.Set("plane_" + meshName + "_vb", vb); + context.Set("plane_" + meshName + "_ib", ib); + context.Set("plane_" + meshName, nlohmann::json{ + {"vertex_count", vertices.size()}, + {"index_count", indices.size()}, + {"stride", 20} + }); + + // Create Bullet physics triangle mesh from BSP geometry + auto* world = context.Get("physics_world", nullptr); + if (world) { + auto* triMesh = new btTriangleMesh(); + for (size_t i = 0; i + 2 < indices.size(); i += 3) { + const auto& v0 = vertices[indices[i]]; + const auto& v1 = vertices[indices[i + 1]]; + const auto& v2 = vertices[indices[i + 2]]; + triMesh->addTriangle( + btVector3(v0.x, v0.y, v0.z), + btVector3(v1.x, v1.y, v1.z), + btVector3(v2.x, v2.y, v2.z)); + } + + auto* triShape = new btBvhTriangleMeshShape(triMesh, true); + btTransform startTransform; + startTransform.setIdentity(); + auto* motionState = new btDefaultMotionState(startTransform); + btRigidBody::btRigidBodyConstructionInfo rbInfo(0.0f, motionState, triShape); + auto* body = new btRigidBody(rbInfo); + world->addRigidBody(body); + context.Set("physics_body_bsp_" + map_name, body); + + if (logger_) { + logger_->Info("bsp.load: Created collision mesh (" + + std::to_string(triMesh->getNumTriangles()) + " triangles)"); + } + } + + // Store as map.nodes array for draw.map compatibility + nlohmann::json mapNodes = nlohmann::json::array(); + mapNodes.push_back({ + {"name", meshName}, + {"index_count", indices.size()} + }); + context.Set("map.nodes", mapNodes); + + if (logger_) { + logger_->Info("bsp.load: '" + map_name + "' ready (" + + std::to_string(vertices.size()) + " verts, " + + std::to_string(indices.size() / 3) + " triangles)"); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp index d3cd4ea80..86aff6367 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp @@ -103,12 +103,13 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo auto* meshSamp = context.Get(texKey + "_sampler", nullptr); if (!meshTex || !meshSamp) continue; - if (shadow_tex && shadow_samp) { + { SDL_GPUTextureSamplerBinding bindings[2] = {}; bindings[0].texture = meshTex; bindings[0].sampler = meshSamp; - bindings[1].texture = shadow_tex; - bindings[1].sampler = shadow_samp; + // Use shadow map if available, otherwise dummy bind the same texture + bindings[1].texture = shadow_tex ? shadow_tex : meshTex; + bindings[1].sampler = shadow_samp ? shadow_samp : meshSamp; SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2); } diff --git a/gameengine/src/services/impl/workflow/workflow_registrar.cpp b/gameengine/src/services/impl/workflow/workflow_registrar.cpp index 6d2a7c638..e4cfef9e6 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -44,6 +44,7 @@ #include "services/interfaces/workflow/rendering/workflow_map_load_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_map_step.hpp" #include "services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_textured_box_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_setup_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_pass_step.hpp" @@ -282,6 +283,7 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr reg registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp new file mode 100644 index 000000000..4b26d1366 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowBspLoadStep : public IWorkflowStep { +public: + explicit WorkflowBspLoadStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl