more physics tweaks (disabled NGS because it behaved poorly, added some small code to keep objects when grounded to moving bodies, fixed oversight with kinematic bodies), made darkengine lightmaps properly dark with baking it as srgb

This commit is contained in:
ecker 2026-07-21 00:11:43 -05:00
parent 5cc441bb63
commit b309a60b8c
16 changed files with 157 additions and 31 deletions

View File

@ -26,7 +26,7 @@ ent:addHook("link:Message.%UID%", function(payload)
if explicitSchema ~= "" then
local resolvedUrl = _G.DarkUtils.playSound(ent, "", explicitSchema, {
spatial = false, streamed = true, volume = 1.0, unique = true, loop = false
spatial = false, streamed = true, volume = 0.2, unique = true, loop = false
})
if resolvedUrl then isPlaying = true end
end

View File

@ -1,11 +1,21 @@
local ent = ent
local scene = entities.currentScene()
local physicsBody = ent:getComponent("PhysicsBody")
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local touching = {}
local tripFlags = darkMeta["trip_flags"] or 0
local triggerOnEnter = bit.band(tripFlags, 1) ~= 0 or tripFlags == 0
local triggerOnExit = bit.band(tripFlags, 2) ~= 0
local triggerOnce = bit.band(tripFlags, 8) ~= 0
local triggerPlayer = bit.band(tripFlags, 32) ~= 0
local hasFired = false
ent:bind( "tick", function(self)
if not physicsBody:initialized() then return end
if triggerOnce and hasFired then return end
local currentCollisions = {}
local collisionEvents = physicsBody:getCollisionEvents()
@ -19,12 +29,18 @@ ent:bind( "tick", function(self)
end
if other then
local uid = other:getObject():uid()
currentCollisions[uid] = true
local otherEnt = other:getObject()
local otherUid = otherEnt:uid()
currentCollisions[otherUid] = true
if not touching[uid] then
touching[uid] = true
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOn" }, 0)
if triggerPlayer and otherEnt:name() ~= "Player" then return end
if not touching[otherUid] then
touching[otherUid] = true
if triggerOnEnter then
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOn" }, 0)
if triggerOnce then hasFired = true end
end
end
end
end
@ -32,7 +48,12 @@ ent:bind( "tick", function(self)
for uid, _ in pairs(touching) do
if not currentCollisions[uid] then
touching[uid] = nil
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOff" }, 0)
if triggerOnExit and not (triggerOnce and hasFired) then
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOn" }, 0)
if triggerOnce then hasFired = true end
elseif triggerOnEnter then
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOff" }, 0)
end
end
end
end )

View File

@ -23,7 +23,9 @@
"timescale": 32
}
},
"ambient": [ 0, 0, 0 ]
"ambient": [ 0, 0, 0 ],
"exposure": 0.75,
"gamma": 1.0
}
}
}

View File

@ -39,8 +39,16 @@ void pbr() {
const float Ldistance = sqrt(Lmagnitude);
// "free" normalization, since we need to compute the above values anyways
Li = Li / Ldistance;
// calculate the smooth windowing
const float radius2 = lights[i].radius * lights[i].radius;
float window = 1.0f;
if ( radius2 > 0.0f ) {
float distOverRadius = Lmagnitude / radius2;
window = max(0.0, 1.0 - (distOverRadius * distOverRadius));
window = window * window; // smoother curve
}
// attenuation factor
const float Lattenuation = 1.0 / (1 + Lmagnitude);
const float Lattenuation = window / (1.0 + Lmagnitude);
// skip if attenuation factor is too low
// if ( Lattenuation <= LIGHT_POWER_CUTOFF ) continue;
// ray cast if our surface is occluded from the light

View File

@ -72,6 +72,8 @@ namespace uf {
void UF_API maxDistance( pod::AudioSource& source, float v );
float UF_API referenceDistance( const pod::AudioSource& source );
void UF_API referenceDistance( pod::AudioSource& source, float v );
void UF_API spatial( pod::AudioSource& source, bool s );
float UF_API distance( const pod::Vector3f& position );
float UF_API occlusion( const pod::Vector3f& position );

View File

@ -53,5 +53,6 @@ namespace impl {
void applyRollingResistance( pod::PhysicsBody& body, float dt );
void snapVelocity( pod::PhysicsBody& body, float dt, float threshold = 0.01f );
void integrateKinematic( pod::PhysicsBody& body, float dt );
void integrate( pod::PhysicsBody& body, float dt );
}

View File

@ -368,6 +368,7 @@ namespace pod {
struct Activity {
bool awake = true;
bool grounded = false;
pod::PhysicsBody* referenceFrame = NULL;
float sleepTimer = 0.0f;
int32_t islandID = -1;
static constexpr float sleepThreshold = 0.5f; // seconds

View File

@ -70,10 +70,9 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
if ( json["rolloffFactor"].is<double>() ) uf::audio::rolloff(source, json["rolloffFactor"].as<float>());
if ( json["maxDistance"].is<double>() ) uf::audio::maxDistance(source, json["maxDistance"].as<float>());
if ( json["referenceDistance"].is<double>() ) uf::audio::referenceDistance(source, json["referenceDistance"].as<float>());
if ( json["spatial"].is<bool>() ) source.settings.spatial = json["spatial"].as<bool>();
if ( json["loop"].is<bool>() ) uf::audio::loop(source, json["loop"].as<bool>());
else if ( json["wants loop"].is<bool>() ) {
if ( json["spatial"].is<bool>() ) uf::audio::spatial( source, json["spatial"].as<bool>() );
if ( json["loop"].is<bool>() ) uf::audio::loop(source, json["loop"].as<bool>()); // explicitly set it
else if ( json["wants loop"].is<bool>() ) { // requests looping, but only if the audio file has it (for Valve-sourced WAVs)
auto wants = json["wants loop"].as<bool>(true);
uf::audio::loop(source, wants && clip && clip->info.loop.has);
}

View File

@ -760,7 +760,6 @@ void ext::ExtSceneBehavior::tick( uf::Object& self ) {
.exposure = metadata.light.exposure
};
if ( shader.hasUniform("UBO") ) shader.updateBuffer( (const void*) &uniforms, sizeof(uniforms), shader.getUniformBuffer("UBO") );
}
}

View File

@ -1085,6 +1085,44 @@ void uf::graph::process( pod::Graph& graph ) {
material.indexAlbedo = -1;
}
}
if ( ext::json::isObject( graphMetadataDark ) ) {
// set transparent
if ( name.find("glass") != uf::stl::string::npos ||
name.find("trans") != uf::stl::string::npos ||
name.find("grate") != uf::stl::string::npos ) {
material.modeAlpha = pod::Material::AlphaMode::BLEND;
material.modeCull = pod::Material::CullMode::NONE;
}
// set emissive
for ( auto nodeID = 0; nodeID < graph.nodes.size(); ++nodeID ) {
auto& node = graph.nodes[nodeID];
// check if owns a light
auto lightName = node.name;
auto nameID = ::fmt::format( "{}_{}", node.name, nodeID );
if ( graph.lights.count( nameID ) > 0 ) lightName = nameID;
if ( graph.lights.count( lightName ) == 0 ) {
continue;
}
auto& light = graph.lights[lightName];
if ( !(0 <= node.mesh && node.mesh < graph.meshes.size()) ) continue;
// iterate primitives for materials
auto& primitives = storage.primitives.map[graph.primitives[node.mesh]];
for ( auto& primitive : primitives ) {
auto materialID = primitive.instance.materialID;
if ( !(0 <= materialID && materialID <= graph.materials.size()) ) {
UF_MSG_DEBUG("node={}, lightName={} has invalid material: {}", node.name, lightName, materialID);
continue;
}
auto& materialName = graph.materials[materialID];
// set emissive
material.modeAlpha = pod::Material::AlphaMode::EMISSIVE;
material.colorEmissive = light.color * light.intensity;
UF_MSG_DEBUG("name={}, light={}, emissive={}", node.name, lightName, uf::vector::toString( material.colorEmissive ));
}
}
}
auto tag = ext::json::find( name, graphMetadataJson["tags"] );
if ( ext::json::isObject( tag ) ) {
@ -1440,11 +1478,13 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
bool emitsAudio = false;
// bind elevator
bool isElevator = false;
bool isElevator = false; // node.name.find("Elevator") != std::string::npos
bool isTrap = false; // node.name.find("Trap") != std::string::npos
if ( ext::json::isArray( metadataDark["scripts"] ) ) {
ext::json::forEach( metadataDark["scripts"], [&]( ext::json::Value& value ){
auto script = value.as<uf::stl::string>();
if ( script == "BaseElevator" || script == "Elevator" ) isElevator = true;
if ( script.ends_with("Elevator") ) isElevator = true;
if ( script.starts_with("Trap") ) isTrap = true;
});
}
@ -1485,7 +1525,7 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
}
// bind trap
if ( node.name == "Sound Trap" || node.name == "VO Trap" ) {
if ( isTrap ) {
loadJson["assets"].emplace_back("ent://scripts/dark/trap.lua");
emitsAudio = true;
// bind ambient

View File

@ -440,6 +440,8 @@ namespace impl {
uf::stl::unordered_map<int32_t, uf::stl::string> classTag;
uf::stl::unordered_map<int32_t, uf::stl::string> schMsg;
uf::stl::unordered_map<int32_t, uf::stl::string> schAction;
uf::stl::unordered_map<int32_t, int32_t> tripFlags;
uf::stl::unordered_map<int32_t, uf::stl::vector<uf::stl::string>> script;
} properties;
@ -1204,6 +1206,9 @@ namespace impl {
(float)((sample >> 5) & 0x1F),
(float)((sample ) & 0x1F),
} / 31.0f;
color = uf::vector::pow( color, 2.2f ); // convert to SRGB
impl::encodeRGBE( color, &image.pixels[(y * w + x) * 4] );
}
}
@ -1527,6 +1532,12 @@ namespace impl {
}
}
// bind trip f l ags
int32_t tripFlags = 0;
if ( ctx.findInheritedProperty( objectID, ctx.properties.tripFlags, tripFlags ) ) {
metadata["trip_flags"] = tripFlags;
}
// bind frobbage
PropertyFrobInfo frob;
bool isFrobbable = false;
@ -1623,6 +1634,7 @@ namespace impl {
}
// bind light (at the end because we insert a new node)
// to-do: verify if this doesn't double up on lights
PropertyLight light;
if ( ctx.findInheritedProperty( objectID, ctx.properties.light, light ) ) {
// create new node
@ -1632,11 +1644,15 @@ namespace impl {
lightNode.name = lightNodeName;
graph.nodes[nodeID].children.emplace_back(lightNodeID);
float radiusFactor = 1.2f;
float intensityFactor = 0.05f;
graph.lights[::fmt::format("{}_{}", lightNode.name, lightNodeID)] = {
.range = light.radius,
.range = light.radius * radiusFactor,
.color = impl::hsvToRgb(light.hue, light.saturation, 1.0f),
.intensity = light.brightness / M_PI,
.intensity = light.brightness * intensityFactor,
};
// set emissive factor
}
}
}
@ -1865,6 +1881,7 @@ namespace impl {
impl::extractProperty( ctx, "PhysState", ctx.properties.physState );
impl::extractProperty( ctx, "SchPlayParams", ctx.properties.schPlayParams );
impl::extractProperty( ctx, "SchLoopParams", ctx.properties.schLoopParams );
impl::extractProperty( ctx, "TripFlags", ctx.properties.tripFlags );
// parse schsamp
if ( inventory.count("SchSamp") > 0 ) {
@ -2066,6 +2083,8 @@ void ext::lgs::loadMis( pod::Graph& graph, const uf::stl::string& filename, cons
impl::processSongs( graph, ctx );
impl::processSchema( graph, ctx );
graph.metadata["dark"]["_"] = true;
// disable postprocessing flags
if ( filename.starts_with("game://") ) graph.metadata["exporter"]["enabled"] = false; // disable exporting if loaded from a VPK
graph.metadata["exporter"]["unwrap"] = false; // do not unwrap UVs for baking (we already have those)

View File

@ -234,7 +234,16 @@ float uf::audio::referenceDistance( const pod::AudioSource& source ) {
return v;
}
void uf::audio::referenceDistance( pod::AudioSource& source, float v ) {
source.alSource.set( AL_REFERENCE_DISTANCE, v );
source.alSource.set( AL_REFERENCE_DISTANCE, v );
}
void uf::audio::spatial( pod::AudioSource& source, bool s ) {
if ( (source.settings.spatial = s) ) {
source.alSource.set( AL_SOURCE_RELATIVE, AL_FALSE );
} else {
source.alSource.set( AL_SOURCE_RELATIVE, AL_TRUE );
source.alSource.set( AL_POSITION, 0.0f, 0.0f, 0.0f );
}
}
//

View File

@ -135,9 +135,8 @@ void uf::physics::step( pod::World& world, float dt ) {
}
}
for ( auto* body : bodies ) {
impl::integrate( *body, dt );
}
for ( auto* body : bodies ) impl::integrateKinematic( *body, dt );
for ( auto* body : bodies ) impl::integrate( *body, dt );
//UF_TIMER_MULTITRACE("Integration & Flattening");
// rebuild static bvh if dirty
@ -251,8 +250,14 @@ void uf::physics::step( pod::World& world, float dt ) {
if ( !isTrigger ) for ( auto& c : manifold.points ) {
if ( std::fabs(uf::vector::dot(c.normal, pod::Vector3f{0,1,0})) > uf::physics::settings.groundedThreshold ) {
// only mark if contact point is below body
if ( c.point.y < impl::getPosition(a).y ) a.activity.grounded = true;
if ( c.point.y < impl::getPosition(b).y ) b.activity.grounded = true;
if ( c.point.y < impl::getPosition(a).y ) {
a.activity.grounded = true;
if ( b.inverseMass == 0.0f ) a.activity.referenceFrame = &b;
}
if ( c.point.y < impl::getPosition(b).y ) {
b.activity.grounded = true;
if ( a.inverseMass == 0.0f ) b.activity.referenceFrame = &a;
}
}
}
@ -554,11 +559,7 @@ void uf::physics::applyTorque( pod::PhysicsBody& body, const pod::Vector3f& torq
}
void uf::physics::setVelocity( pod::PhysicsBody& body, const pod::Vector3f& v ) {
impl::wakeBody( body );
if ( body.inverseMass == 0.0f ) {
body.transform->position += v * uf::physics::time::delta;
} else {
body.velocity = v;
}
body.velocity = v;
}
void uf::physics::applyVelocity( pod::PhysicsBody& body, const pod::Vector3f& v ) {
impl::wakeBody( body );

View File

@ -166,7 +166,7 @@ void impl::applyRollingResistance( pod::PhysicsBody& body, float dt ) {
// snap velocity for grounded bodies
void impl::snapVelocity( pod::PhysicsBody& body, float dt, float threshold ) {
if ( !body.activity.grounded || !body.activity.awake ) return;
if ( !body.activity.grounded || !body.activity.awake || body.inverseMass == 0.0f ) return;
float threshold2 = threshold * threshold;
// snap velocity if body is grounded and nearly still
@ -181,6 +181,21 @@ void impl::snapVelocity( pod::PhysicsBody& body, float dt, float threshold ) {
if ( angSpeed2 < threshold2 ) body.angularVelocity = {};
}
void impl::integrateKinematic( pod::PhysicsBody& body, float dt ) {
if ( !body.activity.awake || body.inverseMass != 0.0f ) return;
auto& transform = *body.transform;
transform.position += body.velocity * dt;
float angularSpeed2 = uf::vector::magnitude( body.angularVelocity );
if ( angularSpeed2 > EPS2 ) {
float angularSpeed = std::sqrt( angularSpeed2 );
pod::Quaternion<> dq = uf::quaternion::axisAngle( body.angularVelocity / angularSpeed, angularSpeed * dt );
uf::transform::rotate( transform, dq );
}
}
void impl::integrate( pod::PhysicsBody& body, float dt ) {
// only integrate awake and dynamic bodies
if ( !body.activity.awake || body.inverseMass == 0.0f ) return;
@ -190,6 +205,13 @@ void impl::integrate( pod::PhysicsBody& body, float dt ) {
auto fT = uf::transform::flatten( transform );
auto gravity = uf::physics::getGravity( body );
if ( body.activity.referenceFrame ) {
auto ref = body.activity.referenceFrame->velocity;
if ( body.velocity.y > ref.y + 1.0f ) {
body.activity.referenceFrame = NULL;
} else if ( body.velocity.y > ref.y ) body.velocity.y = ref.y;
}
// linear integration
pod::Vector3f acceleration = (body.forceAccumulator * body.inverseMass);
acceleration += gravity; // apply gravity

View File

@ -41,6 +41,7 @@ namespace impl {
float vRel = uf::vector::dot((vB - vA), contact.normal);
float e = std::min(a.material.restitution, b.material.restitution);
if ( a.inverseMass == 0.0f || b.inverseMass == 0.0f) e = 0.0f;
float restitutionBias = (vRel < -vSlop) ? -e * vRel : 0.0f;
rhs[i] = -vRel + restitutionBias;

View File

@ -21,6 +21,7 @@ void impl::iterativeImpulseSolver( pod::PhysicsBody& a, pod::PhysicsBody& b, pod
float restitutionBias = 0.0f;
float e = std::min(a.material.restitution, b.material.restitution);
if ( a.inverseMass == 0.0f || b.inverseMass == 0.0f) e = 0.0f;
float velAlongNormal = uf::vector::dot(rv, contact.normal);
if ( velAlongNormal < -vSlop ) restitutionBias = -e * velAlongNormal;
float targetVelocity = restitutionBias;