2025-03-01 14:40:02 -07:00
|
|
|
class_name GunkBody extends StaticBody3D
|
|
|
|
## StaticBody3D with an associated "gunkable" mesh.
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
const CONTINUITY_LIMIT := 128
|
2025-03-01 19:02:13 -07:00
|
|
|
const BUFFER_LIMIT := 3
|
2025-03-05 14:46:13 -07:00
|
|
|
const FACE_EPSILON := 0.01
|
2025-03-01 14:40:02 -07:00
|
|
|
const MASK_COLOR := Color.RED
|
|
|
|
|
2025-03-02 13:16:40 -07:00
|
|
|
@export var mask_dim := 256
|
2025-03-05 14:46:13 -07:00
|
|
|
@export var mesh_instance: MeshInstance3D
|
2025-03-01 17:14:10 -07:00
|
|
|
|
2025-03-01 14:40:02 -07:00
|
|
|
var meshtool := MeshDataTool.new()
|
|
|
|
|
2025-03-01 19:02:13 -07:00
|
|
|
var _polyline_buffer: Array[Vector2] = []
|
2025-03-02 12:00:42 -07:00
|
|
|
var _continued_paint_this_frame := false
|
|
|
|
|
|
|
|
var _multiline_buffer := PackedVector2Array()
|
|
|
|
var _multiline_width := 1.0
|
2025-03-01 19:02:13 -07:00
|
|
|
|
2025-03-02 13:16:40 -07:00
|
|
|
var _clear_total := 0.0
|
|
|
|
var _clear_total_dirty := true
|
|
|
|
|
2025-03-01 14:40:02 -07:00
|
|
|
@onready var mesh: ArrayMesh = mesh_instance.mesh
|
|
|
|
@onready var gunk_mat: ShaderMaterial = mesh_instance.get_surface_override_material(0).next_pass
|
|
|
|
|
2025-03-01 16:12:18 -07:00
|
|
|
@onready var mask_viewport: SubViewport = %MaskViewport
|
|
|
|
@onready var mask_control: DrawController = %MaskControl
|
2025-03-01 14:40:02 -07:00
|
|
|
|
2025-03-05 14:46:13 -07:00
|
|
|
@onready var debug_draw: DebugDraw = $DebugDraw
|
|
|
|
|
2025-03-01 14:40:02 -07:00
|
|
|
|
|
|
|
func _ready() -> void:
|
2025-03-01 17:14:10 -07:00
|
|
|
mask_viewport.size = Vector2(mask_dim, mask_dim)
|
2025-03-01 16:12:18 -07:00
|
|
|
gunk_mat.set_shader_parameter("gunk_mask", mask_viewport.get_texture())
|
2025-03-01 14:40:02 -07:00
|
|
|
meshtool.create_from_surface(mesh, 0)
|
|
|
|
|
|
|
|
|
2025-03-02 13:16:40 -07:00
|
|
|
## Get the precise number of gunk pixels cleared from this image.
|
|
|
|
##
|
|
|
|
## This will use a cached result unless the mask has been painted since the last calculation.
|
|
|
|
## Be aware that cache misses are potentially quite expensive.
|
|
|
|
func get_clear_total() -> float:
|
|
|
|
if _clear_total_dirty:
|
|
|
|
var mask_img := mask_viewport.get_texture().get_image()
|
|
|
|
mask_img.convert(Image.FORMAT_R8)
|
|
|
|
var px_data := mask_img.get_data()
|
|
|
|
var px_sum := 0
|
|
|
|
for px in px_data:
|
|
|
|
px_sum += px
|
|
|
|
_clear_total = px_sum / 255.0
|
|
|
|
_clear_total_dirty = false
|
|
|
|
return _clear_total
|
|
|
|
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
## Transform cartesian coordinates to barycentric wrt the given triangle.
|
2025-03-01 14:40:02 -07:00
|
|
|
func _barycentric(p: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3:
|
|
|
|
var v0 := b - a
|
|
|
|
var v1 := c - a
|
|
|
|
var v2 := p - a
|
|
|
|
var d00 := v0.dot(v0)
|
|
|
|
var d01 := v0.dot(v1)
|
|
|
|
var d11 := v1.dot(v1)
|
|
|
|
var d20 := v2.dot(v0)
|
|
|
|
var d21 := v2.dot(v1)
|
|
|
|
var denom := d00 * d11 - d01 * d01
|
|
|
|
var v := (d11 * d20 - d01 * d21) / denom
|
|
|
|
var w := (d00 * d21 - d01 * d20) / denom
|
|
|
|
var u := 1.0 - v - w
|
|
|
|
return Vector3(u, v, w)
|
|
|
|
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
## Is the given point on the planar triangle defined by v1, v2, and v3?
|
2025-03-01 14:40:02 -07:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
## 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.
|
2025-03-01 14:40:02 -07:00
|
|
|
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:
|
|
|
|
continue
|
|
|
|
# Normals match, so check if the point is on this face
|
|
|
|
var v1 := meshtool.get_vertex(meshtool.get_face_vertex(i, 0))
|
|
|
|
var v2 := meshtool.get_vertex(meshtool.get_face_vertex(i, 1))
|
|
|
|
var v3 := meshtool.get_vertex(meshtool.get_face_vertex(i, 2))
|
|
|
|
if _is_in_triangle(point, v1, v2, v3):
|
|
|
|
return i
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
## 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.
|
2025-03-01 14:40:02 -07:00
|
|
|
func _get_uv(point: Vector3, normal: Vector3) -> Vector2:
|
|
|
|
var face := _get_face(point, normal)
|
|
|
|
if face < 0:
|
|
|
|
return Vector2.INF
|
|
|
|
var fv0 := meshtool.get_face_vertex(face, 0)
|
|
|
|
var fv1 := meshtool.get_face_vertex(face, 1)
|
|
|
|
var fv2 := meshtool.get_face_vertex(face, 2)
|
|
|
|
var v1 := meshtool.get_vertex(fv0)
|
|
|
|
var v2 := meshtool.get_vertex(fv1)
|
|
|
|
var v3 := meshtool.get_vertex(fv2)
|
|
|
|
var bc := _barycentric(point, v1, v2, v3) # TODO memoize
|
|
|
|
var uv1 := meshtool.get_vertex_uv(fv0)
|
|
|
|
var uv2 := meshtool.get_vertex_uv(fv1)
|
|
|
|
var uv3 := meshtool.get_vertex_uv(fv2)
|
|
|
|
return (uv1 * bc.x) + (uv2 * bc.y) + (uv3 * bc.z)
|
|
|
|
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
## 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.
|
2025-03-01 19:02:13 -07:00
|
|
|
func paint_continuous(point: Vector3, normal: Vector3, width: float) -> void:
|
2025-03-05 14:46:13 -07:00
|
|
|
debug_draw.draw_vector(normal, point)
|
|
|
|
|
2025-03-02 12:00:42 -07:00
|
|
|
var px := _get_px(point, normal)
|
2025-03-01 19:02:13 -07:00
|
|
|
if _polyline_buffer and px.distance_to(_polyline_buffer[0]) <= CONTINUITY_LIMIT:
|
|
|
|
_polyline_buffer.push_front(px)
|
|
|
|
if len(_polyline_buffer) > BUFFER_LIMIT:
|
|
|
|
_polyline_buffer.pop_back()
|
|
|
|
var polyline := PackedVector2Array(_polyline_buffer)
|
|
|
|
mask_control.queue_draw(
|
|
|
|
func() -> void: mask_control.draw_polyline(polyline, MASK_COLOR, width * 2, true)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
_polyline_buffer = [px]
|
2025-03-02 12:04:54 -07:00
|
|
|
|
|
|
|
# Always paint a circle, to round out corners & cap ends
|
|
|
|
mask_control.queue_draw(
|
|
|
|
func() -> void: mask_control.draw_circle(px, width, MASK_COLOR, true, -1, true)
|
|
|
|
)
|
2025-03-02 12:00:42 -07:00
|
|
|
_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
|
2025-03-01 19:02:13 -07:00
|
|
|
|
|
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
2025-03-02 12:00:42 -07:00
|
|
|
# If paint_continuous wasn't called last frame, stop the current polyline.
|
|
|
|
if not _continued_paint_this_frame:
|
2025-03-01 19:02:13 -07:00
|
|
|
_polyline_buffer = []
|
2025-03-02 12:00:42 -07:00
|
|
|
_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()
|
2025-03-02 13:16:40 -07:00
|
|
|
|
|
|
|
|
|
|
|
func _on_mask_painted() -> void:
|
|
|
|
_clear_total_dirty = true
|