diff --git a/.flake8 b/.flake8 index fec4bcf..978b73c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] -ignore = E231,W504,F405,F403 -max-line-length = 79 +ignore = E231,W504,F405,F403,W503 +max-line-length = 120 select = B,C,E,F,W,T4,B9 exclude = docs/source/conf.py, diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..225bb9c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,101 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + build: + name: 'Python ${{ matrix.python-version }}' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - python-version: "2.7" + tox-env: "py27" + - python-version: "3.6" + tox-env: "py36" + - python-version: "3.7" + tox-env: "py37" + - python-version: "3.8" + tox-env: "py38" + - python-version: "3.9" + tox-env: "py39" + - python-version: "3.10" + tox-env: "py310" + env: + TOXENV: "${{ matrix.tox-env }}" + + steps: + - uses: actions/checkout@v3 + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@v3 + with: + python-version: '${{ matrix.python-version }}' + - name: Install System dependencies + run: | + sudo apt update + sudo apt install -y libosmesa6-dev freeglut3-dev + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-wheel + - name: Test with tox + run: | + if [ "${{ matrix.tox-env }}" == "py27" ]; then + tox + else + tox -- coveralls --service=github + fi + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.7" + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y libosmesa6-dev freeglut3-dev + python -m pip install --upgrade pip + python -m pip install tox + - name: Linting + run: | + tox -e linting + - name: Docs + run: | + tox -e docs + + deploy: + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-latest + needs: [build, checks] + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.7" + - name: Build package + run: | + python -m pip install --upgrade pip setuptools + pip install wheel + python setup.py sdist bdist_wheel + - name: Publish package to PyPI + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1817eb3..240e7a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,11 @@ repos: -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.1 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 exclude: ^setup.py +- repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + args: [--safe, --quiet] diff --git a/docs/source/.gitignore b/docs/source/.gitignore new file mode 100644 index 0000000..69fa449 --- /dev/null +++ b/docs/source/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/docs/source/conf.py b/docs/source/conf.py index 6bf194c..1cb8d57 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,49 +20,49 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../../')) +sys.path.insert(0, os.path.abspath("../../")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.githubpages', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx_automodapi.automodapi', - 'sphinx_automodapi.smart_resolver' + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_automodapi.automodapi", + "sphinx_automodapi.smart_resolver", ] numpydoc_class_members_toctree = False -automodapi_toctreedirnm = 'generated' +automodapi_toctreedirnm = "generated" automodsumm_inherited_members = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'pyrender' -copyright = u'2018, Matthew Matl' -author = u'Matthew Matl' +project = "pyrender" +copyright = "2018, Matthew Matl" +author = "Matthew Matl" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -82,9 +82,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -92,27 +92,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -123,157 +123,151 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import sphinx_rtd_theme -html_theme = 'sphinx_rtd_theme' + +html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'coredoc' +htmlhelp_basename = "coredoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pyrender.tex', u'pyrender Documentation', - u'Matthew Matl', 'manual'), + (master_doc, "pyrender.tex", "pyrender Documentation", "Matthew Matl", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'pyrender', u'pyrender Documentation', - [author], 1) -] +man_pages = [(master_doc, "pyrender", "pyrender Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -282,26 +276,32 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'pyrender', u'pyrender Documentation', - author, 'pyrender', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "pyrender", + "pyrender Documentation", + author, + "pyrender", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False intersphinx_mapping = { - 'python' : ('https://docs.python.org/', None), - 'pyrender' : ('https://pyrender.readthedocs.io/en/latest/', None), + "python": ("https://docs.python.org/", None), + "pyrender": ("https://pyrender.readthedocs.io/en/latest/", None), } # Autosummary fix @@ -310,8 +310,8 @@ # Try to suppress multiple-definition warnings by always taking the shorter # path when two or more paths have the same base module -class MyPythonDomain(PythonDomain): +class MyPythonDomain(PythonDomain): def find_obj(self, env, modname, classname, name, type, searchmode=0): """Ensures an object always resolves to the desired module if defined there.""" @@ -324,14 +324,14 @@ def find_obj(self, env, modname, classname, name, type, searchmode=0): # If multiple matches, try to take the shortest if all the modules are # the same - first_match_name_sp = orig_matches[0][0].split('.') + first_match_name_sp = orig_matches[0][0].split(".") base_name = first_match_name_sp[0] min_len = len(first_match_name_sp) best_match = orig_matches[0] for match in orig_matches[1:]: match_name = match[0] - match_name_sp = match_name.split('.') + match_name_sp = match_name.split(".") match_base = match_name_sp[0] # If we have mismatched bases, return them all to trigger warnings @@ -348,5 +348,4 @@ def find_obj(self, env, modname, classname, name, type, searchmode=0): def setup(sphinx): """Use MyPythonDomain in place of PythonDomain""" - sphinx.override_domain(MyPythonDomain) - + sphinx.add_domain(MyPythonDomain, override=True) diff --git a/examples/duck.py b/examples/duck.py index 9a94bad..b3d9217 100644 --- a/examples/duck.py +++ b/examples/duck.py @@ -6,7 +6,7 @@ duck_source = "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb" -duck = trimesh.load(BytesIO(requests.get(duck_source).content), file_type='glb') +duck = trimesh.load(BytesIO(requests.get(duck_source).content), file_type="glb") duckmesh = Mesh.from_trimesh(list(duck.geometry.values())[0]) scene = Scene(ambient_light=np.array([1.0, 1.0, 1.0, 1.0])) scene.add(duckmesh) diff --git a/examples/example.py b/examples/example.py index 599a485..ef3342d 100644 --- a/examples/example.py +++ b/examples/example.py @@ -1,157 +1,178 @@ """Examples of using pyrender for viewing and offscreen rendering. """ import pyglet -pyglet.options['shadow_window'] = False + +pyglet.options["shadow_window"] = False import os import numpy as np import trimesh -from pyrender import PerspectiveCamera,\ - DirectionalLight, SpotLight, PointLight,\ - MetallicRoughnessMaterial,\ - Primitive, Mesh, Node, Scene,\ - Viewer, OffscreenRenderer, RenderFlags - -#============================================================================== +from pyrender import ( + PerspectiveCamera, + DirectionalLight, + SpotLight, + PointLight, + MetallicRoughnessMaterial, + Primitive, + Mesh, + Node, + Scene, + Viewer, + OffscreenRenderer, + RenderFlags, +) + +# ============================================================================== # Mesh creation -#============================================================================== +# ============================================================================== -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Creating textured meshes from trimeshes -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fuze trimesh -fuze_trimesh = trimesh.load('./models/fuze.obj') +fuze_trimesh = trimesh.load("./models/fuze.obj") fuze_mesh = Mesh.from_trimesh(fuze_trimesh) # Drill trimesh -drill_trimesh = trimesh.load('./models/drill.obj') +drill_trimesh = trimesh.load("./models/drill.obj") drill_mesh = Mesh.from_trimesh(drill_trimesh) drill_pose = np.eye(4) -drill_pose[0,3] = 0.1 -drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2]) +drill_pose[0, 3] = 0.1 +drill_pose[2, 3] = -np.min(drill_trimesh.vertices[:, 2]) # Wood trimesh -wood_trimesh = trimesh.load('./models/wood.obj') +wood_trimesh = trimesh.load("./models/wood.obj") wood_mesh = Mesh.from_trimesh(wood_trimesh) # Water bottle trimesh -bottle_gltf = trimesh.load('./models/WaterBottle.glb') +bottle_gltf = trimesh.load("./models/WaterBottle.glb") bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]] bottle_mesh = Mesh.from_trimesh(bottle_trimesh) -bottle_pose = np.array([ - [1.0, 0.0, 0.0, 0.1], - [0.0, 0.0, -1.0, -0.16], - [0.0, 1.0, 0.0, 0.13], - [0.0, 0.0, 0.0, 1.0], -]) - -#------------------------------------------------------------------------------ +bottle_pose = np.array( + [ + [1.0, 0.0, 0.0, 0.1], + [0.0, 0.0, -1.0, -0.16], + [0.0, 1.0, 0.0, 0.13], + [0.0, 0.0, 0.0, 1.0], + ] +) + +# ------------------------------------------------------------------------------ # Creating meshes with per-vertex colors -#------------------------------------------------------------------------------ -boxv_trimesh = trimesh.creation.box(extents=0.1*np.ones(3)) +# ------------------------------------------------------------------------------ +boxv_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3)) boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape)) boxv_trimesh.visual.vertex_colors = boxv_vertex_colors boxv_mesh = Mesh.from_trimesh(boxv_trimesh, smooth=False) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Creating meshes with per-face colors -#------------------------------------------------------------------------------ -boxf_trimesh = trimesh.creation.box(extents=0.1*np.ones(3)) +# ------------------------------------------------------------------------------ +boxf_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3)) boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape) boxf_trimesh.visual.face_colors = boxf_face_colors boxf_mesh = Mesh.from_trimesh(boxf_trimesh, smooth=False) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Creating meshes from point clouds -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ points = trimesh.creation.icosphere(radius=0.05).vertices point_colors = np.random.uniform(size=points.shape) points_mesh = Mesh.from_points(points, colors=point_colors) -#============================================================================== +# ============================================================================== # Light creation -#============================================================================== +# ============================================================================== direc_l = DirectionalLight(color=np.ones(3), intensity=1.0) -spot_l = SpotLight(color=np.ones(3), intensity=10.0, - innerConeAngle=np.pi/16, outerConeAngle=np.pi/6) +spot_l = SpotLight( + color=np.ones(3), + intensity=10.0, + innerConeAngle=np.pi / 16, + outerConeAngle=np.pi / 6, +) point_l = PointLight(color=np.ones(3), intensity=10.0) -#============================================================================== +# ============================================================================== # Camera creation -#============================================================================== +# ============================================================================== cam = PerspectiveCamera(yfov=(np.pi / 3.0)) -cam_pose = np.array([ - [0.0, -np.sqrt(2)/2, np.sqrt(2)/2, 0.5], - [1.0, 0.0, 0.0, 0.0], - [0.0, np.sqrt(2)/2, np.sqrt(2)/2, 0.4], - [0.0, 0.0, 0.0, 1.0] -]) - -#============================================================================== +cam_pose = np.array( + [ + [0.0, -np.sqrt(2) / 2, np.sqrt(2) / 2, 0.5], + [1.0, 0.0, 0.0, 0.0], + [0.0, np.sqrt(2) / 2, np.sqrt(2) / 2, 0.4], + [0.0, 0.0, 0.0, 1.0], + ] +) + +# ============================================================================== # Scene creation -#============================================================================== +# ============================================================================== scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02, 1.0])) -#============================================================================== +# ============================================================================== # Adding objects to the scene -#============================================================================== +# ============================================================================== -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # By manually creating nodes -#------------------------------------------------------------------------------ -fuze_node = Node(mesh=fuze_mesh, translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2])])) +# ------------------------------------------------------------------------------ +fuze_node = Node( + mesh=fuze_mesh, + translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:, 2])]), +) scene.add_node(fuze_node) boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05])) scene.add_node(boxv_node) boxf_node = Node(mesh=boxf_mesh, translation=np.array([-0.1, -0.10, 0.05])) scene.add_node(boxf_node) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # By using the add() utility function -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ drill_node = scene.add(drill_mesh, pose=drill_pose) bottle_node = scene.add(bottle_mesh, pose=bottle_pose) wood_node = scene.add(wood_mesh) direc_l_node = scene.add(direc_l, pose=cam_pose) spot_l_node = scene.add(spot_l, pose=cam_pose) -#============================================================================== +# ============================================================================== # Using the viewer with a default camera -#============================================================================== +# ============================================================================== v = Viewer(scene, shadows=True) -#============================================================================== +# ============================================================================== # Using the viewer with a pre-specified camera -#============================================================================== +# ============================================================================== cam_node = scene.add(cam, pose=cam_pose) v = Viewer(scene, central_node=drill_node) -#============================================================================== +# ============================================================================== # Rendering offscreen from that camera -#============================================================================== +# ============================================================================== -r = OffscreenRenderer(viewport_width=640*2, viewport_height=480*2) +r = OffscreenRenderer(viewport_width=640 * 2, viewport_height=480 * 2) color, depth = r.render(scene) import matplotlib.pyplot as plt + plt.figure() plt.imshow(color) plt.show() -#============================================================================== +# ============================================================================== # Segmask rendering -#============================================================================== +# ============================================================================== -nm = {node: 20*(i + 1) for i, node in enumerate(scene.mesh_nodes)} +nm = {node: 20 * (i + 1) for i, node in enumerate(scene.mesh_nodes)} seg = r.render(scene, RenderFlags.SEG, nm)[0] plt.figure() plt.imshow(seg) plt.show() r.delete() - diff --git a/pyrender/__init__.py b/pyrender/__init__.py index ee37098..ebc15b9 100644 --- a/pyrender/__init__.py +++ b/pyrender/__init__.py @@ -1,5 +1,4 @@ -from .camera import (Camera, PerspectiveCamera, OrthographicCamera, - IntrinsicsCamera) +from .camera import Camera, PerspectiveCamera, OrthographicCamera, IntrinsicsCamera from .light import Light, PointLight, DirectionalLight, SpotLight from .sampler import Sampler from .texture import Texture @@ -15,10 +14,27 @@ from .constants import RenderFlags, TextAlign, GLTF __all__ = [ - 'Camera', 'PerspectiveCamera', 'OrthographicCamera', 'IntrinsicsCamera', - 'Light', 'PointLight', 'DirectionalLight', 'SpotLight', - 'Sampler', 'Texture', 'Material', 'MetallicRoughnessMaterial', - 'Primitive', 'Mesh', 'Node', 'Scene', 'Renderer', 'Viewer', - 'OffscreenRenderer', '__version__', 'RenderFlags', 'TextAlign', - 'GLTF' + "Camera", + "PerspectiveCamera", + "OrthographicCamera", + "IntrinsicsCamera", + "Light", + "PointLight", + "DirectionalLight", + "SpotLight", + "Sampler", + "Texture", + "Material", + "MetallicRoughnessMaterial", + "Primitive", + "Mesh", + "Node", + "Scene", + "Renderer", + "Viewer", + "OffscreenRenderer", + "__version__", + "RenderFlags", + "TextAlign", + "GLTF", ] diff --git a/pyrender/camera.py b/pyrender/camera.py index e019358..15f05db 100644 --- a/pyrender/camera.py +++ b/pyrender/camera.py @@ -32,18 +32,14 @@ class Camera(object): The user-defined name of this object. """ - def __init__(self, - znear=DEFAULT_Z_NEAR, - zfar=DEFAULT_Z_FAR, - name=None): + def __init__(self, znear=DEFAULT_Z_NEAR, zfar=DEFAULT_Z_FAR, name=None): self.name = name self.znear = znear self.zfar = zfar @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -54,28 +50,26 @@ def name(self, value): @property def znear(self): - """float : The distance to the near clipping plane. - """ + """float : The distance to the near clipping plane.""" return self._znear @znear.setter def znear(self, value): value = float(value) if value < 0: - raise ValueError('z-near must be >= 0.0') + raise ValueError("z-near must be >= 0.0") self._znear = value @property def zfar(self): - """float : The distance to the far clipping plane. - """ + """float : The distance to the far clipping plane.""" return self._zfar @zfar.setter def zfar(self, value): value = float(value) if value <= 0 or value <= self.znear: - raise ValueError('zfar must be >0 and >znear') + raise ValueError("zfar must be >0 and >znear") self._zfar = value @abc.abstractmethod @@ -114,12 +108,9 @@ class PerspectiveCamera(Camera): The user-defined name of this object. """ - def __init__(self, - yfov, - znear=DEFAULT_Z_NEAR, - zfar=None, - aspectRatio=None, - name=None): + def __init__( + self, yfov, znear=DEFAULT_Z_NEAR, zfar=None, aspectRatio=None, name=None + ): super(PerspectiveCamera, self).__init__( znear=znear, zfar=zfar, @@ -131,21 +122,19 @@ def __init__(self, @property def yfov(self): - """float : The vertical field of view in radians. - """ + """float : The vertical field of view in radians.""" return self._yfov @yfov.setter def yfov(self, value): value = float(value) if value <= 0.0: - raise ValueError('Field of view must be positive') + raise ValueError("Field of view must be positive") self._yfov = value @property def zfar(self): - """float : The distance to the far clipping plane. - """ + """float : The distance to the far clipping plane.""" return self._zfar @zfar.setter @@ -153,13 +142,12 @@ def zfar(self, value): if value is not None: value = float(value) if value <= 0 or value <= self.znear: - raise ValueError('zfar must be >0 and >znear') + raise ValueError("zfar must be >0 and >znear") self._zfar = value @property def aspectRatio(self): - """float : The ratio of the width to the height of the field of view. - """ + """float : The ratio of the width to the height of the field of view.""" return self._aspectRatio @aspectRatio.setter @@ -167,7 +155,7 @@ def aspectRatio(self, value): if value is not None: value = float(value) if value <= 0.0: - raise ValueError('Aspect ratio must be positive') + raise ValueError("Aspect ratio must be positive") self._aspectRatio = value def get_projection_matrix(self, width=None, height=None): @@ -183,7 +171,7 @@ def get_projection_matrix(self, width=None, height=None): aspect_ratio = self.aspectRatio if aspect_ratio is None: if width is None or height is None: - raise ValueError('Aspect ratio of camera must be defined') + raise ValueError("Aspect ratio of camera must be defined") aspect_ratio = float(width) / float(height) a = aspect_ratio @@ -191,7 +179,7 @@ def get_projection_matrix(self, width=None, height=None): n = self.znear f = self.zfar - P = np.zeros((4,4)) + P = np.zeros((4, 4)) P[0][0] = 1.0 / (a * t) P[1][1] = 1.0 / t P[3][2] = -1.0 @@ -226,12 +214,7 @@ class OrthographicCamera(Camera): The user-defined name of this object. """ - def __init__(self, - xmag, - ymag, - znear=DEFAULT_Z_NEAR, - zfar=DEFAULT_Z_FAR, - name=None): + def __init__(self, xmag, ymag, znear=DEFAULT_Z_NEAR, zfar=DEFAULT_Z_FAR, name=None): super(OrthographicCamera, self).__init__( znear=znear, zfar=zfar, @@ -243,41 +226,38 @@ def __init__(self, @property def xmag(self): - """float : The horizontal magnification of the view. - """ + """float : The horizontal magnification of the view.""" return self._xmag @xmag.setter def xmag(self, value): value = float(value) if value <= 0.0: - raise ValueError('X magnification must be positive') + raise ValueError("X magnification must be positive") self._xmag = value @property def ymag(self): - """float : The vertical magnification of the view. - """ + """float : The vertical magnification of the view.""" return self._ymag @ymag.setter def ymag(self, value): value = float(value) if value <= 0.0: - raise ValueError('Y magnification must be positive') + raise ValueError("Y magnification must be positive") self._ymag = value @property def znear(self): - """float : The distance to the near clipping plane. - """ + """float : The distance to the near clipping plane.""" return self._znear @znear.setter def znear(self, value): value = float(value) if value <= 0: - raise ValueError('z-near must be > 0.0') + raise ValueError("z-near must be > 0.0") self._znear = value def get_projection_matrix(self, width=None, height=None): @@ -301,7 +281,7 @@ def get_projection_matrix(self, width=None, height=None): n = self.znear f = self.zfar - P = np.zeros((4,4)) + P = np.zeros((4, 4)) P[0][0] = 1.0 / xmag P[1][1] = 1.0 / ymag P[2][2] = 2.0 / (n - f) @@ -334,14 +314,9 @@ class IntrinsicsCamera(Camera): The user-defined name of this object. """ - def __init__(self, - fx, - fy, - cx, - cy, - znear=DEFAULT_Z_NEAR, - zfar=DEFAULT_Z_FAR, - name=None): + def __init__( + self, fx, fy, cx, cy, znear=DEFAULT_Z_NEAR, zfar=DEFAULT_Z_FAR, name=None + ): super(IntrinsicsCamera, self).__init__( znear=znear, zfar=zfar, @@ -355,8 +330,7 @@ def __init__(self, @property def fx(self): - """float : X-axis focal length in meters. - """ + """float : X-axis focal length in meters.""" return self._fx @fx.setter @@ -365,8 +339,7 @@ def fx(self, value): @property def fy(self): - """float : Y-axis focal length in meters. - """ + """float : Y-axis focal length in meters.""" return self._fy @fy.setter @@ -375,8 +348,7 @@ def fy(self, value): @property def cx(self): - """float : X-axis optical center in pixels. - """ + """float : X-axis optical center in pixels.""" return self._cx @cx.setter @@ -385,8 +357,7 @@ def cx(self, value): @property def cy(self): - """float : Y-axis optical center in pixels. - """ + """float : Y-axis optical center in pixels.""" return self._cy @cy.setter @@ -408,13 +379,13 @@ def get_projection_matrix(self, width, height): cx, cy = self.cx, self.cy fx, fy = self.fx, self.fy - if sys.platform == 'darwin': + if sys.platform == "darwin": cx = self.cx * 2.0 cy = self.cy * 2.0 fx = self.fx * 2.0 fy = self.fy * 2.0 - P = np.zeros((4,4)) + P = np.zeros((4, 4)) P[0][0] = 2.0 * fx / width P[1][1] = 2.0 * fy / height P[0][2] = 1.0 - 2.0 * cx / width @@ -433,5 +404,4 @@ def get_projection_matrix(self, width, height): return P -__all__ = ['Camera', 'PerspectiveCamera', 'OrthographicCamera', - 'IntrinsicsCamera'] +__all__ = ["Camera", "PerspectiveCamera", "OrthographicCamera", "IntrinsicsCamera"] diff --git a/pyrender/constants.py b/pyrender/constants.py index 8a5785b..e5100fc 100644 --- a/pyrender/constants.py +++ b/pyrender/constants.py @@ -1,15 +1,15 @@ -DEFAULT_Z_NEAR = 0.05 # Near clipping plane, in meters -DEFAULT_Z_FAR = 100.0 # Far clipping plane, in meters -DEFAULT_SCENE_SCALE = 2.0 # Default scene scale -MAX_N_LIGHTS = 4 # Maximum number of lights of each type allowed +DEFAULT_Z_NEAR = 0.05 # Near clipping plane, in meters +DEFAULT_Z_FAR = 100.0 # Far clipping plane, in meters +DEFAULT_SCENE_SCALE = 2.0 # Default scene scale +MAX_N_LIGHTS = 4 # Maximum number of lights of each type allowed TARGET_OPEN_GL_MAJOR = 4 # Target OpenGL Major Version TARGET_OPEN_GL_MINOR = 1 # Target OpenGL Minor Version -MIN_OPEN_GL_MAJOR = 3 # Minimum OpenGL Major Version -MIN_OPEN_GL_MINOR = 3 # Minimum OpenGL Minor Version -FLOAT_SZ = 4 # Byte size of GL float32 -UINT_SZ = 4 # Byte size of GL uint32 -SHADOW_TEX_SZ = 2048 # Width and Height of Shadow Textures -TEXT_PADDING = 20 # Width of padding for rendering text (px) +MIN_OPEN_GL_MAJOR = 3 # Minimum OpenGL Major Version +MIN_OPEN_GL_MINOR = 3 # Minimum OpenGL Minor Version +FLOAT_SZ = 4 # Byte size of GL float32 +UINT_SZ = 4 # Byte size of GL uint32 +SHADOW_TEX_SZ = 2048 # Width and Height of Shadow Textures +TEXT_PADDING = 20 # Width of padding for rendering text (px) # Flags for render type @@ -23,6 +23,7 @@ class RenderFlags(object): would result in an offscreen render with directional shadows and vertex normals enabled. """ + NONE = 0 """Normal PBR Render.""" DEPTH_ONLY = 1 @@ -61,6 +62,7 @@ class TextAlign: Only use one at a time. """ + CENTER = 0 """Center the text by width and height.""" CENTER_LEFT = 1 @@ -83,6 +85,7 @@ class TextAlign: class GLTF(object): """Options for GL objects.""" + NEAREST = 9728 """Nearest neighbor interpolation.""" LINEAR = 9729 @@ -146,4 +149,4 @@ class ProgramFlags: FACE_NORMALS = 4 -__all__ = ['RenderFlags', 'TextAlign', 'GLTF'] +__all__ = ["RenderFlags", "TextAlign", "GLTF"] diff --git a/pyrender/font.py b/pyrender/font.py index 5ac530d..ee9dd9d 100644 --- a/pyrender/font.py +++ b/pyrender/font.py @@ -15,15 +15,14 @@ class FontCache(object): - """A cache for fonts. - """ + """A cache for fonts.""" def __init__(self, font_dir=None): self._font_cache = {} self.font_dir = font_dir if self.font_dir is None: base_dir, _ = os.path.split(os.path.realpath(__file__)) - self.font_dir = os.path.join(base_dir, 'fonts') + self.font_dir = os.path.join(base_dir, "fonts") def get_font(self, font_name, font_pt): # If it's a file, load it directly, else, try to load from font dir. @@ -32,7 +31,7 @@ def get_font(self, font_name, font_pt): _, font_name = os.path.split(font_name) font_name, _ = os.path.split(font_name) else: - font_filename = os.path.join(self.font_dir, font_name) + '.ttf' + font_filename = os.path.join(self.font_dir, font_name) + ".ttf" cid = OpenGL.contextdata.getContext() key = (cid, font_name, int(font_pt)) @@ -48,8 +47,7 @@ def clear(self): class Character(object): - """A single character, with its texture and attributes. - """ + """A single character, with its texture and attributes.""" def __init__(self, texture, size, bearing, advance): self.texture = texture @@ -83,25 +81,22 @@ def __init__(self, font_file, font_pt=40): face.load_char(chr(i)) buf = face.glyph.bitmap.buffer src = (np.array(buf) / 255.0).astype(np.float32) - src = src.reshape((face.glyph.bitmap.rows, - face.glyph.bitmap.width)) + src = src.reshape((face.glyph.bitmap.rows, face.glyph.bitmap.width)) tex = Texture( sampler=Sampler( magFilter=GL_LINEAR, minFilter=GL_LINEAR, wrapS=GL_CLAMP_TO_EDGE, - wrapT=GL_CLAMP_TO_EDGE + wrapT=GL_CLAMP_TO_EDGE, ), source=src, - source_channels='R', + source_channels="R", ) character = Character( texture=tex, - size=np.array([face.glyph.bitmap.width, - face.glyph.bitmap.rows]), - bearing=np.array([face.glyph.bitmap_left, - face.glyph.bitmap_top]), - advance=face.glyph.advance.x + size=np.array([face.glyph.bitmap.width, face.glyph.bitmap.rows]), + bearing=np.array([face.glyph.bitmap_left, face.glyph.bitmap_top]), + advance=face.glyph.advance.x, ) self._character_map[chr(i)] = character @@ -110,8 +105,7 @@ def __init__(self, font_file, font_pt=40): @property def font_file(self): - """str : The file the font was loaded from. - """ + """str : The file the font was loaded from.""" return self._font_file @font_file.setter @@ -120,8 +114,7 @@ def font_file(self, value): @property def font_pt(self): - """int : The height of the font in pixels. - """ + """int : The height of the font in pixels.""" return self._font_pt @font_pt.setter @@ -170,8 +163,7 @@ def delete(self): self._unbind() self._remove_from_context() - def render_string(self, text, x, y, scale=1.0, - align=TextAlign.BOTTOM_LEFT): + def render_string(self, text, x, y, scale=1.0, align=TextAlign.BOTTOM_LEFT): """Render a string to the current view buffer. Note @@ -244,21 +236,22 @@ def render_string(self, text, x, y, scale=1.0, w = ch.size[0] * scale h = ch.size[1] * scale - vertices = np.array([ - [xpos, ypos, 0.0, 0.0], - [xpos + w, ypos, 1.0, 0.0], - [xpos + w, ypos + h, 1.0, 1.0], - [xpos + w, ypos + h, 1.0, 1.0], - [xpos, ypos + h, 0.0, 1.0], - [xpos, ypos, 0.0, 0.0], - ], dtype=np.float32) + vertices = np.array( + [ + [xpos, ypos, 0.0, 0.0], + [xpos + w, ypos, 1.0, 0.0], + [xpos + w, ypos + h, 1.0, 1.0], + [xpos + w, ypos + h, 1.0, 1.0], + [xpos, ypos + h, 0.0, 1.0], + [xpos, ypos, 0.0, 0.0], + ], + dtype=np.float32, + ) ch.texture._bind() glBindBuffer(GL_ARRAY_BUFFER, self._vbo) - glBufferData( - GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW - ) + glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW) # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken # glBufferSubData( # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ, diff --git a/pyrender/light.py b/pyrender/light.py index 333d9e4..dc71a31 100644 --- a/pyrender/light.py +++ b/pyrender/light.py @@ -15,7 +15,6 @@ from .camera import OrthographicCamera, PerspectiveCamera - @six.add_metaclass(abc.ABCMeta) class Light(object): """Base class for all light objects. @@ -31,10 +30,8 @@ class Light(object): name : str, optional Name of the light. """ - def __init__(self, - color=None, - intensity=None, - name=None): + + def __init__(self, color=None, intensity=None, name=None): if color is None: color = np.ones(3) @@ -49,8 +46,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -61,8 +57,7 @@ def name(self, value): @property def color(self): - """(3,) float : The light's color. - """ + """(3,) float : The light's color.""" return self._color @color.setter @@ -71,8 +66,7 @@ def color(self, value): @property def intensity(self): - """float : The light's intensity in candela or lux. - """ + """float : The light's intensity in candela or lux.""" return self._intensity @intensity.setter @@ -81,8 +75,7 @@ def intensity(self, value): @property def shadow_texture(self): - """:class:`.Texture` : A texture used to hold shadow maps for this light. - """ + """:class:`.Texture` : A texture used to hold shadow maps for this light.""" return self._shadow_texture @shadow_texture.setter @@ -140,10 +133,7 @@ class DirectionalLight(Light): Name of the light. """ - def __init__(self, - color=None, - intensity=None, - name=None): + def __init__(self, color=None, intensity=None, name=None): super(DirectionalLight, self).__init__( color=color, intensity=intensity, @@ -160,8 +150,9 @@ def _generate_shadow_texture(self, size=None): """ if size is None: size = SHADOW_TEX_SZ - self.shadow_texture = Texture(width=size, height=size, - source_channels='D', data_format=GL_FLOAT) + self.shadow_texture = Texture( + width=size, height=size, source_channels="D", data_format=GL_FLOAT + ) def _get_shadow_camera(self, scene_scale): """Generate and return a shadow mapping camera for this light. @@ -180,7 +171,7 @@ def _get_shadow_camera(self, scene_scale): znear=0.01 * scene_scale, zfar=10 * scene_scale, xmag=scene_scale, - ymag=scene_scale + ymag=scene_scale, ) @@ -205,11 +196,7 @@ class PointLight(Light): Name of the light. """ - def __init__(self, - color=None, - intensity=None, - range=None, - name=None): + def __init__(self, color=None, intensity=None, range=None, name=None): super(PointLight, self).__init__( color=color, intensity=intensity, @@ -219,8 +206,7 @@ def __init__(self, @property def range(self): - """float : The cutoff distance for the light. - """ + """float : The cutoff distance for the light.""" return self._range @range.setter @@ -228,7 +214,7 @@ def range(self, value): if value is not None: value = float(value) if value <= 0: - raise ValueError('Range must be > 0') + raise ValueError("Range must be > 0") self._range = value self._range = value @@ -240,7 +226,7 @@ def _generate_shadow_texture(self, size=None): size : int, optional Size of texture map. Must be a positive power of two. """ - raise NotImplementedError('Shadows not implemented for point lights') + raise NotImplementedError("Shadows not implemented for point lights") def _get_shadow_camera(self, scene_scale): """Generate and return a shadow mapping camera for this light. @@ -255,7 +241,7 @@ def _get_shadow_camera(self, scene_scale): camera : :class:`.Camera` The camera used to render shadowmaps for this light. """ - raise NotImplementedError('Shadows not implemented for point lights') + raise NotImplementedError("Shadows not implemented for point lights") class SpotLight(Light): @@ -293,13 +279,15 @@ class SpotLight(Light): Name of the light. """ - def __init__(self, - color=None, - intensity=None, - range=None, - innerConeAngle=0.0, - outerConeAngle=(np.pi / 4.0), - name=None): + def __init__( + self, + color=None, + intensity=None, + range=None, + innerConeAngle=0.0, + outerConeAngle=(np.pi / 4.0), + name=None, + ): super(SpotLight, self).__init__( name=name, color=color, @@ -311,32 +299,29 @@ def __init__(self, @property def innerConeAngle(self): - """float : The inner cone angle in radians. - """ + """float : The inner cone angle in radians.""" return self._innerConeAngle @innerConeAngle.setter def innerConeAngle(self, value): if value < 0.0 or value > self.outerConeAngle: - raise ValueError('Invalid value for inner cone angle') + raise ValueError("Invalid value for inner cone angle") self._innerConeAngle = float(value) @property def outerConeAngle(self): - """float : The outer cone angle in radians. - """ + """float : The outer cone angle in radians.""" return self._outerConeAngle @outerConeAngle.setter def outerConeAngle(self, value): if value < 0.0 or value > np.pi / 2.0 + 1e-9: - raise ValueError('Invalid value for outer cone angle') + raise ValueError("Invalid value for outer cone angle") self._outerConeAngle = float(value) @property def range(self): - """float : The cutoff distance for the light. - """ + """float : The cutoff distance for the light.""" return self._range @range.setter @@ -344,7 +329,7 @@ def range(self, value): if value is not None: value = float(value) if value <= 0: - raise ValueError('Range must be > 0') + raise ValueError("Range must be > 0") self._range = value self._range = value @@ -358,8 +343,9 @@ def _generate_shadow_texture(self, size=None): """ if size is None: size = SHADOW_TEX_SZ - self.shadow_texture = Texture(width=size, height=size, - source_channels='D', data_format=GL_FLOAT) + self.shadow_texture = Texture( + width=size, height=size, source_channels="D", data_format=GL_FLOAT + ) def _get_shadow_camera(self, scene_scale): """Generate and return a shadow mapping camera for this light. @@ -378,8 +364,8 @@ def _get_shadow_camera(self, scene_scale): znear=0.01 * scene_scale, zfar=10 * scene_scale, yfov=np.clip(2 * self.outerConeAngle + np.pi / 16.0, 0.0, np.pi), - aspectRatio=1.0 + aspectRatio=1.0, ) -__all__ = ['Light', 'DirectionalLight', 'SpotLight', 'PointLight'] +__all__ = ["Light", "DirectionalLight", "SpotLight", "PointLight"] diff --git a/pyrender/material.py b/pyrender/material.py index 3ce9c2d..ae913cf 100644 --- a/pyrender/material.py +++ b/pyrender/material.py @@ -75,21 +75,23 @@ class Material(object): If True, the material is rendered in wireframe mode. """ - def __init__(self, - name=None, - normalTexture=None, - occlusionTexture=None, - emissiveTexture=None, - emissiveFactor=None, - alphaMode=None, - alphaCutoff=None, - doubleSided=False, - smooth=True, - wireframe=False): + def __init__( + self, + name=None, + normalTexture=None, + occlusionTexture=None, + emissiveTexture=None, + emissiveFactor=None, + alphaMode=None, + alphaCutoff=None, + doubleSided=False, + smooth=True, + wireframe=False, + ): # Set defaults if alphaMode is None: - alphaMode = 'OPAQUE' + alphaMode = "OPAQUE" if alphaCutoff is None: alphaCutoff = 0.5 @@ -112,8 +114,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -124,42 +125,38 @@ def name(self, value): @property def normalTexture(self): - """(n,n,3) float or :class:`Texture` : The tangent-space normal map. - """ + """(n,n,3) float or :class:`Texture` : The tangent-space normal map.""" return self._normalTexture @normalTexture.setter def normalTexture(self, value): # TODO TMP - self._normalTexture = self._format_texture(value, 'RGB') + self._normalTexture = self._format_texture(value, "RGB") self._tex_flags = None @property def occlusionTexture(self): - """(n,n,1) float or :class:`Texture` : The ambient occlusion map. - """ + """(n,n,1) float or :class:`Texture` : The ambient occlusion map.""" return self._occlusionTexture @occlusionTexture.setter def occlusionTexture(self, value): - self._occlusionTexture = self._format_texture(value, 'R') + self._occlusionTexture = self._format_texture(value, "R") self._tex_flags = None @property def emissiveTexture(self): - """(n,n,3) float or :class:`Texture` : The emission map. - """ + """(n,n,3) float or :class:`Texture` : The emission map.""" return self._emissiveTexture @emissiveTexture.setter def emissiveTexture(self, value): - self._emissiveTexture = self._format_texture(value, 'RGB') + self._emissiveTexture = self._format_texture(value, "RGB") self._tex_flags = None @property def emissiveFactor(self): - """(3,) float : Base multiplier for emission colors. - """ + """(3,) float : Base multiplier for emission colors.""" return self._emissiveFactor @emissiveFactor.setter @@ -170,38 +167,35 @@ def emissiveFactor(self, value): @property def alphaMode(self): - """str : The mode for blending. - """ + """str : The mode for blending.""" return self._alphaMode @alphaMode.setter def alphaMode(self, value): - if value not in set(['OPAQUE', 'MASK', 'BLEND']): - raise ValueError('Invalid alpha mode {}'.format(value)) + if value not in set(["OPAQUE", "MASK", "BLEND"]): + raise ValueError("Invalid alpha mode {}".format(value)) self._alphaMode = value @property def alphaCutoff(self): - """float : The cutoff threshold in MASK mode. - """ + """float : The cutoff threshold in MASK mode.""" return self._alphaCutoff @alphaCutoff.setter def alphaCutoff(self, value): if value < 0 or value > 1: - raise ValueError('Alpha cutoff must be in range [0,1]') + raise ValueError("Alpha cutoff must be in range [0,1]") self._alphaCutoff = float(value) @property def doubleSided(self): - """bool : Whether the material is double-sided. - """ + """bool : Whether the material is double-sided.""" return self._doubleSided @doubleSided.setter def doubleSided(self, value): if not isinstance(value, bool): - raise TypeError('Double sided must be a boolean value') + raise TypeError("Double sided must be a boolean value") self._doubleSided = value @property @@ -214,31 +208,28 @@ def smooth(self): @smooth.setter def smooth(self, value): if not isinstance(value, bool): - raise TypeError('Double sided must be a boolean value') + raise TypeError("Double sided must be a boolean value") self._smooth = value @property def wireframe(self): - """bool : Whether to render the mesh in wireframe mode. - """ + """bool : Whether to render the mesh in wireframe mode.""" return self._wireframe @wireframe.setter def wireframe(self, value): if not isinstance(value, bool): - raise TypeError('Wireframe must be a boolean value') + raise TypeError("Wireframe must be a boolean value") self._wireframe = value @property def is_transparent(self): - """bool : If True, the object is partially transparent. - """ + """bool : If True, the object is partially transparent.""" return self._compute_transparency() @property def tex_flags(self): - """int : Texture availability flags. - """ + """int : Texture availability flags.""" if self._tex_flags is None: self._tex_flags = self._compute_tex_flags() return self._tex_flags @@ -264,15 +255,12 @@ def _compute_tex_flags(self): return tex_flags def _compute_textures(self): - all_textures = [ - self.normalTexture, self.occlusionTexture, self.emissiveTexture - ] + all_textures = [self.normalTexture, self.occlusionTexture, self.emissiveTexture] textures = set([t for t in all_textures if t is not None]) return textures - def _format_texture(self, texture, target_channels='RGB'): - """Format a texture as a float32 np array. - """ + def _format_texture(self, texture, target_channels="RGB"): + """Format a texture as a float32 np array.""" if isinstance(texture, Texture) or texture is None: return texture else: @@ -370,22 +358,24 @@ class MetallicRoughnessMaterial(Material): are ignored for metallic-roughness calculations. """ - def __init__(self, - name=None, - normalTexture=None, - occlusionTexture=None, - emissiveTexture=None, - emissiveFactor=None, - alphaMode=None, - alphaCutoff=None, - doubleSided=False, - smooth=True, - wireframe=False, - baseColorFactor=None, - baseColorTexture=None, - metallicFactor=1.0, - roughnessFactor=1.0, - metallicRoughnessTexture=None): + def __init__( + self, + name=None, + normalTexture=None, + occlusionTexture=None, + emissiveTexture=None, + emissiveFactor=None, + alphaMode=None, + alphaCutoff=None, + doubleSided=False, + smooth=True, + wireframe=False, + baseColorFactor=None, + baseColorTexture=None, + metallicFactor=1.0, + roughnessFactor=1.0, + metallicRoughnessTexture=None, + ): super(MetallicRoughnessMaterial, self).__init__( name=name, normalTexture=normalTexture, @@ -396,7 +386,7 @@ def __init__(self, alphaCutoff=alphaCutoff, doubleSided=doubleSided, smooth=smooth, - wireframe=wireframe + wireframe=wireframe, ) # Set defaults @@ -411,8 +401,7 @@ def __init__(self, @property def baseColorFactor(self): - """(4,) float or :class:`Texture` : The RGBA base color multiplier. - """ + """(4,) float or :class:`Texture` : The RGBA base color multiplier.""" return self._baseColorFactor @baseColorFactor.setter @@ -423,19 +412,17 @@ def baseColorFactor(self, value): @property def baseColorTexture(self): - """(n,n,4) float or :class:`Texture` : The diffuse texture. - """ + """(n,n,4) float or :class:`Texture` : The diffuse texture.""" return self._baseColorTexture @baseColorTexture.setter def baseColorTexture(self, value): - self._baseColorTexture = self._format_texture(value, 'RGBA') + self._baseColorTexture = self._format_texture(value, "RGBA") self._tex_flags = None @property def metallicFactor(self): - """float : The metalness of the material. - """ + """float : The metalness of the material.""" return self._metallicFactor @metallicFactor.setter @@ -443,13 +430,12 @@ def metallicFactor(self, value): if value is None: value = 1.0 if value < 0 or value > 1: - raise ValueError('Metallic factor must be in range [0,1]') + raise ValueError("Metallic factor must be in range [0,1]") self._metallicFactor = float(value) @property def roughnessFactor(self): - """float : The roughness of the material. - """ + """float : The roughness of the material.""" return self.RoughnessFactor @roughnessFactor.setter @@ -457,18 +443,17 @@ def roughnessFactor(self, value): if value is None: value = 1.0 if value < 0 or value > 1: - raise ValueError('Roughness factor must be in range [0,1]') + raise ValueError("Roughness factor must be in range [0,1]") self.RoughnessFactor = float(value) @property def metallicRoughnessTexture(self): - """(n,n,2) float or :class:`Texture` : The metallic-roughness texture. - """ + """(n,n,2) float or :class:`Texture` : The metallic-roughness texture.""" return self._metallicRoughnessTexture @metallicRoughnessTexture.setter def metallicRoughnessTexture(self, value): - self._metallicRoughnessTexture = self._format_texture(value, 'GB') + self._metallicRoughnessTexture = self._format_texture(value, "GB") self._tex_flags = None def _compute_tex_flags(self): @@ -480,15 +465,16 @@ def _compute_tex_flags(self): return tex_flags def _compute_transparency(self): - if self.alphaMode == 'OPAQUE': + if self.alphaMode == "OPAQUE": return False cutoff = self.alphaCutoff - if self.alphaMode == 'BLEND': + if self.alphaMode == "BLEND": cutoff = 1.0 if self.baseColorFactor[3] < cutoff: return True - if (self.baseColorTexture is not None and - self.baseColorTexture.is_transparent(cutoff)): + if self.baseColorTexture is not None and self.baseColorTexture.is_transparent( + cutoff + ): return True return False @@ -583,22 +569,24 @@ class SpecularGlossinessMaterial(Material): linear space. """ - def __init__(self, - name=None, - normalTexture=None, - occlusionTexture=None, - emissiveTexture=None, - emissiveFactor=None, - alphaMode=None, - alphaCutoff=None, - doubleSided=False, - smooth=True, - wireframe=False, - diffuseFactor=None, - diffuseTexture=None, - specularFactor=None, - glossinessFactor=1.0, - specularGlossinessTexture=None): + def __init__( + self, + name=None, + normalTexture=None, + occlusionTexture=None, + emissiveTexture=None, + emissiveFactor=None, + alphaMode=None, + alphaCutoff=None, + doubleSided=False, + smooth=True, + wireframe=False, + diffuseFactor=None, + diffuseTexture=None, + specularFactor=None, + glossinessFactor=1.0, + specularGlossinessTexture=None, + ): super(SpecularGlossinessMaterial, self).__init__( name=name, normalTexture=normalTexture, @@ -609,7 +597,7 @@ def __init__(self, alphaCutoff=alphaCutoff, doubleSided=doubleSided, smooth=smooth, - wireframe=wireframe + wireframe=wireframe, ) # Set defaults @@ -626,8 +614,7 @@ def __init__(self, @property def diffuseFactor(self): - """(4,) float : The diffuse base color. - """ + """(4,) float : The diffuse base color.""" return self._diffuseFactor @diffuseFactor.setter @@ -636,19 +623,17 @@ def diffuseFactor(self, value): @property def diffuseTexture(self): - """(n,n,4) float or :class:`Texture` : The diffuse map. - """ + """(n,n,4) float or :class:`Texture` : The diffuse map.""" return self._diffuseTexture @diffuseTexture.setter def diffuseTexture(self, value): - self._diffuseTexture = self._format_texture(value, 'RGBA') + self._diffuseTexture = self._format_texture(value, "RGBA") self._tex_flags = None @property def specularFactor(self): - """(3,) float : The specular color of the material. - """ + """(3,) float : The specular color of the material.""" return self._specularFactor @specularFactor.setter @@ -657,25 +642,23 @@ def specularFactor(self, value): @property def glossinessFactor(self): - """float : The glossiness of the material. - """ + """float : The glossiness of the material.""" return self.glossinessFactor @glossinessFactor.setter def glossinessFactor(self, value): if value < 0 or value > 1: - raise ValueError('glossiness factor must be in range [0,1]') + raise ValueError("glossiness factor must be in range [0,1]") self._glossinessFactor = float(value) @property def specularGlossinessTexture(self): - """(n,n,4) or :class:`Texture` : The specular-glossiness texture. - """ + """(n,n,4) or :class:`Texture` : The specular-glossiness texture.""" return self._specularGlossinessTexture @specularGlossinessTexture.setter def specularGlossinessTexture(self, value): - self._specularGlossinessTexture = self._format_texture(value, 'GB') + self._specularGlossinessTexture = self._format_texture(value, "GB") self._tex_flags = None def _compute_tex_flags(self): @@ -687,15 +670,16 @@ def _compute_tex_flags(self): return flags def _compute_transparency(self): - if self.alphaMode == 'OPAQUE': + if self.alphaMode == "OPAQUE": return False cutoff = self.alphaCutoff - if self.alphaMode == 'BLEND': + if self.alphaMode == "BLEND": cutoff = 1.0 if self.diffuseFactor[3] < cutoff: return True - if (self.diffuseTexture is not None and - self.diffuseTexture.is_transparent(cutoff)): + if self.diffuseTexture is not None and self.diffuseTexture.is_transparent( + cutoff + ): return True return False diff --git a/pyrender/mesh.py b/pyrender/mesh.py index 36833ea..6cc126e 100644 --- a/pyrender/mesh.py +++ b/pyrender/mesh.py @@ -38,8 +38,7 @@ def __init__(self, primitives, name=None, weights=None, is_visible=True): @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -61,8 +60,7 @@ def primitives(self, value): @property def weights(self): - """(k,) float : Weights to be applied to morph targets. - """ + """(k,) float : Weights to be applied to morph targets.""" return self._weights @weights.setter @@ -71,8 +69,7 @@ def weights(self, value): @property def is_visible(self): - """bool : Whether the mesh is visible. - """ + """bool : Whether the mesh is visible.""" return self._is_visible @is_visible.setter @@ -81,11 +78,11 @@ def is_visible(self, value): @property def bounds(self): - """(2,3) float : The axis-aligned bounds of the mesh. - """ + """(2,3) float : The axis-aligned bounds of the mesh.""" if self._bounds is None: - bounds = np.array([[np.infty, np.infty, np.infty], - [-np.infty, -np.infty, -np.infty]]) + bounds = np.array( + [[np.infty, np.infty, np.infty], [-np.infty, -np.infty, -np.infty]] + ) for p in self.primitives: bounds[0] = np.minimum(bounds[0], p.bounds[0]) bounds[1] = np.maximum(bounds[1], p.bounds[1]) @@ -101,28 +98,24 @@ def centroid(self): @property def extents(self): - """(3,) float : The lengths of the axes of the mesh's AABB. - """ + """(3,) float : The lengths of the axes of the mesh's AABB.""" return np.diff(self.bounds, axis=0).reshape(-1) @property def scale(self): - """(3,) float : The length of the diagonal of the mesh's AABB. - """ + """(3,) float : The length of the diagonal of the mesh's AABB.""" return np.linalg.norm(self.extents) @property def is_transparent(self): - """bool : If True, the mesh is partially-transparent. - """ + """bool : If True, the mesh is partially-transparent.""" for p in self.primitives: if p.is_transparent: return True return False @staticmethod - def from_points(points, colors=None, normals=None, - is_visible=True, poses=None): + def from_points(points, colors=None, normals=None, is_visible=True, poses=None): """Create a Mesh from a set of points. Parameters @@ -148,14 +141,15 @@ def from_points(points, colors=None, normals=None, normals=normals, color_0=colors, mode=GLTF.POINTS, - poses=poses + poses=poses, ) mesh = Mesh(primitives=[primitive], is_visible=is_visible) return mesh @staticmethod - def from_trimesh(mesh, material=None, is_visible=True, - poses=None, wireframe=False, smooth=True): + def from_trimesh( + mesh, material=None, is_visible=True, poses=None, wireframe=False, smooth=True + ): """Create a Mesh from a :class:`~trimesh.base.Trimesh`. Parameters @@ -187,8 +181,7 @@ def from_trimesh(mesh, material=None, is_visible=True, elif isinstance(mesh, trimesh.Trimesh): meshes = [mesh] else: - raise TypeError('Expected a Trimesh or a list, got a {}' - .format(type(mesh))) + raise TypeError("Expected a Trimesh or a list, got a {}".format(type(mesh))) primitives = [] for m in meshes: @@ -206,35 +199,39 @@ def from_trimesh(mesh, material=None, is_visible=True, normals = np.repeat(m.face_normals, 3, axis=0) # Compute colors, texture coords, and material properties - color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props(m, smooth=smooth, material=material) + color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props( + m, smooth=smooth, material=material + ) # Override if material is given. if material is not None: - #primitive_material = copy.copy(material) + # primitive_material = copy.copy(material) primitive_material = copy.deepcopy(material) # TODO if primitive_material is None: # Replace material with default if needed primitive_material = MetallicRoughnessMaterial( - alphaMode='BLEND', + alphaMode="BLEND", baseColorFactor=[0.3, 0.3, 0.3, 1.0], metallicFactor=0.2, - roughnessFactor=0.8 + roughnessFactor=0.8, ) primitive_material.wireframe = wireframe # Create the primitive - primitives.append(Primitive( - positions=positions, - normals=normals, - texcoord_0=texcoord_0, - color_0=color_0, - indices=indices, - material=primitive_material, - mode=GLTF.TRIANGLES, - poses=poses - )) + primitives.append( + Primitive( + positions=positions, + normals=normals, + texcoord_0=texcoord_0, + color_0=color_0, + indices=indices, + material=primitive_material, + mode=GLTF.TRIANGLES, + poses=poses, + ) + ) return Mesh(primitives=primitives, is_visible=is_visible) @@ -252,36 +249,34 @@ def _get_trimesh_props(mesh, smooth=False, material=None): # Process vertex colors if material is None: - if mesh.visual.kind == 'vertex': + if mesh.visual.kind == "vertex": vc = mesh.visual.vertex_colors.copy() if smooth: colors = vc else: - colors = vc[mesh.faces].reshape( - (3 * len(mesh.faces), vc.shape[1]) - ) + colors = vc[mesh.faces].reshape((3 * len(mesh.faces), vc.shape[1])) material = MetallicRoughnessMaterial( - alphaMode='BLEND', + alphaMode="BLEND", baseColorFactor=[1.0, 1.0, 1.0, 1.0], metallicFactor=0.2, - roughnessFactor=0.8 + roughnessFactor=0.8, ) # Process face colors - elif mesh.visual.kind == 'face': + elif mesh.visual.kind == "face": if smooth: - raise ValueError('Cannot use face colors with a smooth mesh') + raise ValueError("Cannot use face colors with a smooth mesh") else: colors = np.repeat(mesh.visual.face_colors, 3, axis=0) material = MetallicRoughnessMaterial( - alphaMode='BLEND', + alphaMode="BLEND", baseColorFactor=[1.0, 1.0, 1.0, 1.0], metallicFactor=0.2, - roughnessFactor=0.8 + roughnessFactor=0.8, ) # Process texture colors - if mesh.visual.kind == 'texture': + if mesh.visual.kind == "texture": # Configure UV coordinates if mesh.visual.uv is not None and len(mesh.visual.uv) != 0: uv = mesh.visual.uv.copy() @@ -302,22 +297,22 @@ def _get_trimesh_props(mesh, smooth=False, material=None): occlusionTexture=mat.occlusionTexture, emissiveTexture=mat.emissiveTexture, emissiveFactor=mat.emissiveFactor, - alphaMode='BLEND', + alphaMode="BLEND", baseColorFactor=mat.baseColorFactor, baseColorTexture=mat.baseColorTexture, metallicFactor=mat.metallicFactor, roughnessFactor=mat.roughnessFactor, metallicRoughnessTexture=mat.metallicRoughnessTexture, doubleSided=mat.doubleSided, - alphaCutoff=mat.alphaCutoff + alphaCutoff=mat.alphaCutoff, ) elif isinstance(mat, trimesh.visual.texture.SimpleMaterial): - glossiness = mat.kwargs.get('Ns', 1.0) + glossiness = mat.kwargs.get("Ns", 1.0) if isinstance(glossiness, list): glossiness = float(glossiness[0]) roughness = (2 / (glossiness + 2)) ** (1.0 / 4.0) material = MetallicRoughnessMaterial( - alphaMode='BLEND', + alphaMode="BLEND", roughnessFactor=roughness, baseColorFactor=mat.diffuse, baseColorTexture=mat.image, diff --git a/pyrender/node.py b/pyrender/node.py index 1f37f78..7dd5227 100644 --- a/pyrender/node.py +++ b/pyrender/node.py @@ -44,18 +44,20 @@ class Node(object): The light in this node. """ - def __init__(self, - name=None, - camera=None, - children=None, - skin=None, - matrix=None, - mesh=None, - rotation=None, - scale=None, - translation=None, - weights=None, - light=None): + def __init__( + self, + name=None, + camera=None, + children=None, + skin=None, + matrix=None, + mesh=None, + rotation=None, + scale=None, + translation=None, + weights=None, + light=None, + ): # Set defaults if children is None: children = [] @@ -87,8 +89,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -99,20 +100,18 @@ def name(self, value): @property def camera(self): - """:class:`Camera` : The camera in this node. - """ + """:class:`Camera` : The camera in this node.""" return self._camera @camera.setter def camera(self, value): if value is not None and not isinstance(value, Camera): - raise TypeError('Value must be a camera') + raise TypeError("Value must be a camera") self._camera = value @property def children(self): - """list of :class:`Node` : The children of this node. - """ + """list of :class:`Node` : The children of this node.""" return self._children @children.setter @@ -121,8 +120,7 @@ def children(self, value): @property def skin(self): - """int : The skin index for this node. - """ + """int : The skin index for this node.""" return self._skin @skin.setter @@ -131,69 +129,64 @@ def skin(self, value): @property def mesh(self): - """:class:`Mesh` : The mesh in this node. - """ + """:class:`Mesh` : The mesh in this node.""" return self._mesh @mesh.setter def mesh(self, value): if value is not None and not isinstance(value, Mesh): - raise TypeError('Value must be a mesh') + raise TypeError("Value must be a mesh") self._mesh = value @property def light(self): - """:class:`Light` : The light in this node. - """ + """:class:`Light` : The light in this node.""" return self._light @light.setter def light(self, value): if value is not None and not isinstance(value, Light): - raise TypeError('Value must be a light') + raise TypeError("Value must be a light") self._light = value @property def rotation(self): - """(4,) float : The xyzw quaternion for this node. - """ + """(4,) float : The xyzw quaternion for this node.""" return self._rotation @rotation.setter def rotation(self, value): value = np.asanyarray(value) if value.shape != (4,): - raise ValueError('Quaternion must be a (4,) vector') + raise ValueError("Quaternion must be a (4,) vector") if np.abs(np.linalg.norm(value) - 1.0) > 1e-3: - raise ValueError('Quaternion must have norm == 1.0') + raise ValueError("Quaternion must have norm == 1.0") self._rotation = value self._matrix = None @property def translation(self): - """(3,) float : The translation for this node. - """ + """(3,) float : The translation for this node.""" return self._translation @translation.setter def translation(self, value): value = np.asanyarray(value) if value.shape != (3,): - raise ValueError('Translation must be a (3,) vector') + raise ValueError("Translation must be a (3,) vector") self._translation = value self._matrix = None @property def scale(self): - """(3,) float : The scale for this node. - """ + """(3,) float : The scale for this node.""" return self._scale @scale.setter def scale(self, value): value = np.asanyarray(value) if value.shape != (3,): - raise ValueError('Scale must be a (3,) vector') + raise ValueError("Scale must be a (3,) vector") self._scale = value self._matrix = None @@ -206,18 +199,16 @@ def matrix(self): matrix, but not an individual element. """ if self._matrix is None: - self._matrix = self._m_from_tqs( - self.translation, self.rotation, self.scale - ) + self._matrix = self._m_from_tqs(self.translation, self.rotation, self.scale) return self._matrix.copy() @matrix.setter def matrix(self, value): value = np.asanyarray(value) - if value.shape != (4,4): - raise ValueError('Matrix must be a 4x4 numpy ndarray') - if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])): - raise ValueError('Bottom row of matrix must be [0,0,0,1]') + if value.shape != (4, 4): + raise ValueError("Matrix must be a 4x4 numpy ndarray") + if not np.allclose(value[3, :], np.array([0.0, 0.0, 0.0, 1.0])): + raise ValueError("Bottom row of matrix must be [0,0,0,1]") self.rotation = Node._q_from_m(value) self.scale = Node._s_from_m(value) self.translation = Node._t_from_m(value) @@ -225,39 +216,39 @@ def matrix(self, value): @staticmethod def _t_from_m(m): - return m[:3,3] + return m[:3, 3] @staticmethod def _r_from_m(m): - U = m[:3,:3] + U = m[:3, :3] norms = np.linalg.norm(U.T, axis=1) return U / norms @staticmethod def _q_from_m(m): M = np.eye(4) - M[:3,:3] = Node._r_from_m(m) + M[:3, :3] = Node._r_from_m(m) q_wxyz = transformations.quaternion_from_matrix(M) return np.roll(q_wxyz, -1) @staticmethod def _s_from_m(m): - return np.linalg.norm(m[:3,:3].T, axis=1) + return np.linalg.norm(m[:3, :3].T, axis=1) @staticmethod def _r_from_q(q): q_wxyz = np.roll(q, 1) - return transformations.quaternion_matrix(q_wxyz)[:3,:3] + return transformations.quaternion_matrix(q_wxyz)[:3, :3] @staticmethod def _m_from_tqs(t, q, s): S = np.eye(4) - S[:3,:3] = np.diag(s) + S[:3, :3] = np.diag(s) R = np.eye(4) - R[:3,:3] = Node._r_from_q(q) + R[:3, :3] = Node._r_from_q(q) T = np.eye(4) - T[:3,3] = t + T[:3, 3] = t return T.dot(R.dot(S)) diff --git a/pyrender/offscreen.py b/pyrender/offscreen.py index 3401429..d9ee7d4 100644 --- a/pyrender/offscreen.py +++ b/pyrender/offscreen.py @@ -32,8 +32,7 @@ def __init__(self, viewport_width, viewport_height, point_size=1.0): @property def viewport_width(self): - """int : The width of the main viewport, in pixels. - """ + """int : The width of the main viewport, in pixels.""" return self._viewport_width @viewport_width.setter @@ -42,8 +41,7 @@ def viewport_width(self, value): @property def viewport_height(self): - """int : The height of the main viewport, in pixels. - """ + """int : The height of the main viewport, in pixels.""" return self._viewport_height @viewport_height.setter @@ -52,8 +50,7 @@ def viewport_height(self, value): @property def point_size(self): - """float : The pixel size of points in point clouds. - """ + """float : The pixel size of points in point clouds.""" return self._point_size @point_size.setter @@ -86,8 +83,10 @@ def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None): self._platform.make_current() # If platform does not support dynamically-resizing framebuffers, # destroy it and restart it - if (self._platform.viewport_height != self.viewport_height or - self._platform.viewport_width != self.viewport_width): + if ( + self._platform.viewport_height != self.viewport_height + or self._platform.viewport_width != self.viewport_width + ): if not self._platform.supports_framebuffers(): self.delete() self._create() @@ -114,8 +113,7 @@ def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None): return retval def delete(self): - """Free all OpenGL resources. - """ + """Free all OpenGL resources.""" self._platform.make_current() self._renderer.delete() self._platform.delete_context() @@ -124,28 +122,32 @@ def delete(self): self._renderer = None self._platform = None import gc + gc.collect() def _create(self): - if 'PYOPENGL_PLATFORM' not in os.environ: + if "PYOPENGL_PLATFORM" not in os.environ: from pyrender.platforms.pyglet_platform import PygletPlatform - self._platform = PygletPlatform(self.viewport_width, - self.viewport_height) - elif os.environ['PYOPENGL_PLATFORM'] == 'egl': + + self._platform = PygletPlatform(self.viewport_width, self.viewport_height) + elif os.environ["PYOPENGL_PLATFORM"] == "egl": from pyrender.platforms import egl - device_id = int(os.environ.get('EGL_DEVICE_ID', '0')) + + device_id = int(os.environ.get("EGL_DEVICE_ID", "0")) egl_device = egl.get_device_by_index(device_id) - self._platform = egl.EGLPlatform(self.viewport_width, - self.viewport_height, - device=egl_device) - elif os.environ['PYOPENGL_PLATFORM'] == 'osmesa': + self._platform = egl.EGLPlatform( + self.viewport_width, self.viewport_height, device=egl_device + ) + elif os.environ["PYOPENGL_PLATFORM"] == "osmesa": from pyrender.platforms.osmesa import OSMesaPlatform - self._platform = OSMesaPlatform(self.viewport_width, - self.viewport_height) + + self._platform = OSMesaPlatform(self.viewport_width, self.viewport_height) else: - raise ValueError('Unsupported PyOpenGL platform: {}'.format( - os.environ['PYOPENGL_PLATFORM'] - )) + raise ValueError( + "Unsupported PyOpenGL platform: {}".format( + os.environ["PYOPENGL_PLATFORM"] + ) + ) self._platform.init_context() self._platform.make_current() self._renderer = Renderer(self.viewport_width, self.viewport_height) @@ -157,4 +159,4 @@ def __del__(self): pass -__all__ = ['OffscreenRenderer'] +__all__ = ["OffscreenRenderer"] diff --git a/pyrender/platforms/__init__.py b/pyrender/platforms/__init__.py index 7837fd5..0c164e8 100644 --- a/pyrender/platforms/__init__.py +++ b/pyrender/platforms/__init__.py @@ -3,4 +3,4 @@ Author: Matthew Matl """ -from .base import Platform +from .base import Platform # noqa: F401 diff --git a/pyrender/platforms/base.py b/pyrender/platforms/base.py index c9ecda9..d4671d8 100644 --- a/pyrender/platforms/base.py +++ b/pyrender/platforms/base.py @@ -21,8 +21,7 @@ def __init__(self, viewport_width, viewport_height): @property def viewport_width(self): - """int : The width of the main viewport, in pixels. - """ + """int : The width of the main viewport, in pixels.""" return self._viewport_width @viewport_width.setter @@ -31,8 +30,7 @@ def viewport_width(self, value): @property def viewport_height(self): - """int : The height of the main viewport, in pixels. - """ + """int : The height of the main viewport, in pixels.""" return self._viewport_height @viewport_height.setter @@ -41,32 +39,27 @@ def viewport_height(self, value): @abc.abstractmethod def init_context(self): - """Create an OpenGL context. - """ + """Create an OpenGL context.""" pass @abc.abstractmethod def make_current(self): - """Make the OpenGL context current. - """ + """Make the OpenGL context current.""" pass @abc.abstractmethod def make_uncurrent(self): - """Make the OpenGL context uncurrent. - """ + """Make the OpenGL context uncurrent.""" pass @abc.abstractmethod def delete_context(self): - """Delete the OpenGL context. - """ + """Delete the OpenGL context.""" pass @abc.abstractmethod def supports_framebuffers(self): - """Returns True if the method supports framebuffer rendering. - """ + """Returns True if the method supports framebuffer rendering.""" pass def __del__(self): diff --git a/pyrender/platforms/egl.py b/pyrender/platforms/egl.py index ae2478d..f81cd07 100644 --- a/pyrender/platforms/egl.py +++ b/pyrender/platforms/egl.py @@ -10,7 +10,7 @@ def _ensure_egl_loaded(): - plugin = OpenGL.platform.PlatformPlugin.by_name('egl') + plugin = OpenGL.platform.PlatformPlugin.by_name("egl") if plugin is None: raise RuntimeError("EGL platform plugin is not available.") @@ -23,7 +23,7 @@ def _ensure_egl_loaded(): _ensure_egl_loaded() -from OpenGL import EGL as egl +from OpenGL import EGL as egl # noqa: E402 def _get_egl_func(func_name, res_type, *arg_types): @@ -39,14 +39,15 @@ def _get_egl_func(func_name, res_type, *arg_types): def _get_egl_struct(struct_name): from OpenGL._opaque import opaque_pointer_cls + return opaque_pointer_cls(struct_name) # These are not defined in PyOpenGL by default. -_EGLDeviceEXT = _get_egl_struct('EGLDeviceEXT') -_eglGetPlatformDisplayEXT = _get_egl_func('eglGetPlatformDisplayEXT', egl.EGLDisplay) -_eglQueryDevicesEXT = _get_egl_func('eglQueryDevicesEXT', egl.EGLBoolean) -_eglQueryDeviceStringEXT = _get_egl_func('eglQueryDeviceStringEXT', ctypes.c_char_p) +_EGLDeviceEXT = _get_egl_struct("EGLDeviceEXT") +_eglGetPlatformDisplayEXT = _get_egl_func("eglGetPlatformDisplayEXT", egl.EGLDisplay) +_eglQueryDevicesEXT = _get_egl_func("eglQueryDevicesEXT", egl.EGLBoolean) +_eglQueryDeviceStringEXT = _get_egl_func("eglQueryDeviceStringEXT", ctypes.c_char_p) def query_devices(): @@ -59,7 +60,9 @@ def query_devices(): return [] devices = (_EGLDeviceEXT * num_devices.value)() # array of size num_devices - success = _eglQueryDevicesEXT(num_devices.value, devices, ctypes.pointer(num_devices)) + success = _eglQueryDevicesEXT( + num_devices.value, devices, ctypes.pointer(num_devices) + ) if not success or num_devices.value < 1: return [] @@ -80,12 +83,11 @@ def get_device_by_index(device_id): devices = query_devices() if device_id >= len(devices): - raise ValueError('Invalid device ID ({})'.format(device_id, len(devices))) + raise ValueError("Invalid device ID ({})".format(device_id)) return devices[device_id] class EGLDevice: - def __init__(self, display=None): self._display = display @@ -98,21 +100,20 @@ def get_display(self): @property def name(self): if self._display is None: - return 'default' + return "default" name = _eglQueryDeviceStringEXT(self._display, EGL_DRM_DEVICE_FILE_EXT) if name is None: return None - return name.decode('ascii') + return name.decode("ascii") def __repr__(self): return "".format(self.name) class EGLPlatform(Platform): - """Renders using EGL. - """ + """Renders using EGL.""" def __init__(self, viewport_width, viewport_height, device: EGLDevice = None): super(EGLPlatform, self).__init__(viewport_width, viewport_height) @@ -126,52 +127,80 @@ def __init__(self, viewport_width, viewport_height, device: EGLDevice = None): def init_context(self): _ensure_egl_loaded() - from OpenGL.EGL import ( - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE, - EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_DEPTH_SIZE, - EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_CONFORMANT, - EGL_NONE, EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, - EGL_OPENGL_API, EGL_CONTEXT_MAJOR_VERSION, + from OpenGL.EGL import ( # noqa: F401 + EGL_SURFACE_TYPE, + EGL_PBUFFER_BIT, + EGL_BLUE_SIZE, + EGL_RED_SIZE, + EGL_GREEN_SIZE, + EGL_DEPTH_SIZE, + EGL_COLOR_BUFFER_TYPE, + EGL_RGB_BUFFER, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_BIT, + EGL_CONFORMANT, + EGL_NONE, + EGL_DEFAULT_DISPLAY, + EGL_NO_CONTEXT, + EGL_OPENGL_API, + EGL_CONTEXT_MAJOR_VERSION, EGL_CONTEXT_MINOR_VERSION, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - eglGetDisplay, eglInitialize, eglChooseConfig, - eglBindAPI, eglCreateContext, EGLConfig + eglGetDisplay, + eglInitialize, + eglChooseConfig, + eglBindAPI, + eglCreateContext, + EGLConfig, ) from OpenGL import arrays - config_attributes = arrays.GLintArray.asArray([ - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_BLUE_SIZE, 8, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_DEPTH_SIZE, 24, - EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_CONFORMANT, EGL_OPENGL_BIT, - EGL_NONE - ]) - context_attributes = arrays.GLintArray.asArray([ - EGL_CONTEXT_MAJOR_VERSION, 4, - EGL_CONTEXT_MINOR_VERSION, 1, - EGL_CONTEXT_OPENGL_PROFILE_MASK, - EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - EGL_NONE - ]) + config_attributes = arrays.GLintArray.asArray( + [ + EGL_SURFACE_TYPE, + EGL_PBUFFER_BIT, + EGL_BLUE_SIZE, + 8, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_DEPTH_SIZE, + 24, + EGL_COLOR_BUFFER_TYPE, + EGL_RGB_BUFFER, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_BIT, + EGL_CONFORMANT, + EGL_OPENGL_BIT, + EGL_NONE, + ] + ) + context_attributes = arrays.GLintArray.asArray( + [ + EGL_CONTEXT_MAJOR_VERSION, + 4, + EGL_CONTEXT_MINOR_VERSION, + 1, + EGL_CONTEXT_OPENGL_PROFILE_MASK, + EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE, + ] + ) major, minor = ctypes.c_long(), ctypes.c_long() num_configs = ctypes.c_long() configs = (EGLConfig * 1)() # Cache DISPLAY if necessary and get an off-screen EGL display orig_dpy = None - if 'DISPLAY' in os.environ: - orig_dpy = os.environ['DISPLAY'] - del os.environ['DISPLAY'] + if "DISPLAY" in os.environ: + orig_dpy = os.environ["DISPLAY"] + del os.environ["DISPLAY"] self._egl_display = self._egl_device.get_display() if orig_dpy is not None: - os.environ['DISPLAY'] = orig_dpy + os.environ["DISPLAY"] = orig_dpy # Initialize EGL assert eglInitialize(self._egl_display, major, minor) @@ -184,8 +213,7 @@ def init_context(self): # Create an EGL context self._egl_context = eglCreateContext( - self._egl_display, configs[0], - EGL_NO_CONTEXT, context_attributes + self._egl_display, configs[0], EGL_NO_CONTEXT, context_attributes ) # Make it current @@ -193,18 +221,18 @@ def init_context(self): def make_current(self): from OpenGL.EGL import eglMakeCurrent, EGL_NO_SURFACE + assert eglMakeCurrent( - self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self._egl_context + self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self._egl_context ) def make_uncurrent(self): - """Make the OpenGL context uncurrent. - """ + """Make the OpenGL context uncurrent.""" pass def delete_context(self): from OpenGL.EGL import eglDestroyContext, eglTerminate + if self._egl_display is not None: if self._egl_context is not None: eglDestroyContext(self._egl_display, self._egl_context) @@ -216,4 +244,4 @@ def supports_framebuffers(self): return True -__all__ = ['EGLPlatform'] +__all__ = ["EGLPlatform"] diff --git a/pyrender/platforms/osmesa.py b/pyrender/platforms/osmesa.py index deaa5ff..e1e3ca7 100644 --- a/pyrender/platforms/osmesa.py +++ b/pyrender/platforms/osmesa.py @@ -1,7 +1,7 @@ from .base import Platform -__all__ = ['OSMesaPlatform'] +__all__ = ["OSMesaPlatform"] class OSMesaPlatform(Platform): @@ -17,20 +17,31 @@ def __init__(self, viewport_width, viewport_height): def init_context(self): from OpenGL import arrays from OpenGL.osmesa import ( - OSMesaCreateContextAttribs, OSMESA_FORMAT, - OSMESA_RGBA, OSMESA_PROFILE, OSMESA_CORE_PROFILE, - OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION, - OSMESA_DEPTH_BITS + OSMesaCreateContextAttribs, + OSMESA_FORMAT, + OSMESA_RGBA, + OSMESA_PROFILE, + OSMESA_CORE_PROFILE, + OSMESA_CONTEXT_MAJOR_VERSION, + OSMESA_CONTEXT_MINOR_VERSION, + OSMESA_DEPTH_BITS, ) - attrs = arrays.GLintArray.asArray([ - OSMESA_FORMAT, OSMESA_RGBA, - OSMESA_DEPTH_BITS, 24, - OSMESA_PROFILE, OSMESA_CORE_PROFILE, - OSMESA_CONTEXT_MAJOR_VERSION, 3, - OSMESA_CONTEXT_MINOR_VERSION, 3, - 0 - ]) + attrs = arrays.GLintArray.asArray( + [ + OSMESA_FORMAT, + OSMESA_RGBA, + OSMESA_DEPTH_BITS, + 24, + OSMESA_PROFILE, + OSMESA_CORE_PROFILE, + OSMESA_CONTEXT_MAJOR_VERSION, + 3, + OSMESA_CONTEXT_MINOR_VERSION, + 3, + 0, + ] + ) self._context = OSMesaCreateContextAttribs(attrs, None) self._buffer = arrays.GLubyteArray.zeros( (self.viewport_height, self.viewport_width, 4) @@ -39,18 +50,22 @@ def init_context(self): def make_current(self): from OpenGL import GL as gl from OpenGL.osmesa import OSMesaMakeCurrent - assert(OSMesaMakeCurrent( - self._context, self._buffer, gl.GL_UNSIGNED_BYTE, - self.viewport_width, self.viewport_height - )) + + assert OSMesaMakeCurrent( + self._context, + self._buffer, + gl.GL_UNSIGNED_BYTE, + self.viewport_width, + self.viewport_height, + ) def make_uncurrent(self): - """Make the OpenGL context uncurrent. - """ + """Make the OpenGL context uncurrent.""" pass def delete_context(self): from OpenGL.osmesa import OSMesaDestroyContext + OSMesaDestroyContext(self._context) self._context = None self._buffer = None diff --git a/pyrender/platforms/pyglet_platform.py b/pyrender/platforms/pyglet_platform.py index a70cf7b..c893b55 100644 --- a/pyrender/platforms/pyglet_platform.py +++ b/pyrender/platforms/pyglet_platform.py @@ -1,11 +1,15 @@ -from pyrender.constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR, - MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR) +from pyrender.constants import ( + TARGET_OPEN_GL_MAJOR, + TARGET_OPEN_GL_MINOR, + MIN_OPEN_GL_MAJOR, + MIN_OPEN_GL_MINOR, +) from .base import Platform import OpenGL -__all__ = ['PygletPlatform'] +__all__ = ["PygletPlatform"] class PygletPlatform(Platform): @@ -19,7 +23,8 @@ def __init__(self, viewport_width, viewport_height): def init_context(self): import pyglet - pyglet.options['shadow_window'] = False + + pyglet.options["shadow_window"] = False try: pyglet.lib.x11.xlib.XInitThreads() @@ -27,39 +32,52 @@ def init_context(self): pass self._window = None - confs = [pyglet.gl.Config(sample_buffers=1, samples=4, - depth_size=24, - double_buffer=True, - major_version=TARGET_OPEN_GL_MAJOR, - minor_version=TARGET_OPEN_GL_MINOR), - pyglet.gl.Config(depth_size=24, - double_buffer=True, - major_version=TARGET_OPEN_GL_MAJOR, - minor_version=TARGET_OPEN_GL_MINOR), - pyglet.gl.Config(sample_buffers=1, samples=4, - depth_size=24, - double_buffer=True, - major_version=MIN_OPEN_GL_MAJOR, - minor_version=MIN_OPEN_GL_MINOR), - pyglet.gl.Config(depth_size=24, - double_buffer=True, - major_version=MIN_OPEN_GL_MAJOR, - minor_version=MIN_OPEN_GL_MINOR)] + confs = [ + pyglet.gl.Config( + sample_buffers=1, + samples=4, + depth_size=24, + double_buffer=True, + major_version=TARGET_OPEN_GL_MAJOR, + minor_version=TARGET_OPEN_GL_MINOR, + ), + pyglet.gl.Config( + depth_size=24, + double_buffer=True, + major_version=TARGET_OPEN_GL_MAJOR, + minor_version=TARGET_OPEN_GL_MINOR, + ), + pyglet.gl.Config( + sample_buffers=1, + samples=4, + depth_size=24, + double_buffer=True, + major_version=MIN_OPEN_GL_MAJOR, + minor_version=MIN_OPEN_GL_MINOR, + ), + pyglet.gl.Config( + depth_size=24, + double_buffer=True, + major_version=MIN_OPEN_GL_MAJOR, + minor_version=MIN_OPEN_GL_MINOR, + ), + ] + error_messages = [] for conf in confs: try: - self._window = pyglet.window.Window(config=conf, visible=False, - resizable=False, - width=1, height=1) + self._window = pyglet.window.Window( + config=conf, visible=False, resizable=False, width=1, height=1 + ) break except pyglet.window.NoSuchConfigException as e: - pass + error_messages.append(str(e)) if not self._window: raise ValueError( - 'Failed to initialize Pyglet window with an OpenGL >= 3+ ' - 'context. If you\'re logged in via SSH, ensure that you\'re ' - 'running your script with vglrun (i.e. VirtualGL). The ' - 'internal error message was "{}"'.format(e) + "Failed to initialize Pyglet window with an OpenGL >= 3+ " + "context. If you're logged in via SSH, ensure that you're " + "running your script with vglrun (i.e. VirtualGL). The " + 'internal error message was "{}"'.format("\n".join(error_messages)) ) def make_current(self): @@ -69,7 +87,10 @@ def make_current(self): def make_uncurrent(self): try: import pyglet - pyglet.gl.xlib.glx.glXMakeContextCurrent(self._window.context.x_display, 0, 0, None) + + pyglet.gl.xlib.glx.glXMakeContextCurrent( + self._window.context.x_display, 0, 0, None + ) except Exception: pass diff --git a/pyrender/primitive.py b/pyrender/primitive.py index 7f83f46..33ad48e 100644 --- a/pyrender/primitive.py +++ b/pyrender/primitive.py @@ -54,20 +54,22 @@ class Primitive(object): Array of 4x4 transformation matrices for instancing this object. """ - def __init__(self, - positions, - normals=None, - tangents=None, - texcoord_0=None, - texcoord_1=None, - color_0=None, - joints_0=None, - weights_0=None, - indices=None, - material=None, - mode=None, - targets=None, - poses=None): + def __init__( + self, + positions, + normals=None, + tangents=None, + texcoord_0=None, + texcoord_1=None, + color_0=None, + joints_0=None, + weights_0=None, + indices=None, + material=None, + mode=None, + targets=None, + poses=None, + ): if mode is None: mode = GLTF.TRIANGLES @@ -94,8 +96,7 @@ def __init__(self, @property def positions(self): - """(n,3) float : XYZ vertex positions. - """ + """(n,3) float : XYZ vertex positions.""" return self._positions @positions.setter @@ -106,8 +107,7 @@ def positions(self, value): @property def normals(self): - """(n,3) float : Normalized XYZ vertex normals. - """ + """(n,3) float : Normalized XYZ vertex normals.""" return self._normals @normals.setter @@ -116,13 +116,12 @@ def normals(self, value): value = np.asanyarray(value, dtype=np.float32) value = np.ascontiguousarray(value) if value.shape != self.positions.shape: - raise ValueError('Incorrect normals shape') + raise ValueError("Incorrect normals shape") self._normals = value @property def tangents(self): - """(n,4) float : XYZW vertex tangents. - """ + """(n,4) float : XYZW vertex tangents.""" return self._tangents @tangents.setter @@ -131,13 +130,12 @@ def tangents(self, value): value = np.asanyarray(value, dtype=np.float32) value = np.ascontiguousarray(value) if value.shape != (self.positions.shape[0], 4): - raise ValueError('Incorrect tangent shape') + raise ValueError("Incorrect tangent shape") self._tangents = value @property def texcoord_0(self): - """(n,2) float : The first set of UV texture coordinates. - """ + """(n,2) float : The first set of UV texture coordinates.""" return self._texcoord_0 @texcoord_0.setter @@ -145,17 +143,19 @@ def texcoord_0(self, value): if value is not None: value = np.asanyarray(value, dtype=np.float32) value = np.ascontiguousarray(value) - if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or - value.shape[1] < 2): - raise ValueError('Incorrect texture coordinate shape') + if ( + value.ndim != 2 + or value.shape[0] != self.positions.shape[0] + or value.shape[1] < 2 + ): + raise ValueError("Incorrect texture coordinate shape") if value.shape[1] > 2: - value = value[:,:2] + value = value[:, :2] self._texcoord_0 = value @property def texcoord_1(self): - """(n,2) float : The second set of UV texture coordinates. - """ + """(n,2) float : The second set of UV texture coordinates.""" return self._texcoord_1 @texcoord_1.setter @@ -163,15 +163,17 @@ def texcoord_1(self, value): if value is not None: value = np.asanyarray(value, dtype=np.float32) value = np.ascontiguousarray(value) - if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or - value.shape[1] != 2): - raise ValueError('Incorrect texture coordinate shape') + if ( + value.ndim != 2 + or value.shape[0] != self.positions.shape[0] + or value.shape[1] != 2 + ): + raise ValueError("Incorrect texture coordinate shape") self._texcoord_1 = value @property def color_0(self): - """(n,4) float : RGBA vertex colors. - """ + """(n,4) float : RGBA vertex colors.""" return self._color_0 @color_0.setter @@ -185,8 +187,7 @@ def color_0(self, value): @property def joints_0(self): - """(n,4) float : Joint information. - """ + """(n,4) float : Joint information.""" return self._joints_0 @joints_0.setter @@ -195,8 +196,7 @@ def joints_0(self, value): @property def weights_0(self): - """(n,4) float : Weight information for morphing. - """ + """(n,4) float : Weight information for morphing.""" return self._weights_0 @weights_0.setter @@ -205,8 +205,7 @@ def weights_0(self, value): @property def indices(self): - """(m,3) int : Face indices for triangle meshes or fans. - """ + """(m,3) int : Face indices for triangle meshes or fans.""" return self._indices @indices.setter @@ -218,8 +217,7 @@ def indices(self, value): @property def material(self): - """:class:`Material` : The material for this primitive. - """ + """:class:`Material` : The material for this primitive.""" return self._material @material.setter @@ -229,26 +227,24 @@ def material(self, value): value = MetallicRoughnessMaterial() else: if not isinstance(value, Material): - raise TypeError('Object material must be of type Material') + raise TypeError("Object material must be of type Material") self._material = value @property def mode(self): - """int : The type of primitive to render. - """ + """int : The type of primitive to render.""" return self._mode @mode.setter def mode(self, value): value = int(value) if value < GLTF.POINTS or value > GLTF.TRIANGLE_FAN: - raise ValueError('Invalid mode') + raise ValueError("Invalid mode") self._mode = value @property def targets(self): - """(k,) int : Morph target indices. - """ + """(k,) int : Morph target indices.""" return self._targets @targets.setter @@ -257,8 +253,7 @@ def targets(self, value): @property def poses(self): - """(x,4,4) float : Homogenous transforms for instancing this primitive. - """ + """(x,4,4) float : Homogenous transforms for instancing this primitive.""" return self._poses @poses.setter @@ -267,10 +262,12 @@ def poses(self, value): value = np.asanyarray(value, dtype=np.float32) value = np.ascontiguousarray(value) if value.ndim == 2: - value = value[np.newaxis,:,:] + value = value[np.newaxis, :, :] if value.shape[1] != 4 or value.shape[2] != 4: - raise ValueError('Pose matrices must be of shape (n,4,4), ' - 'got {}'.format(value.shape)) + raise ValueError( + "Pose matrices must be of shape (n,4,4), " + "got {}".format(value.shape) + ) self._poses = value self._bounds = None @@ -282,26 +279,22 @@ def bounds(self): @property def centroid(self): - """(3,) float : The centroid of the primitive's AABB. - """ + """(3,) float : The centroid of the primitive's AABB.""" return np.mean(self.bounds, axis=0) @property def extents(self): - """(3,) float : The lengths of the axes of the primitive's AABB. - """ + """(3,) float : The lengths of the axes of the primitive's AABB.""" return np.diff(self.bounds, axis=0).reshape(-1) @property def scale(self): - """(3,) float : The length of the diagonal of the primitive's AABB. - """ + """(3,) float : The length of the diagonal of the primitive's AABB.""" return np.linalg.norm(self.extents) @property def buf_flags(self): - """int : The flags for the render buffer. - """ + """int : The flags for the render buffer.""" if self._buf_flags is None: self._buf_flags = self._compute_buf_flags() return self._buf_flags @@ -312,13 +305,12 @@ def delete(self): @property def is_transparent(self): - """bool : If True, the mesh is partially-transparent. - """ + """bool : If True, the mesh is partially-transparent.""" return self._compute_transparency() def _add_to_context(self): if self._vaid is not None: - raise ValueError('Mesh is already bound to a context') + raise ValueError("Mesh is already bound to a context") # Generate and bind VAO self._vaid = glGenVertexArrays(1) @@ -364,19 +356,20 @@ def _add_to_context(self): # PASS # Copy data to buffer - vertex_data = np.ascontiguousarray( - vertex_data.flatten().astype(np.float32) - ) + vertex_data = np.ascontiguousarray(vertex_data.flatten().astype(np.float32)) glBufferData( - GL_ARRAY_BUFFER, FLOAT_SZ * len(vertex_data), - vertex_data, GL_STATIC_DRAW + GL_ARRAY_BUFFER, FLOAT_SZ * len(vertex_data), vertex_data, GL_STATIC_DRAW ) total_sz = sum(attr_sizes) offset = 0 for i, sz in enumerate(attr_sizes): glVertexAttribPointer( - i, sz, GL_FLOAT, GL_FALSE, FLOAT_SZ * total_sz, - ctypes.c_void_p(FLOAT_SZ * offset) + i, + sz, + GL_FLOAT, + GL_FALSE, + FLOAT_SZ * total_sz, + ctypes.c_void_p(FLOAT_SZ * offset), ) glEnableVertexAttribArray(i) offset += sz @@ -387,27 +380,28 @@ def _add_to_context(self): if self.poses is not None: pose_data = np.ascontiguousarray( - np.transpose(self.poses, [0,2,1]).flatten().astype(np.float32) + np.transpose(self.poses, [0, 2, 1]).flatten().astype(np.float32) ) else: - pose_data = np.ascontiguousarray( - np.eye(4).flatten().astype(np.float32) - ) + pose_data = np.ascontiguousarray(np.eye(4).flatten().astype(np.float32)) modelbuffer = glGenBuffers(1) self._buffers.append(modelbuffer) glBindBuffer(GL_ARRAY_BUFFER, modelbuffer) glBufferData( - GL_ARRAY_BUFFER, FLOAT_SZ * len(pose_data), - pose_data, GL_STATIC_DRAW + GL_ARRAY_BUFFER, FLOAT_SZ * len(pose_data), pose_data, GL_STATIC_DRAW ) for i in range(0, 4): idx = i + len(attr_sizes) glEnableVertexAttribArray(idx) glVertexAttribPointer( - idx, 4, GL_FLOAT, GL_FALSE, FLOAT_SZ * 4 * 4, - ctypes.c_void_p(4 * FLOAT_SZ * i) + idx, + 4, + GL_FLOAT, + GL_FALSE, + FLOAT_SZ * 4 * 4, + ctypes.c_void_p(4 * FLOAT_SZ * i), ) glVertexAttribDivisor(idx, 1) @@ -418,9 +412,12 @@ def _add_to_context(self): elementbuffer = glGenBuffers(1) self._buffers.append(elementbuffer) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer) - glBufferData(GL_ELEMENT_ARRAY_BUFFER, UINT_SZ * self.indices.size, - self.indices.flatten().astype(np.uint32), - GL_STATIC_DRAW) + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + UINT_SZ * self.indices.size, + self.indices.flatten().astype(np.uint32), + GL_STATIC_DRAW, + ) glBindVertexArray(0) @@ -436,35 +433,39 @@ def _in_context(self): def _bind(self): if self._vaid is None: - raise ValueError('Cannot bind a Mesh that has not been added ' - 'to a context') + raise ValueError( + "Cannot bind a Mesh that has not been added " "to a context" + ) glBindVertexArray(self._vaid) def _unbind(self): glBindVertexArray(0) def _compute_bounds(self): - """Compute the bounds of this object. - """ + """Compute the bounds of this object.""" # Compute bounds of this object - bounds = np.array([np.min(self.positions, axis=0), - np.max(self.positions, axis=0)]) + bounds = np.array( + [np.min(self.positions, axis=0), np.max(self.positions, axis=0)] + ) # If instanced, compute translations for approximate bounds if self.poses is not None: - bounds += np.array([np.min(self.poses[:,:3,3], axis=0), - np.max(self.poses[:,:3,3], axis=0)]) + bounds += np.array( + [ + np.min(self.poses[:, :3, 3], axis=0), + np.max(self.poses[:, :3, 3], axis=0), + ] + ) return bounds def _compute_transparency(self): - """Compute whether or not this object is transparent. - """ + """Compute whether or not this object is transparent.""" if self.material.is_transparent: return True if self._is_transparent is None: self._is_transparent = False if self.color_0 is not None: - if np.any(self._color_0[:,3] != 1.0): + if np.any(self._color_0[:, 3] != 1.0): self._is_transparent = True return self._is_transparent diff --git a/pyrender/renderer.py b/pyrender/renderer.py index f212907..6506cc2 100644 --- a/pyrender/renderer.py +++ b/pyrender/renderer.py @@ -7,9 +7,18 @@ import numpy as np import PIL -from .constants import (RenderFlags, TextAlign, GLTF, BufFlags, TexFlags, - ProgramFlags, DEFAULT_Z_FAR, DEFAULT_Z_NEAR, - SHADOW_TEX_SZ, MAX_N_LIGHTS) +from .constants import ( + RenderFlags, + TextAlign, + GLTF, + BufFlags, + TexFlags, + ProgramFlags, + DEFAULT_Z_FAR, + DEFAULT_Z_NEAR, + SHADOW_TEX_SZ, + MAX_N_LIGHTS, +) from .shader_program import ShaderProgramCache from .material import MetallicRoughnessMaterial, SpecularGlossinessMaterial from .light import PointLight, SpotLight, DirectionalLight @@ -40,7 +49,7 @@ class Renderer(object): def __init__(self, viewport_width, viewport_height, point_size=1.0): self.dpscale = 1 # Scaling needed on retina displays - if sys.platform == 'darwin': + if sys.platform == "darwin": self.dpscale = 2 self.viewport_width = viewport_width @@ -69,8 +78,7 @@ def __init__(self, viewport_width, viewport_height, point_size=1.0): @property def viewport_width(self): - """int : The width of the main viewport, in pixels. - """ + """int : The width of the main viewport, in pixels.""" return self._viewport_width @viewport_width.setter @@ -79,8 +87,7 @@ def viewport_width(self, value): @property def viewport_height(self): - """int : The height of the main viewport, in pixels. - """ + """int : The height of the main viewport, in pixels.""" return self._viewport_height @viewport_height.setter @@ -89,8 +96,7 @@ def viewport_height(self, value): @property def point_size(self): - """float : The size of screen-space points, in pixels. - """ + """float : The size of screen-space points, in pixels.""" return self._point_size @point_size.setter @@ -128,14 +134,17 @@ def render(self, scene, flags, seg_node_map=None): if not bool(flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.SEG): for ln in scene.light_nodes: take_pass = False - if (isinstance(ln.light, DirectionalLight) and - bool(flags & RenderFlags.SHADOWS_DIRECTIONAL)): + if isinstance(ln.light, DirectionalLight) and bool( + flags & RenderFlags.SHADOWS_DIRECTIONAL + ): take_pass = True - elif (isinstance(ln.light, SpotLight) and - bool(flags & RenderFlags.SHADOWS_SPOT)): + elif isinstance(ln.light, SpotLight) and bool( + flags & RenderFlags.SHADOWS_SPOT + ): take_pass = True - elif (isinstance(ln.light, PointLight) and - bool(flags & RenderFlags.SHADOWS_POINT)): + elif isinstance(ln.light, PointLight) and bool( + flags & RenderFlags.SHADOWS_POINT + ): take_pass = True if take_pass: self._shadow_mapping_pass(scene, ln, flags) @@ -153,9 +162,17 @@ def render(self, scene, flags, seg_node_map=None): return retval - def render_text(self, text, x, y, font_name='OpenSans-Regular', - font_pt=40, color=None, scale=1.0, - align=TextAlign.BOTTOM_LEFT): + def render_text( + self, + text, + x, + y, + font_name="OpenSans-Regular", + font_pt=40, + color=None, + scale=1.0, + align=TextAlign.BOTTOM_LEFT, + ): """Render text into the current viewport. Note @@ -208,12 +225,12 @@ def render_text(self, text, x, y, font_name='OpenSans-Regular', # Set uniforms p = np.eye(4) - p[0,0] = 2.0 / self.viewport_width - p[0,3] = -1.0 - p[1,1] = 2.0 / self.viewport_height - p[1,3] = -1.0 - program.set_uniform('projection', p) - program.set_uniform('text_color', color) + p[0, 0] = 2.0 / self.viewport_width + p[0, 3] = -1.0 + p[1, 1] = 2.0 / self.viewport_height + p[1, 3] = -1.0 + program.set_uniform("projection", p) + program.set_uniform("text_color", color) # Draw text font.render_string(text, x, y, scale, align) @@ -240,7 +257,7 @@ def read_color_buf(self): color_im = np.flip(color_im, axis=0) # Resize for macos if needed - if sys.platform == 'darwin': + if sys.platform == "darwin": color_im = self._resize_image(color_im, True) return color_im @@ -256,35 +273,32 @@ def read_depth_buf(self): width, height = self.viewport_width, self.viewport_height glBindFramebuffer(GL_READ_FRAMEBUFFER, 0) glReadBuffer(GL_FRONT) - depth_buf = glReadPixels( - 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT - ) + depth_buf = glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT) depth_im = np.frombuffer(depth_buf, dtype=np.float32) depth_im = depth_im.reshape((height, width)) depth_im = np.flip(depth_im, axis=0) - inf_inds = (depth_im == 1.0) + inf_inds = depth_im == 1.0 depth_im = 2.0 * depth_im - 1.0 z_near, z_far = self._latest_znear, self._latest_zfar noninf = np.logical_not(inf_inds) if z_far is None: depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf]) else: - depth_im[noninf] = ((2.0 * z_near * z_far) / - (z_far + z_near - depth_im[noninf] * - (z_far - z_near))) + depth_im[noninf] = (2.0 * z_near * z_far) / ( + z_far + z_near - depth_im[noninf] * (z_far - z_near) + ) depth_im[inf_inds] = 0.0 # Resize for macos if needed - if sys.platform == 'darwin': + if sys.platform == "darwin": depth_im = self._resize_image(depth_im) return depth_im def delete(self): - """Free all allocated OpenGL resources. - """ + """Free all allocated OpenGL resources.""" # Free shaders self._program_cache.clear() @@ -372,17 +386,20 @@ def _forward_pass(self, scene, flags, seg_node_map=None): program._bind() # Set the camera uniforms - program.set_uniform('V', V) - program.set_uniform('P', P) + program.set_uniform("V", V) + program.set_uniform("P", P) program.set_uniform( - 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3] + "cam_pos", scene.get_pose(scene.main_camera_node)[:3, 3] ) if bool(flags & RenderFlags.SEG): - program.set_uniform('color', color) + program.set_uniform("color", color) # Next, bind the lighting - if not (flags & RenderFlags.DEPTH_ONLY or flags & RenderFlags.FLAT or - flags & RenderFlags.SEG): + if not ( + flags & RenderFlags.DEPTH_ONLY + or flags & RenderFlags.FLAT + or flags & RenderFlags.SEG + ): self._bind_lighting(scene, program, node, flags) # Finally, bind and draw the primitive @@ -390,7 +407,7 @@ def _forward_pass(self, scene, flags, seg_node_map=None): primitive=primitive, pose=scene.get_pose(node), program=program, - flags=flags + flags=flags, ) self._reset_active_textures() @@ -431,10 +448,10 @@ def _shadow_mapping_pass(self, scene, light_node, flags): program._bind() # Set the camera uniforms - program.set_uniform('V', V) - program.set_uniform('P', P) + program.set_uniform("V", V) + program.set_uniform("P", P) program.set_uniform( - 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3] + "cam_pos", scene.get_pose(scene.main_camera_node)[:3, 3] ) # Finally, bind and draw the primitive @@ -442,7 +459,7 @@ def _shadow_mapping_pass(self, scene, light_node, flags): primitive=primitive, pose=scene.get_pose(node), program=program, - flags=RenderFlags.DEPTH_ONLY + flags=RenderFlags.DEPTH_ONLY, ) self._reset_active_textures() @@ -483,19 +500,17 @@ def _normals_pass(self, scene, flags): program._bind() # Set the camera uniforms - program.set_uniform('V', V) - program.set_uniform('P', P) - program.set_uniform('normal_magnitude', 0.05 * primitive.scale) - program.set_uniform( - 'normal_color', np.array([0.1, 0.1, 1.0, 1.0]) - ) + program.set_uniform("V", V) + program.set_uniform("P", P) + program.set_uniform("normal_magnitude", 0.05 * primitive.scale) + program.set_uniform("normal_color", np.array([0.1, 0.1, 1.0, 1.0])) # Finally, bind and draw the primitive self._bind_and_draw_primitive( primitive=primitive, pose=scene.get_pose(node), program=program, - flags=RenderFlags.DEPTH_ONLY + flags=RenderFlags.DEPTH_ONLY, ) self._reset_active_textures() @@ -510,7 +525,7 @@ def _normals_pass(self, scene, flags): def _bind_and_draw_primitive(self, primitive, pose, program, flags): # Set model pose matrix - program.set_uniform('M', pose) + program.set_uniform("M", pose) # Bind mesh buffers primitive._bind() @@ -522,50 +537,62 @@ def _bind_and_draw_primitive(self, primitive, pose, program, flags): # Bind textures tf = material.tex_flags if tf & TexFlags.NORMAL: - self._bind_texture(material.normalTexture, - 'material.normal_texture', program) + self._bind_texture( + material.normalTexture, "material.normal_texture", program + ) if tf & TexFlags.OCCLUSION: - self._bind_texture(material.occlusionTexture, - 'material.occlusion_texture', program) + self._bind_texture( + material.occlusionTexture, "material.occlusion_texture", program + ) if tf & TexFlags.EMISSIVE: - self._bind_texture(material.emissiveTexture, - 'material.emissive_texture', program) + self._bind_texture( + material.emissiveTexture, "material.emissive_texture", program + ) if tf & TexFlags.BASE_COLOR: - self._bind_texture(material.baseColorTexture, - 'material.base_color_texture', program) + self._bind_texture( + material.baseColorTexture, "material.base_color_texture", program + ) if tf & TexFlags.METALLIC_ROUGHNESS: - self._bind_texture(material.metallicRoughnessTexture, - 'material.metallic_roughness_texture', - program) + self._bind_texture( + material.metallicRoughnessTexture, + "material.metallic_roughness_texture", + program, + ) if tf & TexFlags.DIFFUSE: - self._bind_texture(material.diffuseTexture, - 'material.diffuse_texture', program) + self._bind_texture( + material.diffuseTexture, "material.diffuse_texture", program + ) if tf & TexFlags.SPECULAR_GLOSSINESS: - self._bind_texture(material.specularGlossinessTexture, - 'material.specular_glossiness_texture', - program) + self._bind_texture( + material.specularGlossinessTexture, + "material.specular_glossiness_texture", + program, + ) # Bind other uniforms - b = 'material.{}' - program.set_uniform(b.format('emissive_factor'), - material.emissiveFactor) + b = "material.{}" + program.set_uniform(b.format("emissive_factor"), material.emissiveFactor) if isinstance(material, MetallicRoughnessMaterial): - program.set_uniform(b.format('base_color_factor'), - material.baseColorFactor) - program.set_uniform(b.format('metallic_factor'), - material.metallicFactor) - program.set_uniform(b.format('roughness_factor'), - material.roughnessFactor) + program.set_uniform( + b.format("base_color_factor"), material.baseColorFactor + ) + program.set_uniform( + b.format("metallic_factor"), material.metallicFactor + ) + program.set_uniform( + b.format("roughness_factor"), material.roughnessFactor + ) elif isinstance(material, SpecularGlossinessMaterial): - program.set_uniform(b.format('diffuse_factor'), - material.diffuseFactor) - program.set_uniform(b.format('specular_factor'), - material.specularFactor) - program.set_uniform(b.format('glossiness_factor'), - material.glossinessFactor) + program.set_uniform(b.format("diffuse_factor"), material.diffuseFactor) + program.set_uniform( + b.format("specular_factor"), material.specularFactor + ) + program.set_uniform( + b.format("glossiness_factor"), material.glossinessFactor + ) # Set blending options - if material.alphaMode == 'BLEND': + if material.alphaMode == "BLEND": glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) else: @@ -607,8 +634,11 @@ def _bind_and_draw_primitive(self, primitive, pose, program, flags): if primitive.indices is not None: glDrawElementsInstanced( - primitive.mode, primitive.indices.size, GL_UNSIGNED_INT, - ctypes.c_void_p(0), n_instances + primitive.mode, + primitive.indices.size, + GL_UNSIGNED_INT, + ctypes.c_void_p(0), + n_instances, ) else: glDrawArraysInstanced( @@ -619,83 +649,80 @@ def _bind_and_draw_primitive(self, primitive, pose, program, flags): primitive._unbind() def _bind_lighting(self, scene, program, node, flags): - """Bind all lighting uniform values for a scene. - """ + """Bind all lighting uniform values for a scene.""" max_n_lights = self._compute_max_n_lights(flags) n_d = min(len(scene.directional_light_nodes), max_n_lights[0]) n_s = min(len(scene.spot_light_nodes), max_n_lights[1]) n_p = min(len(scene.point_light_nodes), max_n_lights[2]) - program.set_uniform('ambient_light', scene.ambient_light) - program.set_uniform('n_directional_lights', n_d) - program.set_uniform('n_spot_lights', n_s) - program.set_uniform('n_point_lights', n_p) + program.set_uniform("ambient_light", scene.ambient_light) + program.set_uniform("n_directional_lights", n_d) + program.set_uniform("n_spot_lights", n_s) + program.set_uniform("n_point_lights", n_p) plc = 0 slc = 0 dlc = 0 light_nodes = scene.light_nodes - if (len(scene.directional_light_nodes) > max_n_lights[0] or - len(scene.spot_light_nodes) > max_n_lights[1] or - len(scene.point_light_nodes) > max_n_lights[2]): - light_nodes = self._sorted_nodes_by_distance( - scene, scene.light_nodes, node - ) + if ( + len(scene.directional_light_nodes) > max_n_lights[0] + or len(scene.spot_light_nodes) > max_n_lights[1] + or len(scene.point_light_nodes) > max_n_lights[2] + ): + light_nodes = self._sorted_nodes_by_distance(scene, scene.light_nodes, node) for n in light_nodes: light = n.light pose = scene.get_pose(n) - position = pose[:3,3] - direction = -pose[:3,2] + position = pose[:3, 3] + direction = -pose[:3, 2] if isinstance(light, PointLight): if plc == max_n_lights[2]: continue - b = 'point_lights[{}].'.format(plc) + b = "point_lights[{}].".format(plc) plc += 1 shadow = bool(flags & RenderFlags.SHADOWS_POINT) - program.set_uniform(b + 'position', position) + program.set_uniform(b + "position", position) elif isinstance(light, SpotLight): if slc == max_n_lights[1]: continue - b = 'spot_lights[{}].'.format(slc) + b = "spot_lights[{}].".format(slc) slc += 1 shadow = bool(flags & RenderFlags.SHADOWS_SPOT) - las = 1.0 / max(0.001, np.cos(light.innerConeAngle) - - np.cos(light.outerConeAngle)) + las = 1.0 / max( + 0.001, np.cos(light.innerConeAngle) - np.cos(light.outerConeAngle) + ) lao = -np.cos(light.outerConeAngle) * las - program.set_uniform(b + 'direction', direction) - program.set_uniform(b + 'position', position) - program.set_uniform(b + 'light_angle_scale', las) - program.set_uniform(b + 'light_angle_offset', lao) + program.set_uniform(b + "direction", direction) + program.set_uniform(b + "position", position) + program.set_uniform(b + "light_angle_scale", las) + program.set_uniform(b + "light_angle_offset", lao) else: if dlc == max_n_lights[0]: continue - b = 'directional_lights[{}].'.format(dlc) + b = "directional_lights[{}].".format(dlc) dlc += 1 shadow = bool(flags & RenderFlags.SHADOWS_DIRECTIONAL) - program.set_uniform(b + 'direction', direction) + program.set_uniform(b + "direction", direction) - program.set_uniform(b + 'color', light.color) - program.set_uniform(b + 'intensity', light.intensity) + program.set_uniform(b + "color", light.color) + program.set_uniform(b + "intensity", light.intensity) # if light.range is not None: # program.set_uniform(b + 'range', light.range) # else: # program.set_uniform(b + 'range', 0) if shadow: - self._bind_texture(light.shadow_texture, - b + 'shadow_map', program) + self._bind_texture(light.shadow_texture, b + "shadow_map", program) if not isinstance(light, PointLight): V, P = self._get_light_cam_matrices(scene, n, flags) - program.set_uniform(b + 'light_matrix', P.dot(V)) + program.set_uniform(b + "light_matrix", P.dot(V)) else: - raise NotImplementedError( - 'Point light shadows not implemented' - ) + raise NotImplementedError("Point light shadows not implemented") def _sorted_mesh_nodes(self, scene): - cam_loc = scene.get_pose(scene.main_camera_node)[:3,3] + cam_loc = scene.get_pose(scene.main_camera_node)[:3, 3] solid_nodes = [] trans_nodes = [] for node in scene.mesh_nodes: @@ -707,19 +734,19 @@ def _sorted_mesh_nodes(self, scene): # TODO BETTER SORTING METHOD trans_nodes.sort( - key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc) + key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3, 3] - cam_loc) ) solid_nodes.sort( - key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3,3] - cam_loc) + key=lambda n: -np.linalg.norm(scene.get_pose(n)[:3, 3] - cam_loc) ) return solid_nodes + trans_nodes def _sorted_nodes_by_distance(self, scene, nodes, compare_node): nodes = list(nodes) - compare_posn = scene.get_pose(compare_node)[:3,3] - nodes.sort(key=lambda n: np.linalg.norm( - scene.get_pose(n)[:3,3] - compare_posn) + compare_posn = scene.get_pose(compare_node)[:3, 3] + nodes.sort( + key=lambda n: np.linalg.norm(scene.get_pose(n)[:3, 3] - compare_posn) ) return nodes @@ -761,22 +788,23 @@ def _update_context(self, scene, flags): self._mesh_textures = mesh_textures.copy() shadow_textures = set() - for l in scene.lights: + for light in scene.lights: # Create if needed active = False - if (isinstance(l, DirectionalLight) and - flags & RenderFlags.SHADOWS_DIRECTIONAL): + if ( + isinstance(light, DirectionalLight) + and flags & RenderFlags.SHADOWS_DIRECTIONAL + ): active = True - elif (isinstance(l, PointLight) and - flags & RenderFlags.SHADOWS_POINT): + elif isinstance(light, PointLight) and flags & RenderFlags.SHADOWS_POINT: active = True - elif isinstance(l, SpotLight) and flags & RenderFlags.SHADOWS_SPOT: + elif isinstance(light, SpotLight) and flags & RenderFlags.SHADOWS_SPOT: active = True - if active and l.shadow_texture is None: - l._generate_shadow_texture() - if l.shadow_texture is not None: - shadow_textures.add(l.shadow_texture) + if active and light.shadow_texture is None: + light._generate_shadow_texture() + if light.shadow_texture is not None: + shadow_textures.add(light.shadow_texture) # Add new textures to context for texture in shadow_textures - self._shadow_textures: @@ -816,7 +844,7 @@ def _reset_active_textures(self): def _get_camera_matrices(self, scene): main_camera_node = scene.main_camera_node if main_camera_node is None: - raise ValueError('Cannot render scene without a camera') + raise ValueError("Cannot render scene without a camera") P = main_camera_node.camera.get_projection_matrix( width=self.viewport_width, height=self.viewport_height ) @@ -831,10 +859,10 @@ def _get_light_cam_matrices(self, scene, light_node, flags): camera = light._get_shadow_camera(s) P = camera.get_projection_matrix() if isinstance(light, DirectionalLight): - direction = -pose[:3,2] + direction = -pose[:3, 2] c = scene.centroid loc = c - direction * s - pose[:3,3] = loc + pose[:3, 3] = loc V = np.linalg.inv(pose) # V maps from world to camera return V, P @@ -844,8 +872,7 @@ def _get_light_cam_matrices(self, scene, light_node, flags): def _get_text_program(self): program = self._program_cache.get_program( - vertex_shader='text.vert', - fragment_shader='text.frag' + vertex_shader="text.vert", fragment_shader="text.frag" ) if not program._in_context(): @@ -882,9 +909,8 @@ def _compute_max_n_lights(self, flags): tex_per_light = n_available_textures // n_shadow_types if flags & RenderFlags.SHADOWS_DIRECTIONAL: - max_n_lights[0] = ( - tex_per_light + - (n_available_textures - tex_per_light * n_shadow_types) + max_n_lights[0] = tex_per_light + ( + n_available_textures - tex_per_light * n_shadow_types ) if flags & RenderFlags.SHADOWS_SPOT: max_n_lights[1] = tex_per_light @@ -899,101 +925,104 @@ def _get_primitive_program(self, primitive, flags, program_flags): geometry_shader = None defines = {} - if (bool(program_flags & ProgramFlags.USE_MATERIAL) and - not flags & RenderFlags.DEPTH_ONLY and - not flags & RenderFlags.FLAT and - not flags & RenderFlags.SEG): - vertex_shader = 'mesh.vert' - fragment_shader = 'mesh.frag' - elif bool(program_flags & (ProgramFlags.VERTEX_NORMALS | - ProgramFlags.FACE_NORMALS)): - vertex_shader = 'vertex_normals.vert' + if ( + bool(program_flags & ProgramFlags.USE_MATERIAL) + and not flags & RenderFlags.DEPTH_ONLY + and not flags & RenderFlags.FLAT + and not flags & RenderFlags.SEG + ): + vertex_shader = "mesh.vert" + fragment_shader = "mesh.frag" + elif bool( + program_flags & (ProgramFlags.VERTEX_NORMALS | ProgramFlags.FACE_NORMALS) + ): + vertex_shader = "vertex_normals.vert" if primitive.mode == GLTF.POINTS: - geometry_shader = 'vertex_normals_pc.geom' + geometry_shader = "vertex_normals_pc.geom" else: - geometry_shader = 'vertex_normals.geom' - fragment_shader = 'vertex_normals.frag' + geometry_shader = "vertex_normals.geom" + fragment_shader = "vertex_normals.frag" elif flags & RenderFlags.FLAT: - vertex_shader = 'flat.vert' - fragment_shader = 'flat.frag' + vertex_shader = "flat.vert" + fragment_shader = "flat.frag" elif flags & RenderFlags.SEG: - vertex_shader = 'segmentation.vert' - fragment_shader = 'segmentation.frag' + vertex_shader = "segmentation.vert" + fragment_shader = "segmentation.frag" else: - vertex_shader = 'mesh_depth.vert' - fragment_shader = 'mesh_depth.frag' + vertex_shader = "mesh_depth.vert" + fragment_shader = "mesh_depth.frag" # Set up vertex buffer DEFINES bf = primitive.buf_flags buf_idx = 1 if bf & BufFlags.NORMAL: - defines['NORMAL_LOC'] = buf_idx + defines["NORMAL_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.TANGENT: - defines['TANGENT_LOC'] = buf_idx + defines["TANGENT_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.TEXCOORD_0: - defines['TEXCOORD_0_LOC'] = buf_idx + defines["TEXCOORD_0_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.TEXCOORD_1: - defines['TEXCOORD_1_LOC'] = buf_idx + defines["TEXCOORD_1_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.COLOR_0: - defines['COLOR_0_LOC'] = buf_idx + defines["COLOR_0_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.JOINTS_0: - defines['JOINTS_0_LOC'] = buf_idx + defines["JOINTS_0_LOC"] = buf_idx buf_idx += 1 if bf & BufFlags.WEIGHTS_0: - defines['WEIGHTS_0_LOC'] = buf_idx + defines["WEIGHTS_0_LOC"] = buf_idx buf_idx += 1 - defines['INST_M_LOC'] = buf_idx + defines["INST_M_LOC"] = buf_idx # Set up shadow mapping defines if flags & RenderFlags.SHADOWS_DIRECTIONAL: - defines['DIRECTIONAL_LIGHT_SHADOWS'] = 1 + defines["DIRECTIONAL_LIGHT_SHADOWS"] = 1 if flags & RenderFlags.SHADOWS_SPOT: - defines['SPOT_LIGHT_SHADOWS'] = 1 + defines["SPOT_LIGHT_SHADOWS"] = 1 if flags & RenderFlags.SHADOWS_POINT: - defines['POINT_LIGHT_SHADOWS'] = 1 + defines["POINT_LIGHT_SHADOWS"] = 1 max_n_lights = self._compute_max_n_lights(flags) - defines['MAX_DIRECTIONAL_LIGHTS'] = max_n_lights[0] - defines['MAX_SPOT_LIGHTS'] = max_n_lights[1] - defines['MAX_POINT_LIGHTS'] = max_n_lights[2] + defines["MAX_DIRECTIONAL_LIGHTS"] = max_n_lights[0] + defines["MAX_SPOT_LIGHTS"] = max_n_lights[1] + defines["MAX_POINT_LIGHTS"] = max_n_lights[2] # Set up vertex normal defines if program_flags & ProgramFlags.VERTEX_NORMALS: - defines['VERTEX_NORMALS'] = 1 + defines["VERTEX_NORMALS"] = 1 if program_flags & ProgramFlags.FACE_NORMALS: - defines['FACE_NORMALS'] = 1 + defines["FACE_NORMALS"] = 1 # Set up material texture defines if bool(program_flags & ProgramFlags.USE_MATERIAL): tf = primitive.material.tex_flags if tf & TexFlags.NORMAL: - defines['HAS_NORMAL_TEX'] = 1 + defines["HAS_NORMAL_TEX"] = 1 if tf & TexFlags.OCCLUSION: - defines['HAS_OCCLUSION_TEX'] = 1 + defines["HAS_OCCLUSION_TEX"] = 1 if tf & TexFlags.EMISSIVE: - defines['HAS_EMISSIVE_TEX'] = 1 + defines["HAS_EMISSIVE_TEX"] = 1 if tf & TexFlags.BASE_COLOR: - defines['HAS_BASE_COLOR_TEX'] = 1 + defines["HAS_BASE_COLOR_TEX"] = 1 if tf & TexFlags.METALLIC_ROUGHNESS: - defines['HAS_METALLIC_ROUGHNESS_TEX'] = 1 + defines["HAS_METALLIC_ROUGHNESS_TEX"] = 1 if tf & TexFlags.DIFFUSE: - defines['HAS_DIFFUSE_TEX'] = 1 + defines["HAS_DIFFUSE_TEX"] = 1 if tf & TexFlags.SPECULAR_GLOSSINESS: - defines['HAS_SPECULAR_GLOSSINESS_TEX'] = 1 + defines["HAS_SPECULAR_GLOSSINESS_TEX"] = 1 if isinstance(primitive.material, MetallicRoughnessMaterial): - defines['USE_METALLIC_MATERIAL'] = 1 + defines["USE_METALLIC_MATERIAL"] = 1 elif isinstance(primitive.material, SpecularGlossinessMaterial): - defines['USE_GLOSSY_MATERIAL'] = 1 + defines["USE_GLOSSY_MATERIAL"] = 1 program = self._program_cache.get_program( vertex_shader=vertex_shader, fragment_shader=fragment_shader, geometry_shader=geometry_shader, - defines=defines + defines=defines, ) if not program._in_context(): @@ -1053,9 +1082,11 @@ def _delete_shadow_framebuffer(self): def _configure_main_framebuffer(self): # If mismatch with prior framebuffer, delete it - if (self._main_fb is not None and - self.viewport_width != self._main_fb_dims[0] or - self.viewport_height != self._main_fb_dims[1]): + if ( + self._main_fb is not None + and self.viewport_width != self._main_fb_dims[0] + or self.viewport_height != self._main_fb_dims[1] + ): self._delete_main_framebuffer() # If framebuffer doesn't exist, create it @@ -1065,48 +1096,56 @@ def _configure_main_framebuffer(self): glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb) glRenderbufferStorage( - GL_RENDERBUFFER, GL_RGBA, - self.viewport_width, self.viewport_height + GL_RENDERBUFFER, GL_RGBA, self.viewport_width, self.viewport_height ) glBindRenderbuffer(GL_RENDERBUFFER, self._main_db) glRenderbufferStorage( - GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, - self.viewport_width, self.viewport_height + GL_RENDERBUFFER, + GL_DEPTH_COMPONENT24, + self.viewport_width, + self.viewport_height, ) self._main_fb = glGenFramebuffers(1) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb) glFramebufferRenderbuffer( - GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_RENDERBUFFER, self._main_cb + GL_DRAW_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, + self._main_cb, ) glFramebufferRenderbuffer( - GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - GL_RENDERBUFFER, self._main_db + GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, self._main_db ) # Generate multisample buffer self._main_cb_ms, self._main_db_ms = glGenRenderbuffers(2) glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb_ms) glRenderbufferStorageMultisample( - GL_RENDERBUFFER, 4, GL_RGBA, - self.viewport_width, self.viewport_height + GL_RENDERBUFFER, 4, GL_RGBA, self.viewport_width, self.viewport_height ) glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms) glRenderbufferStorageMultisample( - GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT24, - self.viewport_width, self.viewport_height + GL_RENDERBUFFER, + 4, + GL_DEPTH_COMPONENT24, + self.viewport_width, + self.viewport_height, ) self._main_fb_ms = glGenFramebuffers(1) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms) glFramebufferRenderbuffer( - GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_RENDERBUFFER, self._main_cb_ms + GL_DRAW_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, + self._main_cb_ms, ) glFramebufferRenderbuffer( - GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - GL_RENDERBUFFER, self._main_db_ms + GL_DRAW_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, + self._main_db_ms, ) self._main_fb_dims = (self.viewport_width, self.viewport_height) @@ -1134,23 +1173,19 @@ def _read_main_framebuffer(self, scene, flags): glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb_ms) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb) glBlitFramebuffer( - 0, 0, width, height, 0, 0, width, height, - GL_COLOR_BUFFER_BIT, GL_LINEAR + 0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR ) glBlitFramebuffer( - 0, 0, width, height, 0, 0, width, height, - GL_DEPTH_BUFFER_BIT, GL_NEAREST + 0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST ) glBindFramebuffer(GL_READ_FRAMEBUFFER, self._main_fb) # Read depth - depth_buf = glReadPixels( - 0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT - ) + depth_buf = glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT) depth_im = np.frombuffer(depth_buf, dtype=np.float32) depth_im = depth_im.reshape((height, width)) depth_im = np.flip(depth_im, axis=0) - inf_inds = (depth_im == 1.0) + inf_inds = depth_im == 1.0 depth_im = 2.0 * depth_im - 1.0 z_near = scene.main_camera_node.camera.znear z_far = scene.main_camera_node.camera.zfar @@ -1158,13 +1193,13 @@ def _read_main_framebuffer(self, scene, flags): if z_far is None: depth_im[noninf] = 2 * z_near / (1.0 - depth_im[noninf]) else: - depth_im[noninf] = ((2.0 * z_near * z_far) / - (z_far + z_near - depth_im[noninf] * - (z_far - z_near))) + depth_im[noninf] = (2.0 * z_near * z_far) / ( + z_far + z_near - depth_im[noninf] * (z_far - z_near) + ) depth_im[inf_inds] = 0.0 # Resize for macos if needed - if sys.platform == 'darwin': + if sys.platform == "darwin": depth_im = self._resize_image(depth_im) if flags & RenderFlags.DEPTH_ONLY: @@ -1172,21 +1207,17 @@ def _read_main_framebuffer(self, scene, flags): # Read color if flags & RenderFlags.RGBA: - color_buf = glReadPixels( - 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE - ) + color_buf = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE) color_im = np.frombuffer(color_buf, dtype=np.uint8) color_im = color_im.reshape((height, width, 4)) else: - color_buf = glReadPixels( - 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE - ) + color_buf = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE) color_im = np.frombuffer(color_buf, dtype=np.uint8) color_im = color_im.reshape((height, width, 3)) color_im = np.flip(color_im, axis=0) # Resize for macos if needed - if sys.platform == 'darwin': + if sys.platform == "darwin": color_im = self._resize_image(color_im, True) return color_im, depth_im @@ -1197,8 +1228,10 @@ def _resize_image(self, value, antialias=False): resample = PIL.Image.NEAREST if antialias: resample = PIL.Image.BILINEAR - size = (self.viewport_width // self.dpscale, - self.viewport_height // self.dpscale) + size = ( + self.viewport_width // self.dpscale, + self.viewport_height // self.dpscale, + ) img = img.resize(size, resample=resample) return np.array(img) @@ -1227,10 +1260,10 @@ def _forward_pass_no_reset(self, scene, flags): program._bind() # Set the camera uniforms - program.set_uniform('V', V) - program.set_uniform('P', P) + program.set_uniform("V", V) + program.set_uniform("P", P) program.set_uniform( - 'cam_pos', scene.get_pose(scene.main_camera_node)[:3,3] + "cam_pos", scene.get_pose(scene.main_camera_node)[:3, 3] ) # Next, bind the lighting @@ -1242,7 +1275,7 @@ def _forward_pass_no_reset(self, scene, flags): primitive=primitive, pose=scene.get_pose(node), program=program, - flags=flags + flags=flags, ) self._reset_active_textures() @@ -1273,7 +1306,7 @@ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False): (0, 4): [0, h // 2, w // 2, h], (1, 4): [w // 2, h // 2, w, h], (2, 4): [0, 0, w // 2, h // 2], - (3, 4): [w // 2, 0, w, h // 2] + (3, 4): [w // 2, 0, w, h // 2], } if tile: @@ -1281,13 +1314,13 @@ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False): light = ln.light if light.shadow_texture is None: - raise ValueError('Light does not have a shadow texture') + raise ValueError("Light does not have a shadow texture") glViewport(*viewport_dims[(i, num_nodes + 1)]) program = self._get_debug_quad_program() program._bind() - self._bind_texture(light.shadow_texture, 'depthMap', program) + self._bind_texture(light.shadow_texture, "depthMap", program) self._render_debug_quad() self._reset_active_textures() glFlush() @@ -1299,13 +1332,13 @@ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False): light = ln.light if light.shadow_texture is None: - raise ValueError('Light does not have a shadow texture') + raise ValueError("Light does not have a shadow texture") glViewport(0, 0, self.viewport_width, self.viewport_height) program = self._get_debug_quad_program() program._bind() - self._bind_texture(light.shadow_texture, 'depthMap', program) + self._bind_texture(light.shadow_texture, "depthMap", program) self._render_debug_quad() self._reset_active_textures() glFlush() @@ -1313,8 +1346,7 @@ def _render_light_shadowmaps(self, scene, light_nodes, flags, tile=False): def _get_debug_quad_program(self): program = self._program_cache.get_program( - vertex_shader='debug_quad.vert', - fragment_shader='debug_quad.frag' + vertex_shader="debug_quad.vert", fragment_shader="debug_quad.frag" ) if not program._in_context(): program._add_to_context() diff --git a/pyrender/sampler.py b/pyrender/sampler.py index e4784d0..69a593b 100644 --- a/pyrender/sampler.py +++ b/pyrender/sampler.py @@ -37,12 +37,14 @@ class Sampler(object): - :attr:`.GLTF.REPEAT` """ - def __init__(self, - name=None, - magFilter=None, - minFilter=None, - wrapS=GLTF.REPEAT, - wrapT=GLTF.REPEAT): + def __init__( + self, + name=None, + magFilter=None, + minFilter=None, + wrapS=GLTF.REPEAT, + wrapT=GLTF.REPEAT, + ): self.name = name self.magFilter = magFilter self.minFilter = minFilter @@ -51,8 +53,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -63,8 +64,7 @@ def name(self, value): @property def magFilter(self): - """int : Magnification filter type. - """ + """int : Magnification filter type.""" return self._magFilter @magFilter.setter @@ -73,8 +73,7 @@ def magFilter(self, value): @property def minFilter(self): - """int : Minification filter type. - """ + """int : Minification filter type.""" return self._minFilter @minFilter.setter @@ -83,8 +82,7 @@ def minFilter(self, value): @property def wrapS(self): - """int : S (U) wrapping mode. - """ + """int : S (U) wrapping mode.""" return self._wrapS @wrapS.setter @@ -93,8 +91,7 @@ def wrapS(self, value): @property def wrapT(self): - """int : T (V) wrapping mode. - """ + """int : T (V) wrapping mode.""" return self._wrapT @wrapT.setter diff --git a/pyrender/scene.py b/pyrender/scene.py index 2fe057e..93ea869 100644 --- a/pyrender/scene.py +++ b/pyrender/scene.py @@ -29,11 +29,7 @@ class Scene(object): The user-defined name of this object. """ - def __init__(self, - nodes=None, - bg_color=None, - ambient_light=None, - name=None): + def __init__(self, nodes=None, bg_color=None, ambient_light=None, name=None): if bg_color is None: bg_color = np.ones(4) @@ -64,7 +60,7 @@ def __init__(self, # Transform tree self._digraph = nx.DiGraph() - self._digraph.add_node('world') + self._digraph.add_node("world") self._path_cache = {} # Find root nodes and add them @@ -73,8 +69,7 @@ def __init__(self, for node in nodes: for child in node.children: if node_parent_map[child] is not None: - raise ValueError('Nodes may not have more than ' - 'one parent') + raise ValueError("Nodes may not have more than " "one parent") node_parent_map[child] = node for node in node_parent_map: if node_parent_map[node] is None: @@ -82,8 +77,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -94,14 +88,12 @@ def name(self, value): @property def nodes(self): - """set of :class:`Node` : Set of nodes in the scene. - """ + """set of :class:`Node` : Set of nodes in the scene.""" return self._nodes @property def bg_color(self): - """(3,) float : The scene background color. - """ + """(3,) float : The scene background color.""" return self._bg_color @bg_color.setter @@ -114,8 +106,7 @@ def bg_color(self, value): @property def ambient_light(self): - """(3,) float : The ambient light in the scene. - """ + """(3,) float : The ambient light in the scene.""" return self._ambient_light @ambient_light.setter @@ -128,51 +119,46 @@ def ambient_light(self, value): @property def meshes(self): - """set of :class:`Mesh` : The meshes in the scene. - """ + """set of :class:`Mesh` : The meshes in the scene.""" return set([n.mesh for n in self.mesh_nodes]) @property def mesh_nodes(self): - """set of :class:`Node` : The nodes containing meshes. - """ + """set of :class:`Node` : The nodes containing meshes.""" return self._mesh_nodes @property def lights(self): - """set of :class:`Light` : The lights in the scene. - """ + """set of :class:`Light` : The lights in the scene.""" return self.point_lights | self.spot_lights | self.directional_lights @property def light_nodes(self): - """set of :class:`Node` : The nodes containing lights. - """ - return (self.point_light_nodes | self.spot_light_nodes | - self.directional_light_nodes) + """set of :class:`Node` : The nodes containing lights.""" + return ( + self.point_light_nodes + | self.spot_light_nodes + | self.directional_light_nodes + ) @property def point_lights(self): - """set of :class:`PointLight` : The point lights in the scene. - """ + """set of :class:`PointLight` : The point lights in the scene.""" return set([n.light for n in self.point_light_nodes]) @property def point_light_nodes(self): - """set of :class:`Node` : The nodes containing point lights. - """ + """set of :class:`Node` : The nodes containing point lights.""" return self._point_light_nodes @property def spot_lights(self): - """set of :class:`SpotLight` : The spot lights in the scene. - """ + """set of :class:`SpotLight` : The spot lights in the scene.""" return set([n.light for n in self.spot_light_nodes]) @property def spot_light_nodes(self): - """set of :class:`Node` : The nodes containing spot lights. - """ + """set of :class:`Node` : The nodes containing spot lights.""" return self._spot_light_nodes @property @@ -184,20 +170,17 @@ def directional_lights(self): @property def directional_light_nodes(self): - """set of :class:`Node` : The nodes containing directional lights. - """ + """set of :class:`Node` : The nodes containing directional lights.""" return self._directional_light_nodes @property def cameras(self): - """set of :class:`Camera` : The cameras in the scene. - """ + """set of :class:`Camera` : The cameras in the scene.""" return set([n.camera for n in self.camera_nodes]) @property def camera_nodes(self): - """set of :class:`Node` : The nodes containing cameras in the scene. - """ + """set of :class:`Node` : The nodes containing cameras in the scene.""" return self._camera_nodes @property @@ -210,13 +193,12 @@ def main_camera_node(self): @main_camera_node.setter def main_camera_node(self, value): if value not in self.nodes: - raise ValueError('New main camera node must already be in scene') + raise ValueError("New main camera node must already be in scene") self._main_camera_node = value @property def bounds(self): - """(2,3) float : The axis-aligned bounds of the scene. - """ + """(2,3) float : The axis-aligned bounds of the scene.""" if self._bounds is None: # Compute corners corners = [] @@ -224,14 +206,15 @@ def bounds(self): mesh = mesh_node.mesh pose = self.get_pose(mesh_node) corners_local = trimesh.bounds.corners(mesh.bounds) - corners_world = pose[:3,:3].dot(corners_local.T).T + pose[:3,3] + corners_world = pose[:3, :3].dot(corners_local.T).T + pose[:3, 3] corners.append(corners_world) if len(corners) == 0: - self._bounds = np.zeros((2,3)) + self._bounds = np.zeros((2, 3)) else: corners = np.vstack(corners) - self._bounds = np.array([np.min(corners, axis=0), - np.max(corners, axis=0)]) + self._bounds = np.array( + [np.min(corners, axis=0), np.max(corners, axis=0)] + ) return self._bounds @property @@ -243,18 +226,15 @@ def centroid(self): @property def extents(self): - """(3,) float : The lengths of the axes of the scene's AABB. - """ + """(3,) float : The lengths of the axes of the scene's AABB.""" return np.diff(self.bounds, axis=0).reshape(-1) @property def scale(self): - """(3,) float : The length of the diagonal of the scene's AABB. - """ + """(3,) float : The length of the diagonal of the scene's AABB.""" return np.linalg.norm(self.extents) - def add(self, obj, name=None, pose=None, - parent_node=None, parent_name=None): + def add(self, obj, name=None, pose=None, parent_node=None, parent_name=None): """Add an object (mesh, light, or camera) to the scene. Parameters @@ -283,16 +263,18 @@ def add(self, obj, name=None, pose=None, elif isinstance(obj, Camera): node = Node(name=name, matrix=pose, camera=obj) else: - raise TypeError('Unrecognized object type') + raise TypeError("Unrecognized object type") if parent_node is None and parent_name is not None: parent_nodes = self.get_nodes(name=parent_name) if len(parent_nodes) == 0: - raise ValueError('No parent node with name {} found' - .format(parent_name)) + raise ValueError( + "No parent node with name {} found".format(parent_name) + ) elif len(parent_nodes) > 1: - raise ValueError('More than one parent node with name {} found' - .format(parent_name)) + raise ValueError( + "More than one parent node with name {} found".format(parent_name) + ) parent_node = list(parent_nodes)[0] self.add_node(node, parent_node=parent_node) @@ -354,7 +336,7 @@ def add_node(self, node, parent_node=None): The parent of this Node. If None, the new node is a root node. """ if node in self.nodes: - raise ValueError('Node already in scene') + raise ValueError("Node already in scene") self.nodes.add(node) # Add node to sets @@ -386,9 +368,9 @@ def add_node(self, node, parent_node=None): self._main_camera_node = node if parent_node is None: - parent_node = 'world' + parent_node = "world" elif parent_node not in self.nodes: - raise ValueError('Parent node must already be in scene') + raise ValueError("Parent node must already be in scene") elif node not in parent_node.children: parent_node.children.append(node) @@ -448,12 +430,12 @@ def get_pose(self, node): The transform matrix for this node. """ if node not in self.nodes: - raise ValueError('Node must already be in scene') + raise ValueError("Node must already be in scene") if node in self._path_cache: path = self._path_cache[node] else: # Get path from from_frame to to_frame - path = nx.shortest_path(self._digraph, node, 'world') + path = nx.shortest_path(self._digraph, node, "world") self._path_cache[node] = path # Traverse from from_node to to_node @@ -474,14 +456,13 @@ def set_pose(self, node, pose): The pose to set the node to. """ if node not in self.nodes: - raise ValueError('Node must already be in scene') + raise ValueError("Node must already be in scene") node._matrix = pose if node.mesh is not None: self._bounds = None def clear(self): - """Clear out all nodes to form an empty scene. - """ + """Clear out all nodes to form an empty scene.""" self._nodes = set() self._name_to_nodes = {} @@ -497,7 +478,7 @@ def clear(self): # Transform tree self._digraph = nx.DiGraph() - self._digraph.add_node('world') + self._digraph.add_node("world") self._path_cache = {} def _remove_node(self, node): @@ -552,8 +533,7 @@ def _remove_node(self, node): self._main_camera_node = None @staticmethod - def from_trimesh_scene(trimesh_scene, - bg_color=None, ambient_light=None): + def from_trimesh_scene(trimesh_scene, bg_color=None, ambient_light=None): """Create a :class:`.Scene` from a :class:`trimesh.scene.scene.Scene`. Parameters @@ -571,8 +551,10 @@ def from_trimesh_scene(trimesh_scene, A scene containing the same geometry as the trimesh scene. """ # convert trimesh geometries to pyrender geometries - geometries = {name: Mesh.from_trimesh(geom) - for name, geom in trimesh_scene.geometry.items()} + geometries = { + name: Mesh.from_trimesh(geom) + for name, geom in trimesh_scene.geometry.items() + } # create the pyrender scene object scene_pr = Scene(bg_color=bg_color, ambient_light=ambient_light) diff --git a/pyrender/shader_program.py b/pyrender/shader_program.py index c1803f2..f6e459a 100644 --- a/pyrender/shader_program.py +++ b/pyrender/shader_program.py @@ -10,18 +10,18 @@ class ShaderProgramCache(object): - """A cache for shader programs. - """ + """A cache for shader programs.""" def __init__(self, shader_dir=None): self._program_cache = {} self.shader_dir = shader_dir if self.shader_dir is None: base_dir, _ = os.path.split(os.path.realpath(__file__)) - self.shader_dir = os.path.join(base_dir, 'shaders') + self.shader_dir = os.path.join(base_dir, "shaders") - def get_program(self, vertex_shader, fragment_shader, - geometry_shader=None, defines=None): + def get_program( + self, vertex_shader, fragment_shader, geometry_shader=None, defines=None + ): """Get a program via a list of shader files to include in the program. Parameters @@ -44,7 +44,8 @@ def get_program(self, vertex_shader, fragment_shader, if defines is None: defines = {} shader_filenames = [ - x for x in [vertex_shader, fragment_shader, geometry_shader] + x + for x in [vertex_shader, fragment_shader, geometry_shader] if x is not None ] for fn in shader_filenames: @@ -53,9 +54,12 @@ def get_program(self, vertex_shader, fragment_shader, _, name = os.path.split(fn) shader_names.append(name) cid = OpenGL.contextdata.getContext() - key = tuple([cid] + sorted( - [(s,1) for s in shader_names] + [(d, defines[d]) for d in defines] - )) + key = tuple( + [cid] + + sorted( + [(s, 1) for s in shader_names] + [(d, defines[d]) for d in defines] + ) + ) if key not in self._program_cache: shader_filenames = [ @@ -65,8 +69,10 @@ def get_program(self, vertex_shader, fragment_shader, shader_filenames.append(None) vs, fs, gs = shader_filenames self._program_cache[key] = ShaderProgram( - vertex_shader=vs, fragment_shader=fs, - geometry_shader=gs, defines=defines + vertex_shader=vs, + fragment_shader=fs, + geometry_shader=gs, + defines=defines, ) return self._program_cache[key] @@ -92,8 +98,9 @@ class ShaderProgram(object): Defines and their values for the shader. """ - def __init__(self, vertex_shader, fragment_shader, - geometry_shader=None, defines=None): + def __init__( + self, vertex_shader, fragment_shader, geometry_shader=None, defines=None + ): self.vertex_shader = vertex_shader self.fragment_shader = fragment_shader @@ -111,21 +118,27 @@ def __init__(self, vertex_shader, fragment_shader, def _add_to_context(self): if self._program_id is not None: - raise ValueError('Shader program already in context') + raise ValueError("Shader program already in context") shader_ids = [] # Load vert shader - shader_ids.append(gl_shader_utils.compileShader( - self._load(self.vertex_shader), GL_VERTEX_SHADER) + shader_ids.append( + gl_shader_utils.compileShader( + self._load(self.vertex_shader), GL_VERTEX_SHADER + ) ) # Load frag shader - shader_ids.append(gl_shader_utils.compileShader( - self._load(self.fragment_shader), GL_FRAGMENT_SHADER) + shader_ids.append( + gl_shader_utils.compileShader( + self._load(self.fragment_shader), GL_FRAGMENT_SHADER + ) ) # Load geometry shader if self.geometry_shader is not None: - shader_ids.append(gl_shader_utils.compileShader( - self._load(self.geometry_shader), GL_GEOMETRY_SHADER) + shader_ids.append( + gl_shader_utils.compileShader( + self._load(self.geometry_shader), GL_GEOMETRY_SHADER + ) ) # Bind empty VAO PYOPENGL BUG @@ -157,21 +170,21 @@ def _load(self, shader_filename): def ifdef(matchobj): if matchobj.group(1) in self.defines: - return '#if 1' + return "#if 1" else: - return '#if 0' + return "#if 0" def ifndef(matchobj): if matchobj.group(1) in self.defines: - return '#if 0' + return "#if 0" else: - return '#if 1' + return "#if 1" ifdef_regex = re.compile( - '#ifdef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE + "#ifdef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$", re.MULTILINE ) ifndef_regex = re.compile( - '#ifndef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$', re.MULTILINE + "#ifndef\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*$", re.MULTILINE ) text = re.sub(ifdef_regex, ifdef, text) text = re.sub(ifndef_regex, ifndef, text) @@ -183,21 +196,18 @@ def ifndef(matchobj): return text def _bind(self): - """Bind this shader program to the current OpenGL context. - """ + """Bind this shader program to the current OpenGL context.""" if self._program_id is None: - raise ValueError('Cannot bind program that is not in context') + raise ValueError("Cannot bind program that is not in context") # glBindVertexArray(self._vao_id) glUseProgram(self._program_id) def _unbind(self): - """Unbind this shader program from the current OpenGL context. - """ + """Unbind this shader program from the current OpenGL context.""" glUseProgram(0) def delete(self): - """Delete this shader program from the current OpenGL context. - """ + """Delete this shader program from the current OpenGL context.""" self._remove_from_context() def set_uniform(self, name, value, unsigned=False): @@ -218,21 +228,20 @@ def set_uniform(self, name, value, unsigned=False): loc = glGetUniformLocation(self._program_id, name) if loc == -1: - raise ValueError('Invalid shader variable: {}'.format(name)) + raise ValueError("Invalid shader variable: {}".format(name)) if isinstance(value, np.ndarray): # DEBUG # self._unif_map[name] = value.size, value.shape if value.ndim == 1: - if (np.issubdtype(value.dtype, np.unsignedinteger) or - unsigned): - dtype = 'u' + if np.issubdtype(value.dtype, np.unsignedinteger) or unsigned: + dtype = "u" value = value.astype(np.uint32) elif np.issubdtype(value.dtype, np.integer): - dtype = 'i' + dtype = "i" value = value.astype(np.int32) else: - dtype = 'f' + dtype = "f" value = value.astype(np.float32) self._FUNC_MAP[(value.shape[0], dtype)](loc, 1, value) else: @@ -254,30 +263,30 @@ def set_uniform(self, name, value, unsigned=False): else: glUniform1i(loc, int(value)) else: - raise ValueError('Invalid data type') + raise ValueError("Invalid data type") except Exception: pass _FUNC_MAP = { - (1,'u'): glUniform1uiv, - (2,'u'): glUniform2uiv, - (3,'u'): glUniform3uiv, - (4,'u'): glUniform4uiv, - (1,'i'): glUniform1iv, - (2,'i'): glUniform2iv, - (3,'i'): glUniform3iv, - (4,'i'): glUniform4iv, - (1,'f'): glUniform1fv, - (2,'f'): glUniform2fv, - (3,'f'): glUniform3fv, - (4,'f'): glUniform4fv, - (2,2): glUniformMatrix2fv, - (2,3): glUniformMatrix2x3fv, - (2,4): glUniformMatrix2x4fv, - (3,2): glUniformMatrix3x2fv, - (3,3): glUniformMatrix3fv, - (3,4): glUniformMatrix3x4fv, - (4,2): glUniformMatrix4x2fv, - (4,3): glUniformMatrix4x3fv, - (4,4): glUniformMatrix4fv, + (1, "u"): glUniform1uiv, + (2, "u"): glUniform2uiv, + (3, "u"): glUniform3uiv, + (4, "u"): glUniform4uiv, + (1, "i"): glUniform1iv, + (2, "i"): glUniform2iv, + (3, "i"): glUniform3iv, + (4, "i"): glUniform4iv, + (1, "f"): glUniform1fv, + (2, "f"): glUniform2fv, + (3, "f"): glUniform3fv, + (4, "f"): glUniform4fv, + (2, 2): glUniformMatrix2fv, + (2, 3): glUniformMatrix2x3fv, + (2, 4): glUniformMatrix2x4fv, + (3, 2): glUniformMatrix3x2fv, + (3, 3): glUniformMatrix3fv, + (3, 4): glUniformMatrix3x4fv, + (4, 2): glUniformMatrix4x2fv, + (4, 3): glUniformMatrix4x3fv, + (4, 4): glUniformMatrix4fv, } diff --git a/pyrender/texture.py b/pyrender/texture.py index 4777597..9eb317a 100644 --- a/pyrender/texture.py +++ b/pyrender/texture.py @@ -37,15 +37,17 @@ class Texture(object): For now, just GL_FLOAT. """ - def __init__(self, - name=None, - sampler=None, - source=None, - source_channels=None, - width=None, - height=None, - tex_type=GL_TEXTURE_2D, - data_format=GL_UNSIGNED_BYTE): + def __init__( + self, + name=None, + sampler=None, + source=None, + source_channels=None, + width=None, + height=None, + tex_type=GL_TEXTURE_2D, + data_format=GL_UNSIGNED_BYTE, + ): self.source_channels = source_channels self.name = name self.sampler = sampler @@ -60,8 +62,7 @@ def __init__(self, @property def name(self): - """str : The user-defined name of this object. - """ + """str : The user-defined name of this object.""" return self._name @name.setter @@ -72,8 +73,7 @@ def name(self, value): @property def sampler(self): - """:class:`Sampler` : The sampler used by this texture. - """ + """:class:`Sampler` : The sampler used by this texture.""" return self._sampler @sampler.setter @@ -99,8 +99,7 @@ def source(self, value): @property def source_channels(self): - """str : The channels that were extracted from the original source. - """ + """str : The channels that were extracted from the original source.""" return self._source_channels @source_channels.setter @@ -109,8 +108,7 @@ def source_channels(self, value): @property def width(self): - """int : The width of the texture buffer. - """ + """int : The width of the texture buffer.""" return self._width @width.setter @@ -119,8 +117,7 @@ def width(self, value): @property def height(self): - """int : The height of the texture buffer. - """ + """int : The height of the texture buffer.""" return self._height @height.setter @@ -129,8 +126,7 @@ def height(self, value): @property def tex_type(self): - """int : The type of the texture. - """ + """int : The type of the texture.""" return self._tex_type @tex_type.setter @@ -139,8 +135,7 @@ def tex_type(self, value): @property def data_format(self): - """int : The format of the texture data. - """ + """int : The format of the texture data.""" return self._data_format @data_format.setter @@ -148,18 +143,16 @@ def data_format(self, value): self._data_format = value def is_transparent(self, cutoff=1.0): - """bool : If True, the texture is partially transparent. - """ + """bool : If True, the texture is partially transparent.""" if self._is_transparent is None: self._is_transparent = False - if self.source_channels == 'RGBA' and self.source is not None: - if np.any(self.source[:,:,3] < cutoff): + if self.source_channels == "RGBA" and self.source is not None: + if np.any(self.source[:, :, 3] < cutoff): self._is_transparent = True return self._is_transparent def delete(self): - """Remove this texture from the OpenGL context. - """ + """Remove this texture from the OpenGL context.""" self._unbind() self._remove_from_context() @@ -168,16 +161,16 @@ def delete(self): ################## def _add_to_context(self): if self._texid is not None: - raise ValueError('Texture already loaded into OpenGL context') + raise ValueError("Texture already loaded into OpenGL context") fmt = GL_DEPTH_COMPONENT - if self.source_channels == 'R': + if self.source_channels == "R": fmt = GL_RED - elif self.source_channels == 'RG' or self.source_channels == 'GB': + elif self.source_channels == "RG" or self.source_channels == "GB": fmt = GL_RG - elif self.source_channels == 'RGB': + elif self.source_channels == "RGB": fmt = GL_RGB - elif self.source_channels == 'RGBA': + elif self.source_channels == "RGBA": fmt = GL_RGBA # Generate the OpenGL texture @@ -195,8 +188,7 @@ def _add_to_context(self): # Bind texture and generate mipmaps glTexImage2D( - self.tex_type, 0, fmt, width, height, 0, fmt, - self.data_format, data + self.tex_type, 0, fmt, width, height, 0, fmt, self.data_format, data ) if self.source is not None: glGenerateMipmap(self.tex_type) @@ -216,7 +208,9 @@ def _add_to_context(self): ) else: if self.source is not None: - glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) + glTexParameteri( + self.tex_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR + ) else: glTexParameteri(self.tex_type, GL_TEXTURE_MIN_FILTER, GL_NEAREST) @@ -225,10 +219,7 @@ def _add_to_context(self): border_color = 255 * np.ones(4).astype(np.uint8) if self.data_format == GL_FLOAT: border_color = np.ones(4).astype(np.float32) - glTexParameterfv( - self.tex_type, GL_TEXTURE_BORDER_COLOR, - border_color - ) + glTexParameterfv(self.tex_type, GL_TEXTURE_BORDER_COLOR, border_color) # Unbind texture glBindTexture(self.tex_type, 0) @@ -251,9 +242,11 @@ def _unbind(self): glBindTexture(self.tex_type, 0) def _bind_as_depth_attachment(self): - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - self.tex_type, self._texid, 0) + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, self.tex_type, self._texid, 0 + ) def _bind_as_color_attachment(self): - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - self.tex_type, self._texid, 0) + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, self.tex_type, self._texid, 0 + ) diff --git a/pyrender/trackball.py b/pyrender/trackball.py index 3e57a0e..9dab590 100644 --- a/pyrender/trackball.py +++ b/pyrender/trackball.py @@ -6,15 +6,14 @@ class Trackball(object): - """A trackball class for creating camera transforms from mouse movements. - """ + """A trackball class for creating camera transforms from mouse movements.""" + STATE_ROTATE = 0 STATE_PAN = 1 STATE_ROLL = 2 STATE_ZOOM = 3 - def __init__(self, pose, size, scale, - target=np.array([0.0, 0.0, 0.0])): + def __init__(self, pose, size, scale, target=np.array([0.0, 0.0, 0.0])): """Initialize a trackball with an initial camera-to-world pose and the given parameters. @@ -48,8 +47,7 @@ def __init__(self, pose, size, scale, @property def pose(self): - """autolab_core.RigidTransform : The current camera-to-world pose. - """ + """autolab_core.RigidTransform : The current camera-to-world pose.""" return self._n_pose def set_state(self, state): @@ -101,22 +99,18 @@ def drag(self, point): mindim = 0.3 * np.min(self._size) target = self._target - x_axis = self._pose[:3,0].flatten() - y_axis = self._pose[:3,1].flatten() - z_axis = self._pose[:3,2].flatten() - eye = self._pose[:3,3].flatten() + x_axis = self._pose[:3, 0].flatten() + y_axis = self._pose[:3, 1].flatten() + z_axis = self._pose[:3, 2].flatten() + eye = self._pose[:3, 3].flatten() # Interpret drag as a rotation if self._state == Trackball.STATE_ROTATE: x_angle = -dx / mindim - x_rot_mat = transformations.rotation_matrix( - x_angle, y_axis, target - ) + x_rot_mat = transformations.rotation_matrix(x_angle, y_axis, target) y_angle = dy / mindim - y_rot_mat = transformations.rotation_matrix( - y_angle, x_axis, target - ) + y_rot_mat = transformations.rotation_matrix(y_angle, x_axis, target) self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose)) @@ -128,8 +122,7 @@ def drag(self, point): v_init = v_init / np.linalg.norm(v_init) v_curr = v_curr / np.linalg.norm(v_curr) - theta = (-np.arctan2(v_curr[1], v_curr[0]) + - np.arctan2(v_init[1], v_init[0])) + theta = -np.arctan2(v_curr[1], v_curr[0]) + np.arctan2(v_init[1], v_init[0]) rot_mat = transformations.rotation_matrix(theta, z_axis, target) @@ -143,7 +136,7 @@ def drag(self, point): translation = dx * x_axis + dy * y_axis self._n_target = self._target + translation t_tf = np.eye(4) - t_tf[:3,3] = translation + t_tf[:3, 3] = translation self._n_pose = t_tf.dot(self._pose) # Interpret drag as a zoom motion @@ -156,7 +149,7 @@ def drag(self, point): ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1]))) translation = -np.sign(dy) * ratio * radius * z_axis t_tf = np.eye(4) - t_tf[:3,3] = translation + t_tf[:3, 3] = translation self._n_pose = t_tf.dot(self._pose) def scroll(self, clicks): @@ -175,22 +168,22 @@ def scroll(self, clicks): if clicks > 0: mult = ratio**clicks elif clicks < 0: - mult = (1.0 / ratio)**abs(clicks) + mult = (1.0 / ratio) ** abs(clicks) - z_axis = self._n_pose[:3,2].flatten() - eye = self._n_pose[:3,3].flatten() + z_axis = self._n_pose[:3, 2].flatten() + eye = self._n_pose[:3, 3].flatten() radius = np.linalg.norm(eye - target) translation = (mult * radius - radius) * z_axis t_tf = np.eye(4) - t_tf[:3,3] = translation + t_tf[:3, 3] = translation self._n_pose = t_tf.dot(self._n_pose) - z_axis = self._pose[:3,2].flatten() - eye = self._pose[:3,3].flatten() + z_axis = self._pose[:3, 2].flatten() + eye = self._pose[:3, 3].flatten() radius = np.linalg.norm(eye - target) translation = (mult * radius - radius) * z_axis t_tf = np.eye(4) - t_tf[:3,3] = translation + t_tf[:3, 3] = translation self._pose = t_tf.dot(self._pose) def rotate(self, azimuth, axis=None): @@ -203,13 +196,13 @@ def rotate(self, azimuth, axis=None): """ target = self._target - y_axis = self._n_pose[:3,1].flatten() + y_axis = self._n_pose[:3, 1].flatten() if axis is not None: y_axis = axis x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) self._n_pose = x_rot_mat.dot(self._n_pose) - y_axis = self._pose[:3,1].flatten() + y_axis = self._pose[:3, 1].flatten() if axis is not None: y_axis = axis x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) diff --git a/pyrender/utils.py b/pyrender/utils.py index 48a11fa..558a7aa 100644 --- a/pyrender/utils.py +++ b/pyrender/utils.py @@ -3,8 +3,7 @@ def format_color_vector(value, length): - """Format a color vector. - """ + """Format a color vector.""" if isinstance(value, int): value = value / 255.0 if isinstance(value, float): @@ -16,20 +15,19 @@ def format_color_vector(value, length): if np.issubdtype(value.dtype, np.integer): value = (value / 255.0).astype(np.float32) if value.ndim != 1: - raise ValueError('Format vector takes only 1-D vectors') + raise ValueError("Format vector takes only 1-D vectors") if length > value.shape[0]: value = np.hstack((value, np.ones(length - value.shape[0]))) elif length < value.shape[0]: value = value[:length] else: - raise ValueError('Invalid vector data type') + raise ValueError("Invalid vector data type") return value.squeeze().astype(np.float32) def format_color_array(value, shape): - """Format an array of colors. - """ + """Format an array of colors.""" # Convert uint8 to floating value = np.asanyarray(value) if np.issubdtype(value.dtype, np.integer): @@ -37,18 +35,17 @@ def format_color_array(value, shape): # Match up shapes if value.ndim == 1: - value = np.tile(value, (shape[0],1)) + value = np.tile(value, (shape[0], 1)) if value.shape[1] < shape[1]: nc = shape[1] - value.shape[1] value = np.column_stack((value, np.ones((value.shape[0], nc)))) elif value.shape[1] > shape[1]: - value = value[:,:shape[1]] + value = value[:, : shape[1]] return value.astype(np.float32) -def format_texture_source(texture, target_channels='RGB'): - """Format a texture as a float32 np array. - """ +def format_texture_source(texture, target_channels="RGB"): + """Format a texture as a float32 np array.""" # Pass through None if texture is None: @@ -56,7 +53,7 @@ def format_texture_source(texture, target_channels='RGB'): # Convert PIL images into numpy arrays if isinstance(texture, Image.Image): - if texture.mode == 'P' and target_channels in ('RGB', 'RGBA'): + if texture.mode == "P" and target_channels in ("RGB", "RGBA"): texture = np.array(texture.convert(target_channels)) else: texture = np.array(texture) @@ -68,48 +65,47 @@ def format_texture_source(texture, target_channels='RGB'): elif np.issubdtype(texture.dtype, np.integer): texture = texture.astype(np.uint8) else: - raise TypeError('Invalid type {} for texture'.format( - type(texture) - )) + raise TypeError("Invalid type {} for texture".format(type(texture))) # Format array by picking out correct texture channels or padding if texture.ndim == 2: - texture = texture[:,:,np.newaxis] - if target_channels == 'R': - texture = texture[:,:,0] + texture = texture[:, :, np.newaxis] + if target_channels == "R": + texture = texture[:, :, 0] texture = texture.squeeze() - elif target_channels == 'RG': + elif target_channels == "RG": if texture.shape[2] == 1: texture = np.repeat(texture, 2, axis=2) else: - texture = texture[:,:,(0,1)] - elif target_channels == 'GB': + texture = texture[:, :, (0, 1)] + elif target_channels == "GB": if texture.shape[2] == 1: texture = np.repeat(texture, 2, axis=2) elif texture.shape[2] > 2: - texture = texture[:,:,(1,2)] - elif target_channels == 'RGB': + texture = texture[:, :, (1, 2)] + elif target_channels == "RGB": if texture.shape[2] == 1: texture = np.repeat(texture, 3, axis=2) elif texture.shape[2] == 2: - raise ValueError('Cannot reformat 2-channel texture into RGB') + raise ValueError("Cannot reformat 2-channel texture into RGB") else: - texture = texture[:,:,(0,1,2)] - elif target_channels == 'RGBA': + texture = texture[:, :, (0, 1, 2)] + elif target_channels == "RGBA": if texture.shape[2] == 1: texture = np.repeat(texture, 4, axis=2) - texture[:,:,3] = 255 + texture[:, :, 3] = 255 elif texture.shape[2] == 2: - raise ValueError('Cannot reformat 2-channel texture into RGBA') + raise ValueError("Cannot reformat 2-channel texture into RGBA") elif texture.shape[2] == 3: tx = np.empty((texture.shape[0], texture.shape[1], 4), dtype=np.uint8) - tx[:,:,:3] = texture - tx[:,:,3] = 255 + tx[:, :, :3] = texture + tx[:, :, 3] = 255 texture = tx else: - raise ValueError('Invalid texture channel specification: {}' - .format(target_channels)) + raise ValueError( + "Invalid texture channel specification: {}".format(target_channels) + ) else: - raise TypeError('Invalid type {} for texture'.format(type(texture))) + raise TypeError("Invalid type {} for texture".format(type(texture))) return texture diff --git a/pyrender/version.py b/pyrender/version.py index a33fc87..0ca817c 100644 --- a/pyrender/version.py +++ b/pyrender/version.py @@ -1 +1 @@ -__version__ = '0.1.45' +__version__ = "0.1.45" diff --git a/pyrender/viewer.py b/pyrender/viewer.py index d2326c3..4f47228 100644 --- a/pyrender/viewer.py +++ b/pyrender/viewer.py @@ -19,10 +19,18 @@ except Exception: pass -from .constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR, - MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR, - TEXT_PADDING, DEFAULT_SCENE_SCALE, - DEFAULT_Z_FAR, DEFAULT_Z_NEAR, RenderFlags, TextAlign) +from .constants import ( + TARGET_OPEN_GL_MAJOR, + TARGET_OPEN_GL_MINOR, + MIN_OPEN_GL_MAJOR, + MIN_OPEN_GL_MINOR, + TEXT_PADDING, + DEFAULT_SCENE_SCALE, + DEFAULT_Z_FAR, + DEFAULT_Z_NEAR, + RenderFlags, + TextAlign, +) from .light import DirectionalLight from .node import Node from .camera import PerspectiveCamera, OrthographicCamera, IntrinsicsCamera @@ -32,7 +40,8 @@ import pyglet from pyglet import clock -pyglet.options['shadow_window'] = False + +pyglet.options["shadow_window"] = False class Viewer(pyglet.window.Window): @@ -173,11 +182,17 @@ class Viewer(pyglet.window.Window): :attr:`.Viewer.render_lock`, and release it when your update is done. """ - def __init__(self, scene, viewport_size=None, - render_flags=None, viewer_flags=None, - registered_keys=None, run_in_thread=False, - auto_start=True, - **kwargs): + def __init__( + self, + scene, + viewport_size=None, + render_flags=None, + viewer_flags=None, + registered_keys=None, + run_in_thread=False, + auto_start=True, + **kwargs + ): ####################################################################### # Save attributes and flags @@ -193,39 +208,39 @@ def __init__(self, scene, viewport_size=None, self._auto_start = auto_start self._default_render_flags = { - 'flip_wireframe': False, - 'all_wireframe': False, - 'all_solid': False, - 'shadows': False, - 'vertex_normals': False, - 'face_normals': False, - 'cull_faces': True, - 'point_size': 1.0, + "flip_wireframe": False, + "all_wireframe": False, + "all_solid": False, + "shadows": False, + "vertex_normals": False, + "face_normals": False, + "cull_faces": True, + "point_size": 1.0, } self._default_viewer_flags = { - 'mouse_pressed': False, - 'rotate': False, - 'rotate_rate': np.pi / 3.0, - 'rotate_axis': np.array([0.0, 0.0, 1.0]), - 'view_center': None, - 'record': False, - 'use_raymond_lighting': False, - 'use_direct_lighting': False, - 'lighting_intensity': 3.0, - 'use_perspective_cam': True, - 'save_directory': None, - 'window_title': 'Scene Viewer', - 'refresh_rate': 30.0, - 'fullscreen': False, - 'show_world_axis': False, - 'show_mesh_axes': False, - 'caption': None + "mouse_pressed": False, + "rotate": False, + "rotate_rate": np.pi / 3.0, + "rotate_axis": np.array([0.0, 0.0, 1.0]), + "view_center": None, + "record": False, + "use_raymond_lighting": False, + "use_direct_lighting": False, + "lighting_intensity": 3.0, + "use_perspective_cam": True, + "save_directory": None, + "window_title": "Scene Viewer", + "refresh_rate": 30.0, + "fullscreen": False, + "show_world_axis": False, + "show_mesh_axes": False, + "caption": None, } self._render_flags = self._default_render_flags.copy() self._viewer_flags = self._default_viewer_flags.copy() - self._viewer_flags['rotate_axis'] = ( - self._default_viewer_flags['rotate_axis'].copy() - ) + self._viewer_flags["rotate_axis"] = self._default_viewer_flags[ + "rotate_axis" + ].copy() if render_flags is not None: self._render_flags.update(render_flags) @@ -239,8 +254,8 @@ def __init__(self, scene, viewport_size=None, self._viewer_flags[key] = kwargs[key] # TODO MAC OS BUG FOR SHADOWS - if sys.platform == 'darwin': - self._render_flags['shadows'] = False + if sys.platform == "darwin": + self._render_flags["shadows"] = False self._registered_keys = {} if registered_keys is not None: @@ -254,7 +269,7 @@ def __init__(self, scene, viewport_size=None, # Set up caption stuff self._message_text = None - self._ticks_till_fade = 2.0 / 3.0 * self.viewer_flags['refresh_rate'] + self._ticks_till_fade = 2.0 / 3.0 * self.viewer_flags["refresh_rate"] self._message_opac = 1.0 + self._ticks_till_fade # Set up raymond lights and direct lights @@ -264,11 +279,14 @@ def __init__(self, scene, viewport_size=None, # Set up axes self._axes = {} self._axis_mesh = Mesh.from_trimesh( - trimesh.creation.axis(origin_size=0.1, axis_radius=0.05, - axis_length=1.0), smooth=False) - if self.viewer_flags['show_world_axis']: - self._set_axes(world=self.viewer_flags['show_world_axis'], - mesh=self.viewer_flags['show_mesh_axes']) + trimesh.creation.axis(origin_size=0.1, axis_radius=0.05, axis_length=1.0), + smooth=False, + ) + if self.viewer_flags["show_world_axis"]: + self._set_axes( + world=self.viewer_flags["show_world_axis"], + mesh=self.viewer_flags["show_mesh_axes"], + ) ####################################################################### # Set up camera node @@ -316,22 +334,18 @@ def __init__(self, scene, viewport_size=None, if scene.scale == 0: xmag = ymag = 1.0 self._default_orth_cam = OrthographicCamera( - xmag=xmag, ymag=ymag, - znear=znear, - zfar=zfar + xmag=xmag, ymag=ymag, znear=znear, zfar=zfar ) if self._default_camera_pose is None: self._default_camera_pose = self._compute_initial_camera_pose() # Pick camera - if self.viewer_flags['use_perspective_cam']: + if self.viewer_flags["use_perspective_cam"]: camera = self._default_persp_cam else: camera = self._default_orth_cam - self._camera_node = Node( - matrix=self._default_camera_pose, camera=camera - ) + self._camera_node = Node(matrix=self._default_camera_pose, camera=camera) scene.add_node(self._camera_node) scene.main_camera_node = self._camera_node self._reset_view() @@ -340,8 +354,9 @@ def __init__(self, scene, viewport_size=None, # Initialize OpenGL context and renderer ####################################################################### self._renderer = Renderer( - self._viewport_size[0], self._viewport_size[1], - self.render_flags['point_size'] + self._viewport_size[0], + self._viewport_size[1], + self.render_flags["point_size"], ) self._is_active = True @@ -357,14 +372,12 @@ def start(self): @property def scene(self): - """:class:`.Scene` : The scene being visualized. - """ + """:class:`.Scene` : The scene being visualized.""" return self._scene @property def viewport_size(self): - """(2,) int : The width and height of the viewing window. - """ + """(2,) int : The width and height of the viewing window.""" return self._viewport_size @property @@ -388,8 +401,7 @@ def is_active(self): @property def run_in_thread(self): - """bool : Whether the viewer was run in a separate thread. - """ + """bool : Whether the viewer was run in a separate thread.""" return self._run_in_thread @property @@ -495,7 +507,7 @@ def close_external(self): """ self._should_close = True while self.is_active: - time.sleep(1.0 / self.viewer_flags['refresh_rate']) + time.sleep(1.0 / self.viewer_flags["refresh_rate"]) def save_gif(self, filename=None): """Save the stored GIF frames to a file. @@ -513,17 +525,20 @@ def save_gif(self, filename=None): to save the GIF file. """ if filename is None: - filename = self._get_save_filename(['gif', 'all']) + filename = self._get_save_filename(["gif", "all"]) if filename is not None: - self.viewer_flags['save_directory'] = os.path.dirname(filename) - imageio.mimwrite(filename, self._saved_frames, - fps=self.viewer_flags['refresh_rate'], - palettesize=128, subrectangles=True) + self.viewer_flags["save_directory"] = os.path.dirname(filename) + imageio.mimwrite( + filename, + self._saved_frames, + fps=self.viewer_flags["refresh_rate"], + palettesize=128, + subrectangles=True, + ) self._saved_frames = [] def on_close(self): - """Exit the event loop when the window is closed. - """ + """Exit the event loop when the window is closed.""" # Remove our camera and restore the prior one if self._camera_node is not None: self.scene.remove_node(self._camera_node) @@ -531,11 +546,11 @@ def on_close(self): self.scene.main_camera_node = self._prior_main_camera_node # Delete any lighting nodes that we've attached - if self.viewer_flags['use_raymond_lighting']: + if self.viewer_flags["use_raymond_lighting"]: for n in self._raymond_lights: if self.scene.has_node(n): self.scene.remove_node(n) - if self.viewer_flags['use_direct_lighting']: + if self.viewer_flags["use_direct_lighting"]: if self.scene.has_node(self._direct_light): self.scene.remove_node(self._direct_light) @@ -559,8 +574,7 @@ def on_close(self): pyglet.app.exit() def on_draw(self): - """Redraw the scene into the viewing window. - """ + """Redraw the scene into the viewing window.""" if self._renderer is None: return @@ -580,31 +594,29 @@ def on_draw(self): self.viewport_size[0] - TEXT_PADDING, TEXT_PADDING, font_pt=20, - color=np.array([0.1, 0.7, 0.2, - np.clip(self._message_opac, 0.0, 1.0)]), - align=TextAlign.BOTTOM_RIGHT + color=np.array([0.1, 0.7, 0.2, np.clip(self._message_opac, 0.0, 1.0)]), + align=TextAlign.BOTTOM_RIGHT, ) - if self.viewer_flags['caption'] is not None: - for caption in self.viewer_flags['caption']: - xpos, ypos = self._location_to_x_y(caption['location']) + if self.viewer_flags["caption"] is not None: + for caption in self.viewer_flags["caption"]: + xpos, ypos = self._location_to_x_y(caption["location"]) self._renderer.render_text( - caption['text'], + caption["text"], xpos, ypos, - font_name=caption['font_name'], - font_pt=caption['font_pt'], - color=caption['color'], - scale=caption['scale'], - align=caption['location'] + font_name=caption["font_name"], + font_pt=caption["font_pt"], + color=caption["color"], + scale=caption["scale"], + align=caption["location"], ) if self.run_in_thread or not self._auto_start: self.render_lock.release() def on_resize(self, width, height): - """Resize the camera and trackball when the window is resized. - """ + """Resize the camera and trackball when the window is resized.""" if self._renderer is None: return @@ -615,42 +627,38 @@ def on_resize(self, width, height): self.on_draw() def on_mouse_press(self, x, y, buttons, modifiers): - """Record an initial mouse press. - """ + """Record an initial mouse press.""" self._trackball.set_state(Trackball.STATE_ROTATE) - if (buttons == pyglet.window.mouse.LEFT): - ctrl = (modifiers & pyglet.window.key.MOD_CTRL) - shift = (modifiers & pyglet.window.key.MOD_SHIFT) - if (ctrl and shift): + if buttons == pyglet.window.mouse.LEFT: + ctrl = modifiers & pyglet.window.key.MOD_CTRL + shift = modifiers & pyglet.window.key.MOD_SHIFT + if ctrl and shift: self._trackball.set_state(Trackball.STATE_ZOOM) elif ctrl: self._trackball.set_state(Trackball.STATE_ROLL) elif shift: self._trackball.set_state(Trackball.STATE_PAN) - elif (buttons == pyglet.window.mouse.MIDDLE): + elif buttons == pyglet.window.mouse.MIDDLE: self._trackball.set_state(Trackball.STATE_PAN) - elif (buttons == pyglet.window.mouse.RIGHT): + elif buttons == pyglet.window.mouse.RIGHT: self._trackball.set_state(Trackball.STATE_ZOOM) self._trackball.down(np.array([x, y])) # Stop animating while using the mouse - self.viewer_flags['mouse_pressed'] = True + self.viewer_flags["mouse_pressed"] = True def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): - """Record a mouse drag. - """ + """Record a mouse drag.""" self._trackball.drag(np.array([x, y])) def on_mouse_release(self, x, y, button, modifiers): - """Record a mouse release. - """ - self.viewer_flags['mouse_pressed'] = False + """Record a mouse release.""" + self.viewer_flags["mouse_pressed"] = False def on_mouse_scroll(self, x, y, dx, dy): - """Record a mouse scroll. - """ - if self.viewer_flags['use_perspective_cam']: + """Record a mouse scroll.""" + if self.viewer_flags["use_perspective_cam"]: self._trackball.scroll(dy) else: spfc = 0.95 @@ -659,7 +667,7 @@ def on_mouse_scroll(self, x, y, dx, dy): if dy > 0: sf = spfc * dy elif dy < 0: - sf = - spbc * dy + sf = -spbc * dy c = self._camera_node.camera xmag = max(c.xmag * sf, 1e-8) @@ -668,8 +676,7 @@ def on_mouse_scroll(self, x, y, dx, dy): c.ymag = ymag def on_key_press(self, symbol, modifiers): - """Record a key press. - """ + """Record a key press.""" # First, check for registered key callbacks if symbol in self.registered_keys: tup = self.registered_keys[symbol] @@ -692,113 +699,113 @@ def on_key_press(self, symbol, modifiers): # A causes the frame to rotate self._message_text = None if symbol == pyglet.window.key.A: - self.viewer_flags['rotate'] = not self.viewer_flags['rotate'] - if self.viewer_flags['rotate']: - self._message_text = 'Rotation On' + self.viewer_flags["rotate"] = not self.viewer_flags["rotate"] + if self.viewer_flags["rotate"]: + self._message_text = "Rotation On" else: - self._message_text = 'Rotation Off' + self._message_text = "Rotation Off" # C toggles backface culling elif symbol == pyglet.window.key.C: - self.render_flags['cull_faces'] = ( - not self.render_flags['cull_faces'] - ) - if self.render_flags['cull_faces']: - self._message_text = 'Cull Faces On' + self.render_flags["cull_faces"] = not self.render_flags["cull_faces"] + if self.render_flags["cull_faces"]: + self._message_text = "Cull Faces On" else: - self._message_text = 'Cull Faces Off' + self._message_text = "Cull Faces Off" # F toggles face normals elif symbol == pyglet.window.key.F: - self.viewer_flags['fullscreen'] = ( - not self.viewer_flags['fullscreen'] - ) - self.set_fullscreen(self.viewer_flags['fullscreen']) + self.viewer_flags["fullscreen"] = not self.viewer_flags["fullscreen"] + self.set_fullscreen(self.viewer_flags["fullscreen"]) self.activate() - if self.viewer_flags['fullscreen']: - self._message_text = 'Fullscreen On' + if self.viewer_flags["fullscreen"]: + self._message_text = "Fullscreen On" else: - self._message_text = 'Fullscreen Off' + self._message_text = "Fullscreen Off" # S toggles shadows - elif symbol == pyglet.window.key.H and sys.platform != 'darwin': - self.render_flags['shadows'] = not self.render_flags['shadows'] - if self.render_flags['shadows']: - self._message_text = 'Shadows On' + elif symbol == pyglet.window.key.H and sys.platform != "darwin": + self.render_flags["shadows"] = not self.render_flags["shadows"] + if self.render_flags["shadows"]: + self._message_text = "Shadows On" else: - self._message_text = 'Shadows Off' + self._message_text = "Shadows Off" elif symbol == pyglet.window.key.I: - if (self.viewer_flags['show_world_axis'] and not - self.viewer_flags['show_mesh_axes']): - self.viewer_flags['show_world_axis'] = False - self.viewer_flags['show_mesh_axes'] = True + if ( + self.viewer_flags["show_world_axis"] + and not self.viewer_flags["show_mesh_axes"] + ): + self.viewer_flags["show_world_axis"] = False + self.viewer_flags["show_mesh_axes"] = True self._set_axes(False, True) - self._message_text = 'Mesh Axes On' - elif (not self.viewer_flags['show_world_axis'] and - self.viewer_flags['show_mesh_axes']): - self.viewer_flags['show_world_axis'] = True - self.viewer_flags['show_mesh_axes'] = True + self._message_text = "Mesh Axes On" + elif ( + not self.viewer_flags["show_world_axis"] + and self.viewer_flags["show_mesh_axes"] + ): + self.viewer_flags["show_world_axis"] = True + self.viewer_flags["show_mesh_axes"] = True self._set_axes(True, True) - self._message_text = 'All Axes On' - elif (self.viewer_flags['show_world_axis'] and - self.viewer_flags['show_mesh_axes']): - self.viewer_flags['show_world_axis'] = False - self.viewer_flags['show_mesh_axes'] = False + self._message_text = "All Axes On" + elif ( + self.viewer_flags["show_world_axis"] + and self.viewer_flags["show_mesh_axes"] + ): + self.viewer_flags["show_world_axis"] = False + self.viewer_flags["show_mesh_axes"] = False self._set_axes(False, False) - self._message_text = 'All Axes Off' + self._message_text = "All Axes Off" else: - self.viewer_flags['show_world_axis'] = True - self.viewer_flags['show_mesh_axes'] = False + self.viewer_flags["show_world_axis"] = True + self.viewer_flags["show_mesh_axes"] = False self._set_axes(True, False) - self._message_text = 'World Axis On' + self._message_text = "World Axis On" # L toggles the lighting mode elif symbol == pyglet.window.key.L: - if self.viewer_flags['use_raymond_lighting']: - self.viewer_flags['use_raymond_lighting'] = False - self.viewer_flags['use_direct_lighting'] = True - self._message_text = 'Direct Lighting' - elif self.viewer_flags['use_direct_lighting']: - self.viewer_flags['use_raymond_lighting'] = False - self.viewer_flags['use_direct_lighting'] = False - self._message_text = 'Default Lighting' + if self.viewer_flags["use_raymond_lighting"]: + self.viewer_flags["use_raymond_lighting"] = False + self.viewer_flags["use_direct_lighting"] = True + self._message_text = "Direct Lighting" + elif self.viewer_flags["use_direct_lighting"]: + self.viewer_flags["use_raymond_lighting"] = False + self.viewer_flags["use_direct_lighting"] = False + self._message_text = "Default Lighting" else: - self.viewer_flags['use_raymond_lighting'] = True - self.viewer_flags['use_direct_lighting'] = False - self._message_text = 'Raymond Lighting' + self.viewer_flags["use_raymond_lighting"] = True + self.viewer_flags["use_direct_lighting"] = False + self._message_text = "Raymond Lighting" # M toggles face normals elif symbol == pyglet.window.key.M: - self.render_flags['face_normals'] = ( - not self.render_flags['face_normals'] - ) - if self.render_flags['face_normals']: - self._message_text = 'Face Normals On' + self.render_flags["face_normals"] = not self.render_flags["face_normals"] + if self.render_flags["face_normals"]: + self._message_text = "Face Normals On" else: - self._message_text = 'Face Normals Off' + self._message_text = "Face Normals Off" # N toggles vertex normals elif symbol == pyglet.window.key.N: - self.render_flags['vertex_normals'] = ( - not self.render_flags['vertex_normals'] - ) - if self.render_flags['vertex_normals']: - self._message_text = 'Vert Normals On' + self.render_flags["vertex_normals"] = not self.render_flags[ + "vertex_normals" + ] + if self.render_flags["vertex_normals"]: + self._message_text = "Vert Normals On" else: - self._message_text = 'Vert Normals Off' + self._message_text = "Vert Normals Off" # O toggles orthographic camera mode elif symbol == pyglet.window.key.O: - self.viewer_flags['use_perspective_cam'] = ( - not self.viewer_flags['use_perspective_cam'] - ) - if self.viewer_flags['use_perspective_cam']: + self.viewer_flags["use_perspective_cam"] = not self.viewer_flags[ + "use_perspective_cam" + ] + if self.viewer_flags["use_perspective_cam"]: camera = self._default_persp_cam - self._message_text = 'Perspective View' + self._message_text = "Perspective View" else: camera = self._default_orth_cam - self._message_text = 'Orthographic View' + self._message_text = "Orthographic View" cam_pose = self._camera_node.matrix.copy() cam_node = Node(matrix=cam_pose, camera=camera) @@ -813,14 +820,14 @@ def on_key_press(self, symbol, modifiers): # R starts recording frames elif symbol == pyglet.window.key.R: - if self.viewer_flags['record']: + if self.viewer_flags["record"]: self.save_gif() - self.set_caption(self.viewer_flags['window_title']) + self.set_caption(self.viewer_flags["window_title"]) else: self.set_caption( - '{} (RECORDING)'.format(self.viewer_flags['window_title']) + "{} (RECORDING)".format(self.viewer_flags["window_title"]) ) - self.viewer_flags['record'] = not self.viewer_flags['record'] + self.viewer_flags["record"] = not self.viewer_flags["record"] # S saves the current frame as an image elif symbol == pyglet.window.key.S: @@ -828,26 +835,26 @@ def on_key_press(self, symbol, modifiers): # W toggles through wireframe modes elif symbol == pyglet.window.key.W: - if self.render_flags['flip_wireframe']: - self.render_flags['flip_wireframe'] = False - self.render_flags['all_wireframe'] = True - self.render_flags['all_solid'] = False - self._message_text = 'All Wireframe' - elif self.render_flags['all_wireframe']: - self.render_flags['flip_wireframe'] = False - self.render_flags['all_wireframe'] = False - self.render_flags['all_solid'] = True - self._message_text = 'All Solid' - elif self.render_flags['all_solid']: - self.render_flags['flip_wireframe'] = False - self.render_flags['all_wireframe'] = False - self.render_flags['all_solid'] = False - self._message_text = 'Default Wireframe' + if self.render_flags["flip_wireframe"]: + self.render_flags["flip_wireframe"] = False + self.render_flags["all_wireframe"] = True + self.render_flags["all_solid"] = False + self._message_text = "All Wireframe" + elif self.render_flags["all_wireframe"]: + self.render_flags["flip_wireframe"] = False + self.render_flags["all_wireframe"] = False + self.render_flags["all_solid"] = True + self._message_text = "All Solid" + elif self.render_flags["all_solid"]: + self.render_flags["flip_wireframe"] = False + self.render_flags["all_wireframe"] = False + self.render_flags["all_solid"] = False + self._message_text = "Default Wireframe" else: - self.render_flags['flip_wireframe'] = True - self.render_flags['all_wireframe'] = False - self.render_flags['all_solid'] = False - self._message_text = 'Flip Wireframe' + self.render_flags["flip_wireframe"] = True + self.render_flags["all_wireframe"] = False + self.render_flags["all_solid"] = False + self._message_text = "Flip Wireframe" # Z resets the camera viewpoint elif symbol == pyglet.window.key.Z: @@ -858,16 +865,14 @@ def on_key_press(self, symbol, modifiers): @staticmethod def _time_event(dt, self): - """The timer callback. - """ + """The timer callback.""" # Don't run old dead events after we've already closed if not self._is_active: return - if self.viewer_flags['record']: + if self.viewer_flags["record"]: self._record() - if (self.viewer_flags['rotate'] and not - self.viewer_flags['mouse_pressed']): + if self.viewer_flags["rotate"] and not self.viewer_flags["mouse_pressed"]: self._rotate() # Manage message opacity @@ -896,8 +901,8 @@ def _reset_view(self): scale = DEFAULT_SCENE_SCALE centroid = self.scene.centroid - if self.viewer_flags['view_center'] is not None: - centroid = self.viewer_flags['view_center'] + if self.viewer_flags["view_center"] is not None: + centroid = self.viewer_flags["view_center"] self._camera_node.matrix = self._default_camera_pose self._trackball = Trackball( @@ -906,20 +911,21 @@ def _reset_view(self): def _get_save_filename(self, file_exts): file_types = { - 'png': ('png files', '*.png'), - 'jpg': ('jpeg files', '*.jpg'), - 'gif': ('gif files', '*.gif'), - 'all': ('all files', '*'), + "png": ("png files", "*.png"), + "jpg": ("jpeg files", "*.jpg"), + "gif": ("gif files", "*.gif"), + "all": ("all files", "*"), } filetypes = [file_types[x] for x in file_exts] try: root = Tk() - save_dir = self.viewer_flags['save_directory'] + save_dir = self.viewer_flags["save_directory"] if save_dir is None: save_dir = os.getcwd() filename = filedialog.asksaveasfilename( - initialdir=save_dir, title='Select file save location', - filetypes=filetypes + initialdir=save_dir, + title="Select file save location", + filetypes=filetypes, ) except Exception: return None @@ -930,34 +936,30 @@ def _get_save_filename(self, file_exts): return filename def _save_image(self): - filename = self._get_save_filename(['png', 'jpg', 'gif', 'all']) + filename = self._get_save_filename(["png", "jpg", "gif", "all"]) if filename is not None: - self.viewer_flags['save_directory'] = os.path.dirname(filename) + self.viewer_flags["save_directory"] = os.path.dirname(filename) imageio.imwrite(filename, self._renderer.read_color_buf()) def _record(self): - """Save another frame for the GIF. - """ + """Save another frame for the GIF.""" data = self._renderer.read_color_buf() if not np.all(data == 0.0): self._saved_frames.append(data) def _rotate(self): - """Animate the scene by rotating the camera. - """ - az = (self.viewer_flags['rotate_rate'] / - self.viewer_flags['refresh_rate']) - self._trackball.rotate(az, self.viewer_flags['rotate_axis']) + """Animate the scene by rotating the camera.""" + az = self.viewer_flags["rotate_rate"] / self.viewer_flags["refresh_rate"] + self._trackball.rotate(az, self.viewer_flags["rotate_axis"]) def _render(self): - """Render the scene into the framebuffer and flip. - """ + """Render the scene into the framebuffer and flip.""" scene = self.scene self._camera_node.matrix = self._trackball.pose.copy() # Set lighting - vli = self.viewer_flags['lighting_intensity'] - if self.viewer_flags['use_raymond_lighting']: + vli = self.viewer_flags["lighting_intensity"] + if self.viewer_flags["use_raymond_lighting"]: for n in self._raymond_lights: n.light.intensity = vli / 3.0 if not self.scene.has_node(n): @@ -968,29 +970,27 @@ def _render(self): if self.scene.has_node(n): self.scene.remove_node(n) - if self.viewer_flags['use_direct_lighting']: + if self.viewer_flags["use_direct_lighting"]: if not self.scene.has_node(self._direct_light): - scene.add_node( - self._direct_light, parent_node=self._camera_node - ) + scene.add_node(self._direct_light, parent_node=self._camera_node) elif self.scene.has_node(self._direct_light): self.scene.remove_node(self._direct_light) flags = RenderFlags.NONE - if self.render_flags['flip_wireframe']: + if self.render_flags["flip_wireframe"]: flags |= RenderFlags.FLIP_WIREFRAME - elif self.render_flags['all_wireframe']: + elif self.render_flags["all_wireframe"]: flags |= RenderFlags.ALL_WIREFRAME - elif self.render_flags['all_solid']: + elif self.render_flags["all_solid"]: flags |= RenderFlags.ALL_SOLID - if self.render_flags['shadows']: + if self.render_flags["shadows"]: flags |= RenderFlags.SHADOWS_DIRECTIONAL | RenderFlags.SHADOWS_SPOT - if self.render_flags['vertex_normals']: + if self.render_flags["vertex_normals"]: flags |= RenderFlags.VERTEX_NORMALS - if self.render_flags['face_normals']: + if self.render_flags["face_normals"]: flags |= RenderFlags.FACE_NORMALS - if not self.render_flags['cull_faces']: + if not self.render_flags["cull_faces"]: flags |= RenderFlags.SKIP_CULL_FACES self._renderer.render(self.scene, flags) @@ -1000,60 +1000,72 @@ def _init_and_start_app(self): # and multisampling and removing these options if exception # Note: multisampling not available on all hardware from pyglet.gl import Config - confs = [Config(sample_buffers=1, samples=4, - depth_size=24, - double_buffer=True, - major_version=TARGET_OPEN_GL_MAJOR, - minor_version=TARGET_OPEN_GL_MINOR), - Config(depth_size=24, - double_buffer=True, - major_version=TARGET_OPEN_GL_MAJOR, - minor_version=TARGET_OPEN_GL_MINOR), - Config(sample_buffers=1, samples=4, - depth_size=24, - double_buffer=True, - major_version=MIN_OPEN_GL_MAJOR, - minor_version=MIN_OPEN_GL_MINOR), - Config(depth_size=24, - double_buffer=True, - major_version=MIN_OPEN_GL_MAJOR, - minor_version=MIN_OPEN_GL_MINOR)] + + confs = [ + Config( + sample_buffers=1, + samples=4, + depth_size=24, + double_buffer=True, + major_version=TARGET_OPEN_GL_MAJOR, + minor_version=TARGET_OPEN_GL_MINOR, + ), + Config( + depth_size=24, + double_buffer=True, + major_version=TARGET_OPEN_GL_MAJOR, + minor_version=TARGET_OPEN_GL_MINOR, + ), + Config( + sample_buffers=1, + samples=4, + depth_size=24, + double_buffer=True, + major_version=MIN_OPEN_GL_MAJOR, + minor_version=MIN_OPEN_GL_MINOR, + ), + Config( + depth_size=24, + double_buffer=True, + major_version=MIN_OPEN_GL_MAJOR, + minor_version=MIN_OPEN_GL_MINOR, + ), + ] for conf in confs: try: - super(Viewer, self).__init__(config=conf, resizable=True, - width=self._viewport_size[0], - height=self._viewport_size[1]) + super(Viewer, self).__init__( + config=conf, + resizable=True, + width=self._viewport_size[0], + height=self._viewport_size[1], + ) break except pyglet.window.NoSuchConfigException: pass if not self.context: - raise ValueError('Unable to initialize an OpenGL 3+ context') + raise ValueError("Unable to initialize an OpenGL 3+ context") clock.schedule_interval( - Viewer._time_event, 1.0 / self.viewer_flags['refresh_rate'], self + Viewer._time_event, 1.0 / self.viewer_flags["refresh_rate"], self ) self.switch_to() - self.set_caption(self.viewer_flags['window_title']) + self.set_caption(self.viewer_flags["window_title"]) pyglet.app.run() def _compute_initial_camera_pose(self): centroid = self.scene.centroid - if self.viewer_flags['view_center'] is not None: - centroid = self.viewer_flags['view_center'] + if self.viewer_flags["view_center"] is not None: + centroid = self.viewer_flags["view_center"] scale = self.scene.scale if scale == 0.0: scale = DEFAULT_SCENE_SCALE s2 = 1.0 / np.sqrt(2.0) cp = np.eye(4) - cp[:3,:3] = np.array([ - [0.0, -s2, s2], - [1.0, 0.0, 0.0], - [0.0, s2, s2] - ]) + cp[:3, :3] = np.array([[0.0, -s2, s2], [1.0, 0.0, 0.0], [0.0, s2, s2]]) hfov = np.pi / 6.0 dist = scale / (2.0 * np.tan(hfov)) - cp[:3,3] = dist * np.array([1.0, 0.0, 1.0]) + centroid + cp[:3, 3] = dist * np.array([1.0, 0.0, 1.0]) + centroid return cp @@ -1077,11 +1089,13 @@ def _create_raymond_lights(self): y = np.cross(z, x) matrix = np.eye(4) - matrix[:3,:3] = np.c_[x,y,z] - nodes.append(Node( - light=DirectionalLight(color=np.ones(3), intensity=1.0), - matrix=matrix - )) + matrix[:3, :3] = np.c_[x, y, z] + nodes.append( + Node( + light=DirectionalLight(color=np.ones(3), intensity=1.0), + matrix=matrix, + ) + ) return nodes @@ -1093,14 +1107,14 @@ def _create_direct_light(self): def _set_axes(self, world, mesh): scale = self.scene.scale if world: - if 'scene' not in self._axes: + if "scene" not in self._axes: n = Node(mesh=self._axis_mesh, scale=np.ones(3) * scale * 0.3) self.scene.add_node(n) - self._axes['scene'] = n + self._axes["scene"] = n else: - if 'scene' in self._axes: - self.scene.remove_node(self._axes['scene']) - self._axes.pop('scene') + if "scene" in self._axes: + self.scene.remove_node(self._axes["scene"]) + self._axes.pop("scene") if mesh: old_nodes = [] @@ -1112,10 +1126,7 @@ def _set_axes(self, world, mesh): for node in old_nodes: if node in self._axes: continue - n = Node( - mesh=self._axis_mesh, - scale=np.ones(3) * node.mesh.scale * 0.5 - ) + n = Node(mesh=self._axis_mesh, scale=np.ones(3) * node.mesh.scale * 0.5) self.scene.add_node(n, parent_node=node) self._axes[node] = n else: @@ -1139,8 +1150,7 @@ def _location_to_x_y(self, location): elif location == TextAlign.CENTER_LEFT: return (TEXT_PADDING, self.viewport_size[1] / 2.0) elif location == TextAlign.CENTER_RIGHT: - return (self.viewport_size[0] - TEXT_PADDING, - self.viewport_size[1] / 2.0) + return (self.viewport_size[0] - TEXT_PADDING, self.viewport_size[1] / 2.0) elif location == TextAlign.BOTTOM_LEFT: return (TEXT_PADDING, TEXT_PADDING) elif location == TextAlign.BOTTOM_RIGHT: @@ -1150,11 +1160,12 @@ def _location_to_x_y(self, location): elif location == TextAlign.TOP_LEFT: return (TEXT_PADDING, self.viewport_size[1] - TEXT_PADDING) elif location == TextAlign.TOP_RIGHT: - return (self.viewport_size[0] - TEXT_PADDING, - self.viewport_size[1] - TEXT_PADDING) + return ( + self.viewport_size[0] - TEXT_PADDING, + self.viewport_size[1] - TEXT_PADDING, + ) elif location == TextAlign.TOP_CENTER: - return (self.viewport_size[0] / 2.0, - self.viewport_size[1] - TEXT_PADDING) + return (self.viewport_size[0] / 2.0, self.viewport_size[1] - TEXT_PADDING) -__all__ = ['Viewer'] +__all__ = ["Viewer"] diff --git a/setup.py b/setup.py index c3b5ba0..32a9b70 100644 --- a/setup.py +++ b/setup.py @@ -3,74 +3,83 @@ Author: Matthew Matl """ -import sys from setuptools import setup # load __version__ -exec(open('pyrender/version.py').read()) +exec(open("pyrender/version.py").read()) -def get_imageio_dep(): - if sys.version[0] == "2": - return 'imageio<=2.6.1' - return 'imageio' requirements = [ - 'freetype-py', # For font loading - get_imageio_dep(), # For Image I/O - 'networkx', # For the scene graph - 'numpy', # Numpy - 'Pillow', # For Trimesh texture conversions - 'pyglet>=1.4.10', # For the pyglet viewer - 'PyOpenGL~=3.1.0', # For OpenGL -# 'PyOpenGL_accelerate~=3.1.0', # For OpenGL - 'scipy', # Because of trimesh missing dep - 'six', # For Python 2/3 interop - 'trimesh', # For meshes + "freetype-py", # For font loading + 'imageio<=2.6.1; python_version < "3"', # For Image I/O + 'imageio<=2.15.0; python_version <= "3.6" and python_version >= "3"', + 'imageio; python_version > "3.6"', + 'networkx<=2.2; python_version < "3"', # For the scene graph + 'networkx; python_version >= "3"', + 'numpy<=1.16.6; python_version < "3"', # Numpy + 'numpy<=1.19.5; python_version <= "3.6" and python_version >= "3"', + 'numpy; python_version > "3.6"', + 'Pillow<=6.2.2; python_version < "3"', # For Trimesh texture conversions + 'Pillow; python_version >= "3"', + 'pyglet<=1.4.11; python_version < "3"', # For the pyglet viewer + 'pyglet; python_version >= "3"', + "PyOpenGL~=3.1.0", # For OpenGL + # 'PyOpenGL_accelerate==3.1.0', # For OpenGL + 'scipy<=1.5.4; python_version <= "3.6"', # Because of trimesh missing dep + 'scipy; python_version > "3.6"', + "six", # For Python 2/3 interop + "trimesh", # For meshes + 'decorator<=4.4.2; python_version < "3"', + 'setuptools_scm; python_version < "3"', ] dev_requirements = [ - 'flake8', # Code formatting checker - 'pre-commit', # Pre-commit hooks - 'pytest', # Code testing - 'pytest-cov', # Coverage testing - 'tox', # Automatic virtualenv testing + "flake8", # Code formatting checker + "pre-commit", # Pre-commit hooks + "pytest", # Code testing + "pytest-cov", # Coverage testing + "tox", # Automatic virtualenv testing ] docs_requirements = [ - 'sphinx', # General doc library - 'sphinx_rtd_theme', # RTD theme for sphinx - 'sphinx-automodapi' # For generating nice tables + "sphinx", # General doc library + "sphinx_rtd_theme", # RTD theme for sphinx + "sphinx-automodapi", # For generating nice tables ] setup( - name = 'pyrender', + name="pyrender", version=__version__, - description='Easy-to-use Python renderer for 3D visualization', - long_description='A simple implementation of Physically-Based Rendering ' - '(PBR) in Python. Compliant with the glTF 2.0 standard.', - author='Matthew Matl', - author_email='matthewcmatl@gmail.com', - license='MIT License', - url = 'https://github.com/mmatl/pyrender', - classifiers = [ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX :: Linux', - 'Operating System :: MacOS :: MacOS X', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Natural Language :: English', - 'Topic :: Scientific/Engineering' + description="Easy-to-use Python renderer for 3D visualization", + long_description="A simple implementation of Physically-Based Rendering " + "(PBR) in Python. Compliant with the glTF 2.0 standard.", + author="Matthew Matl", + author_email="matthewcmatl@gmail.com", + license="MIT License", + url="https://github.com/mmatl/pyrender", + classifiers=[ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Natural Language :: English", + "Topic :: Scientific/Engineering", ], - keywords = 'rendering graphics opengl 3d visualization pbr gltf', - packages = ['pyrender', 'pyrender.platforms'], - setup_requires = requirements, - install_requires = requirements, + keywords="rendering graphics opengl 3d visualization pbr gltf", + packages=["pyrender", "pyrender.platforms"], + setup_requires=requirements, + install_requires=requirements, extras_require={ - 'dev': dev_requirements, - 'docs': docs_requirements, + "dev": dev_requirements, + "docs": docs_requirements, }, - include_package_data=True + include_package_data=True, ) diff --git a/tests/unit/test_cameras.py b/tests/unit/test_cameras.py index 7544ad8..7102dde 100644 --- a/tests/unit/test_cameras.py +++ b/tests/unit/test_cameras.py @@ -23,7 +23,7 @@ def test_perspective_camera(): assert p.znear == 0.05 assert p.zfar is None assert p.aspectRatio is None - p.name = 'asdf' + p.name = "asdf" p.name = None with pytest.raises(ValueError): @@ -67,26 +67,38 @@ def test_perspective_camera(): assert np.allclose( p.get_projection_matrix(width, height), - np.array([ - [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], - [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], - [0.0, 0.0, (zfar + znear) / (znear - zfar), - (2 * zfar * znear) / (znear - zfar)], - [0.0, 0.0, -1.0, 0.0] - ]) + np.array( + [ + [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], + [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], + [ + 0.0, + 0.0, + (zfar + znear) / (znear - zfar), + (2 * zfar * znear) / (znear - zfar), + ], + [0.0, 0.0, -1.0, 0.0], + ] + ), ) # NFA p.aspectRatio = aspectRatio assert np.allclose( p.get_projection_matrix(width, height), - np.array([ - [1.0 / (aspectRatio * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], - [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], - [0.0, 0.0, (zfar + znear) / (znear - zfar), - (2 * zfar * znear) / (znear - zfar)], - [0.0, 0.0, -1.0, 0.0] - ]) + np.array( + [ + [1.0 / (aspectRatio * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], + [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], + [ + 0.0, + 0.0, + (zfar + znear) / (znear - zfar), + (2 * zfar * znear) / (znear - zfar), + ], + [0.0, 0.0, -1.0, 0.0], + ] + ), ) assert np.allclose( p.get_projection_matrix(), p.get_projection_matrix(width, height) @@ -97,12 +109,14 @@ def test_perspective_camera(): p.aspectRatio = None assert np.allclose( p.get_projection_matrix(width, height), - np.array([ - [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], - [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], - [0.0, 0.0, -1.0, -2.0 * znear], - [0.0, 0.0, -1.0, 0.0] - ]) + np.array( + [ + [1.0 / (width / height * np.tan(yfov / 2.0)), 0.0, 0.0, 0.0], + [0.0, 1.0 / np.tan(yfov / 2.0), 0.0, 0.0], + [0.0, 0.0, -1.0, -2.0 * znear], + [0.0, 0.0, -1.0, 0.0], + ] + ), ) @@ -155,10 +169,12 @@ def test_orthographic_camera(): assert np.allclose( c.get_projection_matrix(), - np.array([ - [1.0 / xm, 0, 0, 0], - [0, 1.0 / ym, 0, 0], - [0, 0, 2.0 / (n - f), (f + n) / (n - f)], - [0, 0, 0, 1.0] - ]) + np.array( + [ + [1.0 / xm, 0, 0, 0], + [0, 1.0 / ym, 0, 0], + [0, 0, 2.0 / (n - f), (f + n) / (n - f)], + [0, 0, 0, 1.0], + ] + ), ) diff --git a/tests/unit/test_egl.py b/tests/unit/test_egl.py index e2f4bef..06a2766 100644 --- a/tests/unit/test_egl.py +++ b/tests/unit/test_egl.py @@ -1,3 +1,4 @@ +# flake8: noqa # from pyrender.platforms import egl diff --git a/tests/unit/test_lights.py b/tests/unit/test_lights.py index ffde856..6ab5b3d 100644 --- a/tests/unit/test_lights.py +++ b/tests/unit/test_lights.py @@ -1,8 +1,14 @@ import numpy as np import pytest -from pyrender import (DirectionalLight, SpotLight, PointLight, Texture, - PerspectiveCamera, OrthographicCamera) +from pyrender import ( + DirectionalLight, + SpotLight, + PointLight, + Texture, + PerspectiveCamera, + OrthographicCamera, +) from pyrender.constants import SHADOW_TEX_SZ @@ -13,7 +19,7 @@ def test_directional_light(): assert np.all(d.color == 1.0) assert d.intensity == 1.0 - d.name = 'direc' + d.name = "direc" with pytest.raises(ValueError): d.color = None with pytest.raises(TypeError): diff --git a/tests/unit/test_meshes.py b/tests/unit/test_meshes.py index 7070b01..f995890 100644 --- a/tests/unit/test_meshes.py +++ b/tests/unit/test_meshes.py @@ -2,7 +2,7 @@ import pytest import trimesh -from pyrender import (Mesh, Primitive) +from pyrender import Mesh, Primitive def test_meshes(): @@ -20,17 +20,14 @@ def test_meshes(): assert x.is_visible assert x.weights is None - x.name = 'str' + x.name = "str" # From Trimesh x = Mesh.from_trimesh(trimesh.creation.box()) assert isinstance(x, Mesh) assert len(x.primitives) == 1 assert x.is_visible - assert np.allclose(x.bounds, np.array([ - [-0.5, -0.5, -0.5], - [0.5, 0.5, 0.5] - ])) + assert np.allclose(x.bounds, np.array([[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]])) assert np.allclose(x.centroid, np.zeros(3)) assert np.allclose(x.extents, np.ones(3)) assert np.allclose(x.scale, np.sqrt(3)) @@ -49,10 +46,7 @@ def test_meshes(): with pytest.raises(TypeError): x.material = np.zeros(10) assert x.targets is None - assert np.allclose(x.bounds, np.array([ - [-0.5, -0.5, -0.5], - [0.5, 0.5, 0.5] - ])) + assert np.allclose(x.bounds, np.array([[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]])) assert np.allclose(x.centroid, np.zeros(3)) assert np.allclose(x.extents, np.ones(3)) assert np.allclose(x.scale, np.sqrt(3)) @@ -60,16 +54,14 @@ def test_meshes(): assert x.is_transparent # From two trimeshes - x = Mesh.from_trimesh([trimesh.creation.box(), - trimesh.creation.cylinder(radius=0.1, height=2.0)], - smooth=False) + x = Mesh.from_trimesh( + [trimesh.creation.box(), trimesh.creation.cylinder(radius=0.1, height=2.0)], + smooth=False, + ) assert isinstance(x, Mesh) assert len(x.primitives) == 2 assert x.is_visible - assert np.allclose(x.bounds, np.array([ - [-0.5, -0.5, -1.0], - [0.5, 0.5, 1.0] - ])) + assert np.allclose(x.bounds, np.array([[-0.5, -0.5, -1.0], [0.5, 0.5, 1.0]])) assert np.allclose(x.centroid, np.zeros(3)) assert np.allclose(x.extents, [1.0, 1.0, 2.0]) assert np.allclose(x.scale, np.sqrt(6)) @@ -80,13 +72,10 @@ def test_meshes(): x = Mesh.from_trimesh(None) # With instancing - poses = np.tile(np.eye(4), (5,1,1)) - poses[:,0,3] = np.array([0,1,2,3,4]) + poses = np.tile(np.eye(4), (5, 1, 1)) + poses[:, 0, 3] = np.array([0, 1, 2, 3, 4]) x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) - assert np.allclose(x.bounds, np.array([ - [-0.5, -0.5, -0.5], - [4.5, 0.5, 0.5] - ])) + assert np.allclose(x.bounds, np.array([[-0.5, -0.5, -0.5], [4.5, 0.5, 0.5]])) poses = np.eye(4) x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) poses = np.eye(3) @@ -94,7 +83,7 @@ def test_meshes(): x = Mesh.from_trimesh(trimesh.creation.box(), poses=poses) # From textured meshes - fm = trimesh.load('tests/data/fuze.obj') + fm = trimesh.load("tests/data/fuze.obj") x = Mesh.from_trimesh(fm) assert isinstance(x, Mesh) assert len(x.primitives) == 1 @@ -115,7 +104,8 @@ def test_meshes(): assert x.primitives[0].color_0 is not None assert x.is_transparent - bm = trimesh.load('tests/data/WaterBottle.glb').dump()[0] + # bm = trimesh.load("tests/data/WaterBottle.glb").dump()[0] + bm = trimesh.load("tests/data/WaterBottle.glb").geometry["WaterBottle"] x = Mesh.from_trimesh(bm) assert x.primitives[0].material.baseColorTexture is not None assert x.primitives[0].material.emissiveTexture is not None @@ -124,6 +114,7 @@ def test_meshes(): # From point cloud x = Mesh.from_points(fm.vertices) + # def test_duck(): # bm = trimesh.load('tests/data/Duck.glb').dump()[0] # x = Mesh.from_trimesh(bm) diff --git a/tests/unit/test_nodes.py b/tests/unit/test_nodes.py index 9857c82..9be6387 100644 --- a/tests/unit/test_nodes.py +++ b/tests/unit/test_nodes.py @@ -2,7 +2,7 @@ import pytest from trimesh import transformations -from pyrender import (DirectionalLight, PerspectiveCamera, Mesh, Node) +from pyrender import DirectionalLight, PerspectiveCamera, Mesh, Node def test_nodes(): @@ -14,13 +14,13 @@ def test_nodes(): assert x.skin is None assert np.allclose(x.matrix, np.eye(4)) assert x.mesh is None - assert np.allclose(x.rotation, [0,0,0,1]) + assert np.allclose(x.rotation, [0, 0, 0, 1]) assert np.allclose(x.scale, np.ones(3)) assert np.allclose(x.translation, np.zeros(3)) assert x.weights is None assert x.light is None - x.name = 'node' + x.name = "node" # Test node light/camera/mesh tests c = PerspectiveCamera(yfov=2.0) @@ -53,15 +53,15 @@ def test_nodes(): Mx = transformations.rotation_matrix(np.pi / 2.0, x) qx = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, x), -1) Mxt = Mx.copy() - Mxt[:3,3] = t + Mxt[:3, 3] = t S = np.eye(4) - S[:3,:3] = np.diag(s) + S[:3, :3] = np.diag(s) Mxts = Mxt.dot(S) My = transformations.rotation_matrix(np.pi / 2.0, y) qy = np.roll(transformations.quaternion_about_axis(np.pi / 2.0, y), -1) Myt = My.copy() - Myt[:3,3] = t + Myt[:3, 3] = t x = Node(matrix=Mx) assert np.allclose(x.matrix, Mx) @@ -102,8 +102,8 @@ def test_nodes(): assert np.allclose(x.translation[0], 0) x.matrix = np.eye(4) - x.matrix[0,0] = 500 - assert x.matrix[0,0] == 1.0 + x.matrix[0, 0] = 500 + assert x.matrix[0, 0] == 1.0 # Failures with pytest.raises(ValueError): @@ -111,14 +111,14 @@ def test_nodes(): with pytest.raises(ValueError): x.matrix = np.eye(5) with pytest.raises(ValueError): - x.matrix = np.eye(4).dot([5,1,1,1]) + x.matrix = np.eye(4).dot([5, 1, 1, 1]) with pytest.raises(ValueError): - x.rotation = np.array([1,2]) + x.rotation = np.array([1, 2]) with pytest.raises(ValueError): - x.rotation = np.array([1,2,3]) + x.rotation = np.array([1, 2, 3]) with pytest.raises(ValueError): - x.rotation = np.array([1,2,3,4]) + x.rotation = np.array([1, 2, 3, 4]) with pytest.raises(ValueError): - x.translation = np.array([1,2,3,4]) + x.translation = np.array([1, 2, 3, 4]) with pytest.raises(ValueError): - x.scale = np.array([1,2,3,4]) + x.scale = np.array([1, 2, 3, 4]) diff --git a/tests/unit/test_offscreen.py b/tests/unit/test_offscreen.py index 88983b0..02f878f 100644 --- a/tests/unit/test_offscreen.py +++ b/tests/unit/test_offscreen.py @@ -1,37 +1,46 @@ import numpy as np import trimesh -from pyrender import (OffscreenRenderer, PerspectiveCamera, DirectionalLight, - SpotLight, Mesh, Node, Scene) +from pyrender import ( + OffscreenRenderer, + PerspectiveCamera, + DirectionalLight, + SpotLight, + Mesh, + Node, + Scene, +) def test_offscreen_renderer(tmpdir): # Fuze trimesh - fuze_trimesh = trimesh.load('examples/models/fuze.obj') + fuze_trimesh = trimesh.load("examples/models/fuze.obj") fuze_mesh = Mesh.from_trimesh(fuze_trimesh) # Drill trimesh - drill_trimesh = trimesh.load('examples/models/drill.obj') + drill_trimesh = trimesh.load("examples/models/drill.obj") drill_mesh = Mesh.from_trimesh(drill_trimesh) drill_pose = np.eye(4) - drill_pose[0,3] = 0.1 - drill_pose[2,3] = -np.min(drill_trimesh.vertices[:,2]) + drill_pose[0, 3] = 0.1 + drill_pose[2, 3] = -np.min(drill_trimesh.vertices[:, 2]) # Wood trimesh - wood_trimesh = trimesh.load('examples/models/wood.obj') + wood_trimesh = trimesh.load("examples/models/wood.obj") wood_mesh = Mesh.from_trimesh(wood_trimesh) # Water bottle trimesh - bottle_gltf = trimesh.load('examples/models/WaterBottle.glb') + bottle_gltf = trimesh.load("examples/models/WaterBottle.glb") bottle_trimesh = bottle_gltf.geometry[list(bottle_gltf.geometry.keys())[0]] bottle_mesh = Mesh.from_trimesh(bottle_trimesh) - bottle_pose = np.array([ - [1.0, 0.0, 0.0, 0.1], - [0.0, 0.0, -1.0, -0.16], - [0.0, 1.0, 0.0, 0.13], - [0.0, 0.0, 0.0, 1.0], - ]) + bottle_pose = np.array( + [ + [1.0, 0.0, 0.0, 0.1], + [0.0, 0.0, -1.0, -0.16], + [0.0, 1.0, 0.0, 0.13], + [0.0, 0.0, 0.0, 1.0], + ] + ) boxv_trimesh = trimesh.creation.box(extents=0.1 * np.ones(3)) boxv_vertex_colors = np.random.uniform(size=(boxv_trimesh.vertices.shape)) @@ -41,9 +50,9 @@ def test_offscreen_renderer(tmpdir): boxf_face_colors = np.random.uniform(size=boxf_trimesh.faces.shape) boxf_trimesh.visual.face_colors = boxf_face_colors # Instanced - poses = np.tile(np.eye(4), (2,1,1)) - poses[0,:3,3] = np.array([-0.1, -0.10, 0.05]) - poses[1,:3,3] = np.array([-0.15, -0.10, 0.05]) + poses = np.tile(np.eye(4), (2, 1, 1)) + poses[0, :3, 3] = np.array([-0.1, -0.10, 0.05]) + poses[1, :3, 3] = np.array([-0.15, -0.10, 0.05]) boxf_mesh = Mesh.from_trimesh(boxf_trimesh, poses=poses, smooth=False) points = trimesh.creation.icosphere(radius=0.05).vertices @@ -51,22 +60,29 @@ def test_offscreen_renderer(tmpdir): points_mesh = Mesh.from_points(points, colors=point_colors) direc_l = DirectionalLight(color=np.ones(3), intensity=1.0) - spot_l = SpotLight(color=np.ones(3), intensity=10.0, - innerConeAngle=np.pi / 16, outerConeAngle=np.pi / 6) + spot_l = SpotLight( + color=np.ones(3), + intensity=10.0, + innerConeAngle=np.pi / 16, + outerConeAngle=np.pi / 6, + ) cam = PerspectiveCamera(yfov=(np.pi / 3.0)) - cam_pose = np.array([ - [0.0, -np.sqrt(2) / 2, np.sqrt(2) / 2, 0.5], - [1.0, 0.0, 0.0, 0.0], - [0.0, np.sqrt(2) / 2, np.sqrt(2) / 2, 0.4], - [0.0, 0.0, 0.0, 1.0] - ]) + cam_pose = np.array( + [ + [0.0, -np.sqrt(2) / 2, np.sqrt(2) / 2, 0.5], + [1.0, 0.0, 0.0, 0.0], + [0.0, np.sqrt(2) / 2, np.sqrt(2) / 2, 0.4], + [0.0, 0.0, 0.0, 1.0], + ] + ) scene = Scene(ambient_light=np.array([0.02, 0.02, 0.02])) - fuze_node = Node(mesh=fuze_mesh, translation=np.array([ - 0.1, 0.15, -np.min(fuze_trimesh.vertices[:,2]) - ])) + fuze_node = Node( + mesh=fuze_mesh, + translation=np.array([0.1, 0.15, -np.min(fuze_trimesh.vertices[:, 2])]), + ) scene.add_node(fuze_node) boxv_node = Node(mesh=boxv_mesh, translation=np.array([-0.1, 0.10, 0.05])) scene.add_node(boxv_node) diff --git a/tests/unit/test_scenes.py b/tests/unit/test_scenes.py index d85dd71..ef5a842 100644 --- a/tests/unit/test_scenes.py +++ b/tests/unit/test_scenes.py @@ -2,8 +2,16 @@ import pytest import trimesh -from pyrender import (Mesh, PerspectiveCamera, DirectionalLight, - SpotLight, PointLight, Scene, Node, OrthographicCamera) +from pyrender import ( + Mesh, + PerspectiveCamera, + DirectionalLight, + SpotLight, + PointLight, + Scene, + Node, + OrthographicCamera, +) def test_scenes(): @@ -14,7 +22,7 @@ def test_scenes(): assert np.allclose(s.ambient_light, np.zeros(3)) assert len(s.nodes) == 0 assert s.name is None - s.name = 'asdf' + s.name = "asdf" s.bg_color = None s.ambient_light = None assert np.allclose(s.bg_color, np.ones(4)) @@ -40,7 +48,7 @@ def test_scenes(): assert np.all(s.scale == 0) # From trimesh scene - tms = trimesh.load('tests/data/WaterBottle.glb') + tms = trimesh.load("tests/data/WaterBottle.glb") s = Scene.from_trimesh_scene(tms) assert len(s.meshes) == 1 assert len(s.mesh_nodes) == 1 @@ -84,14 +92,14 @@ def test_scenes(): with pytest.raises(ValueError): s.set_pose(n3, np.eye(4)) tf = np.eye(4) - tf[:3,3] = np.ones(3) + tf[:3, 3] = np.ones(3) s.set_pose(n1, tf) assert np.allclose(s.get_pose(n1), tf) assert np.allclose(s.get_pose(n2), np.eye(4)) nodes = [n1, n2, n3] tf2 = np.eye(4) - tf2[:3,:3] = np.diag([-1,-1,1]) + tf2[:3, :3] = np.diag([-1, -1, 1]) n1.children.append(n2) n1.matrix = tf n2.matrix = tf2 @@ -134,14 +142,14 @@ def test_scenes(): # Now test ADD function s = Scene() - m = Mesh([], name='m') + m = Mesh([], name="m") cp = PerspectiveCamera(yfov=2.0) co = OrthographicCamera(xmag=1.0, ymag=1.0) dl = DirectionalLight() pl = PointLight() sl = SpotLight() - n1 = s.add(m, name='mn') + n1 = s.add(m, name="mn") assert n1.mesh == m assert len(s.nodes) == 1 assert len(s.mesh_nodes) == 1 @@ -153,13 +161,13 @@ def test_scenes(): assert len(s.nodes) == len(s.mesh_nodes) == 2 assert len(s.meshes) == 1 assert len(s.get_nodes(node=n1)) == 1 - assert len(s.get_nodes(node=n1, name='mn')) == 1 - assert len(s.get_nodes(name='mn')) == 1 + assert len(s.get_nodes(node=n1, name="mn")) == 1 + assert len(s.get_nodes(name="mn")) == 1 assert len(s.get_nodes(obj=m)) == 2 - assert len(s.get_nodes(obj=m, obj_name='m')) == 2 + assert len(s.get_nodes(obj=m, obj_name="m")) == 2 assert len(s.get_nodes(obj=co)) == 0 - nsl = s.add(sl, name='sln') - npl = s.add(pl, parent_name='sln') + nsl = s.add(sl, name="sln") + npl = s.add(pl, parent_name="sln") assert nsl.children[0] == npl ndl = s.add(dl, parent_node=npl) assert npl.children[0] == ndl @@ -200,11 +208,11 @@ def test_scenes(): with pytest.raises(ValueError): s.add(m, parent_node=n1) with pytest.raises(ValueError): - s.add(m, name='asdf') - s.add(m, name='asdf') - s.add(m, parent_name='asdf') + s.add(m, name="asdf") + s.add(m, name="asdf") + s.add(m, parent_name="asdf") with pytest.raises(ValueError): - s.add(m, parent_name='asfd') + s.add(m, parent_name="asfd") with pytest.raises(TypeError): s.add(None) @@ -226,7 +234,7 @@ def test_scenes(): s.add_node(n3, parent_node=n2) assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.0, 0.5, 1.5]]) tf = np.eye(4) - tf[:3,3] = np.ones(3) + tf[:3, 3] = np.ones(3) s.set_pose(n3, tf) assert np.allclose(s.bounds, [[-0.5, -0.5, -0.5], [2.5, 1.5, 1.5]]) s.remove_node(n2) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d43af2a --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +envlist = py{27,36,37,38,39,310} + +[testenv] +wheel=true +deps= + pytest + pytest-cov + coveralls +commands= + pytest --cov=pyrender tests + {posargs} +setenv= + PYOPENGL_PLATFORM=osmesa +passenv=GITHUB_* + +[testenv:linting] +skip_install = True +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure + +[testenv:docs] +usedevelop = True +deps = + sphinx + sphinx_rtd_theme + sphinx-automodapi +commands = + sphinx-build -W --keep-going -b html docs/source docs/source/_build