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.

@@ -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()