diff --git a/asset_dev/ui/reticle.xcf b/asset_dev/ui/reticle.xcf new file mode 100644 index 0000000..bacba50 Binary files /dev/null and b/asset_dev/ui/reticle.xcf differ diff --git a/assets/ui/hud/reticle.png b/assets/ui/hud/reticle.png new file mode 100644 index 0000000..da2ef1a --- /dev/null +++ b/assets/ui/hud/reticle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f28a2abd94ffaf335d05fd2b3c9b5def8d9ebfab3eb051e02bf8fbda3f79740 +size 690 diff --git a/assets/ui/hud/reticle.png.import b/assets/ui/hud/reticle.png.import new file mode 100644 index 0000000..2a5b5f3 --- /dev/null +++ b/assets/ui/hud/reticle.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdnurvg7xe6k7" +path.s3tc="res://.godot/imported/reticle.png-0a9f831dae57a5cb1ef00786d2ed0e86.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://assets/ui/hud/reticle.png" +dest_files=["res://.godot/imported/reticle.png-0a9f831dae57a5cb1ef00786d2ed0e86.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/levels/sandbox/sandbox.tscn b/levels/sandbox/sandbox.tscn index 66be482..626d90e 100644 --- a/levels/sandbox/sandbox.tscn +++ b/levels/sandbox/sandbox.tscn @@ -141,7 +141,7 @@ environment = SubResource("Environment_cc548") [node name="OmniLight3D" type="OmniLight3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.16, 0) -light_energy = 32.0 +light_energy = 8.0 shadow_enabled = true omni_range = 20.0 @@ -190,7 +190,7 @@ surface_material_override/0 = SubResource("StandardMaterial3D_ygm4w") shape = SubResource("ConcavePolygonShape3D_0qjrr") [node name="Cylinder" parent="Geometry" instance=ExtResource("1_a67lu")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, -8.312) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.33738, 6, -7.18125) mask_dim = 4096 [node name="MeshInstance3D" type="MeshInstance3D" parent="Geometry/Cylinder"] diff --git a/project.godot b/project.godot index 5f43ccf..587ca74 100644 --- a/project.godot +++ b/project.godot @@ -90,3 +90,8 @@ fire={ "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) ] } + +[layer_names] + +3d_render/layer_1="World" +3d_render/layer_2="HUD" diff --git a/src/equipment/point_spray/point_spray.gd b/src/equipment/point_spray/point_spray.gd new file mode 100644 index 0000000..3b31346 --- /dev/null +++ b/src/equipment/point_spray/point_spray.gd @@ -0,0 +1,24 @@ +class_name PointSpray extends Spray +## Simple single-point spraygun + +@export var spray_scale := 16.0 + +@onready var raycast: RayCast3D = %RayCast3D +@onready var spray_effect: MeshInstance3D = %SprayEffect + + +func fire() -> 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()