basilisk-engine 0.1.2__py3-none-any.whl → 0.1.3__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 (82) hide show
  1. basilisk/__init__.py +11 -11
  2. basilisk/bsk_assets/cube.obj +48 -48
  3. basilisk/collisions/broad/broad_aabb.py +102 -102
  4. basilisk/collisions/broad/broad_bvh.py +137 -137
  5. basilisk/collisions/collider.py +95 -83
  6. basilisk/collisions/collider_handler.py +225 -228
  7. basilisk/collisions/narrow/contact_manifold.py +90 -90
  8. basilisk/collisions/narrow/dataclasses.py +33 -27
  9. basilisk/collisions/narrow/deprecated.py +46 -46
  10. basilisk/collisions/narrow/epa.py +91 -91
  11. basilisk/collisions/narrow/gjk.py +66 -66
  12. basilisk/collisions/narrow/graham_scan.py +24 -24
  13. basilisk/collisions/narrow/helper.py +29 -29
  14. basilisk/collisions/narrow/line_intersections.py +106 -106
  15. basilisk/collisions/narrow/sutherland_hodgman.py +75 -75
  16. basilisk/config.py +2 -2
  17. basilisk/draw/draw.py +100 -100
  18. basilisk/draw/draw_handler.py +180 -210
  19. basilisk/draw/font_renderer.py +28 -28
  20. basilisk/engine.py +195 -195
  21. basilisk/generic/abstract_bvh.py +15 -15
  22. basilisk/generic/abstract_custom.py +133 -133
  23. basilisk/generic/collisions.py +70 -70
  24. basilisk/generic/input_validation.py +67 -28
  25. basilisk/generic/math.py +6 -6
  26. basilisk/generic/matrices.py +33 -33
  27. basilisk/generic/meshes.py +72 -72
  28. basilisk/generic/quat.py +137 -137
  29. basilisk/generic/quat_methods.py +7 -7
  30. basilisk/generic/raycast_result.py +24 -0
  31. basilisk/generic/vec3.py +143 -143
  32. basilisk/input/mouse.py +61 -59
  33. basilisk/mesh/cube.py +33 -33
  34. basilisk/mesh/mesh.py +230 -230
  35. basilisk/mesh/mesh_from_data.py +132 -132
  36. basilisk/mesh/model.py +271 -271
  37. basilisk/mesh/narrow_aabb.py +89 -89
  38. basilisk/mesh/narrow_bvh.py +91 -91
  39. basilisk/mesh/narrow_primative.py +23 -23
  40. basilisk/nodes/helper.py +29 -0
  41. basilisk/nodes/node.py +681 -617
  42. basilisk/nodes/node_handler.py +95 -118
  43. basilisk/particles/particle_handler.py +63 -54
  44. basilisk/particles/particle_renderer.py +87 -87
  45. basilisk/physics/impulse.py +112 -112
  46. basilisk/physics/physics_body.py +43 -43
  47. basilisk/physics/physics_engine.py +35 -35
  48. basilisk/render/batch.py +86 -86
  49. basilisk/render/camera.py +204 -199
  50. basilisk/render/chunk.py +99 -99
  51. basilisk/render/chunk_handler.py +154 -154
  52. basilisk/render/frame.py +181 -181
  53. basilisk/render/image.py +75 -75
  54. basilisk/render/image_handler.py +122 -122
  55. basilisk/render/light.py +96 -96
  56. basilisk/render/light_handler.py +58 -58
  57. basilisk/render/material.py +219 -219
  58. basilisk/render/material_handler.py +135 -135
  59. basilisk/render/shader.py +109 -109
  60. basilisk/render/shader_handler.py +79 -79
  61. basilisk/render/sky.py +120 -120
  62. basilisk/scene.py +250 -210
  63. basilisk/shaders/batch.frag +276 -276
  64. basilisk/shaders/batch.vert +115 -115
  65. basilisk/shaders/draw.frag +21 -21
  66. basilisk/shaders/draw.vert +21 -21
  67. basilisk/shaders/filter.frag +22 -22
  68. basilisk/shaders/frame.frag +12 -12
  69. basilisk/shaders/frame.vert +13 -13
  70. basilisk/shaders/geometry.frag +8 -8
  71. basilisk/shaders/geometry.vert +41 -41
  72. basilisk/shaders/normal.frag +59 -59
  73. basilisk/shaders/normal.vert +96 -96
  74. basilisk/shaders/particle.frag +71 -71
  75. basilisk/shaders/particle.vert +84 -84
  76. basilisk/shaders/sky.frag +9 -9
  77. basilisk/shaders/sky.vert +13 -13
  78. {basilisk_engine-0.1.2.dist-info → basilisk_engine-0.1.3.dist-info}/METADATA +45 -38
  79. basilisk_engine-0.1.3.dist-info/RECORD +97 -0
  80. {basilisk_engine-0.1.2.dist-info → basilisk_engine-0.1.3.dist-info}/WHEEL +1 -1
  81. basilisk_engine-0.1.2.dist-info/RECORD +0 -95
  82. {basilisk_engine-0.1.2.dist-info → basilisk_engine-0.1.3.dist-info}/top_level.txt +0 -0
basilisk/nodes/node.py CHANGED
@@ -1,617 +1,681 @@
1
- import glm
2
- import numpy as np
3
- from ..generic.vec3 import Vec3
4
- from ..generic.quat import Quat
5
- from ..generic.matrices import get_model_matrix
6
- from ..mesh.mesh import Mesh
7
- from ..render.material import Material
8
- from ..physics.physics_body import PhysicsBody
9
- from ..collisions.collider import Collider
10
- from ..render.chunk import Chunk
11
- from ..render.shader import Shader
12
-
13
-
14
- class Node():
15
- position: Vec3
16
- """The position of the node in meters with swizzle xyz"""
17
- scale: Vec3
18
- """The scale of the node in meters in each direction"""
19
- rotation: Quat
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"""
27
- forward: glm.vec3
28
- """The forward facing vector of the node"""
29
- mesh: Mesh
30
- """The mesh of the node stored as a basilisk mesh object"""
31
- material: Material
32
- """The mesh of the node stored as a basilisk material object"""
33
- velocity: glm.vec3
34
- """The translational velocity of the node"""
35
- rotational_velocity: glm.vec3
36
- """The rotational velocity of the node"""
37
- physics: bool
38
- """Allows the node's movement to be affected by the physics engine and collisions"""
39
- mass: float
40
- """The mass of the node in kg"""
41
- collisions: bool
42
- """Gives the node collision with other nodes in the scene"""
43
- collider: str
44
- """The collider type of the node. Can be either 'box' or 'mesh'"""
45
- static_friction: float
46
- """Determines the friction of the node when still: recommended value 0.0 - 1.0"""
47
- kinetic_friction: float
48
- """Determines the friction of the node when moving: recommended value 0.0 - 1.0"""
49
- elasticity: float
50
- """Determines how bouncy an object is: recommended value 0.0 - 1.0"""
51
- collision_group: str
52
- """Nodes of the same collision group do not collide with each other"""
53
- name: str
54
- """The name of the node for reference"""
55
- tags: list[str]
56
- """Tags are used to sort nodes into separate groups"""
57
- static: bool
58
- """Objects that don't move should be marked as static"""
59
- chunk: Chunk
60
- """""" # TODO Jonah description
61
- children: list
62
- """List of nodes that this node is a parent of"""
63
- shader: Shader
64
- """Shader that is used to render the node. If none is given, engine default will be used"""
65
-
66
- def __init__(self,
67
- position: glm.vec3=None,
68
- scale: glm.vec3=None,
69
- rotation: glm.quat=None,
70
- relative_position: bool=True,
71
- relative_scale: bool=True,
72
- relative_rotation: bool=True,
73
- forward: glm.vec3=None,
74
- mesh: Mesh=None,
75
- material: Material=None,
76
- velocity: glm.vec3=None,
77
- rotational_velocity: glm.vec3=None,
78
- physics: bool=False,
79
- mass: float=None,
80
- collisions: bool=False,
81
- collider: str=None,
82
- static_friction: float=None,
83
- kinetic_friction: float=None,
84
- elasticity: float=None,
85
- collision_group : float=None,
86
- name: str='',
87
- tags: list[str]=None,
88
- static: bool=None,
89
- shader: Shader=None
90
- ) -> None:
91
- """
92
- Basilisk node object.
93
- Contains mesh data, translation, material, physics, collider, and descriptive information.
94
- Base building block for populating a Basilisk scene.
95
- """
96
-
97
- # parents
98
- self.node_handler = None
99
- self.scene = None
100
- self.chunk = None
101
- self.parent = None
102
-
103
- # lazy update variables
104
- self.needs_geometric_center = True # pos
105
- self.needs_model_matrix = True # pos, scale, rot
106
-
107
- # node data
108
- self.internal_position: Vec3 = Vec3(position) if position else Vec3(0, 0, 0)
109
- self.internal_scale : Vec3 = Vec3(scale) if scale else Vec3(1, 1, 1)
110
- self.internal_rotation: Quat = Quat(rotation) if rotation else Quat(1, 0, 0, 0)
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
-
117
- self.forward = forward if forward else glm.vec3(1, 0, 0)
118
- self.mesh = mesh
119
- self._mtl_list = material if isinstance(material, list) else [material]
120
- self.material = material if material else None
121
- self.velocity = velocity if velocity else glm.vec3(0, 0, 0)
122
- self.rotational_velocity = rotational_velocity if rotational_velocity else glm.vec3(0, 0, 0)
123
-
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)
128
- elif mass: raise ValueError('Node: cannot have mass if it does not have physics')
129
- else: self.physics_body = None
130
-
131
- # collider
132
- if collisions:
133
- self.collider = Collider(
134
- node = self,
135
- box_mesh = True if collider == 'box' else False,
136
- static_friction = static_friction,
137
- kinetic_friction = kinetic_friction,
138
- elasticity = elasticity,
139
- collision_group = collision_group
140
- )
141
- elif collider: raise ValueError('Node: cannot have collider mesh if it does not allow collisions')
142
- elif static_friction: raise ValueError('Node: cannot have static friction if it does not allow collisions')
143
- elif kinetic_friction: raise ValueError('Node: cannot have kinetic friction if it does not allow collisions')
144
- elif elasticity: raise ValueError('Node: cannot have elasticity if it does not allow collisions')
145
- elif collision_group: raise ValueError('Node: cannot have collider group if it does not allow collisions')
146
- else: self.collider = None
147
-
148
- # information and recursion
149
- self.name = name
150
- self.tags = tags if tags else []
151
-
152
- self.data_index = 0
153
- self.children = []
154
-
155
- # Shader given by user or none for default
156
- self.shader = shader
157
-
158
- # default callback functions for node transform
159
- self.previous_position: Vec3 = Vec3(position) if position else Vec3(0, 0, 0)
160
- self.previous_scale : Vec3 = Vec3(scale) if scale else Vec3(1, 1, 1)
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?
162
-
163
- # callback function to be added to the custom Vec3 and Quat classes
164
- def position_callback():
165
- self.chunk.node_update_callback(self)
166
-
167
- # update variables
168
- self.needs_geometric_center = True
169
- self.needs_model_matrix = True
170
- if self.collider:
171
- self.collider.needs_bvh = True
172
- self.collider.needs_obb = True
173
-
174
- def scale_callback():
175
- self.chunk.node_update_callback(self)
176
-
177
- # update variables
178
- self.needs_model_matrix = True
179
- if self.collider:
180
- self.collider.needs_bvh = True
181
- self.collider.needs_obb = True
182
- self.collider.needs_half_dimensions = True
183
-
184
- def rotation_callback():
185
- self.chunk.node_update_callback(self)
186
-
187
- # update variables
188
- self.needs_model_matrix = True
189
- if self.collider:
190
- self.collider.needs_bvh = True
191
- self.collider.needs_obb = True
192
- self.collider.needs_half_dimensions = True
193
-
194
- self.internal_position.callback = position_callback
195
- self.internal_scale.callback = scale_callback
196
- self.internal_rotation.callback = rotation_callback
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
-
215
- def update(self, dt: float) -> None:
216
- """
217
- Updates the node's movement variables based on the delta time
218
- """
219
- # update based on physical properties
220
- if any(self.velocity): self.position += dt * self.velocity
221
- if any(self.rotational_velocity): self.rotation = glm.normalize(self.rotation - dt / 2 * self.rotation * glm.quat(0, *self.rotational_velocity))
222
-
223
- if self.physics_body:
224
- self.velocity += self.physics_body.get_delta_velocity(dt)
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()
229
-
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()
246
-
247
- def get_nodes(self,
248
- require_mesh: bool=False,
249
- require_collider: bool=False,
250
- require_physics_body: bool=False,
251
- filter_material: Material=None,
252
- filter_tags: list[str]=None
253
- ) -> list:
254
- """
255
- Returns the nodes matching the required filters from this branch of the nodes
256
- """
257
- # adds self to nodes list if it matches the criteria
258
- nodes = []
259
-
260
- if all([
261
- (not require_mesh or self.mesh),
262
- (not require_collider or self.collider),
263
- (not require_physics_body or self.physics_body),
264
- (not filter_material or self.material == filter_material),
265
- (not filter_tags or all([tag in self.tags for tag in filter_tags]))
266
- ]):
267
- nodes.append(self)
268
-
269
- # adds children to nodes list if they match the criteria
270
- for node in self.children: nodes.extend(node.get_nodes(require_mesh, require_collider, require_physics_body, filter_material, filter_tags))
271
- return nodes
272
-
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)
284
-
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)
298
-
299
- def apply_force(self, force: glm.vec3, dt: float) -> None:
300
- """
301
- Applies a force at the center of the node
302
- """
303
- self.apply_offset_force(force, glm.vec3(0.0), dt)
304
-
305
- def apply_offset_force(self, force: glm.vec3, offset: glm.vec3, dt: float) -> None:
306
- """
307
- Applies a force at the given offset
308
- """
309
- # translation
310
- assert self.physics_body, 'Node: Cannot apply a force to a node that doesn\'t have a physics body'
311
- self.velocity = force / self.mass * dt
312
-
313
- # rotation
314
- torque = glm.cross(offset, force)
315
- self.apply_torque(torque, dt)
316
-
317
- def apply_torque(self, torque: glm.vec3, dt: float) -> None:
318
- """
319
- Applies a torque on the node
320
- """
321
- assert self.physics_body, 'Node: Cannot apply a torque to a node that doesn\'t have a physics body'
322
- ...
323
-
324
- # TODO change geometric variables into properties
325
- def get_inverse_inertia(self) -> glm.mat3x3:
326
- """
327
- Transforms the mesh inertia tensor and inverts it
328
- """
329
- if not (self.mesh and self.physics_body): return None
330
- inertia_tensor = self.mesh.get_inertia_tensor(self.scale) / 2
331
-
332
- # mass
333
- if self.physics_body: inertia_tensor *= self.physics_body.mass
334
-
335
- # rotation
336
- rotation_matrix = glm.mat3_cast(self.rotation)
337
- inertia_tensor = rotation_matrix * inertia_tensor * glm.transpose(rotation_matrix)
338
-
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))
346
-
347
- def get_data(self) -> np.ndarray:
348
- """
349
- Gets the node batch data for chunk batching
350
- """
351
-
352
- # Get data from the mesh node
353
- mesh_data = self.mesh.data
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
359
-
360
- # Create an array to hold the node's data
361
- data = np.zeros(shape=(mesh_data.shape[0], 25), dtype='f4')
362
-
363
-
364
- data[:,:14] = mesh_data
365
- data[:,14:] = node_data
366
-
367
- if per_vertex_mtl: data[:,24] = self.material
368
-
369
- return data
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
-
389
- def __repr__(self) -> str:
390
- """
391
- Returns a string representation of the node
392
- """
393
-
394
- return f'<Bailisk Node | {self.name}, {self.mesh}, ({self.position})>'
395
-
396
- @property
397
- def position(self): return self.internal_position.data
398
- @property
399
- def scale(self): return self.internal_scale.data
400
- @property
401
- def rotation(self): return self.internal_rotation.data
402
- @property
403
- def forward(self): return self._forward
404
- @property
405
- def mesh(self): return self._mesh
406
- @property
407
- def material(self): return self._material
408
- @property
409
- def velocity(self): return self._velocity
410
- @property
411
- def rotational_velocity(self): return self._rotational_velocity
412
- @property
413
- def mass(self):
414
- if self.physics_body: return self.physics_body.mass
415
- raise RuntimeError('Node: Cannot access the mass of a node that has no physics body')
416
- @property
417
- def static_friction(self):
418
- if self.collider: return self.collider.static_friction
419
- raise RuntimeError('Node: Cannot access the static friction of a node that has no collider')
420
- @property
421
- def kinetic_friction(self):
422
- if self.collider: return self.collider.kinetic_friction
423
- raise RuntimeError('Node: Cannot access the kinetic friction of a node that has no collider')
424
- @property
425
- def elasticity(self):
426
- if self.collider: return self.collider.elasticity
427
- raise RuntimeError('Node: Cannot access the elasticity of a node that has no collider')
428
- @property
429
- def collision_group(self):
430
- if self.collider: return self.collider.collision_group
431
- raise RuntimeError('Node: Cannot access the collision_group of a node that has no collider')
432
- @property
433
- def name(self): return self._name
434
- @property
435
- def tags(self): return self._tags
436
- @property
437
- def x(self): return self.internal_position.data.x
438
- @property
439
- def y(self): return self.internal_position.data.y
440
- @property
441
- def z(self): return self.internal_position.data.z
442
-
443
- # TODO add descriptions in the class header
444
- @property
445
- def model_matrix(self):
446
- if self.needs_model_matrix:
447
- self._model_matrix = get_model_matrix(self.position, self.scale, self.rotation)
448
- self.needs_model_matrix = False
449
- return self._model_matrix
450
- @property
451
- def geometric_center(self): # assumes the node has a mesh
452
- # if not self.mesh: raise RuntimeError('Node: Cannot retrieve geometric center if node does not have mesh')
453
- if self.needs_geometric_center:
454
- self._geometric_center = self.model_matrix * self.mesh.geometric_center
455
- self.needs_geometric_center = False
456
- return self._geometric_center
457
- @property
458
- def center_of_mass(self):
459
- if not self.mesh: raise RuntimeError('Node: Cannot retrieve center of mass if node does not have mesh')
460
- return self.model_matrix * self.mesh.center_of_mass
461
- @property
462
- def volume(self):
463
- if not self.mesh: raise RuntimeError('Node: Cannot retrieve volume if node does not have mesh')
464
- return self.mesh.volume * self.scale.x * self.scale.y * self.scale.z
465
-
466
- @position.setter
467
- def position(self, value: tuple | list | glm.vec3 | np.ndarray):
468
- if isinstance(value, glm.vec3): self.internal_position.data = value
469
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
470
- if len(value) != 3: raise ValueError(f'Node: Invalid number of values for position. Expected 3, got {len(value)}')
471
- self.internal_position.data = glm.vec3(value)
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
476
-
477
- @scale.setter
478
- def scale(self, value: tuple | list | glm.vec3 | np.ndarray):
479
- if isinstance(value, glm.vec3): self.internal_scale.data = value
480
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
481
- if len(value) != 3: raise ValueError(f'Node: Invalid number of values for scale. Expected 3, got {len(value)}')
482
- self.internal_scale.data = glm.vec3(value)
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
487
-
488
- @rotation.setter
489
- def rotation(self, value: tuple | list | glm.vec3 | glm.quat | glm.vec4 | np.ndarray):
490
- if isinstance(value, glm.quat) or isinstance(value, glm.vec4) or isinstance(value, glm.vec3): self.internal_rotation.data = glm.quat(value)
491
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
492
- if len(value) == 3: self.internal_rotation.data = glm.quat(glm.vec3(*value))
493
- elif len(value) == 4: self.internal_rotation.data = glm.quat(*value)
494
- else: raise ValueError(f'Node: Invalid number of values for rotation. Expected 3 or 4, got {len(value)}')
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)
499
-
500
- @forward.setter
501
- def forward(self, value: tuple | list | glm.vec3 | np.ndarray):
502
- if isinstance(value, glm.vec3): self._forward = value
503
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
504
- if len(value) != 3: raise ValueError(f'Node: Invalid number of values for forward. Expected 3, got {len(value)}')
505
- self._forward = glm.vec3(value)
506
- else: raise TypeError(f'Node: Invalid forward value type {type(value)}')
507
-
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)}')
519
-
520
- @material.setter
521
- def material(self, 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):
535
- self._material = value
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
-
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)
544
-
545
- @velocity.setter
546
- def velocity(self, value: tuple | list | glm.vec3 | np.ndarray):
547
- if isinstance(value, glm.vec3): self._velocity = glm.vec3(value)
548
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
549
- if len(value) != 3: raise ValueError(f'Node: Invalid number of values for velocity. Expected 3, got {len(value)}')
550
- self._velocity = glm.vec3(value)
551
- else: raise TypeError(f'Node: Invalid velocity value type {type(value)}')
552
-
553
- @rotational_velocity.setter
554
- def rotational_velocity(self, value: tuple | list | glm.vec3 | np.ndarray):
555
- if isinstance(value, glm.vec3): self._rotational_velocity = glm.vec3(value)
556
- elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
557
- if len(value) != 3: raise ValueError(f'Node: Invalid number of values for rotational velocity. Expected 3, got {len(value)}')
558
- self._rotational_velocity = glm.vec3(value)
559
- else: raise TypeError(f'Node: Invalid rotational velocity value type {type(value)}')
560
-
561
- @mass.setter
562
- def mass(self, value: int | float):
563
- if not self.physics_body: raise RuntimeError('Node: Cannot set the mass of a node that has no physics body')
564
- if isinstance(value, int) or isinstance(value, float): self.physics_body.mass = value
565
- else: raise TypeError(f'Node: Invalid mass value type {type(value)}')
566
-
567
- @static_friction.setter
568
- def static_friction(self, value: int | float):
569
- if not self.collider: raise RuntimeError('Node: Cannot set the static friction of a node that has no physics body')
570
- if isinstance(value, int) or isinstance(value, float): self.collider.static_friction = value
571
- else: raise TypeError(f'Node: Invalid static friction value type {type(value)}')
572
-
573
- @kinetic_friction.setter
574
- def kinetic_friction(self, value: int | float):
575
- if not self.collider: raise RuntimeError('Node: Cannot set the kinetic friction of a node that has no physics body')
576
- if isinstance(value, int) or isinstance(value, float): self.collider.kinetic_friction = value
577
- else: raise TypeError(f'Node: Invalid kinetic friction value type {type(value)}')
578
-
579
- @elasticity.setter
580
- def elasticity(self, value: int | float):
581
- if not self.collider: raise RuntimeError('Node: Cannot set the elasticity of a node that has no physics body')
582
- if isinstance(value, int) or isinstance(value, float): self.collider.elasticity = value
583
- else: raise TypeError(f'Node: Invalid elasticity value type {type(value)}')
584
-
585
- @collision_group.setter
586
- def collision_group(self, value: str):
587
- if not self.collider: raise RuntimeError('Node: Cannot set the collision gruop of a node that has no physics body')
588
- if isinstance(value, str): self.collider.collision_group = value
589
- else: raise TypeError(f'Node: Invalid collision group value type {type(value)}')
590
-
591
- @name.setter
592
- def name(self, value: str):
593
- if isinstance(value, str): self._name = value
594
- else: raise TypeError(f'Node: Invalid name value type {type(value)}')
595
-
596
- @tags.setter
597
- def tags(self, value: list[str]):
598
- if isinstance(value, list) or isinstance(value, tuple):
599
- for tag in value:
600
- if not isinstance(tag, str): raise TypeError(f'Node: Invalid tag value in tags list of type {type(tag)}')
601
- self._tags = value
602
- else: raise TypeError(f'Node: Invalid tags value type {type(value)}')
603
-
604
- @x.setter
605
- def x(self, value: int | float):
606
- if isinstance(value, int) or isinstance(value, float): self.internal_position.x = value
607
- else: raise TypeError(f'Node: Invalid positional x value type {type(value)}')
608
-
609
- @y.setter
610
- def y(self, value: int | float):
611
- if isinstance(value, int) or isinstance(value, float): self.internal_position.y = value
612
- else: raise TypeError(f'Node: Invalid positional y value type {type(value)}')
613
-
614
- @z.setter
615
- def z(self, value: int | float):
616
- if isinstance(value, int) or isinstance(value, float): self.internal_position.z = value
617
- else: raise TypeError(f'Node: Invalid positional z value type {type(value)}')
1
+ import glm
2
+ import numpy as np
3
+ from .helper import node_is
4
+ from ..generic.vec3 import Vec3
5
+ from ..generic.quat import Quat
6
+ from ..generic.matrices import get_model_matrix
7
+ from ..mesh.mesh import Mesh
8
+ from ..render.material import Material
9
+ from ..physics.physics_body import PhysicsBody
10
+ from ..collisions.collider import Collider
11
+ from ..render.chunk import Chunk
12
+ from ..render.shader import Shader
13
+
14
+
15
+ class Node():
16
+ position: Vec3
17
+ """The position of the node in meters with swizzle xyz"""
18
+ scale: Vec3
19
+ """The scale of the node in meters in each direction"""
20
+ rotation: Quat
21
+ """The rotation of the node"""
22
+ position_relative: bool
23
+ """The position of this node relative to the parent node"""
24
+ scale_relative: bool
25
+ """The scale of this node relative to the parent node"""
26
+ rotation_relative: bool
27
+ """The rotation of this node relative to the parent node"""
28
+ forward: glm.vec3
29
+ """The forward facing vector of the node"""
30
+ mesh: Mesh
31
+ """The mesh of the node stored as a basilisk mesh object"""
32
+ material: Material
33
+ """The mesh of the node stored as a basilisk material object"""
34
+ velocity: glm.vec3
35
+ """The translational velocity of the node"""
36
+ rotational_velocity: glm.vec3
37
+ """The rotational velocity of the node"""
38
+ physics: bool
39
+ """Allows the node's movement to be affected by the physics engine and collisions"""
40
+ mass: float
41
+ """The mass of the node in kg"""
42
+ collision: bool
43
+ """Gives the node collision with other nodes in the scene"""
44
+ collider_mesh: str
45
+ """The collider type of the node. Can be either 'box' or 'mesh'"""
46
+ static_friction: float
47
+ """Determines the friction of the node when still: recommended value 0.0 - 1.0"""
48
+ kinetic_friction: float
49
+ """Determines the friction of the node when moving: recommended value 0.0 - 1.0"""
50
+ elasticity: float
51
+ """Determines how bouncy an object is: recommended value 0.0 - 1.0"""
52
+ collision_group: str
53
+ """Nodes of the same collision group do not collide with each other"""
54
+ name: str
55
+ """The name of the node for reference"""
56
+ tags: list[str]
57
+ """Tags are used to sort nodes into separate groups"""
58
+ static: bool
59
+ """Objects that don't move should be marked as static"""
60
+ chunk: Chunk
61
+ """""" # TODO Jonah description
62
+ children: list
63
+ """List of nodes that this node is a parent of"""
64
+ shader: Shader
65
+ """Shader that is used to render the node. If none is given, engine default will be used"""
66
+
67
+ def __init__(self,
68
+ position: glm.vec3=None,
69
+ scale: glm.vec3=None,
70
+ rotation: glm.quat=None,
71
+ relative_position: bool=True,
72
+ relative_scale: bool=True,
73
+ relative_rotation: bool=True,
74
+ forward: glm.vec3=None,
75
+ mesh: Mesh=None,
76
+ material: Material=None,
77
+ velocity: glm.vec3=None,
78
+ rotational_velocity: glm.vec3=None,
79
+ physics: bool=False,
80
+ mass: float=None,
81
+ collision: bool=False,
82
+ collider_mesh: str|Mesh=None,
83
+ static_friction: float=None,
84
+ kinetic_friction: float=None,
85
+ elasticity: float=None,
86
+ collision_group: float=None,
87
+ name: str='',
88
+ tags: list[str]=None,
89
+ static: bool=None,
90
+ shader: Shader=None
91
+ ) -> None:
92
+ """
93
+ Basilisk node object.
94
+ Contains mesh data, translation, material, physics, collider, and descriptive information.
95
+ Base building block for populating a Basilisk scene.
96
+ """
97
+
98
+ # parents
99
+ self.node_handler = None
100
+ self.scene = None
101
+ self.chunk = None
102
+ self.parent = None
103
+
104
+ # lazy update variables
105
+ self.needs_geometric_center = True # pos
106
+ self.needs_model_matrix = True # pos, scale, rot
107
+
108
+ # node data
109
+ self.internal_position: Vec3 = Vec3(position) if position else Vec3(0, 0, 0)
110
+ self.internal_scale : Vec3 = Vec3(scale) if scale else Vec3(1, 1, 1)
111
+ self.internal_rotation: Quat = Quat(rotation) if rotation else Quat(1, 0, 0, 0)
112
+
113
+ # relative transformations
114
+ self.relative_position = glm.vec3(0, 0, 0) if relative_position else None
115
+ self.relative_scale = glm.vec3(0, 0, 0) if relative_scale else None
116
+ self.relative_rotation = glm.quat(1, 0, 0, 0) if relative_rotation else None
117
+
118
+ self.forward = forward if forward else glm.vec3(1, 0, 0)
119
+ self.mesh = mesh
120
+ self._mtl_list = material if isinstance(material, list) else [material]
121
+ self.material = material if material else None
122
+ self.velocity = velocity if velocity else glm.vec3(0, 0, 0)
123
+ self.rotational_velocity = rotational_velocity if rotational_velocity else glm.vec3(0, 0, 0)
124
+
125
+ self._static = static
126
+
127
+ # Physics updates
128
+ if physics: self.physics_body = PhysicsBody(mass = mass if mass else 1.0)
129
+ elif mass: raise ValueError('Node: cannot have mass if it does not have physics')
130
+ else: self.physics_body = None
131
+
132
+ # collider
133
+ if collision:
134
+ self.collider = Collider(
135
+ node = self,
136
+ collider_mesh = collider_mesh,
137
+ static_friction = static_friction,
138
+ kinetic_friction = kinetic_friction,
139
+ elasticity = elasticity,
140
+ collision_group = collision_group
141
+ )
142
+ elif collider_mesh: raise ValueError('Node: cannot have collider mesh if it does not allow collisions')
143
+ elif static_friction: raise ValueError('Node: cannot have static friction if it does not allow collisions')
144
+ elif kinetic_friction: raise ValueError('Node: cannot have kinetic friction if it does not allow collisions')
145
+ elif elasticity: raise ValueError('Node: cannot have elasticity if it does not allow collisions')
146
+ elif collision_group: raise ValueError('Node: cannot have collider group if it does not allow collisions')
147
+ else: self.collider = None
148
+
149
+ # information and recursion
150
+ self.name = name
151
+ self.tags = tags if tags else []
152
+
153
+ self.data_index = 0
154
+ self.children = []
155
+
156
+ # Shader given by user or none for default
157
+ self.shader = shader
158
+
159
+ # callback function to be added to the custom Vec3 and Quat classes
160
+ def position_callback():
161
+ if self.chunk:
162
+ self.chunk.node_update_callback(self)
163
+
164
+ # update variables
165
+ self.needs_geometric_center = True
166
+ self.needs_model_matrix = True
167
+ if self.collider:
168
+ self.collider.needs_bvh = True
169
+ self.collider.needs_obb = True
170
+
171
+ def scale_callback():
172
+ if self.chunk:
173
+ self.chunk.node_update_callback(self)
174
+
175
+ # update variables
176
+ self.needs_model_matrix = True
177
+ if self.collider:
178
+ self.collider.needs_bvh = True
179
+ self.collider.needs_obb = True
180
+ self.collider.needs_half_dimensions = True
181
+
182
+ def rotation_callback():
183
+ if self.chunk:
184
+ self.chunk.node_update_callback(self)
185
+
186
+ # update variables
187
+ self.needs_model_matrix = True
188
+ if self.collider:
189
+ self.collider.needs_bvh = True
190
+ self.collider.needs_obb = True
191
+ self.collider.needs_half_dimensions = True
192
+
193
+ self.internal_position.callback = position_callback
194
+ self.internal_scale.callback = scale_callback
195
+ self.internal_rotation.callback = rotation_callback
196
+
197
+ def init_scene(self, scene):
198
+ """
199
+ Updates the scene of the node
200
+ """
201
+ self.scene = scene
202
+ self.node_handler = scene.node_handler
203
+
204
+ # Update materials
205
+ self.write_materials()
206
+
207
+ # Update the mesh
208
+ self.mesh = self.mesh if self.mesh else self.scene.engine.cube
209
+
210
+ # Update physics and collider
211
+ if self.physics_body: self.physics_body.physics_engine = scene.physics_engine
212
+ if self.collider: self.collider.collider_handler = scene.collider_handler
213
+
214
+ def update(self, dt: float) -> None:
215
+ """
216
+ Updates the node's movement variables based on the delta time
217
+ """
218
+ # update based on physical properties
219
+ if any(self.velocity): self.position += dt * self.velocity
220
+ if any(self.rotational_velocity): self.rotation = glm.normalize(self.rotation - dt / 2 * self.rotation * glm.quat(0, *self.rotational_velocity))
221
+
222
+ if self.physics_body:
223
+ self.velocity += self.physics_body.get_delta_velocity(dt)
224
+ self.rotational_velocity += self.physics_body.get_delta_rotational_velocity(dt)
225
+
226
+ # update children transforms
227
+ for child in self.children: child.sync_data()
228
+
229
+ def sync_data(self) -> None:
230
+ """
231
+ Syncronizes this node with the parent node based on its relative positioning
232
+ """
233
+ # calculate transform matrix with the given input
234
+ transform = glm.mat4x4()
235
+ if self.relative_position: transform = glm.translate(transform, self.parent.position)
236
+ if self.relative_rotation: transform *= glm.transpose(glm.mat4_cast(self.parent.rotation))
237
+ if self.relative_scale: transform = glm.scale(transform, self.parent.scale)
238
+
239
+ # set this node's transforms based on the parent
240
+ self.position = transform * self.relative_position
241
+ self.scale = self.relative_scale * self.parent.scale
242
+ self.rotation = self.relative_rotation * self.parent.rotation
243
+
244
+ for child in self.children: child.sync_data()
245
+
246
+ def deep_copy(self) -> ...:
247
+ """
248
+ Creates a deep copy of this node and returns it. The new node is not added to the scene.
249
+ """
250
+
251
+ copy = Node(
252
+ position = self.position,
253
+ scale = self.scale,
254
+ rotation = self.rotation,
255
+ relative_position = bool(self.relative_position),
256
+ relative_scale = bool(self.relative_scale),
257
+ relative_rotation = bool(self.relative_rotation),
258
+ forward = glm.vec3(self.forward),
259
+ mesh = self.mesh,
260
+ material = self.material,
261
+ velocity = glm.vec3(self.velocity),
262
+ rotational_velocity = glm.vec3(self.rotational_velocity),
263
+ physics = bool(self.physics_body),
264
+ mass = self.mass if self.physics_body else None,
265
+ collision = bool(self.collider),
266
+ static_friction = self.static_friction if self.collider else None,
267
+ kinetic_friction = self.kinetic_friction if self.collider else None,
268
+ elasticity = self.elasticity if self.collider else None,
269
+ collision_group = self.collision_group if self.collider else None,
270
+ name = self.name,
271
+ tags = [tag for tag in self.tags], # deep copy tags list
272
+ static = self.static,
273
+ shader = self.shader
274
+ )
275
+
276
+ return copy
277
+
278
+ 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) -> list:
279
+ nodes = [self] if node_is(self, position, scale, rotation, forward, mesh, material, velocity, rotational_velocity, physics, mass, collisions, static_friction, kinetic_friction, elasticity, collision_group, name, tags, static) else []
280
+ for node in self.children: nodes += node.get_all(position, scale, rotation, forward, mesh, material, velocity, rotational_velocity, physics, mass, collisions, static_friction, kinetic_friction, elasticity, collision_group, name, tags, static)
281
+ return nodes
282
+
283
+ # tree functions for managing children
284
+ def add(self, child: ..., relative_position: bool=None, relative_scale: bool=None, relative_rotation: glm.vec3=None) -> None:
285
+ """
286
+ 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.
287
+ """
288
+ if child in self.children: return
289
+
290
+ # compute relative transformations
291
+ if relative_position or (relative_position is None and child.relative_position): child.relative_position = child.position - self.position
292
+ if relative_scale or (relative_scale is None and child.relative_scale): child.relative_scale = child.scale / self.scale
293
+ if relative_rotation or (relative_rotation is None and child.relative_rotation): child.relative_rotation = child.rotation * glm.inverse(self.rotation)
294
+
295
+ # add as a child to by synchronized and controlled
296
+ if self.node_handler: self.node_handler.add(child)
297
+ child.parent = self
298
+ self.children.append(child)
299
+
300
+ def remove(self, child: ...) -> None:
301
+ """
302
+ Removes a child node from this nodes chlid list.
303
+ """
304
+ if child in self.children:
305
+ if self.node_handler: self.node_handler.remove(child)
306
+ child.parent = None
307
+ self.children.remove(child)
308
+
309
+ def apply_force(self, force: glm.vec3, dt: float) -> None:
310
+ """
311
+ Applies a force at the center of the node
312
+ """
313
+ self.apply_offset_force(force, glm.vec3(0.0), dt)
314
+
315
+ def apply_offset_force(self, force: glm.vec3, offset: glm.vec3, dt: float) -> None:
316
+ """
317
+ Applies a force at the given offset
318
+ """
319
+ # translation
320
+ assert self.physics_body, 'Node: Cannot apply a force to a node that doesn\'t have a physics body'
321
+ self.velocity += force / self.mass * dt
322
+
323
+ # rotation
324
+ torque = glm.cross(offset, force)
325
+ self.apply_torque(torque, dt)
326
+
327
+ def apply_torque(self, torque: glm.vec3, dt: float) -> None:
328
+ """
329
+ Applies a torque on the node
330
+ """
331
+ assert self.physics_body, 'Node: Cannot apply a torque to a node that doesn\'t have a physics body'
332
+ ...
333
+
334
+ # TODO change geometric variables into properties
335
+ def get_inverse_inertia(self) -> glm.mat3x3:
336
+ """
337
+ Transforms the mesh inertia tensor and inverts it
338
+ """
339
+ if not ((self.mesh or (self.collider and self.collider.mesh)) and self.physics_body): return None
340
+ mesh = self.collider.mesh if self.collider else self.mesh
341
+ inertia_tensor = mesh.get_inertia_tensor(self.scale) / 2
342
+
343
+ # mass
344
+ if self.physics_body: inertia_tensor *= self.physics_body.mass
345
+
346
+ # rotation
347
+ rotation_matrix = glm.mat3_cast(self.rotation)
348
+ inertia_tensor = rotation_matrix * inertia_tensor * glm.transpose(rotation_matrix)
349
+
350
+ return glm.inverse(inertia_tensor)
351
+
352
+ def get_vertex(self, index) -> glm.vec3:
353
+ """
354
+ Gets the world space position of a vertex indicated by the index in the mesh
355
+ """
356
+ return glm.vec3(self.model_matrix * glm.vec4(*self.mesh.points[index], 1))
357
+
358
+ def get_data(self) -> np.ndarray:
359
+ """
360
+ Gets the node batch data for chunk batching
361
+ """
362
+
363
+ # Get data from the mesh node
364
+ mesh_data = self.mesh.data
365
+ node_data = np.array([*self.position, *self.rotation, *self.scale, 0])
366
+
367
+ per_vertex_mtl = isinstance(self.material, list)
368
+
369
+ if not per_vertex_mtl: node_data[-1] = self.material.index
370
+
371
+ # Create an array to hold the node's data
372
+ data = np.zeros(shape=(mesh_data.shape[0], 25), dtype='f4')
373
+
374
+
375
+ data[:,:14] = mesh_data
376
+ data[:,14:] = node_data
377
+
378
+ if per_vertex_mtl: data[:,24] = self.material
379
+
380
+ return data
381
+
382
+ def write_materials(self):
383
+ """
384
+ Internal function to write the material list to the material handler and get the material ids
385
+ """
386
+
387
+ if isinstance(self.material, list):
388
+ mtl_index_list = []
389
+ for mtl in self._mtl_list:
390
+ self.node_handler.scene.material_handler.add(mtl)
391
+ mtl_index_list.append(mtl.index)
392
+ mtl_index_list.append(mtl.index)
393
+ mtl_index_list.append(mtl.index)
394
+ self._material = mtl_index_list
395
+
396
+ if isinstance(self.material, type(None)):
397
+ self.material = self.scene.material_handler.base
398
+
399
+
400
+ def __repr__(self) -> str:
401
+ """
402
+ Returns a string representation of the node
403
+ """
404
+
405
+ return f'<Bailisk Node | {self.name}, {self.mesh}, ({self.position})>'
406
+
407
+ @property
408
+ def position(self): return self.internal_position.data
409
+ @property
410
+ def scale(self): return self.internal_scale.data
411
+ @property
412
+ def rotation(self): return self.internal_rotation.data
413
+ @property
414
+ def forward(self): return self._forward
415
+ @property
416
+ def mesh(self): return self._mesh
417
+ @property
418
+ def material(self): return self._material
419
+ @property
420
+ def velocity(self): return self._velocity
421
+ @property
422
+ def rotational_velocity(self): return self._rotational_velocity
423
+ @property
424
+ def mass(self):
425
+ if self.physics_body: return self.physics_body.mass
426
+ raise RuntimeError('Node: Cannot access the mass of a node that has no physics body')
427
+ @property
428
+ def static_friction(self):
429
+ if self.collider: return self.collider.static_friction
430
+ raise RuntimeError('Node: Cannot access the static friction of a node that has no collider')
431
+ @property
432
+ def kinetic_friction(self):
433
+ if self.collider: return self.collider.kinetic_friction
434
+ raise RuntimeError('Node: Cannot access the kinetic friction of a node that has no collider')
435
+ @property
436
+ def elasticity(self):
437
+ if self.collider: return self.collider.elasticity
438
+ raise RuntimeError('Node: Cannot access the elasticity of a node that has no collider')
439
+ @property
440
+ def collision_group(self):
441
+ if self.collider: return self.collider.collision_group
442
+ raise RuntimeError('Node: Cannot access the collision_group of a node that has no collider')
443
+ @property
444
+ def name(self): return self._name
445
+ @property
446
+ def tags(self): return self._tags
447
+ @property
448
+ def static(self):
449
+ return self._static if self._static is not None else not(self.physics or any(self.velocity) or any(self.rotational_velocity))
450
+ @property
451
+ def x(self): return self.internal_position.data.x
452
+ @property
453
+ def y(self): return self.internal_position.data.y
454
+ @property
455
+ def z(self): return self.internal_position.data.z
456
+
457
+ # TODO add descriptions in the class header
458
+ @property
459
+ def model_matrix(self):
460
+ if self.needs_model_matrix:
461
+ self._model_matrix = get_model_matrix(self.position, self.scale, self.rotation)
462
+ self.needs_model_matrix = False
463
+ return self._model_matrix
464
+ @property
465
+ def geometric_center(self): # assumes the node has a mesh
466
+ # if not self.mesh: raise RuntimeError('Node: Cannot retrieve geometric center if node does not have mesh')
467
+ if self.needs_geometric_center:
468
+ self._geometric_center = self.model_matrix * self.mesh.geometric_center
469
+ self.needs_geometric_center = False
470
+ return self._geometric_center
471
+ @property
472
+ def center_of_mass(self):
473
+ if not self.mesh: raise RuntimeError('Node: Cannot retrieve center of mass if node does not have mesh')
474
+ return self.model_matrix * self.mesh.center_of_mass
475
+ @property
476
+ def volume(self):
477
+ if not self.mesh: raise RuntimeError('Node: Cannot retrieve volume if node does not have mesh')
478
+ return self.mesh.volume * self.scale.x * self.scale.y * self.scale.z
479
+
480
+ @property
481
+ def physics(self):
482
+ return bool(self.physics_body)
483
+ @property
484
+ def collision(self):
485
+ return bool(self.collider)
486
+
487
+ @property
488
+ def collisions(self):
489
+ assert self.collision, 'Node: Cannot access collision data without collisions enabled on Node'
490
+ return self.collider.collisions
491
+
492
+ @position.setter
493
+ def position(self, value: tuple | list | glm.vec3 | np.ndarray):
494
+ if isinstance(value, glm.vec3): self.internal_position.data = value
495
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
496
+ if len(value) != 3: raise ValueError(f'Node: Invalid number of values for position. Expected 3, got {len(value)}')
497
+ self.internal_position.data = glm.vec3(value)
498
+ else: raise TypeError(f'Node: Invalid position value type {type(value)}')
499
+
500
+ # # compute relative position if all cases passed
501
+ # if self.parent and self.relative_position: self.relative_position = self.internal_position.data - self.parent.position
502
+
503
+ @scale.setter
504
+ def scale(self, value: tuple | list | glm.vec3 | np.ndarray):
505
+ if isinstance(value, glm.vec3): self.internal_scale.data = value
506
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
507
+ if len(value) != 3: raise ValueError(f'Node: Invalid number of values for scale. Expected 3, got {len(value)}')
508
+ self.internal_scale.data = glm.vec3(value)
509
+ else: raise TypeError(f'Node: Invalid scale value type {type(value)}')
510
+
511
+ # # compute relative scale
512
+ # if self.parent and self.relative_scale: self.relative_scale = self.internal_scale.data / self.parent.scale
513
+
514
+ @rotation.setter
515
+ def rotation(self, value: tuple | list | glm.vec3 | glm.quat | glm.vec4 | np.ndarray):
516
+ if isinstance(value, glm.quat) or isinstance(value, glm.vec4) or isinstance(value, glm.vec3): self.internal_rotation.data = glm.quat(value)
517
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
518
+ if len(value) == 3: self.internal_rotation.data = glm.quat(glm.vec3(*value))
519
+ elif len(value) == 4: self.internal_rotation.data = glm.quat(*value)
520
+ else: raise ValueError(f'Node: Invalid number of values for rotation. Expected 3 or 4, got {len(value)}')
521
+ else: raise TypeError(f'Node: Invalid rotation value type {type(value)}')
522
+
523
+ # # compute relative rotation
524
+ # if self.parent and self.relative_rotation: self.relative_rotation = self.rotation * glm.inverse(self.parent.rotation)
525
+
526
+ @forward.setter
527
+ def forward(self, value: tuple | list | glm.vec3 | np.ndarray):
528
+ if isinstance(value, glm.vec3): self._forward = value
529
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
530
+ if len(value) != 3: raise ValueError(f'Node: Invalid number of values for forward. Expected 3, got {len(value)}')
531
+ self._forward = glm.vec3(value)
532
+ else: raise TypeError(f'Node: Invalid forward value type {type(value)}')
533
+
534
+ @mesh.setter
535
+ def mesh(self, value: Mesh | None):
536
+ if isinstance(value, Mesh):
537
+ self._mesh = value
538
+ if self.chunk: self.chunk.update()
539
+ elif isinstance(value, type(None)):
540
+ self._mesh = None
541
+ if not self.chunk: return
542
+ self.chunk.remove(self)
543
+ self.chunk.update()
544
+ else: raise TypeError(f'Node: Invalid mesh value type {type(value)}')
545
+
546
+ @material.setter
547
+ def material(self, value: Material):
548
+ if isinstance(value, list):
549
+ self._mtl_list = value
550
+ if not self.node_handler:
551
+ self._material = value
552
+ else:
553
+ mtl_index_list = []
554
+ for mtl in self._mtl_list:
555
+ self.node_handler.scene.material_handler.add(mtl)
556
+ mtl_index_list.append(mtl.index)
557
+ mtl_index_list.append(mtl.index)
558
+ mtl_index_list.append(mtl.index)
559
+ self._material = mtl_index_list
560
+ elif isinstance(value, Material):
561
+ self._material = value
562
+ if self.node_handler: self.node_handler.scene.material_handler.add(value)
563
+ elif isinstance(value, type(None)):
564
+ if self.scene: self._material = self.scene.material_handler.base
565
+ else: self._material = None
566
+
567
+ else: raise TypeError(f'Node: Invalid material value type {type(value)}')
568
+ if not self.chunk: return
569
+ self.chunk.node_update_callback(self)
570
+
571
+ @velocity.setter
572
+ def velocity(self, value: tuple | list | glm.vec3 | np.ndarray):
573
+ if isinstance(value, glm.vec3): self._velocity = glm.vec3(value)
574
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
575
+ if len(value) != 3: raise ValueError(f'Node: Invalid number of values for velocity. Expected 3, got {len(value)}')
576
+ self._velocity = glm.vec3(value)
577
+ else: raise TypeError(f'Node: Invalid velocity value type {type(value)}')
578
+
579
+ @rotational_velocity.setter
580
+ def rotational_velocity(self, value: tuple | list | glm.vec3 | np.ndarray):
581
+ if isinstance(value, glm.vec3): self._rotational_velocity = glm.vec3(value)
582
+ elif isinstance(value, tuple) or isinstance(value, list) or isinstance(value, np.ndarray):
583
+ if len(value) != 3: raise ValueError(f'Node: Invalid number of values for rotational velocity. Expected 3, got {len(value)}')
584
+ self._rotational_velocity = glm.vec3(value)
585
+ else: raise TypeError(f'Node: Invalid rotational velocity value type {type(value)}')
586
+
587
+ @mass.setter
588
+ def mass(self, value: int | float):
589
+ if not self.physics_body: raise RuntimeError('Node: Cannot set the mass of a node that has no physics body')
590
+ if isinstance(value, int) or isinstance(value, float): self.physics_body.mass = value
591
+ else: raise TypeError(f'Node: Invalid mass value type {type(value)}')
592
+
593
+ @static_friction.setter
594
+ def static_friction(self, value: int | float):
595
+ if not self.collider: raise RuntimeError('Node: Cannot set the static friction of a node that has no physics body')
596
+ if isinstance(value, int) or isinstance(value, float): self.collider.static_friction = value
597
+ else: raise TypeError(f'Node: Invalid static friction value type {type(value)}')
598
+
599
+ @kinetic_friction.setter
600
+ def kinetic_friction(self, value: int | float):
601
+ if not self.collider: raise RuntimeError('Node: Cannot set the kinetic friction of a node that has no physics body')
602
+ if isinstance(value, int) or isinstance(value, float): self.collider.kinetic_friction = value
603
+ else: raise TypeError(f'Node: Invalid kinetic friction value type {type(value)}')
604
+
605
+ @elasticity.setter
606
+ def elasticity(self, value: int | float):
607
+ if not self.collider: raise RuntimeError('Node: Cannot set the elasticity of a node that has no physics body')
608
+ if isinstance(value, int) or isinstance(value, float): self.collider.elasticity = value
609
+ else: raise TypeError(f'Node: Invalid elasticity value type {type(value)}')
610
+
611
+ @collision_group.setter
612
+ def collision_group(self, value: str):
613
+ if not self.collider: raise RuntimeError('Node: Cannot set the collision gruop of a node that has no physics body')
614
+ if isinstance(value, str): self.collider.collision_group = value
615
+ else: raise TypeError(f'Node: Invalid collision group value type {type(value)}')
616
+
617
+ @name.setter
618
+ def name(self, value: str):
619
+ if isinstance(value, str): self._name = value
620
+ else: raise TypeError(f'Node: Invalid name value type {type(value)}')
621
+
622
+ @tags.setter
623
+ def tags(self, value: list[str]):
624
+ if isinstance(value, list) or isinstance(value, tuple):
625
+ for tag in value:
626
+ if not isinstance(tag, str): raise TypeError(f'Node: Invalid tag value in tags list of type {type(tag)}')
627
+ self._tags = value
628
+ else: raise TypeError(f'Node: Invalid tags value type {type(value)}')
629
+
630
+ @static.setter
631
+ def static(self, value: bool):
632
+ self._static = value
633
+
634
+ @x.setter
635
+ def x(self, value: int | float):
636
+ if isinstance(value, int) or isinstance(value, float): self.internal_position.x = value
637
+ else: raise TypeError(f'Node: Invalid positional x value type {type(value)}')
638
+
639
+ @y.setter
640
+ def y(self, value: int | float):
641
+ if isinstance(value, int) or isinstance(value, float): self.internal_position.y = value
642
+ else: raise TypeError(f'Node: Invalid positional y value type {type(value)}')
643
+
644
+ @z.setter
645
+ def z(self, value: int | float):
646
+ if isinstance(value, int) or isinstance(value, float): self.internal_position.z = value
647
+ else: raise TypeError(f'Node: Invalid positional z value type {type(value)}')
648
+
649
+ @physics.setter
650
+ def physics(self, value: bool | PhysicsBody):
651
+ if not value and self.physics: # remove physics body from self and scene
652
+ if self.node_handler: self.node_handler.scene.physics_engine.remove(self.physics_body)
653
+ self.physics_body = None
654
+ elif isinstance(value, PhysicsBody): # deep copy physics body
655
+ if self.physics:
656
+ self.mass = value.mass
657
+ else:
658
+ self.physics_body = PhysicsBody(value.mass)
659
+ if self.node_handler: self.physics_body.physics_engine = self.node_handler.scene.physics_engine
660
+ elif not self.physics:
661
+ self.physics_body = PhysicsBody(mass = 1)
662
+ if self.node_handler: self.physics_body.physics_engine = self.node_handler.scene.physics_engine
663
+
664
+
665
+ @collision.setter
666
+ def collision(self, value: bool | PhysicsBody):
667
+ if not value and self.collision:
668
+ if self.node_handler: self.node_handler.scene.collider_handler.remove(self.collider)
669
+ self.collider = None
670
+ elif isinstance(value, Collider):
671
+ if self.collision:
672
+ self.kinetic_friction = value.kinetic_friction
673
+ self.elasticity = value.elasticity
674
+ self.static_friction = value.static_friction
675
+ else:
676
+ self.collider = Collider(self, value.mesh, value.static_friction, value.kinetic_friction, value.elasticity, value.collision_group)
677
+ if self.node_handler: self.collider.collider_handler = self.node_handler.scene.collider_handler
678
+ elif not self.collider:
679
+ self.collider = Collider(self)
680
+ if self.node_handler: self.collider.collider_handler = self.node_handler.scene.collider_handler
681
+