grunk/src/world/gunk_body/gunk_body.gd

159 lines
4.8 KiB
GDScript3
Raw Normal View History

2025-03-01 14:40:02 -07:00
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),
]
2025-03-01 17:14:10 -07:00
@export var mask_dim := 1024
2025-03-01 14:40:02 -07:00
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
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)
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)
## 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)
2025-03-01 16:59:39 -07:00
## Paint a circle on the mask at a given point & normal on the mesh.
func paint_mask(point: Vector3, normal: Vector3, radius: float) -> void:
2025-03-01 14:40:02 -07:00
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
2025-03-01 16:59:39 -07:00
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)