myou-engine/src/graphics/texture.nim
Alberto Torres 67307032da Textures: Add several defines and constructor options for compression, cache.
* Add defines `myouForceAstc`, `myouMinTextureChannels`, 
  `myouLoadUncompressedTextures` and `myouAllCacheFilesExist`.
* Suppress warning messages about emulation for `myouForceAstc`.
* Add argument `use_compression` to `newTexture` (default `true`)
2024-12-16 21:05:21 +01:00

591 lines
24 KiB
Nim

# The contents of this file are subject to the Common Public Attribution License
# Version 1.0 (the “License”); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
# of software over a computer network and provide for limited attribution for
# the Original Developer. In addition, Exhibit A has been modified to be
# consistent with Exhibit B.
#
# Software distributed under the License is distributed on an “AS IS” basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Myou Engine.
#
# the Original Developer is the Initial Developer.
#
# The Initial Developer of the Original Code is the Myou Engine developers.
# All portions of the code written by the Myou Engine developers are Copyright
# (c) 2024. All Rights Reserved.
#
# Alternatively, the contents of this file may be used under the terms of the
# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
# case the provisions of [AGPL-3] License are applicable instead of those above.
#
# If you wish to allow use of your version of this file only under the terms of
# the [AGPL-3] License and not to allow others to use your version of this file
# under the CPAL, indicate your decision by deleting the provisions above and
# replace them with the notice and other provisions required by the [AGPL-3]
# License. If you do not delete the provisions above, a recipient may use your
# version of this file under either the CPAL or the [AGPL-3] License.
import ../types
import vmath except Quat
import arr_ref
# import tinyre
import std/tables
import ../platform/gl
# Forward declarations
func mipmapHigh*(self: Texture): int
proc needsMipmap*(self: Texture): bool
proc setMaxTextures*(count: int32)
proc newTextureStorage*(ttype: TextureType, width, height, depth: int, format: TextureFormat): TextureStorage
proc preallocate(self: Texture)
proc freeTextureStorage(self: Texture)
proc bind_it*(texture: Texture, reserve_slot: static[int32] = -1, needs_active_texture: static[bool] = false)
proc unbind*(texture: Texture)
proc unbindAllTextures*()
proc destroy*(texture: Texture)
proc loadFromPixelsPointer*(self: Texture, pixels: pointer)
proc loadFromPixels*[T](self: Texture, pixels: SliceMem[T]) {.gcsafe.}
proc loadCubeSideFromPixels*(self: Texture, pixels: pointer, side: int32 = 0)
proc setFilter*(self: Texture, filter: TextureFilter)
proc newTexture*(engine: MyouEngine, name: string, width, height: int, depth: int = 1,
format: TextureFormat,
tex_type: TextureType = Tex2D,
filter: TextureFilter = Trilinear,
pixels: ArrRef[float32] = nil): Texture
proc generateMipmap*(self: Texture)
func to_sRGB*(format: TextureFormat): TextureFormat
proc newTexture*(engine: MyouEngine, name: string, data: SliceMem[byte],
is_sRGB: bool, filter: TextureFilter = Trilinear, depth=1,
flip = true, use_compression = true): Texture
proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: bool,
filter: TextureFilter = Trilinear,
tex_type: TextureType = Tex2D,
flip = true,
use_compression = true,
): Texture
proc setExtrapolation*(self: Texture, ext: TextureExtrapolation)
proc getTexturePixels*(self: Texture): TexturePixels
proc setMipmapRange*(self: Texture, first = 0, last = 1000) {.gcsafe.}
func vec3size*(self: Texture, mip_level = -1): Vec3
func toInternalFormat*(format: TextureFormat): GLenum
# End forward declarations
# import sugar
import std/bitops
import std/strformat
import loadable
import dds_ktx
import ../gpu_formats/texture_decode
import ../gpu_formats/texture_optimize
# TODO: use and test destructor
# NOTE: we moved this from the render manager to global state
# (if we ever need multiple instances with different context
# we should move them back)
# NOTE: set as threadvar for optimization (and for having procs gcsafe
# implicitely), we only use them in the main thread
var bound_textures {.threadvar.}: seq[Texture]
var active_texture {.threadvar.}: int32
var next_texture {.threadvar.}: int32
var max_textures {.threadvar.}: int32
var reserved {.threadvar.}: int32
active_texture = -1
if defined(android):
# I don't know why slot 0 makes the mesh not render at all
# TODO: maybe it only happens when building with ArCore support
# even if it's not used, we should test it
reserved = 1
func samplerType*(tex_type: TextureType): string =
case tex_type:
of Tex2D: "sampler2D"
of Tex2DArray: "sampler2DArray"
of Tex3D: "sampler3D"
of TexCube: "samplerCube"
of TexExternal: "samplerExternalOES" ## NOTE: requires GLSL extension
template samplerType*(self: Texture): string = self.tex_type.samplerType
func toInternalFormat*(format: TextureFormat): GLenum =
case format:
of SRGB_u8: GL_SRGB8
of SRGB_Alpha_u8: GL_SRGB8_ALPHA8
of R_u8: GL_R8
of RG_u8: GL_RG8
of RGB_u8: GL_RGB8
of RGBA_u8: GL_RGBA8
of R_u16: GL_R16_EXT
of RG_u16: GL_RG16_EXT
of RGB_u16: GL_RGB16_EXT
of RGBA_u16: GL_RGBA16_EXT
of R_f16: GL_R16F
of RG_f16: GL_RG16F
of RGB_f16: GL_RGB16F
of RGBA_f16: GL_RGBA16F
of R_f32: GL_R32F
of RG_f32: GL_RG32F
of RGB_f32: GL_RGB32F
of RGBA_f32: GL_RGBA32F
of Depth_u16: GL_DEPTH_COMPONENT16
of Depth_u24: GL_DEPTH_COMPONENT24
# of Depth_u24_s8: 0x81A7.GLenum
of Depth_f32: GL_DEPTH_COMPONENT32F
# else:
# raise newException(ValueError, "Unsupported format")
# To change with bindless textures
func texturesNeedBinding*(): bool {.inline.} = true
func mipmapHigh*(self: Texture): int =
let depth = if self.tex_type == Tex2DArray: 1 else: self.depth
floor(log2(max(max(self.width, self.height), depth).float32)).int
func mipmapCount*(self: Texture): int = self.mipmapHigh + 1
proc needsMipmap*(self: Texture): bool =
case self.filter:
of Nearest, Linear: false
else: true
# TODO: this is hacky
# instead we should query the GL API the first time we need this
# or to make a texture manager class
proc setMaxTextures*(count: int32) =
max_textures = count
assert max_textures != 0
bound_textures.setLen max_textures
proc setTextureReservedSlots*(reserved_slots_bitfield: int32) =
reserved = reserved_slots_bitfield
proc resetNextTextureSlot*() =
next_texture = 0
proc newTextureStorage*(ttype: TextureType, width, height, depth: int, format: TextureFormat): TextureStorage =
var ts = new TextureStorage
ts.unit = -1
ts.target = case ttype:
of Tex2D: GL_TEXTURE_2D
of Tex2DArray: GL_TEXTURE_2D_ARRAY
of Tex3D: GL_TEXTURE_3D
of TexCube: GL_TEXTURE_CUBE_MAP
of TexExternal: GL_TEXTURE_EXTERNAL_OES
ts.iformat = format.toInternalFormat
ts.format = case format:
of R_u8, R_u16, R_f16, R_f32: GL_RED
of RG_u8, RG_u16, RG_f16, RG_f32: GL_RG
of SRGB_u8, RGB_u8, RGB_u16, RGB_f16, RGB_f32: GL_RGB
of SRGB_Alpha_u8, RGBA_u8, RGBA_u16, RGBA_f16, RGBA_f32: GL_RGBA
of Depth_u16, Depth_u24, Depth_f32: GL_DEPTH_COMPONENT
# of Depth_u24_s8: GL_DEPTH_COMPONENT
# else:
# raise newException(ValueError, "Unsupported format")
ts.gltype = case format:
of SRGB_u8, SRGB_Alpha_u8, R_u8, RG_u8, RGB_u8, RGBA_u8: GL_UNSIGNED_BYTE
of R_f16, RG_f16, RGB_f16, RGBA_f16: GL_HALF_FLOAT
of R_f32, RG_f32, RGB_f32, RGBA_f32, Depth_f32: cGL_FLOAT
of R_u16, RG_u16, RGB_u16, RGBA_u16, Depth_u16: GL_UNSIGNED_SHORT
of Depth_u24: GL_UNSIGNED_INT
# of Depth_u24_s8: GL_UNSIGNED_INT
ts.layer = 0
ts.tile_size = vec2(1,1)
var tex: GLuint
glGenTextures(1, addr tex)
ts.tex = tex.GPUTexture
return ts
proc preallocate(self: Texture) =
let ts {.cursor.} = self.storage
case self.tex_type:
of Tex2D:
# TODO: only do this if necessary
for m in 0 .. self.mipmapHigh:
glTexImage2D(ts.target, m.GLint, ts.iformat.GLint,
max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m),
0, ts.format, ts.gltype, nil)
of Tex2DArray:
# TODO: only do this if necessary
for m in 0 .. self.mipmapHigh:
glTexImage3D(ts.target, m.GLint, ts.iformat.GLint,
max(1, self.width.GLsizei shr m),
max(1, self.height.GLsizei shr m),
self.depth.GLsizei,
0, ts.format, ts.gltype, nil)
of Tex3D:
# TODO: only do this if necessary
for m in 0 .. self.mipmapHigh:
glTexImage3D(ts.target, m.GLint, ts.iformat.GLint,
max(1, self.width.GLsizei shr m),
max(1, self.height.GLsizei shr m),
max(1, self.depth.GLsizei shr m),
0, ts.format, ts.gltype, nil)
of TexCube:
for m in 0 .. self.mipmapHigh:
for i in GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint .. GL_TEXTURE_CUBE_MAP_NEGATIVE_Z.GLuint:
glTexImage2D(i.GLenum, m.GLint, ts.iformat.GLint,
max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m),
0, ts.format, ts.gltype, nil)
of TexExternal:
discard
proc freeTextureStorage(self: Texture) =
self.storage = nil
proc bind_it*(texture: Texture, reserve_slot: static[int32] = -1, needs_active_texture: static[bool] = false) =
# TODO: rewrite for using texture arrays and/or bindless
if texture.storage.unit == -1:
when reserve_slot >= 0:
let bound_unit = reserve_slot
reserved.setbit bound_unit
else:
while reserved.testbit next_texture:
inc(next_texture)
if next_texture == max_textures:
next_texture = 0
let bound_unit = next_texture
texture.storage.unit = bound_unit
if active_texture != bound_unit:
active_texture = bound_unit
glActiveTexture(cast[GLenum](GL_TEXTURE0.uint32 + bound_unit.uint32))
var old_tex {.cursor.} = bound_textures[bound_unit]
if old_tex != nil:
old_tex.storage.unit = -1
if old_tex.storage.target.uint32 != texture.storage.target.uint32:
glBindTexture(old_tex.storage.target, 0)
# if old_tex.sampler_object != 0:
# glBindSampler(cast[GLuint](bound_unit), 0)
bound_textures[bound_unit] = nil
glBindTexture(texture.storage.target, GLuint(texture.storage.tex))
# if texture.sampler_object != 0:
# glBindSampler(bound_unit.GLuint, texture.sampler_object)
bound_textures[bound_unit] = texture
inc(next_texture)
if next_texture == max_textures:
next_texture = 0
else:
when needs_active_texture:
if active_texture != texture.storage.unit:
active_texture = texture.storage.unit
glActiveTexture((GL_TEXTURE0.uint32 + texture.storage.unit.uint32).GLenum)
proc unbind*(texture: Texture) =
if texture.storage.unit == -1:
return
let bound_unit = texture.storage.unit
var old_tex {.cursor.} = bound_textures[bound_unit]
assert old_tex == texture, "unexpected bound texture"
if old_tex == texture:
bound_textures[bound_unit] = nil
active_texture = bound_unit
glActiveTexture((GL_TEXTURE0.uint32 + bound_unit.uint32).GLenum)
glBindTexture(texture.storage.target, 0)
if texture.sampler_object != 0:
glBindSampler(bound_unit.GLuint, 0)
texture.storage.unit = -1
texture.last_used_shader = nil
reserved.clearbit bound_unit
proc bind_all*(textures: seq[Texture], locations: seq[GLint]) =
# TODO: if not loaded, put a blank texture instead
var used = reserved
for t in textures:
let unit = t.storage.unit
if unit != -1:
used.setbit unit
for i,texture in textures:
var unit = texture.storage.unit
if unit == -1:
while used.testbit next_texture:
inc(next_texture)
if next_texture == max_textures:
next_texture = 0
unit = next_texture
active_texture = unit
texture.storage.unit = unit
glActiveTexture(cast[GLenum](GL_TEXTURE0.uint32 + unit.uint32))
let old_tex {.cursor.} = bound_textures[unit]
if old_tex != nil:
old_tex.storage.unit = -1
if old_tex.storage.target != texture.storage.target:
glBindTexture(old_tex.storage.target, 0)
glBindTexture(texture.storage.target, texture.storage.tex.GLuint)
bound_textures[unit] = texture
inc(next_texture)
if next_texture == max_textures:
next_texture = 0
glUniform1i(locations[i], unit)
proc unbindAllTextures*() =
for tex in bound_textures:
if tex != nil:
tex.unbind()
next_texture = 0
proc destroy*(texture: Texture) =
if texture.storage != nil:
texture.unbind()
texture.freeTextureStorage()
proc loadFromPixelsPointer*(self: Texture, pixels: pointer) =
let ts = self.storage
self.loaded = true
self.bind_it(needs_active_texture=true)
when not defined(release):
assert self.tex_type notin [TexCube, TexExternal]
if self.depth == 1:
glTexImage2D(ts.target, 0, ts.iformat.GLint,
self.width.GLsizei, self.height.GLsizei,
0, ts.format, ts.gltype, pixels)
else:
glTexImage3D(ts.target, 0, ts.iformat.GLint,
self.width.GLsizei, self.height.GLsizei, self.depth.GLsizei,
0, ts.format, ts.gltype, pixels)
if self.needsMipmap:
glGenerateMipmap(ts.target)
proc loadFromPixels*[T](self: Texture, pixels: SliceMem[T]) {.gcsafe.} =
self.loadFromPixelsPointer(pixels.data)
proc loadCubeSideFromPixels*(self: Texture, pixels: pointer, side: int32 = 0) =
let ts = self.storage
self.loaded = true
self.bind_it(needs_active_texture=true)
when not defined(release):
assert self.tex_type == TexCube
glTexImage2D((GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint + cast[GLuint](side)).GLenum, 0, ts.iformat.GLint,
self.width.GLsizei, self.height.GLsizei,
0, ts.format, ts.gltype, pixels)
proc loadCompressedData*(self: Texture, data: KtxInfoParts, refdata: seq[SliceMem[byte]]) {.gcsafe.} =
assert data.info.depth == 1 and data.info.num_layers == 1,
"Compressed array and 3D textures not supported yet"
let ts = self.storage
self.loaded = true
self.bind_it(needs_active_texture=true)
let target = if self.tex_type == TexCube: GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint.int32
else: ts.target.GLuint.int32
for part in data.parts:
glCompressedTexImage2D(cast[GLenum](target+part.face), part.mip_level,
data.info.internal_format.GLenum, part.width.GLsizei, part.height.GLsizei,
0, part.len.GLsizei, part.data)
self.setMipmapRange(0, data.info.num_mipmaps - 1)
proc setFilter*(self: Texture, filter: TextureFilter) =
self.filter = filter
self.engine.renderer.enqueue proc()=
if self.storage == nil: return
let min_filter = case filter
of Nearest: GL_NEAREST
of Pixellated: GL_NEAREST_MIPMAP_NEAREST
of Linear: GL_LINEAR
of Trilinear, Anisotropic: GL_LINEAR_MIPMAP_LINEAR
let mag_filter = case filter
of Nearest, Pixellated: GL_NEAREST
else: GL_LINEAR
self.bind_it(needs_active_texture=true)
glTexParameteri(self.storage.target, GL_TEXTURE_MAG_FILTER, mag_filter.GLint)
glTexParameteri(self.storage.target, GL_TEXTURE_MIN_FILTER, min_filter.GLint)
proc ensure_storage*(self: Texture) =
if self.storage == nil:
self.storage = newTextureStorage(self.tex_type, self.width, self.height, self.depth, self.format)
self.setFilter self.filter
self.mipmap_range = (0, self.mipmapHigh)
proc newTexture*(engine: MyouEngine, name: string,
width, height: int; depth: int = 1,
format: TextureFormat,
tex_type: TextureType = Tex2D,
filter: TextureFilter = Trilinear,
pixels: ArrRef[float32] = nil): Texture =
result = new Texture
result.engine = engine
result.name = name
result.tex_type = tex_type
result.format = format
result.width = width
result.height = height
result.depth = depth
if tex_type == TexCube:
assert width == height
result.depth = width
result.filter = filter
if width != 0 and height != 0:
let self = result
self.engine.renderer.enqueue proc()=
self.ensure_storage()
if pixels != nil:
self.loadFromPixelsPointer(pixels.toPointer)
else:
self.preallocate()
when defined(myouUseRenderdoc):
# Note: when we switch to arrays we'll have to store resolutions
# instead of names
glObjectLabel(GL_TEXTURE, self.storage.tex.GLuint,
GLsizei(self.name.len), self.name.cstring)
proc generateMipmap*(self: Texture) =
assert self.tex_type != TexExternal
self.bind_it(needs_active_texture=true)
glGenerateMipmap(self.storage.target)
func to_sRGB*(format: TextureFormat): TextureFormat =
return case format:
of RGBA_u8: SRGB_Alpha_u8
of RGB_u8: SRGB_u8
else: raise newException(ValueError, "There's no sRGB version of " & $format)
proc newTexture*(engine: MyouEngine, name: string, data: SliceMem[byte],
is_sRGB: bool, filter: TextureFilter = Trilinear, depth=1,
flip = true, use_compression = true): Texture =
var (width, height, format) = getDimensionsFormat(data.data, data.byte_len)
if is_sRGB:
if format in [RGB_u8, RGBA_u8]:
format = format.to_sRGB
else:
# TODO: sRGB shader for 1 and 2 channel textures
echo &"WARNING: Texture {name} is sRGB but has format {format} _"
let self = engine.newTexture(name, width, height, depth, format, filter=filter)
self.is_sRGB = is_sRGB
engine.renderer.enqueue proc() =
if use_compression:
self.loadOptimizedThreaded(@[data], loadFromPixels, loadCompressedData)
else:
self.loadOptimizedThreaded(@[data], loadFromPixels, nil)
return self
proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: bool,
filter: TextureFilter = Trilinear,
tex_type: TextureType = Tex2D,
flip = true,
use_compression = true,
): Texture =
# TODO: Api that stores the LoadableResource so it can be re-loaded later
# note: format does not matter at this point, will be replaced later
let self = engine.newTexture(name, 0, 0, filter=filter, format=RGBA_u8, tex_type=tex_type)
self.is_sRGB = is_sRGB
var res: LoadableResource
proc load(ok: bool, err: string, data: SliceMem[byte]) =
assert ok, &"Error loading texture '{self.name}' from '{file_name}': {err}"
let ktx_info = GetDdsKtxInfo(data.data, data.byte_len)
if ktx_info.isSome:
let ktx_info = ktx_info.get
assert ktx_info.is_cubemap == (self.tex_type == TexCube)
self.width = ktx_info.width
self.height = ktx_info.height
self.depth = ktx_info.depth
self.format = if ktx_info.has_alpha: RGBA_u8 else: RGB_u8
if ktx_info.is_sRGB:
self.format = self.format.to_sRGB
engine.renderer.enqueue proc()=
try:
self.ensure_storage()
let info_parts = KtxInfoParts(
info: ktx_info,
parts: ParseDdsKtx(data.data, data.byte_len))
self.loadCompressedData(info_parts, @[data])
self.loaded = true
# except Exception as e:
except:
# TODO: use logging
# for line in e.getStackTrace.split '\n':
# echo line
echo getCurrentExceptionMsg()
echo "Error loading texture " & file_name
else:
var (width, height, format) = getDimensionsFormat(data.data, data.byte_len, 3)
if is_sRGB:
if format in [RGB_u8, RGBA_u8]:
format = format.to_sRGB
else:
# TODO: sRGB shader for 1 and 2 channel textures
echo &"WARNING: Texture {self.name} is sRGB but has format {format}"
self.width = width
self.height = height
self.format = format
engine.renderer.enqueue proc()=
try:
self.ensure_storage()
if use_compression:
self.loadOptimizedThreaded(@[data], loadFromPixels, loadCompressedData)
else:
self.loadOptimizedThreaded(@[data], loadFromPixels, nil)
except:
# TODO: use logging
echo getCurrentExceptionMsg()
echo "Error loading texture " & file_name
res = file_name.loadUri(load, auto_start=false)
res.start()
return self
proc setExtrapolation*(self: Texture, ext: TextureExtrapolation) =
let e = case ext:
of Clamp: GL_CLAMP_TO_EDGE
of Border: GL_CLAMP_TO_BORDER
of Repeat: GL_REPEAT
of MirroredRepeat: GL_MIRRORED_REPEAT
self.engine.renderer.enqueue proc()=
self.bind_it(needs_active_texture=true)
glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_S, e.GLint)
glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_T, e.GLint)
glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_R, e.GLint)
proc getTexturePixels*(self: Texture): TexturePixels =
when false:
var len_bytes, cube_stride = self.width * self.height * self.format.stride
if self.tex_type == TexCube:
len_bytes *= 6
result.pixels = newArrRef[int8](len_bytes).to(float32)
result.width = self.width
result.height = self.height
result.format = self.format
result.tex_type = self.tex_type
self.bind_it(needs_active_texture=true)
if self.tex_type == TexCube:
var p = cast[ptr UncheckedArray[int8]](result.pixels.toPointer)
var pos = 0
for i in GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint .. GL_TEXTURE_CUBE_MAP_NEGATIVE_Z.GLuint:
glGetTexImage(i.GLenum, 0, self.storage.format, self.storage.gltype,
p[pos].addr)
pos += cube_stride
else:
glGetTexImage(self.storage.target, 0, self.storage.format, self.storage.gltype, result.pixels.toPointer)
# TODO: limit the amount of mip maps to save some draw calls
# (e.g. minimum resolution 16x16 for blurred mipmaps)
proc setMipmapRange*(self: Texture, first = 0, last = 1000) {.gcsafe.} =
self.bind_it(needs_active_texture=true)
self.mipmap_range = (first, last)
glTexParameteri(self.storage.target, GL_TEXTURE_BASE_LEVEL, first.GLint);
glTexParameteri(self.storage.target, GL_TEXTURE_MAX_LEVEL, last.GLint);
func vec3size*(self: Texture, mip_level = -1): Vec3 =
let depth = if self.tex_type == Tex2DArray: 1 else: self.depth
let mip_level = if mip_level != -1: mip_level
else: self.mipmap_range[0]
vec3((max(1, self.width shr mip_level)).float32,
(max(1, self.height shr mip_level)).float32,
(max(1, depth shr mip_level)).float32)
proc set_texture_shadow*(self: Texture) =
self.bind_it(needs_active_texture=true)
if self.format in [Depth_u16, Depth_u24, Depth_f32]:
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.GLint)
# TODO: would GL_LESS be better? That way depth 1.0 would always be not in shadow
# so we wouldn't need to clamp in shader
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.GLint)
proc unset_texture_shadow*(self: Texture) =
self.bind_it(needs_active_texture=true)
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_NONE.GLint)