Skip to content

Commit 178a777

Browse files
Ilia Vitsnudelfacebook-github-bot
Ilia Vitsnudel
authored andcommitted
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
1 parent 823ab75 commit 178a777

File tree

2 files changed

+170
-22
lines changed

2 files changed

+170
-22
lines changed

pytorch3d/io/experimental_gltf_io.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ def get_texture_for_mesh(
393393
attributes = primitive["attributes"]
394394
vertex_colors = self._get_primitive_attribute(attributes, "COLOR_0", np.float32)
395395
if vertex_colors is not None:
396-
return TexturesVertex(torch.from_numpy(vertex_colors))
396+
return TexturesVertex([torch.from_numpy(vertex_colors)])
397397

398398
vertex_texcoords_0 = self._get_primitive_attribute(
399399
attributes, "TEXCOORD_0", np.float32
@@ -559,12 +559,26 @@ def __init__(self, data: Meshes, buffer_stream: BinaryIO) -> None:
559559
meshes = defaultdict(list)
560560
# pyre-fixme[6]: Incompatible parameter type
561561
meshes["name"] = "Node-Mesh"
562-
primitives = {
563-
"attributes": {"POSITION": 0, "TEXCOORD_0": 2},
564-
"indices": 1,
565-
"material": 0, # default material
566-
"mode": _PrimitiveMode.TRIANGLES,
567-
}
562+
if isinstance(self.mesh.textures, TexturesVertex):
563+
primitives = {
564+
"attributes": {"POSITION": 0, "COLOR_0": 2},
565+
"indices": 1,
566+
"mode": _PrimitiveMode.TRIANGLES,
567+
}
568+
elif isinstance(self.mesh.textures, TexturesUV):
569+
primitives = {
570+
"attributes": {"POSITION": 0, "TEXCOORD_0": 2},
571+
"indices": 1,
572+
"mode": _PrimitiveMode.TRIANGLES,
573+
"material": 0,
574+
}
575+
else:
576+
primitives = {
577+
"attributes": {"POSITION": 0},
578+
"indices": 1,
579+
"mode": _PrimitiveMode.TRIANGLES,
580+
}
581+
568582
meshes["primitives"].append(primitives)
569583
self._json_data["meshes"].append(meshes)
570584

@@ -610,6 +624,14 @@ def _write_accessor_json(self, key: str) -> Tuple[int, np.ndarray]:
610624
element_min = list(map(float, np.min(data, axis=0)))
611625
element_max = list(map(float, np.max(data, axis=0)))
612626
byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
627+
elif key == "texvertices":
628+
component_type = _ComponentType.FLOAT
629+
data = self.mesh.textures.verts_features_list()[0].cpu().numpy()
630+
element_type = "VEC3"
631+
buffer_view = 2
632+
element_min = list(map(float, np.min(data, axis=0)))
633+
element_max = list(map(float, np.max(data, axis=0)))
634+
byte_per_element = 3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
613635
elif key == "indices":
614636
component_type = _ComponentType.UNSIGNED_SHORT
615637
data = (
@@ -646,8 +668,10 @@ def _write_accessor_json(self, key: str) -> Tuple[int, np.ndarray]:
646668
return (byte_length, data)
647669

648670
def _write_bufferview(self, key: str, **kwargs):
649-
if key not in ["positions", "texcoords", "indices"]:
650-
raise ValueError("key must be one of positions, texcoords or indices")
671+
if key not in ["positions", "texcoords", "texvertices", "indices"]:
672+
raise ValueError(
673+
"key must be one of positions, texcoords, texvertices or indices"
674+
)
651675

652676
bufferview = {
653677
"name": "bufferView_%s" % key,
@@ -661,6 +685,10 @@ def _write_bufferview(self, key: str, **kwargs):
661685
byte_per_element = 2 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
662686
target = _TargetType.ARRAY_BUFFER
663687
bufferview["byteStride"] = int(byte_per_element)
688+
elif key == "texvertices":
689+
byte_per_element = 3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.FLOAT]]
690+
target = _TargetType.ELEMENT_ARRAY_BUFFER
691+
bufferview["byteStride"] = int(byte_per_element)
664692
elif key == "indices":
665693
byte_per_element = (
666694
3 * _DTYPE_BYTES[_ITEM_TYPES[_ComponentType.UNSIGNED_SHORT]]
@@ -701,12 +729,15 @@ def save(self):
701729
pos_byte, pos_data = self._write_accessor_json("positions")
702730
idx_byte, idx_data = self._write_accessor_json("indices")
703731
include_textures = False
704-
if (
705-
self.mesh.textures is not None
706-
and self.mesh.textures.verts_uvs_list()[0] is not None
707-
):
708-
tex_byte, tex_data = self._write_accessor_json("texcoords")
709-
include_textures = True
732+
if self.mesh.textures is not None:
733+
if hasattr(self.mesh.textures, "verts_features_list"):
734+
tex_byte, tex_data = self._write_accessor_json("texvertices")
735+
include_textures = True
736+
texcoords = False
737+
elif self.mesh.textures.verts_uvs_list()[0] is not None:
738+
tex_byte, tex_data = self._write_accessor_json("texcoords")
739+
include_textures = True
740+
texcoords = True
710741

711742
# bufferViews for positions, texture coords and indices
712743
byte_offset = 0
@@ -717,17 +748,19 @@ def save(self):
717748
byte_offset += idx_byte
718749

719750
if include_textures:
720-
self._write_bufferview(
721-
"texcoords", byte_length=tex_byte, offset=byte_offset
722-
)
751+
if texcoords:
752+
self._write_bufferview(
753+
"texcoords", byte_length=tex_byte, offset=byte_offset
754+
)
755+
else:
756+
self._write_bufferview(
757+
"texvertices", byte_length=tex_byte, offset=byte_offset
758+
)
723759
byte_offset += tex_byte
724760

725761
# image bufferView
726762
include_image = False
727-
if (
728-
self.mesh.textures is not None
729-
and self.mesh.textures.maps_list()[0] is not None
730-
):
763+
if self.mesh.textures is not None and hasattr(self.mesh.textures, "maps_list"):
731764
include_image = True
732765
image_byte, image_data = self._write_image_buffer(offset=byte_offset)
733766
byte_offset += image_byte

tests/test_io_gltf.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def test_load_apartment(self):
120120
The scene is "already lit", i.e. the textures reflect the lighting
121121
already, so we want to render them with full ambient light.
122122
"""
123+
123124
self.skipTest("Data not available")
124125

125126
glb = DATA_DIR / "apartment_1.glb"
@@ -266,3 +267,117 @@ def test_load_cow_no_texture(self):
266267
expected = np.array(f)
267268

268269
self.assertClose(image, expected)
270+
271+
def test_load_save_load_cow_texturesvertex(self):
272+
"""
273+
Load the cow as converted to a single mesh in a glb file and then save it to a glb file.
274+
"""
275+
276+
glb = DATA_DIR / "cow.glb"
277+
self.assertTrue(glb.is_file())
278+
device = torch.device("cuda:0")
279+
mesh = _load(glb, device=device, include_textures=False)
280+
self.assertEqual(len(mesh), 1)
281+
self.assertIsNone(mesh.textures)
282+
283+
self.assertEqual(mesh.faces_packed().shape, (5856, 3))
284+
self.assertEqual(mesh.verts_packed().shape, (3225, 3))
285+
mesh_obj = _load(TUTORIAL_DATA_DIR / "cow_mesh/cow.obj")
286+
self.assertClose(mesh.get_bounding_boxes().cpu(), mesh_obj.get_bounding_boxes())
287+
288+
mesh.textures = TexturesVertex(0.5 * torch.ones_like(mesh.verts_padded()))
289+
290+
image = _render(mesh, "cow_gray")
291+
292+
with Image.open(DATA_DIR / "glb_cow_gray.png") as f:
293+
expected = np.array(f)
294+
295+
self.assertClose(image, expected)
296+
297+
# save the mesh to a glb file
298+
glb = DATA_DIR / "cow_write_texturesvertex.glb"
299+
_write(mesh, glb)
300+
301+
# reload the mesh glb file saved in TexturesVertex format
302+
glb = DATA_DIR / "cow_write_texturesvertex.glb"
303+
self.assertTrue(glb.is_file())
304+
mesh_dash = _load(glb, device=device)
305+
self.assertEqual(len(mesh_dash), 1)
306+
307+
self.assertEqual(mesh_dash.faces_packed().shape, (5856, 3))
308+
self.assertEqual(mesh_dash.verts_packed().shape, (3225, 3))
309+
self.assertEqual(mesh_dash.textures.verts_features_list()[0].shape, (3225, 3))
310+
311+
# check the re-rendered image with expected
312+
image_dash = _render(mesh, "cow_gray_texturesvertex")
313+
self.assertClose(image_dash, expected)
314+
315+
def test_save_toy(self):
316+
"""
317+
Construct a simple mesh and save it to a glb file in TexturesVertex mode.
318+
"""
319+
320+
example = {}
321+
example["POSITION"] = torch.tensor(
322+
[
323+
[
324+
[0.0, 0.0, 0.0],
325+
[-1.0, 0.0, 0.0],
326+
[-1.0, 0.0, 1.0],
327+
[0.0, 0.0, 1.0],
328+
[0.0, 1.0, 0.0],
329+
[-1.0, 1.0, 0.0],
330+
[-1.0, 1.0, 1.0],
331+
[0.0, 1.0, 1.0],
332+
]
333+
]
334+
)
335+
example["indices"] = torch.tensor(
336+
[
337+
[
338+
[1, 4, 2],
339+
[4, 3, 2],
340+
[3, 7, 2],
341+
[7, 6, 2],
342+
[3, 4, 7],
343+
[4, 8, 7],
344+
[8, 5, 7],
345+
[5, 6, 7],
346+
[5, 2, 6],
347+
[5, 1, 2],
348+
[1, 5, 4],
349+
[5, 8, 4],
350+
]
351+
]
352+
)
353+
example["indices"] -= 1
354+
example["COLOR_0"] = torch.tensor(
355+
[
356+
[
357+
[1.0, 0.0, 0.0],
358+
[1.0, 0.0, 0.0],
359+
[1.0, 0.0, 0.0],
360+
[1.0, 0.0, 0.0],
361+
[1.0, 0.0, 0.0],
362+
[1.0, 0.0, 0.0],
363+
[1.0, 0.0, 0.0],
364+
[1.0, 0.0, 0.0],
365+
]
366+
]
367+
)
368+
# example['prop'] = {'material':
369+
# {'pbrMetallicRoughness':
370+
# {'baseColorFactor':
371+
# torch.tensor([[0.7, 0.7, 1, 0.5]]),
372+
# 'metallicFactor': torch.tensor([1]),
373+
# 'roughnessFactor': torch.tensor([0.1])},
374+
# 'alphaMode': 'BLEND',
375+
# 'doubleSided': True}}
376+
377+
texture = TexturesVertex(example["COLOR_0"])
378+
mesh = Meshes(
379+
verts=example["POSITION"], faces=example["indices"], textures=texture
380+
)
381+
382+
glb = DATA_DIR / "example_write_texturesvertex.glb"
383+
_write(mesh, glb)

0 commit comments

Comments
 (0)