# 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