basilisk-engine 0.0.9__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basilisk-engine might be problematic. Click here for more details.

Files changed (61) hide show
  1. basilisk/__init__.py +3 -1
  2. basilisk/collisions/broad/broad_aabb.py +8 -1
  3. basilisk/collisions/broad/broad_bvh.py +38 -2
  4. basilisk/collisions/collider.py +20 -10
  5. basilisk/collisions/collider_handler.py +97 -32
  6. basilisk/collisions/narrow/contact_manifold.py +91 -0
  7. basilisk/collisions/narrow/dataclasses.py +27 -0
  8. basilisk/collisions/narrow/deprecated.py +47 -0
  9. basilisk/collisions/narrow/epa.py +21 -15
  10. basilisk/collisions/narrow/gjk.py +15 -14
  11. basilisk/collisions/narrow/graham_scan.py +25 -0
  12. basilisk/collisions/narrow/helper.py +14 -7
  13. basilisk/collisions/narrow/line_intersections.py +107 -0
  14. basilisk/collisions/narrow/sutherland_hodgman.py +76 -0
  15. basilisk/draw/draw_handler.py +7 -5
  16. basilisk/engine.py +28 -6
  17. basilisk/generic/abstract_custom.py +134 -0
  18. basilisk/generic/collisions.py +47 -2
  19. basilisk/generic/quat.py +84 -65
  20. basilisk/generic/vec3.py +99 -67
  21. basilisk/input/mouse.py +3 -3
  22. basilisk/mesh/cube.py +20 -2
  23. basilisk/mesh/mesh.py +69 -54
  24. basilisk/mesh/mesh_from_data.py +106 -21
  25. basilisk/mesh/narrow_aabb.py +10 -1
  26. basilisk/mesh/narrow_bvh.py +9 -1
  27. basilisk/nodes/node.py +211 -101
  28. basilisk/nodes/node_handler.py +58 -33
  29. basilisk/particles/__init__.py +0 -0
  30. basilisk/particles/particle_handler.py +55 -0
  31. basilisk/particles/particle_renderer.py +87 -0
  32. basilisk/physics/impulse.py +113 -0
  33. basilisk/physics/physics_body.py +10 -2
  34. basilisk/physics/physics_engine.py +2 -3
  35. basilisk/render/batch.py +3 -1
  36. basilisk/render/camera.py +35 -1
  37. basilisk/render/chunk.py +19 -4
  38. basilisk/render/chunk_handler.py +39 -23
  39. basilisk/render/image.py +1 -1
  40. basilisk/render/image_handler.py +17 -14
  41. basilisk/render/light_handler.py +16 -11
  42. basilisk/render/material.py +38 -14
  43. basilisk/render/material_handler.py +31 -18
  44. basilisk/render/shader.py +110 -0
  45. basilisk/render/shader_handler.py +20 -35
  46. basilisk/render/sky.py +8 -5
  47. basilisk/scene.py +116 -33
  48. basilisk/shaders/batch.frag +40 -11
  49. basilisk/shaders/batch.vert +14 -7
  50. basilisk/shaders/geometry.frag +9 -0
  51. basilisk/shaders/geometry.vert +42 -0
  52. basilisk/shaders/normal.frag +60 -0
  53. basilisk/shaders/normal.vert +97 -0
  54. basilisk/shaders/particle.frag +72 -0
  55. basilisk/shaders/particle.vert +85 -0
  56. {basilisk_engine-0.0.9.dist-info → basilisk_engine-0.1.1.dist-info}/METADATA +5 -5
  57. basilisk_engine-0.1.1.dist-info/RECORD +95 -0
  58. basilisk/shaders/image.png +0 -0
  59. basilisk_engine-0.0.9.dist-info/RECORD +0 -78
  60. {basilisk_engine-0.0.9.dist-info → basilisk_engine-0.1.1.dist-info}/WHEEL +0 -0
  61. {basilisk_engine-0.0.9.dist-info → basilisk_engine-0.1.1.dist-info}/top_level.txt +0 -0
basilisk/__init__.py CHANGED
@@ -1,10 +1,12 @@
1
+ import pygame as pg
1
2
  from .engine import Engine
2
3
  from .scene import Scene
3
4
  from .nodes.node import Node
4
5
  from .mesh.mesh import Mesh
5
6
  from .render.image import Image
6
7
  from .render.material import Material
8
+ from .render.shader import Shader
7
9
  from .render.shader_handler import ShaderHandler
8
10
  from .draw import draw
9
- from .render.camera import FreeCamera, StaticCamera
11
+ from .render.camera import FreeCamera, StaticCamera, FollowCamera, OrbitCamera
10
12
  from .render.sky import Sky
@@ -1,6 +1,6 @@
1
1
  import glm
2
2
  from ...generic.abstract_bvh import AbstractAABB as AABB
3
- from ...generic.collisions import collide_aabb_aabb
3
+ from ...generic.collisions import collide_aabb_aabb, collide_aabb_line
4
4
  from ...generic.meshes import get_aabb_surface_area
5
5
  from ..collider import Collider
6
6
 
@@ -79,6 +79,13 @@ class BroadAABB(AABB):
79
79
  if isinstance(self.b, BroadAABB): possible.extend(self.b.get_collided(collider))
80
80
  elif collide_aabb_aabb(self.b.top_right, self.b.bottom_left, collider.top_right, collider.bottom_left): possible.append(self.b)
81
81
  return possible
82
+
83
+ def get_line_collided(self, position: glm.vec3, forward: glm.vec3) -> list[Collider]:
84
+ """
85
+ Returns the colliders that may intersect with the given line
86
+ """
87
+ if not collide_aabb_line(self.top_right, self.bottom_left, position, forward): return []
88
+ return (self.a.get_line_collided(position, forward) if isinstance(self.a, BroadAABB) else [self.a]) + (self.b.get_line_collided(position, forward) if isinstance(self.b, BroadAABB) else [self.b])
82
89
 
83
90
  def get_all_aabbs(self, layer: int) -> list[tuple[glm.vec3, glm.vec3, int]]: # TODO test function
84
91
  """
@@ -58,7 +58,35 @@ class BroadBVH(BVH):
58
58
  if isinstance(self.root, BroadAABB): return self.root.get_all_aabbs(0)
59
59
  return [(self.root.top_right, self.root.bottom_left, 0)]
60
60
 
61
- def remove(self, collider: Collider) -> None: ...
61
+ def remove(self, collider: Collider) -> None:
62
+ """
63
+ Removes a collider from the BVH, refitting the tree and adjusting relations
64
+ """
65
+ parent: BroadAABB | None = collider.parent
66
+
67
+ # if collider is the root, remove the root
68
+ if not parent:
69
+ self.root = None
70
+ return
71
+
72
+ # if collider has no grandparent, remove parent and set sibling as root
73
+ grand = parent.parent
74
+ sibling = parent.b if collider == parent.a else parent.a
75
+ if not grand:
76
+ self.root = sibling
77
+ sibling.parent = None
78
+ return
79
+
80
+ # if grandparent exists
81
+ if parent == grand.a: grand.a = sibling
82
+ else: grand.b = sibling
83
+ sibling.parent = grand
84
+
85
+ # move up and refit tree
86
+ aabb = grand
87
+ while aabb:
88
+ aabb.update_points()
89
+ aabb = aabb.parent
62
90
 
63
91
  def rotate(self, aabb: BroadAABB) -> None:
64
92
  """
@@ -99,4 +127,12 @@ class BroadBVH(BVH):
99
127
  """
100
128
  Returns which objects may be colliding from the BVH
101
129
  """
102
- return self.root.get_collided(collider)
130
+ if isinstance(self.root, BroadAABB): return self.root.get_collided(collider)
131
+ else: return [] # if there is only one collider in the scene then there is nothing to collide with
132
+
133
+ def get_line_collided(self, position: glm.vec3, forward: glm.vec3) -> list[Collider]:
134
+ """
135
+ Returns the colliders that may intersect with the given line
136
+ """
137
+ if isinstance(self.root, BroadAABB): return self.root.get_line_collided(position, forward)
138
+ return [self.root]
@@ -35,22 +35,25 @@ class Collider():
35
35
  mesh: Mesh
36
36
  """Reference to the colliding mesh"""
37
37
 
38
- def __init__(self, collider_handler, node, box_mesh: bool=False, static_friction: glm.vec3=0.7, kinetic_friction: glm.vec3=0.3, elasticity: glm.vec3=0.1, collision_group: str=None):
39
- self.collider_handler = collider_handler
38
+ def __init__(self, node, box_mesh: bool=False, static_friction: glm.vec3=0.7, kinetic_friction: glm.vec3=0.3, elasticity: glm.vec3=0.2, collision_group: str=None):
39
+ self.collider_handler = None
40
40
  self.node = node
41
- self.mesh = self.collider_handler.cube if box_mesh else self.node.mesh
42
- self.static_friction = static_friction
43
- self.kinetic_friction = kinetic_friction
44
- self.elasticity = elasticity
41
+ self.static_friction = static_friction if elasticity else 0.8 # added checks to prevent floats being set to None. Also done for kinetic and elasticity
42
+ self.box_mesh = box_mesh
43
+ self.kinetic_friction = kinetic_friction if elasticity else 0.4
44
+ self.elasticity = elasticity if elasticity else 0.1
45
45
  self.collision_group = collision_group
46
46
  self.collision_velocity = 0
47
47
  self.collisions = {}
48
48
  self.parent = None
49
49
 
50
50
  # lazy update variables TODO change to distinguish between static and nonstatic objects
51
- self.needs_obb = True
52
- self.needs_half_dimensions = True
53
-
51
+ self.needs_obb = True # pos, scale, rot
52
+ self.needs_half_dimensions = True # scale, rot
53
+ self.needs_bvh = True # pos, scale, rot
54
+
55
+ @property
56
+ def collider_handler(self): return self._collider_handler
54
57
  @property
55
58
  def has_collided(self): return bool(self.collisions)
56
59
  @property
@@ -71,4 +74,11 @@ class Collider():
71
74
  if self.needs_obb:
72
75
  self._obb_points = transform_points(self.mesh.aabb_points, self.node.model_matrix)
73
76
  self.needs_obb = False
74
- return self._obb_points
77
+ return self._obb_points
78
+
79
+ @collider_handler.setter
80
+ def collider_handler(self, value):
81
+ self._collider_handler = value
82
+ if not value: return
83
+ self.mesh = value.cube if self.box_mesh else self.node.mesh
84
+ value.add(self)
@@ -1,11 +1,14 @@
1
1
  import glm
2
- from .narrow.gjk import *
2
+
3
3
  from .collider import Collider
4
4
  from .broad.broad_bvh import BroadBVH
5
- from ..mesh.cube import Cube
6
- from ..generic.collisions import get_sat_axes
7
5
  from .narrow.gjk import collide_gjk
8
6
  from .narrow.epa import get_epa_from_gjk
7
+ from .narrow.contact_manifold import get_contact_manifold, separate_polytope
8
+ from .narrow.dataclasses import SupportPoint, ContactPoint, ContactManifold
9
+ from ..nodes.node import Node
10
+ from ..generic.collisions import get_sat_axes
11
+ from ..physics.impulse import calculate_collisions
9
12
 
10
13
  class ColliderHandler():
11
14
  scene: ...
@@ -19,13 +22,14 @@ class ColliderHandler():
19
22
  self.scene = scene
20
23
  self.cube = self.scene.engine.cube
21
24
  self.colliders = []
25
+ self.polytope_data = {}
26
+ self.contact_manifolds: dict[tuple[Collider, Collider] : ContactManifold] = {}
22
27
  self.bvh = BroadBVH(self)
23
28
 
24
- def add(self, node, box_mesh: bool=False, static_friction: glm.vec3=0.7, kinetic_friction: glm.vec3=0.3, elasticity: glm.vec3=0.1, collision_group: str=None) -> Collider:
29
+ def add(self, collider: Collider) -> Collider:
25
30
  """
26
31
  Creates a collider and adds it to the collider list
27
32
  """
28
- collider = Collider(self, node, box_mesh, static_friction, kinetic_friction, elasticity, collision_group)
29
33
  self.colliders.append(collider)
30
34
  self.bvh.add(collider)
31
35
  return collider
@@ -44,9 +48,18 @@ class ColliderHandler():
44
48
  """
45
49
  # reset collision data
46
50
  for collider in self.colliders: collider.collisions = {}
47
- # TODO update BVH
51
+
52
+
53
+ # update BVH
54
+ for collider in self.colliders:
55
+ if collider.needs_bvh:
56
+ self.bvh.remove(collider)
57
+ self.bvh.add(collider)
58
+ collider.needs_bvh = False
59
+
60
+ # resolve collisions
48
61
  broad_collisions = self.resolve_broad_collisions()
49
- self.resolve_narrow_collisions(broad_collisions)
62
+ self.resolve_narrow_collisions(broad_collisions)
50
63
 
51
64
  def collide_obb_obb(self, collider1: Collider, collider2: Collider) -> tuple[glm.vec3, float] | None:
52
65
  """
@@ -59,7 +72,8 @@ class ColliderHandler():
59
72
  # test axes
60
73
  small_axis = None
61
74
  small_overlap = 1e10
62
- for axis in axes: # TODO add optimization for points on cardinal axis of cuboid
75
+ small_index = 0
76
+ for i, axis in enumerate(axes): # TODO add optimization for points on cardinal axis of cuboid
63
77
  # "project" points
64
78
  proj1 = [glm.dot(p, axis) for p in points1]
65
79
  proj2 = [glm.dot(p, axis) for p in points2]
@@ -69,17 +83,15 @@ class ColliderHandler():
69
83
 
70
84
  # if lines are not intersecting
71
85
  if max1 > max2 and min1 < min2: overlap = min(max1 - min2, max2 - min1)
72
- elif max2 > max1 and min2 < min1: overlap = min(max1 - min2, max2 - min1)
86
+ elif max2 > max1 and min2 < min1: overlap = min(max2 - min1, max1 - min2)
73
87
  else: overlap = min(max1, max2) - max(min1, min2) # TODO check if works with containment
74
88
 
75
89
  if abs(overlap) > abs(small_overlap): continue
76
90
  small_overlap = overlap
77
91
  small_axis = axis
78
-
79
- print(axes.index(small_axis), glm.length(small_axis))
80
- print('overlap:', small_overlap)
92
+ small_index = i
81
93
 
82
- return small_axis, small_overlap
94
+ return small_axis, small_overlap, small_index
83
95
 
84
96
  def collide_obb_obb_decision(self, collider1: Collider, collider2: Collider) -> bool:
85
97
  """
@@ -106,26 +118,54 @@ class ColliderHandler():
106
118
  """
107
119
  collisions = set()
108
120
  for collider1 in self.colliders:
109
-
121
+ if collider1.node.static: continue
110
122
  # traverse bvh to find aabb aabb collisions
111
123
  colliding = self.bvh.get_collided(collider1)
112
124
  for collider2 in colliding:
113
- if collider1 == collider2: continue
114
- if (collider1, collider2) in collisions or (collider2, collider1) in collisions: continue # TODO find a secure way for ordering colliders
125
+ if collider1 is collider2: continue
126
+ if ((collider1, collider2) if id(collider1) < id(collider2) else (collider2, collider1)) in collisions: continue
115
127
 
116
128
  # run broad collision for specified mesh types
117
129
  if max(len(collider1.mesh.points), len(collider2.mesh.points)) > 250 and not self.collide_obb_obb_decision(collider1, collider2): continue # contains at least one "large" mesh TODO write heuristic algorithm for determining large meshes
118
-
119
- collisions.add((collider1, collider2)) # TODO find a secure way for ordering colliders
130
+ collisions.add((collider1, collider2) if id(collider1) < id(collider2) else (collider2, collider1))
120
131
 
121
132
  return collisions
122
133
 
134
+ def merge_contact_points(self, vec: glm.vec3, collider1: Collider, collider2: Collider, points1: list[ContactPoint], points2: list[ContactPoint]) -> None:
135
+ """
136
+
137
+ """
138
+ def merge_points(node: Node, existing: dict[int, glm.vec3], incoming: list[ContactPoint]) -> dict[int, glm.vec3]:
139
+ incoming_indices = set()
140
+
141
+ # add incoming points
142
+ for point in incoming:
143
+ incoming_indices.add(point.index)
144
+ if point.index not in existing or glm.length2(point.vertex - existing[point.index]) > 1e-5: existing[point.index] = glm.vec3(point.vertex)
145
+ # if glm.length2(point.vertex - existing[point.index]) != 0: print(point.vertex - existing[point.index])
146
+
147
+ # remove changed stored points
148
+ remove_indices = []
149
+ for index, vertex in existing.items():
150
+ if index in incoming_indices: continue
151
+ if glm.length2(node.get_vertex(index) - vertex) > 1e-5: remove_indices.append(index) # check to see if point has moved
152
+
153
+ # remove unused and moved points
154
+ for index in remove_indices: del existing[index]
155
+ return existing
156
+
157
+ # check if collision is logged, if not create a new one
158
+ collider_tuple = (collider1, collider2)
159
+ if collider_tuple not in self.contact_manifolds or glm.length2(self.contact_manifolds[collider_tuple].normal - vec) > 1e-7: self.contact_manifolds[collider_tuple] = ContactManifold(vec, dict(), dict())
160
+
161
+ # add contact point from current collision and check overlap
162
+ self.contact_manifolds[collider_tuple].contact_points1 = merge_points(collider1.node, self.contact_manifolds[collider_tuple].contact_points1, points1)
163
+ self.contact_manifolds[collider_tuple].contact_points2 = merge_points(collider2.node, self.contact_manifolds[collider_tuple].contact_points2, points2)
164
+
123
165
  def resolve_narrow_collisions(self, broad_collisions: list[tuple[Collider, Collider]]) -> None:
124
166
  """
125
167
  Determines if two colliders are colliding, if so resolves their penetration and applies impulse
126
168
  """
127
- collided = []
128
-
129
169
  for collision in broad_collisions: # assumes that broad collisions are unique
130
170
  collider1 = collision[0]
131
171
  collider2 = collision[1]
@@ -139,7 +179,11 @@ class ColliderHandler():
139
179
  data = self.collide_obb_obb(collider1, collider2)
140
180
  if not data: continue
141
181
 
142
- vec, distance = data
182
+ vec, distance, index = data
183
+
184
+ # TODO replace with own contact algorithm
185
+ points1 = [ContactPoint(index, vertex) for index, vertex in enumerate(collider1.obb_points)]
186
+ points2 = [ContactPoint(index, vertex) for index, vertex in enumerate(collider2.obb_points)]
143
187
 
144
188
  else: # use gjk to determine collisions between non-cuboid meshes
145
189
  has_collided, simplex = collide_gjk(node1, node2)
@@ -148,17 +192,38 @@ class ColliderHandler():
148
192
  face, polytope = get_epa_from_gjk(node1, node2, simplex)
149
193
  vec, distance = face[1], face[0]
150
194
 
151
- if glm.dot(vec, node2.position - node1.position) > 0:
152
- vec *= -1
195
+ # TODO replace with own contact algorithm
196
+ points1 = [ContactPoint(p.index1, p.vertex1) for p in polytope]
197
+ points2 = [ContactPoint(p.index2, p.vertex2) for p in polytope]
153
198
 
154
- print('\033[92m', vec, distance, '\033[0m')
155
-
156
- # resolve collision penetration
157
- node2.position -= vec * distance
199
+ if glm.dot(vec, node2.position - node1.position) > 0: vec *= -1
158
200
 
159
- collided.append((node1, node2, vec * distance))
160
-
161
- # TODO add penetration resolution
162
- # TODO add impulse
201
+ # apply impulse if a collider has a physic body
202
+ if node1.physics_body or node2.physics_body:
203
+
204
+ # determine the contact points from the collision
205
+ points1, points2 = separate_polytope(points1, points2, vec)
206
+ self.merge_contact_points(vec, collider1, collider2, points1, points2)
207
+
208
+ # for manifold in self.contact_manifolds.values(): print(list(manifold.contact_points1.values()) + list(manifold.contact_points2.values()))
209
+
210
+ collider_tuple = (collider1, collider2)
211
+ # print(self.contact_manifolds[collider_tuple])
212
+ manifold = get_contact_manifold(
213
+ node1.position - vec,
214
+ vec,
215
+ self.contact_manifolds[collider_tuple].contact_points1.values(),
216
+ self.contact_manifolds[collider_tuple].contact_points2.values()
217
+ )
218
+
219
+ collision_normal = node1.velocity - node2.velocity
220
+ collision_normal = vec if glm.length2(collision_normal) < 1e-12 else glm.normalize(collision_normal)
221
+ calculate_collisions(collision_normal, node1, node2, manifold, node1.get_inverse_inertia(), node2.get_inverse_inertia(), node1.center_of_mass, node2.center_of_mass)
222
+
223
+ # for i, point in enumerate(manifold):
224
+ # self.scene.add(Node(position = point, scale = (0.1, 0.1, 0.1)))
163
225
 
164
- return collided
226
+ # resolve collision penetration
227
+ multiplier = 0.5 if not (node1.static or node2.static) else 1
228
+ if not node1.static: node1.position += multiplier * vec * distance
229
+ if not node2.static: node2.position -= multiplier * vec * distance
@@ -0,0 +1,91 @@
1
+ import glm
2
+ from random import randint
3
+ from .line_intersections import line_line_intersect, line_poly_intersect
4
+ from .graham_scan import graham_scan
5
+ from .sutherland_hodgman import sutherland_hodgman
6
+ from .dataclasses import ContactPoint
7
+
8
+ # sutherland hodgman clipping algorithm
9
+ def get_contact_manifold(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, points1:list[glm.vec3], points2:list[glm.vec3]) -> list[glm.vec3]:
10
+ """
11
+ computes the contact manifold for a collision between two nearby polyhedra
12
+ """
13
+ if len(points1) == 0 or len(points2) == 0: return []
14
+
15
+ # project vertices onto the 2d plane
16
+ points1 = project_points(contact_plane_point, contact_plane_normal, points1)
17
+ points2 = project_points(contact_plane_point, contact_plane_normal, points2)
18
+
19
+ # check if collsion was on a vertex
20
+ if len(points1) == 1: return points1
21
+ if len(points2) == 1: return points2
22
+
23
+ # convert points to 2d for intersection algorithms
24
+ points1, u1, v1 = points_to_2d(contact_plane_point, contact_plane_normal, points1)
25
+ points2, u2, v2 = points_to_2d(contact_plane_point, contact_plane_normal, points2, u1, v1) #TODO precalc orthogonal basis for 2d conversion
26
+
27
+ # convert arbitrary points to polygon
28
+ if len(points1) > 2: points1 = graham_scan(points1)
29
+ if len(points2) > 2: points2 = graham_scan(points2)
30
+
31
+ # run clipping algorithms
32
+ manifold = []
33
+ is_line1, is_line2 = len(points1) == 2, len(points2) == 2
34
+ if is_line1 and is_line2: manifold = line_line_intersect(points1, points2)
35
+ else:
36
+ if is_line1: manifold = line_poly_intersect(points1, points2)
37
+ elif is_line2: manifold = line_poly_intersect(points2, points1)
38
+ else: manifold = sutherland_hodgman(points1, points2)
39
+
40
+ # fall back if manifold fails to develope
41
+ if len(manifold) == 0: return []
42
+
43
+ # convert inertsection algorithm output to 3d
44
+ return points_to_3d(u1, v1, contact_plane_point, manifold)
45
+
46
+ def separate_polytope(points1: list[ContactPoint], points2: list[ContactPoint], contact_plane_normal, epsilon: float=1e-5) -> tuple[list[ContactPoint], list[ContactPoint]]:
47
+ """
48
+ Determines the potential contact manifold points of each shape based on their position along the penetrating axis
49
+ """
50
+ proj1 = [(glm.dot(point.vertex, contact_plane_normal), point) for point in points1]
51
+ proj2 = [(glm.dot(point.vertex, contact_plane_normal), point) for point in points2]
52
+
53
+ # min1 and max2 should be past the collising points of node2 and node1 respectively
54
+ min1 = min(proj1, key=lambda proj: proj[0])[0]
55
+ max2 = max(proj2, key=lambda proj: proj[0])[0]
56
+
57
+ proj1 = filter(lambda proj: proj[0] <= max2 + epsilon, proj1)
58
+ proj2 = filter(lambda proj: proj[0] + epsilon >= min1, proj2)
59
+
60
+ return [point[1] for point in proj1], [point[1] for point in proj2]
61
+
62
+ def distance_to_plane(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, point:glm.vec3) -> float:
63
+ """gets the smallest distance a point is from a plane"""
64
+ return glm.dot(point - contact_plane_point, contact_plane_normal) #TODO check this formula
65
+
66
+ def project_points(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, points:list[glm.vec3]) -> list[glm.vec3]:
67
+ """gets the projected positions of the given points onto the given plane"""
68
+ return [point - glm.dot(point - contact_plane_point, contact_plane_normal) * contact_plane_normal for point in points]
69
+
70
+ def points_to_2d(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, points:list[glm.vec3], u = None, v = None) -> tuple[list[glm.vec2], glm.vec3, glm.vec3]:
71
+ """converts a list of points on a plane to their 2d representation"""
72
+ # generate a new basis
73
+ k = get_noncolinear_vector(contact_plane_normal)
74
+ u = u if u else glm.normalize(glm.cross(contact_plane_normal, k))
75
+ v = v if v else glm.cross(contact_plane_normal, u)
76
+
77
+ # convert points to new basis
78
+ return [glm.vec2(glm.dot(vec := point - contact_plane_point, u), glm.dot(vec, v)) for point in points], u, v
79
+
80
+ def points_to_3d(u:glm.vec3, v:glm.vec3, contact_plane_point:glm.vec3, points:list[glm.vec2]) -> list[glm.vec3]:
81
+ """converts a list of points on a plane to their 3d representation"""
82
+ return [contact_plane_point + point.x * u + point.y * v for point in points]
83
+
84
+ # vector math
85
+ def get_noncolinear_vector(vector:glm.vec3) -> glm.vec3:
86
+ """generates a non colinear vector based on the given vector"""
87
+ test_vector = (1, 1, 1)
88
+ while glm.cross(test_vector, vector) == (0, 0, 0):
89
+ val = randint(0, 7) # 000 to 111
90
+ test_vector = (val & 1, val & 2, val & 4) # one random for three digits
91
+ return test_vector
@@ -0,0 +1,27 @@
1
+ import glm
2
+ from dataclasses import dataclass
3
+
4
+ # frozen because data does not need to be mutable
5
+ # used in creating polytopes for GJK/EPA
6
+ @dataclass(frozen=True)
7
+ class SupportPoint():
8
+ support_point: glm.vec3
9
+
10
+ index1: int # index of the vertex in the mesh
11
+ vertex1: glm.vec3 # world space location of the vertex at collision
12
+
13
+ index2: int
14
+ vertex2: glm.vec3
15
+
16
+ # used for generating contact points for the contact manifold
17
+ @dataclass(frozen=True)
18
+ class ContactPoint():
19
+ index: int
20
+ vertex: glm.vec3
21
+
22
+ # contact manifold object used in the contact handler list
23
+ @dataclass
24
+ class ContactManifold():
25
+ normal: glm.vec3
26
+ contact_points1: dict[int : glm.vec3] # contact point index : collision position
27
+ contact_points2: dict[int : glm.vec3]
@@ -0,0 +1,47 @@
1
+ # def sat_manifold(self, points1: list[glm.vec3], points2: list[glm.vec3], axis: glm.vec3, plane_point: glm.vec3, digit: int) -> list[glm.vec3]:
2
+ # """
3
+ # Returns the contact manifold from an SAT OBB OBB collision
4
+ # """
5
+ # def get_test_points(contact_plane_normal:glm.vec3, points:list[glm.vec3], count: int):
6
+ # test_points = [(glm.dot(contact_plane_normal, p), p) for p in points]
7
+ # test_points.sort(key=lambda p: p[0])
8
+ # return [p[1] for p in test_points[:count]]
9
+
10
+ # def get_test_points_unknown(contact_plane_normal:glm.vec3, points:list[glm.vec3]):
11
+ # test_points = [(glm.dot(contact_plane_normal, p), p) for p in points]
12
+ # test_points.sort(key=lambda p: p[0])
13
+ # if test_points[2][0] - test_points[0][0] > 1e-3: return [p[1] for p in test_points[:2]]
14
+ # else: return [p[1] for p in test_points[:4]]
15
+
16
+ # if digit < 6: # there must be at least one face in the collision
17
+ # reference, incident = (get_test_points(-axis, points1, 4), get_test_points_unknown(axis, points2)) if digit < 3 else (get_test_points(axis, points2, 4), get_test_points_unknown(-axis, points1))
18
+
19
+ # # project vertices onto the 2d plane
20
+ # reference = project_points(plane_point, axis, reference)
21
+ # incident = project_points(plane_point, axis, incident)
22
+
23
+ # # convert points to 2d for intersection algorithms
24
+ # reference, u1, v1 = points_to_2d(plane_point, axis, reference)
25
+ # incident, u2, v2 = points_to_2d(plane_point, axis, incident, u1, v1)
26
+
27
+ # # convert arbitrary points to polygon
28
+ # reference = graham_scan(reference)
29
+ # if len(incident) == 4: incident = graham_scan(incident)
30
+
31
+ # # run clipping algorithms
32
+ # manifold = []
33
+ # if len(incident) == 2: manifold = line_poly_intersect(incident, reference)
34
+ # else: manifold = sutherland_hodgman(reference, incident)
35
+
36
+ # # # fall back if manifold fails to develope
37
+ # assert len(manifold), 'sat did not generate points'
38
+
39
+ # # # convert inertsection algorithm output to 3d
40
+ # return points_to_3d(u1, v1, plane_point, manifold)
41
+
42
+ # else: # there is an edge edge collision
43
+
44
+ # points1 = get_test_points(-axis, points1, 2)
45
+ # points2 = get_test_points(axis, points2, 2)
46
+
47
+ # return closest_two_lines(*points1, *points2)
@@ -1,13 +1,14 @@
1
1
  import glm
2
2
  from .helper import get_support_point
3
+ from .dataclasses import SupportPoint
3
4
  from...nodes.node import Node
4
5
 
5
6
 
6
7
  # TODO change these to structs when converting to C++
7
8
  face_type = list[tuple[float, glm.vec3, glm.vec3, int, int, int]] # distance, normal, center, index 1, index 2, index 3
8
- polytope_type = list[tuple[glm.vec3, glm.vec3, glm.vec3]] # polytope vertex, node1 vertex, node2 vertex
9
+ polytope_type = list[SupportPoint] # polytope vertex, node1 vertex, node2 vertex
9
10
 
10
- def get_epa_from_gjk(node1: Node, node2: Node, polytope: polytope_type, epsilon: float=1e-7) -> tuple: # TODO determine the return type of get_epa_from_gjk
11
+ def get_epa_from_gjk(node1: Node, node2: Node, polytope: polytope_type, epsilon: float=0) -> tuple[face_type, polytope_type]: # TODO determine the return type of get_epa_from_gjk and if epsilon is good value
11
12
  """
12
13
  Determines the peneration vector from a collision using EPA. The returned face normal is normalized but the rest are not guarunteed to be.
13
14
  """
@@ -18,14 +19,10 @@ def get_epa_from_gjk(node1: Node, node2: Node, polytope: polytope_type, epsilon:
18
19
  # develope the polytope until the nearest real face has been found, within epsilon
19
20
  while True:
20
21
  new_point = get_support_point(node1, node2, faces[0][1])
21
- if new_point in polytope or glm.length(new_point[0]) - faces[0][0] < epsilon:
22
- faces[0] = list(faces[0])
23
- faces[0][0] = glm.sqrt(faces[0][0]) # square root distance squared to get real distance
24
- faces[0][1] = glm.normalize(faces[0][1])
25
- return faces[0], polytope
22
+ if new_point in polytope or glm.length(new_point.support_point) - faces[0][0] < epsilon: return faces[0], polytope
26
23
  faces, polytope = insert_point(polytope, faces, new_point)
27
24
 
28
- def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, epsilon: float=1e-7) -> tuple[face_type, polytope_type]:
25
+ def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, epsilon: float=0) -> tuple[face_type, polytope_type]:
29
26
  """
30
27
  Inserts a point into the polytope sorting by distance from the origin
31
28
  """
@@ -34,8 +31,8 @@ def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, eps
34
31
  support_index = len(polytope) - 1
35
32
  visible_faces = [
36
33
  face for face in faces
37
- if glm.dot(face[1], polytope[support_index][0]) >= epsilon and # if the normal of a face is pointing towards the added point
38
- glm.dot(face[1], polytope[support_index][0] - face[2]) >= epsilon # TODO check if this ever occurs
34
+ if glm.dot(face[1], polytope[support_index].support_point) >= epsilon and # if the normal of a face is pointing towards the added point
35
+ glm.dot(face[1], polytope[support_index].support_point - face[2]) >= epsilon # TODO check if this ever occurs
39
36
  ]
40
37
 
41
38
  # generate new edges
@@ -43,6 +40,9 @@ def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, eps
43
40
  for face in visible_faces:
44
41
  for p1, p2 in get_face_edges(face):
45
42
  if (p2, p1) in edges: edges.remove((p2, p1)) # edges can only be shared by two faces, running opposite to each other.
43
+ elif (p1, p2) in edges: # TODO remove this
44
+ edges.remove((p1, p2))
45
+ # print('not reversed')
46
46
  else: edges.append((p1, p2))
47
47
 
48
48
  # remove visible faces
@@ -58,9 +58,15 @@ def insert_face(polytope: polytope_type, faces: face_type, indices: tuple[int, i
58
58
  """
59
59
  Inserts a face into the face priority queue based on the indices given in the polytope
60
60
  """
61
- center = (polytope[indices[0]][0] + polytope[indices[1]][0] + polytope[indices[2]][0]) / 3
62
- distance = glm.length2(center) # TODO face distance is length squared to reduce calculations
63
- normal = glm.cross(polytope[indices[1]][0] - polytope[indices[0]][0], polytope[indices[2]][0] - polytope[indices[0]][0]) # closest face normal will be normalized once returned to avoid square roots and division
61
+ center = (polytope[indices[0]].support_point + polytope[indices[1]].support_point + polytope[indices[2]].support_point) / 3
62
+ normal = glm.cross(polytope[indices[1]].support_point - polytope[indices[0]].support_point, polytope[indices[2]].support_point - polytope[indices[0]].support_point) # closest face normal will be normalized once returned to avoid square roots and division
63
+ if glm.dot(center, normal) < 0:
64
+ normal *= -1
65
+ indices = (indices[2], indices[1], indices[0])
66
+
67
+ # TODO solve cases where face may contain origin
68
+ normal = glm.normalize(normal)
69
+ distance = abs(glm.dot(polytope[indices[0]].support_point, normal))
64
70
  new_face = (distance, normal, center, *indices)
65
71
 
66
72
  # insert faces into priority queue based on distance from origin
@@ -72,11 +78,11 @@ def insert_face(polytope: polytope_type, faces: face_type, indices: tuple[int, i
72
78
 
73
79
  return faces
74
80
 
75
- def orient_face(polytope: polytope_type, indices: tuple[int, int, int]) -> tuple[int, int, int]: # TODO finish this
81
+ def orient_face(polytope: polytope_type, indices: tuple[int, int, int]) -> tuple[int, int, int]:
76
82
  """
77
83
  Orients the face indices to have a counter clockwise normal
78
84
  """
79
- if glm.dot(glm.cross(polytope[indices[1]][0], polytope[indices[2]][0]), polytope[indices[0]][0]) < 0: return (indices[2], indices[1], indices[0])
85
+ if glm.dot(glm.cross(polytope[indices[1]].support_point, polytope[indices[2]].support_point), polytope[indices[0]].support_point) < 0: return (indices[2], indices[1], indices[0])
80
86
  return indices
81
87
 
82
88
  def get_face_edges(face: tuple[float, glm.vec3, glm.vec3, int, int, int]) -> list[tuple[int, int]]: