From 22984897e2afd33094c9e748e92dc78f5dfc82b3 Mon Sep 17 00:00:00 2001 From: Rob Kelly Date: Sun, 2 Mar 2025 12:00:42 -0700 Subject: [PATCH] Added multiline (wide-angle) spray type --- asset_dev/ui/reticle.xcf | Bin 0 -> 1318 bytes assets/ui/hud/reticle.png | 3 + assets/ui/hud/reticle.png.import | 35 +++++++++++ levels/sandbox/sandbox.tscn | 4 +- project.godot | 5 ++ src/equipment/point_spray/point_spray.gd | 24 ++++++++ src/equipment/point_spray/point_spray.tscn | 35 +++++++++++ src/equipment/spray.gd | 12 ++++ src/equipment/wide_spray/wide_spray.gd | 39 ++++++++++++ src/equipment/wide_spray/wide_spray.tscn | 56 +++++++++++++++++ src/player/player.gd | 25 ++------ src/player/player.tscn | 24 ++------ src/world/gunk_body/gunk_body.gd | 68 ++++++++++++++++++--- 13 files changed, 279 insertions(+), 51 deletions(-) create mode 100644 asset_dev/ui/reticle.xcf create mode 100644 assets/ui/hud/reticle.png create mode 100644 assets/ui/hud/reticle.png.import create mode 100644 src/equipment/point_spray/point_spray.gd create mode 100644 src/equipment/point_spray/point_spray.tscn create mode 100644 src/equipment/spray.gd create mode 100644 src/equipment/wide_spray/wide_spray.gd create mode 100644 src/equipment/wide_spray/wide_spray.tscn diff --git a/asset_dev/ui/reticle.xcf b/asset_dev/ui/reticle.xcf new file mode 100644 index 0000000000000000000000000000000000000000..bacba50754520307e85803c870e7875dbf01dfa5 GIT binary patch literal 1318 zcmaKqO^(w*5QW=@u``nyg?|JgQiDOtEFvWmOG_+ffmk3R-yq{Sj-~u5F)*{^0IWFy zD~{zGaDi#Q@{iPskjr}gUUgMhKU+?-)mGocbNy{RnJ7Zn5d8~01dhY<7<8`K5)eZ{Pg*YR34PA*=m|5qjZhUQhM7d?uHaum4k?=<<>B&3-$LvvsQ9 zlib=#tw&$iS+28nyh!%aznRbXNv_AzZs|%MHj85`oQ0J%QX>TNW}PU>vv`>xl`9M3 z0r{W?ycYf82P?fm_T}7Zts(Ws(yuIiW@&BduBFc{ePQVyTJCGYN66hW|6Q#i^;WbY z;K0)MPr-H*?zT~|t|4;?_h-fBs#yhu`@3R!@qXm0L1a`AJy%}zLY=gttDLAy&oOtD zA9dJo__*HW?ejf61b7+XsUJDwuE1S^y8?Fwr(3}-1@6Eh void: + if raycast.is_colliding(): + var collider := raycast.get_collider() + if collider is GunkBody: + var point := raycast.get_collision_point() + var point_scale := sqrt(point.distance_to(global_position)) * spray_scale + (collider as GunkBody).paint_continuous( + point, raycast.get_collision_normal(), point_scale + ) + + spray_effect.visible = true + + +func idle() -> void: + spray_effect.visible = false diff --git a/src/equipment/point_spray/point_spray.tscn b/src/equipment/point_spray/point_spray.tscn new file mode 100644 index 0000000..2fd65ec --- /dev/null +++ b/src/equipment/point_spray/point_spray.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=5 format=3 uid="uid://cc102xko0u6yj"] + +[ext_resource type="Script" path="res://src/equipment/point_spray/point_spray.gd" id="1_2yl2v"] +[ext_resource type="Texture2D" uid="uid://bdnurvg7xe6k7" path="res://assets/ui/hud/reticle.png" id="1_f2scl"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ng43h"] +transparency = 1 +shading_mode = 0 +vertex_color_use_as_albedo = true +albedo_color = Color(0, 1, 0.301961, 0.254902) + +[sub_resource type="PrismMesh" id="PrismMesh_ow0jh"] +material = SubResource("StandardMaterial3D_ng43h") +size = Vector3(0.2, 2, 0.2) + +[node name="PointSpray" type="Node3D"] +script = ExtResource("1_2yl2v") + +[node name="RayCast3D" type="RayCast3D" parent="."] +unique_name_in_owner = true +target_position = Vector3(0, 0, -2) + +[node name="SprayEffect" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, -4.47035e-08, -1, 0, 1, -4.47035e-08, 0, 0, -1) +visible = false +layers = 2 +mesh = SubResource("PrismMesh_ow0jh") +skeleton = NodePath("../../../..") + +[node name="Decal" type="Decal" parent="."] +transform = Transform3D(1, 0, 0, 0, -4.47035e-08, -1, 0, 1, -4.47035e-08, 0, 0, -1) +size = Vector3(0.2, 2, 0.2) +texture_albedo = ExtResource("1_f2scl") +cull_mask = 1048573 diff --git a/src/equipment/spray.gd b/src/equipment/spray.gd new file mode 100644 index 0000000..4eca758 --- /dev/null +++ b/src/equipment/spray.gd @@ -0,0 +1,12 @@ +class_name Spray extends Node3D +## Abstract base class for spraygun types + + +## Called each frame that this spray is being fired. +func fire() -> void: + pass + + +## Called each frame that this spray is not being fired. +func idle() -> void: + pass diff --git a/src/equipment/wide_spray/wide_spray.gd b/src/equipment/wide_spray/wide_spray.gd new file mode 100644 index 0000000..328b4b8 --- /dev/null +++ b/src/equipment/wide_spray/wide_spray.gd @@ -0,0 +1,39 @@ +class_name WideSpray extends Spray +## Wide spray pattern + +const SPRAYCAST_GROUP := "SprayCast" + +@export var spray_scale := 16.0 + +@onready var spray_casts: Node3D = %SprayCasts +@onready var spray_effect: MeshInstance3D = %SprayEffect + + +func fire() -> void: + var prev_target: GunkBody = null + var prev_point: Vector3 + var prev_normal: Vector3 + + for raycast: RayCast3D in spray_casts.get_children(): + if raycast.is_colliding(): + var target := raycast.get_collider() as GunkBody + if target: + var point := raycast.get_collision_point() + var normal := raycast.get_collision_normal() + + # Always paint at least a dot, to cap the ends of the line + target.paint_dot(point, normal, spray_scale) + if target == prev_target: + # Continue the multiline if possible + target.add_to_multiline(prev_point, prev_normal, point, normal, spray_scale * 2) + prev_point = point + prev_normal = normal + prev_target = target + prev_point = point + prev_normal = normal + + spray_effect.visible = true + + +func idle() -> void: + spray_effect.visible = false diff --git a/src/equipment/wide_spray/wide_spray.tscn b/src/equipment/wide_spray/wide_spray.tscn new file mode 100644 index 0000000..4977ce7 --- /dev/null +++ b/src/equipment/wide_spray/wide_spray.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=5 format=3 uid="uid://d2hnxr5l6w2x4"] + +[ext_resource type="Script" path="res://src/equipment/wide_spray/wide_spray.gd" id="1_ggkto"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cdyoo"] +transparency = 1 +shading_mode = 0 +vertex_color_use_as_albedo = true +albedo_color = Color(0, 1, 0.301961, 0.254902) + +[sub_resource type="PrismMesh" id="PrismMesh_vh2mt"] +material = SubResource("StandardMaterial3D_cdyoo") +size = Vector3(1, 2, 0.2) + +[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_c3qhn"] +load_path = "res://.godot/imported/reticle.png-0a9f831dae57a5cb1ef00786d2ed0e86.s3tc.ctex" + +[node name="WideSpray" type="Node3D"] +script = ExtResource("1_ggkto") + +[node name="SprayCasts" type="Node3D" parent="."] +unique_name_in_owner = true + +[node name="RayCast3D" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(-0.5, 0, -2) + +[node name="RayCast3D2" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(-0.333, 0, -2) + +[node name="RayCast3D3" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(-0.167, 0, -2) + +[node name="RayCast3D4" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(0, 0, -2) + +[node name="RayCast3D5" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(0.167, 0, -2) + +[node name="RayCast3D6" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(0.333, 0, -2) + +[node name="RayCast3D7" type="RayCast3D" parent="SprayCasts"] +target_position = Vector3(0.5, 0, -2) + +[node name="SprayEffect" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, -4.47035e-08, -1, 0, 1, -4.47035e-08, 0, 0, -1) +layers = 2 +mesh = SubResource("PrismMesh_vh2mt") +skeleton = NodePath("../../../..") + +[node name="Decal" type="Decal" parent="."] +transform = Transform3D(1, 0, 0, 0, -4.47035e-08, -1, 0, 1, -4.47035e-08, 0, 0, -1) +size = Vector3(0.2, 2, 0.2) +texture_albedo = SubResource("CompressedTexture2D_c3qhn") +cull_mask = 1048573 diff --git a/src/player/player.gd b/src/player/player.gd index e34a854..d8c81ae 100644 --- a/src/player/player.gd +++ b/src/player/player.gd @@ -7,8 +7,6 @@ const JUMP_FORCE := 4.5 const GROUND_FRICTION := 0.3 const AIR_FRICTION := 0.03 -const SPRAY_SCALE := 16 - var gravity: Vector3 = ( ProjectSettings.get_setting("physics/3d/default_gravity") * ProjectSettings.get_setting("physics/3d/default_gravity_vector") @@ -16,9 +14,6 @@ var gravity: Vector3 = ( @onready var camera_pivot: Node3D = %CameraPivot @onready var spray_muzzle: Marker3D = %SprayMuzzle -@onready var raycast: RayCast3D = %RayCast3D - -@onready var spray_effect: MeshInstance3D = %SprayEffect func get_speed() -> float: @@ -35,27 +30,15 @@ func get_friction() -> float: return AIR_FRICTION -func _scale_spray_point(point: Vector3) -> float: - return point.distance_to(spray_muzzle.global_position) * SPRAY_SCALE - - -func fire_spray() -> void: - if raycast.is_colliding(): - var collider := raycast.get_collider() - if collider is GunkBody: - var point := raycast.get_collision_point() - var point_scale := point.distance_to(spray_muzzle.global_position) * SPRAY_SCALE - (collider as GunkBody).paint_continuous( - point, raycast.get_collision_normal(), point_scale - ) +func get_spray() -> Spray: + return spray_muzzle.get_child(0) func _physics_process(delta: float) -> void: if Input.is_action_pressed("fire"): - fire_spray() - spray_effect.visible = true + get_spray().fire() else: - spray_effect.visible = false + get_spray().idle() # Gravity if not is_on_floor(): diff --git a/src/player/player.tscn b/src/player/player.tscn index cb19c60..010b6d3 100644 --- a/src/player/player.tscn +++ b/src/player/player.tscn @@ -1,21 +1,12 @@ -[gd_scene load_steps=8 format=3 uid="uid://bwe2jdmvinhqd"] +[gd_scene load_steps=7 format=3 uid="uid://bwe2jdmvinhqd"] [ext_resource type="Script" path="res://src/player/player.gd" id="1_npueo"] [ext_resource type="Script" path="res://src/player/camera_controller.gd" id="2_veeqv"] +[ext_resource type="PackedScene" uid="uid://cc102xko0u6yj" path="res://src/equipment/point_spray/point_spray.tscn" id="3_8n2h0"] [sub_resource type="BoxMesh" id="BoxMesh_ua7a2"] size = Vector3(0.05, 0.05, 0.3) -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ng43h"] -transparency = 1 -shading_mode = 0 -vertex_color_use_as_albedo = true -albedo_color = Color(0, 1, 0.301961, 0.254902) - -[sub_resource type="PrismMesh" id="PrismMesh_ow0jh"] -material = SubResource("StandardMaterial3D_ng43h") -size = Vector3(0.2, 2, 0.2) - [sub_resource type="QuadMesh" id="QuadMesh_cn0yq"] size = Vector2(1, 0.05) @@ -32,21 +23,14 @@ script = ExtResource("2_veeqv") [node name="SprayNozzle" type="MeshInstance3D" parent="CameraPivot"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.15, -0.1, -0.1) +layers = 2 mesh = SubResource("BoxMesh_ua7a2") [node name="SprayMuzzle" type="Marker3D" parent="CameraPivot/SprayNozzle"] unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 0.997564, -0.0697565, 0, 0.0697565, 0.997564, 0, 0, -0.15) -[node name="RayCast3D" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]] -unique_name_in_owner = true -target_position = Vector3(0, 0, -2) - -[node name="SprayEffect" type="MeshInstance3D" parent="CameraPivot/SprayNozzle/SprayMuzzle"] -unique_name_in_owner = true -transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, -1) -mesh = SubResource("PrismMesh_ow0jh") -skeleton = NodePath("../../..") +[node name="PointSpray" parent="CameraPivot/SprayNozzle/SprayMuzzle" instance=ExtResource("3_8n2h0")] [node name="Camera3D" type="Camera3D" parent="CameraPivot"] current = true diff --git a/src/world/gunk_body/gunk_body.gd b/src/world/gunk_body/gunk_body.gd index d243e4c..f587966 100644 --- a/src/world/gunk_body/gunk_body.gd +++ b/src/world/gunk_body/gunk_body.gd @@ -1,7 +1,7 @@ class_name GunkBody extends StaticBody3D ## StaticBody3D with an associated "gunkable" mesh. -const CONTINUITY_LIMIT := 64 +const CONTINUITY_LIMIT := 128 const BUFFER_LIMIT := 3 const FACE_EPSILON := 0.4 const MASK_COLOR := Color.RED @@ -11,8 +11,10 @@ const MASK_COLOR := Color.RED var meshtool := MeshDataTool.new() var _polyline_buffer: Array[Vector2] = [] -var _continuous := false -var _painted_this_frame := false +var _continued_paint_this_frame := false + +var _multiline_buffer := PackedVector2Array() +var _multiline_width := 1.0 @onready var mesh_instance: MeshInstance3D = $MeshInstance3D @onready var mesh: ArrayMesh = mesh_instance.mesh @@ -28,7 +30,7 @@ func _ready() -> void: meshtool.create_from_surface(mesh, 0) -## Transform cartesian coordinates to barycentric wrt the given triangle +## Transform cartesian coordinates to barycentric wrt the given triangle. func _barycentric(p: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3: var v0 := b - a var v1 := c - a @@ -45,11 +47,15 @@ func _barycentric(p: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3: return Vector3(u, v, w) +## Is the given point on the planar triangle defined by v1, v2, and v3? func _is_in_triangle(point: Vector3, v1: Vector3, v2: Vector3, v3: Vector3) -> bool: var bc := _barycentric(point, v1, v2, v3) return (bc.x > 0 and bc.x < 1) and (bc.y > 0 and bc.y < 1) and (bc.z > 0 and bc.z < 1) +## Get the index of the mesh face on which the given point+normal lies. +## +## Returns -1 if the given point+normal does not lie on a mesh face within tolerance. func _get_face(point: Vector3, normal: Vector3) -> int: for i in range(meshtool.get_face_count()): if meshtool.get_face_normal(i).distance_squared_to(normal) > FACE_EPSILON: @@ -63,6 +69,9 @@ func _get_face(point: Vector3, normal: Vector3) -> int: return -1 +## UV-unwrap a point+normal in 3D space onto the UV space of this mesh. +## +## Returns Vector2.INF if the given point+normal does not lie on this mesh within tolerance. func _get_uv(point: Vector3, normal: Vector3) -> Vector2: var face := _get_face(point, normal) if face < 0: @@ -80,8 +89,27 @@ func _get_uv(point: Vector3, normal: Vector3) -> Vector2: return (uv1 * bc.x) + (uv2 * bc.y) + (uv3 * bc.z) +## UV-unwrap a point+normal in 3D space onto the pixel space of this mesh's texture. +## +## Returns Vector2.INF if the given point+normal does not lie on this mesh within tolerance. +func _get_px(point: Vector3, normal: Vector3) -> Vector2: + return _get_uv(point * global_transform, normal * global_basis) * mask_control.size + + +## Paint a dot on the gunk mask. +func paint_dot(point: Vector3, normal: Vector3, radius: float) -> void: + var px := _get_px(point, normal) + if px == Vector2.INF: + return + + mask_control.queue_draw( + func() -> void: mask_control.draw_circle(px, radius, MASK_COLOR, true, -1, true) + ) + + +## Paint a continuous line on the gunk mask if called on successive frames. func paint_continuous(point: Vector3, normal: Vector3, width: float) -> void: - var px := _get_uv(point * global_transform, normal * global_basis) * mask_control.size + var px := _get_px(point, normal) if _polyline_buffer and px.distance_to(_polyline_buffer[0]) <= CONTINUITY_LIMIT: _polyline_buffer.push_front(px) if len(_polyline_buffer) > BUFFER_LIMIT: @@ -95,10 +123,34 @@ func paint_continuous(point: Vector3, normal: Vector3, width: float) -> void: mask_control.queue_draw( func() -> void: mask_control.draw_circle(px, width, MASK_COLOR, true, -1, true) ) - _painted_this_frame = true + _continued_paint_this_frame = true + + +## Add a segment to the multiline to paint this frame. +func add_to_multiline( + point_a: Vector3, normal_a: Vector3, point_b: Vector3, normal_b: Vector3, width: float +) -> void: + var px_a := _get_px(point_a, normal_a) + var px_b := _get_px(point_b, normal_b) + if px_a == Vector2.INF or px_b == Vector2.INF or px_a.distance_to(px_b) > CONTINUITY_LIMIT: + return + _multiline_buffer.append(px_a) + _multiline_buffer.append(px_b) + _multiline_width = width func _process(_delta: float) -> void: - if not _painted_this_frame: + # If paint_continuous wasn't called last frame, stop the current polyline. + if not _continued_paint_this_frame: _polyline_buffer = [] - _painted_this_frame = false + _continued_paint_this_frame = false + + # If we've buffered a multiline in the last frame, draw it & clear the buffer. + # The width of the multiline will just be the width of the last call. + if _multiline_buffer: + var points := _multiline_buffer.duplicate() + var width := _multiline_width + mask_control.queue_draw( + func() -> void: mask_control.draw_multiline(points, MASK_COLOR, width, true) + ) + _multiline_buffer = PackedVector2Array()