threequick 0.0.0__tar.gz → 0.0.2__tar.gz

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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threequick
3
- Version: 0.0.0
4
- Summary: A simple python library for creating 3D visualisations without OpenGL.
3
+ Version: 0.0.2
4
+ Summary: A simple python library for creating 3D visualisations
5
5
  Author-email: Hannah Baldwin <contact@hannahbaldwin.net>
6
6
  License: GPL-3.0
7
7
  Project-URL: Homepage, https://github.com/habaldwin01/threequick
@@ -16,4 +16,4 @@ Requires-Dist: pygame>=2.0.0
16
16
  Dynamic: license-file
17
17
 
18
18
  # threequick
19
- A simple python library for creating 3D visualisations without OpenGL
19
+ A simple python library for creating 3D visualisations.
@@ -0,0 +1,2 @@
1
+ # threequick
2
+ A simple python library for creating 3D visualisations.
@@ -5,7 +5,7 @@ authors = [
5
5
  { name="Hannah Baldwin", email="contact@hannahbaldwin.net" },
6
6
  ]
7
7
  license = { text="GPL-3.0" }
8
- description = "A simple python library for creating 3D visualisations without OpenGL."
8
+ description = "A simple python library for creating 3D visualisations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"
11
11
  classifiers = [
@@ -0,0 +1,98 @@
1
+ import pygame
2
+ import numpy as np
3
+ import math
4
+ from threequick import CameraContext, Object3d, Cube, Sphere, PointCloud
5
+
6
+
7
+ window = pygame.display.set_mode((1280, 720), pygame.RESIZABLE)
8
+ pygame.display.set_caption("threequick test")
9
+ done = False
10
+ renderable_objects = []
11
+
12
+
13
+ tc = Cube(0.2)
14
+ # pitch roll yaw
15
+ # x y z
16
+ #tc.set_rotation(0,0,0)
17
+ #tc.set_position(0,0,0)
18
+ tc.set_rotation(30,10,60)
19
+ tc.set_position(1,0,0)
20
+ renderable_objects.append(tc)
21
+
22
+ sp = Sphere(16, 6+1, 1)
23
+ renderable_objects.append(sp)
24
+
25
+ #point_cloud = PointCloud()
26
+ #renderable_objects.append(point_cloud)
27
+
28
+ #point_cloud.add_point([0,0.25,0.25], [0,255,0])
29
+
30
+ cam_dist = 5
31
+
32
+ camera_context = CameraContext([0,-cam_dist,0], [70,0,30], 90, window)
33
+ camera_context.position[0] = (math.sin(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
34
+ camera_context.position[1] = (math.cos(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
35
+ camera_context.position[2] = math.cos(math.radians(camera_context.rotation[0])) * -cam_dist
36
+
37
+ cube_angle = 0
38
+
39
+ pygame.init()
40
+
41
+ last_ticks = pygame.time.get_ticks()
42
+
43
+ while not done:
44
+ current_ticks = pygame.time.get_ticks()
45
+
46
+ pygame.draw.rect(window, (0, 0, 0), (0, 0, window.get_width(), window.get_height()))
47
+
48
+ mx, my = pygame.mouse.get_pos()
49
+ for event in pygame.event.get():
50
+ if event.type == pygame.QUIT:
51
+ exit()
52
+
53
+ left_button, middle_button, right_button = pygame.mouse.get_pressed()
54
+ if not left_button:
55
+ last_rc_pos = (mx, my)
56
+
57
+
58
+ if left_button:
59
+ camera_context.rotation[0] -= (my - last_rc_pos[1]) * 0.2
60
+ if camera_context.rotation[0] > 180:
61
+ camera_context.rotation[0] = 180
62
+ if camera_context.rotation[0] < 0:
63
+ camera_context.rotation[0] = 0
64
+ smx = (math.sin(math.radians(camera_context.rotation[0]))) + 0.5
65
+ camera_context.rotation[2] -= (mx - last_rc_pos[0]) * 0.2 / smx
66
+ last_rc_pos = (mx, my)
67
+
68
+
69
+ orbital_period_s = 8
70
+ cube_angle = ((pygame.time.get_ticks() / 1000) / orbital_period_s) * 360
71
+
72
+ tc.set_rotation(30,10,60 + cube_angle)
73
+ tc.set_position(math.sin(math.radians(cube_angle)),-math.cos(math.radians(cube_angle)),0)
74
+ #tc.set_position(0,0,0)
75
+
76
+ # Slow rotation of camera
77
+ #camera_context.rotation[2] += (last_ticks - current_ticks) / 80
78
+
79
+
80
+ # Calculate camera position based off angle so it always points at 0,0,0
81
+ camera_context.position[0] = (math.sin(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
82
+ camera_context.position[1] = (math.cos(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
83
+ camera_context.position[2] = math.cos(math.radians(camera_context.rotation[0])) * -cam_dist
84
+
85
+
86
+ last_ticks = current_ticks
87
+
88
+ camera_context.update_screenspace()
89
+ for ro in renderable_objects:
90
+ ro.draw(camera_context)
91
+
92
+ for event in pygame.event.get():
93
+ if event.type == pygame.QUIT:
94
+ done = True
95
+
96
+ pygame.display.flip()
97
+
98
+ exit()
@@ -1 +1 @@
1
- from .threequick import CameraContext, Object3d, Cube, Sphere, PointCloud
1
+ from .threequick import CameraContext, Object3d, Cube, Sphere, PointCloud, Ellipsoid
@@ -1,6 +1,12 @@
1
1
  import pygame
2
+ import pygame.font
2
3
  import numpy as np
3
4
  import math
5
+ import random
6
+ from typing import Optional, Union
7
+
8
+ pygame.font.init()
9
+ sans_font = pygame.font.Font(pygame.font.get_default_font(), 36)
4
10
 
5
11
  def transform_3d_point_4x4_mat(matrix, point):
6
12
  # Convert to homogeneous coordinates
@@ -42,10 +48,12 @@ class CameraContext:
42
48
  def __init__(self,
43
49
  position: list[float],
44
50
  rotation: list[float],
51
+ fov: float,
45
52
  surface: pygame.Surface):
46
53
  self.position = position
47
54
  self.rotation = rotation
48
55
  self.screen_size = (1,1)
56
+ self.fov = fov
49
57
  self.frustum_width = 1
50
58
  self.frustum_height = 1
51
59
  self.surface = surface
@@ -54,13 +62,13 @@ class CameraContext:
54
62
  self.screen_size = (self.surface.get_width(), self.surface.get_height())
55
63
 
56
64
  self.min_render_distance = 0.1
57
- self.frustum_width = self.min_render_distance
65
+ self.frustum_width = self.min_render_distance / (math.pi - math.radians(self.fov))
58
66
  self.frustum_height = self.frustum_width * (self.screen_size[1] / self.screen_size[0])
59
67
  #self.projection_matrix = [[nx/2, 0, 0, (nx-1)/2],
60
68
  # [0, ny/2, 0, (ny-1)/2],
61
69
  # [0, 0, 1/2, 1/2 ]]
62
70
 
63
- f = 4 # far
71
+ f = 10 # far
64
72
  n = self.min_render_distance # near
65
73
  r = self.frustum_width # width
66
74
  t = self.frustum_height # height
@@ -73,6 +81,8 @@ class CameraContext:
73
81
  [0, 0, c, d],
74
82
  [0, 0,-1, 0]]
75
83
 
84
+ # From https://songho.ca/opengl/gl_transform.html
85
+
76
86
  self.translation_matrix = [[1, 0, 0, self.position[0]],
77
87
  [0, 1, 0, self.position[1]],
78
88
  [0, 0, 1, self.position[2]],
@@ -103,71 +113,109 @@ class Renderable():
103
113
  class Object3d(Renderable):
104
114
  def __init__(self,
105
115
  vertices: list[list[float]],
106
- edges: list[list[int]],
107
- faces: list[list[int]]) -> None:
108
- self.__mod_vertices = vertices
109
- self.__mod_edges = edges
110
- self.__mod_faces = faces
116
+ faces: list[list[int]],
117
+ color: Optional[list[int]] = None,
118
+ face_color: list[int] = None) -> None:
119
+ self.__mod_vertices = [None] * len(vertices)
120
+ #self.__mod_edges = edges
121
+ #self.__mod_faces = faces
111
122
  self.vertices = vertices
112
- self.edges = edges
113
123
  self.faces = faces
114
- self.position = [0,0,0]
115
- self.rotation_matrix = [[1, 0, 0, 0],
116
- [0, 1, 0, 0],
117
- [0, 0, 1, 0],
118
- [0, 0, 0, 1]]
124
+ self.face_color = face_color
125
+ self.position = [0, 0, 0]
126
+ self.color = color
127
+ self.rotation_matrix = pry_rot_to_4x4(0, 0, 0)
128
+ self.update_vertex_cache()
129
+ edges = set()
130
+ for face in self.faces:
131
+ edges.add((face[0], face[1]))
132
+ edges.add((face[1], face[2]))
133
+ edges.add((face[2], face[0]))
134
+ self.edges = list(edges)
135
+ if self.color is None:
136
+ if self.face_color is None:
137
+ self.face_color = []
138
+ for face in self.faces:
139
+ self.face_color.append([random.randint(0,255),random.randint(0,255),random.randint(0,255)])
140
+
141
+ for face_idx in range(len(self.faces)):
142
+ print(str(self.faces[face_idx]) + " => " + str(self.face_color[face_idx]))
119
143
 
120
144
  def set_rotation(self, pitch, roll, yaw):
121
145
  self.rotation_matrix = pry_rot_to_4x4(pitch, roll, yaw)
122
-
123
- for vindex, vertex in enumerate(self.vertices):
124
- self.__mod_vertices[vindex] = transform_3d_point_4x4_mat(self.rotation_matrix, vertex)
146
+ self.update_vertex_cache()
125
147
 
126
148
  def set_position(self, x, y, z):
127
149
  self.position = [x, y, z]
150
+ self.update_vertex_cache()
128
151
 
152
+ def update_vertex_cache(self):
129
153
  for vindex, vertex in enumerate(self.vertices):
154
+ vertex = transform_3d_point_4x4_mat(self.rotation_matrix, vertex)
130
155
  self.__mod_vertices[vindex] = [vertex[0] + self.position[0], vertex[1] + self.position[1], vertex[2] + self.position[2]]
131
156
 
132
- def draw(self, camera_context: CameraContext) -> None:
157
+ def draw_wireframe(self, camera_context: CameraContext) -> None:
133
158
  scr_center = (camera_context.screen_size[0] / 2, camera_context.screen_size[1] / 2)
134
159
  tpf = camera_context.transform_point
135
160
  for edge in self.edges:
136
161
  p1 = tpf(self.__mod_vertices[edge[0]])
137
162
  p2 = tpf(self.__mod_vertices[edge[1]])
138
163
  if p1 is not None and p2 is not None:
139
- pygame.draw.line(camera_context.surface, (255, 255, 255), p1, p2)
164
+ pygame.draw.line(camera_context.surface, self.color, p1, p2)
165
+
166
+ def draw(self, camera_context: CameraContext) -> None:
167
+ scr_center = (camera_context.screen_size[0] / 2, camera_context.screen_size[1] / 2)
168
+ tpf = camera_context.transform_point
169
+ for idx, face in enumerate(self.faces):
170
+ poly_points = [tpf(self.__mod_vertices[face[0]]),
171
+ tpf(self.__mod_vertices[face[1]]),
172
+ tpf(self.__mod_vertices[face[2]])]
173
+
174
+ dir_sum = 0
175
+ for edgeidx in range(3):
176
+ p1 = poly_points[edgeidx]
177
+ p2 = poly_points[(edgeidx + 1) % 3]
178
+ dir_sum += (p2[0] - p1[0]) * (p2[1] + p1[1])
179
+ backface_cull = dir_sum < 0
180
+
181
+ #if not backface_cull:
182
+ if self.color is None:
183
+ if backface_cull:
184
+ #pygame.draw.lines(camera_context.surface, self.face_color[idx], True, poly_points)
185
+ pass
186
+ else:
187
+ pygame.draw.polygon(camera_context.surface, self.face_color[idx], poly_points)
188
+
189
+
190
+ #for ptidx in range(3):
191
+ # text_surface = sans_font.render(str(face[ptidx]), False, (255,255,255))
192
+ # camera_context.surface.blit(text_surface, poly_points[ptidx])
193
+ else:
194
+ if not backface_cull:
195
+ pygame.draw.polygon(camera_context.surface, self.color, poly_points)
196
+
140
197
 
141
198
  class Cube(Object3d):
142
- edges: list[list[int]] = [
143
- [0, 1],
144
- [1, 2],
145
- [2, 3],
146
- [3, 0],
147
- [4, 5],
148
- [5, 6],
149
- [6, 7],
150
- [7, 4],
151
- [0, 4],
152
- [1, 5],
153
- [2, 6],
154
- [3, 7]
155
- ]
156
199
  faces: list[list[int]] = [
157
- [0, 1, 2],
158
- [1, 2, 3],
159
- [4, 5, 6],
160
- [5, 6, 7],
161
- [0, 1, 5],
162
- [1, 5, 4],
163
- [1, 2, 6],
164
- [2, 6, 5],
165
- [2, 3, 7],
166
- [3, 7, 6],
167
- [3, 0, 4],
168
- [0, 4, 7]
200
+ [0,1,2],
201
+ [0,2,3],
202
+
203
+ [4,7,6],
204
+ [6,5,4],
205
+
206
+ [5,2,1],
207
+ [6,2,5],
208
+
209
+ [5,1,4],
210
+ [4,1,0],
211
+
212
+ [7,0,3],
213
+ [4,0,7],
214
+
215
+ [7,3,6],
216
+ [6,3,2]
169
217
  ]
170
- def __init__(self, size) -> None:
218
+ def __init__(self, size, color = None) -> None:
171
219
  size = size/2
172
220
  vertices: list[list[float]] = [
173
221
  [-size, -size, -size],
@@ -179,17 +227,17 @@ class Cube(Object3d):
179
227
  [size, size, size],
180
228
  [-size, size, size]
181
229
  ]
182
- super().__init__(vertices, Cube.edges, Cube.faces)
230
+ super().__init__(vertices, Cube.faces, color)
183
231
 
184
- class Sphere(Object3d):
185
- def __init__(self, n_meridians, n_parallels, diameter) -> None:
232
+ class Ellipsoid(Object3d):
233
+ def __init__(self, n_meridians, n_parallels, diameter, flattening, color = None) -> None:
234
+ compression_factor = 1 - flattening
186
235
  radius = diameter/2
187
- vertices = [[0,0,radius],[0,0,-radius]]
188
- edges = []
236
+ vertices = [[0,0,radius * compression_factor],[0,0,-radius * compression_factor]]
189
237
  faces = []
190
238
  for ipa in range(n_parallels):
191
239
  vang = ((ipa + 1) / (n_parallels + 1)) * math.pi
192
- vpos = -math.cos(vang) * radius
240
+ vpos = -math.cos(vang) * radius * compression_factor
193
241
  for ime in range(n_meridians):
194
242
  ang = (ime * math.pi * 2) / n_meridians
195
243
  sa = math.sin(ang)
@@ -199,24 +247,29 @@ class Sphere(Object3d):
199
247
 
200
248
  # bottom cap
201
249
  for ime in range(n_meridians):
202
- edges.append([1,2+ime])
250
+ faces.append([2+ime,1,2+((ime+1)%n_meridians)])
203
251
 
204
252
  # top cap
205
253
  for ime in range(n_meridians):
206
- edges.append([0,2+ime+(n_meridians * (n_parallels - 1))])
254
+ bc_offset = (n_meridians * (n_parallels - 1))
255
+ faces.append([0,2+ime+bc_offset,2+((ime+1)%n_meridians)+bc_offset])
207
256
 
208
- # meridians
257
+ # panels
209
258
  for ipa in range(n_parallels - 1):
210
259
  for ime in range(n_meridians):
211
- edges.append([2+ime+(n_meridians * (ipa + 1)),2+ime+(n_meridians * ipa)])
212
-
213
- # parallels
214
- for ipa in range(n_parallels):
215
- for ime in range(n_meridians):
216
- edges.append([2+ime+(n_meridians * ipa),2+((ime + 1)%n_meridians)+(n_meridians * ipa)])
217
-
218
- super().__init__(vertices, edges, faces)
219
-
260
+ pt1 = 2+((ime + 0)%n_meridians)+(n_meridians * ipa)
261
+ pt2 = 2+((ime + 0)%n_meridians)+(n_meridians * (ipa + 1))
262
+ pt3 = 2+((ime + 1)%n_meridians)+(n_meridians * ipa)
263
+ pt4 = 2+((ime + 1)%n_meridians)+(n_meridians * (ipa + 1))
264
+ faces.append([pt2, pt1, pt3])
265
+ faces.append([pt2, pt3, pt4])
266
+ #edges.append([2+ime+(n_meridians * (ipa + 1)),2+ime+(n_meridians * ipa)])
267
+
268
+ super().__init__(vertices, faces, color)
269
+
270
+ class Sphere(Ellipsoid):
271
+ def __init__(self, n_meridians, n_parallels, diameter, color = None) -> None:
272
+ super().__init__(n_meridians, n_parallels, diameter, 0, color)
220
273
 
221
274
  class PointCloud(Renderable):
222
275
  def __init__(self, diameter = 2) -> None:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threequick
3
- Version: 0.0.0
4
- Summary: A simple python library for creating 3D visualisations without OpenGL.
3
+ Version: 0.0.2
4
+ Summary: A simple python library for creating 3D visualisations
5
5
  Author-email: Hannah Baldwin <contact@hannahbaldwin.net>
6
6
  License: GPL-3.0
7
7
  Project-URL: Homepage, https://github.com/habaldwin01/threequick
@@ -16,4 +16,4 @@ Requires-Dist: pygame>=2.0.0
16
16
  Dynamic: license-file
17
17
 
18
18
  # threequick
19
- A simple python library for creating 3D visualisations without OpenGL
19
+ A simple python library for creating 3D visualisations.
@@ -10,4 +10,6 @@ src/threequick.egg-info/PKG-INFO
10
10
  src/threequick.egg-info/SOURCES.txt
11
11
  src/threequick.egg-info/dependency_links.txt
12
12
  src/threequick.egg-info/requires.txt
13
+ src/threequick.egg-info/scm_file_list.json
14
+ src/threequick.egg-info/scm_version.json
13
15
  src/threequick.egg-info/top_level.txt
@@ -0,0 +1,12 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "pyproject.toml",
5
+ "LICENSE",
6
+ ".gitignore",
7
+ "scripts/build-install-test.sh",
8
+ "scripts/example.py",
9
+ "src/threequick/__init__.py",
10
+ "src/threequick/threequick.py"
11
+ ]
12
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.0.2",
3
+ "distance": 0,
4
+ "node": "g1c2fe6dc0ad76f3a32cf39120b3f25647deecce2",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-07-10"
8
+ }
@@ -1,2 +0,0 @@
1
- # threequick
2
- A simple python library for creating 3D visualisations without OpenGL
@@ -1,68 +0,0 @@
1
- import pygame
2
- import numpy as np
3
- import math
4
- from threequick import CameraContext, Object3d, Cube, Sphere, PointCloud
5
-
6
-
7
- window = pygame.display.set_mode((1280, 720), pygame.RESIZABLE)
8
- pygame.display.set_caption("threequick test")
9
- done = False
10
- renderable_objects = []
11
-
12
-
13
- tc = Cube(0.2)
14
- # pitch roll yaw
15
- # x y z
16
- tc.set_rotation(30,10,60)
17
- renderable_objects.append(tc)
18
-
19
- sp = Sphere(16, 6+1, 1)
20
- renderable_objects.append(sp)
21
-
22
- point_cloud = PointCloud()
23
- renderable_objects.append(point_cloud)
24
-
25
- point_cloud.add_point([0,0.25,0.25], [0,255,0])
26
-
27
- cam_dist = 2
28
-
29
- camera_context = CameraContext([0,-cam_dist,0], [90,0,0], window)
30
-
31
-
32
- while not done:
33
- pygame.draw.rect(window, (0, 0, 0), (0, 0, 1280, 720))
34
-
35
- mx, my = pygame.mouse.get_pos()
36
- for event in pygame.event.get():
37
- if event.type == pygame.QUIT:
38
- exit()
39
-
40
- left_button, middle_button, right_button = pygame.mouse.get_pressed()
41
- if not left_button:
42
- last_rc_pos = (mx, my)
43
-
44
-
45
- if left_button:
46
- camera_context.rotation[0] -= (my - last_rc_pos[1]) * 0.2
47
- if camera_context.rotation[0] > 180:
48
- camera_context.rotation[0] = 180
49
- if camera_context.rotation[0] < 0:
50
- camera_context.rotation[0] = 0
51
- smx = (math.sin(math.radians(camera_context.rotation[0]))) + 0.5
52
- camera_context.rotation[2] -= (mx - last_rc_pos[0]) * 0.2 / smx
53
- camera_context.position[0] = (math.sin(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
54
- camera_context.position[1] = (math.cos(math.radians(camera_context.rotation[2])) * -cam_dist) * math.sin(math.radians(camera_context.rotation[0]))
55
- camera_context.position[2] = math.cos(math.radians(camera_context.rotation[0])) * -cam_dist
56
- last_rc_pos = (mx, my)
57
-
58
- camera_context.update_screenspace()
59
- for ro in renderable_objects:
60
- ro.draw(camera_context)
61
-
62
- for event in pygame.event.get():
63
- if event.type == pygame.QUIT:
64
- done = True
65
-
66
- pygame.display.flip()
67
-
68
- exit()
File without changes
File without changes
File without changes