ncca-ngl 0.3.4__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ncca/ngl/PrimData/pack_arrays.py +2 -3
- ncca/ngl/__init__.py +3 -4
- ncca/ngl/base_mesh.py +28 -20
- ncca/ngl/image.py +1 -3
- ncca/ngl/mat2.py +79 -53
- ncca/ngl/mat3.py +104 -185
- ncca/ngl/mat4.py +144 -309
- ncca/ngl/prim_data.py +42 -36
- ncca/ngl/primitives.py +2 -2
- ncca/ngl/pyside_event_handling_mixin.py +0 -108
- ncca/ngl/quaternion.py +69 -36
- ncca/ngl/shader.py +0 -116
- ncca/ngl/shader_program.py +94 -117
- ncca/ngl/texture.py +5 -2
- ncca/ngl/util.py +7 -0
- ncca/ngl/vec2.py +58 -292
- ncca/ngl/vec2_array.py +79 -28
- ncca/ngl/vec3.py +59 -340
- ncca/ngl/vec3_array.py +76 -23
- ncca/ngl/vec4.py +90 -190
- ncca/ngl/vec4_array.py +78 -27
- ncca/ngl/vector_base.py +542 -0
- ncca/ngl/webgpu/__init__.py +20 -0
- ncca/ngl/webgpu/__main__.py +640 -0
- ncca/ngl/webgpu/__main__.py.backup +640 -0
- ncca/ngl/webgpu/base_webgpu_pipeline.py +354 -0
- ncca/ngl/webgpu/custom_shader_pipeline.py +288 -0
- ncca/ngl/webgpu/instanced_geometry_pipeline.py +594 -0
- ncca/ngl/webgpu/line_pipeline.py +405 -0
- ncca/ngl/webgpu/pipeline_factory.py +190 -0
- ncca/ngl/webgpu/pipeline_shaders.py +497 -0
- ncca/ngl/webgpu/point_list_pipeline.py +349 -0
- ncca/ngl/webgpu/point_pipeline.py +336 -0
- ncca/ngl/webgpu/triangle_pipeline.py +419 -0
- ncca/ngl/webgpu/webgpu_constants.py +31 -0
- ncca/ngl/webgpu/webgpu_widget.py +322 -0
- ncca/ngl/webgpu/wip/REFACTORING_SUMMARY.md +82 -0
- ncca/ngl/webgpu/wip/UNIFIED_SYSTEM.md +314 -0
- ncca/ngl/webgpu/wip/buffer_manager.py +396 -0
- ncca/ngl/webgpu/wip/pipeline_config.py +463 -0
- ncca/ngl/webgpu/wip/shader_constants.py +328 -0
- ncca/ngl/webgpu/wip/shader_templates.py +563 -0
- ncca/ngl/webgpu/wip/unified_examples.py +390 -0
- ncca/ngl/webgpu/wip/unified_factory.py +449 -0
- ncca/ngl/webgpu/wip/unified_pipeline.py +469 -0
- ncca/ngl/widgets/__init__.py +18 -2
- ncca/ngl/widgets/__main__.py +2 -1
- ncca/ngl/widgets/lookatwidget.py +2 -1
- ncca/ngl/widgets/mat4widget.py +2 -2
- ncca/ngl/widgets/vec2widget.py +1 -1
- ncca/ngl/widgets/vec3widget.py +1 -0
- {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/METADATA +3 -2
- ncca_ngl-0.5.0.dist-info/RECORD +105 -0
- ncca/ngl/widgets/transformation_widget.py +0 -299
- ncca_ngl-0.3.4.dist-info/RECORD +0 -82
- {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/WHEEL +0 -0
ncca/ngl/prim_data.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
from enum import Enum
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Union
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
6
5
|
|
|
7
6
|
from .vec3 import Vec3
|
|
8
7
|
|
|
8
|
+
RAD_POS = "Radius must be positive"
|
|
9
|
+
NON_NEG = "Height must be non-negative"
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class Prims(Enum):
|
|
11
13
|
"""Enum for the default primitives that can be loaded."""
|
|
12
14
|
|
|
13
15
|
BUDDHA = "buddah"
|
|
@@ -346,64 +348,43 @@ class PrimData:
|
|
|
346
348
|
return np.array(data, dtype=np.float32)
|
|
347
349
|
|
|
348
350
|
@staticmethod
|
|
349
|
-
def
|
|
350
|
-
"""
|
|
351
|
-
Creates a capsule primitive.
|
|
352
|
-
The capsule is aligned along the y-axis.
|
|
353
|
-
It is composed of a cylinder and two hemispherical caps.
|
|
354
|
-
based on code from here https://code.google.com/p/rgine/source/browse/trunk/RGine/opengl/src/RGLShapes.cpp
|
|
355
|
-
and adapted
|
|
356
|
-
"""
|
|
357
|
-
if radius <= 0.0:
|
|
358
|
-
raise ValueError("Radius must be positive")
|
|
359
|
-
if height < 0.0:
|
|
360
|
-
raise ValueError("Height must be non-negative")
|
|
361
|
-
if precision < 4:
|
|
362
|
-
precision = 4
|
|
363
|
-
|
|
364
|
-
data = []
|
|
365
|
-
h = height / 2.0
|
|
366
|
-
ang = np.pi / precision
|
|
367
|
-
|
|
368
|
-
# Cylinder sides
|
|
351
|
+
def _add_cylinder_sides(data: list, radius: float, h: float, ang: float, precision: int):
|
|
352
|
+
"""Generates cylinder side geometry."""
|
|
369
353
|
for i in range(2 * precision):
|
|
370
354
|
c = radius * np.cos(ang * i)
|
|
371
355
|
c1 = radius * np.cos(ang * (i + 1))
|
|
372
356
|
s = radius * np.sin(ang * i)
|
|
373
357
|
s1 = radius * np.sin(ang * (i + 1))
|
|
374
|
-
|
|
375
358
|
# normals for cylinder sides
|
|
376
359
|
nc = np.cos(ang * i)
|
|
377
360
|
ns = np.sin(ang * i)
|
|
378
361
|
nc1 = np.cos(ang * (i + 1))
|
|
379
362
|
ns1 = np.sin(ang * (i + 1))
|
|
380
|
-
|
|
381
363
|
# side top
|
|
382
364
|
data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
|
|
383
365
|
data.extend([c, h, s, nc, 0.0, ns, 0.0, 0.0])
|
|
384
366
|
data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
|
|
385
|
-
|
|
386
367
|
# side bot
|
|
387
368
|
data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
|
|
388
369
|
data.extend([c1, -h, s1, nc1, 0.0, ns1, 0.0, 0.0])
|
|
389
370
|
data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
|
|
390
|
-
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def _add_hemispherical_caps(data: list, radius: float, h: float, ang: float, precision: int):
|
|
374
|
+
"""Generates hemispherical cap geometry."""
|
|
391
375
|
for i in range(2 * precision):
|
|
392
376
|
# longitude
|
|
393
377
|
s = -np.sin(ang * i)
|
|
394
378
|
s1 = -np.sin(ang * (i + 1))
|
|
395
379
|
c = np.cos(ang * i)
|
|
396
380
|
c1 = np.cos(ang * (i + 1))
|
|
397
|
-
|
|
398
381
|
for j in range(precision + 1):
|
|
399
382
|
o = h if j < precision / 2 else -h
|
|
400
|
-
|
|
401
383
|
# latitude
|
|
402
384
|
sb = radius * np.sin(ang * j)
|
|
403
385
|
sb1 = radius * np.sin(ang * (j + 1))
|
|
404
386
|
cb = radius * np.cos(ang * j)
|
|
405
387
|
cb1 = radius * np.cos(ang * (j + 1))
|
|
406
|
-
|
|
407
388
|
if j != precision - 1:
|
|
408
389
|
nx, ny, nz = sb * c, cb, sb * s
|
|
409
390
|
data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
|
|
@@ -411,7 +392,6 @@ class PrimData:
|
|
|
411
392
|
data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
|
|
412
393
|
nx, ny, nz = sb1 * c1, cb1, sb1 * s1
|
|
413
394
|
data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
|
|
414
|
-
|
|
415
395
|
if j != 0:
|
|
416
396
|
nx, ny, nz = sb * c, cb, sb * s
|
|
417
397
|
data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
|
|
@@ -420,6 +400,32 @@ class PrimData:
|
|
|
420
400
|
nx, ny, nz = sb * c1, cb, sb * s1
|
|
421
401
|
data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
|
|
422
402
|
|
|
403
|
+
@staticmethod
|
|
404
|
+
def capsule(radius: float, height: float, precision: int) -> np.ndarray:
|
|
405
|
+
"""
|
|
406
|
+
Creates a capsule primitive.
|
|
407
|
+
The capsule is aligned along the y-axis.
|
|
408
|
+
It is composed of a cylinder and two hemispherical caps.
|
|
409
|
+
based on code from here https://code.google.com/p/rgine/source/browse/trunk/RGine/opengl/src/RGLShapes.cpp
|
|
410
|
+
and adapted
|
|
411
|
+
"""
|
|
412
|
+
if radius <= 0.0:
|
|
413
|
+
raise ValueError(RAD_POS)
|
|
414
|
+
if height < 0.0:
|
|
415
|
+
raise ValueError(NON_NEG)
|
|
416
|
+
if precision < 4:
|
|
417
|
+
precision = 4
|
|
418
|
+
|
|
419
|
+
data = []
|
|
420
|
+
h = height / 2.0
|
|
421
|
+
ang = np.pi / precision
|
|
422
|
+
|
|
423
|
+
# Cylinder sides
|
|
424
|
+
PrimData._add_cylinder_sides(data, radius, h, ang, precision)
|
|
425
|
+
|
|
426
|
+
# Hemispherical caps
|
|
427
|
+
PrimData._add_hemispherical_caps(data, radius, h, ang, precision)
|
|
428
|
+
|
|
423
429
|
return np.array(data, dtype=np.float32)
|
|
424
430
|
|
|
425
431
|
@staticmethod
|
|
@@ -430,9 +436,9 @@ class PrimData:
|
|
|
430
436
|
This method generates the cylinder walls, but not the top and bottom caps.
|
|
431
437
|
"""
|
|
432
438
|
if radius <= 0.0:
|
|
433
|
-
raise ValueError(
|
|
439
|
+
raise ValueError(RAD_POS)
|
|
434
440
|
if height < 0.0:
|
|
435
|
-
raise ValueError(
|
|
441
|
+
raise ValueError(NON_NEG)
|
|
436
442
|
if slices < 3:
|
|
437
443
|
slices = 3
|
|
438
444
|
if stacks < 1:
|
|
@@ -486,7 +492,7 @@ class PrimData:
|
|
|
486
492
|
slices: The number of slices to divide the disk into.
|
|
487
493
|
"""
|
|
488
494
|
if radius <= 0.0:
|
|
489
|
-
raise ValueError(
|
|
495
|
+
raise ValueError(RAD_POS)
|
|
490
496
|
if slices < 3:
|
|
491
497
|
slices = 3
|
|
492
498
|
|
|
@@ -540,7 +546,7 @@ class PrimData:
|
|
|
540
546
|
rings: The number of rings for the torus.
|
|
541
547
|
"""
|
|
542
548
|
if minor_radius <= 0 or major_radius <= 0:
|
|
543
|
-
raise ValueError(
|
|
549
|
+
raise ValueError(RAD_POS)
|
|
544
550
|
if sides < 3 or rings < 3:
|
|
545
551
|
raise ValueError("Sides and rings must be at least 3")
|
|
546
552
|
|
|
@@ -602,7 +608,7 @@ class PrimData:
|
|
|
602
608
|
return np.array(data, dtype=np.float32)
|
|
603
609
|
|
|
604
610
|
@staticmethod
|
|
605
|
-
def primitive(name:
|
|
611
|
+
def primitive(name: str | Enum) -> np.ndarray:
|
|
606
612
|
prim_folder = Path(__file__).parent / "PrimData"
|
|
607
613
|
prims = np.load(prim_folder / "Primitives.npz")
|
|
608
614
|
if isinstance(name, Prims):
|
ncca/ngl/primitives.py
CHANGED
|
@@ -6,7 +6,7 @@ We need to create the data first which is stored in a map as part of the class,
|
|
|
6
6
|
which will generate a pipeline for this object and draw into the current context.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Dict
|
|
9
|
+
from typing import Dict
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import OpenGL.GL as gl
|
|
@@ -104,7 +104,7 @@ class Primitives:
|
|
|
104
104
|
cls._loaded = True
|
|
105
105
|
|
|
106
106
|
@classmethod
|
|
107
|
-
def draw(cls, name:
|
|
107
|
+
def draw(cls, name: str | Prims) -> None:
|
|
108
108
|
"""
|
|
109
109
|
Draws the specified primitive.
|
|
110
110
|
|
|
@@ -22,22 +22,6 @@ from PySide6.QtCore import Qt
|
|
|
22
22
|
from .vec3 import Vec3
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class EventHandlingTarget(Protocol):
|
|
26
|
-
"""
|
|
27
|
-
Protocol defining the interface that classes using EventHandlingMixin must implement.
|
|
28
|
-
|
|
29
|
-
This ensures that the mixin has access to the necessary methods and attributes.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def update(self) -> None:
|
|
33
|
-
"""Trigger a redraw of the window."""
|
|
34
|
-
...
|
|
35
|
-
|
|
36
|
-
def close(self) -> None:
|
|
37
|
-
"""Close the window."""
|
|
38
|
-
...
|
|
39
|
-
|
|
40
|
-
|
|
41
25
|
class PySideEventHandlingMixin:
|
|
42
26
|
"""
|
|
43
27
|
Mixin class providing standard event handling for PyNGL applications.
|
|
@@ -96,32 +80,12 @@ class PySideEventHandlingMixin:
|
|
|
96
80
|
self.INCREMENT = self.translation_sensitivity
|
|
97
81
|
self.ZOOM = self.zoom_sensitivity
|
|
98
82
|
|
|
99
|
-
# def sync_legacy_attributes(self) -> None:
|
|
100
|
-
# """
|
|
101
|
-
# Synchronize legacy attribute names with new ones.
|
|
102
|
-
# Call this if you modify the legacy attributes directly.
|
|
103
|
-
# """
|
|
104
|
-
# self.spin_x_face = self.spinXFace
|
|
105
|
-
# self.spin_y_face = self.spinYFace
|
|
106
|
-
# self.model_position = self.modelPos
|
|
107
|
-
# self.original_x_rotation = self.origX
|
|
108
|
-
# self.original_y_rotation = self.origY
|
|
109
|
-
# self.original_x_pos = self.origXPos
|
|
110
|
-
# self.original_y_pos = self.origYPos
|
|
111
|
-
# self.translation_sensitivity = self.INCREMENT
|
|
112
|
-
# self.zoom_sensitivity = self.ZOOM
|
|
113
|
-
|
|
114
83
|
def reset_camera(self) -> None:
|
|
115
84
|
"""Reset camera rotation and model position to defaults."""
|
|
116
85
|
self.spin_x_face = 0
|
|
117
86
|
self.spin_y_face = 0
|
|
118
87
|
self.model_position.set(0, 0, 0)
|
|
119
88
|
|
|
120
|
-
# # Sync legacy attributes
|
|
121
|
-
# self.spinXFace = 0
|
|
122
|
-
# self.spinYFace = 0
|
|
123
|
-
# self.modelPos.set(0, 0, 0)
|
|
124
|
-
|
|
125
89
|
def keyPressEvent(self, event) -> None:
|
|
126
90
|
"""
|
|
127
91
|
Handle keyboard press events with common shortcuts.
|
|
@@ -175,12 +139,6 @@ class PySideEventHandlingMixin:
|
|
|
175
139
|
self.original_x_rotation = position.x()
|
|
176
140
|
self.original_y_rotation = position.y()
|
|
177
141
|
|
|
178
|
-
# # Sync legacy attributes
|
|
179
|
-
# self.spinXFace = self.spin_x_face
|
|
180
|
-
# self.spinYFace = self.spin_y_face
|
|
181
|
-
# self.origX = self.original_x_rotation
|
|
182
|
-
# self.origY = self.original_y_rotation
|
|
183
|
-
|
|
184
142
|
self.update()
|
|
185
143
|
|
|
186
144
|
# Handle translation with right mouse button
|
|
@@ -194,11 +152,6 @@ class PySideEventHandlingMixin:
|
|
|
194
152
|
self.model_position.x += self.translation_sensitivity * diff_x
|
|
195
153
|
self.model_position.y -= self.translation_sensitivity * diff_y
|
|
196
154
|
|
|
197
|
-
# # Sync legacy attributes
|
|
198
|
-
# self.origXPos = self.original_x_pos
|
|
199
|
-
# self.origYPos = self.original_y_pos
|
|
200
|
-
# self.modelPos = self.model_position
|
|
201
|
-
|
|
202
155
|
self.update()
|
|
203
156
|
|
|
204
157
|
def mousePressEvent(self, event) -> None:
|
|
@@ -218,19 +171,11 @@ class PySideEventHandlingMixin:
|
|
|
218
171
|
self.original_y_rotation = position.y()
|
|
219
172
|
self.rotate = True
|
|
220
173
|
|
|
221
|
-
# # Sync legacy attributes
|
|
222
|
-
# self.origX = self.original_x_rotation
|
|
223
|
-
# self.origY = self.original_y_rotation
|
|
224
|
-
|
|
225
174
|
elif event.button() == Qt.RightButton:
|
|
226
175
|
self.original_x_pos = position.x()
|
|
227
176
|
self.original_y_pos = position.y()
|
|
228
177
|
self.translate = True
|
|
229
178
|
|
|
230
|
-
# # Sync legacy attributes
|
|
231
|
-
# self.origXPos = self.original_x_pos
|
|
232
|
-
# self.origYPos = self.original_y_pos
|
|
233
|
-
|
|
234
179
|
def mouseReleaseEvent(self, event) -> None:
|
|
235
180
|
"""
|
|
236
181
|
Handle mouse button release events to stop rotation or translation.
|
|
@@ -262,57 +207,4 @@ class PySideEventHandlingMixin:
|
|
|
262
207
|
elif delta < 0:
|
|
263
208
|
self.model_position.z -= self.zoom_sensitivity
|
|
264
209
|
|
|
265
|
-
# # Sync legacy attributes
|
|
266
|
-
# self.modelPos = self.model_position
|
|
267
|
-
|
|
268
210
|
self.update()
|
|
269
|
-
|
|
270
|
-
def get_camera_state(self) -> dict:
|
|
271
|
-
"""
|
|
272
|
-
Get the current camera state for serialization or debugging.
|
|
273
|
-
|
|
274
|
-
Returns:
|
|
275
|
-
Dictionary containing current camera state
|
|
276
|
-
"""
|
|
277
|
-
return {
|
|
278
|
-
"spin_x_face": self.spin_x_face,
|
|
279
|
-
"spin_y_face": self.spin_y_face,
|
|
280
|
-
"model_position": [
|
|
281
|
-
self.model_position.x,
|
|
282
|
-
self.model_position.y,
|
|
283
|
-
self.model_position.z,
|
|
284
|
-
],
|
|
285
|
-
"rotation_sensitivity": self.rotation_sensitivity,
|
|
286
|
-
"translation_sensitivity": self.translation_sensitivity,
|
|
287
|
-
"zoom_sensitivity": self.zoom_sensitivity,
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
def set_camera_state(self, state: dict) -> None:
|
|
291
|
-
"""
|
|
292
|
-
Restore camera state from a dictionary.
|
|
293
|
-
|
|
294
|
-
Args:
|
|
295
|
-
state: Dictionary containing camera state (from get_camera_state())
|
|
296
|
-
"""
|
|
297
|
-
self.spin_x_face = state.get("spin_x_face", 0)
|
|
298
|
-
self.spin_y_face = state.get("spin_y_face", 0)
|
|
299
|
-
|
|
300
|
-
pos = state.get("model_position", [0, 0, 0])
|
|
301
|
-
# Handle cases where pos might have fewer than 3 elements
|
|
302
|
-
x = pos[0] if len(pos) > 0 else 0
|
|
303
|
-
y = pos[1] if len(pos) > 1 else 0
|
|
304
|
-
z = pos[2] if len(pos) > 2 else 0
|
|
305
|
-
self.model_position.set(x, y, z)
|
|
306
|
-
|
|
307
|
-
self.rotation_sensitivity = state.get(
|
|
308
|
-
"rotation_sensitivity", self.DEFAULT_ROTATION_SENSITIVITY
|
|
309
|
-
)
|
|
310
|
-
self.translation_sensitivity = state.get(
|
|
311
|
-
"translation_sensitivity", self.DEFAULT_TRANSLATION_SENSITIVITY
|
|
312
|
-
)
|
|
313
|
-
self.zoom_sensitivity = state.get(
|
|
314
|
-
"zoom_sensitivity", self.DEFAULT_ZOOM_SENSITIVITY
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# # Sync legacy attributes
|
|
318
|
-
# self.sync_legacy_attributes()
|
ncca/ngl/quaternion.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
A simple Quaternion class for use in NCCA Python
|
|
3
|
+
NumPy-based implementation for efficient operations
|
|
3
4
|
Attributes:
|
|
4
5
|
s (float): The scalar part of the quaternion.
|
|
5
6
|
x (float): The x-coordinate of the vector part of the quaternion.
|
|
@@ -9,12 +10,14 @@ Attributes:
|
|
|
9
10
|
|
|
10
11
|
import math
|
|
11
12
|
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
12
15
|
from .mat4 import Mat4
|
|
13
16
|
from .vec3 import Vec3
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class Quaternion:
|
|
17
|
-
__slots__ = ["
|
|
20
|
+
__slots__ = ["_data"] # Store as [s, x, y, z]
|
|
18
21
|
|
|
19
22
|
def __init__(self, s: float = 1.0, x: float = 0, y: float = 0, z: float = 0):
|
|
20
23
|
"""
|
|
@@ -25,12 +28,8 @@ class Quaternion:
|
|
|
25
28
|
x (float): The x-coordinate of the vector part of the quaternion.
|
|
26
29
|
y (float): The y-coordinate of the vector part of the quaternion.
|
|
27
30
|
z (float): The z-coordinate of the vector part of the quaternion.
|
|
28
|
-
|
|
29
31
|
"""
|
|
30
|
-
self.
|
|
31
|
-
self.x = float(x)
|
|
32
|
-
self.y = float(y)
|
|
33
|
-
self.z = float(z)
|
|
32
|
+
self._data = np.array([float(s), float(x), float(y), float(z)], dtype=np.float64)
|
|
34
33
|
|
|
35
34
|
@staticmethod
|
|
36
35
|
def from_mat4(mat: "Mat4") -> "Quaternion":
|
|
@@ -42,7 +41,6 @@ class Quaternion:
|
|
|
42
41
|
|
|
43
42
|
Returns:
|
|
44
43
|
Quaternion: A new Quaternion representing the rotation matrix.
|
|
45
|
-
|
|
46
44
|
"""
|
|
47
45
|
matrix = mat.get_matrix()
|
|
48
46
|
T = 1.0 + matrix[0] + matrix[5] + matrix[10]
|
|
@@ -58,14 +56,12 @@ class Quaternion:
|
|
|
58
56
|
y = (matrix[4] + matrix[1]) / scale
|
|
59
57
|
z = (matrix[2] + matrix[8]) / scale
|
|
60
58
|
s = (matrix[6] - matrix[9]) / scale
|
|
61
|
-
|
|
62
59
|
elif matrix[5] > matrix[10]:
|
|
63
60
|
scale = math.sqrt(1.0 + matrix[5] - matrix[0] - matrix[10]) * 2.0
|
|
64
61
|
x = (matrix[4] + matrix[1]) / scale
|
|
65
62
|
y = 0.25 * scale
|
|
66
63
|
z = (matrix[9] + matrix[6]) / scale
|
|
67
64
|
s = (matrix[8] - matrix[2]) / scale
|
|
68
|
-
|
|
69
65
|
else:
|
|
70
66
|
scale = math.sqrt(1.0 + matrix[10] - matrix[0] - matrix[5]) * 2.0
|
|
71
67
|
x = (matrix[8] + matrix[2]) / scale
|
|
@@ -97,38 +93,37 @@ class Quaternion:
|
|
|
97
93
|
return Quaternion(s, x, y, z)
|
|
98
94
|
|
|
99
95
|
def __add__(self, rhs):
|
|
100
|
-
|
|
96
|
+
result = Quaternion()
|
|
97
|
+
result._data = self._data + rhs._data
|
|
98
|
+
return result
|
|
101
99
|
|
|
102
100
|
def __iadd__(self, rhs):
|
|
103
|
-
self.
|
|
104
|
-
self.x += rhs.x
|
|
105
|
-
self.y += rhs.y
|
|
106
|
-
self.z += rhs.z
|
|
101
|
+
self._data += rhs._data
|
|
107
102
|
return self
|
|
108
103
|
|
|
109
104
|
def __sub__(self, rhs):
|
|
110
|
-
|
|
105
|
+
result = Quaternion()
|
|
106
|
+
result._data = self._data - rhs._data
|
|
107
|
+
return result
|
|
111
108
|
|
|
112
109
|
def __isub__(self, rhs):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# def __mul__(self, rhs):
|
|
116
|
-
# return Quaternion(
|
|
117
|
-
# self.s * rhs.s - self.x * rhs.x - self.y * rhs.y - self.z * rhs.z,
|
|
118
|
-
# self.s * rhs.x + self.x * rhs.s + self.y * rhs.z - self.z * rhs.y,
|
|
119
|
-
# self.s * rhs.y - self.x * rhs.z + self.y * rhs.s + self.z * rhs.x,
|
|
120
|
-
# self.s * rhs.z + self.x * rhs.y - self.y * rhs.x + self.z * rhs.s,
|
|
121
|
-
# )
|
|
110
|
+
self._data -= rhs._data
|
|
111
|
+
return self
|
|
122
112
|
|
|
123
113
|
def __mul__(self, rhs):
|
|
124
114
|
if isinstance(rhs, Quaternion):
|
|
115
|
+
# Quaternion multiplication
|
|
116
|
+
s1, x1, y1, z1 = self._data
|
|
117
|
+
s2, x2, y2, z2 = rhs._data
|
|
118
|
+
|
|
125
119
|
return Quaternion(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
s1 * s2 - x1 * x2 - y1 * y2 - z1 * z2,
|
|
121
|
+
s1 * x2 + x1 * s2 + y1 * z2 - z1 * y2,
|
|
122
|
+
s1 * y2 - x1 * z2 + y1 * s2 + z1 * x2,
|
|
123
|
+
s1 * z2 + x1 * y2 - y1 * x2 + z1 * s2,
|
|
130
124
|
)
|
|
131
125
|
elif isinstance(rhs, Vec3):
|
|
126
|
+
# Quaternion-vector multiplication (rotate vector by quaternion)
|
|
132
127
|
qw = self.s
|
|
133
128
|
qx = self.x
|
|
134
129
|
qy = self.y
|
|
@@ -138,13 +133,13 @@ class Quaternion:
|
|
|
138
133
|
vy = rhs.y
|
|
139
134
|
vz = rhs.z
|
|
140
135
|
|
|
141
|
-
# pq
|
|
136
|
+
# pq (quaternion * pure quaternion from vector)
|
|
142
137
|
pw = -qx * vx - qy * vy - qz * vz
|
|
143
138
|
px = qw * vx + qy * vz - qz * vy
|
|
144
139
|
py = qw * vy - qx * vz + qz * vx
|
|
145
140
|
pz = qw * vz + qx * vy - qy * vx
|
|
146
141
|
|
|
147
|
-
# pqp*
|
|
142
|
+
# pqp* (result * conjugate of quaternion)
|
|
148
143
|
return Vec3(
|
|
149
144
|
-pw * qx + px * qw - py * qz + pz * qy,
|
|
150
145
|
-pw * qy + px * qz + py * qw - pz * qx,
|
|
@@ -152,12 +147,25 @@ class Quaternion:
|
|
|
152
147
|
)
|
|
153
148
|
|
|
154
149
|
def normalize(self):
|
|
155
|
-
|
|
150
|
+
"""Normalize the quaternion to unit length"""
|
|
151
|
+
length = np.linalg.norm(self._data)
|
|
156
152
|
if length > 0:
|
|
157
|
-
self.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
self._data /= length
|
|
154
|
+
|
|
155
|
+
def length(self):
|
|
156
|
+
"""Return the length/magnitude of the quaternion"""
|
|
157
|
+
return np.linalg.norm(self._data)
|
|
158
|
+
|
|
159
|
+
def conjugate(self):
|
|
160
|
+
"""Return the conjugate of the quaternion (s, -x, -y, -z)"""
|
|
161
|
+
result = Quaternion()
|
|
162
|
+
result._data = self._data.copy()
|
|
163
|
+
result._data[1:] *= -1 # Negate x, y, z components
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def dot(self, rhs):
|
|
167
|
+
"""Dot product of two quaternions"""
|
|
168
|
+
return np.dot(self._data, rhs._data)
|
|
161
169
|
|
|
162
170
|
def __str__(self) -> str:
|
|
163
171
|
"""
|
|
@@ -165,9 +173,34 @@ class Quaternion:
|
|
|
165
173
|
|
|
166
174
|
Returns:
|
|
167
175
|
str: A string representation of the Quaternion.
|
|
168
|
-
|
|
169
176
|
"""
|
|
170
177
|
return f"Quaternion({self.s}, [{self.x}, {self.y}, {self.z}])"
|
|
171
178
|
|
|
172
179
|
def __repr__(self) -> str:
|
|
173
180
|
return f"Quaternion({self.s}, [{self.x}, {self.y}, {self.z}])"
|
|
181
|
+
|
|
182
|
+
def to_numpy(self):
|
|
183
|
+
"""Return the quaternion as a numpy array [s, x, y, z]"""
|
|
184
|
+
return self._data.copy()
|
|
185
|
+
|
|
186
|
+
def to_list(self):
|
|
187
|
+
"""Return the quaternion as a list [s, x, y, z]"""
|
|
188
|
+
return self._data.tolist()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Helper function to create properties
|
|
192
|
+
def _create_property(index):
|
|
193
|
+
def getter(self):
|
|
194
|
+
return self._data[index]
|
|
195
|
+
|
|
196
|
+
def setter(self, value):
|
|
197
|
+
if not isinstance(value, (int, float, np.float32)):
|
|
198
|
+
raise ValueError("need float or int")
|
|
199
|
+
self._data[index] = value
|
|
200
|
+
|
|
201
|
+
return property(getter, setter)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Dynamically add properties for s, x, y, z, w
|
|
205
|
+
for i, attr in enumerate(["s", "x", "y", "z"]):
|
|
206
|
+
setattr(Quaternion, attr, _create_property(i))
|
ncca/ngl/shader.py
CHANGED
|
@@ -111,119 +111,3 @@ class Shader:
|
|
|
111
111
|
"""
|
|
112
112
|
self._source = shader_source
|
|
113
113
|
gl.glShaderSource(self._id, self._source)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# class ShaderProgram:
|
|
117
|
-
# def __init__(self, name: str, exit_on_error: bool = True):
|
|
118
|
-
# self._name = name
|
|
119
|
-
# self._exit_on_error = exit_on_error
|
|
120
|
-
# self._id = gl.glCreateProgram()
|
|
121
|
-
# self._shaders = []
|
|
122
|
-
# self._uniforms = {}
|
|
123
|
-
|
|
124
|
-
# def attach_shader(self, shader: Shader):
|
|
125
|
-
# gl.glAttachShader(self._id, shader._id)
|
|
126
|
-
# self._shaders.append(shader)
|
|
127
|
-
|
|
128
|
-
# def link(self) -> bool:
|
|
129
|
-
# gl.glLinkProgram(self._id)
|
|
130
|
-
# if gl.glGetProgramiv(self._id, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
|
131
|
-
# info = gl.glGetProgramInfoLog(self._id)
|
|
132
|
-
# print(f"Error linking program {self._name}: {info}")
|
|
133
|
-
# if self._exit_on_error:
|
|
134
|
-
# exit()
|
|
135
|
-
# return False
|
|
136
|
-
# return True
|
|
137
|
-
|
|
138
|
-
# def use(self):
|
|
139
|
-
# gl.glUseProgram(self._id)
|
|
140
|
-
|
|
141
|
-
# def get_id(self) -> int:
|
|
142
|
-
# return self._id
|
|
143
|
-
|
|
144
|
-
# def get_uniform_location(self, name: str) -> int:
|
|
145
|
-
# if name not in self._uniforms:
|
|
146
|
-
# self._uniforms[name] = gl.glGetUniformLocation(self._id, name)
|
|
147
|
-
# return self._uniforms[name]
|
|
148
|
-
|
|
149
|
-
# def set_uniform(self, name: str, *value):
|
|
150
|
-
# loc = self.get_uniform_location(name)
|
|
151
|
-
# if loc != -1:
|
|
152
|
-
# if len(value) == 1:
|
|
153
|
-
# if isinstance(value[0], int):
|
|
154
|
-
# gl.glUniform1i(loc, value[0])
|
|
155
|
-
# elif isinstance(value[0], float):
|
|
156
|
-
# gl.glUniform1f(loc, value[0])
|
|
157
|
-
# else:
|
|
158
|
-
# try:
|
|
159
|
-
# val = list(value[0])
|
|
160
|
-
# if len(val) == 4:
|
|
161
|
-
# gl.glUniformMatrix2fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 4)(*val))
|
|
162
|
-
# elif len(val) == 9:
|
|
163
|
-
# gl.glUniformMatrix3fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 9)(*val))
|
|
164
|
-
# elif len(val) == 16:
|
|
165
|
-
# gl.glUniformMatrix4fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 16)(*val))
|
|
166
|
-
# except TypeError:
|
|
167
|
-
# pass
|
|
168
|
-
# elif len(value) == 2:
|
|
169
|
-
# gl.glUniform2f(loc, *value)
|
|
170
|
-
# elif len(value) == 3:
|
|
171
|
-
# gl.glUniform3f(loc, *value)
|
|
172
|
-
# elif len(value) == 4:
|
|
173
|
-
# gl.glUniform4f(loc, *value)
|
|
174
|
-
|
|
175
|
-
# def get_uniform_1f(self, name: str) -> float:
|
|
176
|
-
# loc = self.get_uniform_location(name)
|
|
177
|
-
# if loc != -1:
|
|
178
|
-
# result = (ctypes.c_float * 1)()
|
|
179
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
180
|
-
# return result[0]
|
|
181
|
-
# return 0.0
|
|
182
|
-
|
|
183
|
-
# def get_uniform_2f(self, name: str) -> list[float]:
|
|
184
|
-
# loc = self.get_uniform_location(name)
|
|
185
|
-
# if loc != -1:
|
|
186
|
-
# result = (ctypes.c_float * 2)()
|
|
187
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
188
|
-
# return list(result)
|
|
189
|
-
# return [0.0, 0.0]
|
|
190
|
-
|
|
191
|
-
# def get_uniform_3f(self, name: str) -> list[float]:
|
|
192
|
-
# loc = self.get_uniform_location(name)
|
|
193
|
-
# if loc != -1:
|
|
194
|
-
# result = (ctypes.c_float * 3)()
|
|
195
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
196
|
-
# return list(result)
|
|
197
|
-
# return [0.0, 0.0, 0.0]
|
|
198
|
-
|
|
199
|
-
# def get_uniform_4f(self, name: str) -> list[float]:
|
|
200
|
-
# loc = self.get_uniform_location(name)
|
|
201
|
-
# if loc != -1:
|
|
202
|
-
# result = (ctypes.c_float * 4)()
|
|
203
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
204
|
-
# return list(result)
|
|
205
|
-
# return [0.0, 0.0, 0.0, 0.0]
|
|
206
|
-
|
|
207
|
-
# def get_uniform_mat2(self, name: str) -> list[float]:
|
|
208
|
-
# loc = self.get_uniform_location(name)
|
|
209
|
-
# if loc != -1:
|
|
210
|
-
# result = (ctypes.c_float * 4)()
|
|
211
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
212
|
-
# return list(result)
|
|
213
|
-
# return [0.0] * 4
|
|
214
|
-
|
|
215
|
-
# def get_uniform_mat3(self, name: str) -> list[float]:
|
|
216
|
-
# loc = self.get_uniform_location(name)
|
|
217
|
-
# if loc != -1:
|
|
218
|
-
# result = (ctypes.c_float * 9)()
|
|
219
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
220
|
-
# return list(result)
|
|
221
|
-
# return [0.0] * 9
|
|
222
|
-
|
|
223
|
-
# def get_uniform_mat4(self, name: str) -> list[float]:
|
|
224
|
-
# loc = self.get_uniform_location(name)
|
|
225
|
-
# if loc != -1:
|
|
226
|
-
# result = (ctypes.c_float * 16)()
|
|
227
|
-
# gl.glGetUniformfv(self._id, loc, result)
|
|
228
|
-
# return list(result)
|
|
229
|
-
# return [0.0] * 16
|