diff --git a/doc/source/user_guide/omniverse_info.rst b/doc/source/user_guide/omniverse_info.rst index 603dd2c163a..ca1e2c8bc1e 100644 --- a/doc/source/user_guide/omniverse_info.rst +++ b/doc/source/user_guide/omniverse_info.rst @@ -270,7 +270,19 @@ If the ``--oneshot`` option is not specified, the tool will run in server mode. the DSG protocol or the directory specified by ``--monitor_directory`` option for geometry data. In this mode, the USD scene in the ``destination`` will be updated to reflect the last scene pushed. Unused files will be removed and items that do not change will not be updated. Thus, server -mode is best suited for dynamic, interactive applications. +mode is best suited for dynamic, interactive applications. If server mode is initiated via the command line, +a single scene push will automatically be performed. One can start subsequent push operations +from the EnSight python interpreter with the following commands. + + +.. code-block:: python + + import enspyqtgui_int + # Current timestep + enspyqtgui_int.dynamic_scene_graph_command("dynamicscenegraph://localhost/client/update") + # All timesteps + enspyqtgui_int.dynamic_scene_graph_command("dynamicscenegraph://localhost/client/update?timesteps=1") + If ``--oneshot`` is specified, only a single conversion is performed and the tool will not maintain a notion of the scene state. This makes the operation simpler and avoids the need for extra processes, @@ -320,4 +332,4 @@ Miscellaneous features: Material Conversions ^^^^^^^^^^^^^^^^^^^^ -A mechanism for semi-automated mapping of materials is currently a work in progress. \ No newline at end of file +A mechanism for semi-automated mapping of materials is currently a work in progress. diff --git a/src/ansys/pyensight/core/utils/dsg_server.py b/src/ansys/pyensight/core/utils/dsg_server.py index a181992a924..603912ee335 100644 --- a/src/ansys/pyensight/core/utils/dsg_server.py +++ b/src/ansys/pyensight/core/utils/dsg_server.py @@ -39,9 +39,22 @@ def __init__(self, session: "DSGSession"): self.node_sizes = numpy.array([], dtype="float32") self.cmd: Optional[Any] = None self.hash = hashlib.new("sha256") + self._material: Optional[Any] = None self.reset() def reset(self, cmd: Any = None) -> None: + """ + Reset the part object state to prepare the object + for a new part representation. Numpy arrays are cleared + and the state reset. + + Parameters + ---------- + cmd: Any + The DSG command that triggered this reset. Most likely + this is a UPDATE_PART command. + + """ self.conn_tris = numpy.array([], dtype="int32") self.conn_lines = numpy.array([], dtype="int32") self.coords = numpy.array([], dtype="float32") @@ -55,6 +68,63 @@ def reset(self, cmd: Any = None) -> None: if cmd is not None: self.hash.update(cmd.hash.encode("utf-8")) self.cmd = cmd + self._material = None + + def _parse_material(self) -> None: + """ + Parse the JSON string in the part command material string and + make the content accessible via material_names() and material(). + """ + if self._material is not None: + return + try: + if self.cmd.material_name: # type: ignore + self._material = json.loads(self.cmd.material_name) # type: ignore + for key, value in self._material.items(): + value["name"] = key + else: + self._material = {} + except Exception as e: + self.session.warn(f"Unable to parse JSON material: {str(e)}") + self._material = {} + + def material_names(self) -> List[str]: + """ + Return the list of material names included in the part material. + + Returns + ------- + List[str] + The list of defined material names. + """ + self._parse_material() + if self._material is None: + return [] + return list(self._material.keys()) + + def material(self, name: str = "") -> dict: + """ + Return the material dictionary for the specified material name. + + Parameters + ---------- + name: str + The material name to query. If no material name is given, the + first name in the material_names() list is used. + + Returns + ------- + dict + The material description dictionary or an empty dictionary. + """ + self._parse_material() + if not name: + names = self.material_names() + if len(names): + name = names[0] + if self._material is None: + return {} + return self._material.get(name, {}) def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None: """ @@ -214,7 +284,7 @@ def nodal_surface_rep(self): return command, verts, conn, normals, tcoords, var_cmd - def _normalize_verts(self, verts: numpy.ndarray): + def _normalize_verts(self, verts: numpy.ndarray) -> float: """ This function scales and translates vertices, so the longest axis in the scene is of length 1.0, and data is centered at the origin diff --git a/src/ansys/pyensight/core/utils/omniverse_dsg_server.py b/src/ansys/pyensight/core/utils/omniverse_dsg_server.py index 044de9509d9..b93e392fa06 100644 --- a/src/ansys/pyensight/core/utils/omniverse_dsg_server.py +++ b/src/ansys/pyensight/core/utils/omniverse_dsg_server.py @@ -23,7 +23,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # ############################################################################### - import logging import math import os @@ -279,6 +278,7 @@ def create_dsg_mesh_block( variable=None, timeline=[0.0, 0.0], first_timestep=False, + mat_info={}, ): # 1D texture map for variables https://graphics.pixar.com/usd/release/tut_simple_shading.html # create the part usd object @@ -316,7 +316,12 @@ def create_dsg_mesh_block( matrixOp.Set(Gf.Matrix4d(*matrix).GetTranspose()) self.create_dsg_material( - part_stage, mesh, "/" + partname, diffuse=diffuse, variable=variable + part_stage, + mesh, + "/" + partname, + diffuse=diffuse, + variable=variable, + mat_info=mat_info, ) timestep_prim = self.add_timestep_group(parent_prim, timeline, first_timestep) @@ -363,6 +368,7 @@ def create_dsg_lines( variable=None, timeline=[0.0, 0.0], first_timestep=False, + mat_info={}, ): # TODO: GLB extension maps to DSG PART attribute map width = self.line_width @@ -431,7 +437,12 @@ def create_dsg_lines( matrixOp.Set(Gf.Matrix4d(*matrix).GetTranspose()) self.create_dsg_material( - part_stage, lines, "/" + partname, diffuse=diffuse, variable=var_cmd + part_stage, + lines, + "/" + partname, + diffuse=diffuse, + variable=var_cmd, + mat_info=mat_info, ) timestep_prim = self.add_timestep_group(parent_prim, timeline, first_timestep) @@ -510,16 +521,19 @@ def create_dsg_points( return part_stage_url def create_dsg_material( - self, stage, mesh, root_name, diffuse=[1.0, 1.0, 1.0, 1.0], variable=None + self, stage, mesh, root_name, diffuse=[1.0, 1.0, 1.0, 1.0], variable=None, mat_info={} ): # https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html # Use ior==1.0 to be more like EnSight - rays of light do not bend when passing through transparent objs material = UsdShade.Material.Define(stage, root_name + "/Material") pbrShader = UsdShade.Shader.Define(stage, root_name + "/Material/PBRShader") pbrShader.CreateIdAttr("UsdPreviewSurface") - pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0) - pbrShader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(0.0) - pbrShader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(diffuse[3]) + smoothness = mat_info.get("smoothness", 0.0) + pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0 - smoothness) + metallic = mat_info.get("metallic", 0.0) + pbrShader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(metallic) + opacity = mat_info.get("opacity", diffuse[3]) + pbrShader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(opacity) pbrShader.CreateInput("ior", Sdf.ValueTypeNames.Float).Set(1.0) pbrShader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1) if variable: @@ -543,9 +557,29 @@ def create_dsg_material( stInput.Set("st") stReader.CreateInput("varname", Sdf.ValueTypeNames.Token).ConnectToSource(stInput) else: - scale = 1.0 - color = Gf.Vec3f(diffuse[0] * scale, diffuse[1] * scale, diffuse[2] * scale) + # The colors are a mixture of content from the DSG PART protocol buffer + # and the JSON material block from the material_name field. + kd = 1.0 + diffuse_color = [diffuse[0], diffuse[1], diffuse[2]] + ke = 1.0 + emissive_color = [0.0, 0.0, 0.0] + ks = 1.0 + specular_color = [0.0, 0.0, 0.0] + mat_name = mat_info.get("name", "") + if mat_name.startswith("ensight"): + diffuse_color = mat_info.get("diffuse", diffuse_color) + if mat_name != "ensight/Default": + ke = mat_info.get("ke", ke) + emissive_color = mat_info.get("emissive", emissive_color) + ks = mat_info.get("ks", ks) + specular_color = mat_info.get("specular", specular_color) + # Set the colors + color = Gf.Vec3f(diffuse_color[0] * kd, diffuse_color[1] * kd, diffuse_color[2] * kd) pbrShader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color) + color = Gf.Vec3f(emissive_color[0] * ke, emissive_color[1] * ke, emissive_color[2] * ke) + pbrShader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(color) + color = Gf.Vec3f(specular_color[0] * ks, specular_color[1] * ks, specular_color[2] * ks) + pbrShader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(color) material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface") UsdShade.MaterialBindingAPI(mesh).Bind(material) @@ -744,6 +778,7 @@ def finalize_part(self, part: Part) -> None: part.cmd.fill_color[3], ] + mat_info = part.material() if part.cmd.render == part.cmd.CONNECTIVITY: has_triangles = False command, verts, conn, normals, tcoords, var_cmd = part.nodal_surface_rep() @@ -764,6 +799,7 @@ def finalize_part(self, part: Part) -> None: variable=var_cmd, timeline=self.session.cur_timeline, first_timestep=(self.session.cur_timeline[0] == self.session.time_limits[0]), + mat_info=mat_info, ) if self._omni.use_lines: command, verts, tcoords, var_cmd = part.line_rep()