mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat(gameengine): Quake 3 BSP map loader with Bullet physics collision
- bsp.load: parse Q3 BSP from pk3 (zip) archives via libzip - Extracts vertices, faces, meshverts from BSP lumps - Coordinate conversion: Q3 Z-up → engine Y-up - Configurable scale (default 1/32 = Q3 units to meters) - Skips sky, clip, trigger, caulk, hint textures - Bullet btBvhTriangleMeshShape collision from BSP geometry - draw.map: graceful fallback when shadow texture missing - Q3 game workflow: FPS controls, 90° FOV, walk around maps - Tested with q3dm17 (The Longest Yard): 8486 verts, 5128 tris - libzip added to cmake dependencies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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"
|
||||
|
||||
129
gameengine/packages/quake3/workflows/q3_frame.json
Normal file
129
gameengine/packages/quake3/workflows/q3_frame.json
Normal file
@@ -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 }] } }
|
||||
}
|
||||
}
|
||||
68
gameengine/packages/quake3/workflows/q3_game.json
Normal file
68
gameengine/packages/quake3/workflows/q3_game.json
Normal file
@@ -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 }] } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
#include "services/interfaces/workflow/rendering/workflow_bsp_load_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <btBulletDynamicsCommon.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <zip.h>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
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<ILogger> 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<std::string>(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<float>(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<SDL_GPUDevice*>("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<uint8_t> 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<const BspHeader*>(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<const BspLump*>(bspData.data() + sizeof(BspHeader));
|
||||
|
||||
// Extract vertices
|
||||
const auto& vtxLump = lumps[LUMP_VERTICES];
|
||||
int numVertices = vtxLump.length / static_cast<int>(sizeof(BspVertex));
|
||||
auto* bspVertices = reinterpret_cast<const BspVertex*>(bspData.data() + vtxLump.offset);
|
||||
|
||||
// Extract faces
|
||||
const auto& faceLump = lumps[LUMP_FACES];
|
||||
int numFaces = faceLump.length / static_cast<int>(sizeof(BspFace));
|
||||
auto* bspFaces = reinterpret_cast<const BspFace*>(bspData.data() + faceLump.offset);
|
||||
|
||||
// Extract mesh vertices (index offsets)
|
||||
const auto& mvLump = lumps[LUMP_MESHVERTS];
|
||||
int numMeshVerts = mvLump.length / 4;
|
||||
auto* meshVerts = reinterpret_cast<const int32_t*>(bspData.data() + mvLump.offset);
|
||||
|
||||
// Extract textures
|
||||
const auto& texLump = lumps[LUMP_TEXTURES];
|
||||
int numTextures = texLump.length / static_cast<int>(sizeof(BspTexture));
|
||||
auto* bspTextures = reinterpret_cast<const BspTexture*>(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<PosUvVertex> vertices;
|
||||
std::vector<uint16_t> 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<int>(vertices.size()) && idx <= 65535) {
|
||||
indices.push_back(static_cast<uint16_t>(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<uint32_t>(vertices.size() * sizeof(PosUvVertex));
|
||||
uint32_t idxSize = static_cast<uint32_t>(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<uint8_t*>(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<SDL_GPUBuffer*>("plane_" + meshName + "_vb", vb);
|
||||
context.Set<SDL_GPUBuffer*>("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<btDiscreteDynamicsWorld*>("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<btRigidBody*>("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
|
||||
@@ -103,12 +103,13 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
|
||||
auto* meshSamp = context.Get<SDL_GPUSampler*>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IWorkflowStepRegistry> reg
|
||||
registry->RegisterStep(std::make_shared<WorkflowMapLoadStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowDrawMapStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowPostfxTaaStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowBspLoadStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowShadowSetupStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowShadowPassStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowRenderPrepareStep>(logger_));
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowBspLoadStep : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowBspLoadStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
Reference in New Issue
Block a user