initial commit
This commit is contained in:
928
addons/cyclops_level_builder/math/convex_volume.gd
Normal file
928
addons/cyclops_level_builder/math/convex_volume.gd
Normal file
@ -0,0 +1,928 @@
|
||||
# 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 mesh:ConvexVolume
|
||||
#var index:int
|
||||
var point:Vector3
|
||||
var normal:Vector3
|
||||
var edge_indices:Array[int] = []
|
||||
var selected:bool
|
||||
#var active: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 mesh:ConvexVolume
|
||||
var start_index:int
|
||||
var end_index:int
|
||||
var face_indices:Array[int] = []
|
||||
var selected:bool
|
||||
#var active:bool
|
||||
|
||||
func _init(mesh:ConvexVolume, start:int = 0, end:int = 0):
|
||||
self.mesh = mesh
|
||||
start_index = start
|
||||
end_index = end
|
||||
|
||||
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 mesh:ConvexVolume
|
||||
var id:int
|
||||
var normal:Vector3 #Face normal points in direction of interior
|
||||
var material_id:int
|
||||
var uv_transform:Transform2D
|
||||
var selected:bool
|
||||
#var active:bool
|
||||
var vertex_indices:Array[int]
|
||||
var triangulation_indices:Array[int]
|
||||
var lightmap_uvs:PackedVector2Array
|
||||
|
||||
func _init(mesh:ConvexVolume, id:int, normal:Vector3, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = 0, 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
|
||||
|
||||
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
|
||||
|
||||
# func get_triangulation()->Array[int]:
|
||||
# if triangulation_indices.is_empty():
|
||||
# var points:PackedVector3Array
|
||||
# var indices:Array[int]
|
||||
# for v_idx in vertex_indices:
|
||||
# points.append(mesh.vertices[v_idx].point)
|
||||
# indices.append(v_idx)
|
||||
#
|
||||
## print("start points %s" % points)
|
||||
#
|
||||
# var normal:Vector3 = MathUtil.face_area_x2(points).normalized()
|
||||
## print("normal %s" % normal)
|
||||
# triangulation_indices = MathUtil.trianglate_face_indices(points, indices, normal)
|
||||
## print("triangulation %s" % str(triangulation_indices))
|
||||
#
|
||||
# return triangulation_indices
|
||||
|
||||
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()
|
||||
|
||||
#class FaceVertexInfo extends RefCounted:
|
||||
# var vert_idx:int
|
||||
# var face_idx:int
|
||||
|
||||
|
||||
|
||||
var vertices:Array[VertexInfo] = []
|
||||
var edges:Array[EdgeInfo] = []
|
||||
var faces:Array[FaceInfo] = []
|
||||
var bounds:AABB
|
||||
|
||||
var lightmap_uvs_dirty = true
|
||||
|
||||
var active_vertex:int = -1
|
||||
var active_edge:int = -1
|
||||
var active_face: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):
|
||||
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)
|
||||
|
||||
|
||||
func init_prism(base_points:Array[Vector3], extrude_dir:Vector3, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = -1):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
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)
|
||||
vertices.append(v)
|
||||
for p in base_points:
|
||||
var v:VertexInfo = VertexInfo.new(self, p + extrude_dir)
|
||||
vertices.append(v)
|
||||
|
||||
var f0:FaceInfo = FaceInfo.new(self, faces.size(), base_normal, uv_transform, material_id)
|
||||
f0.vertex_indices = []
|
||||
f0.vertex_indices.append_array(range(base_points.size()))
|
||||
faces.append(f0)
|
||||
var f1:FaceInfo = FaceInfo.new(self, faces.size(), -base_normal, uv_transform, material_id)
|
||||
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, faces.size(), normal, uv_transform, material_id)
|
||||
f.vertex_indices = [p_idx1, p_idx0, p_idx0 + base_points.size(), p_idx1 + base_points.size()]
|
||||
faces.append(f)
|
||||
|
||||
build_edges()
|
||||
calc_vertex_normals()
|
||||
|
||||
bounds = calc_bounds()
|
||||
calc_lightmap_uvs()
|
||||
|
||||
func init_from_convex_block_data(data:ConvexBlockData):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
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])
|
||||
vertices.append(v)
|
||||
v.selected = data.vertex_selected[i]
|
||||
#v.active = data.vertex_active[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])
|
||||
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]
|
||||
|
||||
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)
|
||||
# var v_idx:int = data.face_vertex_indices[count]
|
||||
face_vertex_count += 1
|
||||
|
||||
var normal = MathUtil.face_area_x2(vert_points).normalized()
|
||||
var f:FaceInfo = FaceInfo.new(self, data.face_ids[face_idx], normal, data.face_uv_transform[face_idx], data.face_material_indices[face_idx])
|
||||
f.selected = data.face_selected[face_idx]
|
||||
#f.active = data.face_active[face_idx]
|
||||
f.vertex_indices = vert_indices
|
||||
|
||||
faces.append(f)
|
||||
|
||||
|
||||
calc_vertex_normals()
|
||||
|
||||
bounds = calc_bounds()
|
||||
calc_lightmap_uvs()
|
||||
#print("init_from_convex_block_data %s" % format_faces_string())
|
||||
|
||||
|
||||
#Calc convex hull bouding points
|
||||
func init_from_points(points:PackedVector3Array, uv_transform:Transform2D = Transform2D.IDENTITY, material_id:int = -1):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
#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:
|
||||
vertices.append(VertexInfo.new(self, p))
|
||||
|
||||
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, faces.size(), plane.normal, uv_transform, material_id)
|
||||
f.vertex_indices = vert_indices
|
||||
faces.append(f)
|
||||
|
||||
|
||||
build_edges()
|
||||
calc_vertex_normals()
|
||||
|
||||
bounds = calc_bounds()
|
||||
calc_lightmap_uvs()
|
||||
|
||||
func calc_vertex_normals():
|
||||
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()
|
||||
|
||||
|
||||
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 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)
|
||||
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)
|
||||
|
||||
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_ids(selected_only:bool = false)->PackedInt32Array:
|
||||
var result:PackedInt32Array
|
||||
for f in faces:
|
||||
if !selected_only || f.selected:
|
||||
result.append(f.id)
|
||||
return result
|
||||
|
||||
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 copy_face_attributes(ref_vol:ConvexVolume):
|
||||
for fl in faces:
|
||||
var ref_face:FaceInfo = ref_vol.get_face_most_similar_to_plane(fl.get_plane())
|
||||
|
||||
fl.material_id = ref_face.material_id
|
||||
fl.uv_transform = ref_face.uv_transform
|
||||
fl.selected = ref_face.selected
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
return result
|
||||
|
||||
func get_face(face_id:int)->FaceInfo:
|
||||
for face in faces:
|
||||
if face.id == face_id:
|
||||
return face
|
||||
return null
|
||||
|
||||
# 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_id: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.id == face_id:
|
||||
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 translate(offset:Vector3, lock_uvs:bool = false):
|
||||
transform(Transform3D(Basis.IDENTITY, offset), lock_uvs)
|
||||
|
||||
|
||||
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:
|
||||
# var xform_inv:Transform3D = xform.affine_inverse()
|
||||
#var xform_inv:Transform3D = xform
|
||||
#print("--xform %s" % xform)
|
||||
|
||||
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()
|
||||
|
||||
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 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)
|
||||
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)->ArrayMesh:
|
||||
# if Engine.is_editor_hint():
|
||||
# return
|
||||
# print("num faces %s" % faces.size())
|
||||
# print("-creating mesh")
|
||||
|
||||
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
|
||||
|
||||
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 uv1s:PackedVector2Array
|
||||
var uv2s:PackedVector2Array
|
||||
|
||||
var material = 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]
|
||||
|
||||
|
||||
var axis:MathUtil.Axis = MathUtil.get_longest_axis(face.normal)
|
||||
|
||||
var fv_trianglation:Array[int] = face.get_triangulation()
|
||||
|
||||
for fv_idx in fv_trianglation:
|
||||
|
||||
var v_idx:int = face.vertex_indices[fv_idx]
|
||||
# var p:Vector3 = triangles[i]
|
||||
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[fv_idx])
|
||||
|
||||
normals.append(face.normal)
|
||||
|
||||
points.append(p)
|
||||
|
||||
var arrays:Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = points
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uv1s
|
||||
arrays[Mesh.ARRAY_TEX_UV2] = 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 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_dir:Vector3 = viewport_camera.global_transform.basis.z
|
||||
var cam_orig:Vector3 = viewport_camera.global_transform.origin
|
||||
|
||||
# print("append_mesh_outline %s" % cam_dir)
|
||||
#points along Z
|
||||
# var cylinder:GeometryMesh = MathGeometry.unit_cylinder(4, thickness, thickness, 0, -1)
|
||||
|
||||
var segments:PackedVector2Array
|
||||
|
||||
for edge in edges:
|
||||
var has_front:bool = false
|
||||
var has_back:bool = false
|
||||
|
||||
for f_idx in edge.face_indices:
|
||||
var face = faces[f_idx]
|
||||
#print("face norm %s" % face.normal)
|
||||
var point_on_plane:Vector3 = vertices[face.vertex_indices[0]].point
|
||||
var to_plane:Vector3 = cam_orig - point_on_plane
|
||||
|
||||
if face.normal.dot(to_plane) > 0:
|
||||
has_front = true
|
||||
elif face.normal.dot(to_plane) < 0:
|
||||
has_back = true
|
||||
|
||||
#print("front %s back %s" % [has_front, has_back])
|
||||
|
||||
if has_front && has_back:
|
||||
#print("drawing edge %s %s" % [edge.start_index, edge.end_index])
|
||||
#Draw edge
|
||||
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 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)
|
||||
|
||||
var loops:Array[PackedVector2Array] = MathUtil.get_loops_from_segments_2d(segments)
|
||||
for loop_points in loops:
|
||||
var out_dirs:PackedVector2Array
|
||||
|
||||
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:
|
||||
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 face in faces:
|
||||
# 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.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
|
||||
|
||||
|
253
addons/cyclops_level_builder/math/face_packer.gd
Normal file
253
addons/cyclops_level_builder/math/face_packer.gd
Normal file
@ -0,0 +1,253 @@
|
||||
# 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 FacePacker
|
||||
|
||||
class SpawnResult extends RefCounted:
|
||||
var point:Vector2
|
||||
var flip:bool
|
||||
|
||||
func _init(point:Vector2, flip:bool):
|
||||
self.point = point
|
||||
self.flip = flip
|
||||
|
||||
class FaceTree extends RefCounted:
|
||||
# var root:FaceTreeNode
|
||||
var size:Vector2
|
||||
var spawn_points:PackedVector2Array = [Vector2.ZERO]
|
||||
var face_list:Array[FaceTracker]
|
||||
var bounds:Rect2
|
||||
|
||||
func _to_string()->String:
|
||||
var res:String = ""
|
||||
for face in face_list:
|
||||
res += "%s,\n" % str(face)
|
||||
return res
|
||||
|
||||
func is_collision(rect:Rect2)->bool:
|
||||
for face in face_list:
|
||||
if face.bounds.intersects(rect):
|
||||
return true
|
||||
return false
|
||||
|
||||
func max_vec_dim(v:Vector2):
|
||||
return max(v.x, v.y)
|
||||
|
||||
func get_best_spawn_point(face:FaceTracker)->SpawnResult:
|
||||
var started:bool = false
|
||||
var best_spawn_point:Vector2 = Vector2.INF
|
||||
var best_bounds:Rect2
|
||||
var best_flip:bool
|
||||
|
||||
for s_idx in spawn_points.size():
|
||||
var spawn_point:Vector2 = spawn_points[s_idx]
|
||||
|
||||
var placed_bounds:Rect2 = face.bounds
|
||||
placed_bounds.position += spawn_point
|
||||
|
||||
if !is_collision(placed_bounds):
|
||||
var new_bounds:Rect2 = bounds.merge(placed_bounds)
|
||||
|
||||
if new_bounds.is_equal_approx(bounds):
|
||||
return SpawnResult.new(spawn_point, false)
|
||||
else:
|
||||
if !started || max_vec_dim(best_bounds.size) > max_vec_dim(new_bounds.size):
|
||||
best_bounds = new_bounds
|
||||
best_flip = false
|
||||
best_spawn_point = spawn_point
|
||||
started = true
|
||||
|
||||
var placed_bounds_flipped:Rect2 = face.bounds
|
||||
placed_bounds_flipped.size = Vector2(placed_bounds_flipped.size.y, placed_bounds_flipped.size.x)
|
||||
placed_bounds_flipped.position += spawn_point
|
||||
|
||||
if !is_collision(placed_bounds_flipped):
|
||||
var new_bounds_flipped:Rect2 = bounds.merge(placed_bounds_flipped)
|
||||
|
||||
if new_bounds_flipped.is_equal_approx(bounds):
|
||||
return SpawnResult.new(spawn_point, true)
|
||||
else:
|
||||
if !started || max_vec_dim(best_bounds.size) > max_vec_dim(new_bounds_flipped.size):
|
||||
best_bounds = new_bounds_flipped
|
||||
best_flip = true
|
||||
best_spawn_point = spawn_point
|
||||
started = true
|
||||
|
||||
return SpawnResult.new(best_spawn_point, best_flip)
|
||||
|
||||
func add_face(face:FaceTracker):
|
||||
var spawn:SpawnResult = get_best_spawn_point(face)
|
||||
|
||||
var idx = spawn_points.find(spawn.point)
|
||||
spawn_points.remove_at(idx)
|
||||
|
||||
if spawn.flip:
|
||||
face.reflect_diagonal()
|
||||
|
||||
face.translate(spawn.point)
|
||||
face_list.append(face)
|
||||
bounds = bounds.merge(face.bounds)
|
||||
|
||||
var sp_0:Vector2 = face.bounds.position + Vector2(face.bounds.size.x, 0)
|
||||
var sp_1:Vector2 = face.bounds.position + Vector2(0, face.bounds.size.y)
|
||||
if !spawn_points.has(sp_0):
|
||||
spawn_points.append(sp_0)
|
||||
if !spawn_points.has(sp_1):
|
||||
spawn_points.append(sp_1)
|
||||
|
||||
|
||||
|
||||
class FaceTracker extends RefCounted:
|
||||
var points:PackedVector2Array
|
||||
var indices:PackedInt32Array
|
||||
var bounds:Rect2
|
||||
var face_index:int
|
||||
|
||||
func _to_string()->String:
|
||||
var res:String = "["
|
||||
for p in points:
|
||||
res += "%s, " % str(p)
|
||||
res += "]"
|
||||
return res
|
||||
|
||||
func max_dim()->float:
|
||||
return max(bounds.size.x, bounds.size.y)
|
||||
|
||||
func reflect_diagonal():
|
||||
for p_idx in points.size():
|
||||
var p:Vector2 = points[p_idx]
|
||||
points[p_idx] = Vector2(p.y, p.x)
|
||||
bounds.size = Vector2(bounds.size.y, bounds.size.x)
|
||||
|
||||
func translate(offset:Vector2):
|
||||
for p_idx in points.size():
|
||||
points[p_idx] += offset
|
||||
bounds.position += offset
|
||||
|
||||
func fit_initial_rect():
|
||||
bounds = Rect2(points[0], Vector2.ZERO)
|
||||
for i in range(1, points.size()):
|
||||
bounds = bounds.expand(points[i])
|
||||
|
||||
#Move so corner of bounds is at (0, 0)
|
||||
for i in points.size():
|
||||
points[i] -= bounds.position
|
||||
bounds.position = Vector2.ZERO
|
||||
|
||||
func get_best_base_index()->int:
|
||||
var best_index:int = -1
|
||||
var best_height:float = INF
|
||||
|
||||
for i0 in points.size():
|
||||
var i1:int = wrap(i0 + 1, 0, points.size())
|
||||
|
||||
var base_dir:Vector2 = points[i1] - points[i0]
|
||||
var base_origin:Vector2 = points[i0]
|
||||
var base_dir_perp:Vector2 = Vector2(-base_dir.y, base_dir.x)
|
||||
|
||||
var max_height:float = 0
|
||||
|
||||
for j in range(2, points.size()):
|
||||
var p_idx:int = wrap(j + i0, 0, points.size())
|
||||
var p:Vector2 = points[p_idx]
|
||||
var offset:Vector2 = p - base_origin
|
||||
var offset_proj:Vector2 = offset.project(base_dir_perp)
|
||||
|
||||
max_height = max(max_height, offset_proj.length_squared())
|
||||
|
||||
if max_height < best_height:
|
||||
best_height = max_height
|
||||
best_index = i0
|
||||
|
||||
return best_index
|
||||
|
||||
func rotate_to_best_fit():
|
||||
var i0:int = get_best_base_index()
|
||||
var i1:int = wrap(i0 + 1, 0, points.size())
|
||||
|
||||
var base_dir:Vector2 = (points[i1] - points[i0]).normalized()
|
||||
var base_dir_perp:Vector2 = Vector2(-base_dir.y, base_dir.x)
|
||||
|
||||
var xform:Transform2D = Transform2D(base_dir, base_dir_perp, Vector2.ZERO)
|
||||
var xform_inv:Transform2D = xform.affine_inverse()
|
||||
|
||||
for p_idx in points.size():
|
||||
var p:Vector2 = xform_inv * points[p_idx]
|
||||
points[p_idx] = p
|
||||
|
||||
|
||||
func pack_faces(faces:Array[FaceTracker])->FaceTree:
|
||||
faces.sort_custom(func (a:FaceTracker, b:FaceTracker): return a.max_dim() > b.max_dim())
|
||||
|
||||
var tree:FaceTree = FaceTree.new()
|
||||
for f in faces:
|
||||
tree.add_face(f)
|
||||
|
||||
#print(tree)
|
||||
return tree
|
||||
|
||||
func build_faces(vol:ConvexVolume, margin:float)->FaceTree:
|
||||
var faces:Array[FaceTracker]
|
||||
|
||||
for f_idx in vol.faces.size():
|
||||
var face:ConvexVolume.FaceInfo = vol.faces[f_idx]
|
||||
var axis:MathUtil.Axis = MathUtil.get_longest_axis(face.normal)
|
||||
|
||||
var cross_vec:Vector3
|
||||
if axis == MathUtil.Axis.Y:
|
||||
cross_vec = Vector3.FORWARD
|
||||
else:
|
||||
cross_vec = Vector3.UP
|
||||
|
||||
var u_axis:Vector3 = face.normal.cross(cross_vec)
|
||||
var v_axis:Vector3 = u_axis.cross(face.normal)
|
||||
var basis:Basis = Basis(u_axis, face.normal, v_axis)
|
||||
|
||||
var xform:Transform3D = Transform3D(basis, face.get_centroid())
|
||||
var xz_xform:Transform3D = xform.affine_inverse()
|
||||
|
||||
var tracker:FaceTracker = FaceTracker.new()
|
||||
tracker.face_index = f_idx
|
||||
faces.append(tracker)
|
||||
|
||||
for v_idx in face.vertex_indices:
|
||||
var v:ConvexVolume.VertexInfo = vol.vertices[v_idx]
|
||||
var proj:Vector3 = xz_xform * v.point
|
||||
tracker.points.append(Vector2(proj.x, proj.z))
|
||||
tracker.indices.append(v_idx)
|
||||
|
||||
#print("face init points %s" % tracker.points)
|
||||
|
||||
tracker.rotate_to_best_fit()
|
||||
#print("after rot %s" % tracker.points)
|
||||
tracker.fit_initial_rect()
|
||||
#print("after fit %s" % tracker.points)
|
||||
for p_idx in tracker.points.size():
|
||||
tracker.points[p_idx] += Vector2(margin, margin)
|
||||
tracker.bounds.size += Vector2(margin, margin) * 2
|
||||
|
||||
return pack_faces(faces)
|
||||
|
||||
|
447
addons/cyclops_level_builder/math/general_mesh.gd
Normal file
447
addons/cyclops_level_builder/math/general_mesh.gd
Normal file
@ -0,0 +1,447 @@
|
||||
# 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 GeneralMesh
|
||||
|
||||
|
||||
class VertexInfo extends RefCounted:
|
||||
var index:int
|
||||
var point:Vector3
|
||||
var edge_indices:Array[int] = []
|
||||
var selected:bool
|
||||
|
||||
func _init(_index:int, _point:Vector3 = Vector3.ZERO):
|
||||
index = _index
|
||||
point = _point
|
||||
|
||||
func _to_string():
|
||||
var s:String = "%s %s [" % [index, point]
|
||||
for i in edge_indices:
|
||||
s += "%s " % i
|
||||
s += "]"
|
||||
|
||||
return s
|
||||
|
||||
class EdgeInfo extends RefCounted:
|
||||
var index:int
|
||||
var start_index:int
|
||||
var end_index:int
|
||||
var face_indices:Array[int] = []
|
||||
var selected:bool
|
||||
|
||||
func _init(_index:int, _start:int = 0, _end:int = 0):
|
||||
index = _index
|
||||
start_index = _start
|
||||
end_index = _end
|
||||
|
||||
func _to_string():
|
||||
var s:String = "%s %s %s [" % [index, start_index, end_index]
|
||||
for i in face_indices:
|
||||
s += "%s " % i
|
||||
s += "]"
|
||||
return s
|
||||
|
||||
class FaceInfo extends RefCounted:
|
||||
var index:int
|
||||
var normal:Vector3
|
||||
# var vertex_indices:Array[int]
|
||||
var face_corner_indices:Array[int]
|
||||
var material_index:int
|
||||
var selected:bool
|
||||
|
||||
func _init(_index:int, _face_corner_indices:Array[int] = [], _mat_index:int = 0):
|
||||
index = _index
|
||||
face_corner_indices = _face_corner_indices
|
||||
material_index = _mat_index
|
||||
|
||||
func _to_string():
|
||||
var s:String = "%s %s %s [" % [index, normal, material_index]
|
||||
for i in face_corner_indices:
|
||||
s += "%s " % i
|
||||
s += "]"
|
||||
return s
|
||||
|
||||
class FaceCornerInfo extends RefCounted:
|
||||
var index:int
|
||||
var uv:Vector2
|
||||
var vertex_index:int
|
||||
var face_index:int
|
||||
var selected:bool
|
||||
|
||||
func _init(_index:int, _vertex_index:int, _face_index:int):
|
||||
vertex_index = _vertex_index
|
||||
face_index = _face_index
|
||||
|
||||
func _to_string():
|
||||
var s:String = "%s %s %s %s" % [index, uv, vertex_index, face_index]
|
||||
return s
|
||||
|
||||
|
||||
|
||||
var vertices:Array[VertexInfo] = []
|
||||
var edges:Array[EdgeInfo] = []
|
||||
var faces:Array[FaceInfo] = []
|
||||
var face_corners:Array[FaceCornerInfo] = []
|
||||
var bounds:AABB
|
||||
|
||||
#var points:PackedVector3Array
|
||||
|
||||
func _init():
|
||||
# init_block(Vector3.ZERO, Vector3.LEFT + Vector3.FORWARD, Vector3.UP)
|
||||
# dump()
|
||||
pass
|
||||
|
||||
func get_face_indices()->PackedInt32Array:
|
||||
var result:PackedInt32Array
|
||||
for f in faces:
|
||||
result.append(f.index)
|
||||
return result
|
||||
|
||||
func clear_lists():
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
face_corners = []
|
||||
bounds = AABB()
|
||||
|
||||
func init_block(block_bounds:AABB):
|
||||
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)
|
||||
|
||||
|
||||
func init_prism(base_points:Array[Vector3], extrude_dir:Vector3):
|
||||
|
||||
var verts:PackedVector3Array
|
||||
for p in base_points:
|
||||
verts.append(p)
|
||||
for p in base_points:
|
||||
verts.append(p + extrude_dir)
|
||||
|
||||
var index_list:PackedInt32Array
|
||||
var face_len_list:PackedInt32Array
|
||||
|
||||
var num_points:int = base_points.size()
|
||||
for i0 in num_points:
|
||||
var i1:int = wrap(i0 + 1, 0, num_points)
|
||||
|
||||
index_list.append(i0)
|
||||
index_list.append(i1)
|
||||
index_list.append(i1 + num_points)
|
||||
index_list.append(i0 + num_points)
|
||||
face_len_list.append(4)
|
||||
|
||||
for i0 in num_points:
|
||||
# index_list.append(i0)
|
||||
index_list.append(num_points - i0 - 1)
|
||||
face_len_list.append(num_points)
|
||||
|
||||
for i0 in num_points:
|
||||
index_list.append(i0 + num_points)
|
||||
# index_list.append(num_points * 2 - i0 - 1)
|
||||
face_len_list.append(num_points)
|
||||
|
||||
init_from_face_lists(verts, index_list, face_len_list)
|
||||
|
||||
|
||||
func init_from_face_lists(verts:PackedVector3Array, index_list:PackedInt32Array, face_len_list:PackedInt32Array):
|
||||
clear_lists()
|
||||
|
||||
for i in verts.size():
|
||||
var v:VertexInfo = VertexInfo.new(i, verts[i])
|
||||
vertices.append(v)
|
||||
|
||||
if i == 0:
|
||||
bounds = AABB(verts[0], Vector3.ZERO)
|
||||
else:
|
||||
bounds = bounds.expand(verts[i])
|
||||
|
||||
var vertex_index_offset:int = 0
|
||||
for face_index in face_len_list.size():
|
||||
var num_face_verts = face_len_list[face_index]
|
||||
# if num_face_verts < 3:
|
||||
# continue
|
||||
|
||||
var face_corners_local:Array[int] = []
|
||||
for i in num_face_verts:
|
||||
var face_corner_index:int = face_corners.size()
|
||||
var face_corner:FaceCornerInfo = FaceCornerInfo.new(face_corner_index, index_list[vertex_index_offset], face_index)
|
||||
face_corners.append(face_corner)
|
||||
face_corners_local.append(face_corner_index)
|
||||
vertex_index_offset += 1
|
||||
|
||||
var face:FaceInfo = FaceInfo.new(face_index, face_corners_local)
|
||||
faces.append(face)
|
||||
|
||||
#Calc normal
|
||||
var fc0:FaceCornerInfo = face_corners[face_corners_local[0]]
|
||||
# var vidx0 = fc0.vertex_index
|
||||
var p0:Vector3 = vertices[fc0.vertex_index].point
|
||||
#
|
||||
var weighted_normal:Vector3
|
||||
for i in range(1, num_face_verts - 1):
|
||||
var fc1:FaceCornerInfo = face_corners[face_corners_local[i]]
|
||||
var fc2:FaceCornerInfo = face_corners[face_corners_local[i + 1]]
|
||||
# var vidx1 = fc1.vertex_index
|
||||
# var vidx2 = fc2.vertex_index
|
||||
var p1:Vector3 = vertices[fc1.vertex_index].point
|
||||
var p2:Vector3 = vertices[fc2.vertex_index].point
|
||||
|
||||
var v1:Vector3 = p1 - p0
|
||||
var v2:Vector3 = p2 - p0
|
||||
weighted_normal += v2.cross(v1)
|
||||
|
||||
face.normal = weighted_normal.normalized()
|
||||
|
||||
#Calculate edges
|
||||
for face in faces:
|
||||
var num_corners = face.face_corner_indices.size()
|
||||
for i0 in num_corners:
|
||||
var i1:int = wrap(i0 + 1, 0, num_corners)
|
||||
var fc0:FaceCornerInfo = face_corners[face.face_corner_indices[i0]]
|
||||
var fc1:FaceCornerInfo = face_corners[face.face_corner_indices[i1]]
|
||||
|
||||
var edge:EdgeInfo = get_edge(fc0.vertex_index, fc1.vertex_index)
|
||||
if !edge:
|
||||
var edge_idx = edges.size()
|
||||
edge = EdgeInfo.new(edge_idx, fc0.vertex_index, fc1.vertex_index)
|
||||
edges.append(edge)
|
||||
|
||||
var v0:VertexInfo = vertices[fc0.vertex_index]
|
||||
v0.edge_indices.append(edge_idx)
|
||||
|
||||
var v1:VertexInfo = vertices[fc1.vertex_index]
|
||||
v1.edge_indices.append(edge_idx)
|
||||
|
||||
edge.face_indices.append(face.index)
|
||||
|
||||
|
||||
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 init_block_data(block:BlockData):
|
||||
clear_lists()
|
||||
|
||||
for i in block.points.size():
|
||||
var v:VertexInfo = VertexInfo.new(i, block.points[i])
|
||||
vertices.append(v)
|
||||
|
||||
if i == 0:
|
||||
bounds = AABB(v.point, Vector3.ZERO)
|
||||
else:
|
||||
bounds = bounds.expand(v.point)
|
||||
|
||||
var corner_index_offset:int = 0
|
||||
for face_index in block.face_vertex_count.size():
|
||||
var num_face_verts = block.face_vertex_count[face_index]
|
||||
|
||||
var face_corners_local:Array[int] = []
|
||||
for i in num_face_verts:
|
||||
var vertex_index = block.face_vertex_indices[corner_index_offset]
|
||||
|
||||
var face_corner:FaceCornerInfo = FaceCornerInfo.new(corner_index_offset, vertex_index, face_index)
|
||||
face_corner.uv = block.uvs[corner_index_offset]
|
||||
face_corners.append(face_corner)
|
||||
face_corners_local.append(corner_index_offset)
|
||||
corner_index_offset += 1
|
||||
|
||||
var face:FaceInfo = FaceInfo.new(face_index, face_corners_local)
|
||||
face.material_index = block.face_material_indices[face_index]
|
||||
faces.append(face)
|
||||
|
||||
#Calc normal
|
||||
var fc0:FaceCornerInfo = face_corners[face_corners_local[0]]
|
||||
var p0:Vector3 = vertices[fc0.vertex_index].point
|
||||
#
|
||||
var weighted_normal:Vector3
|
||||
for i in range(1, num_face_verts - 1):
|
||||
var fc1:FaceCornerInfo = face_corners[face_corners_local[i]]
|
||||
var fc2:FaceCornerInfo = face_corners[face_corners_local[i + 1]]
|
||||
var p1:Vector3 = vertices[fc1.vertex_index].point
|
||||
var p2:Vector3 = vertices[fc2.vertex_index].point
|
||||
|
||||
var v1:Vector3 = p1 - p0
|
||||
var v2:Vector3 = p2 - p0
|
||||
weighted_normal += v2.cross(v1)
|
||||
|
||||
face.normal = weighted_normal.normalized()
|
||||
|
||||
#Calculate edges
|
||||
for face in faces:
|
||||
var num_corners = face.face_corner_indices.size()
|
||||
for i0 in num_corners:
|
||||
var i1:int = wrap(i0 + 1, 0, num_corners)
|
||||
var fc0:FaceCornerInfo = face_corners[face.face_corner_indices[i0]]
|
||||
var fc1:FaceCornerInfo = face_corners[face.face_corner_indices[i1]]
|
||||
|
||||
var edge:EdgeInfo = get_edge(fc0.vertex_index, fc1.vertex_index)
|
||||
if !edge:
|
||||
var edge_idx = edges.size()
|
||||
edge = EdgeInfo.new(edge_idx, fc0.vertex_index, fc1.vertex_index)
|
||||
edges.append(edge)
|
||||
|
||||
var v0:VertexInfo = vertices[fc0.vertex_index]
|
||||
v0.edge_indices.append(edge_idx)
|
||||
|
||||
var v1:VertexInfo = vertices[fc1.vertex_index]
|
||||
v1.edge_indices.append(edge_idx)
|
||||
|
||||
edge.face_indices.append(face.index)
|
||||
|
||||
|
||||
func to_block_data()->BlockData:
|
||||
var block:BlockData = preload("res://addons/cyclops_level_builder/resources/block_data.gd").new()
|
||||
# var block:BlockData = BlockData.new()
|
||||
|
||||
for v in vertices:
|
||||
block.points.append(v.point)
|
||||
|
||||
for f in faces:
|
||||
block.face_vertex_count.append(f.face_corner_indices.size())
|
||||
block.face_material_indices.append(f.material_index)
|
||||
|
||||
for fc_idx in f.face_corner_indices:
|
||||
var fc:FaceCornerInfo = face_corners[fc_idx]
|
||||
block.face_vertex_indices.append(fc.vertex_index)
|
||||
block.uvs.append(fc.uv)
|
||||
|
||||
return block
|
||||
|
||||
func append_mesh(mesh:ImmediateMesh, material:Material, color:Color = Color.WHITE):
|
||||
|
||||
for face in faces:
|
||||
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, material)
|
||||
# print("face %s" % face.index)
|
||||
|
||||
mesh.surface_set_normal(face.normal)
|
||||
|
||||
var num_corners:int = face.face_corner_indices.size()
|
||||
for i in num_corners:
|
||||
var idx = (i + 1) / 2 if i & 1 else wrap(num_corners - (i / 2), 0, num_corners)
|
||||
var fc:FaceCornerInfo = face_corners[face.face_corner_indices[idx]]
|
||||
|
||||
mesh.surface_set_color(color)
|
||||
mesh.surface_set_uv(fc.uv)
|
||||
mesh.surface_add_vertex(vertices[fc.vertex_index].point)
|
||||
# print ("%s %s %s" % [idx, fc.vertex_index, control_mesh.vertices[fc.vertex_index].point])
|
||||
|
||||
mesh.surface_end()
|
||||
|
||||
func triplanar_unwrap(scale:float = 1):
|
||||
for fc in face_corners:
|
||||
var v:VertexInfo = vertices[fc.vertex_index]
|
||||
var f:FaceInfo = faces[fc.face_index]
|
||||
|
||||
if abs(f.normal.x) > abs(f.normal.y) && abs(f.normal.x) > abs(f.normal.z):
|
||||
fc.uv = Vector2(v.point.y, v.point.z) * scale
|
||||
elif abs(f.normal.y) > abs(f.normal.z):
|
||||
fc.uv = Vector2(v.point.x, v.point.z) * scale
|
||||
else:
|
||||
fc.uv = Vector2(v.point.x, v.point.y) * scale
|
||||
|
||||
|
||||
func get_face_points(face:FaceInfo)->PackedVector3Array:
|
||||
var points:PackedVector3Array
|
||||
for fc_idx in face.face_corner_indices:
|
||||
var fc:FaceCornerInfo = face_corners[fc_idx]
|
||||
points.append(vertices[fc.vertex_index].point)
|
||||
return points
|
||||
|
||||
func triangulate_face(face:FaceInfo)->PackedVector3Array:
|
||||
var points:PackedVector3Array = get_face_points(face)
|
||||
return MathUtil.trianglate_face(points, face.normal)
|
||||
|
||||
|
||||
func intersect_ray_closest(origin:Vector3, dir:Vector3)->IntersectResults:
|
||||
if bounds.intersects_ray(origin, dir) == null:
|
||||
return null
|
||||
|
||||
var best_result:IntersectResults
|
||||
|
||||
for f in faces:
|
||||
var tris:PackedVector3Array = triangulate_face(f)
|
||||
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_index = f.index
|
||||
result.normal = f.normal
|
||||
result.position = p_hit
|
||||
result.distance_squared = dist_sq
|
||||
|
||||
best_result = result
|
||||
|
||||
return best_result
|
||||
|
||||
func translate(offset:Vector3):
|
||||
for v in vertices:
|
||||
v.point += offset
|
||||
|
||||
func dump():
|
||||
print ("Verts")
|
||||
for v in vertices:
|
||||
print(v.to_string())
|
||||
print ("Edges")
|
||||
for e in edges:
|
||||
print(e.to_string())
|
||||
print ("Faces")
|
||||
for f in faces:
|
||||
print(f.to_string())
|
||||
print ("Face Corners")
|
||||
for f in face_corners:
|
||||
print(f.to_string())
|
63
addons/cyclops_level_builder/math/geometry_mesh.gd
Normal file
63
addons/cyclops_level_builder/math/geometry_mesh.gd
Normal file
@ -0,0 +1,63 @@
|
||||
# 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 GeometryMesh
|
||||
|
||||
var coords:PackedVector3Array
|
||||
var normals:PackedVector3Array
|
||||
var uvs:PackedVector2Array
|
||||
|
||||
func transform(xform:Transform3D)->GeometryMesh:
|
||||
var result:GeometryMesh = GeometryMesh.new()
|
||||
|
||||
var basis:Basis = xform.basis
|
||||
basis = basis.inverse()
|
||||
basis = basis.transposed()
|
||||
|
||||
for i in coords.size():
|
||||
result.coords.append(xform * coords[i])
|
||||
result.uvs.append(uvs[i])
|
||||
result.normals.append(basis * normals[i])
|
||||
|
||||
return result
|
||||
|
||||
func append_to_immediate_mesh(mesh:ImmediateMesh, material:Material, xform:Transform3D = Transform3D.IDENTITY):
|
||||
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES, material)
|
||||
|
||||
var basis:Basis = xform.basis
|
||||
basis = basis.inverse()
|
||||
basis = basis.transposed()
|
||||
|
||||
for i in coords.size():
|
||||
var normal:Vector3 = basis * normals[i]
|
||||
var coord:Vector3 = xform * coords[i]
|
||||
var uv:Vector2 = uvs[i]
|
||||
|
||||
mesh.surface_set_normal(normal)
|
||||
mesh.surface_set_uv(uv)
|
||||
mesh.surface_add_vertex(coord)
|
||||
|
||||
mesh.surface_end()
|
||||
|
166
addons/cyclops_level_builder/math/math_geometry.gd
Normal file
166
addons/cyclops_level_builder/math/math_geometry.gd
Normal file
@ -0,0 +1,166 @@
|
||||
# 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 MathGeometry
|
||||
|
||||
|
||||
static func unit_cylinder(segs:int = 16, radius0:float = 1, radius1:float = 1, top_height:float = 1, bottom_height:float = -1, bottom_cap:bool = false, top_cap:bool = false)->GeometryMesh:
|
||||
var mesh:GeometryMesh = GeometryMesh.new()
|
||||
|
||||
|
||||
var vc0:Vector3 = Vector3(0, 0, -1)
|
||||
var vc1:Vector3 = Vector3(0, 0, 1)
|
||||
var uvc:Vector2 = Vector2(.5, .5)
|
||||
|
||||
for s in range(segs):
|
||||
|
||||
var sin0:float = sin(deg_to_rad(360 * s / segs))
|
||||
var cos0:float = cos(deg_to_rad(360 * s / segs))
|
||||
var sin1:float = sin(deg_to_rad(360 * (s + 1) / segs))
|
||||
var cos1:float = cos(deg_to_rad(360 * (s + 1) / segs))
|
||||
|
||||
var v00:Vector3 = Vector3(sin0 * radius0, cos0 * radius0, bottom_height)
|
||||
var v10:Vector3 = Vector3(sin1 * radius0, cos1 * radius0, bottom_height)
|
||||
var v01:Vector3 = Vector3(sin0 * radius1, cos0 * radius1, top_height)
|
||||
var v11:Vector3 = Vector3(sin1 * radius1, cos1 * radius1, top_height)
|
||||
|
||||
var tan0:Vector3 = Vector3(cos0, sin0, 0)
|
||||
var n00:Vector3 = (v01 - v00).cross(tan0)
|
||||
n00 = n00.normalized()
|
||||
var n01:Vector3 = n00
|
||||
var tan1:Vector3 = Vector3(cos1, sin1, 0)
|
||||
var n10:Vector3 = (v11 - v10).cross(tan1)
|
||||
n10 = n10.normalized()
|
||||
var n11 = n10
|
||||
|
||||
var uv00:Vector2 = Vector2(s / segs, 0)
|
||||
var uv10:Vector2 = Vector2((s + 1) / segs, 0)
|
||||
var uv01:Vector2 = Vector2(s / segs, 1)
|
||||
var uv11:Vector2 = Vector2((s + 1) / segs, 1)
|
||||
|
||||
if radius0 != 0:
|
||||
mesh.coords.append(v00)
|
||||
mesh.coords.append(v10)
|
||||
mesh.coords.append(v11)
|
||||
|
||||
mesh.normals.append(n00)
|
||||
mesh.normals.append(n10)
|
||||
mesh.normals.append(n11)
|
||||
|
||||
mesh.uvs.append(uv00)
|
||||
mesh.uvs.append(uv10)
|
||||
mesh.uvs.append(uv11)
|
||||
|
||||
if radius1 != 0:
|
||||
mesh.coords.append(v00)
|
||||
mesh.coords.append(v11)
|
||||
mesh.coords.append(v01)
|
||||
|
||||
mesh.normals.append(n00)
|
||||
mesh.normals.append(n11)
|
||||
mesh.normals.append(n01)
|
||||
|
||||
mesh.uvs.append(uv00)
|
||||
mesh.uvs.append(uv11)
|
||||
mesh.uvs.append(uv01)
|
||||
|
||||
if top_cap and radius1 != 0:
|
||||
mesh.coords.append(v01)
|
||||
mesh.coords.append(v11)
|
||||
mesh.coords.append(vc1)
|
||||
|
||||
mesh.normals.append(Vector3(0, 0, 1))
|
||||
mesh.normals.append(Vector3(0, 0, 1))
|
||||
mesh.normals.append(Vector3(0, 0, 1))
|
||||
|
||||
mesh.uvs.append(Vector2(sin0, cos0))
|
||||
mesh.uvs.append(Vector2(sin1, cos1))
|
||||
mesh.uvs.append(uvc)
|
||||
|
||||
if bottom_cap and radius0 != 0:
|
||||
mesh.coords.append(v00)
|
||||
mesh.coords.append(v10)
|
||||
mesh.coords.append(vc0)
|
||||
|
||||
mesh.normals.append(-Vector3(0, 0, 1))
|
||||
mesh.normals.append(-Vector3(0, 0, 1))
|
||||
mesh.normals.append(-Vector3(0, 0, 1))
|
||||
|
||||
mesh.uvs.append(Vector2(sin0, cos0))
|
||||
mesh.uvs.append(Vector2(sin1, cos1))
|
||||
mesh.uvs.append(uvc)
|
||||
|
||||
|
||||
return mesh
|
||||
|
||||
static func unit_sphere(segs_lat:int = 6, segs_long:int = 8)->GeometryMesh:
|
||||
var mesh:GeometryMesh = GeometryMesh.new()
|
||||
|
||||
for la in range(segs_lat):
|
||||
|
||||
var z0:float = cos(deg_to_rad(180 * la / segs_lat))
|
||||
var z1:float = cos(deg_to_rad(180 * (la + 1) / segs_lat))
|
||||
var r0:float = sin(deg_to_rad(180 * la / segs_lat))
|
||||
var r1:float = sin(deg_to_rad(180 * (la + 1) / segs_lat))
|
||||
|
||||
for lo in range(segs_long):
|
||||
var cx0:float = sin(deg_to_rad(360 * lo / segs_long))
|
||||
var cx1:float = sin(deg_to_rad(360 * (lo + 1) / segs_long))
|
||||
var cy0:float = cos(deg_to_rad(360 * lo / segs_long))
|
||||
var cy1:float = cos(deg_to_rad(360 * (lo + 1) / segs_long))
|
||||
|
||||
var v00:Vector3 = Vector3(cx0 * r0, cy0 * r0, z0)
|
||||
var v10:Vector3 = Vector3(cx1 * r0, cy1 * r0, z0)
|
||||
var v01:Vector3 = Vector3(cx0 * r1, cy0 * r1, z1)
|
||||
var v11:Vector3 = Vector3(cx1 * r1, cy1 * r1, z1)
|
||||
|
||||
if la != 0:
|
||||
mesh.coords.append(v00)
|
||||
mesh.coords.append(v11)
|
||||
mesh.coords.append(v10)
|
||||
|
||||
mesh.normals.append(v00)
|
||||
mesh.normals.append(v10)
|
||||
mesh.normals.append(v10)
|
||||
|
||||
mesh.uvs.append(Vector2(lo / segs_long, la / segs_lat))
|
||||
mesh.uvs.append(Vector2((lo + 1) / segs_long, la / segs_lat))
|
||||
mesh.uvs.append(Vector2((lo + 1) / segs_long, (la + 1) / segs_lat))
|
||||
|
||||
if la != segs_lat - 1:
|
||||
mesh.coords.append(v00)
|
||||
mesh.coords.append(v01)
|
||||
mesh.coords.append(v11)
|
||||
|
||||
mesh.normals.append(v00)
|
||||
mesh.normals.append(v01)
|
||||
mesh.normals.append(v11)
|
||||
|
||||
mesh.uvs.append(Vector2(lo / segs_long, la / segs_lat))
|
||||
mesh.uvs.append(Vector2((lo + 1) / segs_long, (la + 1) / segs_lat))
|
||||
mesh.uvs.append(Vector2(lo / segs_long, (la + 1) / segs_lat))
|
||||
|
||||
return mesh
|
||||
|
537
addons/cyclops_level_builder/math/math_util.gd
Normal file
537
addons/cyclops_level_builder/math/math_util.gd
Normal file
@ -0,0 +1,537 @@
|
||||
# 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
|
||||
class_name MathUtil
|
||||
|
||||
enum Axis { X, Y, Z }
|
||||
|
||||
static func square(value:float)->float:
|
||||
return value * value
|
||||
|
||||
static func snap_to_grid(pos:Vector3, cell_size:float)->Vector3:
|
||||
# return floor(pos / cell_size) * cell_size
|
||||
return floor((pos + Vector3(cell_size, cell_size, cell_size) / 2) / cell_size) * cell_size
|
||||
|
||||
|
||||
#Returns intersection of line with point.
|
||||
# plane_perp_dir points in direction of plane's normal and does not need to be normalized
|
||||
static func intersect_plane(ray_origin:Vector3, ray_dir:Vector3, plane_origin:Vector3, plane_perp_dir:Vector3)->Vector3:
|
||||
var s:float = (plane_origin - ray_origin).dot(plane_perp_dir) / ray_dir.dot(plane_perp_dir)
|
||||
return ray_origin + ray_dir * s
|
||||
|
||||
#Returns the closest point on the line to the ray
|
||||
static func closest_point_on_line(ray_origin:Vector3, ray_dir:Vector3, line_origin:Vector3, line_dir:Vector3)->Vector3:
|
||||
var a:Vector3 = ray_dir.cross(line_dir)
|
||||
var w_perp:Vector3 = ray_dir.cross(a)
|
||||
return intersect_plane(line_origin, line_dir, ray_origin, w_perp)
|
||||
|
||||
static func closest_point_on_segment(ray_origin:Vector3, ray_dir:Vector3, seg_start:Vector3, seg_end:Vector3)->Vector3:
|
||||
var seg_span:Vector3 = seg_end - seg_start
|
||||
var p:Vector3 = closest_point_on_line(ray_origin, ray_dir, seg_start, seg_span)
|
||||
var offset:Vector3 = p - seg_start
|
||||
if offset.dot(seg_span) < 0:
|
||||
return seg_start
|
||||
if offset.length_squared() > seg_span.length_squared():
|
||||
return seg_end
|
||||
return p
|
||||
|
||||
#Shortest distance from point to given ray. Returns NAN if point is behind origin of ray.
|
||||
static func distance_to_ray(ray_origin:Vector3, ray_dir:Vector3, point:Vector3):
|
||||
var offset = point - ray_origin
|
||||
var parallel:Vector3 = offset.project(ray_dir)
|
||||
if parallel.dot(ray_dir) < 0:
|
||||
return NAN
|
||||
|
||||
var perp:Vector3 = offset - parallel
|
||||
return perp.length()
|
||||
|
||||
|
||||
static func trianglate_face(points:PackedVector3Array, normal:Vector3)->PackedVector3Array:
|
||||
var result:PackedVector3Array
|
||||
|
||||
while (points.size() >= 3):
|
||||
var num_points:int = points.size()
|
||||
for i in range(0, num_points):
|
||||
var p0:Vector3 = points[i]
|
||||
var p1:Vector3 = points[wrap(i + 1, 0, num_points)]
|
||||
var p2:Vector3 = points[wrap(i + 2, 0, num_points)]
|
||||
|
||||
#Godot uses clockwise winding
|
||||
var tri_norm_dir:Vector3 = (p2 - p0).cross(p1 - p0)
|
||||
if tri_norm_dir.dot(normal) > 0:
|
||||
result.append(p0)
|
||||
result.append(p1)
|
||||
result.append(p2)
|
||||
|
||||
points.remove_at(i + 1)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
static func trianglate_face_indices(points:PackedVector3Array, indices:Array[int], normal:Vector3)->Array[int]:
|
||||
var result:Array[int] = []
|
||||
|
||||
# print("trianglate_face_indices %s" % points)
|
||||
|
||||
while (points.size() >= 3):
|
||||
var num_points:int = points.size()
|
||||
var added_point:bool = false
|
||||
|
||||
for i in range(0, num_points):
|
||||
var idx0:int = i
|
||||
var idx1:int = wrap(i + 1, 0, num_points)
|
||||
var idx2:int = wrap(i + 2, 0, num_points)
|
||||
var p0:Vector3 = points[idx0]
|
||||
var p1:Vector3 = points[idx1]
|
||||
var p2:Vector3 = points[idx2]
|
||||
|
||||
#Godot uses clockwise winding
|
||||
var tri_norm_dir:Vector3 = (p2 - p0).cross(p1 - p0)
|
||||
if tri_norm_dir.dot(normal) > 0:
|
||||
result.append(indices[idx0])
|
||||
result.append(indices[idx1])
|
||||
result.append(indices[idx2])
|
||||
|
||||
# print("adding indices %s %s %s" % [indices[idx0], indices[idx1], indices[idx2]])
|
||||
|
||||
points.remove_at(idx1)
|
||||
indices.remove_at(idx1)
|
||||
added_point = true
|
||||
break
|
||||
|
||||
assert(added_point, "failed to add point in triangulation")
|
||||
# print("tri_done %s" % str(result))
|
||||
|
||||
return result
|
||||
|
||||
static func trianglate_face_vertex_indices(points:PackedVector3Array, normal:Vector3)->Array[int]:
|
||||
var result:Array[int] = []
|
||||
var fv_indices:Array = range(0, points.size())
|
||||
# print("trianglate_face_indices %s" % points)
|
||||
|
||||
while (points.size() >= 3):
|
||||
var num_points:int = points.size()
|
||||
var added_point:bool = false
|
||||
|
||||
for i in range(0, num_points):
|
||||
var idx0:int = i
|
||||
var idx1:int = wrap(i + 1, 0, num_points)
|
||||
var idx2:int = wrap(i + 2, 0, num_points)
|
||||
var p0:Vector3 = points[idx0]
|
||||
var p1:Vector3 = points[idx1]
|
||||
var p2:Vector3 = points[idx2]
|
||||
|
||||
#Godot uses clockwise winding
|
||||
var tri_norm_dir:Vector3 = (p2 - p0).cross(p1 - p0)
|
||||
if tri_norm_dir.dot(normal) > 0:
|
||||
result.append(fv_indices[idx0])
|
||||
result.append(fv_indices[idx1])
|
||||
result.append(fv_indices[idx2])
|
||||
|
||||
# print("adding indices %s %s %s" % [indices[idx0], indices[idx1], indices[idx2]])
|
||||
|
||||
points.remove_at(idx1)
|
||||
fv_indices.remove_at(idx1)
|
||||
added_point = true
|
||||
break
|
||||
|
||||
assert(added_point, "failed to add point in triangulation")
|
||||
# print("tri_done %s" % str(result))
|
||||
|
||||
return result
|
||||
|
||||
static func flip_plane(plane:Plane)->Plane:
|
||||
return Plane(-plane.normal, plane.get_center())
|
||||
|
||||
#Returns a vector pointing along the normal in the clockwise winding direction with a length equal to twice the area of the triangle
|
||||
static func triangle_area_x2(p0:Vector3, p1:Vector3, p2:Vector3)->Vector3:
|
||||
return (p2 - p0).cross(p1 - p0)
|
||||
|
||||
#Returns a vector pointing along the normal in the clockwise winding direction with a lengh equal to twice the area of the face
|
||||
static func face_area_x2(points:PackedVector3Array)->Vector3:
|
||||
if points.size() <= 1:
|
||||
return Vector3.ZERO
|
||||
|
||||
var result:Vector3
|
||||
var p0:Vector3 = points[0]
|
||||
|
||||
for i in range(1, points.size() - 1):
|
||||
var p1:Vector3 = points[i]
|
||||
var p2:Vector3 = points[i + 1]
|
||||
|
||||
result += (p2 - p0).cross(p1 - p0)
|
||||
|
||||
return result
|
||||
|
||||
static func face_area_x2_2d(points:PackedVector2Array)->float:
|
||||
if points.size() <= 1:
|
||||
return 0
|
||||
|
||||
var result:float
|
||||
var p0:Vector2 = points[0]
|
||||
|
||||
for i in range(1, points.size() - 1):
|
||||
var p1:Vector2 = points[i]
|
||||
var p2:Vector2 = points[i + 1]
|
||||
|
||||
result += triange_area_2x_2d(p1 - p0, p2 - p0)
|
||||
|
||||
return result
|
||||
|
||||
static func fit_plane(points:PackedVector3Array)->Plane:
|
||||
var normal:Vector3 = face_area_x2(points).normalized()
|
||||
return Plane(normal, points[0])
|
||||
|
||||
static func snap_to_best_axis_normal(vector:Vector3)->Vector3:
|
||||
if abs(vector.x) > abs(vector.y) and abs(vector.x) > abs(vector.z):
|
||||
return Vector3(1, 0, 0) if vector.x > 0 else Vector3(-1, 0, 0)
|
||||
elif abs(vector.y) > abs(vector.z):
|
||||
return Vector3(0, 1, 0) if vector.y > 0 else Vector3(0, -1, 0)
|
||||
else:
|
||||
return Vector3(0, 0, 1) if vector.z > 0 else Vector3(0, 0, -1)
|
||||
|
||||
static func get_longest_axis(vector:Vector3)->Axis:
|
||||
if abs(vector.x) > abs(vector.y) and abs(vector.x) > abs(vector.z):
|
||||
return Axis.X
|
||||
elif abs(vector.y) > abs(vector.z):
|
||||
return Axis.Y
|
||||
else:
|
||||
return Axis.Z
|
||||
|
||||
static func calc_bounds(points:PackedVector3Array)->AABB:
|
||||
if points.is_empty():
|
||||
return AABB(Vector3.ZERO, Vector3.ZERO)
|
||||
|
||||
var result:AABB = AABB(points[0], Vector3.ZERO)
|
||||
for i in range(1, points.size()):
|
||||
result = result.expand(points[i])
|
||||
return result
|
||||
|
||||
#Returns value equal to twise the area between the two vectors. Clockwise windings have negative area
|
||||
static func triange_area_2x_2d(a:Vector2, b:Vector2)->float:
|
||||
return a.x * b.y - a.y * b.x
|
||||
|
||||
#Finds the bouding polygons of this set of points with a clockwise winding
|
||||
static func bounding_polygon_2d(base_points:PackedVector2Array)->PackedVector2Array:
|
||||
if base_points.size() <= 2:
|
||||
return base_points
|
||||
|
||||
|
||||
#Start with leftmost vertex, topmost if more than one
|
||||
var p_init:Vector2 = base_points[0]
|
||||
for p in base_points:
|
||||
if p.x < p_init.x or (p.x == p_init.x and p.y > p_init.y):
|
||||
p_init = p
|
||||
|
||||
|
||||
var p_cur:Vector2 = p_init
|
||||
var last_segment_dir = Vector2(0, 1)
|
||||
|
||||
var polygon:PackedVector2Array
|
||||
|
||||
while true:
|
||||
var best_point:Vector2
|
||||
var best_dir:Vector2
|
||||
var best_angle:float = 0
|
||||
|
||||
for p in base_points:
|
||||
if p.is_equal_approx(p_cur):
|
||||
continue
|
||||
|
||||
var point_dir:Vector2 = (p - p_cur).normalized()
|
||||
var angle:float = acos(-last_segment_dir.dot(point_dir))
|
||||
|
||||
if angle > best_angle or (angle == best_angle and p_cur.distance_squared_to(p) > p_cur.distance_squared_to(best_point)):
|
||||
best_point = p
|
||||
best_dir = point_dir
|
||||
best_angle = angle
|
||||
|
||||
p_cur = best_point
|
||||
last_segment_dir = best_dir
|
||||
polygon.append(best_point)
|
||||
|
||||
if best_point.is_equal_approx(p_init):
|
||||
break
|
||||
|
||||
return polygon
|
||||
|
||||
#static func bounding_polygon(base_points:PackedVector3Array, plane:Plane)->PackedVector3Array:
|
||||
static func bounding_polygon_3d(base_points:PackedVector3Array, normal:Vector3)->PackedVector3Array:
|
||||
if base_points.size() <= 2:
|
||||
return base_points
|
||||
|
||||
var quat:Quaternion = Quaternion(normal, Vector3.FORWARD)
|
||||
|
||||
# var xform:Transform3D = Transform3D(Basis(quat), -base_points[0])
|
||||
var xform:Transform3D = Transform3D(Basis(quat))
|
||||
xform = xform.translated_local(-base_points[0])
|
||||
var xform_inv = xform.inverse()
|
||||
|
||||
#print("xform %s" % xform)
|
||||
|
||||
var points_local:PackedVector2Array
|
||||
|
||||
for p in base_points:
|
||||
var p_local = xform * p
|
||||
points_local.append(Vector2(p_local.x, p_local.y))
|
||||
|
||||
var points_bounds:PackedVector2Array = bounding_polygon_2d(points_local)
|
||||
|
||||
var result:PackedVector3Array
|
||||
for p in points_bounds:
|
||||
var p_result = xform_inv * Vector3(p.x, p.y, 0)
|
||||
result.append(p_result)
|
||||
|
||||
return result
|
||||
|
||||
static func points_are_colinear(points:PackedVector3Array)->bool:
|
||||
if points.size() <= 2:
|
||||
return true
|
||||
|
||||
var p0:Vector3 = points[0]
|
||||
var p1:Vector3 = p0
|
||||
var index:int = 0
|
||||
for i in range(1, points.size()):
|
||||
if !points[i].is_equal_approx(p0):
|
||||
p1 = points[i]
|
||||
index = i
|
||||
break
|
||||
|
||||
if index == 0:
|
||||
return true
|
||||
|
||||
var v10:Vector3 = p1 - p0
|
||||
for i in range(index + 1, points.size()):
|
||||
if !triangle_area_x2(p0, p1, points[i]).is_zero_approx():
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
static func furthest_point_from_line(line_origin:Vector3, line_dir:Vector3, points:PackedVector3Array)->Vector3:
|
||||
var best_point:Vector3
|
||||
var best_dist:float = 0
|
||||
|
||||
for p in points:
|
||||
var offset:Vector3 = p - line_origin
|
||||
var along:Vector3 = offset.project(line_dir)
|
||||
var perp:Vector3 = offset - along
|
||||
var dist:float = perp.length_squared()
|
||||
if dist > best_dist:
|
||||
best_dist = dist
|
||||
best_point = p
|
||||
|
||||
return best_point
|
||||
|
||||
static func furthest_point_from_plane(plane:Plane, points:PackedVector3Array)->Vector3:
|
||||
var best_point:Vector3
|
||||
var best_distance:float = 0
|
||||
|
||||
for p in points:
|
||||
var dist = abs(plane.distance_to(p))
|
||||
if dist > best_distance:
|
||||
best_point = p
|
||||
best_distance = dist
|
||||
|
||||
return best_point
|
||||
|
||||
static func planar_volume_contains_point(planes:Array[Plane], point:Vector3)->bool:
|
||||
# print("candidate %s" % point)
|
||||
|
||||
for p in planes:
|
||||
var is_over:bool = p.is_point_over(point)
|
||||
var is_on:bool = p.has_point(point)
|
||||
if !is_over && !is_on:
|
||||
# print("reject by %s" % p)
|
||||
return false
|
||||
# print("passed %s" % point)
|
||||
return true
|
||||
|
||||
static func get_convex_hull_points_from_planes(planes:Array[Plane])->Array[Vector3]:
|
||||
#Check for overlapping planes
|
||||
for i0 in range(0, planes.size()):
|
||||
for i1 in range(i0 + 1, planes.size()):
|
||||
var p0:Plane = planes[i0]
|
||||
var p1:Plane = flip_plane(planes[i1])
|
||||
if p0.is_equal_approx(p1):
|
||||
return []
|
||||
|
||||
var points:Array[Vector3]
|
||||
|
||||
for i0 in range(0, planes.size()):
|
||||
for i1 in range(i0 + 1, planes.size()):
|
||||
for i2 in range(i1 + 1, planes.size()):
|
||||
var result = planes[i0].intersect_3(planes[i1], planes[i2])
|
||||
|
||||
if result == null:
|
||||
continue
|
||||
#print("candidate %s" % result)
|
||||
if !planar_volume_contains_point(planes, result):
|
||||
continue
|
||||
if points.any(func(p):return p.is_equal_approx(result)):
|
||||
continue
|
||||
#print("adding %s" % result)
|
||||
points.append(result)
|
||||
|
||||
return points
|
||||
|
||||
static func dist_to_segment_squared_2d(point:Vector2, seg_start:Vector2, seg_end:Vector2)->float:
|
||||
var dist_sq_p0:float = point.distance_squared_to(seg_start)
|
||||
var dist_sq_p1:float = point.distance_squared_to(seg_end)
|
||||
var seg_span:Vector2 = seg_end - seg_start
|
||||
|
||||
var offset:Vector2 = point - seg_start
|
||||
var offset_proj:Vector2 = offset.project(seg_span)
|
||||
var perp_dist_sq:float = (offset - offset_proj).length_squared()
|
||||
|
||||
if seg_span.dot(offset) < 0:
|
||||
return dist_sq_p0
|
||||
elif offset_proj.length_squared() > seg_span.length_squared():
|
||||
return dist_sq_p1
|
||||
return perp_dist_sq
|
||||
|
||||
class Segment2d extends RefCounted:
|
||||
var p0:Vector2
|
||||
var p1:Vector2
|
||||
|
||||
func _init(p0:Vector2, p1:Vector2):
|
||||
self.p0 = p0
|
||||
self.p1 = p1
|
||||
|
||||
func reverse()->Segment2d:
|
||||
return Segment2d.new(p1, p0)
|
||||
|
||||
func _to_string():
|
||||
return "[%s %s]" % [p0, p1]
|
||||
|
||||
static func extract_loop_2d(seg_stack:Array[Segment2d])->PackedVector2Array:
|
||||
var segs_sorted:Array[Segment2d] = []
|
||||
var prev_seg = seg_stack.pop_back()
|
||||
segs_sorted.append(prev_seg)
|
||||
|
||||
while !seg_stack.is_empty():
|
||||
var found_seg:bool = false
|
||||
for s_idx in seg_stack.size():
|
||||
var cur_seg:Segment2d = seg_stack[s_idx]
|
||||
|
||||
if cur_seg.p0.is_equal_approx(prev_seg.p1):
|
||||
# print("matching %s with %s" % [prev_seg, cur_seg])
|
||||
segs_sorted.append(cur_seg)
|
||||
seg_stack.remove_at(s_idx)
|
||||
prev_seg = cur_seg
|
||||
found_seg = true
|
||||
break
|
||||
elif cur_seg.p1.is_equal_approx(prev_seg.p1):
|
||||
# print("matching %s with %s" % [prev_seg, cur_seg])
|
||||
cur_seg = cur_seg.reverse()
|
||||
segs_sorted.append(cur_seg)
|
||||
seg_stack.remove_at(s_idx)
|
||||
prev_seg = cur_seg
|
||||
found_seg = true
|
||||
break
|
||||
|
||||
if !found_seg:
|
||||
# push_warning("loop not continuous")
|
||||
break
|
||||
|
||||
# print("segs_sorted %s" % str(segs_sorted))
|
||||
|
||||
var result:PackedVector2Array
|
||||
for s in segs_sorted:
|
||||
result.append(s.p0)
|
||||
|
||||
if face_area_x2_2d(result) < 0:
|
||||
result.reverse()
|
||||
|
||||
return result
|
||||
|
||||
static func get_loops_from_segments_2d(segments:PackedVector2Array)->Array[PackedVector2Array]:
|
||||
#print("segments %s" % segments)
|
||||
var loops:Array[PackedVector2Array] = []
|
||||
|
||||
var seg_stack:Array[Segment2d] = []
|
||||
for i in range(0, segments.size(), 2):
|
||||
seg_stack.append(Segment2d.new(segments[i], segments[i + 1]))
|
||||
|
||||
# print("segs %s" % str(seg_stack))
|
||||
|
||||
while !seg_stack.is_empty():
|
||||
var loop:PackedVector2Array = extract_loop_2d(seg_stack)
|
||||
loops.append(loop)
|
||||
|
||||
# print("result %s" % str(result))
|
||||
return loops
|
||||
|
||||
static func create_transform(translation:Vector3, rotation_axis:Vector3, rotation_angle:float, scale:Vector3, pivot:Vector3)->Transform3D:
|
||||
var xform:Transform3D = Transform3D.IDENTITY
|
||||
|
||||
xform = xform.translated_local(pivot + translation)
|
||||
xform = xform.rotated_local(rotation_axis, rotation_angle)
|
||||
xform = xform.scaled_local(scale)
|
||||
xform = xform.translated_local(-pivot)
|
||||
|
||||
return xform
|
||||
|
||||
static func create_circle_points(center:Vector3, normal:Vector3, radius:float, num_segments:int)->PackedVector3Array:
|
||||
var result:PackedVector3Array
|
||||
|
||||
var axis:Axis = get_longest_axis(normal)
|
||||
var perp_normal:Vector3
|
||||
match axis:
|
||||
Axis.X:
|
||||
perp_normal = normal.cross(Vector3.UP)
|
||||
Axis.Y:
|
||||
perp_normal = normal.cross(Vector3.FORWARD)
|
||||
Axis.Z:
|
||||
perp_normal = normal.cross(Vector3.UP)
|
||||
|
||||
var angle_incrment = (PI * 2 / num_segments)
|
||||
for i in num_segments:
|
||||
var offset:Vector3 = perp_normal.rotated(normal, i * angle_incrment)
|
||||
result.append(offset * radius + center)
|
||||
|
||||
return result
|
||||
|
||||
static func get_axis_aligned_tangent_and_binormal(normal:Vector3)->Array[Vector3]:
|
||||
var axis:MathUtil.Axis = MathUtil.get_longest_axis(normal)
|
||||
#calc tangent and binormal
|
||||
var u_normal:Vector3
|
||||
var v_normal:Vector3
|
||||
match axis:
|
||||
MathUtil.Axis.Y:
|
||||
u_normal = normal.cross(Vector3.FORWARD)
|
||||
v_normal = u_normal.cross(normal)
|
||||
return [u_normal, v_normal]
|
||||
MathUtil.Axis.X:
|
||||
u_normal = normal.cross(Vector3.UP)
|
||||
v_normal = u_normal.cross(normal)
|
||||
return [u_normal, v_normal]
|
||||
MathUtil.Axis.Z:
|
||||
u_normal = normal.cross(Vector3.UP)
|
||||
v_normal = u_normal.cross(normal)
|
||||
return [u_normal, v_normal]
|
||||
|
||||
return []
|
||||
|
||||
|
360
addons/cyclops_level_builder/math/quick_hull.gd
Normal file
360
addons/cyclops_level_builder/math/quick_hull.gd
Normal file
@ -0,0 +1,360 @@
|
||||
# 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 QuickHull
|
||||
|
||||
class DirectedEdge extends RefCounted:
|
||||
var p0:Vector3
|
||||
var p1:Vector3
|
||||
|
||||
func _init(p0:Vector3, p1:Vector3):
|
||||
self.p0 = p0
|
||||
self.p1 = p1
|
||||
|
||||
func _to_string()->String:
|
||||
return "%s %s" % [p0, p1]
|
||||
|
||||
func reverse()->DirectedEdge:
|
||||
return DirectedEdge.new(p1, p0)
|
||||
|
||||
func equals(e:DirectedEdge)->bool:
|
||||
return p0 == e.p0 && p1 == e.p1
|
||||
|
||||
class Facet extends RefCounted:
|
||||
var plane:Plane
|
||||
var points:PackedVector3Array #Clockwise winding faces out
|
||||
var over_points:PackedVector3Array
|
||||
|
||||
func _to_string():
|
||||
var result:String = "plane %s\ncentroid %s\npoints %s\nover %s" % [plane, (points[0] + points[1] + points[2])/3, points, over_points]
|
||||
|
||||
return result
|
||||
|
||||
func has_edge(e:DirectedEdge)->bool:
|
||||
return (points[0] == e.p0 && points[1] == e.p1) || \
|
||||
(points[1] == e.p0 && points[2] == e.p1) || \
|
||||
(points[2] == e.p0 && points[0] == e.p1)
|
||||
|
||||
func get_edges()->Array[DirectedEdge]:
|
||||
var result:Array[DirectedEdge] = []
|
||||
|
||||
result.append(DirectedEdge.new(points[0], points[1]))
|
||||
result.append(DirectedEdge.new(points[1], points[2]))
|
||||
result.append(DirectedEdge.new(points[2], points[0]))
|
||||
return result
|
||||
|
||||
func init_from_points(p0:Vector3, p1:Vector3, p2:Vector3):
|
||||
#Facet normal points to outside
|
||||
plane = Plane(p0, p1, p2)
|
||||
points = [p0, p1, p2]
|
||||
|
||||
#Create a facet with vertices at p0, p1, p2 and winding such that under_ref
|
||||
# is on the under side of the plane
|
||||
func init_from_points_under(p0:Vector3, p1:Vector3, p2:Vector3, under_ref:Vector3):
|
||||
#Facet normal points to outside
|
||||
plane = Plane(p0, p1, p2)
|
||||
if plane.is_point_over(under_ref):
|
||||
plane = Plane(p0, p2, p1)
|
||||
points = [p0, p2, p1]
|
||||
else:
|
||||
points = [p0, p1, p2]
|
||||
|
||||
func get_furthest_point()->Vector3:
|
||||
var best_point:Vector3
|
||||
var best_distance:float = 0
|
||||
|
||||
for p in over_points:
|
||||
var dist = abs(plane.distance_to(p))
|
||||
if dist > best_distance:
|
||||
best_point = p
|
||||
best_distance = dist
|
||||
|
||||
return best_point
|
||||
|
||||
class Hull extends RefCounted:
|
||||
var facets:Array[Facet] = []
|
||||
|
||||
func get_non_empty_facet()->Facet:
|
||||
for f in facets:
|
||||
if !f.over_points.is_empty():
|
||||
return f
|
||||
return null
|
||||
|
||||
func get_facet_with_edge(e:DirectedEdge)->Facet:
|
||||
for f in facets:
|
||||
if f.has_edge(e):
|
||||
return f
|
||||
return null
|
||||
|
||||
func _to_string():
|
||||
var result:String = ""
|
||||
for f in facets:
|
||||
result += "%s\n" % f
|
||||
return result
|
||||
|
||||
|
||||
func get_points()->Array[Vector3]:
|
||||
var result:Array[Vector3]
|
||||
|
||||
for f in facets:
|
||||
for p in f.points:
|
||||
if !result.any(func(pl):return pl.is_equal_approx(p)):
|
||||
result.append(p)
|
||||
|
||||
return result
|
||||
|
||||
func format_points()->String:
|
||||
var result:String = ""
|
||||
for f in facets:
|
||||
result += "%s,\n" % f.points
|
||||
return result
|
||||
|
||||
|
||||
static func form_loop(edges:Array[DirectedEdge])->PackedVector3Array:
|
||||
var sorted:Array[DirectedEdge] = []
|
||||
|
||||
var cur_edge:DirectedEdge = edges.pop_back()
|
||||
sorted.append(cur_edge)
|
||||
|
||||
while !edges.is_empty():
|
||||
var found_edge:bool = false
|
||||
for i in edges.size():
|
||||
var e:DirectedEdge = edges[i]
|
||||
if e.p0.is_equal_approx(cur_edge.p1):
|
||||
edges.remove_at(i)
|
||||
cur_edge = e
|
||||
sorted.append(e)
|
||||
found_edge = true
|
||||
break
|
||||
|
||||
if !found_edge:
|
||||
assert(found_edge, "Unable to complete loop")
|
||||
pass
|
||||
# if !found_edge:
|
||||
# assert(false, "Unable to complete loop")
|
||||
# return PackedVector3Array()
|
||||
|
||||
var result:PackedVector3Array
|
||||
for e in sorted:
|
||||
result.append(e.p0)
|
||||
return result
|
||||
|
||||
static func merge_coplanar_facets(hull:Hull)->Hull:
|
||||
# print("hull %s " % hull)
|
||||
#print("hull %s " % hull.format_points())
|
||||
|
||||
var new_hull:Hull = Hull.new()
|
||||
var already_seen:Array[Facet] = []
|
||||
|
||||
for facet_idx in hull.facets.size():
|
||||
var facet:Facet = hull.facets[facet_idx]
|
||||
if already_seen.has(facet):
|
||||
continue
|
||||
already_seen.append(facet)
|
||||
|
||||
#print("merging facet %s" % facet)
|
||||
|
||||
var neighbor_set:Array[Facet] = []
|
||||
neighbor_set.append(facet)
|
||||
var boundary:Array[DirectedEdge] = []
|
||||
|
||||
while !neighbor_set.is_empty():
|
||||
var cur_facet:Facet = neighbor_set.pop_back()
|
||||
var edges:Array[DirectedEdge] = cur_facet.get_edges()
|
||||
|
||||
for e in edges:
|
||||
var neighbor:Facet = hull.get_facet_with_edge(e.reverse())
|
||||
if neighbor.plane.is_equal_approx(facet.plane):
|
||||
if !already_seen.has(neighbor):
|
||||
already_seen.append(neighbor)
|
||||
neighbor_set.append(neighbor)
|
||||
else:
|
||||
boundary.append(e)
|
||||
|
||||
|
||||
var points:PackedVector3Array = form_loop(boundary)
|
||||
|
||||
var nf:Facet = Facet.new()
|
||||
nf.plane = facet.plane
|
||||
nf.points = points
|
||||
new_hull.facets.append(nf)
|
||||
|
||||
return new_hull
|
||||
|
||||
|
||||
static func create_initial_simplex(points:PackedVector3Array)->Hull:
|
||||
if points.size() < 4:
|
||||
return null
|
||||
|
||||
#For first two points, pick furthest apart along one of the axes
|
||||
var max_x:Vector3 = points[0]
|
||||
var min_x:Vector3 = points[0]
|
||||
var max_y:Vector3 = points[0]
|
||||
var min_y:Vector3 = points[0]
|
||||
var max_z:Vector3 = points[0]
|
||||
var min_z:Vector3 = points[0]
|
||||
|
||||
for idx in range(1, points.size()):
|
||||
var p:Vector3 = points[idx]
|
||||
if p.x > max_x.x:
|
||||
max_x = p
|
||||
if p.x < min_x.x:
|
||||
min_x = p
|
||||
if p.y > max_y.y:
|
||||
max_y = p
|
||||
if p.y < min_y.y:
|
||||
min_y = p
|
||||
if p.z > max_z.z:
|
||||
max_z = p
|
||||
if p.z < min_z.z:
|
||||
min_z = p
|
||||
|
||||
var p0:Vector3
|
||||
var p1:Vector3
|
||||
var dx:float = max_x.distance_squared_to(min_x)
|
||||
var dy:float = max_y.distance_squared_to(min_y)
|
||||
var dz:float = max_z.distance_squared_to(min_z)
|
||||
|
||||
if dx > dy and dx > dz:
|
||||
p0 = max_x
|
||||
p1 = min_x
|
||||
elif dy > dz:
|
||||
p0 = max_y
|
||||
p1 = min_y
|
||||
else:
|
||||
p0 = max_z
|
||||
p1 = min_z
|
||||
|
||||
#Find furthest point from line for second point
|
||||
var p2:Vector3 = MathUtil.furthest_point_from_line(p0, p1 - p0, points)
|
||||
var p3:Vector3 = MathUtil.furthest_point_from_plane(Plane(p0, p1, p2), points)
|
||||
|
||||
#Make simplex
|
||||
var hull:Hull = Hull.new()
|
||||
|
||||
var f0:Facet = Facet.new()
|
||||
f0.init_from_points_under(p1, p2, p3, p0)
|
||||
var f1:Facet = Facet.new()
|
||||
f1.init_from_points_under(p2, p3, p0, p1)
|
||||
var f2:Facet = Facet.new()
|
||||
f2.init_from_points_under(p3, p0, p1, p2)
|
||||
var f3:Facet = Facet.new()
|
||||
f3.init_from_points_under(p0, p1, p2, p3)
|
||||
|
||||
hull.facets.append(f0)
|
||||
hull.facets.append(f1)
|
||||
hull.facets.append(f2)
|
||||
hull.facets.append(f3)
|
||||
|
||||
for p in points:
|
||||
for f in hull.facets:
|
||||
if f.plane.is_point_over(p) && !f.plane.has_point(p):
|
||||
f.over_points.append(p)
|
||||
|
||||
return hull
|
||||
|
||||
|
||||
static func quickhull(points:PackedVector3Array)->Hull:
|
||||
if points.size() < 4:
|
||||
return null
|
||||
|
||||
var hull:Hull = create_initial_simplex(points)
|
||||
if !hull:
|
||||
return null
|
||||
|
||||
#print("initial points %s" % points)
|
||||
#print("initial simplex %s" % hull.format_points())
|
||||
|
||||
while true:
|
||||
var facet:Facet = hull.get_non_empty_facet()
|
||||
if facet == null:
|
||||
break
|
||||
|
||||
#print("-facet %s" % facet)
|
||||
|
||||
var p_over:Vector3 = facet.get_furthest_point()
|
||||
#print("over point %s" % p_over)
|
||||
|
||||
#print("hull %s" % hull.format_points())
|
||||
|
||||
var visibile_faces:Array[Facet] = [facet]
|
||||
var edges:Array[DirectedEdge] = facet.get_edges()
|
||||
var visited_edges:Array[DirectedEdge] = []
|
||||
var boundary_edges:Array[DirectedEdge] = []
|
||||
|
||||
# for e in edges:
|
||||
# print("init edge search set %s" % e)
|
||||
|
||||
|
||||
#Find set of edges that form the boundary of faces visible to point
|
||||
# being added. We're basically flood filling from central facet until
|
||||
# we hit faces pointing away from reference point.
|
||||
while !edges.is_empty():
|
||||
var edge:DirectedEdge = edges.pop_back()
|
||||
visited_edges.append(edge)
|
||||
var edge_inv:DirectedEdge = edge.reverse()
|
||||
|
||||
var neighbor_facet:Facet = hull.get_facet_with_edge(edge_inv)
|
||||
if neighbor_facet.plane.is_point_over(p_over):
|
||||
visibile_faces.append(neighbor_facet)
|
||||
visited_edges.append(edge_inv)
|
||||
var neighbor_edges:Array[DirectedEdge] = neighbor_facet.get_edges()
|
||||
for e in neighbor_edges:
|
||||
if !visited_edges.any(func(edge): return edge.equals(e)):
|
||||
#print("adding edge to search set %s" % e)
|
||||
edges.append(e)
|
||||
else:
|
||||
boundary_edges.append(edge)
|
||||
#print("adding edge to boundary set %s" % edge)
|
||||
|
||||
var remaining_over_points:PackedVector3Array
|
||||
for f in visibile_faces:
|
||||
for pf in f.over_points:
|
||||
if pf == p_over:
|
||||
continue
|
||||
if !remaining_over_points.has(pf):
|
||||
remaining_over_points.append(pf)
|
||||
#print("over point for test %s" % pf)
|
||||
|
||||
hull.facets.remove_at(hull.facets.find(f))
|
||||
|
||||
for e in boundary_edges:
|
||||
var f:Facet = Facet.new()
|
||||
f.init_from_points(e.p0, e.p1, p_over)
|
||||
hull.facets.append(f)
|
||||
|
||||
#print("adding facet %s" % f)
|
||||
|
||||
for p in remaining_over_points:
|
||||
if f.plane.is_point_over(p) && !f.plane.has_point(p):
|
||||
f.over_points.append(p)
|
||||
|
||||
#print("hull %s" % hull.format_points())
|
||||
|
||||
hull = merge_coplanar_facets(hull)
|
||||
return hull
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user