q3dviewer 1.2.2__tar.gz → 1.2.4__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.2 → q3dviewer-1.2.4}/PKG-INFO +24 -11
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/README.md +21 -9
- q3dviewer-1.2.4/q3dviewer/__init__.py +5 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/__init__.py +2 -0
- q3dviewer-1.2.4/q3dviewer/custom_items/mesh_item.py +487 -0
- q3dviewer-1.2.4/q3dviewer/custom_items/static_mesh_item.py +330 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/glwidget.py +26 -1
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/cloud_viewer.py +67 -57
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/film_maker.py +7 -8
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/cloud_io.py +46 -19
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/helpers.py +12 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/PKG-INFO +24 -11
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/SOURCES.txt +1 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/requires.txt +0 -1
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/setup.py +3 -2
- q3dviewer-1.2.2/q3dviewer/__init__.py +0 -14
- q3dviewer-1.2.2/q3dviewer/custom_items/mesh_item.py +0 -425
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/Qt/__init__.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/base_glwidget.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/base_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/axis_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_io_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/frame_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/gaussian_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/grid_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/image_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/line_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/text3d_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/text_item.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/__init__.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/example_viewer.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/gaussian_viewer.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_calib.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_cam_calib.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/ros_viewer.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/__init__.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/convert_ros_msg.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/gl_helper.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/maths.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/range_slider.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/viewer.py +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/dependency_links.txt +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/entry_points.txt +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/top_level.txt +0 -0
- {q3dviewer-1.2.2 → q3dviewer-1.2.4}/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.4
|
|
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
|
+
|
|
@@ -0,0 +1,487 @@
|
|
|
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
|
+
|
|
15
|
+
import os
|
|
16
|
+
from q3dviewer.utils import set_uniform, text_to_rgba
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MeshItem(BaseItem):
|
|
21
|
+
"""
|
|
22
|
+
A OpenGL mesh item for rendering 3D triangular meshes.
|
|
23
|
+
Attributes:
|
|
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.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, color='lightblue', wireframe=False):
|
|
28
|
+
super(MeshItem, self).__init__()
|
|
29
|
+
self.wireframe = wireframe
|
|
30
|
+
self.color = color
|
|
31
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
32
|
+
|
|
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)
|
|
39
|
+
|
|
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}
|
|
45
|
+
|
|
46
|
+
# OpenGL objects
|
|
47
|
+
self.vao = None
|
|
48
|
+
self.vbo = None
|
|
49
|
+
self.program = None
|
|
50
|
+
self._gpu_face_capacity = 0 # Track GPU buffer capacity
|
|
51
|
+
|
|
52
|
+
# Fixed rendering parameters (not adjustable via UI)
|
|
53
|
+
self.enable_lighting = True
|
|
54
|
+
self.line_width = 1.0
|
|
55
|
+
self.light_pos = [1.0, 1.0, 1.0]
|
|
56
|
+
self.light_color = [1.0, 1.0, 1.0]
|
|
57
|
+
self.ambient_strength = 0.1
|
|
58
|
+
self.diffuse_strength = 1.2
|
|
59
|
+
self.specular_strength = 0.1
|
|
60
|
+
self.shininess = 32.0
|
|
61
|
+
self.alpha = 1.0
|
|
62
|
+
|
|
63
|
+
# Settings flag
|
|
64
|
+
self.need_update_setting = True
|
|
65
|
+
self.need_update_buffer = True
|
|
66
|
+
self.path = os.path.dirname(__file__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_setting(self, layout):
|
|
70
|
+
"""Add UI controls for mesh visualization"""
|
|
71
|
+
# Only keep wireframe toggle - all other parameters are fixed
|
|
72
|
+
self.wireframe_box = QCheckBox("Wireframe Mode")
|
|
73
|
+
self.wireframe_box.setChecked(self.wireframe)
|
|
74
|
+
self.wireframe_box.toggled.connect(self.update_wireframe)
|
|
75
|
+
layout.addWidget(self.wireframe_box)
|
|
76
|
+
|
|
77
|
+
# Enable lighting toggle
|
|
78
|
+
self.lighting_box = QCheckBox("Enable Lighting")
|
|
79
|
+
self.lighting_box.setChecked(self.enable_lighting)
|
|
80
|
+
self.lighting_box.toggled.connect(self.update_enable_lighting)
|
|
81
|
+
layout.addWidget(self.lighting_box)
|
|
82
|
+
|
|
83
|
+
label_rgb = QLabel("Color:")
|
|
84
|
+
label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
85
|
+
layout.addWidget(label_rgb)
|
|
86
|
+
self.edit_rgb = QLineEdit()
|
|
87
|
+
self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
88
|
+
self.edit_rgb.setText(self.color)
|
|
89
|
+
self.edit_rgb.textChanged.connect(self._on_color)
|
|
90
|
+
layout.addWidget(self.edit_rgb)
|
|
91
|
+
|
|
92
|
+
# Material property controls for Phong lighting
|
|
93
|
+
if self.enable_lighting:
|
|
94
|
+
# Ambient strength control (slider 0-100 mapped to 0.0-1.0)
|
|
95
|
+
ambient_layout = QHBoxLayout()
|
|
96
|
+
ambient_label = QLabel("Ambient Strength:")
|
|
97
|
+
ambient_layout.addWidget(ambient_label)
|
|
98
|
+
self.ambient_slider = QSlider()
|
|
99
|
+
self.ambient_slider.setOrientation(1) # Qt.Horizontal
|
|
100
|
+
self.ambient_slider.setRange(0, 100)
|
|
101
|
+
self.ambient_slider.setValue(int(self.ambient_strength * 100))
|
|
102
|
+
self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
|
|
103
|
+
ambient_layout.addWidget(self.ambient_slider)
|
|
104
|
+
layout.addLayout(ambient_layout)
|
|
105
|
+
|
|
106
|
+
# Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
|
|
107
|
+
diffuse_layout = QHBoxLayout()
|
|
108
|
+
diffuse_label = QLabel("Diffuse Strength:")
|
|
109
|
+
diffuse_layout.addWidget(diffuse_label)
|
|
110
|
+
self.diffuse_slider = QSlider()
|
|
111
|
+
self.diffuse_slider.setOrientation(1)
|
|
112
|
+
self.diffuse_slider.setRange(0, 200)
|
|
113
|
+
self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
|
|
114
|
+
self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
|
|
115
|
+
diffuse_layout.addWidget(self.diffuse_slider)
|
|
116
|
+
layout.addLayout(diffuse_layout)
|
|
117
|
+
|
|
118
|
+
# Specular strength control (slider 0-200 mapped to 0.0-2.0)
|
|
119
|
+
specular_layout = QHBoxLayout()
|
|
120
|
+
specular_label = QLabel("Specular Strength:")
|
|
121
|
+
specular_layout.addWidget(specular_label)
|
|
122
|
+
self.specular_slider = QSlider()
|
|
123
|
+
self.specular_slider.setOrientation(1)
|
|
124
|
+
self.specular_slider.setRange(0, 200)
|
|
125
|
+
self.specular_slider.setValue(int(self.specular_strength * 100))
|
|
126
|
+
self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
|
|
127
|
+
specular_layout.addWidget(self.specular_slider)
|
|
128
|
+
layout.addLayout(specular_layout)
|
|
129
|
+
|
|
130
|
+
# Shininess control (slider 1-256 mapped to 1-256)
|
|
131
|
+
shininess_layout = QHBoxLayout()
|
|
132
|
+
shininess_label = QLabel("Shininess:")
|
|
133
|
+
shininess_layout.addWidget(shininess_label)
|
|
134
|
+
self.shininess_slider = QSlider()
|
|
135
|
+
self.shininess_slider.setOrientation(1)
|
|
136
|
+
self.shininess_slider.setRange(1, 256)
|
|
137
|
+
self.shininess_slider.setValue(int(self.shininess))
|
|
138
|
+
self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
|
|
139
|
+
shininess_layout.addWidget(self.shininess_slider)
|
|
140
|
+
layout.addLayout(shininess_layout)
|
|
141
|
+
|
|
142
|
+
def _on_color(self, color):
|
|
143
|
+
try:
|
|
144
|
+
self.color = color
|
|
145
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
146
|
+
self.need_update_setting = True
|
|
147
|
+
except ValueError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def update_wireframe(self, value):
|
|
151
|
+
self.wireframe = value
|
|
152
|
+
|
|
153
|
+
def update_enable_lighting(self, value):
|
|
154
|
+
self.enable_lighting = value
|
|
155
|
+
self.need_update_setting = True
|
|
156
|
+
|
|
157
|
+
def update_line_width(self, value):
|
|
158
|
+
self.line_width = value
|
|
159
|
+
self.need_update_setting = True
|
|
160
|
+
|
|
161
|
+
def update_ambient_strength(self, value):
|
|
162
|
+
self.ambient_strength = value
|
|
163
|
+
self.need_update_setting = True
|
|
164
|
+
|
|
165
|
+
def update_diffuse_strength(self, value):
|
|
166
|
+
self.diffuse_strength = value
|
|
167
|
+
self.need_update_setting = True
|
|
168
|
+
|
|
169
|
+
def update_specular_strength(self, value):
|
|
170
|
+
self.specular_strength = value
|
|
171
|
+
self.need_update_setting = True
|
|
172
|
+
|
|
173
|
+
def update_shininess(self, value):
|
|
174
|
+
self.shininess = value
|
|
175
|
+
self.need_update_setting = True
|
|
176
|
+
|
|
177
|
+
def update_alpha(self, value):
|
|
178
|
+
"""Update mesh alpha (opacity)"""
|
|
179
|
+
self.alpha = float(value)
|
|
180
|
+
self.need_update_setting = True
|
|
181
|
+
|
|
182
|
+
def set_data(self, data):
|
|
183
|
+
"""
|
|
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
|
|
189
|
+
"""
|
|
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")
|
|
210
|
+
|
|
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.
|
|
215
|
+
|
|
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]}")
|
|
231
|
+
|
|
232
|
+
num_faces = data.shape[0] // 3
|
|
233
|
+
faces = np.zeros((num_faces, 13), dtype=np.float32)
|
|
234
|
+
|
|
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]}")
|
|
254
|
+
|
|
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
|
|
277
|
+
|
|
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)
|
|
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
|
+
|
|
337
|
+
def initialize_gl(self):
|
|
338
|
+
"""OpenGL initialization"""
|
|
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
|
|
352
|
+
|
|
353
|
+
def update_render_buffer(self):
|
|
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:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
# Initialize buffers on first call
|
|
364
|
+
if self.vao is None:
|
|
365
|
+
self.vao = glGenVertexArrays(1)
|
|
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
|
|
383
|
+
glEnableVertexAttribArray(1)
|
|
384
|
+
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(0))
|
|
385
|
+
glVertexAttribDivisor(1, 1)
|
|
386
|
+
|
|
387
|
+
# v1 (location 2) - vec3
|
|
388
|
+
glEnableVertexAttribArray(2)
|
|
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)
|
|
421
|
+
|
|
422
|
+
def update_setting(self):
|
|
423
|
+
"""Set fixed rendering parameters (called once during initialization)"""
|
|
424
|
+
if not self.need_update_setting:
|
|
425
|
+
return
|
|
426
|
+
# Set fixed uniforms for instanced shaders
|
|
427
|
+
set_uniform(self.program, int(self.enable_lighting), 'if_light')
|
|
428
|
+
set_uniform(self.program, 1, 'two_sided')
|
|
429
|
+
|
|
430
|
+
set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
|
|
431
|
+
set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
|
|
432
|
+
set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
|
|
433
|
+
set_uniform(self.program, float(self.specular_strength), 'specular_strength')
|
|
434
|
+
set_uniform(self.program, float(self.shininess), 'shininess')
|
|
435
|
+
set_uniform(self.program, float(self.alpha), 'alpha')
|
|
436
|
+
set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
|
|
437
|
+
self.need_update_setting = False
|
|
438
|
+
|
|
439
|
+
def paint(self):
|
|
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:
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
glUseProgram(self.program)
|
|
449
|
+
|
|
450
|
+
self.update_render_buffer()
|
|
451
|
+
self.update_setting()
|
|
452
|
+
|
|
453
|
+
view_matrix = self.glwidget().view_matrix
|
|
454
|
+
set_uniform(self.program, view_matrix, 'view')
|
|
455
|
+
project_matrix = self.glwidget().projection_matrix
|
|
456
|
+
set_uniform(self.program, project_matrix, 'projection')
|
|
457
|
+
view_pos = self.glwidget().center
|
|
458
|
+
set_uniform(self.program, np.array(view_pos), 'view_pos')
|
|
459
|
+
|
|
460
|
+
# Enable blending and depth testing
|
|
461
|
+
glEnable(GL_BLEND)
|
|
462
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
463
|
+
glEnable(GL_DEPTH_TEST)
|
|
464
|
+
glDisable(GL_CULL_FACE) # two-sided rendering
|
|
465
|
+
|
|
466
|
+
# Set line width
|
|
467
|
+
glLineWidth(self.line_width)
|
|
468
|
+
|
|
469
|
+
# Bind VAO (vertex positions are now in VBO attributes)
|
|
470
|
+
glBindVertexArray(self.vao)
|
|
471
|
+
|
|
472
|
+
if self.wireframe:
|
|
473
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
474
|
+
else:
|
|
475
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
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)
|
|
483
|
+
glBindVertexArray(0)
|
|
484
|
+
glDisable(GL_DEPTH_TEST)
|
|
485
|
+
glDisable(GL_BLEND)
|
|
486
|
+
glUseProgram(0)
|
|
487
|
+
|