├── .github └── workflows │ ├── publish.yml │ └── static_code_analysis.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── human.md ├── human_teaser.gif └── output.gif ├── examples ├── 01_objects.py ├── 02_robots.py ├── 03_animation.py ├── 04_image_and_video.py ├── 05_teaser.py ├── 05_teaser_with_human.py ├── 06_human.py ├── spade.obj └── texture.png ├── pyproject.toml ├── setup.py ├── src └── robomeshcat │ ├── __init__.py │ ├── human.py │ ├── object.py │ ├── robot.py │ └── scene.py └── tests ├── test_robot_q.py └── test_urdf.urdf /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI.org 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | pypi: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | with: 12 | fetch-depth: 0 13 | - run: python3 -m pip install --upgrade build && python3 -m build 14 | - name: Publish package 15 | uses: pypa/gh-action-pypi-publish@release/v1 16 | with: 17 | password: ${{ secrets.PYPI_API_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/static_code_analysis.yml: -------------------------------------------------------------------------------- 1 | name: static_code_analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | flake8-lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.8" 17 | - uses: py-actions/flake8@v2 18 | with: 19 | args: "--per-file-ignores=__init__.py:F401" 20 | max-line-length: "120" 21 | path: "src/" 22 | 23 | black-lint: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: psf/black@stable 28 | with: 29 | options: "--check --diff --verbose --skip-string-normalization --line-length=120" 30 | src: "src/" 31 | 32 | pytype-lint: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-python@v4 37 | with: 38 | python-version: "3.10" 39 | - run: pip install pytype>=2023.2.14 40 | - run: pytype -d import-error src/ 41 | 42 | pytest: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | - uses: actions/setup-python@v4 47 | with: 48 | python-version: "3.10" 49 | - run: pip install pytest 50 | - run: pip install -e . 51 | - run: pytest tests 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Vladimír Petrík 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py 2 | include MANIFEST.in 3 | include LICENSE 4 | include README.md 5 | 6 | graft tests 7 | graft examples 8 | graft docs 9 | graft src 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RoboMeshCat 2 | 3 | [![](https://anaconda.org/conda-forge/robomeshcat/badges/version.svg)](https://anaconda.org/conda-forge/robomeshcat) 4 | [![PyPI version](https://badge.fury.io/py/robomeshcat.svg)](https://badge.fury.io/py/robomeshcat) 5 | ![](https://anaconda.org/conda-forge/robomeshcat/badges/downloads.svg) 6 | [![License](https://img.shields.io/badge/License-BSD_2--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause) 7 | 8 | Set of utilities for visualizing robots in web-based visualizer [MeshCat](https://github.com/rdeits/meshcat-python). 9 | The whole library is object and robot centric allowing you to modify properties of instances rather than manipulating 10 | the visualization tree of the MeshCat itself. 11 | The library allows you to easily generate videos like this (source code is [here](examples/05_teaser.py)): 12 | 13 | ![](https://raw.githubusercontent.com/petrikvladimir/robomeshcat/main/docs/output.gif) 14 | 15 | or like this (by installing a few more [dependencies](docs/human.md); [source code](examples/05_teaser_with_human.py)): 16 | 17 | ![](https://raw.githubusercontent.com/petrikvladimir/robomeshcat/main/docs/human_teaser.gif) 18 | 19 | # Installation 20 | 21 | ## From 22 | 23 | ```bash 24 | conda install -c conda-forge robomeshcat 25 | ``` 26 | 27 | ## From PyPI 28 | 29 | ```bash 30 | pip install robomeshcat 31 | ``` 32 | 33 | # About 34 | 35 | This library allows you to visualize and animate objects and robots in web browser. 36 | Compared to [MeshCat](https://github.com/rdeits/meshcat-python) library, on which we build, our library is 37 | object-oriented allowing you to modify the properties of individual objects, for example: 38 | 39 | ```python 40 | o = Object.create_sphere(radius=0.2) 41 | o.pos[2] = 2. # modify position 42 | o.opacity = 0.3 # modify transparency 43 | o.color[0] = 1. # modify red channel of the color 44 | o.hide() # hide the object, i.e. set o.visible = False 45 | ``` 46 | 47 | In addition to objects, it allows you to easily create and manipulate robots (loaded from `URDF` file): 48 | 49 | ```python 50 | r = Robot(urdf_path='robot.urdf') 51 | r[0] = np.pi # set the value of the first joint 52 | r['joint5'] = 0 # set the value of the joint named 'joint5' 53 | r.pos = [0, 0, 0] # set the base pose of the robot 54 | r.color, r.opacity, r.visibility, r.rot = ... # change the color, opacity, visibility, or rotation 55 | ``` 56 | 57 | All changes will be displayed immediately in the browser, that we call 'online' rendering. 58 | By default, you can rotate the camera view with your mouse. 59 | However, our library allows you to control the camera from the code as well through the `Scene` object, that is the main 60 | point for visualization: 61 | 62 | ```python 63 | scene = Scene() 64 | scene.add_object(o) # add object 'o' into the scene, that will display it 65 | scene.add_robot(r) # add robot 'r' into the scene, that will display it 66 | scene.camera_pos = [1, 1, 1] # set camera position 67 | scene.camera_pos[2] = 2 # change height of the camera 68 | scene.camera_zoom = 2 # zoom in 69 | scene.reset_camera() # reset camera such that you can control it with your mouse again 70 | ``` 71 | 72 | For complete examples of object and robot interface see our two examples: [01_object.py](examples/01_objects.py) 73 | and [02_robots.py](examples/02_robots.py). 74 | 75 | It is also possible to visualize human model after installing a few more dependencies, 76 | see [installation](docs/human.md) and example [06_human.py](examples/06_human.py). 77 | 78 | ## Animation 79 | 80 | This library allows you to animate all the properties of the objects and robots (e.g. position, robot configuration, 81 | color,opacity) inside the browser (from which you can replay, slow-down, etc.). Simply use: 82 | 83 | ```python 84 | scene = Scene() 85 | scene.add_object(o) 86 | 87 | with scene.animation(fps=25): 88 | o.pos[2] = 2. 89 | scene.render() # create a first frame of the animation, with object position z-axis set to 2. 90 | o.pos[2] = 0. 91 | scene.render() # create a second frame of the animation with object on the ground 92 | ``` 93 | 94 | You can also store the animation into the video, using the same principle: 95 | 96 | ```python 97 | with scene.video_recording(filename='video.mp4', fps=25): 98 | scene.render() 99 | ``` 100 | 101 | See our examples on [Animation](examples/03_animation.py) and [Image and video](examples/04_image_and_video.py). 102 | 103 | -------------------------------------------------------------------------------- /docs/human.md: -------------------------------------------------------------------------------- 1 | # Visualizing human model in meshcat via SMPL-X 2 | 3 | ## Installation 4 | 5 | The SMPL-X library and model data are not included in a standard RoboMeshCat packages. 6 | It must be installed as described in [SMPL-X package](https://github.com/vchoutas/smplx), i.e. by running 7 | 8 | ```bash 9 | pip install smplx[all] 10 | ``` 11 | 12 | and then downloading the data from [here](https://smpl-x.is.tue.mpg.de/), we use SMPL-X v1_1 data from the webpage. 13 | The path to the model must be specified while creating the instance of the Human. 14 | In the examples, the following folder structure is assumed: 15 | 16 | ```bash 17 | examples/ 18 | models/ # this is a folder downloaded from the webpage above 19 | smplx/ 20 | SMPLX_FEMALE.npz 21 | SMPLX_FEMALE.pkl 22 | SMPLX_MALE.npz 23 | ... 24 | 06_human.py # this is an example script 25 | ``` 26 | 27 | ## Usage 28 | 29 | The human model functionality is available through the class `Human`. 30 | You can change human pose (i.e. position and orientation), color, visibility, etc. in the same way as for regular 31 | RoboMeshCat objects, and you can also change the body pose (i.e. configuration), shape and expression through the 32 | instance of the class. 33 | For complete example have a look at [06_human.py](../examples/06_human.py). 34 | 35 | ### Online usage 36 | 37 | ```python 38 | human = Human(pose=human_default_pose, color=[1., 0., 0.], model_path=smplx_models_path) 39 | scene.add_object(human) # add human to the scene, it will be visualized immediately 40 | 41 | human.update_vertices(vertices=human.get_vertices(expression=torch.randn([1, 10]))) 42 | 43 | human.smplx_model.body_pose.data += 0.1 44 | human.update_vertices() 45 | ``` 46 | 47 | ### Animation 48 | 49 | Function `update_vertices` cannot be used in animation as it is modifying geometry of the object internally. 50 | Instead, you need to use 'morphologies', that need to be specified before adding human to the scene: 51 | 52 | ```python 53 | human = Human(pose=human_default_pose, color=[1., 0., 0.], model_path=smplx_models_path) 54 | human.smplx_model.betas.data += 1 55 | human.add_morph(human.get_vertices()) # the first morph changes the shape 56 | 57 | human.smplx_model.body_pose.data += 0.1 58 | human.add_morph(human.get_vertices()) # the second morp changes the body pose 59 | 60 | scene.add_object(human) # add human to the scene, no morphology can be added/modified after this step 61 | 62 | "Let's animate" 63 | with scene.animation(fps=1): 64 | human.display_morph(None) # this will display the human shape that is not affected by morphologies 65 | scene.render() 66 | 67 | human.display_morph(0) 68 | scene.render() 69 | 70 | human.display_morph(1) 71 | scene.render() 72 | ``` 73 | 74 | ### Coloring of human 75 | 76 | You have two options to color the human mesh: (i) uniform color and (ii) per vertex color. 77 | Uniform color is default, it can be set with `color` argument of human and changed/animated by `.color` property. 78 | 79 | Per vertex color cannot be animated as it requires to change the geometry internally. -------------------------------------------------------------------------------- /docs/human_teaser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petrikvladimir/RoboMeshCat/4c803c4c457d4e5db51ac8b11b04eadfec19239d/docs/human_teaser.gif -------------------------------------------------------------------------------- /docs/output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petrikvladimir/RoboMeshCat/4c803c4c457d4e5db51ac8b11b04eadfec19239d/docs/output.gif -------------------------------------------------------------------------------- /examples/01_objects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) CTU -- All Rights Reserved 3 | # Created on: 2022-10-11 4 | # Author: Vladimir Petrik 5 | # 6 | 7 | import numpy as np 8 | from pathlib import Path 9 | from pinocchio.utils import rotate 10 | 11 | from robomeshcat import Object, Scene 12 | 13 | " This example shows how to update properties of the objects online - i.e., changes are apparent in the visualizer " 14 | "immediately as they are set." 15 | 16 | "All elements are stored in Scene that is responsible for rendering them in the MeshCat. " 17 | scene = Scene() 18 | 19 | "You can add objects into the scene and refer to them through the object instance or by the name" 20 | obj1 = Object.create_sphere(radius=0.1, name='red_sphere', opacity=0.5, color=[1., 0., 0.]) 21 | scene.add_object(obj1) 22 | 23 | 24 | "Let's set object position in z-axis, both options do the same" 25 | input('Press enter to continue: to move sphere up') 26 | obj1.pos[2] = 1. 27 | scene['red_sphere'].pos[2] = 1. 28 | 29 | 30 | "Other objects could be added to the scene too" 31 | input('Press enter to continue: to add more objects') 32 | scene.add_object(Object.create_cuboid(lengths=0.1, name='box', color=[1, 0, 0])) 33 | scene.add_object(Object.create_cylinder(length=0.6, radius=0.1, name='cylinder')) 34 | scene.add_object(Object.create_mesh(path_to_mesh=Path(__file__).parent.joinpath('spade.obj'), scale=1e-3, name='spade')) 35 | 36 | scene['box'].pos[1] = 0.25 # other options: scene['box'].pos = [1, 1, 1], scene['box'].pose = ... 37 | scene['cylinder'].pos[0] = -0.5 38 | scene['cylinder'].rot = rotate('x', np.deg2rad(90)) 39 | scene['spade'].pos[1] = 0.75 40 | 41 | 42 | "You can also adjust color, opacity, or visibility of the object." 43 | input('Press enter to continue: to change the color and opacity') 44 | scene['box'].color = [0, 1, 0] # other options: scene['box'].color[1] = 1. 45 | scene['box'].opacity = 0.5 46 | 47 | input('Press enter to continue: to hide box and finis') 48 | scene['box'].hide() # other options: scene['box'].visible = False 49 | -------------------------------------------------------------------------------- /examples/02_robots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) CTU -- All Rights Reserved 3 | # Created on: 2022-10-11 4 | # Author: Vladimir Petrik 5 | # 6 | 7 | import time 8 | from pathlib import Path 9 | import numpy as np 10 | from robomeshcat import Robot, Scene 11 | 12 | "This example shows how to add and control robots online in the visualizer." 13 | "Robots can be loaded from URDF; we will add multiple robots to show the visual/collision models of the robot." 14 | 15 | "Example robot data package for the robot meshes and URDF, please install it with" 16 | "conda install -c conda-forge example-robot-data" 17 | 18 | "All elements are stored in Scene that is responsible for rendering them in the MeshCat" 19 | scene = Scene() 20 | 21 | from example_robot_data.robots_loader import PandaLoader as RobLoader 22 | 23 | "Create the first robot and add it to the scene" 24 | rob = Robot(urdf_path=RobLoader().df_path, mesh_folder_path=Path(RobLoader().model_path).parent.parent, name='visual') 25 | scene.add_robot(rob) 26 | 27 | "Modify the configuration of the robot" 28 | input('Press enter to change configuration of the robot') 29 | rob[3] = np.deg2rad(-90) # access by joint id, you can also use scene['visual'][3] = ... 30 | rob['panda_joint5'] = np.deg2rad(90) # access by joint name, you can also use scene['visual']['panda_joint5'] = ... 31 | 32 | input('Press enter to change the pose of the robot') 33 | rob.pos[1] = -1 34 | 35 | input('Press enter to change the color of the robot') 36 | rob.color[0] = 1. 37 | 38 | input('Press enter to add a new robot that shows collision model of the robot') 39 | 40 | col = Robot(urdf_path=RobLoader().df_path, mesh_folder_path=Path(RobLoader().model_path).parent.parent, 41 | show_collision_models=True) 42 | scene.add_robot(col) 43 | col[:] = rob[:] # use the same pose for the collision model 44 | 45 | input('Press enter to hide visual model and exit') 46 | rob.hide() 47 | 48 | time.sleep(1.) # note that some delay is needed to propagate all the changes 49 | -------------------------------------------------------------------------------- /examples/03_animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-10-14 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | import time 9 | import numpy as np 10 | from pathlib import Path 11 | from example_robot_data.robots_loader import PandaLoader 12 | from robomeshcat import Scene, Robot 13 | 14 | "Meshcat can also create animations, that you can rewind forward/backward in the browser. This example shows how to " 15 | "create such an animation, by changing the configuration of the robot." 16 | 17 | scene = Scene() 18 | robot = Robot(urdf_path=PandaLoader().df_path, mesh_folder_path=Path(PandaLoader().model_path).parent.parent) 19 | scene.add_robot(robot) 20 | 21 | with scene.animation(fps=1): # start the animation with the current scene 22 | scene.render() 23 | 24 | robot[3] = np.deg2rad(-90) # modify the scene 25 | scene.render() # store the changes in the frame and start new frame modifications 26 | 27 | robot.opacity = 0.2 28 | scene.render() 29 | 30 | robot.opacity = 1. 31 | scene.render() 32 | 33 | robot.color = [0, 0, 0.7] 34 | scene.render() 35 | 36 | # then let's rotate camera around the robot for 10s; 37 | # note that setting camera will forbid user to rotate the scene 38 | for tt in np.linspace(0, 2 * np.pi, 10): 39 | scene.camera_pos[0] = np.sin(tt) 40 | scene.camera_pos[1] = np.cos(tt) 41 | scene.camera_pos[2] = 1 42 | scene.render() 43 | 44 | scene.reset_camera() # resetting the camera will allow user interaction again 45 | 46 | time.sleep(5) # we need some time to send the animation to the browser 47 | -------------------------------------------------------------------------------- /examples/04_image_and_video.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-11-10 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | from pathlib import Path 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | from robomeshcat import Object, Scene 13 | 14 | example_folder = Path(__file__).parent 15 | 16 | "All elements are stored in Scene that is responsible for rendering them in the MeshCat" 17 | scene = Scene() 18 | 19 | "You can objects into the scene and refer to them through the object instance or by the name" 20 | obj1 = Object.create_sphere(radius=0.1, name='red_sphere', opacity=0.5, color=[1., 0., 0.]) 21 | scene.add_object(obj1) 22 | 23 | scene.render() 24 | input('Rotate the scene as you wish and press enter to capture the image.') 25 | 26 | "Render the image via matplotlib" 27 | img = scene.render_image() 28 | 29 | fig, ax = plt.subplots(1, 1, squeeze=True) # type: plt.Figure, plt.Axes 30 | ax.imshow(img) 31 | plt.show() 32 | 33 | "Store the video" 34 | with scene.video_recording(filename='/tmp/video.mp4', fps=30): 35 | for t in np.linspace(1, 0, 30): 36 | obj1.pos[2] = t 37 | scene.render() 38 | 39 | t = np.linspace(0, 2 * np.pi, 60) 40 | for tt in t: 41 | scene.camera_pos[0] = np.sin(tt) 42 | scene.camera_pos[1] = np.cos(tt) 43 | scene.camera_pos[2] = 1 44 | scene.render() 45 | 46 | scene.reset_camera() 47 | scene.render() 48 | -------------------------------------------------------------------------------- /examples/05_teaser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-11-10 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | import numpy as np 9 | from pathlib import Path 10 | from example_robot_data.robots_loader import TalosFullLoader as TalosLoader 11 | from example_robot_data.robots_loader import PandaLoader 12 | from robomeshcat import Scene, Robot 13 | 14 | scene = Scene() 15 | 16 | panda = Robot(urdf_path=PandaLoader().df_path, mesh_folder_path=Path(PandaLoader().model_path).parent.parent) 17 | scene.add_robot(panda) 18 | panda.pos = [1, 0, 0] 19 | panda[3] = -np.pi/2 20 | panda[5] = np.pi/2 21 | 22 | talos = Robot(urdf_path=TalosLoader().df_path, mesh_folder_path=Path(TalosLoader().model_path).parent.parent) 23 | scene.add_robot(talos) 24 | talos.pos[2] = 1.075 25 | 26 | # with scene.animation(fps=30): 27 | with scene.video_recording(filename='/tmp/teaser.mp4', fps=30): 28 | scene.camera_pos = [2, 0, 2.5] 29 | for t in np.linspace(0., 1., 60): 30 | talos['arm_right_1_joint'] = -t 31 | talos['arm_left_1_joint'] = t 32 | panda[0] = t 33 | panda[2] = t 34 | scene.render() 35 | 36 | for t in np.linspace(0, 2 * np.pi, 60): 37 | scene.camera_pos[0] = 2 * np.cos(t) 38 | scene.camera_pos[1] = 2 * np.sin(t) 39 | talos['head_1_joint'] = t 40 | scene.render() 41 | 42 | -------------------------------------------------------------------------------- /examples/05_teaser_with_human.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-12-8 5 | # Author: Vladimir Petrik 6 | # 7 | import time 8 | 9 | import numpy as np 10 | from pathlib import Path 11 | from example_robot_data.robots_loader import TalosFullLoader as TalosLoader 12 | import pinocchio as pin 13 | import torch 14 | from robomeshcat import Scene, Robot, Human 15 | 16 | scene = Scene() 17 | 18 | smplx_models_path = str(Path(__file__).parent.joinpath('models').joinpath('smplx')) 19 | pose = np.eye(4) 20 | human_default_pose = pin.exp6(np.array([0, 0, 0, np.pi / 2, 0., 0.])).homogeneous 21 | human_default_pose[2, 3] = -.2 22 | human = Human(pose=human_default_pose.copy(), color=[0.6] * 3, model_path=smplx_models_path) 23 | 24 | sad_face = torch.tensor([[-0.4969, -0.2114, 1.5251, 0.1503, 0.4488, 1.7344, 2.1032, -0.3620, -1.2451, 1.8487]]) 25 | smile_face = torch.tensor([[3.4081, -1.1111, -1.4181, 0.5018, 0.0286, -0.5347, -0.0042, 0.1118, -0.2230, -0.1172]]) 26 | neutral_face = torch.tensor([[-0.5131, 1.0546, 0.6059, -0.6452, 2.7049, 0.8512, 0.0777, 0.8451, -1.4651, 0.3700]]) 27 | 28 | human.add_morph(human.get_vertices(expression=sad_face)) 29 | human.add_morph(human.get_vertices(expression=smile_face)) 30 | human.add_morph(human.get_vertices(expression=neutral_face)) 31 | # add some dancing morph 32 | human.add_morph(human.get_vertices(body_pose=torch.randn(human.smplx_model.body_pose.shape) * 0.1)) 33 | human.add_morph(human.get_vertices(body_pose=torch.randn(human.smplx_model.body_pose.shape) * 0.1)) 34 | human.add_morph(human.get_vertices(body_pose=torch.randn(human.smplx_model.body_pose.shape) * 0.1)) 35 | scene.add_object(human) 36 | 37 | talos = Robot(urdf_path=TalosLoader().df_path, mesh_folder_path=Path(TalosLoader().model_path).parent.parent) 38 | scene.add_robot(talos) 39 | talos.pos[0] = 1. 40 | talos.pos[2] = 1.075 41 | talos.rot = pin.utils.rotate('z', np.deg2rad(-90)) 42 | talos.opacity = 0. 43 | 44 | q1 = np.array( 45 | [0.57943216, -0.1309057, -0.75505065, 0.78430028, -0.61956061, -0.27349631, 0.13615252, 0.0711049, -0.03615876, 46 | 0.49826378, 0.17217602, 0.50618769, 0.44123115, -0.02707293, -1.18121182, 0.30893653, 2.01942401, -2.13127587, 47 | -0.10865551, 0.30782173, -0.58293303, -0.23586322, 0.42843663, 0.3494325, 0.52727565, 0.50386685, -0.48822942, 48 | 0.09145592, -0.6189864, -0.09982653, -0.33399487, -0.99386967, -0.78832615, 1.12503886, 0.4816953, -0.33853157, 49 | 0.15645548, 0.77799908, 0.25617193, 0.92783777, -0.06406897, 1.03065562, 0.65546472, 0.28488222]) 50 | 51 | q2 = np.array( 52 | [0.67953954, -0.23498704, -0.30815908, 1.26050064, -0.75429557, 0.39308716, -0.09183746, -0.3519678, 0.6029438, 53 | 1.92670204, -0.85517111, 0.31218583, 1.12134325, -0.08521749, -0.2414049, 0.41116012, 2.19232313, -0.13271861, 54 | 0.13766665, 0.79690452, -0.64291739, -1.02337668, 0.74399798, 0.32299157, 0.25029159, 0.81949992, -0.4262274, 55 | 0.61293056, 0.01760217, -2.08710036, 0.20761188, -0.27267571, 0.2487861, -0.8711323, -0.19324595, -0.19482248, 56 | 0.06016944, 0.13445533, 1.02400687, 0.02380557, -0.13022461, 0.19958255, 0.60717046, 0.81290787]) 57 | 58 | q0 = np.zeros_like(q1) 59 | scene.render() 60 | 61 | with scene.animation(fps=1): 62 | scene.camera_pos = [0, -0.3, 0.2] 63 | scene.render() 64 | human.display_morph(0) 65 | scene.render() 66 | human.display_morph(1) 67 | scene.render() 68 | human.display_morph(2) 69 | human.pos[0] = -.5 70 | human.pos[2] = 1.3 71 | scene.camera_pos = [0, -2., 2.5] 72 | scene.render() 73 | talos.opacity = 1. 74 | scene.render() 75 | 76 | for _ in range(2): 77 | talos[:] = q1 78 | human.display_morph(3) 79 | scene.render() 80 | talos[:] = q2 81 | human.display_morph(4) 82 | scene.render() 83 | talos[:] = q0 84 | human.display_morph(5) 85 | scene.render() 86 | 87 | human.display_morph(None) 88 | human.pose = human_default_pose 89 | scene.camera_pos = [0, -0.3, 0.2] 90 | scene.render() 91 | 92 | time.sleep(3.) 93 | -------------------------------------------------------------------------------- /examples/06_human.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-11-30 5 | # Author: Vladimir Petrik 6 | # 7 | from pathlib import Path 8 | import numpy as np 9 | import pinocchio as pin 10 | import torch 11 | 12 | from robomeshcat import Scene, Human 13 | 14 | "This examples show how to use the human model and how to animate its pose and color. " 15 | "We show three case studies: " 16 | "(i) the first case study shows how to manipulate human online, i.e. without animation" 17 | "(ii) this case study shows how to animate pose and color of the human in case of uniform color" 18 | "(iii) this case study shows how use per vertex color of the human (only in online mode, no animation yet!)" 19 | 20 | case_study = 2 # chose which case study to visualize 21 | scene = Scene() 22 | "Set smplx_models_path to the directory where are your SMPLX models" 23 | smplx_models_path = str(Path(__file__).parent.joinpath('models').joinpath('smplx')) 24 | human_default_pose = pin.exp6(np.array([0, 0, 0, np.pi / 2, 0., 0.])).homogeneous 25 | human_default_pose[2, 3] = 1.2 26 | 27 | if case_study == 0: 28 | "First let's create the human, arguments are forward to smplx constructor, so you can adjust the human model args" 29 | human = Human(pose=human_default_pose, color=[1., 0., 0.], model_path=smplx_models_path) 30 | scene.add_object(human) # add human to the scene, it will be visualized immediately 31 | 32 | input('Press enter to change the body pose and shape of the human') 33 | human.smplx_model.body_pose.data += 0.1 # modify the pose 34 | human.smplx_model.betas.data += 0.1 # modify shape params 35 | human.smplx_model.expression.data += 0.1 # modify expression param 36 | human.update_vertices() # recreate the geometry model to update in the viewer, this is allowed only in online use 37 | 38 | input('Press enter to change the color, opacity, and position of the human') 39 | human.pos[0] = 1. 40 | human.color = [0, 1, 0] 41 | human.opacity = 0.5 42 | input('Press enter to hide the model and exit.') 43 | human.hide() 44 | 45 | elif case_study == 1: 46 | human = Human(pose=human_default_pose, color=[1., 0., 0.], model_path=smplx_models_path) 47 | 48 | # You need to create all the animation poses of the human in advance of adding the human to the scene 49 | # It's called morphologies of the pose 50 | human.smplx_model.betas.data += 1 51 | human.add_morph(human.get_vertices()) # the first morph changes the shape 52 | 53 | human.smplx_model.body_pose.data += 0.1 54 | human.add_morph(human.get_vertices()) # the second morp changes the body pose 55 | 56 | scene.add_object(human) # add human to the scene, no morphology can be added/modified after this step 57 | 58 | "Let's animate" 59 | with scene.animation(fps=1): 60 | human.display_morph(None) # this will display the human shape that is not affected by morphologies 61 | scene.render() 62 | 63 | human.display_morph(0) 64 | scene.render() 65 | 66 | human.display_morph(1) 67 | scene.render() 68 | 69 | human.color = [0, 0.8, 0] 70 | scene.render() 71 | 72 | # You can also change the .pos, .rot, .opacity, .visible, in animation 73 | elif case_study == 2: 74 | # To have per vertex colors, use attribute use_vertex_colors=True 75 | human = Human(pose=human_default_pose, color=[1., 0., 0.], model_path=smplx_models_path, use_vertex_colors=True) 76 | scene.add_object(human) 77 | 78 | input('press enter to change colors to random') 79 | human.update_vertices(vertices_colors=np.random.rand(human.smplx_model.get_num_verts(), 3)) 80 | 81 | input('press enter to change colors to blue') 82 | human.update_vertices(vertices_colors=[[0., 0., 0.75]] * human.smplx_model.get_num_verts()) 83 | 84 | input('press enter to display wireframe') 85 | human._show_wireframe = True 86 | human.update_vertices(vertices_colors=[[0., 0., 0.]] * human.smplx_model.get_num_verts()) 87 | 88 | input('sample new expressions and store them into video, rotate manually to see the face') 89 | # human.update_vertices(vertices=human.get_vertices(betas=torch.randn([1, 10]))) 90 | human._show_wireframe = False 91 | human._vertex_colors[:] = 0.6 92 | with scene.video_recording(filename='/tmp/face_expression.mp4', fps=1): 93 | for _ in range(10): 94 | human.update_vertices(vertices=human.get_vertices(expression=torch.randn([1, 10]))) 95 | scene.render() 96 | -------------------------------------------------------------------------------- /examples/spade.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.81 (sub 16) OBJ File: 'spade.blend' 2 | # www.blender.org 3 | mtllib spade.mtl 4 | o Spade_hull_1 5 | v 14.513958 12.500002 -4.176317 6 | v -46.748081 -12.499999 -72.906143 7 | v -46.748081 -12.500001 -17.268295 8 | v 47.422077 -12.500001 -17.268295 9 | v 51.762924 14.371905 -76.161461 10 | v -51.088928 14.371905 -76.161461 11 | v 47.422077 -12.499999 -72.906143 12 | v -48.743149 14.371903 -13.053298 13 | v 51.762924 14.371903 -15.394273 14 | v -14.642105 -12.500002 -2.285420 15 | v 15.316101 -12.500002 -2.285420 16 | v -39.400303 14.371902 -8.378899 17 | v 44.745724 14.371902 -10.712322 18 | v -51.088928 14.371903 -15.394273 19 | v -13.839962 12.500002 -4.176317 20 | v -36.046089 -12.500002 -10.845088 21 | v 45.274303 -12.500001 -15.124923 22 | vt 0.931774 0.885932 23 | vt 0.645638 1.000000 24 | vt 0.936913 0.826202 25 | vt 1.000000 0.000000 26 | vt 0.042205 0.044065 27 | vt 0.000000 0.000000 28 | vt 0.042205 0.797189 29 | vt 0.957795 0.797189 30 | vt 0.957795 0.044065 31 | vt 0.022807 0.854244 32 | vt 1.000000 0.822556 33 | vt 0.637839 0.974404 34 | vt 0.354362 1.000000 35 | vt 0.113645 0.917517 36 | vt 0.000000 0.822556 37 | vt 0.362161 0.974404 38 | vt 0.146257 0.884135 39 | vn 0.3899 -0.1417 0.9099 40 | vn 0.0000 -0.1203 -0.9927 41 | vn -0.9872 -0.1595 -0.0000 42 | vn 0.9872 -0.1595 -0.0000 43 | vn 0.0000 1.0000 0.0000 44 | vn 0.0000 0.0754 0.9972 45 | vn 0.2059 0.0804 0.9753 46 | vn 0.0087 0.9500 0.3121 47 | vn -0.6971 -0.1613 0.6986 48 | vn 0.0000 0.9135 0.4069 49 | vn -0.1561 0.0795 0.9845 50 | vn -0.0000 -1.0000 -0.0000 51 | vn -0.5071 -0.1702 0.8449 52 | vn -0.4432 -0.1366 0.8859 53 | vn -0.3681 -0.1304 0.9206 54 | vn 0.6971 -0.1613 0.6986 55 | vn 0.5507 -0.1247 0.8253 56 | g Spade_hull_1_Spade_hull_1_Material.001 57 | usemtl Material.001 58 | s off 59 | f 13/1/1 11/2/1 17/3/1 60 | f 5/4/2 2/5/2 6/6/2 61 | f 2/5/3 3/7/3 6/6/3 62 | f 5/4/4 4/8/4 7/9/4 63 | f 2/5/2 5/4/2 7/9/2 64 | f 5/4/5 6/6/5 8/10/5 65 | f 4/8/4 5/4/4 9/11/4 66 | f 5/4/5 8/10/5 9/11/5 67 | f 1/12/6 10/13/6 11/2/6 68 | f 9/11/5 8/10/5 12/14/5 69 | f 1/12/7 11/2/7 13/1/7 70 | f 12/14/8 1/12/8 13/1/8 71 | f 9/11/5 12/14/5 13/1/5 72 | f 6/6/3 3/7/3 14/15/3 73 | f 3/7/9 8/10/9 14/15/9 74 | f 8/10/5 6/6/5 14/15/5 75 | f 10/13/6 1/12/6 15/16/6 76 | f 1/12/10 12/14/10 15/16/10 77 | f 12/14/11 10/13/11 15/16/11 78 | f 3/7/12 2/5/12 16/17/12 79 | f 8/10/13 3/7/13 16/17/13 80 | f 2/5/12 10/13/12 16/17/12 81 | f 12/14/14 8/10/14 16/17/14 82 | f 10/13/15 12/14/15 16/17/15 83 | f 2/5/12 7/9/12 17/3/12 84 | f 7/9/12 4/8/12 17/3/12 85 | f 4/8/16 9/11/16 17/3/16 86 | f 10/13/12 2/5/12 17/3/12 87 | f 11/2/12 10/13/12 17/3/12 88 | f 9/11/17 13/1/17 17/3/17 89 | o Spade_hull_2 90 | v -13.890795 7.457798 -0.596692 91 | v 14.364998 5.018061 223.095016 92 | v 15.084849 -4.570136 228.793839 93 | v -14.683066 -7.166142 230.948853 94 | v 7.350998 -13.684101 0.985001 95 | v 12.128835 9.777925 0.000000 96 | v -4.337171 14.371893 223.095016 97 | v -13.890795 -6.769997 -0.596692 98 | v -6.677002 -13.684111 223.095016 99 | v -4.337171 14.371902 0.985002 100 | v 14.603409 -4.409708 -0.902424 101 | v 13.086816 -9.856565 233.182266 102 | v 5.011167 14.371893 223.095016 103 | v -13.691002 7.357893 223.095016 104 | v -6.677002 -13.684101 0.985001 105 | v 7.350998 -13.684111 223.095016 106 | v 5.011167 14.371902 0.985002 107 | v 14.603409 5.097509 -0.902424 108 | v 9.688083 12.032062 223.095016 109 | v -9.097026 12.135739 0.000001 110 | v -9.014087 12.032062 223.095016 111 | v 12.128835 -9.090124 -0.000000 112 | v 9.771022 12.135739 0.000001 113 | v 12.025167 9.694977 223.095016 114 | vt 0.003855 0.821491 115 | vt 0.956908 0.818705 116 | vt 0.956908 0.897215 117 | vt 0.990459 0.000000 118 | vt 0.001306 0.026615 119 | vt 0.001306 0.026615 120 | vt 0.000000 0.983827 121 | vt 0.956908 0.347552 122 | vt 1.000000 0.932880 123 | vt 0.956908 0.268949 124 | vt 0.981253 1.000000 125 | vt 0.008063 0.347552 126 | vt 0.956908 0.661593 127 | vt 0.956908 0.033327 128 | vt 0.008063 0.268949 129 | vt 0.008063 0.740195 130 | vt 0.956908 0.740195 131 | vt 0.008063 0.661593 132 | vt 0.956908 0.975818 133 | vt 0.000000 0.983827 134 | vt 0.003855 0.187653 135 | vt 0.956908 0.190439 136 | vt 0.003855 0.900698 137 | vt 0.003855 0.900698 138 | vn 0.7071 0.7071 0.0006 139 | vn -1.0000 -0.0000 -0.0034 140 | vn -0.0107 0.0000 -0.9999 141 | vn -0.0402 0.3593 0.9324 142 | vn -0.1243 -0.8221 0.5556 143 | vn 0.9348 -0.3551 -0.0022 144 | vn 0.0000 1.0000 0.0000 145 | vn 0.0000 0.3844 0.9232 146 | vn -0.9976 0.0686 0.0009 147 | vn -0.6915 -0.7223 -0.0036 148 | vn 0.0000 -0.2230 -0.9748 149 | vn -0.6314 -0.7755 -0.0000 150 | vn 0.0000 -1.0000 -0.0000 151 | vn 0.0000 -0.9350 0.3548 152 | vn 0.5551 -0.8318 -0.0000 153 | vn 0.9971 0.0755 0.0011 154 | vn 1.0000 -0.0000 -0.0021 155 | vn 0.2182 0.4362 0.8730 156 | vn -0.4252 0.9051 0.0000 157 | vn -0.0001 0.1267 -0.9919 158 | vn -0.2116 0.4229 0.8811 159 | vn -0.7069 0.7073 0.0009 160 | vn -0.4484 0.4487 0.7731 161 | vn -0.4474 0.8943 0.0006 162 | vn -0.6984 0.7157 0.0006 163 | vn 0.0034 -0.2130 -0.9770 164 | vn 0.0054 -0.1921 -0.9814 165 | vn 0.6925 -0.7214 -0.0052 166 | vn 0.8836 -0.4682 -0.0052 167 | vn 0.0000 0.4031 -0.9152 168 | vn 0.4252 0.9051 0.0000 169 | vn 0.3541 0.3541 -0.8656 170 | vn 0.4474 0.8943 0.0006 171 | vn 0.0000 0.1272 -0.9919 172 | vn 0.7533 0.3769 0.5389 173 | vn 0.5363 0.4098 0.7378 174 | vn 0.8943 0.4474 0.0011 175 | vn 0.8841 0.4673 0.0006 176 | vn 0.4319 0.4319 0.7917 177 | g Spade_hull_2_Spade_hull_2_None 178 | usemtl None 179 | s off 180 | f 40/18/18 36/19/18 41/20/18 181 | f 21/21/19 18/22/19 25/23/19 182 | f 25/23/20 18/22/20 28/24/20 183 | f 24/25/21 21/21/21 29/26/21 184 | f 21/21/22 26/27/22 29/26/22 185 | f 28/24/23 20/28/23 29/26/23 186 | f 27/29/24 24/25/24 30/30/24 187 | f 24/25/25 29/26/25 30/30/25 188 | f 18/22/26 21/21/26 31/31/26 189 | f 21/21/27 25/23/27 32/32/27 190 | f 25/23/28 22/33/28 32/32/28 191 | f 26/27/29 21/21/29 32/32/29 192 | f 22/33/30 26/27/30 32/32/30 193 | f 26/27/30 22/33/30 33/34/30 194 | f 29/26/31 26/27/31 33/34/31 195 | f 22/33/32 29/26/32 33/34/32 196 | f 27/29/24 30/30/24 34/35/24 197 | f 19/36/33 20/28/33 35/37/33 198 | f 20/28/34 28/24/34 35/37/34 199 | f 28/24/20 18/22/20 35/37/20 200 | f 30/30/35 29/26/35 36/19/35 201 | f 24/25/36 27/29/36 37/38/36 202 | f 35/37/37 18/22/37 37/38/37 203 | f 21/21/38 24/25/38 38/39/38 204 | f 18/22/39 31/31/39 38/39/39 205 | f 31/31/40 21/21/40 38/39/40 206 | f 24/25/41 37/38/41 38/39/41 207 | f 37/38/42 18/22/42 38/39/42 208 | f 22/33/43 25/23/43 39/40/43 209 | f 25/23/44 28/24/44 39/40/44 210 | f 29/26/45 22/33/45 39/40/45 211 | f 28/24/46 29/26/46 39/40/46 212 | f 27/29/47 34/35/47 40/18/47 213 | f 34/35/48 30/30/48 40/18/48 214 | f 23/41/49 35/37/49 40/18/49 215 | f 30/30/50 36/19/50 40/18/50 216 | f 37/38/47 27/29/47 40/18/47 217 | f 35/37/51 37/38/51 40/18/51 218 | f 20/28/52 19/36/52 41/20/52 219 | f 29/26/53 20/28/53 41/20/53 220 | f 19/36/54 35/37/54 41/20/54 221 | f 35/37/55 23/41/55 41/20/55 222 | f 36/19/56 29/26/56 41/20/56 223 | f 23/41/18 40/18/18 41/20/18 224 | o Spade_hull_3 225 | v 77.490997 16.702112 262.835297 226 | v 16.708946 -41.740112 255.821991 227 | v 16.708946 5.011197 230.113831 228 | v 14.980774 -18.823496 280.796783 229 | v 75.146820 -23.035883 276.861938 230 | v 61.117447 21.385893 279.201538 231 | v 21.385403 -4.344008 225.440125 232 | v 35.414776 -41.740112 258.156097 233 | v 68.132133 -30.049196 279.201538 234 | v 20.606052 7.141351 249.118271 235 | v 72.808594 9.688798 255.821991 236 | v 77.490997 21.385893 276.861938 237 | v 18.206045 -29.070272 277.819611 238 | v 16.708946 0.333594 223.095032 239 | v 77.490997 -16.016390 279.201538 240 | v 35.414776 -41.740112 262.835297 241 | v 30.738319 5.011197 230.113831 242 | v 30.738319 -39.398224 253.482391 243 | v 70.470360 -2.002120 260.495697 244 | v 56.440987 -34.720619 274.522339 245 | v 77.490997 12.024509 260.495697 246 | v 72.808594 14.366400 255.821991 247 | v 61.117447 21.385893 276.861938 248 | v 19.047174 -2.002118 223.095032 249 | v 16.708946 -25.371593 241.806351 250 | v 63.455673 -32.384911 274.522339 251 | v 54.102760 5.011196 244.140457 252 | v 26.061861 -6.679720 230.113831 253 | v 26.304573 9.573268 251.148285 254 | vt 1.000000 0.738066 255 | vt 1.000000 0.738066 256 | vt 0.812872 0.181151 257 | vt 0.740603 0.027646 258 | vt 0.363030 0.000000 259 | vt 0.774347 0.089990 260 | vt 0.000000 0.027646 261 | vt 0.200707 0.051596 262 | vt 0.185200 0.850283 263 | vt 0.666504 0.027646 264 | vt 0.407498 1.000000 265 | vt 0.296300 0.962499 266 | vt 0.925803 1.000000 267 | vt 1.000000 1.000000 268 | vt 0.000000 0.326891 269 | vt 0.000000 0.326891 270 | vt 0.740603 0.252079 271 | vt 0.592404 0.102457 272 | vt 0.037099 0.252079 273 | vt 0.629503 0.887688 274 | vt 0.814702 0.925094 275 | vt 0.111198 0.663255 276 | vt 0.851703 1.000000 277 | vt 0.888802 0.925094 278 | vt 0.629503 0.065052 279 | vt 0.259299 0.027646 280 | vt 0.148199 0.775472 281 | vt 0.740603 0.625849 282 | vt 0.555403 0.177268 283 | vn -0.3213 0.9470 0.0000 284 | vn -0.9152 0.3758 0.1455 285 | vn -0.5763 0.6793 0.4544 286 | vn -0.8940 -0.3592 0.2677 287 | vn -0.0321 -0.2882 0.9570 288 | vn -0.9994 0.0300 -0.0200 289 | vn 0.0250 0.0110 0.9996 290 | vn 0.0263 -0.0175 0.9995 291 | vn 0.6398 -0.4267 0.6392 292 | vn 1.0000 -0.0000 0.0000 293 | vn 0.1412 0.0618 0.9881 294 | vn 0.0000 -1.0000 0.0000 295 | vn -0.1874 -0.8456 0.4998 296 | vn 0.0000 0.8321 -0.5546 297 | vn 0.5072 -0.4518 -0.7339 298 | vn 0.0619 -0.8662 -0.4959 299 | vn 0.5432 -0.4370 -0.7169 300 | vn 0.6127 -0.3960 -0.6840 301 | vn -0.0337 -0.6639 0.7470 302 | vn -0.0663 -0.7982 0.5987 303 | vn 0.7444 -0.3227 -0.5846 304 | vn 0.9281 -0.2067 -0.3098 305 | vn 0.0013 0.9485 -0.3167 306 | vn 0.0000 0.9397 -0.3420 307 | vn 0.7449 0.2985 -0.5967 308 | vn 0.7064 0.0000 -0.7078 309 | vn 0.0000 1.0000 0.0000 310 | vn 0.0000 0.9486 -0.3165 311 | vn -0.0105 0.9468 -0.3217 312 | vn 0.3313 0.3317 -0.8833 313 | vn 0.2174 -0.5737 -0.7897 314 | vn 0.0225 -0.6262 -0.7793 315 | vn -0.9987 -0.0331 -0.0387 316 | vn -0.9988 -0.0287 -0.0394 317 | vn -0.0181 -0.6503 -0.7595 318 | vn -0.5068 -0.5073 -0.6970 319 | vn 0.5524 -0.5297 -0.6436 320 | vn 0.6336 -0.7244 -0.2717 321 | vn 0.3165 -0.9486 0.0000 322 | vn 0.3121 -0.9372 0.1559 323 | vn 0.3159 -0.9488 0.0015 324 | vn 0.5552 -0.4021 -0.7281 325 | vn 0.5143 0.0414 -0.8566 326 | vn 0.5297 0.0000 -0.8482 327 | vn 0.5347 -0.2684 -0.8012 328 | vn 0.5147 0.0000 -0.8574 329 | vn 0.5079 -0.4514 -0.7337 330 | vn 0.5395 -0.4334 -0.7219 331 | vn 0.5383 -0.4032 -0.7401 332 | vn 0.5407 -0.4276 -0.7244 333 | vn -0.3850 0.9226 -0.0245 334 | vn -0.4378 0.8825 0.1717 335 | vn -0.2597 0.9615 -0.0900 336 | g Spade_hull_3_Spade_hull_3_None 337 | usemtl None 338 | s off 339 | f 47/42/57 64/43/57 70/44/57 340 | f 44/45/58 45/46/58 51/47/58 341 | f 45/46/59 47/42/59 51/47/59 342 | f 45/46/60 43/48/60 54/49/60 343 | f 50/50/61 45/46/61 54/49/61 344 | f 45/46/62 44/45/62 55/51/62 345 | f 47/42/63 45/46/63 56/52/63 346 | f 45/46/64 50/50/64 56/52/64 347 | f 50/50/65 46/53/65 56/52/65 348 | f 42/54/66 53/55/66 56/52/66 349 | f 53/55/67 47/42/67 56/52/67 350 | f 43/48/68 49/56/68 57/57/68 351 | f 54/49/69 43/48/69 57/57/69 352 | f 55/51/70 44/45/70 58/58/70 353 | f 48/59/71 49/56/71 59/60/71 354 | f 49/56/72 43/48/72 59/60/72 355 | f 46/53/73 49/56/73 60/61/73 356 | f 52/62/74 46/53/74 60/61/74 357 | f 50/50/75 54/49/75 61/63/75 358 | f 54/49/76 57/57/76 61/63/76 359 | f 46/53/77 52/62/77 62/64/77 360 | f 42/54/66 56/52/66 62/64/66 361 | f 56/52/78 46/53/78 62/64/78 362 | f 53/55/79 42/54/79 63/65/79 363 | f 58/58/80 44/45/80 63/65/80 364 | f 42/54/81 62/64/81 63/65/81 365 | f 62/64/82 52/62/82 63/65/82 366 | f 47/42/83 53/55/83 64/43/83 367 | f 53/55/84 63/65/84 64/43/84 368 | f 63/65/85 44/45/85 64/43/85 369 | f 55/51/86 58/58/86 65/66/86 370 | f 48/59/87 59/60/87 65/66/87 371 | f 65/66/88 59/60/88 66/67/88 372 | f 43/48/89 45/46/89 66/67/89 373 | f 45/46/90 55/51/90 66/67/90 374 | f 59/60/91 43/48/91 66/67/91 375 | f 55/51/92 65/66/92 66/67/92 376 | f 49/56/93 46/53/93 67/68/93 377 | f 46/53/94 50/50/94 67/68/94 378 | f 57/57/95 49/56/95 67/68/95 379 | f 50/50/96 61/63/96 67/68/96 380 | f 61/63/97 57/57/97 67/68/97 381 | f 52/62/98 60/61/98 68/69/98 382 | f 58/58/99 63/65/99 68/69/99 383 | f 63/65/100 52/62/100 68/69/100 384 | f 48/59/101 65/66/101 68/69/101 385 | f 65/66/102 58/58/102 68/69/102 386 | f 49/56/103 48/59/103 69/70/103 387 | f 60/61/104 49/56/104 69/70/104 388 | f 48/59/105 68/69/105 69/70/105 389 | f 68/69/106 60/61/106 69/70/106 390 | f 44/45/107 51/47/107 70/44/107 391 | f 51/47/108 47/42/108 70/44/108 392 | f 64/43/109 44/45/109 70/44/109 393 | o Spade_hull_4 394 | v -27.721521 2.681664 227.780426 395 | v -76.817001 -20.693991 276.859650 396 | v -76.817001 -20.693991 279.201782 397 | v -18.367004 -41.740112 255.822693 398 | v -60.448029 21.385893 279.201782 399 | v -16.657425 -18.836853 280.774445 400 | v -76.817001 14.366400 258.159546 401 | v -18.367004 9.688798 251.154251 402 | v -34.735977 -41.740112 258.159546 403 | v -18.367004 -4.344008 225.433044 404 | v -55.767906 -34.720619 274.522797 405 | v -76.817001 21.385893 276.859650 406 | v -18.367004 5.011197 230.117279 407 | v -34.735977 0.333594 232.454132 408 | v -19.834827 -29.051123 277.851501 409 | v -69.802544 -30.049196 279.201782 410 | v -76.817001 9.688798 258.159546 411 | v -34.735977 -41.740112 262.833252 412 | v -27.721521 -27.707306 244.143677 413 | v -69.802544 -30.049196 276.859650 414 | v -72.136887 14.366400 255.822693 415 | v -60.448029 21.385893 276.859650 416 | v -23.047123 -2.002118 225.433044 417 | v -23.047123 2.681664 225.433044 418 | v -30.061581 -39.398224 253.485840 419 | v -34.735977 -34.720619 253.485840 420 | v -18.367004 -25.371593 241.806824 421 | vt 0.037099 0.777190 422 | vt 0.222298 0.816088 423 | vt 0.259299 0.971583 424 | vt 1.000000 0.272093 425 | vt 0.333399 0.000000 426 | vt 0.362818 1.000000 427 | vt 0.333399 0.000000 428 | vt 0.888802 0.000000 429 | vt 0.814702 0.971583 430 | vt 1.000000 0.000000 431 | vt 0.740603 0.971583 432 | vt 0.592404 0.971583 433 | vt 0.000000 0.971583 434 | vt 0.201011 0.947184 435 | vt 0.111198 0.349888 436 | vt 0.185200 0.116598 437 | vt 0.814702 0.000000 438 | vt 0.703700 0.816088 439 | vt 0.666504 0.699490 440 | vt 0.000000 0.699490 441 | vt 0.000000 0.699490 442 | vt 0.185200 0.116598 443 | vt 0.888802 0.077795 444 | vt 1.000000 0.272093 445 | vt 0.629503 0.893788 446 | vt 0.703700 0.893788 447 | vt 0.111198 0.699490 448 | vn -0.0415 -0.6186 -0.7846 449 | vn -0.0264 0.0103 0.9996 450 | vn -1.0000 0.0000 0.0000 451 | vn 0.5476 0.6183 0.5638 452 | vn -0.1414 0.0550 0.9884 453 | vn 0.9988 0.0486 -0.0108 454 | vn 0.9995 0.0137 -0.0273 455 | vn 0.8970 -0.3546 0.2640 456 | vn 0.0335 -0.6553 0.7546 457 | vn -0.8001 -0.5999 0.0000 458 | vn -0.0255 -0.0191 0.9995 459 | vn 0.0316 -0.2840 0.9583 460 | vn -0.5262 0.0000 -0.8504 461 | vn -0.5330 -0.0838 -0.8420 462 | vn 0.0000 -1.0000 0.0000 463 | vn 0.2134 -0.8403 0.4983 464 | vn 0.0696 -0.7950 0.6026 465 | vn -0.3147 -0.9492 0.0039 466 | vn -0.3163 -0.9487 0.0000 467 | vn -0.5730 -0.4296 -0.6980 468 | vn -0.1728 0.9221 -0.3461 469 | vn 0.0108 0.9467 -0.3219 470 | vn 0.2678 0.9635 0.0000 471 | vn 0.0000 1.0000 0.0000 472 | vn 0.1374 0.9669 -0.2150 473 | vn 0.0000 0.9486 -0.3165 474 | vn -0.5273 -0.1035 -0.8434 475 | vn -0.2676 -0.5348 -0.8015 476 | vn -0.5258 -0.4360 -0.7304 477 | vn -0.4111 0.4011 -0.8186 478 | vn 0.5578 0.3716 -0.7421 479 | vn -0.4074 0.4104 -0.8158 480 | vn -0.0393 0.9098 -0.4132 481 | vn -0.4488 0.0000 -0.8936 482 | vn 0.0000 0.0000 -1.0000 483 | vn -0.0719 -0.8611 -0.5033 484 | vn -0.4850 -0.4846 -0.7280 485 | vn -0.5380 -0.4337 -0.7228 486 | vn -0.5316 -0.4358 -0.7263 487 | vn -0.5322 -0.4692 -0.7047 488 | vn -0.5389 -0.4347 -0.7216 489 | vn -0.4850 -0.4847 -0.7279 490 | vn 0.9987 -0.0328 -0.0383 491 | vn 0.9988 -0.0302 -0.0388 492 | vn -0.0437 -0.6138 -0.7883 493 | vn 0.0215 -0.6503 -0.7594 494 | g Spade_hull_4_Spade_hull_4_None 495 | usemtl None 496 | s off 497 | f 95/71/110 89/72/110 97/73/110 498 | f 75/74/111 73/75/111 76/76/111 499 | f 72/77/112 73/75/112 77/78/112 500 | f 75/74/113 76/76/113 78/79/113 501 | f 73/75/114 75/74/114 82/80/114 502 | f 77/78/112 73/75/112 82/80/112 503 | f 78/79/115 76/76/115 83/81/115 504 | f 76/76/116 80/82/116 83/81/116 505 | f 74/83/117 76/76/117 85/84/117 506 | f 81/85/118 85/84/118 86/86/118 507 | f 73/75/119 72/77/119 86/86/119 508 | f 76/76/120 73/75/120 86/86/120 509 | f 85/84/121 76/76/121 86/86/121 510 | f 72/77/112 77/78/112 87/87/112 511 | f 77/78/122 71/88/122 87/87/122 512 | f 71/88/123 84/89/123 87/87/123 513 | f 79/90/124 74/83/124 88/91/124 514 | f 74/83/125 85/84/125 88/91/125 515 | f 85/84/126 81/85/126 88/91/126 516 | f 81/85/127 86/86/127 88/91/127 517 | f 86/86/128 79/90/128 88/91/128 518 | f 86/86/119 72/77/119 90/92/119 519 | f 79/90/128 86/86/128 90/92/128 520 | f 72/77/129 87/87/129 90/92/129 521 | f 77/78/130 82/80/130 91/93/130 522 | f 83/81/131 91/93/131 92/94/131 523 | f 75/74/132 78/79/132 92/94/132 524 | f 82/80/133 75/74/133 92/94/133 525 | f 78/79/134 83/81/134 92/94/134 526 | f 91/93/135 82/80/135 92/94/135 527 | f 84/89/136 71/88/136 93/95/136 528 | f 80/82/137 89/72/137 93/95/137 529 | f 89/72/138 84/89/138 93/95/138 530 | f 71/88/139 77/78/139 94/96/139 531 | f 83/81/140 80/82/140 94/96/140 532 | f 77/78/141 91/93/141 94/96/141 533 | f 91/93/142 83/81/142 94/96/142 534 | f 93/95/143 71/88/143 94/96/143 535 | f 80/82/144 93/95/144 94/96/144 536 | f 74/83/145 79/90/145 95/71/145 537 | f 89/72/146 95/71/146 96/97/146 538 | f 87/87/147 84/89/147 96/97/147 539 | f 84/89/148 89/72/148 96/97/148 540 | f 79/90/149 90/92/149 96/97/149 541 | f 90/92/150 87/87/150 96/97/150 542 | f 95/71/151 79/90/151 96/97/151 543 | f 76/76/152 74/83/152 97/73/152 544 | f 80/82/153 76/76/153 97/73/153 545 | f 89/72/154 80/82/154 97/73/154 546 | f 74/83/155 95/71/155 97/73/155 547 | o Spade_hull_5 548 | v -18.367002 -2.001431 223.095032 549 | v -18.367002 -41.740112 255.821991 550 | v 16.702997 -41.740112 255.821991 551 | v 16.702997 9.686740 244.140457 552 | v -18.367002 9.686740 248.819672 553 | v 7.348480 -13.684109 223.095032 554 | v 17.862478 -18.667355 281.056519 555 | v 5.010709 14.371893 260.495697 556 | v -19.526440 -18.667343 281.056458 557 | v 9.682818 12.032064 223.095032 558 | v 16.702997 -2.001431 223.095032 559 | v -17.413399 -29.156620 277.675934 560 | v 15.749385 -29.156610 277.675903 561 | v -4.333510 14.371893 260.495697 562 | v -4.015960 11.827512 225.639160 563 | v -6.674714 -13.684109 223.095032 564 | v -13.684594 7.357894 223.095032 565 | v 16.702997 9.686740 246.480072 566 | v 14.870256 -38.807728 261.613770 567 | vt 0.941675 0.224257 568 | vt 0.941675 0.224257 569 | vt 0.664558 0.052260 570 | vt 0.564633 0.000000 571 | vt 0.564633 0.000000 572 | vt 0.000000 0.500000 573 | vt 0.000000 0.708203 574 | vt 0.999999 0.411191 575 | vt 0.443823 0.916504 576 | vt 0.000000 0.958301 577 | vt 0.645267 1.000000 578 | vt 0.363093 0.916504 579 | vt 0.000000 0.708203 580 | vt 1.000000 0.411191 581 | vt 0.645267 1.000000 582 | vt 0.043894 0.954655 583 | vt 0.000000 0.500000 584 | vt 0.000000 0.875000 585 | vt 0.403458 0.916504 586 | vn 0.0000 -0.8572 0.5150 587 | vn 0.0000 -0.7592 -0.6508 588 | vn -0.9994 -0.0216 -0.0262 589 | vn -0.9996 0.0270 -0.0122 590 | vn 0.0000 0.0000 -1.0000 591 | vn 0.3548 0.9348 -0.0142 592 | vn 0.6218 -0.4979 -0.6046 593 | vn 0.9994 -0.0216 -0.0262 594 | vn 0.9996 0.0237 -0.0132 595 | vn 0.8679 0.4342 -0.2411 596 | vn 0.0000 -0.3067 0.9518 597 | vn -0.9498 -0.2513 0.1862 598 | vn 0.9498 -0.2513 0.1862 599 | vn 0.0000 -0.3068 0.9518 600 | vn 0.0000 0.5284 0.8490 601 | vn -0.6206 0.5997 0.5052 602 | vn 0.0000 0.9980 -0.0624 603 | vn -0.0284 0.9969 -0.0730 604 | vn -0.2609 0.9626 -0.0726 605 | vn -0.5362 -0.5366 -0.6516 606 | vn -0.8764 0.4385 -0.1992 607 | vn -0.1403 0.7013 -0.6989 608 | vn -0.3815 0.9118 -0.1520 609 | vn 0.9992 0.0409 0.0000 610 | vn 0.3720 0.9283 0.0000 611 | vn 0.7245 0.5447 0.4224 612 | vn 0.0000 -0.8922 0.4517 613 | vn -0.0106 -0.8664 0.4993 614 | vn 0.2146 -0.8423 0.4944 615 | g Spade_hull_5_Spade_hull_5_None 616 | usemtl None 617 | s off 618 | f 110/98/156 109/99/156 116/100/156 619 | f 100/101/157 99/102/157 103/103/157 620 | f 98/104/158 99/102/158 106/105/158 621 | f 102/106/159 98/104/159 106/105/159 622 | f 103/103/160 98/104/160 107/107/160 623 | f 105/108/161 101/109/161 107/107/161 624 | f 100/101/162 103/103/162 108/110/162 625 | f 104/111/163 100/101/163 108/110/163 626 | f 101/109/164 104/111/164 108/110/164 627 | f 107/107/165 101/109/165 108/110/165 628 | f 103/103/160 107/107/160 108/110/160 629 | f 104/111/166 106/105/166 109/99/166 630 | f 106/105/167 99/102/167 109/99/167 631 | f 100/101/168 104/111/168 110/98/168 632 | f 104/111/169 109/99/169 110/98/169 633 | f 104/111/170 105/108/170 111/112/170 634 | f 106/105/170 104/111/170 111/112/170 635 | f 102/106/171 106/105/171 111/112/171 636 | f 105/108/172 107/107/172 111/112/172 637 | f 111/112/173 107/107/173 112/113/173 638 | f 102/106/174 111/112/174 112/113/174 639 | f 99/102/175 98/104/175 113/114/175 640 | f 98/104/160 103/103/160 113/114/160 641 | f 103/103/157 99/102/157 113/114/157 642 | f 98/104/176 102/106/176 114/115/176 643 | f 107/107/160 98/104/160 114/115/160 644 | f 112/113/177 107/107/177 114/115/177 645 | f 102/106/178 112/113/178 114/115/178 646 | f 104/111/179 101/109/179 115/116/179 647 | f 101/109/180 105/108/180 115/116/180 648 | f 105/108/181 104/111/181 115/116/181 649 | f 99/102/182 100/101/182 116/100/182 650 | f 109/99/183 99/102/183 116/100/183 651 | f 100/101/184 110/98/184 116/100/184 652 | o Spade_hull_6 653 | v 16.702995 37.746849 393.750061 654 | v -41.747002 -30.050114 279.218475 655 | v 16.702995 -30.050114 279.218475 656 | v 16.702995 47.099770 384.402191 657 | v -41.747002 49.441891 389.081848 658 | v -40.020741 -16.607862 277.884308 659 | v -41.747002 33.062614 386.736298 660 | v 16.702995 -13.678620 283.898132 661 | v 16.702995 -27.707998 286.232239 662 | v -41.747002 -27.707998 286.232239 663 | v -41.747002 40.081188 396.095642 664 | v 16.702995 49.441891 389.081848 665 | v 16.702995 7.361547 344.653625 666 | v 16.702995 -18.355078 279.218475 667 | v -41.868546 -16.127695 278.975433 668 | v -41.747002 47.099770 384.402191 669 | v 16.702995 40.081188 396.095642 670 | v 16.702995 -1.991367 302.605316 671 | v -41.747002 7.361547 344.653625 672 | v 16.702995 33.062614 386.736298 673 | v -41.747002 -30.050114 281.563995 674 | vt 0.070619 0.029464 675 | vt 0.009230 0.175142 676 | vt 0.031128 0.000000 677 | vt 0.980158 0.852878 678 | vt 0.011286 0.000000 679 | vt 0.901080 0.970536 680 | vt 0.011286 0.000000 681 | vt 0.000000 0.169102 682 | vt 0.050874 0.205951 683 | vt 0.070619 0.029464 684 | vt 0.920825 0.793951 685 | vt 1.000000 0.882244 686 | vt 0.940667 1.000000 687 | vt 0.940667 1.000000 688 | vt 0.564830 0.470634 689 | vt 0.011286 0.147122 690 | vt 0.901080 0.970536 691 | vt 1.000000 0.882244 692 | vt 0.209126 0.352976 693 | vt 0.564830 0.470634 694 | vt 0.920825 0.793951 695 | vn -1.0000 -0.0080 0.0040 696 | vn 1.0000 0.0000 0.0000 697 | vn 0.0000 -0.0988 -0.9951 698 | vn -0.0079 -0.8000 0.5999 699 | vn 0.0000 0.8943 -0.4476 700 | vn 0.0000 0.5996 0.8003 701 | vn 0.0000 -0.8574 0.5147 702 | vn 0.0235 0.0000 -0.9997 703 | vn 0.0384 0.7068 -0.7063 704 | vn -0.5121 -0.0195 -0.8587 705 | vn -1.0000 0.0006 0.0008 706 | vn -1.0000 -0.0038 0.0029 707 | vn 0.0000 0.8582 -0.5133 708 | vn -0.0805 0.8549 -0.5126 709 | vn -0.9999 0.0116 -0.0058 710 | vn 0.0000 -0.7088 0.7054 711 | vn 0.0033 0.8574 -0.5146 712 | vn 0.0124 0.8480 -0.5298 713 | vn 0.0000 -0.8534 0.5212 714 | vn -1.0000 -0.0073 0.0045 715 | vn -1.0000 -0.0076 0.0046 716 | vn 0.0000 -0.8316 0.5554 717 | vn 0.0000 -1.0000 0.0000 718 | vn 0.0127 -0.9484 0.3167 719 | vn 0.0000 -0.8938 0.4484 720 | vn -1.0000 -0.0087 0.0000 721 | g Spade_hull_6_Spade_hull_6_None 722 | usemtl None 723 | s off 724 | f 126/117/185 131/118/185 137/119/185 725 | f 117/120/186 119/121/186 120/122/186 726 | f 119/121/187 118/123/187 122/124/187 727 | f 120/122/186 119/121/186 124/125/186 728 | f 119/121/186 117/120/186 125/126/186 729 | f 123/127/188 117/120/188 127/128/188 730 | f 117/120/186 120/122/186 128/129/186 731 | f 120/122/189 121/130/189 128/129/189 732 | f 121/130/190 127/128/190 128/129/190 733 | f 125/126/186 117/120/186 129/131/186 734 | f 126/117/191 125/126/191 129/131/191 735 | f 119/121/192 122/124/192 130/132/192 736 | f 124/125/186 119/121/186 130/132/186 737 | f 122/124/193 124/125/193 130/132/193 738 | f 122/124/194 118/123/194 131/118/194 739 | f 127/128/195 121/130/195 131/118/195 740 | f 123/127/196 127/128/196 131/118/196 741 | f 121/130/189 120/122/189 132/133/189 742 | f 120/122/197 122/124/197 132/133/197 743 | f 122/124/198 131/118/198 132/133/198 744 | f 131/118/199 121/130/199 132/133/199 745 | f 127/128/200 117/120/200 133/134/200 746 | f 117/120/186 128/129/186 133/134/186 747 | f 128/129/190 127/128/190 133/134/190 748 | f 122/124/201 120/122/201 134/135/201 749 | f 120/122/186 124/125/186 134/135/186 750 | f 124/125/202 122/124/202 134/135/202 751 | f 129/131/203 123/127/203 135/136/203 752 | f 126/117/191 129/131/191 135/136/191 753 | f 123/127/204 131/118/204 135/136/204 754 | f 131/118/205 126/117/205 135/136/205 755 | f 117/120/206 123/127/206 136/137/206 756 | f 129/131/186 117/120/186 136/137/186 757 | f 123/127/203 129/131/203 136/137/203 758 | f 118/123/207 119/121/207 137/119/207 759 | f 119/121/208 125/126/208 137/119/208 760 | f 125/126/209 126/117/209 137/119/209 761 | f 131/118/210 118/123/210 137/119/210 762 | o Spade_hull_7 763 | v 16.702999 37.746849 393.750061 764 | v 16.702999 -30.050114 279.218475 765 | v 47.096996 -30.050114 279.218475 766 | v 45.142422 -9.560382 277.473816 767 | v 47.096996 40.081188 396.095642 768 | v 16.702999 49.441891 389.081848 769 | v 47.096996 49.441891 389.081848 770 | v 16.702999 -13.678620 283.898132 771 | v 47.096996 -27.707998 286.232239 772 | v 16.702999 -27.707998 286.232239 773 | v 47.096996 33.062614 386.736298 774 | v 47.213421 -9.145409 278.770782 775 | v 16.702999 7.361547 344.653625 776 | v 16.702999 47.099770 384.402191 777 | v 16.702999 -18.355078 279.218475 778 | v 16.702999 40.081188 396.095642 779 | v 16.702999 -1.991367 302.605316 780 | v 47.096996 7.361547 344.653625 781 | v 16.702999 33.062614 386.736298 782 | v 47.096996 47.099770 384.402191 783 | vt 0.000000 0.257758 784 | vt 0.901422 0.970536 785 | vt 0.901422 0.970536 786 | vt 0.014708 0.000000 787 | vt 0.014708 0.000000 788 | vt 0.980226 0.852878 789 | vt 0.940873 1.000000 790 | vt 1.000000 0.882244 791 | vt 0.940873 1.000000 792 | vt 0.054158 0.205951 793 | vt 0.073835 0.029464 794 | vt 0.073835 0.029464 795 | vt 0.921099 0.793951 796 | vt 0.010934 0.262979 797 | vt 0.566336 0.470634 798 | vt 0.014708 0.147122 799 | vt 1.000000 0.882244 800 | vt 0.211862 0.352976 801 | vt 0.566336 0.470634 802 | vt 0.921099 0.793951 803 | vn 0.0000 0.8836 -0.4682 804 | vn 0.0000 -0.0848 -0.9964 805 | vn -1.0000 0.0000 0.0000 806 | vn 0.0000 0.5996 0.8003 807 | vn 0.0000 -0.9485 0.3167 808 | vn 0.0151 -0.7999 0.5999 809 | vn 1.0000 -0.0030 0.0023 810 | vn 0.5337 -0.0211 -0.8454 811 | vn 1.0000 0.0006 0.0008 812 | vn 1.0000 -0.0055 0.0018 813 | vn 0.0000 -0.8574 0.5147 814 | vn 0.0000 0.8943 -0.4476 815 | vn -0.0612 0.0000 -0.9981 816 | vn -0.2535 0.6842 -0.6838 817 | vn 0.0000 -0.7088 0.7054 818 | vn -0.2357 0.8242 -0.5149 819 | vn -0.2209 0.8362 -0.5019 820 | vn 1.0000 -0.0049 0.0030 821 | vn 1.0000 -0.0051 0.0030 822 | vn 0.0000 -0.8534 0.5212 823 | vn 0.0000 -0.8316 0.5554 824 | vn 0.1166 0.8767 -0.4667 825 | vn 0.9993 0.0344 -0.0172 826 | g Spade_hull_7_Spade_hull_7_None 827 | usemtl None 828 | s off 829 | f 141/138/211 151/139/211 157/140/211 830 | f 140/141/212 139/142/212 141/138/212 831 | f 139/142/213 138/143/213 143/144/213 832 | f 143/144/214 142/145/214 144/146/214 833 | f 139/142/213 143/144/213 145/147/213 834 | f 139/142/215 140/141/215 146/148/215 835 | f 138/143/213 139/142/213 147/149/213 836 | f 139/142/215 146/148/215 147/149/215 837 | f 142/145/216 138/143/216 148/150/216 838 | f 142/145/217 148/150/217 149/151/217 839 | f 140/141/218 141/138/218 149/151/218 840 | f 144/146/219 142/145/219 149/151/219 841 | f 146/148/220 140/141/220 149/151/220 842 | f 138/143/213 147/149/213 150/152/213 843 | f 147/149/221 146/148/221 150/152/221 844 | f 143/144/222 144/146/222 151/139/222 845 | f 145/147/213 143/144/213 151/139/213 846 | f 141/138/223 139/142/223 152/153/223 847 | f 139/142/213 145/147/213 152/153/213 848 | f 145/147/224 141/138/224 152/153/224 849 | f 138/143/225 142/145/225 153/154/225 850 | f 143/144/213 138/143/213 153/154/213 851 | f 142/145/214 143/144/214 153/154/214 852 | f 141/138/226 145/147/226 154/155/226 853 | f 151/139/227 141/138/227 154/155/227 854 | f 145/147/213 151/139/213 154/155/213 855 | f 149/151/228 148/150/228 155/156/228 856 | f 146/148/229 149/151/229 155/156/229 857 | f 150/152/221 146/148/221 155/156/221 858 | f 148/150/230 150/152/230 155/156/230 859 | f 148/150/231 138/143/231 156/157/231 860 | f 138/143/213 150/152/213 156/157/213 861 | f 150/152/230 148/150/230 156/157/230 862 | f 149/151/232 141/138/232 157/140/232 863 | f 144/146/233 149/151/233 157/140/233 864 | f 151/139/222 144/146/222 157/140/222 865 | o Spade_hull_8 866 | v 84.501106 26.060055 375.054291 867 | v 47.100883 -30.050117 279.218475 868 | v 70.473106 -30.050117 279.218475 869 | v 61.128883 23.720686 281.563995 870 | v 47.100883 49.436394 393.750061 871 | v 86.842995 58.793892 377.388397 872 | v 79.825104 19.041939 290.911896 873 | v 47.100883 37.748222 393.750061 874 | v 46.386471 -7.421408 277.117462 875 | v 86.842995 40.087597 396.095642 876 | v 77.487106 -20.692621 279.218475 877 | v 68.135101 58.793892 377.388397 878 | v 75.149101 -27.710745 286.232239 879 | v 76.810699 20.930420 282.977631 880 | v 86.842995 58.793892 386.736298 881 | v 47.100883 49.436398 389.081848 882 | v 47.100883 -27.710745 286.232239 883 | v 48.457745 -2.901729 275.948273 884 | v 86.842995 40.087601 384.402191 885 | v 77.487106 -25.371370 288.577789 886 | v 61.128883 30.730108 300.259766 887 | v 47.100883 0.344342 332.994537 888 | v 61.128883 21.381313 279.218475 889 | v 68.135101 58.793892 386.736298 890 | v 84.501106 33.069481 386.736298 891 | v 47.100883 40.087597 396.095642 892 | v 86.842995 56.454517 389.081848 893 | v 47.100883 33.069481 386.736298 894 | v 79.825104 -6.665080 321.301086 895 | v 86.842995 37.748222 393.750061 896 | v 77.487106 -25.371370 283.898132 897 | vt 0.980477 0.763117 898 | vt 0.105117 0.052663 899 | vt 0.066168 0.052663 900 | vt 0.027218 0.000000 901 | vt 0.027218 0.000000 902 | vt 0.085595 0.026331 903 | vt 0.046740 0.605227 904 | vt 0.844298 1.000000 905 | vt 0.058506 0.573821 906 | vt 0.124544 0.552565 907 | vt 0.027218 0.105325 908 | vt 1.000000 0.789448 909 | vt 0.922101 1.000000 910 | vt 0.844298 1.000000 911 | vt 0.009731 0.254702 912 | vt 0.980477 0.894675 913 | vt 0.941623 0.894675 914 | vt 0.085595 0.026331 915 | vt 0.000000 0.305574 916 | vt 0.902674 0.789448 917 | vt 0.202347 0.684123 918 | vt 0.474803 0.342110 919 | vt 0.027218 0.578896 920 | vt 0.922101 1.000000 921 | vt 1.000000 0.789448 922 | vt 0.980477 0.763117 923 | vt 0.941623 0.973669 924 | vt 0.922101 0.710454 925 | vt 0.922101 0.710454 926 | vt 0.377477 0.263215 927 | vt 0.824871 0.631558 928 | vn 0.9892 -0.1466 0.0000 929 | vn 0.0000 -0.9486 0.3164 930 | vn 0.1952 0.9029 -0.3829 931 | vn 0.8906 0.3814 -0.2476 932 | vn 0.9374 0.0464 -0.3451 933 | vn 1.0000 -0.0000 0.0000 934 | vn 0.0000 1.0000 0.0000 935 | vn -0.9999 0.0126 0.0000 936 | vn -0.4065 0.9137 0.0000 937 | vn -0.9995 -0.0306 0.0102 938 | vn 0.0000 -0.1196 -0.9928 939 | vn -0.3191 -0.0976 -0.9427 940 | vn 0.0768 -0.0576 -0.9954 941 | vn -0.8615 0.4568 -0.2217 942 | vn -0.9109 0.3703 -0.1822 943 | vn 0.9973 -0.0259 -0.0690 944 | vn 0.9970 -0.0392 -0.0660 945 | vn 0.0311 0.9359 -0.3509 946 | vn 0.0000 0.9397 -0.3419 947 | vn -0.6978 0.6707 -0.2515 948 | vn -0.5074 0.8236 -0.2536 949 | vn -0.9995 -0.0277 0.0166 950 | vn 0.0000 -0.8575 0.5145 951 | vn 0.1863 0.6956 -0.6938 952 | vn 0.2346 0.0912 -0.9678 953 | vn 0.1461 0.0568 -0.9876 954 | vn -0.7614 0.4590 -0.4578 955 | vn -1.0000 0.0014 0.0055 956 | vn -0.9999 -0.0100 0.0100 957 | vn 0.0000 0.2434 0.9699 958 | vn 0.0000 -0.7080 0.7062 959 | vn 0.0384 0.3936 0.9185 960 | vn -0.0192 0.6270 0.7788 961 | vn 0.0000 0.7080 0.7062 962 | vn -0.9997 -0.0219 0.0146 963 | vn -0.9995 -0.0272 0.0166 964 | vn 0.0000 -0.8319 0.5549 965 | vn 0.0000 -0.8541 0.5201 966 | vn 0.3754 -0.8160 0.4396 967 | vn 0.0002 -0.8575 0.5146 968 | vn 0.0863 -0.8543 0.5126 969 | vn 0.0023 -0.8548 0.5189 970 | vn 0.1697 -0.8450 0.5070 971 | vn 0.3482 -0.8139 0.4652 972 | vn 0.8288 -0.5092 0.2318 973 | vn 0.6862 -0.5144 -0.5143 974 | vn 0.6175 -0.7712 -0.1544 975 | vn 0.9968 -0.0562 -0.0562 976 | vn 0.7073 -0.7069 0.0000 977 | vn 0.9944 -0.1027 -0.0257 978 | g Spade_hull_8_Spade_hull_8_None 979 | usemtl None 980 | s off 981 | f 187/158/234 177/159/234 188/160/234 982 | f 159/161/235 160/162/235 170/163/235 983 | f 161/164/236 163/165/236 171/166/236 984 | f 163/165/237 164/167/237 171/166/237 985 | f 164/167/238 168/168/238 171/166/238 986 | f 167/169/239 163/165/239 172/170/239 987 | f 163/165/240 169/171/240 172/170/240 988 | f 166/172/241 162/173/241 173/174/241 989 | f 162/173/242 169/171/242 173/174/242 990 | f 166/172/243 159/161/243 174/175/243 991 | f 159/161/235 170/163/235 174/175/235 992 | f 160/162/244 159/161/244 175/176/244 993 | f 159/161/245 166/172/245 175/176/245 994 | f 168/168/246 160/162/246 175/176/246 995 | f 173/174/247 161/164/247 175/176/247 996 | f 166/172/248 173/174/248 175/176/248 997 | f 164/167/249 163/165/249 176/177/249 998 | f 163/165/239 167/169/239 176/177/239 999 | f 168/168/250 164/167/250 176/177/250 1000 | f 163/165/251 161/164/251 178/178/251 1001 | f 169/171/252 163/165/252 178/178/252 1002 | f 161/164/253 173/174/253 178/178/253 1003 | f 173/174/254 169/171/254 178/178/254 1004 | f 166/172/255 174/175/255 179/179/255 1005 | f 174/175/256 170/163/256 179/179/256 1006 | f 161/164/257 171/166/257 180/180/257 1007 | f 171/166/258 168/168/258 180/180/258 1008 | f 168/168/259 175/176/259 180/180/259 1009 | f 175/176/260 161/164/260 180/180/260 1010 | f 169/171/242 162/173/242 181/181/242 1011 | f 172/170/240 169/171/240 181/181/240 1012 | f 162/173/261 166/172/261 183/182/261 1013 | f 166/172/262 165/183/262 183/182/262 1014 | f 167/169/263 162/173/263 183/182/263 1015 | f 165/183/264 167/169/264 183/182/264 1016 | f 162/173/265 167/169/265 184/184/265 1017 | f 167/169/239 172/170/239 184/184/239 1018 | f 181/181/266 162/173/266 184/184/266 1019 | f 172/170/267 181/181/267 184/184/267 1020 | f 165/183/268 166/172/268 185/185/268 1021 | f 166/172/269 179/179/269 185/185/269 1022 | f 182/186/270 165/183/270 185/185/270 1023 | f 179/179/271 182/186/271 185/185/271 1024 | f 170/163/272 177/159/272 186/187/272 1025 | f 179/179/273 170/163/273 186/187/273 1026 | f 158/188/274 182/186/274 186/187/274 1027 | f 182/186/275 179/179/275 186/187/275 1028 | f 167/169/264 165/183/264 187/158/264 1029 | f 176/177/239 167/169/239 187/158/239 1030 | f 182/186/276 158/188/276 187/158/276 1031 | f 165/183/270 182/186/270 187/158/270 1032 | f 158/188/277 186/187/277 187/158/277 1033 | f 186/187/278 177/159/278 187/158/278 1034 | f 160/162/279 168/168/279 188/160/279 1035 | f 170/163/280 160/162/280 188/160/280 1036 | f 168/168/281 176/177/281 188/160/281 1037 | f 177/159/282 170/163/282 188/160/282 1038 | f 176/177/283 187/158/283 188/160/283 1039 | o Spade_hull_9 1040 | v -86.168999 44.757645 396.095642 1041 | v -69.804825 -30.050117 279.218475 1042 | v -60.453514 -30.050117 279.218475 1043 | v -60.453514 23.720686 281.563995 1044 | v -60.453514 40.087597 396.095642 1045 | v -79.151108 23.720686 281.563995 1046 | v -67.468887 58.793892 377.388397 1047 | v -76.815170 -27.710745 286.232239 1048 | v -86.168999 58.793892 377.388397 1049 | v -86.168999 33.069481 386.736298 1050 | v -60.453514 49.436394 393.750061 1051 | v -60.453514 -13.683200 309.619110 1052 | v -86.168999 58.793892 386.736298 1053 | v -79.151108 -13.683200 300.259766 1054 | v -86.168999 51.775772 368.040497 1055 | v -76.815170 -23.031996 279.218475 1056 | v -60.453514 33.069481 386.736298 1057 | v -67.468887 58.793892 384.402191 1058 | v -86.168999 40.087597 396.095642 1059 | v -86.168999 33.069481 379.722504 1060 | v -65.130432 49.436398 351.667419 1061 | v -79.151108 19.041941 281.563995 1062 | v -60.453514 49.436398 389.081848 1063 | v -60.453514 -27.710745 286.232239 1064 | vt 0.260108 0.184221 1065 | vt 0.060010 0.026331 1066 | vt 0.060010 0.026331 1067 | vt 0.000000 0.000000 1068 | vt 0.020068 0.605227 1069 | vt 1.000000 0.789448 1070 | vt 0.000000 0.000000 1071 | vt 1.000000 0.842013 1072 | vt 0.839941 1.000000 1073 | vt 0.919922 0.710454 1074 | vt 0.979931 0.894675 1075 | vt 0.839941 1.000000 1076 | vt 0.919922 1.000000 1077 | vt 0.180029 0.184221 1078 | vt 0.020068 0.605227 1079 | vt 0.759961 0.921006 1080 | vt 0.000000 0.078994 1081 | vt 0.919922 0.710454 1082 | vt 0.899951 1.000000 1083 | vt 1.000000 0.789448 1084 | vt 0.859912 0.710454 1085 | vt 0.619873 0.894675 1086 | vt 0.020068 0.552565 1087 | vt 0.939990 0.894675 1088 | vn 0.0000 -0.8576 0.5144 1089 | vn 1.0000 0.0000 0.0000 1090 | vn 0.0000 -0.9486 0.3164 1091 | vn -1.0000 -0.0000 0.0000 1092 | vn 0.0441 0.2431 0.9690 1093 | vn -0.0053 -0.8559 0.5171 1094 | vn 0.0000 1.0000 0.0000 1095 | vn -0.0250 0.5546 0.8317 1096 | vn -0.9830 -0.1827 0.0190 1097 | vn -0.9728 0.1852 -0.1390 1098 | vn 0.0000 0.0000 -1.0000 1099 | vn 0.0187 0.0436 -0.9989 1100 | vn 0.0000 0.0501 -0.9987 1101 | vn -0.6399 -0.6392 -0.4264 1102 | vn -0.9929 -0.0992 -0.0662 1103 | vn 0.0000 -0.8001 0.5999 1104 | vn 0.0000 -0.8551 0.5184 1105 | vn 0.8001 0.5998 0.0000 1106 | vn 0.0840 0.7351 0.6728 1107 | vn 0.0000 0.0000 1.0000 1108 | vn -0.9889 -0.1484 0.0000 1109 | vn -0.9971 -0.0402 -0.0644 1110 | vn 0.0000 0.9388 -0.3444 1111 | vn -0.0028 0.9390 -0.3439 1112 | vn 0.0000 0.9397 -0.3419 1113 | vn -0.9971 -0.0380 -0.0665 1114 | vn -0.9967 0.0000 -0.0809 1115 | vn -0.7086 0.0000 -0.7056 1116 | vn -0.9948 -0.0503 -0.0881 1117 | vn 0.8001 0.5999 0.0000 1118 | vn 0.8809 0.4604 -0.1101 1119 | vn 0.8550 0.5075 -0.1069 1120 | g Spade_hull_9_Spade_hull_9_None 1121 | usemtl None 1122 | s off 1123 | f 200/189/284 196/190/284 212/191/284 1124 | f 191/192/285 192/193/285 193/194/285 1125 | f 190/195/286 191/192/286 196/190/286 1126 | f 189/196/287 197/197/287 198/198/287 1127 | f 189/196/288 193/194/288 199/199/288 1128 | f 193/194/285 192/193/285 199/199/285 1129 | f 191/192/285 193/194/285 200/189/285 1130 | f 198/198/289 196/190/289 200/189/289 1131 | f 195/200/290 197/197/290 201/201/290 1132 | f 197/197/287 189/196/287 201/201/287 1133 | f 189/196/291 199/199/291 201/201/291 1134 | f 196/190/292 198/198/292 202/202/292 1135 | f 197/197/293 194/203/293 203/204/293 1136 | f 198/198/287 197/197/287 203/204/287 1137 | f 191/192/294 190/195/294 204/205/294 1138 | f 192/193/295 191/192/295 204/205/295 1139 | f 194/203/296 192/193/296 204/205/296 1140 | f 190/195/297 196/190/297 204/205/297 1141 | f 196/190/298 202/202/298 204/205/298 1142 | f 193/194/299 198/198/299 205/206/299 1143 | f 200/189/285 193/194/285 205/206/285 1144 | f 198/198/300 200/189/300 205/206/300 1145 | f 199/199/301 195/200/301 206/207/301 1146 | f 195/200/290 201/201/290 206/207/290 1147 | f 201/201/302 199/199/302 206/207/302 1148 | f 193/194/303 189/196/303 207/208/303 1149 | f 189/196/287 198/198/287 207/208/287 1150 | f 198/198/299 193/194/299 207/208/299 1151 | f 202/202/304 198/198/304 208/209/304 1152 | f 198/198/287 203/204/287 208/209/287 1153 | f 203/204/305 202/202/305 208/209/305 1154 | f 192/193/306 194/203/306 209/210/306 1155 | f 194/203/307 197/197/307 209/210/307 1156 | f 197/197/308 195/200/308 209/210/308 1157 | f 202/202/309 203/204/309 210/211/309 1158 | f 203/204/310 194/203/310 210/211/310 1159 | f 194/203/311 204/205/311 210/211/311 1160 | f 204/205/312 202/202/312 210/211/312 1161 | f 199/199/285 192/193/285 211/212/285 1162 | f 195/200/313 199/199/313 211/212/313 1163 | f 192/193/314 209/210/314 211/212/314 1164 | f 209/210/315 195/200/315 211/212/315 1165 | f 196/190/286 191/192/286 212/191/286 1166 | f 191/192/285 200/189/285 212/191/285 1167 | o Spade_hull_10 1168 | v -60.450996 -30.050114 279.207031 1169 | v -41.748833 -30.050114 279.207031 1170 | v -61.000923 8.457848 278.863281 1171 | v -41.748833 -11.346113 290.897034 1172 | v -60.450996 -25.370455 288.556976 1173 | v -41.748833 -13.678621 279.207031 1174 | v -41.748833 -25.370455 288.556976 1175 | v -61.244213 6.406167 283.785858 1176 | v -61.243397 -6.273876 291.392334 1177 | v -59.332413 -21.632908 290.197845 1178 | v -53.435856 2.681887 279.207031 1179 | v -60.450996 -30.050114 281.547089 1180 | v -58.111397 7.357888 281.547089 1181 | vt 0.485718 1.000000 1182 | vt 0.850006 0.400523 1183 | vt 0.971435 0.160695 1184 | vt 0.000000 1.000000 1185 | vt 0.000000 0.040687 1186 | vt 1.000000 0.012479 1187 | vt 0.425146 1.000000 1188 | vt 0.121524 1.000000 1189 | vt 0.946721 0.000000 1190 | vt 0.617437 0.000042 1191 | vt 0.218584 0.098064 1192 | vt 0.121524 0.040687 1193 | vt 0.000000 0.040687 1194 | vn 0.7266 0.6810 0.0909 1195 | vn 0.0000 -0.0089 -1.0000 1196 | vn 0.0178 0.0000 -0.9998 1197 | vn 1.0000 0.0000 0.0000 1198 | vn -0.9984 -0.0148 -0.0555 1199 | vn -0.9994 -0.0180 -0.0299 1200 | vn 0.1538 0.5083 0.8473 1201 | vn 0.0570 -0.1643 0.9848 1202 | vn 0.0000 -0.4020 0.9156 1203 | vn 0.0053 -0.0769 0.9970 1204 | vn -0.6425 -0.1386 0.7536 1205 | vn 0.0992 0.0709 -0.9925 1206 | vn 0.8083 0.5774 -0.1152 1207 | vn 0.0000 -1.0000 0.0000 1208 | vn 0.0559 -0.8928 0.4469 1209 | vn 0.0000 -0.8317 0.5552 1210 | vn -0.9985 -0.0460 0.0307 1211 | vn -0.9994 -0.0333 0.0000 1212 | vn -0.0057 0.9231 0.3845 1213 | vn 0.3154 0.6304 0.7093 1214 | vn 0.5836 0.7451 -0.3229 1215 | g Spade_hull_10_Spade_hull_10_None 1216 | usemtl None 1217 | s off 1218 | f 216/213/316 223/214/316 225/215/316 1219 | f 214/216/317 213/217/317 215/218/317 1220 | f 214/216/318 215/218/318 218/219/318 1221 | f 216/213/319 214/216/319 218/219/319 1222 | f 214/216/319 216/213/319 219/220/319 1223 | f 215/218/320 213/217/320 220/221/320 1224 | f 220/221/321 213/217/321 221/222/321 1225 | f 216/213/322 220/221/322 221/222/322 1226 | f 219/220/323 216/213/323 222/223/323 1227 | f 217/224/324 219/220/324 222/223/324 1228 | f 216/213/325 221/222/325 222/223/325 1229 | f 221/222/326 217/224/326 222/223/326 1230 | f 218/219/327 215/218/327 223/214/327 1231 | f 216/213/328 218/219/328 223/214/328 1232 | f 213/217/329 214/216/329 224/225/329 1233 | f 214/216/330 219/220/330 224/225/330 1234 | f 219/220/331 217/224/331 224/225/331 1235 | f 217/224/332 221/222/332 224/225/332 1236 | f 221/222/333 213/217/333 224/225/333 1237 | f 215/218/334 220/221/334 225/215/334 1238 | f 220/221/335 216/213/335 225/215/335 1239 | f 223/214/336 215/218/336 225/215/336 1240 | o Spade_hull_11 1241 | v -41.748833 40.077068 396.086487 1242 | v -60.450996 -23.036114 290.897064 1243 | v -41.748833 -23.036114 290.897064 1244 | v -61.708733 -4.037617 297.106079 1245 | v -60.450996 37.750053 393.748962 1246 | v -41.748833 47.100685 384.398773 1247 | v -60.450996 49.441891 389.073853 1248 | v -41.748833 -6.668969 295.592743 1249 | v -41.748833 -20.694912 297.930298 1250 | v -60.450996 -20.694912 297.930298 1251 | v -41.748833 33.067650 386.736328 1252 | v -60.566914 -11.648576 290.245087 1253 | v -41.748833 49.441891 389.073853 1254 | v -41.748833 -11.344282 290.897064 1255 | v -60.450996 0.347547 332.993439 1256 | v -60.450996 40.077068 396.086487 1257 | v -41.748833 -1.993655 302.605377 1258 | v -60.450996 47.100685 384.398773 1259 | v -60.450996 33.067650 386.736328 1260 | vt 0.403891 0.322631 1261 | vt 0.911659 0.774080 1262 | vt 0.911659 0.774080 1263 | vt 1.000000 0.870791 1264 | vt 0.006160 0.000000 1265 | vt 0.889573 0.967698 1266 | vt 0.050525 0.225822 1267 | vt 0.072611 0.032302 1268 | vt 0.006160 0.000000 1269 | vt 0.064823 0.262128 1270 | vt 0.072611 0.032302 1271 | vt 0.977915 0.838684 1272 | vt 0.000000 0.157117 1273 | vt 0.933744 1.000000 1274 | vt 0.933744 1.000000 1275 | vt 0.006160 0.161316 1276 | vt 1.000000 0.870791 1277 | vt 0.116781 0.290329 1278 | vt 0.889573 0.967698 1279 | vn 0.0000 -0.8541 0.5200 1280 | vn 1.0000 0.0000 0.0000 1281 | vn 0.0000 -0.9488 0.3158 1282 | vn -0.9969 -0.0741 0.0247 1283 | vn 0.0246 -0.7999 0.5996 1284 | vn 0.0000 -0.0572 -0.9984 1285 | vn -0.9894 -0.0183 -0.1443 1286 | vn 0.0000 0.5994 0.8005 1287 | vn 0.0000 0.8941 -0.4478 1288 | vn 0.0399 0.7081 -0.7050 1289 | vn 0.0346 0.0000 -0.9994 1290 | vn 0.0148 0.6707 -0.7416 1291 | vn 0.0000 -0.8574 0.5146 1292 | vn -0.9964 -0.0731 0.0438 1293 | vn 0.0084 -0.8554 0.5179 1294 | vn 0.0000 -0.7087 0.7055 1295 | vn -0.9995 -0.0231 0.0230 1296 | vn -0.9999 0.0071 0.0095 1297 | vn 0.0539 0.8562 -0.5139 1298 | vn 0.0675 0.8301 -0.5535 1299 | vn 0.0000 0.8628 -0.5055 1300 | vn -0.9825 0.1665 -0.0834 1301 | vn -0.9978 -0.0552 0.0368 1302 | vn 0.0000 -0.8316 0.5553 1303 | vn -0.9965 -0.0718 0.0437 1304 | g Spade_hull_11_Spade_hull_11_Material.001 1305 | usemtl Material.001 1306 | s off 1307 | f 240/226/337 236/227/337 244/228/337 1308 | f 226/229/338 228/230/338 231/231/338 1309 | f 231/231/338 228/230/338 233/232/338 1310 | f 228/230/338 226/229/338 234/233/338 1311 | f 227/234/339 228/230/339 234/233/339 1312 | f 229/235/340 227/234/340 235/236/340 1313 | f 227/234/339 234/233/339 235/236/339 1314 | f 226/229/341 230/237/341 236/227/341 1315 | f 234/233/338 226/229/338 236/227/338 1316 | f 228/230/342 227/234/342 237/238/342 1317 | f 227/234/343 229/235/343 237/238/343 1318 | f 226/229/338 231/231/338 238/239/338 1319 | f 232/240/344 226/229/344 238/239/344 1320 | f 231/231/345 232/240/345 238/239/345 1321 | f 233/232/338 228/230/338 239/241/338 1322 | f 229/235/346 233/232/346 239/241/346 1323 | f 228/230/347 237/238/347 239/241/347 1324 | f 237/238/348 229/235/348 239/241/348 1325 | f 235/236/349 234/233/349 240/226/349 1326 | f 229/235/350 235/236/350 240/226/350 1327 | f 234/233/351 236/227/351 240/226/351 1328 | f 230/237/352 226/229/352 241/242/352 1329 | f 229/235/353 230/237/353 241/242/353 1330 | f 226/229/344 232/240/344 241/242/344 1331 | f 232/240/354 229/235/354 241/242/354 1332 | f 229/235/355 231/231/355 242/243/355 1333 | f 233/232/356 229/235/356 242/243/356 1334 | f 231/231/338 233/232/338 242/243/338 1335 | f 231/231/357 229/235/357 243/244/357 1336 | f 229/235/358 232/240/358 243/244/358 1337 | f 232/240/345 231/231/345 243/244/345 1338 | f 230/237/359 229/235/359 244/228/359 1339 | f 236/227/360 230/237/360 244/228/360 1340 | f 229/235/361 240/226/361 244/228/361 1341 | -------------------------------------------------------------------------------- /examples/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petrikvladimir/RoboMeshCat/4c803c4c457d4e5db51ac8b11b04eadfec19239d/examples/texture.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | 7 | [project] 8 | name = "robomeshcat" 9 | dynamic = ["version"] 10 | license = { text = "BSD-2-Clause" } 11 | authors = [ 12 | { name = "Vladimir Petrik", email = "vladimir.petrik@cvut.cz" }, 13 | ] 14 | description = "RoboMeshCat - Set of utilities for visualizing robots in web-based visualizer MeshCat." 15 | readme = "README.md" 16 | requires-python = ">=3.7" # we use future annotations supported from 3.7 17 | dependencies = ["pin", "meshcat", "trimesh", "imageio", "imageio-ffmpeg"] 18 | classifiers = [ 19 | "Topic :: Scientific/Engineering :: Visualization", 20 | "Framework :: Robot Framework", 21 | ] 22 | 23 | [project.urls] 24 | "Homepage" = "https://github.com/petrikvladimir/RoboMeshCat" 25 | "Bug Tracker" = "https://github.com/petrikvladimir/RoboMeshCat/issues" 26 | 27 | [project.optional-dependencies] 28 | dev = [ 29 | "pycollada", # used to load 'dae' meshes 30 | ] 31 | 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-10-11 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | from setuptools import setup 9 | 10 | if __name__ == '__main__': 11 | setup() 12 | -------------------------------------------------------------------------------- /src/robomeshcat/__init__.py: -------------------------------------------------------------------------------- 1 | from .object import Object 2 | from .robot import Robot 3 | from .scene import Scene 4 | from .human import Human 5 | -------------------------------------------------------------------------------- /src/robomeshcat/human.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-12-6 5 | # Author: Vladimir Petrik 6 | # 7 | from __future__ import annotations 8 | 9 | import numpy as np 10 | from meshcat import geometry as g 11 | 12 | from . import Object 13 | 14 | 15 | class Human(Object): 16 | def __init__( 17 | self, 18 | pose=None, 19 | color: list[float] | None = None, 20 | opacity: float = 1.0, 21 | name: str | None = None, 22 | use_vertex_colors: bool = False, 23 | show_wireframe: bool = False, 24 | **kwargs, 25 | ) -> None: 26 | from smplx import SMPLX # we import SMPLX here on purpose, so that smplx dependencies are optional 27 | 28 | self.smplx_model = SMPLX(**kwargs) 29 | super().__init__(None, pose, color if not use_vertex_colors else [1, 1, 1], None, opacity, name) 30 | 31 | self._use_vertex_colors = use_vertex_colors 32 | self._show_wireframe = show_wireframe 33 | clr = self._color_from_input(color)[np.newaxis, :] 34 | self._vertex_colors = np.repeat(clr, self.smplx_model.get_num_verts(), 0) if use_vertex_colors else None 35 | self.update_vertices(set_object=False) # this will create a geometry 36 | 37 | "Additional properties that are modifiable " 38 | self._morph_target_influences = None 39 | 40 | @property 41 | def _material(self): 42 | mat = super()._material 43 | mat.wireframe = self._show_wireframe 44 | mat.vertexColors = self._use_vertex_colors 45 | return mat 46 | 47 | def get_vertices(self, **kwargs): 48 | """Return vertices of the mesh for the given smplx parameters.""" 49 | output = self.smplx_model(return_verts=True, **kwargs) 50 | return output.vertices.detach().cpu().numpy().squeeze() 51 | 52 | def update_vertices(self, vertices=None, vertices_colors=None, set_object=True): 53 | if self._is_animation(): 54 | print( 55 | 'Update vertices of the mesh will recreate the geometry. It cannot be used in animation for ' 56 | 'which you should use this.add_morph function' 57 | ) 58 | return 59 | self._geometry = TriangularMeshGeometryWithMorphAttributes( 60 | vertices=np.asarray(vertices) if vertices is not None else self.get_vertices(), 61 | faces=self.smplx_model.faces, 62 | color=np.asarray(vertices_colors) if vertices_colors is not None else self._vertex_colors, 63 | morph_positions=[], 64 | morph_colors=[], 65 | ) 66 | if set_object: 67 | self._set_object() 68 | 69 | def add_morph(self, vertices, vertex_colors=None): 70 | """Add new morphology through which we can create animations.""" 71 | self._geometry.morph_positions.append(vertices) 72 | if vertex_colors is not None: 73 | self._geometry.morph_colors.append(np.asarray(vertex_colors)) 74 | 75 | def display_morph(self, morph_id): 76 | """Set morphTargetInfluences to display only the given morph_id.""" 77 | self._morph_target_influences = [0] * self._geometry.number_of_morphs() 78 | if morph_id is not None: 79 | self._morph_target_influences[morph_id] = 1 80 | 81 | def _set_morph_property(self): 82 | if self._morph_target_influences is None: 83 | self._morph_target_influences = [0] * len(self._geometry.morph_positions) 84 | self._set_property('morphTargetInfluences', self._morph_target_influences, 'vector') 85 | 86 | def _reset_all_properties(self): 87 | super()._reset_all_properties() 88 | self._set_morph_property() 89 | 90 | 91 | class TriangularMeshGeometryWithMorphAttributes(g.TriangularMeshGeometry): 92 | def __init__(self, morph_positions=None, morph_colors=None, **kwargs): 93 | super(TriangularMeshGeometryWithMorphAttributes, self).__init__(**kwargs) 94 | self.morph_positions = morph_positions 95 | self.morph_colors = morph_colors 96 | 97 | def number_of_morphs(self) -> int: 98 | return max(len(self.morph_colors), len(self.morph_positions)) 99 | 100 | def lower(self, object_data): 101 | ret = super(TriangularMeshGeometryWithMorphAttributes, self).lower(object_data=object_data) 102 | ret[u"data"][u"morphAttributes"] = {} 103 | if self.morph_positions is not None: 104 | ret[u"data"][u"morphAttributes"][u"position"] = [g.pack_numpy_array(pos.T) for pos in self.morph_positions] 105 | if self.morph_colors is not None: 106 | ret[u"data"][u"morphAttributes"][u"color"] = [g.pack_numpy_array(c.T) for c in self.morph_colors] 107 | return ret 108 | -------------------------------------------------------------------------------- /src/robomeshcat/object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-10-11 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | from __future__ import annotations 9 | 10 | import itertools 11 | from pathlib import Path 12 | import trimesh 13 | import numpy as np 14 | from PIL import Image 15 | import io 16 | import meshcat.geometry as g 17 | from meshcat.animation import AnimationFrameVisualizer 18 | 19 | 20 | class Object: 21 | """Represent an object with arbitrary geometry that can be rendered in the meshcat.""" 22 | 23 | id_iterator = itertools.count() 24 | 25 | def __init__( 26 | self, 27 | geometry=None, 28 | pose=None, 29 | color: list[float] | None = None, 30 | texture: g.ImageTexture | Path | None = None, 31 | opacity: float = 1.0, 32 | name: str | None = None, 33 | ) -> None: 34 | """Create an object with given geometry, pose, color and opacity. 35 | :param geometry 36 | :param pose - 4x4 pose of the object 37 | :param color - RGB color of the object either tuple of integers [0-255] or tuple of floats [0. - 1.] 38 | :param texture - alternative to color, you can specify texture that object uses to render as meshcat texture or 39 | as a path to png file. 40 | :param opacity - transparency of the object from 0. to 1. 41 | :param name - unique name of the object, it's created automatically if not specified 42 | """ 43 | super().__init__() 44 | self.name = f'obj{next(self.id_iterator)}' if name is None else name 45 | self._vis = None # either a visualization tree or the frame of the animation, is set by the scene 46 | 47 | "List of properties that could be updated for all objects" 48 | self._pose = ArrayWithCallbackOnSetItem(np.eye(4) if pose is None else pose, cb=self._set_transform) 49 | self._color = ArrayWithCallbackOnSetItem(self._color_from_input(color), cb=self._color_reset_on_set_item) 50 | self._opacity = opacity 51 | self._visible = True 52 | 53 | "Not updatable properties." 54 | self._texture = g.ImageTexture(g.PngImage.from_file(texture)) if isinstance(texture, (Path, str)) else texture 55 | self._geometry = geometry 56 | 57 | @staticmethod 58 | def _color_from_input(clr: list[float] | np.ndarray | None, default=None): 59 | """Modify input color to always be represented by numpy array with values from 0 to 1""" 60 | if clr is None: 61 | clr = np.random.uniform(0, 1, 3) if default is None else default 62 | clr = np.asarray(clr) 63 | assert clr.shape == (3,) 64 | if np.any(clr > 1) and clr.dtype == np.int: 65 | clr = clr.astype(dtype=np.float) / 255 66 | clr = clr.clip(0.0, 1.0) 67 | return clr 68 | 69 | def _set_vis(self, vis): 70 | """Set visualizer. Used internally to create frames of animation.""" 71 | self._vis = vis[self.name] 72 | 73 | def _assert_vis(self): 74 | assert ( 75 | self._vis is not None 76 | ), 'The properties of the object cannot be modified unless object is added to the scene.' 77 | 78 | def _set_object(self): 79 | """Create an object in meshcat and set all the initial properties.""" 80 | self._assert_vis() 81 | self._vis.set_object(self._geometry, self._material) 82 | self._set_transform() 83 | 84 | def _delete_object(self): 85 | """Delete an object from meshcat.""" 86 | self._vis.delete() 87 | 88 | def _set_transform(self): 89 | """Update transformation in the meshcat.""" 90 | self._assert_vis() 91 | self._vis.set_transform(self._pose) 92 | 93 | def _set_property(self, key, value, prop_type='number', subpath=''): 94 | """Set property of the object, handle animation frames set_property internally.""" 95 | self._assert_vis() 96 | element = self._vis if subpath is None else self._vis[subpath] 97 | if self._is_animation(): 98 | element.set_property(key, prop_type, value) 99 | else: 100 | element.set_property(key, value) 101 | 102 | def _is_animation(self): 103 | """Return true if rendering to the animation. Used internally to modify the way of assigning properties until 104 | PR https://github.com/rdeits/meshcat/pull/137 is merged.""" 105 | return isinstance(self._vis, AnimationFrameVisualizer) 106 | 107 | @property 108 | def _material(self): 109 | if isinstance(self._geometry, g.Object): 110 | return None 111 | if self._texture is not None: 112 | return g.MeshLambertMaterial(map=self._texture, opacity=self.opacity) 113 | color = self.color.copy() * 255 114 | color = np.clip(color, 0, 255) 115 | return g.MeshLambertMaterial( 116 | color=int(color[0]) * 256**2 + int(color[1]) * 256 + int(color[2]), opacity=self.opacity 117 | ) 118 | 119 | """=== Control of the pose ===""" 120 | 121 | @property 122 | def pose(self): 123 | return self._pose 124 | 125 | @pose.setter 126 | def pose(self, v): 127 | self._pose[:, :] = v 128 | 129 | @property 130 | def pos(self): 131 | return self._pose[:3, 3] 132 | 133 | @pos.setter 134 | def pos(self, p): 135 | self._pose[:3, 3] = p 136 | 137 | @property 138 | def rot(self): 139 | return self._pose[:3, :3] 140 | 141 | @rot.setter 142 | def rot(self, r): 143 | self._pose[:3, :3] = r 144 | 145 | """=== Control of the object visibility ===""" 146 | 147 | @property 148 | def visible(self): 149 | return self._visible 150 | 151 | @visible.setter 152 | def visible(self, v): 153 | self._visible = bool(v) 154 | self._set_property('visible', value=self._visible, prop_type='boolean') 155 | 156 | def hide(self): 157 | self.visible = False 158 | 159 | def show(self): 160 | self.visible = True 161 | 162 | """=== Control of the object transparency ===""" 163 | 164 | @property 165 | def opacity(self): 166 | return self._opacity 167 | 168 | @opacity.setter 169 | def opacity(self, v): 170 | self._opacity = v 171 | if self._is_animation(): 172 | self._set_property('material.opacity', value=self._opacity, prop_type='number') 173 | else: 174 | self._set_object() # only way how to update opacity online is to reset the object 175 | 176 | @property 177 | def color(self): 178 | return self._color 179 | 180 | @color.setter 181 | def color(self, v): 182 | self._color[:] = self._color_from_input(v) 183 | 184 | def _color_reset_on_set_item(self): 185 | if self._is_animation(): 186 | self._set_property('material.color', value=self.color.tolist(), prop_type='vector') 187 | else: 188 | self._set_object() # only way how to update color online is to reset the object 189 | 190 | def _reset_all_properties(self): 191 | """Reset all properties in the meshcat, i.e. call setters with the same variable. Useful for the animation 192 | frames.""" 193 | self.pose = self.pose 194 | self.color = self.color 195 | self.opacity = self.opacity 196 | self.visible = self.visible 197 | 198 | """=== Helper functions to create basic primitives ===""" 199 | 200 | @classmethod 201 | def create_cuboid( 202 | cls, 203 | lengths: list[float] | float, 204 | pose=None, 205 | color: list[float] | None = None, 206 | texture: g.ImageTexture | Path | None = None, 207 | opacity: float = 1.0, 208 | name: str | None = None, 209 | ): 210 | """Create cuboid with a given size.""" 211 | box = g.Box(lengths=[lengths] * 3 if isinstance(lengths, (float, int)) else lengths) 212 | return cls(box, pose=pose, color=color, texture=texture, opacity=opacity, name=name) 213 | 214 | @classmethod 215 | def create_sphere( 216 | cls, 217 | radius: float, 218 | pose=None, 219 | color: list[float] | None = None, 220 | texture: g.ImageTexture | Path | None = None, 221 | opacity: float = 1.0, 222 | name: str | None = None, 223 | ): 224 | """Create a sphere with a given radius.""" 225 | return cls(g.Sphere(radius=radius), pose=pose, color=color, texture=texture, opacity=opacity, name=name) 226 | 227 | @classmethod 228 | def create_cylinder( 229 | cls, 230 | radius: float, 231 | length: float, 232 | pose=None, 233 | color: list[float] | None = None, 234 | texture: g.ImageTexture | Path | None = None, 235 | opacity: float = 1.0, 236 | name: str | None = None, 237 | ): 238 | """Create a cylinder with a given radius and length. The axis of rotational symmetry is aligned with the z-axis 239 | that is common in robotics. To achieve that, we create a mesh of a cylinder instead of using meshcat cylinder 240 | that is aligned with y-axis.""" 241 | mesh: trimesh.Trimesh = trimesh.creation.cylinder(radius=radius, height=length, sections=50) 242 | exp_obj = trimesh.exchange.obj.export_obj(mesh) 243 | mesh = g.ObjMeshGeometry.from_stream(trimesh.util.wrap_as_stream(exp_obj)) 244 | return cls(mesh, pose=pose, color=color, texture=texture, opacity=opacity, name=name) 245 | 246 | @classmethod 247 | def create_mesh( 248 | cls, 249 | path_to_mesh: str | Path, 250 | scale: float | list[float] = 1.0, 251 | pose=None, 252 | color: list[float] | None = None, 253 | texture: g.ImageTexture | Path | None = None, 254 | opacity: float = 1.0, 255 | name: str | None = None, 256 | ): 257 | """Create a mesh object by loading it from the :param path_to_mesh. Loading is performed by 'trimesh' library 258 | internally.""" 259 | try: 260 | mesh: trimesh.Trimesh = trimesh.load(path_to_mesh, force='mesh') 261 | except ValueError as e: 262 | if str(e) == 'File type: dae not supported': 263 | print( 264 | 'To load DAE meshes you need to install pycollada package via ' 265 | '`conda install -c conda-forge pycollada`' 266 | ' or `pip install pycollada`' 267 | ) 268 | raise 269 | except Exception as e: 270 | print( 271 | f'Loading of a mesh failed with message: \'{e}\'. ' 272 | f'Trying to load with with \'ignore_broken\' but consider to fix the mesh located here:' 273 | f' \'{path_to_mesh}\'.' 274 | ) 275 | mesh: trimesh.Trimesh = trimesh.load(path_to_mesh, force='mesh', ignore_broken=True) 276 | 277 | mesh.apply_scale(scale) 278 | 279 | try: # try to get texture from the mesh 280 | if hasattr(mesh, 'visual') and hasattr(mesh.visual, 'material') and texture is None: 281 | n = 100 282 | 283 | def xy_to_uv(xy, w=n, h=n): 284 | u = xy[..., 0] / (w - 1) 285 | v = 1 - xy[..., 1] / (h - 1) 286 | return np.stack((u, v), axis=-1) 287 | 288 | X, Y = np.meshgrid(np.arange(n), np.arange(n)) 289 | uv = xy_to_uv(np.stack((X, Y), axis=-1)) 290 | data = mesh.visual.material.to_color(uv.reshape(-1, 2)) 291 | if data.shape == (4,): 292 | data = np.tile(data, n * n).reshape(n, n, 4) 293 | else: 294 | data = data.reshape(n, n, 4) 295 | b = io.BytesIO() 296 | Image.fromarray(data).save(b, 'png') 297 | texture = g.ImageTexture(g.PngImage(b.getvalue())) 298 | except Exception: 299 | pass 300 | 301 | try: 302 | exp_obj = trimesh.exchange.obj.export_obj(mesh) 303 | except ValueError: 304 | exp_obj = trimesh.exchange.obj.export_obj(mesh, include_texture=False) 305 | 306 | return cls( 307 | g.ObjMeshGeometry.from_stream(trimesh.util.wrap_as_stream(exp_obj)), 308 | pose=pose, 309 | color=color, 310 | texture=texture, 311 | opacity=opacity, 312 | name=name, 313 | ) 314 | 315 | 316 | class ArrayWithCallbackOnSetItem(np.ndarray): 317 | def __new__(cls, input_array, cb=None): 318 | obj = np.asarray(input_array).view(cls) 319 | obj.cb = cb 320 | return obj 321 | 322 | def __setitem__(self, *args, **kwargs): 323 | super().__setitem__(*args, **kwargs) 324 | self.cb() 325 | 326 | def __array_finalize__(self, obj, **kwargs): 327 | if obj is None: 328 | return 329 | self.cb = getattr(obj, 'cb', None) 330 | -------------------------------------------------------------------------------- /src/robomeshcat/robot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-10-13 5 | # Author: Vladimir Petrik 6 | # 7 | from __future__ import annotations 8 | 9 | import itertools 10 | from pathlib import Path 11 | 12 | import numpy as np 13 | import pinocchio as pin 14 | 15 | from .object import Object, ArrayWithCallbackOnSetItem 16 | 17 | 18 | class Robot: 19 | id_iterator = itertools.count() 20 | 21 | def __init__( 22 | self, 23 | urdf_path: Path | str | None = None, 24 | mesh_folder_path: Path | str | list[Path] | list[str] | None = None, 25 | pinocchio_model: pin.Model | None = None, 26 | pinocchio_data: pin.Data | None = None, 27 | pinocchio_geometry_model: pin.GeometryModel | None = None, 28 | pinocchio_geometry_data: pin.GeometryData | None = None, 29 | show_collision_models: bool = False, 30 | name: str | None = None, 31 | color: list[float] | None = None, 32 | opacity: float | None = None, 33 | pose=None, 34 | ) -> None: 35 | """ 36 | Create a robot using pinocchio loader, you have to option to create a robot: (i) using URDF or 37 | (ii) using pinocchio models and data. 38 | :param urdf_path: path to the urdf file that contains robot description 39 | :param mesh_folder_path: either a single path to the directory of meshes or list of paths to meshes directory, 40 | it's set to directory of urdf_path file by default 41 | :param pinocchio_model, pinocchio_data, pinocchio_geometry_model, pinocchio_geometry_data: alternativelly, you 42 | can use pinocchio model instead of the urdf_path to construct the robot instance, either urdf_path or 43 | pinocchio_{model, data, geometry_model, geometry_data} has to be specified. 44 | :param show_collision_models: weather to show collision model instead of visual model 45 | :param name: name of the robot used in meshcat tree 46 | :param color: optional color that overwrites one from the urdf 47 | :param opacity: optional opacity that overwrites one from the urdf 48 | """ 49 | super().__init__() 50 | self.name = f'robot{next(self.id_iterator)}' if name is None else name 51 | pin_not_defined = ( 52 | pinocchio_model is None 53 | and pinocchio_data is None 54 | and pinocchio_geometry_model is None 55 | and pinocchio_geometry_data is None 56 | ) 57 | assert urdf_path is None or pin_not_defined, 'You need to specify either urdf or pinocchio, not both.' 58 | if pin_not_defined: 59 | self._model, self._data, self._geom_model, self._geom_data = self._build_model_from_urdf( 60 | urdf_path, mesh_folder_path, show_collision_models 61 | ) 62 | else: 63 | self._model, self._data = pinocchio_model, pinocchio_data 64 | self._geom_model, self._geom_data = pinocchio_geometry_model, pinocchio_geometry_data 65 | 66 | """ Adjustable properties """ 67 | self._pose = ArrayWithCallbackOnSetItem(np.eye(4) if pose is None else pose, cb=self._fk) 68 | self._q = ArrayWithCallbackOnSetItem(pin.neutral(self._model), cb=self._fk) 69 | self._color = ArrayWithCallbackOnSetItem(Object._color_from_input(color), cb=self._color_reset_on_set_item) 70 | self._opacity = opacity 71 | self._visible = True 72 | 73 | """Set of objects used to visualize the links.""" 74 | self._objects: dict[str, Object] = {} 75 | self._init_objects(overwrite_color=color is not None) 76 | 77 | @staticmethod 78 | def _build_model_from_urdf( 79 | urdf_path, mesh_folder_path, show_collision_models 80 | ) -> tuple[pin.Model, pin.Data, pin.GeometryModel, pin.GeometryData]: 81 | """Use pinocchio to load models and datas used for the visualizer.""" 82 | if mesh_folder_path is None: 83 | mesh_folder_path = str(Path(urdf_path).parent) # by default use the urdf parent directory 84 | elif isinstance(mesh_folder_path, Path): 85 | mesh_folder_path = str(mesh_folder_path) 86 | elif isinstance(mesh_folder_path, list) and any((isinstance(path, Path) for path in mesh_folder_path)): 87 | mesh_folder_path = [str(path) for path in mesh_folder_path] 88 | 89 | model, col_model, vis_model = pin.buildModelsFromUrdf(str(urdf_path), mesh_folder_path) 90 | data, col_data, vis_data = pin.createDatas(model, col_model, vis_model) 91 | geom_model: pin.GeometryModel = col_model if show_collision_models else vis_model 92 | geom_data: pin.GeometryData = col_data if show_collision_models else vis_data 93 | return model, data, geom_model, geom_data 94 | 95 | def _fk(self): 96 | """Compute ForwardKinematics and update the objects poses.""" 97 | pin.forwardKinematics(self._model, self._data, self._q) 98 | pin.updateGeometryPlacements(self._model, self._data, self._geom_model, self._geom_data) 99 | base = pin.SE3(self._pose) 100 | for g, f in zip(self._geom_model.geometryObjects, self._geom_data.oMg): 101 | self._objects[f'{self.name}/{g.name}'].pose = (base * f).homogeneous 102 | 103 | def _init_objects(self, overwrite_color=False): 104 | """Fill in objects dictionary based on the data from pinocchio""" 105 | pin.forwardKinematics(self._model, self._data, self._q) 106 | pin.updateGeometryPlacements(self._model, self._data, self._geom_model, self._geom_data) 107 | base = pin.SE3(self._pose) 108 | for g, f in zip(self._geom_model.geometryObjects, self._geom_data.oMg): 109 | kwargs = dict( 110 | name=f'{self.name}/{g.name}', 111 | color=g.meshColor[:3] if not overwrite_color else self._color, 112 | opacity=g.meshColor[3] if self._opacity is None else self._opacity, 113 | texture=g.meshTexturePath if g.meshTexturePath else None, 114 | pose=(base * f).homogeneous, 115 | ) 116 | if g.meshPath == 'BOX': 117 | self._objects[kwargs['name']] = Object.create_cuboid(lengths=2 * g.geometry.halfSide, **kwargs) 118 | elif g.meshPath == 'SPHERE': 119 | self._objects[kwargs['name']] = Object.create_sphere(radius=g.geometry.radius, **kwargs) 120 | elif g.meshPath == 'CYLINDER': 121 | radius, length = g.geometry.radius, 2 * g.geometry.halfLength 122 | self._objects[kwargs['name']] = Object.create_cylinder(radius=radius, length=length, **kwargs) 123 | else: 124 | self._objects[kwargs['name']] = Object.create_mesh(path_to_mesh=g.meshPath, scale=g.meshScale, **kwargs) 125 | 126 | """ === Methods for adjusting the base pose of the robot. ===""" 127 | 128 | @property 129 | def pose(self): 130 | return self._pose 131 | 132 | @pose.setter 133 | def pose(self, v): 134 | self._pose[:, :] = v 135 | 136 | @property 137 | def pos(self): 138 | return self._pose[:3, 3] 139 | 140 | @pos.setter 141 | def pos(self, p): 142 | self._pose[:3, 3] = p 143 | 144 | @property 145 | def rot(self): 146 | return self._pose[:3, :3] 147 | 148 | @rot.setter 149 | def rot(self, r): 150 | self._pose[:3, :3] = r 151 | 152 | def _get_joint_id(self, key: str | int): 153 | """Get the joint id either from joint name or integer. The universe joint is 154 | not counted as we assume it is fixed.""" 155 | if not isinstance(key, str): 156 | return key 157 | jid = self._model.getJointId(key) 158 | # there is often universe joint that is not counted, so we need to adjust the 159 | # index 160 | if len(self._model.names) == len(self._q) + 1: 161 | jid -= 1 162 | if jid == self._model.nq: 163 | raise KeyError(f'Joint {key} not found.') 164 | return jid 165 | 166 | def __getitem__(self, key): 167 | return self._q[self._get_joint_id(key)] 168 | 169 | def __setitem__(self, key, value): 170 | self._q[self._get_joint_id(key)] = value 171 | 172 | """=== Control of the object visibility ===""" 173 | 174 | @property 175 | def visible(self): 176 | return self._visible 177 | 178 | @visible.setter 179 | def visible(self, v): 180 | self._visible = bool(v) 181 | for o in self._objects.values(): 182 | o.visible = self._visible 183 | 184 | def hide(self): 185 | self.visible = False 186 | 187 | def show(self): 188 | self.visible = True 189 | 190 | """=== Control of the object transparency ===""" 191 | 192 | @property 193 | def opacity(self): 194 | return self._opacity 195 | 196 | @opacity.setter 197 | def opacity(self, v): 198 | self._opacity = v 199 | for o in self._objects.values(): 200 | o.opacity = self._opacity 201 | 202 | @property 203 | def color(self): 204 | return self._color 205 | 206 | @color.setter 207 | def color(self, v): 208 | self._color = Object._color_from_input(v) 209 | self._color_reset_on_set_item() 210 | 211 | def _color_reset_on_set_item(self): 212 | for o in self._objects.values(): 213 | o.color = self._color 214 | -------------------------------------------------------------------------------- /src/robomeshcat/scene.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2022-10-11 5 | # Author: Vladimir Petrik 6 | # 7 | from __future__ import annotations 8 | 9 | import itertools 10 | import time 11 | from pathlib import Path 12 | from tempfile import gettempdir 13 | 14 | import imageio 15 | import numpy as np 16 | from PIL.Image import Image 17 | 18 | import meshcat 19 | from meshcat.animation import Animation, AnimationFrameVisualizer 20 | 21 | from .object import Object, ArrayWithCallbackOnSetItem 22 | from .robot import Robot 23 | 24 | 25 | class Scene: 26 | def __init__(self, open: bool = True, wait_for_open: bool = True) -> None: 27 | super().__init__() 28 | self.objects: dict[str, Object] = {} 29 | self.robots: dict[str, Robot] = {} 30 | 31 | self.vis = meshcat.Visualizer() 32 | if open: 33 | self.vis.open() 34 | if wait_for_open: 35 | self.vis.wait() 36 | self.set_background_color() 37 | 38 | "Variables used to control the camera" 39 | self._camera_vis = self.vis["/Cameras/default"] 40 | self._camera_pose = ArrayWithCallbackOnSetItem(np.eye(4), cb=self._set_camera_transform) 41 | self._camera_pose_modified = False 42 | self._camera_zoom = 1.0 43 | 44 | " Variables used internally in case we are rendering to animation " 45 | self._animation: Animation | None = None 46 | self._animation_frame: AnimationFrameVisualizer | None = None 47 | self._animation_frame_counter: itertools.count | None = None 48 | 49 | " Variables used internally to write frames of the video " 50 | self._video_writer = None 51 | 52 | def add_object(self, obj: Object, verbose: bool = True): 53 | if verbose and obj.name in self.objects: 54 | print('Object with the same name is already inside the scene, it will be replaced. ') 55 | if verbose and self._animation is not None: 56 | print( 57 | 'Adding objects while animating is not allowed.' 58 | 'You need to add all objects before starting the animation.' 59 | ) 60 | return 61 | self.objects[obj.name] = obj 62 | obj._set_vis(self.vis) 63 | obj._set_object() 64 | 65 | def remove_object(self, obj: Object, verbose: bool = True): 66 | if verbose and self._animation is not None: 67 | print('Removing object while animating is not allowed.') 68 | return 69 | obj._delete_object() 70 | self.objects.pop(obj.name) 71 | 72 | def add_robot(self, robot: Robot, verbose: bool = True): 73 | if verbose and robot.name in self.robots: 74 | print('Robot with the same name is already inside the scene, it will be replaced. ') 75 | self.robots[robot.name] = robot 76 | for obj in robot._objects.values(): 77 | self.add_object(obj, verbose=verbose) 78 | 79 | def remove_robot(self, robot: Robot, verbose: bool = True): 80 | if verbose and self._animation is not None: 81 | print('Removing robots while animating is not allowed.') 82 | return 83 | self.robots.pop(robot.name) 84 | for obj in robot._objects.values(): 85 | self.remove_object(obj, verbose=verbose) 86 | 87 | def render(self): 88 | """Render current scene either to browser, video or to the next frame of the animation.""" 89 | if self._animation is not None: 90 | self._reset_all_properties() 91 | self._next_animation_frame() 92 | if self._video_writer is not None: 93 | self._video_writer.append_data(np.array(self.render_image())) 94 | 95 | # 96 | def video_recording( 97 | self, filename: Path | str | None = None, fps: int = 30, directory: Path | str | None = None, **kwargs 98 | ): 99 | """Return a context manager for video recording. 100 | The output filename is given by: 101 | 1) filename parameter if it is not None 102 | 2) directory/timestemp.mp4 if filename is None and directory is not None 103 | 3) /tmp/timestemp if filename is None and directory is None 104 | """ 105 | if filename is None: 106 | if directory is None: 107 | directory = gettempdir() 108 | filename = Path(directory).joinpath(time.strftime("%Y%m%d_%H%M%S.mp4")) 109 | return VideoContext(scene=self, fps=fps, filename=filename, **kwargs) 110 | 111 | def render_image(self) -> Image: 112 | return self.vis.get_image() 113 | 114 | def __getitem__(self, item): 115 | return self.objects[item] if item in self.objects else self.robots[item] 116 | 117 | def clear(self): 118 | """Remove all the objects/robots from the scene""" 119 | for r in list(self.robots.values()): 120 | self.remove_robot(r) 121 | for o in list(self.objects.values()): 122 | self.remove_object(o) 123 | 124 | def set_background_color(self, top_color=None, bottom_color=None): 125 | """Set background color of the visualizer. Use white background by default.""" 126 | clr = Object._color_from_input(top_color, default=np.ones(3)) 127 | self.vis["/Background"].set_property("top_color", clr.tolist()) 128 | clr = Object._color_from_input(bottom_color, default=clr) 129 | self.vis["/Background"].set_property("bottom_color", clr.tolist()) 130 | 131 | """=== The following set of functions handle animations ===""" 132 | 133 | def animation(self, fps: int = 30): 134 | """Return context of the animation that allow us to record animations. 135 | Usage: 136 | with scene.animation(fps=30): 137 | scene['obj'].pos = 3. # set properties of a first frame 138 | scene.render() # create a second frame of animation 139 | scene['obj'].pos = 3. 140 | """ 141 | return AnimationContext(scene=self, fps=fps) 142 | 143 | def _next_animation_frame(self): 144 | """Close the current frame (if exists) and create a new one. Applicable only if animation exists. Set objects 145 | visualizer to the current frame.""" 146 | assert self._animation is not None 147 | if self._animation_frame is not None: 148 | self._animation_frame.__exit__() 149 | self._animation_frame = self._animation.at_frame(self.vis, next(self._animation_frame_counter)) 150 | self._animation_frame.__enter__() 151 | "Set all objects visualizers to the frame" 152 | for o in self.objects.values(): 153 | o._set_vis(self._animation_frame) 154 | self._camera_vis = self._animation_frame["/Cameras/default"] 155 | 156 | def _close_animation(self): 157 | """Close the current frame and set objects visualizer to online.""" 158 | assert self._animation is not None 159 | self._animation_frame.__exit__() 160 | self._animation_frame = None 161 | self._animation_frame_counter = None 162 | self._animation = None 163 | for o in self.objects.values(): 164 | o._set_vis(self.vis) 165 | self._camera_vis = self.vis["/Cameras/default"] 166 | 167 | def _start_animation(self, fps): 168 | """Start animation instead of online changes.""" 169 | self._animation_frame_counter = itertools.count() 170 | self._animation = Animation(default_framerate=fps) 171 | self._next_animation_frame() 172 | 173 | def _reset_all_properties(self): 174 | """Send all the properties to the browser. It is reset for all animation frames to ensure values do not change 175 | due to the interpolation by the properties set in the future.""" 176 | for o in self.objects.values(): 177 | o._reset_all_properties() 178 | self.camera_zoom = self._camera_zoom # this will set the starting value of the property 179 | if self._camera_pose_modified: 180 | self.camera_pose = self._camera_pose 181 | 182 | """=== Following functions handle the camera control ===""" 183 | 184 | @property 185 | def camera_zoom(self): 186 | return self._camera_zoom 187 | 188 | @camera_zoom.setter 189 | def camera_zoom(self, v): 190 | self._camera_zoom = v 191 | self._set_property(self._camera_vis['rotated/'], 'zoom', self._camera_zoom, 'number') 192 | 193 | @property 194 | def camera_pose(self): 195 | return self._camera_pose 196 | 197 | @camera_pose.setter 198 | def camera_pose(self, v): 199 | self._camera_pose[:, :] = v 200 | 201 | @property 202 | def camera_pos(self): 203 | return self._camera_pose[:3, 3] 204 | 205 | @camera_pos.setter 206 | def camera_pos(self, p): 207 | self._camera_pose[:3, 3] = p 208 | 209 | @property 210 | def camera_rot(self): 211 | return self._camera_pose[:3, :3] 212 | 213 | @camera_rot.setter 214 | def camera_rot(self, r): 215 | self._camera_pose[:3, :3] = r 216 | 217 | def reset_camera(self): 218 | """Reset camera to default pose and let user interact with it again.""" 219 | self._camera_pose = ArrayWithCallbackOnSetItem(np.eye(4), cb=self._set_camera_transform) 220 | self._camera_vis.set_transform(self._camera_pose) 221 | self.camera_zoom = 1.0 222 | self._camera_enable_user_control() 223 | self._camera_pose_modified = False 224 | 225 | def _set_camera_transform(self): 226 | # disable human interaction if camera pose is set 227 | self._camera_vis.set_transform(self._camera_pose) 228 | self._camera_disable_user_control() 229 | self._camera_pose_modified = True 230 | 231 | def _camera_disable_user_control(self): 232 | self._set_property(self._camera_vis['rotated/'], 'position', [0, 0, 0], 'vector') 233 | 234 | def _camera_enable_user_control(self): 235 | self._set_property(self._camera_vis['rotated/'], 'position', [3, 1, 0], 'vector') 236 | 237 | @staticmethod 238 | def _set_property(element, key, value, prop_type='number'): 239 | """Set property of the object, handle animation frames set_property internally.""" 240 | if isinstance(element, AnimationFrameVisualizer): 241 | element.set_property(key, prop_type, value) 242 | else: 243 | element.set_property(key, value) 244 | 245 | 246 | class AnimationContext: 247 | """Used to provide 'with animation' capability for the viewer.""" 248 | 249 | def __init__(self, scene: Scene, fps: int) -> None: 250 | super().__init__() 251 | self.scene: Scene = scene 252 | self.fps: int = fps 253 | 254 | def __enter__(self): 255 | self.scene._start_animation(self.fps) 256 | return self 257 | 258 | def __exit__(self, exc_type, exc_val, exc_tb): 259 | """Publish animation and clear all internal changes that were required to render to frame instead of online""" 260 | self.remove_clips_duplicates() 261 | self.scene.vis['animations/animation'].set_animation(self.scene._animation) 262 | self.scene._close_animation() 263 | 264 | def remove_clips_duplicates(self): 265 | """Meshcat doesn't like if same property as modified twice in the same frame - it does weird jumping in 266 | animation. In this function we find such a duplicates and keep only the last change of the property.""" 267 | for clip in self.scene._animation.clips.values(): 268 | for track in clip.tracks.values(): 269 | indices_to_remove = set() 270 | last_f = None 271 | for i, f in enumerate(track.frames): 272 | if f == last_f: 273 | indices_to_remove.add(i - 1) 274 | last_f = f 275 | if len(indices_to_remove) > 0: 276 | track.frames = [f for i, f in enumerate(track.frames) if i not in indices_to_remove] 277 | track.values = [v for i, v in enumerate(track.values) if i not in indices_to_remove] 278 | 279 | 280 | class VideoContext: 281 | def __init__(self, scene: Scene, fps: int, filename: str | Path, **kwargs) -> None: 282 | super().__init__() 283 | self.scene = scene 284 | self.video_writer = imageio.get_writer(uri=filename, fps=fps, **kwargs) 285 | 286 | def __enter__(self): 287 | self.scene._video_writer = self.video_writer 288 | return self 289 | 290 | def __exit__(self, exc_type, exc_val, exc_tb): 291 | self.video_writer.close() 292 | self.scene._video_writer = None 293 | -------------------------------------------------------------------------------- /tests/test_robot_q.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) CTU -- All Rights Reserved 4 | # Created on: 2024-11-27 5 | # Author: Vladimir Petrik 6 | # 7 | 8 | import unittest 9 | from pathlib import Path 10 | from robomeshcat import Robot 11 | 12 | 13 | class TestRobot(unittest.TestCase): 14 | def test_q_by_name(self): 15 | robot = Robot(urdf_path=Path(__file__).parent / 'test_urdf.urdf') 16 | 17 | robot["shoulder_pan_joint"] = 0.1 18 | self.assertAlmostEqual(robot["shoulder_pan_joint"], 0.1) 19 | self.assertAlmostEqual(robot[0], 0.1) 20 | 21 | def test_q_by_name_wrong_key(self): 22 | robot = Robot(urdf_path=Path(__file__).parent / 'test_urdf.urdf') 23 | self.assertRaises(KeyError, lambda: robot["wrong_key"]) 24 | 25 | def test_q_by_multiindex(self): 26 | robot = Robot(urdf_path=Path(__file__).parent / 'test_urdf.urdf') 27 | robot[:] = 0.1 28 | self.assertAlmostEquals(robot["shoulder_pan_joint"], 0.1) 29 | 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /tests/test_urdf.urdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | transmission_interface/SimpleTransmission 208 | 209 | PositionJointInterface 210 | 211 | 212 | 1 213 | 214 | 215 | 216 | transmission_interface/SimpleTransmission 217 | 218 | PositionJointInterface 219 | 220 | 221 | 1 222 | 223 | 224 | 225 | transmission_interface/SimpleTransmission 226 | 227 | PositionJointInterface 228 | 229 | 230 | 1 231 | 232 | 233 | 234 | transmission_interface/SimpleTransmission 235 | 236 | PositionJointInterface 237 | 238 | 239 | 1 240 | 241 | 242 | 243 | transmission_interface/SimpleTransmission 244 | 245 | PositionJointInterface 246 | 247 | 248 | 1 249 | 250 | 251 | 252 | transmission_interface/SimpleTransmission 253 | 254 | PositionJointInterface 255 | 256 | 257 | 1 258 | 259 | 260 | 261 | 262 | 263 | 264 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | --------------------------------------------------------------------------------