vumichien commited on
Commit
b4c8bc3
1 Parent(s): 2c37367

First commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. pyrender/.coveragerc +5 -0
  2. pyrender/.flake8 +8 -0
  3. pyrender/.gitignore +106 -0
  4. pyrender/.pre-commit-config.yaml +6 -0
  5. pyrender/.travis.yml +43 -0
  6. pyrender/LICENSE +21 -0
  7. pyrender/MANIFEST.in +5 -0
  8. pyrender/README.md +92 -0
  9. pyrender/docs/Makefile +23 -0
  10. pyrender/docs/make.bat +35 -0
  11. pyrender/docs/source/api/index.rst +59 -0
  12. pyrender/docs/source/conf.py +352 -0
  13. pyrender/docs/source/examples/cameras.rst +26 -0
  14. pyrender/docs/source/examples/index.rst +20 -0
  15. pyrender/docs/source/examples/lighting.rst +21 -0
  16. pyrender/docs/source/examples/models.rst +143 -0
  17. pyrender/docs/source/examples/offscreen.rst +87 -0
  18. pyrender/docs/source/examples/quickstart.rst +71 -0
  19. pyrender/docs/source/examples/scenes.rst +78 -0
  20. pyrender/docs/source/examples/viewer.rst +61 -0
  21. pyrender/docs/source/index.rst +41 -0
  22. pyrender/docs/source/install/index.rst +172 -0
  23. pyrender/examples/duck.py +13 -0
  24. pyrender/examples/example.py +157 -0
  25. pyrender/pyrender/__init__.py +24 -0
  26. pyrender/pyrender/camera.py +437 -0
  27. pyrender/pyrender/constants.py +149 -0
  28. pyrender/pyrender/font.py +272 -0
  29. pyrender/pyrender/fonts/OpenSans-Bold.ttf +0 -0
  30. pyrender/pyrender/fonts/OpenSans-BoldItalic.ttf +0 -0
  31. pyrender/pyrender/fonts/OpenSans-ExtraBold.ttf +0 -0
  32. pyrender/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf +0 -0
  33. pyrender/pyrender/fonts/OpenSans-Italic.ttf +0 -0
  34. pyrender/pyrender/fonts/OpenSans-Light.ttf +0 -0
  35. pyrender/pyrender/fonts/OpenSans-LightItalic.ttf +0 -0
  36. pyrender/pyrender/fonts/OpenSans-Regular.ttf +0 -0
  37. pyrender/pyrender/fonts/OpenSans-Semibold.ttf +0 -0
  38. pyrender/pyrender/fonts/OpenSans-SemiboldItalic.ttf +0 -0
  39. pyrender/pyrender/light.py +385 -0
  40. pyrender/pyrender/material.py +707 -0
  41. pyrender/pyrender/mesh.py +328 -0
  42. pyrender/pyrender/node.py +263 -0
  43. pyrender/pyrender/offscreen.py +160 -0
  44. pyrender/pyrender/platforms/__init__.py +6 -0
  45. pyrender/pyrender/platforms/base.py +76 -0
  46. pyrender/pyrender/platforms/egl.py +219 -0
  47. pyrender/pyrender/platforms/osmesa.py +59 -0
  48. pyrender/pyrender/platforms/pyglet_platform.py +90 -0
  49. pyrender/pyrender/primitive.py +489 -0
  50. pyrender/pyrender/renderer.py +1339 -0
pyrender/.coveragerc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ [report]
2
+ exclude_lines =
3
+ def __repr__
4
+ def __str__
5
+ @abc.abstractmethod
pyrender/.flake8 ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ [flake8]
2
+ ignore = E231,W504,F405,F403
3
+ max-line-length = 79
4
+ select = B,C,E,F,W,T4,B9
5
+ exclude =
6
+ docs/source/conf.py,
7
+ __pycache__,
8
+ examples/*
pyrender/.gitignore ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ docs/**/generated/**
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+
61
+ # Flask stuff:
62
+ instance/
63
+ .webassets-cache
64
+
65
+ # Scrapy stuff:
66
+ .scrapy
67
+
68
+ # Sphinx documentation
69
+ docs/_build/
70
+
71
+ # PyBuilder
72
+ target/
73
+
74
+ # Jupyter Notebook
75
+ .ipynb_checkpoints
76
+
77
+ # pyenv
78
+ .python-version
79
+
80
+ # celery beat schedule file
81
+ celerybeat-schedule
82
+
83
+ # SageMath parsed files
84
+ *.sage.py
85
+
86
+ # Environments
87
+ .env
88
+ .venv
89
+ env/
90
+ venv/
91
+ ENV/
92
+ env.bak/
93
+ venv.bak/
94
+
95
+ # Spyder project settings
96
+ .spyderproject
97
+ .spyproject
98
+
99
+ # Rope project settings
100
+ .ropeproject
101
+
102
+ # mkdocs documentation
103
+ /site
104
+
105
+ # mypy
106
+ .mypy_cache/
pyrender/.pre-commit-config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://gitlab.com/pycqa/flake8
3
+ rev: 3.7.1
4
+ hooks:
5
+ - id: flake8
6
+ exclude: ^setup.py
pyrender/.travis.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ language: python
2
+ sudo: required
3
+ dist: xenial
4
+
5
+ python:
6
+ - '3.6'
7
+ - '3.7'
8
+
9
+ before_install:
10
+ # Pre-install osmesa
11
+ - sudo apt update
12
+ - sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb
13
+ - sudo dpkg -i ./mesa_18.3.3-0.deb || true
14
+ - sudo apt install -f
15
+ - git clone https://github.com/mmatl/pyopengl.git
16
+ - cd pyopengl
17
+ - pip install .
18
+ - cd ..
19
+
20
+ install:
21
+ - pip install .
22
+ # - pip install -q pytest pytest-cov coveralls
23
+ - pip install pytest pytest-cov coveralls
24
+ - pip install ./pyopengl
25
+
26
+ script:
27
+ - PYOPENGL_PLATFORM=osmesa pytest --cov=pyrender tests
28
+
29
+ after_success:
30
+ - coveralls || true
31
+
32
+ deploy:
33
+ provider: pypi
34
+ skip_existing: true
35
+ user: mmatl
36
+ on:
37
+ tags: true
38
+ branch: master
39
+ password:
40
+ secure: O4WWMbTYb2eVYIO4mMOVa6/xyhX7mPvJpd96cxfNvJdyuqho8VapOhzqsI5kahMB1hFjWWr61yR4+Ru5hoDYf3XA6BQVk8eCY9+0H7qRfvoxex71lahKAqfHLMoE1xNdiVTgl+QN9hYjOnopLod24rx8I8eXfpHu/mfCpuTYGyLlNcDP5St3bXpXLPB5wg8Jo1YRRv6W/7fKoXyuWjewk9cJAS0KrEgnDnSkdwm6Pb+80B2tcbgdGvpGaByw5frndwKiMUMgVUownepDU5POQq2p29wwn9lCvRucULxjEgO+63jdbZRj5fNutLarFa2nISfYnrd72LOyDfbJubwAzzAIsy2JbFORyeHvCgloiuE9oE7a9oOQt/1QHBoIV0seiawMWn55Yp70wQ7HlJs4xSGJWCGa5+9883QRNsvj420atkb3cgO8P+PXwiwTi78Dq7Z/xHqccsU0b8poqBneQoA+pUGgNnF6V7Z8e9RsCcse2gAWSZWuOK3ua+9xCgH7I7MeL3afykr2aJ+yFCoYJMFrUjJeodMX2RbL0q+3FzIPZeGW3WdhTEAL9TSKRcJBSQTskaQlZx/OcpobxS7t3d2S68CCLG9uMTqOTYws55WZ1etalA75sRk9K2MR7ZGjZW3jdtvMViISc/t6Rrjea1GE8ZHGJC6/IeLIWA2c7nc=
41
+ distributions: sdist bdist_wheel
42
+ notifications:
43
+ email: false
pyrender/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Matthew Matl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyrender/MANIFEST.in ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ # Include the license
2
+ include LICENSE
3
+ include README.rst
4
+ include pyrender/fonts/*
5
+ include pyrender/shaders/*
pyrender/README.md ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pyrender
2
+
3
+ [![Build Status](https://travis-ci.org/mmatl/pyrender.svg?branch=master)](https://travis-ci.org/mmatl/pyrender)
4
+ [![Documentation Status](https://readthedocs.org/projects/pyrender/badge/?version=latest)](https://pyrender.readthedocs.io/en/latest/?badge=latest)
5
+ [![Coverage Status](https://coveralls.io/repos/github/mmatl/pyrender/badge.svg?branch=master)](https://coveralls.io/github/mmatl/pyrender?branch=master)
6
+ [![PyPI version](https://badge.fury.io/py/pyrender.svg)](https://badge.fury.io/py/pyrender)
7
+ [![Downloads](https://pepy.tech/badge/pyrender)](https://pepy.tech/project/pyrender)
8
+
9
+ Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based
10
+ rendering and visualization.
11
+ It is designed to meet the [glTF 2.0 specification from Khronos](https://www.khronos.org/gltf/).
12
+
13
+ Pyrender is lightweight, easy to install, and simple to use.
14
+ It comes packaged with both an intuitive scene viewer and a headache-free
15
+ offscreen renderer with support for GPU-accelerated rendering on headless
16
+ servers, which makes it perfect for machine learning applications.
17
+
18
+ Extensive documentation, including a quickstart guide, is provided [here](https://pyrender.readthedocs.io/en/latest/).
19
+
20
+ For a minimal working example of GPU-accelerated offscreen rendering using EGL,
21
+ check out the [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing).
22
+
23
+
24
+ <p align="center">
25
+ <img width="48%" src="https://github.com/mmatl/pyrender/blob/master/docs/source/_static/rotation.gif?raw=true" alt="GIF of Viewer"/>
26
+ <img width="48%" src="https://github.com/mmatl/pyrender/blob/master/docs/source/_static/damaged_helmet.png?raw=true" alt="Damaged Helmet"/>
27
+ </p>
28
+
29
+ ## Installation
30
+ You can install pyrender directly from pip.
31
+
32
+ ```bash
33
+ pip install pyrender
34
+ ```
35
+
36
+ ## Features
37
+
38
+ Despite being lightweight, pyrender has lots of features, including:
39
+
40
+ * Simple interoperation with the amazing [trimesh](https://github.com/mikedh/trimesh) project,
41
+ which enables out-of-the-box support for dozens of mesh types, including OBJ,
42
+ STL, DAE, OFF, PLY, and GLB.
43
+ * An easy-to-use scene viewer with support for animation, showing face and vertex
44
+ normals, toggling lighting conditions, and saving images and GIFs.
45
+ * An offscreen rendering module that supports OSMesa and EGL backends.
46
+ * Shadow mapping for directional and spot lights.
47
+ * Metallic-roughness materials for physically-based rendering, including several
48
+ types of texture and normal mapping.
49
+ * Transparency.
50
+ * Depth and color image generation.
51
+
52
+ ## Sample Usage
53
+
54
+ For sample usage, check out the [quickstart
55
+ guide](https://pyrender.readthedocs.io/en/latest/examples/index.html) or one of
56
+ the Google CoLab Notebooks:
57
+
58
+ * [EGL Google CoLab Notebook](https://colab.research.google.com/drive/1pcndwqeY8vker3bLKQNJKr3B-7-SYenE?usp=sharing)
59
+
60
+ ## Viewer Keyboard and Mouse Controls
61
+
62
+ When using the viewer, the basic controls for moving about the scene are as follows:
63
+
64
+ * To rotate the camera about the center of the scene, hold the left mouse button and drag the cursor.
65
+ * To rotate the camera about its viewing axis, hold `CTRL` left mouse button and drag the cursor.
66
+ * To pan the camera, do one of the following:
67
+ * Hold `SHIFT`, then hold the left mouse button and drag the cursor.
68
+ * Hold the middle mouse button and drag the cursor.
69
+ * To zoom the camera in or out, do one of the following:
70
+ * Scroll the mouse wheel.
71
+ * Hold the right mouse button and drag the cursor.
72
+
73
+ The available keyboard commands are as follows:
74
+
75
+ * `a`: Toggles rotational animation mode.
76
+ * `c`: Toggles backface culling.
77
+ * `f`: Toggles fullscreen mode.
78
+ * `h`: Toggles shadow rendering.
79
+ * `i`: Toggles axis display mode (no axes, world axis, mesh axes, all axes).
80
+ * `l`: Toggles lighting mode (scene lighting, Raymond lighting, or direct lighting).
81
+ * `m`: Toggles face normal visualization.
82
+ * `n`: Toggles vertex normal visualization.
83
+ * `o`: Toggles orthographic camera mode.
84
+ * `q`: Quits the viewer.
85
+ * `r`: Starts recording a GIF, and pressing again stops recording and opens a file dialog.
86
+ * `s`: Opens a file dialog to save the current view as an image.
87
+ * `w`: Toggles wireframe mode (scene default, flip wireframes, all wireframe, or all solid).
88
+ * `z`: Resets the camera to the default view.
89
+
90
+ As a note, displaying shadows significantly slows down rendering, so if you're
91
+ experiencing low framerates, just kill shadows or reduce the number of lights in
92
+ your scene.
pyrender/docs/Makefile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line.
5
+ SPHINXOPTS =
6
+ SPHINXBUILD = sphinx-build
7
+ SOURCEDIR = source
8
+ BUILDDIR = build
9
+
10
+ # Put it first so that "make" without argument is like "make help".
11
+ help:
12
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13
+
14
+ .PHONY: help Makefile
15
+
16
+ clean:
17
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
18
+ rm -rf ./source/generated/*
19
+
20
+ # Catch-all target: route all unknown targets to Sphinx using the new
21
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
22
+ %: Makefile
23
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
pyrender/docs/make.bat ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO OFF
2
+
3
+ pushd %~dp0
4
+
5
+ REM Command file for Sphinx documentation
6
+
7
+ if "%SPHINXBUILD%" == "" (
8
+ set SPHINXBUILD=sphinx-build
9
+ )
10
+ set SOURCEDIR=source
11
+ set BUILDDIR=build
12
+
13
+ if "%1" == "" goto help
14
+
15
+ %SPHINXBUILD% >NUL 2>NUL
16
+ if errorlevel 9009 (
17
+ echo.
18
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19
+ echo.installed, then set the SPHINXBUILD environment variable to point
20
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
21
+ echo.may add the Sphinx directory to PATH.
22
+ echo.
23
+ echo.If you don't have Sphinx installed, grab it from
24
+ echo.http://sphinx-doc.org/
25
+ exit /b 1
26
+ )
27
+
28
+ %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
29
+ goto end
30
+
31
+ :help
32
+ %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
33
+
34
+ :end
35
+ popd
pyrender/docs/source/api/index.rst ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Pyrender API Documentation
2
+ ==========================
3
+
4
+ Constants
5
+ ---------
6
+ .. automodapi:: pyrender.constants
7
+ :no-inheritance-diagram:
8
+ :no-main-docstr:
9
+ :no-heading:
10
+
11
+ Cameras
12
+ -------
13
+ .. automodapi:: pyrender.camera
14
+ :no-inheritance-diagram:
15
+ :no-main-docstr:
16
+ :no-heading:
17
+
18
+ Lighting
19
+ --------
20
+ .. automodapi:: pyrender.light
21
+ :no-inheritance-diagram:
22
+ :no-main-docstr:
23
+ :no-heading:
24
+
25
+ Objects
26
+ -------
27
+ .. automodapi:: pyrender
28
+ :no-inheritance-diagram:
29
+ :no-main-docstr:
30
+ :no-heading:
31
+ :skip: Camera, DirectionalLight, Light, OffscreenRenderer, Node
32
+ :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags
33
+ :skip: Renderer, Scene, SpotLight, TextAlign, Viewer, GLTF
34
+
35
+ Scenes
36
+ ------
37
+ .. automodapi:: pyrender
38
+ :no-inheritance-diagram:
39
+ :no-main-docstr:
40
+ :no-heading:
41
+ :skip: Camera, DirectionalLight, Light, OffscreenRenderer
42
+ :skip: OrthographicCamera, PerspectiveCamera, PointLight, RenderFlags
43
+ :skip: Renderer, SpotLight, TextAlign, Viewer, Sampler, Texture, Material
44
+ :skip: MetallicRoughnessMaterial, Primitive, Mesh, GLTF
45
+
46
+ On-Screen Viewer
47
+ ----------------
48
+ .. automodapi:: pyrender.viewer
49
+ :no-inheritance-diagram:
50
+ :no-inherited-members:
51
+ :no-main-docstr:
52
+ :no-heading:
53
+
54
+ Off-Screen Rendering
55
+ --------------------
56
+ .. automodapi:: pyrender.offscreen
57
+ :no-inheritance-diagram:
58
+ :no-main-docstr:
59
+ :no-heading:
pyrender/docs/source/conf.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # core documentation build configuration file, created by
4
+ # sphinx-quickstart on Sun Oct 16 14:33:48 2016.
5
+ #
6
+ # This file is execfile()d with the current directory set to its
7
+ # containing dir.
8
+ #
9
+ # Note that not all possible configuration values are present in this
10
+ # autogenerated file.
11
+ #
12
+ # All configuration values have a default; values that are commented out
13
+ # serve to show the default.
14
+
15
+ import sys
16
+ import os
17
+ from pyrender import __version__
18
+ from sphinx.domains.python import PythonDomain
19
+
20
+ # If extensions (or modules to document with autodoc) are in another directory,
21
+ # add these directories to sys.path here. If the directory is relative to the
22
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
23
+ sys.path.insert(0, os.path.abspath('../../'))
24
+
25
+ # -- General configuration ------------------------------------------------
26
+
27
+ # If your documentation needs a minimal Sphinx version, state it here.
28
+ #needs_sphinx = '1.0'
29
+
30
+ # Add any Sphinx extension module names here, as strings. They can be
31
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32
+ # ones.
33
+ extensions = [
34
+ 'sphinx.ext.autodoc',
35
+ 'sphinx.ext.autosummary',
36
+ 'sphinx.ext.coverage',
37
+ 'sphinx.ext.githubpages',
38
+ 'sphinx.ext.intersphinx',
39
+ 'sphinx.ext.napoleon',
40
+ 'sphinx.ext.viewcode',
41
+ 'sphinx_automodapi.automodapi',
42
+ 'sphinx_automodapi.smart_resolver'
43
+ ]
44
+ numpydoc_class_members_toctree = False
45
+ automodapi_toctreedirnm = 'generated'
46
+ automodsumm_inherited_members = True
47
+
48
+ # Add any paths that contain templates here, relative to this directory.
49
+ templates_path = ['_templates']
50
+
51
+ # The suffix(es) of source filenames.
52
+ # You can specify multiple suffix as a list of string:
53
+ # source_suffix = ['.rst', '.md']
54
+ source_suffix = '.rst'
55
+
56
+ # The encoding of source files.
57
+ #source_encoding = 'utf-8-sig'
58
+
59
+ # The master toctree document.
60
+ master_doc = 'index'
61
+
62
+ # General information about the project.
63
+ project = u'pyrender'
64
+ copyright = u'2018, Matthew Matl'
65
+ author = u'Matthew Matl'
66
+
67
+ # The version info for the project you're documenting, acts as replacement for
68
+ # |version| and |release|, also used in various other places throughout the
69
+ # built documents.
70
+ #
71
+ # The short X.Y version.
72
+ version = __version__
73
+ # The full version, including alpha/beta/rc tags.
74
+ release = __version__
75
+
76
+ # The language for content autogenerated by Sphinx. Refer to documentation
77
+ # for a list of supported languages.
78
+ #
79
+ # This is also used if you do content translation via gettext catalogs.
80
+ # Usually you set "language" from the command line for these cases.
81
+ language = None
82
+
83
+ # There are two options for replacing |today|: either, you set today to some
84
+ # non-false value, then it is used:
85
+ #today = ''
86
+ # Else, today_fmt is used as the format for a strftime call.
87
+ #today_fmt = '%B %d, %Y'
88
+
89
+ # List of patterns, relative to source directory, that match files and
90
+ # directories to ignore when looking for source files.
91
+ exclude_patterns = []
92
+
93
+ # The reST default role (used for this markup: `text`) to use for all
94
+ # documents.
95
+ #default_role = None
96
+
97
+ # If true, '()' will be appended to :func: etc. cross-reference text.
98
+ #add_function_parentheses = True
99
+
100
+ # If true, the current module name will be prepended to all description
101
+ # unit titles (such as .. function::).
102
+ #add_module_names = True
103
+
104
+ # If true, sectionauthor and moduleauthor directives will be shown in the
105
+ # output. They are ignored by default.
106
+ #show_authors = False
107
+
108
+ # The name of the Pygments (syntax highlighting) style to use.
109
+ pygments_style = 'sphinx'
110
+
111
+ # A list of ignored prefixes for module index sorting.
112
+ #modindex_common_prefix = []
113
+
114
+ # If true, keep warnings as "system message" paragraphs in the built documents.
115
+ #keep_warnings = False
116
+
117
+ # If true, `todo` and `todoList` produce output, else they produce nothing.
118
+ todo_include_todos = False
119
+
120
+
121
+ # -- Options for HTML output ----------------------------------------------
122
+
123
+ # The theme to use for HTML and HTML Help pages. See the documentation for
124
+ # a list of builtin themes.
125
+ import sphinx_rtd_theme
126
+ html_theme = 'sphinx_rtd_theme'
127
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
128
+
129
+ # Theme options are theme-specific and customize the look and feel of a theme
130
+ # further. For a list of options available for each theme, see the
131
+ # documentation.
132
+ #html_theme_options = {}
133
+
134
+ # Add any paths that contain custom themes here, relative to this directory.
135
+ #html_theme_path = []
136
+
137
+ # The name for this set of Sphinx documents. If None, it defaults to
138
+ # "<project> v<release> documentation".
139
+ #html_title = None
140
+
141
+ # A shorter title for the navigation bar. Default is the same as html_title.
142
+ #html_short_title = None
143
+
144
+ # The name of an image file (relative to this directory) to place at the top
145
+ # of the sidebar.
146
+ #html_logo = None
147
+
148
+ # The name of an image file (relative to this directory) to use as a favicon of
149
+ # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
150
+ # pixels large.
151
+ #html_favicon = None
152
+
153
+ # Add any paths that contain custom static files (such as style sheets) here,
154
+ # relative to this directory. They are copied after the builtin static files,
155
+ # so a file named "default.css" will overwrite the builtin "default.css".
156
+ html_static_path = ['_static']
157
+
158
+ # Add any extra paths that contain custom files (such as robots.txt or
159
+ # .htaccess) here, relative to this directory. These files are copied
160
+ # directly to the root of the documentation.
161
+ #html_extra_path = []
162
+
163
+ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
164
+ # using the given strftime format.
165
+ #html_last_updated_fmt = '%b %d, %Y'
166
+
167
+ # If true, SmartyPants will be used to convert quotes and dashes to
168
+ # typographically correct entities.
169
+ #html_use_smartypants = True
170
+
171
+ # Custom sidebar templates, maps document names to template names.
172
+ #html_sidebars = {}
173
+
174
+ # Additional templates that should be rendered to pages, maps page names to
175
+ # template names.
176
+ #html_additional_pages = {}
177
+
178
+ # If false, no module index is generated.
179
+ #html_domain_indices = True
180
+
181
+ # If false, no index is generated.
182
+ #html_use_index = True
183
+
184
+ # If true, the index is split into individual pages for each letter.
185
+ #html_split_index = False
186
+
187
+ # If true, links to the reST sources are added to the pages.
188
+ #html_show_sourcelink = True
189
+
190
+ # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
191
+ #html_show_sphinx = True
192
+
193
+ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
194
+ #html_show_copyright = True
195
+
196
+ # If true, an OpenSearch description file will be output, and all pages will
197
+ # contain a <link> tag referring to it. The value of this option must be the
198
+ # base URL from which the finished HTML is served.
199
+ #html_use_opensearch = ''
200
+
201
+ # This is the file name suffix for HTML files (e.g. ".xhtml").
202
+ #html_file_suffix = None
203
+
204
+ # Language to be used for generating the HTML full-text search index.
205
+ # Sphinx supports the following languages:
206
+ # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
207
+ # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
208
+ #html_search_language = 'en'
209
+
210
+ # A dictionary with options for the search language support, empty by default.
211
+ # Now only 'ja' uses this config value
212
+ #html_search_options = {'type': 'default'}
213
+
214
+ # The name of a javascript file (relative to the configuration directory) that
215
+ # implements a search results scorer. If empty, the default will be used.
216
+ #html_search_scorer = 'scorer.js'
217
+
218
+ # Output file base name for HTML help builder.
219
+ htmlhelp_basename = 'coredoc'
220
+
221
+ # -- Options for LaTeX output ---------------------------------------------
222
+
223
+ latex_elements = {
224
+ # The paper size ('letterpaper' or 'a4paper').
225
+ #'papersize': 'letterpaper',
226
+
227
+ # The font size ('10pt', '11pt' or '12pt').
228
+ #'pointsize': '10pt',
229
+
230
+ # Additional stuff for the LaTeX preamble.
231
+ #'preamble': '',
232
+
233
+ # Latex figure (float) alignment
234
+ #'figure_align': 'htbp',
235
+ }
236
+
237
+ # Grouping the document tree into LaTeX files. List of tuples
238
+ # (source start file, target name, title,
239
+ # author, documentclass [howto, manual, or own class]).
240
+ latex_documents = [
241
+ (master_doc, 'pyrender.tex', u'pyrender Documentation',
242
+ u'Matthew Matl', 'manual'),
243
+ ]
244
+
245
+ # The name of an image file (relative to this directory) to place at the top of
246
+ # the title page.
247
+ #latex_logo = None
248
+
249
+ # For "manual" documents, if this is true, then toplevel headings are parts,
250
+ # not chapters.
251
+ #latex_use_parts = False
252
+
253
+ # If true, show page references after internal links.
254
+ #latex_show_pagerefs = False
255
+
256
+ # If true, show URL addresses after external links.
257
+ #latex_show_urls = False
258
+
259
+ # Documents to append as an appendix to all manuals.
260
+ #latex_appendices = []
261
+
262
+ # If false, no module index is generated.
263
+ #latex_domain_indices = True
264
+
265
+
266
+ # -- Options for manual page output ---------------------------------------
267
+
268
+ # One entry per manual page. List of tuples
269
+ # (source start file, name, description, authors, manual section).
270
+ man_pages = [
271
+ (master_doc, 'pyrender', u'pyrender Documentation',
272
+ [author], 1)
273
+ ]
274
+
275
+ # If true, show URL addresses after external links.
276
+ #man_show_urls = False
277
+
278
+
279
+ # -- Options for Texinfo output -------------------------------------------
280
+
281
+ # Grouping the document tree into Texinfo files. List of tuples
282
+ # (source start file, target name, title, author,
283
+ # dir menu entry, description, category)
284
+ texinfo_documents = [
285
+ (master_doc, 'pyrender', u'pyrender Documentation',
286
+ author, 'pyrender', 'One line description of project.',
287
+ 'Miscellaneous'),
288
+ ]
289
+
290
+ # Documents to append as an appendix to all manuals.
291
+ #texinfo_appendices = []
292
+
293
+ # If false, no module index is generated.
294
+ #texinfo_domain_indices = True
295
+
296
+ # How to display URL addresses: 'footnote', 'no', or 'inline'.
297
+ #texinfo_show_urls = 'footnote'
298
+
299
+ # If true, do not generate a @detailmenu in the "Top" node's menu.
300
+ #texinfo_no_detailmenu = False
301
+
302
+ intersphinx_mapping = {
303
+ 'python' : ('https://docs.python.org/', None),
304
+ 'pyrender' : ('https://pyrender.readthedocs.io/en/latest/', None),
305
+ }
306
+
307
+ # Autosummary fix
308
+ autosummary_generate = True
309
+
310
+ # Try to suppress multiple-definition warnings by always taking the shorter
311
+ # path when two or more paths have the same base module
312
+
313
+ class MyPythonDomain(PythonDomain):
314
+
315
+ def find_obj(self, env, modname, classname, name, type, searchmode=0):
316
+ """Ensures an object always resolves to the desired module
317
+ if defined there."""
318
+ orig_matches = PythonDomain.find_obj(
319
+ self, env, modname, classname, name, type, searchmode
320
+ )
321
+
322
+ if len(orig_matches) <= 1:
323
+ return orig_matches
324
+
325
+ # If multiple matches, try to take the shortest if all the modules are
326
+ # the same
327
+ first_match_name_sp = orig_matches[0][0].split('.')
328
+ base_name = first_match_name_sp[0]
329
+ min_len = len(first_match_name_sp)
330
+ best_match = orig_matches[0]
331
+
332
+ for match in orig_matches[1:]:
333
+ match_name = match[0]
334
+ match_name_sp = match_name.split('.')
335
+ match_base = match_name_sp[0]
336
+
337
+ # If we have mismatched bases, return them all to trigger warnings
338
+ if match_base != base_name:
339
+ return orig_matches
340
+
341
+ # Otherwise, check and see if it's shorter
342
+ if len(match_name_sp) < min_len:
343
+ min_len = len(match_name_sp)
344
+ best_match = match
345
+
346
+ return (best_match,)
347
+
348
+
349
+ def setup(sphinx):
350
+ """Use MyPythonDomain in place of PythonDomain"""
351
+ sphinx.override_domain(MyPythonDomain)
352
+
pyrender/docs/source/examples/cameras.rst ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _camera_guide:
2
+
3
+ Creating Cameras
4
+ ================
5
+
6
+ Pyrender supports three camera types -- :class:`.PerspectiveCamera` and
7
+ :class:`.IntrinsicsCamera` types,
8
+ which render scenes as a human would see them, and
9
+ :class:`.OrthographicCamera` types, which preserve distances between points.
10
+
11
+ Creating cameras is easy -- just specify their basic attributes:
12
+
13
+ >>> pc = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414)
14
+ >>> oc = pyrender.OrthographicCamera(xmag=1.0, ymag=1.0)
15
+
16
+ For more information, see the Khronos group's documentation here_:
17
+
18
+ .. _here: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#projection-matrices
19
+
20
+ When you add cameras to the scene, make sure that you're using OpenGL camera
21
+ coordinates to specify their pose. See the illustration below for details.
22
+ Basically, the camera z-axis points away from the scene, the x-axis points
23
+ right in image space, and the y-axis points up in image space.
24
+
25
+ .. image:: /_static/camera_coords.png
26
+
pyrender/docs/source/examples/index.rst ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _guide:
2
+
3
+ User Guide
4
+ ==========
5
+
6
+ This section contains guides on how to use Pyrender to quickly visualize
7
+ your 3D data, including a quickstart guide and more detailed descriptions
8
+ of each part of the rendering pipeline.
9
+
10
+
11
+ .. toctree::
12
+ :maxdepth: 2
13
+
14
+ quickstart.rst
15
+ models.rst
16
+ lighting.rst
17
+ cameras.rst
18
+ scenes.rst
19
+ offscreen.rst
20
+ viewer.rst
pyrender/docs/source/examples/lighting.rst ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _lighting_guide:
2
+
3
+ Creating Lights
4
+ ===============
5
+
6
+ Pyrender supports three types of punctual light:
7
+
8
+ - :class:`.PointLight`: Point-based light sources, such as light bulbs.
9
+ - :class:`.SpotLight`: A conical light source, like a flashlight.
10
+ - :class:`.DirectionalLight`: A general light that does not attenuate with
11
+ distance.
12
+
13
+ Creating lights is easy -- just specify their basic attributes:
14
+
15
+ >>> pl = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0)
16
+ >>> sl = pyrender.SpotLight(color=[1.0, 1.0, 1.0], intensity=2.0,
17
+ ... innerConeAngle=0.05, outerConeAngle=0.5)
18
+ >>> dl = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.0)
19
+
20
+ For more information about how these lighting models are implemented,
21
+ see their class documentation.
pyrender/docs/source/examples/models.rst ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _model_guide:
2
+
3
+ Loading and Configuring Models
4
+ ==============================
5
+ The first step to any rendering application is loading your models.
6
+ Pyrender implements the GLTF 2.0 specification, which means that all
7
+ models are composed of a hierarchy of objects.
8
+
9
+ At the top level, we have a :class:`.Mesh`. The :class:`.Mesh` is
10
+ basically a wrapper of any number of :class:`.Primitive` types,
11
+ which actually represent geometry that can be drawn to the screen.
12
+
13
+ Primitives are composed of a variety of parameters, including
14
+ vertex positions, vertex normals, color and texture information,
15
+ and triangle indices if smooth rendering is desired.
16
+ They can implement point clouds, triangular meshes, or lines
17
+ depending on how you configure their data and set their
18
+ :attr:`.Primitive.mode` parameter.
19
+
20
+ Although you can create primitives yourself if you want to,
21
+ it's probably easier to just use the utility functions provided
22
+ in the :class:`.Mesh` class.
23
+
24
+ Creating Triangular Meshes
25
+ --------------------------
26
+
27
+ Simple Construction
28
+ ~~~~~~~~~~~~~~~~~~~
29
+ Pyrender allows you to create a :class:`.Mesh` containing a
30
+ triangular mesh model directly from a :class:`~trimesh.base.Trimesh` object
31
+ using the :meth:`.Mesh.from_trimesh` static method.
32
+
33
+ >>> import trimesh
34
+ >>> import pyrender
35
+ >>> import numpy as np
36
+ >>> tm = trimesh.load('examples/models/fuze.obj')
37
+ >>> m = pyrender.Mesh.from_trimesh(tm)
38
+ >>> m.primitives
39
+ [<pyrender.primitive.Primitive at 0x7fbb0af60e50>]
40
+
41
+ You can also create a single :class:`.Mesh` from a list of
42
+ :class:`~trimesh.base.Trimesh` objects:
43
+
44
+ >>> tms = [trimesh.creation.icosahedron(), trimesh.creation.cylinder()]
45
+ >>> m = pyrender.Mesh.from_trimesh(tms)
46
+ [<pyrender.primitive.Primitive at 0x7fbb0c2b74d0>,
47
+ <pyrender.primitive.Primitive at 0x7fbb0c2b7550>]
48
+
49
+ Vertex Smoothing
50
+ ~~~~~~~~~~~~~~~~
51
+
52
+ The :meth:`.Mesh.from_trimesh` method has a few additional optional parameters.
53
+ If you want to render the mesh without interpolating face normals, which can
54
+ be useful for meshes that are supposed to be angular (e.g. a cube), you
55
+ can specify ``smooth=False``.
56
+
57
+ >>> m = pyrender.Mesh.from_trimesh(tm, smooth=False)
58
+
59
+ Per-Face or Per-Vertex Coloration
60
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
61
+
62
+ If you have an untextured trimesh, you can color it in with per-face or
63
+ per-vertex colors:
64
+
65
+ >>> tm.visual.vertex_colors = np.random.uniform(size=tm.vertices.shape)
66
+ >>> tm.visual.face_colors = np.random.uniform(size=tm.faces.shape)
67
+ >>> m = pyrender.Mesh.from_trimesh(tm)
68
+
69
+ Instancing
70
+ ~~~~~~~~~~
71
+
72
+ If you want to render many copies of the same mesh at different poses,
73
+ you can statically create a vast array of them in an efficient manner.
74
+ Simply specify the ``poses`` parameter to be a list of ``N`` 4x4 homogenous
75
+ transformation matrics that position the meshes relative to their common
76
+ base frame:
77
+
78
+ >>> tfs = np.tile(np.eye(4), (3,1,1))
79
+ >>> tfs[1,:3,3] = [0.1, 0.0, 0.0]
80
+ >>> tfs[2,:3,3] = [0.2, 0.0, 0.0]
81
+ >>> tfs
82
+ array([[[1. , 0. , 0. , 0. ],
83
+ [0. , 1. , 0. , 0. ],
84
+ [0. , 0. , 1. , 0. ],
85
+ [0. , 0. , 0. , 1. ]],
86
+ [[1. , 0. , 0. , 0.1],
87
+ [0. , 1. , 0. , 0. ],
88
+ [0. , 0. , 1. , 0. ],
89
+ [0. , 0. , 0. , 1. ]],
90
+ [[1. , 0. , 0. , 0.2],
91
+ [0. , 1. , 0. , 0. ],
92
+ [0. , 0. , 1. , 0. ],
93
+ [0. , 0. , 0. , 1. ]]])
94
+
95
+ >>> m = pyrender.Mesh.from_trimesh(tm, poses=tfs)
96
+
97
+ Custom Materials
98
+ ~~~~~~~~~~~~~~~~
99
+
100
+ You can also specify a custom material for any triangular mesh you create
101
+ in the ``material`` parameter of :meth:`.Mesh.from_trimesh`.
102
+ The main material supported by Pyrender is the
103
+ :class:`.MetallicRoughnessMaterial`.
104
+ The metallic-roughness model supports rendering highly-realistic objects across
105
+ a wide gamut of materials.
106
+
107
+ For more information, see the documentation of the
108
+ :class:`.MetallicRoughnessMaterial` constructor or look at the Khronos_
109
+ documentation for more information.
110
+
111
+ .. _Khronos: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials
112
+
113
+ Creating Point Clouds
114
+ ---------------------
115
+
116
+ Point Sprites
117
+ ~~~~~~~~~~~~~
118
+ Pyrender also allows you to create a :class:`.Mesh` containing a
119
+ point cloud directly from :class:`numpy.ndarray` instances
120
+ using the :meth:`.Mesh.from_points` static method.
121
+
122
+ Simply provide a list of points and optional per-point colors and normals.
123
+
124
+ >>> pts = tm.vertices.copy()
125
+ >>> colors = np.random.uniform(size=pts.shape)
126
+ >>> m = pyrender.Mesh.from_points(pts, colors=colors)
127
+
128
+ Point clouds created in this way will be rendered as square point sprites.
129
+
130
+ .. image:: /_static/points.png
131
+
132
+ Point Spheres
133
+ ~~~~~~~~~~~~~
134
+ If you have a monochromatic point cloud and would like to render it with
135
+ spheres, you can render it by instancing a spherical trimesh:
136
+
137
+ >>> sm = trimesh.creation.uv_sphere(radius=0.1)
138
+ >>> sm.visual.vertex_colors = [1.0, 0.0, 0.0]
139
+ >>> tfs = np.tile(np.eye(4), (len(pts), 1, 1))
140
+ >>> tfs[:,:3,3] = pts
141
+ >>> m = pyrender.Mesh.from_trimesh(sm, poses=tfs)
142
+
143
+ .. image:: /_static/points2.png
pyrender/docs/source/examples/offscreen.rst ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _offscreen_guide:
2
+
3
+ Offscreen Rendering
4
+ ===================
5
+
6
+ .. note::
7
+ If you're using a headless server, you'll need to use either EGL (for
8
+ GPU-accelerated rendering) or OSMesa (for CPU-only software rendering).
9
+ If you're using OSMesa, be sure that you've installed it properly. See
10
+ :ref:`osmesa` for details.
11
+
12
+ Choosing a Backend
13
+ ------------------
14
+
15
+ Once you have a scene set up with its geometry, cameras, and lights,
16
+ you can render it using the :class:`.OffscreenRenderer`. Pyrender supports
17
+ three backends for offscreen rendering:
18
+
19
+ - Pyglet, the same engine that runs the viewer. This requires an active
20
+ display manager, so you can't run it on a headless server. This is the
21
+ default option.
22
+ - OSMesa, a software renderer.
23
+ - EGL, which allows for GPU-accelerated rendering without a display manager.
24
+
25
+ If you want to use OSMesa or EGL, you need to set the ``PYOPENGL_PLATFORM``
26
+ environment variable before importing pyrender or any other OpenGL library.
27
+ You can do this at the command line:
28
+
29
+ .. code-block:: bash
30
+
31
+ PYOPENGL_PLATFORM=osmesa python render.py
32
+
33
+ or at the top of your Python script:
34
+
35
+ .. code-block:: bash
36
+
37
+ # Top of main python script
38
+ import os
39
+ os.environ['PYOPENGL_PLATFORM'] = 'egl'
40
+
41
+ The handle for EGL is ``egl``, and the handle for OSMesa is ``osmesa``.
42
+
43
+ Running the Renderer
44
+ --------------------
45
+
46
+ Once you've set your environment variable appropriately, create your scene and
47
+ then configure the :class:`.OffscreenRenderer` object with a window width,
48
+ a window height, and a size for point-cloud points:
49
+
50
+ >>> r = pyrender.OffscreenRenderer(viewport_width=640,
51
+ ... viewport_height=480,
52
+ ... point_size=1.0)
53
+
54
+ Then, just call the :meth:`.OffscreenRenderer.render` function:
55
+
56
+ >>> color, depth = r.render(scene)
57
+
58
+ .. image:: /_static/scene.png
59
+
60
+ This will return a ``(w,h,3)`` channel floating-point color image and
61
+ a ``(w,h)`` floating-point depth image rendered from the scene's main camera.
62
+
63
+ You can customize the rendering process by using flag options from
64
+ :class:`.RenderFlags` and bitwise or-ing them together. For example,
65
+ the following code renders a color image with an alpha channel
66
+ and enables shadow mapping for all directional lights:
67
+
68
+ >>> flags = RenderFlags.RGBA | RenderFlags.SHADOWS_DIRECTIONAL
69
+ >>> color, depth = r.render(scene, flags=flags)
70
+
71
+ Once you're done with the offscreen renderer, you need to close it before you
72
+ can run a different renderer or open the viewer for the same scene:
73
+
74
+ >>> r.delete()
75
+
76
+ Google CoLab Examples
77
+ ---------------------
78
+
79
+ For a minimal working example of offscreen rendering using OSMesa,
80
+ see the `OSMesa Google CoLab notebook`_.
81
+
82
+ .. _OSMesa Google CoLab notebook: https://colab.research.google.com/drive/1Z71mHIc-Sqval92nK290vAsHZRUkCjUx
83
+
84
+ For a minimal working example of offscreen rendering using EGL,
85
+ see the `EGL Google CoLab notebook`_.
86
+
87
+ .. _EGL Google CoLab notebook: https://colab.research.google.com/drive/1rTLHk0qxh4dn8KNe-mCnN8HAWdd2_BEh
pyrender/docs/source/examples/quickstart.rst ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _quickstart_guide:
2
+
3
+ Quickstart
4
+ ==========
5
+
6
+
7
+ Minimal Example for 3D Viewer
8
+ -----------------------------
9
+ Here is a minimal example of loading and viewing a triangular mesh model
10
+ in pyrender.
11
+
12
+ >>> import trimesh
13
+ >>> import pyrender
14
+ >>> fuze_trimesh = trimesh.load('examples/models/fuze.obj')
15
+ >>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
16
+ >>> scene = pyrender.Scene()
17
+ >>> scene.add(mesh)
18
+ >>> pyrender.Viewer(scene, use_raymond_lighting=True)
19
+
20
+ .. image:: /_static/fuze.png
21
+
22
+
23
+ Minimal Example for Offscreen Rendering
24
+ ---------------------------------------
25
+ .. note::
26
+ If you're using a headless server, make sure that you followed the guide
27
+ for installing OSMesa. See :ref:`osmesa`.
28
+
29
+ Here is a minimal example of rendering a mesh model offscreen in pyrender.
30
+ The only additional necessities are that you need to add lighting and a camera.
31
+
32
+ >>> import numpy as np
33
+ >>> import trimesh
34
+ >>> import pyrender
35
+ >>> import matplotlib.pyplot as plt
36
+
37
+ >>> fuze_trimesh = trimesh.load('examples/models/fuze.obj')
38
+ >>> mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
39
+ >>> scene = pyrender.Scene()
40
+ >>> scene.add(mesh)
41
+ >>> camera = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.0)
42
+ >>> s = np.sqrt(2)/2
43
+ >>> camera_pose = np.array([
44
+ ... [0.0, -s, s, 0.3],
45
+ ... [1.0, 0.0, 0.0, 0.0],
46
+ ... [0.0, s, s, 0.35],
47
+ ... [0.0, 0.0, 0.0, 1.0],
48
+ ... ])
49
+ >>> scene.add(camera, pose=camera_pose)
50
+ >>> light = pyrender.SpotLight(color=np.ones(3), intensity=3.0,
51
+ ... innerConeAngle=np.pi/16.0,
52
+ ... outerConeAngle=np.pi/6.0)
53
+ >>> scene.add(light, pose=camera_pose)
54
+ >>> r = pyrender.OffscreenRenderer(400, 400)
55
+ >>> color, depth = r.render(scene)
56
+ >>> plt.figure()
57
+ >>> plt.subplot(1,2,1)
58
+ >>> plt.axis('off')
59
+ >>> plt.imshow(color)
60
+ >>> plt.subplot(1,2,2)
61
+ >>> plt.axis('off')
62
+ >>> plt.imshow(depth, cmap=plt.cm.gray_r)
63
+ >>> plt.show()
64
+
65
+ .. image:: /_static/minexcolor.png
66
+ :width: 45%
67
+ :align: left
68
+ .. image:: /_static/minexdepth.png
69
+ :width: 45%
70
+ :align: right
71
+
pyrender/docs/source/examples/scenes.rst ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _scene_guide:
2
+
3
+ Creating Scenes
4
+ ===============
5
+
6
+ Before you render anything, you need to put all of your lights, cameras,
7
+ and meshes into a scene. The :class:`.Scene` object keeps track of the relative
8
+ poses of these primitives by inserting them into :class:`.Node` objects and
9
+ keeping them in a directed acyclic graph.
10
+
11
+ Adding Objects
12
+ --------------
13
+
14
+ To create a :class:`.Scene`, simply call the constructor. You can optionally
15
+ specify an ambient light color and a background color:
16
+
17
+ >>> scene = pyrender.Scene(ambient_light=[0.02, 0.02, 0.02],
18
+ ... bg_color=[1.0, 1.0, 1.0])
19
+
20
+ You can add objects to a scene by first creating a :class:`.Node` object
21
+ and adding the object and its pose to the :class:`.Node`. Poses are specified
22
+ as 4x4 homogenous transformation matrices that are stored in the node's
23
+ :attr:`.Node.matrix` attribute. Note that the :class:`.Node`
24
+ constructor requires you to specify whether you're adding a mesh, light,
25
+ or camera.
26
+
27
+ >>> mesh = pyrender.Mesh.from_trimesh(tm)
28
+ >>> light = pyrender.PointLight(color=[1.0, 1.0, 1.0], intensity=2.0)
29
+ >>> cam = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.414)
30
+ >>> nm = pyrender.Node(mesh=mesh, matrix=np.eye(4))
31
+ >>> nl = pyrender.Node(light=light, matrix=np.eye(4))
32
+ >>> nc = pyrender.Node(camera=cam, matrix=np.eye(4))
33
+ >>> scene.add_node(nm)
34
+ >>> scene.add_node(nl)
35
+ >>> scene.add_node(nc)
36
+
37
+ You can also add objects directly to a scene with the :meth:`.Scene.add` function,
38
+ which takes care of creating a :class:`.Node` for you.
39
+
40
+ >>> scene.add(mesh, pose=np.eye(4))
41
+ >>> scene.add(light, pose=np.eye(4))
42
+ >>> scene.add(cam, pose=np.eye(4))
43
+
44
+ Nodes can be hierarchical, in which case the node's :attr:`.Node.matrix`
45
+ specifies that node's pose relative to its parent frame. You can add nodes to
46
+ a scene hierarchically by specifying a parent node in your calls to
47
+ :meth:`.Scene.add` or :meth:`.Scene.add_node`:
48
+
49
+ >>> scene.add_node(nl, parent_node=nc)
50
+ >>> scene.add(cam, parent_node=nm)
51
+
52
+ If you add multiple cameras to a scene, you can specify which one to render from
53
+ by setting the :attr:`.Scene.main_camera_node` attribute.
54
+
55
+ Updating Objects
56
+ ----------------
57
+
58
+ You can update the poses of existing nodes with the :meth:`.Scene.set_pose`
59
+ function. Simply call it with a :class:`.Node` that is already in the scene
60
+ and the new pose of that node with respect to its parent as a 4x4 homogenous
61
+ transformation matrix:
62
+
63
+ >>> scene.set_pose(nl, pose=np.eye(4))
64
+
65
+ If you want to get the local pose of a node, you can just access its
66
+ :attr:`.Node.matrix` attribute. However, if you want to the get
67
+ the pose of a node *with respect to the world frame*, you can call the
68
+ :meth:`.Scene.get_pose` method.
69
+
70
+ >>> tf = scene.get_pose(nl)
71
+
72
+ Removing Objects
73
+ ----------------
74
+
75
+ Finally, you can remove a :class:`.Node` and all of its children from the
76
+ scene with the :meth:`.Scene.remove_node` function:
77
+
78
+ >>> scene.remove_node(nl)
pyrender/docs/source/examples/viewer.rst ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _viewer_guide:
2
+
3
+ Live Scene Viewer
4
+ =================
5
+
6
+ Standard Usage
7
+ --------------
8
+ In addition to the offscreen renderer, Pyrender comes with a live scene viewer.
9
+ In its standard invocation, calling the :class:`.Viewer`'s constructor will
10
+ immediately pop a viewing window that you can navigate around in.
11
+
12
+ >>> pyrender.Viewer(scene)
13
+
14
+ By default, the viewer uses your scene's lighting. If you'd like to start with
15
+ some additional lighting that moves around with the camera, you can specify that
16
+ with:
17
+
18
+ >>> pyrender.Viewer(scene, use_raymond_lighting=True)
19
+
20
+ For a full list of the many options that the :class:`.Viewer` supports, check out its
21
+ documentation.
22
+
23
+ .. image:: /_static/rotation.gif
24
+
25
+ Running the Viewer in a Separate Thread
26
+ ---------------------------------------
27
+ If you'd like to animate your models, you'll want to run the viewer in a
28
+ separate thread so that you can update the scene while the viewer is running.
29
+ To do this, first pop the viewer in a separate thread by calling its constructor
30
+ with the ``run_in_thread`` option set:
31
+
32
+ >>> v = pyrender.Viewer(scene, run_in_thread=True)
33
+
34
+ Then, you can manipulate the :class:`.Scene` while the viewer is running to
35
+ animate things. However, be careful to acquire the viewer's
36
+ :attr:`.Viewer.render_lock` before editing the scene to prevent data corruption:
37
+
38
+ >>> i = 0
39
+ >>> while True:
40
+ ... pose = np.eye(4)
41
+ ... pose[:3,3] = [i, 0, 0]
42
+ ... v.render_lock.acquire()
43
+ ... scene.set_pose(mesh_node, pose)
44
+ ... v.render_lock.release()
45
+ ... i += 0.01
46
+
47
+ .. image:: /_static/scissors.gif
48
+
49
+ You can wait on the viewer to be closed manually:
50
+
51
+ >>> while v.is_active:
52
+ ... pass
53
+
54
+ Or you can close it from the main thread forcibly.
55
+ Make sure to still loop and block for the viewer to actually exit before using
56
+ the scene object again.
57
+
58
+ >>> v.close_external()
59
+ >>> while v.is_active:
60
+ ... pass
61
+
pyrender/docs/source/index.rst ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. core documentation master file, created by
2
+ sphinx-quickstart on Sun Oct 16 14:33:48 2016.
3
+ You can adapt this file completely to your liking, but it should at least
4
+ contain the root `toctree` directive.
5
+
6
+ Pyrender Documentation
7
+ ========================
8
+ Pyrender is a pure Python (2.7, 3.4, 3.5, 3.6) library for physically-based
9
+ rendering and visualization.
10
+ It is designed to meet the glTF 2.0 specification_ from Khronos
11
+
12
+ .. _specification: https://www.khronos.org/gltf/
13
+
14
+ Pyrender is lightweight, easy to install, and simple to use.
15
+ It comes packaged with both an intuitive scene viewer and a headache-free
16
+ offscreen renderer with support for GPU-accelerated rendering on headless
17
+ servers, which makes it perfect for machine learning applications.
18
+ Check out the :ref:`guide` for a full tutorial, or fork me on
19
+ Github_.
20
+
21
+ .. _Github: https://github.com/mmatl/pyrender
22
+
23
+ .. image:: _static/rotation.gif
24
+
25
+ .. image:: _static/damaged_helmet.png
26
+
27
+ .. toctree::
28
+ :maxdepth: 2
29
+
30
+ install/index.rst
31
+ examples/index.rst
32
+ api/index.rst
33
+
34
+
35
+ Indices and tables
36
+ ==================
37
+
38
+ * :ref:`genindex`
39
+ * :ref:`modindex`
40
+ * :ref:`search`
41
+
pyrender/docs/source/install/index.rst ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Installation Guide
2
+ ==================
3
+
4
+ Python Installation
5
+ -------------------
6
+
7
+ This package is available via ``pip``.
8
+
9
+ .. code-block:: bash
10
+
11
+ pip install pyrender
12
+
13
+ If you're on MacOS, you'll need
14
+ to pre-install my fork of ``pyglet``, as the version on PyPI hasn't yet included
15
+ my change that enables OpenGL contexts on MacOS.
16
+
17
+ .. code-block:: bash
18
+
19
+ git clone https://github.com/mmatl/pyglet.git
20
+ cd pyglet
21
+ pip install .
22
+
23
+ .. _osmesa:
24
+
25
+ Getting Pyrender Working with OSMesa
26
+ ------------------------------------
27
+ If you want to render scenes offscreen but don't want to have to
28
+ install a display manager or deal with the pains of trying to get
29
+ OpenGL to work over SSH, you have two options.
30
+
31
+ The first (and preferred) option is using EGL, which enables you to perform
32
+ GPU-accelerated rendering on headless servers.
33
+ However, you'll need EGL 1.5 to get modern OpenGL contexts.
34
+ This comes packaged with NVIDIA's current drivers, but if you are having issues
35
+ getting EGL to work with your hardware, you can try using OSMesa,
36
+ a software-based offscreen renderer that is included with any Mesa
37
+ install.
38
+
39
+ If you want to use OSMesa with pyrender, you'll have to perform two additional
40
+ installation steps:
41
+
42
+ - :ref:`installmesa`
43
+ - :ref:`installpyopengl`
44
+
45
+ Then, read the offscreen rendering tutorial. See :ref:`offscreen_guide`.
46
+
47
+ .. _installmesa:
48
+
49
+ Installing OSMesa
50
+ *****************
51
+
52
+ As a first step, you'll need to rebuild and re-install Mesa with support
53
+ for fast offscreen rendering and OpenGL 3+ contexts.
54
+ I'd recommend installing from source, but you can also try my ``.deb``
55
+ for Ubuntu 16.04 and up.
56
+
57
+ Installing from a Debian Package
58
+ ********************************
59
+
60
+ If you're running Ubuntu 16.04 or newer, you should be able to install the
61
+ required version of Mesa from my ``.deb`` file.
62
+
63
+ .. code-block:: bash
64
+
65
+ sudo apt update
66
+ sudo wget https://github.com/mmatl/travis_debs/raw/master/xenial/mesa_18.3.3-0.deb
67
+ sudo dpkg -i ./mesa_18.3.3-0.deb || true
68
+ sudo apt install -f
69
+
70
+ If this doesn't work, try building from source.
71
+
72
+ Building From Source
73
+ ********************
74
+
75
+ First, install build dependencies via `apt` or your system's package manager.
76
+
77
+ .. code-block:: bash
78
+
79
+ sudo apt-get install llvm-6.0 freeglut3 freeglut3-dev
80
+
81
+ Then, download the current release of Mesa from here_.
82
+ Unpack the source and go to the source folder:
83
+
84
+ .. _here: https://archive.mesa3d.org/mesa-18.3.3.tar.gz
85
+
86
+ .. code-block:: bash
87
+
88
+ tar xfv mesa-18.3.3.tar.gz
89
+ cd mesa-18.3.3
90
+
91
+ Replace ``PREFIX`` with the path you want to install Mesa at.
92
+ If you're not worried about overwriting your default Mesa install,
93
+ a good place is at ``/usr/local``.
94
+
95
+ Now, configure the installation by running the following command:
96
+
97
+ .. code-block:: bash
98
+
99
+ ./configure --prefix=PREFIX \
100
+ --enable-opengl --disable-gles1 --disable-gles2 \
101
+ --disable-va --disable-xvmc --disable-vdpau \
102
+ --enable-shared-glapi \
103
+ --disable-texture-float \
104
+ --enable-gallium-llvm --enable-llvm-shared-libs \
105
+ --with-gallium-drivers=swrast,swr \
106
+ --disable-dri --with-dri-drivers= \
107
+ --disable-egl --with-egl-platforms= --disable-gbm \
108
+ --disable-glx \
109
+ --disable-osmesa --enable-gallium-osmesa \
110
+ ac_cv_path_LLVM_CONFIG=llvm-config-6.0
111
+
112
+ Finally, build and install Mesa.
113
+
114
+ .. code-block:: bash
115
+
116
+ make -j8
117
+ make install
118
+
119
+ Finally, if you didn't install Mesa in the system path,
120
+ add the following lines to your ``~/.bashrc`` file after
121
+ changing ``MESA_HOME`` to your mesa installation path (i.e. what you used as
122
+ ``PREFIX`` during the configure command).
123
+
124
+ .. code-block:: bash
125
+
126
+ MESA_HOME=/path/to/your/mesa/installation
127
+ export LIBRARY_PATH=$LIBRARY_PATH:$MESA_HOME/lib
128
+ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MESA_HOME/lib
129
+ export C_INCLUDE_PATH=$C_INCLUDE_PATH:$MESA_HOME/include/
130
+ export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:$MESA_HOME/include/
131
+
132
+ .. _installpyopengl:
133
+
134
+ Installing a Compatible Fork of PyOpenGL
135
+ ****************************************
136
+
137
+ Next, install and use my fork of ``PyOpenGL``.
138
+ This fork enables getting modern OpenGL contexts with OSMesa.
139
+ My patch has been included in ``PyOpenGL``, but it has not yet been released
140
+ on PyPI.
141
+
142
+ .. code-block:: bash
143
+
144
+ git clone https://github.com/mmatl/pyopengl.git
145
+ pip install ./pyopengl
146
+
147
+
148
+ Building Documentation
149
+ ----------------------
150
+
151
+ The online documentation for ``pyrender`` is automatically built by Read The Docs.
152
+ Building ``pyrender``'s documentation locally requires a few extra dependencies --
153
+ specifically, `sphinx`_ and a few plugins.
154
+
155
+ .. _sphinx: http://www.sphinx-doc.org/en/master/
156
+
157
+ To install the dependencies required, simply change directories into the `pyrender` source and run
158
+
159
+ .. code-block:: bash
160
+
161
+ $ pip install .[docs]
162
+
163
+ Then, go to the ``docs`` directory and run ``make`` with the appropriate target.
164
+ For example,
165
+
166
+ .. code-block:: bash
167
+
168
+ $ cd docs/
169
+ $ make html
170
+
171
+ will generate a set of web pages. Any documentation files
172
+ generated in this manner can be found in ``docs/build``.
pyrender/examples/duck.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrender import Mesh, Scene, Viewer
2
+ from io import BytesIO
3
+ import numpy as np
4
+ import trimesh
5
+ import requests
6
+
7
+ duck_source = "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb"
8
+
9
+ duck = trimesh.load(BytesIO(requests.get(duck_source).content), file_type='glb')
10
+ duckmesh = Mesh.from_trimesh(list(duck.geometry.values())[0])
11
+ scene = Scene(ambient_light=np.array([1.0, 1.0, 1.0, 1.0]))
12
+ scene.add(duckmesh)
13
+ Viewer(scene)
pyrender/examples/example.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Examples of using pyrender for viewing and offscreen rendering.
2
+ """
3
+ import pyglet
4
+ pyglet.options['shadow_window'] = False
5
+ import os
6
+ import numpy as np
7
+ import trimesh
8
+
9
+ from pyrender import PerspectiveCamera,\
10
+ DirectionalLight, SpotLight, PointLight,\
11
+ MetallicRoughnessMaterial,\
12
+ Primitive, Mesh, Node, Scene,\
13
+ Viewer, OffscreenRenderer, RenderFlags
14
+
15
+ #==============================================================================
16
+ # Mesh creation
17
+ #==============================================================================
18
+
19
+ #------------------------------------------------------------------------------
20
+ # Creating textured meshes from trimeshes
21
+ #------------------------------------------------------------------------------
22
+
23
+ # Fuze trimesh
24
+ fuze_trimesh = trimesh.load('./models/fuze.obj')
25
+ fuze_mesh = Mesh.from_trimesh(fuze_trimesh)
26
+
27
+ # Drill trimesh
28
+ drill_trimesh = trimesh.load('./models/drill.obj')
29
+ drill_mesh = Mesh.from_trimesh(drill_trimesh)
30
+ drill_pose = np.eye(4)
31
+ drill_pose[0,3] = 0.1
32
+ drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2])
33
+
34
+ # Wood trimesh
35
+ wood_trimesh = trimesh.load('./models/wood.obj')
36
+ wood_mesh = Mesh.from_trimesh(wood_trimesh)
37
+
38
+ # Water bottle trimesh
39
+ bottle_gltf = trimesh.load('./models/WaterBottle.glb')
40
+ bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]]
41
+ bottle_mesh = Mesh.from_trimesh(bottle_trimesh)
42
+ bottle_pose = np.array([
43
+ [1.0, 0.0, 0.0, 0.1],
44
+ [0.0, 0.0, -1.0, -0.16],
45
+ [0.0, 1.0, 0.0, 0.13],
46
+ [0.0, 0.0, 0.0, 1.0],
47
+ ])
48
+
49
+ #------------------------------------------------------------------------------
50
+ # Creating meshes with per-vertex colors
51
+ #------------------------------------------------------------------------------
52
+ boxv_trimesh = trimesh.creation.box(extents=0.1*np.ones(3))
53
+ boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape))
54
+ boxv_trimesh.visual.vertex_colors = boxv_vertex_colors
55
+ boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False)
56
+
57
+ #------------------------------------------------------------------------------
58
+ # Creating meshes with per-face colors
59
+ #------------------------------------------------------------------------------
60
+ boxf_trimesh = trimesh.creation.box(extents=0.1*np.ones(3))
61
+ boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape)
62
+ boxf_trimesh.visual.face_colors = boxf_face_colors
63
+ boxf_mesh = Mesh.from_trimesh(boxf_trimesh, smooth=False)
64
+
65
+ #------------------------------------------------------------------------------
66
+ # Creating meshes from point clouds
67
+ #------------------------------------------------------------------------------
68
+ points = trimesh.creation.icosphere(radius=0.05).vertices
69
+ point_colors = np.random.uniform(size=points.shape)
70
+ points_mesh = Mesh.from_points(points, colors=point_colors)
71
+
72
+ #==============================================================================
73
+ # Light creation
74
+ #==============================================================================
75
+
76
+ direc_l = DirectionalLight(color=np.ones(3), intensity=1.0)
77
+ spot_l = SpotLight(color=np.ones(3), intensity=10.0,
78
+ innerConeAngle=np.pi/16, outerConeAngle=np.pi/6)
79
+ point_l = PointLight(color=np.ones(3), intensity=10.0)
80
+
81
+ #==============================================================================
82
+ # Camera creation
83
+ #==============================================================================
84
+
85
+ cam = PerspectiveCamera(yfov=(np.pi / 3.0))
86
+ cam_pose = np.array([
87
+ [0.0, -np.sqrt(2)/2, np.sqrt(2)/2, 0.5],
88
+ [1.0, 0.0, 0.0, 0.0],
89
+ [0.0, np.sqrt(2)/2, np.sqrt(2)/2, 0.4],
90
+ [0.0, 0.0, 0.0, 1.0]
91
+ ])
92
+
93
+ #==============================================================================
94
+ # Scene creation
95
+ #==============================================================================
96
+
97
+ scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02, 1.0]))
98
+
99
+ #==============================================================================
100
+ # Adding objects to the scene
101
+ #==============================================================================
102
+
103
+ #------------------------------------------------------------------------------
104
+ # By manually creating nodes
105
+ #------------------------------------------------------------------------------
106
+ fuze_node = Node(mesh=fuze_mesh, translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2])]))
107
+ scene.add_node(fuze_node)
108
+ boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05]))
109
+ scene.add_node(boxv_node)
110
+ boxf_node = Node(mesh=boxf_mesh, translation=np.array([-0.1, -0.10, 0.05]))
111
+ scene.add_node(boxf_node)
112
+
113
+ #------------------------------------------------------------------------------
114
+ # By using the add() utility function
115
+ #------------------------------------------------------------------------------
116
+ drill_node = scene.add(drill_mesh, pose=drill_pose)
117
+ bottle_node = scene.add(bottle_mesh, pose=bottle_pose)
118
+ wood_node = scene.add(wood_mesh)
119
+ direc_l_node = scene.add(direc_l, pose=cam_pose)
120
+ spot_l_node = scene.add(spot_l, pose=cam_pose)
121
+
122
+ #==============================================================================
123
+ # Using the viewer with a default camera
124
+ #==============================================================================
125
+
126
+ v = Viewer(scene, shadows=True)
127
+
128
+ #==============================================================================
129
+ # Using the viewer with a pre-specified camera
130
+ #==============================================================================
131
+ cam_node = scene.add(cam, pose=cam_pose)
132
+ v = Viewer(scene, central_node=drill_node)
133
+
134
+ #==============================================================================
135
+ # Rendering offscreen from that camera
136
+ #==============================================================================
137
+
138
+ r = OffscreenRenderer(viewport_width=640*2, viewport_height=480*2)
139
+ color, depth = r.render(scene)
140
+
141
+ import matplotlib.pyplot as plt
142
+ plt.figure()
143
+ plt.imshow(color)
144
+ plt.show()
145
+
146
+ #==============================================================================
147
+ # Segmask rendering
148
+ #==============================================================================
149
+
150
+ nm = {node: 20*(i + 1) for i, node in enumerate(scene.mesh_nodes)}
151
+ seg = r.render(scene, RenderFlags.SEG, nm)[0]
152
+ plt.figure()
153
+ plt.imshow(seg)
154
+ plt.show()
155
+
156
+ r.delete()
157
+
pyrender/pyrender/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .camera import (Camera, PerspectiveCamera, OrthographicCamera,
2
+ IntrinsicsCamera)
3
+ from .light import Light, PointLight, DirectionalLight, SpotLight
4
+ from .sampler import Sampler
5
+ from .texture import Texture
6
+ from .material import Material, MetallicRoughnessMaterial
7
+ from .primitive import Primitive
8
+ from .mesh import Mesh
9
+ from .node import Node
10
+ from .scene import Scene
11
+ from .renderer import Renderer
12
+ from .viewer import Viewer
13
+ from .offscreen import OffscreenRenderer
14
+ from .version import __version__
15
+ from .constants import RenderFlags, TextAlign, GLTF
16
+
17
+ __all__ = [
18
+ 'Camera', 'PerspectiveCamera', 'OrthographicCamera', 'IntrinsicsCamera',
19
+ 'Light', 'PointLight', 'DirectionalLight', 'SpotLight',
20
+ 'Sampler', 'Texture', 'Material', 'MetallicRoughnessMaterial',
21
+ 'Primitive', 'Mesh', 'Node', 'Scene', 'Renderer', 'Viewer',
22
+ 'OffscreenRenderer', '__version__', 'RenderFlags', 'TextAlign',
23
+ 'GLTF'
24
+ ]
pyrender/pyrender/camera.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Virtual cameras compliant with the glTF 2.0 specification as described at
2
+ https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-camera
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import abc
7
+ import numpy as np
8
+ import six
9
+ import sys
10
+
11
+ from .constants import DEFAULT_Z_NEAR, DEFAULT_Z_FAR
12
+
13
+
14
+ @six.add_metaclass(abc.ABCMeta)
15
+ class Camera(object):
16
+ """Abstract base class for all cameras.
17
+
18
+ Note
19
+ ----
20
+ Camera poses are specified in the OpenGL format,
21
+ where the z axis points away from the view direction and the
22
+ x and y axes point to the right and up in the image plane, respectively.
23
+
24
+ Parameters
25
+ ----------
26
+ znear : float
27
+ The floating-point distance to the near clipping plane.
28
+ zfar : float
29
+ The floating-point distance to the far clipping plane.
30
+ ``zfar`` must be greater than ``znear``.
31
+ name : str, optional
32
+ The user-defined name of this object.
33
+ """
34
+
35
+ def __init__(self,
36
+ znear=DEFAULT_Z_NEAR,
37
+ zfar=DEFAULT_Z_FAR,
38
+ name=None):
39
+ self.name = name
40
+ self.znear = znear
41
+ self.zfar = zfar
42
+
43
+ @property
44
+ def name(self):
45
+ """str : The user-defined name of this object.
46
+ """
47
+ return self._name
48
+
49
+ @name.setter
50
+ def name(self, value):
51
+ if value is not None:
52
+ value = str(value)
53
+ self._name = value
54
+
55
+ @property
56
+ def znear(self):
57
+ """float : The distance to the near clipping plane.
58
+ """
59
+ return self._znear
60
+
61
+ @znear.setter
62
+ def znear(self, value):
63
+ value = float(value)
64
+ if value < 0:
65
+ raise ValueError('z-near must be >= 0.0')
66
+ self._znear = value
67
+
68
+ @property
69
+ def zfar(self):
70
+ """float : The distance to the far clipping plane.
71
+ """
72
+ return self._zfar
73
+
74
+ @zfar.setter
75
+ def zfar(self, value):
76
+ value = float(value)
77
+ if value <= 0 or value <= self.znear:
78
+ raise ValueError('zfar must be >0 and >znear')
79
+ self._zfar = value
80
+
81
+ @abc.abstractmethod
82
+ def get_projection_matrix(self, width=None, height=None):
83
+ """Return the OpenGL projection matrix for this camera.
84
+
85
+ Parameters
86
+ ----------
87
+ width : int
88
+ Width of the current viewport, in pixels.
89
+ height : int
90
+ Height of the current viewport, in pixels.
91
+ """
92
+ pass
93
+
94
+
95
+ class PerspectiveCamera(Camera):
96
+
97
+ """A perspective camera for perspective projection.
98
+
99
+ Parameters
100
+ ----------
101
+ yfov : float
102
+ The floating-point vertical field of view in radians.
103
+ znear : float
104
+ The floating-point distance to the near clipping plane.
105
+ If not specified, defaults to 0.05.
106
+ zfar : float, optional
107
+ The floating-point distance to the far clipping plane.
108
+ ``zfar`` must be greater than ``znear``.
109
+ If None, the camera uses an infinite projection matrix.
110
+ aspectRatio : float, optional
111
+ The floating-point aspect ratio of the field of view.
112
+ If not specified, the camera uses the viewport's aspect ratio.
113
+ name : str, optional
114
+ The user-defined name of this object.
115
+ """
116
+
117
+ def __init__(self,
118
+ yfov,
119
+ znear=DEFAULT_Z_NEAR,
120
+ zfar=None,
121
+ aspectRatio=None,
122
+ name=None):
123
+ super(PerspectiveCamera, self).__init__(
124
+ znear=znear,
125
+ zfar=zfar,
126
+ name=name,
127
+ )
128
+
129
+ self.yfov = yfov
130
+ self.aspectRatio = aspectRatio
131
+
132
+ @property
133
+ def yfov(self):
134
+ """float : The vertical field of view in radians.
135
+ """
136
+ return self._yfov
137
+
138
+ @yfov.setter
139
+ def yfov(self, value):
140
+ value = float(value)
141
+ if value <= 0.0:
142
+ raise ValueError('Field of view must be positive')
143
+ self._yfov = value
144
+
145
+ @property
146
+ def zfar(self):
147
+ """float : The distance to the far clipping plane.
148
+ """
149
+ return self._zfar
150
+
151
+ @zfar.setter
152
+ def zfar(self, value):
153
+ if value is not None:
154
+ value = float(value)
155
+ if value <= 0 or value <= self.znear:
156
+ raise ValueError('zfar must be >0 and >znear')
157
+ self._zfar = value
158
+
159
+ @property
160
+ def aspectRatio(self):
161
+ """float : The ratio of the width to the height of the field of view.
162
+ """
163
+ return self._aspectRatio
164
+
165
+ @aspectRatio.setter
166
+ def aspectRatio(self, value):
167
+ if value is not None:
168
+ value = float(value)
169
+ if value <= 0.0:
170
+ raise ValueError('Aspect ratio must be positive')
171
+ self._aspectRatio = value
172
+
173
+ def get_projection_matrix(self, width=None, height=None):
174
+ """Return the OpenGL projection matrix for this camera.
175
+
176
+ Parameters
177
+ ----------
178
+ width : int
179
+ Width of the current viewport, in pixels.
180
+ height : int
181
+ Height of the current viewport, in pixels.
182
+ """
183
+ aspect_ratio = self.aspectRatio
184
+ if aspect_ratio is None:
185
+ if width is None or height is None:
186
+ raise ValueError('Aspect ratio of camera must be defined')
187
+ aspect_ratio = float(width) / float(height)
188
+
189
+ a = aspect_ratio
190
+ t = np.tan(self.yfov / 2.0)
191
+ n = self.znear
192
+ f = self.zfar
193
+
194
+ P = np.zeros((4,4))
195
+ P[0][0] = 1.0 / (a * t)
196
+ P[1][1] = 1.0 / t
197
+ P[3][2] = -1.0
198
+
199
+ if f is None:
200
+ P[2][2] = -1.0
201
+ P[2][3] = -2.0 * n
202
+ else:
203
+ P[2][2] = (f + n) / (n - f)
204
+ P[2][3] = (2 * f * n) / (n - f)
205
+
206
+ return P
207
+
208
+
209
+ class OrthographicCamera(Camera):
210
+ """An orthographic camera for orthographic projection.
211
+
212
+ Parameters
213
+ ----------
214
+ xmag : float
215
+ The floating-point horizontal magnification of the view.
216
+ ymag : float
217
+ The floating-point vertical magnification of the view.
218
+ znear : float
219
+ The floating-point distance to the near clipping plane.
220
+ If not specified, defaults to 0.05.
221
+ zfar : float
222
+ The floating-point distance to the far clipping plane.
223
+ ``zfar`` must be greater than ``znear``.
224
+ If not specified, defaults to 100.0.
225
+ name : str, optional
226
+ The user-defined name of this object.
227
+ """
228
+
229
+ def __init__(self,
230
+ xmag,
231
+ ymag,
232
+ znear=DEFAULT_Z_NEAR,
233
+ zfar=DEFAULT_Z_FAR,
234
+ name=None):
235
+ super(OrthographicCamera, self).__init__(
236
+ znear=znear,
237
+ zfar=zfar,
238
+ name=name,
239
+ )
240
+
241
+ self.xmag = xmag
242
+ self.ymag = ymag
243
+
244
+ @property
245
+ def xmag(self):
246
+ """float : The horizontal magnification of the view.
247
+ """
248
+ return self._xmag
249
+
250
+ @xmag.setter
251
+ def xmag(self, value):
252
+ value = float(value)
253
+ if value <= 0.0:
254
+ raise ValueError('X magnification must be positive')
255
+ self._xmag = value
256
+
257
+ @property
258
+ def ymag(self):
259
+ """float : The vertical magnification of the view.
260
+ """
261
+ return self._ymag
262
+
263
+ @ymag.setter
264
+ def ymag(self, value):
265
+ value = float(value)
266
+ if value <= 0.0:
267
+ raise ValueError('Y magnification must be positive')
268
+ self._ymag = value
269
+
270
+ @property
271
+ def znear(self):
272
+ """float : The distance to the near clipping plane.
273
+ """
274
+ return self._znear
275
+
276
+ @znear.setter
277
+ def znear(self, value):
278
+ value = float(value)
279
+ if value <= 0:
280
+ raise ValueError('z-near must be > 0.0')
281
+ self._znear = value
282
+
283
+ def get_projection_matrix(self, width=None, height=None):
284
+ """Return the OpenGL projection matrix for this camera.
285
+
286
+ Parameters
287
+ ----------
288
+ width : int
289
+ Width of the current viewport, in pixels.
290
+ Unused in this function.
291
+ height : int
292
+ Height of the current viewport, in pixels.
293
+ Unused in this function.
294
+ """
295
+ xmag = self.xmag
296
+ ymag = self.ymag
297
+
298
+ # If screen width/height defined, rescale xmag
299
+ if width is not None and height is not None:
300
+ xmag = width / height * ymag
301
+
302
+ n = self.znear
303
+ f = self.zfar
304
+ P = np.zeros((4,4))
305
+ P[0][0] = 1.0 / xmag
306
+ P[1][1] = 1.0 / ymag
307
+ P[2][2] = 2.0 / (n - f)
308
+ P[2][3] = (f + n) / (n - f)
309
+ P[3][3] = 1.0
310
+ return P
311
+
312
+
313
+ class IntrinsicsCamera(Camera):
314
+ """A perspective camera with custom intrinsics.
315
+
316
+ Parameters
317
+ ----------
318
+ fx : float
319
+ X-axis focal length in pixels.
320
+ fy : float
321
+ Y-axis focal length in pixels.
322
+ cx : float
323
+ X-axis optical center in pixels.
324
+ cy : float
325
+ Y-axis optical center in pixels.
326
+ znear : float
327
+ The floating-point distance to the near clipping plane.
328
+ If not specified, defaults to 0.05.
329
+ zfar : float
330
+ The floating-point distance to the far clipping plane.
331
+ ``zfar`` must be greater than ``znear``.
332
+ If not specified, defaults to 100.0.
333
+ name : str, optional
334
+ The user-defined name of this object.
335
+ """
336
+
337
+ def __init__(self,
338
+ fx,
339
+ fy,
340
+ cx,
341
+ cy,
342
+ znear=DEFAULT_Z_NEAR,
343
+ zfar=DEFAULT_Z_FAR,
344
+ name=None):
345
+ super(IntrinsicsCamera, self).__init__(
346
+ znear=znear,
347
+ zfar=zfar,
348
+ name=name,
349
+ )
350
+
351
+ self.fx = fx
352
+ self.fy = fy
353
+ self.cx = cx
354
+ self.cy = cy
355
+
356
+ @property
357
+ def fx(self):
358
+ """float : X-axis focal length in meters.
359
+ """
360
+ return self._fx
361
+
362
+ @fx.setter
363
+ def fx(self, value):
364
+ self._fx = float(value)
365
+
366
+ @property
367
+ def fy(self):
368
+ """float : Y-axis focal length in meters.
369
+ """
370
+ return self._fy
371
+
372
+ @fy.setter
373
+ def fy(self, value):
374
+ self._fy = float(value)
375
+
376
+ @property
377
+ def cx(self):
378
+ """float : X-axis optical center in pixels.
379
+ """
380
+ return self._cx
381
+
382
+ @cx.setter
383
+ def cx(self, value):
384
+ self._cx = float(value)
385
+
386
+ @property
387
+ def cy(self):
388
+ """float : Y-axis optical center in pixels.
389
+ """
390
+ return self._cy
391
+
392
+ @cy.setter
393
+ def cy(self, value):
394
+ self._cy = float(value)
395
+
396
+ def get_projection_matrix(self, width, height):
397
+ """Return the OpenGL projection matrix for this camera.
398
+
399
+ Parameters
400
+ ----------
401
+ width : int
402
+ Width of the current viewport, in pixels.
403
+ height : int
404
+ Height of the current viewport, in pixels.
405
+ """
406
+ width = float(width)
407
+ height = float(height)
408
+
409
+ cx, cy = self.cx, self.cy
410
+ fx, fy = self.fx, self.fy
411
+ if sys.platform == 'darwin':
412
+ cx = self.cx * 2.0
413
+ cy = self.cy * 2.0
414
+ fx = self.fx * 2.0
415
+ fy = self.fy * 2.0
416
+
417
+ P = np.zeros((4,4))
418
+ P[0][0] = 2.0 * fx / width
419
+ P[1][1] = 2.0 * fy / height
420
+ P[0][2] = 1.0 - 2.0 * cx / width
421
+ P[1][2] = 2.0 * cy / height - 1.0
422
+ P[3][2] = -1.0
423
+
424
+ n = self.znear
425
+ f = self.zfar
426
+ if f is None:
427
+ P[2][2] = -1.0
428
+ P[2][3] = -2.0 * n
429
+ else:
430
+ P[2][2] = (f + n) / (n - f)
431
+ P[2][3] = (2 * f * n) / (n - f)
432
+
433
+ return P
434
+
435
+
436
+ __all__ = ['Camera', 'PerspectiveCamera', 'OrthographicCamera',
437
+ 'IntrinsicsCamera']
pyrender/pyrender/constants.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DEFAULT_Z_NEAR = 0.05 # Near clipping plane, in meters
2
+ DEFAULT_Z_FAR = 100.0 # Far clipping plane, in meters
3
+ DEFAULT_SCENE_SCALE = 2.0 # Default scene scale
4
+ MAX_N_LIGHTS = 4 # Maximum number of lights of each type allowed
5
+ TARGET_OPEN_GL_MAJOR = 4 # Target OpenGL Major Version
6
+ TARGET_OPEN_GL_MINOR = 1 # Target OpenGL Minor Version
7
+ MIN_OPEN_GL_MAJOR = 3 # Minimum OpenGL Major Version
8
+ MIN_OPEN_GL_MINOR = 3 # Minimum OpenGL Minor Version
9
+ FLOAT_SZ = 4 # Byte size of GL float32
10
+ UINT_SZ = 4 # Byte size of GL uint32
11
+ SHADOW_TEX_SZ = 2048 # Width and Height of Shadow Textures
12
+ TEXT_PADDING = 20 # Width of padding for rendering text (px)
13
+
14
+
15
+ # Flags for render type
16
+ class RenderFlags(object):
17
+ """Flags for rendering in the scene.
18
+
19
+ Combine them with the bitwise or. For example,
20
+
21
+ >>> flags = OFFSCREEN | SHADOWS_DIRECTIONAL | VERTEX_NORMALS
22
+
23
+ would result in an offscreen render with directional shadows and
24
+ vertex normals enabled.
25
+ """
26
+ NONE = 0
27
+ """Normal PBR Render."""
28
+ DEPTH_ONLY = 1
29
+ """Only render the depth buffer."""
30
+ OFFSCREEN = 2
31
+ """Render offscreen and return the depth and (optionally) color buffers."""
32
+ FLIP_WIREFRAME = 4
33
+ """Invert the status of wireframe rendering for each mesh."""
34
+ ALL_WIREFRAME = 8
35
+ """Render all meshes as wireframes."""
36
+ ALL_SOLID = 16
37
+ """Render all meshes as solids."""
38
+ SHADOWS_DIRECTIONAL = 32
39
+ """Render shadows for directional lights."""
40
+ SHADOWS_POINT = 64
41
+ """Render shadows for point lights."""
42
+ SHADOWS_SPOT = 128
43
+ """Render shadows for spot lights."""
44
+ SHADOWS_ALL = 32 | 64 | 128
45
+ """Render shadows for all lights."""
46
+ VERTEX_NORMALS = 256
47
+ """Render vertex normals."""
48
+ FACE_NORMALS = 512
49
+ """Render face normals."""
50
+ SKIP_CULL_FACES = 1024
51
+ """Do not cull back faces."""
52
+ RGBA = 2048
53
+ """Render the color buffer with the alpha channel enabled."""
54
+ FLAT = 4096
55
+ """Render the color buffer flat, with no lighting computations."""
56
+ SEG = 8192
57
+
58
+
59
+ class TextAlign:
60
+ """Text alignment options for captions.
61
+
62
+ Only use one at a time.
63
+ """
64
+ CENTER = 0
65
+ """Center the text by width and height."""
66
+ CENTER_LEFT = 1
67
+ """Center the text by height and left-align it."""
68
+ CENTER_RIGHT = 2
69
+ """Center the text by height and right-align it."""
70
+ BOTTOM_LEFT = 3
71
+ """Put the text in the bottom-left corner."""
72
+ BOTTOM_RIGHT = 4
73
+ """Put the text in the bottom-right corner."""
74
+ BOTTOM_CENTER = 5
75
+ """Center the text by width and fix it to the bottom."""
76
+ TOP_LEFT = 6
77
+ """Put the text in the top-left corner."""
78
+ TOP_RIGHT = 7
79
+ """Put the text in the top-right corner."""
80
+ TOP_CENTER = 8
81
+ """Center the text by width and fix it to the top."""
82
+
83
+
84
+ class GLTF(object):
85
+ """Options for GL objects."""
86
+ NEAREST = 9728
87
+ """Nearest neighbor interpolation."""
88
+ LINEAR = 9729
89
+ """Linear interpolation."""
90
+ NEAREST_MIPMAP_NEAREST = 9984
91
+ """Nearest mipmapping."""
92
+ LINEAR_MIPMAP_NEAREST = 9985
93
+ """Linear mipmapping."""
94
+ NEAREST_MIPMAP_LINEAR = 9986
95
+ """Nearest mipmapping."""
96
+ LINEAR_MIPMAP_LINEAR = 9987
97
+ """Linear mipmapping."""
98
+ CLAMP_TO_EDGE = 33071
99
+ """Clamp to the edge of the texture."""
100
+ MIRRORED_REPEAT = 33648
101
+ """Mirror the texture."""
102
+ REPEAT = 10497
103
+ """Repeat the texture."""
104
+ POINTS = 0
105
+ """Render as points."""
106
+ LINES = 1
107
+ """Render as lines."""
108
+ LINE_LOOP = 2
109
+ """Render as a line loop."""
110
+ LINE_STRIP = 3
111
+ """Render as a line strip."""
112
+ TRIANGLES = 4
113
+ """Render as triangles."""
114
+ TRIANGLE_STRIP = 5
115
+ """Render as a triangle strip."""
116
+ TRIANGLE_FAN = 6
117
+ """Render as a triangle fan."""
118
+
119
+
120
+ class BufFlags(object):
121
+ POSITION = 0
122
+ NORMAL = 1
123
+ TANGENT = 2
124
+ TEXCOORD_0 = 4
125
+ TEXCOORD_1 = 8
126
+ COLOR_0 = 16
127
+ JOINTS_0 = 32
128
+ WEIGHTS_0 = 64
129
+
130
+
131
+ class TexFlags(object):
132
+ NONE = 0
133
+ NORMAL = 1
134
+ OCCLUSION = 2
135
+ EMISSIVE = 4
136
+ BASE_COLOR = 8
137
+ METALLIC_ROUGHNESS = 16
138
+ DIFFUSE = 32
139
+ SPECULAR_GLOSSINESS = 64
140
+
141
+
142
+ class ProgramFlags:
143
+ NONE = 0
144
+ USE_MATERIAL = 1
145
+ VERTEX_NORMALS = 2
146
+ FACE_NORMALS = 4
147
+
148
+
149
+ __all__ = ['RenderFlags', 'TextAlign', 'GLTF']
pyrender/pyrender/font.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Font texture loader and processor.
2
+
3
+ Author: Matthew Matl
4
+ """
5
+ import freetype
6
+ import numpy as np
7
+ import os
8
+
9
+ import OpenGL
10
+ from OpenGL.GL import *
11
+
12
+ from .constants import TextAlign, FLOAT_SZ
13
+ from .texture import Texture
14
+ from .sampler import Sampler
15
+
16
+
17
+ class FontCache(object):
18
+ """A cache for fonts.
19
+ """
20
+
21
+ def __init__(self, font_dir=None):
22
+ self._font_cache = {}
23
+ self.font_dir = font_dir
24
+ if self.font_dir is None:
25
+ base_dir, _ = os.path.split(os.path.realpath(__file__))
26
+ self.font_dir = os.path.join(base_dir, 'fonts')
27
+
28
+ def get_font(self, font_name, font_pt):
29
+ # If it's a file, load it directly, else, try to load from font dir.
30
+ if os.path.isfile(font_name):
31
+ font_filename = font_name
32
+ _, font_name = os.path.split(font_name)
33
+ font_name, _ = os.path.split(font_name)
34
+ else:
35
+ font_filename = os.path.join(self.font_dir, font_name) + '.ttf'
36
+
37
+ cid = OpenGL.contextdata.getContext()
38
+ key = (cid, font_name, int(font_pt))
39
+
40
+ if key not in self._font_cache:
41
+ self._font_cache[key] = Font(font_filename, font_pt)
42
+ return self._font_cache[key]
43
+
44
+ def clear(self):
45
+ for key in self._font_cache:
46
+ self._font_cache[key].delete()
47
+ self._font_cache = {}
48
+
49
+
50
+ class Character(object):
51
+ """A single character, with its texture and attributes.
52
+ """
53
+
54
+ def __init__(self, texture, size, bearing, advance):
55
+ self.texture = texture
56
+ self.size = size
57
+ self.bearing = bearing
58
+ self.advance = advance
59
+
60
+
61
+ class Font(object):
62
+ """A font object.
63
+
64
+ Parameters
65
+ ----------
66
+ font_file : str
67
+ The file to load the font from.
68
+ font_pt : int
69
+ The height of the font in pixels.
70
+ """
71
+
72
+ def __init__(self, font_file, font_pt=40):
73
+ self.font_file = font_file
74
+ self.font_pt = int(font_pt)
75
+ self._face = freetype.Face(font_file)
76
+ self._face.set_pixel_sizes(0, font_pt)
77
+ self._character_map = {}
78
+
79
+ for i in range(0, 128):
80
+
81
+ # Generate texture
82
+ face = self._face
83
+ face.load_char(chr(i))
84
+ buf = face.glyph.bitmap.buffer
85
+ src = (np.array(buf) / 255.0).astype(np.float32)
86
+ src = src.reshape((face.glyph.bitmap.rows,
87
+ face.glyph.bitmap.width))
88
+ tex = Texture(
89
+ sampler=Sampler(
90
+ magFilter=GL_LINEAR,
91
+ minFilter=GL_LINEAR,
92
+ wrapS=GL_CLAMP_TO_EDGE,
93
+ wrapT=GL_CLAMP_TO_EDGE
94
+ ),
95
+ source=src,
96
+ source_channels='R',
97
+ )
98
+ character = Character(
99
+ texture=tex,
100
+ size=np.array([face.glyph.bitmap.width,
101
+ face.glyph.bitmap.rows]),
102
+ bearing=np.array([face.glyph.bitmap_left,
103
+ face.glyph.bitmap_top]),
104
+ advance=face.glyph.advance.x
105
+ )
106
+ self._character_map[chr(i)] = character
107
+
108
+ self._vbo = None
109
+ self._vao = None
110
+
111
+ @property
112
+ def font_file(self):
113
+ """str : The file the font was loaded from.
114
+ """
115
+ return self._font_file
116
+
117
+ @font_file.setter
118
+ def font_file(self, value):
119
+ self._font_file = value
120
+
121
+ @property
122
+ def font_pt(self):
123
+ """int : The height of the font in pixels.
124
+ """
125
+ return self._font_pt
126
+
127
+ @font_pt.setter
128
+ def font_pt(self, value):
129
+ self._font_pt = int(value)
130
+
131
+ def _add_to_context(self):
132
+
133
+ self._vao = glGenVertexArrays(1)
134
+ glBindVertexArray(self._vao)
135
+ self._vbo = glGenBuffers(1)
136
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
137
+ glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW)
138
+ glEnableVertexAttribArray(0)
139
+ glVertexAttribPointer(
140
+ 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0)
141
+ )
142
+ glBindVertexArray(0)
143
+
144
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
145
+ for c in self._character_map:
146
+ ch = self._character_map[c]
147
+ if not ch.texture._in_context():
148
+ ch.texture._add_to_context()
149
+
150
+ def _remove_from_context(self):
151
+ for c in self._character_map:
152
+ ch = self._character_map[c]
153
+ ch.texture.delete()
154
+ if self._vao is not None:
155
+ glDeleteVertexArrays(1, [self._vao])
156
+ glDeleteBuffers(1, [self._vbo])
157
+ self._vao = None
158
+ self._vbo = None
159
+
160
+ def _in_context(self):
161
+ return self._vao is not None
162
+
163
+ def _bind(self):
164
+ glBindVertexArray(self._vao)
165
+
166
+ def _unbind(self):
167
+ glBindVertexArray(0)
168
+
169
+ def delete(self):
170
+ self._unbind()
171
+ self._remove_from_context()
172
+
173
+ def render_string(self, text, x, y, scale=1.0,
174
+ align=TextAlign.BOTTOM_LEFT):
175
+ """Render a string to the current view buffer.
176
+
177
+ Note
178
+ ----
179
+ Assumes correct shader program already bound w/ uniforms set.
180
+
181
+ Parameters
182
+ ----------
183
+ text : str
184
+ The text to render.
185
+ x : int
186
+ Horizontal pixel location of text.
187
+ y : int
188
+ Vertical pixel location of text.
189
+ scale : int
190
+ Scaling factor for text.
191
+ align : int
192
+ One of the TextAlign options which specifies where the ``x``
193
+ and ``y`` parameters lie on the text. For example,
194
+ :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate
195
+ the position of the bottom-left corner of the textbox.
196
+ """
197
+ glActiveTexture(GL_TEXTURE0)
198
+ glEnable(GL_BLEND)
199
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
200
+ glDisable(GL_DEPTH_TEST)
201
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
202
+ self._bind()
203
+
204
+ # Determine width and height of text relative to x, y
205
+ width = 0.0
206
+ height = 0.0
207
+ for c in text:
208
+ ch = self._character_map[c]
209
+ height = max(height, ch.bearing[1] * scale)
210
+ width += (ch.advance >> 6) * scale
211
+
212
+ # Determine offsets based on alignments
213
+ xoff = 0
214
+ yoff = 0
215
+ if align == TextAlign.BOTTOM_RIGHT:
216
+ xoff = -width
217
+ elif align == TextAlign.BOTTOM_CENTER:
218
+ xoff = -width / 2.0
219
+ elif align == TextAlign.TOP_LEFT:
220
+ yoff = -height
221
+ elif align == TextAlign.TOP_RIGHT:
222
+ yoff = -height
223
+ xoff = -width
224
+ elif align == TextAlign.TOP_CENTER:
225
+ yoff = -height
226
+ xoff = -width / 2.0
227
+ elif align == TextAlign.CENTER:
228
+ xoff = -width / 2.0
229
+ yoff = -height / 2.0
230
+ elif align == TextAlign.CENTER_LEFT:
231
+ yoff = -height / 2.0
232
+ elif align == TextAlign.CENTER_RIGHT:
233
+ xoff = -width
234
+ yoff = -height / 2.0
235
+
236
+ x += xoff
237
+ y += yoff
238
+
239
+ ch = None
240
+ for c in text:
241
+ ch = self._character_map[c]
242
+ xpos = x + ch.bearing[0] * scale
243
+ ypos = y - (ch.size[1] - ch.bearing[1]) * scale
244
+ w = ch.size[0] * scale
245
+ h = ch.size[1] * scale
246
+
247
+ vertices = np.array([
248
+ [xpos, ypos, 0.0, 0.0],
249
+ [xpos + w, ypos, 1.0, 0.0],
250
+ [xpos + w, ypos + h, 1.0, 1.0],
251
+ [xpos + w, ypos + h, 1.0, 1.0],
252
+ [xpos, ypos + h, 0.0, 1.0],
253
+ [xpos, ypos, 0.0, 0.0],
254
+ ], dtype=np.float32)
255
+
256
+ ch.texture._bind()
257
+
258
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
259
+ glBufferData(
260
+ GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW
261
+ )
262
+ # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken
263
+ # glBufferSubData(
264
+ # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ,
265
+ # np.ascontiguousarray(vertices.flatten)
266
+ # )
267
+ glDrawArrays(GL_TRIANGLES, 0, 6)
268
+ x += (ch.advance >> 6) * scale
269
+
270
+ self._unbind()
271
+ if ch:
272
+ ch.texture._unbind()
pyrender/pyrender/fonts/OpenSans-Bold.ttf ADDED
Binary file (225 kB). View file
pyrender/pyrender/fonts/OpenSans-BoldItalic.ttf ADDED
Binary file (213 kB). View file
pyrender/pyrender/fonts/OpenSans-ExtraBold.ttf ADDED
Binary file (223 kB). View file
pyrender/pyrender/fonts/OpenSans-ExtraBoldItalic.ttf ADDED
Binary file (213 kB). View file
pyrender/pyrender/fonts/OpenSans-Italic.ttf ADDED
Binary file (213 kB). View file
pyrender/pyrender/fonts/OpenSans-Light.ttf ADDED
Binary file (222 kB). View file
pyrender/pyrender/fonts/OpenSans-LightItalic.ttf ADDED
Binary file (213 kB). View file
pyrender/pyrender/fonts/OpenSans-Regular.ttf ADDED
Binary file (217 kB). View file
pyrender/pyrender/fonts/OpenSans-Semibold.ttf ADDED
Binary file (221 kB). View file
pyrender/pyrender/fonts/OpenSans-SemiboldItalic.ttf ADDED
Binary file (213 kB). View file
pyrender/pyrender/light.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Punctual light sources as defined by the glTF 2.0 KHR extension at
2
+ https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import abc
7
+ import numpy as np
8
+ import six
9
+
10
+ from OpenGL.GL import *
11
+
12
+ from .utils import format_color_vector
13
+ from .texture import Texture
14
+ from .constants import SHADOW_TEX_SZ
15
+ from .camera import OrthographicCamera, PerspectiveCamera
16
+
17
+
18
+
19
+ @six.add_metaclass(abc.ABCMeta)
20
+ class Light(object):
21
+ """Base class for all light objects.
22
+
23
+ Parameters
24
+ ----------
25
+ color : (3,) float
26
+ RGB value for the light's color in linear space.
27
+ intensity : float
28
+ Brightness of light. The units that this is defined in depend on the
29
+ type of light. Point and spot lights use luminous intensity in candela
30
+ (lm/sr), while directional lights use illuminance in lux (lm/m2).
31
+ name : str, optional
32
+ Name of the light.
33
+ """
34
+ def __init__(self,
35
+ color=None,
36
+ intensity=None,
37
+ name=None):
38
+
39
+ if color is None:
40
+ color = np.ones(3)
41
+ if intensity is None:
42
+ intensity = 1.0
43
+
44
+ self.name = name
45
+ self.color = color
46
+ self.intensity = intensity
47
+ self._shadow_camera = None
48
+ self._shadow_texture = None
49
+
50
+ @property
51
+ def name(self):
52
+ """str : The user-defined name of this object.
53
+ """
54
+ return self._name
55
+
56
+ @name.setter
57
+ def name(self, value):
58
+ if value is not None:
59
+ value = str(value)
60
+ self._name = value
61
+
62
+ @property
63
+ def color(self):
64
+ """(3,) float : The light's color.
65
+ """
66
+ return self._color
67
+
68
+ @color.setter
69
+ def color(self, value):
70
+ self._color = format_color_vector(value, 3)
71
+
72
+ @property
73
+ def intensity(self):
74
+ """float : The light's intensity in candela or lux.
75
+ """
76
+ return self._intensity
77
+
78
+ @intensity.setter
79
+ def intensity(self, value):
80
+ self._intensity = float(value)
81
+
82
+ @property
83
+ def shadow_texture(self):
84
+ """:class:`.Texture` : A texture used to hold shadow maps for this light.
85
+ """
86
+ return self._shadow_texture
87
+
88
+ @shadow_texture.setter
89
+ def shadow_texture(self, value):
90
+ if self._shadow_texture is not None:
91
+ if self._shadow_texture._in_context():
92
+ self._shadow_texture.delete()
93
+ self._shadow_texture = value
94
+
95
+ @abc.abstractmethod
96
+ def _generate_shadow_texture(self, size=None):
97
+ """Generate a shadow texture for this light.
98
+
99
+ Parameters
100
+ ----------
101
+ size : int, optional
102
+ Size of texture map. Must be a positive power of two.
103
+ """
104
+ pass
105
+
106
+ @abc.abstractmethod
107
+ def _get_shadow_camera(self, scene_scale):
108
+ """Generate and return a shadow mapping camera for this light.
109
+
110
+ Parameters
111
+ ----------
112
+ scene_scale : float
113
+ Length of scene's bounding box diagonal.
114
+
115
+ Returns
116
+ -------
117
+ camera : :class:`.Camera`
118
+ The camera used to render shadowmaps for this light.
119
+ """
120
+ pass
121
+
122
+
123
+ class DirectionalLight(Light):
124
+ """Directional lights are light sources that act as though they are
125
+ infinitely far away and emit light in the direction of the local -z axis.
126
+ This light type inherits the orientation of the node that it belongs to;
127
+ position and scale are ignored except for their effect on the inherited
128
+ node orientation. Because it is at an infinite distance, the light is
129
+ not attenuated. Its intensity is defined in lumens per metre squared,
130
+ or lux (lm/m2).
131
+
132
+ Parameters
133
+ ----------
134
+ color : (3,) float, optional
135
+ RGB value for the light's color in linear space. Defaults to white
136
+ (i.e. [1.0, 1.0, 1.0]).
137
+ intensity : float, optional
138
+ Brightness of light, in lux (lm/m^2). Defaults to 1.0
139
+ name : str, optional
140
+ Name of the light.
141
+ """
142
+
143
+ def __init__(self,
144
+ color=None,
145
+ intensity=None,
146
+ name=None):
147
+ super(DirectionalLight, self).__init__(
148
+ color=color,
149
+ intensity=intensity,
150
+ name=name,
151
+ )
152
+
153
+ def _generate_shadow_texture(self, size=None):
154
+ """Generate a shadow texture for this light.
155
+
156
+ Parameters
157
+ ----------
158
+ size : int, optional
159
+ Size of texture map. Must be a positive power of two.
160
+ """
161
+ if size is None:
162
+ size = SHADOW_TEX_SZ
163
+ self.shadow_texture = Texture(width=size, height=size,
164
+ source_channels='D', data_format=GL_FLOAT)
165
+
166
+ def _get_shadow_camera(self, scene_scale):
167
+ """Generate and return a shadow mapping camera for this light.
168
+
169
+ Parameters
170
+ ----------
171
+ scene_scale : float
172
+ Length of scene's bounding box diagonal.
173
+
174
+ Returns
175
+ -------
176
+ camera : :class:`.Camera`
177
+ The camera used to render shadowmaps for this light.
178
+ """
179
+ return OrthographicCamera(
180
+ znear=0.01 * scene_scale,
181
+ zfar=10 * scene_scale,
182
+ xmag=scene_scale,
183
+ ymag=scene_scale
184
+ )
185
+
186
+
187
+ class PointLight(Light):
188
+ """Point lights emit light in all directions from their position in space;
189
+ rotation and scale are ignored except for their effect on the inherited
190
+ node position. The brightness of the light attenuates in a physically
191
+ correct manner as distance increases from the light's position (i.e.
192
+ brightness goes like the inverse square of the distance). Point light
193
+ intensity is defined in candela, which is lumens per square radian (lm/sr).
194
+
195
+ Parameters
196
+ ----------
197
+ color : (3,) float
198
+ RGB value for the light's color in linear space.
199
+ intensity : float
200
+ Brightness of light in candela (lm/sr).
201
+ range : float
202
+ Cutoff distance at which light's intensity may be considered to
203
+ have reached zero. If None, the range is assumed to be infinite.
204
+ name : str, optional
205
+ Name of the light.
206
+ """
207
+
208
+ def __init__(self,
209
+ color=None,
210
+ intensity=None,
211
+ range=None,
212
+ name=None):
213
+ super(PointLight, self).__init__(
214
+ color=color,
215
+ intensity=intensity,
216
+ name=name,
217
+ )
218
+ self.range = range
219
+
220
+ @property
221
+ def range(self):
222
+ """float : The cutoff distance for the light.
223
+ """
224
+ return self._range
225
+
226
+ @range.setter
227
+ def range(self, value):
228
+ if value is not None:
229
+ value = float(value)
230
+ if value <= 0:
231
+ raise ValueError('Range must be > 0')
232
+ self._range = value
233
+ self._range = value
234
+
235
+ def _generate_shadow_texture(self, size=None):
236
+ """Generate a shadow texture for this light.
237
+
238
+ Parameters
239
+ ----------
240
+ size : int, optional
241
+ Size of texture map. Must be a positive power of two.
242
+ """
243
+ raise NotImplementedError('Shadows not implemented for point lights')
244
+
245
+ def _get_shadow_camera(self, scene_scale):
246
+ """Generate and return a shadow mapping camera for this light.
247
+
248
+ Parameters
249
+ ----------
250
+ scene_scale : float
251
+ Length of scene's bounding box diagonal.
252
+
253
+ Returns
254
+ -------
255
+ camera : :class:`.Camera`
256
+ The camera used to render shadowmaps for this light.
257
+ """
258
+ raise NotImplementedError('Shadows not implemented for point lights')
259
+
260
+
261
+ class SpotLight(Light):
262
+ """Spot lights emit light in a cone in the direction of the local -z axis.
263
+ The angle and falloff of the cone is defined using two numbers, the
264
+ ``innerConeAngle`` and ``outerConeAngle``.
265
+ As with point lights, the brightness
266
+ also attenuates in a physically correct manner as distance increases from
267
+ the light's position (i.e. brightness goes like the inverse square of the
268
+ distance). Spot light intensity refers to the brightness inside the
269
+ ``innerConeAngle`` (and at the location of the light) and is defined in
270
+ candela, which is lumens per square radian (lm/sr). A spot light's position
271
+ and orientation are inherited from its node transform. Inherited scale does
272
+ not affect cone shape, and is ignored except for its effect on position
273
+ and orientation.
274
+
275
+ Parameters
276
+ ----------
277
+ color : (3,) float
278
+ RGB value for the light's color in linear space.
279
+ intensity : float
280
+ Brightness of light in candela (lm/sr).
281
+ range : float
282
+ Cutoff distance at which light's intensity may be considered to
283
+ have reached zero. If None, the range is assumed to be infinite.
284
+ innerConeAngle : float
285
+ Angle, in radians, from centre of spotlight where falloff begins.
286
+ Must be greater than or equal to ``0`` and less
287
+ than ``outerConeAngle``. Defaults to ``0``.
288
+ outerConeAngle : float
289
+ Angle, in radians, from centre of spotlight where falloff ends.
290
+ Must be greater than ``innerConeAngle`` and less than or equal to
291
+ ``PI / 2.0``. Defaults to ``PI / 4.0``.
292
+ name : str, optional
293
+ Name of the light.
294
+ """
295
+
296
+ def __init__(self,
297
+ color=None,
298
+ intensity=None,
299
+ range=None,
300
+ innerConeAngle=0.0,
301
+ outerConeAngle=(np.pi / 4.0),
302
+ name=None):
303
+ super(SpotLight, self).__init__(
304
+ name=name,
305
+ color=color,
306
+ intensity=intensity,
307
+ )
308
+ self.outerConeAngle = outerConeAngle
309
+ self.innerConeAngle = innerConeAngle
310
+ self.range = range
311
+
312
+ @property
313
+ def innerConeAngle(self):
314
+ """float : The inner cone angle in radians.
315
+ """
316
+ return self._innerConeAngle
317
+
318
+ @innerConeAngle.setter
319
+ def innerConeAngle(self, value):
320
+ if value < 0.0 or value > self.outerConeAngle:
321
+ raise ValueError('Invalid value for inner cone angle')
322
+ self._innerConeAngle = float(value)
323
+
324
+ @property
325
+ def outerConeAngle(self):
326
+ """float : The outer cone angle in radians.
327
+ """
328
+ return self._outerConeAngle
329
+
330
+ @outerConeAngle.setter
331
+ def outerConeAngle(self, value):
332
+ if value < 0.0 or value > np.pi / 2.0 + 1e-9:
333
+ raise ValueError('Invalid value for outer cone angle')
334
+ self._outerConeAngle = float(value)
335
+
336
+ @property
337
+ def range(self):
338
+ """float : The cutoff distance for the light.
339
+ """
340
+ return self._range
341
+
342
+ @range.setter
343
+ def range(self, value):
344
+ if value is not None:
345
+ value = float(value)
346
+ if value <= 0:
347
+ raise ValueError('Range must be > 0')
348
+ self._range = value
349
+ self._range = value
350
+
351
+ def _generate_shadow_texture(self, size=None):
352
+ """Generate a shadow texture for this light.
353
+
354
+ Parameters
355
+ ----------
356
+ size : int, optional
357
+ Size of texture map. Must be a positive power of two.
358
+ """
359
+ if size is None:
360
+ size = SHADOW_TEX_SZ
361
+ self.shadow_texture = Texture(width=size, height=size,
362
+ source_channels='D', data_format=GL_FLOAT)
363
+
364
+ def _get_shadow_camera(self, scene_scale):
365
+ """Generate and return a shadow mapping camera for this light.
366
+
367
+ Parameters
368
+ ----------
369
+ scene_scale : float
370
+ Length of scene's bounding box diagonal.
371
+
372
+ Returns
373
+ -------
374
+ camera : :class:`.Camera`
375
+ The camera used to render shadowmaps for this light.
376
+ """
377
+ return PerspectiveCamera(
378
+ znear=0.01 * scene_scale,
379
+ zfar=10 * scene_scale,
380
+ yfov=np.clip(2 * self.outerConeAngle + np.pi / 16.0, 0.0, np.pi),
381
+ aspectRatio=1.0
382
+ )
383
+
384
+
385
+ __all__ = ['Light', 'DirectionalLight', 'SpotLight', 'PointLight']
pyrender/pyrender/material.py ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Material properties, conforming to the glTF 2.0 standards as specified in
2
+ https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-material
3
+ and
4
+ https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
5
+
6
+ Author: Matthew Matl
7
+ """
8
+ import abc
9
+ import numpy as np
10
+ import six
11
+
12
+ from .constants import TexFlags
13
+ from .utils import format_color_vector, format_texture_source
14
+ from .texture import Texture
15
+
16
+
17
+ @six.add_metaclass(abc.ABCMeta)
18
+ class Material(object):
19
+ """Base for standard glTF 2.0 materials.
20
+
21
+ Parameters
22
+ ----------
23
+ name : str, optional
24
+ The user-defined name of this object.
25
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
26
+ A tangent space normal map. The texture contains RGB components in
27
+ linear space. Each texel represents the XYZ components of a normal
28
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
29
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
30
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
31
+ right and +Y is up. +Z points toward the viewer.
32
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
33
+ The occlusion map texture. The occlusion values are sampled from the R
34
+ channel. Higher values indicate areas that should receive full indirect
35
+ lighting and lower values indicate no indirect lighting. These values
36
+ are linear. If other channels are present (GBA), they are ignored for
37
+ occlusion calculations.
38
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
39
+ The emissive map controls the color and intensity of the light being
40
+ emitted by the material. This texture contains RGB components in sRGB
41
+ color space. If a fourth component (A) is present, it is ignored.
42
+ emissiveFactor : (3,) float, optional
43
+ The RGB components of the emissive color of the material. These values
44
+ are linear. If an emissiveTexture is specified, this value is
45
+ multiplied with the texel values.
46
+ alphaMode : str, optional
47
+ The material's alpha rendering mode enumeration specifying the
48
+ interpretation of the alpha value of the main factor and texture.
49
+ Allowed Values:
50
+
51
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
52
+ fully opaque.
53
+ - `"MASK"` The rendered output is either fully opaque or fully
54
+ transparent depending on the alpha value and the specified alpha
55
+ cutoff value.
56
+ - `"BLEND"` The alpha value is used to composite the source and
57
+ destination areas. The rendered output is combined with the
58
+ background using the normal painting operation (i.e. the Porter
59
+ and Duff over operator).
60
+
61
+ alphaCutoff : float, optional
62
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
63
+ greater than or equal to this value then it is rendered as fully
64
+ opaque, otherwise, it is rendered as fully transparent.
65
+ A value greater than 1.0 will render the entire material as fully
66
+ transparent. This value is ignored for other modes.
67
+ doubleSided : bool, optional
68
+ Specifies whether the material is double sided. When this value is
69
+ false, back-face culling is enabled. When this value is true,
70
+ back-face culling is disabled and double sided lighting is enabled.
71
+ smooth : bool, optional
72
+ If True, the material is rendered smoothly by using only one normal
73
+ per vertex and face indexing.
74
+ wireframe : bool, optional
75
+ If True, the material is rendered in wireframe mode.
76
+ """
77
+
78
+ def __init__(self,
79
+ name=None,
80
+ normalTexture=None,
81
+ occlusionTexture=None,
82
+ emissiveTexture=None,
83
+ emissiveFactor=None,
84
+ alphaMode=None,
85
+ alphaCutoff=None,
86
+ doubleSided=False,
87
+ smooth=True,
88
+ wireframe=False):
89
+
90
+ # Set defaults
91
+ if alphaMode is None:
92
+ alphaMode = 'OPAQUE'
93
+
94
+ if alphaCutoff is None:
95
+ alphaCutoff = 0.5
96
+
97
+ if emissiveFactor is None:
98
+ emissiveFactor = np.zeros(3).astype(np.float32)
99
+
100
+ self.name = name
101
+ self.normalTexture = normalTexture
102
+ self.occlusionTexture = occlusionTexture
103
+ self.emissiveTexture = emissiveTexture
104
+ self.emissiveFactor = emissiveFactor
105
+ self.alphaMode = alphaMode
106
+ self.alphaCutoff = alphaCutoff
107
+ self.doubleSided = doubleSided
108
+ self.smooth = smooth
109
+ self.wireframe = wireframe
110
+
111
+ self._tex_flags = None
112
+
113
+ @property
114
+ def name(self):
115
+ """str : The user-defined name of this object.
116
+ """
117
+ return self._name
118
+
119
+ @name.setter
120
+ def name(self, value):
121
+ if value is not None:
122
+ value = str(value)
123
+ self._name = value
124
+
125
+ @property
126
+ def normalTexture(self):
127
+ """(n,n,3) float or :class:`Texture` : The tangent-space normal map.
128
+ """
129
+ return self._normalTexture
130
+
131
+ @normalTexture.setter
132
+ def normalTexture(self, value):
133
+ # TODO TMP
134
+ self._normalTexture = self._format_texture(value, 'RGB')
135
+ self._tex_flags = None
136
+
137
+ @property
138
+ def occlusionTexture(self):
139
+ """(n,n,1) float or :class:`Texture` : The ambient occlusion map.
140
+ """
141
+ return self._occlusionTexture
142
+
143
+ @occlusionTexture.setter
144
+ def occlusionTexture(self, value):
145
+ self._occlusionTexture = self._format_texture(value, 'R')
146
+ self._tex_flags = None
147
+
148
+ @property
149
+ def emissiveTexture(self):
150
+ """(n,n,3) float or :class:`Texture` : The emission map.
151
+ """
152
+ return self._emissiveTexture
153
+
154
+ @emissiveTexture.setter
155
+ def emissiveTexture(self, value):
156
+ self._emissiveTexture = self._format_texture(value, 'RGB')
157
+ self._tex_flags = None
158
+
159
+ @property
160
+ def emissiveFactor(self):
161
+ """(3,) float : Base multiplier for emission colors.
162
+ """
163
+ return self._emissiveFactor
164
+
165
+ @emissiveFactor.setter
166
+ def emissiveFactor(self, value):
167
+ if value is None:
168
+ value = np.zeros(3)
169
+ self._emissiveFactor = format_color_vector(value, 3)
170
+
171
+ @property
172
+ def alphaMode(self):
173
+ """str : The mode for blending.
174
+ """
175
+ return self._alphaMode
176
+
177
+ @alphaMode.setter
178
+ def alphaMode(self, value):
179
+ if value not in set(['OPAQUE', 'MASK', 'BLEND']):
180
+ raise ValueError('Invalid alpha mode {}'.format(value))
181
+ self._alphaMode = value
182
+
183
+ @property
184
+ def alphaCutoff(self):
185
+ """float : The cutoff threshold in MASK mode.
186
+ """
187
+ return self._alphaCutoff
188
+
189
+ @alphaCutoff.setter
190
+ def alphaCutoff(self, value):
191
+ if value < 0 or value > 1:
192
+ raise ValueError('Alpha cutoff must be in range [0,1]')
193
+ self._alphaCutoff = float(value)
194
+
195
+ @property
196
+ def doubleSided(self):
197
+ """bool : Whether the material is double-sided.
198
+ """
199
+ return self._doubleSided
200
+
201
+ @doubleSided.setter
202
+ def doubleSided(self, value):
203
+ if not isinstance(value, bool):
204
+ raise TypeError('Double sided must be a boolean value')
205
+ self._doubleSided = value
206
+
207
+ @property
208
+ def smooth(self):
209
+ """bool : Whether to render the mesh smoothly by
210
+ interpolating vertex normals.
211
+ """
212
+ return self._smooth
213
+
214
+ @smooth.setter
215
+ def smooth(self, value):
216
+ if not isinstance(value, bool):
217
+ raise TypeError('Double sided must be a boolean value')
218
+ self._smooth = value
219
+
220
+ @property
221
+ def wireframe(self):
222
+ """bool : Whether to render the mesh in wireframe mode.
223
+ """
224
+ return self._wireframe
225
+
226
+ @wireframe.setter
227
+ def wireframe(self, value):
228
+ if not isinstance(value, bool):
229
+ raise TypeError('Wireframe must be a boolean value')
230
+ self._wireframe = value
231
+
232
+ @property
233
+ def is_transparent(self):
234
+ """bool : If True, the object is partially transparent.
235
+ """
236
+ return self._compute_transparency()
237
+
238
+ @property
239
+ def tex_flags(self):
240
+ """int : Texture availability flags.
241
+ """
242
+ if self._tex_flags is None:
243
+ self._tex_flags = self._compute_tex_flags()
244
+ return self._tex_flags
245
+
246
+ @property
247
+ def textures(self):
248
+ """list of :class:`Texture` : The textures associated with this
249
+ material.
250
+ """
251
+ return self._compute_textures()
252
+
253
+ def _compute_transparency(self):
254
+ return False
255
+
256
+ def _compute_tex_flags(self):
257
+ tex_flags = TexFlags.NONE
258
+ if self.normalTexture is not None:
259
+ tex_flags |= TexFlags.NORMAL
260
+ if self.occlusionTexture is not None:
261
+ tex_flags |= TexFlags.OCCLUSION
262
+ if self.emissiveTexture is not None:
263
+ tex_flags |= TexFlags.EMISSIVE
264
+ return tex_flags
265
+
266
+ def _compute_textures(self):
267
+ all_textures = [
268
+ self.normalTexture, self.occlusionTexture, self.emissiveTexture
269
+ ]
270
+ textures = set([t for t in all_textures if t is not None])
271
+ return textures
272
+
273
+ def _format_texture(self, texture, target_channels='RGB'):
274
+ """Format a texture as a float32 np array.
275
+ """
276
+ if isinstance(texture, Texture) or texture is None:
277
+ return texture
278
+ else:
279
+ source = format_texture_source(texture, target_channels)
280
+ return Texture(source=source, source_channels=target_channels)
281
+
282
+
283
+ class MetallicRoughnessMaterial(Material):
284
+ """A material based on the metallic-roughness material model from
285
+ Physically-Based Rendering (PBR) methodology.
286
+
287
+ Parameters
288
+ ----------
289
+ name : str, optional
290
+ The user-defined name of this object.
291
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
292
+ A tangent space normal map. The texture contains RGB components in
293
+ linear space. Each texel represents the XYZ components of a normal
294
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
295
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
296
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
297
+ right and +Y is up. +Z points toward the viewer.
298
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
299
+ The occlusion map texture. The occlusion values are sampled from the R
300
+ channel. Higher values indicate areas that should receive full indirect
301
+ lighting and lower values indicate no indirect lighting. These values
302
+ are linear. If other channels are present (GBA), they are ignored for
303
+ occlusion calculations.
304
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
305
+ The emissive map controls the color and intensity of the light being
306
+ emitted by the material. This texture contains RGB components in sRGB
307
+ color space. If a fourth component (A) is present, it is ignored.
308
+ emissiveFactor : (3,) float, optional
309
+ The RGB components of the emissive color of the material. These values
310
+ are linear. If an emissiveTexture is specified, this value is
311
+ multiplied with the texel values.
312
+ alphaMode : str, optional
313
+ The material's alpha rendering mode enumeration specifying the
314
+ interpretation of the alpha value of the main factor and texture.
315
+ Allowed Values:
316
+
317
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
318
+ fully opaque.
319
+ - `"MASK"` The rendered output is either fully opaque or fully
320
+ transparent depending on the alpha value and the specified alpha
321
+ cutoff value.
322
+ - `"BLEND"` The alpha value is used to composite the source and
323
+ destination areas. The rendered output is combined with the
324
+ background using the normal painting operation (i.e. the Porter
325
+ and Duff over operator).
326
+
327
+ alphaCutoff : float, optional
328
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
329
+ greater than or equal to this value then it is rendered as fully
330
+ opaque, otherwise, it is rendered as fully transparent.
331
+ A value greater than 1.0 will render the entire material as fully
332
+ transparent. This value is ignored for other modes.
333
+ doubleSided : bool, optional
334
+ Specifies whether the material is double sided. When this value is
335
+ false, back-face culling is enabled. When this value is true,
336
+ back-face culling is disabled and double sided lighting is enabled.
337
+ smooth : bool, optional
338
+ If True, the material is rendered smoothly by using only one normal
339
+ per vertex and face indexing.
340
+ wireframe : bool, optional
341
+ If True, the material is rendered in wireframe mode.
342
+ baseColorFactor : (4,) float, optional
343
+ The RGBA components of the base color of the material. The fourth
344
+ component (A) is the alpha coverage of the material. The alphaMode
345
+ property specifies how alpha is interpreted. These values are linear.
346
+ If a baseColorTexture is specified, this value is multiplied with the
347
+ texel values.
348
+ baseColorTexture : (n,n,4) float or :class:`Texture`, optional
349
+ The base color texture. This texture contains RGB(A) components in sRGB
350
+ color space. The first three components (RGB) specify the base color of
351
+ the material. If the fourth component (A) is present, it represents the
352
+ alpha coverage of the material. Otherwise, an alpha of 1.0 is assumed.
353
+ The alphaMode property specifies how alpha is interpreted.
354
+ The stored texels must not be premultiplied.
355
+ metallicFactor : float
356
+ The metalness of the material. A value of 1.0 means the material is a
357
+ metal. A value of 0.0 means the material is a dielectric. Values in
358
+ between are for blending between metals and dielectrics such as dirty
359
+ metallic surfaces. This value is linear. If a metallicRoughnessTexture
360
+ is specified, this value is multiplied with the metallic texel values.
361
+ roughnessFactor : float
362
+ The roughness of the material. A value of 1.0 means the material is
363
+ completely rough. A value of 0.0 means the material is completely
364
+ smooth. This value is linear. If a metallicRoughnessTexture is
365
+ specified, this value is multiplied with the roughness texel values.
366
+ metallicRoughnessTexture : (n,n,2) float or :class:`Texture`, optional
367
+ The metallic-roughness texture. The metalness values are sampled from
368
+ the B channel. The roughness values are sampled from the G channel.
369
+ These values are linear. If other channels are present (R or A), they
370
+ are ignored for metallic-roughness calculations.
371
+ """
372
+
373
+ def __init__(self,
374
+ name=None,
375
+ normalTexture=None,
376
+ occlusionTexture=None,
377
+ emissiveTexture=None,
378
+ emissiveFactor=None,
379
+ alphaMode=None,
380
+ alphaCutoff=None,
381
+ doubleSided=False,
382
+ smooth=True,
383
+ wireframe=False,
384
+ baseColorFactor=None,
385
+ baseColorTexture=None,
386
+ metallicFactor=1.0,
387
+ roughnessFactor=1.0,
388
+ metallicRoughnessTexture=None):
389
+ super(MetallicRoughnessMaterial, self).__init__(
390
+ name=name,
391
+ normalTexture=normalTexture,
392
+ occlusionTexture=occlusionTexture,
393
+ emissiveTexture=emissiveTexture,
394
+ emissiveFactor=emissiveFactor,
395
+ alphaMode=alphaMode,
396
+ alphaCutoff=alphaCutoff,
397
+ doubleSided=doubleSided,
398
+ smooth=smooth,
399
+ wireframe=wireframe
400
+ )
401
+
402
+ # Set defaults
403
+ if baseColorFactor is None:
404
+ baseColorFactor = np.ones(4).astype(np.float32)
405
+
406
+ self.baseColorFactor = baseColorFactor
407
+ self.baseColorTexture = baseColorTexture
408
+ self.metallicFactor = metallicFactor
409
+ self.roughnessFactor = roughnessFactor
410
+ self.metallicRoughnessTexture = metallicRoughnessTexture
411
+
412
+ @property
413
+ def baseColorFactor(self):
414
+ """(4,) float or :class:`Texture` : The RGBA base color multiplier.
415
+ """
416
+ return self._baseColorFactor
417
+
418
+ @baseColorFactor.setter
419
+ def baseColorFactor(self, value):
420
+ if value is None:
421
+ value = np.ones(4)
422
+ self._baseColorFactor = format_color_vector(value, 4)
423
+
424
+ @property
425
+ def baseColorTexture(self):
426
+ """(n,n,4) float or :class:`Texture` : The diffuse texture.
427
+ """
428
+ return self._baseColorTexture
429
+
430
+ @baseColorTexture.setter
431
+ def baseColorTexture(self, value):
432
+ self._baseColorTexture = self._format_texture(value, 'RGBA')
433
+ self._tex_flags = None
434
+
435
+ @property
436
+ def metallicFactor(self):
437
+ """float : The metalness of the material.
438
+ """
439
+ return self._metallicFactor
440
+
441
+ @metallicFactor.setter
442
+ def metallicFactor(self, value):
443
+ if value is None:
444
+ value = 1.0
445
+ if value < 0 or value > 1:
446
+ raise ValueError('Metallic factor must be in range [0,1]')
447
+ self._metallicFactor = float(value)
448
+
449
+ @property
450
+ def roughnessFactor(self):
451
+ """float : The roughness of the material.
452
+ """
453
+ return self.RoughnessFactor
454
+
455
+ @roughnessFactor.setter
456
+ def roughnessFactor(self, value):
457
+ if value is None:
458
+ value = 1.0
459
+ if value < 0 or value > 1:
460
+ raise ValueError('Roughness factor must be in range [0,1]')
461
+ self.RoughnessFactor = float(value)
462
+
463
+ @property
464
+ def metallicRoughnessTexture(self):
465
+ """(n,n,2) float or :class:`Texture` : The metallic-roughness texture.
466
+ """
467
+ return self._metallicRoughnessTexture
468
+
469
+ @metallicRoughnessTexture.setter
470
+ def metallicRoughnessTexture(self, value):
471
+ self._metallicRoughnessTexture = self._format_texture(value, 'GB')
472
+ self._tex_flags = None
473
+
474
+ def _compute_tex_flags(self):
475
+ tex_flags = super(MetallicRoughnessMaterial, self)._compute_tex_flags()
476
+ if self.baseColorTexture is not None:
477
+ tex_flags |= TexFlags.BASE_COLOR
478
+ if self.metallicRoughnessTexture is not None:
479
+ tex_flags |= TexFlags.METALLIC_ROUGHNESS
480
+ return tex_flags
481
+
482
+ def _compute_transparency(self):
483
+ if self.alphaMode == 'OPAQUE':
484
+ return False
485
+ cutoff = self.alphaCutoff
486
+ if self.alphaMode == 'BLEND':
487
+ cutoff = 1.0
488
+ if self.baseColorFactor[3] < cutoff:
489
+ return True
490
+ if (self.baseColorTexture is not None and
491
+ self.baseColorTexture.is_transparent(cutoff)):
492
+ return True
493
+ return False
494
+
495
+ def _compute_textures(self):
496
+ textures = super(MetallicRoughnessMaterial, self)._compute_textures()
497
+ all_textures = [self.baseColorTexture, self.metallicRoughnessTexture]
498
+ all_textures = {t for t in all_textures if t is not None}
499
+ textures |= all_textures
500
+ return textures
501
+
502
+
503
+ class SpecularGlossinessMaterial(Material):
504
+ """A material based on the specular-glossiness material model from
505
+ Physically-Based Rendering (PBR) methodology.
506
+
507
+ Parameters
508
+ ----------
509
+ name : str, optional
510
+ The user-defined name of this object.
511
+ normalTexture : (n,n,3) float or :class:`Texture`, optional
512
+ A tangent space normal map. The texture contains RGB components in
513
+ linear space. Each texel represents the XYZ components of a normal
514
+ vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. Green
515
+ [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z
516
+ [1/255 to 1]. The normal vectors use OpenGL conventions where +X is
517
+ right and +Y is up. +Z points toward the viewer.
518
+ occlusionTexture : (n,n,1) float or :class:`Texture`, optional
519
+ The occlusion map texture. The occlusion values are sampled from the R
520
+ channel. Higher values indicate areas that should receive full indirect
521
+ lighting and lower values indicate no indirect lighting. These values
522
+ are linear. If other channels are present (GBA), they are ignored for
523
+ occlusion calculations.
524
+ emissiveTexture : (n,n,3) float or :class:`Texture`, optional
525
+ The emissive map controls the color and intensity of the light being
526
+ emitted by the material. This texture contains RGB components in sRGB
527
+ color space. If a fourth component (A) is present, it is ignored.
528
+ emissiveFactor : (3,) float, optional
529
+ The RGB components of the emissive color of the material. These values
530
+ are linear. If an emissiveTexture is specified, this value is
531
+ multiplied with the texel values.
532
+ alphaMode : str, optional
533
+ The material's alpha rendering mode enumeration specifying the
534
+ interpretation of the alpha value of the main factor and texture.
535
+ Allowed Values:
536
+
537
+ - `"OPAQUE"` The alpha value is ignored and the rendered output is
538
+ fully opaque.
539
+ - `"MASK"` The rendered output is either fully opaque or fully
540
+ transparent depending on the alpha value and the specified alpha
541
+ cutoff value.
542
+ - `"BLEND"` The alpha value is used to composite the source and
543
+ destination areas. The rendered output is combined with the
544
+ background using the normal painting operation (i.e. the Porter
545
+ and Duff over operator).
546
+
547
+ alphaCutoff : float, optional
548
+ Specifies the cutoff threshold when in MASK mode. If the alpha value is
549
+ greater than or equal to this value then it is rendered as fully
550
+ opaque, otherwise, it is rendered as fully transparent.
551
+ A value greater than 1.0 will render the entire material as fully
552
+ transparent. This value is ignored for other modes.
553
+ doubleSided : bool, optional
554
+ Specifies whether the material is double sided. When this value is
555
+ false, back-face culling is enabled. When this value is true,
556
+ back-face culling is disabled and double sided lighting is enabled.
557
+ smooth : bool, optional
558
+ If True, the material is rendered smoothly by using only one normal
559
+ per vertex and face indexing.
560
+ wireframe : bool, optional
561
+ If True, the material is rendered in wireframe mode.
562
+ diffuseFactor : (4,) float
563
+ The RGBA components of the reflected diffuse color of the material.
564
+ Metals have a diffuse value of [0.0, 0.0, 0.0]. The fourth component
565
+ (A) is the opacity of the material. The values are linear.
566
+ diffuseTexture : (n,n,4) float or :class:`Texture`, optional
567
+ The diffuse texture. This texture contains RGB(A) components of the
568
+ reflected diffuse color of the material in sRGB color space. If the
569
+ fourth component (A) is present, it represents the alpha coverage of
570
+ the material. Otherwise, an alpha of 1.0 is assumed.
571
+ The alphaMode property specifies how alpha is interpreted.
572
+ The stored texels must not be premultiplied.
573
+ specularFactor : (3,) float
574
+ The specular RGB color of the material. This value is linear.
575
+ glossinessFactor : float
576
+ The glossiness or smoothness of the material. A value of 1.0 means the
577
+ material has full glossiness or is perfectly smooth. A value of 0.0
578
+ means the material has no glossiness or is perfectly rough. This value
579
+ is linear.
580
+ specularGlossinessTexture : (n,n,4) or :class:`Texture`, optional
581
+ The specular-glossiness texture is a RGBA texture, containing the
582
+ specular color (RGB) in sRGB space and the glossiness value (A) in
583
+ linear space.
584
+ """
585
+
586
+ def __init__(self,
587
+ name=None,
588
+ normalTexture=None,
589
+ occlusionTexture=None,
590
+ emissiveTexture=None,
591
+ emissiveFactor=None,
592
+ alphaMode=None,
593
+ alphaCutoff=None,
594
+ doubleSided=False,
595
+ smooth=True,
596
+ wireframe=False,
597
+ diffuseFactor=None,
598
+ diffuseTexture=None,
599
+ specularFactor=None,
600
+ glossinessFactor=1.0,
601
+ specularGlossinessTexture=None):
602
+ super(SpecularGlossinessMaterial, self).__init__(
603
+ name=name,
604
+ normalTexture=normalTexture,
605
+ occlusionTexture=occlusionTexture,
606
+ emissiveTexture=emissiveTexture,
607
+ emissiveFactor=emissiveFactor,
608
+ alphaMode=alphaMode,
609
+ alphaCutoff=alphaCutoff,
610
+ doubleSided=doubleSided,
611
+ smooth=smooth,
612
+ wireframe=wireframe
613
+ )
614
+
615
+ # Set defaults
616
+ if diffuseFactor is None:
617
+ diffuseFactor = np.ones(4).astype(np.float32)
618
+ if specularFactor is None:
619
+ specularFactor = np.ones(3).astype(np.float32)
620
+
621
+ self.diffuseFactor = diffuseFactor
622
+ self.diffuseTexture = diffuseTexture
623
+ self.specularFactor = specularFactor
624
+ self.glossinessFactor = glossinessFactor
625
+ self.specularGlossinessTexture = specularGlossinessTexture
626
+
627
+ @property
628
+ def diffuseFactor(self):
629
+ """(4,) float : The diffuse base color.
630
+ """
631
+ return self._diffuseFactor
632
+
633
+ @diffuseFactor.setter
634
+ def diffuseFactor(self, value):
635
+ self._diffuseFactor = format_color_vector(value, 4)
636
+
637
+ @property
638
+ def diffuseTexture(self):
639
+ """(n,n,4) float or :class:`Texture` : The diffuse map.
640
+ """
641
+ return self._diffuseTexture
642
+
643
+ @diffuseTexture.setter
644
+ def diffuseTexture(self, value):
645
+ self._diffuseTexture = self._format_texture(value, 'RGBA')
646
+ self._tex_flags = None
647
+
648
+ @property
649
+ def specularFactor(self):
650
+ """(3,) float : The specular color of the material.
651
+ """
652
+ return self._specularFactor
653
+
654
+ @specularFactor.setter
655
+ def specularFactor(self, value):
656
+ self._specularFactor = format_color_vector(value, 3)
657
+
658
+ @property
659
+ def glossinessFactor(self):
660
+ """float : The glossiness of the material.
661
+ """
662
+ return self.glossinessFactor
663
+
664
+ @glossinessFactor.setter
665
+ def glossinessFactor(self, value):
666
+ if value < 0 or value > 1:
667
+ raise ValueError('glossiness factor must be in range [0,1]')
668
+ self._glossinessFactor = float(value)
669
+
670
+ @property
671
+ def specularGlossinessTexture(self):
672
+ """(n,n,4) or :class:`Texture` : The specular-glossiness texture.
673
+ """
674
+ return self._specularGlossinessTexture
675
+
676
+ @specularGlossinessTexture.setter
677
+ def specularGlossinessTexture(self, value):
678
+ self._specularGlossinessTexture = self._format_texture(value, 'GB')
679
+ self._tex_flags = None
680
+
681
+ def _compute_tex_flags(self):
682
+ flags = super(SpecularGlossinessMaterial, self)._compute_tex_flags()
683
+ if self.diffuseTexture is not None:
684
+ flags |= TexFlags.DIFFUSE
685
+ if self.specularGlossinessTexture is not None:
686
+ flags |= TexFlags.SPECULAR_GLOSSINESS
687
+ return flags
688
+
689
+ def _compute_transparency(self):
690
+ if self.alphaMode == 'OPAQUE':
691
+ return False
692
+ cutoff = self.alphaCutoff
693
+ if self.alphaMode == 'BLEND':
694
+ cutoff = 1.0
695
+ if self.diffuseFactor[3] < cutoff:
696
+ return True
697
+ if (self.diffuseTexture is not None and
698
+ self.diffuseTexture.is_transparent(cutoff)):
699
+ return True
700
+ return False
701
+
702
+ def _compute_textures(self):
703
+ textures = super(SpecularGlossinessMaterial, self)._compute_textures()
704
+ all_textures = [self.diffuseTexture, self.specularGlossinessTexture]
705
+ all_textures = {t for t in all_textures if t is not None}
706
+ textures |= all_textures
707
+ return textures
pyrender/pyrender/mesh.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Meshes, conforming to the glTF 2.0 standards as specified in
2
+ https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import copy
7
+
8
+ import numpy as np
9
+ import trimesh
10
+
11
+ from .primitive import Primitive
12
+ from .constants import GLTF
13
+ from .material import MetallicRoughnessMaterial
14
+
15
+
16
+ class Mesh(object):
17
+ """A set of primitives to be rendered.
18
+
19
+ Parameters
20
+ ----------
21
+ name : str
22
+ The user-defined name of this object.
23
+ primitives : list of :class:`Primitive`
24
+ The primitives associated with this mesh.
25
+ weights : (k,) float
26
+ Array of weights to be applied to the Morph Targets.
27
+ is_visible : bool
28
+ If False, the mesh will not be rendered.
29
+ """
30
+
31
+ def __init__(self, primitives, name=None, weights=None, is_visible=True):
32
+ self.primitives = primitives
33
+ self.name = name
34
+ self.weights = weights
35
+ self.is_visible = is_visible
36
+
37
+ self._bounds = None
38
+
39
+ @property
40
+ def name(self):
41
+ """str : The user-defined name of this object.
42
+ """
43
+ return self._name
44
+
45
+ @name.setter
46
+ def name(self, value):
47
+ if value is not None:
48
+ value = str(value)
49
+ self._name = value
50
+
51
+ @property
52
+ def primitives(self):
53
+ """list of :class:`Primitive` : The primitives associated
54
+ with this mesh.
55
+ """
56
+ return self._primitives
57
+
58
+ @primitives.setter
59
+ def primitives(self, value):
60
+ self._primitives = value
61
+
62
+ @property
63
+ def weights(self):
64
+ """(k,) float : Weights to be applied to morph targets.
65
+ """
66
+ return self._weights
67
+
68
+ @weights.setter
69
+ def weights(self, value):
70
+ self._weights = value
71
+
72
+ @property
73
+ def is_visible(self):
74
+ """bool : Whether the mesh is visible.
75
+ """
76
+ return self._is_visible
77
+
78
+ @is_visible.setter
79
+ def is_visible(self, value):
80
+ self._is_visible = value
81
+
82
+ @property
83
+ def bounds(self):
84
+ """(2,3) float : The axis-aligned bounds of the mesh.
85
+ """
86
+ if self._bounds is None:
87
+ bounds = np.array([[np.infty, np.infty, np.infty],
88
+ [-np.infty, -np.infty, -np.infty]])
89
+ for p in self.primitives:
90
+ bounds[0] = np.minimum(bounds[0], p.bounds[0])
91
+ bounds[1] = np.maximum(bounds[1], p.bounds[1])
92
+ self._bounds = bounds
93
+ return self._bounds
94
+
95
+ @property
96
+ def centroid(self):
97
+ """(3,) float : The centroid of the mesh's axis-aligned bounding box
98
+ (AABB).
99
+ """
100
+ return np.mean(self.bounds, axis=0)
101
+
102
+ @property
103
+ def extents(self):
104
+ """(3,) float : The lengths of the axes of the mesh's AABB.
105
+ """
106
+ return np.diff(self.bounds, axis=0).reshape(-1)
107
+
108
+ @property
109
+ def scale(self):
110
+ """(3,) float : The length of the diagonal of the mesh's AABB.
111
+ """
112
+ return np.linalg.norm(self.extents)
113
+
114
+ @property
115
+ def is_transparent(self):
116
+ """bool : If True, the mesh is partially-transparent.
117
+ """
118
+ for p in self.primitives:
119
+ if p.is_transparent:
120
+ return True
121
+ return False
122
+
123
+ @staticmethod
124
+ def from_points(points, colors=None, normals=None,
125
+ is_visible=True, poses=None):
126
+ """Create a Mesh from a set of points.
127
+
128
+ Parameters
129
+ ----------
130
+ points : (n,3) float
131
+ The point positions.
132
+ colors : (n,3) or (n,4) float, optional
133
+ RGB or RGBA colors for each point.
134
+ normals : (n,3) float, optionals
135
+ The normal vectors for each point.
136
+ is_visible : bool
137
+ If False, the points will not be rendered.
138
+ poses : (x,4,4)
139
+ Array of 4x4 transformation matrices for instancing this object.
140
+
141
+ Returns
142
+ -------
143
+ mesh : :class:`Mesh`
144
+ The created mesh.
145
+ """
146
+ primitive = Primitive(
147
+ positions=points,
148
+ normals=normals,
149
+ color_0=colors,
150
+ mode=GLTF.POINTS,
151
+ poses=poses
152
+ )
153
+ mesh = Mesh(primitives=[primitive], is_visible=is_visible)
154
+ return mesh
155
+
156
+ @staticmethod
157
+ def from_trimesh(mesh, material=None, is_visible=True,
158
+ poses=None, wireframe=False, smooth=True):
159
+ """Create a Mesh from a :class:`~trimesh.base.Trimesh`.
160
+
161
+ Parameters
162
+ ----------
163
+ mesh : :class:`~trimesh.base.Trimesh` or list of them
164
+ A triangular mesh or a list of meshes.
165
+ material : :class:`Material`
166
+ The material of the object. Overrides any mesh material.
167
+ If not specified and the mesh has no material, a default material
168
+ will be used.
169
+ is_visible : bool
170
+ If False, the mesh will not be rendered.
171
+ poses : (n,4,4) float
172
+ Array of 4x4 transformation matrices for instancing this object.
173
+ wireframe : bool
174
+ If `True`, the mesh will be rendered as a wireframe object
175
+ smooth : bool
176
+ If `True`, the mesh will be rendered with interpolated vertex
177
+ normals. Otherwise, the mesh edges will stay sharp.
178
+
179
+ Returns
180
+ -------
181
+ mesh : :class:`Mesh`
182
+ The created mesh.
183
+ """
184
+
185
+ if isinstance(mesh, (list, tuple, set, np.ndarray)):
186
+ meshes = list(mesh)
187
+ elif isinstance(mesh, trimesh.Trimesh):
188
+ meshes = [mesh]
189
+ else:
190
+ raise TypeError('Expected a Trimesh or a list, got a {}'
191
+ .format(type(mesh)))
192
+
193
+ primitives = []
194
+ for m in meshes:
195
+ positions = None
196
+ normals = None
197
+ indices = None
198
+
199
+ # Compute positions, normals, and indices
200
+ if smooth:
201
+ positions = m.vertices.copy()
202
+ normals = m.vertex_normals.copy()
203
+ indices = m.faces.copy()
204
+ else:
205
+ positions = m.vertices[m.faces].reshape((3 * len(m.faces), 3))
206
+ normals = np.repeat(m.face_normals, 3, axis=0)
207
+
208
+ # Compute colors, texture coords, and material properties
209
+ color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props(m, smooth=smooth, material=material)
210
+
211
+ # Override if material is given.
212
+ if material is not None:
213
+ #primitive_material = copy.copy(material)
214
+ primitive_material = copy.deepcopy(material) # TODO
215
+
216
+ if primitive_material is None:
217
+ # Replace material with default if needed
218
+ primitive_material = MetallicRoughnessMaterial(
219
+ alphaMode='BLEND',
220
+ baseColorFactor=[0.3, 0.3, 0.3, 1.0],
221
+ metallicFactor=0.2,
222
+ roughnessFactor=0.8
223
+ )
224
+
225
+ primitive_material.wireframe = wireframe
226
+
227
+ # Create the primitive
228
+ primitives.append(Primitive(
229
+ positions=positions,
230
+ normals=normals,
231
+ texcoord_0=texcoord_0,
232
+ color_0=color_0,
233
+ indices=indices,
234
+ material=primitive_material,
235
+ mode=GLTF.TRIANGLES,
236
+ poses=poses
237
+ ))
238
+
239
+ return Mesh(primitives=primitives, is_visible=is_visible)
240
+
241
+ @staticmethod
242
+ def _get_trimesh_props(mesh, smooth=False, material=None):
243
+ """Gets the vertex colors, texture coordinates, and material properties
244
+ from a :class:`~trimesh.base.Trimesh`.
245
+ """
246
+ colors = None
247
+ texcoords = None
248
+
249
+ # If the trimesh visual is undefined, return none for both
250
+ if not mesh.visual.defined:
251
+ return colors, texcoords, material
252
+
253
+ # Process vertex colors
254
+ if material is None:
255
+ if mesh.visual.kind == 'vertex':
256
+ vc = mesh.visual.vertex_colors.copy()
257
+ if smooth:
258
+ colors = vc
259
+ else:
260
+ colors = vc[mesh.faces].reshape(
261
+ (3 * len(mesh.faces), vc.shape[1])
262
+ )
263
+ material = MetallicRoughnessMaterial(
264
+ alphaMode='BLEND',
265
+ baseColorFactor=[1.0, 1.0, 1.0, 1.0],
266
+ metallicFactor=0.2,
267
+ roughnessFactor=0.8
268
+ )
269
+ # Process face colors
270
+ elif mesh.visual.kind == 'face':
271
+ if smooth:
272
+ raise ValueError('Cannot use face colors with a smooth mesh')
273
+ else:
274
+ colors = np.repeat(mesh.visual.face_colors, 3, axis=0)
275
+
276
+ material = MetallicRoughnessMaterial(
277
+ alphaMode='BLEND',
278
+ baseColorFactor=[1.0, 1.0, 1.0, 1.0],
279
+ metallicFactor=0.2,
280
+ roughnessFactor=0.8
281
+ )
282
+
283
+ # Process texture colors
284
+ if mesh.visual.kind == 'texture':
285
+ # Configure UV coordinates
286
+ if mesh.visual.uv is not None and len(mesh.visual.uv) != 0:
287
+ uv = mesh.visual.uv.copy()
288
+ if smooth:
289
+ texcoords = uv
290
+ else:
291
+ texcoords = uv[mesh.faces].reshape(
292
+ (3 * len(mesh.faces), uv.shape[1])
293
+ )
294
+
295
+ if material is None:
296
+ # Configure mesh material
297
+ mat = mesh.visual.material
298
+
299
+ if isinstance(mat, trimesh.visual.texture.PBRMaterial):
300
+ material = MetallicRoughnessMaterial(
301
+ normalTexture=mat.normalTexture,
302
+ occlusionTexture=mat.occlusionTexture,
303
+ emissiveTexture=mat.emissiveTexture,
304
+ emissiveFactor=mat.emissiveFactor,
305
+ alphaMode='BLEND',
306
+ baseColorFactor=mat.baseColorFactor,
307
+ baseColorTexture=mat.baseColorTexture,
308
+ metallicFactor=mat.metallicFactor,
309
+ roughnessFactor=mat.roughnessFactor,
310
+ metallicRoughnessTexture=mat.metallicRoughnessTexture,
311
+ doubleSided=mat.doubleSided,
312
+ alphaCutoff=mat.alphaCutoff
313
+ )
314
+ elif isinstance(mat, trimesh.visual.texture.SimpleMaterial):
315
+ glossiness = mat.kwargs.get('Ns', 1.0)
316
+ if isinstance(glossiness, list):
317
+ glossiness = float(glossiness[0])
318
+ roughness = (2 / (glossiness + 2)) ** (1.0 / 4.0)
319
+ material = MetallicRoughnessMaterial(
320
+ alphaMode='BLEND',
321
+ roughnessFactor=roughness,
322
+ baseColorFactor=mat.diffuse,
323
+ baseColorTexture=mat.image,
324
+ )
325
+ elif isinstance(mat, MetallicRoughnessMaterial):
326
+ material = mat
327
+
328
+ return colors, texcoords, material
pyrender/pyrender/node.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Nodes, conforming to the glTF 2.0 standards as specified in
2
+ https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import numpy as np
7
+
8
+ import trimesh.transformations as transformations
9
+
10
+ from .camera import Camera
11
+ from .mesh import Mesh
12
+ from .light import Light
13
+
14
+
15
+ class Node(object):
16
+ """A node in the node hierarchy.
17
+
18
+ Parameters
19
+ ----------
20
+ name : str, optional
21
+ The user-defined name of this object.
22
+ camera : :class:`Camera`, optional
23
+ The camera in this node.
24
+ children : list of :class:`Node`
25
+ The children of this node.
26
+ skin : int, optional
27
+ The index of the skin referenced by this node.
28
+ matrix : (4,4) float, optional
29
+ A floating-point 4x4 transformation matrix.
30
+ mesh : :class:`Mesh`, optional
31
+ The mesh in this node.
32
+ rotation : (4,) float, optional
33
+ The node's unit quaternion in the order (x, y, z, w), where
34
+ w is the scalar.
35
+ scale : (3,) float, optional
36
+ The node's non-uniform scale, given as the scaling factors along the x,
37
+ y, and z axes.
38
+ translation : (3,) float, optional
39
+ The node's translation along the x, y, and z axes.
40
+ weights : (n,) float
41
+ The weights of the instantiated Morph Target. Number of elements must
42
+ match number of Morph Targets of used mesh.
43
+ light : :class:`Light`, optional
44
+ The light in this node.
45
+ """
46
+
47
+ def __init__(self,
48
+ name=None,
49
+ camera=None,
50
+ children=None,
51
+ skin=None,
52
+ matrix=None,
53
+ mesh=None,
54
+ rotation=None,
55
+ scale=None,
56
+ translation=None,
57
+ weights=None,
58
+ light=None):
59
+ # Set defaults
60
+ if children is None:
61
+ children = []
62
+
63
+ self._matrix = None
64
+ self._scale = None
65
+ self._rotation = None
66
+ self._translation = None
67
+ if matrix is None:
68
+ if rotation is None:
69
+ rotation = np.array([0.0, 0.0, 0.0, 1.0])
70
+ if translation is None:
71
+ translation = np.zeros(3)
72
+ if scale is None:
73
+ scale = np.ones(3)
74
+ self.rotation = rotation
75
+ self.translation = translation
76
+ self.scale = scale
77
+ else:
78
+ self.matrix = matrix
79
+
80
+ self.name = name
81
+ self.camera = camera
82
+ self.children = children
83
+ self.skin = skin
84
+ self.mesh = mesh
85
+ self.weights = weights
86
+ self.light = light
87
+
88
+ @property
89
+ def name(self):
90
+ """str : The user-defined name of this object.
91
+ """
92
+ return self._name
93
+
94
+ @name.setter
95
+ def name(self, value):
96
+ if value is not None:
97
+ value = str(value)
98
+ self._name = value
99
+
100
+ @property
101
+ def camera(self):
102
+ """:class:`Camera` : The camera in this node.
103
+ """
104
+ return self._camera
105
+
106
+ @camera.setter
107
+ def camera(self, value):
108
+ if value is not None and not isinstance(value, Camera):
109
+ raise TypeError('Value must be a camera')
110
+ self._camera = value
111
+
112
+ @property
113
+ def children(self):
114
+ """list of :class:`Node` : The children of this node.
115
+ """
116
+ return self._children
117
+
118
+ @children.setter
119
+ def children(self, value):
120
+ self._children = value
121
+
122
+ @property
123
+ def skin(self):
124
+ """int : The skin index for this node.
125
+ """
126
+ return self._skin
127
+
128
+ @skin.setter
129
+ def skin(self, value):
130
+ self._skin = value
131
+
132
+ @property
133
+ def mesh(self):
134
+ """:class:`Mesh` : The mesh in this node.
135
+ """
136
+ return self._mesh
137
+
138
+ @mesh.setter
139
+ def mesh(self, value):
140
+ if value is not None and not isinstance(value, Mesh):
141
+ raise TypeError('Value must be a mesh')
142
+ self._mesh = value
143
+
144
+ @property
145
+ def light(self):
146
+ """:class:`Light` : The light in this node.
147
+ """
148
+ return self._light
149
+
150
+ @light.setter
151
+ def light(self, value):
152
+ if value is not None and not isinstance(value, Light):
153
+ raise TypeError('Value must be a light')
154
+ self._light = value
155
+
156
+ @property
157
+ def rotation(self):
158
+ """(4,) float : The xyzw quaternion for this node.
159
+ """
160
+ return self._rotation
161
+
162
+ @rotation.setter
163
+ def rotation(self, value):
164
+ value = np.asanyarray(value)
165
+ if value.shape != (4,):
166
+ raise ValueError('Quaternion must be a (4,) vector')
167
+ if np.abs(np.linalg.norm(value) - 1.0) > 1e-3:
168
+ raise ValueError('Quaternion must have norm == 1.0')
169
+ self._rotation = value
170
+ self._matrix = None
171
+
172
+ @property
173
+ def translation(self):
174
+ """(3,) float : The translation for this node.
175
+ """
176
+ return self._translation
177
+
178
+ @translation.setter
179
+ def translation(self, value):
180
+ value = np.asanyarray(value)
181
+ if value.shape != (3,):
182
+ raise ValueError('Translation must be a (3,) vector')
183
+ self._translation = value
184
+ self._matrix = None
185
+
186
+ @property
187
+ def scale(self):
188
+ """(3,) float : The scale for this node.
189
+ """
190
+ return self._scale
191
+
192
+ @scale.setter
193
+ def scale(self, value):
194
+ value = np.asanyarray(value)
195
+ if value.shape != (3,):
196
+ raise ValueError('Scale must be a (3,) vector')
197
+ self._scale = value
198
+ self._matrix = None
199
+
200
+ @property
201
+ def matrix(self):
202
+ """(4,4) float : The homogenous transform matrix for this node.
203
+
204
+ Note that this matrix's elements are not settable,
205
+ it's just a copy of the internal matrix. You can set the whole
206
+ matrix, but not an individual element.
207
+ """
208
+ if self._matrix is None:
209
+ self._matrix = self._m_from_tqs(
210
+ self.translation, self.rotation, self.scale
211
+ )
212
+ return self._matrix.copy()
213
+
214
+ @matrix.setter
215
+ def matrix(self, value):
216
+ value = np.asanyarray(value)
217
+ if value.shape != (4,4):
218
+ raise ValueError('Matrix must be a 4x4 numpy ndarray')
219
+ if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])):
220
+ raise ValueError('Bottom row of matrix must be [0,0,0,1]')
221
+ self.rotation = Node._q_from_m(value)
222
+ self.scale = Node._s_from_m(value)
223
+ self.translation = Node._t_from_m(value)
224
+ self._matrix = value
225
+
226
+ @staticmethod
227
+ def _t_from_m(m):
228
+ return m[:3,3]
229
+
230
+ @staticmethod
231
+ def _r_from_m(m):
232
+ U = m[:3,:3]
233
+ norms = np.linalg.norm(U.T, axis=1)
234
+ return U / norms
235
+
236
+ @staticmethod
237
+ def _q_from_m(m):
238
+ M = np.eye(4)
239
+ M[:3,:3] = Node._r_from_m(m)
240
+ q_wxyz = transformations.quaternion_from_matrix(M)
241
+ return np.roll(q_wxyz, -1)
242
+
243
+ @staticmethod
244
+ def _s_from_m(m):
245
+ return np.linalg.norm(m[:3,:3].T, axis=1)
246
+
247
+ @staticmethod
248
+ def _r_from_q(q):
249
+ q_wxyz = np.roll(q, 1)
250
+ return transformations.quaternion_matrix(q_wxyz)[:3,:3]
251
+
252
+ @staticmethod
253
+ def _m_from_tqs(t, q, s):
254
+ S = np.eye(4)
255
+ S[:3,:3] = np.diag(s)
256
+
257
+ R = np.eye(4)
258
+ R[:3,:3] = Node._r_from_q(q)
259
+
260
+ T = np.eye(4)
261
+ T[:3,3] = t
262
+
263
+ return T.dot(R.dot(S))
pyrender/pyrender/offscreen.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wrapper for offscreen rendering.
2
+
3
+ Author: Matthew Matl
4
+ """
5
+ import os
6
+
7
+ from .renderer import Renderer
8
+ from .constants import RenderFlags
9
+
10
+
11
+ class OffscreenRenderer(object):
12
+ """A wrapper for offscreen rendering.
13
+
14
+ Parameters
15
+ ----------
16
+ viewport_width : int
17
+ The width of the main viewport, in pixels.
18
+ viewport_height : int
19
+ The height of the main viewport, in pixels.
20
+ point_size : float
21
+ The size of screen-space points in pixels.
22
+ """
23
+
24
+ def __init__(self, viewport_width, viewport_height, point_size=1.0):
25
+ self.viewport_width = viewport_width
26
+ self.viewport_height = viewport_height
27
+ self.point_size = point_size
28
+
29
+ self._platform = None
30
+ self._renderer = None
31
+ self._create()
32
+
33
+ @property
34
+ def viewport_width(self):
35
+ """int : The width of the main viewport, in pixels.
36
+ """
37
+ return self._viewport_width
38
+
39
+ @viewport_width.setter
40
+ def viewport_width(self, value):
41
+ self._viewport_width = int(value)
42
+
43
+ @property
44
+ def viewport_height(self):
45
+ """int : The height of the main viewport, in pixels.
46
+ """
47
+ return self._viewport_height
48
+
49
+ @viewport_height.setter
50
+ def viewport_height(self, value):
51
+ self._viewport_height = int(value)
52
+
53
+ @property
54
+ def point_size(self):
55
+ """float : The pixel size of points in point clouds.
56
+ """
57
+ return self._point_size
58
+
59
+ @point_size.setter
60
+ def point_size(self, value):
61
+ self._point_size = float(value)
62
+
63
+ def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None):
64
+ """Render a scene with the given set of flags.
65
+
66
+ Parameters
67
+ ----------
68
+ scene : :class:`Scene`
69
+ A scene to render.
70
+ flags : int
71
+ A bitwise or of one or more flags from :class:`.RenderFlags`.
72
+ seg_node_map : dict
73
+ A map from :class:`.Node` objects to (3,) colors for each.
74
+ If specified along with flags set to :attr:`.RenderFlags.SEG`,
75
+ the color image will be a segmentation image.
76
+
77
+ Returns
78
+ -------
79
+ color_im : (h, w, 3) uint8 or (h, w, 4) uint8
80
+ The color buffer in RGB format, or in RGBA format if
81
+ :attr:`.RenderFlags.RGBA` is set.
82
+ Not returned if flags includes :attr:`.RenderFlags.DEPTH_ONLY`.
83
+ depth_im : (h, w) float32
84
+ The depth buffer in linear units.
85
+ """
86
+ self._platform.make_current()
87
+ # If platform does not support dynamically-resizing framebuffers,
88
+ # destroy it and restart it
89
+ if (self._platform.viewport_height != self.viewport_height or
90
+ self._platform.viewport_width != self.viewport_width):
91
+ if not self._platform.supports_framebuffers():
92
+ self.delete()
93
+ self._create()
94
+
95
+ self._platform.make_current()
96
+ self._renderer.viewport_width = self.viewport_width
97
+ self._renderer.viewport_height = self.viewport_height
98
+ self._renderer.point_size = self.point_size
99
+
100
+ if self._platform.supports_framebuffers():
101
+ flags |= RenderFlags.OFFSCREEN
102
+ retval = self._renderer.render(scene, flags, seg_node_map)
103
+ else:
104
+ self._renderer.render(scene, flags, seg_node_map)
105
+ depth = self._renderer.read_depth_buf()
106
+ if flags & RenderFlags.DEPTH_ONLY:
107
+ retval = depth
108
+ else:
109
+ color = self._renderer.read_color_buf()
110
+ retval = color, depth
111
+
112
+ # Make the platform not current
113
+ self._platform.make_uncurrent()
114
+ return retval
115
+
116
+ def delete(self):
117
+ """Free all OpenGL resources.
118
+ """
119
+ self._platform.make_current()
120
+ self._renderer.delete()
121
+ self._platform.delete_context()
122
+ del self._renderer
123
+ del self._platform
124
+ self._renderer = None
125
+ self._platform = None
126
+ import gc
127
+ gc.collect()
128
+
129
+ def _create(self):
130
+ if 'PYOPENGL_PLATFORM' not in os.environ:
131
+ from pyrender.platforms.pyglet_platform import PygletPlatform
132
+ self._platform = PygletPlatform(self.viewport_width,
133
+ self.viewport_height)
134
+ elif os.environ['PYOPENGL_PLATFORM'] == 'egl':
135
+ from pyrender.platforms import egl
136
+ device_id = int(os.environ.get('EGL_DEVICE_ID', '0'))
137
+ egl_device = egl.get_device_by_index(device_id)
138
+ self._platform = egl.EGLPlatform(self.viewport_width,
139
+ self.viewport_height,
140
+ device=egl_device)
141
+ elif os.environ['PYOPENGL_PLATFORM'] == 'osmesa':
142
+ from pyrender.platforms.osmesa import OSMesaPlatform
143
+ self._platform = OSMesaPlatform(self.viewport_width,
144
+ self.viewport_height)
145
+ else:
146
+ raise ValueError('Unsupported PyOpenGL platform: {}'.format(
147
+ os.environ['PYOPENGL_PLATFORM']
148
+ ))
149
+ self._platform.init_context()
150
+ self._platform.make_current()
151
+ self._renderer = Renderer(self.viewport_width, self.viewport_height)
152
+
153
+ def __del__(self):
154
+ try:
155
+ self.delete()
156
+ except Exception:
157
+ pass
158
+
159
+
160
+ __all__ = ['OffscreenRenderer']
pyrender/pyrender/platforms/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ """Platforms for generating offscreen OpenGL contexts for rendering.
2
+
3
+ Author: Matthew Matl
4
+ """
5
+
6
+ from .base import Platform
pyrender/pyrender/platforms/base.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+
3
+ import six
4
+
5
+
6
+ @six.add_metaclass(abc.ABCMeta)
7
+ class Platform(object):
8
+ """Base class for all OpenGL platforms.
9
+
10
+ Parameters
11
+ ----------
12
+ viewport_width : int
13
+ The width of the main viewport, in pixels.
14
+ viewport_height : int
15
+ The height of the main viewport, in pixels
16
+ """
17
+
18
+ def __init__(self, viewport_width, viewport_height):
19
+ self.viewport_width = viewport_width
20
+ self.viewport_height = viewport_height
21
+
22
+ @property
23
+ def viewport_width(self):
24
+ """int : The width of the main viewport, in pixels.
25
+ """
26
+ return self._viewport_width
27
+
28
+ @viewport_width.setter
29
+ def viewport_width(self, value):
30
+ self._viewport_width = value
31
+
32
+ @property
33
+ def viewport_height(self):
34
+ """int : The height of the main viewport, in pixels.
35
+ """
36
+ return self._viewport_height
37
+
38
+ @viewport_height.setter
39
+ def viewport_height(self, value):
40
+ self._viewport_height = value
41
+
42
+ @abc.abstractmethod
43
+ def init_context(self):
44
+ """Create an OpenGL context.
45
+ """
46
+ pass
47
+
48
+ @abc.abstractmethod
49
+ def make_current(self):
50
+ """Make the OpenGL context current.
51
+ """
52
+ pass
53
+
54
+ @abc.abstractmethod
55
+ def make_uncurrent(self):
56
+ """Make the OpenGL context uncurrent.
57
+ """
58
+ pass
59
+
60
+ @abc.abstractmethod
61
+ def delete_context(self):
62
+ """Delete the OpenGL context.
63
+ """
64
+ pass
65
+
66
+ @abc.abstractmethod
67
+ def supports_framebuffers(self):
68
+ """Returns True if the method supports framebuffer rendering.
69
+ """
70
+ pass
71
+
72
+ def __del__(self):
73
+ try:
74
+ self.delete_context()
75
+ except Exception:
76
+ pass
pyrender/pyrender/platforms/egl.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import os
3
+
4
+ import OpenGL.platform
5
+
6
+ from .base import Platform
7
+
8
+ EGL_PLATFORM_DEVICE_EXT = 0x313F
9
+ EGL_DRM_DEVICE_FILE_EXT = 0x3233
10
+
11
+
12
+ def _ensure_egl_loaded():
13
+ plugin = OpenGL.platform.PlatformPlugin.by_name('egl')
14
+ if plugin is None:
15
+ raise RuntimeError("EGL platform plugin is not available.")
16
+
17
+ plugin_class = plugin.load()
18
+ plugin.loaded = True
19
+ # create instance of this platform implementation
20
+ plugin = plugin_class()
21
+
22
+ plugin.install(vars(OpenGL.platform))
23
+
24
+
25
+ _ensure_egl_loaded()
26
+ from OpenGL import EGL as egl
27
+
28
+
29
+ def _get_egl_func(func_name, res_type, *arg_types):
30
+ address = egl.eglGetProcAddress(func_name)
31
+ if address is None:
32
+ return None
33
+
34
+ proto = ctypes.CFUNCTYPE(res_type)
35
+ proto.argtypes = arg_types
36
+ func = proto(address)
37
+ return func
38
+
39
+
40
+ def _get_egl_struct(struct_name):
41
+ from OpenGL._opaque import opaque_pointer_cls
42
+ return opaque_pointer_cls(struct_name)
43
+
44
+
45
+ # These are not defined in PyOpenGL by default.
46
+ _EGLDeviceEXT = _get_egl_struct('EGLDeviceEXT')
47
+ _eglGetPlatformDisplayEXT = _get_egl_func('eglGetPlatformDisplayEXT', egl.EGLDisplay)
48
+ _eglQueryDevicesEXT = _get_egl_func('eglQueryDevicesEXT', egl.EGLBoolean)
49
+ _eglQueryDeviceStringEXT = _get_egl_func('eglQueryDeviceStringEXT', ctypes.c_char_p)
50
+
51
+
52
+ def query_devices():
53
+ if _eglQueryDevicesEXT is None:
54
+ raise RuntimeError("EGL query extension is not loaded or is not supported.")
55
+
56
+ num_devices = egl.EGLint()
57
+ success = _eglQueryDevicesEXT(0, None, ctypes.pointer(num_devices))
58
+ if not success or num_devices.value < 1:
59
+ return []
60
+
61
+ devices = (_EGLDeviceEXT * num_devices.value)() # array of size num_devices
62
+ success = _eglQueryDevicesEXT(num_devices.value, devices, ctypes.pointer(num_devices))
63
+ if not success or num_devices.value < 1:
64
+ return []
65
+
66
+ return [EGLDevice(devices[i]) for i in range(num_devices.value)]
67
+
68
+
69
+ def get_default_device():
70
+ # Fall back to not using query extension.
71
+ if _eglQueryDevicesEXT is None:
72
+ return EGLDevice(None)
73
+
74
+ return query_devices()[0]
75
+
76
+
77
+ def get_device_by_index(device_id):
78
+ if _eglQueryDevicesEXT is None and device_id == 0:
79
+ return get_default_device()
80
+
81
+ devices = query_devices()
82
+ if device_id >= len(devices):
83
+ raise ValueError('Invalid device ID ({})'.format(device_id, len(devices)))
84
+ return devices[device_id]
85
+
86
+
87
+ class EGLDevice:
88
+
89
+ def __init__(self, display=None):
90
+ self._display = display
91
+
92
+ def get_display(self):
93
+ if self._display is None:
94
+ return egl.eglGetDisplay(egl.EGL_DEFAULT_DISPLAY)
95
+
96
+ return _eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, self._display, None)
97
+
98
+ @property
99
+ def name(self):
100
+ if self._display is None:
101
+ return 'default'
102
+
103
+ name = _eglQueryDeviceStringEXT(self._display, EGL_DRM_DEVICE_FILE_EXT)
104
+ if name is None:
105
+ return None
106
+
107
+ return name.decode('ascii')
108
+
109
+ def __repr__(self):
110
+ return "<EGLDevice(name={})>".format(self.name)
111
+
112
+
113
+ class EGLPlatform(Platform):
114
+ """Renders using EGL.
115
+ """
116
+
117
+ def __init__(self, viewport_width, viewport_height, device: EGLDevice = None):
118
+ super(EGLPlatform, self).__init__(viewport_width, viewport_height)
119
+ if device is None:
120
+ device = get_default_device()
121
+
122
+ self._egl_device = device
123
+ self._egl_display = None
124
+ self._egl_context = None
125
+
126
+ def init_context(self):
127
+ _ensure_egl_loaded()
128
+
129
+ from OpenGL.EGL import (
130
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE,
131
+ EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_DEPTH_SIZE,
132
+ EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
133
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_CONFORMANT,
134
+ EGL_NONE, EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT,
135
+ EGL_OPENGL_API, EGL_CONTEXT_MAJOR_VERSION,
136
+ EGL_CONTEXT_MINOR_VERSION,
137
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
138
+ EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
139
+ eglGetDisplay, eglInitialize, eglChooseConfig,
140
+ eglBindAPI, eglCreateContext, EGLConfig
141
+ )
142
+ from OpenGL import arrays
143
+
144
+ config_attributes = arrays.GLintArray.asArray([
145
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
146
+ EGL_BLUE_SIZE, 8,
147
+ EGL_RED_SIZE, 8,
148
+ EGL_GREEN_SIZE, 8,
149
+ EGL_DEPTH_SIZE, 24,
150
+ EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
151
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
152
+ EGL_CONFORMANT, EGL_OPENGL_BIT,
153
+ EGL_NONE
154
+ ])
155
+ context_attributes = arrays.GLintArray.asArray([
156
+ EGL_CONTEXT_MAJOR_VERSION, 4,
157
+ EGL_CONTEXT_MINOR_VERSION, 1,
158
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
159
+ EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
160
+ EGL_NONE
161
+ ])
162
+ major, minor = ctypes.c_long(), ctypes.c_long()
163
+ num_configs = ctypes.c_long()
164
+ configs = (EGLConfig * 1)()
165
+
166
+ # Cache DISPLAY if necessary and get an off-screen EGL display
167
+ orig_dpy = None
168
+ if 'DISPLAY' in os.environ:
169
+ orig_dpy = os.environ['DISPLAY']
170
+ del os.environ['DISPLAY']
171
+
172
+ self._egl_display = self._egl_device.get_display()
173
+ if orig_dpy is not None:
174
+ os.environ['DISPLAY'] = orig_dpy
175
+
176
+ # Initialize EGL
177
+ assert eglInitialize(self._egl_display, major, minor)
178
+ assert eglChooseConfig(
179
+ self._egl_display, config_attributes, configs, 1, num_configs
180
+ )
181
+
182
+ # Bind EGL to the OpenGL API
183
+ assert eglBindAPI(EGL_OPENGL_API)
184
+
185
+ # Create an EGL context
186
+ self._egl_context = eglCreateContext(
187
+ self._egl_display, configs[0],
188
+ EGL_NO_CONTEXT, context_attributes
189
+ )
190
+
191
+ # Make it current
192
+ self.make_current()
193
+
194
+ def make_current(self):
195
+ from OpenGL.EGL import eglMakeCurrent, EGL_NO_SURFACE
196
+ assert eglMakeCurrent(
197
+ self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
198
+ self._egl_context
199
+ )
200
+
201
+ def make_uncurrent(self):
202
+ """Make the OpenGL context uncurrent.
203
+ """
204
+ pass
205
+
206
+ def delete_context(self):
207
+ from OpenGL.EGL import eglDestroyContext, eglTerminate
208
+ if self._egl_display is not None:
209
+ if self._egl_context is not None:
210
+ eglDestroyContext(self._egl_display, self._egl_context)
211
+ self._egl_context = None
212
+ eglTerminate(self._egl_display)
213
+ self._egl_display = None
214
+
215
+ def supports_framebuffers(self):
216
+ return True
217
+
218
+
219
+ __all__ = ['EGLPlatform']
pyrender/pyrender/platforms/osmesa.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import Platform
2
+
3
+
4
+ __all__ = ['OSMesaPlatform']
5
+
6
+
7
+ class OSMesaPlatform(Platform):
8
+ """Renders into a software buffer using OSMesa. Requires special versions
9
+ of OSMesa to be installed, plus PyOpenGL upgrade.
10
+ """
11
+
12
+ def __init__(self, viewport_width, viewport_height):
13
+ super(OSMesaPlatform, self).__init__(viewport_width, viewport_height)
14
+ self._context = None
15
+ self._buffer = None
16
+
17
+ def init_context(self):
18
+ from OpenGL import arrays
19
+ from OpenGL.osmesa import (
20
+ OSMesaCreateContextAttribs, OSMESA_FORMAT,
21
+ OSMESA_RGBA, OSMESA_PROFILE, OSMESA_CORE_PROFILE,
22
+ OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION,
23
+ OSMESA_DEPTH_BITS
24
+ )
25
+
26
+ attrs = arrays.GLintArray.asArray([
27
+ OSMESA_FORMAT, OSMESA_RGBA,
28
+ OSMESA_DEPTH_BITS, 24,
29
+ OSMESA_PROFILE, OSMESA_CORE_PROFILE,
30
+ OSMESA_CONTEXT_MAJOR_VERSION, 3,
31
+ OSMESA_CONTEXT_MINOR_VERSION, 3,
32
+ 0
33
+ ])
34
+ self._context = OSMesaCreateContextAttribs(attrs, None)
35
+ self._buffer = arrays.GLubyteArray.zeros(
36
+ (self.viewport_height, self.viewport_width, 4)
37
+ )
38
+
39
+ def make_current(self):
40
+ from OpenGL import GL as gl
41
+ from OpenGL.osmesa import OSMesaMakeCurrent
42
+ assert(OSMesaMakeCurrent(
43
+ self._context, self._buffer, gl.GL_UNSIGNED_BYTE,
44
+ self.viewport_width, self.viewport_height
45
+ ))
46
+
47
+ def make_uncurrent(self):
48
+ """Make the OpenGL context uncurrent.
49
+ """
50
+ pass
51
+
52
+ def delete_context(self):
53
+ from OpenGL.osmesa import OSMesaDestroyContext
54
+ OSMesaDestroyContext(self._context)
55
+ self._context = None
56
+ self._buffer = None
57
+
58
+ def supports_framebuffers(self):
59
+ return False
pyrender/pyrender/platforms/pyglet_platform.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrender.constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR,
2
+ MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR)
3
+ from .base import Platform
4
+
5
+ import OpenGL
6
+
7
+
8
+ __all__ = ['PygletPlatform']
9
+
10
+
11
+ class PygletPlatform(Platform):
12
+ """Renders on-screen using a 1x1 hidden Pyglet window for getting
13
+ an OpenGL context.
14
+ """
15
+
16
+ def __init__(self, viewport_width, viewport_height):
17
+ super(PygletPlatform, self).__init__(viewport_width, viewport_height)
18
+ self._window = None
19
+
20
+ def init_context(self):
21
+ import pyglet
22
+ pyglet.options['shadow_window'] = False
23
+
24
+ try:
25
+ pyglet.lib.x11.xlib.XInitThreads()
26
+ except Exception:
27
+ pass
28
+
29
+ self._window = None
30
+ confs = [pyglet.gl.Config(sample_buffers=1, samples=4,
31
+ depth_size=24,
32
+ double_buffer=True,
33
+ major_version=TARGET_OPEN_GL_MAJOR,
34
+ minor_version=TARGET_OPEN_GL_MINOR),
35
+ pyglet.gl.Config(depth_size=24,
36
+ double_buffer=True,
37
+ major_version=TARGET_OPEN_GL_MAJOR,
38
+ minor_version=TARGET_OPEN_GL_MINOR),
39
+ pyglet.gl.Config(sample_buffers=1, samples=4,
40
+ depth_size=24,
41
+ double_buffer=True,
42
+ major_version=MIN_OPEN_GL_MAJOR,
43
+ minor_version=MIN_OPEN_GL_MINOR),
44
+ pyglet.gl.Config(depth_size=24,
45
+ double_buffer=True,
46
+ major_version=MIN_OPEN_GL_MAJOR,
47
+ minor_version=MIN_OPEN_GL_MINOR)]
48
+ for conf in confs:
49
+ try:
50
+ self._window = pyglet.window.Window(config=conf, visible=False,
51
+ resizable=False,
52
+ width=1, height=1)
53
+ break
54
+ except pyglet.window.NoSuchConfigException as e:
55
+ pass
56
+
57
+ if not self._window:
58
+ raise ValueError(
59
+ 'Failed to initialize Pyglet window with an OpenGL >= 3+ '
60
+ 'context. If you\'re logged in via SSH, ensure that you\'re '
61
+ 'running your script with vglrun (i.e. VirtualGL). The '
62
+ 'internal error message was "{}"'.format(e)
63
+ )
64
+
65
+ def make_current(self):
66
+ if self._window:
67
+ self._window.switch_to()
68
+
69
+ def make_uncurrent(self):
70
+ try:
71
+ import pyglet
72
+ pyglet.gl.xlib.glx.glXMakeContextCurrent(self._window.context.x_display, 0, 0, None)
73
+ except Exception:
74
+ pass
75
+
76
+ def delete_context(self):
77
+ if self._window is not None:
78
+ self.make_current()
79
+ cid = OpenGL.contextdata.getContext()
80
+ try:
81
+ self._window.context.destroy()
82
+ self._window.close()
83
+ except Exception:
84
+ pass
85
+ self._window = None
86
+ OpenGL.contextdata.cleanupContext(cid)
87
+ del cid
88
+
89
+ def supports_framebuffers(self):
90
+ return True
pyrender/pyrender/primitive.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Primitives, conforming to the glTF 2.0 standards as specified in
2
+ https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-primitive
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import numpy as np
7
+
8
+ from OpenGL.GL import *
9
+
10
+ from .material import Material, MetallicRoughnessMaterial
11
+ from .constants import FLOAT_SZ, UINT_SZ, BufFlags, GLTF
12
+ from .utils import format_color_array
13
+
14
+
15
+ class Primitive(object):
16
+ """A primitive object which can be rendered.
17
+
18
+ Parameters
19
+ ----------
20
+ positions : (n, 3) float
21
+ XYZ vertex positions.
22
+ normals : (n, 3) float
23
+ Normalized XYZ vertex normals.
24
+ tangents : (n, 4) float
25
+ XYZW vertex tangents where the w component is a sign value
26
+ (either +1 or -1) indicating the handedness of the tangent basis.
27
+ texcoord_0 : (n, 2) float
28
+ The first set of UV texture coordinates.
29
+ texcoord_1 : (n, 2) float
30
+ The second set of UV texture coordinates.
31
+ color_0 : (n, 4) float
32
+ RGBA vertex colors.
33
+ joints_0 : (n, 4) float
34
+ Joint information.
35
+ weights_0 : (n, 4) float
36
+ Weight information for morphing.
37
+ indices : (m, 3) int
38
+ Face indices for triangle meshes or fans.
39
+ material : :class:`Material`
40
+ The material to apply to this primitive when rendering.
41
+ mode : int
42
+ The type of primitives to render, one of the following:
43
+
44
+ - ``0``: POINTS
45
+ - ``1``: LINES
46
+ - ``2``: LINE_LOOP
47
+ - ``3``: LINE_STRIP
48
+ - ``4``: TRIANGLES
49
+ - ``5``: TRIANGLES_STRIP
50
+ - ``6``: TRIANGLES_FAN
51
+ targets : (k,) int
52
+ Morph target indices.
53
+ poses : (x,4,4), float
54
+ Array of 4x4 transformation matrices for instancing this object.
55
+ """
56
+
57
+ def __init__(self,
58
+ positions,
59
+ normals=None,
60
+ tangents=None,
61
+ texcoord_0=None,
62
+ texcoord_1=None,
63
+ color_0=None,
64
+ joints_0=None,
65
+ weights_0=None,
66
+ indices=None,
67
+ material=None,
68
+ mode=None,
69
+ targets=None,
70
+ poses=None):
71
+
72
+ if mode is None:
73
+ mode = GLTF.TRIANGLES
74
+
75
+ self.positions = positions
76
+ self.normals = normals
77
+ self.tangents = tangents
78
+ self.texcoord_0 = texcoord_0
79
+ self.texcoord_1 = texcoord_1
80
+ self.color_0 = color_0
81
+ self.joints_0 = joints_0
82
+ self.weights_0 = weights_0
83
+ self.indices = indices
84
+ self.material = material
85
+ self.mode = mode
86
+ self.targets = targets
87
+ self.poses = poses
88
+
89
+ self._bounds = None
90
+ self._vaid = None
91
+ self._buffers = []
92
+ self._is_transparent = None
93
+ self._buf_flags = None
94
+
95
+ @property
96
+ def positions(self):
97
+ """(n,3) float : XYZ vertex positions.
98
+ """
99
+ return self._positions
100
+
101
+ @positions.setter
102
+ def positions(self, value):
103
+ value = np.asanyarray(value, dtype=np.float32)
104
+ self._positions = np.ascontiguousarray(value)
105
+ self._bounds = None
106
+
107
+ @property
108
+ def normals(self):
109
+ """(n,3) float : Normalized XYZ vertex normals.
110
+ """
111
+ return self._normals
112
+
113
+ @normals.setter
114
+ def normals(self, value):
115
+ if value is not None:
116
+ value = np.asanyarray(value, dtype=np.float32)
117
+ value = np.ascontiguousarray(value)
118
+ if value.shape != self.positions.shape:
119
+ raise ValueError('Incorrect normals shape')
120
+ self._normals = value
121
+
122
+ @property
123
+ def tangents(self):
124
+ """(n,4) float : XYZW vertex tangents.
125
+ """
126
+ return self._tangents
127
+
128
+ @tangents.setter
129
+ def tangents(self, value):
130
+ if value is not None:
131
+ value = np.asanyarray(value, dtype=np.float32)
132
+ value = np.ascontiguousarray(value)
133
+ if value.shape != (self.positions.shape[0], 4):
134
+ raise ValueError('Incorrect tangent shape')
135
+ self._tangents = value
136
+
137
+ @property
138
+ def texcoord_0(self):
139
+ """(n,2) float : The first set of UV texture coordinates.
140
+ """
141
+ return self._texcoord_0
142
+
143
+ @texcoord_0.setter
144
+ def texcoord_0(self, value):
145
+ if value is not None:
146
+ value = np.asanyarray(value, dtype=np.float32)
147
+ value = np.ascontiguousarray(value)
148
+ if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or
149
+ value.shape[1] < 2):
150
+ raise ValueError('Incorrect texture coordinate shape')
151
+ if value.shape[1] > 2:
152
+ value = value[:,:2]
153
+ self._texcoord_0 = value
154
+
155
+ @property
156
+ def texcoord_1(self):
157
+ """(n,2) float : The second set of UV texture coordinates.
158
+ """
159
+ return self._texcoord_1
160
+
161
+ @texcoord_1.setter
162
+ def texcoord_1(self, value):
163
+ if value is not None:
164
+ value = np.asanyarray(value, dtype=np.float32)
165
+ value = np.ascontiguousarray(value)
166
+ if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or
167
+ value.shape[1] != 2):
168
+ raise ValueError('Incorrect texture coordinate shape')
169
+ self._texcoord_1 = value
170
+
171
+ @property
172
+ def color_0(self):
173
+ """(n,4) float : RGBA vertex colors.
174
+ """
175
+ return self._color_0
176
+
177
+ @color_0.setter
178
+ def color_0(self, value):
179
+ if value is not None:
180
+ value = np.ascontiguousarray(
181
+ format_color_array(value, shape=(len(self.positions), 4))
182
+ )
183
+ self._is_transparent = None
184
+ self._color_0 = value
185
+
186
+ @property
187
+ def joints_0(self):
188
+ """(n,4) float : Joint information.
189
+ """
190
+ return self._joints_0
191
+
192
+ @joints_0.setter
193
+ def joints_0(self, value):
194
+ self._joints_0 = value
195
+
196
+ @property
197
+ def weights_0(self):
198
+ """(n,4) float : Weight information for morphing.
199
+ """
200
+ return self._weights_0
201
+
202
+ @weights_0.setter
203
+ def weights_0(self, value):
204
+ self._weights_0 = value
205
+
206
+ @property
207
+ def indices(self):
208
+ """(m,3) int : Face indices for triangle meshes or fans.
209
+ """
210
+ return self._indices
211
+
212
+ @indices.setter
213
+ def indices(self, value):
214
+ if value is not None:
215
+ value = np.asanyarray(value, dtype=np.float32)
216
+ value = np.ascontiguousarray(value)
217
+ self._indices = value
218
+
219
+ @property
220
+ def material(self):
221
+ """:class:`Material` : The material for this primitive.
222
+ """
223
+ return self._material
224
+
225
+ @material.setter
226
+ def material(self, value):
227
+ # Create default material
228
+ if value is None:
229
+ value = MetallicRoughnessMaterial()
230
+ else:
231
+ if not isinstance(value, Material):
232
+ raise TypeError('Object material must be of type Material')
233
+ self._material = value
234
+
235
+ @property
236
+ def mode(self):
237
+ """int : The type of primitive to render.
238
+ """
239
+ return self._mode
240
+
241
+ @mode.setter
242
+ def mode(self, value):
243
+ value = int(value)
244
+ if value < GLTF.POINTS or value > GLTF.TRIANGLE_FAN:
245
+ raise ValueError('Invalid mode')
246
+ self._mode = value
247
+
248
+ @property
249
+ def targets(self):
250
+ """(k,) int : Morph target indices.
251
+ """
252
+ return self._targets
253
+
254
+ @targets.setter
255
+ def targets(self, value):
256
+ self._targets = value
257
+
258
+ @property
259
+ def poses(self):
260
+ """(x,4,4) float : Homogenous transforms for instancing this primitive.
261
+ """
262
+ return self._poses
263
+
264
+ @poses.setter
265
+ def poses(self, value):
266
+ if value is not None:
267
+ value = np.asanyarray(value, dtype=np.float32)
268
+ value = np.ascontiguousarray(value)
269
+ if value.ndim == 2:
270
+ value = value[np.newaxis,:,:]
271
+ if value.shape[1] != 4 or value.shape[2] != 4:
272
+ raise ValueError('Pose matrices must be of shape (n,4,4), '
273
+ 'got {}'.format(value.shape))
274
+ self._poses = value
275
+ self._bounds = None
276
+
277
+ @property
278
+ def bounds(self):
279
+ if self._bounds is None:
280
+ self._bounds = self._compute_bounds()
281
+ return self._bounds
282
+
283
+ @property
284
+ def centroid(self):
285
+ """(3,) float : The centroid of the primitive's AABB.
286
+ """
287
+ return np.mean(self.bounds, axis=0)
288
+
289
+ @property
290
+ def extents(self):
291
+ """(3,) float : The lengths of the axes of the primitive's AABB.
292
+ """
293
+ return np.diff(self.bounds, axis=0).reshape(-1)
294
+
295
+ @property
296
+ def scale(self):
297
+ """(3,) float : The length of the diagonal of the primitive's AABB.
298
+ """
299
+ return np.linalg.norm(self.extents)
300
+
301
+ @property
302
+ def buf_flags(self):
303
+ """int : The flags for the render buffer.
304
+ """
305
+ if self._buf_flags is None:
306
+ self._buf_flags = self._compute_buf_flags()
307
+ return self._buf_flags
308
+
309
+ def delete(self):
310
+ self._unbind()
311
+ self._remove_from_context()
312
+
313
+ @property
314
+ def is_transparent(self):
315
+ """bool : If True, the mesh is partially-transparent.
316
+ """
317
+ return self._compute_transparency()
318
+
319
+ def _add_to_context(self):
320
+ if self._vaid is not None:
321
+ raise ValueError('Mesh is already bound to a context')
322
+
323
+ # Generate and bind VAO
324
+ self._vaid = glGenVertexArrays(1)
325
+ glBindVertexArray(self._vaid)
326
+
327
+ #######################################################################
328
+ # Fill vertex buffer
329
+ #######################################################################
330
+
331
+ # Generate and bind vertex buffer
332
+ vertexbuffer = glGenBuffers(1)
333
+ self._buffers.append(vertexbuffer)
334
+ glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer)
335
+
336
+ # positions
337
+ vertex_data = self.positions
338
+ attr_sizes = [3]
339
+
340
+ # Normals
341
+ if self.normals is not None:
342
+ vertex_data = np.hstack((vertex_data, self.normals))
343
+ attr_sizes.append(3)
344
+
345
+ # Tangents
346
+ if self.tangents is not None:
347
+ vertex_data = np.hstack((vertex_data, self.tangents))
348
+ attr_sizes.append(4)
349
+
350
+ # Texture Coordinates
351
+ if self.texcoord_0 is not None:
352
+ vertex_data = np.hstack((vertex_data, self.texcoord_0))
353
+ attr_sizes.append(2)
354
+ if self.texcoord_1 is not None:
355
+ vertex_data = np.hstack((vertex_data, self.texcoord_1))
356
+ attr_sizes.append(2)
357
+
358
+ # Color
359
+ if self.color_0 is not None:
360
+ vertex_data = np.hstack((vertex_data, self.color_0))
361
+ attr_sizes.append(4)
362
+
363
+ # TODO JOINTS AND WEIGHTS
364
+ # PASS
365
+
366
+ # Copy data to buffer
367
+ vertex_data = np.ascontiguousarray(
368
+ vertex_data.flatten().astype(np.float32)
369
+ )
370
+ glBufferData(
371
+ GL_ARRAY_BUFFER, FLOAT_SZ * len(vertex_data),
372
+ vertex_data, GL_STATIC_DRAW
373
+ )
374
+ total_sz = sum(attr_sizes)
375
+ offset = 0
376
+ for i, sz in enumerate(attr_sizes):
377
+ glVertexAttribPointer(
378
+ i, sz, GL_FLOAT, GL_FALSE, FLOAT_SZ * total_sz,
379
+ ctypes.c_void_p(FLOAT_SZ * offset)
380
+ )
381
+ glEnableVertexAttribArray(i)
382
+ offset += sz
383
+
384
+ #######################################################################
385
+ # Fill model matrix buffer
386
+ #######################################################################
387
+
388
+ if self.poses is not None:
389
+ pose_data = np.ascontiguousarray(
390
+ np.transpose(self.poses, [0,2,1]).flatten().astype(np.float32)
391
+ )
392
+ else:
393
+ pose_data = np.ascontiguousarray(
394
+ np.eye(4).flatten().astype(np.float32)
395
+ )
396
+
397
+ modelbuffer = glGenBuffers(1)
398
+ self._buffers.append(modelbuffer)
399
+ glBindBuffer(GL_ARRAY_BUFFER, modelbuffer)
400
+ glBufferData(
401
+ GL_ARRAY_BUFFER, FLOAT_SZ * len(pose_data),
402
+ pose_data, GL_STATIC_DRAW
403
+ )
404
+
405
+ for i in range(0, 4):
406
+ idx = i + len(attr_sizes)
407
+ glEnableVertexAttribArray(idx)
408
+ glVertexAttribPointer(
409
+ idx, 4, GL_FLOAT, GL_FALSE, FLOAT_SZ * 4 * 4,
410
+ ctypes.c_void_p(4 * FLOAT_SZ * i)
411
+ )
412
+ glVertexAttribDivisor(idx, 1)
413
+
414
+ #######################################################################
415
+ # Fill element buffer
416
+ #######################################################################
417
+ if self.indices is not None:
418
+ elementbuffer = glGenBuffers(1)
419
+ self._buffers.append(elementbuffer)
420
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer)
421
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, UINT_SZ * self.indices.size,
422
+ self.indices.flatten().astype(np.uint32),
423
+ GL_STATIC_DRAW)
424
+
425
+ glBindVertexArray(0)
426
+
427
+ def _remove_from_context(self):
428
+ if self._vaid is not None:
429
+ glDeleteVertexArrays(1, [self._vaid])
430
+ glDeleteBuffers(len(self._buffers), self._buffers)
431
+ self._vaid = None
432
+ self._buffers = []
433
+
434
+ def _in_context(self):
435
+ return self._vaid is not None
436
+
437
+ def _bind(self):
438
+ if self._vaid is None:
439
+ raise ValueError('Cannot bind a Mesh that has not been added '
440
+ 'to a context')
441
+ glBindVertexArray(self._vaid)
442
+
443
+ def _unbind(self):
444
+ glBindVertexArray(0)
445
+
446
+ def _compute_bounds(self):
447
+ """Compute the bounds of this object.
448
+ """
449
+ # Compute bounds of this object
450
+ bounds = np.array([np.min(self.positions, axis=0),
451
+ np.max(self.positions, axis=0)])
452
+
453
+ # If instanced, compute translations for approximate bounds
454
+ if self.poses is not None:
455
+ bounds += np.array([np.min(self.poses[:,:3,3], axis=0),
456
+ np.max(self.poses[:,:3,3], axis=0)])
457
+ return bounds
458
+
459
+ def _compute_transparency(self):
460
+ """Compute whether or not this object is transparent.
461
+ """
462
+ if self.material.is_transparent:
463
+ return True
464
+ if self._is_transparent is None:
465
+ self._is_transparent = False
466
+ if self.color_0 is not None:
467
+ if np.any(self._color_0[:,3] != 1.0):
468
+ self._is_transparent = True
469
+ return self._is_transparent
470
+
471
+ def _compute_buf_flags(self):
472
+ buf_flags = BufFlags.POSITION
473
+
474
+ if self.normals is not None:
475
+ buf_flags |= BufFlags.NORMAL
476
+ if self.tangents is not None:
477
+ buf_flags |= BufFlags.TANGENT
478
+ if self.texcoord_0 is not None:
479
+ buf_flags |= BufFlags.TEXCOORD_0
480
+ if self.texcoord_1 is not None:
481
+ buf_flags |= BufFlags.TEXCOORD_1
482
+ if self.color_0 is not None:
483
+ buf_flags |= BufFlags.COLOR_0
484
+ if self.joints_0 is not None:
485
+ buf_flags |= BufFlags.JOINTS_0
486
+ if self.weights_0 is not None:
487
+ buf_flags |= BufFlags.WEIGHTS_0
488
+
489
+ return buf_flags
pyrender/pyrender/renderer.py ADDED
@@ -0,0 +1,1339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PBR renderer for Python.
2
+
3
+ Author: Matthew Matl
4
+ """
5
+ import sys
6
+
7
+ import numpy as np
8
+ import PIL
9
+
10
+ from .constants import (RenderFlags, TextAlign, GLTF, BufFlags, TexFlags,
11
+ ProgramFlags, DEFAULT_Z_FAR, DEFAULT_Z_NEAR,
12
+ SHADOW_TEX_SZ, MAX_N_LIGHTS)
13
+ from .shader_program import ShaderProgramCache
14
+ from .material import MetallicRoughnessMaterial, SpecularGlossinessMaterial
15
+ from .light import PointLight, SpotLight, DirectionalLight
16
+ from .font import FontCache
17
+ from .utils import format_color_vector
18
+
19
+ from OpenGL.GL import *
20
+
21
+
22
+ class Renderer(object):
23
+ """Class for handling all rendering operations on a scene.
24
+
25
+ Note
26
+ ----
27
+ This renderer relies on the existence of an OpenGL context and
28
+ does not create one on its own.
29
+
30
+ Parameters
31
+ ----------
32
+ viewport_width : int
33
+ Width of the viewport in pixels.
34
+ viewport_height : int
35
+ Width of the viewport height in pixels.
36
+ point_size : float, optional
37
+ Size of points in pixels. Defaults to 1.0.
38
+ """
39
+
40
+ def __init__(self, viewport_width, viewport_height, point_size=1.0):
41
+ self.dpscale = 1
42
+ # Scaling needed on retina displays
43
+ if sys.platform == 'darwin':
44
+ self.dpscale = 2
45
+
46
+ self.viewport_width = viewport_width
47
+ self.viewport_height = viewport_height
48
+ self.point_size = point_size
49
+
50
+ # Optional framebuffer for offscreen renders
51
+ self._main_fb = None
52
+ self._main_cb = None
53
+ self._main_db = None
54
+ self._main_fb_ms = None
55
+ self._main_cb_ms = None
56
+ self._main_db_ms = None
57
+ self._main_fb_dims = (None, None)
58
+ self._shadow_fb = None
59
+ self._latest_znear = DEFAULT_Z_NEAR
60
+ self._latest_zfar = DEFAULT_Z_FAR
61
+
62
+ # Shader Program Cache
63
+ self._program_cache = ShaderProgramCache()
64
+ self._font_cache = FontCache()
65
+ self._meshes = set()
66
+ self._mesh_textures = set()
67
+ self._shadow_textures = set()
68
+ self._texture_alloc_idx = 0
69
+
70
+ @property
71
+ def viewport_width(self):
72
+ """int : The width of the main viewport, in pixels.
73
+ """
74
+ return self._viewport_width
75
+
76
+ @viewport_width.setter
77
+ def viewport_width(self, value):
78
+ self._viewport_width = self.dpscale * value
79
+
80
+ @property
81
+ def viewport_height(self):
82
+ """int : The height of the main viewport, in pixels.
83
+ """
84
+ return self._viewport_height
85
+
86
+ @viewport_height.setter
87
+ def viewport_height(self, value):
88
+ self._viewport_height = self.dpscale * value
89
+
90
+ @property
91
+ def point_size(self):
92
+ """float : The size of screen-space points, in pixels.
93
+ """
94
+ return self._point_size
95
+
96
+ @point_size.setter
97
+ def point_size(self, value):
98
+ self._point_size = float(value)
99
+
100
+ def render(self, scene, flags, seg_node_map=None):
101
+ """Render a scene with the given set of flags.
102
+
103
+ Parameters
104
+ ----------
105
+ scene : :class:`Scene`
106
+ A scene to render.
107
+ flags : int
108
+ A specification from :class:`.RenderFlags`.
109
+ seg_node_map : dict
110
+ A map from :class:`.Node` objects to (3,) colors for each.
111
+ If specified along with flags set to :attr:`.RenderFlags.SEG`,
112
+ the color image will be a segmentation image.
113
+
114
+ Returns
115
+ -------
116
+ color_im : (h, w, 3) uint8 or (h, w, 4) uint8
117
+ If :attr:`RenderFlags.OFFSCREEN` is set, the color buffer. This is
118
+ normally an RGB buffer, but if :attr:`.RenderFlags.RGBA` is set,
119
+ the buffer will be a full RGBA buffer.
120
+ depth_im : (h, w) float32
121
+ If :attr:`RenderFlags.OFFSCREEN` is set, the depth buffer
122
+ in linear units.
123
+ """
124
+ # Update context with meshes and textures
125
+ self._update_context(scene, flags)
126
+
127
+ # Render necessary shadow maps
128
+ if not bool(flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.SEG):
129
+ for ln in scene.light_nodes:
130
+ take_pass = False
131
+ if (isinstance(ln.light, DirectionalLight) and
132
+ bool(flags & RenderFlags.SHADOWS_DIRECTIONAL)):
133
+ take_pass = True
134
+ elif (isinstance(ln.light, SpotLight) and
135
+ bool(flags & RenderFlags.SHADOWS_SPOT)):
136
+ take_pass = True
137
+ elif (isinstance(ln.light, PointLight) and
138
+ bool(flags & RenderFlags.SHADOWS_POINT)):
139
+ take_pass = True
140
+ if take_pass:
141
+ self._shadow_mapping_pass(scene, ln, flags)
142
+
143
+ # Make forward pass
144
+ retval = self._forward_pass(scene, flags, seg_node_map=seg_node_map)
145
+
146
+ # If necessary, make normals pass
147
+ if flags & (RenderFlags.VERTEX_NORMALS | RenderFlags.FACE_NORMALS):
148
+ self._normals_pass(scene, flags)
149
+
150
+ # Update camera settings for retrieving depth buffers
151
+ self._latest_znear = scene.main_camera_node.camera.znear
152
+ self._latest_zfar = scene.main_camera_node.camera.zfar
153
+
154
+ return retval
155
+
156
+ def render_text(self, text, x, y, font_name='OpenSans-Regular',
157
+ font_pt=40, color=None, scale=1.0,
158
+ align=TextAlign.BOTTOM_LEFT):
159
+ """Render text into the current viewport.
160
+
161
+ Note
162
+ ----
163
+ This cannot be done into an offscreen buffer.
164
+
165
+ Parameters
166
+ ----------
167
+ text : str
168
+ The text to render.
169
+ x : int
170
+ Horizontal pixel location of text.
171
+ y : int
172
+ Vertical pixel location of text.
173
+ font_name : str
174
+ Name of font, from the ``pyrender/fonts`` folder, or
175
+ a path to a ``.ttf`` file.
176
+ font_pt : int
177
+ Height of the text, in font points.
178
+ color : (4,) float
179
+ The color of the text. Default is black.
180
+ scale : int
181
+ Scaling factor for text.
182
+ align : int
183
+ One of the :class:`TextAlign` options which specifies where the
184
+ ``x`` and ``y`` parameters lie on the text. For example,
185
+ :attr:`TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate
186
+ the position of the bottom-left corner of the textbox.
187
+ """
188
+ x *= self.dpscale
189
+ y *= self.dpscale
190
+ font_pt *= self.dpscale
191
+
192
+ if color is None:
193
+ color = np.array([0.0, 0.0, 0.0, 1.0])
194
+ else:
195
+ color = format_color_vector(color, 4)
196
+
197
+ # Set up viewport for render
198
+ self._configure_forward_pass_viewport(0)
199
+
200
+ # Load font
201
+ font = self._font_cache.get_font(font_name, font_pt)
202
+ if not font._in_context():
203
+ font._add_to_context()
204
+
205
+ # Load program
206
+ program = self._get_text_program()
207
+ program._bind()
208
+
209
+ # Set uniforms
210
+ p = np.eye(4)
211
+ p[0,0] = 2.0 / self.viewport_width
212
+ p[0,3] = -1.0
213
+ p[1,1] = 2.0 / self.viewport_height
214
+ p[1,3] = -1.0
215
+ program.set_uniform('projection', p)
216
+ program.set_uniform('text_color', color)
217
+
218
+ # Draw text
219
+ font.render_string(text, x, y, scale, align)
220
+
221
+ def read_color_buf(self):
222
+ """Read and return the current viewport's color buffer.
223
+
224
+ Alpha cannot be computed for an on-screen buffer.
225
+
226
+ Returns
227
+ -------
228
+ color_im : (h, w, 3) uint8
229
+ The color buffer in RGB byte format.
230
+ """
231
+ # Extract color image from frame buffer
232
+ width, height = self.viewport_width, self.viewport_height
233
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
234
+ glReadBuffer(GL_FRONT)
235
+ color_buf = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
236
+
237
+ # Re-format them into numpy arrays
238
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
239
+ color_im = color_im.reshape((height, width, 3))
240
+ color_im = np.flip(color_im, axis=0)
241
+
242
+ # Resize for macos if needed
243
+ if sys.platform == 'darwin':
244
+ color_im = self._resize_image(color_im, True)
245
+
246
+ return color_im
247
+
248
+ def read_depth_buf(self):
249
+ """Read and return the current viewport's color buffer.
250
+
251
+ Returns
252
+ -------
253
+ depth_im : (h, w) float32
254
+ The depth buffer in linear units.
255
+ """
256
+ width, height = self.viewport_width, self.viewport_height
257
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
258
+ glReadBuffer(GL_FRONT)
259
+ depth_buf = glReadPixels(
260
+ 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT
261
+ )
262
+
263
+ depth_im = np.frombuffer(depth_buf, dtype=np.float32)
264
+ depth_im = depth_im.reshape((height, width))
265
+ depth_im = np.flip(depth_im, axis=0)
266
+
267
+ inf_inds = (depth_im == 1.0)
268
+ depth_im = 2.0 * depth_im - 1.0
269
+ z_near, z_far = self._latest_znear, self._latest_zfar
270
+ noninf = np.logical_not(inf_inds)
271
+ if z_far is None:
272
+ depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf])
273
+ else:
274
+ depth_im[noninf] = ((2.0 * z_near * z_far) /
275
+ (z_far + z_near - depth_im[noninf] *
276
+ (z_far - z_near)))
277
+ depth_im[inf_inds] = 0.0
278
+
279
+ # Resize for macos if needed
280
+ if sys.platform == 'darwin':
281
+ depth_im = self._resize_image(depth_im)
282
+
283
+ return depth_im
284
+
285
+ def delete(self):
286
+ """Free all allocated OpenGL resources.
287
+ """
288
+ # Free shaders
289
+ self._program_cache.clear()
290
+
291
+ # Free fonts
292
+ self._font_cache.clear()
293
+
294
+ # Free meshes
295
+ for mesh in self._meshes:
296
+ for p in mesh.primitives:
297
+ p.delete()
298
+
299
+ # Free textures
300
+ for mesh_texture in self._mesh_textures:
301
+ mesh_texture.delete()
302
+
303
+ for shadow_texture in self._shadow_textures:
304
+ shadow_texture.delete()
305
+
306
+ self._meshes = set()
307
+ self._mesh_textures = set()
308
+ self._shadow_textures = set()
309
+ self._texture_alloc_idx = 0
310
+
311
+ self._delete_main_framebuffer()
312
+ self._delete_shadow_framebuffer()
313
+
314
+ def __del__(self):
315
+ try:
316
+ self.delete()
317
+ except Exception:
318
+ pass
319
+
320
+ ###########################################################################
321
+ # Rendering passes
322
+ ###########################################################################
323
+
324
+ def _forward_pass(self, scene, flags, seg_node_map=None):
325
+ # Set up viewport for render
326
+ self._configure_forward_pass_viewport(flags)
327
+
328
+ # Clear it
329
+ if bool(flags & RenderFlags.SEG):
330
+ glClearColor(0.0, 0.0, 0.0, 1.0)
331
+ if seg_node_map is None:
332
+ seg_node_map = {}
333
+ else:
334
+ glClearColor(*scene.bg_color)
335
+
336
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
337
+
338
+ if not bool(flags & RenderFlags.SEG):
339
+ glEnable(GL_MULTISAMPLE)
340
+ else:
341
+ glDisable(GL_MULTISAMPLE)
342
+
343
+ # Set up camera matrices
344
+ V, P = self._get_camera_matrices(scene)
345
+
346
+ program = None
347
+ # Now, render each object in sorted order
348
+ for node in self._sorted_mesh_nodes(scene):
349
+ mesh = node.mesh
350
+
351
+ # Skip the mesh if it's not visible
352
+ if not mesh.is_visible:
353
+ continue
354
+
355
+ # If SEG, set color
356
+ if bool(flags & RenderFlags.SEG):
357
+ if node not in seg_node_map:
358
+ continue
359
+ color = seg_node_map[node]
360
+ if not isinstance(color, (list, tuple, np.ndarray)):
361
+ color = np.repeat(color, 3)
362
+ else:
363
+ color = np.asanyarray(color)
364
+ color = color / 255.0
365
+
366
+ for primitive in mesh.primitives:
367
+
368
+ # First, get and bind the appropriate program
369
+ program = self._get_primitive_program(
370
+ primitive, flags, ProgramFlags.USE_MATERIAL
371
+ )
372
+ program._bind()
373
+
374
+ # Set the camera uniforms
375
+ program.set_uniform('V', V)
376
+ program.set_uniform('P', P)
377
+ program.set_uniform(
378
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
379
+ )
380
+ if bool(flags & RenderFlags.SEG):
381
+ program.set_uniform('color', color)
382
+
383
+ # Next, bind the lighting
384
+ if not (flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.FLAT or
385
+ flags & RenderFlags.SEG):
386
+ self._bind_lighting(scene, program, node, flags)
387
+
388
+ # Finally, bind and draw the primitive
389
+ self._bind_and_draw_primitive(
390
+ primitive=primitive,
391
+ pose=scene.get_pose(node),
392
+ program=program,
393
+ flags=flags
394
+ )
395
+ self._reset_active_textures()
396
+
397
+ # Unbind the shader and flush the output
398
+ if program is not None:
399
+ program._unbind()
400
+ glFlush()
401
+
402
+ # If doing offscreen render, copy result from framebuffer and return
403
+ if flags & RenderFlags.OFFSCREEN:
404
+ return self._read_main_framebuffer(scene, flags)
405
+ else:
406
+ return
407
+
408
+ def _shadow_mapping_pass(self, scene, light_node, flags):
409
+ light = light_node.light
410
+
411
+ # Set up viewport for render
412
+ self._configure_shadow_mapping_viewport(light, flags)
413
+
414
+ # Set up camera matrices
415
+ V, P = self._get_light_cam_matrices(scene, light_node, flags)
416
+
417
+ # Now, render each object in sorted order
418
+ for node in self._sorted_mesh_nodes(scene):
419
+ mesh = node.mesh
420
+
421
+ # Skip the mesh if it's not visible
422
+ if not mesh.is_visible:
423
+ continue
424
+
425
+ for primitive in mesh.primitives:
426
+
427
+ # First, get and bind the appropriate program
428
+ program = self._get_primitive_program(
429
+ primitive, flags, ProgramFlags.NONE
430
+ )
431
+ program._bind()
432
+
433
+ # Set the camera uniforms
434
+ program.set_uniform('V', V)
435
+ program.set_uniform('P', P)
436
+ program.set_uniform(
437
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
438
+ )
439
+
440
+ # Finally, bind and draw the primitive
441
+ self._bind_and_draw_primitive(
442
+ primitive=primitive,
443
+ pose=scene.get_pose(node),
444
+ program=program,
445
+ flags=RenderFlags.DEPTH_ONLY
446
+ )
447
+ self._reset_active_textures()
448
+
449
+ # Unbind the shader and flush the output
450
+ if program is not None:
451
+ program._unbind()
452
+ glFlush()
453
+
454
+ def _normals_pass(self, scene, flags):
455
+ # Set up viewport for render
456
+ self._configure_forward_pass_viewport(flags)
457
+ program = None
458
+
459
+ # Set up camera matrices
460
+ V, P = self._get_camera_matrices(scene)
461
+
462
+ # Now, render each object in sorted order
463
+ for node in self._sorted_mesh_nodes(scene):
464
+ mesh = node.mesh
465
+
466
+ # Skip the mesh if it's not visible
467
+ if not mesh.is_visible:
468
+ continue
469
+
470
+ for primitive in mesh.primitives:
471
+
472
+ # Skip objects that don't have normals
473
+ if not primitive.buf_flags & BufFlags.NORMAL:
474
+ continue
475
+
476
+ # First, get and bind the appropriate program
477
+ pf = ProgramFlags.NONE
478
+ if flags & RenderFlags.VERTEX_NORMALS:
479
+ pf = pf | ProgramFlags.VERTEX_NORMALS
480
+ if flags & RenderFlags.FACE_NORMALS:
481
+ pf = pf | ProgramFlags.FACE_NORMALS
482
+ program = self._get_primitive_program(primitive, flags, pf)
483
+ program._bind()
484
+
485
+ # Set the camera uniforms
486
+ program.set_uniform('V', V)
487
+ program.set_uniform('P', P)
488
+ program.set_uniform('normal_magnitude', 0.05 * primitive.scale)
489
+ program.set_uniform(
490
+ 'normal_color', np.array([0.1, 0.1, 1.0, 1.0])
491
+ )
492
+
493
+ # Finally, bind and draw the primitive
494
+ self._bind_and_draw_primitive(
495
+ primitive=primitive,
496
+ pose=scene.get_pose(node),
497
+ program=program,
498
+ flags=RenderFlags.DEPTH_ONLY
499
+ )
500
+ self._reset_active_textures()
501
+
502
+ # Unbind the shader and flush the output
503
+ if program is not None:
504
+ program._unbind()
505
+ glFlush()
506
+
507
+ ###########################################################################
508
+ # Handlers for binding uniforms and drawing primitives
509
+ ###########################################################################
510
+
511
+ def _bind_and_draw_primitive(self, primitive, pose, program, flags):
512
+ # Set model pose matrix
513
+ program.set_uniform('M', pose)
514
+
515
+ # Bind mesh buffers
516
+ primitive._bind()
517
+
518
+ # Bind mesh material
519
+ if not (flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.SEG):
520
+ material = primitive.material
521
+
522
+ # Bind textures
523
+ tf = material.tex_flags
524
+ if tf & TexFlags.NORMAL:
525
+ self._bind_texture(material.normalTexture,
526
+ 'material.normal_texture', program)
527
+ if tf & TexFlags.OCCLUSION:
528
+ self._bind_texture(material.occlusionTexture,
529
+ 'material.occlusion_texture', program)
530
+ if tf & TexFlags.EMISSIVE:
531
+ self._bind_texture(material.emissiveTexture,
532
+ 'material.emissive_texture', program)
533
+ if tf & TexFlags.BASE_COLOR:
534
+ self._bind_texture(material.baseColorTexture,
535
+ 'material.base_color_texture', program)
536
+ if tf & TexFlags.METALLIC_ROUGHNESS:
537
+ self._bind_texture(material.metallicRoughnessTexture,
538
+ 'material.metallic_roughness_texture',
539
+ program)
540
+ if tf & TexFlags.DIFFUSE:
541
+ self._bind_texture(material.diffuseTexture,
542
+ 'material.diffuse_texture', program)
543
+ if tf & TexFlags.SPECULAR_GLOSSINESS:
544
+ self._bind_texture(material.specularGlossinessTexture,
545
+ 'material.specular_glossiness_texture',
546
+ program)
547
+
548
+ # Bind other uniforms
549
+ b = 'material.{}'
550
+ program.set_uniform(b.format('emissive_factor'),
551
+ material.emissiveFactor)
552
+ if isinstance(material, MetallicRoughnessMaterial):
553
+ program.set_uniform(b.format('base_color_factor'),
554
+ material.baseColorFactor)
555
+ program.set_uniform(b.format('metallic_factor'),
556
+ material.metallicFactor)
557
+ program.set_uniform(b.format('roughness_factor'),
558
+ material.roughnessFactor)
559
+ elif isinstance(material, SpecularGlossinessMaterial):
560
+ program.set_uniform(b.format('diffuse_factor'),
561
+ material.diffuseFactor)
562
+ program.set_uniform(b.format('specular_factor'),
563
+ material.specularFactor)
564
+ program.set_uniform(b.format('glossiness_factor'),
565
+ material.glossinessFactor)
566
+
567
+ # Set blending options
568
+ if material.alphaMode == 'BLEND':
569
+ glEnable(GL_BLEND)
570
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
571
+ else:
572
+ glEnable(GL_BLEND)
573
+ glBlendFunc(GL_ONE, GL_ZERO)
574
+
575
+ # Set wireframe mode
576
+ wf = material.wireframe
577
+ if flags & RenderFlags.FLIP_WIREFRAME:
578
+ wf = not wf
579
+ if (flags & RenderFlags.ALL_WIREFRAME) or wf:
580
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
581
+ else:
582
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
583
+
584
+ # Set culling mode
585
+ if material.doubleSided or flags & RenderFlags.SKIP_CULL_FACES:
586
+ glDisable(GL_CULL_FACE)
587
+ else:
588
+ glEnable(GL_CULL_FACE)
589
+ glCullFace(GL_BACK)
590
+ else:
591
+ glEnable(GL_CULL_FACE)
592
+ glEnable(GL_BLEND)
593
+ glCullFace(GL_BACK)
594
+ glBlendFunc(GL_ONE, GL_ZERO)
595
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
596
+
597
+ # Set point size if needed
598
+ glDisable(GL_PROGRAM_POINT_SIZE)
599
+ if primitive.mode == GLTF.POINTS:
600
+ glEnable(GL_PROGRAM_POINT_SIZE)
601
+ glPointSize(self.point_size)
602
+
603
+ # Render mesh
604
+ n_instances = 1
605
+ if primitive.poses is not None:
606
+ n_instances = len(primitive.poses)
607
+
608
+ if primitive.indices is not None:
609
+ glDrawElementsInstanced(
610
+ primitive.mode, primitive.indices.size, GL_UNSIGNED_INT,
611
+ ctypes.c_void_p(0), n_instances
612
+ )
613
+ else:
614
+ glDrawArraysInstanced(
615
+ primitive.mode, 0, len(primitive.positions), n_instances
616
+ )
617
+
618
+ # Unbind mesh buffers
619
+ primitive._unbind()
620
+
621
+ def _bind_lighting(self, scene, program, node, flags):
622
+ """Bind all lighting uniform values for a scene.
623
+ """
624
+ max_n_lights = self._compute_max_n_lights(flags)
625
+
626
+ n_d = min(len(scene.directional_light_nodes), max_n_lights[0])
627
+ n_s = min(len(scene.spot_light_nodes), max_n_lights[1])
628
+ n_p = min(len(scene.point_light_nodes), max_n_lights[2])
629
+ program.set_uniform('ambient_light', scene.ambient_light)
630
+ program.set_uniform('n_directional_lights', n_d)
631
+ program.set_uniform('n_spot_lights', n_s)
632
+ program.set_uniform('n_point_lights', n_p)
633
+ plc = 0
634
+ slc = 0
635
+ dlc = 0
636
+
637
+ light_nodes = scene.light_nodes
638
+ if (len(scene.directional_light_nodes) > max_n_lights[0] or
639
+ len(scene.spot_light_nodes) > max_n_lights[1] or
640
+ len(scene.point_light_nodes) > max_n_lights[2]):
641
+ light_nodes = self._sorted_nodes_by_distance(
642
+ scene, scene.light_nodes, node
643
+ )
644
+
645
+ for n in light_nodes:
646
+ light = n.light
647
+ pose = scene.get_pose(n)
648
+ position = pose[:3,3]
649
+ direction = -pose[:3,2]
650
+
651
+ if isinstance(light, PointLight):
652
+ if plc == max_n_lights[2]:
653
+ continue
654
+ b = 'point_lights[{}].'.format(plc)
655
+ plc += 1
656
+ shadow = bool(flags & RenderFlags.SHADOWS_POINT)
657
+ program.set_uniform(b + 'position', position)
658
+ elif isinstance(light, SpotLight):
659
+ if slc == max_n_lights[1]:
660
+ continue
661
+ b = 'spot_lights[{}].'.format(slc)
662
+ slc += 1
663
+ shadow = bool(flags & RenderFlags.SHADOWS_SPOT)
664
+ las = 1.0 / max(0.001, np.cos(light.innerConeAngle) -
665
+ np.cos(light.outerConeAngle))
666
+ lao = -np.cos(light.outerConeAngle) * las
667
+ program.set_uniform(b + 'direction', direction)
668
+ program.set_uniform(b + 'position', position)
669
+ program.set_uniform(b + 'light_angle_scale', las)
670
+ program.set_uniform(b + 'light_angle_offset', lao)
671
+ else:
672
+ if dlc == max_n_lights[0]:
673
+ continue
674
+ b = 'directional_lights[{}].'.format(dlc)
675
+ dlc += 1
676
+ shadow = bool(flags & RenderFlags.SHADOWS_DIRECTIONAL)
677
+ program.set_uniform(b + 'direction', direction)
678
+
679
+ program.set_uniform(b + 'color', light.color)
680
+ program.set_uniform(b + 'intensity', light.intensity)
681
+ # if light.range is not None:
682
+ # program.set_uniform(b + 'range', light.range)
683
+ # else:
684
+ # program.set_uniform(b + 'range', 0)
685
+
686
+ if shadow:
687
+ self._bind_texture(light.shadow_texture,
688
+ b + 'shadow_map', program)
689
+ if not isinstance(light, PointLight):
690
+ V, P = self._get_light_cam_matrices(scene, n, flags)
691
+ program.set_uniform(b + 'light_matrix', P.dot(V))
692
+ else:
693
+ raise NotImplementedError(
694
+ 'Point light shadows not implemented'
695
+ )
696
+
697
+ def _sorted_mesh_nodes(self, scene):
698
+ cam_loc = scene.get_pose(scene.main_camera_node)[:3,3]
699
+ solid_nodes = []
700
+ trans_nodes = []
701
+ for node in scene.mesh_nodes:
702
+ mesh = node.mesh
703
+ if mesh.is_transparent:
704
+ trans_nodes.append(node)
705
+ else:
706
+ solid_nodes.append(node)
707
+
708
+ # TODO BETTER SORTING METHOD
709
+ trans_nodes.sort(
710
+ key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc)
711
+ )
712
+ solid_nodes.sort(
713
+ key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc)
714
+ )
715
+
716
+ return solid_nodes + trans_nodes
717
+
718
+ def _sorted_nodes_by_distance(self, scene, nodes, compare_node):
719
+ nodes = list(nodes)
720
+ compare_posn = scene.get_pose(compare_node)[:3,3]
721
+ nodes.sort(key=lambda n: np.linalg.norm(
722
+ scene.get_pose(n)[:3,3] - compare_posn)
723
+ )
724
+ return nodes
725
+
726
+ ###########################################################################
727
+ # Context Management
728
+ ###########################################################################
729
+
730
+ def _update_context(self, scene, flags):
731
+
732
+ # Update meshes
733
+ scene_meshes = scene.meshes
734
+
735
+ # Add new meshes to context
736
+ for mesh in scene_meshes - self._meshes:
737
+ for p in mesh.primitives:
738
+ p._add_to_context()
739
+
740
+ # Remove old meshes from context
741
+ for mesh in self._meshes - scene_meshes:
742
+ for p in mesh.primitives:
743
+ p.delete()
744
+
745
+ self._meshes = scene_meshes.copy()
746
+
747
+ # Update mesh textures
748
+ mesh_textures = set()
749
+ for m in scene_meshes:
750
+ for p in m.primitives:
751
+ mesh_textures |= p.material.textures
752
+
753
+ # Add new textures to context
754
+ for texture in mesh_textures - self._mesh_textures:
755
+ texture._add_to_context()
756
+
757
+ # Remove old textures from context
758
+ for texture in self._mesh_textures - mesh_textures:
759
+ texture.delete()
760
+
761
+ self._mesh_textures = mesh_textures.copy()
762
+
763
+ shadow_textures = set()
764
+ for l in scene.lights:
765
+ # Create if needed
766
+ active = False
767
+ if (isinstance(l, DirectionalLight) and
768
+ flags & RenderFlags.SHADOWS_DIRECTIONAL):
769
+ active = True
770
+ elif (isinstance(l, PointLight) and
771
+ flags & RenderFlags.SHADOWS_POINT):
772
+ active = True
773
+ elif isinstance(l, SpotLight) and flags & RenderFlags.SHADOWS_SPOT:
774
+ active = True
775
+
776
+ if active and l.shadow_texture is None:
777
+ l._generate_shadow_texture()
778
+ if l.shadow_texture is not None:
779
+ shadow_textures.add(l.shadow_texture)
780
+
781
+ # Add new textures to context
782
+ for texture in shadow_textures - self._shadow_textures:
783
+ texture._add_to_context()
784
+
785
+ # Remove old textures from context
786
+ for texture in self._shadow_textures - shadow_textures:
787
+ texture.delete()
788
+
789
+ self._shadow_textures = shadow_textures.copy()
790
+
791
+ ###########################################################################
792
+ # Texture Management
793
+ ###########################################################################
794
+
795
+ def _bind_texture(self, texture, uniform_name, program):
796
+ """Bind a texture to an active texture unit and return
797
+ the texture unit index that was used.
798
+ """
799
+ tex_id = self._get_next_active_texture()
800
+ glActiveTexture(GL_TEXTURE0 + tex_id)
801
+ texture._bind()
802
+ program.set_uniform(uniform_name, tex_id)
803
+
804
+ def _get_next_active_texture(self):
805
+ val = self._texture_alloc_idx
806
+ self._texture_alloc_idx += 1
807
+ return val
808
+
809
+ def _reset_active_textures(self):
810
+ self._texture_alloc_idx = 0
811
+
812
+ ###########################################################################
813
+ # Camera Matrix Management
814
+ ###########################################################################
815
+
816
+ def _get_camera_matrices(self, scene):
817
+ main_camera_node = scene.main_camera_node
818
+ if main_camera_node is None:
819
+ raise ValueError('Cannot render scene without a camera')
820
+ P = main_camera_node.camera.get_projection_matrix(
821
+ width=self.viewport_width, height=self.viewport_height
822
+ )
823
+ pose = scene.get_pose(main_camera_node)
824
+ V = np.linalg.inv(pose) # V maps from world to camera
825
+ return V, P
826
+
827
+ def _get_light_cam_matrices(self, scene, light_node, flags):
828
+ light = light_node.light
829
+ pose = scene.get_pose(light_node).copy()
830
+ s = scene.scale
831
+ camera = light._get_shadow_camera(s)
832
+ P = camera.get_projection_matrix()
833
+ if isinstance(light, DirectionalLight):
834
+ direction = -pose[:3,2]
835
+ c = scene.centroid
836
+ loc = c - direction * s
837
+ pose[:3,3] = loc
838
+ V = np.linalg.inv(pose) # V maps from world to camera
839
+ return V, P
840
+
841
+ ###########################################################################
842
+ # Shader Program Management
843
+ ###########################################################################
844
+
845
+ def _get_text_program(self):
846
+ program = self._program_cache.get_program(
847
+ vertex_shader='text.vert',
848
+ fragment_shader='text.frag'
849
+ )
850
+
851
+ if not program._in_context():
852
+ program._add_to_context()
853
+
854
+ return program
855
+
856
+ def _compute_max_n_lights(self, flags):
857
+ max_n_lights = [MAX_N_LIGHTS, MAX_N_LIGHTS, MAX_N_LIGHTS]
858
+ n_tex_units = glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS)
859
+
860
+ # Reserved texture units: 6
861
+ # Normal Map
862
+ # Occlusion Map
863
+ # Emissive Map
864
+ # Base Color or Diffuse Map
865
+ # MR or SG Map
866
+ # Environment cubemap
867
+
868
+ n_reserved_textures = 6
869
+ n_available_textures = n_tex_units - n_reserved_textures
870
+
871
+ # Distribute textures evenly among lights with shadows, with
872
+ # a preference for directional lights
873
+ n_shadow_types = 0
874
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
875
+ n_shadow_types += 1
876
+ if flags & RenderFlags.SHADOWS_SPOT:
877
+ n_shadow_types += 1
878
+ if flags & RenderFlags.SHADOWS_POINT:
879
+ n_shadow_types += 1
880
+
881
+ if n_shadow_types > 0:
882
+ tex_per_light = n_available_textures // n_shadow_types
883
+
884
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
885
+ max_n_lights[0] = (
886
+ tex_per_light +
887
+ (n_available_textures - tex_per_light * n_shadow_types)
888
+ )
889
+ if flags & RenderFlags.SHADOWS_SPOT:
890
+ max_n_lights[1] = tex_per_light
891
+ if flags & RenderFlags.SHADOWS_POINT:
892
+ max_n_lights[2] = tex_per_light
893
+
894
+ return max_n_lights
895
+
896
+ def _get_primitive_program(self, primitive, flags, program_flags):
897
+ vertex_shader = None
898
+ fragment_shader = None
899
+ geometry_shader = None
900
+ defines = {}
901
+
902
+ if (bool(program_flags & ProgramFlags.USE_MATERIAL) and
903
+ not flags & RenderFlags.DEPTH_ONLY and
904
+ not flags & RenderFlags.FLAT and
905
+ not flags & RenderFlags.SEG):
906
+ vertex_shader = 'mesh.vert'
907
+ fragment_shader = 'mesh.frag'
908
+ elif bool(program_flags & (ProgramFlags.VERTEX_NORMALS |
909
+ ProgramFlags.FACE_NORMALS)):
910
+ vertex_shader = 'vertex_normals.vert'
911
+ if primitive.mode == GLTF.POINTS:
912
+ geometry_shader = 'vertex_normals_pc.geom'
913
+ else:
914
+ geometry_shader = 'vertex_normals.geom'
915
+ fragment_shader = 'vertex_normals.frag'
916
+ elif flags & RenderFlags.FLAT:
917
+ vertex_shader = 'flat.vert'
918
+ fragment_shader = 'flat.frag'
919
+ elif flags & RenderFlags.SEG:
920
+ vertex_shader = 'segmentation.vert'
921
+ fragment_shader = 'segmentation.frag'
922
+ else:
923
+ vertex_shader = 'mesh_depth.vert'
924
+ fragment_shader = 'mesh_depth.frag'
925
+
926
+ # Set up vertex buffer DEFINES
927
+ bf = primitive.buf_flags
928
+ buf_idx = 1
929
+ if bf & BufFlags.NORMAL:
930
+ defines['NORMAL_LOC'] = buf_idx
931
+ buf_idx += 1
932
+ if bf & BufFlags.TANGENT:
933
+ defines['TANGENT_LOC'] = buf_idx
934
+ buf_idx += 1
935
+ if bf & BufFlags.TEXCOORD_0:
936
+ defines['TEXCOORD_0_LOC'] = buf_idx
937
+ buf_idx += 1
938
+ if bf & BufFlags.TEXCOORD_1:
939
+ defines['TEXCOORD_1_LOC'] = buf_idx
940
+ buf_idx += 1
941
+ if bf & BufFlags.COLOR_0:
942
+ defines['COLOR_0_LOC'] = buf_idx
943
+ buf_idx += 1
944
+ if bf & BufFlags.JOINTS_0:
945
+ defines['JOINTS_0_LOC'] = buf_idx
946
+ buf_idx += 1
947
+ if bf & BufFlags.WEIGHTS_0:
948
+ defines['WEIGHTS_0_LOC'] = buf_idx
949
+ buf_idx += 1
950
+ defines['INST_M_LOC'] = buf_idx
951
+
952
+ # Set up shadow mapping defines
953
+ if flags & RenderFlags.SHADOWS_DIRECTIONAL:
954
+ defines['DIRECTIONAL_LIGHT_SHADOWS'] = 1
955
+ if flags & RenderFlags.SHADOWS_SPOT:
956
+ defines['SPOT_LIGHT_SHADOWS'] = 1
957
+ if flags & RenderFlags.SHADOWS_POINT:
958
+ defines['POINT_LIGHT_SHADOWS'] = 1
959
+ max_n_lights = self._compute_max_n_lights(flags)
960
+ defines['MAX_DIRECTIONAL_LIGHTS'] = max_n_lights[0]
961
+ defines['MAX_SPOT_LIGHTS'] = max_n_lights[1]
962
+ defines['MAX_POINT_LIGHTS'] = max_n_lights[2]
963
+
964
+ # Set up vertex normal defines
965
+ if program_flags & ProgramFlags.VERTEX_NORMALS:
966
+ defines['VERTEX_NORMALS'] = 1
967
+ if program_flags & ProgramFlags.FACE_NORMALS:
968
+ defines['FACE_NORMALS'] = 1
969
+
970
+ # Set up material texture defines
971
+ if bool(program_flags & ProgramFlags.USE_MATERIAL):
972
+ tf = primitive.material.tex_flags
973
+ if tf & TexFlags.NORMAL:
974
+ defines['HAS_NORMAL_TEX'] = 1
975
+ if tf & TexFlags.OCCLUSION:
976
+ defines['HAS_OCCLUSION_TEX'] = 1
977
+ if tf & TexFlags.EMISSIVE:
978
+ defines['HAS_EMISSIVE_TEX'] = 1
979
+ if tf & TexFlags.BASE_COLOR:
980
+ defines['HAS_BASE_COLOR_TEX'] = 1
981
+ if tf & TexFlags.METALLIC_ROUGHNESS:
982
+ defines['HAS_METALLIC_ROUGHNESS_TEX'] = 1
983
+ if tf & TexFlags.DIFFUSE:
984
+ defines['HAS_DIFFUSE_TEX'] = 1
985
+ if tf & TexFlags.SPECULAR_GLOSSINESS:
986
+ defines['HAS_SPECULAR_GLOSSINESS_TEX'] = 1
987
+ if isinstance(primitive.material, MetallicRoughnessMaterial):
988
+ defines['USE_METALLIC_MATERIAL'] = 1
989
+ elif isinstance(primitive.material, SpecularGlossinessMaterial):
990
+ defines['USE_GLOSSY_MATERIAL'] = 1
991
+
992
+ program = self._program_cache.get_program(
993
+ vertex_shader=vertex_shader,
994
+ fragment_shader=fragment_shader,
995
+ geometry_shader=geometry_shader,
996
+ defines=defines
997
+ )
998
+
999
+ if not program._in_context():
1000
+ program._add_to_context()
1001
+
1002
+ return program
1003
+
1004
+ ###########################################################################
1005
+ # Viewport Management
1006
+ ###########################################################################
1007
+
1008
+ def _configure_forward_pass_viewport(self, flags):
1009
+
1010
+ # If using offscreen render, bind main framebuffer
1011
+ if flags & RenderFlags.OFFSCREEN:
1012
+ self._configure_main_framebuffer()
1013
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms)
1014
+ else:
1015
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
1016
+
1017
+ glViewport(0, 0, self.viewport_width, self.viewport_height)
1018
+ glEnable(GL_DEPTH_TEST)
1019
+ glDepthMask(GL_TRUE)
1020
+ glDepthFunc(GL_LESS)
1021
+ glDepthRange(0.0, 1.0)
1022
+
1023
+ def _configure_shadow_mapping_viewport(self, light, flags):
1024
+ self._configure_shadow_framebuffer()
1025
+ glBindFramebuffer(GL_FRAMEBUFFER, self._shadow_fb)
1026
+ light.shadow_texture._bind()
1027
+ light.shadow_texture._bind_as_depth_attachment()
1028
+ glActiveTexture(GL_TEXTURE0)
1029
+ light.shadow_texture._bind()
1030
+ glDrawBuffer(GL_NONE)
1031
+ glReadBuffer(GL_NONE)
1032
+
1033
+ glClear(GL_DEPTH_BUFFER_BIT)
1034
+ glViewport(0, 0, SHADOW_TEX_SZ, SHADOW_TEX_SZ)
1035
+ glEnable(GL_DEPTH_TEST)
1036
+ glDepthMask(GL_TRUE)
1037
+ glDepthFunc(GL_LESS)
1038
+ glDepthRange(0.0, 1.0)
1039
+ glDisable(GL_CULL_FACE)
1040
+ glDisable(GL_BLEND)
1041
+
1042
+ ###########################################################################
1043
+ # Framebuffer Management
1044
+ ###########################################################################
1045
+
1046
+ def _configure_shadow_framebuffer(self):
1047
+ if self._shadow_fb is None:
1048
+ self._shadow_fb = glGenFramebuffers(1)
1049
+
1050
+ def _delete_shadow_framebuffer(self):
1051
+ if self._shadow_fb is not None:
1052
+ glDeleteFramebuffers(1, [self._shadow_fb])
1053
+
1054
+ def _configure_main_framebuffer(self):
1055
+ # If mismatch with prior framebuffer, delete it
1056
+ if (self._main_fb is not None and
1057
+ self.viewport_width != self._main_fb_dims[0] or
1058
+ self.viewport_height != self._main_fb_dims[1]):
1059
+ self._delete_main_framebuffer()
1060
+
1061
+ # If framebuffer doesn't exist, create it
1062
+ if self._main_fb is None:
1063
+ # Generate standard buffer
1064
+ self._main_cb, self._main_db = glGenRenderbuffers(2)
1065
+
1066
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb)
1067
+ glRenderbufferStorage(
1068
+ GL_RENDERBUFFER, GL_RGBA,
1069
+ self.viewport_width, self.viewport_height
1070
+ )
1071
+
1072
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_db)
1073
+ glRenderbufferStorage(
1074
+ GL_RENDERBUFFER, GL_DEPTH_COMPONENT24,
1075
+ self.viewport_width, self.viewport_height
1076
+ )
1077
+
1078
+ self._main_fb = glGenFramebuffers(1)
1079
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb)
1080
+ glFramebufferRenderbuffer(
1081
+ GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
1082
+ GL_RENDERBUFFER, self._main_cb
1083
+ )
1084
+ glFramebufferRenderbuffer(
1085
+ GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
1086
+ GL_RENDERBUFFER, self._main_db
1087
+ )
1088
+
1089
+ # Generate multisample buffer
1090
+ self._main_cb_ms, self._main_db_ms = glGenRenderbuffers(2)
1091
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb_ms)
1092
+ # glRenderbufferStorageMultisample(
1093
+ # GL_RENDERBUFFER, 4, GL_RGBA,
1094
+ # self.viewport_width, self.viewport_height
1095
+ # )
1096
+ # glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms)
1097
+ # glRenderbufferStorageMultisample(
1098
+ # GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT24,
1099
+ # self.viewport_width, self.viewport_height
1100
+ # )
1101
+ # 增加这一行
1102
+ num_samples = min(glGetIntegerv(GL_MAX_SAMPLES), 4) # No more than GL_MAX_SAMPLES
1103
+
1104
+ # 其实就是把 4 替换成 num_samples,其余不变
1105
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA, self.viewport_width, self.viewport_height)
1106
+
1107
+ glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms) # 这行不变
1108
+
1109
+ # 这一行也是将 4 替换成 num_samples
1110
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, self.viewport_width, self.viewport_height)
1111
+
1112
+ self._main_fb_ms = glGenFramebuffers(1)
1113
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms)
1114
+ glFramebufferRenderbuffer(
1115
+ GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
1116
+ GL_RENDERBUFFER, self._main_cb_ms
1117
+ )
1118
+ glFramebufferRenderbuffer(
1119
+ GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
1120
+ GL_RENDERBUFFER, self._main_db_ms
1121
+ )
1122
+
1123
+ self._main_fb_dims = (self.viewport_width, self.viewport_height)
1124
+
1125
+ def _delete_main_framebuffer(self):
1126
+ if self._main_fb is not None:
1127
+ glDeleteFramebuffers(2, [self._main_fb, self._main_fb_ms])
1128
+ if self._main_cb is not None:
1129
+ glDeleteRenderbuffers(2, [self._main_cb, self._main_cb_ms])
1130
+ if self._main_db is not None:
1131
+ glDeleteRenderbuffers(2, [self._main_db, self._main_db_ms])
1132
+
1133
+ self._main_fb = None
1134
+ self._main_cb = None
1135
+ self._main_db = None
1136
+ self._main_fb_ms = None
1137
+ self._main_cb_ms = None
1138
+ self._main_db_ms = None
1139
+ self._main_fb_dims = (None, None)
1140
+
1141
+ def _read_main_framebuffer(self, scene, flags):
1142
+ width, height = self._main_fb_dims[0], self._main_fb_dims[1]
1143
+
1144
+ # Bind framebuffer and blit buffers
1145
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb_ms)
1146
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb)
1147
+ glBlitFramebuffer(
1148
+ 0, 0, width, height, 0, 0, width, height,
1149
+ GL_COLOR_BUFFER_BIT, GL_LINEAR
1150
+ )
1151
+ glBlitFramebuffer(
1152
+ 0, 0, width, height, 0, 0, width, height,
1153
+ GL_DEPTH_BUFFER_BIT, GL_NEAREST
1154
+ )
1155
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb)
1156
+
1157
+ # Read depth
1158
+ depth_buf = glReadPixels(
1159
+ 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT
1160
+ )
1161
+ depth_im = np.frombuffer(depth_buf, dtype=np.float32)
1162
+ depth_im = depth_im.reshape((height, width))
1163
+ depth_im = np.flip(depth_im, axis=0)
1164
+ inf_inds = (depth_im == 1.0)
1165
+ depth_im = 2.0 * depth_im - 1.0
1166
+ z_near = scene.main_camera_node.camera.znear
1167
+ z_far = scene.main_camera_node.camera.zfar
1168
+ noninf = np.logical_not(inf_inds)
1169
+ if z_far is None:
1170
+ depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf])
1171
+ else:
1172
+ depth_im[noninf] = ((2.0 * z_near * z_far) /
1173
+ (z_far + z_near - depth_im[noninf] *
1174
+ (z_far - z_near)))
1175
+ depth_im[inf_inds] = 0.0
1176
+
1177
+ # Resize for macos if needed
1178
+ if sys.platform == 'darwin':
1179
+ depth_im = self._resize_image(depth_im)
1180
+
1181
+ if flags & RenderFlags.DEPTH_ONLY:
1182
+ return depth_im
1183
+
1184
+ # Read color
1185
+ if flags & RenderFlags.RGBA:
1186
+ color_buf = glReadPixels(
1187
+ 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE
1188
+ )
1189
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
1190
+ color_im = color_im.reshape((height, width, 4))
1191
+ else:
1192
+ color_buf = glReadPixels(
1193
+ 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE
1194
+ )
1195
+ color_im = np.frombuffer(color_buf, dtype=np.uint8)
1196
+ color_im = color_im.reshape((height, width, 3))
1197
+ color_im = np.flip(color_im, axis=0)
1198
+
1199
+ # Resize for macos if needed
1200
+ if sys.platform == 'darwin':
1201
+ color_im = self._resize_image(color_im, True)
1202
+
1203
+ return color_im, depth_im
1204
+
1205
+ def _resize_image(self, value, antialias=False):
1206
+ """If needed, rescale the render for MacOS."""
1207
+ img = PIL.Image.fromarray(value)
1208
+ resample = PIL.Image.NEAREST
1209
+ if antialias:
1210
+ resample = PIL.Image.BILINEAR
1211
+ size = (self.viewport_width // self.dpscale,
1212
+ self.viewport_height // self.dpscale)
1213
+ img = img.resize(size, resample=resample)
1214
+ return np.array(img)
1215
+
1216
+ ###########################################################################
1217
+ # Shadowmap Debugging
1218
+ ###########################################################################
1219
+
1220
+ def _forward_pass_no_reset(self, scene, flags):
1221
+ # Set up camera matrices
1222
+ V, P = self._get_camera_matrices(scene)
1223
+
1224
+ # Now, render each object in sorted order
1225
+ for node in self._sorted_mesh_nodes(scene):
1226
+ mesh = node.mesh
1227
+
1228
+ # Skip the mesh if it's not visible
1229
+ if not mesh.is_visible:
1230
+ continue
1231
+
1232
+ for primitive in mesh.primitives:
1233
+
1234
+ # First, get and bind the appropriate program
1235
+ program = self._get_primitive_program(
1236
+ primitive, flags, ProgramFlags.USE_MATERIAL
1237
+ )
1238
+ program._bind()
1239
+
1240
+ # Set the camera uniforms
1241
+ program.set_uniform('V', V)
1242
+ program.set_uniform('P', P)
1243
+ program.set_uniform(
1244
+ 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3]
1245
+ )
1246
+
1247
+ # Next, bind the lighting
1248
+ if not flags & RenderFlags.DEPTH_ONLY and not flags & RenderFlags.FLAT:
1249
+ self._bind_lighting(scene, program, node, flags)
1250
+
1251
+ # Finally, bind and draw the primitive
1252
+ self._bind_and_draw_primitive(
1253
+ primitive=primitive,
1254
+ pose=scene.get_pose(node),
1255
+ program=program,
1256
+ flags=flags
1257
+ )
1258
+ self._reset_active_textures()
1259
+
1260
+ # Unbind the shader and flush the output
1261
+ if program is not None:
1262
+ program._unbind()
1263
+ glFlush()
1264
+
1265
+ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False):
1266
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
1267
+ glClearColor(*scene.bg_color)
1268
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
1269
+ glEnable(GL_DEPTH_TEST)
1270
+ glDepthMask(GL_TRUE)
1271
+ glDepthFunc(GL_LESS)
1272
+ glDepthRange(0.0, 1.0)
1273
+
1274
+ w = self.viewport_width
1275
+ h = self.viewport_height
1276
+
1277
+ num_nodes = len(light_nodes)
1278
+ viewport_dims = {
1279
+ (0, 2): [0, h // 2, w // 2, h],
1280
+ (1, 2): [w // 2, h // 2, w, h],
1281
+ (0, 3): [0, h // 2, w // 2, h],
1282
+ (1, 3): [w // 2, h // 2, w, h],
1283
+ (2, 3): [0, 0, w // 2, h // 2],
1284
+ (0, 4): [0, h // 2, w // 2, h],
1285
+ (1, 4): [w // 2, h // 2, w, h],
1286
+ (2, 4): [0, 0, w // 2, h // 2],
1287
+ (3, 4): [w // 2, 0, w, h // 2]
1288
+ }
1289
+
1290
+ if tile:
1291
+ for i, ln in enumerate(light_nodes):
1292
+ light = ln.light
1293
+
1294
+ if light.shadow_texture is None:
1295
+ raise ValueError('Light does not have a shadow texture')
1296
+
1297
+ glViewport(*viewport_dims[(i, num_nodes + 1)])
1298
+
1299
+ program = self._get_debug_quad_program()
1300
+ program._bind()
1301
+ self._bind_texture(light.shadow_texture, 'depthMap', program)
1302
+ self._render_debug_quad()
1303
+ self._reset_active_textures()
1304
+ glFlush()
1305
+ i += 1
1306
+ glViewport(*viewport_dims[(i, num_nodes + 1)])
1307
+ self._forward_pass_no_reset(scene, flags)
1308
+ else:
1309
+ for i, ln in enumerate(light_nodes):
1310
+ light = ln.light
1311
+
1312
+ if light.shadow_texture is None:
1313
+ raise ValueError('Light does not have a shadow texture')
1314
+
1315
+ glViewport(0, 0, self.viewport_width, self.viewport_height)
1316
+
1317
+ program = self._get_debug_quad_program()
1318
+ program._bind()
1319
+ self._bind_texture(light.shadow_texture, 'depthMap', program)
1320
+ self._render_debug_quad()
1321
+ self._reset_active_textures()
1322
+ glFlush()
1323
+ return
1324
+
1325
+ def _get_debug_quad_program(self):
1326
+ program = self._program_cache.get_program(
1327
+ vertex_shader='debug_quad.vert',
1328
+ fragment_shader='debug_quad.frag'
1329
+ )
1330
+ if not program._in_context():
1331
+ program._add_to_context()
1332
+ return program
1333
+
1334
+ def _render_debug_quad(self):
1335
+ x = glGenVertexArrays(1)
1336
+ glBindVertexArray(x)
1337
+ glDrawArrays(GL_TRIANGLES, 0, 6)
1338
+ glBindVertexArray(0)
1339
+ glDeleteVertexArrays(1, [x])