Added multiline (wide-angle) spray type

This commit is contained in:
Rob Kelly 2025-03-02 12:00:42 -07:00
parent 2cf7010c92
commit 22984897e2
13 changed files with 279 additions and 51 deletions

BIN
asset_dev/ui/reticle.xcf Normal file

Binary file not shown.

BIN
assets/ui/hud/reticle.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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

View File

@ -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

12
src/equipment/spray.gd Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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

View File

@ -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()