ansys-pyensight-core 0.7.6__py3-none-any.whl → 0.7.8__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/ensight_grpc.py +430 -411
- ansys/pyensight/core/renderable.py +42 -21
- ansys/pyensight/core/session.py +5 -2
- ansys/pyensight/core/utils/omniverse.py +239 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1354 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +54 -0
- ansys/pyensight/core/utils/variables.py +119 -107
- {ansys_pyensight_core-0.7.6.dist-info → ansys_pyensight_core-0.7.8.dist-info}/METADATA +4 -2
- {ansys_pyensight_core-0.7.6.dist-info → ansys_pyensight_core-0.7.8.dist-info}/RECORD +15 -8
- {ansys_pyensight_core-0.7.6.dist-info → ansys_pyensight_core-0.7.8.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.7.6.dist-info → ansys_pyensight_core-0.7.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file borrows heavily from the Omniverse Example Connector which
|
|
3
|
+
# contains the following notice:
|
|
4
|
+
#
|
|
5
|
+
###############################################################################
|
|
6
|
+
# Copyright 2020 NVIDIA Corporation
|
|
7
|
+
#
|
|
8
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
9
|
+
# this software and associated documentation files (the "Software"), to deal in
|
|
10
|
+
# the Software without restriction, including without limitation the rights to
|
|
11
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
12
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
|
13
|
+
# subject to the following conditions:
|
|
14
|
+
#
|
|
15
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
# copies or substantial portions of the Software.
|
|
17
|
+
#
|
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
20
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
21
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
22
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
23
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
24
|
+
#
|
|
25
|
+
###############################################################################
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import logging
|
|
29
|
+
import math
|
|
30
|
+
import os
|
|
31
|
+
import queue
|
|
32
|
+
import shutil
|
|
33
|
+
import sys
|
|
34
|
+
import threading
|
|
35
|
+
from typing import Any, List, Optional
|
|
36
|
+
|
|
37
|
+
from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
|
|
38
|
+
from ansys.pyensight.core import ensight_grpc
|
|
39
|
+
import numpy
|
|
40
|
+
import omni.client
|
|
41
|
+
import png
|
|
42
|
+
from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdShade
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OmniverseWrapper:
|
|
46
|
+
verbose = 0
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def logCallback(threadName: None, component: Any, level: Any, message: str) -> None:
|
|
50
|
+
if OmniverseWrapper.verbose:
|
|
51
|
+
logging.info(message)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def connectionStatusCallback(
|
|
55
|
+
url: Any, connectionStatus: "omni.client.ConnectionStatus"
|
|
56
|
+
) -> None:
|
|
57
|
+
if connectionStatus is omni.client.ConnectionStatus.CONNECT_ERROR:
|
|
58
|
+
sys.exit("[ERROR] Failed connection, exiting.")
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
live_edit: bool = False,
|
|
63
|
+
path: str = "omniverse://localhost/Users/test",
|
|
64
|
+
verbose: int = 0,
|
|
65
|
+
):
|
|
66
|
+
self._cleaned_index = 0
|
|
67
|
+
self._cleaned_names: dict = {}
|
|
68
|
+
self._connectionStatusSubscription = None
|
|
69
|
+
self._stage = None
|
|
70
|
+
self._destinationPath = path
|
|
71
|
+
self._old_stages: list = []
|
|
72
|
+
self._stagename = "dsg_scene.usd"
|
|
73
|
+
self._live_edit = live_edit
|
|
74
|
+
if self._live_edit:
|
|
75
|
+
self._stagename = "dsg_scene.live"
|
|
76
|
+
OmniverseWrapper.verbose = verbose
|
|
77
|
+
|
|
78
|
+
omni.client.set_log_callback(OmniverseWrapper.logCallback)
|
|
79
|
+
if verbose > 1:
|
|
80
|
+
omni.client.set_log_level(omni.client.LogLevel.DEBUG)
|
|
81
|
+
|
|
82
|
+
if not omni.client.initialize():
|
|
83
|
+
sys.exit("[ERROR] Unable to initialize Omniverse client, exiting.")
|
|
84
|
+
|
|
85
|
+
self._connectionStatusSubscription = omni.client.register_connection_status_callback(
|
|
86
|
+
OmniverseWrapper.connectionStatusCallback
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not self.isValidOmniUrl(self._destinationPath):
|
|
90
|
+
self.log("Note technically the Omniverse URL {self._destinationPath} is not valid")
|
|
91
|
+
|
|
92
|
+
def log(self, msg: str) -> None:
|
|
93
|
+
if OmniverseWrapper.verbose:
|
|
94
|
+
logging.info(msg)
|
|
95
|
+
|
|
96
|
+
def shutdown(self) -> None:
|
|
97
|
+
omni.client.live_wait_for_pending_updates()
|
|
98
|
+
self._connectionStatusSubscription = None
|
|
99
|
+
omni.client.shutdown()
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def isValidOmniUrl(url: str) -> bool:
|
|
103
|
+
omniURL = omni.client.break_url(url)
|
|
104
|
+
if omniURL.scheme == "omniverse" or omniURL.scheme == "omni":
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def stage_url(self, name: Optional[str] = None) -> str:
|
|
109
|
+
if name is None:
|
|
110
|
+
name = self._stagename
|
|
111
|
+
return self._destinationPath + "/" + name
|
|
112
|
+
|
|
113
|
+
def delete_old_stages(self) -> None:
|
|
114
|
+
while self._old_stages:
|
|
115
|
+
stage = self._old_stages.pop()
|
|
116
|
+
omni.client.delete(stage)
|
|
117
|
+
|
|
118
|
+
def create_new_stage(self) -> None:
|
|
119
|
+
self.log(f"Creating Omniverse stage: {self.stage_url()}")
|
|
120
|
+
if self._stage:
|
|
121
|
+
self._stage.Unload()
|
|
122
|
+
self._stage = None
|
|
123
|
+
self.delete_old_stages()
|
|
124
|
+
self._stage = Usd.Stage.CreateNew(self.stage_url())
|
|
125
|
+
self._old_stages.append(self.stage_url())
|
|
126
|
+
UsdGeom.SetStageUpAxis(self._stage, UsdGeom.Tokens.y)
|
|
127
|
+
# in M
|
|
128
|
+
UsdGeom.SetStageMetersPerUnit(self._stage, 1.0)
|
|
129
|
+
self.log(f"Created stage: {self.stage_url()}")
|
|
130
|
+
|
|
131
|
+
def save_stage(self) -> None:
|
|
132
|
+
self._stage.GetRootLayer().Save() # type:ignore
|
|
133
|
+
omni.client.live_process()
|
|
134
|
+
|
|
135
|
+
# This function will add a commented checkpoint to a file on Nucleus if:
|
|
136
|
+
# Live mode is disabled (live checkpoints are ill-supported)
|
|
137
|
+
# The Nucleus server supports checkpoints
|
|
138
|
+
def checkpoint(self, comment: str = "") -> None:
|
|
139
|
+
if self._live_edit:
|
|
140
|
+
return
|
|
141
|
+
result, serverInfo = omni.client.get_server_info(self.stage_url())
|
|
142
|
+
if result and serverInfo and serverInfo.checkpoints_enabled:
|
|
143
|
+
bForceCheckpoint = True
|
|
144
|
+
self.log(f"Adding checkpoint comment <{comment}> to stage <{self.stage_url()}>")
|
|
145
|
+
omni.client.create_checkpoint(self.stage_url(), comment, bForceCheckpoint)
|
|
146
|
+
|
|
147
|
+
def username(self, display: bool = True) -> Optional[str]:
|
|
148
|
+
result, serverInfo = omni.client.get_server_info(self.stage_url())
|
|
149
|
+
if serverInfo:
|
|
150
|
+
if display:
|
|
151
|
+
self.log(f"Connected username:{serverInfo.username}")
|
|
152
|
+
return serverInfo.username
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
h = 50.0
|
|
156
|
+
boxVertexIndices = [
|
|
157
|
+
0,
|
|
158
|
+
1,
|
|
159
|
+
2,
|
|
160
|
+
1,
|
|
161
|
+
3,
|
|
162
|
+
2,
|
|
163
|
+
4,
|
|
164
|
+
5,
|
|
165
|
+
6,
|
|
166
|
+
4,
|
|
167
|
+
6,
|
|
168
|
+
7,
|
|
169
|
+
8,
|
|
170
|
+
9,
|
|
171
|
+
10,
|
|
172
|
+
8,
|
|
173
|
+
10,
|
|
174
|
+
11,
|
|
175
|
+
12,
|
|
176
|
+
13,
|
|
177
|
+
14,
|
|
178
|
+
12,
|
|
179
|
+
14,
|
|
180
|
+
15,
|
|
181
|
+
16,
|
|
182
|
+
17,
|
|
183
|
+
18,
|
|
184
|
+
16,
|
|
185
|
+
18,
|
|
186
|
+
19,
|
|
187
|
+
20,
|
|
188
|
+
21,
|
|
189
|
+
22,
|
|
190
|
+
20,
|
|
191
|
+
22,
|
|
192
|
+
23,
|
|
193
|
+
]
|
|
194
|
+
boxVertexCounts = [3] * 12
|
|
195
|
+
boxNormals = [
|
|
196
|
+
(0, 0, -1),
|
|
197
|
+
(0, 0, -1),
|
|
198
|
+
(0, 0, -1),
|
|
199
|
+
(0, 0, -1),
|
|
200
|
+
(0, 0, 1),
|
|
201
|
+
(0, 0, 1),
|
|
202
|
+
(0, 0, 1),
|
|
203
|
+
(0, 0, 1),
|
|
204
|
+
(0, -1, 0),
|
|
205
|
+
(0, -1, 0),
|
|
206
|
+
(0, -1, 0),
|
|
207
|
+
(0, -1, 0),
|
|
208
|
+
(1, 0, 0),
|
|
209
|
+
(1, 0, 0),
|
|
210
|
+
(1, 0, 0),
|
|
211
|
+
(1, 0, 0),
|
|
212
|
+
(0, 1, 0),
|
|
213
|
+
(0, 1, 0),
|
|
214
|
+
(0, 1, 0),
|
|
215
|
+
(0, 1, 0),
|
|
216
|
+
(-1, 0, 0),
|
|
217
|
+
(-1, 0, 0),
|
|
218
|
+
(-1, 0, 0),
|
|
219
|
+
(-1, 0, 0),
|
|
220
|
+
]
|
|
221
|
+
boxPoints = [
|
|
222
|
+
(h, -h, -h),
|
|
223
|
+
(-h, -h, -h),
|
|
224
|
+
(h, h, -h),
|
|
225
|
+
(-h, h, -h),
|
|
226
|
+
(h, h, h),
|
|
227
|
+
(-h, h, h),
|
|
228
|
+
(-h, -h, h),
|
|
229
|
+
(h, -h, h),
|
|
230
|
+
(h, -h, h),
|
|
231
|
+
(-h, -h, h),
|
|
232
|
+
(-h, -h, -h),
|
|
233
|
+
(h, -h, -h),
|
|
234
|
+
(h, h, h),
|
|
235
|
+
(h, -h, h),
|
|
236
|
+
(h, -h, -h),
|
|
237
|
+
(h, h, -h),
|
|
238
|
+
(-h, h, h),
|
|
239
|
+
(h, h, h),
|
|
240
|
+
(h, h, -h),
|
|
241
|
+
(-h, h, -h),
|
|
242
|
+
(-h, -h, h),
|
|
243
|
+
(-h, h, h),
|
|
244
|
+
(-h, h, -h),
|
|
245
|
+
(-h, -h, -h),
|
|
246
|
+
]
|
|
247
|
+
boxUVs = [
|
|
248
|
+
(0, 0),
|
|
249
|
+
(0, 1),
|
|
250
|
+
(1, 1),
|
|
251
|
+
(1, 0),
|
|
252
|
+
(0, 0),
|
|
253
|
+
(0, 1),
|
|
254
|
+
(1, 1),
|
|
255
|
+
(1, 0),
|
|
256
|
+
(0, 0),
|
|
257
|
+
(0, 1),
|
|
258
|
+
(1, 1),
|
|
259
|
+
(1, 0),
|
|
260
|
+
(0, 0),
|
|
261
|
+
(0, 1),
|
|
262
|
+
(1, 1),
|
|
263
|
+
(1, 0),
|
|
264
|
+
(0, 0),
|
|
265
|
+
(0, 1),
|
|
266
|
+
(1, 1),
|
|
267
|
+
(1, 0),
|
|
268
|
+
(0, 0),
|
|
269
|
+
(0, 1),
|
|
270
|
+
(1, 1),
|
|
271
|
+
(1, 0),
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
def createBox(self, box_number: int = 0) -> "UsdGeom.Mesh":
|
|
275
|
+
rootUrl = "/Root"
|
|
276
|
+
boxUrl = rootUrl + "/Boxes/box_%d" % box_number
|
|
277
|
+
xformPrim = UsdGeom.Xform.Define(self._stage, rootUrl) # noqa: F841
|
|
278
|
+
# Define the defaultPrim as the /Root prim
|
|
279
|
+
rootPrim = self._stage.GetPrimAtPath(rootUrl) # type:ignore
|
|
280
|
+
self._stage.SetDefaultPrim(rootPrim) # type:ignore
|
|
281
|
+
boxPrim = UsdGeom.Mesh.Define(self._stage, boxUrl)
|
|
282
|
+
boxPrim.CreateDisplayColorAttr([(0.463, 0.725, 0.0)])
|
|
283
|
+
boxPrim.CreatePointsAttr(OmniverseWrapper.boxPoints)
|
|
284
|
+
boxPrim.CreateNormalsAttr(OmniverseWrapper.boxNormals)
|
|
285
|
+
boxPrim.CreateFaceVertexCountsAttr(OmniverseWrapper.boxVertexCounts)
|
|
286
|
+
boxPrim.CreateFaceVertexIndicesAttr(OmniverseWrapper.boxVertexIndices)
|
|
287
|
+
# USD 22.08 changed the primvar API
|
|
288
|
+
if hasattr(boxPrim, "CreatePrimvar"):
|
|
289
|
+
texCoords = boxPrim.CreatePrimvar(
|
|
290
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
primvarsAPI = UsdGeom.PrimvarsAPI(boxPrim)
|
|
294
|
+
texCoords = primvarsAPI.CreatePrimvar(
|
|
295
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
296
|
+
)
|
|
297
|
+
texCoords.Set(OmniverseWrapper.boxUVs)
|
|
298
|
+
texCoords.SetInterpolation("vertex")
|
|
299
|
+
if not boxPrim:
|
|
300
|
+
sys.exit("[ERROR] Failure to create box")
|
|
301
|
+
self.save_stage()
|
|
302
|
+
return boxPrim
|
|
303
|
+
|
|
304
|
+
def clear_cleaned_names(self) -> None:
|
|
305
|
+
"""Clear the list of cleaned names"""
|
|
306
|
+
self._cleaned_names = {}
|
|
307
|
+
self._cleaned_index = 0
|
|
308
|
+
|
|
309
|
+
def clean_name(self, name: str, id_name: Any = None) -> str:
|
|
310
|
+
"""Generate a vais USD name
|
|
311
|
+
|
|
312
|
+
From a base (EnSight) varname, partname, etc. and the DSG id, generate
|
|
313
|
+
a unique, valid USD name. Save the names so that if the same name
|
|
314
|
+
comes in again, the previously computed name is returned and if the
|
|
315
|
+
manipulation results in a conflict, the name can be made unique.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
name:
|
|
320
|
+
The name to generate a USD name for.
|
|
321
|
+
|
|
322
|
+
id_name:
|
|
323
|
+
The DSG id associated with the DSG name, if any.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
A unique USD name.
|
|
328
|
+
"""
|
|
329
|
+
# return any previously generated name
|
|
330
|
+
if (name, id_name) in self._cleaned_names:
|
|
331
|
+
return self._cleaned_names[(name, id_name)]
|
|
332
|
+
# replace invalid characters
|
|
333
|
+
name = name.replace("+", "_").replace("-", "_")
|
|
334
|
+
name = name.replace(".", "_").replace(":", "_")
|
|
335
|
+
name = name.replace("[", "_").replace("]", "_")
|
|
336
|
+
name = name.replace("(", "_").replace(")", "_")
|
|
337
|
+
name = name.replace("<", "_").replace(">", "_")
|
|
338
|
+
name = name.replace("/", "_").replace("=", "_")
|
|
339
|
+
name = name.replace(",", "_").replace(" ", "_")
|
|
340
|
+
if id_name is not None:
|
|
341
|
+
name = name + "_" + str(id_name)
|
|
342
|
+
if name in self._cleaned_names.values():
|
|
343
|
+
# Make the name unique
|
|
344
|
+
while f"{name}_{self._cleaned_index}" in self._cleaned_names.values():
|
|
345
|
+
self._cleaned_index += 1
|
|
346
|
+
name = f"{name}_{self._cleaned_index}"
|
|
347
|
+
# store off the cleaned name
|
|
348
|
+
self._cleaned_names[(name, id_name)] = name
|
|
349
|
+
return name
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def decompose_matrix(values: Any) -> Any:
|
|
353
|
+
# ang_convert = 180.0/math.pi
|
|
354
|
+
ang_convert = 1.0
|
|
355
|
+
trans_convert = 1.0
|
|
356
|
+
m = Gf.Matrix4f(*values)
|
|
357
|
+
m = m.GetTranspose()
|
|
358
|
+
|
|
359
|
+
s = math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2])
|
|
360
|
+
# cleanup scale
|
|
361
|
+
m = m.RemoveScaleShear()
|
|
362
|
+
# r = m.ExtractRotation()
|
|
363
|
+
R = m.ExtractRotationMatrix()
|
|
364
|
+
r = [
|
|
365
|
+
math.atan2(R[2][1], R[2][2]) * ang_convert,
|
|
366
|
+
math.atan2(-R[2][0], 1.0) * ang_convert,
|
|
367
|
+
math.atan2(R[1][0], R[0][0]) * ang_convert,
|
|
368
|
+
]
|
|
369
|
+
t = m.ExtractTranslation()
|
|
370
|
+
t = [t[0] * trans_convert, t[1] * trans_convert, t[2] * trans_convert]
|
|
371
|
+
return s, r, t
|
|
372
|
+
|
|
373
|
+
def create_dsg_mesh_block(
|
|
374
|
+
self,
|
|
375
|
+
name,
|
|
376
|
+
id,
|
|
377
|
+
parent_prim,
|
|
378
|
+
verts,
|
|
379
|
+
conn,
|
|
380
|
+
normals,
|
|
381
|
+
tcoords,
|
|
382
|
+
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],
|
|
383
|
+
diffuse=[1.0, 1.0, 1.0, 1.0],
|
|
384
|
+
variable=None,
|
|
385
|
+
):
|
|
386
|
+
# 1D texture map for variables https://graphics.pixar.com/usd/release/tut_simple_shading.html
|
|
387
|
+
# create the part usd object
|
|
388
|
+
partname = self.clean_name(name, id)
|
|
389
|
+
stage_name = "/Parts/" + partname + ".usd"
|
|
390
|
+
part_stage_url = self.stage_url(stage_name)
|
|
391
|
+
omni.client.delete(part_stage_url)
|
|
392
|
+
part_stage = Usd.Stage.CreateNew(part_stage_url)
|
|
393
|
+
self._old_stages.append(part_stage_url)
|
|
394
|
+
xform = UsdGeom.Xform.Define(part_stage, "/" + partname)
|
|
395
|
+
mesh = UsdGeom.Mesh.Define(part_stage, "/" + partname + "/Mesh")
|
|
396
|
+
# mesh.CreateDisplayColorAttr()
|
|
397
|
+
mesh.CreateDoubleSidedAttr().Set(True)
|
|
398
|
+
mesh.CreatePointsAttr(verts)
|
|
399
|
+
mesh.CreateNormalsAttr(normals)
|
|
400
|
+
mesh.CreateFaceVertexCountsAttr([3] * int(conn.size / 3))
|
|
401
|
+
mesh.CreateFaceVertexIndicesAttr(conn)
|
|
402
|
+
if (tcoords is not None) and variable:
|
|
403
|
+
# USD 22.08 changed the primvar API
|
|
404
|
+
if hasattr(mesh, "CreatePrimvar"):
|
|
405
|
+
texCoords = mesh.CreatePrimvar(
|
|
406
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
primvarsAPI = UsdGeom.PrimvarsAPI(mesh)
|
|
410
|
+
texCoords = primvarsAPI.CreatePrimvar(
|
|
411
|
+
"st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.varying
|
|
412
|
+
)
|
|
413
|
+
texCoords.Set(tcoords)
|
|
414
|
+
texCoords.SetInterpolation("vertex")
|
|
415
|
+
# sphere = part_stage.DefinePrim('/' + partname + '/sphere', 'Sphere')
|
|
416
|
+
part_prim = part_stage.GetPrimAtPath("/" + partname)
|
|
417
|
+
part_stage.SetDefaultPrim(part_prim)
|
|
418
|
+
|
|
419
|
+
# Currently, this will never happen, but it is a setup for rigid body transforms
|
|
420
|
+
# At present, the group transforms have been cooked into the vertices so this is not needed
|
|
421
|
+
matrixOp = xform.AddXformOp(UsdGeom.XformOp.TypeTransform, UsdGeom.XformOp.PrecisionDouble)
|
|
422
|
+
matrixOp.Set(Gf.Matrix4d(*matrix).GetTranspose())
|
|
423
|
+
|
|
424
|
+
self.create_dsg_material(
|
|
425
|
+
part_stage, mesh, "/" + partname, diffuse=diffuse, variable=variable
|
|
426
|
+
)
|
|
427
|
+
part_stage.GetRootLayer().Save()
|
|
428
|
+
|
|
429
|
+
# glue it into our stage
|
|
430
|
+
path = parent_prim.GetPath().AppendChild("part_ref_" + partname)
|
|
431
|
+
part_ref = self._stage.OverridePrim(path)
|
|
432
|
+
part_ref.GetReferences().AddReference("." + stage_name)
|
|
433
|
+
|
|
434
|
+
return part_stage_url
|
|
435
|
+
|
|
436
|
+
def create_dsg_material(
|
|
437
|
+
self, stage, mesh, root_name, diffuse=[1.0, 1.0, 1.0, 1.0], variable=None
|
|
438
|
+
):
|
|
439
|
+
# https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html
|
|
440
|
+
material = UsdShade.Material.Define(stage, root_name + "/Material")
|
|
441
|
+
pbrShader = UsdShade.Shader.Define(stage, root_name + "/Material/PBRShader")
|
|
442
|
+
pbrShader.CreateIdAttr("UsdPreviewSurface")
|
|
443
|
+
pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0)
|
|
444
|
+
pbrShader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(0.0)
|
|
445
|
+
pbrShader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(diffuse[3])
|
|
446
|
+
pbrShader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
|
|
447
|
+
if variable:
|
|
448
|
+
stReader = UsdShade.Shader.Define(stage, root_name + "/Material/stReader")
|
|
449
|
+
stReader.CreateIdAttr("UsdPrimvarReader_float2")
|
|
450
|
+
diffuseTextureSampler = UsdShade.Shader.Define(
|
|
451
|
+
stage, root_name + "/Material/diffuseTexture"
|
|
452
|
+
)
|
|
453
|
+
diffuseTextureSampler.CreateIdAttr("UsdUVTexture")
|
|
454
|
+
name = self.clean_name(variable.name)
|
|
455
|
+
filename = self._destinationPath + f"/Parts/Textures/palette_{name}.png"
|
|
456
|
+
diffuseTextureSampler.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(filename)
|
|
457
|
+
diffuseTextureSampler.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(
|
|
458
|
+
stReader.ConnectableAPI(), "result"
|
|
459
|
+
)
|
|
460
|
+
diffuseTextureSampler.CreateOutput("rgb", Sdf.ValueTypeNames.Float3)
|
|
461
|
+
pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).ConnectToSource(
|
|
462
|
+
diffuseTextureSampler.ConnectableAPI(), "rgb"
|
|
463
|
+
)
|
|
464
|
+
stInput = material.CreateInput("frame:stPrimvarName", Sdf.ValueTypeNames.Token)
|
|
465
|
+
stInput.Set("st")
|
|
466
|
+
stReader.CreateInput("varname", Sdf.ValueTypeNames.Token).ConnectToSource(stInput)
|
|
467
|
+
else:
|
|
468
|
+
scale = 1.0
|
|
469
|
+
color = Gf.Vec3f(diffuse[0] * scale, diffuse[1] * scale, diffuse[2] * scale)
|
|
470
|
+
pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
471
|
+
|
|
472
|
+
material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface")
|
|
473
|
+
UsdShade.MaterialBindingAPI(mesh).Bind(material)
|
|
474
|
+
|
|
475
|
+
return material
|
|
476
|
+
|
|
477
|
+
def create_dsg_variable_textures(self, variables):
|
|
478
|
+
# make folder: scratch/Textures/{palette_*.png}
|
|
479
|
+
shutil.rmtree("scratch", ignore_errors=True, onerror=None)
|
|
480
|
+
os.makedirs("scratch/Textures", exist_ok=True)
|
|
481
|
+
for var in variables.values():
|
|
482
|
+
data = bytearray(var.texture)
|
|
483
|
+
n_pixels = int(len(data) / 4)
|
|
484
|
+
row = []
|
|
485
|
+
for i in range(n_pixels):
|
|
486
|
+
row.append(data[i * 4 + 0])
|
|
487
|
+
row.append(data[i * 4 + 1])
|
|
488
|
+
row.append(data[i * 4 + 2])
|
|
489
|
+
io = png.Writer(width=n_pixels, height=2, bitdepth=8, greyscale=False)
|
|
490
|
+
rows = [row, row]
|
|
491
|
+
name = self.clean_name(var.name)
|
|
492
|
+
with open(f"scratch/Textures/palette_{name}.png", "wb") as fp:
|
|
493
|
+
io.write(fp, rows)
|
|
494
|
+
uriPath = self._destinationPath + "/Parts/Textures"
|
|
495
|
+
omni.client.delete(uriPath)
|
|
496
|
+
omni.client.copy("scratch/Textures", uriPath)
|
|
497
|
+
|
|
498
|
+
def create_dsg_root(self, camera=None):
|
|
499
|
+
root_name = "/Root"
|
|
500
|
+
root_prim = UsdGeom.Xform.Define(self._stage, root_name)
|
|
501
|
+
# Define the defaultPrim as the /Root prim
|
|
502
|
+
root_prim = self._stage.GetPrimAtPath(root_name)
|
|
503
|
+
self._stage.SetDefaultPrim(root_prim)
|
|
504
|
+
|
|
505
|
+
if camera is not None:
|
|
506
|
+
cam_name = "/Root/Cam"
|
|
507
|
+
cam_prim = UsdGeom.Xform.Define(self._stage, cam_name)
|
|
508
|
+
cam_pos = Gf.Vec3d(camera.lookfrom[0], camera.lookfrom[1], camera.lookfrom[2])
|
|
509
|
+
target_pos = Gf.Vec3d(camera.lookat[0], camera.lookat[1], camera.lookat[2])
|
|
510
|
+
up_vec = Gf.Vec3d(camera.upvector[0], camera.upvector[1], camera.upvector[2])
|
|
511
|
+
cam_prim = self._stage.GetPrimAtPath(cam_name)
|
|
512
|
+
geom_cam = UsdGeom.Camera(cam_prim)
|
|
513
|
+
if not geom_cam:
|
|
514
|
+
geom_cam = UsdGeom.Camera.Define(self._stage, cam_name)
|
|
515
|
+
# Set camera values
|
|
516
|
+
# center of interest attribute unique for Kit defines the pivot for tumbling the camera
|
|
517
|
+
# Set as an attribute on the prim
|
|
518
|
+
coi_attr = cam_prim.GetAttribute("omni:kit:centerOfInterest")
|
|
519
|
+
if not coi_attr.IsValid():
|
|
520
|
+
coi_attr = cam_prim.CreateAttribute(
|
|
521
|
+
"omni:kit:centerOfInterest", Sdf.ValueTypeNames.Vector3d
|
|
522
|
+
)
|
|
523
|
+
coi_attr.Set(target_pos)
|
|
524
|
+
# get the camera
|
|
525
|
+
cam = geom_cam.GetCamera()
|
|
526
|
+
# LOL, not sure why is might be correct, but so far it seems to work???
|
|
527
|
+
cam.focalLength = camera.fieldofview
|
|
528
|
+
cam.clippingRange = Gf.Range1f(0.1, 10)
|
|
529
|
+
look_at = Gf.Matrix4d()
|
|
530
|
+
look_at.SetLookAt(cam_pos, target_pos, up_vec)
|
|
531
|
+
trans_row = look_at.GetRow(3)
|
|
532
|
+
trans_row = Gf.Vec4d(-trans_row[0], -trans_row[1], -trans_row[2], trans_row[3])
|
|
533
|
+
look_at.SetRow(3, trans_row)
|
|
534
|
+
# print(look_at)
|
|
535
|
+
cam.transform = look_at
|
|
536
|
+
|
|
537
|
+
# set the updated camera
|
|
538
|
+
geom_cam.SetFromCamera(cam)
|
|
539
|
+
return root_prim
|
|
540
|
+
|
|
541
|
+
def create_dsg_group(
|
|
542
|
+
self,
|
|
543
|
+
name: str,
|
|
544
|
+
parent_prim,
|
|
545
|
+
obj_type: Any = None,
|
|
546
|
+
matrix: List[float] = [
|
|
547
|
+
1.0,
|
|
548
|
+
0.0,
|
|
549
|
+
0.0,
|
|
550
|
+
0.0,
|
|
551
|
+
0.0,
|
|
552
|
+
1.0,
|
|
553
|
+
0.0,
|
|
554
|
+
0.0,
|
|
555
|
+
0.0,
|
|
556
|
+
0.0,
|
|
557
|
+
1.0,
|
|
558
|
+
0.0,
|
|
559
|
+
0.0,
|
|
560
|
+
0.0,
|
|
561
|
+
0.0,
|
|
562
|
+
1.0,
|
|
563
|
+
],
|
|
564
|
+
):
|
|
565
|
+
path = parent_prim.GetPath().AppendChild(self.clean_name(name))
|
|
566
|
+
group_prim = UsdGeom.Xform.Define(self._stage, path)
|
|
567
|
+
# At present, the group transforms have been cooked into the vertices so this is not needed
|
|
568
|
+
matrixOp = group_prim.AddXformOp(
|
|
569
|
+
UsdGeom.XformOp.TypeTransform, UsdGeom.XformOp.PrecisionDouble
|
|
570
|
+
)
|
|
571
|
+
matrixOp.Set(Gf.Matrix4d(*matrix).GetTranspose())
|
|
572
|
+
self.log(f"Created group:'{name}' {str(obj_type)}")
|
|
573
|
+
return group_prim
|
|
574
|
+
|
|
575
|
+
def uploadMaterial(self):
|
|
576
|
+
uriPath = self._destinationPath + "/Materials"
|
|
577
|
+
omni.client.delete(uriPath)
|
|
578
|
+
omni.client.copy("resources/Materials", uriPath)
|
|
579
|
+
|
|
580
|
+
def createMaterial(self, mesh):
|
|
581
|
+
# Create a material instance for this in USD
|
|
582
|
+
materialName = "Fieldstone"
|
|
583
|
+
newMat = UsdShade.Material.Define(self._stage, "/Root/Looks/Fieldstone")
|
|
584
|
+
|
|
585
|
+
matPath = "/Root/Looks/Fieldstone"
|
|
586
|
+
|
|
587
|
+
# MDL Shader
|
|
588
|
+
# Create the MDL shader
|
|
589
|
+
mdlShader = UsdShade.Shader.Define(self._stage, matPath + "/Fieldstone")
|
|
590
|
+
mdlShader.CreateIdAttr("mdlMaterial")
|
|
591
|
+
|
|
592
|
+
mdlShaderModule = "./Materials/Fieldstone.mdl"
|
|
593
|
+
mdlShader.SetSourceAsset(mdlShaderModule, "mdl")
|
|
594
|
+
# mdlShader.GetPrim().CreateAttribute("info:mdl:sourceAsset:subIdentifier",
|
|
595
|
+
# Sdf.ValueTypeNames.Token, True).Set(materialName)
|
|
596
|
+
# mdlOutput = newMat.CreateSurfaceOutput("mdl")
|
|
597
|
+
# mdlOutput.ConnectToSource(mdlShader, "out")
|
|
598
|
+
mdlShader.SetSourceAssetSubIdentifier(materialName, "mdl")
|
|
599
|
+
shaderOutput = mdlShader.CreateOutput("out", Sdf.ValueTypeNames.Token)
|
|
600
|
+
shaderOutput.SetRenderType("material")
|
|
601
|
+
newMat.CreateSurfaceOutput("mdl").ConnectToSource(shaderOutput)
|
|
602
|
+
newMat.CreateDisplacementOutput("mdl").ConnectToSource(shaderOutput)
|
|
603
|
+
newMat.CreateVolumeOutput("mdl").ConnectToSource(shaderOutput)
|
|
604
|
+
|
|
605
|
+
# USD Preview Surface Shaders
|
|
606
|
+
|
|
607
|
+
# Create the "USD Primvar reader for float2" shader
|
|
608
|
+
primStShader = UsdShade.Shader.Define(self._stage, matPath + "/PrimST")
|
|
609
|
+
primStShader.CreateIdAttr("UsdPrimvarReader_float2")
|
|
610
|
+
primStShader.CreateOutput("result", Sdf.ValueTypeNames.Float2)
|
|
611
|
+
primStShader.CreateInput("varname", Sdf.ValueTypeNames.Token).Set("st")
|
|
612
|
+
|
|
613
|
+
# Create the "Diffuse Color Tex" shader
|
|
614
|
+
diffuseColorShader = UsdShade.Shader.Define(self._stage, matPath + "/DiffuseColorTex")
|
|
615
|
+
diffuseColorShader.CreateIdAttr("UsdUVTexture")
|
|
616
|
+
texInput = diffuseColorShader.CreateInput("file", Sdf.ValueTypeNames.Asset)
|
|
617
|
+
texInput.Set("./Materials/Fieldstone/Fieldstone_BaseColor.png")
|
|
618
|
+
texInput.GetAttr().SetColorSpace("RGB")
|
|
619
|
+
diffuseColorShader.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(
|
|
620
|
+
primStShader.CreateOutput("result", Sdf.ValueTypeNames.Float2)
|
|
621
|
+
)
|
|
622
|
+
diffuseColorShaderOutput = diffuseColorShader.CreateOutput("rgb", Sdf.ValueTypeNames.Float3)
|
|
623
|
+
|
|
624
|
+
# Create the "Normal Tex" shader
|
|
625
|
+
normalShader = UsdShade.Shader.Define(self._stage, matPath + "/NormalTex")
|
|
626
|
+
normalShader.CreateIdAttr("UsdUVTexture")
|
|
627
|
+
normalTexInput = normalShader.CreateInput("file", Sdf.ValueTypeNames.Asset)
|
|
628
|
+
normalTexInput.Set("./Materials/Fieldstone/Fieldstone_N.png")
|
|
629
|
+
normalTexInput.GetAttr().SetColorSpace("RAW")
|
|
630
|
+
normalShader.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(
|
|
631
|
+
primStShader.CreateOutput("result", Sdf.ValueTypeNames.Float2)
|
|
632
|
+
)
|
|
633
|
+
normalShaderOutput = normalShader.CreateOutput("rgb", Sdf.ValueTypeNames.Float3)
|
|
634
|
+
|
|
635
|
+
# Create the USD Preview Surface shader
|
|
636
|
+
usdPreviewSurfaceShader = UsdShade.Shader.Define(self._stage, matPath + "/PreviewSurface")
|
|
637
|
+
usdPreviewSurfaceShader.CreateIdAttr("UsdPreviewSurface")
|
|
638
|
+
diffuseColorInput = usdPreviewSurfaceShader.CreateInput(
|
|
639
|
+
"diffuseColor", Sdf.ValueTypeNames.Color3f
|
|
640
|
+
)
|
|
641
|
+
diffuseColorInput.ConnectToSource(diffuseColorShaderOutput)
|
|
642
|
+
normalInput = usdPreviewSurfaceShader.CreateInput("normal", Sdf.ValueTypeNames.Normal3f)
|
|
643
|
+
normalInput.ConnectToSource(normalShaderOutput)
|
|
644
|
+
|
|
645
|
+
# Set the linkage between material and USD Preview surface shader
|
|
646
|
+
# usdPreviewSurfaceOutput = newMat.CreateSurfaceOutput()
|
|
647
|
+
# usdPreviewSurfaceOutput.ConnectToSource(usdPreviewSurfaceShader, "surface")
|
|
648
|
+
# UsdShade.MaterialBindingAPI(mesh).Bind(newMat)
|
|
649
|
+
|
|
650
|
+
usdPreviewSurfaceShaderOutput = usdPreviewSurfaceShader.CreateOutput(
|
|
651
|
+
"surface", Sdf.ValueTypeNames.Token
|
|
652
|
+
)
|
|
653
|
+
usdPreviewSurfaceShaderOutput.SetRenderType("material")
|
|
654
|
+
newMat.CreateSurfaceOutput().ConnectToSource(usdPreviewSurfaceShaderOutput)
|
|
655
|
+
|
|
656
|
+
UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim()).Bind(newMat)
|
|
657
|
+
|
|
658
|
+
# self.save_stage()
|
|
659
|
+
|
|
660
|
+
# Create a distant light in the scene.
|
|
661
|
+
def createDistantLight(self):
|
|
662
|
+
newLight = UsdLux.DistantLight.Define(self._stage, "/Root/DistantLight")
|
|
663
|
+
newLight.CreateAngleAttr(0.53)
|
|
664
|
+
newLight.CreateColorAttr(Gf.Vec3f(1.0, 1.0, 0.745))
|
|
665
|
+
newLight.CreateIntensityAttr(500.0)
|
|
666
|
+
|
|
667
|
+
# self.save_stage()
|
|
668
|
+
|
|
669
|
+
# Create a dome light in the scene.
|
|
670
|
+
def createDomeLight(self, texturePath):
|
|
671
|
+
newLight = UsdLux.DomeLight.Define(self._stage, "/Root/DomeLight")
|
|
672
|
+
newLight.CreateIntensityAttr(2200.0)
|
|
673
|
+
newLight.CreateTextureFileAttr(texturePath)
|
|
674
|
+
newLight.CreateTextureFormatAttr("latlong")
|
|
675
|
+
|
|
676
|
+
# Set rotation on domelight
|
|
677
|
+
xForm = newLight
|
|
678
|
+
rotateOp = xForm.AddXformOp(UsdGeom.XformOp.TypeRotateZYX, UsdGeom.XformOp.PrecisionFloat)
|
|
679
|
+
rotateOp.Set(Gf.Vec3f(270, 0, 0))
|
|
680
|
+
|
|
681
|
+
# self.save_stage()
|
|
682
|
+
|
|
683
|
+
def createEmptyFolder(self, emptyFolderPath):
|
|
684
|
+
folder = self._destinationPath + emptyFolderPath
|
|
685
|
+
self.log(f"Creating new folder: {folder}")
|
|
686
|
+
result = omni.client.create_folder(folder)
|
|
687
|
+
self.log(f"Finished creating: {result.name}")
|
|
688
|
+
return result.name
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class Part(object):
|
|
692
|
+
def __init__(self, link: "DSGOmniverseLink"):
|
|
693
|
+
self._link = link
|
|
694
|
+
self.cmd: Optional[Any] = None
|
|
695
|
+
self.reset()
|
|
696
|
+
|
|
697
|
+
def reset(self, cmd: Any = None) -> None:
|
|
698
|
+
self.conn_tris = numpy.array([], dtype="int32")
|
|
699
|
+
self.conn_lines = numpy.array([], dtype="int32")
|
|
700
|
+
self.coords = numpy.array([], dtype="float32")
|
|
701
|
+
self.normals = numpy.array([], dtype="float32")
|
|
702
|
+
self.normals_elem = False
|
|
703
|
+
self.tcoords = numpy.array([], dtype="float32")
|
|
704
|
+
self.tcoords_var = None
|
|
705
|
+
self.tcoords_elem = False
|
|
706
|
+
self.cmd = cmd
|
|
707
|
+
|
|
708
|
+
def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None:
|
|
709
|
+
if cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.COORDINATES:
|
|
710
|
+
if self.coords.size != cmd.total_array_size:
|
|
711
|
+
self.coords = numpy.resize(self.coords, cmd.total_array_size)
|
|
712
|
+
self.coords[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
|
|
713
|
+
elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.TRIANGLES:
|
|
714
|
+
if self.conn_tris.size != cmd.total_array_size:
|
|
715
|
+
self.conn_tris = numpy.resize(self.conn_tris, cmd.total_array_size)
|
|
716
|
+
self.conn_tris[cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)] = cmd.int_array
|
|
717
|
+
elif cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.LINES:
|
|
718
|
+
if self.conn_lines.size != cmd.total_array_size:
|
|
719
|
+
self.conn_lines = numpy.resize(self.conn_lines, cmd.total_array_size)
|
|
720
|
+
self.conn_lines[
|
|
721
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.int_array)
|
|
722
|
+
] = cmd.int_array
|
|
723
|
+
elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS) or (
|
|
724
|
+
cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_NORMALS
|
|
725
|
+
):
|
|
726
|
+
self.normals_elem = cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_NORMALS
|
|
727
|
+
if self.normals.size != cmd.total_array_size:
|
|
728
|
+
self.normals = numpy.resize(self.normals, cmd.total_array_size)
|
|
729
|
+
self.normals[cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)] = cmd.flt_array
|
|
730
|
+
elif (cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE) or (
|
|
731
|
+
cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.NODE_VARIABLE
|
|
732
|
+
):
|
|
733
|
+
# Get the variable definition
|
|
734
|
+
if cmd.variable_id in self._link._variables:
|
|
735
|
+
self.tcoords_var = cmd.variable_id
|
|
736
|
+
self.tcoords_elem = (
|
|
737
|
+
cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE
|
|
738
|
+
)
|
|
739
|
+
if self.tcoords.size != cmd.total_array_size:
|
|
740
|
+
self.tcoords = numpy.resize(self.tcoords, cmd.total_array_size)
|
|
741
|
+
self.tcoords[
|
|
742
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
|
|
743
|
+
] = cmd.flt_array
|
|
744
|
+
else:
|
|
745
|
+
self.tcoords_var = None
|
|
746
|
+
|
|
747
|
+
def build(self):
|
|
748
|
+
if self.cmd is None:
|
|
749
|
+
return
|
|
750
|
+
if self.conn_lines.size:
|
|
751
|
+
self._link.log(
|
|
752
|
+
f"Note, part '{self.cmd.name}' has lines which are not currently supported."
|
|
753
|
+
)
|
|
754
|
+
self.cmd = None
|
|
755
|
+
return
|
|
756
|
+
verts = self.coords
|
|
757
|
+
if self._link._normalize_geometry and self._link._scene_bounds is not None:
|
|
758
|
+
midx = (self._link._scene_bounds[3] + self._link._scene_bounds[0]) * 0.5
|
|
759
|
+
midy = (self._link._scene_bounds[4] + self._link._scene_bounds[1]) * 0.5
|
|
760
|
+
midz = (self._link._scene_bounds[5] + self._link._scene_bounds[2]) * 0.5
|
|
761
|
+
dx = self._link._scene_bounds[3] - self._link._scene_bounds[0]
|
|
762
|
+
dy = self._link._scene_bounds[4] - self._link._scene_bounds[1]
|
|
763
|
+
dz = self._link._scene_bounds[5] - self._link._scene_bounds[2]
|
|
764
|
+
s = dx
|
|
765
|
+
if dy > s:
|
|
766
|
+
s = dy
|
|
767
|
+
if dz > s:
|
|
768
|
+
s = dz
|
|
769
|
+
if s == 0:
|
|
770
|
+
s = 1.0
|
|
771
|
+
num_verts = int(verts.size / 3)
|
|
772
|
+
for i in range(num_verts):
|
|
773
|
+
j = i * 3
|
|
774
|
+
verts[j + 0] = (verts[j + 0] - midx) / s
|
|
775
|
+
verts[j + 1] = (verts[j + 1] - midy) / s
|
|
776
|
+
verts[j + 2] = (verts[j + 2] - midz) / s
|
|
777
|
+
|
|
778
|
+
conn = self.conn_tris
|
|
779
|
+
normals = self.normals
|
|
780
|
+
tcoords = None
|
|
781
|
+
if self.tcoords.size:
|
|
782
|
+
tcoords = self.tcoords
|
|
783
|
+
if self.tcoords_elem or self.normals_elem:
|
|
784
|
+
verts_per_prim = 3
|
|
785
|
+
num_prims = int(conn.size / verts_per_prim)
|
|
786
|
+
# "flatten" the triangles to move values from elements to nodes
|
|
787
|
+
new_verts = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
|
|
788
|
+
new_conn = numpy.ndarray((num_prims * verts_per_prim,), dtype="int32")
|
|
789
|
+
new_tcoords = None
|
|
790
|
+
if tcoords is not None:
|
|
791
|
+
# remember that the input values are 1D at this point, we will expand to 2D later
|
|
792
|
+
new_tcoords = numpy.ndarray((num_prims * verts_per_prim,), dtype="float32")
|
|
793
|
+
new_normals = None
|
|
794
|
+
if normals is not None:
|
|
795
|
+
if normals.size == 0:
|
|
796
|
+
print("Warning: zero length normals!")
|
|
797
|
+
else:
|
|
798
|
+
new_normals = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
|
|
799
|
+
j = 0
|
|
800
|
+
for i0 in range(num_prims):
|
|
801
|
+
for i1 in range(verts_per_prim):
|
|
802
|
+
idx = conn[i0 * verts_per_prim + i1]
|
|
803
|
+
# new connectivity (identity)
|
|
804
|
+
new_conn[j] = j
|
|
805
|
+
# copy the vertex
|
|
806
|
+
new_verts[j * 3 + 0] = verts[idx * 3 + 0]
|
|
807
|
+
new_verts[j * 3 + 1] = verts[idx * 3 + 1]
|
|
808
|
+
new_verts[j * 3 + 2] = verts[idx * 3 + 2]
|
|
809
|
+
if new_normals is not None:
|
|
810
|
+
if self.normals_elem:
|
|
811
|
+
# copy the normal associated with the face
|
|
812
|
+
new_normals[j * 3 + 0] = normals[i0 * 3 + 0]
|
|
813
|
+
new_normals[j * 3 + 1] = normals[i0 * 3 + 1]
|
|
814
|
+
new_normals[j * 3 + 2] = normals[i0 * 3 + 2]
|
|
815
|
+
else:
|
|
816
|
+
# copy the same normal as the vertex
|
|
817
|
+
new_normals[j * 3 + 0] = normals[idx * 3 + 0]
|
|
818
|
+
new_normals[j * 3 + 1] = normals[idx * 3 + 1]
|
|
819
|
+
new_normals[j * 3 + 2] = normals[idx * 3 + 2]
|
|
820
|
+
if new_tcoords is not None:
|
|
821
|
+
# remember, 1D texture coords at this point
|
|
822
|
+
if self.tcoords_elem:
|
|
823
|
+
# copy the texture coord associated with the face
|
|
824
|
+
new_tcoords[j] = tcoords[i0]
|
|
825
|
+
else:
|
|
826
|
+
# copy the same texture coord as the vertex
|
|
827
|
+
new_tcoords[j] = tcoords[idx]
|
|
828
|
+
j += 1
|
|
829
|
+
# new arrays.
|
|
830
|
+
verts = new_verts
|
|
831
|
+
conn = new_conn
|
|
832
|
+
normals = new_normals
|
|
833
|
+
if tcoords is not None:
|
|
834
|
+
tcoords = new_tcoords
|
|
835
|
+
|
|
836
|
+
var = None
|
|
837
|
+
# texture coords need transformation from variable value to [ST]
|
|
838
|
+
if tcoords is not None:
|
|
839
|
+
var_id = self.cmd.color_variableid
|
|
840
|
+
var = self._link._variables[var_id]
|
|
841
|
+
v_min = None
|
|
842
|
+
v_max = None
|
|
843
|
+
for lvl in var.levels:
|
|
844
|
+
if (v_min is None) or (v_min > lvl.value):
|
|
845
|
+
v_min = lvl.value
|
|
846
|
+
if (v_max is None) or (v_max < lvl.value):
|
|
847
|
+
v_max = lvl.value
|
|
848
|
+
var_minmax = [v_min, v_max]
|
|
849
|
+
# build a power of two x 1 texture
|
|
850
|
+
num_texels = int(len(var.texture) / 4)
|
|
851
|
+
half_texel = 1 / (num_texels * 2.0)
|
|
852
|
+
num_verts = int(verts.size / 3)
|
|
853
|
+
tmp = numpy.ndarray((num_verts * 2,), dtype="float32")
|
|
854
|
+
tmp.fill(0.5) # fill in the T coordinate...
|
|
855
|
+
tex_width = half_texel * 2 * (num_texels - 1) # center to center of num_texels
|
|
856
|
+
# if the range is 0, adjust the min by -1. The result is that the texture
|
|
857
|
+
# coords will get mapped to S=1.0 which is what EnSight does in this situation
|
|
858
|
+
if (var_minmax[1] - var_minmax[0]) == 0.0:
|
|
859
|
+
var_minmax[0] = var_minmax[0] - 1.0
|
|
860
|
+
var_width = var_minmax[1] - var_minmax[0]
|
|
861
|
+
for idx in range(num_verts):
|
|
862
|
+
# normalized S coord value (clamp)
|
|
863
|
+
s = (tcoords[idx] - var_minmax[0]) / var_width
|
|
864
|
+
if s < 0.0:
|
|
865
|
+
s = 0.0
|
|
866
|
+
if s > 1.0:
|
|
867
|
+
s = 1.0
|
|
868
|
+
# map to the texture range and set the S value
|
|
869
|
+
tmp[idx * 2] = s * tex_width + half_texel
|
|
870
|
+
tcoords = tmp
|
|
871
|
+
|
|
872
|
+
parent = self._link._groups[self.cmd.parent_id]
|
|
873
|
+
color = [
|
|
874
|
+
self.cmd.fill_color[0] * self.cmd.diffuse,
|
|
875
|
+
self.cmd.fill_color[1] * self.cmd.diffuse,
|
|
876
|
+
self.cmd.fill_color[2] * self.cmd.diffuse,
|
|
877
|
+
self.cmd.fill_color[3],
|
|
878
|
+
]
|
|
879
|
+
obj_id = self._link._mesh_block_count
|
|
880
|
+
# prim =
|
|
881
|
+
_ = self._link._omni.create_dsg_mesh_block(
|
|
882
|
+
self.cmd.name,
|
|
883
|
+
obj_id,
|
|
884
|
+
parent[1],
|
|
885
|
+
verts,
|
|
886
|
+
conn,
|
|
887
|
+
normals,
|
|
888
|
+
tcoords,
|
|
889
|
+
matrix=self.cmd.matrix4x4,
|
|
890
|
+
diffuse=color,
|
|
891
|
+
variable=var,
|
|
892
|
+
)
|
|
893
|
+
self._link.log(
|
|
894
|
+
f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris, {self.conn_lines.size/2} lines."
|
|
895
|
+
)
|
|
896
|
+
self.cmd = None
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class DSGOmniverseLink(object):
|
|
900
|
+
def __init__(
|
|
901
|
+
self,
|
|
902
|
+
omni: OmniverseWrapper,
|
|
903
|
+
port: int = 12345,
|
|
904
|
+
host: str = "127.0.0.1",
|
|
905
|
+
security_code: str = "",
|
|
906
|
+
verbose: int = 0,
|
|
907
|
+
normalize_geometry: bool = False,
|
|
908
|
+
vrmode: bool = False,
|
|
909
|
+
):
|
|
910
|
+
super().__init__()
|
|
911
|
+
self._grpc = ensight_grpc.EnSightGRPC(port=port, host=host, secret_key=security_code)
|
|
912
|
+
self._verbose = verbose
|
|
913
|
+
self._thread: Optional[threading.Thread] = None
|
|
914
|
+
self._message_queue: queue.Queue = queue.Queue() # Messages coming from EnSight
|
|
915
|
+
self._dsg_queue: Optional[queue.SimpleQueue] = None # Outgoing messages to EnSight
|
|
916
|
+
self._shutdown = False
|
|
917
|
+
self._dsg = None
|
|
918
|
+
self._omni = omni
|
|
919
|
+
self._normalize_geometry = normalize_geometry
|
|
920
|
+
self._vrmode = vrmode
|
|
921
|
+
self._mesh_block_count = 0
|
|
922
|
+
self._variables: dict = {}
|
|
923
|
+
self._groups: dict = {}
|
|
924
|
+
self._part: Part = Part(self)
|
|
925
|
+
self._scene_bounds: Optional[List] = None
|
|
926
|
+
|
|
927
|
+
def log(self, s: str) -> None:
|
|
928
|
+
"""Log a string to the logging system
|
|
929
|
+
|
|
930
|
+
If verbosity is set, log the string.
|
|
931
|
+
"""
|
|
932
|
+
if self._verbose > 0:
|
|
933
|
+
logging.info(s)
|
|
934
|
+
|
|
935
|
+
def start(self) -> int:
|
|
936
|
+
"""Start a gRPC connection to an EnSight instance
|
|
937
|
+
|
|
938
|
+
Make a gRPC connection and start a DSG stream handler.
|
|
939
|
+
|
|
940
|
+
Returns
|
|
941
|
+
-------
|
|
942
|
+
0 on success, -1 on an error.
|
|
943
|
+
"""
|
|
944
|
+
# Start by setting up and verifying the connection
|
|
945
|
+
self._grpc.connect()
|
|
946
|
+
if not self._grpc.is_connected():
|
|
947
|
+
logging.info(
|
|
948
|
+
f"Unable to establish gRPC connection to: {self._grpc.host()}:{self._grpc.port()}"
|
|
949
|
+
)
|
|
950
|
+
return -1
|
|
951
|
+
# Streaming API requires an iterator, so we make one from a queue
|
|
952
|
+
# it also returns an iterator. self._dsg_queue is the input stream interface
|
|
953
|
+
# self._dsg is the returned stream iterator.
|
|
954
|
+
if self._dsg is not None:
|
|
955
|
+
return 0
|
|
956
|
+
self._dsg_queue = queue.SimpleQueue()
|
|
957
|
+
self._dsg = self._grpc.dynamic_scene_graph_stream(
|
|
958
|
+
iter(self._dsg_queue.get, None) # type:ignore
|
|
959
|
+
)
|
|
960
|
+
self._thread = threading.Thread(target=self.poll_messages)
|
|
961
|
+
if self._thread is not None:
|
|
962
|
+
self._thread.start()
|
|
963
|
+
return 0
|
|
964
|
+
|
|
965
|
+
def end(self):
|
|
966
|
+
"""Stop a gRPC connection to the EnSight instance"""
|
|
967
|
+
self._grpc.stop_server()
|
|
968
|
+
self._shutdown = True
|
|
969
|
+
self._thread.join()
|
|
970
|
+
self._grpc.shutdown()
|
|
971
|
+
self._dsg = None
|
|
972
|
+
self._thread = None
|
|
973
|
+
self._dsg_queue = None
|
|
974
|
+
|
|
975
|
+
def is_shutdown(self):
|
|
976
|
+
"""Check the service shutdown request status"""
|
|
977
|
+
return self._shutdown
|
|
978
|
+
|
|
979
|
+
def request_an_update(self, animation: bool = False) -> None:
|
|
980
|
+
"""Start a DSG update
|
|
981
|
+
Send a command to the DSG protocol to "init" an update.
|
|
982
|
+
|
|
983
|
+
Parameters
|
|
984
|
+
----------
|
|
985
|
+
animation:
|
|
986
|
+
if True, export all EnSight timesteps.
|
|
987
|
+
"""
|
|
988
|
+
# Send an INIT command to trigger a stream of update packets
|
|
989
|
+
cmd = dynamic_scene_graph_pb2.SceneClientCommand()
|
|
990
|
+
cmd.command_type = dynamic_scene_graph_pb2.SceneClientCommand.INIT
|
|
991
|
+
# Allow EnSight push commands, but full scene only for now...
|
|
992
|
+
cmd.init.allow_spontaneous = True
|
|
993
|
+
cmd.init.include_temporal_geometry = animation
|
|
994
|
+
cmd.init.allow_incremental_updates = False
|
|
995
|
+
cmd.init.maximum_chunk_size = 1024 * 1024
|
|
996
|
+
self._dsg_queue.put(cmd) # type:ignore
|
|
997
|
+
# Handle the update messages
|
|
998
|
+
self.handle_one_update()
|
|
999
|
+
|
|
1000
|
+
def poll_messages(self) -> None:
|
|
1001
|
+
"""Core interface to grab DSG events from gRPC and queue them for processing
|
|
1002
|
+
|
|
1003
|
+
This is run by a thread that is monitoring the dsg RPC call for update messages
|
|
1004
|
+
it places them in _message_queue as it finds them. They are picked up by the
|
|
1005
|
+
main thread via get_next_message()
|
|
1006
|
+
"""
|
|
1007
|
+
while not self._shutdown:
|
|
1008
|
+
try:
|
|
1009
|
+
self._message_queue.put(next(self._dsg)) # type:ignore
|
|
1010
|
+
except Exception:
|
|
1011
|
+
self._shutdown = True
|
|
1012
|
+
logging.info("DSG connection broken, calling exit")
|
|
1013
|
+
os._exit(0)
|
|
1014
|
+
|
|
1015
|
+
def get_next_message(self, wait: bool = True) -> Any:
|
|
1016
|
+
"""Get the next queued up protobuffer message
|
|
1017
|
+
|
|
1018
|
+
Called by the main thread to get any messages that were pulled in from the
|
|
1019
|
+
dsg stream and placed here by poll_messages()
|
|
1020
|
+
"""
|
|
1021
|
+
try:
|
|
1022
|
+
return self._message_queue.get(block=wait)
|
|
1023
|
+
except queue.Empty:
|
|
1024
|
+
return None
|
|
1025
|
+
|
|
1026
|
+
def handle_one_update(self) -> None:
|
|
1027
|
+
"""Monitor the DSG stream and handle a single update operation
|
|
1028
|
+
|
|
1029
|
+
Wait until we get the scene update begin message. From there, reset the current
|
|
1030
|
+
scene buckets and then parse all the incoming commands until we get the scene
|
|
1031
|
+
update end command. At which point, save the generated stage (started in the
|
|
1032
|
+
view command handler). Note: Parts are handled with an available bucket at all times.
|
|
1033
|
+
When a new part update comes in or the scene update end happens, the part is "finished".
|
|
1034
|
+
"""
|
|
1035
|
+
# An update starts with a UPDATE_SCENE_BEGIN command
|
|
1036
|
+
cmd = self.get_next_message()
|
|
1037
|
+
while (cmd is not None) and (
|
|
1038
|
+
cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_BEGIN
|
|
1039
|
+
):
|
|
1040
|
+
# Look for a begin command
|
|
1041
|
+
cmd = self.get_next_message()
|
|
1042
|
+
self.log("Begin update ------------------------")
|
|
1043
|
+
|
|
1044
|
+
# Start anew
|
|
1045
|
+
self._variables = {}
|
|
1046
|
+
self._groups = {}
|
|
1047
|
+
self._part = Part(self)
|
|
1048
|
+
self._scene_bounds = None
|
|
1049
|
+
self._mesh_block_count = 0 # reset when a new group shows up
|
|
1050
|
+
self._omni.clear_cleaned_names()
|
|
1051
|
+
|
|
1052
|
+
# handle the various commands until UPDATE_SCENE_END
|
|
1053
|
+
cmd = self.get_next_message()
|
|
1054
|
+
while (cmd is not None) and (
|
|
1055
|
+
cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_END
|
|
1056
|
+
):
|
|
1057
|
+
self.handle_update_command(cmd)
|
|
1058
|
+
cmd = self.get_next_message()
|
|
1059
|
+
|
|
1060
|
+
# Flush the last part
|
|
1061
|
+
self.finish_part()
|
|
1062
|
+
|
|
1063
|
+
# Stage update complete
|
|
1064
|
+
self._omni.save_stage()
|
|
1065
|
+
|
|
1066
|
+
self.log("End update --------------------------")
|
|
1067
|
+
|
|
1068
|
+
# handle an incoming gRPC update command
|
|
1069
|
+
def handle_update_command(self, cmd: dynamic_scene_graph_pb2.SceneUpdateCommand) -> None:
|
|
1070
|
+
"""Dispatch out a scene update command to the proper handler
|
|
1071
|
+
|
|
1072
|
+
Given a command object, pull out the correct portion of the protobuffer union and
|
|
1073
|
+
pass it to the appropriate handler.
|
|
1074
|
+
|
|
1075
|
+
Parameters
|
|
1076
|
+
----------
|
|
1077
|
+
cmd:
|
|
1078
|
+
The command to be dispatched.
|
|
1079
|
+
"""
|
|
1080
|
+
name = "Unknown"
|
|
1081
|
+
if cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.DELETE_ID:
|
|
1082
|
+
name = "Delete IDs"
|
|
1083
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_PART:
|
|
1084
|
+
name = "Part update"
|
|
1085
|
+
tmp = cmd.update_part
|
|
1086
|
+
self.handle_part(tmp)
|
|
1087
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GROUP:
|
|
1088
|
+
name = "Group update"
|
|
1089
|
+
tmp = cmd.update_group
|
|
1090
|
+
self.handle_group(tmp)
|
|
1091
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_GEOM:
|
|
1092
|
+
name = "Geom update"
|
|
1093
|
+
tmp = cmd.update_geom
|
|
1094
|
+
self._part.update_geom(tmp)
|
|
1095
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VARIABLE:
|
|
1096
|
+
name = "Variable update"
|
|
1097
|
+
tmp = cmd.update_variable
|
|
1098
|
+
self.handle_variable(tmp)
|
|
1099
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_VIEW:
|
|
1100
|
+
name = "View update"
|
|
1101
|
+
tmp = cmd.update_view
|
|
1102
|
+
self.handle_view(tmp)
|
|
1103
|
+
elif cmd.command_type == dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_TEXTURE:
|
|
1104
|
+
name = "Texture update"
|
|
1105
|
+
self.log(f"{name} --------------------------")
|
|
1106
|
+
|
|
1107
|
+
def finish_part(self) -> None:
|
|
1108
|
+
"""Complete the current part
|
|
1109
|
+
|
|
1110
|
+
There is always a part being modified. This method completes the current part, commits
|
|
1111
|
+
it to the Omniverse USD, and sets up the next part.
|
|
1112
|
+
"""
|
|
1113
|
+
self._part.build()
|
|
1114
|
+
self._mesh_block_count += 1
|
|
1115
|
+
|
|
1116
|
+
def handle_part(self, part: Any) -> None:
|
|
1117
|
+
"""Handle a DSG UPDATE_GROUP command
|
|
1118
|
+
Parameters
|
|
1119
|
+
----------
|
|
1120
|
+
part:
|
|
1121
|
+
The command coming from the EnSight stream.
|
|
1122
|
+
"""
|
|
1123
|
+
self.finish_part()
|
|
1124
|
+
self._part.reset(part)
|
|
1125
|
+
|
|
1126
|
+
def handle_group(self, group: Any) -> None:
|
|
1127
|
+
"""Handle a DSG UPDATE_GROUP command
|
|
1128
|
+
Parameters
|
|
1129
|
+
----------
|
|
1130
|
+
group:
|
|
1131
|
+
The command coming from the EnSight stream.
|
|
1132
|
+
"""
|
|
1133
|
+
# reset current mesh (part) count for unique "part" naming in USD
|
|
1134
|
+
self._mesh_block_count = 0
|
|
1135
|
+
# get the parent group or view
|
|
1136
|
+
parent = self._groups[group.parent_id]
|
|
1137
|
+
obj_type = group.attributes.get("ENS_OBJ_TYPE", None)
|
|
1138
|
+
matrix = group.matrix4x4
|
|
1139
|
+
# The Case matrix is basically the camera transform. In vrmode, we only want
|
|
1140
|
+
# the raw geometry, so use the identity matrix.
|
|
1141
|
+
if (obj_type == "ENS_CASE") and self._vrmode:
|
|
1142
|
+
matrix = [
|
|
1143
|
+
1.0,
|
|
1144
|
+
0.0,
|
|
1145
|
+
0.0,
|
|
1146
|
+
0.0,
|
|
1147
|
+
0.0,
|
|
1148
|
+
1.0,
|
|
1149
|
+
0.0,
|
|
1150
|
+
0.0,
|
|
1151
|
+
0.0,
|
|
1152
|
+
0.0,
|
|
1153
|
+
1.0,
|
|
1154
|
+
0.0,
|
|
1155
|
+
0.0,
|
|
1156
|
+
0.0,
|
|
1157
|
+
0.0,
|
|
1158
|
+
1.0,
|
|
1159
|
+
]
|
|
1160
|
+
prim = self._omni.create_dsg_group(group.name, parent[1], matrix=matrix, obj_type=obj_type)
|
|
1161
|
+
# record the scene bounds in case they are needed later
|
|
1162
|
+
self._groups[group.id] = [group, prim]
|
|
1163
|
+
bounds = group.attributes.get("ENS_SCENE_BOUNDS", None)
|
|
1164
|
+
if bounds:
|
|
1165
|
+
minmax = []
|
|
1166
|
+
for v in bounds.split(","):
|
|
1167
|
+
try:
|
|
1168
|
+
minmax.append(float(v))
|
|
1169
|
+
except Exception:
|
|
1170
|
+
pass
|
|
1171
|
+
if len(minmax) == 6:
|
|
1172
|
+
self._scene_bounds = minmax
|
|
1173
|
+
|
|
1174
|
+
def handle_variable(self, var: Any) -> None:
|
|
1175
|
+
"""Handle a DSG UPDATE_VARIABLE command
|
|
1176
|
+
|
|
1177
|
+
Save off the EnSight variable DSG command object.
|
|
1178
|
+
|
|
1179
|
+
Parameters
|
|
1180
|
+
----------
|
|
1181
|
+
var:
|
|
1182
|
+
The command coming from the EnSight stream.
|
|
1183
|
+
"""
|
|
1184
|
+
self._variables[var.id] = var
|
|
1185
|
+
|
|
1186
|
+
def handle_view(self, view: Any) -> None:
|
|
1187
|
+
"""Handle a DSG UPDATE_VIEW command
|
|
1188
|
+
|
|
1189
|
+
Map a view command into a new Omniverse stage and populate it with materials/lights.
|
|
1190
|
+
|
|
1191
|
+
Parameters
|
|
1192
|
+
----------
|
|
1193
|
+
view:
|
|
1194
|
+
The command coming from the EnSight stream.
|
|
1195
|
+
"""
|
|
1196
|
+
self._scene_bounds = None
|
|
1197
|
+
# Create a new root stage in Omniverse
|
|
1198
|
+
self._omni.create_new_stage()
|
|
1199
|
+
# Create the root group/camera
|
|
1200
|
+
camera_info = view
|
|
1201
|
+
if self._vrmode:
|
|
1202
|
+
camera_info = None
|
|
1203
|
+
root = self._omni.create_dsg_root(camera=camera_info)
|
|
1204
|
+
self._omni.checkpoint("Created base scene")
|
|
1205
|
+
# Create a distance and dome light in the scene
|
|
1206
|
+
# self._omni.createDistantLight()
|
|
1207
|
+
# self._omni.createDomeLight("./Materials/kloofendal_48d_partly_cloudy.hdr")
|
|
1208
|
+
self._omni.createDomeLight("./Materials/000_sky.exr")
|
|
1209
|
+
self._omni.checkpoint("Added lights to stage")
|
|
1210
|
+
# Upload a material and textures to the Omniverse server
|
|
1211
|
+
self._omni.uploadMaterial()
|
|
1212
|
+
self._omni.create_dsg_variable_textures(self._variables)
|
|
1213
|
+
# record
|
|
1214
|
+
self._groups[view.id] = [view, root]
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
if __name__ == "__main__":
|
|
1218
|
+
parser = argparse.ArgumentParser(
|
|
1219
|
+
description="Python Omniverse EnSight Dynamic Scene Graph Client",
|
|
1220
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
1221
|
+
)
|
|
1222
|
+
parser.add_argument(
|
|
1223
|
+
"--path",
|
|
1224
|
+
action="store",
|
|
1225
|
+
default="omniverse://localhost/Users/test",
|
|
1226
|
+
help="Omniverse pathname. Default=omniverse://localhost/Users/test",
|
|
1227
|
+
)
|
|
1228
|
+
parser.add_argument(
|
|
1229
|
+
"--port",
|
|
1230
|
+
metavar="ensight_grpc_port",
|
|
1231
|
+
nargs="?",
|
|
1232
|
+
default=12345,
|
|
1233
|
+
type=int,
|
|
1234
|
+
help="EnSight gRPC port number",
|
|
1235
|
+
)
|
|
1236
|
+
parser.add_argument(
|
|
1237
|
+
"--host",
|
|
1238
|
+
metavar="ensight_grpc_host",
|
|
1239
|
+
nargs="?",
|
|
1240
|
+
default="127.0.0.1",
|
|
1241
|
+
type=str,
|
|
1242
|
+
help="EnSight gRPC hostname",
|
|
1243
|
+
)
|
|
1244
|
+
parser.add_argument(
|
|
1245
|
+
"--security",
|
|
1246
|
+
metavar="ensight_grpc_security_code",
|
|
1247
|
+
nargs="?",
|
|
1248
|
+
default="",
|
|
1249
|
+
type=str,
|
|
1250
|
+
help="EnSight gRPC security code",
|
|
1251
|
+
)
|
|
1252
|
+
parser.add_argument(
|
|
1253
|
+
"--verbose",
|
|
1254
|
+
metavar="verbose_level",
|
|
1255
|
+
default=0,
|
|
1256
|
+
type=int,
|
|
1257
|
+
help="Enable debugging information",
|
|
1258
|
+
)
|
|
1259
|
+
parser.add_argument(
|
|
1260
|
+
"--animation", dest="animation", action="store_true", help="Save all timesteps (default)"
|
|
1261
|
+
)
|
|
1262
|
+
parser.add_argument(
|
|
1263
|
+
"--no-animation",
|
|
1264
|
+
dest="animation",
|
|
1265
|
+
action="store_false",
|
|
1266
|
+
help="Save only the current timestep",
|
|
1267
|
+
)
|
|
1268
|
+
parser.set_defaults(animation=False)
|
|
1269
|
+
parser.add_argument(
|
|
1270
|
+
"--log_file",
|
|
1271
|
+
metavar="log_filename",
|
|
1272
|
+
default="",
|
|
1273
|
+
type=str,
|
|
1274
|
+
help="Save program output to the named log file instead of stdout",
|
|
1275
|
+
)
|
|
1276
|
+
parser.add_argument(
|
|
1277
|
+
"--live",
|
|
1278
|
+
dest="live",
|
|
1279
|
+
action="store_true",
|
|
1280
|
+
default=False,
|
|
1281
|
+
help="Enable continuous operation",
|
|
1282
|
+
)
|
|
1283
|
+
parser.add_argument(
|
|
1284
|
+
"--normalize_geometry",
|
|
1285
|
+
dest="normalize",
|
|
1286
|
+
action="store_true",
|
|
1287
|
+
default=False,
|
|
1288
|
+
help="Spatially normalize incoming geometry",
|
|
1289
|
+
)
|
|
1290
|
+
parser.add_argument(
|
|
1291
|
+
"--vrmode",
|
|
1292
|
+
dest="vrmode",
|
|
1293
|
+
action="store_true",
|
|
1294
|
+
default=False,
|
|
1295
|
+
help="In this mode do not include a camera or the case level matrix. Geometry only.",
|
|
1296
|
+
)
|
|
1297
|
+
args = parser.parse_args()
|
|
1298
|
+
|
|
1299
|
+
log_args = dict(format="DSG/Omniverse: %(message)s", level=logging.INFO)
|
|
1300
|
+
if args.log_file:
|
|
1301
|
+
log_args["filename"] = args.log_file
|
|
1302
|
+
logging.basicConfig(**log_args) # type: ignore
|
|
1303
|
+
|
|
1304
|
+
destinationPath = args.path
|
|
1305
|
+
loggingEnabled = args.verbose
|
|
1306
|
+
|
|
1307
|
+
# Make the OmniVerse connection
|
|
1308
|
+
target = OmniverseWrapper(path=destinationPath, verbose=loggingEnabled)
|
|
1309
|
+
|
|
1310
|
+
# Print the username for the server
|
|
1311
|
+
target.username()
|
|
1312
|
+
|
|
1313
|
+
if loggingEnabled:
|
|
1314
|
+
logging.info("OmniVerse connection established.")
|
|
1315
|
+
|
|
1316
|
+
dsg_link = DSGOmniverseLink(
|
|
1317
|
+
omni=target,
|
|
1318
|
+
port=args.port,
|
|
1319
|
+
host=args.host,
|
|
1320
|
+
vrmode=args.vrmode,
|
|
1321
|
+
security_code=args.security,
|
|
1322
|
+
verbose=loggingEnabled,
|
|
1323
|
+
normalize_geometry=args.normalize,
|
|
1324
|
+
)
|
|
1325
|
+
if loggingEnabled:
|
|
1326
|
+
logging.info(f"Make DSG connection to: {args.host}:{args.port}")
|
|
1327
|
+
|
|
1328
|
+
# Start the DSG link
|
|
1329
|
+
err = dsg_link.start()
|
|
1330
|
+
if err < 0:
|
|
1331
|
+
sys.exit(err)
|
|
1332
|
+
|
|
1333
|
+
# Simple pull request
|
|
1334
|
+
dsg_link.request_an_update(animation=args.animation)
|
|
1335
|
+
|
|
1336
|
+
# Live operation
|
|
1337
|
+
if args.live:
|
|
1338
|
+
if loggingEnabled:
|
|
1339
|
+
logging.info("Waiting for remote push operations")
|
|
1340
|
+
while not dsg_link.is_shutdown():
|
|
1341
|
+
dsg_link.handle_one_update()
|
|
1342
|
+
|
|
1343
|
+
# Done...
|
|
1344
|
+
if loggingEnabled:
|
|
1345
|
+
logging.info("Shutting down DSG connection")
|
|
1346
|
+
dsg_link.end()
|
|
1347
|
+
|
|
1348
|
+
# Add a material to the box
|
|
1349
|
+
# target.createMaterial(boxMesh)
|
|
1350
|
+
|
|
1351
|
+
# Add a Nucleus Checkpoint to the stage
|
|
1352
|
+
# target.checkpoint("Add material to the box")
|
|
1353
|
+
|
|
1354
|
+
target.shutdown()
|