├── .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 | [](https://travis-ci.org/mmatl/pyrender)
4 | [](https://pyrender.readthedocs.io/en/latest/?badge=latest)
5 | [](https://coveralls.io/github/mmatl/pyrender?branch=master)
6 | [](https://badge.fury.io/py/pyrender)
7 | [](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 |
26 |
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 |
--------------------------------------------------------------------------------