Skip to content

Commit

Permalink
Add support for DSG lines in OV export (#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
randallfrank authored Oct 22, 2024
1 parent 927a16d commit e26d059
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 45 deletions.
163 changes: 122 additions & 41 deletions src/ansys/pyensight/core/utils/dsg_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def nodal_surface_rep(self):
self.session.log(f"Note: part '{self.cmd.name}' contains no triangles.")
return None, None, None, None, None, None
verts = self.coords
self.normalize_verts(verts)
_ = self._normalize_verts(verts)

conn = self.conn_tris
normals = self.normals
Expand All @@ -151,7 +151,7 @@ def nodal_surface_rep(self):
tcoords = self.tcoords
if self.tcoords_elem or self.normals_elem:
verts_per_prim = 3
num_prims = int(conn.size / verts_per_prim)
num_prims = conn.size // verts_per_prim
# "flatten" the triangles to move values from elements to nodes
new_verts = numpy.ndarray((num_prims * verts_per_prim * 3,), dtype="float32")
new_conn = numpy.ndarray((num_prims * verts_per_prim,), dtype="int32")
Expand Down Expand Up @@ -205,47 +205,16 @@ def nodal_surface_rep(self):
var_cmd = None
# texture coords need transformation from variable value to [ST]
if tcoords is not None:
var_dsg_id = self.cmd.color_variableid
var_cmd = self.session.variables[var_dsg_id]
v_min = None
v_max = None
for lvl in var_cmd.levels:
if (v_min is None) or (v_min > lvl.value):
v_min = lvl.value
if (v_max is None) or (v_max < lvl.value):
v_max = lvl.value
var_minmax = [v_min, v_max]
# build a power of two x 1 texture
num_texels = int(len(var_cmd.texture) / 4)
half_texel = 1 / (num_texels * 2.0)
num_verts = int(verts.size / 3)
tmp = numpy.ndarray((num_verts * 2,), dtype="float32")
tmp.fill(0.5) # fill in the T coordinate...
tex_width = half_texel * 2 * (num_texels - 1) # center to center of num_texels
# if the range is 0, adjust the min by -1. The result is that the texture
# coords will get mapped to S=1.0 which is what EnSight does in this situation
if (var_minmax[1] - var_minmax[0]) == 0.0:
var_minmax[0] = var_minmax[0] - 1.0
var_width = var_minmax[1] - var_minmax[0]
for idx in range(num_verts):
# normalized S coord value (clamp)
s = (tcoords[idx] - var_minmax[0]) / var_width
if s < 0.0:
s = 0.0
if s > 1.0:
s = 1.0
# map to the texture range and set the S value
tmp[idx * 2] = s * tex_width + half_texel
tcoords = tmp
tcoords, var_cmd = self._build_st_coords(tcoords, verts.size // 3)

self.session.log(
f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris."
f"Part '{self.cmd.name}' defined: {self.coords.size // 3} verts, {self.conn_tris.size // 3} tris."
)
command = self.cmd

return command, verts, conn, normals, tcoords, var_cmd

def normalize_verts(self, verts: numpy.ndarray):
def _normalize_verts(self, verts: numpy.ndarray):
"""
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 All @@ -254,7 +223,7 @@ def normalize_verts(self, verts: numpy.ndarray):
"""
s = 1.0
if self.session.normalize_geometry and self.session.scene_bounds is not None:
num_verts = int(verts.size / 3)
num_verts = verts.size // 3
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
Expand All @@ -275,6 +244,118 @@ def normalize_verts(self, verts: numpy.ndarray):
verts[j + 2] = (verts[j + 2] - midz) / s
return 1.0 / s

def _build_st_coords(self, tcoords: numpy.ndarray, num_verts: int):
"""
The Omniverse interface uses 2D texturing (s,t) to reference the texture map.
This method converts DSG texture coordinates (1D and in "variable" units) into
2D OpenGL style [0.,1.] normalized coordinate space. the "t" coordinate will
always be 0.5.
Parameters
----------
tcoords: numpy.ndarray
The DSG 1D texture coordinates, which are actually variable values.
num_verts: int
The number of vertices in the mesh.
Returns
-------
numpy.ndarray, Any
The ST OpenGL GL texture coordinate array and the variable definition DSG command.
"""
var_dsg_id = self.cmd.color_variableid # type: ignore
var_cmd = self.session.variables[var_dsg_id]
v_min = None
v_max = None
for lvl in var_cmd.levels:
if (v_min is None) or (v_min > lvl.value):
v_min = lvl.value
if (v_max is None) or (v_max < lvl.value):
v_max = lvl.value
var_minmax: List[float] = [v_min, v_max] # type: ignore
# build a power of two x 1 texture
num_texels = len(var_cmd.texture) // 4
half_texel = 1 / (num_texels * 2.0)
tmp = numpy.ndarray((num_verts * 2,), dtype="float32")
tmp.fill(0.5) # fill in the T coordinate...
tex_width = half_texel * 2 * (num_texels - 1) # center to center of num_texels
# if the range is 0, adjust the min by -1. The result is that the texture
# coords will get mapped to S=1.0 which is what EnSight does in this situation
if (var_minmax[1] - var_minmax[0]) == 0.0:
var_minmax[0] = var_minmax[0] - 1.0
var_width = var_minmax[1] - var_minmax[0]
for idx in range(num_verts):
# normalized S coord value (clamp)
s = (tcoords[idx] - var_minmax[0]) / var_width
if s < 0.0:
s = 0.0
if s > 1.0:
s = 1.0
# map to the texture range and set the S value
tmp[idx * 2] = s * tex_width + half_texel
return tmp, var_cmd

def line_rep(self):
"""
This function processes the geometry arrays and returns values to represent line data.
The vertex array embeds the connectivity, so every two points represent a line segment.
The tcoords similarly follow the vertex array notion.
Returns
-------
On failure, the method returns None for the first return value. The returned tuple is:
(part_command, vertices, connectivity, tex_coords, var_command)
part_command: UPDATE_PART command object
vertices: numpy array of per-node coordinates (two per line segment)
tcoords: numpy array of per vertex texture coordinates (optional)
var_command: UPDATE_VARIABLE command object for the variable the colors correspond to, if any
"""
if self.cmd is None:
return None, None, None, None
if self.cmd.render != self.cmd.CONNECTIVITY:
# Early out. Rendering type for this object is a surface rep, not a point rep
return None, None, None, None

num_lines = self.conn_lines.size // 2
if num_lines == 0:
return None, None, None, None
verts = numpy.ndarray((num_lines * 2 * 3,), dtype="float32")
tcoords = None
if self.tcoords.size:
tcoords = numpy.ndarray((num_lines * 2,), dtype="float32")
# TODO: handle elemental line values (self.tcoords_elem) by converting to nodal...
# if self.tcoords_elem:
for i in range(num_lines):
i0 = self.conn_lines[i * 2]
i1 = self.conn_lines[i * 2 + 1]
offset = i * 6
verts[offset + 0] = self.coords[i0 * 3 + 0]
verts[offset + 1] = self.coords[i0 * 3 + 1]
verts[offset + 2] = self.coords[i0 * 3 + 2]
verts[offset + 3] = self.coords[i1 * 3 + 0]
verts[offset + 4] = self.coords[i1 * 3 + 1]
verts[offset + 5] = self.coords[i1 * 3 + 2]
if tcoords is not None:
# tcoords are 1D at this point
offset = i * 2
tcoords[offset + 0] = self.tcoords[i0]
tcoords[offset + 1] = self.tcoords[i1]

_ = self._normalize_verts(verts)

var_cmd = None
# texture coords need transformation from variable value to [ST]
if tcoords is not None:
tcoords, var_cmd = self._build_st_coords(tcoords, verts.size // 3)

self.session.log(f"Part '{self.cmd.name}' defined: {num_lines} lines.")
command = self.cmd

return command, verts, tcoords, var_cmd

def point_rep(self):
"""
This function processes the geometry arrays and returns values to represent point data
Expand All @@ -297,8 +378,8 @@ def point_rep(self):
# Early out. Rendering type for this object is a surface rep, not a point rep
return None, None, None, None, None
verts = self.coords
num_verts = int(verts.size / 3)
norm_scale = self.normalize_verts(verts)
num_verts = verts.size // 3
norm_scale = self._normalize_verts(verts)

# Convert var values in self.tcoords to RGB colors
# For now, look up RGB colors. Planned USD enhancements should allow tex coords instead.
Expand Down Expand Up @@ -359,7 +440,7 @@ def point_rep(self):
colors[idx * 3 + ii] = (
col0[ii] * pal_sub + col1[ii] * (1.0 - pal_sub)
) / 255.0
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size // 3} points.")

node_sizes = None
if self.node_sizes.size and self.node_sizes.size == num_verts:
Expand All @@ -375,7 +456,7 @@ def point_rep(self):
for ii in range(0, num_verts):
node_sizes[ii] = node_size_default

self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size // 3} points.")
command = self.cmd

return command, verts, node_sizes, colors, var_cmd
Expand Down
34 changes: 32 additions & 2 deletions src/ansys/pyensight/core/utils/omniverse_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def __init__(
normalize_geometry: bool = False,
dsg_uri: str = "",
monitor_directory: str = "",
line_width: float = -0.0001,
use_lines: bool = False,
) -> None:
self._dsg_uri = dsg_uri
self._destination = destination
Expand All @@ -93,6 +95,8 @@ def __init__(
self._server_process = None
self._status_filename: str = ""
self._monitor_directory: str = monitor_directory
self._line_width = line_width
self._use_lines = use_lines

@property
def monitor_directory(self) -> Optional[str]:
Expand Down Expand Up @@ -184,7 +188,9 @@ def run_server(self, one_shot: bool = False) -> None:
"""

# Build the Omniverse connection
omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
omni_link = ov_dsg_server.OmniverseWrapper(
destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
)
logging.info("Omniverse connection established.")

# parse the DSG USI
Expand Down Expand Up @@ -269,7 +275,9 @@ def run_monitor(self):
return

# Build the Omniverse connection
omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
omni_link = ov_dsg_server.OmniverseWrapper(
destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
)
logging.info("Omniverse connection established.")

# use an OmniverseUpdateHandler
Expand Down Expand Up @@ -449,6 +457,20 @@ def run_monitor(self):
type=str2bool_type,
help="Convert a single geometry into USD and exit. Default: false",
)
line_default: Any = os.environ.get("ANSYS_OV_LINE_WIDTH", None)
if line_default is not None:
try:
line_default = float(line_default)
except ValueError:
line_default = None
# Potential future default: -0.0001
parser.add_argument(
"--line_width",
metavar="line_width",
default=line_default,
type=float,
help=f"Width of lines: >0=absolute size. <0=fraction of diagonal. 0=wireframe. Default: {line_default}",
)

# parse the command line
args = parser.parse_args()
Expand All @@ -469,6 +491,12 @@ def run_monitor(self):
logging.root.removeHandler(logging.root.handlers[0])
logging.basicConfig(**log_args) # type: ignore

# size of lines in data units or fraction of bounding box diagonal
use_lines = args.line_width is not None
line_width = -0.0001
if args.line_width is not None:
line_width = args.line_width

# Build the server object
server = OmniverseGeometryServer(
destination=args.destination,
Expand All @@ -479,6 +507,8 @@ def run_monitor(self):
normalize_geometry=args.normalize_geometry,
vrmode=not args.include_camera,
temporal=args.temporal,
line_width=line_width,
use_lines=use_lines,
)

# run the server
Expand Down
Loading

0 comments on commit e26d059

Please sign in to comment.