bluestyle97's picture
Upload 147 files
184193d verified
raw
history blame
27.1 kB
import os
import math
import numpy as np
import cv2
import torch
import trimesh
import torch.nn.functional as F
import pygltflib
import xatlas
import miniball
from trimesh.visual import TextureVisuals
from PIL import Image
def dot(x, y):
return torch.sum(x * y, -1, keepdim=True)
def length(x, eps=1e-20):
return torch.sqrt(torch.clamp(dot(x, x), min=eps))
def safe_normalize(x, eps=1e-20):
return x / length(x, eps)
class Mesh:
def __init__(
self,
v=None,
f=None,
vn=None,
fn=None,
vt=None,
ft=None,
vc=None,
albedo=None,
device=None,
textureless=False):
self.device = device
self.v = v
self.vn = vn
self.vt = vt
self.vc = vc
self.f = f
self.fn = fn
self.ft = ft
self.face_normals = None
# only support a single albedo
self.albedo = albedo
self.textureless = textureless
self.ori_center = 0
self.ori_scale = 1
def detach(self):
self.v = self.v.detach() if self.v is not None else None
self.vn = self.vn.detach() if self.vn is not None else None
self.vt = self.vt.detach() if self.vt is not None else None
self.vc = self.vc.detach() if self.vc is not None else None
self.f = self.f.detach() if self.f is not None else None
self.fn = self.fn.detach() if self.fn is not None else None
self.ft = self.ft.detach() if self.ft is not None else None
self.face_normals = self.face_normals.detach() if self.face_normals is not None else None
self.albedo = self.albedo.detach() if self.albedo is not None else None
return self
@classmethod
def load(cls, path=None, resize=False, auto_uv=True, flip_yz=False, **kwargs):
# assume init with kwargs
if path is None:
mesh = cls(**kwargs)
# obj supports face uv
elif path.endswith(".obj"):
mesh = cls.load_obj(path, **kwargs)
# trimesh only supports vertex uv, but can load more formats
else:
mesh = cls.load_trimesh(path, **kwargs)
print(f"[Mesh loading] v: {mesh.v.shape}, f: {mesh.f.shape}")
# auto-normalize
if resize:
mesh.auto_size()
# auto-fix normal
if mesh.vn is None:
mesh.auto_normal()
print(f"[Mesh loading] vn: {mesh.vn.shape}, fn: {mesh.fn.shape}")
# auto-fix texture
if mesh.vt is None and auto_uv:
mesh.auto_uv(cache_path=path)
if mesh.vt is not None and mesh.ft is not None:
print(f"[Mesh loading] vt: {mesh.vt.shape}, ft: {mesh.ft.shape}")
if flip_yz:
mesh.v[..., [1, 2]] = mesh.v[..., [2, 1]]
mesh.vn[..., [1, 2]] = mesh.vn[..., [2, 1]]
mesh.v[..., 1] = -mesh.v[..., 1]
mesh.vn[..., 1] = -mesh.vn[..., 1]
return mesh
# load from obj file
@classmethod
def load_obj(cls, path, albedo_path=None, device=None):
assert os.path.splitext(path)[-1] == ".obj"
mesh = cls()
# device
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
mesh.device = device
# load obj
with open(path, "r") as f:
lines = f.readlines()
def parse_f_v(fv):
# pass in a vertex term of a face, return {v, vt, vn} (-1 if not provided)
# supported forms:
# f v1 v2 v3
# f v1/vt1 v2/vt2 v3/vt3
# f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
# f v1//vn1 v2//vn2 v3//vn3
xs = [int(x) - 1 if x != "" else -1 for x in fv.split("/")]
xs.extend([-1] * (3 - len(xs)))
return xs[0], xs[1], xs[2]
# NOTE: we ignore usemtl, and assume the mesh ONLY uses one material (first in mtl)
vertices, texcoords, normals = [], [], []
faces, tfaces, nfaces = [], [], []
mtl_path = None
for line in lines:
split_line = line.split()
# empty line
if len(split_line) == 0:
continue
prefix = split_line[0].lower()
# mtllib
if prefix == "mtllib":
mtl_path = split_line[1]
# usemtl
elif prefix == "usemtl":
pass # ignored
# v/vn/vt
elif prefix == "v":
vertices.append([float(v) for v in split_line[1:]])
elif prefix == "vn":
normals.append([float(v) for v in split_line[1:]])
elif prefix == "vt":
val = [float(v) for v in split_line[1:]]
texcoords.append([val[0], 1.0 - val[1]])
elif prefix == "f":
vs = split_line[1:]
nv = len(vs)
v0, t0, n0 = parse_f_v(vs[0])
for i in range(nv - 2): # triangulate (assume vertices are ordered)
v1, t1, n1 = parse_f_v(vs[i + 1])
v2, t2, n2 = parse_f_v(vs[i + 2])
faces.append([v0, v1, v2])
tfaces.append([t0, t1, t2])
nfaces.append([n0, n1, n2])
mesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
mesh.vt = (
torch.tensor(texcoords, dtype=torch.float32, device=device)
if len(texcoords) > 0
else None
)
mesh.vn = (
torch.tensor(normals, dtype=torch.float32, device=device)
if len(normals) > 0
else None
)
mesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
mesh.ft = (
torch.tensor(tfaces, dtype=torch.int32, device=device)
if len(texcoords) > 0
else None
)
mesh.fn = (
torch.tensor(nfaces, dtype=torch.int32, device=device)
if len(normals) > 0
else None
)
# see if there is vertex color
if mesh.v.size(-1) > 3:
mesh.vc = mesh.v[:, 3:]
mesh.v = mesh.v[:, :3]
if mesh.vc.size(-1) == 3:
mesh.vc = torch.cat([mesh.vc, torch.ones_like(mesh.vc[:, :1])], dim=-1)
print(f"[load_obj] use vertex color: {mesh.vc.shape}")
# try to retrieve mtl file
mtl_path_candidates = []
if mtl_path is not None:
mtl_path_candidates.append(mtl_path)
mtl_path_candidates.append(os.path.join(os.path.dirname(path), mtl_path))
mtl_path_candidates.append(path.replace(".obj", ".mtl"))
mtl_path = None
for candidate in mtl_path_candidates:
if os.path.exists(candidate):
mtl_path = candidate
break
# if albedo_path is not provided, try retrieve it from mtl
if mtl_path is not None and albedo_path is None:
with open(mtl_path, "r") as f:
lines = f.readlines()
for line in lines:
split_line = line.split()
# empty line
if len(split_line) == 0:
continue
prefix = split_line[0]
# NOTE: simply use the first map_Kd as albedo!
if "map_Kd" in prefix:
albedo_path = os.path.join(os.path.dirname(path), split_line[1])
print(f"[load_obj] use texture from: {albedo_path}")
break
# still not found albedo_path, or the path doesn't exist
if albedo_path is None or not os.path.exists(albedo_path):
# init an empty texture
print(f"[load_obj] init empty albedo!")
# albedo = np.random.rand(1024, 1024, 3).astype(np.float32)
albedo = np.ones((1024, 1024, 3), dtype=np.float32) * np.array([0.5, 0.5, 0.5]) # default color
mesh.textureless = True
else:
albedo = cv2.imread(albedo_path, cv2.IMREAD_UNCHANGED)
albedo = cv2.cvtColor(albedo, cv2.COLOR_BGR2RGB)
albedo = albedo.astype(np.float32) / 255
print(f"[load_obj] load texture: {albedo.shape}")
# import matplotlib.pyplot as plt
# plt.imshow(albedo)
# plt.show()
mesh.albedo = torch.tensor(albedo, dtype=torch.float32, device=device)
return mesh
@classmethod
def load_trimesh(cls, path, device=None):
mesh = cls()
# device
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
mesh.device = device
# use trimesh to load glb, assume only has one single RootMesh...
_data = trimesh.load(path)
if isinstance(_data, trimesh.Scene):
mesh_keys = list(_data.geometry.keys())
assert (
len(mesh_keys) == 1
), f"{path} contains more than one meshes, not supported!"
_mesh = _data.geometry[mesh_keys[0]]
elif isinstance(_data, trimesh.Trimesh):
_mesh = _data
else:
raise NotImplementedError(f"type {type(_data)} not supported!")
if hasattr(_mesh.visual, "material"):
_material = _mesh.visual.material
if isinstance(_material, trimesh.visual.material.PBRMaterial):
texture = np.array(_material.baseColorTexture).astype(np.float32) / 255
elif isinstance(_material, trimesh.visual.material.SimpleMaterial):
texture = (
np.array(_material.to_pbr().baseColorTexture).astype(np.float32) / 255
)
else:
raise NotImplementedError(f"material type {type(_material)} not supported!")
print(f"[load_obj] load texture: {texture.shape}")
mesh.albedo = torch.tensor(texture, dtype=torch.float32, device=device)
if hasattr(_mesh.visual, "uv"):
texcoords = _mesh.visual.uv
texcoords[:, 1] = 1 - texcoords[:, 1]
mesh.vt = (
torch.tensor(texcoords, dtype=torch.float32, device=device)
if len(texcoords) > 0
else None
)
else:
texcoords = None
if hasattr(_mesh.visual, "vertex_colors"):
colors = _mesh.visual.vertex_colors
mesh.vc = (
torch.tensor(colors, dtype=torch.float32, device=device) / 255
if len(colors) > 0
else None
)
vertices = _mesh.vertices
normals = _mesh.vertex_normals
# trimesh only support vertex uv...
faces = tfaces = nfaces = _mesh.faces
mesh.v = torch.tensor(vertices, dtype=torch.float32, device=device)
mesh.vn = (
torch.tensor(normals, dtype=torch.float32, device=device)
if len(normals) > 0
else None
)
mesh.f = torch.tensor(faces, dtype=torch.int32, device=device)
mesh.ft = (
torch.tensor(tfaces, dtype=torch.int32, device=device)
if texcoords is not None
else None
)
mesh.fn = (
torch.tensor(nfaces, dtype=torch.int32, device=device)
if normals is not None
else None
)
return mesh
# aabb
def aabb(self):
return torch.min(self.v, dim=0).values, torch.max(self.v, dim=0).values
# unit size
@torch.no_grad()
def auto_size(self):
vmin, vmax = self.aabb()
self.ori_center = (vmax + vmin) / 2
self.ori_scale = 1.2 / torch.max(vmax - vmin).item() # to ~ [-0.6, 0.6]
self.v = (self.v - self.ori_center) * self.ori_scale
def auto_normal(self):
i0, i1, i2 = self.f[:, 0].long(), self.f[:, 1].long(), self.f[:, 2].long()
v0, v1, v2 = self.v[i0, :], self.v[i1, :], self.v[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0)
# Splat face normals to vertices
face_normals = F.normalize(face_normals, dim=-1)
vn = torch.zeros_like(self.v)
vn.scatter_add_(0, i0[:, None].repeat(1, 3), face_normals)
vn.scatter_add_(0, i1[:, None].repeat(1, 3), face_normals)
vn.scatter_add_(0, i2[:, None].repeat(1, 3), face_normals)
vn = F.normalize(vn, dim=-1)
self.vn = vn
self.fn = self.f
self.face_normals = face_normals
def auto_uv(self, cache_path=None, vmap=True):
# try to load cache
if cache_path is not None:
cache_path = os.path.splitext(cache_path)[0] + '_uv.npz'
if cache_path is not None and os.path.exists(cache_path):
data = np.load(cache_path)
vt_np, ft_np, vmapping = data['vt'], data['ft'], data['vmapping']
else:
v_np = self.v.detach().cpu().numpy()
f_np = self.f.detach().int().cpu().numpy()
atlas = xatlas.Atlas()
atlas.add_mesh(v_np, f_np)
chart_options = xatlas.ChartOptions()
# chart_options.max_iterations = 4
atlas.generate(chart_options=chart_options)
vmapping, ft_np, vt_np = atlas[0] # [N], [M, 3], [N, 2]
# save to cache
if cache_path is not None:
np.savez(cache_path, vt=vt_np, ft=ft_np, vmapping=vmapping)
vt = torch.from_numpy(vt_np.astype(np.float32)).to(self.device)
ft = torch.from_numpy(ft_np.astype(np.int32)).to(self.device)
self.vt = vt
self.ft = ft
if vmap:
# remap v/f to vt/ft, so each v correspond to a unique vt. (necessary for gltf)
vmapping = torch.from_numpy(vmapping.astype(np.int64)).long().to(self.device)
self.align_v_to_vt(vmapping)
def align_v_to_vt(self, vmapping=None):
# remap v/f and vn/vn to vt/ft.
if vmapping is None:
ft = self.ft.view(-1).long()
f = self.f.view(-1).long()
vmapping = torch.zeros(self.vt.shape[0], dtype=torch.long, device=self.device)
vmapping[ft] = f # scatter, randomly choose one if index is not unique
if self.vn is not None and (self.f == self.fn).all():
self.vn = self.vn[vmapping]
self.fn = self.ft
self.v = self.v[vmapping]
self.f = self.ft
def align_vn_to_vt(self, vmapping=None):
if vmapping is None:
ft = self.ft.view(-1).long()
fn = self.f.view(-1).long()
vmapping = torch.zeros(self.vt.shape[0], dtype=torch.long, device=self.device)
vmapping[ft] = fn # scatter, randomly choose one if index is not unique
self.vn = self.vn[vmapping]
self.fn = self.ft
def to(self, device):
self.device = device
for name in ['v', 'f', 'vn', 'fn', 'vt', 'ft', 'albedo', 'vc', 'face_normals']:
tensor = getattr(self, name)
if tensor is not None:
setattr(self, name, tensor.to(device))
return self
def copy(self):
return Mesh(
v=self.v,
f=self.f,
vn=self.vn,
fn=self.fn,
vt=self.vt,
ft=self.ft,
vc=self.vc,
albedo=self.albedo,
device=self.device,
textureless=self.textureless)
def write(self, path, flip_yz=False):
mesh = self.copy()
if flip_yz:
mesh.v = mesh.v.clone()
mesh.vn = mesh.vn.clone()
mesh.v[..., 1] = -mesh.v[..., 1]
mesh.vn[..., 1] = -mesh.vn[..., 1]
mesh.v[..., [1, 2]] = mesh.v[..., [2, 1]]
mesh.vn[..., [1, 2]] = mesh.vn[..., [2, 1]]
if path.endswith('.ply'):
mesh.write_ply(path)
elif path.endswith('.obj'):
mesh.write_obj(path)
elif path.endswith('.glb') or path.endswith('.gltf'):
mesh.write_glb(path)
else:
raise NotImplementedError(f'format {path} not supported!')
# write to ply file (only geom)
def write_ply(self, path):
v_np = self.v.detach().cpu().numpy()
f_np = self.f.detach().cpu().numpy()
_mesh = trimesh.Trimesh(vertices=v_np, faces=f_np)
_mesh.export(path)
# write to gltf/glb file (geom + texture)
def write_glb(self, path):
assert self.vn is not None
if self.vt is None:
self.vt = self.v.new_zeros((self.v.size(0), 2))
self.ft = self.f
if (self.f != self.ft).any():
self.align_v_to_vt()
if (self.fn != self.ft).any():
self.align_vn_to_vt()
assert self.v.shape[0] == self.vn.shape[0] and self.v.shape[0] == self.vt.shape[0]
f_np = self.f.detach().cpu().numpy().astype(np.uint32)
v_np = self.v.detach().cpu().numpy().astype(np.float32)
vt_np = self.vt.detach().cpu().numpy().astype(np.float32)
vn_np = self.vn.detach().cpu().numpy().astype(np.float32)
albedo = self.albedo.detach().cpu().numpy() if self.albedo is not None \
else np.full((1024, 1024, 3), 0.5, dtype=np.float32)
albedo = (albedo * 255).astype(np.uint8)
albedo = cv2.cvtColor(albedo, cv2.COLOR_RGB2BGR)
f_np_blob = f_np.flatten().tobytes()
v_np_blob = v_np.tobytes()
vt_np_blob = vt_np.tobytes()
vn_np_blob = vn_np.tobytes()
albedo_blob = cv2.imencode('.png', albedo)[1].tobytes()
gltf = pygltflib.GLTF2(
scene=0,
scenes=[pygltflib.Scene(nodes=[0])],
nodes=[pygltflib.Node(mesh=0)],
meshes=[pygltflib.Mesh(primitives=[
pygltflib.Primitive(
# indices to accessors (0 is triangles)
attributes=pygltflib.Attributes(
POSITION=1, TEXCOORD_0=2, NORMAL=3
),
indices=0, material=0,
)
])],
materials=[
pygltflib.Material(
pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(
baseColorTexture=pygltflib.TextureInfo(index=0, texCoord=0),
metallicFactor=0.0,
roughnessFactor=1.0,
),
alphaCutoff=0,
doubleSided=True,
)
],
textures=[
pygltflib.Texture(sampler=0, source=0),
],
samplers=[
pygltflib.Sampler(magFilter=pygltflib.LINEAR, minFilter=pygltflib.LINEAR_MIPMAP_LINEAR,
wrapS=pygltflib.REPEAT, wrapT=pygltflib.REPEAT),
],
images=[
# use embedded (buffer) image
pygltflib.Image(bufferView=4, mimeType="image/png"),
],
buffers=[
pygltflib.Buffer(
byteLength=len(f_np_blob) + len(v_np_blob) + len(vt_np_blob) + len(vn_np_blob) + len(albedo_blob))
],
# buffer view (based on dtype)
bufferViews=[
# triangles; as flatten (element) array
pygltflib.BufferView(
buffer=0,
byteLength=len(f_np_blob),
target=pygltflib.ELEMENT_ARRAY_BUFFER, # GL_ELEMENT_ARRAY_BUFFER (34963)
),
# positions; as vec3 array
pygltflib.BufferView(
buffer=0,
byteOffset=len(f_np_blob),
byteLength=len(v_np_blob),
byteStride=12, # vec3
target=pygltflib.ARRAY_BUFFER, # GL_ARRAY_BUFFER (34962)
),
# texcoords; as vec2 array
pygltflib.BufferView(
buffer=0,
byteOffset=len(f_np_blob) + len(v_np_blob),
byteLength=len(vt_np_blob),
byteStride=8, # vec2
target=pygltflib.ARRAY_BUFFER,
),
# normals; as vec3 array
pygltflib.BufferView(
buffer=0,
byteOffset=len(f_np_blob) + len(v_np_blob) + len(vt_np_blob),
byteLength=len(vn_np_blob),
byteStride=12, # vec3
target=pygltflib.ARRAY_BUFFER,
),
# texture; as none target
pygltflib.BufferView(
buffer=0,
byteOffset=len(f_np_blob) + len(v_np_blob) + len(vt_np_blob) + len(vn_np_blob),
byteLength=len(albedo_blob),
),
],
accessors=[
# 0 = triangles
pygltflib.Accessor(
bufferView=0,
componentType=pygltflib.UNSIGNED_INT, # GL_UNSIGNED_INT (5125)
count=f_np.size,
type=pygltflib.SCALAR,
max=[int(f_np.max())],
min=[int(f_np.min())],
),
# 1 = positions
pygltflib.Accessor(
bufferView=1,
componentType=pygltflib.FLOAT, # GL_FLOAT (5126)
count=len(v_np),
type=pygltflib.VEC3,
max=v_np.max(axis=0).tolist(),
min=v_np.min(axis=0).tolist(),
),
# 2 = texcoords
pygltflib.Accessor(
bufferView=2,
componentType=pygltflib.FLOAT,
count=len(vt_np),
type=pygltflib.VEC2,
max=vt_np.max(axis=0).tolist(),
min=vt_np.min(axis=0).tolist(),
),
# 3 = normals
pygltflib.Accessor(
bufferView=3,
componentType=pygltflib.FLOAT,
count=len(vn_np),
type=pygltflib.VEC3,
max=vn_np.max(axis=0).tolist(),
min=vn_np.min(axis=0).tolist(),
),
],
)
# set actual data
gltf.set_binary_blob(f_np_blob + v_np_blob + vt_np_blob + vn_np_blob + albedo_blob)
# glb = b"".join(gltf.save_to_bytes())
gltf.save(path)
# write to obj file (geom + texture)
def write_obj(self, path):
mtl_path = path.replace(".obj", ".mtl")
albedo_path = path.replace(".obj", "_albedo.png")
v_np = self.v.detach().cpu().numpy()
vt_np = self.vt.detach().cpu().numpy() if self.vt is not None else None
vn_np = self.vn.detach().cpu().numpy() if self.vn is not None else None
f_np = self.f.detach().cpu().numpy()
ft_np = self.ft.detach().cpu().numpy() if self.ft is not None else None
fn_np = self.fn.detach().cpu().numpy() if self.fn is not None else None
with open(path, "w") as fp:
fp.write(f"mtllib {os.path.basename(mtl_path)} \n")
for v in v_np:
fp.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f} \n")
if vt_np is not None:
for v in vt_np:
fp.write(f"vt {v[0]:.4f} {1 - v[1]:.4f} \n")
if vn_np is not None:
for v in vn_np:
fp.write(f"vn {v[0]:.4f} {v[1]:.4f} {v[2]:.4f} \n")
fp.write(f"usemtl defaultMat \n")
for i in range(len(f_np)):
fp.write(
f'f {f_np[i, 0] + 1}/{ft_np[i, 0] + 1 if ft_np is not None else ""}/{fn_np[i, 0] + 1 if fn_np is not None else ""} \
{f_np[i, 1] + 1}/{ft_np[i, 1] + 1 if ft_np is not None else ""}/{fn_np[i, 1] + 1 if fn_np is not None else ""} \
{f_np[i, 2] + 1}/{ft_np[i, 2] + 1 if ft_np is not None else ""}/{fn_np[i, 2] + 1 if fn_np is not None else ""} \n'
)
with open(mtl_path, "w") as fp:
fp.write(f"newmtl defaultMat \n")
fp.write(f"Ka 1 1 1 \n")
fp.write(f"Kd 1 1 1 \n")
fp.write(f"Ks 0 0 0 \n")
fp.write(f"Tr 1 \n")
fp.write(f"illum 1 \n")
fp.write(f"Ns 0 \n")
if not self.textureless and self.albedo is not None:
fp.write(f"map_Kd {os.path.basename(albedo_path)} \n")
if not self.textureless and self.albedo is not None:
albedo = self.albedo.detach().cpu().numpy()
albedo = (albedo * 255).astype(np.uint8)
cv2.imwrite(albedo_path, cv2.cvtColor(albedo, cv2.COLOR_RGB2BGR))
def normalize_mesh(mesh, tgt_radius=0.9):
mb = miniball.Miniball(mesh.v[:, :2].cpu().numpy())
center_xy = mb.center()
radius_xy_sq = mb.squared_radius()
max_z = mesh.v[:, 2].max().item()
min_z = mesh.v[:, 2].min().item()
center = mesh.v.new_tensor([center_xy[0], center_xy[1], (max_z + min_z) / 2])
radius = max(math.sqrt(radius_xy_sq), (max_z - min_z) / 2)
scale = tgt_radius / radius
mesh.v = (mesh.v - center) * scale
return mesh, center.tolist(), scale
def check_has_texture_single(geom):
return isinstance(geom.visual, TextureVisuals) and geom.visual.material.baseColorTexture is not None
def check_has_texture(mesh):
if isinstance(mesh, trimesh.Scene):
has_texture = []
for geom in mesh.geometry.values():
has_texture.append(check_has_texture_single(geom))
elif isinstance(mesh, trimesh.Trimesh):
has_texture = check_has_texture_single(mesh)
else:
raise NotImplementedError(f"type {type(mesh)} not supported!")
return has_texture
def create_texture(geom):
if hasattr(geom.visual, 'material') and hasattr(geom.visual.material, 'main_color'):
main_color = tuple(geom.visual.material.main_color)
else:
main_color = (128, 128, 128)
geom.visual = trimesh.visual.TextureVisuals(
uv=np.full((geom.vertices.shape[0], 2), 0.5),
material=trimesh.visual.material.PBRMaterial(
baseColorTexture=Image.new('RGB', (8, 8), main_color)
)
)
def color_to_texture(mesh):
if isinstance(mesh, trimesh.Scene):
for geom in mesh.geometry.values():
if not check_has_texture_single(geom):
create_texture(geom)
elif isinstance(mesh, trimesh.Trimesh):
if not check_has_texture_single(mesh):
create_texture(mesh)
else:
raise NotImplementedError(f"type {type(mesh)} not supported!")
return mesh
def purge_scene(scene):
update_flag = False
delete_list = []
for name, geom in scene.geometry.items():
if not isinstance(geom, trimesh.Trimesh):
update_flag = True
delete_list.append(name)
for name in delete_list:
scene.delete_geometry(name)
return update_flag