Adding save mesh into glb file in TexturesVertex format

Summary:
Added a suit of functions and code additions to experimental_gltf_io.py file to enable saving Meshes in TexturesVertex format into .glb file.
Also added a test to tets_io_gltf.py to check the functionality with the test described in Test Plane.

Reviewed By: bottler

Differential Revision: D44969144

fbshipit-source-id: 9ce815a1584b510442fa36cc4dbc8d41cc3786d5
This commit is contained in:
Ilia Vitsnudel 2023-05-01 00:41:47 -07:00 committed by Facebook GitHub Bot
parent 823ab75d27
commit 178a7774d4
2 changed files with 170 additions and 22 deletions

View File

@ -393,7 +393,7 @@ class _GLTFLoader:
attributes = primitive["attributes"] attributes = primitive["attributes"]
vertex_colors = self._get_primitive_attribute(attributes, "COLOR_0", np.float32) vertex_colors = self._get_primitive_attribute(attributes, "COLOR_0", np.float32)
if vertex_colors is not None: if vertex_colors is not None:
return TexturesVertex(torch.from_numpy(vertex_colors)) return TexturesVertex([torch.from_numpy(vertex_colors)])
vertex_texcoords_0 = self._get_primitive_attribute( vertex_texcoords_0 = self._get_primitive_attribute(
attributes, "TEXCOORD_0", np.float32 attributes, "TEXCOORD_0", np.float32
@ -559,12 +559,26 @@ class _GLTFWriter:
meshes = defaultdict(list) meshes = defaultdict(list)
# pyre-fixme[6]: Incompatible parameter type # pyre-fixme[6]: Incompatible parameter type
meshes["name"] = "Node-Mesh" meshes["name"] = "Node-Mesh"
primitives = { if isinstance(self.mesh.textures, TexturesVertex):
"attributes": {"POSITION": 0, "TEXCOORD_0": 2}, primitives = {
"indices": 1, "attributes": {"POSITION": 0, "COLOR_0": 2},
"material": 0, # default material "indices": 1,
"mode": _PrimitiveMode.TRIANGLES, "mode": _PrimitiveMode.TRIANGLES,
} }
elif isinstance(self.mesh.textures, TexturesUV):
primitives = {
"attributes": {"POSITION": 0, "TEXCOORD_0": 2},
"indices": 1,
"mode": _PrimitiveMode.TRIANGLES,
"material": 0,
}
else:
primitives = {
"attributes": {"POSITION": 0},
"indices": 1,
"mode": _PrimitiveMode.TRIANGLES,
}
meshes["primitives"].append(primitives) meshes["primitives"].append(primitives)
self._json_data["meshes"].append(meshes) self._json_data["meshes"].append(meshes)
@ -610,6 +624,14 @@ class _GLTFWriter:
element_min = list(map(float, np.min(data, axis=0))) element_min = list(map(float, np.min(data, axis=0)))
element_max = list(map(float, np.max(data, axis=0))) element_max = list(map(float, np.max(data, axis=0)))
byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]] byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
elif key == "texvertices":
component_type = _ComponentType.FLOAT
data = self.mesh.textures.verts_features_list()[0].cpu().numpy()
element_type = "VEC3"
buffer_view = 2
element_min = list(map(float, np.min(data, axis=0)))
element_max = list(map(float, np.max(data, axis=0)))
byte_per_element = 3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
elif key == "indices": elif key == "indices":
component_type = _ComponentType.UNSIGNED_SHORT component_type = _ComponentType.UNSIGNED_SHORT
data = ( data = (
@ -646,8 +668,10 @@ class _GLTFWriter:
return (byte_length, data) return (byte_length, data)
def _write_bufferview(self, key: str, **kwargs): def _write_bufferview(self, key: str, **kwargs):
if key not in ["positions", "texcoords", "indices"]: if key not in ["positions", "texcoords", "texvertices", "indices"]:
raise ValueError("key must be one of positions, texcoords or indices") raise ValueError(
"key must be one of positions, texcoords, texvertices or indices"
)
bufferview = { bufferview = {
"name": "bufferView_%s" % key, "name": "bufferView_%s" % key,
@ -661,6 +685,10 @@ class _GLTFWriter:
byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]] byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
target = _TargetType.ARRAY_BUFFER target = _TargetType.ARRAY_BUFFER
bufferview["byteStride"] = int(byte_per_element) bufferview["byteStride"] = int(byte_per_element)
elif key == "texvertices":
byte_per_element = 3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
target = _TargetType.ELEMENT_ARRAY_BUFFER
bufferview["byteStride"] = int(byte_per_element)
elif key == "indices": elif key == "indices":
byte_per_element = ( byte_per_element = (
3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.UNSIGNED_SHORT]] 3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.UNSIGNED_SHORT]]
@ -701,12 +729,15 @@ class _GLTFWriter:
pos_byte, pos_data = self._write_accessor_json("positions") pos_byte, pos_data = self._write_accessor_json("positions")
idx_byte, idx_data = self._write_accessor_json("indices") idx_byte, idx_data = self._write_accessor_json("indices")
include_textures = False include_textures = False
if ( if self.mesh.textures is not None:
self.mesh.textures is not None if hasattr(self.mesh.textures, "verts_features_list"):
and self.mesh.textures.verts_uvs_list()[0] is not None tex_byte, tex_data = self._write_accessor_json("texvertices")
): include_textures = True
tex_byte, tex_data = self._write_accessor_json("texcoords") texcoords = False
include_textures = True elif self.mesh.textures.verts_uvs_list()[0] is not None:
tex_byte, tex_data = self._write_accessor_json("texcoords")
include_textures = True
texcoords = True
# bufferViews for positions, texture coords and indices # bufferViews for positions, texture coords and indices
byte_offset = 0 byte_offset = 0
@ -717,17 +748,19 @@ class _GLTFWriter:
byte_offset += idx_byte byte_offset += idx_byte
if include_textures: if include_textures:
self._write_bufferview( if texcoords:
"texcoords", byte_length=tex_byte, offset=byte_offset self._write_bufferview(
) "texcoords", byte_length=tex_byte, offset=byte_offset
)
else:
self._write_bufferview(
"texvertices", byte_length=tex_byte, offset=byte_offset
)
byte_offset += tex_byte byte_offset += tex_byte
# image bufferView # image bufferView
include_image = False include_image = False
if ( if self.mesh.textures is not None and hasattr(self.mesh.textures, "maps_list"):
self.mesh.textures is not None
and self.mesh.textures.maps_list()[0] is not None
):
include_image = True include_image = True
image_byte, image_data = self._write_image_buffer(offset=byte_offset) image_byte, image_data = self._write_image_buffer(offset=byte_offset)
byte_offset += image_byte byte_offset += image_byte

View File

@ -120,6 +120,7 @@ class TestMeshGltfIO(TestCaseMixin, unittest.TestCase):
The scene is "already lit", i.e. the textures reflect the lighting The scene is "already lit", i.e. the textures reflect the lighting
already, so we want to render them with full ambient light. already, so we want to render them with full ambient light.
""" """
self.skipTest("Data not available") self.skipTest("Data not available")
glb = DATA_DIR / "apartment_1.glb" glb = DATA_DIR / "apartment_1.glb"
@ -266,3 +267,117 @@ class TestMeshGltfIO(TestCaseMixin, unittest.TestCase):
expected = np.array(f) expected = np.array(f)
self.assertClose(image, expected) self.assertClose(image, expected)
def test_load_save_load_cow_texturesvertex(self):
"""
Load the cow as converted to a single mesh in a glb file and then save it to a glb file.
"""
glb = DATA_DIR / "cow.glb"
self.assertTrue(glb.is_file())
device = torch.device("cuda:0")
mesh = _load(glb, device=device, include_textures=False)
self.assertEqual(len(mesh), 1)
self.assertIsNone(mesh.textures)
self.assertEqual(mesh.faces_packed().shape, (5856, 3))
self.assertEqual(mesh.verts_packed().shape, (3225, 3))
mesh_obj = _load(TUTORIAL_DATA_DIR / "cow_mesh/cow.obj")
self.assertClose(mesh.get_bounding_boxes().cpu(), mesh_obj.get_bounding_boxes())
mesh.textures = TexturesVertex(0.5 * torch.ones_like(mesh.verts_padded()))
image = _render(mesh, "cow_gray")
with Image.open(DATA_DIR / "glb_cow_gray.png") as f:
expected = np.array(f)
self.assertClose(image, expected)
# save the mesh to a glb file
glb = DATA_DIR / "cow_write_texturesvertex.glb"
_write(mesh, glb)
# reload the mesh glb file saved in TexturesVertex format
glb = DATA_DIR / "cow_write_texturesvertex.glb"
self.assertTrue(glb.is_file())
mesh_dash = _load(glb, device=device)
self.assertEqual(len(mesh_dash), 1)
self.assertEqual(mesh_dash.faces_packed().shape, (5856, 3))
self.assertEqual(mesh_dash.verts_packed().shape, (3225, 3))
self.assertEqual(mesh_dash.textures.verts_features_list()[0].shape, (3225, 3))
# check the re-rendered image with expected
image_dash = _render(mesh, "cow_gray_texturesvertex")
self.assertClose(image_dash, expected)
def test_save_toy(self):
"""
Construct a simple mesh and save it to a glb file in TexturesVertex mode.
"""
example = {}
example["POSITION"] = torch.tensor(
[
[
[0.0, 0.0, 0.0],
[-1.0, 0.0, 0.0],
[-1.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
]
]
)
example["indices"] = torch.tensor(
[
[
[1, 4, 2],
[4, 3, 2],
[3, 7, 2],
[7, 6, 2],
[3, 4, 7],
[4, 8, 7],
[8, 5, 7],
[5, 6, 7],
[5, 2, 6],
[5, 1, 2],
[1, 5, 4],
[5, 8, 4],
]
]
)
example["indices"] -= 1
example["COLOR_0"] = torch.tensor(
[
[
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
]
]
)
# example['prop'] = {'material':
# {'pbrMetallicRoughness':
# {'baseColorFactor':
# torch.tensor([[0.7, 0.7, 1, 0.5]]),
# 'metallicFactor': torch.tensor([1]),
# 'roughnessFactor': torch.tensor([0.1])},
# 'alphaMode': 'BLEND',
# 'doubleSided': True}}
texture = TexturesVertex(example["COLOR_0"])
mesh = Meshes(
verts=example["POSITION"], faces=example["indices"], textures=texture
)
glb = DATA_DIR / "example_write_texturesvertex.glb"
_write(mesh, glb)