import numpy as np import copy from .base import Visuals from . import color from .. import util from .. import caching from .. import grouping from .material import SimpleMaterial, PBRMaterial, empty_material # NOQA class TextureVisuals(Visuals): def __init__(self, uv=None, material=None, image=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._data = caching.DataStore() # cache calculated values self._cache = caching.Cache(self._data.fast_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 def _verify_crc(self): """ Dump the cache if anything in self._data 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 crc(self): """ Get a CRC of the stored data. Returns -------------- crc : int Hash of items in self._data """ return self._data.crc() @property def uv(self): """ Get the stored UV coordinates. Returns ------------ uv : (n, 2) float Pixel position per- vertex """ if 'uv' in self._data: return self._data['uv'] return None @uv.setter def uv(self, values): """ Set the UV coordinates. Parameters -------------- values : (n, 2) float Pixel locations on a texture per- vertex """ if values is None: self._data.clear() else: self._data['uv'] = np.asanyarray( values, dtype=np.float64) def copy(self): """ Return a copy of the current TextureVisuals object. Returns ---------- copied : TextureVisuals Contains the same information in a new object """ uv = self.uv if uv is not None: uv = uv.copy() copied = TextureVisuals( uv=uv, material=copy.deepcopy(self.material)) 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 """ return self.copy() def update_vertices(self, mask): """ Apply a mask to remove or duplicate vertex properties. """ if self.uv is not None: self.uv = self.uv[mask] def update_faces(self, mask): """ Apply a mask to remove or duplicate face properties """ pass 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 """ util.log.warning('concatenating texture: may result in visual artifacts') from .objects import concatenate return concatenate(self, others) def unmerge_faces(faces, *args): """ 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 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 """ # 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, 3)) # 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(new_size, resample=resample) return image.copy()