新增射线追踪音频插件
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
class_name RaytracedAudioPlayer3D extends AudioStreamPlayer3D
|
||||
## Audio stream player that allows for audio muffling
|
||||
##
|
||||
## You can already use the reverb and ambient audio buses (see the documentation for [RaytracedAudioListener] for more details)
|
||||
## to make use of raytraced audio effects, but using [RaytracedAudioPlayer3D]s allows you to use more capabilities.
|
||||
## Namely, muffling sounds behind walls.
|
||||
## [br]
|
||||
## [br][b][color=yellow]Warning[/color][/b]: [RaytracedAudioPlayer3D]s will default to using the reverb audio bus
|
||||
## [br]If you wish to use another bus instead, please set it after this node has been added to the scene tree (i.e. after [code]_enter_tree()[/code])
|
||||
## [br]
|
||||
## [br][i]Technical note[/i]:
|
||||
## [br]Currently, there is no way to muffle only one audio player (or apply any effect for that matter).
|
||||
## [br]So we create a new bus for every audio player that is audible from the [RaytracedAudioListener]
|
||||
## [br]Godot doesn't provide methods to calculate the volume of an audio at certain distances, so we calculate
|
||||
## that ourselves (see [method calculate_audible_distance_threshold()])
|
||||
|
||||
## All [RaytracedAudioPlayer3D]s (regardless of state) will be in this group
|
||||
const GROUP_NAME: StringName = &"raytraced_audio_player_3d"
|
||||
## All [b]enabled[/b] [RaytracedAudioPlayer3D]s will be in this group
|
||||
const ENABLED_GROUP_NAME: StringName = &"enabled_raytraced_audio_player_3d"
|
||||
## The lowpass frequency when completely muffled
|
||||
const LOWPASS_MIN_HZ: float = 250.0
|
||||
## The lowpass frequency when completely not-muffled (?)
|
||||
const LOWPASS_MAX_HZ: float = 20000.0
|
||||
## The factor the nyquist frequency is multiplied with to get a safe max_cutoff
|
||||
const NYQUIST_MULTIPLIER: float = 0.95
|
||||
|
||||
## Used in internal calculations
|
||||
const LOG2: float = log(2.0)
|
||||
## Used in internal calculations
|
||||
const LOG_MIN_HZ: float = log(LOWPASS_MIN_HZ) / LOG2
|
||||
## Used in internal calculations
|
||||
const LOG_MAX_HZ: float = log(LOWPASS_MAX_HZ) / LOG2
|
||||
|
||||
## Emitted when this node is enabled
|
||||
## [br]See also [constant ENABLED_GROUP_NAME]
|
||||
signal enabled
|
||||
## Emitted when this node is disabled
|
||||
signal disabled
|
||||
## Emitted when the maximum audible distance for this node is changed
|
||||
## [br]See also [member AudioStreamPlayer3D.max_distance] and [member audibility_threshold_db]
|
||||
signal audible_distance_updated(distance: float)
|
||||
|
||||
## The threshold (in decibels) at which sounds will be considered inaudible
|
||||
## [br]This is used to enable / disable this node when it's not audible to save resources
|
||||
## [br]The distance at which this node is no longer considered audible is automatically stored inside [member AudioStreamPlayer3D.max_distance]
|
||||
## (if not already set), also to save resources
|
||||
## [br]Though, setting [member AudioStreamPlayer3D.max_distance] doesn't update this field automatically
|
||||
## [br]See also [method get_volume_db_from_pos] and [method calculate_audible_distance_threshold]
|
||||
@export var audibility_threshold_db: float = -30.0:
|
||||
set(v):
|
||||
audibility_threshold_db = v
|
||||
# Max distance not configured
|
||||
if is_node_ready() and max_distance == 0.0:
|
||||
max_distance = calculate_audible_distance_threshold()
|
||||
audible_distance_updated.emit(max_distance)
|
||||
|
||||
var _lowpass_rays_count: int = 0
|
||||
var _lowpass_max_safe_cutoff: float = LOWPASS_MAX_HZ
|
||||
var _is_enabled: bool = false
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
add_to_group(GROUP_NAME)
|
||||
bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback
|
||||
|
||||
func _ready() -> void:
|
||||
# Max distance not configured
|
||||
if max_distance == 0.0:
|
||||
max_distance = calculate_audible_distance_threshold()
|
||||
audible_distance_updated.emit(max_distance)
|
||||
|
||||
# Update the lowpass limits on start
|
||||
_lowpass_max_safe_cutoff = calculate_lowpass_limit()
|
||||
|
||||
|
||||
## Enables this node
|
||||
## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually
|
||||
## [br]See [method update]
|
||||
func enable():
|
||||
if _is_enabled:
|
||||
return
|
||||
_is_enabled = true
|
||||
var i: int = _create_bus()
|
||||
bus = AudioServer.get_bus_name(i)
|
||||
add_to_group(ENABLED_GROUP_NAME)
|
||||
|
||||
enabled.emit()
|
||||
|
||||
## Enables this node
|
||||
## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually
|
||||
## [br]See [method update]
|
||||
func disable():
|
||||
if !_is_enabled:
|
||||
return
|
||||
|
||||
# Don't remove the fallback bus lol
|
||||
if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"):
|
||||
_disable()
|
||||
return
|
||||
|
||||
# Remove this node's specific bus
|
||||
var idx: int = AudioServer.get_bus_index(bus)
|
||||
if idx == -1:
|
||||
push_warning("audio bus ", bus, " not found")
|
||||
_disable()
|
||||
return
|
||||
_disable()
|
||||
AudioServer.remove_bus(idx)
|
||||
|
||||
func _disable():
|
||||
_is_enabled = false
|
||||
bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback
|
||||
remove_from_group(ENABLED_GROUP_NAME)
|
||||
_lowpass_rays_count = 0
|
||||
|
||||
disabled.emit()
|
||||
|
||||
|
||||
# Returns the index of the created bus
|
||||
func _create_bus() -> int:
|
||||
var i: int = AudioServer.bus_count
|
||||
AudioServer.add_bus()
|
||||
AudioServer.set_bus_name(i, generate_bus_name())
|
||||
AudioServer.set_bus_send(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus"))
|
||||
AudioServer.add_bus_effect(i, AudioEffectLowPassFilter.new())
|
||||
return i
|
||||
|
||||
## Returns a name for the audio bus created for this node
|
||||
func generate_bus_name() -> String:
|
||||
return str("RTAudioPlayer3D_", name, randi())
|
||||
|
||||
## ✨ the name says it all ✨
|
||||
func is_enabled() -> bool:
|
||||
return _is_enabled
|
||||
|
||||
## Updates this [RaytracedAudioPlayer3D]
|
||||
## [br]This method will adjust the muffle of the played stream, as well as enable or disable it
|
||||
## based on whether it's audible from the given [RaytracedAudioListener]
|
||||
## [br]If you are updating this node manually, this is the method to call
|
||||
func update(listener: RaytracedAudioListener, delta: float) -> void:
|
||||
if _is_enabled:
|
||||
_update(listener.rays_count, listener.muffle_interpolation, delta)
|
||||
|
||||
_lowpass_rays_count = 0
|
||||
|
||||
# Enable based on position
|
||||
var dist_sq: float = global_position.distance_squared_to(listener.global_position)
|
||||
if dist_sq > max_distance*max_distance or !playing:
|
||||
disable()
|
||||
else:
|
||||
enable()
|
||||
|
||||
|
||||
func _update(rays_count: int, interpolation: float, delta: float):
|
||||
if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"):
|
||||
_disable()
|
||||
return
|
||||
|
||||
var idx: int = AudioServer.get_bus_index(bus)
|
||||
if idx == -1:
|
||||
push_error("audio bus ", bus, " not found")
|
||||
_disable()
|
||||
else:
|
||||
var ratio: float = float(_lowpass_rays_count) / float(rays_count)
|
||||
ratio = clamp(ratio, 0, 1)
|
||||
var lowpass: AudioEffectLowPassFilter = AudioServer.get_bus_effect(idx, 0)
|
||||
|
||||
# Frequencies aren't linear, they scale logarithmically (log2 space) +1 octave = 2x the frequency
|
||||
# So we scale frequencies down before lerping, then scale them back up
|
||||
var log_t: float = lerpf(LOG_MIN_HZ, LOG_MAX_HZ, ratio)
|
||||
var log_hz: float = log(lowpass.cutoff_hz) / LOG2 # Scale current frequency down: log2(x) = ln(x) / ln(2)
|
||||
|
||||
log_hz = lerpf(log_hz, log_t, 1.0 - exp(-delta * interpolation)) # Lerp in scaled down space
|
||||
lowpass.cutoff_hz = min(pow(2, log_hz), _lowpass_max_safe_cutoff) # Limit at max cutoff
|
||||
|
||||
|
||||
# Translated from the godot repo
|
||||
## Calculates the volume (in decibels) of the audio stream from the given position
|
||||
## [br]Please note that the final volume will vary depending on the stream that is being played
|
||||
func get_volume_db_from_pos(from_pos: Vector3) -> float:
|
||||
const CMP_EPSILON: float = 0.0001
|
||||
|
||||
var dist: float = from_pos.distance_to(global_position)
|
||||
var vol: float = 0.0
|
||||
match attenuation_model:
|
||||
ATTENUATION_INVERSE_DISTANCE:
|
||||
vol = linear_to_db(1.0 / ((dist / unit_size) + CMP_EPSILON))
|
||||
ATTENUATION_INVERSE_SQUARE_DISTANCE:
|
||||
var d: float = (dist / unit_size)
|
||||
vol = linear_to_db(1.0 / (d*d + CMP_EPSILON))
|
||||
ATTENUATION_LOGARITHMIC:
|
||||
vol = -20.0 * log(dist / unit_size + CMP_EPSILON)
|
||||
ATTENUATION_DISABLED:
|
||||
pass
|
||||
_:
|
||||
push_error("Unknown attenuation type: '", attenuation_model, "'")
|
||||
vol = minf(vol + volume_db, max_db)
|
||||
return vol
|
||||
|
||||
|
||||
## Calculates the distance at which this [RaytracedAudioPlayer3D] is no longer considered audible
|
||||
## [br]See also [member audibility_threshold_db]
|
||||
func calculate_audible_distance_threshold() -> float:
|
||||
if max_distance > 0.0:
|
||||
return max_distance
|
||||
|
||||
match attenuation_model:
|
||||
ATTENUATION_INVERSE_DISTANCE:
|
||||
var t_lin: float = db_to_linear(audibility_threshold_db - volume_db)
|
||||
# Unit_size / dist < T_lin
|
||||
return unit_size / t_lin
|
||||
ATTENUATION_INVERSE_SQUARE_DISTANCE:
|
||||
var t_lin: float = db_to_linear(audibility_threshold_db - volume_db)
|
||||
# (Unit_size / dist)^2 < T_lin
|
||||
return sqrt(unit_size*unit_size / t_lin)
|
||||
ATTENUATION_LOGARITHMIC:
|
||||
var t_db: float = audibility_threshold_db - volume_db
|
||||
# -20 * log(dist / Unit_size) > T_db
|
||||
return exp(t_db / -20.0) * unit_size
|
||||
ATTENUATION_DISABLED:
|
||||
return 0.0
|
||||
_:
|
||||
push_error("Unknown attenuation model: '", attenuation_model, "'")
|
||||
return 0.0
|
||||
|
||||
|
||||
## Gets the current sampling rate of the AudioServer and calculates
|
||||
## a safe cutoff_hz value for the lowpass filter.
|
||||
func calculate_lowpass_limit() -> float:
|
||||
var nyquist_frequency = AudioServer.get_mix_rate() * 0.5
|
||||
return nyquist_frequency * NYQUIST_MULTIPLIER
|
||||
|
||||
|
||||
## Checks wheter this [RaytracedAudioPlayer3D] is considered audible from the given position
|
||||
## [br]See also [member audibility_threshold_db]
|
||||
func is_audible(from_pos: Vector3) -> bool:
|
||||
return get_volume_db_from_pos(from_pos) >= audibility_threshold_db
|
||||
Reference in New Issue
Block a user