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.
- basilisk/__init__.py +3 -1
- basilisk/collisions/broad/broad_bvh.py +31 -2
- basilisk/collisions/collider.py +6 -5
- basilisk/collisions/collider_handler.py +87 -24
- basilisk/collisions/narrow/contact_manifold.py +92 -0
- basilisk/collisions/narrow/epa.py +13 -8
- basilisk/collisions/narrow/graham_scan.py +25 -0
- basilisk/collisions/narrow/helper.py +7 -1
- basilisk/collisions/narrow/line_intersections.py +107 -0
- basilisk/collisions/narrow/sutherland_hodgman.py +24 -0
- basilisk/draw/draw_handler.py +3 -3
- basilisk/engine.py +16 -3
- basilisk/generic/collisions.py +1 -2
- basilisk/generic/quat.py +10 -2
- basilisk/generic/vec3.py +9 -1
- basilisk/input/mouse.py +3 -3
- basilisk/nodes/node.py +48 -77
- basilisk/nodes/node_handler.py +8 -4
- basilisk/physics/impulse.py +119 -0
- basilisk/physics/physics_engine.py +1 -1
- basilisk/render/batch.py +2 -0
- basilisk/render/camera.py +30 -1
- basilisk/render/chunk_handler.py +10 -1
- basilisk/render/frame.py +2 -2
- basilisk/render/image_handler.py +14 -12
- basilisk/render/light_handler.py +2 -2
- basilisk/render/material.py +13 -13
- basilisk/render/material_handler.py +11 -7
- basilisk/render/shader.py +110 -0
- basilisk/render/shader_handler.py +18 -34
- basilisk/render/sky.py +4 -3
- basilisk/scene.py +3 -2
- basilisk/shaders/geometry.frag +9 -0
- basilisk/shaders/geometry.vert +42 -0
- basilisk/shaders/normal.frag +60 -0
- basilisk/shaders/normal.vert +92 -0
- {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/METADATA +1 -1
- {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/RECORD +40 -30
- {basilisk_engine-0.0.8.dist-info → basilisk_engine-0.1.0.dist-info}/WHEEL +0 -0
- {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 []
|
basilisk/collisions/collider.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
basilisk/draw/draw_handler.py
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|
|
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()
|
basilisk/generic/collisions.py
CHANGED
|
@@ -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
|