basilisk-engine 0.1.0__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 (55) hide show
  1. basilisk/collisions/broad/broad_aabb.py +8 -1
  2. basilisk/collisions/broad/broad_bvh.py +8 -1
  3. basilisk/collisions/collider.py +15 -6
  4. basilisk/collisions/collider_handler.py +70 -68
  5. basilisk/collisions/narrow/contact_manifold.py +9 -10
  6. basilisk/collisions/narrow/dataclasses.py +27 -0
  7. basilisk/collisions/narrow/deprecated.py +47 -0
  8. basilisk/collisions/narrow/epa.py +11 -10
  9. basilisk/collisions/narrow/gjk.py +15 -14
  10. basilisk/collisions/narrow/helper.py +8 -7
  11. basilisk/collisions/narrow/sutherland_hodgman.py +52 -0
  12. basilisk/draw/draw_handler.py +5 -3
  13. basilisk/engine.py +14 -5
  14. basilisk/generic/abstract_custom.py +134 -0
  15. basilisk/generic/collisions.py +46 -0
  16. basilisk/generic/quat.py +77 -66
  17. basilisk/generic/vec3.py +91 -67
  18. basilisk/mesh/cube.py +20 -2
  19. basilisk/mesh/mesh.py +69 -54
  20. basilisk/mesh/mesh_from_data.py +106 -21
  21. basilisk/mesh/narrow_aabb.py +10 -1
  22. basilisk/mesh/narrow_bvh.py +9 -1
  23. basilisk/nodes/node.py +169 -30
  24. basilisk/nodes/node_handler.py +51 -30
  25. basilisk/particles/__init__.py +0 -0
  26. basilisk/particles/particle_handler.py +55 -0
  27. basilisk/particles/particle_renderer.py +87 -0
  28. basilisk/physics/impulse.py +7 -13
  29. basilisk/physics/physics_body.py +10 -2
  30. basilisk/physics/physics_engine.py +1 -2
  31. basilisk/render/batch.py +2 -2
  32. basilisk/render/camera.py +5 -0
  33. basilisk/render/chunk.py +19 -4
  34. basilisk/render/chunk_handler.py +33 -26
  35. basilisk/render/image.py +1 -1
  36. basilisk/render/image_handler.py +4 -3
  37. basilisk/render/light_handler.py +16 -11
  38. basilisk/render/material.py +25 -1
  39. basilisk/render/material_handler.py +22 -13
  40. basilisk/render/shader.py +7 -7
  41. basilisk/render/shader_handler.py +13 -12
  42. basilisk/render/sky.py +5 -3
  43. basilisk/scene.py +114 -32
  44. basilisk/shaders/batch.frag +40 -11
  45. basilisk/shaders/batch.vert +14 -7
  46. basilisk/shaders/normal.frag +5 -5
  47. basilisk/shaders/normal.vert +8 -3
  48. basilisk/shaders/particle.frag +72 -0
  49. basilisk/shaders/particle.vert +85 -0
  50. {basilisk_engine-0.1.0.dist-info → basilisk_engine-0.1.1.dist-info}/METADATA +5 -5
  51. basilisk_engine-0.1.1.dist-info/RECORD +95 -0
  52. basilisk/shaders/image.png +0 -0
  53. basilisk_engine-0.1.0.dist-info/RECORD +0 -88
  54. {basilisk_engine-0.1.0.dist-info → basilisk_engine-0.1.1.dist-info}/WHEEL +0 -0
  55. {basilisk_engine-0.1.0.dist-info → basilisk_engine-0.1.1.dist-info}/top_level.txt +0 -0
basilisk/nodes/node.py CHANGED
@@ -18,6 +18,12 @@ class Node():
18
18
  """The scale of the node in meters in each direction"""
19
19
  rotation: Quat
20
20
  """The rotation of the node"""
21
+ position_relative: bool
22
+ """The position of this node relative to the parent node"""
23
+ scale_relative: bool
24
+ """The scale of this node relative to the parent node"""
25
+ rotation_relative: bool
26
+ """The rotation of this node relative to the parent node"""
21
27
  forward: glm.vec3
22
28
  """The forward facing vector of the node"""
23
29
  mesh: Mesh
@@ -57,10 +63,13 @@ class Node():
57
63
  shader: Shader
58
64
  """Shader that is used to render the node. If none is given, engine default will be used"""
59
65
 
60
- def __init__(self, node_handler,
66
+ def __init__(self,
61
67
  position: glm.vec3=None,
62
68
  scale: glm.vec3=None,
63
69
  rotation: glm.quat=None,
70
+ relative_position: bool=True,
71
+ relative_scale: bool=True,
72
+ relative_rotation: bool=True,
64
73
  forward: glm.vec3=None,
65
74
  mesh: Mesh=None,
66
75
  material: Material=None,
@@ -86,8 +95,10 @@ class Node():
86
95
  """
87
96
 
88
97
  # parents
89
- self.node_handler = node_handler
98
+ self.node_handler = None
99
+ self.scene = None
90
100
  self.chunk = None
101
+ self.parent = None
91
102
 
92
103
  # lazy update variables
93
104
  self.needs_geometric_center = True # pos
@@ -98,21 +109,28 @@ class Node():
98
109
  self.internal_scale : Vec3 = Vec3(scale) if scale else Vec3(1, 1, 1)
99
110
  self.internal_rotation: Quat = Quat(rotation) if rotation else Quat(1, 0, 0, 0)
100
111
 
112
+ # relative transformations
113
+ self.relative_position = glm.vec3(0, 0, 0) if relative_position else None
114
+ self.relative_scale = glm.vec3(0, 0, 0) if relative_scale else None
115
+ self.relative_rotation = glm.quat(1, 0, 0, 0) if relative_rotation else None
116
+
101
117
  self.forward = forward if forward else glm.vec3(1, 0, 0)
102
- self.mesh = mesh if mesh else self.node_handler.scene.engine.cube
103
- self.material = material if material else None # TODO add default base material
118
+ self.mesh = mesh
119
+ self._mtl_list = material if isinstance(material, list) else [material]
120
+ self.material = material if material else None
104
121
  self.velocity = velocity if velocity else glm.vec3(0, 0, 0)
105
122
  self.rotational_velocity = rotational_velocity if rotational_velocity else glm.vec3(0, 0, 0)
106
123
 
107
- # physics body
108
- if physics: self.physics_body: PhysicsBody = self.node_handler.scene.physics_engine.add(mass = mass if mass else 1.0)
124
+ self.static = static if static != None else not(physics or any(self.velocity) or any(self.rotational_velocity))
125
+
126
+ # Physics updates
127
+ if physics: self.physics_body = PhysicsBody(mass = mass if mass else 1.0)
109
128
  elif mass: raise ValueError('Node: cannot have mass if it does not have physics')
110
129
  else: self.physics_body = None
111
130
 
112
131
  # collider
113
132
  if collisions:
114
- if not self.mesh: raise ValueError('Node: cannot collide if it doezsnt have a mesh')
115
- self.collider: Collider = self.node_handler.scene.collider_handler.add(
133
+ self.collider = Collider(
116
134
  node = self,
117
135
  box_mesh = True if collider == 'box' else False,
118
136
  static_friction = static_friction,
@@ -126,13 +144,11 @@ class Node():
126
144
  elif elasticity: raise ValueError('Node: cannot have elasticity if it does not allow collisions')
127
145
  elif collision_group: raise ValueError('Node: cannot have collider group if it does not allow collisions')
128
146
  else: self.collider = None
129
-
147
+
130
148
  # information and recursion
131
149
  self.name = name
132
150
  self.tags = tags if tags else []
133
- if static == None:
134
- self.static = not(physics or any(self.velocity) or any(self.rotational_velocity))
135
- else: self.static = static
151
+
136
152
  self.data_index = 0
137
153
  self.children = []
138
154
 
@@ -143,7 +159,7 @@ class Node():
143
159
  self.previous_position: Vec3 = Vec3(position) if position else Vec3(0, 0, 0)
144
160
  self.previous_scale : Vec3 = Vec3(scale) if scale else Vec3(1, 1, 1)
145
161
  self.previous_rotation: Quat = Quat(rotation) if rotation else Quat(1, 0, 0, 0) # TODO Do these need to be the callback class or can they just be glm?
146
-
162
+
147
163
  # callback function to be added to the custom Vec3 and Quat classes
148
164
  def position_callback():
149
165
  self.chunk.node_update_callback(self)
@@ -178,20 +194,55 @@ class Node():
178
194
  self.internal_position.callback = position_callback
179
195
  self.internal_scale.callback = scale_callback
180
196
  self.internal_rotation.callback = rotation_callback
181
-
197
+
198
+ def init_scene(self, scene):
199
+ """
200
+ Updates the scene of the node
201
+ """
202
+ self.scene = scene
203
+ self.node_handler = scene.node_handler
204
+
205
+ # Update materials
206
+ self.write_materials()
207
+
208
+ # Update the mesh
209
+ self.mesh = self.mesh if self.mesh else self.scene.engine.cube
210
+
211
+ # Update physics and collider
212
+ if self.physics_body: self.physics_body.physics_engine = scene.physics_engine
213
+ if self.collider: self.collider.collider_handler = scene.collider_handler
214
+
182
215
  def update(self, dt: float) -> None:
183
216
  """
184
217
  Updates the node's movement variables based on the delta time
185
218
  """
219
+ # update based on physical properties
186
220
  if any(self.velocity): self.position += dt * self.velocity
187
- if any(self.rotational_velocity): self.rotation = glm.normalize(self.rotation - 0.5 * dt * self.rotation * glm.quat(0, *self.rotational_velocity))
221
+ if any(self.rotational_velocity): self.rotation = glm.normalize(self.rotation - dt / 2 * self.rotation * glm.quat(0, *self.rotational_velocity))
188
222
 
189
223
  if self.physics_body:
190
224
  self.velocity += self.physics_body.get_delta_velocity(dt)
191
225
  self.rotational_velocity += self.physics_body.get_delta_rotational_velocity(dt)
226
+
227
+ # update children transforms
228
+ for child in self.children: child.sync_data()
192
229
 
193
- def sync_data(self, dt: float) -> None: # TODO only needed for child nodes now
194
- ...
230
+ def sync_data(self) -> None:
231
+ """
232
+ Syncronizes this node with the parent node based on its relative positioning
233
+ """
234
+ # calculate transform matrix with the given input
235
+ transform = glm.mat4x4()
236
+ if self.relative_position: transform = glm.translate(transform, self.parent.position)
237
+ if self.relative_rotation: transform *= glm.transpose(glm.mat4_cast(self.parent.rotation))
238
+ if self.relative_scale: transform = glm.scale(transform, self.parent.scale)
239
+
240
+ # set this node's transforms based on the parent
241
+ self.position = transform * self.relative_position
242
+ self.scale = self.relative_scale * self.parent.scale
243
+ self.rotation = self.relative_rotation * self.parent.rotation
244
+
245
+ for child in self.children: child.sync_data()
195
246
 
196
247
  def get_nodes(self,
197
248
  require_mesh: bool=False,
@@ -217,13 +268,33 @@ class Node():
217
268
 
218
269
  # adds children to nodes list if they match the criteria
219
270
  for node in self.children: nodes.extend(node.get_nodes(require_mesh, require_collider, require_physics_body, filter_material, filter_tags))
220
- return node
271
+ return nodes
221
272
 
222
- def adopt_child(self, node) -> None: # TODO determine the best way for the user to do this through the scene
223
- ...
273
+ # tree functions for managing children
274
+ def add(self, child: ..., relative_position: bool=None, relative_scale: bool=None, relative_rotation: glm.vec3=None) -> None:
275
+ """
276
+ Adopts a node as a child. Relative transforms can be changed, if left bank they will not be chnaged from the current child nodes settings.
277
+ """
278
+ if child in self.children: return
279
+
280
+ # compute relative transformations
281
+ if relative_position or (relative_position is None and child.relative_position): child.relative_position = child.position - self.position
282
+ if relative_scale or (relative_scale is None and child.relative_scale): child.relative_scale = child.scale / self.scale
283
+ if relative_rotation or (relative_rotation is None and child.relative_rotation): child.relative_rotation = child.rotation * glm.inverse(self.rotation)
224
284
 
225
- def add_child(self) -> None: # TODO add node constructor
226
- ...
285
+ # add as a child to by synchronized and controlled
286
+ if self.node_handler: self.node_handler.add(child)
287
+ child.parent = self
288
+ self.children.append(child)
289
+
290
+ def remove(self, child: ...) -> None:
291
+ """
292
+ Removes a child node from this nodes chlid list.
293
+ """
294
+ if child in self.children:
295
+ if self.node_handler: self.node_handler.remove(child)
296
+ child.parent = None
297
+ self.children.remove(child)
227
298
 
228
299
  def apply_force(self, force: glm.vec3, dt: float) -> None:
229
300
  """
@@ -256,7 +327,7 @@ class Node():
256
327
  Transforms the mesh inertia tensor and inverts it
257
328
  """
258
329
  if not (self.mesh and self.physics_body): return None
259
- inertia_tensor = self.mesh.get_inertia_tensor(self.scale)
330
+ inertia_tensor = self.mesh.get_inertia_tensor(self.scale) / 2
260
331
 
261
332
  # mass
262
333
  if self.physics_body: inertia_tensor *= self.physics_body.mass
@@ -266,6 +337,12 @@ class Node():
266
337
  inertia_tensor = rotation_matrix * inertia_tensor * glm.transpose(rotation_matrix)
267
338
 
268
339
  return glm.inverse(inertia_tensor)
340
+
341
+ def get_vertex(self, index) -> glm.vec3:
342
+ """
343
+ Gets the world space position of a vertex indicated by the index in the mesh
344
+ """
345
+ return glm.vec3(self.model_matrix * glm.vec4(*self.mesh.points[index], 1))
269
346
 
270
347
  def get_data(self) -> np.ndarray:
271
348
  """
@@ -274,15 +351,41 @@ class Node():
274
351
 
275
352
  # Get data from the mesh node
276
353
  mesh_data = self.mesh.data
277
- node_data = np.array([*self.position, *self.rotation, *self.scale, self.material.index])
354
+ node_data = np.array([*self.position, *self.rotation, *self.scale, 0])
355
+
356
+ per_vertex_mtl = isinstance(self.material, list)
357
+
358
+ if not per_vertex_mtl: node_data[-1] = self.material.index
278
359
 
279
360
  # Create an array to hold the node's data
280
361
  data = np.zeros(shape=(mesh_data.shape[0], 25), dtype='f4')
362
+
363
+
281
364
  data[:,:14] = mesh_data
282
365
  data[:,14:] = node_data
283
366
 
367
+ if per_vertex_mtl: data[:,24] = self.material
368
+
284
369
  return data
285
370
 
371
+ def write_materials(self):
372
+ """
373
+ Internal function to write the material list to the material handler and get the material ids
374
+ """
375
+
376
+ if isinstance(self.material, list):
377
+ mtl_index_list = []
378
+ for mtl in self._mtl_list:
379
+ self.node_handler.scene.material_handler.add(mtl)
380
+ mtl_index_list.append(mtl.index)
381
+ mtl_index_list.append(mtl.index)
382
+ mtl_index_list.append(mtl.index)
383
+ self._material = mtl_index_list
384
+
385
+ if isinstance(self.material, type(None)):
386
+ self.material = self.scene.material_handler.base
387
+
388
+
286
389
  def __repr__(self) -> str:
287
390
  """
288
391
  Returns a string representation of the node
@@ -298,7 +401,8 @@ class Node():
298
401
  def rotation(self): return self.internal_rotation.data
299
402
  @property
300
403
  def forward(self): return self._forward
301
- # TODO add property for Mesh
404
+ @property
405
+ def mesh(self): return self._mesh
302
406
  @property
303
407
  def material(self): return self._material
304
408
  @property
@@ -366,6 +470,9 @@ class Node():
366
470
  if len(value) != 3: raise ValueError(f'Node: Invalid number of values for position. Expected 3, got {len(value)}')
367
471
  self.internal_position.data = glm.vec3(value)
368
472
  else: raise TypeError(f'Node: Invalid position value type {type(value)}')
473
+
474
+ # # compute relative position if all cases passed
475
+ # if self.parent and self.relative_position: self.relative_position = self.internal_position.data - self.parent.position
369
476
 
370
477
  @scale.setter
371
478
  def scale(self, value: tuple | list | glm.vec3 | np.ndarray):
@@ -374,6 +481,9 @@ class Node():
374
481
  if len(value) != 3: raise ValueError(f'Node: Invalid number of values for scale. Expected 3, got {len(value)}')
375
482
  self.internal_scale.data = glm.vec3(value)
376
483
  else: raise TypeError(f'Node: Invalid scale value type {type(value)}')
484
+
485
+ # # compute relative scale
486
+ # if self.parent and self.relative_scale: self.relative_scale = self.internal_scale.data / self.parent.scale
377
487
 
378
488
  @rotation.setter
379
489
  def rotation(self, value: tuple | list | glm.vec3 | glm.quat | glm.vec4 | np.ndarray):
@@ -383,6 +493,9 @@ class Node():
383
493
  elif len(value) == 4: self.internal_rotation.data = glm.quat(*value)
384
494
  else: raise ValueError(f'Node: Invalid number of values for rotation. Expected 3 or 4, got {len(value)}')
385
495
  else: raise TypeError(f'Node: Invalid rotation value type {type(value)}')
496
+
497
+ # # compute relative rotation
498
+ # if self.parent and self.relative_rotation: self.relative_rotation = self.rotation * glm.inverse(self.parent.rotation)
386
499
 
387
500
  @forward.setter
388
501
  def forward(self, value: tuple | list | glm.vec3 | np.ndarray):
@@ -392,16 +505,42 @@ class Node():
392
505
  self._forward = glm.vec3(value)
393
506
  else: raise TypeError(f'Node: Invalid forward value type {type(value)}')
394
507
 
395
- # TODO add setter for Mesh
508
+ @mesh.setter
509
+ def mesh(self, value: Mesh | None):
510
+ if isinstance(value, Mesh):
511
+ self._mesh = value
512
+ if self.chunk: self.chunk.update()
513
+ elif isinstance(value, type(None)):
514
+ self._mesh = None
515
+ if not self.chunk: return
516
+ self.chunk.remove(self)
517
+ self.chunk.update()
518
+ else: raise TypeError(f'Node: Invalid mesh value type {type(value)}')
396
519
 
397
520
  @material.setter
398
521
  def material(self, value: Material):
399
- if isinstance(value, Material):
522
+ if isinstance(value, list):
523
+ self._mtl_list = value
524
+ if not self.node_handler:
525
+ self._material = value
526
+ else:
527
+ mtl_index_list = []
528
+ for mtl in self._mtl_list:
529
+ self.node_handler.scene.material_handler.add(mtl)
530
+ mtl_index_list.append(mtl.index)
531
+ mtl_index_list.append(mtl.index)
532
+ mtl_index_list.append(mtl.index)
533
+ self._material = mtl_index_list
534
+ elif isinstance(value, Material):
400
535
  self._material = value
401
- self.node_handler.scene.material_handler.add(value)
402
- if not self.chunk: return
403
- self.chunk.node_update_callback(self)
536
+ if self.node_handler: self.node_handler.scene.material_handler.add(value)
537
+ elif isinstance(value, type(None)):
538
+ if self.scene: self._material = self.scene.material_handler.base
539
+ else: self._material = None
540
+
404
541
  else: raise TypeError(f'Node: Invalid material value type {type(value)}')
542
+ if not self.chunk: return
543
+ self.chunk.node_update_callback(self)
405
544
 
406
545
  @velocity.setter
407
546
  def velocity(self, value: tuple | list | glm.vec3 | np.ndarray):
@@ -37,62 +37,83 @@ class NodeHandler():
37
37
 
38
38
  self.chunk_handler.render()
39
39
 
40
- def add(self,
41
- position: glm.vec3=None,
42
- scale: glm.vec3=None,
43
- rotation: glm.quat=None,
44
- forward: glm.vec3=None,
45
- mesh: Mesh=None,
46
- material: Material=None,
47
- velocity: glm.vec3=None,
48
- rotational_velocity: glm.quat=None,
49
- physics: bool=False,
50
- mass: float=None,
51
- collisions: bool=False,
52
- collider: str=None,
53
- static_friction: float=None,
54
- kinetic_friction: float=None,
55
- elasticity: float=None,
56
- collision_group : float=None,
57
- name: str='',
58
- tags: list[str]=None,
59
- static: bool=True
60
- ) -> Node:
40
+ def add(self, node: Node) -> Node:
61
41
  """
62
42
  Adds a new node to the node handler
63
43
  """
44
+ if node in self.nodes: return
64
45
 
65
- node = Node(self, position, scale, rotation, forward, mesh, material, velocity, rotational_velocity, physics, mass, collisions, collider, static_friction, kinetic_friction, elasticity, collision_group, name, tags, static)
66
-
67
- self.nodes.append(node)
68
- self.chunk_handler.add(node)
46
+ for n in node.get_nodes(): # gets all nodes including the node to be added
47
+
48
+ # Update scene Handlers
49
+ self.scene.shader_handler.add(n.shader)
50
+ if not n.material: n.material = self.scene.material_handler.base
51
+ self.scene.material_handler.add(n.material)
52
+
53
+ # Update the node attributes
54
+ n.init_scene(self.scene)
55
+
56
+ # Add the node to internal data
57
+ self.nodes.append(n)
58
+ self.chunk_handler.add(n)
69
59
 
70
60
  return node
61
+
62
+ def node_is(self, node: Node, position: glm.vec3=None, scale: glm.vec3=None, rotation: glm.quat=None, forward: glm.vec3=None, mesh: Mesh=None, material: Material=None, velocity: glm.vec3=None, rotational_velocity: glm.quat=None, physics: bool=None, mass: float=None, collisions: bool=None, static_friction: float=None, kinetic_friction: float=None, elasticity: float=None, collision_group: float=None, name: str=None, tags: list[str]=None,static: bool=None) -> bool:
63
+ """
64
+ Determine if a node meets the requirements given by the parameters. If a parameter is None, then the filter is not applied.
65
+ """
66
+ return all([
67
+ position is None or position == node.position,
68
+ scale is None or scale == node.scale,
69
+ rotation is None or rotation == node.rotation,
70
+ forward is None or forward == node.forward,
71
+ mesh is None or mesh == node.mesh,
72
+ material is None or material == node.material,
73
+ velocity is None or velocity == node.velocity,
74
+ rotational_velocity is None or rotational_velocity == node.rotational_velocity,
75
+ physics is None or bool(node.physics_body) == physics,
76
+ mass is None or (node.physics_body and mass == node.physics_body.mass),
77
+ collisions is None or bool(node.collider) == collisions,
78
+ static_friction is None or (node.collider and node.collider.static_friction == static_friction),
79
+ kinetic_friction is None or (node.collider and node.collider.kinetic_friction == kinetic_friction),
80
+ elasticity is None or (node.collider and node.collider.elasticity == elasticity),
81
+ collision_group is None or (node.collider and node.collider.collision_group == collision_group),
82
+ name is None or node.name == name,
83
+ tags is None or all([tag in node.tags for tag in tags]),
84
+ static is None or node.static == static
85
+ ])
71
86
 
72
- def get(self, name: str) -> Node: # TODO switch to full filter and adapt to search roots
87
+ def get(self, position: glm.vec3=None, scale: glm.vec3=None, rotation: glm.quat=None, forward: glm.vec3=None, mesh: Mesh=None, material: Material=None, velocity: glm.vec3=None, rotational_velocity: glm.quat=None, physics: bool=None, mass: float=None, collisions: bool=None, static_friction: float=None, kinetic_friction: float=None, elasticity: float=None, collision_group: float=None, name: str=None, tags: list[str]=None,static: bool=None) -> Node:
73
88
  """
74
89
  Returns the first node with the given traits
75
90
  """
76
91
  for node in self.nodes:
77
- if node.name == name: return node
92
+ if self.node_is(node, position, scale, rotation, forward, mesh, material, velocity, rotational_velocity, physics, mass, collisions, static_friction, kinetic_friction, elasticity, collision_group, name, tags, static): return node
78
93
  return None
79
94
 
80
- def get_all(self, name: str) -> Node:
95
+ def get_all(self, position: glm.vec3=None, scale: glm.vec3=None, rotation: glm.quat=None, forward: glm.vec3=None, mesh: Mesh=None, material: Material=None, velocity: glm.vec3=None, rotational_velocity: glm.quat=None, physics: bool=None, mass: float=None, collisions: bool=None, static_friction: float=None, kinetic_friction: float=None, elasticity: float=None, collision_group: float=None, name: str=None, tags: list[str]=None,static: bool=None) -> Node:
81
96
  """
82
97
  Returns all nodes with the given traits
83
98
  """
84
99
  nodes = []
85
100
  for node in self.nodes:
86
- if node.name == name: nodes.append(node)
101
+ if self.node_is(node, position, scale, rotation, forward, mesh, material, velocity, rotational_velocity, physics, mass, collisions, static_friction, kinetic_friction, elasticity, collision_group, name, tags, static): nodes.append(node)
87
102
  return nodes
88
103
 
89
104
  def remove(self, node: Node) -> None:
90
105
  """
91
106
  Removes a node and all of its children from their handlers
92
107
  """
108
+
109
+ if node == None: return
110
+
93
111
  # TODO add support for recursive nodes
94
112
  if node in self.nodes:
95
113
  if node.physics_body: self.scene.physics_engine.remove(node.physics_body)
96
114
  if node.collider: self.scene.collider_handler.remove(node.collider)
115
+ self.chunk_handler.remove(node)
116
+ self.nodes.remove(node)
97
117
  node.node_handler = None
98
- self.nodes.remove(node)
118
+
119
+ for child in node.children: self.remove(child)
File without changes
@@ -0,0 +1,55 @@
1
+ from .particle_renderer import ParticleRenderer
2
+ from ..mesh.mesh import Mesh
3
+ from ..render.material import Material
4
+
5
+
6
+ class ParticleHandler:
7
+ def __init__(self, scene):
8
+ """
9
+ A handler for all particles in a scene
10
+ """
11
+
12
+ self.scene = scene
13
+ self.cube = Mesh(scene.engine.root + '/bsk_assets/cube.obj')
14
+ self.particle_renderers = {self.cube : ParticleRenderer(scene, self.cube)}
15
+
16
+
17
+ def add(self, mesh: Mesh=None, life=1.0, position=(0, 0, 0), material: Material=None, scale=1.0, velocity=(0, 3, 0), acceleration=(0, -10, 0)) -> bool:
18
+ """
19
+ Add a new particle to the scene
20
+ Args:
21
+ mesh: Mesh
22
+ The basilisk mesh of the particle
23
+ life: float
24
+ The duration of the particle in seconds
25
+ position: tuple (x, y, z)
26
+ The initial position of the particle
27
+ color: tuple (r, g, b) (components out of 255)
28
+ The color of the particle
29
+ scale: float
30
+ The overall scale factor of the particle
31
+ velocity: tuple (x, y, z)
32
+ The inital velocity of the particle as a vector
33
+ acceleration: tuple (x, y, z)
34
+ The permanent acceleration of the particle as a vector
35
+ """
36
+
37
+ # Get the mesh and make a new particle renderer if the mesh is new
38
+ if mesh == None: mesh = self.cube
39
+ elif not isinstance(mesh, Mesh): raise ValueError(f'particle_handler.add: invlaid mesh type for particle: {type(mesh)}')
40
+ if mesh not in self.particle_renderers: self.particle_renderers[mesh] = ParticleRenderer(self.scene, mesh)
41
+
42
+ # Get material ID
43
+ if material == None: material_index = 0
44
+ elif isinstance(material, Material):
45
+ self.scene.material_handler.add(material)
46
+ material_index = material.index
47
+ else: raise ValueError(f'particle_handler.add: Invalid particle material type: {type(material)}')
48
+
49
+ # Add the particle to the renderer
50
+ self.particle_renderers[mesh].add(life, position, material_index, scale, velocity, acceleration)
51
+
52
+ def render(self) -> None:
53
+ for renderer in self.particle_renderers.values(): renderer.render()
54
+ def update(self) -> None:
55
+ for renderer in self.particle_renderers.values(): renderer.update()
@@ -0,0 +1,87 @@
1
+ import numpy as np
2
+ from ..render.shader import Shader
3
+ from ..mesh.mesh import Mesh
4
+ from ..render.material import Material
5
+ from numba import njit
6
+
7
+
8
+ @njit
9
+ def update_particle_matrix(particle_instances, dt):
10
+ particle_instances[:,6:9] += particle_instances[:,9:12] * dt
11
+ particle_instances[:,:3] += particle_instances[:,6:9] * dt
12
+ particle_instances[:,5] -= dt/3
13
+ return particle_instances
14
+
15
+ @njit
16
+ def get_alive(particles):
17
+ return particles[particles[:, 5] >= 0]
18
+
19
+ update_particle_matrix(np.zeros(shape=(2, 12), dtype='f4'), 1)
20
+ get_alive(np.zeros(shape=(2, 12), dtype='f4'))
21
+
22
+
23
+ class ParticleRenderer:
24
+ def __init__(self, scene: ..., mesh: Mesh) -> None:
25
+ """
26
+ Handels and renders the particles of a single mesh type
27
+ """
28
+
29
+ self.scene = scene
30
+ self.ctx = scene.ctx
31
+ root = scene.engine.root
32
+ self.shader = Shader(scene.engine, vert=root + '/shaders/particle.vert', frag=root + '/shaders/particle.frag')
33
+ scene.shader_handler.add(self.shader)
34
+
35
+ self.particle_cube_size = 25
36
+
37
+ self.particle_instances = np.zeros(shape=(1, 12), dtype='f4')
38
+ self.instance_buffer = self.ctx.buffer(reserve=(12 * 3) * (self.particle_cube_size ** 3))
39
+
40
+ self.vao = self.ctx.vertex_array( self.shader.program,
41
+ [(self.ctx.buffer(mesh.data), '3f 2f 3f 3f 3f', *['in_position', 'in_uv', 'in_normal', 'in_tangent', 'in_bitangent']),
42
+ (self.instance_buffer, '3f 1f 1f 1f /i', 'in_instance_pos', 'in_instance_mtl', 'scale', 'life')],
43
+ skip_errors=True)
44
+
45
+ def render(self) -> None:
46
+ """
47
+ Renders the alive particles in the scene
48
+ """
49
+
50
+ # Get the current particles
51
+ alive_particles = get_alive(self.particle_instances)
52
+ n = len(alive_particles)
53
+
54
+ # Write and render
55
+ self.instance_buffer.write(np.array(alive_particles[:,:6], order='C'))
56
+ self.vao.render(instances=n)
57
+
58
+ def update(self) -> None:
59
+ """
60
+ Updates the particle positions based on their given properties
61
+ """
62
+
63
+ self.particle_instances = get_alive(self.particle_instances)
64
+ self.particle_instances = update_particle_matrix(self.particle_instances, self.scene.engine.delta_time)
65
+
66
+ def add(self, life=1.0, position=(0, 0, 0), material: int=0, scale=1.0, velocity=(0, 3, 0), acceleration=(0, -10, 0)) -> bool:
67
+ """
68
+ Add a new particle to the scene
69
+ Args:
70
+ life: float
71
+ The duration of the particle in seconds
72
+ position: tuple (x, y, z)
73
+ The initial position of the particle
74
+ color: tuple (r, g, b) (components out of 255)
75
+ The color of the particle
76
+ scale: float
77
+ The overall scale factor of the particle
78
+ velocity: tuple (x, y, z)
79
+ The inital velocity of the particle as a vector
80
+ acceleration: tuple (x, y, z)
81
+ The permanent acceleration of the particle as a vector
82
+ """
83
+ # Check if there is already the max number of particles
84
+ if len(self.particle_instances) >= (self.particle_cube_size ** 3): return False
85
+ # Create and add the particle to the scene
86
+ new_particle = np.array([*position, material, scale, life, *velocity, *acceleration])
87
+ self.particle_instances = np.vstack([new_particle, self.particle_instances], dtype='f4')
@@ -21,9 +21,6 @@ def calculate_collisions(normal:glm.vec3, node1: Node, node2: Node, contact_poin
21
21
  elasticity = max(collider1.elasticity, collider2.elasticity)
22
22
  kinetic = min(collider1.kinetic_friction, collider2.kinetic_friction)
23
23
  static = min(collider1.static_friction, collider2.static_friction)
24
- num_points = len(contact_points)
25
-
26
- total_impulse = glm.vec3(0, 0, 0)
27
24
 
28
25
  # calculate impulses from contact points
29
26
  if has_physics1 and has_physics2:
@@ -32,29 +29,26 @@ def calculate_collisions(normal:glm.vec3, node1: Node, node2: Node, contact_poin
32
29
  # apply impulse based reduced by total points
33
30
  radius1, radius2 = contact_point - center1, contact_point - center2
34
31
  impulse = calculate_impulse2(node1, node2, inv_mass1, inv_mass2, node1.rotational_velocity, node2.rotational_velocity, radius1, radius2, inv_inertia1, inv_inertia2, elasticity, kinetic, static, normal)
35
- total_impulse += impulse / num_points
36
32
 
37
- # apply impulses
38
- apply_impulse(radius1, total_impulse, inv_inertia1, inv_mass1, node1)
39
- apply_impulse(radius2, -total_impulse, inv_inertia2, inv_mass2, node2)
33
+ # apply impulses
34
+ apply_impulse(radius1, impulse, inv_inertia1, inv_mass1, node1)
35
+ apply_impulse(radius2, -impulse, inv_inertia2, inv_mass2, node2)
40
36
 
41
37
  elif has_physics1:
42
38
  for contact_point in contact_points:
43
39
  radius = contact_point - center1
44
40
  impulse = calculate_impluse1(node1, inv_mass1, node1.rotational_velocity, radius, inv_inertia1, elasticity, kinetic, static, normal)
45
- total_impulse += impulse / num_points
46
41
 
47
- # apply impulses
48
- apply_impulse(radius, total_impulse, inv_inertia1, inv_mass1, node1)
42
+ # apply impulses
43
+ apply_impulse(radius, impulse, inv_inertia1, inv_mass1, node1)
49
44
 
50
45
  else: # only physics body 2
51
46
  for contact_point in contact_points:
52
47
  radius = contact_point - center2
53
48
  impulse = calculate_impluse1(node2, inv_mass2, node2.rotational_velocity, radius, inv_inertia2, elasticity, kinetic, static, normal)
54
- total_impulse += impulse / num_points
55
49
 
56
- # apply impulse
57
- apply_impulse(radius, total_impulse, inv_inertia2, inv_mass2, node2)
50
+ # apply impulse
51
+ apply_impulse(radius, impulse, inv_inertia2, inv_mass2, node2)
58
52
 
59
53
  def calculate_impluse1(node: Node, inv_mass, omega, radius, inv_inertia, elasticity, kinetic, static, normal) -> glm.vec3:
60
54
  """