[docs]classScene(Geometry3D):""" A simple scene graph which can be rendered directly via pyglet/openGL or through other endpoints such as a raytracer. Meshes are added by name, which can then be moved by updating transform in the transform tree. """
[docs]def__init__(self,geometry=None,base_frame='world',metadata=None,graph=None,camera=None,lights=None,camera_transform=None):""" Create a new Scene object. Parameters ------------- geometry : Trimesh, Path2D, Path3D PointCloud or list Geometry to initially add to the scene base_frame : str or hashable Name of base frame metadata : dict Any metadata about the scene graph : TransformForest or None A passed transform graph to use camera : Camera or None A passed camera to use lights : [trimesh.scene.lighting.Light] or None A passed lights to use camera_transform : (4, 4) float or None Camera transform in the base frame """# mesh name : Trimesh objectself.geometry=collections.OrderedDict()# create a new graphself.graph=SceneGraph(base_frame=base_frame)# create our cacheself._cache=caching.Cache(id_function=self.__hash__)# add passed geometry to sceneself.add_geometry(geometry)# hold metadata about the sceneself.metadata={}ifisinstance(metadata,dict):self.metadata.update(metadata)ifgraphisnotNone:# if we've been passed a graph override the defaultself.graph=graphself.camera=cameraself.lights=lightsifcameraisnotNoneandcamera_transformisnotNone:self.camera_transform=camera_transform
[docs]defapply_transform(self,transform):""" Apply a transform to all children of the base frame without modifying any geometry. Parameters -------------- transform : (4, 4) Homogeneous transformation matrix. """base=self.graph.base_frameforchildinself.graph.transforms.children[base]:combined=np.dot(transform,self.graph[child][0])self.graph.update(frame_from=base,frame_to=child,matrix=combined)returnself
[docs]defadd_geometry(self,geometry,node_name=None,geom_name=None,parent_node_name=None,transform=None,extras=None):""" Add a geometry to the scene. If the mesh has multiple transforms defined in its metadata, they will all be copied into the TransformForest of the current scene automatically. Parameters ---------- geometry : Trimesh, Path2D, Path3D PointCloud or list Geometry to initially add to the scene node_name: Name of the added node. geom_name: Name of the added geometry. parent_node_name: Name of the parent node in the graph. transform: Transform that applies to the added node. extras: Optional metadata for the node. Returns ---------- node_name : str Name of single node in self.graph (passed in) or None if node was not added (eg. geometry was null or a Scene). """ifgeometryisNone:return# PointCloud objects will look like a sequenceelifutil.is_sequence(geometry):# if passed a sequence add all elementsreturn[self.add_geometry(geometry=value,node_name=node_name,geom_name=geom_name,parent_node_name=parent_node_name,transform=transform,extras=extras)forvalueingeometry]elifisinstance(geometry,dict):# if someone passed us a dict of geometryreturn{k:self.add_geometry(v,geom_name=k,extras=extras)fork,vingeometry.items()}elifisinstance(geometry,Scene):# concatenate current scene with passed sceneconcat=self+geometry# replace geometry in-placeself.geometry.clear()self.geometry.update(concat.geometry)# replace graph data with concatenated graphself.graph.transforms=concat.graph.transformsreturnifnothasattr(geometry,'vertices'):util.log.debug('unknown type ({}) added to scene!'.format(type(geometry).__name__))return# get or create a name to reference the geometry byifgeom_nameisnotNone:# if name is passed use itname=geom_nameelif'name'ingeometry.metadata:# if name is in metadata use itname=geometry.metadata['name']elif'file_name'ingeometry.metadata:name=geometry.metadata['file_name']else:# try to create a simple namename='geometry_'+str(len(self.geometry))# if its already taken use our unique name logicname=unique_name(start=name,contains=self.geometry.keys())# save the geometry referenceself.geometry[name]=geometry# create a unique node name if not passedifnode_nameisNone:# if the name of the geometry is also a transform node# which graph nodes already existexisting=self.graph.transforms.node_data.keys()# find a name that isn't contained already starting# at the name we havenode_name=unique_name(name,existing)assertnode_namenotinexistingiftransformisNone:# create an identity transform from parent_nodetransform=np.eye(4)self.graph.update(frame_to=node_name,frame_from=parent_node_name,matrix=transform,geometry=name,geometry_flags={'visible':True},extras=extras)returnnode_name
[docs]defdelete_geometry(self,names):""" Delete one more multiple geometries from the scene and also remove any node in the transform graph which references it. Parameters -------------- name : hashable Name that references self.geometry """# make sure we have a set we can checkifutil.is_string(names):names=[names]names=set(names)# remove the geometry reference from relevant nodesself.graph.remove_geometries(names)# remove the geometries from our geometry store[self.geometry.pop(name,None)fornameinnames]
[docs]defstrip_visuals(self):""" Strip visuals from every Trimesh geometry and set them to an empty `ColorVisuals`. """from..visual.colorimportColorVisualsforgeometryinself.geometry.values():ifutil.is_instance_named(geometry,'Trimesh'):geometry.visual=ColorVisuals(mesh=geometry)
[docs]def__hash__(self):""" Return information about scene which is hashable. Returns --------- hashable : str Data which can be hashed. """# avoid accessing attribute in tight loopgeometry=self.geometry# start with the last modified time of the scene graphhashable=[hex(self.graph.transforms.__hash__())]# take the re-hex string of the hashhashable.extend(hex(geometry[k].__hash__())forkingeometry.keys())returncaching.hash_fast(''.join(hashable).encode('utf-8'))
@propertydefis_empty(self):""" Does the scene have anything in it. Returns ---------- is_empty: bool, True if nothing is in the scene """is_empty=len(self.geometry)==0returnis_empty@propertydefis_valid(self):""" Is every geometry connected to the root node. Returns ----------- is_valid : bool Does every geometry have a transform """iflen(self.geometry)==0:returnTruetry:referenced={self.graph[i][1]foriinself.graph.nodes_geometry}exceptBaseException:# if connectivity to world frame is broken return falsereturnFalse# every geometry is referencedok=referenced==set(self.geometry.keys())returnok@caching.cache_decoratordefbounds_corners(self):""" Get the post-transform AABB for each node which has geometry defined. Returns ----------- corners : dict Bounds for each node with vertices: {node_name : (2, 3) float} """# collect AABB for each geometrycorners={}# collect vertices for every meshvertices={k:m.verticesfork,minself.geometry.items()ifhasattr(m,'vertices')andlen(m.vertices)>0}# handle 2D geometriesvertices.update({k:np.column_stack((v,np.zeros(len(v))))fork,vinvertices.items()ifv.shape[1]==2})# loop through every node with geometryfornode_nameinself.graph.nodes_geometry:# access the transform and geometry name from nodetransform,geometry_name=self.graph[node_name]# will be None if no vertices for this nodepoints=vertices.get(geometry_name)# skip empty geometriesifpointsisNone:continue# apply just the rotation to skip N multipliesdot=np.dot(transform[:3,:3],points.T)# append the AABB with translation applied aftercorners[node_name]=np.array([dot.min(axis=1)+transform[:3,3],dot.max(axis=1)+transform[:3,3]])returncorners@caching.cache_decoratordefbounds(self):""" Return the overall bounding box of the scene. Returns -------- bounds : (2, 3) float or None Position of [min, max] bounding box Returns None if no valid bounds exist """bounds_corners=self.bounds_cornersiflen(bounds_corners)==0:returnNone# combine each geometry node AABB into a larger listcorners=np.vstack(list(self.bounds_corners.values()))returnnp.array([corners.min(axis=0),corners.max(axis=0)],dtype=np.float64)@caching.cache_decoratordefextents(self):""" Return the axis aligned box size of the current scene. Returns ---------- extents : (3,) float Bounding box sides length """returnnp.diff(self.bounds,axis=0).reshape(-1)@caching.cache_decoratordefscale(self):""" The approximate scale of the mesh Returns ----------- scale : float The mean of the bounding box edge lengths """scale=(self.extents**2).sum()**.5returnscale@caching.cache_decoratordefcentroid(self):""" Return the center of the bounding box for the scene. Returns -------- centroid : (3) float Point for center of bounding box """centroid=np.mean(self.bounds,axis=0)returncentroid@caching.cache_decoratordefcenter_mass(self):""" Find the center of mass for every instance in the scene. Returns ------------ center_mass : (3,) float The center of mass of the scene """# get the center of mass and volume for each geometrycenter_mass={k:m.center_massfork,minself.geometry.items()ifhasattr(m,'center_mass')}mass={k:m.massfork,minself.geometry.items()ifhasattr(m,'mass')}# get the geometry name and transform for each instancegraph=self.graphinstance=[graph[n]forningraph.nodes_geometry]# get the transformed center of mass for each instancetransformed=np.array([np.dot(mat,np.append(center_mass[g],1))[:3]format,gininstanceifgincenter_mass],dtype=np.float64)# weight the center of mass locations by volumeweights=np.array([mass[g]for_,gininstance],dtype=np.float64)weights/=weights.sum()return(transformed*weights.reshape((-1,1))).sum(axis=0)@caching.cache_decoratordefmoment_inertia(self):""" Return the moment of inertia of the current scene with respect to the center of mass of the current scene. Returns ------------ inertia : (3, 3) float Inertia with respect to cartesian axis at `scene.center_mass` """returninertia.scene_inertia(scene=self,transform=transformations.translation_matrix(self.center_mass))
[docs]defmoment_inertia_frame(self,transform):""" Return the moment of inertia of the current scene relative to a transform from the base frame. Parameters transform : (4, 4) float Homogenous transformation matrix. Returns ------------- inertia : (3, 3) float Inertia tensor at requested frame. """returninertia.scene_inertia(scene=self,transform=transform)
@caching.cache_decoratordefarea(self):""" What is the summed area of every geometry which has area. Returns ------------ area : float Summed area of every instanced geometry """# get the area of every geometry that has an area propertyareas={n:g.areaforn,ginself.geometry.items()ifhasattr(g,'area')}# sum the area including instancingreturnsum((areas.get(self.graph[n][1],0.0)forninself.graph.nodes_geometry),0.0)@caching.cache_decoratordefvolume(self):""" What is the summed volume of every geometry which has volume Returns ------------ volume : float Summed area of every instanced geometry """# get the area of every geometry that has a volume attributevolume={n:g.volumeforn,ginself.geometry.items()ifhasattr(g,'area')}# sum the area including instancingreturnsum((volume.get(self.graph[n][1],0.0)forninself.graph.nodes_geometry),0.0)@caching.cache_decoratordeftriangles(self):""" Return a correctly transformed polygon soup of the current scene. Returns ---------- triangles : (n, 3, 3) float Triangles in space """triangles=collections.deque()triangles_node=collections.deque()fornode_nameinself.graph.nodes_geometry:# which geometry does this node refer totransform,geometry_name=self.graph[node_name]# get the actual potential mesh instancegeometry=self.geometry[geometry_name]ifnothasattr(geometry,'triangles'):continue# append the (n, 3, 3) triangles to a sequencetriangles.append(transformations.transform_points(geometry.triangles.copy().reshape((-1,3)),matrix=transform))# save the node names for each triangletriangles_node.append(np.tile(node_name,len(geometry.triangles)))# save the resulting nodes to the cacheself._cache['triangles_node']=np.hstack(triangles_node)triangles=np.vstack(triangles).reshape((-1,3,3))returntriangles@caching.cache_decoratordeftriangles_node(self):""" Which node of self.graph does each triangle come from. Returns --------- triangles_index : (len(self.triangles),) Node name for each triangle """populate=self.triangles# NOQAreturnself._cache['triangles_node']@caching.cache_decoratordefgeometry_identifiers(self):""" Look up geometries by identifier hash Returns --------- identifiers : dict {Identifier hash: key in self.geometry} """identifiers={mesh.identifier_hash:nameforname,meshinself.geometry.items()}returnidentifiers@caching.cache_decoratordefduplicate_nodes(self):""" Return a sequence of node keys of identical meshes. Will include meshes with different geometry but identical spatial hashes as well as meshes repeated by self.nodes. Returns ----------- duplicates : (m) sequence Keys of self.graph that represent identical geometry """# if there is no geometry we can have no duplicate nodesiflen(self.geometry)==0:return[]# geometry name : hash of meshhashes={k:int(m.identifier_hash,16)fork,minself.geometry.items()ifhasattr(m,'identifier_hash')}# bring into local scope for loopgraph=self.graph# get a hash for each node name# scene.graph node name : hashed geometrynode_hash={node:hashes.get(graph[node][1])fornodeingraph.nodes_geometry}# collect node names for each hash keyduplicates=collections.defaultdict(list)# use a slightly off-label list comprehension# for debatable function call overhead avoidance[duplicates[hashed].append(node)fornode,hashedinnode_hash.items()ifhashedisnotNone]# we only care about the values keys are garbagereturnlist(duplicates.values())
[docs]defdeduplicated(self):""" Return a new scene where each unique geometry is only included once and transforms are discarded. Returns ------------- dedupe : Scene One copy of each unique geometry from scene """# collect geometrygeometry={}# loop through groups of identical nodesforgroupinself.duplicate_nodes:# get the name of the geometryname=self.graph[group[0]][1]# collect our unique collection of geometrygeometry[name]=self.geometry[name]returnScene(geometry)
[docs]defset_camera(self,angles=None,distance=None,center=None,resolution=None,fov=None):""" Create a camera object for self.camera, and add a transform to self.graph for it. If arguments are not passed sane defaults will be figured out which show the mesh roughly centered. Parameters ----------- angles : (3,) float Initial euler angles in radians distance : float Distance from centroid center : (3,) float Point camera should be center on camera : Camera object Object that stores camera parameters """iffovisNone:fov=np.array([60,45])# if no geometry nothing to set camera toiflen(self.geometry)==0:self._camera=cameras.Camera(fov=fov)self.graph[self._camera.name]=np.eye(4)returnself._camera# set with no rotation by defaultifanglesisNone:angles=np.zeros(3)rotation=transformations.euler_matrix(*angles)transform=cameras.look_at(self.bounds,fov=fov,rotation=rotation,distance=distance,center=center)ifhasattr(self,'_camera')andself._cameraisnotNone:self._camera.fov=fovifresolutionisnotNone:self._camera.resolution=resolutionelse:# create a new camera objectself._camera=cameras.Camera(fov=fov,resolution=resolution)self.graph[self._camera.name]=transformreturnself._camera
@propertydefcamera_transform(self):""" Get camera transform in the base frame. Returns ------- camera_transform : (4, 4) float Camera transform in the base frame """returnself.graph[self.camera.name][0]@camera_transform.setterdefcamera_transform(self,matrix):""" Set the camera transform in the base frame Parameters ---------- camera_transform : (4, 4) float Camera transform in the base frame """self.graph[self.camera.name]=matrix
[docs]defcamera_rays(self):""" Calculate the trimesh.scene.Camera origin and ray direction vectors. Returns one ray per pixel as set in camera.resolution Returns -------------- origin: (n, 3) float Ray origins in space vectors: (n, 3) float Ray direction unit vectors in world coordinates pixels : (n, 2) int Which pixel does each ray correspond to in an image """# get the unit vectors of the cameravectors,pixels=self.camera.to_rays()# find our scene's transform for the cameratransform=self.camera_transform# apply the rotation to the unit ray direction vectorsvectors=transformations.transform_points(vectors,transform,translate=False)# camera origin is single point so extract fromorigins=(np.ones_like(vectors)*transformations.translation_from_matrix(transform))returnorigins,vectors,pixels
@propertydefcamera(self):""" Get the single camera for the scene. If not manually set one will abe automatically generated. Returns ---------- camera : trimesh.scene.Camera Camera object defined for the scene """# no camera set for the scene yetifnotself.has_camera:# will create a camera with everything in viewreturnself.set_camera()assertself._cameraisnotNonereturnself._camera@camera.setterdefcamera(self,camera):""" Set a camera object for the Scene. Parameters ----------- camera : trimesh.scene.Camera Camera object for the scene """ifcameraisNone:returnself._camera=camera@propertydefhas_camera(self):returnhasattr(self,'_camera')andself._cameraisnotNone@propertydeflights(self):""" Get a list of the lights in the scene. If nothing is set it will generate some automatically. Returns ------------- lights : [trimesh.scene.lighting.Light] Lights in the scene. """ifnothasattr(self,'_lights')orself._lightsisNone:# do some automatic lightinglights,transforms=lighting.autolight(self)# assign the transforms to the scene graphforL,Tinzip(lights,transforms):self.graph[L.name]=T# set the lightsself._lights=lightsreturnself._lights@lights.setterdeflights(self,lights):""" Assign a list of light objects to the scene Parameters -------------- lights : [trimesh.scene.lighting.Light] Lights in the scene. """self._lights=lights
[docs]defrezero(self):""" Move the current scene so that the AABB of the whole scene is centered at the origin. Does this by changing the base frame to a new, offset base frame. """ifself.is_emptyornp.allclose(self.centroid,0.0):# early exit since what we want already existsreturn# the transformation to move the overall scene to AABB centroidmatrix=np.eye(4)matrix[:3,3]=-self.centroid# we are going to change the base framenew_base=str(self.graph.base_frame)+'_I'self.graph.update(frame_from=new_base,frame_to=self.graph.base_frame,matrix=matrix)self.graph.base_frame=new_base
[docs]defdump(self,concatenate=False):""" Append all meshes in scene freezing transforms. Parameters ------------ concatenate : bool If True, concatenate results into single mesh Returns ---------- dumped : (n,) Trimesh or Trimesh Trimesh objects transformed to their location the scene.graph """result=[]fornode_nameinself.graph.nodes_geometry:transform,geometry_name=self.graph[node_name]# get a copy of the geometrycurrent=self.geometry[geometry_name].copy()# move the geometry vertices into the requested framecurrent.apply_transform(transform)current.metadata['name']=geometry_namecurrent.metadata['node']=node_name# save to our list of meshesresult.append(current)ifconcatenate:returnutil.concatenate(result)returnnp.array(result)
[docs]defsubscene(self,node):""" Get part of a scene that succeeds a specified node. Parameters ------------ node : any Hashable key in `scene.graph` Returns ----------- subscene : Scene Partial scene generated from current. """# get every node that is a successor to specified node# this includes `node`graph=self.graphnodes=graph.transforms.successors(node)# get every edge that has an included nodeedges=[eforeingraph.to_edgelist()ife[0]innodes]# create a scene graph whengraph=SceneGraph(base_frame=node)graph.from_edgelist(edges)geometry_names=set([e[2]['geometry']foreinedgesif'geometry'ine[2]])geometry={k:self.geometry[k]forkingeometry_names}result=Scene(geometry=geometry,graph=graph)returnresult
@caching.cache_decoratordefconvex_hull(self):""" The convex hull of the whole scene Returns --------- hull: Trimesh object, convex hull of all meshes in scene """points=util.vstack_empty([m.verticesforminself.dump()])hull=convex.convex_hull(points)returnhull
[docs]defexport(self,file_obj=None,file_type=None,**kwargs):""" Export a snapshot of the current scene. Parameters ---------- file_obj : str, file-like, or None File object to export to file_type : str or None What encoding to use for meshes IE: dict, dict64, stl Returns ---------- export : bytes Only returned if file_obj is None """returnexport.export_scene(scene=self,file_obj=file_obj,file_type=file_type,**kwargs)
[docs]defsave_image(self,resolution=None,**kwargs):""" Get a PNG image of a scene. Parameters ----------- resolution : (2,) int Resolution to render image **kwargs Passed to SceneViewer constructor Returns ----------- png : bytes Render of scene as a PNG """from..viewer.windowedimportrender_scenepng=render_scene(scene=self,resolution=resolution,**kwargs)returnpng
@propertydefunits(self):""" Get the units for every model in the scene, and raise a ValueError if there are mixed units. Returns ----------- units : str Units for every model in the scene """existing=[i.unitsforiinself.geometry.values()]ifany(existing[0]!=eforeinexisting):# if all of our geometry doesn't have the same units already# this function will only do some hot nonsenseraiseValueError('models in scene have inconsistent units!')returnexisting[0]@units.setterdefunits(self,value):""" Set the units for every model in the scene without converting any units just setting the tag. Parameters ------------ value : str Value to set every geometry unit value to """forminself.geometry.values():m.units=value
[docs]defconvert_units(self,desired,guess=False):""" If geometry has units defined convert them to new units. Returns a new scene with geometries and transforms scaled. Parameters ---------- desired : str Desired final unit system: 'inches', 'mm', etc. guess : bool Is the converter allowed to guess scale when models don't have it specified in their metadata. Returns ---------- scaled : trimesh.Scene Copy of scene with scaling applied and units set for every model """# if there is no geometry do nothingiflen(self.geometry)==0:returnself.copy()current=self.unitsifcurrentisNone:# will raise ValueError if not in metadata# and not allowed to guesscurrent=units.units_from_metadata(self,guess=guess)# find the float conversionscale=units.unit_conversion(current=current,desired=desired)# exit early if our current units are the same as desired unitsifnp.isclose(scale,1.0):result=self.copy()else:result=self.scaled(scale=scale)# apply the units to every geometry of the scaled resultresult.units=desiredreturnresult
[docs]defexplode(self,vector=None,origin=None):""" Explode a scene around a point and vector. Parameters ----------- vector : (3,) float or float Explode radially around a direction vector or spherically origin : (3,) float Point to explode around """iforiginisNone:origin=self.centroidifvectorisNone:vector=self.scale/25.0vector=np.asanyarray(vector,dtype=np.float64)origin=np.asanyarray(origin,dtype=np.float64)fornode_nameinself.graph.nodes_geometry:transform,geometry_name=self.graph[node_name]centroid=self.geometry[geometry_name].centroid# transform centroid into nodes locationcentroid=np.dot(transform,np.append(centroid,1))[:3]ifvector.shape==():# case where our vector is a single numberoffset=(centroid-origin)*vectorelifnp.shape(vector)==(3,):projected=np.dot(vector,(centroid-origin))offset=vector*projectedelse:raiseValueError('explode vector wrong shape!')# original transform is read-onlyT_new=transform.copy()T_new[:3,3]+=offsetself.graph[node_name]=T_new
[docs]defscaled(self,scale):""" Return a copy of the current scene, with meshes and scene transforms scaled to the requested factor. Parameters ----------- scale : float or (3,) float Factor to scale meshes and transforms Returns ----------- scaled : trimesh.Scene A copy of the current scene but scaled """# convert 2D geometries to 3D for 3D scaling factorsscale_is_3D=isinstance(scale,(list,tuple,np.ndarray))andlen(scale)==3ifscale_is_3Dandnp.all(np.asarray(scale)==scale[0]):# scale is uniformscale=float(scale[0])scale_is_3D=Falseelifnotscale_is_3D:scale=float(scale)# result is a copyresult=self.copy()ifscale_is_3D:# Copy all geometries that appear multiple times in the scene,# such that no two nodes share the same geometry.# This is required since the non-uniform scaling will most likely# affect the same geometry in different poses differently.# Note, that this is not needed in the case of uniform scaling.forgeom_nameinresult.graph.geometry_nodes:nodes_with_geom=result.graph.geometry_nodes[geom_name]iflen(nodes_with_geom)>1:geom=result.geometry[geom_name]forninnodes_with_geom:p=result.graph.transforms.parents[n]result.add_geometry(geometry=geom.copy(),geom_name=geom_name,node_name=n,parent_node_name=p,transform=result.graph.transforms.edge_data[(p,n)].get('matrix',None),extras=result.graph.transforms.edge_data[(p,n)].get('extras',None))result.delete_geometry(geom_name)# Convert all 2D paths to 3D pathsforgeom_nameinresult.geometry:ifresult.geometry[geom_name].vertices.shape[1]==2:result.geometry[geom_name]=result.geometry[geom_name].to_3D()# Scale all geometries by un-doing their local rotations firstforkeyinresult.graph.nodes_geometry:T,geom_name=result.graph.get(key)# transform from graph should be read-onlyT=T.copy()T[:3,3]=0.0# Get geometry transform w.r.t. base frameresult.geometry[geom_name].apply_transform(T).apply_scale(scale).apply_transform(np.linalg.inv(T))# Scale all transformations in the scene graphedge_data=result.graph.transforms.edge_dataforuvinedge_data:if'matrix'inedge_data[uv]:props=edge_data[uv]T=edge_data[uv]['matrix'].copy()T[:3,3]*=scaleprops['matrix']=Tresult.graph.update(frame_from=uv[0],frame_to=uv[1],**props)# Clear cacheresult.graph.transforms._cache={}result.graph.transforms._modified=str(uuid.uuid4())result.graph._cache.clear()else:# matrix for 2D scalingscale_2D=np.eye(3)*scale# matrix for 3D scalingscale_3D=np.eye(4)*scale# preallocate transforms and geometriesnodes=np.array(self.graph.nodes_geometry)transforms=np.zeros((len(nodes),4,4))geometries=[None]*len(nodes)# collect list of transformsfori,nodeinenumerate(nodes):transforms[i],geometries[i]=self.graph[node]# remove all existing transformsresult.graph.clear()forgroupingrouping.group(geometries):# hashable reference to self.geometrygeometry=geometries[group[0]]# original transform from world to geometryoriginal=transforms[group[0]]# transform for geometrynew_geom=np.dot(scale_3D,original)ifresult.geometry[geometry].vertices.shape[1]==2:# if our scene is 2D only scale in 2Dresult.geometry[geometry].apply_transform(scale_2D)else:# otherwise apply the full transformresult.geometry[geometry].apply_transform(new_geom)fornode,Tinzip(nodes[group],transforms[group]):# generate the new transformstransform=util.multi_dot([scale_3D,T,np.linalg.inv(new_geom)])# apply scale to translationtransform[:3,3]*=scale# update scene with new transformsresult.graph.update(frame_to=node,matrix=transform,geometry=geometry)returnresult
[docs]defcopy(self):""" Return a deep copy of the current scene Returns ---------- copied : trimesh.Scene Copy of the current scene """# use the geometries copy method to# allow them to handle references to unpickle-able objectsgeometry={n:g.copy()forn,ginself.geometry.items()}ifnothasattr(self,'_camera')orself._cameraisNone:# if no camera set don't include itcamera=Noneelse:# otherwise get a copy of the cameracamera=self.camera.copy()# create a new scene with copied geometry and graphcopied=Scene(geometry=geometry,graph=self.graph.copy(),metadata=self.metadata.copy(),camera=camera)returncopied
[docs]defshow(self,viewer=None,**kwargs):""" Display the current scene. Parameters ----------- viewer: str What kind of viewer to open, including 'gl' to open a pyglet window, 'notebook' for a jupyter notebook or None kwargs : dict Includes `smooth`, which will turn on or off automatic smooth shading """ifviewerisNone:# check to see if we are in a notebook or notfrom..viewerimportin_notebookviewer='gl'ifin_notebook():viewer='notebook'ifviewer=='gl':# this imports pyglet, and will raise an ImportError# if pyglet is not availablefrom..viewerimportSceneViewerreturnSceneViewer(self,**kwargs)elifviewer=='notebook':from..viewerimportscene_to_notebookreturnscene_to_notebook(self,**kwargs)else:raiseValueError('viewer must be "gl", "notebook", or None')
[docs]def__add__(self,other):""" Concatenate the current scene with another scene or mesh. Parameters ------------ other : trimesh.Scene, trimesh.Trimesh, trimesh.Path Other object to append into the result scene Returns ------------ appended : trimesh.Scene Scene with geometry from both scenes """result=append_scenes([self,other],common=[self.graph.base_frame])returnresult
defsplit_scene(geometry,**kwargs):""" Given a geometry, list of geometries, or a Scene return them as a single Scene object. Parameters ---------- geometry : splittable Returns --------- scene: trimesh.Scene """# already a scene, so return itifutil.is_instance_named(geometry,'Scene'):returngeometry# a list of thingsifutil.is_sequence(geometry):metadata={}forgingeometry:try:metadata.update(g.metadata)exceptBaseException:continuereturnScene(geometry,metadata=metadata)# a single geometry so we are going to splitsplit=[]metadata={}forginutil.make_sequence(geometry):split.extend(g.split(**kwargs))metadata.update(g.metadata)# if there is only one geometry in the mesh# name it from the file nameiflen(split)==1and'file_name'inmetadata:split={metadata['file_name']:split[0]}scene=Scene(split,metadata=metadata)returnscenedefappend_scenes(iterable,common=None,base_frame='world'):""" Concatenate multiple scene objects into one scene. Parameters ------------- iterable : (n,) Trimesh or Scene Geometries that should be appended common : (n,) str Nodes that shouldn't be remapped base_frame : str Base frame of the resulting scene Returns ------------ result : trimesh.Scene Scene containing all geometry """ifisinstance(iterable,Scene):returniterableifcommonisNone:common=[base_frame]# save geometry in dictgeometry={}# save transforms as edge tuplesedges=[]# nodes which shouldn't be remappedcommon=set(common)# nodes which are consumed and need to be remappedconsumed=set()defnode_remap(node):""" Remap node to new name if necessary Parameters ------------- node : hashable Node name in original scene Returns ------------- name : hashable Node name in concatenated scene """# if we've already remapped a node use itifnodeinmap_node:returnmap_node[node]# if a node is consumed and isn't one of the nodes# we're going to hold common between scenes remap itifnodenotincommonandnodeinconsumed:# generate a name not in consumedname=node+util.unique_id()map_node[node]=namenode=name# keep track of which nodes have been used# in the current scenecurrent.add(node)returnnode# loop through every geometryforsiniterable:# allow Trimesh/Path2D geometry to be passedifhasattr(s,'scene'):s=s.scene()# if we don't have a scene raise an exceptionifnotisinstance(s,Scene):raiseValueError('{} is not a scene!'.format(type(s).__name__))# remap geometries if they have been consumedmap_geom={}fork,vins.geometry.items():# if a geometry already exists add a UUID to the namename=unique_name(start=k,contains=geometry.keys())# store name mappingmap_geom[k]=name# store geometry with new namegeometry[name]=v# remap nodes and edges so duplicates won't# stomp all over each othermap_node={}# the nodes used in this scenecurrent=set()fora,b,attrins.graph.to_edgelist():# remap node names from local namesa,b=node_remap(a),node_remap(b)# remap geometry keys# if key is not in map_geom it means one of the scenes# referred to geometry that doesn't exist# rather than crash here we ignore it as the user# possibly intended to add in geometries back laterif'geometry'inattrandattr['geometry']inmap_geom:attr['geometry']=map_geom[attr['geometry']]# save the new edgeedges.append((a,b,attr))# mark nodes from current scene as consumedconsumed.update(current)# add all data to a new sceneresult=Scene(base_frame=base_frame)result.graph.from_edgelist(edges)result.geometry.update(geometry)returnresult