basilisk-engine 0.0.8__py3-none-any.whl → 0.1.0__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 (40) hide show
  1. basilisk/__init__.py +3 -1
  2. basilisk/collisions/broad/broad_bvh.py +31 -2
  3. basilisk/collisions/collider.py +6 -5
  4. basilisk/collisions/collider_handler.py +87 -24
  5. basilisk/collisions/narrow/contact_manifold.py +92 -0
  6. basilisk/collisions/narrow/epa.py +13 -8
  7. basilisk/collisions/narrow/graham_scan.py +25 -0
  8. basilisk/collisions/narrow/helper.py +7 -1
  9. basilisk/collisions/narrow/line_intersections.py +107 -0
  10. basilisk/collisions/narrow/sutherland_hodgman.py +24 -0
  11. basilisk/draw/draw_handler.py +3 -3
  12. basilisk/engine.py +16 -3
  13. basilisk/generic/collisions.py +1 -2
  14. basilisk/generic/quat.py +10 -2
  15. basilisk/generic/vec3.py +9 -1
  16. basilisk/input/mouse.py +3 -3
  17. basilisk/nodes/node.py +48 -77
  18. basilisk/nodes/node_handler.py +8 -4
  19. basilisk/physics/impulse.py +119 -0
  20. basilisk/physics/physics_engine.py +1 -1
  21. basilisk/render/batch.py +2 -0
  22. basilisk/render/camera.py +30 -1
  23. basilisk/render/chunk_handler.py +10 -1
  24. basilisk/render/frame.py +2 -2
  25. basilisk/render/image_handler.py +14 -12
  26. basilisk/render/light_handler.py +2 -2
  27. basilisk/render/material.py +13 -13
  28. basilisk/render/material_handler.py +11 -7
  29. basilisk/render/shader.py +110 -0
  30. basilisk/render/shader_handler.py +18 -34
  31. basilisk/render/sky.py +4 -3
  32. basilisk/scene.py +3 -2
  33. basilisk/shaders/geometry.frag +9 -0
  34. basilisk/shaders/geometry.vert +42 -0
  35. basilisk/shaders/normal.frag +60 -0
  36. basilisk/shaders/normal.vert +92 -0
  37. {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/METADATA +1 -1
  38. {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/RECORD +40 -30
  39. {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/WHEEL +0 -0
  40. {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.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
@@ -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,5 @@ 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 []
@@ -39,17 +39,18 @@ class Collider():
39
39
  self.collider_handler = collider_handler
40
40
  self.node = node
41
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
42
+ self.static_friction = static_friction if elasticity else 0.8
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
51
+ self.needs_obb = True # pos, scale, rot
52
+ self.needs_half_dimensions = True # scale, rot
53
+ self.needs_bvh = True # pos, scale, rot
53
54
 
54
55
  @property
55
56
  def has_collided(self): return bool(self.collisions)
@@ -1,11 +1,16 @@
1
1
  import glm
2
- from .narrow.gjk import *
2
+
3
+ from basilisk.collisions.narrow.graham_scan import graham_scan
4
+ from basilisk.collisions.narrow.sutherland_hodgman import sutherland_hodgman
3
5
  from .collider import Collider
4
6
  from .broad.broad_bvh import BroadBVH
5
- from ..mesh.cube import Cube
6
- from ..generic.collisions import get_sat_axes
7
7
  from .narrow.gjk import collide_gjk
8
8
  from .narrow.epa import get_epa_from_gjk
9
+ from .narrow.contact_manifold import get_contact_manifold, points_to_2d, points_to_3d, project_points
10
+ from .narrow.line_intersections import closest_two_lines, line_poly_intersect
11
+ from ..nodes.node import Node
12
+ from ..generic.collisions import get_sat_axes
13
+ from ..physics.impulse import calculate_collisions
9
14
 
10
15
  class ColliderHandler():
11
16
  scene: ...
@@ -44,9 +49,15 @@ class ColliderHandler():
44
49
  """
45
50
  # reset collision data
46
51
  for collider in self.colliders: collider.collisions = {}
47
- # TODO update BVH
52
+ # update BVH
53
+ for collider in self.colliders:
54
+ if collider.needs_bvh:
55
+ self.bvh.remove(collider)
56
+ self.bvh.add(collider)
57
+
58
+ # resolve collisions
48
59
  broad_collisions = self.resolve_broad_collisions()
49
- self.resolve_narrow_collisions(broad_collisions)
60
+ self.resolve_narrow_collisions(broad_collisions)
50
61
 
51
62
  def collide_obb_obb(self, collider1: Collider, collider2: Collider) -> tuple[glm.vec3, float] | None:
52
63
  """
@@ -59,7 +70,8 @@ class ColliderHandler():
59
70
  # test axes
60
71
  small_axis = None
61
72
  small_overlap = 1e10
62
- for axis in axes: # TODO add optimization for points on cardinal axis of cuboid
73
+ small_index = 0
74
+ for i, axis in enumerate(axes): # TODO add optimization for points on cardinal axis of cuboid
63
75
  # "project" points
64
76
  proj1 = [glm.dot(p, axis) for p in points1]
65
77
  proj2 = [glm.dot(p, axis) for p in points2]
@@ -69,17 +81,63 @@ class ColliderHandler():
69
81
 
70
82
  # if lines are not intersecting
71
83
  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)
84
+ elif max2 > max1 and min2 < min1: overlap = min(max2 - min1, max1 - min2)
73
85
  else: overlap = min(max1, max2) - max(min1, min2) # TODO check if works with containment
74
86
 
75
87
  if abs(overlap) > abs(small_overlap): continue
76
88
  small_overlap = overlap
77
89
  small_axis = axis
90
+ small_index = i
91
+
92
+ return small_axis, small_overlap, small_index
93
+
94
+ def sat_manifold(self, points1: list[glm.vec3], points2: list[glm.vec3], axis: glm.vec3, plane_point: glm.vec3, digit: int) -> list[glm.vec3]:
95
+ """
96
+ Returns the contact manifold from an SAT OBB OBB collision
97
+ """
98
+ def get_test_points(contact_plane_normal:glm.vec3, points:list[glm.vec3], count: int):
99
+ test_points = [(glm.dot(contact_plane_normal, p), p) for p in points]
100
+ test_points.sort(key=lambda p: p[0])
101
+ return [p[1] for p in test_points[:count]]
102
+
103
+ def get_test_points_unknown(contact_plane_normal:glm.vec3, points:list[glm.vec3]):
104
+ test_points = [(glm.dot(contact_plane_normal, p), p) for p in points]
105
+ test_points.sort(key=lambda p: p[0])
106
+ if test_points[2][0] - test_points[0][0] > 1e-3: return [p[1] for p in test_points[:2]]
107
+ else: return [p[1] for p in test_points[:4]]
108
+
109
+ if digit < 6: # there must be at least one face in the collision
110
+ 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))
111
+
112
+ # project vertices onto the 2d plane
113
+ reference = project_points(plane_point, axis, reference)
114
+ incident = project_points(plane_point, axis, incident)
115
+
116
+ # convert points to 2d for intersection algorithms
117
+ reference, u1, v1 = points_to_2d(plane_point, axis, reference)
118
+ incident, u2, v2 = points_to_2d(plane_point, axis, incident, u1, v1) #TODO precalc orthogonal basis for 2d conversion
119
+
120
+ # convert arbitrary points to polygon
121
+ reference = graham_scan(reference)
122
+ if len(incident) == 4: incident = graham_scan(incident)
123
+
124
+ # run clipping algorithms
125
+ manifold = []
126
+ if len(incident) == 2: manifold = line_poly_intersect(incident, reference)
127
+ else: manifold = sutherland_hodgman(reference, incident)
128
+
129
+ # # fall back if manifold fails to develope
130
+ assert len(manifold), 'sat did not generate points'
131
+
132
+ # # convert inertsection algorithm output to 3d
133
+ return points_to_3d(u1, v1, plane_point, manifold)
78
134
 
79
- print(axes.index(small_axis), glm.length(small_axis))
80
- print('overlap:', small_overlap)
135
+ else: # there is an edge edge collision
136
+
137
+ points1 = get_test_points(-axis, points1, 2)
138
+ points2 = get_test_points(axis, points2, 2)
81
139
 
82
- return small_axis, small_overlap
140
+ return closest_two_lines(*points1, *points2)
83
141
 
84
142
  def collide_obb_obb_decision(self, collider1: Collider, collider2: Collider) -> bool:
85
143
  """
@@ -124,8 +182,6 @@ class ColliderHandler():
124
182
  """
125
183
  Determines if two colliders are colliding, if so resolves their penetration and applies impulse
126
184
  """
127
- collided = []
128
-
129
185
  for collision in broad_collisions: # assumes that broad collisions are unique
130
186
  collider1 = collision[0]
131
187
  collider2 = collision[1]
@@ -139,7 +195,11 @@ class ColliderHandler():
139
195
  data = self.collide_obb_obb(collider1, collider2)
140
196
  if not data: continue
141
197
 
142
- vec, distance = data
198
+ vec, distance, index = data
199
+
200
+ # TODO replace with own contact algorithm
201
+ points1 = collider1.obb_points
202
+ points2 = collider2.obb_points
143
203
 
144
204
  else: # use gjk to determine collisions between non-cuboid meshes
145
205
  has_collided, simplex = collide_gjk(node1, node2)
@@ -148,17 +208,20 @@ class ColliderHandler():
148
208
  face, polytope = get_epa_from_gjk(node1, node2, simplex)
149
209
  vec, distance = face[1], face[0]
150
210
 
151
- if glm.dot(vec, node2.position - node1.position) > 0:
152
- vec *= -1
211
+ # TODO replace with own contact algorithm
212
+ points1 = [p[1] for p in polytope]
213
+ points2 = [p[2] for p in polytope]
153
214
 
154
- print('\033[92m', vec, distance, '\033[0m')
155
-
156
- # resolve collision penetration
157
- node2.position -= vec * distance
158
-
159
- collided.append((node1, node2, vec * distance))
215
+ if glm.dot(vec, node2.position - node1.position) > 0: vec *= -1
160
216
 
161
- # TODO add penetration resolution
162
- # TODO add impulse
217
+ if node1.physics_body or node2.physics_body:
218
+ manifold = get_contact_manifold(node1.position - vec, vec, points1, points2)
219
+ if len(manifold) == 0:
220
+ print('manifold failed to generate')
221
+ continue
222
+ calculate_collisions(vec, node1, node2, manifold, node1.get_inverse_inertia(), node2.get_inverse_inertia(), node1.center_of_mass, node2.center_of_mass)
163
223
 
164
- return collided
224
+ # resolve collision penetration
225
+ multiplier = 0.5 if not (node1.static or node2.static) else 1
226
+ if not node1.static: node1.position += multiplier * vec * distance
227
+ if not node2.static: node2.position -= multiplier * vec * distance
@@ -0,0 +1,92 @@
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
+
7
+ # sutherland hodgman clipping algorithm
8
+ 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]:
9
+ """
10
+ computes the contact manifold for a collision between two nearby polyhedra
11
+ """
12
+ # determine the contact points from the collision
13
+ points1, points2 = separate_polytope(points1, points2, contact_plane_normal)
14
+
15
+ if len(points1) == 0 or len(points2) == 0: return []
16
+
17
+ # project vertices onto the 2d plane
18
+ points1 = project_points(contact_plane_point, contact_plane_normal, points1)
19
+ points2 = project_points(contact_plane_point, contact_plane_normal, points2)
20
+
21
+ # check if collsion was on a vertex
22
+ if len(points1) == 1: return points1
23
+ if len(points2) == 1: return points2
24
+
25
+ # convert points to 2d for intersection algorithms
26
+ points1, u1, v1 = points_to_2d(contact_plane_point, contact_plane_normal, points1)
27
+ points2, u2, v2 = points_to_2d(contact_plane_point, contact_plane_normal, points2, u1, v1) #TODO precalc orthogonal basis for 2d conversion
28
+
29
+ # convert arbitrary points to polygon
30
+ if len(points1) > 2: points1 = graham_scan(points1)
31
+ if len(points2) > 2: points2 = graham_scan(points2)
32
+
33
+ # run clipping algorithms
34
+ manifold = []
35
+ is_line1, is_line2 = len(points1) == 2, len(points2) == 2
36
+ if is_line1 and is_line2: manifold = line_line_intersect(points1, points2)
37
+ else:
38
+ if is_line1: manifold = line_poly_intersect(points1, points2)
39
+ elif is_line2: manifold = line_poly_intersect(points2, points1)
40
+ else: manifold = sutherland_hodgman(points1, points2)
41
+
42
+ # fall back if manifold fails to develope
43
+ if len(manifold) == 0: return []
44
+ # convert inertsection algorithm output to 3d
45
+ return points_to_3d(u1, v1, contact_plane_point, manifold)
46
+
47
+ def separate_polytope(points1: list[glm.vec3], points2: list[glm.vec3], contact_plane_normal) -> list[glm.vec3]:
48
+ """
49
+ Determines the potential contact manifold points of each shape based on their position along the penetrating axis
50
+ """
51
+ proj1 = [(glm.dot(point, contact_plane_normal), point) for point in points1]
52
+ proj2 = [(glm.dot(point, contact_plane_normal), point) for point in points2]
53
+
54
+ # min1 and max2 should be past the collising points of node2 and node1 respectively
55
+ min1 = min(proj1, key=lambda point: point[0])
56
+ max2 = max(proj2, key=lambda point: point[0])
57
+
58
+ proj1 = filter(lambda proj: proj < max2, proj1)
59
+ proj2 = filter(lambda proj: proj > min1, proj2)
60
+
61
+ return [point[1] for point in proj1], [point[1] for point in proj2]
62
+
63
+ def distance_to_plane(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, point:glm.vec3) -> float:
64
+ """gets the smallest distance a point is from a plane"""
65
+ return glm.dot(point - contact_plane_point, contact_plane_normal) #TODO check this formula
66
+
67
+ def project_points(contact_plane_point:glm.vec3, contact_plane_normal:glm.vec3, points:list[glm.vec3]) -> list[glm.vec3]:
68
+ """gets the projected positions of the given points onto the given plane"""
69
+ return [point - glm.dot(point - contact_plane_point, contact_plane_normal) * contact_plane_normal for point in points]
70
+
71
+ 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]:
72
+ """converts a list of points on a plane to their 2d representation"""
73
+ # generate a new basis
74
+ k = get_noncolinear_vector(contact_plane_normal)
75
+ u = u if u else glm.normalize(glm.cross(contact_plane_normal, k))
76
+ v = v if v else glm.cross(contact_plane_normal, u)
77
+
78
+ # convert points to new basis
79
+ return [glm.vec2(glm.dot(vec := point - contact_plane_point, u), glm.dot(vec, v)) for point in points], u, v
80
+
81
+ def points_to_3d(u:glm.vec3, v:glm.vec3, contact_plane_point:glm.vec3, points:list[glm.vec2]) -> list[glm.vec3]:
82
+ """converts a list of points on a plane to their 3d representation"""
83
+ return [contact_plane_point + point.x * u + point.y * v for point in points]
84
+
85
+ # vector math
86
+ def get_noncolinear_vector(vector:glm.vec3) -> glm.vec3:
87
+ """generates a non colinear vector based on the given vector"""
88
+ test_vector = (1, 1, 1)
89
+ while glm.cross(test_vector, vector) == (0, 0, 0):
90
+ val = randint(0, 7) # 000 to 111
91
+ test_vector = (val & 1, val & 2, val & 4) # one random for three digits
92
+ return test_vector
@@ -7,7 +7,7 @@ from...nodes.node import Node
7
7
  face_type = list[tuple[float, glm.vec3, glm.vec3, int, int, int]] # distance, normal, center, index 1, index 2, index 3
8
8
  polytope_type = list[tuple[glm.vec3, glm.vec3, glm.vec3]] # polytope vertex, node1 vertex, node2 vertex
9
9
 
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
10
+ def get_epa_from_gjk(node1: Node, node2: Node, polytope: polytope_type, epsilon: float=0) -> tuple: # TODO determine the return type of get_epa_from_gjk and if epsilon is good value
11
11
  """
12
12
  Determines the peneration vector from a collision using EPA. The returned face normal is normalized but the rest are not guarunteed to be.
13
13
  """
@@ -18,14 +18,10 @@ def get_epa_from_gjk(node1: Node, node2: Node, polytope: polytope_type, epsilon:
18
18
  # develope the polytope until the nearest real face has been found, within epsilon
19
19
  while True:
20
20
  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
21
+ if new_point in polytope or glm.length(new_point[0]) - faces[0][0] < epsilon: return faces[0], polytope
26
22
  faces, polytope = insert_point(polytope, faces, new_point)
27
23
 
28
- def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, epsilon: float=1e-7) -> tuple[face_type, polytope_type]:
24
+ def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, epsilon: float=0) -> tuple[face_type, polytope_type]:
29
25
  """
30
26
  Inserts a point into the polytope sorting by distance from the origin
31
27
  """
@@ -43,6 +39,9 @@ def insert_point(polytope: polytope_type, faces: face_type, point: glm.vec3, eps
43
39
  for face in visible_faces:
44
40
  for p1, p2 in get_face_edges(face):
45
41
  if (p2, p1) in edges: edges.remove((p2, p1)) # edges can only be shared by two faces, running opposite to each other.
42
+ elif (p1, p2) in edges: # TODO remove this
43
+ edges.remove((p1, p2))
44
+ # print('not reversed')
46
45
  else: edges.append((p1, p2))
47
46
 
48
47
  # remove visible faces
@@ -59,8 +58,14 @@ def insert_face(polytope: polytope_type, faces: face_type, indices: tuple[int, i
59
58
  Inserts a face into the face priority queue based on the indices given in the polytope
60
59
  """
61
60
  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
61
  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
62
+ if glm.dot(center, normal) < 0:
63
+ normal *= -1
64
+ indices = (indices[2], indices[1], indices[0])
65
+
66
+ # TODO solve cases where face may contain origin
67
+ normal = glm.normalize(normal)
68
+ distance = abs(glm.dot(polytope[indices[0]][0], normal))
64
69
  new_face = (distance, normal, center, *indices)
65
70
 
66
71
  # insert faces into priority queue based on distance from origin
@@ -0,0 +1,25 @@
1
+ import glm
2
+ from math import atan2
3
+ from .helper import is_ccw_turn
4
+
5
+ def graham_scan(points:list[glm.vec2]) -> None:
6
+ """converts list of arbitrary points into polygon sorted ccw"""
7
+ # get pivot point
8
+ pivot = min(points, key=lambda p: (p.y, p.x))
9
+ points.remove(pivot)
10
+
11
+ # sort points by polar angle and start hull
12
+ points = sorted(points, key=lambda p: (get_polar_angle(pivot, p), glm.length(pivot - p)))
13
+ hull = [pivot, points.pop(0)]
14
+
15
+ for point in points:
16
+ while len(hull) > 1 and not is_ccw_turn(hull[-2], hull[-1], point):
17
+ hull.pop()
18
+ hull.append(point)
19
+
20
+ return hull
21
+
22
+ def get_polar_angle(pivot:glm.vec2, point:glm.vec2) -> float:
23
+ """gets the polar angle between two points from the horizontal"""
24
+ vector = point - pivot
25
+ return atan2(vector.y, vector.x)
@@ -20,4 +20,10 @@ def get_furthest_point(node: Node, dir_vec: glm.vec3) -> glm.vec3:
20
20
  vertex = node.model_matrix * glm.vec4(vertex, 1.0)
21
21
 
22
22
  # transform point to world space
23
- return glm.vec3(vertex)
23
+ return glm.vec3(vertex)
24
+
25
+ def is_ccw_turn(a:glm.vec2, b:glm.vec2, c:glm.vec2) -> bool:
26
+ """
27
+ Determines if the series of points results in a left hand turn
28
+ """
29
+ return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0 # TODO check formula
@@ -0,0 +1,107 @@
1
+ import glm
2
+ from .helper import is_ccw_turn
3
+
4
+ # intersectionn algorithms for lines
5
+ def line_line_intersect(points1:list[glm.vec2], points2:list[glm.vec2]) -> list[glm.vec2]:
6
+ """gets the intersection of 2 2d lines. if the lines are parallel, returns the second line"""
7
+ # orders points from smallest x to greatest x
8
+ points1 = sorted(points1, key=lambda p: (p.x, p.y))
9
+ points2 = sorted(points2, key=lambda p: (p.x, p.y))
10
+ vec1, vec2 = points1[1] - points1[0], points2[1] - points2[0]
11
+
12
+ # if vectors have the same slope return the smallest line
13
+ if have_same_slope(vec1, vec2): return sorted(points1 + points2, key=lambda p: (p.x, p.y))[1:3]
14
+
15
+ # line - line intersection
16
+ det = vec1.x * vec2.y - vec1.y * vec2.x
17
+ if det == 0: return []
18
+ t = (points2[0].x - points1[0].x) * vec2.y - (points2[0].y - points1[0].y) * vec2.x
19
+ t /= det
20
+ return [points1[0] + t * vec1]
21
+
22
+ def have_same_slope(vec1:glm.vec2, vec2:glm.vec2, epsilon:float=1e-5) -> bool:
23
+ """determines if two vectors moving in the positive x direction have the same slope"""
24
+ return abs(vec1.y * vec2.x - vec2.y * vec1.x) < epsilon
25
+
26
+ def line_poly_intersect(line:list[glm.vec2], polygon:list[glm.vec2]) -> list[glm.vec2]: #TODO Reseach into faster algorithm < O(2n)
27
+ """computes which parts of the line clip with the polygon"""
28
+ # calculate the center of the polygon
29
+ assert len(polygon) > 2, 'polygon is does not contain engough points'
30
+ center = glm.vec2(0,0)
31
+ for point in polygon: center += point
32
+ center /= len(polygon)
33
+ orig_line = line[:]
34
+ # determine which points are in or out of the polygon
35
+ exterior_points = []
36
+ for i in range(len(polygon)): # nearest even number below n
37
+ for line_point in line[:]:
38
+ if not is_ccw_turn(polygon[i], polygon[(i + 1) % len(polygon)], line_point): # if point is on the outside
39
+ exterior_points.append((polygon[i], polygon[(i + 1) % len(polygon)], line_point)) # polypoint1, polypoint2, linepoint
40
+ line.remove(line_point) # removes line point if it is confirmed to be outside
41
+
42
+ # determine what to with line based on number of points found outside
43
+ if len(exterior_points) == 0:
44
+ return line
45
+ if len(exterior_points) == 1:
46
+ return line_line_intersect(line + [exterior_points[0][2]], exterior_points[0][0:2]) + [line[0]] # [intersecting point, exterior point]
47
+ if len(exterior_points) == 2: # line must intersect with two edges
48
+ points = []
49
+ for i in range(len(polygon)):
50
+ intersection = line_line_intersect(orig_line, [polygon[i], polygon[(i + 1) % len(polygon)]])
51
+ if len(intersection) > 0: points += intersection
52
+ if len(points) > 1: break # exit if two intersections have been found
53
+ else: return [] # fallback if 0 or one intersections found
54
+ return points
55
+
56
+ def closest_two_lines(p1: glm.vec3, q1: glm.vec3, p2: glm.vec3, q2: glm.vec3, epsilon: float=1e-7) -> tuple[glm.vec3, glm.vec3]:
57
+ """
58
+ Determines the closest point on each line segment to the other line segment.
59
+ """
60
+ # create direction vector
61
+ d1 = q1 - p1
62
+ d2 = q2 - p2
63
+ r = p1 - p2
64
+
65
+ # get lengths of line segments
66
+ a = glm.dot(d1, d1)
67
+ e = glm.dot(d2, d2)
68
+ f = glm.dot(d2, r)
69
+
70
+ # check if either or both line segment degenerate into points
71
+ if a <= epsilon and e <= epsilon:
72
+ # both segments degenerate
73
+ return p1, p2
74
+
75
+ if a <= epsilon:
76
+ s = 0
77
+ t = glm.clamp(f / e, 0, 1)
78
+ else:
79
+ c = glm.dot(d1, r)
80
+ if e <= epsilon:
81
+ # the second line degenerates to a point
82
+ t = 0
83
+ s = glm.clamp(-c / a, 0, 1)
84
+ else:
85
+ # if neither of them degenerate to a point
86
+ b = glm.dot(d1, d2)
87
+ denom = a * e - b ** 2 # this will always be non-negative
88
+
89
+ # if segments are not parallel, compute closest point from l1 to l2
90
+ s = glm.clamp((b * f - c * e) / denom, 0, 1) if denom else 0
91
+
92
+ # compute closest point from l2 on s1(s)
93
+ t = (b * s + f) / e
94
+
95
+ # if t is not in [0, 1], clamp and recompute s
96
+ if t < 0:
97
+ t = 0
98
+ s = glm.clamp(-c / a, 0, 1)
99
+ elif t > 1:
100
+ t = 1
101
+ s = glm.clamp((b - c) / a, 0, 1)
102
+
103
+ c1 = p1 + d1 * s
104
+ c2 = p2 + d2 * t
105
+ return c1, c2
106
+
107
+
@@ -0,0 +1,24 @@
1
+ import glm
2
+ from .helper import is_ccw_turn
3
+ from .line_intersections import line_line_intersect
4
+
5
+ def sutherland_hodgman(subject:list[glm.vec2], clip:list[glm.vec2]) -> list[glm.vec2]:
6
+ """determines the clipped polygon vertices from ccw oriented polygons"""
7
+ output_poly = subject
8
+
9
+ for i in range(len(clip)):
10
+ input_poly = output_poly
11
+ output_poly = []
12
+
13
+ edge_start, edge_end = clip[i], clip[(i + 1) % len(clip)]
14
+ for j in range(len(input_poly)):
15
+ prev_point, curr_point = input_poly[j - 1], input_poly[j]
16
+
17
+ if is_ccw_turn(curr_point, edge_start, edge_end):
18
+ if not is_ccw_turn(prev_point, edge_start, edge_end):
19
+ output_poly += line_line_intersect([edge_end, edge_start], [prev_point, curr_point])
20
+ output_poly.append(curr_point)
21
+ elif is_ccw_turn(prev_point, edge_start, edge_end):
22
+ output_poly += line_line_intersect([edge_end, edge_start], [prev_point, curr_point])
23
+
24
+ return output_poly
@@ -16,9 +16,9 @@ class DrawHandler():
16
16
  """2D draw program"""
17
17
  draw_data: list[float]
18
18
  """Temporary buffer for user draw calls"""
19
- vbo: mgl.Buffer
19
+ vbo: mgl.Buffer=None
20
20
  """Buffer for all 2D draws"""
21
- vao: mgl.VertexArray
21
+ vao: mgl.VertexArray=None
22
22
  """VAO for rendering all 2D draw calls"""
23
23
 
24
24
  def __init__(self, scene) -> None:
@@ -28,7 +28,7 @@ class DrawHandler():
28
28
  self.ctx = scene.engine.ctx
29
29
 
30
30
  # Get the program
31
- self.program = self.scene.shader_handler.programs['draw']
31
+ self.program = self.scene.shader_handler.shaders['draw'].program
32
32
 
33
33
  # Initialize draw data as blank
34
34
  self.draw_data = []
basilisk/engine.py CHANGED
@@ -5,7 +5,8 @@ import moderngl as mgl
5
5
  from .config import Config
6
6
  from .input.mouse import Mouse
7
7
  from .mesh.cube import Cube
8
- import time
8
+ from .render.shader import Shader
9
+
9
10
 
10
11
  class Engine():
11
12
  win_size: tuple
@@ -90,6 +91,9 @@ class Engine():
90
91
  # Scene being used by the engine
91
92
  self.scene = None
92
93
 
94
+ # Load a default shader
95
+ self.shader = Shader(self, self.root + '/shaders/batch.vert', self.root + '/shaders/batch.frag')
96
+
93
97
  # Set the scene to running
94
98
  self.running = True
95
99
 
@@ -167,8 +171,17 @@ class Engine():
167
171
 
168
172
  @property
169
173
  def scene(self): return self._scene
170
-
174
+ @property
175
+ def shader(self): return self._shader
176
+
171
177
  @scene.setter
172
178
  def scene(self, value):
173
179
  self._scene = value
174
- if self._scene: self._scene.set_engine(self)
180
+ if self._scene:
181
+ self._scene.set_engine(self)
182
+ self.shader.use()
183
+
184
+ @shader.setter
185
+ def shader(self, value):
186
+ self._shader = value
187
+ if self.scene: value.use()
@@ -14,13 +14,12 @@ def get_sat_axes(rotation1: glm.quat, rotation2: glm.quat) -> list[glm.vec3]:
14
14
  axes = []
15
15
  axes.extend(glm.transpose(glm.mat3_cast(rotation1)))
16
16
  axes.extend(glm.transpose(glm.mat3_cast(rotation2)))
17
- # axes.extend(glm.mat3_cast(rotation1))
18
- # axes.extend(glm.mat3_cast(rotation2))
19
17
 
20
18
  # crossed roots
21
19
  for i in range(0, 3):
22
20
  for j in range(3, 6):
23
21
  cross = glm.cross(axes[i], axes[j])
22
+ if glm.length2(cross) < 1e-6: continue
24
23
  axes.append(glm.normalize(cross))
25
24
 
26
25
  return axes