mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2025-08-02 03:42:50 +08:00
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:
parent
823ab75d27
commit
178a7774d4
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user