diff --git a/lights.gd b/lights.gd index bec7596..10792b6 100644 --- a/lights.gd +++ b/lights.gd @@ -3,6 +3,7 @@ class_name Lights @export var light_count: int = 1 @export var light_radius: float = 5.0 +@export var light_slowdown_speed: float = 1.0 @export var light_template: PackedScene @export var pattern_repetition: int = 5 @@ -19,21 +20,19 @@ func _ready() -> void: light_instance.position = Vector3(x, 0, z) add_child(light_instance) -const BASE_DELTA: float = 16 var virtual_rotation: float = 0.0 const FULL_DARK_ROTATION_DELTA := .1 func _process(delta: float) -> void: - var frames_elapsed = delta / BASE_DELTA - virtual_rotation += expected_rotation_delta - var max_darkness = expected_rotation_delta / FULL_DARK_ROTATION_DELTA + virtual_rotation += expected_rotation_delta * (delta * 60) + var max_darkness := clampf(abs(expected_rotation_delta / FULL_DARK_ROTATION_DELTA), 0, 1) var i := 0 for light in get_children(): if light is SpotLight3D: - var pattern_offset := ((i + int(virtual_rotation * pattern_repetition)) % pattern_repetition) / float(pattern_repetition) + var pattern_offset := wrapf((i + (virtual_rotation * pattern_repetition)) / pattern_repetition, 0, 1) light.light_color = light_color - light.light_energy = 1 - (1 - pattern_offset) * max_darkness - i += 1 - expected_rotation_delta = lerp(expected_rotation_delta, 0.0, frames_elapsed * 5) \ No newline at end of file + light.light_energy = (1 - (1 - pattern_offset) * max_darkness) * light_intensity + i += 1 + expected_rotation_delta = lerp(expected_rotation_delta, 0.0, delta * light_slowdown_speed) \ No newline at end of file diff --git a/main.gd b/main.gd index e6c8c97..bcb39fb 100644 --- a/main.gd +++ b/main.gd @@ -2,18 +2,33 @@ extends Node3D @onready var lights: Lights = %Lights @onready var slider: HSlider = %HSlider -@onready var plane: MeshInstance3D = %Plane +@onready var networked_lights: Lights = %NetworkedLights @export var light_rotation: float = 0.0 @export var color_gradient: Gradient +var peer: PacketPeerUDP + func _ready(): - slider.value_changed.connect(set_light_rotation) + slider.value_changed.connect(set_local_light_rotation) + peer = PacketPeerUDP.new() + peer.bind(4433) + peer.set_dest_address("rpi", 4444) + + +func send_data(data: Dictionary) -> void: + var json_data := JSON.stringify(data) + var byte_array := json_data.to_utf8_buffer() + peer.put_packet(byte_array) var error_correction := 0. -func set_light_rotation(value: float) -> void: +func set_local_light_rotation(value: float) -> void: value = wrapf(value, 0, 1) + var polarized_value := pingpong(value * 4, 1) + lights.light_intensity = 1.0 - polarized_value + lights.light_color = color_gradient.sample(value) + slider.value = value # get shortest diff including wrapping var diff = light_rotation - value @@ -25,12 +40,26 @@ func set_light_rotation(value: float) -> void: diff = wrapped_diff # ignore small movements until a bigger one occurs or until they accumulate enough, to avoid reseting the light movement - # diff += error_correction - # if diff < .05: - # error_correction = diff - # return + diff += error_correction + if abs(diff) < .05: + error_correction = diff + send_data({"value": value}) + return lights.expected_rotation_delta = diff light_rotation = value - lights.light_color = color_gradient.sample(value) - slider.value = value + send_data({"value": value, "delta": diff}) + +func _process(_delta): + if peer.get_available_packet_count() > 0: + var array_bytes = peer.get_packet() + var packet_string = array_bytes.get_string_from_ascii() + var json = JSON.parse_string(packet_string) + if json != null: + var value = json.value + var delta = json.delta + networked_lights.expected_rotation_delta = delta + + var polarized_value := pingpong(value * 4, 1) + networked_lights.light_intensity = 1.0 - polarized_value + networked_lights.light_color = color_gradient.sample(value) \ No newline at end of file diff --git a/main.tscn b/main.tscn index ae89cd0..27951da 100644 --- a/main.tscn +++ b/main.tscn @@ -24,6 +24,7 @@ sky_material = SubResource("ProceduralSkyMaterial_7dm0k") [sub_resource type="Environment" id="Environment_0xm2m"] background_mode = 2 sky = SubResource("Sky_ig7tw") +glow_enabled = true [node name="Node3D" type="Node3D"] script = ExtResource("1_h2yge") @@ -31,16 +32,11 @@ color_gradient = SubResource("Gradient_h2yge") [node name="Plane" type="MeshInstance3D" parent="."] unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 0) mesh = SubResource("CylinderMesh_0xm2m") surface_material_override/0 = SubResource("StandardMaterial3D_7dm0k") -[node name="WorldEnvironment" type="WorldEnvironment" parent="."] -environment = SubResource("Environment_0xm2m") - -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 2, 0) - -[node name="Lights" type="Marker3D" parent="."] +[node name="Lights" type="Marker3D" parent="Plane"] unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) gizmo_extents = 0.7 @@ -49,6 +45,27 @@ light_count = 20 light_radius = 0.7 light_template = ExtResource("2_0xm2m") +[node name="NetworkedPlane" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0) +mesh = SubResource("CylinderMesh_0xm2m") +surface_material_override/0 = SubResource("StandardMaterial3D_7dm0k") + +[node name="NetworkedLights" type="Marker3D" parent="NetworkedPlane"] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +gizmo_extents = 0.7 +script = ExtResource("1_ig7tw") +light_count = 20 +light_radius = 0.7 +light_template = ExtResource("2_0xm2m") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_0xm2m") + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 2, 0) + [node name="HSlider" type="HSlider" parent="."] unique_name_in_owner = true anchors_preset = 7 diff --git a/project.godot b/project.godot index 5c597b2..90c95c6 100644 --- a/project.godot +++ b/project.godot @@ -14,3 +14,8 @@ config/name="ambientlightdemo" run/main_scene="uid://bfysabo18nycq" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" + +[rendering] + +anti_aliasing/quality/msaa_2d=2 +anti_aliasing/quality/msaa_3d=2 diff --git a/rpi/README.md b/rpi/README.md new file mode 100644 index 0000000..d61e9cf --- /dev/null +++ b/rpi/README.md @@ -0,0 +1 @@ +https://learn.adafruit.com/neopixels-on-raspberry-pi/python-usage \ No newline at end of file diff --git a/rpi/lights.py b/rpi/lights.py new file mode 100644 index 0000000..3700193 --- /dev/null +++ b/rpi/lights.py @@ -0,0 +1,85 @@ +import socket +import time +import board +import neopixel +import json + +num_pixels = 30 +local_pixels = neopixel.NeoPixel( + board.D18, num_pixels, brightness=0.2, auto_write=False, pixel_order=neopixel.GRB +) +networked_pixels = neopixel.NeoPixel( + board.D21, num_pixels, brightness=0.2, auto_write=False, pixel_order=neopixel.GRB +) + +colors = ["d062ff", "00b097", "50cc00", "8dcaff", "d062ff"] +colors_rgb = [tuple(int(colors[i][j:j+2], 16) for j in (0, 2, 4)) for i in range(len(colors))] +color_offsets = [0, .25, .5, .75, 1] + +UDP_HOST = "steamdeck" +UDP_PORT_LOCAL = 4444 +UDP_PORT_NETWORKED = 4433 + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("", UDP_PORT_LOCAL)) +sock.setblocking(0) + +FULL_DARK_ROTATION_DELTA = .1 +PATTERN_REPETITION = 5 +LIGHT_SLOWDOWN_SPEED = 1 + +def clamp(value, min_value, max_value): + return max(min_value, min(value, max_value)) +def wrap(value, min_value, max_value): + range_size = max_value - min_value + while value < min_value: + value += range_size + while value >= max_value: + value -= range_size + return value +def lerp(a, b, t): + return a + (b - a) * t +def pingpong(value, max): + value = wrap(value, 0, max * 2) + if value > max: + value = max * 2 - value + return value +def sample_color_gradient(t): + t = wrap(t, 0, 1) + for i in range(len(color_offsets) - 1): + if t >= color_offsets[i] and t <= color_offsets[i + 1]: + local_t = (t - color_offsets[i]) / (color_offsets[i + 1] - color_offsets[i]) + color_a = colors_rgb[i] + color_b = colors_rgb[i + 1] + return tuple(int(lerp(color_a[j], color_b[j], local_t)) for j in range(3)) + return colors_rgb[-1] +def send_data(value, delta): + message = json.dumps({"value": value, "delta": delta}).encode('utf-8') + sock.sendto(message, (UDP_HOST, UDP_PORT_NETWORKED)) +def shortest_diff(old, new): + diff = old - new + wrapped_diff = (new + 1) - old + if abs(wrapped_diff) < abs(diff): + diff = -wrapped_diff + wrapped_diff = (old + 1) - new + if abs(wrapped_diff) < abs(diff): + diff = wrapped_diff + return diff + +class Lights: + virtual_rotation = 0 + color = (255, 255, 255) + expected_rotation_delta = 0 + def __init__(self, pixels): + self.pixels = pixels + + def process(self, delta): + self.virtual_rotation += self.expected_rotation_delta * (delta * 60) + max_darkness = clamp(abs(self.expected_rotation_delta / FULL_DARK_ROTATION_DELTA), 0, 1) + for i in range(num_pixels): + pattern_offset = wrap((i + (self.virtual_rotation * PATTERN_REPETITION)) / PATTERN_REPETITION, 0, 1) + energy = (1 - (1 - pattern_offset) * max_darkness) + new_color = tuple(x * energy for x in self.color) + self.pixels[i] = new_color + self.pixels.show() + self.expected_rotation_delta = lerp(self.expected_rotation_delta, 0, delta * LIGHT_SLOWDOWN_SPEED) \ No newline at end of file