Skip to content

Commit

Permalink
Work around issues with black material surfaces (#471)
Browse files Browse the repository at this point in the history
  • Loading branch information
randallfrank authored Oct 30, 2024
1 parent ecaac4e commit 3020856
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 12 deletions.
16 changes: 14 additions & 2 deletions doc/source/user_guide/omniverse_info.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -320,4 +332,4 @@ Miscellaneous features:
Material Conversions
^^^^^^^^^^^^^^^^^^^^

A mechanism for semi-automated mapping of materials is currently a work in progress.
A mechanism for semi-automated mapping of materials is currently a work in progress.
72 changes: 71 additions & 1 deletion src/ansys/pyensight/core/utils/dsg_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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
Expand Down
54 changes: 45 additions & 9 deletions src/ansys/pyensight/core/utils/omniverse_dsg_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
###############################################################################

import logging
import math
import os
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down

0 comments on commit 3020856

Please sign in to comment.