Source code for silx.gui._glutils.Texture

# /*##########################################################################
#
# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module provides a class wrapping OpenGL 2D and 3D texture."""

__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "04/10/2016"


from collections import abc
from ctypes import c_void_p
import logging

import numpy

from silx._utils import NP_OPTIONAL_COPY
from . import gl, utils


_logger = logging.getLogger(__name__)


[docs] class Texture(object): """Base class to wrap OpenGL 2D and 3D texture :param internalFormat: OpenGL texture internal format :param data: The data to copy to the texture or None for an empty texture :type data: numpy.ndarray or None :param format_: Input data format if different from internalFormat :param shape: If data is None, shape of the texture (height, width) or (depth, height, width) :type shape: List[int] :param int texUnit: The texture unit to use :param minFilter: OpenGL texture minimization filter (default: GL_NEAREST) :param magFilter: OpenGL texture magnification filter (default: GL_LINEAR) :param wrap: Texture wrap mode for dimensions: (t, s) or (r, t, s) If a single value is provided, it used for all dimensions. :type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode """ def __init__( self, internalFormat, data=None, format_=None, shape=None, texUnit=0, minFilter=None, magFilter=None, wrap=None, ): self._internalFormat = internalFormat if format_ is None: format_ = self.internalFormat if data is None: assert shape is not None else: assert shape is None data = numpy.asarray(data, order="C") if format_ != gl.GL_RED: shape = data.shape[:-1] # Last dimension is channels else: shape = data.shape self._deferredUpdates = [(format_, data, None)] assert len(shape) in (2, 3) self._shape = tuple(shape) self._ndim = len(shape) self.texUnit = texUnit self._texParameterUpdates = {} # Store texture params to update self._minFilter = minFilter if minFilter is not None else gl.GL_NEAREST self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = self._minFilter self._magFilter = magFilter if magFilter is not None else gl.GL_LINEAR self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = self._magFilter self._name = None # Store texture ID if wrap is not None: if not isinstance(wrap, abc.Iterable): wrap = [wrap] * self.ndim assert len(wrap) == self.ndim self._texParameterUpdates[gl.GL_TEXTURE_WRAP_S] = wrap[-1] self._texParameterUpdates[gl.GL_TEXTURE_WRAP_T] = wrap[-2] if self.ndim == 3: self._texParameterUpdates[gl.GL_TEXTURE_WRAP_R] = wrap[0] @property def target(self): """OpenGL target type of this texture""" return gl.GL_TEXTURE_2D if self.ndim == 2 else gl.GL_TEXTURE_3D @property def ndim(self): """The number of dimensions: 2 or 3""" return self._ndim @property def internalFormat(self): """Texture internal format""" return self._internalFormat @property def shape(self): """Shape of the texture: (height, width) or (depth, height, width)""" return self._shape @property def name(self): """OpenGL texture name. It is None if not initialized or already discarded. """ return self._name @property def minFilter(self): """Minifying function parameter (GL_TEXTURE_MIN_FILTER)""" return self._minFilter @minFilter.setter def minFilter(self, minFilter): if minFilter != self.minFilter: self._minFilter = minFilter self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = minFilter @property def magFilter(self): """Magnification function parameter (GL_TEXTURE_MAG_FILTER)""" return self._magFilter @magFilter.setter def magFilter(self, magFilter): if magFilter != self.magFilter: self._magFilter = magFilter self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = magFilter def _isPrepareRequired(self) -> bool: """Returns True if OpenGL texture needs to be updated. :rtype: bool """ return self._name is None or self._texParameterUpdates or self._deferredUpdates def _prepareAndBind(self, texUnit=None): """Synchronizes the OpenGL texture""" if self._name is None: self._name = gl.glGenTextures(1) self._bind(texUnit) # Synchronizes texture parameters for pname, param in self._texParameterUpdates.items(): gl.glTexParameter(self.target, pname, param) self._texParameterUpdates = {} # Copy data to texture for format_, data, offset in self._deferredUpdates: gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) # This are the defaults, useless to set if not modified # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) if data is None: data = c_void_p(0) type_ = gl.GL_UNSIGNED_BYTE else: type_ = utils.numpyToGLType(data.dtype) if offset is None: # Initialize texture if self.ndim == 2: _logger.debug( "Creating 2D texture shape: (%d, %d)," " internal format: %s, format: %s, type: %s", self.shape[0], self.shape[1], str(self.internalFormat), str(format_), str(type_), ) gl.glTexImage2D( gl.GL_TEXTURE_2D, 0, self.internalFormat, self.shape[1], self.shape[0], 0, format_, type_, data, ) else: _logger.debug( "Creating 3D texture shape: (%d, %d, %d)," " internal format: %s, format: %s, type: %s", self.shape[0], self.shape[1], self.shape[2], str(self.internalFormat), str(format_), str(type_), ) gl.glTexImage3D( gl.GL_TEXTURE_3D, 0, self.internalFormat, self.shape[2], self.shape[1], self.shape[0], 0, format_, type_, data, ) else: # Update already existing texture if self.ndim == 2: gl.glTexSubImage2D( gl.GL_TEXTURE_2D, 0, offset[1], offset[0], data.shape[1], data.shape[0], format_, type_, data, ) else: gl.glTexSubImage3D( gl.GL_TEXTURE_3D, 0, offset[2], offset[1], offset[0], data.shape[2], data.shape[1], data.shape[0], format_, type_, data, ) self._deferredUpdates = [] def _bind(self, texUnit=None): """Bind the texture to a texture unit. :param int texUnit: The texture unit to use """ if texUnit is None: texUnit = self.texUnit gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) gl.glBindTexture(self.target, self.name) def _unbind(self, texUnit=None): """Reset texture binding to a texture unit. :param int texUnit: The texture unit to use """ if texUnit is None: texUnit = self.texUnit gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) gl.glBindTexture(self.target, 0)
[docs] def prepare(self): """Synchronizes the OpenGL texture. This method must be called with a current OpenGL context. """ if self._isPrepareRequired(): self._prepareAndBind() self._unbind()
[docs] def bind(self, texUnit=None): """Bind the texture to a texture unit. The OpenGL texture is updated if needed. This method must be called with a current OpenGL context. :param int texUnit: The texture unit to use """ if self._isPrepareRequired(): self._prepareAndBind(texUnit) else: self._bind(texUnit)
[docs] def discard(self): """Delete associated OpenGL texture. This method must be called with a current OpenGL context. """ if self._name is not None: gl.glDeleteTextures(self._name) self._name = None else: _logger.warning("Texture not initialized or already discarded")
# with statement def __enter__(self): self.bind() def __exit__(self, exc_type, exc_val, exc_tb): self._unbind()
[docs] def update(self, format_, data, offset=(0, 0, 0), copy=True): """Update the content of the texture. Texture is not resized, so data must fit into texture with the given offset. This update is performed lazily during next call to :meth:`prepare` or :meth:`bind`. Data MUST not be changed until then. :param format_: The OpenGL format of the data :param data: The data to use to update the texture :param List[int] offset: Offset in the texture where to copy the data :param bool copy: True (default) to copy data, False to use as is (do not modify) """ data = numpy.array(data, copy=copy or NP_OPTIONAL_COPY, order="C") offset = tuple(offset) assert data.ndim == self.ndim assert len(offset) >= self.ndim for i in range(self.ndim): assert offset[i] + data.shape[i] <= self.shape[i] self._deferredUpdates.append((format_, data, offset))