ansys-pyensight-core 0.11.0__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.
- ansys/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# Copyright 2020 NVIDIA Corporation
|
|
3
|
+
|
|
4
|
+
# contains the following notice:
|
|
5
|
+
#
|
|
6
|
+
###############################################################################
|
|
7
|
+
# Copyright 2020 NVIDIA Corporation
|
|
8
|
+
#
|
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10
|
+
# this software and associated documentation files (the "Software"), to deal in
|
|
11
|
+
# the Software without restriction, including without limitation the rights to
|
|
12
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
13
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
|
14
|
+
# subject to the following conditions:
|
|
15
|
+
#
|
|
16
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
# copies or substantial portions of the Software.
|
|
18
|
+
#
|
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
21
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
22
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
23
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
24
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
25
|
+
#
|
|
26
|
+
###############################################################################
|
|
27
|
+
import logging
|
|
28
|
+
import math
|
|
29
|
+
import os
|
|
30
|
+
import platform
|
|
31
|
+
import shutil
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
from typing import Any, Dict, List, Optional
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
from ansys.pyensight.core.utils.dsg_server import Part, UpdateHandler
|
|
38
|
+
import numpy
|
|
39
|
+
import png
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from pxr import Gf, Kind, Sdf, Usd, UsdGeom, UsdLux, UsdShade
|
|
43
|
+
except ModuleNotFoundError:
|
|
44
|
+
if sys.version_info.minor >= 14:
|
|
45
|
+
warnings.warn("USD Export not supported for Python >= 3.14")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
is_linux_arm64 = platform.system() == "Linux" and platform.machine() == "aarch64"
|
|
48
|
+
if is_linux_arm64:
|
|
49
|
+
warnings.warn("USD Export not supported on Linux ARM platforms")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OmniverseWrapper(object):
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
live_edit: bool = False,
|
|
57
|
+
destination: str = "",
|
|
58
|
+
line_width: float = 0.0,
|
|
59
|
+
) -> None:
|
|
60
|
+
# File extension. For debugging, .usda is sometimes helpful.
|
|
61
|
+
self._ext = ".usd"
|
|
62
|
+
self._cleaned_index = 0
|
|
63
|
+
self._cleaned_names: dict = {}
|
|
64
|
+
self._connectionStatusSubscription = None
|
|
65
|
+
self._stage = None
|
|
66
|
+
self._destinationPath: str = ""
|
|
67
|
+
self._old_stages: list = []
|
|
68
|
+
self._stagename: str = "dsg_scene" + self._ext
|
|
69
|
+
self._live_edit: bool = live_edit
|
|
70
|
+
if self._live_edit:
|
|
71
|
+
self._stagename = "dsg_scene.live"
|
|
72
|
+
# USD time slider will have 120 tick marks per second of animation time
|
|
73
|
+
self._time_codes_per_second: float = 120.0
|
|
74
|
+
# Omniverse content currently only scales correctly for scenes in cm. DJB, Feb 2025
|
|
75
|
+
self._units_per_meter: float = 100.0
|
|
76
|
+
self._up_axis: str = UsdGeom.Tokens.y
|
|
77
|
+
if destination:
|
|
78
|
+
self.destination = destination
|
|
79
|
+
|
|
80
|
+
self._line_width = line_width
|
|
81
|
+
self._centroid: Optional[list] = None
|
|
82
|
+
# Record the files per timestep, per mesh type. {part_name: {"surfaces": [], "lines": [], "points": []} }
|
|
83
|
+
self._time_files: dict = {}
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def destination(self) -> str:
|
|
87
|
+
"""The current output directory."""
|
|
88
|
+
return self._destinationPath
|
|
89
|
+
|
|
90
|
+
@destination.setter
|
|
91
|
+
def destination(self, directory: str) -> None:
|
|
92
|
+
self._destinationPath = directory
|
|
93
|
+
if not self.is_valid_destination(directory):
|
|
94
|
+
logging.warning(f"Invalid destination path: {directory}")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def line_width(self) -> float:
|
|
98
|
+
return self._line_width
|
|
99
|
+
|
|
100
|
+
@line_width.setter
|
|
101
|
+
def line_width(self, line_width: float) -> None:
|
|
102
|
+
self._line_width = line_width
|
|
103
|
+
|
|
104
|
+
def shutdown(self) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Shutdown the connection to Omniverse cleanly.
|
|
107
|
+
"""
|
|
108
|
+
self._connectionStatusSubscription = None
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def is_valid_destination(path: str) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Verify that the target path is a writeable directory.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
path
|
|
118
|
+
The path to check
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
True if the path is a writeable directory, False otherwise.
|
|
123
|
+
"""
|
|
124
|
+
return os.access(path, os.W_OK)
|
|
125
|
+
|
|
126
|
+
def stage_url(self, name: Optional[str] = None) -> str:
|
|
127
|
+
"""
|
|
128
|
+
For a given object name, create the URL for the item.
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
name: the name of the object to generate the URL for. If None, it will be the URL for the
|
|
132
|
+
stage name.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
The URL for the object.
|
|
137
|
+
"""
|
|
138
|
+
if name is None:
|
|
139
|
+
name = self._stagename
|
|
140
|
+
return os.path.join(self._destinationPath, name)
|
|
141
|
+
|
|
142
|
+
def delete_old_stages(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Remove all the stages included in the "_old_stages" list.
|
|
145
|
+
If a stage is in use and cannot be removed, keep its name in _old_stages
|
|
146
|
+
to retry later.
|
|
147
|
+
"""
|
|
148
|
+
stages_unremoved = list()
|
|
149
|
+
while self._old_stages:
|
|
150
|
+
stage = self._old_stages.pop()
|
|
151
|
+
try:
|
|
152
|
+
if os.path.isfile(stage):
|
|
153
|
+
os.remove(stage)
|
|
154
|
+
else:
|
|
155
|
+
shutil.rmtree(stage, ignore_errors=True, onerror=None)
|
|
156
|
+
except OSError:
|
|
157
|
+
if not stage.endswith("_manifest" + self._ext):
|
|
158
|
+
stages_unremoved.append(stage)
|
|
159
|
+
self._old_stages = stages_unremoved
|
|
160
|
+
|
|
161
|
+
def create_new_stage(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Create a new stage. using the current stage name.
|
|
164
|
+
"""
|
|
165
|
+
logging.info(f"Creating Omniverse stage: {self.stage_url()}")
|
|
166
|
+
if self._stage:
|
|
167
|
+
self._stage.Unload()
|
|
168
|
+
self._stage = None
|
|
169
|
+
self.delete_old_stages()
|
|
170
|
+
self._stage = Usd.Stage.CreateNew(self.stage_url())
|
|
171
|
+
# record the stage in the "_old_stages" list.
|
|
172
|
+
self._old_stages.append(self.stage_url())
|
|
173
|
+
UsdGeom.SetStageUpAxis(self._stage, self._up_axis)
|
|
174
|
+
UsdGeom.SetStageMetersPerUnit(self._stage, 1.0 / self._units_per_meter)
|
|
175
|
+
logging.info(f"Created stage: {self.stage_url()}")
|
|
176
|
+
|
|
177
|
+
def save_stage(self, comment: str = "") -> None:
|
|
178
|
+
"""
|
|
179
|
+
For live connections, save the current edit and allow live processing.
|
|
180
|
+
|
|
181
|
+
Presently, live connections are disabled.
|
|
182
|
+
"""
|
|
183
|
+
self._stage.GetRootLayer().Save() # type:ignore
|
|
184
|
+
|
|
185
|
+
def clear_cleaned_names(self) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Clear the list of cleaned names
|
|
188
|
+
"""
|
|
189
|
+
self._cleaned_names = {}
|
|
190
|
+
self._cleaned_index = 0
|
|
191
|
+
# Reset the list of files per timestep
|
|
192
|
+
self._time_files = {}
|
|
193
|
+
|
|
194
|
+
def clean_name(self, name: str, id_name: Any = None) -> str:
|
|
195
|
+
"""Generate a valid USD name
|
|
196
|
+
|
|
197
|
+
From a base (EnSight) varname, partname, etc. and the DSG id, generate
|
|
198
|
+
a unique, valid USD name. Save the names so that if the same name
|
|
199
|
+
comes in again, the previously computed name is returned and if the
|
|
200
|
+
manipulation results in a conflict, the name can be made unique.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
name:
|
|
205
|
+
The name to generate a USD name for.
|
|
206
|
+
|
|
207
|
+
id_name:
|
|
208
|
+
The DSG id associated with the DSG name, if any.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
A unique USD name.
|
|
213
|
+
"""
|
|
214
|
+
orig_name = name
|
|
215
|
+
# return any previously generated name
|
|
216
|
+
if (name, id_name) in self._cleaned_names:
|
|
217
|
+
return self._cleaned_names[(name, id_name)]
|
|
218
|
+
# replace invalid characters. EnSight uses a number of characters that are illegal in USD names.
|
|
219
|
+
replacements = {
|
|
220
|
+
ord("+"): "_",
|
|
221
|
+
ord("-"): "_",
|
|
222
|
+
ord("."): "_",
|
|
223
|
+
ord(":"): "_",
|
|
224
|
+
ord("["): "_",
|
|
225
|
+
ord("]"): "_",
|
|
226
|
+
ord("("): "_",
|
|
227
|
+
ord(")"): "_",
|
|
228
|
+
ord("<"): "_",
|
|
229
|
+
ord(">"): "_",
|
|
230
|
+
ord("/"): "_",
|
|
231
|
+
ord("="): "_",
|
|
232
|
+
ord(","): "_",
|
|
233
|
+
ord(" "): "_",
|
|
234
|
+
ord("\\"): "_",
|
|
235
|
+
ord("^"): "_",
|
|
236
|
+
ord("!"): "_",
|
|
237
|
+
ord("#"): "_",
|
|
238
|
+
ord("%"): "_",
|
|
239
|
+
ord("&"): "_",
|
|
240
|
+
}
|
|
241
|
+
name = name.translate(replacements)
|
|
242
|
+
if name[0].isdigit():
|
|
243
|
+
name = f"_{name}"
|
|
244
|
+
if id_name is not None:
|
|
245
|
+
name = name + "_" + str(id_name)
|
|
246
|
+
if name in self._cleaned_names.values():
|
|
247
|
+
# Make the name unique
|
|
248
|
+
while f"{name}_{self._cleaned_index}" in self._cleaned_names.values():
|
|
249
|
+
self._cleaned_index += 1
|
|
250
|
+
name = f"{name}_{self._cleaned_index}"
|
|
251
|
+
# store off the cleaned name
|
|
252
|
+
self._cleaned_names[(orig_name, id_name)] = name
|
|
253
|
+
return name
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def decompose_matrix(values: Any) -> Any:
|
|
257
|
+
"""
|
|
258
|
+
Decompose an array of floats (representing a 4x4 matrix) into scale, rotation and translation.
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
values:
|
|
262
|
+
16 values (input to Gf.Matrix4f CTOR)
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
(scale, rotation, translation)
|
|
267
|
+
"""
|
|
268
|
+
# ang_convert = 180.0/math.pi
|
|
269
|
+
ang_convert = 1.0
|
|
270
|
+
trans_convert = 1.0
|
|
271
|
+
m = Gf.Matrix4f(*values)
|
|
272
|
+
m = m.GetTranspose()
|
|
273
|
+
|
|
274
|
+
s = math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2])
|
|
275
|
+
# cleanup scale
|
|
276
|
+
m = m.RemoveScaleShear()
|
|
277
|
+
# r = m.ExtractRotation()
|
|
278
|
+
R = m.ExtractRotationMatrix()
|
|
279
|
+
r = [
|
|
280
|
+
math.atan2(R[2][1], R[2][2]) * ang_convert,
|
|
281
|
+
math.atan2(-R[2][0], 1.0) * ang_convert,
|
|
282
|
+
math.atan2(R[1][0], R[0][0]) * ang_convert,
|
|
283
|
+
]
|
|
284
|
+
t = m.ExtractTranslation()
|
|
285
|
+
t = [t[0] * trans_convert, t[1] * trans_convert, t[2] * trans_convert]
|
|
286
|
+
return s, r, t
|
|
287
|
+
|
|
288
|
+
# Common code to create the part manifest file and the file per timestep
|
|
289
|
+
def create_dsg_surfaces_file(
|
|
290
|
+
self,
|
|
291
|
+
file_url, # SdfPath, location on disk
|
|
292
|
+
part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
|
|
293
|
+
verts,
|
|
294
|
+
normals,
|
|
295
|
+
conn,
|
|
296
|
+
tcoords,
|
|
297
|
+
diffuse,
|
|
298
|
+
variable,
|
|
299
|
+
mat_info,
|
|
300
|
+
is_manifest: bool,
|
|
301
|
+
):
|
|
302
|
+
if is_manifest and file_url in self._old_stages:
|
|
303
|
+
return False
|
|
304
|
+
if not is_manifest and os.path.exists(file_url):
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
stage = Usd.Stage.CreateNew(file_url)
|
|
308
|
+
UsdGeom.SetStageUpAxis(stage, self._up_axis)
|
|
309
|
+
UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
|
|
310
|
+
self._old_stages.append(file_url)
|
|
311
|
+
|
|
312
|
+
part_prim = stage.OverridePrim(part_path)
|
|
313
|
+
|
|
314
|
+
surfaces_prim = self.create_xform_node(stage, part_path + "/surfaces")
|
|
315
|
+
mesh = UsdGeom.Mesh.Define(stage, str(surfaces_prim.GetPath()) + "/Mesh")
|
|
316
|
+
mesh.CreateDoubleSidedAttr().Set(True)
|
|
317
|
+
pt_attr = mesh.CreatePointsAttr()
|
|
318
|
+
if verts is not None:
|
|
319
|
+
pt_attr.Set(verts, 0)
|
|
320
|
+
norm_attr = mesh.CreateNormalsAttr()
|
|
321
|
+
if normals is not None:
|
|
322
|
+
norm_attr.Set(normals, 0)
|
|
323
|
+
fvc_attr = mesh.CreateFaceVertexCountsAttr()
|
|
324
|
+
fvi_attr = mesh.CreateFaceVertexIndicesAttr()
|
|
325
|
+
if conn is not None:
|
|
326
|
+
fvc_attr.Set([3] * (conn.size // 3), 0)
|
|
327
|
+
fvi_attr.Set(conn, 0)
|
|
328
|
+
|
|
329
|
+
primvarsAPI = UsdGeom.PrimvarsAPI(mesh)
|
|
330
|
+
texCoords = primvarsAPI.CreatePrimvar(
|
|
331
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
332
|
+
)
|
|
333
|
+
texCoords.SetInterpolation("vertex")
|
|
334
|
+
if tcoords is not None and variable is not None:
|
|
335
|
+
texCoords.Set(tcoords, 0)
|
|
336
|
+
|
|
337
|
+
stage.SetDefaultPrim(part_prim)
|
|
338
|
+
stage.SetStartTimeCode(0)
|
|
339
|
+
stage.SetEndTimeCode(0)
|
|
340
|
+
|
|
341
|
+
self.create_dsg_material(
|
|
342
|
+
stage,
|
|
343
|
+
mesh,
|
|
344
|
+
str(surfaces_prim.GetPath()),
|
|
345
|
+
diffuse=diffuse,
|
|
346
|
+
variable=variable,
|
|
347
|
+
mat_info=mat_info,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
stage.Save()
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
def create_dsg_mesh_block(
|
|
354
|
+
self,
|
|
355
|
+
part: Part,
|
|
356
|
+
name,
|
|
357
|
+
id,
|
|
358
|
+
part_hash,
|
|
359
|
+
parent_prim,
|
|
360
|
+
verts,
|
|
361
|
+
conn,
|
|
362
|
+
normals,
|
|
363
|
+
tcoords,
|
|
364
|
+
matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
|
365
|
+
diffuse=[1.0, 1.0, 1.0, 1.0],
|
|
366
|
+
variable=None,
|
|
367
|
+
timeline=[0.0, 0.0],
|
|
368
|
+
first_timestep=False,
|
|
369
|
+
mat_info={},
|
|
370
|
+
):
|
|
371
|
+
if self._stage is None:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# 1D texture map for variables https://graphics.pixar.com/usd/release/tut_simple_shading.html
|
|
375
|
+
# create the part usd object
|
|
376
|
+
part_base_name = self.clean_name(name)
|
|
377
|
+
partname = part_base_name + part_hash.hexdigest()
|
|
378
|
+
stage_name = "/Parts/" + partname + self._ext
|
|
379
|
+
part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
|
|
380
|
+
|
|
381
|
+
# Make the manifest file - once for all timesteps
|
|
382
|
+
part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
|
|
383
|
+
part_manifest_url = self.stage_url(part_manifest_url_relative)
|
|
384
|
+
created_file = self.create_dsg_surfaces_file(
|
|
385
|
+
part_manifest_url,
|
|
386
|
+
str(parent_prim.GetPath()),
|
|
387
|
+
None,
|
|
388
|
+
None,
|
|
389
|
+
None,
|
|
390
|
+
None,
|
|
391
|
+
diffuse,
|
|
392
|
+
variable,
|
|
393
|
+
mat_info,
|
|
394
|
+
True,
|
|
395
|
+
)
|
|
396
|
+
if created_file:
|
|
397
|
+
self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
|
|
398
|
+
|
|
399
|
+
# Make the per-timestep file
|
|
400
|
+
created_file = self.create_dsg_surfaces_file(
|
|
401
|
+
part_stage_url,
|
|
402
|
+
str(parent_prim.GetPath()),
|
|
403
|
+
verts,
|
|
404
|
+
normals,
|
|
405
|
+
conn,
|
|
406
|
+
tcoords,
|
|
407
|
+
diffuse,
|
|
408
|
+
variable,
|
|
409
|
+
mat_info,
|
|
410
|
+
False,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Glue the file into the main stage
|
|
414
|
+
path = parent_prim.GetPath().AppendChild("surfaces")
|
|
415
|
+
surfaces_prim = self._stage.OverridePrim(path)
|
|
416
|
+
self.add_timestep_valueclip(
|
|
417
|
+
part_base_name,
|
|
418
|
+
"surfaces",
|
|
419
|
+
surfaces_prim,
|
|
420
|
+
part_manifest_url_relative,
|
|
421
|
+
timeline,
|
|
422
|
+
stage_name,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
return part_stage_url
|
|
426
|
+
|
|
427
|
+
def get_time_files(self, part_name: str, mesh_type: str):
|
|
428
|
+
if part_name not in self._time_files:
|
|
429
|
+
self._time_files[part_name] = {"surfaces": [], "lines": [], "points": []}
|
|
430
|
+
return self._time_files[part_name][mesh_type]
|
|
431
|
+
|
|
432
|
+
def add_timestep_valueclip(
|
|
433
|
+
self,
|
|
434
|
+
part_name: str,
|
|
435
|
+
mesh_type: str,
|
|
436
|
+
part_prim: UsdGeom.Xform,
|
|
437
|
+
manifest_path: str,
|
|
438
|
+
timeline: List[float],
|
|
439
|
+
stage_name: str,
|
|
440
|
+
) -> None:
|
|
441
|
+
clips_api = Usd.ClipsAPI(part_prim)
|
|
442
|
+
asset_path = "." + stage_name
|
|
443
|
+
|
|
444
|
+
time_files = self.get_time_files(part_name, mesh_type)
|
|
445
|
+
|
|
446
|
+
if len(time_files) == 0 or time_files[-1][0] != asset_path:
|
|
447
|
+
time_files.append((asset_path, timeline[0]))
|
|
448
|
+
clips_api.SetClipAssetPaths([time_file[0] for time_file in time_files])
|
|
449
|
+
clips_api.SetClipActive(
|
|
450
|
+
[
|
|
451
|
+
(time_file[1] * self._time_codes_per_second, ii)
|
|
452
|
+
for ii, time_file in enumerate(time_files)
|
|
453
|
+
]
|
|
454
|
+
)
|
|
455
|
+
clips_api.SetClipTimes(
|
|
456
|
+
[
|
|
457
|
+
(time_file[1] * self._time_codes_per_second, 0)
|
|
458
|
+
for ii, time_file in enumerate(time_files)
|
|
459
|
+
]
|
|
460
|
+
)
|
|
461
|
+
clips_api.SetClipPrimPath(str(part_prim.GetPath()))
|
|
462
|
+
clips_api.SetClipManifestAssetPath(Sdf.AssetPath(manifest_path))
|
|
463
|
+
|
|
464
|
+
# Common code to create the part manifest file and the file per timestep
|
|
465
|
+
def create_dsg_lines_file(
|
|
466
|
+
self,
|
|
467
|
+
file_url, # SdfPath, location on disk
|
|
468
|
+
part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
|
|
469
|
+
verts,
|
|
470
|
+
width: float,
|
|
471
|
+
tcoords,
|
|
472
|
+
diffuse,
|
|
473
|
+
var_cmd,
|
|
474
|
+
mat_info,
|
|
475
|
+
is_manifest: bool,
|
|
476
|
+
):
|
|
477
|
+
if is_manifest and file_url in self._old_stages:
|
|
478
|
+
return False
|
|
479
|
+
if not is_manifest and os.path.exists(file_url):
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
stage = Usd.Stage.CreateNew(file_url)
|
|
483
|
+
UsdGeom.SetStageUpAxis(stage, self._up_axis)
|
|
484
|
+
UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
|
|
485
|
+
self._old_stages.append(file_url)
|
|
486
|
+
|
|
487
|
+
part_prim = stage.OverridePrim(part_path)
|
|
488
|
+
|
|
489
|
+
lines_prim = self.create_xform_node(stage, part_path + "/lines")
|
|
490
|
+
|
|
491
|
+
lines = UsdGeom.BasisCurves.Define(stage, str(lines_prim.GetPath()) + "/Lines")
|
|
492
|
+
lines.CreateDoubleSidedAttr().Set(True)
|
|
493
|
+
pt_attr = lines.CreatePointsAttr()
|
|
494
|
+
vc_attr = lines.CreateCurveVertexCountsAttr()
|
|
495
|
+
if verts is not None:
|
|
496
|
+
pt_attr.Set(verts, 0)
|
|
497
|
+
vc_attr.Set([2] * (verts.size // 6), 0)
|
|
498
|
+
lines.CreatePurposeAttr().Set("render")
|
|
499
|
+
lines.CreateTypeAttr().Set("linear")
|
|
500
|
+
lines.CreateWidthsAttr([width])
|
|
501
|
+
lines.SetWidthsInterpolation("constant")
|
|
502
|
+
|
|
503
|
+
# Rounded endpoint are a primvar
|
|
504
|
+
primvarsAPI = UsdGeom.PrimvarsAPI(lines)
|
|
505
|
+
endCaps = primvarsAPI.CreatePrimvar(
|
|
506
|
+
"endcaps", Sdf.ValueTypeNames.Int, UsdGeom.Tokens.constant
|
|
507
|
+
)
|
|
508
|
+
endCaps.Set(2) # Rounded = 2
|
|
509
|
+
|
|
510
|
+
prim = lines.GetPrim()
|
|
511
|
+
wireframe = width == 0.0
|
|
512
|
+
prim.CreateAttribute("omni:scene:visualization:drawWireframe", Sdf.ValueTypeNames.Bool).Set(
|
|
513
|
+
wireframe
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if (tcoords is not None) and var_cmd:
|
|
517
|
+
primvarsAPI = UsdGeom.PrimvarsAPI(lines)
|
|
518
|
+
texCoords = primvarsAPI.CreatePrimvar(
|
|
519
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
520
|
+
)
|
|
521
|
+
if tcoords is not None and var_cmd is not None:
|
|
522
|
+
texCoords.Set(tcoords, 0)
|
|
523
|
+
texCoords.SetInterpolation("vertex")
|
|
524
|
+
stage.SetDefaultPrim(part_prim)
|
|
525
|
+
stage.SetStartTimeCode(0)
|
|
526
|
+
stage.SetEndTimeCode(0)
|
|
527
|
+
|
|
528
|
+
self.create_dsg_material(
|
|
529
|
+
stage,
|
|
530
|
+
lines,
|
|
531
|
+
str(lines_prim.GetPath()),
|
|
532
|
+
diffuse=diffuse,
|
|
533
|
+
variable=var_cmd,
|
|
534
|
+
mat_info=mat_info,
|
|
535
|
+
)
|
|
536
|
+
stage.Save()
|
|
537
|
+
return True
|
|
538
|
+
|
|
539
|
+
def create_dsg_lines(
|
|
540
|
+
self,
|
|
541
|
+
name,
|
|
542
|
+
id,
|
|
543
|
+
part_hash,
|
|
544
|
+
parent_prim,
|
|
545
|
+
verts,
|
|
546
|
+
tcoords,
|
|
547
|
+
width,
|
|
548
|
+
matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
|
549
|
+
diffuse=[1.0, 1.0, 1.0, 1.0],
|
|
550
|
+
variable=None,
|
|
551
|
+
timeline=[0.0, 0.0],
|
|
552
|
+
first_timestep=False,
|
|
553
|
+
mat_info={},
|
|
554
|
+
):
|
|
555
|
+
# include the line width in the hash
|
|
556
|
+
part_hash.update(str(self.line_width).encode("utf-8"))
|
|
557
|
+
|
|
558
|
+
part_base_name = self.clean_name(name) + "_l"
|
|
559
|
+
partname = part_base_name + part_hash.hexdigest()
|
|
560
|
+
stage_name = "/Parts/" + partname + self._ext
|
|
561
|
+
part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
|
|
562
|
+
|
|
563
|
+
# Make the manifest file - once for all timesteps
|
|
564
|
+
part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
|
|
565
|
+
part_manifest_url = self.stage_url(part_manifest_url_relative)
|
|
566
|
+
created_file = self.create_dsg_lines_file(
|
|
567
|
+
part_manifest_url,
|
|
568
|
+
str(parent_prim.GetPath()),
|
|
569
|
+
None,
|
|
570
|
+
width,
|
|
571
|
+
None,
|
|
572
|
+
diffuse,
|
|
573
|
+
variable,
|
|
574
|
+
mat_info,
|
|
575
|
+
True,
|
|
576
|
+
)
|
|
577
|
+
if created_file:
|
|
578
|
+
self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
|
|
579
|
+
|
|
580
|
+
# Make the per-timestep file
|
|
581
|
+
created_file = self.create_dsg_lines_file(
|
|
582
|
+
part_stage_url,
|
|
583
|
+
str(parent_prim.GetPath()),
|
|
584
|
+
verts,
|
|
585
|
+
width,
|
|
586
|
+
tcoords,
|
|
587
|
+
diffuse,
|
|
588
|
+
variable,
|
|
589
|
+
mat_info,
|
|
590
|
+
False,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Glue the file into the main stage
|
|
594
|
+
path = parent_prim.GetPath().AppendChild("lines")
|
|
595
|
+
lines_prim = self._stage.OverridePrim(path)
|
|
596
|
+
self.add_timestep_valueclip(
|
|
597
|
+
part_base_name, "lines", lines_prim, part_manifest_url_relative, timeline, stage_name
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return part_stage_url
|
|
601
|
+
|
|
602
|
+
# Common code to create the part manifest file and the file per timestep
|
|
603
|
+
def create_dsg_points_file(
|
|
604
|
+
self,
|
|
605
|
+
file_url, # SdfPath, location on disk
|
|
606
|
+
part_path: str, # base path name, such as "/Root/Case_1/Isosurface_part"
|
|
607
|
+
verts,
|
|
608
|
+
sizes,
|
|
609
|
+
colors,
|
|
610
|
+
default_size: float,
|
|
611
|
+
default_color,
|
|
612
|
+
is_manifest: bool,
|
|
613
|
+
):
|
|
614
|
+
if is_manifest and file_url in self._old_stages:
|
|
615
|
+
return False
|
|
616
|
+
if not is_manifest and os.path.exists(file_url):
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
stage = Usd.Stage.CreateNew(file_url)
|
|
620
|
+
UsdGeom.SetStageUpAxis(stage, self._up_axis)
|
|
621
|
+
UsdGeom.SetStageMetersPerUnit(stage, 1.0 / self._units_per_meter)
|
|
622
|
+
self._old_stages.append(file_url)
|
|
623
|
+
|
|
624
|
+
part_prim = stage.OverridePrim(part_path)
|
|
625
|
+
points = UsdGeom.Points.Define(stage, part_path + "/points")
|
|
626
|
+
pt_attr = points.CreatePointsAttr()
|
|
627
|
+
w_attr = points.CreateWidthsAttr()
|
|
628
|
+
if verts is not None:
|
|
629
|
+
pt_attr.Set(verts, 0)
|
|
630
|
+
if sizes is not None and sizes.size == (verts.size // 3):
|
|
631
|
+
w_attr.Set(sizes, 0)
|
|
632
|
+
else:
|
|
633
|
+
w_attr.Set([default_size] * (verts.size // 3), 0)
|
|
634
|
+
|
|
635
|
+
colorAttr = points.GetPrim().GetAttribute("primvars:displayColor")
|
|
636
|
+
colorAttr.SetMetadata("interpolation", "vertex")
|
|
637
|
+
if verts is not None:
|
|
638
|
+
if colors is not None and colors.size == verts.size:
|
|
639
|
+
colorAttr.Set(colors, 0)
|
|
640
|
+
else:
|
|
641
|
+
colorAttr.Set([default_color[0:3]] * (verts.size // 3), 0)
|
|
642
|
+
|
|
643
|
+
stage.SetDefaultPrim(part_prim)
|
|
644
|
+
stage.SetStartTimeCode(0)
|
|
645
|
+
stage.SetEndTimeCode(0)
|
|
646
|
+
|
|
647
|
+
stage.Save()
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
def create_dsg_points(
|
|
651
|
+
self,
|
|
652
|
+
name,
|
|
653
|
+
id,
|
|
654
|
+
part_hash,
|
|
655
|
+
parent_prim,
|
|
656
|
+
verts,
|
|
657
|
+
sizes,
|
|
658
|
+
colors,
|
|
659
|
+
matrix=[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
|
660
|
+
default_size=1.0,
|
|
661
|
+
default_color=[1.0, 1.0, 1.0, 1.0],
|
|
662
|
+
timeline=[0.0, 0.0],
|
|
663
|
+
first_timestep=False,
|
|
664
|
+
):
|
|
665
|
+
part_base_name = self.clean_name(name) + "_p"
|
|
666
|
+
partname = part_base_name + part_hash.hexdigest()
|
|
667
|
+
stage_name = "/Parts/" + partname + self._ext
|
|
668
|
+
part_stage_url = self.stage_url(os.path.join("Parts", partname + self._ext))
|
|
669
|
+
|
|
670
|
+
# Make the manifest file - once for all timesteps
|
|
671
|
+
part_manifest_url_relative = "./Parts/" + part_base_name + "_manifest" + self._ext
|
|
672
|
+
part_manifest_url = self.stage_url(part_manifest_url_relative)
|
|
673
|
+
created_file = self.create_dsg_points_file(
|
|
674
|
+
part_manifest_url,
|
|
675
|
+
str(parent_prim.GetPath()),
|
|
676
|
+
None,
|
|
677
|
+
None,
|
|
678
|
+
None,
|
|
679
|
+
default_size,
|
|
680
|
+
default_color,
|
|
681
|
+
True,
|
|
682
|
+
)
|
|
683
|
+
if created_file:
|
|
684
|
+
self._stage.GetRootLayer().subLayerPaths.append(part_manifest_url_relative)
|
|
685
|
+
|
|
686
|
+
# Make the per-timestep file
|
|
687
|
+
created_file = self.create_dsg_points_file(
|
|
688
|
+
part_stage_url,
|
|
689
|
+
str(parent_prim.GetPath()),
|
|
690
|
+
verts,
|
|
691
|
+
sizes,
|
|
692
|
+
colors,
|
|
693
|
+
default_size,
|
|
694
|
+
default_color,
|
|
695
|
+
False,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Glue the file into the main stage
|
|
699
|
+
path = parent_prim.GetPath().AppendChild("points")
|
|
700
|
+
points_prim = self._stage.OverridePrim(path)
|
|
701
|
+
self.add_timestep_valueclip(
|
|
702
|
+
part_base_name, "points", points_prim, part_manifest_url_relative, timeline, stage_name
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
return part_stage_url
|
|
706
|
+
|
|
707
|
+
def create_dsg_material(
|
|
708
|
+
self, stage, mesh, root_name, diffuse=[1.0, 1.0, 1.0, 1.0], variable=None, mat_info={}
|
|
709
|
+
):
|
|
710
|
+
# https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html
|
|
711
|
+
# Use ior==1.0 to be more like EnSight - rays of light do not bend when passing through transparent objs
|
|
712
|
+
material = UsdShade.Material.Define(stage, root_name + "/Material")
|
|
713
|
+
pbrShader = UsdShade.Shader.Define(stage, root_name + "/Material/PBRShader")
|
|
714
|
+
pbrShader.CreateIdAttr("UsdPreviewSurface")
|
|
715
|
+
smoothness = mat_info.get("smoothness", 0.0)
|
|
716
|
+
pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0 - smoothness)
|
|
717
|
+
metallic = mat_info.get("metallic", 0.0)
|
|
718
|
+
pbrShader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(metallic)
|
|
719
|
+
opacity = mat_info.get("opacity", diffuse[3])
|
|
720
|
+
pbrShader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(opacity)
|
|
721
|
+
pbrShader.CreateInput("ior", Sdf.ValueTypeNames.Float).Set(1.0)
|
|
722
|
+
pbrShader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
|
|
723
|
+
if variable:
|
|
724
|
+
stReader = UsdShade.Shader.Define(stage, root_name + "/Material/stReader")
|
|
725
|
+
stReader.CreateIdAttr("UsdPrimvarReader_float2")
|
|
726
|
+
diffuseTextureSampler = UsdShade.Shader.Define(
|
|
727
|
+
stage, root_name + "/Material/diffuseTexture"
|
|
728
|
+
)
|
|
729
|
+
diffuseTextureSampler.CreateIdAttr("UsdUVTexture")
|
|
730
|
+
name = self.clean_name(variable.name)
|
|
731
|
+
filename = f"./Textures/palette_{name}.png"
|
|
732
|
+
diffuseTextureSampler.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(filename)
|
|
733
|
+
diffuseTextureSampler.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(
|
|
734
|
+
stReader.ConnectableAPI(), "result"
|
|
735
|
+
)
|
|
736
|
+
diffuseTextureSampler.CreateOutput("rgb", Sdf.ValueTypeNames.Float3)
|
|
737
|
+
pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).ConnectToSource(
|
|
738
|
+
diffuseTextureSampler.ConnectableAPI(), "rgb"
|
|
739
|
+
)
|
|
740
|
+
stInput = material.CreateInput("frame:stPrimvarName", Sdf.ValueTypeNames.Token)
|
|
741
|
+
stInput.Set("st")
|
|
742
|
+
stReader.CreateInput("varname", Sdf.ValueTypeNames.Token).ConnectToSource(stInput)
|
|
743
|
+
else:
|
|
744
|
+
# The colors are a mixture of content from the DSG PART protocol buffer
|
|
745
|
+
# and the JSON material block from the material_name field.
|
|
746
|
+
kd = 1.0
|
|
747
|
+
diffuse_color = [diffuse[0], diffuse[1], diffuse[2]]
|
|
748
|
+
ke = 1.0
|
|
749
|
+
emissive_color = [0.0, 0.0, 0.0]
|
|
750
|
+
ks = 1.0
|
|
751
|
+
specular_color = [0.0, 0.0, 0.0]
|
|
752
|
+
mat_name = mat_info.get("name", "")
|
|
753
|
+
if mat_name.startswith("ensight"):
|
|
754
|
+
diffuse_color = mat_info.get("diffuse", diffuse_color)
|
|
755
|
+
if mat_name != "ensight/Default":
|
|
756
|
+
ke = mat_info.get("ke", ke)
|
|
757
|
+
emissive_color = mat_info.get("emissive", emissive_color)
|
|
758
|
+
ks = mat_info.get("ks", ks)
|
|
759
|
+
specular_color = mat_info.get("specular", specular_color)
|
|
760
|
+
# Set the colors
|
|
761
|
+
color = Gf.Vec3f(diffuse_color[0] * kd, diffuse_color[1] * kd, diffuse_color[2] * kd)
|
|
762
|
+
pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
763
|
+
color = Gf.Vec3f(emissive_color[0] * ke, emissive_color[1] * ke, emissive_color[2] * ke)
|
|
764
|
+
pbrShader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
765
|
+
color = Gf.Vec3f(specular_color[0] * ks, specular_color[1] * ks, specular_color[2] * ks)
|
|
766
|
+
pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
767
|
+
|
|
768
|
+
material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
|
|
769
|
+
mat_binding_api = UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim())
|
|
770
|
+
mat_binding_api.Bind(material)
|
|
771
|
+
|
|
772
|
+
return material
|
|
773
|
+
|
|
774
|
+
def create_dsg_variable_textures(self, variables):
|
|
775
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
776
|
+
# make folder: {tempdir}/scratch/Textures/{palette_*.png}
|
|
777
|
+
os.makedirs(f"{tempdir}/scratch/Textures", exist_ok=True)
|
|
778
|
+
for var in variables.values():
|
|
779
|
+
data = bytearray(var.texture)
|
|
780
|
+
n_pixels = int(len(data) / 4)
|
|
781
|
+
row = []
|
|
782
|
+
for i in range(n_pixels):
|
|
783
|
+
row.append(data[i * 4 + 0])
|
|
784
|
+
row.append(data[i * 4 + 1])
|
|
785
|
+
row.append(data[i * 4 + 2])
|
|
786
|
+
io = png.Writer(width=n_pixels, height=2, bitdepth=8, greyscale=False)
|
|
787
|
+
rows = [row, row]
|
|
788
|
+
name = self.clean_name(var.name)
|
|
789
|
+
with open(f"{tempdir}/scratch/Textures/palette_{name}.png", "wb") as fp:
|
|
790
|
+
io.write(fp, rows)
|
|
791
|
+
uriPath = self._destinationPath + "/Parts/Textures"
|
|
792
|
+
shutil.rmtree(uriPath, ignore_errors=True, onerror=None)
|
|
793
|
+
shutil.copytree(f"{tempdir}/scratch/Textures", uriPath)
|
|
794
|
+
|
|
795
|
+
def create_xform_node(self, stage, name):
|
|
796
|
+
xform_node = UsdGeom.Xform.Define(stage, name)
|
|
797
|
+
xform_node.AddTranslateOp().Set((0, 0, 0))
|
|
798
|
+
xform_node.AddRotateXYZOp().Set((0, 0, 0))
|
|
799
|
+
xform_node.AddScaleOp().Set((1, 1, 1))
|
|
800
|
+
if self._centroid is not None:
|
|
801
|
+
xform_api = UsdGeom.XformCommonAPI(xform_node.GetPrim())
|
|
802
|
+
xform_api.SetPivot(Gf.Vec3f(self._centroid) * self._units_per_meter)
|
|
803
|
+
return xform_node
|
|
804
|
+
|
|
805
|
+
def create_dsg_root(self):
|
|
806
|
+
root_name = "/Root"
|
|
807
|
+
self.create_xform_node(self._stage, root_name)
|
|
808
|
+
|
|
809
|
+
# Define the defaultPrim as the /Root prim
|
|
810
|
+
root_prim = self._stage.GetPrimAtPath(root_name)
|
|
811
|
+
self._stage.SetDefaultPrim(root_prim)
|
|
812
|
+
return root_prim
|
|
813
|
+
|
|
814
|
+
def update_camera(self, camera):
|
|
815
|
+
if camera is not None:
|
|
816
|
+
cam_name = "/Root/Cam"
|
|
817
|
+
cam_prim = UsdGeom.Xform.Define(self._stage, cam_name)
|
|
818
|
+
s = self._units_per_meter
|
|
819
|
+
cam_pos = Gf.Vec3d(camera.lookfrom[0], camera.lookfrom[1], camera.lookfrom[2]) * s
|
|
820
|
+
target_pos = Gf.Vec3d(camera.lookat[0], camera.lookat[1], camera.lookat[2]) * s
|
|
821
|
+
|
|
822
|
+
up_vec = Gf.Vec3d(camera.upvector[0], camera.upvector[1], camera.upvector[2])
|
|
823
|
+
cam_prim = self._stage.GetPrimAtPath(cam_name)
|
|
824
|
+
geom_cam = UsdGeom.Camera(cam_prim)
|
|
825
|
+
if not geom_cam:
|
|
826
|
+
geom_cam = UsdGeom.Camera.Define(self._stage, cam_name)
|
|
827
|
+
# Set camera values
|
|
828
|
+
# center of interest attribute unique for Kit defines the pivot for tumbling the camera
|
|
829
|
+
# Set as an attribute on the prim
|
|
830
|
+
coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
|
|
831
|
+
if not coi_attr.IsValid():
|
|
832
|
+
coi_attr = cam_prim.CreateAttribute(
|
|
833
|
+
"omni:kit:centerOfInterest", Sdf.ValueTypeNames.Vector3d
|
|
834
|
+
)
|
|
835
|
+
coi_attr.Set(target_pos)
|
|
836
|
+
# get the camera
|
|
837
|
+
cam = geom_cam.GetCamera()
|
|
838
|
+
# LOL, not sure why is might be correct, but so far it seems to work???
|
|
839
|
+
cam.focalLength = camera.fieldofview
|
|
840
|
+
dist = (target_pos - cam_pos).GetLength()
|
|
841
|
+
cam.clippingRange = Gf.Range1f(0.1 * dist, 1000.0 * dist)
|
|
842
|
+
look_at = Gf.Matrix4d()
|
|
843
|
+
look_at.SetLookAt(cam_pos, target_pos, up_vec)
|
|
844
|
+
trans_row = look_at.GetRow(3)
|
|
845
|
+
trans_row = Gf.Vec4d(-trans_row[0], -trans_row[1], -trans_row[2], trans_row[3])
|
|
846
|
+
look_at.SetRow(3, trans_row)
|
|
847
|
+
cam.transform = look_at
|
|
848
|
+
|
|
849
|
+
# set the updated camera
|
|
850
|
+
geom_cam.SetFromCamera(cam)
|
|
851
|
+
|
|
852
|
+
def create_dsg_group(
|
|
853
|
+
self,
|
|
854
|
+
name: str,
|
|
855
|
+
parent_prim,
|
|
856
|
+
obj_type: Any = None,
|
|
857
|
+
matrix: List[float] = [
|
|
858
|
+
1.0,
|
|
859
|
+
0.0,
|
|
860
|
+
0.0,
|
|
861
|
+
0.0,
|
|
862
|
+
0.0,
|
|
863
|
+
1.0,
|
|
864
|
+
0.0,
|
|
865
|
+
0.0,
|
|
866
|
+
0.0,
|
|
867
|
+
0.0,
|
|
868
|
+
1.0,
|
|
869
|
+
0.0,
|
|
870
|
+
0.0,
|
|
871
|
+
0.0,
|
|
872
|
+
0.0,
|
|
873
|
+
1.0,
|
|
874
|
+
],
|
|
875
|
+
):
|
|
876
|
+
path = parent_prim.GetPath().AppendChild(self.clean_name(name))
|
|
877
|
+
group_prim = UsdGeom.Xform.Get(self._stage, path)
|
|
878
|
+
if not group_prim:
|
|
879
|
+
group_prim = self.create_xform_node(self._stage, path)
|
|
880
|
+
|
|
881
|
+
# Map kinds
|
|
882
|
+
kind = Kind.Tokens.group
|
|
883
|
+
if obj_type == "ENS_CASE":
|
|
884
|
+
kind = Kind.Tokens.assembly
|
|
885
|
+
elif obj_type == "ENS_PART":
|
|
886
|
+
kind = Kind.Tokens.component
|
|
887
|
+
Usd.ModelAPI(group_prim).SetKind(kind)
|
|
888
|
+
group_prim.GetPrim().SetDisplayName(name)
|
|
889
|
+
logging.info(f"Created group:'{name}' {str(obj_type)}")
|
|
890
|
+
|
|
891
|
+
return group_prim
|
|
892
|
+
|
|
893
|
+
def uploadMaterial(self):
|
|
894
|
+
uriPath = self._destinationPath + "/Materials"
|
|
895
|
+
shutil.rmtree(uriPath, ignore_errors=True, onerror=None)
|
|
896
|
+
fullpath = os.path.join(os.path.dirname(__file__), "resources", "Materials")
|
|
897
|
+
shutil.copytree(fullpath, uriPath)
|
|
898
|
+
|
|
899
|
+
# Create a dome light in the scene.
|
|
900
|
+
def createDomeLight(self, texturePath):
|
|
901
|
+
newLight = UsdLux.DomeLight.Define(self._stage, "/Root/DomeLight")
|
|
902
|
+
newLight.CreateIntensityAttr(2200.0)
|
|
903
|
+
newLight.CreateTextureFileAttr(texturePath)
|
|
904
|
+
newLight.CreateTextureFormatAttr("latlong")
|
|
905
|
+
|
|
906
|
+
# Set rotation on domelight
|
|
907
|
+
xForm = newLight
|
|
908
|
+
rotateOp = xForm.AddXformOp(UsdGeom.XformOp.TypeRotateZYX, UsdGeom.XformOp.PrecisionFloat)
|
|
909
|
+
rotateOp.Set(Gf.Vec3f(270, 0, 0))
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
class OmniverseUpdateHandler(UpdateHandler):
|
|
913
|
+
"""
|
|
914
|
+
Implement the Omniverse glue to a DSGSession instance
|
|
915
|
+
"""
|
|
916
|
+
|
|
917
|
+
def __init__(self, omni: OmniverseWrapper):
|
|
918
|
+
super().__init__()
|
|
919
|
+
self._omni = omni
|
|
920
|
+
self._group_prims: Dict[int, Any] = dict()
|
|
921
|
+
self._root_prim = None
|
|
922
|
+
self._sent_textures = False
|
|
923
|
+
self._case_xform_applied_to_camera = False
|
|
924
|
+
|
|
925
|
+
def add_group(self, id: int, view: bool = False) -> None:
|
|
926
|
+
super().add_group(id, view)
|
|
927
|
+
group = self.session.groups[id]
|
|
928
|
+
|
|
929
|
+
if not view:
|
|
930
|
+
# Capture changes in line/sphere sizes if it was not set from cli
|
|
931
|
+
width = self.get_dsg_cmd_attribute(group, "ANSYS_linewidth")
|
|
932
|
+
if width:
|
|
933
|
+
try:
|
|
934
|
+
self._omni.line_width = float(width)
|
|
935
|
+
except ValueError:
|
|
936
|
+
pass
|
|
937
|
+
|
|
938
|
+
parent_prim = self._group_prims[group.parent_id]
|
|
939
|
+
# get the EnSight object type and the transform matrix
|
|
940
|
+
obj_type = self.get_dsg_cmd_attribute(group, "ENS_OBJ_TYPE")
|
|
941
|
+
matrix = group.matrix4x4
|
|
942
|
+
# Is this a "case" group (it will contain part of the camera view in the matrix)
|
|
943
|
+
if obj_type == "ENS_CASE":
|
|
944
|
+
if self.session.scene_bounds is not None:
|
|
945
|
+
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
|
|
946
|
+
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
|
|
947
|
+
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
|
|
948
|
+
self._omni._centroid = [midx, midy, midz]
|
|
949
|
+
|
|
950
|
+
if not self.session.vrmode and not self._case_xform_applied_to_camera:
|
|
951
|
+
# if in camera mode, we need to update the camera matrix so we can
|
|
952
|
+
# use the identity matrix on this group. The camera should have been
|
|
953
|
+
# created in the "view" handler
|
|
954
|
+
self._case_xform_applied_to_camera = True
|
|
955
|
+
cam_name = "/Root/Cam"
|
|
956
|
+
cam_prim = self._omni._stage.GetPrimAtPath(cam_name) # type: ignore
|
|
957
|
+
geom_cam = UsdGeom.Camera(cam_prim)
|
|
958
|
+
# get the camera
|
|
959
|
+
cam = geom_cam.GetCamera()
|
|
960
|
+
c = cam.transform
|
|
961
|
+
m = Gf.Matrix4d(*matrix).GetTranspose()
|
|
962
|
+
s = self._omni._units_per_meter
|
|
963
|
+
trans = m.GetRow(3)
|
|
964
|
+
trans = Gf.Vec4d(trans[0] * s, trans[1] * s, trans[2] * s, trans[3])
|
|
965
|
+
m.SetRow(3, trans)
|
|
966
|
+
# move the model transform to the camera transform
|
|
967
|
+
cam.transform = c * m.GetInverse()
|
|
968
|
+
|
|
969
|
+
# Determine if the camera is principally more Y, or Z up. X up not supported.
|
|
970
|
+
# Omniverse' built in navigator tries to keep this direction up
|
|
971
|
+
# If the view is principally -Y, there is no good choice. +Y is least bad.
|
|
972
|
+
cam_upvec = Gf.Vec4d(0, 1, 0, 0) * cam.transform
|
|
973
|
+
if abs(cam_upvec[1]) >= abs(cam_upvec[2]):
|
|
974
|
+
self._up_axis = UsdGeom.Tokens.y
|
|
975
|
+
else:
|
|
976
|
+
self._up_axis = UsdGeom.Tokens.z
|
|
977
|
+
UsdGeom.SetStageUpAxis(self._omni._stage, self._up_axis)
|
|
978
|
+
|
|
979
|
+
# set the updated camera
|
|
980
|
+
geom_cam.SetFromCamera(cam)
|
|
981
|
+
# apply the inverse cam transform to move the center of interest
|
|
982
|
+
# from data space to camera space
|
|
983
|
+
coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
|
|
984
|
+
if coi_attr.IsValid():
|
|
985
|
+
coi_data = coi_attr.Get()
|
|
986
|
+
coi_cam = (
|
|
987
|
+
Gf.Vec4d(coi_data[0], coi_data[1], coi_data[2], 1.0)
|
|
988
|
+
* cam.transform.GetInverse()
|
|
989
|
+
)
|
|
990
|
+
coi_attr.Set(
|
|
991
|
+
Gf.Vec3d(
|
|
992
|
+
0,
|
|
993
|
+
0,
|
|
994
|
+
coi_cam[2] / coi_cam[3],
|
|
995
|
+
)
|
|
996
|
+
)
|
|
997
|
+
# use the camera view by default
|
|
998
|
+
self._omni._stage.GetRootLayer().customLayerData = { # type: ignore
|
|
999
|
+
"cameraSettings": {"boundCamera": "/Root/Cam"}
|
|
1000
|
+
}
|
|
1001
|
+
matrix = [
|
|
1002
|
+
1.0,
|
|
1003
|
+
0.0,
|
|
1004
|
+
0.0,
|
|
1005
|
+
0.0,
|
|
1006
|
+
0.0,
|
|
1007
|
+
1.0,
|
|
1008
|
+
0.0,
|
|
1009
|
+
0.0,
|
|
1010
|
+
0.0,
|
|
1011
|
+
0.0,
|
|
1012
|
+
1.0,
|
|
1013
|
+
0.0,
|
|
1014
|
+
0.0,
|
|
1015
|
+
0.0,
|
|
1016
|
+
0.0,
|
|
1017
|
+
1.0,
|
|
1018
|
+
]
|
|
1019
|
+
prim = self._omni.create_dsg_group(
|
|
1020
|
+
group.name, parent_prim, matrix=matrix, obj_type=obj_type
|
|
1021
|
+
)
|
|
1022
|
+
self._group_prims[id] = prim
|
|
1023
|
+
else:
|
|
1024
|
+
# Map a view command into a new Omniverse stage and populate it with materials/lights.
|
|
1025
|
+
# Create a new root stage in Omniverse
|
|
1026
|
+
|
|
1027
|
+
# Create or update the root group/camera
|
|
1028
|
+
if not self.session.vrmode and not self._case_xform_applied_to_camera:
|
|
1029
|
+
self._omni.update_camera(camera=group)
|
|
1030
|
+
|
|
1031
|
+
# record
|
|
1032
|
+
self._group_prims[id] = self._root_prim
|
|
1033
|
+
|
|
1034
|
+
if self._omni._stage is not None:
|
|
1035
|
+
self._omni._stage.SetStartTimeCode(
|
|
1036
|
+
self.session.time_limits[0] * self._omni._time_codes_per_second
|
|
1037
|
+
)
|
|
1038
|
+
self._omni._stage.SetEndTimeCode(
|
|
1039
|
+
self.session.time_limits[1] * self._omni._time_codes_per_second
|
|
1040
|
+
)
|
|
1041
|
+
self._omni._stage.SetTimeCodesPerSecond(self._omni._time_codes_per_second)
|
|
1042
|
+
|
|
1043
|
+
# Send the variable textures. Safe to do so once the first view is processed.
|
|
1044
|
+
if not self._sent_textures:
|
|
1045
|
+
self._omni.create_dsg_variable_textures(self.session.variables)
|
|
1046
|
+
self._sent_textures = True
|
|
1047
|
+
|
|
1048
|
+
def add_variable(self, id: int) -> None:
|
|
1049
|
+
super().add_variable(id)
|
|
1050
|
+
|
|
1051
|
+
def finalize_part(self, part: Part) -> None:
|
|
1052
|
+
# generate an Omniverse compliant mesh from the Part
|
|
1053
|
+
if part is None or part.cmd is None:
|
|
1054
|
+
return
|
|
1055
|
+
parent_prim = self._group_prims[part.cmd.parent_id]
|
|
1056
|
+
obj_id = self.session.mesh_block_count
|
|
1057
|
+
matrix = part.cmd.matrix4x4
|
|
1058
|
+
name = part.cmd.name
|
|
1059
|
+
color = [
|
|
1060
|
+
part.cmd.fill_color[0] * part.cmd.diffuse,
|
|
1061
|
+
part.cmd.fill_color[1] * part.cmd.diffuse,
|
|
1062
|
+
part.cmd.fill_color[2] * part.cmd.diffuse,
|
|
1063
|
+
part.cmd.fill_color[3],
|
|
1064
|
+
]
|
|
1065
|
+
|
|
1066
|
+
mat_info = part.material()
|
|
1067
|
+
if part.cmd.render == part.cmd.CONNECTIVITY:
|
|
1068
|
+
has_triangles = False
|
|
1069
|
+
command, verts, conn, normals, tcoords, var_cmd = part.nodal_surface_rep()
|
|
1070
|
+
if verts is not None:
|
|
1071
|
+
verts = numpy.multiply(verts, self._omni._units_per_meter)
|
|
1072
|
+
if command is not None:
|
|
1073
|
+
has_triangles = True
|
|
1074
|
+
# Generate the mesh block
|
|
1075
|
+
_ = self._omni.create_dsg_mesh_block(
|
|
1076
|
+
part,
|
|
1077
|
+
name,
|
|
1078
|
+
obj_id,
|
|
1079
|
+
part.hash,
|
|
1080
|
+
parent_prim,
|
|
1081
|
+
verts,
|
|
1082
|
+
conn,
|
|
1083
|
+
normals,
|
|
1084
|
+
tcoords,
|
|
1085
|
+
matrix=matrix,
|
|
1086
|
+
diffuse=color,
|
|
1087
|
+
variable=var_cmd,
|
|
1088
|
+
timeline=self.session.cur_timeline,
|
|
1089
|
+
first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
|
|
1090
|
+
mat_info=mat_info,
|
|
1091
|
+
)
|
|
1092
|
+
command, verts, tcoords, var_cmd = part.line_rep()
|
|
1093
|
+
if verts is not None:
|
|
1094
|
+
verts = numpy.multiply(verts, self._omni._units_per_meter)
|
|
1095
|
+
if command is not None:
|
|
1096
|
+
# If there are no triangle (ideally if these are not hidden line
|
|
1097
|
+
# edges), then use the base color for the part. If there are
|
|
1098
|
+
# triangles, then assume these are hidden line edges and use the
|
|
1099
|
+
# line_color.
|
|
1100
|
+
line_color = color
|
|
1101
|
+
if has_triangles:
|
|
1102
|
+
line_color = [
|
|
1103
|
+
part.cmd.line_color[0] * part.cmd.diffuse,
|
|
1104
|
+
part.cmd.line_color[1] * part.cmd.diffuse,
|
|
1105
|
+
part.cmd.line_color[2] * part.cmd.diffuse,
|
|
1106
|
+
part.cmd.line_color[3],
|
|
1107
|
+
]
|
|
1108
|
+
# TODO: texture coordinates on lines are currently invalid in Omniverse
|
|
1109
|
+
var_cmd = None
|
|
1110
|
+
tcoords = None
|
|
1111
|
+
# line info can come from self or our parent group
|
|
1112
|
+
width = self._omni.line_width
|
|
1113
|
+
# Allow the group to override
|
|
1114
|
+
group = self.session.find_group_pb(part.cmd.parent_id)
|
|
1115
|
+
if group:
|
|
1116
|
+
try:
|
|
1117
|
+
width = float(group.attributes.get("ANSYS_linewidth", str(width)))
|
|
1118
|
+
except ValueError:
|
|
1119
|
+
pass
|
|
1120
|
+
if width < 0.0:
|
|
1121
|
+
tmp = verts.reshape(-1, 3)
|
|
1122
|
+
mins = numpy.min(tmp, axis=0)
|
|
1123
|
+
maxs = numpy.max(tmp, axis=0)
|
|
1124
|
+
dx = maxs[0] - mins[0]
|
|
1125
|
+
dy = maxs[1] - mins[1]
|
|
1126
|
+
dz = maxs[2] - mins[2]
|
|
1127
|
+
diagonal = math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
1128
|
+
width = diagonal * math.fabs(width) / self._omni._units_per_meter
|
|
1129
|
+
if self._omni.line_width < 0.0:
|
|
1130
|
+
self._omni.line_width = width
|
|
1131
|
+
width = width * self._omni._units_per_meter
|
|
1132
|
+
# Generate the lines
|
|
1133
|
+
_ = self._omni.create_dsg_lines(
|
|
1134
|
+
name,
|
|
1135
|
+
obj_id,
|
|
1136
|
+
part.hash,
|
|
1137
|
+
parent_prim,
|
|
1138
|
+
verts,
|
|
1139
|
+
tcoords,
|
|
1140
|
+
width,
|
|
1141
|
+
matrix=matrix,
|
|
1142
|
+
diffuse=line_color,
|
|
1143
|
+
variable=var_cmd,
|
|
1144
|
+
timeline=self.session.cur_timeline,
|
|
1145
|
+
first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
elif part.cmd.render == part.cmd.NODES:
|
|
1149
|
+
command, verts, sizes, colors, var_cmd = part.point_rep()
|
|
1150
|
+
if verts is not None:
|
|
1151
|
+
verts = numpy.multiply(verts, self._omni._units_per_meter)
|
|
1152
|
+
if sizes is not None:
|
|
1153
|
+
sizes = numpy.multiply(sizes, self._omni._units_per_meter)
|
|
1154
|
+
if command is not None:
|
|
1155
|
+
_ = self._omni.create_dsg_points(
|
|
1156
|
+
name,
|
|
1157
|
+
obj_id,
|
|
1158
|
+
part.hash,
|
|
1159
|
+
parent_prim,
|
|
1160
|
+
verts,
|
|
1161
|
+
sizes,
|
|
1162
|
+
colors,
|
|
1163
|
+
matrix=matrix,
|
|
1164
|
+
default_size=part.cmd.node_size_default * self._omni._units_per_meter,
|
|
1165
|
+
default_color=color,
|
|
1166
|
+
timeline=self.session.cur_timeline,
|
|
1167
|
+
first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]),
|
|
1168
|
+
)
|
|
1169
|
+
super().finalize_part(part)
|
|
1170
|
+
|
|
1171
|
+
def start_connection(self) -> None:
|
|
1172
|
+
super().start_connection()
|
|
1173
|
+
|
|
1174
|
+
def end_connection(self) -> None:
|
|
1175
|
+
super().end_connection()
|
|
1176
|
+
|
|
1177
|
+
def begin_update(self) -> None:
|
|
1178
|
+
super().begin_update()
|
|
1179
|
+
# restart the name tables
|
|
1180
|
+
self._omni.clear_cleaned_names()
|
|
1181
|
+
# clear the group Omni prims list
|
|
1182
|
+
self._group_prims = dict()
|
|
1183
|
+
self._case_xform_applied_to_camera = False
|
|
1184
|
+
|
|
1185
|
+
self._omni.create_new_stage()
|
|
1186
|
+
self._root_prim = self._omni.create_dsg_root()
|
|
1187
|
+
# Create a distance and dome light in the scene
|
|
1188
|
+
self._omni.createDomeLight("./Materials/000_sky.exr")
|
|
1189
|
+
# Upload a material to the Omniverse server
|
|
1190
|
+
self._omni.uploadMaterial()
|
|
1191
|
+
self._sent_textures = False
|
|
1192
|
+
|
|
1193
|
+
def end_update(self) -> None:
|
|
1194
|
+
super().end_update()
|
|
1195
|
+
# Stage update complete
|
|
1196
|
+
self._omni.save_stage()
|