hub/venv/lib/python3.7/site-packages/trimesh/visual/material.py

408 lines
11 KiB
Python
Raw Normal View History

"""
material.py
-------------
Store visual materials as objects.
"""
import numpy as np
from . import color
from .. import util
from .. import grouping
class Material(object):
def __init__(self, *args, **kwargs):
raise NotImplementedError('material must be subclassed!')
def __hash__(self):
return id(self)
@property
def main_color(self):
raise NotImplementedError('material must be subclassed!')
class SimpleMaterial(Material):
"""
Hold a single image texture.
"""
def __init__(self,
image=None,
diffuse=None,
ambient=None,
specular=None,
glossiness=None,
**kwargs):
# save image
self.image = image
# save material colors as RGBA
self.ambient = color.to_rgba(ambient)
self.diffuse = color.to_rgba(diffuse)
self.specular = color.to_rgba(specular)
# save Ns
self.glossiness = glossiness
# save other keyword arguments
self.kwargs = kwargs
def to_color(self, uv):
return color.uv_to_color(uv, self.image)
def to_obj(self, tex_name=None, mtl_name=None):
"""
Convert the current material to an OBJ format material.
Parameters
-----------
name : str or None
Name to apply to the material
Returns
-----------
tex_name : str
Name of material
mtl_name : str
Name of mtl file in files
files : dict
Data as {file name : bytes}
"""
# material parameters as 0.0-1.0 RGB
Ka = color.to_float(self.ambient)[:3]
Kd = color.to_float(self.diffuse)[:3]
Ks = color.to_float(self.specular)[:3]
if tex_name is None:
tex_name = 'material0'
if mtl_name is None:
mtl_name = '{}.mtl'.format(tex_name)
# what is the name of the export image to save
image_type = self.image.format
if image_type is None:
image_type = 'png'
image_name = '{}.{}'.format(
tex_name, image_type).lower()
# create an MTL file
mtl = '\n'.join(
['# https://github.com/mikedh/trimesh',
'newmtl {}'.format(tex_name),
'Ka {:0.8f} {:0.8f} {:0.8f}'.format(*Ka),
'Kd {:0.8f} {:0.8f} {:0.8f}'.format(*Kd),
'Ks {:0.8f} {:0.8f} {:0.8f}'.format(*Ks),
'Ns {:0.8f}'.format(self.glossiness),
'map_Kd {}'.format(image_name)])
# save the image texture as bytes in the original format
f_obj = util.BytesIO()
self.image.save(fp=f_obj, format=image_type)
f_obj.seek(0)
# collect the OBJ data into files
data = {mtl_name: mtl.encode('utf-8'),
image_name: f_obj.read()}
return data, tex_name, mtl_name
def __hash__(self):
"""
Provide a hash of the material so we can detect
duplicates.
Returns
------------
hash : int
Hash of image and parameters
"""
if hasattr(self.image, 'tobytes'):
# start with hash of raw image bytes
hashed = hash(self.image.tobytes())
else:
# otherwise start with zero
hashed = 0
# we will add additional parameters with
# an in-place xor of the additional value
# if stored as numpy arrays add parameters
if hasattr(self.ambient, 'tobytes'):
hashed ^= hash(self.ambient.tobytes())
if hasattr(self.diffuse, 'tobytes'):
hashed ^= hash(self.diffuse.tobytes())
if hasattr(self.specular, 'tobytes'):
hashed ^= hash(self.specular.tobytes())
if isinstance(self.glossiness, float):
hashed ^= hash(int(self.glossiness * 1000))
return hashed
@property
def main_color(self):
"""
Return the most prominent color.
"""
return self.diffuse
@property
def glossiness(self):
if hasattr(self, '_glossiness'):
return self._glossiness
return 1.0
@glossiness.setter
def glossiness(self, value):
if value is None:
return
self._glossiness = float(value)
def to_pbr(self):
"""
Convert the current simple material to a PBR material.
Returns
------------
pbr : PBRMaterial
Contains material information in PBR format.
"""
# convert specular exponent to roughness
roughness = (2 / (self.glossiness + 2)) ** (1.0 / 4.0)
return PBRMaterial(roughnessFactor=roughness,
baseColorTexture=self.image,
baseColorFactor=self.diffuse)
class PBRMaterial(Material):
"""
Create a material for physically based rendering as
specified by GLTF 2.0:
https://git.io/fhkPZ
Parameters with `Texture` in them must be PIL.Image objects
"""
def __init__(self,
name=None,
emissiveFactor=None,
emissiveTexture=None,
normalTexture=None,
occlusionTexture=None,
baseColorTexture=None,
baseColorFactor=None,
metallicFactor=None,
roughnessFactor=None,
metallicRoughnessTexture=None,
doubleSided=False,
alphaMode='OPAQUE',
alphaCutoff=0.5):
# (4,) float
if baseColorFactor is not None:
baseColorFactor = color.to_rgba(baseColorFactor)
self.baseColorFactor = baseColorFactor
if emissiveFactor is not None:
emissiveFactor = np.array(emissiveFactor, dtype=np.float64)
# (3,) float
self.emissiveFactor = emissiveFactor
# float
self.metallicFactor = metallicFactor
self.roughnessFactor = roughnessFactor
self.alphaCutoff = alphaCutoff
# PIL image
self.normalTexture = normalTexture
self.emissiveTexture = emissiveTexture
self.occlusionTexture = occlusionTexture
self.baseColorTexture = baseColorTexture
self.metallicRoughnessTexture = metallicRoughnessTexture
# bool
self.doubleSided = doubleSided
# str
self.name = name
self.alphaMode = alphaMode
def to_color(self, uv):
"""
Get the rough color at a list of specified UV coordinates.
Parameters
-------------
uv : (n, 2) float
UV coordinates on the material
Returns
-------------
colors :
"""
colors = color.uv_to_color(uv=uv, image=self.baseColorTexture)
if colors is None and self.baseColorFactor is not None:
colors = self.baseColorFactor.copy()
return colors
@property
def main_color(self):
# will return default color if None
result = color.to_rgba(self.baseColorFactor)
return result
def __hash__(self):
"""
Provide a hash of the material so we can detect
duplicate materials.
Returns
------------
hash : int
Hash of image and parameters
"""
if hasattr(self.baseColorTexture, 'tobytes'):
# start with hash of raw image bytes
hashed = hash(self.baseColorTexture.tobytes())
else:
# otherwise start with zero
hashed = 0
# we will add additional parameters with
# an in-place xor of the additional value
# if stored as numpy arrays add parameters
if hasattr(self.baseColorFactor, 'tobytes'):
hashed ^= hash(self.baseColorFactor.tobytes())
return hashed
def empty_material(color=None):
"""
Return an empty material set to a single color
Parameters
-----------
color : None or (3,) uint8
RGB color
Returns
-------------
material : SimpleMaterial
Image is a a one pixel RGB
"""
from PIL import Image
if color is None or np.shape(color) not in ((3,), (4,)):
color = np.array([255, 255, 255], dtype=np.uint8)
else:
color = np.array(color, dtype=np.uint8)[:3]
# create a one pixel RGB image
image = Image.fromarray(
np.tile(color, (4, 1)).reshape((2, 2, 3)))
return SimpleMaterial(image=image)
def from_color(vertex_colors):
"""
Convert vertex colors into UV coordinates and materials.
TODO : pack colors
Parameters
------------
vertex_colors : (n, 3) float
Array of vertex colors
Returns
------------
material : SimpleMaterial
Material containing color information
uvs : (n, 2) float
UV coordinates
"""
unique, inverse = grouping.unique_rows(vertex_colors)
# TODO : tile colors nicely
material = empty_material(color=vertex_colors[unique[0]])
uvs = np.zeros((len(vertex_colors), 2)) + 0.5
return material, uvs
def pack(materials, uvs, deduplicate=True):
"""
Pack multiple materials with texture into a single material.
Parameters
-----------
materials : (n,) Material
List of multiple materials
uvs : (n, m, 2) float
Original UV coordinates
Returns
------------
material : Material
Combined material
uv : (p, 2) float
Combined UV coordinates
"""
from PIL import Image
from ..path import packing
import collections
if deduplicate:
# start by collecting a list of indexes for each material hash
unique_idx = collections.defaultdict(list)
[unique_idx[hash(m)].append(i) for i, m in enumerate(materials)]
# now we only need the indexes and don't care about the hashes
mat_idx = list(unique_idx.values())
else:
# otherwise just use all the indexes
mat_idx = np.arange(len(materials)).reshape((-1, 1))
# store the images to combine later
images = []
# first collect the images from the materials
for idx in mat_idx:
# get the first material from the group
m = materials[idx[0]]
# extract an image for each material
if isinstance(m, PBRMaterial):
if m.baseColorTexture is not None:
img = m.baseColorTexture
elif m.baseColorFactor is not None:
img = Image.fromarray(m.baseColorFactor[:3].reshape((1, 1, 3)))
else:
img = Image.new(mode='RGB', size=(1, 1))
elif hasattr(m, 'image'):
img = m.image
else:
raise ValueError('no image to pack!')
images.append(img)
# pack the multiple images into a single large image
final, offsets = packing.images(images, power_resize=True)
# the size of the final texture image
final_size = np.array(final.size, dtype=np.float64)
# collect scaled new UV coordinates
new_uv = []
for idxs, img, off in zip(mat_idx, images, offsets):
# how big was the original image
scale = img.size / final_size
# what is the offset in fractions of final image
uv_off = off / final_size
# scale and translate each of the new UV coordinates
# [new_uv.append((uvs[i] * scale) + uv_off) for i in idxs]
# TODO : figure out why this is broken sometimes...
[new_uv.append((uvs[i] * scale) + uv_off) for i in idxs]
# stack UV coordinates into single (n, 2) array
stacked = np.vstack(new_uv)
return SimpleMaterial(image=final), stacked