ansys-pyensight-core 0.8.11__py3-none-any.whl → 0.8.12__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.
Potentially problematic release.
This version of ansys-pyensight-core might be problematic. Click here for more details.
- ansys/pyensight/core/deep_pixel_view.html +10 -10
- ansys/pyensight/core/libuserd.py +45 -37
- ansys/pyensight/core/locallauncher.py +2 -0
- ansys/pyensight/core/renderable.py +12 -3
- ansys/pyensight/core/utils/dsg_server.py +27 -5
- ansys/pyensight/core/utils/omniverse_cli.py +33 -24
- ansys/pyensight/core/utils/omniverse_dsg_server.py +3 -1
- ansys/pyensight/core/utils/omniverse_glb_server.py +493 -147
- ansys/pyensight/core/utils/parts.py +1 -1
- ansys/pyensight/core/utils/variables.py +1 -1
- {ansys_pyensight_core-0.8.11.dist-info → ansys_pyensight_core-0.8.12.dist-info}/METADATA +10 -8
- {ansys_pyensight_core-0.8.11.dist-info → ansys_pyensight_core-0.8.12.dist-info}/RECORD +14 -14
- {ansys_pyensight_core-0.8.11.dist-info → ansys_pyensight_core-0.8.12.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.8.11.dist-info → ansys_pyensight_core-0.8.12.dist-info}/WHEEL +0 -0
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import io
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
import uuid
|
|
7
7
|
|
|
8
|
+
from PIL import Image
|
|
8
9
|
from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
|
|
10
|
+
import ansys.pyensight.core.utils.dsg_server as dsg_server
|
|
11
|
+
import numpy
|
|
9
12
|
import pygltflib
|
|
10
13
|
|
|
11
14
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
12
|
-
from dsg_server import
|
|
15
|
+
from dsg_server import UpdateHandler # noqa: E402
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
class GLBSession(
|
|
18
|
+
class GLBSession(dsg_server.DSGSession):
|
|
16
19
|
def __init__(
|
|
17
20
|
self,
|
|
18
21
|
verbose: int = 0,
|
|
19
22
|
normalize_geometry: bool = False,
|
|
20
23
|
time_scale: float = 1.0,
|
|
24
|
+
vrmode: bool = False,
|
|
21
25
|
handler: UpdateHandler = UpdateHandler(),
|
|
22
26
|
):
|
|
23
27
|
"""
|
|
@@ -36,169 +40,299 @@ class GLBSession(object):
|
|
|
36
40
|
The default is not to remap coordinates.
|
|
37
41
|
time_scale : float
|
|
38
42
|
Scale time values by this factor after being read. The default is ``1.0``.
|
|
43
|
+
vrmode : bool
|
|
44
|
+
If True, do not include the camera in the output.
|
|
39
45
|
handler : UpdateHandler
|
|
40
46
|
This is an UpdateHandler subclass that is called back when the state of
|
|
41
47
|
a scene transfer changes. For example, methods are called when the
|
|
42
48
|
transfer begins or ends and when a Part (mesh block) is ready for processing.
|
|
43
49
|
"""
|
|
44
|
-
super().__init__(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-sys.float_info.max,
|
|
52
|
-
] # Min/max across all time steps
|
|
53
|
-
self._mesh_block_count = 0
|
|
54
|
-
self._node_idx: int = -1
|
|
55
|
-
self._variables: Dict[int, Any] = dict()
|
|
56
|
-
self._groups: Dict[int, Any] = dict()
|
|
57
|
-
self._part: Part = Part(self)
|
|
58
|
-
self._scene_bounds: Optional[List] = None
|
|
59
|
-
self._cur_timeline: List = [0.0, 0.0] # Start/End time for current update
|
|
60
|
-
self._callback_handler.session = self
|
|
61
|
-
# log any status changes to this file. external apps will be monitoring
|
|
62
|
-
self._status_file = os.environ.get("ANSYS_OV_SERVER_STATUS_FILENAME", "")
|
|
63
|
-
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
50
|
+
super().__init__(
|
|
51
|
+
verbose=verbose,
|
|
52
|
+
normalize_geometry=normalize_geometry,
|
|
53
|
+
time_scale=time_scale,
|
|
54
|
+
vrmode=vrmode,
|
|
55
|
+
handler=handler,
|
|
56
|
+
)
|
|
64
57
|
self._gltf: pygltflib.GLTF2 = pygltflib.GLTF2()
|
|
65
58
|
self._id_num: int = 0
|
|
59
|
+
self._node_idx: int = -1
|
|
60
|
+
self._glb_textures: dict = {}
|
|
61
|
+
self._scene_id: int = 0
|
|
66
62
|
|
|
67
|
-
def _reset(self):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
def _reset(self) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Reset the current state to prepare for a new dataset.
|
|
66
|
+
"""
|
|
67
|
+
super()._reset()
|
|
72
68
|
self._cur_timeline = [0.0, 0.0] # Start/End time for current update
|
|
73
69
|
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
74
70
|
self._gltf = pygltflib.GLTF2()
|
|
75
71
|
self._node_idx = -1
|
|
76
|
-
self._mesh_block_count = 0
|
|
77
72
|
self._id_num = 0
|
|
73
|
+
self._glb_textures = {}
|
|
74
|
+
self._scene_id = 0
|
|
78
75
|
|
|
79
76
|
def _next_id(self) -> int:
|
|
77
|
+
"""Simple sequential number source
|
|
78
|
+
Called whenever a unique integer is needed.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
int
|
|
83
|
+
A unique, monotonically increasing integer.
|
|
84
|
+
"""
|
|
80
85
|
self._id_num += 1
|
|
81
86
|
return self._id_num
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def mesh_block_count(self) -> int:
|
|
89
|
-
return self._mesh_block_count
|
|
90
|
-
|
|
91
|
-
@property
|
|
92
|
-
def normalize_geometry(self) -> bool:
|
|
93
|
-
return self._normalize_geometry
|
|
94
|
-
|
|
95
|
-
@normalize_geometry.setter
|
|
96
|
-
def normalize_geometry(self, value: bool) -> None:
|
|
97
|
-
self._normalize_geometry = value
|
|
98
|
-
|
|
99
|
-
@property
|
|
100
|
-
def variables(self) -> dict:
|
|
101
|
-
return self._variables
|
|
102
|
-
|
|
103
|
-
@property
|
|
104
|
-
def groups(self) -> dict:
|
|
105
|
-
return self._groups
|
|
88
|
+
def _map_material(self, glb_materialid: int, part_pb: Any) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Apply various material properties to part protocol buffer.
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
glb_materialid : int
|
|
95
|
+
The GLB material ID to use as the source information.
|
|
96
|
+
part_pb : Any
|
|
97
|
+
The DSG UpdatePart protocol buffer to update.
|
|
98
|
+
"""
|
|
99
|
+
mat = self._gltf.materials[glb_materialid]
|
|
100
|
+
color = [1.0, 1.0, 1.0, 1.0]
|
|
101
|
+
# Change the color if we can find one
|
|
102
|
+
if hasattr(mat, "pbrMetallicRoughness"):
|
|
103
|
+
if hasattr(mat.pbrMetallicRoughness, "baseColorFactor"):
|
|
104
|
+
color = mat.pbrMetallicRoughness.baseColorFactor
|
|
105
|
+
part_pb.fill_color.extend(color)
|
|
106
|
+
part_pb.line_color.extend(color)
|
|
107
|
+
# Constants for now
|
|
108
|
+
part_pb.ambient = 1.0
|
|
109
|
+
part_pb.diffuse = 1.0
|
|
110
|
+
part_pb.specular_intensity = 1.0
|
|
111
|
+
# if the material maps to a variable, set the variable id for coloring
|
|
112
|
+
glb_varid = self._find_variable_from_glb_mat(glb_materialid)
|
|
113
|
+
if glb_varid:
|
|
114
|
+
part_pb.color_variableid = glb_varid
|
|
115
|
+
|
|
116
|
+
def _parse_mesh(self, meshid: int, parentid: int, parentname: str) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Walk a mesh id found in a "node" instance. This amounts to
|
|
119
|
+
walking the list of "primitives" in the "meshes" list indexed
|
|
120
|
+
by the meshid.
|
|
110
121
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
meshid: int
|
|
125
|
+
The index of the mesh in the "meshes" list.
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return self._cur_timeline
|
|
127
|
+
parentid: int
|
|
128
|
+
The DSG parent id.
|
|
118
129
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
parentname: str
|
|
131
|
+
The name of the GROUP parent of the meshes.
|
|
132
|
+
"""
|
|
133
|
+
mesh = self._gltf.meshes[meshid]
|
|
134
|
+
for prim_idx, prim in enumerate(mesh.primitives):
|
|
135
|
+
# POINTS, LINES, LINE_LOOP, LINE_STRIP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN
|
|
136
|
+
mode = prim.mode
|
|
137
|
+
if mode not in (pygltflib.TRIANGLES, pygltflib.LINES, pygltflib.POINTS):
|
|
138
|
+
self.warn(
|
|
139
|
+
f"Unhandled connectivity {mode}. Currently only TRIANGLE and LINE connectivity is supported."
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
glb_materialid = prim.material
|
|
143
|
+
|
|
144
|
+
# GLB Prim -> DSG Part
|
|
145
|
+
part_name = f"{parentname}_prim{prim_idx}_"
|
|
146
|
+
cmd, part_pb = self._create_pb("PART", parent_id=parentid, name=part_name)
|
|
147
|
+
part_pb.render = dynamic_scene_graph_pb2.UpdatePart.RenderingMode.CONNECTIVITY
|
|
148
|
+
part_pb.shading = dynamic_scene_graph_pb2.UpdatePart.ShadingMode.NODAL
|
|
149
|
+
self._map_material(glb_materialid, part_pb)
|
|
150
|
+
part_dsg_id = part_pb.id
|
|
151
|
+
self._handle_update_command(cmd)
|
|
152
|
+
|
|
153
|
+
# GLB Attributes -> DSG Geom
|
|
154
|
+
conn = self._get_data(prim.indices, 0)
|
|
155
|
+
cmd, conn_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
|
|
156
|
+
if mode == pygltflib.TRIANGLES:
|
|
157
|
+
conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.TRIANGLES
|
|
158
|
+
elif mode == pygltflib.LINES:
|
|
159
|
+
conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.LINES
|
|
160
|
+
else:
|
|
161
|
+
conn_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.POINTS
|
|
162
|
+
conn_pb.int_array.extend(conn)
|
|
163
|
+
conn_pb.chunk_offset = 0
|
|
164
|
+
conn_pb.total_array_size = len(conn)
|
|
165
|
+
self._handle_update_command(cmd)
|
|
166
|
+
|
|
167
|
+
if prim.attributes.POSITION is not None:
|
|
168
|
+
verts = self._get_data(prim.attributes.POSITION)
|
|
169
|
+
cmd, verts_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
|
|
170
|
+
verts_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.COORDINATES
|
|
171
|
+
verts_pb.flt_array.extend(verts)
|
|
172
|
+
verts_pb.chunk_offset = 0
|
|
173
|
+
verts_pb.total_array_size = len(verts)
|
|
174
|
+
self._handle_update_command(cmd)
|
|
175
|
+
|
|
176
|
+
if prim.attributes.NORMAL is not None:
|
|
177
|
+
normals = self._get_data(prim.attributes.NORMAL)
|
|
178
|
+
cmd, normals_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
|
|
179
|
+
normals_pb.payload_type = dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_NORMALS
|
|
180
|
+
normals_pb.flt_array.extend(normals)
|
|
181
|
+
normals_pb.chunk_offset = 0
|
|
182
|
+
normals_pb.total_array_size = len(normals)
|
|
183
|
+
self._handle_update_command(cmd)
|
|
184
|
+
|
|
185
|
+
if prim.attributes.TEXCOORD_0 is not None:
|
|
186
|
+
# Note: texture coords are stored as VEC2, so we get 2 components back
|
|
187
|
+
texcoords = self._get_data(prim.attributes.TEXCOORD_0, components=2)
|
|
188
|
+
# we only want the 's' component of an s,t pairing
|
|
189
|
+
texcoords = texcoords[::2]
|
|
190
|
+
cmd, texcoords_pb = self._create_pb("GEOM", parent_id=part_dsg_id)
|
|
191
|
+
texcoords_pb.payload_type = (
|
|
192
|
+
dynamic_scene_graph_pb2.UpdateGeom.ArrayType.NODE_VARIABLE
|
|
193
|
+
)
|
|
194
|
+
texcoords_pb.flt_array.extend(texcoords)
|
|
195
|
+
texcoords_pb.chunk_offset = 0
|
|
196
|
+
texcoords_pb.total_array_size = len(texcoords)
|
|
197
|
+
glb_varid = self._find_variable_from_glb_mat(glb_materialid)
|
|
198
|
+
if glb_varid:
|
|
199
|
+
texcoords_pb.variable_id = glb_varid
|
|
200
|
+
self._handle_update_command(cmd)
|
|
201
|
+
|
|
202
|
+
def _get_data(
|
|
203
|
+
self,
|
|
204
|
+
accessorid: int,
|
|
205
|
+
components: int = 3,
|
|
206
|
+
) -> numpy.ndarray:
|
|
207
|
+
"""
|
|
208
|
+
Return the float buffer corresponding to the given accessorid. The id
|
|
209
|
+
is usually obtained from a primitive: primitive.attributes.POSITION
|
|
210
|
+
or primitive.attributes.NORMAL or primitive.attributes.TEXCOORD_0.
|
|
211
|
+
It can also come from primitive.indices. In that case, the number of
|
|
212
|
+
components needs to be set to 0.
|
|
124
213
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
accessorid: int
|
|
217
|
+
The accessor index of the primitive.
|
|
129
218
|
|
|
130
|
-
|
|
131
|
-
|
|
219
|
+
components: int
|
|
220
|
+
The number of floats per vertex for the values 1,2,3 if the number
|
|
221
|
+
of components is 0, read integer indices.
|
|
132
222
|
|
|
133
|
-
|
|
134
|
-
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
numpy.ndarray
|
|
226
|
+
The float buffer corresponding to the nodal data or an int buffer of connectivity.
|
|
135
227
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
228
|
+
dtypes = {}
|
|
229
|
+
dtypes[pygltflib.BYTE] = numpy.int8
|
|
230
|
+
dtypes[pygltflib.UNSIGNED_BYTE] = numpy.uint8
|
|
231
|
+
dtypes[pygltflib.SHORT] = numpy.int16
|
|
232
|
+
dtypes[pygltflib.UNSIGNED_SHORT] = numpy.uint16
|
|
233
|
+
dtypes[pygltflib.UNSIGNED_INT] = numpy.uint32
|
|
234
|
+
dtypes[pygltflib.FLOAT] = numpy.float32
|
|
235
|
+
|
|
236
|
+
binary_blob = self._gltf.binary_blob()
|
|
237
|
+
accessor = self._gltf.accessors[accessorid]
|
|
238
|
+
buffer_view = self._gltf.bufferViews[accessor.bufferView]
|
|
239
|
+
dtype = numpy.float32
|
|
240
|
+
data_dtype = dtypes[accessor.componentType]
|
|
241
|
+
count = accessor.count * components
|
|
242
|
+
# connectivity
|
|
243
|
+
if components == 0:
|
|
244
|
+
dtype = numpy.uint32
|
|
245
|
+
count = accessor.count
|
|
246
|
+
offset = buffer_view.byteOffset + accessor.byteOffset
|
|
247
|
+
blob = binary_blob[offset : offset + buffer_view.byteLength]
|
|
248
|
+
ret = numpy.frombuffer(blob, dtype=data_dtype, count=count)
|
|
249
|
+
if data_dtype != dtype:
|
|
250
|
+
return ret.astype(dtype)
|
|
251
|
+
return ret
|
|
252
|
+
|
|
253
|
+
def _walk_node(self, nodeid: int, parentid: int) -> None:
|
|
140
254
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
255
|
+
Given a node id (likely from walking a scenes array), walk the mesh
|
|
256
|
+
objects in the node. A "node" has the keys "mesh" and "name".
|
|
143
257
|
|
|
144
|
-
|
|
145
|
-
'status' : "working|idle",
|
|
146
|
-
'start_time' : timestamp_of_update_begin,
|
|
147
|
-
'processed_buffers' : number_of_protobuffers_processed,
|
|
148
|
-
'total_buffers' : number_of_protobuffers_total,
|
|
149
|
-
}
|
|
258
|
+
Each node has a single mesh object in it.
|
|
150
259
|
|
|
151
260
|
Parameters
|
|
152
261
|
----------
|
|
153
|
-
|
|
154
|
-
|
|
262
|
+
nodeid: int
|
|
263
|
+
The node id to walk.
|
|
264
|
+
|
|
265
|
+
parentid: int
|
|
266
|
+
The DSG parent id.
|
|
155
267
|
|
|
156
268
|
"""
|
|
157
|
-
if self._status_file:
|
|
158
|
-
current_time = time.time()
|
|
159
|
-
if timed:
|
|
160
|
-
last_time = self._status.get("last_time", 0.0)
|
|
161
|
-
if current_time - last_time < 1.0: # type: ignore
|
|
162
|
-
return
|
|
163
|
-
self._status["last_time"] = current_time
|
|
164
|
-
try:
|
|
165
|
-
message = json.dumps(self._status)
|
|
166
|
-
with open(self._status_file, "w") as status_file:
|
|
167
|
-
status_file.write(message)
|
|
168
|
-
except IOError:
|
|
169
|
-
pass # Note failure is expected here in some cases
|
|
170
|
-
|
|
171
|
-
def _parse_mesh(self, meshid: Any) -> None:
|
|
172
|
-
mesh = self._gltf.meshes[meshid]
|
|
173
|
-
logging.warning(f"mesh id: {meshid}, {mesh}")
|
|
174
|
-
for prim in mesh.primitives:
|
|
175
|
-
# TODO: GLB Prim -> DSG Part
|
|
176
|
-
self.log(f"prim {prim}")
|
|
177
|
-
# TODO: GLB Attributes -> DSG Geom
|
|
178
|
-
|
|
179
|
-
# mesh.mode, mesh.indices
|
|
180
|
-
# mesh.attributes(POSITION, NORMAL, COLOR_0, TEXCOORD_0, TEXCOORD_1)
|
|
181
|
-
# mesh.material
|
|
182
|
-
# mesh.images
|
|
183
|
-
|
|
184
|
-
def _walk_node(self, nodeid: Any) -> None:
|
|
185
269
|
node = self._gltf.nodes[nodeid]
|
|
186
|
-
self.
|
|
187
|
-
|
|
270
|
+
name = self._name(node)
|
|
271
|
+
matrix = self._transform(node)
|
|
272
|
+
|
|
273
|
+
# GLB node -> DSG Group
|
|
274
|
+
cmd, group_pb = self._create_pb("GROUP", parent_id=parentid, name=name)
|
|
275
|
+
group_pb.matrix4x4.extend(matrix)
|
|
276
|
+
self._handle_update_command(cmd)
|
|
188
277
|
|
|
189
278
|
if node.mesh is not None:
|
|
190
|
-
self._parse_mesh(node.mesh)
|
|
279
|
+
self._parse_mesh(node.mesh, group_pb.id, name)
|
|
191
280
|
|
|
192
281
|
# Handle node.rotation, node.translation, node.scale, node.matrix
|
|
193
282
|
for child_id in node.children:
|
|
194
|
-
self._walk_node(child_id)
|
|
283
|
+
self._walk_node(child_id, group_pb.id)
|
|
195
284
|
|
|
196
|
-
def
|
|
197
|
-
"""
|
|
285
|
+
def start_uploads(self, timeline: List[float]) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Begin an upload process for a potential collection of files.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
timeline : List[float]
|
|
292
|
+
The time values for the files span this range of values.
|
|
293
|
+
"""
|
|
294
|
+
self._scene_id = self._next_id()
|
|
295
|
+
self._cur_timeline = timeline
|
|
296
|
+
self._callback_handler.begin_update()
|
|
297
|
+
self._update_status_file()
|
|
298
|
+
|
|
299
|
+
def end_uploads(self) -> None:
|
|
300
|
+
"""
|
|
301
|
+
The upload process for the current collection of files is complete.
|
|
302
|
+
"""
|
|
303
|
+
self._reset()
|
|
304
|
+
self._update_status_file()
|
|
305
|
+
|
|
306
|
+
def _find_variable_from_glb_mat(self, glb_material_id: int) -> Optional[int]:
|
|
307
|
+
"""
|
|
308
|
+
Given a glb_material id, find the corresponding dsg variable id
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
glb_material_id : int
|
|
313
|
+
The material id from the glb file.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
Optional[int]
|
|
318
|
+
The dsg variable id or None, if no variable is found.
|
|
319
|
+
"""
|
|
320
|
+
value = self._glb_textures.get(glb_material_id, None)
|
|
321
|
+
if value is not None:
|
|
322
|
+
return value["pb"].id
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def upload_file(self, glb_filename: str, timeline: List[float] = [0.0, 0.0]) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Parse a GLB file and call out to the handler to present the data
|
|
198
328
|
to another interface (e.g. Omniverse)
|
|
199
329
|
|
|
200
330
|
Parameters
|
|
201
331
|
----------
|
|
332
|
+
timeline : List[float]
|
|
333
|
+
The first and last time value for which the content of this file should be
|
|
334
|
+
visible.
|
|
335
|
+
|
|
202
336
|
glb_filename : str
|
|
203
337
|
The name of the GLB file to parse
|
|
204
338
|
|
|
@@ -212,43 +346,253 @@ class GLBSession(object):
|
|
|
212
346
|
self._gltf = pygltflib.GLTF2().load(glb_filename)
|
|
213
347
|
self.log(f"File: {glb_filename} Info: {self._gltf.asset}")
|
|
214
348
|
|
|
215
|
-
|
|
216
|
-
self.
|
|
217
|
-
|
|
218
|
-
|
|
349
|
+
# check for GLTFWriter source
|
|
350
|
+
if (self._gltf.asset.generator is None) or (
|
|
351
|
+
("GLTF Writer" not in self._gltf.asset.generator)
|
|
352
|
+
and ("Ansys Ensight" not in self._gltf.asset.generator)
|
|
353
|
+
):
|
|
354
|
+
self.error(
|
|
355
|
+
f"Unable to process: {glb_filename} : Not written by GLTF Writer library"
|
|
356
|
+
)
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# Walk texture nodes -> DSG Variable buffers
|
|
360
|
+
for tex_idx, texture in enumerate(self._gltf.textures):
|
|
361
|
+
image = self._gltf.images[texture.source]
|
|
362
|
+
raw_png = self._gltf.get_data_from_buffer_uri(image.uri)
|
|
363
|
+
png_img = Image.open(io.BytesIO(raw_png))
|
|
364
|
+
raw_rgba = png_img.tobytes()
|
|
365
|
+
raw_rgba = raw_rgba[0 : len(raw_rgba) // png_img.size[1]]
|
|
366
|
+
var_name = "Variable_" + str(tex_idx)
|
|
367
|
+
cmd, var_pb = self._create_pb("VARIABLE", parent_id=self._scene_id, name=var_name)
|
|
368
|
+
var_pb.location = dynamic_scene_graph_pb2.UpdateVariable.VarLocation.NODAL
|
|
369
|
+
var_pb.dimension = dynamic_scene_graph_pb2.UpdateVariable.VarDimension.SCALAR
|
|
370
|
+
var_pb.undefined_value = -1e38
|
|
371
|
+
var_pb.pal_interp = (
|
|
372
|
+
dynamic_scene_graph_pb2.UpdateVariable.PaletteInterpolation.CONTINUOUS
|
|
373
|
+
)
|
|
374
|
+
var_pb.sub_levels = 0
|
|
375
|
+
var_pb.undefined_display = (
|
|
376
|
+
dynamic_scene_graph_pb2.UpdateVariable.UndefinedDisplay.AS_ZERO
|
|
377
|
+
)
|
|
378
|
+
var_pb.texture = raw_rgba
|
|
379
|
+
colors = numpy.frombuffer(raw_rgba, dtype=numpy.uint8)
|
|
380
|
+
colors.shape = (-1, 4)
|
|
381
|
+
num = len(colors)
|
|
382
|
+
levels = []
|
|
383
|
+
for i, c in enumerate(colors):
|
|
384
|
+
level = dynamic_scene_graph_pb2.VariableLevel()
|
|
385
|
+
level.value = float(i) / float(num - 1)
|
|
386
|
+
level.red = float(c[0]) / 255.0
|
|
387
|
+
level.green = float(c[1]) / 255.0
|
|
388
|
+
level.blue = float(c[2]) / 255.0
|
|
389
|
+
level.alpha = float(c[3]) / 255.0
|
|
390
|
+
levels.append(level)
|
|
391
|
+
var_pb.levels.extend(levels)
|
|
392
|
+
# create a map from GLB material index to glb
|
|
393
|
+
d = dict(pb=var_pb, idx=tex_idx)
|
|
394
|
+
# Find all the materials that map to this texture
|
|
395
|
+
for mat_idx, mat in enumerate(self._gltf.materials):
|
|
396
|
+
if not hasattr(mat, "pbrMetallicRoughness"):
|
|
397
|
+
continue
|
|
398
|
+
if not hasattr(mat.pbrMetallicRoughness, "baseColorTexture"):
|
|
399
|
+
continue
|
|
400
|
+
if not hasattr(mat.pbrMetallicRoughness.baseColorTexture, "index"):
|
|
401
|
+
continue
|
|
402
|
+
if mat.pbrMetallicRoughness.baseColorTexture.index == tex_idx:
|
|
403
|
+
material_index = mat_idx
|
|
404
|
+
# does this Variable/texture already exist?
|
|
405
|
+
duplicate = None
|
|
406
|
+
saved_id = var_pb.id
|
|
407
|
+
saved_name = var_pb.name
|
|
408
|
+
for key, value in self._glb_textures.items():
|
|
409
|
+
var_pb.name = value["pb"].name
|
|
410
|
+
var_pb.id = value["pb"].id
|
|
411
|
+
if value["pb"] == var_pb:
|
|
412
|
+
duplicate = key
|
|
413
|
+
break
|
|
414
|
+
var_pb.id = saved_id
|
|
415
|
+
var_pb.name = saved_name
|
|
416
|
+
# if a new texture, add the Variable and create an index to the material
|
|
417
|
+
if duplicate is None:
|
|
418
|
+
self._handle_update_command(cmd)
|
|
419
|
+
self._glb_textures[material_index] = d
|
|
420
|
+
else:
|
|
421
|
+
# create an additional reference to this variable from this material
|
|
422
|
+
self._glb_textures[material_index] = self._glb_textures[duplicate]
|
|
423
|
+
|
|
424
|
+
# GLB file: general layout
|
|
425
|
+
# scene: "default_index"
|
|
426
|
+
# scenes: [scene_index].nodes -> [node ids]
|
|
427
|
+
# was scene_id = self._gltf.scene
|
|
428
|
+
num_scenes = len(self._gltf.scenes)
|
|
429
|
+
for scene_idx in range(num_scenes):
|
|
430
|
+
# GLB Scene -> DSG View
|
|
431
|
+
cmd, view_pb = self._create_pb("VIEW", parent_id=self._scene_id)
|
|
432
|
+
view_pb.lookat.extend([0.0, 0.0, -1.0])
|
|
433
|
+
view_pb.lookfrom.extend([0.0, 0.0, 0.0])
|
|
434
|
+
view_pb.upvector.extend([0.0, 1.0, 0.0])
|
|
435
|
+
view_pb.timeline.extend(self._build_scene_timeline(scene_idx, timeline))
|
|
436
|
+
if len(self._gltf.cameras) > 0:
|
|
437
|
+
camera = self._gltf.cameras[0]
|
|
438
|
+
if camera.type == "orthographic":
|
|
439
|
+
view_pb.nearfar.extend(
|
|
440
|
+
[float(camera.orthographic.znear), float(camera.orthographic.zfar)]
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
view_pb.nearfar.extend(
|
|
444
|
+
[float(camera.perspective.znear), float(camera.perspective.zfar)]
|
|
445
|
+
)
|
|
446
|
+
view_pb.fieldofview = camera.perspective.yfov
|
|
447
|
+
view_pb.aspectratio = camera.aspectratio.aspectRatio
|
|
448
|
+
self._handle_update_command(cmd)
|
|
449
|
+
for node_id in self._gltf.scenes[scene_idx].nodes:
|
|
450
|
+
self._walk_node(node_id, view_pb.id)
|
|
451
|
+
self._finish_part()
|
|
219
452
|
|
|
220
|
-
# TODO: GLB Scene -> DSG View
|
|
221
|
-
|
|
222
|
-
# for present, just the default scene
|
|
223
|
-
for node_id in self._gltf.scenes[self._gltf.scene].nodes:
|
|
224
|
-
self._walk_node(node_id)
|
|
225
|
-
|
|
226
|
-
self._finish_part()
|
|
227
453
|
self._callback_handler.end_update()
|
|
228
454
|
|
|
229
455
|
except Exception as e:
|
|
230
|
-
|
|
456
|
+
import traceback
|
|
457
|
+
|
|
458
|
+
self.error(f"Unable to process: {glb_filename} : {e}")
|
|
459
|
+
traceback_str = "".join(traceback.format_tb(e.__traceback__))
|
|
460
|
+
logging.debug(f"Traceback: {traceback_str}")
|
|
231
461
|
ok = False
|
|
232
462
|
|
|
233
|
-
self._reset()
|
|
234
|
-
self._update_status_file()
|
|
235
463
|
return ok
|
|
236
464
|
|
|
237
|
-
def
|
|
238
|
-
"""
|
|
465
|
+
def _build_scene_timeline(self, scene_idx: int, input_timeline: List[float]) -> List[float]:
|
|
466
|
+
"""
|
|
467
|
+
For a given scene and externally supplied timeline, compute the timeline for the scene.
|
|
468
|
+
|
|
469
|
+
If the ANSYS_scene_time extension is present, use that value.
|
|
470
|
+
If there is only a single scene, return the supplied timeline.
|
|
471
|
+
If the supplied timeline is empty, use an integer timeline based on the number of scenes in the GLB file.
|
|
472
|
+
Carve up the timeline into chunks, one per scene.
|
|
473
|
+
|
|
474
|
+
Parameters
|
|
475
|
+
----------
|
|
476
|
+
scene_idx: int
|
|
477
|
+
The index of the scene to compute for.
|
|
478
|
+
|
|
479
|
+
input_timeline: List[float]
|
|
480
|
+
An externally supplied timeline.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
List[float]
|
|
485
|
+
The computed timeline.
|
|
486
|
+
"""
|
|
487
|
+
# if ANSYS_scene_time is used, time ranges will come from there
|
|
488
|
+
if "ANSYS_scene_time" in self._gltf.scenes[scene_idx].extensions:
|
|
489
|
+
return self._gltf.scenes[scene_idx].extensions["ANSYS_scene_time"]
|
|
490
|
+
# if there is only one scene, then use the input timeline
|
|
491
|
+
num_scenes = len(self._gltf.scenes)
|
|
492
|
+
if num_scenes == 1:
|
|
493
|
+
return input_timeline
|
|
494
|
+
# if the timeline has zero length, we make it the number of scenes
|
|
495
|
+
timeline = input_timeline
|
|
496
|
+
if timeline[1] - timeline[0] <= 0.0:
|
|
497
|
+
timeline = [0.0, float(num_scenes - 1)]
|
|
498
|
+
# carve time into the input timeline.
|
|
499
|
+
delta = (timeline[1] - timeline[0]) / float(num_scenes)
|
|
500
|
+
output: List[float] = []
|
|
501
|
+
output.append(float(scene_idx) * delta + timeline[0])
|
|
502
|
+
output.append(output[0] + delta)
|
|
503
|
+
return output
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _transform(node: Any) -> List[float]:
|
|
507
|
+
"""
|
|
508
|
+
Convert the node "matrix" or "translation", "rotation" and "scale" values into
|
|
509
|
+
a 4x4 matrix representation.
|
|
510
|
+
|
|
511
|
+
"nodes": [
|
|
512
|
+
{
|
|
513
|
+
"matrix": [
|
|
514
|
+
1,0,0,0,
|
|
515
|
+
0,1,0,0,
|
|
516
|
+
0,0,1,0,
|
|
517
|
+
5,6,7,1
|
|
518
|
+
],
|
|
519
|
+
...
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"translation":
|
|
523
|
+
[ 0,0,0 ],
|
|
524
|
+
"rotation":
|
|
525
|
+
[ 0,0,0,1 ],
|
|
526
|
+
"scale":
|
|
527
|
+
[ 1,1,1 ]
|
|
528
|
+
...
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
Parameters
|
|
533
|
+
----------
|
|
534
|
+
node: Any
|
|
535
|
+
The node to compute the matrix transform for.
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
List[float]
|
|
540
|
+
The 4x4 transformation matrix.
|
|
239
541
|
|
|
240
|
-
There is always a part being modified. This method completes the current part, committing
|
|
241
|
-
it to the handler.
|
|
242
542
|
"""
|
|
243
|
-
|
|
244
|
-
|
|
543
|
+
identity = numpy.identity(4)
|
|
544
|
+
if node.matrix:
|
|
545
|
+
tmp = numpy.array(node.matrix)
|
|
546
|
+
tmp.shape = (4, 4)
|
|
547
|
+
tmp = tmp.transpose()
|
|
548
|
+
return list(tmp.flatten())
|
|
549
|
+
if node.translation:
|
|
550
|
+
identity[3][0] = node.translation[0]
|
|
551
|
+
identity[3][1] = node.translation[1]
|
|
552
|
+
identity[3][2] = node.translation[2]
|
|
553
|
+
if node.rotation:
|
|
554
|
+
# In GLB, the null quat is [0,0,0,1] so reverse the vector here
|
|
555
|
+
q = [node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]]
|
|
556
|
+
rot = numpy.array(
|
|
557
|
+
[
|
|
558
|
+
[q[0], -q[1], -q[2], -q[3]],
|
|
559
|
+
[q[1], q[0], -q[3], q[2]],
|
|
560
|
+
[q[2], q[3], q[0], -q[1]],
|
|
561
|
+
[q[3], -q[2], q[1], q[0]],
|
|
562
|
+
]
|
|
563
|
+
)
|
|
564
|
+
identity = numpy.multiply(identity, rot)
|
|
565
|
+
if node.scale:
|
|
566
|
+
s = node.scale
|
|
567
|
+
scale = numpy.array(
|
|
568
|
+
[
|
|
569
|
+
[s[0], 0.0, 0.0, 0.0],
|
|
570
|
+
[0.0, s[1], 0.0, 0.0],
|
|
571
|
+
[0.0, 0.0, s[2], 0.0],
|
|
572
|
+
[0.0, 0.0, 0.0, 1.0],
|
|
573
|
+
]
|
|
574
|
+
)
|
|
575
|
+
identity = numpy.multiply(identity, scale)
|
|
576
|
+
return list(identity.flatten())
|
|
245
577
|
|
|
246
578
|
def _name(self, node: Any) -> str:
|
|
247
|
-
|
|
579
|
+
"""
|
|
580
|
+
Given a GLB node object, return the name of the node. If the node does not
|
|
581
|
+
have a name, give it a generated node.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
node: Any
|
|
586
|
+
The GLB node to get the name of.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
str
|
|
591
|
+
The name of the node.
|
|
592
|
+
"""
|
|
593
|
+
if hasattr(node, "name") and node.name:
|
|
248
594
|
return node.name
|
|
249
595
|
self._node_idx += 1
|
|
250
|
-
if self._node_idx == 0:
|
|
251
|
-
return "Root"
|
|
252
596
|
return f"Node_{self._node_idx}"
|
|
253
597
|
|
|
254
598
|
def _create_pb(
|
|
@@ -258,6 +602,7 @@ class GLBSession(object):
|
|
|
258
602
|
if cmd_type == "PART":
|
|
259
603
|
cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART
|
|
260
604
|
subcmd = cmd.update_part
|
|
605
|
+
subcmd.hash = str(uuid.uuid1())
|
|
261
606
|
elif cmd_type == "GROUP":
|
|
262
607
|
cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP
|
|
263
608
|
subcmd = cmd.update_group
|
|
@@ -267,6 +612,7 @@ class GLBSession(object):
|
|
|
267
612
|
elif cmd_type == "GEOM":
|
|
268
613
|
cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM
|
|
269
614
|
subcmd = cmd.update_geom
|
|
615
|
+
subcmd.hash = str(uuid.uuid1())
|
|
270
616
|
elif cmd_type == "VIEW":
|
|
271
617
|
cmd.command_type = dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW
|
|
272
618
|
subcmd = cmd.update_view
|