├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── .gitignore ├── Makefile ├── conf.py ├── images │ ├── tutorial1.png │ └── tutorial2.png ├── index.rst ├── programs.rst └── tutorial.rst ├── examples ├── __init__.py ├── android.obj ├── avoid.py ├── bronze.jpg ├── bus.jpg ├── bus.obj ├── bus.py ├── circles.py ├── context.py ├── csg.py ├── cuboids.py ├── earth.png ├── field.py ├── font.py ├── gusev.jpg ├── gusev.py ├── hirise.py ├── ladybug.obj ├── lego.obj ├── lego.py ├── moving_spheres.py ├── pipes.py ├── poisson_spheres.py ├── scenes.py ├── sphere.py ├── sprites.py ├── starfield.py ├── suzanne.obj ├── suzanne.py ├── teapot.obj ├── temp.py ├── terrain.py ├── texture.jpg ├── textured_sphere.py └── tutorial.py ├── main.py ├── pg ├── __init__.py ├── camera.py ├── core.py ├── csg.py ├── font.py ├── geometry.py ├── gl.py ├── glfw.py ├── matrix.py ├── noise.py ├── obj.py ├── pack.py ├── poisson.py ├── programs.py ├── sprite.py ├── stl.py ├── util.py └── wasd.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | screenshots 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pg: The Python Graphics Framework 2 | 3 | pg is a lightweight, high-level OpenGL graphics framework for Python. It is a 4 | work in progress. 5 | 6 | ### Tutorial 7 | 8 | A basic tutorial is available here: 9 | 10 | http://pg.readthedocs.org/en/latest/tutorial.html 11 | 12 | ### Features 13 | 14 | Many OpenGL applications have a lot of features in common, but there's a lot of 15 | boilerplate involved when using OpenGL. This high-level framework lets you 16 | focus on your application-specific functionality instead. 17 | 18 | * shaders 19 | * compile and link 20 | * attributes and uniforms 21 | * built-in shaders for common use-cases 22 | * vertex buffers 23 | * optionally interleaved 24 | * matrices 25 | * translate, rotate, scale 26 | * perspective and orthographic projections 27 | * transpose, determinant, inverse 28 | * textures 29 | * geometric shapes 30 | * sphere, cuboid, plane, cylinder, cone, axes 31 | * models 32 | * .obj and .stl file formats 33 | * WASD movement 34 | * built-in! 35 | * windowing and input 36 | * glfw-based 37 | * multiple windows 38 | 39 | ### Dependencies 40 | 41 | brew tap homebrew/versions 42 | brew install glfw3 43 | pip install Pillow PyOpenGL 44 | 45 | ### Examples 46 | 47 | Clone the repository and run main.py to see these and several other examples. 48 | 49 | #### 3D Pipes: [pipes.py](https://github.com/fogleman/pg/blob/master/examples/pipes.py) 50 | 51 | ![Screenshot](http://i.imgur.com/za11AqP.png) 52 | 53 | #### Gusev Crater: [gusev.py](https://github.com/fogleman/pg/blob/master/examples/gusev.py) 54 | 55 | ![Screenshot](http://i.imgur.com/fiIJKIt.png) 56 | 57 | #### OBJ and STL Models: [suzanne.py](https://github.com/fogleman/pg/blob/master/examples/suzanne.py) 58 | 59 | ![Screenshot](http://i.imgur.com/Jictnlz.png) 60 | 61 | #### Constructive Solid Geometry (CSG): [csg.py](https://github.com/fogleman/pg/blob/master/examples/csg.py) 62 | 63 | ![Screenshot](http://i.imgur.com/3QJFHw1.png) 64 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pg.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pg.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pg" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pg" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pg documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Aug 6 19:55:58 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # Mock modules for readthedocs.org support. 19 | MOCK_MODULES = [ 20 | ] 21 | 22 | class Mock(object): 23 | def __init__(self, *args, **kwargs): 24 | pass 25 | def __call__(self, *args, **kwargs): 26 | return Mock() 27 | @classmethod 28 | def __int__(cls): 29 | return 1 30 | @classmethod 31 | def __contains__(cls, x): 32 | return False 33 | @classmethod 34 | def __len__(cls): 35 | return 1 36 | @classmethod 37 | def __iter__(cls): 38 | return iter([]) 39 | @classmethod 40 | def __exit__(cls): 41 | return False 42 | @classmethod 43 | def __complex__(cls): 44 | return 1j 45 | @classmethod 46 | def __float__(cls): 47 | return 1.0 48 | @classmethod 49 | def __bool__(cls): 50 | return True 51 | @classmethod 52 | def __index__(cls, x): 53 | return 1 54 | @classmethod 55 | def __getattr__(cls, x): 56 | return Mock() 57 | @classmethod 58 | def __getitem__(cls, x): 59 | return Mock() 60 | 61 | for mod_name in MOCK_MODULES: 62 | sys.modules[mod_name] = Mock() 63 | 64 | # If extensions (or modules to document with autodoc) are in another directory, 65 | # add these directories to sys.path here. If the directory is relative to the 66 | # documentation root, use os.path.abspath to make it absolute, like shown here. 67 | sys.path.insert(0, os.path.abspath('..')) 68 | 69 | # -- General configuration ------------------------------------------------ 70 | 71 | # If your documentation needs a minimal Sphinx version, state it here. 72 | #needs_sphinx = '1.0' 73 | 74 | # Add any Sphinx extension module names here, as strings. They can be 75 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 76 | # ones. 77 | extensions = [ 78 | 'sphinx.ext.autodoc', 79 | ] 80 | 81 | # Add any paths that contain templates here, relative to this directory. 82 | templates_path = ['_templates'] 83 | 84 | # The suffix of source filenames. 85 | source_suffix = '.rst' 86 | 87 | # The encoding of source files. 88 | #source_encoding = 'utf-8-sig' 89 | 90 | # The master toctree document. 91 | master_doc = 'index' 92 | 93 | # General information about the project. 94 | project = u'pg' 95 | copyright = u'2014, Michael Fogleman' 96 | 97 | # The version info for the project you're documenting, acts as replacement for 98 | # |version| and |release|, also used in various other places throughout the 99 | # built documents. 100 | # 101 | # The short X.Y version. 102 | version = '0.1' 103 | # The full version, including alpha/beta/rc tags. 104 | release = '0.1' 105 | 106 | # The language for content autogenerated by Sphinx. Refer to documentation 107 | # for a list of supported languages. 108 | #language = None 109 | 110 | # There are two options for replacing |today|: either, you set today to some 111 | # non-false value, then it is used: 112 | #today = '' 113 | # Else, today_fmt is used as the format for a strftime call. 114 | #today_fmt = '%B %d, %Y' 115 | 116 | # List of patterns, relative to source directory, that match files and 117 | # directories to ignore when looking for source files. 118 | exclude_patterns = ['_build'] 119 | 120 | # The reST default role (used for this markup: `text`) to use for all 121 | # documents. 122 | #default_role = None 123 | 124 | # If true, '()' will be appended to :func: etc. cross-reference text. 125 | #add_function_parentheses = True 126 | 127 | # If true, the current module name will be prepended to all description 128 | # unit titles (such as .. function::). 129 | #add_module_names = True 130 | 131 | # If true, sectionauthor and moduleauthor directives will be shown in the 132 | # output. They are ignored by default. 133 | #show_authors = False 134 | 135 | # The name of the Pygments (syntax highlighting) style to use. 136 | pygments_style = 'sphinx' 137 | 138 | # A list of ignored prefixes for module index sorting. 139 | #modindex_common_prefix = [] 140 | 141 | # If true, keep warnings as "system message" paragraphs in the built documents. 142 | #keep_warnings = False 143 | 144 | 145 | # -- Options for HTML output ---------------------------------------------- 146 | 147 | # The theme to use for HTML and HTML Help pages. See the documentation for 148 | # a list of builtin themes. 149 | html_theme = 'default' 150 | 151 | # Theme options are theme-specific and customize the look and feel of a theme 152 | # further. For a list of options available for each theme, see the 153 | # documentation. 154 | #html_theme_options = {} 155 | 156 | # Add any paths that contain custom themes here, relative to this directory. 157 | #html_theme_path = [] 158 | 159 | # The name for this set of Sphinx documents. If None, it defaults to 160 | # " v documentation". 161 | #html_title = None 162 | 163 | # A shorter title for the navigation bar. Default is the same as html_title. 164 | #html_short_title = None 165 | 166 | # The name of an image file (relative to this directory) to place at the top 167 | # of the sidebar. 168 | #html_logo = None 169 | 170 | # The name of an image file (within the static path) to use as favicon of the 171 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 172 | # pixels large. 173 | #html_favicon = None 174 | 175 | # Add any paths that contain custom static files (such as style sheets) here, 176 | # relative to this directory. They are copied after the builtin static files, 177 | # so a file named "default.css" will overwrite the builtin "default.css". 178 | html_static_path = ['_static'] 179 | 180 | # Add any extra paths that contain custom files (such as robots.txt or 181 | # .htaccess) here, relative to this directory. These files are copied 182 | # directly to the root of the documentation. 183 | #html_extra_path = [] 184 | 185 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 186 | # using the given strftime format. 187 | #html_last_updated_fmt = '%b %d, %Y' 188 | 189 | # If true, SmartyPants will be used to convert quotes and dashes to 190 | # typographically correct entities. 191 | #html_use_smartypants = True 192 | 193 | # Custom sidebar templates, maps document names to template names. 194 | #html_sidebars = {} 195 | 196 | # Additional templates that should be rendered to pages, maps page names to 197 | # template names. 198 | #html_additional_pages = {} 199 | 200 | # If false, no module index is generated. 201 | #html_domain_indices = True 202 | 203 | # If false, no index is generated. 204 | #html_use_index = True 205 | 206 | # If true, the index is split into individual pages for each letter. 207 | #html_split_index = False 208 | 209 | # If true, links to the reST sources are added to the pages. 210 | #html_show_sourcelink = True 211 | 212 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 213 | #html_show_sphinx = True 214 | 215 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 216 | #html_show_copyright = True 217 | 218 | # If true, an OpenSearch description file will be output, and all pages will 219 | # contain a tag referring to it. The value of this option must be the 220 | # base URL from which the finished HTML is served. 221 | #html_use_opensearch = '' 222 | 223 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 224 | #html_file_suffix = None 225 | 226 | # Output file base name for HTML help builder. 227 | htmlhelp_basename = 'pgdoc' 228 | 229 | 230 | # -- Options for LaTeX output --------------------------------------------- 231 | 232 | latex_elements = { 233 | # The paper size ('letterpaper' or 'a4paper'). 234 | #'papersize': 'letterpaper', 235 | 236 | # The font size ('10pt', '11pt' or '12pt'). 237 | #'pointsize': '10pt', 238 | 239 | # Additional stuff for the LaTeX preamble. 240 | #'preamble': '', 241 | } 242 | 243 | # Grouping the document tree into LaTeX files. List of tuples 244 | # (source start file, target name, title, 245 | # author, documentclass [howto, manual, or own class]). 246 | latex_documents = [ 247 | ('index', 'pg.tex', u'pg Documentation', 248 | u'Michael Fogleman', 'manual'), 249 | ] 250 | 251 | # The name of an image file (relative to this directory) to place at the top of 252 | # the title page. 253 | #latex_logo = None 254 | 255 | # For "manual" documents, if this is true, then toplevel headings are parts, 256 | # not chapters. 257 | #latex_use_parts = False 258 | 259 | # If true, show page references after internal links. 260 | #latex_show_pagerefs = False 261 | 262 | # If true, show URL addresses after external links. 263 | #latex_show_urls = False 264 | 265 | # Documents to append as an appendix to all manuals. 266 | #latex_appendices = [] 267 | 268 | # If false, no module index is generated. 269 | #latex_domain_indices = True 270 | 271 | 272 | # -- Options for manual page output --------------------------------------- 273 | 274 | # One entry per manual page. List of tuples 275 | # (source start file, name, description, authors, manual section). 276 | man_pages = [ 277 | ('index', 'pg', u'pg Documentation', 278 | [u'Michael Fogleman'], 1) 279 | ] 280 | 281 | # If true, show URL addresses after external links. 282 | #man_show_urls = False 283 | 284 | 285 | # -- Options for Texinfo output ------------------------------------------- 286 | 287 | # Grouping the document tree into Texinfo files. List of tuples 288 | # (source start file, target name, title, author, 289 | # dir menu entry, description, category) 290 | texinfo_documents = [ 291 | ('index', 'pg', u'pg Documentation', 292 | u'Michael Fogleman', 'pg', 'One line description of project.', 293 | 'Miscellaneous'), 294 | ] 295 | 296 | # Documents to append as an appendix to all manuals. 297 | #texinfo_appendices = [] 298 | 299 | # If false, no module index is generated. 300 | #texinfo_domain_indices = True 301 | 302 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 303 | #texinfo_show_urls = 'footnote' 304 | 305 | # If true, do not generate a @detailmenu in the "Top" node's menu. 306 | #texinfo_no_detailmenu = False 307 | -------------------------------------------------------------------------------- /docs/images/tutorial1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/docs/images/tutorial1.png -------------------------------------------------------------------------------- /docs/images/tutorial2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/docs/images/tutorial2.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Python Graphics (pg) Documentation 2 | ================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | tutorial 8 | programs 9 | 10 | Indices and tables 11 | ================== 12 | 13 | * :ref:`genindex` 14 | * :ref:`modindex` 15 | * :ref:`search` 16 | -------------------------------------------------------------------------------- /docs/programs.rst: -------------------------------------------------------------------------------- 1 | Programs 2 | ======== 3 | 4 | This portion of the documentation details the built-in shader programs. 5 | 6 | .. automodule:: pg.programs 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | In this tutorial we will explore various aspects of the ``pg`` framework from 5 | opening a window to flying through a scene of several geometric objects. 6 | 7 | Creating a Window 8 | ----------------- 9 | 10 | The first step in any ``pg`` program is to create an ``App`` object. This 11 | object owns the main window loop. Then you can instantiate one or more 12 | ``Window`` objects:: 13 | 14 | import pg 15 | 16 | app = pg.App() 17 | pg.Window() 18 | app.run() 19 | 20 | This will create your first blank window. However, typically we will want 21 | to extend the ``Window`` class to add our own functionality. We should also 22 | only construct and run the ``App`` inside of a ``__main__`` block:: 23 | 24 | import pg 25 | 26 | class Window(pg.Window): 27 | pass 28 | 29 | if __name__ == "__main__": 30 | app = pg.App() 31 | Window() 32 | app.run() 33 | 34 | This ``__main__`` block is so common that ``pg`` includes a shortcut:: 35 | 36 | if __name__ == "__main__": 37 | pg.run(Window) 38 | 39 | Note that the ``run`` function takes a ``Window`` class, not an instance. 40 | 41 | Window Lifecycle 42 | ---------------- 43 | 44 | Typically we will override several methods in the ``Window`` class. Together, 45 | these methods manage the lifecycle of the ``Window``. 46 | 47 | * ``setup()``: code to be run once when the window is created 48 | * ``update(t, dt)``: called each frame with elapsed time and time since last frame 49 | * ``draw()``: called each frame for rendering the scene 50 | * ``teardown()``: cleanup to be performed when the window is closed 51 | 52 | Here is a basic code template for starting a new ``pg`` project:: 53 | 54 | import pg 55 | 56 | class Window(pg.Window): 57 | def setup(self): 58 | pass 59 | def update(self, t, dt): 60 | pass 61 | def draw(self): 62 | pass 63 | def teardown(self): 64 | pass 65 | 66 | if __name__ == "__main__": 67 | pg.run(Window) 68 | 69 | GLSL Shaders and Programs 70 | ------------------------- 71 | 72 | Modern OpenGL uses shaders and programs in place of the deprecated, 73 | fixed-function pipeline. ``pg`` provides classes to easily work with shaders 74 | and even includes some built-in shaders with basic functionality. 75 | 76 | In our ``Window.setup`` function we should load and compile a shader program 77 | that will render our geometric primitives. One of the built-in shaders is 78 | ``SolidColorProgram`` which renders all primitives with the specified color:: 79 | 80 | self.program = pg.SolidColorProgram() 81 | 82 | To hold the configuration for our program, we must use a ``Context``. Multiple 83 | contexts can be created for a single program:: 84 | 85 | self.context = pg.Context(self.program) 86 | 87 | Now, we can set attributes on the context corresponding to the attributes and 88 | uniforms defined in the vertex and fragment shaders. The ``SolidColorProgram`` 89 | defines a ``color`` uniform. Let's set it to white:: 90 | 91 | self.context.color = (1, 1, 1) 92 | 93 | If you wanted to use your own vertex and fragment shaders, you would simply 94 | do the following instead:: 95 | 96 | self.program = pg.Program(vs, fs) 97 | 98 | ``vs`` and ``fs`` can be the shader source code or a filename or instances of 99 | ``VertexShader`` and ``FragmentShader``, respectively. 100 | 101 | Our ``setup`` function is not yet complete but looks like this:: 102 | 103 | def setup(self): 104 | self.program = pg.SolidColorProgram() 105 | self.context = pg.Context(self.program) 106 | self.context.color = (1, 1, 1) 107 | 108 | Built-in Geometric Shapes 109 | ------------------------- 110 | 111 | ``pg`` includes functions for generating several 3-dimensional primitives 112 | including spheres, cuboids, cylinders, cones, planes, axes, etc. 113 | 114 | Let's create a sphere:: 115 | 116 | sphere = pg.Sphere(3, 0.5, (0, 0, 0)) 117 | 118 | The first argument, `detail`, indicates how detailed to make the sphere. It 119 | is the number of times to recursively split the triangles. The second argument 120 | specifies the `radius` and the third argument specifies the `center` of the 121 | sphere. 122 | 123 | Vertex Buffers 124 | -------------- 125 | 126 | The sphere object has lists specifying its vertex positions, normals and 127 | texture coordinates. For the ``SolidColorProgram``, we only need the positions. 128 | 129 | Now it's time to load this data into a vertex buffer so our graphics card can 130 | access it:: 131 | 132 | self.context.position = pg.VertexBuffer(sphere.positions) 133 | 134 | Transformation Matrices 135 | ----------------------- 136 | 137 | Dealing with matrices is a big part of using OpenGL. ``pg`` includes a 138 | ``Matrix`` class that will help us with most scenarios. 139 | 140 | For our code, we'll set the camera position with a translation and we'll 141 | use a perspective projection:: 142 | 143 | matrix = pg.Matrix() 144 | matrix = matrix.translate((0, 0, -2)) 145 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 146 | self.context.matrix = matrix 147 | 148 | Now our setup function is complete:: 149 | 150 | def setup(self): 151 | self.program = pg.SolidColorProgram() 152 | self.context = pg.Context(self.program) 153 | self.context.color = (1, 1, 1) 154 | sphere = pg.Sphere(3, 0.5, (0, 0, 0)) 155 | self.context.position = pg.VertexBuffer(sphere.positions) 156 | matrix = pg.Matrix() 157 | matrix = matrix.translate((0, 0, -2)) 158 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 159 | self.context.matrix = matrix 160 | 161 | Rendering 162 | --------- 163 | 164 | Finally, we can render the scene as shown below:: 165 | 166 | def draw(self): 167 | self.clear() 168 | self.context.draw(pg.GL_TRIANGLES) 169 | 170 | Because we're using a single color without shading, our sphere just looks like 171 | a circle right now. 172 | 173 | .. image:: images/tutorial1.png 174 | 175 | We can instead use the ``DirectionalLightProgram`` which renders the scene 176 | with a single, directional light source. This program has several uniforms 177 | that can be configured but most of them have sensible defaults. At a minimum 178 | we should set the ``camera_position`` so that the lighting will look correct:: 179 | 180 | self.context.camera_position = (0, 0, 2) 181 | 182 | We also now need to provide the sphere normal vectors to the program:: 183 | 184 | self.context.normal = pg.VertexBuffer(sphere.normals) 185 | 186 | Here is the updated code:: 187 | 188 | class Window(pg.Window): 189 | def setup(self): 190 | self.program = pg.DirectionalLightProgram() 191 | self.context = pg.Context(self.program) 192 | sphere = pg.Sphere(3, 0.5, (0, 0, 0)) 193 | self.context.position = pg.VertexBuffer(sphere.positions) 194 | self.context.normal = pg.VertexBuffer(sphere.normals) 195 | matrix = pg.Matrix() 196 | matrix = matrix.translate((0, 0, -2)) 197 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 198 | self.context.matrix = matrix 199 | self.context.camera_position = (0, 0, 2) 200 | def draw(self): 201 | self.clear() 202 | self.context.draw(pg.GL_TRIANGLES) 203 | 204 | And here is what it looks like. 205 | 206 | .. image:: images/tutorial2.png 207 | 208 | Flying Around with WASD 209 | ----------------------- 210 | 211 | ``pg`` includes a ``WASD`` class that makes it incredibly easy to fly around 212 | your scene. The ``WASD`` object hooks into your window's keyboard and mouse 213 | callbacks and provides you with a matrix with the translation and rotation 214 | for the camera position. 215 | 216 | First, let's construct the ``WASD`` object in our ``setup`` function:: 217 | 218 | self.wasd = pg.WASD(self) 219 | 220 | The initial camera position and viewing target can be set with 221 | ``WASD.look_at``:: 222 | 223 | self.wasd.look_at((0, 0, 2), (0, 0, 0)) 224 | 225 | Now we need to update our context's matrix each frame. The matrix code is 226 | removed from the ``setup`` function and goes in the ``update`` function 227 | with a few changes:: 228 | 229 | def update(self, t, dt): 230 | matrix = self.wasd.get_matrix() 231 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 232 | self.context.matrix = matrix 233 | self.context.camera_position = self.wasd.position 234 | 235 | Complete Example 236 | ---------------- 237 | 238 | :: 239 | 240 | import pg 241 | 242 | class Window(pg.Window): 243 | def setup(self): 244 | self.wasd = pg.WASD(self) 245 | self.wasd.look_at((0, 0, 2), (0, 0, 0)) 246 | self.program = pg.DirectionalLightProgram() 247 | self.context = pg.Context(self.program) 248 | sphere = pg.Sphere(3, 0.5, (0, 0, 0)) 249 | self.context.position = pg.VertexBuffer(sphere.positions) 250 | self.context.normal = pg.VertexBuffer(sphere.normals) 251 | def update(self, t, dt): 252 | matrix = self.wasd.get_matrix() 253 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 254 | self.context.matrix = matrix 255 | self.context.camera_position = self.wasd.position 256 | def draw(self): 257 | self.clear() 258 | self.context.draw(pg.GL_TRIANGLES) 259 | 260 | if __name__ == "__main__": 261 | pg.run(Window) 262 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/__init__.py -------------------------------------------------------------------------------- /examples/avoid.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi, atan2, hypot 2 | import pg 3 | import random 4 | 5 | SPEED = 50 6 | 7 | class Bot(object): 8 | def __init__(self, position, target): 9 | self.y = (random.random() - 0.5) * 20 10 | self.position = position 11 | self.target = target 12 | self.speed = random.random() + 0.5 13 | self.padding = random.random() * 8 + 16 14 | self.angle = 0 15 | def get_position(self, offset): 16 | px, py = self.position 17 | # tx, ty = self.target 18 | angle = self.angle# or atan2(ty - py, tx - px) 19 | return (px + cos(angle) * offset, py + sin(angle) * offset) 20 | def update(self, bots): 21 | px, py = self.position 22 | tx, ty = self.target 23 | angle = atan2(ty - py, tx - px) 24 | dx = cos(angle) 25 | dy = sin(angle) 26 | for bot in bots: 27 | if bot == self: 28 | continue 29 | x, y = bot.position 30 | d = hypot(px - x, py - y) ** 2 31 | p = bot.padding ** 2 32 | angle = atan2(py - y, px - x) 33 | dx += cos(angle) / d * p 34 | dy += sin(angle) / d * p 35 | angle = atan2(dy, dx) 36 | magnitude = hypot(dx, dy) 37 | self.angle = angle 38 | return angle, magnitude 39 | def set_position(self, position): 40 | self.position = position 41 | 42 | class Model(object): 43 | def __init__(self, width, height, count): 44 | self.width = width 45 | self.height = height 46 | self.bots = self.create_bots(count) 47 | def create_bots(self, count): 48 | result = [] 49 | for i in range(count): 50 | position = self.select_point() 51 | target = self.select_point() 52 | bot = Bot(position, target) 53 | result.append(bot) 54 | return result 55 | def select_point(self): 56 | cx = self.width / 2.0 57 | cy = self.height / 2.0 58 | radius = min(self.width, self.height) * 0.4 59 | angle = random.random() * 2 * pi 60 | x = cx + cos(angle) * radius 61 | y = cy + sin(angle) * radius 62 | return (x, y) 63 | def update(self, dt): 64 | data = [bot.update(self.bots) for bot in self.bots] 65 | for bot, (angle, magnitude) in zip(self.bots, data): 66 | speed = min(1, 0.2 + magnitude * 0.8) 67 | dx = cos(angle) * dt * SPEED * bot.speed * speed 68 | dy = sin(angle) * dt * SPEED * bot.speed * speed 69 | px, py = bot.position 70 | tx, ty = bot.target 71 | bot.set_position((px + dx, py + dy)) 72 | if hypot(px - tx, py - ty) < 10: 73 | bot.target = self.select_point() 74 | 75 | class Window(pg.Window): 76 | def setup(self): 77 | self.wasd = pg.WASD(self, speed=100) 78 | self.wasd.look_at((300, 15, 300), (0, -50, 0)) 79 | self.context = pg.Context(pg.DirectionalLightProgram()) 80 | self.target = pg.Sphere(3, 2) 81 | a = pg.Solid(pg.Sphere(3, 4)) 82 | b = pg.Solid(pg.Sphere(2, 2, (3, 0, 0))) 83 | c = pg.Solid(pg.Cylinder((-6, 0, 0), (0, 0, 0), 2, 16)) 84 | d = pg.Solid(pg.Sphere(2, 2, (-6, 0, 0))) 85 | solid = (a - b) | c | d 86 | self.mesh = solid.mesh() 87 | self.model = Model(400, 400, 100) 88 | def set_matrix(self, x, y, z, a): 89 | matrix = pg.Matrix().rotate((0, 1, 0), a).translate((x, y, z)) 90 | inverse = pg.Matrix().rotate((0, 1, 0), -a) 91 | self.context.light_direction = inverse * pg.normalize((1, 1, 1)) 92 | self.context.camera_position = matrix.inverse() * self.wasd.position 93 | matrix = self.wasd.get_matrix(matrix) 94 | matrix = matrix.perspective(65, self.aspect, 0.1, 1000) 95 | self.context.matrix = matrix 96 | # mesh.draw(self.context) 97 | def update(self, t, dt): 98 | self.clear() 99 | self.model.update(dt) 100 | bot = self.model.bots[0] 101 | # setup camera 102 | x1, z1 = bot.get_position(0) 103 | x2, z2 = bot.get_position(50) 104 | # self.wasd.look_at((x1, bot.y, z1), (x2, bot.y, z2)) 105 | # self.wasd.look_at(self.wasd.position, (x1, bot.y, z1)) 106 | # draw target 107 | self.context.object_color = (1, 0, 0) 108 | x, z = bot.target 109 | self.set_matrix(x, bot.y, z, 0) 110 | self.target.draw(self.context) 111 | # draw bots 112 | self.context.object_color = (0.4, 0.6, 0.8) 113 | for bot in self.model.bots: 114 | x, z = bot.position 115 | self.set_matrix(x, bot.y, z, bot.angle) 116 | self.mesh.draw(self.context) 117 | 118 | if __name__ == "__main__": 119 | pg.run(Window) 120 | -------------------------------------------------------------------------------- /examples/bronze.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/bronze.jpg -------------------------------------------------------------------------------- /examples/bus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/bus.jpg -------------------------------------------------------------------------------- /examples/bus.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self, speed=4) 6 | self.wasd.look_at((-3, 1, -6), (0, 0, -2)) 7 | self.context = pg.Context(pg.DirectionalLightProgram()) 8 | self.context.ambient_color = (0.7, 0.7, 0.7) 9 | self.context.light_color = (0.3, 0.3, 0.3) 10 | self.context.use_texture = True 11 | self.context.sampler = pg.Texture(0, 'examples/bus.jpg') 12 | self.mesh = pg.OBJ('examples/bus.obj').center().smooth_normals() 13 | def update(self, t, dt): 14 | matrix = pg.Matrix() 15 | matrix = self.wasd.get_matrix(matrix) 16 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 17 | self.context.matrix = matrix 18 | self.context.camera_position = self.wasd.position 19 | def draw(self): 20 | self.clear() 21 | self.mesh.draw(self.context) 22 | 23 | if __name__ == "__main__": 24 | pg.run(Window) 25 | -------------------------------------------------------------------------------- /examples/circles.py: -------------------------------------------------------------------------------- 1 | from math import pi, sin 2 | import pg 3 | 4 | SCALE = 256 5 | MIN_CIRCLE_SIZE = SCALE / 2 6 | MAX_CIRCLE_SIZE = SCALE * 2 7 | CIRCLE_SPACING = SCALE * 1 8 | GRID_SIZE = 7 9 | SPEED = 1.0 / 10 10 | OFFSET_MULTIPLIER = 2.0 11 | CIRCLE_COUNT = GRID_SIZE * GRID_SIZE 12 | 13 | class Window(pg.Window): 14 | def setup(self): 15 | plane = pg.Plane((0, 0, 0), (0, 0, 1), 1) 16 | self.context = pg.Context(Program()) 17 | self.context.position = pg.VertexBuffer(plane.positions) 18 | self.context.matrix = pg.Matrix().orthographic(-1, 1, -1, 1, -1, 1) 19 | self.positions = [] 20 | n = GRID_SIZE 21 | m = CIRCLE_SPACING 22 | for i in range(n): 23 | for j in range(n): 24 | x = (i - (n - 1) / 2.0) * m 25 | y = (j - (n - 1) / 2.0) * m 26 | d = (x * x + y * y) ** 0.5 27 | self.positions.append((x, y, d)) 28 | self.max_distance = max(x[-1] for x in self.positions) 29 | def circles(self): 30 | result = [] 31 | for x, y, d in self.positions: 32 | t = d / self.max_distance * pi * OFFSET_MULTIPLIER 33 | p = (sin(t - self.t * 2 * pi * SPEED) + 1) / 2 34 | r = MIN_CIRCLE_SIZE + p * (MAX_CIRCLE_SIZE - MIN_CIRCLE_SIZE) 35 | result.append((x, y, r)) 36 | return result 37 | def draw(self): 38 | self.clear() 39 | self.context.w, self.context.h = self.framebuffer_size 40 | self.context.circles = self.circles() 41 | self.context.draw() 42 | 43 | class Program(pg.BaseProgram): 44 | VS = ''' 45 | #version 120 46 | 47 | uniform mat4 matrix; 48 | attribute vec4 position; 49 | 50 | void main() { 51 | gl_Position = matrix * position; 52 | } 53 | ''' 54 | FS = ''' 55 | #version 120 56 | 57 | uniform float w; 58 | uniform float h; 59 | uniform vec3 circles[%d]; 60 | 61 | const vec4 palette[10] = vec4[10]( 62 | vec4(0.122, 0.467, 0.706, 1.0), 63 | vec4(1.000, 0.498, 0.055, 1.0), 64 | vec4(0.173, 0.627, 0.173, 1.0), 65 | vec4(0.839, 0.153, 0.157, 1.0), 66 | vec4(0.580, 0.404, 0.741, 1.0), 67 | vec4(0.549, 0.337, 0.294, 1.0), 68 | vec4(0.890, 0.467, 0.761, 1.0), 69 | vec4(0.498, 0.498, 0.498, 1.0), 70 | vec4(0.737, 0.741, 0.133, 1.0), 71 | vec4(0.090, 0.745, 0.812, 1.0) 72 | ); 73 | 74 | const vec4 background = vec4(0, 0, 0, 1); 75 | 76 | void main() { 77 | int count = 0; 78 | float e = 1e9; 79 | vec2 p = gl_FragCoord.xy - vec2(w / 2.0, h / 2.0); 80 | for (int i = 0; i < %d; i++) { 81 | vec2 c = circles[i].xy; 82 | float r = circles[i].z; 83 | float d = distance(p, c); 84 | e = min(e, abs(d - r)); 85 | if (d <= r) { 86 | count++; 87 | } 88 | } 89 | if (mod(count, 2) == 0) { 90 | gl_FragColor = background; 91 | } 92 | else { 93 | vec4 color = palette[int(mod(count / 2, palette.length()))]; 94 | float pct = clamp(pow(e / 2, 2), 0, 1); 95 | gl_FragColor = mix(background, color, pct); 96 | } 97 | } 98 | ''' % (CIRCLE_COUNT, CIRCLE_COUNT) 99 | 100 | def main(): 101 | app = pg.App() 102 | Window((1000, 1000)) 103 | app.run() 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /examples/context.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self) 6 | self.wasd.look_at((0, 0, 4), (0, 0, 0)) 7 | self.program = pg.DirectionalLightProgram() 8 | self.context1 = pg.Context(self.program) 9 | self.context2 = pg.Context(self.program) 10 | self.sphere1 = pg.Sphere(4, 0.5, (2, 0, 0)) 11 | self.sphere2 = pg.Sphere(4, 0.5, (-2, 0, 0)) 12 | def update(self, t, dt): 13 | matrix = pg.Matrix() 14 | matrix = self.wasd.get_matrix(matrix) 15 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 16 | self.context1.matrix = matrix 17 | self.context1.camera_position = self.wasd.position 18 | self.context1.object_color = (1.0, 0.2, 0.0) 19 | self.context2.matrix = matrix 20 | self.context2.camera_position = self.wasd.position 21 | def draw(self): 22 | self.clear() 23 | self.sphere1.draw(self.context1) 24 | self.sphere2.draw(self.context2) 25 | 26 | if __name__ == "__main__": 27 | pg.run(Window) 28 | -------------------------------------------------------------------------------- /examples/csg.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self, speed=5) 6 | self.wasd.look_at((-2, 2, 2), (0, 0, 0)) 7 | self.context = pg.Context(pg.DirectionalLightProgram()) 8 | self.context.sampler = pg.Texture(0, 'examples/bronze.jpg') 9 | self.context.use_texture = True 10 | a = pg.Solid(pg.Cuboid(-1, 1, -1, 1, -1, 1)) 11 | b = pg.Solid(pg.Sphere(2, 1.35)) 12 | c = pg.Solid(pg.Cylinder((-1, 0, 0), (1, 0, 0), 0.5, 18)) 13 | d = pg.Solid(pg.Cylinder((0, -1, 0), (0, 1, 0), 0.5, 18)) 14 | e = pg.Solid(pg.Cylinder((0, 0, -1), (0, 0, 1), 0.5, 18)) 15 | solid = (a & b) - (c | d | e) 16 | self.mesh = solid.mesh() 17 | def update(self, t, dt): 18 | matrix = pg.Matrix() 19 | matrix = self.wasd.get_matrix(matrix) 20 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 21 | self.context.matrix = matrix 22 | self.context.camera_position = self.wasd.position 23 | def draw(self): 24 | self.clear() 25 | self.mesh.draw(self.context) 26 | 27 | if __name__ == "__main__": 28 | pg.run(Window) 29 | -------------------------------------------------------------------------------- /examples/cuboids.py: -------------------------------------------------------------------------------- 1 | from OpenGL.GL import * 2 | import pg 3 | import random 4 | 5 | class Bullet(object): 6 | def __init__(self, t, position, vector): 7 | self.time = t 8 | self.position = position 9 | self.vector = vector 10 | 11 | class Window(pg.Window): 12 | def setup(self): 13 | self.wasd = pg.WASD(self, speed=8) 14 | self.wasd.look_at((-10, 0, 0), (0, 0, 0)) 15 | # cuboids 16 | self.context = pg.Context(pg.DirectionalLightProgram()) 17 | self.context.use_color = True 18 | self.context.ambient_color = (0.5, 0.5, 0.5) 19 | self.context.light_color = (0.5, 0.5, 0.5) 20 | self.context.light_direction = pg.normalize((-1, 1, 1)) 21 | data = [] 22 | n = 16 23 | for x in range(256): 24 | z = random.randint(-n, n) 25 | y = random.randint(-n, n) 26 | cuboid = pg.Cuboid(x, x + 1, y - 0.5, y + 0.5, z - 0.5, z + 0.5) 27 | color = pg.hex_color(random.randint(0, 0xffffff)) 28 | colors = [color] * len(cuboid.positions) 29 | data.extend(pg.interleave( 30 | cuboid.positions, cuboid.normals, colors)) 31 | self.context.position, self.context.normal, self.context.color = ( 32 | pg.VertexBuffer(data).slices(3, 3, 3)) 33 | # bullets 34 | self.bullet = pg.Context(pg.DirectionalLightProgram()) 35 | self.bullet.ambient_color = (0.5, 0.5, 0.5) 36 | self.bullet.light_color = (0.5, 0.5, 0.5) 37 | sphere = pg.Sphere(3, 0.05, (0, 0, 0)) 38 | self.bullet.position = pg.VertexBuffer(sphere.positions) 39 | self.bullet.normal = pg.VertexBuffer(sphere.normals) 40 | self.bullets = [] 41 | # crosshairs 42 | self.crosshairs = pg.Context(pg.SolidColorProgram()) 43 | self.crosshairs.position = pg.VertexBuffer(pg.Crosshairs().positions) 44 | def draw(self): 45 | self.wasd.x = -10 46 | self.clear() 47 | # cuboids 48 | self.context.camera_position = self.wasd.position 49 | model_matrix = pg.Matrix().translate((self.t * -4, 0, 0)) 50 | matrix = self.wasd.get_matrix() 51 | matrix = matrix.perspective(65, self.aspect, 0.1, 500) 52 | self.context.matrix = matrix * model_matrix 53 | self.context.model_matrix = model_matrix 54 | self.context.draw() 55 | # bullets 56 | self.bullet.camera_position = self.wasd.position 57 | for bullet in list(self.bullets): 58 | dt = self.time - bullet.time 59 | x, y, z = pg.add(bullet.position, pg.mul(bullet.vector, dt * 16)) 60 | model_matrix = pg.Matrix().translate((x, y, z)) 61 | self.bullet.model_matrix = model_matrix 62 | self.bullet.matrix = matrix * model_matrix 63 | self.bullet.draw() 64 | if dt > 10: 65 | self.bullets.remove(bullet) 66 | # crosshairs 67 | width, height = self.size 68 | matrix = pg.Matrix().translate((width / 2, height / 2, 0)) 69 | matrix = matrix.orthographic(0, width, 0, height, -1, 1) 70 | self.crosshairs.matrix = matrix 71 | glEnable(GL_COLOR_LOGIC_OP) 72 | glLogicOp(GL_INVERT) 73 | glLineWidth(3) 74 | self.crosshairs.draw(pg.gl.GL_LINES) 75 | glDisable(GL_COLOR_LOGIC_OP) 76 | def on_mouse_button(self, button, action, mods): 77 | if button == 0 and action == 1: 78 | bullet = Bullet( 79 | self.time, self.wasd.position, self.wasd.get_sight_vector()) 80 | self.bullets.append(bullet) 81 | 82 | if __name__ == "__main__": 83 | pg.run(Window) 84 | -------------------------------------------------------------------------------- /examples/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/earth.png -------------------------------------------------------------------------------- /examples/field.py: -------------------------------------------------------------------------------- 1 | from math import pi, sin, cos 2 | import pg 3 | import random 4 | 5 | SCALE = 512 6 | PARTICLE_COUNT = 13 7 | 8 | def points(sides, radius, rotation): 9 | rotation = rotation - pi / 2 10 | angle = 2 * pi / sides 11 | angles = [angle * i + rotation for i in range(sides)] 12 | return [(cos(a) * radius, sin(a) * radius, 1.0) for a in angles] 13 | 14 | class Window(pg.Window): 15 | def setup(self): 16 | plane = pg.Plane((0, 0, 0), (0, 0, 1), 1) 17 | self.context = pg.Context(Program()) 18 | self.context.position = pg.VertexBuffer(plane.positions) 19 | self.context.matrix = pg.Matrix().orthographic(-1, 1, -1, 1, -1, 1) 20 | def update(self, t, dt): 21 | t0 = t / 24 22 | t1 = t0 23 | t2 = -t0 * pi 24 | r1 = SCALE 25 | r2 = SCALE / 2 26 | m = sin(t0 * pi / 2) * 2 27 | particles = [(0, 0, m)] + points(6, r1, t1) + points(6, r2, t2) 28 | self.context.particles = particles 29 | self.context.t = t0 30 | def draw(self): 31 | self.clear() 32 | self.context.w, self.context.h = self.framebuffer_size 33 | self.context.draw() 34 | 35 | class Program(pg.BaseProgram): 36 | VS = ''' 37 | #version 120 38 | 39 | uniform mat4 matrix; 40 | attribute vec4 position; 41 | 42 | void main() { 43 | gl_Position = matrix * position; 44 | } 45 | ''' 46 | FS = ''' 47 | #version 120 48 | 49 | uniform float w; 50 | uniform float h; 51 | uniform float t; 52 | uniform vec3 particles[%d]; 53 | 54 | const vec4 palette[10] = vec4[10]( 55 | vec4(0.122, 0.467, 0.706, 1.0), 56 | vec4(1.000, 0.498, 0.055, 1.0), 57 | vec4(0.173, 0.627, 0.173, 1.0), 58 | vec4(0.839, 0.153, 0.157, 1.0), 59 | vec4(0.580, 0.404, 0.741, 1.0), 60 | vec4(0.549, 0.337, 0.294, 1.0), 61 | vec4(0.890, 0.467, 0.761, 1.0), 62 | vec4(0.498, 0.498, 0.498, 1.0), 63 | vec4(0.737, 0.741, 0.133, 1.0), 64 | vec4(0.090, 0.745, 0.812, 1.0) 65 | ); 66 | 67 | const vec4 background = vec4(0, 0, 0, 1); 68 | const float pi = 3.141592653589793; 69 | 70 | vec4 compute(vec2 c) { 71 | float dx = 0; 72 | float dy = 0; 73 | for (int i = 0; i < %d; i++) { 74 | vec3 p = particles[i]; 75 | float d = distance(p.xy, c); 76 | float a = atan(c.y - p.y, c.x - p.x); 77 | dx += p.z * cos(a) / d; 78 | dy += p.z * sin(a) / d; 79 | } 80 | float d = length(vec2(dx, dy)); 81 | float n = log(d) / log(2.0 + sin(t) * 0.8); 82 | float f = mod(n, 1.0); 83 | if (f < 0.5) { 84 | return background; 85 | } 86 | else { 87 | float e = (abs(f - 0.75) - 0.25) * 4; 88 | int index = int(mod(n, palette.length())); 89 | float pct = clamp(pow(e, 0.3), 0, 1); 90 | return mix(background, palette[index], pct); 91 | } 92 | } 93 | 94 | void main() { 95 | vec2 c = gl_FragCoord.xy - vec2(w / 2.0, h / 2.0); 96 | gl_FragColor = compute(c); 97 | } 98 | ''' % (PARTICLE_COUNT, PARTICLE_COUNT) 99 | 100 | def main(): 101 | app = pg.App() 102 | Window((1000, 1000)) 103 | app.run() 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /examples/font.py: -------------------------------------------------------------------------------- 1 | from OpenGL.GL import * 2 | import pg 3 | 4 | class Window(pg.Window): 5 | def setup(self): 6 | self.font = pg.Font(self, 0, '/Library/Fonts/Arial.ttf', 72) 7 | def draw(self): 8 | self.clear() 9 | w, h = self.size 10 | self.font.render('Hello, world!', (w / 2, h / 2), (0.5, 0.5)) 11 | 12 | if __name__ == "__main__": 13 | pg.run(Window) 14 | -------------------------------------------------------------------------------- /examples/gusev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/gusev.jpg -------------------------------------------------------------------------------- /examples/gusev.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import pg 3 | 4 | # download the gusev.stl file here: 5 | # http://www.michaelfogleman.com/static/gusev.stl.zip 6 | 7 | class Window(pg.Window): 8 | def setup(self): 9 | self.set_clear_color(0.87, 0.81, 0.70) 10 | self.wasd = pg.WASD(self, speed=10) 11 | self.wasd.look_at((0, 0, 0), (30, -5, 30)) 12 | self.context = pg.Context(pg.DirectionalLightProgram()) 13 | self.context.specular_multiplier = 0.25 14 | self.context.ambient_color = (0.5, 0.5, 0.5) 15 | self.context.light_color = (0.5, 0.5, 0.5) 16 | self.context.use_texture = True 17 | self.context.sampler = pg.Texture(0, 'examples/gusev.jpg') 18 | mesh = pg.STL('examples/gusev.stl').smooth_normals() 19 | (x0, y0, z0), (x1, y1, z1) = pg.bounding_box(mesh.positions) 20 | for x, y, z in mesh.positions: 21 | u = 1 - (z - z0) / (z1 - z0) 22 | v = 1 - (x - x0) / (x1 - x0) 23 | mesh.uvs.append((u, v)) 24 | self.mesh = mesh 25 | p = self.mesh.positions 26 | self.lookup = defaultdict(list) 27 | for v1, v2, v3 in zip(p[::3], p[1::3], p[2::3]): 28 | x, y, z = v1 29 | x, z = int(round(x)), int(round(z)) 30 | self.lookup[(x, z)].append((v1, v2, v3)) 31 | def adjust_height(self): 32 | o = x, y, z = self.wasd.position 33 | d = (0, -1, 0) 34 | x, z = int(round(x)), int(round(z)) 35 | for i in xrange(x - 1, x + 2): 36 | for j in xrange(z - 1, z + 2): 37 | for v1, v2, v3 in self.lookup[(i, j)]: 38 | t = pg.ray_triangle_intersection(v1, v2, v3, o, d) 39 | if t and t < 1: 40 | self.wasd.y += 1 - t 41 | return 42 | def draw(self): 43 | self.adjust_height() 44 | self.clear() 45 | matrix = self.wasd.get_matrix() 46 | matrix = matrix.perspective(65, self.aspect, 0.1, 1000) 47 | self.context.matrix = matrix 48 | self.context.camera_position = self.wasd.position 49 | self.mesh.draw(self.context) 50 | 51 | if __name__ == "__main__": 52 | pg.run(Window) 53 | -------------------------------------------------------------------------------- /examples/hirise.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from math import atan2 3 | import pg 4 | 5 | NAME = 'ESP_025735_2185' 6 | FONT = '/Library/Fonts/Arial.ttf' 7 | FLY = True 8 | STEP = 16 9 | HEIGHT = 1.8288 * 10 10 | SPEED = 1.34 * 500 11 | SHOW_INFO = False 12 | PAN = None #(-920, 2000, -7760), (895, 2000, 7530) 13 | 14 | class LoadingScene(pg.Scene): 15 | def setup(self): 16 | self.title_font = pg.Font(self, 3, FONT, 72, (0, 0, 0, 1)) 17 | self.font = pg.Font(self, 4, FONT, 36, (0, 0, 0, 1)) 18 | def update(self, t, dt): 19 | result = self.window.worker.result 20 | if result: 21 | self.window.set_scene(MainScene(self.window, result)) 22 | def draw(self): 23 | self.window.clear() 24 | w, h = self.window.size 25 | title = 'Mars HiRISE Viewer' 26 | message = self.window.worker.message 27 | triangles = self.window.worker.triangles 28 | self.title_font.render(title, (w / 2, h / 2 - 10), (0.5, 1)) 29 | self.font.render(message, (w / 2, h / 2 + 10), (0.5, 0)) 30 | self.font.render(NAME, (w / 2, 50), (0.5, 0)) 31 | self.font.render(triangles, (w / 2, h - 50), (0.5, 1)) 32 | 33 | class MainScene(pg.Scene): 34 | def __init__(self, window, data): 35 | super(MainScene, self).__init__(window) 36 | for key, value in data.items(): 37 | setattr(self, key, value) 38 | self.context.sampler.bind() 39 | self.context.normal_sampler.bind() 40 | def setup(self): 41 | self.wasd = pg.WASD(self, speed=SPEED) 42 | self.dy = 0 43 | def get_height(self): 44 | p = x, y, z = self.wasd.position 45 | x, z = int(round(x / STEP)), int(round(z / STEP)) 46 | for i in xrange(x - 1, x + 2): 47 | for j in xrange(z - 1, z + 2): 48 | for v1, v2, v3 in self.lookup[(i, j)]: 49 | t = pg.ray_triangle_intersection(v1, v2, v3, p, (0, -1, 0)) 50 | if t: 51 | return t 52 | t = pg.ray_triangle_intersection(v1, v2, v3, p, (0, 1, 0)) 53 | if t: 54 | return -t 55 | return None 56 | def on_key(self, key, scancode, action, mods): 57 | if key == pg.KEY_SPACE and action == pg.PRESS: 58 | if self.dy == 0: 59 | self.dy = 2.0 60 | def update(self, t, dt): 61 | if PAN: 62 | a, b = PAN 63 | p = abs((t * 0.01) % 2 - 1) / 1.0 64 | x, y, z = pg.interpolate(a, b, p) 65 | self.wasd.look_at((x, y, z), (x, 0, z)) 66 | self.wasd.rx = atan2(b[2] - a[2], b[0] - a[0]) 67 | return 68 | self.dy = max(self.dy - dt * 2.5, -25.0) 69 | if not FLY: 70 | self.wasd.y += self.dy 71 | h = self.get_height() 72 | if h is None: 73 | return 74 | if h < HEIGHT: 75 | self.dy = 0 76 | self.wasd.y -= h - HEIGHT 77 | def draw(self): 78 | self.window.clear() 79 | matrix = self.wasd.get_matrix() 80 | matrix = matrix.perspective(65, self.window.aspect, 1, 100000) 81 | self.context.matrix = matrix 82 | self.context.camera_position = self.wasd.position 83 | self.context.draw() 84 | if SHOW_INFO: 85 | w, h = self.window.size 86 | self.font.render('%.1f fps' % self.window.fps, (w - 5, 0), (1, 0)) 87 | text = 'x=%.2f, y=%.2f, z=%.2f' % self.wasd.position 88 | self.font.render(text, (5, 0)) 89 | 90 | class Worker(pg.Worker): 91 | def __init__(self): 92 | self.message = '' 93 | self.triangles = '' 94 | self.result = None 95 | super(Worker, self).__init__() 96 | def run(self): 97 | context = pg.Context(Program()) 98 | font = pg.Font(self, 2, FONT, 24, (0, 0, 0, 1)) 99 | self.message = 'loading triangle mesh' 100 | mesh = pg.STL('examples/%s.stl' % NAME).center() 101 | self.triangles = '%d triangles' % (len(mesh.positions) / 3) 102 | self.message = 'computing bounding box' 103 | (x0, y0, z0), (x1, y1, z1) = pg.bounding_box(mesh.positions) 104 | context.uv0 = (x0, z0) 105 | context.uv1 = (x1, z1) 106 | self.message = 'generating vertex buffer' 107 | context.position = pg.VertexBuffer(mesh.positions) 108 | self.message = 'generating height map' 109 | p = mesh.positions 110 | lookup = defaultdict(list) 111 | for v1, v2, v3 in zip(p[::3], p[1::3], p[2::3]): 112 | x, y, z = v1 113 | x, z = int(round(x / STEP)), int(round(z / STEP)) 114 | lookup[(x, z)].append((v1, v2, v3)) 115 | self.message = 'loading brightness texture' 116 | context.sampler = pg.Texture(0, 'examples/%s.jpg' % NAME) 117 | self.message = 'loading normal texture' 118 | context.normal_sampler = pg.Texture(1, 'examples/%s.png' % NAME) 119 | self.result = { 120 | 'font': font, 121 | 'context': context, 122 | 'lookup': lookup, 123 | } 124 | 125 | class Window(pg.Window): 126 | def __init__(self, worker): 127 | self.worker = worker 128 | super(Window, self).__init__(share=self.worker) 129 | self.worker.start() 130 | def setup(self): 131 | self.set_clear_color(0.74, 0.70, 0.64) 132 | self.set_scene(LoadingScene(self)) 133 | 134 | class Program(pg.Program): 135 | VS = ''' 136 | #version 120 137 | 138 | uniform mat4 matrix; 139 | uniform mat4 model_matrix; 140 | uniform vec2 uv0; 141 | uniform vec2 uv1; 142 | 143 | attribute vec4 position; 144 | 145 | varying vec3 frag_position; 146 | varying vec2 frag_uv; 147 | 148 | void main() { 149 | gl_Position = matrix * position; 150 | frag_position = vec3(model_matrix * position); 151 | float u = (position.x - uv0.x) / (uv1.x - uv0.x); 152 | float v = 1.0 - (position.z - uv0.y) / (uv1.y - uv0.y); 153 | frag_uv = vec2(u, v); 154 | } 155 | ''' 156 | FS = ''' 157 | #version 120 158 | 159 | uniform sampler2D sampler; 160 | uniform sampler2D normal_sampler; 161 | uniform vec3 camera_position; 162 | 163 | uniform mat4 normal_matrix; 164 | uniform vec3 light_direction; 165 | uniform vec3 ambient_color; 166 | uniform vec3 light_color; 167 | uniform vec3 fog_color; 168 | uniform float fog_distance; 169 | uniform float specular_power; 170 | uniform float specular_multiplier; 171 | 172 | varying vec3 frag_position; 173 | varying vec2 frag_uv; 174 | 175 | void main() { 176 | vec3 norm = vec3(texture2D(normal_sampler, frag_uv)); 177 | norm = norm * vec3(2.0) - vec3(1.0); 178 | norm = norm.yzx; 179 | vec3 color1 = vec3(0.47, 0.31, 0.24) * 0.8; 180 | vec3 color2 = vec3(0.82, 0.56, 0.39) * 1.4; 181 | float pct = vec3(texture2D(sampler, frag_uv)).r; 182 | vec3 color = mix(color1, color2, pct); 183 | float diffuse = max(dot(mat3(normal_matrix) * norm, 184 | light_direction), 0.0); 185 | float specular = 0.0; 186 | if (diffuse > 0.0) { 187 | vec3 camera_vector = normalize(camera_position - frag_position); 188 | specular = pow(max(dot(camera_vector, 189 | reflect(-light_direction, norm)), 0.0), specular_power); 190 | } 191 | vec3 light = ambient_color + light_color * diffuse + 192 | specular * specular_multiplier; 193 | color = min(color * light, vec3(1.0)); 194 | float camera_distance = distance(camera_position, frag_position); 195 | float fog_factor = pow(min(camera_distance / fog_distance, 1.0), 3.0); 196 | color = mix(color, fog_color, fog_factor); 197 | gl_FragColor = vec4(color, 1.0); 198 | } 199 | ''' 200 | def __init__(self): 201 | super(Program, self).__init__(self.VS, self.FS) 202 | def set_defaults(self, context): 203 | context.model_matrix = pg.Matrix() 204 | context.normal_matrix = pg.Matrix().inverse().transpose() 205 | context.specular_power = 32.0 206 | context.specular_multiplier = 0.2 207 | context.ambient_color = (0.4, 0.4, 0.4) 208 | context.light_color = (0.8, 0.8, 0.8) 209 | context.fog_color = (0.74, 0.70, 0.64) 210 | context.fog_distance = 10000 211 | context.light_direction = pg.normalize((1, 0.5, 1)) 212 | 213 | def main(): 214 | app = pg.App() 215 | Window(Worker()) 216 | app.run() 217 | 218 | if __name__ == "__main__": 219 | main() 220 | -------------------------------------------------------------------------------- /examples/lego.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi 2 | import pg 3 | import random 4 | 5 | class LegoMan(object): 6 | def __init__(self): 7 | self.x = (random.random() - 0.5) * 50 8 | self.z = (random.random() - 0.5) * 50 9 | self.a = random.random() * pi * 2 10 | def update(self, t, dt): 11 | if random.random() < 0.02: 12 | self.a += random.randint(-1, 1) * pi / 8 13 | dx = cos(self.a) 14 | dz = sin(self.a) 15 | self.x += dx * dt 16 | self.z += dz * dt 17 | 18 | class Window(pg.Window): 19 | def setup(self): 20 | self.wasd = pg.WASD(self, speed=10) 21 | self.wasd.look_at((0, 8, 30), (0, 0, 0)) 22 | self.context = pg.Context(pg.DirectionalLightProgram()) 23 | self.mesh = pg.OBJ('examples/lego.obj').center().smooth_normals() 24 | self.men = [LegoMan() for _ in xrange(50)] 25 | def update(self, t, dt): 26 | self.wasd.y = 1.5 27 | self.clear() 28 | for man in self.men: 29 | man.update(t, dt) 30 | a = man.a + pi / 2 31 | matrix = pg.Matrix().rotate((0, 1, 0), a).translate((-man.x, 0, -man.z)) 32 | inverse = pg.Matrix().rotate((0, 1, 0), -a) 33 | self.context.light_direction = inverse * pg.normalize((1, 1, 1)) 34 | self.context.camera_position = matrix.inverse() * self.wasd.position 35 | matrix = self.wasd.get_matrix(matrix) 36 | matrix = matrix.perspective(65, self.aspect, 0.1, 1000) 37 | self.context.matrix = matrix 38 | self.mesh.draw(self.context) 39 | 40 | if __name__ == "__main__": 41 | pg.run(Window) 42 | -------------------------------------------------------------------------------- /examples/moving_spheres.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi 2 | import pg 3 | 4 | RED = 0xF04326 5 | YELLOW = 0xFAC02D 6 | GREEN = 0x1AB243 7 | BLUE = 0x1256D1 8 | 9 | COLORS = [pg.hex_color(x) for x in [RED, YELLOW, GREEN, BLUE]] 10 | 11 | class Window(pg.Window): 12 | def setup(self): 13 | self.font = pg.Font(self, 0, '/Library/Fonts/Arial.ttf', 24) 14 | self.wasd = pg.WASD(self, speed=5) 15 | self.wasd.look_at((14, 0, 0), (0, 0, 0)) 16 | self.context = pg.Context(pg.DirectionalLightProgram()) 17 | self.sphere = pg.Sphere(5, 0.4, (0, 0, 0)) 18 | self.context.ambient_color = (0.4, 0.4, 0.4) 19 | self.context.light_color = (0.6, 0.6, 0.6) 20 | def draw(self): 21 | self.clear() 22 | self.context.camera_position = self.wasd.position 23 | matrix = self.wasd.get_matrix() 24 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 25 | for z in range(-2, 3): 26 | for x in range(-10, 11): 27 | y = sin(self.t * pi / 4 + x * 0.5 + z * pi) * 3 28 | model_matrix = pg.Matrix().translate((x, y, z * 3)) 29 | self.context.model_matrix = model_matrix 30 | self.context.matrix = matrix * model_matrix 31 | self.context.object_color = COLORS[(z + x) % len(COLORS)] 32 | self.sphere.draw(self.context) 33 | w, h = self.size 34 | self.font.render('%.1f fps' % self.fps, (w - 5, 0), (1, 0)) 35 | text = 'x=%.2f, y=%.2f, z=%.2f' % self.wasd.position 36 | self.font.render(text, (5, 0)) 37 | 38 | if __name__ == "__main__": 39 | pg.run(Window) 40 | -------------------------------------------------------------------------------- /examples/pipes.py: -------------------------------------------------------------------------------- 1 | import pg 2 | import random 3 | 4 | PIPES = 8 5 | SIZE = 16 6 | TURN_PROBABILITY = 0.3 7 | UPDATE_RATE = 0.05 8 | RESTART_RATE = 30 9 | 10 | DIRECTIONS = [ 11 | (0, -1, 0, 0), (0, 1, 0, 0), 12 | (1, 0, -1, 0), (1, 0, 1, 0), 13 | (2, 0, 0, -1), (2, 0, 0, 1), 14 | ] 15 | 16 | COLORS = [ 17 | 0x1f77b4, 0xaec7e8, 0xff7f0e, 0xffbb78, 0x2ca02c, 0x98df8a, 18 | 0xd62728, 0xff9896, 0x9467bd, 0xc5b0d5, 0x8c564b, 0xc49c94, 19 | 0xe377c2, 0xf7b6d2, 0x7f7f7f, 0xc7c7c7, 0x17becf, 0x9edae5, 20 | ] 21 | 22 | SPHERE = pg.Sphere(2, 0.25, (0, 0, 0)) 23 | 24 | CYLINDERS = [ 25 | pg.Cylinder((-0.5, 0, 0), (0.5, 0, 0), 0.25, 16, True), 26 | pg.Cylinder((0, -0.5, 0), (0, 0.5, 0), 0.25, 16, True), 27 | pg.Cylinder((0, 0, -0.5), (0, 0, 0.5), 0.25, 16, True), 28 | ] 29 | 30 | class Pipe(object): 31 | def __init__(self, occupied): 32 | self.occupied = occupied 33 | self.context = pg.Context(pg.DirectionalLightProgram()) 34 | self.context.object_color = pg.hex_color(random.choice(COLORS)) 35 | self.context.position = self.positions = pg.VertexBuffer() 36 | self.context.normal = self.normals = pg.VertexBuffer() 37 | self.restart() 38 | def add_cylinder(self, position, axis): 39 | mesh = pg.Matrix().translate(position) * CYLINDERS[axis] 40 | self.positions.extend(mesh.positions) 41 | self.normals.extend(mesh.normals) 42 | def add_sphere(self, position): 43 | mesh = pg.Matrix().translate(position) * SPHERE 44 | self.positions.extend(mesh.positions) 45 | self.normals.extend(mesh.normals) 46 | def restart(self): 47 | while True: 48 | x = random.randint(-SIZE, SIZE) 49 | y = random.randint(-SIZE, SIZE) 50 | z = random.randint(-SIZE, SIZE) 51 | if (x, y, z) not in self.occupied: 52 | break 53 | self.position = (x, y, z) 54 | self.direction = random.choice(DIRECTIONS) 55 | self.occupied.add(self.position) 56 | self.add_sphere(self.position) 57 | def update(self): 58 | x, y, z = self.position 59 | directions = list(DIRECTIONS) 60 | random.shuffle(directions) 61 | if random.random() > TURN_PROBABILITY: 62 | directions.remove(self.direction) 63 | directions.insert(0, self.direction) 64 | for direction in directions: 65 | axis, dx, dy, dz = direction 66 | nx, ny, nz = x + dx, y + dy, z + dz 67 | if (nx, ny, nz) in self.occupied: 68 | continue 69 | if any(n < -SIZE or n > SIZE for n in (nx, ny, nz)): 70 | continue 71 | self.position = (nx, ny, nz) 72 | self.occupied.add(self.position) 73 | mx, my, mz = x + dx / 2.0, y + dy / 2.0, z + dz / 2.0 74 | self.add_cylinder((mx, my, mz), axis) 75 | if direction != self.direction: 76 | self.add_sphere((x, y, z)) 77 | self.direction = direction 78 | return 79 | self.add_sphere((x, y, z)) 80 | self.restart() 81 | 82 | class Window(pg.Window): 83 | def setup(self): 84 | self.wasd = pg.WASD(self, speed=10) 85 | self.wasd.look_at((SIZE + 10, 0, 0), (0, 0, 0)) 86 | self.pipes = [] 87 | self.restart() 88 | self.last_update = 0 89 | self.last_restart = 0 90 | def restart(self): 91 | for pipe in self.pipes: 92 | pipe.positions.delete() 93 | pipe.normals.delete() 94 | occupied = set() 95 | self.pipes = [Pipe(occupied) for _ in xrange(PIPES)] 96 | def update(self, t, dt): 97 | if t - self.last_restart >= RESTART_RATE: 98 | self.last_restart += RESTART_RATE 99 | self.restart() 100 | if t - self.last_update >= UPDATE_RATE: 101 | self.last_update += UPDATE_RATE 102 | for pipe in self.pipes: 103 | pipe.update() 104 | def draw(self): 105 | matrix = self.wasd.get_matrix() 106 | matrix = matrix.perspective(65, self.aspect, 0.1, 1000) 107 | self.clear() 108 | for pipe in self.pipes: 109 | pipe.context.matrix = matrix 110 | pipe.context.camera_position = self.wasd.position 111 | pipe.context.draw() 112 | 113 | if __name__ == "__main__": 114 | pg.run(Window) 115 | -------------------------------------------------------------------------------- /examples/poisson_spheres.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self, speed=10) 6 | self.wasd.look_at((0, 3, 12), (0, 0, 7)) 7 | self.context = pg.Context(pg.DirectionalLightProgram()) 8 | self.points = pg.poisson_disc(-10, -10, 10, 10, 1.5, 32) 9 | self.mats = [pg.Matrix().translate((x, 0, z)) for x, z in self.points] 10 | self.sphere = pg.Sphere(4, 0.7) 11 | def draw(self): 12 | self.clear() 13 | self.context.camera_position = self.wasd.position 14 | matrix = self.wasd.get_matrix() 15 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 16 | for (x, z), mat in zip(self.points, self.mats): 17 | self.context.model_matrix = mat 18 | self.context.matrix = matrix * mat 19 | self.sphere.draw(self.context) 20 | 21 | if __name__ == "__main__": 22 | pg.run(Window) 23 | -------------------------------------------------------------------------------- /examples/scenes.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class SphereScene(pg.Scene): 4 | def setup(self): 5 | print 'SphereScene.setup()' 6 | self.wasd = pg.WASD(self) 7 | self.wasd.look_at((-1, 1, 1), (0, 0, 0)) 8 | self.context = pg.Context(pg.DirectionalLightProgram()) 9 | self.sphere = pg.Sphere(4, 0.5, (0, 0, 0)) 10 | def enter(self): 11 | print 'SphereScene.enter()' 12 | def update(self, t, dt): 13 | matrix = self.wasd.get_matrix() 14 | matrix = matrix.perspective(65, self.window.aspect, 0.01, 100) 15 | self.context.matrix = matrix 16 | self.context.camera_position = self.wasd.position 17 | def draw(self): 18 | self.window.clear() 19 | self.sphere.draw(self.context) 20 | def exit(self): 21 | print 'SphereScene.exit()' 22 | def teardown(self): 23 | print 'SphereScene.teardown()' 24 | def on_mouse_button(self, button, action, mods): 25 | if action == pg.PRESS: 26 | self.window.set_scene(self.window.cylinder_scene) 27 | 28 | class CylinderScene(pg.Scene): 29 | def setup(self): 30 | print 'CylinderScene.setup()' 31 | self.wasd = pg.WASD(self) 32 | self.wasd.look_at((-1, 1, 1), (0, 0, 0)) 33 | self.context = pg.Context(pg.DirectionalLightProgram()) 34 | self.cylinder = pg.Cylinder((0, -0.5, 0), (0, 0.5, 0), 0.5, 18) 35 | def enter(self): 36 | print 'CylinderScene.enter()' 37 | def update(self, t, dt): 38 | matrix = self.wasd.get_matrix() 39 | matrix = matrix.perspective(65, self.window.aspect, 0.01, 100) 40 | self.context.matrix = matrix 41 | self.context.camera_position = self.wasd.position 42 | def draw(self): 43 | self.window.clear() 44 | self.cylinder.draw(self.context) 45 | def exit(self): 46 | print 'CylinderScene.exit()' 47 | def teardown(self): 48 | print 'CylinderScene.teardown()' 49 | def on_mouse_button(self, button, action, mods): 50 | if action == pg.PRESS: 51 | self.window.set_scene(self.window.sphere_scene) 52 | 53 | class Window(pg.Window): 54 | def setup(self): 55 | self.sphere_scene = SphereScene(self) 56 | self.cylinder_scene = CylinderScene(self) 57 | self.set_scene(self.sphere_scene) 58 | 59 | if __name__ == "__main__": 60 | pg.run(Window) 61 | -------------------------------------------------------------------------------- /examples/sphere.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self) 6 | self.wasd.look_at((-1, 1, 1), (0, 0, 0)) 7 | self.context = pg.Context(pg.DirectionalLightProgram()) 8 | self.sphere = pg.Sphere(4, 0.5, (0, 0, 0)) 9 | def update(self, t, dt): 10 | matrix = pg.Matrix() 11 | matrix = self.wasd.get_matrix(matrix) 12 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 13 | self.context.matrix = matrix 14 | self.context.camera_position = self.wasd.position 15 | def draw(self): 16 | self.clear() 17 | self.sphere.draw(self.context) 18 | 19 | if __name__ == "__main__": 20 | pg.run(Window) 21 | -------------------------------------------------------------------------------- /examples/sprites.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi 2 | import pg 3 | 4 | class Window(pg.Window): 5 | def setup(self): 6 | sheet = pg.SpriteSheet(0, '/Users/fogleman/Desktop/Sprites') 7 | self.batch = pg.SpriteBatch(sheet) 8 | n = 1000 9 | for y in range(-n, n + 1, 64): 10 | for x in range(-n, n + 1, 64): 11 | sprite = sheet.star(self.batch) 12 | sprite.position = (x, y) 13 | def update(self, t, dt): 14 | for sprite in self.batch.sprites: 15 | sprite.rotation = -t * 4 16 | def draw(self): 17 | w, h = self.size 18 | w, h = w / 2, h / 2 19 | matrix = pg.Matrix().orthographic(-w, w, -h, h, -1, 1) 20 | self.clear() 21 | self.batch.draw(matrix) 22 | 23 | if __name__ == "__main__": 24 | pg.run(Window) 25 | -------------------------------------------------------------------------------- /examples/starfield.py: -------------------------------------------------------------------------------- 1 | import pg 2 | import random 3 | 4 | SPEED = 250 5 | COUNT = 10000 6 | FIELD_SIZE = 2000 7 | FIELD_DEPTH = 2500 8 | 9 | class Window(pg.Window): 10 | def setup(self): 11 | data = [] 12 | shape = pg.Plane((0, 0, 0), (0, 0, 1), 0.5, False) 13 | for _ in xrange(COUNT): 14 | x = (random.random() - 0.5) * FIELD_SIZE 15 | y = (random.random() - 0.5) * FIELD_SIZE 16 | z = random.random() * FIELD_DEPTH 17 | mesh = pg.Matrix().translate((x, y, z)) * shape 18 | data.extend(mesh.positions) 19 | self.context = pg.Context(pg.SolidColorProgram()) 20 | self.context.position = pg.VertexBuffer(data) 21 | def draw(self): 22 | self.clear() 23 | for m in xrange(-1, 2): 24 | z = m * FIELD_DEPTH + (-self.t * SPEED) % FIELD_DEPTH 25 | matrix = pg.Matrix().translate((0, 0, -z)) 26 | matrix = matrix.perspective(65, self.aspect, 1, 1000) 27 | self.context.matrix = matrix 28 | self.context.camera_position = (0, 0, z) 29 | self.context.draw() 30 | 31 | if __name__ == "__main__": 32 | pg.run(Window) 33 | -------------------------------------------------------------------------------- /examples/suzanne.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.71 (sub 0) OBJ File: '' 2 | # www.blender.org 3 | mtllib untitled.mtl 4 | o Suzanne 5 | v 1.477897 -0.299786 1.135786 6 | v 0.602897 -0.299786 1.135786 7 | v 1.540397 -0.370098 1.057661 8 | v 0.540397 -0.370098 1.057661 9 | v 1.587272 -0.409161 0.948286 10 | v 0.493522 -0.409161 0.948286 11 | v 1.391959 -0.487286 0.987349 12 | v 0.688834 -0.487286 0.987349 13 | v 1.391959 -0.432598 1.088911 14 | v 0.688834 -0.432598 1.088911 15 | v 1.391959 -0.331036 1.151411 16 | v 0.688834 -0.331036 1.151411 17 | v 1.313834 -0.299786 1.167036 18 | v 0.766959 -0.299786 1.167036 19 | v 1.243522 -0.370098 1.112349 20 | v 0.837272 -0.370098 1.112349 21 | v 1.196647 -0.409161 1.018599 22 | v 0.884147 -0.409161 1.018599 23 | v 1.118522 -0.221661 1.026411 24 | v 0.962272 -0.221661 1.026411 25 | v 1.181022 -0.221661 1.112349 26 | v 0.899772 -0.221661 1.112349 27 | v 1.282584 -0.221661 1.167036 28 | v 0.798209 -0.221661 1.167036 29 | v 1.313834 -0.135723 1.167036 30 | v 0.766959 -0.135723 1.167036 31 | v 1.243522 -0.073223 1.112349 32 | v 0.837272 -0.073223 1.112349 33 | v 1.196647 -0.026348 1.018599 34 | v 0.884147 -0.026348 1.018599 35 | v 1.391959 0.051777 0.987349 36 | v 0.688834 0.051777 0.987349 37 | v 1.391959 -0.010723 1.088911 38 | v 0.688834 -0.010723 1.088911 39 | v 1.391959 -0.104473 1.151411 40 | v 0.688834 -0.104473 1.151411 41 | v 1.477897 -0.135723 1.135786 42 | v 0.602897 -0.135723 1.135786 43 | v 1.540397 -0.073223 1.057661 44 | v 0.540397 -0.073223 1.057661 45 | v 1.587272 -0.026348 0.948286 46 | v 0.493522 -0.026348 0.948286 47 | v 1.665397 -0.221661 0.932661 48 | v 0.415397 -0.221661 0.932661 49 | v 1.602897 -0.221661 1.042036 50 | v 0.477897 -0.221661 1.042036 51 | v 1.509147 -0.221661 1.127974 52 | v 0.571647 -0.221661 1.127974 53 | v 1.516959 -0.221661 1.143599 54 | v 0.563834 -0.221661 1.143599 55 | v 1.485709 -0.127911 1.151411 56 | v 0.595084 -0.127911 1.151411 57 | v 1.391959 -0.088848 1.174849 58 | v 0.688834 -0.088848 1.174849 59 | v 1.306022 -0.127911 1.190474 60 | v 0.774772 -0.127911 1.190474 61 | v 1.266959 -0.221661 1.190474 62 | v 0.813834 -0.221661 1.190474 63 | v 1.306022 -0.307598 1.190474 64 | v 0.774772 -0.307598 1.190474 65 | v 1.391959 -0.221661 1.198286 66 | v 0.688834 -0.221661 1.198286 67 | v 1.391959 -0.346661 1.174849 68 | v 0.688834 -0.346661 1.174849 69 | v 1.485709 -0.307598 1.151411 70 | v 0.595084 -0.307598 1.151411 71 | v 1.040397 -0.034161 1.112349 72 | v 1.040397 -0.112286 1.190474 73 | v 1.040397 -1.143536 1.104536 74 | v 1.040397 -0.784161 1.151411 75 | v 1.040397 -0.651348 1.167036 76 | v 1.040397 -1.237286 1.088911 77 | v 1.040397 -0.057598 0.971724 78 | v 1.040397 0.106464 0.940474 79 | v 1.040397 0.434589 -0.176714 80 | v 1.040397 0.098652 -0.481401 81 | v 1.040397 -0.393536 -0.457964 82 | v 1.040397 -0.846661 0.018599 83 | v 1.243522 -0.651348 0.932661 84 | v 0.837272 -0.651348 0.932661 85 | v 1.352897 -0.901348 0.940474 86 | v 0.727897 -0.901348 0.940474 87 | v 1.391959 -1.159161 0.940474 88 | v 0.688834 -1.159161 0.940474 89 | v 1.407584 -1.354473 0.901411 90 | v 0.673209 -1.354473 0.901411 91 | v 1.368522 -1.409161 0.893599 92 | v 0.712272 -1.409161 0.893599 93 | v 1.220084 -1.432598 0.924849 94 | v 0.860709 -1.432598 0.924849 95 | v 1.040397 -1.448223 0.948286 96 | v 1.477897 -0.604473 0.901411 97 | v 0.602897 -0.604473 0.901411 98 | v 1.673209 -0.502911 0.909224 99 | v 0.407584 -0.502911 0.909224 100 | v 1.868522 -0.315411 0.815474 101 | v 0.212272 -0.315411 0.815474 102 | v 1.899772 -0.034161 0.963911 103 | v 0.181022 -0.034161 0.963911 104 | v 1.751334 0.020527 0.995161 105 | v 0.329459 0.020527 0.995161 106 | v 1.532584 0.137714 1.057661 107 | v 0.548209 0.137714 1.057661 108 | v 1.360709 0.293964 1.104536 109 | v 0.720084 0.293964 1.104536 110 | v 1.196647 0.254902 1.127974 111 | v 0.884147 0.254902 1.127974 112 | v 1.102897 0.028339 1.120161 113 | v 0.977897 0.028339 1.120161 114 | v 1.204459 -0.049786 1.143599 115 | v 0.876334 -0.049786 1.143599 116 | v 1.165397 -0.159161 1.135786 117 | v 0.915397 -0.159161 1.135786 118 | v 1.243522 -0.370098 1.112349 119 | v 0.837272 -0.370098 1.112349 120 | v 1.415397 -0.448223 1.073286 121 | v 0.665397 -0.448223 1.073286 122 | v 1.532584 -0.401348 1.042036 123 | v 0.548209 -0.401348 1.042036 124 | v 1.665397 -0.276348 1.018599 125 | v 0.415397 -0.276348 1.018599 126 | v 1.681022 -0.166973 1.018599 127 | v 0.399772 -0.166973 1.018599 128 | v 1.641959 -0.088848 1.034224 129 | v 0.438834 -0.088848 1.034224 130 | v 1.470084 -0.026348 1.088911 131 | v 0.610709 -0.026348 1.088911 132 | v 1.290397 0.004902 1.127974 133 | v 0.790397 0.004902 1.127974 134 | v 1.040397 -1.229473 1.104536 135 | v 1.149772 -1.182598 1.104536 136 | v 0.931022 -1.182598 1.104536 137 | v 1.157584 -1.299786 1.081099 138 | v 0.923209 -1.299786 1.081099 139 | v 1.102897 -1.346661 1.065474 140 | v 0.977897 -1.346661 1.065474 141 | v 1.040397 -1.354473 1.057661 142 | v 1.040397 -0.659161 1.120161 143 | v 1.040397 -0.604473 1.112349 144 | v 1.141959 -0.612286 1.112349 145 | v 0.938834 -0.612286 1.112349 146 | v 1.165397 -0.690411 1.120161 147 | v 0.915397 -0.690411 1.120161 148 | v 1.126334 -0.752911 1.112349 149 | v 0.954459 -0.752911 1.112349 150 | v 1.438834 -0.510723 1.042036 151 | v 0.641959 -0.510723 1.042036 152 | v 1.657584 -0.409161 0.995161 153 | v 0.423209 -0.409161 0.995161 154 | v 1.766959 -0.260723 0.971724 155 | v 0.313834 -0.260723 0.971724 156 | v 1.782584 -0.088848 1.026411 157 | v 0.298209 -0.088848 1.026411 158 | v 1.727897 -0.049786 1.096724 159 | v 0.352897 -0.049786 1.096724 160 | v 1.477897 0.083027 1.167036 161 | v 0.602897 0.083027 1.167036 162 | v 1.352897 0.176777 1.206099 163 | v 0.727897 0.176777 1.206099 164 | v 1.243522 0.153339 1.221724 165 | v 0.837272 0.153339 1.221724 166 | v 1.141959 -0.034161 1.213911 167 | v 0.938834 -0.034161 1.213911 168 | v 1.165397 -0.565411 1.182661 169 | v 0.915397 -0.565411 1.182661 170 | v 1.251334 -0.909161 1.081099 171 | v 0.829459 -0.909161 1.081099 172 | v 1.290397 -1.166973 1.057661 173 | v 0.790397 -1.166973 1.057661 174 | v 1.306022 -1.284161 1.034224 175 | v 0.774772 -1.284161 1.034224 176 | v 1.274772 -1.377911 1.002974 177 | v 0.806022 -1.377911 1.002974 178 | v 1.204459 -1.393536 1.002974 179 | v 0.876334 -1.393536 1.002974 180 | v 1.040397 -1.409161 1.010786 181 | v 1.040397 -0.416973 1.096724 182 | v 1.040397 -0.252911 1.135786 183 | v 1.368522 0.012714 1.112349 184 | v 0.712272 0.012714 1.112349 185 | v 1.204459 -0.323223 1.120161 186 | v 0.876334 -0.323223 1.120161 187 | v 1.173209 -0.252911 1.127974 188 | v 0.907584 -0.252911 1.127974 189 | v 1.157584 -1.151348 1.104536 190 | v 0.923209 -1.151348 1.104536 191 | v 1.118522 -0.909161 1.120161 192 | v 0.962272 -0.909161 1.120161 193 | v 1.040397 -0.909161 1.120161 194 | v 1.040397 -0.791973 1.112349 195 | v 1.134147 -0.737286 1.151411 196 | v 0.946647 -0.737286 1.151411 197 | v 1.173209 -0.690411 1.167036 198 | v 0.907584 -0.690411 1.167036 199 | v 1.149772 -0.596661 1.151411 200 | v 0.931022 -0.596661 1.151411 201 | v 1.079459 -0.588848 1.151411 202 | v 1.001334 -0.588848 1.151411 203 | v 1.040397 -0.666973 1.198286 204 | v 1.087272 -0.612286 1.182661 205 | v 0.993522 -0.612286 1.182661 206 | v 1.134147 -0.620098 1.182661 207 | v 0.946647 -0.620098 1.182661 208 | v 1.149772 -0.690411 1.198286 209 | v 0.931022 -0.690411 1.198286 210 | v 1.118522 -0.713848 1.174849 211 | v 0.962272 -0.713848 1.174849 212 | v 1.040397 -0.752911 1.174849 213 | v 1.298209 -0.776348 0.924849 214 | v 0.782584 -0.776348 0.924849 215 | v 1.204459 -0.706036 1.081099 216 | v 0.876334 -0.706036 1.081099 217 | v 1.220084 -0.776348 1.081099 218 | v 0.860709 -0.776348 1.081099 219 | v 1.274772 -0.713848 0.924849 220 | v 0.806022 -0.713848 0.924849 221 | v 1.040397 -1.338848 1.057661 222 | v 1.087272 -1.331036 1.057661 223 | v 0.993522 -1.331036 1.057661 224 | v 1.134147 -1.284161 1.081099 225 | v 0.946647 -1.284161 1.081099 226 | v 1.134147 -1.206036 1.096724 227 | v 0.946647 -1.206036 1.096724 228 | v 1.040397 -1.245098 1.026411 229 | v 1.134147 -1.213848 1.034224 230 | v 0.946647 -1.213848 1.034224 231 | v 1.134147 -1.276348 1.010786 232 | v 0.946647 -1.276348 1.010786 233 | v 1.087272 -1.315411 1.002974 234 | v 0.993522 -1.315411 1.002974 235 | v 1.040397 -1.323223 1.002974 236 | v 1.212272 -0.245098 1.151411 237 | v 0.868522 -0.245098 1.151411 238 | v 1.227897 -0.307598 1.143599 239 | v 0.852897 -0.307598 1.143599 240 | v 1.376334 -0.034161 1.127974 241 | v 0.704459 -0.034161 1.127974 242 | v 1.313834 -0.041973 1.143599 243 | v 0.766959 -0.041973 1.143599 244 | v 1.462272 -0.065411 1.143599 245 | v 0.618522 -0.065411 1.143599 246 | v 1.602897 -0.112286 1.065474 247 | v 0.477897 -0.112286 1.065474 248 | v 1.626334 -0.174786 1.057661 249 | v 0.454459 -0.174786 1.057661 250 | v 1.618522 -0.268536 1.049849 251 | v 0.462272 -0.268536 1.049849 252 | v 1.516959 -0.362286 1.088911 253 | v 0.563834 -0.362286 1.088911 254 | v 1.415397 -0.401348 1.112349 255 | v 0.665397 -0.401348 1.112349 256 | v 1.266959 -0.354473 1.151411 257 | v 0.813834 -0.354473 1.151411 258 | v 1.220084 -0.166973 1.151411 259 | v 0.860709 -0.166973 1.151411 260 | v 1.251334 -0.088848 1.151411 261 | v 0.829459 -0.088848 1.151411 262 | v 1.274772 -0.104473 1.127974 263 | v 0.806022 -0.104473 1.127974 264 | v 1.235709 -0.166973 1.127974 265 | v 0.845084 -0.166973 1.127974 266 | v 1.282584 -0.338848 1.127974 267 | v 0.798209 -0.338848 1.127974 268 | v 1.415397 -0.377911 1.096724 269 | v 0.665397 -0.377911 1.096724 270 | v 1.501334 -0.346661 1.073286 271 | v 0.579459 -0.346661 1.073286 272 | v 1.587272 -0.252911 1.042036 273 | v 0.493522 -0.252911 1.042036 274 | v 1.595084 -0.182598 1.042036 275 | v 0.485709 -0.182598 1.042036 276 | v 1.571647 -0.127911 1.049849 277 | v 0.509147 -0.127911 1.049849 278 | v 1.454459 -0.073223 1.120161 279 | v 0.626334 -0.073223 1.120161 280 | v 1.321647 -0.065411 1.135786 281 | v 0.759147 -0.065411 1.135786 282 | v 1.376334 -0.057598 1.120161 283 | v 0.704459 -0.057598 1.120161 284 | v 1.243522 -0.291973 1.120161 285 | v 0.837272 -0.291973 1.120161 286 | v 1.235709 -0.237286 1.120161 287 | v 0.845084 -0.237286 1.120161 288 | v 1.149772 -0.002911 0.979536 289 | v 0.931022 -0.002911 0.979536 290 | v 1.235709 0.200214 0.987349 291 | v 0.845084 0.200214 0.987349 292 | v 1.376334 0.223652 0.963911 293 | v 0.704459 0.223652 0.963911 294 | v 1.524772 0.090839 0.924849 295 | v 0.556022 0.090839 0.924849 296 | v 1.720084 -0.010723 0.862349 297 | v 0.360709 -0.010723 0.862349 298 | v 1.837272 -0.057598 0.831099 299 | v 0.243522 -0.057598 0.831099 300 | v 1.813834 -0.299786 0.745161 301 | v 0.266959 -0.299786 0.745161 302 | v 1.641959 -0.463848 0.784224 303 | v 0.438834 -0.463848 0.784224 304 | v 1.477897 -0.557598 0.838911 305 | v 0.602897 -0.557598 0.838911 306 | v 1.040397 0.434589 0.659224 307 | v 1.040397 0.520527 0.292036 308 | v 1.040397 -0.659161 -0.301714 309 | v 1.040397 -0.924786 0.557661 310 | v 1.040397 -1.440411 0.831099 311 | v 1.040397 -1.268536 0.713911 312 | v 1.040397 -1.034161 0.690474 313 | v 1.040397 -0.948223 0.651411 314 | v 1.891959 -0.229473 0.424849 315 | v 0.188834 -0.229473 0.424849 316 | v 1.899772 -0.143536 0.323286 317 | v 0.181022 -0.143536 0.323286 318 | v 1.813834 -0.198223 -0.067339 319 | v 0.266959 -0.198223 -0.067339 320 | v 1.501334 -0.026348 -0.332964 321 | v 0.579459 -0.026348 -0.332964 322 | v 1.774772 -0.510723 0.440474 323 | v 0.306022 -0.510723 0.440474 324 | v 1.634147 -0.588848 0.206099 325 | v 0.446647 -0.588848 0.206099 326 | v 1.681022 -0.471661 -0.059526 327 | v 0.399772 -0.471661 -0.059526 328 | v 1.376334 -0.409161 -0.293901 329 | v 0.704459 -0.409161 -0.293901 330 | v 1.274772 -0.815411 0.776411 331 | v 0.806022 -0.815411 0.776411 332 | v 1.220084 -0.877911 0.627974 333 | v 0.860709 -0.877911 0.627974 334 | v 1.329459 -1.174786 0.752974 335 | v 0.751334 -1.174786 0.752974 336 | v 1.290397 -0.963848 0.760786 337 | v 0.790397 -0.963848 0.760786 338 | v 1.368522 -1.377911 0.768599 339 | v 0.712272 -1.377911 0.768599 340 | v 1.181022 -1.221661 0.737349 341 | v 0.899772 -1.221661 0.737349 342 | v 1.165397 -1.002911 0.729536 343 | v 0.915397 -1.002911 0.729536 344 | v 1.204459 -1.409161 0.807661 345 | v 0.876334 -1.409161 0.807661 346 | v 1.259147 -0.745098 0.799849 347 | v 0.821647 -0.745098 0.799849 348 | v 1.251334 -0.690411 0.838911 349 | v 0.829459 -0.690411 0.838911 350 | v 1.243522 -0.635723 0.870161 351 | v 0.837272 -0.635723 0.870161 352 | v 1.251334 -0.854473 0.534224 353 | v 0.829459 -0.854473 0.534224 354 | v 1.337272 -0.776348 0.104536 355 | v 0.743522 -0.776348 0.104536 356 | v 1.384147 -0.612286 -0.168901 357 | v 0.696647 -0.612286 -0.168901 358 | v 1.493522 0.403339 -0.012651 359 | v 0.587272 0.403339 -0.012651 360 | v 1.493522 0.465839 0.299849 361 | v 0.587272 0.465839 0.299849 362 | v 1.493522 0.387714 0.604536 363 | v 0.587272 0.387714 0.604536 364 | v 1.501334 0.059589 0.799849 365 | v 0.579459 0.059589 0.799849 366 | v 1.766959 -0.057598 0.706099 367 | v 0.313834 -0.057598 0.706099 368 | v 1.673209 -0.010723 0.651411 369 | v 0.407584 -0.010723 0.651411 370 | v 1.681022 0.239277 0.424849 371 | v 0.399772 0.239277 0.424849 372 | v 1.837272 0.098652 0.495161 373 | v 0.243522 0.098652 0.495161 374 | v 1.837272 0.153339 0.252974 375 | v 0.243522 0.153339 0.252974 376 | v 1.681022 0.286152 0.174849 377 | v 0.399772 0.286152 0.174849 378 | v 1.681022 0.215839 -0.075151 379 | v 0.399772 0.215839 -0.075151 380 | v 1.837272 0.075214 0.010786 381 | v 0.243522 0.075214 0.010786 382 | v 1.657584 -0.135723 -0.215776 383 | v 0.423209 -0.135723 -0.215776 384 | v 1.524772 -0.440411 -0.176714 385 | v 0.556022 -0.440411 -0.176714 386 | v 1.860709 -0.135723 0.167036 387 | v 0.220084 -0.135723 0.167036 388 | v 1.446647 -0.635723 0.518599 389 | v 0.634147 -0.635723 0.518599 390 | v 1.470084 -0.659161 0.159224 391 | v 0.610709 -0.659161 0.159224 392 | v 1.931022 -0.057598 0.135786 393 | v 0.149772 -0.057598 0.135786 394 | v 1.813834 -0.604473 0.245161 395 | v 0.266959 -0.604473 0.245161 396 | v 2.079459 -0.565411 0.042036 397 | v 0.001334 -0.565411 0.042036 398 | v 2.321647 -0.409161 -0.059526 399 | v -0.240853 -0.409161 -0.059526 400 | v 2.391959 -0.143536 -0.051714 401 | v -0.311166 -0.143536 -0.051714 402 | v 2.274772 0.043964 -0.051714 403 | v -0.193978 0.043964 -0.051714 404 | v 2.063834 0.012714 0.057661 405 | v 0.016959 0.012714 0.057661 406 | v 2.056022 -0.049786 0.081099 407 | v 0.024772 -0.049786 0.081099 408 | v 2.227897 -0.026348 -0.020464 409 | v -0.147103 -0.026348 -0.020464 410 | v 2.306022 -0.174786 -0.036089 411 | v -0.225228 -0.174786 -0.036089 412 | v 2.251334 -0.385723 -0.036089 413 | v -0.170541 -0.385723 -0.036089 414 | v 2.071647 -0.502911 0.065474 415 | v 0.009147 -0.502911 0.065474 416 | v 1.868522 -0.534161 0.237349 417 | v 0.212272 -0.534161 0.237349 418 | v 1.962272 -0.104473 0.151411 419 | v 0.118522 -0.104473 0.151411 420 | v 1.985709 -0.159161 0.081099 421 | v 0.095084 -0.159161 0.081099 422 | v 1.923209 -0.487286 0.159224 423 | v 0.157584 -0.487286 0.159224 424 | v 2.079459 -0.463848 0.002974 425 | v 0.001334 -0.463848 0.002974 426 | v 2.227897 -0.370098 -0.075151 427 | v -0.147103 -0.370098 -0.075151 428 | v 2.274772 -0.213848 -0.075151 429 | v -0.193978 -0.213848 -0.075151 430 | v 2.212272 -0.104473 -0.067339 431 | v -0.131478 -0.104473 -0.067339 432 | v 2.063834 -0.120098 0.010786 433 | v 0.016959 -0.120098 0.010786 434 | v 1.884147 -0.174786 0.159224 435 | v 0.196647 -0.174786 0.159224 436 | v 1.876334 -0.291973 0.096724 437 | v 0.204459 -0.291973 0.096724 438 | v 1.798209 -0.370098 0.096724 439 | v 0.282584 -0.370098 0.096724 440 | v 1.860709 -0.377911 0.096724 441 | v 0.220084 -0.377911 0.096724 442 | v 1.884147 -0.448223 0.096724 443 | v 0.196647 -0.448223 0.096724 444 | v 1.852897 -0.479473 0.096724 445 | v 0.227897 -0.479473 0.096724 446 | v 1.766959 -0.463848 0.299849 447 | v 0.313834 -0.463848 0.299849 448 | v 1.759147 -0.487286 0.198286 449 | v 0.321647 -0.487286 0.198286 450 | v 1.759147 -0.424786 0.182661 451 | v 0.321647 -0.424786 0.182661 452 | v 1.837272 -0.260723 0.159224 453 | v 0.243522 -0.260723 0.159224 454 | v 1.931022 -0.221661 0.104536 455 | v 0.149772 -0.221661 0.104536 456 | v 1.931022 -0.229473 0.049849 457 | v 0.149772 -0.229473 0.049849 458 | v 1.852897 -0.479473 0.049849 459 | v 0.227897 -0.479473 0.049849 460 | v 1.891959 -0.448223 0.049849 461 | v 0.188834 -0.448223 0.049849 462 | v 1.868522 -0.385723 0.049849 463 | v 0.212272 -0.385723 0.049849 464 | v 1.806022 -0.370098 0.049849 465 | v 0.274772 -0.370098 0.049849 466 | v 1.884147 -0.291973 0.049849 467 | v 0.196647 -0.291973 0.049849 468 | v 2.079459 -0.135723 -0.043901 469 | v 0.001334 -0.135723 -0.043901 470 | v 2.227897 -0.120098 -0.114214 471 | v -0.147103 -0.120098 -0.114214 472 | v 2.298209 -0.221661 -0.122026 473 | v -0.217416 -0.221661 -0.122026 474 | v 2.251334 -0.377911 -0.114214 475 | v -0.170541 -0.377911 -0.114214 476 | v 2.087272 -0.463848 -0.051714 477 | v -0.006478 -0.463848 -0.051714 478 | v 1.923209 -0.479473 0.104536 479 | v 0.157584 -0.479473 0.104536 480 | v 1.993522 -0.174786 0.026411 481 | v 0.087272 -0.174786 0.026411 482 | v 1.931022 -0.354473 0.042036 483 | v 0.149772 -0.354473 0.042036 484 | v 1.977897 -0.401348 0.034224 485 | v 0.102897 -0.401348 0.034224 486 | v 2.040397 -0.338848 0.002974 487 | v 0.040397 -0.338848 0.002974 488 | v 2.001334 -0.291973 0.018599 489 | v 0.079459 -0.291973 0.018599 490 | v 2.056022 -0.229473 -0.004839 491 | v 0.024772 -0.229473 -0.004839 492 | v 2.095084 -0.276348 -0.012651 493 | v -0.014291 -0.276348 -0.012651 494 | v 2.149772 -0.252911 -0.020464 495 | v -0.068978 -0.252911 -0.020464 496 | v 2.126334 -0.190411 -0.020464 497 | v -0.045541 -0.190411 -0.020464 498 | v 2.063834 -0.026348 -0.114214 499 | v 0.016959 -0.026348 -0.114214 500 | v 2.290397 0.004902 -0.176714 501 | v -0.209603 0.004902 -0.176714 502 | v 2.407584 -0.166973 -0.129839 503 | v -0.326791 -0.166973 -0.129839 504 | v 2.352897 -0.409161 -0.161089 505 | v -0.272103 -0.409161 -0.161089 506 | v 2.079459 -0.549786 -0.122026 507 | v 0.001334 -0.549786 -0.122026 508 | v 1.829459 -0.588848 0.042036 509 | v 0.251334 -0.588848 0.042036 510 | v 1.899772 -0.081036 -0.012651 511 | v 0.181022 -0.081036 -0.012651 512 | usemtl None 513 | s off 514 | f 47 1 3 45 515 | f 4 2 48 46 516 | f 45 3 5 43 517 | f 6 4 46 44 518 | f 3 9 7 5 519 | f 8 10 4 6 520 | f 1 11 9 3 521 | f 10 12 2 4 522 | f 11 13 15 9 523 | f 16 14 12 10 524 | f 9 15 17 7 525 | f 18 16 10 8 526 | f 15 21 19 17 527 | f 20 22 16 18 528 | f 13 23 21 15 529 | f 22 24 14 16 530 | f 23 25 27 21 531 | f 28 26 24 22 532 | f 21 27 29 19 533 | f 30 28 22 20 534 | f 27 33 31 29 535 | f 32 34 28 30 536 | f 25 35 33 27 537 | f 34 36 26 28 538 | f 35 37 39 33 539 | f 40 38 36 34 540 | f 33 39 41 31 541 | f 42 40 34 32 542 | f 39 45 43 41 543 | f 44 46 40 42 544 | f 37 47 45 39 545 | f 46 48 38 40 546 | f 47 37 51 49 547 | f 52 38 48 50 548 | f 37 35 53 51 549 | f 54 36 38 52 550 | f 35 25 55 53 551 | f 56 26 36 54 552 | f 25 23 57 55 553 | f 58 24 26 56 554 | f 23 13 59 57 555 | f 60 14 24 58 556 | f 13 11 63 59 557 | f 64 12 14 60 558 | f 11 1 65 63 559 | f 66 2 12 64 560 | f 1 47 49 65 561 | f 50 48 2 66 562 | f 61 65 49 563 | f 50 66 62 564 | f 63 65 61 565 | f 62 66 64 566 | f 61 59 63 567 | f 64 60 62 568 | f 61 57 59 569 | f 60 58 62 570 | f 61 55 57 571 | f 58 56 62 572 | f 61 53 55 573 | f 56 54 62 574 | f 61 51 53 575 | f 54 52 62 576 | f 61 49 51 577 | f 52 50 62 578 | f 89 174 176 91 579 | f 176 175 90 91 580 | f 87 172 174 89 581 | f 175 173 88 90 582 | f 85 170 172 87 583 | f 173 171 86 88 584 | f 83 168 170 85 585 | f 171 169 84 86 586 | f 81 166 168 83 587 | f 169 167 82 84 588 | f 79 92 146 164 589 | f 147 93 80 165 590 | f 92 94 148 146 591 | f 149 95 93 147 592 | f 94 96 150 148 593 | f 151 97 95 149 594 | f 96 98 152 150 595 | f 153 99 97 151 596 | f 98 100 154 152 597 | f 155 101 99 153 598 | f 100 102 156 154 599 | f 157 103 101 155 600 | f 102 104 158 156 601 | f 159 105 103 157 602 | f 104 106 160 158 603 | f 161 107 105 159 604 | f 106 108 162 160 605 | f 163 109 107 161 606 | f 108 67 68 162 607 | f 68 67 109 163 608 | f 110 128 160 162 609 | f 161 129 111 163 610 | f 128 179 158 160 611 | f 159 180 129 161 612 | f 126 156 158 179 613 | f 159 157 127 180 614 | f 124 154 156 126 615 | f 157 155 125 127 616 | f 122 152 154 124 617 | f 155 153 123 125 618 | f 120 150 152 122 619 | f 153 151 121 123 620 | f 118 148 150 120 621 | f 151 149 119 121 622 | f 116 146 148 118 623 | f 149 147 117 119 624 | f 114 164 146 116 625 | f 147 165 115 117 626 | f 114 181 177 164 627 | f 177 182 115 165 628 | f 110 162 68 112 629 | f 68 163 111 113 630 | f 112 68 178 183 631 | f 178 68 113 184 632 | f 177 181 183 178 633 | f 184 182 177 178 634 | f 135 137 176 174 635 | f 176 137 136 175 636 | f 133 135 174 172 637 | f 175 136 134 173 638 | f 131 133 172 170 639 | f 173 134 132 171 640 | f 166 187 185 168 641 | f 186 188 167 169 642 | f 131 170 168 185 643 | f 169 171 132 186 644 | f 144 190 189 187 645 | f 189 190 145 188 646 | f 185 187 189 69 647 | f 189 188 186 69 648 | f 130 131 185 69 649 | f 186 132 130 69 650 | f 142 193 191 144 651 | f 192 194 143 145 652 | f 140 195 193 142 653 | f 194 196 141 143 654 | f 139 197 195 140 655 | f 196 198 139 141 656 | f 138 71 197 139 657 | f 198 71 138 139 658 | f 190 144 191 70 659 | f 192 145 190 70 660 | f 70 191 206 208 661 | f 207 192 70 208 662 | f 71 199 200 197 663 | f 201 199 71 198 664 | f 197 200 202 195 665 | f 203 201 198 196 666 | f 195 202 204 193 667 | f 205 203 196 194 668 | f 193 204 206 191 669 | f 207 205 194 192 670 | f 199 204 202 200 671 | f 203 205 199 201 672 | f 199 208 206 204 673 | f 207 208 199 205 674 | f 139 140 164 177 675 | f 165 141 139 177 676 | f 140 142 211 164 677 | f 212 143 141 165 678 | f 142 144 213 211 679 | f 214 145 143 212 680 | f 144 187 166 213 681 | f 167 188 145 214 682 | f 81 209 213 166 683 | f 214 210 82 167 684 | f 209 215 211 213 685 | f 212 216 210 214 686 | f 79 164 211 215 687 | f 212 165 80 216 688 | f 131 130 72 222 689 | f 72 130 132 223 690 | f 133 131 222 220 691 | f 223 132 134 221 692 | f 135 133 220 218 693 | f 221 134 136 219 694 | f 137 135 218 217 695 | f 219 136 137 217 696 | f 217 218 229 231 697 | f 230 219 217 231 698 | f 218 220 227 229 699 | f 228 221 219 230 700 | f 220 222 225 227 701 | f 226 223 221 228 702 | f 222 72 224 225 703 | f 224 72 223 226 704 | f 224 231 229 225 705 | f 230 231 224 226 706 | f 225 229 227 707 | f 228 230 226 708 | f 183 181 234 232 709 | f 235 182 184 233 710 | f 112 183 232 254 711 | f 233 184 113 255 712 | f 110 112 254 256 713 | f 255 113 111 257 714 | f 181 114 252 234 715 | f 253 115 182 235 716 | f 114 116 250 252 717 | f 251 117 115 253 718 | f 116 118 248 250 719 | f 249 119 117 251 720 | f 118 120 246 248 721 | f 247 121 119 249 722 | f 120 122 244 246 723 | f 245 123 121 247 724 | f 122 124 242 244 725 | f 243 125 123 245 726 | f 124 126 240 242 727 | f 241 127 125 243 728 | f 126 179 236 240 729 | f 237 180 127 241 730 | f 179 128 238 236 731 | f 239 129 180 237 732 | f 128 110 256 238 733 | f 257 111 129 239 734 | f 238 256 258 276 735 | f 259 257 239 277 736 | f 236 238 276 278 737 | f 277 239 237 279 738 | f 240 236 278 274 739 | f 279 237 241 275 740 | f 242 240 274 272 741 | f 275 241 243 273 742 | f 244 242 272 270 743 | f 273 243 245 271 744 | f 246 244 270 268 745 | f 271 245 247 269 746 | f 248 246 268 266 747 | f 269 247 249 267 748 | f 250 248 266 264 749 | f 267 249 251 265 750 | f 252 250 264 262 751 | f 265 251 253 263 752 | f 234 252 262 280 753 | f 263 253 235 281 754 | f 256 254 260 258 755 | f 261 255 257 259 756 | f 254 232 282 260 757 | f 283 233 255 261 758 | f 232 234 280 282 759 | f 281 235 233 283 760 | f 67 108 284 73 761 | f 285 109 67 73 762 | f 108 106 286 284 763 | f 287 107 109 285 764 | f 106 104 288 286 765 | f 289 105 107 287 766 | f 104 102 290 288 767 | f 291 103 105 289 768 | f 102 100 292 290 769 | f 293 101 103 291 770 | f 100 98 294 292 771 | f 295 99 101 293 772 | f 98 96 296 294 773 | f 297 97 99 295 774 | f 96 94 298 296 775 | f 299 95 97 297 776 | f 94 92 300 298 777 | f 301 93 95 299 778 | f 308 309 328 338 779 | f 329 309 308 339 780 | f 307 308 338 336 781 | f 339 308 307 337 782 | f 306 307 336 340 783 | f 337 307 306 341 784 | f 89 91 306 340 785 | f 306 91 90 341 786 | f 87 89 340 334 787 | f 341 90 88 335 788 | f 85 87 334 330 789 | f 335 88 86 331 790 | f 83 85 330 332 791 | f 331 86 84 333 792 | f 330 336 338 332 793 | f 339 337 331 333 794 | f 330 334 340 336 795 | f 341 335 331 337 796 | f 326 332 338 328 797 | f 339 333 327 329 798 | f 81 83 332 326 799 | f 333 84 82 327 800 | f 209 342 344 215 801 | f 345 343 210 216 802 | f 81 326 342 209 803 | f 343 327 82 210 804 | f 79 215 344 346 805 | f 345 216 80 347 806 | f 79 346 300 92 807 | f 301 347 80 93 808 | f 77 324 352 304 809 | f 353 325 77 304 810 | f 304 352 350 78 811 | f 351 353 304 78 812 | f 78 350 348 305 813 | f 349 351 78 305 814 | f 305 348 328 309 815 | f 329 349 305 309 816 | f 326 328 348 342 817 | f 349 329 327 343 818 | f 296 298 318 310 819 | f 319 299 297 311 820 | f 76 316 324 77 821 | f 325 317 76 77 822 | f 302 358 356 303 823 | f 357 359 302 303 824 | f 303 356 354 75 825 | f 355 357 303 75 826 | f 75 354 316 76 827 | f 317 355 75 76 828 | f 292 294 362 364 829 | f 363 295 293 365 830 | f 364 362 368 366 831 | f 369 363 365 367 832 | f 366 368 370 372 833 | f 371 369 367 373 834 | f 372 370 376 374 835 | f 377 371 373 375 836 | f 314 378 374 376 837 | f 375 379 315 377 838 | f 316 354 374 378 839 | f 375 355 317 379 840 | f 354 356 372 374 841 | f 373 357 355 375 842 | f 356 358 366 372 843 | f 367 359 357 373 844 | f 358 360 364 366 845 | f 365 361 359 367 846 | f 290 292 364 360 847 | f 365 293 291 361 848 | f 74 360 358 302 849 | f 359 361 74 302 850 | f 284 286 288 290 851 | f 289 287 285 291 852 | f 284 290 360 74 853 | f 361 291 285 74 854 | f 73 284 74 855 | f 74 285 73 856 | f 294 296 310 362 857 | f 311 297 295 363 858 | f 310 312 368 362 859 | f 369 313 311 363 860 | f 312 382 370 368 861 | f 371 383 313 369 862 | f 314 376 370 382 863 | f 371 377 315 383 864 | f 348 350 386 384 865 | f 387 351 349 385 866 | f 318 384 386 320 867 | f 387 385 319 321 868 | f 298 300 384 318 869 | f 385 301 299 319 870 | f 300 344 342 384 871 | f 343 345 301 385 872 | f 342 348 384 873 | f 385 349 343 874 | f 300 346 344 875 | f 345 347 301 876 | f 314 322 380 378 877 | f 381 323 315 379 878 | f 316 378 380 324 879 | f 381 379 317 325 880 | f 320 386 380 322 881 | f 381 387 321 323 882 | f 350 352 380 386 883 | f 381 353 351 387 884 | f 324 380 352 885 | f 353 381 325 886 | f 400 388 414 402 887 | f 415 389 401 403 888 | f 400 402 404 398 889 | f 405 403 401 399 890 | f 398 404 406 396 891 | f 407 405 399 397 892 | f 396 406 408 394 893 | f 409 407 397 395 894 | f 394 408 410 392 895 | f 411 409 395 393 896 | f 392 410 412 390 897 | f 413 411 393 391 898 | f 410 420 418 412 899 | f 419 421 411 413 900 | f 408 422 420 410 901 | f 421 423 409 411 902 | f 406 424 422 408 903 | f 423 425 407 409 904 | f 404 426 424 406 905 | f 425 427 405 407 906 | f 402 428 426 404 907 | f 427 429 403 405 908 | f 402 414 416 428 909 | f 417 415 403 429 910 | f 318 320 444 442 911 | f 445 321 319 443 912 | f 320 390 412 444 913 | f 413 391 321 445 914 | f 310 318 442 312 915 | f 443 319 311 313 916 | f 382 430 414 388 917 | f 415 431 383 389 918 | f 412 418 440 444 919 | f 441 419 413 445 920 | f 438 446 444 440 921 | f 445 447 439 441 922 | f 434 446 438 436 923 | f 439 447 435 437 924 | f 432 448 446 434 925 | f 447 449 433 435 926 | f 430 448 432 450 927 | f 433 449 431 451 928 | f 414 430 450 416 929 | f 451 431 415 417 930 | f 312 448 430 382 931 | f 431 449 313 383 932 | f 312 442 446 448 933 | f 447 443 313 449 934 | f 442 444 446 935 | f 447 445 443 936 | f 416 450 452 476 937 | f 453 451 417 477 938 | f 450 432 462 452 939 | f 463 433 451 453 940 | f 432 434 460 462 941 | f 461 435 433 463 942 | f 434 436 458 460 943 | f 459 437 435 461 944 | f 436 438 456 458 945 | f 457 439 437 459 946 | f 438 440 454 456 947 | f 455 441 439 457 948 | f 440 418 474 454 949 | f 475 419 441 455 950 | f 428 416 476 464 951 | f 477 417 429 465 952 | f 426 428 464 466 953 | f 465 429 427 467 954 | f 424 426 466 468 955 | f 467 427 425 469 956 | f 422 424 468 470 957 | f 469 425 423 471 958 | f 420 422 470 472 959 | f 471 423 421 473 960 | f 418 420 472 474 961 | f 473 421 419 475 962 | f 458 456 480 478 963 | f 481 457 459 479 964 | f 478 480 482 484 965 | f 483 481 479 485 966 | f 484 482 488 486 967 | f 489 483 485 487 968 | f 486 488 490 492 969 | f 491 489 487 493 970 | f 464 476 486 492 971 | f 487 477 465 493 972 | f 452 484 486 476 973 | f 487 485 453 477 974 | f 452 462 478 484 975 | f 479 463 453 485 976 | f 458 478 462 460 977 | f 463 479 459 461 978 | f 454 474 480 456 979 | f 481 475 455 457 980 | f 472 482 480 474 981 | f 481 483 473 475 982 | f 470 488 482 472 983 | f 483 489 471 473 984 | f 468 490 488 470 985 | f 489 491 469 471 986 | f 466 492 490 468 987 | f 491 493 467 469 988 | f 464 492 466 989 | f 467 493 465 990 | f 392 390 504 502 991 | f 505 391 393 503 992 | f 394 392 502 500 993 | f 503 393 395 501 994 | f 396 394 500 498 995 | f 501 395 397 499 996 | f 398 396 498 496 997 | f 499 397 399 497 998 | f 400 398 496 494 999 | f 497 399 401 495 1000 | f 388 400 494 506 1001 | f 495 401 389 507 1002 | f 494 502 504 506 1003 | f 505 503 495 507 1004 | f 494 496 500 502 1005 | f 501 497 495 503 1006 | f 496 498 500 1007 | f 501 499 497 1008 | f 314 382 388 506 1009 | f 389 383 315 507 1010 | f 314 506 504 322 1011 | f 505 507 315 323 1012 | f 320 322 504 390 1013 | f 505 323 321 391 1014 | -------------------------------------------------------------------------------- /examples/suzanne.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self) 6 | self.wasd.look_at((0, 0, 3), (0, 0, 0)) 7 | self.context = pg.Context(pg.DirectionalLightProgram()) 8 | mesh1 = pg.OBJ('examples/suzanne.obj').center() 9 | mesh2 = mesh1.smooth_normals() 10 | self.meshes = [mesh1, mesh2] 11 | def update(self, t, dt): 12 | matrix = self.wasd.get_matrix() 13 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 14 | self.context.matrix = matrix 15 | self.context.camera_position = self.wasd.position 16 | def draw(self): 17 | self.clear() 18 | mesh = self.meshes[int(self.t % len(self.meshes))] 19 | mesh.draw(self.context) 20 | 21 | if __name__ == "__main__": 22 | pg.run(Window) 23 | -------------------------------------------------------------------------------- /examples/temp.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, radians, pi 2 | import pg 3 | 4 | class Window(pg.Window): 5 | def setup(self): 6 | self.wasd = pg.WASD(self, speed=3) 7 | self.wasd.look_at((-5, 4, 5), (0, 0, 0)) 8 | self.context = pg.Context(pg.DirectionalLightProgram()) 9 | data = [] 10 | points = pg.poisson_disc(-4, -4, 4, 4, 1, 32) 11 | for x, z in points: 12 | noise = pg.simplex2(10 + x * 0.25, 10 + z * 0.25, 4) 13 | y = (noise + 1) / 1 14 | shape = pg.Cone((x, 0, z), (x, y, z), 0.4, 36) 15 | data.extend(pg.interleave(shape.positions, shape.normals)) 16 | shape = pg.Sphere(3, 0.3, (x, y, z)) 17 | data.extend(pg.interleave(shape.positions, shape.normals)) 18 | self.context.position, self.context.normal = ( 19 | pg.VertexBuffer(data).slices(3, 3)) 20 | self.plane = pg.Context(pg.DirectionalLightProgram()) 21 | self.plane.object_color = (1, 1, 1) 22 | shape = pg.Plane((0, -0.1, 0), (0, 1, 0), 5) 23 | data = pg.interleave(shape.positions, shape.normals) 24 | self.plane.position, self.plane.normal = ( 25 | pg.VertexBuffer(data).slices(3, 3)) 26 | self.axes = pg.Context(pg.SolidColorProgram()) 27 | self.axes.color = (0.3, 0.3, 0.3) 28 | self.axes.position = pg.VertexBuffer(pg.Axes(100).positions) 29 | def update(self, t, dt): 30 | matrix = pg.Matrix()#.rotate((0, 1, 0), t * 2 * pi / 60) 31 | matrix = self.wasd.get_matrix(matrix) 32 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 33 | self.context.matrix = matrix 34 | self.plane.matrix = matrix 35 | self.axes.matrix = matrix 36 | self.context.camera_position = self.wasd.position 37 | self.plane.camera_position = self.wasd.position 38 | def draw(self): 39 | self.clear() 40 | self.plane.draw() 41 | self.context.draw() 42 | self.axes.draw(pg.gl.GL_LINES) 43 | 44 | if __name__ == "__main__": 45 | pg.run(Window) 46 | -------------------------------------------------------------------------------- /examples/terrain.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import colorsys 3 | import pg 4 | 5 | def noise(x, z): 6 | a = pg.simplex2(-x * 0.01, -z * 0.01, 4) 7 | b = pg.simplex2(x * 0.1, z * 0.1, 4) 8 | return (a + 1) * 16 + b / 10 9 | 10 | def generate_color(x, z): 11 | m = 0.005 12 | h = (pg.simplex2(x * m, z * m, 4) + 1) / 2 13 | s = (pg.simplex2(-x * m, z * m, 4) + 1) / 2 14 | v = (pg.simplex2(x * m, -z * m, 4) + 1) / 2 15 | v = v * 0.5 + 0.5 16 | return colorsys.hsv_to_rgb(h, s, v) 17 | 18 | class Window(pg.Window): 19 | def setup(self): 20 | self.wasd = pg.WASD(self, speed=30) 21 | self.wasd.look_at((-20, 20, -8), (0, 0, 0)) 22 | self.context = pg.Context(pg.DirectionalLightProgram()) 23 | self.context.use_color = True 24 | self.context.specular_power = 8.0 25 | self.context.specular_multiplier = 0.3 26 | normals = defaultdict(list) 27 | position = [] 28 | normal = [] 29 | color = [] 30 | size = 50 31 | # generate height map 32 | height = {} 33 | colors = {} 34 | for x in xrange(-size, size + 1): 35 | for z in xrange(-size, size + 1): 36 | height[(x, z)] = noise(x, z) 37 | colors[(x, z)] = generate_color(x, z) 38 | # generate triangles and track normals for all vertices 39 | for x in xrange(-size, size): 40 | for z in xrange(-size, size): 41 | t1 = [x + 0, z + 0, x + 1, z + 0, x + 0, z + 1] 42 | t2 = [x + 0, z + 1, x + 1, z + 0, x + 1, z + 1] 43 | for t in [t1, t2]: 44 | x1, z1, x2, z2, x3, z3 = t 45 | p1 = (x1, height[(x1, z1)], z1) 46 | p2 = (x2, height[(x2, z2)], z2) 47 | p3 = (x3, height[(x3, z3)], z3) 48 | c1 = colors[(x1, z1)] 49 | c2 = colors[(x2, z2)] 50 | c3 = colors[(x3, z3)] 51 | position.extend([p3, p2, p1]) 52 | color.extend([c3, c2, c1]) 53 | n = pg.normalize(pg.cross(pg.sub(p3, p1), pg.sub(p2, p1))) 54 | normals[(x1, z1)].append(n) 55 | normals[(x2, z2)].append(n) 56 | normals[(x3, z3)].append(n) 57 | # compute average normal for all vertices 58 | for key, value in normals.items(): 59 | normals[key] = pg.normalize(reduce(pg.add, value)) 60 | for x, y, z in position: 61 | normal.append(normals[(x, z)]) 62 | # generate vertex buffer 63 | vb = pg.VertexBuffer(pg.interleave(position, normal, color)) 64 | self.context.position, self.context.normal, self.context.color = ( 65 | vb.slices(3, 3, 3)) 66 | def update(self, t, dt): 67 | matrix = pg.Matrix() 68 | matrix = self.wasd.get_matrix(matrix) 69 | matrix = matrix.perspective(65, self.aspect, 0.1, 1000) 70 | self.context.matrix = matrix 71 | self.context.camera_position = self.wasd.position 72 | def draw(self): 73 | self.clear() 74 | self.context.draw() 75 | 76 | if __name__ == "__main__": 77 | pg.run(Window) 78 | -------------------------------------------------------------------------------- /examples/texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pg/124ea3803c788b2c98c4f3a428e5d26842a67b58/examples/texture.jpg -------------------------------------------------------------------------------- /examples/textured_sphere.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.font = pg.Font(self, 1, '/Library/Fonts/Arial.ttf', 24) 6 | self.wasd = pg.WASD(self) 7 | self.wasd.look_at(pg.normalize((1, 0, 1)), (0, 0, 0)) 8 | self.context = pg.Context(pg.DirectionalLightProgram()) 9 | self.context.sampler = pg.Texture(0, 'examples/earth.png') 10 | self.context.use_texture = True 11 | sphere = pg.Sphere(4, 0.5, (0, 0, 0)) 12 | self.context.position = pg.VertexBuffer(sphere.positions) 13 | self.context.normal = pg.VertexBuffer(sphere.normals) 14 | self.context.uv = pg.VertexBuffer(sphere.uvs) 15 | def update(self, t, dt): 16 | matrix = pg.Matrix() 17 | self.context.model_matrix = matrix 18 | normal_matrix = matrix.inverse().transpose() 19 | matrix = self.wasd.get_matrix(matrix) 20 | matrix = matrix.perspective(65, self.aspect, 0.01, 100) 21 | self.context.matrix = matrix 22 | self.context.normal_matrix = normal_matrix 23 | self.context.camera_position = self.wasd.position 24 | def draw(self): 25 | self.clear() 26 | self.context.draw() 27 | w, h = self.size 28 | self.font.render('%.1f fps' % self.fps, (w - 5, 0), (1, 0)) 29 | text = 'x=%.2f, y=%.2f, z=%.2f' % self.wasd.position 30 | self.font.render(text, (5, 0)) 31 | 32 | if __name__ == "__main__": 33 | pg.run(Window) 34 | -------------------------------------------------------------------------------- /examples/tutorial.py: -------------------------------------------------------------------------------- 1 | import pg 2 | 3 | class Window(pg.Window): 4 | def setup(self): 5 | self.wasd = pg.WASD(self) 6 | self.wasd.look_at((0, 0, 2), (0, 0, 0)) 7 | self.program = pg.DirectionalLightProgram() 8 | self.context = pg.Context(self.program) 9 | sphere = pg.Sphere(3, 0.5, (0, 0, 0)) 10 | self.context.position = pg.VertexBuffer(sphere.positions) 11 | self.context.normal = pg.VertexBuffer(sphere.normals) 12 | def update(self, t, dt): 13 | matrix = self.wasd.get_matrix() 14 | matrix = matrix.perspective(65, self.aspect, 0.1, 100) 15 | self.context.matrix = matrix 16 | self.context.camera_position = self.wasd.position 17 | def draw(self): 18 | self.clear() 19 | self.context.draw() 20 | 21 | if __name__ == "__main__": 22 | pg.run(Window) 23 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | import os 3 | import pg 4 | import sys 5 | 6 | def find_examples(): 7 | result = [] 8 | for filename in os.listdir('examples'): 9 | name, ext = os.path.splitext(filename) 10 | if ext == '.py' and name != '__init__': 11 | result.append(name) 12 | return result 13 | 14 | EXAMPLES = find_examples() 15 | 16 | def get_argument_example(): 17 | args = sys.argv[1:] 18 | if len(args) != 1: 19 | return None 20 | arg = args[0] 21 | if arg in EXAMPLES: 22 | return arg 23 | try: 24 | return EXAMPLES[int(arg) - 1] 25 | except Exception: 26 | return None 27 | 28 | def get_menu_example(): 29 | print 'Select an example to run:' 30 | for index, name in enumerate(EXAMPLES): 31 | print '%3d. %s' % (index + 1, name) 32 | try: 33 | return EXAMPLES[int(raw_input('> ')) - 1] 34 | except Exception: 35 | return None 36 | 37 | def generate_screenshots(size): 38 | try: 39 | os.mkdir('screenshots') 40 | except Exception: 41 | pass 42 | app = pg.App() 43 | for name in EXAMPLES: 44 | module = import_module('examples.%s' % name) 45 | window = module.Window(size) 46 | app.tick() 47 | app.tick() 48 | window.save_image('screenshots/%s.png' % name) 49 | window.close() 50 | app.run() 51 | 52 | def main(): 53 | name = get_argument_example() or get_menu_example() 54 | if name is None: 55 | return 56 | module = import_module('examples.%s' % name) 57 | if hasattr(module, 'main'): 58 | module.main() 59 | else: 60 | names = ['Window', 'Scene'] 61 | for name in names: 62 | if hasattr(module, name): 63 | pg.run(getattr(module, name)) 64 | return 65 | 66 | if __name__ == '__main__': 67 | # generate_screenshots((800, 600)) 68 | main() 69 | -------------------------------------------------------------------------------- /pg/__init__.py: -------------------------------------------------------------------------------- 1 | # use case: pg.GL_CONST or pg.glFunc() 2 | from .gl import * 3 | 4 | # use case: from pg.gl import * 5 | from . import gl 6 | 7 | from .camera import ( 8 | Camera, 9 | ) 10 | 11 | from .core import ( 12 | App, 13 | async, 14 | call_after, 15 | Context, 16 | delete_all, 17 | FragmentShader, 18 | index, 19 | IndexBuffer, 20 | Mesh, 21 | poll_events, 22 | Program, 23 | run, 24 | Scene, 25 | Shader, 26 | Texture, 27 | Ticker, 28 | VertexBuffer, 29 | VertexShader, 30 | Window, 31 | Worker, 32 | ) 33 | 34 | from .csg import ( 35 | Solid, 36 | ) 37 | 38 | from .font import ( 39 | Font, 40 | ) 41 | 42 | from .geometry import ( 43 | Axes, 44 | Cone, 45 | Crosshairs, 46 | Cuboid, 47 | Cylinder, 48 | CylinderAxes, 49 | Plane, 50 | Sphere, 51 | ) 52 | 53 | from .matrix import ( 54 | Matrix, 55 | ) 56 | 57 | from .noise import ( 58 | Noise, 59 | simplex2, 60 | ) 61 | 62 | from .obj import ( 63 | OBJ, 64 | ) 65 | 66 | from .poisson import ( 67 | poisson_disc, 68 | ) 69 | 70 | from .programs import ( 71 | BaseProgram, 72 | DirectionalLightProgram, 73 | SolidColorProgram, 74 | TextProgram, 75 | TextureProgram, 76 | ) 77 | 78 | from .sprite import ( 79 | Sprite, 80 | SpriteBatch, 81 | SpriteFrame, 82 | SpriteSheet, 83 | ) 84 | 85 | from .stl import ( 86 | STL, 87 | ) 88 | 89 | from .util import ( 90 | add, 91 | bounding_box, 92 | cross, 93 | distance, 94 | distinct, 95 | flatten, 96 | hex_color, 97 | interleave, 98 | interpolate, 99 | mul, 100 | neg, 101 | normal_from_points, 102 | normalize, 103 | pack_list, 104 | ray_triangle_intersection, 105 | recenter, 106 | smooth_normals, 107 | sub, 108 | ) 109 | 110 | from .wasd import ( 111 | WASD, 112 | ) 113 | 114 | from .glfw import ( 115 | RELEASE, 116 | PRESS, 117 | REPEAT, 118 | KEY_UNKNOWN, 119 | KEY_SPACE, 120 | KEY_APOSTROPHE, 121 | KEY_COMMA, 122 | KEY_MINUS, 123 | KEY_PERIOD, 124 | KEY_SLASH, 125 | KEY_0, 126 | KEY_1, 127 | KEY_2, 128 | KEY_3, 129 | KEY_4, 130 | KEY_5, 131 | KEY_6, 132 | KEY_7, 133 | KEY_8, 134 | KEY_9, 135 | KEY_SEMICOLON, 136 | KEY_EQUAL, 137 | KEY_A, 138 | KEY_B, 139 | KEY_C, 140 | KEY_D, 141 | KEY_E, 142 | KEY_F, 143 | KEY_G, 144 | KEY_H, 145 | KEY_I, 146 | KEY_J, 147 | KEY_K, 148 | KEY_L, 149 | KEY_M, 150 | KEY_N, 151 | KEY_O, 152 | KEY_P, 153 | KEY_Q, 154 | KEY_R, 155 | KEY_S, 156 | KEY_T, 157 | KEY_U, 158 | KEY_V, 159 | KEY_W, 160 | KEY_X, 161 | KEY_Y, 162 | KEY_Z, 163 | KEY_LEFT_BRACKET, 164 | KEY_BACKSLASH, 165 | KEY_RIGHT_BRACKET, 166 | KEY_GRAVE_ACCENT, 167 | KEY_WORLD_1, 168 | KEY_WORLD_2, 169 | KEY_ESCAPE, 170 | KEY_ENTER, 171 | KEY_TAB, 172 | KEY_BACKSPACE, 173 | KEY_INSERT, 174 | KEY_DELETE, 175 | KEY_RIGHT, 176 | KEY_LEFT, 177 | KEY_DOWN, 178 | KEY_UP, 179 | KEY_PAGE_UP, 180 | KEY_PAGE_DOWN, 181 | KEY_HOME, 182 | KEY_END, 183 | KEY_CAPS_LOCK, 184 | KEY_SCROLL_LOCK, 185 | KEY_NUM_LOCK, 186 | KEY_PRINT_SCREEN, 187 | KEY_PAUSE, 188 | KEY_F1, 189 | KEY_F2, 190 | KEY_F3, 191 | KEY_F4, 192 | KEY_F5, 193 | KEY_F6, 194 | KEY_F7, 195 | KEY_F8, 196 | KEY_F9, 197 | KEY_F10, 198 | KEY_F11, 199 | KEY_F12, 200 | KEY_F13, 201 | KEY_F14, 202 | KEY_F15, 203 | KEY_F16, 204 | KEY_F17, 205 | KEY_F18, 206 | KEY_F19, 207 | KEY_F20, 208 | KEY_F21, 209 | KEY_F22, 210 | KEY_F23, 211 | KEY_F24, 212 | KEY_F25, 213 | KEY_KP_0, 214 | KEY_KP_1, 215 | KEY_KP_2, 216 | KEY_KP_3, 217 | KEY_KP_4, 218 | KEY_KP_5, 219 | KEY_KP_6, 220 | KEY_KP_7, 221 | KEY_KP_8, 222 | KEY_KP_9, 223 | KEY_KP_DECIMAL, 224 | KEY_KP_DIVIDE, 225 | KEY_KP_MULTIPLY, 226 | KEY_KP_SUBTRACT, 227 | KEY_KP_ADD, 228 | KEY_KP_ENTER, 229 | KEY_KP_EQUAL, 230 | KEY_LEFT_SHIFT, 231 | KEY_LEFT_CONTROL, 232 | KEY_LEFT_ALT, 233 | KEY_LEFT_SUPER, 234 | KEY_RIGHT_SHIFT, 235 | KEY_RIGHT_CONTROL, 236 | KEY_RIGHT_ALT, 237 | KEY_RIGHT_SUPER, 238 | KEY_MENU, 239 | KEY_LAST, 240 | MOD_SHIFT, 241 | MOD_CONTROL, 242 | MOD_ALT, 243 | MOD_SUPER, 244 | MOUSE_BUTTON_1, 245 | MOUSE_BUTTON_2, 246 | MOUSE_BUTTON_3, 247 | MOUSE_BUTTON_4, 248 | MOUSE_BUTTON_5, 249 | MOUSE_BUTTON_6, 250 | MOUSE_BUTTON_7, 251 | MOUSE_BUTTON_8, 252 | MOUSE_BUTTON_LAST, 253 | MOUSE_BUTTON_LEFT, 254 | MOUSE_BUTTON_RIGHT, 255 | MOUSE_BUTTON_MIDDLE, 256 | JOYSTICK_1, 257 | JOYSTICK_2, 258 | JOYSTICK_3, 259 | JOYSTICK_4, 260 | JOYSTICK_5, 261 | JOYSTICK_6, 262 | JOYSTICK_7, 263 | JOYSTICK_8, 264 | JOYSTICK_9, 265 | JOYSTICK_10, 266 | JOYSTICK_11, 267 | JOYSTICK_12, 268 | JOYSTICK_13, 269 | JOYSTICK_14, 270 | JOYSTICK_15, 271 | JOYSTICK_16, 272 | JOYSTICK_LAST, 273 | NOT_INITIALIZED, 274 | NO_CURRENT_CONTEXT, 275 | INVALID_ENUM, 276 | INVALID_VALUE, 277 | OUT_OF_MEMORY, 278 | API_UNAVAILABLE, 279 | VERSION_UNAVAILABLE, 280 | PLATFORM_ERROR, 281 | FORMAT_UNAVAILABLE, 282 | FOCUSED, 283 | ICONIFIED, 284 | RESIZABLE, 285 | VISIBLE, 286 | DECORATED, 287 | RED_BITS, 288 | GREEN_BITS, 289 | BLUE_BITS, 290 | ALPHA_BITS, 291 | DEPTH_BITS, 292 | STENCIL_BITS, 293 | ACCUM_RED_BITS, 294 | ACCUM_GREEN_BITS, 295 | ACCUM_BLUE_BITS, 296 | ACCUM_ALPHA_BITS, 297 | AUX_BUFFERS, 298 | STEREO, 299 | SAMPLES, 300 | SRGB_CAPABLE, 301 | REFRESH_RATE, 302 | CLIENT_API, 303 | CONTEXT_VERSION_MAJOR, 304 | CONTEXT_VERSION_MINOR, 305 | CONTEXT_REVISION, 306 | CONTEXT_ROBUSTNESS, 307 | OPENGL_FORWARD_COMPAT, 308 | OPENGL_DEBUG_CONTEXT, 309 | OPENGL_PROFILE, 310 | OPENGL_API, 311 | OPENGL_ES_API, 312 | NO_ROBUSTNESS, 313 | NO_RESET_NOTIFICATION, 314 | LOSE_CONTEXT_ON_RESET, 315 | OPENGL_ANY_PROFILE, 316 | OPENGL_CORE_PROFILE, 317 | OPENGL_COMPAT_PROFILE, 318 | CURSOR, 319 | STICKY_KEYS, 320 | STICKY_MOUSE_BUTTONS, 321 | CURSOR_NORMAL, 322 | CURSOR_HIDDEN, 323 | CURSOR_DISABLED, 324 | CONNECTED, 325 | DISCONNECTED, 326 | ) 327 | 328 | __all__ = dir() 329 | -------------------------------------------------------------------------------- /pg/camera.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi, atan2, asin 2 | from .matrix import Matrix 3 | from .util import normalize 4 | 5 | class Camera(object): 6 | def __init__(self): 7 | self.x = 0 8 | self.y = 0 9 | self.z = 0 10 | self.rx = 0 11 | self.ry = 0 12 | @property 13 | def position(self): 14 | return (self.x, self.y, self.z) 15 | def look_at(self, position, target): 16 | px, py, pz = position 17 | tx, ty, tz = target 18 | dx, dy, dz = normalize((tx - px, ty - py, tz - pz)) 19 | self.x = px 20 | self.y = py 21 | self.z = pz 22 | self.rx = 2 * pi - (atan2(dx, dz) + pi) 23 | self.ry = asin(dy) 24 | def get_matrix(self, matrix=None, translate=True): 25 | matrix = matrix or Matrix() 26 | if translate: 27 | matrix = matrix.translate((-self.x, -self.y, -self.z)) 28 | matrix = matrix.rotate((cos(self.rx), 0, sin(self.rx)), self.ry) 29 | matrix = matrix.rotate((0, 1, 0), -self.rx) 30 | return matrix 31 | -------------------------------------------------------------------------------- /pg/csg.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from .core import Mesh 3 | import random 4 | 5 | class Vector(object): 6 | def __init__(self, x, y, z): 7 | self.x = x 8 | self.y = y 9 | self.z = z 10 | def get_tuple(self): 11 | return (self.x, self.y, self.z) 12 | def clone(self): 13 | return Vector(self.x, self.y, self.z) 14 | def negate(self): 15 | return Vector(-self.x, -self.y, -self.z) 16 | def add(self, a): 17 | return Vector(self.x + a.x, self.y + a.y, self.z + a.z) 18 | def subtract(self, a): 19 | return Vector(self.x - a.x, self.y - a.y, self.z - a.z) 20 | def multiply(self, a): 21 | return Vector(self.x * a, self.y * a, self.z * a) 22 | def divide(self, a): 23 | return Vector(self.x / a, self.y / a, self.z / a) 24 | def length(self): 25 | return self.dot(self) ** 0.5 26 | def dot(self, a): 27 | return self.x * a.x + self.y * a.y + self.z * a.z 28 | def cross(self, a): 29 | return Vector( 30 | self.y * a.z - self.z * a.y, 31 | self.z * a.x - self.x * a.z, 32 | self.x * a.y - self.y * a.x) 33 | def normalize(self): 34 | return self.divide(self.length()); 35 | def interpolate(self, a, t): 36 | return self.add(a.subtract(self).multiply(t)) 37 | 38 | class Vertex(object): 39 | def __init__(self, position, normal, uv): 40 | self.position = position 41 | self.normal = normal 42 | self.uv = uv 43 | def clone(self): 44 | return Vertex( 45 | self.position.clone(), self.normal.clone(), self.uv.clone()) 46 | def flip(self): 47 | self.normal = self.normal.negate() 48 | def interpolate(self, a, t): 49 | return Vertex( 50 | self.position.interpolate(a.position, t), 51 | self.normal.interpolate(a.normal, t), 52 | self.uv.interpolate(a.uv, t)) 53 | 54 | class Plane(object): 55 | @staticmethod 56 | def from_points(a, b, c): 57 | normal = b.subtract(a).cross(c.subtract(a)).normalize() 58 | return Plane(normal, normal.dot(a)) 59 | def __init__(self, normal, w): 60 | self.normal = normal 61 | self.w = w 62 | def clone(self): 63 | return Plane(self.normal.clone(), self.w) 64 | def flip(self): 65 | self.normal = self.normal.negate() 66 | self.w = -self.w 67 | def split(self, polygon, co_front, co_back, front, back): 68 | COPLANAR = 0 69 | FRONT = 1 70 | BACK = 2 71 | BOTH = 3 72 | EPS = 1e-5 73 | polygon_type = 0 74 | vertex_types = [] 75 | for vertex in polygon.vertices: 76 | w = self.normal.dot(vertex.position) - self.w 77 | t = COPLANAR 78 | if w < -EPS: 79 | t = BACK 80 | if w > EPS: 81 | t = FRONT 82 | polygon_type |= t 83 | vertex_types.append(t) 84 | if polygon_type == COPLANAR: 85 | if self.normal.dot(polygon.plane.normal) > 0: 86 | co_front.append(polygon) 87 | else: 88 | co_back.append(polygon) 89 | elif polygon_type == FRONT: 90 | front.append(polygon) 91 | elif polygon_type == BACK: 92 | back.append(polygon) 93 | else: 94 | f = [] 95 | b = [] 96 | for i in xrange(len(polygon.vertices)): 97 | j = (i + 1) % len(polygon.vertices) 98 | v1 = polygon.vertices[i] 99 | v2 = polygon.vertices[j] 100 | t1 = vertex_types[i] 101 | t2 = vertex_types[j] 102 | if t1 != BACK: 103 | f.append(v1) 104 | if t1 != FRONT: 105 | if t1 != BACK: 106 | b.append(v1.clone()) 107 | else: 108 | b.append(v1) 109 | if (t1 | t2) == BOTH: 110 | n = self.w - self.normal.dot(v1.position) 111 | d = self.normal.dot(v2.position.subtract(v1.position)) 112 | v = v1.interpolate(v2, n / d) 113 | f.append(v) 114 | b.append(v.clone()) 115 | if len(f) >= 3: 116 | front.append(Polygon(f, polygon.shared)) 117 | if len(b) >= 3: 118 | back.append(Polygon(b, polygon.shared)) 119 | 120 | class Polygon(object): 121 | def __init__(self, vertices, shared): 122 | self.vertices = vertices 123 | self.shared = shared 124 | self.plane = Plane.from_points(*[a.position for a in vertices[:3]]) 125 | def clone(self): 126 | vertices = [a.clone() for a in self.vertices] 127 | return Polygon(vertices, self.shared) 128 | def flip(self): 129 | self.vertices.reverse() 130 | for vertex in self.vertices: 131 | vertex.flip() 132 | self.plane.flip() 133 | 134 | class Node(object): 135 | def __init__(self, polygons=None): 136 | self.plane = None 137 | self.front = None 138 | self.back = None 139 | self.polygons = [] 140 | if polygons: 141 | self.build(polygons) 142 | def clone(self): 143 | node = Node() 144 | if self.plane: 145 | node.plane = self.plane.clone() 146 | if self.front: 147 | node.front = self.front.clone() 148 | if self.back: 149 | node.back = self.back.clone() 150 | node.polygons = [a.clone() for a in self.polygons] 151 | return node 152 | def invert(self): 153 | for polygon in self.polygons: 154 | polygon.flip() 155 | self.plane.flip() 156 | if self.front: 157 | self.front.invert() 158 | if self.back: 159 | self.back.invert() 160 | self.front, self.back = self.back, self.front 161 | def clip_polygons(self, polygons): 162 | if not self.plane: 163 | return list(polygons) 164 | front = [] 165 | back = [] 166 | for polygon in polygons: 167 | self.plane.split(polygon, front, back, front, back) 168 | if self.front: 169 | front = self.front.clip_polygons(front) 170 | if self.back: 171 | back = self.back.clip_polygons(back) 172 | else: 173 | back = [] 174 | return front + back 175 | def clip_to(self, node): 176 | self.polygons = node.clip_polygons(self.polygons) 177 | if self.front: 178 | self.front.clip_to(node) 179 | if self.back: 180 | self.back.clip_to(node) 181 | def get_polygons(self): 182 | result = list(self.polygons) 183 | if self.front: 184 | result.extend(self.front.get_polygons()) 185 | if self.back: 186 | result.extend(self.back.get_polygons()) 187 | return result 188 | def build(self, polygons): 189 | if not polygons: 190 | return 191 | self.plane = self.plane or random.choice(polygons).plane.clone() 192 | front = [] 193 | back = [] 194 | for polygon in polygons: 195 | self.plane.split(polygon, self.polygons, self.polygons, front, back) 196 | if front: 197 | self.front = self.front or Node() 198 | self.front.build(front) 199 | if back: 200 | self.back = self.back or Node() 201 | self.back.build(back) 202 | 203 | class Model(object): 204 | def __init__(self, polygons=None): 205 | self.polygons = polygons or [] 206 | def clone(self): 207 | return Model([a.clone() for a in self.polygons]) 208 | def get_polygons(self): 209 | return self.polygons 210 | def __or__(self, other): 211 | return self.union(other) 212 | def __and__(self, other): 213 | return self.intersection(other) 214 | def __sub__(self, other): 215 | return self.difference(other) 216 | def __invert__(self): 217 | return self.inverse() 218 | def union(self, other): 219 | a = Node(self.clone().polygons) 220 | b = Node(other.clone().polygons) 221 | a.clip_to(b) 222 | b.clip_to(a) 223 | b.invert() 224 | b.clip_to(a) 225 | b.invert() 226 | a.build(b.get_polygons()) 227 | return Model(a.get_polygons()) 228 | def difference(self, other): 229 | a = Node(self.clone().polygons) 230 | b = Node(other.clone().polygons) 231 | a.invert() 232 | a.clip_to(b) 233 | b.clip_to(a) 234 | b.invert() 235 | b.clip_to(a) 236 | b.invert() 237 | a.build(b.get_polygons()) 238 | a.invert() 239 | return Model(a.get_polygons()) 240 | def intersection(self, other): 241 | a = Node(self.clone().polygons) 242 | b = Node(other.clone().polygons) 243 | a.invert() 244 | b.clip_to(a) 245 | b.invert() 246 | a.clip_to(b) 247 | b.clip_to(a) 248 | a.build(b.get_polygons()) 249 | a.invert() 250 | return Model(a.get_polygons()) 251 | def inverse(self): 252 | polygons = [a.clone() for a in self.polygons] 253 | for polygon in polygons: 254 | polygon.flip() 255 | return Model(polygons) 256 | def mesh(self): 257 | positions = [] 258 | normals = [] 259 | uvs = [] 260 | for polygon in self.get_polygons(): 261 | for i in xrange(2, len(polygon.vertices)): 262 | a = polygon.vertices[0] 263 | b = polygon.vertices[i - 1] 264 | c = polygon.vertices[i] 265 | positions.append(a.position.get_tuple()) 266 | positions.append(b.position.get_tuple()) 267 | positions.append(c.position.get_tuple()) 268 | normals.append(a.normal.get_tuple()) 269 | normals.append(b.normal.get_tuple()) 270 | normals.append(c.normal.get_tuple()) 271 | uvs.append(a.uv.get_tuple()[:2]) 272 | uvs.append(b.uv.get_tuple()[:2]) 273 | uvs.append(c.uv.get_tuple()[:2]) 274 | return Mesh(positions, normals, uvs) 275 | 276 | class Solid(Model): 277 | def __init__(self, shape): 278 | polygons = [] 279 | for i in xrange(0, len(shape.positions), 3): 280 | positions = shape.positions[i:i+3] 281 | normals = shape.normals[i:i+3] 282 | uvs = shape.uvs[i:i+3] 283 | vertices = [Vertex(Vector(*a), Vector(*b), Vector(c[0], c[1], 0)) 284 | for a, b, c in zip(positions, normals, uvs)] 285 | polygon = Polygon(vertices, None) 286 | polygons.append(polygon) 287 | super(Solid, self).__init__(polygons) 288 | -------------------------------------------------------------------------------- /pg/font.py: -------------------------------------------------------------------------------- 1 | from OpenGL.GL import * 2 | from itertools import product 3 | from math import ceil, log 4 | from PIL import Image, ImageDraw, ImageFont 5 | from .core import Context, Texture, VertexBuffer, Scene 6 | from .matrix import Matrix 7 | from .programs import TextProgram 8 | from .util import interleave 9 | 10 | def float_to_byte_color(color): 11 | return tuple(int(round(x * 255)) for x in color) 12 | 13 | class Font(object): 14 | def __init__(self, scene_or_window, unit, name, size, fg=None, bg=None): 15 | window = scene_or_window 16 | if isinstance(scene_or_window, Scene): 17 | window = scene_or_window.window 18 | self.fg = float_to_byte_color(fg or (1.0, 1.0, 1.0, 1.0)) 19 | self.bg = float_to_byte_color(bg or (0.0, 0.0, 0.0, 0.0)) 20 | if len(self.fg) == 3: 21 | self.fg += (255,) 22 | if len(self.bg) == 3: 23 | self.bg += (255,) 24 | self.window = window 25 | self.kerning = {} 26 | self.load(name, size) 27 | self.context = Context(TextProgram()) 28 | self.context.sampler = Texture(unit, self.im) 29 | def render(self, text, coord=(0, 0), anchor=(0, 0)): 30 | size, positions, uvs = self.generate_vertex_data(text) 31 | ww, wh = self.window.size 32 | tx, ty = coord 33 | ax, ay = anchor 34 | tw, th = size 35 | matrix = Matrix() 36 | matrix = matrix.translate((tx - tw * ax, ty - th * ay, 0)) 37 | matrix = matrix.orthographic(0, ww, wh, 0, -1, 1) 38 | self.context.matrix = matrix 39 | vertex_buffer = VertexBuffer(interleave(positions, uvs)) 40 | self.context.position, self.context.uv = vertex_buffer.slices(2, 2) 41 | glEnable(GL_BLEND) 42 | glDisable(GL_DEPTH_TEST) 43 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 44 | self.context.draw(GL_TRIANGLES) 45 | glEnable(GL_DEPTH_TEST) 46 | glDisable(GL_BLEND) 47 | vertex_buffer.delete() 48 | def generate_vertex_data(self, text): 49 | positions = [] 50 | uvs = [] 51 | data = [ 52 | (0, 0), (0, 1), (1, 0), 53 | (0, 1), (1, 1), (1, 0), 54 | ] 55 | x = y = 0 56 | previous = None 57 | for c in text: 58 | if c not in self.sizes: 59 | c = ' ' 60 | index = ord(c) - 32 61 | row = index / 10 62 | col = index % 10 63 | u = self.du * col 64 | v = self.dv * row 65 | sx, sy = self.sizes[c] 66 | ox, oy = self.offsets[c] 67 | k = self.get_kerning(previous, c) if previous else 0 68 | x += k 69 | for i, j in data: 70 | cx = x + i * self.dx + ox 71 | cy = y + j * self.dy + oy 72 | positions.append((cx, cy)) 73 | uvs.append((u + i * self.du, 1 - v - j * self.dv)) 74 | x += ox + sx 75 | previous = c 76 | size = (x, self.dy) 77 | return size, positions, uvs 78 | def get_kerning(self, c1, c2): 79 | key = c1 + c2 80 | if key not in self.kerning: 81 | a = self.sizes[c1][0] + self.sizes[c2][0] 82 | b = self.font.getsize(key)[0] 83 | self.kerning[key] = b - a 84 | return self.kerning[key] 85 | def load(self, name, size): 86 | font = ImageFont.truetype(name, size) 87 | chars = [chr(x) for x in range(32, 127)] 88 | sizes = dict((c, font.getsize(c)) for c in chars) 89 | offsets = dict((c, font.getoffset(c)) for c in chars) 90 | mw = max(sizes[c][0] for c in chars) + 1 91 | mh = max(sizes[c][1] for c in chars) + 1 92 | rows = 10 93 | cols = 10 94 | w = mw * cols 95 | h = mh * rows 96 | w = int(2 ** ceil(log(w) / log(2))) 97 | h = int(2 ** ceil(log(h) / log(2))) 98 | im = Image.new('RGBA', (w, h), self.bg) 99 | draw = ImageDraw.Draw(im) 100 | for (row, col), c in zip(product(range(rows), range(cols)), chars): 101 | x = col * mw 102 | y = row * mh 103 | dx, dy = offsets[c] 104 | draw.text((x + 1 - dx, y + 1 - dy), c, self.fg, font) 105 | self.dx = mw 106 | self.dy = mh 107 | self.du = float(mw) / w 108 | self.dv = float(mh) / h 109 | self.sizes = sizes 110 | self.offsets = offsets 111 | self.im = im 112 | self.font = font 113 | -------------------------------------------------------------------------------- /pg/geometry.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from math import asin, pi, atan2, hypot, sin, cos 4 | from .core import Mesh 5 | from .matrix import Matrix 6 | from .util import distance, normalize 7 | 8 | class Sphere(Mesh): 9 | def __init__(self, detail, radius=0.5, center=(0, 0, 0)): 10 | super(Sphere, self).__init__() 11 | self.detail = detail 12 | self.radius = radius 13 | self.center = center 14 | self.setup() 15 | def setup(self): 16 | indices = [ 17 | (0, 3, 4), (0, 4, 1), (5, 4, 3), (5, 1, 4), 18 | (2, 3, 0), (1, 2, 0), (3, 2, 5), (2, 1, 5), 19 | ] 20 | positions = [ 21 | (0, 0, -1), (1, 0, 0), (0, -1, 0), 22 | (-1, 0, 0), (0, 1, 0), (0, 0, 1), 23 | ] 24 | for a, b, c in indices: 25 | position = (positions[a], positions[b], positions[c]) 26 | self._setup(self.detail, position) 27 | def _setup(self, detail, position): 28 | a, b, c = position 29 | r = self.radius 30 | p = self.center 31 | if detail == 0: 32 | self.normals.append(a) 33 | self.normals.append(b) 34 | self.normals.append(c) 35 | self.positions.append(tuple(r * a[i] + p[i] for i in xrange(3))) 36 | self.positions.append(tuple(r * b[i] + p[i] for i in xrange(3))) 37 | self.positions.append(tuple(r * c[i] + p[i] for i in xrange(3))) 38 | ta = [0.5 + atan2(a[0], a[2]) / (2 * pi), 0.5 + asin(a[1]) / pi] 39 | tb = [0.5 + atan2(b[0], b[2]) / (2 * pi), 0.5 + asin(b[1]) / pi] 40 | tc = [0.5 + atan2(c[0], c[2]) / (2 * pi), 0.5 + asin(c[1]) / pi] 41 | ab = abs(ta[0] - tb[0]) 42 | ac = abs(ta[0] - tc[0]) 43 | bc = abs(tb[0] - tc[0]) 44 | if any(x > 0.5 for x in [ab, ac, bc]): 45 | ta[0] = (ta[0] + 1) % 1.0 46 | tb[0] = (tb[0] + 1) % 1.0 47 | tc[0] = (tc[0] + 1) % 1.0 48 | self.uvs.append(tuple(ta)) 49 | self.uvs.append(tuple(tb)) 50 | self.uvs.append(tuple(tc)) 51 | else: 52 | ab = normalize([(a[i] + b[i]) / 2.0 for i in xrange(3)]) 53 | ac = normalize([(a[i] + c[i]) / 2.0 for i in xrange(3)]) 54 | bc = normalize([(b[i] + c[i]) / 2.0 for i in xrange(3)]) 55 | self._setup(detail - 1, (a, ab, ac)) 56 | self._setup(detail - 1, (b, bc, ab)) 57 | self._setup(detail - 1, (c, ac, bc)) 58 | self._setup(detail - 1, (ab, bc, ac)) 59 | 60 | class Cone(Mesh): 61 | def __init__(self, p1, p2, radius, detail): 62 | super(Cone, self).__init__() 63 | self.setup(p1, p2, radius, detail) 64 | def setup(self, p1, p2, radius, detail): 65 | x1, y1, z1 = p1 66 | x2, y2, z2 = p2 67 | dx, dy, dz = x2 - x1, y2 - y1, z2 - z1 68 | cx, cy, cz = x1 + dx / 2, y1 + dy / 2, z1 + dz / 2 69 | a = atan2(dz, dx) - pi / 2 70 | b = atan2(dy, hypot(dx, dz)) - pi / 2 71 | matrix = Matrix() 72 | matrix = matrix.rotate((cos(a), 0, sin(a)), b) 73 | normal_matrix = matrix 74 | matrix = matrix.translate((cx, cy, cz)) 75 | d = distance(p1, p2) 76 | y = -sin(pi / 2 - atan2(d, radius)) * radius 77 | angles = [i * 2 * pi / detail for i in xrange(detail + 1)] 78 | for a1, a2 in zip(angles, angles[1:]): 79 | x1, z1 = cos(a1) * radius, sin(a1) * radius 80 | x2, z2 = cos(a2) * radius, sin(a2) * radius 81 | y1, y2 = -d / 2, d / 2 82 | n1, n2 = normalize((x1, y, z1)), normalize((x2, y, z2)) 83 | uv1 = (0.5 + cos(a1) * 0.5, 0.5 + sin(a1) * 0.5) 84 | uv2 = (0.5 + cos(a2) * 0.5, 0.5 + sin(a2) * 0.5) 85 | u1 = a1 % (2 * pi) 86 | u2 = a2 % (2 * pi) 87 | if u2 < u1: 88 | u2 += 2 * pi 89 | positions = [ 90 | (0, y2, 0), (x2, y2, z2), (x1, y2, z1), 91 | (0, y1, 0), (x1, y2, z1), (x2, y2, z2), 92 | ] 93 | normals = [ 94 | (0, 1, 0), (0, 1, 0), (0, 1, 0), 95 | (0, -1, 0), n1, n2, 96 | ] 97 | uvs = [ 98 | (0.5, 0.5), uv1, uv2, 99 | (0.5, 0.5), uv2, uv1, 100 | ] 101 | for position in positions: 102 | self.positions.append(matrix * position) 103 | for normal in normals: 104 | self.normals.append(normal_matrix * normal) 105 | for uv in uvs: 106 | self.uvs.append(uv) 107 | 108 | class Cylinder(Mesh): 109 | def __init__(self, p1, p2, radius, detail, hollow=False): 110 | super(Cylinder, self).__init__() 111 | self.setup(p1, p2, radius, detail, hollow) 112 | def setup(self, p1, p2, radius, detail, hollow): 113 | x1, y1, z1 = p1 114 | x2, y2, z2 = p2 115 | dx, dy, dz = x2 - x1, y2 - y1, z2 - z1 116 | cx, cy, cz = x1 + dx / 2, y1 + dy / 2, z1 + dz / 2 117 | a = atan2(dz, dx) - pi / 2 118 | b = atan2(dy, hypot(dx, dz)) - pi / 2 119 | matrix = Matrix() 120 | matrix = matrix.rotate((cos(a), 0, sin(a)), b) 121 | normal_matrix = matrix 122 | matrix = matrix.translate((cx, cy, cz)) 123 | d = distance(p1, p2) 124 | angles = [i * 2 * pi / detail for i in xrange(detail + 1)] 125 | for a1, a2 in zip(angles, angles[1:]): 126 | x1, z1 = cos(a1) * radius, sin(a1) * radius 127 | x2, z2 = cos(a2) * radius, sin(a2) * radius 128 | y1, y2 = -d / 2, d / 2 129 | n1, n2 = normalize((x1, 0, z1)), normalize((x2, 0, z2)) 130 | uv1 = (0.5 + cos(a1) * 0.5, 0.5 + sin(a1) * 0.5) 131 | uv2 = (0.5 + cos(a2) * 0.5, 0.5 + sin(a2) * 0.5) 132 | u1 = a1 % (2 * pi) 133 | u2 = a2 % (2 * pi) 134 | if u2 < u1: 135 | u2 += 2 * pi 136 | positions = [ 137 | (0, y1, 0), (x1, y1, z1), (x2, y1, z2), 138 | (0, y2, 0), (x2, y2, z2), (x1, y2, z1), 139 | (x1, y1, z1), (x1, y2, z1), (x2, y1, z2), 140 | (x2, y1, z2), (x1, y2, z1), (x2, y2, z2), 141 | ] 142 | normals = [ 143 | (0, -1, 0), (0, -1, 0), (0, -1, 0), 144 | (0, 1, 0), (0, 1, 0), (0, 1, 0), 145 | n1, n1, n2, 146 | n2, n1, n2, 147 | ] 148 | uvs = [ 149 | (0.5, 0.5), uv1, uv2, 150 | (0.5, 0.5), uv2, uv1, 151 | (u1, 0), (u1, 1), (u2, 0), 152 | (u2, 0), (u1, 1), (u2, 1), 153 | ] 154 | if hollow: 155 | positions = positions[6:] 156 | normals = normals[6:] 157 | uvs = uvs[6:] 158 | for position in positions: 159 | self.positions.append(matrix * position) 160 | for normal in normals: 161 | self.normals.append(normal_matrix * normal) 162 | for uv in uvs: 163 | self.uvs.append(uv) 164 | 165 | class Cuboid(Mesh): 166 | def __init__(self, x1, x2, y1, y2, z1, z2): 167 | super(Cuboid, self).__init__() 168 | self.setup(x1, x2, y1, y2, z1, z2) 169 | def setup(self, x1, x2, y1, y2, z1, z2): 170 | positions = [ 171 | ((x1, y1, z1), (x1, y1, z2), (x1, y2, z1), (x1, y2, z2)), 172 | ((x2, y1, z1), (x2, y1, z2), (x2, y2, z1), (x2, y2, z2)), 173 | ((x1, y2, z1), (x1, y2, z2), (x2, y2, z1), (x2, y2, z2)), 174 | ((x1, y1, z1), (x1, y1, z2), (x2, y1, z1), (x2, y1, z2)), 175 | ((x1, y1, z1), (x1, y2, z1), (x2, y1, z1), (x2, y2, z1)), 176 | ((x1, y1, z2), (x1, y2, z2), (x2, y1, z2), (x2, y2, z2)), 177 | ] 178 | normals = [ 179 | (-1, 0, 0), 180 | (1, 0, 0), 181 | (0, 1, 0), 182 | (0, -1, 0), 183 | (0, 0, -1), 184 | (0, 0, 1), 185 | ] 186 | uvs = [ 187 | ((0, 0), (1, 0), (0, 1), (1, 1)), 188 | ((1, 0), (0, 0), (1, 1), (0, 1)), 189 | ((0, 1), (0, 0), (1, 1), (1, 0)), 190 | ((0, 0), (0, 1), (1, 0), (1, 1)), 191 | ((0, 0), (0, 1), (1, 0), (1, 1)), 192 | ((1, 0), (1, 1), (0, 0), (0, 1)), 193 | ] 194 | indices = [ 195 | (0, 3, 2, 0, 1, 3), 196 | (0, 3, 1, 0, 2, 3), 197 | (0, 3, 2, 0, 1, 3), 198 | (0, 3, 1, 0, 2, 3), 199 | (0, 3, 2, 0, 1, 3), 200 | (0, 3, 1, 0, 2, 3), 201 | ] 202 | for i in xrange(6): 203 | for v in xrange(6): 204 | j = indices[i][v] 205 | self.positions.append(positions[i][j]) 206 | self.normals.append(normals[i]) 207 | self.uvs.append(uvs[i][j]) 208 | 209 | class Plane(Mesh): 210 | def __init__(self, point, normal, size=0.5, both=True): 211 | super(Plane, self).__init__() 212 | nx, ny, nz = normal 213 | self.setup(point, (nx, ny, nz), size) 214 | if both: 215 | self.setup(point, (-nx, -ny, -nz), size) 216 | def setup(self, point, normal, size): 217 | try: 218 | w, h = size 219 | except Exception: 220 | w = h = size 221 | positions = [ 222 | (-w, 0, -h), (w, 0, -h), (-w, 0, h), 223 | (-w, 0, h), (w, 0, -h), (w, 0, h) 224 | ] 225 | uvs = [ 226 | (0, 0), (1, 0), (0, 1), 227 | (0, 1), (1, 0), (1, 1) 228 | ] 229 | normal = normalize(normal) 230 | nx, ny, nz = normal 231 | a = atan2(nz, nx) + pi 232 | b = atan2(ny, hypot(nx, nz)) + pi / 2 233 | rx, rz = cos(a + pi / 2), sin(a + pi / 2) 234 | matrix = Matrix() 235 | matrix = matrix.rotate((0, 1, 0), a) 236 | matrix = matrix.rotate((rx, 0, rz), b) 237 | matrix = matrix.translate(point) 238 | for position in positions: 239 | self.positions.append(matrix * position) 240 | self.normals.extend([normal] * 6) 241 | for uv in uvs: 242 | self.uvs.append(uv) 243 | 244 | class Axes(Mesh): 245 | def __init__(self, size=1): 246 | super(Axes, self).__init__() 247 | n = size 248 | self.positions = [ 249 | (0, 0, 0), (n, 0, 0), 250 | (0, 0, 0), (0, n, 0), 251 | (0, 0, 0), (0, 0, n), 252 | (0, 0, 0), (-n, 0, 0), 253 | (0, 0, 0), (0, -n, 0), 254 | (0, 0, 0), (0, 0, -n), 255 | ] 256 | 257 | class CylinderAxes(Mesh): 258 | def __init__(self, size=1, radius=0.0625, detail=12): 259 | super(CylinderAxes, self).__init__() 260 | n = size 261 | cylinders = [ 262 | Cylinder((-n, 0, 0), (n, 0, 0), radius, detail), 263 | Cylinder((0, -n, 0), (0, n, 0), radius, detail), 264 | Cylinder((0, 0, -n), (0, 0, n), radius, detail), 265 | ] 266 | for cylinder in cylinders: 267 | self.positions.extend(cylinder.positions) 268 | self.normals.extend(cylinder.normals) 269 | self.uvs.extend(cylinder.uvs) 270 | 271 | class Crosshairs(Mesh): 272 | def __init__(self, size=10): 273 | super(Crosshairs, self).__init__() 274 | n = size 275 | self.positions = [ 276 | (0, -n), (0, n), 277 | (-n, 0), (n, 0), 278 | ] 279 | -------------------------------------------------------------------------------- /pg/gl.py: -------------------------------------------------------------------------------- 1 | from OpenGL.GL import * 2 | -------------------------------------------------------------------------------- /pg/matrix.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from math import sin, cos, tan, pi 4 | from .util import normalize 5 | 6 | class Matrix(object): 7 | def __init__(self, value=None): 8 | if value is None: 9 | value = [ 10 | 1, 0, 0, 0, 11 | 0, 1, 0, 0, 12 | 0, 0, 1, 0, 13 | 0, 0, 0, 1, 14 | ] 15 | self.value = map(float, value) 16 | def get_uniform_value(self): 17 | return self.value 18 | def __repr__(self): 19 | result = [] 20 | for r in xrange(4): 21 | x = ','.join('% .3f' % self.value[c * 4 + r] for c in xrange(4)) 22 | result.append('[%s]' % x) 23 | return '\n'.join(result) 24 | def __mul__(self, other): 25 | if isinstance(other, Matrix): 26 | return self.matrix_multiply(other) 27 | elif isinstance(other, (tuple, list)): 28 | if len(other) == 2: 29 | return self.vector2_multiply(other) 30 | elif len(other) == 3: 31 | return self.vector3_multiply(other) 32 | elif len(other) == 4: 33 | return self.vector4_multiply(other) 34 | else: 35 | return NotImplemented 36 | else: 37 | return NotImplemented 38 | # def __getitem__(self, index): 39 | # return self.value[self.index(index)] 40 | # def __setitem__(self, index, value): 41 | # self.value[self.index(index)] = value 42 | # def index(self, index): 43 | # try: 44 | # row, col = index 45 | # return col * 4 + row 46 | # except Exception: 47 | # return index 48 | def matrix_multiply(self, other): 49 | ( 50 | a00, a10, a20, a30, a01, a11, a21, a31, 51 | a02, a12, a22, a32, a03, a13, a23, a33, 52 | ) = self.value 53 | ( 54 | b00, b10, b20, b30, b01, b11, b21, b31, 55 | b02, b12, b22, b32, b03, b13, b23, b33, 56 | ) = other.value 57 | c00 = a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30 58 | c10 = a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30 59 | c20 = a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30 60 | c30 = a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30 61 | c01 = a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31 62 | c11 = a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31 63 | c21 = a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31 64 | c31 = a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31 65 | c02 = a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32 66 | c12 = a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32 67 | c22 = a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32 68 | c32 = a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32 69 | c03 = a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33 70 | c13 = a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33 71 | c23 = a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33 72 | c33 = a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33 73 | return Matrix([ 74 | c00, c10, c20, c30, c01, c11, c21, c31, 75 | c02, c12, c22, c32, c03, c13, c23, c33]) 76 | def vector2_multiply(self, other): 77 | ( 78 | a00, a10, a20, a30, a01, a11, a21, a31, 79 | a02, a12, a22, a32, a03, a13, a23, a33, 80 | ) = self.value 81 | b0, b1 = other 82 | return ( 83 | a00 * b0 + a01 * b1 + a03, 84 | a10 * b0 + a11 * b1 + a13, 85 | ) 86 | def vector3_multiply(self, other): 87 | ( 88 | a00, a10, a20, a30, a01, a11, a21, a31, 89 | a02, a12, a22, a32, a03, a13, a23, a33, 90 | ) = self.value 91 | b0, b1, b2 = other 92 | return ( 93 | a00 * b0 + a01 * b1 + a02 * b2 + a03, 94 | a10 * b0 + a11 * b1 + a12 * b2 + a13, 95 | a20 * b0 + a21 * b1 + a22 * b2 + a23, 96 | ) 97 | def vector4_multiply(self, other): 98 | ( 99 | a00, a10, a20, a30, a01, a11, a21, a31, 100 | a02, a12, a22, a32, a03, a13, a23, a33, 101 | ) = self.value 102 | b0, b1, b2, b3 = other 103 | return ( 104 | a00 * b0 + a01 * b1 + a02 * b2 + a03 * b3, 105 | a10 * b0 + a11 * b1 + a12 * b2 + a13 * b3, 106 | a20 * b0 + a21 * b1 + a22 * b2 + a23 * b3, 107 | a30 * b0 + a31 * b1 + a32 * b2 + a33 * b3, 108 | ) 109 | def identity(self): 110 | return Matrix() 111 | def transpose(self): 112 | ( 113 | a00, a10, a20, a30, a01, a11, a21, a31, 114 | a02, a12, a22, a32, a03, a13, a23, a33, 115 | ) = self.value 116 | return Matrix([ 117 | a00, a01, a02, a03, a10, a11, a12, a13, 118 | a20, a21, a22, a23, a30, a31, a32, a33]) 119 | def determinant(self): 120 | ( 121 | m00, m10, m20, m30, m01, m11, m21, m31, 122 | m02, m12, m22, m32, m03, m13, m23, m33, 123 | ) = self.value 124 | return ( 125 | m00 * m11 * m22 * m33 - m00 * m11 * m23 * m32 + 126 | m00 * m12 * m23 * m31 - m00 * m12 * m21 * m33 + 127 | m00 * m13 * m21 * m32 - m00 * m13 * m22 * m31 - 128 | m01 * m12 * m23 * m30 + m01 * m12 * m20 * m33 - 129 | m01 * m13 * m20 * m32 + m01 * m13 * m22 * m30 - 130 | m01 * m10 * m22 * m33 + m01 * m10 * m23 * m32 + 131 | m02 * m13 * m20 * m31 - m02 * m13 * m21 * m30 + 132 | m02 * m10 * m21 * m33 - m02 * m10 * m23 * m31 + 133 | m02 * m11 * m23 * m30 - m02 * m11 * m20 * m33 - 134 | m03 * m10 * m21 * m32 + m03 * m10 * m22 * m31 - 135 | m03 * m11 * m22 * m30 + m03 * m11 * m20 * m32 - 136 | m03 * m12 * m20 * m31 + m03 * m12 * m21 * m30) 137 | def inverse(self): 138 | ( 139 | m00, m10, m20, m30, m01, m11, m21, m31, 140 | m02, m12, m22, m32, m03, m13, m23, m33, 141 | ) = self.value 142 | d = self.determinant() 143 | n00 = (m12 * m23 * m31 - m13 * m22 * m31 + m13 * m21 * m32 - 144 | m11 * m23 * m32 - m12 * m21 * m33 + m11 * m22 * m33) / d 145 | n01 = (m03 * m22 * m31 - m02 * m23 * m31 - m03 * m21 * m32 + 146 | m01 * m23 * m32 + m02 * m21 * m33 - m01 * m22 * m33) / d 147 | n02 = (m02 * m13 * m31 - m03 * m12 * m31 + m03 * m11 * m32 - 148 | m01 * m13 * m32 - m02 * m11 * m33 + m01 * m12 * m33) / d 149 | n03 = (m03 * m12 * m21 - m02 * m13 * m21 - m03 * m11 * m22 + 150 | m01 * m13 * m22 + m02 * m11 * m23 - m01 * m12 * m23) / d 151 | n10 = (m13 * m22 * m30 - m12 * m23 * m30 - m13 * m20 * m32 + 152 | m10 * m23 * m32 + m12 * m20 * m33 - m10 * m22 * m33) / d 153 | n11 = (m02 * m23 * m30 - m03 * m22 * m30 + m03 * m20 * m32 - 154 | m00 * m23 * m32 - m02 * m20 * m33 + m00 * m22 * m33) / d 155 | n12 = (m03 * m12 * m30 - m02 * m13 * m30 - m03 * m10 * m32 + 156 | m00 * m13 * m32 + m02 * m10 * m33 - m00 * m12 * m33) / d 157 | n13 = (m02 * m13 * m20 - m03 * m12 * m20 + m03 * m10 * m22 - 158 | m00 * m13 * m22 - m02 * m10 * m23 + m00 * m12 * m23) / d 159 | n20 = (m11 * m23 * m30 - m13 * m21 * m30 + m13 * m20 * m31 - 160 | m10 * m23 * m31 - m11 * m20 * m33 + m10 * m21 * m33) / d 161 | n21 = (m03 * m21 * m30 - m01 * m23 * m30 - m03 * m20 * m31 + 162 | m00 * m23 * m31 + m01 * m20 * m33 - m00 * m21 * m33) / d 163 | n22 = (m01 * m13 * m30 - m03 * m11 * m30 + m03 * m10 * m31 - 164 | m00 * m13 * m31 - m01 * m10 * m33 + m00 * m11 * m33) / d 165 | n23 = (m03 * m11 * m20 - m01 * m13 * m20 - m03 * m10 * m21 + 166 | m00 * m13 * m21 + m01 * m10 * m23 - m00 * m11 * m23) / d 167 | n30 = (m12 * m21 * m30 - m11 * m22 * m30 - m12 * m20 * m31 + 168 | m10 * m22 * m31 + m11 * m20 * m32 - m10 * m21 * m32) / d 169 | n31 = (m01 * m22 * m30 - m02 * m21 * m30 + m02 * m20 * m31 - 170 | m00 * m22 * m31 - m01 * m20 * m32 + m00 * m21 * m32) / d 171 | n32 = (m02 * m11 * m30 - m01 * m12 * m30 - m02 * m10 * m31 + 172 | m00 * m12 * m31 + m01 * m10 * m32 - m00 * m11 * m32) / d 173 | n33 = (m01 * m12 * m20 - m02 * m11 * m20 + m02 * m10 * m21 - 174 | m00 * m12 * m21 - m01 * m10 * m22 + m00 * m11 * m22) / d 175 | return Matrix([ 176 | n00, n10, n20, n30, n01, n11, n21, n31, 177 | n02, n12, n22, n32, n03, n13, n23, n33]) 178 | def translate(self, value): 179 | x, y, z = value 180 | matrix = Matrix([ 181 | 1, 0, 0, 0, 182 | 0, 1, 0, 0, 183 | 0, 0, 1, 0, 184 | x, y, z, 1, 185 | ]) 186 | return matrix * self 187 | def scale(self, value): 188 | x, y, z = value 189 | matrix = Matrix([ 190 | x, 0, 0, 0, 191 | 0, y, 0, 0, 192 | 0, 0, z, 0, 193 | 0, 0, 0, 1, 194 | ]) 195 | return matrix * self 196 | def rotate(self, vector, angle): 197 | x, y, z = normalize(vector) 198 | s = sin(angle) 199 | c = cos(angle) 200 | m = 1 - c 201 | matrix = Matrix([ 202 | m * x * x + c, 203 | m * x * y - z * s, 204 | m * z * x + y * s, 205 | 0, 206 | m * x * y + z * s, 207 | m * y * y + c, 208 | m * y * z - x * s, 209 | 0, 210 | m * z * x - y * s, 211 | m * y * z + x * s, 212 | m * z * z + c, 213 | 0, 214 | 0, 215 | 0, 216 | 0, 217 | 1, 218 | ]) 219 | return matrix * self 220 | def frustum(self, left, right, bottom, top, near, far): 221 | t1 = 2 * near 222 | t2 = right - left 223 | t3 = top - bottom 224 | t4 = far - near 225 | matrix = Matrix([ 226 | t1 / t2, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | t1 / t3, 232 | 0, 233 | 0, 234 | (right + left) / t2, 235 | (top + bottom) / t3, 236 | (-far - near) / t4, 237 | -1, 238 | 0, 239 | 0, 240 | (-t1 * far) / t4, 241 | 0, 242 | ]) 243 | return matrix * self 244 | def perspective(self, fov, aspect, near, far): 245 | ymax = near * tan(fov * pi / 360) 246 | xmax = ymax * aspect 247 | return self.frustum(-xmax, xmax, -ymax, ymax, near, far) 248 | def orthographic(self, left, right, bottom, top, near, far): 249 | matrix = Matrix([ 250 | 2 / (right - left), 251 | 0, 252 | 0, 253 | 0, 254 | 0, 255 | 2 / (top - bottom), 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | -2 / (far - near), 261 | 0, 262 | -(right + left) / (right - left), 263 | -(top + bottom) / (top - bottom), 264 | -(far + near) / (far - near), 265 | 1, 266 | ]) 267 | return matrix * self 268 | -------------------------------------------------------------------------------- /pg/noise.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | import random 3 | 4 | F2 = (3 ** 0.5 - 1) * 0.5 5 | G2 = (3 - 3 ** 0.5) / 6.0 6 | 7 | GRAD = [ 8 | (1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0), 9 | (1, 0, 1), (-1, 0, 1), (1, 0, -1), (-1, 0, -1), 10 | (0, 1, 1), (0, -1, 1), (0, 1, -1), (0, -1, -1), 11 | (1, 1, 0), (0, -1, 1), (-1, 1, 0), (0, -1, -1), 12 | ] 13 | 14 | PERM = [ 15 | 151, 160, 137, 91, 90, 15, 131, 13, 16 | 201, 95, 96, 53, 194, 233, 7, 225, 17 | 140, 36, 103, 30, 69, 142, 8, 99, 18 | 37, 240, 21, 10, 23, 190, 6, 148, 19 | 247, 120, 234, 75, 0, 26, 197, 62, 20 | 94, 252, 219, 203, 117, 35, 11, 32, 21 | 57, 177, 33, 88, 237, 149, 56, 87, 22 | 174, 20, 125, 136, 171, 168, 68, 175, 23 | 74, 165, 71, 134, 139, 48, 27, 166, 24 | 77, 146, 158, 231, 83, 111, 229, 122, 25 | 60, 211, 133, 230, 220, 105, 92, 41, 26 | 55, 46, 245, 40, 244, 102, 143, 54, 27 | 65, 25, 63, 161, 1, 216, 80, 73, 28 | 209, 76, 132, 187, 208, 89, 18, 169, 29 | 200, 196, 135, 130, 116, 188, 159, 86, 30 | 164, 100, 109, 198, 173, 186, 3, 64, 31 | 52, 217, 226, 250, 124, 123, 5, 202, 32 | 38, 147, 118, 126, 255, 82, 85, 212, 33 | 207, 206, 59, 227, 47, 16, 58, 17, 34 | 182, 189, 28, 42, 223, 183, 170, 213, 35 | 119, 248, 152, 2, 44, 154, 163, 70, 36 | 221, 153, 101, 155, 167, 43, 172, 9, 37 | 129, 22, 39, 253, 19, 98, 108, 110, 38 | 79, 113, 224, 232, 178, 185, 112, 104, 39 | 218, 246, 97, 228, 251, 34, 242, 193, 40 | 238, 210, 144, 12, 191, 179, 162, 241, 41 | 81, 51, 145, 235, 249, 14, 239, 107, 42 | 49, 192, 214, 31, 181, 199, 106, 157, 43 | 184, 84, 204, 176, 115, 121, 50, 45, 44 | 127, 4, 150, 254, 138, 236, 205, 93, 45 | 222, 114, 67, 29, 24, 72, 243, 141, 46 | 128, 195, 78, 66, 215, 61, 156, 180, 47 | ] 48 | 49 | class Noise(object): 50 | def __init__(self, seed=None): 51 | self.seed(seed) 52 | def seed(self, seed=None): 53 | perm = list(PERM) 54 | if seed is not None: 55 | random.Random(seed).shuffle(perm) 56 | self.perm = perm + perm 57 | def _simplex2(self, x, y): 58 | perm = self.perm 59 | s = (x + y) * F2 60 | i = floor(x + s) 61 | j = floor(y + s) 62 | t = (i + j) * G2 63 | x0 = x - (i - t) 64 | y0 = y - (j - t) 65 | i1 = int(x0 > y0) 66 | j1 = int(x0 <= y0) 67 | x1 = x0 - i1 + G2 68 | y1 = y0 - j1 + G2 69 | x2 = x0 + G2 * 2 - 1 70 | y2 = y0 + G2 * 2 - 1 71 | i = int(i) % 256 72 | j = int(j) % 256 73 | noise = 0.0 74 | f = 0.5 - x0 * x0 - y0 * y0 75 | if f > 0: 76 | gx, gy, _ = GRAD[perm[i + perm[j]] % 12] 77 | noise += f * f * f * f * (gx * x0 + gy * y0) 78 | f = 0.5 - x1 * x1 - y1 * y1 79 | if f > 0: 80 | gx, gy, _ = GRAD[perm[i + i1 + perm[j + j1]] % 12] 81 | noise += f * f * f * f * (gx * x1 + gy * y1) 82 | f = 0.5 - x2 * x2 - y2 * y2 83 | if f > 0: 84 | gx, gy, _ = GRAD[perm[i + 1 + perm[j + 1]] % 12] 85 | noise += f * f * f * f * (gx * x2 + gy * y2) 86 | return noise * 70 87 | def simplex2(self, x, y, octaves=1, persistence=0.5, lacunarity=2.0): 88 | frequency = 1.0 89 | amplitude = 1.0 90 | maximum = 1.0 91 | total = self._simplex2(x, y) 92 | for _ in xrange(octaves - 1): 93 | frequency *= lacunarity 94 | amplitude *= persistence 95 | maximum += amplitude 96 | total += self._simplex2(x * frequency, y * frequency) * amplitude 97 | return total / maximum 98 | 99 | _instance = Noise() 100 | 101 | def simplex2(x, y, octaves=1, persistence=0.5, lacunarity=2.0): 102 | return _instance.simplex2(x, y, octaves, persistence, lacunarity) 103 | -------------------------------------------------------------------------------- /pg/obj.py: -------------------------------------------------------------------------------- 1 | from .core import Mesh 2 | from .util import normal_from_points 3 | from itertools import izip_longest 4 | import os 5 | 6 | def parse_obj(path): 7 | if os.path.exists(path): 8 | with open(path, 'r') as fp: 9 | path = fp.read() 10 | lines = path.split('\n') 11 | lu_v = [] 12 | lu_vt = [] 13 | lu_vn = [] 14 | positions = [] 15 | normals = [] 16 | uvs = [] 17 | for line in lines: 18 | line = line.strip() 19 | if not line or line.startswith('#'): 20 | continue 21 | args = line.split() 22 | keyword = args[0] 23 | args = args[1:] 24 | if keyword == 'v': 25 | v = tuple(map(float, args)) 26 | lu_v.append(v) 27 | elif keyword == 'vt': 28 | vt = tuple(map(float, args)) 29 | lu_vt.append(vt) 30 | elif keyword == 'vn': 31 | vn = tuple(map(float, args)) 32 | lu_vn.append(vn) 33 | elif keyword == 'f': 34 | data = [(x + '//').split('/')[:3] for x in args] 35 | data = [[(int(x) - 1) if x.isdigit() else None for x in row] 36 | for row in data] 37 | a = data[0] 38 | for b, c in zip(data[1:], data[2:]): 39 | try: 40 | n = normal_from_points(*[lu_v[x[0]] for x in [a, b, c]]) 41 | except ZeroDivisionError: 42 | continue 43 | for vertex in [a, b, c]: 44 | v, vt, vn = vertex 45 | if v is not None: 46 | positions.append(lu_v[v]) 47 | if vt is not None: 48 | uvs.append(lu_vt[vt]) 49 | if vn is not None: 50 | normals.append(lu_vn[vn]) 51 | else: 52 | normals.append(n) 53 | return positions, normals, uvs 54 | 55 | def save_obj(self, path): 56 | lines = [] 57 | v = sorted(set(self.positions)) 58 | vt = sorted(set(self.uvs)) 59 | vn = sorted(set(self.normals)) 60 | lu_v = dict((x, i + 1) for i, x in enumerate(v)) 61 | lu_vt = dict((x, i + 1) for i, x in enumerate(vt)) 62 | lu_vn = dict((x, i + 1) for i, x in enumerate(vn)) 63 | for x in v: 64 | lines.append('v %f %f %f' % x) 65 | for x in vt: 66 | lines.append('vt %f %f' % x) 67 | for x in vn: 68 | lines.append('vn %f %f %f' % x) 69 | f = [] 70 | for v, vt, vn in izip_longest(self.positions, self.uvs, self.normals): 71 | v = lu_v.get(v, '') 72 | vt = lu_vt.get(vt, '') 73 | vn = lu_vn.get(vn, '') 74 | f.append(('%s/%s/%s' % (v, vt, vn)).strip('/')) 75 | if len(f) == 3: 76 | lines.append('f %s' % ' '.join(f)) 77 | f = [] 78 | data = '\n'.join(lines) 79 | with open(path, 'w') as fp: 80 | fp.write(data) 81 | 82 | Mesh.save_obj = save_obj 83 | 84 | class OBJ(Mesh): 85 | def __init__(self, path): 86 | super(OBJ, self).__init__() 87 | positions, normals, uvs = parse_obj(path) 88 | self.positions = positions 89 | self.normals = normals 90 | self.uvs = uvs 91 | -------------------------------------------------------------------------------- /pg/pack.py: -------------------------------------------------------------------------------- 1 | from math import ceil, log 2 | 3 | class Node(object): 4 | def __init__(self, x, y, w, h): 5 | self.x = x 6 | self.y = y 7 | self.w = w 8 | self.h = h 9 | self.right = None 10 | self.down = None 11 | def insert(self, w, h): 12 | if self.right: 13 | result = self.right.insert(w, h) 14 | if result: 15 | return result 16 | result = self.down.insert(w, h) 17 | if result: 18 | return result 19 | return None 20 | elif w <= self.w and h <= self.h: 21 | self.right = Node(self.x + w, self.y, self.w - w, h) 22 | self.down = Node(self.x, self.y + h, self.w, self.h - h) 23 | return (self.x, self.y, w, h) 24 | else: 25 | return None 26 | 27 | def pot(x): 28 | return 2 ** int(ceil(log(x) / log(2))) 29 | 30 | def estimate_size(sizes): 31 | a = sum(w * h for w, h in sizes) 32 | mw = max(w for w, h in sizes) 33 | mh = max(h for w, h in sizes) 34 | w1 = pot(mw) 35 | h1 = pot(mh) 36 | w2 = pot(a ** 0.5) 37 | h2 = pot(float(a) / w2) 38 | return (max(w1, w2), max(h1, h2)) 39 | 40 | def try_pack(tw, th, items): 41 | result = [] 42 | node = Node(0, 0, tw, th) 43 | for index, (w, h) in items: 44 | position = node.insert(w, h) 45 | if position is None: 46 | return None 47 | result.append((index, position)) 48 | result.sort() 49 | result = [x[1] for x in result] 50 | return result 51 | 52 | def pack(sizes): 53 | items = enumerate(sizes) 54 | items = sorted(items, key=lambda x: max(x[1]), reverse=True) 55 | tw, th = estimate_size(sizes) 56 | while True: 57 | result = try_pack(tw, th, items) 58 | if result: 59 | tw = max(x + w for x, y, w, h in result) 60 | th = max(y + h for x, y, w, h in result) 61 | return (tw, th), result 62 | if tw <= th: 63 | tw *= 2 64 | else: 65 | th *= 2 66 | -------------------------------------------------------------------------------- /pg/poisson.py: -------------------------------------------------------------------------------- 1 | from math import pi, sin, cos, hypot, floor 2 | import random 3 | 4 | class Grid(object): 5 | def __init__(self, r): 6 | self.r = r 7 | self.size = r / 2 ** 0.5 8 | self.cells = {} 9 | def points(self): 10 | return self.cells.values() 11 | def normalize(self, x, y): 12 | i = int(floor(x / self.size)) 13 | j = int(floor(y / self.size)) 14 | return (i, j) 15 | def nearby(self, x, y): 16 | result = [] 17 | i, j = self.normalize(x, y) 18 | for p in xrange(i - 2, i + 3): 19 | for q in xrange(j - 2, j + 3): 20 | if (p, q) in self.cells: 21 | result.append(self.cells[(p, q)]) 22 | return result 23 | def insert(self, x, y): 24 | for bx, by in self.nearby(x, y): 25 | if hypot(x - bx, y - by) < self.r: 26 | return False 27 | i, j = self.normalize(x, y) 28 | self.cells[(i, j)] = (x, y) 29 | return True 30 | 31 | def poisson_disc(x1, y1, x2, y2, r, n): 32 | x = x1 + (x2 - x1) / 2.0 33 | y = y1 + (y2 - y1) / 2.0 34 | active = [(x, y)] 35 | grid = Grid(r) 36 | grid.insert(x, y) 37 | while active: 38 | ax, ay = random.choice(active) 39 | for i in xrange(n): 40 | a = random.random() * 2 * pi 41 | d = random.random() * r + r 42 | x = ax + cos(a) * d 43 | y = ay + sin(a) * d 44 | if x < x1 or y < y1 or x > x2 or y > y2: 45 | continue 46 | if not grid.insert(x, y): 47 | continue 48 | active.append((x, y)) 49 | break 50 | else: 51 | active.remove((ax, ay)) 52 | return grid.points() 53 | -------------------------------------------------------------------------------- /pg/programs.py: -------------------------------------------------------------------------------- 1 | from .core import Program 2 | from .matrix import Matrix 3 | from .util import normalize 4 | 5 | class BaseProgram(Program): 6 | def __init__(self): 7 | super(BaseProgram, self).__init__(self.VS, self.FS) 8 | 9 | class SolidColorProgram(BaseProgram): 10 | '''Renders all primitives with a single, solid color. 11 | 12 | :param matrix: the model-view-projection matrix, required 13 | :param position: vertex buffer containing vertex positions, required 14 | :param color: the color to use (default: white) 15 | ''' 16 | VS = ''' 17 | #version 120 18 | 19 | uniform mat4 matrix; 20 | 21 | attribute vec4 position; 22 | 23 | void main() { 24 | gl_Position = matrix * position; 25 | } 26 | ''' 27 | FS = ''' 28 | #version 120 29 | 30 | uniform vec4 color; 31 | 32 | void main() { 33 | gl_FragColor = color; 34 | } 35 | ''' 36 | def set_defaults(self, context): 37 | context.color = (1.0, 1.0, 1.0, 1.0) 38 | 39 | class TextureProgram(BaseProgram): 40 | '''Renders with a texture and no lighting. 41 | 42 | :param matrix: the model-view-projection matrix, required 43 | :param position: vertex buffer containing vertex positions, required 44 | :param sampler: texture to use, required 45 | ''' 46 | VS = ''' 47 | #version 120 48 | 49 | uniform mat4 matrix; 50 | 51 | attribute vec4 position; 52 | attribute vec2 uv; 53 | 54 | varying vec2 frag_uv; 55 | 56 | void main() { 57 | gl_Position = matrix * position; 58 | frag_uv = uv; 59 | } 60 | ''' 61 | FS = ''' 62 | #version 120 63 | 64 | uniform sampler2D sampler; 65 | 66 | varying vec2 frag_uv; 67 | 68 | void main() { 69 | vec4 color = texture2D(sampler, frag_uv); 70 | if (color.a == 0) { 71 | discard; 72 | } 73 | gl_FragColor = color; 74 | } 75 | ''' 76 | def set_defaults(self, context): 77 | pass 78 | 79 | class DirectionalLightProgram(BaseProgram): 80 | '''Renders the scene with a single, directional light source. Optionally, 81 | primitives can be textured or independently colored. 82 | 83 | :param matrix: the model-view-projection matrix, required 84 | :param model_matrix: the model matrix, required 85 | :param position: vertex buffer containing vertex positions, required 86 | :param normal: vertex buffer containing vertex normals, required 87 | :param uv: vertex buffer containing vertex texture coordinates, required if use_texture == True 88 | :param color: vertex buffer containing vertex colors, required if use_color == True 89 | :param sampler: texture to use, required if use_texture == True 90 | :param camera_position: the camera position in model space, required 91 | :param normal_matrix: the normal matrix, transposed inverse of model matrix, required 92 | :param light_direction: vector specifying light direction, default: (1, 1, 1) normalized 93 | :param object_color: color for all vertices if textures and color attributes are disabled, default: (0.4, 0.6, 0.8) 94 | :param ambient_color: ambient light color, default: (0.3, 0.3, 0.3) 95 | :param light_color: directional light color, default: (0.7, 0.7, 0.7) 96 | :param specular_power: controls exponent used in specular lighting, default: 32.0 97 | :param specular_multiplier: controls intensity of specular lighting, default: 1.0 98 | :param use_texture: controls whether a texture is to be used, default: False 99 | :param use_color: controls whether per-vertex colors are provided, default: False 100 | ''' 101 | VS = ''' 102 | #version 120 103 | 104 | uniform mat4 matrix; 105 | uniform mat4 model_matrix; 106 | uniform mat4 normal_matrix; 107 | 108 | attribute vec4 position; 109 | attribute vec3 normal; 110 | attribute vec2 uv; 111 | attribute vec3 color; 112 | 113 | varying vec3 frag_position; 114 | varying vec3 frag_normal; 115 | varying vec2 frag_uv; 116 | varying vec3 frag_color; 117 | 118 | void main() { 119 | gl_Position = matrix * position; 120 | frag_position = vec3(model_matrix * position); 121 | frag_normal = mat3(normal_matrix) * normal; 122 | frag_uv = uv; 123 | frag_color = color; 124 | } 125 | ''' 126 | FS = ''' 127 | #version 120 128 | 129 | uniform sampler2D sampler; 130 | uniform vec3 camera_position; 131 | 132 | uniform vec3 light_direction; 133 | uniform vec3 object_color; 134 | uniform vec3 ambient_color; 135 | uniform vec3 light_color; 136 | uniform float specular_power; 137 | uniform float specular_multiplier; 138 | uniform bool use_texture; 139 | uniform bool use_color; 140 | 141 | varying vec3 frag_position; 142 | varying vec3 frag_normal; 143 | varying vec2 frag_uv; 144 | varying vec3 frag_color; 145 | 146 | void main() { 147 | vec3 color = object_color; 148 | if (use_color) { 149 | color = frag_color; 150 | } 151 | if (use_texture) { 152 | color = vec3(texture2D(sampler, frag_uv)); 153 | } 154 | float diffuse = max(dot(frag_normal, light_direction), 0.0); 155 | float specular = 0.0; 156 | if (diffuse > 0.0) { 157 | vec3 camera_vector = normalize(camera_position - frag_position); 158 | specular = pow(max(dot(camera_vector, 159 | reflect(-light_direction, frag_normal)), 0.0), specular_power); 160 | } 161 | vec3 light = ambient_color + light_color * diffuse + 162 | specular * specular_multiplier; 163 | gl_FragColor = vec4(min(color * light, vec3(1.0)), 1.0); 164 | } 165 | ''' 166 | def set_defaults(self, context): 167 | context.model_matrix = Matrix() 168 | context.normal_matrix = Matrix().inverse().transpose() 169 | context.light_direction = normalize((1, 1, 1)) 170 | context.object_color = (0.4, 0.6, 0.8) 171 | context.ambient_color = (0.3, 0.3, 0.3) 172 | context.light_color = (0.7, 0.7, 0.7) 173 | context.specular_power = 32.0 174 | context.specular_multiplier = 1.0 175 | context.use_texture = False 176 | context.use_color = False 177 | 178 | # TODO: same as TextureProgram? consolidate? 179 | class TextProgram(BaseProgram): 180 | '''Renders 2D text using a font texture. Used by the built-in ``pg.Font``. 181 | 182 | :param matrix: the model-view-projection matrix, required 183 | :param position: vertex buffer containing vertex positions, required 184 | :param uv: vertex buffer containing vertex texture coordinates, required 185 | :param sampler: font texture to use, required 186 | ''' 187 | VS = ''' 188 | #version 120 189 | 190 | uniform mat4 matrix; 191 | 192 | attribute vec4 position; 193 | attribute vec2 uv; 194 | 195 | varying vec2 frag_uv; 196 | 197 | void main() { 198 | gl_Position = matrix * position; 199 | frag_uv = uv; 200 | } 201 | ''' 202 | FS = ''' 203 | #version 120 204 | 205 | uniform sampler2D sampler; 206 | 207 | varying vec2 frag_uv; 208 | 209 | void main() { 210 | vec4 color = texture2D(sampler, frag_uv); 211 | if (color.a == 0) { 212 | discard; 213 | } 214 | gl_FragColor = color; 215 | } 216 | ''' 217 | def set_defaults(self, context): 218 | pass 219 | -------------------------------------------------------------------------------- /pg/sprite.py: -------------------------------------------------------------------------------- 1 | from .core import VertexBuffer, Texture, Context, App 2 | from .matrix import Matrix 3 | from .pack import pack 4 | from .programs import TextureProgram 5 | from .util import interleave 6 | from math import sin, cos 7 | from OpenGL.GL import * 8 | from PIL import Image 9 | import os 10 | 11 | def load_directory(path): 12 | names = [] 13 | images = [] 14 | extensions = set(['.png', '.jpg']) 15 | for name in os.listdir(path): 16 | base, ext = os.path.splitext(name) 17 | if ext.lower() not in extensions: 18 | continue 19 | im = Image.open(os.path.join(path, name)) 20 | names.append(base) 21 | images.append(im) 22 | return names, images 23 | 24 | def load_images(paths): 25 | names = [] 26 | images = [] 27 | for path in paths: 28 | base, ext = os.path.splitext(path) 29 | im = Image.open(path) 30 | names.append(os.path.basename(base)) 31 | images.append(im) 32 | return names, images 33 | 34 | class SpriteBatch(object): 35 | def __init__(self, sheet): 36 | self.sprites = [] 37 | self.minz = -10000 38 | self.maxz = 10000 39 | self.vb = VertexBuffer() 40 | self.context = Context(TextureProgram()) 41 | self.context.sampler = sheet 42 | self.context.position, self.context.uv = self.vb.slices(3, 2) 43 | def delete(self): 44 | self.vb.delete() 45 | def append(self, sprite): 46 | self.sprites.append(sprite) 47 | def get_vertex_data(self): 48 | result = [] 49 | for sprite in self.sprites: 50 | result.extend(sprite.get_vertex_data()) 51 | return result 52 | def draw(self, matrix=None): 53 | dirty = not all(x.vertex_data for x in self.sprites) 54 | w, h = App.instance.current_window.size 55 | self.context.matrix = matrix or Matrix().orthographic( 56 | 0, w, 0, h, self.minz, self.maxz) 57 | if dirty: 58 | self.vb.set_data(self.get_vertex_data()) 59 | glEnable(GL_BLEND) 60 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 61 | self.context.draw() 62 | glDisable(GL_BLEND) 63 | 64 | class Sprite(object): 65 | ATTRIBUTES = set([ 66 | 'frame', 67 | 'anchor', 68 | 'position', 69 | 'rotation', 70 | 'scale', 71 | 'z', 72 | ]) 73 | def __init__(self, frame, batch=None): 74 | self.vertex_data = None 75 | self.frame = frame 76 | self.anchor = (0.5, 0.5) 77 | self.position = (0, 0) 78 | self.rotation = 0 79 | self.scale = 1 80 | self.z = 0 81 | if batch: 82 | batch.append(self) 83 | def __setattr__(self, name, value): 84 | if name in Sprite.ATTRIBUTES: 85 | self.vertex_data = None 86 | super(Sprite, self).__setattr__(name, value) 87 | def get_vertex_data(self): 88 | if self.vertex_data: 89 | return self.vertex_data 90 | ax, ay = self.anchor 91 | px, py = self.position 92 | fw, fh = self.frame.size 93 | rs = sin(self.rotation) 94 | rc = cos(self.rotation) 95 | s = self.scale 96 | z = self.z 97 | coords = self.frame.coords 98 | u = (coords[0], coords[2]) 99 | v = (coords[1], coords[3]) 100 | points = [(0, 0), (1, 0), (0, 1), (1, 1)] 101 | data = [] 102 | for i, j in points: 103 | x, y = (i - ax) * fw * s, (j - ay) * fh * s 104 | x, y = px + x * rc - y * rs, py + x * rs + y * rc 105 | data.append((x, y, z, u[i], v[j])) 106 | indexes = [0, 1, 2, 1, 3, 2] 107 | self.vertex_data = [data[i] for i in indexes] 108 | return self.vertex_data 109 | def draw(self, context): 110 | vb = VertexBuffer(self.get_vertex_data()) 111 | context.position, context.uv = vb.slices(3, 2) 112 | glEnable(GL_BLEND) 113 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 114 | context.draw() 115 | glDisable(GL_BLEND) 116 | vb.delete() 117 | 118 | class SpriteFrame(object): 119 | def __init__(self, name, size, coords): 120 | self.name = name 121 | self.size = size 122 | self.coords = coords 123 | def __call__(self, *args, **kwargs): 124 | return Sprite(self, *args, **kwargs) 125 | 126 | class SpriteSheet(object): 127 | def __init__(self, unit, arg): 128 | if isinstance(arg, basestring): 129 | if os.path.isdir(arg): 130 | names, images = load_directory(arg) 131 | else: 132 | names, images = load_images([arg]) 133 | else: 134 | names, images = load_images(arg) 135 | p = 1 136 | sizes = [x.size for x in images] 137 | sizes = [(w + p * 2, h + p * 2) for w, h in sizes] 138 | size, positions = pack(sizes) 139 | im = Image.new('RGBA', size) 140 | tw, th = size 141 | self.lookup = {} 142 | for name, image, (x, y, w, h) in zip(names, images, positions): 143 | u1 = (x + p) / float(tw - 1) 144 | u2 = (x + w - p) / float(tw - 1) 145 | v2 = 1 - (y + p) / float(th - 1) 146 | v1 = 1 - (y + h - p) / float(th - 1) 147 | im.paste(image, (x + p, y + p)) 148 | frame = SpriteFrame(name, (w, h), (u1, v1, u2, v2)) 149 | self.lookup[name] = frame 150 | self.texture = Texture(unit, im) 151 | def get_uniform_value(self): 152 | return self.texture.unit 153 | def get(self, name): 154 | return self.lookup.get(name) 155 | def __getattr__(self, name): 156 | if name in self.lookup: 157 | return self.lookup[name] 158 | return super(SpriteSheet, self).__getattr__(name) 159 | def __getitem__(self, name): 160 | return self.lookup[name] 161 | -------------------------------------------------------------------------------- /pg/stl.py: -------------------------------------------------------------------------------- 1 | from .core import Mesh 2 | from .util import normal_from_points 3 | from itertools import izip_longest 4 | import os 5 | import struct 6 | 7 | def parse_ascii_stl(data): 8 | if os.path.exists(data): 9 | with open(data, 'r') as fp: 10 | data = fp.read() 11 | rows = [] 12 | for line in data.split('\n'): 13 | args = line.strip().lower().split() 14 | if 'vertex' in args or 'normal' in args: 15 | rows.append(tuple(map(float, args[-3:]))) 16 | positions = [] 17 | normals = [] 18 | uvs = [] 19 | for i in xrange(0, len(rows), 4): 20 | n, v1, v2, v3 = rows[i:i+4] 21 | if not any(n): 22 | try: 23 | n = normal_from_points(v1, v2, v3) 24 | except ZeroDivisionError: 25 | continue 26 | positions.extend([v1, v2, v3]) 27 | normals.extend([n, n, n]) 28 | return positions, normals, uvs 29 | 30 | def parse_binary_stl(data): 31 | if os.path.exists(data): 32 | with open(data, 'rb') as fp: 33 | data = fp.read() 34 | positions = [] 35 | normals = [] 36 | uvs = [] 37 | count = struct.unpack('> (8 * 2)) & 255) / 255.0 10 | g = ((value >> (8 * 1)) & 255) / 255.0 11 | b = ((value >> (8 * 0)) & 255) / 255.0 12 | return (r, g, b) 13 | 14 | def normalize(vector): 15 | '''Normalizes the `vector` so that its length is 1. `vector` can have 16 | any number of components. 17 | ''' 18 | d = sum(x * x for x in vector) ** 0.5 19 | return tuple(x / d for x in vector) 20 | 21 | def distance(p1, p2): 22 | '''Computes and returns the distance between two points, `p1` and `p2`. 23 | The points can have any number of components. 24 | ''' 25 | return sum((a - b) ** 2 for a, b in zip(p1, p2)) ** 0.5 26 | 27 | def cross(v1, v2): 28 | '''Computes the cross product of two vectors. 29 | ''' 30 | return ( 31 | v1[1] * v2[2] - v1[2] * v2[1], 32 | v1[2] * v2[0] - v1[0] * v2[2], 33 | v1[0] * v2[1] - v1[1] * v2[0], 34 | ) 35 | 36 | def dot(v1, v2): 37 | '''Computes the dot product of two vectors. 38 | ''' 39 | x1, y1, z1 = v1 40 | x2, y2, z2 = v2 41 | return x1 * x2 + y1 * y2 + z1 * z2 42 | 43 | def add(v1, v2): 44 | '''Adds two vectors. 45 | ''' 46 | return tuple(a + b for a, b in zip(v1, v2)) 47 | 48 | def sub(v1, v2): 49 | '''Subtracts two vectors. 50 | ''' 51 | return tuple(a - b for a, b in zip(v1, v2)) 52 | 53 | def mul(v, s): 54 | '''Multiplies a vector and a scalar. 55 | ''' 56 | return tuple(a * s for a in v) 57 | 58 | def neg(vector): 59 | '''Negates a vector. 60 | ''' 61 | return tuple(-x for x in vector) 62 | 63 | def interpolate(v1, v2, t): 64 | '''Interpolate from one vector to another. 65 | ''' 66 | return add(v1, mul(sub(v2, v1), t)) 67 | 68 | def normal_from_points(a, b, c): 69 | '''Computes a normal vector given three points. 70 | ''' 71 | x1, y1, z1 = a 72 | x2, y2, z2 = b 73 | x3, y3, z3 = c 74 | ab = (x2 - x1, y2 - y1, z2 - z1) 75 | ac = (x3 - x1, y3 - y1, z3 - z1) 76 | x, y, z = cross(ab, ac) 77 | d = (x * x + y * y + z * z) ** 0.5 78 | return (x / d, y / d, z / d) 79 | 80 | def smooth_normals(positions, normals): 81 | '''Assigns an averaged normal to each position based on all of the normals 82 | originally used for the position. 83 | ''' 84 | lookup = defaultdict(list) 85 | for position, normal in zip(positions, normals): 86 | lookup[position].append(normal) 87 | result = [] 88 | for position in positions: 89 | tx = ty = tz = 0 90 | for x, y, z in lookup[position]: 91 | tx += x 92 | ty += y 93 | tz += z 94 | d = (tx * tx + ty * ty + tz * tz) ** 0.5 95 | result.append((tx / d, ty / d, tz / d)) 96 | return result 97 | 98 | def bounding_box(positions): 99 | '''Computes the bounding box for a list of 3-dimensional points. 100 | ''' 101 | (x0, y0, z0) = (x1, y1, z1) = positions[0] 102 | for x, y, z in positions: 103 | x0 = min(x0, x) 104 | y0 = min(y0, y) 105 | z0 = min(z0, z) 106 | x1 = max(x1, x) 107 | y1 = max(y1, y) 108 | z1 = max(z1, z) 109 | return (x0, y0, z0), (x1, y1, z1) 110 | 111 | def recenter(positions): 112 | '''Returns a list of new positions centered around the origin. 113 | ''' 114 | (x0, y0, z0), (x1, y1, z1) = bounding_box(positions) 115 | dx = x1 - (x1 - x0) / 2.0 116 | dy = y1 - (y1 - y0) / 2.0 117 | dz = z1 - (z1 - z0) / 2.0 118 | result = [] 119 | for x, y, z in positions: 120 | result.append((x - dx, y - dy, z - dz)) 121 | return result 122 | 123 | def interleave(*args): 124 | '''Interleaves the elements of the provided arrays. 125 | 126 | >>> a = [(0, 0), (1, 0), (2, 0), (3, 0)] 127 | >>> b = [(0, 0), (0, 1), (0, 2), (0, 3)] 128 | >>> interleave(a, b) 129 | [(0, 0, 0, 0), (1, 0, 0, 1), (2, 0, 0, 2), (3, 0, 0, 3)] 130 | 131 | This is useful for combining multiple vertex attributes into a single 132 | vertex buffer. The shader attributes can be assigned a slice of the 133 | vertex buffer. 134 | ''' 135 | result = [] 136 | for array in zip(*args): 137 | result.append(tuple(flatten(array))) 138 | return result 139 | 140 | def flatten(array): 141 | '''Flattens the elements of the provided array, `data`. 142 | 143 | >>> a = [(0, 0), (1, 0), (2, 0), (3, 0)] 144 | >>> flatten(a) 145 | [0, 0, 1, 0, 2, 0, 3, 0] 146 | 147 | The flattening process is not recursive, it is only one level deep. 148 | ''' 149 | result = [] 150 | for value in array: 151 | result.extend(value) 152 | return result 153 | 154 | def distinct(iterable, keyfunc=None): 155 | '''Yields distinct items from `iterable` in the order that they appear. 156 | ''' 157 | seen = set() 158 | for item in iterable: 159 | key = item if keyfunc is None else keyfunc(item) 160 | if key not in seen: 161 | seen.add(key) 162 | yield item 163 | 164 | def ray_triangle_intersection(v1, v2, v3, o, d): 165 | '''Computes the distance from a point to a triangle given a ray. 166 | ''' 167 | eps = 1e-6 168 | e1 = sub(v2, v1) 169 | e2 = sub(v3, v1) 170 | p = cross(d, e2) 171 | det = dot(e1, p) 172 | if abs(det) < eps: 173 | return None 174 | inv = 1.0 / det 175 | t = sub(o, v1) 176 | u = dot(t, p) * inv 177 | if u < 0 or u > 1: 178 | return None 179 | q = cross(t, e1) 180 | v = dot(d, q) * inv 181 | if v < 0 or v > 1: 182 | return None 183 | t = dot(e2, q) * inv 184 | if t > eps: 185 | return t 186 | return None 187 | 188 | def pack_list(fmt, data): 189 | '''Convert a Python list into a ctypes buffer. 190 | 191 | This appears to be faster than the typical method of creating a ctypes 192 | array, e.g. (c_float * len(data))(*data) 193 | ''' 194 | func = struct.Struct(fmt).pack 195 | return create_string_buffer(''.join([func(x) for x in data])) 196 | -------------------------------------------------------------------------------- /pg/wasd.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, pi, atan2, asin 2 | from .core import Scene 3 | from .matrix import Matrix 4 | from .util import normalize 5 | from . import glfw 6 | 7 | class WASD(object): 8 | def __init__(self, scene_or_window, 9 | speed=1.0, sensitivity=2.5, invert=False, exclusive=True): 10 | self.window = scene_or_window 11 | if isinstance(scene_or_window, Scene): 12 | self.window = scene_or_window.window 13 | self.speed = speed 14 | self.sensitivity = sensitivity 15 | self.invert = invert 16 | self.exclusive = exclusive 17 | self.x = 0 18 | self.y = 0 19 | self.z = 0 20 | self.rx = 0 21 | self.ry = 0 22 | self.mx = 0 23 | self.my = 0 24 | self.discard = True 25 | if self.exclusive: 26 | self.window.set_exclusive() 27 | scene_or_window.listeners.append(self) 28 | @property 29 | def position(self): 30 | return (self.x, self.y, self.z) 31 | def look_at(self, position, target): 32 | px, py, pz = position 33 | tx, ty, tz = target 34 | dx, dy, dz = normalize((tx - px, ty - py, tz - pz)) 35 | self.x = px 36 | self.y = py 37 | self.z = pz 38 | self.rx = 2 * pi - (atan2(dx, dz) + pi) 39 | self.ry = asin(dy) 40 | def enter(self): 41 | self.discard = True 42 | def on_mouse_button(self, button, action, mods): 43 | if self.exclusive and not self.window.exclusive: 44 | if button == glfw.MOUSE_BUTTON_1 and action == glfw.PRESS: 45 | self.window.set_exclusive() 46 | self.discard = True 47 | return True 48 | def on_key(self, key, scancode, action, mods): 49 | if self.exclusive: 50 | if key == glfw.KEY_ESCAPE: 51 | self.window.set_exclusive(False) 52 | def on_cursor_pos(self, mx, my): 53 | if self.exclusive and not self.window.exclusive: 54 | return 55 | if self.discard: 56 | self.mx = mx 57 | self.my = my 58 | self.discard = False 59 | return 60 | m = self.sensitivity / 1000.0 61 | self.rx += (mx - self.mx) * m 62 | if self.invert: 63 | self.ry += (my - self.my) * m 64 | else: 65 | self.ry -= (my - self.my) * m 66 | if self.rx < 0: 67 | self.rx += 2 * pi 68 | if self.rx >= 2 * pi: 69 | self.rx -= 2 * pi 70 | self.ry = max(self.ry, -pi / 2) 71 | self.ry = min(self.ry, pi / 2) 72 | self.mx = mx 73 | self.my = my 74 | def get_strafe(self): 75 | sx = sz = 0 76 | if glfw.get_key(self.window.handle, ord('W')): 77 | sz -= 1 78 | if glfw.get_key(self.window.handle, ord('S')): 79 | sz += 1 80 | if glfw.get_key(self.window.handle, ord('A')): 81 | sx -= 1 82 | if glfw.get_key(self.window.handle, ord('D')): 83 | sx += 1 84 | return (sx, sz) 85 | def get_matrix(self, matrix=None, translate=True): 86 | matrix = matrix or Matrix() 87 | if translate: 88 | matrix = matrix.translate((-self.x, -self.y, -self.z)) 89 | matrix = matrix.rotate((cos(self.rx), 0, sin(self.rx)), self.ry) 90 | matrix = matrix.rotate((0, 1, 0), -self.rx) 91 | return matrix 92 | def get_sight_vector(self): 93 | m = cos(self.ry) 94 | vx = cos(self.rx - pi / 2) * m 95 | vy = sin(self.ry) 96 | vz = sin(self.rx - pi / 2) * m 97 | return (vx, vy, vz) 98 | def get_motion_vector(self): 99 | sx, sz = self.get_strafe() 100 | if sx == 0 and sz == 0: 101 | return (0, 0, 0) 102 | strafe = atan2(sz, sx) 103 | m = cos(self.ry) 104 | y = sin(self.ry) 105 | if sx: 106 | if not sz: 107 | y = 0 108 | m = 1 109 | if sz > 0: 110 | y = -y 111 | vx = cos(self.rx + strafe) * m 112 | vy = y 113 | vz = sin(self.rx + strafe) * m 114 | return normalize((vx, vy, vz)) 115 | def update(self, t, dt): 116 | vx, vy, vz = self.get_motion_vector() 117 | self.x += vx * self.speed * dt 118 | self.y += vy * self.speed * dt 119 | self.z += vz * self.speed * dt 120 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==2.5.1 2 | PyOpenGL==3.1.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pg', 5 | version='0.1', 6 | description='Python OpenGL Graphics Framework', 7 | license='MIT', 8 | author='Michael Fogleman', 9 | author_email='michael.fogleman@gmail.com', 10 | url='https://github.com/fogleman/pg', 11 | packages=['pg'], 12 | install_requires=[ 13 | 'Pillow', 14 | 'PyOpenGL', 15 | ], 16 | classifiers=[ 17 | 'Development Status :: 3 - Alpha', 18 | 'Intended Audience :: Developers', 19 | 'Topic :: Multimedia :: Graphics :: 3D Rendering', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 2.6', 23 | 'Programming Language :: Python :: 2.7', 24 | ], 25 | ) 26 | --------------------------------------------------------------------------------