Adds missing data

This commit is contained in:
Michel 2025-02-03 19:17:20 +01:00
parent e6391d9fdd
commit 53cdcc3433
620 changed files with 47293 additions and 151 deletions

View file

@ -0,0 +1,33 @@
# 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 ClipPolyResult
var polygons:Array[PackedVector3Array]
var cut_segments:Array[Segment3]
func _init(polygons:Array[PackedVector3Array] = [], cut_segments:Array[Segment3] = []):
self.polygons = polygons
self.cut_segments = cut_segments

File diff suppressed because it is too large Load diff

View 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)

View file

@ -0,0 +1,446 @@
# 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.
#@deprecated
@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():
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())

View 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()

View file

@ -0,0 +1,36 @@
# 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 Resource
class_name Loop2D
var points:PackedVector2Array
var closed:bool
func reverse():
points.reverse()
func _to_string():
return "Loop2D(%s, %s)" % [closed, str(points)]

View file

@ -0,0 +1,182 @@
# 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 circle_points(radius:float = 1, segs:int = 16, u_axis:Vector3 = Vector3.RIGHT, v_axis:Vector3 = Vector3.BACK)->PackedVector3Array:
#var result:PackedVector3Array
#
#for i in (segs + 1):
#result.append(u_axis * cos(i / float(segs)) + v_axis * sin(i / float(segs)))
#
#return result
#
#static func circle(radius:float = 1, segs:int = 16, u_axis:Vector3 = Vector3.RIGHT, v_axis:Vector3 = Vector3.BACK)->GeometryMesh:
#var mesh:GeometryMesh = GeometryMesh.new()
#
#for i in (segs + 1):
#mesh.coords.append(u_axis * cos(i / float(segs)) + v_axis * sin(i / float(segs)))
#mesh.uvs.append(Vector2(i / float(segs), 0))
#mesh.normals.append(mesh.coords[-1].normalized())
#
#return mesh
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

View file

@ -0,0 +1,934 @@
# 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
static func intersects_triangle(ray_origin:Vector3, ray_dir:Vector3, p0:Vector3, p1:Vector3, p2:Vector3)->bool:
#Godot uses clockwise winding
var tri_area_x2:Vector3 = MathUtil.triangle_area_x2(p0, p1, p2)
var p_hit:Vector3 = MathUtil.intersect_plane(ray_origin, ray_dir, p0, tri_area_x2)
if !p_hit.is_finite():
return false
if MathUtil.triangle_area_x2(p_hit, p0, p1).dot(tri_area_x2) < 0:
return false
if MathUtil.triangle_area_x2(p_hit, p1, p2).dot(tri_area_x2) < 0:
return false
if MathUtil.triangle_area_x2(p_hit, p2, p0).dot(tri_area_x2) < 0:
return false
return true
class IntersectTriangleResult:
var position:Vector3
var normal:Vector3
static func intersect_triangle(ray_origin:Vector3, ray_dir:Vector3, p0:Vector3, p1:Vector3, p2:Vector3)->IntersectTriangleResult:
#Godot uses clockwise winding
var tri_area_x2:Vector3 = MathUtil.triangle_area_x2(p0, p1, p2)
var p_hit:Vector3 = MathUtil.intersect_plane(ray_origin, ray_dir, p0, tri_area_x2)
if !p_hit.is_finite():
return null
if MathUtil.triangle_area_x2(p_hit, p0, p1).dot(tri_area_x2) < 0:
return null
if MathUtil.triangle_area_x2(p_hit, p1, p2).dot(tri_area_x2) < 0:
return null
if MathUtil.triangle_area_x2(p_hit, p2, p0).dot(tri_area_x2) < 0:
return null
var result:IntersectTriangleResult = IntersectTriangleResult.new()
result.position = p_hit
result.normal = tri_area_x2.normalized()
return result
#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_plane(point:Vector3, plane_origin:Vector3, plane_dir:Vector3)->Vector3:
return point - (point - plane_origin).project(plane_dir)
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:
if seg_start.is_equal_approx(seg_end):
return point.distance_squared_to(seg_start)
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])->Loop2D:
var segs_sorted:Array[Segment2d] = []
var seg_tail = seg_stack.pop_back()
segs_sorted.append(seg_tail)
var seg_head = seg_tail
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(seg_tail.p1):
#print("matching %s with %s" % [seg_tail, cur_seg])
segs_sorted.append(cur_seg)
seg_stack.remove_at(s_idx)
seg_tail = cur_seg
found_seg = true
break
elif cur_seg.p1.is_equal_approx(seg_tail.p1):
#print("matching %s with %s" % [seg_tail, cur_seg])
cur_seg = cur_seg.reverse()
segs_sorted.append(cur_seg)
seg_stack.remove_at(s_idx)
seg_tail = cur_seg
found_seg = true
break
elif cur_seg.p1.is_equal_approx(seg_head.p0):
#print("matching %s with %s" % [seg_head, cur_seg])
segs_sorted.insert(0, cur_seg)
seg_stack.remove_at(s_idx)
seg_head = cur_seg
found_seg = true
break
elif cur_seg.p0.is_equal_approx(seg_head.p0):
#print("matching %s with %s" % [seg_head, cur_seg])
cur_seg = cur_seg.reverse()
segs_sorted.insert(0, cur_seg)
seg_stack.remove_at(s_idx)
seg_head = cur_seg
found_seg = true
break
if !found_seg:
# push_warning("loop not continuous")
break
#print("segs_sorted %s" % str(segs_sorted))
var result:Loop2D = Loop2D.new()
result.closed = true
for s in segs_sorted:
result.points.append(s.p0)
if seg_head.p0 != seg_tail.p1:
result.points.append(seg_tail.p1)
result.closed = false
if face_area_x2_2d(result.points) < 0:
result.reverse()
#print("loop %s" % str(result))
return result
static func get_loops_from_segments_2d(segments:PackedVector2Array)->Array[Loop2D]:
#print("segments %s" % segments)
var loops:Array[Loop2D] = []
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:Loop2D = extract_loop_2d(seg_stack)
loops.append(loop)
#print("result %s" % str(loops))
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 []
#Returns the planes of a frustum for the rectangular region on the camera's near
# plane with all planes pointing toward the interior of the frustum
static func calc_frustum_camera_rect(cam:Camera3D, p0:Vector2, p1:Vector2)->Array[Plane]:
var x0 = min(p0.x, p1.x)
var x1 = max(p0.x, p1.x)
var y0 = min(p0.y, p1.y)
var y1 = max(p0.y, p1.y)
var p00:Vector2 = Vector2(x0, y0)
var p01:Vector2 = Vector2(x0, y1)
var p10:Vector2 = Vector2(x1, y0)
var p11:Vector2 = Vector2(x1, y1)
# print("cam rect %s" % str([p00, p11]))
#Cam project_position does not work if we set distance to far plane, so back off a bit
var far_scalar:float = .95
var p000:Vector3 = cam.project_position(p00, cam.near)
var p100:Vector3 = cam.project_position(p10, cam.near)
var p010:Vector3 = cam.project_position(p01, cam.near)
var p110:Vector3 = cam.project_position(p11, cam.near)
var p001:Vector3 = cam.project_position(p00, cam.far * far_scalar)
var p101:Vector3 = cam.project_position(p10, cam.far * far_scalar)
var p011:Vector3 = cam.project_position(p01, cam.far * far_scalar)
var p111:Vector3 = cam.project_position(p11, cam.far * far_scalar)
# print("points %s" % str([p000, p100, p010, p110, p001, p101, p011, p111, ]))
var plane_left:Plane = Plane(p001, p011, p010)
var plane_right:Plane = Plane(p101, p110, p111)
var plane_top:Plane = Plane(p011, p111, p110)
var plane_bottom:Plane = Plane(p001, p100, p101)
var plane_near:Plane = Plane(p000, p110, p100)
var plane_far:Plane = Plane(p001, p111, p011)
return [plane_left, plane_right, plane_top, plane_bottom, plane_near, plane_far]
static func clip_polygon(points:PackedVector3Array, plane:Plane)->PackedVector3Array:
var result:PackedVector3Array
#Cut at planr intersection
var points_on_or_over:PackedVector3Array
for p_idx0 in points.size():
var p_idx1:int = wrap(p_idx0 + 1, 0, points.size())
var p0:Vector3 = points[p_idx0]
var p1:Vector3 = points[p_idx1]
var on0:bool = plane.has_point(p0)
var over0:bool = plane.is_point_over(p0)
var under0:bool = !on0 && !over0
var on1:bool = plane.has_point(p1)
var over1:bool = plane.is_point_over(p1)
var under1:bool = !on1 && !over1
if on0 || over0:
points_on_or_over.append(p0)
if (under0 && over1) || (over0 && under1):
points_on_or_over.append(plane.intersects_segment(p0, p1))
return points_on_or_over
#Snaps point to a point appearing in the list if distance to it is <= radius. Otherwise appends
# point to point list
static func snap_point_to_point_list_or_append(point:Vector3, list:PackedVector3Array, radius:float = .005):
for p in list:
if p.distance_squared_to(point) < radius * radius:
return p
list.append(point)
return point
static func create_loop_from_directed_segments(segs:Array[Segment3], snap_radius:float = .005)->PackedVector3Array:
var snap_list:PackedVector3Array
for seg in segs:
seg.p0 = snap_point_to_point_list_or_append(seg.p0, snap_list, snap_radius)
seg.p1 = snap_point_to_point_list_or_append(seg.p1, snap_list, snap_radius)
var seg_stack:Array[Segment3]
var sorted_segs:Array[Segment3]
for s in segs:
if !is_zero_approx(s.length_squared()):
seg_stack.append(s)
sorted_segs.append(seg_stack.pop_back())
while !seg_stack.is_empty():
var found_seg:bool = false
var min_dist:float = 10000
for i in seg_stack.size():
var s:Segment3 = seg_stack[i]
# if s.p0.is_equal_approx(sorted_segs.back().p1):
var dist:float = s.p0.distance_to(sorted_segs.back().p1)
min_dist = min(min_dist, dist)
if dist < .005:
# if s.p0.is_equal_approx(sorted_segs.back().p1):
sorted_segs.append(s)
seg_stack.remove_at(i)
found_seg = true
break
# if s.p1.is_equal_approx(sorted_segs.back().p1):
# sorted_segs.append(s.reversed())
# seg_stack.remove_at(i)
# found_seg = true
# break
if !found_seg:
print("Error: could not form loop")
return []
var result:PackedVector3Array
for s in sorted_segs:
result.append(s.p0)
return result
static func clip_polygon_separate(points:PackedVector3Array, plane:Plane)->ClipPolyResult:
#Clip points to plane.
var clipped_points:PackedVector3Array = clip_polygon(points, plane)
#Every point should now be on or above the plane
var is_over:Array[bool]
var all_over:bool = true
var none_over:bool = true
for p in clipped_points:
var is_on:bool = plane.has_point(p)
if is_on:
all_over = false
else:
none_over = false
is_over.append(!is_on)
if all_over:
return ClipPolyResult.new([clipped_points])
if none_over:
return ClipPolyResult.new()
var start_idx:int = -1
for p_idx0 in clipped_points.size():
var p_idx1:int = wrap(p_idx0 + 1, 0, clipped_points.size())
var over0:bool = is_over[p_idx0]
var over1:bool = is_over[p_idx1]
if !over0 && over1:
start_idx = p_idx0
break
#If you think of the clipped_points as a string where every point on the plane is
# represented by the character 'n' and every point over the plane is the character
# 'v', then every sub polygon will be a string that can be represented by the
# regular expression "(nv+n)"
var results:Array[PackedVector3Array]= []
var cut_segments:Array[Segment3]
var writing_shape:bool = true
var sub_poly:PackedVector3Array
for i in clipped_points.size():
var p_idx0:int = wrap(i + start_idx, 0, clipped_points.size())
var p_idx1:int = wrap(i + start_idx + 1, 0, clipped_points.size())
if is_over[p_idx1]:
sub_poly.append(clipped_points[p_idx0])
elif is_over[p_idx0]:
sub_poly.append(clipped_points[p_idx0])
sub_poly.append(clipped_points[p_idx1])
cut_segments.append(Segment3.new(sub_poly[sub_poly.size() - 1], sub_poly[0]))
results.append(sub_poly.duplicate())
sub_poly.clear()
return ClipPolyResult.new(results, cut_segments)
static func polygon_intersects_frustum(points:PackedVector3Array, frustum:Array[Plane])->bool:
var points_i:PackedVector3Array = points
for plane in frustum:
points_i = clip_polygon(points_i, plane)
if points_i.is_empty():
return false
return true
static func frustum_contians_point(planes:Array[Plane], point:Vector3)->bool:
for plane in planes:
if !plane.is_point_over(point) && !plane.has_point(point):
return false
return true
static func frustum_intersects_sphere(planes:Array[Plane], center:Vector3, radius:float)->bool:
for plane in planes:
var dist:float = plane.distance_to(center)
if dist < -radius:
return false
return true
func plane_intesects_point_cloud(points:PackedVector3Array, plane:Plane)->bool:
var is_over:bool = false
var is_under:bool = false
for p in points:
if plane.has_point(p):
continue
if plane.is_point_over(p):
is_over = true
else:
is_under = true
if is_over && is_under:
return true
return false
#Returns vector with [R, Q] where R is the orthogonal basis
# and Q is a triangular matrix such that basis = R * Q
static func gram_schmidt_decomposition(basis:Basis)->Array[Basis]:
#https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process
var v0:Vector3 = basis.x
var v1:Vector3 = basis.y
var v2:Vector3 = basis.z
var u0:Vector3 = v0
var u1:Vector3 = v1 - v1.project(u0)
var u2:Vector3 = v2 - v2.project(u0) - v2.project(u1)
var R:Basis = Basis(u0.normalized(), u1.normalized(), u2.normalized())
var R_inv:Basis = R.inverse()
var Q:Basis = R_inv * basis
return [R, Q]
#Decomposes matrix into translate, rotate, scale and shear vectors where
# M = T * R * Sh * S
# where:
# T - translate matrix
# R - rotate matrix
# Sh - shear matrix
# S - scale matrix
#
# Shear matrix for vector (x, y, z) is
# [1 x y]
# [0 1 z]
# [0 0 1]
static func decompose_matrix_3d(m:Transform3D, order:EulerOrder = EULER_ORDER_YXZ)->Dictionary:
if is_zero_approx(m.basis.determinant()):
return {"valid": false}
var basis:Basis = m.basis
var gram_schmidt = gram_schmidt_decomposition(basis)
var rot_mtx = gram_schmidt[0]
var euler:Vector3 = rot_mtx.get_euler(order)
var scale_shear = gram_schmidt[1]
var scale:Vector3 = Vector3(scale_shear.x.x, scale_shear.y.y, scale_shear.z.z)
var scale_mat:Basis = Basis.from_scale(scale)
var shear:Basis = scale_shear * scale_mat.inverse()
#print(shear)
return {
"valid": true,
"translate": m.origin,
"rotate": euler,
"scale": scale,
"shear": Vector3(shear.y.x, shear.z.x, shear.z.y)
}
static func compose_matrix_3d(translate:Vector3, rotate:Vector3 = Vector3.ZERO, order:EulerOrder = EULER_ORDER_YXZ, shear:Vector3 = Vector3.ZERO, scale:Vector3 = Vector3.ONE)->Transform3D:
var scale_mat:Basis = Basis.from_scale(scale)
var shear_mat:Basis = Basis(
Vector3(1, 0, 0),
Vector3(shear.x, 1, 0),
Vector3(shear.y, shear.z, 1))
var rot_mat:Basis = Basis.from_euler(rotate, order)
var basis:Basis = rot_mat * shear_mat * scale_mat
return Transform3D(basis, translate)
static func clip_segment_to_plane_3d(p:Plane, v0:Vector3, v1:Vector3)->PackedVector3Array:
var clip_v0:bool = !p.is_point_over(v0)
var clip_v1:bool = !p.is_point_over(v1)
if clip_v0 && clip_v1:
return []
if clip_v0:
v0 = p.intersects_segment(v0, v1)
elif clip_v1:
v1 = p.intersects_segment(v0, v1)
return [v0, v1]
static func blend_over_with_alpha(src:Color, dest:Color):
#https://en.wikipedia.org/wiki/Alpha_compositing
var a0:float = src.a + dest.a * (1 - src.a)
var r0:float = (src.r * src.a + dest.r * dest.a * (1 - src.a)) / a0
var g0:float = (src.g * src.a + dest.g * dest.a * (1 - src.a)) / a0
var b0:float = (src.b * src.a + dest.b * dest.a * (1 - src.a)) / a0
return Color(r0, g0, b0, a0)
static func blend_colors_with_alpha(src:Color, dest:Color, weight:float)->Color:
var col:Color = blend_over_with_alpha(src, dest)
col.a *= weight
return blend_over_with_alpha(col, dest)
static func blend_colors_ignore_alpha(src:Color, dest:Color, weight:float)->Color:
return weight * src + (1 - weight) * dest

View file

@ -0,0 +1,94 @@
# 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 PenStroke
extends Resource
class StrokePoint extends Resource:
@export var position:Vector3
@export var pressure:float
#func _init(position:Vector3 = Vector3.ZERO, pressure:float = 1):
#print("ppp ", position)
#self.position = position
#self.pressure = pressure
func _to_string()->String:
return "%s %f" % [str(position), pressure]
func lerp(p:StrokePoint, weight:float):
var r:StrokePoint = StrokePoint.new()
r.position = lerp(position, p.position, weight)
r.pressure = lerp(pressure, p.pressure, weight)
return r
var stroke_points:Array[StrokePoint]
func clear():
stroke_points.clear()
func is_empty()->bool:
return stroke_points.is_empty()
func append_stroke_point(position:Vector3, pressure:float = 1):
var p:StrokePoint = StrokePoint.new()
p.position = position
p.pressure = pressure
stroke_points.append(p)
func resample_points(resample_dist:float)->PenStroke:
if stroke_points.is_empty():
return null
var result:PenStroke = PenStroke.new()
#var p_start:StrokePoint = stroke_points[0]
#var p_start1:StrokePoint = p_start.duplicate(true)
#print("p_start ", p_start)
#print("p_start1 ", p_start1)
#print("stroke_points[0] ", stroke_points[0].position)
result.stroke_points.append(stroke_points[0].duplicate())
#print("--stroke_points[0] ", stroke_points[0].position)
var seg_dist_covered:float = 0
var last_pos_plotted:float = 0
for src_p_idx in stroke_points.size() - 1:
var p0:StrokePoint = stroke_points[src_p_idx]
var p1:StrokePoint = stroke_points[src_p_idx + 1]
var seg_len:float = p0.position.distance_to(p1.position)
while last_pos_plotted + resample_dist <= seg_dist_covered + seg_len:
var pn:StrokePoint = p0.lerp(p1, \
(last_pos_plotted + resample_dist - seg_dist_covered) / seg_len)
result.stroke_points.append(pn)
last_pos_plotted += resample_dist
seg_dist_covered += seg_len
#print("stroke points res ", str(result.stroke_points))
return result

View 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

View file

@ -0,0 +1,43 @@
# 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 Segment3
var p0:Vector3
var p1:Vector3
func _init(p0:Vector3 = Vector3.ZERO, p1:Vector3 = Vector3.ZERO):
self.p0 = p0
self.p1 = p1
func reversed()->Segment3:
return Segment3.new(p1, p0)
func length_squared()->float:
return p0.distance_squared_to(p1)
func _to_string():
return "[%s, %s]" % [str(p0), str(p1)]