q3dviewer 1.2.2__py3-none-any.whl → 1.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- q3dviewer/custom_items/mesh_item.py +234 -217
- q3dviewer/glwidget.py +26 -1
- q3dviewer/shaders/cloud_vert.glsl +1 -1
- q3dviewer/shaders/mesh_geom.glsl +83 -0
- q3dviewer/shaders/mesh_vert.glsl +28 -56
- q3dviewer/tools/cloud_viewer.py +65 -56
- q3dviewer/utils/cloud_io.py +46 -19
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/METADATA +1 -2
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/RECORD +13 -12
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/LICENSE +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/WHEEL +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/entry_points.txt +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.3.dist-info}/top_level.txt +0 -0
|
@@ -10,148 +10,75 @@ import numpy as np
|
|
|
10
10
|
from q3dviewer.base_item import BaseItem
|
|
11
11
|
from OpenGL.GL import *
|
|
12
12
|
from OpenGL.GL import shaders
|
|
13
|
-
from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
|
|
14
|
-
|
|
13
|
+
from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
|
|
14
|
+
|
|
15
15
|
import os
|
|
16
16
|
from q3dviewer.utils import set_uniform, text_to_rgba
|
|
17
|
+
import time
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class MeshItem(BaseItem):
|
|
21
22
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
A OpenGL mesh item for rendering 3D triangular meshes.
|
|
24
24
|
Attributes:
|
|
25
|
-
color (str or tuple):
|
|
26
|
-
|
|
27
|
-
wireframe (bool): If True, render the mesh in wireframe mode (edges only).
|
|
28
|
-
If False, render filled triangles. Default is False.
|
|
29
|
-
enable_lighting (bool): Whether to enable Phong lighting for the mesh.
|
|
30
|
-
If True, the mesh will be shaded based on light direction and material properties.
|
|
31
|
-
If False, the mesh will use flat shading with object colors only. Default is True.
|
|
32
|
-
color_mode (str): The coloring mode for mesh vertices.
|
|
33
|
-
- 'FLAT': Single flat color for all vertices (uses the `color` attribute).
|
|
34
|
-
- 'I': Color by intensity channel from per-vertex colors (rainbow gradient).
|
|
35
|
-
- 'RGB': Per-vertex RGB color from per-vertex color data.
|
|
36
|
-
alpha (float): The transparency of the mesh, in the range [0, 1],
|
|
37
|
-
where 0 is fully transparent and 1 is fully opaque. Default is 1.0.
|
|
38
|
-
line_width (float): The width of lines when rendering in wireframe mode.
|
|
39
|
-
Range is typically 0.5 to 5.0. Default is 1.0.
|
|
40
|
-
|
|
41
|
-
Material Properties (Phong Lighting):
|
|
42
|
-
ambient_strength (float): Ambient light contribution [0.0-1.0]. Default is 0.1.
|
|
43
|
-
diffuse_strength (float): Diffuse light contribution [0.0-2.0]. Default is 1.2.
|
|
44
|
-
specular_strength (float): Specular highlight contribution [0.0-2.0]. Default is 0.1.
|
|
45
|
-
shininess (float): Specular shininess exponent [1-256]. Higher values = smaller highlights. Default is 32.0.
|
|
46
|
-
|
|
47
|
-
Methods:
|
|
48
|
-
set_data(verts, faces, colors=None): Set mesh geometry and optional per-vertex colors.
|
|
49
|
-
- verts: np.ndarray of shape (N, 3) - vertex positions
|
|
50
|
-
- faces: np.ndarray of shape (M, 3) with uint32 indices - triangle indices
|
|
51
|
-
- colors: np.ndarray of shape (N,) with uint32 IRGB format (optional)
|
|
52
|
-
uint32 format: I (bits 24-31), R (bits 16-23), G (bits 8-15), B (bits 0-7)
|
|
53
|
-
|
|
54
|
-
Example:
|
|
55
|
-
# Create a simple triangle mesh with per-vertex colors
|
|
56
|
-
verts = np.array([[0,0,0], [1,0,0], [0,1,0]], dtype=np.float32)
|
|
57
|
-
faces = np.array([[0,1,2]], dtype=np.uint32)
|
|
58
|
-
colors = np.array([
|
|
59
|
-
(255 << 24) | (255 << 16) | (0 << 8) | 0, # Red, intensity=255
|
|
60
|
-
(200 << 24) | (0 << 16) | (255 << 8) | 0, # Green, intensity=200
|
|
61
|
-
(150 << 24) | (0 << 16) | (0 << 8) | 255 # Blue, intensity=150
|
|
62
|
-
], dtype=np.uint32)
|
|
63
|
-
|
|
64
|
-
mesh = q3d.MeshItem(color='lightblue', color_mode='RGB', enable_lighting=True)
|
|
65
|
-
mesh.set_data(verts, faces, colors)
|
|
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.
|
|
66
27
|
"""
|
|
67
|
-
def __init__(self, color='lightblue', wireframe=False
|
|
28
|
+
def __init__(self, color='lightblue', wireframe=False):
|
|
68
29
|
super(MeshItem, self).__init__()
|
|
30
|
+
self.wireframe = wireframe
|
|
69
31
|
self.color = color
|
|
70
32
|
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
71
|
-
self.wireframe = wireframe
|
|
72
|
-
self.enable_lighting = enable_lighting
|
|
73
33
|
|
|
74
|
-
#
|
|
75
|
-
self.
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
43
|
|
|
79
|
-
|
|
80
|
-
self.
|
|
81
|
-
self.vmin = 0
|
|
82
|
-
self.vmax = 255
|
|
44
|
+
# key2index: mapping from face_key to face buffer index
|
|
45
|
+
self.key2index = {} # {face_key: face_index}
|
|
83
46
|
|
|
84
47
|
# OpenGL objects
|
|
85
48
|
self.vao = None
|
|
86
|
-
self.
|
|
87
|
-
self.vbo_normals = None
|
|
88
|
-
self.vbo_colors = None
|
|
49
|
+
self.vbo = None
|
|
89
50
|
self.program = None
|
|
51
|
+
self._gpu_face_capacity = 0 # Track GPU buffer capacity
|
|
90
52
|
|
|
91
|
-
#
|
|
53
|
+
# Fixed rendering parameters (not adjustable via UI)
|
|
54
|
+
self.enable_lighting = True
|
|
92
55
|
self.line_width = 1.0
|
|
93
56
|
self.light_pos = [1.0, 1.0, 1.0]
|
|
94
57
|
self.light_color = [1.0, 1.0, 1.0]
|
|
95
|
-
|
|
96
|
-
# Phong lighting material properties
|
|
97
58
|
self.ambient_strength = 0.1
|
|
98
59
|
self.diffuse_strength = 1.2
|
|
99
60
|
self.specular_strength = 0.1
|
|
100
61
|
self.shininess = 32.0
|
|
101
|
-
# Alpha (opacity)
|
|
102
62
|
self.alpha = 1.0
|
|
103
63
|
|
|
104
|
-
#
|
|
105
|
-
self.need_update_buffer = True
|
|
64
|
+
# Settings flag
|
|
106
65
|
self.need_update_setting = True
|
|
107
66
|
self.path = os.path.dirname(__file__)
|
|
108
67
|
|
|
109
68
|
|
|
110
69
|
def add_setting(self, layout):
|
|
111
70
|
"""Add UI controls for mesh visualization"""
|
|
112
|
-
#
|
|
71
|
+
# Only keep wireframe toggle - all other parameters are fixed
|
|
113
72
|
self.wireframe_box = QCheckBox("Wireframe Mode")
|
|
114
73
|
self.wireframe_box.setChecked(self.wireframe)
|
|
115
74
|
self.wireframe_box.toggled.connect(self.update_wireframe)
|
|
116
75
|
layout.addWidget(self.wireframe_box)
|
|
117
|
-
|
|
76
|
+
|
|
118
77
|
# Enable lighting toggle
|
|
119
78
|
self.lighting_box = QCheckBox("Enable Lighting")
|
|
120
79
|
self.lighting_box.setChecked(self.enable_lighting)
|
|
121
80
|
self.lighting_box.toggled.connect(self.update_enable_lighting)
|
|
122
81
|
layout.addWidget(self.lighting_box)
|
|
123
|
-
|
|
124
|
-
# Line width control
|
|
125
|
-
line_width_label = QLabel("Line Width:")
|
|
126
|
-
layout.addWidget(line_width_label)
|
|
127
|
-
self.line_width_box = QDoubleSpinBox()
|
|
128
|
-
self.line_width_box.setRange(0.5, 5.0)
|
|
129
|
-
self.line_width_box.setSingleStep(0.5)
|
|
130
|
-
self.line_width_box.setValue(self.line_width)
|
|
131
|
-
self.line_width_box.valueChanged.connect(self.update_line_width)
|
|
132
|
-
layout.addWidget(self.line_width_box)
|
|
133
|
-
|
|
134
|
-
# Alpha control
|
|
135
|
-
alpha_label = QLabel("Alpha:")
|
|
136
|
-
layout.addWidget(alpha_label)
|
|
137
|
-
alpha_box = QDoubleSpinBox()
|
|
138
|
-
alpha_box.setRange(0.0, 1.0)
|
|
139
|
-
alpha_box.setSingleStep(0.05)
|
|
140
|
-
alpha_box.setValue(self.alpha)
|
|
141
|
-
alpha_box.valueChanged.connect(self.update_alpha)
|
|
142
|
-
layout.addWidget(alpha_box)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# Color mode selection
|
|
146
|
-
label_color = QLabel("Color Mode:")
|
|
147
|
-
layout.addWidget(label_color)
|
|
148
|
-
self.combo_color = QComboBox()
|
|
149
|
-
self.combo_color.addItem("flat color")
|
|
150
|
-
self.combo_color.addItem("intensity")
|
|
151
|
-
self.combo_color.addItem("RGB")
|
|
152
|
-
self.combo_color.setCurrentIndex(self.color_mode)
|
|
153
|
-
self.combo_color.currentIndexChanged.connect(self._on_color_mode)
|
|
154
|
-
layout.addWidget(self.combo_color)
|
|
155
82
|
|
|
156
83
|
label_rgb = QLabel("Color:")
|
|
157
84
|
label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
@@ -162,7 +89,6 @@ class MeshItem(BaseItem):
|
|
|
162
89
|
self.edit_rgb.textChanged.connect(self._on_color)
|
|
163
90
|
layout.addWidget(self.edit_rgb)
|
|
164
91
|
|
|
165
|
-
|
|
166
92
|
# Material property controls for Phong lighting
|
|
167
93
|
if self.enable_lighting:
|
|
168
94
|
# Ambient strength control (slider 0-100 mapped to 0.0-1.0)
|
|
@@ -221,12 +147,6 @@ class MeshItem(BaseItem):
|
|
|
221
147
|
except ValueError:
|
|
222
148
|
pass
|
|
223
149
|
|
|
224
|
-
def _on_color_mode(self, index):
|
|
225
|
-
print(f"Color mode1 : {index}")
|
|
226
|
-
self.color_mode = index
|
|
227
|
-
self.edit_rgb.setVisible(index == self.mode_table['FLAT'])
|
|
228
|
-
self.need_update_setting = True
|
|
229
|
-
|
|
230
150
|
def update_wireframe(self, value):
|
|
231
151
|
self.wireframe = value
|
|
232
152
|
|
|
@@ -258,135 +178,233 @@ class MeshItem(BaseItem):
|
|
|
258
178
|
"""Update mesh alpha (opacity)"""
|
|
259
179
|
self.alpha = float(value)
|
|
260
180
|
self.need_update_setting = True
|
|
261
|
-
|
|
262
|
-
def set_data(self,
|
|
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):
|
|
263
226
|
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
268
238
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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)
|
|
278
259
|
else:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
# Reshape vertices into triangles (N, 3, 3) where N is number of triangles
|
|
298
|
-
vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
|
|
299
|
-
|
|
300
|
-
v0 = vertices_reshaped[:, 0, :]
|
|
301
|
-
v1 = vertices_reshaped[:, 1, :]
|
|
302
|
-
v2 = vertices_reshaped[:, 2, :]
|
|
303
|
-
|
|
304
|
-
# Calculate edges for all triangles at once
|
|
305
|
-
edge1 = v1 - v0
|
|
306
|
-
edge2 = v2 - v0
|
|
307
|
-
|
|
308
|
-
face_normals = np.cross(edge1, edge2)
|
|
309
|
-
|
|
310
|
-
norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
|
|
311
|
-
norms[norms < 1e-6] = 1.0
|
|
312
|
-
face_normals = face_normals / norms
|
|
313
|
-
|
|
314
|
-
normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
|
|
315
|
-
normals = normals_per_vertex.reshape(-1, 3)
|
|
316
|
-
return normals.astype(np.float32)
|
|
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
|
|
317
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
|
+
|
|
318
295
|
def initialize_gl(self):
|
|
319
296
|
"""OpenGL initialization"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
shaders.
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
328
310
|
|
|
329
311
|
def update_render_buffer(self):
|
|
330
|
-
"""
|
|
331
|
-
|
|
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:
|
|
332
319
|
return
|
|
333
|
-
|
|
334
|
-
#
|
|
320
|
+
|
|
321
|
+
# Initialize buffers on first call
|
|
335
322
|
if self.vao is None:
|
|
336
323
|
self.vao = glGenVertexArrays(1)
|
|
337
|
-
self.
|
|
338
|
-
self.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
353
341
|
glEnableVertexAttribArray(1)
|
|
354
|
-
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
glBindBuffer(GL_ARRAY_BUFFER, self.vbo_colors)
|
|
359
|
-
glBufferData(GL_ARRAY_BUFFER, self.vertex_colors.nbytes, self.vertex_colors, GL_STATIC_DRAW)
|
|
342
|
+
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(0))
|
|
343
|
+
glVertexAttribDivisor(1, 1)
|
|
344
|
+
|
|
345
|
+
# v1 (location 2) - vec3
|
|
360
346
|
glEnableVertexAttribArray(2)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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)
|
|
365
376
|
|
|
366
377
|
def update_setting(self):
|
|
367
|
-
|
|
378
|
+
"""Set fixed rendering parameters (called once during initialization)"""
|
|
379
|
+
if not self.need_update_setting:
|
|
368
380
|
return
|
|
381
|
+
# Set fixed uniforms for instanced shaders
|
|
369
382
|
set_uniform(self.program, int(self.enable_lighting), 'if_light')
|
|
370
383
|
set_uniform(self.program, 1, 'two_sided')
|
|
371
|
-
|
|
384
|
+
|
|
385
|
+
set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
|
|
372
386
|
set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
|
|
373
387
|
set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
|
|
374
388
|
set_uniform(self.program, float(self.specular_strength), 'specular_strength')
|
|
375
389
|
set_uniform(self.program, float(self.shininess), 'shininess')
|
|
376
390
|
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
377
391
|
set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
|
|
378
|
-
set_uniform(self.program, int(self.color_mode), 'color_mode')
|
|
379
|
-
set_uniform(self.program, float(self.vmin), 'vmin')
|
|
380
|
-
set_uniform(self.program, float(self.vmax), 'vmax')
|
|
381
392
|
self.need_update_setting = False
|
|
382
393
|
|
|
383
394
|
def paint(self):
|
|
384
|
-
"""
|
|
385
|
-
|
|
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:
|
|
386
401
|
return
|
|
402
|
+
|
|
387
403
|
glUseProgram(self.program)
|
|
404
|
+
|
|
388
405
|
self.update_render_buffer()
|
|
389
406
|
self.update_setting()
|
|
407
|
+
|
|
390
408
|
view_matrix = self.glwidget().view_matrix
|
|
391
409
|
set_uniform(self.program, view_matrix, 'view')
|
|
392
410
|
project_matrix = self.glwidget().projection_matrix
|
|
@@ -394,30 +412,29 @@ class MeshItem(BaseItem):
|
|
|
394
412
|
view_pos = self.glwidget().center
|
|
395
413
|
set_uniform(self.program, np.array(view_pos), 'view_pos')
|
|
396
414
|
|
|
397
|
-
|
|
398
415
|
# Enable blending and depth testing
|
|
399
416
|
glEnable(GL_BLEND)
|
|
400
417
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
401
418
|
glEnable(GL_DEPTH_TEST)
|
|
402
419
|
glDisable(GL_CULL_FACE) # two-sided rendering
|
|
403
|
-
|
|
420
|
+
|
|
404
421
|
# Set line width
|
|
405
422
|
glLineWidth(self.line_width)
|
|
406
423
|
|
|
407
|
-
# Bind VAO
|
|
424
|
+
# Bind VAO (vertex positions are now in VBO attributes)
|
|
408
425
|
glBindVertexArray(self.vao)
|
|
409
|
-
|
|
410
|
-
if len(self.triangles) > 0:
|
|
411
|
-
# Render faces
|
|
412
|
-
if self.wireframe:
|
|
413
|
-
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
414
|
-
else:
|
|
415
|
-
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
416
426
|
|
|
417
|
-
|
|
418
|
-
|
|
427
|
+
if self.wireframe:
|
|
428
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
429
|
+
else:
|
|
419
430
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
420
|
-
|
|
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)
|
|
421
438
|
glBindVertexArray(0)
|
|
422
439
|
glDisable(GL_DEPTH_TEST)
|
|
423
440
|
glDisable(GL_BLEND)
|
q3dviewer/glwidget.py
CHANGED
|
@@ -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.
|
|
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):
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#version 430 core
|
|
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
|
+
layout(points) in;
|
|
9
|
+
layout(triangle_strip, max_vertices = 6) out;
|
|
10
|
+
|
|
11
|
+
// Input from vertex shader
|
|
12
|
+
in VS_OUT {
|
|
13
|
+
vec3 v0, v1, v2, v3;
|
|
14
|
+
float good;
|
|
15
|
+
} gs_in[];
|
|
16
|
+
|
|
17
|
+
// Uniforms
|
|
18
|
+
uniform mat4 view;
|
|
19
|
+
uniform mat4 projection;
|
|
20
|
+
|
|
21
|
+
uniform int flat_rgb;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// Output to fragment shader
|
|
25
|
+
out vec3 FragPos;
|
|
26
|
+
out vec3 Normal;
|
|
27
|
+
out vec3 objectColor;
|
|
28
|
+
|
|
29
|
+
// Calculate normal from three vertices
|
|
30
|
+
vec3 calculateNormal(vec3 a, vec3 b, vec3 c) {
|
|
31
|
+
vec3 edge1 = b - a;
|
|
32
|
+
vec3 edge2 = c - a;
|
|
33
|
+
return normalize(cross(edge1, edge2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
void emitVertex(vec3 pos, vec3 normal, vec3 color) {
|
|
37
|
+
FragPos = pos;
|
|
38
|
+
Normal = normal;
|
|
39
|
+
objectColor = color;
|
|
40
|
+
gl_Position = projection * view * vec4(pos, 1.0);
|
|
41
|
+
EmitVertex();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void main()
|
|
45
|
+
{
|
|
46
|
+
// Discard if face is not good
|
|
47
|
+
if (gs_in[0].good != 1.0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
vec3 v0 = gs_in[0].v0;
|
|
52
|
+
vec3 v1 = gs_in[0].v1;
|
|
53
|
+
vec3 v2 = gs_in[0].v2;
|
|
54
|
+
vec3 v3 = gs_in[0].v3;
|
|
55
|
+
|
|
56
|
+
float eps = 0.0001;
|
|
57
|
+
|
|
58
|
+
// Use default light blue color
|
|
59
|
+
vec3 color = vec3(
|
|
60
|
+
float((uint(flat_rgb) & uint(0x00FF0000)) >> 16)/255.,
|
|
61
|
+
float((uint(flat_rgb) & uint(0x0000FF00)) >> 8)/255.,
|
|
62
|
+
float( uint(flat_rgb) & uint(0x000000FF))/255.
|
|
63
|
+
);
|
|
64
|
+
// Triangle 1: (v0, v1, v2)
|
|
65
|
+
vec3 normal1 = calculateNormal(v0, v1, v2);
|
|
66
|
+
// Skip degenerate triangles
|
|
67
|
+
if (length(normal1) > eps) {
|
|
68
|
+
emitVertex(v0, normal1, color);
|
|
69
|
+
emitVertex(v1, normal1, color);
|
|
70
|
+
emitVertex(v2, normal1, color);
|
|
71
|
+
EndPrimitive();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Triangle 2: (v0, v1, v3)
|
|
75
|
+
vec3 normal2 = calculateNormal(v0, v1, v3);
|
|
76
|
+
// Skip degenerate triangles
|
|
77
|
+
if (length(normal2) > eps) {
|
|
78
|
+
emitVertex(v0, normal2, color);
|
|
79
|
+
emitVertex(v1, normal2, color);
|
|
80
|
+
emitVertex(v3, normal2, color);
|
|
81
|
+
EndPrimitive();
|
|
82
|
+
}
|
|
83
|
+
}
|
q3dviewer/shaders/mesh_vert.glsl
CHANGED
|
@@ -1,65 +1,37 @@
|
|
|
1
|
-
#version
|
|
2
|
-
layout (location = 0) in vec3 aPos;
|
|
3
|
-
layout (location = 1) in vec3 aNormal;
|
|
4
|
-
layout (location = 2) in uint aColor;
|
|
1
|
+
#version 430 core
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/*
|
|
4
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
5
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
6
|
+
*/
|
|
9
7
|
|
|
8
|
+
// Face attributes (per-instance) - vertex positions embedded in each face
|
|
9
|
+
layout(location = 1) in vec3 v0;
|
|
10
|
+
layout(location = 2) in vec3 v1;
|
|
11
|
+
layout(location = 3) in vec3 v2;
|
|
12
|
+
layout(location = 4) in vec3 v3;
|
|
13
|
+
layout(location = 5) in float good;
|
|
14
|
+
|
|
15
|
+
// Uniforms
|
|
10
16
|
uniform mat4 view;
|
|
11
17
|
uniform mat4 projection;
|
|
12
|
-
uniform int flat_rgb;
|
|
13
|
-
uniform int color_mode; // 0: FLAT, 1: Intensity, 2: RGB
|
|
14
|
-
uniform float vmin;
|
|
15
|
-
uniform float vmax;
|
|
16
|
-
|
|
17
|
-
vec3 getRainbowColor(uint value_raw) {
|
|
18
|
-
float range = vmax - vmin;
|
|
19
|
-
float value = 1.0 - (float(value_raw) - vmin) / range;
|
|
20
|
-
value = clamp(value, 0.0, 1.0);
|
|
21
|
-
float hue = value * 5.0 + 1.0;
|
|
22
|
-
int i = int(floor(hue));
|
|
23
|
-
float f = hue - float(i);
|
|
24
|
-
if (mod(i, 2) == 0) f = 1.0 - f;
|
|
25
|
-
float n = 1.0 - f;
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
else color = vec3(1.0, n, 0.0);
|
|
33
|
-
return color;
|
|
34
|
-
}
|
|
19
|
+
// Outputs to fragment shader (via geometry shader)
|
|
20
|
+
out VS_OUT {
|
|
21
|
+
vec3 v0, v1, v2, v3;
|
|
22
|
+
float good;
|
|
23
|
+
} vs_out;
|
|
35
24
|
|
|
36
25
|
void main()
|
|
37
26
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
// Pass vertex positions directly (no SSBO lookup needed)
|
|
28
|
+
vs_out.v0 = v0;
|
|
29
|
+
vs_out.v1 = v1;
|
|
30
|
+
vs_out.v2 = v2;
|
|
31
|
+
vs_out.v3 = v3;
|
|
32
|
+
vs_out.good = good;
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
c.x = float((uint(flat_rgb) & uint(0x00FF0000)) >> 16)/255.;
|
|
48
|
-
}
|
|
49
|
-
else if (color_mode == 1) {
|
|
50
|
-
// Intensity: use intensity channel (bits 24-31) for rainbow color
|
|
51
|
-
uint intensity = aColor >> 24;
|
|
52
|
-
c = getRainbowColor(intensity);
|
|
53
|
-
}
|
|
54
|
-
else if (color_mode == 2) {
|
|
55
|
-
// RGB: use RGB channels (bits 0-23)
|
|
56
|
-
c.z = float(aColor & uint(0x000000FF))/255.;
|
|
57
|
-
c.y = float((aColor & uint(0x0000FF00)) >> 8)/255.;
|
|
58
|
-
c.x = float((aColor & uint(0x00FF0000)) >> 16)/255.;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
objectColor = c;
|
|
62
|
-
|
|
63
|
-
// Final vertex position (apply view/projection)
|
|
64
|
-
gl_Position = projection * view * vec4(aPos, 1.0);
|
|
65
|
-
}
|
|
34
|
+
// Output dummy point (geometry shader will generate triangles)
|
|
35
|
+
// Note: This position is not used; geometry shader generates actual triangles
|
|
36
|
+
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
37
|
+
}
|
q3dviewer/tools/cloud_viewer.py
CHANGED
|
@@ -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,
|
|
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
|
|
17
|
+
self.setWindowTitle("Loading")
|
|
20
18
|
self.setModal(True)
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
19
|
+
self.setMinimumWidth(400)
|
|
20
|
+
self.label = QLabel(self)
|
|
21
|
+
self.label.setAlignment(Qt.AlignCenter)
|
|
23
22
|
layout = QVBoxLayout()
|
|
24
|
-
layout.addWidget(self.
|
|
25
|
-
layout.addWidget(self.progress_bar)
|
|
23
|
+
layout.addWidget(self.label)
|
|
26
24
|
self.setLayout(layout)
|
|
27
25
|
|
|
28
|
-
def
|
|
29
|
-
|
|
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,25 +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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
color = np.zeros((verts.shape[0],), dtype=np.uint32)
|
|
63
|
-
# random uint32 colors in IRGB format
|
|
64
|
-
color = np.random.randint(0, 0xFFFFFFFF, size=(verts.shape[0],), dtype=np.uint32)
|
|
65
|
-
mesh_item.set_data(verts=verts, faces=faces, colors=color)
|
|
66
|
-
break
|
|
53
|
+
mesh = load_stl(file_path)
|
|
54
|
+
mesh_item.set_data(mesh)
|
|
67
55
|
else:
|
|
68
56
|
cloud = cloud_item.load(file_path, append=(i > 0))
|
|
69
57
|
center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
|
|
70
58
|
self.viewer.glwidget.set_cam_position(center=center)
|
|
71
|
-
self.progress.emit(int((i + 1) / len(self.files) * 100))
|
|
72
59
|
self.finished.emit()
|
|
73
60
|
|
|
74
61
|
|
|
@@ -135,20 +122,19 @@ class CloudViewer(q3d.Viewer):
|
|
|
135
122
|
"""
|
|
136
123
|
Overwrite the drop event to open the cloud file.
|
|
137
124
|
"""
|
|
138
|
-
self.
|
|
139
|
-
self.
|
|
125
|
+
self.progress_window = ProgressWindow(self)
|
|
126
|
+
self.progress_window.show()
|
|
140
127
|
files = event.mimeData().urls()
|
|
141
128
|
self.progress_thread = FileLoaderThread(self, files)
|
|
142
|
-
self['cloud'].load(files[0].toLocalFile(), append=False)
|
|
143
129
|
self.progress_thread.progress.connect(self.file_loading_progress)
|
|
144
130
|
self.progress_thread.finished.connect(self.file_loading_finished)
|
|
145
131
|
self.progress_thread.start()
|
|
146
132
|
|
|
147
|
-
def file_loading_progress(self,
|
|
148
|
-
self.
|
|
133
|
+
def file_loading_progress(self, current, total, file_name):
|
|
134
|
+
self.progress_window.update_progress(current, total, file_name)
|
|
149
135
|
|
|
150
136
|
def file_loading_finished(self):
|
|
151
|
-
self.
|
|
137
|
+
self.progress_window.close()
|
|
152
138
|
|
|
153
139
|
def open_cloud_file(self, file, append=False):
|
|
154
140
|
cloud_item = self['cloud']
|
|
@@ -159,27 +145,50 @@ class CloudViewer(q3d.Viewer):
|
|
|
159
145
|
center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
|
|
160
146
|
self.glwidget.set_cam_position(center=center)
|
|
161
147
|
|
|
162
|
-
# print a quick help message
|
|
148
|
+
# print a quick help message using rich
|
|
163
149
|
def print_help():
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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]")
|
|
169
186
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{GREEN}• Measure distance between points:{END}
|
|
176
|
-
{BLUE}- Hold Ctrl and left-click to select points on the cloud.{END}
|
|
177
|
-
{BLUE}- Hold Ctrl and right-click to remove the last selected point.{END}
|
|
178
|
-
{BLUE}- The total distance between selected points will be displayed.{END}
|
|
179
|
-
{GREEN}• Press 'M' to open the settings window.{END}
|
|
180
|
-
{BLUE}- Use the settings window to adjust item properties.{END}
|
|
181
|
-
"""
|
|
182
|
-
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()
|
|
183
192
|
|
|
184
193
|
def main():
|
|
185
194
|
print_help()
|
q3dviewer/utils/cloud_io.py
CHANGED
|
@@ -7,25 +7,52 @@ import numpy as np
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def load_stl(file_path):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
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.
|
|
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
|
|
@@ -17,7 +17,6 @@ Requires-Dist: laspy
|
|
|
17
17
|
Requires-Dist: matplotlib
|
|
18
18
|
Requires-Dist: meshio
|
|
19
19
|
Requires-Dist: numpy
|
|
20
|
-
Requires-Dist: numpy-stl
|
|
21
20
|
Requires-Dist: pye57
|
|
22
21
|
Requires-Dist: pypcd4
|
|
23
22
|
Requires-Dist: pyside6
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
q3dviewer/__init__.py,sha256=cjyfUE5zK6xohDGDQIWfb0DKkWChVznBd7CrVLg7whQ,376
|
|
2
2
|
q3dviewer/base_glwidget.py,sha256=QxAuZzQSBbzTwpHHqYpiM-Jqv41E4YJmFG4KRF-HruY,15274
|
|
3
3
|
q3dviewer/base_item.py,sha256=63MarHyoWszPL40ox-vPoOAQ1N4ypekOjoRARdPik-E,1755
|
|
4
|
-
q3dviewer/glwidget.py,sha256=
|
|
4
|
+
q3dviewer/glwidget.py,sha256=taewDTQUqmKOrDYz_ygghu00dlCnWzFRpDnEaTG25pA,6390
|
|
5
5
|
q3dviewer/viewer.py,sha256=Vq3ucDlBcBBoiVVGmqG1sRjhLePl50heblx6wJpsc1A,2603
|
|
6
6
|
q3dviewer/Qt/__init__.py,sha256=VJj7Ge6N_81__T9eHFl_YQpa1HyQrlLhMqC_9pUOYtc,2233
|
|
7
7
|
q3dviewer/custom_items/__init__.py,sha256=kaaf84wOObfybJ8a12FqPMeg8ImTJWggA6g5nvpY2YY,621
|
|
@@ -13,19 +13,20 @@ q3dviewer/custom_items/gaussian_item.py,sha256=JMubpahkTPh0E8ShL3FLTahv0e35ODzjg
|
|
|
13
13
|
q3dviewer/custom_items/grid_item.py,sha256=LDB_MYACoxld-xvz01_MfAf12vLcRkH7R_WtGHHdSgk,4945
|
|
14
14
|
q3dviewer/custom_items/image_item.py,sha256=k7HNTqdL2ckTbxMx7A7eKaP4aksZ85-pBjNdbpm6PXM,5355
|
|
15
15
|
q3dviewer/custom_items/line_item.py,sha256=rel-lx8AgjDY7qyIecHxHQZzaswRn2ZTiOIjB_0Mrqo,4444
|
|
16
|
-
q3dviewer/custom_items/mesh_item.py,sha256=
|
|
16
|
+
q3dviewer/custom_items/mesh_item.py,sha256=L6GzCcxiAkeYMLBUbbTvQgY0s-maCYQZqZqeMDtxuFE,17568
|
|
17
17
|
q3dviewer/custom_items/text3d_item.py,sha256=DYBPXnCmMEzWDE1y523YsWSl91taXAdu0kdnhUcwE4A,5524
|
|
18
18
|
q3dviewer/custom_items/text_item.py,sha256=toeGjBu7RtT8CMUuaDWnmXPnA1UKHhnCzUNeonGczSo,2703
|
|
19
19
|
q3dviewer/shaders/cloud_frag.glsl,sha256=psKVt9qI6BW0bCqOk4lcKqUd6XgYGtdFigyN9OdYSNI,609
|
|
20
|
-
q3dviewer/shaders/cloud_vert.glsl,sha256=
|
|
20
|
+
q3dviewer/shaders/cloud_vert.glsl,sha256=7tNkdx0iuoq1sG3YZ1UuF2JOhEohcodZ363WUV2B2Q4,2370
|
|
21
21
|
q3dviewer/shaders/gau_frag.glsl,sha256=vWt5I3Ojrc2PCxRlBJGyJhujbveSicMA54T01Fk293A,975
|
|
22
22
|
q3dviewer/shaders/gau_prep.glsl,sha256=0BiWhYCQGeX2iN-e7m3dy1xWXqWrErErRAzHlcmWHF0,7218
|
|
23
23
|
q3dviewer/shaders/gau_vert.glsl,sha256=_rkm51zaWgPDJ-otJL-WX12fDvnPBOTooVfqo21Rexs,1666
|
|
24
24
|
q3dviewer/shaders/mesh_frag.glsl,sha256=i9ljnO2kjLNGaR1TPQIK4-4iJ-JppJ5bCsOHg1730gQ,1997
|
|
25
|
-
q3dviewer/shaders/
|
|
25
|
+
q3dviewer/shaders/mesh_geom.glsl,sha256=HwNEZy7UAQm-PKEP-LnqHTlsSqZ5eqm49CYZSVylzXk,1964
|
|
26
|
+
q3dviewer/shaders/mesh_vert.glsl,sha256=bFg7HXesdVg7UaGDuZ4g7IpRdSKcfc6jzTcxLjNXkt8,968
|
|
26
27
|
q3dviewer/shaders/sort_by_key.glsl,sha256=M5RK6uRDp40vVH6XtBIrdJTcYatqXyZwd6kCzEa2DZg,1097
|
|
27
28
|
q3dviewer/tools/__init__.py,sha256=01wG7BGM6VX0QyFBKsqPmyf2e-vrmV_N3-mo-VQ1VBg,20
|
|
28
|
-
q3dviewer/tools/cloud_viewer.py,sha256=
|
|
29
|
+
q3dviewer/tools/cloud_viewer.py,sha256=wjgQNrn9IzHXd8V2Y45ANm12_YZRaV3uL7F8HkylrN0,8185
|
|
29
30
|
q3dviewer/tools/example_viewer.py,sha256=C867mLnCBjawS6LGgRsJ_c6-6wztfL9vOBQt85KbbdU,572
|
|
30
31
|
q3dviewer/tools/film_maker.py,sha256=xLFgRhFWoMQ37qlvcu1lXWaTWXMNRYlRcZFfHW5JtmQ,16676
|
|
31
32
|
q3dviewer/tools/gaussian_viewer.py,sha256=vIwWmiFhjNmknrEkBLzt2yiegeH7LP3OeNjnGM6GzaI,1633
|
|
@@ -33,15 +34,15 @@ q3dviewer/tools/lidar_calib.py,sha256=hHnsSaQh_Pkdh8tPntt0MgEW26nQyAdC_HQHq4I3sw
|
|
|
33
34
|
q3dviewer/tools/lidar_cam_calib.py,sha256=4CDcZZiFZDeKo2Y2_lXF9tfbiF9dPsz0OjppQdxQsU4,11430
|
|
34
35
|
q3dviewer/tools/ros_viewer.py,sha256=ARB3I5wohY3maP8dCu0O0hxObd6JFKuK2y7AApVgMWA,2551
|
|
35
36
|
q3dviewer/utils/__init__.py,sha256=dwTNAAebTiKY4ygv2G1O-w6-TbJnmnNVO2UfJXvJhaQ,107
|
|
36
|
-
q3dviewer/utils/cloud_io.py,sha256=
|
|
37
|
+
q3dviewer/utils/cloud_io.py,sha256=OLmVQWbrnGrlZHPz3zdoVn79r50JhM6V0zV-KwogEU8,13732
|
|
37
38
|
q3dviewer/utils/convert_ros_msg.py,sha256=lNbLIawJfwp3VzygdW3dUXkfSG8atg_CoZbQFmt8H70,3142
|
|
38
39
|
q3dviewer/utils/gl_helper.py,sha256=dRY_kUqyPMr7NTcupUr6_VTvgnj53iE2C0Lk0-oFYsI,1435
|
|
39
40
|
q3dviewer/utils/helpers.py,sha256=SqR4YTQZi13FKbkVUYgodXce1JJ_YmrHEIRkUmnIUas,3085
|
|
40
41
|
q3dviewer/utils/maths.py,sha256=zHaPtvVZIuo8xepIXCMeSL9tpx8FahUrq0l4K1oXrBk,8834
|
|
41
42
|
q3dviewer/utils/range_slider.py,sha256=Cs_xrwt6FCDVxGxan7r-ARd5ySwQ50xnCzcmz0dB_X0,4215
|
|
42
|
-
q3dviewer-1.2.
|
|
43
|
-
q3dviewer-1.2.
|
|
44
|
-
q3dviewer-1.2.
|
|
45
|
-
q3dviewer-1.2.
|
|
46
|
-
q3dviewer-1.2.
|
|
47
|
-
q3dviewer-1.2.
|
|
43
|
+
q3dviewer-1.2.3.dist-info/LICENSE,sha256=81cMOyNfw8KLb1JnPYngGHJ5W83gSbZEBU9MEP3tl-E,1124
|
|
44
|
+
q3dviewer-1.2.3.dist-info/METADATA,sha256=JjSE7g9HjmxRfl18Z79_EA90drP_EKhBVWuiMYk7yNM,8024
|
|
45
|
+
q3dviewer-1.2.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
46
|
+
q3dviewer-1.2.3.dist-info/entry_points.txt,sha256=EOjker7XYaBk70ffvNB_knPcfA33Bnlg21ZjEeM1EyI,362
|
|
47
|
+
q3dviewer-1.2.3.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
|
|
48
|
+
q3dviewer-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|