├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── assets │ ├── img_shader.png │ ├── pfp.png │ └── screen_shader.png ├── compute_shaders.rst ├── conf.py ├── index.rst ├── make.bat ├── modules.rst ├── pygame_shaders.rst ├── quick_start.rst ├── screen_shaders.rst ├── surface_shaders.rst └── writing_shaders.rst ├── examples ├── cellular automata │ ├── frag.glsl │ └── main.py ├── compute │ ├── compute.glsl │ └── main.py ├── custom_screen_shader │ ├── main.py │ ├── screen_frag.glsl │ └── screen_vert.glsl ├── helloworld_shader │ ├── default_frag.glsl │ ├── main.py │ └── vertex.txt ├── rotation │ ├── main.py │ └── rotate.glsl ├── simple │ ├── effect.glsl │ └── main.py ├── smoothlife │ ├── frag.glsl │ └── main.py └── water │ ├── default_frag.glsl │ ├── main.py │ └── vertex.txt ├── pygame_shaders ├── __init__.py ├── pygame_shaders.py ├── screen_rect.py ├── shader_utils.py └── texture.py ├── pyproject.toml └── setup.cfg /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | # End of https://www.toptal.com/developers/gitignore/api/python 167 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | # python: 34 | # install: 35 | # - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ScriptLine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pygame Shaders 2 | [![wakatime](https://wakatime.com/badge/github/ScriptLineStudios/pygame_shaders.svg)](https://wakatime.com/badge/github/ScriptLineStudios/pygame_shaders) 3 | ![Lines of code](https://img.shields.io/tokei/lines/github/ScriptLineStudios/pygame_shaders) 4 | 5 | [![Downloads](https://pepy.tech/badge/pygame-shaders)](https://pepy.tech/project/pygame-shaders) 6 | ![PyPI](https://img.shields.io/pypi/v/pygame_shaders) 7 | ![PyPI - Format](https://img.shields.io/pypi/format/pygame_shaders) 8 | [![Downloads](https://pepy.tech/badge/pygame-shaders/month)](https://pepy.tech/project/pygame-shaders) 9 | 10 | ## Easily integrate shaders into your new or existing pygame projects 11 | 12 | This project allows for GLSL shaders to easily be intergrated with either your new or existing Pygame projects without having to touch OpenGL. 13 | 14 | ```python 15 | import pygame 16 | import pygame_shaders 17 | 18 | pygame.init() 19 | 20 | clock = pygame.time.Clock() 21 | 22 | #Create an opengl pygame Surface, this will act as our opengl context. 23 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 24 | 25 | #This is our main display we will do all of our standard pygame rendering on. 26 | display = pygame.Surface((600, 600)) 27 | 28 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 29 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 30 | 31 | #This is our shader object which we can use to render the given shaders onto the screen in various ways. 32 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "fragment.glsl", screen) #<- Because we plan on using this shader for direct rendering (we supply the surface on which we plan to do said direct rendering in this case, screen) 33 | 34 | while True: 35 | #Fill the display with white 36 | display.fill((255, 255, 255)) 37 | 38 | #Standard pygame event stuff 39 | for event in pygame.event.get(): 40 | if event.type == pygame.QUIT: 41 | pygame.quit() 42 | 43 | #Render a rect onto the display using the standard pygame method for drawing rects. 44 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 45 | 46 | #Render the contents of "display" (main surface) onto the opengl screen. 47 | screen_shader.render() 48 | 49 | #Render the shader directly onto the display. 50 | shader.render_direct(pygame.Rect(0, 0, 100, 100)) 51 | 52 | #Update the opengl context 53 | pygame.display.flip() 54 | clock.tick(60) 55 | ``` -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/assets/img_shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptLineStudios/pygame_shaders/776837f204b88e7b3803bef35deb36d8abd1494b/docs/assets/img_shader.png -------------------------------------------------------------------------------- /docs/assets/pfp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptLineStudios/pygame_shaders/776837f204b88e7b3803bef35deb36d8abd1494b/docs/assets/pfp.png -------------------------------------------------------------------------------- /docs/assets/screen_shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptLineStudios/pygame_shaders/776837f204b88e7b3803bef35deb36d8abd1494b/docs/assets/screen_shader.png -------------------------------------------------------------------------------- /docs/compute_shaders.rst: -------------------------------------------------------------------------------- 1 | Compute Shaders 2 | ================= 3 | 4 | Compute shaders are great for arbitray calculations which can benifit from being computed on the GPU in parallel. pygame_shaders provides an easy API to spin up your first compute shader in no time. 5 | 6 | Lets start in a new python file: 7 | 8 | .. code-block:: python 9 | 10 | import pygame 11 | import pygame_shaders 12 | 13 | pygame.init() 14 | 15 | clock = pygame.time.Clock() 16 | 17 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 18 | 19 | display = pygame.Surface((600, 600)) 20 | 21 | screen_shader = pygame_shaders.DefaultScreenShader(display) 22 | compute_shader = pygame_shaders.ComputeShader("compute.glsl") 23 | 24 | surf = pygame.Surface((600, 600)) 25 | 26 | texture = pygame_shaders.Texture(surf, compute_shader.ctx) 27 | dt = 0 28 | while True: 29 | display.fill((255, 255, 255)) 30 | 31 | for event in pygame.event.get(): 32 | if event.type == pygame.QUIT: 33 | pygame.quit() 34 | running = False 35 | 36 | texture.bind(0) 37 | compute_shader.dispatch(600, 600, 1) 38 | 39 | display.blit(texture.as_surface(), (0, 0)) 40 | 41 | screen_shader.render() 42 | 43 | pygame.display.flip() 44 | 45 | clock.tick() 46 | 47 | 48 | We start with the usual pygame/pygame_shaders setup. Create an OpenGL pygame display, a surface to render onto, and screen shader to display it. We then create a 2 things, a ComputeShader and a Texture. The ComputeShader object will be responsible for dispatching our compute shader when the time is right, 49 | and the texture will be used to hold the result of the compute shader. Speaking of the compute shader, here it is: 50 | 51 | .. code-block:: glsl 52 | 53 | #version 460 core 54 | layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; 55 | layout(rgba32f, binding = 0) uniform image2D screen; 56 | void main() 57 | { 58 | vec4 pixel = vec4(1.0, 0.0, 0.0, 1.0); 59 | ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); 60 | 61 | imageStore(screen, pixel_coords, pixel); 62 | } 63 | 64 | Essetially what this shader is doing, is receiving an image which is bound to the GPU via slot 0. We are then calculating 65 | the current pixel based on the coordinates of the current global invocation. And we are setting the color of that pixel to a brigh red. Not the most interseting usage for a compute shader, but good enough for a simple use case example. 66 | 67 | Back in the python code, in our game loop we first bind our texture to the same binding slot we specified in the compute shader (0 in this case) we then dispatch our compute shader with the dimensions, 600, 600, 1 these are the same dimensions as 68 | the surface which we are using for our Texture. Once we have dispatched and the compute shader has run, we can take our texture, convert it to a pygame Surface and blit it onto our display. This gives the desired result! -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'pygame_shaders' 21 | copyright = '2024, ScriptLine' 22 | author = 'ScriptLine' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.1.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.duration", 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.coverage", 37 | "sphinx.ext.intersphinx", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | # html_theme = "furo" 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pygame_shaders documentation master file, created by 2 | sphinx-quickstart on Wed Jun 28 22:29:13 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pygame_shaders's documentation! 7 | ========================================== 8 | 9 | pygame_shaders is a simple and easy to use way to integrate GLSL shaders into new 10 | or existing pygame projects. 11 | 12 | Features 13 | ================== 14 | * Support for Frgament, Vertex and Compute GLSL shaders. 15 | * Ability to apply shaders to pygame surfaces, as well as direct rendering onto pygame display. 16 | * Easy wrapper around moderngl textures, allowing for easy communication between OpenGL textures and pygame surfaces. 17 | 18 | Installation 19 | ================== 20 | 21 | .. code-block:: console 22 | 23 | pip install pygame_shaders 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | :caption: Table of contents: 28 | 29 | quick_start 30 | writing_shaders 31 | screen_shaders 32 | surface_shaders 33 | compute_shaders 34 | modules 35 | 36 | Indices and tables 37 | ================== 38 | 39 | * :ref:`genindex` 40 | * :ref:`modindex` 41 | * :ref:`search` 42 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============== 3 | 4 | .. autosummary:: 5 | :toctree: generated 6 | 7 | pygame_shaders 8 | -------------------------------------------------------------------------------- /docs/pygame_shaders.rst: -------------------------------------------------------------------------------- 1 | pygame\_shaders package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | pygame\_shaders.pygame\_shaders module 8 | -------------------------------------- 9 | 10 | .. automodule:: pygame_shaders.pygame_shaders 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pygame\_shaders.texture module 16 | -------------------------------------- 17 | 18 | .. automodule:: pygame_shaders.texture 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start Guide 2 | =================== 3 | 4 | The most simple pygame_shaders project is as follows: 5 | 6 | .. code-block:: python 7 | 8 | import pygame 9 | import pygame_shaders 10 | 11 | pygame.init() 12 | 13 | clock = pygame.time.Clock() 14 | 15 | #Create an opengl pygame Surface, this will act as our opengl context. 16 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 17 | 18 | #This is our main display we will do all of our standard pygame rendering on. 19 | display = pygame.Surface((600, 600)) 20 | 21 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 22 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 23 | 24 | while True: 25 | #Fill the display with white 26 | display.fill((255, 255, 255)) 27 | 28 | #Standard pygame event stuff 29 | for event in pygame.event.get(): 30 | if event.type == pygame.QUIT: 31 | pygame.quit() 32 | 33 | #Render a rect onto the display using the standard pygame method for drawing rects. 34 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 35 | 36 | #Render the contents of "display" (main surface) onto the opengl screen. 37 | screen_shader.render() 38 | 39 | #Update the opengl context 40 | pygame.display.flip() 41 | clock.tick(60) 42 | 43 | In this example, we are simpily setting up a DefaultScreenShader, this will be responsible for 44 | taking the contents of the main pygame surface, which is where we will doing all our regular pygame drawing stuff; and 45 | rendering it onto the OpenGL context. In this example we don't actually do any actual shader rendering, 46 | just taking the Surface and putting it directly onto the OpenGL context. In order to do that, lets create a 47 | new shader: 48 | 49 | .. code-block:: python 50 | 51 | import pygame 52 | import pygame_shaders 53 | 54 | pygame.init() 55 | 56 | clock = pygame.time.Clock() 57 | 58 | #Create an opengl pygame Surface, this will act as our opengl context. 59 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 60 | 61 | #This is our main display we will do all of our standard pygame rendering on. 62 | display = pygame.Surface((600, 600)) 63 | 64 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 65 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 66 | 67 | #This is our shader object which we can use to render the given shaders onto the screen in various ways. 68 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "fragment.glsl", screen) #<- Because we plan on using this shader for direct rendering (we supply the surface on which we plan to do said direct rendering in this case, screen) 69 | 70 | while True: 71 | #Fill the display with white 72 | display.fill((255, 255, 255)) 73 | 74 | #Standard pygame event stuff 75 | for event in pygame.event.get(): 76 | if event.type == pygame.QUIT: 77 | pygame.quit() 78 | 79 | #Render a rect onto the display using the standard pygame method for drawing rects. 80 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 81 | 82 | #Render the contents of "display" (main surface) onto the opengl screen. 83 | screen_shader.render() 84 | 85 | #Render the shader directly onto the display. 86 | shader.render_direct(pygame.Rect(0, 0, 100, 100)) 87 | 88 | #Update the opengl context 89 | pygame.display.flip() 90 | clock.tick(60) 91 | 92 | Here we add onto the previous example with a new pygame_shaders.Shader object which we will use by drawing the 93 | result of the shader directly onto the OpenGL context. 94 | 95 | fragment.glsl: 96 | 97 | .. code-block:: glsl 98 | 99 | #version 330 100 | 101 | //Provided by the pygame_shaders library. Do not modify... 102 | in vec3 fragmentColor; 103 | in vec2 fragmentTexCoord; 104 | uniform sampler2D imageTexture; 105 | 106 | //Color output of the shader 107 | out vec4 color; 108 | 109 | //Note: Add your custom uniforms and variables here. 110 | 111 | void main() { 112 | color = vec4(fragmentTexCoord.x, fragmentTexCoord.y, 0.0, 1.0); 113 | } 114 | 115 | Notice in the example the usage of .render_direct() this will render the outputted shader directly onto the OpenGL context at the position specified 116 | via the pygame.Rect object. An alternative method is to produce a pygame.Surface object: 117 | 118 | .. code-block:: python 119 | 120 | import pygame 121 | import pygame_shaders 122 | 123 | pygame.init() 124 | 125 | clock = pygame.time.Clock() 126 | 127 | #Create an opengl pygame Surface, this will act as our opengl context. 128 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 129 | 130 | #This is our main display we will do all of our standard pygame rendering on. 131 | display = pygame.Surface((600, 600)) 132 | 133 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 134 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 135 | 136 | shader_res = pygame.Surface((200, 200)) 137 | #This is our shader object which we can use to render the given shaders onto the screen in various ways. 138 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "fragment.glsl", shader_res) 139 | 140 | while True: 141 | #Fill the display with white 142 | display.fill((255, 255, 255)) 143 | 144 | #Standard pygame event stuff 145 | for event in pygame.event.get(): 146 | if event.type == pygame.QUIT: 147 | pygame.quit() 148 | 149 | #Render a rect onto the display using the standard pygame method for drawing rects. 150 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 151 | 152 | shader_res = shader.render() 153 | display.blit(shader_res, (0, 0)) 154 | 155 | #Render the contents of "display" (main surface) onto the opengl screen. 156 | screen_shader.render() 157 | 158 | #Update the opengl context 159 | pygame.display.flip() 160 | clock.tick(60) 161 | 162 | 163 | -------------------------------------------------------------------------------- /docs/screen_shaders.rst: -------------------------------------------------------------------------------- 1 | Screen Shaders 2 | =================== 3 | 4 | The most basic pygame_shaders shader type is the DefaultScreenShader, this shader allows us to draw a pygame surface 5 | directly onto an OpenGL initialized display. 6 | 7 | .. code-block:: python 8 | 9 | #An opengl initialized display 10 | screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.OPENGL | pygame.DOUBLEBUF) 11 | 12 | #This will be the display to which we do regular pygame drawing. 13 | display = pygame.Surface((WIDTH, HEIGHT)) 14 | 15 | #In order to draw the contents of display onto the OpenGL display, we need a ScreenShader 16 | screen_shader = pygame_shaders.DefaultScreenShader(display) #Here we specify the surface which we want to place onto the OpenGL display. 17 | 18 | With our created screen shader, we now have the ability to render whatever we place on our display surface onto the OpenGL display. 19 | 20 | .. code-block:: python 21 | 22 | #Your previous code here... 23 | 24 | screen_shader.render() 25 | 26 | clock.tick() 27 | pygame.display.flip() 28 | 29 | While the DefaultScreenShader can be useful to quickly get the contents of the display rendered. It will not provide sufficient if the user wants to apply a custom shader to the display before rendering it. For that we are going to need to setup a custom shader. 30 | 31 | .. code-block:: python 32 | 33 | import pygame 34 | import pygame_shaders 35 | import glm 36 | 37 | pygame.init() 38 | 39 | clock = pygame.time.Clock() 40 | 41 | #Create an opengl pygame Surface, this will act as our opengl context. 42 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 43 | 44 | #This is our main display we will do all of our standard pygame rendering on. 45 | display = pygame.Surface((600, 600)) 46 | 47 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 48 | screen_shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "screen_frag.glsl", display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 49 | 50 | while True: 51 | #Fill the display with white 52 | display.fill((255, 255, 255)) 53 | 54 | #Standard pygame event stuff 55 | for event in pygame.event.get(): 56 | if event.type == pygame.QUIT: 57 | pygame.quit() 58 | 59 | #Render a rect onto the display using the standard pygame method for drawing rects. 60 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 61 | 62 | #Render the contents of "display" (main surface) onto the opengl screen. 63 | screen_shader.render_direct(pygame.Rect(0, 0, 600, 600)) 64 | 65 | #Update the opengl context 66 | pygame.display.flip() 67 | clock.tick(60) 68 | 69 | screen_frag.glsl: 70 | 71 | .. code-block:: glsl 72 | 73 | #version 330 core 74 | uniform sampler2D image; 75 | 76 | out vec4 color; 77 | in vec2 fragmentTexCoord; 78 | 79 | void main() { 80 | vec2 center = vec2(0.5, 0.5); 81 | vec2 off_center = fragmentTexCoord - center; 82 | 83 | off_center *= 1.0 + 8.8 * pow(abs(off_center.yx), vec2(5.5)); 84 | 85 | vec2 v_text2 = center+off_center; 86 | 87 | if (v_text2.x > 1.0 || v_text2.x < 0.0 || v_text2.y > 1.0 || v_text2.y < 0.0) 88 | color = vec4(0.0, 0.0, 0.0, 1.0); 89 | else 90 | color = vec4(texture(image, v_text2).rgb, 1.0); 91 | } 92 | 93 | produces: 94 | 95 | .. image:: assets/screen_shader.png 96 | 97 | Here we are essentially creating our own version of the screen shader class. We are once again using render_direct in order to render the contents of the given pygame surface (display in this case) onto the OpenGL display. 98 | We are specifying our own fragment shader which instead of simpily calling texture() on the given sampler will additionally mesaure the distance of the current pixel to the center of the screen and make changes accordingly (In this case giving a stretched effect.) -------------------------------------------------------------------------------- /docs/surface_shaders.rst: -------------------------------------------------------------------------------- 1 | Surface Shaders 2 | ================= 3 | 4 | So far we have looked at how we can apply shaders to pygame Surface objects and render them directly onto the OpenGL initialized display. However what if we instead want to produce a new pygame Surface object for usage elsewhere? 5 | 6 | Instead of calling ``Shader.render_direct()`` to draw our shader, lets instead create a new "target surface" which we will give to our shader when we create it, and can later use for rendering: 7 | 8 | .. code-block:: python 9 | 10 | import pygame 11 | import pygame_shaders 12 | import glm 13 | 14 | pygame.init() 15 | 16 | clock = pygame.time.Clock() 17 | 18 | #Create an opengl pygame Surface, this will act as our opengl context. 19 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 20 | 21 | #This is our main display we will do all of our standard pygame rendering on. 22 | display = pygame.Surface((600, 600)) 23 | 24 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 25 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 26 | 27 | #create our target surface 28 | target_surface = pygame.Surface((200, 200)) 29 | 30 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "custom_frag.glsl", target_surface) #<- give it to our shader 31 | 32 | while True: 33 | #Fill the display with white 34 | display.fill((255, 255, 255)) 35 | 36 | #Standard pygame event stuff 37 | for event in pygame.event.get(): 38 | if event.type == pygame.QUIT: 39 | pygame.quit() 40 | 41 | #Render a rect onto the display using the standard pygame method for drawing rects. 42 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 43 | 44 | #Render the shader onto the surface object 45 | target_shader = shader.render() 46 | 47 | #Blit the new (shader applied!) surface onto the display 48 | display.blit(target_shader, (0, 0)) 49 | 50 | #Render the contents of "display" (main surface) onto the opengl screen. 51 | screen_shader.render() 52 | 53 | #Update the opengl context 54 | pygame.display.flip() 55 | clock.tick(60) 56 | 57 | you may notice here that unlike when we were using render_direct we place the call to shader render above the call to the screen shader render, thats because here the process of putting a shader onto the screen become a `part` of the regular pygame drawing process (since we are using a regular pygame blit to display it) and hence must be done `before` we render the display onto the OpenGL pygame Surface. 58 | 59 | 60 | Lets run through another (more concrete) example. Imagine we have an image: 61 | 62 | .. image:: assets/pfp.png 63 | 64 | which we want to render with a shader. Lets start by writing our custom fragment shader: 65 | 66 | custom_frag.glsl: 67 | 68 | .. code-block:: glsl 69 | 70 | #version 330 71 | 72 | //Provided by the pygame_shaders library. Do not modify... 73 | in vec3 fragmentColor; 74 | in vec2 fragmentTexCoord; 75 | uniform sampler2D imageTexture; 76 | 77 | //Color output of the shader 78 | out vec4 color; 79 | 80 | //Note: Add your custom uniforms and variables here. 81 | 82 | void main() { 83 | color = texture(imageTexture, -fragmentTexCoord) * fragmentTexCoord.x; 84 | } 85 | 86 | This shader will essentially render our image as normal (we are negating the provided fragment tex coord to account for the OpenGL coordiante system) but we additionally multiply the provided texture color with the x value of the current pixel coordinate. 87 | 88 | Hooking this up to the previous program is simple. All we need to do is blit our image onto the surface. 89 | 90 | .. code-block:: python 91 | 92 | #create our target surface 93 | target_surface = pygame.Surface((200, 200)) 94 | target_surface.blit(pygame.image.load("image.png")) 95 | 96 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "custom_frag.glsl", target_surface) #<- give it to our shader 97 | 98 | Run our new program and voila! 99 | 100 | .. image:: assets/img_shader.png -------------------------------------------------------------------------------- /docs/writing_shaders.rst: -------------------------------------------------------------------------------- 1 | Writing Shaders 2 | ================= 3 | 4 | pygame_shaders provides support for the integartion of GLSL vertex, fragment and compute shaders into pygame projects. Typically GLSL shaders will end in the extenison ``.glsl`` In the case of pygame_shaders, the typical starting point of your shaders will look like this: 5 | 6 | vertex.glsl: 7 | 8 | .. code-block:: glsl 9 | 10 | #version 330 core //version 11 | 12 | /* 13 | pygame_shaders provides a vec3 which repesents the current vertex positon and a vec2 14 | representing the current texture coordinate on layout location 0 and 1 respectively. 15 | */ 16 | layout (location = 0) in vec3 vertexPos; 17 | layout (location = 1) in vec2 vertexTexCoord; 18 | 19 | // the vertex shader outputs a fragment texture coordinate. 20 | out vec2 fragmentTexCoord; 21 | 22 | //add your own variables here. 23 | 24 | void main() 25 | { 26 | fragmentTexCoord = vertexTexCoord; //set the fragment tex coord to the texture coordinate 27 | gl_Position = vec4(vertexPos, 1.0); //position the vertex at the vertex position. 28 | } 29 | 30 | fragment.glsl: 31 | 32 | .. code-block:: glsl 33 | 34 | #version 330 core //version 35 | 36 | in vec3 fragmentColor; // The color of the current coordinate/ 37 | in vec2 fragmentTexCoord; // The texture coordinate which we will use in the sampler2D lookup. 38 | 39 | out vec4 color; // The color we are outputting. 40 | 41 | uniform sampler2D imageTexture; // The texture which the shader is provided. 42 | 43 | void main() { 44 | color = texture(imageTexture, fragmentTexCoord); //Peform the above desribed lookup and output it to the color. 45 | } 46 | 47 | While the names of library provided variables are up to you to decide. pygame_shaders provides a DEFAULT_VERTEX_SHADER and DEFAULT_FRAGMENT_SHADER, if one of these default shaders is in use, you will have to adhere to the above naming. 48 | 49 | A default compute shader is slightly more flexible: 50 | 51 | compute.glsl: 52 | 53 | .. code-block:: glsl 54 | 55 | #version 460 core //compute shaders always need version 46o over above 56 | layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; // The number of threads you want to invoke. (Can be changed to suit the needs of your computations) 57 | 58 | layout(rgba32f, binding = 0) uniform image2D screen; // Typically a compute shader will take in an image via a binding as input (once again, not required can be modified based on your needs) 59 | 60 | void main() { 61 | // Whatever you like :D 62 | } -------------------------------------------------------------------------------- /examples/cellular automata/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec3 fragmentColor; 4 | in vec2 fragmentTexCoord; 5 | 6 | out vec4 color; 7 | 8 | uniform sampler2D imageTexture; 9 | 10 | vec2 res = vec2(100, 100); 11 | 12 | float grid(float x, float y) 13 | { 14 | float tx = x/res.x; 15 | float ty = y/res.y; 16 | vec4 t = texture(imageTexture, vec2(tx, ty)); 17 | if (t.y > 0.5) { 18 | return 1.0; 19 | } 20 | else { 21 | return 0.0; 22 | } 23 | } 24 | int birth_limit = 4; 25 | 26 | void main() { 27 | float cx = fragmentTexCoord.x*res.x; 28 | float cy = (1 - fragmentTexCoord.y)*res.y; 29 | 30 | float alive_cells = 0; 31 | 32 | for (float x = cx - 1; x <= cx + 1; x++) { 33 | for (float y = cy - 1; y <= cy + 1; y++) { 34 | if (x >= 0 && x < res.x && y >= 0 && y < res.y) { 35 | alive_cells += grid(x, y); 36 | } 37 | else { 38 | alive_cells++; 39 | } 40 | } 41 | } 42 | if (alive_cells > 4) { 43 | color = vec4(0, 0.8, 0, 1); //alive 44 | } 45 | else if (alive_cells < 4) { 46 | color = vec4(0, 0, 0.5, 1); //dead 47 | } 48 | } -------------------------------------------------------------------------------- /examples/cellular automata/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import math 4 | import random 5 | 6 | pygame.init() 7 | 8 | clock = pygame.time.Clock() 9 | 10 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 11 | 12 | display = pygame.Surface((600, 600)) 13 | 14 | img = pygame.Surface((100, 100)) 15 | for y in range(100): 16 | for x in range(100): 17 | img.set_at((x, y), (255, 255, 255)) 18 | if (x == 0 or x == 100 - 1 or y == 0 or y == 100 - 1): 19 | img.set_at((x, y), (255, 255, 255)) 20 | else: 21 | if random.randrange(0, 100) < 50: 22 | img.set_at((x, y), (0, 0, 0)) 23 | 24 | shader_res = [img, img] 25 | 26 | bg_shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "frag.glsl", shader_res[0]) 27 | screen_shader = pygame_shaders.DefaultScreenShader(display) 28 | 29 | running = True 30 | i = 0 31 | count = 0 32 | while running: 33 | display.fill((255, 255, 255)) 34 | 35 | for event in pygame.event.get(): 36 | if event.type == pygame.QUIT: 37 | pygame.quit() 38 | running = False 39 | 40 | #Set the target surface of the shader to the flipped version of i 41 | bg_shader.set_target_surface(shader_res[1 - i]) 42 | 43 | #Peform the rendering of the shader onto the surface set above 44 | new = bg_shader.render() 45 | 46 | #Set the index i to the new surface 47 | shader_res[i] = new 48 | 49 | #If i = 0 then we set surface 1 as the target, perfom the shader render and store the resulting surface in slot 0 we then flip i (i = 1) and blit the new i onto display. 50 | 51 | #Flip i 52 | i = 1 - i 53 | count += 1 54 | 55 | #display the outputted shader onto the display 56 | display.blit(pygame.transform.scale(shader_res[1], (600, 600)), (0, 0)) 57 | # pygame.draw.rect(display, (255, 0, 0), (0, 0, 600, 600)) 58 | 59 | #Render the contents of display onto the opengl display 60 | screen_shader.render() 61 | 62 | #Update the opengl display 63 | pygame.display.flip() 64 | 65 | #Tick the clock 66 | clock.tick() 67 | pygame.display.set_caption(f"{clock.get_fps()}") -------------------------------------------------------------------------------- /examples/compute/compute.glsl: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; 3 | layout(rgba32f, binding = 0) uniform image2D screen; 4 | void main() 5 | { 6 | vec4 pixel = vec4(0.075, 0.133, 0.173, 1.0); 7 | ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); 8 | 9 | ivec2 dims = imageSize(screen); 10 | float x = -(float(pixel_coords.x * 2 - dims.x) / dims.x); // transforms to [-1.0, 1.0] 11 | float y = -(float(pixel_coords.y * 2 - dims.y) / dims.y); // transforms to [-1.0, 1.0] 12 | 13 | float fov = 90.0; 14 | vec3 cam_o = vec3(0.0, 0.0, -tan(fov / 2.0)); 15 | vec3 ray_o = vec3(x, y, 0.0); 16 | vec3 ray_d = normalize(ray_o - cam_o); 17 | 18 | vec3 sphere_c = vec3(0.0, 0.0, -5.0); 19 | float sphere_r = 1.0; 20 | 21 | vec3 o_c = ray_o - sphere_c; 22 | float b = dot(ray_d, o_c); 23 | float c = dot(o_c, o_c) - sphere_r * sphere_r; 24 | float intersectionState = b * b - c; 25 | vec3 intersection = ray_o + ray_d * (-b + sqrt(b * b - c)); 26 | 27 | if (intersectionState >= 0.0) 28 | { 29 | pixel = vec4((normalize(intersection - sphere_c) + 1.0) / 2.0, 1.0); 30 | } 31 | 32 | imageStore(screen, pixel_coords, pixel); 33 | } -------------------------------------------------------------------------------- /examples/compute/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | 4 | pygame.init() 5 | 6 | clock = pygame.time.Clock() 7 | 8 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 9 | 10 | display = pygame.Surface((600, 600)) 11 | 12 | screen_shader = pygame_shaders.DefaultScreenShader(display) 13 | compute_shader = pygame_shaders.ComputeShader("compute.glsl") 14 | 15 | surf = pygame.Surface((600, 600)) 16 | 17 | surface_shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, pygame_shaders.DEFAULT_FRAGMENT_SHADER, surf) 18 | 19 | texture = pygame_shaders.Texture(surf, compute_shader.ctx) 20 | dt = 0 21 | while True: 22 | display.fill((255, 255, 255)) 23 | 24 | for event in pygame.event.get(): 25 | if event.type == pygame.QUIT: 26 | pygame.quit() 27 | running = False 28 | 29 | texture.bind(0) 30 | compute_shader.dispatch(600, 600, 1) 31 | 32 | screen_shader.render() 33 | 34 | surface_shader.set_target_texture(texture) 35 | surface_shader.render_direct(pygame.Rect(0, 0, 600, 600), False) 36 | 37 | pygame.display.flip() 38 | 39 | clock.tick() 40 | -------------------------------------------------------------------------------- /examples/custom_screen_shader/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import glm 4 | 5 | pygame.init() 6 | 7 | clock = pygame.time.Clock() 8 | 9 | #Create an opengl pygame Surface, this will act as our opengl context. 10 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 11 | 12 | #This is our main display we will do all of our standard pygame rendering on. 13 | display = pygame.Surface((600, 600)) 14 | 15 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 16 | screen_shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "screen_frag.glsl", display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 17 | 18 | while True: 19 | #Fill the display with white 20 | display.fill((40, 60, 100)) 21 | 22 | #Standard pygame event stuff 23 | for event in pygame.event.get(): 24 | if event.type == pygame.QUIT: 25 | pygame.quit() 26 | 27 | #Render a rect onto the display using the standard pygame method for drawing rects. 28 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 29 | 30 | #Render the contents of "display" (main surface) onto the opengl screen. 31 | screen_shader.render_direct(pygame.Rect(0, 0, 600, 600)) 32 | 33 | #Update the opengl context 34 | pygame.display.flip() 35 | clock.tick(60) 36 | -------------------------------------------------------------------------------- /examples/custom_screen_shader/screen_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | uniform sampler2D image; 3 | 4 | out vec4 color; 5 | in vec2 fragmentTexCoord; 6 | 7 | void main() { 8 | vec2 center = vec2(0.5, 0.5); 9 | vec2 off_center = fragmentTexCoord - center; 10 | 11 | off_center *= 1.0 + 8.8 * pow(abs(off_center.yx), vec2(5.5)); 12 | 13 | vec2 v_text2 = center+off_center; 14 | 15 | if (v_text2.x > 1.0 || v_text2.x < 0.0 || v_text2.y > 1.0 || v_text2.y < 0.0) 16 | color = vec4(0.0, 0.0, 0.0, 1.0); 17 | else 18 | color = vec4(texture(image, v_text2).rgb, 1.0); 19 | } -------------------------------------------------------------------------------- /examples/custom_screen_shader/screen_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec3 vertexPos; 4 | layout (location = 1) in vec2 vertexTexCoord; 5 | 6 | out vec2 fragmentTexCoord; 7 | 8 | void main() 9 | { 10 | fragmentTexCoord = vertexTexCoord; 11 | gl_Position = vec4(vertexPos, 1.0); 12 | } -------------------------------------------------------------------------------- /examples/helloworld_shader/default_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | //Provided by the pygame_shaders library. Do not modify... 4 | in vec3 fragmentColor; 5 | in vec2 fragmentTexCoord; 6 | uniform sampler2D imageTexture; 7 | 8 | //Color output of the shader 9 | out vec4 color; 10 | 11 | //Note: Add your custom uniforms and variables here. 12 | 13 | void main() { 14 | color = texture(imageTexture, fragmentTexCoord) * 0.5 - fragmentTexCoord.x / 2; 15 | } 16 | -------------------------------------------------------------------------------- /examples/helloworld_shader/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import glm 4 | 5 | pygame.init() 6 | 7 | clock = pygame.time.Clock() 8 | 9 | #Create an opengl pygame Surface, this will act as our opengl context. 10 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 11 | 12 | #This is our main display we will do all of our standard pygame rendering on. 13 | display = pygame.Surface((600, 600)) 14 | 15 | #The shader we are using to communicate with the opengl context (standard pygame drawing functionality does not work on opengl displays) 16 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 17 | 18 | #create our target surface 19 | target_surface = pygame.Surface((200, 200)) 20 | target_surface.blit(pygame.image.load("../../docs/assets/pfp.png"), (0, 0)) 21 | 22 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "default_frag.glsl", target_surface) #<- give it to our shader 23 | 24 | while True: 25 | #Fill the display with white 26 | display.fill((255, 255, 255)) 27 | 28 | #Standard pygame event stuff 29 | for event in pygame.event.get(): 30 | if event.type == pygame.QUIT: 31 | pygame.quit() 32 | 33 | #Render a rect onto the display using the standard pygame method for drawing rects. 34 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 35 | 36 | #Render the shader onto the surface object 37 | target_shader = shader.render() 38 | 39 | #Blit the new (shader applied!) surface onto the display 40 | display.blit(target_shader, (0, 0)) 41 | 42 | #Render the contents of "display" (main surface) onto the opengl screen. 43 | screen_shader.render() 44 | 45 | #Update the opengl context 46 | pygame.display.flip() 47 | clock.tick(60) 48 | -------------------------------------------------------------------------------- /examples/helloworld_shader/vertex.txt: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | layout (location=0) in vec3 vertexPos; 4 | layout (location=1) in vec2 vertexTexCoord; 5 | 6 | out vec3 fragmentColor; 7 | out vec2 fragmentTexCoord; 8 | 9 | void main() 10 | { 11 | gl_Position = vec4(vertexPos.xyz, 1.0); 12 | fragmentTexCoord = vertexTexCoord; 13 | } 14 | -------------------------------------------------------------------------------- /examples/rotation/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import glm 4 | 5 | pygame.init() 6 | 7 | clock = pygame.time.Clock() 8 | 9 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 10 | 11 | display = pygame.Surface((600, 600)) 12 | 13 | screen_shader = pygame_shaders.Shader("rotate.glsl", 14 | pygame_shaders.DEFAULT_FRAGMENT_SHADER, display) 15 | 16 | img = pygame.transform.flip(pygame.image.load("../../docs/assets/pfp.png"), False, True) 17 | 18 | target_surface = pygame.Surface((200, 200)) 19 | target_surface.blit(img, (0, 0)) 20 | 21 | shader = pygame_shaders.Shader("rotate.glsl", pygame_shaders.DEFAULT_FRAGMENT_SHADER, target_surface) #<- give it to our shader 22 | rotation = glm.mat4() 23 | dt = 0 24 | 25 | while True: 26 | shader.clear((0, 0, 0)) 27 | display.fill((10, 20, 30)) 28 | 29 | target_surface.blit(img, (0, 0)) 30 | 31 | for event in pygame.event.get(): 32 | if event.type == pygame.QUIT: 33 | pygame.quit() 34 | 35 | dt += 0.05 36 | 37 | rotation = glm.mat4() 38 | rotation = glm.rotate(rotation, dt, glm.vec3(1, 1, 1)) 39 | shader.send("rotation", [*rotation[0], *rotation[1], *rotation[2], *rotation[3]]) 40 | 41 | rotation = glm.mat4() 42 | rotation = glm.rotate(rotation, dt, glm.vec3(-2, -2, -2)) 43 | screen_shader.send("rotation", [*rotation[0], *rotation[1], *rotation[2], *rotation[3]]) 44 | 45 | screen_shader.render_direct(pygame.Rect(0, 0, 600, 600)) 46 | shader.render_direct(pygame.Rect(0, 0, 100, 100)) 47 | 48 | pygame.display.flip() 49 | clock.tick(60) 50 | -------------------------------------------------------------------------------- /examples/rotation/rotate.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec3 vertexPos; 4 | layout (location = 1) in vec2 vertexTexCoord; 5 | 6 | out vec2 fragmentTexCoord; 7 | 8 | uniform mat4 rotation; 9 | 10 | void main() 11 | { 12 | fragmentTexCoord = vertexTexCoord; 13 | gl_Position = rotation * vec4(vertexPos, 1.0); 14 | } -------------------------------------------------------------------------------- /examples/simple/effect.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | //Provided by the pygame_shaders library. Do not modify... 4 | in vec3 fragmentColor; 5 | in vec2 fragmentTexCoord; 6 | uniform sampler2D imageTexture; 7 | 8 | //Color output of the shader 9 | out vec4 color; 10 | 11 | //Note: Add your custom uniforms and variables here. 12 | 13 | uniform float time; 14 | 15 | vec2 res = vec2(1000, 800); 16 | 17 | void main() { 18 | vec2 cPos = (-1.0 + 2.0 * gl_FragCoord.xy / res.xy); 19 | float cLength = length(cPos); 20 | 21 | vec2 uv = gl_FragCoord.xy/res.xy+(cPos/cLength)*cos(cLength*12.0-time*4.0)*0.003; 22 | vec3 col = texture2D(imageTexture, uv).xyz; 23 | 24 | gl_FragColor = vec4(col,1.0); 25 | } 26 | -------------------------------------------------------------------------------- /examples/simple/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | 4 | pygame.init() 5 | 6 | display = pygame.display.set_mode((1000, 800), pygame.DOUBLEBUF | pygame.OPENGL) 7 | clock = pygame.time.Clock() 8 | 9 | display = pygame.Surface((1000, 800)) 10 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "effect.glsl", display) 11 | 12 | image = pygame.image.load("../../docs/assets/pfp.png") 13 | 14 | font = pygame.font.SysFont("Arial", 32) 15 | 16 | time = 0 17 | while True: 18 | time += 1/60 19 | display.fill((0, 0, 0)) 20 | 21 | for event in pygame.event.get(): 22 | if event.type == pygame.QUIT: 23 | raise SystemExit 24 | 25 | text = font.render("Hello World! - This font is being modified by a shader", False, "white") 26 | display.blit(text, (40, 600)) 27 | 28 | display.blit(image, (100, 100)) 29 | 30 | shader.send("time", time) 31 | 32 | shader.render_direct(pygame.Rect(0, 0, 1000, 800)) 33 | 34 | pygame.display.flip() 35 | clock.tick(60) -------------------------------------------------------------------------------- /examples/smoothlife/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec3 fragmentColor; 4 | in vec2 fragmentTexCoord; 5 | 6 | out vec4 color; 7 | 8 | uniform sampler2D imageTexture; 9 | 10 | vec2 res = vec2(600, 600); 11 | 12 | 13 | float ra = 20; 14 | 15 | // Stolen from https://www.shadertoy.com/view/XtdSDn 16 | float b1 = 0.257; 17 | float b2 = 0.336; 18 | float d1 = 0.365; 19 | float d2 = 0.549; 20 | float alpha_n = 0.028; 21 | float alpha_m = 0.147; 22 | 23 | float dt = 1.5; 24 | 25 | float sigma(float x, float a, float alpha) 26 | { 27 | return 1.0/(1.0 + exp(-(x - a)*4.0/alpha)); 28 | } 29 | 30 | float sigma_n(float x, float a, float b) 31 | { 32 | return sigma(x, a, alpha_n)*(1.0 - sigma(x, b, alpha_n)); 33 | } 34 | 35 | float sigma_m(float x, float y, float m) 36 | { 37 | return x*(1 - sigma(m, 0.5, alpha_m)) + y*sigma(m, 0.5, alpha_m); 38 | } 39 | 40 | float s(float n, float m) 41 | { 42 | return sigma_n(n, sigma_m(b1, d1, m), sigma_m(b2, d2, m)); 43 | } 44 | 45 | float grid(float x, float y) 46 | { 47 | float tx = x/res.x; 48 | float ty = y/res.y; 49 | vec4 t = texture(imageTexture, vec2(tx, ty)); 50 | return max(max(t.x, t.y), t.z); 51 | } 52 | 53 | // A = πr^2 54 | 55 | #define PI 3.14159265359 56 | 57 | void main() { 58 | float cx = fragmentTexCoord.x*res.x; 59 | float cy = (1 - fragmentTexCoord.y)*res.y; 60 | float ri = ra/3.0; 61 | float m = 0; 62 | float M = PI*ri*ri; 63 | float n = 0; 64 | float N = PI*ra*ra - M; 65 | 66 | for (float dy = -ra; dy <= ra; dy += 1.0) { 67 | for (float dx = -ra; dx <= ra; dx += 1.0) { 68 | float x = cx + dx; 69 | float y = cy + dy; 70 | if (dx*dx + dy*dy <= ri*ri) { 71 | m += grid(x, y); 72 | } else if (dx*dx + dy*dy <= ra*ra) { 73 | n += grid(x, y); 74 | } 75 | } 76 | } 77 | m /= M; 78 | n /= N; 79 | float q = s(n, m); 80 | float diff = 2.0*q - 1.0; 81 | float v = clamp(grid(cx, cy) + dt*diff, 0.0, 1.0); 82 | 83 | color = vec4(v, v, v, 1); 84 | } -------------------------------------------------------------------------------- /examples/smoothlife/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import math 4 | import random 5 | 6 | random.seed(601) 7 | pygame.init() 8 | 9 | clock = pygame.time.Clock() 10 | 11 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 12 | 13 | display = pygame.Surface((400, 400)) 14 | 15 | img = pygame.Surface((400, 400)) 16 | for y in range(400): 17 | for x in range(400): 18 | if math.dist([x, y], [200, 200]) < 100: 19 | v = random.randrange(0, 255) 20 | img.set_at((x, y), (v, v, v)) 21 | 22 | shader_res = [img, img] 23 | bg_shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "frag.glsl", shader_res[0]) 24 | 25 | screen_shader = pygame_shaders.DefaultScreenShader(display) 26 | 27 | running = True 28 | dt = 1.0 29 | i = 0 30 | while running: 31 | display.fill((255, 255, 255)) 32 | 33 | dt += .01 34 | 35 | for event in pygame.event.get(): 36 | if event.type == pygame.QUIT: 37 | pygame.quit() 38 | running = False 39 | 40 | bg_shader.set_target_surface(shader_res[1 - i]) 41 | 42 | new = bg_shader.render() 43 | shader_res[i] = new 44 | i = 1 - i 45 | 46 | display.blit(shader_res[1], (0, 0)) 47 | 48 | screen_shader.render() 49 | pygame.display.flip() 50 | 51 | clock.tick(60) 52 | pygame.display.set_caption(f"{clock.get_fps()}") 53 | -------------------------------------------------------------------------------- /examples/water/default_frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | //Provided by the pygame_shaders library. Do not modify... 4 | in vec3 fragmentColor; 5 | in vec2 fragmentTexCoord; 6 | uniform sampler2D imageTexture; 7 | 8 | uniform float time; 9 | 10 | vec2 res = vec2(2, 2); 11 | 12 | void main() 13 | { 14 | vec2 uv = fragmentTexCoord.xy / res.xy + vec2(time / 10, time); 15 | 16 | vec4 texture_color = vec4(0.192156862745098, 0.6627450980392157, 0.9333333333333333, 1.0); 17 | 18 | vec4 k = vec4(time)*0.8; 19 | k.xy = uv * 7.0; 20 | float val1 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5)); 21 | float val2 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.2)); 22 | float val3 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5)); 23 | vec4 color = vec4 ( pow(min(min(val1,val2),val3), 7.0) * 3.0)+texture_color; 24 | gl_FragColor = color; 25 | } 26 | -------------------------------------------------------------------------------- /examples/water/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_shaders 3 | import glm 4 | 5 | pygame.init() 6 | 7 | clock = pygame.time.Clock() 8 | 9 | screen = pygame.display.set_mode((600, 600), pygame.OPENGL | pygame.DOUBLEBUF) 10 | 11 | display = pygame.Surface((600, 600)) 12 | 13 | screen_shader = pygame_shaders.DefaultScreenShader(display) # <- Here we supply our default display, it's this display which will be displayed onto the opengl context via the screen_shader 14 | 15 | target_surface = pygame.Surface((200, 200)) 16 | target_surface.blit(pygame.image.load("../../docs/assets/pfp.png"), (0, 0)) 17 | 18 | shader = pygame_shaders.Shader(pygame_shaders.DEFAULT_VERTEX_SHADER, "default_frag.glsl", target_surface) #<- give it to our shader 19 | 20 | t = 0 21 | while True: 22 | display.fill((255, 255, 255)) 23 | t += 0.1 24 | 25 | for event in pygame.event.get(): 26 | if event.type == pygame.QUIT: 27 | pygame.quit() 28 | 29 | pygame.draw.rect(display, (255, 0, 0), (200, 200, 20, 20)) 30 | 31 | shader.send("time", t) 32 | target_shader = shader.render() 33 | 34 | display.blit(target_shader, (0, 0)) 35 | 36 | screen_shader.render() 37 | 38 | pygame.display.flip() 39 | clock.tick(60) 40 | -------------------------------------------------------------------------------- /examples/water/vertex.txt: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | layout (location=0) in vec3 vertexPos; 4 | layout (location=1) in vec2 vertexTexCoord; 5 | 6 | out vec3 fragmentColor; 7 | out vec2 fragmentTexCoord; 8 | 9 | void main() 10 | { 11 | gl_Position = vec4(vertexPos.xyz, 1.0); 12 | fragmentTexCoord = vertexTexCoord; 13 | } 14 | -------------------------------------------------------------------------------- /pygame_shaders/__init__.py: -------------------------------------------------------------------------------- 1 | from pygame_shaders.pygame_shaders import Shader 2 | from pygame_shaders.pygame_shaders import DefaultScreenShader 3 | from pygame_shaders.pygame_shaders import ComputeShader 4 | from pygame_shaders.pygame_shaders import DEFAULT_FRAGMENT_SHADER 5 | from pygame_shaders.pygame_shaders import DEFAULT_VERTEX_SHADER 6 | from pygame_shaders.texture import Texture 7 | -------------------------------------------------------------------------------- /pygame_shaders/pygame_shaders.py: -------------------------------------------------------------------------------- 1 | import pygame_shaders.texture as texture 2 | import pygame_shaders.screen_rect as screen_rect 3 | import pygame_shaders.shader_utils as shader_utils 4 | 5 | import moderngl 6 | import pygame 7 | import typing 8 | 9 | DEFAULT_VERTEX_SHADER = """ 10 | #version 330 core 11 | 12 | layout (location = 0) in vec3 vertexPos; 13 | layout (location = 1) in vec2 vertexTexCoord; 14 | 15 | out vec2 fragmentTexCoord; 16 | 17 | void main() 18 | { 19 | fragmentTexCoord = vertexTexCoord; 20 | gl_Position = vec4(vertexPos, 1.0); 21 | } 22 | """ 23 | 24 | DEFAULT_FRAGMENT_SHADER = """ 25 | #version 330 core 26 | 27 | in vec3 fragmentColor; 28 | in vec2 fragmentTexCoord; 29 | 30 | out vec4 color; 31 | 32 | uniform sampler2D imageTexture; 33 | 34 | void main() { 35 | color = texture(imageTexture, fragmentTexCoord); 36 | } 37 | """ 38 | 39 | class Shader: 40 | """ 41 | Main shader class responsibe for creating a shader object based on a given vertex and fragment shader. 42 | Takes a path to a glsl vertex shader, fragment shader as well as a target surface. This target surface will 43 | act as the texture to which the shader will be applied. 44 | """ 45 | 46 | @staticmethod 47 | def create_vertfrag_shader(ctx: moderngl.Context, vertex_filepath: str, fragment_filepath: str) -> moderngl.Program: 48 | """ 49 | Create a moderngl shader program containing the shaders at the given filepaths. 50 | """ 51 | 52 | if vertex_filepath != DEFAULT_VERTEX_SHADER: 53 | with open(vertex_filepath,'r') as f: 54 | vertex_src = f.read() 55 | else: 56 | vertex_src = DEFAULT_VERTEX_SHADER 57 | if fragment_filepath != DEFAULT_FRAGMENT_SHADER: 58 | with open(fragment_filepath,'r') as f: 59 | fragment_src = f.read() 60 | else: 61 | fragment_src = DEFAULT_FRAGMENT_SHADER 62 | 63 | shader = ctx.program(vertex_shader=vertex_src, fragment_shader=fragment_src) 64 | return shader 65 | 66 | def __init__(self, vertex_path: str, fragment_path: str, target_surface: pygame.Surface) -> None: 67 | self.ctx = moderngl.create_context() 68 | self.ctx.enable(moderngl.BLEND) 69 | self.ctx.blend_func = self.ctx.SRC_ALPHA, self.ctx.ONE_MINUS_SRC_ALPHA 70 | 71 | self.target_surface = target_surface 72 | 73 | self.shader_data = {} 74 | self.shader = Shader.create_vertfrag_shader(self.ctx, vertex_path, fragment_path) 75 | self.render_rect = screen_rect.ScreenRect(self.target_surface.get_size(),self.target_surface.get_size(), (0, 0), self.ctx, self.shader) 76 | 77 | self.screen_texture = texture.Texture(pygame.Surface(self.target_surface.get_size()), self.ctx) 78 | self.framebuffer = self.ctx.simple_framebuffer(size=self.target_surface.get_size(), components=4) 79 | self.scope = self.ctx.scope(self.framebuffer) 80 | 81 | self.window_size = pygame.display.get_surface().get_size() 82 | 83 | def clear(self, color: typing.Union[pygame.Color, typing.Tuple[int]]) -> None: 84 | """ 85 | Clears the shader and provided 86 | """ 87 | 88 | self.target_surface.fill(color) 89 | self.ctx.clear(color=(color[0]/255, color[1]/255, color[2]/255)) 90 | 91 | def send(self, name: str, data: typing.Any) -> None: 92 | """ 93 | Used to send uniform data to the shader 94 | """ 95 | self.shader[name] = data 96 | 97 | def set_target_surface(self, surface: pygame.Surface) -> None: 98 | """ 99 | Update the current shader texture object with a new pygame Surface object 100 | """ 101 | 102 | # self.screen_texture.texture.release() 103 | self.target_surface = surface 104 | # self.screen_texture = texture.Texture(pygame.Surface(self.target_surface.get_size()), self.ctx) 105 | 106 | def set_target_texture(self, texture: texture.Texture) -> None: 107 | """ 108 | Set the target texture object 109 | """ 110 | 111 | self.screen_texture = texture 112 | 113 | def __upload_uniforms(self) -> None: 114 | for key in self.shader_data.keys(): 115 | data = self.shader_data[key] 116 | if len(data) == 1: 117 | self.shader[key].value = data[0] 118 | 119 | elif len(data) == 2: 120 | self.shader[key].value = (data[0], data[1]) 121 | 122 | def render_direct(self, rect: pygame.Rect, update_surface: bool=True, autoscale: bool=False) -> None: 123 | """ 124 | Render the shader directly onto the opengl context. Instead of rendering onto the shader 125 | onto a surface which we can then perform standard pygame functionality on, we instead render 126 | straight onto the opengl context. 127 | """ 128 | #this rect is in the pygame coordinate system, our goal is to convert it into our custom coordinate systems 129 | #(0,0);pygame -> (-600, 600) in ours 130 | if autoscale: 131 | size = (self.target_surface.get_width(), self.target_surface.get_height()) 132 | else: 133 | size = self.window_size 134 | 135 | rect = screen_rect.ScreenRect.pygame_rect_to_screen_rect(rect, self.target_surface, size) 136 | 137 | # self.__upload_uniforms() 138 | self.render_rect = screen_rect.ScreenRect((rect.w, rect.h), size, (rect.x, rect.y), self.ctx, self.shader) 139 | 140 | if update_surface: 141 | self.screen_texture.update(self.target_surface) 142 | 143 | self.screen_texture.use() 144 | self.render_rect.vao.render() 145 | 146 | def render(self, update_surface: bool=True) -> pygame.Surface: 147 | """ 148 | Render the shader onto a pygame Surface making use of the target surface provided. 149 | """ 150 | 151 | # self.upload_uniforms() 152 | 153 | if update_surface: 154 | self.screen_texture.update(self.target_surface) 155 | self.screen_texture.use() 156 | 157 | with self.scope: 158 | self.framebuffer.use() 159 | self.render_rect.vao.render() 160 | surf = pygame.image.frombuffer(self.framebuffer.read(), self.target_surface.get_size(), "RGB") 161 | return pygame.transform.flip(surf, False, True) 162 | 163 | class ComputeShader: 164 | """ 165 | Shader class responsible for handling a GLSL compute shader. 166 | """ 167 | 168 | @staticmethod 169 | def create_compute_shader(ctx: moderngl.Context, compute_shader_path: str) -> moderngl.ComputeShader: 170 | """ 171 | Returns a moderngl compute shader object 172 | """ 173 | 174 | with open(compute_shader_path) as f: 175 | return ctx.compute_shader(f.read()) 176 | 177 | def __init__(self, computer_shader_path: str) -> None: 178 | self.ctx = moderngl.create_context(require=430) 179 | 180 | self.path = computer_shader_path 181 | self.program = ComputeShader.create_compute_shader(self.ctx, self.path) 182 | 183 | def dispatch(self, x: int, y: int, z: int) -> None: 184 | """ 185 | Run the compute shader with the given dimensions. 186 | """ 187 | 188 | self.program.run(x, y, z) 189 | 190 | class DefaultScreenShader(Shader): 191 | """ 192 | A convinience class used to quickly create a screen shader which can take the contents of a pygame Surface and display it on an OpenGL context display. 193 | """ 194 | 195 | def __init__(self, screen: pygame.Surface) -> None: 196 | super().__init__(DEFAULT_VERTEX_SHADER, DEFAULT_FRAGMENT_SHADER, screen) 197 | 198 | def render(self) -> None: 199 | """ 200 | Render the display onto the OpenGL context 201 | """ 202 | 203 | super().render_direct(pygame.Rect(0, 0, self.target_surface.get_width(), self.target_surface.get_height()), autoscale=True) 204 | -------------------------------------------------------------------------------- /pygame_shaders/screen_rect.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import moderngl 3 | import pygame 4 | 5 | class ScreenRect: 6 | @staticmethod 7 | def pygame_rect_to_screen_rect(rect: pygame.Rect, target_surface: pygame.Surface, size: tuple) -> pygame.Rect: 8 | w = size[0] 9 | h = size[1] 10 | 11 | return pygame.Rect(((rect.x*2) - w) + rect.w, ((rect.y*2) + h) - rect.h, rect.w, rect.h) 12 | 13 | def __init__(self, size, win_size, offset, ctx, program): 14 | self.size = size 15 | offset = (offset[0]/win_size[0], offset[1]/win_size[1]) 16 | self.current_w, self.current_h = win_size 17 | 18 | x = self.size[0] / self.current_w 19 | y = self.size[1] / self.current_h 20 | self.vertices = [ 21 | (-x + offset[0], y + offset[1]), 22 | (x + offset[0], y + offset[1]), 23 | (-x + offset[0], -y + offset[1]), 24 | 25 | (-x + offset[0], -y + offset[1]), 26 | (x + offset[0], y + offset[1]), 27 | (x + offset[0], -y + offset[1]), 28 | ] 29 | self.tex_coords = [ 30 | (0.0, 1.0), 31 | (1.0, 1.0), 32 | (0.0, 0.0), 33 | 34 | (0.0, 0.0), 35 | (1.0, 1.0), 36 | (1.0, 0.0), 37 | ] 38 | 39 | self.vertices = np.array(self.vertices, dtype=np.float32) 40 | self.tex_coords = np.array(self.tex_coords, dtype=np.float32) 41 | self.data = np.hstack([self.vertices, self.tex_coords]) 42 | 43 | self.vertex_count = 6 44 | 45 | self.vbo = ctx.buffer(self.data) 46 | 47 | try: 48 | self.vao = ctx.vertex_array(program, [ 49 | (self.vbo, '2f 2f', 'vertexPos', 'vertexTexCoord'), 50 | ]) 51 | except moderngl.error.Error: 52 | self.vbo = ctx.buffer(self.vertices) 53 | self.vao = ctx.vertex_array(program, [ 54 | (self.vbo, '2f', 'vertexPos'), 55 | ]) 56 | 57 | self.program = program -------------------------------------------------------------------------------- /pygame_shaders/shader_utils.py: -------------------------------------------------------------------------------- 1 | import pygame_shaders 2 | 3 | def create_shader(vertex_filepath, fragment_filepath, ctx): 4 | if vertex_filepath != pygame_shaders.DEFAULT_VERTEX_SHADER: 5 | with open(vertex_filepath,'r') as f: 6 | vertex_src = f.read() 7 | else: 8 | vertex_src = pygame_shaders.DEFAULT_VERTEX_SHADER 9 | if fragment_filepath != pygame_shaders.DEFAULT_FRAGMENT_SHADER: 10 | with open(fragment_filepath,'r') as f: 11 | fragment_src = f.read() 12 | else: 13 | fragment_src = pygame_shaders.DEFAULT_FRAGMENT_SHADER 14 | 15 | shader = ctx.program(vertex_shader=vertex_src, fragment_shader=fragment_src) 16 | return shader -------------------------------------------------------------------------------- /pygame_shaders/texture.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" 3 | import pygame 4 | import moderngl 5 | import typing 6 | 7 | class Texture: 8 | """ 9 | Responsible for handling an OpenGL texture object. 10 | """ 11 | def __init__(self, image: pygame.Surface, ctx: moderngl.Context) -> None: 12 | image = pygame.transform.flip(image, False, True) 13 | self.image_width, self.image_height = image.get_rect().size 14 | img_data = pygame.image.tostring(image, "RGBA") 15 | self.texture = ctx.texture(size=image.get_size(), components=4, data=img_data) 16 | self.texture.filter = (moderngl.NEAREST, moderngl.NEAREST) 17 | 18 | def update(self, image: pygame.Surface) -> None: 19 | """ 20 | Writes the contents of the pygame Surface to OpenGL texture. 21 | """ 22 | 23 | image = pygame.transform.flip(image, False, True) 24 | image_width, image_height = image.get_rect().size 25 | img_data = pygame.image.tostring(image, "RGBA") 26 | 27 | self.texture.write(img_data) 28 | 29 | def as_surface(self) -> pygame.Surface: 30 | """ 31 | Returns the OpenGL texture as a pygame Surface. 32 | """ 33 | 34 | buffer = self.texture.read() 35 | surf = pygame.image.frombuffer(buffer, (self.image_width, self.image_height), "RGBA") 36 | return surf 37 | 38 | def bind(self, unit: int, read: bool=True, write: bool=True) -> None: 39 | """ 40 | Bind the texture to a certain texture slot with given permissions 41 | """ 42 | 43 | self.texture.bind_to_image(unit, read=read, write=write) 44 | 45 | def use(self, _id: typing.Union[None, int] = None) -> None: 46 | """ 47 | Use the texture object for rendering 48 | """ 49 | if not _id: 50 | self.texture.use() 51 | else: 52 | self.texture.use(_id) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pygame_shaders" 3 | version = "2.0.1" 4 | dependencies = ["numpy", "moderngl", "pygame"] 5 | dynamic = ["description", "readme", "authors", "classifiers"] 6 | 7 | [build-system] 8 | requires = [ 9 | "setuptools>=42", 10 | "wheel", 11 | ] 12 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pygame_shaders 3 | version = 2.0.1 4 | author = ScriptLine Studios 5 | author_email = scriptlinestudios@protonmail.com 6 | description = a library to easily integrate shaders into your new or existing pygame projects 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/ScriptLineStudios/pygame_shaders 10 | project_urls = 11 | Bug Tracker = https://github.com/ScriptLineStudios/pygame_shaders/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent --------------------------------------------------------------------------------