q3dviewer 1.1.9__tar.gz → 1.2.1__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.1.9 → q3dviewer-1.2.1}/PKG-INFO +1 -1
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/base_glwidget.py +0 -3
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/__init__.py +1 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/cloud_item.py +5 -16
- q3dviewer-1.2.1/q3dviewer/custom_items/mesh_item.py +342 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/cloud_viewer.py +17 -5
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/cloud_io.py +21 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/range_slider.py +7 -5
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/PKG-INFO +1 -1
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/SOURCES.txt +1 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/requires.txt +1 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/setup.py +2 -1
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/README.md +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/Qt/__init__.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/__init__.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/base_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/axis_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/cloud_io_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/frame_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/gaussian_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/grid_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/image_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/line_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/text3d_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/text_item.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/glwidget.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/__init__.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/example_viewer.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/film_maker.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/gaussian_viewer.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/lidar_calib.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/lidar_cam_calib.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/ros_viewer.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/__init__.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/convert_ros_msg.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/gl_helper.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/helpers.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/maths.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/viewer.py +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/dependency_links.txt +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/entry_points.txt +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/top_level.txt +0 -0
- {q3dviewer-1.1.9 → q3dviewer-1.2.1}/setup.cfg +0 -0
|
@@ -399,8 +399,6 @@ class BaseGLWidget(QOpenGLWidget):
|
|
|
399
399
|
points.append((x0 + dx, y0 + dy))
|
|
400
400
|
points = sorted(points, key=lambda p: (p[0]-x0)**2 + (p[1]-y0)**2)
|
|
401
401
|
|
|
402
|
-
print("points to check:", len(points))
|
|
403
|
-
|
|
404
402
|
gl_y0 = height - y0 - 1
|
|
405
403
|
z = 1.0
|
|
406
404
|
for x, y in points:
|
|
@@ -411,7 +409,6 @@ class BaseGLWidget(QOpenGLWidget):
|
|
|
411
409
|
z = glReadPixels(x, gl_y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
|
|
412
410
|
z = np.frombuffer(z, dtype=np.float32)[0]
|
|
413
411
|
if z != 1.0 and z != 0.0:
|
|
414
|
-
print("dist to p:", np.sqrt((x - x0)**2 + (y - y0)**2))
|
|
415
412
|
break
|
|
416
413
|
|
|
417
414
|
if z == 1.0 or z == 0.0:
|
|
@@ -8,3 +8,4 @@ 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.mesh_item import MeshItem
|
|
@@ -42,8 +42,7 @@ class CloudItem(BaseItem):
|
|
|
42
42
|
def __init__(self, size, alpha,
|
|
43
43
|
color_mode='I',
|
|
44
44
|
color='white',
|
|
45
|
-
point_type='PIXEL'
|
|
46
|
-
depth_test=False):
|
|
45
|
+
point_type='PIXEL'):
|
|
47
46
|
super().__init__()
|
|
48
47
|
self.STRIDE = 16 # stride of cloud array
|
|
49
48
|
self.valid_buff_top = 0
|
|
@@ -70,7 +69,6 @@ class CloudItem(BaseItem):
|
|
|
70
69
|
self.need_update_setting = True
|
|
71
70
|
self.max_cloud_size = 300000000
|
|
72
71
|
# Enable depth test when full opaque
|
|
73
|
-
self.depth_test = depth_test
|
|
74
72
|
self.path = os.path.dirname(__file__)
|
|
75
73
|
|
|
76
74
|
def add_setting(self, layout):
|
|
@@ -126,13 +124,6 @@ class CloudItem(BaseItem):
|
|
|
126
124
|
self.slider_v.rangeChanged.connect(self._on_range)
|
|
127
125
|
layout.addWidget(self.slider_v)
|
|
128
126
|
|
|
129
|
-
self.checkbox_depth_test = QCheckBox(
|
|
130
|
-
"Show front points first (Depth Test)")
|
|
131
|
-
self.checkbox_depth_test.setChecked(self.depth_test)
|
|
132
|
-
self.checkbox_depth_test.stateChanged.connect(self.set_depthtest)
|
|
133
|
-
self._on_color_mode(self.color_mode)
|
|
134
|
-
layout.addWidget(self.checkbox_depth_test)
|
|
135
|
-
|
|
136
127
|
def _on_range(self, lower, upper):
|
|
137
128
|
self.vmin = lower
|
|
138
129
|
self.vmax = upper
|
|
@@ -192,9 +183,6 @@ class CloudItem(BaseItem):
|
|
|
192
183
|
self.size = size
|
|
193
184
|
self.need_update_setting = True
|
|
194
185
|
|
|
195
|
-
def set_depthtest(self, state):
|
|
196
|
-
self.depth_test = state
|
|
197
|
-
|
|
198
186
|
def clear(self):
|
|
199
187
|
data = np.empty((0), self.data_type)
|
|
200
188
|
self.set_data(data)
|
|
@@ -304,15 +292,16 @@ class CloudItem(BaseItem):
|
|
|
304
292
|
def paint(self):
|
|
305
293
|
self.update_render_buffer()
|
|
306
294
|
self.update_setting()
|
|
295
|
+
|
|
307
296
|
glEnable(GL_BLEND)
|
|
308
297
|
glEnable(GL_PROGRAM_POINT_SIZE)
|
|
309
298
|
glEnable(GL_POINT_SPRITE)
|
|
310
299
|
glEnable(GL_DEPTH_TEST)
|
|
311
300
|
|
|
312
|
-
if
|
|
313
|
-
glDepthFunc(GL_ALWAYS)
|
|
301
|
+
if self.alpha < 0.9:
|
|
302
|
+
glDepthFunc(GL_ALWAYS)
|
|
314
303
|
else:
|
|
315
|
-
glDepthFunc(GL_LESS)
|
|
304
|
+
glDepthFunc(GL_LESS)
|
|
316
305
|
|
|
317
306
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
318
307
|
glUseProgram(self.program)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
5
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from q3dviewer.base_item import BaseItem
|
|
11
|
+
from OpenGL.GL import *
|
|
12
|
+
from OpenGL.GL import shaders
|
|
13
|
+
from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
|
|
14
|
+
import matplotlib.colors as mcolors
|
|
15
|
+
import os
|
|
16
|
+
from q3dviewer.utils import set_uniform, text_to_rgba
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MeshItem(BaseItem):
|
|
21
|
+
def __init__(self, color='lightblue', wireframe=False, enable_lighting=True):
|
|
22
|
+
super(MeshItem, self).__init__()
|
|
23
|
+
self.color = color
|
|
24
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
25
|
+
self.wireframe = wireframe
|
|
26
|
+
self.enable_lighting = enable_lighting
|
|
27
|
+
|
|
28
|
+
# Mesh data
|
|
29
|
+
self.triangles = None
|
|
30
|
+
self.normals = None
|
|
31
|
+
|
|
32
|
+
# OpenGL objects
|
|
33
|
+
self.vao = None
|
|
34
|
+
self.vbo_vertices = None
|
|
35
|
+
self.vbo_normals = None
|
|
36
|
+
# self.vbo_colors = None
|
|
37
|
+
self.program = None
|
|
38
|
+
|
|
39
|
+
# Rendering parameters
|
|
40
|
+
self.line_width = 1.0
|
|
41
|
+
self.light_pos = [1.0, 1.0, 1.0]
|
|
42
|
+
self.light_color = [1.0, 1.0, 1.0]
|
|
43
|
+
|
|
44
|
+
# Phong lighting material properties
|
|
45
|
+
self.ambient_strength = 0.1
|
|
46
|
+
self.diffuse_strength = 1.2
|
|
47
|
+
self.specular_strength = 0.1
|
|
48
|
+
self.shininess = 32.0
|
|
49
|
+
# Alpha (opacity)
|
|
50
|
+
self.alpha = 1.0
|
|
51
|
+
|
|
52
|
+
# Buffer initialization flag
|
|
53
|
+
self.need_update_buffer = True
|
|
54
|
+
self.need_update_setting = True
|
|
55
|
+
self.path = os.path.dirname(__file__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def add_setting(self, layout):
|
|
59
|
+
"""Add UI controls for mesh visualization"""
|
|
60
|
+
# Wireframe toggle
|
|
61
|
+
self.wireframe_box = QCheckBox("Wireframe Mode")
|
|
62
|
+
self.wireframe_box.setChecked(self.wireframe)
|
|
63
|
+
self.wireframe_box.toggled.connect(self.update_wireframe)
|
|
64
|
+
layout.addWidget(self.wireframe_box)
|
|
65
|
+
|
|
66
|
+
# Enable lighting toggle
|
|
67
|
+
self.lighting_box = QCheckBox("Enable Lighting")
|
|
68
|
+
self.lighting_box.setChecked(self.enable_lighting)
|
|
69
|
+
self.lighting_box.toggled.connect(self.update_enable_lighting)
|
|
70
|
+
layout.addWidget(self.lighting_box)
|
|
71
|
+
|
|
72
|
+
# Line width control
|
|
73
|
+
line_width_label = QLabel("Line Width:")
|
|
74
|
+
layout.addWidget(line_width_label)
|
|
75
|
+
self.line_width_box = QDoubleSpinBox()
|
|
76
|
+
self.line_width_box.setRange(0.5, 5.0)
|
|
77
|
+
self.line_width_box.setSingleStep(0.5)
|
|
78
|
+
self.line_width_box.setValue(self.line_width)
|
|
79
|
+
self.line_width_box.valueChanged.connect(self.update_line_width)
|
|
80
|
+
layout.addWidget(self.line_width_box)
|
|
81
|
+
|
|
82
|
+
# Alpha control
|
|
83
|
+
alpha_label = QLabel("Alpha:")
|
|
84
|
+
layout.addWidget(alpha_label)
|
|
85
|
+
alpha_box = QDoubleSpinBox()
|
|
86
|
+
alpha_box.setRange(0.0, 1.0)
|
|
87
|
+
alpha_box.setSingleStep(0.05)
|
|
88
|
+
alpha_box.setValue(self.alpha)
|
|
89
|
+
alpha_box.valueChanged.connect(self.update_alpha)
|
|
90
|
+
layout.addWidget(alpha_box)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
label_rgb = QLabel("Color:")
|
|
94
|
+
label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
95
|
+
layout.addWidget(label_rgb)
|
|
96
|
+
self.edit_rgb = QLineEdit()
|
|
97
|
+
self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
98
|
+
self.edit_rgb.setText(self.color)
|
|
99
|
+
self.edit_rgb.textChanged.connect(self._on_color)
|
|
100
|
+
layout.addWidget(self.edit_rgb)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Material property controls for Phong lighting
|
|
104
|
+
if self.enable_lighting:
|
|
105
|
+
# Ambient strength control (slider 0-100 mapped to 0.0-1.0)
|
|
106
|
+
ambient_layout = QHBoxLayout()
|
|
107
|
+
ambient_label = QLabel("Ambient Strength:")
|
|
108
|
+
ambient_layout.addWidget(ambient_label)
|
|
109
|
+
self.ambient_slider = QSlider()
|
|
110
|
+
self.ambient_slider.setOrientation(1) # Qt.Horizontal
|
|
111
|
+
self.ambient_slider.setRange(0, 100)
|
|
112
|
+
self.ambient_slider.setValue(int(self.ambient_strength * 100))
|
|
113
|
+
self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
|
|
114
|
+
ambient_layout.addWidget(self.ambient_slider)
|
|
115
|
+
layout.addLayout(ambient_layout)
|
|
116
|
+
|
|
117
|
+
# Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
|
|
118
|
+
diffuse_layout = QHBoxLayout()
|
|
119
|
+
diffuse_label = QLabel("Diffuse Strength:")
|
|
120
|
+
diffuse_layout.addWidget(diffuse_label)
|
|
121
|
+
self.diffuse_slider = QSlider()
|
|
122
|
+
self.diffuse_slider.setOrientation(1)
|
|
123
|
+
self.diffuse_slider.setRange(0, 200)
|
|
124
|
+
self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
|
|
125
|
+
self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
|
|
126
|
+
diffuse_layout.addWidget(self.diffuse_slider)
|
|
127
|
+
layout.addLayout(diffuse_layout)
|
|
128
|
+
|
|
129
|
+
# Specular strength control (slider 0-200 mapped to 0.0-2.0)
|
|
130
|
+
specular_layout = QHBoxLayout()
|
|
131
|
+
specular_label = QLabel("Specular Strength:")
|
|
132
|
+
specular_layout.addWidget(specular_label)
|
|
133
|
+
self.specular_slider = QSlider()
|
|
134
|
+
self.specular_slider.setOrientation(1)
|
|
135
|
+
self.specular_slider.setRange(0, 200)
|
|
136
|
+
self.specular_slider.setValue(int(self.specular_strength * 100))
|
|
137
|
+
self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
|
|
138
|
+
specular_layout.addWidget(self.specular_slider)
|
|
139
|
+
layout.addLayout(specular_layout)
|
|
140
|
+
|
|
141
|
+
# Shininess control (slider 1-256 mapped to 1-256)
|
|
142
|
+
shininess_layout = QHBoxLayout()
|
|
143
|
+
shininess_label = QLabel("Shininess:")
|
|
144
|
+
shininess_layout.addWidget(shininess_label)
|
|
145
|
+
self.shininess_slider = QSlider()
|
|
146
|
+
self.shininess_slider.setOrientation(1)
|
|
147
|
+
self.shininess_slider.setRange(1, 256)
|
|
148
|
+
self.shininess_slider.setValue(int(self.shininess))
|
|
149
|
+
self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
|
|
150
|
+
shininess_layout.addWidget(self.shininess_slider)
|
|
151
|
+
layout.addLayout(shininess_layout)
|
|
152
|
+
|
|
153
|
+
def _on_color(self, color):
|
|
154
|
+
try:
|
|
155
|
+
self.color = color
|
|
156
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
157
|
+
self.need_update_setting = True
|
|
158
|
+
except ValueError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def update_wireframe(self, value):
|
|
163
|
+
self.wireframe = value
|
|
164
|
+
|
|
165
|
+
def update_enable_lighting(self, value):
|
|
166
|
+
self.enable_lighting = value
|
|
167
|
+
self.need_update_setting = True
|
|
168
|
+
|
|
169
|
+
def update_line_width(self, value):
|
|
170
|
+
self.line_width = value
|
|
171
|
+
self.need_update_setting = True
|
|
172
|
+
|
|
173
|
+
def update_ambient_strength(self, value):
|
|
174
|
+
self.ambient_strength = value
|
|
175
|
+
self.need_update_setting = True
|
|
176
|
+
|
|
177
|
+
def update_diffuse_strength(self, value):
|
|
178
|
+
self.diffuse_strength = value
|
|
179
|
+
self.need_update_setting = True
|
|
180
|
+
|
|
181
|
+
def update_specular_strength(self, value):
|
|
182
|
+
self.specular_strength = value
|
|
183
|
+
self.need_update_setting = True
|
|
184
|
+
|
|
185
|
+
def update_shininess(self, value):
|
|
186
|
+
self.shininess = value
|
|
187
|
+
self.need_update_setting = True
|
|
188
|
+
|
|
189
|
+
def update_alpha(self, value):
|
|
190
|
+
"""Update mesh alpha (opacity)"""
|
|
191
|
+
self.alpha = float(value)
|
|
192
|
+
self.need_update_setting = True
|
|
193
|
+
|
|
194
|
+
def set_data(self, verts, faces):
|
|
195
|
+
"""
|
|
196
|
+
verts: np.ndarray of shape (N, 3)
|
|
197
|
+
faces: np.ndarray of shape (M, 3) with uint32 indices
|
|
198
|
+
"""
|
|
199
|
+
verts = np.asarray(verts, dtype=np.float32)
|
|
200
|
+
faces = np.asarray(faces, dtype=np.uint32)
|
|
201
|
+
triangles = verts[faces.flatten()]
|
|
202
|
+
|
|
203
|
+
self.triangles = np.asarray(triangles, dtype=np.float32)
|
|
204
|
+
self.normals = self.calculate_normals()
|
|
205
|
+
self.need_update_buffer = True
|
|
206
|
+
|
|
207
|
+
def calculate_normals(self):
|
|
208
|
+
if self.triangles is None or len(self.triangles) == 0:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
# Ensure we have complete triangles
|
|
212
|
+
num_vertices = len(self.triangles)
|
|
213
|
+
num_triangles = num_vertices // 3
|
|
214
|
+
if num_triangles == 0:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Reshape vertices into triangles (N, 3, 3) where N is number of triangles
|
|
218
|
+
vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
|
|
219
|
+
|
|
220
|
+
v0 = vertices_reshaped[:, 0, :]
|
|
221
|
+
v1 = vertices_reshaped[:, 1, :]
|
|
222
|
+
v2 = vertices_reshaped[:, 2, :]
|
|
223
|
+
|
|
224
|
+
# Calculate edges for all triangles at once
|
|
225
|
+
edge1 = v1 - v0
|
|
226
|
+
edge2 = v2 - v0
|
|
227
|
+
|
|
228
|
+
face_normals = np.cross(edge1, edge2)
|
|
229
|
+
|
|
230
|
+
norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
|
|
231
|
+
norms[norms < 1e-6] = 1.0
|
|
232
|
+
face_normals = face_normals / norms
|
|
233
|
+
|
|
234
|
+
normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
|
|
235
|
+
normals = normals_per_vertex.reshape(-1, 3)
|
|
236
|
+
return normals.astype(np.float32)
|
|
237
|
+
|
|
238
|
+
def initialize_gl(self):
|
|
239
|
+
"""OpenGL initialization"""
|
|
240
|
+
vertex_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
|
|
241
|
+
fragment_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
|
|
242
|
+
|
|
243
|
+
program = shaders.compileProgram(
|
|
244
|
+
shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
|
|
245
|
+
shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
|
|
246
|
+
)
|
|
247
|
+
self.program = program
|
|
248
|
+
|
|
249
|
+
def update_render_buffer(self):
|
|
250
|
+
"""Initialize OpenGL buffers"""
|
|
251
|
+
if not self.need_update_buffer:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Generate VAO and VBOs
|
|
255
|
+
if self.vao is None:
|
|
256
|
+
self.vao = glGenVertexArrays(1)
|
|
257
|
+
self.vbo_vertices = glGenBuffers(1)
|
|
258
|
+
self.vbo_normals = glGenBuffers(1)
|
|
259
|
+
# self.vbo_colors = glGenBuffers(1)
|
|
260
|
+
|
|
261
|
+
glBindVertexArray(self.vao)
|
|
262
|
+
|
|
263
|
+
# Vertex buffer
|
|
264
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo_vertices)
|
|
265
|
+
glBufferData(GL_ARRAY_BUFFER, self.triangles.nbytes, self.triangles, GL_STATIC_DRAW)
|
|
266
|
+
glEnableVertexAttribArray(0)
|
|
267
|
+
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
|
|
268
|
+
|
|
269
|
+
# Normal buffer
|
|
270
|
+
if self.normals is not None:
|
|
271
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo_normals)
|
|
272
|
+
glBufferData(GL_ARRAY_BUFFER, self.normals.nbytes, self.normals, GL_STATIC_DRAW)
|
|
273
|
+
glEnableVertexAttribArray(1)
|
|
274
|
+
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)
|
|
275
|
+
|
|
276
|
+
# Color buffer
|
|
277
|
+
# if self.flat_rgbs is not None:
|
|
278
|
+
# glBindBuffer(GL_ARRAY_BUFFER, self.vbo_colors)
|
|
279
|
+
# glBufferData(GL_ARRAY_BUFFER, self.flat_rgbs.nbytes, self.flat_rgbs, GL_STATIC_DRAW)
|
|
280
|
+
# glEnableVertexAttribArray(2)
|
|
281
|
+
# glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, None)
|
|
282
|
+
#
|
|
283
|
+
glBindVertexArray(0)
|
|
284
|
+
self.need_update_buffer = False
|
|
285
|
+
|
|
286
|
+
def update_setting(self):
|
|
287
|
+
if (self.need_update_setting is False):
|
|
288
|
+
return
|
|
289
|
+
set_uniform(self.program, int(self.enable_lighting), 'if_light')
|
|
290
|
+
set_uniform(self.program, 1, 'two_sided')
|
|
291
|
+
set_uniform(self.program, np.array(self.light_color), 'light_color')
|
|
292
|
+
set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
|
|
293
|
+
set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
|
|
294
|
+
set_uniform(self.program, float(self.specular_strength), 'specular_strength')
|
|
295
|
+
set_uniform(self.program, float(self.shininess), 'shininess')
|
|
296
|
+
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
297
|
+
set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
|
|
298
|
+
self.need_update_setting = False
|
|
299
|
+
|
|
300
|
+
def paint(self):
|
|
301
|
+
"""Render the mesh using modern OpenGL with shaders"""
|
|
302
|
+
if self.triangles is None or len(self.triangles) == 0:
|
|
303
|
+
return
|
|
304
|
+
glUseProgram(self.program)
|
|
305
|
+
self.update_render_buffer()
|
|
306
|
+
self.update_setting()
|
|
307
|
+
view_matrix = self.glwidget().view_matrix
|
|
308
|
+
set_uniform(self.program, view_matrix, 'view')
|
|
309
|
+
project_matrix = self.glwidget().projection_matrix
|
|
310
|
+
set_uniform(self.program, project_matrix, 'projection')
|
|
311
|
+
view_pos = self.glwidget().center
|
|
312
|
+
set_uniform(self.program, np.array(view_pos), 'view_pos')
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Enable blending and depth testing
|
|
316
|
+
glEnable(GL_BLEND)
|
|
317
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
318
|
+
glEnable(GL_DEPTH_TEST)
|
|
319
|
+
glDisable(GL_CULL_FACE) # two-sided rendering
|
|
320
|
+
|
|
321
|
+
# Set line width
|
|
322
|
+
glLineWidth(self.line_width)
|
|
323
|
+
|
|
324
|
+
# Bind VAO and render
|
|
325
|
+
glBindVertexArray(self.vao)
|
|
326
|
+
|
|
327
|
+
if len(self.triangles) > 0:
|
|
328
|
+
# Render faces
|
|
329
|
+
if self.wireframe:
|
|
330
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
331
|
+
else:
|
|
332
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
333
|
+
|
|
334
|
+
# Draw triangles
|
|
335
|
+
glDrawArrays(GL_TRIANGLES, 0, len(self.triangles))
|
|
336
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
337
|
+
|
|
338
|
+
glBindVertexArray(0)
|
|
339
|
+
glDisable(GL_DEPTH_TEST)
|
|
340
|
+
glDisable(GL_BLEND)
|
|
341
|
+
glUseProgram(0)
|
|
342
|
+
|
|
@@ -49,13 +49,21 @@ class FileLoaderThread(QThread):
|
|
|
49
49
|
|
|
50
50
|
def run(self):
|
|
51
51
|
cloud_item = self.viewer['cloud']
|
|
52
|
+
mesh_item = self.viewer['mesh']
|
|
52
53
|
for i, url in enumerate(self.files):
|
|
54
|
+
# if the file is a mesh file, use mesh_item to load
|
|
55
|
+
file_path = url.toLocalFile()
|
|
53
56
|
file_path = url.toLocalFile()
|
|
54
57
|
self.viewer.progress_dialog.set_file_name(file_path)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
if url.toLocalFile().lower().endswith(('.stl')):
|
|
59
|
+
from q3dviewer.utils.cloud_io import load_stl
|
|
60
|
+
verts, faces = load_stl(file_path)
|
|
61
|
+
mesh_item.set_data(verts=verts, faces=faces)
|
|
62
|
+
else:
|
|
63
|
+
cloud = cloud_item.load(file_path, append=(i > 0))
|
|
64
|
+
center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
|
|
65
|
+
self.viewer.glwidget.set_cam_position(center=center)
|
|
66
|
+
self.progress.emit(int((i + 1) / len(self.files) * 100))
|
|
59
67
|
self.finished.emit()
|
|
60
68
|
|
|
61
69
|
|
|
@@ -157,6 +165,8 @@ def print_help():
|
|
|
157
165
|
help_msg = f"""
|
|
158
166
|
{BOLD}Cloud Viewer Help:{END}
|
|
159
167
|
{GREEN}• Drag and drop cloud files into the viewer to load them.{END}
|
|
168
|
+
{BLUE}- support .pcd, .ply, .las, .e57, for point clouds.{END}
|
|
169
|
+
{BLUE}- support .stl for mesh files.{END}
|
|
160
170
|
{GREEN}• Measure distance between points:{END}
|
|
161
171
|
{BLUE}- Hold Ctrl and left-click to select points on the cloud.{END}
|
|
162
172
|
{BLUE}- Hold Ctrl and right-click to remove the last selected point.{END}
|
|
@@ -179,13 +189,15 @@ def main():
|
|
|
179
189
|
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
180
190
|
marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
|
|
181
191
|
text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
|
|
192
|
+
mesh_item = q3d.MeshItem() # Added MeshIOItem for mesh support
|
|
182
193
|
|
|
183
194
|
viewer.add_items(
|
|
184
195
|
{'marker': marker_item,
|
|
185
196
|
'cloud': cloud_item,
|
|
186
197
|
'grid': grid_item,
|
|
187
198
|
'axis': axis_item,
|
|
188
|
-
'text': text_item
|
|
199
|
+
'text': text_item,
|
|
200
|
+
'mesh': mesh_item})
|
|
189
201
|
|
|
190
202
|
if args.path:
|
|
191
203
|
pcd_fn = args.path
|
|
@@ -6,6 +6,27 @@ Distributed under MIT license. See LICENSE for more information.
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def load_stl(file_path):
|
|
10
|
+
from stl import mesh as stlmesh
|
|
11
|
+
m = stlmesh.Mesh.from_file(file_path)
|
|
12
|
+
verts = m.vectors.reshape(-1, 3).astype(np.float32)
|
|
13
|
+
faces = np.arange(len(verts), dtype=np.uint32).reshape(-1, 3)
|
|
14
|
+
return verts, faces
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_stl(verts, faces, save_path):
|
|
18
|
+
"""Save the generated mesh as an STL file."""
|
|
19
|
+
from stl import mesh as stlmesh
|
|
20
|
+
from stl import Mode
|
|
21
|
+
verts = np.asarray(verts, dtype=np.float32)
|
|
22
|
+
faces = np.asarray(faces, dtype=np.uint32)
|
|
23
|
+
# Create the mesh
|
|
24
|
+
m = stlmesh.Mesh(np.zeros(faces.shape[0], dtype=stlmesh.Mesh.dtype))
|
|
25
|
+
m.vectors[:] = verts[faces].astype(np.float32)
|
|
26
|
+
# Save to file
|
|
27
|
+
m.save(save_path, mode=Mode.BINARY)
|
|
28
|
+
|
|
29
|
+
|
|
9
30
|
def save_ply(cloud, save_path):
|
|
10
31
|
import meshio
|
|
11
32
|
xyz = cloud['xyz']
|
|
@@ -37,19 +37,21 @@ class RangeSlider(QSlider):
|
|
|
37
37
|
self.active_handle = "upper"
|
|
38
38
|
|
|
39
39
|
def mouseMoveEvent(self, event):
|
|
40
|
-
"""Override to update handle positions."""
|
|
40
|
+
"""Override to update handle positions, always clamp and int for cross-platform safety."""
|
|
41
41
|
if event.buttons() != Qt.LeftButton:
|
|
42
42
|
return
|
|
43
43
|
|
|
44
44
|
pos = self.pixelPosToValue(event.pos())
|
|
45
|
+
minv, maxv = self.minimum(), self.maximum()
|
|
45
46
|
if self.active_handle == "lower":
|
|
46
|
-
self.lower_value = max(
|
|
47
|
-
|
|
47
|
+
self.lower_value = max(minv, min(pos, self.upper_value - 1))
|
|
48
|
+
self.lower_value = int(round(self.lower_value))
|
|
48
49
|
QToolTip.showText(event.globalPos(), f"Lower: {self.lower_value:.1f}")
|
|
49
50
|
elif self.active_handle == "upper":
|
|
50
|
-
self.upper_value = min(
|
|
51
|
-
|
|
51
|
+
self.upper_value = min(maxv, max(pos, self.lower_value + 1))
|
|
52
|
+
self.upper_value = int(round(self.upper_value))
|
|
52
53
|
QToolTip.showText(event.globalPos(), f"Upper: {self.upper_value:.1f}")
|
|
54
|
+
# Always emit clamped, int values
|
|
53
55
|
self.rangeChanged.emit(self.lower_value, self.upper_value)
|
|
54
56
|
self.update()
|
|
55
57
|
|
|
@@ -21,6 +21,7 @@ q3dviewer/custom_items/gaussian_item.py
|
|
|
21
21
|
q3dviewer/custom_items/grid_item.py
|
|
22
22
|
q3dviewer/custom_items/image_item.py
|
|
23
23
|
q3dviewer/custom_items/line_item.py
|
|
24
|
+
q3dviewer/custom_items/mesh_item.py
|
|
24
25
|
q3dviewer/custom_items/text3d_item.py
|
|
25
26
|
q3dviewer/custom_items/text_item.py
|
|
26
27
|
q3dviewer/tools/__init__.py
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='q3dviewer',
|
|
5
|
-
version='1.1
|
|
5
|
+
version='1.2.1',
|
|
6
6
|
author="Liu Yang",
|
|
7
7
|
description="A library designed for quickly deploying a 3D viewer.",
|
|
8
8
|
long_description=open("README.md").read(),
|
|
@@ -29,6 +29,7 @@ setup(
|
|
|
29
29
|
'imageio',
|
|
30
30
|
'imageio[ffmpeg]',
|
|
31
31
|
'matplotlib',
|
|
32
|
+
'numpy-stl',
|
|
32
33
|
],
|
|
33
34
|
entry_points={
|
|
34
35
|
'console_scripts': [
|
|
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
|