From 5c53032c730ae95e76b2baa033922c24fa4485b1 Mon Sep 17 00:00:00 2001 From: hiina Date: Fri, 14 Feb 2025 15:38:53 -0700 Subject: [PATCH] organize targets slightly better --- scenes/proxy_collision_detector.gd | 125 ++++++++++++++++------------- scenes/target.gd | 33 ++++++++ scenes/target_highlight_test.gd | 29 ------- scenes/title_scene.tscn | 31 +++---- 4 files changed, 114 insertions(+), 104 deletions(-) create mode 100644 scenes/target.gd delete mode 100644 scenes/target_highlight_test.gd diff --git a/scenes/proxy_collision_detector.gd b/scenes/proxy_collision_detector.gd index ca132f5..e083ed4 100644 --- a/scenes/proxy_collision_detector.gd +++ b/scenes/proxy_collision_detector.gd @@ -1,7 +1,8 @@ # This script is designed to drive gameplay logic based on the number of GPUParticles3D -# that “hit” various static target areas in 3D space. A cloned particle system (rendered -# in a dedicated SubViewport on layer 20) uses a special testing shader that, for each particle, -# checks its 3D world-space position against up to eight static target bounding boxes. +# that "hit" various static target areas in 3D space. Multiple particle systems can be cloned +# and rendered in a dedicated SubViewport on layer 20. Each uses a special testing shader that, +# for each particle, checks its 3D world-space position against up to eight static target +# bounding boxes. # # The testing shader maps particles that hit a target into a designated horizontal slice of clip # space so that the SubViewport (sized 8×1 pixels) accumulates red intensity in each column. Higher @@ -9,23 +10,19 @@ # # In this version, the targets are static so their parameters (center, extents, and the inverse of # their 3×3 rotation basis) are computed once and sent to the shader. The attractors are shared -# (set up in the main scene and used by the cloned particle system) rather than cloned. +# (set up in the main scene and used by the cloned particle systems) rather than cloned. # -# Other game scripts can connect to the "target_occupancy_changed" signal, which is emitted each frame -# for every target (0–7) with the measured red intensity (occupancy). +# The script looks for nodes in the "targets" group that are instances of the Target class, +# and directly calls their set_occupancy() method with the measured red intensity. extends Node3D -# Signal emitted when the occupancy of a target changes. Sent every frame. -# occupancy is a float between 0 and 1 corresponding to how many particles are in the target. -signal target_occupancy_changed(target_index: int, occupancy: float) # Optional debug mesh to visualize the viewport texture @export var debug_mesh_path: NodePath var debug_mesh: MeshInstance3D -# Exported node paths. -@export var source_particles_path: NodePath # The original GPUParticles3D to clone for collision detection. -@export var target_collision_shapes_paths: Array[NodePath] = [] # Array of CollisionShape3D nodes (each with a BoxShape3D). +# Exported node paths for particle systems to clone +@export var source_particles_paths: Array[NodePath] = [] # Array of GPUParticles3D nodes to clone # Path to the testing shader resource. const shader = preload("res://scenes/particle_test.gdshader") @@ -34,19 +31,24 @@ const shader = preload("res://scenes/particle_test.gdshader") const MAX_TARGETS: int = 8 # References. -var clone_particles: GPUParticles3D +var clone_particles: Array[GPUParticles3D] = [] @onready var sub_viewport: SubViewport = $SubViewport -var target_collision_shapes: Array[CollisionShape3D] = [] +var targets: Array[Target] = [] func _ready(): - # Get references from the scene. - var source_particles = get_node(source_particles_path) as GPUParticles3D - for path in target_collision_shapes_paths: - var shape = get_node(path) as CollisionShape3D - target_collision_shapes.append(shape) - # Pad to MAX_TARGETS if necessary. - while target_collision_shapes.size() < MAX_TARGETS: - target_collision_shapes.append(null) + # Find all Target nodes in the "targets" group + for node in get_tree().get_nodes_in_group("targets"): + print("Found node in targets group: ", node.name, " of type ", node.get_class()) + if node is Target: + print("Adding target: ", node.name) + targets.append(node) + else: + print("Skipping non-Target node: ", node.name) + print("Found ", targets.size(), " valid targets") + # Ensure we don't exceed MAX_TARGETS + if targets.size() > MAX_TARGETS: + push_warning("More than %d targets found, extras will be ignored" % MAX_TARGETS) + targets.resize(MAX_TARGETS) # Set up debug mesh if path provided if not debug_mesh_path.is_empty(): @@ -56,56 +58,67 @@ func _ready(): debug_material.albedo_texture = sub_viewport.get_texture() debug_mesh.set_surface_override_material(0, debug_material) - # Load the testing shader and assign it to the cloned particle system’s draw pass. + # Create shared shader material var shader_material = ShaderMaterial.new() shader_material.shader = shader - - # Clone the source particles and set the shader material. - clone_particles = source_particles.duplicate() - # diable trails since they add extra meshes. - clone_particles.trail_enabled = false - # replace the draw pass with a simple quad mesh. - clone_particles.draw_pass_1 = QuadMesh.new() - # and set the special material - clone_particles.draw_pass_1.surface_set_material(0, shader_material) - # set visual layer to 20 so it doesn't draw on main camera - clone_particles.layers = 0 - clone_particles.set_layer_mask_value(20, true) - # add clone as child of SubViewport - sub_viewport.add_child(clone_particles) - # Precompute static target parameters and set them in the shader. - var centers: Array = [] # Array of Vector3 centers. - var extents: Array = [] # Array of Vector3 half-sizes. - var inv_bases: Array = [] # Array of mat3 (precomputed inverse bases). + # Precompute static target parameters and set them in the shader once + var centers: Array = [] # Array of Vector3 centers + var extents: Array = [] # Array of Vector3 half-sizes + var inv_bases: Array = [] # Array of mat3 (precomputed inverse bases) for i in range(MAX_TARGETS): - if i < target_collision_shapes.size() and target_collision_shapes[i] and (target_collision_shapes[i].shape is BoxShape3D): - var box: BoxShape3D = target_collision_shapes[i].shape as BoxShape3D - var gxf: Transform3D = target_collision_shapes[i].global_transform - print("Target %d:" % i) - print(" Box extents: ", box.extents) - print(" Global transform origin: ", gxf.origin) - print(" Global transform basis: ", gxf.basis) - centers.append(gxf.origin) - extents.append(box.extents) - inv_bases.append(gxf.basis.inverse()) # Precompute the inverse once. + if i < targets.size(): + var collision_shape = targets[i].get_node_or_null("CollisionShape3D") as CollisionShape3D + if collision_shape and collision_shape.shape is BoxShape3D: + var box: BoxShape3D = collision_shape.shape + var gxf: Transform3D = collision_shape.global_transform + print("Target %d:" % i) + print(" Box extents: ", box.extents) + print(" Global transform origin: ", gxf.origin) + print(" Global transform basis: ", gxf.basis) + centers.append(gxf.origin) + extents.append(box.extents) + inv_bases.append(gxf.basis.inverse()) # Precompute the inverse once + else: + centers.append(Vector3.ZERO) + extents.append(Vector3.ZERO) # Zero extents means this target is disabled + inv_bases.append(Basis.IDENTITY) else: centers.append(Vector3.ZERO) - extents.append(Vector3.ZERO) # Zero extents means this target is disabled. + extents.append(Vector3.ZERO) inv_bases.append(Basis.IDENTITY) shader_material.set_shader_parameter("target_center", centers) shader_material.set_shader_parameter("target_extents", extents) shader_material.set_shader_parameter("target_inv_basis", inv_bases) + # Create clones of each particle system + for path in source_particles_paths: + var source_particles = get_node(path) as GPUParticles3D + if source_particles: + # Clone the source particles + var clone = source_particles.duplicate() + # disable trails since they add extra meshes + clone.trail_enabled = false + # replace the draw pass with a simple quad mesh + clone.draw_pass_1 = QuadMesh.new() + # and set the shared material + clone.draw_pass_1.surface_set_material(0, shader_material) + # set visual layer to 20 so it doesn't draw on main camera + clone.layers = 0 + clone.set_layer_mask_value(20, true) + # add clone as child of SubViewport + sub_viewport.add_child(clone) + clone_particles.append(clone) + func _process(_delta): - # Each frame, read back the 8 pixel wide image from the SubViewport. + # Each frame, read back the 8 pixel wide image from the SubViewport var viewport_tex = sub_viewport.get_texture() if viewport_tex: var img: Image = viewport_tex.get_image() if img: - for x in range(target_collision_shapes.size()): + for x in range(targets.size()): var col: Color = img.get_pixel(x, 0) - var occupancy: float = col.r # The red channel intensity. - target_occupancy_changed.emit(x, occupancy) + var occupancy: float = col.r # The red channel intensity + targets[x].set_occupancy(occupancy) diff --git a/scenes/target.gd b/scenes/target.gd new file mode 100644 index 0000000..64f158b --- /dev/null +++ b/scenes/target.gd @@ -0,0 +1,33 @@ +# A target that can be hit by gpu particles. the ProxyCollisionDetector +# does the actual detection. Expects a MeshInstance3d child as a visual +# and an AudioStreamPlayer3d to vary the volume based on the number of +# particles hitting it. As well as a CollisionShape3d for the collision + +class_name Target +# this is kind of silly, but I'd like to use the actual CollisionShape3d +# node for the collision shape since it has an editor gizmo thing already built. +# this doesn't really interact with the real cpu physics engine though. +extends StaticBody3D + +@export var lerp_speed: float = 5.0 +@onready var mesh: MeshInstance3D = $MeshInstance3D +@onready var audio: AudioStreamPlayer3D = $AudioStreamPlayer3D + +var _target_occupancy: float = 0.0 +var _current_occupancy: float = 0.0 + +func _ready() -> void: + pass + +func _process(delta: float) -> void: + _current_occupancy = lerp(_current_occupancy, _target_occupancy, lerp_speed * delta) + + var material = mesh.get_surface_override_material(0) + if material: + material.emission_energy_multiplier = _current_occupancy * 10 + if audio: + audio.volume_db = linear_to_db(_current_occupancy) + +# called from the ProxyCollisionDetector +func set_occupancy(occupancy: float) -> void: + _target_occupancy = occupancy diff --git a/scenes/target_highlight_test.gd b/scenes/target_highlight_test.gd deleted file mode 100644 index a5151a7..0000000 --- a/scenes/target_highlight_test.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends StaticBody3D - - -@export var index: int = 0 -@export var lerp_speed: float = 5.0 -@onready var mesh: MeshInstance3D = $MeshInstance3D -@onready var audio: AudioStreamPlayer3D = $AudioStreamPlayer3D - -var _target_occupancy: float = 0.0 -var _current_occupancy: float = 0.0 - -func _ready() -> void: - pass - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - _current_occupancy = lerp(_current_occupancy, _target_occupancy, lerp_speed * delta) - - var material = mesh.get_surface_override_material(0) - if material: - material.emission_energy_multiplier = _current_occupancy * 10 - if audio: - audio.volume_db = linear_to_db(_current_occupancy) - - -func _on_proxy_collision_detector_target_occupancy_changed(target_index: int, occupancy: float) -> void: - if target_index == index: - _target_occupancy = occupancy diff --git a/scenes/title_scene.tscn b/scenes/title_scene.tscn index dc265fe..1f436e0 100644 --- a/scenes/title_scene.tscn +++ b/scenes/title_scene.tscn @@ -10,7 +10,7 @@ [ext_resource type="PackedScene" uid="uid://rsrnbs08nv1n" path="res://scenes/proxy_collision_detector.tscn" id="7_1kkxh"] [ext_resource type="Script" path="res://scenes/voxel_gi_toggle.gd" id="7_4p11s"] [ext_resource type="PackedScene" uid="uid://c20kawop2lrv" path="res://assets/Arcane Source 2.glb" id="8_h17hj"] -[ext_resource type="Script" path="res://scenes/target_highlight_test.gd" id="9_jig6v"] +[ext_resource type="Script" path="res://scenes/target.gd" id="11_7oif6"] [ext_resource type="AudioStream" uid="uid://dnwh2iqwi86ku" path="res://assets/04 bass.ogg" id="12_8dki8"] [ext_resource type="AudioStream" uid="uid://cv0f1tu5pac60" path="res://assets/02 midpiano.ogg" id="13_6bsa0"] [ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="13_ab6mb"] @@ -237,18 +237,14 @@ size = Vector3(13.1045, 5.27588, 4.47766) bake_mask = 4293918721 texture = ExtResource("6_l378m") -[node name="ProxyCollisionDetector" parent="." instance=ExtResource("7_1kkxh")] -source_particles_path = NodePath("../GPUParticles3D") -target_collision_shapes_paths = Array[NodePath]([NodePath("../Target1/CollisionShape3D1"), NodePath("../Target2/CollisionShape3D2"), NodePath("../Target3/CollisionShape3D3")]) - [node name="Arcane Source 2" parent="." instance=ExtResource("8_h17hj")] transform = Transform3D(0.837576, 0, 0.546321, 0, 1, 0, -0.546321, 0, 0.837576, -3.43117, -0.0137267, -5.21196) -[node name="Target1" type="StaticBody3D" parent="."] +[node name="Target1" type="StaticBody3D" parent="." groups=["targets"]] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.31548, 4.27292, -10.1667) -script = ExtResource("9_jig6v") +script = ExtResource("11_7oif6") -[node name="CollisionShape3D1" type="CollisionShape3D" parent="Target1"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Target1"] shape = SubResource("BoxShape3D_gofwq") [node name="MeshInstance3D" type="MeshInstance3D" parent="Target1"] @@ -262,12 +258,11 @@ stream = ExtResource("14_8b4v6") volume_db = -80.0 autoplay = true -[node name="Target2" type="StaticBody3D" parent="."] +[node name="Target2" type="StaticBody3D" parent="." groups=["targets"]] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.21807, 4.27292, -10.0553) -script = ExtResource("9_jig6v") -index = 1 +script = ExtResource("11_7oif6") -[node name="CollisionShape3D2" type="CollisionShape3D" parent="Target2"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Target2"] shape = SubResource("BoxShape3D_gofwq") [node name="MeshInstance3D" type="MeshInstance3D" parent="Target2"] @@ -281,12 +276,11 @@ stream = ExtResource("13_6bsa0") volume_db = -80.0 autoplay = true -[node name="Target3" type="StaticBody3D" parent="."] +[node name="Target3" type="StaticBody3D" parent="." groups=["targets"]] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.005826, 1.90465, -9.1875) -script = ExtResource("9_jig6v") -index = 2 +script = ExtResource("11_7oif6") -[node name="CollisionShape3D3" type="CollisionShape3D" parent="Target3"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Target3"] shape = SubResource("BoxShape3D_gofwq") [node name="MeshInstance3D" type="MeshInstance3D" parent="Target3"] @@ -351,6 +345,5 @@ process_material = SubResource("ParticleProcessMaterial_afs4e") draw_pass_1 = SubResource("QuadMesh_f40pn") script = ExtResource("19_er8ew") -[connection signal="target_occupancy_changed" from="ProxyCollisionDetector" to="Target1" method="_on_proxy_collision_detector_target_occupancy_changed"] -[connection signal="target_occupancy_changed" from="ProxyCollisionDetector" to="Target2" method="_on_proxy_collision_detector_target_occupancy_changed"] -[connection signal="target_occupancy_changed" from="ProxyCollisionDetector" to="Target3" method="_on_proxy_collision_detector_target_occupancy_changed"] +[node name="ProxyCollisionDetector" parent="." instance=ExtResource("7_1kkxh")] +source_particles_paths = Array[NodePath]([NodePath("../GPUParticles3D")])