mj177/addons/cyclops_level_builder/math/convex_volume.gd
2025-02-03 19:17:20 +01:00

1447 lines
42 KiB
GDScript

# MIT License
#
# Copyright (c) 2023 Mark McKay
# https://github.com/blackears/cyclopsLevelBuilder
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
@tool
extends RefCounted
class_name ConvexVolume
class VertexInfo extends RefCounted:
var index:int
var mesh:ConvexVolume
var point:Vector3
var normal:Vector3
var edge_indices:Array[int] = []
var selected:bool
func _init(mesh:ConvexVolume, point:Vector3 = Vector3.ZERO):
self.mesh = mesh
self.point = point
func _to_string():
var s:String = "%s [" % [point]
for i in edge_indices:
s += "%s " % i
s += "]"
return s
class EdgeInfo extends RefCounted:
var index:int
var mesh:ConvexVolume
var start_index:int
var end_index:int
var face_indices:Array[int] = []
var selected:bool
func _init(mesh:ConvexVolume, start:int = 0, end:int = 0):
self.mesh = mesh
start_index = start
end_index = end
func get_midpoint()->Vector3:
var p0:Vector3 = mesh.vertices[start_index].point
var p1:Vector3 = mesh.vertices[end_index].point
return (p0 + p1) / 2
func _to_string():
var s:String = "%s %s [" % [start_index, end_index]
for i in face_indices:
s += "%s " % i
s += "]"
return s
class FaceInfo extends RefCounted:
var index:int
var mesh:ConvexVolume
#@deprecated
#var id:int
var normal:Vector3 #Face normal points in direction of interior
var material_id:int
var uv_transform:Transform2D
var color:Color
var visible:bool
var selected:bool
var vertex_indices:Array[int]
var face_vertex_indices:Array[int]
var triangulation_indices:Array[int]
var lightmap_uvs:PackedVector2Array
func _init(mesh:ConvexVolume, normal:Vector3, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = 0, visible:bool = true, color:Color = Color.WHITE, selected:bool = false):
self.mesh = mesh
#self.id = id
self.normal = normal
self.material_id = material_id
self.uv_transform = uv_transform
self.selected = selected
self.visible = visible
self.color = color
func get_plane()->Plane:
return Plane(normal, mesh.vertices[vertex_indices[0]].point)
func get_points()->PackedVector3Array:
var result:PackedVector3Array
for i in vertex_indices:
result.append(mesh.vertices[i].point)
return result
func get_centroid()->Vector3:
var points:PackedVector3Array = get_points()
var center:Vector3
for p in points:
center += p
center /= points.size()
return center
##Returns vector with magnitude equal to twice the area of the face and
## pointing along the face normal
func get_area_vector_x2()->Vector3:
var points:PackedVector3Array = get_points()
return MathUtil.face_area_x2(points)
func get_triangulation()->Array[int]:
if triangulation_indices.is_empty():
var points:PackedVector3Array
for v_idx in vertex_indices:
points.append(mesh.vertices[v_idx].point)
# print("start points %s" % points)
var normal:Vector3 = MathUtil.face_area_x2(points).normalized()
# print("normal %s" % normal)
triangulation_indices = MathUtil.trianglate_face_vertex_indices(points, normal)
# print("triangulation %s" % str(triangulation_indices))
return triangulation_indices
func get_trianges()->PackedVector3Array:
var indices:Array[int] = get_triangulation()
var result:PackedVector3Array
for fv_idx in indices:
var v_idx:int = vertex_indices[fv_idx]
result.append(mesh.vertices[v_idx].point)
# print("triangules %s" % result)
return result
func reverse():
normal = -normal
vertex_indices.reverse()
triangulation_indices.clear()
#Vertex on face closest to given point
func get_closest_vertex(point:Vector3)->int:
var best_dist:float = -1
var best_idx:int = -1
for v_idx in vertex_indices:
var v:VertexInfo = mesh.vertices[v_idx]
var dist:float = v.point.distance_to(point)
if best_idx == -1 || dist < best_dist:
best_idx = v_idx
best_dist = dist
return best_idx
class FaceVertexInfo extends RefCounted:
var index:int
var mesh:ConvexVolume
var face_index:int
var vertex_index:int
var vertex_local_index:int #Position of vertex within this face loop - eg, if this face has 5 verts, the local vert numbers are in order [0, 1, 2, 3, 4]
var color:Color = Color.WHITE
var normal:Vector3
var vertices:Array[VertexInfo] = []
var edges:Array[EdgeInfo] = []
var faces:Array[FaceInfo] = []
var face_vertices:Array[FaceVertexInfo] = []
var face_vertex_coord_map:Dictionary
var bounds:AABB
var lightmap_uvs_dirty = true
var active_vertex:int = -1
var active_edge:int = -1
var active_face:int = -1
var active_face_vertex:int = -1
func _to_string()->String:
var result:String = ""
for v in vertices:
result += str(v.point) + ", "
return result
func init_block(block_bounds:AABB, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = -1, visible:bool = true, color:Color = Color.WHITE):
var p000:Vector3 = block_bounds.position
var p111:Vector3 = block_bounds.end
var p001:Vector3 = Vector3(p000.x, p000.y, p111.z)
var p010:Vector3 = Vector3(p000.x, p111.y, p000.z)
var p011:Vector3 = Vector3(p000.x, p111.y, p111.z)
var p100:Vector3 = Vector3(p111.x, p000.y, p000.z)
var p101:Vector3 = Vector3(p111.x, p000.y, p111.z)
var p110:Vector3 = Vector3(p111.x, p111.y, p000.z)
init_prism([p000, p001, p011, p010], p100 - p000, uv_transform, material_id, visible, color)
func init_prism(base_points:Array[Vector3], extrude_dir:Vector3, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = -1, visible:bool = true, color:Color = Color.WHITE):
vertices = []
edges = []
faces = []
face_vertices = []
face_vertex_coord_map.clear()
var base_normal = -extrude_dir.normalized()
var face_area_x2:Vector3 = MathUtil.face_area_x2(base_points)
if face_area_x2.dot(extrude_dir) > 0:
base_points.reverse()
for p in base_points:
var v:VertexInfo = VertexInfo.new(self, p)
v.index = vertices.size()
vertices.append(v)
for p in base_points:
var v:VertexInfo = VertexInfo.new(self, p + extrude_dir)
v.index = vertices.size()
vertices.append(v)
var f0:FaceInfo = FaceInfo.new(self, base_normal, uv_transform, material_id, visible, color)
f0.index = faces.size()
f0.vertex_indices = []
f0.vertex_indices.append_array(range(base_points.size()))
faces.append(f0)
var f1:FaceInfo = FaceInfo.new(self, -base_normal, uv_transform, material_id, visible, color)
f1.index = faces.size()
f1.vertex_indices = []
f1.vertex_indices.append_array(range(base_points.size(), base_points.size() * 2))
f1.vertex_indices.reverse()
faces.append(f1)
for i in base_points.size():
var p_idx0:int = i
var p_idx1:int = wrap(i + 1, 0, base_points.size())
var v0:VertexInfo = vertices[p_idx0]
var v1:VertexInfo = vertices[p_idx1]
var normal = base_normal.cross(v1.point - v0.point).normalized()
var f:FaceInfo = FaceInfo.new(self, normal, uv_transform, material_id, visible, color)
f.index = faces.size()
f.vertex_indices = [p_idx1, p_idx0, p_idx0 + base_points.size(), p_idx1 + base_points.size()]
faces.append(f)
build_edges()
build_face_vertices()
calc_vertex_normals()
bounds = calc_bounds()
calc_lightmap_uvs()
func init_from_convex_block_data(data:ConvexBlockData):
#print("init_from_convex_block_data")
#print(var_to_str(data))
vertices = []
edges = []
faces = []
face_vertices = []
face_vertex_coord_map.clear()
if !data:
return
#data.validate_arrays()
active_vertex = data.active_vertex
active_edge = data.active_edge
active_face = data.active_face
for i in data.vertex_points.size():
var v:VertexInfo = VertexInfo.new(self, data.vertex_points[i])
v.index = vertices.size()
vertices.append(v)
v.selected = data.vertex_selected[i]
var num_edges:int = data.edge_vertex_indices.size() / 2
for i in num_edges:
var edge:EdgeInfo = EdgeInfo.new(self, data.edge_vertex_indices[i * 2], data.edge_vertex_indices[i * 2 + 1])
edge.index = edges.size()
edges.append(edge)
edge.face_indices.append(data.edge_face_indices[i * 2])
edge.face_indices.append(data.edge_face_indices[i * 2 + 1])
edge.selected = data.edge_selected[i]
#edge.active = data.edge_active[i]
#print("data.face_vertex_count ", data.face_vertex_count)
var face_vertex_count:int = 0
for face_idx in data.face_vertex_count.size():
var num_verts:int = data.face_vertex_count[face_idx]
var vert_indices:Array[int]
var vert_points:PackedVector3Array
for i in num_verts:
var vert_idx:int = data.face_vertex_indices[face_vertex_count]
vert_indices.append(vert_idx)
vert_points.append(vertices[vert_idx].point)
face_vertex_count += 1
var normal = MathUtil.face_area_x2(vert_points).normalized()
var face_uv_transform:Transform2D = data.face_uv_transform[face_idx]
var face_mat_index:int = data.face_material_indices[face_idx]
var face_visible:int = data.face_visible[face_idx]
var face_color:Color = data.face_color[face_idx]
var f:FaceInfo = FaceInfo.new(self, normal, face_uv_transform, face_mat_index, face_visible, face_color)
f.index = faces.size()
f.selected = data.face_selected[face_idx]
#f.active = data.face_active[face_idx]
f.vertex_indices = vert_indices
faces.append(f)
#print("faces buit ", faces.size())
bounds = calc_bounds()
calc_lightmap_uvs()
#Rebuild face verticies if input data is erronious
var all_zero:bool = true
for f_idx in data.face_vertex_face_index:
if f_idx != 0:
all_zero = false
break
if data.face_vertex_face_index.size() == 0 || all_zero:
#print("<<0>>")
#Face vertices not initialized - generate new ones
build_face_vertices()
else:
#print("<<1>>")
for fv_idx in data.face_vertex_face_index.size():
var f_idx:int = data.face_vertex_face_index[fv_idx]
var v_idx:int = data.face_vertex_vertex_index[fv_idx]
var fv:FaceVertexInfo = FaceVertexInfo.new()
face_vertices.append(fv)
#faces[f_idx].face_vertex_indices.append(fv_idx)
fv.face_index = f_idx
fv.vertex_index = v_idx
var coord:Vector2i = Vector2i(f_idx, v_idx)
face_vertex_coord_map[coord] = fv
var f:FaceInfo = faces[f_idx]
fv.normal = data.face_vertex_normal[fv_idx] if data.face_vertex_normal.size() > fv_idx else f.normal
fv.color = data.face_vertex_color[fv_idx] if data.face_vertex_color.size() > fv_idx else Color(1, 1, 1, 1)
#print("init_from_convex_block_data face_vertex_coord_map ", face_vertex_coord_map)
for f_idx in faces.size():
var face:FaceInfo = faces[f_idx]
for v_idx in face.vertex_indices:
face.face_vertex_indices.append(face_vertex_coord_map[Vector2i(f_idx, v_idx)].index)
calc_vertex_normals()
#print("init_from_convex_block_data %s" % format_faces_string())
func init_from_mesh_vector_data(mvd:MeshVectorData):
#print("init_from_mesh_vector_data")
var block_data:ConvexBlockData = ConvexBlockData.new()
block_data.init_from_mesh_vector_data(mvd)
init_from_convex_block_data(block_data)
#Calc convex hull bouding points
func init_from_points(points:PackedVector3Array, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = -1, visible:bool = true, color:Color = Color.WHITE):
vertices = []
edges = []
faces = []
face_vertices = []
face_vertex_coord_map.clear()
#print("init_from_points %s" % points)
var hull:QuickHull.Hull = QuickHull.quickhull(points)
#print("hull %s" % hull.format_points())
var hull_points:Array[Vector3] = hull.get_points()
for p in hull_points:
var v:VertexInfo = VertexInfo.new(self, p)
v.index = vertices.size()
vertices.append(v)
for facet in hull.facets:
var plane:Plane = facet.plane
var vert_indices:Array[int] = []
for p in facet.points:
var vert_idx:int = hull_points.find(p)
vert_indices.append(vert_idx)
var f:FaceInfo = FaceInfo.new(self, plane.normal, uv_transform, material_id, visible, color)
f.index = faces.size()
f.vertex_indices = vert_indices
faces.append(f)
build_edges()
build_face_vertices()
calc_vertex_normals()
bounds = calc_bounds()
calc_lightmap_uvs()
func calc_vertex_normals(smooth:bool = false):
#print("calc_vertex_normals ", _to_string())
#print("calc_vertex_normals face_vertex_coord_map ", face_vertex_coord_map)
for v_idx in vertices.size():
var v:VertexInfo = vertices[v_idx]
var weighted_normal:Vector3
for face in faces:
if face.vertex_indices.has(v_idx):
weighted_normal += MathUtil.face_area_x2(face.get_points())
v.normal = weighted_normal.normalized()
#Calc face vertices
for f_idx in faces.size():
var face:FaceInfo = faces[f_idx]
if face.vertex_indices.has(v_idx):
var fv:FaceVertexInfo = face_vertex_coord_map[Vector2i(f_idx, v_idx)]
fv.normal = v.normal if smooth else face.normal
func get_vertices_in_sphere(center:Vector3, radius:float)->Array[VertexInfo]:
var result:Array[VertexInfo]
for v in vertices:
var dist2 = v.point.distance_squared_to(center)
if dist2 <= radius * radius:
result.append(v)
return result
func get_edge(vert_idx0:int, vert_idx1:int)->EdgeInfo:
for e in edges:
if e.start_index == vert_idx0 && e.end_index == vert_idx1:
return e
if e.start_index == vert_idx1 && e.end_index == vert_idx0:
return e
return null
func get_face_vertex(face_idx:int, vertex_idx:int)->FaceVertexInfo:
var coord:Vector2i = Vector2i(face_idx, vertex_idx)
return face_vertex_coord_map[coord]
func build_face_vertices():
#print("build_face_vertices")
for f_idx in faces.size():
var face:FaceInfo = faces[f_idx]
for v_local_idx in face.vertex_indices.size():
var v_idx = face.vertex_indices[v_local_idx]
var vert:VertexInfo = vertices[v_idx]
var fv:FaceVertexInfo = FaceVertexInfo.new()
var fv_idx:int = face_vertices.size()
face_vertices.append(fv)
var coord:Vector2i = Vector2i(f_idx, v_idx)
#print("Storing fv ", coord)
face_vertex_coord_map[coord] = fv
fv.index = fv_idx
fv.mesh = self
fv.face_index = f_idx
fv.vertex_index = v_idx
fv.vertex_local_index = v_local_idx
fv.color = face.color
face.face_vertex_indices.append(fv_idx)
func build_edges():
#Calculate edges
for face in faces:
var num_corners = face.vertex_indices.size()
for i0 in num_corners:
var i1:int = wrap(i0 + 1, 0, num_corners)
var v0_idx:int = face.vertex_indices[i0]
var v1_idx:int = face.vertex_indices[i1]
var edge:EdgeInfo = get_edge(v0_idx, v1_idx)
if !edge:
var edge_idx = edges.size()
edge = EdgeInfo.new(self, v0_idx, v1_idx)
edge.index = edges.size()
edges.append(edge)
var v0:VertexInfo = vertices[v0_idx]
v0.edge_indices.append(edge_idx)
var v1:VertexInfo = vertices[v1_idx]
v1.edge_indices.append(edge_idx)
# edge.face_indices.append(face.id)
edge.face_indices.append(face.index)
func get_face_coincident_with_plane(plane:Plane)->FaceInfo:
for f in faces:
var p:Plane = f.get_plane()
if p.is_equal_approx(plane):
return f
return null
func get_face_indices(selected_only:bool = false)->PackedInt32Array:
var result:PackedInt32Array
for f_idx in faces.size():
var f:FaceInfo = faces[f_idx]
if !selected_only || f.selected:
result.append(f_idx)
return result
func get_trimesh_indices()->PackedInt32Array:
var result:PackedInt32Array
for f in faces:
for fv_idx in f.get_triangulation():
var v_idx:int = f.vertex_indices[fv_idx]
result.append(v_idx)
return result
func get_face_most_similar_to_plane(plane:Plane)->FaceInfo:
var best_dot:float = -1
var best_face:FaceInfo
for f in faces:
var p:Plane = f.get_plane()
var dot = p.normal.dot(plane.normal)
if dot >= best_dot:
best_dot = dot
best_face = f
return best_face
func get_vertex_at_position(point:Vector3)->VertexInfo:
for v in vertices:
if v.point.is_equal_approx(point):
return v
return null
func get_edge_at_position(point:Vector3)->EdgeInfo:
for e in edges:
if e.get_midpoint().is_equal_approx(point):
return e
return null
func get_face_at_position(point:Vector3)->FaceInfo:
for f in faces:
if f.get_centroid().is_equal_approx(point):
return f
return null
func copy_vertex_attributes(ref_vol:ConvexVolume):
for v_idx in vertices.size():
var v:VertexInfo = vertices[v_idx]
var ref_v:VertexInfo = ref_vol.get_vertex_at_position(v.point)
if ref_v:
v.selected = ref_v.selected
func copy_face_attributes(ref_vol:ConvexVolume):
for f_idx in faces.size():
var f:FaceInfo = faces[f_idx]
var ref_face:FaceInfo = ref_vol.get_face_most_similar_to_plane(f.get_plane())
f.material_id = ref_face.material_id
f.uv_transform = ref_face.uv_transform
f.visible = ref_face.visible
f.color = ref_face.color
f.selected = ref_face.selected
#Copy face vertex values
for v_local_idx in f.vertex_indices.size():
var v_idx:int = f.vertex_indices[v_local_idx]
var v:VertexInfo = vertices[v_idx]
var fv:FaceVertexInfo = face_vertex_coord_map[Vector2i(f_idx, v_idx)]
var v_idx_ref:int = ref_face.get_closest_vertex(v.point)
var fv_ref:FaceVertexInfo = ref_vol.face_vertex_coord_map[Vector2i(ref_face.index, v_idx_ref)]
fv.normal = fv_ref.normal
fv.color = fv_ref.color
func to_convex_block_data()->ConvexBlockData:
var result:ConvexBlockData = ConvexBlockData.new()
result.active_vertex = active_vertex
result.active_edge = active_edge
result.active_face = active_face
result.active_face_vertex = active_face_vertex
for v in vertices:
result.vertex_points.append(v.point)
result.vertex_selected.append(v.selected)
#result.vertex_active.append(v.active)
for e in edges:
result.edge_vertex_indices.append_array([e.start_index, e.end_index])
result.edge_face_indices.append_array([e.face_indices[0], e.face_indices[1]])
result.edge_selected.append(e.selected)
#result.edge_active.append(e.active)
for face in faces:
var num_verts:int = face.vertex_indices.size()
result.face_vertex_count.append(num_verts)
result.face_vertex_indices.append_array(face.vertex_indices)
#result.face_ids.append(face.id)
result.face_selected.append(face.selected)
#result.face_active.append(face.active)
result.face_material_indices.append(face.material_id)
result.face_uv_transform.append(face.uv_transform)
result.face_visible.append(face.visible)
result.face_color.append(face.color)
for fv_idx in face_vertices.size():
var fv:FaceVertexInfo = face_vertices[fv_idx]
#print("to_convex_block_data fv ", fv.face_index, " ", fv.vertex_index)
result.face_vertex_face_index.append(fv.face_index)
result.face_vertex_vertex_index.append(fv.vertex_index)
result.face_vertex_normal.append(fv.normal)
result.face_vertex_color.append(fv.color)
return result
func to_mesh_vector_data()->MeshVectorData:
var mvd:MeshVectorData = MeshVectorData.new()
var block_data:ConvexBlockData = to_convex_block_data()
mvd.create_from_convex_block(block_data)
return mvd
func get_face(face_index:int)->FaceInfo:
return faces[face_index]
func get_centroid()->Vector3:
var points:PackedVector3Array = get_points()
var sum:Vector3
for p in points:
sum += p
return sum / points.size()
# Creates a new volume that is equal to the portion of this volume on the top
# side of the passed plane. Does not modify the geometry of this volume.
func cut_with_plane(plane:Plane, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = 0)->ConvexVolume:
#
var planes:Array[Plane]
for f in faces:
#Top of planr should point toward interior
planes.append(MathUtil.flip_plane(f.get_plane()))
planes.append(plane)
#print("planes %s" % GeneralUtil.format_planes_string(planes))
var hull_points:Array[Vector3] = MathUtil.get_convex_hull_points_from_planes(planes)
if hull_points.is_empty():
return null
var new_vol:ConvexVolume = ConvexVolume.new()
new_vol.init_from_points(hull_points)
new_vol.copy_face_attributes(self)
for f in new_vol.faces:
var f_plane:Plane = MathUtil.flip_plane(f.get_plane())
if f_plane.is_equal_approx(plane):
f.uv_transform = uv_transform
f.material_id = material_id
break
return new_vol
func is_empty():
return bounds.size.is_zero_approx()
# Returns a new ConvexVolume equal to this volume after the plane of the
# indicated face has been translated the given offset. Does not modify the
# geometry of this volume.
func translate_face_plane(face_index:int, offset:Vector3, lock_uvs:bool = false)->ConvexVolume:
var xform:Transform3D = Transform3D(Basis.IDENTITY, -offset)
var source_face:FaceInfo
var transformed_plane:Plane
var planes:Array[Plane] = []
for f in faces:
if f.index == face_index:
transformed_plane = MathUtil.flip_plane(f.get_plane()) * xform
planes.append(transformed_plane)
source_face = f
else:
planes.append(MathUtil.flip_plane(f.get_plane()))
#print("planes %s" % str(planes))
var hull_points:Array[Vector3] = MathUtil.get_convex_hull_points_from_planes(planes)
if hull_points.is_empty():
return null
var new_vol:ConvexVolume = ConvexVolume.new()
new_vol.init_from_points(hull_points)
new_vol.copy_face_attributes(self)
return new_vol
func translated(offset:Vector3, lock_uvs:bool = false)->ConvexVolume:
return transformed(Transform3D(Basis.IDENTITY, offset), lock_uvs)
func translate(offset:Vector3, lock_uvs:bool = false):
transform(Transform3D(Basis.IDENTITY, offset), lock_uvs)
func transformed(xform:Transform3D, lock_uvs:bool = false)->ConvexVolume:
var new_vol:ConvexVolume = ConvexVolume.new()
new_vol.init_from_convex_block_data(to_convex_block_data())
new_vol.transform(xform)
return new_vol
func transform_uvs(xform:Transform3D):
# var xform:Transform3D = obj_xform.affine_inverse()
for f in faces:
var axis:MathUtil.Axis = MathUtil.get_longest_axis(f.normal)
match axis:
MathUtil.Axis.X:
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(0, 0, 1) - orig_p
var v_p:Vector3 = xform * Vector3(0, 1, 0) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.z, u_p.y), \
Vector2(v_p.z, v_p.y), \
Vector2(orig_p.z, orig_p.y))
f.uv_transform = f.uv_transform * move_xform
MathUtil.Axis.Y:
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(1, 0, 0) - orig_p
var v_p:Vector3 = xform * Vector3(0, 0, 1) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.x, u_p.z), \
Vector2(v_p.x, v_p.z), \
Vector2(orig_p.x, orig_p.z))
f.uv_transform = f.uv_transform * move_xform
MathUtil.Axis.Z:
#var xform_inv = xform.affine_inverse()
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(1, 0, 0) - orig_p
var v_p:Vector3 = xform * Vector3(0, 1, 0) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.x, u_p.y), \
Vector2(v_p.x, v_p.y), \
Vector2(orig_p.x, orig_p.y))
f.uv_transform = f.uv_transform * move_xform
func transform(xform:Transform3D, lock_uvs:bool = false):
for v in vertices:
v.point = xform * v.point
if xform.basis.determinant() < 0:
for f in faces:
f.reverse()
if lock_uvs:
for f in faces:
var axis:MathUtil.Axis = MathUtil.get_longest_axis(f.normal)
match axis:
MathUtil.Axis.X:
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(0, 0, 1) - orig_p
var v_p:Vector3 = xform * Vector3(0, 1, 0) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.z, u_p.y), \
Vector2(v_p.z, v_p.y), \
Vector2(orig_p.z, orig_p.y))
f.uv_transform = f.uv_transform * move_xform
MathUtil.Axis.Y:
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(1, 0, 0) - orig_p
var v_p:Vector3 = xform * Vector3(0, 0, 1) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.x, u_p.z), \
Vector2(v_p.x, v_p.z), \
Vector2(orig_p.x, orig_p.z))
f.uv_transform = f.uv_transform * move_xform
MathUtil.Axis.Z:
#var xform_inv = xform.affine_inverse()
var orig_p:Vector3 = xform.origin
var u_p:Vector3 = xform * Vector3(1, 0, 0) - orig_p
var v_p:Vector3 = xform * Vector3(0, 1, 0) - orig_p
var move_xform:Transform2D = Transform2D(Vector2(u_p.x, u_p.y), \
Vector2(v_p.x, v_p.y), \
Vector2(orig_p.x, orig_p.y))
f.uv_transform = f.uv_transform * move_xform
#calc_lightmap_uvs()
#@deprecated
#func unused_face_id()->int:
#var idx = 0
#for p in faces:
#idx = max(idx, p.id)
#return idx + 1
func contains_point(point:Vector3)->bool:
for f in faces:
var plane:Plane = f.get_plane()
if !plane.has_point(point) && !plane.is_point_over(point):
return false
return true
func get_points()->PackedVector3Array:
var points:PackedVector3Array
for v in vertices:
points.append(v.point)
return points
func calc_bounds()->AABB:
if vertices.is_empty():
return AABB()
var result:AABB = AABB(vertices[0].point, Vector3.ZERO)
for v_idx in range(1, vertices.size()):
result = result.expand(vertices[v_idx].point)
return result
func calc_bounds_xform(xform:Transform3D)->AABB:
if vertices.is_empty():
return AABB()
var result:AABB = AABB(xform * vertices[0].point, Vector3.ZERO)
for v_idx in range(1, vertices.size()):
result = result.expand(xform * vertices[v_idx].point)
return result
func tristrip_vertex_range(num_verts:int)->PackedInt32Array:
var result:PackedInt32Array
result.append(0)
result.append(1)
for i in range(2, num_verts):
if (i & 1) == 0:
result.append(num_verts - (i >> 1))
else:
result.append((i >> 1) + 1)
return result
func tristrip_vertex_range_reverse(num_verts:int)->PackedInt32Array:
var result:PackedInt32Array
result.append(1)
result.append(0)
for i in range(2, num_verts):
if (i & 1) == 0:
result.append((i >> 1) + 1)
else:
result.append(num_verts - (i >> 1))
return result
func calc_lightmap_uvs():
var packer:FacePacker = FacePacker.new()
var max_dim:float = max(bounds.size.x, bounds.size.y, bounds.size.z)
var tree:FacePacker.FaceTree = packer.build_faces(self, max_dim * .1)
var xform:Transform2D = Transform2D.IDENTITY
xform = xform.scaled(tree.bounds.size)
if is_zero_approx(xform.determinant()):
return
var xform_inv = xform.affine_inverse()
for ft in tree.face_list:
var face:FaceInfo = faces[ft.face_index]
face.lightmap_uvs = xform_inv * ft.points
func create_mesh(material_list:Array[Material], default_material:Material, override_with_default_material:bool = false)->ArrayMesh:
var mesh:ArrayMesh = ArrayMesh.new()
mesh.blend_shape_mode = Mesh.BLEND_SHAPE_MODE_NORMALIZED
mesh.lightmap_size_hint = Vector2(1000, 1000)
var shadow_mesh:ArrayMesh = ArrayMesh.new()
shadow_mesh.blend_shape_mode = Mesh.BLEND_SHAPE_MODE_NORMALIZED
#print("create_mesh")
#print("faces.size() ", faces.size())
var face_dict:Dictionary = {}
for f_idx in faces.size():
# print("check F_idx %s" % f_idx)
var face:FaceInfo = faces[f_idx]
if face_dict.has(face.material_id):
var arr = face_dict[face.material_id]
arr.append(f_idx)
# print("arr %s" % [arr])
face_dict[face.material_id] = arr
# print("append %s to %s" % [f_idx, face.material_id])
else:
face_dict[face.material_id] = [f_idx]
# print("starting %s to %s" % [f_idx, face.material_id])
var surface_idx:int = 0
for mat_id in face_dict.keys():
# print("surface mat grp %s" % mat_id)
var points:PackedVector3Array
var normals:PackedVector3Array
var tangents:PackedFloat32Array
var colors:PackedColorArray
var uv1s:PackedVector2Array
var uv2s:PackedVector2Array
var material = default_material
if !override_with_default_material:
if mat_id >= 0 && mat_id < material_list.size():
material = material_list[mat_id]
for f_idx in face_dict[mat_id]:
# print("f_idx %s" % f_idx)
var face:FaceInfo = faces[f_idx]
if !face.visible:
continue
var axis:MathUtil.Axis = MathUtil.get_longest_axis(face.normal)
var fv_trianglation:Array[int] = face.get_triangulation()
for v_local_idx in fv_trianglation:
var v_idx:int = face.vertex_indices[v_local_idx]
# var fv_idx:int = face.face_vertex_indices[v_local_idx]
# var fv_idx:int = face_vertex_coord_map[Vector2i(f_idx, v_idx)].index
var fv:FaceVertexInfo = face_vertex_coord_map[Vector2i(f_idx, v_idx)]
#var fv:ConvexVolume.FaceVertexInfo = face
#vol.get_face_vertex(f_idx, v_idx)
var p:Vector3 = vertices[v_idx].point
var uv:Vector2
if axis == MathUtil.Axis.X:
uv = Vector2(-p.z, -p.y)
elif axis == MathUtil.Axis.Y:
uv = Vector2(-p.x, -p.z)
elif axis == MathUtil.Axis.Z:
uv = Vector2(-p.x, -p.y)
uv = face.uv_transform * uv
uv1s.append(uv)
uv2s.append(face.lightmap_uvs[v_local_idx])
# normals.append(face.normal)
normals.append(fv.normal)
# colors.append(face.color)
# colors.append(face_vertices[fv_idx].color)
colors.append(fv.color)
points.append(p)
#Calculate tangents
#http://foundationsofgameenginedev.com/FGED2-sample.pdf
for i in range(0, points.size(), 3):
var p0:Vector3 = points[i]
var p1:Vector3 = points[i + 1]
var p2:Vector3 = points[i + 2]
var uv0:Vector2 = uv1s[i]
var uv1:Vector2 = uv1s[i + 1]
var uv2:Vector2 = uv1s[i + 2]
var n:Vector3 = normals[i]
var e1:Vector3 = p1 - p0
var e2:Vector3 = p2 - p0
var duv1:Vector2 = uv1 - uv0
var duv2:Vector2 = uv2 - uv0
var r:float = 1.0 / (duv1.x * duv2.y - duv2.x * duv1.y)
var t:Vector3 = (e1 * duv2.y - e2 * duv1.y) * r
var b:Vector3 = (e2 * duv1.x - e1 * duv2.x) * r
t = t.normalized()
for j in 3:
tangents.append(t.x)
tangents.append(t.y)
tangents.append(t.z)
tangents.append(-1.0 if t.cross(b).dot(n) > 0 else 1.0)
var arrays:Array = create_indexed_vertex_array(points, normals, tangents, colors, uv1s, uv2s)
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(surface_idx, material)
var shadow_arrays:Array = []
shadow_arrays.resize(Mesh.ARRAY_MAX)
shadow_arrays[Mesh.ARRAY_VERTEX] = points
shadow_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, shadow_arrays)
shadow_mesh.surface_set_material(surface_idx, material)
surface_idx += 1
mesh.shadow_mesh = shadow_mesh
# var err = mesh.lightmap_unwrap(Transform3D.IDENTITY, 10)
# print("Lightmap unwrap Error: %s" % err)
return mesh
func create_indexed_vertex_array(points:PackedVector3Array, normals:PackedVector3Array, tangents:PackedFloat32Array, colors:PackedColorArray, uv1s:PackedVector2Array, uv2s:PackedVector2Array)->Array:
var vert_idx_map:Dictionary
var indices:PackedInt32Array
var points_indexed:PackedVector3Array
var normals_indexed:PackedVector3Array
var tangents_indexed:PackedFloat32Array
var colors_indexed:PackedColorArray
var uv1s_indexed:PackedVector2Array
var uv2s_indexed:PackedVector2Array
for v_idx in points.size():
var vertex:PackedFloat32Array
vertex.append(points[v_idx].x)
vertex.append(points[v_idx].y)
vertex.append(points[v_idx].z)
vertex.append(normals[v_idx].x)
vertex.append(normals[v_idx].y)
vertex.append(normals[v_idx].z)
vertex.append(tangents[v_idx * 4])
vertex.append(tangents[v_idx * 4 + 1])
vertex.append(tangents[v_idx * 4 + 2])
vertex.append(tangents[v_idx * 4 + 3])
vertex.append(colors[v_idx].r)
vertex.append(colors[v_idx].g)
vertex.append(colors[v_idx].b)
vertex.append(colors[v_idx].a)
vertex.append(uv1s[v_idx].x)
vertex.append(uv1s[v_idx].y)
vertex.append(uv2s[v_idx].x)
vertex.append(uv2s[v_idx].y)
var new_index:int
if !vert_idx_map.has(vertex):
#print("alloc vtx ", vertex)
new_index = vert_idx_map.size()
vert_idx_map[vertex] = new_index
points_indexed.append(points[v_idx])
normals_indexed.append(normals[v_idx])
tangents_indexed.append(tangents[v_idx * 4])
tangents_indexed.append(tangents[v_idx * 4 + 1])
tangents_indexed.append(tangents[v_idx * 4 + 2])
tangents_indexed.append(tangents[v_idx * 4 + 3])
colors_indexed.append(colors[v_idx])
uv1s_indexed.append(uv1s[v_idx])
uv2s_indexed.append(uv2s[v_idx])
else:
new_index = vert_idx_map[vertex]
#print("index ", new_index)
indices.append(new_index)
#print("indices ", indices)
var arrays:Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = points_indexed
arrays[Mesh.ARRAY_NORMAL] = normals_indexed
arrays[Mesh.ARRAY_TANGENT] = tangents_indexed
arrays[Mesh.ARRAY_TEX_UV] = uv1s_indexed
arrays[Mesh.ARRAY_TEX_UV2] = uv2s_indexed
arrays[Mesh.ARRAY_COLOR] = colors_indexed
arrays[Mesh.ARRAY_INDEX] = indices
return arrays
func append_mesh_backfacing(mesh:ImmediateMesh, material:Material, offset:float = .2):
# if Engine.is_editor_hint():
# return
for face in faces:
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, material)
# print("face %s" % face.index)
mesh.surface_set_normal(face.normal)
# for i in tristrip_vertex_range_reverse(face.vertex_indices.size()):
for i in tristrip_vertex_range_reverse(face.vertex_indices.size()):
var v_idx:int = face.vertex_indices[i]
var v:VertexInfo = vertices[v_idx]
var p:Vector3 = v.point + v.normal * offset
#var p:Vector3 = v.point + Vector3(.1, .1, .1)
mesh.surface_add_vertex(p)
mesh.surface_end()
func append_mesh_outline(mesh:ImmediateMesh, viewport_camera:Camera3D, local_to_world:Transform3D, material:Material, thickness:float = 4):
var cam_orig:Vector3 = viewport_camera.global_transform.origin
var segments:PackedVector2Array
#print("--append_mesh_outline")
#var view_plane:Plane = Plane(-viewport_camera.global_basis.z,
#viewport_camera.global_position
#- viewport_camera.global_basis.z * viewport_camera.near * 1)
var frustum:Array[Plane] = viewport_camera.get_frustum()
for edge in edges:
var v0:VertexInfo = vertices[edge.start_index]
var v1:VertexInfo = vertices[edge.end_index]
var p0_world:Vector3 = local_to_world * v0.point
var p1_world:Vector3 = local_to_world * v1.point
var frustum_culled:bool = false
for p in frustum:
var p_flip:Plane = MathUtil.flip_plane(p)
var result:PackedVector3Array = MathUtil.clip_segment_to_plane_3d(p_flip, p0_world, p1_world)
if result.is_empty():
frustum_culled = true
break
p0_world = result[0]
p1_world = result[1]
if frustum_culled:
continue
var has_front:bool = false
var has_back:bool = false
for f_idx in edge.face_indices:
var face = faces[f_idx]
var plane = face.get_plane()
plane = local_to_world * plane
if plane.is_point_over(cam_orig):
has_front = true
else:
has_back = true
#print("front %s back %s" % [has_front, has_back])
if has_front && has_back:
#Draw edge
var p0_screen:Vector2 = viewport_camera.unproject_position(p0_world)
var p1_screen:Vector2 = viewport_camera.unproject_position(p1_world)
segments.append(p0_screen)
segments.append(p1_screen)
#print("seg %s %s" % [p0_screen, p1_screen])
#print("segments ", segments)
var loops:Array[Loop2D] = MathUtil.get_loops_from_segments_2d(segments)
for loop in loops:
var out_dirs:PackedVector2Array
#print("loop ", loop)
for v_idx in loop.points.size():
var p0_screen:Vector2 = loop.points[wrap(v_idx - 1, 0, loop.points.size())]
var p1_screen:Vector2 = loop.points[v_idx]
var p2_screen:Vector2 = loop.points[wrap(v_idx + + 1, 0, loop.points.size())]
#var span:Vector2 = p2_screen - p1_screen
var norm01:Vector2 = (p1_screen - p0_screen).normalized()
var norm12:Vector2 = (p2_screen - p1_screen).normalized()
var out_dir1:Vector2 = (-norm01 + norm12).normalized()
var perp:Vector2 = out_dir1 - out_dir1.project(norm12)
#Check winding
if perp.x * norm12.y - perp.y * norm12.x < 0:
out_dir1 = -out_dir1
out_dirs.append(out_dir1)
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, material)
for v_idx in loop.points.size() + (1 if loop.closed else 0):
var p_screen:Vector2 = loop.points[wrap(v_idx, 0, loop.points.size())]
var p_out_dir:Vector2 = out_dirs[wrap(v_idx, 0, loop.points.size())]
var z_pos:float = (viewport_camera.near + viewport_camera.far) / 2
var p0:Vector3 = viewport_camera.project_position(p_screen, z_pos)
var p1:Vector3 = viewport_camera.project_position(p_screen + p_out_dir * thickness, z_pos)
mesh.surface_add_vertex(p0)
mesh.surface_add_vertex(p1)
mesh.surface_end()
func create_mesh_wire(material:Material)->ImmediateMesh:
# if Engine.is_editor_hint():
# return
var mesh:ImmediateMesh = ImmediateMesh.new()
mesh.surface_begin(Mesh.PRIMITIVE_LINES, material)
for e in edges:
var v0:VertexInfo = vertices[e.start_index]
var v1:VertexInfo = vertices[e.end_index]
mesh.surface_add_vertex(v0.point)
mesh.surface_add_vertex(v1.point)
mesh.surface_end()
return mesh
func intersect_ray_closest(origin:Vector3, dir:Vector3)->IntersectResults:
if bounds.intersects_ray(origin, dir) == null:
return null
var best_result:IntersectResults
for f_idx in faces.size():
var face:FaceInfo = faces[f_idx]
# var tris:PackedVector3Array = MathUtil.trianglate_face(face.get_points(), face.normal)
var tris:PackedVector3Array = face.get_trianges()
for i in range(0, tris.size(), 3):
var p0:Vector3 = tris[i]
var p1:Vector3 = tris[i + 1]
var p2:Vector3 = tris[i + 2]
#Godot uses clockwise winding
var tri_area_x2:Vector3 = MathUtil.triangle_area_x2(p0, p1, p2)
var p_hit:Vector3 = MathUtil.intersect_plane(origin, dir, p0, tri_area_x2)
if !p_hit.is_finite():
continue
if MathUtil.triangle_area_x2(p_hit, p0, p1).dot(tri_area_x2) < 0:
continue
if MathUtil.triangle_area_x2(p_hit, p1, p2).dot(tri_area_x2) < 0:
continue
if MathUtil.triangle_area_x2(p_hit, p2, p0).dot(tri_area_x2) < 0:
continue
#Intersection
var dist_sq:float = (origin - p_hit).length_squared()
if !best_result || best_result.distance_squared > dist_sq:
var result:IntersectResults = IntersectResults.new()
#result.face_id = face.id
result.face_index = f_idx
result.normal = face.normal
result.position = p_hit
result.distance_squared = dist_sq
best_result = result
return best_result
func format_faces_string()->String:
var s:String = ""
for f in faces:
s = s + "["
for v_idx in f.vertex_indices:
s += "%s, " % vertices[v_idx].point
s = s + "],\n"
return s
func update_edge_and_face_selection_from_vertices():
for e in edges:
e.selected = vertices[e.start_index].selected && vertices[e.end_index].selected
for f in faces:
var all_sel:bool = true
for v_idx in f.vertex_indices:
if !vertices[v_idx].selected:
all_sel = false
break
f.selected = all_sel
func intersects_plane(plane:Plane)->bool:
var is_over:bool = false
var is_under:bool = false
for v in vertices:
var p:Vector3 = v.point
if plane.has_point(p):
continue
if plane.is_point_over(p):
is_over = true
else:
is_under = true
if is_over && is_under:
return true
return false
func subtract(subtrahend:ConvexVolume)->Array[ConvexVolume]:
var result_list:Array[ConvexVolume]
var split_vol:ConvexVolume = self
for face in subtrahend.faces:
var p:Plane = face.get_plane()
if !split_vol.intersects_plane(p):
continue
var vol_over:ConvexVolume = split_vol.cut_with_plane(p)
var vol_under:ConvexVolume = split_vol.cut_with_plane(MathUtil.flip_plane(p))
result_list.append(vol_over)
split_vol = vol_under
# result_list.append(split_vol)
return result_list
func intersect(subtrahend:ConvexVolume)->ConvexVolume:
var result_list:Array[ConvexVolume]
var split_vol:ConvexVolume = self
for face in subtrahend.faces:
var p:Plane = face.get_plane()
if !split_vol.intersects_plane(p):
continue
var vol_over:ConvexVolume = split_vol.cut_with_plane(p)
var vol_under:ConvexVolume = split_vol.cut_with_plane(MathUtil.flip_plane(p))
result_list.append(vol_over)
split_vol = vol_under
return split_vol
func is_over_or_on_plane(plane:Plane)->bool:
for v in vertices:
if !plane.is_point_over(v.point) && !plane.has_point(v.point):
return false
return true
func intersects_convex_volume(vol:ConvexVolume)->bool:
#Look for plane of separtion between two volumes
for f in vol.faces:
var p:Plane = f.get_plane()
if is_over_or_on_plane(p):
return false
return true
func intersects_frustum(frustum:Array[Plane])->bool:
for face in faces:
var points:PackedVector3Array = face.get_points()
if MathUtil.polygon_intersects_frustum(points, frustum):
return true
return false
func make_convex():
var selected_points:PackedVector3Array
var new_points:PackedVector3Array
for v in vertices:
new_points.append(v.point)
var new_vol:ConvexVolume = ConvexVolume.new()
new_vol.init_from_points(new_points)
new_vol.copy_vertex_attributes(self)
new_vol.copy_face_attributes(self)
if active_vertex != -1:
var v:VertexInfo = vertices[active_vertex]
var new_v:VertexInfo = new_vol.get_vertex_at_position(v.point)
if new_v:
new_vol.active_vertex = new_v.index
if active_edge != -1:
var e:EdgeInfo = edges[active_edge]
var mp:Vector3 = e.get_midpoint()
var new_e:EdgeInfo = new_vol.get_edge_at_position(mp)
if new_e:
new_vol.active_edge = new_e.index
if active_face != -1:
var f:FaceInfo = faces[active_face]
var centroid:Vector3 = f.get_centroid()
var new_f:FaceInfo = new_vol.get_face_at_position(centroid)
if new_f:
new_vol.active_face = new_f.index
#for v_idx in new_vol.vertices.size():
#var v:ConvexVolume.VertexInfo = new_vol.vertices[v_idx]
## print ("vol point %s " % v.point)
#if selected_points.has(v.point):
## print("set sel")
#v.selected = true
#
#block.mesh_vector_data = new_vol.to_mesh_vector_data()