Initial rhythm implementation (+shaders and stagelight)
This commit is contained in:
parent
a69ed298dc
commit
71d967d988
18
game/assets/shaders/rhythm/bar_test.gdshader
Normal file
18
game/assets/shaders/rhythm/bar_test.gdshader
Normal file
@ -0,0 +1,18 @@
|
||||
shader_type spatial;
|
||||
#include "uid://cy7b01or0ckgk" // Rhythm Helper
|
||||
|
||||
|
||||
void vertex() {
|
||||
// Called for every vertex the material is visible on.
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float phase = get_bar_phase();
|
||||
ALPHA = (1.0 - phase) * (UV.y > phase ? 1.0 : 0.0) * (UV.x < 1.0 - get_song_time_ratio() ? 1.0 : 0.0);
|
||||
ALBEDO.gb = vec2(distance(phase + 0.75, 0.75));
|
||||
}
|
||||
|
||||
//void light() {
|
||||
// // Called for every pixel for every light affecting the material.
|
||||
// // Uncomment to replace the default light processing function with this one.
|
||||
//}
|
||||
1
game/assets/shaders/rhythm/bar_test.gdshader.uid
Normal file
1
game/assets/shaders/rhythm/bar_test.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dlqjg6dby6pdy
|
||||
18
game/assets/shaders/rhythm/beat_test.gdshader
Normal file
18
game/assets/shaders/rhythm/beat_test.gdshader
Normal file
@ -0,0 +1,18 @@
|
||||
shader_type spatial;
|
||||
#include "uid://cy7b01or0ckgk" // Rhythm Helper
|
||||
|
||||
|
||||
void vertex() {
|
||||
// Called for every vertex the material is visible on.
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float phase = get_beat_phase();
|
||||
ALPHA = (1.0 - phase) * (UV.y > phase ? 1.0 : 0.0) * (-UV.x < -get_song_time_ratio() ? 1.0 : 0.0);
|
||||
ALBEDO.gb = vec2(distance(phase + 0.5, 0.5));
|
||||
}
|
||||
|
||||
//void light() {
|
||||
// // Called for every pixel for every light affecting the material.
|
||||
// // Uncomment to replace the default light processing function with this one.
|
||||
//}
|
||||
1
game/assets/shaders/rhythm/beat_test.gdshader.uid
Normal file
1
game/assets/shaders/rhythm/beat_test.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c7n5s8eodlca6
|
||||
24
game/assets/shaders/rhythm/beating_lightbeam.gdshader
Normal file
24
game/assets/shaders/rhythm/beating_lightbeam.gdshader
Normal file
@ -0,0 +1,24 @@
|
||||
shader_type spatial;
|
||||
render_mode unshaded, shadows_disabled, cull_disabled;
|
||||
#include "uid://cy7b01or0ckgk" // Rhythm Helper
|
||||
|
||||
uniform vec3 color: source_color = vec3(1.0, 1.0, 1.0);
|
||||
uniform float alpha_multiplier: hint_range(0.0, 1.0, 0.001) = 0.25;
|
||||
uniform sampler2D albedo;
|
||||
uniform float flash_exponent = 3.0;
|
||||
|
||||
void vertex() {
|
||||
// Called for every vertex the material is visible on.
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float phase = get_beat_phase();
|
||||
vec4 tex = texture(albedo, UV);
|
||||
ALBEDO = mix(tex.rgb, color.rgb, 0.5);
|
||||
ALPHA = tex.a * ease(1.0 - phase, flash_exponent) * alpha_multiplier;//mix(tex.a, color.a * (1.0 - phase), 0.5);
|
||||
}
|
||||
|
||||
//void light() {
|
||||
// // Called for every pixel for every light affecting the material.
|
||||
// // Uncomment to replace the default light processing function with this one.
|
||||
//}
|
||||
@ -0,0 +1 @@
|
||||
uid://c32arbbdbo7m8
|
||||
23
game/assets/shaders/rhythm/beating_surface.gdshader
Normal file
23
game/assets/shaders/rhythm/beating_surface.gdshader
Normal file
@ -0,0 +1,23 @@
|
||||
shader_type spatial;
|
||||
render_mode unshaded, shadows_disabled;
|
||||
#include "uid://cy7b01or0ckgk" // Rhythm Helper
|
||||
|
||||
uniform sampler2D albedo;
|
||||
uniform float flash_exponent = 3.0;
|
||||
uniform float intensity_multiplier = 1.0;
|
||||
|
||||
void vertex() {
|
||||
// Called for every vertex the material is visible on.
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float phase = get_beat_phase();
|
||||
vec4 tex = texture(albedo, UV);
|
||||
float intensity = ease((1.0 - phase), flash_exponent) * intensity_multiplier;
|
||||
ALBEDO = tex.rgb * intensity;
|
||||
}
|
||||
|
||||
//void light() {
|
||||
// // Called for every pixel for every light affecting the material.
|
||||
// // Uncomment to replace the default light processing function with this one.
|
||||
//}
|
||||
1
game/assets/shaders/rhythm/beating_surface.gdshader.uid
Normal file
1
game/assets/shaders/rhythm/beating_surface.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b46o5a45g58xb
|
||||
106
game/assets/shaders/rhythm/rhythm_helper.gdshaderinc
Normal file
106
game/assets/shaders/rhythm/rhythm_helper.gdshaderinc
Normal file
@ -0,0 +1,106 @@
|
||||
global uniform float beat;
|
||||
global uniform float bar;
|
||||
global uniform float song_time;
|
||||
global uniform float total_song_time;
|
||||
global uniform float user_offset_ms;
|
||||
|
||||
uniform float phase_offset = 0.0;
|
||||
uniform float phase_multiplier = 1.0;
|
||||
|
||||
instance uniform float instance_phase_offset = 0.0;
|
||||
instance uniform float instance_phase_multiplier = 1.0;
|
||||
|
||||
|
||||
float get_beat_with_offset() {
|
||||
return (beat * phase_multiplier * instance_phase_multiplier) + phase_offset + instance_phase_offset;// + (user_offset_ms / 1000.0);
|
||||
}
|
||||
|
||||
|
||||
float get_bar_with_offset() {
|
||||
return (bar * phase_multiplier * instance_phase_multiplier) + phase_offset + instance_phase_offset;// + (user_offset_ms / 1000.0);
|
||||
}
|
||||
|
||||
|
||||
float get_beat_phase() {
|
||||
float beat_offset = get_beat_with_offset();
|
||||
return beat_offset - floor(beat_offset);
|
||||
}
|
||||
|
||||
|
||||
float get_bar_phase() {
|
||||
float bar_offset = get_bar_with_offset();
|
||||
return bar_offset - floor(bar_offset);
|
||||
}
|
||||
|
||||
float get_song_time_ratio() {
|
||||
return song_time / max(total_song_time, 0.001);
|
||||
}
|
||||
|
||||
float ease(float x, float c) {
|
||||
// Godot's source code converted via ChatGPT to GLSL-style
|
||||
//branchless:
|
||||
//x = clamp(x, 0.0, 1.0);
|
||||
//float pos = mix(
|
||||
//1.0 - pow(1.0 - x, 1.0 / c),
|
||||
//pow(x, c),
|
||||
//step(1.0, c)
|
||||
//);
|
||||
//
|
||||
//float k = -c;
|
||||
//float x2 = x * 2.0;
|
||||
//float inout_v = mix(
|
||||
//pow(x2, k) * 0.5,
|
||||
//(1.0 - pow(2.0 - x2, k)) * 0.5 + 0.5,
|
||||
//step(0.5, x)
|
||||
//);
|
||||
//
|
||||
//float isPos = step(0.0, c);
|
||||
//float isNeg = step(c, 0.0);
|
||||
//
|
||||
//return pos * isPos + inout_v * isNeg;
|
||||
|
||||
// a few branches:
|
||||
x = clamp(x, 0.0, 1.0);
|
||||
|
||||
float pos = mix(
|
||||
1.0 - pow(1.0 - x, 1.0 / c), // 0 < c < 1
|
||||
pow(x, c), // c >= 1
|
||||
step(1.0, c)
|
||||
);
|
||||
|
||||
float k = -c;
|
||||
float x2 = x * 2.0;
|
||||
float inout_v = mix(
|
||||
pow(x2, k) * 0.5,
|
||||
(1.0 - pow(2.0 - x2, k)) * 0.5 + 0.5,
|
||||
step(0.5, x)
|
||||
);
|
||||
|
||||
return (c > 0.0) ? pos :
|
||||
(c < 0.0) ? inout_v :
|
||||
0.0;
|
||||
|
||||
// From godot's source code:
|
||||
//if (x < 0.0) {
|
||||
//x = 0.0;
|
||||
//} else if (x > 1.0) {
|
||||
//x = 1.0;
|
||||
//}
|
||||
//if (c > 0.0) {
|
||||
//if (c < 1.0) {
|
||||
//return 1.0 - pow(1.0 - x, 1.0 / c);
|
||||
//} else {
|
||||
//return pow(x, c);
|
||||
//}
|
||||
//} else if (c < 0.0) {
|
||||
////inout ease
|
||||
//
|
||||
//if (x < 0.5) {
|
||||
//return pow(x * 2.0, -c) * 0.5;
|
||||
//} else {
|
||||
//return (1.0 - pow(1.0 - (x - 0.5) * 2.0, -c)) * 0.5 + 0.5;
|
||||
//}
|
||||
//} else {
|
||||
//return 0.0; // no ease (raw)
|
||||
//}
|
||||
}
|
||||
1
game/assets/shaders/rhythm/rhythm_helper.gdshaderinc.uid
Normal file
1
game/assets/shaders/rhythm/rhythm_helper.gdshaderinc.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cy7b01or0ckgk
|
||||
@ -22,6 +22,7 @@ SPrint="*uid://cpfunq8fyixco"
|
||||
VersionDisplay="*uid://bqxtpo2c64h22"
|
||||
InputManager="*uid://cocp0vmvgd4ln"
|
||||
ControllerIcons="*uid://bdosbfkp568je"
|
||||
ShaderGlobals="*uid://d2lr860r1ysrm"
|
||||
LimboConsole="*uid://dyxornv8vwibg"
|
||||
|
||||
[debug]
|
||||
@ -204,3 +205,26 @@ locale/translations=PackedStringArray("res://localization/localization.en.transl
|
||||
rendering_device/driver.windows="d3d12"
|
||||
anti_aliasing/quality/msaa_3d=3
|
||||
occlusion_culling/use_occlusion_culling=true
|
||||
|
||||
[shader_globals]
|
||||
|
||||
song_time={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
total_song_time={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
beat={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
bar={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
user_offset_ms={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
|
||||
33
game/src/core/autoloads/shader_globals.gd
Normal file
33
game/src/core/autoloads/shader_globals.gd
Normal file
@ -0,0 +1,33 @@
|
||||
extends Node
|
||||
|
||||
|
||||
var beat: float = 0.0: set = set_beat
|
||||
var bar: float = 0.0: set = set_bar
|
||||
var song_time: float = 0.0: set = set_song_time
|
||||
var total_song_time: float = 0.0: set = set_total_song_time
|
||||
var user_offset_ms: float = 0.0
|
||||
|
||||
|
||||
func set_user_offset_ms(offset: float) -> void:
|
||||
user_offset_ms = offset
|
||||
RenderingServer.global_shader_parameter_set(&"user_offset_ms", beat)
|
||||
|
||||
|
||||
func set_beat(_beat: float) -> void:
|
||||
beat = _beat
|
||||
RenderingServer.global_shader_parameter_set(&"beat", beat)
|
||||
|
||||
|
||||
func set_bar(_bar: float) -> void:
|
||||
bar = _bar
|
||||
RenderingServer.global_shader_parameter_set(&"bar", bar)
|
||||
|
||||
|
||||
func set_song_time(time: float) -> void:
|
||||
song_time = time
|
||||
RenderingServer.global_shader_parameter_set(&"song_time", time)
|
||||
|
||||
|
||||
func set_total_song_time(time: float) -> void:
|
||||
total_song_time = time
|
||||
RenderingServer.global_shader_parameter_set(&"total_song_time", time)
|
||||
1
game/src/core/autoloads/shader_globals.gd.uid
Normal file
1
game/src/core/autoloads/shader_globals.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d2lr860r1ysrm
|
||||
66
game/src/core/rhythm/rhythm_listener.gd
Normal file
66
game/src/core/rhythm/rhythm_listener.gd
Normal file
@ -0,0 +1,66 @@
|
||||
class_name RhythmListener
|
||||
extends Node
|
||||
|
||||
|
||||
signal beat_tick(beat_index: int)
|
||||
signal bar_tick(bar_index: int)
|
||||
|
||||
@export_range(0.0, 1.0, 0.001, "or_greater", "or_less") var phase_shift: float = 0.0
|
||||
|
||||
var beat: float = 0.0
|
||||
var bar: float = 0.0
|
||||
var _last_beat_time: float = 0.0
|
||||
var _last_beat_index: int = -1
|
||||
var _last_bar_index: int = -1
|
||||
var _last_bar_time: float = 0.0
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if RhythmPlayer.paused:
|
||||
_last_beat_index = 0
|
||||
_last_bar_index = 0
|
||||
return
|
||||
|
||||
beat = RhythmPlayer.beat + phase_shift
|
||||
bar = RhythmPlayer.bar + phase_shift
|
||||
|
||||
var beat_index: int = floori(beat)
|
||||
var bar_index: int = floori(bar)
|
||||
|
||||
if _last_beat_index != beat_index:
|
||||
on_beat_tick(beat_index)
|
||||
_last_beat_index = beat_index
|
||||
_last_beat_time = RhythmPlayer.song_time + phase_shift
|
||||
|
||||
if _last_bar_index != bar_index:
|
||||
on_bar_tick(bar_index)
|
||||
_last_bar_index = bar_index
|
||||
_last_bar_time = RhythmPlayer.song_time + phase_shift
|
||||
|
||||
#SPrint.print_msgf("Time To Next Bar: %s" % get_time_to_next_bar())
|
||||
#SPrint.print_msgf("Time To Next Beat: %s" % get_time_to_next_beat())
|
||||
|
||||
|
||||
func get_time_to_next_beat() -> float:
|
||||
var beat_duration: float = 60.0 / RhythmPlayer.bpm
|
||||
return (_last_beat_time - (RhythmPlayer.song_time + phase_shift)) + beat_duration
|
||||
|
||||
|
||||
func get_time_to_next_bar() -> float:
|
||||
var bar_duration: float = 60.0 / (RhythmPlayer.bpm / RhythmPlayer.beats_per_bar)
|
||||
return (_last_bar_time - (RhythmPlayer.song_time + phase_shift)) + bar_duration
|
||||
|
||||
|
||||
#func get_time_to_beat(target_beat: float) -> float:
|
||||
#var beat_duration: float = 60.0 / RhythmPlayer.bpm
|
||||
#
|
||||
|
||||
|
||||
func on_beat_tick(beat_index: int) -> void:
|
||||
#SPrint.print_msg("Beat received %s" % beat_index, 0.75)
|
||||
beat_tick.emit(beat_index)
|
||||
|
||||
|
||||
func on_bar_tick(bar_index: int) -> void:
|
||||
#SPrint.print_msg("Bar received %s" % bar_index, 1.0)
|
||||
bar_tick.emit(bar_index)
|
||||
1
game/src/core/rhythm/rhythm_listener.gd.uid
Normal file
1
game/src/core/rhythm/rhythm_listener.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://co7j2qtqpud6b
|
||||
168
game/src/core/rhythm/rhythm_player.gd
Normal file
168
game/src/core/rhythm/rhythm_player.gd
Normal file
@ -0,0 +1,168 @@
|
||||
class_name RhythmPlayer
|
||||
extends Node
|
||||
|
||||
signal beat_tick(beat_index: int)
|
||||
signal bar_tick(bar_index: int)
|
||||
|
||||
static var user_offset_ms: float = 0.0: set = set_user_offset_ms
|
||||
|
||||
# Since only one rhythm player should play at any time, these are all static.
|
||||
static var current_song_info: SongInfo
|
||||
static var paused: bool = true
|
||||
static var bpm: float = 120.0
|
||||
static var beats_per_bar: int = 4
|
||||
|
||||
static var song_time: float = 0.0
|
||||
static var beat: float = 0.0
|
||||
static var beat_phase: float = 0.0
|
||||
static var bar: float = 0.0
|
||||
static var bar_phase: float = 0.0
|
||||
|
||||
@export var song_info: SongInfo
|
||||
@export var autoplay: bool = false
|
||||
@export var playback_position: float = 0.0
|
||||
|
||||
@export_node_path("AudioStreamPlayer", "AudioStreamPlayer2D", "AudioStreamPlayer3D")
|
||||
var audio_player: NodePath: set = set_audio_player
|
||||
|
||||
var _audio_player: Node
|
||||
var _last_beat: int = -1
|
||||
var _last_bar: int = -1
|
||||
|
||||
|
||||
static func set_user_offset_ms(offset: float) -> void:
|
||||
user_offset_ms = offset
|
||||
ShaderGlobals.set_user_offset_ms(offset)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_audio_player(audio_player)
|
||||
|
||||
if autoplay:
|
||||
play(playback_position)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_instance_valid(_audio_player):
|
||||
return
|
||||
|
||||
paused = not _audio_player.playing
|
||||
|
||||
if not _audio_player.playing:
|
||||
_last_beat = -1
|
||||
_last_bar = -1
|
||||
return
|
||||
|
||||
_update_playback()
|
||||
|
||||
|
||||
func get_song_time() -> float:
|
||||
if not is_instance_valid(_audio_player):
|
||||
return 0.0
|
||||
|
||||
var time: float = _audio_player.get_playback_position()
|
||||
time += AudioServer.get_time_since_last_mix()
|
||||
time -= AudioServer.get_output_latency()
|
||||
return time
|
||||
|
||||
|
||||
func get_song_time_with_user_offset() -> float:
|
||||
return get_song_time() + user_offset_ms / 1000.0
|
||||
|
||||
|
||||
func get_beat() -> float:
|
||||
return song_info.tempo_map.get_beat_at_time(get_song_time_with_user_offset() + song_info.phase_shift)
|
||||
|
||||
|
||||
func get_bar_phase() -> float:
|
||||
return bar - floorf(bar)
|
||||
|
||||
|
||||
func get_bar() -> float:
|
||||
return beat / beats_per_bar
|
||||
|
||||
|
||||
## 0.0 -> beat start
|
||||
## 0.5 -> halfway
|
||||
## 0.99 -> almost next beat
|
||||
func get_beat_phase() -> float:
|
||||
return beat - floorf(beat)
|
||||
|
||||
|
||||
func beat_to_signature() -> int:
|
||||
return posmod(floori(beat), beats_per_bar) + 1
|
||||
|
||||
|
||||
static func bar_to_signature(bar_index: int, _bar_signature: int) -> int:
|
||||
return bar_index
|
||||
#return posmod(bar_index, bar_signature) + 1
|
||||
|
||||
|
||||
func set_audio_player(audio_player_path: NodePath) -> void:
|
||||
audio_player = audio_player_path
|
||||
_audio_player = get_node_or_null(audio_player)
|
||||
|
||||
if not is_instance_valid(_audio_player):
|
||||
set_process(false)
|
||||
return
|
||||
|
||||
if (
|
||||
not _audio_player.has_method(&"play")
|
||||
or not _audio_player.has_method(&"get_playback_position")
|
||||
or not &"playing" in _audio_player
|
||||
):
|
||||
_audio_player = null
|
||||
push_error("audio_player is not a valid player.")
|
||||
return
|
||||
|
||||
#set_process(_audio_player.playing)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func play(position: float = 0.0) -> void:
|
||||
current_song_info = song_info
|
||||
_audio_player.stream = song_info.audio_stream
|
||||
ShaderGlobals.set_total_song_time(song_info.audio_stream.get_length())
|
||||
|
||||
bpm = song_info.bpm
|
||||
beats_per_bar = song_info.beats_per_bar
|
||||
|
||||
song_info.create_tempo_map()
|
||||
_audio_player.play(position)
|
||||
|
||||
|
||||
func stop() -> void:
|
||||
_audio_player.stop()
|
||||
|
||||
|
||||
func _update_playback() -> void:
|
||||
beat = get_beat()
|
||||
beat_phase = get_beat_phase()
|
||||
|
||||
bar = get_bar()
|
||||
bar_phase = get_bar_phase()
|
||||
|
||||
song_time = get_song_time_with_user_offset() + song_info.phase_shift
|
||||
|
||||
ShaderGlobals.set_song_time(song_time)
|
||||
|
||||
var tempo_section: TempoMap.TempoSection = song_info.tempo_map.get_tempo_section_at_time(song_time)
|
||||
bpm = tempo_section.bpm
|
||||
beats_per_bar = tempo_section.beats_per_bar
|
||||
|
||||
ShaderGlobals.set_beat(beat)
|
||||
ShaderGlobals.set_bar(bar)
|
||||
|
||||
var beat_index: int = floori(beat)
|
||||
var bar_index: int = floori(bar)
|
||||
SPrint.print_msgf("Beat: %s (%s) on beat: %s" % [beat_index, beat_to_signature(), beat_phase <= (1.0 / beats_per_bar)])
|
||||
SPrint.print_msgf("Bar: %s (%s)" % [bar_to_signature(bar_index, beats_per_bar), str(bar_phase).pad_decimals(4)])
|
||||
#SPrint.print_msgf("Bar: %s (%s) bar on beat: %s" % [bar_to_signature(bar_index, _bars), bar_index, bar_index % _bars == 0])
|
||||
|
||||
if _last_beat != beat_index:
|
||||
beat_tick.emit(beat_index)
|
||||
_last_beat = beat_index
|
||||
|
||||
if _last_bar != bar_index:
|
||||
bar_tick.emit(bar_index)
|
||||
_last_bar = bar_index
|
||||
1
game/src/core/rhythm/rhythm_player.gd.uid
Normal file
1
game/src/core/rhythm/rhythm_player.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bdi06itcm6wfp
|
||||
59
game/src/core/rhythm/rhythm_property_setter.gd
Normal file
59
game/src/core/rhythm/rhythm_property_setter.gd
Normal file
@ -0,0 +1,59 @@
|
||||
class_name RhythmPropertySetter
|
||||
extends Node
|
||||
|
||||
|
||||
@export var nodes: Array[Node] = []
|
||||
@export var beat_nodes: Array[Node] = []
|
||||
@export var bar_nodes: Array[Node] = []
|
||||
@export var beat_property: StringName = &""
|
||||
@export var bar_property: StringName = &""
|
||||
|
||||
@export var phase_shift: float = 0.0
|
||||
@export var phase_multiplier: float = 1.0
|
||||
|
||||
@export_group("Beat Value", "beat_")
|
||||
@export var beat_range_min: float = 0.0
|
||||
@export var beat_range_max: float = 1.0
|
||||
@export_exp_easing("attenuation") var beat_exponent: float = 1.0
|
||||
|
||||
@export_group("Bar Value", "bar_")
|
||||
@export var bar_range_min: float = 0.0
|
||||
@export var bar_range_max: float = 1.0
|
||||
@export_exp_easing("attenuation") var bar_exponent: float = 1.0
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if RhythmPlayer.paused:
|
||||
return
|
||||
|
||||
if not beat_property.is_empty():
|
||||
_update_beat_property()
|
||||
|
||||
if not bar_property.is_empty():
|
||||
_update_bar_property()
|
||||
|
||||
|
||||
func _update_beat_property() -> void:
|
||||
var beat: float = RhythmPlayer.beat * phase_multiplier + phase_shift
|
||||
var beat_phase: float = beat - floorf(beat)
|
||||
var beat_value: float = remap(beat_phase, 0.0, 1.0, beat_range_min, beat_range_max)
|
||||
beat_value *= ease(1.0 - beat_phase, beat_exponent)
|
||||
|
||||
var _nodes: Array[Node] = beat_nodes.duplicate()
|
||||
_nodes.append_array(nodes)
|
||||
|
||||
for node: Node in _nodes:
|
||||
node.set_indexed(NodePath(beat_property), beat_value)
|
||||
|
||||
|
||||
func _update_bar_property() -> void:
|
||||
var bar: float = RhythmPlayer.bar * phase_multiplier + phase_shift
|
||||
var bar_phase: float = bar - floorf(bar)
|
||||
var bar_value: float = remap(bar_phase, 0.0, 1.0, bar_range_min, bar_range_max)
|
||||
bar_value *= ease(1.0 - bar_phase, bar_exponent)
|
||||
|
||||
var _nodes: Array[Node] = bar_nodes.duplicate()
|
||||
_nodes.append_array(nodes)
|
||||
|
||||
for node: Node in _nodes:
|
||||
node.set_indexed(NodePath(bar_property), bar_value)
|
||||
1
game/src/core/rhythm/rhythm_property_setter.gd.uid
Normal file
1
game/src/core/rhythm/rhythm_property_setter.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bvmfdeypbeqsn
|
||||
41
game/src/core/rhythm/song_info.gd
Normal file
41
game/src/core/rhythm/song_info.gd
Normal file
@ -0,0 +1,41 @@
|
||||
class_name SongInfo
|
||||
extends Resource
|
||||
|
||||
|
||||
#@export_file("*.tres", "*.res", "*.ogg", "*.wav", "*.mp3")
|
||||
#var audio_path: String
|
||||
@export var audio_stream: AudioStream
|
||||
@export var bpm: float = 120
|
||||
@export_range(0, 8, 1, "or_greater") var beats_per_bar: int = 4
|
||||
@export_range(0.0, 1.0, 0.001, "or_greater", "or_less") var phase_shift: float = 0.0
|
||||
#@export_enum("1/2", "1/4", "1/8", "1/16", "1/32", "1/64") var time_signature: int = 1
|
||||
|
||||
@export_group("Tempo Sections")
|
||||
@export var tempo_infos: Array[TempoSectionInfo] = []
|
||||
@export var tempo_sections_in_seconds: Dictionary[float, float]
|
||||
@export_subgroup("Tempo Sections Simple")
|
||||
@export var tempo_section_round_to_bar: bool = true
|
||||
@export var tempo_section_round_to_beat: bool = false
|
||||
|
||||
var tempo_map: TempoMap
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
create_tempo_map()
|
||||
|
||||
|
||||
func create_tempo_map() -> void:
|
||||
if tempo_infos.is_empty():
|
||||
tempo_map = TempoMap.create_from_time_dictionary(
|
||||
tempo_sections_in_seconds,
|
||||
bpm,
|
||||
tempo_section_round_to_bar,
|
||||
tempo_section_round_to_beat,
|
||||
beats_per_bar
|
||||
)
|
||||
else:
|
||||
tempo_map = TempoMap.create_from_section_infos(tempo_infos, bpm, beats_per_bar)
|
||||
|
||||
|
||||
#func get_time_signature() -> int:
|
||||
#return int(pow(2, time_signature + 1))
|
||||
1
game/src/core/rhythm/song_info.gd.uid
Normal file
1
game/src/core/rhythm/song_info.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c5mqtmsvgt4e8
|
||||
194
game/src/core/rhythm/tempo_map.gd
Normal file
194
game/src/core/rhythm/tempo_map.gd
Normal file
@ -0,0 +1,194 @@
|
||||
class_name TempoMap
|
||||
extends Resource
|
||||
|
||||
@export var time_sections: Dictionary[float, float] = {}
|
||||
@export var section_infos: Array[TempoSectionInfo] = []
|
||||
|
||||
var sections: Array[TempoSection] = []
|
||||
|
||||
|
||||
static func create_from_beat_dictionary(
|
||||
dictionary: Dictionary[float, float],
|
||||
fallback_bpm: float
|
||||
) -> TempoMap:
|
||||
var map := TempoMap.new()
|
||||
|
||||
if dictionary.is_empty():
|
||||
map.add_section(0.0, fallback_bpm)
|
||||
return map
|
||||
|
||||
var keys: Array[float] = dictionary.keys()
|
||||
keys.sort()
|
||||
|
||||
for key: float in keys:
|
||||
map.add_section(key, dictionary.get(key))
|
||||
|
||||
map.recalculate_times()
|
||||
return map
|
||||
|
||||
|
||||
static func create_from_time_dictionary(
|
||||
dictionary: Dictionary[float, float],
|
||||
fallback_bpm: float,
|
||||
round_to_bars: bool = true,
|
||||
round_to_whole_beats: bool = false,
|
||||
beats_per_bar: int = 4
|
||||
) -> TempoMap:
|
||||
var map := TempoMap.new()
|
||||
|
||||
if dictionary.is_empty():
|
||||
map.add_section(0.0, fallback_bpm, beats_per_bar)
|
||||
return map
|
||||
|
||||
var times: Array[float] = dictionary.keys()
|
||||
times.sort()
|
||||
|
||||
var current_beat: float = 0.0
|
||||
var previous_time: float = 0.0
|
||||
var previous_bpm: float = fallback_bpm
|
||||
|
||||
map.add_section(0.0, fallback_bpm)
|
||||
|
||||
for time: float in times:
|
||||
var delta: float = time - previous_time
|
||||
current_beat += delta * previous_bpm / 60.0
|
||||
|
||||
var bpm: float = dictionary[time]
|
||||
|
||||
if round_to_bars:
|
||||
current_beat = roundf(current_beat / beats_per_bar) * beats_per_bar
|
||||
if round_to_whole_beats:
|
||||
current_beat = roundf(current_beat)
|
||||
|
||||
map.add_section(current_beat, bpm)
|
||||
|
||||
previous_time = time
|
||||
previous_bpm = bpm
|
||||
|
||||
map.recalculate_times()
|
||||
return map
|
||||
|
||||
|
||||
static func create_from_section_infos(infos: Array[TempoSectionInfo], fallback_bpm: float, beats_per_bar_fallback: int) -> TempoMap:
|
||||
var map := TempoMap.new()
|
||||
|
||||
if infos.is_empty():
|
||||
map.add_section(0.0, fallback_bpm, beats_per_bar_fallback)
|
||||
return map
|
||||
|
||||
var times: Dictionary[float, TempoSectionInfo] = {}
|
||||
|
||||
for info: TempoSectionInfo in infos:
|
||||
times.set(info.time, info)
|
||||
|
||||
times.sort()
|
||||
|
||||
var current_beat: float = 0.0
|
||||
var previous_time: float = 0.0
|
||||
var previous_bpm: float = fallback_bpm
|
||||
var previous_beats_per_bar: int = beats_per_bar_fallback
|
||||
|
||||
map.add_section(0.0, fallback_bpm)
|
||||
|
||||
for time: float in times.keys():
|
||||
var delta: float = time - previous_time
|
||||
current_beat += delta * previous_bpm / 60.0
|
||||
|
||||
var info: TempoSectionInfo = times[time]
|
||||
var bpm: float = info.bpm
|
||||
var beats_per_bar: int = info.beats_per_bar
|
||||
|
||||
if info.snap_to_previous_beat:
|
||||
current_beat = roundf(current_beat)
|
||||
if info.snap_to_previous_bar:
|
||||
current_beat = roundf(current_beat / previous_beats_per_bar) * previous_beats_per_bar
|
||||
|
||||
map.add_section(current_beat, bpm, beats_per_bar)
|
||||
|
||||
previous_time = time
|
||||
previous_bpm = bpm
|
||||
previous_beats_per_bar = beats_per_bar
|
||||
|
||||
map.recalculate_times()
|
||||
return map
|
||||
|
||||
|
||||
func add_section(beat: float, bpm: float, beats_per_bar: int = 4) -> void:
|
||||
if not sections.is_empty() and is_equal_approx(sections.back().beat, beat):
|
||||
sections.back().bpm = bpm
|
||||
sections.back().beats_per_bar = beats_per_bar
|
||||
else:
|
||||
sections.append(TempoSection.new(bpm, beat, beats_per_bar))
|
||||
|
||||
|
||||
func recalculate_times() -> void:
|
||||
if sections.is_empty():
|
||||
return
|
||||
|
||||
sections.front().time = 0.0
|
||||
print("[TempoMap] Calculating tempo changes:")
|
||||
|
||||
for index: int in range(1, sections.size()):
|
||||
var previous: TempoSection = sections[index - 1]
|
||||
var current: TempoSection = sections[index]
|
||||
|
||||
var beat_delta: float = current.beat - previous.beat
|
||||
var seconds: float = beat_delta * 60.0 / previous.bpm
|
||||
|
||||
current.time = previous.time + seconds
|
||||
print_rich("- [b]%s[/b] [i]bpm[/i] ([b]1/%s[/b]) at [b]%s[/b] [i]seconds[/i] (beat delta: [b]%s[/b], seconds delta: [b]%s[/b])." % [current.bpm, current.beats_per_bar, current.time, beat_delta, seconds])
|
||||
|
||||
|
||||
func get_beat_at_time(time: float) -> float:
|
||||
if sections.is_empty():
|
||||
return 0.0
|
||||
|
||||
# O(log n) (Binary search)
|
||||
var low: int = 0
|
||||
var high: int = sections.size() - 1
|
||||
|
||||
while low <= high:
|
||||
@warning_ignore("integer_division")
|
||||
var mid: int = (low + high) / 2
|
||||
|
||||
if sections[mid].time <= time:
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
var section: TempoSection = sections[maxi(high, 0)]
|
||||
SPrint.print_msgf("Section: %s" % section.bpm)
|
||||
return section.beat + (time - section.time) * section.bpm / 60.0
|
||||
|
||||
|
||||
func get_tempo_section_at_time(time: float) -> TempoSection:
|
||||
if sections.is_empty():
|
||||
return null
|
||||
|
||||
# O(log n) (Binary search)
|
||||
var low: int = 0
|
||||
var high: int = sections.size() - 1
|
||||
|
||||
while low <= high:
|
||||
@warning_ignore("integer_division")
|
||||
var mid: int = (low + high) / 2
|
||||
|
||||
if sections[mid].time <= time:
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
var section: TempoSection = sections[maxi(high, 0)]
|
||||
return section
|
||||
|
||||
|
||||
class TempoSection extends Resource:
|
||||
var beat: float
|
||||
var beats_per_bar: int
|
||||
var bpm: float
|
||||
var time: float
|
||||
|
||||
func _init(_bpm: float, _beat: float, _beats_per_bar: int = 4) -> void:
|
||||
bpm = _bpm
|
||||
beat = _beat
|
||||
beats_per_bar = _beats_per_bar
|
||||
1
game/src/core/rhythm/tempo_map.gd.uid
Normal file
1
game/src/core/rhythm/tempo_map.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b7vjbaiu1nst
|
||||
9
game/src/core/rhythm/tempo_section_info.gd
Normal file
9
game/src/core/rhythm/tempo_section_info.gd
Normal file
@ -0,0 +1,9 @@
|
||||
class_name TempoSectionInfo
|
||||
extends Resource
|
||||
|
||||
@export var time: float = 0.0
|
||||
@export var bpm: float = 120.0
|
||||
@export var beats_per_bar: int = 4
|
||||
|
||||
@export var snap_to_previous_beat: bool = true
|
||||
@export var snap_to_previous_bar: bool = false
|
||||
1
game/src/core/rhythm/tempo_section_info.gd.uid
Normal file
1
game/src/core/rhythm/tempo_section_info.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cx1nws4t8hs2u
|
||||
65
game/src/gameplay/objects/stagelight/stagelight.gd
Normal file
65
game/src/gameplay/objects/stagelight/stagelight.gd
Normal file
@ -0,0 +1,65 @@
|
||||
@tool
|
||||
class_name Stagelight
|
||||
extends Node3D
|
||||
|
||||
@export var light_color := Color.WHITE: set = set_light_color
|
||||
@export var lightbeam_material: ShaderMaterial: set = set_lightbeam_material
|
||||
@export var light_surface_material: ShaderMaterial: set = set_light_surface_material
|
||||
@export var phase_multiplier: float = 0.5: set = set_phase_multiplier
|
||||
@export var phase_offset: float = 0.0: set = set_phase_offset
|
||||
|
||||
@onready var spot_light: SpotLight3D = %SpotLight
|
||||
@onready var rhythm_property_setter: RhythmPropertySetter = %RhythmPropertySetter
|
||||
@onready var lightbeam: MeshInstance3D = %Lightbeam
|
||||
@onready var cylinder: MeshInstance3D = $StudioSpotLight/Cylinder
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_light_color(light_color)
|
||||
set_lightbeam_material(lightbeam_material)
|
||||
set_light_surface_material(light_surface_material)
|
||||
|
||||
|
||||
func set_light_color(color: Color) -> void:
|
||||
light_color = color
|
||||
|
||||
if not is_node_ready():
|
||||
await ready
|
||||
|
||||
spot_light.light_color = light_color
|
||||
|
||||
|
||||
func set_lightbeam_material(shader_material: ShaderMaterial) -> void:
|
||||
lightbeam_material = shader_material
|
||||
|
||||
if not is_node_ready():
|
||||
await ready
|
||||
|
||||
lightbeam.material_override = lightbeam_material
|
||||
|
||||
|
||||
func set_light_surface_material(shader_material: ShaderMaterial) -> void:
|
||||
light_surface_material = shader_material
|
||||
|
||||
if not is_node_ready():
|
||||
await ready
|
||||
|
||||
cylinder.set_surface_override_material(1, light_surface_material)
|
||||
|
||||
|
||||
func set_phase_multiplier(multiplier: float) -> void:
|
||||
phase_multiplier = multiplier
|
||||
|
||||
if not is_node_ready():
|
||||
await ready
|
||||
|
||||
rhythm_property_setter.phase_multiplier = phase_multiplier
|
||||
|
||||
|
||||
func set_phase_offset(offset: float) -> void:
|
||||
phase_offset = offset
|
||||
|
||||
if not is_node_ready():
|
||||
await ready
|
||||
|
||||
rhythm_property_setter.phase_shift = phase_offset
|
||||
1
game/src/gameplay/objects/stagelight/stagelight.gd.uid
Normal file
1
game/src/gameplay/objects/stagelight/stagelight.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cmh1y78o47f06
|
||||
98
game/src/gameplay/objects/stagelight/stagelight.tscn
Normal file
98
game/src/gameplay/objects/stagelight/stagelight.tscn
Normal file
@ -0,0 +1,98 @@
|
||||
[gd_scene format=3 uid="uid://bc84qk6pfgctg"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://pvll6bliout1" path="res://assets/models/electrics/spot_light/studio_spot_light.glb" id="1_3cilv"]
|
||||
[ext_resource type="Script" uid="uid://cmh1y78o47f06" path="res://src/gameplay/objects/stagelight/stagelight.gd" id="1_wce40"]
|
||||
[ext_resource type="Shader" uid="uid://b46o5a45g58xb" path="res://assets/shaders/rhythm/beating_surface.gdshader" id="2_wce40"]
|
||||
[ext_resource type="PackedScene" uid="uid://bwejp67rw5erj" path="res://assets/models/electrics/spot_light/studio_spot_light_framing_01.glb" id="3_otype"]
|
||||
[ext_resource type="Shader" uid="uid://c32arbbdbo7m8" path="res://assets/shaders/rhythm/beating_lightbeam.gdshader" id="4_d6ag8"]
|
||||
[ext_resource type="Script" uid="uid://bvmfdeypbeqsn" path="res://src/core/rhythm/rhythm_property_setter.gd" id="5_uce1x"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_wj31t"]
|
||||
interpolation_mode = 2
|
||||
offsets = PackedFloat32Array(0, 0.38108107, 0.5054054, 0.7162162)
|
||||
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0.44602686, 1, 1, 1, 0.08750595, 1, 1, 1, 0)
|
||||
|
||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_du0pf"]
|
||||
gradient = SubResource("Gradient_wj31t")
|
||||
width = 1
|
||||
fill_to = Vector2(0, 0.48931623)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_xn83m"]
|
||||
render_priority = 0
|
||||
shader = ExtResource("4_d6ag8")
|
||||
shader_parameter/phase_offset = 0.0
|
||||
shader_parameter/phase_multiplier = 0.5
|
||||
shader_parameter/color = Color(1, 1, 1, 1)
|
||||
shader_parameter/alpha_multiplier = 0.25
|
||||
shader_parameter/albedo = SubResource("GradientTexture2D_du0pf")
|
||||
shader_parameter/flash_exponent = 4.0
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_vtuqv"]
|
||||
interpolation_mode = 1
|
||||
offsets = PackedFloat32Array(1)
|
||||
colors = PackedColorArray(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_glp4g"]
|
||||
gradient = SubResource("Gradient_vtuqv")
|
||||
width = 1
|
||||
height = 1
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_nqu5b"]
|
||||
render_priority = 0
|
||||
shader = ExtResource("2_wce40")
|
||||
shader_parameter/phase_offset = 0.0
|
||||
shader_parameter/phase_multiplier = 0.5
|
||||
shader_parameter/albedo = SubResource("GradientTexture2D_glp4g")
|
||||
shader_parameter/flash_exponent = 4.0
|
||||
shader_parameter/intensity_multiplier = 8.0
|
||||
|
||||
[sub_resource type="CylinderMesh" id="CylinderMesh_787dp"]
|
||||
top_radius = 0.275
|
||||
bottom_radius = 2.0
|
||||
height = 12.0
|
||||
cap_top = false
|
||||
cap_bottom = false
|
||||
|
||||
[node name="Stagelight" type="Node3D" unique_id=729680764]
|
||||
script = ExtResource("1_wce40")
|
||||
lightbeam_material = SubResource("ShaderMaterial_xn83m")
|
||||
light_surface_material = SubResource("ShaderMaterial_nqu5b")
|
||||
|
||||
[node name="StudioSpotLight" parent="." unique_id=1407009402 instance=ExtResource("1_3cilv")]
|
||||
|
||||
[node name="Cylinder" parent="StudioSpotLight" index="0" unique_id=1873656815]
|
||||
surface_material_override/1 = SubResource("ShaderMaterial_nqu5b")
|
||||
|
||||
[node name="StudioSpotLightFraming01" parent="." unique_id=166030914 instance=ExtResource("3_otype")]
|
||||
|
||||
[node name="LightbeamRoot" type="Node3D" parent="." unique_id=1922157293]
|
||||
transform = Transform3D(-4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0, 1, 0.2, 8.742278e-09, 0)
|
||||
|
||||
[node name="Lightbeam" type="MeshInstance3D" parent="LightbeamRoot" unique_id=1840014799]
|
||||
unique_name_in_owner = true
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.49999994, 0, 0, 0, 0.5, 0, -2.9999995, 0)
|
||||
material_override = SubResource("ShaderMaterial_xn83m")
|
||||
cast_shadow = 0
|
||||
instance_shader_parameters/instance_phase_offset = 0.0
|
||||
mesh = SubResource("CylinderMesh_787dp")
|
||||
|
||||
[node name="SpotLight" type="SpotLight3D" parent="LightbeamRoot" unique_id=1841045966]
|
||||
unique_name_in_owner = true
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 0.8, 0)
|
||||
spot_range = 50.0
|
||||
spot_attenuation = 1.38
|
||||
spot_angle = 8.75
|
||||
spot_angle_attenuation = 0.23325838
|
||||
|
||||
[node name="RhythmPropertySetter" type="Node" parent="LightbeamRoot" unique_id=1355223878 node_paths=PackedStringArray("nodes")]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("5_uce1x")
|
||||
nodes = [NodePath("../SpotLight")]
|
||||
beat_property = &"light_energy"
|
||||
phase_multiplier = 0.5
|
||||
beat_range_min = 10.0
|
||||
beat_range_max = 0.0
|
||||
beat_exponent = 4.0
|
||||
metadata/_custom_type_script = "uid://bvmfdeypbeqsn"
|
||||
|
||||
[editable path="StudioSpotLight"]
|
||||
Loading…
Reference in New Issue
Block a user