Generating Human Motion from Textual Descriptions (T2M-GPT)
+ This space uses T2M-GPT models based on Vector Quantised-Variational AutoEncoder (VQ-VAE) and Generative Pre-trained Transformer (GPT) for human motion generation from textural descriptions🤗
+
+ ''')
+ with gr.Row():
+ with gr.Column():
+ gr.Markdown('''
+
+
+ a man starts off in an up right position with botg arms extended out by his sides, he then brings his arms down to his body and claps his hands together. after this he wals down amd the the left where he proceeds to sit on a seat
+
+
+ ''')
+ with gr.Column():
+ gr.Markdown('''
+
+
+ a person puts their hands together, leans forwards slightly then swings the arms from right to left
+
+
+ ''')
+ with gr.Column():
+ gr.Markdown('''
+
+
+ a man is practicing the waltz with a partner
+
+
+ ''')
+ with gr.Row():
+ with gr.Column():
+ gr.Markdown('''
+ ### Generate human motion by **T2M-GPT**
+ ##### Step 1. Give prompt text describing human motion
+ ##### Step 2. Choice method to render output (Fast: Sketch skeleton; Slow: SMPL mesh, only work with GPU and running time around 2 mins)
+ ##### Step 3. Generate output and enjoy
+ ''')
+ with gr.Column():
+ with gr.Row():
+ text_prompt.render()
+ method = gr.Dropdown(["slow", "fast"], label="Method", value="slow")
+ with gr.Row():
+ generate_btn = gr.Button("Generate")
+ generate_btn.click(predict, [text_prompt, method], [video_out], api_name="generate")
+ print(video_out)
+ with gr.Row():
+ video_out.render()
+ with gr.Row():
+ gr.Markdown('''
+ ### You can test by following examples:
+ ''')
+ examples = gr.Examples(examples=
+ [ "a person jogs in place, slowly at first, then increases speed. they then back up and squat down.",
+ "a man steps forward and does a handstand",
+ "a man rises from the ground, walks in a circle and sits back down on the ground"],
+ label="Examples", inputs=[text_prompt])
+
+demo.launch(debug=True)
diff --git a/pyrender/.coveragerc b/pyrender/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..ee31cded3509cbd991a33dd27e2525b93a1a6558
--- /dev/null
+++ b/pyrender/.coveragerc
@@ -0,0 +1,5 @@
+[report]
+exclude_lines =
+ def __repr__
+ def __str__
+ @abc.abstractmethod
diff --git a/pyrender/.flake8 b/pyrender/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..fec4bcfc3ba774b53a866d839ea15bae6ebdb4a6
--- /dev/null
+++ b/pyrender/.flake8
@@ -0,0 +1,8 @@
+[flake8]
+ignore = E231,W504,F405,F403
+max-line-length = 79
+select = B,C,E,F,W,T4,B9
+exclude =
+ docs/source/conf.py,
+ __pycache__,
+ examples/*
diff --git a/pyrender/.gitignore b/pyrender/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ae59dec631f71a23d4255aaf9c0274a699f4ba25
--- /dev/null
+++ b/pyrender/.gitignore
@@ -0,0 +1,106 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+docs/**/generated/**
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/pyrender/.pre-commit-config.yaml b/pyrender/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1817eb39bf409aff80c7d2cc79a3bc3856c70dbd
--- /dev/null
+++ b/pyrender/.pre-commit-config.yaml
@@ -0,0 +1,6 @@
+repos:
+- repo: https://gitlab.com/pycqa/flake8
+ rev: 3.7.1
+ hooks:
+ - id: flake8
+ exclude: ^setup.py
diff --git a/pyrender/.travis.yml b/pyrender/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ad289ae1513eaf8fda74f8d5ab7840be3ef56cb
--- /dev/null
+++ b/pyrender/.travis.yml
@@ -0,0 +1,43 @@
+language: python
+sudo: required
+dist: xenial
+
+python:
+- '3.6'
+- '3.7'
+
+before_install:
+ # Pre-install osmesa
+ - sudo apt update
+ - sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb
+ - sudo dpkg -i ./mesa_18.3.3-0.deb || true
+ - sudo apt install -f
+ - git clone https://github.com/mmatl/pyopengl.git
+ - cd pyopengl
+ - pip install .
+ - cd ..
+
+install:
+ - pip install .
+ # - pip install -q pytest pytest-cov coveralls
+ - pip install pytest pytest-cov coveralls
+ - pip install ./pyopengl
+
+script:
+ - PYOPENGL_PLATFORM=osmesa pytest --cov=pyrender tests
+
+after_success:
+- coveralls || true
+
+deploy:
+ provider: pypi
+ skip_existing: true
+ user: mmatl
+ on:
+ tags: true
+ branch: master
+ password:
+ secure: O4WWMbTYb2eVYIO4mMOVa6/xyhX7mPvJpd96cxfNvJdyuqho8VapOhzqsI5kahMB1hFjWWr61yR4+Ru5hoDYf3XA6BQVk8eCY9+0H7qRfvoxex71lahKAqfHLMoE1xNdiVTgl+QN9hYjOnopLod24rx8I8eXfpHu/mfCpuTYGyLlNcDP5St3bXpXLPB5wg8Jo1YRRv6W/7fKoXyuWjewk9cJAS0KrEgnDnSkdwm6Pb+80B2tcbgdGvpGaByw5frndwKiMUMgVUownepDU5POQq2p29wwn9lCvRucULxjEgO+63jdbZRj5fNutLarFa2nISfYnrd72LOyDfbJubwAzzAIsy2JbFORyeHvCgloiuE9oE7a9oOQt/1QHBoIV0seiawMWn55Yp70wQ7HlJs4xSGJWCGa5+9883QRNsvj420atkb3cgO8P+PXwiwTi78Dq7Z/xHqccsU0b8poqBneQoA+pUGgNnF6V7Z8e9RsCcse2gAWSZWuOK3ua+9xCgH7I7MeL3afykr2aJ+yFCoYJMFrUjJeodMX2RbL0q+3FzIPZeGW3WdhTEAL9TSKRcJBSQTskaQlZx/OcpobxS7t3d2S68CCLG9uMTqOTYws55WZ1etalA75sRk9K2MR7ZGjZW3jdtvMViISc/t6Rrjea1GE8ZHGJC6/IeLIWA2c7nc=
+ distributions: sdist bdist_wheel
+notifications:
+ email: false
diff --git a/pyrender/LICENSE b/pyrender/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..4276f7d204e4d85104246df637e0e36adbef14a7
--- /dev/null
+++ b/pyrender/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Matthew Matl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pyrender/MANIFEST.in b/pyrender/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..097bcca3b4fccdc39ddd63c10f710ad524898e95
--- /dev/null
+++ b/pyrender/MANIFEST.in
@@ -0,0 +1,5 @@
+# Include the license
+include LICENSE
+include README.rst
+include pyrender/fonts/*
+include pyrender/shaders/*
diff --git a/pyrender/README.md b/pyrender/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ae88ed1c5e78f247e38291ed83cf4c81230bf976
--- /dev/null
+++ b/pyrender/README.md
@@ -0,0 +1,92 @@
+# Pyrender
+
+[](https://travis-ci.org/mmatl/pyrender)
+[](https://pyrender.readthedocs.io/en/latest/?badge=latest)
+[](https://coveralls.io/github/mmatl/pyrender?branch=master)
+[](https://badge.fury.io/py/pyrender)
+[](https://pepy.tech/project/pyrender)
+
+Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based
+rendering and visualization.
+It is designed to meet the [glTF 2.0 specification from Khronos](https://www.khronos.org/gltf/).
+
+Pyrender is lightweight, easy to install, and simple to use.
+It comes packaged with both an intuitive scene viewer and a headache-free
+offscreen renderer with support for GPU-accelerated rendering on headless
+servers, which makes it perfect for machine learning applications.
+
+Extensive documentation, including a quickstart guide, is provided [here](https://pyrender.readthedocs.io/en/latest/).
+
+For a minimal working example of GPU-accelerated offscreen rendering using EGL,
+check out the [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing).
+
+
+
+
+
+
+
+## Installation
+You can install pyrender directly from pip.
+
+```bash
+pip install pyrender
+```
+
+## Features
+
+Despite being lightweight, pyrender has lots of features, including:
+
+* Simple interoperation with the amazing [trimesh](https://github.com/mikedh/trimesh) project,
+which enables out-of-the-box support for dozens of mesh types, including OBJ,
+STL, DAE, OFF, PLY, and GLB.
+* An easy-to-use scene viewer with support for animation, showing face and vertex
+normals, toggling lighting conditions, and saving images and GIFs.
+* An offscreen rendering module that supports OSMesa and EGL backends.
+* Shadow mapping for directional and spot lights.
+* Metallic-roughness materials for physically-based rendering, including several
+types of texture and normal mapping.
+* Transparency.
+* Depth and color image generation.
+
+## Sample Usage
+
+For sample usage, check out the [quickstart
+guide](https://pyrender.readthedocs.io/en/latest/examples/index.html) or one of
+the Google CoLab Notebooks:
+
+* [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing)
+
+## Viewer Keyboard and Mouse Controls
+
+When using the viewer, the basic controls for moving about the scene are as follows:
+
+* To rotate the camera about the center of the scene, hold the left mouse button and drag the cursor.
+* To rotate the camera about its viewing axis, hold `CTRL` left mouse button and drag the cursor.
+* To pan the camera, do one of the following:
+ * Hold `SHIFT`, then hold the left mouse button and drag the cursor.
+ * Hold the middle mouse button and drag the cursor.
+* To zoom the camera in or out, do one of the following:
+ * Scroll the mouse wheel.
+ * Hold the right mouse button and drag the cursor.
+
+The available keyboard commands are as follows:
+
+* `a`: Toggles rotational animation mode.
+* `c`: Toggles backface culling.
+* `f`: Toggles fullscreen mode.
+* `h`: Toggles shadow rendering.
+* `i`: Toggles axis display mode (no axes, world axis, mesh axes, all axes).
+* `l`: Toggles lighting mode (scene lighting, Raymond lighting, or direct lighting).
+* `m`: Toggles face normal visualization.
+* `n`: Toggles vertex normal visualization.
+* `o`: Toggles orthographic camera mode.
+* `q`: Quits the viewer.
+* `r`: Starts recording a GIF, and pressing again stops recording and opens a file dialog.
+* `s`: Opens a file dialog to save the current view as an image.
+* `w`: Toggles wireframe mode (scene default, flip wireframes, all wireframe, or all solid).
+* `z`: Resets the camera to the default view.
+
+As a note, displaying shadows significantly slows down rendering, so if you're
+experiencing low framerates, just kill shadows or reduce the number of lights in
+your scene.
diff --git a/pyrender/docs/Makefile b/pyrender/docs/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..b1064a04362a0c4372fae351f99ed3bd9f82ff92
--- /dev/null
+++ b/pyrender/docs/Makefile
@@ -0,0 +1,23 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+clean:
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+ rm -rf ./source/generated/*
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/pyrender/docs/make.bat b/pyrender/docs/make.bat
new file mode 100644
index 0000000000000000000000000000000000000000..543c6b13b473ff3c586d5d97ae418d267ee795c4
--- /dev/null
+++ b/pyrender/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/pyrender/docs/source/api/index.rst b/pyrender/docs/source/api/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b6e473149d8f132f176e242c93406fdb84ce0b04
--- /dev/null
+++ b/pyrender/docs/source/api/index.rst
@@ -0,0 +1,59 @@
+Pyrender API Documentation
+==========================
+
+Constants
+---------
+.. automodapi:: pyrender.constants
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
+
+Cameras
+-------
+.. automodapi:: pyrender.camera
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
+
+Lighting
+--------
+.. automodapi:: pyrender.light
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
+
+Objects
+-------
+.. automodapi:: pyrender
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
+ :skip: Camera, DirectionalLight, Light, OffscreenRenderer, Node
+ :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags
+ :skip: Renderer, Scene, SpotLight, TextAlign, Viewer, GLTF
+
+Scenes
+------
+.. automodapi:: pyrender
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
+ :skip: Camera, DirectionalLight, Light, OffscreenRenderer
+ :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags
+ :skip: Renderer, SpotLight, TextAlign, Viewer, Sampler, Texture, Material
+ :skip: MetallicRoughnessMaterial, Primitive, Mesh, GLTF
+
+On-Screen Viewer
+----------------
+.. automodapi:: pyrender.viewer
+ :no-inheritance-diagram:
+ :no-inherited-members:
+ :no-main-docstr:
+ :no-heading:
+
+Off-Screen Rendering
+--------------------
+.. automodapi:: pyrender.offscreen
+ :no-inheritance-diagram:
+ :no-main-docstr:
+ :no-heading:
diff --git a/pyrender/docs/source/conf.py b/pyrender/docs/source/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bf194c375e7e789b334a838953adfeaf2eb59b6
--- /dev/null
+++ b/pyrender/docs/source/conf.py
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+#
+# core documentation build configuration file, created by
+# sphinx-quickstart on Sun Oct 16 14:33:48 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+from pyrender import __version__
+from sphinx.domains.python import PythonDomain
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../../'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.githubpages',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.viewcode',
+ 'sphinx_automodapi.automodapi',
+ 'sphinx_automodapi.smart_resolver'
+]
+numpydoc_class_members_toctree = False
+automodapi_toctreedirnm = 'generated'
+automodsumm_inherited_members = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'pyrender'
+copyright = u'2018, Matthew Matl'
+author = u'Matthew Matl'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = __version__
+# The full version, including alpha/beta/rc tags.
+release = __version__
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+import sphinx_rtd_theme
+html_theme = 'sphinx_rtd_theme'
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# " v documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'coredoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'pyrender.tex', u'pyrender Documentation',
+ u'Matthew Matl', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'pyrender', u'pyrender Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'pyrender', u'pyrender Documentation',
+ author, 'pyrender', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+intersphinx_mapping = {
+ 'python' : ('https://docs.python.org/', None),
+ 'pyrender' : ('https://pyrender.readthedocs.io/en/latest/', None),
+}
+
+# Autosummary fix
+autosummary_generate = True
+
+# Try to suppress multiple-definition warnings by always taking the shorter
+# path when two or more paths have the same base module
+
+class MyPythonDomain(PythonDomain):
+
+ def find_obj(self, env, modname, classname, name, type, searchmode=0):
+ """Ensures an object always resolves to the desired module
+ if defined there."""
+ orig_matches = PythonDomain.find_obj(
+ self, env, modname, classname, name, type, searchmode
+ )
+
+ if len(orig_matches) <= 1:
+ return orig_matches
+
+ # If multiple matches, try to take the shortest if all the modules are
+ # the same
+ first_match_name_sp = orig_matches[0][0].split('.')
+ base_name = first_match_name_sp[0]
+ min_len = len(first_match_name_sp)
+ best_match = orig_matches[0]
+
+ for match in orig_matches[1:]:
+ match_name = match[0]
+ match_name_sp = match_name.split('.')
+ match_base = match_name_sp[0]
+
+ # If we have mismatched bases, return them all to trigger warnings
+ if match_base != base_name:
+ return orig_matches
+
+ # Otherwise, check and see if it's shorter
+ if len(match_name_sp) < min_len:
+ min_len = len(match_name_sp)
+ best_match = match
+
+ return (best_match,)
+
+
+def setup(sphinx):
+ """Use MyPythonDomain in place of PythonDomain"""
+ sphinx.override_domain(MyPythonDomain)
+
diff --git a/pyrender/docs/source/examples/cameras.rst b/pyrender/docs/source/examples/cameras.rst
new file mode 100644
index 0000000000000000000000000000000000000000..39186b75b16584d11fd1606b92291c104e0452bd
--- /dev/null
+++ b/pyrender/docs/source/examples/cameras.rst
@@ -0,0 +1,26 @@
+.. _camera_guide:
+
+Creating Cameras
+================
+
+Pyrender supports three camera types -- :class:`.PerspectiveCamera` and
+:class:`.IntrinsicsCamera` types,
+which render scenes as a human would see them, and
+:class:`.OrthographicCamera` types, which preserve distances between points.
+
+Creating cameras is easy -- just specify their basic attributes:
+
+>>> pc = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414)
+>>> oc = pyrender.OrthographicCamera(xmag=1.0, ymag=1.0)
+
+For more information, see the Khronos group's documentation here_:
+
+.. _here: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#projection-matrices
+
+When you add cameras to the scene, make sure that you're using OpenGL camera
+coordinates to specify their pose. See the illustration below for details.
+Basically, the camera z-axis points away from the scene, the x-axis points
+right in image space, and the y-axis points up in image space.
+
+.. image:: /_static/camera_coords.png
+
diff --git a/pyrender/docs/source/examples/index.rst b/pyrender/docs/source/examples/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4be536cd62c1cca112228f4e114e783be77a0ab8
--- /dev/null
+++ b/pyrender/docs/source/examples/index.rst
@@ -0,0 +1,20 @@
+.. _guide:
+
+User Guide
+==========
+
+This section contains guides on how to use Pyrender to quickly visualize
+your 3D data, including a quickstart guide and more detailed descriptions
+of each part of the rendering pipeline.
+
+
+.. toctree::
+ :maxdepth: 2
+
+ quickstart.rst
+ models.rst
+ lighting.rst
+ cameras.rst
+ scenes.rst
+ offscreen.rst
+ viewer.rst
diff --git a/pyrender/docs/source/examples/lighting.rst b/pyrender/docs/source/examples/lighting.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f89bee7d15027a0f52711622b053b49cc6e1b410
--- /dev/null
+++ b/pyrender/docs/source/examples/lighting.rst
@@ -0,0 +1,21 @@
+.. _lighting_guide:
+
+Creating Lights
+===============
+
+Pyrender supports three types of punctual light:
+
+- :class:`.PointLight`: Point-based light sources, such as light bulbs.
+- :class:`.SpotLight`: A conical light source, like a flashlight.
+- :class:`.DirectionalLight`: A general light that does not attenuate with
+ distance.
+
+Creating lights is easy -- just specify their basic attributes:
+
+>>> pl = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0)
+>>> sl = pyrender.SpotLight(color=[1.0, 1.0, 1.0], intensity=2.0,
+... innerConeAngle=0.05, outerConeAngle=0.5)
+>>> dl = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.0)
+
+For more information about how these lighting models are implemented,
+see their class documentation.
diff --git a/pyrender/docs/source/examples/models.rst b/pyrender/docs/source/examples/models.rst
new file mode 100644
index 0000000000000000000000000000000000000000..84e71c4ff41a8d2e0eb2dc48434caedb757ff954
--- /dev/null
+++ b/pyrender/docs/source/examples/models.rst
@@ -0,0 +1,143 @@
+.. _model_guide:
+
+Loading and Configuring Models
+==============================
+The first step to any rendering application is loading your models.
+Pyrender implements the GLTF 2.0 specification, which means that all
+models are composed of a hierarchy of objects.
+
+At the top level, we have a :class:`.Mesh`. The :class:`.Mesh` is
+basically a wrapper of any number of :class:`.Primitive` types,
+which actually represent geometry that can be drawn to the screen.
+
+Primitives are composed of a variety of parameters, including
+vertex positions, vertex normals, color and texture information,
+and triangle indices if smooth rendering is desired.
+They can implement point clouds, triangular meshes, or lines
+depending on how you configure their data and set their
+:attr:`.Primitive.mode` parameter.
+
+Although you can create primitives yourself if you want to,
+it's probably easier to just use the utility functions provided
+in the :class:`.Mesh` class.
+
+Creating Triangular Meshes
+--------------------------
+
+Simple Construction
+~~~~~~~~~~~~~~~~~~~
+Pyrender allows you to create a :class:`.Mesh` containing a
+triangular mesh model directly from a :class:`~trimesh.base.Trimesh` object
+using the :meth:`.Mesh.from_trimesh` static method.
+
+>>> import trimesh
+>>> import pyrender
+>>> import numpy as np
+>>> tm = trimesh.load('examples/models/fuze.obj')
+>>> m = pyrender.Mesh.from_trimesh(tm)
+>>> m.primitives
+[]
+
+You can also create a single :class:`.Mesh` from a list of
+:class:`~trimesh.base.Trimesh` objects:
+
+>>> tms = [trimesh.creation.icosahedron(), trimesh.creation.cylinder()]
+>>> m = pyrender.Mesh.from_trimesh(tms)
+[,
+ ]
+
+Vertex Smoothing
+~~~~~~~~~~~~~~~~
+
+The :meth:`.Mesh.from_trimesh` method has a few additional optional parameters.
+If you want to render the mesh without interpolating face normals, which can
+be useful for meshes that are supposed to be angular (e.g. a cube), you
+can specify ``smooth=False``.
+
+>>> m = pyrender.Mesh.from_trimesh(tm, smooth=False)
+
+Per-Face or Per-Vertex Coloration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you have an untextured trimesh, you can color it in with per-face or
+per-vertex colors:
+
+>>> tm.visual.vertex_colors = np.random.uniform(size=tm.vertices.shape)
+>>> tm.visual.face_colors = np.random.uniform(size=tm.faces.shape)
+>>> m = pyrender.Mesh.from_trimesh(tm)
+
+Instancing
+~~~~~~~~~~
+
+If you want to render many copies of the same mesh at different poses,
+you can statically create a vast array of them in an efficient manner.
+Simply specify the ``poses`` parameter to be a list of ``N`` 4x4 homogenous
+transformation matrics that position the meshes relative to their common
+base frame:
+
+>>> tfs = np.tile(np.eye(4), (3,1,1))
+>>> tfs[1,:3,3] = [0.1, 0.0, 0.0]
+>>> tfs[2,:3,3] = [0.2, 0.0, 0.0]
+>>> tfs
+array([[[1. , 0. , 0. , 0. ],
+ [0. , 1. , 0. , 0. ],
+ [0. , 0. , 1. , 0. ],
+ [0. , 0. , 0. , 1. ]],
+ [[1. , 0. , 0. , 0.1],
+ [0. , 1. , 0. , 0. ],
+ [0. , 0. , 1. , 0. ],
+ [0. , 0. , 0. , 1. ]],
+ [[1. , 0. , 0. , 0.2],
+ [0. , 1. , 0. , 0. ],
+ [0. , 0. , 1. , 0. ],
+ [0. , 0. , 0. , 1. ]]])
+
+>>> m = pyrender.Mesh.from_trimesh(tm, poses=tfs)
+
+Custom Materials
+~~~~~~~~~~~~~~~~
+
+You can also specify a custom material for any triangular mesh you create
+in the ``material`` parameter of :meth:`.Mesh.from_trimesh`.
+The main material supported by Pyrender is the
+:class:`.MetallicRoughnessMaterial`.
+The metallic-roughness model supports rendering highly-realistic objects across
+a wide gamut of materials.
+
+For more information, see the documentation of the
+:class:`.MetallicRoughnessMaterial` constructor or look at the Khronos_
+documentation for more information.
+
+.. _Khronos: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials
+
+Creating Point Clouds
+---------------------
+
+Point Sprites
+~~~~~~~~~~~~~
+Pyrender also allows you to create a :class:`.Mesh` containing a
+point cloud directly from :class:`numpy.ndarray` instances
+using the :meth:`.Mesh.from_points` static method.
+
+Simply provide a list of points and optional per-point colors and normals.
+
+>>> pts = tm.vertices.copy()
+>>> colors = np.random.uniform(size=pts.shape)
+>>> m = pyrender.Mesh.from_points(pts, colors=colors)
+
+Point clouds created in this way will be rendered as square point sprites.
+
+.. image:: /_static/points.png
+
+Point Spheres
+~~~~~~~~~~~~~
+If you have a monochromatic point cloud and would like to render it with
+spheres, you can render it by instancing a spherical trimesh:
+
+>>> sm = trimesh.creation.uv_sphere(radius=0.1)
+>>> sm.visual.vertex_colors = [1.0, 0.0, 0.0]
+>>> tfs = np.tile(np.eye(4), (len(pts), 1, 1))
+>>> tfs[:,:3,3] = pts
+>>> m = pyrender.Mesh.from_trimesh(sm, poses=tfs)
+
+.. image:: /_static/points2.png
diff --git a/pyrender/docs/source/examples/offscreen.rst b/pyrender/docs/source/examples/offscreen.rst
new file mode 100644
index 0000000000000000000000000000000000000000..291532b6e0c0e512df35a97e3c826cc83015aeca
--- /dev/null
+++ b/pyrender/docs/source/examples/offscreen.rst
@@ -0,0 +1,87 @@
+.. _offscreen_guide:
+
+Offscreen Rendering
+===================
+
+.. note::
+ If you're using a headless server, you'll need to use either EGL (for
+ GPU-accelerated rendering) or OSMesa (for CPU-only software rendering).
+ If you're using OSMesa, be sure that you've installed it properly. See
+ :ref:`osmesa` for details.
+
+Choosing a Backend
+------------------
+
+Once you have a scene set up with its geometry, cameras, and lights,
+you can render it using the :class:`.OffscreenRenderer`. Pyrender supports
+three backends for offscreen rendering:
+
+- Pyglet, the same engine that runs the viewer. This requires an active
+ display manager, so you can't run it on a headless server. This is the
+ default option.
+- OSMesa, a software renderer.
+- EGL, which allows for GPU-accelerated rendering without a display manager.
+
+If you want to use OSMesa or EGL, you need to set the ``PYOPENGL_PLATFORM``
+environment variable before importing pyrender or any other OpenGL library.
+You can do this at the command line:
+
+.. code-block:: bash
+
+ PYOPENGL_PLATFORM=osmesa python render.py
+
+or at the top of your Python script:
+
+.. code-block:: bash
+
+ # Top of main python script
+ import os
+ os.environ['PYOPENGL_PLATFORM'] = 'egl'
+
+The handle for EGL is ``egl``, and the handle for OSMesa is ``osmesa``.
+
+Running the Renderer
+--------------------
+
+Once you've set your environment variable appropriately, create your scene and
+then configure the :class:`.OffscreenRenderer` object with a window width,
+a window height, and a size for point-cloud points:
+
+>>> r = pyrender.OffscreenRenderer(viewport_width=640,
+... viewport_height=480,
+... point_size=1.0)
+
+Then, just call the :meth:`.OffscreenRenderer.render` function:
+
+>>> color, depth = r.render(scene)
+
+.. image:: /_static/scene.png
+
+This will return a ``(w,h,3)`` channel floating-point color image and
+a ``(w,h)`` floating-point depth image rendered from the scene's main camera.
+
+You can customize the rendering process by using flag options from
+:class:`.RenderFlags` and bitwise or-ing them together. For example,
+the following code renders a color image with an alpha channel
+and enables shadow mapping for all directional lights:
+
+>>> flags = RenderFlags.RGBA | RenderFlags.SHADOWS_DIRECTIONAL
+>>> color, depth = r.render(scene, flags=flags)
+
+Once you're done with the offscreen renderer, you need to close it before you
+can run a different renderer or open the viewer for the same scene:
+
+>>> r.delete()
+
+Google CoLab Examples
+---------------------
+
+For a minimal working example of offscreen rendering using OSMesa,
+see the `OSMesa Google CoLab notebook`_.
+
+.. _OSMesa Google CoLab notebook: https://colab.research.google.com/drive/1Z71mHIc-Sqval92nK290vAsHZRUkCjUx
+
+For a minimal working example of offscreen rendering using EGL,
+see the `EGL Google CoLab notebook`_.
+
+.. _EGL Google CoLab notebook: https://colab.research.google.com/drive/1rTLHk0qxh4dn8KNe-mCnN8HAWdd2_BEh
diff --git a/pyrender/docs/source/examples/quickstart.rst b/pyrender/docs/source/examples/quickstart.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ac556419e5206c2ccd4bc985feb1a8c7347310af
--- /dev/null
+++ b/pyrender/docs/source/examples/quickstart.rst
@@ -0,0 +1,71 @@
+.. _quickstart_guide:
+
+Quickstart
+==========
+
+
+Minimal Example for 3D Viewer
+-----------------------------
+Here is a minimal example of loading and viewing a triangular mesh model
+in pyrender.
+
+>>> import trimesh
+>>> import pyrender
+>>> fuze_trimesh = trimesh.load('examples/models/fuze.obj')
+>>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
+>>> scene = pyrender.Scene()
+>>> scene.add(mesh)
+>>> pyrender.Viewer(scene, use_raymond_lighting=True)
+
+.. image:: /_static/fuze.png
+
+
+Minimal Example for Offscreen Rendering
+---------------------------------------
+.. note::
+ If you're using a headless server, make sure that you followed the guide
+ for installing OSMesa. See :ref:`osmesa`.
+
+Here is a minimal example of rendering a mesh model offscreen in pyrender.
+The only additional necessities are that you need to add lighting and a camera.
+
+>>> import numpy as np
+>>> import trimesh
+>>> import pyrender
+>>> import matplotlib.pyplot as plt
+
+>>> fuze_trimesh = trimesh.load('examples/models/fuze.obj')
+>>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
+>>> scene = pyrender.Scene()
+>>> scene.add(mesh)
+>>> camera = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.0)
+>>> s = np.sqrt(2)/2
+>>> camera_pose = np.array([
+... [0.0, -s, s, 0.3],
+... [1.0, 0.0, 0.0, 0.0],
+... [0.0, s, s, 0.35],
+... [0.0, 0.0, 0.0, 1.0],
+... ])
+>>> scene.add(camera, pose=camera_pose)
+>>> light = pyrender.SpotLight(color=np.ones(3), intensity=3.0,
+... innerConeAngle=np.pi/16.0,
+... outerConeAngle=np.pi/6.0)
+>>> scene.add(light, pose=camera_pose)
+>>> r = pyrender.OffscreenRenderer(400, 400)
+>>> color, depth = r.render(scene)
+>>> plt.figure()
+>>> plt.subplot(1,2,1)
+>>> plt.axis('off')
+>>> plt.imshow(color)
+>>> plt.subplot(1,2,2)
+>>> plt.axis('off')
+>>> plt.imshow(depth, cmap=plt.cm.gray_r)
+>>> plt.show()
+
+.. image:: /_static/minexcolor.png
+ :width: 45%
+ :align: left
+.. image:: /_static/minexdepth.png
+ :width: 45%
+ :align: right
+
diff --git a/pyrender/docs/source/examples/scenes.rst b/pyrender/docs/source/examples/scenes.rst
new file mode 100644
index 0000000000000000000000000000000000000000..94c243f8b860b9669ac26105fd2b9906054f4568
--- /dev/null
+++ b/pyrender/docs/source/examples/scenes.rst
@@ -0,0 +1,78 @@
+.. _scene_guide:
+
+Creating Scenes
+===============
+
+Before you render anything, you need to put all of your lights, cameras,
+and meshes into a scene. The :class:`.Scene` object keeps track of the relative
+poses of these primitives by inserting them into :class:`.Node` objects and
+keeping them in a directed acyclic graph.
+
+Adding Objects
+--------------
+
+To create a :class:`.Scene`, simply call the constructor. You can optionally
+specify an ambient light color and a background color:
+
+>>> scene = pyrender.Scene(ambient_light=[0.02, 0.02, 0.02],
+... bg_color=[1.0, 1.0, 1.0])
+
+You can add objects to a scene by first creating a :class:`.Node` object
+and adding the object and its pose to the :class:`.Node`. Poses are specified
+as 4x4 homogenous transformation matrices that are stored in the node's
+:attr:`.Node.matrix` attribute. Note that the :class:`.Node`
+constructor requires you to specify whether you're adding a mesh, light,
+or camera.
+
+>>> mesh = pyrender.Mesh.from_trimesh(tm)
+>>> light = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0)
+>>> cam = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414)
+>>> nm = pyrender.Node(mesh=mesh, matrix=np.eye(4))
+>>> nl = pyrender.Node(light=light, matrix=np.eye(4))
+>>> nc = pyrender.Node(camera=cam, matrix=np.eye(4))
+>>> scene.add_node(nm)
+>>> scene.add_node(nl)
+>>> scene.add_node(nc)
+
+You can also add objects directly to a scene with the :meth:`.Scene.add` function,
+which takes care of creating a :class:`.Node` for you.
+
+>>> scene.add(mesh, pose=np.eye(4))
+>>> scene.add(light, pose=np.eye(4))
+>>> scene.add(cam, pose=np.eye(4))
+
+Nodes can be hierarchical, in which case the node's :attr:`.Node.matrix`
+specifies that node's pose relative to its parent frame. You can add nodes to
+a scene hierarchically by specifying a parent node in your calls to
+:meth:`.Scene.add` or :meth:`.Scene.add_node`:
+
+>>> scene.add_node(nl, parent_node=nc)
+>>> scene.add(cam, parent_node=nm)
+
+If you add multiple cameras to a scene, you can specify which one to render from
+by setting the :attr:`.Scene.main_camera_node` attribute.
+
+Updating Objects
+----------------
+
+You can update the poses of existing nodes with the :meth:`.Scene.set_pose`
+function. Simply call it with a :class:`.Node` that is already in the scene
+and the new pose of that node with respect to its parent as a 4x4 homogenous
+transformation matrix:
+
+>>> scene.set_pose(nl, pose=np.eye(4))
+
+If you want to get the local pose of a node, you can just access its
+:attr:`.Node.matrix` attribute. However, if you want to the get
+the pose of a node *with respect to the world frame*, you can call the
+:meth:`.Scene.get_pose` method.
+
+>>> tf = scene.get_pose(nl)
+
+Removing Objects
+----------------
+
+Finally, you can remove a :class:`.Node` and all of its children from the
+scene with the :meth:`.Scene.remove_node` function:
+
+>>> scene.remove_node(nl)
diff --git a/pyrender/docs/source/examples/viewer.rst b/pyrender/docs/source/examples/viewer.rst
new file mode 100644
index 0000000000000000000000000000000000000000..00a7973b46ec7da33b51b65581af6f25c1b1652f
--- /dev/null
+++ b/pyrender/docs/source/examples/viewer.rst
@@ -0,0 +1,61 @@
+.. _viewer_guide:
+
+Live Scene Viewer
+=================
+
+Standard Usage
+--------------
+In addition to the offscreen renderer, Pyrender comes with a live scene viewer.
+In its standard invocation, calling the :class:`.Viewer`'s constructor will
+immediately pop a viewing window that you can navigate around in.
+
+>>> pyrender.Viewer(scene)
+
+By default, the viewer uses your scene's lighting. If you'd like to start with
+some additional lighting that moves around with the camera, you can specify that
+with:
+
+>>> pyrender.Viewer(scene, use_raymond_lighting=True)
+
+For a full list of the many options that the :class:`.Viewer` supports, check out its
+documentation.
+
+.. image:: /_static/rotation.gif
+
+Running the Viewer in a Separate Thread
+---------------------------------------
+If you'd like to animate your models, you'll want to run the viewer in a
+separate thread so that you can update the scene while the viewer is running.
+To do this, first pop the viewer in a separate thread by calling its constructor
+with the ``run_in_thread`` option set:
+
+>>> v = pyrender.Viewer(scene, run_in_thread=True)
+
+Then, you can manipulate the :class:`.Scene` while the viewer is running to
+animate things. However, be careful to acquire the viewer's
+:attr:`.Viewer.render_lock` before editing the scene to prevent data corruption:
+
+>>> i = 0
+>>> while True:
+... pose = np.eye(4)
+... pose[:3,3] = [i, 0, 0]
+... v.render_lock.acquire()
+... scene.set_pose(mesh_node, pose)
+... v.render_lock.release()
+... i += 0.01
+
+.. image:: /_static/scissors.gif
+
+You can wait on the viewer to be closed manually:
+
+>>> while v.is_active:
+... pass
+
+Or you can close it from the main thread forcibly.
+Make sure to still loop and block for the viewer to actually exit before using
+the scene object again.
+
+>>> v.close_external()
+>>> while v.is_active:
+... pass
+
diff --git a/pyrender/docs/source/index.rst b/pyrender/docs/source/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..baf189ede6bb3435cad5b8795e1937ef1a3c2c56
--- /dev/null
+++ b/pyrender/docs/source/index.rst
@@ -0,0 +1,41 @@
+.. core documentation master file, created by
+ sphinx-quickstart on Sun Oct 16 14:33:48 2016.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Pyrender Documentation
+========================
+Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based
+rendering and visualization.
+It is designed to meet the glTF 2.0 specification_ from Khronos
+
+.. _specification: https://www.khronos.org/gltf/
+
+Pyrender is lightweight, easy to install, and simple to use.
+It comes packaged with both an intuitive scene viewer and a headache-free
+offscreen renderer with support for GPU-accelerated rendering on headless
+servers, which makes it perfect for machine learning applications.
+Check out the :ref:`guide` for a full tutorial, or fork me on
+Github_.
+
+.. _Github: https://github.com/mmatl/pyrender
+
+.. image:: _static/rotation.gif
+
+.. image:: _static/damaged_helmet.png
+
+.. toctree::
+ :maxdepth: 2
+
+ install/index.rst
+ examples/index.rst
+ api/index.rst
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/pyrender/docs/source/install/index.rst b/pyrender/docs/source/install/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c785f202d877f8bbaf286c21eddca1925973f75e
--- /dev/null
+++ b/pyrender/docs/source/install/index.rst
@@ -0,0 +1,172 @@
+Installation Guide
+==================
+
+Python Installation
+-------------------
+
+This package is available via ``pip``.
+
+.. code-block:: bash
+
+ pip install pyrender
+
+If you're on MacOS, you'll need
+to pre-install my fork of ``pyglet``, as the version on PyPI hasn't yet included
+my change that enables OpenGL contexts on MacOS.
+
+.. code-block:: bash
+
+ git clone https://github.com/mmatl/pyglet.git
+ cd pyglet
+ pip install .
+
+.. _osmesa:
+
+Getting Pyrender Working with OSMesa
+------------------------------------
+If you want to render scenes offscreen but don't want to have to
+install a display manager or deal with the pains of trying to get
+OpenGL to work over SSH, you have two options.
+
+The first (and preferred) option is using EGL, which enables you to perform
+GPU-accelerated rendering on headless servers.
+However, you'll need EGL 1.5 to get modern OpenGL contexts.
+This comes packaged with NVIDIA's current drivers, but if you are having issues
+getting EGL to work with your hardware, you can try using OSMesa,
+a software-based offscreen renderer that is included with any Mesa
+install.
+
+If you want to use OSMesa with pyrender, you'll have to perform two additional
+installation steps:
+
+- :ref:`installmesa`
+- :ref:`installpyopengl`
+
+Then, read the offscreen rendering tutorial. See :ref:`offscreen_guide`.
+
+.. _installmesa:
+
+Installing OSMesa
+*****************
+
+As a first step, you'll need to rebuild and re-install Mesa with support
+for fast offscreen rendering and OpenGL 3+ contexts.
+I'd recommend installing from source, but you can also try my ``.deb``
+for Ubuntu 16.04 and up.
+
+Installing from a Debian Package
+********************************
+
+If you're running Ubuntu 16.04 or newer, you should be able to install the
+required version of Mesa from my ``.deb`` file.
+
+.. code-block:: bash
+
+ sudo apt update
+ sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb
+ sudo dpkg -i ./mesa_18.3.3-0.deb || true
+ sudo apt install -f
+
+If this doesn't work, try building from source.
+
+Building From Source
+********************
+
+First, install build dependencies via `apt` or your system's package manager.
+
+.. code-block:: bash
+
+ sudo apt-get install llvm-6.0 freeglut3 freeglut3-dev
+
+Then, download the current release of Mesa from here_.
+Unpack the source and go to the source folder:
+
+.. _here: https://archive.mesa3d.org/mesa-18.3.3.tar.gz
+
+.. code-block:: bash
+
+ tar xfv mesa-18.3.3.tar.gz
+ cd mesa-18.3.3
+
+Replace ``PREFIX`` with the path you want to install Mesa at.
+If you're not worried about overwriting your default Mesa install,
+a good place is at ``/usr/local``.
+
+Now, configure the installation by running the following command:
+
+.. code-block:: bash
+
+ ./configure --prefix=PREFIX \
+ --enable-opengl --disable-gles1 --disable-gles2 \
+ --disable-va --disable-xvmc --disable-vdpau \
+ --enable-shared-glapi \
+ --disable-texture-float \
+ --enable-gallium-llvm --enable-llvm-shared-libs \
+ --with-gallium-drivers=swrast,swr \
+ --disable-dri --with-dri-drivers= \
+ --disable-egl --with-egl-platforms= --disable-gbm \
+ --disable-glx \
+ --disable-osmesa --enable-gallium-osmesa \
+ ac_cv_path_LLVM_CONFIG=llvm-config-6.0
+
+Finally, build and install Mesa.
+
+.. code-block:: bash
+
+ make -j8
+ make install
+
+Finally, if you didn't install Mesa in the system path,
+add the following lines to your ``~/.bashrc`` file after
+changing ``MESA_HOME`` to your mesa installation path (i.e. what you used as
+``PREFIX`` during the configure command).
+
+.. code-block:: bash
+
+ MESA_HOME=/path/to/your/mesa/installation
+ export LIBRARY_PATH=$LIBRARY_PATH:$MESA_HOME/lib
+ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MESA_HOME/lib
+ export C_INCLUDE_PATH=$C_INCLUDE_PATH:$MESA_HOME/include/
+ export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:$MESA_HOME/include/
+
+.. _installpyopengl:
+
+Installing a Compatible Fork of PyOpenGL
+****************************************
+
+Next, install and use my fork of ``PyOpenGL``.
+This fork enables getting modern OpenGL contexts with OSMesa.
+My patch has been included in ``PyOpenGL``, but it has not yet been released
+on PyPI.
+
+.. code-block:: bash
+
+ git clone https://github.com/mmatl/pyopengl.git
+ pip install ./pyopengl
+
+
+Building Documentation
+----------------------
+
+The online documentation for ``pyrender`` is automatically built by Read The Docs.
+Building ``pyrender``'s documentation locally requires a few extra dependencies --
+specifically, `sphinx`_ and a few plugins.
+
+.. _sphinx: http://www.sphinx-doc.org/en/master/
+
+To install the dependencies required, simply change directories into the `pyrender` source and run
+
+.. code-block:: bash
+
+ $ pip install .[docs]
+
+Then, go to the ``docs`` directory and run ``make`` with the appropriate target.
+For example,
+
+.. code-block:: bash
+
+ $ cd docs/
+ $ make html
+
+will generate a set of web pages. Any documentation files
+generated in this manner can be found in ``docs/build``.
diff --git a/pyrender/examples/duck.py b/pyrender/examples/duck.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a94bad5bfb30493f7364f2e52cbb4badbccb2c7
--- /dev/null
+++ b/pyrender/examples/duck.py
@@ -0,0 +1,13 @@
+from pyrender import Mesh, Scene, Viewer
+from io import BytesIO
+import numpy as np
+import trimesh
+import requests
+
+duck_source = "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb"
+
+duck = trimesh.load(BytesIO(requests.get(duck_source).content), file_type='glb')
+duckmesh = Mesh.from_trimesh(list(duck.geometry.values())[0])
+scene = Scene(ambient_light=np.array([1.0, 1.0, 1.0, 1.0]))
+scene.add(duckmesh)
+Viewer(scene)
diff --git a/pyrender/examples/example.py b/pyrender/examples/example.py
new file mode 100644
index 0000000000000000000000000000000000000000..599a4850a5899cdeb1a76db1c5cf1c91c263cd41
--- /dev/null
+++ b/pyrender/examples/example.py
@@ -0,0 +1,157 @@
+"""Examples of using pyrender for viewing and offscreen rendering.
+"""
+import pyglet
+pyglet.options['shadow_window'] = False
+import os
+import numpy as np
+import trimesh
+
+from pyrender import PerspectiveCamera,\
+ DirectionalLight, SpotLight, PointLight,\
+ MetallicRoughnessMaterial,\
+ Primitive, Mesh, Node, Scene,\
+ Viewer, OffscreenRenderer, RenderFlags
+
+#==============================================================================
+# Mesh creation
+#==============================================================================
+
+#------------------------------------------------------------------------------
+# Creating textured meshes from trimeshes
+#------------------------------------------------------------------------------
+
+# Fuze trimesh
+fuze_trimesh = trimesh.load('./models/fuze.obj')
+fuze_mesh = Mesh.from_trimesh(fuze_trimesh)
+
+# Drill trimesh
+drill_trimesh = trimesh.load('./models/drill.obj')
+drill_mesh = Mesh.from_trimesh(drill_trimesh)
+drill_pose = np.eye(4)
+drill_pose[0,3] = 0.1
+drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2])
+
+# Wood trimesh
+wood_trimesh = trimesh.load('./models/wood.obj')
+wood_mesh = Mesh.from_trimesh(wood_trimesh)
+
+# Water bottle trimesh
+bottle_gltf = trimesh.load('./models/WaterBottle.glb')
+bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]]
+bottle_mesh = Mesh.from_trimesh(bottle_trimesh)
+bottle_pose = np.array([
+ [1.0, 0.0, 0.0, 0.1],
+ [0.0, 0.0, -1.0, -0.16],
+ [0.0, 1.0, 0.0, 0.13],
+ [0.0, 0.0, 0.0, 1.0],
+])
+
+#------------------------------------------------------------------------------
+# Creating meshes with per-vertex colors
+#------------------------------------------------------------------------------
+boxv_trimesh = trimesh.creation.box(extents=0.1*np.ones(3))
+boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape))
+boxv_trimesh.visual.vertex_colors = boxv_vertex_colors
+boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False)
+
+#------------------------------------------------------------------------------
+# Creating meshes with per-face colors
+#------------------------------------------------------------------------------
+boxf_trimesh = trimesh.creation.box(extents=0.1*np.ones(3))
+boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape)
+boxf_trimesh.visual.face_colors = boxf_face_colors
+boxf_mesh = Mesh.from_trimesh(boxf_trimesh, smooth=False)
+
+#------------------------------------------------------------------------------
+# Creating meshes from point clouds
+#------------------------------------------------------------------------------
+points = trimesh.creation.icosphere(radius=0.05).vertices
+point_colors = np.random.uniform(size=points.shape)
+points_mesh = Mesh.from_points(points, colors=point_colors)
+
+#==============================================================================
+# Light creation
+#==============================================================================
+
+direc_l = DirectionalLight(color=np.ones(3), intensity=1.0)
+spot_l = SpotLight(color=np.ones(3), intensity=10.0,
+ innerConeAngle=np.pi/16, outerConeAngle=np.pi/6)
+point_l = PointLight(color=np.ones(3), intensity=10.0)
+
+#==============================================================================
+# Camera creation
+#==============================================================================
+
+cam = PerspectiveCamera(yfov=(np.pi / 3.0))
+cam_pose = np.array([
+ [0.0, -np.sqrt(2)/2, np.sqrt(2)/2, 0.5],
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, np.sqrt(2)/2, np.sqrt(2)/2, 0.4],
+ [0.0, 0.0, 0.0, 1.0]
+])
+
+#==============================================================================
+# Scene creation
+#==============================================================================
+
+scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02, 1.0]))
+
+#==============================================================================
+# Adding objects to the scene
+#==============================================================================
+
+#------------------------------------------------------------------------------
+# By manually creating nodes
+#------------------------------------------------------------------------------
+fuze_node = Node(mesh=fuze_mesh, translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2])]))
+scene.add_node(fuze_node)
+boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05]))
+scene.add_node(boxv_node)
+boxf_node = Node(mesh=boxf_mesh, translation=np.array([-0.1, -0.10, 0.05]))
+scene.add_node(boxf_node)
+
+#------------------------------------------------------------------------------
+# By using the add() utility function
+#------------------------------------------------------------------------------
+drill_node = scene.add(drill_mesh, pose=drill_pose)
+bottle_node = scene.add(bottle_mesh, pose=bottle_pose)
+wood_node = scene.add(wood_mesh)
+direc_l_node = scene.add(direc_l, pose=cam_pose)
+spot_l_node = scene.add(spot_l, pose=cam_pose)
+
+#==============================================================================
+# Using the viewer with a default camera
+#==============================================================================
+
+v = Viewer(scene, shadows=True)
+
+#==============================================================================
+# Using the viewer with a pre-specified camera
+#==============================================================================
+cam_node = scene.add(cam, pose=cam_pose)
+v = Viewer(scene, central_node=drill_node)
+
+#==============================================================================
+# Rendering offscreen from that camera
+#==============================================================================
+
+r = OffscreenRenderer(viewport_width=640*2, viewport_height=480*2)
+color, depth = r.render(scene)
+
+import matplotlib.pyplot as plt
+plt.figure()
+plt.imshow(color)
+plt.show()
+
+#==============================================================================
+# Segmask rendering
+#==============================================================================
+
+nm = {node: 20*(i + 1) for i, node in enumerate(scene.mesh_nodes)}
+seg = r.render(scene, RenderFlags.SEG, nm)[0]
+plt.figure()
+plt.imshow(seg)
+plt.show()
+
+r.delete()
+
diff --git a/pyrender/pyrender/__init__.py b/pyrender/pyrender/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee3709846823b7c4b71b22da0e24d63d805528a8
--- /dev/null
+++ b/pyrender/pyrender/__init__.py
@@ -0,0 +1,24 @@
+from .camera import (Camera, PerspectiveCamera, OrthographicCamera,
+ IntrinsicsCamera)
+from .light import Light, PointLight, DirectionalLight, SpotLight
+from .sampler import Sampler
+from .texture import Texture
+from .material import Material, MetallicRoughnessMaterial
+from .primitive import Primitive
+from .mesh import Mesh
+from .node import Node
+from .scene import Scene
+from .renderer import Renderer
+from .viewer import Viewer
+from .offscreen import OffscreenRenderer
+from .version import __version__
+from .constants import RenderFlags, TextAlign, GLTF
+
+__all__ = [
+ 'Camera', 'PerspectiveCamera', 'OrthographicCamera', 'IntrinsicsCamera',
+ 'Light', 'PointLight', 'DirectionalLight', 'SpotLight',
+ 'Sampler', 'Texture', 'Material', 'MetallicRoughnessMaterial',
+ 'Primitive', 'Mesh', 'Node', 'Scene', 'Renderer', 'Viewer',
+ 'OffscreenRenderer', '__version__', 'RenderFlags', 'TextAlign',
+ 'GLTF'
+]
diff --git a/pyrender/pyrender/camera.py b/pyrender/pyrender/camera.py
new file mode 100644
index 0000000000000000000000000000000000000000..e019358039033c3a372c990ebad3151258c3651d
--- /dev/null
+++ b/pyrender/pyrender/camera.py
@@ -0,0 +1,437 @@
+"""Virtual cameras compliant with the glTF 2.0 specification as described at
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-camera
+
+Author: Matthew Matl
+"""
+import abc
+import numpy as np
+import six
+import sys
+
+from .constants import DEFAULT_Z_NEAR, DEFAULT_Z_FAR
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Camera(object):
+ """Abstract base class for all cameras.
+
+ Note
+ ----
+ Camera poses are specified in the OpenGL format,
+ where the z axis points away from the view direction and the
+ x and y axes point to the right and up in the image plane, respectively.
+
+ Parameters
+ ----------
+ znear : float
+ The floating-point distance to the near clipping plane.
+ zfar : float
+ The floating-point distance to the far clipping plane.
+ ``zfar`` must be greater than ``znear``.
+ name : str, optional
+ The user-defined name of this object.
+ """
+
+ def __init__(self,
+ znear=DEFAULT_Z_NEAR,
+ zfar=DEFAULT_Z_FAR,
+ name=None):
+ self.name = name
+ self.znear = znear
+ self.zfar = zfar
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def znear(self):
+ """float : The distance to the near clipping plane.
+ """
+ return self._znear
+
+ @znear.setter
+ def znear(self, value):
+ value = float(value)
+ if value < 0:
+ raise ValueError('z-near must be >= 0.0')
+ self._znear = value
+
+ @property
+ def zfar(self):
+ """float : The distance to the far clipping plane.
+ """
+ return self._zfar
+
+ @zfar.setter
+ def zfar(self, value):
+ value = float(value)
+ if value <= 0 or value <= self.znear:
+ raise ValueError('zfar must be >0 and >znear')
+ self._zfar = value
+
+ @abc.abstractmethod
+ def get_projection_matrix(self, width=None, height=None):
+ """Return the OpenGL projection matrix for this camera.
+
+ Parameters
+ ----------
+ width : int
+ Width of the current viewport, in pixels.
+ height : int
+ Height of the current viewport, in pixels.
+ """
+ pass
+
+
+class PerspectiveCamera(Camera):
+
+ """A perspective camera for perspective projection.
+
+ Parameters
+ ----------
+ yfov : float
+ The floating-point vertical field of view in radians.
+ znear : float
+ The floating-point distance to the near clipping plane.
+ If not specified, defaults to 0.05.
+ zfar : float, optional
+ The floating-point distance to the far clipping plane.
+ ``zfar`` must be greater than ``znear``.
+ If None, the camera uses an infinite projection matrix.
+ aspectRatio : float, optional
+ The floating-point aspect ratio of the field of view.
+ If not specified, the camera uses the viewport's aspect ratio.
+ name : str, optional
+ The user-defined name of this object.
+ """
+
+ def __init__(self,
+ yfov,
+ znear=DEFAULT_Z_NEAR,
+ zfar=None,
+ aspectRatio=None,
+ name=None):
+ super(PerspectiveCamera, self).__init__(
+ znear=znear,
+ zfar=zfar,
+ name=name,
+ )
+
+ self.yfov = yfov
+ self.aspectRatio = aspectRatio
+
+ @property
+ def yfov(self):
+ """float : The vertical field of view in radians.
+ """
+ return self._yfov
+
+ @yfov.setter
+ def yfov(self, value):
+ value = float(value)
+ if value <= 0.0:
+ raise ValueError('Field of view must be positive')
+ self._yfov = value
+
+ @property
+ def zfar(self):
+ """float : The distance to the far clipping plane.
+ """
+ return self._zfar
+
+ @zfar.setter
+ def zfar(self, value):
+ if value is not None:
+ value = float(value)
+ if value <= 0 or value <= self.znear:
+ raise ValueError('zfar must be >0 and >znear')
+ self._zfar = value
+
+ @property
+ def aspectRatio(self):
+ """float : The ratio of the width to the height of the field of view.
+ """
+ return self._aspectRatio
+
+ @aspectRatio.setter
+ def aspectRatio(self, value):
+ if value is not None:
+ value = float(value)
+ if value <= 0.0:
+ raise ValueError('Aspect ratio must be positive')
+ self._aspectRatio = value
+
+ def get_projection_matrix(self, width=None, height=None):
+ """Return the OpenGL projection matrix for this camera.
+
+ Parameters
+ ----------
+ width : int
+ Width of the current viewport, in pixels.
+ height : int
+ Height of the current viewport, in pixels.
+ """
+ aspect_ratio = self.aspectRatio
+ if aspect_ratio is None:
+ if width is None or height is None:
+ raise ValueError('Aspect ratio of camera must be defined')
+ aspect_ratio = float(width) / float(height)
+
+ a = aspect_ratio
+ t = np.tan(self.yfov / 2.0)
+ n = self.znear
+ f = self.zfar
+
+ P = np.zeros((4,4))
+ P[0][0] = 1.0 / (a * t)
+ P[1][1] = 1.0 / t
+ P[3][2] = -1.0
+
+ if f is None:
+ P[2][2] = -1.0
+ P[2][3] = -2.0 * n
+ else:
+ P[2][2] = (f + n) / (n - f)
+ P[2][3] = (2 * f * n) / (n - f)
+
+ return P
+
+
+class OrthographicCamera(Camera):
+ """An orthographic camera for orthographic projection.
+
+ Parameters
+ ----------
+ xmag : float
+ The floating-point horizontal magnification of the view.
+ ymag : float
+ The floating-point vertical magnification of the view.
+ znear : float
+ The floating-point distance to the near clipping plane.
+ If not specified, defaults to 0.05.
+ zfar : float
+ The floating-point distance to the far clipping plane.
+ ``zfar`` must be greater than ``znear``.
+ If not specified, defaults to 100.0.
+ name : str, optional
+ The user-defined name of this object.
+ """
+
+ def __init__(self,
+ xmag,
+ ymag,
+ znear=DEFAULT_Z_NEAR,
+ zfar=DEFAULT_Z_FAR,
+ name=None):
+ super(OrthographicCamera, self).__init__(
+ znear=znear,
+ zfar=zfar,
+ name=name,
+ )
+
+ self.xmag = xmag
+ self.ymag = ymag
+
+ @property
+ def xmag(self):
+ """float : The horizontal magnification of the view.
+ """
+ return self._xmag
+
+ @xmag.setter
+ def xmag(self, value):
+ value = float(value)
+ if value <= 0.0:
+ raise ValueError('X magnification must be positive')
+ self._xmag = value
+
+ @property
+ def ymag(self):
+ """float : The vertical magnification of the view.
+ """
+ return self._ymag
+
+ @ymag.setter
+ def ymag(self, value):
+ value = float(value)
+ if value <= 0.0:
+ raise ValueError('Y magnification must be positive')
+ self._ymag = value
+
+ @property
+ def znear(self):
+ """float : The distance to the near clipping plane.
+ """
+ return self._znear
+
+ @znear.setter
+ def znear(self, value):
+ value = float(value)
+ if value <= 0:
+ raise ValueError('z-near must be > 0.0')
+ self._znear = value
+
+ def get_projection_matrix(self, width=None, height=None):
+ """Return the OpenGL projection matrix for this camera.
+
+ Parameters
+ ----------
+ width : int
+ Width of the current viewport, in pixels.
+ Unused in this function.
+ height : int
+ Height of the current viewport, in pixels.
+ Unused in this function.
+ """
+ xmag = self.xmag
+ ymag = self.ymag
+
+ # If screen width/height defined, rescale xmag
+ if width is not None and height is not None:
+ xmag = width / height * ymag
+
+ n = self.znear
+ f = self.zfar
+ P = np.zeros((4,4))
+ P[0][0] = 1.0 / xmag
+ P[1][1] = 1.0 / ymag
+ P[2][2] = 2.0 / (n - f)
+ P[2][3] = (f + n) / (n - f)
+ P[3][3] = 1.0
+ return P
+
+
+class IntrinsicsCamera(Camera):
+ """A perspective camera with custom intrinsics.
+
+ Parameters
+ ----------
+ fx : float
+ X-axis focal length in pixels.
+ fy : float
+ Y-axis focal length in pixels.
+ cx : float
+ X-axis optical center in pixels.
+ cy : float
+ Y-axis optical center in pixels.
+ znear : float
+ The floating-point distance to the near clipping plane.
+ If not specified, defaults to 0.05.
+ zfar : float
+ The floating-point distance to the far clipping plane.
+ ``zfar`` must be greater than ``znear``.
+ If not specified, defaults to 100.0.
+ name : str, optional
+ The user-defined name of this object.
+ """
+
+ def __init__(self,
+ fx,
+ fy,
+ cx,
+ cy,
+ znear=DEFAULT_Z_NEAR,
+ zfar=DEFAULT_Z_FAR,
+ name=None):
+ super(IntrinsicsCamera, self).__init__(
+ znear=znear,
+ zfar=zfar,
+ name=name,
+ )
+
+ self.fx = fx
+ self.fy = fy
+ self.cx = cx
+ self.cy = cy
+
+ @property
+ def fx(self):
+ """float : X-axis focal length in meters.
+ """
+ return self._fx
+
+ @fx.setter
+ def fx(self, value):
+ self._fx = float(value)
+
+ @property
+ def fy(self):
+ """float : Y-axis focal length in meters.
+ """
+ return self._fy
+
+ @fy.setter
+ def fy(self, value):
+ self._fy = float(value)
+
+ @property
+ def cx(self):
+ """float : X-axis optical center in pixels.
+ """
+ return self._cx
+
+ @cx.setter
+ def cx(self, value):
+ self._cx = float(value)
+
+ @property
+ def cy(self):
+ """float : Y-axis optical center in pixels.
+ """
+ return self._cy
+
+ @cy.setter
+ def cy(self, value):
+ self._cy = float(value)
+
+ def get_projection_matrix(self, width, height):
+ """Return the OpenGL projection matrix for this camera.
+
+ Parameters
+ ----------
+ width : int
+ Width of the current viewport, in pixels.
+ height : int
+ Height of the current viewport, in pixels.
+ """
+ width = float(width)
+ height = float(height)
+
+ cx, cy = self.cx, self.cy
+ fx, fy = self.fx, self.fy
+ if sys.platform == 'darwin':
+ cx = self.cx * 2.0
+ cy = self.cy * 2.0
+ fx = self.fx * 2.0
+ fy = self.fy * 2.0
+
+ P = np.zeros((4,4))
+ P[0][0] = 2.0 * fx / width
+ P[1][1] = 2.0 * fy / height
+ P[0][2] = 1.0 - 2.0 * cx / width
+ P[1][2] = 2.0 * cy / height - 1.0
+ P[3][2] = -1.0
+
+ n = self.znear
+ f = self.zfar
+ if f is None:
+ P[2][2] = -1.0
+ P[2][3] = -2.0 * n
+ else:
+ P[2][2] = (f + n) / (n - f)
+ P[2][3] = (2 * f * n) / (n - f)
+
+ return P
+
+
+__all__ = ['Camera', 'PerspectiveCamera', 'OrthographicCamera',
+ 'IntrinsicsCamera']
diff --git a/pyrender/pyrender/constants.py b/pyrender/pyrender/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a5785b6fdb21910a174252c5af2f05b40ece4a5
--- /dev/null
+++ b/pyrender/pyrender/constants.py
@@ -0,0 +1,149 @@
+DEFAULT_Z_NEAR = 0.05 # Near clipping plane, in meters
+DEFAULT_Z_FAR = 100.0 # Far clipping plane, in meters
+DEFAULT_SCENE_SCALE = 2.0 # Default scene scale
+MAX_N_LIGHTS = 4 # Maximum number of lights of each type allowed
+TARGET_OPEN_GL_MAJOR = 4 # Target OpenGL Major Version
+TARGET_OPEN_GL_MINOR = 1 # Target OpenGL Minor Version
+MIN_OPEN_GL_MAJOR = 3 # Minimum OpenGL Major Version
+MIN_OPEN_GL_MINOR = 3 # Minimum OpenGL Minor Version
+FLOAT_SZ = 4 # Byte size of GL float32
+UINT_SZ = 4 # Byte size of GL uint32
+SHADOW_TEX_SZ = 2048 # Width and Height of Shadow Textures
+TEXT_PADDING = 20 # Width of padding for rendering text (px)
+
+
+# Flags for render type
+class RenderFlags(object):
+ """Flags for rendering in the scene.
+
+ Combine them with the bitwise or. For example,
+
+ >>> flags = OFFSCREEN | SHADOWS_DIRECTIONAL | VERTEX_NORMALS
+
+ would result in an offscreen render with directional shadows and
+ vertex normals enabled.
+ """
+ NONE = 0
+ """Normal PBR Render."""
+ DEPTH_ONLY = 1
+ """Only render the depth buffer."""
+ OFFSCREEN = 2
+ """Render offscreen and return the depth and (optionally) color buffers."""
+ FLIP_WIREFRAME = 4
+ """Invert the status of wireframe rendering for each mesh."""
+ ALL_WIREFRAME = 8
+ """Render all meshes as wireframes."""
+ ALL_SOLID = 16
+ """Render all meshes as solids."""
+ SHADOWS_DIRECTIONAL = 32
+ """Render shadows for directional lights."""
+ SHADOWS_POINT = 64
+ """Render shadows for point lights."""
+ SHADOWS_SPOT = 128
+ """Render shadows for spot lights."""
+ SHADOWS_ALL = 32 | 64 | 128
+ """Render shadows for all lights."""
+ VERTEX_NORMALS = 256
+ """Render vertex normals."""
+ FACE_NORMALS = 512
+ """Render face normals."""
+ SKIP_CULL_FACES = 1024
+ """Do not cull back faces."""
+ RGBA = 2048
+ """Render the color buffer with the alpha channel enabled."""
+ FLAT = 4096
+ """Render the color buffer flat, with no lighting computations."""
+ SEG = 8192
+
+
+class TextAlign:
+ """Text alignment options for captions.
+
+ Only use one at a time.
+ """
+ CENTER = 0
+ """Center the text by width and height."""
+ CENTER_LEFT = 1
+ """Center the text by height and left-align it."""
+ CENTER_RIGHT = 2
+ """Center the text by height and right-align it."""
+ BOTTOM_LEFT = 3
+ """Put the text in the bottom-left corner."""
+ BOTTOM_RIGHT = 4
+ """Put the text in the bottom-right corner."""
+ BOTTOM_CENTER = 5
+ """Center the text by width and fix it to the bottom."""
+ TOP_LEFT = 6
+ """Put the text in the top-left corner."""
+ TOP_RIGHT = 7
+ """Put the text in the top-right corner."""
+ TOP_CENTER = 8
+ """Center the text by width and fix it to the top."""
+
+
+class GLTF(object):
+ """Options for GL objects."""
+ NEAREST = 9728
+ """Nearest neighbor interpolation."""
+ LINEAR = 9729
+ """Linear interpolation."""
+ NEAREST_MIPMAP_NEAREST = 9984
+ """Nearest mipmapping."""
+ LINEAR_MIPMAP_NEAREST = 9985
+ """Linear mipmapping."""
+ NEAREST_MIPMAP_LINEAR = 9986
+ """Nearest mipmapping."""
+ LINEAR_MIPMAP_LINEAR = 9987
+ """Linear mipmapping."""
+ CLAMP_TO_EDGE = 33071
+ """Clamp to the edge of the texture."""
+ MIRRORED_REPEAT = 33648
+ """Mirror the texture."""
+ REPEAT = 10497
+ """Repeat the texture."""
+ POINTS = 0
+ """Render as points."""
+ LINES = 1
+ """Render as lines."""
+ LINE_LOOP = 2
+ """Render as a line loop."""
+ LINE_STRIP = 3
+ """Render as a line strip."""
+ TRIANGLES = 4
+ """Render as triangles."""
+ TRIANGLE_STRIP = 5
+ """Render as a triangle strip."""
+ TRIANGLE_FAN = 6
+ """Render as a triangle fan."""
+
+
+class BufFlags(object):
+ POSITION = 0
+ NORMAL = 1
+ TANGENT = 2
+ TEXCOORD_0 = 4
+ TEXCOORD_1 = 8
+ COLOR_0 = 16
+ JOINTS_0 = 32
+ WEIGHTS_0 = 64
+
+
+class TexFlags(object):
+ NONE = 0
+ NORMAL = 1
+ OCCLUSION = 2
+ EMISSIVE = 4
+ BASE_COLOR = 8
+ METALLIC_ROUGHNESS = 16
+ DIFFUSE = 32
+ SPECULAR_GLOSSINESS = 64
+
+
+class ProgramFlags:
+ NONE = 0
+ USE_MATERIAL = 1
+ VERTEX_NORMALS = 2
+ FACE_NORMALS = 4
+
+
+__all__ = ['RenderFlags', 'TextAlign', 'GLTF']
diff --git a/pyrender/pyrender/font.py b/pyrender/pyrender/font.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ac530d7b949f50314a0d9cf5d744bedcace0571
--- /dev/null
+++ b/pyrender/pyrender/font.py
@@ -0,0 +1,272 @@
+"""Font texture loader and processor.
+
+Author: Matthew Matl
+"""
+import freetype
+import numpy as np
+import os
+
+import OpenGL
+from OpenGL.GL import *
+
+from .constants import TextAlign, FLOAT_SZ
+from .texture import Texture
+from .sampler import Sampler
+
+
+class FontCache(object):
+ """A cache for fonts.
+ """
+
+ def __init__(self, font_dir=None):
+ self._font_cache = {}
+ self.font_dir = font_dir
+ if self.font_dir is None:
+ base_dir, _ = os.path.split(os.path.realpath(__file__))
+ self.font_dir = os.path.join(base_dir, 'fonts')
+
+ def get_font(self, font_name, font_pt):
+ # If it's a file, load it directly, else, try to load from font dir.
+ if os.path.isfile(font_name):
+ font_filename = font_name
+ _, font_name = os.path.split(font_name)
+ font_name, _ = os.path.split(font_name)
+ else:
+ font_filename = os.path.join(self.font_dir, font_name) + '.ttf'
+
+ cid = OpenGL.contextdata.getContext()
+ key = (cid, font_name, int(font_pt))
+
+ if key not in self._font_cache:
+ self._font_cache[key] = Font(font_filename, font_pt)
+ return self._font_cache[key]
+
+ def clear(self):
+ for key in self._font_cache:
+ self._font_cache[key].delete()
+ self._font_cache = {}
+
+
+class Character(object):
+ """A single character, with its texture and attributes.
+ """
+
+ def __init__(self, texture, size, bearing, advance):
+ self.texture = texture
+ self.size = size
+ self.bearing = bearing
+ self.advance = advance
+
+
+class Font(object):
+ """A font object.
+
+ Parameters
+ ----------
+ font_file : str
+ The file to load the font from.
+ font_pt : int
+ The height of the font in pixels.
+ """
+
+ def __init__(self, font_file, font_pt=40):
+ self.font_file = font_file
+ self.font_pt = int(font_pt)
+ self._face = freetype.Face(font_file)
+ self._face.set_pixel_sizes(0, font_pt)
+ self._character_map = {}
+
+ for i in range(0, 128):
+
+ # Generate texture
+ face = self._face
+ face.load_char(chr(i))
+ buf = face.glyph.bitmap.buffer
+ src = (np.array(buf) / 255.0).astype(np.float32)
+ src = src.reshape((face.glyph.bitmap.rows,
+ face.glyph.bitmap.width))
+ tex = Texture(
+ sampler=Sampler(
+ magFilter=GL_LINEAR,
+ minFilter=GL_LINEAR,
+ wrapS=GL_CLAMP_TO_EDGE,
+ wrapT=GL_CLAMP_TO_EDGE
+ ),
+ source=src,
+ source_channels='R',
+ )
+ character = Character(
+ texture=tex,
+ size=np.array([face.glyph.bitmap.width,
+ face.glyph.bitmap.rows]),
+ bearing=np.array([face.glyph.bitmap_left,
+ face.glyph.bitmap_top]),
+ advance=face.glyph.advance.x
+ )
+ self._character_map[chr(i)] = character
+
+ self._vbo = None
+ self._vao = None
+
+ @property
+ def font_file(self):
+ """str : The file the font was loaded from.
+ """
+ return self._font_file
+
+ @font_file.setter
+ def font_file(self, value):
+ self._font_file = value
+
+ @property
+ def font_pt(self):
+ """int : The height of the font in pixels.
+ """
+ return self._font_pt
+
+ @font_pt.setter
+ def font_pt(self, value):
+ self._font_pt = int(value)
+
+ def _add_to_context(self):
+
+ self._vao = glGenVertexArrays(1)
+ glBindVertexArray(self._vao)
+ self._vbo = glGenBuffers(1)
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
+ glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW)
+ glEnableVertexAttribArray(0)
+ glVertexAttribPointer(
+ 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0)
+ )
+ glBindVertexArray(0)
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
+ for c in self._character_map:
+ ch = self._character_map[c]
+ if not ch.texture._in_context():
+ ch.texture._add_to_context()
+
+ def _remove_from_context(self):
+ for c in self._character_map:
+ ch = self._character_map[c]
+ ch.texture.delete()
+ if self._vao is not None:
+ glDeleteVertexArrays(1, [self._vao])
+ glDeleteBuffers(1, [self._vbo])
+ self._vao = None
+ self._vbo = None
+
+ def _in_context(self):
+ return self._vao is not None
+
+ def _bind(self):
+ glBindVertexArray(self._vao)
+
+ def _unbind(self):
+ glBindVertexArray(0)
+
+ def delete(self):
+ self._unbind()
+ self._remove_from_context()
+
+ def render_string(self, text, x, y, scale=1.0,
+ align=TextAlign.BOTTOM_LEFT):
+ """Render a string to the current view buffer.
+
+ Note
+ ----
+ Assumes correct shader program already bound w/ uniforms set.
+
+ Parameters
+ ----------
+ text : str
+ The text to render.
+ x : int
+ Horizontal pixel location of text.
+ y : int
+ Vertical pixel location of text.
+ scale : int
+ Scaling factor for text.
+ align : int
+ One of the TextAlign options which specifies where the ``x``
+ and ``y`` parameters lie on the text. For example,
+ :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate
+ the position of the bottom-left corner of the textbox.
+ """
+ glActiveTexture(GL_TEXTURE0)
+ glEnable(GL_BLEND)
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ glDisable(GL_DEPTH_TEST)
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
+ self._bind()
+
+ # Determine width and height of text relative to x, y
+ width = 0.0
+ height = 0.0
+ for c in text:
+ ch = self._character_map[c]
+ height = max(height, ch.bearing[1] * scale)
+ width += (ch.advance >> 6) * scale
+
+ # Determine offsets based on alignments
+ xoff = 0
+ yoff = 0
+ if align == TextAlign.BOTTOM_RIGHT:
+ xoff = -width
+ elif align == TextAlign.BOTTOM_CENTER:
+ xoff = -width / 2.0
+ elif align == TextAlign.TOP_LEFT:
+ yoff = -height
+ elif align == TextAlign.TOP_RIGHT:
+ yoff = -height
+ xoff = -width
+ elif align == TextAlign.TOP_CENTER:
+ yoff = -height
+ xoff = -width / 2.0
+ elif align == TextAlign.CENTER:
+ xoff = -width / 2.0
+ yoff = -height / 2.0
+ elif align == TextAlign.CENTER_LEFT:
+ yoff = -height / 2.0
+ elif align == TextAlign.CENTER_RIGHT:
+ xoff = -width
+ yoff = -height / 2.0
+
+ x += xoff
+ y += yoff
+
+ ch = None
+ for c in text:
+ ch = self._character_map[c]
+ xpos = x + ch.bearing[0] * scale
+ ypos = y - (ch.size[1] - ch.bearing[1]) * scale
+ w = ch.size[0] * scale
+ h = ch.size[1] * scale
+
+ vertices = np.array([
+ [xpos, ypos, 0.0, 0.0],
+ [xpos + w, ypos, 1.0, 0.0],
+ [xpos + w, ypos + h, 1.0, 1.0],
+ [xpos + w, ypos + h, 1.0, 1.0],
+ [xpos, ypos + h, 0.0, 1.0],
+ [xpos, ypos, 0.0, 0.0],
+ ], dtype=np.float32)
+
+ ch.texture._bind()
+
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
+ glBufferData(
+ GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW
+ )
+ # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken
+ # glBufferSubData(
+ # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ,
+ # np.ascontiguousarray(vertices.flatten)
+ # )
+ glDrawArrays(GL_TRIANGLES, 0, 6)
+ x += (ch.advance >> 6) * scale
+
+ self._unbind()
+ if ch:
+ ch.texture._unbind()
diff --git a/pyrender/pyrender/fonts/OpenSans-Bold.ttf b/pyrender/pyrender/fonts/OpenSans-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..fd79d43bea0293ac1b20e8aca1142627983d2c07
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-Bold.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-BoldItalic.ttf b/pyrender/pyrender/fonts/OpenSans-BoldItalic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..9bc800958a421d937fc392e00beaef4eea76dc71
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-BoldItalic.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-ExtraBold.ttf b/pyrender/pyrender/fonts/OpenSans-ExtraBold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..21f6f84a0799946fc4ae02c52b27e61c3762c745
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-ExtraBold.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf b/pyrender/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..31cb688340eff462dddf47efbb4dfef66cb7fbed
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-Italic.ttf b/pyrender/pyrender/fonts/OpenSans-Italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..c90da48ff3b8ad6167236d70c48df4d7b5de3bbb
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-Italic.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-Light.ttf b/pyrender/pyrender/fonts/OpenSans-Light.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..0d381897da20345fa63112f19042561f44ee3aa0
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-Light.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-LightItalic.ttf b/pyrender/pyrender/fonts/OpenSans-LightItalic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..68299c4bc6b5b7adfff2c9aee4aed7c1547100ef
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-LightItalic.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-Regular.ttf b/pyrender/pyrender/fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..db433349b7047f72f40072630c1bc110620bf09e
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-Regular.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-Semibold.ttf b/pyrender/pyrender/fonts/OpenSans-Semibold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..1a7679e3949fb045f152f456bc4adad31e8b9f55
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-Semibold.ttf differ
diff --git a/pyrender/pyrender/fonts/OpenSans-SemiboldItalic.ttf b/pyrender/pyrender/fonts/OpenSans-SemiboldItalic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..59b6d16b065f6baa6f70ddbd4322a4f44bb9636a
Binary files /dev/null and b/pyrender/pyrender/fonts/OpenSans-SemiboldItalic.ttf differ
diff --git a/pyrender/pyrender/light.py b/pyrender/pyrender/light.py
new file mode 100644
index 0000000000000000000000000000000000000000..333d9e4e553a245c259251a89b69cb46b73b1278
--- /dev/null
+++ b/pyrender/pyrender/light.py
@@ -0,0 +1,385 @@
+"""Punctual light sources as defined by the glTF 2.0 KHR extension at
+https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
+
+Author: Matthew Matl
+"""
+import abc
+import numpy as np
+import six
+
+from OpenGL.GL import *
+
+from .utils import format_color_vector
+from .texture import Texture
+from .constants import SHADOW_TEX_SZ
+from .camera import OrthographicCamera, PerspectiveCamera
+
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Light(object):
+ """Base class for all light objects.
+
+ Parameters
+ ----------
+ color : (3,) float
+ RGB value for the light's color in linear space.
+ intensity : float
+ Brightness of light. The units that this is defined in depend on the
+ type of light. Point and spot lights use luminous intensity in candela
+ (lm/sr), while directional lights use illuminance in lux (lm/m2).
+ name : str, optional
+ Name of the light.
+ """
+ def __init__(self,
+ color=None,
+ intensity=None,
+ name=None):
+
+ if color is None:
+ color = np.ones(3)
+ if intensity is None:
+ intensity = 1.0
+
+ self.name = name
+ self.color = color
+ self.intensity = intensity
+ self._shadow_camera = None
+ self._shadow_texture = None
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def color(self):
+ """(3,) float : The light's color.
+ """
+ return self._color
+
+ @color.setter
+ def color(self, value):
+ self._color = format_color_vector(value, 3)
+
+ @property
+ def intensity(self):
+ """float : The light's intensity in candela or lux.
+ """
+ return self._intensity
+
+ @intensity.setter
+ def intensity(self, value):
+ self._intensity = float(value)
+
+ @property
+ def shadow_texture(self):
+ """:class:`.Texture` : A texture used to hold shadow maps for this light.
+ """
+ return self._shadow_texture
+
+ @shadow_texture.setter
+ def shadow_texture(self, value):
+ if self._shadow_texture is not None:
+ if self._shadow_texture._in_context():
+ self._shadow_texture.delete()
+ self._shadow_texture = value
+
+ @abc.abstractmethod
+ def _generate_shadow_texture(self, size=None):
+ """Generate a shadow texture for this light.
+
+ Parameters
+ ----------
+ size : int, optional
+ Size of texture map. Must be a positive power of two.
+ """
+ pass
+
+ @abc.abstractmethod
+ def _get_shadow_camera(self, scene_scale):
+ """Generate and return a shadow mapping camera for this light.
+
+ Parameters
+ ----------
+ scene_scale : float
+ Length of scene's bounding box diagonal.
+
+ Returns
+ -------
+ camera : :class:`.Camera`
+ The camera used to render shadowmaps for this light.
+ """
+ pass
+
+
+class DirectionalLight(Light):
+ """Directional lights are light sources that act as though they are
+ infinitely far away and emit light in the direction of the local -z axis.
+ This light type inherits the orientation of the node that it belongs to;
+ position and scale are ignored except for their effect on the inherited
+ node orientation. Because it is at an infinite distance, the light is
+ not attenuated. Its intensity is defined in lumens per metre squared,
+ or lux (lm/m2).
+
+ Parameters
+ ----------
+ color : (3,) float, optional
+ RGB value for the light's color in linear space. Defaults to white
+ (i.e. [1.0, 1.0, 1.0]).
+ intensity : float, optional
+ Brightness of light, in lux (lm/m^2). Defaults to 1.0
+ name : str, optional
+ Name of the light.
+ """
+
+ def __init__(self,
+ color=None,
+ intensity=None,
+ name=None):
+ super(DirectionalLight, self).__init__(
+ color=color,
+ intensity=intensity,
+ name=name,
+ )
+
+ def _generate_shadow_texture(self, size=None):
+ """Generate a shadow texture for this light.
+
+ Parameters
+ ----------
+ size : int, optional
+ Size of texture map. Must be a positive power of two.
+ """
+ if size is None:
+ size = SHADOW_TEX_SZ
+ self.shadow_texture = Texture(width=size, height=size,
+ source_channels='D', data_format=GL_FLOAT)
+
+ def _get_shadow_camera(self, scene_scale):
+ """Generate and return a shadow mapping camera for this light.
+
+ Parameters
+ ----------
+ scene_scale : float
+ Length of scene's bounding box diagonal.
+
+ Returns
+ -------
+ camera : :class:`.Camera`
+ The camera used to render shadowmaps for this light.
+ """
+ return OrthographicCamera(
+ znear=0.01 * scene_scale,
+ zfar=10 * scene_scale,
+ xmag=scene_scale,
+ ymag=scene_scale
+ )
+
+
+class PointLight(Light):
+ """Point lights emit light in all directions from their position in space;
+ rotation and scale are ignored except for their effect on the inherited
+ node position. The brightness of the light attenuates in a physically
+ correct manner as distance increases from the light's position (i.e.
+ brightness goes like the inverse square of the distance). Point light
+ intensity is defined in candela, which is lumens per square radian (lm/sr).
+
+ Parameters
+ ----------
+ color : (3,) float
+ RGB value for the light's color in linear space.
+ intensity : float
+ Brightness of light in candela (lm/sr).
+ range : float
+ Cutoff distance at which light's intensity may be considered to
+ have reached zero. If None, the range is assumed to be infinite.
+ name : str, optional
+ Name of the light.
+ """
+
+ def __init__(self,
+ color=None,
+ intensity=None,
+ range=None,
+ name=None):
+ super(PointLight, self).__init__(
+ color=color,
+ intensity=intensity,
+ name=name,
+ )
+ self.range = range
+
+ @property
+ def range(self):
+ """float : The cutoff distance for the light.
+ """
+ return self._range
+
+ @range.setter
+ def range(self, value):
+ if value is not None:
+ value = float(value)
+ if value <= 0:
+ raise ValueError('Range must be > 0')
+ self._range = value
+ self._range = value
+
+ def _generate_shadow_texture(self, size=None):
+ """Generate a shadow texture for this light.
+
+ Parameters
+ ----------
+ size : int, optional
+ Size of texture map. Must be a positive power of two.
+ """
+ raise NotImplementedError('Shadows not implemented for point lights')
+
+ def _get_shadow_camera(self, scene_scale):
+ """Generate and return a shadow mapping camera for this light.
+
+ Parameters
+ ----------
+ scene_scale : float
+ Length of scene's bounding box diagonal.
+
+ Returns
+ -------
+ camera : :class:`.Camera`
+ The camera used to render shadowmaps for this light.
+ """
+ raise NotImplementedError('Shadows not implemented for point lights')
+
+
+class SpotLight(Light):
+ """Spot lights emit light in a cone in the direction of the local -z axis.
+ The angle and falloff of the cone is defined using two numbers, the
+ ``innerConeAngle`` and ``outerConeAngle``.
+ As with point lights, the brightness
+ also attenuates in a physically correct manner as distance increases from
+ the light's position (i.e. brightness goes like the inverse square of the
+ distance). Spot light intensity refers to the brightness inside the
+ ``innerConeAngle`` (and at the location of the light) and is defined in
+ candela, which is lumens per square radian (lm/sr). A spot light's position
+ and orientation are inherited from its node transform. Inherited scale does
+ not affect cone shape, and is ignored except for its effect on position
+ and orientation.
+
+ Parameters
+ ----------
+ color : (3,) float
+ RGB value for the light's color in linear space.
+ intensity : float
+ Brightness of light in candela (lm/sr).
+ range : float
+ Cutoff distance at which light's intensity may be considered to
+ have reached zero. If None, the range is assumed to be infinite.
+ innerConeAngle : float
+ Angle, in radians, from centre of spotlight where falloff begins.
+ Must be greater than or equal to ``0`` and less
+ than ``outerConeAngle``. Defaults to ``0``.
+ outerConeAngle : float
+ Angle, in radians, from centre of spotlight where falloff ends.
+ Must be greater than ``innerConeAngle`` and less than or equal to
+ ``PI / 2.0``. Defaults to ``PI / 4.0``.
+ name : str, optional
+ Name of the light.
+ """
+
+ def __init__(self,
+ color=None,
+ intensity=None,
+ range=None,
+ innerConeAngle=0.0,
+ outerConeAngle=(np.pi / 4.0),
+ name=None):
+ super(SpotLight, self).__init__(
+ name=name,
+ color=color,
+ intensity=intensity,
+ )
+ self.outerConeAngle = outerConeAngle
+ self.innerConeAngle = innerConeAngle
+ self.range = range
+
+ @property
+ def innerConeAngle(self):
+ """float : The inner cone angle in radians.
+ """
+ return self._innerConeAngle
+
+ @innerConeAngle.setter
+ def innerConeAngle(self, value):
+ if value < 0.0 or value > self.outerConeAngle:
+ raise ValueError('Invalid value for inner cone angle')
+ self._innerConeAngle = float(value)
+
+ @property
+ def outerConeAngle(self):
+ """float : The outer cone angle in radians.
+ """
+ return self._outerConeAngle
+
+ @outerConeAngle.setter
+ def outerConeAngle(self, value):
+ if value < 0.0 or value > np.pi / 2.0 + 1e-9:
+ raise ValueError('Invalid value for outer cone angle')
+ self._outerConeAngle = float(value)
+
+ @property
+ def range(self):
+ """float : The cutoff distance for the light.
+ """
+ return self._range
+
+ @range.setter
+ def range(self, value):
+ if value is not None:
+ value = float(value)
+ if value <= 0:
+ raise ValueError('Range must be > 0')
+ self._range = value
+ self._range = value
+
+ def _generate_shadow_texture(self, size=None):
+ """Generate a shadow texture for this light.
+
+ Parameters
+ ----------
+ size : int, optional
+ Size of texture map. Must be a positive power of two.
+ """
+ if size is None:
+ size = SHADOW_TEX_SZ
+ self.shadow_texture = Texture(width=size, height=size,
+ source_channels='D', data_format=GL_FLOAT)
+
+ def _get_shadow_camera(self, scene_scale):
+ """Generate and return a shadow mapping camera for this light.
+
+ Parameters
+ ----------
+ scene_scale : float
+ Length of scene's bounding box diagonal.
+
+ Returns
+ -------
+ camera : :class:`.Camera`
+ The camera used to render shadowmaps for this light.
+ """
+ return PerspectiveCamera(
+ znear=0.01 * scene_scale,
+ zfar=10 * scene_scale,
+ yfov=np.clip(2 * self.outerConeAngle + np.pi / 16.0, 0.0, np.pi),
+ aspectRatio=1.0
+ )
+
+
+__all__ = ['Light', 'DirectionalLight', 'SpotLight', 'PointLight']
diff --git a/pyrender/pyrender/material.py b/pyrender/pyrender/material.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ce9c2d184ed213c84b015e36bea558cd1efc6b7
--- /dev/null
+++ b/pyrender/pyrender/material.py
@@ -0,0 +1,707 @@
+"""Material properties, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-material
+and
+https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
+
+Author: Matthew Matl
+"""
+import abc
+import numpy as np
+import six
+
+from .constants import TexFlags
+from .utils import format_color_vector, format_texture_source
+from .texture import Texture
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Material(object):
+ """Base for standard glTF 2.0 materials.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
+ A tangent space normal map. The texture contains RGB components in
+ linear space. Each texel represents the XYZ components of a normal
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
+ right and +Y is up. +Z points toward the viewer.
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
+ The occlusion map texture. The occlusion values are sampled from the R
+ channel. Higher values indicate areas that should receive full indirect
+ lighting and lower values indicate no indirect lighting. These values
+ are linear. If other channels are present (GBA), they are ignored for
+ occlusion calculations.
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
+ The emissive map controls the color and intensity of the light being
+ emitted by the material. This texture contains RGB components in sRGB
+ color space. If a fourth component (A) is present, it is ignored.
+ emissiveFactor : (3,) float, optional
+ The RGB components of the emissive color of the material. These values
+ are linear. If an emissiveTexture is specified, this value is
+ multiplied with the texel values.
+ alphaMode : str, optional
+ The material's alpha rendering mode enumeration specifying the
+ interpretation of the alpha value of the main factor and texture.
+ Allowed Values:
+
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
+ fully opaque.
+ - `"MASK"` The rendered output is either fully opaque or fully
+ transparent depending on the alpha value and the specified alpha
+ cutoff value.
+ - `"BLEND"` The alpha value is used to composite the source and
+ destination areas. The rendered output is combined with the
+ background using the normal painting operation (i.e. the Porter
+ and Duff over operator).
+
+ alphaCutoff : float, optional
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
+ greater than or equal to this value then it is rendered as fully
+ opaque, otherwise, it is rendered as fully transparent.
+ A value greater than 1.0 will render the entire material as fully
+ transparent. This value is ignored for other modes.
+ doubleSided : bool, optional
+ Specifies whether the material is double sided. When this value is
+ false, back-face culling is enabled. When this value is true,
+ back-face culling is disabled and double sided lighting is enabled.
+ smooth : bool, optional
+ If True, the material is rendered smoothly by using only one normal
+ per vertex and face indexing.
+ wireframe : bool, optional
+ If True, the material is rendered in wireframe mode.
+ """
+
+ def __init__(self,
+ name=None,
+ normalTexture=None,
+ occlusionTexture=None,
+ emissiveTexture=None,
+ emissiveFactor=None,
+ alphaMode=None,
+ alphaCutoff=None,
+ doubleSided=False,
+ smooth=True,
+ wireframe=False):
+
+ # Set defaults
+ if alphaMode is None:
+ alphaMode = 'OPAQUE'
+
+ if alphaCutoff is None:
+ alphaCutoff = 0.5
+
+ if emissiveFactor is None:
+ emissiveFactor = np.zeros(3).astype(np.float32)
+
+ self.name = name
+ self.normalTexture = normalTexture
+ self.occlusionTexture = occlusionTexture
+ self.emissiveTexture = emissiveTexture
+ self.emissiveFactor = emissiveFactor
+ self.alphaMode = alphaMode
+ self.alphaCutoff = alphaCutoff
+ self.doubleSided = doubleSided
+ self.smooth = smooth
+ self.wireframe = wireframe
+
+ self._tex_flags = None
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def normalTexture(self):
+ """(n,n,3) float or :class:`Texture` : The tangent-space normal map.
+ """
+ return self._normalTexture
+
+ @normalTexture.setter
+ def normalTexture(self, value):
+ # TODO TMP
+ self._normalTexture = self._format_texture(value, 'RGB')
+ self._tex_flags = None
+
+ @property
+ def occlusionTexture(self):
+ """(n,n,1) float or :class:`Texture` : The ambient occlusion map.
+ """
+ return self._occlusionTexture
+
+ @occlusionTexture.setter
+ def occlusionTexture(self, value):
+ self._occlusionTexture = self._format_texture(value, 'R')
+ self._tex_flags = None
+
+ @property
+ def emissiveTexture(self):
+ """(n,n,3) float or :class:`Texture` : The emission map.
+ """
+ return self._emissiveTexture
+
+ @emissiveTexture.setter
+ def emissiveTexture(self, value):
+ self._emissiveTexture = self._format_texture(value, 'RGB')
+ self._tex_flags = None
+
+ @property
+ def emissiveFactor(self):
+ """(3,) float : Base multiplier for emission colors.
+ """
+ return self._emissiveFactor
+
+ @emissiveFactor.setter
+ def emissiveFactor(self, value):
+ if value is None:
+ value = np.zeros(3)
+ self._emissiveFactor = format_color_vector(value, 3)
+
+ @property
+ def alphaMode(self):
+ """str : The mode for blending.
+ """
+ return self._alphaMode
+
+ @alphaMode.setter
+ def alphaMode(self, value):
+ if value not in set(['OPAQUE', 'MASK', 'BLEND']):
+ raise ValueError('Invalid alpha mode {}'.format(value))
+ self._alphaMode = value
+
+ @property
+ def alphaCutoff(self):
+ """float : The cutoff threshold in MASK mode.
+ """
+ return self._alphaCutoff
+
+ @alphaCutoff.setter
+ def alphaCutoff(self, value):
+ if value < 0 or value > 1:
+ raise ValueError('Alpha cutoff must be in range [0,1]')
+ self._alphaCutoff = float(value)
+
+ @property
+ def doubleSided(self):
+ """bool : Whether the material is double-sided.
+ """
+ return self._doubleSided
+
+ @doubleSided.setter
+ def doubleSided(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Double sided must be a boolean value')
+ self._doubleSided = value
+
+ @property
+ def smooth(self):
+ """bool : Whether to render the mesh smoothly by
+ interpolating vertex normals.
+ """
+ return self._smooth
+
+ @smooth.setter
+ def smooth(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Double sided must be a boolean value')
+ self._smooth = value
+
+ @property
+ def wireframe(self):
+ """bool : Whether to render the mesh in wireframe mode.
+ """
+ return self._wireframe
+
+ @wireframe.setter
+ def wireframe(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Wireframe must be a boolean value')
+ self._wireframe = value
+
+ @property
+ def is_transparent(self):
+ """bool : If True, the object is partially transparent.
+ """
+ return self._compute_transparency()
+
+ @property
+ def tex_flags(self):
+ """int : Texture availability flags.
+ """
+ if self._tex_flags is None:
+ self._tex_flags = self._compute_tex_flags()
+ return self._tex_flags
+
+ @property
+ def textures(self):
+ """list of :class:`Texture` : The textures associated with this
+ material.
+ """
+ return self._compute_textures()
+
+ def _compute_transparency(self):
+ return False
+
+ def _compute_tex_flags(self):
+ tex_flags = TexFlags.NONE
+ if self.normalTexture is not None:
+ tex_flags |= TexFlags.NORMAL
+ if self.occlusionTexture is not None:
+ tex_flags |= TexFlags.OCCLUSION
+ if self.emissiveTexture is not None:
+ tex_flags |= TexFlags.EMISSIVE
+ return tex_flags
+
+ def _compute_textures(self):
+ all_textures = [
+ self.normalTexture, self.occlusionTexture, self.emissiveTexture
+ ]
+ textures = set([t for t in all_textures if t is not None])
+ return textures
+
+ def _format_texture(self, texture, target_channels='RGB'):
+ """Format a texture as a float32 np array.
+ """
+ if isinstance(texture, Texture) or texture is None:
+ return texture
+ else:
+ source = format_texture_source(texture, target_channels)
+ return Texture(source=source, source_channels=target_channels)
+
+
+class MetallicRoughnessMaterial(Material):
+ """A material based on the metallic-roughness material model from
+ Physically-Based Rendering (PBR) methodology.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
+ A tangent space normal map. The texture contains RGB components in
+ linear space. Each texel represents the XYZ components of a normal
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
+ right and +Y is up. +Z points toward the viewer.
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
+ The occlusion map texture. The occlusion values are sampled from the R
+ channel. Higher values indicate areas that should receive full indirect
+ lighting and lower values indicate no indirect lighting. These values
+ are linear. If other channels are present (GBA), they are ignored for
+ occlusion calculations.
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
+ The emissive map controls the color and intensity of the light being
+ emitted by the material. This texture contains RGB components in sRGB
+ color space. If a fourth component (A) is present, it is ignored.
+ emissiveFactor : (3,) float, optional
+ The RGB components of the emissive color of the material. These values
+ are linear. If an emissiveTexture is specified, this value is
+ multiplied with the texel values.
+ alphaMode : str, optional
+ The material's alpha rendering mode enumeration specifying the
+ interpretation of the alpha value of the main factor and texture.
+ Allowed Values:
+
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
+ fully opaque.
+ - `"MASK"` The rendered output is either fully opaque or fully
+ transparent depending on the alpha value and the specified alpha
+ cutoff value.
+ - `"BLEND"` The alpha value is used to composite the source and
+ destination areas. The rendered output is combined with the
+ background using the normal painting operation (i.e. the Porter
+ and Duff over operator).
+
+ alphaCutoff : float, optional
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
+ greater than or equal to this value then it is rendered as fully
+ opaque, otherwise, it is rendered as fully transparent.
+ A value greater than 1.0 will render the entire material as fully
+ transparent. This value is ignored for other modes.
+ doubleSided : bool, optional
+ Specifies whether the material is double sided. When this value is
+ false, back-face culling is enabled. When this value is true,
+ back-face culling is disabled and double sided lighting is enabled.
+ smooth : bool, optional
+ If True, the material is rendered smoothly by using only one normal
+ per vertex and face indexing.
+ wireframe : bool, optional
+ If True, the material is rendered in wireframe mode.
+ baseColorFactor : (4,) float, optional
+ The RGBA components of the base color of the material. The fourth
+ component (A) is the alpha coverage of the material. The alphaMode
+ property specifies how alpha is interpreted. These values are linear.
+ If a baseColorTexture is specified, this value is multiplied with the
+ texel values.
+ baseColorTexture : (n,n,4) float or :class:`Texture`, optional
+ The base color texture. This texture contains RGB(A) components in sRGB
+ color space. The first three components (RGB) specify the base color of
+ the material. If the fourth component (A) is present, it represents the
+ alpha coverage of the material. Otherwise, an alpha of 1.0 is assumed.
+ The alphaMode property specifies how alpha is interpreted.
+ The stored texels must not be premultiplied.
+ metallicFactor : float
+ The metalness of the material. A value of 1.0 means the material is a
+ metal. A value of 0.0 means the material is a dielectric. Values in
+ between are for blending between metals and dielectrics such as dirty
+ metallic surfaces. This value is linear. If a metallicRoughnessTexture
+ is specified, this value is multiplied with the metallic texel values.
+ roughnessFactor : float
+ The roughness of the material. A value of 1.0 means the material is
+ completely rough. A value of 0.0 means the material is completely
+ smooth. This value is linear. If a metallicRoughnessTexture is
+ specified, this value is multiplied with the roughness texel values.
+ metallicRoughnessTexture : (n,n,2) float or :class:`Texture`, optional
+ The metallic-roughness texture. The metalness values are sampled from
+ the B channel. The roughness values are sampled from the G channel.
+ These values are linear. If other channels are present (R or A), they
+ are ignored for metallic-roughness calculations.
+ """
+
+ def __init__(self,
+ name=None,
+ normalTexture=None,
+ occlusionTexture=None,
+ emissiveTexture=None,
+ emissiveFactor=None,
+ alphaMode=None,
+ alphaCutoff=None,
+ doubleSided=False,
+ smooth=True,
+ wireframe=False,
+ baseColorFactor=None,
+ baseColorTexture=None,
+ metallicFactor=1.0,
+ roughnessFactor=1.0,
+ metallicRoughnessTexture=None):
+ super(MetallicRoughnessMaterial, self).__init__(
+ name=name,
+ normalTexture=normalTexture,
+ occlusionTexture=occlusionTexture,
+ emissiveTexture=emissiveTexture,
+ emissiveFactor=emissiveFactor,
+ alphaMode=alphaMode,
+ alphaCutoff=alphaCutoff,
+ doubleSided=doubleSided,
+ smooth=smooth,
+ wireframe=wireframe
+ )
+
+ # Set defaults
+ if baseColorFactor is None:
+ baseColorFactor = np.ones(4).astype(np.float32)
+
+ self.baseColorFactor = baseColorFactor
+ self.baseColorTexture = baseColorTexture
+ self.metallicFactor = metallicFactor
+ self.roughnessFactor = roughnessFactor
+ self.metallicRoughnessTexture = metallicRoughnessTexture
+
+ @property
+ def baseColorFactor(self):
+ """(4,) float or :class:`Texture` : The RGBA base color multiplier.
+ """
+ return self._baseColorFactor
+
+ @baseColorFactor.setter
+ def baseColorFactor(self, value):
+ if value is None:
+ value = np.ones(4)
+ self._baseColorFactor = format_color_vector(value, 4)
+
+ @property
+ def baseColorTexture(self):
+ """(n,n,4) float or :class:`Texture` : The diffuse texture.
+ """
+ return self._baseColorTexture
+
+ @baseColorTexture.setter
+ def baseColorTexture(self, value):
+ self._baseColorTexture = self._format_texture(value, 'RGBA')
+ self._tex_flags = None
+
+ @property
+ def metallicFactor(self):
+ """float : The metalness of the material.
+ """
+ return self._metallicFactor
+
+ @metallicFactor.setter
+ def metallicFactor(self, value):
+ if value is None:
+ value = 1.0
+ if value < 0 or value > 1:
+ raise ValueError('Metallic factor must be in range [0,1]')
+ self._metallicFactor = float(value)
+
+ @property
+ def roughnessFactor(self):
+ """float : The roughness of the material.
+ """
+ return self.RoughnessFactor
+
+ @roughnessFactor.setter
+ def roughnessFactor(self, value):
+ if value is None:
+ value = 1.0
+ if value < 0 or value > 1:
+ raise ValueError('Roughness factor must be in range [0,1]')
+ self.RoughnessFactor = float(value)
+
+ @property
+ def metallicRoughnessTexture(self):
+ """(n,n,2) float or :class:`Texture` : The metallic-roughness texture.
+ """
+ return self._metallicRoughnessTexture
+
+ @metallicRoughnessTexture.setter
+ def metallicRoughnessTexture(self, value):
+ self._metallicRoughnessTexture = self._format_texture(value, 'GB')
+ self._tex_flags = None
+
+ def _compute_tex_flags(self):
+ tex_flags = super(MetallicRoughnessMaterial, self)._compute_tex_flags()
+ if self.baseColorTexture is not None:
+ tex_flags |= TexFlags.BASE_COLOR
+ if self.metallicRoughnessTexture is not None:
+ tex_flags |= TexFlags.METALLIC_ROUGHNESS
+ return tex_flags
+
+ def _compute_transparency(self):
+ if self.alphaMode == 'OPAQUE':
+ return False
+ cutoff = self.alphaCutoff
+ if self.alphaMode == 'BLEND':
+ cutoff = 1.0
+ if self.baseColorFactor[3] < cutoff:
+ return True
+ if (self.baseColorTexture is not None and
+ self.baseColorTexture.is_transparent(cutoff)):
+ return True
+ return False
+
+ def _compute_textures(self):
+ textures = super(MetallicRoughnessMaterial, self)._compute_textures()
+ all_textures = [self.baseColorTexture, self.metallicRoughnessTexture]
+ all_textures = {t for t in all_textures if t is not None}
+ textures |= all_textures
+ return textures
+
+
+class SpecularGlossinessMaterial(Material):
+ """A material based on the specular-glossiness material model from
+ Physically-Based Rendering (PBR) methodology.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
+ A tangent space normal map. The texture contains RGB components in
+ linear space. Each texel represents the XYZ components of a normal
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
+ right and +Y is up. +Z points toward the viewer.
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
+ The occlusion map texture. The occlusion values are sampled from the R
+ channel. Higher values indicate areas that should receive full indirect
+ lighting and lower values indicate no indirect lighting. These values
+ are linear. If other channels are present (GBA), they are ignored for
+ occlusion calculations.
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
+ The emissive map controls the color and intensity of the light being
+ emitted by the material. This texture contains RGB components in sRGB
+ color space. If a fourth component (A) is present, it is ignored.
+ emissiveFactor : (3,) float, optional
+ The RGB components of the emissive color of the material. These values
+ are linear. If an emissiveTexture is specified, this value is
+ multiplied with the texel values.
+ alphaMode : str, optional
+ The material's alpha rendering mode enumeration specifying the
+ interpretation of the alpha value of the main factor and texture.
+ Allowed Values:
+
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
+ fully opaque.
+ - `"MASK"` The rendered output is either fully opaque or fully
+ transparent depending on the alpha value and the specified alpha
+ cutoff value.
+ - `"BLEND"` The alpha value is used to composite the source and
+ destination areas. The rendered output is combined with the
+ background using the normal painting operation (i.e. the Porter
+ and Duff over operator).
+
+ alphaCutoff : float, optional
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
+ greater than or equal to this value then it is rendered as fully
+ opaque, otherwise, it is rendered as fully transparent.
+ A value greater than 1.0 will render the entire material as fully
+ transparent. This value is ignored for other modes.
+ doubleSided : bool, optional
+ Specifies whether the material is double sided. When this value is
+ false, back-face culling is enabled. When this value is true,
+ back-face culling is disabled and double sided lighting is enabled.
+ smooth : bool, optional
+ If True, the material is rendered smoothly by using only one normal
+ per vertex and face indexing.
+ wireframe : bool, optional
+ If True, the material is rendered in wireframe mode.
+ diffuseFactor : (4,) float
+ The RGBA components of the reflected diffuse color of the material.
+ Metals have a diffuse value of [0.0, 0.0, 0.0]. The fourth component
+ (A) is the opacity of the material. The values are linear.
+ diffuseTexture : (n,n,4) float or :class:`Texture`, optional
+ The diffuse texture. This texture contains RGB(A) components of the
+ reflected diffuse color of the material in sRGB color space. If the
+ fourth component (A) is present, it represents the alpha coverage of
+ the material. Otherwise, an alpha of 1.0 is assumed.
+ The alphaMode property specifies how alpha is interpreted.
+ The stored texels must not be premultiplied.
+ specularFactor : (3,) float
+ The specular RGB color of the material. This value is linear.
+ glossinessFactor : float
+ The glossiness or smoothness of the material. A value of 1.0 means the
+ material has full glossiness or is perfectly smooth. A value of 0.0
+ means the material has no glossiness or is perfectly rough. This value
+ is linear.
+ specularGlossinessTexture : (n,n,4) or :class:`Texture`, optional
+ The specular-glossiness texture is a RGBA texture, containing the
+ specular color (RGB) in sRGB space and the glossiness value (A) in
+ linear space.
+ """
+
+ def __init__(self,
+ name=None,
+ normalTexture=None,
+ occlusionTexture=None,
+ emissiveTexture=None,
+ emissiveFactor=None,
+ alphaMode=None,
+ alphaCutoff=None,
+ doubleSided=False,
+ smooth=True,
+ wireframe=False,
+ diffuseFactor=None,
+ diffuseTexture=None,
+ specularFactor=None,
+ glossinessFactor=1.0,
+ specularGlossinessTexture=None):
+ super(SpecularGlossinessMaterial, self).__init__(
+ name=name,
+ normalTexture=normalTexture,
+ occlusionTexture=occlusionTexture,
+ emissiveTexture=emissiveTexture,
+ emissiveFactor=emissiveFactor,
+ alphaMode=alphaMode,
+ alphaCutoff=alphaCutoff,
+ doubleSided=doubleSided,
+ smooth=smooth,
+ wireframe=wireframe
+ )
+
+ # Set defaults
+ if diffuseFactor is None:
+ diffuseFactor = np.ones(4).astype(np.float32)
+ if specularFactor is None:
+ specularFactor = np.ones(3).astype(np.float32)
+
+ self.diffuseFactor = diffuseFactor
+ self.diffuseTexture = diffuseTexture
+ self.specularFactor = specularFactor
+ self.glossinessFactor = glossinessFactor
+ self.specularGlossinessTexture = specularGlossinessTexture
+
+ @property
+ def diffuseFactor(self):
+ """(4,) float : The diffuse base color.
+ """
+ return self._diffuseFactor
+
+ @diffuseFactor.setter
+ def diffuseFactor(self, value):
+ self._diffuseFactor = format_color_vector(value, 4)
+
+ @property
+ def diffuseTexture(self):
+ """(n,n,4) float or :class:`Texture` : The diffuse map.
+ """
+ return self._diffuseTexture
+
+ @diffuseTexture.setter
+ def diffuseTexture(self, value):
+ self._diffuseTexture = self._format_texture(value, 'RGBA')
+ self._tex_flags = None
+
+ @property
+ def specularFactor(self):
+ """(3,) float : The specular color of the material.
+ """
+ return self._specularFactor
+
+ @specularFactor.setter
+ def specularFactor(self, value):
+ self._specularFactor = format_color_vector(value, 3)
+
+ @property
+ def glossinessFactor(self):
+ """float : The glossiness of the material.
+ """
+ return self.glossinessFactor
+
+ @glossinessFactor.setter
+ def glossinessFactor(self, value):
+ if value < 0 or value > 1:
+ raise ValueError('glossiness factor must be in range [0,1]')
+ self._glossinessFactor = float(value)
+
+ @property
+ def specularGlossinessTexture(self):
+ """(n,n,4) or :class:`Texture` : The specular-glossiness texture.
+ """
+ return self._specularGlossinessTexture
+
+ @specularGlossinessTexture.setter
+ def specularGlossinessTexture(self, value):
+ self._specularGlossinessTexture = self._format_texture(value, 'GB')
+ self._tex_flags = None
+
+ def _compute_tex_flags(self):
+ flags = super(SpecularGlossinessMaterial, self)._compute_tex_flags()
+ if self.diffuseTexture is not None:
+ flags |= TexFlags.DIFFUSE
+ if self.specularGlossinessTexture is not None:
+ flags |= TexFlags.SPECULAR_GLOSSINESS
+ return flags
+
+ def _compute_transparency(self):
+ if self.alphaMode == 'OPAQUE':
+ return False
+ cutoff = self.alphaCutoff
+ if self.alphaMode == 'BLEND':
+ cutoff = 1.0
+ if self.diffuseFactor[3] < cutoff:
+ return True
+ if (self.diffuseTexture is not None and
+ self.diffuseTexture.is_transparent(cutoff)):
+ return True
+ return False
+
+ def _compute_textures(self):
+ textures = super(SpecularGlossinessMaterial, self)._compute_textures()
+ all_textures = [self.diffuseTexture, self.specularGlossinessTexture]
+ all_textures = {t for t in all_textures if t is not None}
+ textures |= all_textures
+ return textures
diff --git a/pyrender/pyrender/mesh.py b/pyrender/pyrender/mesh.py
new file mode 100644
index 0000000000000000000000000000000000000000..36833ea3dfa6c095a18fc745ff34cf106e83c95d
--- /dev/null
+++ b/pyrender/pyrender/mesh.py
@@ -0,0 +1,328 @@
+"""Meshes, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh
+
+Author: Matthew Matl
+"""
+import copy
+
+import numpy as np
+import trimesh
+
+from .primitive import Primitive
+from .constants import GLTF
+from .material import MetallicRoughnessMaterial
+
+
+class Mesh(object):
+ """A set of primitives to be rendered.
+
+ Parameters
+ ----------
+ name : str
+ The user-defined name of this object.
+ primitives : list of :class:`Primitive`
+ The primitives associated with this mesh.
+ weights : (k,) float
+ Array of weights to be applied to the Morph Targets.
+ is_visible : bool
+ If False, the mesh will not be rendered.
+ """
+
+ def __init__(self, primitives, name=None, weights=None, is_visible=True):
+ self.primitives = primitives
+ self.name = name
+ self.weights = weights
+ self.is_visible = is_visible
+
+ self._bounds = None
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def primitives(self):
+ """list of :class:`Primitive` : The primitives associated
+ with this mesh.
+ """
+ return self._primitives
+
+ @primitives.setter
+ def primitives(self, value):
+ self._primitives = value
+
+ @property
+ def weights(self):
+ """(k,) float : Weights to be applied to morph targets.
+ """
+ return self._weights
+
+ @weights.setter
+ def weights(self, value):
+ self._weights = value
+
+ @property
+ def is_visible(self):
+ """bool : Whether the mesh is visible.
+ """
+ return self._is_visible
+
+ @is_visible.setter
+ def is_visible(self, value):
+ self._is_visible = value
+
+ @property
+ def bounds(self):
+ """(2,3) float : The axis-aligned bounds of the mesh.
+ """
+ if self._bounds is None:
+ bounds = np.array([[np.infty, np.infty, np.infty],
+ [-np.infty, -np.infty, -np.infty]])
+ for p in self.primitives:
+ bounds[0] = np.minimum(bounds[0], p.bounds[0])
+ bounds[1] = np.maximum(bounds[1], p.bounds[1])
+ self._bounds = bounds
+ return self._bounds
+
+ @property
+ def centroid(self):
+ """(3,) float : The centroid of the mesh's axis-aligned bounding box
+ (AABB).
+ """
+ return np.mean(self.bounds, axis=0)
+
+ @property
+ def extents(self):
+ """(3,) float : The lengths of the axes of the mesh's AABB.
+ """
+ return np.diff(self.bounds, axis=0).reshape(-1)
+
+ @property
+ def scale(self):
+ """(3,) float : The length of the diagonal of the mesh's AABB.
+ """
+ return np.linalg.norm(self.extents)
+
+ @property
+ def is_transparent(self):
+ """bool : If True, the mesh is partially-transparent.
+ """
+ for p in self.primitives:
+ if p.is_transparent:
+ return True
+ return False
+
+ @staticmethod
+ def from_points(points, colors=None, normals=None,
+ is_visible=True, poses=None):
+ """Create a Mesh from a set of points.
+
+ Parameters
+ ----------
+ points : (n,3) float
+ The point positions.
+ colors : (n,3) or (n,4) float, optional
+ RGB or RGBA colors for each point.
+ normals : (n,3) float, optionals
+ The normal vectors for each point.
+ is_visible : bool
+ If False, the points will not be rendered.
+ poses : (x,4,4)
+ Array of 4x4 transformation matrices for instancing this object.
+
+ Returns
+ -------
+ mesh : :class:`Mesh`
+ The created mesh.
+ """
+ primitive = Primitive(
+ positions=points,
+ normals=normals,
+ color_0=colors,
+ mode=GLTF.POINTS,
+ poses=poses
+ )
+ mesh = Mesh(primitives=[primitive], is_visible=is_visible)
+ return mesh
+
+ @staticmethod
+ def from_trimesh(mesh, material=None, is_visible=True,
+ poses=None, wireframe=False, smooth=True):
+ """Create a Mesh from a :class:`~trimesh.base.Trimesh`.
+
+ Parameters
+ ----------
+ mesh : :class:`~trimesh.base.Trimesh` or list of them
+ A triangular mesh or a list of meshes.
+ material : :class:`Material`
+ The material of the object. Overrides any mesh material.
+ If not specified and the mesh has no material, a default material
+ will be used.
+ is_visible : bool
+ If False, the mesh will not be rendered.
+ poses : (n,4,4) float
+ Array of 4x4 transformation matrices for instancing this object.
+ wireframe : bool
+ If `True`, the mesh will be rendered as a wireframe object
+ smooth : bool
+ If `True`, the mesh will be rendered with interpolated vertex
+ normals. Otherwise, the mesh edges will stay sharp.
+
+ Returns
+ -------
+ mesh : :class:`Mesh`
+ The created mesh.
+ """
+
+ if isinstance(mesh, (list, tuple, set, np.ndarray)):
+ meshes = list(mesh)
+ elif isinstance(mesh, trimesh.Trimesh):
+ meshes = [mesh]
+ else:
+ raise TypeError('Expected a Trimesh or a list, got a {}'
+ .format(type(mesh)))
+
+ primitives = []
+ for m in meshes:
+ positions = None
+ normals = None
+ indices = None
+
+ # Compute positions, normals, and indices
+ if smooth:
+ positions = m.vertices.copy()
+ normals = m.vertex_normals.copy()
+ indices = m.faces.copy()
+ else:
+ positions = m.vertices[m.faces].reshape((3 * len(m.faces), 3))
+ normals = np.repeat(m.face_normals, 3, axis=0)
+
+ # Compute colors, texture coords, and material properties
+ color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props(m, smooth=smooth, material=material)
+
+ # Override if material is given.
+ if material is not None:
+ #primitive_material = copy.copy(material)
+ primitive_material = copy.deepcopy(material) # TODO
+
+ if primitive_material is None:
+ # Replace material with default if needed
+ primitive_material = MetallicRoughnessMaterial(
+ alphaMode='BLEND',
+ baseColorFactor=[0.3, 0.3, 0.3, 1.0],
+ metallicFactor=0.2,
+ roughnessFactor=0.8
+ )
+
+ primitive_material.wireframe = wireframe
+
+ # Create the primitive
+ primitives.append(Primitive(
+ positions=positions,
+ normals=normals,
+ texcoord_0=texcoord_0,
+ color_0=color_0,
+ indices=indices,
+ material=primitive_material,
+ mode=GLTF.TRIANGLES,
+ poses=poses
+ ))
+
+ return Mesh(primitives=primitives, is_visible=is_visible)
+
+ @staticmethod
+ def _get_trimesh_props(mesh, smooth=False, material=None):
+ """Gets the vertex colors, texture coordinates, and material properties
+ from a :class:`~trimesh.base.Trimesh`.
+ """
+ colors = None
+ texcoords = None
+
+ # If the trimesh visual is undefined, return none for both
+ if not mesh.visual.defined:
+ return colors, texcoords, material
+
+ # Process vertex colors
+ if material is None:
+ if mesh.visual.kind == 'vertex':
+ vc = mesh.visual.vertex_colors.copy()
+ if smooth:
+ colors = vc
+ else:
+ colors = vc[mesh.faces].reshape(
+ (3 * len(mesh.faces), vc.shape[1])
+ )
+ material = MetallicRoughnessMaterial(
+ alphaMode='BLEND',
+ baseColorFactor=[1.0, 1.0, 1.0, 1.0],
+ metallicFactor=0.2,
+ roughnessFactor=0.8
+ )
+ # Process face colors
+ elif mesh.visual.kind == 'face':
+ if smooth:
+ raise ValueError('Cannot use face colors with a smooth mesh')
+ else:
+ colors = np.repeat(mesh.visual.face_colors, 3, axis=0)
+
+ material = MetallicRoughnessMaterial(
+ alphaMode='BLEND',
+ baseColorFactor=[1.0, 1.0, 1.0, 1.0],
+ metallicFactor=0.2,
+ roughnessFactor=0.8
+ )
+
+ # Process texture colors
+ if mesh.visual.kind == 'texture':
+ # Configure UV coordinates
+ if mesh.visual.uv is not None and len(mesh.visual.uv) != 0:
+ uv = mesh.visual.uv.copy()
+ if smooth:
+ texcoords = uv
+ else:
+ texcoords = uv[mesh.faces].reshape(
+ (3 * len(mesh.faces), uv.shape[1])
+ )
+
+ if material is None:
+ # Configure mesh material
+ mat = mesh.visual.material
+
+ if isinstance(mat, trimesh.visual.texture.PBRMaterial):
+ material = MetallicRoughnessMaterial(
+ normalTexture=mat.normalTexture,
+ occlusionTexture=mat.occlusionTexture,
+ emissiveTexture=mat.emissiveTexture,
+ emissiveFactor=mat.emissiveFactor,
+ alphaMode='BLEND',
+ baseColorFactor=mat.baseColorFactor,
+ baseColorTexture=mat.baseColorTexture,
+ metallicFactor=mat.metallicFactor,
+ roughnessFactor=mat.roughnessFactor,
+ metallicRoughnessTexture=mat.metallicRoughnessTexture,
+ doubleSided=mat.doubleSided,
+ alphaCutoff=mat.alphaCutoff
+ )
+ elif isinstance(mat, trimesh.visual.texture.SimpleMaterial):
+ glossiness = mat.kwargs.get('Ns', 1.0)
+ if isinstance(glossiness, list):
+ glossiness = float(glossiness[0])
+ roughness = (2 / (glossiness + 2)) ** (1.0 / 4.0)
+ material = MetallicRoughnessMaterial(
+ alphaMode='BLEND',
+ roughnessFactor=roughness,
+ baseColorFactor=mat.diffuse,
+ baseColorTexture=mat.image,
+ )
+ elif isinstance(mat, MetallicRoughnessMaterial):
+ material = mat
+
+ return colors, texcoords, material
diff --git a/pyrender/pyrender/node.py b/pyrender/pyrender/node.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f37f7856cc732a37dc58253022a7c331489493e
--- /dev/null
+++ b/pyrender/pyrender/node.py
@@ -0,0 +1,263 @@
+"""Nodes, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node
+
+Author: Matthew Matl
+"""
+import numpy as np
+
+import trimesh.transformations as transformations
+
+from .camera import Camera
+from .mesh import Mesh
+from .light import Light
+
+
+class Node(object):
+ """A node in the node hierarchy.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ camera : :class:`Camera`, optional
+ The camera in this node.
+ children : list of :class:`Node`
+ The children of this node.
+ skin : int, optional
+ The index of the skin referenced by this node.
+ matrix : (4,4) float, optional
+ A floating-point 4x4 transformation matrix.
+ mesh : :class:`Mesh`, optional
+ The mesh in this node.
+ rotation : (4,) float, optional
+ The node's unit quaternion in the order (x, y, z, w), where
+ w is the scalar.
+ scale : (3,) float, optional
+ The node's non-uniform scale, given as the scaling factors along the x,
+ y, and z axes.
+ translation : (3,) float, optional
+ The node's translation along the x, y, and z axes.
+ weights : (n,) float
+ The weights of the instantiated Morph Target. Number of elements must
+ match number of Morph Targets of used mesh.
+ light : :class:`Light`, optional
+ The light in this node.
+ """
+
+ def __init__(self,
+ name=None,
+ camera=None,
+ children=None,
+ skin=None,
+ matrix=None,
+ mesh=None,
+ rotation=None,
+ scale=None,
+ translation=None,
+ weights=None,
+ light=None):
+ # Set defaults
+ if children is None:
+ children = []
+
+ self._matrix = None
+ self._scale = None
+ self._rotation = None
+ self._translation = None
+ if matrix is None:
+ if rotation is None:
+ rotation = np.array([0.0, 0.0, 0.0, 1.0])
+ if translation is None:
+ translation = np.zeros(3)
+ if scale is None:
+ scale = np.ones(3)
+ self.rotation = rotation
+ self.translation = translation
+ self.scale = scale
+ else:
+ self.matrix = matrix
+
+ self.name = name
+ self.camera = camera
+ self.children = children
+ self.skin = skin
+ self.mesh = mesh
+ self.weights = weights
+ self.light = light
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def camera(self):
+ """:class:`Camera` : The camera in this node.
+ """
+ return self._camera
+
+ @camera.setter
+ def camera(self, value):
+ if value is not None and not isinstance(value, Camera):
+ raise TypeError('Value must be a camera')
+ self._camera = value
+
+ @property
+ def children(self):
+ """list of :class:`Node` : The children of this node.
+ """
+ return self._children
+
+ @children.setter
+ def children(self, value):
+ self._children = value
+
+ @property
+ def skin(self):
+ """int : The skin index for this node.
+ """
+ return self._skin
+
+ @skin.setter
+ def skin(self, value):
+ self._skin = value
+
+ @property
+ def mesh(self):
+ """:class:`Mesh` : The mesh in this node.
+ """
+ return self._mesh
+
+ @mesh.setter
+ def mesh(self, value):
+ if value is not None and not isinstance(value, Mesh):
+ raise TypeError('Value must be a mesh')
+ self._mesh = value
+
+ @property
+ def light(self):
+ """:class:`Light` : The light in this node.
+ """
+ return self._light
+
+ @light.setter
+ def light(self, value):
+ if value is not None and not isinstance(value, Light):
+ raise TypeError('Value must be a light')
+ self._light = value
+
+ @property
+ def rotation(self):
+ """(4,) float : The xyzw quaternion for this node.
+ """
+ return self._rotation
+
+ @rotation.setter
+ def rotation(self, value):
+ value = np.asanyarray(value)
+ if value.shape != (4,):
+ raise ValueError('Quaternion must be a (4,) vector')
+ if np.abs(np.linalg.norm(value) - 1.0) > 1e-3:
+ raise ValueError('Quaternion must have norm == 1.0')
+ self._rotation = value
+ self._matrix = None
+
+ @property
+ def translation(self):
+ """(3,) float : The translation for this node.
+ """
+ return self._translation
+
+ @translation.setter
+ def translation(self, value):
+ value = np.asanyarray(value)
+ if value.shape != (3,):
+ raise ValueError('Translation must be a (3,) vector')
+ self._translation = value
+ self._matrix = None
+
+ @property
+ def scale(self):
+ """(3,) float : The scale for this node.
+ """
+ return self._scale
+
+ @scale.setter
+ def scale(self, value):
+ value = np.asanyarray(value)
+ if value.shape != (3,):
+ raise ValueError('Scale must be a (3,) vector')
+ self._scale = value
+ self._matrix = None
+
+ @property
+ def matrix(self):
+ """(4,4) float : The homogenous transform matrix for this node.
+
+ Note that this matrix's elements are not settable,
+ it's just a copy of the internal matrix. You can set the whole
+ matrix, but not an individual element.
+ """
+ if self._matrix is None:
+ self._matrix = self._m_from_tqs(
+ self.translation, self.rotation, self.scale
+ )
+ return self._matrix.copy()
+
+ @matrix.setter
+ def matrix(self, value):
+ value = np.asanyarray(value)
+ if value.shape != (4,4):
+ raise ValueError('Matrix must be a 4x4 numpy ndarray')
+ if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])):
+ raise ValueError('Bottom row of matrix must be [0,0,0,1]')
+ self.rotation = Node._q_from_m(value)
+ self.scale = Node._s_from_m(value)
+ self.translation = Node._t_from_m(value)
+ self._matrix = value
+
+ @staticmethod
+ def _t_from_m(m):
+ return m[:3,3]
+
+ @staticmethod
+ def _r_from_m(m):
+ U = m[:3,:3]
+ norms = np.linalg.norm(U.T, axis=1)
+ return U / norms
+
+ @staticmethod
+ def _q_from_m(m):
+ M = np.eye(4)
+ M[:3,:3] = Node._r_from_m(m)
+ q_wxyz = transformations.quaternion_from_matrix(M)
+ return np.roll(q_wxyz, -1)
+
+ @staticmethod
+ def _s_from_m(m):
+ return np.linalg.norm(m[:3,:3].T, axis=1)
+
+ @staticmethod
+ def _r_from_q(q):
+ q_wxyz = np.roll(q, 1)
+ return transformations.quaternion_matrix(q_wxyz)[:3,:3]
+
+ @staticmethod
+ def _m_from_tqs(t, q, s):
+ S = np.eye(4)
+ S[:3,:3] = np.diag(s)
+
+ R = np.eye(4)
+ R[:3,:3] = Node._r_from_q(q)
+
+ T = np.eye(4)
+ T[:3,3] = t
+
+ return T.dot(R.dot(S))
diff --git a/pyrender/pyrender/offscreen.py b/pyrender/pyrender/offscreen.py
new file mode 100644
index 0000000000000000000000000000000000000000..340142983006cdc6f51b6d114e9b2b294aa4a919
--- /dev/null
+++ b/pyrender/pyrender/offscreen.py
@@ -0,0 +1,160 @@
+"""Wrapper for offscreen rendering.
+
+Author: Matthew Matl
+"""
+import os
+
+from .renderer import Renderer
+from .constants import RenderFlags
+
+
+class OffscreenRenderer(object):
+ """A wrapper for offscreen rendering.
+
+ Parameters
+ ----------
+ viewport_width : int
+ The width of the main viewport, in pixels.
+ viewport_height : int
+ The height of the main viewport, in pixels.
+ point_size : float
+ The size of screen-space points in pixels.
+ """
+
+ def __init__(self, viewport_width, viewport_height, point_size=1.0):
+ self.viewport_width = viewport_width
+ self.viewport_height = viewport_height
+ self.point_size = point_size
+
+ self._platform = None
+ self._renderer = None
+ self._create()
+
+ @property
+ def viewport_width(self):
+ """int : The width of the main viewport, in pixels.
+ """
+ return self._viewport_width
+
+ @viewport_width.setter
+ def viewport_width(self, value):
+ self._viewport_width = int(value)
+
+ @property
+ def viewport_height(self):
+ """int : The height of the main viewport, in pixels.
+ """
+ return self._viewport_height
+
+ @viewport_height.setter
+ def viewport_height(self, value):
+ self._viewport_height = int(value)
+
+ @property
+ def point_size(self):
+ """float : The pixel size of points in point clouds.
+ """
+ return self._point_size
+
+ @point_size.setter
+ def point_size(self, value):
+ self._point_size = float(value)
+
+ def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None):
+ """Render a scene with the given set of flags.
+
+ Parameters
+ ----------
+ scene : :class:`Scene`
+ A scene to render.
+ flags : int
+ A bitwise or of one or more flags from :class:`.RenderFlags`.
+ seg_node_map : dict
+ A map from :class:`.Node` objects to (3,) colors for each.
+ If specified along with flags set to :attr:`.RenderFlags.SEG`,
+ the color image will be a segmentation image.
+
+ Returns
+ -------
+ color_im : (h, w, 3) uint8 or (h, w, 4) uint8
+ The color buffer in RGB format, or in RGBA format if
+ :attr:`.RenderFlags.RGBA` is set.
+ Not returned if flags includes :attr:`.RenderFlags.DEPTH_ONLY`.
+ depth_im : (h, w) float32
+ The depth buffer in linear units.
+ """
+ self._platform.make_current()
+ # If platform does not support dynamically-resizing framebuffers,
+ # destroy it and restart it
+ if (self._platform.viewport_height != self.viewport_height or
+ self._platform.viewport_width != self.viewport_width):
+ if not self._platform.supports_framebuffers():
+ self.delete()
+ self._create()
+
+ self._platform.make_current()
+ self._renderer.viewport_width = self.viewport_width
+ self._renderer.viewport_height = self.viewport_height
+ self._renderer.point_size = self.point_size
+
+ if self._platform.supports_framebuffers():
+ flags |= RenderFlags.OFFSCREEN
+ retval = self._renderer.render(scene, flags, seg_node_map)
+ else:
+ self._renderer.render(scene, flags, seg_node_map)
+ depth = self._renderer.read_depth_buf()
+ if flags & RenderFlags.DEPTH_ONLY:
+ retval = depth
+ else:
+ color = self._renderer.read_color_buf()
+ retval = color, depth
+
+ # Make the platform not current
+ self._platform.make_uncurrent()
+ return retval
+
+ def delete(self):
+ """Free all OpenGL resources.
+ """
+ self._platform.make_current()
+ self._renderer.delete()
+ self._platform.delete_context()
+ del self._renderer
+ del self._platform
+ self._renderer = None
+ self._platform = None
+ import gc
+ gc.collect()
+
+ def _create(self):
+ if 'PYOPENGL_PLATFORM' not in os.environ:
+ from pyrender.platforms.pyglet_platform import PygletPlatform
+ self._platform = PygletPlatform(self.viewport_width,
+ self.viewport_height)
+ elif os.environ['PYOPENGL_PLATFORM'] == 'egl':
+ from pyrender.platforms import egl
+ device_id = int(os.environ.get('EGL_DEVICE_ID', '0'))
+ egl_device = egl.get_device_by_index(device_id)
+ self._platform = egl.EGLPlatform(self.viewport_width,
+ self.viewport_height,
+ device=egl_device)
+ elif os.environ['PYOPENGL_PLATFORM'] == 'osmesa':
+ from pyrender.platforms.osmesa import OSMesaPlatform
+ self._platform = OSMesaPlatform(self.viewport_width,
+ self.viewport_height)
+ else:
+ raise ValueError('Unsupported PyOpenGL platform: {}'.format(
+ os.environ['PYOPENGL_PLATFORM']
+ ))
+ self._platform.init_context()
+ self._platform.make_current()
+ self._renderer = Renderer(self.viewport_width, self.viewport_height)
+
+ def __del__(self):
+ try:
+ self.delete()
+ except Exception:
+ pass
+
+
+__all__ = ['OffscreenRenderer']
diff --git a/pyrender/pyrender/platforms/__init__.py b/pyrender/pyrender/platforms/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7837fd5fdeccab5e48c85e41d20b238ea7396599
--- /dev/null
+++ b/pyrender/pyrender/platforms/__init__.py
@@ -0,0 +1,6 @@
+"""Platforms for generating offscreen OpenGL contexts for rendering.
+
+Author: Matthew Matl
+"""
+
+from .base import Platform
diff --git a/pyrender/pyrender/platforms/base.py b/pyrender/pyrender/platforms/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9ecda906145e239737901809aa59db8d3e231c6
--- /dev/null
+++ b/pyrender/pyrender/platforms/base.py
@@ -0,0 +1,76 @@
+import abc
+
+import six
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Platform(object):
+ """Base class for all OpenGL platforms.
+
+ Parameters
+ ----------
+ viewport_width : int
+ The width of the main viewport, in pixels.
+ viewport_height : int
+ The height of the main viewport, in pixels
+ """
+
+ def __init__(self, viewport_width, viewport_height):
+ self.viewport_width = viewport_width
+ self.viewport_height = viewport_height
+
+ @property
+ def viewport_width(self):
+ """int : The width of the main viewport, in pixels.
+ """
+ return self._viewport_width
+
+ @viewport_width.setter
+ def viewport_width(self, value):
+ self._viewport_width = value
+
+ @property
+ def viewport_height(self):
+ """int : The height of the main viewport, in pixels.
+ """
+ return self._viewport_height
+
+ @viewport_height.setter
+ def viewport_height(self, value):
+ self._viewport_height = value
+
+ @abc.abstractmethod
+ def init_context(self):
+ """Create an OpenGL context.
+ """
+ pass
+
+ @abc.abstractmethod
+ def make_current(self):
+ """Make the OpenGL context current.
+ """
+ pass
+
+ @abc.abstractmethod
+ def make_uncurrent(self):
+ """Make the OpenGL context uncurrent.
+ """
+ pass
+
+ @abc.abstractmethod
+ def delete_context(self):
+ """Delete the OpenGL context.
+ """
+ pass
+
+ @abc.abstractmethod
+ def supports_framebuffers(self):
+ """Returns True if the method supports framebuffer rendering.
+ """
+ pass
+
+ def __del__(self):
+ try:
+ self.delete_context()
+ except Exception:
+ pass
diff --git a/pyrender/pyrender/platforms/egl.py b/pyrender/pyrender/platforms/egl.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae2478d29c9a538c53ad83fa31f8e2277cd897c8
--- /dev/null
+++ b/pyrender/pyrender/platforms/egl.py
@@ -0,0 +1,219 @@
+import ctypes
+import os
+
+import OpenGL.platform
+
+from .base import Platform
+
+EGL_PLATFORM_DEVICE_EXT = 0x313F
+EGL_DRM_DEVICE_FILE_EXT = 0x3233
+
+
+def _ensure_egl_loaded():
+ plugin = OpenGL.platform.PlatformPlugin.by_name('egl')
+ if plugin is None:
+ raise RuntimeError("EGL platform plugin is not available.")
+
+ plugin_class = plugin.load()
+ plugin.loaded = True
+ # create instance of this platform implementation
+ plugin = plugin_class()
+
+ plugin.install(vars(OpenGL.platform))
+
+
+_ensure_egl_loaded()
+from OpenGL import EGL as egl
+
+
+def _get_egl_func(func_name, res_type, *arg_types):
+ address = egl.eglGetProcAddress(func_name)
+ if address is None:
+ return None
+
+ proto = ctypes.CFUNCTYPE(res_type)
+ proto.argtypes = arg_types
+ func = proto(address)
+ return func
+
+
+def _get_egl_struct(struct_name):
+ from OpenGL._opaque import opaque_pointer_cls
+ return opaque_pointer_cls(struct_name)
+
+
+# These are not defined in PyOpenGL by default.
+_EGLDeviceEXT = _get_egl_struct('EGLDeviceEXT')
+_eglGetPlatformDisplayEXT = _get_egl_func('eglGetPlatformDisplayEXT', egl.EGLDisplay)
+_eglQueryDevicesEXT = _get_egl_func('eglQueryDevicesEXT', egl.EGLBoolean)
+_eglQueryDeviceStringEXT = _get_egl_func('eglQueryDeviceStringEXT', ctypes.c_char_p)
+
+
+def query_devices():
+ if _eglQueryDevicesEXT is None:
+ raise RuntimeError("EGL query extension is not loaded or is not supported.")
+
+ num_devices = egl.EGLint()
+ success = _eglQueryDevicesEXT(0, None, ctypes.pointer(num_devices))
+ if not success or num_devices.value < 1:
+ return []
+
+ devices = (_EGLDeviceEXT * num_devices.value)() # array of size num_devices
+ success = _eglQueryDevicesEXT(num_devices.value, devices, ctypes.pointer(num_devices))
+ if not success or num_devices.value < 1:
+ return []
+
+ return [EGLDevice(devices[i]) for i in range(num_devices.value)]
+
+
+def get_default_device():
+ # Fall back to not using query extension.
+ if _eglQueryDevicesEXT is None:
+ return EGLDevice(None)
+
+ return query_devices()[0]
+
+
+def get_device_by_index(device_id):
+ if _eglQueryDevicesEXT is None and device_id == 0:
+ return get_default_device()
+
+ devices = query_devices()
+ if device_id >= len(devices):
+ raise ValueError('Invalid device ID ({})'.format(device_id, len(devices)))
+ return devices[device_id]
+
+
+class EGLDevice:
+
+ def __init__(self, display=None):
+ self._display = display
+
+ def get_display(self):
+ if self._display is None:
+ return egl.eglGetDisplay(egl.EGL_DEFAULT_DISPLAY)
+
+ return _eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, self._display, None)
+
+ @property
+ def name(self):
+ if self._display is None:
+ return 'default'
+
+ name = _eglQueryDeviceStringEXT(self._display, EGL_DRM_DEVICE_FILE_EXT)
+ if name is None:
+ return None
+
+ return name.decode('ascii')
+
+ def __repr__(self):
+ return "".format(self.name)
+
+
+class EGLPlatform(Platform):
+ """Renders using EGL.
+ """
+
+ def __init__(self, viewport_width, viewport_height, device: EGLDevice = None):
+ super(EGLPlatform, self).__init__(viewport_width, viewport_height)
+ if device is None:
+ device = get_default_device()
+
+ self._egl_device = device
+ self._egl_display = None
+ self._egl_context = None
+
+ def init_context(self):
+ _ensure_egl_loaded()
+
+ from OpenGL.EGL import (
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE,
+ EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_DEPTH_SIZE,
+ EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_CONFORMANT,
+ EGL_NONE, EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT,
+ EGL_OPENGL_API, EGL_CONTEXT_MAJOR_VERSION,
+ EGL_CONTEXT_MINOR_VERSION,
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
+ EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+ eglGetDisplay, eglInitialize, eglChooseConfig,
+ eglBindAPI, eglCreateContext, EGLConfig
+ )
+ from OpenGL import arrays
+
+ config_attributes = arrays.GLintArray.asArray([
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_BLUE_SIZE, 8,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_DEPTH_SIZE, 24,
+ EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
+ EGL_CONFORMANT, EGL_OPENGL_BIT,
+ EGL_NONE
+ ])
+ context_attributes = arrays.GLintArray.asArray([
+ EGL_CONTEXT_MAJOR_VERSION, 4,
+ EGL_CONTEXT_MINOR_VERSION, 1,
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
+ EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+ EGL_NONE
+ ])
+ major, minor = ctypes.c_long(), ctypes.c_long()
+ num_configs = ctypes.c_long()
+ configs = (EGLConfig * 1)()
+
+ # Cache DISPLAY if necessary and get an off-screen EGL display
+ orig_dpy = None
+ if 'DISPLAY' in os.environ:
+ orig_dpy = os.environ['DISPLAY']
+ del os.environ['DISPLAY']
+
+ self._egl_display = self._egl_device.get_display()
+ if orig_dpy is not None:
+ os.environ['DISPLAY'] = orig_dpy
+
+ # Initialize EGL
+ assert eglInitialize(self._egl_display, major, minor)
+ assert eglChooseConfig(
+ self._egl_display, config_attributes, configs, 1, num_configs
+ )
+
+ # Bind EGL to the OpenGL API
+ assert eglBindAPI(EGL_OPENGL_API)
+
+ # Create an EGL context
+ self._egl_context = eglCreateContext(
+ self._egl_display, configs[0],
+ EGL_NO_CONTEXT, context_attributes
+ )
+
+ # Make it current
+ self.make_current()
+
+ def make_current(self):
+ from OpenGL.EGL import eglMakeCurrent, EGL_NO_SURFACE
+ assert eglMakeCurrent(
+ self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ self._egl_context
+ )
+
+ def make_uncurrent(self):
+ """Make the OpenGL context uncurrent.
+ """
+ pass
+
+ def delete_context(self):
+ from OpenGL.EGL import eglDestroyContext, eglTerminate
+ if self._egl_display is not None:
+ if self._egl_context is not None:
+ eglDestroyContext(self._egl_display, self._egl_context)
+ self._egl_context = None
+ eglTerminate(self._egl_display)
+ self._egl_display = None
+
+ def supports_framebuffers(self):
+ return True
+
+
+__all__ = ['EGLPlatform']
diff --git a/pyrender/pyrender/platforms/osmesa.py b/pyrender/pyrender/platforms/osmesa.py
new file mode 100644
index 0000000000000000000000000000000000000000..deaa5ff44031a107883913ae9a18fc425d650f3d
--- /dev/null
+++ b/pyrender/pyrender/platforms/osmesa.py
@@ -0,0 +1,59 @@
+from .base import Platform
+
+
+__all__ = ['OSMesaPlatform']
+
+
+class OSMesaPlatform(Platform):
+ """Renders into a software buffer using OSMesa. Requires special versions
+ of OSMesa to be installed, plus PyOpenGL upgrade.
+ """
+
+ def __init__(self, viewport_width, viewport_height):
+ super(OSMesaPlatform, self).__init__(viewport_width, viewport_height)
+ self._context = None
+ self._buffer = None
+
+ def init_context(self):
+ from OpenGL import arrays
+ from OpenGL.osmesa import (
+ OSMesaCreateContextAttribs, OSMESA_FORMAT,
+ OSMESA_RGBA, OSMESA_PROFILE, OSMESA_CORE_PROFILE,
+ OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION,
+ OSMESA_DEPTH_BITS
+ )
+
+ attrs = arrays.GLintArray.asArray([
+ OSMESA_FORMAT, OSMESA_RGBA,
+ OSMESA_DEPTH_BITS, 24,
+ OSMESA_PROFILE, OSMESA_CORE_PROFILE,
+ OSMESA_CONTEXT_MAJOR_VERSION, 3,
+ OSMESA_CONTEXT_MINOR_VERSION, 3,
+ 0
+ ])
+ self._context = OSMesaCreateContextAttribs(attrs, None)
+ self._buffer = arrays.GLubyteArray.zeros(
+ (self.viewport_height, self.viewport_width, 4)
+ )
+
+ def make_current(self):
+ from OpenGL import GL as gl
+ from OpenGL.osmesa import OSMesaMakeCurrent
+ assert(OSMesaMakeCurrent(
+ self._context, self._buffer, gl.GL_UNSIGNED_BYTE,
+ self.viewport_width, self.viewport_height
+ ))
+
+ def make_uncurrent(self):
+ """Make the OpenGL context uncurrent.
+ """
+ pass
+
+ def delete_context(self):
+ from OpenGL.osmesa import OSMesaDestroyContext
+ OSMesaDestroyContext(self._context)
+ self._context = None
+ self._buffer = None
+
+ def supports_framebuffers(self):
+ return False
diff --git a/pyrender/pyrender/platforms/pyglet_platform.py b/pyrender/pyrender/platforms/pyglet_platform.py
new file mode 100644
index 0000000000000000000000000000000000000000..a70cf7b659bc85a92f6c9c8ebcc360662a068507
--- /dev/null
+++ b/pyrender/pyrender/platforms/pyglet_platform.py
@@ -0,0 +1,90 @@
+from pyrender.constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR,
+ MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR)
+from .base import Platform
+
+import OpenGL
+
+
+__all__ = ['PygletPlatform']
+
+
+class PygletPlatform(Platform):
+ """Renders on-screen using a 1x1 hidden Pyglet window for getting
+ an OpenGL context.
+ """
+
+ def __init__(self, viewport_width, viewport_height):
+ super(PygletPlatform, self).__init__(viewport_width, viewport_height)
+ self._window = None
+
+ def init_context(self):
+ import pyglet
+ pyglet.options['shadow_window'] = False
+
+ try:
+ pyglet.lib.x11.xlib.XInitThreads()
+ except Exception:
+ pass
+
+ self._window = None
+ confs = [pyglet.gl.Config(sample_buffers=1, samples=4,
+ depth_size=24,
+ double_buffer=True,
+ major_version=TARGET_OPEN_GL_MAJOR,
+ minor_version=TARGET_OPEN_GL_MINOR),
+ pyglet.gl.Config(depth_size=24,
+ double_buffer=True,
+ major_version=TARGET_OPEN_GL_MAJOR,
+ minor_version=TARGET_OPEN_GL_MINOR),
+ pyglet.gl.Config(sample_buffers=1, samples=4,
+ depth_size=24,
+ double_buffer=True,
+ major_version=MIN_OPEN_GL_MAJOR,
+ minor_version=MIN_OPEN_GL_MINOR),
+ pyglet.gl.Config(depth_size=24,
+ double_buffer=True,
+ major_version=MIN_OPEN_GL_MAJOR,
+ minor_version=MIN_OPEN_GL_MINOR)]
+ for conf in confs:
+ try:
+ self._window = pyglet.window.Window(config=conf, visible=False,
+ resizable=False,
+ width=1, height=1)
+ break
+ except pyglet.window.NoSuchConfigException as e:
+ pass
+
+ if not self._window:
+ raise ValueError(
+ 'Failed to initialize Pyglet window with an OpenGL >= 3+ '
+ 'context. If you\'re logged in via SSH, ensure that you\'re '
+ 'running your script with vglrun (i.e. VirtualGL). The '
+ 'internal error message was "{}"'.format(e)
+ )
+
+ def make_current(self):
+ if self._window:
+ self._window.switch_to()
+
+ def make_uncurrent(self):
+ try:
+ import pyglet
+ pyglet.gl.xlib.glx.glXMakeContextCurrent(self._window.context.x_display, 0, 0, None)
+ except Exception:
+ pass
+
+ def delete_context(self):
+ if self._window is not None:
+ self.make_current()
+ cid = OpenGL.contextdata.getContext()
+ try:
+ self._window.context.destroy()
+ self._window.close()
+ except Exception:
+ pass
+ self._window = None
+ OpenGL.contextdata.cleanupContext(cid)
+ del cid
+
+ def supports_framebuffers(self):
+ return True
diff --git a/pyrender/pyrender/primitive.py b/pyrender/pyrender/primitive.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f83f46f532b126a4573e715dd03d079fef755ca
--- /dev/null
+++ b/pyrender/pyrender/primitive.py
@@ -0,0 +1,489 @@
+"""Primitives, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-primitive
+
+Author: Matthew Matl
+"""
+import numpy as np
+
+from OpenGL.GL import *
+
+from .material import Material, MetallicRoughnessMaterial
+from .constants import FLOAT_SZ, UINT_SZ, BufFlags, GLTF
+from .utils import format_color_array
+
+
+class Primitive(object):
+ """A primitive object which can be rendered.
+
+ Parameters
+ ----------
+ positions : (n, 3) float
+ XYZ vertex positions.
+ normals : (n, 3) float
+ Normalized XYZ vertex normals.
+ tangents : (n, 4) float
+ XYZW vertex tangents where the w component is a sign value
+ (either +1 or -1) indicating the handedness of the tangent basis.
+ texcoord_0 : (n, 2) float
+ The first set of UV texture coordinates.
+ texcoord_1 : (n, 2) float
+ The second set of UV texture coordinates.
+ color_0 : (n, 4) float
+ RGBA vertex colors.
+ joints_0 : (n, 4) float
+ Joint information.
+ weights_0 : (n, 4) float
+ Weight information for morphing.
+ indices : (m, 3) int
+ Face indices for triangle meshes or fans.
+ material : :class:`Material`
+ The material to apply to this primitive when rendering.
+ mode : int
+ The type of primitives to render, one of the following:
+
+ - ``0``: POINTS
+ - ``1``: LINES
+ - ``2``: LINE_LOOP
+ - ``3``: LINE_STRIP
+ - ``4``: TRIANGLES
+ - ``5``: TRIANGLES_STRIP
+ - ``6``: TRIANGLES_FAN
+ targets : (k,) int
+ Morph target indices.
+ poses : (x,4,4), float
+ Array of 4x4 transformation matrices for instancing this object.
+ """
+
+ def __init__(self,
+ positions,
+ normals=None,
+ tangents=None,
+ texcoord_0=None,
+ texcoord_1=None,
+ color_0=None,
+ joints_0=None,
+ weights_0=None,
+ indices=None,
+ material=None,
+ mode=None,
+ targets=None,
+ poses=None):
+
+ if mode is None:
+ mode = GLTF.TRIANGLES
+
+ self.positions = positions
+ self.normals = normals
+ self.tangents = tangents
+ self.texcoord_0 = texcoord_0
+ self.texcoord_1 = texcoord_1
+ self.color_0 = color_0
+ self.joints_0 = joints_0
+ self.weights_0 = weights_0
+ self.indices = indices
+ self.material = material
+ self.mode = mode
+ self.targets = targets
+ self.poses = poses
+
+ self._bounds = None
+ self._vaid = None
+ self._buffers = []
+ self._is_transparent = None
+ self._buf_flags = None
+
+ @property
+ def positions(self):
+ """(n,3) float : XYZ vertex positions.
+ """
+ return self._positions
+
+ @positions.setter
+ def positions(self, value):
+ value = np.asanyarray(value, dtype=np.float32)
+ self._positions = np.ascontiguousarray(value)
+ self._bounds = None
+
+ @property
+ def normals(self):
+ """(n,3) float : Normalized XYZ vertex normals.
+ """
+ return self._normals
+
+ @normals.setter
+ def normals(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ if value.shape != self.positions.shape:
+ raise ValueError('Incorrect normals shape')
+ self._normals = value
+
+ @property
+ def tangents(self):
+ """(n,4) float : XYZW vertex tangents.
+ """
+ return self._tangents
+
+ @tangents.setter
+ def tangents(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ if value.shape != (self.positions.shape[0], 4):
+ raise ValueError('Incorrect tangent shape')
+ self._tangents = value
+
+ @property
+ def texcoord_0(self):
+ """(n,2) float : The first set of UV texture coordinates.
+ """
+ return self._texcoord_0
+
+ @texcoord_0.setter
+ def texcoord_0(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or
+ value.shape[1] < 2):
+ raise ValueError('Incorrect texture coordinate shape')
+ if value.shape[1] > 2:
+ value = value[:,:2]
+ self._texcoord_0 = value
+
+ @property
+ def texcoord_1(self):
+ """(n,2) float : The second set of UV texture coordinates.
+ """
+ return self._texcoord_1
+
+ @texcoord_1.setter
+ def texcoord_1(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or
+ value.shape[1] != 2):
+ raise ValueError('Incorrect texture coordinate shape')
+ self._texcoord_1 = value
+
+ @property
+ def color_0(self):
+ """(n,4) float : RGBA vertex colors.
+ """
+ return self._color_0
+
+ @color_0.setter
+ def color_0(self, value):
+ if value is not None:
+ value = np.ascontiguousarray(
+ format_color_array(value, shape=(len(self.positions), 4))
+ )
+ self._is_transparent = None
+ self._color_0 = value
+
+ @property
+ def joints_0(self):
+ """(n,4) float : Joint information.
+ """
+ return self._joints_0
+
+ @joints_0.setter
+ def joints_0(self, value):
+ self._joints_0 = value
+
+ @property
+ def weights_0(self):
+ """(n,4) float : Weight information for morphing.
+ """
+ return self._weights_0
+
+ @weights_0.setter
+ def weights_0(self, value):
+ self._weights_0 = value
+
+ @property
+ def indices(self):
+ """(m,3) int : Face indices for triangle meshes or fans.
+ """
+ return self._indices
+
+ @indices.setter
+ def indices(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ self._indices = value
+
+ @property
+ def material(self):
+ """:class:`Material` : The material for this primitive.
+ """
+ return self._material
+
+ @material.setter
+ def material(self, value):
+ # Create default material
+ if value is None:
+ value = MetallicRoughnessMaterial()
+ else:
+ if not isinstance(value, Material):
+ raise TypeError('Object material must be of type Material')
+ self._material = value
+
+ @property
+ def mode(self):
+ """int : The type of primitive to render.
+ """
+ return self._mode
+
+ @mode.setter
+ def mode(self, value):
+ value = int(value)
+ if value < GLTF.POINTS or value > GLTF.TRIANGLE_FAN:
+ raise ValueError('Invalid mode')
+ self._mode = value
+
+ @property
+ def targets(self):
+ """(k,) int : Morph target indices.
+ """
+ return self._targets
+
+ @targets.setter
+ def targets(self, value):
+ self._targets = value
+
+ @property
+ def poses(self):
+ """(x,4,4) float : Homogenous transforms for instancing this primitive.
+ """
+ return self._poses
+
+ @poses.setter
+ def poses(self, value):
+ if value is not None:
+ value = np.asanyarray(value, dtype=np.float32)
+ value = np.ascontiguousarray(value)
+ if value.ndim == 2:
+ value = value[np.newaxis,:,:]
+ if value.shape[1] != 4 or value.shape[2] != 4:
+ raise ValueError('Pose matrices must be of shape (n,4,4), '
+ 'got {}'.format(value.shape))
+ self._poses = value
+ self._bounds = None
+
+ @property
+ def bounds(self):
+ if self._bounds is None:
+ self._bounds = self._compute_bounds()
+ return self._bounds
+
+ @property
+ def centroid(self):
+ """(3,) float : The centroid of the primitive's AABB.
+ """
+ return np.mean(self.bounds, axis=0)
+
+ @property
+ def extents(self):
+ """(3,) float : The lengths of the axes of the primitive's AABB.
+ """
+ return np.diff(self.bounds, axis=0).reshape(-1)
+
+ @property
+ def scale(self):
+ """(3,) float : The length of the diagonal of the primitive's AABB.
+ """
+ return np.linalg.norm(self.extents)
+
+ @property
+ def buf_flags(self):
+ """int : The flags for the render buffer.
+ """
+ if self._buf_flags is None:
+ self._buf_flags = self._compute_buf_flags()
+ return self._buf_flags
+
+ def delete(self):
+ self._unbind()
+ self._remove_from_context()
+
+ @property
+ def is_transparent(self):
+ """bool : If True, the mesh is partially-transparent.
+ """
+ return self._compute_transparency()
+
+ def _add_to_context(self):
+ if self._vaid is not None:
+ raise ValueError('Mesh is already bound to a context')
+
+ # Generate and bind VAO
+ self._vaid = glGenVertexArrays(1)
+ glBindVertexArray(self._vaid)
+
+ #######################################################################
+ # Fill vertex buffer
+ #######################################################################
+
+ # Generate and bind vertex buffer
+ vertexbuffer = glGenBuffers(1)
+ self._buffers.append(vertexbuffer)
+ glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer)
+
+ # positions
+ vertex_data = self.positions
+ attr_sizes = [3]
+
+ # Normals
+ if self.normals is not None:
+ vertex_data = np.hstack((vertex_data, self.normals))
+ attr_sizes.append(3)
+
+ # Tangents
+ if self.tangents is not None:
+ vertex_data = np.hstack((vertex_data, self.tangents))
+ attr_sizes.append(4)
+
+ # Texture Coordinates
+ if self.texcoord_0 is not None:
+ vertex_data = np.hstack((vertex_data, self.texcoord_0))
+ attr_sizes.append(2)
+ if self.texcoord_1 is not None:
+ vertex_data = np.hstack((vertex_data, self.texcoord_1))
+ attr_sizes.append(2)
+
+ # Color
+ if self.color_0 is not None:
+ vertex_data = np.hstack((vertex_data, self.color_0))
+ attr_sizes.append(4)
+
+ # TODO JOINTS AND WEIGHTS
+ # PASS
+
+ # Copy data to buffer
+ vertex_data = np.ascontiguousarray(
+ vertex_data.flatten().astype(np.float32)
+ )
+ glBufferData(
+ GL_ARRAY_BUFFER, FLOAT_SZ * len(vertex_data),
+ vertex_data, GL_STATIC_DRAW
+ )
+ total_sz = sum(attr_sizes)
+ offset = 0
+ for i, sz in enumerate(attr_sizes):
+ glVertexAttribPointer(
+ i, sz, GL_FLOAT, GL_FALSE, FLOAT_SZ * total_sz,
+ ctypes.c_void_p(FLOAT_SZ * offset)
+ )
+ glEnableVertexAttribArray(i)
+ offset += sz
+
+ #######################################################################
+ # Fill model matrix buffer
+ #######################################################################
+
+ if self.poses is not None:
+ pose_data = np.ascontiguousarray(
+ np.transpose(self.poses, [0,2,1]).flatten().astype(np.float32)
+ )
+ else:
+ pose_data = np.ascontiguousarray(
+ np.eye(4).flatten().astype(np.float32)
+ )
+
+ modelbuffer = glGenBuffers(1)
+ self._buffers.append(modelbuffer)
+ glBindBuffer(GL_ARRAY_BUFFER, modelbuffer)
+ glBufferData(
+ GL_ARRAY_BUFFER, FLOAT_SZ * len(pose_data),
+ pose_data, GL_STATIC_DRAW
+ )
+
+ for i in range(0, 4):
+ idx = i + len(attr_sizes)
+ glEnableVertexAttribArray(idx)
+ glVertexAttribPointer(
+ idx, 4, GL_FLOAT, GL_FALSE, FLOAT_SZ * 4 * 4,
+ ctypes.c_void_p(4 * FLOAT_SZ * i)
+ )
+ glVertexAttribDivisor(idx, 1)
+
+ #######################################################################
+ # Fill element buffer
+ #######################################################################
+ if self.indices is not None:
+ elementbuffer = glGenBuffers(1)
+ self._buffers.append(elementbuffer)
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer)
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, UINT_SZ * self.indices.size,
+ self.indices.flatten().astype(np.uint32),
+ GL_STATIC_DRAW)
+
+ glBindVertexArray(0)
+
+ def _remove_from_context(self):
+ if self._vaid is not None:
+ glDeleteVertexArrays(1, [self._vaid])
+ glDeleteBuffers(len(self._buffers), self._buffers)
+ self._vaid = None
+ self._buffers = []
+
+ def _in_context(self):
+ return self._vaid is not None
+
+ def _bind(self):
+ if self._vaid is None:
+ raise ValueError('Cannot bind a Mesh that has not been added '
+ 'to a context')
+ glBindVertexArray(self._vaid)
+
+ def _unbind(self):
+ glBindVertexArray(0)
+
+ def _compute_bounds(self):
+ """Compute the bounds of this object.
+ """
+ # Compute bounds of this object
+ bounds = np.array([np.min(self.positions, axis=0),
+ np.max(self.positions, axis=0)])
+
+ # If instanced, compute translations for approximate bounds
+ if self.poses is not None:
+ bounds += np.array([np.min(self.poses[:,:3,3], axis=0),
+ np.max(self.poses[:,:3,3], axis=0)])
+ return bounds
+
+ def _compute_transparency(self):
+ """Compute whether or not this object is transparent.
+ """
+ if self.material.is_transparent:
+ return True
+ if self._is_transparent is None:
+ self._is_transparent = False
+ if self.color_0 is not None:
+ if np.any(self._color_0[:,3] != 1.0):
+ self._is_transparent = True
+ return self._is_transparent
+
+ def _compute_buf_flags(self):
+ buf_flags = BufFlags.POSITION
+
+ if self.normals is not None:
+ buf_flags |= BufFlags.NORMAL
+ if self.tangents is not None:
+ buf_flags |= BufFlags.TANGENT
+ if self.texcoord_0 is not None:
+ buf_flags |= BufFlags.TEXCOORD_0
+ if self.texcoord_1 is not None:
+ buf_flags |= BufFlags.TEXCOORD_1
+ if self.color_0 is not None:
+ buf_flags |= BufFlags.COLOR_0
+ if self.joints_0 is not None:
+ buf_flags |= BufFlags.JOINTS_0
+ if self.weights_0 is not None:
+ buf_flags |= BufFlags.WEIGHTS_0
+
+ return buf_flags
diff --git a/pyrender/pyrender/renderer.py b/pyrender/pyrender/renderer.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ae14c5cdb1785226a52ae6b71b08f01de069962
--- /dev/null
+++ b/pyrender/pyrender/renderer.py
@@ -0,0 +1,1339 @@
+"""PBR renderer for Python.
+
+Author: Matthew Matl
+"""
+import sys
+
+import numpy as np
+import PIL
+
+from .constants import (RenderFlags, TextAlign, GLTF, BufFlags, TexFlags,
+ ProgramFlags, DEFAULT_Z_FAR, DEFAULT_Z_NEAR,
+ SHADOW_TEX_SZ, MAX_N_LIGHTS)
+from .shader_program import ShaderProgramCache
+from .material import MetallicRoughnessMaterial, SpecularGlossinessMaterial
+from .light import PointLight, SpotLight, DirectionalLight
+from .font import FontCache
+from .utils import format_color_vector
+
+from OpenGL.GL import *
+
+
+class Renderer(object):
+ """Class for handling all rendering operations on a scene.
+
+ Note
+ ----
+ This renderer relies on the existence of an OpenGL context and
+ does not create one on its own.
+
+ Parameters
+ ----------
+ viewport_width : int
+ Width of the viewport in pixels.
+ viewport_height : int
+ Width of the viewport height in pixels.
+ point_size : float, optional
+ Size of points in pixels. Defaults to 1.0.
+ """
+
+ def __init__(self, viewport_width, viewport_height, point_size=1.0):
+ self.dpscale = 1
+ # Scaling needed on retina displays
+ if sys.platform == 'darwin':
+ self.dpscale = 2
+
+ self.viewport_width = viewport_width
+ self.viewport_height = viewport_height
+ self.point_size = point_size
+
+ # Optional framebuffer for offscreen renders
+ self._main_fb = None
+ self._main_cb = None
+ self._main_db = None
+ self._main_fb_ms = None
+ self._main_cb_ms = None
+ self._main_db_ms = None
+ self._main_fb_dims = (None, None)
+ self._shadow_fb = None
+ self._latest_znear = DEFAULT_Z_NEAR
+ self._latest_zfar = DEFAULT_Z_FAR
+
+ # Shader Program Cache
+ self._program_cache = ShaderProgramCache()
+ self._font_cache = FontCache()
+ self._meshes = set()
+ self._mesh_textures = set()
+ self._shadow_textures = set()
+ self._texture_alloc_idx = 0
+
+ @property
+ def viewport_width(self):
+ """int : The width of the main viewport, in pixels.
+ """
+ return self._viewport_width
+
+ @viewport_width.setter
+ def viewport_width(self, value):
+ self._viewport_width = self.dpscale * value
+
+ @property
+ def viewport_height(self):
+ """int : The height of the main viewport, in pixels.
+ """
+ return self._viewport_height
+
+ @viewport_height.setter
+ def viewport_height(self, value):
+ self._viewport_height = self.dpscale * value
+
+ @property
+ def point_size(self):
+ """float : The size of screen-space points, in pixels.
+ """
+ return self._point_size
+
+ @point_size.setter
+ def point_size(self, value):
+ self._point_size = float(value)
+
+ def render(self, scene, flags, seg_node_map=None):
+ """Render a scene with the given set of flags.
+
+ Parameters
+ ----------
+ scene : :class:`Scene`
+ A scene to render.
+ flags : int
+ A specification from :class:`.RenderFlags`.
+ seg_node_map : dict
+ A map from :class:`.Node` objects to (3,) colors for each.
+ If specified along with flags set to :attr:`.RenderFlags.SEG`,
+ the color image will be a segmentation image.
+
+ Returns
+ -------
+ color_im : (h, w, 3) uint8 or (h, w, 4) uint8
+ If :attr:`RenderFlags.OFFSCREEN` is set, the color buffer. This is
+ normally an RGB buffer, but if :attr:`.RenderFlags.RGBA` is set,
+ the buffer will be a full RGBA buffer.
+ depth_im : (h, w) float32
+ If :attr:`RenderFlags.OFFSCREEN` is set, the depth buffer
+ in linear units.
+ """
+ # Update context with meshes and textures
+ self._update_context(scene, flags)
+
+ # Render necessary shadow maps
+ if not bool(flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.SEG):
+ for ln in scene.light_nodes:
+ take_pass = False
+ if (isinstance(ln.light, DirectionalLight) and
+ bool(flags & RenderFlags.SHADOWS_DIRECTIONAL)):
+ take_pass = True
+ elif (isinstance(ln.light, SpotLight) and
+ bool(flags & RenderFlags.SHADOWS_SPOT)):
+ take_pass = True
+ elif (isinstance(ln.light, PointLight) and
+ bool(flags & RenderFlags.SHADOWS_POINT)):
+ take_pass = True
+ if take_pass:
+ self._shadow_mapping_pass(scene, ln, flags)
+
+ # Make forward pass
+ retval = self._forward_pass(scene, flags, seg_node_map=seg_node_map)
+
+ # If necessary, make normals pass
+ if flags & (RenderFlags.VERTEX_NORMALS | RenderFlags.FACE_NORMALS):
+ self._normals_pass(scene, flags)
+
+ # Update camera settings for retrieving depth buffers
+ self._latest_znear = scene.main_camera_node.camera.znear
+ self._latest_zfar = scene.main_camera_node.camera.zfar
+
+ return retval
+
+ def render_text(self, text, x, y, font_name='OpenSans-Regular',
+ font_pt=40, color=None, scale=1.0,
+ align=TextAlign.BOTTOM_LEFT):
+ """Render text into the current viewport.
+
+ Note
+ ----
+ This cannot be done into an offscreen buffer.
+
+ Parameters
+ ----------
+ text : str
+ The text to render.
+ x : int
+ Horizontal pixel location of text.
+ y : int
+ Vertical pixel location of text.
+ font_name : str
+ Name of font, from the ``pyrender/fonts`` folder, or
+ a path to a ``.ttf`` file.
+ font_pt : int
+ Height of the text, in font points.
+ color : (4,) float
+ The color of the text. Default is black.
+ scale : int
+ Scaling factor for text.
+ align : int
+ One of the :class:`TextAlign` options which specifies where the
+ ``x`` and ``y`` parameters lie on the text. For example,
+ :attr:`TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate
+ the position of the bottom-left corner of the textbox.
+ """
+ x *= self.dpscale
+ y *= self.dpscale
+ font_pt *= self.dpscale
+
+ if color is None:
+ color = np.array([0.0, 0.0, 0.0, 1.0])
+ else:
+ color = format_color_vector(color, 4)
+
+ # Set up viewport for render
+ self._configure_forward_pass_viewport(0)
+
+ # Load font
+ font = self._font_cache.get_font(font_name, font_pt)
+ if not font._in_context():
+ font._add_to_context()
+
+ # Load program
+ program = self._get_text_program()
+ program._bind()
+
+ # Set uniforms
+ p = np.eye(4)
+ p[0,0] = 2.0 / self.viewport_width
+ p[0,3] = -1.0
+ p[1,1] = 2.0 / self.viewport_height
+ p[1,3] = -1.0
+ program.set_uniform('projection', p)
+ program.set_uniform('text_color', color)
+
+ # Draw text
+ font.render_string(text, x, y, scale, align)
+
+ def read_color_buf(self):
+ """Read and return the current viewport's color buffer.
+
+ Alpha cannot be computed for an on-screen buffer.
+
+ Returns
+ -------
+ color_im : (h, w, 3) uint8
+ The color buffer in RGB byte format.
+ """
+ # Extract color image from frame buffer
+ width, height = self.viewport_width, self.viewport_height
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
+ glReadBuffer(GL_FRONT)
+ color_buf = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
+
+ # Re-format them into numpy arrays
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
+ color_im = color_im.reshape((height, width, 3))
+ color_im = np.flip(color_im, axis=0)
+
+ # Resize for macos if needed
+ if sys.platform == 'darwin':
+ color_im = self._resize_image(color_im, True)
+
+ return color_im
+
+ def read_depth_buf(self):
+ """Read and return the current viewport's color buffer.
+
+ Returns
+ -------
+ depth_im : (h, w) float32
+ The depth buffer in linear units.
+ """
+ width, height = self.viewport_width, self.viewport_height
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
+ glReadBuffer(GL_FRONT)
+ depth_buf = glReadPixels(
+ 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT
+ )
+
+ depth_im = np.frombuffer(depth_buf, dtype=np.float32)
+ depth_im = depth_im.reshape((height, width))
+ depth_im = np.flip(depth_im, axis=0)
+
+ inf_inds = (depth_im == 1.0)
+ depth_im = 2.0 * depth_im - 1.0
+ z_near, z_far = self._latest_znear, self._latest_zfar
+ noninf = np.logical_not(inf_inds)
+ if z_far is None:
+ depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf])
+ else:
+ depth_im[noninf] = ((2.0 * z_near * z_far) /
+ (z_far + z_near - depth_im[noninf] *
+ (z_far - z_near)))
+ depth_im[inf_inds] = 0.0
+
+ # Resize for macos if needed
+ if sys.platform == 'darwin':
+ depth_im = self._resize_image(depth_im)
+
+ return depth_im
+
+ def delete(self):
+ """Free all allocated OpenGL resources.
+ """
+ # Free shaders
+ self._program_cache.clear()
+
+ # Free fonts
+ self._font_cache.clear()
+
+ # Free meshes
+ for mesh in self._meshes:
+ for p in mesh.primitives:
+ p.delete()
+
+ # Free textures
+ for mesh_texture in self._mesh_textures:
+ mesh_texture.delete()
+
+ for shadow_texture in self._shadow_textures:
+ shadow_texture.delete()
+
+ self._meshes = set()
+ self._mesh_textures = set()
+ self._shadow_textures = set()
+ self._texture_alloc_idx = 0
+
+ self._delete_main_framebuffer()
+ self._delete_shadow_framebuffer()
+
+ def __del__(self):
+ try:
+ self.delete()
+ except Exception:
+ pass
+
+ ###########################################################################
+ # Rendering passes
+ ###########################################################################
+
+ def _forward_pass(self, scene, flags, seg_node_map=None):
+ # Set up viewport for render
+ self._configure_forward_pass_viewport(flags)
+
+ # Clear it
+ if bool(flags & RenderFlags.SEG):
+ glClearColor(0.0, 0.0, 0.0, 1.0)
+ if seg_node_map is None:
+ seg_node_map = {}
+ else:
+ glClearColor(*scene.bg_color)
+
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+
+ if not bool(flags & RenderFlags.SEG):
+ glEnable(GL_MULTISAMPLE)
+ else:
+ glDisable(GL_MULTISAMPLE)
+
+ # Set up camera matrices
+ V, P = self._get_camera_matrices(scene)
+
+ program = None
+ # Now, render each object in sorted order
+ for node in self._sorted_mesh_nodes(scene):
+ mesh = node.mesh
+
+ # Skip the mesh if it's not visible
+ if not mesh.is_visible:
+ continue
+
+ # If SEG, set color
+ if bool(flags & RenderFlags.SEG):
+ if node not in seg_node_map:
+ continue
+ color = seg_node_map[node]
+ if not isinstance(color, (list, tuple, np.ndarray)):
+ color = np.repeat(color, 3)
+ else:
+ color = np.asanyarray(color)
+ color = color / 255.0
+
+ for primitive in mesh.primitives:
+
+ # First, get and bind the appropriate program
+ program = self._get_primitive_program(
+ primitive, flags, ProgramFlags.USE_MATERIAL
+ )
+ program._bind()
+
+ # Set the camera uniforms
+ program.set_uniform('V', V)
+ program.set_uniform('P', P)
+ program.set_uniform(
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
+ )
+ if bool(flags & RenderFlags.SEG):
+ program.set_uniform('color', color)
+
+ # Next, bind the lighting
+ if not (flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.FLAT or
+ flags & RenderFlags.SEG):
+ self._bind_lighting(scene, program, node, flags)
+
+ # Finally, bind and draw the primitive
+ self._bind_and_draw_primitive(
+ primitive=primitive,
+ pose=scene.get_pose(node),
+ program=program,
+ flags=flags
+ )
+ self._reset_active_textures()
+
+ # Unbind the shader and flush the output
+ if program is not None:
+ program._unbind()
+ glFlush()
+
+ # If doing offscreen render, copy result from framebuffer and return
+ if flags & RenderFlags.OFFSCREEN:
+ return self._read_main_framebuffer(scene, flags)
+ else:
+ return
+
+ def _shadow_mapping_pass(self, scene, light_node, flags):
+ light = light_node.light
+
+ # Set up viewport for render
+ self._configure_shadow_mapping_viewport(light, flags)
+
+ # Set up camera matrices
+ V, P = self._get_light_cam_matrices(scene, light_node, flags)
+
+ # Now, render each object in sorted order
+ for node in self._sorted_mesh_nodes(scene):
+ mesh = node.mesh
+
+ # Skip the mesh if it's not visible
+ if not mesh.is_visible:
+ continue
+
+ for primitive in mesh.primitives:
+
+ # First, get and bind the appropriate program
+ program = self._get_primitive_program(
+ primitive, flags, ProgramFlags.NONE
+ )
+ program._bind()
+
+ # Set the camera uniforms
+ program.set_uniform('V', V)
+ program.set_uniform('P', P)
+ program.set_uniform(
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
+ )
+
+ # Finally, bind and draw the primitive
+ self._bind_and_draw_primitive(
+ primitive=primitive,
+ pose=scene.get_pose(node),
+ program=program,
+ flags=RenderFlags.DEPTH_ONLY
+ )
+ self._reset_active_textures()
+
+ # Unbind the shader and flush the output
+ if program is not None:
+ program._unbind()
+ glFlush()
+
+ def _normals_pass(self, scene, flags):
+ # Set up viewport for render
+ self._configure_forward_pass_viewport(flags)
+ program = None
+
+ # Set up camera matrices
+ V, P = self._get_camera_matrices(scene)
+
+ # Now, render each object in sorted order
+ for node in self._sorted_mesh_nodes(scene):
+ mesh = node.mesh
+
+ # Skip the mesh if it's not visible
+ if not mesh.is_visible:
+ continue
+
+ for primitive in mesh.primitives:
+
+ # Skip objects that don't have normals
+ if not primitive.buf_flags & BufFlags.NORMAL:
+ continue
+
+ # First, get and bind the appropriate program
+ pf = ProgramFlags.NONE
+ if flags & RenderFlags.VERTEX_NORMALS:
+ pf = pf | ProgramFlags.VERTEX_NORMALS
+ if flags & RenderFlags.FACE_NORMALS:
+ pf = pf | ProgramFlags.FACE_NORMALS
+ program = self._get_primitive_program(primitive, flags, pf)
+ program._bind()
+
+ # Set the camera uniforms
+ program.set_uniform('V', V)
+ program.set_uniform('P', P)
+ program.set_uniform('normal_magnitude', 0.05 * primitive.scale)
+ program.set_uniform(
+ 'normal_color', np.array([0.1, 0.1, 1.0, 1.0])
+ )
+
+ # Finally, bind and draw the primitive
+ self._bind_and_draw_primitive(
+ primitive=primitive,
+ pose=scene.get_pose(node),
+ program=program,
+ flags=RenderFlags.DEPTH_ONLY
+ )
+ self._reset_active_textures()
+
+ # Unbind the shader and flush the output
+ if program is not None:
+ program._unbind()
+ glFlush()
+
+ ###########################################################################
+ # Handlers for binding uniforms and drawing primitives
+ ###########################################################################
+
+ def _bind_and_draw_primitive(self, primitive, pose, program, flags):
+ # Set model pose matrix
+ program.set_uniform('M', pose)
+
+ # Bind mesh buffers
+ primitive._bind()
+
+ # Bind mesh material
+ if not (flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.SEG):
+ material = primitive.material
+
+ # Bind textures
+ tf = material.tex_flags
+ if tf & TexFlags.NORMAL:
+ self._bind_texture(material.normalTexture,
+ 'material.normal_texture', program)
+ if tf & TexFlags.OCCLUSION:
+ self._bind_texture(material.occlusionTexture,
+ 'material.occlusion_texture', program)
+ if tf & TexFlags.EMISSIVE:
+ self._bind_texture(material.emissiveTexture,
+ 'material.emissive_texture', program)
+ if tf & TexFlags.BASE_COLOR:
+ self._bind_texture(material.baseColorTexture,
+ 'material.base_color_texture', program)
+ if tf & TexFlags.METALLIC_ROUGHNESS:
+ self._bind_texture(material.metallicRoughnessTexture,
+ 'material.metallic_roughness_texture',
+ program)
+ if tf & TexFlags.DIFFUSE:
+ self._bind_texture(material.diffuseTexture,
+ 'material.diffuse_texture', program)
+ if tf & TexFlags.SPECULAR_GLOSSINESS:
+ self._bind_texture(material.specularGlossinessTexture,
+ 'material.specular_glossiness_texture',
+ program)
+
+ # Bind other uniforms
+ b = 'material.{}'
+ program.set_uniform(b.format('emissive_factor'),
+ material.emissiveFactor)
+ if isinstance(material, MetallicRoughnessMaterial):
+ program.set_uniform(b.format('base_color_factor'),
+ material.baseColorFactor)
+ program.set_uniform(b.format('metallic_factor'),
+ material.metallicFactor)
+ program.set_uniform(b.format('roughness_factor'),
+ material.roughnessFactor)
+ elif isinstance(material, SpecularGlossinessMaterial):
+ program.set_uniform(b.format('diffuse_factor'),
+ material.diffuseFactor)
+ program.set_uniform(b.format('specular_factor'),
+ material.specularFactor)
+ program.set_uniform(b.format('glossiness_factor'),
+ material.glossinessFactor)
+
+ # Set blending options
+ if material.alphaMode == 'BLEND':
+ glEnable(GL_BLEND)
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ else:
+ glEnable(GL_BLEND)
+ glBlendFunc(GL_ONE, GL_ZERO)
+
+ # Set wireframe mode
+ wf = material.wireframe
+ if flags & RenderFlags.FLIP_WIREFRAME:
+ wf = not wf
+ if (flags & RenderFlags.ALL_WIREFRAME) or wf:
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
+ else:
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
+
+ # Set culling mode
+ if material.doubleSided or flags & RenderFlags.SKIP_CULL_FACES:
+ glDisable(GL_CULL_FACE)
+ else:
+ glEnable(GL_CULL_FACE)
+ glCullFace(GL_BACK)
+ else:
+ glEnable(GL_CULL_FACE)
+ glEnable(GL_BLEND)
+ glCullFace(GL_BACK)
+ glBlendFunc(GL_ONE, GL_ZERO)
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
+
+ # Set point size if needed
+ glDisable(GL_PROGRAM_POINT_SIZE)
+ if primitive.mode == GLTF.POINTS:
+ glEnable(GL_PROGRAM_POINT_SIZE)
+ glPointSize(self.point_size)
+
+ # Render mesh
+ n_instances = 1
+ if primitive.poses is not None:
+ n_instances = len(primitive.poses)
+
+ if primitive.indices is not None:
+ glDrawElementsInstanced(
+ primitive.mode, primitive.indices.size, GL_UNSIGNED_INT,
+ ctypes.c_void_p(0), n_instances
+ )
+ else:
+ glDrawArraysInstanced(
+ primitive.mode, 0, len(primitive.positions), n_instances
+ )
+
+ # Unbind mesh buffers
+ primitive._unbind()
+
+ def _bind_lighting(self, scene, program, node, flags):
+ """Bind all lighting uniform values for a scene.
+ """
+ max_n_lights = self._compute_max_n_lights(flags)
+
+ n_d = min(len(scene.directional_light_nodes), max_n_lights[0])
+ n_s = min(len(scene.spot_light_nodes), max_n_lights[1])
+ n_p = min(len(scene.point_light_nodes), max_n_lights[2])
+ program.set_uniform('ambient_light', scene.ambient_light)
+ program.set_uniform('n_directional_lights', n_d)
+ program.set_uniform('n_spot_lights', n_s)
+ program.set_uniform('n_point_lights', n_p)
+ plc = 0
+ slc = 0
+ dlc = 0
+
+ light_nodes = scene.light_nodes
+ if (len(scene.directional_light_nodes) > max_n_lights[0] or
+ len(scene.spot_light_nodes) > max_n_lights[1] or
+ len(scene.point_light_nodes) > max_n_lights[2]):
+ light_nodes = self._sorted_nodes_by_distance(
+ scene, scene.light_nodes, node
+ )
+
+ for n in light_nodes:
+ light = n.light
+ pose = scene.get_pose(n)
+ position = pose[:3,3]
+ direction = -pose[:3,2]
+
+ if isinstance(light, PointLight):
+ if plc == max_n_lights[2]:
+ continue
+ b = 'point_lights[{}].'.format(plc)
+ plc += 1
+ shadow = bool(flags & RenderFlags.SHADOWS_POINT)
+ program.set_uniform(b + 'position', position)
+ elif isinstance(light, SpotLight):
+ if slc == max_n_lights[1]:
+ continue
+ b = 'spot_lights[{}].'.format(slc)
+ slc += 1
+ shadow = bool(flags & RenderFlags.SHADOWS_SPOT)
+ las = 1.0 / max(0.001, np.cos(light.innerConeAngle) -
+ np.cos(light.outerConeAngle))
+ lao = -np.cos(light.outerConeAngle) * las
+ program.set_uniform(b + 'direction', direction)
+ program.set_uniform(b + 'position', position)
+ program.set_uniform(b + 'light_angle_scale', las)
+ program.set_uniform(b + 'light_angle_offset', lao)
+ else:
+ if dlc == max_n_lights[0]:
+ continue
+ b = 'directional_lights[{}].'.format(dlc)
+ dlc += 1
+ shadow = bool(flags & RenderFlags.SHADOWS_DIRECTIONAL)
+ program.set_uniform(b + 'direction', direction)
+
+ program.set_uniform(b + 'color', light.color)
+ program.set_uniform(b + 'intensity', light.intensity)
+ # if light.range is not None:
+ # program.set_uniform(b + 'range', light.range)
+ # else:
+ # program.set_uniform(b + 'range', 0)
+
+ if shadow:
+ self._bind_texture(light.shadow_texture,
+ b + 'shadow_map', program)
+ if not isinstance(light, PointLight):
+ V, P = self._get_light_cam_matrices(scene, n, flags)
+ program.set_uniform(b + 'light_matrix', P.dot(V))
+ else:
+ raise NotImplementedError(
+ 'Point light shadows not implemented'
+ )
+
+ def _sorted_mesh_nodes(self, scene):
+ cam_loc = scene.get_pose(scene.main_camera_node)[:3,3]
+ solid_nodes = []
+ trans_nodes = []
+ for node in scene.mesh_nodes:
+ mesh = node.mesh
+ if mesh.is_transparent:
+ trans_nodes.append(node)
+ else:
+ solid_nodes.append(node)
+
+ # TODO BETTER SORTING METHOD
+ trans_nodes.sort(
+ key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc)
+ )
+ solid_nodes.sort(
+ key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc)
+ )
+
+ return solid_nodes + trans_nodes
+
+ def _sorted_nodes_by_distance(self, scene, nodes, compare_node):
+ nodes = list(nodes)
+ compare_posn = scene.get_pose(compare_node)[:3,3]
+ nodes.sort(key=lambda n: np.linalg.norm(
+ scene.get_pose(n)[:3,3] - compare_posn)
+ )
+ return nodes
+
+ ###########################################################################
+ # Context Management
+ ###########################################################################
+
+ def _update_context(self, scene, flags):
+
+ # Update meshes
+ scene_meshes = scene.meshes
+
+ # Add new meshes to context
+ for mesh in scene_meshes - self._meshes:
+ for p in mesh.primitives:
+ p._add_to_context()
+
+ # Remove old meshes from context
+ for mesh in self._meshes - scene_meshes:
+ for p in mesh.primitives:
+ p.delete()
+
+ self._meshes = scene_meshes.copy()
+
+ # Update mesh textures
+ mesh_textures = set()
+ for m in scene_meshes:
+ for p in m.primitives:
+ mesh_textures |= p.material.textures
+
+ # Add new textures to context
+ for texture in mesh_textures - self._mesh_textures:
+ texture._add_to_context()
+
+ # Remove old textures from context
+ for texture in self._mesh_textures - mesh_textures:
+ texture.delete()
+
+ self._mesh_textures = mesh_textures.copy()
+
+ shadow_textures = set()
+ for l in scene.lights:
+ # Create if needed
+ active = False
+ if (isinstance(l, DirectionalLight) and
+ flags & RenderFlags.SHADOWS_DIRECTIONAL):
+ active = True
+ elif (isinstance(l, PointLight) and
+ flags & RenderFlags.SHADOWS_POINT):
+ active = True
+ elif isinstance(l, SpotLight) and flags & RenderFlags.SHADOWS_SPOT:
+ active = True
+
+ if active and l.shadow_texture is None:
+ l._generate_shadow_texture()
+ if l.shadow_texture is not None:
+ shadow_textures.add(l.shadow_texture)
+
+ # Add new textures to context
+ for texture in shadow_textures - self._shadow_textures:
+ texture._add_to_context()
+
+ # Remove old textures from context
+ for texture in self._shadow_textures - shadow_textures:
+ texture.delete()
+
+ self._shadow_textures = shadow_textures.copy()
+
+ ###########################################################################
+ # Texture Management
+ ###########################################################################
+
+ def _bind_texture(self, texture, uniform_name, program):
+ """Bind a texture to an active texture unit and return
+ the texture unit index that was used.
+ """
+ tex_id = self._get_next_active_texture()
+ glActiveTexture(GL_TEXTURE0 + tex_id)
+ texture._bind()
+ program.set_uniform(uniform_name, tex_id)
+
+ def _get_next_active_texture(self):
+ val = self._texture_alloc_idx
+ self._texture_alloc_idx += 1
+ return val
+
+ def _reset_active_textures(self):
+ self._texture_alloc_idx = 0
+
+ ###########################################################################
+ # Camera Matrix Management
+ ###########################################################################
+
+ def _get_camera_matrices(self, scene):
+ main_camera_node = scene.main_camera_node
+ if main_camera_node is None:
+ raise ValueError('Cannot render scene without a camera')
+ P = main_camera_node.camera.get_projection_matrix(
+ width=self.viewport_width, height=self.viewport_height
+ )
+ pose = scene.get_pose(main_camera_node)
+ V = np.linalg.inv(pose) # V maps from world to camera
+ return V, P
+
+ def _get_light_cam_matrices(self, scene, light_node, flags):
+ light = light_node.light
+ pose = scene.get_pose(light_node).copy()
+ s = scene.scale
+ camera = light._get_shadow_camera(s)
+ P = camera.get_projection_matrix()
+ if isinstance(light, DirectionalLight):
+ direction = -pose[:3,2]
+ c = scene.centroid
+ loc = c - direction * s
+ pose[:3,3] = loc
+ V = np.linalg.inv(pose) # V maps from world to camera
+ return V, P
+
+ ###########################################################################
+ # Shader Program Management
+ ###########################################################################
+
+ def _get_text_program(self):
+ program = self._program_cache.get_program(
+ vertex_shader='text.vert',
+ fragment_shader='text.frag'
+ )
+
+ if not program._in_context():
+ program._add_to_context()
+
+ return program
+
+ def _compute_max_n_lights(self, flags):
+ max_n_lights = [MAX_N_LIGHTS, MAX_N_LIGHTS, MAX_N_LIGHTS]
+ n_tex_units = glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS)
+
+ # Reserved texture units: 6
+ # Normal Map
+ # Occlusion Map
+ # Emissive Map
+ # Base Color or Diffuse Map
+ # MR or SG Map
+ # Environment cubemap
+
+ n_reserved_textures = 6
+ n_available_textures = n_tex_units - n_reserved_textures
+
+ # Distribute textures evenly among lights with shadows, with
+ # a preference for directional lights
+ n_shadow_types = 0
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
+ n_shadow_types += 1
+ if flags & RenderFlags.SHADOWS_SPOT:
+ n_shadow_types += 1
+ if flags & RenderFlags.SHADOWS_POINT:
+ n_shadow_types += 1
+
+ if n_shadow_types > 0:
+ tex_per_light = n_available_textures // n_shadow_types
+
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
+ max_n_lights[0] = (
+ tex_per_light +
+ (n_available_textures - tex_per_light * n_shadow_types)
+ )
+ if flags & RenderFlags.SHADOWS_SPOT:
+ max_n_lights[1] = tex_per_light
+ if flags & RenderFlags.SHADOWS_POINT:
+ max_n_lights[2] = tex_per_light
+
+ return max_n_lights
+
+ def _get_primitive_program(self, primitive, flags, program_flags):
+ vertex_shader = None
+ fragment_shader = None
+ geometry_shader = None
+ defines = {}
+
+ if (bool(program_flags & ProgramFlags.USE_MATERIAL) and
+ not flags & RenderFlags.DEPTH_ONLY and
+ not flags & RenderFlags.FLAT and
+ not flags & RenderFlags.SEG):
+ vertex_shader = 'mesh.vert'
+ fragment_shader = 'mesh.frag'
+ elif bool(program_flags & (ProgramFlags.VERTEX_NORMALS |
+ ProgramFlags.FACE_NORMALS)):
+ vertex_shader = 'vertex_normals.vert'
+ if primitive.mode == GLTF.POINTS:
+ geometry_shader = 'vertex_normals_pc.geom'
+ else:
+ geometry_shader = 'vertex_normals.geom'
+ fragment_shader = 'vertex_normals.frag'
+ elif flags & RenderFlags.FLAT:
+ vertex_shader = 'flat.vert'
+ fragment_shader = 'flat.frag'
+ elif flags & RenderFlags.SEG:
+ vertex_shader = 'segmentation.vert'
+ fragment_shader = 'segmentation.frag'
+ else:
+ vertex_shader = 'mesh_depth.vert'
+ fragment_shader = 'mesh_depth.frag'
+
+ # Set up vertex buffer DEFINES
+ bf = primitive.buf_flags
+ buf_idx = 1
+ if bf & BufFlags.NORMAL:
+ defines['NORMAL_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.TANGENT:
+ defines['TANGENT_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.TEXCOORD_0:
+ defines['TEXCOORD_0_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.TEXCOORD_1:
+ defines['TEXCOORD_1_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.COLOR_0:
+ defines['COLOR_0_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.JOINTS_0:
+ defines['JOINTS_0_LOC'] = buf_idx
+ buf_idx += 1
+ if bf & BufFlags.WEIGHTS_0:
+ defines['WEIGHTS_0_LOC'] = buf_idx
+ buf_idx += 1
+ defines['INST_M_LOC'] = buf_idx
+
+ # Set up shadow mapping defines
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
+ defines['DIRECTIONAL_LIGHT_SHADOWS'] = 1
+ if flags & RenderFlags.SHADOWS_SPOT:
+ defines['SPOT_LIGHT_SHADOWS'] = 1
+ if flags & RenderFlags.SHADOWS_POINT:
+ defines['POINT_LIGHT_SHADOWS'] = 1
+ max_n_lights = self._compute_max_n_lights(flags)
+ defines['MAX_DIRECTIONAL_LIGHTS'] = max_n_lights[0]
+ defines['MAX_SPOT_LIGHTS'] = max_n_lights[1]
+ defines['MAX_POINT_LIGHTS'] = max_n_lights[2]
+
+ # Set up vertex normal defines
+ if program_flags & ProgramFlags.VERTEX_NORMALS:
+ defines['VERTEX_NORMALS'] = 1
+ if program_flags & ProgramFlags.FACE_NORMALS:
+ defines['FACE_NORMALS'] = 1
+
+ # Set up material texture defines
+ if bool(program_flags & ProgramFlags.USE_MATERIAL):
+ tf = primitive.material.tex_flags
+ if tf & TexFlags.NORMAL:
+ defines['HAS_NORMAL_TEX'] = 1
+ if tf & TexFlags.OCCLUSION:
+ defines['HAS_OCCLUSION_TEX'] = 1
+ if tf & TexFlags.EMISSIVE:
+ defines['HAS_EMISSIVE_TEX'] = 1
+ if tf & TexFlags.BASE_COLOR:
+ defines['HAS_BASE_COLOR_TEX'] = 1
+ if tf & TexFlags.METALLIC_ROUGHNESS:
+ defines['HAS_METALLIC_ROUGHNESS_TEX'] = 1
+ if tf & TexFlags.DIFFUSE:
+ defines['HAS_DIFFUSE_TEX'] = 1
+ if tf & TexFlags.SPECULAR_GLOSSINESS:
+ defines['HAS_SPECULAR_GLOSSINESS_TEX'] = 1
+ if isinstance(primitive.material, MetallicRoughnessMaterial):
+ defines['USE_METALLIC_MATERIAL'] = 1
+ elif isinstance(primitive.material, SpecularGlossinessMaterial):
+ defines['USE_GLOSSY_MATERIAL'] = 1
+
+ program = self._program_cache.get_program(
+ vertex_shader=vertex_shader,
+ fragment_shader=fragment_shader,
+ geometry_shader=geometry_shader,
+ defines=defines
+ )
+
+ if not program._in_context():
+ program._add_to_context()
+
+ return program
+
+ ###########################################################################
+ # Viewport Management
+ ###########################################################################
+
+ def _configure_forward_pass_viewport(self, flags):
+
+ # If using offscreen render, bind main framebuffer
+ if flags & RenderFlags.OFFSCREEN:
+ self._configure_main_framebuffer()
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms)
+ else:
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
+
+ glViewport(0, 0, self.viewport_width, self.viewport_height)
+ glEnable(GL_DEPTH_TEST)
+ glDepthMask(GL_TRUE)
+ glDepthFunc(GL_LESS)
+ glDepthRange(0.0, 1.0)
+
+ def _configure_shadow_mapping_viewport(self, light, flags):
+ self._configure_shadow_framebuffer()
+ glBindFramebuffer(GL_FRAMEBUFFER, self._shadow_fb)
+ light.shadow_texture._bind()
+ light.shadow_texture._bind_as_depth_attachment()
+ glActiveTexture(GL_TEXTURE0)
+ light.shadow_texture._bind()
+ glDrawBuffer(GL_NONE)
+ glReadBuffer(GL_NONE)
+
+ glClear(GL_DEPTH_BUFFER_BIT)
+ glViewport(0, 0, SHADOW_TEX_SZ, SHADOW_TEX_SZ)
+ glEnable(GL_DEPTH_TEST)
+ glDepthMask(GL_TRUE)
+ glDepthFunc(GL_LESS)
+ glDepthRange(0.0, 1.0)
+ glDisable(GL_CULL_FACE)
+ glDisable(GL_BLEND)
+
+ ###########################################################################
+ # Framebuffer Management
+ ###########################################################################
+
+ def _configure_shadow_framebuffer(self):
+ if self._shadow_fb is None:
+ self._shadow_fb = glGenFramebuffers(1)
+
+ def _delete_shadow_framebuffer(self):
+ if self._shadow_fb is not None:
+ glDeleteFramebuffers(1, [self._shadow_fb])
+
+ def _configure_main_framebuffer(self):
+ # If mismatch with prior framebuffer, delete it
+ if (self._main_fb is not None and
+ self.viewport_width != self._main_fb_dims[0] or
+ self.viewport_height != self._main_fb_dims[1]):
+ self._delete_main_framebuffer()
+
+ # If framebuffer doesn't exist, create it
+ if self._main_fb is None:
+ # Generate standard buffer
+ self._main_cb, self._main_db = glGenRenderbuffers(2)
+
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb)
+ glRenderbufferStorage(
+ GL_RENDERBUFFER, GL_RGBA,
+ self.viewport_width, self.viewport_height
+ )
+
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_db)
+ glRenderbufferStorage(
+ GL_RENDERBUFFER, GL_DEPTH_COMPONENT24,
+ self.viewport_width, self.viewport_height
+ )
+
+ self._main_fb = glGenFramebuffers(1)
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb)
+ glFramebufferRenderbuffer(
+ GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, self._main_cb
+ )
+ glFramebufferRenderbuffer(
+ GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, self._main_db
+ )
+
+ # Generate multisample buffer
+ self._main_cb_ms, self._main_db_ms = glGenRenderbuffers(2)
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb_ms)
+ # glRenderbufferStorageMultisample(
+ # GL_RENDERBUFFER, 4, GL_RGBA,
+ # self.viewport_width, self.viewport_height
+ # )
+ # glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms)
+ # glRenderbufferStorageMultisample(
+ # GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT24,
+ # self.viewport_width, self.viewport_height
+ # )
+ # 增加这一行
+ num_samples = min(glGetIntegerv(GL_MAX_SAMPLES), 4) # No more than GL_MAX_SAMPLES
+
+ # 其实就是把 4 替换成 num_samples,其余不变
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA, self.viewport_width, self.viewport_height)
+
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms) # 这行不变
+
+ # 这一行也是将 4 替换成 num_samples
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, self.viewport_width, self.viewport_height)
+
+ self._main_fb_ms = glGenFramebuffers(1)
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms)
+ glFramebufferRenderbuffer(
+ GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, self._main_cb_ms
+ )
+ glFramebufferRenderbuffer(
+ GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, self._main_db_ms
+ )
+
+ self._main_fb_dims = (self.viewport_width, self.viewport_height)
+
+ def _delete_main_framebuffer(self):
+ if self._main_fb is not None:
+ glDeleteFramebuffers(2, [self._main_fb, self._main_fb_ms])
+ if self._main_cb is not None:
+ glDeleteRenderbuffers(2, [self._main_cb, self._main_cb_ms])
+ if self._main_db is not None:
+ glDeleteRenderbuffers(2, [self._main_db, self._main_db_ms])
+
+ self._main_fb = None
+ self._main_cb = None
+ self._main_db = None
+ self._main_fb_ms = None
+ self._main_cb_ms = None
+ self._main_db_ms = None
+ self._main_fb_dims = (None, None)
+
+ def _read_main_framebuffer(self, scene, flags):
+ width, height = self._main_fb_dims[0], self._main_fb_dims[1]
+
+ # Bind framebuffer and blit buffers
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb_ms)
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb)
+ glBlitFramebuffer(
+ 0, 0, width, height, 0, 0, width, height,
+ GL_COLOR_BUFFER_BIT, GL_LINEAR
+ )
+ glBlitFramebuffer(
+ 0, 0, width, height, 0, 0, width, height,
+ GL_DEPTH_BUFFER_BIT, GL_NEAREST
+ )
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb)
+
+ # Read depth
+ depth_buf = glReadPixels(
+ 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT
+ )
+ depth_im = np.frombuffer(depth_buf, dtype=np.float32)
+ depth_im = depth_im.reshape((height, width))
+ depth_im = np.flip(depth_im, axis=0)
+ inf_inds = (depth_im == 1.0)
+ depth_im = 2.0 * depth_im - 1.0
+ z_near = scene.main_camera_node.camera.znear
+ z_far = scene.main_camera_node.camera.zfar
+ noninf = np.logical_not(inf_inds)
+ if z_far is None:
+ depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf])
+ else:
+ depth_im[noninf] = ((2.0 * z_near * z_far) /
+ (z_far + z_near - depth_im[noninf] *
+ (z_far - z_near)))
+ depth_im[inf_inds] = 0.0
+
+ # Resize for macos if needed
+ if sys.platform == 'darwin':
+ depth_im = self._resize_image(depth_im)
+
+ if flags & RenderFlags.DEPTH_ONLY:
+ return depth_im
+
+ # Read color
+ if flags & RenderFlags.RGBA:
+ color_buf = glReadPixels(
+ 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE
+ )
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
+ color_im = color_im.reshape((height, width, 4))
+ else:
+ color_buf = glReadPixels(
+ 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE
+ )
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
+ color_im = color_im.reshape((height, width, 3))
+ color_im = np.flip(color_im, axis=0)
+
+ # Resize for macos if needed
+ if sys.platform == 'darwin':
+ color_im = self._resize_image(color_im, True)
+
+ return color_im, depth_im
+
+ def _resize_image(self, value, antialias=False):
+ """If needed, rescale the render for MacOS."""
+ img = PIL.Image.fromarray(value)
+ resample = PIL.Image.NEAREST
+ if antialias:
+ resample = PIL.Image.BILINEAR
+ size = (self.viewport_width // self.dpscale,
+ self.viewport_height // self.dpscale)
+ img = img.resize(size, resample=resample)
+ return np.array(img)
+
+ ###########################################################################
+ # Shadowmap Debugging
+ ###########################################################################
+
+ def _forward_pass_no_reset(self, scene, flags):
+ # Set up camera matrices
+ V, P = self._get_camera_matrices(scene)
+
+ # Now, render each object in sorted order
+ for node in self._sorted_mesh_nodes(scene):
+ mesh = node.mesh
+
+ # Skip the mesh if it's not visible
+ if not mesh.is_visible:
+ continue
+
+ for primitive in mesh.primitives:
+
+ # First, get and bind the appropriate program
+ program = self._get_primitive_program(
+ primitive, flags, ProgramFlags.USE_MATERIAL
+ )
+ program._bind()
+
+ # Set the camera uniforms
+ program.set_uniform('V', V)
+ program.set_uniform('P', P)
+ program.set_uniform(
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
+ )
+
+ # Next, bind the lighting
+ if not flags & RenderFlags.DEPTH_ONLY and not flags & RenderFlags.FLAT:
+ self._bind_lighting(scene, program, node, flags)
+
+ # Finally, bind and draw the primitive
+ self._bind_and_draw_primitive(
+ primitive=primitive,
+ pose=scene.get_pose(node),
+ program=program,
+ flags=flags
+ )
+ self._reset_active_textures()
+
+ # Unbind the shader and flush the output
+ if program is not None:
+ program._unbind()
+ glFlush()
+
+ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False):
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
+ glClearColor(*scene.bg_color)
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ glEnable(GL_DEPTH_TEST)
+ glDepthMask(GL_TRUE)
+ glDepthFunc(GL_LESS)
+ glDepthRange(0.0, 1.0)
+
+ w = self.viewport_width
+ h = self.viewport_height
+
+ num_nodes = len(light_nodes)
+ viewport_dims = {
+ (0, 2): [0, h // 2, w // 2, h],
+ (1, 2): [w // 2, h // 2, w, h],
+ (0, 3): [0, h // 2, w // 2, h],
+ (1, 3): [w // 2, h // 2, w, h],
+ (2, 3): [0, 0, w // 2, h // 2],
+ (0, 4): [0, h // 2, w // 2, h],
+ (1, 4): [w // 2, h // 2, w, h],
+ (2, 4): [0, 0, w // 2, h // 2],
+ (3, 4): [w // 2, 0, w, h // 2]
+ }
+
+ if tile:
+ for i, ln in enumerate(light_nodes):
+ light = ln.light
+
+ if light.shadow_texture is None:
+ raise ValueError('Light does not have a shadow texture')
+
+ glViewport(*viewport_dims[(i, num_nodes + 1)])
+
+ program = self._get_debug_quad_program()
+ program._bind()
+ self._bind_texture(light.shadow_texture, 'depthMap', program)
+ self._render_debug_quad()
+ self._reset_active_textures()
+ glFlush()
+ i += 1
+ glViewport(*viewport_dims[(i, num_nodes + 1)])
+ self._forward_pass_no_reset(scene, flags)
+ else:
+ for i, ln in enumerate(light_nodes):
+ light = ln.light
+
+ if light.shadow_texture is None:
+ raise ValueError('Light does not have a shadow texture')
+
+ glViewport(0, 0, self.viewport_width, self.viewport_height)
+
+ program = self._get_debug_quad_program()
+ program._bind()
+ self._bind_texture(light.shadow_texture, 'depthMap', program)
+ self._render_debug_quad()
+ self._reset_active_textures()
+ glFlush()
+ return
+
+ def _get_debug_quad_program(self):
+ program = self._program_cache.get_program(
+ vertex_shader='debug_quad.vert',
+ fragment_shader='debug_quad.frag'
+ )
+ if not program._in_context():
+ program._add_to_context()
+ return program
+
+ def _render_debug_quad(self):
+ x = glGenVertexArrays(1)
+ glBindVertexArray(x)
+ glDrawArrays(GL_TRIANGLES, 0, 6)
+ glBindVertexArray(0)
+ glDeleteVertexArrays(1, [x])
diff --git a/pyrender/pyrender/sampler.py b/pyrender/pyrender/sampler.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4784d068f808a40a56c8e748d83175f7f4e6233
--- /dev/null
+++ b/pyrender/pyrender/sampler.py
@@ -0,0 +1,102 @@
+"""Samplers, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-sampler
+
+Author: Matthew Matl
+"""
+from .constants import GLTF
+
+
+class Sampler(object):
+ """Texture sampler properties for filtering and wrapping modes.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ magFilter : int, optional
+ Magnification filter. Valid values:
+ - :attr:`.GLTF.NEAREST`
+ - :attr:`.GLTF.LINEAR`
+ minFilter : int, optional
+ Minification filter. Valid values:
+ - :attr:`.GLTF.NEAREST`
+ - :attr:`.GLTF.LINEAR`
+ - :attr:`.GLTF.NEAREST_MIPMAP_NEAREST`
+ - :attr:`.GLTF.LINEAR_MIPMAP_NEAREST`
+ - :attr:`.GLTF.NEAREST_MIPMAP_LINEAR`
+ - :attr:`.GLTF.LINEAR_MIPMAP_LINEAR`
+ wrapS : int, optional
+ S (U) wrapping mode. Valid values:
+ - :attr:`.GLTF.CLAMP_TO_EDGE`
+ - :attr:`.GLTF.MIRRORED_REPEAT`
+ - :attr:`.GLTF.REPEAT`
+ wrapT : int, optional
+ T (V) wrapping mode. Valid values:
+ - :attr:`.GLTF.CLAMP_TO_EDGE`
+ - :attr:`.GLTF.MIRRORED_REPEAT`
+ - :attr:`.GLTF.REPEAT`
+ """
+
+ def __init__(self,
+ name=None,
+ magFilter=None,
+ minFilter=None,
+ wrapS=GLTF.REPEAT,
+ wrapT=GLTF.REPEAT):
+ self.name = name
+ self.magFilter = magFilter
+ self.minFilter = minFilter
+ self.wrapS = wrapS
+ self.wrapT = wrapT
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def magFilter(self):
+ """int : Magnification filter type.
+ """
+ return self._magFilter
+
+ @magFilter.setter
+ def magFilter(self, value):
+ self._magFilter = value
+
+ @property
+ def minFilter(self):
+ """int : Minification filter type.
+ """
+ return self._minFilter
+
+ @minFilter.setter
+ def minFilter(self, value):
+ self._minFilter = value
+
+ @property
+ def wrapS(self):
+ """int : S (U) wrapping mode.
+ """
+ return self._wrapS
+
+ @wrapS.setter
+ def wrapS(self, value):
+ self._wrapS = value
+
+ @property
+ def wrapT(self):
+ """int : T (V) wrapping mode.
+ """
+ return self._wrapT
+
+ @wrapT.setter
+ def wrapT(self, value):
+ self._wrapT = value
diff --git a/pyrender/pyrender/scene.py b/pyrender/pyrender/scene.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fe057ec66f52f2dd9c1363aacf72a7c6cec4e6c
--- /dev/null
+++ b/pyrender/pyrender/scene.py
@@ -0,0 +1,585 @@
+"""Scenes, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-scene
+
+Author: Matthew Matl
+"""
+import numpy as np
+import networkx as nx
+import trimesh
+
+from .mesh import Mesh
+from .camera import Camera
+from .light import Light, PointLight, DirectionalLight, SpotLight
+from .node import Node
+from .utils import format_color_vector
+
+
+class Scene(object):
+ """A hierarchical scene graph.
+
+ Parameters
+ ----------
+ nodes : list of :class:`Node`
+ The set of all nodes in the scene.
+ bg_color : (4,) float, optional
+ Background color of scene.
+ ambient_light : (3,) float, optional
+ Color of ambient light. Defaults to no ambient light.
+ name : str, optional
+ The user-defined name of this object.
+ """
+
+ def __init__(self,
+ nodes=None,
+ bg_color=None,
+ ambient_light=None,
+ name=None):
+
+ if bg_color is None:
+ bg_color = np.ones(4)
+ else:
+ bg_color = format_color_vector(bg_color, 4)
+
+ if ambient_light is None:
+ ambient_light = np.zeros(3)
+
+ if nodes is None:
+ nodes = set()
+ self._nodes = set() # Will be added at the end of this function
+
+ self.bg_color = bg_color
+ self.ambient_light = ambient_light
+ self.name = name
+
+ self._name_to_nodes = {}
+ self._obj_to_nodes = {}
+ self._obj_name_to_nodes = {}
+ self._mesh_nodes = set()
+ self._point_light_nodes = set()
+ self._spot_light_nodes = set()
+ self._directional_light_nodes = set()
+ self._camera_nodes = set()
+ self._main_camera_node = None
+ self._bounds = None
+
+ # Transform tree
+ self._digraph = nx.DiGraph()
+ self._digraph.add_node('world')
+ self._path_cache = {}
+
+ # Find root nodes and add them
+ if len(nodes) > 0:
+ node_parent_map = {n: None for n in nodes}
+ for node in nodes:
+ for child in node.children:
+ if node_parent_map[child] is not None:
+ raise ValueError('Nodes may not have more than '
+ 'one parent')
+ node_parent_map[child] = node
+ for node in node_parent_map:
+ if node_parent_map[node] is None:
+ self.add_node(node)
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def nodes(self):
+ """set of :class:`Node` : Set of nodes in the scene.
+ """
+ return self._nodes
+
+ @property
+ def bg_color(self):
+ """(3,) float : The scene background color.
+ """
+ return self._bg_color
+
+ @bg_color.setter
+ def bg_color(self, value):
+ if value is None:
+ value = np.ones(4)
+ else:
+ value = format_color_vector(value, 4)
+ self._bg_color = value
+
+ @property
+ def ambient_light(self):
+ """(3,) float : The ambient light in the scene.
+ """
+ return self._ambient_light
+
+ @ambient_light.setter
+ def ambient_light(self, value):
+ if value is None:
+ value = np.zeros(3)
+ else:
+ value = format_color_vector(value, 3)
+ self._ambient_light = value
+
+ @property
+ def meshes(self):
+ """set of :class:`Mesh` : The meshes in the scene.
+ """
+ return set([n.mesh for n in self.mesh_nodes])
+
+ @property
+ def mesh_nodes(self):
+ """set of :class:`Node` : The nodes containing meshes.
+ """
+ return self._mesh_nodes
+
+ @property
+ def lights(self):
+ """set of :class:`Light` : The lights in the scene.
+ """
+ return self.point_lights | self.spot_lights | self.directional_lights
+
+ @property
+ def light_nodes(self):
+ """set of :class:`Node` : The nodes containing lights.
+ """
+ return (self.point_light_nodes | self.spot_light_nodes |
+ self.directional_light_nodes)
+
+ @property
+ def point_lights(self):
+ """set of :class:`PointLight` : The point lights in the scene.
+ """
+ return set([n.light for n in self.point_light_nodes])
+
+ @property
+ def point_light_nodes(self):
+ """set of :class:`Node` : The nodes containing point lights.
+ """
+ return self._point_light_nodes
+
+ @property
+ def spot_lights(self):
+ """set of :class:`SpotLight` : The spot lights in the scene.
+ """
+ return set([n.light for n in self.spot_light_nodes])
+
+ @property
+ def spot_light_nodes(self):
+ """set of :class:`Node` : The nodes containing spot lights.
+ """
+ return self._spot_light_nodes
+
+ @property
+ def directional_lights(self):
+ """set of :class:`DirectionalLight` : The directional lights in
+ the scene.
+ """
+ return set([n.light for n in self.directional_light_nodes])
+
+ @property
+ def directional_light_nodes(self):
+ """set of :class:`Node` : The nodes containing directional lights.
+ """
+ return self._directional_light_nodes
+
+ @property
+ def cameras(self):
+ """set of :class:`Camera` : The cameras in the scene.
+ """
+ return set([n.camera for n in self.camera_nodes])
+
+ @property
+ def camera_nodes(self):
+ """set of :class:`Node` : The nodes containing cameras in the scene.
+ """
+ return self._camera_nodes
+
+ @property
+ def main_camera_node(self):
+ """set of :class:`Node` : The node containing the main camera in the
+ scene.
+ """
+ return self._main_camera_node
+
+ @main_camera_node.setter
+ def main_camera_node(self, value):
+ if value not in self.nodes:
+ raise ValueError('New main camera node must already be in scene')
+ self._main_camera_node = value
+
+ @property
+ def bounds(self):
+ """(2,3) float : The axis-aligned bounds of the scene.
+ """
+ if self._bounds is None:
+ # Compute corners
+ corners = []
+ for mesh_node in self.mesh_nodes:
+ mesh = mesh_node.mesh
+ pose = self.get_pose(mesh_node)
+ corners_local = trimesh.bounds.corners(mesh.bounds)
+ corners_world = pose[:3,:3].dot(corners_local.T).T + pose[:3,3]
+ corners.append(corners_world)
+ if len(corners) == 0:
+ self._bounds = np.zeros((2,3))
+ else:
+ corners = np.vstack(corners)
+ self._bounds = np.array([np.min(corners, axis=0),
+ np.max(corners, axis=0)])
+ return self._bounds
+
+ @property
+ def centroid(self):
+ """(3,) float : The centroid of the scene's axis-aligned bounding box
+ (AABB).
+ """
+ return np.mean(self.bounds, axis=0)
+
+ @property
+ def extents(self):
+ """(3,) float : The lengths of the axes of the scene's AABB.
+ """
+ return np.diff(self.bounds, axis=0).reshape(-1)
+
+ @property
+ def scale(self):
+ """(3,) float : The length of the diagonal of the scene's AABB.
+ """
+ return np.linalg.norm(self.extents)
+
+ def add(self, obj, name=None, pose=None,
+ parent_node=None, parent_name=None):
+ """Add an object (mesh, light, or camera) to the scene.
+
+ Parameters
+ ----------
+ obj : :class:`Mesh`, :class:`Light`, or :class:`Camera`
+ The object to add to the scene.
+ name : str
+ A name for the new node to be created.
+ pose : (4,4) float
+ The local pose of this node relative to its parent node.
+ parent_node : :class:`Node`
+ The parent of this Node. If None, the new node is a root node.
+ parent_name : str
+ The name of the parent node, can be specified instead of
+ `parent_node`.
+
+ Returns
+ -------
+ node : :class:`Node`
+ The newly-created and inserted node.
+ """
+ if isinstance(obj, Mesh):
+ node = Node(name=name, matrix=pose, mesh=obj)
+ elif isinstance(obj, Light):
+ node = Node(name=name, matrix=pose, light=obj)
+ elif isinstance(obj, Camera):
+ node = Node(name=name, matrix=pose, camera=obj)
+ else:
+ raise TypeError('Unrecognized object type')
+
+ if parent_node is None and parent_name is not None:
+ parent_nodes = self.get_nodes(name=parent_name)
+ if len(parent_nodes) == 0:
+ raise ValueError('No parent node with name {} found'
+ .format(parent_name))
+ elif len(parent_nodes) > 1:
+ raise ValueError('More than one parent node with name {} found'
+ .format(parent_name))
+ parent_node = list(parent_nodes)[0]
+
+ self.add_node(node, parent_node=parent_node)
+
+ return node
+
+ def get_nodes(self, node=None, name=None, obj=None, obj_name=None):
+ """Search for existing nodes. Only nodes matching all specified
+ parameters is returned, or None if no such node exists.
+
+ Parameters
+ ----------
+ node : :class:`Node`, optional
+ If present, returns this node if it is in the scene.
+ name : str
+ A name for the Node.
+ obj : :class:`Mesh`, :class:`Light`, or :class:`Camera`
+ An object that is attached to the node.
+ obj_name : str
+ The name of an object that is attached to the node.
+
+ Returns
+ -------
+ nodes : set of :class:`.Node`
+ The nodes that match all query terms.
+ """
+ if node is not None:
+ if node in self.nodes:
+ return set([node])
+ else:
+ return set()
+ nodes = set(self.nodes)
+ if name is not None:
+ matches = set()
+ if name in self._name_to_nodes:
+ matches = self._name_to_nodes[name]
+ nodes = nodes & matches
+ if obj is not None:
+ matches = set()
+ if obj in self._obj_to_nodes:
+ matches = self._obj_to_nodes[obj]
+ nodes = nodes & matches
+ if obj_name is not None:
+ matches = set()
+ if obj_name in self._obj_name_to_nodes:
+ matches = self._obj_name_to_nodes[obj_name]
+ nodes = nodes & matches
+
+ return nodes
+
+ def add_node(self, node, parent_node=None):
+ """Add a Node to the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to be added.
+ parent_node : :class:`Node`
+ The parent of this Node. If None, the new node is a root node.
+ """
+ if node in self.nodes:
+ raise ValueError('Node already in scene')
+ self.nodes.add(node)
+
+ # Add node to sets
+ if node.name is not None:
+ if node.name not in self._name_to_nodes:
+ self._name_to_nodes[node.name] = set()
+ self._name_to_nodes[node.name].add(node)
+ for obj in [node.mesh, node.camera, node.light]:
+ if obj is not None:
+ if obj not in self._obj_to_nodes:
+ self._obj_to_nodes[obj] = set()
+ self._obj_to_nodes[obj].add(node)
+ if obj.name is not None:
+ if obj.name not in self._obj_name_to_nodes:
+ self._obj_name_to_nodes[obj.name] = set()
+ self._obj_name_to_nodes[obj.name].add(node)
+ if node.mesh is not None:
+ self._mesh_nodes.add(node)
+ if node.light is not None:
+ if isinstance(node.light, PointLight):
+ self._point_light_nodes.add(node)
+ if isinstance(node.light, SpotLight):
+ self._spot_light_nodes.add(node)
+ if isinstance(node.light, DirectionalLight):
+ self._directional_light_nodes.add(node)
+ if node.camera is not None:
+ self._camera_nodes.add(node)
+ if self._main_camera_node is None:
+ self._main_camera_node = node
+
+ if parent_node is None:
+ parent_node = 'world'
+ elif parent_node not in self.nodes:
+ raise ValueError('Parent node must already be in scene')
+ elif node not in parent_node.children:
+ parent_node.children.append(node)
+
+ # Create node in graph
+ self._digraph.add_node(node)
+ self._digraph.add_edge(node, parent_node)
+
+ # Iterate over children
+ for child in node.children:
+ self.add_node(child, node)
+
+ self._path_cache = {}
+ self._bounds = None
+
+ def has_node(self, node):
+ """Check if a node is already in the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to be checked.
+
+ Returns
+ -------
+ has_node : bool
+ True if the node is already in the scene and false otherwise.
+ """
+ return node in self.nodes
+
+ def remove_node(self, node):
+ """Remove a node and all its children from the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to be removed.
+ """
+ # Disconnect self from parent who is staying in the graph
+ parent = list(self._digraph.neighbors(node))[0]
+ self._remove_node(node)
+ if isinstance(parent, Node):
+ parent.children.remove(node)
+ self._path_cache = {}
+ self._bounds = None
+
+ def get_pose(self, node):
+ """Get the world-frame pose of a node in the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to find the pose of.
+
+ Returns
+ -------
+ pose : (4,4) float
+ The transform matrix for this node.
+ """
+ if node not in self.nodes:
+ raise ValueError('Node must already be in scene')
+ if node in self._path_cache:
+ path = self._path_cache[node]
+ else:
+ # Get path from from_frame to to_frame
+ path = nx.shortest_path(self._digraph, node, 'world')
+ self._path_cache[node] = path
+
+ # Traverse from from_node to to_node
+ pose = np.eye(4)
+ for n in path[:-1]:
+ pose = np.dot(n.matrix, pose)
+
+ return pose
+
+ def set_pose(self, node, pose):
+ """Set the local-frame pose of a node in the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to set the pose of.
+ pose : (4,4) float
+ The pose to set the node to.
+ """
+ if node not in self.nodes:
+ raise ValueError('Node must already be in scene')
+ node._matrix = pose
+ if node.mesh is not None:
+ self._bounds = None
+
+ def clear(self):
+ """Clear out all nodes to form an empty scene.
+ """
+ self._nodes = set()
+
+ self._name_to_nodes = {}
+ self._obj_to_nodes = {}
+ self._obj_name_to_nodes = {}
+ self._mesh_nodes = set()
+ self._point_light_nodes = set()
+ self._spot_light_nodes = set()
+ self._directional_light_nodes = set()
+ self._camera_nodes = set()
+ self._main_camera_node = None
+ self._bounds = None
+
+ # Transform tree
+ self._digraph = nx.DiGraph()
+ self._digraph.add_node('world')
+ self._path_cache = {}
+
+ def _remove_node(self, node):
+ """Remove a node and all its children from the scene.
+
+ Parameters
+ ----------
+ node : :class:`Node`
+ The node to be removed.
+ """
+
+ # Remove self from nodes
+ self.nodes.remove(node)
+
+ # Remove children
+ for child in node.children:
+ self._remove_node(child)
+
+ # Remove self from the graph
+ self._digraph.remove_node(node)
+
+ # Remove from maps
+ if node.name in self._name_to_nodes:
+ self._name_to_nodes[node.name].remove(node)
+ if len(self._name_to_nodes[node.name]) == 0:
+ self._name_to_nodes.pop(node.name)
+ for obj in [node.mesh, node.camera, node.light]:
+ if obj is None:
+ continue
+ self._obj_to_nodes[obj].remove(node)
+ if len(self._obj_to_nodes[obj]) == 0:
+ self._obj_to_nodes.pop(obj)
+ if obj.name is not None:
+ self._obj_name_to_nodes[obj.name].remove(node)
+ if len(self._obj_name_to_nodes[obj.name]) == 0:
+ self._obj_name_to_nodes.pop(obj.name)
+ if node.mesh is not None:
+ self._mesh_nodes.remove(node)
+ if node.light is not None:
+ if isinstance(node.light, PointLight):
+ self._point_light_nodes.remove(node)
+ if isinstance(node.light, SpotLight):
+ self._spot_light_nodes.remove(node)
+ if isinstance(node.light, DirectionalLight):
+ self._directional_light_nodes.remove(node)
+ if node.camera is not None:
+ self._camera_nodes.remove(node)
+ if self._main_camera_node == node:
+ if len(self._camera_nodes) > 0:
+ self._main_camera_node = next(iter(self._camera_nodes))
+ else:
+ self._main_camera_node = None
+
+ @staticmethod
+ def from_trimesh_scene(trimesh_scene,
+ bg_color=None, ambient_light=None):
+ """Create a :class:`.Scene` from a :class:`trimesh.scene.scene.Scene`.
+
+ Parameters
+ ----------
+ trimesh_scene : :class:`trimesh.scene.scene.Scene`
+ Scene with :class:~`trimesh.base.Trimesh` objects.
+ bg_color : (4,) float
+ Background color for the created scene.
+ ambient_light : (3,) float or None
+ Ambient light in the scene.
+
+ Returns
+ -------
+ scene_pr : :class:`Scene`
+ A scene containing the same geometry as the trimesh scene.
+ """
+ # convert trimesh geometries to pyrender geometries
+ geometries = {name: Mesh.from_trimesh(geom)
+ for name, geom in trimesh_scene.geometry.items()}
+
+ # create the pyrender scene object
+ scene_pr = Scene(bg_color=bg_color, ambient_light=ambient_light)
+
+ # add every node with geometry to the pyrender scene
+ for node in trimesh_scene.graph.nodes_geometry:
+ pose, geom_name = trimesh_scene.graph[node]
+ scene_pr.add(geometries[geom_name], pose=pose)
+
+ return scene_pr
diff --git a/pyrender/pyrender/shader_program.py b/pyrender/pyrender/shader_program.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1803f280c98033abe0769771a9ad8ecfec942e3
--- /dev/null
+++ b/pyrender/pyrender/shader_program.py
@@ -0,0 +1,283 @@
+"""OpenGL shader program wrapper.
+"""
+import numpy as np
+import os
+import re
+
+import OpenGL
+from OpenGL.GL import *
+from OpenGL.GL import shaders as gl_shader_utils
+
+
+class ShaderProgramCache(object):
+ """A cache for shader programs.
+ """
+
+ def __init__(self, shader_dir=None):
+ self._program_cache = {}
+ self.shader_dir = shader_dir
+ if self.shader_dir is None:
+ base_dir, _ = os.path.split(os.path.realpath(__file__))
+ self.shader_dir = os.path.join(base_dir, 'shaders')
+
+ def get_program(self, vertex_shader, fragment_shader,
+ geometry_shader=None, defines=None):
+ """Get a program via a list of shader files to include in the program.
+
+ Parameters
+ ----------
+ vertex_shader : str
+ The vertex shader filename.
+ fragment_shader : str
+ The fragment shader filename.
+ geometry_shader : str
+ The geometry shader filename.
+ defines : dict
+ Defines and their values for the shader.
+
+ Returns
+ -------
+ program : :class:`.ShaderProgram`
+ The program.
+ """
+ shader_names = []
+ if defines is None:
+ defines = {}
+ shader_filenames = [
+ x for x in [vertex_shader, fragment_shader, geometry_shader]
+ if x is not None
+ ]
+ for fn in shader_filenames:
+ if fn is None:
+ continue
+ _, name = os.path.split(fn)
+ shader_names.append(name)
+ cid = OpenGL.contextdata.getContext()
+ key = tuple([cid] + sorted(
+ [(s,1) for s in shader_names] + [(d, defines[d]) for d in defines]
+ ))
+
+ if key not in self._program_cache:
+ shader_filenames = [
+ os.path.join(self.shader_dir, fn) for fn in shader_filenames
+ ]
+ if len(shader_filenames) == 2:
+ shader_filenames.append(None)
+ vs, fs, gs = shader_filenames
+ self._program_cache[key] = ShaderProgram(
+ vertex_shader=vs, fragment_shader=fs,
+ geometry_shader=gs, defines=defines
+ )
+ return self._program_cache[key]
+
+ def clear(self):
+ for key in self._program_cache:
+ self._program_cache[key].delete()
+ self._program_cache = {}
+
+
+class ShaderProgram(object):
+ """A thin wrapper about OpenGL shader programs that supports easy creation,
+ binding, and uniform-setting.
+
+ Parameters
+ ----------
+ vertex_shader : str
+ The vertex shader filename.
+ fragment_shader : str
+ The fragment shader filename.
+ geometry_shader : str
+ The geometry shader filename.
+ defines : dict
+ Defines and their values for the shader.
+ """
+
+ def __init__(self, vertex_shader, fragment_shader,
+ geometry_shader=None, defines=None):
+
+ self.vertex_shader = vertex_shader
+ self.fragment_shader = fragment_shader
+ self.geometry_shader = geometry_shader
+
+ self.defines = defines
+ if self.defines is None:
+ self.defines = {}
+
+ self._program_id = None
+ self._vao_id = None # PYOPENGL BUG
+
+ # DEBUG
+ # self._unif_map = {}
+
+ def _add_to_context(self):
+ if self._program_id is not None:
+ raise ValueError('Shader program already in context')
+ shader_ids = []
+
+ # Load vert shader
+ shader_ids.append(gl_shader_utils.compileShader(
+ self._load(self.vertex_shader), GL_VERTEX_SHADER)
+ )
+ # Load frag shader
+ shader_ids.append(gl_shader_utils.compileShader(
+ self._load(self.fragment_shader), GL_FRAGMENT_SHADER)
+ )
+ # Load geometry shader
+ if self.geometry_shader is not None:
+ shader_ids.append(gl_shader_utils.compileShader(
+ self._load(self.geometry_shader), GL_GEOMETRY_SHADER)
+ )
+
+ # Bind empty VAO PYOPENGL BUG
+ if self._vao_id is None:
+ self._vao_id = glGenVertexArrays(1)
+ glBindVertexArray(self._vao_id)
+
+ # Compile program
+ self._program_id = gl_shader_utils.compileProgram(*shader_ids)
+
+ # Unbind empty VAO PYOPENGL BUG
+ glBindVertexArray(0)
+
+ def _in_context(self):
+ return self._program_id is not None
+
+ def _remove_from_context(self):
+ if self._program_id is not None:
+ glDeleteProgram(self._program_id)
+ glDeleteVertexArrays(1, [self._vao_id])
+ self._program_id = None
+ self._vao_id = None
+
+ def _load(self, shader_filename):
+ path, _ = os.path.split(shader_filename)
+
+ with open(shader_filename) as f:
+ text = f.read()
+
+ def ifdef(matchobj):
+ if matchobj.group(1) in self.defines:
+ return '#if 1'
+ else:
+ return '#if 0'
+
+ def ifndef(matchobj):
+ if matchobj.group(1) in self.defines:
+ return '#if 0'
+ else:
+ return '#if 1'
+
+ ifdef_regex = re.compile(
+ '#ifdef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE
+ )
+ ifndef_regex = re.compile(
+ '#ifndef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE
+ )
+ text = re.sub(ifdef_regex, ifdef, text)
+ text = re.sub(ifndef_regex, ifndef, text)
+
+ for define in self.defines:
+ value = str(self.defines[define])
+ text = text.replace(define, value)
+
+ return text
+
+ def _bind(self):
+ """Bind this shader program to the current OpenGL context.
+ """
+ if self._program_id is None:
+ raise ValueError('Cannot bind program that is not in context')
+ # glBindVertexArray(self._vao_id)
+ glUseProgram(self._program_id)
+
+ def _unbind(self):
+ """Unbind this shader program from the current OpenGL context.
+ """
+ glUseProgram(0)
+
+ def delete(self):
+ """Delete this shader program from the current OpenGL context.
+ """
+ self._remove_from_context()
+
+ def set_uniform(self, name, value, unsigned=False):
+ """Set a uniform value in the current shader program.
+
+ Parameters
+ ----------
+ name : str
+ Name of the uniform to set.
+ value : int, float, or ndarray
+ Value to set the uniform to.
+ unsigned : bool
+ If True, ints will be treated as unsigned values.
+ """
+ try:
+ # DEBUG
+ # self._unif_map[name] = 1, (1,)
+ loc = glGetUniformLocation(self._program_id, name)
+
+ if loc == -1:
+ raise ValueError('Invalid shader variable: {}'.format(name))
+
+ if isinstance(value, np.ndarray):
+ # DEBUG
+ # self._unif_map[name] = value.size, value.shape
+ if value.ndim == 1:
+ if (np.issubdtype(value.dtype, np.unsignedinteger) or
+ unsigned):
+ dtype = 'u'
+ value = value.astype(np.uint32)
+ elif np.issubdtype(value.dtype, np.integer):
+ dtype = 'i'
+ value = value.astype(np.int32)
+ else:
+ dtype = 'f'
+ value = value.astype(np.float32)
+ self._FUNC_MAP[(value.shape[0], dtype)](loc, 1, value)
+ else:
+ self._FUNC_MAP[(value.shape[0], value.shape[1])](
+ loc, 1, GL_TRUE, value
+ )
+
+ # Call correct uniform function
+ elif isinstance(value, float):
+ glUniform1f(loc, value)
+ elif isinstance(value, int):
+ if unsigned:
+ glUniform1ui(loc, value)
+ else:
+ glUniform1i(loc, value)
+ elif isinstance(value, bool):
+ if unsigned:
+ glUniform1ui(loc, int(value))
+ else:
+ glUniform1i(loc, int(value))
+ else:
+ raise ValueError('Invalid data type')
+ except Exception:
+ pass
+
+ _FUNC_MAP = {
+ (1,'u'): glUniform1uiv,
+ (2,'u'): glUniform2uiv,
+ (3,'u'): glUniform3uiv,
+ (4,'u'): glUniform4uiv,
+ (1,'i'): glUniform1iv,
+ (2,'i'): glUniform2iv,
+ (3,'i'): glUniform3iv,
+ (4,'i'): glUniform4iv,
+ (1,'f'): glUniform1fv,
+ (2,'f'): glUniform2fv,
+ (3,'f'): glUniform3fv,
+ (4,'f'): glUniform4fv,
+ (2,2): glUniformMatrix2fv,
+ (2,3): glUniformMatrix2x3fv,
+ (2,4): glUniformMatrix2x4fv,
+ (3,2): glUniformMatrix3x2fv,
+ (3,3): glUniformMatrix3fv,
+ (3,4): glUniformMatrix3x4fv,
+ (4,2): glUniformMatrix4x2fv,
+ (4,3): glUniformMatrix4x3fv,
+ (4,4): glUniformMatrix4fv,
+ }
diff --git a/pyrender/pyrender/shaders/debug_quad.frag b/pyrender/pyrender/shaders/debug_quad.frag
new file mode 100644
index 0000000000000000000000000000000000000000..4647bb50dfa1e4510e2d4afb37959c7f57532eca
--- /dev/null
+++ b/pyrender/pyrender/shaders/debug_quad.frag
@@ -0,0 +1,23 @@
+#version 330 core
+out vec4 FragColor;
+
+in vec2 TexCoords;
+
+uniform sampler2D depthMap;
+//uniform float near_plane;
+//uniform float far_plane;
+//
+//// required when using a perspective projection matrix
+//float LinearizeDepth(float depth)
+//{
+// float z = depth * 2.0 - 1.0; // Back to NDC
+// return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
+//}
+
+void main()
+{
+ float depthValue = texture(depthMap, TexCoords).r;
+ // FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
+ FragColor = vec4(vec3(depthValue), 1.0); // orthographic
+ //FragColor = vec4(1.0, 1.0, 0.0, 1.0);
+}
diff --git a/pyrender/pyrender/shaders/debug_quad.vert b/pyrender/pyrender/shaders/debug_quad.vert
new file mode 100644
index 0000000000000000000000000000000000000000..d2f2fcb7626f6c22e0d52bf4d6c91251cbdb9f52
--- /dev/null
+++ b/pyrender/pyrender/shaders/debug_quad.vert
@@ -0,0 +1,25 @@
+#version 330 core
+//layout (location = 0) in vec3 aPos;
+//layout (location = 1) in vec2 aTexCoords;
+//
+//out vec2 TexCoords;
+//
+//void main()
+//{
+// TexCoords = aTexCoords;
+// gl_Position = vec4(aPos, 1.0);
+//}
+//
+//
+//layout(location = 0) out vec2 uv;
+
+out vec2 TexCoords;
+
+void main()
+{
+ float x = float(((uint(gl_VertexID) + 2u) / 3u)%2u);
+ float y = float(((uint(gl_VertexID) + 1u) / 3u)%2u);
+
+ gl_Position = vec4(-1.0f + x*2.0f, -1.0f+y*2.0f, 0.0f, 1.0f);
+ TexCoords = vec2(x, y);
+}
diff --git a/pyrender/pyrender/shaders/flat.frag b/pyrender/pyrender/shaders/flat.frag
new file mode 100644
index 0000000000000000000000000000000000000000..7ec01c6d095ec5dacc693accd3ad507ced61a79a
--- /dev/null
+++ b/pyrender/pyrender/shaders/flat.frag
@@ -0,0 +1,126 @@
+#version 330 core
+///////////////////////////////////////////////////////////////////////////////
+// Structs
+///////////////////////////////////////////////////////////////////////////////
+
+struct Material {
+ vec3 emissive_factor;
+
+#ifdef USE_METALLIC_MATERIAL
+ vec4 base_color_factor;
+ float metallic_factor;
+ float roughness_factor;
+#endif
+
+#ifdef USE_GLOSSY_MATERIAL
+ vec4 diffuse_factor;
+ vec3 specular_factor;
+ float glossiness_factor;
+#endif
+
+#ifdef HAS_NORMAL_TEX
+ sampler2D normal_texture;
+#endif
+#ifdef HAS_OCCLUSION_TEX
+ sampler2D occlusion_texture;
+#endif
+#ifdef HAS_EMISSIVE_TEX
+ sampler2D emissive_texture;
+#endif
+#ifdef HAS_BASE_COLOR_TEX
+ sampler2D base_color_texture;
+#endif
+#ifdef HAS_METALLIC_ROUGHNESS_TEX
+ sampler2D metallic_roughness_texture;
+#endif
+#ifdef HAS_DIFFUSE_TEX
+ sampler2D diffuse_texture;
+#endif
+#ifdef HAS_SPECULAR_GLOSSINESS_TEX
+ sampler2D specular_glossiness;
+#endif
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Uniforms
+///////////////////////////////////////////////////////////////////////////////
+uniform Material material;
+uniform vec3 cam_pos;
+
+#ifdef USE_IBL
+uniform samplerCube diffuse_env;
+uniform samplerCube specular_env;
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+// Inputs
+///////////////////////////////////////////////////////////////////////////////
+
+in vec3 frag_position;
+#ifdef NORMAL_LOC
+in vec3 frag_normal;
+#endif
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+in mat3 tbn;
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+in vec2 uv_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+in vec2 uv_1;
+#endif
+#ifdef COLOR_0_LOC
+in vec4 color_multiplier;
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+// OUTPUTS
+///////////////////////////////////////////////////////////////////////////////
+
+out vec4 frag_color;
+
+///////////////////////////////////////////////////////////////////////////////
+// Constants
+///////////////////////////////////////////////////////////////////////////////
+const float PI = 3.141592653589793;
+const float min_roughness = 0.04;
+
+///////////////////////////////////////////////////////////////////////////////
+// Utility Functions
+///////////////////////////////////////////////////////////////////////////////
+vec4 srgb_to_linear(vec4 srgb)
+{
+#ifndef SRGB_CORRECTED
+ // Fast Approximation
+ //vec3 linOut = pow(srgbIn.xyz,vec3(2.2));
+ //
+ vec3 b_less = step(vec3(0.04045),srgb.xyz);
+ vec3 lin_out = mix( srgb.xyz/vec3(12.92), pow((srgb.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), b_less );
+ return vec4(lin_out, srgb.w);
+#else
+ return srgb;
+#endif
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// MAIN
+///////////////////////////////////////////////////////////////////////////////
+void main()
+{
+
+ // Compute albedo
+ vec4 base_color = material.base_color_factor;
+#ifdef HAS_BASE_COLOR_TEX
+ base_color = base_color * texture(material.base_color_texture, uv_0);
+#endif
+
+#ifdef COLOR_0_LOC
+ base_color *= color_multiplier;
+#endif
+
+ frag_color = clamp(base_color, 0.0, 1.0);
+}
diff --git a/pyrender/pyrender/shaders/flat.vert b/pyrender/pyrender/shaders/flat.vert
new file mode 100644
index 0000000000000000000000000000000000000000..cfd241c3544718a261f961c3aa3c03aa13c97761
--- /dev/null
+++ b/pyrender/pyrender/shaders/flat.vert
@@ -0,0 +1,86 @@
+#version 330 core
+
+// Vertex Attributes
+layout(location = 0) in vec3 position;
+#ifdef NORMAL_LOC
+layout(location = NORMAL_LOC) in vec3 normal;
+#endif
+#ifdef TANGENT_LOC
+layout(location = TANGENT_LOC) in vec4 tangent;
+#endif
+#ifdef TEXCOORD_0_LOC
+layout(location = TEXCOORD_0_LOC) in vec2 texcoord_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+layout(location = TEXCOORD_1_LOC) in vec2 texcoord_1;
+#endif
+#ifdef COLOR_0_LOC
+layout(location = COLOR_0_LOC) in vec4 color_0;
+#endif
+#ifdef JOINTS_0_LOC
+layout(location = JOINTS_0_LOC) in vec4 joints_0;
+#endif
+#ifdef WEIGHTS_0_LOC
+layout(location = WEIGHTS_0_LOC) in vec4 weights_0;
+#endif
+layout(location = INST_M_LOC) in mat4 inst_m;
+
+// Uniforms
+uniform mat4 M;
+uniform mat4 V;
+uniform mat4 P;
+
+// Outputs
+out vec3 frag_position;
+#ifdef NORMAL_LOC
+out vec3 frag_normal;
+#endif
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+out mat3 tbn;
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+out vec2 uv_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+out vec2 uv_1;
+#endif
+#ifdef COLOR_0_LOC
+out vec4 color_multiplier;
+#endif
+
+
+void main()
+{
+ gl_Position = P * V * M * inst_m * vec4(position, 1);
+ frag_position = vec3(M * inst_m * vec4(position, 1.0));
+
+ mat4 N = transpose(inverse(M * inst_m));
+
+#ifdef NORMAL_LOC
+ frag_normal = normalize(vec3(N * vec4(normal, 0.0)));
+#endif
+
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+ vec3 normal_w = normalize(vec3(N * vec4(normal, 0.0)));
+ vec3 tangent_w = normalize(vec3(N * vec4(tangent.xyz, 0.0)));
+ vec3 bitangent_w = cross(normal_w, tangent_w) * tangent.w;
+ tbn = mat3(tangent_w, bitangent_w, normal_w);
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+ uv_0 = texcoord_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+ uv_1 = texcoord_1;
+#endif
+#ifdef COLOR_0_LOC
+ color_multiplier = color_0;
+#endif
+}
diff --git a/pyrender/pyrender/shaders/mesh.frag b/pyrender/pyrender/shaders/mesh.frag
new file mode 100644
index 0000000000000000000000000000000000000000..43187621b4388b18badf4e562a7ad300e59b029d
--- /dev/null
+++ b/pyrender/pyrender/shaders/mesh.frag
@@ -0,0 +1,456 @@
+#version 330 core
+///////////////////////////////////////////////////////////////////////////////
+// Structs
+///////////////////////////////////////////////////////////////////////////////
+
+struct SpotLight {
+ vec3 color;
+ float intensity;
+ float range;
+ vec3 position;
+ vec3 direction;
+ float light_angle_scale;
+ float light_angle_offset;
+
+ #ifdef SPOT_LIGHT_SHADOWS
+ sampler2D shadow_map;
+ mat4 light_matrix;
+ #endif
+};
+
+struct DirectionalLight {
+ vec3 color;
+ float intensity;
+ vec3 direction;
+
+ #ifdef DIRECTIONAL_LIGHT_SHADOWS
+ sampler2D shadow_map;
+ mat4 light_matrix;
+ #endif
+};
+
+struct PointLight {
+ vec3 color;
+ float intensity;
+ float range;
+ vec3 position;
+
+ #ifdef POINT_LIGHT_SHADOWS
+ samplerCube shadow_map;
+ #endif
+};
+
+struct Material {
+ vec3 emissive_factor;
+
+#ifdef USE_METALLIC_MATERIAL
+ vec4 base_color_factor;
+ float metallic_factor;
+ float roughness_factor;
+#endif
+
+#ifdef USE_GLOSSY_MATERIAL
+ vec4 diffuse_factor;
+ vec3 specular_factor;
+ float glossiness_factor;
+#endif
+
+#ifdef HAS_NORMAL_TEX
+ sampler2D normal_texture;
+#endif
+#ifdef HAS_OCCLUSION_TEX
+ sampler2D occlusion_texture;
+#endif
+#ifdef HAS_EMISSIVE_TEX
+ sampler2D emissive_texture;
+#endif
+#ifdef HAS_BASE_COLOR_TEX
+ sampler2D base_color_texture;
+#endif
+#ifdef HAS_METALLIC_ROUGHNESS_TEX
+ sampler2D metallic_roughness_texture;
+#endif
+#ifdef HAS_DIFFUSE_TEX
+ sampler2D diffuse_texture;
+#endif
+#ifdef HAS_SPECULAR_GLOSSINESS_TEX
+ sampler2D specular_glossiness;
+#endif
+};
+
+struct PBRInfo {
+ float nl;
+ float nv;
+ float nh;
+ float lh;
+ float vh;
+ float roughness;
+ float metallic;
+ vec3 f0;
+ vec3 c_diff;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Uniforms
+///////////////////////////////////////////////////////////////////////////////
+uniform Material material;
+uniform PointLight point_lights[MAX_POINT_LIGHTS];
+uniform int n_point_lights;
+uniform DirectionalLight directional_lights[MAX_DIRECTIONAL_LIGHTS];
+uniform int n_directional_lights;
+uniform SpotLight spot_lights[MAX_SPOT_LIGHTS];
+uniform int n_spot_lights;
+uniform vec3 cam_pos;
+uniform vec3 ambient_light;
+
+#ifdef USE_IBL
+uniform samplerCube diffuse_env;
+uniform samplerCube specular_env;
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+// Inputs
+///////////////////////////////////////////////////////////////////////////////
+
+in vec3 frag_position;
+#ifdef NORMAL_LOC
+in vec3 frag_normal;
+#endif
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+in mat3 tbn;
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+in vec2 uv_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+in vec2 uv_1;
+#endif
+#ifdef COLOR_0_LOC
+in vec4 color_multiplier;
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+// OUTPUTS
+///////////////////////////////////////////////////////////////////////////////
+
+out vec4 frag_color;
+
+///////////////////////////////////////////////////////////////////////////////
+// Constants
+///////////////////////////////////////////////////////////////////////////////
+const float PI = 3.141592653589793;
+const float min_roughness = 0.04;
+
+///////////////////////////////////////////////////////////////////////////////
+// Utility Functions
+///////////////////////////////////////////////////////////////////////////////
+vec4 srgb_to_linear(vec4 srgb)
+{
+#ifndef SRGB_CORRECTED
+ // Fast Approximation
+ //vec3 linOut = pow(srgbIn.xyz,vec3(2.2));
+ //
+ vec3 b_less = step(vec3(0.04045),srgb.xyz);
+ vec3 lin_out = mix( srgb.xyz/vec3(12.92), pow((srgb.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), b_less );
+ return vec4(lin_out, srgb.w);
+#else
+ return srgb;
+#endif
+}
+
+// Normal computation
+vec3 get_normal()
+{
+#ifdef HAS_NORMAL_TEX
+
+#ifndef HAS_TANGENTS
+ vec3 pos_dx = dFdx(frag_position);
+ vec3 pos_dy = dFdy(frag_position);
+ vec3 tex_dx = dFdx(vec3(uv_0, 0.0));
+ vec3 tex_dy = dFdy(vec3(uv_0, 0.0));
+ vec3 t = (tex_dy.t * pos_dx - tex_dx.t * pos_dy) / (tex_dx.s * tex_dy.t - tex_dy.s * tex_dx.t);
+
+#ifdef NORMAL_LOC
+ vec3 ng = normalize(frag_normal);
+#else
+ vec3 = cross(pos_dx, pos_dy);
+#endif
+
+ t = normalize(t - ng * dot(ng, t));
+ vec3 b = normalize(cross(ng, t));
+ mat3 tbn_n = mat3(t, b, ng);
+
+#else
+
+ mat3 tbn_n = tbn;
+
+#endif
+
+ vec3 n = texture(material.normal_texture, uv_0).rgb;
+ n = normalize(tbn_n * ((2.0 * n - 1.0) * vec3(1.0, 1.0, 1.0)));
+ return n; // TODO NORMAL MAPPING
+
+#else
+
+#ifdef NORMAL_LOC
+ return frag_normal;
+#else
+ return normalize(cam_pos - frag_position);
+#endif
+
+#endif
+}
+
+// Fresnel
+vec3 specular_reflection(PBRInfo info)
+{
+ vec3 res = info.f0 + (1.0 - info.f0) * pow(clamp(1.0 - info.vh, 0.0, 1.0), 5.0);
+ return res;
+}
+
+// Smith
+float geometric_occlusion(PBRInfo info)
+{
+ float r = info.roughness + 1.0;
+ float k = r * r / 8.0;
+ float g1 = info.nv / (info.nv * (1.0 - k) + k);
+ float g2 = info.nl / (info.nl * (1.0 - k) + k);
+ //float k = info.roughness * sqrt(2.0 / PI);
+ //float g1 = info.lh / (info.lh * (1.0 - k) + k);
+ //float g2 = info.nh / (info.nh * (1.0 - k) + k);
+ return g1 * g2;
+}
+
+float microfacet_distribution(PBRInfo info)
+{
+ float a = info.roughness * info.roughness;
+ float a2 = a * a;
+ float nh2 = info.nh * info.nh;
+
+ float denom = (nh2 * (a2 - 1.0) + 1.0);
+ return a2 / (PI * denom * denom);
+}
+
+vec3 compute_brdf(vec3 n, vec3 v, vec3 l,
+ float roughness, float metalness,
+ vec3 f0, vec3 c_diff, vec3 albedo,
+ vec3 radiance)
+{
+ vec3 h = normalize(l+v);
+ float nl = clamp(dot(n, l), 0.001, 1.0);
+ float nv = clamp(abs(dot(n, v)), 0.001, 1.0);
+ float nh = clamp(dot(n, h), 0.0, 1.0);
+ float lh = clamp(dot(l, h), 0.0, 1.0);
+ float vh = clamp(dot(v, h), 0.0, 1.0);
+
+ PBRInfo info = PBRInfo(nl, nv, nh, lh, vh, roughness, metalness, f0, c_diff);
+
+ // Compute PBR terms
+ vec3 F = specular_reflection(info);
+ float G = geometric_occlusion(info);
+ float D = microfacet_distribution(info);
+
+ // Compute BRDF
+ vec3 diffuse_contrib = (1.0 - F) * c_diff / PI;
+ vec3 spec_contrib = F * G * D / (4.0 * nl * nv + 0.001);
+
+ vec3 color = nl * radiance * (diffuse_contrib + spec_contrib);
+ return color;
+}
+
+float texture2DCompare(sampler2D depths, vec2 uv, float compare) {
+ return compare > texture(depths, uv.xy).r ? 1.0 : 0.0;
+}
+
+float texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare) {
+ vec2 texelSize = vec2(1.0)/size;
+ vec2 f = fract(uv*size+0.5);
+ vec2 centroidUV = floor(uv*size+0.5)/size;
+
+ float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare);
+ float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare);
+ float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare);
+ float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare);
+ float a = mix(lb, lt, f.y);
+ float b = mix(rb, rt, f.y);
+ float c = mix(a, b, f.x);
+ return c;
+}
+
+float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){
+ float result = 0.0;
+ for(int x=-1; x<=1; x++){
+ for(int y=-1; y<=1; y++){
+ vec2 off = vec2(x,y)/size;
+ result += texture2DShadowLerp(depths, size, uv+off, compare);
+ }
+ }
+ return result/9.0;
+}
+
+float shadow_calc(mat4 light_matrix, sampler2D shadow_map, float nl)
+{
+ // Compute light texture UV coords
+ vec4 proj_coords = vec4(light_matrix * vec4(frag_position.xyz, 1.0));
+ vec3 light_coords = proj_coords.xyz / proj_coords.w;
+ light_coords = light_coords * 0.5 + 0.5;
+ float current_depth = light_coords.z;
+ float bias = max(0.001 * (1.0 - nl), 0.0001) / proj_coords.w;
+ float compare = (current_depth - bias);
+ float shadow = PCF(shadow_map, textureSize(shadow_map, 0), light_coords.xy, compare);
+ if (light_coords.z > 1.0) {
+ shadow = 0.0;
+ }
+ return shadow;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// MAIN
+///////////////////////////////////////////////////////////////////////////////
+void main()
+{
+
+ vec4 color = vec4(vec3(0.0), 1.0);
+///////////////////////////////////////////////////////////////////////////////
+// Handle Metallic Materials
+///////////////////////////////////////////////////////////////////////////////
+#ifdef USE_METALLIC_MATERIAL
+
+ // Compute metallic/roughness factors
+ float roughness = material.roughness_factor;
+ float metallic = material.metallic_factor;
+#ifdef HAS_METALLIC_ROUGHNESS_TEX
+ vec2 mr = texture(material.metallic_roughness_texture, uv_0).rg;
+ roughness = roughness * mr.r;
+ metallic = metallic * mr.g;
+#endif
+ roughness = clamp(roughness, min_roughness, 1.0);
+ metallic = clamp(metallic, 0.0, 1.0);
+ // In convention, material roughness is perceputal roughness ^ 2
+ float alpha_roughness = roughness * roughness;
+
+ // Compute albedo
+ vec4 base_color = material.base_color_factor;
+#ifdef HAS_BASE_COLOR_TEX
+ base_color = base_color * srgb_to_linear(texture(material.base_color_texture, uv_0));
+#endif
+
+ // Compute specular and diffuse colors
+ vec3 dialectric_spec = vec3(min_roughness);
+ vec3 c_diff = mix(vec3(0.0), base_color.rgb * (1 - min_roughness), 1.0 - metallic);
+ vec3 f0 = mix(dialectric_spec, base_color.rgb, metallic);
+
+ // Compute normal
+ vec3 n = normalize(get_normal());
+
+ // Loop over lights
+ for (int i = 0; i < n_directional_lights; i++) {
+ vec3 direction = directional_lights[i].direction;
+ vec3 v = normalize(cam_pos - frag_position); // Vector towards camera
+ vec3 l = normalize(-1.0 * direction); // Vector towards light
+
+ // Compute attenuation and radiance
+ float attenuation = directional_lights[i].intensity;
+ vec3 radiance = attenuation * directional_lights[i].color;
+
+ // Compute outbound color
+ vec3 res = compute_brdf(n, v, l, roughness, metallic,
+ f0, c_diff, base_color.rgb, radiance);
+
+ // Compute shadow
+#ifdef DIRECTIONAL_LIGHT_SHADOWS
+ float nl = clamp(dot(n,l), 0.0, 1.0);
+ float shadow = shadow_calc(
+ directional_lights[i].light_matrix,
+ directional_lights[i].shadow_map,
+ nl
+ );
+ res = res * (1.0 - shadow);
+#endif
+ color.xyz += res;
+ }
+
+ for (int i = 0; i < n_point_lights; i++) {
+ vec3 position = point_lights[i].position;
+ vec3 v = normalize(cam_pos - frag_position); // Vector towards camera
+ vec3 l = normalize(position - frag_position); // Vector towards light
+
+ // Compute attenuation and radiance
+ float dist = length(position - frag_position);
+ float attenuation = point_lights[i].intensity / (dist * dist);
+ vec3 radiance = attenuation * point_lights[i].color;
+
+ // Compute outbound color
+ vec3 res = compute_brdf(n, v, l, roughness, metallic,
+ f0, c_diff, base_color.rgb, radiance);
+ color.xyz += res;
+ }
+ for (int i = 0; i < n_spot_lights; i++) {
+ vec3 position = spot_lights[i].position;
+ vec3 v = normalize(cam_pos - frag_position); // Vector towards camera
+ vec3 l = normalize(position - frag_position); // Vector towards light
+
+ // Compute attenuation and radiance
+ vec3 direction = spot_lights[i].direction;
+ float las = spot_lights[i].light_angle_scale;
+ float lao = spot_lights[i].light_angle_offset;
+ float dist = length(position - frag_position);
+ float cd = clamp(dot(direction, -l), 0.0, 1.0);
+ float attenuation = clamp(cd * las + lao, 0.0, 1.0);
+ attenuation = attenuation * attenuation * spot_lights[i].intensity;
+ attenuation = attenuation / (dist * dist);
+ vec3 radiance = attenuation * spot_lights[i].color;
+
+ // Compute outbound color
+ vec3 res = compute_brdf(n, v, l, roughness, metallic,
+ f0, c_diff, base_color.rgb, radiance);
+#ifdef SPOT_LIGHT_SHADOWS
+ float nl = clamp(dot(n,l), 0.0, 1.0);
+ float shadow = shadow_calc(
+ spot_lights[i].light_matrix,
+ spot_lights[i].shadow_map,
+ nl
+ );
+ res = res * (1.0 - shadow);
+#endif
+ color.xyz += res;
+ }
+ color.xyz += base_color.xyz * ambient_light;
+
+ // Calculate lighting from environment
+#ifdef USE_IBL
+ // TODO
+#endif
+
+ // Apply occlusion
+#ifdef HAS_OCCLUSION_TEX
+ float ao = texture(material.occlusion_texture, uv_0).r;
+ color.xyz *= ao;
+#endif
+
+ // Apply emissive map
+ vec3 emissive = material.emissive_factor;
+#ifdef HAS_EMISSIVE_TEX
+ emissive *= srgb_to_linear(texture(material.emissive_texture, uv_0)).rgb;
+#endif
+ color.xyz += emissive * material.emissive_factor;
+
+#ifdef COLOR_0_LOC
+ color *= color_multiplier;
+#endif
+
+ frag_color = clamp(vec4(pow(color.xyz, vec3(1.0/2.2)), color.a * base_color.a), 0.0, 1.0);
+
+#else
+ // TODO GLOSSY MATERIAL BRDF
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+// Handle Glossy Materials
+///////////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/pyrender/pyrender/shaders/mesh.vert b/pyrender/pyrender/shaders/mesh.vert
new file mode 100644
index 0000000000000000000000000000000000000000..cfd241c3544718a261f961c3aa3c03aa13c97761
--- /dev/null
+++ b/pyrender/pyrender/shaders/mesh.vert
@@ -0,0 +1,86 @@
+#version 330 core
+
+// Vertex Attributes
+layout(location = 0) in vec3 position;
+#ifdef NORMAL_LOC
+layout(location = NORMAL_LOC) in vec3 normal;
+#endif
+#ifdef TANGENT_LOC
+layout(location = TANGENT_LOC) in vec4 tangent;
+#endif
+#ifdef TEXCOORD_0_LOC
+layout(location = TEXCOORD_0_LOC) in vec2 texcoord_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+layout(location = TEXCOORD_1_LOC) in vec2 texcoord_1;
+#endif
+#ifdef COLOR_0_LOC
+layout(location = COLOR_0_LOC) in vec4 color_0;
+#endif
+#ifdef JOINTS_0_LOC
+layout(location = JOINTS_0_LOC) in vec4 joints_0;
+#endif
+#ifdef WEIGHTS_0_LOC
+layout(location = WEIGHTS_0_LOC) in vec4 weights_0;
+#endif
+layout(location = INST_M_LOC) in mat4 inst_m;
+
+// Uniforms
+uniform mat4 M;
+uniform mat4 V;
+uniform mat4 P;
+
+// Outputs
+out vec3 frag_position;
+#ifdef NORMAL_LOC
+out vec3 frag_normal;
+#endif
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+out mat3 tbn;
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+out vec2 uv_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+out vec2 uv_1;
+#endif
+#ifdef COLOR_0_LOC
+out vec4 color_multiplier;
+#endif
+
+
+void main()
+{
+ gl_Position = P * V * M * inst_m * vec4(position, 1);
+ frag_position = vec3(M * inst_m * vec4(position, 1.0));
+
+ mat4 N = transpose(inverse(M * inst_m));
+
+#ifdef NORMAL_LOC
+ frag_normal = normalize(vec3(N * vec4(normal, 0.0)));
+#endif
+
+#ifdef HAS_NORMAL_TEX
+#ifdef TANGENT_LOC
+#ifdef NORMAL_LOC
+ vec3 normal_w = normalize(vec3(N * vec4(normal, 0.0)));
+ vec3 tangent_w = normalize(vec3(N * vec4(tangent.xyz, 0.0)));
+ vec3 bitangent_w = cross(normal_w, tangent_w) * tangent.w;
+ tbn = mat3(tangent_w, bitangent_w, normal_w);
+#endif
+#endif
+#endif
+#ifdef TEXCOORD_0_LOC
+ uv_0 = texcoord_0;
+#endif
+#ifdef TEXCOORD_1_LOC
+ uv_1 = texcoord_1;
+#endif
+#ifdef COLOR_0_LOC
+ color_multiplier = color_0;
+#endif
+}
diff --git a/pyrender/pyrender/shaders/mesh_depth.frag b/pyrender/pyrender/shaders/mesh_depth.frag
new file mode 100644
index 0000000000000000000000000000000000000000..d8b1fac6091cfa457ba835ae0758e955f06d8754
--- /dev/null
+++ b/pyrender/pyrender/shaders/mesh_depth.frag
@@ -0,0 +1,8 @@
+#version 330 core
+
+out vec4 frag_color;
+
+void main()
+{
+ frag_color = vec4(1.0);
+}
diff --git a/pyrender/pyrender/shaders/mesh_depth.vert b/pyrender/pyrender/shaders/mesh_depth.vert
new file mode 100644
index 0000000000000000000000000000000000000000..e534c058fb3e7b0efbec090513d55982db68ccaf
--- /dev/null
+++ b/pyrender/pyrender/shaders/mesh_depth.vert
@@ -0,0 +1,13 @@
+#version 330 core
+layout(location = 0) in vec3 position;
+layout(location = INST_M_LOC) in mat4 inst_m;
+
+uniform mat4 P;
+uniform mat4 V;
+uniform mat4 M;
+
+void main()
+{
+ mat4 light_matrix = P * V;
+ gl_Position = light_matrix * M * inst_m * vec4(position, 1.0);
+}
diff --git a/pyrender/pyrender/shaders/segmentation.frag b/pyrender/pyrender/shaders/segmentation.frag
new file mode 100644
index 0000000000000000000000000000000000000000..40deb92cbdef3ec9fd952632624cd5f4b5ce0c84
--- /dev/null
+++ b/pyrender/pyrender/shaders/segmentation.frag
@@ -0,0 +1,13 @@
+#version 330 core
+
+uniform vec3 color;
+out vec4 frag_color;
+
+///////////////////////////////////////////////////////////////////////////////
+// MAIN
+///////////////////////////////////////////////////////////////////////////////
+void main()
+{
+ frag_color = vec4(color, 1.0);
+ //frag_color = vec4(1.0, 0.5, 0.5, 1.0);
+}
diff --git a/pyrender/pyrender/shaders/segmentation.vert b/pyrender/pyrender/shaders/segmentation.vert
new file mode 100644
index 0000000000000000000000000000000000000000..503382599dae3c9415845f35b99d6678cfc7f716
--- /dev/null
+++ b/pyrender/pyrender/shaders/segmentation.vert
@@ -0,0 +1,14 @@
+#version 330 core
+layout(location = 0) in vec3 position;
+layout(location = INST_M_LOC) in mat4 inst_m;
+
+uniform mat4 P;
+uniform mat4 V;
+uniform mat4 M;
+
+void main()
+{
+ mat4 light_matrix = P * V;
+ gl_Position = light_matrix * M * inst_m * vec4(position, 1.0);
+}
+
diff --git a/pyrender/pyrender/shaders/text.frag b/pyrender/pyrender/shaders/text.frag
new file mode 100644
index 0000000000000000000000000000000000000000..486c97dc94ed5e9083ae348bc1e85c5cb26c44dc
--- /dev/null
+++ b/pyrender/pyrender/shaders/text.frag
@@ -0,0 +1,12 @@
+#version 330 core
+in vec2 uv;
+out vec4 color;
+
+uniform sampler2D text;
+uniform vec4 text_color;
+
+void main()
+{
+ vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, uv).r);
+ color = text_color * sampled;
+}
diff --git a/pyrender/pyrender/shaders/text.vert b/pyrender/pyrender/shaders/text.vert
new file mode 100644
index 0000000000000000000000000000000000000000..005bc439b3d63522df99e5db2088953eb8defcf4
--- /dev/null
+++ b/pyrender/pyrender/shaders/text.vert
@@ -0,0 +1,12 @@
+#version 330 core
+layout (location = 0) in vec4 vertex;
+
+out vec2 uv;
+
+uniform mat4 projection;
+
+void main()
+{
+ gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
+ uv = vertex.zw;
+}
diff --git a/pyrender/pyrender/shaders/vertex_normals.frag b/pyrender/pyrender/shaders/vertex_normals.frag
new file mode 100644
index 0000000000000000000000000000000000000000..edf5beb7f283dd67e1710bff922555539966cee4
--- /dev/null
+++ b/pyrender/pyrender/shaders/vertex_normals.frag
@@ -0,0 +1,10 @@
+#version 330 core
+
+out vec4 frag_color;
+
+uniform vec4 normal_color;
+
+void main()
+{
+ frag_color = normal_color;
+}
diff --git a/pyrender/pyrender/shaders/vertex_normals.geom b/pyrender/pyrender/shaders/vertex_normals.geom
new file mode 100644
index 0000000000000000000000000000000000000000..57f0b0e645e72d41116f5767d66fc37d01ed2714
--- /dev/null
+++ b/pyrender/pyrender/shaders/vertex_normals.geom
@@ -0,0 +1,74 @@
+#version 330 core
+
+layout (triangles) in;
+
+#ifdef FACE_NORMALS
+
+#ifdef VERTEX_NORMALS
+ layout (line_strip, max_vertices = 8) out;
+#else
+ layout (line_strip, max_vertices = 2) out;
+#endif
+
+#else
+
+ layout (line_strip, max_vertices = 6) out;
+
+#endif
+
+in VS_OUT {
+ vec3 position;
+ vec3 normal;
+ mat4 mvp;
+} gs_in[];
+
+uniform float normal_magnitude;
+
+void GenerateVertNormal(int index)
+{
+
+ vec4 p0 = gs_in[index].mvp * vec4(gs_in[index].position, 1.0);
+ vec4 p1 = gs_in[index].mvp * vec4(normal_magnitude * normalize(gs_in[index].normal) + gs_in[index].position, 1.0);
+ gl_Position = p0;
+ EmitVertex();
+ gl_Position = p1;
+ EmitVertex();
+ EndPrimitive();
+}
+
+void GenerateFaceNormal()
+{
+ vec3 p0 = gs_in[0].position.xyz;
+ vec3 p1 = gs_in[1].position.xyz;
+ vec3 p2 = gs_in[2].position.xyz;
+
+ vec3 v0 = p0 - p1;
+ vec3 v1 = p2 - p1;
+
+ vec3 N = normalize(cross(v1, v0));
+ vec3 P = (p0 + p1 + p2) / 3.0;
+
+ vec4 np0 = gs_in[0].mvp * vec4(P, 1.0);
+ vec4 np1 = gs_in[0].mvp * vec4(normal_magnitude * N + P, 1.0);
+
+ gl_Position = np0;
+ EmitVertex();
+ gl_Position = np1;
+ EmitVertex();
+ EndPrimitive();
+}
+
+void main()
+{
+
+#ifdef FACE_NORMALS
+ GenerateFaceNormal();
+#endif
+
+#ifdef VERTEX_NORMALS
+ GenerateVertNormal(0);
+ GenerateVertNormal(1);
+ GenerateVertNormal(2);
+#endif
+
+}
diff --git a/pyrender/pyrender/shaders/vertex_normals.vert b/pyrender/pyrender/shaders/vertex_normals.vert
new file mode 100644
index 0000000000000000000000000000000000000000..be22eed2a0e904bcaf1ac5a4721558e574cddc62
--- /dev/null
+++ b/pyrender/pyrender/shaders/vertex_normals.vert
@@ -0,0 +1,27 @@
+#version 330 core
+
+// Inputs
+layout(location = 0) in vec3 position;
+layout(location = NORMAL_LOC) in vec3 normal;
+layout(location = INST_M_LOC) in mat4 inst_m;
+
+// Output data
+out VS_OUT {
+ vec3 position;
+ vec3 normal;
+ mat4 mvp;
+} vs_out;
+
+// Uniform data
+uniform mat4 M;
+uniform mat4 V;
+uniform mat4 P;
+
+// Render loop
+void main() {
+ vs_out.mvp = P * V * M * inst_m;
+ vs_out.position = position;
+ vs_out.normal = normal;
+
+ gl_Position = vec4(position, 1.0);
+}
diff --git a/pyrender/pyrender/shaders/vertex_normals_pc.geom b/pyrender/pyrender/shaders/vertex_normals_pc.geom
new file mode 100644
index 0000000000000000000000000000000000000000..4ea4e7b8542703f64b8d28fd187e425137861fe4
--- /dev/null
+++ b/pyrender/pyrender/shaders/vertex_normals_pc.geom
@@ -0,0 +1,29 @@
+#version 330 core
+
+layout (points) in;
+
+layout (line_strip, max_vertices = 2) out;
+
+in VS_OUT {
+ vec3 position;
+ vec3 normal;
+ mat4 mvp;
+} gs_in[];
+
+uniform float normal_magnitude;
+
+void GenerateVertNormal(int index)
+{
+ vec4 p0 = gs_in[index].mvp * vec4(gs_in[index].position, 1.0);
+ vec4 p1 = gs_in[index].mvp * vec4(normal_magnitude * normalize(gs_in[index].normal) + gs_in[index].position, 1.0);
+ gl_Position = p0;
+ EmitVertex();
+ gl_Position = p1;
+ EmitVertex();
+ EndPrimitive();
+}
+
+void main()
+{
+ GenerateVertNormal(0);
+}
diff --git a/pyrender/pyrender/texture.py b/pyrender/pyrender/texture.py
new file mode 100644
index 0000000000000000000000000000000000000000..477759729d7b995a4f276e81d649617d045a066e
--- /dev/null
+++ b/pyrender/pyrender/texture.py
@@ -0,0 +1,259 @@
+"""Textures, conforming to the glTF 2.0 standards as specified in
+https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-texture
+
+Author: Matthew Matl
+"""
+import numpy as np
+
+from OpenGL.GL import *
+
+from .utils import format_texture_source
+from .sampler import Sampler
+
+
+class Texture(object):
+ """A texture and its sampler.
+
+ Parameters
+ ----------
+ name : str, optional
+ The user-defined name of this object.
+ sampler : :class:`Sampler`
+ The sampler used by this texture.
+ source : (h,w,c) uint8 or (h,w,c) float or :class:`PIL.Image.Image`
+ The image used by this texture. If None, the texture is created
+ empty and width and height must be specified.
+ source_channels : str
+ Either `D`, `R`, `RG`, `GB`, `RGB`, or `RGBA`. Indicates the
+ channels to extract from `source`. Any missing channels will be filled
+ with `1.0`.
+ width : int, optional
+ For empty textures, the width of the texture buffer.
+ height : int, optional
+ For empty textures, the height of the texture buffer.
+ tex_type : int
+ Either GL_TEXTURE_2D or GL_TEXTURE_CUBE.
+ data_format : int
+ For now, just GL_FLOAT.
+ """
+
+ def __init__(self,
+ name=None,
+ sampler=None,
+ source=None,
+ source_channels=None,
+ width=None,
+ height=None,
+ tex_type=GL_TEXTURE_2D,
+ data_format=GL_UNSIGNED_BYTE):
+ self.source_channels = source_channels
+ self.name = name
+ self.sampler = sampler
+ self.source = source
+ self.width = width
+ self.height = height
+ self.tex_type = tex_type
+ self.data_format = data_format
+
+ self._texid = None
+ self._is_transparent = False
+
+ @property
+ def name(self):
+ """str : The user-defined name of this object.
+ """
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ if value is not None:
+ value = str(value)
+ self._name = value
+
+ @property
+ def sampler(self):
+ """:class:`Sampler` : The sampler used by this texture.
+ """
+ return self._sampler
+
+ @sampler.setter
+ def sampler(self, value):
+ if value is None:
+ value = Sampler()
+ self._sampler = value
+
+ @property
+ def source(self):
+ """(h,w,c) uint8 or float or :class:`PIL.Image.Image` : The image
+ used in this texture.
+ """
+ return self._source
+
+ @source.setter
+ def source(self, value):
+ if value is None:
+ self._source = None
+ else:
+ self._source = format_texture_source(value, self.source_channels)
+ self._is_transparent = False
+
+ @property
+ def source_channels(self):
+ """str : The channels that were extracted from the original source.
+ """
+ return self._source_channels
+
+ @source_channels.setter
+ def source_channels(self, value):
+ self._source_channels = value
+
+ @property
+ def width(self):
+ """int : The width of the texture buffer.
+ """
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._width = value
+
+ @property
+ def height(self):
+ """int : The height of the texture buffer.
+ """
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._height = value
+
+ @property
+ def tex_type(self):
+ """int : The type of the texture.
+ """
+ return self._tex_type
+
+ @tex_type.setter
+ def tex_type(self, value):
+ self._tex_type = value
+
+ @property
+ def data_format(self):
+ """int : The format of the texture data.
+ """
+ return self._data_format
+
+ @data_format.setter
+ def data_format(self, value):
+ self._data_format = value
+
+ def is_transparent(self, cutoff=1.0):
+ """bool : If True, the texture is partially transparent.
+ """
+ if self._is_transparent is None:
+ self._is_transparent = False
+ if self.source_channels == 'RGBA' and self.source is not None:
+ if np.any(self.source[:,:,3] < cutoff):
+ self._is_transparent = True
+ return self._is_transparent
+
+ def delete(self):
+ """Remove this texture from the OpenGL context.
+ """
+ self._unbind()
+ self._remove_from_context()
+
+ ##################
+ # OpenGL code
+ ##################
+ def _add_to_context(self):
+ if self._texid is not None:
+ raise ValueError('Texture already loaded into OpenGL context')
+
+ fmt = GL_DEPTH_COMPONENT
+ if self.source_channels == 'R':
+ fmt = GL_RED
+ elif self.source_channels == 'RG' or self.source_channels == 'GB':
+ fmt = GL_RG
+ elif self.source_channels == 'RGB':
+ fmt = GL_RGB
+ elif self.source_channels == 'RGBA':
+ fmt = GL_RGBA
+
+ # Generate the OpenGL texture
+ self._texid = glGenTextures(1)
+ glBindTexture(self.tex_type, self._texid)
+
+ # Flip data for OpenGL buffer
+ data = None
+ width = self.width
+ height = self.height
+ if self.source is not None:
+ data = np.ascontiguousarray(np.flip(self.source, axis=0).flatten())
+ width = self.source.shape[1]
+ height = self.source.shape[0]
+
+ # Bind texture and generate mipmaps
+ glTexImage2D(
+ self.tex_type, 0, fmt, width, height, 0, fmt,
+ self.data_format, data
+ )
+ if self.source is not None:
+ glGenerateMipmap(self.tex_type)
+
+ if self.sampler.magFilter is not None:
+ glTexParameteri(
+ self.tex_type, GL_TEXTURE_MAG_FILTER, self.sampler.magFilter
+ )
+ else:
+ if self.source is not None:
+ glTexParameteri(self.tex_type, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ else:
+ glTexParameteri(self.tex_type, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
+ if self.sampler.minFilter is not None:
+ glTexParameteri(
+ self.tex_type, GL_TEXTURE_MIN_FILTER, self.sampler.minFilter
+ )
+ else:
+ if self.source is not None:
+ glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ else:
+ glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
+
+ glTexParameteri(self.tex_type, GL_TEXTURE_WRAP_S, self.sampler.wrapS)
+ glTexParameteri(self.tex_type, GL_TEXTURE_WRAP_T, self.sampler.wrapT)
+ border_color = 255 * np.ones(4).astype(np.uint8)
+ if self.data_format == GL_FLOAT:
+ border_color = np.ones(4).astype(np.float32)
+ glTexParameterfv(
+ self.tex_type, GL_TEXTURE_BORDER_COLOR,
+ border_color
+ )
+
+ # Unbind texture
+ glBindTexture(self.tex_type, 0)
+
+ def _remove_from_context(self):
+ if self._texid is not None:
+ # TODO OPENGL BUG?
+ # glDeleteTextures(1, [self._texid])
+ glDeleteTextures([self._texid])
+ self._texid = None
+
+ def _in_context(self):
+ return self._texid is not None
+
+ def _bind(self):
+ # TODO HANDLE INDEXING INTO OTHER UV's
+ glBindTexture(self.tex_type, self._texid)
+
+ def _unbind(self):
+ glBindTexture(self.tex_type, 0)
+
+ def _bind_as_depth_attachment(self):
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ self.tex_type, self._texid, 0)
+
+ def _bind_as_color_attachment(self):
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ self.tex_type, self._texid, 0)
diff --git a/pyrender/pyrender/trackball.py b/pyrender/pyrender/trackball.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e57a0e82d3f07b80754f575c28a0e05cb73fc50
--- /dev/null
+++ b/pyrender/pyrender/trackball.py
@@ -0,0 +1,216 @@
+"""Trackball class for 3D manipulation of viewpoints.
+"""
+import numpy as np
+
+import trimesh.transformations as transformations
+
+
+class Trackball(object):
+ """A trackball class for creating camera transforms from mouse movements.
+ """
+ STATE_ROTATE = 0
+ STATE_PAN = 1
+ STATE_ROLL = 2
+ STATE_ZOOM = 3
+
+ def __init__(self, pose, size, scale,
+ target=np.array([0.0, 0.0, 0.0])):
+ """Initialize a trackball with an initial camera-to-world pose
+ and the given parameters.
+
+ Parameters
+ ----------
+ pose : [4,4]
+ An initial camera-to-world pose for the trackball.
+
+ size : (float, float)
+ The width and height of the camera image in pixels.
+
+ scale : float
+ The diagonal of the scene's bounding box --
+ used for ensuring translation motions are sufficiently
+ fast for differently-sized scenes.
+
+ target : (3,) float
+ The center of the scene in world coordinates.
+ The trackball will revolve around this point.
+ """
+ self._size = np.array(size)
+ self._scale = float(scale)
+
+ self._pose = pose
+ self._n_pose = pose
+
+ self._target = target
+ self._n_target = target
+
+ self._state = Trackball.STATE_ROTATE
+
+ @property
+ def pose(self):
+ """autolab_core.RigidTransform : The current camera-to-world pose.
+ """
+ return self._n_pose
+
+ def set_state(self, state):
+ """Set the state of the trackball in order to change the effect of
+ dragging motions.
+
+ Parameters
+ ----------
+ state : int
+ One of Trackball.STATE_ROTATE, Trackball.STATE_PAN,
+ Trackball.STATE_ROLL, and Trackball.STATE_ZOOM.
+ """
+ self._state = state
+
+ def resize(self, size):
+ """Resize the window.
+
+ Parameters
+ ----------
+ size : (float, float)
+ The new width and height of the camera image in pixels.
+ """
+ self._size = np.array(size)
+
+ def down(self, point):
+ """Record an initial mouse press at a given point.
+
+ Parameters
+ ----------
+ point : (2,) int
+ The x and y pixel coordinates of the mouse press.
+ """
+ self._pdown = np.array(point, dtype=np.float32)
+ self._pose = self._n_pose
+ self._target = self._n_target
+
+ def drag(self, point):
+ """Update the tracball during a drag.
+
+ Parameters
+ ----------
+ point : (2,) int
+ The current x and y pixel coordinates of the mouse during a drag.
+ This will compute a movement for the trackball with the relative
+ motion between this point and the one marked by down().
+ """
+ point = np.array(point, dtype=np.float32)
+ dx, dy = point - self._pdown
+ mindim = 0.3 * np.min(self._size)
+
+ target = self._target
+ x_axis = self._pose[:3,0].flatten()
+ y_axis = self._pose[:3,1].flatten()
+ z_axis = self._pose[:3,2].flatten()
+ eye = self._pose[:3,3].flatten()
+
+ # Interpret drag as a rotation
+ if self._state == Trackball.STATE_ROTATE:
+ x_angle = -dx / mindim
+ x_rot_mat = transformations.rotation_matrix(
+ x_angle, y_axis, target
+ )
+
+ y_angle = dy / mindim
+ y_rot_mat = transformations.rotation_matrix(
+ y_angle, x_axis, target
+ )
+
+ self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose))
+
+ # Interpret drag as a roll about the camera axis
+ elif self._state == Trackball.STATE_ROLL:
+ center = self._size / 2.0
+ v_init = self._pdown - center
+ v_curr = point - center
+ v_init = v_init / np.linalg.norm(v_init)
+ v_curr = v_curr / np.linalg.norm(v_curr)
+
+ theta = (-np.arctan2(v_curr[1], v_curr[0]) +
+ np.arctan2(v_init[1], v_init[0]))
+
+ rot_mat = transformations.rotation_matrix(theta, z_axis, target)
+
+ self._n_pose = rot_mat.dot(self._pose)
+
+ # Interpret drag as a camera pan in view plane
+ elif self._state == Trackball.STATE_PAN:
+ dx = -dx / (5.0 * mindim) * self._scale
+ dy = -dy / (5.0 * mindim) * self._scale
+
+ translation = dx * x_axis + dy * y_axis
+ self._n_target = self._target + translation
+ t_tf = np.eye(4)
+ t_tf[:3,3] = translation
+ self._n_pose = t_tf.dot(self._pose)
+
+ # Interpret drag as a zoom motion
+ elif self._state == Trackball.STATE_ZOOM:
+ radius = np.linalg.norm(eye - target)
+ ratio = 0.0
+ if dy > 0:
+ ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0
+ elif dy < 0:
+ ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1])))
+ translation = -np.sign(dy) * ratio * radius * z_axis
+ t_tf = np.eye(4)
+ t_tf[:3,3] = translation
+ self._n_pose = t_tf.dot(self._pose)
+
+ def scroll(self, clicks):
+ """Zoom using a mouse scroll wheel motion.
+
+ Parameters
+ ----------
+ clicks : int
+ The number of clicks. Positive numbers indicate forward wheel
+ movement.
+ """
+ target = self._target
+ ratio = 0.90
+
+ mult = 1.0
+ if clicks > 0:
+ mult = ratio**clicks
+ elif clicks < 0:
+ mult = (1.0 / ratio)**abs(clicks)
+
+ z_axis = self._n_pose[:3,2].flatten()
+ eye = self._n_pose[:3,3].flatten()
+ radius = np.linalg.norm(eye - target)
+ translation = (mult * radius - radius) * z_axis
+ t_tf = np.eye(4)
+ t_tf[:3,3] = translation
+ self._n_pose = t_tf.dot(self._n_pose)
+
+ z_axis = self._pose[:3,2].flatten()
+ eye = self._pose[:3,3].flatten()
+ radius = np.linalg.norm(eye - target)
+ translation = (mult * radius - radius) * z_axis
+ t_tf = np.eye(4)
+ t_tf[:3,3] = translation
+ self._pose = t_tf.dot(self._pose)
+
+ def rotate(self, azimuth, axis=None):
+ """Rotate the trackball about the "Up" axis by azimuth radians.
+
+ Parameters
+ ----------
+ azimuth : float
+ The number of radians to rotate.
+ """
+ target = self._target
+
+ y_axis = self._n_pose[:3,1].flatten()
+ if axis is not None:
+ y_axis = axis
+ x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
+ self._n_pose = x_rot_mat.dot(self._n_pose)
+
+ y_axis = self._pose[:3,1].flatten()
+ if axis is not None:
+ y_axis = axis
+ x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
+ self._pose = x_rot_mat.dot(self._pose)
diff --git a/pyrender/pyrender/utils.py b/pyrender/pyrender/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..48a11faf991606ad7fb0691582f0bc6f06101a45
--- /dev/null
+++ b/pyrender/pyrender/utils.py
@@ -0,0 +1,115 @@
+import numpy as np
+from PIL import Image
+
+
+def format_color_vector(value, length):
+ """Format a color vector.
+ """
+ if isinstance(value, int):
+ value = value / 255.0
+ if isinstance(value, float):
+ value = np.repeat(value, length)
+ if isinstance(value, list) or isinstance(value, tuple):
+ value = np.array(value)
+ if isinstance(value, np.ndarray):
+ value = value.squeeze()
+ if np.issubdtype(value.dtype, np.integer):
+ value = (value / 255.0).astype(np.float32)
+ if value.ndim != 1:
+ raise ValueError('Format vector takes only 1-D vectors')
+ if length > value.shape[0]:
+ value = np.hstack((value, np.ones(length - value.shape[0])))
+ elif length < value.shape[0]:
+ value = value[:length]
+ else:
+ raise ValueError('Invalid vector data type')
+
+ return value.squeeze().astype(np.float32)
+
+
+def format_color_array(value, shape):
+ """Format an array of colors.
+ """
+ # Convert uint8 to floating
+ value = np.asanyarray(value)
+ if np.issubdtype(value.dtype, np.integer):
+ value = (value / 255.0).astype(np.float32)
+
+ # Match up shapes
+ if value.ndim == 1:
+ value = np.tile(value, (shape[0],1))
+ if value.shape[1] < shape[1]:
+ nc = shape[1] - value.shape[1]
+ value = np.column_stack((value, np.ones((value.shape[0], nc))))
+ elif value.shape[1] > shape[1]:
+ value = value[:,:shape[1]]
+ return value.astype(np.float32)
+
+
+def format_texture_source(texture, target_channels='RGB'):
+ """Format a texture as a float32 np array.
+ """
+
+ # Pass through None
+ if texture is None:
+ return None
+
+ # Convert PIL images into numpy arrays
+ if isinstance(texture, Image.Image):
+ if texture.mode == 'P' and target_channels in ('RGB', 'RGBA'):
+ texture = np.array(texture.convert(target_channels))
+ else:
+ texture = np.array(texture)
+
+ # Format numpy arrays
+ if isinstance(texture, np.ndarray):
+ if np.issubdtype(texture.dtype, np.floating):
+ texture = np.array(texture * 255.0, dtype=np.uint8)
+ elif np.issubdtype(texture.dtype, np.integer):
+ texture = texture.astype(np.uint8)
+ else:
+ raise TypeError('Invalid type {} for texture'.format(
+ type(texture)
+ ))
+
+ # Format array by picking out correct texture channels or padding
+ if texture.ndim == 2:
+ texture = texture[:,:,np.newaxis]
+ if target_channels == 'R':
+ texture = texture[:,:,0]
+ texture = texture.squeeze()
+ elif target_channels == 'RG':
+ if texture.shape[2] == 1:
+ texture = np.repeat(texture, 2, axis=2)
+ else:
+ texture = texture[:,:,(0,1)]
+ elif target_channels == 'GB':
+ if texture.shape[2] == 1:
+ texture = np.repeat(texture, 2, axis=2)
+ elif texture.shape[2] > 2:
+ texture = texture[:,:,(1,2)]
+ elif target_channels == 'RGB':
+ if texture.shape[2] == 1:
+ texture = np.repeat(texture, 3, axis=2)
+ elif texture.shape[2] == 2:
+ raise ValueError('Cannot reformat 2-channel texture into RGB')
+ else:
+ texture = texture[:,:,(0,1,2)]
+ elif target_channels == 'RGBA':
+ if texture.shape[2] == 1:
+ texture = np.repeat(texture, 4, axis=2)
+ texture[:,:,3] = 255
+ elif texture.shape[2] == 2:
+ raise ValueError('Cannot reformat 2-channel texture into RGBA')
+ elif texture.shape[2] == 3:
+ tx = np.empty((texture.shape[0], texture.shape[1], 4), dtype=np.uint8)
+ tx[:,:,:3] = texture
+ tx[:,:,3] = 255
+ texture = tx
+ else:
+ raise ValueError('Invalid texture channel specification: {}'
+ .format(target_channels))
+ else:
+ raise TypeError('Invalid type {} for texture'.format(type(texture)))
+
+ return texture
diff --git a/pyrender/pyrender/version.py b/pyrender/pyrender/version.py
new file mode 100644
index 0000000000000000000000000000000000000000..a33fc87f61f528780e3319a5160769cc84512b1b
--- /dev/null
+++ b/pyrender/pyrender/version.py
@@ -0,0 +1 @@
+__version__ = '0.1.45'
diff --git a/pyrender/pyrender/viewer.py b/pyrender/pyrender/viewer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2326c38205c6eaddb4f567e3b088329187af258
--- /dev/null
+++ b/pyrender/pyrender/viewer.py
@@ -0,0 +1,1160 @@
+"""A pyglet-based interactive 3D scene viewer.
+"""
+import copy
+import os
+import sys
+from threading import Thread, RLock
+import time
+
+import imageio
+import numpy as np
+import OpenGL
+import trimesh
+
+try:
+ from Tkinter import Tk, tkFileDialog as filedialog
+except Exception:
+ try:
+ from tkinter import Tk, filedialog as filedialog
+ except Exception:
+ pass
+
+from .constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR,
+ MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR,
+ TEXT_PADDING, DEFAULT_SCENE_SCALE,
+ DEFAULT_Z_FAR, DEFAULT_Z_NEAR, RenderFlags, TextAlign)
+from .light import DirectionalLight
+from .node import Node
+from .camera import PerspectiveCamera, OrthographicCamera, IntrinsicsCamera
+from .trackball import Trackball
+from .renderer import Renderer
+from .mesh import Mesh
+
+import pyglet
+from pyglet import clock
+pyglet.options['shadow_window'] = False
+
+
+class Viewer(pyglet.window.Window):
+ """An interactive viewer for 3D scenes.
+
+ The viewer's camera is separate from the scene's, but will take on
+ the parameters of the scene's main view camera and start in the same pose.
+ If the scene does not have a camera, a suitable default will be provided.
+
+ Parameters
+ ----------
+ scene : :class:`Scene`
+ The scene to visualize.
+ viewport_size : (2,) int
+ The width and height of the initial viewing window.
+ render_flags : dict
+ A set of flags for rendering the scene. Described in the note below.
+ viewer_flags : dict
+ A set of flags for controlling the viewer's behavior.
+ Described in the note below.
+ registered_keys : dict
+ A map from ASCII key characters to tuples containing:
+
+ - A function to be called whenever the key is pressed,
+ whose first argument will be the viewer itself.
+ - (Optionally) A list of additional positional arguments
+ to be passed to the function.
+ - (Optionally) A dict of keyword arguments to be passed
+ to the function.
+
+ kwargs : dict
+ Any keyword arguments left over will be interpreted as belonging to
+ either the :attr:`.Viewer.render_flags` or :attr:`.Viewer.viewer_flags`
+ dictionaries. Those flag sets will be updated appropriately.
+
+ Note
+ ----
+ The basic commands for moving about the scene are given as follows:
+
+ - **Rotating about the scene**: Hold the left mouse button and
+ drag the cursor.
+ - **Rotating about the view axis**: Hold ``CTRL`` and the left mouse
+ button and drag the cursor.
+ - **Panning**:
+
+ - Hold SHIFT, then hold the left mouse button and drag the cursor, or
+ - Hold the middle mouse button and drag the cursor.
+
+ - **Zooming**:
+
+ - Scroll the mouse wheel, or
+ - Hold the right mouse button and drag the cursor.
+
+ Other keyboard commands are as follows:
+
+ - ``a``: Toggles rotational animation mode.
+ - ``c``: Toggles backface culling.
+ - ``f``: Toggles fullscreen mode.
+ - ``h``: Toggles shadow rendering.
+ - ``i``: Toggles axis display mode
+ (no axes, world axis, mesh axes, all axes).
+ - ``l``: Toggles lighting mode
+ (scene lighting, Raymond lighting, or direct lighting).
+ - ``m``: Toggles face normal visualization.
+ - ``n``: Toggles vertex normal visualization.
+ - ``o``: Toggles orthographic mode.
+ - ``q``: Quits the viewer.
+ - ``r``: Starts recording a GIF, and pressing again stops recording
+ and opens a file dialog.
+ - ``s``: Opens a file dialog to save the current view as an image.
+ - ``w``: Toggles wireframe mode
+ (scene default, flip wireframes, all wireframe, or all solid).
+ - ``z``: Resets the camera to the initial view.
+
+ Note
+ ----
+ The valid keys for ``render_flags`` are as follows:
+
+ - ``flip_wireframe``: `bool`, If `True`, all objects will have their
+ wireframe modes flipped from what their material indicates.
+ Defaults to `False`.
+ - ``all_wireframe``: `bool`, If `True`, all objects will be rendered
+ in wireframe mode. Defaults to `False`.
+ - ``all_solid``: `bool`, If `True`, all objects will be rendered in
+ solid mode. Defaults to `False`.
+ - ``shadows``: `bool`, If `True`, shadows will be rendered.
+ Defaults to `False`.
+ - ``vertex_normals``: `bool`, If `True`, vertex normals will be
+ rendered as blue lines. Defaults to `False`.
+ - ``face_normals``: `bool`, If `True`, face normals will be rendered as
+ blue lines. Defaults to `False`.
+ - ``cull_faces``: `bool`, If `True`, backfaces will be culled.
+ Defaults to `True`.
+ - ``point_size`` : float, The point size in pixels. Defaults to 1px.
+
+ Note
+ ----
+ The valid keys for ``viewer_flags`` are as follows:
+
+ - ``rotate``: `bool`, If `True`, the scene's camera will rotate
+ about an axis. Defaults to `False`.
+ - ``rotate_rate``: `float`, The rate of rotation in radians per second.
+ Defaults to `PI / 3.0`.
+ - ``rotate_axis``: `(3,) float`, The axis in world coordinates to rotate
+ about. Defaults to ``[0,0,1]``.
+ - ``view_center``: `(3,) float`, The position to rotate the scene about.
+ Defaults to the scene's centroid.
+ - ``use_raymond_lighting``: `bool`, If `True`, an additional set of three
+ directional lights that move with the camera will be added to the scene.
+ Defaults to `False`.
+ - ``use_direct_lighting``: `bool`, If `True`, an additional directional
+ light that moves with the camera and points out of it will be added to
+ the scene. Defaults to `False`.
+ - ``lighting_intensity``: `float`, The overall intensity of the
+ viewer's additional lights (when they're in use). Defaults to 3.0.
+ - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will
+ be used. Otherwise, an orthographic camera is used. Defaults to `True`.
+ - ``save_directory``: `str`, A directory to open the file dialogs in.
+ Defaults to `None`.
+ - ``window_title``: `str`, A title for the viewer's application window.
+ Defaults to `"Scene Viewer"`.
+ - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz.
+ Defaults to `30.0`.
+ - ``fullscreen``: `bool`, Whether to make viewer fullscreen.
+ Defaults to `False`.
+ - ``show_world_axis``: `bool`, Whether to show the world axis.
+ Defaults to `False`.
+ - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes.
+ Defaults to `False`.
+ - ``caption``: `list of dict`, Text caption(s) to display on the viewer.
+ Defaults to `None`.
+
+ Note
+ ----
+ Animation can be accomplished by running the viewer with ``run_in_thread``
+ enabled. Then, just run a loop in your main thread, updating the scene as
+ needed. Before updating the scene, be sure to acquire the
+ :attr:`.Viewer.render_lock`, and release it when your update is done.
+ """
+
+ def __init__(self, scene, viewport_size=None,
+ render_flags=None, viewer_flags=None,
+ registered_keys=None, run_in_thread=False,
+ auto_start=True,
+ **kwargs):
+
+ #######################################################################
+ # Save attributes and flags
+ #######################################################################
+ if viewport_size is None:
+ viewport_size = (640, 480)
+ self._scene = scene
+ self._viewport_size = viewport_size
+ self._render_lock = RLock()
+ self._is_active = False
+ self._should_close = False
+ self._run_in_thread = run_in_thread
+ self._auto_start = auto_start
+
+ self._default_render_flags = {
+ 'flip_wireframe': False,
+ 'all_wireframe': False,
+ 'all_solid': False,
+ 'shadows': False,
+ 'vertex_normals': False,
+ 'face_normals': False,
+ 'cull_faces': True,
+ 'point_size': 1.0,
+ }
+ self._default_viewer_flags = {
+ 'mouse_pressed': False,
+ 'rotate': False,
+ 'rotate_rate': np.pi / 3.0,
+ 'rotate_axis': np.array([0.0, 0.0, 1.0]),
+ 'view_center': None,
+ 'record': False,
+ 'use_raymond_lighting': False,
+ 'use_direct_lighting': False,
+ 'lighting_intensity': 3.0,
+ 'use_perspective_cam': True,
+ 'save_directory': None,
+ 'window_title': 'Scene Viewer',
+ 'refresh_rate': 30.0,
+ 'fullscreen': False,
+ 'show_world_axis': False,
+ 'show_mesh_axes': False,
+ 'caption': None
+ }
+ self._render_flags = self._default_render_flags.copy()
+ self._viewer_flags = self._default_viewer_flags.copy()
+ self._viewer_flags['rotate_axis'] = (
+ self._default_viewer_flags['rotate_axis'].copy()
+ )
+
+ if render_flags is not None:
+ self._render_flags.update(render_flags)
+ if viewer_flags is not None:
+ self._viewer_flags.update(viewer_flags)
+
+ for key in kwargs:
+ if key in self.render_flags:
+ self._render_flags[key] = kwargs[key]
+ elif key in self.viewer_flags:
+ self._viewer_flags[key] = kwargs[key]
+
+ # TODO MAC OS BUG FOR SHADOWS
+ if sys.platform == 'darwin':
+ self._render_flags['shadows'] = False
+
+ self._registered_keys = {}
+ if registered_keys is not None:
+ self._registered_keys = {
+ ord(k.lower()): registered_keys[k] for k in registered_keys
+ }
+
+ #######################################################################
+ # Save internal settings
+ #######################################################################
+
+ # Set up caption stuff
+ self._message_text = None
+ self._ticks_till_fade = 2.0 / 3.0 * self.viewer_flags['refresh_rate']
+ self._message_opac = 1.0 + self._ticks_till_fade
+
+ # Set up raymond lights and direct lights
+ self._raymond_lights = self._create_raymond_lights()
+ self._direct_light = self._create_direct_light()
+
+ # Set up axes
+ self._axes = {}
+ self._axis_mesh = Mesh.from_trimesh(
+ trimesh.creation.axis(origin_size=0.1, axis_radius=0.05,
+ axis_length=1.0), smooth=False)
+ if self.viewer_flags['show_world_axis']:
+ self._set_axes(world=self.viewer_flags['show_world_axis'],
+ mesh=self.viewer_flags['show_mesh_axes'])
+
+ #######################################################################
+ # Set up camera node
+ #######################################################################
+ self._camera_node = None
+ self._prior_main_camera_node = None
+ self._default_camera_pose = None
+ self._default_persp_cam = None
+ self._default_orth_cam = None
+ self._trackball = None
+ self._saved_frames = []
+
+ # Extract main camera from scene and set up our mirrored copy
+ znear = None
+ zfar = None
+ if scene.main_camera_node is not None:
+ n = scene.main_camera_node
+ camera = copy.copy(n.camera)
+ if isinstance(camera, (PerspectiveCamera, IntrinsicsCamera)):
+ self._default_persp_cam = camera
+ znear = camera.znear
+ zfar = camera.zfar
+ elif isinstance(camera, OrthographicCamera):
+ self._default_orth_cam = camera
+ znear = camera.znear
+ zfar = camera.zfar
+ self._default_camera_pose = scene.get_pose(scene.main_camera_node)
+ self._prior_main_camera_node = n
+
+ # Set defaults as needed
+ if zfar is None:
+ zfar = max(scene.scale * 10.0, DEFAULT_Z_FAR)
+ if znear is None or znear == 0:
+ if scene.scale == 0:
+ znear = DEFAULT_Z_NEAR
+ else:
+ znear = min(scene.scale / 10.0, DEFAULT_Z_NEAR)
+
+ if self._default_persp_cam is None:
+ self._default_persp_cam = PerspectiveCamera(
+ yfov=np.pi / 3.0, znear=znear, zfar=zfar
+ )
+ if self._default_orth_cam is None:
+ xmag = ymag = scene.scale
+ if scene.scale == 0:
+ xmag = ymag = 1.0
+ self._default_orth_cam = OrthographicCamera(
+ xmag=xmag, ymag=ymag,
+ znear=znear,
+ zfar=zfar
+ )
+ if self._default_camera_pose is None:
+ self._default_camera_pose = self._compute_initial_camera_pose()
+
+ # Pick camera
+ if self.viewer_flags['use_perspective_cam']:
+ camera = self._default_persp_cam
+ else:
+ camera = self._default_orth_cam
+
+ self._camera_node = Node(
+ matrix=self._default_camera_pose, camera=camera
+ )
+ scene.add_node(self._camera_node)
+ scene.main_camera_node = self._camera_node
+ self._reset_view()
+
+ #######################################################################
+ # Initialize OpenGL context and renderer
+ #######################################################################
+ self._renderer = Renderer(
+ self._viewport_size[0], self._viewport_size[1],
+ self.render_flags['point_size']
+ )
+ self._is_active = True
+
+ if self.run_in_thread:
+ self._thread = Thread(target=self._init_and_start_app)
+ self._thread.start()
+ else:
+ if auto_start:
+ self._init_and_start_app()
+
+ def start(self):
+ self._init_and_start_app()
+
+ @property
+ def scene(self):
+ """:class:`.Scene` : The scene being visualized.
+ """
+ return self._scene
+
+ @property
+ def viewport_size(self):
+ """(2,) int : The width and height of the viewing window.
+ """
+ return self._viewport_size
+
+ @property
+ def render_lock(self):
+ """:class:`threading.RLock` : If acquired, prevents the viewer from
+ rendering until released.
+
+ Run :meth:`.Viewer.render_lock.acquire` before making updates to
+ the scene in a different thread, and run
+ :meth:`.Viewer.render_lock.release` once you're done to let the viewer
+ continue.
+ """
+ return self._render_lock
+
+ @property
+ def is_active(self):
+ """bool : `True` if the viewer is active, or `False` if it has
+ been closed.
+ """
+ return self._is_active
+
+ @property
+ def run_in_thread(self):
+ """bool : Whether the viewer was run in a separate thread.
+ """
+ return self._run_in_thread
+
+ @property
+ def render_flags(self):
+ """dict : Flags for controlling the renderer's behavior.
+
+ - ``flip_wireframe``: `bool`, If `True`, all objects will have their
+ wireframe modes flipped from what their material indicates.
+ Defaults to `False`.
+ - ``all_wireframe``: `bool`, If `True`, all objects will be rendered
+ in wireframe mode. Defaults to `False`.
+ - ``all_solid``: `bool`, If `True`, all objects will be rendered in
+ solid mode. Defaults to `False`.
+ - ``shadows``: `bool`, If `True`, shadows will be rendered.
+ Defaults to `False`.
+ - ``vertex_normals``: `bool`, If `True`, vertex normals will be
+ rendered as blue lines. Defaults to `False`.
+ - ``face_normals``: `bool`, If `True`, face normals will be rendered as
+ blue lines. Defaults to `False`.
+ - ``cull_faces``: `bool`, If `True`, backfaces will be culled.
+ Defaults to `True`.
+ - ``point_size`` : float, The point size in pixels. Defaults to 1px.
+
+ """
+ return self._render_flags
+
+ @render_flags.setter
+ def render_flags(self, value):
+ self._render_flags = value
+
+ @property
+ def viewer_flags(self):
+ """dict : Flags for controlling the viewer's behavior.
+
+ The valid keys for ``viewer_flags`` are as follows:
+
+ - ``rotate``: `bool`, If `True`, the scene's camera will rotate
+ about an axis. Defaults to `False`.
+ - ``rotate_rate``: `float`, The rate of rotation in radians per second.
+ Defaults to `PI / 3.0`.
+ - ``rotate_axis``: `(3,) float`, The axis in world coordinates to
+ rotate about. Defaults to ``[0,0,1]``.
+ - ``view_center``: `(3,) float`, The position to rotate the scene
+ about. Defaults to the scene's centroid.
+ - ``use_raymond_lighting``: `bool`, If `True`, an additional set of
+ three directional lights that move with the camera will be added to
+ the scene. Defaults to `False`.
+ - ``use_direct_lighting``: `bool`, If `True`, an additional directional
+ light that moves with the camera and points out of it will be
+ added to the scene. Defaults to `False`.
+ - ``lighting_intensity``: `float`, The overall intensity of the
+ viewer's additional lights (when they're in use). Defaults to 3.0.
+ - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will
+ be used. Otherwise, an orthographic camera is used. Defaults to
+ `True`.
+ - ``save_directory``: `str`, A directory to open the file dialogs in.
+ Defaults to `None`.
+ - ``window_title``: `str`, A title for the viewer's application window.
+ Defaults to `"Scene Viewer"`.
+ - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz.
+ Defaults to `30.0`.
+ - ``fullscreen``: `bool`, Whether to make viewer fullscreen.
+ Defaults to `False`.
+ - ``show_world_axis``: `bool`, Whether to show the world axis.
+ Defaults to `False`.
+ - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes.
+ Defaults to `False`.
+ - ``caption``: `list of dict`, Text caption(s) to display on
+ the viewer. Defaults to `None`.
+
+ """
+ return self._viewer_flags
+
+ @viewer_flags.setter
+ def viewer_flags(self, value):
+ self._viewer_flags = value
+
+ @property
+ def registered_keys(self):
+ """dict : Map from ASCII key character to a handler function.
+
+ This is a map from ASCII key characters to tuples containing:
+
+ - A function to be called whenever the key is pressed,
+ whose first argument will be the viewer itself.
+ - (Optionally) A list of additional positional arguments
+ to be passed to the function.
+ - (Optionally) A dict of keyword arguments to be passed
+ to the function.
+
+ """
+ return self._registered_keys
+
+ @registered_keys.setter
+ def registered_keys(self, value):
+ self._registered_keys = value
+
+ def close_external(self):
+ """Close the viewer from another thread.
+
+ This function will wait for the actual close, so you immediately
+ manipulate the scene afterwards.
+ """
+ self._should_close = True
+ while self.is_active:
+ time.sleep(1.0 / self.viewer_flags['refresh_rate'])
+
+ def save_gif(self, filename=None):
+ """Save the stored GIF frames to a file.
+
+ To use this asynchronously, run the viewer with the ``record``
+ flag and the ``run_in_thread`` flags set.
+ Kill the viewer after your desired time with
+ :meth:`.Viewer.close_external`, and then call :meth:`.Viewer.save_gif`.
+
+ Parameters
+ ----------
+ filename : str
+ The file to save the GIF to. If not specified,
+ a file dialog will be opened to ask the user where
+ to save the GIF file.
+ """
+ if filename is None:
+ filename = self._get_save_filename(['gif', 'all'])
+ if filename is not None:
+ self.viewer_flags['save_directory'] = os.path.dirname(filename)
+ imageio.mimwrite(filename, self._saved_frames,
+ fps=self.viewer_flags['refresh_rate'],
+ palettesize=128, subrectangles=True)
+ self._saved_frames = []
+
+ def on_close(self):
+ """Exit the event loop when the window is closed.
+ """
+ # Remove our camera and restore the prior one
+ if self._camera_node is not None:
+ self.scene.remove_node(self._camera_node)
+ if self._prior_main_camera_node is not None:
+ self.scene.main_camera_node = self._prior_main_camera_node
+
+ # Delete any lighting nodes that we've attached
+ if self.viewer_flags['use_raymond_lighting']:
+ for n in self._raymond_lights:
+ if self.scene.has_node(n):
+ self.scene.remove_node(n)
+ if self.viewer_flags['use_direct_lighting']:
+ if self.scene.has_node(self._direct_light):
+ self.scene.remove_node(self._direct_light)
+
+ # Delete any axis nodes that we've attached
+ self._remove_axes()
+
+ # Delete renderer
+ if self._renderer is not None:
+ self._renderer.delete()
+ self._renderer = None
+
+ # Force clean-up of OpenGL context data
+ try:
+ OpenGL.contextdata.cleanupContext()
+ self.close()
+ except Exception:
+ pass
+ finally:
+ self._is_active = False
+ super(Viewer, self).on_close()
+ pyglet.app.exit()
+
+ def on_draw(self):
+ """Redraw the scene into the viewing window.
+ """
+ if self._renderer is None:
+ return
+
+ if self.run_in_thread or not self._auto_start:
+ self.render_lock.acquire()
+
+ # Make OpenGL context current
+ self.switch_to()
+
+ # Render the scene
+ self.clear()
+ self._render()
+
+ if self._message_text is not None:
+ self._renderer.render_text(
+ self._message_text,
+ self.viewport_size[0] - TEXT_PADDING,
+ TEXT_PADDING,
+ font_pt=20,
+ color=np.array([0.1, 0.7, 0.2,
+ np.clip(self._message_opac, 0.0, 1.0)]),
+ align=TextAlign.BOTTOM_RIGHT
+ )
+
+ if self.viewer_flags['caption'] is not None:
+ for caption in self.viewer_flags['caption']:
+ xpos, ypos = self._location_to_x_y(caption['location'])
+ self._renderer.render_text(
+ caption['text'],
+ xpos,
+ ypos,
+ font_name=caption['font_name'],
+ font_pt=caption['font_pt'],
+ color=caption['color'],
+ scale=caption['scale'],
+ align=caption['location']
+ )
+
+ if self.run_in_thread or not self._auto_start:
+ self.render_lock.release()
+
+ def on_resize(self, width, height):
+ """Resize the camera and trackball when the window is resized.
+ """
+ if self._renderer is None:
+ return
+
+ self._viewport_size = (width, height)
+ self._trackball.resize(self._viewport_size)
+ self._renderer.viewport_width = self._viewport_size[0]
+ self._renderer.viewport_height = self._viewport_size[1]
+ self.on_draw()
+
+ def on_mouse_press(self, x, y, buttons, modifiers):
+ """Record an initial mouse press.
+ """
+ self._trackball.set_state(Trackball.STATE_ROTATE)
+ if (buttons == pyglet.window.mouse.LEFT):
+ ctrl = (modifiers & pyglet.window.key.MOD_CTRL)
+ shift = (modifiers & pyglet.window.key.MOD_SHIFT)
+ if (ctrl and shift):
+ self._trackball.set_state(Trackball.STATE_ZOOM)
+ elif ctrl:
+ self._trackball.set_state(Trackball.STATE_ROLL)
+ elif shift:
+ self._trackball.set_state(Trackball.STATE_PAN)
+ elif (buttons == pyglet.window.mouse.MIDDLE):
+ self._trackball.set_state(Trackball.STATE_PAN)
+ elif (buttons == pyglet.window.mouse.RIGHT):
+ self._trackball.set_state(Trackball.STATE_ZOOM)
+
+ self._trackball.down(np.array([x, y]))
+
+ # Stop animating while using the mouse
+ self.viewer_flags['mouse_pressed'] = True
+
+ def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
+ """Record a mouse drag.
+ """
+ self._trackball.drag(np.array([x, y]))
+
+ def on_mouse_release(self, x, y, button, modifiers):
+ """Record a mouse release.
+ """
+ self.viewer_flags['mouse_pressed'] = False
+
+ def on_mouse_scroll(self, x, y, dx, dy):
+ """Record a mouse scroll.
+ """
+ if self.viewer_flags['use_perspective_cam']:
+ self._trackball.scroll(dy)
+ else:
+ spfc = 0.95
+ spbc = 1.0 / 0.95
+ sf = 1.0
+ if dy > 0:
+ sf = spfc * dy
+ elif dy < 0:
+ sf = - spbc * dy
+
+ c = self._camera_node.camera
+ xmag = max(c.xmag * sf, 1e-8)
+ ymag = max(c.ymag * sf, 1e-8 * c.ymag / c.xmag)
+ c.xmag = xmag
+ c.ymag = ymag
+
+ def on_key_press(self, symbol, modifiers):
+ """Record a key press.
+ """
+ # First, check for registered key callbacks
+ if symbol in self.registered_keys:
+ tup = self.registered_keys[symbol]
+ callback = None
+ args = []
+ kwargs = {}
+ if not isinstance(tup, (list, tuple, np.ndarray)):
+ callback = tup
+ else:
+ callback = tup[0]
+ if len(tup) == 2:
+ args = tup[1]
+ if len(tup) == 3:
+ kwargs = tup[2]
+ callback(self, *args, **kwargs)
+ return
+
+ # Otherwise, use default key functions
+
+ # A causes the frame to rotate
+ self._message_text = None
+ if symbol == pyglet.window.key.A:
+ self.viewer_flags['rotate'] = not self.viewer_flags['rotate']
+ if self.viewer_flags['rotate']:
+ self._message_text = 'Rotation On'
+ else:
+ self._message_text = 'Rotation Off'
+
+ # C toggles backface culling
+ elif symbol == pyglet.window.key.C:
+ self.render_flags['cull_faces'] = (
+ not self.render_flags['cull_faces']
+ )
+ if self.render_flags['cull_faces']:
+ self._message_text = 'Cull Faces On'
+ else:
+ self._message_text = 'Cull Faces Off'
+
+ # F toggles face normals
+ elif symbol == pyglet.window.key.F:
+ self.viewer_flags['fullscreen'] = (
+ not self.viewer_flags['fullscreen']
+ )
+ self.set_fullscreen(self.viewer_flags['fullscreen'])
+ self.activate()
+ if self.viewer_flags['fullscreen']:
+ self._message_text = 'Fullscreen On'
+ else:
+ self._message_text = 'Fullscreen Off'
+
+ # S toggles shadows
+ elif symbol == pyglet.window.key.H and sys.platform != 'darwin':
+ self.render_flags['shadows'] = not self.render_flags['shadows']
+ if self.render_flags['shadows']:
+ self._message_text = 'Shadows On'
+ else:
+ self._message_text = 'Shadows Off'
+
+ elif symbol == pyglet.window.key.I:
+ if (self.viewer_flags['show_world_axis'] and not
+ self.viewer_flags['show_mesh_axes']):
+ self.viewer_flags['show_world_axis'] = False
+ self.viewer_flags['show_mesh_axes'] = True
+ self._set_axes(False, True)
+ self._message_text = 'Mesh Axes On'
+ elif (not self.viewer_flags['show_world_axis'] and
+ self.viewer_flags['show_mesh_axes']):
+ self.viewer_flags['show_world_axis'] = True
+ self.viewer_flags['show_mesh_axes'] = True
+ self._set_axes(True, True)
+ self._message_text = 'All Axes On'
+ elif (self.viewer_flags['show_world_axis'] and
+ self.viewer_flags['show_mesh_axes']):
+ self.viewer_flags['show_world_axis'] = False
+ self.viewer_flags['show_mesh_axes'] = False
+ self._set_axes(False, False)
+ self._message_text = 'All Axes Off'
+ else:
+ self.viewer_flags['show_world_axis'] = True
+ self.viewer_flags['show_mesh_axes'] = False
+ self._set_axes(True, False)
+ self._message_text = 'World Axis On'
+
+ # L toggles the lighting mode
+ elif symbol == pyglet.window.key.L:
+ if self.viewer_flags['use_raymond_lighting']:
+ self.viewer_flags['use_raymond_lighting'] = False
+ self.viewer_flags['use_direct_lighting'] = True
+ self._message_text = 'Direct Lighting'
+ elif self.viewer_flags['use_direct_lighting']:
+ self.viewer_flags['use_raymond_lighting'] = False
+ self.viewer_flags['use_direct_lighting'] = False
+ self._message_text = 'Default Lighting'
+ else:
+ self.viewer_flags['use_raymond_lighting'] = True
+ self.viewer_flags['use_direct_lighting'] = False
+ self._message_text = 'Raymond Lighting'
+
+ # M toggles face normals
+ elif symbol == pyglet.window.key.M:
+ self.render_flags['face_normals'] = (
+ not self.render_flags['face_normals']
+ )
+ if self.render_flags['face_normals']:
+ self._message_text = 'Face Normals On'
+ else:
+ self._message_text = 'Face Normals Off'
+
+ # N toggles vertex normals
+ elif symbol == pyglet.window.key.N:
+ self.render_flags['vertex_normals'] = (
+ not self.render_flags['vertex_normals']
+ )
+ if self.render_flags['vertex_normals']:
+ self._message_text = 'Vert Normals On'
+ else:
+ self._message_text = 'Vert Normals Off'
+
+ # O toggles orthographic camera mode
+ elif symbol == pyglet.window.key.O:
+ self.viewer_flags['use_perspective_cam'] = (
+ not self.viewer_flags['use_perspective_cam']
+ )
+ if self.viewer_flags['use_perspective_cam']:
+ camera = self._default_persp_cam
+ self._message_text = 'Perspective View'
+ else:
+ camera = self._default_orth_cam
+ self._message_text = 'Orthographic View'
+
+ cam_pose = self._camera_node.matrix.copy()
+ cam_node = Node(matrix=cam_pose, camera=camera)
+ self.scene.remove_node(self._camera_node)
+ self.scene.add_node(cam_node)
+ self.scene.main_camera_node = cam_node
+ self._camera_node = cam_node
+
+ # Q quits the viewer
+ elif symbol == pyglet.window.key.Q:
+ self.on_close()
+
+ # R starts recording frames
+ elif symbol == pyglet.window.key.R:
+ if self.viewer_flags['record']:
+ self.save_gif()
+ self.set_caption(self.viewer_flags['window_title'])
+ else:
+ self.set_caption(
+ '{} (RECORDING)'.format(self.viewer_flags['window_title'])
+ )
+ self.viewer_flags['record'] = not self.viewer_flags['record']
+
+ # S saves the current frame as an image
+ elif symbol == pyglet.window.key.S:
+ self._save_image()
+
+ # W toggles through wireframe modes
+ elif symbol == pyglet.window.key.W:
+ if self.render_flags['flip_wireframe']:
+ self.render_flags['flip_wireframe'] = False
+ self.render_flags['all_wireframe'] = True
+ self.render_flags['all_solid'] = False
+ self._message_text = 'All Wireframe'
+ elif self.render_flags['all_wireframe']:
+ self.render_flags['flip_wireframe'] = False
+ self.render_flags['all_wireframe'] = False
+ self.render_flags['all_solid'] = True
+ self._message_text = 'All Solid'
+ elif self.render_flags['all_solid']:
+ self.render_flags['flip_wireframe'] = False
+ self.render_flags['all_wireframe'] = False
+ self.render_flags['all_solid'] = False
+ self._message_text = 'Default Wireframe'
+ else:
+ self.render_flags['flip_wireframe'] = True
+ self.render_flags['all_wireframe'] = False
+ self.render_flags['all_solid'] = False
+ self._message_text = 'Flip Wireframe'
+
+ # Z resets the camera viewpoint
+ elif symbol == pyglet.window.key.Z:
+ self._reset_view()
+
+ if self._message_text is not None:
+ self._message_opac = 1.0 + self._ticks_till_fade
+
+ @staticmethod
+ def _time_event(dt, self):
+ """The timer callback.
+ """
+ # Don't run old dead events after we've already closed
+ if not self._is_active:
+ return
+
+ if self.viewer_flags['record']:
+ self._record()
+ if (self.viewer_flags['rotate'] and not
+ self.viewer_flags['mouse_pressed']):
+ self._rotate()
+
+ # Manage message opacity
+ if self._message_text is not None:
+ if self._message_opac > 1.0:
+ self._message_opac -= 1.0
+ else:
+ self._message_opac *= 0.90
+ if self._message_opac < 0.05:
+ self._message_opac = 1.0 + self._ticks_till_fade
+ self._message_text = None
+
+ if self._should_close:
+ self.on_close()
+ else:
+ self.on_draw()
+
+ def _reset_view(self):
+ """Reset the view to a good initial state.
+
+ The view is initially along the positive x-axis at a
+ sufficient distance from the scene.
+ """
+ scale = self.scene.scale
+ if scale == 0.0:
+ scale = DEFAULT_SCENE_SCALE
+ centroid = self.scene.centroid
+
+ if self.viewer_flags['view_center'] is not None:
+ centroid = self.viewer_flags['view_center']
+
+ self._camera_node.matrix = self._default_camera_pose
+ self._trackball = Trackball(
+ self._default_camera_pose, self.viewport_size, scale, centroid
+ )
+
+ def _get_save_filename(self, file_exts):
+ file_types = {
+ 'png': ('png files', '*.png'),
+ 'jpg': ('jpeg files', '*.jpg'),
+ 'gif': ('gif files', '*.gif'),
+ 'all': ('all files', '*'),
+ }
+ filetypes = [file_types[x] for x in file_exts]
+ try:
+ root = Tk()
+ save_dir = self.viewer_flags['save_directory']
+ if save_dir is None:
+ save_dir = os.getcwd()
+ filename = filedialog.asksaveasfilename(
+ initialdir=save_dir, title='Select file save location',
+ filetypes=filetypes
+ )
+ except Exception:
+ return None
+
+ root.destroy()
+ if filename == ():
+ return None
+ return filename
+
+ def _save_image(self):
+ filename = self._get_save_filename(['png', 'jpg', 'gif', 'all'])
+ if filename is not None:
+ self.viewer_flags['save_directory'] = os.path.dirname(filename)
+ imageio.imwrite(filename, self._renderer.read_color_buf())
+
+ def _record(self):
+ """Save another frame for the GIF.
+ """
+ data = self._renderer.read_color_buf()
+ if not np.all(data == 0.0):
+ self._saved_frames.append(data)
+
+ def _rotate(self):
+ """Animate the scene by rotating the camera.
+ """
+ az = (self.viewer_flags['rotate_rate'] /
+ self.viewer_flags['refresh_rate'])
+ self._trackball.rotate(az, self.viewer_flags['rotate_axis'])
+
+ def _render(self):
+ """Render the scene into the framebuffer and flip.
+ """
+ scene = self.scene
+ self._camera_node.matrix = self._trackball.pose.copy()
+
+ # Set lighting
+ vli = self.viewer_flags['lighting_intensity']
+ if self.viewer_flags['use_raymond_lighting']:
+ for n in self._raymond_lights:
+ n.light.intensity = vli / 3.0
+ if not self.scene.has_node(n):
+ scene.add_node(n, parent_node=self._camera_node)
+ else:
+ self._direct_light.light.intensity = vli
+ for n in self._raymond_lights:
+ if self.scene.has_node(n):
+ self.scene.remove_node(n)
+
+ if self.viewer_flags['use_direct_lighting']:
+ if not self.scene.has_node(self._direct_light):
+ scene.add_node(
+ self._direct_light, parent_node=self._camera_node
+ )
+ elif self.scene.has_node(self._direct_light):
+ self.scene.remove_node(self._direct_light)
+
+ flags = RenderFlags.NONE
+ if self.render_flags['flip_wireframe']:
+ flags |= RenderFlags.FLIP_WIREFRAME
+ elif self.render_flags['all_wireframe']:
+ flags |= RenderFlags.ALL_WIREFRAME
+ elif self.render_flags['all_solid']:
+ flags |= RenderFlags.ALL_SOLID
+
+ if self.render_flags['shadows']:
+ flags |= RenderFlags.SHADOWS_DIRECTIONAL | RenderFlags.SHADOWS_SPOT
+ if self.render_flags['vertex_normals']:
+ flags |= RenderFlags.VERTEX_NORMALS
+ if self.render_flags['face_normals']:
+ flags |= RenderFlags.FACE_NORMALS
+ if not self.render_flags['cull_faces']:
+ flags |= RenderFlags.SKIP_CULL_FACES
+
+ self._renderer.render(self.scene, flags)
+
+ def _init_and_start_app(self):
+ # Try multiple configs starting with target OpenGL version
+ # and multisampling and removing these options if exception
+ # Note: multisampling not available on all hardware
+ from pyglet.gl import Config
+ confs = [Config(sample_buffers=1, samples=4,
+ depth_size=24,
+ double_buffer=True,
+ major_version=TARGET_OPEN_GL_MAJOR,
+ minor_version=TARGET_OPEN_GL_MINOR),
+ Config(depth_size=24,
+ double_buffer=True,
+ major_version=TARGET_OPEN_GL_MAJOR,
+ minor_version=TARGET_OPEN_GL_MINOR),
+ Config(sample_buffers=1, samples=4,
+ depth_size=24,
+ double_buffer=True,
+ major_version=MIN_OPEN_GL_MAJOR,
+ minor_version=MIN_OPEN_GL_MINOR),
+ Config(depth_size=24,
+ double_buffer=True,
+ major_version=MIN_OPEN_GL_MAJOR,
+ minor_version=MIN_OPEN_GL_MINOR)]
+ for conf in confs:
+ try:
+ super(Viewer, self).__init__(config=conf, resizable=True,
+ width=self._viewport_size[0],
+ height=self._viewport_size[1])
+ break
+ except pyglet.window.NoSuchConfigException:
+ pass
+
+ if not self.context:
+ raise ValueError('Unable to initialize an OpenGL 3+ context')
+ clock.schedule_interval(
+ Viewer._time_event, 1.0 / self.viewer_flags['refresh_rate'], self
+ )
+ self.switch_to()
+ self.set_caption(self.viewer_flags['window_title'])
+ pyglet.app.run()
+
+ def _compute_initial_camera_pose(self):
+ centroid = self.scene.centroid
+ if self.viewer_flags['view_center'] is not None:
+ centroid = self.viewer_flags['view_center']
+ scale = self.scene.scale
+ if scale == 0.0:
+ scale = DEFAULT_SCENE_SCALE
+
+ s2 = 1.0 / np.sqrt(2.0)
+ cp = np.eye(4)
+ cp[:3,:3] = np.array([
+ [0.0, -s2, s2],
+ [1.0, 0.0, 0.0],
+ [0.0, s2, s2]
+ ])
+ hfov = np.pi / 6.0
+ dist = scale / (2.0 * np.tan(hfov))
+ cp[:3,3] = dist * np.array([1.0, 0.0, 1.0]) + centroid
+
+ return cp
+
+ def _create_raymond_lights(self):
+ thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0])
+ phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0])
+
+ nodes = []
+
+ for phi, theta in zip(phis, thetas):
+ xp = np.sin(theta) * np.cos(phi)
+ yp = np.sin(theta) * np.sin(phi)
+ zp = np.cos(theta)
+
+ z = np.array([xp, yp, zp])
+ z = z / np.linalg.norm(z)
+ x = np.array([-z[1], z[0], 0.0])
+ if np.linalg.norm(x) == 0:
+ x = np.array([1.0, 0.0, 0.0])
+ x = x / np.linalg.norm(x)
+ y = np.cross(z, x)
+
+ matrix = np.eye(4)
+ matrix[:3,:3] = np.c_[x,y,z]
+ nodes.append(Node(
+ light=DirectionalLight(color=np.ones(3), intensity=1.0),
+ matrix=matrix
+ ))
+
+ return nodes
+
+ def _create_direct_light(self):
+ light = DirectionalLight(color=np.ones(3), intensity=1.0)
+ n = Node(light=light, matrix=np.eye(4))
+ return n
+
+ def _set_axes(self, world, mesh):
+ scale = self.scene.scale
+ if world:
+ if 'scene' not in self._axes:
+ n = Node(mesh=self._axis_mesh, scale=np.ones(3) * scale * 0.3)
+ self.scene.add_node(n)
+ self._axes['scene'] = n
+ else:
+ if 'scene' in self._axes:
+ self.scene.remove_node(self._axes['scene'])
+ self._axes.pop('scene')
+
+ if mesh:
+ old_nodes = []
+ existing_axes = set([self._axes[k] for k in self._axes])
+ for node in self.scene.mesh_nodes:
+ if node not in existing_axes:
+ old_nodes.append(node)
+
+ for node in old_nodes:
+ if node in self._axes:
+ continue
+ n = Node(
+ mesh=self._axis_mesh,
+ scale=np.ones(3) * node.mesh.scale * 0.5
+ )
+ self.scene.add_node(n, parent_node=node)
+ self._axes[node] = n
+ else:
+ to_remove = set()
+ for main_node in self._axes:
+ if main_node in self.scene.mesh_nodes:
+ self.scene.remove_node(self._axes[main_node])
+ to_remove.add(main_node)
+ for main_node in to_remove:
+ self._axes.pop(main_node)
+
+ def _remove_axes(self):
+ for main_node in self._axes:
+ axis_node = self._axes[main_node]
+ self.scene.remove_node(axis_node)
+ self._axes = {}
+
+ def _location_to_x_y(self, location):
+ if location == TextAlign.CENTER:
+ return (self.viewport_size[0] / 2.0, self.viewport_size[1] / 2.0)
+ elif location == TextAlign.CENTER_LEFT:
+ return (TEXT_PADDING, self.viewport_size[1] / 2.0)
+ elif location == TextAlign.CENTER_RIGHT:
+ return (self.viewport_size[0] - TEXT_PADDING,
+ self.viewport_size[1] / 2.0)
+ elif location == TextAlign.BOTTOM_LEFT:
+ return (TEXT_PADDING, TEXT_PADDING)
+ elif location == TextAlign.BOTTOM_RIGHT:
+ return (self.viewport_size[0] - TEXT_PADDING, TEXT_PADDING)
+ elif location == TextAlign.BOTTOM_CENTER:
+ return (self.viewport_size[0] / 2.0, TEXT_PADDING)
+ elif location == TextAlign.TOP_LEFT:
+ return (TEXT_PADDING, self.viewport_size[1] - TEXT_PADDING)
+ elif location == TextAlign.TOP_RIGHT:
+ return (self.viewport_size[0] - TEXT_PADDING,
+ self.viewport_size[1] - TEXT_PADDING)
+ elif location == TextAlign.TOP_CENTER:
+ return (self.viewport_size[0] / 2.0,
+ self.viewport_size[1] - TEXT_PADDING)
+
+
+__all__ = ['Viewer']
diff --git a/pyrender/requirements.txt b/pyrender/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8c40b74256f0dc6697754bb8609f69a39d51beba
--- /dev/null
+++ b/pyrender/requirements.txt
@@ -0,0 +1,14 @@
+freetype-py
+imageio
+networkx
+numpy
+Pillow
+pyglet==1.4.0a1
+PyOpenGL
+PyOpenGL_accelerate
+six
+trimesh
+sphinx
+sphinx_rtd_theme
+sphinx-automodapi
+
diff --git a/pyrender/setup.py b/pyrender/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3b5ba0da2b0f17b759e5556597981096a80bda8
--- /dev/null
+++ b/pyrender/setup.py
@@ -0,0 +1,76 @@
+"""
+Setup of pyrender Python codebase.
+
+Author: Matthew Matl
+"""
+import sys
+from setuptools import setup
+
+# load __version__
+exec(open('pyrender/version.py').read())
+
+def get_imageio_dep():
+ if sys.version[0] == "2":
+ return 'imageio<=2.6.1'
+ return 'imageio'
+
+requirements = [
+ 'freetype-py', # For font loading
+ get_imageio_dep(), # For Image I/O
+ 'networkx', # For the scene graph
+ 'numpy', # Numpy
+ 'Pillow', # For Trimesh texture conversions
+ 'pyglet>=1.4.10', # For the pyglet viewer
+ 'PyOpenGL~=3.1.0', # For OpenGL
+# 'PyOpenGL_accelerate~=3.1.0', # For OpenGL
+ 'scipy', # Because of trimesh missing dep
+ 'six', # For Python 2/3 interop
+ 'trimesh', # For meshes
+]
+
+dev_requirements = [
+ 'flake8', # Code formatting checker
+ 'pre-commit', # Pre-commit hooks
+ 'pytest', # Code testing
+ 'pytest-cov', # Coverage testing
+ 'tox', # Automatic virtualenv testing
+]
+
+docs_requirements = [
+ 'sphinx', # General doc library
+ 'sphinx_rtd_theme', # RTD theme for sphinx
+ 'sphinx-automodapi' # For generating nice tables
+]
+
+
+setup(
+ name = 'pyrender',
+ version=__version__,
+ description='Easy-to-use Python renderer for 3D visualization',
+ long_description='A simple implementation of Physically-Based Rendering '
+ '(PBR) in Python. Compliant with the glTF 2.0 standard.',
+ author='Matthew Matl',
+ author_email='matthewcmatl@gmail.com',
+ license='MIT License',
+ url = 'https://github.com/mmatl/pyrender',
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: POSIX :: Linux',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Natural Language :: English',
+ 'Topic :: Scientific/Engineering'
+ ],
+ keywords = 'rendering graphics opengl 3d visualization pbr gltf',
+ packages = ['pyrender', 'pyrender.platforms'],
+ setup_requires = requirements,
+ install_requires = requirements,
+ extras_require={
+ 'dev': dev_requirements,
+ 'docs': docs_requirements,
+ },
+ include_package_data=True
+)
diff --git a/pyrender/tests/__init__.py b/pyrender/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyrender/tests/conftest.py b/pyrender/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyrender/tests/pytest.ini b/pyrender/tests/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyrender/tests/unit/__init__.py b/pyrender/tests/unit/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyrender/tests/unit/test_cameras.py b/pyrender/tests/unit/test_cameras.py
new file mode 100644
index 0000000000000000000000000000000000000000..7544ad8f8e3ee55236fd2e32dbc12065153cbe5b
--- /dev/null
+++ b/pyrender/tests/unit/test_cameras.py
@@ -0,0 +1,164 @@
+import numpy as np
+import pytest
+
+from pyrender import PerspectiveCamera, OrthographicCamera
+
+
+def test_perspective_camera():
+
+ # Set up constants
+ znear = 0.05
+ zfar = 100
+ yfov = np.pi / 3.0
+ width = 1000.0
+ height = 500.0
+ aspectRatio = 640.0 / 480.0
+
+ # Test basics
+ with pytest.raises(TypeError):
+ p = PerspectiveCamera()
+
+ p = PerspectiveCamera(yfov=yfov)
+ assert p.yfov == yfov
+ assert p.znear == 0.05
+ assert p.zfar is None
+ assert p.aspectRatio is None
+ p.name = 'asdf'
+ p.name = None
+
+ with pytest.raises(ValueError):
+ p.yfov = 0.0
+
+ with pytest.raises(ValueError):
+ p.yfov = -1.0
+
+ with pytest.raises(ValueError):
+ p.znear = -1.0
+
+ p.znear = 0.0
+ p.znear = 0.05
+ p.zfar = 100.0
+ assert p.zfar == 100.0
+
+ with pytest.raises(ValueError):
+ p.zfar = 0.03
+
+ with pytest.raises(ValueError):
+ p.zfar = 0.05
+
+ p.aspectRatio = 10.0
+ assert p.aspectRatio == 10.0
+
+ with pytest.raises(ValueError):
+ p.aspectRatio = 0.0
+
+ with pytest.raises(ValueError):
+ p.aspectRatio = -1.0
+
+ # Test matrix getting/setting
+
+ # NF
+ p.znear = 0.05
+ p.zfar = 100
+ p.aspectRatio = None
+
+ with pytest.raises(ValueError):
+ p.get_projection_matrix()
+
+ assert np.allclose(
+ p.get_projection_matrix(width, height),
+ np.array([
+ [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0],
+ [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0],
+ [0.0, 0.0, (zfar + znear) / (znear - zfar),
+ (2 * zfar * znear) / (znear - zfar)],
+ [0.0, 0.0, -1.0, 0.0]
+ ])
+ )
+
+ # NFA
+ p.aspectRatio = aspectRatio
+ assert np.allclose(
+ p.get_projection_matrix(width, height),
+ np.array([
+ [1.0 / (aspectRatio * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0],
+ [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0],
+ [0.0, 0.0, (zfar + znear) / (znear - zfar),
+ (2 * zfar * znear) / (znear - zfar)],
+ [0.0, 0.0, -1.0, 0.0]
+ ])
+ )
+ assert np.allclose(
+ p.get_projection_matrix(), p.get_projection_matrix(width, height)
+ )
+
+ # N
+ p.zfar = None
+ p.aspectRatio = None
+ assert np.allclose(
+ p.get_projection_matrix(width, height),
+ np.array([
+ [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0],
+ [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0],
+ [0.0, 0.0, -1.0, -2.0 * znear],
+ [0.0, 0.0, -1.0, 0.0]
+ ])
+ )
+
+
+def test_orthographic_camera():
+ xm = 1.0
+ ym = 2.0
+ n = 0.05
+ f = 100.0
+
+ with pytest.raises(TypeError):
+ c = OrthographicCamera()
+
+ c = OrthographicCamera(xmag=xm, ymag=ym)
+
+ assert c.xmag == xm
+ assert c.ymag == ym
+ assert c.znear == 0.05
+ assert c.zfar == 100.0
+ assert c.name is None
+
+ with pytest.raises(TypeError):
+ c.ymag = None
+
+ with pytest.raises(ValueError):
+ c.ymag = 0.0
+
+ with pytest.raises(ValueError):
+ c.ymag = -1.0
+
+ with pytest.raises(TypeError):
+ c.xmag = None
+
+ with pytest.raises(ValueError):
+ c.xmag = 0.0
+
+ with pytest.raises(ValueError):
+ c.xmag = -1.0
+
+ with pytest.raises(TypeError):
+ c.znear = None
+
+ with pytest.raises(ValueError):
+ c.znear = 0.0
+
+ with pytest.raises(ValueError):
+ c.znear = -1.0
+
+ with pytest.raises(ValueError):
+ c.zfar = 0.01
+
+ assert np.allclose(
+ c.get_projection_matrix(),
+ np.array([
+ [1.0 / xm, 0, 0, 0],
+ [0, 1.0 / ym, 0, 0],
+ [0, 0, 2.0 / (n - f), (f + n) / (n - f)],
+ [0, 0, 0, 1.0]
+ ])
+ )
diff --git a/pyrender/tests/unit/test_egl.py b/pyrender/tests/unit/test_egl.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2f4bef39e33c2794e6837b5a1bb127d8d4dba06
--- /dev/null
+++ b/pyrender/tests/unit/test_egl.py
@@ -0,0 +1,16 @@
+# from pyrender.platforms import egl
+
+
+def tmp_test_default_device():
+ egl.get_default_device()
+
+
+def tmp_test_query_device():
+ devices = egl.query_devices()
+ assert len(devices) > 0
+
+
+def tmp_test_init_context():
+ device = egl.query_devices()[0]
+ platform = egl.EGLPlatform(128, 128, device=device)
+ platform.init_context()
diff --git a/pyrender/tests/unit/test_lights.py b/pyrender/tests/unit/test_lights.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffde856b21e8cce9532f0308fcd1c7eb2d1eba90
--- /dev/null
+++ b/pyrender/tests/unit/test_lights.py
@@ -0,0 +1,104 @@
+import numpy as np
+import pytest
+
+from pyrender import (DirectionalLight, SpotLight, PointLight, Texture,
+ PerspectiveCamera, OrthographicCamera)
+from pyrender.constants import SHADOW_TEX_SZ
+
+
+def test_directional_light():
+
+ d = DirectionalLight()
+ assert d.name is None
+ assert np.all(d.color == 1.0)
+ assert d.intensity == 1.0
+
+ d.name = 'direc'
+ with pytest.raises(ValueError):
+ d.color = None
+ with pytest.raises(TypeError):
+ d.intensity = None
+
+ d = DirectionalLight(color=[0.0, 0.0, 0.0])
+ assert np.all(d.color == 0.0)
+
+ d._generate_shadow_texture()
+ st = d.shadow_texture
+ assert isinstance(st, Texture)
+ assert st.width == st.height == SHADOW_TEX_SZ
+
+ sc = d._get_shadow_camera(scene_scale=5.0)
+ assert isinstance(sc, OrthographicCamera)
+ assert sc.xmag == sc.ymag == 5.0
+ assert sc.znear == 0.01 * 5.0
+ assert sc.zfar == 10 * 5.0
+
+
+def test_spot_light():
+
+ s = SpotLight()
+ assert s.name is None
+ assert np.all(s.color == 1.0)
+ assert s.intensity == 1.0
+ assert s.innerConeAngle == 0.0
+ assert s.outerConeAngle == np.pi / 4.0
+ assert s.range is None
+
+ with pytest.raises(ValueError):
+ s.range = -1.0
+
+ with pytest.raises(ValueError):
+ s.range = 0.0
+
+ with pytest.raises(ValueError):
+ s.innerConeAngle = -1.0
+
+ with pytest.raises(ValueError):
+ s.innerConeAngle = np.pi / 3.0
+
+ with pytest.raises(ValueError):
+ s.outerConeAngle = -1.0
+
+ with pytest.raises(ValueError):
+ s.outerConeAngle = np.pi
+
+ s.range = 5.0
+ s.outerConeAngle = np.pi / 2 - 0.05
+ s.innerConeAngle = np.pi / 3
+ s.innerConeAngle = 0.0
+ s.outerConeAngle = np.pi / 4.0
+
+ s._generate_shadow_texture()
+ st = s.shadow_texture
+ assert isinstance(st, Texture)
+ assert st.width == st.height == SHADOW_TEX_SZ
+
+ sc = s._get_shadow_camera(scene_scale=5.0)
+ assert isinstance(sc, PerspectiveCamera)
+ assert sc.znear == 0.01 * 5.0
+ assert sc.zfar == 10 * 5.0
+ assert sc.aspectRatio == 1.0
+ assert np.allclose(sc.yfov, np.pi / 16.0 * 9.0) # Plus pi / 16
+
+
+def test_point_light():
+
+ s = PointLight()
+ assert s.name is None
+ assert np.all(s.color == 1.0)
+ assert s.intensity == 1.0
+ assert s.range is None
+
+ with pytest.raises(ValueError):
+ s.range = -1.0
+
+ with pytest.raises(ValueError):
+ s.range = 0.0
+
+ s.range = 5.0
+
+ with pytest.raises(NotImplementedError):
+ s._generate_shadow_texture()
+
+ with pytest.raises(NotImplementedError):
+ s._get_shadow_camera(scene_scale=5.0)
diff --git a/pyrender/tests/unit/test_meshes.py b/pyrender/tests/unit/test_meshes.py
new file mode 100644
index 0000000000000000000000000000000000000000..7070b01171c97069fa013c6eba8eee217017f08e
--- /dev/null
+++ b/pyrender/tests/unit/test_meshes.py
@@ -0,0 +1,133 @@
+import numpy as np
+import pytest
+import trimesh
+
+from pyrender import (Mesh, Primitive)
+
+
+def test_meshes():
+
+ with pytest.raises(TypeError):
+ x = Mesh()
+ with pytest.raises(TypeError):
+ x = Primitive()
+ with pytest.raises(ValueError):
+ x = Primitive([], mode=10)
+
+ # Basics
+ x = Mesh([])
+ assert x.name is None
+ assert x.is_visible
+ assert x.weights is None
+
+ x.name = 'str'
+
+ # From Trimesh
+ x = Mesh.from_trimesh(trimesh.creation.box())
+ assert isinstance(x, Mesh)
+ assert len(x.primitives) == 1
+ assert x.is_visible
+ assert np.allclose(x.bounds, np.array([
+ [-0.5, -0.5, -0.5],
+ [0.5, 0.5, 0.5]
+ ]))
+ assert np.allclose(x.centroid, np.zeros(3))
+ assert np.allclose(x.extents, np.ones(3))
+ assert np.allclose(x.scale, np.sqrt(3))
+ assert not x.is_transparent
+
+ # Test some primitive functions
+ x = x.primitives[0]
+ with pytest.raises(ValueError):
+ x.normals = np.zeros(10)
+ with pytest.raises(ValueError):
+ x.tangents = np.zeros(10)
+ with pytest.raises(ValueError):
+ x.texcoord_0 = np.zeros(10)
+ with pytest.raises(ValueError):
+ x.texcoord_1 = np.zeros(10)
+ with pytest.raises(TypeError):
+ x.material = np.zeros(10)
+ assert x.targets is None
+ assert np.allclose(x.bounds, np.array([
+ [-0.5, -0.5, -0.5],
+ [0.5, 0.5, 0.5]
+ ]))
+ assert np.allclose(x.centroid, np.zeros(3))
+ assert np.allclose(x.extents, np.ones(3))
+ assert np.allclose(x.scale, np.sqrt(3))
+ x.material.baseColorFactor = np.array([0.0, 0.0, 0.0, 0.0])
+ assert x.is_transparent
+
+ # From two trimeshes
+ x = Mesh.from_trimesh([trimesh.creation.box(),
+ trimesh.creation.cylinder(radius=0.1, height=2.0)],
+ smooth=False)
+ assert isinstance(x, Mesh)
+ assert len(x.primitives) == 2
+ assert x.is_visible
+ assert np.allclose(x.bounds, np.array([
+ [-0.5, -0.5, -1.0],
+ [0.5, 0.5, 1.0]
+ ]))
+ assert np.allclose(x.centroid, np.zeros(3))
+ assert np.allclose(x.extents, [1.0, 1.0, 2.0])
+ assert np.allclose(x.scale, np.sqrt(6))
+ assert not x.is_transparent
+
+ # From bad data
+ with pytest.raises(TypeError):
+ x = Mesh.from_trimesh(None)
+
+ # With instancing
+ poses = np.tile(np.eye(4), (5,1,1))
+ poses[:,0,3] = np.array([0,1,2,3,4])
+ x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses)
+ assert np.allclose(x.bounds, np.array([
+ [-0.5, -0.5, -0.5],
+ [4.5, 0.5, 0.5]
+ ]))
+ poses = np.eye(4)
+ x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses)
+ poses = np.eye(3)
+ with pytest.raises(ValueError):
+ x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses)
+
+ # From textured meshes
+ fm = trimesh.load('tests/data/fuze.obj')
+ x = Mesh.from_trimesh(fm)
+ assert isinstance(x, Mesh)
+ assert len(x.primitives) == 1
+ assert x.is_visible
+ assert not x.is_transparent
+ assert x.primitives[0].material.baseColorTexture is not None
+
+ x = Mesh.from_trimesh(fm, smooth=False)
+ fm.visual = fm.visual.to_color()
+ fm.visual.face_colors = np.array([1.0, 0.0, 0.0, 1.0])
+ x = Mesh.from_trimesh(fm, smooth=False)
+ with pytest.raises(ValueError):
+ x = Mesh.from_trimesh(fm, smooth=True)
+
+ fm.visual.vertex_colors = np.array([1.0, 0.0, 0.0, 0.5])
+ x = Mesh.from_trimesh(fm, smooth=False)
+ x = Mesh.from_trimesh(fm, smooth=True)
+ assert x.primitives[0].color_0 is not None
+ assert x.is_transparent
+
+ bm = trimesh.load('tests/data/WaterBottle.glb').dump()[0]
+ x = Mesh.from_trimesh(bm)
+ assert x.primitives[0].material.baseColorTexture is not None
+ assert x.primitives[0].material.emissiveTexture is not None
+ assert x.primitives[0].material.metallicRoughnessTexture is not None
+
+ # From point cloud
+ x = Mesh.from_points(fm.vertices)
+
+# def test_duck():
+# bm = trimesh.load('tests/data/Duck.glb').dump()[0]
+# x = Mesh.from_trimesh(bm)
+# assert x.primitives[0].material.baseColorTexture is not None
+# pixel = x.primitives[0].material.baseColorTexture.source[100, 100]
+# yellowish = np.array([1.0, 0.7411765, 0.0, 1.0])
+# assert np.allclose(pixel, yellowish)
diff --git a/pyrender/tests/unit/test_nodes.py b/pyrender/tests/unit/test_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..9857c8221b7f6fb8530699bdf5593f8f0b74e152
--- /dev/null
+++ b/pyrender/tests/unit/test_nodes.py
@@ -0,0 +1,124 @@
+import numpy as np
+import pytest
+from trimesh import transformations
+
+from pyrender import (DirectionalLight, PerspectiveCamera, Mesh, Node)
+
+
+def test_nodes():
+
+ x = Node()
+ assert x.name is None
+ assert x.camera is None
+ assert x.children == []
+ assert x.skin is None
+ assert np.allclose(x.matrix, np.eye(4))
+ assert x.mesh is None
+ assert np.allclose(x.rotation, [0,0,0,1])
+ assert np.allclose(x.scale, np.ones(3))
+ assert np.allclose(x.translation, np.zeros(3))
+ assert x.weights is None
+ assert x.light is None
+
+ x.name = 'node'
+
+ # Test node light/camera/mesh tests
+ c = PerspectiveCamera(yfov=2.0)
+ m = Mesh([])
+ d = DirectionalLight()
+ x.camera = c
+ assert x.camera == c
+ with pytest.raises(TypeError):
+ x.camera = m
+ x.camera = d
+ x.camera = None
+ x.mesh = m
+ assert x.mesh == m
+ with pytest.raises(TypeError):
+ x.mesh = c
+ x.mesh = d
+ x.light = d
+ assert x.light == d
+ with pytest.raises(TypeError):
+ x.light = m
+ x.light = c
+
+ # Test transformations getters/setters/etc...
+ # Set up test values
+ x = np.array([1.0, 0.0, 0.0])
+ y = np.array([0.0, 1.0, 0.0])
+ t = np.array([1.0, 2.0, 3.0])
+ s = np.array([0.5, 2.0, 1.0])
+
+ Mx = transformations.rotation_matrix(np.pi / 2.0, x)
+ qx = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, x), -1)
+ Mxt = Mx.copy()
+ Mxt[:3,3] = t
+ S = np.eye(4)
+ S[:3,:3] = np.diag(s)
+ Mxts = Mxt.dot(S)
+
+ My = transformations.rotation_matrix(np.pi / 2.0, y)
+ qy = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, y), -1)
+ Myt = My.copy()
+ Myt[:3,3] = t
+
+ x = Node(matrix=Mx)
+ assert np.allclose(x.matrix, Mx)
+ assert np.allclose(x.rotation, qx)
+ assert np.allclose(x.translation, np.zeros(3))
+ assert np.allclose(x.scale, np.ones(3))
+
+ x.matrix = My
+ assert np.allclose(x.matrix, My)
+ assert np.allclose(x.rotation, qy)
+ assert np.allclose(x.translation, np.zeros(3))
+ assert np.allclose(x.scale, np.ones(3))
+ x.translation = t
+ assert np.allclose(x.matrix, Myt)
+ assert np.allclose(x.rotation, qy)
+ x.rotation = qx
+ assert np.allclose(x.matrix, Mxt)
+ x.scale = s
+ assert np.allclose(x.matrix, Mxts)
+
+ x = Node(matrix=Mxt)
+ assert np.allclose(x.matrix, Mxt)
+ assert np.allclose(x.rotation, qx)
+ assert np.allclose(x.translation, t)
+ assert np.allclose(x.scale, np.ones(3))
+
+ x = Node(matrix=Mxts)
+ assert np.allclose(x.matrix, Mxts)
+ assert np.allclose(x.rotation, qx)
+ assert np.allclose(x.translation, t)
+ assert np.allclose(x.scale, s)
+
+ # Individual element getters
+ x.scale[0] = 0
+ assert np.allclose(x.scale[0], 0)
+
+ x.translation[0] = 0
+ assert np.allclose(x.translation[0], 0)
+
+ x.matrix = np.eye(4)
+ x.matrix[0,0] = 500
+ assert x.matrix[0,0] == 1.0
+
+ # Failures
+ with pytest.raises(ValueError):
+ x.matrix = 5 * np.eye(4)
+ with pytest.raises(ValueError):
+ x.matrix = np.eye(5)
+ with pytest.raises(ValueError):
+ x.matrix = np.eye(4).dot([5,1,1,1])
+ with pytest.raises(ValueError):
+ x.rotation = np.array([1,2])
+ with pytest.raises(ValueError):
+ x.rotation = np.array([1,2,3])
+ with pytest.raises(ValueError):
+ x.rotation = np.array([1,2,3,4])
+ with pytest.raises(ValueError):
+ x.translation = np.array([1,2,3,4])
+ with pytest.raises(ValueError):
+ x.scale = np.array([1,2,3,4])
diff --git a/pyrender/tests/unit/test_offscreen.py b/pyrender/tests/unit/test_offscreen.py
new file mode 100644
index 0000000000000000000000000000000000000000..88983b0ff4e2ab6f5ef252c51f2ac669c3a0e0ca
--- /dev/null
+++ b/pyrender/tests/unit/test_offscreen.py
@@ -0,0 +1,92 @@
+import numpy as np
+import trimesh
+
+from pyrender import (OffscreenRenderer, PerspectiveCamera, DirectionalLight,
+ SpotLight, Mesh, Node, Scene)
+
+
+def test_offscreen_renderer(tmpdir):
+
+ # Fuze trimesh
+ fuze_trimesh = trimesh.load('examples/models/fuze.obj')
+ fuze_mesh = Mesh.from_trimesh(fuze_trimesh)
+
+ # Drill trimesh
+ drill_trimesh = trimesh.load('examples/models/drill.obj')
+ drill_mesh = Mesh.from_trimesh(drill_trimesh)
+ drill_pose = np.eye(4)
+ drill_pose[0,3] = 0.1
+ drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2])
+
+ # Wood trimesh
+ wood_trimesh = trimesh.load('examples/models/wood.obj')
+ wood_mesh = Mesh.from_trimesh(wood_trimesh)
+
+ # Water bottle trimesh
+ bottle_gltf = trimesh.load('examples/models/WaterBottle.glb')
+ bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]]
+ bottle_mesh = Mesh.from_trimesh(bottle_trimesh)
+ bottle_pose = np.array([
+ [1.0, 0.0, 0.0, 0.1],
+ [0.0, 0.0, -1.0, -0.16],
+ [0.0, 1.0, 0.0, 0.13],
+ [0.0, 0.0, 0.0, 1.0],
+ ])
+
+ boxv_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3))
+ boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape))
+ boxv_trimesh.visual.vertex_colors = boxv_vertex_colors
+ boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False)
+ boxf_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3))
+ boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape)
+ boxf_trimesh.visual.face_colors = boxf_face_colors
+ # Instanced
+ poses = np.tile(np.eye(4), (2,1,1))
+ poses[0,:3,3] = np.array([-0.1, -0.10, 0.05])
+ poses[1,:3,3] = np.array([-0.15, -0.10, 0.05])
+ boxf_mesh = Mesh.from_trimesh(boxf_trimesh, poses=poses, smooth=False)
+
+ points = trimesh.creation.icosphere(radius=0.05).vertices
+ point_colors = np.random.uniform(size=points.shape)
+ points_mesh = Mesh.from_points(points, colors=point_colors)
+
+ direc_l = DirectionalLight(color=np.ones(3), intensity=1.0)
+ spot_l = SpotLight(color=np.ones(3), intensity=10.0,
+ innerConeAngle=np.pi / 16, outerConeAngle=np.pi / 6)
+
+ cam = PerspectiveCamera(yfov=(np.pi / 3.0))
+ cam_pose = np.array([
+ [0.0, -np.sqrt(2) / 2, np.sqrt(2) / 2, 0.5],
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, np.sqrt(2) / 2, np.sqrt(2) / 2, 0.4],
+ [0.0, 0.0, 0.0, 1.0]
+ ])
+
+ scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02]))
+
+ fuze_node = Node(mesh=fuze_mesh, translation=np.array([
+ 0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2])
+ ]))
+ scene.add_node(fuze_node)
+ boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05]))
+ scene.add_node(boxv_node)
+ boxf_node = Node(mesh=boxf_mesh)
+ scene.add_node(boxf_node)
+
+ _ = scene.add(drill_mesh, pose=drill_pose)
+ _ = scene.add(bottle_mesh, pose=bottle_pose)
+ _ = scene.add(wood_mesh)
+ _ = scene.add(direc_l, pose=cam_pose)
+ _ = scene.add(spot_l, pose=cam_pose)
+ _ = scene.add(points_mesh)
+
+ _ = scene.add(cam, pose=cam_pose)
+
+ r = OffscreenRenderer(viewport_width=640, viewport_height=480)
+ color, depth = r.render(scene)
+
+ assert color.shape == (480, 640, 3)
+ assert depth.shape == (480, 640)
+ assert np.max(depth.data) > 0.05
+ assert np.count_nonzero(depth.data) > (0.2 * depth.size)
+ r.delete()
diff --git a/pyrender/tests/unit/test_scenes.py b/pyrender/tests/unit/test_scenes.py
new file mode 100644
index 0000000000000000000000000000000000000000..d85dd714cb5d842ea12dee4140adfd7db55c9c01
--- /dev/null
+++ b/pyrender/tests/unit/test_scenes.py
@@ -0,0 +1,235 @@
+import numpy as np
+import pytest
+import trimesh
+
+from pyrender import (Mesh, PerspectiveCamera, DirectionalLight,
+ SpotLight, PointLight, Scene, Node, OrthographicCamera)
+
+
+def test_scenes():
+
+ # Basics
+ s = Scene()
+ assert np.allclose(s.bg_color, np.ones(4))
+ assert np.allclose(s.ambient_light, np.zeros(3))
+ assert len(s.nodes) == 0
+ assert s.name is None
+ s.name = 'asdf'
+ s.bg_color = None
+ s.ambient_light = None
+ assert np.allclose(s.bg_color, np.ones(4))
+ assert np.allclose(s.ambient_light, np.zeros(3))
+
+ assert s.nodes == set()
+ assert s.cameras == set()
+ assert s.lights == set()
+ assert s.point_lights == set()
+ assert s.spot_lights == set()
+ assert s.directional_lights == set()
+ assert s.meshes == set()
+ assert s.camera_nodes == set()
+ assert s.light_nodes == set()
+ assert s.point_light_nodes == set()
+ assert s.spot_light_nodes == set()
+ assert s.directional_light_nodes == set()
+ assert s.mesh_nodes == set()
+ assert s.main_camera_node is None
+ assert np.all(s.bounds == 0)
+ assert np.all(s.centroid == 0)
+ assert np.all(s.extents == 0)
+ assert np.all(s.scale == 0)
+
+ # From trimesh scene
+ tms = trimesh.load('tests/data/WaterBottle.glb')
+ s = Scene.from_trimesh_scene(tms)
+ assert len(s.meshes) == 1
+ assert len(s.mesh_nodes) == 1
+
+ # Test bg color formatting
+ s = Scene(bg_color=[0, 1.0, 0])
+ assert np.allclose(s.bg_color, np.array([0.0, 1.0, 0.0, 1.0]))
+
+ # Test constructor for nodes
+ n1 = Node()
+ n2 = Node()
+ n3 = Node()
+ nodes = [n1, n2, n3]
+ s = Scene(nodes=nodes)
+ n1.children.append(n2)
+ s = Scene(nodes=nodes)
+ n3.children.append(n2)
+ with pytest.raises(ValueError):
+ s = Scene(nodes=nodes)
+ n3.children = []
+ n2.children.append(n3)
+ n3.children.append(n2)
+ with pytest.raises(ValueError):
+ s = Scene(nodes=nodes)
+
+ # Test node accessors
+ n1 = Node()
+ n2 = Node()
+ n3 = Node()
+ nodes = [n1, n2]
+ s = Scene(nodes=nodes)
+ assert s.has_node(n1)
+ assert s.has_node(n2)
+ assert not s.has_node(n3)
+
+ # Test node poses
+ for n in nodes:
+ assert np.allclose(s.get_pose(n), np.eye(4))
+ with pytest.raises(ValueError):
+ s.get_pose(n3)
+ with pytest.raises(ValueError):
+ s.set_pose(n3, np.eye(4))
+ tf = np.eye(4)
+ tf[:3,3] = np.ones(3)
+ s.set_pose(n1, tf)
+ assert np.allclose(s.get_pose(n1), tf)
+ assert np.allclose(s.get_pose(n2), np.eye(4))
+
+ nodes = [n1, n2, n3]
+ tf2 = np.eye(4)
+ tf2[:3,:3] = np.diag([-1,-1,1])
+ n1.children.append(n2)
+ n1.matrix = tf
+ n2.matrix = tf2
+ s = Scene(nodes=nodes)
+ assert np.allclose(s.get_pose(n1), tf)
+ assert np.allclose(s.get_pose(n2), tf.dot(tf2))
+ assert np.allclose(s.get_pose(n3), np.eye(4))
+
+ n1 = Node()
+ n2 = Node()
+ n3 = Node()
+ n1.children.append(n2)
+ s = Scene()
+ s.add_node(n1)
+ with pytest.raises(ValueError):
+ s.add_node(n2)
+ s.set_pose(n1, tf)
+ assert np.allclose(s.get_pose(n1), tf)
+ assert np.allclose(s.get_pose(n2), tf)
+ s.set_pose(n2, tf2)
+ assert np.allclose(s.get_pose(n2), tf.dot(tf2))
+
+ # Test node removal
+ n1 = Node()
+ n2 = Node()
+ n3 = Node()
+ n1.children.append(n2)
+ n2.children.append(n3)
+ s = Scene(nodes=[n1, n2, n3])
+ s.remove_node(n2)
+ assert len(s.nodes) == 1
+ assert n1 in s.nodes
+ assert len(n1.children) == 0
+ assert len(n2.children) == 1
+ s.add_node(n2, parent_node=n1)
+ assert len(n1.children) == 1
+ n1.matrix = tf
+ n3.matrix = tf2
+ assert np.allclose(s.get_pose(n3), tf.dot(tf2))
+
+ # Now test ADD function
+ s = Scene()
+ m = Mesh([], name='m')
+ cp = PerspectiveCamera(yfov=2.0)
+ co = OrthographicCamera(xmag=1.0, ymag=1.0)
+ dl = DirectionalLight()
+ pl = PointLight()
+ sl = SpotLight()
+
+ n1 = s.add(m, name='mn')
+ assert n1.mesh == m
+ assert len(s.nodes) == 1
+ assert len(s.mesh_nodes) == 1
+ assert n1 in s.mesh_nodes
+ assert len(s.meshes) == 1
+ assert m in s.meshes
+ assert len(s.get_nodes(node=n2)) == 0
+ n2 = s.add(m, pose=tf)
+ assert len(s.nodes) == len(s.mesh_nodes) == 2
+ assert len(s.meshes) == 1
+ assert len(s.get_nodes(node=n1)) == 1
+ assert len(s.get_nodes(node=n1, name='mn')) == 1
+ assert len(s.get_nodes(name='mn')) == 1
+ assert len(s.get_nodes(obj=m)) == 2
+ assert len(s.get_nodes(obj=m, obj_name='m')) == 2
+ assert len(s.get_nodes(obj=co)) == 0
+ nsl = s.add(sl, name='sln')
+ npl = s.add(pl, parent_name='sln')
+ assert nsl.children[0] == npl
+ ndl = s.add(dl, parent_node=npl)
+ assert npl.children[0] == ndl
+ nco = s.add(co)
+ ncp = s.add(cp)
+
+ assert len(s.light_nodes) == len(s.lights) == 3
+ assert len(s.point_light_nodes) == len(s.point_lights) == 1
+ assert npl in s.point_light_nodes
+ assert len(s.spot_light_nodes) == len(s.spot_lights) == 1
+ assert nsl in s.spot_light_nodes
+ assert len(s.directional_light_nodes) == len(s.directional_lights) == 1
+ assert ndl in s.directional_light_nodes
+ assert len(s.cameras) == len(s.camera_nodes) == 2
+ assert s.main_camera_node == nco
+ s.main_camera_node = ncp
+ s.remove_node(ncp)
+ assert len(s.cameras) == len(s.camera_nodes) == 1
+ assert s.main_camera_node == nco
+ s.remove_node(n2)
+ assert len(s.meshes) == 1
+ s.remove_node(n1)
+ assert len(s.meshes) == 0
+ s.remove_node(nsl)
+ assert len(s.lights) == 0
+ s.remove_node(nco)
+ assert s.main_camera_node is None
+
+ s.add_node(n1)
+ s.clear()
+ assert len(s.nodes) == 0
+
+ # Trigger final errors
+ with pytest.raises(ValueError):
+ s.main_camera_node = None
+ with pytest.raises(ValueError):
+ s.main_camera_node = ncp
+ with pytest.raises(ValueError):
+ s.add(m, parent_node=n1)
+ with pytest.raises(ValueError):
+ s.add(m, name='asdf')
+ s.add(m, name='asdf')
+ s.add(m, parent_name='asdf')
+ with pytest.raises(ValueError):
+ s.add(m, parent_name='asfd')
+ with pytest.raises(TypeError):
+ s.add(None)
+
+ s.clear()
+ # Test bounds
+ m1 = Mesh.from_trimesh(trimesh.creation.box())
+ m2 = Mesh.from_trimesh(trimesh.creation.box())
+ m3 = Mesh.from_trimesh(trimesh.creation.box())
+ n1 = Node(mesh=m1)
+ n2 = Node(mesh=m2, translation=[1.0, 0.0, 0.0])
+ n3 = Node(mesh=m3, translation=[0.5, 0.0, 1.0])
+ s.add_node(n1)
+ s.add_node(n2)
+ s.add_node(n3)
+ assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [1.5, 0.5, 1.5]])
+ s.clear()
+ s.add_node(n1)
+ s.add_node(n2, parent_node=n1)
+ s.add_node(n3, parent_node=n2)
+ assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.0, 0.5, 1.5]])
+ tf = np.eye(4)
+ tf[:3,3] = np.ones(3)
+ s.set_pose(n3, tf)
+ assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.5, 1.5, 1.5]])
+ s.remove_node(n2)
+ assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]])
+ s.clear()
+ assert np.allclose(s.bounds, 0.0)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9ba1fcbfec8acbde3f407ba282affacd91b72b95
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,27 @@
+git+https://github.com/openai/CLIP.git
+numpy==1.23.3
+matplotlib==3.4.3
+matplotlib-inline==0.1.2
+transformers
+h5py
+smplx
+shapely
+freetype-py
+imageio
+networkx
+numpy
+Pillow
+pyglet==1.4.0a1
+PyOpenGL
+PyOpenGL_accelerate
+six
+trimesh
+sphinx
+sphinx_rtd_theme
+sphinx-automodapi
+mapbox_earcut
+chumpy
+gdown
+MoviePy
+ffmpeg
+gradio==3.12
\ No newline at end of file