q3dviewer 1.2.3__tar.gz → 1.2.5__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.
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/PKG-INFO +24 -11
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/README.md +21 -9
- q3dviewer-1.2.5/q3dviewer/__init__.py +5 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/__init__.py +2 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/cloud_item.py +22 -20
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/mesh_item.py +155 -78
- q3dviewer-1.2.5/q3dviewer/custom_items/static_mesh_item.py +330 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/cloud_viewer.py +3 -2
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/film_maker.py +7 -8
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/helpers.py +12 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/PKG-INFO +24 -11
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/SOURCES.txt +1 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/setup.py +3 -1
- q3dviewer-1.2.3/q3dviewer/__init__.py +0 -14
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/Qt/__init__.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/base_glwidget.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/base_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/axis_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/cloud_io_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/frame_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/gaussian_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/grid_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/image_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/line_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/text3d_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/text_item.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/glwidget.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/__init__.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/example_viewer.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/gaussian_viewer.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/lidar_calib.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/lidar_cam_calib.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/ros_viewer.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/__init__.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/cloud_io.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/convert_ros_msg.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/gl_helper.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/maths.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/range_slider.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/viewer.py +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/dependency_links.txt +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/entry_points.txt +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/requires.txt +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/top_level.txt +0 -0
- {q3dviewer-1.2.3 → q3dviewer-1.2.5}/setup.cfg +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: q3dviewer
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
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
|
|
7
|
-
|
|
7
|
+
Author-email: liu.yang@jp.panasonic.com
|
|
8
|
+
License: MIT
|
|
8
9
|
Description:
|
|
9
10
|

|
|
10
11
|
|
|
@@ -58,22 +59,34 @@ Description:
|
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
**Basic Operations**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
📁 **Load Files** - Drag and drop files into the viewer
|
|
64
|
+
* Point clouds: .pcd, .ply, .las, .e57
|
|
65
|
+
* Mesh files: .stl
|
|
66
|
+
|
|
67
|
+
📏 **Measure Distance** - Interactive point measurement
|
|
68
|
+
* `Ctrl + Left Click`: Add measurement point
|
|
69
|
+
* `Ctrl + Right Click`: Remove last point
|
|
70
|
+
* Total distance displayed automatically
|
|
71
|
+
|
|
72
|
+
🎥 **Camera Controls** - Navigate the 3D scene
|
|
73
|
+
* `Double Click`: Set camera center to point
|
|
74
|
+
* `Right Drag`: Rotate view
|
|
75
|
+
* `Left Drag`: Pan view
|
|
76
|
+
* `Mouse Wheel`: Zoom in/out
|
|
77
|
+
|
|
78
|
+
⚙️ **Settings** - Press `M` to open settings window
|
|
79
|
+
* Adjust visualization properties
|
|
67
80
|
|
|
68
81
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
69
82
|
|
|
70
83
|
[Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
|
|
71
84
|
|
|
72
|
-

|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
**Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
|
|
75
88
|
|
|
76
|
-

|
|
77
90
|
|
|
78
91
|
### 2. ROS Viewer
|
|
79
92
|
|
|
@@ -51,22 +51,34 @@ python3 -m q3dviewer.tools.cloud_viewer
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
**Basic Operations**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
|
|
55
|
+
📁 **Load Files** - Drag and drop files into the viewer
|
|
56
|
+
* Point clouds: .pcd, .ply, .las, .e57
|
|
57
|
+
* Mesh files: .stl
|
|
58
|
+
|
|
59
|
+
📏 **Measure Distance** - Interactive point measurement
|
|
60
|
+
* `Ctrl + Left Click`: Add measurement point
|
|
61
|
+
* `Ctrl + Right Click`: Remove last point
|
|
62
|
+
* Total distance displayed automatically
|
|
63
|
+
|
|
64
|
+
🎥 **Camera Controls** - Navigate the 3D scene
|
|
65
|
+
* `Double Click`: Set camera center to point
|
|
66
|
+
* `Right Drag`: Rotate view
|
|
67
|
+
* `Left Drag`: Pan view
|
|
68
|
+
* `Mouse Wheel`: Zoom in/out
|
|
69
|
+
|
|
70
|
+
⚙️ **Settings** - Press `M` to open settings window
|
|
71
|
+
* Adjust visualization properties
|
|
60
72
|
|
|
61
73
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
62
74
|
|
|
63
75
|
[Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
|
|
64
76
|
|
|
65
|
-

|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
**Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
|
|
68
80
|
|
|
69
|
-

|
|
70
82
|
|
|
71
83
|
### 2. ROS Viewer
|
|
72
84
|
|
|
@@ -8,4 +8,6 @@ from q3dviewer.custom_items.text_item import Text2DItem
|
|
|
8
8
|
from q3dviewer.custom_items.image_item import ImageItem
|
|
9
9
|
from q3dviewer.custom_items.line_item import LineItem
|
|
10
10
|
from q3dviewer.custom_items.text3d_item import Text3DItem
|
|
11
|
+
from q3dviewer.custom_items.static_mesh_item import StaticMeshItem
|
|
11
12
|
from q3dviewer.custom_items.mesh_item import MeshItem
|
|
13
|
+
|
|
@@ -39,32 +39,34 @@ class CloudItem(BaseItem):
|
|
|
39
39
|
- 'SPHERE': Draw each point as a sphere in 3D space.
|
|
40
40
|
depth_test (bool): Whether to enable depth testing. If True, points closer to the camera will appear in front of farther ones.
|
|
41
41
|
"""
|
|
42
|
+
# Class-level constants
|
|
43
|
+
STRIDE = 16 # Stride of cloud array in bytes
|
|
44
|
+
CAPACITY = 10000000 # Initial buffer capacity (10M points)
|
|
45
|
+
DATA_TYPE = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
46
|
+
MODE_TABLE = {'FLAT': 0, 'I': 1, 'RGB': 2, 'GRAY': 3}
|
|
47
|
+
POINT_TYPE_TABLE = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
|
|
48
|
+
|
|
42
49
|
def __init__(self, size, alpha,
|
|
43
50
|
color_mode='I',
|
|
44
51
|
color='white',
|
|
45
52
|
point_type='PIXEL'):
|
|
46
53
|
super().__init__()
|
|
47
|
-
self.STRIDE = 16 # stride of cloud array
|
|
48
54
|
self.valid_buff_top = 0
|
|
49
55
|
self.add_buff_loc = 0
|
|
50
56
|
self.alpha = alpha
|
|
51
57
|
self.size = size
|
|
52
58
|
self.point_type = point_type
|
|
53
59
|
self.mutex = threading.Lock()
|
|
54
|
-
self.data_type = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
55
60
|
self.color = color
|
|
56
61
|
try:
|
|
57
62
|
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
58
63
|
except ValueError:
|
|
59
64
|
print(f"Invalid color: {color}, please use matplotlib color format")
|
|
60
65
|
exit(1)
|
|
61
|
-
self.
|
|
62
|
-
self.point_type_table = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
|
|
63
|
-
self.color_mode = self.mode_table[color_mode]
|
|
64
|
-
self.CAPACITY = 10000000 # 10MB * 3 (x,y,z, color) * 4
|
|
66
|
+
self.color_mode = self.MODE_TABLE[color_mode]
|
|
65
67
|
self.vmin = 0
|
|
66
68
|
self.vmax = 255
|
|
67
|
-
self.buff = np.empty((0), self.
|
|
69
|
+
self.buff = np.empty((0), self.DATA_TYPE)
|
|
68
70
|
self.wait_add_data = None
|
|
69
71
|
self.need_update_setting = True
|
|
70
72
|
self.max_cloud_size = 300000000
|
|
@@ -78,7 +80,7 @@ class CloudItem(BaseItem):
|
|
|
78
80
|
combo_ptype.addItem("pixels")
|
|
79
81
|
combo_ptype.addItem("flat squares")
|
|
80
82
|
combo_ptype.addItem("spheres")
|
|
81
|
-
combo_ptype.setCurrentIndex(self.
|
|
83
|
+
combo_ptype.setCurrentIndex(self.POINT_TYPE_TABLE[self.point_type])
|
|
82
84
|
combo_ptype.currentIndexChanged.connect(self._on_point_type_selection)
|
|
83
85
|
layout.addWidget(combo_ptype)
|
|
84
86
|
|
|
@@ -88,7 +90,7 @@ class CloudItem(BaseItem):
|
|
|
88
90
|
self.box_size.setValue(int(self.size))
|
|
89
91
|
self.box_size.setRange(0, 100)
|
|
90
92
|
self.box_size.valueChanged.connect(self.set_size)
|
|
91
|
-
self._on_point_type_selection(self.
|
|
93
|
+
self._on_point_type_selection(self.POINT_TYPE_TABLE[self.point_type])
|
|
92
94
|
layout.addWidget(self.box_size)
|
|
93
95
|
|
|
94
96
|
box_alpha = QDoubleSpinBox()
|
|
@@ -133,11 +135,11 @@ class CloudItem(BaseItem):
|
|
|
133
135
|
self.color_mode = index
|
|
134
136
|
self.edit_rgb.hide()
|
|
135
137
|
self.slider_v.hide()
|
|
136
|
-
if (index == self.
|
|
138
|
+
if (index == self.MODE_TABLE['FLAT']): # flat color
|
|
137
139
|
self.edit_rgb.show()
|
|
138
|
-
elif (index == self.
|
|
140
|
+
elif (index == self.MODE_TABLE['I']): # intensity
|
|
139
141
|
self.slider_v.show()
|
|
140
|
-
elif (index == self.
|
|
142
|
+
elif (index == self.MODE_TABLE['GRAY']): # grayscale
|
|
141
143
|
self.slider_v.show()
|
|
142
144
|
|
|
143
145
|
self.need_update_setting = True
|
|
@@ -145,15 +147,15 @@ class CloudItem(BaseItem):
|
|
|
145
147
|
def set_color_mode(self, color_mode):
|
|
146
148
|
if color_mode in {'FLAT', 'RGB', 'I', 'GRAY'}:
|
|
147
149
|
try:
|
|
148
|
-
self.combo_color.setCurrentIndex(self.
|
|
150
|
+
self.combo_color.setCurrentIndex(self.MODE_TABLE[color_mode])
|
|
149
151
|
except:
|
|
150
|
-
self.color_mode = self.
|
|
152
|
+
self.color_mode = self.MODE_TABLE[color_mode]
|
|
151
153
|
self.need_update_setting = True
|
|
152
154
|
else:
|
|
153
155
|
print(f"Invalid color mode: {color_mode}")
|
|
154
156
|
|
|
155
157
|
def _on_point_type_selection(self, index):
|
|
156
|
-
self.point_type = list(self.
|
|
158
|
+
self.point_type = list(self.POINT_TYPE_TABLE.keys())[index]
|
|
157
159
|
if self.point_type == 'PIXEL':
|
|
158
160
|
self.box_size.setPrefix("Set size (pixel): ")
|
|
159
161
|
else:
|
|
@@ -184,7 +186,7 @@ class CloudItem(BaseItem):
|
|
|
184
186
|
self.need_update_setting = True
|
|
185
187
|
|
|
186
188
|
def clear(self):
|
|
187
|
-
data = np.empty((0), self.
|
|
189
|
+
data = np.empty((0), self.DATA_TYPE)
|
|
188
190
|
self.set_data(data)
|
|
189
191
|
|
|
190
192
|
def set_data(self, data, append=False):
|
|
@@ -193,7 +195,7 @@ class CloudItem(BaseItem):
|
|
|
193
195
|
|
|
194
196
|
if data.dtype in {np.dtype('float32'), np.dtype('float64')}:
|
|
195
197
|
if data.size == 0:
|
|
196
|
-
data = np.empty((0), self.
|
|
198
|
+
data = np.empty((0), self.DATA_TYPE)
|
|
197
199
|
elif data.ndim == 2 and data.shape[1] >= 3:
|
|
198
200
|
xyz = data[:, :3]
|
|
199
201
|
if data.shape[1] >= 4:
|
|
@@ -201,7 +203,7 @@ class CloudItem(BaseItem):
|
|
|
201
203
|
else:
|
|
202
204
|
color = np.zeros(data.shape[0], dtype=np.uint32)
|
|
203
205
|
data = np.rec.fromarrays(
|
|
204
|
-
[xyz, color[:data.shape[0]]], dtype=self.
|
|
206
|
+
[xyz, color[:data.shape[0]]], dtype=self.DATA_TYPE)
|
|
205
207
|
|
|
206
208
|
with self.mutex:
|
|
207
209
|
if append:
|
|
@@ -226,7 +228,7 @@ class CloudItem(BaseItem):
|
|
|
226
228
|
set_uniform(self.program, float(self.vmin), 'vmin')
|
|
227
229
|
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
228
230
|
set_uniform(self.program, int(self.size), 'point_size')
|
|
229
|
-
set_uniform(self.program, int(self.
|
|
231
|
+
set_uniform(self.program, int(self.POINT_TYPE_TABLE[self.point_type]), 'point_type')
|
|
230
232
|
glUseProgram(0)
|
|
231
233
|
self.need_update_setting = False
|
|
232
234
|
|
|
@@ -245,7 +247,7 @@ class CloudItem(BaseItem):
|
|
|
245
247
|
buff_capacity += self.CAPACITY
|
|
246
248
|
if Q3D_DEBUG is not None:
|
|
247
249
|
print("[Cloud Item] Update capacity to %d" % buff_capacity)
|
|
248
|
-
new_buff = np.empty((buff_capacity), self.
|
|
250
|
+
new_buff = np.empty((buff_capacity), self.DATA_TYPE)
|
|
249
251
|
new_buff[:self.add_buff_loc] = self.buff[:self.add_buff_loc]
|
|
250
252
|
new_buff[self.add_buff_loc:new_buff_top] = self.wait_add_data
|
|
251
253
|
self.buff = new_buff
|
|
@@ -17,7 +17,6 @@ from q3dviewer.utils import set_uniform, text_to_rgba
|
|
|
17
17
|
import time
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
class MeshItem(BaseItem):
|
|
22
21
|
"""
|
|
23
22
|
A OpenGL mesh item for rendering 3D triangular meshes.
|
|
@@ -25,15 +24,21 @@ class MeshItem(BaseItem):
|
|
|
25
24
|
color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
|
|
26
25
|
wireframe (bool): If True, renders the mesh in wireframe mode.
|
|
27
26
|
"""
|
|
27
|
+
# Class-level constants
|
|
28
|
+
FACE_CAPACITY = 1000000 # Initial capacity for faces
|
|
29
|
+
BIG_INT = 2**31 - 1 # Sentinel value for dirty region tracking
|
|
30
|
+
FACE_INPUT_DTYPE = np.dtype([
|
|
31
|
+
('key', np.int64),
|
|
32
|
+
('vertices', np.float32, (12,)),
|
|
33
|
+
('good', np.uint8)
|
|
34
|
+
])
|
|
35
|
+
|
|
28
36
|
def __init__(self, color='lightblue', wireframe=False):
|
|
29
37
|
super(MeshItem, self).__init__()
|
|
30
38
|
self.wireframe = wireframe
|
|
31
39
|
self.color = color
|
|
32
40
|
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
33
41
|
|
|
34
|
-
# Incremental buffer management
|
|
35
|
-
self.FACE_CAPACITY = 1000000 # Initial capacity for faces
|
|
36
|
-
|
|
37
42
|
# Faces buffer: N x 13 numpy array
|
|
38
43
|
# 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
44
|
self.faces = np.zeros((self.FACE_CAPACITY, 13), dtype=np.float32)
|
|
@@ -41,6 +46,12 @@ class MeshItem(BaseItem):
|
|
|
41
46
|
# valid_f_top: pointer to end of valid faces
|
|
42
47
|
self.valid_f_top = 0
|
|
43
48
|
|
|
49
|
+
# Dirty region tracking for efficient GPU updates
|
|
50
|
+
# dirty_min: start index of modified region (inclusive)
|
|
51
|
+
# dirty_max: end index of modified region (exclusive)
|
|
52
|
+
self.dirty_min = self.BIG_INT
|
|
53
|
+
self.dirty_max = 0
|
|
54
|
+
|
|
44
55
|
# key2index: mapping from face_key to face buffer index
|
|
45
56
|
self.key2index = {} # {face_key: face_index}
|
|
46
57
|
|
|
@@ -63,6 +74,7 @@ class MeshItem(BaseItem):
|
|
|
63
74
|
|
|
64
75
|
# Settings flag
|
|
65
76
|
self.need_update_setting = True
|
|
77
|
+
self.need_update_buffer = True
|
|
66
78
|
self.path = os.path.dirname(__file__)
|
|
67
79
|
|
|
68
80
|
|
|
@@ -181,102 +193,151 @@ class MeshItem(BaseItem):
|
|
|
181
193
|
|
|
182
194
|
def set_data(self, data):
|
|
183
195
|
"""
|
|
184
|
-
Set complete mesh data at once.
|
|
185
|
-
|
|
186
196
|
Args:
|
|
187
|
-
data:
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
data: One of the following formats:
|
|
198
|
+
- Nx3 numpy array (N must be divisible by 3): vertex list -> static
|
|
199
|
+
- Nx9 numpy array: triangle list -> static
|
|
200
|
+
- Structured array with dtype [('key', int64), ('vertices', float32, (12,)), ('good', uint32)] -> incremental
|
|
190
201
|
"""
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
if not isinstance(data, np.ndarray):
|
|
203
|
+
raise ValueError("Data must be a numpy array")
|
|
204
|
+
|
|
205
|
+
# Structured array format -> use incremental path (has keys for updates)
|
|
206
|
+
if data.dtype == self.FACE_INPUT_DTYPE:
|
|
195
207
|
self.set_incremental_data(data)
|
|
196
208
|
return
|
|
197
|
-
|
|
198
|
-
#
|
|
209
|
+
|
|
210
|
+
# Nx3 or Nx9 format -> use static path (more efficient, no key overhead)
|
|
211
|
+
if data.ndim == 2 and data.shape[1] in [3, 9]:
|
|
212
|
+
self.set_static_data(data)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
raise ValueError("Data must be Nx3, Nx9, or structured array format")
|
|
199
216
|
|
|
217
|
+
def set_static_data(self, data):
|
|
218
|
+
"""
|
|
219
|
+
Efficiently set static mesh data without key2index overhead.
|
|
220
|
+
For static meshes that don't need incremental updates.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
data: numpy array in one of these formats:
|
|
224
|
+
- Nx3: vertex list (N must be divisible by 3)
|
|
225
|
+
- Nx9: triangle list
|
|
226
|
+
"""
|
|
200
227
|
if not isinstance(data, np.ndarray):
|
|
201
|
-
raise ValueError("
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
raise ValueError("Data must be a numpy array")
|
|
229
|
+
|
|
230
|
+
if data.ndim != 2:
|
|
231
|
+
raise ValueError(f"Data must be 2D array, got {data.ndim}D")
|
|
232
|
+
self.clear_mesh()
|
|
233
|
+
# Handle Nx3 format
|
|
234
|
+
if data.shape[1] == 3:
|
|
235
|
+
if data.shape[0] % 3 != 0:
|
|
236
|
+
raise ValueError(f"Nx3 format requires N divisible by 3, got N={data.shape[0]}")
|
|
237
|
+
|
|
238
|
+
num_faces = data.shape[0] // 3
|
|
239
|
+
faces = np.zeros((num_faces, 13), dtype=np.float32)
|
|
240
|
+
|
|
241
|
+
# Reshape to (num_faces, 9) for efficient copying
|
|
242
|
+
tmp = data.reshape(num_faces, 9)
|
|
243
|
+
faces[:, 0:3] = tmp[:, 0:3] # v0
|
|
244
|
+
faces[:, 3:6] = tmp[:, 3:6] # v1
|
|
245
|
+
faces[:, 6:9] = tmp[:, 6:9] # v2
|
|
246
|
+
faces[:, 9:12] = tmp[:, 6:9] # v3 = v2 (degenerate)
|
|
247
|
+
faces[:, 12] = 1.0 # good=1.0
|
|
248
|
+
|
|
249
|
+
# Handle Nx9 format
|
|
250
|
+
elif data.shape[1] == 9:
|
|
251
|
+
num_faces = data.shape[0]
|
|
252
|
+
faces = np.zeros((num_faces, 13), dtype=np.float32)
|
|
253
|
+
|
|
254
|
+
faces[:, 0:9] = data # Copy all 9 vertices
|
|
255
|
+
faces[:, 9:12] = data[:, 6:9] # v3 = v2 (degenerate)
|
|
256
|
+
faces[:, 12] = 1.0 # good=1.0
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError(f"Data shape must be Nx3 or Nx9, got Nx{data.shape[1]}")
|
|
260
|
+
|
|
261
|
+
# Replace faces buffer (static data, no key management)
|
|
262
|
+
self.clear_mesh()
|
|
221
263
|
self.faces = faces
|
|
222
|
-
self.valid_f_top =
|
|
264
|
+
self.valid_f_top = num_faces
|
|
265
|
+
|
|
266
|
+
# Mark entire buffer as dirty
|
|
267
|
+
self.dirty_min = 0
|
|
268
|
+
self.dirty_max = self.valid_f_top
|
|
269
|
+
self.need_update_buffer = True
|
|
223
270
|
|
|
224
271
|
|
|
225
272
|
def set_incremental_data(self, fs):
|
|
226
273
|
"""
|
|
227
274
|
Incrementally update mesh with new face data.
|
|
228
275
|
Args:
|
|
229
|
-
fs:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Triangle 2: (v0, v3, v2)
|
|
276
|
+
fs: Structured numpy array with dtype:
|
|
277
|
+
[('key', np.int64), ('vertices', np.float32, (12,)), ('good', np.uint32)]
|
|
278
|
+
- key: unique identifier for the face
|
|
279
|
+
- vertices: 12 floats representing 4 vertices (v0, v1, v2, v3)
|
|
280
|
+
- good: 0 or 1, whether to render this face
|
|
235
281
|
Updates:
|
|
236
282
|
- faces: updates existing faces or appends new ones
|
|
237
283
|
- key2index: tracks face_key -> face_index mapping
|
|
238
284
|
"""
|
|
239
|
-
if
|
|
285
|
+
if fs is None or len(fs) == 0:
|
|
240
286
|
return
|
|
287
|
+
|
|
288
|
+
if not isinstance(fs, np.ndarray) or fs.dtype.names is None:
|
|
289
|
+
raise ValueError("fs must be a structured numpy array with fields: key, vertices, good")
|
|
241
290
|
|
|
242
291
|
# Ensure enough capacity in faces buffer
|
|
243
|
-
# wasted cases are better than frequent expansions
|
|
244
292
|
while self.valid_f_top + len(fs) > len(self.faces):
|
|
245
293
|
self._expand_face_buffer()
|
|
246
294
|
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
# Prepare face data: convert structured array to Nx13 format
|
|
296
|
+
n_faces = len(fs)
|
|
297
|
+
face_data = np.zeros((n_faces, 13), dtype=np.float32)
|
|
298
|
+
|
|
299
|
+
# Copy vertices (12 floats -> positions 0:12)
|
|
300
|
+
face_data[:, :12] = fs['vertices']
|
|
301
|
+
|
|
302
|
+
# Copy good flag (position 12)
|
|
303
|
+
face_data[:, 12] = fs['good'].astype(np.float32)
|
|
304
|
+
|
|
305
|
+
# Extract keys
|
|
306
|
+
keys = fs['key']
|
|
307
|
+
|
|
308
|
+
# Optimization: Separate updates from new insertions
|
|
309
|
+
update_mask = np.array([key in self.key2index for key in keys], dtype=bool)
|
|
310
|
+
new_mask = ~update_mask
|
|
311
|
+
|
|
263
312
|
# Batch update existing faces
|
|
264
|
-
if
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self.faces[
|
|
313
|
+
if np.any(update_mask):
|
|
314
|
+
update_keys = keys[update_mask]
|
|
315
|
+
update_indices = np.array([self.key2index[key] for key in update_keys], dtype=np.int32)
|
|
316
|
+
self.faces[update_indices] = face_data[update_mask]
|
|
317
|
+
|
|
318
|
+
# Update dirty region for modified faces
|
|
319
|
+
self.dirty_min = min(self.dirty_min, int(np.min(update_indices)))
|
|
320
|
+
self.dirty_max = max(self.dirty_max, int(np.max(update_indices) + 1))
|
|
321
|
+
self.need_update_buffer = True
|
|
268
322
|
|
|
269
323
|
# Batch insert new faces
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
324
|
+
if np.any(new_mask):
|
|
325
|
+
new_keys = keys[new_mask]
|
|
326
|
+
new_face_data = face_data[new_mask]
|
|
327
|
+
n_new = len(new_keys)
|
|
328
|
+
|
|
329
|
+
# Update dirty region for new faces
|
|
330
|
+
start_index = self.valid_f_top
|
|
331
|
+
# Insert data
|
|
332
|
+
self.faces[start_index: start_index + n_new] = new_face_data
|
|
333
|
+
|
|
275
334
|
# Update key2index mapping for new faces
|
|
276
335
|
for i, face_key in enumerate(new_keys):
|
|
277
|
-
self.key2index[face_key] =
|
|
278
|
-
|
|
336
|
+
self.key2index[face_key] = start_index + i
|
|
279
337
|
self.valid_f_top += n_new
|
|
338
|
+
self.dirty_min = min(self.dirty_min, start_index)
|
|
339
|
+
self.dirty_max = max(self.dirty_max, start_index + n_new)
|
|
340
|
+
self.need_update_buffer = True
|
|
280
341
|
|
|
281
342
|
def _expand_face_buffer(self):
|
|
282
343
|
"""Expand the faces buffer when capacity is reached"""
|
|
@@ -288,6 +349,8 @@ class MeshItem(BaseItem):
|
|
|
288
349
|
def clear_mesh(self):
|
|
289
350
|
"""Clear all mesh data and reset buffers"""
|
|
290
351
|
self.valid_f_top = 0
|
|
352
|
+
self.dirty_min = self.BIG_INT
|
|
353
|
+
self.dirty_max = 0
|
|
291
354
|
self.key2index.clear()
|
|
292
355
|
if hasattr(self, 'indices_array'):
|
|
293
356
|
self.indices_array = np.array([], dtype=np.uint32)
|
|
@@ -366,13 +429,27 @@ class MeshItem(BaseItem):
|
|
|
366
429
|
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
367
430
|
self._gpu_face_capacity = len(self.faces)
|
|
368
431
|
|
|
369
|
-
# Upload faces to VBO
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
432
|
+
# Upload faces to VBO (only dirty region)
|
|
433
|
+
if self.need_update_buffer:
|
|
434
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
435
|
+
|
|
436
|
+
# Calculate the range to upload [dirty_min, dirty_max)
|
|
437
|
+
start_index = int(self.dirty_min)
|
|
438
|
+
end_index = int(self.dirty_max)
|
|
439
|
+
count = end_index - start_index
|
|
440
|
+
|
|
441
|
+
# Upload only the modified region
|
|
442
|
+
glBufferSubData(GL_ARRAY_BUFFER,
|
|
443
|
+
start_index * 13 * 4, # offset in bytes
|
|
444
|
+
count * 13 * 4, # size in bytes
|
|
445
|
+
self.faces[start_index:end_index])
|
|
446
|
+
|
|
447
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
448
|
+
self.need_update_buffer = False
|
|
449
|
+
|
|
450
|
+
# Reset dirty region
|
|
451
|
+
self.dirty_min = self.BIG_INT
|
|
452
|
+
self.dirty_max = 0
|
|
376
453
|
|
|
377
454
|
def update_setting(self):
|
|
378
455
|
"""Set fixed rendering parameters (called once during initialization)"""
|
|
@@ -0,0 +1,330 @@
|
|
|
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
|
+
import numpy as np
|
|
9
|
+
from q3dviewer.base_item import BaseItem
|
|
10
|
+
from OpenGL.GL import *
|
|
11
|
+
from OpenGL.GL import shaders
|
|
12
|
+
from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from q3dviewer.utils import set_uniform, text_to_rgba
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StaticMeshItem(BaseItem):
|
|
19
|
+
"""
|
|
20
|
+
A OpenGL mesh item for rendering static 3D triangular meshes.
|
|
21
|
+
Optimized for static geometry with triangle-only rendering.
|
|
22
|
+
Data format: Nx9 numpy array (3 vertices per triangle, no good flag needed)
|
|
23
|
+
|
|
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(StaticMeshItem, self).__init__()
|
|
30
|
+
self.wireframe = wireframe
|
|
31
|
+
self.color = color
|
|
32
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
33
|
+
|
|
34
|
+
# Static mesh buffer: N x 9 numpy array
|
|
35
|
+
# Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
|
|
36
|
+
self.vertices = None
|
|
37
|
+
self.num_triangles = 0
|
|
38
|
+
|
|
39
|
+
# OpenGL objects
|
|
40
|
+
self.vao = None
|
|
41
|
+
self.vbo = None
|
|
42
|
+
self.program = None
|
|
43
|
+
|
|
44
|
+
# Fixed rendering parameters
|
|
45
|
+
self.enable_lighting = True
|
|
46
|
+
self.line_width = 1.0
|
|
47
|
+
self.light_color = [1.0, 1.0, 1.0]
|
|
48
|
+
self.ambient_strength = 0.1
|
|
49
|
+
self.diffuse_strength = 1.2
|
|
50
|
+
self.specular_strength = 0.1
|
|
51
|
+
self.shininess = 32.0
|
|
52
|
+
self.alpha = 1.0
|
|
53
|
+
|
|
54
|
+
# Settings flag
|
|
55
|
+
self.need_update_setting = True
|
|
56
|
+
self.need_update_buffer = True
|
|
57
|
+
self.path = os.path.dirname(__file__)
|
|
58
|
+
|
|
59
|
+
def add_setting(self, layout):
|
|
60
|
+
"""Add UI controls for mesh visualization"""
|
|
61
|
+
# Wireframe toggle
|
|
62
|
+
self.wireframe_box = QCheckBox("Wireframe Mode")
|
|
63
|
+
self.wireframe_box.setChecked(self.wireframe)
|
|
64
|
+
self.wireframe_box.toggled.connect(self.update_wireframe)
|
|
65
|
+
layout.addWidget(self.wireframe_box)
|
|
66
|
+
|
|
67
|
+
# Enable lighting toggle
|
|
68
|
+
self.lighting_box = QCheckBox("Enable Lighting")
|
|
69
|
+
self.lighting_box.setChecked(self.enable_lighting)
|
|
70
|
+
self.lighting_box.toggled.connect(self.update_enable_lighting)
|
|
71
|
+
layout.addWidget(self.lighting_box)
|
|
72
|
+
|
|
73
|
+
# Color setting
|
|
74
|
+
label_rgb = QLabel("Color:")
|
|
75
|
+
label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
76
|
+
layout.addWidget(label_rgb)
|
|
77
|
+
self.edit_rgb = QLineEdit()
|
|
78
|
+
self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
79
|
+
self.edit_rgb.setText(self.color)
|
|
80
|
+
self.edit_rgb.textChanged.connect(self._on_color)
|
|
81
|
+
layout.addWidget(self.edit_rgb)
|
|
82
|
+
|
|
83
|
+
# Material property controls for Phong lighting
|
|
84
|
+
if self.enable_lighting:
|
|
85
|
+
# Ambient strength control
|
|
86
|
+
ambient_layout = QHBoxLayout()
|
|
87
|
+
ambient_label = QLabel("Ambient Strength:")
|
|
88
|
+
ambient_layout.addWidget(ambient_label)
|
|
89
|
+
self.ambient_slider = QSlider()
|
|
90
|
+
self.ambient_slider.setOrientation(1) # Qt.Horizontal
|
|
91
|
+
self.ambient_slider.setRange(0, 100)
|
|
92
|
+
self.ambient_slider.setValue(int(self.ambient_strength * 100))
|
|
93
|
+
self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
|
|
94
|
+
ambient_layout.addWidget(self.ambient_slider)
|
|
95
|
+
layout.addLayout(ambient_layout)
|
|
96
|
+
|
|
97
|
+
# Diffuse strength control
|
|
98
|
+
diffuse_layout = QHBoxLayout()
|
|
99
|
+
diffuse_label = QLabel("Diffuse Strength:")
|
|
100
|
+
diffuse_layout.addWidget(diffuse_label)
|
|
101
|
+
self.diffuse_slider = QSlider()
|
|
102
|
+
self.diffuse_slider.setOrientation(1)
|
|
103
|
+
self.diffuse_slider.setRange(0, 200)
|
|
104
|
+
self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
|
|
105
|
+
self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
|
|
106
|
+
diffuse_layout.addWidget(self.diffuse_slider)
|
|
107
|
+
layout.addLayout(diffuse_layout)
|
|
108
|
+
|
|
109
|
+
# Specular strength control
|
|
110
|
+
specular_layout = QHBoxLayout()
|
|
111
|
+
specular_label = QLabel("Specular Strength:")
|
|
112
|
+
specular_layout.addWidget(specular_label)
|
|
113
|
+
self.specular_slider = QSlider()
|
|
114
|
+
self.specular_slider.setOrientation(1)
|
|
115
|
+
self.specular_slider.setRange(0, 100)
|
|
116
|
+
self.specular_slider.setValue(int(self.specular_strength * 100))
|
|
117
|
+
self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
|
|
118
|
+
specular_layout.addWidget(self.specular_slider)
|
|
119
|
+
layout.addLayout(specular_layout)
|
|
120
|
+
|
|
121
|
+
# Shininess control
|
|
122
|
+
shininess_layout = QHBoxLayout()
|
|
123
|
+
shininess_label = QLabel("Shininess:")
|
|
124
|
+
shininess_layout.addWidget(shininess_label)
|
|
125
|
+
self.shininess_slider = QSlider()
|
|
126
|
+
self.shininess_slider.setOrientation(1)
|
|
127
|
+
self.shininess_slider.setRange(1, 256)
|
|
128
|
+
self.shininess_slider.setValue(int(self.shininess))
|
|
129
|
+
self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
|
|
130
|
+
shininess_layout.addWidget(self.shininess_slider)
|
|
131
|
+
layout.addLayout(shininess_layout)
|
|
132
|
+
|
|
133
|
+
def _on_color(self, color):
|
|
134
|
+
try:
|
|
135
|
+
self.color = color
|
|
136
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
137
|
+
self.need_update_setting = True
|
|
138
|
+
except ValueError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
def update_wireframe(self, value):
|
|
142
|
+
self.wireframe = value
|
|
143
|
+
|
|
144
|
+
def update_enable_lighting(self, value):
|
|
145
|
+
self.enable_lighting = value
|
|
146
|
+
self.need_update_setting = True
|
|
147
|
+
|
|
148
|
+
def update_line_width(self, value):
|
|
149
|
+
self.line_width = value
|
|
150
|
+
self.need_update_setting = True
|
|
151
|
+
|
|
152
|
+
def update_ambient_strength(self, value):
|
|
153
|
+
self.ambient_strength = value
|
|
154
|
+
self.need_update_setting = True
|
|
155
|
+
|
|
156
|
+
def update_diffuse_strength(self, value):
|
|
157
|
+
self.diffuse_strength = value
|
|
158
|
+
self.need_update_setting = True
|
|
159
|
+
|
|
160
|
+
def update_specular_strength(self, value):
|
|
161
|
+
self.specular_strength = value
|
|
162
|
+
self.need_update_setting = True
|
|
163
|
+
|
|
164
|
+
def update_shininess(self, value):
|
|
165
|
+
self.shininess = value
|
|
166
|
+
self.need_update_setting = True
|
|
167
|
+
|
|
168
|
+
def update_alpha(self, value):
|
|
169
|
+
"""Update mesh alpha (opacity)"""
|
|
170
|
+
self.alpha = float(value)
|
|
171
|
+
self.need_update_setting = True
|
|
172
|
+
|
|
173
|
+
def set_data(self, data):
|
|
174
|
+
"""
|
|
175
|
+
Set complete mesh data at once.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: Nx9 numpy array where N is the number of triangles
|
|
179
|
+
Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
|
|
180
|
+
"""
|
|
181
|
+
print("Setting static mesh data with {} triangles".format(len(data)))
|
|
182
|
+
if not isinstance(data, np.ndarray):
|
|
183
|
+
raise ValueError("Data must be a numpy array")
|
|
184
|
+
|
|
185
|
+
# Check shape
|
|
186
|
+
if data.ndim != 2 or data.shape[1] != 9:
|
|
187
|
+
# Try to reshape if it's Nx3 (vertex list)
|
|
188
|
+
if data.ndim == 2 and data.shape[1] == 3 and data.shape[0] % 3 == 0:
|
|
189
|
+
data = data.reshape(-1, 9)
|
|
190
|
+
else:
|
|
191
|
+
raise ValueError(f"Invalid data shape {data.shape}. Expected Nx9 or (N*3)x3")
|
|
192
|
+
|
|
193
|
+
self.vertices = data.astype(np.float32)
|
|
194
|
+
self.num_triangles = len(self.vertices)
|
|
195
|
+
self.need_update_buffer = True
|
|
196
|
+
|
|
197
|
+
def clear_mesh(self):
|
|
198
|
+
"""Clear all mesh data"""
|
|
199
|
+
self.vertices = None
|
|
200
|
+
self.num_triangles = 0
|
|
201
|
+
self.need_update_buffer = True
|
|
202
|
+
|
|
203
|
+
def initialize_gl(self):
|
|
204
|
+
"""OpenGL initialization - load triangle shader"""
|
|
205
|
+
frag_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
vert_shader = open(self.path + '/../shaders/triangle_mesh_vert.glsl', 'r').read()
|
|
209
|
+
geom_shader = open(self.path + '/../shaders/triangle_mesh_geom.glsl', 'r').read()
|
|
210
|
+
self.program = shaders.compileProgram(
|
|
211
|
+
shaders.compileShader(vert_shader, GL_VERTEX_SHADER),
|
|
212
|
+
shaders.compileShader(geom_shader, GL_GEOMETRY_SHADER),
|
|
213
|
+
shaders.compileShader(frag_shader, GL_FRAGMENT_SHADER),
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
print(f"Error compiling static mesh shader: {e}")
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
def update_render_buffer(self):
|
|
220
|
+
"""
|
|
221
|
+
Update GPU buffer with triangle data.
|
|
222
|
+
Each triangle: 9 floats (3 vertices x 3 coordinates)
|
|
223
|
+
"""
|
|
224
|
+
if self.num_triangles == 0 or self.vertices is None:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Initialize buffers on first call
|
|
228
|
+
if self.vao is None:
|
|
229
|
+
self.vao = glGenVertexArrays(1)
|
|
230
|
+
self.vbo = glGenBuffers(1)
|
|
231
|
+
|
|
232
|
+
if not self.need_update_buffer:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
glBindVertexArray(self.vao)
|
|
236
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
237
|
+
|
|
238
|
+
# Upload all triangle data
|
|
239
|
+
glBufferData(GL_ARRAY_BUFFER,
|
|
240
|
+
self.vertices.nbytes,
|
|
241
|
+
self.vertices,
|
|
242
|
+
GL_STATIC_DRAW)
|
|
243
|
+
|
|
244
|
+
# Setup vertex attributes
|
|
245
|
+
# Triangle: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
|
|
246
|
+
# 9 floats = 36 bytes stride
|
|
247
|
+
stride = 36
|
|
248
|
+
|
|
249
|
+
# v0 (location 1) - vec3
|
|
250
|
+
glEnableVertexAttribArray(1)
|
|
251
|
+
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
|
|
252
|
+
glVertexAttribDivisor(1, 1)
|
|
253
|
+
|
|
254
|
+
# v1 (location 2) - vec3
|
|
255
|
+
glEnableVertexAttribArray(2)
|
|
256
|
+
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
|
|
257
|
+
glVertexAttribDivisor(2, 1)
|
|
258
|
+
|
|
259
|
+
# v2 (location 3) - vec3
|
|
260
|
+
glEnableVertexAttribArray(3)
|
|
261
|
+
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24))
|
|
262
|
+
glVertexAttribDivisor(3, 1)
|
|
263
|
+
|
|
264
|
+
glBindVertexArray(0)
|
|
265
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
266
|
+
self.need_update_buffer = False
|
|
267
|
+
|
|
268
|
+
def update_setting(self):
|
|
269
|
+
"""Set rendering parameters"""
|
|
270
|
+
if not self.need_update_setting:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
set_uniform(self.program, int(self.enable_lighting), 'if_light')
|
|
274
|
+
set_uniform(self.program, 1, 'two_sided')
|
|
275
|
+
set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
|
|
276
|
+
set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
|
|
277
|
+
set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
|
|
278
|
+
set_uniform(self.program, float(self.specular_strength), 'specular_strength')
|
|
279
|
+
set_uniform(self.program, float(self.shininess), 'shininess')
|
|
280
|
+
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
281
|
+
set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
|
|
282
|
+
self.need_update_setting = False
|
|
283
|
+
|
|
284
|
+
def paint(self):
|
|
285
|
+
"""
|
|
286
|
+
Render the static mesh using instanced rendering with geometry shader.
|
|
287
|
+
Each triangle instance is rendered as a point, geometry shader generates 1 triangle.
|
|
288
|
+
"""
|
|
289
|
+
if self.num_triangles == 0 or self.vertices is None:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
glUseProgram(self.program)
|
|
293
|
+
|
|
294
|
+
self.update_render_buffer()
|
|
295
|
+
self.update_setting()
|
|
296
|
+
|
|
297
|
+
view_matrix = self.glwidget().view_matrix
|
|
298
|
+
set_uniform(self.program, view_matrix, 'view')
|
|
299
|
+
project_matrix = self.glwidget().projection_matrix
|
|
300
|
+
set_uniform(self.program, project_matrix, 'projection')
|
|
301
|
+
view_pos = self.glwidget().center
|
|
302
|
+
set_uniform(self.program, np.array(view_pos), 'view_pos')
|
|
303
|
+
|
|
304
|
+
# Enable blending and depth testing
|
|
305
|
+
glEnable(GL_BLEND)
|
|
306
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
307
|
+
glEnable(GL_DEPTH_TEST)
|
|
308
|
+
glDisable(GL_CULL_FACE) # two-sided rendering
|
|
309
|
+
|
|
310
|
+
# Set line width
|
|
311
|
+
glLineWidth(self.line_width)
|
|
312
|
+
|
|
313
|
+
# Bind VAO
|
|
314
|
+
glBindVertexArray(self.vao)
|
|
315
|
+
|
|
316
|
+
if self.wireframe:
|
|
317
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
318
|
+
else:
|
|
319
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
320
|
+
|
|
321
|
+
# Draw using instanced rendering
|
|
322
|
+
# Input: POINTS (one per triangle instance)
|
|
323
|
+
# Geometry shader generates 1 triangle (3 vertices) per point
|
|
324
|
+
glDrawArraysInstanced(GL_POINTS, 0, 1, self.num_triangles)
|
|
325
|
+
|
|
326
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
327
|
+
glBindVertexArray(0)
|
|
328
|
+
glDisable(GL_DEPTH_TEST)
|
|
329
|
+
glDisable(GL_BLEND)
|
|
330
|
+
glUseProgram(0)
|
|
@@ -10,6 +10,7 @@ import q3dviewer as q3d
|
|
|
10
10
|
from q3dviewer.Qt.QtWidgets import QVBoxLayout, QDialog, QLabel
|
|
11
11
|
from q3dviewer.Qt.QtCore import QThread, Signal, Qt
|
|
12
12
|
from q3dviewer import GLWidget
|
|
13
|
+
from q3dviewer.utils.helpers import get_version
|
|
13
14
|
|
|
14
15
|
class ProgressWindow(QDialog):
|
|
15
16
|
def __init__(self, parent=None):
|
|
@@ -186,7 +187,7 @@ def print_help():
|
|
|
186
187
|
|
|
187
188
|
# Print title and table without border
|
|
188
189
|
console.print()
|
|
189
|
-
console.print("[bold magenta]☁️ Cloud Viewer Help[/bold magenta]\n")
|
|
190
|
+
console.print(f"[bold magenta]☁️ Cloud Viewer ({get_version()}) Help[/bold magenta]\n")
|
|
190
191
|
console.print(table)
|
|
191
192
|
console.print()
|
|
192
193
|
|
|
@@ -205,7 +206,7 @@ def main():
|
|
|
205
206
|
marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
|
|
206
207
|
text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
|
|
207
208
|
text_item.disable_setting()
|
|
208
|
-
mesh_item = q3d.
|
|
209
|
+
mesh_item = q3d.StaticMeshItem()
|
|
209
210
|
|
|
210
211
|
viewer.add_items(
|
|
211
212
|
{'marker': marker_item,
|
|
@@ -12,7 +12,7 @@ from q3dviewer.Qt.QtCore import QTimer
|
|
|
12
12
|
from q3dviewer.Qt.QtGui import QKeyEvent
|
|
13
13
|
from q3dviewer.Qt import QtCore
|
|
14
14
|
from q3dviewer import GLWidget
|
|
15
|
-
from q3dviewer.tools.cloud_viewer import
|
|
15
|
+
from q3dviewer.tools.cloud_viewer import FileLoaderThread, ProgressWindow
|
|
16
16
|
|
|
17
17
|
import imageio.v2 as imageio
|
|
18
18
|
import os
|
|
@@ -386,20 +386,19 @@ class CMMViewer(q3d.Viewer):
|
|
|
386
386
|
"""
|
|
387
387
|
Overwrite the drop event to open the cloud file.
|
|
388
388
|
"""
|
|
389
|
-
self.
|
|
390
|
-
self.
|
|
389
|
+
self.progress_window = ProgressWindow(self)
|
|
390
|
+
self.progress_window.show()
|
|
391
391
|
files = event.mimeData().urls()
|
|
392
392
|
self.progress_thread = FileLoaderThread(self, files)
|
|
393
|
-
self['cloud'].load(files[0].toLocalFile(), append=False)
|
|
394
393
|
self.progress_thread.progress.connect(self.file_loading_progress)
|
|
395
394
|
self.progress_thread.finished.connect(self.file_loading_finished)
|
|
396
395
|
self.progress_thread.start()
|
|
397
396
|
|
|
398
|
-
def file_loading_progress(self,
|
|
399
|
-
self.
|
|
397
|
+
def file_loading_progress(self, current, total, file_name):
|
|
398
|
+
self.progress_window.update_progress(current, total, file_name)
|
|
400
399
|
|
|
401
400
|
def file_loading_finished(self):
|
|
402
|
-
self.
|
|
401
|
+
self.progress_window.close()
|
|
403
402
|
|
|
404
403
|
def open_cloud_file(self, file, append=False):
|
|
405
404
|
cloud_item = self['cloud']
|
|
@@ -417,7 +416,7 @@ def main():
|
|
|
417
416
|
args = parser.parse_args()
|
|
418
417
|
app = q3d.QApplication(['Film Maker'])
|
|
419
418
|
viewer = CMMViewer(name='Film Maker', update_interval=30)
|
|
420
|
-
cloud_item = q3d.CloudIOItem(size=1, point_type='SPHERE', alpha=0.5
|
|
419
|
+
cloud_item = q3d.CloudIOItem(size=1, point_type='SPHERE', alpha=0.5)
|
|
421
420
|
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
422
421
|
|
|
423
422
|
viewer.add_items(
|
|
@@ -7,6 +7,18 @@ import numpy as np
|
|
|
7
7
|
from OpenGL.GL import *
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def get_version():
|
|
11
|
+
"""
|
|
12
|
+
Get the version of q3dviewer package.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
return version('q3dviewer')
|
|
17
|
+
except Exception:
|
|
18
|
+
# Fallback if package is not installed
|
|
19
|
+
return 'unknown'
|
|
20
|
+
|
|
21
|
+
|
|
10
22
|
def rainbow(scalars, scalar_min=0, scalar_max=255):
|
|
11
23
|
range = scalar_max - scalar_min
|
|
12
24
|
values = 1.0 - (scalars - scalar_min) / range
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: q3dviewer
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
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
|
|
7
|
-
|
|
7
|
+
Author-email: liu.yang@jp.panasonic.com
|
|
8
|
+
License: MIT
|
|
8
9
|
Description:
|
|
9
10
|

|
|
10
11
|
|
|
@@ -58,22 +59,34 @@ Description:
|
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
**Basic Operations**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
📁 **Load Files** - Drag and drop files into the viewer
|
|
64
|
+
* Point clouds: .pcd, .ply, .las, .e57
|
|
65
|
+
* Mesh files: .stl
|
|
66
|
+
|
|
67
|
+
📏 **Measure Distance** - Interactive point measurement
|
|
68
|
+
* `Ctrl + Left Click`: Add measurement point
|
|
69
|
+
* `Ctrl + Right Click`: Remove last point
|
|
70
|
+
* Total distance displayed automatically
|
|
71
|
+
|
|
72
|
+
🎥 **Camera Controls** - Navigate the 3D scene
|
|
73
|
+
* `Double Click`: Set camera center to point
|
|
74
|
+
* `Right Drag`: Rotate view
|
|
75
|
+
* `Left Drag`: Pan view
|
|
76
|
+
* `Mouse Wheel`: Zoom in/out
|
|
77
|
+
|
|
78
|
+
⚙️ **Settings** - Press `M` to open settings window
|
|
79
|
+
* Adjust visualization properties
|
|
67
80
|
|
|
68
81
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
69
82
|
|
|
70
83
|
[Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
|
|
71
84
|
|
|
72
|
-

|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
**Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
|
|
75
88
|
|
|
76
|
-

|
|
77
90
|
|
|
78
91
|
### 2. ROS Viewer
|
|
79
92
|
|
|
@@ -22,6 +22,7 @@ q3dviewer/custom_items/grid_item.py
|
|
|
22
22
|
q3dviewer/custom_items/image_item.py
|
|
23
23
|
q3dviewer/custom_items/line_item.py
|
|
24
24
|
q3dviewer/custom_items/mesh_item.py
|
|
25
|
+
q3dviewer/custom_items/static_mesh_item.py
|
|
25
26
|
q3dviewer/custom_items/text3d_item.py
|
|
26
27
|
q3dviewer/custom_items/text_item.py
|
|
27
28
|
q3dviewer/tools/__init__.py
|
|
@@ -2,8 +2,10 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='q3dviewer',
|
|
5
|
-
version='1.2.
|
|
5
|
+
version='1.2.5',
|
|
6
6
|
author="Liu Yang",
|
|
7
|
+
author_email="liu.yang@jp.panasonic.com",
|
|
8
|
+
license="MIT",
|
|
7
9
|
description="A library designed for quickly deploying a 3D viewer.",
|
|
8
10
|
long_description=open("README.md").read(),
|
|
9
11
|
long_description_content_type="text/markdown",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
t1 = time.time()
|
|
3
|
-
from q3dviewer.custom_items import *
|
|
4
|
-
t2 = time.time()
|
|
5
|
-
from q3dviewer.glwidget import *
|
|
6
|
-
from q3dviewer.viewer import *
|
|
7
|
-
from q3dviewer.base_item import *
|
|
8
|
-
from q3dviewer.base_glwidget import *
|
|
9
|
-
t3 = time.time()
|
|
10
|
-
|
|
11
|
-
from q3dviewer.Qt import Q3D_DEBUG
|
|
12
|
-
if Q3D_DEBUG:
|
|
13
|
-
print("Import custom items: ", t2 - t1)
|
|
14
|
-
print("Import base q3dviewer: ", t3 - t2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|