feat(physics): Enhance physics bridge service with new functionalities

- Added SetGravity method to adjust the gravity in the physics world.
- Introduced AddSphereRigidBody method for creating sphere rigid bodies.
- Implemented RemoveRigidBody method to delete existing rigid bodies.
- Added SetRigidBodyTransform method to update the position and rotation of rigid bodies.
- Included ApplyForce and ApplyImpulse methods for applying forces and impulses to rigid bodies.
- Added SetLinearVelocity method to set the linear velocity of rigid bodies.
- Enhanced StepSimulation method to accept a maxSubSteps parameter.
- Implemented GetBodyCount method to retrieve the number of rigid bodies in the world.
- Added Clear method to remove all rigid bodies from the physics world.
- Updated the script engine service to bind new physics methods to Lua.
- Enhanced material configuration handling in shader script service.
- Introduced MaterialXMaterialConfig structure for better material management.
- Added texture binding support in ShaderPaths structure.
- Included stb_image implementation for image loading support.
This commit is contained in:
2026-01-07 00:20:19 +00:00
parent ffeba6c142
commit cb5b58ca9e
42 changed files with 3168 additions and 226 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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()