Initial rhythm implementation (+shaders and stagelight)

This commit is contained in:
SchimmelSpreu83 2026-03-23 20:28:11 +01:00
parent a69ed298dc
commit 71d967d988
28 changed files with 959 additions and 0 deletions

View 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.
//}

View File

@ -0,0 +1 @@
uid://dlqjg6dby6pdy

View 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.
//}

View File

@ -0,0 +1 @@
uid://c7n5s8eodlca6

View 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.
//}

View File

@ -0,0 +1 @@
uid://c32arbbdbo7m8

View 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.
//}

View File

@ -0,0 +1 @@
uid://b46o5a45g58xb

View 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)
//}
}

View File

@ -0,0 +1 @@
uid://cy7b01or0ckgk

View File

@ -22,6 +22,7 @@ SPrint="*uid://cpfunq8fyixco"
VersionDisplay="*uid://bqxtpo2c64h22" VersionDisplay="*uid://bqxtpo2c64h22"
InputManager="*uid://cocp0vmvgd4ln" InputManager="*uid://cocp0vmvgd4ln"
ControllerIcons="*uid://bdosbfkp568je" ControllerIcons="*uid://bdosbfkp568je"
ShaderGlobals="*uid://d2lr860r1ysrm"
LimboConsole="*uid://dyxornv8vwibg" LimboConsole="*uid://dyxornv8vwibg"
[debug] [debug]
@ -204,3 +205,26 @@ locale/translations=PackedStringArray("res://localization/localization.en.transl
rendering_device/driver.windows="d3d12" rendering_device/driver.windows="d3d12"
anti_aliasing/quality/msaa_3d=3 anti_aliasing/quality/msaa_3d=3
occlusion_culling/use_occlusion_culling=true 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
}

View 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)

View File

@ -0,0 +1 @@
uid://d2lr860r1ysrm

View 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)

View File

@ -0,0 +1 @@
uid://co7j2qtqpud6b

View 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

View File

@ -0,0 +1 @@
uid://bdi06itcm6wfp

View 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)

View File

@ -0,0 +1 @@
uid://bvmfdeypbeqsn

View 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))

View File

@ -0,0 +1 @@
uid://c5mqtmsvgt4e8

View 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

View File

@ -0,0 +1 @@
uid://b7vjbaiu1nst

View 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

View File

@ -0,0 +1 @@
uid://cx1nws4t8hs2u

View 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

View File

@ -0,0 +1 @@
uid://cmh1y78o47f06

View 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"]