Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
23 changes: 22 additions & 1 deletion rts/Game/Camera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "Camera.h"
#include "CameraHandler.h"
#include "Game/TraceRay.h"
#include "UI/MouseHandler.h"
#include "Map/Ground.h"
#include "Map/ReadMap.h"
Expand Down Expand Up @@ -156,6 +157,8 @@ void CCamera::Update(const UpdateParams& p)
if (p.updateFrustum)
UpdateFrustum();

TraceToTerrain();

LoadMatrices();
// not done here
// LoadViewPort();
Expand Down Expand Up @@ -708,6 +711,24 @@ void CCamera::ClipFrustumLines(const float zmin, const float zmax, bool neg)
}
}

void CCamera::TraceToTerrain() {
if (camType != CAMTYPE_PLAYER)
return;

const CUnit* hitUnit = nullptr;
const CFeature* hitFeature = nullptr;

terrainDistance = TraceRay::GuiTraceRay(
GetPos(),
GetForward(),
30000,
nullptr,
hitUnit,
hitFeature,
true,
true
);
}
Comment thread
Majavaa marked this conversation as resolved.

float3 CCamera::GetMoveVectorFromState(bool fromKeyState) const
{
Expand All @@ -716,7 +737,7 @@ float3 CCamera::GetMoveVectorFromState(bool fromKeyState) const

if (useInterpolate > 0)
camDeltaTime = 1000.0f / std::fmax(globalRendering->FPS, 1.0f);

float camMoveSpeed = 1.0f;

camMoveSpeed *= movState[MOVE_STATE_SLW] ? moveSlowMult : 1.0f;
Expand Down
4 changes: 4 additions & 0 deletions rts/Game/Camera.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class CCamera {
float GetNearPlaneDist() const { return frustum.scales.z; }
float GetFarPlaneDist() const { return frustum.scales.w; }
float GetAspectRatio() const { return aspectRatio; }
float GetTerrainDistance() const { return terrainDistance; }

float3 GetMoveVectorFromState(bool fromKeyState) const;

Expand Down Expand Up @@ -253,6 +254,7 @@ class CCamera {

void UpdateDirsFromRot(const float3& r);

void TraceToTerrain();
public:
float3 pos;
float3 rot; ///< x = inclination, y = azimuth (to the -z axis!), z = roll
Expand Down Expand Up @@ -309,6 +311,8 @@ class CCamera {

uint8_t inViewPlanesMask;

float terrainDistance = 1000;

bool movState[10]; // fwd, back, left, right, up, down, fast, slow, tilt, reset
bool rotState[4]; // unused
};
Expand Down
69 changes: 69 additions & 0 deletions rts/System/Sound/AttenuationModels/RtsAttenuationModel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */

#include "RtsAttenuationModel.h"
#include "Game/Camera.h"
#include "Game/CameraHandler.h"
#include "System/Sound/ISoundAttenuationModel.h"
#include "Game/TraceRay.h"
#include <algorithm>
#include <cmath>

SoundAttenuationOutput RtsAttenuationModel::Evaluate(const SoundAttenuationInput& in) const {

SoundAttenuationOutput out;

// -----------------------------------------------------------------
// Convert the soundPosition in to camera space and calculate necessary data
// -----------------------------------------------------------------

{
CCamera* playerCamera = CCameraHandler::GetCamera(CCamera::CAMTYPE_PLAYER);

float terrainDistance = playerCamera->GetTerrainDistance();

out.zoomFactor = 1 - std::clamp(terrainDistance == -1 ? 1.0f : terrainDistance / GetForwardAttenuationRange(), 0.0f, 1.0f);
out.tiltFactor = playerCamera->GetForward().dot(float3(0.0f, -1.0f, 0.0f));

float3 toSound = in.soundPosition - playerCamera->GetPos();

float camForward = playerCamera->GetForward().dot(toSound);
float camRight = playerCamera->GetRight().dot(toSound);
float camUp = playerCamera->GetUp().dot(toSound);

out.forwardDistance = camForward;

float hfov = playerCamera->GetHFOV() * math::DEG_TO_RAD;
float vfov = playerCamera->GetVFOV() * math::DEG_TO_RAD;

float distance = std::abs(camForward);
if (distance <= 0.0f) distance = 1.0f;

out.frustumWidth = distance * std::tan(hfov * 0.5f);
out.frustumHeight = distance * std::tan(vfov * 0.5f);

float outsideRight = std::max(0.0f, std::abs(camRight) - out.frustumWidth);
float outsideUp = std::max(0.0f, std::abs(camUp) - out.frustumHeight);

out.outerDistance = std::sqrt(outsideRight * outsideRight + outsideUp * outsideUp);
}

// -----------------------------------------------------------------
// Convert all the positional data to normalized ranges and calculate the final ranges
// -----------------------------------------------------------------

{
// Calculate forward attenuation
float forwardValue = std::clamp(out.forwardDistance >= 0 ?
std::lerp(GetMinVolumeAttenuation(), 1.0f, 1.0f - out.forwardDistance / GetForwardAttenuationRange()) :
std::clamp(1.0f - (-out.forwardDistance) / GetBackwardAttenuationRange(), 0.0f, 1.0f), GetMinVolumeAttenuation(), 1.0f);

// Calculate outer attenuation
float outerValue = std::clamp(std::lerp(GetMinFilterAttenuation(), 1.0f, 1.0f - out.outerDistance / GetOuterAttenuationRange()), GetMinFilterAttenuation(), 1.0f);

out.volumeFactor = std::pow(forwardValue, 6) * outerValue;
out.filterFactor = std::pow(forwardValue, 1) * outerValue;
}

return out;
}

22 changes: 22 additions & 0 deletions rts/System/Sound/AttenuationModels/RtsAttenuationModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */

#pragma once

#include "System/Config/ConfigHandler.h"
#include "System/Sound/ISoundAttenuationModel.h"

class RtsAttenuationModel : public ISoundAttenuationModel
{
public:
SoundAttenuationOutput Evaluate(const SoundAttenuationInput& in) const override;

private:
float GetForwardAttenuationRange() const { return configHandler->GetFloat("snd_forwardAttenuationRange"); }
float GetBackwardAttenuationRange() const { return configHandler->GetFloat("snd_backwardAttenuationRange"); }
float GetOuterAttenuationRange() const { return configHandler->GetFloat("snd_outerAttenuationRange"); }
float GetMinVolumeAttenuation() const { return configHandler->GetFloat("snd_minVolumeAttenuation"); }
float GetMinFilterAttenuation() const { return configHandler->GetFloat("snd_minFilterAttenuation"); }
};

// "RTS Model: Designed for RTS games with viewport-based attenuation, forward/backward distance handling, off-screen attenuation, and zoom-aware center attenuation. Seen in games like \"Planetary Annihilation\" or \"Supreme Commander 2\""

2 changes: 2 additions & 0 deletions rts/System/Sound/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set(noSoundSources
ISound.cpp
Null/SoundChannels.cpp
Null/NullSound.cpp
AttenuationModels/RtsAttenuationModel.cpp
)

add_library(no-sound STATIC EXCLUDE_FROM_ALL ${noSoundSources})
Expand Down Expand Up @@ -42,6 +43,7 @@ if (NOT NO_SOUND)
OpenAL/SoundItem.cpp
OpenAL/SoundSource.cpp
OpenAL/VorbisShared.cpp
AttenuationModels/RtsAttenuationModel.cpp
)

find_package_static(OpenAL 1.18.2 REQUIRED)
Expand Down
6 changes: 6 additions & 0 deletions rts/System/Sound/ISound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ CONFIG(float, snd_airAbsorption).defaultValue(0.1f);

CONFIG(std::string, snd_device).defaultValue("").description("Sets the used output device. See \"Available Devices\" section in infolog.txt.");

CONFIG(bool, snd_useAttenuationModel).defaultValue(false).description("Use the new AttenuationModel system. Affects how you hear 3D sounds in the game. Recommended: ON");
Comment thread
Majavaa marked this conversation as resolved.
CONFIG(float, snd_forwardAttenuationRange).defaultValue(8000).description("Distance in front of the camera over which sound volume and clarity gradually fade.");
CONFIG(float, snd_backwardAttenuationRange).defaultValue(300).description("Distance behind the camera over which sounds are attenuated.");
CONFIG(float, snd_outerAttenuationRange).defaultValue(500).description("How far sounds can be outside the camera view before being strongly attenuated.");
CONFIG(float, snd_minVolumeAttenuation).defaultValue(0.0f).minimumValue(0).maximumValue(1).description("Minimum volume multiplier for attenuated sounds to prevent them from becoming silent. Smaller is quieter");
CONFIG(float, snd_minFilterAttenuation).defaultValue(0.05f).minimumValue(0).maximumValue(1).description("Minimum clarity level for attenuated sounds, controlling how muffled distant sounds become. Smaller numbers meaning more filtered");


#ifndef NO_SOUND
Expand Down
4 changes: 4 additions & 0 deletions rts/System/Sound/ISound.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#ifndef _I_SOUND_H_
#define _I_SOUND_H_

#include "System/Sound/ISoundAttenuationModel.h"
#include <cstdint>
#include <string>
#include <vector>
Expand Down Expand Up @@ -79,6 +80,9 @@ class ISound {
private:
virtual bool LoadSoundDefsImpl(LuaParser* defsParser) = 0;
static bool IsNullAudio();

public:
virtual ISoundAttenuationModel* GetAttenuationModel() { return nullptr; }
};

#define sound ISound::GetInstance()
Expand Down
29 changes: 29 additions & 0 deletions rts/System/Sound/ISoundAttenuationModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include "System/float3.h"
struct SoundAttenuationInput {
float3 soundPosition;
//maybe some sound specific settings like should it be resistant to attenuation
};

/// All of the data used to calculate the totalFactor as well as the final totalFactor
struct SoundAttenuationOutput {
float forwardDistance;
float outerDistance;
float frustumHeight;
float frustumWidth;
float volumeFactor;
float filterFactor;
float tiltFactor;
float zoomFactor;
};

class ISoundAttenuationModel {
public:
virtual ~ISoundAttenuationModel() = default;

virtual SoundAttenuationOutput Evaluate(
const SoundAttenuationInput& in
) const = 0;
};

32 changes: 29 additions & 3 deletions rts/System/Sound/OpenAL/Sound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

#include "System/float3.h"

#include "Rendering/GL/glExtra.h"


spring::recursive_mutex soundMutex;

Expand All @@ -58,7 +60,6 @@ CSound::~CSound()
configHandler->RemoveObserver(this);
}


void CSound::Init()
{
std::lock_guard<spring::recursive_mutex> lck(soundMutex);
Expand All @@ -79,6 +80,8 @@ void CSound::Init()
updateListener = false;
soundThreadQuit = false;
canLoadDefs = false;

attenuationModel = new RtsAttenuationModel();
Comment thread
Majavaa marked this conversation as resolved.
}
{
Channels::General->SetVolume(configHandler->GetInt("snd_volgeneral") * 0.01f);
Expand Down Expand Up @@ -121,6 +124,9 @@ void CSound::Kill()
soundThread.join();
}

delete attenuationModel;
attenuationModel = nullptr;

SoundBuffer::Deinitialise();
}

Expand Down Expand Up @@ -358,7 +364,7 @@ void CSound::DeviceChanged(uint32_t sdlDeviceIndex)
// In these cases, no event is emitted — SDL2 switches the active audio device internally through the OS-specific audio backend (WASAPI, PulseAudio, etc.).
// However, with certain device changes a short dropout may occur, and SDL2 will emit the SDL_AUDIODEVICEREMOVED event.
// Shortly afterwards, the default device can usually be reinitialized.

// This behavior can be reproduced on several test systems, for example when switching the Windows default device from monitor audio over HDMI to a sound card (HDMI->analog)

std::lock_guard<spring::recursive_mutex> lck(soundMutex);
Expand Down Expand Up @@ -513,7 +519,7 @@ bool CSound::OpenSdlDevice(const std::string& deviceName, SDL_AudioSpec& obtaine
desiredSpec.samples = 4096;
desiredSpec.callback = RenderSDLSamples;
desiredSpec.userdata = this;

/* SDL bug: can return devices with >2 channels (3D surround), even if we ask for just 2.
* This causes the 2 "primary" channels to be moved in the 3D space compared to their "normal" state
* and directional sound doesn't work anymore (though volume change with distance still does).
Expand Down Expand Up @@ -1108,3 +1114,23 @@ std::vector<std::string> CSound::GetSoundDevices()
}
return devices;
}

void CSound::DrawDebug() const
{
// Only draw 3D sound sources that are currently playing
for (const CSoundSource& source: soundSources) {
// Use your extracted data functions instead of OpenAL calls
if (source.IsPlaying(false) && source.IsIn3D()) {
// Get position using your accessor methods instead of OpenAL calls
float3 pos = source.GetPosition(); // Use your implemented accessor

// Draw a wireframe sphere at the sound source position
float color[4] = {1.0f, 0.0f, 0.0f, 0.7f}; // Red with some transparency
CMatrix44f matrix;
matrix.Translate(pos.x, pos.y, pos.z);
matrix.Scale(CSoundSource::REFERENCE_DIST * ELMOS_TO_METERS); // Adjusted scale for better visibility
GL::shapes.DrawWireSphere(8, 8, matrix, color);
}
}
}
Comment on lines +1118 to +1135
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Thread safety concern: DrawDebug() accesses soundSources without locking.

DrawDebug() iterates over soundSources which can be modified by the sound thread. Other methods like Update() and GetNextBestSource() acquire soundMutex before accessing this collection. Consider adding a lock or documenting the threading constraints.

 void CSound::DrawDebug() const
 {
+    std::lock_guard<spring::recursive_mutex> lck(soundMutex);
+
     // Only draw 3D sound sources that are currently playing
     for (const CSoundSource& source: soundSources) {

Note: This requires making soundMutex mutable or removing the const qualifier from DrawDebug().

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In rts/System/Sound/OpenAL/Sound.cpp around lines 1115 to 1132, DrawDebug()
currently iterates soundSources without taking the soundMutex which can race
with the sound thread; acquire the same soundMutex used by
Update()/GetNextBestSource() for the duration of the traversal (or make
soundMutex mutable if DrawDebug() must remain const), then iterate and release
the lock when done, or alternatively remove the const qualifier from DrawDebug()
and lock normally; ensure the lock type matches existing usage (e.g.,
std::lock_guard or std::unique_lock) so access to soundSources is thread-safe.


8 changes: 8 additions & 0 deletions rts/System/Sound/OpenAL/Sound.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <alc.h>
#include <SDL.h>

#include "System/Sound/AttenuationModels/RtsAttenuationModel.h"
#include "System/Sound/ISound.h"
#include "System/float3.h"
#include "System/UnorderedMap.hpp"
Expand All @@ -25,6 +26,8 @@ class SoundItem;
/// Default sound system implementation (OpenAL)
class CSound : public ISound
{
public:
void DrawDebug() const; // Debug visualization method
public:
CSound();
~CSound();
Expand Down Expand Up @@ -73,6 +76,9 @@ class CSound : public ISound
int GetFrameSize() const { return frameSize; }

std::vector<std::string> GetSoundDevices() override;

ISoundAttenuationModel* GetAttenuationModel() override { return attenuationModel; }

private:
typedef spring::unordered_map<std::string, std::string> SoundItemNameMap;
typedef spring::unordered_map<std::string, SoundItemNameMap> SoundItemDefsMap;
Expand Down Expand Up @@ -103,6 +109,8 @@ class CSound : public ISound

std::string selectedDeviceName = "";

ISoundAttenuationModel* attenuationModel = nullptr;

spring::thread soundThread;
spring::unordered_map<std::string, size_t> soundMap; // <name, id>
spring::unordered_set<std::string> preloadSet;
Expand Down
Loading