Source code for arcade.gl.glsl

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Iterable

from pyglet import gl

if TYPE_CHECKING:
    from .context import Context as ArcadeGlContext

from .exceptions import ShaderException
from .types import SHADER_TYPE_NAMES, PyGLenum


[docs] class ShaderSource: """ GLSL source container for making source parsing simpler. We support locating out attributes, applying ``#defines`` values and injecting common source. .. note::: We do assume the source is neat enough to be parsed this way and don't contain several statements on one line. Args: ctx: The context this framebuffer belongs to source: The GLSL source code common: Common source code to inject source_type: The shader type depth_attachment: A depth attachment (optional) """ def __init__( self, ctx: "ArcadeGlContext", source: str, common: Iterable[str] | None, source_type: PyGLenum, ): self._source = source.strip() self._type = source_type self._lines = self._source.split("\n") if source else [] self._out_attributes: list[str] = [] if not self._lines: raise ValueError("Shader source is empty") self._version = self._find_glsl_version() # GLES specific modifications if ctx.gl_api == "gles": # TODO: Use the version from the context self._lines[0] = "#version 310 es" self._lines.insert(1, "precision mediump float;") if self._type == gl.GL_GEOMETRY_SHADER: self._lines.insert(1, "#extension GL_EXT_geometry_shader : require") if self._type == gl.GL_COMPUTE_SHADER: self._lines.insert(1, "precision mediump image2D;") self._version = self._find_glsl_version() # Inject common source self.inject_common_sources(common) if self._type in [gl.GL_VERTEX_SHADER, gl.GL_GEOMETRY_SHADER]: self._parse_out_attributes() @property def version(self) -> int: """The glsl version""" return self._version @property def out_attributes(self) -> list[str]: """The out attributes for this program""" return self._out_attributes
[docs] def inject_common_sources(self, common: Iterable[str] | None) -> None: """ Inject common source code into the shader source. Args: common: A list of common source code strings to inject """ if not common: return # Find the main function for line_number, line in enumerate(self._lines): if "main()" in line: break else: raise ShaderException("No main() function found when injecting common source") # Insert all common sources for source in common: lines = source.split("\n") self._lines = self._lines[:line_number] + lines + self._lines[line_number:]
[docs] def get_source(self, *, defines: dict[str, str] | None = None) -> str: """Return the shader source Args: defines: Defines to replace in the source. """ if not defines: return "\n".join(self._lines) lines = ShaderSource.apply_defines(self._lines, defines) return "\n".join(lines)
def _find_glsl_version(self) -> int: if self._lines[0].strip().startswith("#version"): try: return int(self._lines[0].split()[1]) except Exception: pass source = "\n".join(f"{str(i + 1).zfill(3)}: {line} " for i, line in enumerate(self._lines)) raise ShaderException( ( "Cannot find #version in shader source. " "Please provide at least a #version 330 statement in the beginning of the shader.\n" f"---- [{SHADER_TYPE_NAMES[self._type]}] ---\n" f"{source}" ) )
[docs] @staticmethod def apply_defines(lines: list[str], defines: dict[str, str]) -> list[str]: """Locate and apply #define values Args: lines: List of source lines defines: dict with ``name: value`` pairs. """ for nr, line in enumerate(lines): line = line.strip() if line.startswith("#define"): try: name = line.split()[1] value = defines.get(name, None) if value is None: continue lines[nr] = "#define {} {}".format(name, str(value)) except IndexError: pass return lines
def _parse_out_attributes(self): """ Locates out attributes so we don't have to manually supply them. Note that this currently doesn't work for structs. """ for line in self._lines: res = re.match(r"(layout(.+)\))?(\s+)?(out)(\s+)(\w+)(\s+)(\w+)", line.strip()) if res: self._out_attributes.append(res.groups()[-1])