Skip to content

Commit f8755e2

Browse files
authored
Basic support for sampler objects (#2445)
* Basic support for sampler objects * Import sorting * docs + import fix
1 parent a01e60c commit f8755e2

19 files changed

+311
-34
lines changed

arcade/gl/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .buffer import Buffer
2626
from .vertex_array import Geometry, VertexArray
2727
from .texture import Texture2D
28+
from .sampler import Sampler
2829
from .framebuffer import Framebuffer
2930
from .program import Program
3031
from .query import Query
@@ -42,5 +43,6 @@
4243
"ShaderException",
4344
"VertexArray",
4445
"Texture2D",
46+
"Sampler",
4547
"geometry",
4648
]

arcade/gl/context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .glsl import ShaderSource
3131
from .program import Program
3232
from .query import Query
33+
from .sampler import Sampler
3334
from .texture import Texture2D
3435
from .types import BufferDescription, GLenumLike, PyGLenum
3536
from .vertex_array import Geometry
@@ -1084,6 +1085,16 @@ def depth_texture(
10841085
"""
10851086
return Texture2D(self, size, data=data, depth=True)
10861087

1088+
def sampler(self, texture: Texture2D) -> Sampler:
1089+
"""
1090+
Create a sampler object for a texture.
1091+
1092+
Args:
1093+
texture:
1094+
The texture to create a sampler for
1095+
"""
1096+
return Sampler(self, texture)
1097+
10871098
def geometry(
10881099
self,
10891100
content: Sequence[BufferDescription] | None = None,

arcade/gl/sampler.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from __future__ import annotations
2+
3+
import weakref
4+
from ctypes import byref, c_uint32
5+
from typing import TYPE_CHECKING
6+
7+
from pyglet import gl
8+
9+
from .types import PyGLuint, compare_funcs
10+
11+
if TYPE_CHECKING:
12+
from arcade.gl import Context, Texture2D
13+
14+
15+
class Sampler:
16+
"""
17+
OpenGL sampler object.
18+
19+
When bound to a texture unit it overrides all the
20+
sampling parameters of the texture channel.
21+
"""
22+
23+
def __init__(
24+
self,
25+
ctx: "Context",
26+
texture: Texture2D,
27+
*,
28+
filter: tuple[PyGLuint, PyGLuint] | None = None,
29+
wrap_x: PyGLuint | None = None,
30+
wrap_y: PyGLuint | None = None,
31+
):
32+
self._ctx = ctx
33+
self._glo = -1
34+
35+
value = c_uint32()
36+
gl.glGenSamplers(1, byref(value))
37+
self._glo = value.value
38+
39+
self.texture = texture
40+
41+
# Default filters for float and integer textures
42+
# Integer textures should have NEAREST interpolation
43+
# by default 3.3 core doesn't really support it consistently.
44+
if "f" in self.texture._dtype:
45+
self._filter = gl.GL_LINEAR, gl.GL_LINEAR
46+
else:
47+
self._filter = gl.GL_NEAREST, gl.GL_NEAREST
48+
49+
self._wrap_x = gl.GL_REPEAT
50+
self._wrap_y = gl.GL_REPEAT
51+
self._anisotropy = 1.0
52+
self._compare_func: str | None = None
53+
54+
# Only set texture parameters on non-multisample textures
55+
if self.texture._samples == 0:
56+
self.filter = filter or self._filter
57+
self.wrap_x = wrap_x or self._wrap_x
58+
self.wrap_y = wrap_y or self._wrap_y
59+
60+
if self._ctx.gc_mode == "auto":
61+
weakref.finalize(self, Sampler.delete_glo, self._glo)
62+
63+
@property
64+
def glo(self) -> PyGLuint:
65+
"""The OpenGL sampler id"""
66+
return self._glo
67+
68+
def use(self, unit: int):
69+
"""
70+
Bind the sampler to a texture unit
71+
"""
72+
gl.glBindSampler(unit, self._glo)
73+
74+
def clear(self, unit: int):
75+
"""
76+
Unbind the sampler from a texture unit
77+
"""
78+
gl.glBindSampler(unit, 0)
79+
80+
@property
81+
def filter(self) -> tuple[int, int]:
82+
"""
83+
Get or set the ``(min, mag)`` filter for this texture.
84+
85+
These are rules for how a texture interpolates.
86+
The filter is specified for minification and magnification.
87+
88+
Default value is ``LINEAR, LINEAR``.
89+
Can be set to ``NEAREST, NEAREST`` for pixelated graphics.
90+
91+
When mipmapping is used the min filter needs to be one of the
92+
``MIPMAP`` variants.
93+
94+
Accepted values::
95+
96+
# Enums can be accessed on the context or arcade.gl
97+
NEAREST # Nearest pixel
98+
LINEAR # Linear interpolate
99+
NEAREST_MIPMAP_NEAREST # Minification filter for mipmaps
100+
LINEAR_MIPMAP_NEAREST # Minification filter for mipmaps
101+
NEAREST_MIPMAP_LINEAR # Minification filter for mipmaps
102+
LINEAR_MIPMAP_LINEAR # Minification filter for mipmaps
103+
104+
Also see
105+
106+
* https://www.khronos.org/opengl/wiki/Texture#Mip_maps
107+
* https://www.khronos.org/opengl/wiki/Sampler_Object#Filtering
108+
"""
109+
return self._filter
110+
111+
@filter.setter
112+
def filter(self, value: tuple[int, int]):
113+
if not isinstance(value, tuple) or not len(value) == 2:
114+
raise ValueError("Texture filter must be a 2 component tuple (min, mag)")
115+
116+
self._filter = value
117+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MIN_FILTER, self._filter[0])
118+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MAG_FILTER, self._filter[1])
119+
120+
@property
121+
def wrap_x(self) -> int:
122+
"""
123+
Get or set the horizontal wrapping of the texture.
124+
125+
This decides how textures are read when texture coordinates are outside
126+
the ``[0.0, 1.0]`` area. Default value is ``REPEAT``.
127+
128+
Valid options are::
129+
130+
# Note: Enums can also be accessed in arcade.gl
131+
# Repeat pixels on the y axis
132+
texture.wrap_x = ctx.REPEAT
133+
# Repeat pixels on the y axis mirrored
134+
texture.wrap_x = ctx.MIRRORED_REPEAT
135+
# Repeat the edge pixels when reading outside the texture
136+
texture.wrap_x = ctx.CLAMP_TO_EDGE
137+
# Use the border color (black by default) when reading outside the texture
138+
texture.wrap_x = ctx.CLAMP_TO_BORDER
139+
"""
140+
return self._wrap_x
141+
142+
@wrap_x.setter
143+
def wrap_x(self, value: int):
144+
self._wrap_x = value
145+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_S, value)
146+
147+
@property
148+
def wrap_y(self) -> int:
149+
"""
150+
Get or set the horizontal wrapping of the texture.
151+
152+
This decides how textures are read when texture coordinates are outside the
153+
``[0.0, 1.0]`` area. Default value is ``REPEAT``.
154+
155+
Valid options are::
156+
157+
# Note: Enums can also be accessed in arcade.gl
158+
# Repeat pixels on the x axis
159+
texture.wrap_x = ctx.REPEAT
160+
# Repeat pixels on the x axis mirrored
161+
texture.wrap_x = ctx.MIRRORED_REPEAT
162+
# Repeat the edge pixels when reading outside the texture
163+
texture.wrap_x = ctx.CLAMP_TO_EDGE
164+
# Use the border color (black by default) when reading outside the texture
165+
texture.wrap_x = ctx.CLAMP_TO_BORDER
166+
"""
167+
return self._wrap_y
168+
169+
@wrap_y.setter
170+
def wrap_y(self, value: int):
171+
self._wrap_y = value
172+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_T, value)
173+
174+
@property
175+
def anisotropy(self) -> float:
176+
"""Get or set the anisotropy for this texture."""
177+
return self._anisotropy
178+
179+
@anisotropy.setter
180+
def anisotropy(self, value):
181+
self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY))
182+
gl.glSamplerParameterf(self._glo, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy)
183+
184+
@property
185+
def compare_func(self) -> str | None:
186+
"""
187+
Get or set the compare function for a depth texture::
188+
189+
texture.compare_func = None # Disable depth comparison completely
190+
texture.compare_func = '<=' # GL_LEQUAL
191+
texture.compare_func = '<' # GL_LESS
192+
texture.compare_func = '>=' # GL_GEQUAL
193+
texture.compare_func = '>' # GL_GREATER
194+
texture.compare_func = '==' # GL_EQUAL
195+
texture.compare_func = '!=' # GL_NOTEQUAL
196+
texture.compare_func = '0' # GL_NEVER
197+
texture.compare_func = '1' # GL_ALWAYS
198+
"""
199+
return self._compare_func
200+
201+
@compare_func.setter
202+
def compare_func(self, value: str | None):
203+
if not self.texture._depth:
204+
raise ValueError("Depth comparison function can only be set on depth textures")
205+
206+
if not isinstance(value, str) and value is not None:
207+
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")
208+
209+
func = compare_funcs.get(value, None)
210+
if func is None:
211+
raise ValueError(f"value must be as string: {compare_funcs.keys()}")
212+
213+
self._compare_func = value
214+
if value is None:
215+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE)
216+
else:
217+
gl.glSamplerParameteri(
218+
self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE
219+
)
220+
gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_FUNC, func)
221+
222+
@staticmethod
223+
def delete_glo(glo: int) -> None:
224+
"""
225+
Delete the OpenGL object
226+
"""
227+
gl.glDeleteSamplers(1, glo)

arcade/gl/texture.py

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88

99
from ..types import BufferProtocol
1010
from .buffer import Buffer
11-
from .types import BufferOrBufferProtocol, PyGLuint, pixel_formats
11+
from .types import (
12+
BufferOrBufferProtocol,
13+
PyGLuint,
14+
compare_funcs,
15+
pixel_formats,
16+
swizzle_enum_to_str,
17+
swizzle_str_to_enum,
18+
)
1219
from .utils import data_to_ctypes
1320

1421
if TYPE_CHECKING: # handle import cycle caused by type hinting
@@ -101,34 +108,6 @@ class Texture2D:
101108
"_compressed",
102109
"_compressed_data",
103110
)
104-
_compare_funcs = {
105-
None: gl.GL_NONE,
106-
"<=": gl.GL_LEQUAL,
107-
"<": gl.GL_LESS,
108-
">=": gl.GL_GEQUAL,
109-
">": gl.GL_GREATER,
110-
"==": gl.GL_EQUAL,
111-
"!=": gl.GL_NOTEQUAL,
112-
"0": gl.GL_NEVER,
113-
"1": gl.GL_ALWAYS,
114-
}
115-
# Swizzle conversion lookup
116-
_swizzle_enum_to_str = {
117-
gl.GL_RED: "R",
118-
gl.GL_GREEN: "G",
119-
gl.GL_BLUE: "B",
120-
gl.GL_ALPHA: "A",
121-
gl.GL_ZERO: "0",
122-
gl.GL_ONE: "1",
123-
}
124-
_swizzle_str_to_enum = {
125-
"R": gl.GL_RED,
126-
"G": gl.GL_GREEN,
127-
"B": gl.GL_BLUE,
128-
"A": gl.GL_ALPHA,
129-
"0": gl.GL_ZERO,
130-
"1": gl.GL_ONE,
131-
}
132111

133112
def __init__(
134113
self,
@@ -195,7 +174,7 @@ def __init__(
195174

196175
self._texture_2d(data)
197176

198-
# Only set texture parameters on non-multisamples textures
177+
# Only set texture parameters on non-multisample textures
199178
if self._samples == 0:
200179
self.filter = filter or self._filter
201180
self.wrap_x = wrap_x or self._wrap_x
@@ -440,7 +419,7 @@ def swizzle(self) -> str:
440419

441420
swizzle_str = ""
442421
for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]:
443-
swizzle_str += self._swizzle_enum_to_str[v.value]
422+
swizzle_str += swizzle_enum_to_str[v.value]
444423

445424
return swizzle_str
446425

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

442+
gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit)
443+
gl.glBindTexture(self._target, self._glo)
444+
463445
gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0])
464446
gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1])
465447
gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2])
@@ -602,9 +584,9 @@ def compare_func(self, value: str | None):
602584
if not isinstance(value, str) and value is not None:
603585
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")
604586

605-
func = self._compare_funcs.get(value, None)
587+
func = compare_funcs.get(value, None)
606588
if func is None:
607-
raise ValueError(f"value must be as string: {self._compare_funcs.keys()}")
589+
raise ValueError(f"value must be as string: {compare_funcs.keys()}")
608590

609591
self._compare_func = value
610592
gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit)

0 commit comments

Comments
 (0)