class_name GunkBody extends StaticBody3D ## StaticBody3D with an associated "gunkable" mesh. const FACE_EPSILON := 0.4 const MASK_COLOR := Color.RED const WRAPPINGS := [ Vector2(-1, -1), Vector2(-1, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(0, 0), Vector2(0, 1), Vector2(1, -1), Vector2(1, 0), Vector2(1, 1), ] @export var mask_dim := 1024 var meshtool := MeshDataTool.new() @onready var mesh_instance: MeshInstance3D = $MeshInstance3D @onready var mesh: ArrayMesh = mesh_instance.mesh @onready var gunk_mat: ShaderMaterial = mesh_instance.get_surface_override_material(0).next_pass @onready var mask_viewport: SubViewport = %MaskViewport @onready var mask_control: DrawController = %MaskControl func _ready() -> void: mask_viewport.size = Vector2(mask_dim, mask_dim) gunk_mat.set_shader_parameter("gunk_mask", mask_viewport.get_texture()) meshtool.create_from_surface(mesh, 0) ## 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 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) 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) 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 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) ## Paint a circle on the mask at a given point & normal on the mesh. func paint_mask(point: Vector3, normal: Vector3, radius: float) -> void: var local_point := point * global_transform var local_normal := normal * global_basis var uv := _get_uv(local_point, local_normal) if uv == Vector2.INF: return var px_center: Vector2 = uv * mask_control.size mask_control.queue_draw( func() -> void: mask_control.draw_circle(px_center, radius, MASK_COLOR, true, -1, true) ) func paint_line( point_a: Vector3, normal_a: Vector3, point_b: Vector3, normal_b: Vector3, width: float ) -> void: var uv_a := _get_uv(point_a * global_transform, normal_a * global_basis) var uv_b := _get_uv(point_b * global_transform, normal_b * global_basis) if uv_a == Vector2.INF or uv_b == Vector2.INF: return var px_a := uv_a * mask_control.size var px_b := uv_b * mask_control.size mask_control.queue_draw( func() -> void: mask_control.draw_line(px_a, px_b, MASK_COLOR, width, true) ) func _render_bar(uv_a: Vector2, scale_a: float, uv_b: Vector2, scale_b: float) -> void: var diff := (uv_b - uv_a).normalized() var ortho := diff.orthogonal() var points := PackedVector2Array( [ uv_a + ortho * scale_a, uv_a - ortho * scale_a, uv_b - ortho * scale_b, uv_b + ortho * scale_b, ] ) mask_control.queue_draw(func() -> void: mask_control.draw_colored_polygon(points, MASK_COLOR)) func paint_bar( point_a: Vector3, normal_a: Vector3, scale_a: float, point_b: Vector3, normal_b: Vector3, scale_b: float ) -> void: var uv_a := _get_uv(point_a * global_transform, normal_a * global_basis) * mask_control.size var uv_b := _get_uv(point_b * global_transform, normal_b * global_basis) * mask_control.size if uv_a == Vector2.INF or uv_b == Vector2.INF: return # TODO just draw square around one valid point var closest_b := uv_b var wrapped_a := uv_a var dist_sq := INF for wrap_vec: Vector2 in WRAPPINGS: var wrapped_b := uv_b + wrap_vec * mask_dim var d := wrapped_b.distance_squared_to(uv_a) if d < dist_sq: closest_b = wrapped_b wrapped_a = uv_a - wrap_vec * mask_dim dist_sq = d _render_bar(uv_a, scale_a, closest_b, scale_b) _render_bar(wrapped_a, scale_a, uv_b, scale_b)