feat: Add MaterialX support and enhance time retrieval in scripting

This commit is contained in:
2026-01-07 01:38:03 +00:00
parent 4f86e0488a
commit fb811f4460
8 changed files with 191 additions and 16 deletions

View File

@@ -13,6 +13,29 @@
"bgfx": {
"renderer": "vulkan"
},
"materialx": {
"enabled": true,
"parameters_enabled": true,
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
"shader_key": "pbr",
"material": "Tiled_Wood",
"library_path": "MaterialX/libraries",
"library_folders": [
"stdlib",
"pbrlib",
"lights",
"bxdf",
"cmlib",
"nprlib",
"targets"
],
"use_constant_color": false,
"constant_color": [
1.0,
1.0,
1.0
]
},
"mouse_grab": {
"enabled": true,
"grab_on_click": true,
@@ -68,7 +91,7 @@
"quake3": {
"pk3_path": "/home/rewrich/Documents/GitHub/q3/pak0.pk3",
"map_path": "q3dm1",
"scale": 0.01,
"scale": 0.03,
"rotate_x_degrees": -90.0,
"offset": [0.0, 0.0, 0.0],
"shader_key": "pbr",

View File

@@ -257,6 +257,16 @@ local controls = {
}
local last_frame_time = nil
local function get_time_seconds()
if type(time_get_seconds) == "function" then
local ok, value = pcall(time_get_seconds)
if ok and type(value) == "number" then
return value
end
end
return os.clock()
end
local movement_log_cooldown = 0.0
local world_up = {0.0, 1.0, 0.0}
local room = {
@@ -1040,7 +1050,7 @@ end
local function build_view_state(aspect)
local now = os.clock()
local now = get_time_seconds()
local dt = 0.0
if last_frame_time then
dt = now - last_frame_time

View File

@@ -99,6 +99,17 @@ local function transform_point(matrix, point)
}
end
local function transform_direction(matrix, direction)
local x = direction[1]
local y = direction[2]
local z = direction[3]
return {
matrix[1] * x + matrix[5] * y + matrix[9] * z,
matrix[2] * x + matrix[6] * y + matrix[10] * z,
matrix[3] * x + matrix[7] * y + matrix[11] * z,
}
end
local function compute_bounds(vertices, matrix)
if type(vertices) ~= "table" then
return nil
@@ -145,6 +156,10 @@ local function normalize(vec)
return {x / len, y / len, z / len}
end
local function scale_vec3(vec, scale)
return {vec[1] * scale, vec[2] * scale, vec[3] * scale}
end
local function cross(a, b)
return {
a[2] * b[3] - a[3] * b[2],
@@ -213,6 +228,7 @@ local function build_map_model_matrix()
end
local map_model_matrix = build_map_model_matrix()
local map_up = normalize(transform_direction(map_model_matrix, {0.0, 0.0, 1.0}))
if type(load_mesh_from_pk3) ~= "function" then
error("load_mesh_from_pk3() is unavailable; rebuild with libzip support")
@@ -259,7 +275,7 @@ local function compute_spawn_position(bounds)
}
end
local function is_position_far_from_bounds(position, bounds)
local function is_position_far_from_bounds(position, bounds, margin_scale)
if not bounds then
return false
end
@@ -268,7 +284,8 @@ local function is_position_far_from_bounds(position, bounds)
local extent_x = max_bounds[1] - min_bounds[1]
local extent_y = max_bounds[2] - min_bounds[2]
local extent_z = max_bounds[3] - min_bounds[3]
local margin = math.max(extent_x, extent_y, extent_z) * 0.5
local scale = resolve_number(margin_scale, 0.5)
local margin = math.max(extent_x, extent_y, extent_z) * scale
if position[1] < min_bounds[1] - margin or position[1] > max_bounds[1] + margin then
return true
end
@@ -282,17 +299,25 @@ local function is_position_far_from_bounds(position, bounds)
end
local camera_position = resolve_vec3(camera_config.position, {0.0, 48.0, 0.0})
local spawn_position = compute_spawn_position(map_bounds)
local default_spawn_position = compute_spawn_position(map_bounds)
local spawn_position = resolve_vec3(quake3_config.spawn_position, camera_position)
local auto_spawn = quake3_config.auto_spawn
if auto_spawn == nil then
auto_spawn = true
end
if auto_spawn and is_position_far_from_bounds(camera_position, map_bounds) then
camera_position = {spawn_position[1], spawn_position[2], spawn_position[3]}
if auto_spawn and is_position_far_from_bounds(camera_position, map_bounds, quake3_config.bounds_margin_scale) then
camera_position = {default_spawn_position[1], default_spawn_position[2], default_spawn_position[3]}
spawn_position = resolve_vec3(quake3_config.spawn_position, camera_position)
log_debug("Camera spawn adjusted to map bounds (%.2f, %.2f, %.2f)",
camera_position[1], camera_position[2], camera_position[3])
end
local respawn_config = resolve_table(quake3_config.respawn)
local respawn_enabled = resolve_boolean(respawn_config.enabled, true)
local respawn_margin_scale = resolve_number(
respawn_config.margin_scale,
resolve_number(quake3_config.bounds_margin_scale, 0.5))
local camera = {
position = camera_position,
yaw = math.rad(resolve_number(camera_config.yaw_degrees, 0.0)),
@@ -322,6 +347,15 @@ if physics_enabled and not physics_available then
log_debug("Physics disabled: required bindings are unavailable")
end
local align_gravity_to_map = resolve_boolean(physics_config.align_gravity_to_map, true)
local default_gravity = {0.0, -9.8, 0.0}
if align_gravity_to_map then
default_gravity = scale_vec3(map_up, -9.8)
log_debug("Gravity aligned to map up=(%.2f, %.2f, %.2f) gravity=(%.2f, %.2f, %.2f)",
map_up[1], map_up[2], map_up[3],
default_gravity[1], default_gravity[2], default_gravity[3])
end
local physics_state = {
enabled = physics_enabled and physics_available,
ready = false,
@@ -331,7 +365,7 @@ local physics_state = {
player_radius = resolve_number(physics_config.player_radius, resolve_number(quake3_config.player_radius, 0.6)),
player_mass = resolve_number(physics_config.player_mass, resolve_number(quake3_config.player_mass, 1.0)),
eye_height = resolve_number(physics_config.eye_height, resolve_number(quake3_config.eye_height, 0.6)),
gravity = resolve_vec3(physics_config.gravity, {0.0, -9.8, 0.0}),
gravity = resolve_vec3(physics_config.gravity, default_gravity),
jump_impulse = resolve_number(physics_config.jump_impulse, resolve_number(quake3_config.jump_impulse, 4.5)),
jump_velocity_threshold = resolve_number(
physics_config.jump_velocity_threshold,
@@ -340,14 +374,24 @@ local physics_state = {
}
physics_state.spawn_position = {
camera.position[1],
camera.position[2] - physics_state.eye_height,
camera.position[3],
spawn_position[1],
spawn_position[2] - physics_state.eye_height,
spawn_position[3],
}
local last_frame_time = nil
local world_up = {0.0, 1.0, 0.0}
local function get_time_seconds()
if type(time_get_seconds) == "function" then
local ok, value = pcall(time_get_seconds)
if ok and type(value) == "number" then
return value
end
end
return os.clock()
end
local function update_camera_angles()
local look_delta_x = 0.0
local look_delta_y = 0.0
@@ -502,6 +546,45 @@ local function sync_camera_from_physics()
end
end
local function reset_player_to_spawn(reason)
camera.position[1] = spawn_position[1]
camera.position[2] = spawn_position[2]
camera.position[3] = spawn_position[3]
if physics_state.enabled and physics_state.ready and type(physics_set_transform) == "function" then
local rotation = {0.0, 0.0, 0.0, 1.0}
local reset_position = {
spawn_position[1],
spawn_position[2] - physics_state.eye_height,
spawn_position[3],
}
local ok, err = physics_set_transform(
physics_state.player_body_name,
reset_position,
rotation)
if not ok then
log_debug("Physics respawn failed: %s", err or "unknown")
elseif type(physics_set_linear_velocity) == "function" then
physics_set_linear_velocity(physics_state.player_body_name, {0.0, 0.0, 0.0})
end
end
log_debug("Respawned player (%s) at (%.2f, %.2f, %.2f)",
tostring(reason or "unknown"),
spawn_position[1],
spawn_position[2],
spawn_position[3])
end
local function check_respawn(position)
if not respawn_enabled then
return
end
if is_position_far_from_bounds(position, map_bounds, respawn_margin_scale) then
reset_player_to_spawn("out_of_bounds")
end
end
local shader_variants_module = require("shader_variants")
local shader_variants = shader_variants_module.build_cube_variants(config, log_debug)
@@ -523,7 +606,7 @@ function get_shader_paths()
end
local function build_view_state(aspect)
local now = os.clock()
local now = get_time_seconds()
local dt = 0.0
if last_frame_time then
dt = now - last_frame_time
@@ -566,15 +649,18 @@ local function build_view_state(aspect)
end
if physics_state.noclip then
update_free_fly(dt, forward_flat, right)
check_respawn(camera.position)
else
apply_physics_controls(forward_flat, right)
if dt > 0.0 then
physics_step_simulation(dt, physics_state.max_sub_steps)
end
sync_camera_from_physics()
check_respawn(camera.position)
end
else
update_free_fly(dt, forward_flat, right)
check_respawn(camera.position)
end
local center = {
camera.position[1] + forward[1],

View File

@@ -670,10 +670,17 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label,
bool isVertex) const {
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1);
options.SetAutoBindUniforms(true);
options.SetAutoMapLocations(true);
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "CreateShader",
"label=" + label +
", renderer=" + RendererTypeName(bgfx::getRendererType()) +
", sourceLength=" + std::to_string(source.size()));
}
shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader;
auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options);

View File

@@ -25,6 +25,29 @@ namespace {
constexpr uint64_t kGuiSamplerFlags = BGFX_SAMPLER_U_CLAMP |
BGFX_SAMPLER_V_CLAMP;
const char* RendererTypeName(bgfx::RendererType::Enum type) {
switch (type) {
case bgfx::RendererType::Vulkan:
return "Vulkan";
case bgfx::RendererType::OpenGL:
return "OpenGL";
case bgfx::RendererType::OpenGLES:
return "OpenGLES";
case bgfx::RendererType::Direct3D11:
return "Direct3D11";
case bgfx::RendererType::Direct3D12:
return "Direct3D12";
case bgfx::RendererType::Metal:
return "Metal";
case bgfx::RendererType::Noop:
return "Noop";
case bgfx::RendererType::Count:
return "Auto";
default:
return "Unknown";
}
}
const char* kGuiVertexSource = R"(
#version 450
@@ -35,7 +58,9 @@ layout(location = 2) in vec2 inTexCoord;
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec2 fragTexCoord;
uniform mat4 u_modelViewProj;
layout(std140) uniform GuiUniforms {
mat4 u_modelViewProj;
};
void main() {
fragColor = inColor;
@@ -832,13 +857,15 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label,
bool isVertex) const {
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1);
options.SetAutoBindUniforms(true);
options.SetAutoMapLocations(true);
if (logger_) {
logger_->Trace("BgfxGuiService", "CreateShader",
"label=" + label + ", sourceLength=" + std::to_string(source.size()));
"label=" + label +
", renderer=" + std::string(RendererTypeName(bgfx::getRendererType())) +
", sourceLength=" + std::to_string(source.size()));
}
shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader;

View File

@@ -286,6 +286,7 @@ bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
outPayload.positions.resize(vertexCount);
outPayload.normals.resize(vertexCount);
outPayload.colors.resize(vertexCount);
outPayload.texcoords.resize(vertexCount);
outPayload.indices.clear();
for (size_t i = 0; i < vertexCount; ++i) {
@@ -297,6 +298,7 @@ bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
static_cast<float>(vertex.color[1]) / 255.0f,
static_cast<float>(vertex.color[2]) / 255.0f
};
outPayload.texcoords[i] = {vertex.texCoord[0], vertex.texCoord[1]};
}
size_t trianglesBuilt = 0;
@@ -344,6 +346,7 @@ bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
", meshVertCount=" + std::to_string(meshVertCount) +
", faceCount=" + std::to_string(faceCount) +
", trianglesBuilt=" + std::to_string(trianglesBuilt) +
", texcoordCount=" + std::to_string(outPayload.texcoords.size()) +
", trianglesSkipped=" + std::to_string(trianglesSkipped));
}

View File

@@ -520,6 +520,7 @@ void ScriptEngineService::RegisterBindings(lua_State* L) {
bind("config_get_json", &ScriptEngineService::ConfigGetJson);
bind("config_get_table", &ScriptEngineService::ConfigGetTable);
bind("materialx_get_surface_parameters", &ScriptEngineService::MaterialXGetSurfaceParameters);
bind("time_get_seconds", &ScriptEngineService::TimeGetSeconds);
bind("window_get_size", &ScriptEngineService::WindowGetSize);
bind("window_set_title", &ScriptEngineService::WindowSetTitle);
bind("window_is_minimized", &ScriptEngineService::WindowIsMinimized);
@@ -1320,6 +1321,23 @@ int ScriptEngineService::ConfigGetTable(lua_State* L) {
return 1;
}
int ScriptEngineService::TimeGetSeconds(lua_State* L) {
auto* context = static_cast<LuaBindingContext*>(lua_touserdata(L, lua_upvalueindex(1)));
auto logger = context ? context->logger : nullptr;
const Uint64 counter = SDL_GetPerformanceCounter();
const Uint64 frequency = SDL_GetPerformanceFrequency();
double seconds = 0.0;
if (frequency > 0) {
seconds = static_cast<double>(counter) / static_cast<double>(frequency);
}
if (logger) {
logger->Trace("ScriptEngineService", "TimeGetSeconds",
"seconds=" + std::to_string(seconds));
}
lua_pushnumber(L, seconds);
return 1;
}
int ScriptEngineService::MaterialXGetSurfaceParameters(lua_State* L) {
auto* context = static_cast<LuaBindingContext*>(lua_touserdata(L, lua_upvalueindex(1)));
auto logger = context ? context->logger : nullptr;

View File

@@ -97,6 +97,7 @@ private:
static int ConfigGetJson(lua_State* L);
static int ConfigGetTable(lua_State* L);
static int MaterialXGetSurfaceParameters(lua_State* L);
static int TimeGetSeconds(lua_State* L);
static int WindowGetSize(lua_State* L);
static int WindowSetTitle(lua_State* L);
static int WindowIsMinimized(lua_State* L);