Skip to content

Commit aeabb23

Browse files
ACMLCZHduburcqa
andauthored
[FEATURE] Add USD Materials Baking. (#1300)
Co-authored-by: Alexis Duburcq <alexis.duburcq@gmail.com>
1 parent 5e0985e commit aeabb23

File tree

18 files changed

+519
-264
lines changed

18 files changed

+519
-264
lines changed

.github/workflows/production.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ jobs:
2525
GENESIS_IMAGE_VER: "1_1"
2626
TIMEOUT_MINUTES: 180
2727
MADRONA_DISABLE_CUDA_HEAP_SIZE: "1"
28+
OMNI_KIT_ACCEPT_EULA: "yes"
29+
OMNI_KIT_ALLOW_ROOT: "1"
2830

2931
steps:
3032
- name: Checkout code
@@ -48,6 +50,9 @@ jobs:
4850
NODELIST="--nodelist=$IDLE_NODES"
4951
fi
5052
53+
# TODO: USD baking does not currently support Python 3.11 since
54+
# NVIDIA does not currently release `omniverse-kit==107.3` on PyPI.
55+
# See: https://github.com/Genesis-Embodied-AI/Genesis/pull/1300
5156
srun \
5257
--container-image="/mnt/data/images/genesis-v${GENESIS_IMAGE_VER}.sqsh" \
5358
--container-mounts=\
@@ -60,7 +65,8 @@ jobs:
6065
--partition=hpc-mid ${NODELIST} --nodes=1 --time="${TIMEOUT_MINUTES}" \
6166
--job-name=${SLURM_JOB_NAME} \
6267
bash -c "
63-
pip install -e '.[dev,render]' && \
68+
pip install --extra-index-url https://pypi.nvidia.com/ omniverse-kit && \
69+
pip install -e '.[dev,render,usd]' && \
6470
pytest -v --forked ./tests
6571
"
6672

genesis/assets/tests/chopper.glb

-2.3 MB
Binary file not shown.
-167 KB
Binary file not shown.
-63.8 KB
Binary file not shown.

genesis/engine/mesh.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from genesis.options.surfaces import Surface
1414
import genesis.utils.mesh as mu
1515
import genesis.utils.gltf as gltf_utils
16-
import genesis.utils.usda as usda_utils
1716
import genesis.utils.particle as pu
1817
from genesis.repr_base import RBC
1918

@@ -229,6 +228,7 @@ def from_trimesh(
229228
"""
230229
if surface is None:
231230
surface = gs.surfaces.Default()
231+
surface.update_texture()
232232
else:
233233
surface = surface.copy()
234234
mesh = mesh.copy(include_cache=True)
@@ -341,22 +341,20 @@ def from_morph_surface(cls, morph, surface=None):
341341
If the morph is a Mesh morph (morphs.Mesh), it could contain multiple submeshes, so we return a list.
342342
"""
343343
if isinstance(morph, gs.options.morphs.Mesh):
344-
if morph.file.endswith(("obj", "ply", "stl")):
344+
if morph.is_format(gs.options.morphs.MESH_FORMATS):
345345
meshes = mu.parse_mesh_trimesh(morph.file, morph.group_by_material, morph.scale, surface)
346-
347-
elif morph.file.endswith(("glb", "gltf")):
346+
elif morph.is_format(gs.options.morphs.GLTF_FORMATS):
348347
if morph.parse_glb_with_trimesh:
349348
meshes = mu.parse_mesh_trimesh(morph.file, morph.group_by_material, morph.scale, surface)
350349
else:
351350
meshes = gltf_utils.parse_mesh_glb(morph.file, morph.group_by_material, morph.scale, surface)
351+
elif morph.is_format(gs.options.morphs.USD_FORMATS):
352+
import genesis.utils.usda as usda_utils
352353

353-
elif morph.file.endswith(("usd", "usda", "usdc", "usdz")):
354354
meshes = usda_utils.parse_mesh_usd(morph.file, morph.group_by_material, morph.scale, surface)
355-
356355
elif isinstance(morph, gs.options.morphs.MeshSet):
357356
assert all(isinstance(mesh, trimesh.Trimesh) for mesh in morph.files)
358357
meshes = [mu.trimesh_to_mesh(mesh, morph.scale, surface) for mesh in morph.files]
359-
360358
else:
361359
gs.raise_exception(
362360
f"File type not supported (yet). Submit a feature request if you need this: {morph.file}."

genesis/options/morphs.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""
2+
We define all types of morphologies here: shape primitives, meshes, URDF, MJCF, and soft robot description files.
3+
4+
These are independent of backend solver type and are shared by different solvers, e.g. a mesh can be either loaded as a
5+
rigid object / MPM object / FEM object.
6+
"""
7+
18
import os
29
from typing import Any, List, Optional, Sequence, Tuple, Union
310

@@ -10,11 +17,12 @@
1017
from .misc import CoacdOptions
1118
from .options import Options
1219

13-
"""
14-
We define all types of morphologies here: shape primitives, meshes, URDF, MJCF, and soft robot description files.
15-
These are independent of backend solver type and are shared by different solvers.
16-
E.g. a mesh can be either loaded as a rigid object / MPM object / FEM object.
17-
"""
20+
21+
URDF_FORMAT = ".urdf"
22+
MJCF_FORMAT = ".xml"
23+
MESH_FORMATS = (".obj", ".ply", ".stl")
24+
GLTF_FORMATS = (".glb", ".gltf")
25+
USD_FORMATS = (".usd", ".usda", ".usdc", ".usdz")
1826

1927

2028
class TetGenMixin(Options):
@@ -556,6 +564,9 @@ def __init__(self, **data):
556564
def _repr_type(self):
557565
return f"<gs.morphs.{self.__class__.__name__}(file='{self.file}')>"
558566

567+
def is_format(self, format):
568+
return self.file.lower().endswith(format)
569+
559570

560571
class Mesh(FileMorph, TetGenMixin):
561572
"""
@@ -768,8 +779,8 @@ class MJCF(FileMorph):
768779

769780
def __init__(self, **data):
770781
super().__init__(**data)
771-
if not self.file.endswith(".xml"):
772-
gs.raise_exception(f"Expected `.xml` extension for MJCF file: {self.file}")
782+
if not self.is_format(MJCF_FORMAT):
783+
gs.raise_exception(f"Expected `{MJCF_FORMAT}` extension for MJCF file: {self.file}")
773784

774785
# What you want to do with scaling is kinda "zoom" the world from the perspective of the entity, i.e. scale the
775786
# geometric properties of an entity wrt its root pose. In the general case, ie for a 3D vector scale, (x, y, z)
@@ -878,8 +889,8 @@ class URDF(FileMorph):
878889

879890
def __init__(self, **data):
880891
super().__init__(**data)
881-
if isinstance(self.file, str) and not self.file.endswith(".urdf"):
882-
gs.raise_exception(f"Expected `.urdf` extension for URDF file: {self.file}")
892+
if isinstance(self.file, str) and not self.is_format(URDF_FORMAT):
893+
gs.raise_exception(f"Expected `{URDF_FORMAT}` extension for URDF file: {self.file}")
883894

884895
# Anisotropic scaling is ill-defined for poly-articulated robots. See related MJCF about this for details.
885896
if isinstance(self.scale, np.ndarray) and self.scale.std() > gs.EPS:
@@ -997,10 +1008,10 @@ def __init__(self, **data):
9971008
# Make sure that Propellers links are preserved
9981009
self.links_to_keep = tuple(set([*self.links_to_keep, *self.propellers_link_name]))
9991010

1000-
if isinstance(self.file, str) and not self.file.endswith(".urdf"):
1001-
gs.raise_exception(f"Drone only supports `.urdf` extension: {self.file}")
1011+
if isinstance(self.file, str) and not self.is_format(URDF_FORMAT):
1012+
gs.raise_exception(f"Drone only supports `{URDF_FORMAT}` extension: {self.file}")
10021013

1003-
if self.model not in ["CF2X", "CF2P", "RACE"]:
1014+
if self.model not in ("CF2X", "CF2P", "RACE"):
10041015
gs.raise_exception(f"Unsupported `model`: {self.model}.")
10051016

10061017

genesis/options/renderers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def __init__(self, **data):
9797
else:
9898
gs.logger.warning("`env_euler` is ignored when `env_quat` is specified.")
9999

100+
if self.env_surface is not None:
101+
self.env_surface.update_texture()
102+
100103

101104
class BatchRenderer(RendererOptions):
102105
"""

genesis/options/textures.py

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from PIL import Image
88

99
import genesis as gs
10+
import genesis.utils.mesh as mu
1011

1112
from .options import Options
1213

@@ -32,6 +33,9 @@ def check_simplify(self):
3233
def apply_cutoff(self, cutoff):
3334
raise NotImplementedError
3435

36+
def is_black(self):
37+
raise NotImplementedError
38+
3539

3640
class ColorTexture(Texture):
3741
"""
@@ -67,6 +71,9 @@ def apply_cutoff(self, cutoff):
6771
return
6872
self.color = tuple(1.0 if c >= cutoff else 0.0 for c in self.color)
6973

74+
def is_black(self):
75+
return all(c < gs.EPS for c in self.color)
76+
7077

7178
class ImageTexture(Texture):
7279
"""
@@ -115,25 +122,7 @@ def __init__(self, **data):
115122
if self.image_path.endswith((".hdr", ".exr")):
116123
self.encoding = "linear" # .exr or .hdr images should be encoded with 'linear'
117124
if self.image_path.endswith((".exr")):
118-
exr_file = OpenEXR.InputFile(self.image_path)
119-
exr_header = exr_file.header()
120-
121-
if exr_header["compression"].v > Imath.Compression.PIZ_COMPRESSION:
122-
new_image_path = f"{self.image_path[:-4]}_ZIP.exr"
123-
gs.logger.warning(
124-
f"EXR image {self.image_path}'s compression type {exr_header['compression']} is not supported. "
125-
f"Converting to compression type ZIP_COMPRESSION and saving to {new_image_path}."
126-
)
127-
self.image_path = new_image_path
128-
129-
if not os.path.exists(new_image_path):
130-
channel_data = {channel: exr_file.channel(channel) for channel in exr_header["channels"]}
131-
exr_header["compression"] = Imath.Compression(Imath.Compression.ZIP_COMPRESSION)
132-
new_exr_file = OpenEXR.OutputFile(new_image_path, exr_header)
133-
new_exr_file.writePixels(channel_data)
134-
new_exr_file.close()
135-
136-
exr_file.close()
125+
self.image_path = mu.check_exr_compression(self.image_path)
137126
else:
138127
self.image_array = np.array(Image.open(self.image_path))
139128
self.image_path = None
@@ -158,16 +147,8 @@ def __init__(self, **data):
158147
)
159148
self.image_array = arr
160149

161-
# calculate channel
162-
if self.image_array is None:
163-
if isinstance(self.resolution, (tuple, list)):
164-
H, W = self.resolution
165-
else:
166-
H = W = self.resolution
167-
168-
# Default to 3-channel RGB
169-
white = np.array([255, 255, 255], dtype=np.uint8)
170-
self.image_array = np.full((H, W, 3), white, dtype=np.uint8)
150+
# just calculate channel
151+
if self.image_array is None: # Using 'image_path'
171152
self._mean_color = np.array([1.0, 1.0, 1.0], dtype=np.float16)
172153
self._channel = 3
173154
else:
@@ -216,3 +197,6 @@ def apply_cutoff(self, cutoff):
216197
if cutoff is None or self.image_array is None: # Cutoff does not apply on image file.
217198
return
218199
self.image_array = np.where(self.image_array >= 255.0 * cutoff, 255, 0).astype(np.uint8)
200+
201+
def is_black(self):
202+
return all(c < gs.EPS for c in self.image_color) or np.max(self.image_array) == 0

genesis/utils/gltf.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,9 @@ def parse_glb_material(glb, material_index, surface):
219219
if material.emissiveFactor is not None:
220220
emissive_factor = np.array(material.emissiveFactor, dtype=np.float32)
221221

222-
if emissive_factor is not None and np.any(emissive_factor > 0.0): # Make sure to check emissive
223-
emissive_texture = mu.create_texture(emissive_image, emissive_factor, "srgb")
222+
emissive_texture = mu.create_texture(emissive_image, emissive_factor, "srgb")
223+
if emissive_texture.is_black(): # Make sure to check emissive
224+
emissive_texture = None
224225

225226
# TODO: Parse them!
226227
for extension_name, extension_material in material.extensions.items():
@@ -317,7 +318,7 @@ def parse_mesh_glb(path, group_by_material, scale, surface):
317318
primitive.material, parse_glb_material(glb, primitive.material, surface)
318319
)
319320
else:
320-
material, uv_used, material_name = None, 0, ""
321+
material, uv_used, material_name = surface.copy(), 0, ""
321322

322323
uvs = None
323324
if "KHR_draco_mesh_compression" in primitive.extensions:

genesis/utils/mesh.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import hashlib
2+
import json
23
import math
34
import os
45
import pickle as pkl
56
from functools import lru_cache
6-
7+
from pathlib import Path
78

89
import numpy as np
910
import trimesh
1011
from PIL import Image
12+
import OpenEXR
13+
import Imath
1114

1215
import coacd
1316
import igl
@@ -20,10 +23,12 @@
2023
from .misc import (
2124
get_assets_dir,
2225
get_cvx_cache_dir,
26+
get_exr_cache_dir,
2327
get_gsd_cache_dir,
2428
get_ptc_cache_dir,
2529
get_remesh_cache_dir,
2630
get_src_dir,
31+
get_usd_cache_dir,
2732
get_tet_cache_dir,
2833
)
2934

@@ -130,6 +135,26 @@ def get_remesh_path(verts, faces, edge_len_abs, edge_len_ratio, fix):
130135
return os.path.join(get_remesh_cache_dir(), f"{hashkey}.rm")
131136

132137

138+
def get_exr_path(file_path):
139+
hashkey = get_file_hashkey(file_path)
140+
return os.path.join(get_exr_cache_dir(), f"{hashkey}.exr")
141+
142+
143+
def get_usd_zip_path(file_path):
144+
hashkey = get_file_hashkey(file_path)
145+
return os.path.join(get_usd_cache_dir(), "zip", hashkey)
146+
147+
148+
def get_usd_bake_path(file_path):
149+
hashkey = get_file_hashkey(file_path)
150+
return os.path.join(get_usd_cache_dir(), "bake", hashkey)
151+
152+
153+
def get_file_hashkey(file):
154+
file_obj = Path(file)
155+
return get_hashkey(file_obj.resolve().as_posix().encode(), str(file_obj.stat().st_size).encode())
156+
157+
133158
def get_hashkey(*args):
134159
hasher = hashlib.sha256()
135160
for arg in args:
@@ -987,3 +1012,29 @@ def visualize_tet(tet, pv_data, show_surface=True, plot_cell_qual=False):
9871012
plotter.add_mesh(pv_data, "r", "wireframe")
9881013
plotter.add_legend([[" Input Mesh ", "r"], [" Tessellated Mesh ", "black"]])
9891014
plotter.show()
1015+
1016+
1017+
def check_exr_compression(exr_path):
1018+
exr_file = OpenEXR.InputFile(exr_path)
1019+
exr_header = exr_file.header()
1020+
if exr_header["compression"].v > Imath.Compression.PIZ_COMPRESSION:
1021+
new_exr_path = get_exr_path(exr_path)
1022+
if os.path.exists(new_exr_path):
1023+
gs.logger.info(f"Assets of fixed compression detected and used: {new_exr_path}.")
1024+
else:
1025+
gs.logger.warning(
1026+
f"EXR image {exr_path}'s compression type {exr_header['compression']} is not supported. "
1027+
f"Converting to compression type ZIP_COMPRESSION and saving to {new_exr_path}."
1028+
)
1029+
1030+
channel_data = {channel: exr_file.channel(channel) for channel in exr_header["channels"]}
1031+
exr_header["compression"] = Imath.Compression(Imath.Compression.ZIP_COMPRESSION)
1032+
1033+
os.makedirs(os.path.dirname(new_exr_path), exist_ok=True)
1034+
new_exr_file = OpenEXR.OutputFile(new_exr_path, exr_header)
1035+
new_exr_file.writePixels(channel_data)
1036+
new_exr_file.close()
1037+
1038+
exr_path = new_exr_path
1039+
1040+
exr_file.close()

0 commit comments

Comments
 (0)