Skip to content

Commit 532ddf4

Browse files
committed
[FEATURE] Add USD Materials Baking.
1 parent 5e0985e commit 532ddf4

File tree

19 files changed

+519
-265
lines changed

19 files changed

+519
-265
lines changed

.github/workflows/generic.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
TI_ENABLE_METAL: "0"
3333
TI_ENABLE_OPENGL: "0"
3434
TI_ENABLE_VULKAN: "0"
35+
OMNI_KIT_ACCEPT_EULA: "yes"
36+
OMNI_KIT_ALLOW_ROOT: "1"
3537

3638
runs-on: ${{ matrix.OS }}
3739
if: github.event_name != 'release'
@@ -108,7 +110,13 @@ jobs:
108110
109111
- name: Install Genesis
110112
run: |
111-
pip install -e '.[dev]'
113+
# TODO: USD baking does not currently support Python 3.11 since
114+
# NVIDIA does not currently release `omniverse-kit==107.3` on PyPI.
115+
# See: https://github.com/Genesis-Embodied-AI/Genesis/pull/1300
116+
pip install -e '.[dev,usd]'
117+
if [[ "${{ matrix.PYTHON_VERSION }}" == '3.10' ]] ; then
118+
pip install --extra-index-url https://pypi.nvidia.com/ omniverse-kit
119+
fi
112120
113121
- name: Run unit tests
114122
run: |

.github/workflows/production.yml

Lines changed: 4 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
@@ -60,7 +62,8 @@ jobs:
6062
--partition=hpc-mid ${NODELIST} --nodes=1 --time="${TIMEOUT_MINUTES}" \
6163
--job-name=${SLURM_JOB_NAME} \
6264
bash -c "
63-
pip install -e '.[dev,render]' && \
65+
pip install --extra-index-url https://pypi.nvidia.com/ omniverse-kit && \
66+
pip install -e '.[dev,render,usd]' && \
6467
pytest -v --forked ./tests
6568
"
6669

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:

0 commit comments

Comments
 (0)