'''
This module allows to load texture and to sample from it
'''
import numpy as np
from vulkbare import load_image, resize_image
from vulk import vulkanobject as vo
from vulk import vulkanconstant as vc
from vulk.util import mipmap_size, mipmap_levels
[docs]class RawTexture():
"""A Raw texture is not initialized with an image file but can be filled
manually"""
def __init__(self, context, width, height, texture_format, mip_levels=1):
"""
Args:
context (VulkContext)
width (int): Width of the texture
height (int): Height of the texture
texture_format (Format): Format of vulkan texture
mip_levels (int): Number of mipmaps
"""
self.width = width
self.height = height
self.format = texture_format
self.mip_levels = mip_levels or mipmap_levels(width, height)
self.texture = self.init_texture(context, self.mip_levels)
# Init bitmap
self.bitmap = self.init_bitmap()
# Init view and sampler
self.view = None
self.init_view(context)
self.sampler = None
self.init_sampler(context)
[docs] def init_texture(self, context, mip_levels):
return vo.HighPerformanceImage(
context, vc.ImageType.TYPE_2D, self.format, self.width,
self.height, 1, mip_levels, 1, vc.SampleCount.COUNT_1)
[docs] def init_view(self, context):
self.set_view(context)
[docs] def init_sampler(self, context):
self.set_sampler(context)
[docs] def init_bitmap(self):
# pylint: disable=unused-argument
'''Return the numpy array containing bitmap'''
_, _, pixel_size = vc.format_info(self.format)
return np.zeros(self.width*self.height*pixel_size, dtype=np.uint8)
[docs] def set_view(self, context):
"""Set texture view
Args:
context (VulkContext)
"""
texture_range = vo.ImageSubresourceRange(
vc.ImageAspect.COLOR, 0, 1, 0, 1)
self.view = vo.ImageView(
context, self.texture.final_image,
vc.ImageViewType.TYPE_2D, self.format, texture_range)
[docs] def set_sampler(self, context, mag_filter=vc.Filter.NEAREST,
min_filter=vc.Filter.NEAREST,
mipmap_mode=vc.SamplerMipmapMode.NEAREST,
address_mode_u=vc.SamplerAddressMode.REPEAT,
address_mode_v=vc.SamplerAddressMode.REPEAT,
address_mode_w=vc.SamplerAddressMode.REPEAT,
anisotropy_enable=False, max_anisotropy=16):
"""Set the texture sampler
By default, sampler is configured for the best performance.
If you want better quality, you must enable manually bilinear,
trilinear or anisotropic filtering.
Args:
context (VulkContext): Context
mag_filter (Filter): Magnification filter to apply to lookups
min_filter (Filter): Minification filter to apply to lookups
mipmap_mode (SamplerMipmapMode): Mipmap filter to apply to lookups
address_mode_u (SamplerAddressMode):
address_mode_v (SamplerAddressMode):
address_mode_w (SamplerAddressMode):
anisotropy_enable (bool): Whether to enable anisotropy
max_anisotropy (int): Anisotropy value clamp
"""
if self.sampler:
self.sampler.destroy(context)
self.sampler = vo.Sampler(
context, mag_filter, min_filter, mipmap_mode,
address_mode_u, address_mode_v, address_mode_w, 0,
anisotropy_enable, max_anisotropy, False, vc.CompareOp.ALWAYS,
0, 0, vc.BorderColor.INT_OPAQUE_BLACK, False)
[docs] def upload(self, context):
"""Make texture accessible for shader
If this function is not called, the texture can't be used.
When all your buffers are uploaded, call this function
"""
self.texture.finalize(context)
[docs]class BinaryTexture(RawTexture):
"""RawTexture with provided bitmap buffer.
**Warning: You are responsible of the bitmap buffer**
"""
def __init__(self, context, width, height, texture_format, raw_bitmap,
mip_levels=1):
"""
Args:
context (VulkContext)
width (int): Texture width
height (int): Texture height
texture_format (Format): Texture format
raw_bitmap (buffer): Bitmap buffer
mip_levels (int): Number of mipmaps to generate (0 = until 1x1)
"""
self.raw_bitmap = raw_bitmap
# Create all the components by calling parent init
super().__init__(context, width, height, texture_format,
mip_levels=mip_levels)
# Upload data
self.generate_mipmaps(context)
self.upload(context)
[docs] def init_bitmap(self):
'''Initialize bitmap array with `raw_bitmap`'''
return np.array(self.raw_bitmap, dtype=np.uint8, copy=False)
[docs] def upload_buffer(self, context, mip_level):
"""Upload bitmap into Vulkan memory
Args:
context (VulkContext)
mip_level (int): Level of mip
"""
base_width = self.width
base_height = self.height
components = vc.format_info(self.format)[1]
width, height = mipmap_size(base_width, base_height, mip_level)
if width == base_width and height == base_height:
upload_bitmap = self.bitmap
else:
upload_raw_bitmap = resize_image(
self.raw_bitmap, base_width, base_height, components,
width, height
)
upload_bitmap = np.array(upload_raw_bitmap, dtype=np.uint8,
copy=False)
with self.texture.bind_buffer(context, mip_level) as buf:
np.copyto(np.array(buf, copy=False),
upload_bitmap,
casting='no')
[docs] def generate_mipmaps(self, context):
"""Generate mipmap automatically
This method generates mipmap on processor and then upload it on GPU.
This method is heavy, use it with care. You shouldn't need to call it
several times unless raw_bitmap is modified.
You must call `upload` to update the texture in Graphic Card.
Args:
context (VulkContext)
"""
for i in range(self.mip_levels):
self.upload_buffer(context, i)
[docs]class Texture(BinaryTexture):
"""BinaryTexture with file managing"""
def __init__(self, context, path_file, mip_levels=1):
"""
Args:
context (VulkContext)
path_file (str): Path to the image to load
mip_levels (int): Number of mip level (0=max)
"""
# Load bitmap
with open(path_file, 'rb') as f:
raw_bitmap, width, height, components = load_image(f.read())
texture_format = Texture.components_to_format(components)
# Create all the components by calling parent init
super().__init__(context, width, height, texture_format, raw_bitmap,
mip_levels=mip_levels)
[docs] @staticmethod
def components_to_format(components):
'''Convert number of channel components in image to Vulkan format
*Parameters:*
- `components`: Number of components
'''
if components < 1 or components > 4:
raise ValueError("components must be between 0 and 4")
return [vc.Format.NONE, vc.Format.R8_UNORM, vc.Format.R8G8_UNORM,
vc.Format.R8G8B8_UNORM, vc.Format.R8G8B8A8_UNORM][components]
[docs]class HighQualityTexture(Texture):
"""Texture with best quality
To get best quality, we generate automatically all mipmaps and
set filter to trilinear or anisotropy filtering.
It's really just a helper class.
"""
def __init__(self, context, path_file, anisotropy=0):
self.anisotropy = anisotropy
# Set mipmap_levels to 0 to generate all mipmaps
super().__init__(context, path_file, 0)
[docs] def init_sampler(self, context):
anisotropy_enable = self.anisotropy > 0
self.set_sampler(context, mag_filter=vc.Filter.LINEAR,
min_filter=vc.Filter.LINEAR,
mipmap_mode=vc.SamplerMipmapMode.LINEAR,
anisotropy_enable=anisotropy_enable,
max_anisotropy=self.anisotropy)
[docs]class TextureRegion():
'''
Defines a rectangular area of a texture. The coordinate system used has
its origin in the upper left corner with the x-axis pointing to the
right and the y axis pointing downwards.
'''
def __init__(self, texture, u=0, v=0, u2=1, v2=1):
'''Initialize texture region
*Parameters:*
- `texture`: `RawTexture`
- `u`, `u2`: X coordinate relative to texture size
- `v`, `v2`: Y coordinate relative to texture size
'''
self.texture = texture
self.u = u
self.u2 = u2
self.v = v
self.v2 = v2
[docs] @staticmethod
def from_pixels(texture, x, y, width, height):
"""Create a TextureRegion with pixel coordinates
Args:
texture (Texture): Base texture of region
x (int): X offset (left to right)
y (int): Y offset (top to bottom)
width (int): Region width
height (int): Region height
Returns:
The new TextureRegion
"""
u = x / texture.width
u2 = u + width / texture.width
v = y / texture.height
v2 = v + height / texture.height
return TextureRegion(texture, u, v, u2, v2)
[docs] def set_texture(self, texture):
'''Set texture of `TextureRegion`
*Parameters:*
- `texture`: `RawTexture`
'''
self.texture = texture
[docs] def set_region(self, u, u2, v, v2):
'''Set coordinate relatively to texture size
*Parameters:*
- `u`, `u2`: X coordinate relative to texture size
- `v`, `v2`: Y coordinate relative to texture size
'''
self.u = u
self.u2 = u2
self.v = v
self.v2 = v2
[docs] def set_region_pixel(self, x, y, width, height):
'''Set coordinate relatively to pixel size
*Parameters:*
- `x`: X coordinate of the texture
- `y`: Y coordinate of the texture
- `width`: Width of the region
- `height`: Height of the region
'''
inv_width = 1. / self.texture.width
inv_height = 1. / self.texture.height
self.set_region(
x * inv_width, y * inv_height,
(x + width) * inv_width, (y + height) * inv_height
)