feat(gameengine): glTF map loading, draw.map with JSON texture mapping

- map.load: Assimp-based glTF/GLB scene loader with auto physics generation
- draw.map: renders all map meshes with JSON-driven texture-to-mesh mapping
- export_room_gltf.py: exports seed workflow physics bodies to GLB
- Seed demo room exported as map.glb (14 static meshes)
- Texture mapping configurable per mesh name pattern in workflow JSON
- Maps can be edited in Blender and re-exported

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 10:16:37 +00:00
parent 5805fcc17a
commit 915e18d67b
10 changed files with 686 additions and 2 deletions

View File

@@ -281,6 +281,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_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
src/services/impl/workflow/rendering/workflow_draw_viewmodel_step.cpp
@@ -291,6 +292,7 @@ if(BUILD_SDL3_APP)
src/services/impl/workflow/rendering/workflow_frame_end_scene_step.cpp
src/services/impl/workflow/rendering/workflow_geometry_create_flashlight_step.cpp
src/services/impl/workflow/rendering/workflow_lighting_setup_step.cpp
src/services/impl/workflow/rendering/workflow_map_load_step.cpp
src/services/impl/workflow/rendering/workflow_model_load_step.cpp
src/services/impl/workflow/rendering/workflow_postfx_bloom_blur_step.cpp
src/services/impl/workflow/rendering/workflow_postfx_bloom_extract_step.cpp

Binary file not shown.

View File

@@ -108,6 +108,21 @@
"typeVersion": 1,
"position": [1000, 0]
},
{
"id": "draw_map",
"name": "Draw Map (glTF)",
"type": "draw.map",
"typeVersion": 1,
"position": [1050, 0],
"parameters": {
"default_texture": "walls_texture",
"floor": "floor_texture",
"platform": "floor_texture",
"ceiling": "ceiling_texture",
"roughness": 0.8,
"metallic": 0.0
}
},
{
"id": "draw_floor",
"name": "Draw Floor",
@@ -502,7 +517,12 @@
},
"draw_bodies": {
"main": {
"0": [{ "node": "draw_floor", "type": "main", "index": 0 }]
"0": [{ "node": "draw_map", "type": "main", "index": 0 }]
}
},
"draw_map": {
"main": {
"0": [{ "node": "draw_torch", "type": "main", "index": 0 }]
}
},
"draw_floor": {

View File

@@ -330,6 +330,18 @@
"texture": "ceiling_texture"
}
},
{
"id": "load_map",
"name": "Load Map (glTF)",
"type": "map.load",
"typeVersion": 1,
"position": [750, 100],
"parameters": {
"file_path": "packages/seed/map.glb",
"scale": 1.0,
"create_physics": 1
}
},
{
"id": "plane_floor",
"name": "Create Floor Plane",
@@ -1226,7 +1238,12 @@
},
"tex_ceiling": {
"main": {
"0": [{ "node": "plane_floor", "type": "main", "index": 0 }]
"0": [{ "node": "load_map", "type": "main", "index": 0 }]
}
},
"load_map": {
"main": {
"0": [{ "node": "player", "type": "main", "index": 0 }]
}
},
"plane_floor": {

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Export the seed demo room as a glTF/GLB file.
Reads the seed_game.json workflow and converts all physics bodies
(boxes) into a glTF scene with meshes. The output can be opened
in Blender, edited, and re-exported for the engine to load.
Usage:
python export_room_gltf.py [--output packages/seed/map.glb]
"""
import argparse
import json
import struct
import math
from pathlib import Path
def create_box_mesh(sx, sy, sz):
"""Create a unit box mesh scaled by sx, sy, sz. Returns (vertices, indices).
Vertex format: position(3) + normal(3) + uv(2) = 8 floats per vertex.
"""
hx, hy, hz = sx / 2, sy / 2, sz / 2
# 6 faces, 4 vertices each = 24 vertices
# Each face has its own normal for flat shading
faces = [
# front (+Z)
(( hx, hy, hz), (-hx, hy, hz), (-hx, -hy, hz), ( hx, -hy, hz), ( 0, 0, 1)),
# back (-Z)
((-hx, hy, -hz), ( hx, hy, -hz), ( hx, -hy, -hz), (-hx, -hy, -hz), ( 0, 0, -1)),
# right (+X)
(( hx, hy, -hz), ( hx, hy, hz), ( hx, -hy, hz), ( hx, -hy, -hz), ( 1, 0, 0)),
# left (-X)
((-hx, hy, hz), (-hx, hy, -hz), (-hx, -hy, -hz), (-hx, -hy, hz), (-1, 0, 0)),
# top (+Y)
(( hx, hy, -hz), (-hx, hy, -hz), (-hx, hy, hz), ( hx, hy, hz), ( 0, 1, 0)),
# bottom (-Y)
(( hx, -hy, hz), (-hx, -hy, hz), (-hx, -hy, -hz), ( hx, -hy, -hz), ( 0, -1, 0)),
]
vertices = []
indices = []
uvs_face = [(1, 0), (0, 0), (0, 1), (1, 1)]
for face_idx, (v0, v1, v2, v3, n) in enumerate(faces):
base = len(vertices)
for vi, (vx, vy, vz) in enumerate([v0, v1, v2, v3]):
u, v = uvs_face[vi]
# Scale UVs by face dimensions for tiling
if abs(n[0]) > 0.5: # X-facing
u *= sz
v *= sy
elif abs(n[1]) > 0.5: # Y-facing
u *= sx
v *= sz
else: # Z-facing
u *= sx
v *= sy
vertices.append((vx, vy, vz, n[0], n[1], n[2], u, v))
indices.extend([base, base + 1, base + 2, base, base + 2, base + 3])
return vertices, indices
def build_gltf(bodies, output_path):
"""Build a GLB file from a list of physics bodies."""
import io
nodes = []
meshes = []
accessors = []
buffer_views = []
bin_data = io.BytesIO()
for body in bodies:
name = body["name"]
px = body.get("pos_x", 0)
py = body.get("pos_y", 0)
pz = body.get("pos_z", 0)
sx = body.get("size_x", 1)
sy = body.get("size_y", 1)
sz = body.get("size_z", 1)
shape = body.get("shape", "box")
if shape == "capsule":
continue # Skip player
verts, idxs = create_box_mesh(sx, sy, sz)
# Write index data (uint16)
idx_offset = bin_data.tell()
for i in idxs:
bin_data.write(struct.pack("<H", i))
idx_size = bin_data.tell() - idx_offset
# Pad to 4-byte boundary
while bin_data.tell() % 4 != 0:
bin_data.write(b'\x00')
# Write vertex data (float32 x 8 per vertex)
vtx_offset = bin_data.tell()
pos_min = [1e9, 1e9, 1e9]
pos_max = [-1e9, -1e9, -1e9]
for v in verts:
bin_data.write(struct.pack("<8f", *v))
for j in range(3):
pos_min[j] = min(pos_min[j], v[j])
pos_max[j] = max(pos_max[j], v[j])
vtx_size = bin_data.tell() - vtx_offset
# Buffer views
idx_bv = len(buffer_views)
buffer_views.append({
"buffer": 0, "byteOffset": idx_offset,
"byteLength": idx_size, "target": 34963 # ELEMENT_ARRAY_BUFFER
})
vtx_bv = len(buffer_views)
buffer_views.append({
"buffer": 0, "byteOffset": vtx_offset,
"byteLength": vtx_size, "byteStride": 32,
"target": 34962 # ARRAY_BUFFER
})
# Accessors
idx_acc = len(accessors)
accessors.append({
"bufferView": idx_bv, "componentType": 5123, # UNSIGNED_SHORT
"count": len(idxs), "type": "SCALAR"
})
pos_acc = len(accessors)
accessors.append({
"bufferView": vtx_bv, "byteOffset": 0,
"componentType": 5126, "count": len(verts), # FLOAT
"type": "VEC3", "min": pos_min, "max": pos_max
})
nrm_acc = len(accessors)
accessors.append({
"bufferView": vtx_bv, "byteOffset": 12,
"componentType": 5126, "count": len(verts),
"type": "VEC3"
})
uv_acc = len(accessors)
accessors.append({
"bufferView": vtx_bv, "byteOffset": 24,
"componentType": 5126, "count": len(verts),
"type": "VEC2"
})
# Mesh
mesh_idx = len(meshes)
meshes.append({
"name": name,
"primitives": [{
"attributes": {
"POSITION": pos_acc,
"NORMAL": nrm_acc,
"TEXCOORD_0": uv_acc
},
"indices": idx_acc
}]
})
# Node with translation
nodes.append({
"name": name,
"mesh": mesh_idx,
"translation": [px, py, pz]
})
# Build glTF JSON
gltf = {
"asset": {"version": "2.0", "generator": "metabuilder-export"},
"scene": 0,
"scenes": [{"name": "SeedDemo", "nodes": list(range(len(nodes)))}],
"nodes": nodes,
"meshes": meshes,
"accessors": accessors,
"bufferViews": buffer_views,
"buffers": [{"byteLength": bin_data.tell()}]
}
# Write GLB
gltf_json = json.dumps(gltf, separators=(',', ':'))
# Pad JSON to 4-byte boundary
while len(gltf_json) % 4 != 0:
gltf_json += ' '
bin_bytes = bin_data.getvalue()
while len(bin_bytes) % 4 != 0:
bin_bytes += b'\x00'
total_size = 12 + 8 + len(gltf_json) + 8 + len(bin_bytes)
with open(output_path, 'wb') as f:
# GLB header
f.write(struct.pack("<III", 0x46546C67, 2, total_size))
# JSON chunk
f.write(struct.pack("<II", len(gltf_json), 0x4E4F534A))
f.write(gltf_json.encode('ascii'))
# BIN chunk
f.write(struct.pack("<II", len(bin_bytes), 0x004E4942))
f.write(bin_bytes)
print(f"Exported {len(nodes)} nodes to {output_path} ({total_size} bytes)")
def main():
parser = argparse.ArgumentParser(description="Export seed room as glTF/GLB")
parser.add_argument("--workflow", default="packages/seed/workflows/seed_game.json")
parser.add_argument("--output", default="packages/seed/map.glb")
args = parser.parse_args()
with open(args.workflow) as f:
workflow = json.load(f)
# Extract static level geometry only (mass=0, not player/dynamic)
skip_keywords = {"player", "cube", "crate"}
bodies = []
for node in workflow.get("nodes", []):
if node.get("type") == "physics.body.add":
params = node.get("parameters", {})
name = params.get("name", "")
mass = params.get("mass", 0)
# Only static bodies (mass=0), skip capsules and dynamic objects
if mass != 0 or params.get("shape") == "capsule":
continue
if any(kw in name for kw in skip_keywords):
continue
if params.get("spinning", 0):
continue
bodies.append(params)
build_gltf(bodies, args.output)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,142 @@
#include "services/interfaces/workflow/rendering/workflow_draw_map_step.hpp"
#include "services/interfaces/workflow/rendering/rendering_types.hpp"
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
#include <SDL3/SDL_gpu.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <nlohmann/json.hpp>
#include <cstring>
namespace sdl3cpp::services::impl {
WorkflowDrawMapStep::WorkflowDrawMapStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowDrawMapStep::GetPluginId() const {
return "draw.map";
}
void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
if (context.GetBool("frame_skip", false)) return;
auto* pass = context.Get<SDL_GPURenderPass*>("gpu_render_pass", nullptr);
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
auto* pipeline = context.Get<SDL_GPUGraphicsPipeline*>("gpu_pipeline_textured", nullptr);
if (!pass || !cmd || !pipeline) return;
const auto* mapNodes = context.TryGet<nlohmann::json>("map.nodes");
if (!mapNodes || !mapNodes->is_array()) return;
// Read texture mapping from parameters: "textures" object maps name patterns to texture keys
// e.g. { "floor": "floor_texture", "ceiling": "ceiling_texture", "*": "walls_texture" }
std::vector<std::pair<std::string, std::string>> textureMappings;
std::string defaultTexture;
for (const auto& [key, param] : step.parameters) {
if (key == "roughness" || key == "metallic") continue;
if (param.type == WorkflowParameterValue::Type::String) {
if (key == "default_texture") {
defaultTexture = param.stringValue;
} else {
// Key is the name pattern, value is the texture context key
textureMappings.emplace_back(key, param.stringValue);
}
}
}
if (defaultTexture.empty() && !textureMappings.empty()) {
defaultTexture = textureMappings.back().second;
}
WorkflowStepParameterResolver params;
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 float roughness = getNum("roughness", 0.8f);
const float metallic = getNum("metallic", 0.0f);
auto view = context.Get<glm::mat4>("render.view_matrix", glm::mat4(1.0f));
auto proj = context.Get<glm::mat4>("render.proj_matrix", glm::mat4(1.0f));
auto camPos = context.Get<glm::vec3>("render.camera_pos", glm::vec3(0.0f));
auto shadowVP = context.Get<glm::mat4>("render.shadow_vp", glm::mat4(1.0f));
auto fu = context.Get<rendering::FragmentUniformData>("render.frag_uniforms", rendering::FragmentUniformData{});
fu.material[0] = roughness;
fu.material[1] = metallic;
SDL_BindGPUGraphicsPipeline(pass, pipeline);
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 mvp = proj * view * model;
rendering::VertexUniformData vu = {};
std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16);
std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16);
vu.normal[0] = 0; vu.normal[1] = 1; vu.normal[2] = 0;
vu.uv_scale[0] = 1.0f; vu.uv_scale[1] = 1.0f;
vu.camera_pos[0] = camPos.x; vu.camera_pos[1] = camPos.y; vu.camera_pos[2] = camPos.z;
std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16);
auto* shadow_tex = context.Get<SDL_GPUTexture*>("shadow_depth_texture", nullptr);
auto* shadow_samp = context.Get<SDL_GPUSampler*>("shadow_depth_sampler", nullptr);
for (const auto& node : *mapNodes) {
std::string meshName = node["name"];
auto* vb = context.Get<SDL_GPUBuffer*>("plane_" + meshName + "_vb", nullptr);
auto* ib = context.Get<SDL_GPUBuffer*>("plane_" + meshName + "_ib", nullptr);
if (!vb || !ib) continue;
uint32_t indexCount = node["index_count"];
// Find matching texture from JSON mappings
std::string texKey = defaultTexture;
for (const auto& [pattern, texName] : textureMappings) {
if (meshName.find(pattern) != std::string::npos) {
texKey = texName;
break;
}
}
auto* meshTex = context.Get<SDL_GPUTexture*>(texKey + "_gpu", nullptr);
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;
SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2);
}
SDL_GPUBufferBinding vbBind = {}; vbBind.buffer = vb;
SDL_BindGPUVertexBuffers(pass, 0, &vbBind, 1);
SDL_GPUBufferBinding ibBind = {}; ibBind.buffer = ib;
SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Normal from bounding box thinnest axis
if (node.contains("bb_min") && node.contains("bb_max")) {
auto bbMin = node["bb_min"];
auto bbMax = node["bb_max"];
float dx = bbMax[0].get<float>() - bbMin[0].get<float>();
float dy = bbMax[1].get<float>() - bbMin[1].get<float>();
float dz = bbMax[2].get<float>() - bbMin[2].get<float>();
if (dy < dx && dy < dz) {
vu.normal[0] = 0; vu.normal[1] = 1; vu.normal[2] = 0;
} else if (dx < dz) {
vu.normal[0] = 1; vu.normal[1] = 0; vu.normal[2] = 0;
} else {
vu.normal[0] = 0; vu.normal[1] = 0; vu.normal[2] = 1;
}
}
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
SDL_DrawGPUIndexedPrimitives(pass, indexCount, 1, 0, 0, 0);
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,223 @@
#include "services/interfaces/workflow/rendering/workflow_map_load_step.hpp"
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
#include <SDL3/SDL_gpu.h>
#include <btBulletDynamicsCommon.h>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <nlohmann/json.hpp>
#include <cstring>
#include <cmath>
#include <stdexcept>
#include <vector>
#include <string>
namespace sdl3cpp::services::impl {
WorkflowMapLoadStep::WorkflowMapLoadStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowMapLoadStep::GetPluginId() const {
return "map.load";
}
void WorkflowMapLoadStep::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 file_path = getStr("file_path", "");
const float scale = getNum("scale", 1.0f);
const bool create_physics = static_cast<int>(getNum("create_physics", 1)) != 0;
if (file_path.empty()) {
throw std::runtime_error("map.load: 'file_path' parameter is required");
}
SDL_GPUDevice* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
if (!device) throw std::runtime_error("map.load: GPU device not found");
// Load scene with Assimp
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(file_path,
aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs |
aiProcess_JoinIdenticalVertices);
if (!scene || !scene->mRootNode) {
throw std::runtime_error("map.load: Failed to load '" + file_path + "': " +
importer.GetErrorString());
}
// Vertex format: float3 pos + float2 uv = 20 bytes (matches textured pipeline)
struct PosUvVertex { float x, y, z, u, v; };
auto* world = context.Get<btDiscreteDynamicsWorld*>("physics_world", nullptr);
nlohmann::json mapNodes = nlohmann::json::array();
int meshCount = 0;
// Process each node in the scene
std::function<void(const aiNode*, aiMatrix4x4)> processNode =
[&](const aiNode* node, aiMatrix4x4 parentTransform) {
aiMatrix4x4 transform = parentTransform * node->mTransformation;
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
std::string meshName = node->mName.C_Str();
if (meshName.empty()) meshName = "map_mesh_" + std::to_string(meshCount);
// Extract vertices with transform applied
std::vector<PosUvVertex> vertices;
aiVector3D bbMin(1e9f, 1e9f, 1e9f), bbMax(-1e9f, -1e9f, -1e9f);
for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
aiVector3D pos = transform * mesh->mVertices[i];
pos *= scale;
PosUvVertex vert;
vert.x = pos.x;
vert.y = pos.y;
vert.z = pos.z;
if (mesh->mTextureCoords[0]) {
vert.u = mesh->mTextureCoords[0][i].x;
vert.v = mesh->mTextureCoords[0][i].y;
} else {
vert.u = 0.0f;
vert.v = 0.0f;
}
vertices.push_back(vert);
bbMin.x = std::min(bbMin.x, pos.x);
bbMin.y = std::min(bbMin.y, pos.y);
bbMin.z = std::min(bbMin.z, pos.z);
bbMax.x = std::max(bbMax.x, pos.x);
bbMax.y = std::max(bbMax.y, pos.y);
bbMax.z = std::max(bbMax.z, pos.z);
}
std::vector<uint16_t> indices;
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
const aiFace& face = mesh->mFaces[f];
for (unsigned int j = 0; j < face.mNumIndices; ++j) {
indices.push_back(static_cast<uint16_t>(face.mIndices[j]));
}
}
if (vertices.empty()) continue;
// Upload to GPU
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 with plane_ prefix (compatible with draw.textured)
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 physics body from bounding box
if (create_physics && world) {
float cx = (bbMin.x + bbMax.x) * 0.5f;
float cy = (bbMin.y + bbMax.y) * 0.5f;
float cz = (bbMin.z + bbMax.z) * 0.5f;
float hx = (bbMax.x - bbMin.x) * 0.5f;
float hy = (bbMax.y - bbMin.y) * 0.5f;
float hz = (bbMax.z - bbMin.z) * 0.5f;
if (hx > 0.01f && hy > 0.01f && hz > 0.01f) {
auto* shape = new btBoxShape(btVector3(hx, hy, hz));
btTransform startTransform;
startTransform.setIdentity();
startTransform.setOrigin(btVector3(cx, cy, cz));
auto* motionState = new btDefaultMotionState(startTransform);
btRigidBody::btRigidBodyConstructionInfo rbInfo(0.0f, motionState, shape);
auto* body = new btRigidBody(rbInfo);
world->addRigidBody(body);
context.Set<btRigidBody*>("physics_body_" + meshName, body);
}
}
// Track for the frame loop draw step
nlohmann::json nodeInfo = {
{"name", meshName},
{"index_count", indices.size()},
{"bb_min", {bbMin.x, bbMin.y, bbMin.z}},
{"bb_max", {bbMax.x, bbMax.y, bbMax.z}}
};
mapNodes.push_back(nodeInfo);
meshCount++;
}
// Process children
for (unsigned int c = 0; c < node->mNumChildren; ++c) {
processNode(node->mChildren[c], transform);
}
};
aiMatrix4x4 identity;
processNode(scene->mRootNode, identity);
context.Set("map.nodes", mapNodes);
if (logger_) {
logger_->Info("map.load: Loaded '" + file_path + "' (" +
std::to_string(meshCount) + " meshes, " +
std::to_string(scene->mNumMeshes) + " total in file)");
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -41,6 +41,8 @@
#include "services/interfaces/workflow/rendering/workflow_model_load_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp"
#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_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"
@@ -276,6 +278,8 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
registry->RegisterStep(std::make_shared<WorkflowModelLoadStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowDrawViewmodelStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowGeometryCreateFlashlightStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowMapLoadStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowDrawMapStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowShadowSetupStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowShadowPassStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowRenderPrepareStep>(logger_));

View File

@@ -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 WorkflowDrawMapStep : public IWorkflowStep {
public:
explicit WorkflowDrawMapStep(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

View File

@@ -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 WorkflowMapLoadStep : public IWorkflowStep {
public:
explicit WorkflowMapLoadStep(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