mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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
|
||||
|
||||
BIN
gameengine/packages/seed/map.glb
Normal file
BIN
gameengine/packages/seed/map.glb
Normal file
Binary file not shown.
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
238
gameengine/python/export_room_gltf.py
Normal file
238
gameengine/python/export_room_gltf.py
Normal 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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_));
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user