Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions python/audio2/occlusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def ClearOccluderSpheres(self):
"""Remove every occluder sphere and fade all emitters back to clear."""
self.occlusion.ClearOccluderSpheres()

def ClearSightlineSource(self):
"""Return sightlines to starting at the listener."""
self.occlusion.ClearSightlineSource()

def GetBlockedOcclusion(self):
"""Return the occlusion applied to an emitter whose line of sight an occluder sphere blocks."""
return self.occlusion.blockedOcclusion
Expand Down Expand Up @@ -83,6 +87,10 @@ def GetRadiusScale(self):
"""Return what fraction of an occluder's radius counts as solid."""
return self.occlusion.radiusScale

def HasSightlineSource(self):
"""Return whether a sightline source override is currently set."""
return self.occlusion.HasSightlineSource()

def RemoveOccluderSphere(self, occluderID):
"""Remove a single occluder sphere. Emitters it was blocking fade back to clear.

Expand Down Expand Up @@ -165,3 +173,17 @@ def SetRadiusScale(self, scale):
:type scale: float
"""
self.occlusion.radiusScale = scale

def SetSightlineSource(self, x, y, z):
"""Trace sightlines from this game world point instead of from the listener.

For testing where occlusion should be judged from - the camera or the player's
ship. Feed the ship's world position every tick while the override is wanted;
ClearSightlineSource() flips back to the listener, so switching perspective
mid-flight is one call either way.

:param x, y, z: The point sightlines start from, in game world space (e.g. the
ship's raw destiny ball position).
:type x, y, z: float
"""
self.occlusion.SetSightlineSource(x, y, z)
107 changes: 81 additions & 26 deletions src/AudObstructionOcclusion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ namespace
/**
* @brief A translated occluder ready for a single line-of-sight pass.
*
* Carries how far its near surface sits from the listener. That distance is what
* lets an emitter stop scanning once the spheres are farther away than it is.
* Carries how far its near surface sits from the sightline start. That distance is
* what lets an emitter stop scanning once the spheres are farther away than it is.
*/
struct SortableOccluder
{
Expand All @@ -34,6 +34,10 @@ AudObstructionOcclusion::AudObstructionOcclusion(AudManager* audioManager) :
m_originX(0.0),
m_originY(0.0),
m_originZ(0.0),
m_sightlineSourceX(0.0),
m_sightlineSourceY(0.0),
m_sightlineSourceZ(0.0),
m_hasSightlineSource(false),
m_fadeRate(DEFAULT_FADE_RATE),
m_blockedOcclusion(DEFAULT_BLOCKED_OCCLUSION),
m_occluderRadiusScale(DEFAULT_OCCLUDER_RADIUS_SCALE),
Expand Down Expand Up @@ -219,6 +223,39 @@ void AudObstructionOcclusion::SetOccluderOrigin(double x, double y, double z)
// It is read when line of sight is next computed, which is soon enough.
}

void AudObstructionOcclusion::SetSightlineSource(double x, double y, double z)
{
CcpAutoMutex lock(m_mutex);
// Switching perspective should be audible immediately, but once the override is
// in place it moves with the ship every tick, and reacting to that would defeat
// the throttle just as reacting to the origin would (see SetOccluderOrigin).
if (!m_hasSightlineSource)
{
m_hasComputedLos = false;
}
m_hasSightlineSource = true;
m_sightlineSourceX = x;
m_sightlineSourceY = y;
m_sightlineSourceZ = z;
}

void AudObstructionOcclusion::ClearSightlineSource()
{
CcpAutoMutex lock(m_mutex);
if (!m_hasSightlineSource)
{
return;
}
m_hasSightlineSource = false;
m_hasComputedLos = false;
}

bool AudObstructionOcclusion::HasSightlineSource() const
{
CcpAutoMutex lock(m_mutex);
return m_hasSightlineSource;
}

void AudObstructionOcclusion::RemoveOccluderSphere(uint64_t occluderID)
{
CcpAutoMutex lock(m_mutex);
Expand Down Expand Up @@ -312,13 +349,11 @@ uint64_t AudObstructionOcclusion::GetBlockingOccluder(AkGameObjectID emitterID)
return 0;
}

Vector3 listenerPosition;
bool listenerIsPlaced = false;
const bool haveListener = m_audioManager->WithCallbackGameObject(LISTENER_GAME_OBJ_ID,
[&listenerPosition, &listenerIsPlaced](AudGameObjResource* listener) {
listenerPosition = listener->GetPosition();
listenerIsPlaced = listener->HasUsableWorldPosition();
});
Vector3 sightlineStart;
if (!GetSightlineStartLocked(sightlineStart))
{
return 0;
}

Vector3 emitterPosition;
bool emitterIsPlaced = false;
Expand All @@ -328,15 +363,15 @@ uint64_t AudObstructionOcclusion::GetBlockingOccluder(AkGameObjectID emitterID)
emitterIsPlaced = emitter->HasUsableWorldPosition();
});

if (!haveListener || !listenerIsPlaced || !haveEmitter || !emitterIsPlaced)
if (!haveEmitter || !emitterIsPlaced)
{
return 0;
}

for (const auto& occluderEntry : m_occluders)
{
const TranslatedOccluder translated = TranslateToAudioSpace(occluderEntry.second);
if (SegmentHitsSphere(listenerPosition, emitterPosition, translated.center, translated.radius))
if (SegmentHitsSphere(sightlineStart, emitterPosition, translated.center, translated.radius))
{
return occluderEntry.first;
}
Expand Down Expand Up @@ -379,16 +414,15 @@ AudObstructionOcclusion::TranslatedOccluder AudObstructionOcclusion::TranslateTo
sphere.radius * m_occluderRadiusScale };
}

void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::time_point now)
bool AudObstructionOcclusion::GetSightlineStartLocked(Vector3& start) const
{
if (m_hasComputedLos &&
std::chrono::duration<float>(now - m_lastLosComputeTime).count() < m_losRecomputeInterval)
if (m_hasSightlineSource)
{
return;
start = Vector3(static_cast<float>(m_sightlineSourceX - m_originX),
static_cast<float>(m_sightlineSourceY - m_originY),
static_cast<float>(m_sightlineSourceZ - m_originZ));
return true;
}
m_lastLosComputeTime = now;
m_hasComputedLos = true;
m_blockingOccluderIDs.clear();

Vector3 listenerPosition;
bool listenerIsPlaced = false;
Expand All @@ -399,6 +433,27 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::
});
// Until a camera drives the listener it sits at the initial spawn position
if (!haveListener || !listenerIsPlaced)
{
return false;
}

start = listenerPosition;
return true;
}

void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::time_point now)
{
if (m_hasComputedLos &&
std::chrono::duration<float>(now - m_lastLosComputeTime).count() < m_losRecomputeInterval)
{
return;
}
m_lastLosComputeTime = now;
m_hasComputedLos = true;
m_blockingOccluderIDs.clear();

Vector3 sightlineStart;
if (!GetSightlineStartLocked(sightlineStart))
{
return;
}
Expand Down Expand Up @@ -431,9 +486,9 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::
sortable.center = translated.center;
sortable.radius = translated.radius;

const Vector3 fromListener = translated.center - listenerPosition;
const Vector3 fromStart = translated.center - sightlineStart;
sortable.nearSurfaceDistance =
std::sqrt(Dot(fromListener, fromListener)) - translated.radius;
std::sqrt(Dot(fromStart, fromStart)) - translated.radius;

occluders.push_back(sortable);
}
Expand All @@ -449,7 +504,7 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::

for (const auto& [emitterID, position] : emitters)
{
const Vector3 toEmitter = position - listenerPosition;
const Vector3 toEmitter = position - sightlineStart;
const float segmentLength = std::sqrt(Dot(toEmitter, toEmitter));

bool blocked = false;
Expand All @@ -463,7 +518,7 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::
break;
}

if (SegmentHitsSphere(listenerPosition, position, occluder.center, occluder.radius))
if (SegmentHitsSphere(sightlineStart, position, occluder.center, occluder.radius))
{
blocked = true;
m_blockingOccluderIDs.insert(occluder.occluderID);
Expand Down Expand Up @@ -499,9 +554,9 @@ bool AudObstructionOcclusion::SegmentHitsSphere(const Vector3& segmentStart, con
const Vector3 toCenter = sphereCenter - segmentStart;
const float radiusSq = sphereRadius * sphereRadius;

// A sphere the listener is inside is not between the listener and anything. Occluder
// radii are coarse bounding spheres and the player routinely flies inside a large
// one, so counting that as blockage would muffle the entire world at once.
// A sphere the segment's start is inside is not between that point and anything.
// Occluder radii are coarse bounding spheres and the player routinely flies inside
// a large one, so counting that as blockage would muffle the entire world at once.
if (Dot(toCenter, toCenter) <= radiusSq)
{
return false;
Expand All @@ -512,7 +567,7 @@ bool AudObstructionOcclusion::SegmentHitsSphere(const Vector3& segmentStart, con
// division is needed to compare it against the ends.
const float centerProjection = Dot(toCenter, segment);

// An emitter inside a sphere needs more care than the listener did, because the two
// An emitter inside a sphere needs more care than the start did, because the two
// situations that produce it want opposite answers. A turret or beam emitter mounted
// on an object sits inside that object's own bounding sphere while being in plain
// sight, and muffling it would be permanent. A sound on the far side of the object,
Expand Down
34 changes: 32 additions & 2 deletions src/AudObstructionOcclusion.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ class AudObstructionOcclusion
*/
void SetOccluderOrigin(double x, double y, double z);

/**
* @brief Traces sightlines from this game world point instead of from the listener.
*
* For testing where occlusion should be judged from - the camera or the player's
* ship. The game feeds the ship's world position every tick while the override is
* wanted; @c ClearSightlineSource() flips back to the listener, so switching
* perspective mid-flight is one call either way. Like occluder centres, the point
* is world-space doubles translated against the origin each time line of sight is
* computed.
*/
void SetSightlineSource(double x, double y, double z);

/**
* @brief Returns sightlines to starting at the listener.
*/
void ClearSightlineSource();

/// Whether a sightline source override is currently set.
bool HasSightlineSource() const;

/**
* @brief Removes a single occluder sphere. Emitters it was blocking fade back to clear.
*/
Expand Down Expand Up @@ -207,6 +227,11 @@ class AudObstructionOcclusion
/// current origin. Callers must hold m_mutex.
TranslatedOccluder TranslateToAudioSpace(const OccluderSphere& sphere) const;

/// Where sightlines start: the translated source override if one is set, otherwise
/// the listener's position. Returns false when there is no usable start (no
/// override and the listener is missing or unplaced). Callers must hold m_mutex.
bool GetSightlineStartLocked(Vector3& start) const;

bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const;

/// Fade every tracked emitter back to clear. Callers must hold m_mutex.
Expand All @@ -224,8 +249,8 @@ class AudObstructionOcclusion
/// All three positions are in audio space.
///
/// Not a pure intersection test at the endpoints, and deliberately asymmetric: a
/// sphere containing the listener never blocks, while one containing the emitter
/// blocks only if the emitter is past the deepest point of the crossing.
/// sphere containing the segment's start never blocks, while one containing the
/// emitter blocks only if the emitter is past the deepest point of the crossing.
static bool SegmentHitsSphere(const Vector3& segmentStart, const Vector3& segmentEnd,
const Vector3& sphereCenter, float sphereRadius);

Expand All @@ -244,6 +269,11 @@ class AudObstructionOcclusion
double m_originX;
double m_originY;
double m_originZ;
// An optional game world point sightlines start from instead of the listener.
double m_sightlineSourceX;
double m_sightlineSourceY;
double m_sightlineSourceZ;
bool m_hasSightlineSource;
float m_fadeRate;
float m_blockedOcclusion;
float m_occluderRadiusScale;
Expand Down
22 changes: 22 additions & 0 deletions src/AudOcclusion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ void AudOcclusion::SetOrigin( double x, double y, double z )
}
}

void AudOcclusion::SetSightlineSource( double x, double y, double z )
{
if( AudObstructionOcclusion* occlusion = Occlusion() )
{
occlusion->SetSightlineSource( x, y, z );
}
}

void AudOcclusion::ClearSightlineSource()
{
if( AudObstructionOcclusion* occlusion = Occlusion() )
{
occlusion->ClearSightlineSource();
}
}

bool AudOcclusion::HasSightlineSource() const
{
AudObstructionOcclusion* occlusion = Occlusion();
return occlusion != nullptr ? occlusion->HasSightlineSource() : false;
}

void AudOcclusion::RemoveOccluderSphere( uint64_t occluderID )
{
if( AudObstructionOcclusion* occlusion = Occlusion() )
Expand Down
18 changes: 18 additions & 0 deletions src/AudOcclusion.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ BLUE_CLASS( AudOcclusion ) : public IRoot
*/
void SetOrigin( double x, double y, double z );

/**
* @brief Traces sightlines from this game world point instead of from the listener.
*
* For testing where occlusion should be judged from - the camera or the player's
* ship. Feed the ship's world position every tick while the override is wanted;
* @c ClearSightlineSource() flips back to the listener, so switching perspective
* mid-flight is one call either way.
*/
void SetSightlineSource( double x, double y, double z );

/**
* @brief Returns sightlines to starting at the listener.
*/
void ClearSightlineSource();

/// Whether a sightline source override is currently set.
bool HasSightlineSource() const;

/**
* @brief Removes a single occluder sphere. Emitters it was blocking fade back to clear.
*/
Expand Down
23 changes: 23 additions & 0 deletions src/AudOcclusion_Blue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ const Be::ClassInfo* AudOcclusion::ExposeToBlue()
" (in EVE, the ego ball's position)."
)
MAP_METHOD_AND_WRAP
(
"SetSightlineSource",
SetSightlineSource,
"Trace sightlines from this game world point instead of from the listener. For testing\n"
"where occlusion should be judged from - the camera or the player's ship. Feed the ship's\n"
"world position every tick while the override is wanted; ClearSightlineSource() flips back\n"
"to the listener.\n"
":param x, y, z: The point sightlines start from, in game world space (e.g. the\n"
" ship's raw destiny ball position)."
)
MAP_METHOD_AND_WRAP
(
"ClearSightlineSource",
ClearSightlineSource,
"Return sightlines to starting at the listener."
)
MAP_METHOD_AND_WRAP
(
"HasSightlineSource",
HasSightlineSource,
"Whether a sightline source override is currently set."
)
MAP_METHOD_AND_WRAP
(
"RemoveOccluderSphere",
RemoveOccluderSphere,
Expand Down
Loading