├── .coveragerc ├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── camera_coords.png │ ├── damaged_helmet.png │ ├── fuze.png │ ├── minexcolor.png │ ├── minexdepth.png │ ├── points.png │ ├── points2.png │ ├── rotation.gif │ ├── scene.png │ └── scissors.gif │ ├── api │ └── index.rst │ ├── conf.py │ ├── examples │ ├── cameras.rst │ ├── index.rst │ ├── lighting.rst │ ├── models.rst │ ├── offscreen.rst │ ├── quickstart.rst │ ├── scenes.rst │ └── viewer.rst │ ├── index.rst │ └── install │ └── index.rst ├── examples ├── duck.py ├── example.py └── models │ ├── WaterBottle.glb │ ├── drill.obj │ ├── drill.obj.mtl │ ├── drill_uv.png │ ├── fuze.obj │ ├── fuze.obj.mtl │ ├── fuze_uv.jpg │ ├── wood.obj │ ├── wood.obj.mtl │ └── wood_uv.png ├── pyrender ├── __init__.py ├── camera.py ├── constants.py ├── font.py ├── fonts │ ├── OpenSans-Bold.ttf │ ├── OpenSans-BoldItalic.ttf │ ├── OpenSans-ExtraBold.ttf │ ├── OpenSans-ExtraBoldItalic.ttf │ ├── OpenSans-Italic.ttf │ ├── OpenSans-Light.ttf │ ├── OpenSans-LightItalic.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-Semibold.ttf │ └── OpenSans-SemiboldItalic.ttf ├── light.py ├── material.py ├── mesh.py ├── node.py ├── offscreen.py ├── platforms │ ├── __init__.py │ ├── base.py │ ├── egl.py │ ├── osmesa.py │ └── pyglet_platform.py ├── primitive.py ├── renderer.py ├── sampler.py ├── scene.py ├── shader_program.py ├── shaders │ ├── debug_quad.frag │ ├── debug_quad.vert │ ├── flat.frag │ ├── flat.vert │ ├── mesh.frag │ ├── mesh.vert │ ├── mesh_depth.frag │ ├── mesh_depth.vert │ ├── segmentation.frag │ ├── segmentation.vert │ ├── text.frag │ ├── text.vert │ ├── vertex_normals.frag │ ├── vertex_normals.geom │ ├── vertex_normals.vert │ └── vertex_normals_pc.geom ├── texture.py ├── trackball.py ├── utils.py ├── version.py └── viewer.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── data ├── Duck.glb ├── WaterBottle.glb ├── drill.obj ├── drill.obj.mtl ├── drill_uv.png ├── fuze.obj ├── fuze.obj.mtl ├── fuze_uv.jpg ├── wood.obj ├── wood.obj.mtl └── wood_uv.png ├── pytest.ini └── unit ├── __init__.py ├── test_cameras.py ├── test_egl.py ├── test_lights.py ├── test_meshes.py ├── test_nodes.py ├── test_offscreen.py └── test_scenes.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | def __repr__ 4 | def __str__ 5 | @abc.abstractmethod 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E231,W504,F405,F403 3 | max-line-length = 79 4 | select = B,C,E,F,W,T4,B9 5 | exclude = 6 | docs/source/conf.py, 7 | __pycache__, 8 | examples/* 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | docs/**/generated/** 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | 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 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://gitlab.com/pycqa/flake8 3 | rev: 3.7.1 4 | hooks: 5 | - id: flake8 6 | exclude: ^setup.py 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | dist: xenial 4 | 5 | python: 6 | - '3.6' 7 | - '3.7' 8 | 9 | before_install: 10 | # Pre-install osmesa 11 | - sudo apt update 12 | - sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb 13 | - sudo dpkg -i ./mesa_18.3.3-0.deb || true 14 | - sudo apt install -f 15 | - git clone https://github.com/mmatl/pyopengl.git 16 | - cd pyopengl 17 | - pip install . 18 | - cd .. 19 | 20 | install: 21 | - pip install . 22 | # - pip install -q pytest pytest-cov coveralls 23 | - pip install pytest pytest-cov coveralls 24 | - pip install ./pyopengl 25 | 26 | script: 27 | - PYOPENGL_PLATFORM=osmesa pytest --cov=pyrender tests 28 | 29 | after_success: 30 | - coveralls || true 31 | 32 | deploy: 33 | provider: pypi 34 | skip_existing: true 35 | user: mmatl 36 | on: 37 | tags: true 38 | branch: master 39 | password: 40 | secure: O4WWMbTYb2eVYIO4mMOVa6/xyhX7mPvJpd96cxfNvJdyuqho8VapOhzqsI5kahMB1hFjWWr61yR4+Ru5hoDYf3XA6BQVk8eCY9+0H7qRfvoxex71lahKAqfHLMoE1xNdiVTgl+QN9hYjOnopLod24rx8I8eXfpHu/mfCpuTYGyLlNcDP5St3bXpXLPB5wg8Jo1YRRv6W/7fKoXyuWjewk9cJAS0KrEgnDnSkdwm6Pb+80B2tcbgdGvpGaByw5frndwKiMUMgVUownepDU5POQq2p29wwn9lCvRucULxjEgO+63jdbZRj5fNutLarFa2nISfYnrd72LOyDfbJubwAzzAIsy2JbFORyeHvCgloiuE9oE7a9oOQt/1QHBoIV0seiawMWn55Yp70wQ7HlJs4xSGJWCGa5+9883QRNsvj420atkb3cgO8P+PXwiwTi78Dq7Z/xHqccsU0b8poqBneQoA+pUGgNnF6V7Z8e9RsCcse2gAWSZWuOK3ua+9xCgH7I7MeL3afykr2aJ+yFCoYJMFrUjJeodMX2RbL0q+3FzIPZeGW3WdhTEAL9TSKRcJBSQTskaQlZx/OcpobxS7t3d2S68CCLG9uMTqOTYws55WZ1etalA75sRk9K2MR7ZGjZW3jdtvMViISc/t6Rrjea1GE8ZHGJC6/IeLIWA2c7nc= 41 | distributions: sdist bdist_wheel 42 | notifications: 43 | email: false 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthew Matl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license 2 | include LICENSE 3 | include README.rst 4 | include pyrender/fonts/* 5 | include pyrender/shaders/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyrender 2 | 3 | [![Build Status](https://travis-ci.org/mmatl/pyrender.svg?branch=master)](https://travis-ci.org/mmatl/pyrender) 4 | [![Documentation Status](https://readthedocs.org/projects/pyrender/badge/?version=latest)](https://pyrender.readthedocs.io/en/latest/?badge=latest) 5 | [![Coverage Status](https://coveralls.io/repos/github/mmatl/pyrender/badge.svg?branch=master)](https://coveralls.io/github/mmatl/pyrender?branch=master) 6 | [![PyPI version](https://badge.fury.io/py/pyrender.svg)](https://badge.fury.io/py/pyrender) 7 | [![Downloads](https://pepy.tech/badge/pyrender)](https://pepy.tech/project/pyrender) 8 | 9 | Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based 10 | rendering and visualization. 11 | It is designed to meet the [glTF 2.0 specification from Khronos](https://www.khronos.org/gltf/). 12 | 13 | Pyrender is lightweight, easy to install, and simple to use. 14 | It comes packaged with both an intuitive scene viewer and a headache-free 15 | offscreen renderer with support for GPU-accelerated rendering on headless 16 | servers, which makes it perfect for machine learning applications. 17 | 18 | Extensive documentation, including a quickstart guide, is provided [here](https://pyrender.readthedocs.io/en/latest/). 19 | 20 | For a minimal working example of GPU-accelerated offscreen rendering using EGL, 21 | check out the [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing). 22 | 23 | 24 |

25 | GIF of Viewer 26 | Damaged Helmet 27 |

28 | 29 | ## Installation 30 | You can install pyrender directly from pip. 31 | 32 | ```bash 33 | pip install pyrender 34 | ``` 35 | 36 | ## Features 37 | 38 | Despite being lightweight, pyrender has lots of features, including: 39 | 40 | * Simple interoperation with the amazing [trimesh](https://github.com/mikedh/trimesh) project, 41 | which enables out-of-the-box support for dozens of mesh types, including OBJ, 42 | STL, DAE, OFF, PLY, and GLB. 43 | * An easy-to-use scene viewer with support for animation, showing face and vertex 44 | normals, toggling lighting conditions, and saving images and GIFs. 45 | * An offscreen rendering module that supports OSMesa and EGL backends. 46 | * Shadow mapping for directional and spot lights. 47 | * Metallic-roughness materials for physically-based rendering, including several 48 | types of texture and normal mapping. 49 | * Transparency. 50 | * Depth and color image generation. 51 | 52 | ## Sample Usage 53 | 54 | For sample usage, check out the [quickstart 55 | guide](https://pyrender.readthedocs.io/en/latest/examples/index.html) or one of 56 | the Google CoLab Notebooks: 57 | 58 | * [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing) 59 | 60 | ## Viewer Keyboard and Mouse Controls 61 | 62 | When using the viewer, the basic controls for moving about the scene are as follows: 63 | 64 | * To rotate the camera about the center of the scene, hold the left mouse button and drag the cursor. 65 | * To rotate the camera about its viewing axis, hold `CTRL` left mouse button and drag the cursor. 66 | * To pan the camera, do one of the following: 67 | * Hold `SHIFT`, then hold the left mouse button and drag the cursor. 68 | * Hold the middle mouse button and drag the cursor. 69 | * To zoom the camera in or out, do one of the following: 70 | * Scroll the mouse wheel. 71 | * Hold the right mouse button and drag the cursor. 72 | 73 | The available keyboard commands are as follows: 74 | 75 | * `a`: Toggles rotational animation mode. 76 | * `c`: Toggles backface culling. 77 | * `f`: Toggles fullscreen mode. 78 | * `h`: Toggles shadow rendering. 79 | * `i`: Toggles axis display mode (no axes, world axis, mesh axes, all axes). 80 | * `l`: Toggles lighting mode (scene lighting, Raymond lighting, or direct lighting). 81 | * `m`: Toggles face normal visualization. 82 | * `n`: Toggles vertex normal visualization. 83 | * `o`: Toggles orthographic camera mode. 84 | * `q`: Quits the viewer. 85 | * `r`: Starts recording a GIF, and pressing again stops recording and opens a file dialog. 86 | * `s`: Opens a file dialog to save the current view as an image. 87 | * `w`: Toggles wireframe mode (scene default, flip wireframes, all wireframe, or all solid). 88 | * `z`: Resets the camera to the default view. 89 | 90 | As a note, displaying shadows significantly slows down rendering, so if you're 91 | experiencing low framerates, just kill shadows or reduce the number of lights in 92 | your scene. 93 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | clean: 17 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 18 | rm -rf ./source/generated/* 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/camera_coords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/camera_coords.png -------------------------------------------------------------------------------- /docs/source/_static/damaged_helmet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/damaged_helmet.png -------------------------------------------------------------------------------- /docs/source/_static/fuze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/fuze.png -------------------------------------------------------------------------------- /docs/source/_static/minexcolor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/minexcolor.png -------------------------------------------------------------------------------- /docs/source/_static/minexdepth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/minexdepth.png -------------------------------------------------------------------------------- /docs/source/_static/points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/points.png -------------------------------------------------------------------------------- /docs/source/_static/points2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/points2.png -------------------------------------------------------------------------------- /docs/source/_static/rotation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/rotation.gif -------------------------------------------------------------------------------- /docs/source/_static/scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/scene.png -------------------------------------------------------------------------------- /docs/source/_static/scissors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/docs/source/_static/scissors.gif -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | Pyrender API Documentation 2 | ========================== 3 | 4 | Constants 5 | --------- 6 | .. automodapi:: pyrender.constants 7 | :no-inheritance-diagram: 8 | :no-main-docstr: 9 | :no-heading: 10 | 11 | Cameras 12 | ------- 13 | .. automodapi:: pyrender.camera 14 | :no-inheritance-diagram: 15 | :no-main-docstr: 16 | :no-heading: 17 | 18 | Lighting 19 | -------- 20 | .. automodapi:: pyrender.light 21 | :no-inheritance-diagram: 22 | :no-main-docstr: 23 | :no-heading: 24 | 25 | Objects 26 | ------- 27 | .. automodapi:: pyrender 28 | :no-inheritance-diagram: 29 | :no-main-docstr: 30 | :no-heading: 31 | :skip: Camera, DirectionalLight, Light, OffscreenRenderer, Node 32 | :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags 33 | :skip: Renderer, Scene, SpotLight, TextAlign, Viewer, GLTF 34 | 35 | Scenes 36 | ------ 37 | .. automodapi:: pyrender 38 | :no-inheritance-diagram: 39 | :no-main-docstr: 40 | :no-heading: 41 | :skip: Camera, DirectionalLight, Light, OffscreenRenderer 42 | :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags 43 | :skip: Renderer, SpotLight, TextAlign, Viewer, Sampler, Texture, Material 44 | :skip: MetallicRoughnessMaterial, Primitive, Mesh, GLTF 45 | 46 | On-Screen Viewer 47 | ---------------- 48 | .. automodapi:: pyrender.viewer 49 | :no-inheritance-diagram: 50 | :no-inherited-members: 51 | :no-main-docstr: 52 | :no-heading: 53 | 54 | Off-Screen Rendering 55 | -------------------- 56 | .. automodapi:: pyrender.offscreen 57 | :no-inheritance-diagram: 58 | :no-main-docstr: 59 | :no-heading: 60 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # core documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Oct 16 14:33:48 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | from pyrender import __version__ 18 | from sphinx.domains.python import PythonDomain 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath('../../')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.autosummary', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.githubpages', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.napoleon', 40 | 'sphinx.ext.viewcode', 41 | 'sphinx_automodapi.automodapi', 42 | 'sphinx_automodapi.smart_resolver' 43 | ] 44 | numpydoc_class_members_toctree = False 45 | automodapi_toctreedirnm = 'generated' 46 | automodsumm_inherited_members = True 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The encoding of source files. 57 | #source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = u'pyrender' 64 | copyright = u'2018, Matthew Matl' 65 | author = u'Matthew Matl' 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | # 71 | # The short X.Y version. 72 | version = __version__ 73 | # The full version, including alpha/beta/rc tags. 74 | release = __version__ 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # 79 | # This is also used if you do content translation via gettext catalogs. 80 | # Usually you set "language" from the command line for these cases. 81 | language = None 82 | 83 | # There are two options for replacing |today|: either, you set today to some 84 | # non-false value, then it is used: 85 | #today = '' 86 | # Else, today_fmt is used as the format for a strftime call. 87 | #today_fmt = '%B %d, %Y' 88 | 89 | # List of patterns, relative to source directory, that match files and 90 | # directories to ignore when looking for source files. 91 | exclude_patterns = [] 92 | 93 | # The reST default role (used for this markup: `text`) to use for all 94 | # documents. 95 | #default_role = None 96 | 97 | # If true, '()' will be appended to :func: etc. cross-reference text. 98 | #add_function_parentheses = True 99 | 100 | # If true, the current module name will be prepended to all description 101 | # unit titles (such as .. function::). 102 | #add_module_names = True 103 | 104 | # If true, sectionauthor and moduleauthor directives will be shown in the 105 | # output. They are ignored by default. 106 | #show_authors = False 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # A list of ignored prefixes for module index sorting. 112 | #modindex_common_prefix = [] 113 | 114 | # If true, keep warnings as "system message" paragraphs in the built documents. 115 | #keep_warnings = False 116 | 117 | # If true, `todo` and `todoList` produce output, else they produce nothing. 118 | todo_include_todos = False 119 | 120 | 121 | # -- Options for HTML output ---------------------------------------------- 122 | 123 | # The theme to use for HTML and HTML Help pages. See the documentation for 124 | # a list of builtin themes. 125 | import sphinx_rtd_theme 126 | html_theme = 'sphinx_rtd_theme' 127 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 128 | 129 | # Theme options are theme-specific and customize the look and feel of a theme 130 | # further. For a list of options available for each theme, see the 131 | # documentation. 132 | #html_theme_options = {} 133 | 134 | # Add any paths that contain custom themes here, relative to this directory. 135 | #html_theme_path = [] 136 | 137 | # The name for this set of Sphinx documents. If None, it defaults to 138 | # " v documentation". 139 | #html_title = None 140 | 141 | # A shorter title for the navigation bar. Default is the same as html_title. 142 | #html_short_title = None 143 | 144 | # The name of an image file (relative to this directory) to place at the top 145 | # of the sidebar. 146 | #html_logo = None 147 | 148 | # The name of an image file (relative to this directory) to use as a favicon of 149 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 150 | # pixels large. 151 | #html_favicon = None 152 | 153 | # Add any paths that contain custom static files (such as style sheets) here, 154 | # relative to this directory. They are copied after the builtin static files, 155 | # so a file named "default.css" will overwrite the builtin "default.css". 156 | html_static_path = ['_static'] 157 | 158 | # Add any extra paths that contain custom files (such as robots.txt or 159 | # .htaccess) here, relative to this directory. These files are copied 160 | # directly to the root of the documentation. 161 | #html_extra_path = [] 162 | 163 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 164 | # using the given strftime format. 165 | #html_last_updated_fmt = '%b %d, %Y' 166 | 167 | # If true, SmartyPants will be used to convert quotes and dashes to 168 | # typographically correct entities. 169 | #html_use_smartypants = True 170 | 171 | # Custom sidebar templates, maps document names to template names. 172 | #html_sidebars = {} 173 | 174 | # Additional templates that should be rendered to pages, maps page names to 175 | # template names. 176 | #html_additional_pages = {} 177 | 178 | # If false, no module index is generated. 179 | #html_domain_indices = True 180 | 181 | # If false, no index is generated. 182 | #html_use_index = True 183 | 184 | # If true, the index is split into individual pages for each letter. 185 | #html_split_index = False 186 | 187 | # If true, links to the reST sources are added to the pages. 188 | #html_show_sourcelink = True 189 | 190 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 191 | #html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 194 | #html_show_copyright = True 195 | 196 | # If true, an OpenSearch description file will be output, and all pages will 197 | # contain a tag referring to it. The value of this option must be the 198 | # base URL from which the finished HTML is served. 199 | #html_use_opensearch = '' 200 | 201 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 202 | #html_file_suffix = None 203 | 204 | # Language to be used for generating the HTML full-text search index. 205 | # Sphinx supports the following languages: 206 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 207 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 208 | #html_search_language = 'en' 209 | 210 | # A dictionary with options for the search language support, empty by default. 211 | # Now only 'ja' uses this config value 212 | #html_search_options = {'type': 'default'} 213 | 214 | # The name of a javascript file (relative to the configuration directory) that 215 | # implements a search results scorer. If empty, the default will be used. 216 | #html_search_scorer = 'scorer.js' 217 | 218 | # Output file base name for HTML help builder. 219 | htmlhelp_basename = 'coredoc' 220 | 221 | # -- Options for LaTeX output --------------------------------------------- 222 | 223 | latex_elements = { 224 | # The paper size ('letterpaper' or 'a4paper'). 225 | #'papersize': 'letterpaper', 226 | 227 | # The font size ('10pt', '11pt' or '12pt'). 228 | #'pointsize': '10pt', 229 | 230 | # Additional stuff for the LaTeX preamble. 231 | #'preamble': '', 232 | 233 | # Latex figure (float) alignment 234 | #'figure_align': 'htbp', 235 | } 236 | 237 | # Grouping the document tree into LaTeX files. List of tuples 238 | # (source start file, target name, title, 239 | # author, documentclass [howto, manual, or own class]). 240 | latex_documents = [ 241 | (master_doc, 'pyrender.tex', u'pyrender Documentation', 242 | u'Matthew Matl', 'manual'), 243 | ] 244 | 245 | # The name of an image file (relative to this directory) to place at the top of 246 | # the title page. 247 | #latex_logo = None 248 | 249 | # For "manual" documents, if this is true, then toplevel headings are parts, 250 | # not chapters. 251 | #latex_use_parts = False 252 | 253 | # If true, show page references after internal links. 254 | #latex_show_pagerefs = False 255 | 256 | # If true, show URL addresses after external links. 257 | #latex_show_urls = False 258 | 259 | # Documents to append as an appendix to all manuals. 260 | #latex_appendices = [] 261 | 262 | # If false, no module index is generated. 263 | #latex_domain_indices = True 264 | 265 | 266 | # -- Options for manual page output --------------------------------------- 267 | 268 | # One entry per manual page. List of tuples 269 | # (source start file, name, description, authors, manual section). 270 | man_pages = [ 271 | (master_doc, 'pyrender', u'pyrender Documentation', 272 | [author], 1) 273 | ] 274 | 275 | # If true, show URL addresses after external links. 276 | #man_show_urls = False 277 | 278 | 279 | # -- Options for Texinfo output ------------------------------------------- 280 | 281 | # Grouping the document tree into Texinfo files. List of tuples 282 | # (source start file, target name, title, author, 283 | # dir menu entry, description, category) 284 | texinfo_documents = [ 285 | (master_doc, 'pyrender', u'pyrender Documentation', 286 | author, 'pyrender', 'One line description of project.', 287 | 'Miscellaneous'), 288 | ] 289 | 290 | # Documents to append as an appendix to all manuals. 291 | #texinfo_appendices = [] 292 | 293 | # If false, no module index is generated. 294 | #texinfo_domain_indices = True 295 | 296 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 297 | #texinfo_show_urls = 'footnote' 298 | 299 | # If true, do not generate a @detailmenu in the "Top" node's menu. 300 | #texinfo_no_detailmenu = False 301 | 302 | intersphinx_mapping = { 303 | 'python' : ('https://docs.python.org/', None), 304 | 'pyrender' : ('https://pyrender.readthedocs.io/en/latest/', None), 305 | } 306 | 307 | # Autosummary fix 308 | autosummary_generate = True 309 | 310 | # Try to suppress multiple-definition warnings by always taking the shorter 311 | # path when two or more paths have the same base module 312 | 313 | class MyPythonDomain(PythonDomain): 314 | 315 | def find_obj(self, env, modname, classname, name, type, searchmode=0): 316 | """Ensures an object always resolves to the desired module 317 | if defined there.""" 318 | orig_matches = PythonDomain.find_obj( 319 | self, env, modname, classname, name, type, searchmode 320 | ) 321 | 322 | if len(orig_matches) <= 1: 323 | return orig_matches 324 | 325 | # If multiple matches, try to take the shortest if all the modules are 326 | # the same 327 | first_match_name_sp = orig_matches[0][0].split('.') 328 | base_name = first_match_name_sp[0] 329 | min_len = len(first_match_name_sp) 330 | best_match = orig_matches[0] 331 | 332 | for match in orig_matches[1:]: 333 | match_name = match[0] 334 | match_name_sp = match_name.split('.') 335 | match_base = match_name_sp[0] 336 | 337 | # If we have mismatched bases, return them all to trigger warnings 338 | if match_base != base_name: 339 | return orig_matches 340 | 341 | # Otherwise, check and see if it's shorter 342 | if len(match_name_sp) < min_len: 343 | min_len = len(match_name_sp) 344 | best_match = match 345 | 346 | return (best_match,) 347 | 348 | 349 | def setup(sphinx): 350 | """Use MyPythonDomain in place of PythonDomain""" 351 | sphinx.override_domain(MyPythonDomain) 352 | 353 | -------------------------------------------------------------------------------- /docs/source/examples/cameras.rst: -------------------------------------------------------------------------------- 1 | .. _camera_guide: 2 | 3 | Creating Cameras 4 | ================ 5 | 6 | Pyrender supports three camera types -- :class:`.PerspectiveCamera` and 7 | :class:`.IntrinsicsCamera` types, 8 | which render scenes as a human would see them, and 9 | :class:`.OrthographicCamera` types, which preserve distances between points. 10 | 11 | Creating cameras is easy -- just specify their basic attributes: 12 | 13 | >>> pc = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414) 14 | >>> oc = pyrender.OrthographicCamera(xmag=1.0, ymag=1.0) 15 | 16 | For more information, see the Khronos group's documentation here_: 17 | 18 | .. _here: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#projection-matrices 19 | 20 | When you add cameras to the scene, make sure that you're using OpenGL camera 21 | coordinates to specify their pose. See the illustration below for details. 22 | Basically, the camera z-axis points away from the scene, the x-axis points 23 | right in image space, and the y-axis points up in image space. 24 | 25 | .. image:: /_static/camera_coords.png 26 | 27 | -------------------------------------------------------------------------------- /docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | .. _guide: 2 | 3 | User Guide 4 | ========== 5 | 6 | This section contains guides on how to use Pyrender to quickly visualize 7 | your 3D data, including a quickstart guide and more detailed descriptions 8 | of each part of the rendering pipeline. 9 | 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | quickstart.rst 15 | models.rst 16 | lighting.rst 17 | cameras.rst 18 | scenes.rst 19 | offscreen.rst 20 | viewer.rst 21 | -------------------------------------------------------------------------------- /docs/source/examples/lighting.rst: -------------------------------------------------------------------------------- 1 | .. _lighting_guide: 2 | 3 | Creating Lights 4 | =============== 5 | 6 | Pyrender supports three types of punctual light: 7 | 8 | - :class:`.PointLight`: Point-based light sources, such as light bulbs. 9 | - :class:`.SpotLight`: A conical light source, like a flashlight. 10 | - :class:`.DirectionalLight`: A general light that does not attenuate with 11 | distance. 12 | 13 | Creating lights is easy -- just specify their basic attributes: 14 | 15 | >>> pl = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0) 16 | >>> sl = pyrender.SpotLight(color=[1.0, 1.0, 1.0], intensity=2.0, 17 | ... innerConeAngle=0.05, outerConeAngle=0.5) 18 | >>> dl = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.0) 19 | 20 | For more information about how these lighting models are implemented, 21 | see their class documentation. 22 | -------------------------------------------------------------------------------- /docs/source/examples/models.rst: -------------------------------------------------------------------------------- 1 | .. _model_guide: 2 | 3 | Loading and Configuring Models 4 | ============================== 5 | The first step to any rendering application is loading your models. 6 | Pyrender implements the GLTF 2.0 specification, which means that all 7 | models are composed of a hierarchy of objects. 8 | 9 | At the top level, we have a :class:`.Mesh`. The :class:`.Mesh` is 10 | basically a wrapper of any number of :class:`.Primitive` types, 11 | which actually represent geometry that can be drawn to the screen. 12 | 13 | Primitives are composed of a variety of parameters, including 14 | vertex positions, vertex normals, color and texture information, 15 | and triangle indices if smooth rendering is desired. 16 | They can implement point clouds, triangular meshes, or lines 17 | depending on how you configure their data and set their 18 | :attr:`.Primitive.mode` parameter. 19 | 20 | Although you can create primitives yourself if you want to, 21 | it's probably easier to just use the utility functions provided 22 | in the :class:`.Mesh` class. 23 | 24 | Creating Triangular Meshes 25 | -------------------------- 26 | 27 | Simple Construction 28 | ~~~~~~~~~~~~~~~~~~~ 29 | Pyrender allows you to create a :class:`.Mesh` containing a 30 | triangular mesh model directly from a :class:`~trimesh.base.Trimesh` object 31 | using the :meth:`.Mesh.from_trimesh` static method. 32 | 33 | >>> import trimesh 34 | >>> import pyrender 35 | >>> import numpy as np 36 | >>> tm = trimesh.load('examples/models/fuze.obj') 37 | >>> m = pyrender.Mesh.from_trimesh(tm) 38 | >>> m.primitives 39 | [] 40 | 41 | You can also create a single :class:`.Mesh` from a list of 42 | :class:`~trimesh.base.Trimesh` objects: 43 | 44 | >>> tms = [trimesh.creation.icosahedron(), trimesh.creation.cylinder()] 45 | >>> m = pyrender.Mesh.from_trimesh(tms) 46 | [, 47 | ] 48 | 49 | Vertex Smoothing 50 | ~~~~~~~~~~~~~~~~ 51 | 52 | The :meth:`.Mesh.from_trimesh` method has a few additional optional parameters. 53 | If you want to render the mesh without interpolating face normals, which can 54 | be useful for meshes that are supposed to be angular (e.g. a cube), you 55 | can specify ``smooth=False``. 56 | 57 | >>> m = pyrender.Mesh.from_trimesh(tm, smooth=False) 58 | 59 | Per-Face or Per-Vertex Coloration 60 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | 62 | If you have an untextured trimesh, you can color it in with per-face or 63 | per-vertex colors: 64 | 65 | >>> tm.visual.vertex_colors = np.random.uniform(size=tm.vertices.shape) 66 | >>> tm.visual.face_colors = np.random.uniform(size=tm.faces.shape) 67 | >>> m = pyrender.Mesh.from_trimesh(tm) 68 | 69 | Instancing 70 | ~~~~~~~~~~ 71 | 72 | If you want to render many copies of the same mesh at different poses, 73 | you can statically create a vast array of them in an efficient manner. 74 | Simply specify the ``poses`` parameter to be a list of ``N`` 4x4 homogenous 75 | transformation matrics that position the meshes relative to their common 76 | base frame: 77 | 78 | >>> tfs = np.tile(np.eye(4), (3,1,1)) 79 | >>> tfs[1,:3,3] = [0.1, 0.0, 0.0] 80 | >>> tfs[2,:3,3] = [0.2, 0.0, 0.0] 81 | >>> tfs 82 | array([[[1. , 0. , 0. , 0. ], 83 | [0. , 1. , 0. , 0. ], 84 | [0. , 0. , 1. , 0. ], 85 | [0. , 0. , 0. , 1. ]], 86 | [[1. , 0. , 0. , 0.1], 87 | [0. , 1. , 0. , 0. ], 88 | [0. , 0. , 1. , 0. ], 89 | [0. , 0. , 0. , 1. ]], 90 | [[1. , 0. , 0. , 0.2], 91 | [0. , 1. , 0. , 0. ], 92 | [0. , 0. , 1. , 0. ], 93 | [0. , 0. , 0. , 1. ]]]) 94 | 95 | >>> m = pyrender.Mesh.from_trimesh(tm, poses=tfs) 96 | 97 | Custom Materials 98 | ~~~~~~~~~~~~~~~~ 99 | 100 | You can also specify a custom material for any triangular mesh you create 101 | in the ``material`` parameter of :meth:`.Mesh.from_trimesh`. 102 | The main material supported by Pyrender is the 103 | :class:`.MetallicRoughnessMaterial`. 104 | The metallic-roughness model supports rendering highly-realistic objects across 105 | a wide gamut of materials. 106 | 107 | For more information, see the documentation of the 108 | :class:`.MetallicRoughnessMaterial` constructor or look at the Khronos_ 109 | documentation for more information. 110 | 111 | .. _Khronos: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials 112 | 113 | Creating Point Clouds 114 | --------------------- 115 | 116 | Point Sprites 117 | ~~~~~~~~~~~~~ 118 | Pyrender also allows you to create a :class:`.Mesh` containing a 119 | point cloud directly from :class:`numpy.ndarray` instances 120 | using the :meth:`.Mesh.from_points` static method. 121 | 122 | Simply provide a list of points and optional per-point colors and normals. 123 | 124 | >>> pts = tm.vertices.copy() 125 | >>> colors = np.random.uniform(size=pts.shape) 126 | >>> m = pyrender.Mesh.from_points(pts, colors=colors) 127 | 128 | Point clouds created in this way will be rendered as square point sprites. 129 | 130 | .. image:: /_static/points.png 131 | 132 | Point Spheres 133 | ~~~~~~~~~~~~~ 134 | If you have a monochromatic point cloud and would like to render it with 135 | spheres, you can render it by instancing a spherical trimesh: 136 | 137 | >>> sm = trimesh.creation.uv_sphere(radius=0.1) 138 | >>> sm.visual.vertex_colors = [1.0, 0.0, 0.0] 139 | >>> tfs = np.tile(np.eye(4), (len(pts), 1, 1)) 140 | >>> tfs[:,:3,3] = pts 141 | >>> m = pyrender.Mesh.from_trimesh(sm, poses=tfs) 142 | 143 | .. image:: /_static/points2.png 144 | -------------------------------------------------------------------------------- /docs/source/examples/offscreen.rst: -------------------------------------------------------------------------------- 1 | .. _offscreen_guide: 2 | 3 | Offscreen Rendering 4 | =================== 5 | 6 | .. note:: 7 | If you're using a headless server, you'll need to use either EGL (for 8 | GPU-accelerated rendering) or OSMesa (for CPU-only software rendering). 9 | If you're using OSMesa, be sure that you've installed it properly. See 10 | :ref:`osmesa` for details. 11 | 12 | Choosing a Backend 13 | ------------------ 14 | 15 | Once you have a scene set up with its geometry, cameras, and lights, 16 | you can render it using the :class:`.OffscreenRenderer`. Pyrender supports 17 | three backends for offscreen rendering: 18 | 19 | - Pyglet, the same engine that runs the viewer. This requires an active 20 | display manager, so you can't run it on a headless server. This is the 21 | default option. 22 | - OSMesa, a software renderer. 23 | - EGL, which allows for GPU-accelerated rendering without a display manager. 24 | 25 | If you want to use OSMesa or EGL, you need to set the ``PYOPENGL_PLATFORM`` 26 | environment variable before importing pyrender or any other OpenGL library. 27 | You can do this at the command line: 28 | 29 | .. code-block:: bash 30 | 31 | PYOPENGL_PLATFORM=osmesa python render.py 32 | 33 | or at the top of your Python script: 34 | 35 | .. code-block:: bash 36 | 37 | # Top of main python script 38 | import os 39 | os.environ['PYOPENGL_PLATFORM'] = 'egl' 40 | 41 | The handle for EGL is ``egl``, and the handle for OSMesa is ``osmesa``. 42 | 43 | Running the Renderer 44 | -------------------- 45 | 46 | Once you've set your environment variable appropriately, create your scene and 47 | then configure the :class:`.OffscreenRenderer` object with a window width, 48 | a window height, and a size for point-cloud points: 49 | 50 | >>> r = pyrender.OffscreenRenderer(viewport_width=640, 51 | ... viewport_height=480, 52 | ... point_size=1.0) 53 | 54 | Then, just call the :meth:`.OffscreenRenderer.render` function: 55 | 56 | >>> color, depth = r.render(scene) 57 | 58 | .. image:: /_static/scene.png 59 | 60 | This will return a ``(w,h,3)`` channel floating-point color image and 61 | a ``(w,h)`` floating-point depth image rendered from the scene's main camera. 62 | 63 | You can customize the rendering process by using flag options from 64 | :class:`.RenderFlags` and bitwise or-ing them together. For example, 65 | the following code renders a color image with an alpha channel 66 | and enables shadow mapping for all directional lights: 67 | 68 | >>> flags = RenderFlags.RGBA | RenderFlags.SHADOWS_DIRECTIONAL 69 | >>> color, depth = r.render(scene, flags=flags) 70 | 71 | Once you're done with the offscreen renderer, you need to close it before you 72 | can run a different renderer or open the viewer for the same scene: 73 | 74 | >>> r.delete() 75 | 76 | Google CoLab Examples 77 | --------------------- 78 | 79 | For a minimal working example of offscreen rendering using OSMesa, 80 | see the `OSMesa Google CoLab notebook`_. 81 | 82 | .. _OSMesa Google CoLab notebook: https://colab.research.google.com/drive/1Z71mHIc-Sqval92nK290vAsHZRUkCjUx 83 | 84 | For a minimal working example of offscreen rendering using EGL, 85 | see the `EGL Google CoLab notebook`_. 86 | 87 | .. _EGL Google CoLab notebook: https://colab.research.google.com/drive/1rTLHk0qxh4dn8KNe-mCnN8HAWdd2_BEh 88 | -------------------------------------------------------------------------------- /docs/source/examples/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart_guide: 2 | 3 | Quickstart 4 | ========== 5 | 6 | 7 | Minimal Example for 3D Viewer 8 | ----------------------------- 9 | Here is a minimal example of loading and viewing a triangular mesh model 10 | in pyrender. 11 | 12 | >>> import trimesh 13 | >>> import pyrender 14 | >>> fuze_trimesh = trimesh.load('examples/models/fuze.obj') 15 | >>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh) 16 | >>> scene = pyrender.Scene() 17 | >>> scene.add(mesh) 18 | >>> pyrender.Viewer(scene, use_raymond_lighting=True) 19 | 20 | .. image:: /_static/fuze.png 21 | 22 | 23 | Minimal Example for Offscreen Rendering 24 | --------------------------------------- 25 | .. note:: 26 | If you're using a headless server, make sure that you followed the guide 27 | for installing OSMesa. See :ref:`osmesa`. 28 | 29 | Here is a minimal example of rendering a mesh model offscreen in pyrender. 30 | The only additional necessities are that you need to add lighting and a camera. 31 | 32 | >>> import numpy as np 33 | >>> import trimesh 34 | >>> import pyrender 35 | >>> import matplotlib.pyplot as plt 36 | 37 | >>> fuze_trimesh = trimesh.load('examples/models/fuze.obj') 38 | >>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh) 39 | >>> scene = pyrender.Scene() 40 | >>> scene.add(mesh) 41 | >>> camera = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.0) 42 | >>> s = np.sqrt(2)/2 43 | >>> camera_pose = np.array([ 44 | ... [0.0, -s, s, 0.3], 45 | ... [1.0, 0.0, 0.0, 0.0], 46 | ... [0.0, s, s, 0.35], 47 | ... [0.0, 0.0, 0.0, 1.0], 48 | ... ]) 49 | >>> scene.add(camera, pose=camera_pose) 50 | >>> light = pyrender.SpotLight(color=np.ones(3), intensity=3.0, 51 | ... innerConeAngle=np.pi/16.0, 52 | ... outerConeAngle=np.pi/6.0) 53 | >>> scene.add(light, pose=camera_pose) 54 | >>> r = pyrender.OffscreenRenderer(400, 400) 55 | >>> color, depth = r.render(scene) 56 | >>> plt.figure() 57 | >>> plt.subplot(1,2,1) 58 | >>> plt.axis('off') 59 | >>> plt.imshow(color) 60 | >>> plt.subplot(1,2,2) 61 | >>> plt.axis('off') 62 | >>> plt.imshow(depth, cmap=plt.cm.gray_r) 63 | >>> plt.show() 64 | 65 | .. image:: /_static/minexcolor.png 66 | :width: 45% 67 | :align: left 68 | .. image:: /_static/minexdepth.png 69 | :width: 45% 70 | :align: right 71 | 72 | -------------------------------------------------------------------------------- /docs/source/examples/scenes.rst: -------------------------------------------------------------------------------- 1 | .. _scene_guide: 2 | 3 | Creating Scenes 4 | =============== 5 | 6 | Before you render anything, you need to put all of your lights, cameras, 7 | and meshes into a scene. The :class:`.Scene` object keeps track of the relative 8 | poses of these primitives by inserting them into :class:`.Node` objects and 9 | keeping them in a directed acyclic graph. 10 | 11 | Adding Objects 12 | -------------- 13 | 14 | To create a :class:`.Scene`, simply call the constructor. You can optionally 15 | specify an ambient light color and a background color: 16 | 17 | >>> scene = pyrender.Scene(ambient_light=[0.02, 0.02, 0.02], 18 | ... bg_color=[1.0, 1.0, 1.0]) 19 | 20 | You can add objects to a scene by first creating a :class:`.Node` object 21 | and adding the object and its pose to the :class:`.Node`. Poses are specified 22 | as 4x4 homogenous transformation matrices that are stored in the node's 23 | :attr:`.Node.matrix` attribute. Note that the :class:`.Node` 24 | constructor requires you to specify whether you're adding a mesh, light, 25 | or camera. 26 | 27 | >>> mesh = pyrender.Mesh.from_trimesh(tm) 28 | >>> light = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0) 29 | >>> cam = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414) 30 | >>> nm = pyrender.Node(mesh=mesh, matrix=np.eye(4)) 31 | >>> nl = pyrender.Node(light=light, matrix=np.eye(4)) 32 | >>> nc = pyrender.Node(camera=cam, matrix=np.eye(4)) 33 | >>> scene.add_node(nm) 34 | >>> scene.add_node(nl) 35 | >>> scene.add_node(nc) 36 | 37 | You can also add objects directly to a scene with the :meth:`.Scene.add` function, 38 | which takes care of creating a :class:`.Node` for you. 39 | 40 | >>> scene.add(mesh, pose=np.eye(4)) 41 | >>> scene.add(light, pose=np.eye(4)) 42 | >>> scene.add(cam, pose=np.eye(4)) 43 | 44 | Nodes can be hierarchical, in which case the node's :attr:`.Node.matrix` 45 | specifies that node's pose relative to its parent frame. You can add nodes to 46 | a scene hierarchically by specifying a parent node in your calls to 47 | :meth:`.Scene.add` or :meth:`.Scene.add_node`: 48 | 49 | >>> scene.add_node(nl, parent_node=nc) 50 | >>> scene.add(cam, parent_node=nm) 51 | 52 | If you add multiple cameras to a scene, you can specify which one to render from 53 | by setting the :attr:`.Scene.main_camera_node` attribute. 54 | 55 | Updating Objects 56 | ---------------- 57 | 58 | You can update the poses of existing nodes with the :meth:`.Scene.set_pose` 59 | function. Simply call it with a :class:`.Node` that is already in the scene 60 | and the new pose of that node with respect to its parent as a 4x4 homogenous 61 | transformation matrix: 62 | 63 | >>> scene.set_pose(nl, pose=np.eye(4)) 64 | 65 | If you want to get the local pose of a node, you can just access its 66 | :attr:`.Node.matrix` attribute. However, if you want to the get 67 | the pose of a node *with respect to the world frame*, you can call the 68 | :meth:`.Scene.get_pose` method. 69 | 70 | >>> tf = scene.get_pose(nl) 71 | 72 | Removing Objects 73 | ---------------- 74 | 75 | Finally, you can remove a :class:`.Node` and all of its children from the 76 | scene with the :meth:`.Scene.remove_node` function: 77 | 78 | >>> scene.remove_node(nl) 79 | -------------------------------------------------------------------------------- /docs/source/examples/viewer.rst: -------------------------------------------------------------------------------- 1 | .. _viewer_guide: 2 | 3 | Live Scene Viewer 4 | ================= 5 | 6 | Standard Usage 7 | -------------- 8 | In addition to the offscreen renderer, Pyrender comes with a live scene viewer. 9 | In its standard invocation, calling the :class:`.Viewer`'s constructor will 10 | immediately pop a viewing window that you can navigate around in. 11 | 12 | >>> pyrender.Viewer(scene) 13 | 14 | By default, the viewer uses your scene's lighting. If you'd like to start with 15 | some additional lighting that moves around with the camera, you can specify that 16 | with: 17 | 18 | >>> pyrender.Viewer(scene, use_raymond_lighting=True) 19 | 20 | For a full list of the many options that the :class:`.Viewer` supports, check out its 21 | documentation. 22 | 23 | .. image:: /_static/rotation.gif 24 | 25 | Running the Viewer in a Separate Thread 26 | --------------------------------------- 27 | If you'd like to animate your models, you'll want to run the viewer in a 28 | separate thread so that you can update the scene while the viewer is running. 29 | To do this, first pop the viewer in a separate thread by calling its constructor 30 | with the ``run_in_thread`` option set: 31 | 32 | >>> v = pyrender.Viewer(scene, run_in_thread=True) 33 | 34 | Then, you can manipulate the :class:`.Scene` while the viewer is running to 35 | animate things. However, be careful to acquire the viewer's 36 | :attr:`.Viewer.render_lock` before editing the scene to prevent data corruption: 37 | 38 | >>> i = 0 39 | >>> while True: 40 | ... pose = np.eye(4) 41 | ... pose[:3,3] = [i, 0, 0] 42 | ... v.render_lock.acquire() 43 | ... scene.set_pose(mesh_node, pose) 44 | ... v.render_lock.release() 45 | ... i += 0.01 46 | 47 | .. image:: /_static/scissors.gif 48 | 49 | You can wait on the viewer to be closed manually: 50 | 51 | >>> while v.is_active: 52 | ... pass 53 | 54 | Or you can close it from the main thread forcibly. 55 | Make sure to still loop and block for the viewer to actually exit before using 56 | the scene object again. 57 | 58 | >>> v.close_external() 59 | >>> while v.is_active: 60 | ... pass 61 | 62 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. core documentation master file, created by 2 | sphinx-quickstart on Sun Oct 16 14:33:48 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Pyrender Documentation 7 | ======================== 8 | Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based 9 | rendering and visualization. 10 | It is designed to meet the glTF 2.0 specification_ from Khronos 11 | 12 | .. _specification: https://www.khronos.org/gltf/ 13 | 14 | Pyrender is lightweight, easy to install, and simple to use. 15 | It comes packaged with both an intuitive scene viewer and a headache-free 16 | offscreen renderer with support for GPU-accelerated rendering on headless 17 | servers, which makes it perfect for machine learning applications. 18 | Check out the :ref:`guide` for a full tutorial, or fork me on 19 | Github_. 20 | 21 | .. _Github: https://github.com/mmatl/pyrender 22 | 23 | .. image:: _static/rotation.gif 24 | 25 | .. image:: _static/damaged_helmet.png 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | install/index.rst 31 | examples/index.rst 32 | api/index.rst 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | 42 | -------------------------------------------------------------------------------- /docs/source/install/index.rst: -------------------------------------------------------------------------------- 1 | Installation Guide 2 | ================== 3 | 4 | Python Installation 5 | ------------------- 6 | 7 | This package is available via ``pip``. 8 | 9 | .. code-block:: bash 10 | 11 | pip install pyrender 12 | 13 | If you're on MacOS, you'll need 14 | to pre-install my fork of ``pyglet``, as the version on PyPI hasn't yet included 15 | my change that enables OpenGL contexts on MacOS. 16 | 17 | .. code-block:: bash 18 | 19 | git clone https://github.com/mmatl/pyglet.git 20 | cd pyglet 21 | pip install . 22 | 23 | .. _osmesa: 24 | 25 | Getting Pyrender Working with OSMesa 26 | ------------------------------------ 27 | If you want to render scenes offscreen but don't want to have to 28 | install a display manager or deal with the pains of trying to get 29 | OpenGL to work over SSH, you have two options. 30 | 31 | The first (and preferred) option is using EGL, which enables you to perform 32 | GPU-accelerated rendering on headless servers. 33 | However, you'll need EGL 1.5 to get modern OpenGL contexts. 34 | This comes packaged with NVIDIA's current drivers, but if you are having issues 35 | getting EGL to work with your hardware, you can try using OSMesa, 36 | a software-based offscreen renderer that is included with any Mesa 37 | install. 38 | 39 | If you want to use OSMesa with pyrender, you'll have to perform two additional 40 | installation steps: 41 | 42 | - :ref:`installmesa` 43 | - :ref:`installpyopengl` 44 | 45 | Then, read the offscreen rendering tutorial. See :ref:`offscreen_guide`. 46 | 47 | .. _installmesa: 48 | 49 | Installing OSMesa 50 | ***************** 51 | 52 | As a first step, you'll need to rebuild and re-install Mesa with support 53 | for fast offscreen rendering and OpenGL 3+ contexts. 54 | I'd recommend installing from source, but you can also try my ``.deb`` 55 | for Ubuntu 16.04 and up. 56 | 57 | Installing from a Debian Package 58 | ******************************** 59 | 60 | If you're running Ubuntu 16.04 or newer, you should be able to install the 61 | required version of Mesa from my ``.deb`` file. 62 | 63 | .. code-block:: bash 64 | 65 | sudo apt update 66 | sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb 67 | sudo dpkg -i ./mesa_18.3.3-0.deb || true 68 | sudo apt install -f 69 | 70 | If this doesn't work, try building from source. 71 | 72 | Building From Source 73 | ******************** 74 | 75 | First, install build dependencies via `apt` or your system's package manager. 76 | 77 | .. code-block:: bash 78 | 79 | sudo apt-get install llvm-6.0 freeglut3 freeglut3-dev 80 | 81 | Then, download the current release of Mesa from here_. 82 | Unpack the source and go to the source folder: 83 | 84 | .. _here: https://archive.mesa3d.org/mesa-18.3.3.tar.gz 85 | 86 | .. code-block:: bash 87 | 88 | tar xfv mesa-18.3.3.tar.gz 89 | cd mesa-18.3.3 90 | 91 | Replace ``PREFIX`` with the path you want to install Mesa at. 92 | If you're not worried about overwriting your default Mesa install, 93 | a good place is at ``/usr/local``. 94 | 95 | Now, configure the installation by running the following command: 96 | 97 | .. code-block:: bash 98 | 99 | ./configure --prefix=PREFIX \ 100 | --enable-opengl --disable-gles1 --disable-gles2 \ 101 | --disable-va --disable-xvmc --disable-vdpau \ 102 | --enable-shared-glapi \ 103 | --disable-texture-float \ 104 | --enable-gallium-llvm --enable-llvm-shared-libs \ 105 | --with-gallium-drivers=swrast,swr \ 106 | --disable-dri --with-dri-drivers= \ 107 | --disable-egl --with-egl-platforms= --disable-gbm \ 108 | --disable-glx \ 109 | --disable-osmesa --enable-gallium-osmesa \ 110 | ac_cv_path_LLVM_CONFIG=llvm-config-6.0 111 | 112 | Finally, build and install Mesa. 113 | 114 | .. code-block:: bash 115 | 116 | make -j8 117 | make install 118 | 119 | Finally, if you didn't install Mesa in the system path, 120 | add the following lines to your ``~/.bashrc`` file after 121 | changing ``MESA_HOME`` to your mesa installation path (i.e. what you used as 122 | ``PREFIX`` during the configure command). 123 | 124 | .. code-block:: bash 125 | 126 | MESA_HOME=/path/to/your/mesa/installation 127 | export LIBRARY_PATH=$LIBRARY_PATH:$MESA_HOME/lib 128 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MESA_HOME/lib 129 | export C_INCLUDE_PATH=$C_INCLUDE_PATH:$MESA_HOME/include/ 130 | export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:$MESA_HOME/include/ 131 | 132 | .. _installpyopengl: 133 | 134 | Installing a Compatible Fork of PyOpenGL 135 | **************************************** 136 | 137 | Next, install and use my fork of ``PyOpenGL``. 138 | This fork enables getting modern OpenGL contexts with OSMesa. 139 | My patch has been included in ``PyOpenGL``, but it has not yet been released 140 | on PyPI. 141 | 142 | .. code-block:: bash 143 | 144 | git clone https://github.com/mmatl/pyopengl.git 145 | pip install ./pyopengl 146 | 147 | 148 | Building Documentation 149 | ---------------------- 150 | 151 | The online documentation for ``pyrender`` is automatically built by Read The Docs. 152 | Building ``pyrender``'s documentation locally requires a few extra dependencies -- 153 | specifically, `sphinx`_ and a few plugins. 154 | 155 | .. _sphinx: http://www.sphinx-doc.org/en/master/ 156 | 157 | To install the dependencies required, simply change directories into the `pyrender` source and run 158 | 159 | .. code-block:: bash 160 | 161 | $ pip install .[docs] 162 | 163 | Then, go to the ``docs`` directory and run ``make`` with the appropriate target. 164 | For example, 165 | 166 | .. code-block:: bash 167 | 168 | $ cd docs/ 169 | $ make html 170 | 171 | will generate a set of web pages. Any documentation files 172 | generated in this manner can be found in ``docs/build``. 173 | -------------------------------------------------------------------------------- /examples/duck.py: -------------------------------------------------------------------------------- 1 | from pyrender import Mesh, Scene, Viewer 2 | from io import BytesIO 3 | import numpy as np 4 | import trimesh 5 | import requests 6 | 7 | duck_source = "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb" 8 | 9 | duck = trimesh.load(BytesIO(requests.get(duck_source).content), file_type='glb') 10 | duckmesh = Mesh.from_trimesh(list(duck.geometry.values())[0]) 11 | scene = Scene(ambient_light=np.array([1.0, 1.0, 1.0, 1.0])) 12 | scene.add(duckmesh) 13 | Viewer(scene) 14 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | """Examples of using pyrender for viewing and offscreen rendering. 2 | """ 3 | import pyglet 4 | pyglet.options['shadow_window'] = False 5 | import os 6 | import numpy as np 7 | import trimesh 8 | 9 | from pyrender import PerspectiveCamera,\ 10 | DirectionalLight, SpotLight, PointLight,\ 11 | MetallicRoughnessMaterial,\ 12 | Primitive, Mesh, Node, Scene,\ 13 | Viewer, OffscreenRenderer, RenderFlags 14 | 15 | #============================================================================== 16 | # Mesh creation 17 | #============================================================================== 18 | 19 | #------------------------------------------------------------------------------ 20 | # Creating textured meshes from trimeshes 21 | #------------------------------------------------------------------------------ 22 | 23 | # Fuze trimesh 24 | fuze_trimesh = trimesh.load('./models/fuze.obj') 25 | fuze_mesh = Mesh.from_trimesh(fuze_trimesh) 26 | 27 | # Drill trimesh 28 | drill_trimesh = trimesh.load('./models/drill.obj') 29 | drill_mesh = Mesh.from_trimesh(drill_trimesh) 30 | drill_pose = np.eye(4) 31 | drill_pose[0,3] = 0.1 32 | drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2]) 33 | 34 | # Wood trimesh 35 | wood_trimesh = trimesh.load('./models/wood.obj') 36 | wood_mesh = Mesh.from_trimesh(wood_trimesh) 37 | 38 | # Water bottle trimesh 39 | bottle_gltf = trimesh.load('./models/WaterBottle.glb') 40 | bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]] 41 | bottle_mesh = Mesh.from_trimesh(bottle_trimesh) 42 | bottle_pose = np.array([ 43 | [1.0, 0.0, 0.0, 0.1], 44 | [0.0, 0.0, -1.0, -0.16], 45 | [0.0, 1.0, 0.0, 0.13], 46 | [0.0, 0.0, 0.0, 1.0], 47 | ]) 48 | 49 | #------------------------------------------------------------------------------ 50 | # Creating meshes with per-vertex colors 51 | #------------------------------------------------------------------------------ 52 | boxv_trimesh = trimesh.creation.box(extents=0.1*np.ones(3)) 53 | boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape)) 54 | boxv_trimesh.visual.vertex_colors = boxv_vertex_colors 55 | boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False) 56 | 57 | #------------------------------------------------------------------------------ 58 | # Creating meshes with per-face colors 59 | #------------------------------------------------------------------------------ 60 | boxf_trimesh = trimesh.creation.box(extents=0.1*np.ones(3)) 61 | boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape) 62 | boxf_trimesh.visual.face_colors = boxf_face_colors 63 | boxf_mesh = Mesh.from_trimesh(boxf_trimesh, smooth=False) 64 | 65 | #------------------------------------------------------------------------------ 66 | # Creating meshes from point clouds 67 | #------------------------------------------------------------------------------ 68 | points = trimesh.creation.icosphere(radius=0.05).vertices 69 | point_colors = np.random.uniform(size=points.shape) 70 | points_mesh = Mesh.from_points(points, colors=point_colors) 71 | 72 | #============================================================================== 73 | # Light creation 74 | #============================================================================== 75 | 76 | direc_l = DirectionalLight(color=np.ones(3), intensity=1.0) 77 | spot_l = SpotLight(color=np.ones(3), intensity=10.0, 78 | innerConeAngle=np.pi/16, outerConeAngle=np.pi/6) 79 | point_l = PointLight(color=np.ones(3), intensity=10.0) 80 | 81 | #============================================================================== 82 | # Camera creation 83 | #============================================================================== 84 | 85 | cam = PerspectiveCamera(yfov=(np.pi / 3.0)) 86 | cam_pose = np.array([ 87 | [0.0, -np.sqrt(2)/2, np.sqrt(2)/2, 0.5], 88 | [1.0, 0.0, 0.0, 0.0], 89 | [0.0, np.sqrt(2)/2, np.sqrt(2)/2, 0.4], 90 | [0.0, 0.0, 0.0, 1.0] 91 | ]) 92 | 93 | #============================================================================== 94 | # Scene creation 95 | #============================================================================== 96 | 97 | scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02, 1.0])) 98 | 99 | #============================================================================== 100 | # Adding objects to the scene 101 | #============================================================================== 102 | 103 | #------------------------------------------------------------------------------ 104 | # By manually creating nodes 105 | #------------------------------------------------------------------------------ 106 | fuze_node = Node(mesh=fuze_mesh, translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2])])) 107 | scene.add_node(fuze_node) 108 | boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05])) 109 | scene.add_node(boxv_node) 110 | boxf_node = Node(mesh=boxf_mesh, translation=np.array([-0.1, -0.10, 0.05])) 111 | scene.add_node(boxf_node) 112 | 113 | #------------------------------------------------------------------------------ 114 | # By using the add() utility function 115 | #------------------------------------------------------------------------------ 116 | drill_node = scene.add(drill_mesh, pose=drill_pose) 117 | bottle_node = scene.add(bottle_mesh, pose=bottle_pose) 118 | wood_node = scene.add(wood_mesh) 119 | direc_l_node = scene.add(direc_l, pose=cam_pose) 120 | spot_l_node = scene.add(spot_l, pose=cam_pose) 121 | 122 | #============================================================================== 123 | # Using the viewer with a default camera 124 | #============================================================================== 125 | 126 | v = Viewer(scene, shadows=True) 127 | 128 | #============================================================================== 129 | # Using the viewer with a pre-specified camera 130 | #============================================================================== 131 | cam_node = scene.add(cam, pose=cam_pose) 132 | v = Viewer(scene, central_node=drill_node) 133 | 134 | #============================================================================== 135 | # Rendering offscreen from that camera 136 | #============================================================================== 137 | 138 | r = OffscreenRenderer(viewport_width=640*2, viewport_height=480*2) 139 | color, depth = r.render(scene) 140 | 141 | import matplotlib.pyplot as plt 142 | plt.figure() 143 | plt.imshow(color) 144 | plt.show() 145 | 146 | #============================================================================== 147 | # Segmask rendering 148 | #============================================================================== 149 | 150 | nm = {node: 20*(i + 1) for i, node in enumerate(scene.mesh_nodes)} 151 | seg = r.render(scene, RenderFlags.SEG, nm)[0] 152 | plt.figure() 153 | plt.imshow(seg) 154 | plt.show() 155 | 156 | r.delete() 157 | 158 | -------------------------------------------------------------------------------- /examples/models/WaterBottle.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/examples/models/WaterBottle.glb -------------------------------------------------------------------------------- /examples/models/drill.obj.mtl: -------------------------------------------------------------------------------- 1 | newmtl material_0 2 | # shader_type beckmann 3 | map_Kd drill_uv.png 4 | 5 | -------------------------------------------------------------------------------- /examples/models/drill_uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/examples/models/drill_uv.png -------------------------------------------------------------------------------- /examples/models/fuze.obj.mtl: -------------------------------------------------------------------------------- 1 | # 2 | # Wavefront material file 3 | # Converted by Meshlab Group 4 | # 5 | 6 | newmtl material_0 7 | Ka 0.200000 0.200000 0.200000 8 | Kd 1.000000 1.000000 1.000000 9 | Ks 1.000000 1.000000 1.000000 10 | Tr 1.000000 11 | illum 2 12 | Ns 0.000000 13 | map_Kd fuze_uv.jpg 14 | 15 | -------------------------------------------------------------------------------- /examples/models/fuze_uv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/examples/models/fuze_uv.jpg -------------------------------------------------------------------------------- /examples/models/wood.obj: -------------------------------------------------------------------------------- 1 | mtllib ./wood.obj.mtl 2 | 3 | v -0.300000 -0.300000 0.000000 4 | v 0.300000 -0.300000 0.000000 5 | v -0.300000 0.300000 0.000000 6 | v 0.300000 0.300000 0.000000 7 | vn 0.000000 0.000000 1.000000 8 | vn 0.000000 0.000000 1.000000 9 | vn 0.000000 0.000000 1.000000 10 | vn 0.000000 0.000000 1.000000 11 | vt 0.000000 0.000000 12 | vt 1.000000 0.000000 13 | vt 0.000000 1.000000 14 | vt 1.000000 1.000000 15 | 16 | usemtl material_0 17 | f 1/1/1 2/2/2 4/4/4 18 | f 1/1/1 4/4/4 3/3/3 19 | -------------------------------------------------------------------------------- /examples/models/wood.obj.mtl: -------------------------------------------------------------------------------- 1 | newmtl material_0 2 | map_Kd wood_uv.png 3 | -------------------------------------------------------------------------------- /examples/models/wood_uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/examples/models/wood_uv.png -------------------------------------------------------------------------------- /pyrender/__init__.py: -------------------------------------------------------------------------------- 1 | from .camera import (Camera, PerspectiveCamera, OrthographicCamera, 2 | IntrinsicsCamera) 3 | from .light import Light, PointLight, DirectionalLight, SpotLight 4 | from .sampler import Sampler 5 | from .texture import Texture 6 | from .material import Material, MetallicRoughnessMaterial 7 | from .primitive import Primitive 8 | from .mesh import Mesh 9 | from .node import Node 10 | from .scene import Scene 11 | from .renderer import Renderer 12 | from .viewer import Viewer 13 | from .offscreen import OffscreenRenderer 14 | from .version import __version__ 15 | from .constants import RenderFlags, TextAlign, GLTF 16 | 17 | __all__ = [ 18 | 'Camera', 'PerspectiveCamera', 'OrthographicCamera', 'IntrinsicsCamera', 19 | 'Light', 'PointLight', 'DirectionalLight', 'SpotLight', 20 | 'Sampler', 'Texture', 'Material', 'MetallicRoughnessMaterial', 21 | 'Primitive', 'Mesh', 'Node', 'Scene', 'Renderer', 'Viewer', 22 | 'OffscreenRenderer', '__version__', 'RenderFlags', 'TextAlign', 23 | 'GLTF' 24 | ] 25 | -------------------------------------------------------------------------------- /pyrender/camera.py: -------------------------------------------------------------------------------- 1 | """Virtual cameras compliant with the glTF 2.0 specification as described at 2 | https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-camera 3 | 4 | Author: Matthew Matl 5 | """ 6 | import abc 7 | import numpy as np 8 | import six 9 | import sys 10 | 11 | from .constants import DEFAULT_Z_NEAR, DEFAULT_Z_FAR 12 | 13 | 14 | @six.add_metaclass(abc.ABCMeta) 15 | class Camera(object): 16 | """Abstract base class for all cameras. 17 | 18 | Note 19 | ---- 20 | Camera poses are specified in the OpenGL format, 21 | where the z axis points away from the view direction and the 22 | x and y axes point to the right and up in the image plane, respectively. 23 | 24 | Parameters 25 | ---------- 26 | znear : float 27 | The floating-point distance to the near clipping plane. 28 | zfar : float 29 | The floating-point distance to the far clipping plane. 30 | ``zfar`` must be greater than ``znear``. 31 | name : str, optional 32 | The user-defined name of this object. 33 | """ 34 | 35 | def __init__(self, 36 | znear=DEFAULT_Z_NEAR, 37 | zfar=DEFAULT_Z_FAR, 38 | name=None): 39 | self.name = name 40 | self.znear = znear 41 | self.zfar = zfar 42 | 43 | @property 44 | def name(self): 45 | """str : The user-defined name of this object. 46 | """ 47 | return self._name 48 | 49 | @name.setter 50 | def name(self, value): 51 | if value is not None: 52 | value = str(value) 53 | self._name = value 54 | 55 | @property 56 | def znear(self): 57 | """float : The distance to the near clipping plane. 58 | """ 59 | return self._znear 60 | 61 | @znear.setter 62 | def znear(self, value): 63 | value = float(value) 64 | if value < 0: 65 | raise ValueError('z-near must be >= 0.0') 66 | self._znear = value 67 | 68 | @property 69 | def zfar(self): 70 | """float : The distance to the far clipping plane. 71 | """ 72 | return self._zfar 73 | 74 | @zfar.setter 75 | def zfar(self, value): 76 | value = float(value) 77 | if value <= 0 or value <= self.znear: 78 | raise ValueError('zfar must be >0 and >znear') 79 | self._zfar = value 80 | 81 | @abc.abstractmethod 82 | def get_projection_matrix(self, width=None, height=None): 83 | """Return the OpenGL projection matrix for this camera. 84 | 85 | Parameters 86 | ---------- 87 | width : int 88 | Width of the current viewport, in pixels. 89 | height : int 90 | Height of the current viewport, in pixels. 91 | """ 92 | pass 93 | 94 | 95 | class PerspectiveCamera(Camera): 96 | 97 | """A perspective camera for perspective projection. 98 | 99 | Parameters 100 | ---------- 101 | yfov : float 102 | The floating-point vertical field of view in radians. 103 | znear : float 104 | The floating-point distance to the near clipping plane. 105 | If not specified, defaults to 0.05. 106 | zfar : float, optional 107 | The floating-point distance to the far clipping plane. 108 | ``zfar`` must be greater than ``znear``. 109 | If None, the camera uses an infinite projection matrix. 110 | aspectRatio : float, optional 111 | The floating-point aspect ratio of the field of view. 112 | If not specified, the camera uses the viewport's aspect ratio. 113 | name : str, optional 114 | The user-defined name of this object. 115 | """ 116 | 117 | def __init__(self, 118 | yfov, 119 | znear=DEFAULT_Z_NEAR, 120 | zfar=None, 121 | aspectRatio=None, 122 | name=None): 123 | super(PerspectiveCamera, self).__init__( 124 | znear=znear, 125 | zfar=zfar, 126 | name=name, 127 | ) 128 | 129 | self.yfov = yfov 130 | self.aspectRatio = aspectRatio 131 | 132 | @property 133 | def yfov(self): 134 | """float : The vertical field of view in radians. 135 | """ 136 | return self._yfov 137 | 138 | @yfov.setter 139 | def yfov(self, value): 140 | value = float(value) 141 | if value <= 0.0: 142 | raise ValueError('Field of view must be positive') 143 | self._yfov = value 144 | 145 | @property 146 | def zfar(self): 147 | """float : The distance to the far clipping plane. 148 | """ 149 | return self._zfar 150 | 151 | @zfar.setter 152 | def zfar(self, value): 153 | if value is not None: 154 | value = float(value) 155 | if value <= 0 or value <= self.znear: 156 | raise ValueError('zfar must be >0 and >znear') 157 | self._zfar = value 158 | 159 | @property 160 | def aspectRatio(self): 161 | """float : The ratio of the width to the height of the field of view. 162 | """ 163 | return self._aspectRatio 164 | 165 | @aspectRatio.setter 166 | def aspectRatio(self, value): 167 | if value is not None: 168 | value = float(value) 169 | if value <= 0.0: 170 | raise ValueError('Aspect ratio must be positive') 171 | self._aspectRatio = value 172 | 173 | def get_projection_matrix(self, width=None, height=None): 174 | """Return the OpenGL projection matrix for this camera. 175 | 176 | Parameters 177 | ---------- 178 | width : int 179 | Width of the current viewport, in pixels. 180 | height : int 181 | Height of the current viewport, in pixels. 182 | """ 183 | aspect_ratio = self.aspectRatio 184 | if aspect_ratio is None: 185 | if width is None or height is None: 186 | raise ValueError('Aspect ratio of camera must be defined') 187 | aspect_ratio = float(width) / float(height) 188 | 189 | a = aspect_ratio 190 | t = np.tan(self.yfov / 2.0) 191 | n = self.znear 192 | f = self.zfar 193 | 194 | P = np.zeros((4,4)) 195 | P[0][0] = 1.0 / (a * t) 196 | P[1][1] = 1.0 / t 197 | P[3][2] = -1.0 198 | 199 | if f is None: 200 | P[2][2] = -1.0 201 | P[2][3] = -2.0 * n 202 | else: 203 | P[2][2] = (f + n) / (n - f) 204 | P[2][3] = (2 * f * n) / (n - f) 205 | 206 | return P 207 | 208 | 209 | class OrthographicCamera(Camera): 210 | """An orthographic camera for orthographic projection. 211 | 212 | Parameters 213 | ---------- 214 | xmag : float 215 | The floating-point horizontal magnification of the view. 216 | ymag : float 217 | The floating-point vertical magnification of the view. 218 | znear : float 219 | The floating-point distance to the near clipping plane. 220 | If not specified, defaults to 0.05. 221 | zfar : float 222 | The floating-point distance to the far clipping plane. 223 | ``zfar`` must be greater than ``znear``. 224 | If not specified, defaults to 100.0. 225 | name : str, optional 226 | The user-defined name of this object. 227 | """ 228 | 229 | def __init__(self, 230 | xmag, 231 | ymag, 232 | znear=DEFAULT_Z_NEAR, 233 | zfar=DEFAULT_Z_FAR, 234 | name=None): 235 | super(OrthographicCamera, self).__init__( 236 | znear=znear, 237 | zfar=zfar, 238 | name=name, 239 | ) 240 | 241 | self.xmag = xmag 242 | self.ymag = ymag 243 | 244 | @property 245 | def xmag(self): 246 | """float : The horizontal magnification of the view. 247 | """ 248 | return self._xmag 249 | 250 | @xmag.setter 251 | def xmag(self, value): 252 | value = float(value) 253 | if value <= 0.0: 254 | raise ValueError('X magnification must be positive') 255 | self._xmag = value 256 | 257 | @property 258 | def ymag(self): 259 | """float : The vertical magnification of the view. 260 | """ 261 | return self._ymag 262 | 263 | @ymag.setter 264 | def ymag(self, value): 265 | value = float(value) 266 | if value <= 0.0: 267 | raise ValueError('Y magnification must be positive') 268 | self._ymag = value 269 | 270 | @property 271 | def znear(self): 272 | """float : The distance to the near clipping plane. 273 | """ 274 | return self._znear 275 | 276 | @znear.setter 277 | def znear(self, value): 278 | value = float(value) 279 | if value <= 0: 280 | raise ValueError('z-near must be > 0.0') 281 | self._znear = value 282 | 283 | def get_projection_matrix(self, width=None, height=None): 284 | """Return the OpenGL projection matrix for this camera. 285 | 286 | Parameters 287 | ---------- 288 | width : int 289 | Width of the current viewport, in pixels. 290 | Unused in this function. 291 | height : int 292 | Height of the current viewport, in pixels. 293 | Unused in this function. 294 | """ 295 | xmag = self.xmag 296 | ymag = self.ymag 297 | 298 | # If screen width/height defined, rescale xmag 299 | if width is not None and height is not None: 300 | xmag = width / height * ymag 301 | 302 | n = self.znear 303 | f = self.zfar 304 | P = np.zeros((4,4)) 305 | P[0][0] = 1.0 / xmag 306 | P[1][1] = 1.0 / ymag 307 | P[2][2] = 2.0 / (n - f) 308 | P[2][3] = (f + n) / (n - f) 309 | P[3][3] = 1.0 310 | return P 311 | 312 | 313 | class IntrinsicsCamera(Camera): 314 | """A perspective camera with custom intrinsics. 315 | 316 | Parameters 317 | ---------- 318 | fx : float 319 | X-axis focal length in pixels. 320 | fy : float 321 | Y-axis focal length in pixels. 322 | cx : float 323 | X-axis optical center in pixels. 324 | cy : float 325 | Y-axis optical center in pixels. 326 | znear : float 327 | The floating-point distance to the near clipping plane. 328 | If not specified, defaults to 0.05. 329 | zfar : float 330 | The floating-point distance to the far clipping plane. 331 | ``zfar`` must be greater than ``znear``. 332 | If not specified, defaults to 100.0. 333 | name : str, optional 334 | The user-defined name of this object. 335 | """ 336 | 337 | def __init__(self, 338 | fx, 339 | fy, 340 | cx, 341 | cy, 342 | znear=DEFAULT_Z_NEAR, 343 | zfar=DEFAULT_Z_FAR, 344 | name=None): 345 | super(IntrinsicsCamera, self).__init__( 346 | znear=znear, 347 | zfar=zfar, 348 | name=name, 349 | ) 350 | 351 | self.fx = fx 352 | self.fy = fy 353 | self.cx = cx 354 | self.cy = cy 355 | 356 | @property 357 | def fx(self): 358 | """float : X-axis focal length in meters. 359 | """ 360 | return self._fx 361 | 362 | @fx.setter 363 | def fx(self, value): 364 | self._fx = float(value) 365 | 366 | @property 367 | def fy(self): 368 | """float : Y-axis focal length in meters. 369 | """ 370 | return self._fy 371 | 372 | @fy.setter 373 | def fy(self, value): 374 | self._fy = float(value) 375 | 376 | @property 377 | def cx(self): 378 | """float : X-axis optical center in pixels. 379 | """ 380 | return self._cx 381 | 382 | @cx.setter 383 | def cx(self, value): 384 | self._cx = float(value) 385 | 386 | @property 387 | def cy(self): 388 | """float : Y-axis optical center in pixels. 389 | """ 390 | return self._cy 391 | 392 | @cy.setter 393 | def cy(self, value): 394 | self._cy = float(value) 395 | 396 | def get_projection_matrix(self, width, height): 397 | """Return the OpenGL projection matrix for this camera. 398 | 399 | Parameters 400 | ---------- 401 | width : int 402 | Width of the current viewport, in pixels. 403 | height : int 404 | Height of the current viewport, in pixels. 405 | """ 406 | width = float(width) 407 | height = float(height) 408 | 409 | cx, cy = self.cx, self.cy 410 | fx, fy = self.fx, self.fy 411 | if sys.platform == 'darwin': 412 | cx = self.cx * 2.0 413 | cy = self.cy * 2.0 414 | fx = self.fx * 2.0 415 | fy = self.fy * 2.0 416 | 417 | P = np.zeros((4,4)) 418 | P[0][0] = 2.0 * fx / width 419 | P[1][1] = 2.0 * fy / height 420 | P[0][2] = 1.0 - 2.0 * cx / width 421 | P[1][2] = 2.0 * cy / height - 1.0 422 | P[3][2] = -1.0 423 | 424 | n = self.znear 425 | f = self.zfar 426 | if f is None: 427 | P[2][2] = -1.0 428 | P[2][3] = -2.0 * n 429 | else: 430 | P[2][2] = (f + n) / (n - f) 431 | P[2][3] = (2 * f * n) / (n - f) 432 | 433 | return P 434 | 435 | 436 | __all__ = ['Camera', 'PerspectiveCamera', 'OrthographicCamera', 437 | 'IntrinsicsCamera'] 438 | -------------------------------------------------------------------------------- /pyrender/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_Z_NEAR = 0.05 # Near clipping plane, in meters 2 | DEFAULT_Z_FAR = 100.0 # Far clipping plane, in meters 3 | DEFAULT_SCENE_SCALE = 2.0 # Default scene scale 4 | MAX_N_LIGHTS = 4 # Maximum number of lights of each type allowed 5 | TARGET_OPEN_GL_MAJOR = 4 # Target OpenGL Major Version 6 | TARGET_OPEN_GL_MINOR = 1 # Target OpenGL Minor Version 7 | MIN_OPEN_GL_MAJOR = 3 # Minimum OpenGL Major Version 8 | MIN_OPEN_GL_MINOR = 3 # Minimum OpenGL Minor Version 9 | FLOAT_SZ = 4 # Byte size of GL float32 10 | UINT_SZ = 4 # Byte size of GL uint32 11 | SHADOW_TEX_SZ = 2048 # Width and Height of Shadow Textures 12 | TEXT_PADDING = 20 # Width of padding for rendering text (px) 13 | 14 | 15 | # Flags for render type 16 | class RenderFlags(object): 17 | """Flags for rendering in the scene. 18 | 19 | Combine them with the bitwise or. For example, 20 | 21 | >>> flags = OFFSCREEN | SHADOWS_DIRECTIONAL | VERTEX_NORMALS 22 | 23 | would result in an offscreen render with directional shadows and 24 | vertex normals enabled. 25 | """ 26 | NONE = 0 27 | """Normal PBR Render.""" 28 | DEPTH_ONLY = 1 29 | """Only render the depth buffer.""" 30 | OFFSCREEN = 2 31 | """Render offscreen and return the depth and (optionally) color buffers.""" 32 | FLIP_WIREFRAME = 4 33 | """Invert the status of wireframe rendering for each mesh.""" 34 | ALL_WIREFRAME = 8 35 | """Render all meshes as wireframes.""" 36 | ALL_SOLID = 16 37 | """Render all meshes as solids.""" 38 | SHADOWS_DIRECTIONAL = 32 39 | """Render shadows for directional lights.""" 40 | SHADOWS_POINT = 64 41 | """Render shadows for point lights.""" 42 | SHADOWS_SPOT = 128 43 | """Render shadows for spot lights.""" 44 | SHADOWS_ALL = 32 | 64 | 128 45 | """Render shadows for all lights.""" 46 | VERTEX_NORMALS = 256 47 | """Render vertex normals.""" 48 | FACE_NORMALS = 512 49 | """Render face normals.""" 50 | SKIP_CULL_FACES = 1024 51 | """Do not cull back faces.""" 52 | RGBA = 2048 53 | """Render the color buffer with the alpha channel enabled.""" 54 | FLAT = 4096 55 | """Render the color buffer flat, with no lighting computations.""" 56 | SEG = 8192 57 | 58 | 59 | class TextAlign: 60 | """Text alignment options for captions. 61 | 62 | Only use one at a time. 63 | """ 64 | CENTER = 0 65 | """Center the text by width and height.""" 66 | CENTER_LEFT = 1 67 | """Center the text by height and left-align it.""" 68 | CENTER_RIGHT = 2 69 | """Center the text by height and right-align it.""" 70 | BOTTOM_LEFT = 3 71 | """Put the text in the bottom-left corner.""" 72 | BOTTOM_RIGHT = 4 73 | """Put the text in the bottom-right corner.""" 74 | BOTTOM_CENTER = 5 75 | """Center the text by width and fix it to the bottom.""" 76 | TOP_LEFT = 6 77 | """Put the text in the top-left corner.""" 78 | TOP_RIGHT = 7 79 | """Put the text in the top-right corner.""" 80 | TOP_CENTER = 8 81 | """Center the text by width and fix it to the top.""" 82 | 83 | 84 | class GLTF(object): 85 | """Options for GL objects.""" 86 | NEAREST = 9728 87 | """Nearest neighbor interpolation.""" 88 | LINEAR = 9729 89 | """Linear interpolation.""" 90 | NEAREST_MIPMAP_NEAREST = 9984 91 | """Nearest mipmapping.""" 92 | LINEAR_MIPMAP_NEAREST = 9985 93 | """Linear mipmapping.""" 94 | NEAREST_MIPMAP_LINEAR = 9986 95 | """Nearest mipmapping.""" 96 | LINEAR_MIPMAP_LINEAR = 9987 97 | """Linear mipmapping.""" 98 | CLAMP_TO_EDGE = 33071 99 | """Clamp to the edge of the texture.""" 100 | MIRRORED_REPEAT = 33648 101 | """Mirror the texture.""" 102 | REPEAT = 10497 103 | """Repeat the texture.""" 104 | POINTS = 0 105 | """Render as points.""" 106 | LINES = 1 107 | """Render as lines.""" 108 | LINE_LOOP = 2 109 | """Render as a line loop.""" 110 | LINE_STRIP = 3 111 | """Render as a line strip.""" 112 | TRIANGLES = 4 113 | """Render as triangles.""" 114 | TRIANGLE_STRIP = 5 115 | """Render as a triangle strip.""" 116 | TRIANGLE_FAN = 6 117 | """Render as a triangle fan.""" 118 | 119 | 120 | class BufFlags(object): 121 | POSITION = 0 122 | NORMAL = 1 123 | TANGENT = 2 124 | TEXCOORD_0 = 4 125 | TEXCOORD_1 = 8 126 | COLOR_0 = 16 127 | JOINTS_0 = 32 128 | WEIGHTS_0 = 64 129 | 130 | 131 | class TexFlags(object): 132 | NONE = 0 133 | NORMAL = 1 134 | OCCLUSION = 2 135 | EMISSIVE = 4 136 | BASE_COLOR = 8 137 | METALLIC_ROUGHNESS = 16 138 | DIFFUSE = 32 139 | SPECULAR_GLOSSINESS = 64 140 | 141 | 142 | class ProgramFlags: 143 | NONE = 0 144 | USE_MATERIAL = 1 145 | VERTEX_NORMALS = 2 146 | FACE_NORMALS = 4 147 | 148 | 149 | __all__ = ['RenderFlags', 'TextAlign', 'GLTF'] 150 | -------------------------------------------------------------------------------- /pyrender/font.py: -------------------------------------------------------------------------------- 1 | """Font texture loader and processor. 2 | 3 | Author: Matthew Matl 4 | """ 5 | import freetype 6 | import numpy as np 7 | import os 8 | 9 | import OpenGL 10 | from OpenGL.GL import * 11 | 12 | from .constants import TextAlign, FLOAT_SZ 13 | from .texture import Texture 14 | from .sampler import Sampler 15 | 16 | 17 | class FontCache(object): 18 | """A cache for fonts. 19 | """ 20 | 21 | def __init__(self, font_dir=None): 22 | self._font_cache = {} 23 | self.font_dir = font_dir 24 | if self.font_dir is None: 25 | base_dir, _ = os.path.split(os.path.realpath(__file__)) 26 | self.font_dir = os.path.join(base_dir, 'fonts') 27 | 28 | def get_font(self, font_name, font_pt): 29 | # If it's a file, load it directly, else, try to load from font dir. 30 | if os.path.isfile(font_name): 31 | font_filename = font_name 32 | _, font_name = os.path.split(font_name) 33 | font_name, _ = os.path.split(font_name) 34 | else: 35 | font_filename = os.path.join(self.font_dir, font_name) + '.ttf' 36 | 37 | cid = OpenGL.contextdata.getContext() 38 | key = (cid, font_name, int(font_pt)) 39 | 40 | if key not in self._font_cache: 41 | self._font_cache[key] = Font(font_filename, font_pt) 42 | return self._font_cache[key] 43 | 44 | def clear(self): 45 | for key in self._font_cache: 46 | self._font_cache[key].delete() 47 | self._font_cache = {} 48 | 49 | 50 | class Character(object): 51 | """A single character, with its texture and attributes. 52 | """ 53 | 54 | def __init__(self, texture, size, bearing, advance): 55 | self.texture = texture 56 | self.size = size 57 | self.bearing = bearing 58 | self.advance = advance 59 | 60 | 61 | class Font(object): 62 | """A font object. 63 | 64 | Parameters 65 | ---------- 66 | font_file : str 67 | The file to load the font from. 68 | font_pt : int 69 | The height of the font in pixels. 70 | """ 71 | 72 | def __init__(self, font_file, font_pt=40): 73 | self.font_file = font_file 74 | self.font_pt = int(font_pt) 75 | self._face = freetype.Face(font_file) 76 | self._face.set_pixel_sizes(0, font_pt) 77 | self._character_map = {} 78 | 79 | for i in range(0, 128): 80 | 81 | # Generate texture 82 | face = self._face 83 | face.load_char(chr(i)) 84 | buf = face.glyph.bitmap.buffer 85 | src = (np.array(buf) / 255.0).astype(np.float32) 86 | src = src.reshape((face.glyph.bitmap.rows, 87 | face.glyph.bitmap.width)) 88 | tex = Texture( 89 | sampler=Sampler( 90 | magFilter=GL_LINEAR, 91 | minFilter=GL_LINEAR, 92 | wrapS=GL_CLAMP_TO_EDGE, 93 | wrapT=GL_CLAMP_TO_EDGE 94 | ), 95 | source=src, 96 | source_channels='R', 97 | ) 98 | character = Character( 99 | texture=tex, 100 | size=np.array([face.glyph.bitmap.width, 101 | face.glyph.bitmap.rows]), 102 | bearing=np.array([face.glyph.bitmap_left, 103 | face.glyph.bitmap_top]), 104 | advance=face.glyph.advance.x 105 | ) 106 | self._character_map[chr(i)] = character 107 | 108 | self._vbo = None 109 | self._vao = None 110 | 111 | @property 112 | def font_file(self): 113 | """str : The file the font was loaded from. 114 | """ 115 | return self._font_file 116 | 117 | @font_file.setter 118 | def font_file(self, value): 119 | self._font_file = value 120 | 121 | @property 122 | def font_pt(self): 123 | """int : The height of the font in pixels. 124 | """ 125 | return self._font_pt 126 | 127 | @font_pt.setter 128 | def font_pt(self, value): 129 | self._font_pt = int(value) 130 | 131 | def _add_to_context(self): 132 | 133 | self._vao = glGenVertexArrays(1) 134 | glBindVertexArray(self._vao) 135 | self._vbo = glGenBuffers(1) 136 | glBindBuffer(GL_ARRAY_BUFFER, self._vbo) 137 | glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW) 138 | glEnableVertexAttribArray(0) 139 | glVertexAttribPointer( 140 | 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0) 141 | ) 142 | glBindVertexArray(0) 143 | 144 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 145 | for c in self._character_map: 146 | ch = self._character_map[c] 147 | if not ch.texture._in_context(): 148 | ch.texture._add_to_context() 149 | 150 | def _remove_from_context(self): 151 | for c in self._character_map: 152 | ch = self._character_map[c] 153 | ch.texture.delete() 154 | if self._vao is not None: 155 | glDeleteVertexArrays(1, [self._vao]) 156 | glDeleteBuffers(1, [self._vbo]) 157 | self._vao = None 158 | self._vbo = None 159 | 160 | def _in_context(self): 161 | return self._vao is not None 162 | 163 | def _bind(self): 164 | glBindVertexArray(self._vao) 165 | 166 | def _unbind(self): 167 | glBindVertexArray(0) 168 | 169 | def delete(self): 170 | self._unbind() 171 | self._remove_from_context() 172 | 173 | def render_string(self, text, x, y, scale=1.0, 174 | align=TextAlign.BOTTOM_LEFT): 175 | """Render a string to the current view buffer. 176 | 177 | Note 178 | ---- 179 | Assumes correct shader program already bound w/ uniforms set. 180 | 181 | Parameters 182 | ---------- 183 | text : str 184 | The text to render. 185 | x : int 186 | Horizontal pixel location of text. 187 | y : int 188 | Vertical pixel location of text. 189 | scale : int 190 | Scaling factor for text. 191 | align : int 192 | One of the TextAlign options which specifies where the ``x`` 193 | and ``y`` parameters lie on the text. For example, 194 | :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate 195 | the position of the bottom-left corner of the textbox. 196 | """ 197 | glActiveTexture(GL_TEXTURE0) 198 | glEnable(GL_BLEND) 199 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 200 | glDisable(GL_DEPTH_TEST) 201 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 202 | self._bind() 203 | 204 | # Determine width and height of text relative to x, y 205 | width = 0.0 206 | height = 0.0 207 | for c in text: 208 | ch = self._character_map[c] 209 | height = max(height, ch.bearing[1] * scale) 210 | width += (ch.advance >> 6) * scale 211 | 212 | # Determine offsets based on alignments 213 | xoff = 0 214 | yoff = 0 215 | if align == TextAlign.BOTTOM_RIGHT: 216 | xoff = -width 217 | elif align == TextAlign.BOTTOM_CENTER: 218 | xoff = -width / 2.0 219 | elif align == TextAlign.TOP_LEFT: 220 | yoff = -height 221 | elif align == TextAlign.TOP_RIGHT: 222 | yoff = -height 223 | xoff = -width 224 | elif align == TextAlign.TOP_CENTER: 225 | yoff = -height 226 | xoff = -width / 2.0 227 | elif align == TextAlign.CENTER: 228 | xoff = -width / 2.0 229 | yoff = -height / 2.0 230 | elif align == TextAlign.CENTER_LEFT: 231 | yoff = -height / 2.0 232 | elif align == TextAlign.CENTER_RIGHT: 233 | xoff = -width 234 | yoff = -height / 2.0 235 | 236 | x += xoff 237 | y += yoff 238 | 239 | ch = None 240 | for c in text: 241 | ch = self._character_map[c] 242 | xpos = x + ch.bearing[0] * scale 243 | ypos = y - (ch.size[1] - ch.bearing[1]) * scale 244 | w = ch.size[0] * scale 245 | h = ch.size[1] * scale 246 | 247 | vertices = np.array([ 248 | [xpos, ypos, 0.0, 0.0], 249 | [xpos + w, ypos, 1.0, 0.0], 250 | [xpos + w, ypos + h, 1.0, 1.0], 251 | [xpos + w, ypos + h, 1.0, 1.0], 252 | [xpos, ypos + h, 0.0, 1.0], 253 | [xpos, ypos, 0.0, 0.0], 254 | ], dtype=np.float32) 255 | 256 | ch.texture._bind() 257 | 258 | glBindBuffer(GL_ARRAY_BUFFER, self._vbo) 259 | glBufferData( 260 | GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW 261 | ) 262 | # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken 263 | # glBufferSubData( 264 | # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ, 265 | # np.ascontiguousarray(vertices.flatten) 266 | # ) 267 | glDrawArrays(GL_TRIANGLES, 0, 6) 268 | x += (ch.advance >> 6) * scale 269 | 270 | self._unbind() 271 | if ch: 272 | ch.texture._unbind() 273 | -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /pyrender/fonts/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/pyrender/fonts/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /pyrender/mesh.py: -------------------------------------------------------------------------------- 1 | """Meshes, conforming to the glTF 2.0 standards as specified in 2 | https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh 3 | 4 | Author: Matthew Matl 5 | """ 6 | import copy 7 | 8 | import numpy as np 9 | import trimesh 10 | 11 | from .primitive import Primitive 12 | from .constants import GLTF 13 | from .material import MetallicRoughnessMaterial 14 | 15 | 16 | class Mesh(object): 17 | """A set of primitives to be rendered. 18 | 19 | Parameters 20 | ---------- 21 | name : str 22 | The user-defined name of this object. 23 | primitives : list of :class:`Primitive` 24 | The primitives associated with this mesh. 25 | weights : (k,) float 26 | Array of weights to be applied to the Morph Targets. 27 | is_visible : bool 28 | If False, the mesh will not be rendered. 29 | """ 30 | 31 | def __init__(self, primitives, name=None, weights=None, is_visible=True): 32 | self.primitives = primitives 33 | self.name = name 34 | self.weights = weights 35 | self.is_visible = is_visible 36 | 37 | self._bounds = None 38 | 39 | @property 40 | def name(self): 41 | """str : The user-defined name of this object. 42 | """ 43 | return self._name 44 | 45 | @name.setter 46 | def name(self, value): 47 | if value is not None: 48 | value = str(value) 49 | self._name = value 50 | 51 | @property 52 | def primitives(self): 53 | """list of :class:`Primitive` : The primitives associated 54 | with this mesh. 55 | """ 56 | return self._primitives 57 | 58 | @primitives.setter 59 | def primitives(self, value): 60 | self._primitives = value 61 | 62 | @property 63 | def weights(self): 64 | """(k,) float : Weights to be applied to morph targets. 65 | """ 66 | return self._weights 67 | 68 | @weights.setter 69 | def weights(self, value): 70 | self._weights = value 71 | 72 | @property 73 | def is_visible(self): 74 | """bool : Whether the mesh is visible. 75 | """ 76 | return self._is_visible 77 | 78 | @is_visible.setter 79 | def is_visible(self, value): 80 | self._is_visible = value 81 | 82 | @property 83 | def bounds(self): 84 | """(2,3) float : The axis-aligned bounds of the mesh. 85 | """ 86 | if self._bounds is None: 87 | bounds = np.array([[np.infty, np.infty, np.infty], 88 | [-np.infty, -np.infty, -np.infty]]) 89 | for p in self.primitives: 90 | bounds[0] = np.minimum(bounds[0], p.bounds[0]) 91 | bounds[1] = np.maximum(bounds[1], p.bounds[1]) 92 | self._bounds = bounds 93 | return self._bounds 94 | 95 | @property 96 | def centroid(self): 97 | """(3,) float : The centroid of the mesh's axis-aligned bounding box 98 | (AABB). 99 | """ 100 | return np.mean(self.bounds, axis=0) 101 | 102 | @property 103 | def extents(self): 104 | """(3,) float : The lengths of the axes of the mesh's AABB. 105 | """ 106 | return np.diff(self.bounds, axis=0).reshape(-1) 107 | 108 | @property 109 | def scale(self): 110 | """(3,) float : The length of the diagonal of the mesh's AABB. 111 | """ 112 | return np.linalg.norm(self.extents) 113 | 114 | @property 115 | def is_transparent(self): 116 | """bool : If True, the mesh is partially-transparent. 117 | """ 118 | for p in self.primitives: 119 | if p.is_transparent: 120 | return True 121 | return False 122 | 123 | @staticmethod 124 | def from_points(points, colors=None, normals=None, 125 | is_visible=True, poses=None): 126 | """Create a Mesh from a set of points. 127 | 128 | Parameters 129 | ---------- 130 | points : (n,3) float 131 | The point positions. 132 | colors : (n,3) or (n,4) float, optional 133 | RGB or RGBA colors for each point. 134 | normals : (n,3) float, optionals 135 | The normal vectors for each point. 136 | is_visible : bool 137 | If False, the points will not be rendered. 138 | poses : (x,4,4) 139 | Array of 4x4 transformation matrices for instancing this object. 140 | 141 | Returns 142 | ------- 143 | mesh : :class:`Mesh` 144 | The created mesh. 145 | """ 146 | primitive = Primitive( 147 | positions=points, 148 | normals=normals, 149 | color_0=colors, 150 | mode=GLTF.POINTS, 151 | poses=poses 152 | ) 153 | mesh = Mesh(primitives=[primitive], is_visible=is_visible) 154 | return mesh 155 | 156 | @staticmethod 157 | def from_trimesh(mesh, material=None, is_visible=True, 158 | poses=None, wireframe=False, smooth=True): 159 | """Create a Mesh from a :class:`~trimesh.base.Trimesh`. 160 | 161 | Parameters 162 | ---------- 163 | mesh : :class:`~trimesh.base.Trimesh` or list of them 164 | A triangular mesh or a list of meshes. 165 | material : :class:`Material` 166 | The material of the object. Overrides any mesh material. 167 | If not specified and the mesh has no material, a default material 168 | will be used. 169 | is_visible : bool 170 | If False, the mesh will not be rendered. 171 | poses : (n,4,4) float 172 | Array of 4x4 transformation matrices for instancing this object. 173 | wireframe : bool 174 | If `True`, the mesh will be rendered as a wireframe object 175 | smooth : bool 176 | If `True`, the mesh will be rendered with interpolated vertex 177 | normals. Otherwise, the mesh edges will stay sharp. 178 | 179 | Returns 180 | ------- 181 | mesh : :class:`Mesh` 182 | The created mesh. 183 | """ 184 | 185 | if isinstance(mesh, (list, tuple, set, np.ndarray)): 186 | meshes = list(mesh) 187 | elif isinstance(mesh, trimesh.Trimesh): 188 | meshes = [mesh] 189 | else: 190 | raise TypeError('Expected a Trimesh or a list, got a {}' 191 | .format(type(mesh))) 192 | 193 | primitives = [] 194 | for m in meshes: 195 | positions = None 196 | normals = None 197 | indices = None 198 | 199 | # Compute positions, normals, and indices 200 | if smooth: 201 | positions = m.vertices.copy() 202 | normals = m.vertex_normals.copy() 203 | indices = m.faces.copy() 204 | else: 205 | positions = m.vertices[m.faces].reshape((3 * len(m.faces), 3)) 206 | normals = np.repeat(m.face_normals, 3, axis=0) 207 | 208 | # Compute colors, texture coords, and material properties 209 | color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props(m, smooth=smooth, material=material) 210 | 211 | # Override if material is given. 212 | if material is not None: 213 | #primitive_material = copy.copy(material) 214 | primitive_material = copy.deepcopy(material) # TODO 215 | 216 | if primitive_material is None: 217 | # Replace material with default if needed 218 | primitive_material = MetallicRoughnessMaterial( 219 | alphaMode='BLEND', 220 | baseColorFactor=[0.3, 0.3, 0.3, 1.0], 221 | metallicFactor=0.2, 222 | roughnessFactor=0.8 223 | ) 224 | 225 | primitive_material.wireframe = wireframe 226 | 227 | # Create the primitive 228 | primitives.append(Primitive( 229 | positions=positions, 230 | normals=normals, 231 | texcoord_0=texcoord_0, 232 | color_0=color_0, 233 | indices=indices, 234 | material=primitive_material, 235 | mode=GLTF.TRIANGLES, 236 | poses=poses 237 | )) 238 | 239 | return Mesh(primitives=primitives, is_visible=is_visible) 240 | 241 | @staticmethod 242 | def _get_trimesh_props(mesh, smooth=False, material=None): 243 | """Gets the vertex colors, texture coordinates, and material properties 244 | from a :class:`~trimesh.base.Trimesh`. 245 | """ 246 | colors = None 247 | texcoords = None 248 | 249 | # If the trimesh visual is undefined, return none for both 250 | if not mesh.visual.defined: 251 | return colors, texcoords, material 252 | 253 | # Process vertex colors 254 | if material is None: 255 | if mesh.visual.kind == 'vertex': 256 | vc = mesh.visual.vertex_colors.copy() 257 | if smooth: 258 | colors = vc 259 | else: 260 | colors = vc[mesh.faces].reshape( 261 | (3 * len(mesh.faces), vc.shape[1]) 262 | ) 263 | material = MetallicRoughnessMaterial( 264 | alphaMode='BLEND', 265 | baseColorFactor=[1.0, 1.0, 1.0, 1.0], 266 | metallicFactor=0.2, 267 | roughnessFactor=0.8 268 | ) 269 | # Process face colors 270 | elif mesh.visual.kind == 'face': 271 | if smooth: 272 | raise ValueError('Cannot use face colors with a smooth mesh') 273 | else: 274 | colors = np.repeat(mesh.visual.face_colors, 3, axis=0) 275 | 276 | material = MetallicRoughnessMaterial( 277 | alphaMode='BLEND', 278 | baseColorFactor=[1.0, 1.0, 1.0, 1.0], 279 | metallicFactor=0.2, 280 | roughnessFactor=0.8 281 | ) 282 | 283 | # Process texture colors 284 | if mesh.visual.kind == 'texture': 285 | # Configure UV coordinates 286 | if mesh.visual.uv is not None and len(mesh.visual.uv) != 0: 287 | uv = mesh.visual.uv.copy() 288 | if smooth: 289 | texcoords = uv 290 | else: 291 | texcoords = uv[mesh.faces].reshape( 292 | (3 * len(mesh.faces), uv.shape[1]) 293 | ) 294 | 295 | if material is None: 296 | # Configure mesh material 297 | mat = mesh.visual.material 298 | 299 | if isinstance(mat, trimesh.visual.texture.PBRMaterial): 300 | material = MetallicRoughnessMaterial( 301 | normalTexture=mat.normalTexture, 302 | occlusionTexture=mat.occlusionTexture, 303 | emissiveTexture=mat.emissiveTexture, 304 | emissiveFactor=mat.emissiveFactor, 305 | alphaMode='BLEND', 306 | baseColorFactor=mat.baseColorFactor, 307 | baseColorTexture=mat.baseColorTexture, 308 | metallicFactor=mat.metallicFactor, 309 | roughnessFactor=mat.roughnessFactor, 310 | metallicRoughnessTexture=mat.metallicRoughnessTexture, 311 | doubleSided=mat.doubleSided, 312 | alphaCutoff=mat.alphaCutoff 313 | ) 314 | elif isinstance(mat, trimesh.visual.texture.SimpleMaterial): 315 | glossiness = mat.kwargs.get('Ns', 1.0) 316 | if isinstance(glossiness, list): 317 | glossiness = float(glossiness[0]) 318 | roughness = (2 / (glossiness + 2)) ** (1.0 / 4.0) 319 | material = MetallicRoughnessMaterial( 320 | alphaMode='BLEND', 321 | roughnessFactor=roughness, 322 | baseColorFactor=mat.diffuse, 323 | baseColorTexture=mat.image, 324 | ) 325 | elif isinstance(mat, MetallicRoughnessMaterial): 326 | material = mat 327 | 328 | return colors, texcoords, material 329 | -------------------------------------------------------------------------------- /pyrender/node.py: -------------------------------------------------------------------------------- 1 | """Nodes, conforming to the glTF 2.0 standards as specified in 2 | https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node 3 | 4 | Author: Matthew Matl 5 | """ 6 | import numpy as np 7 | 8 | import trimesh.transformations as transformations 9 | 10 | from .camera import Camera 11 | from .mesh import Mesh 12 | from .light import Light 13 | 14 | 15 | class Node(object): 16 | """A node in the node hierarchy. 17 | 18 | Parameters 19 | ---------- 20 | name : str, optional 21 | The user-defined name of this object. 22 | camera : :class:`Camera`, optional 23 | The camera in this node. 24 | children : list of :class:`Node` 25 | The children of this node. 26 | skin : int, optional 27 | The index of the skin referenced by this node. 28 | matrix : (4,4) float, optional 29 | A floating-point 4x4 transformation matrix. 30 | mesh : :class:`Mesh`, optional 31 | The mesh in this node. 32 | rotation : (4,) float, optional 33 | The node's unit quaternion in the order (x, y, z, w), where 34 | w is the scalar. 35 | scale : (3,) float, optional 36 | The node's non-uniform scale, given as the scaling factors along the x, 37 | y, and z axes. 38 | translation : (3,) float, optional 39 | The node's translation along the x, y, and z axes. 40 | weights : (n,) float 41 | The weights of the instantiated Morph Target. Number of elements must 42 | match number of Morph Targets of used mesh. 43 | light : :class:`Light`, optional 44 | The light in this node. 45 | """ 46 | 47 | def __init__(self, 48 | name=None, 49 | camera=None, 50 | children=None, 51 | skin=None, 52 | matrix=None, 53 | mesh=None, 54 | rotation=None, 55 | scale=None, 56 | translation=None, 57 | weights=None, 58 | light=None): 59 | # Set defaults 60 | if children is None: 61 | children = [] 62 | 63 | self._matrix = None 64 | self._scale = None 65 | self._rotation = None 66 | self._translation = None 67 | if matrix is None: 68 | if rotation is None: 69 | rotation = np.array([0.0, 0.0, 0.0, 1.0]) 70 | if translation is None: 71 | translation = np.zeros(3) 72 | if scale is None: 73 | scale = np.ones(3) 74 | self.rotation = rotation 75 | self.translation = translation 76 | self.scale = scale 77 | else: 78 | self.matrix = matrix 79 | 80 | self.name = name 81 | self.camera = camera 82 | self.children = children 83 | self.skin = skin 84 | self.mesh = mesh 85 | self.weights = weights 86 | self.light = light 87 | 88 | @property 89 | def name(self): 90 | """str : The user-defined name of this object. 91 | """ 92 | return self._name 93 | 94 | @name.setter 95 | def name(self, value): 96 | if value is not None: 97 | value = str(value) 98 | self._name = value 99 | 100 | @property 101 | def camera(self): 102 | """:class:`Camera` : The camera in this node. 103 | """ 104 | return self._camera 105 | 106 | @camera.setter 107 | def camera(self, value): 108 | if value is not None and not isinstance(value, Camera): 109 | raise TypeError('Value must be a camera') 110 | self._camera = value 111 | 112 | @property 113 | def children(self): 114 | """list of :class:`Node` : The children of this node. 115 | """ 116 | return self._children 117 | 118 | @children.setter 119 | def children(self, value): 120 | self._children = value 121 | 122 | @property 123 | def skin(self): 124 | """int : The skin index for this node. 125 | """ 126 | return self._skin 127 | 128 | @skin.setter 129 | def skin(self, value): 130 | self._skin = value 131 | 132 | @property 133 | def mesh(self): 134 | """:class:`Mesh` : The mesh in this node. 135 | """ 136 | return self._mesh 137 | 138 | @mesh.setter 139 | def mesh(self, value): 140 | if value is not None and not isinstance(value, Mesh): 141 | raise TypeError('Value must be a mesh') 142 | self._mesh = value 143 | 144 | @property 145 | def light(self): 146 | """:class:`Light` : The light in this node. 147 | """ 148 | return self._light 149 | 150 | @light.setter 151 | def light(self, value): 152 | if value is not None and not isinstance(value, Light): 153 | raise TypeError('Value must be a light') 154 | self._light = value 155 | 156 | @property 157 | def rotation(self): 158 | """(4,) float : The xyzw quaternion for this node. 159 | """ 160 | return self._rotation 161 | 162 | @rotation.setter 163 | def rotation(self, value): 164 | value = np.asanyarray(value) 165 | if value.shape != (4,): 166 | raise ValueError('Quaternion must be a (4,) vector') 167 | if np.abs(np.linalg.norm(value) - 1.0) > 1e-3: 168 | raise ValueError('Quaternion must have norm == 1.0') 169 | self._rotation = value 170 | self._matrix = None 171 | 172 | @property 173 | def translation(self): 174 | """(3,) float : The translation for this node. 175 | """ 176 | return self._translation 177 | 178 | @translation.setter 179 | def translation(self, value): 180 | value = np.asanyarray(value) 181 | if value.shape != (3,): 182 | raise ValueError('Translation must be a (3,) vector') 183 | self._translation = value 184 | self._matrix = None 185 | 186 | @property 187 | def scale(self): 188 | """(3,) float : The scale for this node. 189 | """ 190 | return self._scale 191 | 192 | @scale.setter 193 | def scale(self, value): 194 | value = np.asanyarray(value) 195 | if value.shape != (3,): 196 | raise ValueError('Scale must be a (3,) vector') 197 | self._scale = value 198 | self._matrix = None 199 | 200 | @property 201 | def matrix(self): 202 | """(4,4) float : The homogenous transform matrix for this node. 203 | 204 | Note that this matrix's elements are not settable, 205 | it's just a copy of the internal matrix. You can set the whole 206 | matrix, but not an individual element. 207 | """ 208 | if self._matrix is None: 209 | self._matrix = self._m_from_tqs( 210 | self.translation, self.rotation, self.scale 211 | ) 212 | return self._matrix.copy() 213 | 214 | @matrix.setter 215 | def matrix(self, value): 216 | value = np.asanyarray(value) 217 | if value.shape != (4,4): 218 | raise ValueError('Matrix must be a 4x4 numpy ndarray') 219 | if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])): 220 | raise ValueError('Bottom row of matrix must be [0,0,0,1]') 221 | self.rotation = Node._q_from_m(value) 222 | self.scale = Node._s_from_m(value) 223 | self.translation = Node._t_from_m(value) 224 | self._matrix = value 225 | 226 | @staticmethod 227 | def _t_from_m(m): 228 | return m[:3,3] 229 | 230 | @staticmethod 231 | def _r_from_m(m): 232 | U = m[:3,:3] 233 | norms = np.linalg.norm(U.T, axis=1) 234 | return U / norms 235 | 236 | @staticmethod 237 | def _q_from_m(m): 238 | M = np.eye(4) 239 | M[:3,:3] = Node._r_from_m(m) 240 | q_wxyz = transformations.quaternion_from_matrix(M) 241 | return np.roll(q_wxyz, -1) 242 | 243 | @staticmethod 244 | def _s_from_m(m): 245 | return np.linalg.norm(m[:3,:3].T, axis=1) 246 | 247 | @staticmethod 248 | def _r_from_q(q): 249 | q_wxyz = np.roll(q, 1) 250 | return transformations.quaternion_matrix(q_wxyz)[:3,:3] 251 | 252 | @staticmethod 253 | def _m_from_tqs(t, q, s): 254 | S = np.eye(4) 255 | S[:3,:3] = np.diag(s) 256 | 257 | R = np.eye(4) 258 | R[:3,:3] = Node._r_from_q(q) 259 | 260 | T = np.eye(4) 261 | T[:3,3] = t 262 | 263 | return T.dot(R.dot(S)) 264 | -------------------------------------------------------------------------------- /pyrender/offscreen.py: -------------------------------------------------------------------------------- 1 | """Wrapper for offscreen rendering. 2 | 3 | Author: Matthew Matl 4 | """ 5 | import os 6 | 7 | from .renderer import Renderer 8 | from .constants import RenderFlags 9 | 10 | 11 | class OffscreenRenderer(object): 12 | """A wrapper for offscreen rendering. 13 | 14 | Parameters 15 | ---------- 16 | viewport_width : int 17 | The width of the main viewport, in pixels. 18 | viewport_height : int 19 | The height of the main viewport, in pixels. 20 | point_size : float 21 | The size of screen-space points in pixels. 22 | """ 23 | 24 | def __init__(self, viewport_width, viewport_height, point_size=1.0): 25 | self.viewport_width = viewport_width 26 | self.viewport_height = viewport_height 27 | self.point_size = point_size 28 | 29 | self._platform = None 30 | self._renderer = None 31 | self._create() 32 | 33 | @property 34 | def viewport_width(self): 35 | """int : The width of the main viewport, in pixels. 36 | """ 37 | return self._viewport_width 38 | 39 | @viewport_width.setter 40 | def viewport_width(self, value): 41 | self._viewport_width = int(value) 42 | 43 | @property 44 | def viewport_height(self): 45 | """int : The height of the main viewport, in pixels. 46 | """ 47 | return self._viewport_height 48 | 49 | @viewport_height.setter 50 | def viewport_height(self, value): 51 | self._viewport_height = int(value) 52 | 53 | @property 54 | def point_size(self): 55 | """float : The pixel size of points in point clouds. 56 | """ 57 | return self._point_size 58 | 59 | @point_size.setter 60 | def point_size(self, value): 61 | self._point_size = float(value) 62 | 63 | def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None): 64 | """Render a scene with the given set of flags. 65 | 66 | Parameters 67 | ---------- 68 | scene : :class:`Scene` 69 | A scene to render. 70 | flags : int 71 | A bitwise or of one or more flags from :class:`.RenderFlags`. 72 | seg_node_map : dict 73 | A map from :class:`.Node` objects to (3,) colors for each. 74 | If specified along with flags set to :attr:`.RenderFlags.SEG`, 75 | the color image will be a segmentation image. 76 | 77 | Returns 78 | ------- 79 | color_im : (h, w, 3) uint8 or (h, w, 4) uint8 80 | The color buffer in RGB format, or in RGBA format if 81 | :attr:`.RenderFlags.RGBA` is set. 82 | Not returned if flags includes :attr:`.RenderFlags.DEPTH_ONLY`. 83 | depth_im : (h, w) float32 84 | The depth buffer in linear units. 85 | """ 86 | self._platform.make_current() 87 | # If platform does not support dynamically-resizing framebuffers, 88 | # destroy it and restart it 89 | if (self._platform.viewport_height != self.viewport_height or 90 | self._platform.viewport_width != self.viewport_width): 91 | if not self._platform.supports_framebuffers(): 92 | self.delete() 93 | self._create() 94 | 95 | self._platform.make_current() 96 | self._renderer.viewport_width = self.viewport_width 97 | self._renderer.viewport_height = self.viewport_height 98 | self._renderer.point_size = self.point_size 99 | 100 | if self._platform.supports_framebuffers(): 101 | flags |= RenderFlags.OFFSCREEN 102 | retval = self._renderer.render(scene, flags, seg_node_map) 103 | else: 104 | self._renderer.render(scene, flags, seg_node_map) 105 | depth = self._renderer.read_depth_buf() 106 | if flags & RenderFlags.DEPTH_ONLY: 107 | retval = depth 108 | else: 109 | color = self._renderer.read_color_buf() 110 | retval = color, depth 111 | 112 | # Make the platform not current 113 | self._platform.make_uncurrent() 114 | return retval 115 | 116 | def delete(self): 117 | """Free all OpenGL resources. 118 | """ 119 | self._platform.make_current() 120 | self._renderer.delete() 121 | self._platform.delete_context() 122 | del self._renderer 123 | del self._platform 124 | self._renderer = None 125 | self._platform = None 126 | import gc 127 | gc.collect() 128 | 129 | def _create(self): 130 | if 'PYOPENGL_PLATFORM' not in os.environ: 131 | from pyrender.platforms.pyglet_platform import PygletPlatform 132 | self._platform = PygletPlatform(self.viewport_width, 133 | self.viewport_height) 134 | elif os.environ['PYOPENGL_PLATFORM'] == 'egl': 135 | from pyrender.platforms import egl 136 | device_id = int(os.environ.get('EGL_DEVICE_ID', '0')) 137 | egl_device = egl.get_device_by_index(device_id) 138 | self._platform = egl.EGLPlatform(self.viewport_width, 139 | self.viewport_height, 140 | device=egl_device) 141 | elif os.environ['PYOPENGL_PLATFORM'] == 'osmesa': 142 | from pyrender.platforms.osmesa import OSMesaPlatform 143 | self._platform = OSMesaPlatform(self.viewport_width, 144 | self.viewport_height) 145 | else: 146 | raise ValueError('Unsupported PyOpenGL platform: {}'.format( 147 | os.environ['PYOPENGL_PLATFORM'] 148 | )) 149 | self._platform.init_context() 150 | self._platform.make_current() 151 | self._renderer = Renderer(self.viewport_width, self.viewport_height) 152 | 153 | def __del__(self): 154 | try: 155 | self.delete() 156 | except Exception: 157 | pass 158 | 159 | 160 | __all__ = ['OffscreenRenderer'] 161 | -------------------------------------------------------------------------------- /pyrender/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | """Platforms for generating offscreen OpenGL contexts for rendering. 2 | 3 | Author: Matthew Matl 4 | """ 5 | 6 | from .base import Platform 7 | -------------------------------------------------------------------------------- /pyrender/platforms/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import six 4 | 5 | 6 | @six.add_metaclass(abc.ABCMeta) 7 | class Platform(object): 8 | """Base class for all OpenGL platforms. 9 | 10 | Parameters 11 | ---------- 12 | viewport_width : int 13 | The width of the main viewport, in pixels. 14 | viewport_height : int 15 | The height of the main viewport, in pixels 16 | """ 17 | 18 | def __init__(self, viewport_width, viewport_height): 19 | self.viewport_width = viewport_width 20 | self.viewport_height = viewport_height 21 | 22 | @property 23 | def viewport_width(self): 24 | """int : The width of the main viewport, in pixels. 25 | """ 26 | return self._viewport_width 27 | 28 | @viewport_width.setter 29 | def viewport_width(self, value): 30 | self._viewport_width = value 31 | 32 | @property 33 | def viewport_height(self): 34 | """int : The height of the main viewport, in pixels. 35 | """ 36 | return self._viewport_height 37 | 38 | @viewport_height.setter 39 | def viewport_height(self, value): 40 | self._viewport_height = value 41 | 42 | @abc.abstractmethod 43 | def init_context(self): 44 | """Create an OpenGL context. 45 | """ 46 | pass 47 | 48 | @abc.abstractmethod 49 | def make_current(self): 50 | """Make the OpenGL context current. 51 | """ 52 | pass 53 | 54 | @abc.abstractmethod 55 | def make_uncurrent(self): 56 | """Make the OpenGL context uncurrent. 57 | """ 58 | pass 59 | 60 | @abc.abstractmethod 61 | def delete_context(self): 62 | """Delete the OpenGL context. 63 | """ 64 | pass 65 | 66 | @abc.abstractmethod 67 | def supports_framebuffers(self): 68 | """Returns True if the method supports framebuffer rendering. 69 | """ 70 | pass 71 | 72 | def __del__(self): 73 | try: 74 | self.delete_context() 75 | except Exception: 76 | pass 77 | -------------------------------------------------------------------------------- /pyrender/platforms/egl.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | 4 | import OpenGL.platform 5 | 6 | from .base import Platform 7 | 8 | EGL_PLATFORM_DEVICE_EXT = 0x313F 9 | EGL_DRM_DEVICE_FILE_EXT = 0x3233 10 | 11 | 12 | def _ensure_egl_loaded(): 13 | plugin = OpenGL.platform.PlatformPlugin.by_name('egl') 14 | if plugin is None: 15 | raise RuntimeError("EGL platform plugin is not available.") 16 | 17 | plugin_class = plugin.load() 18 | plugin.loaded = True 19 | # create instance of this platform implementation 20 | plugin = plugin_class() 21 | 22 | plugin.install(vars(OpenGL.platform)) 23 | 24 | 25 | _ensure_egl_loaded() 26 | from OpenGL import EGL as egl 27 | 28 | 29 | def _get_egl_func(func_name, res_type, *arg_types): 30 | address = egl.eglGetProcAddress(func_name) 31 | if address is None: 32 | return None 33 | 34 | proto = ctypes.CFUNCTYPE(res_type) 35 | proto.argtypes = arg_types 36 | func = proto(address) 37 | return func 38 | 39 | 40 | def _get_egl_struct(struct_name): 41 | from OpenGL._opaque import opaque_pointer_cls 42 | return opaque_pointer_cls(struct_name) 43 | 44 | 45 | # These are not defined in PyOpenGL by default. 46 | _EGLDeviceEXT = _get_egl_struct('EGLDeviceEXT') 47 | _eglGetPlatformDisplayEXT = _get_egl_func('eglGetPlatformDisplayEXT', egl.EGLDisplay) 48 | _eglQueryDevicesEXT = _get_egl_func('eglQueryDevicesEXT', egl.EGLBoolean) 49 | _eglQueryDeviceStringEXT = _get_egl_func('eglQueryDeviceStringEXT', ctypes.c_char_p) 50 | 51 | 52 | def query_devices(): 53 | if _eglQueryDevicesEXT is None: 54 | raise RuntimeError("EGL query extension is not loaded or is not supported.") 55 | 56 | num_devices = egl.EGLint() 57 | success = _eglQueryDevicesEXT(0, None, ctypes.pointer(num_devices)) 58 | if not success or num_devices.value < 1: 59 | return [] 60 | 61 | devices = (_EGLDeviceEXT * num_devices.value)() # array of size num_devices 62 | success = _eglQueryDevicesEXT(num_devices.value, devices, ctypes.pointer(num_devices)) 63 | if not success or num_devices.value < 1: 64 | return [] 65 | 66 | return [EGLDevice(devices[i]) for i in range(num_devices.value)] 67 | 68 | 69 | def get_default_device(): 70 | # Fall back to not using query extension. 71 | if _eglQueryDevicesEXT is None: 72 | return EGLDevice(None) 73 | 74 | return query_devices()[0] 75 | 76 | 77 | def get_device_by_index(device_id): 78 | if _eglQueryDevicesEXT is None and device_id == 0: 79 | return get_default_device() 80 | 81 | devices = query_devices() 82 | if device_id >= len(devices): 83 | raise ValueError('Invalid device ID ({})'.format(device_id, len(devices))) 84 | return devices[device_id] 85 | 86 | 87 | class EGLDevice: 88 | 89 | def __init__(self, display=None): 90 | self._display = display 91 | 92 | def get_display(self): 93 | if self._display is None: 94 | return egl.eglGetDisplay(egl.EGL_DEFAULT_DISPLAY) 95 | 96 | return _eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, self._display, None) 97 | 98 | @property 99 | def name(self): 100 | if self._display is None: 101 | return 'default' 102 | 103 | name = _eglQueryDeviceStringEXT(self._display, EGL_DRM_DEVICE_FILE_EXT) 104 | if name is None: 105 | return None 106 | 107 | return name.decode('ascii') 108 | 109 | def __repr__(self): 110 | return "".format(self.name) 111 | 112 | 113 | class EGLPlatform(Platform): 114 | """Renders using EGL. 115 | """ 116 | 117 | def __init__(self, viewport_width, viewport_height, device: EGLDevice = None): 118 | super(EGLPlatform, self).__init__(viewport_width, viewport_height) 119 | if device is None: 120 | device = get_default_device() 121 | 122 | self._egl_device = device 123 | self._egl_display = None 124 | self._egl_context = None 125 | 126 | def init_context(self): 127 | _ensure_egl_loaded() 128 | 129 | from OpenGL.EGL import ( 130 | EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE, 131 | EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_DEPTH_SIZE, 132 | EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, 133 | EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_CONFORMANT, 134 | EGL_NONE, EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, 135 | EGL_OPENGL_API, EGL_CONTEXT_MAJOR_VERSION, 136 | EGL_CONTEXT_MINOR_VERSION, 137 | EGL_CONTEXT_OPENGL_PROFILE_MASK, 138 | EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, 139 | eglGetDisplay, eglInitialize, eglChooseConfig, 140 | eglBindAPI, eglCreateContext, EGLConfig 141 | ) 142 | from OpenGL import arrays 143 | 144 | config_attributes = arrays.GLintArray.asArray([ 145 | EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, 146 | EGL_BLUE_SIZE, 8, 147 | EGL_RED_SIZE, 8, 148 | EGL_GREEN_SIZE, 8, 149 | EGL_DEPTH_SIZE, 24, 150 | EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, 151 | EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, 152 | EGL_CONFORMANT, EGL_OPENGL_BIT, 153 | EGL_NONE 154 | ]) 155 | context_attributes = arrays.GLintArray.asArray([ 156 | EGL_CONTEXT_MAJOR_VERSION, 4, 157 | EGL_CONTEXT_MINOR_VERSION, 1, 158 | EGL_CONTEXT_OPENGL_PROFILE_MASK, 159 | EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, 160 | EGL_NONE 161 | ]) 162 | major, minor = ctypes.c_long(), ctypes.c_long() 163 | num_configs = ctypes.c_long() 164 | configs = (EGLConfig * 1)() 165 | 166 | # Cache DISPLAY if necessary and get an off-screen EGL display 167 | orig_dpy = None 168 | if 'DISPLAY' in os.environ: 169 | orig_dpy = os.environ['DISPLAY'] 170 | del os.environ['DISPLAY'] 171 | 172 | self._egl_display = self._egl_device.get_display() 173 | if orig_dpy is not None: 174 | os.environ['DISPLAY'] = orig_dpy 175 | 176 | # Initialize EGL 177 | assert eglInitialize(self._egl_display, major, minor) 178 | assert eglChooseConfig( 179 | self._egl_display, config_attributes, configs, 1, num_configs 180 | ) 181 | 182 | # Bind EGL to the OpenGL API 183 | assert eglBindAPI(EGL_OPENGL_API) 184 | 185 | # Create an EGL context 186 | self._egl_context = eglCreateContext( 187 | self._egl_display, configs[0], 188 | EGL_NO_CONTEXT, context_attributes 189 | ) 190 | 191 | # Make it current 192 | self.make_current() 193 | 194 | def make_current(self): 195 | from OpenGL.EGL import eglMakeCurrent, EGL_NO_SURFACE 196 | assert eglMakeCurrent( 197 | self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, 198 | self._egl_context 199 | ) 200 | 201 | def make_uncurrent(self): 202 | """Make the OpenGL context uncurrent. 203 | """ 204 | pass 205 | 206 | def delete_context(self): 207 | from OpenGL.EGL import eglDestroyContext, eglTerminate 208 | if self._egl_display is not None: 209 | if self._egl_context is not None: 210 | eglDestroyContext(self._egl_display, self._egl_context) 211 | self._egl_context = None 212 | eglTerminate(self._egl_display) 213 | self._egl_display = None 214 | 215 | def supports_framebuffers(self): 216 | return True 217 | 218 | 219 | __all__ = ['EGLPlatform'] 220 | -------------------------------------------------------------------------------- /pyrender/platforms/osmesa.py: -------------------------------------------------------------------------------- 1 | from .base import Platform 2 | 3 | 4 | __all__ = ['OSMesaPlatform'] 5 | 6 | 7 | class OSMesaPlatform(Platform): 8 | """Renders into a software buffer using OSMesa. Requires special versions 9 | of OSMesa to be installed, plus PyOpenGL upgrade. 10 | """ 11 | 12 | def __init__(self, viewport_width, viewport_height): 13 | super(OSMesaPlatform, self).__init__(viewport_width, viewport_height) 14 | self._context = None 15 | self._buffer = None 16 | 17 | def init_context(self): 18 | from OpenGL import arrays 19 | from OpenGL.osmesa import ( 20 | OSMesaCreateContextAttribs, OSMESA_FORMAT, 21 | OSMESA_RGBA, OSMESA_PROFILE, OSMESA_CORE_PROFILE, 22 | OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION, 23 | OSMESA_DEPTH_BITS 24 | ) 25 | 26 | attrs = arrays.GLintArray.asArray([ 27 | OSMESA_FORMAT, OSMESA_RGBA, 28 | OSMESA_DEPTH_BITS, 24, 29 | OSMESA_PROFILE, OSMESA_CORE_PROFILE, 30 | OSMESA_CONTEXT_MAJOR_VERSION, 3, 31 | OSMESA_CONTEXT_MINOR_VERSION, 3, 32 | 0 33 | ]) 34 | self._context = OSMesaCreateContextAttribs(attrs, None) 35 | self._buffer = arrays.GLubyteArray.zeros( 36 | (self.viewport_height, self.viewport_width, 4) 37 | ) 38 | 39 | def make_current(self): 40 | from OpenGL import GL as gl 41 | from OpenGL.osmesa import OSMesaMakeCurrent 42 | assert(OSMesaMakeCurrent( 43 | self._context, self._buffer, gl.GL_UNSIGNED_BYTE, 44 | self.viewport_width, self.viewport_height 45 | )) 46 | 47 | def make_uncurrent(self): 48 | """Make the OpenGL context uncurrent. 49 | """ 50 | pass 51 | 52 | def delete_context(self): 53 | from OpenGL.osmesa import OSMesaDestroyContext 54 | OSMesaDestroyContext(self._context) 55 | self._context = None 56 | self._buffer = None 57 | 58 | def supports_framebuffers(self): 59 | return False 60 | -------------------------------------------------------------------------------- /pyrender/platforms/pyglet_platform.py: -------------------------------------------------------------------------------- 1 | from pyrender.constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR, 2 | MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR) 3 | from .base import Platform 4 | 5 | import OpenGL 6 | 7 | 8 | __all__ = ['PygletPlatform'] 9 | 10 | 11 | class PygletPlatform(Platform): 12 | """Renders on-screen using a 1x1 hidden Pyglet window for getting 13 | an OpenGL context. 14 | """ 15 | 16 | def __init__(self, viewport_width, viewport_height): 17 | super(PygletPlatform, self).__init__(viewport_width, viewport_height) 18 | self._window = None 19 | 20 | def init_context(self): 21 | import pyglet 22 | pyglet.options['shadow_window'] = False 23 | 24 | try: 25 | pyglet.lib.x11.xlib.XInitThreads() 26 | except Exception: 27 | pass 28 | 29 | self._window = None 30 | confs = [pyglet.gl.Config(sample_buffers=1, samples=4, 31 | depth_size=24, 32 | double_buffer=True, 33 | major_version=TARGET_OPEN_GL_MAJOR, 34 | minor_version=TARGET_OPEN_GL_MINOR), 35 | pyglet.gl.Config(depth_size=24, 36 | double_buffer=True, 37 | major_version=TARGET_OPEN_GL_MAJOR, 38 | minor_version=TARGET_OPEN_GL_MINOR), 39 | pyglet.gl.Config(sample_buffers=1, samples=4, 40 | depth_size=24, 41 | double_buffer=True, 42 | major_version=MIN_OPEN_GL_MAJOR, 43 | minor_version=MIN_OPEN_GL_MINOR), 44 | pyglet.gl.Config(depth_size=24, 45 | double_buffer=True, 46 | major_version=MIN_OPEN_GL_MAJOR, 47 | minor_version=MIN_OPEN_GL_MINOR)] 48 | for conf in confs: 49 | try: 50 | self._window = pyglet.window.Window(config=conf, visible=False, 51 | resizable=False, 52 | width=1, height=1) 53 | break 54 | except pyglet.window.NoSuchConfigException as e: 55 | pass 56 | 57 | if not self._window: 58 | raise ValueError( 59 | 'Failed to initialize Pyglet window with an OpenGL >= 3+ ' 60 | 'context. If you\'re logged in via SSH, ensure that you\'re ' 61 | 'running your script with vglrun (i.e. VirtualGL). The ' 62 | 'internal error message was "{}"'.format(e) 63 | ) 64 | 65 | def make_current(self): 66 | if self._window: 67 | self._window.switch_to() 68 | 69 | def make_uncurrent(self): 70 | try: 71 | import pyglet 72 | pyglet.gl.xlib.glx.glXMakeContextCurrent(self._window.context.x_display, 0, 0, None) 73 | except Exception: 74 | pass 75 | 76 | def delete_context(self): 77 | if self._window is not None: 78 | self.make_current() 79 | cid = OpenGL.contextdata.getContext() 80 | try: 81 | self._window.context.destroy() 82 | self._window.close() 83 | except Exception: 84 | pass 85 | self._window = None 86 | OpenGL.contextdata.cleanupContext(cid) 87 | del cid 88 | 89 | def supports_framebuffers(self): 90 | return True 91 | -------------------------------------------------------------------------------- /pyrender/sampler.py: -------------------------------------------------------------------------------- 1 | """Samplers, conforming to the glTF 2.0 standards as specified in 2 | https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-sampler 3 | 4 | Author: Matthew Matl 5 | """ 6 | from .constants import GLTF 7 | 8 | 9 | class Sampler(object): 10 | """Texture sampler properties for filtering and wrapping modes. 11 | 12 | Parameters 13 | ---------- 14 | name : str, optional 15 | The user-defined name of this object. 16 | magFilter : int, optional 17 | Magnification filter. Valid values: 18 | - :attr:`.GLTF.NEAREST` 19 | - :attr:`.GLTF.LINEAR` 20 | minFilter : int, optional 21 | Minification filter. Valid values: 22 | - :attr:`.GLTF.NEAREST` 23 | - :attr:`.GLTF.LINEAR` 24 | - :attr:`.GLTF.NEAREST_MIPMAP_NEAREST` 25 | - :attr:`.GLTF.LINEAR_MIPMAP_NEAREST` 26 | - :attr:`.GLTF.NEAREST_MIPMAP_LINEAR` 27 | - :attr:`.GLTF.LINEAR_MIPMAP_LINEAR` 28 | wrapS : int, optional 29 | S (U) wrapping mode. Valid values: 30 | - :attr:`.GLTF.CLAMP_TO_EDGE` 31 | - :attr:`.GLTF.MIRRORED_REPEAT` 32 | - :attr:`.GLTF.REPEAT` 33 | wrapT : int, optional 34 | T (V) wrapping mode. Valid values: 35 | - :attr:`.GLTF.CLAMP_TO_EDGE` 36 | - :attr:`.GLTF.MIRRORED_REPEAT` 37 | - :attr:`.GLTF.REPEAT` 38 | """ 39 | 40 | def __init__(self, 41 | name=None, 42 | magFilter=None, 43 | minFilter=None, 44 | wrapS=GLTF.REPEAT, 45 | wrapT=GLTF.REPEAT): 46 | self.name = name 47 | self.magFilter = magFilter 48 | self.minFilter = minFilter 49 | self.wrapS = wrapS 50 | self.wrapT = wrapT 51 | 52 | @property 53 | def name(self): 54 | """str : The user-defined name of this object. 55 | """ 56 | return self._name 57 | 58 | @name.setter 59 | def name(self, value): 60 | if value is not None: 61 | value = str(value) 62 | self._name = value 63 | 64 | @property 65 | def magFilter(self): 66 | """int : Magnification filter type. 67 | """ 68 | return self._magFilter 69 | 70 | @magFilter.setter 71 | def magFilter(self, value): 72 | self._magFilter = value 73 | 74 | @property 75 | def minFilter(self): 76 | """int : Minification filter type. 77 | """ 78 | return self._minFilter 79 | 80 | @minFilter.setter 81 | def minFilter(self, value): 82 | self._minFilter = value 83 | 84 | @property 85 | def wrapS(self): 86 | """int : S (U) wrapping mode. 87 | """ 88 | return self._wrapS 89 | 90 | @wrapS.setter 91 | def wrapS(self, value): 92 | self._wrapS = value 93 | 94 | @property 95 | def wrapT(self): 96 | """int : T (V) wrapping mode. 97 | """ 98 | return self._wrapT 99 | 100 | @wrapT.setter 101 | def wrapT(self, value): 102 | self._wrapT = value 103 | -------------------------------------------------------------------------------- /pyrender/shader_program.py: -------------------------------------------------------------------------------- 1 | """OpenGL shader program wrapper. 2 | """ 3 | import numpy as np 4 | import os 5 | import re 6 | 7 | import OpenGL 8 | from OpenGL.GL import * 9 | from OpenGL.GL import shaders as gl_shader_utils 10 | 11 | 12 | class ShaderProgramCache(object): 13 | """A cache for shader programs. 14 | """ 15 | 16 | def __init__(self, shader_dir=None): 17 | self._program_cache = {} 18 | self.shader_dir = shader_dir 19 | if self.shader_dir is None: 20 | base_dir, _ = os.path.split(os.path.realpath(__file__)) 21 | self.shader_dir = os.path.join(base_dir, 'shaders') 22 | 23 | def get_program(self, vertex_shader, fragment_shader, 24 | geometry_shader=None, defines=None): 25 | """Get a program via a list of shader files to include in the program. 26 | 27 | Parameters 28 | ---------- 29 | vertex_shader : str 30 | The vertex shader filename. 31 | fragment_shader : str 32 | The fragment shader filename. 33 | geometry_shader : str 34 | The geometry shader filename. 35 | defines : dict 36 | Defines and their values for the shader. 37 | 38 | Returns 39 | ------- 40 | program : :class:`.ShaderProgram` 41 | The program. 42 | """ 43 | shader_names = [] 44 | if defines is None: 45 | defines = {} 46 | shader_filenames = [ 47 | x for x in [vertex_shader, fragment_shader, geometry_shader] 48 | if x is not None 49 | ] 50 | for fn in shader_filenames: 51 | if fn is None: 52 | continue 53 | _, name = os.path.split(fn) 54 | shader_names.append(name) 55 | cid = OpenGL.contextdata.getContext() 56 | key = tuple([cid] + sorted( 57 | [(s,1) for s in shader_names] + [(d, defines[d]) for d in defines] 58 | )) 59 | 60 | if key not in self._program_cache: 61 | shader_filenames = [ 62 | os.path.join(self.shader_dir, fn) for fn in shader_filenames 63 | ] 64 | if len(shader_filenames) == 2: 65 | shader_filenames.append(None) 66 | vs, fs, gs = shader_filenames 67 | self._program_cache[key] = ShaderProgram( 68 | vertex_shader=vs, fragment_shader=fs, 69 | geometry_shader=gs, defines=defines 70 | ) 71 | return self._program_cache[key] 72 | 73 | def clear(self): 74 | for key in self._program_cache: 75 | self._program_cache[key].delete() 76 | self._program_cache = {} 77 | 78 | 79 | class ShaderProgram(object): 80 | """A thin wrapper about OpenGL shader programs that supports easy creation, 81 | binding, and uniform-setting. 82 | 83 | Parameters 84 | ---------- 85 | vertex_shader : str 86 | The vertex shader filename. 87 | fragment_shader : str 88 | The fragment shader filename. 89 | geometry_shader : str 90 | The geometry shader filename. 91 | defines : dict 92 | Defines and their values for the shader. 93 | """ 94 | 95 | def __init__(self, vertex_shader, fragment_shader, 96 | geometry_shader=None, defines=None): 97 | 98 | self.vertex_shader = vertex_shader 99 | self.fragment_shader = fragment_shader 100 | self.geometry_shader = geometry_shader 101 | 102 | self.defines = defines 103 | if self.defines is None: 104 | self.defines = {} 105 | 106 | self._program_id = None 107 | self._vao_id = None # PYOPENGL BUG 108 | 109 | # DEBUG 110 | # self._unif_map = {} 111 | 112 | def _add_to_context(self): 113 | if self._program_id is not None: 114 | raise ValueError('Shader program already in context') 115 | shader_ids = [] 116 | 117 | # Load vert shader 118 | shader_ids.append(gl_shader_utils.compileShader( 119 | self._load(self.vertex_shader), GL_VERTEX_SHADER) 120 | ) 121 | # Load frag shader 122 | shader_ids.append(gl_shader_utils.compileShader( 123 | self._load(self.fragment_shader), GL_FRAGMENT_SHADER) 124 | ) 125 | # Load geometry shader 126 | if self.geometry_shader is not None: 127 | shader_ids.append(gl_shader_utils.compileShader( 128 | self._load(self.geometry_shader), GL_GEOMETRY_SHADER) 129 | ) 130 | 131 | # Bind empty VAO PYOPENGL BUG 132 | if self._vao_id is None: 133 | self._vao_id = glGenVertexArrays(1) 134 | glBindVertexArray(self._vao_id) 135 | 136 | # Compile program 137 | self._program_id = gl_shader_utils.compileProgram(*shader_ids) 138 | 139 | # Unbind empty VAO PYOPENGL BUG 140 | glBindVertexArray(0) 141 | 142 | def _in_context(self): 143 | return self._program_id is not None 144 | 145 | def _remove_from_context(self): 146 | if self._program_id is not None: 147 | glDeleteProgram(self._program_id) 148 | glDeleteVertexArrays(1, [self._vao_id]) 149 | self._program_id = None 150 | self._vao_id = None 151 | 152 | def _load(self, shader_filename): 153 | path, _ = os.path.split(shader_filename) 154 | 155 | with open(shader_filename) as f: 156 | text = f.read() 157 | 158 | def ifdef(matchobj): 159 | if matchobj.group(1) in self.defines: 160 | return '#if 1' 161 | else: 162 | return '#if 0' 163 | 164 | def ifndef(matchobj): 165 | if matchobj.group(1) in self.defines: 166 | return '#if 0' 167 | else: 168 | return '#if 1' 169 | 170 | ifdef_regex = re.compile( 171 | '#ifdef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE 172 | ) 173 | ifndef_regex = re.compile( 174 | '#ifndef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE 175 | ) 176 | text = re.sub(ifdef_regex, ifdef, text) 177 | text = re.sub(ifndef_regex, ifndef, text) 178 | 179 | for define in self.defines: 180 | value = str(self.defines[define]) 181 | text = text.replace(define, value) 182 | 183 | return text 184 | 185 | def _bind(self): 186 | """Bind this shader program to the current OpenGL context. 187 | """ 188 | if self._program_id is None: 189 | raise ValueError('Cannot bind program that is not in context') 190 | # glBindVertexArray(self._vao_id) 191 | glUseProgram(self._program_id) 192 | 193 | def _unbind(self): 194 | """Unbind this shader program from the current OpenGL context. 195 | """ 196 | glUseProgram(0) 197 | 198 | def delete(self): 199 | """Delete this shader program from the current OpenGL context. 200 | """ 201 | self._remove_from_context() 202 | 203 | def set_uniform(self, name, value, unsigned=False): 204 | """Set a uniform value in the current shader program. 205 | 206 | Parameters 207 | ---------- 208 | name : str 209 | Name of the uniform to set. 210 | value : int, float, or ndarray 211 | Value to set the uniform to. 212 | unsigned : bool 213 | If True, ints will be treated as unsigned values. 214 | """ 215 | try: 216 | # DEBUG 217 | # self._unif_map[name] = 1, (1,) 218 | loc = glGetUniformLocation(self._program_id, name) 219 | 220 | if loc == -1: 221 | raise ValueError('Invalid shader variable: {}'.format(name)) 222 | 223 | if isinstance(value, np.ndarray): 224 | # DEBUG 225 | # self._unif_map[name] = value.size, value.shape 226 | if value.ndim == 1: 227 | if (np.issubdtype(value.dtype, np.unsignedinteger) or 228 | unsigned): 229 | dtype = 'u' 230 | value = value.astype(np.uint32) 231 | elif np.issubdtype(value.dtype, np.integer): 232 | dtype = 'i' 233 | value = value.astype(np.int32) 234 | else: 235 | dtype = 'f' 236 | value = value.astype(np.float32) 237 | self._FUNC_MAP[(value.shape[0], dtype)](loc, 1, value) 238 | else: 239 | self._FUNC_MAP[(value.shape[0], value.shape[1])]( 240 | loc, 1, GL_TRUE, value 241 | ) 242 | 243 | # Call correct uniform function 244 | elif isinstance(value, float): 245 | glUniform1f(loc, value) 246 | elif isinstance(value, int): 247 | if unsigned: 248 | glUniform1ui(loc, value) 249 | else: 250 | glUniform1i(loc, value) 251 | elif isinstance(value, bool): 252 | if unsigned: 253 | glUniform1ui(loc, int(value)) 254 | else: 255 | glUniform1i(loc, int(value)) 256 | else: 257 | raise ValueError('Invalid data type') 258 | except Exception: 259 | pass 260 | 261 | _FUNC_MAP = { 262 | (1,'u'): glUniform1uiv, 263 | (2,'u'): glUniform2uiv, 264 | (3,'u'): glUniform3uiv, 265 | (4,'u'): glUniform4uiv, 266 | (1,'i'): glUniform1iv, 267 | (2,'i'): glUniform2iv, 268 | (3,'i'): glUniform3iv, 269 | (4,'i'): glUniform4iv, 270 | (1,'f'): glUniform1fv, 271 | (2,'f'): glUniform2fv, 272 | (3,'f'): glUniform3fv, 273 | (4,'f'): glUniform4fv, 274 | (2,2): glUniformMatrix2fv, 275 | (2,3): glUniformMatrix2x3fv, 276 | (2,4): glUniformMatrix2x4fv, 277 | (3,2): glUniformMatrix3x2fv, 278 | (3,3): glUniformMatrix3fv, 279 | (3,4): glUniformMatrix3x4fv, 280 | (4,2): glUniformMatrix4x2fv, 281 | (4,3): glUniformMatrix4x3fv, 282 | (4,4): glUniformMatrix4fv, 283 | } 284 | -------------------------------------------------------------------------------- /pyrender/shaders/debug_quad.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec2 TexCoords; 5 | 6 | uniform sampler2D depthMap; 7 | //uniform float near_plane; 8 | //uniform float far_plane; 9 | // 10 | //// required when using a perspective projection matrix 11 | //float LinearizeDepth(float depth) 12 | //{ 13 | // float z = depth * 2.0 - 1.0; // Back to NDC 14 | // return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane)); 15 | //} 16 | 17 | void main() 18 | { 19 | float depthValue = texture(depthMap, TexCoords).r; 20 | // FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective 21 | FragColor = vec4(vec3(depthValue), 1.0); // orthographic 22 | //FragColor = vec4(1.0, 1.0, 0.0, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /pyrender/shaders/debug_quad.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | //layout (location = 0) in vec3 aPos; 3 | //layout (location = 1) in vec2 aTexCoords; 4 | // 5 | //out vec2 TexCoords; 6 | // 7 | //void main() 8 | //{ 9 | // TexCoords = aTexCoords; 10 | // gl_Position = vec4(aPos, 1.0); 11 | //} 12 | // 13 | // 14 | //layout(location = 0) out vec2 uv; 15 | 16 | out vec2 TexCoords; 17 | 18 | void main() 19 | { 20 | float x = float(((uint(gl_VertexID) + 2u) / 3u)%2u); 21 | float y = float(((uint(gl_VertexID) + 1u) / 3u)%2u); 22 | 23 | gl_Position = vec4(-1.0f + x*2.0f, -1.0f+y*2.0f, 0.0f, 1.0f); 24 | TexCoords = vec2(x, y); 25 | } 26 | -------------------------------------------------------------------------------- /pyrender/shaders/flat.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | /////////////////////////////////////////////////////////////////////////////// 3 | // Structs 4 | /////////////////////////////////////////////////////////////////////////////// 5 | 6 | struct Material { 7 | vec3 emissive_factor; 8 | 9 | #ifdef USE_METALLIC_MATERIAL 10 | vec4 base_color_factor; 11 | float metallic_factor; 12 | float roughness_factor; 13 | #endif 14 | 15 | #ifdef USE_GLOSSY_MATERIAL 16 | vec4 diffuse_factor; 17 | vec3 specular_factor; 18 | float glossiness_factor; 19 | #endif 20 | 21 | #ifdef HAS_NORMAL_TEX 22 | sampler2D normal_texture; 23 | #endif 24 | #ifdef HAS_OCCLUSION_TEX 25 | sampler2D occlusion_texture; 26 | #endif 27 | #ifdef HAS_EMISSIVE_TEX 28 | sampler2D emissive_texture; 29 | #endif 30 | #ifdef HAS_BASE_COLOR_TEX 31 | sampler2D base_color_texture; 32 | #endif 33 | #ifdef HAS_METALLIC_ROUGHNESS_TEX 34 | sampler2D metallic_roughness_texture; 35 | #endif 36 | #ifdef HAS_DIFFUSE_TEX 37 | sampler2D diffuse_texture; 38 | #endif 39 | #ifdef HAS_SPECULAR_GLOSSINESS_TEX 40 | sampler2D specular_glossiness; 41 | #endif 42 | }; 43 | 44 | /////////////////////////////////////////////////////////////////////////////// 45 | // Uniforms 46 | /////////////////////////////////////////////////////////////////////////////// 47 | uniform Material material; 48 | uniform vec3 cam_pos; 49 | 50 | #ifdef USE_IBL 51 | uniform samplerCube diffuse_env; 52 | uniform samplerCube specular_env; 53 | #endif 54 | 55 | /////////////////////////////////////////////////////////////////////////////// 56 | // Inputs 57 | /////////////////////////////////////////////////////////////////////////////// 58 | 59 | in vec3 frag_position; 60 | #ifdef NORMAL_LOC 61 | in vec3 frag_normal; 62 | #endif 63 | #ifdef HAS_NORMAL_TEX 64 | #ifdef TANGENT_LOC 65 | #ifdef NORMAL_LOC 66 | in mat3 tbn; 67 | #endif 68 | #endif 69 | #endif 70 | #ifdef TEXCOORD_0_LOC 71 | in vec2 uv_0; 72 | #endif 73 | #ifdef TEXCOORD_1_LOC 74 | in vec2 uv_1; 75 | #endif 76 | #ifdef COLOR_0_LOC 77 | in vec4 color_multiplier; 78 | #endif 79 | 80 | /////////////////////////////////////////////////////////////////////////////// 81 | // OUTPUTS 82 | /////////////////////////////////////////////////////////////////////////////// 83 | 84 | out vec4 frag_color; 85 | 86 | /////////////////////////////////////////////////////////////////////////////// 87 | // Constants 88 | /////////////////////////////////////////////////////////////////////////////// 89 | const float PI = 3.141592653589793; 90 | const float min_roughness = 0.04; 91 | 92 | /////////////////////////////////////////////////////////////////////////////// 93 | // Utility Functions 94 | /////////////////////////////////////////////////////////////////////////////// 95 | vec4 srgb_to_linear(vec4 srgb) 96 | { 97 | #ifndef SRGB_CORRECTED 98 | // Fast Approximation 99 | //vec3 linOut = pow(srgbIn.xyz,vec3(2.2)); 100 | // 101 | vec3 b_less = step(vec3(0.04045),srgb.xyz); 102 | vec3 lin_out = mix( srgb.xyz/vec3(12.92), pow((srgb.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), b_less ); 103 | return vec4(lin_out, srgb.w); 104 | #else 105 | return srgb; 106 | #endif 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////// 110 | // MAIN 111 | /////////////////////////////////////////////////////////////////////////////// 112 | void main() 113 | { 114 | 115 | // Compute albedo 116 | vec4 base_color = material.base_color_factor; 117 | #ifdef HAS_BASE_COLOR_TEX 118 | base_color = base_color * texture(material.base_color_texture, uv_0); 119 | #endif 120 | 121 | #ifdef COLOR_0_LOC 122 | base_color *= color_multiplier; 123 | #endif 124 | 125 | frag_color = clamp(base_color, 0.0, 1.0); 126 | } 127 | -------------------------------------------------------------------------------- /pyrender/shaders/flat.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Vertex Attributes 4 | layout(location = 0) in vec3 position; 5 | #ifdef NORMAL_LOC 6 | layout(location = NORMAL_LOC) in vec3 normal; 7 | #endif 8 | #ifdef TANGENT_LOC 9 | layout(location = TANGENT_LOC) in vec4 tangent; 10 | #endif 11 | #ifdef TEXCOORD_0_LOC 12 | layout(location = TEXCOORD_0_LOC) in vec2 texcoord_0; 13 | #endif 14 | #ifdef TEXCOORD_1_LOC 15 | layout(location = TEXCOORD_1_LOC) in vec2 texcoord_1; 16 | #endif 17 | #ifdef COLOR_0_LOC 18 | layout(location = COLOR_0_LOC) in vec4 color_0; 19 | #endif 20 | #ifdef JOINTS_0_LOC 21 | layout(location = JOINTS_0_LOC) in vec4 joints_0; 22 | #endif 23 | #ifdef WEIGHTS_0_LOC 24 | layout(location = WEIGHTS_0_LOC) in vec4 weights_0; 25 | #endif 26 | layout(location = INST_M_LOC) in mat4 inst_m; 27 | 28 | // Uniforms 29 | uniform mat4 M; 30 | uniform mat4 V; 31 | uniform mat4 P; 32 | 33 | // Outputs 34 | out vec3 frag_position; 35 | #ifdef NORMAL_LOC 36 | out vec3 frag_normal; 37 | #endif 38 | #ifdef HAS_NORMAL_TEX 39 | #ifdef TANGENT_LOC 40 | #ifdef NORMAL_LOC 41 | out mat3 tbn; 42 | #endif 43 | #endif 44 | #endif 45 | #ifdef TEXCOORD_0_LOC 46 | out vec2 uv_0; 47 | #endif 48 | #ifdef TEXCOORD_1_LOC 49 | out vec2 uv_1; 50 | #endif 51 | #ifdef COLOR_0_LOC 52 | out vec4 color_multiplier; 53 | #endif 54 | 55 | 56 | void main() 57 | { 58 | gl_Position = P * V * M * inst_m * vec4(position, 1); 59 | frag_position = vec3(M * inst_m * vec4(position, 1.0)); 60 | 61 | mat4 N = transpose(inverse(M * inst_m)); 62 | 63 | #ifdef NORMAL_LOC 64 | frag_normal = normalize(vec3(N * vec4(normal, 0.0))); 65 | #endif 66 | 67 | #ifdef HAS_NORMAL_TEX 68 | #ifdef TANGENT_LOC 69 | #ifdef NORMAL_LOC 70 | vec3 normal_w = normalize(vec3(N * vec4(normal, 0.0))); 71 | vec3 tangent_w = normalize(vec3(N * vec4(tangent.xyz, 0.0))); 72 | vec3 bitangent_w = cross(normal_w, tangent_w) * tangent.w; 73 | tbn = mat3(tangent_w, bitangent_w, normal_w); 74 | #endif 75 | #endif 76 | #endif 77 | #ifdef TEXCOORD_0_LOC 78 | uv_0 = texcoord_0; 79 | #endif 80 | #ifdef TEXCOORD_1_LOC 81 | uv_1 = texcoord_1; 82 | #endif 83 | #ifdef COLOR_0_LOC 84 | color_multiplier = color_0; 85 | #endif 86 | } 87 | -------------------------------------------------------------------------------- /pyrender/shaders/mesh.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Vertex Attributes 4 | layout(location = 0) in vec3 position; 5 | #ifdef NORMAL_LOC 6 | layout(location = NORMAL_LOC) in vec3 normal; 7 | #endif 8 | #ifdef TANGENT_LOC 9 | layout(location = TANGENT_LOC) in vec4 tangent; 10 | #endif 11 | #ifdef TEXCOORD_0_LOC 12 | layout(location = TEXCOORD_0_LOC) in vec2 texcoord_0; 13 | #endif 14 | #ifdef TEXCOORD_1_LOC 15 | layout(location = TEXCOORD_1_LOC) in vec2 texcoord_1; 16 | #endif 17 | #ifdef COLOR_0_LOC 18 | layout(location = COLOR_0_LOC) in vec4 color_0; 19 | #endif 20 | #ifdef JOINTS_0_LOC 21 | layout(location = JOINTS_0_LOC) in vec4 joints_0; 22 | #endif 23 | #ifdef WEIGHTS_0_LOC 24 | layout(location = WEIGHTS_0_LOC) in vec4 weights_0; 25 | #endif 26 | layout(location = INST_M_LOC) in mat4 inst_m; 27 | 28 | // Uniforms 29 | uniform mat4 M; 30 | uniform mat4 V; 31 | uniform mat4 P; 32 | 33 | // Outputs 34 | out vec3 frag_position; 35 | #ifdef NORMAL_LOC 36 | out vec3 frag_normal; 37 | #endif 38 | #ifdef HAS_NORMAL_TEX 39 | #ifdef TANGENT_LOC 40 | #ifdef NORMAL_LOC 41 | out mat3 tbn; 42 | #endif 43 | #endif 44 | #endif 45 | #ifdef TEXCOORD_0_LOC 46 | out vec2 uv_0; 47 | #endif 48 | #ifdef TEXCOORD_1_LOC 49 | out vec2 uv_1; 50 | #endif 51 | #ifdef COLOR_0_LOC 52 | out vec4 color_multiplier; 53 | #endif 54 | 55 | 56 | void main() 57 | { 58 | gl_Position = P * V * M * inst_m * vec4(position, 1); 59 | frag_position = vec3(M * inst_m * vec4(position, 1.0)); 60 | 61 | mat4 N = transpose(inverse(M * inst_m)); 62 | 63 | #ifdef NORMAL_LOC 64 | frag_normal = normalize(vec3(N * vec4(normal, 0.0))); 65 | #endif 66 | 67 | #ifdef HAS_NORMAL_TEX 68 | #ifdef TANGENT_LOC 69 | #ifdef NORMAL_LOC 70 | vec3 normal_w = normalize(vec3(N * vec4(normal, 0.0))); 71 | vec3 tangent_w = normalize(vec3(N * vec4(tangent.xyz, 0.0))); 72 | vec3 bitangent_w = cross(normal_w, tangent_w) * tangent.w; 73 | tbn = mat3(tangent_w, bitangent_w, normal_w); 74 | #endif 75 | #endif 76 | #endif 77 | #ifdef TEXCOORD_0_LOC 78 | uv_0 = texcoord_0; 79 | #endif 80 | #ifdef TEXCOORD_1_LOC 81 | uv_1 = texcoord_1; 82 | #endif 83 | #ifdef COLOR_0_LOC 84 | color_multiplier = color_0; 85 | #endif 86 | } 87 | -------------------------------------------------------------------------------- /pyrender/shaders/mesh_depth.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 frag_color; 4 | 5 | void main() 6 | { 7 | frag_color = vec4(1.0); 8 | } 9 | -------------------------------------------------------------------------------- /pyrender/shaders/mesh_depth.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout(location = 0) in vec3 position; 3 | layout(location = INST_M_LOC) in mat4 inst_m; 4 | 5 | uniform mat4 P; 6 | uniform mat4 V; 7 | uniform mat4 M; 8 | 9 | void main() 10 | { 11 | mat4 light_matrix = P * V; 12 | gl_Position = light_matrix * M * inst_m * vec4(position, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /pyrender/shaders/segmentation.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform vec3 color; 4 | out vec4 frag_color; 5 | 6 | /////////////////////////////////////////////////////////////////////////////// 7 | // MAIN 8 | /////////////////////////////////////////////////////////////////////////////// 9 | void main() 10 | { 11 | frag_color = vec4(color, 1.0); 12 | //frag_color = vec4(1.0, 0.5, 0.5, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /pyrender/shaders/segmentation.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout(location = 0) in vec3 position; 3 | layout(location = INST_M_LOC) in mat4 inst_m; 4 | 5 | uniform mat4 P; 6 | uniform mat4 V; 7 | uniform mat4 M; 8 | 9 | void main() 10 | { 11 | mat4 light_matrix = P * V; 12 | gl_Position = light_matrix * M * inst_m * vec4(position, 1.0); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /pyrender/shaders/text.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | in vec2 uv; 3 | out vec4 color; 4 | 5 | uniform sampler2D text; 6 | uniform vec4 text_color; 7 | 8 | void main() 9 | { 10 | vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, uv).r); 11 | color = text_color * sampled; 12 | } 13 | -------------------------------------------------------------------------------- /pyrender/shaders/text.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec4 vertex; 3 | 4 | out vec2 uv; 5 | 6 | uniform mat4 projection; 7 | 8 | void main() 9 | { 10 | gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); 11 | uv = vertex.zw; 12 | } 13 | -------------------------------------------------------------------------------- /pyrender/shaders/vertex_normals.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 frag_color; 4 | 5 | uniform vec4 normal_color; 6 | 7 | void main() 8 | { 9 | frag_color = normal_color; 10 | } 11 | -------------------------------------------------------------------------------- /pyrender/shaders/vertex_normals.geom: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (triangles) in; 4 | 5 | #ifdef FACE_NORMALS 6 | 7 | #ifdef VERTEX_NORMALS 8 | layout (line_strip, max_vertices = 8) out; 9 | #else 10 | layout (line_strip, max_vertices = 2) out; 11 | #endif 12 | 13 | #else 14 | 15 | layout (line_strip, max_vertices = 6) out; 16 | 17 | #endif 18 | 19 | in VS_OUT { 20 | vec3 position; 21 | vec3 normal; 22 | mat4 mvp; 23 | } gs_in[]; 24 | 25 | uniform float normal_magnitude; 26 | 27 | void GenerateVertNormal(int index) 28 | { 29 | 30 | vec4 p0 = gs_in[index].mvp * vec4(gs_in[index].position, 1.0); 31 | vec4 p1 = gs_in[index].mvp * vec4(normal_magnitude * normalize(gs_in[index].normal) + gs_in[index].position, 1.0); 32 | gl_Position = p0; 33 | EmitVertex(); 34 | gl_Position = p1; 35 | EmitVertex(); 36 | EndPrimitive(); 37 | } 38 | 39 | void GenerateFaceNormal() 40 | { 41 | vec3 p0 = gs_in[0].position.xyz; 42 | vec3 p1 = gs_in[1].position.xyz; 43 | vec3 p2 = gs_in[2].position.xyz; 44 | 45 | vec3 v0 = p0 - p1; 46 | vec3 v1 = p2 - p1; 47 | 48 | vec3 N = normalize(cross(v1, v0)); 49 | vec3 P = (p0 + p1 + p2) / 3.0; 50 | 51 | vec4 np0 = gs_in[0].mvp * vec4(P, 1.0); 52 | vec4 np1 = gs_in[0].mvp * vec4(normal_magnitude * N + P, 1.0); 53 | 54 | gl_Position = np0; 55 | EmitVertex(); 56 | gl_Position = np1; 57 | EmitVertex(); 58 | EndPrimitive(); 59 | } 60 | 61 | void main() 62 | { 63 | 64 | #ifdef FACE_NORMALS 65 | GenerateFaceNormal(); 66 | #endif 67 | 68 | #ifdef VERTEX_NORMALS 69 | GenerateVertNormal(0); 70 | GenerateVertNormal(1); 71 | GenerateVertNormal(2); 72 | #endif 73 | 74 | } 75 | -------------------------------------------------------------------------------- /pyrender/shaders/vertex_normals.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Inputs 4 | layout(location = 0) in vec3 position; 5 | layout(location = NORMAL_LOC) in vec3 normal; 6 | layout(location = INST_M_LOC) in mat4 inst_m; 7 | 8 | // Output data 9 | out VS_OUT { 10 | vec3 position; 11 | vec3 normal; 12 | mat4 mvp; 13 | } vs_out; 14 | 15 | // Uniform data 16 | uniform mat4 M; 17 | uniform mat4 V; 18 | uniform mat4 P; 19 | 20 | // Render loop 21 | void main() { 22 | vs_out.mvp = P * V * M * inst_m; 23 | vs_out.position = position; 24 | vs_out.normal = normal; 25 | 26 | gl_Position = vec4(position, 1.0); 27 | } 28 | -------------------------------------------------------------------------------- /pyrender/shaders/vertex_normals_pc.geom: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (points) in; 4 | 5 | layout (line_strip, max_vertices = 2) out; 6 | 7 | in VS_OUT { 8 | vec3 position; 9 | vec3 normal; 10 | mat4 mvp; 11 | } gs_in[]; 12 | 13 | uniform float normal_magnitude; 14 | 15 | void GenerateVertNormal(int index) 16 | { 17 | vec4 p0 = gs_in[index].mvp * vec4(gs_in[index].position, 1.0); 18 | vec4 p1 = gs_in[index].mvp * vec4(normal_magnitude * normalize(gs_in[index].normal) + gs_in[index].position, 1.0); 19 | gl_Position = p0; 20 | EmitVertex(); 21 | gl_Position = p1; 22 | EmitVertex(); 23 | EndPrimitive(); 24 | } 25 | 26 | void main() 27 | { 28 | GenerateVertNormal(0); 29 | } 30 | -------------------------------------------------------------------------------- /pyrender/texture.py: -------------------------------------------------------------------------------- 1 | """Textures, conforming to the glTF 2.0 standards as specified in 2 | https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-texture 3 | 4 | Author: Matthew Matl 5 | """ 6 | import numpy as np 7 | 8 | from OpenGL.GL import * 9 | 10 | from .utils import format_texture_source 11 | from .sampler import Sampler 12 | 13 | 14 | class Texture(object): 15 | """A texture and its sampler. 16 | 17 | Parameters 18 | ---------- 19 | name : str, optional 20 | The user-defined name of this object. 21 | sampler : :class:`Sampler` 22 | The sampler used by this texture. 23 | source : (h,w,c) uint8 or (h,w,c) float or :class:`PIL.Image.Image` 24 | The image used by this texture. If None, the texture is created 25 | empty and width and height must be specified. 26 | source_channels : str 27 | Either `D`, `R`, `RG`, `GB`, `RGB`, or `RGBA`. Indicates the 28 | channels to extract from `source`. Any missing channels will be filled 29 | with `1.0`. 30 | width : int, optional 31 | For empty textures, the width of the texture buffer. 32 | height : int, optional 33 | For empty textures, the height of the texture buffer. 34 | tex_type : int 35 | Either GL_TEXTURE_2D or GL_TEXTURE_CUBE. 36 | data_format : int 37 | For now, just GL_FLOAT. 38 | """ 39 | 40 | def __init__(self, 41 | name=None, 42 | sampler=None, 43 | source=None, 44 | source_channels=None, 45 | width=None, 46 | height=None, 47 | tex_type=GL_TEXTURE_2D, 48 | data_format=GL_UNSIGNED_BYTE): 49 | self.source_channels = source_channels 50 | self.name = name 51 | self.sampler = sampler 52 | self.source = source 53 | self.width = width 54 | self.height = height 55 | self.tex_type = tex_type 56 | self.data_format = data_format 57 | 58 | self._texid = None 59 | self._is_transparent = False 60 | 61 | @property 62 | def name(self): 63 | """str : The user-defined name of this object. 64 | """ 65 | return self._name 66 | 67 | @name.setter 68 | def name(self, value): 69 | if value is not None: 70 | value = str(value) 71 | self._name = value 72 | 73 | @property 74 | def sampler(self): 75 | """:class:`Sampler` : The sampler used by this texture. 76 | """ 77 | return self._sampler 78 | 79 | @sampler.setter 80 | def sampler(self, value): 81 | if value is None: 82 | value = Sampler() 83 | self._sampler = value 84 | 85 | @property 86 | def source(self): 87 | """(h,w,c) uint8 or float or :class:`PIL.Image.Image` : The image 88 | used in this texture. 89 | """ 90 | return self._source 91 | 92 | @source.setter 93 | def source(self, value): 94 | if value is None: 95 | self._source = None 96 | else: 97 | self._source = format_texture_source(value, self.source_channels) 98 | self._is_transparent = False 99 | 100 | @property 101 | def source_channels(self): 102 | """str : The channels that were extracted from the original source. 103 | """ 104 | return self._source_channels 105 | 106 | @source_channels.setter 107 | def source_channels(self, value): 108 | self._source_channels = value 109 | 110 | @property 111 | def width(self): 112 | """int : The width of the texture buffer. 113 | """ 114 | return self._width 115 | 116 | @width.setter 117 | def width(self, value): 118 | self._width = value 119 | 120 | @property 121 | def height(self): 122 | """int : The height of the texture buffer. 123 | """ 124 | return self._height 125 | 126 | @height.setter 127 | def height(self, value): 128 | self._height = value 129 | 130 | @property 131 | def tex_type(self): 132 | """int : The type of the texture. 133 | """ 134 | return self._tex_type 135 | 136 | @tex_type.setter 137 | def tex_type(self, value): 138 | self._tex_type = value 139 | 140 | @property 141 | def data_format(self): 142 | """int : The format of the texture data. 143 | """ 144 | return self._data_format 145 | 146 | @data_format.setter 147 | def data_format(self, value): 148 | self._data_format = value 149 | 150 | def is_transparent(self, cutoff=1.0): 151 | """bool : If True, the texture is partially transparent. 152 | """ 153 | if self._is_transparent is None: 154 | self._is_transparent = False 155 | if self.source_channels == 'RGBA' and self.source is not None: 156 | if np.any(self.source[:,:,3] < cutoff): 157 | self._is_transparent = True 158 | return self._is_transparent 159 | 160 | def delete(self): 161 | """Remove this texture from the OpenGL context. 162 | """ 163 | self._unbind() 164 | self._remove_from_context() 165 | 166 | ################## 167 | # OpenGL code 168 | ################## 169 | def _add_to_context(self): 170 | if self._texid is not None: 171 | raise ValueError('Texture already loaded into OpenGL context') 172 | 173 | fmt = GL_DEPTH_COMPONENT 174 | if self.source_channels == 'R': 175 | fmt = GL_RED 176 | elif self.source_channels == 'RG' or self.source_channels == 'GB': 177 | fmt = GL_RG 178 | elif self.source_channels == 'RGB': 179 | fmt = GL_RGB 180 | elif self.source_channels == 'RGBA': 181 | fmt = GL_RGBA 182 | 183 | # Generate the OpenGL texture 184 | self._texid = glGenTextures(1) 185 | glBindTexture(self.tex_type, self._texid) 186 | 187 | # Flip data for OpenGL buffer 188 | data = None 189 | width = self.width 190 | height = self.height 191 | if self.source is not None: 192 | data = np.ascontiguousarray(np.flip(self.source, axis=0).flatten()) 193 | width = self.source.shape[1] 194 | height = self.source.shape[0] 195 | 196 | # Bind texture and generate mipmaps 197 | glTexImage2D( 198 | self.tex_type, 0, fmt, width, height, 0, fmt, 199 | self.data_format, data 200 | ) 201 | if self.source is not None: 202 | glGenerateMipmap(self.tex_type) 203 | 204 | if self.sampler.magFilter is not None: 205 | glTexParameteri( 206 | self.tex_type, GL_TEXTURE_MAG_FILTER, self.sampler.magFilter 207 | ) 208 | else: 209 | if self.source is not None: 210 | glTexParameteri(self.tex_type, GL_TEXTURE_MAG_FILTER, GL_LINEAR) 211 | else: 212 | glTexParameteri(self.tex_type, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 213 | if self.sampler.minFilter is not None: 214 | glTexParameteri( 215 | self.tex_type, GL_TEXTURE_MIN_FILTER, self.sampler.minFilter 216 | ) 217 | else: 218 | if self.source is not None: 219 | glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) 220 | else: 221 | glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 222 | 223 | glTexParameteri(self.tex_type, GL_TEXTURE_WRAP_S, self.sampler.wrapS) 224 | glTexParameteri(self.tex_type, GL_TEXTURE_WRAP_T, self.sampler.wrapT) 225 | border_color = 255 * np.ones(4).astype(np.uint8) 226 | if self.data_format == GL_FLOAT: 227 | border_color = np.ones(4).astype(np.float32) 228 | glTexParameterfv( 229 | self.tex_type, GL_TEXTURE_BORDER_COLOR, 230 | border_color 231 | ) 232 | 233 | # Unbind texture 234 | glBindTexture(self.tex_type, 0) 235 | 236 | def _remove_from_context(self): 237 | if self._texid is not None: 238 | # TODO OPENGL BUG? 239 | # glDeleteTextures(1, [self._texid]) 240 | glDeleteTextures([self._texid]) 241 | self._texid = None 242 | 243 | def _in_context(self): 244 | return self._texid is not None 245 | 246 | def _bind(self): 247 | # TODO HANDLE INDEXING INTO OTHER UV's 248 | glBindTexture(self.tex_type, self._texid) 249 | 250 | def _unbind(self): 251 | glBindTexture(self.tex_type, 0) 252 | 253 | def _bind_as_depth_attachment(self): 254 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, 255 | self.tex_type, self._texid, 0) 256 | 257 | def _bind_as_color_attachment(self): 258 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 259 | self.tex_type, self._texid, 0) 260 | -------------------------------------------------------------------------------- /pyrender/trackball.py: -------------------------------------------------------------------------------- 1 | """Trackball class for 3D manipulation of viewpoints. 2 | """ 3 | import numpy as np 4 | 5 | import trimesh.transformations as transformations 6 | 7 | 8 | class Trackball(object): 9 | """A trackball class for creating camera transforms from mouse movements. 10 | """ 11 | STATE_ROTATE = 0 12 | STATE_PAN = 1 13 | STATE_ROLL = 2 14 | STATE_ZOOM = 3 15 | 16 | def __init__(self, pose, size, scale, 17 | target=np.array([0.0, 0.0, 0.0])): 18 | """Initialize a trackball with an initial camera-to-world pose 19 | and the given parameters. 20 | 21 | Parameters 22 | ---------- 23 | pose : [4,4] 24 | An initial camera-to-world pose for the trackball. 25 | 26 | size : (float, float) 27 | The width and height of the camera image in pixels. 28 | 29 | scale : float 30 | The diagonal of the scene's bounding box -- 31 | used for ensuring translation motions are sufficiently 32 | fast for differently-sized scenes. 33 | 34 | target : (3,) float 35 | The center of the scene in world coordinates. 36 | The trackball will revolve around this point. 37 | """ 38 | self._size = np.array(size) 39 | self._scale = float(scale) 40 | 41 | self._pose = pose 42 | self._n_pose = pose 43 | 44 | self._target = target 45 | self._n_target = target 46 | 47 | self._state = Trackball.STATE_ROTATE 48 | 49 | @property 50 | def pose(self): 51 | """autolab_core.RigidTransform : The current camera-to-world pose. 52 | """ 53 | return self._n_pose 54 | 55 | def set_state(self, state): 56 | """Set the state of the trackball in order to change the effect of 57 | dragging motions. 58 | 59 | Parameters 60 | ---------- 61 | state : int 62 | One of Trackball.STATE_ROTATE, Trackball.STATE_PAN, 63 | Trackball.STATE_ROLL, and Trackball.STATE_ZOOM. 64 | """ 65 | self._state = state 66 | 67 | def resize(self, size): 68 | """Resize the window. 69 | 70 | Parameters 71 | ---------- 72 | size : (float, float) 73 | The new width and height of the camera image in pixels. 74 | """ 75 | self._size = np.array(size) 76 | 77 | def down(self, point): 78 | """Record an initial mouse press at a given point. 79 | 80 | Parameters 81 | ---------- 82 | point : (2,) int 83 | The x and y pixel coordinates of the mouse press. 84 | """ 85 | self._pdown = np.array(point, dtype=np.float32) 86 | self._pose = self._n_pose 87 | self._target = self._n_target 88 | 89 | def drag(self, point): 90 | """Update the tracball during a drag. 91 | 92 | Parameters 93 | ---------- 94 | point : (2,) int 95 | The current x and y pixel coordinates of the mouse during a drag. 96 | This will compute a movement for the trackball with the relative 97 | motion between this point and the one marked by down(). 98 | """ 99 | point = np.array(point, dtype=np.float32) 100 | dx, dy = point - self._pdown 101 | mindim = 0.3 * np.min(self._size) 102 | 103 | target = self._target 104 | x_axis = self._pose[:3,0].flatten() 105 | y_axis = self._pose[:3,1].flatten() 106 | z_axis = self._pose[:3,2].flatten() 107 | eye = self._pose[:3,3].flatten() 108 | 109 | # Interpret drag as a rotation 110 | if self._state == Trackball.STATE_ROTATE: 111 | x_angle = -dx / mindim 112 | x_rot_mat = transformations.rotation_matrix( 113 | x_angle, y_axis, target 114 | ) 115 | 116 | y_angle = dy / mindim 117 | y_rot_mat = transformations.rotation_matrix( 118 | y_angle, x_axis, target 119 | ) 120 | 121 | self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose)) 122 | 123 | # Interpret drag as a roll about the camera axis 124 | elif self._state == Trackball.STATE_ROLL: 125 | center = self._size / 2.0 126 | v_init = self._pdown - center 127 | v_curr = point - center 128 | v_init = v_init / np.linalg.norm(v_init) 129 | v_curr = v_curr / np.linalg.norm(v_curr) 130 | 131 | theta = (-np.arctan2(v_curr[1], v_curr[0]) + 132 | np.arctan2(v_init[1], v_init[0])) 133 | 134 | rot_mat = transformations.rotation_matrix(theta, z_axis, target) 135 | 136 | self._n_pose = rot_mat.dot(self._pose) 137 | 138 | # Interpret drag as a camera pan in view plane 139 | elif self._state == Trackball.STATE_PAN: 140 | dx = -dx / (5.0 * mindim) * self._scale 141 | dy = -dy / (5.0 * mindim) * self._scale 142 | 143 | translation = dx * x_axis + dy * y_axis 144 | self._n_target = self._target + translation 145 | t_tf = np.eye(4) 146 | t_tf[:3,3] = translation 147 | self._n_pose = t_tf.dot(self._pose) 148 | 149 | # Interpret drag as a zoom motion 150 | elif self._state == Trackball.STATE_ZOOM: 151 | radius = np.linalg.norm(eye - target) 152 | ratio = 0.0 153 | if dy > 0: 154 | ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0 155 | elif dy < 0: 156 | ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1]))) 157 | translation = -np.sign(dy) * ratio * radius * z_axis 158 | t_tf = np.eye(4) 159 | t_tf[:3,3] = translation 160 | self._n_pose = t_tf.dot(self._pose) 161 | 162 | def scroll(self, clicks): 163 | """Zoom using a mouse scroll wheel motion. 164 | 165 | Parameters 166 | ---------- 167 | clicks : int 168 | The number of clicks. Positive numbers indicate forward wheel 169 | movement. 170 | """ 171 | target = self._target 172 | ratio = 0.90 173 | 174 | mult = 1.0 175 | if clicks > 0: 176 | mult = ratio**clicks 177 | elif clicks < 0: 178 | mult = (1.0 / ratio)**abs(clicks) 179 | 180 | z_axis = self._n_pose[:3,2].flatten() 181 | eye = self._n_pose[:3,3].flatten() 182 | radius = np.linalg.norm(eye - target) 183 | translation = (mult * radius - radius) * z_axis 184 | t_tf = np.eye(4) 185 | t_tf[:3,3] = translation 186 | self._n_pose = t_tf.dot(self._n_pose) 187 | 188 | z_axis = self._pose[:3,2].flatten() 189 | eye = self._pose[:3,3].flatten() 190 | radius = np.linalg.norm(eye - target) 191 | translation = (mult * radius - radius) * z_axis 192 | t_tf = np.eye(4) 193 | t_tf[:3,3] = translation 194 | self._pose = t_tf.dot(self._pose) 195 | 196 | def rotate(self, azimuth, axis=None): 197 | """Rotate the trackball about the "Up" axis by azimuth radians. 198 | 199 | Parameters 200 | ---------- 201 | azimuth : float 202 | The number of radians to rotate. 203 | """ 204 | target = self._target 205 | 206 | y_axis = self._n_pose[:3,1].flatten() 207 | if axis is not None: 208 | y_axis = axis 209 | x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) 210 | self._n_pose = x_rot_mat.dot(self._n_pose) 211 | 212 | y_axis = self._pose[:3,1].flatten() 213 | if axis is not None: 214 | y_axis = axis 215 | x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) 216 | self._pose = x_rot_mat.dot(self._pose) 217 | -------------------------------------------------------------------------------- /pyrender/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image 3 | 4 | 5 | def format_color_vector(value, length): 6 | """Format a color vector. 7 | """ 8 | if isinstance(value, int): 9 | value = value / 255.0 10 | if isinstance(value, float): 11 | value = np.repeat(value, length) 12 | if isinstance(value, list) or isinstance(value, tuple): 13 | value = np.array(value) 14 | if isinstance(value, np.ndarray): 15 | value = value.squeeze() 16 | if np.issubdtype(value.dtype, np.integer): 17 | value = (value / 255.0).astype(np.float32) 18 | if value.ndim != 1: 19 | raise ValueError('Format vector takes only 1-D vectors') 20 | if length > value.shape[0]: 21 | value = np.hstack((value, np.ones(length - value.shape[0]))) 22 | elif length < value.shape[0]: 23 | value = value[:length] 24 | else: 25 | raise ValueError('Invalid vector data type') 26 | 27 | return value.squeeze().astype(np.float32) 28 | 29 | 30 | def format_color_array(value, shape): 31 | """Format an array of colors. 32 | """ 33 | # Convert uint8 to floating 34 | value = np.asanyarray(value) 35 | if np.issubdtype(value.dtype, np.integer): 36 | value = (value / 255.0).astype(np.float32) 37 | 38 | # Match up shapes 39 | if value.ndim == 1: 40 | value = np.tile(value, (shape[0],1)) 41 | if value.shape[1] < shape[1]: 42 | nc = shape[1] - value.shape[1] 43 | value = np.column_stack((value, np.ones((value.shape[0], nc)))) 44 | elif value.shape[1] > shape[1]: 45 | value = value[:,:shape[1]] 46 | return value.astype(np.float32) 47 | 48 | 49 | def format_texture_source(texture, target_channels='RGB'): 50 | """Format a texture as a float32 np array. 51 | """ 52 | 53 | # Pass through None 54 | if texture is None: 55 | return None 56 | 57 | # Convert PIL images into numpy arrays 58 | if isinstance(texture, Image.Image): 59 | if texture.mode == 'P' and target_channels in ('RGB', 'RGBA'): 60 | texture = np.array(texture.convert(target_channels)) 61 | else: 62 | texture = np.array(texture) 63 | 64 | # Format numpy arrays 65 | if isinstance(texture, np.ndarray): 66 | if np.issubdtype(texture.dtype, np.floating): 67 | texture = np.array(texture * 255.0, dtype=np.uint8) 68 | elif np.issubdtype(texture.dtype, np.integer): 69 | texture = texture.astype(np.uint8) 70 | else: 71 | raise TypeError('Invalid type {} for texture'.format( 72 | type(texture) 73 | )) 74 | 75 | # Format array by picking out correct texture channels or padding 76 | if texture.ndim == 2: 77 | texture = texture[:,:,np.newaxis] 78 | if target_channels == 'R': 79 | texture = texture[:,:,0] 80 | texture = texture.squeeze() 81 | elif target_channels == 'RG': 82 | if texture.shape[2] == 1: 83 | texture = np.repeat(texture, 2, axis=2) 84 | else: 85 | texture = texture[:,:,(0,1)] 86 | elif target_channels == 'GB': 87 | if texture.shape[2] == 1: 88 | texture = np.repeat(texture, 2, axis=2) 89 | elif texture.shape[2] > 2: 90 | texture = texture[:,:,(1,2)] 91 | elif target_channels == 'RGB': 92 | if texture.shape[2] == 1: 93 | texture = np.repeat(texture, 3, axis=2) 94 | elif texture.shape[2] == 2: 95 | raise ValueError('Cannot reformat 2-channel texture into RGB') 96 | else: 97 | texture = texture[:,:,(0,1,2)] 98 | elif target_channels == 'RGBA': 99 | if texture.shape[2] == 1: 100 | texture = np.repeat(texture, 4, axis=2) 101 | texture[:,:,3] = 255 102 | elif texture.shape[2] == 2: 103 | raise ValueError('Cannot reformat 2-channel texture into RGBA') 104 | elif texture.shape[2] == 3: 105 | tx = np.empty((texture.shape[0], texture.shape[1], 4), dtype=np.uint8) 106 | tx[:,:,:3] = texture 107 | tx[:,:,3] = 255 108 | texture = tx 109 | else: 110 | raise ValueError('Invalid texture channel specification: {}' 111 | .format(target_channels)) 112 | else: 113 | raise TypeError('Invalid type {} for texture'.format(type(texture))) 114 | 115 | return texture 116 | -------------------------------------------------------------------------------- /pyrender/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.45' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | freetype-py 2 | imageio 3 | networkx 4 | numpy 5 | Pillow 6 | pyglet==1.4.0a1 7 | PyOpenGL 8 | PyOpenGL_accelerate 9 | six 10 | trimesh 11 | sphinx 12 | sphinx_rtd_theme 13 | sphinx-automodapi 14 | 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup of pyrender Python codebase. 3 | 4 | Author: Matthew Matl 5 | """ 6 | import sys 7 | from setuptools import setup 8 | 9 | # load __version__ 10 | exec(open('pyrender/version.py').read()) 11 | 12 | def get_imageio_dep(): 13 | if sys.version[0] == "2": 14 | return 'imageio<=2.6.1' 15 | return 'imageio' 16 | 17 | requirements = [ 18 | 'freetype-py', # For font loading 19 | get_imageio_dep(), # For Image I/O 20 | 'networkx', # For the scene graph 21 | 'numpy', # Numpy 22 | 'Pillow', # For Trimesh texture conversions 23 | 'pyglet>=1.4.10', # For the pyglet viewer 24 | 'PyOpenGL~=3.1.0', # For OpenGL 25 | # 'PyOpenGL_accelerate~=3.1.0', # For OpenGL 26 | 'scipy', # Because of trimesh missing dep 27 | 'six', # For Python 2/3 interop 28 | 'trimesh', # For meshes 29 | ] 30 | 31 | dev_requirements = [ 32 | 'flake8', # Code formatting checker 33 | 'pre-commit', # Pre-commit hooks 34 | 'pytest', # Code testing 35 | 'pytest-cov', # Coverage testing 36 | 'tox', # Automatic virtualenv testing 37 | ] 38 | 39 | docs_requirements = [ 40 | 'sphinx', # General doc library 41 | 'sphinx_rtd_theme', # RTD theme for sphinx 42 | 'sphinx-automodapi' # For generating nice tables 43 | ] 44 | 45 | 46 | setup( 47 | name = 'pyrender', 48 | version=__version__, 49 | description='Easy-to-use Python renderer for 3D visualization', 50 | long_description='A simple implementation of Physically-Based Rendering ' 51 | '(PBR) in Python. Compliant with the glTF 2.0 standard.', 52 | author='Matthew Matl', 53 | author_email='matthewcmatl@gmail.com', 54 | license='MIT License', 55 | url = 'https://github.com/mmatl/pyrender', 56 | classifiers = [ 57 | 'Development Status :: 4 - Beta', 58 | 'License :: OSI Approved :: MIT License', 59 | 'Operating System :: POSIX :: Linux', 60 | 'Operating System :: MacOS :: MacOS X', 61 | 'Programming Language :: Python :: 2.7', 62 | 'Programming Language :: Python :: 3.5', 63 | 'Programming Language :: Python :: 3.6', 64 | 'Natural Language :: English', 65 | 'Topic :: Scientific/Engineering' 66 | ], 67 | keywords = 'rendering graphics opengl 3d visualization pbr gltf', 68 | packages = ['pyrender', 'pyrender.platforms'], 69 | setup_requires = requirements, 70 | install_requires = requirements, 71 | extras_require={ 72 | 'dev': dev_requirements, 73 | 'docs': docs_requirements, 74 | }, 75 | include_package_data=True 76 | ) 77 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/conftest.py -------------------------------------------------------------------------------- /tests/data/Duck.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/data/Duck.glb -------------------------------------------------------------------------------- /tests/data/WaterBottle.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/data/WaterBottle.glb -------------------------------------------------------------------------------- /tests/data/drill.obj.mtl: -------------------------------------------------------------------------------- 1 | newmtl material_0 2 | # shader_type beckmann 3 | map_Kd drill_uv.png 4 | 5 | -------------------------------------------------------------------------------- /tests/data/drill_uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/data/drill_uv.png -------------------------------------------------------------------------------- /tests/data/fuze.obj.mtl: -------------------------------------------------------------------------------- 1 | # 2 | # Wavefront material file 3 | # Converted by Meshlab Group 4 | # 5 | 6 | newmtl material_0 7 | Ka 0.200000 0.200000 0.200000 8 | Kd 1.000000 1.000000 1.000000 9 | Ks 1.000000 1.000000 1.000000 10 | Tr 1.000000 11 | illum 2 12 | Ns 0.000000 13 | map_Kd fuze_uv.jpg 14 | 15 | -------------------------------------------------------------------------------- /tests/data/fuze_uv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/data/fuze_uv.jpg -------------------------------------------------------------------------------- /tests/data/wood.obj: -------------------------------------------------------------------------------- 1 | mtllib ./wood.obj.mtl 2 | 3 | v -0.300000 -0.300000 0.000000 4 | v 0.300000 -0.300000 0.000000 5 | v -0.300000 0.300000 0.000000 6 | v 0.300000 0.300000 0.000000 7 | vn 0.000000 0.000000 1.000000 8 | vn 0.000000 0.000000 1.000000 9 | vn 0.000000 0.000000 1.000000 10 | vn 0.000000 0.000000 1.000000 11 | vt 0.000000 0.000000 12 | vt 1.000000 0.000000 13 | vt 0.000000 1.000000 14 | vt 1.000000 1.000000 15 | 16 | usemtl material_0 17 | f 1/1/1 2/2/2 4/4/4 18 | f 1/1/1 4/4/4 3/3/3 19 | -------------------------------------------------------------------------------- /tests/data/wood.obj.mtl: -------------------------------------------------------------------------------- 1 | newmtl material_0 2 | map_Kd wood_uv.png 3 | -------------------------------------------------------------------------------- /tests/data/wood_uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/data/wood_uv.png -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/pytest.ini -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmatl/pyrender/a59963ef890891656fd17c90e12d663233dcaa99/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_cameras.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyrender import PerspectiveCamera, OrthographicCamera 5 | 6 | 7 | def test_perspective_camera(): 8 | 9 | # Set up constants 10 | znear = 0.05 11 | zfar = 100 12 | yfov = np.pi / 3.0 13 | width = 1000.0 14 | height = 500.0 15 | aspectRatio = 640.0 / 480.0 16 | 17 | # Test basics 18 | with pytest.raises(TypeError): 19 | p = PerspectiveCamera() 20 | 21 | p = PerspectiveCamera(yfov=yfov) 22 | assert p.yfov == yfov 23 | assert p.znear == 0.05 24 | assert p.zfar is None 25 | assert p.aspectRatio is None 26 | p.name = 'asdf' 27 | p.name = None 28 | 29 | with pytest.raises(ValueError): 30 | p.yfov = 0.0 31 | 32 | with pytest.raises(ValueError): 33 | p.yfov = -1.0 34 | 35 | with pytest.raises(ValueError): 36 | p.znear = -1.0 37 | 38 | p.znear = 0.0 39 | p.znear = 0.05 40 | p.zfar = 100.0 41 | assert p.zfar == 100.0 42 | 43 | with pytest.raises(ValueError): 44 | p.zfar = 0.03 45 | 46 | with pytest.raises(ValueError): 47 | p.zfar = 0.05 48 | 49 | p.aspectRatio = 10.0 50 | assert p.aspectRatio == 10.0 51 | 52 | with pytest.raises(ValueError): 53 | p.aspectRatio = 0.0 54 | 55 | with pytest.raises(ValueError): 56 | p.aspectRatio = -1.0 57 | 58 | # Test matrix getting/setting 59 | 60 | # NF 61 | p.znear = 0.05 62 | p.zfar = 100 63 | p.aspectRatio = None 64 | 65 | with pytest.raises(ValueError): 66 | p.get_projection_matrix() 67 | 68 | assert np.allclose( 69 | p.get_projection_matrix(width, height), 70 | np.array([ 71 | [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], 72 | [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], 73 | [0.0, 0.0, (zfar + znear) / (znear - zfar), 74 | (2 * zfar * znear) / (znear - zfar)], 75 | [0.0, 0.0, -1.0, 0.0] 76 | ]) 77 | ) 78 | 79 | # NFA 80 | p.aspectRatio = aspectRatio 81 | assert np.allclose( 82 | p.get_projection_matrix(width, height), 83 | np.array([ 84 | [1.0 / (aspectRatio * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], 85 | [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], 86 | [0.0, 0.0, (zfar + znear) / (znear - zfar), 87 | (2 * zfar * znear) / (znear - zfar)], 88 | [0.0, 0.0, -1.0, 0.0] 89 | ]) 90 | ) 91 | assert np.allclose( 92 | p.get_projection_matrix(), p.get_projection_matrix(width, height) 93 | ) 94 | 95 | # N 96 | p.zfar = None 97 | p.aspectRatio = None 98 | assert np.allclose( 99 | p.get_projection_matrix(width, height), 100 | np.array([ 101 | [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], 102 | [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], 103 | [0.0, 0.0, -1.0, -2.0 * znear], 104 | [0.0, 0.0, -1.0, 0.0] 105 | ]) 106 | ) 107 | 108 | 109 | def test_orthographic_camera(): 110 | xm = 1.0 111 | ym = 2.0 112 | n = 0.05 113 | f = 100.0 114 | 115 | with pytest.raises(TypeError): 116 | c = OrthographicCamera() 117 | 118 | c = OrthographicCamera(xmag=xm, ymag=ym) 119 | 120 | assert c.xmag == xm 121 | assert c.ymag == ym 122 | assert c.znear == 0.05 123 | assert c.zfar == 100.0 124 | assert c.name is None 125 | 126 | with pytest.raises(TypeError): 127 | c.ymag = None 128 | 129 | with pytest.raises(ValueError): 130 | c.ymag = 0.0 131 | 132 | with pytest.raises(ValueError): 133 | c.ymag = -1.0 134 | 135 | with pytest.raises(TypeError): 136 | c.xmag = None 137 | 138 | with pytest.raises(ValueError): 139 | c.xmag = 0.0 140 | 141 | with pytest.raises(ValueError): 142 | c.xmag = -1.0 143 | 144 | with pytest.raises(TypeError): 145 | c.znear = None 146 | 147 | with pytest.raises(ValueError): 148 | c.znear = 0.0 149 | 150 | with pytest.raises(ValueError): 151 | c.znear = -1.0 152 | 153 | with pytest.raises(ValueError): 154 | c.zfar = 0.01 155 | 156 | assert np.allclose( 157 | c.get_projection_matrix(), 158 | np.array([ 159 | [1.0 / xm, 0, 0, 0], 160 | [0, 1.0 / ym, 0, 0], 161 | [0, 0, 2.0 / (n - f), (f + n) / (n - f)], 162 | [0, 0, 0, 1.0] 163 | ]) 164 | ) 165 | -------------------------------------------------------------------------------- /tests/unit/test_egl.py: -------------------------------------------------------------------------------- 1 | # from pyrender.platforms import egl 2 | 3 | 4 | def tmp_test_default_device(): 5 | egl.get_default_device() 6 | 7 | 8 | def tmp_test_query_device(): 9 | devices = egl.query_devices() 10 | assert len(devices) > 0 11 | 12 | 13 | def tmp_test_init_context(): 14 | device = egl.query_devices()[0] 15 | platform = egl.EGLPlatform(128, 128, device=device) 16 | platform.init_context() 17 | -------------------------------------------------------------------------------- /tests/unit/test_lights.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyrender import (DirectionalLight, SpotLight, PointLight, Texture, 5 | PerspectiveCamera, OrthographicCamera) 6 | from pyrender.constants import SHADOW_TEX_SZ 7 | 8 | 9 | def test_directional_light(): 10 | 11 | d = DirectionalLight() 12 | assert d.name is None 13 | assert np.all(d.color == 1.0) 14 | assert d.intensity == 1.0 15 | 16 | d.name = 'direc' 17 | with pytest.raises(ValueError): 18 | d.color = None 19 | with pytest.raises(TypeError): 20 | d.intensity = None 21 | 22 | d = DirectionalLight(color=[0.0, 0.0, 0.0]) 23 | assert np.all(d.color == 0.0) 24 | 25 | d._generate_shadow_texture() 26 | st = d.shadow_texture 27 | assert isinstance(st, Texture) 28 | assert st.width == st.height == SHADOW_TEX_SZ 29 | 30 | sc = d._get_shadow_camera(scene_scale=5.0) 31 | assert isinstance(sc, OrthographicCamera) 32 | assert sc.xmag == sc.ymag == 5.0 33 | assert sc.znear == 0.01 * 5.0 34 | assert sc.zfar == 10 * 5.0 35 | 36 | 37 | def test_spot_light(): 38 | 39 | s = SpotLight() 40 | assert s.name is None 41 | assert np.all(s.color == 1.0) 42 | assert s.intensity == 1.0 43 | assert s.innerConeAngle == 0.0 44 | assert s.outerConeAngle == np.pi / 4.0 45 | assert s.range is None 46 | 47 | with pytest.raises(ValueError): 48 | s.range = -1.0 49 | 50 | with pytest.raises(ValueError): 51 | s.range = 0.0 52 | 53 | with pytest.raises(ValueError): 54 | s.innerConeAngle = -1.0 55 | 56 | with pytest.raises(ValueError): 57 | s.innerConeAngle = np.pi / 3.0 58 | 59 | with pytest.raises(ValueError): 60 | s.outerConeAngle = -1.0 61 | 62 | with pytest.raises(ValueError): 63 | s.outerConeAngle = np.pi 64 | 65 | s.range = 5.0 66 | s.outerConeAngle = np.pi / 2 - 0.05 67 | s.innerConeAngle = np.pi / 3 68 | s.innerConeAngle = 0.0 69 | s.outerConeAngle = np.pi / 4.0 70 | 71 | s._generate_shadow_texture() 72 | st = s.shadow_texture 73 | assert isinstance(st, Texture) 74 | assert st.width == st.height == SHADOW_TEX_SZ 75 | 76 | sc = s._get_shadow_camera(scene_scale=5.0) 77 | assert isinstance(sc, PerspectiveCamera) 78 | assert sc.znear == 0.01 * 5.0 79 | assert sc.zfar == 10 * 5.0 80 | assert sc.aspectRatio == 1.0 81 | assert np.allclose(sc.yfov, np.pi / 16.0 * 9.0) # Plus pi / 16 82 | 83 | 84 | def test_point_light(): 85 | 86 | s = PointLight() 87 | assert s.name is None 88 | assert np.all(s.color == 1.0) 89 | assert s.intensity == 1.0 90 | assert s.range is None 91 | 92 | with pytest.raises(ValueError): 93 | s.range = -1.0 94 | 95 | with pytest.raises(ValueError): 96 | s.range = 0.0 97 | 98 | s.range = 5.0 99 | 100 | with pytest.raises(NotImplementedError): 101 | s._generate_shadow_texture() 102 | 103 | with pytest.raises(NotImplementedError): 104 | s._get_shadow_camera(scene_scale=5.0) 105 | -------------------------------------------------------------------------------- /tests/unit/test_meshes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import trimesh 4 | 5 | from pyrender import (Mesh, Primitive) 6 | 7 | 8 | def test_meshes(): 9 | 10 | with pytest.raises(TypeError): 11 | x = Mesh() 12 | with pytest.raises(TypeError): 13 | x = Primitive() 14 | with pytest.raises(ValueError): 15 | x = Primitive([], mode=10) 16 | 17 | # Basics 18 | x = Mesh([]) 19 | assert x.name is None 20 | assert x.is_visible 21 | assert x.weights is None 22 | 23 | x.name = 'str' 24 | 25 | # From Trimesh 26 | x = Mesh.from_trimesh(trimesh.creation.box()) 27 | assert isinstance(x, Mesh) 28 | assert len(x.primitives) == 1 29 | assert x.is_visible 30 | assert np.allclose(x.bounds, np.array([ 31 | [-0.5, -0.5, -0.5], 32 | [0.5, 0.5, 0.5] 33 | ])) 34 | assert np.allclose(x.centroid, np.zeros(3)) 35 | assert np.allclose(x.extents, np.ones(3)) 36 | assert np.allclose(x.scale, np.sqrt(3)) 37 | assert not x.is_transparent 38 | 39 | # Test some primitive functions 40 | x = x.primitives[0] 41 | with pytest.raises(ValueError): 42 | x.normals = np.zeros(10) 43 | with pytest.raises(ValueError): 44 | x.tangents = np.zeros(10) 45 | with pytest.raises(ValueError): 46 | x.texcoord_0 = np.zeros(10) 47 | with pytest.raises(ValueError): 48 | x.texcoord_1 = np.zeros(10) 49 | with pytest.raises(TypeError): 50 | x.material = np.zeros(10) 51 | assert x.targets is None 52 | assert np.allclose(x.bounds, np.array([ 53 | [-0.5, -0.5, -0.5], 54 | [0.5, 0.5, 0.5] 55 | ])) 56 | assert np.allclose(x.centroid, np.zeros(3)) 57 | assert np.allclose(x.extents, np.ones(3)) 58 | assert np.allclose(x.scale, np.sqrt(3)) 59 | x.material.baseColorFactor = np.array([0.0, 0.0, 0.0, 0.0]) 60 | assert x.is_transparent 61 | 62 | # From two trimeshes 63 | x = Mesh.from_trimesh([trimesh.creation.box(), 64 | trimesh.creation.cylinder(radius=0.1, height=2.0)], 65 | smooth=False) 66 | assert isinstance(x, Mesh) 67 | assert len(x.primitives) == 2 68 | assert x.is_visible 69 | assert np.allclose(x.bounds, np.array([ 70 | [-0.5, -0.5, -1.0], 71 | [0.5, 0.5, 1.0] 72 | ])) 73 | assert np.allclose(x.centroid, np.zeros(3)) 74 | assert np.allclose(x.extents, [1.0, 1.0, 2.0]) 75 | assert np.allclose(x.scale, np.sqrt(6)) 76 | assert not x.is_transparent 77 | 78 | # From bad data 79 | with pytest.raises(TypeError): 80 | x = Mesh.from_trimesh(None) 81 | 82 | # With instancing 83 | poses = np.tile(np.eye(4), (5,1,1)) 84 | poses[:,0,3] = np.array([0,1,2,3,4]) 85 | x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) 86 | assert np.allclose(x.bounds, np.array([ 87 | [-0.5, -0.5, -0.5], 88 | [4.5, 0.5, 0.5] 89 | ])) 90 | poses = np.eye(4) 91 | x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) 92 | poses = np.eye(3) 93 | with pytest.raises(ValueError): 94 | x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) 95 | 96 | # From textured meshes 97 | fm = trimesh.load('tests/data/fuze.obj') 98 | x = Mesh.from_trimesh(fm) 99 | assert isinstance(x, Mesh) 100 | assert len(x.primitives) == 1 101 | assert x.is_visible 102 | assert not x.is_transparent 103 | assert x.primitives[0].material.baseColorTexture is not None 104 | 105 | x = Mesh.from_trimesh(fm, smooth=False) 106 | fm.visual = fm.visual.to_color() 107 | fm.visual.face_colors = np.array([1.0, 0.0, 0.0, 1.0]) 108 | x = Mesh.from_trimesh(fm, smooth=False) 109 | with pytest.raises(ValueError): 110 | x = Mesh.from_trimesh(fm, smooth=True) 111 | 112 | fm.visual.vertex_colors = np.array([1.0, 0.0, 0.0, 0.5]) 113 | x = Mesh.from_trimesh(fm, smooth=False) 114 | x = Mesh.from_trimesh(fm, smooth=True) 115 | assert x.primitives[0].color_0 is not None 116 | assert x.is_transparent 117 | 118 | bm = trimesh.load('tests/data/WaterBottle.glb').dump()[0] 119 | x = Mesh.from_trimesh(bm) 120 | assert x.primitives[0].material.baseColorTexture is not None 121 | assert x.primitives[0].material.emissiveTexture is not None 122 | assert x.primitives[0].material.metallicRoughnessTexture is not None 123 | 124 | # From point cloud 125 | x = Mesh.from_points(fm.vertices) 126 | 127 | # def test_duck(): 128 | # bm = trimesh.load('tests/data/Duck.glb').dump()[0] 129 | # x = Mesh.from_trimesh(bm) 130 | # assert x.primitives[0].material.baseColorTexture is not None 131 | # pixel = x.primitives[0].material.baseColorTexture.source[100, 100] 132 | # yellowish = np.array([1.0, 0.7411765, 0.0, 1.0]) 133 | # assert np.allclose(pixel, yellowish) 134 | -------------------------------------------------------------------------------- /tests/unit/test_nodes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from trimesh import transformations 4 | 5 | from pyrender import (DirectionalLight, PerspectiveCamera, Mesh, Node) 6 | 7 | 8 | def test_nodes(): 9 | 10 | x = Node() 11 | assert x.name is None 12 | assert x.camera is None 13 | assert x.children == [] 14 | assert x.skin is None 15 | assert np.allclose(x.matrix, np.eye(4)) 16 | assert x.mesh is None 17 | assert np.allclose(x.rotation, [0,0,0,1]) 18 | assert np.allclose(x.scale, np.ones(3)) 19 | assert np.allclose(x.translation, np.zeros(3)) 20 | assert x.weights is None 21 | assert x.light is None 22 | 23 | x.name = 'node' 24 | 25 | # Test node light/camera/mesh tests 26 | c = PerspectiveCamera(yfov=2.0) 27 | m = Mesh([]) 28 | d = DirectionalLight() 29 | x.camera = c 30 | assert x.camera == c 31 | with pytest.raises(TypeError): 32 | x.camera = m 33 | x.camera = d 34 | x.camera = None 35 | x.mesh = m 36 | assert x.mesh == m 37 | with pytest.raises(TypeError): 38 | x.mesh = c 39 | x.mesh = d 40 | x.light = d 41 | assert x.light == d 42 | with pytest.raises(TypeError): 43 | x.light = m 44 | x.light = c 45 | 46 | # Test transformations getters/setters/etc... 47 | # Set up test values 48 | x = np.array([1.0, 0.0, 0.0]) 49 | y = np.array([0.0, 1.0, 0.0]) 50 | t = np.array([1.0, 2.0, 3.0]) 51 | s = np.array([0.5, 2.0, 1.0]) 52 | 53 | Mx = transformations.rotation_matrix(np.pi / 2.0, x) 54 | qx = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, x), -1) 55 | Mxt = Mx.copy() 56 | Mxt[:3,3] = t 57 | S = np.eye(4) 58 | S[:3,:3] = np.diag(s) 59 | Mxts = Mxt.dot(S) 60 | 61 | My = transformations.rotation_matrix(np.pi / 2.0, y) 62 | qy = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, y), -1) 63 | Myt = My.copy() 64 | Myt[:3,3] = t 65 | 66 | x = Node(matrix=Mx) 67 | assert np.allclose(x.matrix, Mx) 68 | assert np.allclose(x.rotation, qx) 69 | assert np.allclose(x.translation, np.zeros(3)) 70 | assert np.allclose(x.scale, np.ones(3)) 71 | 72 | x.matrix = My 73 | assert np.allclose(x.matrix, My) 74 | assert np.allclose(x.rotation, qy) 75 | assert np.allclose(x.translation, np.zeros(3)) 76 | assert np.allclose(x.scale, np.ones(3)) 77 | x.translation = t 78 | assert np.allclose(x.matrix, Myt) 79 | assert np.allclose(x.rotation, qy) 80 | x.rotation = qx 81 | assert np.allclose(x.matrix, Mxt) 82 | x.scale = s 83 | assert np.allclose(x.matrix, Mxts) 84 | 85 | x = Node(matrix=Mxt) 86 | assert np.allclose(x.matrix, Mxt) 87 | assert np.allclose(x.rotation, qx) 88 | assert np.allclose(x.translation, t) 89 | assert np.allclose(x.scale, np.ones(3)) 90 | 91 | x = Node(matrix=Mxts) 92 | assert np.allclose(x.matrix, Mxts) 93 | assert np.allclose(x.rotation, qx) 94 | assert np.allclose(x.translation, t) 95 | assert np.allclose(x.scale, s) 96 | 97 | # Individual element getters 98 | x.scale[0] = 0 99 | assert np.allclose(x.scale[0], 0) 100 | 101 | x.translation[0] = 0 102 | assert np.allclose(x.translation[0], 0) 103 | 104 | x.matrix = np.eye(4) 105 | x.matrix[0,0] = 500 106 | assert x.matrix[0,0] == 1.0 107 | 108 | # Failures 109 | with pytest.raises(ValueError): 110 | x.matrix = 5 * np.eye(4) 111 | with pytest.raises(ValueError): 112 | x.matrix = np.eye(5) 113 | with pytest.raises(ValueError): 114 | x.matrix = np.eye(4).dot([5,1,1,1]) 115 | with pytest.raises(ValueError): 116 | x.rotation = np.array([1,2]) 117 | with pytest.raises(ValueError): 118 | x.rotation = np.array([1,2,3]) 119 | with pytest.raises(ValueError): 120 | x.rotation = np.array([1,2,3,4]) 121 | with pytest.raises(ValueError): 122 | x.translation = np.array([1,2,3,4]) 123 | with pytest.raises(ValueError): 124 | x.scale = np.array([1,2,3,4]) 125 | -------------------------------------------------------------------------------- /tests/unit/test_offscreen.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import trimesh 3 | 4 | from pyrender import (OffscreenRenderer, PerspectiveCamera, DirectionalLight, 5 | SpotLight, Mesh, Node, Scene) 6 | 7 | 8 | def test_offscreen_renderer(tmpdir): 9 | 10 | # Fuze trimesh 11 | fuze_trimesh = trimesh.load('examples/models/fuze.obj') 12 | fuze_mesh = Mesh.from_trimesh(fuze_trimesh) 13 | 14 | # Drill trimesh 15 | drill_trimesh = trimesh.load('examples/models/drill.obj') 16 | drill_mesh = Mesh.from_trimesh(drill_trimesh) 17 | drill_pose = np.eye(4) 18 | drill_pose[0,3] = 0.1 19 | drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2]) 20 | 21 | # Wood trimesh 22 | wood_trimesh = trimesh.load('examples/models/wood.obj') 23 | wood_mesh = Mesh.from_trimesh(wood_trimesh) 24 | 25 | # Water bottle trimesh 26 | bottle_gltf = trimesh.load('examples/models/WaterBottle.glb') 27 | bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]] 28 | bottle_mesh = Mesh.from_trimesh(bottle_trimesh) 29 | bottle_pose = np.array([ 30 | [1.0, 0.0, 0.0, 0.1], 31 | [0.0, 0.0, -1.0, -0.16], 32 | [0.0, 1.0, 0.0, 0.13], 33 | [0.0, 0.0, 0.0, 1.0], 34 | ]) 35 | 36 | boxv_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3)) 37 | boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape)) 38 | boxv_trimesh.visual.vertex_colors = boxv_vertex_colors 39 | boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False) 40 | boxf_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3)) 41 | boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape) 42 | boxf_trimesh.visual.face_colors = boxf_face_colors 43 | # Instanced 44 | poses = np.tile(np.eye(4), (2,1,1)) 45 | poses[0,:3,3] = np.array([-0.1, -0.10, 0.05]) 46 | poses[1,:3,3] = np.array([-0.15, -0.10, 0.05]) 47 | boxf_mesh = Mesh.from_trimesh(boxf_trimesh, poses=poses, smooth=False) 48 | 49 | points = trimesh.creation.icosphere(radius=0.05).vertices 50 | point_colors = np.random.uniform(size=points.shape) 51 | points_mesh = Mesh.from_points(points, colors=point_colors) 52 | 53 | direc_l = DirectionalLight(color=np.ones(3), intensity=1.0) 54 | spot_l = SpotLight(color=np.ones(3), intensity=10.0, 55 | innerConeAngle=np.pi / 16, outerConeAngle=np.pi / 6) 56 | 57 | cam = PerspectiveCamera(yfov=(np.pi / 3.0)) 58 | cam_pose = np.array([ 59 | [0.0, -np.sqrt(2) / 2, np.sqrt(2) / 2, 0.5], 60 | [1.0, 0.0, 0.0, 0.0], 61 | [0.0, np.sqrt(2) / 2, np.sqrt(2) / 2, 0.4], 62 | [0.0, 0.0, 0.0, 1.0] 63 | ]) 64 | 65 | scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02])) 66 | 67 | fuze_node = Node(mesh=fuze_mesh, translation=np.array([ 68 | 0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2]) 69 | ])) 70 | scene.add_node(fuze_node) 71 | boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05])) 72 | scene.add_node(boxv_node) 73 | boxf_node = Node(mesh=boxf_mesh) 74 | scene.add_node(boxf_node) 75 | 76 | _ = scene.add(drill_mesh, pose=drill_pose) 77 | _ = scene.add(bottle_mesh, pose=bottle_pose) 78 | _ = scene.add(wood_mesh) 79 | _ = scene.add(direc_l, pose=cam_pose) 80 | _ = scene.add(spot_l, pose=cam_pose) 81 | _ = scene.add(points_mesh) 82 | 83 | _ = scene.add(cam, pose=cam_pose) 84 | 85 | r = OffscreenRenderer(viewport_width=640, viewport_height=480) 86 | color, depth = r.render(scene) 87 | 88 | assert color.shape == (480, 640, 3) 89 | assert depth.shape == (480, 640) 90 | assert np.max(depth.data) > 0.05 91 | assert np.count_nonzero(depth.data) > (0.2 * depth.size) 92 | r.delete() 93 | -------------------------------------------------------------------------------- /tests/unit/test_scenes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import trimesh 4 | 5 | from pyrender import (Mesh, PerspectiveCamera, DirectionalLight, 6 | SpotLight, PointLight, Scene, Node, OrthographicCamera) 7 | 8 | 9 | def test_scenes(): 10 | 11 | # Basics 12 | s = Scene() 13 | assert np.allclose(s.bg_color, np.ones(4)) 14 | assert np.allclose(s.ambient_light, np.zeros(3)) 15 | assert len(s.nodes) == 0 16 | assert s.name is None 17 | s.name = 'asdf' 18 | s.bg_color = None 19 | s.ambient_light = None 20 | assert np.allclose(s.bg_color, np.ones(4)) 21 | assert np.allclose(s.ambient_light, np.zeros(3)) 22 | 23 | assert s.nodes == set() 24 | assert s.cameras == set() 25 | assert s.lights == set() 26 | assert s.point_lights == set() 27 | assert s.spot_lights == set() 28 | assert s.directional_lights == set() 29 | assert s.meshes == set() 30 | assert s.camera_nodes == set() 31 | assert s.light_nodes == set() 32 | assert s.point_light_nodes == set() 33 | assert s.spot_light_nodes == set() 34 | assert s.directional_light_nodes == set() 35 | assert s.mesh_nodes == set() 36 | assert s.main_camera_node is None 37 | assert np.all(s.bounds == 0) 38 | assert np.all(s.centroid == 0) 39 | assert np.all(s.extents == 0) 40 | assert np.all(s.scale == 0) 41 | 42 | # From trimesh scene 43 | tms = trimesh.load('tests/data/WaterBottle.glb') 44 | s = Scene.from_trimesh_scene(tms) 45 | assert len(s.meshes) == 1 46 | assert len(s.mesh_nodes) == 1 47 | 48 | # Test bg color formatting 49 | s = Scene(bg_color=[0, 1.0, 0]) 50 | assert np.allclose(s.bg_color, np.array([0.0, 1.0, 0.0, 1.0])) 51 | 52 | # Test constructor for nodes 53 | n1 = Node() 54 | n2 = Node() 55 | n3 = Node() 56 | nodes = [n1, n2, n3] 57 | s = Scene(nodes=nodes) 58 | n1.children.append(n2) 59 | s = Scene(nodes=nodes) 60 | n3.children.append(n2) 61 | with pytest.raises(ValueError): 62 | s = Scene(nodes=nodes) 63 | n3.children = [] 64 | n2.children.append(n3) 65 | n3.children.append(n2) 66 | with pytest.raises(ValueError): 67 | s = Scene(nodes=nodes) 68 | 69 | # Test node accessors 70 | n1 = Node() 71 | n2 = Node() 72 | n3 = Node() 73 | nodes = [n1, n2] 74 | s = Scene(nodes=nodes) 75 | assert s.has_node(n1) 76 | assert s.has_node(n2) 77 | assert not s.has_node(n3) 78 | 79 | # Test node poses 80 | for n in nodes: 81 | assert np.allclose(s.get_pose(n), np.eye(4)) 82 | with pytest.raises(ValueError): 83 | s.get_pose(n3) 84 | with pytest.raises(ValueError): 85 | s.set_pose(n3, np.eye(4)) 86 | tf = np.eye(4) 87 | tf[:3,3] = np.ones(3) 88 | s.set_pose(n1, tf) 89 | assert np.allclose(s.get_pose(n1), tf) 90 | assert np.allclose(s.get_pose(n2), np.eye(4)) 91 | 92 | nodes = [n1, n2, n3] 93 | tf2 = np.eye(4) 94 | tf2[:3,:3] = np.diag([-1,-1,1]) 95 | n1.children.append(n2) 96 | n1.matrix = tf 97 | n2.matrix = tf2 98 | s = Scene(nodes=nodes) 99 | assert np.allclose(s.get_pose(n1), tf) 100 | assert np.allclose(s.get_pose(n2), tf.dot(tf2)) 101 | assert np.allclose(s.get_pose(n3), np.eye(4)) 102 | 103 | n1 = Node() 104 | n2 = Node() 105 | n3 = Node() 106 | n1.children.append(n2) 107 | s = Scene() 108 | s.add_node(n1) 109 | with pytest.raises(ValueError): 110 | s.add_node(n2) 111 | s.set_pose(n1, tf) 112 | assert np.allclose(s.get_pose(n1), tf) 113 | assert np.allclose(s.get_pose(n2), tf) 114 | s.set_pose(n2, tf2) 115 | assert np.allclose(s.get_pose(n2), tf.dot(tf2)) 116 | 117 | # Test node removal 118 | n1 = Node() 119 | n2 = Node() 120 | n3 = Node() 121 | n1.children.append(n2) 122 | n2.children.append(n3) 123 | s = Scene(nodes=[n1, n2, n3]) 124 | s.remove_node(n2) 125 | assert len(s.nodes) == 1 126 | assert n1 in s.nodes 127 | assert len(n1.children) == 0 128 | assert len(n2.children) == 1 129 | s.add_node(n2, parent_node=n1) 130 | assert len(n1.children) == 1 131 | n1.matrix = tf 132 | n3.matrix = tf2 133 | assert np.allclose(s.get_pose(n3), tf.dot(tf2)) 134 | 135 | # Now test ADD function 136 | s = Scene() 137 | m = Mesh([], name='m') 138 | cp = PerspectiveCamera(yfov=2.0) 139 | co = OrthographicCamera(xmag=1.0, ymag=1.0) 140 | dl = DirectionalLight() 141 | pl = PointLight() 142 | sl = SpotLight() 143 | 144 | n1 = s.add(m, name='mn') 145 | assert n1.mesh == m 146 | assert len(s.nodes) == 1 147 | assert len(s.mesh_nodes) == 1 148 | assert n1 in s.mesh_nodes 149 | assert len(s.meshes) == 1 150 | assert m in s.meshes 151 | assert len(s.get_nodes(node=n2)) == 0 152 | n2 = s.add(m, pose=tf) 153 | assert len(s.nodes) == len(s.mesh_nodes) == 2 154 | assert len(s.meshes) == 1 155 | assert len(s.get_nodes(node=n1)) == 1 156 | assert len(s.get_nodes(node=n1, name='mn')) == 1 157 | assert len(s.get_nodes(name='mn')) == 1 158 | assert len(s.get_nodes(obj=m)) == 2 159 | assert len(s.get_nodes(obj=m, obj_name='m')) == 2 160 | assert len(s.get_nodes(obj=co)) == 0 161 | nsl = s.add(sl, name='sln') 162 | npl = s.add(pl, parent_name='sln') 163 | assert nsl.children[0] == npl 164 | ndl = s.add(dl, parent_node=npl) 165 | assert npl.children[0] == ndl 166 | nco = s.add(co) 167 | ncp = s.add(cp) 168 | 169 | assert len(s.light_nodes) == len(s.lights) == 3 170 | assert len(s.point_light_nodes) == len(s.point_lights) == 1 171 | assert npl in s.point_light_nodes 172 | assert len(s.spot_light_nodes) == len(s.spot_lights) == 1 173 | assert nsl in s.spot_light_nodes 174 | assert len(s.directional_light_nodes) == len(s.directional_lights) == 1 175 | assert ndl in s.directional_light_nodes 176 | assert len(s.cameras) == len(s.camera_nodes) == 2 177 | assert s.main_camera_node == nco 178 | s.main_camera_node = ncp 179 | s.remove_node(ncp) 180 | assert len(s.cameras) == len(s.camera_nodes) == 1 181 | assert s.main_camera_node == nco 182 | s.remove_node(n2) 183 | assert len(s.meshes) == 1 184 | s.remove_node(n1) 185 | assert len(s.meshes) == 0 186 | s.remove_node(nsl) 187 | assert len(s.lights) == 0 188 | s.remove_node(nco) 189 | assert s.main_camera_node is None 190 | 191 | s.add_node(n1) 192 | s.clear() 193 | assert len(s.nodes) == 0 194 | 195 | # Trigger final errors 196 | with pytest.raises(ValueError): 197 | s.main_camera_node = None 198 | with pytest.raises(ValueError): 199 | s.main_camera_node = ncp 200 | with pytest.raises(ValueError): 201 | s.add(m, parent_node=n1) 202 | with pytest.raises(ValueError): 203 | s.add(m, name='asdf') 204 | s.add(m, name='asdf') 205 | s.add(m, parent_name='asdf') 206 | with pytest.raises(ValueError): 207 | s.add(m, parent_name='asfd') 208 | with pytest.raises(TypeError): 209 | s.add(None) 210 | 211 | s.clear() 212 | # Test bounds 213 | m1 = Mesh.from_trimesh(trimesh.creation.box()) 214 | m2 = Mesh.from_trimesh(trimesh.creation.box()) 215 | m3 = Mesh.from_trimesh(trimesh.creation.box()) 216 | n1 = Node(mesh=m1) 217 | n2 = Node(mesh=m2, translation=[1.0, 0.0, 0.0]) 218 | n3 = Node(mesh=m3, translation=[0.5, 0.0, 1.0]) 219 | s.add_node(n1) 220 | s.add_node(n2) 221 | s.add_node(n3) 222 | assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [1.5, 0.5, 1.5]]) 223 | s.clear() 224 | s.add_node(n1) 225 | s.add_node(n2, parent_node=n1) 226 | s.add_node(n3, parent_node=n2) 227 | assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.0, 0.5, 1.5]]) 228 | tf = np.eye(4) 229 | tf[:3,3] = np.ones(3) 230 | s.set_pose(n3, tf) 231 | assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.5, 1.5, 1.5]]) 232 | s.remove_node(n2) 233 | assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]) 234 | s.clear() 235 | assert np.allclose(s.bounds, 0.0) 236 | --------------------------------------------------------------------------------