q3dviewer 1.2.1__tar.gz → 1.2.3__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.
Files changed (44) hide show
  1. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/PKG-INFO +1 -1
  2. q3dviewer-1.2.3/q3dviewer/custom_items/mesh_item.py +442 -0
  3. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/glwidget.py +26 -1
  4. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/cloud_viewer.py +70 -54
  5. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/cloud_io.py +46 -19
  6. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/PKG-INFO +1 -1
  7. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/requires.txt +0 -1
  8. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/setup.py +1 -2
  9. q3dviewer-1.2.1/q3dviewer/custom_items/mesh_item.py +0 -342
  10. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/README.md +0 -0
  11. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/Qt/__init__.py +0 -0
  12. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/__init__.py +0 -0
  13. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/base_glwidget.py +0 -0
  14. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/base_item.py +0 -0
  15. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/__init__.py +0 -0
  16. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/axis_item.py +0 -0
  17. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  18. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/cloud_item.py +0 -0
  19. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/frame_item.py +0 -0
  20. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/gaussian_item.py +0 -0
  21. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/grid_item.py +0 -0
  22. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/image_item.py +0 -0
  23. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/line_item.py +0 -0
  24. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/text3d_item.py +0 -0
  25. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/custom_items/text_item.py +0 -0
  26. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/__init__.py +0 -0
  27. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/example_viewer.py +0 -0
  28. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/film_maker.py +0 -0
  29. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/gaussian_viewer.py +0 -0
  30. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/lidar_calib.py +0 -0
  31. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  32. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/tools/ros_viewer.py +0 -0
  33. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/__init__.py +0 -0
  34. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/convert_ros_msg.py +0 -0
  35. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/gl_helper.py +0 -0
  36. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/helpers.py +0 -0
  37. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/maths.py +0 -0
  38. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/utils/range_slider.py +0 -0
  39. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer/viewer.py +0 -0
  40. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/SOURCES.txt +0 -0
  41. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/dependency_links.txt +0 -0
  42. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/entry_points.txt +0 -0
  43. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/q3dviewer.egg-info/top_level.txt +0 -0
  44. {q3dviewer-1.2.1 → q3dviewer-1.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: A library designed for quickly deploying a 3D viewer.
5
5
  Home-page: https://github.com/scomup/q3dviewer
6
6
  Author: Liu Yang
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
5
+ Distributed under MIT license. See LICENSE for more information.
6
+ """
7
+
8
+
9
+ import numpy as np
10
+ from q3dviewer.base_item import BaseItem
11
+ from OpenGL.GL import *
12
+ from OpenGL.GL import shaders
13
+ from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
14
+
15
+ import os
16
+ from q3dviewer.utils import set_uniform, text_to_rgba
17
+ import time
18
+
19
+
20
+
21
+ class MeshItem(BaseItem):
22
+ """
23
+ A OpenGL mesh item for rendering 3D triangular meshes.
24
+ Attributes:
25
+ color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
26
+ wireframe (bool): If True, renders the mesh in wireframe mode.
27
+ """
28
+ def __init__(self, color='lightblue', wireframe=False):
29
+ super(MeshItem, self).__init__()
30
+ self.wireframe = wireframe
31
+ self.color = color
32
+ self.flat_rgb = text_to_rgba(color, flat=True)
33
+
34
+ # Incremental buffer management
35
+ self.FACE_CAPACITY = 1000000 # Initial capacity for faces
36
+
37
+ # Faces buffer: N x 13 numpy array
38
+ # Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z, good]
39
+ self.faces = np.zeros((self.FACE_CAPACITY, 13), dtype=np.float32)
40
+
41
+ # valid_f_top: pointer to end of valid faces
42
+ self.valid_f_top = 0
43
+
44
+ # key2index: mapping from face_key to face buffer index
45
+ self.key2index = {} # {face_key: face_index}
46
+
47
+ # OpenGL objects
48
+ self.vao = None
49
+ self.vbo = None
50
+ self.program = None
51
+ self._gpu_face_capacity = 0 # Track GPU buffer capacity
52
+
53
+ # Fixed rendering parameters (not adjustable via UI)
54
+ self.enable_lighting = True
55
+ self.line_width = 1.0
56
+ self.light_pos = [1.0, 1.0, 1.0]
57
+ self.light_color = [1.0, 1.0, 1.0]
58
+ self.ambient_strength = 0.1
59
+ self.diffuse_strength = 1.2
60
+ self.specular_strength = 0.1
61
+ self.shininess = 32.0
62
+ self.alpha = 1.0
63
+
64
+ # Settings flag
65
+ self.need_update_setting = True
66
+ self.path = os.path.dirname(__file__)
67
+
68
+
69
+ def add_setting(self, layout):
70
+ """Add UI controls for mesh visualization"""
71
+ # Only keep wireframe toggle - all other parameters are fixed
72
+ self.wireframe_box = QCheckBox("Wireframe Mode")
73
+ self.wireframe_box.setChecked(self.wireframe)
74
+ self.wireframe_box.toggled.connect(self.update_wireframe)
75
+ layout.addWidget(self.wireframe_box)
76
+
77
+ # Enable lighting toggle
78
+ self.lighting_box = QCheckBox("Enable Lighting")
79
+ self.lighting_box.setChecked(self.enable_lighting)
80
+ self.lighting_box.toggled.connect(self.update_enable_lighting)
81
+ layout.addWidget(self.lighting_box)
82
+
83
+ label_rgb = QLabel("Color:")
84
+ label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
85
+ layout.addWidget(label_rgb)
86
+ self.edit_rgb = QLineEdit()
87
+ self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
88
+ self.edit_rgb.setText(self.color)
89
+ self.edit_rgb.textChanged.connect(self._on_color)
90
+ layout.addWidget(self.edit_rgb)
91
+
92
+ # Material property controls for Phong lighting
93
+ if self.enable_lighting:
94
+ # Ambient strength control (slider 0-100 mapped to 0.0-1.0)
95
+ ambient_layout = QHBoxLayout()
96
+ ambient_label = QLabel("Ambient Strength:")
97
+ ambient_layout.addWidget(ambient_label)
98
+ self.ambient_slider = QSlider()
99
+ self.ambient_slider.setOrientation(1) # Qt.Horizontal
100
+ self.ambient_slider.setRange(0, 100)
101
+ self.ambient_slider.setValue(int(self.ambient_strength * 100))
102
+ self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
103
+ ambient_layout.addWidget(self.ambient_slider)
104
+ layout.addLayout(ambient_layout)
105
+
106
+ # Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
107
+ diffuse_layout = QHBoxLayout()
108
+ diffuse_label = QLabel("Diffuse Strength:")
109
+ diffuse_layout.addWidget(diffuse_label)
110
+ self.diffuse_slider = QSlider()
111
+ self.diffuse_slider.setOrientation(1)
112
+ self.diffuse_slider.setRange(0, 200)
113
+ self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
114
+ self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
115
+ diffuse_layout.addWidget(self.diffuse_slider)
116
+ layout.addLayout(diffuse_layout)
117
+
118
+ # Specular strength control (slider 0-200 mapped to 0.0-2.0)
119
+ specular_layout = QHBoxLayout()
120
+ specular_label = QLabel("Specular Strength:")
121
+ specular_layout.addWidget(specular_label)
122
+ self.specular_slider = QSlider()
123
+ self.specular_slider.setOrientation(1)
124
+ self.specular_slider.setRange(0, 200)
125
+ self.specular_slider.setValue(int(self.specular_strength * 100))
126
+ self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
127
+ specular_layout.addWidget(self.specular_slider)
128
+ layout.addLayout(specular_layout)
129
+
130
+ # Shininess control (slider 1-256 mapped to 1-256)
131
+ shininess_layout = QHBoxLayout()
132
+ shininess_label = QLabel("Shininess:")
133
+ shininess_layout.addWidget(shininess_label)
134
+ self.shininess_slider = QSlider()
135
+ self.shininess_slider.setOrientation(1)
136
+ self.shininess_slider.setRange(1, 256)
137
+ self.shininess_slider.setValue(int(self.shininess))
138
+ self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
139
+ shininess_layout.addWidget(self.shininess_slider)
140
+ layout.addLayout(shininess_layout)
141
+
142
+ def _on_color(self, color):
143
+ try:
144
+ self.color = color
145
+ self.flat_rgb = text_to_rgba(color, flat=True)
146
+ self.need_update_setting = True
147
+ except ValueError:
148
+ pass
149
+
150
+ def update_wireframe(self, value):
151
+ self.wireframe = value
152
+
153
+ def update_enable_lighting(self, value):
154
+ self.enable_lighting = value
155
+ self.need_update_setting = True
156
+
157
+ def update_line_width(self, value):
158
+ self.line_width = value
159
+ self.need_update_setting = True
160
+
161
+ def update_ambient_strength(self, value):
162
+ self.ambient_strength = value
163
+ self.need_update_setting = True
164
+
165
+ def update_diffuse_strength(self, value):
166
+ self.diffuse_strength = value
167
+ self.need_update_setting = True
168
+
169
+ def update_specular_strength(self, value):
170
+ self.specular_strength = value
171
+ self.need_update_setting = True
172
+
173
+ def update_shininess(self, value):
174
+ self.shininess = value
175
+ self.need_update_setting = True
176
+
177
+ def update_alpha(self, value):
178
+ """Update mesh alpha (opacity)"""
179
+ self.alpha = float(value)
180
+ self.need_update_setting = True
181
+
182
+ def set_data(self, data):
183
+ """
184
+ Set complete mesh data at once.
185
+
186
+ Args:
187
+ data: is Nx3 numpy array (N must be divisible by 3) or dict
188
+ if is dict, uses the dict format:
189
+ [face_key: (v0.x, v0.y, v0.z, ..., v3.z, good)]
190
+ """
191
+ self.clear_mesh()
192
+
193
+ if isinstance(data, dict):
194
+ # Use dict format directly (from get_mesh_data/get_incremental_mesh_data)
195
+ self.set_incremental_data(data)
196
+ return
197
+
198
+ # Check if Nx3 array
199
+
200
+ if not isinstance(data, np.ndarray):
201
+ raise ValueError("Invalid data type")
202
+
203
+ good_format = False
204
+ if data.ndim == 2 and \
205
+ data.shape[1] == 3 and \
206
+ data.shape[0] % 3 == 0:
207
+ good_format = True
208
+
209
+ if not good_format:
210
+ raise ValueError("Invalid data shape")
211
+
212
+ # Convert to Nx13 numpy array
213
+ N = data.shape[0] // 3
214
+ faces = np.zeros((N, 13), dtype=np.float32)
215
+ tmp = data.reshape(N, 9)
216
+ faces[:, 0:3] = tmp[:, 0:3] # copy v0
217
+ faces[:, 3:6] = tmp[:, 3:6] # copy v1
218
+ faces[:, 6:9] = tmp[:, 6:9] # copy v2
219
+ faces[:, 9:12] = tmp[:, 6:9] # copy v3 from v2 (degenerate quad)
220
+ faces[:, 12] = 1.0 # set good=1.0
221
+ self.faces = faces
222
+ self.valid_f_top = N
223
+
224
+
225
+ def set_incremental_data(self, fs):
226
+ """
227
+ Incrementally update mesh with new face data.
228
+ Args:
229
+ fs: Dict {face_key: (v0.x, v0.y, v0.z, v1.x, v1.y, v1.z,
230
+ v2.x, v2.y, v2.z, v3.x, v3.y, v3.z, good), ...}
231
+ 13-tuple with vertex positions and good flag (0.0 or 1.0)
232
+ If good==1:
233
+ Triangle 1: (v0, v1, v3)
234
+ Triangle 2: (v0, v3, v2)
235
+ Updates:
236
+ - faces: updates existing faces or appends new ones
237
+ - key2index: tracks face_key -> face_index mapping
238
+ """
239
+ if not fs:
240
+ return
241
+
242
+ # Ensure enough capacity in faces buffer
243
+ # wasted cases are better than frequent expansions
244
+ while self.valid_f_top + len(fs) > len(self.faces):
245
+ self._expand_face_buffer()
246
+
247
+ # Optimization: Separate updates from new insertions to avoid
248
+ # dictionary lookup performance degradation during growth
249
+ update_idxs = [] # [idx, ...]
250
+ update_data = [] # [face_data, ...]
251
+ new_keys = [] # [key, ...]
252
+ new_data = [] # [face_data, ...]
253
+
254
+ for face_key, face_data in fs.items():
255
+ face_idx = self.key2index.get(face_key)
256
+ if face_idx is not None:
257
+ update_idxs.append(face_idx)
258
+ update_data.append(face_data)
259
+ else:
260
+ new_keys.append(face_key)
261
+ new_data.append(face_data)
262
+
263
+ # Batch update existing faces
264
+ if update_data:
265
+ indices = np.array(update_idxs, dtype=np.int32)
266
+ data = np.array(update_data, dtype=np.float32)
267
+ self.faces[indices] = data
268
+
269
+ # Batch insert new faces
270
+ if new_data:
271
+ n_new = len(new_data)
272
+ data = np.array(new_data, dtype=np.float32)
273
+ self.faces[self.valid_f_top: self.valid_f_top + n_new] = data
274
+
275
+ # Update key2index mapping for new faces
276
+ for i, face_key in enumerate(new_keys):
277
+ self.key2index[face_key] = self.valid_f_top + i
278
+
279
+ self.valid_f_top += n_new
280
+
281
+ def _expand_face_buffer(self):
282
+ """Expand the faces buffer when capacity is reached"""
283
+ new_capacity = len(self.faces) + self.FACE_CAPACITY
284
+ new_buffer = np.zeros((new_capacity, 13), dtype=np.float32)
285
+ new_buffer[:len(self.faces)] = self.faces
286
+ self.faces = new_buffer
287
+
288
+ def clear_mesh(self):
289
+ """Clear all mesh data and reset buffers"""
290
+ self.valid_f_top = 0
291
+ self.key2index.clear()
292
+ if hasattr(self, 'indices_array'):
293
+ self.indices_array = np.array([], dtype=np.uint32)
294
+
295
+ def initialize_gl(self):
296
+ """OpenGL initialization"""
297
+ # Use instanced mesh shaders with geometry shader for GPU-side triangle generation
298
+ vert_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
299
+ geom_shader = open(self.path + '/../shaders/mesh_geom.glsl', 'r').read()
300
+ frag_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
301
+ try:
302
+ program = shaders.compileProgram(
303
+ shaders.compileShader(vert_shader, GL_VERTEX_SHADER),
304
+ shaders.compileShader(geom_shader, GL_GEOMETRY_SHADER),
305
+ shaders.compileShader(frag_shader, GL_FRAGMENT_SHADER),
306
+ )
307
+ self.program = program
308
+ except Exception as e:
309
+ raise
310
+
311
+ def update_render_buffer(self):
312
+ """
313
+ Update GPU buffer with face data (no separate vertex buffer).
314
+ Each face contains embedded vertex positions (13 floats).
315
+ Geometry shader generates triangles on GPU from face vertices.
316
+ Dynamically resizes GPU buffer when Python buffer expands.
317
+ """
318
+ if self.valid_f_top == 0:
319
+ return
320
+
321
+ # Initialize buffers on first call
322
+ if self.vao is None:
323
+ self.vao = glGenVertexArrays(1)
324
+ self.vbo = glGenBuffers(1)
325
+ self._gpu_face_capacity = 0
326
+
327
+ # Check if we need to reallocate VBO for faces
328
+ if self._gpu_face_capacity < len(self.faces):
329
+ glBindVertexArray(self.vao)
330
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
331
+ glBufferData(GL_ARRAY_BUFFER,
332
+ self.faces.nbytes,
333
+ None,
334
+ GL_DYNAMIC_DRAW)
335
+
336
+ # Setup face attributes (per-instance)
337
+ # Face data: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z, good]
338
+ # 13 floats = 52 bytes stride
339
+
340
+ # v0 (location 1) - vec3
341
+ glEnableVertexAttribArray(1)
342
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(0))
343
+ glVertexAttribDivisor(1, 1)
344
+
345
+ # v1 (location 2) - vec3
346
+ glEnableVertexAttribArray(2)
347
+ glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(12))
348
+ glVertexAttribDivisor(2, 1)
349
+
350
+ # v2 (location 3) - vec3
351
+ glEnableVertexAttribArray(3)
352
+ glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(24))
353
+ glVertexAttribDivisor(3, 1)
354
+
355
+ # v3 (location 4) - vec3
356
+ glEnableVertexAttribArray(4)
357
+ glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(36))
358
+ glVertexAttribDivisor(4, 1)
359
+
360
+ # good flag (location 5) - float
361
+ glEnableVertexAttribArray(5)
362
+ glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(48))
363
+ glVertexAttribDivisor(5, 1)
364
+
365
+ glBindVertexArray(0)
366
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
367
+ self._gpu_face_capacity = len(self.faces)
368
+
369
+ # Upload faces to VBO
370
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
371
+ glBufferSubData(GL_ARRAY_BUFFER,
372
+ 0,
373
+ self.valid_f_top * 13 * 4, # 13 floats * 4 bytes per face
374
+ self.faces[:self.valid_f_top])
375
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
376
+
377
+ def update_setting(self):
378
+ """Set fixed rendering parameters (called once during initialization)"""
379
+ if not self.need_update_setting:
380
+ return
381
+ # Set fixed uniforms for instanced shaders
382
+ set_uniform(self.program, int(self.enable_lighting), 'if_light')
383
+ set_uniform(self.program, 1, 'two_sided')
384
+
385
+ set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
386
+ set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
387
+ set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
388
+ set_uniform(self.program, float(self.specular_strength), 'specular_strength')
389
+ set_uniform(self.program, float(self.shininess), 'shininess')
390
+ set_uniform(self.program, float(self.alpha), 'alpha')
391
+ set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
392
+ self.need_update_setting = False
393
+
394
+ def paint(self):
395
+ """
396
+ Render the mesh using instanced rendering with geometry shader.
397
+ Each face instance is rendered as a point, geometry shader generates 2 triangles.
398
+ GPU filters faces based on good flag.
399
+ """
400
+ if self.valid_f_top == 0:
401
+ return
402
+
403
+ glUseProgram(self.program)
404
+
405
+ self.update_render_buffer()
406
+ self.update_setting()
407
+
408
+ view_matrix = self.glwidget().view_matrix
409
+ set_uniform(self.program, view_matrix, 'view')
410
+ project_matrix = self.glwidget().projection_matrix
411
+ set_uniform(self.program, project_matrix, 'projection')
412
+ view_pos = self.glwidget().center
413
+ set_uniform(self.program, np.array(view_pos), 'view_pos')
414
+
415
+ # Enable blending and depth testing
416
+ glEnable(GL_BLEND)
417
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
418
+ glEnable(GL_DEPTH_TEST)
419
+ glDisable(GL_CULL_FACE) # two-sided rendering
420
+
421
+ # Set line width
422
+ glLineWidth(self.line_width)
423
+
424
+ # Bind VAO (vertex positions are now in VBO attributes)
425
+ glBindVertexArray(self.vao)
426
+
427
+ if self.wireframe:
428
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
429
+ else:
430
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
431
+
432
+ # Draw using instanced rendering
433
+ # Input: POINTS (one per face instance)
434
+ # Geometry shader generates 2 triangles (6 vertices) per point
435
+ glDrawArraysInstanced(GL_POINTS, 0, 1, self.valid_f_top)
436
+
437
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
438
+ glBindVertexArray(0)
439
+ glDisable(GL_DEPTH_TEST)
440
+ glDisable(GL_BLEND)
441
+ glUseProgram(0)
442
+
@@ -55,6 +55,7 @@ class GLWidget(BaseGLWidget):
55
55
  self.followable_item_name = None
56
56
  self.setting_window = SettingWindow()
57
57
  self.enable_show_center = True
58
+ self.old_center = None
58
59
  super(GLWidget, self).__init__()
59
60
 
60
61
  def keyPressEvent(self, ev: QKeyEvent):
@@ -63,14 +64,38 @@ class GLWidget(BaseGLWidget):
63
64
  self.open_setting_window()
64
65
  else:
65
66
  super().keyPressEvent(ev)
67
+ if ev.key() == QtCore.Qt.Key_F: # reset follow
68
+ if self.followable_item_name is None:
69
+ self.initial_followable()
70
+
71
+ if self.followed_name != 'none':
72
+ self.followed_name = 'none'
73
+ print("Reset follow.")
74
+ elif len(self.followable_item_name) > 1:
75
+ self.followed_name = self.followable_item_name[1]
76
+ print("Set follow to ", self.followed_name)
77
+ else:
78
+ pass # do nothing
66
79
 
67
80
  def on_followable_selection(self, index):
68
81
  self.followed_name = self.followable_item_name[index]
69
82
 
83
+ def mouseDoubleClickEvent(self, event):
84
+ """Double click to set center."""
85
+ p = self.get_point(event.x(), event.y())
86
+ if p is not None:
87
+ self.set_center(p)
88
+ super().mouseDoubleClickEvent(event)
89
+
70
90
  def update(self):
71
91
  if self.followed_name != 'none':
72
92
  new_center = self.named_items[self.followed_name].T[:3, 3]
73
- self.set_center(new_center)
93
+ if self.old_center is None:
94
+ self.old_center = self.center
95
+ return
96
+ delta = new_center - self.old_center
97
+ self.set_center(self.center + delta)
98
+ self.old_center = new_center
74
99
  super().update()
75
100
 
76
101
  def add_setting(self, layout):
@@ -7,39 +7,29 @@ Distributed under MIT license. See LICENSE for more information.
7
7
 
8
8
  import numpy as np
9
9
  import q3dviewer as q3d
10
- from q3dviewer.Qt.QtWidgets import QVBoxLayout, QProgressBar, QDialog, QLabel
10
+ from q3dviewer.Qt.QtWidgets import QVBoxLayout, QDialog, QLabel
11
11
  from q3dviewer.Qt.QtCore import QThread, Signal, Qt
12
- from q3dviewer.Qt.QtGui import QKeyEvent
13
12
  from q3dviewer import GLWidget
14
13
 
15
-
16
- class ProgressDialog(QDialog):
14
+ class ProgressWindow(QDialog):
17
15
  def __init__(self, parent=None):
18
16
  super().__init__(parent)
19
- self.setWindowTitle("Loading Cloud File")
17
+ self.setWindowTitle("Loading")
20
18
  self.setModal(True)
21
- self.progress_bar = QProgressBar(self)
22
- self.file_label = QLabel(self)
19
+ self.setMinimumWidth(400)
20
+ self.label = QLabel(self)
21
+ self.label.setAlignment(Qt.AlignCenter)
23
22
  layout = QVBoxLayout()
24
- layout.addWidget(self.file_label)
25
- layout.addWidget(self.progress_bar)
23
+ layout.addWidget(self.label)
26
24
  self.setLayout(layout)
27
25
 
28
- def set_value(self, value):
29
- self.progress_bar.setValue(value)
30
-
31
- def set_file_name(self, file_name):
32
- self.file_label.setText(f"Loading: {file_name}")
33
-
34
- def closeEvent(self, event):
35
- if self.parent().progress_thread and self.parent().progress_thread.isRunning():
36
- event.ignore()
37
- else:
38
- event.accept()
26
+ def update_progress(self, current, total, file_name):
27
+ text = f"[{current}/{total}] loading file: {file_name}"
28
+ self.label.setText(text)
39
29
 
40
30
 
41
31
  class FileLoaderThread(QThread):
42
- progress = Signal(int)
32
+ progress = Signal(int, int, str) # current, total, filename
43
33
  finished = Signal()
44
34
 
45
35
  def __init__(self, viewer, files):
@@ -50,20 +40,22 @@ class FileLoaderThread(QThread):
50
40
  def run(self):
51
41
  cloud_item = self.viewer['cloud']
52
42
  mesh_item = self.viewer['mesh']
43
+ total = len(self.files)
53
44
  for i, url in enumerate(self.files):
54
45
  # if the file is a mesh file, use mesh_item to load
55
46
  file_path = url.toLocalFile()
56
- file_path = url.toLocalFile()
57
- self.viewer.progress_dialog.set_file_name(file_path)
47
+ import os
48
+ file_name = os.path.basename(file_path)
49
+ self.progress.emit(i + 1, total, file_name)
50
+
58
51
  if url.toLocalFile().lower().endswith(('.stl')):
59
52
  from q3dviewer.utils.cloud_io import load_stl
60
- verts, faces = load_stl(file_path)
61
- mesh_item.set_data(verts=verts, faces=faces)
53
+ mesh = load_stl(file_path)
54
+ mesh_item.set_data(mesh)
62
55
  else:
63
56
  cloud = cloud_item.load(file_path, append=(i > 0))
64
57
  center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
65
58
  self.viewer.glwidget.set_cam_position(center=center)
66
- self.progress.emit(int((i + 1) / len(self.files) * 100))
67
59
  self.finished.emit()
68
60
 
69
61
 
@@ -130,20 +122,19 @@ class CloudViewer(q3d.Viewer):
130
122
  """
131
123
  Overwrite the drop event to open the cloud file.
132
124
  """
133
- self.progress_dialog = ProgressDialog(self)
134
- self.progress_dialog.show()
125
+ self.progress_window = ProgressWindow(self)
126
+ self.progress_window.show()
135
127
  files = event.mimeData().urls()
136
128
  self.progress_thread = FileLoaderThread(self, files)
137
- self['cloud'].load(files[0].toLocalFile(), append=False)
138
129
  self.progress_thread.progress.connect(self.file_loading_progress)
139
130
  self.progress_thread.finished.connect(self.file_loading_finished)
140
131
  self.progress_thread.start()
141
132
 
142
- def file_loading_progress(self, value):
143
- self.progress_dialog.set_value(value)
133
+ def file_loading_progress(self, current, total, file_name):
134
+ self.progress_window.update_progress(current, total, file_name)
144
135
 
145
136
  def file_loading_finished(self):
146
- self.progress_dialog.close()
137
+ self.progress_window.close()
147
138
 
148
139
  def open_cloud_file(self, file, append=False):
149
140
  cloud_item = self['cloud']
@@ -154,27 +145,50 @@ class CloudViewer(q3d.Viewer):
154
145
  center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
155
146
  self.glwidget.set_cam_position(center=center)
156
147
 
157
- # print a quick help message
148
+ # print a quick help message using rich
158
149
  def print_help():
159
- # ANSI color codes
160
- GREEN = '\033[92m'
161
- BLUE = '\033[94m'
162
- BOLD = '\033[1m'
163
- END = '\033[0m'
150
+ from rich.console import Console
151
+ from rich.panel import Panel
152
+ from rich.table import Table
153
+ from rich.text import Text
154
+
155
+ console = Console()
156
+
157
+ # Create a table for better organization
158
+ table = Table(show_header=False, box=None, padding=(0, 2))
159
+ table.add_column(style="bold cyan", width=30)
160
+ table.add_column(style="white")
161
+
162
+ # File loading section
163
+ table.add_row("📁 Load Files","Drag and drop files into the viewer")
164
+ table.add_row("","[dim]• Point clouds: .pcd, .ply, .las, .e57[/dim]")
165
+ table.add_row("","[dim]• Mesh files: .stl[/dim]")
166
+ table.add_row("", "")
167
+
168
+ # Measurement section
169
+ table.add_row("📏 Measure Distance", "Interactive point measurement")
170
+ table.add_row("","[dim]• Ctrl + Left Click: Add measurement point[/dim]")
171
+ table.add_row("","[dim]• Ctrl + Right Click: Remove last point[/dim]")
172
+ table.add_row("","[dim]• Total distance displayed automatically[/dim]")
173
+ table.add_row("", "")
174
+
175
+ # Camera controls
176
+ table.add_row("🎥 Camera Controls","Navigate the 3D scene")
177
+ table.add_row("","[dim]• Double Click: Set camera center to point[/dim]")
178
+ table.add_row("","[dim]• Right Drag: Rotate view[/dim]")
179
+ table.add_row("","[dim]• Left Drag: Pan view[/dim]")
180
+ table.add_row("","[dim]• Mouse Wheel: Zoom in/out[/dim]")
181
+ table.add_row("", "")
182
+
183
+ # Settings section
184
+ table.add_row("⚙️ Settings","Press [bold green]'M'[/bold green] to open settings window")
185
+ table.add_row("","[dim]Adjust visualization properties[/dim]")
164
186
 
165
- help_msg = f"""
166
- {BOLD}Cloud Viewer Help:{END}
167
- {GREEN}• Drag and drop cloud files into the viewer to load them.{END}
168
- {BLUE}- support .pcd, .ply, .las, .e57, for point clouds.{END}
169
- {BLUE}- support .stl for mesh files.{END}
170
- {GREEN}• Measure distance between points:{END}
171
- {BLUE}- Hold Ctrl and left-click to select points on the cloud.{END}
172
- {BLUE}- Hold Ctrl and right-click to remove the last selected point.{END}
173
- {BLUE}- The total distance between selected points will be displayed.{END}
174
- {GREEN}• Press 'M' to open the settings window.{END}
175
- {BLUE}- Use the settings window to adjust item properties.{END}
176
- """
177
- print(help_msg)
187
+ # Print title and table without border
188
+ console.print()
189
+ console.print("[bold magenta]☁️ Cloud Viewer Help[/bold magenta]\n")
190
+ console.print(table)
191
+ console.print()
178
192
 
179
193
  def main():
180
194
  print_help()
@@ -186,18 +200,20 @@ def main():
186
200
  viewer = CloudViewer(name='Cloud Viewer')
187
201
  cloud_item = q3d.CloudIOItem(size=1, alpha=0.1)
188
202
  axis_item = q3d.AxisItem(size=0.5, width=5)
203
+ axis_item.disable_setting()
189
204
  grid_item = q3d.GridItem(size=1000, spacing=20)
190
205
  marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
191
206
  text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
207
+ text_item.disable_setting()
192
208
  mesh_item = q3d.MeshItem() # Added MeshIOItem for mesh support
193
209
 
194
210
  viewer.add_items(
195
211
  {'marker': marker_item,
196
- 'cloud': cloud_item,
212
+ 'cloud': cloud_item,
213
+ 'mesh': mesh_item,
197
214
  'grid': grid_item,
198
215
  'axis': axis_item,
199
- 'text': text_item,
200
- 'mesh': mesh_item})
216
+ 'text': text_item,})
201
217
 
202
218
  if args.path:
203
219
  pcd_fn = args.path
@@ -7,25 +7,52 @@ import numpy as np
7
7
 
8
8
 
9
9
  def load_stl(file_path):
10
- from stl import mesh as stlmesh
11
- m = stlmesh.Mesh.from_file(file_path)
12
- verts = m.vectors.reshape(-1, 3).astype(np.float32)
13
- faces = np.arange(len(verts), dtype=np.uint32).reshape(-1, 3)
14
- return verts, faces
15
-
16
-
17
- def save_stl(verts, faces, save_path):
18
- """Save the generated mesh as an STL file."""
19
- from stl import mesh as stlmesh
20
- from stl import Mode
21
- verts = np.asarray(verts, dtype=np.float32)
22
- faces = np.asarray(faces, dtype=np.uint32)
23
- # Create the mesh
24
- m = stlmesh.Mesh(np.zeros(faces.shape[0], dtype=stlmesh.Mesh.dtype))
25
- m.vectors[:] = verts[faces].astype(np.float32)
26
- # Save to file
27
- m.save(save_path, mode=Mode.BINARY)
28
-
10
+ import meshio
11
+ mesh = meshio.read(file_path)
12
+ # meshio returns cells as a list of (cell_type, cell_data) tuples
13
+ # For STL, we expect 'triangle' cells
14
+ vertices = mesh.points.astype(np.float32)
15
+
16
+ # Find triangle cells
17
+ triangles = None
18
+ for cell_block in mesh.cells:
19
+ if cell_block.type == 'triangle':
20
+ triangles = cell_block.data
21
+ break
22
+
23
+ if triangles is None:
24
+ raise ValueError(f"No triangle cells found in STL file: {file_path}")
25
+
26
+ # Convert indexed triangles to flat vertex array (N*3, 3)
27
+ faces = vertices[triangles.flatten()].astype(np.float32)
28
+ return faces
29
+
30
+
31
+ def save_stl(faces, save_path, binary=True):
32
+ import meshio
33
+ faces = np.asarray(faces, dtype=np.float32)
34
+ if faces.shape[0] % 3 != 0:
35
+ raise ValueError(f"Invalid faces shape: {faces.shape}, must be (N*3, 3)")
36
+
37
+ # Reshape to (num_triangles, 3, 3)
38
+ num_triangles = faces.shape[0] // 3
39
+ triangles = faces.reshape(num_triangles, 3, 3)
40
+
41
+ # Get unique vertices and create index array
42
+ vertices, indices = np.unique(triangles.reshape(-1, 3), axis=0, return_inverse=True)
43
+ triangle_indices = indices.reshape(num_triangles, 3)
44
+
45
+ # Create meshio mesh object
46
+ mesh = meshio.Mesh(
47
+ points=vertices,
48
+ cells=[("triangle", triangle_indices)]
49
+ )
50
+ # meshio automatically saves STL as binary by default
51
+ # Use file_format="stl-ascii" for ASCII format
52
+ if binary:
53
+ mesh.write(save_path, binary=True)
54
+ else:
55
+ mesh.write(save_path, binary=False)
29
56
 
30
57
  def save_ply(cloud, save_path):
31
58
  import meshio
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: A library designed for quickly deploying a 3D viewer.
5
5
  Home-page: https://github.com/scomup/q3dviewer
6
6
  Author: Liu Yang
@@ -5,7 +5,6 @@ laspy
5
5
  matplotlib
6
6
  meshio
7
7
  numpy
8
- numpy-stl
9
8
  pye57
10
9
  pypcd4
11
10
  pyside6
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='q3dviewer',
5
- version='1.2.1',
5
+ version='1.2.3',
6
6
  author="Liu Yang",
7
7
  description="A library designed for quickly deploying a 3D viewer.",
8
8
  long_description=open("README.md").read(),
@@ -29,7 +29,6 @@ setup(
29
29
  'imageio',
30
30
  'imageio[ffmpeg]',
31
31
  'matplotlib',
32
- 'numpy-stl',
33
32
  ],
34
33
  entry_points={
35
34
  'console_scripts': [
@@ -1,342 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- """
4
- Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
5
- Distributed under MIT license. See LICENSE for more information.
6
- """
7
-
8
-
9
- import numpy as np
10
- from q3dviewer.base_item import BaseItem
11
- from OpenGL.GL import *
12
- from OpenGL.GL import shaders
13
- from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
14
- import matplotlib.colors as mcolors
15
- import os
16
- from q3dviewer.utils import set_uniform, text_to_rgba
17
-
18
-
19
-
20
- class MeshItem(BaseItem):
21
- def __init__(self, color='lightblue', wireframe=False, enable_lighting=True):
22
- super(MeshItem, self).__init__()
23
- self.color = color
24
- self.flat_rgb = text_to_rgba(color, flat=True)
25
- self.wireframe = wireframe
26
- self.enable_lighting = enable_lighting
27
-
28
- # Mesh data
29
- self.triangles = None
30
- self.normals = None
31
-
32
- # OpenGL objects
33
- self.vao = None
34
- self.vbo_vertices = None
35
- self.vbo_normals = None
36
- # self.vbo_colors = None
37
- self.program = None
38
-
39
- # Rendering parameters
40
- self.line_width = 1.0
41
- self.light_pos = [1.0, 1.0, 1.0]
42
- self.light_color = [1.0, 1.0, 1.0]
43
-
44
- # Phong lighting material properties
45
- self.ambient_strength = 0.1
46
- self.diffuse_strength = 1.2
47
- self.specular_strength = 0.1
48
- self.shininess = 32.0
49
- # Alpha (opacity)
50
- self.alpha = 1.0
51
-
52
- # Buffer initialization flag
53
- self.need_update_buffer = True
54
- self.need_update_setting = True
55
- self.path = os.path.dirname(__file__)
56
-
57
-
58
- def add_setting(self, layout):
59
- """Add UI controls for mesh visualization"""
60
- # Wireframe toggle
61
- self.wireframe_box = QCheckBox("Wireframe Mode")
62
- self.wireframe_box.setChecked(self.wireframe)
63
- self.wireframe_box.toggled.connect(self.update_wireframe)
64
- layout.addWidget(self.wireframe_box)
65
-
66
- # Enable lighting toggle
67
- self.lighting_box = QCheckBox("Enable Lighting")
68
- self.lighting_box.setChecked(self.enable_lighting)
69
- self.lighting_box.toggled.connect(self.update_enable_lighting)
70
- layout.addWidget(self.lighting_box)
71
-
72
- # Line width control
73
- line_width_label = QLabel("Line Width:")
74
- layout.addWidget(line_width_label)
75
- self.line_width_box = QDoubleSpinBox()
76
- self.line_width_box.setRange(0.5, 5.0)
77
- self.line_width_box.setSingleStep(0.5)
78
- self.line_width_box.setValue(self.line_width)
79
- self.line_width_box.valueChanged.connect(self.update_line_width)
80
- layout.addWidget(self.line_width_box)
81
-
82
- # Alpha control
83
- alpha_label = QLabel("Alpha:")
84
- layout.addWidget(alpha_label)
85
- alpha_box = QDoubleSpinBox()
86
- alpha_box.setRange(0.0, 1.0)
87
- alpha_box.setSingleStep(0.05)
88
- alpha_box.setValue(self.alpha)
89
- alpha_box.valueChanged.connect(self.update_alpha)
90
- layout.addWidget(alpha_box)
91
-
92
-
93
- label_rgb = QLabel("Color:")
94
- label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
95
- layout.addWidget(label_rgb)
96
- self.edit_rgb = QLineEdit()
97
- self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
98
- self.edit_rgb.setText(self.color)
99
- self.edit_rgb.textChanged.connect(self._on_color)
100
- layout.addWidget(self.edit_rgb)
101
-
102
-
103
- # Material property controls for Phong lighting
104
- if self.enable_lighting:
105
- # Ambient strength control (slider 0-100 mapped to 0.0-1.0)
106
- ambient_layout = QHBoxLayout()
107
- ambient_label = QLabel("Ambient Strength:")
108
- ambient_layout.addWidget(ambient_label)
109
- self.ambient_slider = QSlider()
110
- self.ambient_slider.setOrientation(1) # Qt.Horizontal
111
- self.ambient_slider.setRange(0, 100)
112
- self.ambient_slider.setValue(int(self.ambient_strength * 100))
113
- self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
114
- ambient_layout.addWidget(self.ambient_slider)
115
- layout.addLayout(ambient_layout)
116
-
117
- # Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
118
- diffuse_layout = QHBoxLayout()
119
- diffuse_label = QLabel("Diffuse Strength:")
120
- diffuse_layout.addWidget(diffuse_label)
121
- self.diffuse_slider = QSlider()
122
- self.diffuse_slider.setOrientation(1)
123
- self.diffuse_slider.setRange(0, 200)
124
- self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
125
- self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
126
- diffuse_layout.addWidget(self.diffuse_slider)
127
- layout.addLayout(diffuse_layout)
128
-
129
- # Specular strength control (slider 0-200 mapped to 0.0-2.0)
130
- specular_layout = QHBoxLayout()
131
- specular_label = QLabel("Specular Strength:")
132
- specular_layout.addWidget(specular_label)
133
- self.specular_slider = QSlider()
134
- self.specular_slider.setOrientation(1)
135
- self.specular_slider.setRange(0, 200)
136
- self.specular_slider.setValue(int(self.specular_strength * 100))
137
- self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
138
- specular_layout.addWidget(self.specular_slider)
139
- layout.addLayout(specular_layout)
140
-
141
- # Shininess control (slider 1-256 mapped to 1-256)
142
- shininess_layout = QHBoxLayout()
143
- shininess_label = QLabel("Shininess:")
144
- shininess_layout.addWidget(shininess_label)
145
- self.shininess_slider = QSlider()
146
- self.shininess_slider.setOrientation(1)
147
- self.shininess_slider.setRange(1, 256)
148
- self.shininess_slider.setValue(int(self.shininess))
149
- self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
150
- shininess_layout.addWidget(self.shininess_slider)
151
- layout.addLayout(shininess_layout)
152
-
153
- def _on_color(self, color):
154
- try:
155
- self.color = color
156
- self.flat_rgb = text_to_rgba(color, flat=True)
157
- self.need_update_setting = True
158
- except ValueError:
159
- pass
160
-
161
-
162
- def update_wireframe(self, value):
163
- self.wireframe = value
164
-
165
- def update_enable_lighting(self, value):
166
- self.enable_lighting = value
167
- self.need_update_setting = True
168
-
169
- def update_line_width(self, value):
170
- self.line_width = value
171
- self.need_update_setting = True
172
-
173
- def update_ambient_strength(self, value):
174
- self.ambient_strength = value
175
- self.need_update_setting = True
176
-
177
- def update_diffuse_strength(self, value):
178
- self.diffuse_strength = value
179
- self.need_update_setting = True
180
-
181
- def update_specular_strength(self, value):
182
- self.specular_strength = value
183
- self.need_update_setting = True
184
-
185
- def update_shininess(self, value):
186
- self.shininess = value
187
- self.need_update_setting = True
188
-
189
- def update_alpha(self, value):
190
- """Update mesh alpha (opacity)"""
191
- self.alpha = float(value)
192
- self.need_update_setting = True
193
-
194
- def set_data(self, verts, faces):
195
- """
196
- verts: np.ndarray of shape (N, 3)
197
- faces: np.ndarray of shape (M, 3) with uint32 indices
198
- """
199
- verts = np.asarray(verts, dtype=np.float32)
200
- faces = np.asarray(faces, dtype=np.uint32)
201
- triangles = verts[faces.flatten()]
202
-
203
- self.triangles = np.asarray(triangles, dtype=np.float32)
204
- self.normals = self.calculate_normals()
205
- self.need_update_buffer = True
206
-
207
- def calculate_normals(self):
208
- if self.triangles is None or len(self.triangles) == 0:
209
- return None
210
-
211
- # Ensure we have complete triangles
212
- num_vertices = len(self.triangles)
213
- num_triangles = num_vertices // 3
214
- if num_triangles == 0:
215
- return None
216
-
217
- # Reshape vertices into triangles (N, 3, 3) where N is number of triangles
218
- vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
219
-
220
- v0 = vertices_reshaped[:, 0, :]
221
- v1 = vertices_reshaped[:, 1, :]
222
- v2 = vertices_reshaped[:, 2, :]
223
-
224
- # Calculate edges for all triangles at once
225
- edge1 = v1 - v0
226
- edge2 = v2 - v0
227
-
228
- face_normals = np.cross(edge1, edge2)
229
-
230
- norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
231
- norms[norms < 1e-6] = 1.0
232
- face_normals = face_normals / norms
233
-
234
- normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
235
- normals = normals_per_vertex.reshape(-1, 3)
236
- return normals.astype(np.float32)
237
-
238
- def initialize_gl(self):
239
- """OpenGL initialization"""
240
- vertex_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
241
- fragment_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
242
-
243
- program = shaders.compileProgram(
244
- shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
245
- shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
246
- )
247
- self.program = program
248
-
249
- def update_render_buffer(self):
250
- """Initialize OpenGL buffers"""
251
- if not self.need_update_buffer:
252
- return
253
-
254
- # Generate VAO and VBOs
255
- if self.vao is None:
256
- self.vao = glGenVertexArrays(1)
257
- self.vbo_vertices = glGenBuffers(1)
258
- self.vbo_normals = glGenBuffers(1)
259
- # self.vbo_colors = glGenBuffers(1)
260
-
261
- glBindVertexArray(self.vao)
262
-
263
- # Vertex buffer
264
- glBindBuffer(GL_ARRAY_BUFFER, self.vbo_vertices)
265
- glBufferData(GL_ARRAY_BUFFER, self.triangles.nbytes, self.triangles, GL_STATIC_DRAW)
266
- glEnableVertexAttribArray(0)
267
- glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
268
-
269
- # Normal buffer
270
- if self.normals is not None:
271
- glBindBuffer(GL_ARRAY_BUFFER, self.vbo_normals)
272
- glBufferData(GL_ARRAY_BUFFER, self.normals.nbytes, self.normals, GL_STATIC_DRAW)
273
- glEnableVertexAttribArray(1)
274
- glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)
275
-
276
- # Color buffer
277
- # if self.flat_rgbs is not None:
278
- # glBindBuffer(GL_ARRAY_BUFFER, self.vbo_colors)
279
- # glBufferData(GL_ARRAY_BUFFER, self.flat_rgbs.nbytes, self.flat_rgbs, GL_STATIC_DRAW)
280
- # glEnableVertexAttribArray(2)
281
- # glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, None)
282
- #
283
- glBindVertexArray(0)
284
- self.need_update_buffer = False
285
-
286
- def update_setting(self):
287
- if (self.need_update_setting is False):
288
- return
289
- set_uniform(self.program, int(self.enable_lighting), 'if_light')
290
- set_uniform(self.program, 1, 'two_sided')
291
- set_uniform(self.program, np.array(self.light_color), 'light_color')
292
- set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
293
- set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
294
- set_uniform(self.program, float(self.specular_strength), 'specular_strength')
295
- set_uniform(self.program, float(self.shininess), 'shininess')
296
- set_uniform(self.program, float(self.alpha), 'alpha')
297
- set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
298
- self.need_update_setting = False
299
-
300
- def paint(self):
301
- """Render the mesh using modern OpenGL with shaders"""
302
- if self.triangles is None or len(self.triangles) == 0:
303
- return
304
- glUseProgram(self.program)
305
- self.update_render_buffer()
306
- self.update_setting()
307
- view_matrix = self.glwidget().view_matrix
308
- set_uniform(self.program, view_matrix, 'view')
309
- project_matrix = self.glwidget().projection_matrix
310
- set_uniform(self.program, project_matrix, 'projection')
311
- view_pos = self.glwidget().center
312
- set_uniform(self.program, np.array(view_pos), 'view_pos')
313
-
314
-
315
- # Enable blending and depth testing
316
- glEnable(GL_BLEND)
317
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
318
- glEnable(GL_DEPTH_TEST)
319
- glDisable(GL_CULL_FACE) # two-sided rendering
320
-
321
- # Set line width
322
- glLineWidth(self.line_width)
323
-
324
- # Bind VAO and render
325
- glBindVertexArray(self.vao)
326
-
327
- if len(self.triangles) > 0:
328
- # Render faces
329
- if self.wireframe:
330
- glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
331
- else:
332
- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
333
-
334
- # Draw triangles
335
- glDrawArrays(GL_TRIANGLES, 0, len(self.triangles))
336
- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
337
-
338
- glBindVertexArray(0)
339
- glDisable(GL_DEPTH_TEST)
340
- glDisable(GL_BLEND)
341
- glUseProgram(0)
342
-
File without changes
File without changes
File without changes