class_name GunkBody extends StaticBody3D ## StaticBody3D with an associated "gunkable" mesh. const CONTINUITY_LIMIT := 64 const BUFFER_LIMIT := 3 const FACE_EPSILON := 0.4 const MASK_COLOR := Color.RED @export var mask_dim := 1024 var meshtool := MeshDataTool.new() var _polyline_buffer: Array[Vector2] = [] var _continuous := false var _painted_this_frame := false @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) func paint_continuous(point: Vector3, normal: Vector3, width: float) -> void: var px := _get_uv(point * global_transform, normal * global_basis) * mask_control.size 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] mask_control.queue_draw( func() -> void: mask_control.draw_circle(px, width, MASK_COLOR, true, -1, true) ) _painted_this_frame = true func _process(_delta: float) -> void: if not _painted_this_frame: _polyline_buffer = [] _painted_this_frame = false