audio effects via gaytracing, fixed IO connectivity of triggers, fixed linear doors not dooring

This commit is contained in:
ecker 2026-06-16 00:26:52 -05:00
parent de5fc31ff5
commit a834adca13
26 changed files with 527 additions and 56 deletions

View File

@ -3,10 +3,10 @@
"scenes": {
"start": "StartMenu",
"lights": { "enabled": true,
"lightmaps": false,
"lightmaps": true,
"max": 32,
"shadows": {
"enabled": true,
"enabled": false,
"update": 4,
"max": 16,
"samples": 1

View File

@ -15,6 +15,7 @@ local transform = ent:getComponent("Transform")
local physicsBody = ent:getComponent("PhysicsBody")
local metadata = ent:getComponent("Metadata")
local metadataDoor = metadata["door"] or {}
local metadataValve = metadata["valve"] or {}
local speed = metadataDoor["speed"] or 100.0
local wait = metadataDoor["wait"] or 4.0
@ -30,6 +31,19 @@ if isToggle then
wait = -1
end
local sounds = {
locked = "doors/default_locked.wav",
open = "doors/default_move.wav",
close = "doors/default_stop.wav",
}
if metadataValve["noise1"] then
sounds["open"] = metadataValve["noise1"]
end
if metadataValve["noise2"] then
sounds["close"] = metadataValve["noise2"]
end
if isRotating then
local ax = metadataDoor["axis"]
rotAxis = Vector3f(ax[1], ax[2], ax[3]):normalize()
@ -61,17 +75,24 @@ else
local size = obb.extent * 2.0
local travelSize = math.abs(moveDir:dot(size))
if travelSize < 0.8 then
travelSize = 2.4
end
targetDistance = travelSize - lip
else
targetDistance = 96.0 - lip
targetDistance = 2.4 - lip
end
if targetDistance <= 0 then targetDistance = 0.1 end
end
local soundEmitter = ent
local playSound = function( key, loop )
if not loop then loop = false end
local url = "/door/" .. key .. ".ogg"
local url = "valve://sound/" .. key
soundEmitter:queueHook("sound:Emit.%UID%", {
filename = string.resolveURI(url, metadata["system"]["root"]),
spatial = true, streamed = true, volume = "sfx", loop = loop
@ -81,7 +102,7 @@ end
local function toggleDoor( payload )
if state == 0 or state == 3 then
state = 1
playSound("default_move")
playSound(sounds["open"])
if isRotating and payload.uid ~= nil then
local isOneWay = (math.floor(flags / 16) % 2) ~= 0
@ -115,7 +136,7 @@ local function toggleDoor( payload )
end
elseif state == 2 then
state = 3
playSound("default_move")
playSound(sounds["open"])
end
end
@ -133,7 +154,7 @@ ent:bind( "tick", function(self)
currentDistance = targetDistance
state = 2
timer:reset()
playSound("default_stop")
playSound(sounds["close"])
ent:queueHook("io:FireOutput.%UID%", { output = "OnOpen" }, 0)
end
@ -145,12 +166,12 @@ ent:bind( "tick", function(self)
deltaMove = deltaMove - currentDistance
currentDistance = 0
state = 0
playSound("default_stop")
playSound(sounds["close"])
end
elseif state == 2 and wait >= 0 then
if timer:elapsed() >= wait then
state = 3
playSound("default_move")
playSound(sounds["open"])
end
end
@ -167,7 +188,6 @@ ent:bind( "tick", function(self)
else
local vec = moveDir * finalMove
if physicsBody:initialized() then
print("Moving by: ", finalMove, " Axis: ", moveDir.x, moveDir.y, moveDir.z)
physicsBody:setVelocity(moveDir * (finalMove / time.delta()))
else
transform.position = transform.position + vec
@ -191,7 +211,7 @@ ent:addHook( "entity:Use.%UID%", function( payload )
ent:queueHook("io:FireOutput.%UID%", { output = "OnUse" }, 0)
else
playSound("default_locked")
playSound(sounds["locked"])
end
end )

View File

@ -9,12 +9,58 @@ if not timer:running() then
timer:start()
end
--[[
local classname = metadataValve["classname"] or "trigger_multiple"
local wait = tonumber(metadataValve["wait"]) or 0.2
local isActive = true
if classname == "trigger_once" then
wait = -1
end
local touching = {}
local nextTriggerTime = 0
ent:bind( "tick", function(self)
if not isActive or not physicsBody:initialized() then return end
local currentCollisions = {}
local collisionEvents = physicsBody:getCollisionEvents()
for i, event in ipairs(collisionEvents) do
-- do something
-- print( event.state, event.a, event.b, event.point, event.normal, event.impulse )
local other = nil
if event.a:getObject():uid() == ent:uid() then
other = event.b
elseif event.b:getObject():uid() == ent:uid() then
other = event.a
end
if other then
local uid = other:getObject():uid()
currentCollisions[uid] = true
if not touching[uid] then
touching[uid] = true
ent:queueHook("io:FireOutput.%UID%", { output = "OnStartTouch" }, 0)
if wait == -1 or timer:elapsed() >= nextTriggerTime then
ent:queueHook("io:FireOutput.%UID%", { output = "OnTrigger" }, 0)
if wait == -1 then
isActive = false
--entities.destroy(ent)
return
else
nextTriggerTime = timer:elapsed() + wait
end
end
end
end
end
end )
]]
for uid, _ in pairs(touching) do
if not currentCollisions[uid] then
touching[uid] = nil
ent:queueHook("io:FireOutput.%UID%", { output = "OnEndTouch" }, 0)
end
end
end )

View File

@ -27,9 +27,9 @@
"resolution": 2048
} },*/
"ambient_generic": { "action": "load", "payload": { "assets": ["ent://scripts/ambient_generic.lua"], "behaviors": ["AudioEmitterBehavior"] } },
// automatically handled
// "ambient_generic": { "action": "load", "payload": { "assets": ["ent://scripts/ambient_generic.lua"], "behaviors": ["AudioEmitterBehavior"] } },
// "/^func_door/": { "action": "load", "payload": { "import": "/door.json" } },
// "/^prop_door/": { "action": "load", "payload": { "import": "/door.json" } },
// "/^prop_static/": { "action": "load", "payload": { "import": "ent://prop.json" } },

View File

@ -1,7 +1,7 @@
{
"import": "./base_sourceengine.json",
"assets": [
// { "filename": "./maps/mcdonalds-mds.bsp" }
{ "filename": "./maps/mcdonalds-mds/graph.json" }
{ "filename": "./maps/mcdonalds-mds.bsp" }
// { "filename": "./maps/mcdonalds-mds/graph.json" }
]
}

View File

@ -1,7 +1,7 @@
{
// "import": "./rp_downtown_v2.json"
// "import": "./ss2_medsci1.json"
"import": "./mds_mcdonalds.json"
// "import": "./cs_office.json"
// "import": "./mds_mcdonalds.json"
"import": "./cs_office.json"
// "import": "./gm_construct.json"
}

View File

@ -0,0 +1,46 @@
#pragma once
#include <uf/config.h>
#if UF_USE_OPENAL
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alut.h>
#define AL_ALEXT_PROTOTYPES 1
#include <AL/efx.h>
namespace ext {
namespace al {
class Filter {
protected:
ALuint m_index = 0;
public:
void initialize();
void destroy();
ALuint getIndex() const;
void set( ALenum name, ALfloat x );
void set( ALenum name, ALint x );
};
class Effect {
ALuint m_index = 0;
public:
void initialize();
void destroy();
ALuint getIndex() const;
void set( ALenum name, ALfloat x );
void set( ALenum name, ALint x );
};
class EffectSlot {
ALuint m_index = 0;
public:
void initialize();
void destroy();
ALuint getIndex() const;
void set( ALenum name, ALfloat x );
void set( ALenum name, ALint x );
};
}
}
#endif

View File

@ -27,6 +27,7 @@
#include "source.h"
#include "buffer.h"
#include "filter.h"
#include <uf/utils/audio/metadata.h>
#include <uf/utils/math/transform.h>

View File

@ -66,6 +66,10 @@ namespace uf {
void UF_API rolloff( pod::AudioSource& source, float v );
float UF_API maxDistance( const pod::AudioSource& source );
void UF_API maxDistance( pod::AudioSource& source, float v );
float UF_API occlusion( const pod::Vector3f& position );
void UF_API occlude( pod::AudioSource& source, float factor );
void UF_API acoustics( const pod::Vector3f&, const pod::Quaternion<>&, float&, float&, int& );
}
}

View File

@ -6,6 +6,11 @@ namespace uf {
typedef uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::AudioSource>> container_t;
protected:
container_t m_container;
ext::al::Effect m_effect;
ext::al::EffectSlot m_effectSlot;
bool m_efxInitialized = false;
uf::Timer<> m_acousticTimer = {true};
public:
~AudioEmitter();
@ -21,6 +26,7 @@ namespace uf {
void update();
void update( const pod::Vector3f& position, const pod::Quaternion<>& orientation );
void updateAcoustics( const pod::Vector3f& position, const pod::Quaternion<>& orientation );
void cleanup( bool purge = false );
};

View File

@ -1,6 +1,7 @@
#pragma once
#include <fstream>
#include <uf/utils/math/transform.h>
#include <uf/utils/memory/string.h>
#include <uf/utils/memory/vector.h>
@ -50,9 +51,13 @@ namespace pod {
struct UF_API AudioSource {
ext::al::Source alSource;
#if !UF_ENV_DREAMCAST
ext::al::Filter alFilter;
#endif
ext::al::Buffer streamBuffers;
pod::AudioClip* clip = NULL;
pod::Transform<> transform;
struct {
uf::Timer<> timer;

View File

@ -51,6 +51,9 @@ namespace impl {
pod::Vector3f triangleNormal( const pod::TriangleWithNormal& tri );
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID, const pod::PhysicsBody& body );
float getMaterialTransmittance( const uf::stl::string& materialName );
uf::stl::string getMaterialName( const pod::PhysicsBody& body, uint32_t triID );
/*FORCE_INLINE*/ bool aabbOverlap( const pod::AABB& a, const pod::AABB& b );
/*FORCE_INLINE*/ float aabbSurfaceArea( const pod::AABB& aabb );
/*FORCE_INLINE*/ pod::AABB computeSegmentAABB( const pod::Vector3f& p1, const pod::Vector3f p2, float r );

View File

@ -57,7 +57,8 @@ namespace uf {
pod::World& UF_API getWorld();
const pod::CollisionEvent::events_t& UF_API getCollisionEvents( const pod::World& world );
pod::CollisionEvent::events_t UF_API getCollisionEvents( const pod::PhysicsBody& body );
uf::stl::string UF_API getCollisionMaterialName( const pod::CollisionEvent& event );
pod::PhysicsBody& UF_API create( uf::Object&, float mass = 0.0f, const pod::Vector3f& = {}, const pod::Quaternion<>& = {0,0,0,1} );
pod::PhysicsBody& UF_API create( pod::World&, uf::Object&, float mass = 0.0f, const pod::Vector3f& = {}, const pod::Quaternion<>& = {0,0,0,1} );
void UF_API destroy( uf::Object& );

View File

@ -22,5 +22,9 @@ namespace uf {
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::PhysicsBody&, float = FLT_MAX );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, float = FLT_MAX );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, const pod::PhysicsBody*, float = FLT_MAX );
float UF_API occlusion( const pod::Vector3f& to, const pod::Vector3f& from );
uf::stl::string UF_API getRayMaterialName( const pod::RayQuery& query );
pod::AcousticBounce UF_API acousticReflection( const pod::Vector3f& sourcePos, const pod::Vector3f& rayDirection, const pod::Vector3f& listenerPos, float maxDistance );
}
}

View File

@ -217,6 +217,13 @@ namespace pod {
pod::Vector3f rB;
pod::Vector3f n;
};
struct AcousticBounce {
bool valid = false;
float totalDistance = 0.0f;
float retainedEnergy = 1.0f;
pod::Vector3f hitPoint;
};
}
// BVH

View File

@ -302,4 +302,11 @@ namespace std {
};
}
// maths
namespace uf {
namespace math {
uf::stl::vector<pod::Vector3f> UF_API fibonacciSphere( int points );
}
}
#include "vector/vector.inl"

View File

@ -591,7 +591,7 @@ void ext::PlayerBehavior::tick( uf::Object& self ) {
metadata.audio.footstep.timer -= uf::physics::time::delta;
if ( metadata.audio.footstep.timer <= 0.0f ) {
// player/footsteps
uf::stl::vector<uf::stl::string> surfaces = {
static uf::stl::vector<uf::stl::string> surfaces = {
"chainlink",
"concrete",
"dirt",
@ -609,30 +609,22 @@ void ext::PlayerBehavior::tick( uf::Object& self ) {
"wood",
"woodpanel",
};
uf::stl::string filename = "concrete";
uf::stl::string surface = "concrete";
auto events = uf::physics::getCollisionEvents( physicsBody );
for ( const auto& event : events ) {
if ( event.normal.y > -0.7f ) continue;
auto triID = MIN( event.featureA, event.featureB );
auto* other = event.a != &physicsBody ? event.a : event.b;
if ( triID == (uint32_t)(-1) ) continue;
if ( other->collider.type != pod::ShapeType::MESH ) continue;
// to-do: sugar it up with a helper function
auto& mesh = *other->collider.mesh.mesh;
auto drawCommand = uf::mesh::fetchDrawCommand( mesh, triID );
auto instance = uf::graph::getInstance( graph, drawCommand.instanceID );
auto materialName = uf::graph::getMaterialName( graph, instance.materialID );
if ( event.normal.y > -0.7f ) continue; // to-do: reorient?
auto materialName = uf::physics::getCollisionMaterialName( event );
for ( auto& key : surfaces ) {
if ( !uf::string::contains( materialName, key ) ) continue;
filename = key;
surface = key;
break;
}
break;
}
filename = ::fmt::format("valve://sound/player/footsteps/{}{}.wav", filename, (rand() % 3) + 1 );
uf::stl::string filename = ::fmt::format("valve://sound/player/footsteps/{}{}.wav", surface, (rand() % 3) + 1 );
if ( !uf::asset::has(filename) ) {
auto payload = uf::asset::resolveToPayload(filename, "");
payload.type = uf::asset::Type::AUDIO;

View File

@ -1298,8 +1298,12 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
loadJson["assets"].emplace_back("ent://scripts/io.lua");
}
// bind ambient
if ( node.name.starts_with("ambient_") ) {
loadJson["assets"].emplace_back("ent://scripts/ambient_generic.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
// bind door script
if ( ext::json::isObject( metadataValve["door"] ) ) {
} else if ( ext::json::isObject( metadataValve["door"] ) ) {
node.metadata["door"] = metadataValve["door"];
loadJson["imports"].emplace_back("ent://door.json");
}
@ -1335,7 +1339,14 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
}
// check if trigger
if ( 0 <= node.mesh && node.mesh < graph.meshes.size() ) {
if ( node.name.starts_with("trigger_") ) {
loadJson["assets"].emplace_back("ent://scripts/trigger.lua");
// signal to assign a physics body
if ( ext::json::isNull( node.metadata["physics"] ) ) {
node.metadata["physics"]["type"] = "bounding box";
node.metadata["physics"]["category"] = "trigger";
}
} else if ( 0 <= node.mesh && node.mesh < graph.meshes.size() ) {
auto& primitives = storage.primitives.map[graph.primitives[node.mesh]];
for ( auto& primitive : primitives ) {
auto materialID = primitive.instance.materialID;

View File

@ -32,7 +32,7 @@ void ext::al::initialize() {
}
ALboolean enumeration = alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT");
if (enumeration == AL_FALSE) {
if ( enumeration == AL_FALSE ) {
// do something
UF_EXCEPTION("Device enumeration not available");
}
@ -42,6 +42,11 @@ void ext::al::initialize() {
UF_EXCEPTION(ext::al::getError());
}
#endif
ALboolean hasEfx = alcIsExtensionPresent(::device, "ALC_EXT_EFX");
if ( hasEfx == AL_FALSE ) {
UF_MSG_WARNING("AL_EXT_EFX not supported! Spatial audio will lack occlusion.");
}
}
void ext::al::destroy() {
#if UF_USE_ALUT
@ -94,6 +99,7 @@ void ext::al::Source::get( ALenum name, ALint& x ) const { AL_CHECK_RESULT_ENUM(
void ext::al::Source::get( ALenum name, ALint& x, ALint& y, ALint& z ) const { AL_CHECK_RESULT_ENUM(alGetSource3i, this->m_index, name, &x, &y, &z ); }
void ext::al::Source::get( ALenum name, ALint* f ) const { AL_CHECK_RESULT_ENUM(alGetSourceiv, this->m_index, name, f ); }
// string=>enum not used internally at the moment
void ext::al::Source::get( const uf::stl::string& string, ALfloat& x ) const {
// alSourcef
if ( string == "PITCH" ) return this->get( AL_PITCH, x );
@ -241,4 +247,66 @@ void ext::al::Buffer::buffer(ALenum format, const ALvoid* data, ALsizei size, AL
if ( !this->initialized() ) this->initialize();
AL_CHECK_RESULT(alBufferData(this->m_indices[i], format, data, size, frequency));
}
void ext::al::Filter::initialize() {
if ( this->m_index ) this->destroy();
AL_CHECK_RESULT(alGenFilters(1, &this->m_index));
AL_CHECK_RESULT(alFilteri(this->m_index, AL_FILTER_TYPE, AL_FILTER_LOWPASS));
}
void ext::al::Filter::destroy() {
if ( this->m_index && alIsFilter(this->m_index) ) {
AL_CHECK_RESULT(alDeleteFilters(1, &this->m_index));
}
this->m_index = 0;
}
ALuint ext::al::Filter::getIndex() const {
return this->m_index;
}
void ext::al::Filter::set( ALenum name, ALfloat x ) {
AL_CHECK_RESULT_ENUM( alFilterf, this->m_index, name, x );
}
void ext::al::Filter::set( ALenum name, ALint x ) {
AL_CHECK_RESULT_ENUM( alFilteri, this->m_index, name, x );
}
void ext::al::Effect::initialize() {
if ( this->m_index ) this->destroy();
AL_CHECK_RESULT(alGenEffects(1, &this->m_index));
AL_CHECK_RESULT(alEffecti(this->m_index, AL_EFFECT_TYPE, AL_EFFECT_REVERB));
}
void ext::al::Effect::destroy() {
if ( this->m_index && alIsEffect(this->m_index) ) {
AL_CHECK_RESULT(alDeleteEffects(1, &this->m_index));
}
this->m_index = 0;
}
ALuint ext::al::Effect::getIndex() const { return this->m_index; }
void ext::al::Effect::set( ALenum name, ALfloat x ) {
AL_CHECK_RESULT_ENUM( alEffectf, this->m_index, name, x );
}
void ext::al::Effect::set( ALenum name, ALint x ) {
AL_CHECK_RESULT_ENUM( alEffecti, this->m_index, name, x );
}
void ext::al::EffectSlot::initialize() {
if ( this->m_index ) this->destroy();
AL_CHECK_RESULT(alGenAuxiliaryEffectSlots(1, &this->m_index));
}
void ext::al::EffectSlot::destroy() {
if ( this->m_index && alIsAuxiliaryEffectSlot(this->m_index) ) {
AL_CHECK_RESULT(alDeleteAuxiliaryEffectSlots(1, &this->m_index));
}
this->m_index = 0;
}
ALuint ext::al::EffectSlot::getIndex() const { return this->m_index; }
void ext::al::EffectSlot::set( ALenum name, ALfloat x ) {
AL_CHECK_RESULT_ENUM( alAuxiliaryEffectSlotf, this->m_index, name, x );
}
void ext::al::EffectSlot::set( ALenum name, ALint x ) {
AL_CHECK_RESULT_ENUM( alAuxiliaryEffectSloti, this->m_index, name, x );
}
#endif

View File

@ -595,11 +595,7 @@ namespace impl {
if ( classname.starts_with("func_door") ) {
auto& metadataDoor = metadata["door"];
metadataDoor["speed"] = metadata["speed"].as<float>(100.0f);
metadataDoor["wait"] = metadata["wait"].as<float>(4.0f);
metadataDoor["lip"] = metadata["lip"].as<float>(8.0f);
metadataDoor["spawnflags"] = metadata["spawnflags"].as<int>(0);
float scale = impl::sourceToMeters;
if ( classname == "func_door" ) {
auto movedirStr = metadata["movedir"].as<uf::stl::string>("");
if ( movedirStr == "" && metadata["angle"].as<float>() ) {
@ -610,14 +606,20 @@ namespace impl {
}
if ( movedirStr != "" ) {
pod::Transform<> t;
auto pyr = impl::str2vec<pod::Vector3f>( movedirStr ) * -DEG_2_RAD;
t.orientation = uf::quaternion::euler( pyr );
auto axes = uf::transform::axes( t );
auto pyr = impl::str2vec<pod::Vector3f>( movedirStr );
metadataDoor["direction"] = uf::vector::encode( axes.forward );
float pitch = pyr.x * DEG_2_RAD;
float yaw = pyr.y * DEG_2_RAD;
pod::Vector3f slideDir;
slideDir.x = cos(yaw) * cos(pitch);
slideDir.z = -(sin(yaw) * cos(pitch));
slideDir.y = -sin(pitch);
metadataDoor["direction"] = uf::vector::encode( uf::vector::normalize(slideDir) );
}
} else if ( classname == "func_door_rotating" ) {
scale = 1.0f;
metadataDoor["distance"] = metadata["distance"].as<float>(90.0f);
int flags = metadataDoor["spawnflags"].as<int>();
@ -627,6 +629,11 @@ namespace impl {
metadataDoor["axis"] = uf::vector::encode( axis );
}
metadataDoor["speed"] = metadata["speed"].as<float>(100.0f) * scale;
metadataDoor["wait"] = metadata["wait"].as<float>(4.0f);
metadataDoor["lip"] = metadata["lip"].as<float>(8.0f) * scale;
metadataDoor["spawnflags"] = metadata["spawnflags"].as<int>(0);
}
// parse parent

View File

@ -1,6 +1,7 @@
#include <uf/utils/audio/audio.h>
#include <uf/utils/string/ext.h>
#include <uf/utils/thread/thread.h>
#include <uf/utils/math/physics.h>
#if UF_USE_OPENAL
#include <uf/ext/openal/openal.h>
@ -16,6 +17,11 @@
bool uf::audio::muted = true;
#endif
namespace {
ext::al::Filter occlusionFilter;
pod::Transform<> listener;
}
bool uf::audio::asyncUpdate = false;
bool uf::audio::streamsByDefault = true;
uint8_t uf::audio::buffers = 4;
@ -36,9 +42,11 @@ void uf::audio::initialize( pod::AudioSource& source ) {
source.alSource.initialize();
source.alSource.set( AL_PITCH, 1.0f );
source.alSource.set( AL_GAIN, 1.0f );
}
//
#if !UF_ENV_DREAMCAST
source.alFilter.initialize();
#endif
}
bool uf::audio::load( pod::AudioClip& clip, const uf::stl::string& filename, bool streamed ) {
uf::audio::destroy( clip );
@ -118,8 +126,11 @@ void uf::audio::destroy( pod::AudioSource& source ) {
else if ( source.clip->extension == "pcm" ) ext::pcm::close( source );
}
source.alSource.destroy();
source.clip = nullptr;
source.alSource.destroy();
#if !UF_ENV_DREAMCAST
source.alFilter.destroy();
#endif
}
void uf::audio::destroy( pod::AudioClip& clip ) {
@ -131,6 +142,7 @@ void uf::audio::destroy( pod::AudioClip& clip ) {
void uf::audio::listener( const pod::Transform<>& transform ) {
if ( uf::audio::muted ) return;
::listener = transform;
auto axes = uf::transform::axes( transform );
axes.forward *= -1;
float o[6] = { axes.forward.x, axes.forward.y, axes.forward.z, axes.up.x, axes.up.y, axes.up.z };
@ -147,11 +159,12 @@ void uf::audio::loop( pod::AudioSource& source, bool state ) {
}
void uf::audio::position( pod::AudioSource& source, const pod::Vector3f& v ) {
source.transform.position = v;
source.alSource.set( AL_POSITION, v[0], v[1], v[2] );
}
void uf::audio::orientation( pod::AudioSource& source, const pod::Quaternion<>& q ) {
// to-do: set
source.transform.orientation = q;
}
float uf::audio::time( const pod::AudioSource& source ) {
@ -191,4 +204,40 @@ float uf::audio::maxDistance( const pod::AudioSource& source ) {
}
void uf::audio::maxDistance( pod::AudioSource& source, float v ) {
source.alSource.set( AL_MAX_DISTANCE, v );
}
//
float uf::audio::occlusion( const pod::Vector3f& position ) {
return uf::physics::occlusion( ::listener.position, position );
}
void uf::audio::occlude( pod::AudioSource& source, float factor ) {
#if UF_ENV_DREAMCAST
uf::audio::gain( source, factor );
#else
if ( factor >= 0.99f ) {
source.alSource.set( AL_DIRECT_FILTER, AL_FILTER_NULL );
} else {
ALuint filterId = source.alFilter.getIndex();
AL_CHECK_RESULT(alFilterf( filterId, AL_LOWPASS_GAINHF, factor ));
AL_CHECK_RESULT(alFilterf( filterId, AL_LOWPASS_GAIN, 0.8f ));
source.alSource.set( AL_DIRECT_FILTER, (ALint)filterId );
}
#endif
}
void uf::audio::acoustics( const pod::Vector3f& position, const pod::Quaternion<>& orientation, float& energy, float& distance, int& bounces ) {
#if !UF_ENV_DREAMCAST
static uf::stl::vector<pod::Vector3f> sphere = uf::math::fibonacciSphere( 32 );
for ( const auto& dir : sphere ) {
auto bounce = uf::physics::acousticReflection( position, dir, ::listener.position, 50.0f );
if ( bounce.valid ) {
energy += bounce.retainedEnergy;
distance += bounce.totalDistance;
bounces++;
}
}
#endif
}

View File

@ -36,16 +36,83 @@ void uf::AudioEmitter::update() {
}
void uf::AudioEmitter::update( const pod::Vector3f& position, const pod::Quaternion<>& orientation ) {
bool spatial = false;
for ( auto& pair : this->m_container ) {
for ( auto& source : pair.second ) {
if ( source.alSource.playing() && source.settings.spatial ) {
spatial = true;
break;
}
}
if ( spatial ) break;
}
float occlusionValue = 1.0f;
if ( spatial ) {
occlusionValue = uf::audio::occlusion( position );
if ( this->m_acousticTimer.elapsed().asDouble() >= 0.1 ) {
this->updateAcoustics( position, orientation );
this->m_acousticTimer.reset();
}
}
for ( auto& pair : this->m_container ) {
for ( auto& source : pair.second ) {
if ( !source.alSource.playing() ) continue;
uf::audio::position( source, position );
uf::audio::orientation( source, orientation );
if ( source.settings.spatial ) {
uf::audio::position( source, position );
uf::audio::orientation( source, orientation );
uf::audio::occlude( source, occlusionValue );
if ( m_efxInitialized ) {
AL_CHECK_RESULT(alSource3i( source.alSource.getIndex(), AL_AUXILIARY_SEND_FILTER, (ALint)m_effectSlot.getIndex(), 0, AL_FILTER_NULL ));
}
} else {
if ( m_efxInitialized ) {
AL_CHECK_RESULT(alSource3i( source.alSource.getIndex(), AL_AUXILIARY_SEND_FILTER, 0, 0, AL_FILTER_NULL ));
}
}
uf::audio::update( source );
}
}
}
void uf::AudioEmitter::updateAcoustics( const pod::Vector3f& position, const pod::Quaternion<>& orientation ) {
if ( !m_efxInitialized ) {
m_effect.initialize();
m_effectSlot.initialize();
m_efxInitialized = true;
}
float energy = 0.0f;
float distance = 0.0f;
int bounces = 0;
uf::audio::acoustics( position, orientation, energy, distance, bounces );
if ( bounces == 0 ) {
m_effect.set( AL_REVERB_GAIN, 0.0f );
m_effectSlot.set( AL_EFFECTSLOT_EFFECT, (ALint)m_effect.getIndex() );
return;
}
float avgEnergy = energy / bounces;
float avgDistance = distance / bounces;
float decayTime = std::max( 0.1f, std::min( (avgDistance / 15.0f) * avgEnergy, 1.5f ) );
float gain = avgEnergy * 0.3f;
float hfRatio = std::max( 0.1f, 1.0f - (avgDistance / 30.0f) );
m_effect.set( AL_REVERB_DECAY_TIME, decayTime );
m_effect.set( AL_REVERB_GAIN, gain );
m_effect.set( AL_REVERB_DECAY_HFRATIO, hfRatio );
m_effect.set( AL_REVERB_DIFFUSION, 1.0f );
m_effectSlot.set( AL_EFFECTSLOT_EFFECT, (ALint)m_effect.getIndex() );
}
void uf::AudioEmitter::cleanup( bool purge ) {
for ( auto mapIt = this->m_container.begin(); mapIt != this->m_container.end(); ) {
auto& pool = mapIt->second;

View File

@ -1,4 +1,5 @@
#include <uf/utils/math/math.h>
#include <uf/utils/math/vector.h>
uint16_t uf::math::quantizeShort( float v ) {
union { float f; uint32_t ui; } u = {v};
@ -13,4 +14,24 @@ uint16_t uf::math::quantizeShort( float v ) {
}
float uf::math::unquantize( uint16_t v ) {
return v;
}
uf::stl::vector<pod::Vector3f> uf::math::fibonacciSphere( int num ) {
uf::stl::vector<pod::Vector3f> points;
points.reserve( num );
const float goldenAngle = M_PI * ( 3.0f - std::sqrt( 5.0f ) );
for ( auto i = 0; i < num; ++i ) {
float y = 1.0f - ( i / float( num - 1 ) ) * 2.0f;
float radius = std::sqrt( 1.0f - y * y );
float theta = goldenAngle * i;
points.emplace_back( pod::Vector3f{
.x = std::cos( theta ) * radius,
.y = y,
.z = std::sin( theta ) * radius,
});
}
return points;
}

View File

@ -1,4 +1,5 @@
#include <uf/utils/math/physics/common.h>
#include <uf/engine/graph/graph.h>
// create ID from pointers
size_t impl::makePairKey( const pod::PhysicsBody& a, const pod::PhysicsBody& b ) {
@ -791,4 +792,33 @@ pod::AABB impl::transformAabbToLocal( const pod::AABB& box, const pod::Transform
out.max = uf::vector::max( out.max, local );
});
return out;
}
float impl::getMaterialTransmittance( const uf::stl::string& materialName ) {
if ( uf::string::contains(materialName, "chainlink") ||
uf::string::contains(materialName, "metalgrate") ) return 0.9f;
// woods
if ( uf::string::contains(materialName, "wood") ) return 0.4f;
// hollow metal
if ( uf::string::contains(materialName, "duct") ) return 0.7f;
// solid dense materials
if ( uf::string::contains(materialName, "concrete") ||
uf::string::contains(materialName, "metal") ||
uf::string::contains(materialName, "dirt") ) return 0.15f;
return 0.2f;
}
uf::stl::string impl::getMaterialName( const pod::PhysicsBody& body, uint32_t triID ) {
if ( triID == (uint32_t)(-1) ) return "";
if ( body.collider.type != pod::ShapeType::MESH ) return "";
auto& scene = uf::scene::getCurrentScene();
auto& graph = scene.getComponent<pod::Graph>();
auto& mesh = *body.collider.mesh.mesh;
auto drawCommand = uf::mesh::fetchDrawCommand( mesh, triID );
auto instance = uf::graph::getInstance( graph, drawCommand.instanceID );
return uf::graph::getMaterialName( graph, instance.materialID );
}

View File

@ -8,6 +8,7 @@
#include <uf/utils/math/physics/constraints.h>
#include <uf/engine/scene/scene.h>
#include <uf/engine/graph/graph.h>
#define UF_PHYSICS_TEST 0
@ -476,7 +477,11 @@ 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 );
body.velocity = v;
if ( body.inverseMass == 0.0f ) {
body.transform->position += v * uf::physics::time::delta;
} else {
body.velocity = v;
}
}
void uf::physics::applyVelocity( pod::PhysicsBody& body, const pod::Vector3f& v ) {
impl::wakeBody( body );
@ -543,6 +548,12 @@ pod::CollisionEvent::events_t uf::physics::getCollisionEvents( const pod::Physic
}
return res;
}
uf::stl::string uf::physics::getCollisionMaterialName( const pod::CollisionEvent& event ) {
auto triID = MIN( event.featureA, event.featureB );
auto* other = event.a->collider.type == pod::ShapeType::MESH ? event.a : event.b;
return impl::getMaterialName( *other, triID );
}
// body creation
pod::PhysicsBody& uf::physics::create( pod::World& world, uf::Object& object, float mass, const pod::Vector3f& positionOffset, const pod::Quaternion<>& orientationOffset ) {
auto& root = object.getComponent<pod::PhysicsBody>();

View File

@ -348,4 +348,69 @@ pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world
if ( uf::physics::settings.debugDraw.rays ) impl::drawRay( ray, rayHit );
return rayHit;
}
float uf::physics::occlusion( const pod::Vector3f& to, const pod::Vector3f& from ) {
pod::Vector3f dir = from - to;
float mag = uf::vector::magnitude( dir );
if ( mag <= EPS ) return 1.0f;
float dist = std::sqrt( mag );
dir /= dist;
pod::Ray ray;
ray.origin = from;
ray.direction = dir;
pod::RayQuery hit = uf::physics::rayCast( ray, uf::physics::getWorld(), dist );
if ( hit.contact.penetration >= dist ) return 1.0f;
auto materialName = uf::physics::getRayMaterialName( hit );
return impl::getMaterialTransmittance( materialName );
}
uf::stl::string uf::physics::getRayMaterialName( const pod::RayQuery& query ) {
return impl::getMaterialName( *query.body, query.contact.featureA );
}
pod::AcousticBounce uf::physics::acousticReflection( const pod::Vector3f& sourcePos, const pod::Vector3f& rayDirection, const pod::Vector3f& listenerPos, float maxDistance ) {
pod::AcousticBounce result;
pod::Ray primaryRay;
primaryRay.origin = sourcePos;
primaryRay.direction = rayDirection;
pod::RayQuery firstHit = uf::physics::rayCast( primaryRay, uf::physics::getWorld(), maxDistance );
if ( firstHit.contact.penetration >= maxDistance ) return result;
auto materialName = uf::physics::getRayMaterialName( firstHit );
float transmittance = impl::getMaterialTransmittance( materialName );
float reflectance = 1.0f - transmittance;
pod::Vector3f N = firstHit.contact.normal;
pod::Vector3f D = rayDirection;
float dotProduct = uf::vector::dot( D, N );
pod::Vector3f reflectDir = D - (N * (2.0f * dotProduct));
uf::vector::normalize( reflectDir );
pod::Vector3f bounceOrigin = firstHit.contact.point + (N * EPS);
pod::Vector3f toListener = listenerPos - bounceOrigin;
float distToListener = uf::vector::magnitude( toListener );
pod::Ray secondaryRay;
secondaryRay.origin = bounceOrigin;
secondaryRay.direction = toListener / distToListener;
pod::RayQuery secondHit = uf::physics::rayCast( secondaryRay, uf::physics::getWorld(), distToListener );
if ( secondHit.contact.penetration >= distToListener ) {
result.valid = true;
result.totalDistance = firstHit.contact.penetration + distToListener;
result.retainedEnergy = reflectance;
result.hitPoint = firstHit.contact.point;
}
return result;
}