Skip to content

Basic support for sampler objects #2445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions arcade/gl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .buffer import Buffer
from .vertex_array import Geometry, VertexArray
from .texture import Texture2D
from .sampler import Sampler
from .framebuffer import Framebuffer
from .program import Program
from .query import Query
Expand All @@ -42,5 +43,6 @@
"ShaderException",
"VertexArray",
"Texture2D",
"Sampler",
"geometry",
]
11 changes: 11 additions & 0 deletions arcade/gl/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .glsl import ShaderSource
from .program import Program
from .query import Query
from .sampler import Sampler
from .texture import Texture2D
from .types import BufferDescription, GLenumLike, PyGLenum
from .vertex_array import Geometry
Expand Down Expand Up @@ -1084,6 +1085,16 @@ def depth_texture(
"""
return Texture2D(self, size, data=data, depth=True)

def sampler(self, texture: Texture2D) -> Sampler:
"""
Create a sampler object for a texture.

Args:
texture:
The texture to create a sampler for
"""
return Sampler(self, texture)

def geometry(
self,
content: Sequence[BufferDescription] | None = None,
Expand Down
227 changes: 227 additions & 0 deletions arcade/gl/sampler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
from __future__ import annotations

import weakref
from ctypes import byref, c_uint32
from typing import TYPE_CHECKING

from pyglet import gl

from .types import PyGLuint, compare_funcs

if TYPE_CHECKING:
from arcade.gl import Context, Texture2D


class Sampler:
"""
OpenGL sampler object.

When bound to a texture unit it overrides all the
sampling parameters of the texture channel.
"""

def __init__(
self,
ctx: "Context",
texture: Texture2D,
*,
filter: tuple[PyGLuint, PyGLuint] | None = None,
wrap_x: PyGLuint | None = None,
wrap_y: PyGLuint | None = None,
):
self._ctx = ctx
self._glo = -1

value = c_uint32()
gl.glGenSamplers(1, byref(value))
self._glo = value.value

self.texture = texture

# Default filters for float and integer textures
# Integer textures should have NEAREST interpolation
# by default 3.3 core doesn't really support it consistently.
if "f" in self.texture._dtype:
self._filter = gl.GL_LINEAR, gl.GL_LINEAR
else:
self._filter = gl.GL_NEAREST, gl.GL_NEAREST

self._wrap_x = gl.GL_REPEAT
self._wrap_y = gl.GL_REPEAT
self._anisotropy = 1.0
self._compare_func: str | None = None

# Only set texture parameters on non-multisample textures
if self.texture._samples == 0:
self.filter = filter or self._filter
self.wrap_x = wrap_x or self._wrap_x
self.wrap_y = wrap_y or self._wrap_y

if self._ctx.gc_mode == "auto":
weakref.finalize(self, Sampler.delete_glo, self._glo)

@property
def glo(self) -> PyGLuint:
"""The OpenGL sampler id"""
return self._glo

def use(self, unit: int):
"""
Bind the sampler to a texture unit
"""
gl.glBindSampler(unit, self._glo)

def clear(self, unit: int):
"""
Unbind the sampler from a texture unit
"""
gl.glBindSampler(unit, 0)

@property
def filter(self) -> tuple[int, int]:
"""
Get or set the ``(min, mag)`` filter for this texture.

These are rules for how a texture interpolates.
The filter is specified for minification and magnification.

Default value is ``LINEAR, LINEAR``.
Can be set to ``NEAREST, NEAREST`` for pixelated graphics.

When mipmapping is used the min filter needs to be one of the
``MIPMAP`` variants.

Accepted values::

# Enums can be accessed on the context or arcade.gl
NEAREST # Nearest pixel
LINEAR # Linear interpolate
NEAREST_MIPMAP_NEAREST # Minification filter for mipmaps
LINEAR_MIPMAP_NEAREST # Minification filter for mipmaps
NEAREST_MIPMAP_LINEAR # Minification filter for mipmaps
LINEAR_MIPMAP_LINEAR # Minification filter for mipmaps

Also see

* https://www.khronos.org/opengl/wiki/Texture#Mip_maps
* https://www.khronos.org/opengl/wiki/Sampler_Object#Filtering
"""
return self._filter

@filter.setter
def filter(self, value: tuple[int, int]):
if not isinstance(value, tuple) or not len(value) == 2:
raise ValueError("Texture filter must be a 2 component tuple (min, mag)")

self._filter = value
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MIN_FILTER, self._filter[0])
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MAG_FILTER, self._filter[1])

@property
def wrap_x(self) -> int:
"""
Get or set the horizontal wrapping of the texture.

This decides how textures are read when texture coordinates are outside
the ``[0.0, 1.0]`` area. Default value is ``REPEAT``.

Valid options are::

# Note: Enums can also be accessed in arcade.gl
# Repeat pixels on the y axis
texture.wrap_x = ctx.REPEAT
# Repeat pixels on the y axis mirrored
texture.wrap_x = ctx.MIRRORED_REPEAT
# Repeat the edge pixels when reading outside the texture
texture.wrap_x = ctx.CLAMP_TO_EDGE
# Use the border color (black by default) when reading outside the texture
texture.wrap_x = ctx.CLAMP_TO_BORDER
"""
return self._wrap_x

@wrap_x.setter
def wrap_x(self, value: int):
self._wrap_x = value
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_S, value)

@property
def wrap_y(self) -> int:
"""
Get or set the horizontal wrapping of the texture.

This decides how textures are read when texture coordinates are outside the
``[0.0, 1.0]`` area. Default value is ``REPEAT``.

Valid options are::

# Note: Enums can also be accessed in arcade.gl
# Repeat pixels on the x axis
texture.wrap_x = ctx.REPEAT
# Repeat pixels on the x axis mirrored
texture.wrap_x = ctx.MIRRORED_REPEAT
# Repeat the edge pixels when reading outside the texture
texture.wrap_x = ctx.CLAMP_TO_EDGE
# Use the border color (black by default) when reading outside the texture
texture.wrap_x = ctx.CLAMP_TO_BORDER
"""
return self._wrap_y

@wrap_y.setter
def wrap_y(self, value: int):
self._wrap_y = value
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_T, value)

@property
def anisotropy(self) -> float:
"""Get or set the anisotropy for this texture."""
return self._anisotropy

@anisotropy.setter
def anisotropy(self, value):
self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY))
gl.glSamplerParameterf(self._glo, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy)

@property
def compare_func(self) -> str | None:
"""
Get or set the compare function for a depth texture::

texture.compare_func = None # Disable depth comparison completely
texture.compare_func = '<=' # GL_LEQUAL
texture.compare_func = '<' # GL_LESS
texture.compare_func = '>=' # GL_GEQUAL
texture.compare_func = '>' # GL_GREATER
texture.compare_func = '==' # GL_EQUAL
texture.compare_func = '!=' # GL_NOTEQUAL
texture.compare_func = '0' # GL_NEVER
texture.compare_func = '1' # GL_ALWAYS
"""
return self._compare_func

@compare_func.setter
def compare_func(self, value: str | None):
if not self.texture._depth:
raise ValueError("Depth comparison function can only be set on depth textures")

if not isinstance(value, str) and value is not None:
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")

func = compare_funcs.get(value, None)
if func is None:
raise ValueError(f"value must be as string: {compare_funcs.keys()}")

self._compare_func = value
if value is None:
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE)
else:
gl.glSamplerParameteri(
self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE
)
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_FUNC, func)

@staticmethod
def delete_glo(glo: int) -> None:
"""
Delete the OpenGL object
"""
gl.glDeleteSamplers(1, glo)
50 changes: 16 additions & 34 deletions arcade/gl/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

from ..types import BufferProtocol
from .buffer import Buffer
from .types import BufferOrBufferProtocol, PyGLuint, pixel_formats
from .types import (
BufferOrBufferProtocol,
PyGLuint,
compare_funcs,
pixel_formats,
swizzle_enum_to_str,
swizzle_str_to_enum,
)
from .utils import data_to_ctypes

if TYPE_CHECKING: # handle import cycle caused by type hinting
Expand Down Expand Up @@ -101,34 +108,6 @@ class Texture2D:
"_compressed",
"_compressed_data",
)
_compare_funcs = {
None: gl.GL_NONE,
"<=": gl.GL_LEQUAL,
"<": gl.GL_LESS,
">=": gl.GL_GEQUAL,
">": gl.GL_GREATER,
"==": gl.GL_EQUAL,
"!=": gl.GL_NOTEQUAL,
"0": gl.GL_NEVER,
"1": gl.GL_ALWAYS,
}
# Swizzle conversion lookup
_swizzle_enum_to_str = {
gl.GL_RED: "R",
gl.GL_GREEN: "G",
gl.GL_BLUE: "B",
gl.GL_ALPHA: "A",
gl.GL_ZERO: "0",
gl.GL_ONE: "1",
}
_swizzle_str_to_enum = {
"R": gl.GL_RED,
"G": gl.GL_GREEN,
"B": gl.GL_BLUE,
"A": gl.GL_ALPHA,
"0": gl.GL_ZERO,
"1": gl.GL_ONE,
}

def __init__(
self,
Expand Down Expand Up @@ -195,7 +174,7 @@ def __init__(

self._texture_2d(data)

# Only set texture parameters on non-multisamples textures
# Only set texture parameters on non-multisample textures
if self._samples == 0:
self.filter = filter or self._filter
self.wrap_x = wrap_x or self._wrap_x
Expand Down Expand Up @@ -440,7 +419,7 @@ def swizzle(self) -> str:

swizzle_str = ""
for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]:
swizzle_str += self._swizzle_enum_to_str[v.value]
swizzle_str += swizzle_enum_to_str[v.value]

return swizzle_str

Expand All @@ -456,10 +435,13 @@ def swizzle(self, value: str):
for c in value:
try:
c = c.upper()
swizzle_enums.append(self._swizzle_str_to_enum[c])
swizzle_enums.append(swizzle_str_to_enum[c])
except KeyError:
raise ValueError(f"Swizzle value '{c}' invalid. Must be one of RGBA01")

gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit)
gl.glBindTexture(self._target, self._glo)

gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0])
gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1])
gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2])
Expand Down Expand Up @@ -602,9 +584,9 @@ def compare_func(self, value: str | None):
if not isinstance(value, str) and value is not None:
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")

func = self._compare_funcs.get(value, None)
func = compare_funcs.get(value, None)
if func is None:
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")
raise ValueError(f"value must be as string: {compare_funcs.keys()}")

self._compare_func = value
gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit)
Expand Down
Loading
Loading