1import os 2import numpy as np 3import collections 4 5from .. import util 6from .. import units 7from .. import convex 8from .. import caching 9from .. import grouping 10from .. import transformations 11 12from .. import bounds as bounds_module 13 14from ..exchange import gltf 15from ..parent import Geometry 16 17from . import cameras 18from . import lighting 19 20from .transforms import TransformForest 21 22 23class Scene(Geometry): 24 """ 25 A simple scene graph which can be rendered directly via 26 pyglet/openGL or through other endpoints such as a 27 raytracer. Meshes are added by name, which can then be 28 moved by updating transform in the transform tree. 29 """ 30 31 def __init__(self, 32 geometry=None, 33 base_frame='world', 34 metadata={}, 35 graph=None, 36 camera=None, 37 lights=None, 38 camera_transform=None): 39 """ 40 Create a new Scene object. 41 42 Parameters 43 ------------- 44 geometry : Trimesh, Path2D, Path3D PointCloud or list 45 Geometry to initially add to the scene 46 base_frame : str or hashable 47 Name of base frame 48 metadata : dict 49 Any metadata about the scene 50 graph : TransformForest or None 51 A passed transform graph to use 52 camera : Camera or None 53 A passed camera to use 54 lights : [trimesh.scene.lighting.Light] or None 55 A passed lights to use 56 camera_transform : (4, 4) float or None 57 Camera transform in the base frame 58 """ 59 # mesh name : Trimesh object 60 self.geometry = collections.OrderedDict() 61 62 # create a new graph 63 self.graph = TransformForest(base_frame=base_frame) 64 65 # create our cache 66 self._cache = caching.Cache(id_function=self.md5) 67 68 # add passed geometry to scene 69 self.add_geometry(geometry) 70 71 # hold metadata about the scene 72 self.metadata = {} 73 self.metadata.update(metadata) 74 75 if graph is not None: 76 # if we've been passed a graph override the default 77 self.graph = graph 78 79 self.camera = camera 80 self.lights = lights 81 self.camera_transform = camera_transform 82 83 def apply_transform(self, transform): 84 """ 85 Apply a transform to every geometry in the scene. 86 87 Parameters 88 -------------- 89 transform : (4, 4) 90 Homogeneous transformation matrix 91 """ 92 for geometry in self.geometry.values(): 93 geometry.apply_transform(transform) 94 95 def add_geometry(self, 96 geometry, 97 node_name=None, 98 geom_name=None, 99 parent_node_name=None, 100 transform=None): 101 """ 102 Add a geometry to the scene. 103 104 If the mesh has multiple transforms defined in its 105 metadata, they will all be copied into the 106 TransformForest of the current scene automatically. 107 108 Parameters 109 ---------- 110 geometry : Trimesh, Path2D, Path3D PointCloud or list 111 Geometry to initially add to the scene 112 base_frame : str or hashable 113 Name of base frame 114 metadata : dict 115 Any metadata about the scene 116 graph : TransformForest or None 117 A passed transform graph to use 118 119 Returns 120 ---------- 121 node_name : str 122 Name of node in self.graph 123 """ 124 125 if geometry is None: 126 return 127 # PointCloud objects will look like a sequence 128 elif util.is_sequence(geometry): 129 # if passed a sequence add all elements 130 for i, value in enumerate(geometry): 131 if i == 0: 132 node_name = self.add_geometry( 133 geometry=value, 134 node_name=node_name, 135 geom_name=geom_name, 136 parent_node_name=parent_node_name, 137 transform=transform) 138 else: 139 self.add_geometry( 140 geometry=value, 141 geom_name=geom_name, 142 parent_node_name=node_name) 143 return 144 145 elif isinstance(geometry, dict): 146 # if someone passed us a dict of geometry 147 for key, value in geometry.items(): 148 self.add_geometry(value, geom_name=key) 149 return 150 elif isinstance(geometry, Scene): 151 # concatenate current scene with passed scene 152 concat = self + geometry 153 # replace geometry in-place 154 self.geometry.clear() 155 self.geometry.update(concat.geometry) 156 # replace graph data with concatenated graph 157 self.graph.transforms = concat.graph.transforms 158 return 159 elif not hasattr(geometry, 'vertices'): 160 util.log.warning('unknown type ({}) added to scene!'.format( 161 type(geometry).__name__)) 162 163 # get or create a name to reference the geometry by 164 if geom_name is not None: 165 # if name is passed use it 166 name = geom_name 167 elif 'name' in geometry.metadata: 168 # if name is in metadata use it 169 name = geometry.metadata['name'] 170 elif 'file_name' in geometry.metadata: 171 name = geometry.metadata['file_name'] 172 else: 173 # try to create a simple name 174 name = 'geometry_' + str(len(self.geometry)) 175 176 # if its already taken add a unique random string to it 177 if name in self.geometry: 178 name += ':' + util.unique_id().upper() 179 180 # save the geometry reference 181 self.geometry[name] = geometry 182 183 # create a unique node name if not passed 184 if node_name is None: 185 # a random unique identifier 186 unique = util.unique_id(increment=len(self.geometry)) 187 # geometry name + UUID 188 node_name = name + '_' + unique.upper() 189 190 if transform is None: 191 # create an identity transform from parent_node 192 transform = np.eye(4) 193 194 self.graph.update(frame_to=node_name, 195 frame_from=parent_node_name, 196 matrix=transform, 197 geometry=name, 198 geometry_flags={'visible': True}) 199 return node_name 200 201 def delete_geometry(self, names): 202 """ 203 Delete one more multiple geometries from the scene and also 204 remove any node in the transform graph which references it. 205 206 Parameters 207 -------------- 208 name : hashable 209 Name that references self.geometry 210 """ 211 # make sure we have a set we can check 212 if util.is_string(names): 213 names = [names] 214 names = set(names) 215 216 # remove the geometry reference from relevant nodes 217 self.graph.remove_geometries(names) 218 # remove the geometries from our geometry store 219 [self.geometry.pop(name, None) for name in names] 220 221 def md5(self): 222 """ 223 MD5 of scene which will change when meshes or 224 transforms are changed 225 226 Returns 227 -------- 228 hashed : str 229 MD5 hash of scene 230 """ 231 # start with transforms hash 232 hashes = [self.graph.md5()] 233 for g in self.geometry.values(): 234 if hasattr(g, 'md5'): 235 hashes.append(g.md5()) 236 elif hasattr(g, 'tostring'): 237 hashes.append(str(hash(g.tostring()))) 238 else: 239 # try to just straight up hash 240 # this may raise errors 241 hashes.append(str(hash(g))) 242 243 md5 = util.md5_object(''.join(hashes)) 244 245 return md5 246 247 @property 248 def is_empty(self): 249 """ 250 Does the scene have anything in it. 251 252 Returns 253 ---------- 254 is_empty: bool, True if nothing is in the scene 255 """ 256 257 is_empty = len(self.geometry) == 0 258 return is_empty 259 260 @property 261 def is_valid(self): 262 """ 263 Is every geometry connected to the root node. 264 265 Returns 266 ----------- 267 is_valid : bool 268 Does every geometry have a transform 269 """ 270 if len(self.geometry) == 0: 271 return True 272 273 try: 274 referenced = {self.graph[i][1] 275 for i in self.graph.nodes_geometry} 276 except BaseException: 277 # if connectivity to world frame is broken return false 278 return False 279 280 # every geometry is referenced 281 ok = referenced == set(self.geometry.keys()) 282 283 return ok 284 285 @caching.cache_decorator 286 def bounds_corners(self): 287 """ 288 A list of points that represent the corners of the 289 AABB of every geometry in the scene. 290 291 This can be useful if you want to take the AABB in 292 a specific frame. 293 294 Returns 295 ----------- 296 corners: (n, 3) float, points in space 297 """ 298 # the saved corners of each instance 299 corners_inst = [] 300 # (n, 3) float corners of each geometry 301 corners_geom = {k: bounds_module.corners(v.bounds) 302 for k, v in self.geometry.items() 303 if v.bounds is not None} 304 if len(corners_geom) == 0: 305 return np.array([]) 306 307 for node_name in self.graph.nodes_geometry: 308 # access the transform and geometry name from node 309 transform, geometry_name = self.graph[node_name] 310 # not all nodes have associated geometry 311 if geometry_name not in corners_geom: 312 continue 313 # transform geometry corners into where 314 # the instance of the geometry is located 315 corners_inst.extend( 316 transformations.transform_points( 317 corners_geom[geometry_name], 318 transform)) 319 # make corners numpy array 320 corners_inst = np.array(corners_inst, 321 dtype=np.float64) 322 return corners_inst 323 324 @caching.cache_decorator 325 def bounds(self): 326 """ 327 Return the overall bounding box of the scene. 328 329 Returns 330 -------- 331 bounds : (2, 3) float or None 332 Position of [min, max] bounding box 333 Returns None if no valid bounds exist 334 """ 335 corners = self.bounds_corners 336 if len(corners) == 0: 337 return None 338 bounds = np.array([corners.min(axis=0), 339 corners.max(axis=0)]) 340 return bounds 341 342 @caching.cache_decorator 343 def extents(self): 344 """ 345 Return the axis aligned box size of the current scene. 346 347 Returns 348 ---------- 349 extents : (3,) float 350 Bounding box sides length 351 """ 352 return np.diff(self.bounds, axis=0).reshape(-1) 353 354 @caching.cache_decorator 355 def scale(self): 356 """ 357 The approximate scale of the mesh 358 359 Returns 360 ----------- 361 scale : float 362 The mean of the bounding box edge lengths 363 """ 364 scale = (self.extents ** 2).sum() ** .5 365 return scale 366 367 @caching.cache_decorator 368 def centroid(self): 369 """ 370 Return the center of the bounding box for the scene. 371 372 Returns 373 -------- 374 centroid : (3) float 375 Point for center of bounding box 376 """ 377 centroid = np.mean(self.bounds, axis=0) 378 return centroid 379 380 @caching.cache_decorator 381 def triangles(self): 382 """ 383 Return a correctly transformed polygon soup of the 384 current scene. 385 386 Returns 387 ---------- 388 triangles : (n, 3, 3) float 389 Triangles in space 390 """ 391 triangles = collections.deque() 392 triangles_node = collections.deque() 393 394 for node_name in self.graph.nodes_geometry: 395 # which geometry does this node refer to 396 transform, geometry_name = self.graph[node_name] 397 398 # get the actual potential mesh instance 399 geometry = self.geometry[geometry_name] 400 if not hasattr(geometry, 'triangles'): 401 continue 402 # append the (n, 3, 3) triangles to a sequence 403 triangles.append( 404 transformations.transform_points( 405 geometry.triangles.copy().reshape((-1, 3)), 406 matrix=transform)) 407 # save the node names for each triangle 408 triangles_node.append( 409 np.tile(node_name, 410 len(geometry.triangles))) 411 # save the resulting nodes to the cache 412 self._cache['triangles_node'] = np.hstack(triangles_node) 413 triangles = np.vstack(triangles).reshape((-1, 3, 3)) 414 return triangles 415 416 @caching.cache_decorator 417 def triangles_node(self): 418 """ 419 Which node of self.graph does each triangle come from. 420 421 Returns 422 --------- 423 triangles_index : (len(self.triangles),) 424 Node name for each triangle 425 """ 426 populate = self.triangles # NOQA 427 return self._cache['triangles_node'] 428 429 @caching.cache_decorator 430 def geometry_identifiers(self): 431 """ 432 Look up geometries by identifier MD5 433 434 Returns 435 --------- 436 identifiers : dict 437 {Identifier MD5: key in self.geometry} 438 """ 439 identifiers = {mesh.identifier_md5: name 440 for name, mesh in self.geometry.items()} 441 return identifiers 442 443 @caching.cache_decorator 444 def duplicate_nodes(self): 445 """ 446 Return a sequence of node keys of identical meshes. 447 448 Will include meshes with different geometry but identical 449 spatial hashes as well as meshes repeated by self.nodes. 450 451 Returns 452 ----------- 453 duplicates : (m) sequenc 454 Keys of self.nodes that represent identical geometry 455 """ 456 # if there is no geometry we can have no duplicate nodes 457 if len(self.geometry) == 0: 458 return [] 459 460 # geometry name : md5 of mesh 461 mesh_hash = {k: int(m.identifier_md5, 16) 462 for k, m in self.geometry.items()} 463 # the name of nodes in the scene graph with geometry 464 node_names = np.array(self.graph.nodes_geometry) 465 # the geometry names for each node in the same order 466 node_geom = np.array([self.graph[i][1] for i in node_names]) 467 # the mesh md5 for each node in the same order 468 node_hash = np.array([mesh_hash[v] for v in node_geom]) 469 # indexes of identical hashes 470 node_groups = grouping.group(node_hash) 471 # sequence of node names where each 472 # sublist has identical geometry 473 duplicates = [np.sort(node_names[g]).tolist() 474 for g in node_groups] 475 return duplicates 476 477 def deduplicated(self): 478 """ 479 Return a new scene where each unique geometry is only 480 included once and transforms are discarded. 481 482 Returns 483 ------------- 484 dedupe : Scene 485 One copy of each unique geometry from scene 486 """ 487 # collect geometry 488 geometry = {} 489 # loop through groups of identical nodes 490 for group in self.duplicate_nodes: 491 # get the name of the geometry 492 name = self.graph[group[0]][1] 493 # collect our unique collection of geometry 494 geometry[name] = self.geometry[name] 495 496 return Scene(geometry) 497 498 def set_camera(self, 499 angles=None, 500 distance=None, 501 center=None, 502 resolution=None, 503 fov=None): 504 """ 505 Create a camera object for self.camera, and add 506 a transform to self.graph for it. 507 508 If arguments are not passed sane defaults will be figured 509 out which show the mesh roughly centered. 510 511 Parameters 512 ----------- 513 angles : (3,) float 514 Initial euler angles in radians 515 distance : float 516 Distance from centroid 517 center : (3,) float 518 Point camera should be center on 519 camera : Camera object 520 Object that stores camera parameters 521 """ 522 523 if fov is None: 524 fov = np.array([60, 45]) 525 526 # if no geometry nothing to set camera to 527 if len(self.geometry) == 0: 528 self._camera = cameras.Camera(fov=fov) 529 self.graph[self._camera.name] = None 530 return self._camera 531 # set with no rotation by default 532 if angles is None: 533 angles = np.zeros(3) 534 535 rotation = transformations.euler_matrix(*angles) 536 transform = cameras.look_at( 537 self.bounds_corners, 538 fov=fov, 539 rotation=rotation, 540 distance=distance, 541 center=center) 542 543 if hasattr(self, '_camera') and self._camera is not None: 544 self._camera.fov = fov 545 if resolution is not None: 546 self._camera.resolution = resolution 547 else: 548 # create a new camera object 549 self._camera = cameras.Camera(fov=fov, resolution=resolution) 550 551 self.graph[self._camera.name] = transform 552 553 return self._camera 554 555 @property 556 def camera_transform(self): 557 """ 558 Get camera transform in the base frame 559 560 Returns 561 ------- 562 camera_transform : (4, 4) float 563 Camera transform in the base frame 564 """ 565 return self.graph[self.camera.name][0] 566 567 def camera_rays(self): 568 """ 569 Calculate the trimesh.scene.Camera origin and ray 570 direction vectors. Returns one ray per pixel as set 571 in camera.resolution 572 573 Returns 574 -------------- 575 origins: (n, 3) float 576 Ray origins in space 577 vectors: (n, 3) float 578 Ray direction unit vectors in world coordinates 579 pixels : (n, 2) int 580 Which pixel does each ray correspond to in an image 581 """ 582 vectors, pixels = self.camera.to_rays() 583 transform = self.camera_transform 584 585 # apply the rotation to the direction vectors 586 vectors = transformations.transform_points( 587 vectors, 588 transform, 589 translate=False) 590 # camera origin is single point so extract from transform 591 origins = (np.ones_like(vectors) * 592 transformations.translation_from_matrix( 593 transform)) 594 return origins, vectors, pixels 595 596 @camera_transform.setter 597 def camera_transform(self, camera_transform): 598 """ 599 Set the camera transform in the base frame 600 601 Parameters 602 ---------- 603 camera_transform : (4, 4) float 604 Camera transform in the base frame 605 """ 606 if camera_transform is None: 607 return 608 self.graph[self.camera.name] = camera_transform 609 610 @property 611 def camera(self): 612 """ 613 Get the single camera for the scene. If not manually 614 set one will abe automatically generated. 615 616 Returns 617 ---------- 618 camera : trimesh.scene.Camera 619 Camera object defined for the scene 620 """ 621 # no camera set for the scene yet 622 if not hasattr(self, '_camera') or self._camera is None: 623 # will create a camera with everything in view 624 return self.set_camera() 625 assert self._camera is not None 626 627 return self._camera 628 629 @camera.setter 630 def camera(self, camera): 631 """ 632 Set a camera object for the Scene. 633 634 Parameters 635 ----------- 636 camera : trimesh.scene.Camera 637 Camera object for the scene 638 """ 639 if camera is None: 640 return 641 self._camera = camera 642 643 @property 644 def lights(self): 645 """ 646 Get a list of the lights in the scene. If nothing is 647 set it will generate some automatically. 648 649 Returns 650 ------------- 651 lights : [trimesh.scene.lighting.Light] 652 Lights in the scene. 653 """ 654 if not hasattr(self, '_lights') or self._lights is None: 655 # do some automatic lighting 656 lights, transforms = lighting.autolight(self) 657 # assign the transforms to the scene graph 658 for L, T in zip(lights, transforms): 659 self.graph[L.name] = T 660 # set the lights 661 self._lights = lights 662 return self._lights 663 664 @lights.setter 665 def lights(self, lights): 666 """ 667 Assign a list of light objects to the scene 668 669 Parameters 670 -------------- 671 lights : [trimesh.scene.lighting.Light] 672 Lights in the scene. 673 """ 674 self._lights = lights 675 676 def rezero(self): 677 """ 678 Move the current scene so that the AABB of the whole 679 scene is centered at the origin. 680 681 Does this by changing the base frame to a new, offset 682 base frame. 683 """ 684 if self.is_empty or np.allclose(self.centroid, 0.0): 685 # early exit since what we want already exists 686 return 687 688 # the transformation to move the overall scene to AABB centroid 689 matrix = np.eye(4) 690 matrix[:3, 3] = -self.centroid 691 692 # we are going to change the base frame 693 new_base = str(self.graph.base_frame) + '_I' 694 self.graph.update(frame_from=new_base, 695 frame_to=self.graph.base_frame, 696 matrix=matrix) 697 self.graph.base_frame = new_base 698 699 def dump(self, concatenate=False): 700 """ 701 Append all meshes in scene to a list of meshes. 702 703 Returns 704 ---------- 705 dumped : (n,) list 706 Trimesh objects transformed to their 707 location the scene.graph 708 """ 709 result = [] 710 for node_name in self.graph.nodes_geometry: 711 transform, geometry_name = self.graph[node_name] 712 # get a copy of the geometry 713 current = self.geometry[geometry_name].copy() 714 # move the geometry vertices into the requested frame 715 current.apply_transform(transform) 716 # save to our list of meshes 717 result.append(current) 718 719 if concatenate: 720 return util.concatenate(result) 721 722 return np.array(result) 723 724 @caching.cache_decorator 725 def convex_hull(self): 726 """ 727 The convex hull of the whole scene 728 729 Returns 730 --------- 731 hull: Trimesh object, convex hull of all meshes in scene 732 """ 733 points = util.vstack_empty([m.vertices for m in self.dump()]) 734 hull = convex.convex_hull(points) 735 return hull 736 737 def export(self, file_obj=None, file_type=None, **kwargs): 738 """ 739 Export a snapshot of the current scene. 740 741 Parameters 742 ---------- 743 file_type: what encoding to use for meshes 744 ie: dict, dict64, stl 745 746 Returns 747 ---------- 748 export : bytes 749 Only returned if file_obj is None 750 """ 751 752 # if we weren't passed a file type extract from file_obj 753 if file_type is None: 754 file_type = str(file_obj).split('.')[-1] 755 756 # always remove whitepace and leading characters 757 file_type = file_type.strip().lower().lstrip('.') 758 759 if file_type == 'gltf': 760 data = gltf.export_gltf(self, **kwargs) 761 elif file_type == 'glb': 762 data = gltf.export_glb(self, **kwargs) 763 elif file_type == 'dict': 764 from ..exchange.export import scene_to_dict 765 data = scene_to_dict(self) 766 elif file_type == 'dict64': 767 from ..exchange.export import scene_to_dict 768 data = scene_to_dict(self, use_base64=True) 769 else: 770 raise ValueError('unsupported export format: {}'.format(file_type)) 771 772 # now write the data or return bytes of result 773 if hasattr(file_obj, 'write'): 774 # if it's just a regular file object 775 file_obj.write(data) 776 elif util.is_string(file_obj): 777 # assume strings are file paths 778 file_path = os.path.expanduser(os.path.abspath(file_obj)) 779 with open(file_path, 'wb') as f: 780 f.write(data) 781 else: 782 # no writeable file object so return data 783 return data 784 785 def save_image(self, resolution=None, **kwargs): 786 """ 787 Get a PNG image of a scene. 788 789 Parameters 790 ----------- 791 resolution : (2,) int 792 Resolution to render image 793 **kwargs 794 Passed to SceneViewer constructor 795 796 Returns 797 ----------- 798 png : bytes 799 Render of scene as a PNG 800 """ 801 from ..viewer import render_scene 802 png = render_scene(scene=self, 803 resolution=resolution, 804 **kwargs) 805 return png 806 807 @property 808 def units(self): 809 """ 810 Get the units for every model in the scene, and 811 raise a ValueError if there are mixed units. 812 813 Returns 814 ----------- 815 units : str 816 Units for every model in the scene 817 """ 818 existing = [i.units for i in self.geometry.values()] 819 820 if any(existing[0] != e for e in existing): 821 # if all of our geometry doesn't have the same units already 822 # this function will only do some hot nonsense 823 raise ValueError('models in scene have inconsistent units!') 824 825 return existing[0] 826 827 @units.setter 828 def units(self, value): 829 """ 830 Set the units for every model in the scene without 831 converting any units just setting the tag. 832 833 Parameters 834 ------------ 835 value : str 836 Value to set every geometry unit value to 837 """ 838 for m in self.geometry.values(): 839 m.units = value 840 841 def convert_units(self, desired, guess=False): 842 """ 843 If geometry has units defined convert them to new units. 844 845 Returns a new scene with geometries and transforms scaled. 846 847 Parameters 848 ---------- 849 desired : str 850 Desired final unit system: 'inches', 'mm', etc. 851 guess : bool 852 Is the converter allowed to guess scale when models 853 don't have it specified in their metadata. 854 855 Returns 856 ---------- 857 scaled : trimesh.Scene 858 Copy of scene with scaling applied and units set 859 for every model 860 """ 861 # if there is no geometry do nothing 862 if len(self.geometry) == 0: 863 return self.copy() 864 865 current = self.units 866 if current is None: 867 # will raise ValueError if not in metadata 868 # and not allowed to guess 869 current = units.units_from_metadata(self, guess=guess) 870 871 # find the float conversion 872 scale = units.unit_conversion(current=current, 873 desired=desired) 874 875 # exit early if our current units are the same as desired units 876 if np.isclose(scale, 1.0): 877 result = self.copy() 878 else: 879 result = self.scaled(scale=scale) 880 881 # apply the units to every geometry of the scaled result 882 result.units = desired 883 884 return result 885 886 def explode(self, vector=None, origin=None): 887 """ 888 Explode a scene around a point and vector. 889 890 Parameters 891 ----------- 892 vector : (3,) float or float 893 Explode radially around a direction vector or spherically 894 origin : (3,) float 895 Point to explode around 896 """ 897 if origin is None: 898 origin = self.centroid 899 if vector is None: 900 vector = self.scale / 25.0 901 902 vector = np.asanyarray(vector, dtype=np.float64) 903 origin = np.asanyarray(origin, dtype=np.float64) 904 905 for node_name in self.graph.nodes_geometry: 906 transform, geometry_name = self.graph[node_name] 907 908 centroid = self.geometry[geometry_name].centroid 909 # transform centroid into nodes location 910 centroid = np.dot(transform, 911 np.append(centroid, 1))[:3] 912 913 if vector.shape == (): 914 # case where our vector is a single number 915 offset = (centroid - origin) * vector 916 elif np.shape(vector) == (3,): 917 projected = np.dot(vector, (centroid - origin)) 918 offset = vector * projected 919 else: 920 raise ValueError('explode vector wrong shape!') 921 922 transform[0:3, 3] += offset 923 self.graph[node_name] = transform 924 925 def scaled(self, scale): 926 """ 927 Return a copy of the current scene, with meshes and scene 928 transforms scaled to the requested factor. 929 930 Parameters 931 ----------- 932 scale : float 933 Factor to scale meshes and transforms 934 935 Returns 936 ----------- 937 scaled : trimesh.Scene 938 A copy of the current scene but scaled 939 """ 940 scale = float(scale) 941 # matrix for 2D scaling 942 scale_2D = np.eye(3) * scale 943 # matrix for 3D scaling 944 scale_3D = np.eye(4) * scale 945 946 # preallocate transforms and geometries 947 nodes = self.graph.nodes_geometry 948 transforms = np.zeros((len(nodes), 4, 4)) 949 geometries = [None] * len(nodes) 950 951 # collect list of transforms 952 for i, node in enumerate(nodes): 953 transforms[i], geometries[i] = self.graph[node] 954 955 # result is a copy 956 result = self.copy() 957 # remove all existing transforms 958 result.graph.clear() 959 960 for group in grouping.group(geometries): 961 # hashable reference to self.geometry 962 geometry = geometries[group[0]] 963 # original transform from world to geometry 964 original = transforms[group[0]] 965 # transform for geometry 966 new_geom = np.dot(scale_3D, original) 967 968 if result.geometry[geometry].vertices.shape[1] == 2: 969 # if our scene is 2D only scale in 2D 970 result.geometry[geometry].apply_transform(scale_2D) 971 else: 972 # otherwise apply the full transform 973 result.geometry[geometry].apply_transform(new_geom) 974 975 for node, T in zip(self.graph.nodes_geometry[group], 976 transforms[group]): 977 # generate the new transforms 978 transform = util.multi_dot( 979 [scale_3D, T, np.linalg.inv(new_geom)]) 980 # apply scale to translation 981 transform[:3, 3] *= scale 982 # update scene with new transforms 983 result.graph.update(frame_to=node, 984 matrix=transform, 985 geometry=geometry) 986 return result 987 988 def copy(self): 989 """ 990 Return a deep copy of the current scene 991 992 Returns 993 ---------- 994 copied : trimesh.Scene 995 Copy of the current scene 996 """ 997 # use the geometries copy method to 998 # allow them to handle references to unpickle-able objects 999 geometry = {n: g.copy() for n, g in self.geometry.items()} 1000 1001 if not hasattr(self, '_camera') or self._camera is None: 1002 # if no camera set don't include it 1003 camera = None 1004 else: 1005 # otherwise get a copy of the camera 1006 camera = self.camera.copy() 1007 # create a new scene with copied geometry and graph 1008 copied = Scene(geometry=geometry, 1009 graph=self.graph.copy(), 1010 camera=camera) 1011 return copied 1012 1013 def show(self, viewer=None, **kwargs): 1014 """ 1015 Display the current scene. 1016 1017 Parameters 1018 ----------- 1019 viewer: str 1020 What kind of viewer to open, including 1021 'gl' to open a pyglet window, 'notebook' 1022 for a jupyter notebook or None 1023 kwargs : dict 1024 Includes `smooth`, which will turn 1025 on or off automatic smooth shading 1026 """ 1027 1028 if viewer is None: 1029 # check to see if we are in a notebook or not 1030 from ..viewer import in_notebook 1031 viewer = 'gl' 1032 if in_notebook(): 1033 viewer = 'notebook' 1034 1035 if viewer == 'gl': 1036 # this imports pyglet, and will raise an ImportError 1037 # if pyglet is not available 1038 from ..viewer import SceneViewer 1039 return SceneViewer(self, **kwargs) 1040 elif viewer == 'notebook': 1041 from ..viewer import scene_to_notebook 1042 return scene_to_notebook(self, **kwargs) 1043 else: 1044 raise ValueError('viewer must be "gl", "notebook", or None') 1045 1046 def __add__(self, other): 1047 """ 1048 Concatenate the current scene with another scene or mesh. 1049 1050 Parameters 1051 ------------ 1052 other : trimesh.Scene, trimesh.Trimesh, trimesh.Path 1053 Other object to append into the result scene 1054 1055 Returns 1056 ------------ 1057 appended : trimesh.Scene 1058 Scene with geometry from both scenes 1059 """ 1060 result = append_scenes([self, other], 1061 common=[self.graph.base_frame]) 1062 return result 1063 1064 1065def split_scene(geometry): 1066 """ 1067 Given a geometry, list of geometries, or a Scene 1068 return them as a single Scene object. 1069 1070 Parameters 1071 ---------- 1072 geometry : splittable 1073 1074 Returns 1075 --------- 1076 scene: trimesh.Scene 1077 """ 1078 # already a scene, so return it 1079 if util.is_instance_named(geometry, 'Scene'): 1080 return geometry 1081 1082 # a list of things 1083 if util.is_sequence(geometry): 1084 metadata = {} 1085 for g in geometry: 1086 try: 1087 metadata.update(g.metadata) 1088 except BaseException: 1089 continue 1090 return Scene(geometry, 1091 metadata=metadata) 1092 1093 # a single geometry so we are going to split 1094 split = [] 1095 metadata = {} 1096 for g in util.make_sequence(geometry): 1097 split.extend(g.split()) 1098 metadata.update(g.metadata) 1099 1100 # if there is only one geometry in the mesh 1101 # name it from the file name 1102 if len(split) == 1 and 'file_name' in metadata: 1103 split = {metadata['file_name']: split[0]} 1104 1105 scene = Scene(split, metadata=metadata) 1106 1107 return scene 1108 1109 1110def append_scenes(iterable, common=['world']): 1111 """ 1112 Concatenate multiple scene objects into one scene. 1113 1114 Parameters 1115 ------------- 1116 iterable : (n,) Trimesh or Scene 1117 Geometries that should be appended 1118 common : (n,) str 1119 Nodes that shouldn't be remapped 1120 1121 Returns 1122 ------------ 1123 result : trimesh.Scene 1124 Scene containing all geometry 1125 """ 1126 if isinstance(iterable, Scene): 1127 return iterable 1128 1129 # save geometry in dict 1130 geometry = {} 1131 # save transforms as edge tuples 1132 edges = [] 1133 1134 # nodes which shouldn't be remapped 1135 common = set(common) 1136 # nodes which are consumed and need to be remapped 1137 consumed = set() 1138 1139 def node_remap(node): 1140 """ 1141 Remap node to new name if necessary 1142 1143 Parameters 1144 ------------- 1145 node : hashable 1146 Node name in original scene 1147 1148 Returns 1149 ------------- 1150 name : hashable 1151 Node name in concatenated scene 1152 """ 1153 1154 # if we've already remapped a node use it 1155 if node in map_node: 1156 return map_node[node] 1157 1158 # if a node is consumed and isn't one of the nodes 1159 # we're going to hold common between scenes remap it 1160 if node not in common and node in consumed: 1161 name = str(node) + '-' + util.unique_id().upper() 1162 map_node[node] = name 1163 node = name 1164 1165 # keep track of which nodes have been used 1166 # in the current scene 1167 current.add(node) 1168 return node 1169 1170 # loop through every geometry 1171 for s in iterable: 1172 # allow Trimesh/Path2D geometry to be passed 1173 if hasattr(s, 'scene'): 1174 s = s.scene() 1175 # if we don't have a scene raise an exception 1176 if not isinstance(s, Scene): 1177 raise ValueError('{} is not a scene!'.format( 1178 type(s).__name__)) 1179 1180 # remap geometries if they have been consumed 1181 map_geom = {} 1182 for k, v in s.geometry.items(): 1183 # if a geometry already exists add a UUID to the name 1184 if k in geometry: 1185 name = str(k) + '-' + util.unique_id().upper() 1186 else: 1187 name = k 1188 # store name mapping 1189 map_geom[k] = name 1190 # store geometry with new name 1191 geometry[name] = v 1192 1193 # remap nodes and edges so duplicates won't 1194 # stomp all over each other 1195 map_node = {} 1196 # the nodes used in this scene 1197 current = set() 1198 for a, b, attr in s.graph.to_edgelist(): 1199 # remap node names from local names 1200 a, b = node_remap(a), node_remap(b) 1201 # remap geometry keys 1202 # if key is not in map_geom it means one of the scenes 1203 # referred to geometry that doesn't exist 1204 # rather than crash here we ignore it as the user 1205 # possibly intended to add in geometries back later 1206 if 'geometry' in attr and attr['geometry'] in map_geom: 1207 attr['geometry'] = map_geom[attr['geometry']] 1208 # save the new edge 1209 edges.append((a, b, attr)) 1210 # mark nodes from current scene as consumed 1211 consumed.update(current) 1212 1213 # add all data to a new scene 1214 result = Scene() 1215 result.graph.from_edgelist(edges) 1216 result.geometry.update(geometry) 1217 1218 return result 1219