diff --git a/bin/data/config.json b/bin/data/config.json index b2d30592..a3aa95f5 100644 --- a/bin/data/config.json +++ b/bin/data/config.json @@ -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 diff --git a/bin/data/entities/scripts/door.lua b/bin/data/entities/scripts/door.lua index 10ecc404..56837867 100644 --- a/bin/data/entities/scripts/door.lua +++ b/bin/data/entities/scripts/door.lua @@ -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 ) diff --git a/bin/data/entities/scripts/trigger.lua b/bin/data/entities/scripts/trigger.lua index e16337e2..a9453967 100644 --- a/bin/data/entities/scripts/trigger.lua +++ b/bin/data/entities/scripts/trigger.lua @@ -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 ) -]] \ No newline at end of file + + 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 ) \ No newline at end of file diff --git a/bin/data/scenes/sourceengine/base_sourceengine.json b/bin/data/scenes/sourceengine/base_sourceengine.json index 458ed124..91d65f11 100644 --- a/bin/data/scenes/sourceengine/base_sourceengine.json +++ b/bin/data/scenes/sourceengine/base_sourceengine.json @@ -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" } }, diff --git a/bin/data/scenes/sourceengine/mds_mcdonalds.json b/bin/data/scenes/sourceengine/mds_mcdonalds.json index c5759688..14e178a3 100644 --- a/bin/data/scenes/sourceengine/mds_mcdonalds.json +++ b/bin/data/scenes/sourceengine/mds_mcdonalds.json @@ -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" } ] } \ No newline at end of file diff --git a/bin/data/scenes/sourceengine/sourceengine.json b/bin/data/scenes/sourceengine/sourceengine.json index bdb37541..065d566b 100644 --- a/bin/data/scenes/sourceengine/sourceengine.json +++ b/bin/data/scenes/sourceengine/sourceengine.json @@ -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" } \ No newline at end of file diff --git a/engine/inc/uf/ext/openal/filter.h b/engine/inc/uf/ext/openal/filter.h new file mode 100644 index 00000000..1906403f --- /dev/null +++ b/engine/inc/uf/ext/openal/filter.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#if UF_USE_OPENAL + +#include +#include +#include +#define AL_ALEXT_PROTOTYPES 1 +#include +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 \ No newline at end of file diff --git a/engine/inc/uf/ext/openal/openal.h b/engine/inc/uf/ext/openal/openal.h index ca6284c4..2b378dd7 100644 --- a/engine/inc/uf/ext/openal/openal.h +++ b/engine/inc/uf/ext/openal/openal.h @@ -27,6 +27,7 @@ #include "source.h" #include "buffer.h" +#include "filter.h" #include #include diff --git a/engine/inc/uf/utils/audio/audio.h b/engine/inc/uf/utils/audio/audio.h index 7d87e950..bf214e77 100644 --- a/engine/inc/uf/utils/audio/audio.h +++ b/engine/inc/uf/utils/audio/audio.h @@ -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& ); } } diff --git a/engine/inc/uf/utils/audio/emitter.h b/engine/inc/uf/utils/audio/emitter.h index c91458eb..1d7ea5b9 100644 --- a/engine/inc/uf/utils/audio/emitter.h +++ b/engine/inc/uf/utils/audio/emitter.h @@ -6,6 +6,11 @@ namespace uf { typedef uf::stl::unordered_map> 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 ); }; diff --git a/engine/inc/uf/utils/audio/metadata.h b/engine/inc/uf/utils/audio/metadata.h index 8a69ac25..95948055 100644 --- a/engine/inc/uf/utils/audio/metadata.h +++ b/engine/inc/uf/utils/audio/metadata.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -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; diff --git a/engine/inc/uf/utils/math/physics/common.h b/engine/inc/uf/utils/math/physics/common.h index b3382835..c61ed502 100644 --- a/engine/inc/uf/utils/math/physics/common.h +++ b/engine/inc/uf/utils/math/physics/common.h @@ -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 ); diff --git a/engine/inc/uf/utils/math/physics/impl.h b/engine/inc/uf/utils/math/physics/impl.h index a8b5baa4..5d04b7ba 100644 --- a/engine/inc/uf/utils/math/physics/impl.h +++ b/engine/inc/uf/utils/math/physics/impl.h @@ -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& ); diff --git a/engine/inc/uf/utils/math/physics/narrowphase/ray.h b/engine/inc/uf/utils/math/physics/narrowphase/ray.h index 923f755a..98a8e46f 100644 --- a/engine/inc/uf/utils/math/physics/narrowphase/ray.h +++ b/engine/inc/uf/utils/math/physics/narrowphase/ray.h @@ -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 ); } } \ No newline at end of file diff --git a/engine/inc/uf/utils/math/physics/structs.h b/engine/inc/uf/utils/math/physics/structs.h index 872958dc..6f4aad63 100644 --- a/engine/inc/uf/utils/math/physics/structs.h +++ b/engine/inc/uf/utils/math/physics/structs.h @@ -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 diff --git a/engine/inc/uf/utils/math/vector.h b/engine/inc/uf/utils/math/vector.h index 0e59737b..54c23d20 100644 --- a/engine/inc/uf/utils/math/vector.h +++ b/engine/inc/uf/utils/math/vector.h @@ -302,4 +302,11 @@ namespace std { }; } +// maths +namespace uf { + namespace math { + uf::stl::vector UF_API fibonacciSphere( int points ); + } +} + #include "vector/vector.inl" \ No newline at end of file diff --git a/engine/src/engine/ext/player/behavior.cpp b/engine/src/engine/ext/player/behavior.cpp index b9f2568c..f1c0c663 100644 --- a/engine/src/engine/ext/player/behavior.cpp +++ b/engine/src/engine/ext/player/behavior.cpp @@ -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 surfaces = { + static uf::stl::vector 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; diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index acee143b..ca301164 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -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; diff --git a/engine/src/ext/openal/openal.cpp b/engine/src/ext/openal/openal.cpp index c37ebbc6..37d99c62 100644 --- a/engine/src/ext/openal/openal.cpp +++ b/engine/src/ext/openal/openal.cpp @@ -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 \ No newline at end of file diff --git a/engine/src/ext/valve/bsp.cpp b/engine/src/ext/valve/bsp.cpp index 5a7e9029..17e82def 100644 --- a/engine/src/ext/valve/bsp.cpp +++ b/engine/src/ext/valve/bsp.cpp @@ -595,11 +595,7 @@ namespace impl { if ( classname.starts_with("func_door") ) { auto& metadataDoor = metadata["door"]; - metadataDoor["speed"] = metadata["speed"].as(100.0f); - metadataDoor["wait"] = metadata["wait"].as(4.0f); - metadataDoor["lip"] = metadata["lip"].as(8.0f); - metadataDoor["spawnflags"] = metadata["spawnflags"].as(0); - + float scale = impl::sourceToMeters; if ( classname == "func_door" ) { auto movedirStr = metadata["movedir"].as(""); if ( movedirStr == "" && metadata["angle"].as() ) { @@ -610,14 +606,20 @@ namespace impl { } if ( movedirStr != "" ) { - pod::Transform<> t; - auto pyr = impl::str2vec( movedirStr ) * -DEG_2_RAD; - t.orientation = uf::quaternion::euler( pyr ); - auto axes = uf::transform::axes( t ); + auto pyr = impl::str2vec( 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(90.0f); int flags = metadataDoor["spawnflags"].as(); @@ -627,6 +629,11 @@ namespace impl { metadataDoor["axis"] = uf::vector::encode( axis ); } + + metadataDoor["speed"] = metadata["speed"].as(100.0f) * scale; + metadataDoor["wait"] = metadata["wait"].as(4.0f); + metadataDoor["lip"] = metadata["lip"].as(8.0f) * scale; + metadataDoor["spawnflags"] = metadata["spawnflags"].as(0); } // parse parent diff --git a/engine/src/utils/audio/audio.cpp b/engine/src/utils/audio/audio.cpp index b7c88c75..ddf55c37 100644 --- a/engine/src/utils/audio/audio.cpp +++ b/engine/src/utils/audio/audio.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #if UF_USE_OPENAL #include @@ -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 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 } \ No newline at end of file diff --git a/engine/src/utils/audio/emitter.cpp b/engine/src/utils/audio/emitter.cpp index ce213458..670a5932 100644 --- a/engine/src/utils/audio/emitter.cpp +++ b/engine/src/utils/audio/emitter.cpp @@ -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; diff --git a/engine/src/utils/math/math.cpp b/engine/src/utils/math/math.cpp index 688fff6b..e5ba0650 100644 --- a/engine/src/utils/math/math.cpp +++ b/engine/src/utils/math/math.cpp @@ -1,4 +1,5 @@ #include +#include 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 uf::math::fibonacciSphere( int num ) { + uf::stl::vector 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; } \ No newline at end of file diff --git a/engine/src/utils/math/physics/common.cpp b/engine/src/utils/math/physics/common.cpp index a8194cc4..c732ef73 100644 --- a/engine/src/utils/math/physics/common.cpp +++ b/engine/src/utils/math/physics/common.cpp @@ -1,4 +1,5 @@ #include +#include // 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(); + + 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 ); } \ No newline at end of file diff --git a/engine/src/utils/math/physics/impl.cpp b/engine/src/utils/math/physics/impl.cpp index c1c88c09..acf5db50 100644 --- a/engine/src/utils/math/physics/impl.cpp +++ b/engine/src/utils/math/physics/impl.cpp @@ -8,6 +8,7 @@ #include #include +#include #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(); diff --git a/engine/src/utils/math/physics/narrowphase/ray.cpp b/engine/src/utils/math/physics/narrowphase/ray.cpp index 8a14b2aa..7911a070 100644 --- a/engine/src/utils/math/physics/narrowphase/ray.cpp +++ b/engine/src/utils/math/physics/narrowphase/ray.cpp @@ -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; } \ No newline at end of file