新增射线追踪音频插件
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Tienne_k
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
# raytraced-audio
|
||||
Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle.
|
||||
|
||||
Check out [this amazing video](https://youtu.be/u6EuAUjq92k?si=6W-sGozYBQITEgQo) by **Vercidium** for insights on how this works!
|
||||
|
||||
No need to create and maintain zones while creating levels, just put the `RaytracedAudioListener` node in your scene and most features will be available for you to use!
|
||||
|
||||
Additionally, by using `RaytracedAudioPlayer3D`s instead of regular `AudioPlayer3D`s, sounds will get automatically muffled behind walls.
|
||||
|
||||
### Audio buses
|
||||
Right off the bat, this plugin creates 2 new audio buses for you to use across your project:
|
||||
a *"Reverb"* bus, and an *"Ambient"* bus.
|
||||
Note: both buses' names can be changed under `Project Settings > Raytraced Audio`.
|
||||
|
||||
The reverb bus controls echo / reverb.
|
||||
For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open.
|
||||
|
||||
The ambient bus controls the strength and pan of sounds coming from outside.
|
||||
For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it.
|
||||
|
||||
### Performace
|
||||
|
||||
Because the rays need to gather different informations about the environment, the actual number of processed rays can go up to:
|
||||
|
||||
`rays_count * (2 + n)` where `n` is the number of enabled `RaytracedAudioPlayer3D`s in the scene.
|
||||
|
||||
That is:
|
||||
|
||||
`(rays_count: configurable) * (1 ray that bounces around + 1 echo ray + (1 muffle ray per enabled RaytracedAudioPlayer3D))`
|
||||
|
||||
Raytraced Audio also adds 2 performance monitors:
|
||||
- `raytraced_audio/raycast_updates`: How many raycast updates happened in the update tick
|
||||
- `raytraced_audio/enabled_players_count`: How many `RaytracedAudioPlayer3D`s are currently enabled in the scene
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
#### Manual installation
|
||||
|
||||
- Download or clone this repository
|
||||
- Copy the `addons/raytraced_audio` folder into your project's `addons/` folder
|
||||
- Enable the plugin in `Project Settings > Plugins > Raytraced Audio`
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
extends RayCast3D
|
||||
|
||||
# dont you worry about this class, habibi
|
||||
# shhhhhh its okay
|
||||
# ( - ͜ʖ -)☞(ʘ_ʘ; )
|
||||
|
||||
var cast_dist: float = 0.0
|
||||
var max_bounces: int = 1
|
||||
var ray_scatter: Callable
|
||||
|
||||
var echo_dist: float = 0.0
|
||||
var echo_count: int = 0
|
||||
var bounces: int = 0
|
||||
var has_bounced_this_tick: bool = false
|
||||
var ray_casts_this_tick: int = 0
|
||||
var escaped: bool = false
|
||||
var escape_dir: Vector3 = Vector3.ZERO
|
||||
|
||||
var _space_state: PhysicsDirectSpaceState3D
|
||||
|
||||
|
||||
func _init(raycast_dist: float, max_bounce_count: int) -> void:
|
||||
top_level = true
|
||||
enabled = false
|
||||
cast_dist = raycast_dist
|
||||
max_bounces = max_bounce_count
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
owner = get_parent()
|
||||
# since the root can be custom, this assert may fail
|
||||
# assert(owner is RaytracedAudioListener)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_space_state = get_world_3d().direct_space_state
|
||||
reset()
|
||||
|
||||
|
||||
# TODO: optimize
|
||||
func update():
|
||||
# Reset if needed
|
||||
if escaped or bounces > max_bounces:
|
||||
reset()
|
||||
|
||||
force_raycast_update()
|
||||
ray_casts_this_tick += 1
|
||||
bounces += 1
|
||||
|
||||
# Escaped outside
|
||||
if !is_colliding():
|
||||
escaped = true
|
||||
global_position += target_position
|
||||
# Muffle ray: Check for line of sight with audio players
|
||||
for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME):
|
||||
var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty()
|
||||
player._lowpass_rays_count += int(has_line_of_sight)
|
||||
return
|
||||
|
||||
# Bounce
|
||||
var hit_pos: Vector3 = get_collision_point()
|
||||
var normal: Vector3 = get_collision_normal()
|
||||
global_position = hit_pos + normal * 0.1
|
||||
target_position = target_position.bounce(normal)
|
||||
has_bounced_this_tick = true
|
||||
|
||||
# Muffle ray: Check for line of sight with audio players
|
||||
for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME):
|
||||
var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty()
|
||||
player._lowpass_rays_count += int(has_line_of_sight)
|
||||
# Same as:
|
||||
# if has_line_of_sight:
|
||||
# player.lowpass_rays_count += 1
|
||||
|
||||
# Echo ray: Check for line of light with listener
|
||||
# Optimization: no need to raycast for first echo bounce
|
||||
if bounces == 1:
|
||||
echo_dist = hit_pos.distance_to(owner.global_position)
|
||||
echo_count += 1
|
||||
escape_dir = target_position.normalized()
|
||||
elif _cast_ray(owner.global_position).is_empty():
|
||||
# The way is clear -> echo
|
||||
echo_dist = hit_pos.distance_to(owner.global_position)
|
||||
echo_count += 1
|
||||
escape_dir = owner.global_position.direction_to(hit_pos)
|
||||
|
||||
|
||||
# Return to the listener with a random direction
|
||||
func reset():
|
||||
var dir: Vector3 = ray_scatter.call()
|
||||
|
||||
global_position = owner.global_position
|
||||
target_position = dir * cast_dist
|
||||
|
||||
bounces = 0
|
||||
escaped = false
|
||||
escape_dir = dir
|
||||
reset_tick_stats()
|
||||
|
||||
|
||||
# Called after this ray is done ticking, in RaytracedAudioListener
|
||||
func reset_tick_stats() -> void:
|
||||
has_bounced_this_tick = false
|
||||
ray_casts_this_tick = 0
|
||||
echo_dist = 0.0
|
||||
echo_count = 0
|
||||
|
||||
|
||||
func set_scatter_model(model: RaytracedAudioListener.RayScatterModel) -> void:
|
||||
match model:
|
||||
RaytracedAudioListener.RayScatterModel.RANDOM:
|
||||
ray_scatter = _random_dir
|
||||
RaytracedAudioListener.RayScatterModel.XZ:
|
||||
ray_scatter = _random_dir_xz_plane
|
||||
_:
|
||||
push_error("Unknown ray scatter model: '", model, "'")
|
||||
ray_scatter = _random_dir
|
||||
|
||||
|
||||
|
||||
func _cast_ray(to: Vector3) -> Dictionary:
|
||||
var params: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(
|
||||
global_position,
|
||||
to,
|
||||
0b01
|
||||
)
|
||||
ray_casts_this_tick += 1
|
||||
return _space_state.intersect_ray(params)
|
||||
|
||||
|
||||
func _random_dir_xz_plane() -> Vector3:
|
||||
var yaw = randf_range(0.0, TAU)
|
||||
return Vector3(-sin(yaw), 0.0, -cos(yaw))
|
||||
|
||||
func _random_dir() -> Vector3:
|
||||
var theta: float = randf_range(0.0, TAU)
|
||||
var y: float = randf_range(-1.0, 1.0)
|
||||
var k: float = sqrt(1.0 - y*y)
|
||||
return Vector3(k * cos(theta), k * sin(theta), y)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqb6rv4pmub8c
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="Raytraced Audio"
|
||||
description="Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle"
|
||||
author="Tienne_k"
|
||||
version="1.0"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,83 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
_setup_settings()
|
||||
|
||||
# For some reason, adding custom types like this fails to generate documentation sometimes??
|
||||
# add_custom_type(
|
||||
# "RaytracedAudioListener",
|
||||
# "AudioListener3D",
|
||||
# load("res://addons/raytraced_audio/raytraced_audio_listener.gd"),
|
||||
# null
|
||||
# )
|
||||
# add_custom_type(
|
||||
# "RaytracedAudioPlayer3D",
|
||||
# "AudioStreamPlayer3D",
|
||||
# load("res://addons/raytraced_audio/raytraced_audio_player_3d.gd"),
|
||||
# null
|
||||
# )
|
||||
|
||||
_setup_audio_buses()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# remove_custom_type("RaytracedAudioListener")
|
||||
# remove_custom_type("RaytracedAudioPlayer3D")
|
||||
|
||||
_clean_up_settings()
|
||||
_clean_up_audio_buses()
|
||||
|
||||
|
||||
func _setup_settings() -> void:
|
||||
ProjectSettings.set_setting("raytraced_audio/reverb_bus", &"RaytracedReverb")
|
||||
ProjectSettings.set_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient")
|
||||
|
||||
if ProjectSettings.get_setting("audio/general/3d_panning_strength") < 1.0:
|
||||
print("[INFO] RaytracedAudio: I recommend setting Audio/General/3d_panning_strength in Project Settings to 1.0 or above")
|
||||
|
||||
|
||||
func _clean_up_settings() -> void:
|
||||
ProjectSettings.set_setting("raytraced_audio/reverb_bus", null)
|
||||
ProjectSettings.set_setting("raytraced_audio/ambient_bus", null)
|
||||
|
||||
|
||||
func _setup_audio_buses() -> void:
|
||||
print("[INFO] Raytraced Audio: setting up audio buses")
|
||||
_clean_up_audio_buses()
|
||||
|
||||
# Reverb
|
||||
var i: int = AudioServer.bus_count
|
||||
AudioServer.add_bus()
|
||||
AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb"))
|
||||
AudioServer.set_bus_send(i, &"Master")
|
||||
var reverb: AudioEffectReverb = AudioEffectReverb.new()
|
||||
reverb.hipass = 1.0
|
||||
reverb.resource_name = "reverb"
|
||||
AudioServer.add_bus_effect(i, reverb)
|
||||
|
||||
# Ambient
|
||||
i = AudioServer.bus_count
|
||||
AudioServer.add_bus()
|
||||
AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient"))
|
||||
AudioServer.set_bus_send(i, &"Master")
|
||||
var panner: AudioEffectPanner = AudioEffectPanner.new()
|
||||
panner.resource_name = "pan"
|
||||
AudioServer.add_bus_effect(i, panner)
|
||||
|
||||
# There's a bug in Godot that makes it so the ambient bus doesnt show properly in the editor
|
||||
# This is "fixable" by adding a temporary bus afterwards and deleting it immediately because ofc.
|
||||
# fuck you (respectfully)
|
||||
i = AudioServer.bus_count
|
||||
AudioServer.add_bus()
|
||||
AudioServer.remove_bus(i)
|
||||
|
||||
|
||||
func _clean_up_audio_buses() -> void:
|
||||
var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb"))
|
||||
if i != -1:
|
||||
AudioServer.remove_bus(i)
|
||||
i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient"))
|
||||
if i != -1:
|
||||
AudioServer.remove_bus(i)
|
||||
@@ -0,0 +1 @@
|
||||
uid://8y7slc7nck0o
|
||||
@@ -0,0 +1,356 @@
|
||||
class_name RaytracedAudioListener extends AudioListener3D
|
||||
## 3D audio listener for raytraced audio
|
||||
##
|
||||
## [b]Audio buses[/b]
|
||||
## [br]This plugin creates 2 new audio buses for you to use across your project:
|
||||
## a [i]"Reverb"[/i] bus, and an [i]"Ambient"[/i] bus.
|
||||
## [br]Note: both buses' names can be changed under [code]Project Settings > Raytraced Audio[/code].
|
||||
## [br]
|
||||
## [br]The reverb bus controls echo / reverb.
|
||||
## [br]For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open
|
||||
## [br]
|
||||
## [br]The ambient bus controls the strength and pan of sounds coming from outside.
|
||||
## [br]For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it
|
||||
## [br]
|
||||
## [br][b]Performace[/b]
|
||||
## [br]Raytraced Audio adds 2 performance monitors:
|
||||
## [br] - [code]raytraced_audio/raycast_updates[/code]: How many raycast updates happened in one update tick
|
||||
## [br] - [code]raytraced_audio/enabled_players_count[/code]: How many [RaytracedAudioPlayer3D]s are enabled in the scene
|
||||
## [br]
|
||||
## [br]Note: There should be only one [RaytracedAudioListener] in a given scene, just like how there should be only one AudioListener3D in a scene.
|
||||
## [br]See also [RaytracedAudioPlayer3D]
|
||||
|
||||
## Speed of sound in m/s
|
||||
const SPEED_OF_SOUND: float = 343.0
|
||||
## All [RaytracedAudioListener]s will be in this group
|
||||
const GROUP_NAME: StringName = &"raytraced_audio_listener"
|
||||
|
||||
const AudioRay: Script = preload("res://addons/raytraced_audio/audio_ray.gd")
|
||||
|
||||
enum RayScatterModel {
|
||||
## Rays will be shot out in a random 3d direction
|
||||
RANDOM,
|
||||
## Rays will be shot out on the listener's XZ plane (i.e. [code]Vector3(random, 0, random)[/code])
|
||||
XZ,
|
||||
}
|
||||
|
||||
## Emitted when this node is enabled
|
||||
signal enabled
|
||||
## Emitted when this node is disabled
|
||||
signal disabled
|
||||
## Emitted when any configuration for the rays are changed
|
||||
## [br]This includes: [member rays_count], [member max_raycast_dist], [member max_bounces], and [member ray_scatter_model]
|
||||
signal ray_configuration_changed
|
||||
|
||||
## List of rays instanced by this node
|
||||
var rays: Array[AudioRay] = []
|
||||
|
||||
## Enable or disable raycasting
|
||||
## [br]Disabled nodes can't be updated (see [method update]) even if [member auto_update] is set to [code]true[/code]
|
||||
@export var is_enabled: bool = true:
|
||||
set(v):
|
||||
if is_enabled == v:
|
||||
return
|
||||
is_enabled = v
|
||||
|
||||
if is_enabled:
|
||||
if is_node_ready():
|
||||
setup()
|
||||
enabled.emit()
|
||||
else:
|
||||
disabled.emit()
|
||||
if is_node_ready():
|
||||
clear()
|
||||
|
||||
## Whether to update automatically
|
||||
## [br]If set to [code]true[/code], this [RaytracedAudioListener] will update every [i]process[/i] frame
|
||||
@export var auto_update: bool = true:
|
||||
set(v):
|
||||
auto_update = v
|
||||
set_process(auto_update)
|
||||
|
||||
## Number of rays to use
|
||||
## [br]More rays and more bounces mean a more accurate model of the environment can be made
|
||||
## [br] See also [member max_bounces]
|
||||
## [br]
|
||||
## [br][i]Technical note[/i]:
|
||||
## [br] Because the rays need to gather different informations about the environment,
|
||||
## the actual number of processed rays can go up to:
|
||||
## [br] [code]rays_count * (2 + n)[/code] where [code]n[/code] is the number of enabled [RaytracedAudioPlayer3D]s in the scene
|
||||
## [br] ([code]rays_count * (1 ray that bounces around + 1 echo ray + 1 muffle ray per enabled RaytracedAudioPlayer3D[/code])
|
||||
@export var rays_count: int = 4:
|
||||
set(v):
|
||||
if v == rays_count:
|
||||
return
|
||||
rays_count = maxi(v, 1)
|
||||
clear()
|
||||
setup()
|
||||
ray_configuration_changed.emit()
|
||||
|
||||
## The maximum distance which any given ray instanced by this node will cast
|
||||
@export var max_raycast_dist: float = SPEED_OF_SOUND:
|
||||
set(v):
|
||||
max_raycast_dist = v
|
||||
_update_ray_configuration()
|
||||
ray_configuration_changed.emit()
|
||||
## [br]More rays and more bounces mean a more accurate model of the environment can be made
|
||||
## [br] See also [member rays_count]
|
||||
@export var max_bounces: int = 3:
|
||||
set(v):
|
||||
max_bounces = v
|
||||
_update_ray_configuration()
|
||||
ray_configuration_changed.emit()
|
||||
|
||||
## Controls how rays will be instanced
|
||||
@export var ray_scatter_model: RayScatterModel = RayScatterModel.RANDOM:
|
||||
set(v):
|
||||
ray_scatter_model = v
|
||||
_update_ray_configuration()
|
||||
ray_configuration_changed.emit()
|
||||
|
||||
## Custom root for ray position calculations
|
||||
@export var custom_root: Node3D
|
||||
|
||||
@export_category("Muffle")
|
||||
## Enables [RaytracedAudioPlayer3D]s muffling behind walls
|
||||
@export var muffle_enabled: bool = true
|
||||
## The interpolation strength of the muffle
|
||||
## [br]See [member muffle_enabled]
|
||||
@export_range(1.0, 25.0, 0.1) var muffle_interpolation: float = 5.0
|
||||
@export_category("Echo")
|
||||
## Enables updates to the reverb audio bus
|
||||
## [br]See [code]Project Settings > Raytraced Audio > Reverb Bus[/code]
|
||||
@export var echo_enabled: bool = true
|
||||
## The "intensity" of the reverb
|
||||
## [br]More specifically, multiplies the reverb's room size by this amount
|
||||
## [br]The default is [code]2.0[/code] to account for sound waves coming back to the listener after hitting a wall
|
||||
@export var echo_room_size_multiplier: float = 2.0
|
||||
## The interpolation strength of the echo
|
||||
## [br]See [member echo_enabled]
|
||||
@export_range(1.0, 25.0, 0.1) var echo_interpolation: float = 5.0
|
||||
@export_category("Ambient")
|
||||
## Enables updates to the ambient audio bus
|
||||
## [br]See [code]Project Settings > Raytraced Audio > Ambient Bus[/code]
|
||||
@export var ambient_enabled: bool = true
|
||||
## The interpolation strength of ambient sounds' direction (pan)
|
||||
## [br]See [member ambient_enabled], [member ambient_pan_strength]
|
||||
@export_range(1.0, 25.0, 0.1) var ambient_pan_interpolation: float = 5.0
|
||||
## How strong the pan between right and left ear will be
|
||||
## Setting this to 0 disables panning completely
|
||||
## [br]See [member ambient_enabled], [member ambient_pan_interpolation]
|
||||
@export_range(0.0, 1.0, 0.01) var ambient_pan_strength: float = 1.0
|
||||
## The interpolation strength of ambient sounds' volume
|
||||
## [br]See [member ambient_enabled]
|
||||
@export_range(1.0, 25.0, 0.1) var ambient_volume_interpolation: float = 5.0
|
||||
## How smoothly the ambient sounds will fade away when the [AudioListener3D] is no longer "outside"
|
||||
## Values close to 1 will make sounds linger for longer, while values close to 0 will make them drop suddenly
|
||||
## [br]See [member ambient_enabled]
|
||||
@export_range(0.0, 1.0, 0.001) var ambient_volume_attenuation: float = 0.998
|
||||
|
||||
## The size of the room based on data gathered from rays, in world units
|
||||
## [br]Used in reverb calculation
|
||||
var room_size: float = 0.0
|
||||
## How "outside" this [RaytracedAudioListener] is based on data gathered from rays, between 0 and 1
|
||||
var ambience: float = 0.0
|
||||
## The direction to "outside" based on data gathered from rays
|
||||
## [br]Should average out to 0 when this [RaytracedAudioListener] is completely outside
|
||||
var ambient_dir: Vector3 = Vector3.ZERO
|
||||
|
||||
## For debugging purposes
|
||||
## [br]See the [code]raytraced_audio/raycast_updates[/code] performance monitor
|
||||
var ray_casts_this_tick: int = 0
|
||||
|
||||
var _reverb_effect: AudioEffectReverb
|
||||
var _pan_effect: AudioEffectPanner
|
||||
|
||||
# Keep track of whether we've set up debug monitors
|
||||
var _debug_monitors_setup: bool = false
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
add_to_group(GROUP_NAME)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Reverb effect
|
||||
var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus"))
|
||||
if i == -1:
|
||||
push_error("Failed to get reverb bus for raytraced audio. Disabling echo...")
|
||||
echo_enabled = false
|
||||
else:
|
||||
_reverb_effect = AudioServer.get_bus_effect(i, 0)
|
||||
|
||||
# Pan effect
|
||||
i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus"))
|
||||
if i == -1:
|
||||
push_error("Failed to get ambient bus for raytraced audio. Disabling ambience...")
|
||||
ambient_enabled = false
|
||||
else:
|
||||
_pan_effect = AudioServer.get_bus_effect(i, 0)
|
||||
|
||||
if is_enabled:
|
||||
if custom_root:
|
||||
call_deferred("setup")
|
||||
else:
|
||||
setup()
|
||||
|
||||
set_process(auto_update)
|
||||
if is_enabled:
|
||||
make_current()
|
||||
|
||||
_setup_debug()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_cleanup_debug()
|
||||
|
||||
|
||||
func _setup_debug() -> void:
|
||||
if _debug_monitors_setup:
|
||||
return
|
||||
|
||||
_debug_monitors_setup = true
|
||||
|
||||
Performance.add_custom_monitor(&"raytraced_audio/raycast_updates", func():
|
||||
# Check if this instance is still valid
|
||||
if not is_instance_valid(self):
|
||||
return 0
|
||||
return ray_casts_this_tick
|
||||
)
|
||||
|
||||
Performance.add_custom_monitor(&"raytraced_audio/enabled_players_count", func():
|
||||
# Check if this instance is still valid and if we're in a scene tree
|
||||
if not is_instance_valid(self) or not is_inside_tree():
|
||||
return 0
|
||||
return get_tree().get_node_count_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME)
|
||||
)
|
||||
|
||||
|
||||
func _cleanup_debug() -> void:
|
||||
if not _debug_monitors_setup:
|
||||
return
|
||||
|
||||
_debug_monitors_setup = false
|
||||
|
||||
# Remove the custom monitors
|
||||
Performance.remove_custom_monitor(&"raytraced_audio/raycast_updates")
|
||||
Performance.remove_custom_monitor(&"raytraced_audio/enabled_players_count")
|
||||
|
||||
|
||||
|
||||
## Initiates this [RaytracedAudioListener]'s rays
|
||||
func setup() -> void:
|
||||
for __ in rays_count:
|
||||
var rc: AudioRay = AudioRay.new(max_raycast_dist, max_bounces)
|
||||
rc.set_scatter_model(ray_scatter_model)
|
||||
# Add to the custom root if present
|
||||
if custom_root:
|
||||
custom_root.add_child(rc, INTERNAL_MODE_BACK)
|
||||
else:
|
||||
add_child(rc, INTERNAL_MODE_BACK)
|
||||
rays.push_back(rc)
|
||||
|
||||
|
||||
## Clears all created rays
|
||||
func clear():
|
||||
for ray: AudioRay in rays:
|
||||
if custom_root:
|
||||
custom_root.remove_child(ray)
|
||||
else:
|
||||
remove_child(ray)
|
||||
ray.queue_free()
|
||||
rays.clear()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if !auto_update:
|
||||
set_process(false)
|
||||
return
|
||||
update(delta)
|
||||
|
||||
|
||||
## Updates this [RaytracedAudioListener]
|
||||
## [br]If you are updating this node manually (i.e. [member auto_update] is [code]false[/code]), this is the method to call
|
||||
func update(delta: float):
|
||||
ray_casts_this_tick = 0
|
||||
if !is_enabled:
|
||||
return
|
||||
|
||||
var echo: float = 0.0 # Avg echo from all rays
|
||||
var echo_count: int = 0 # Number of echo rays that came back
|
||||
var bounces_this_tick: int = 0
|
||||
var escaped_count: int = 0
|
||||
var escaped_dir: Vector3 = Vector3.ZERO # Avg escape direction
|
||||
var escaped_strength: float = 0.0
|
||||
|
||||
# Gather data
|
||||
for ray: AudioRay in rays:
|
||||
ray.update()
|
||||
ray_casts_this_tick += ray.ray_casts_this_tick
|
||||
|
||||
echo += ray.echo_dist
|
||||
echo_count += ray.echo_count
|
||||
bounces_this_tick += int(ray.has_bounced_this_tick)
|
||||
|
||||
if ray.escaped:
|
||||
escaped_count += 1
|
||||
escaped_strength += 1.0 / ray.bounces
|
||||
escaped_dir += ray.escape_dir
|
||||
|
||||
ray.reset_tick_stats()
|
||||
|
||||
echo = 0.0 if echo_count == 0 else (echo / float(echo_count))
|
||||
escaped_dir = Vector3.ZERO if escaped_count == 0 else (escaped_dir / float(escaped_count))
|
||||
|
||||
if muffle_enabled:
|
||||
_update_muffle(delta)
|
||||
if echo_enabled:
|
||||
_update_echo(echo, echo_count, bounces_this_tick, delta)
|
||||
if ambient_enabled:
|
||||
_update_ambient(escaped_strength, escaped_dir, delta)
|
||||
|
||||
|
||||
func _update_muffle(delta: float) -> void:
|
||||
for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.GROUP_NAME):
|
||||
player.update(self, delta)
|
||||
|
||||
|
||||
func _update_echo(echo: float, echo_count: int, bounces: int, delta: float) -> void:
|
||||
# Length -> echo delay
|
||||
room_size = lerpf(room_size, echo, 1.0 - exp(-delta * echo_interpolation))
|
||||
var e: float = (room_size * echo_room_size_multiplier) / SPEED_OF_SOUND
|
||||
# print("e = ", e)
|
||||
_reverb_effect.room_size = lerpf(_reverb_effect.room_size, clampf(e, 0.0, 1.0), 1.0 - exp(-delta * echo_interpolation))
|
||||
_reverb_effect.predelay_msec = lerpf(_reverb_effect.predelay_msec, e * 1000, 1.0 - exp(-delta * echo_interpolation))
|
||||
_reverb_effect.predelay_feedback = lerpf(_reverb_effect.predelay_feedback, clampf(e, 0.0, 0.98), 1.0 - exp(-delta * echo_interpolation))
|
||||
# More rays % -> echo strength
|
||||
var return_ratio: float = 0.0 if bounces == 0 else float(echo_count) / float(bounces)
|
||||
_reverb_effect.hipass = lerpf(_reverb_effect.hipass, 1.0 - return_ratio, 1.0 - exp(-delta * echo_interpolation))
|
||||
|
||||
|
||||
func _update_ambient(escaped_strength: float, escaped_dir: Vector3, delta: float) -> void:
|
||||
var ambience_ratio: float = float(escaped_strength) / float(rays_count)
|
||||
|
||||
# More rays % -> louder
|
||||
if escaped_strength > 0:
|
||||
#should it use delta?
|
||||
#ambience = lerpf(ambience, 1.0, 1.0 - exp(-delta * ambience_ratio))
|
||||
ambience = lerpf(ambience, 1.0, ambience_ratio)
|
||||
else:
|
||||
ambience *= ambient_volume_attenuation
|
||||
var ambient_bus_idx: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus"))
|
||||
var volume: float = AudioServer.get_bus_volume_linear(ambient_bus_idx)
|
||||
AudioServer.set_bus_volume_linear(ambient_bus_idx, lerpf(volume, ambience, 1.0 - exp(-delta * ambient_volume_interpolation)))
|
||||
|
||||
# Avg escape direction -> pan
|
||||
ambient_dir = ambient_dir.slerp(escaped_dir, 1.0 - exp(-delta * ambient_pan_interpolation))
|
||||
var target_pan: float = 0.0 if ambient_dir.is_zero_approx() else owner.transform.basis.x.dot(ambient_dir.normalized())
|
||||
_pan_effect.pan = target_pan * ambient_pan_strength
|
||||
|
||||
|
||||
# ✨ the name says it all ✨
|
||||
func _update_ray_configuration() -> void:
|
||||
for ray: AudioRay in rays:
|
||||
ray.cast_dist = max_raycast_dist
|
||||
ray.max_bounces = max_bounces
|
||||
ray.set_scatter_model(ray_scatter_model)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjpmu5sklv5h8
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkb8ltjc6067n
|
||||
Reference in New Issue
Block a user