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 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, QComboBox
14
- import matplotlib.colors as mcolors
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
- An OpenGL mesh item for rendering triangulated 3D surfaces.
23
-
22
+ A OpenGL mesh item for rendering 3D triangular meshes.
24
23
  Attributes:
25
- color (str or tuple): The flat color to use when `color_mode` is 'FLAT'.
26
- Accepts any valid matplotlib color (e.g., 'lightblue', 'red', '#FF4500', (1.0, 0.5, 0.0)).
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, enable_lighting=True, color_mode='FLAT'):
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
- # Mesh data
75
- self.triangles = None
76
- self.normals = None
77
- self.vertex_colors = None # Per-vertex colors (uint32 IRGB format)
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
- self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2}
80
- self.color_mode = self.mode_table[color_mode]
81
- self.vmin = 0
82
- self.vmax = 255
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.vbo_vertices = None
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
- # Rendering parameters
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
- # Buffer initialization flag
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
- # Wireframe toggle
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, verts, faces, colors=None):
181
+
182
+ def set_data(self, data):
263
183
  """
264
- verts: np.ndarray of shape (N, 3)
265
- faces: np.ndarray of shape (M, 3) with uint32 indices
266
- colors: np.ndarray of shape (N,) with uint32 IRGB format (optional)
267
- uint32 contains: I (bits 24-31), R (bits 16-23), G (bits 8-15), B (bits 0-7)
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
- verts = np.asarray(verts, dtype=np.float32)
270
- faces = np.asarray(faces, dtype=np.uint32)
271
- triangles = verts[faces.flatten()]
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
- if colors is not None:
274
- colors = np.asarray(colors, dtype=np.uint32)
275
- if len(colors) == len(verts):
276
- # Expand per-vertex colors to per-triangle-vertex
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
- def calculate_normals(self):
288
- if self.triangles is None or len(self.triangles) == 0:
289
- return None
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
- # Ensure we have complete triangles
292
- num_vertices = len(self.triangles)
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
- # Reshape vertices into triangles (N, 3, 3) where N is number of triangles
298
- vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
299
-
300
- v0 = vertices_reshaped[:, 0, :]
301
- v1 = vertices_reshaped[:, 1, :]
302
- v2 = vertices_reshaped[:, 2, :]
303
-
304
- # Calculate edges for all triangles at once
305
- edge1 = v1 - v0
306
- edge2 = v2 - v0
307
-
308
- face_normals = np.cross(edge1, edge2)
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
- norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
311
- norms[norms < 1e-6] = 1.0
312
- face_normals = face_normals / norms
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
- normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
315
- normals = normals_per_vertex.reshape(-1, 3)
316
- return normals.astype(np.float32)
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
- vertex_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
321
- fragment_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
322
-
323
- program = shaders.compileProgram(
324
- shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
325
- shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
326
- )
327
- self.program = program
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
- """Initialize OpenGL buffers"""
331
- if not self.need_update_buffer:
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
- # Generate VAO and VBOs
362
+
363
+ # Initialize buffers on first call
335
364
  if self.vao is None:
336
365
  self.vao = glGenVertexArrays(1)
337
- self.vbo_vertices = glGenBuffers(1)
338
- self.vbo_normals = glGenBuffers(1)
339
- self.vbo_colors = glGenBuffers(1)
340
-
341
- glBindVertexArray(self.vao)
342
-
343
- # Vertex buffer
344
- glBindBuffer(GL_ARRAY_BUFFER, self.vbo_vertices)
345
- glBufferData(GL_ARRAY_BUFFER, self.triangles.nbytes, self.triangles, GL_STATIC_DRAW)
346
- glEnableVertexAttribArray(0)
347
- glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
348
-
349
- # Normal buffer
350
- if self.normals is not None:
351
- glBindBuffer(GL_ARRAY_BUFFER, self.vbo_normals)
352
- glBufferData(GL_ARRAY_BUFFER, self.normals.nbytes, self.normals, GL_STATIC_DRAW)
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, 0, None)
355
-
356
- # Color buffer (uint32 IRGB format)
357
- if self.vertex_colors is not None:
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
- glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, 0, None)
362
-
363
- glBindVertexArray(0)
364
- self.need_update_buffer = False
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
- if (self.need_update_setting is False):
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
- set_uniform(self.program, np.array(self.light_color), 'light_color')
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
- """Render the mesh using modern OpenGL with shaders"""
385
- if self.triangles is None or len(self.triangles) == 0:
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 and render
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
- # Draw triangles
418
- glDrawArrays(GL_TRIANGLES, 0, len(self.triangles))
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)