init
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# flake8: NOQA
|
||||
"""
|
||||
visual
|
||||
-------------
|
||||
|
||||
Handle visual properties for meshes, including color and texture
|
||||
"""
|
||||
|
||||
from .color import (
|
||||
ColorVisuals,
|
||||
random_color,
|
||||
to_rgba,
|
||||
DEFAULT_COLOR,
|
||||
interpolate,
|
||||
uv_to_color,
|
||||
uv_to_interpolated_color,
|
||||
linear_color_map,
|
||||
)
|
||||
|
||||
from .texture import TextureVisuals
|
||||
from .objects import create_visual, concatenate
|
||||
|
||||
from . import color
|
||||
from . import texture
|
||||
from . import objects
|
||||
from . import material
|
||||
from .. import resolvers
|
||||
|
||||
|
||||
# explicitly list imports in __all__
|
||||
# as otherwise flake8 gets mad
|
||||
__all__ = [
|
||||
"color",
|
||||
"texture",
|
||||
"resolvers",
|
||||
"TextureVisuals",
|
||||
"ColorVisuals",
|
||||
"random_color",
|
||||
"to_rgba",
|
||||
"create_visual",
|
||||
"DEFAULT_COLOR",
|
||||
"interpolate",
|
||||
"linear_color_map",
|
||||
"uv_to_color",
|
||||
"uv_to_interpolated_color",
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
base.py
|
||||
-------------
|
||||
|
||||
The base class for `Visual` objects
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
from ..util import ABC
|
||||
|
||||
|
||||
class Visuals(ABC):
|
||||
"""
|
||||
Parent of Visual classes.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def kind(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_vertices(self, mask):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_faces(self, mask):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def concatenate(self, other):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def __hash__(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def copy(self):
|
||||
pass
|
||||
|
||||
def __add__(self, other):
|
||||
"""
|
||||
Concatenate two ColorVisuals objects into a single object.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
other : Visuals
|
||||
Other visual to concatenate
|
||||
|
||||
Returns
|
||||
-----------
|
||||
result : Visuals
|
||||
Object containing information from current
|
||||
object and other in the order (self, other)
|
||||
"""
|
||||
return self.concatenate(other)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,351 @@
|
||||
import numpy as np
|
||||
|
||||
from ..constants import log
|
||||
from ..exceptions import ExceptionWrapper
|
||||
from ..typed import ArrayLike, Number, Optional
|
||||
from .color import linear_to_srgb, srgb_to_linear
|
||||
|
||||
try:
|
||||
from PIL.Image import Image, fromarray
|
||||
except BaseException as E:
|
||||
Image = ExceptionWrapper(E)
|
||||
fromarray = ExceptionWrapper(E)
|
||||
|
||||
|
||||
def specular_to_pbr(
|
||||
specularFactor: Optional[ArrayLike] = None,
|
||||
glossinessFactor: Optional[Number] = None,
|
||||
specularGlossinessTexture: Optional["Image"] = None,
|
||||
diffuseTexture: Optional["Image"] = None,
|
||||
diffuseFactor: Optional[ArrayLike] = None,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
"""
|
||||
Convert the KHR_materials_pbrSpecularGlossiness to a
|
||||
metallicRoughness visual.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
specularFactor : list[float]
|
||||
Specular color values. Ignored if specularGlossinessTexture
|
||||
is present and defaults to [1.0, 1.0, 1.0].
|
||||
glossinessFactor : float
|
||||
glossiness factor in range [0, 1], scaled
|
||||
specularGlossinessTexture if present.
|
||||
Defaults to 1.0.
|
||||
specularGlossinessTexture : PIL.Image
|
||||
Texture with 4 color channels. With [0,1,2] representing
|
||||
specular RGB and 3 glossiness.
|
||||
diffuseTexture : PIL.Image
|
||||
Texture with 4 color channels. With [0,1,2] representing diffuse
|
||||
RGB and 3 opacity.
|
||||
diffuseFactor: float
|
||||
Diffuse RGBA color. scales diffuseTexture if present.
|
||||
Defaults to [1.0, 1.0, 1.0, 1.0].
|
||||
|
||||
Returns
|
||||
----------
|
||||
kwargs : dict
|
||||
Constructor args for a PBRMaterial object.
|
||||
Containing:
|
||||
- either baseColorTexture or baseColorFactor
|
||||
- either metallicRoughnessTexture or metallicFactor and roughnessFactor
|
||||
"""
|
||||
# based on:
|
||||
# https://github.com/KhronosGroup/glTF/blob/89427b26fcac884385a2e6d5803d917ab5d1b04f/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js#L33-L64
|
||||
|
||||
if isinstance(Image, ExceptionWrapper):
|
||||
log.debug("unable to convert specular-glossy material without pillow!")
|
||||
result = {}
|
||||
if isinstance(diffuseTexture, dict):
|
||||
result["baseColorTexture"] = diffuseTexture
|
||||
if diffuseFactor is not None:
|
||||
result["baseColorFactor"] = diffuseFactor
|
||||
return result
|
||||
|
||||
dielectric_specular = np.array([0.04, 0.04, 0.04], dtype=np.float32)
|
||||
epsilon = 1e-6
|
||||
|
||||
def solve_metallic(diffuse, specular, one_minus_specular_strength):
|
||||
if isinstance(specular, float) and specular < dielectric_specular[0]:
|
||||
return 0.0
|
||||
|
||||
if len(diffuse.shape) == 2:
|
||||
diffuse = diffuse[..., None]
|
||||
if len(specular.shape) == 2:
|
||||
specular = specular[..., None]
|
||||
|
||||
a = dielectric_specular[0]
|
||||
b = (
|
||||
diffuse * one_minus_specular_strength / (1.0 - dielectric_specular[0])
|
||||
+ specular
|
||||
- 2.0 * dielectric_specular[0]
|
||||
)
|
||||
c = dielectric_specular[0] - specular
|
||||
D = b * b - 4.0 * a * c
|
||||
D = np.clip(D, epsilon, None)
|
||||
metallic = np.clip((-b + np.sqrt(D)) / (2.0 * a), 0.0, 1.0)
|
||||
if isinstance(metallic, np.ndarray):
|
||||
metallic[specular < dielectric_specular[0]] = 0.0
|
||||
return metallic
|
||||
|
||||
def get_perceived_brightness(rgb):
|
||||
return np.sqrt(np.dot(rgb[..., :3] ** 2, [0.299, 0.587, 0.114]))
|
||||
|
||||
def toPIL(img, mode=None):
|
||||
if isinstance(img, Image):
|
||||
return img
|
||||
if img.dtype == np.float32 or img.dtype == np.float64:
|
||||
img = (np.clip(img, 0.0, 1.0) * 255.0).astype(np.uint8)
|
||||
return fromarray(img)
|
||||
|
||||
def get_float(val):
|
||||
if isinstance(val, float):
|
||||
return val
|
||||
if isinstance(val, np.ndarray) and len(val.shape) == 1:
|
||||
return val[0]
|
||||
return val.tolist()
|
||||
|
||||
def get_diffuse(diffuseFactor, diffuseTexture):
|
||||
diffuseFactor = (
|
||||
diffuseFactor if diffuseFactor is not None else [1.0, 1.0, 1.0, 1.0]
|
||||
)
|
||||
diffuseFactor = np.array(diffuseFactor, dtype=np.float32)
|
||||
|
||||
if diffuseTexture is not None:
|
||||
if diffuseTexture.mode == "BGR":
|
||||
diffuseTexture = diffuseTexture.convert("RGB")
|
||||
elif diffuseTexture.mode == "BGRA":
|
||||
diffuseTexture = diffuseTexture.convert("RGBA")
|
||||
|
||||
diffuse = np.array(diffuseTexture) / 255.0
|
||||
# diffuseFactor must be applied to linear scaled colors .
|
||||
# Sometimes, diffuse texture is only 2 channels, how do we know
|
||||
# if they are encoded sRGB or linear?
|
||||
diffuse = convert_texture_srgb2lin(diffuse)
|
||||
|
||||
if len(diffuse.shape) == 2:
|
||||
diffuse = diffuse[..., None]
|
||||
if diffuse.shape[-1] == 1:
|
||||
diffuse = diffuse * diffuseFactor
|
||||
elif diffuse.shape[-1] == 2:
|
||||
alpha = diffuse[..., 1:2]
|
||||
diffuse = diffuse[..., :1] * diffuseFactor
|
||||
if diffuseFactor.shape[-1] == 3:
|
||||
# this should actually not happen, but it seems like many materials are not complying with the spec
|
||||
diffuse = np.concatenate([diffuse, alpha], axis=-1)
|
||||
else:
|
||||
diffuse[..., -1:] *= alpha
|
||||
elif diffuse.shape[-1] == diffuseFactor.shape[-1]:
|
||||
diffuse = diffuse * diffuseFactor
|
||||
elif diffuse.shape[-1] == 3 and diffuseFactor.shape[-1] == 4:
|
||||
diffuse = (
|
||||
np.concatenate([diffuse, np.ones_like(diffuse[..., :1])], axis=-1)
|
||||
* diffuseFactor
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
"`diffuseFactor` and `diffuseTexture` have incompatible shapes: "
|
||||
+ f"{diffuseFactor.shape} and {diffuse.shape}"
|
||||
)
|
||||
else:
|
||||
diffuse = diffuseFactor if diffuseFactor is not None else [1, 1, 1, 1]
|
||||
diffuse = np.array(diffuse, dtype=np.float32)
|
||||
return diffuse
|
||||
|
||||
def get_specular_glossiness(
|
||||
specularFactor, glossinessFactor, specularGlossinessTexture
|
||||
):
|
||||
if specularFactor is None:
|
||||
specularFactor = [1.0, 1.0, 1.0]
|
||||
specularFactor = np.array(specularFactor, dtype=np.float32)
|
||||
if glossinessFactor is None:
|
||||
glossinessFactor = 1.0
|
||||
glossinessFactor = np.array([glossinessFactor], dtype=np.float32)
|
||||
|
||||
# specularGlossinessTexture should be a texture with 4 channels,
|
||||
# 3 sRGB channels for specular and 1 linear channel for glossiness.
|
||||
# in practice, it can also have just 1, 2, or 3 channels which are then to
|
||||
# be multiplied with the provided factors
|
||||
|
||||
if specularGlossinessTexture is not None:
|
||||
if specularGlossinessTexture.mode == "BGR":
|
||||
specularGlossinessTexture = specularGlossinessTexture.convert("RGB")
|
||||
elif specularGlossinessTexture.mode == "BGRA":
|
||||
specularGlossinessTexture = specularGlossinessTexture.convert("RGBA")
|
||||
|
||||
specularGlossinessTexture = np.array(specularGlossinessTexture) / 255.0
|
||||
specularTexture, glossinessTexture = None, None
|
||||
|
||||
if len(specularGlossinessTexture.shape) == 2:
|
||||
# use the one channel as a multiplier for specular and glossiness
|
||||
specularTexture = glossinessTexture = specularGlossinessTexture.reshape(
|
||||
specularGlossinessTexture.shape[0],
|
||||
specularGlossinessTexture.shape[1],
|
||||
1,
|
||||
)
|
||||
elif specularGlossinessTexture.shape[-1] == 1:
|
||||
# use the one channel as a multiplier for specular and glossiness
|
||||
specularTexture = glossinessTexture = specularGlossinessTexture[
|
||||
..., np.newaxis
|
||||
]
|
||||
elif specularGlossinessTexture.shape[-1] == 3:
|
||||
# all channels are specular, glossiness is only a factor
|
||||
specularTexture = specularGlossinessTexture[..., :3]
|
||||
elif specularGlossinessTexture.shape[-1] == 2:
|
||||
# first channel is specular, last channel is glossiness
|
||||
specularTexture = specularGlossinessTexture[..., :1]
|
||||
glossinessTexture = specularGlossinessTexture[..., 1:2]
|
||||
elif specularGlossinessTexture.shape[-1] == 4:
|
||||
# first 3 channels are specular, last channel is glossiness
|
||||
specularTexture = specularGlossinessTexture[..., :3]
|
||||
glossinessTexture = specularGlossinessTexture[..., 3:]
|
||||
|
||||
if specularTexture is not None:
|
||||
# specular texture channels are sRGB
|
||||
specularTexture = convert_texture_srgb2lin(specularTexture)
|
||||
specular = specularTexture * specularFactor
|
||||
else:
|
||||
specular = specularFactor
|
||||
|
||||
if glossinessTexture is not None:
|
||||
# glossiness texture channel is linear
|
||||
glossiness = glossinessTexture * glossinessFactor
|
||||
else:
|
||||
glossiness = glossinessFactor
|
||||
|
||||
one_minus_specular_strength = 1.0 - np.max(specular, axis=-1, keepdims=True)
|
||||
else:
|
||||
specular = specularFactor if specularFactor is not None else [1.0, 1.0, 1.0]
|
||||
specular = np.array(specular, dtype=np.float32)
|
||||
glossiness = glossinessFactor if glossinessFactor is not None else 1.0
|
||||
glossiness = np.array(glossiness, dtype=np.float32)
|
||||
one_minus_specular_strength = 1.0 - max(specular[:3])
|
||||
|
||||
return specular, glossiness, one_minus_specular_strength
|
||||
|
||||
if diffuseTexture is not None and specularGlossinessTexture is not None:
|
||||
# reshape to the size of the largest texture
|
||||
max_shape = tuple(
|
||||
max(diffuseTexture.size[i], specularGlossinessTexture.size[i])
|
||||
for i in range(2)
|
||||
)
|
||||
if (
|
||||
diffuseTexture.size[0] != max_shape[0]
|
||||
or diffuseTexture.size[1] != max_shape[1]
|
||||
):
|
||||
diffuseTexture = diffuseTexture.resize(max_shape)
|
||||
if (
|
||||
specularGlossinessTexture.size[0] != max_shape[0]
|
||||
or specularGlossinessTexture.size[1] != max_shape[1]
|
||||
):
|
||||
specularGlossinessTexture = specularGlossinessTexture.resize(max_shape)
|
||||
|
||||
def convert_texture_srgb2lin(texture):
|
||||
"""
|
||||
Wrapper for srgb2lin that converts color values from sRGB to linear.
|
||||
If texture has 2 or 4 channels, the last channel (alpha) is left unchanged.
|
||||
"""
|
||||
result = texture.copy()
|
||||
color_channels = result.shape[-1]
|
||||
# only scale the color channels, not the alpha channel
|
||||
if color_channels == 4 or color_channels == 2:
|
||||
color_channels -= 1
|
||||
result[..., :color_channels] = srgb_to_linear(result[..., :color_channels])
|
||||
return result
|
||||
|
||||
def convert_texture_lin2srgb(texture):
|
||||
"""
|
||||
Wrapper for lin2srgb that converts color values from linear to sRGB.
|
||||
If texture has 2 or 4 channels, the last channel (alpha) is left unchanged.
|
||||
"""
|
||||
|
||||
result = texture.copy()
|
||||
color_channels = result.shape[-1]
|
||||
# only scale the color channels, not the alpha channel
|
||||
if color_channels == 4 or color_channels == 2:
|
||||
color_channels -= 1
|
||||
result[..., :color_channels] = linear_to_srgb(result[..., :color_channels])
|
||||
return result
|
||||
|
||||
diffuse = get_diffuse(diffuseFactor, diffuseTexture)
|
||||
specular, glossiness, one_minus_specular_strength = get_specular_glossiness(
|
||||
specularFactor, glossinessFactor, specularGlossinessTexture
|
||||
)
|
||||
|
||||
metallic = solve_metallic(
|
||||
get_perceived_brightness(diffuse),
|
||||
get_perceived_brightness(specular),
|
||||
one_minus_specular_strength,
|
||||
)
|
||||
if not isinstance(metallic, np.ndarray):
|
||||
metallic = np.array(metallic, dtype=np.float32)
|
||||
|
||||
diffuse_rgb = diffuse[..., :3]
|
||||
|
||||
base_color_from_diffuse = diffuse_rgb * (
|
||||
one_minus_specular_strength
|
||||
/ (1.0 - dielectric_specular[0])
|
||||
/ np.clip((1.0 - metallic), epsilon, None)
|
||||
)
|
||||
base_color_from_specular = (specular - dielectric_specular * (1.0 - metallic)) * (
|
||||
1.0 / np.clip(metallic, epsilon, None)
|
||||
)
|
||||
mm = metallic * metallic
|
||||
base_color = mm * base_color_from_specular + (1.0 - mm) * base_color_from_diffuse
|
||||
base_color = np.clip(base_color, 0.0, 1.0)
|
||||
|
||||
# get opacity
|
||||
try:
|
||||
if diffuse.shape == (4,):
|
||||
# opacity is a single scalar value
|
||||
opacity = diffuse[-1]
|
||||
if base_color.shape == (3,):
|
||||
# simple case with one color and diffuse with opacity
|
||||
# add on the opacity from the diffuse color
|
||||
base_color = np.append(base_color, opacity)
|
||||
elif len(base_color.shape) == 3:
|
||||
# stack opacity to match the base color array
|
||||
dim = base_color.shape
|
||||
base_color = np.dstack(
|
||||
(
|
||||
base_color,
|
||||
np.full(np.prod(dim[:2]), opacity).reshape((dim[0], dim[1], 1)),
|
||||
)
|
||||
)
|
||||
elif diffuse.shape[-1] == 4:
|
||||
opacity = diffuse[..., -1]
|
||||
base_color = np.concatenate([base_color, opacity[..., None]], axis=-1)
|
||||
except BaseException:
|
||||
log.error("unable to get opacity", exc_info=True)
|
||||
|
||||
result = {}
|
||||
if len(base_color.shape) > 1:
|
||||
# convert back to sRGB
|
||||
result["baseColorTexture"] = toPIL(
|
||||
convert_texture_lin2srgb(base_color),
|
||||
mode=("RGB" if base_color.shape[-1] == 3 else "RGBA"),
|
||||
)
|
||||
else:
|
||||
result["baseColorFactor"] = base_color.tolist()
|
||||
|
||||
if len(metallic.shape) > 1 or len(glossiness.shape) > 1:
|
||||
if len(glossiness.shape) == 1:
|
||||
glossiness = np.tile(glossiness, (metallic.shape[0], metallic.shape[1], 1))
|
||||
if len(metallic.shape) == 1:
|
||||
metallic = np.tile(metallic, (glossiness.shape[0], glossiness.shape[1], 1))
|
||||
|
||||
# we need to use RGB textures, because 2 channel textures can cause problems
|
||||
result["metallicRoughnessTexture"] = toPIL(
|
||||
np.concatenate(
|
||||
[np.zeros_like(metallic), 1.0 - glossiness, metallic], axis=-1
|
||||
),
|
||||
mode="RGB",
|
||||
)
|
||||
result["metallicFactor"] = 1.0
|
||||
result["roughnessFactor"] = 1.0
|
||||
else:
|
||||
result["metallicFactor"] = get_float(metallic)
|
||||
result["roughnessFactor"] = get_float(1.0 - glossiness)
|
||||
|
||||
return result
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
objects.py
|
||||
--------------
|
||||
|
||||
Deal with objects which hold visual properties, like
|
||||
ColorVisuals and TextureVisuals.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .color import ColorVisuals, color_to_uv
|
||||
from .material import pack
|
||||
from .texture import TextureVisuals
|
||||
|
||||
|
||||
def create_visual(**kwargs):
|
||||
"""
|
||||
Create Visuals object from keyword arguments.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
face_colors : (n, 3|4) uint8
|
||||
Face colors
|
||||
vertex_colors : (n, 3|4) uint8
|
||||
Vertex colors
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh object
|
||||
|
||||
Returns
|
||||
----------
|
||||
visuals : ColorVisuals
|
||||
Visual object created from arguments
|
||||
"""
|
||||
return ColorVisuals(**kwargs)
|
||||
|
||||
|
||||
def concatenate(visuals, *args):
|
||||
"""
|
||||
Concatenate multiple visual objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
visuals : ColorVisuals or list
|
||||
Visuals to concatenate
|
||||
*args : ColorVisuals or list
|
||||
More visuals to concatenate
|
||||
|
||||
Returns
|
||||
----------
|
||||
concat : Visuals
|
||||
If all are color
|
||||
"""
|
||||
# get a flat list of Visuals objects
|
||||
if len(args) > 0:
|
||||
visuals = np.append(visuals, args)
|
||||
else:
|
||||
visuals = np.array(visuals)
|
||||
|
||||
# if there are any texture visuals convert all to texture
|
||||
if any(v.kind == "texture" for v in visuals):
|
||||
# first collect materials and UV coordinates
|
||||
mats = []
|
||||
uvs = []
|
||||
for v in visuals:
|
||||
if v.kind == "texture":
|
||||
mats.append(v.material)
|
||||
if v.uv is None:
|
||||
# otherwise use zeros
|
||||
uvs.append(np.zeros((len(v.mesh.vertices), 2)) + 0.5)
|
||||
else:
|
||||
# if uvs are of correct shape use them
|
||||
uvs.append(v.uv)
|
||||
|
||||
else:
|
||||
# create a material and UV coordinates from vertex colors
|
||||
color_mat, color_uv = color_to_uv(vertex_colors=v.vertex_colors)
|
||||
mats.append(color_mat)
|
||||
uvs.append(color_uv)
|
||||
# pack the materials and UV coordinates into one
|
||||
new_mat, new_uv = pack(materials=mats, uvs=uvs)
|
||||
return TextureVisuals(material=new_mat, uv=new_uv)
|
||||
|
||||
# convert all visuals to the first valid kind
|
||||
kind = next((v.kind for v in visuals if v.kind is not None), None)
|
||||
if kind == "face":
|
||||
colors = np.vstack([v.face_colors for v in visuals])
|
||||
return ColorVisuals(face_colors=colors)
|
||||
elif kind == "vertex":
|
||||
colors = np.vstack([v.vertex_colors for v in visuals])
|
||||
return ColorVisuals(vertex_colors=colors)
|
||||
|
||||
return ColorVisuals()
|
||||
@@ -0,0 +1,350 @@
|
||||
import copy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import caching, grouping, util
|
||||
from . import color
|
||||
from .base import Visuals
|
||||
from .material import PBRMaterial, SimpleMaterial, empty_material # NOQA
|
||||
|
||||
|
||||
class TextureVisuals(Visuals):
|
||||
def __init__(self, uv=None, material=None, image=None, face_materials=None):
|
||||
"""
|
||||
Store a single material and per-vertex UV coordinates
|
||||
for a mesh.
|
||||
|
||||
If passed UV coordinates and a single image it will
|
||||
create a SimpleMaterial for the image.
|
||||
|
||||
Parameters
|
||||
--------------
|
||||
uv : (n, 2) float
|
||||
UV coordinates for the mesh
|
||||
material : Material
|
||||
Store images and properties
|
||||
image : PIL.Image
|
||||
Can be passed to automatically create material
|
||||
"""
|
||||
|
||||
# store values we care about enough to hash
|
||||
self.vertex_attributes = caching.DataStore()
|
||||
# cache calculated values
|
||||
self._cache = caching.Cache(self.vertex_attributes.__hash__)
|
||||
|
||||
# should be (n, 2) float
|
||||
self.uv = uv
|
||||
|
||||
if material is None:
|
||||
if image is None:
|
||||
self.material = empty_material()
|
||||
else:
|
||||
# if an image is passed create a SimpleMaterial
|
||||
self.material = SimpleMaterial(image=image)
|
||||
else:
|
||||
# if passed assign
|
||||
self.material = material
|
||||
|
||||
self.face_materials = face_materials
|
||||
|
||||
def _verify_hash(self):
|
||||
"""
|
||||
Dump the cache if anything in self.vertex_attributes
|
||||
has changed.
|
||||
"""
|
||||
self._cache.verify()
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
"""
|
||||
Return the type of visual data stored
|
||||
|
||||
Returns
|
||||
----------
|
||||
kind : str
|
||||
What type of visuals are defined
|
||||
"""
|
||||
return "texture"
|
||||
|
||||
@property
|
||||
def defined(self):
|
||||
"""
|
||||
Check if any data is stored
|
||||
|
||||
Returns
|
||||
----------
|
||||
defined : bool
|
||||
Are UV coordinates and images set?
|
||||
"""
|
||||
ok = self.material is not None
|
||||
return ok
|
||||
|
||||
def __hash__(self):
|
||||
"""
|
||||
Get a CRC of the stored data.
|
||||
|
||||
Returns
|
||||
--------------
|
||||
crc : int
|
||||
Hash of items in self.vertex_attributes
|
||||
"""
|
||||
return self.vertex_attributes.__hash__()
|
||||
|
||||
@property
|
||||
def uv(self):
|
||||
"""
|
||||
Get the stored UV coordinates.
|
||||
|
||||
Returns
|
||||
------------
|
||||
uv : (n, 2) float or None
|
||||
Pixel position per-vertex.
|
||||
"""
|
||||
return self.vertex_attributes.get("uv", None)
|
||||
|
||||
@uv.setter
|
||||
def uv(self, values):
|
||||
"""
|
||||
Set the UV coordinates.
|
||||
|
||||
Parameters
|
||||
--------------
|
||||
values : (n, 2) float or None
|
||||
Pixel locations on a texture per- vertex
|
||||
"""
|
||||
if values is None:
|
||||
self.vertex_attributes.pop("uv")
|
||||
else:
|
||||
self.vertex_attributes["uv"] = np.asanyarray(values, dtype=np.float64)
|
||||
|
||||
def copy(self, uv=None):
|
||||
"""
|
||||
Return a copy of the current TextureVisuals object.
|
||||
|
||||
Returns
|
||||
----------
|
||||
copied : TextureVisuals
|
||||
Contains the same information in a new object
|
||||
"""
|
||||
if uv is None:
|
||||
uv = self.uv
|
||||
if uv is not None:
|
||||
uv = uv.copy()
|
||||
copied = TextureVisuals(
|
||||
uv=uv,
|
||||
material=self.material.copy(),
|
||||
face_materials=copy.copy(self.face_materials),
|
||||
)
|
||||
|
||||
return copied
|
||||
|
||||
def to_color(self):
|
||||
"""
|
||||
Convert textured visuals to a ColorVisuals with vertex
|
||||
color calculated from texture.
|
||||
|
||||
Returns
|
||||
-----------
|
||||
vis : trimesh.visuals.ColorVisuals
|
||||
Contains vertex color from texture
|
||||
"""
|
||||
# find the color at each UV coordinate
|
||||
colors = self.material.to_color(self.uv)
|
||||
# create ColorVisuals from result
|
||||
vis = color.ColorVisuals(vertex_colors=colors)
|
||||
return vis
|
||||
|
||||
def face_subset(self, face_index):
|
||||
"""
|
||||
Get a copy of
|
||||
"""
|
||||
if self.uv is not None:
|
||||
indices = np.unique(self.mesh.faces[face_index].flatten())
|
||||
return self.copy(self.uv[indices])
|
||||
else:
|
||||
return self.copy()
|
||||
|
||||
def update_vertices(self, mask):
|
||||
"""
|
||||
Apply a mask to remove or duplicate vertex properties.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
mask : (len(vertices),) bool or (n,) int
|
||||
Mask which can be used like: `vertex_attribute[mask]`
|
||||
"""
|
||||
# collect updated masked values
|
||||
updates = {}
|
||||
for key, value in self.vertex_attributes.items():
|
||||
# DataStore will convert None to zero-length array
|
||||
if len(value) == 0:
|
||||
continue
|
||||
try:
|
||||
# store the update
|
||||
updates[key] = value[mask]
|
||||
except BaseException:
|
||||
# usual reason is an incorrect size or index
|
||||
util.log.warning(f"failed to update visual: `{key}`")
|
||||
# clear all values from the vertex attributes
|
||||
self.vertex_attributes.clear()
|
||||
# apply the updated values
|
||||
self.vertex_attributes.update(updates)
|
||||
|
||||
def update_faces(self, mask):
|
||||
"""
|
||||
Apply a mask to remove or duplicate face properties,
|
||||
not applicable to texture visuals.
|
||||
"""
|
||||
|
||||
def concatenate(self, others):
|
||||
"""
|
||||
Concatenate this TextureVisuals object with others
|
||||
and return the result without modifying this visual.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
others : (n,) Visuals
|
||||
Other visual objects to concatenate
|
||||
|
||||
Returns
|
||||
-----------
|
||||
concatenated : TextureVisuals
|
||||
Concatenated visual objects
|
||||
"""
|
||||
from .objects import concatenate
|
||||
|
||||
return concatenate(self, others)
|
||||
|
||||
|
||||
def unmerge_faces(faces, *args, **kwargs):
|
||||
"""
|
||||
Textured meshes can come with faces referencing vertex
|
||||
indices (`v`) and an array the same shape which references
|
||||
vertex texture indices (`vt`) and sometimes even normal (`vn`).
|
||||
|
||||
Vertex locations with different values of any of these can't
|
||||
be considered the "same" vertex, and for our simple data
|
||||
model we need to not combine these vertices.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
faces : (n, d) int
|
||||
References vertex indices
|
||||
*args : (n, d) int
|
||||
Various references of corresponding values
|
||||
This is usually UV coordinates or normal indexes
|
||||
maintain_faces : bool
|
||||
Do not alter original faces and return no-op masks.
|
||||
|
||||
Returns
|
||||
-------------
|
||||
new_faces : (m, d) int
|
||||
New faces for masked vertices
|
||||
mask_v : (p,) int
|
||||
A mask to apply to vertices
|
||||
mask_* : (p,) int
|
||||
A mask to apply to vt array to get matching UV coordinates
|
||||
Returns as many of these as args were passed
|
||||
"""
|
||||
# unfortunately Python2 doesn't let us put named kwargs
|
||||
# after an `*args` sequence so we have to do this ugly get
|
||||
maintain_faces = kwargs.get("maintain_faces", False)
|
||||
|
||||
# don't alter faces
|
||||
if maintain_faces:
|
||||
# start with not altering faces at all
|
||||
result = [faces]
|
||||
# find the maximum index referenced by faces
|
||||
max_idx = faces.max()
|
||||
# add a vertex mask which is just ordered
|
||||
result.append(np.arange(max_idx + 1))
|
||||
|
||||
# now given the order is fixed do our best on the rest of the order
|
||||
for arg in args:
|
||||
# create a mask of the attribute-vertex mapping
|
||||
# note that these might conflict since we're not unmerging
|
||||
masks = np.full((3, max_idx + 1), -1, dtype=np.int64)
|
||||
# set the mask using the unmodified face indexes
|
||||
for i, f, a in zip(range(3), faces.T, arg.T):
|
||||
masks[i][f] = a
|
||||
# find the most commonly occurring attribute (i.e. UV coordinate)
|
||||
# and use that index note that this is doing a float conversion
|
||||
# and then median before converting back to int: could also do this as
|
||||
# a column diff and sort but this seemed easier and is fast enough
|
||||
# turn default attribute value of -1 to nan before median computation
|
||||
# and use nanmedian to compute the median ignoring the nan values
|
||||
masks_nan = np.where(masks != -1, masks, np.nan)
|
||||
result.append(np.nanmedian(masks_nan, axis=0).astype(np.int64))
|
||||
|
||||
return result
|
||||
|
||||
# stack into pairs of (vertex index, texture index)
|
||||
stackable = [np.asanyarray(faces).reshape(-1)]
|
||||
# append multiple args to the correlated stack
|
||||
# this is usually UV coordinates (vt) and normals (vn)
|
||||
for arg in args:
|
||||
stackable.append(np.asanyarray(arg).reshape(-1))
|
||||
|
||||
# unify them into rows of a numpy array
|
||||
stack = np.column_stack(stackable)
|
||||
# find unique pairs: we're trying to avoid merging
|
||||
# vertices that have the same position but different
|
||||
# texture coordinates
|
||||
unique, inverse = grouping.unique_rows(stack)
|
||||
|
||||
# only take the unique pairs
|
||||
pairs = stack[unique]
|
||||
# try to maintain original vertex order
|
||||
order = pairs[:, 0].argsort()
|
||||
# apply the order to the pairs
|
||||
pairs = pairs[order]
|
||||
|
||||
# we re-ordered the vertices to try to maintain
|
||||
# the original vertex order as much as possible
|
||||
# so to reconstruct the faces we need to remap
|
||||
remap = np.zeros(len(order), dtype=np.int64)
|
||||
remap[order] = np.arange(len(order))
|
||||
|
||||
# the faces are just the inverse with the new order
|
||||
new_faces = remap[inverse].reshape((-1, faces.shape[1]))
|
||||
|
||||
# the mask for vertices and masks for other args
|
||||
result = [new_faces]
|
||||
result.extend(pairs.T)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def power_resize(image, resample=1, square=False):
|
||||
"""
|
||||
Resize a PIL image so every dimension is a power of two.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
image : PIL.Image
|
||||
Input image
|
||||
resample : int
|
||||
Passed to Image.resize
|
||||
square : bool
|
||||
If True, upsize to a square image
|
||||
|
||||
Returns
|
||||
-------------
|
||||
resized : PIL.Image
|
||||
Input image resized
|
||||
"""
|
||||
# what is the current resolution of the image in pixels
|
||||
size = np.array(image.size, dtype=np.int64)
|
||||
# what is the resolution of the image upsized to the nearest
|
||||
# power of two on each axis: allow rectangular textures
|
||||
new_size = (2 ** np.ceil(np.log2(size))).astype(np.int64)
|
||||
|
||||
# make every dimension the largest
|
||||
if square:
|
||||
new_size = np.ones(2, dtype=np.int64) * new_size.max()
|
||||
|
||||
# if we're not powers of two upsize
|
||||
if (size != new_size).any():
|
||||
return image.resize(tuple(new_size), resample=resample)
|
||||
|
||||
return image.copy()
|
||||
Reference in New Issue
Block a user