ncca-ngl 0.1.6__py3-none-any.whl → 0.2.2__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/__init__.py CHANGED
@@ -25,7 +25,8 @@ from .obj import (
25
25
  ObjParseVertexError,
26
26
  )
27
27
  from .plane import Plane
28
- from .primitives import Primitives, Prims
28
+ from .prim_data import PrimData, Prims
29
+ from .primitives import Primitives
29
30
  from .pyside_event_handling_mixin import PySideEventHandlingMixin
30
31
  from .quaternion import Quaternion
31
32
  from .random import Random
@@ -103,6 +104,7 @@ all = [
103
104
  logger,
104
105
  Primitives,
105
106
  Prims,
107
+ PrimData,
106
108
  FirstPersonCamera,
107
109
  PySideEventHandlingMixin,
108
110
  ]
ncca/ngl/prim_data.py ADDED
@@ -0,0 +1,611 @@
1
+ import enum
2
+ from pathlib import Path
3
+ from typing import Union
4
+
5
+ import numpy as np
6
+
7
+ from .vec3 import Vec3
8
+
9
+
10
+ class Prims(enum.Enum):
11
+ """Enum for the default primitives that can be loaded."""
12
+
13
+ BUDDAH = "buddah"
14
+ BUNNY = "bunny"
15
+ CUBE = "cube"
16
+ DODECAHEDRON = "dodecahedron"
17
+ DRAGON = "dragon"
18
+ FOOTBALL = "football"
19
+ ICOSAHEDRON = "icosahedron"
20
+ OCTAHEDRON = "octahedron"
21
+ TEAPOT = "teapot"
22
+ TETRAHEDRON = "tetrahedron"
23
+ TROLL = "troll"
24
+ SPHERE = "sphere"
25
+ TORUS = "torus"
26
+ LINE_GRID = "line_grid"
27
+ TRIANGLE_PLANE = "triangle_plane"
28
+ CONE = "cone"
29
+ CAPSULE = "capsule"
30
+ CYLINDER = "cylinder"
31
+ DISK = "disk"
32
+
33
+
34
+ def _circle_table(n: int) -> np.ndarray:
35
+ """
36
+ Generates a table of sine and cosine values for a circle divided into n segments.
37
+
38
+ Args:
39
+ n: The number of segments to divide the circle into.
40
+
41
+ Returns:
42
+ A numpy array of shape (n+1, 2) containing the cosine and sine values.
43
+ """
44
+ # Determine the angle between samples
45
+ angle = 2.0 * np.pi / (n if n != 0 else 1)
46
+
47
+ # Allocate list for n samples, plus duplicate of first entry at the end
48
+ cs = np.zeros((n + 1, 2), dtype=np.float32)
49
+
50
+ # Compute cos and sin around the circle
51
+ cs[0, 0] = 1.0 # cost
52
+ cs[0, 1] = 0.0 # sint
53
+
54
+ for i in range(1, n):
55
+ cs[i, 1] = np.sin(angle * i) # sint
56
+ cs[i, 0] = np.cos(angle * i) # cost
57
+
58
+ # Last sample is duplicate of the first
59
+ cs[n, 1] = cs[0, 1] # sint
60
+ cs[n, 0] = cs[0, 0] # cost
61
+
62
+ return cs
63
+
64
+
65
+ class PrimData:
66
+ @staticmethod
67
+ def line_grid(width: float, depth: float, steps: int) -> np.ndarray:
68
+ """
69
+ Creates a line grid primitive.
70
+
71
+ Args:
72
+ width: The width of the grid.
73
+ depth: The depth of the grid.
74
+ steps: The number of steps in the grid.
75
+ """
76
+ # Calculate the step size for each grid value
77
+ wstep = width / steps
78
+ ws2 = width / 2.0
79
+ v1 = -ws2
80
+
81
+ dstep = depth / steps
82
+ ds2 = depth / 2.0
83
+ v2 = -ds2
84
+
85
+ # Create a list to store the vertex data
86
+ data = []
87
+
88
+ for _ in range(steps + 1):
89
+ # Vertex 1 x, y, z
90
+ data.append([-ws2, 0.0, v1])
91
+ # Vertex 2 x, y, z
92
+ data.append([ws2, 0.0, v1])
93
+
94
+ # Vertex 1 x, y, z
95
+ data.append([v2, 0.0, ds2])
96
+ # Vertex 2 x, y, z
97
+ data.append([v2, 0.0, -ds2])
98
+
99
+ # Now change our step value
100
+ v1 += wstep
101
+ v2 += dstep
102
+
103
+ # Convert the list to a NumPy array
104
+ return np.array(data, dtype=np.float32)
105
+
106
+ @staticmethod
107
+ def triangle_plane(width: float, depth: float, w_p: int, d_p: int, v_n: Vec3) -> np.ndarray:
108
+ """
109
+ Creates a triangle plane primitive.
110
+
111
+ Args:
112
+ width: The width of the plane.
113
+ depth: The depth of the plane.
114
+ w_p: The number of width partitions.
115
+ d_p: The number of depth partitions.
116
+ v_n: The normal vector for the plane.
117
+ """
118
+ w2 = width / 2.0
119
+ d2 = depth / 2.0
120
+ w_step = width / w_p
121
+ d_step = depth / d_p
122
+
123
+ du = 0.9 / w_p
124
+ dv = 0.9 / d_p
125
+
126
+ data = []
127
+ v = 0.0
128
+ d = -d2
129
+ for _ in range(d_p):
130
+ u = 0.0
131
+ w = -w2
132
+ for _ in range(w_p):
133
+ # tri 1
134
+ # vert 1
135
+ data.extend([w, 0.0, d + d_step, v_n.x, v_n.y, v_n.z, u, v + dv])
136
+ # vert 2
137
+ data.extend([w + w_step, 0.0, d + d_step, v_n.x, v_n.y, v_n.z, u + du, v + dv])
138
+ # vert 3
139
+ data.extend([w, 0.0, d, v_n.x, v_n.y, v_n.z, u, v])
140
+
141
+ # tri 2
142
+ # vert 1
143
+ data.extend([w + w_step, 0.0, d + d_step, v_n.x, v_n.y, v_n.z, u + du, v + dv])
144
+ # vert 2
145
+ data.extend([w + w_step, 0.0, d, v_n.x, v_n.y, v_n.z, u + du, v])
146
+ # vert 3
147
+ data.extend([w, 0.0, d, v_n.x, v_n.y, v_n.z, u, v])
148
+ u += du
149
+ w += w_step
150
+ v += dv
151
+ d += d_step
152
+
153
+ return np.array(data, dtype=np.float32)
154
+
155
+ @staticmethod
156
+ def sphere(radius: float, precision: int) -> np.ndarray:
157
+ """
158
+ Creates a sphere primitive.
159
+
160
+ Args:
161
+ radius: The radius of the sphere.
162
+ precision: The precision of the sphere (number of slices).
163
+ """
164
+ # Sphere code based on a function Written by Paul Bourke.
165
+ # http://astronomy.swin.edu.au/~pbourke/opengl/sphere/
166
+ # the next part of the code calculates the P,N,UV of the sphere for triangles
167
+
168
+ # Disallow a negative number for radius.
169
+ if radius < 0.0:
170
+ radius = -radius
171
+
172
+ # Disallow a negative number for precision.
173
+ if precision < 4:
174
+ precision = 4
175
+
176
+ # Create a numpy array to store our verts
177
+ data = []
178
+
179
+ for i in range(precision // 2):
180
+ theta1 = i * 2.0 * np.pi / precision - np.pi / 2.0
181
+ theta2 = (i + 1) * 2.0 * np.pi / precision - np.pi / 2.0
182
+
183
+ for j in range(precision):
184
+ theta3 = j * 2.0 * np.pi / precision
185
+ theta4 = (j + 1) * 2.0 * np.pi / precision
186
+
187
+ # First triangle
188
+ nx1 = np.cos(theta2) * np.cos(theta3)
189
+ ny1 = np.sin(theta2)
190
+ nz1 = np.cos(theta2) * np.sin(theta3)
191
+ x1 = radius * nx1
192
+ y1 = radius * ny1
193
+ z1 = radius * nz1
194
+ u1 = j / precision
195
+ v1 = 2.0 * (i + 1) / precision
196
+ data.append([x1, y1, z1, nx1, ny1, nz1, u1, v1])
197
+
198
+ nx2 = np.cos(theta1) * np.cos(theta3)
199
+ ny2 = np.sin(theta1)
200
+ nz2 = np.cos(theta1) * np.sin(theta3)
201
+ x2 = radius * nx2
202
+ y2 = radius * ny2
203
+ z2 = radius * nz2
204
+ u2 = j / precision
205
+ v2 = 2.0 * i / precision
206
+ data.append([x2, y2, z2, nx2, ny2, nz2, u2, v2])
207
+
208
+ nx3 = np.cos(theta1) * np.cos(theta4)
209
+ ny3 = np.sin(theta1)
210
+ nz3 = np.cos(theta1) * np.sin(theta4)
211
+ x3 = radius * nx3
212
+ y3 = radius * ny3
213
+ z3 = radius * nz3
214
+ u3 = (j + 1) / precision
215
+ v3 = 2.0 * i / precision
216
+ data.append([x3, y3, z3, nx3, ny3, nz3, u3, v3])
217
+
218
+ # Second triangle
219
+ nx4 = np.cos(theta2) * np.cos(theta4)
220
+ ny4 = np.sin(theta2)
221
+ nz4 = np.cos(theta2) * np.sin(theta4)
222
+ x4 = radius * nx4
223
+ y4 = radius * ny4
224
+ z4 = radius * nz4
225
+ u4 = (j + 1) / precision
226
+ v4 = 2.0 * (i + 1) / precision
227
+ data.append([x4, y4, z4, nx4, ny4, nz4, u4, v4])
228
+
229
+ data.append([x1, y1, z1, nx1, ny1, nz1, u1, v1])
230
+ data.append([x3, y3, z3, nx3, ny3, nz3, u3, v3])
231
+
232
+ return np.array(data, dtype=np.float32)
233
+
234
+ @staticmethod
235
+ def cone(base: float, height: float, slices: int, stacks: int) -> np.ndarray:
236
+ """
237
+ Creates a cone primitive.
238
+
239
+ Args:
240
+ base: The radius of the cone's base.
241
+ height: The height of the cone.
242
+ slices: The number of divisions around the cone.
243
+ stacks: The number of divisions along the cone's height.
244
+ """
245
+ z_step = height / (stacks if stacks > 0 else 1)
246
+ r_step = base / (stacks if stacks > 0 else 1)
247
+
248
+ cosn = height / np.sqrt(height * height + base * base)
249
+ sinn = base / np.sqrt(height * height + base * base)
250
+
251
+ cs = _circle_table(slices)
252
+
253
+ z0 = 0.0
254
+ z1 = z_step
255
+
256
+ r0 = base
257
+ r1 = r0 - r_step
258
+
259
+ du = 1.0 / stacks
260
+ dv = 1.0 / slices
261
+
262
+ u = 1.0
263
+ v = 1.0
264
+
265
+ data = []
266
+
267
+ for _ in range(stacks):
268
+ for j in range(slices):
269
+ # First triangle
270
+ d1 = [0] * 8
271
+ d1[6] = u
272
+ d1[7] = v
273
+ d1[3] = cs[j, 0] * cosn # nx
274
+ d1[4] = cs[j, 1] * sinn # ny
275
+ d1[5] = sinn # nz
276
+ d1[0] = cs[j, 0] * r0 # x
277
+ d1[1] = cs[j, 1] * r0 # y
278
+ d1[2] = z0 # z
279
+ data.append(d1)
280
+
281
+ d2 = [0] * 8
282
+ d2[6] = u
283
+ d2[7] = v - dv
284
+ d2[3] = cs[j, 0] * cosn # nx
285
+ d2[4] = cs[j, 1] * sinn # ny
286
+ d2[5] = sinn # nz
287
+ d2[0] = cs[j, 0] * r1 # x
288
+ d2[1] = cs[j, 1] * r1 # y
289
+ d2[2] = z1 # z
290
+ data.append(d2)
291
+
292
+ d3 = [0] * 8
293
+ d3[6] = u - du
294
+ d3[7] = v - dv
295
+ d3[3] = cs[j + 1, 0] * cosn # nx
296
+ d3[4] = cs[j + 1, 1] * sinn # ny
297
+ d3[5] = sinn # nz
298
+ d3[0] = cs[j + 1, 0] * r1 # x
299
+ d3[1] = cs[j + 1, 1] * r1 # y
300
+ d3[2] = z1 # z
301
+ data.append(d3)
302
+
303
+ # Second triangle
304
+ d4 = [0] * 8
305
+ d4[6] = u
306
+ d4[7] = v
307
+ d4[3] = cs[j, 0] * cosn # nx
308
+ d4[4] = cs[j, 1] * sinn # ny
309
+ d4[5] = sinn # nz
310
+ d4[0] = cs[j, 0] * r0 # x
311
+ d4[1] = cs[j, 1] * r0 # y
312
+ d4[2] = z0 # z
313
+ data.append(d4)
314
+
315
+ d5 = [0] * 8
316
+ d5[6] = u - du
317
+ d5[7] = v - dv
318
+ d5[3] = cs[j + 1, 0] * cosn # nx
319
+ d5[4] = cs[j + 1, 1] * sinn # ny
320
+ d5[5] = sinn # nz
321
+ d5[0] = cs[j + 1, 0] * r1 # x
322
+ d5[1] = cs[j + 1, 1] * r1 # y
323
+ d5[2] = z1 # z
324
+ data.append(d5)
325
+
326
+ d6 = [0] * 8
327
+ d6[6] = u - du
328
+ d6[7] = v
329
+ d6[3] = cs[j + 1, 0] * cosn # nx
330
+ d6[4] = cs[j + 1, 1] * sinn # ny
331
+ d6[5] = sinn # nz
332
+ d6[0] = cs[j + 1, 0] * r0 # x
333
+ d6[1] = cs[j + 1, 1] * r0 # y
334
+ d6[2] = z0 # z
335
+ data.append(d6)
336
+
337
+ u -= du
338
+
339
+ v -= dv
340
+ u = 1.0
341
+ z0 = z1
342
+ z1 += z_step
343
+ r0 = r1
344
+ r1 -= r_step
345
+
346
+ return np.array(data, dtype=np.float32)
347
+
348
+ @staticmethod
349
+ def capsule(radius: float, height: float, precision: int) -> np.ndarray:
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
369
+ for i in range(2 * precision):
370
+ c = radius * np.cos(ang * i)
371
+ c1 = radius * np.cos(ang * (i + 1))
372
+ s = radius * np.sin(ang * i)
373
+ s1 = radius * np.sin(ang * (i + 1))
374
+
375
+ # normals for cylinder sides
376
+ nc = np.cos(ang * i)
377
+ ns = np.sin(ang * i)
378
+ nc1 = np.cos(ang * (i + 1))
379
+ ns1 = np.sin(ang * (i + 1))
380
+
381
+ # side top
382
+ data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
383
+ data.extend([c, h, s, nc, 0.0, ns, 0.0, 0.0])
384
+ data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
385
+
386
+ # side bot
387
+ data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
388
+ data.extend([c1, -h, s1, nc1, 0.0, ns1, 0.0, 0.0])
389
+ data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
390
+ # Hemispherical caps
391
+ for i in range(2 * precision):
392
+ # longitude
393
+ s = -np.sin(ang * i)
394
+ s1 = -np.sin(ang * (i + 1))
395
+ c = np.cos(ang * i)
396
+ c1 = np.cos(ang * (i + 1))
397
+
398
+ for j in range(precision + 1):
399
+ o = h if j < precision / 2 else -h
400
+
401
+ # latitude
402
+ sb = radius * np.sin(ang * j)
403
+ sb1 = radius * np.sin(ang * (j + 1))
404
+ cb = radius * np.cos(ang * j)
405
+ cb1 = radius * np.cos(ang * (j + 1))
406
+
407
+ if j != precision - 1:
408
+ nx, ny, nz = sb * c, cb, sb * s
409
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
410
+ nx, ny, nz = sb1 * c, cb1, sb1 * s
411
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
412
+ nx, ny, nz = sb1 * c1, cb1, sb1 * s1
413
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
414
+
415
+ if j != 0:
416
+ nx, ny, nz = sb * c, cb, sb * s
417
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
418
+ nx, ny, nz = sb1 * c1, cb1, sb1 * s1
419
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
420
+ nx, ny, nz = sb * c1, cb, sb * s1
421
+ data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
422
+
423
+ return np.array(data, dtype=np.float32)
424
+
425
+ @staticmethod
426
+ def cylinder(radius: float, height: float, slices: int, stacks: int) -> np.ndarray:
427
+ """
428
+ Creates a cylinder primitive.
429
+ The cylinder is aligned along the y-axis.
430
+ This method generates the cylinder walls, but not the top and bottom caps.
431
+ """
432
+ if radius <= 0.0:
433
+ raise ValueError("Radius must be positive")
434
+ if height < 0.0:
435
+ raise ValueError("Height must be non-negative")
436
+ if slices < 3:
437
+ slices = 3
438
+ if stacks < 1:
439
+ stacks = 1
440
+
441
+ data = []
442
+ h2 = height / 2.0
443
+ y_step = height / stacks
444
+
445
+ cs = _circle_table(slices)
446
+
447
+ du = 1.0 / slices
448
+ dv = 1.0 / stacks
449
+
450
+ for i in range(stacks):
451
+ y0 = -h2 + i * y_step
452
+ y1 = -h2 + (i + 1) * y_step
453
+ v = i * dv
454
+ for j in range(slices):
455
+ u = j * du
456
+
457
+ nx1, nz1 = cs[j, 0], cs[j, 1]
458
+ x1, z1 = radius * nx1, radius * nz1
459
+
460
+ nx2, nz2 = cs[j + 1, 0], cs[j + 1, 1]
461
+ x2, z2 = radius * nx2, radius * nz2
462
+
463
+ p_bl = [x1, y0, z1, nx1, 0, nz1, u, v]
464
+ p_br = [x2, y0, z2, nx2, 0, nz2, u + du, v]
465
+ p_tl = [x1, y1, z1, nx1, 0, nz1, u, v + dv]
466
+ p_tr = [x2, y1, z2, nx2, 0, nz2, u + du, v + dv]
467
+
468
+ # Triangle 1
469
+ data.extend(p_bl)
470
+ data.extend(p_tl)
471
+ data.extend(p_br)
472
+ # Triangle 2
473
+ data.extend(p_br)
474
+ data.extend(p_tl)
475
+ data.extend(p_tr)
476
+
477
+ return np.array(data, dtype=np.float32)
478
+
479
+ @staticmethod
480
+ def disk(radius: float, slices: int) -> np.ndarray:
481
+ """
482
+ Creates a disk primitive.
483
+
484
+ Args:
485
+ radius: The radius of the disk.
486
+ slices: The number of slices to divide the disk into.
487
+ """
488
+ if radius <= 0.0:
489
+ raise ValueError("Radius must be positive")
490
+ if slices < 3:
491
+ slices = 3
492
+
493
+ data = []
494
+ cs = _circle_table(slices)
495
+
496
+ center = [0, 0, 0, 0, 1, 0, 0.5, 0.5]
497
+
498
+ for i in range(slices):
499
+ p1 = [
500
+ radius * cs[i, 0],
501
+ 0,
502
+ radius * cs[i, 1],
503
+ 0,
504
+ 1,
505
+ 0,
506
+ cs[i, 0] * 0.5 + 0.5,
507
+ cs[i, 1] * 0.5 + 0.5,
508
+ ]
509
+ p2 = [
510
+ radius * cs[i + 1, 0],
511
+ 0,
512
+ radius * cs[i + 1, 1],
513
+ 0,
514
+ 1,
515
+ 0,
516
+ cs[i + 1, 0] * 0.5 + 0.5,
517
+ cs[i + 1, 1] * 0.5 + 0.5,
518
+ ]
519
+
520
+ data.extend(center)
521
+ data.extend(p2)
522
+ data.extend(p1)
523
+
524
+ return np.array(data, dtype=np.float32)
525
+
526
+ @staticmethod
527
+ def torus(
528
+ minor_radius: float,
529
+ major_radius: float,
530
+ sides: int,
531
+ rings: int,
532
+ ) -> np.ndarray:
533
+ """
534
+ Creates a torus primitive.
535
+
536
+ Args:
537
+ minor_radius: The minor radius of the torus.
538
+ major_radius: The major radius of the torus.
539
+ sides: The number of sides for each ring.
540
+ rings: The number of rings for the torus.
541
+ """
542
+ if minor_radius <= 0 or major_radius <= 0:
543
+ raise ValueError("Radii must be positive")
544
+ if sides < 3 or rings < 3:
545
+ raise ValueError("Sides and rings must be at least 3")
546
+
547
+ d_psi = 2.0 * np.pi / rings
548
+ d_phi = -2.0 * np.pi / sides
549
+
550
+ psi = 0.0
551
+
552
+ vertices = []
553
+ normals = []
554
+ uvs = []
555
+
556
+ for j in range(rings + 1):
557
+ c_psi = np.cos(psi)
558
+ s_psi = np.sin(psi)
559
+ phi = 0.0
560
+ for i in range(sides + 1):
561
+ c_phi = np.cos(phi)
562
+ s_phi = np.sin(phi)
563
+
564
+ x = c_psi * (major_radius + c_phi * minor_radius)
565
+ z = s_psi * (major_radius + c_phi * minor_radius)
566
+ y = s_phi * minor_radius
567
+ vertices.append([x, y, z])
568
+
569
+ nx = c_psi * c_phi
570
+ nz = s_psi * c_phi
571
+ ny = s_phi
572
+ normals.append([nx, ny, nz])
573
+
574
+ u = i / sides
575
+ v = j / rings
576
+ uvs.append([u, v])
577
+
578
+ phi += d_phi
579
+ psi += d_psi
580
+
581
+ data = []
582
+ for j in range(rings):
583
+ for i in range(sides):
584
+ idx1 = j * (sides + 1) + i
585
+ idx2 = j * (sides + 1) + (i + 1)
586
+ idx3 = (j + 1) * (sides + 1) + i
587
+ idx4 = (j + 1) * (sides + 1) + (i + 1)
588
+
589
+ p1 = vertices[idx1] + normals[idx1] + uvs[idx1]
590
+ p2 = vertices[idx2] + normals[idx2] + uvs[idx2]
591
+ p3 = vertices[idx3] + normals[idx3] + uvs[idx3]
592
+ p4 = vertices[idx4] + normals[idx4] + uvs[idx4]
593
+
594
+ data.extend(p1)
595
+ data.extend(p3)
596
+ data.extend(p2)
597
+
598
+ data.extend(p2)
599
+ data.extend(p3)
600
+ data.extend(p4)
601
+
602
+ return np.array(data, dtype=np.float32)
603
+
604
+ @staticmethod
605
+ def primitive(name: Union[str, enum]) -> np.ndarray:
606
+ prim_folder = Path(__file__).parent / "PrimData"
607
+ prims = np.load(prim_folder / "Primitives.npz")
608
+ try:
609
+ return prims[name]
610
+ except KeyError:
611
+ raise ValueError(f"Primitive '{name}' not found")