q3dviewer 1.2.2__py3-none-any.whl → 1.2.4__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/__init__.py +1 -10
- q3dviewer/custom_items/__init__.py +2 -0
- q3dviewer/custom_items/mesh_item.py +275 -213
- q3dviewer/custom_items/static_mesh_item.py +330 -0
- 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/shaders/triangle_mesh_geom.glsl +66 -0
- q3dviewer/shaders/triangle_mesh_vert.glsl +31 -0
- q3dviewer/tools/cloud_viewer.py +67 -57
- q3dviewer/tools/film_maker.py +7 -8
- q3dviewer/utils/cloud_io.py +46 -19
- q3dviewer/utils/helpers.py +12 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/METADATA +24 -12
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/RECORD +20 -16
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/LICENSE +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/WHEEL +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/entry_points.txt +0 -0
- {q3dviewer-1.2.2.dist-info → q3dviewer-1.2.4.dist-info}/top_level.txt +0 -0
q3dviewer/__init__.py
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import time
|
|
2
|
-
t1 = time.time()
|
|
3
1
|
from q3dviewer.custom_items import *
|
|
4
|
-
t2 = time.time()
|
|
5
2
|
from q3dviewer.glwidget import *
|
|
6
3
|
from q3dviewer.viewer import *
|
|
7
4
|
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)
|
|
5
|
+
from q3dviewer.base_glwidget import *
|
|
@@ -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
|
+
|
|
@@ -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
|
-
|
|
17
|
+
import time
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class MeshItem(BaseItem):
|
|
21
21
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
A OpenGL mesh item for rendering 3D triangular meshes.
|
|
24
23
|
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)
|
|
24
|
+
color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
|
|
25
|
+
wireframe (bool): If True, renders the mesh in wireframe mode.
|
|
66
26
|
"""
|
|
67
|
-
def __init__(self, color='lightblue', wireframe=False
|
|
27
|
+
def __init__(self, color='lightblue', wireframe=False):
|
|
68
28
|
super(MeshItem, self).__init__()
|
|
29
|
+
self.wireframe = wireframe
|
|
69
30
|
self.color = color
|
|
70
31
|
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
71
|
-
self.wireframe = wireframe
|
|
72
|
-
self.enable_lighting = enable_lighting
|
|
73
32
|
|
|
74
|
-
#
|
|
75
|
-
self.
|
|
76
|
-
|
|
77
|
-
|
|
33
|
+
# Incremental buffer management
|
|
34
|
+
self.FACE_CAPACITY = 1000000 # Initial capacity for faces
|
|
35
|
+
|
|
36
|
+
# Faces buffer: N x 13 numpy array
|
|
37
|
+
# 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]
|
|
38
|
+
self.faces = np.zeros((self.FACE_CAPACITY, 13), dtype=np.float32)
|
|
78
39
|
|
|
79
|
-
|
|
80
|
-
self.
|
|
81
|
-
|
|
82
|
-
|
|
40
|
+
# valid_f_top: pointer to end of valid faces
|
|
41
|
+
self.valid_f_top = 0
|
|
42
|
+
|
|
43
|
+
# key2index: mapping from face_key to face buffer index
|
|
44
|
+
self.key2index = {} # {face_key: face_index}
|
|
83
45
|
|
|
84
46
|
# OpenGL objects
|
|
85
47
|
self.vao = None
|
|
86
|
-
self.
|
|
87
|
-
self.vbo_normals = None
|
|
88
|
-
self.vbo_colors = None
|
|
48
|
+
self.vbo = None
|
|
89
49
|
self.program = None
|
|
50
|
+
self._gpu_face_capacity = 0 # Track GPU buffer capacity
|
|
90
51
|
|
|
91
|
-
#
|
|
52
|
+
# Fixed rendering parameters (not adjustable via UI)
|
|
53
|
+
self.enable_lighting = True
|
|
92
54
|
self.line_width = 1.0
|
|
93
55
|
self.light_pos = [1.0, 1.0, 1.0]
|
|
94
56
|
self.light_color = [1.0, 1.0, 1.0]
|
|
95
|
-
|
|
96
|
-
# Phong lighting material properties
|
|
97
57
|
self.ambient_strength = 0.1
|
|
98
58
|
self.diffuse_strength = 1.2
|
|
99
59
|
self.specular_strength = 0.1
|
|
100
60
|
self.shininess = 32.0
|
|
101
|
-
# Alpha (opacity)
|
|
102
61
|
self.alpha = 1.0
|
|
103
62
|
|
|
104
|
-
#
|
|
105
|
-
self.need_update_buffer = True
|
|
63
|
+
# Settings flag
|
|
106
64
|
self.need_update_setting = True
|
|
65
|
+
self.need_update_buffer = 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,278 @@ 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):
|
|
263
183
|
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
184
|
+
Args:
|
|
185
|
+
data: One of the following formats:
|
|
186
|
+
- Nx3 numpy array (N must be divisible by 3): vertex list -> static
|
|
187
|
+
- Nx9 numpy array: triangle list -> static
|
|
188
|
+
- Structured array with dtype [('key', int64), ('vertices', float32, (12,)), ('good', uint32)] -> incremental
|
|
268
189
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
190
|
+
if not isinstance(data, np.ndarray):
|
|
191
|
+
raise ValueError("Data must be a numpy array")
|
|
192
|
+
|
|
193
|
+
want_dtype = np.dtype([
|
|
194
|
+
('key', np.int64),
|
|
195
|
+
('vertices', np.float32, (12,)),
|
|
196
|
+
('good', np.uint32)
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
# Structured array format -> use incremental path (has keys for updates)
|
|
200
|
+
if data.dtype == want_dtype:
|
|
201
|
+
self.set_incremental_data(data)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Nx3 or Nx9 format -> use static path (more efficient, no key overhead)
|
|
205
|
+
if data.ndim == 2 and data.shape[1] in [3, 9]:
|
|
206
|
+
self.set_static_data(data)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
raise ValueError("Data must be Nx3, Nx9, or structured array format")
|
|
272
210
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
self.vertex_colors = colors[faces.flatten()]
|
|
278
|
-
else:
|
|
279
|
-
self.vertex_colors = None
|
|
280
|
-
else:
|
|
281
|
-
self.vertex_colors = None
|
|
282
|
-
|
|
283
|
-
self.triangles = np.asarray(triangles, dtype=np.float32)
|
|
284
|
-
self.normals = self.calculate_normals()
|
|
285
|
-
self.need_update_buffer = True
|
|
211
|
+
def set_static_data(self, data):
|
|
212
|
+
"""
|
|
213
|
+
Efficiently set static mesh data without key2index overhead.
|
|
214
|
+
For static meshes that don't need incremental updates.
|
|
286
215
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
216
|
+
Args:
|
|
217
|
+
data: numpy array in one of these formats:
|
|
218
|
+
- Nx3: vertex list (N must be divisible by 3)
|
|
219
|
+
- Nx9: triangle list
|
|
220
|
+
"""
|
|
221
|
+
if not isinstance(data, np.ndarray):
|
|
222
|
+
raise ValueError("Data must be a numpy array")
|
|
223
|
+
|
|
224
|
+
if data.ndim != 2:
|
|
225
|
+
raise ValueError(f"Data must be 2D array, got {data.ndim}D")
|
|
226
|
+
self.clear_mesh()
|
|
227
|
+
# Handle Nx3 format
|
|
228
|
+
if data.shape[1] == 3:
|
|
229
|
+
if data.shape[0] % 3 != 0:
|
|
230
|
+
raise ValueError(f"Nx3 format requires N divisible by 3, got N={data.shape[0]}")
|
|
290
231
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
num_triangles = num_vertices // 3
|
|
294
|
-
if num_triangles == 0:
|
|
295
|
-
return None
|
|
232
|
+
num_faces = data.shape[0] // 3
|
|
233
|
+
faces = np.zeros((num_faces, 13), dtype=np.float32)
|
|
296
234
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
235
|
+
# Reshape to (num_faces, 9) for efficient copying
|
|
236
|
+
tmp = data.reshape(num_faces, 9)
|
|
237
|
+
faces[:, 0:3] = tmp[:, 0:3] # v0
|
|
238
|
+
faces[:, 3:6] = tmp[:, 3:6] # v1
|
|
239
|
+
faces[:, 6:9] = tmp[:, 6:9] # v2
|
|
240
|
+
faces[:, 9:12] = tmp[:, 6:9] # v3 = v2 (degenerate)
|
|
241
|
+
faces[:, 12] = 1.0 # good=1.0
|
|
242
|
+
|
|
243
|
+
# Handle Nx9 format
|
|
244
|
+
elif data.shape[1] == 9:
|
|
245
|
+
num_faces = data.shape[0]
|
|
246
|
+
faces = np.zeros((num_faces, 13), dtype=np.float32)
|
|
247
|
+
|
|
248
|
+
faces[:, 0:9] = data # Copy all 9 vertices
|
|
249
|
+
faces[:, 9:12] = data[:, 6:9] # v3 = v2 (degenerate)
|
|
250
|
+
faces[:, 12] = 1.0 # good=1.0
|
|
251
|
+
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError(f"Data shape must be Nx3 or Nx9, got Nx{data.shape[1]}")
|
|
309
254
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
255
|
+
# Replace faces buffer (static data, no key management)
|
|
256
|
+
self.clear_mesh()
|
|
257
|
+
self.faces = faces
|
|
258
|
+
self.valid_f_top = num_faces
|
|
259
|
+
self.need_update_buffer = True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def set_incremental_data(self, fs):
|
|
263
|
+
"""
|
|
264
|
+
Incrementally update mesh with new face data.
|
|
265
|
+
Args:
|
|
266
|
+
fs: Structured numpy array with dtype:
|
|
267
|
+
[('key', np.int64), ('vertices', np.float32, (12,)), ('good', np.uint32)]
|
|
268
|
+
- key: unique identifier for the face
|
|
269
|
+
- vertices: 12 floats representing 4 vertices (v0, v1, v2, v3)
|
|
270
|
+
- good: 0 or 1, whether to render this face
|
|
271
|
+
Updates:
|
|
272
|
+
- faces: updates existing faces or appends new ones
|
|
273
|
+
- key2index: tracks face_key -> face_index mapping
|
|
274
|
+
"""
|
|
275
|
+
if fs is None or len(fs) == 0:
|
|
276
|
+
return
|
|
313
277
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
278
|
+
if not isinstance(fs, np.ndarray) or fs.dtype.names is None:
|
|
279
|
+
raise ValueError("fs must be a structured numpy array with fields: key, vertices, good")
|
|
280
|
+
|
|
281
|
+
# Ensure enough capacity in faces buffer
|
|
282
|
+
while self.valid_f_top + len(fs) > len(self.faces):
|
|
283
|
+
self._expand_face_buffer()
|
|
284
|
+
|
|
285
|
+
# Prepare face data: convert structured array to Nx13 format
|
|
286
|
+
n_faces = len(fs)
|
|
287
|
+
face_data = np.zeros((n_faces, 13), dtype=np.float32)
|
|
288
|
+
|
|
289
|
+
# Copy vertices (12 floats -> positions 0:12)
|
|
290
|
+
face_data[:, :12] = fs['vertices']
|
|
291
|
+
|
|
292
|
+
# Copy good flag (position 12)
|
|
293
|
+
face_data[:, 12] = fs['good'].astype(np.float32)
|
|
294
|
+
|
|
295
|
+
# Extract keys
|
|
296
|
+
keys = fs['key']
|
|
297
|
+
|
|
298
|
+
# Optimization: Separate updates from new insertions
|
|
299
|
+
update_mask = np.array([key in self.key2index for key in keys], dtype=bool)
|
|
300
|
+
new_mask = ~update_mask
|
|
301
|
+
|
|
302
|
+
# Batch update existing faces
|
|
303
|
+
if np.any(update_mask):
|
|
304
|
+
update_keys = keys[update_mask]
|
|
305
|
+
update_indices = np.array([self.key2index[key] for key in update_keys], dtype=np.int32)
|
|
306
|
+
self.faces[update_indices] = face_data[update_mask]
|
|
307
|
+
|
|
308
|
+
# Batch insert new faces
|
|
309
|
+
if np.any(new_mask):
|
|
310
|
+
new_keys = keys[new_mask]
|
|
311
|
+
new_face_data = face_data[new_mask]
|
|
312
|
+
n_new = len(new_keys)
|
|
317
313
|
|
|
314
|
+
# Insert data
|
|
315
|
+
self.faces[self.valid_f_top: self.valid_f_top + n_new] = new_face_data
|
|
316
|
+
|
|
317
|
+
# Update key2index mapping for new faces
|
|
318
|
+
for i, face_key in enumerate(new_keys):
|
|
319
|
+
self.key2index[face_key] = self.valid_f_top + i
|
|
320
|
+
self.valid_f_top += n_new
|
|
321
|
+
self.need_update_buffer = True
|
|
322
|
+
|
|
323
|
+
def _expand_face_buffer(self):
|
|
324
|
+
"""Expand the faces buffer when capacity is reached"""
|
|
325
|
+
new_capacity = len(self.faces) + self.FACE_CAPACITY
|
|
326
|
+
new_buffer = np.zeros((new_capacity, 13), dtype=np.float32)
|
|
327
|
+
new_buffer[:len(self.faces)] = self.faces
|
|
328
|
+
self.faces = new_buffer
|
|
329
|
+
|
|
330
|
+
def clear_mesh(self):
|
|
331
|
+
"""Clear all mesh data and reset buffers"""
|
|
332
|
+
self.valid_f_top = 0
|
|
333
|
+
self.key2index.clear()
|
|
334
|
+
if hasattr(self, 'indices_array'):
|
|
335
|
+
self.indices_array = np.array([], dtype=np.uint32)
|
|
336
|
+
|
|
318
337
|
def initialize_gl(self):
|
|
319
338
|
"""OpenGL initialization"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
shaders.
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
# Use instanced mesh shaders with geometry shader for GPU-side triangle generation
|
|
340
|
+
vert_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
|
|
341
|
+
geom_shader = open(self.path + '/../shaders/mesh_geom.glsl', 'r').read()
|
|
342
|
+
frag_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
|
|
343
|
+
try:
|
|
344
|
+
program = shaders.compileProgram(
|
|
345
|
+
shaders.compileShader(vert_shader, GL_VERTEX_SHADER),
|
|
346
|
+
shaders.compileShader(geom_shader, GL_GEOMETRY_SHADER),
|
|
347
|
+
shaders.compileShader(frag_shader, GL_FRAGMENT_SHADER),
|
|
348
|
+
)
|
|
349
|
+
self.program = program
|
|
350
|
+
except Exception as e:
|
|
351
|
+
raise
|
|
328
352
|
|
|
329
353
|
def update_render_buffer(self):
|
|
330
|
-
"""
|
|
331
|
-
|
|
354
|
+
"""
|
|
355
|
+
Update GPU buffer with face data (no separate vertex buffer).
|
|
356
|
+
Each face contains embedded vertex positions (13 floats).
|
|
357
|
+
Geometry shader generates triangles on GPU from face vertices.
|
|
358
|
+
Dynamically resizes GPU buffer when Python buffer expands.
|
|
359
|
+
"""
|
|
360
|
+
if self.valid_f_top == 0:
|
|
332
361
|
return
|
|
333
|
-
|
|
334
|
-
#
|
|
362
|
+
|
|
363
|
+
# Initialize buffers on first call
|
|
335
364
|
if self.vao is None:
|
|
336
365
|
self.vao = glGenVertexArrays(1)
|
|
337
|
-
self.
|
|
338
|
-
self.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
366
|
+
self.vbo = glGenBuffers(1)
|
|
367
|
+
self._gpu_face_capacity = 0
|
|
368
|
+
|
|
369
|
+
# Check if we need to reallocate VBO for faces
|
|
370
|
+
if self._gpu_face_capacity < len(self.faces):
|
|
371
|
+
glBindVertexArray(self.vao)
|
|
372
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
373
|
+
glBufferData(GL_ARRAY_BUFFER,
|
|
374
|
+
self.faces.nbytes,
|
|
375
|
+
None,
|
|
376
|
+
GL_DYNAMIC_DRAW)
|
|
377
|
+
|
|
378
|
+
# Setup face attributes (per-instance)
|
|
379
|
+
# 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]
|
|
380
|
+
# 13 floats = 52 bytes stride
|
|
381
|
+
|
|
382
|
+
# v0 (location 1) - vec3
|
|
353
383
|
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)
|
|
384
|
+
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(0))
|
|
385
|
+
glVertexAttribDivisor(1, 1)
|
|
386
|
+
|
|
387
|
+
# v1 (location 2) - vec3
|
|
360
388
|
glEnableVertexAttribArray(2)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(12))
|
|
390
|
+
glVertexAttribDivisor(2, 1)
|
|
391
|
+
|
|
392
|
+
# v2 (location 3) - vec3
|
|
393
|
+
glEnableVertexAttribArray(3)
|
|
394
|
+
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(24))
|
|
395
|
+
glVertexAttribDivisor(3, 1)
|
|
396
|
+
|
|
397
|
+
# v3 (location 4) - vec3
|
|
398
|
+
glEnableVertexAttribArray(4)
|
|
399
|
+
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(36))
|
|
400
|
+
glVertexAttribDivisor(4, 1)
|
|
401
|
+
|
|
402
|
+
# good flag (location 5) - float
|
|
403
|
+
glEnableVertexAttribArray(5)
|
|
404
|
+
glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(48))
|
|
405
|
+
glVertexAttribDivisor(5, 1)
|
|
406
|
+
|
|
407
|
+
glBindVertexArray(0)
|
|
408
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
409
|
+
self._gpu_face_capacity = len(self.faces)
|
|
410
|
+
|
|
411
|
+
# Upload faces to VBO
|
|
412
|
+
if self.need_update_buffer:
|
|
413
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
414
|
+
glBufferSubData(GL_ARRAY_BUFFER,
|
|
415
|
+
0,
|
|
416
|
+
self.valid_f_top * 13 * 4, # 13 floats * 4 bytes per face
|
|
417
|
+
self.faces[:self.valid_f_top])
|
|
418
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
419
|
+
self.need_update_buffer = False
|
|
420
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
365
421
|
|
|
366
422
|
def update_setting(self):
|
|
367
|
-
|
|
423
|
+
"""Set fixed rendering parameters (called once during initialization)"""
|
|
424
|
+
if not self.need_update_setting:
|
|
368
425
|
return
|
|
426
|
+
# Set fixed uniforms for instanced shaders
|
|
369
427
|
set_uniform(self.program, int(self.enable_lighting), 'if_light')
|
|
370
428
|
set_uniform(self.program, 1, 'two_sided')
|
|
371
|
-
|
|
429
|
+
|
|
430
|
+
set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
|
|
372
431
|
set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
|
|
373
432
|
set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
|
|
374
433
|
set_uniform(self.program, float(self.specular_strength), 'specular_strength')
|
|
375
434
|
set_uniform(self.program, float(self.shininess), 'shininess')
|
|
376
435
|
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
377
436
|
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
437
|
self.need_update_setting = False
|
|
382
438
|
|
|
383
439
|
def paint(self):
|
|
384
|
-
"""
|
|
385
|
-
|
|
440
|
+
"""
|
|
441
|
+
Render the mesh using instanced rendering with geometry shader.
|
|
442
|
+
Each face instance is rendered as a point, geometry shader generates 2 triangles.
|
|
443
|
+
GPU filters faces based on good flag.
|
|
444
|
+
"""
|
|
445
|
+
if self.valid_f_top == 0:
|
|
386
446
|
return
|
|
447
|
+
|
|
387
448
|
glUseProgram(self.program)
|
|
449
|
+
|
|
388
450
|
self.update_render_buffer()
|
|
389
451
|
self.update_setting()
|
|
452
|
+
|
|
390
453
|
view_matrix = self.glwidget().view_matrix
|
|
391
454
|
set_uniform(self.program, view_matrix, 'view')
|
|
392
455
|
project_matrix = self.glwidget().projection_matrix
|
|
@@ -394,30 +457,29 @@ class MeshItem(BaseItem):
|
|
|
394
457
|
view_pos = self.glwidget().center
|
|
395
458
|
set_uniform(self.program, np.array(view_pos), 'view_pos')
|
|
396
459
|
|
|
397
|
-
|
|
398
460
|
# Enable blending and depth testing
|
|
399
461
|
glEnable(GL_BLEND)
|
|
400
462
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
401
463
|
glEnable(GL_DEPTH_TEST)
|
|
402
464
|
glDisable(GL_CULL_FACE) # two-sided rendering
|
|
403
|
-
|
|
465
|
+
|
|
404
466
|
# Set line width
|
|
405
467
|
glLineWidth(self.line_width)
|
|
406
468
|
|
|
407
|
-
# Bind VAO
|
|
469
|
+
# Bind VAO (vertex positions are now in VBO attributes)
|
|
408
470
|
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
471
|
|
|
417
|
-
|
|
418
|
-
|
|
472
|
+
if self.wireframe:
|
|
473
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
474
|
+
else:
|
|
419
475
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
420
|
-
|
|
476
|
+
|
|
477
|
+
# Draw using instanced rendering
|
|
478
|
+
# Input: POINTS (one per face instance)
|
|
479
|
+
# Geometry shader generates 2 triangles (6 vertices) per point
|
|
480
|
+
glDrawArraysInstanced(GL_POINTS, 0, 1, self.valid_f_top)
|
|
481
|
+
|
|
482
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
421
483
|
glBindVertexArray(0)
|
|
422
484
|
glDisable(GL_DEPTH_TEST)
|
|
423
485
|
glDisable(GL_BLEND)
|