diff --git a/CMakeLists.txt b/CMakeLists.txt index e311e2e..b13fd73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,12 +136,14 @@ find_package(EnTT CONFIG REQUIRED) find_package(bgfx CONFIG REQUIRED) find_package(materialx CONFIG REQUIRED) find_package(Freetype CONFIG REQUIRED) +find_package(lunasvg CONFIG REQUIRED) find_package(assimp CONFIG REQUIRED) find_package(libzip CONFIG REQUIRED) find_package(Bullet CONFIG REQUIRED) find_package(Vorbis CONFIG REQUIRED) find_package(glm CONFIG REQUIRED) find_package(cpptrace REQUIRED) +find_package(stb CONFIG REQUIRED) set(SDL3CPP_RENDER_STACK_LIBS EnTT::EnTT) if(TARGET bgfx::bgfx) @@ -187,11 +189,21 @@ if(TARGET libzip::zip) elseif(TARGET libzip::libzip) list(APPEND SDL3CPP_ZIP_LIBS libzip::libzip) endif() + +set(SDL3CPP_STB_LIBS) +if(TARGET stb::stb_image) + list(APPEND SDL3CPP_STB_LIBS stb::stb_image) +elseif(TARGET stb::stb) + list(APPEND SDL3CPP_STB_LIBS stb::stb) +elseif(TARGET stb) + list(APPEND SDL3CPP_STB_LIBS stb) +endif() endif() if(BUILD_SDL3_APP) add_executable(sdl3_app src/main.cpp + src/stb_image.cpp src/di/service_registry.cpp src/events/event_bus.cpp src/services/impl/json_config_service.cpp @@ -206,6 +218,7 @@ if(BUILD_SDL3_APP) src/services/impl/shader_script_service.cpp src/services/impl/materialx_shader_generator.cpp src/services/impl/gui_script_service.cpp + $<$>:src/services/impl/bgfx_gui_service.cpp> src/services/impl/audio_command_service.cpp src/services/impl/physics_bridge_service.cpp src/services/impl/mesh_service.cpp @@ -217,6 +230,7 @@ if(BUILD_SDL3_APP) src/services/impl/application_loop_service.cpp src/services/impl/render_coordinator_service.cpp src/services/impl/null_gui_service.cpp + src/services/impl/bgfx_gui_service.cpp src/services/impl/bullet_physics_service.cpp src/services/impl/scene_service.cpp src/services/impl/graphics_service.cpp @@ -230,14 +244,17 @@ if(BUILD_SDL3_APP) lua::lua CLI11::CLI11 rapidjson + ${SDL3CPP_STB_LIBS} ${SDL3CPP_RENDER_STACK_LIBS} ${SDL3CPP_FREETYPE_LIBS} + $<$>:lunasvg::lunasvg> ${SDL3CPP_ZIP_LIBS} assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis + lunasvg::lunasvg cpptrace::cpptrace ) if(NOT ENABLE_VITA) @@ -249,7 +266,8 @@ if(BUILD_SDL3_APP) message(FATAL_ERROR "shaderc CMake target not found") endif() endif() - target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED) + target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED + $<$:SDL3CPP_ENABLE_VITA>) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/shaders") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/shaders" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/conanfile.py b/conanfile.py index 5aac29c..7e2e4d0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -34,6 +34,7 @@ class SDL3CppConan(ConanFile): "ffmpeg/8.0.1", "cairo/1.18.0", "libzip/1.10.1", + "stb/cci.20230920", ) RENDER_STACK_REQUIRES = ( "bgfx/1.129.8930-495", diff --git a/config/gui_runtime.json b/config/gui_runtime.json index adfd8a7..c86fbc6 100644 --- a/config/gui_runtime.json +++ b/config/gui_runtime.json @@ -61,6 +61,11 @@ }, "gamepad_axis_action_threshold": 0.5 }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "gui_opacity": 1.0, "config_file": "config/gui_runtime.json" } diff --git a/config/quake3_runtime.json b/config/quake3_runtime.json index 0367381..42ee707 100644 --- a/config/quake3_runtime.json +++ b/config/quake3_runtime.json @@ -60,6 +60,11 @@ }, "gamepad_axis_action_threshold": 0.5 }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "quake3": { "pk3_path": "/home/rewrich/Documents/GitHub/q3/pak0.pk3", "map_path": "q3dm1", diff --git a/config/seed_runtime.json b/config/seed_runtime.json index df2ba84..76a7696 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -83,6 +83,28 @@ 1.0 ] }, + "materialx_materials": [ + { + "shader_key": "floor", + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx", + "material": "Tiled_Wood" + }, + { + "shader_key": "wall", + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx", + "material": "M_BrickPattern" + }, + { + "shader_key": "ceiling", + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx", + "material": "Marble_3D" + }, + { + "shader_key": "solid", + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx", + "material": "Tiled_Brass" + } + ], "atmospherics": { "ambient_strength": 0.006, "fog_density": 0.006, @@ -97,6 +119,11 @@ "pbr_roughness": 0.28, "pbr_metallic": 0.08 }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "gui_opacity": 1.0, "config_file": "config/seed_runtime.json" } diff --git a/config/soundboard_runtime.json b/config/soundboard_runtime.json index 8be8276..1316ee2 100644 --- a/config/soundboard_runtime.json +++ b/config/soundboard_runtime.json @@ -13,6 +13,11 @@ "bgfx": { "renderer": "vulkan" }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "config_file": "config/soundboard_runtime.json", "input_bindings": { "move_forward": "W", diff --git a/config/vita_cube_runtime.json b/config/vita_cube_runtime.json index cc14bdc..80db982 100644 --- a/config/vita_cube_runtime.json +++ b/config/vita_cube_runtime.json @@ -52,6 +52,11 @@ "enabled": false } }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "camera": { "fov_degrees": 75, "near_clip": 0.1, diff --git a/config/vita_gui_runtime.json b/config/vita_gui_runtime.json index 9bef9e9..25443dc 100644 --- a/config/vita_gui_runtime.json +++ b/config/vita_gui_runtime.json @@ -45,10 +45,15 @@ } } }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "performance": { "target_fps": 60, "vsync": true, "frame_skip_allowed": false, "texture_compression": "vita_compatible" } -} \ No newline at end of file +} diff --git a/config/vita_soundboard_runtime.json b/config/vita_soundboard_runtime.json index eec54c4..6cabc40 100644 --- a/config/vita_soundboard_runtime.json +++ b/config/vita_soundboard_runtime.json @@ -42,6 +42,11 @@ } } }, + "gui_font": { + "use_freetype": true, + "font_path": "scripts/assets/fonts/Roboto-Regular.ttf", + "font_size": 18.0 + }, "audio": { "master_volume": 0.8, "music_volume": 1.0, @@ -67,4 +72,4 @@ "low_power_mode": false, "background_audio": true } -} \ No newline at end of file +} diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index be26a03..a83b869 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -202,6 +202,11 @@ local flight_layout = { height = 28, spacing = 8, } +local physics_layout = { + width = 140, + height = 28, + spacing = 8, +} local ui_state = { flyUpActive = false, flyDownActive = false, @@ -271,6 +276,36 @@ local player_state = { noclip_toggle_pressed = false, } +local function physics_is_available() + return type(physics_create_box) == "function" + and type(physics_step_simulation) == "function" + and type(physics_get_transform) == "function" + and type(math3d.from_transform) == "function" +end + +local physics_cube_half_extents = {1.5, 1.5, 1.5} +local physics_cube_scale = {physics_cube_half_extents[1], physics_cube_half_extents[2], physics_cube_half_extents[3]} +local physics_cube_spawn = { + 0.0, + room.floor_top + room.wall_height + physics_cube_half_extents[2] + 0.5, + 0.0, +} + +local physics_state = { + enabled = physics_is_available(), + ready = false, + last_step_time = nil, + max_sub_steps = 10, + cube_name = "demo_cube", + cube_half_extents = physics_cube_half_extents, + cube_scale = physics_cube_scale, + cube_mass = 1.0, + cube_color = {0.92, 0.34, 0.28}, + cube_spawn = physics_cube_spawn, + kick_strength = 6.0, + gravity = {0.0, -9.8, 0.0}, +} + camera.position[1] = 0.0 camera.position[2] = room.floor_top + player_state.eye_height camera.position[3] = 10.0 @@ -294,6 +329,104 @@ local function scale_matrix(x, y, z) } end +local function ensure_physics_setup() + if not physics_state.enabled then + return false + end + if physics_state.ready then + return true + end + + physics_state.last_step_time = nil + if type(physics_clear) == "function" then + physics_clear() + end + if type(physics_set_gravity) == "function" then + local ok, err = physics_set_gravity(physics_state.gravity) + if not ok then + log_debug("Physics gravity failed: %s", err or "unknown") + end + end + + local rotation = {0.0, 0.0, 0.0, 1.0} + local function add_static_body(name, half_extents, origin) + local ok, err = physics_create_box(name, half_extents, 0.0, origin, rotation) + if not ok then + log_debug("Physics static body %s failed: %s", name, err or "unknown") + end + end + + local floor_center_y = room.floor_top - room.floor_half_thickness + local wall_center_y = room.floor_top + room.wall_height + local ceiling_y = room.floor_top + room.wall_height * 2 + room.floor_half_thickness + local wall_offset = room.half_size + room.wall_thickness + + add_static_body("room_floor", + {room.half_size, room.floor_half_thickness, room.half_size}, + {0.0, floor_center_y, 0.0}) + add_static_body("room_ceiling", + {room.half_size, room.floor_half_thickness, room.half_size}, + {0.0, ceiling_y, 0.0}) + add_static_body("room_wall_north", + {room.half_size, room.wall_height, room.wall_thickness}, + {0.0, wall_center_y, -wall_offset}) + add_static_body("room_wall_south", + {room.half_size, room.wall_height, room.wall_thickness}, + {0.0, wall_center_y, wall_offset}) + add_static_body("room_wall_west", + {room.wall_thickness, room.wall_height, room.half_size}, + {-wall_offset, wall_center_y, 0.0}) + add_static_body("room_wall_east", + {room.wall_thickness, room.wall_height, room.half_size}, + {wall_offset, wall_center_y, 0.0}) + + local ok, err = physics_create_box( + physics_state.cube_name, + physics_state.cube_half_extents, + physics_state.cube_mass, + physics_state.cube_spawn, + rotation) + if not ok then + log_debug("Physics cube create failed: %s", err or "unknown") + return false + end + + if type(physics_set_linear_velocity) == "function" then + physics_set_linear_velocity(physics_state.cube_name, {0.0, 0.0, 0.0}) + end + + physics_state.ready = true + log_debug("Physics demo initialized") + return true +end + +local function step_physics(time) + if not physics_state.ready then + return + end + if type(time) ~= "number" then + return + end + if physics_state.last_step_time == time then + return + end + + local dt = 0.0 + if physics_state.last_step_time then + dt = time - physics_state.last_step_time + end + physics_state.last_step_time = time + + if dt <= 0.0 then + return + end + if dt > 0.1 then + dt = 0.1 + end + + physics_step_simulation(dt, physics_state.max_sub_steps) +end + local function normalize(vec) local x, y, z = vec[1], vec[2], vec[3] local len = math.sqrt(x * x + y * y + z * z) @@ -332,6 +465,49 @@ local function forward_from_angles(yaw, pitch) } end +local function reset_physics_cube() + if not physics_state.ready then + return + end + if type(physics_set_transform) ~= "function" then + return + end + local rotation = {0.0, 0.0, 0.0, 1.0} + local ok, err = physics_set_transform( + physics_state.cube_name, + physics_state.cube_spawn, + rotation) + if not ok then + log_debug("Physics reset failed: %s", err or "unknown") + return + end + if type(physics_set_linear_velocity) == "function" then + physics_set_linear_velocity(physics_state.cube_name, {0.0, 0.0, 0.0}) + end + physics_state.last_step_time = nil +end + +local function kick_physics_cube() + if not physics_state.ready then + return + end + if type(physics_apply_impulse) ~= "function" then + return + end + local forward = forward_from_angles(camera.yaw, camera.pitch) + local lift = math.max(forward[2], 0.2) + local direction = normalize({forward[1], lift, forward[3]}) + local impulse = { + direction[1] * physics_state.kick_strength, + direction[2] * physics_state.kick_strength, + direction[3] * physics_state.kick_strength, + } + local ok, err = physics_apply_impulse(physics_state.cube_name, impulse) + if not ok then + log_debug("Physics impulse failed: %s", err or "unknown") + end +end + local atan2_available = type(math.atan2) == "function" if not atan2_available then log_debug("math.atan2 unavailable; using fallback for compass heading") @@ -540,6 +716,7 @@ local function apply_color_to_vertices(color) position = v.position, normal = v.normal, color = color, + texcoord = v.texcoord, } end return colored_vertices @@ -579,6 +756,39 @@ local function create_skybox() } end +local function create_physics_cube() + if not ensure_physics_setup() then + return nil + end + local shader_key = resolve_material_shader() + local last_matrix = math3d.identity() + + local function compute_model_matrix(time) + step_physics(time) + local transform, err = physics_get_transform(physics_state.cube_name) + if not transform then + if lua_debug then + log_debug("physics_get_transform failed: %s", err or "unknown") + end + return last_matrix + end + local matrix = math3d.from_transform(transform.position, transform.rotation) + matrix = math3d.multiply(matrix, scale_matrix( + physics_state.cube_scale[1], + physics_state.cube_scale[2], + physics_state.cube_scale[3])) + last_matrix = matrix + return matrix + end + + return { + vertices = apply_color_to_vertices(physics_state.cube_color), + indices = (#cube_indices_double_sided > 0) and cube_indices_double_sided or cube_indices, + compute_model_matrix = compute_model_matrix, + shader_key = shader_key, + } +end + local function create_spinning_cube() local shader_key = resolve_material_shader() log_debug("Spinning cube shader=%s", shader_key) @@ -597,11 +807,19 @@ local function create_spinning_cube() } end +local function create_dynamic_cube() + local physics_cube = create_physics_cube() + if physics_cube then + return physics_cube + end + return create_spinning_cube() +end + local function create_lantern(x, z) local lantern_height = 8 local lantern_size = 0.2 return create_static_cube({x, lantern_height, z}, - {lantern_size, lantern_size, lantern_size}, {1.0, 0.9, 0.6}) + {lantern_size, lantern_size, lantern_size}, {1.0, 0.9, 0.6}, "solid") end local function create_room_objects() @@ -766,13 +984,53 @@ local function draw_flight_buttons() ui_state.flyDownPulse = down_clicked end +local function draw_physics_buttons() + if not physics_state.enabled then + return + end + local x = ui_layout.width - physics_layout.width - ui_layout.margin + local y = ui_layout.margin * 3 + compass_layout.size + + flight_layout.height * 2 + flight_layout.spacing + + Gui.text(gui_context, { + x = x, + y = y, + width = physics_layout.width, + height = 16, + }, "Physics", { + fontSize = 12, + alignX = "center", + color = {0.82, 0.88, 0.95, 1.0}, + }) + + local kick_clicked = Gui.button(gui_context, "physics_kick", { + x = x, + y = y + 18, + width = physics_layout.width, + height = physics_layout.height, + }, "Kick Cube") + if kick_clicked then + kick_physics_cube() + end + + local reset_clicked = Gui.button(gui_context, "physics_reset", { + x = x, + y = y + 18 + physics_layout.height + physics_layout.spacing, + width = physics_layout.width, + height = physics_layout.height, + }, "Reset Cube") + if reset_clicked then + reset_physics_cube() + end +end + function get_scene_objects() local objects = {} objects[#objects + 1] = create_skybox() for i = 1, #room_objects do objects[#objects + 1] = room_objects[i] end - objects[#objects + 1] = create_spinning_cube() + objects[#objects + 1] = create_dynamic_cube() return objects end @@ -827,6 +1085,7 @@ function get_gui_commands() gui_context:beginFrame(gui_input) draw_compass_widget() draw_flight_buttons() + draw_physics_buttons() gui_context:endFrame() return gui_context:getCommands() end diff --git a/scripts/gui.lua b/scripts/gui.lua index 00cf8f0..1326c31 100644 --- a/scripts/gui.lua +++ b/scripts/gui.lua @@ -125,6 +125,12 @@ Context.__index = Context function Context:new(options) options = options or {} local style = options.style or DEFAULT_STYLE + if options.style == nil and type(config) == "table" then + local guiFont = config.gui_font + if type(guiFont) == "table" and type(guiFont.font_size) == "number" then + style.fontSize = guiFont.font_size + end + end local opacity = 1.0 if type(config) == "table" and type(config.gui_opacity) == "number" then opacity = config.gui_opacity diff --git a/scripts/math3d.lua b/scripts/math3d.lua index 2f7fdc4..a7c2635 100644 --- a/scripts/math3d.lua +++ b/scripts/math3d.lua @@ -1,5 +1,22 @@ local math3d = {} +local function require_glm(name) + local fn = _G[name] + if type(fn) ~= "function" then + error("math3d requires missing binding: " .. name) + end + return fn +end + +local glm_identity = require_glm("glm_matrix_identity") +local glm_multiply = require_glm("glm_matrix_multiply") +local glm_translation = require_glm("glm_matrix_translation") +local glm_rotation_x = require_glm("glm_matrix_rotation_x") +local glm_rotation_y = require_glm("glm_matrix_rotation_y") +local glm_from_transform = require_glm("glm_matrix_from_transform") +local glm_look_at = require_glm("glm_matrix_look_at") +local glm_perspective = require_glm("glm_matrix_perspective") + local function normalize(vec) local x, y, z = vec[1], vec[2], vec[3] local len = math.sqrt(x * x + y * y + z * z) @@ -31,89 +48,35 @@ local function identity_matrix() end function math3d.identity() - return identity_matrix() + return glm_identity() end function math3d.multiply(a, b) - local result = {} - for row = 1, 4 do - for col = 1, 4 do - local sum = 0.0 - for idx = 1, 4 do - sum = sum + a[(idx - 1) * 4 + row] * b[(col - 1) * 4 + idx] - end - result[(col - 1) * 4 + row] = sum - end - end - return result + return glm_multiply(a, b) end function math3d.translation(x, y, z) - return { - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - x, y, z, 1.0, - } + return glm_translation(x, y, z) end function math3d.rotation_x(radians) - local c = math.cos(radians) - local s = math.sin(radians) - return { - 1.0, 0.0, 0.0, 0.0, - 0.0, c, s, 0.0, - 0.0, -s, c, 0.0, - 0.0, 0.0, 0.0, 1.0, - } + return glm_rotation_x(radians) end function math3d.rotation_y(radians) - local c = math.cos(radians) - local s = math.sin(radians) - return { - c, 0.0, -s, 0.0, - 0.0, 1.0, 0.0, 0.0, - s, 0.0, c, 0.0, - 0.0, 0.0, 0.0, 1.0, - } + return glm_rotation_y(radians) +end + +function math3d.from_transform(translation, rotation) + return glm_from_transform(translation, rotation) end function math3d.look_at(eye, center, up) - local f = normalize({center[1] - eye[1], center[2] - eye[2], center[3] - eye[3]}) - local s = normalize(cross(f, up)) - local u = cross(s, f) - - local result = identity_matrix() - result[1] = s[1] - result[2] = u[1] - result[3] = -f[1] - result[5] = s[2] - result[6] = u[2] - result[7] = -f[2] - result[9] = s[3] - result[10] = u[3] - result[11] = -f[3] - result[13] = -dot(s, eye) - result[14] = -dot(u, eye) - result[15] = dot(f, eye) - return result + return glm_look_at(eye, center, up) end function math3d.perspective(fov, aspect, zNear, zFar) - local tanHalf = math.tan(fov / 2.0) - local result = { - 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, - } - result[1] = 1.0 / (aspect * tanHalf) - result[6] = -1.0 / tanHalf - result[11] = zFar / (zNear - zFar) - result[12] = -1.0 - result[15] = (zNear * zFar) / (zNear - zFar) - return result + return glm_perspective(fov, aspect, zNear, zFar) end return math3d diff --git a/scripts/shader_toolkit.lua b/scripts/shader_toolkit.lua index dd55a4b..b6d4190 100644 --- a/scripts/shader_toolkit.lua +++ b/scripts/shader_toolkit.lua @@ -316,7 +316,7 @@ local vertex_color_source = [[ layout(location = 0) in vec3 inPos; layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inColor; +layout(location = 3) in vec3 inColor; layout(location = 0) out vec3 fragColor; @@ -391,7 +391,7 @@ local shadow_vertex_source = [[ layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inColor; +layout(location = 3) in vec3 inColor; layout(push_constant) uniform PushConstants { mat4 model; @@ -596,7 +596,7 @@ local vertex_world_color_source = [[ layout(location = 0) in vec3 inPos; layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inColor; +layout(location = 3) in vec3 inColor; layout(location = 0) out vec3 fragColor; layout(location = 1) out vec3 fragWorldPos; @@ -1030,7 +1030,7 @@ local pbr_vertex_source = [[ layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inColor; +layout(location = 3) in vec3 inColor; layout(location = 0) out vec3 fragColor; layout(location = 1) out vec3 fragWorldPos; diff --git a/scripts/shader_variants.lua b/scripts/shader_variants.lua index 6a670f7..e00a5ae 100644 --- a/scripts/shader_variants.lua +++ b/scripts/shader_variants.lua @@ -203,7 +203,7 @@ local function build_static_cube_variants() layout(location = 0) in vec3 inPos; layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec3 inColor; +layout(location = 3) in vec3 inColor; layout(location = 0) out vec3 fragColor; @@ -275,72 +275,11 @@ void main() { } end -local function count_shader_variants(variants) - local count = 0 - for _ in pairs(variants) do - count = count + 1 - end - return count -end - function M.build_cube_variants(config, log_debug, base_skybox_color) local logger = get_logger(log_debug) local skybox_color = resolve_skybox_color(config, base_skybox_color or {0.04, 0.05, 0.08}) - local shader_parameters = build_shader_parameter_overrides(config, logger) - local materialx_parameters = load_materialx_parameters(config, logger) - if materialx_parameters then - local entry = shader_parameters.pbr or {} - local albedo = resolve_color3_optional(materialx_parameters.material_albedo) - local roughness = resolve_number_optional(materialx_parameters.material_roughness) - local metallic = resolve_number_optional(materialx_parameters.material_metallic) - if albedo ~= nil then - entry.material_albedo = albedo - end - if roughness ~= nil then - entry.material_roughness = roughness - end - if metallic ~= nil then - entry.material_metallic = metallic - end - if next(entry) ~= nil then - shader_parameters.pbr = entry - logger("MaterialX PBR overrides: albedo=%s roughness=%s metallic=%s", - format_optional_color(albedo), - format_optional_number(roughness), - format_optional_number(metallic)) - end - end - - local ok, toolkit = pcall(require, "shader_toolkit") - if not ok then - logger("Shader toolkit unavailable: %s", tostring(toolkit)) - return build_static_cube_variants(), skybox_color - end - - local output_mode = "source" - local compile = false - local ok_generate, generated = pcall(toolkit.generate_cube_demo_variants, - {compile = compile, output_mode = output_mode, parameters = shader_parameters}) - if not ok_generate then - logger("Shader generation failed: %s", tostring(generated)) - return build_static_cube_variants(), skybox_color - end - - local ok_skybox, skybox_variant = pcall(toolkit.generate_variant, { - key = "skybox", - template = "solid_color", - output_mode = output_mode, - compile = compile, - parameters = {color = skybox_color}, - }) - if ok_skybox then - generated.skybox = skybox_variant - else - logger("Skybox shader generation failed: %s", tostring(skybox_variant)) - end - - logger("Generated %d shader variants", count_shader_variants(generated)) - return generated, skybox_color + logger("Cube shaders: using fallback sources; MaterialX provides scene material") + return build_static_cube_variants(), skybox_color end function M.build_gui_variants() diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index fbcc03b..921cefb 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -25,6 +25,9 @@ #include "services/impl/scene_service.hpp" #include "services/impl/sdl_audio_service.hpp" #include "services/impl/null_gui_service.hpp" +#if !defined(SDL3CPP_ENABLE_VITA) +#include "services/impl/bgfx_gui_service.hpp" +#endif #include "services/impl/bullet_physics_service.hpp" #include "services/impl/crash_recovery_service.hpp" #include "services/impl/logger_service.hpp" @@ -287,8 +290,14 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService()); // GUI service +#if defined(SDL3CPP_ENABLE_VITA) registry_.RegisterService( registry_.GetService()); +#else + registry_.RegisterService( + registry_.GetService(), + registry_.GetService()); +#endif // Physics service registry_.RegisterService( diff --git a/src/core/vertex.hpp b/src/core/vertex.hpp index c69c2e0..fcd717a 100644 --- a/src/core/vertex.hpp +++ b/src/core/vertex.hpp @@ -8,6 +8,7 @@ namespace sdl3cpp::core { struct Vertex { std::array position; std::array normal; + std::array texcoord; std::array color; }; diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp index 7c6e4ca..c9bd8d7 100644 --- a/src/services/impl/bgfx_graphics_backend.cpp +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -1,4 +1,5 @@ #include "bgfx_graphics_backend.hpp" +#include #include #include @@ -49,6 +50,18 @@ bool IsIdentityMatrix(const std::array& value) { return true; } +uint64_t DefaultSamplerFlags() { + return BGFX_SAMPLER_U_REPEAT | + BGFX_SAMPLER_V_REPEAT | + BGFX_SAMPLER_MIN_LINEAR | + BGFX_SAMPLER_MAG_LINEAR; +} + +bgfx::TextureHandle CreateSolidTexture(uint32_t rgba, uint64_t flags) { + const bgfx::Memory* mem = bgfx::copy(&rgba, sizeof(rgba)); + return bgfx::createTexture2D(1, 1, false, 1, bgfx::TextureFormat::RGBA8, flags, mem); +} + void SetUniformIfValid(bgfx::UniformHandle handle, const void* data, uint16_t count = 1) { if (bgfx::isValid(handle)) { bgfx::setUniform(handle, data, count); @@ -273,6 +286,7 @@ BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr configS vertexLayout_.begin() .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) + .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) .add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Float) .end(); @@ -677,6 +691,54 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label, return bgfx::createShader(mem); } +bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& path, + uint64_t samplerFlags) const { + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", "path=" + path); + } + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (!pixels || width <= 0 || height <= 0) { + if (logger_) { + logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: failed to load " + path + + " reason=" + (stbi_failure_reason() ? stbi_failure_reason() : "unknown")); + } + if (pixels) { + stbi_image_free(pixels); + } + return BGFX_INVALID_HANDLE; + } + + const uint32_t size = static_cast(width * height * 4); + const bgfx::Memory* mem = bgfx::copy(pixels, size); + stbi_image_free(pixels); + + bgfx::TextureHandle handle = bgfx::createTexture2D( + static_cast(width), + static_cast(height), + false, + 1, + bgfx::TextureFormat::RGBA8, + samplerFlags, + mem); + + if (!bgfx::isValid(handle) && logger_) { + logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: createTexture2D failed for " + path); + } + + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", + "path=" + path + + ", width=" + std::to_string(width) + + ", height=" + std::to_string(height)); + } + + return handle; +} + void BgfxGraphicsBackend::InitializeUniforms() { materialXUniforms_.worldMatrix = bgfx::createUniform("u_worldMatrix", bgfx::UniformType::Mat4); materialXUniforms_.viewMatrix = bgfx::createUniform("u_viewMatrix", bgfx::UniformType::Mat4); @@ -753,6 +815,40 @@ GraphicsPipelineHandle BgfxGraphicsBackend::CreatePipeline(GraphicsDeviceHandle auto entry = std::make_unique(); entry->program = program; + if (!shaderPaths.textures.empty()) { + const uint64_t samplerFlags = DefaultSamplerFlags(); + uint8_t stage = 0; + const uint8_t maxStages = BGFX_CONFIG_MAX_TEXTURE_SAMPLERS; + for (const auto& texture : shaderPaths.textures) { + if (stage >= maxStages) { + if (logger_) { + logger_->Warn("BgfxGraphicsBackend::CreatePipeline: texture limit reached for " + + shaderKey); + } + break; + } + if (texture.uniformName.empty() || texture.path.empty()) { + continue; + } + PipelineEntry::TextureBinding binding{}; + binding.stage = stage++; + binding.uniformName = texture.uniformName; + binding.sourcePath = texture.path; + binding.sampler = bgfx::createUniform(binding.uniformName.c_str(), bgfx::UniformType::Sampler); + binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags); + if (!bgfx::isValid(binding.texture)) { + binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags); + } + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "CreatePipeline", + "shaderKey=" + shaderKey + + ", textureUniform=" + binding.uniformName + + ", texturePath=" + binding.sourcePath + + ", stage=" + std::to_string(binding.stage)); + } + entry->textures.push_back(std::move(binding)); + } + } GraphicsPipelineHandle handle = reinterpret_cast(entry.get()); pipelines_.emplace(handle, std::move(entry)); return handle; @@ -766,6 +862,14 @@ void BgfxGraphicsBackend::DestroyPipeline(GraphicsDeviceHandle device, GraphicsP if (it == pipelines_.end()) { return; } + for (const auto& binding : it->second->textures) { + if (bgfx::isValid(binding.texture)) { + bgfx::destroy(binding.texture); + } + if (bgfx::isValid(binding.sampler)) { + bgfx::destroy(binding.sampler); + } + } if (bgfx::isValid(it->second->program)) { bgfx::destroy(it->second->program); } @@ -889,6 +993,11 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand bgfx::setTransform(modelMatrix.data()); ApplyMaterialXUniforms(modelMatrix); + for (const auto& binding : pipelineIt->second->textures) { + if (bgfx::isValid(binding.sampler) && bgfx::isValid(binding.texture)) { + bgfx::setTexture(binding.stage, binding.sampler, binding.texture); + } + } bgfx::setVertexBuffer(0, vb->handle, startVertex, availableVertices); bgfx::setIndexBuffer(ib->handle, indexOffset, indexCount); bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | @@ -918,6 +1027,14 @@ void* BgfxGraphicsBackend::GetGraphicsQueue() const { void BgfxGraphicsBackend::DestroyPipelines() { for (auto& [handle, entry] : pipelines_) { + for (const auto& binding : entry->textures) { + if (bgfx::isValid(binding.texture)) { + bgfx::destroy(binding.texture); + } + if (bgfx::isValid(binding.sampler)) { + bgfx::destroy(binding.sampler); + } + } if (bgfx::isValid(entry->program)) { bgfx::destroy(entry->program); } diff --git a/src/services/impl/bgfx_graphics_backend.hpp b/src/services/impl/bgfx_graphics_backend.hpp index 0ca207d..0980120 100644 --- a/src/services/impl/bgfx_graphics_backend.hpp +++ b/src/services/impl/bgfx_graphics_backend.hpp @@ -51,6 +51,14 @@ public: private: struct PipelineEntry { bgfx::ProgramHandle program = BGFX_INVALID_HANDLE; + struct TextureBinding { + bgfx::UniformHandle sampler = BGFX_INVALID_HANDLE; + bgfx::TextureHandle texture = BGFX_INVALID_HANDLE; + uint8_t stage = 0; + std::string uniformName; + std::string sourcePath; + }; + std::vector textures; }; struct VertexBufferEntry { @@ -93,6 +101,7 @@ private: bgfx::ShaderHandle CreateShader(const std::string& label, const std::string& source, bool isVertex) const; + bgfx::TextureHandle LoadTextureFromFile(const std::string& path, uint64_t samplerFlags) const; void InitializeUniforms(); void DestroyUniforms(); void ApplyMaterialXUniforms(const std::array& modelMatrix); diff --git a/src/services/impl/bgfx_gui_service.cpp b/src/services/impl/bgfx_gui_service.cpp new file mode 100644 index 0000000..ad4d152 --- /dev/null +++ b/src/services/impl/bgfx_gui_service.cpp @@ -0,0 +1,867 @@ +#include "bgfx_gui_service.hpp" + +#include "../interfaces/config_types.hpp" +#include "../interfaces/gui_types.hpp" + +#include +#include +#include + +#include +#include FT_FREETYPE_H + +#include + +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +namespace { + +constexpr uint64_t kGuiSamplerFlags = BGFX_SAMPLER_U_CLAMP | + BGFX_SAMPLER_V_CLAMP | + BGFX_SAMPLER_MIN_LINEAR | + BGFX_SAMPLER_MAG_LINEAR; + +const char* kGuiVertexSource = R"( +#version 450 + +layout(location = 0) in vec3 inPos; +layout(location = 1) in vec4 inColor; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec4 fragColor; +layout(location = 1) out vec2 fragTexCoord; + +uniform mat4 u_modelViewProj; + +void main() { + fragColor = inColor; + fragTexCoord = inTexCoord; + gl_Position = u_modelViewProj * vec4(inPos, 1.0); +} +)"; + +const char* kGuiFragmentSource = R"( +#version 450 + +layout(location = 0) in vec4 fragColor; +layout(location = 1) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +layout(binding = 0) uniform sampler2D s_tex; + +void main() { + outColor = fragColor * texture(s_tex, fragTexCoord); +} +)"; + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return value; +} + +float Clamp01(float value) { + return std::clamp(value, 0.0f, 1.0f); +} + +} // namespace + +struct BgfxGuiService::FreeTypeState { + FT_Library library = nullptr; + FT_Face face = nullptr; + std::filesystem::path fontPath; + bool ready = false; +}; + +BgfxGuiService::BgfxGuiService(std::shared_ptr configService, + std::shared_ptr logger) + : configService_(std::move(configService)), + logger_(std::move(logger)), + freeType_(std::make_unique()) { + if (logger_) { + logger_->Trace("BgfxGuiService", "BgfxGuiService", + "configService=" + std::string(configService_ ? "set" : "null")); + } +} + +BgfxGuiService::~BgfxGuiService() { + if (initialized_) { + Shutdown(); + } +} + +void BgfxGuiService::PrepareFrame(const std::vector& commands, + uint32_t width, + uint32_t height) { + if (!initialized_) { + InitializeResources(); + } + + if (!bgfx::isValid(program_) || !bgfx::isValid(whiteTexture_)) { + if (logger_) { + logger_->Warn("BgfxGuiService::PrepareFrame: GUI resources not initialized"); + } + return; + } + + ApplyGuiView(width, height); + scissorStack_.clear(); + ++frameIndex_; + + for (const auto& command : commands) { + switch (command.type) { + case GuiCommand::Type::ClipPush: { + ScissorRect incoming{command.rect.x, command.rect.y, command.rect.width, command.rect.height}; + auto current = CurrentScissor(); + if (current) { + auto merged = IntersectScissor(*current, incoming); + if (merged) { + scissorStack_.push_back(*merged); + } else { + scissorStack_.push_back(ScissorRect{0.0f, 0.0f, 0.0f, 0.0f}); + } + } else { + scissorStack_.push_back(incoming); + } + break; + } + case GuiCommand::Type::ClipPop: { + if (!scissorStack_.empty()) { + scissorStack_.pop_back(); + } + break; + } + case GuiCommand::Type::Rect: { + SubmitRect(command, BuildScissor(std::nullopt)); + break; + } + case GuiCommand::Type::Text: { + std::optional scoped; + if (command.hasClipRect) { + scoped = ScissorRect{command.clipRect.x, command.clipRect.y, + command.clipRect.width, command.clipRect.height}; + } + SubmitText(command, BuildScissor(scoped)); + break; + } + case GuiCommand::Type::Svg: { + SubmitSvg(command, BuildScissor(std::nullopt)); + break; + } + default: + break; + } + } + + PruneTextCache(); + PruneSvgCache(); +} + +void BgfxGuiService::Shutdown() noexcept { + if (logger_) { + logger_->Trace("BgfxGuiService", "Shutdown"); + } + + for (auto& [key, entry] : textCache_) { + if (bgfx::isValid(entry.texture)) { + bgfx::destroy(entry.texture); + } + } + textCache_.clear(); + + for (auto& [key, entry] : svgCache_) { + if (bgfx::isValid(entry.texture)) { + bgfx::destroy(entry.texture); + } + } + svgCache_.clear(); + + if (bgfx::isValid(whiteTexture_)) { + bgfx::destroy(whiteTexture_); + whiteTexture_ = BGFX_INVALID_HANDLE; + } + if (bgfx::isValid(program_)) { + bgfx::destroy(program_); + program_ = BGFX_INVALID_HANDLE; + } + if (bgfx::isValid(sampler_)) { + bgfx::destroy(sampler_); + sampler_ = BGFX_INVALID_HANDLE; + } + + if (freeType_) { + if (freeType_->face) { + FT_Done_Face(freeType_->face); + freeType_->face = nullptr; + } + if (freeType_->library) { + FT_Done_FreeType(freeType_->library); + freeType_->library = nullptr; + } + freeType_->ready = false; + } + + initialized_ = false; +} + +void BgfxGuiService::InitializeResources() { + if (initialized_) { + return; + } + + layout_.begin() + .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) + .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float) + .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) + .end(); + + sampler_ = bgfx::createUniform("s_tex", bgfx::UniformType::Sampler); + program_ = CreateProgram(kGuiVertexSource, kGuiFragmentSource); + + const uint32_t whitePixel = 0xffffffff; + whiteTexture_ = CreateTexture(reinterpret_cast(&whitePixel), 1, 1, kGuiSamplerFlags); + + if (!bgfx::isValid(program_) && logger_) { + logger_->Error("BgfxGuiService::InitializeResources: Failed to create GUI shader program"); + } + + EnsureFontReady(); + initialized_ = true; +} + +void BgfxGuiService::EnsureFontReady() { + if (!freeType_ || freeType_->ready) { + return; + } + + GuiFontConfig fontConfig{}; + if (configService_) { + fontConfig = configService_->GetGuiFontConfig(); + } + + if (!fontConfig.useFreeType) { + if (logger_) { + logger_->Warn("BgfxGuiService::EnsureFontReady: use_freetype disabled; GUI text disabled"); + } + return; + } + + defaultFontSize_ = fontConfig.fontSize > 0.0f + ? static_cast(std::lround(fontConfig.fontSize)) + : defaultFontSize_; + + std::filesystem::path fontPath = fontConfig.fontPath; + if (fontPath.empty()) { + fontPath = ResolveDefaultFontPath(); + } + fontPath = ResolvePath(fontPath); + + if (fontPath.empty() || !std::filesystem::exists(fontPath)) { + if (logger_) { + logger_->Warn("BgfxGuiService::EnsureFontReady: font path missing; GUI text disabled"); + } + return; + } + + if (FT_Init_FreeType(&freeType_->library) != 0) { + if (logger_) { + logger_->Error("BgfxGuiService::EnsureFontReady: FreeType init failed"); + } + return; + } + + if (FT_New_Face(freeType_->library, fontPath.string().c_str(), 0, &freeType_->face) != 0) { + if (logger_) { + logger_->Error("BgfxGuiService::EnsureFontReady: Failed to load font " + fontPath.string()); + } + FT_Done_FreeType(freeType_->library); + freeType_->library = nullptr; + return; + } + + freeType_->fontPath = fontPath; + freeType_->ready = true; + + if (logger_) { + logger_->Trace("BgfxGuiService", "EnsureFontReady", + "fontPath=" + fontPath.string() + + ", defaultSize=" + std::to_string(defaultFontSize_)); + } +} + +void BgfxGuiService::ApplyGuiView(uint32_t width, uint32_t height) { + frameWidth_ = width; + frameHeight_ = height; + + float view[16]; + float proj[16]; + bx::mtxIdentity(view); + const bool homogeneousDepth = bgfx::getCaps() && bgfx::getCaps()->homogeneousDepth; + bx::mtxOrtho(proj, + 0.0f, + static_cast(width), + static_cast(height), + 0.0f, + 0.0f, + 100.0f, + 0.0f, + homogeneousDepth); + + bgfx::setViewTransform(viewId_, view, proj); + bgfx::setViewRect(viewId_, 0, 0, + static_cast(std::min(width, 0xffff)), + static_cast(std::min(height, 0xffff))); + bgfx::touch(viewId_); +} + +std::filesystem::path BgfxGuiService::ResolvePath(const std::filesystem::path& path) const { + if (path.empty() || path.is_absolute()) { + return path; + } + + std::vector roots; + if (configService_) { + auto scriptPath = configService_->GetScriptPath(); + if (!scriptPath.empty()) { + auto scriptDir = scriptPath.parent_path(); + if (!scriptDir.empty()) { + roots.push_back(scriptDir); + auto projectRoot = scriptDir.parent_path(); + if (!projectRoot.empty()) { + roots.push_back(projectRoot); + } + } + } + } + roots.push_back(std::filesystem::current_path()); + + for (const auto& root : roots) { + auto candidate = root / path; + if (std::filesystem::exists(candidate)) { + return candidate; + } + } + + return path; +} + +std::filesystem::path BgfxGuiService::ResolveDefaultFontPath() const { + std::vector candidates; + if (configService_) { + auto scriptPath = configService_->GetScriptPath(); + if (!scriptPath.empty()) { + auto scriptDir = scriptPath.parent_path(); + candidates.push_back(scriptDir / "assets" / "fonts" / "Roboto-Regular.ttf"); + candidates.push_back(scriptDir.parent_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf"); + } + } + candidates.push_back(std::filesystem::current_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf"); + + for (const auto& candidate : candidates) { + if (!candidate.empty() && std::filesystem::exists(candidate)) { + return candidate; + } + } + + return {}; +} + +std::optional BgfxGuiService::IntersectScissor(const ScissorRect& a, + const ScissorRect& b) const { + float x0 = std::max(a.x, b.x); + float y0 = std::max(a.y, b.y); + float x1 = std::min(a.x + a.width, b.x + b.width); + float y1 = std::min(a.y + a.height, b.y + b.height); + if (x1 <= x0 || y1 <= y0) { + return std::nullopt; + } + return ScissorRect{x0, y0, x1 - x0, y1 - y0}; +} + +std::optional BgfxGuiService::CurrentScissor() const { + if (scissorStack_.empty()) { + return std::nullopt; + } + return scissorStack_.back(); +} + +std::optional BgfxGuiService::BuildScissor( + const std::optional& scoped) const { + auto current = CurrentScissor(); + if (current && scoped) { + return IntersectScissor(*current, *scoped); + } + if (current) { + return current; + } + return scoped; +} + +void BgfxGuiService::SetScissor(const std::optional& scissor) const { + if (!scissor || scissor->width <= 0.0f || scissor->height <= 0.0f) { + bgfx::setScissor(0, 0, 0, 0); + return; + } + + uint16_t x = static_cast(std::clamp(scissor->x, 0.0f, static_cast(frameWidth_))); + uint16_t y = static_cast(std::clamp(scissor->y, 0.0f, static_cast(frameHeight_))); + uint16_t w = static_cast(std::clamp(scissor->width, 0.0f, static_cast(frameWidth_ - x))); + uint16_t h = static_cast(std::clamp(scissor->height, 0.0f, static_cast(frameHeight_ - y))); + bgfx::setScissor(x, y, w, h); +} + +void BgfxGuiService::SubmitRect(const GuiCommand& command, const std::optional& scissor) { + GuiColor color = command.color; + color.r = Clamp01(color.r); + color.g = Clamp01(color.g); + color.b = Clamp01(color.b); + color.a = Clamp01(color.a); + if (color.a > 0.0f) { + GuiVertex v0{command.rect.x, command.rect.y, 0.0f, + Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 0.0f, 0.0f}; + GuiVertex v1{command.rect.x + command.rect.width, command.rect.y, 0.0f, + Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 1.0f, 0.0f}; + GuiVertex v2{command.rect.x + command.rect.width, command.rect.y + command.rect.height, 0.0f, + Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 1.0f, 1.0f}; + GuiVertex v3{command.rect.x, command.rect.y + command.rect.height, 0.0f, + Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 0.0f, 1.0f}; + SubmitQuad(v0, v1, v2, v3, whiteTexture_, scissor); + } + + if (command.borderWidth <= 0.0f) { + return; + } + + GuiColor border = command.borderColor; + if (border.a <= 0.0f) { + return; + } + border.r = Clamp01(border.r); + border.g = Clamp01(border.g); + border.b = Clamp01(border.b); + border.a = Clamp01(border.a); + + float bw = std::min(command.borderWidth, std::min(command.rect.width, command.rect.height)); + float x = command.rect.x; + float y = command.rect.y; + float w = command.rect.width; + float h = command.rect.height; + + GuiVertex vt0{x, y, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f}; + GuiVertex vt1{x + w, y, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f}; + GuiVertex vt2{x + w, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f}; + GuiVertex vt3{x, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f}; + SubmitQuad(vt0, vt1, vt2, vt3, whiteTexture_, scissor); + + GuiVertex vb0{x, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f}; + GuiVertex vb1{x + w, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f}; + GuiVertex vb2{x + w, y + h, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f}; + GuiVertex vb3{x, y + h, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f}; + SubmitQuad(vb0, vb1, vb2, vb3, whiteTexture_, scissor); + + GuiVertex vl0{x, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f}; + GuiVertex vl1{x + bw, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f}; + GuiVertex vl2{x + bw, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f}; + GuiVertex vl3{x, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f}; + SubmitQuad(vl0, vl1, vl2, vl3, whiteTexture_, scissor); + + GuiVertex vr0{x + w - bw, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f}; + GuiVertex vr1{x + w, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f}; + GuiVertex vr2{x + w, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f}; + GuiVertex vr3{x + w - bw, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f}; + SubmitQuad(vr0, vr1, vr2, vr3, whiteTexture_, scissor); +} + +void BgfxGuiService::SubmitText(const GuiCommand& command, const std::optional& scissor) { + if (command.text.empty()) { + return; + } + + int fontSize = command.fontSize > 0.0f + ? static_cast(std::lround(command.fontSize)) + : defaultFontSize_; + fontSize = std::max(8, fontSize); + + const TextTexture* texture = GetTextTexture(command.text, fontSize); + if (!texture || !bgfx::isValid(texture->texture) || texture->width == 0 || texture->height == 0) { + return; + } + + std::string alignX = ToLower(command.alignX); + std::string alignY = ToLower(command.alignY); + + float x = command.rect.x; + float y = command.rect.y; + float width = static_cast(texture->width); + float height = static_cast(texture->height); + + if (alignX == "center") { + x -= width * 0.5f; + } else if (alignX == "right") { + x -= width; + } + + if (alignY == "center") { + y -= height * 0.5f; + } else if (alignY == "bottom") { + y -= height; + } + + GuiColor color = command.color; + GuiVertex v0{x, y, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 0.0f}; + GuiVertex v1{x + width, y, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 0.0f}; + GuiVertex v2{x + width, y + height, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 1.0f}; + GuiVertex v3{x, y + height, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 1.0f}; + SubmitQuad(v0, v1, v2, v3, texture->texture, scissor); +} + +void BgfxGuiService::SubmitSvg(const GuiCommand& command, const std::optional& scissor) { + if (command.svgPath.empty()) { + return; + } + + int width = static_cast(std::lround(command.rect.width)); + int height = static_cast(std::lround(command.rect.height)); + if (width <= 0 || height <= 0) { + return; + } + + const SvgTexture* texture = GetSvgTexture(command.svgPath, width, height); + if (!texture || !bgfx::isValid(texture->texture)) { + return; + } + + GuiColor color = command.svgTint; + color.r = Clamp01(color.r); + color.g = Clamp01(color.g); + color.b = Clamp01(color.b); + color.a = Clamp01(color.a); + GuiVertex v0{command.rect.x, command.rect.y, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 0.0f}; + GuiVertex v1{command.rect.x + command.rect.width, command.rect.y, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 0.0f}; + GuiVertex v2{command.rect.x + command.rect.width, command.rect.y + command.rect.height, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 1.0f}; + GuiVertex v3{command.rect.x, command.rect.y + command.rect.height, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 1.0f}; + SubmitQuad(v0, v1, v2, v3, texture->texture, scissor); +} + +void BgfxGuiService::SubmitQuad(const GuiVertex& v0, + const GuiVertex& v1, + const GuiVertex& v2, + const GuiVertex& v3, + bgfx::TextureHandle texture, + const std::optional& scissor) { + if (!bgfx::isValid(program_) || !bgfx::isValid(texture)) { + return; + } + if (scissor && (scissor->width <= 0.0f || scissor->height <= 0.0f)) { + return; + } + + if (bgfx::getAvailTransientVertexBuffer(4, layout_) < 4 || + bgfx::getAvailTransientIndexBuffer(6) < 6) { + if (logger_) { + logger_->Trace("BgfxGuiService", "SubmitQuad", "Transient buffer exhausted"); + } + return; + } + + bgfx::TransientVertexBuffer tvb{}; + bgfx::TransientIndexBuffer tib{}; + bgfx::allocTransientVertexBuffer(&tvb, 4, layout_); + bgfx::allocTransientIndexBuffer(&tib, 6); + + auto* vertices = reinterpret_cast(tvb.data); + vertices[0] = v0; + vertices[1] = v1; + vertices[2] = v2; + vertices[3] = v3; + + auto* indices = reinterpret_cast(tib.data); + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 0; + indices[4] = 2; + indices[5] = 3; + + float identity[16]; + bx::mtxIdentity(identity); + + SetScissor(scissor); + bgfx::setTransform(identity); + bgfx::setTexture(0, sampler_, texture); + bgfx::setVertexBuffer(0, &tvb, 0, 4); + bgfx::setIndexBuffer(&tib, 0, 6); + bgfx::setState(BGFX_STATE_WRITE_RGB | + BGFX_STATE_WRITE_A | + BGFX_STATE_BLEND_ALPHA | + BGFX_STATE_MSAA); + bgfx::submit(viewId_, program_); +} + +const BgfxGuiService::TextTexture* BgfxGuiService::GetTextTexture(const std::string& text, int fontSize) { + if (text.empty()) { + return nullptr; + } + + EnsureFontReady(); + if (!freeType_ || !freeType_->ready || !freeType_->face) { + return nullptr; + } + + TextKey key{text, fontSize}; + auto it = textCache_.find(key); + if (it != textCache_.end()) { + it->second.lastUsedFrame = frameIndex_; + return &it->second; + } + + FT_Face face = freeType_->face; + if (FT_Set_Pixel_Sizes(face, 0, fontSize) != 0) { + return nullptr; + } + + int ascent = face->size->metrics.ascender >> 6; + int descent = face->size->metrics.descender >> 6; + int height = ascent - descent; + int width = 0; + + for (unsigned char ch : text) { + if (FT_Load_Char(face, ch, FT_LOAD_RENDER) != 0) { + continue; + } + width += face->glyph->advance.x >> 6; + } + + if (width <= 0 || height <= 0) { + return nullptr; + } + + std::vector pixels(static_cast(width * height * 4), 0); + int penX = 0; + for (unsigned char ch : text) { + if (FT_Load_Char(face, ch, FT_LOAD_RENDER) != 0) { + continue; + } + + FT_GlyphSlot glyph = face->glyph; + FT_Bitmap& bitmap = glyph->bitmap; + int pitch = bitmap.pitch; + if (pitch < 0) { + pitch = -pitch; + } + + int x0 = penX + glyph->bitmap_left; + int y0 = ascent - glyph->bitmap_top; + + for (int row = 0; row < static_cast(bitmap.rows); ++row) { + int y = y0 + row; + if (y < 0 || y >= height) { + continue; + } + for (int col = 0; col < static_cast(bitmap.width); ++col) { + int x = x0 + col; + if (x < 0 || x >= width) { + continue; + } + uint8_t alpha = bitmap.buffer[row * pitch + col]; + size_t idx = static_cast((y * width + x) * 4); + pixels[idx + 0] = 255; + pixels[idx + 1] = 255; + pixels[idx + 2] = 255; + pixels[idx + 3] = alpha; + } + } + + penX += glyph->advance.x >> 6; + } + + TextTexture entry{}; + entry.texture = CreateTexture(pixels.data(), + static_cast(width), + static_cast(height), + kGuiSamplerFlags); + entry.width = width; + entry.height = height; + entry.baseline = ascent; + entry.fontSize = fontSize; + entry.lastUsedFrame = frameIndex_; + + auto [insertIt, inserted] = textCache_.emplace(std::move(key), entry); + if (!inserted) { + return nullptr; + } + return &insertIt->second; +} + +const BgfxGuiService::SvgTexture* BgfxGuiService::GetSvgTexture(const std::string& path, + int width, + int height) { + if (path.empty()) { + return nullptr; + } + + SvgKey key{ResolvePath(path).string(), width, height}; + auto it = svgCache_.find(key); + if (it != svgCache_.end()) { + it->second.lastUsedFrame = frameIndex_; + return &it->second; + } + + auto document = lunasvg::Document::loadFromFile(key.path); + if (!document) { + if (logger_) { + logger_->Warn("BgfxGuiService::GetSvgTexture: Failed to load " + key.path); + } + return nullptr; + } + + auto bitmap = document->renderToBitmap(width, height); + if (!bitmap.valid()) { + return nullptr; + } + + const uint8_t* data = bitmap.data(); + const uint32_t w = static_cast(bitmap.width()); + const uint32_t h = static_cast(bitmap.height()); + if (!data || w == 0 || h == 0) { + return nullptr; + } + + std::vector rgba(static_cast(w * h * 4), 0); + for (uint32_t i = 0; i < w * h; ++i) { + const uint8_t b = data[i * 4 + 0]; + const uint8_t g = data[i * 4 + 1]; + const uint8_t r = data[i * 4 + 2]; + const uint8_t a = data[i * 4 + 3]; + if (a > 0) { + rgba[i * 4 + 0] = static_cast(std::min(255, (static_cast(r) * 255) / a)); + rgba[i * 4 + 1] = static_cast(std::min(255, (static_cast(g) * 255) / a)); + rgba[i * 4 + 2] = static_cast(std::min(255, (static_cast(b) * 255) / a)); + rgba[i * 4 + 3] = a; + } else { + rgba[i * 4 + 0] = 0; + rgba[i * 4 + 1] = 0; + rgba[i * 4 + 2] = 0; + rgba[i * 4 + 3] = 0; + } + } + + SvgTexture entry{}; + entry.texture = CreateTexture(rgba.data(), w, h, kGuiSamplerFlags); + entry.width = static_cast(w); + entry.height = static_cast(h); + entry.lastUsedFrame = frameIndex_; + + auto [insertIt, inserted] = svgCache_.emplace(std::move(key), entry); + if (!inserted) { + return nullptr; + } + return &insertIt->second; +} + +bgfx::TextureHandle BgfxGuiService::CreateTexture(const uint8_t* rgba, + uint32_t width, + uint32_t height, + uint64_t flags) const { + if (!rgba || width == 0 || height == 0) { + return BGFX_INVALID_HANDLE; + } + const uint32_t size = width * height * 4; + const bgfx::Memory* mem = bgfx::copy(rgba, size); + return bgfx::createTexture2D(static_cast(width), + static_cast(height), + false, + 1, + bgfx::TextureFormat::RGBA8, + flags, + mem); +} + +bgfx::ProgramHandle BgfxGuiService::CreateProgram(const char* vertexSource, + const char* fragmentSource) const { + if (!vertexSource || !fragmentSource) { + return BGFX_INVALID_HANDLE; + } + + bgfx::ShaderHandle vs = CreateShader("gui_vertex", vertexSource, true); + bgfx::ShaderHandle fs = CreateShader("gui_fragment", fragmentSource, false); + if (!bgfx::isValid(vs) || !bgfx::isValid(fs)) { + if (bgfx::isValid(vs)) { + bgfx::destroy(vs); + } + if (bgfx::isValid(fs)) { + bgfx::destroy(fs); + } + return BGFX_INVALID_HANDLE; + } + return bgfx::createProgram(vs, fs, true); +} + +bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, + const std::string& source, + bool isVertex) const { + shaderc::Compiler compiler; + shaderc::CompileOptions options; + options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + + shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader; + auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options); + if (result.GetCompilationStatus() != shaderc_compilation_status_success) { + if (logger_) { + logger_->Error("BgfxGuiService::CreateShader: " + label + "\n" + result.GetErrorMessage()); + } + return BGFX_INVALID_HANDLE; + } + + std::vector spirv(result.cbegin(), result.cend()); + const bgfx::Memory* mem = bgfx::copy(spirv.data(), + static_cast(spirv.size() * sizeof(uint32_t))); + return bgfx::createShader(mem); +} + +void BgfxGuiService::PruneTextCache() { + if (textCache_.size() <= maxTextCacheEntries_) { + return; + } + while (textCache_.size() > maxTextCacheEntries_) { + auto oldest = std::min_element(textCache_.begin(), textCache_.end(), + [](const auto& left, const auto& right) { + return left.second.lastUsedFrame < right.second.lastUsedFrame; + }); + if (oldest == textCache_.end()) { + break; + } + if (bgfx::isValid(oldest->second.texture)) { + bgfx::destroy(oldest->second.texture); + } + textCache_.erase(oldest); + } +} + +void BgfxGuiService::PruneSvgCache() { + if (svgCache_.size() <= maxSvgCacheEntries_) { + return; + } + while (svgCache_.size() > maxSvgCacheEntries_) { + auto oldest = std::min_element(svgCache_.begin(), svgCache_.end(), + [](const auto& left, const auto& right) { + return left.second.lastUsedFrame < right.second.lastUsedFrame; + }); + if (oldest == svgCache_.end()) { + break; + } + if (bgfx::isValid(oldest->second.texture)) { + bgfx::destroy(oldest->second.texture); + } + svgCache_.erase(oldest); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/bgfx_gui_service.hpp b/src/services/impl/bgfx_gui_service.hpp new file mode 100644 index 0000000..1d7ad61 --- /dev/null +++ b/src/services/impl/bgfx_gui_service.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include "../interfaces/i_config_service.hpp" +#include "../interfaces/i_gui_service.hpp" +#include "../interfaces/i_logger.hpp" +#include "../../di/lifecycle.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +class BgfxGuiService final : public IGuiService, + public di::IShutdownable { +public: + BgfxGuiService(std::shared_ptr configService, + std::shared_ptr logger); + ~BgfxGuiService() override; + + void PrepareFrame(const std::vector& commands, + uint32_t width, + uint32_t height) override; + + void Shutdown() noexcept override; + +private: + struct GuiVertex { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; + float a = 1.0f; + float u = 0.0f; + float v = 0.0f; + }; + + struct ScissorRect { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + }; + + struct TextKey { + std::string text; + int fontSize = 0; + + bool operator==(const TextKey& other) const { + return fontSize == other.fontSize && text == other.text; + } + }; + + struct TextKeyHash { + size_t operator()(const TextKey& key) const { + return std::hash{}(key.text) ^ (static_cast(key.fontSize) << 1); + } + }; + + struct TextTexture { + bgfx::TextureHandle texture = BGFX_INVALID_HANDLE; + int width = 0; + int height = 0; + int baseline = 0; + int fontSize = 0; + uint64_t lastUsedFrame = 0; + }; + + struct SvgKey { + std::string path; + int width = 0; + int height = 0; + + bool operator==(const SvgKey& other) const { + return width == other.width && height == other.height && path == other.path; + } + }; + + struct SvgKeyHash { + size_t operator()(const SvgKey& key) const { + size_t seed = std::hash{}(key.path); + seed ^= (static_cast(key.width) << 1); + seed ^= (static_cast(key.height) << 2); + return seed; + } + }; + + struct SvgTexture { + bgfx::TextureHandle texture = BGFX_INVALID_HANDLE; + int width = 0; + int height = 0; + uint64_t lastUsedFrame = 0; + }; + + struct FreeTypeState; + + void InitializeResources(); + void EnsureFontReady(); + void ApplyGuiView(uint32_t width, uint32_t height); + + std::filesystem::path ResolvePath(const std::filesystem::path& path) const; + std::filesystem::path ResolveDefaultFontPath() const; + + std::optional IntersectScissor(const ScissorRect& a, const ScissorRect& b) const; + std::optional CurrentScissor() const; + std::optional BuildScissor(const std::optional& scoped) const; + void SetScissor(const std::optional& scissor) const; + + void SubmitRect(const GuiCommand& command, const std::optional& scissor); + void SubmitText(const GuiCommand& command, const std::optional& scissor); + void SubmitSvg(const GuiCommand& command, const std::optional& scissor); + + void SubmitQuad(const GuiVertex& v0, + const GuiVertex& v1, + const GuiVertex& v2, + const GuiVertex& v3, + bgfx::TextureHandle texture, + const std::optional& scissor); + + const TextTexture* GetTextTexture(const std::string& text, int fontSize); + const SvgTexture* GetSvgTexture(const std::string& path, int width, int height); + void PruneTextCache(); + void PruneSvgCache(); + + bgfx::TextureHandle CreateTexture(const uint8_t* rgba, + uint32_t width, + uint32_t height, + uint64_t flags) const; + bgfx::ProgramHandle CreateProgram(const char* vertexSource, const char* fragmentSource) const; + bgfx::ShaderHandle CreateShader(const std::string& label, const std::string& source, bool isVertex) const; + + std::shared_ptr configService_; + std::shared_ptr logger_; + + std::unique_ptr freeType_; + std::unordered_map textCache_; + std::unordered_map svgCache_; + + bgfx::ProgramHandle program_ = BGFX_INVALID_HANDLE; + bgfx::UniformHandle sampler_ = BGFX_INVALID_HANDLE; + bgfx::TextureHandle whiteTexture_ = BGFX_INVALID_HANDLE; + bgfx::VertexLayout layout_; + + std::vector scissorStack_; + int defaultFontSize_ = 16; + uint32_t frameWidth_ = 0; + uint32_t frameHeight_ = 0; + uint16_t viewId_ = 1; + bool initialized_ = false; + uint64_t frameIndex_ = 0; + size_t maxTextCacheEntries_ = 256; + size_t maxSvgCacheEntries_ = 64; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/bullet_physics_service.cpp b/src/services/impl/bullet_physics_service.cpp index 1fe651e..fd83bbc 100644 --- a/src/services/impl/bullet_physics_service.cpp +++ b/src/services/impl/bullet_physics_service.cpp @@ -26,14 +26,24 @@ void BulletPhysicsService::Initialize(const btVector3& gravity) { ", gravity.y=" + std::to_string(gravity.getY()) + ", gravity.z=" + std::to_string(gravity.getZ())); - if (initialized_) { - return; + gravity_ = gravity; + bool wasInitialized = initialized_; + + if (!initialized_) { + physicsBridge_ = std::make_unique(logger_); + initialized_ = true; } - physicsBridge_ = std::make_unique(logger_); - initialized_ = true; + if (physicsBridge_) { + std::string error; + if (!physicsBridge_->SetGravity(gravity_, error)) { + logger_->Warn("SetGravity failed: " + error); + } + } - logger_->Info("Physics service initialized"); + if (!wasInitialized && initialized_) { + logger_->Info("Physics service initialized"); + } } void BulletPhysicsService::Shutdown() noexcept { @@ -87,17 +97,31 @@ bool BulletPhysicsService::AddSphereRigidBody(const std::string& name, ", origin.y=" + std::to_string(transform.getOrigin().getY()) + ", origin.z=" + std::to_string(transform.getOrigin().getZ())); - // PhysicsBridgeService doesn't support sphere rigid bodies in current implementation - logger_->Warn("AddSphereRigidBody not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->AddSphereRigidBody(name, radius, mass, transform, error)) { + logger_->Error("AddSphereRigidBody failed: " + error); + return false; + } + return true; } bool BulletPhysicsService::RemoveRigidBody(const std::string& name) { logger_->Trace("BulletPhysicsService", "RemoveRigidBody", "name=" + name); - // PhysicsBridgeService doesn't support removing bodies in current implementation - logger_->Warn("RemoveRigidBody not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->RemoveRigidBody(name, error)) { + logger_->Error("RemoveRigidBody failed: " + error); + return false; + } + return true; } void BulletPhysicsService::StepSimulation(float deltaTime, int maxSubSteps) { @@ -109,7 +133,7 @@ void BulletPhysicsService::StepSimulation(float deltaTime, int maxSubSteps) { throw std::runtime_error("Physics service not initialized"); } - physicsBridge_->StepSimulation(deltaTime); + physicsBridge_->StepSimulation(deltaTime, maxSubSteps); } bool BulletPhysicsService::GetTransform(const std::string& name, btTransform& outTransform) const { @@ -135,9 +159,16 @@ bool BulletPhysicsService::SetTransform(const std::string& name, const btTransfo ", origin.y=" + std::to_string(transform.getOrigin().getY()) + ", origin.z=" + std::to_string(transform.getOrigin().getZ())); - // PhysicsBridgeService doesn't support setting transforms in current implementation - logger_->Warn("SetTransform not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->SetRigidBodyTransform(name, transform, error)) { + logger_->Error("SetTransform failed: " + error); + return false; + } + return true; } bool BulletPhysicsService::ApplyForce(const std::string& name, const btVector3& force) { @@ -147,9 +178,16 @@ bool BulletPhysicsService::ApplyForce(const std::string& name, const btVector3& ", force.y=" + std::to_string(force.getY()) + ", force.z=" + std::to_string(force.getZ())); - // PhysicsBridgeService doesn't support applying forces in current implementation - logger_->Warn("ApplyForce not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->ApplyForce(name, force, error)) { + logger_->Error("ApplyForce failed: " + error); + return false; + } + return true; } bool BulletPhysicsService::ApplyImpulse(const std::string& name, const btVector3& impulse) { @@ -159,9 +197,16 @@ bool BulletPhysicsService::ApplyImpulse(const std::string& name, const btVector3 ", impulse.y=" + std::to_string(impulse.getY()) + ", impulse.z=" + std::to_string(impulse.getZ())); - // PhysicsBridgeService doesn't support applying impulses in current implementation - logger_->Warn("ApplyImpulse not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->ApplyImpulse(name, impulse, error)) { + logger_->Error("ApplyImpulse failed: " + error); + return false; + } + return true; } bool BulletPhysicsService::SetLinearVelocity(const std::string& name, const btVector3& velocity) { @@ -171,16 +216,24 @@ bool BulletPhysicsService::SetLinearVelocity(const std::string& name, const btVe ", velocity.y=" + std::to_string(velocity.getY()) + ", velocity.z=" + std::to_string(velocity.getZ())); - // PhysicsBridgeService doesn't support setting velocity in current implementation - logger_->Warn("SetLinearVelocity not supported by PhysicsBridgeService"); - return false; + if (!physicsBridge_) { + throw std::runtime_error("Physics service not initialized"); + } + + std::string error; + if (!physicsBridge_->SetLinearVelocity(name, velocity, error)) { + logger_->Error("SetLinearVelocity failed: " + error); + return false; + } + return true; } size_t BulletPhysicsService::GetBodyCount() const { logger_->Trace("BulletPhysicsService", "GetBodyCount"); - // PhysicsBridgeService doesn't expose GetBodyCount in current implementation - // Returning 0 as stub - could track bodies in wrapper if needed - return 0; + if (!physicsBridge_) { + return 0; + } + return physicsBridge_->GetBodyCount(); } void BulletPhysicsService::Clear() { @@ -190,10 +243,7 @@ void BulletPhysicsService::Clear() { return; } - // PhysicsBridgeService doesn't expose Clear in current implementation - // Shutdown and reinitialize to clear all bodies - physicsBridge_.reset(); - physicsBridge_ = std::make_unique(logger_); + physicsBridge_->Clear(); } } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/bullet_physics_service.hpp b/src/services/impl/bullet_physics_service.hpp index d666f64..7844cdd 100644 --- a/src/services/impl/bullet_physics_service.hpp +++ b/src/services/impl/bullet_physics_service.hpp @@ -58,6 +58,7 @@ public: private: std::shared_ptr logger_; std::unique_ptr physicsBridge_; + btVector3 gravity_ = btVector3(0.0f, -9.8f, 0.0f); bool initialized_ = false; }; diff --git a/src/services/impl/gui_script_service.cpp b/src/services/impl/gui_script_service.cpp index a42548c..7c0162b 100644 --- a/src/services/impl/gui_script_service.cpp +++ b/src/services/impl/gui_script_service.cpp @@ -136,6 +136,24 @@ std::vector GuiScriptService::LoadGuiCommands() { } else if (std::strcmp(typeName, "text") == 0) { command.type = GuiCommand::Type::Text; ReadStringField(L, commandIndex, "text", command.text); + bool hasX = false; + bool hasY = false; + lua_getfield(L, commandIndex, "x"); + if (lua_isnumber(L, -1)) { + command.rect.x = static_cast(lua_tonumber(L, -1)); + hasX = true; + } + lua_pop(L, 1); + lua_getfield(L, commandIndex, "y"); + if (lua_isnumber(L, -1)) { + command.rect.y = static_cast(lua_tonumber(L, -1)); + hasY = true; + } + lua_pop(L, 1); + if (logger_ && (!hasX || !hasY)) { + logger_->Trace("GuiScriptService", "LoadGuiCommands", + "Text command missing x/y; defaulting to 0"); + } lua_getfield(L, commandIndex, "fontSize"); if (lua_isnumber(L, -1)) { command.fontSize = static_cast(lua_tonumber(L, -1)); @@ -163,7 +181,17 @@ std::vector GuiScriptService::LoadGuiCommands() { command.color = ReadColorField(L, commandIndex, "color", GuiColor{1.0f, 1.0f, 1.0f, 1.0f}); } else if (std::strcmp(typeName, "clip_push") == 0) { command.type = GuiCommand::Type::ClipPush; - command.rect = ReadRect(L, commandIndex); + lua_getfield(L, commandIndex, "rect"); + if (lua_istable(L, -1)) { + command.rect = ReadRect(L, -1); + } else { + command.rect = ReadRect(L, commandIndex); + if (logger_) { + logger_->Trace("GuiScriptService", "LoadGuiCommands", + "clipPushFallback=true"); + } + } + lua_pop(L, 1); } else if (std::strcmp(typeName, "clip_pop") == 0) { command.type = GuiCommand::Type::ClipPop; } else if (std::strcmp(typeName, "svg") == 0) { diff --git a/src/services/impl/gui_script_service.hpp b/src/services/impl/gui_script_service.hpp index 8e49ee5..b77c03f 100644 --- a/src/services/impl/gui_script_service.hpp +++ b/src/services/impl/gui_script_service.hpp @@ -31,6 +31,7 @@ public: private: lua_State* GetLuaState() const; GuiCommand::RectData ReadRect(lua_State* L, int index) const; + float ReadNumberField(lua_State* L, int index, const char* name, float defaultValue) const; GuiColor ReadColor(lua_State* L, int index, const GuiColor& defaultColor) const; GuiColor ReadColorField(lua_State* L, int index, const char* name, const GuiColor& defaultColor) const; bool ReadStringField(lua_State* L, int index, const char* name, std::string& outString) const; diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 805c1b9..db9cd3f 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -440,6 +440,87 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, } } + if (document.HasMember("materialx_materials")) { + const auto& materialsValue = document["materialx_materials"]; + if (!materialsValue.IsArray()) { + throw std::runtime_error("JSON member 'materialx_materials' must be an array"); + } + config.materialXMaterials.clear(); + for (rapidjson::SizeType i = 0; i < materialsValue.Size(); ++i) { + const auto& entry = materialsValue[i]; + if (!entry.IsObject()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + "]' must be an object"); + } + MaterialXMaterialConfig materialConfig; + if (entry.HasMember("enabled")) { + const auto& value = entry["enabled"]; + if (!value.IsBool()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].enabled' must be a boolean"); + } + materialConfig.enabled = value.GetBool(); + } + if (entry.HasMember("document")) { + const auto& value = entry["document"]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].document' must be a string"); + } + materialConfig.documentPath = value.GetString(); + } + if (entry.HasMember("shader_key")) { + const auto& value = entry["shader_key"]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].shader_key' must be a string"); + } + materialConfig.shaderKey = value.GetString(); + } + if (entry.HasMember("material")) { + const auto& value = entry["material"]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].material' must be a string"); + } + materialConfig.materialName = value.GetString(); + } + if (entry.HasMember("use_constant_color")) { + const auto& value = entry["use_constant_color"]; + if (!value.IsBool()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].use_constant_color' must be a boolean"); + } + materialConfig.useConstantColor = value.GetBool(); + } + if (entry.HasMember("constant_color")) { + const auto& value = entry["constant_color"]; + if (!value.IsArray() || value.Size() != 3) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].constant_color' must be an array of 3 numbers"); + } + for (rapidjson::SizeType channel = 0; channel < 3; ++channel) { + if (!value[channel].IsNumber()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].constant_color[" + std::to_string(channel) + + "]' must be a number"); + } + materialConfig.constantColor[channel] = static_cast(value[channel].GetDouble()); + } + } + + if (materialConfig.shaderKey.empty()) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].shader_key' must be provided"); + } + if (materialConfig.documentPath.empty() && !materialConfig.useConstantColor) { + throw std::runtime_error("JSON member 'materialx_materials[" + std::to_string(i) + + "].document' is required when use_constant_color is false"); + } + + config.materialXMaterials.push_back(std::move(materialConfig)); + } + } + if (document.HasMember("gui_font")) { const auto& fontValue = document["gui_font"]; if (!fontValue.IsObject()) { @@ -550,6 +631,31 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, materialObject.AddMember("constant_color", constantColor, allocator); document.AddMember("materialx", materialObject, allocator); + if (!config.materialXMaterials.empty()) { + rapidjson::Value materialsArray(rapidjson::kArrayType); + for (const auto& material : config.materialXMaterials) { + rapidjson::Value entry(rapidjson::kObjectType); + entry.AddMember("enabled", material.enabled, allocator); + entry.AddMember("document", + rapidjson::Value(material.documentPath.string().c_str(), allocator), + allocator); + entry.AddMember("shader_key", + rapidjson::Value(material.shaderKey.c_str(), allocator), + allocator); + entry.AddMember("material", + rapidjson::Value(material.materialName.c_str(), allocator), + allocator); + entry.AddMember("use_constant_color", material.useConstantColor, allocator); + rapidjson::Value materialColor(rapidjson::kArrayType); + materialColor.PushBack(material.constantColor[0], allocator); + materialColor.PushBack(material.constantColor[1], allocator); + materialColor.PushBack(material.constantColor[2], allocator); + entry.AddMember("constant_color", materialColor, allocator); + materialsArray.PushBack(entry, allocator); + } + document.AddMember("materialx_materials", materialsArray, allocator); + } + rapidjson::Value fontObject(rapidjson::kObjectType); fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator); fontObject.AddMember("font_path", diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index 54bc406..300f8b3 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -99,6 +99,12 @@ public: } return config_.materialX; } + const std::vector& GetMaterialXMaterialConfigs() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetMaterialXMaterialConfigs"); + } + return config_.materialXMaterials; + } const GuiFontConfig& GetGuiFontConfig() const override { if (logger_) { logger_->Trace("JsonConfigService", "GetGuiFontConfig"); diff --git a/src/services/impl/lua_helpers.cpp b/src/services/impl/lua_helpers.cpp index d477779..a2c5107 100644 --- a/src/services/impl/lua_helpers.cpp +++ b/src/services/impl/lua_helpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace sdl3cpp::services::impl::lua { @@ -28,6 +29,25 @@ std::array ReadVector3(lua_State* L, int index) { return result; } +std::array ReadVector2(lua_State* L, int index) { + std::array result{}; + int absIndex = lua_absindex(L, index); + size_t len = lua_rawlen(L, absIndex); + if (len != 2) { + throw std::runtime_error("Expected vector with 2 components"); + } + for (size_t i = 1; i <= 2; ++i) { + lua_rawgeti(L, absIndex, static_cast(i)); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + throw std::runtime_error("Vector component is not a number"); + } + result[i - 1] = static_cast(lua_tonumber(L, -1)); + lua_pop(L, 1); + } + return result; +} + std::array ReadQuaternion(lua_State* L, int index) { std::array result{}; int absIndex = lua_absindex(L, index); @@ -99,6 +119,70 @@ void PushMatrix(lua_State* L, const glm::mat4& matrix) { } // namespace +int LuaGlmMatrixIdentity(lua_State* L) { + glm::mat4 matrix(1.0f); + PushMatrix(L, matrix); + return 1; +} + +int LuaGlmMatrixMultiply(lua_State* L) { + std::array left = ReadMatrix(L, 1); + std::array right = ReadMatrix(L, 2); + glm::mat4 leftMat = glm::make_mat4(left.data()); + glm::mat4 rightMat = glm::make_mat4(right.data()); + glm::mat4 combined = leftMat * rightMat; + PushMatrix(L, combined); + return 1; +} + +int LuaGlmMatrixTranslation(lua_State* L) { + float x = static_cast(luaL_checknumber(L, 1)); + float y = static_cast(luaL_checknumber(L, 2)); + float z = static_cast(luaL_checknumber(L, 3)); + glm::mat4 matrix = glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z)); + PushMatrix(L, matrix); + return 1; +} + +int LuaGlmMatrixRotationX(lua_State* L) { + float radians = static_cast(luaL_checknumber(L, 1)); + glm::mat4 matrix = glm::rotate(glm::mat4(1.0f), -radians, glm::vec3(1.0f, 0.0f, 0.0f)); + PushMatrix(L, matrix); + return 1; +} + +int LuaGlmMatrixRotationY(lua_State* L) { + float radians = static_cast(luaL_checknumber(L, 1)); + glm::mat4 matrix = glm::rotate(glm::mat4(1.0f), -radians, glm::vec3(0.0f, 1.0f, 0.0f)); + PushMatrix(L, matrix); + return 1; +} + +int LuaGlmMatrixLookAt(lua_State* L) { + std::array eye = ReadVector3(L, 1); + std::array center = ReadVector3(L, 2); + std::array up = ReadVector3(L, 3); + glm::mat4 matrix = glm::lookAt(ToVec3(eye), ToVec3(center), ToVec3(up)); + PushMatrix(L, matrix); + return 1; +} + +int LuaGlmMatrixPerspective(lua_State* L) { + float fov = static_cast(luaL_checknumber(L, 1)); + float aspect = static_cast(luaL_checknumber(L, 2)); + float zNear = static_cast(luaL_checknumber(L, 3)); + float zFar = static_cast(luaL_checknumber(L, 4)); + float tanHalf = std::tan(fov * 0.5f); + glm::mat4 matrix(0.0f); + matrix[0][0] = 1.0f / (aspect * tanHalf); + matrix[1][1] = -1.0f / tanHalf; + matrix[2][2] = zFar / (zNear - zFar); + matrix[2][3] = -1.0f; + matrix[3][2] = (zNear * zFar) / (zNear - zFar); + PushMatrix(L, matrix); + return 1; +} + int LuaGlmMatrixFromTransform(lua_State* L) { std::array translation = ReadVector3(L, 1); std::array rotation = ReadQuaternion(L, 2); diff --git a/src/services/impl/lua_helpers.hpp b/src/services/impl/lua_helpers.hpp index ad18666..d7abaf9 100644 --- a/src/services/impl/lua_helpers.hpp +++ b/src/services/impl/lua_helpers.hpp @@ -8,10 +8,18 @@ struct lua_State; namespace sdl3cpp::services::impl::lua { std::array ReadVector3(lua_State* L, int index); +std::array ReadVector2(lua_State* L, int index); std::array ReadQuaternion(lua_State* L, int index); std::array ReadMatrix(lua_State* L, int index); std::string GetLuaError(lua_State* L); std::array IdentityMatrix(); +int LuaGlmMatrixIdentity(lua_State* L); +int LuaGlmMatrixMultiply(lua_State* L); +int LuaGlmMatrixTranslation(lua_State* L); +int LuaGlmMatrixRotationX(lua_State* L); +int LuaGlmMatrixRotationY(lua_State* L); +int LuaGlmMatrixLookAt(lua_State* L); +int LuaGlmMatrixPerspective(lua_State* L); int LuaGlmMatrixFromTransform(lua_State* L); } // namespace sdl3cpp::services::impl::lua diff --git a/src/services/impl/materialx_shader_generator.cpp b/src/services/impl/materialx_shader_generator.cpp index ebb63a8..f5eb71a 100644 --- a/src/services/impl/materialx_shader_generator.cpp +++ b/src/services/impl/materialx_shader_generator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include @@ -167,17 +169,19 @@ void AddFallbackTokenSubstitutions(mx::StringMap& substitutions, } } +template +struct HasHwAiryFresnelIterations : std::false_type {}; + template -constexpr bool HasHwAiryFresnelIterations = requires(const T& options) { - options.hwAiryFresnelIterations; -}; +struct HasHwAiryFresnelIterations().hwAiryFresnelIterations)>> + : std::true_type {}; template unsigned int ResolveAiryFresnelIterationsFromOptions(const Options& options, unsigned int defaultIterations, bool& fromOptions, const std::shared_ptr& logger) { - if constexpr (HasHwAiryFresnelIterations) { + if constexpr (HasHwAiryFresnelIterations::value) { fromOptions = true; if (logger) { logger->Trace("MaterialXShaderGenerator", "Generate", @@ -450,6 +454,177 @@ std::string ConvertIndividualInputsToBlock(const std::string& source) { return result; } +std::vector BuildTextureSearchRoots( + const std::filesystem::path& documentPath, + const std::filesystem::path& libraryPath, + const std::filesystem::path& scriptDirectory) { + std::vector roots; + auto addRoot = [&](const std::filesystem::path& root) { + if (root.empty()) { + return; + } + std::error_code ec; + auto canonical = std::filesystem::weakly_canonical(root, ec); + if (ec) { + canonical = root; + } + roots.push_back(canonical); + }; + + if (!documentPath.empty()) { + addRoot(documentPath.parent_path()); + } + if (!libraryPath.empty()) { + addRoot(libraryPath); + addRoot(libraryPath.parent_path()); + } + if (!scriptDirectory.empty()) { + addRoot(scriptDirectory); + addRoot(scriptDirectory.parent_path()); + } + addRoot(std::filesystem::current_path()); + + std::unordered_set seen; + std::vector unique; + for (const auto& root : roots) { + auto key = root.string(); + if (seen.insert(key).second) { + unique.push_back(root); + } + } + return unique; +} + +std::filesystem::path ResolveTexturePath(const std::string& filename, + const std::filesystem::path& documentPath, + const std::vector& searchRoots, + const std::shared_ptr& logger) { + if (filename.empty()) { + return {}; + } + + std::filesystem::path candidate(filename); + auto tryPath = [&](const std::filesystem::path& path) -> std::filesystem::path { + std::error_code ec; + if (std::filesystem::exists(path, ec)) { + return std::filesystem::weakly_canonical(path, ec); + } + return {}; + }; + + if (candidate.is_absolute()) { + auto resolved = tryPath(candidate); + if (!resolved.empty()) { + return resolved; + } + } + + if (!documentPath.empty()) { + auto resolved = tryPath(documentPath.parent_path() / candidate); + if (!resolved.empty()) { + return resolved; + } + } + + for (const auto& root : searchRoots) { + auto resolved = tryPath(root / candidate); + if (!resolved.empty()) { + return resolved; + } + } + + if (!candidate.has_parent_path()) { + for (const auto& root : searchRoots) { + std::error_code ec; + for (const auto& entry : std::filesystem::recursive_directory_iterator(root, ec)) { + if (ec) { + break; + } + if (!entry.is_regular_file(ec)) { + continue; + } + if (entry.path().filename() == candidate) { + return entry.path(); + } + } + } + } + + if (logger) { + logger->Trace("MaterialXShaderGenerator", "Generate", + "texturePathResolutionFailed file=" + filename); + } + return {}; +} + +std::vector CollectTextureBindings( + const mx::Shader& shader, + const std::filesystem::path& documentPath, + const std::filesystem::path& libraryPath, + const std::filesystem::path& scriptDirectory, + const std::shared_ptr& logger) { + std::vector bindings; + const mx::ShaderStage& pixelStage = shader.getStage(mx::Stage::PIXEL); + const auto& uniformBlocks = pixelStage.getUniformBlocks(); + if (uniformBlocks.empty()) { + return bindings; + } + + const auto searchRoots = BuildTextureSearchRoots(documentPath, libraryPath, scriptDirectory); + std::unordered_set seenUniforms; + + for (const auto& entry : uniformBlocks) { + const auto& block = entry.second; + if (!block) { + continue; + } + for (size_t i = 0; i < block->size(); ++i) { + const mx::ShaderPort* port = (*block)[i]; + if (!port) { + continue; + } + const auto& type = port->getType(); + if (type.getName() != "filename") { + continue; + } + mx::ValuePtr value = port->getValue(); + if (!value) { + continue; + } + std::string file = value->getValueString(); + if (file.empty()) { + continue; + } + std::string uniformName = port->getVariable(); + if (uniformName.empty()) { + uniformName = port->getName(); + } + if (uniformName.empty()) { + continue; + } + if (!seenUniforms.insert(uniformName).second) { + continue; + } + auto resolved = ResolveTexturePath(file, documentPath, searchRoots, logger); + if (resolved.empty()) { + continue; + } + ShaderPaths::TextureBinding binding; + binding.uniformName = uniformName; + binding.path = resolved.string(); + bindings.push_back(std::move(binding)); + if (logger) { + logger->Trace("MaterialXShaderGenerator", "Generate", + "textureBinding uniform=" + uniformName + + ", file=" + file + + ", resolved=" + resolved.string()); + } + } + } + + return bindings; +} + } // namespace MaterialXShaderGenerator::MaterialXShaderGenerator(std::shared_ptr logger) @@ -521,6 +696,7 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config, context.registerSourceCodeSearchPath(sourceSearchPath); mx::ShaderPtr shader; + std::filesystem::path documentPath; if (config.useConstantColor) { mx::Color3 color(config.constantColor[0], config.constantColor[1], config.constantColor[2]); shader = mx::createConstantShader(context, stdLib, config.shaderKey, color); @@ -533,7 +709,7 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config, throw std::runtime_error("MaterialX document path is required when use_constant_color is false"); } - std::filesystem::path documentPath = ResolvePath(config.documentPath, scriptDirectory); + documentPath = ResolvePath(config.documentPath, scriptDirectory); if (documentPath.empty()) { throw std::runtime_error("MaterialX document path could not be resolved"); } @@ -596,6 +772,7 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config, ShaderPaths paths; paths.vertexSource = shader->getSourceCode(mx::Stage::VERTEX); paths.fragmentSource = shader->getSourceCode(mx::Stage::PIXEL); + paths.textures = CollectTextureBindings(*shader, documentPath, libraryPath, scriptDirectory, logger_); // Fix vertex shader outputs: convert individual layout outputs to VertexData block // MaterialX VkShaderGenerator incorrectly emits individual out variables instead of diff --git a/src/services/impl/mesh_service.cpp b/src/services/impl/mesh_service.cpp index 425cd68..6a50d1c 100644 --- a/src/services/impl/mesh_service.cpp +++ b/src/services/impl/mesh_service.cpp @@ -7,8 +7,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -103,6 +106,260 @@ private: } }; +bool ReadArchiveEntry(zip_t* archive, + const std::string& entryPath, + const zip_stat_t& entryStat, + std::vector& buffer, + std::string& outError) { + if (!archive) { + outError = "Archive handle is null"; + return false; + } + if (entryStat.size == 0) { + outError = "Archive entry is empty: " + entryPath; + return false; + } + if (entryStat.size > std::numeric_limits::max()) { + outError = "Archive entry exceeds addressable size: " + entryPath; + return false; + } + + std::unique_ptr file( + zip_fopen(archive, entryPath.c_str(), ZIP_FL_ENC_GUESS)); + if (!file) { + outError = "Failed to open archive entry: " + BuildZipArchiveErrorMessage(archive); + return false; + } + + size_t entrySize = static_cast(entryStat.size); + buffer.assign(entrySize, 0); + zip_int64_t totalRead = 0; + while (static_cast(totalRead) < entrySize) { + zip_int64_t bytesRead = zip_fread(file.get(), + buffer.data() + totalRead, + entrySize - static_cast(totalRead)); + if (bytesRead < 0) { + outError = "Failed to read archive entry: " + BuildZipArchiveErrorMessage(archive); + return false; + } + if (bytesRead == 0) { + break; + } + totalRead += bytesRead; + } + if (static_cast(totalRead) != entrySize) { + outError = "Archive entry read incomplete: " + entryPath; + return false; + } + + return true; +} + +#pragma pack(push, 1) +struct BspHeader { + char id[4]; + int32_t version; +}; + +struct BspLump { + int32_t offset; + int32_t length; +}; + +struct BspVertex { + float position[3]; + float texCoord[2]; + float lightmapCoord[2]; + float normal[3]; + uint8_t color[4]; +}; + +struct BspFace { + int32_t textureId; + int32_t effect; + int32_t type; + int32_t vertexIndex; + int32_t numVertices; + int32_t meshVertIndex; + int32_t numMeshVerts; + int32_t lightmapId; + int32_t lightmapCorner[2]; + int32_t lightmapSize[2]; + float lightmapPos[3]; + float lightmapVecs[2][3]; + float normal[3]; + int32_t patchSize[2]; +}; +#pragma pack(pop) + +static_assert(sizeof(BspHeader) == 8, "Unexpected BSP header size"); +static_assert(sizeof(BspLump) == 8, "Unexpected BSP lump size"); +static_assert(sizeof(BspVertex) == 44, "Unexpected BSP vertex size"); +static_assert(sizeof(BspFace) == 104, "Unexpected BSP face size"); + +bool BuildPayloadFromBspBuffer(const std::vector& buffer, + MeshPayload& outPayload, + std::string& outError, + const std::shared_ptr& logger) { + constexpr int32_t kBspExpectedVersion = 46; + constexpr size_t kBspLumpCount = 17; + constexpr size_t kBspVerticesLump = 10; + constexpr size_t kBspMeshVertsLump = 11; + constexpr size_t kBspFacesLump = 13; + constexpr int32_t kBspFacePolygon = 1; + constexpr int32_t kBspFaceMesh = 3; + + if (buffer.size() < sizeof(BspHeader) + kBspLumpCount * sizeof(BspLump)) { + outError = "BSP buffer is too small to contain header and lumps"; + return false; + } + + BspHeader header{}; + std::memcpy(&header, buffer.data(), sizeof(BspHeader)); + if (std::string(header.id, sizeof(header.id)) != "IBSP") { + outError = "BSP header mismatch: expected IBSP"; + return false; + } + if (header.version != kBspExpectedVersion) { + outError = "Unsupported BSP version: " + std::to_string(header.version); + return false; + } + + std::array lumps{}; + std::memcpy(lumps.data(), buffer.data() + sizeof(BspHeader), + kBspLumpCount * sizeof(BspLump)); + + auto validateLump = [&](size_t index, + size_t elementSize, + size_t& outCount, + const std::string& label) -> bool { + const BspLump& lump = lumps[index]; + if (lump.offset < 0 || lump.length < 0) { + outError = "BSP " + label + " lump has negative bounds"; + return false; + } + size_t offset = static_cast(lump.offset); + size_t length = static_cast(lump.length); + if (offset + length > buffer.size()) { + outError = "BSP " + label + " lump exceeds buffer size"; + return false; + } + if (length % elementSize != 0) { + outError = "BSP " + label + " lump size is not aligned"; + return false; + } + outCount = length / elementSize; + return true; + }; + + size_t vertexCount = 0; + size_t meshVertCount = 0; + size_t faceCount = 0; + if (!validateLump(kBspVerticesLump, sizeof(BspVertex), vertexCount, "vertex")) { + return false; + } + if (!validateLump(kBspMeshVertsLump, sizeof(int32_t), meshVertCount, "meshvert")) { + return false; + } + if (!validateLump(kBspFacesLump, sizeof(BspFace), faceCount, "face")) { + return false; + } + if (vertexCount == 0) { + outError = "BSP contains no vertices"; + return false; + } + if (meshVertCount == 0 || faceCount == 0) { + outError = "BSP contains no face indices"; + return false; + } + + const uint8_t* vertexData = buffer.data() + lumps[kBspVerticesLump].offset; + const uint8_t* meshVertData = buffer.data() + lumps[kBspMeshVertsLump].offset; + const uint8_t* faceData = buffer.data() + lumps[kBspFacesLump].offset; + + std::vector vertices(vertexCount); + std::memcpy(vertices.data(), vertexData, vertexCount * sizeof(BspVertex)); + + std::vector meshVerts(meshVertCount); + std::memcpy(meshVerts.data(), meshVertData, meshVertCount * sizeof(int32_t)); + + outPayload.positions.resize(vertexCount); + outPayload.normals.resize(vertexCount); + outPayload.colors.resize(vertexCount); + outPayload.indices.clear(); + + for (size_t i = 0; i < vertexCount; ++i) { + const BspVertex& vertex = vertices[i]; + outPayload.positions[i] = {vertex.position[0], vertex.position[1], vertex.position[2]}; + outPayload.normals[i] = {vertex.normal[0], vertex.normal[1], vertex.normal[2]}; + outPayload.colors[i] = { + static_cast(vertex.color[0]) / 255.0f, + static_cast(vertex.color[1]) / 255.0f, + static_cast(vertex.color[2]) / 255.0f + }; + } + + size_t trianglesBuilt = 0; + size_t trianglesSkipped = 0; + for (size_t faceIndex = 0; faceIndex < faceCount; ++faceIndex) { + BspFace face{}; + std::memcpy(&face, faceData + faceIndex * sizeof(BspFace), sizeof(BspFace)); + if (face.type != kBspFacePolygon && face.type != kBspFaceMesh) { + continue; + } + if (face.numMeshVerts < 3) { + continue; + } + + for (int32_t i = 0; i + 2 < face.numMeshVerts; i += 3) { + int32_t meshIndex = face.meshVertIndex + i; + if (meshIndex < 0 || static_cast(meshIndex + 2) >= meshVerts.size()) { + ++trianglesSkipped; + continue; + } + int32_t index0 = face.vertexIndex + meshVerts[static_cast(meshIndex)]; + int32_t index1 = face.vertexIndex + meshVerts[static_cast(meshIndex + 1)]; + int32_t index2 = face.vertexIndex + meshVerts[static_cast(meshIndex + 2)]; + if (index0 < 0 || index1 < 0 || index2 < 0) { + ++trianglesSkipped; + continue; + } + if (static_cast(index0) >= vertexCount || + static_cast(index1) >= vertexCount || + static_cast(index2) >= vertexCount) { + ++trianglesSkipped; + continue; + } + + outPayload.indices.push_back(static_cast(index0)); + outPayload.indices.push_back(static_cast(index1)); + outPayload.indices.push_back(static_cast(index2)); + ++trianglesBuilt; + } + } + + if (logger) { + logger->Trace("MeshService", "BuildPayloadFromBspBuffer", + "vertexCount=" + std::to_string(vertexCount) + + ", meshVertCount=" + std::to_string(meshVertCount) + + ", faceCount=" + std::to_string(faceCount) + + ", trianglesBuilt=" + std::to_string(trianglesBuilt) + + ", trianglesSkipped=" + std::to_string(trianglesSkipped)); + } + + if (outPayload.indices.empty()) { + outError = "BSP contains no triangle faces"; + return false; + } + if (outPayload.positions.size() > std::numeric_limits::max()) { + outError = "Mesh vertex count exceeds uint16_t index range: " + + std::to_string(outPayload.positions.size()); + return false; + } + + return true; +} + bool WriteArchiveEntryToTempFile(const std::vector& buffer, const std::string& entryPath, const std::string& extensionHint, @@ -158,6 +415,56 @@ aiColor3D ResolveMaterialColor(const aiScene* scene, const aiMesh* mesh) { return defaultColor; } +struct MeshBounds { + aiVector3D min; + aiVector3D max; +}; + +MeshBounds ComputeMeshBounds(const aiMesh* mesh) { + MeshBounds bounds{}; + bounds.min = aiVector3D(std::numeric_limits::max()); + bounds.max = aiVector3D(std::numeric_limits::lowest()); + if (!mesh || !mesh->mNumVertices) { + return bounds; + } + for (unsigned i = 0; i < mesh->mNumVertices; ++i) { + const aiVector3D& v = mesh->mVertices[i]; + bounds.min.x = std::min(bounds.min.x, v.x); + bounds.min.y = std::min(bounds.min.y, v.y); + bounds.min.z = std::min(bounds.min.z, v.z); + bounds.max.x = std::max(bounds.max.x, v.x); + bounds.max.y = std::max(bounds.max.y, v.y); + bounds.max.z = std::max(bounds.max.z, v.z); + } + return bounds; +} + +float NormalizeCoord(float value, float minValue, float maxValue) { + float range = maxValue - minValue; + if (range == 0.0f) { + return 0.0f; + } + return (value - minValue) / range; +} + +std::array ComputeFallbackTexcoord(const aiVector3D& position, + const aiVector3D& normal, + const MeshBounds& bounds) { + float ax = std::fabs(normal.x); + float ay = std::fabs(normal.y); + float az = std::fabs(normal.z); + if (ax >= ay && ax >= az) { + return {NormalizeCoord(position.z, bounds.min.z, bounds.max.z), + NormalizeCoord(position.y, bounds.min.y, bounds.max.y)}; + } + if (ay >= ax && ay >= az) { + return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x), + NormalizeCoord(position.z, bounds.min.z, bounds.max.z)}; + } + return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x), + NormalizeCoord(position.y, bounds.min.y, bounds.max.y)}; +} + bool AppendMeshPayload(const aiScene* scene, const aiMesh* mesh, MeshPayload& outPayload, @@ -176,15 +483,19 @@ bool AppendMeshPayload(const aiScene* scene, } aiColor3D materialColor = ResolveMaterialColor(scene, mesh); + const MeshBounds bounds = ComputeMeshBounds(mesh); + const bool hasTexcoords = mesh->HasTextureCoords(0) && mesh->mTextureCoords[0]; size_t positionsStart = outPayload.positions.size(); size_t normalsStart = outPayload.normals.size(); size_t colorsStart = outPayload.colors.size(); + size_t texcoordsStart = outPayload.texcoords.size(); size_t indicesStart = outPayload.indices.size(); outPayload.positions.reserve(positionsStart + mesh->mNumVertices); outPayload.normals.reserve(normalsStart + mesh->mNumVertices); outPayload.colors.reserve(colorsStart + mesh->mNumVertices); + outPayload.texcoords.reserve(texcoordsStart + mesh->mNumVertices); outPayload.indices.reserve(indicesStart + mesh->mNumFaces * 3); for (unsigned i = 0; i < mesh->mNumVertices; ++i) { @@ -203,6 +514,13 @@ bool AppendMeshPayload(const aiScene* scene, color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b); } outPayload.colors.push_back({color.r, color.g, color.b}); + + if (hasTexcoords) { + const aiVector3D& uv = mesh->mTextureCoords[0][i]; + outPayload.texcoords.push_back({uv.x, uv.y}); + } else { + outPayload.texcoords.push_back(ComputeFallbackTexcoord(vertex, normal, bounds)); + } } for (unsigned faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) { @@ -379,55 +697,56 @@ bool MeshService::LoadFromArchive(const std::string& archivePath, logger_->Trace("MeshService", "LoadFromArchive", "Detected BSP entry; using archive importer. archive=" + resolvedArchive.string() + ", entry=" + entryPath); + logger_->Trace("MeshService", "LoadFromArchive", + "BSP entry size=" + std::to_string(entryStat.size)); } std::string importName = resolvedArchive.string() + "," + entryPath; Assimp::Importer importer; importer.SetIOHandler(new ArchiveMapAwareIOSystem()); const aiScene* scene = importer.ReadFile(importName, kAssimpLoadFlags); + if (scene && logger_) { + unsigned int rootMeshes = 0; + unsigned int rootChildren = 0; + if (scene->mRootNode) { + rootMeshes = scene->mRootNode->mNumMeshes; + rootChildren = scene->mRootNode->mNumChildren; + } + logger_->Trace("MeshService", "LoadFromArchive", + "BSP scene stats: meshes=" + std::to_string(scene->mNumMeshes) + + ", materials=" + std::to_string(scene->mNumMaterials) + + ", textures=" + std::to_string(scene->mNumTextures) + + ", rootMeshes=" + std::to_string(rootMeshes) + + ", rootChildren=" + std::to_string(rootChildren)); + } + std::string assimpError; if (!scene) { - std::string importError = importer.GetErrorString() ? importer.GetErrorString() - : "Assimp failed to load BSP from archive"; - outError = "Assimp failed to load BSP from archive: " + importError; + assimpError = importer.GetErrorString() ? importer.GetErrorString() + : "Assimp failed to load BSP from archive"; + } else if (buildPayload(scene, assimpError)) { + return true; + } + + if (logger_) { + logger_->Trace("MeshService", "LoadFromArchive", + "Assimp BSP import did not yield meshes, falling back to BSP parser: " + + assimpError); + } + + std::vector buffer; + std::string readError; + if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, readError)) { + outError = "Failed to read BSP entry for fallback parser: " + readError; return false; } - return buildPayload(scene, outError); - } - - if (entryStat.size == 0) { - outError = "Archive entry is empty: " + entryPath; - return false; - } - if (entryStat.size > std::numeric_limits::max()) { - outError = "Archive entry exceeds addressable size: " + entryPath; - return false; - } - - std::unique_ptr file( - zip_fopen(archive.get(), entryPath.c_str(), ZIP_FL_ENC_GUESS)); - if (!file) { - outError = "Failed to open archive entry: " + BuildZipArchiveErrorMessage(archive.get()); - return false; - } - - size_t entrySize = static_cast(entryStat.size); - std::vector buffer(entrySize); - zip_int64_t totalRead = 0; - while (static_cast(totalRead) < entrySize) { - zip_int64_t bytesRead = zip_fread(file.get(), - buffer.data() + totalRead, - entrySize - static_cast(totalRead)); - if (bytesRead < 0) { - outError = "Failed to read archive entry: " + BuildZipArchiveErrorMessage(archive.get()); + if (!BuildPayloadFromBspBuffer(buffer, outPayload, outError, logger_)) { return false; } - if (bytesRead == 0) { - break; - } - totalRead += bytesRead; + return true; } - if (static_cast(totalRead) != entrySize) { - outError = "Archive entry read incomplete: " + entryPath; + + std::vector buffer; + if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, outError)) { return false; } @@ -522,6 +841,7 @@ void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) { "positions.size=" + std::to_string(payload.positions.size()) + ", normals.size=" + std::to_string(payload.normals.size()) + ", colors.size=" + std::to_string(payload.colors.size()) + + ", texcoords.size=" + std::to_string(payload.texcoords.size()) + ", indices.size=" + std::to_string(payload.indices.size()) + ", luaStateIsNull=" + std::string(L ? "false" : "true")); } @@ -556,6 +876,17 @@ void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) { } lua_setfield(L, -2, "color"); + lua_newtable(L); + std::array texcoord = {0.0f, 0.0f}; + if (vertexIndex < payload.texcoords.size()) { + texcoord = payload.texcoords[vertexIndex]; + } + for (int component = 0; component < 2; ++component) { + lua_pushnumber(L, texcoord[component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "texcoord"); + lua_rawseti(L, -2, static_cast(vertexIndex + 1)); } lua_setfield(L, -2, "vertices"); diff --git a/src/services/impl/physics_bridge_service.cpp b/src/services/impl/physics_bridge_service.cpp index f1f234f..615e4e1 100644 --- a/src/services/impl/physics_bridge_service.cpp +++ b/src/services/impl/physics_bridge_service.cpp @@ -36,6 +36,22 @@ PhysicsBridgeService::~PhysicsBridgeService() { } } +bool PhysicsBridgeService::SetGravity(const btVector3& gravity, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "SetGravity", + "gravity.x=" + std::to_string(gravity.getX()) + + ", gravity.y=" + std::to_string(gravity.getY()) + + ", gravity.z=" + std::to_string(gravity.getZ())); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + world_->setGravity(gravity); + return true; +} + bool PhysicsBridgeService::AddBoxRigidBody(const std::string& name, const btVector3& halfExtents, float mass, @@ -56,6 +72,14 @@ bool PhysicsBridgeService::AddBoxRigidBody(const std::string& name, error = "Rigid body name must not be empty"; return false; } + if (halfExtents.getX() <= 0.0f || halfExtents.getY() <= 0.0f || halfExtents.getZ() <= 0.0f) { + error = "Box half extents must be positive"; + return false; + } + if (mass < 0.0f) { + error = "Rigid body mass must be non-negative"; + return false; + } if (!world_) { error = "Physics world is not initialized"; return false; @@ -85,14 +109,194 @@ bool PhysicsBridgeService::AddBoxRigidBody(const std::string& name, return true; } -int PhysicsBridgeService::StepSimulation(float deltaTime) { +bool PhysicsBridgeService::AddSphereRigidBody(const std::string& name, + float radius, + float mass, + const btTransform& transform, + std::string& error) { if (logger_) { - logger_->Trace("PhysicsBridgeService", "StepSimulation", "deltaTime=" + std::to_string(deltaTime)); + logger_->Trace("PhysicsBridgeService", "AddSphereRigidBody", + "name=" + name + + ", radius=" + std::to_string(radius) + + ", mass=" + std::to_string(mass) + + ", origin.x=" + std::to_string(transform.getOrigin().getX()) + + ", origin.y=" + std::to_string(transform.getOrigin().getY()) + + ", origin.z=" + std::to_string(transform.getOrigin().getZ())); + } + if (name.empty()) { + error = "Rigid body name must not be empty"; + return false; + } + if (radius <= 0.0f) { + error = "Sphere radius must be positive"; + return false; + } + if (mass < 0.0f) { + error = "Rigid body mass must be non-negative"; + return false; + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + if (bodies_.count(name)) { + error = "Rigid body already exists: " + name; + return false; + } + auto shape = std::make_unique(radius); + btVector3 inertia(0.0f, 0.0f, 0.0f); + if (mass > 0.0f) { + shape->calculateLocalInertia(mass, inertia); + } + auto motionState = std::make_unique(transform); + btRigidBody::btRigidBodyConstructionInfo constructionInfo( + mass, + motionState.get(), + shape.get(), + inertia); + auto body = std::make_unique(constructionInfo); + world_->addRigidBody(body.get()); + bodies_.emplace(name, BodyRecord{ + std::move(shape), + std::move(motionState), + std::move(body), + }); + return true; +} + +bool PhysicsBridgeService::RemoveRigidBody(const std::string& name, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "RemoveRigidBody", "name=" + name); + } + if (name.empty()) { + error = "Rigid body name must not be empty"; + return false; + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto it = bodies_.find(name); + if (it == bodies_.end()) { + error = "Rigid body not found: " + name; + return false; + } + if (it->second.body) { + world_->removeRigidBody(it->second.body.get()); + } + bodies_.erase(it); + return true; +} + +bool PhysicsBridgeService::SetRigidBodyTransform(const std::string& name, + const btTransform& transform, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "SetRigidBodyTransform", + "name=" + name + + ", origin.x=" + std::to_string(transform.getOrigin().getX()) + + ", origin.y=" + std::to_string(transform.getOrigin().getY()) + + ", origin.z=" + std::to_string(transform.getOrigin().getZ())); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto* record = FindBodyRecord(name, error); + if (!record || !record->body) { + return false; + } + record->body->setWorldTransform(transform); + if (record->motionState) { + record->motionState->setWorldTransform(transform); + } + record->body->activate(true); + return true; +} + +bool PhysicsBridgeService::ApplyForce(const std::string& name, + const btVector3& force, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "ApplyForce", + "name=" + name + + ", force.x=" + std::to_string(force.getX()) + + ", force.y=" + std::to_string(force.getY()) + + ", force.z=" + std::to_string(force.getZ())); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto* record = FindBodyRecord(name, error); + if (!record || !record->body) { + return false; + } + record->body->applyCentralForce(force); + record->body->activate(true); + return true; +} + +bool PhysicsBridgeService::ApplyImpulse(const std::string& name, + const btVector3& impulse, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "ApplyImpulse", + "name=" + name + + ", impulse.x=" + std::to_string(impulse.getX()) + + ", impulse.y=" + std::to_string(impulse.getY()) + + ", impulse.z=" + std::to_string(impulse.getZ())); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto* record = FindBodyRecord(name, error); + if (!record || !record->body) { + return false; + } + record->body->applyCentralImpulse(impulse); + record->body->activate(true); + return true; +} + +bool PhysicsBridgeService::SetLinearVelocity(const std::string& name, + const btVector3& velocity, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "SetLinearVelocity", + "name=" + name + + ", velocity.x=" + std::to_string(velocity.getX()) + + ", velocity.y=" + std::to_string(velocity.getY()) + + ", velocity.z=" + std::to_string(velocity.getZ())); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto* record = FindBodyRecord(name, error); + if (!record || !record->body) { + return false; + } + record->body->setLinearVelocity(velocity); + record->body->activate(true); + return true; +} + +int PhysicsBridgeService::StepSimulation(float deltaTime, int maxSubSteps) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "StepSimulation", + "deltaTime=" + std::to_string(deltaTime) + + ", maxSubSteps=" + std::to_string(maxSubSteps)); } if (!world_) { return 0; } - return static_cast(world_->stepSimulation(deltaTime, 10, 1.0f / 60.0f)); + if (maxSubSteps < 0) { + maxSubSteps = 0; + } + return static_cast(world_->stepSimulation(deltaTime, maxSubSteps, 1.0f / 60.0f)); } bool PhysicsBridgeService::GetRigidBodyTransform(const std::string& name, @@ -101,17 +305,69 @@ bool PhysicsBridgeService::GetRigidBodyTransform(const std::string& name, if (logger_) { logger_->Trace("PhysicsBridgeService", "GetRigidBodyTransform", "name=" + name); } + auto* record = FindBodyRecord(name, error); + if (!record) { + return false; + } + if (record->motionState) { + record->motionState->getWorldTransform(outTransform); + return true; + } + if (record->body) { + outTransform = record->body->getWorldTransform(); + return true; + } + error = "Rigid body handle is missing"; + return false; +} + +size_t PhysicsBridgeService::GetBodyCount() const { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "GetBodyCount"); + } + return bodies_.size(); +} + +void PhysicsBridgeService::Clear() { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "Clear"); + } + if (world_) { + for (auto& [name, entry] : bodies_) { + if (entry.body) { + world_->removeRigidBody(entry.body.get()); + } + } + } + bodies_.clear(); +} + +PhysicsBridgeService::BodyRecord* PhysicsBridgeService::FindBodyRecord(const std::string& name, + std::string& error) { auto it = bodies_.find(name); if (it == bodies_.end()) { error = "Rigid body not found: " + name; - return false; + return nullptr; } - if (!it->second.motionState) { - error = "Rigid body motion state is missing"; - return false; + if (!it->second.body) { + error = "Rigid body handle is missing"; + return nullptr; } - it->second.motionState->getWorldTransform(outTransform); - return true; + return &it->second; +} + +const PhysicsBridgeService::BodyRecord* PhysicsBridgeService::FindBodyRecord(const std::string& name, + std::string& error) const { + auto it = bodies_.find(name); + if (it == bodies_.end()) { + error = "Rigid body not found: " + name; + return nullptr; + } + if (!it->second.body) { + error = "Rigid body handle is missing"; + return nullptr; + } + return &it->second; } } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/physics_bridge_service.hpp b/src/services/impl/physics_bridge_service.hpp index bb751da..26ac218 100644 --- a/src/services/impl/physics_bridge_service.hpp +++ b/src/services/impl/physics_bridge_service.hpp @@ -27,15 +27,39 @@ public: explicit PhysicsBridgeService(std::shared_ptr logger); ~PhysicsBridgeService() override; + bool SetGravity(const btVector3& gravity, + std::string& error) override; + bool AddBoxRigidBody(const std::string& name, const btVector3& halfExtents, float mass, const btTransform& transform, std::string& error) override; - int StepSimulation(float deltaTime) override; + bool AddSphereRigidBody(const std::string& name, + float radius, + float mass, + const btTransform& transform, + std::string& error) override; + bool RemoveRigidBody(const std::string& name, + std::string& error) override; + bool SetRigidBodyTransform(const std::string& name, + const btTransform& transform, + std::string& error) override; + bool ApplyForce(const std::string& name, + const btVector3& force, + std::string& error) override; + bool ApplyImpulse(const std::string& name, + const btVector3& impulse, + std::string& error) override; + bool SetLinearVelocity(const std::string& name, + const btVector3& velocity, + std::string& error) override; + int StepSimulation(float deltaTime, int maxSubSteps = 10) override; bool GetRigidBodyTransform(const std::string& name, btTransform& outTransform, std::string& error) const override; + size_t GetBodyCount() const override; + void Clear() override; private: struct BodyRecord { @@ -44,6 +68,9 @@ private: std::unique_ptr body; }; + BodyRecord* FindBodyRecord(const std::string& name, std::string& error); + const BodyRecord* FindBodyRecord(const std::string& name, std::string& error) const; + std::unique_ptr collisionConfig_; std::unique_ptr dispatcher_; std::unique_ptr broadphase_; diff --git a/src/services/impl/scene_script_service.cpp b/src/services/impl/scene_script_service.cpp index 5314e77..226a25c 100644 --- a/src/services/impl/scene_script_service.cpp +++ b/src/services/impl/scene_script_service.cpp @@ -99,6 +99,14 @@ std::vector ReadVertexArray(lua_State* L, int index, const std::sh vertex.color = lua::ReadVector3(L, -1); lua_pop(L, 1); + lua_getfield(L, vertexIndex, "texcoord"); + if (lua_istable(L, -1)) { + vertex.texcoord = lua::ReadVector2(L, -1); + } else { + vertex.texcoord = {0.0f, 0.0f}; + } + lua_pop(L, 1); + lua_pop(L, 1); vertices.push_back(vertex); } diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index 655dbce..9dc87bf 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -473,8 +473,24 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("load_mesh_from_file", &ScriptEngineService::LoadMeshFromFile); bind("load_mesh_from_pk3", &ScriptEngineService::LoadMeshFromArchive); bind("physics_create_box", &ScriptEngineService::PhysicsCreateBox); + bind("physics_create_sphere", &ScriptEngineService::PhysicsCreateSphere); + bind("physics_remove_body", &ScriptEngineService::PhysicsRemoveBody); + bind("physics_set_transform", &ScriptEngineService::PhysicsSetTransform); + bind("physics_apply_force", &ScriptEngineService::PhysicsApplyForce); + bind("physics_apply_impulse", &ScriptEngineService::PhysicsApplyImpulse); + bind("physics_set_linear_velocity", &ScriptEngineService::PhysicsSetLinearVelocity); + bind("physics_set_gravity", &ScriptEngineService::PhysicsSetGravity); bind("physics_step_simulation", &ScriptEngineService::PhysicsStepSimulation); bind("physics_get_transform", &ScriptEngineService::PhysicsGetTransform); + bind("physics_get_body_count", &ScriptEngineService::PhysicsGetBodyCount); + bind("physics_clear", &ScriptEngineService::PhysicsClear); + bind("glm_matrix_identity", &ScriptEngineService::GlmMatrixIdentity); + bind("glm_matrix_multiply", &ScriptEngineService::GlmMatrixMultiply); + bind("glm_matrix_translation", &ScriptEngineService::GlmMatrixTranslation); + bind("glm_matrix_rotation_x", &ScriptEngineService::GlmMatrixRotationX); + bind("glm_matrix_rotation_y", &ScriptEngineService::GlmMatrixRotationY); + bind("glm_matrix_look_at", &ScriptEngineService::GlmMatrixLookAt); + bind("glm_matrix_perspective", &ScriptEngineService::GlmMatrixPerspective); bind("glm_matrix_from_transform", &ScriptEngineService::GlmMatrixFromTransform); bind("audio_play_background", &ScriptEngineService::AudioPlayBackground); bind("audio_play_sound", &ScriptEngineService::AudioPlaySound); @@ -604,6 +620,249 @@ int ScriptEngineService::PhysicsCreateBox(lua_State* L) { return 1; } +int ScriptEngineService::PhysicsCreateSphere(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsCreateSphere", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 4) || !lua_istable(L, 5)) { + luaL_error(L, "physics_create_sphere expects vector tables for origin and rotation"); + } + + float radius = static_cast(luaL_checknumber(L, 2)); + float mass = static_cast(luaL_checknumber(L, 3)); + std::array origin = lua::ReadVector3(L, 4); + std::array rotation = lua::ReadQuaternion(L, 5); + + btTransform transform; + transform.setIdentity(); + transform.setOrigin(btVector3(origin[0], origin[1], origin[2])); + transform.setRotation(btQuaternion(rotation[0], rotation[1], rotation[2], rotation[3])); + + std::string error; + if (!context->physicsBridgeService->AddSphereRigidBody(name, radius, mass, transform, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsRemoveBody(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsRemoveBody", + "name=" + std::string(name)); + } + + std::string error; + if (!context->physicsBridgeService->RemoveRigidBody(name, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsSetTransform(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsSetTransform", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 2) || !lua_istable(L, 3)) { + luaL_error(L, "physics_set_transform expects vector tables for origin and rotation"); + } + + std::array origin = lua::ReadVector3(L, 2); + std::array rotation = lua::ReadQuaternion(L, 3); + + btTransform transform; + transform.setIdentity(); + transform.setOrigin(btVector3(origin[0], origin[1], origin[2])); + transform.setRotation(btQuaternion(rotation[0], rotation[1], rotation[2], rotation[3])); + + std::string error; + if (!context->physicsBridgeService->SetRigidBodyTransform(name, transform, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsApplyForce(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsApplyForce", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 2)) { + luaL_error(L, "physics_apply_force expects a vector table for force"); + } + + std::array force = lua::ReadVector3(L, 2); + + std::string error; + if (!context->physicsBridgeService->ApplyForce( + name, + btVector3(force[0], force[1], force[2]), + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsApplyImpulse(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsApplyImpulse", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 2)) { + luaL_error(L, "physics_apply_impulse expects a vector table for impulse"); + } + + std::array impulse = lua::ReadVector3(L, 2); + + std::string error; + if (!context->physicsBridgeService->ApplyImpulse( + name, + btVector3(impulse[0], impulse[1], impulse[2]), + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsSetLinearVelocity(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsSetLinearVelocity", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 2)) { + luaL_error(L, "physics_set_linear_velocity expects a vector table for velocity"); + } + + std::array velocity = lua::ReadVector3(L, 2); + + std::string error; + if (!context->physicsBridgeService->SetLinearVelocity( + name, + btVector3(velocity[0], velocity[1], velocity[2]), + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::PhysicsSetGravity(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + if (!lua_istable(L, 1)) { + luaL_error(L, "physics_set_gravity expects a vector table for gravity"); + } + + std::array gravity = lua::ReadVector3(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsSetGravity", + "gravity.x=" + std::to_string(gravity[0]) + + ", gravity.y=" + std::to_string(gravity[1]) + + ", gravity.z=" + std::to_string(gravity[2])); + } + + std::string error; + if (!context->physicsBridgeService->SetGravity( + btVector3(gravity[0], gravity[1], gravity[2]), + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + int ScriptEngineService::PhysicsStepSimulation(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; @@ -615,7 +874,11 @@ int ScriptEngineService::PhysicsStepSimulation(lua_State* L) { logger->Trace("ScriptEngineService", "PhysicsStepSimulation"); } float deltaTime = static_cast(luaL_checknumber(L, 1)); - int steps = context->physicsBridgeService->StepSimulation(deltaTime); + int maxSubSteps = 10; + if (lua_gettop(L) >= 2 && lua_isnumber(L, 2)) { + maxSubSteps = static_cast(lua_tointeger(L, 2)); + } + int steps = context->physicsBridgeService->StepSimulation(deltaTime, maxSubSteps); lua_pushinteger(L, steps); return 1; } @@ -668,6 +931,37 @@ int ScriptEngineService::PhysicsGetTransform(lua_State* L) { return 1; } +int ScriptEngineService::PhysicsGetBodyCount(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsGetBodyCount"); + } + lua_pushinteger(L, static_cast(context->physicsBridgeService->GetBodyCount())); + return 1; +} + +int ScriptEngineService::PhysicsClear(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsClear"); + } + context->physicsBridgeService->Clear(); + lua_pushboolean(L, 1); + return 1; +} + int ScriptEngineService::AudioPlayBackground(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; @@ -1083,6 +1377,69 @@ int ScriptEngineService::WindowIsCursorVisible(lua_State* L) { return 1; } +int ScriptEngineService::GlmMatrixIdentity(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixIdentity"); + } + return lua::LuaGlmMatrixIdentity(L); +} + +int ScriptEngineService::GlmMatrixMultiply(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixMultiply"); + } + return lua::LuaGlmMatrixMultiply(L); +} + +int ScriptEngineService::GlmMatrixTranslation(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixTranslation"); + } + return lua::LuaGlmMatrixTranslation(L); +} + +int ScriptEngineService::GlmMatrixRotationX(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixRotationX"); + } + return lua::LuaGlmMatrixRotationX(L); +} + +int ScriptEngineService::GlmMatrixRotationY(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixRotationY"); + } + return lua::LuaGlmMatrixRotationY(L); +} + +int ScriptEngineService::GlmMatrixLookAt(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixLookAt"); + } + return lua::LuaGlmMatrixLookAt(L); +} + +int ScriptEngineService::GlmMatrixPerspective(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (logger) { + logger->Trace("ScriptEngineService", "GlmMatrixPerspective"); + } + return lua::LuaGlmMatrixPerspective(L); +} + int ScriptEngineService::GlmMatrixFromTransform(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; diff --git a/src/services/impl/script_engine_service.hpp b/src/services/impl/script_engine_service.hpp index 2294e7b..14022fb 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -63,11 +63,27 @@ private: static int LoadMeshFromFile(lua_State* L); static int LoadMeshFromArchive(lua_State* L); static int PhysicsCreateBox(lua_State* L); + static int PhysicsCreateSphere(lua_State* L); + static int PhysicsRemoveBody(lua_State* L); + static int PhysicsSetTransform(lua_State* L); + static int PhysicsApplyForce(lua_State* L); + static int PhysicsApplyImpulse(lua_State* L); + static int PhysicsSetLinearVelocity(lua_State* L); + static int PhysicsSetGravity(lua_State* L); static int PhysicsStepSimulation(lua_State* L); static int PhysicsGetTransform(lua_State* L); + static int PhysicsGetBodyCount(lua_State* L); + static int PhysicsClear(lua_State* L); static int AudioPlayBackground(lua_State* L); static int AudioPlaySound(lua_State* L); static int AudioStopBackground(lua_State* L); + static int GlmMatrixIdentity(lua_State* L); + static int GlmMatrixMultiply(lua_State* L); + static int GlmMatrixTranslation(lua_State* L); + static int GlmMatrixRotationX(lua_State* L); + static int GlmMatrixRotationY(lua_State* L); + static int GlmMatrixLookAt(lua_State* L); + static int GlmMatrixPerspective(lua_State* L); static int GlmMatrixFromTransform(lua_State* L); static int InputGetMousePosition(lua_State* L); static int InputGetMouseDelta(lua_State* L); diff --git a/src/services/impl/shader_script_service.cpp b/src/services/impl/shader_script_service.cpp index 1c44445..82d06c2 100644 --- a/src/services/impl/shader_script_service.cpp +++ b/src/services/impl/shader_script_service.cpp @@ -72,7 +72,34 @@ std::unordered_map ShaderScriptService::LoadShaderPath if (configService_) { const auto& materialConfig = configService_->GetMaterialXConfig(); - if (materialConfig.enabled) { + const auto& materialOverrides = configService_->GetMaterialXMaterialConfigs(); + if (!materialOverrides.empty()) { + for (const auto& overrideConfig : materialOverrides) { + if (!overrideConfig.enabled) { + continue; + } + MaterialXConfig resolvedConfig = materialConfig; + resolvedConfig.enabled = true; + resolvedConfig.documentPath = overrideConfig.documentPath; + resolvedConfig.shaderKey = overrideConfig.shaderKey; + resolvedConfig.materialName = overrideConfig.materialName; + resolvedConfig.useConstantColor = overrideConfig.useConstantColor; + resolvedConfig.constantColor = overrideConfig.constantColor; + try { + ShaderPaths materialShader = materialxGenerator_.Generate( + resolvedConfig, + engineService_ ? engineService_->GetScriptDirectory() : std::filesystem::path{}); + if (!resolvedConfig.shaderKey.empty()) { + shaderMap[resolvedConfig.shaderKey] = std::move(materialShader); + } + } catch (const std::exception& ex) { + if (logger_) { + logger_->Error("MaterialX shader generation failed for key=" + + overrideConfig.shaderKey + ": " + std::string(ex.what())); + } + } + } + } else if (materialConfig.enabled) { try { ShaderPaths materialShader = materialxGenerator_.Generate( materialConfig, diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index ef50bfe..c1dcf30 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -105,8 +105,17 @@ struct MaterialXConfig { std::array constantColor = {1.0f, 1.0f, 1.0f}; }; +struct MaterialXMaterialConfig { + bool enabled = true; + std::filesystem::path documentPath; + std::string shaderKey; + std::string materialName; + bool useConstantColor = false; + std::array constantColor = {1.0f, 1.0f, 1.0f}; +}; + struct GuiFontConfig { - bool useFreeType = false; + bool useFreeType = true; std::filesystem::path fontPath; float fontSize = 18.0f; }; @@ -125,6 +134,7 @@ struct RuntimeConfig { AtmosphericsConfig atmospherics{}; BgfxConfig bgfx{}; MaterialXConfig materialX{}; + std::vector materialXMaterials{}; GuiFontConfig guiFont{}; float guiOpacity = 1.0f; }; diff --git a/src/services/interfaces/graphics_types.hpp b/src/services/interfaces/graphics_types.hpp index 78f4107..34e3742 100644 --- a/src/services/interfaces/graphics_types.hpp +++ b/src/services/interfaces/graphics_types.hpp @@ -30,6 +30,11 @@ struct ShaderPaths { std::string tessEvalSource; std::string compute; std::string computeSource; + struct TextureBinding { + std::string uniformName; + std::string path; + }; + std::vector textures{}; bool disableCulling = false; bool disableDepthTest = false; }; diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index a8c6edc..8bbf556 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -73,6 +73,12 @@ public: */ virtual const MaterialXConfig& GetMaterialXConfig() const = 0; + /** + * @brief Get MaterialX material overrides. + * @return MaterialX material configurations + */ + virtual const std::vector& GetMaterialXMaterialConfigs() const = 0; + /** * @brief Get GUI font settings. * @return GUI font configuration diff --git a/src/services/interfaces/i_physics_bridge_service.hpp b/src/services/interfaces/i_physics_bridge_service.hpp index 7d12c90..58c6ef8 100644 --- a/src/services/interfaces/i_physics_bridge_service.hpp +++ b/src/services/interfaces/i_physics_bridge_service.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include class btVector3; @@ -14,15 +15,39 @@ class IPhysicsBridgeService { public: virtual ~IPhysicsBridgeService() = default; + virtual bool SetGravity(const btVector3& gravity, + std::string& error) = 0; + virtual bool AddBoxRigidBody(const std::string& name, const btVector3& halfExtents, float mass, const btTransform& transform, std::string& error) = 0; - virtual int StepSimulation(float deltaTime) = 0; + virtual bool AddSphereRigidBody(const std::string& name, + float radius, + float mass, + const btTransform& transform, + std::string& error) = 0; + virtual bool RemoveRigidBody(const std::string& name, + std::string& error) = 0; + virtual bool SetRigidBodyTransform(const std::string& name, + const btTransform& transform, + std::string& error) = 0; + virtual bool ApplyForce(const std::string& name, + const btVector3& force, + std::string& error) = 0; + virtual bool ApplyImpulse(const std::string& name, + const btVector3& impulse, + std::string& error) = 0; + virtual bool SetLinearVelocity(const std::string& name, + const btVector3& velocity, + std::string& error) = 0; + virtual int StepSimulation(float deltaTime, int maxSubSteps = 10) = 0; virtual bool GetRigidBodyTransform(const std::string& name, btTransform& outTransform, std::string& error) const = 0; + virtual size_t GetBodyCount() const = 0; + virtual void Clear() = 0; }; } // namespace sdl3cpp::services diff --git a/src/services/interfaces/mesh_types.hpp b/src/services/interfaces/mesh_types.hpp index d025ee6..56399db 100644 --- a/src/services/interfaces/mesh_types.hpp +++ b/src/services/interfaces/mesh_types.hpp @@ -10,6 +10,7 @@ struct MeshPayload { std::vector> positions; std::vector> normals; std::vector> colors; + std::vector> texcoords; std::vector indices; }; diff --git a/src/stb_image.cpp b/src/stb_image.cpp new file mode 100644 index 0000000..9177288 --- /dev/null +++ b/src/stb_image.cpp @@ -0,0 +1,2 @@ +#define STB_IMAGE_IMPLEMENTATION +#include