package ru.dbotthepony.kstarbound.client.gl import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL46.* import org.lwjgl.stb.STBImage import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kvector.vector.Vector2i import java.io.File import java.io.FileNotFoundException import java.nio.ByteBuffer import kotlin.reflect.KProperty class TextureLoadingException(message: String) : Throwable(message) data class UVCoord(val u: Float, val v: Float) private class GLTexturePropertyTracker(private val flag: Int, private var value: Int) { operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int { return value } operator fun setValue(thisRef: GLTexture2D, property: KProperty<*>, value: Int) { thisRef.state.ensureSameThread() if (this.value == value) return this.value = value glTextureParameteri(thisRef.pointer, flag, value) checkForGLError() } } @Suppress("SameParameterValue") class GLTexture2D(val state: GLStateTracker, val name: String = "") { init { state.ensureSameThread() } val pointer = glGenTextures() init { checkForGLError() state.registerCleanable(this, ::glDeleteTextures, pointer) } var width = 0 private set var height = 0 private set var uploaded = false private set val aspectRatioWH: Float get() { if (height == 0) { return 1f } return width.toFloat() / height.toFloat() } val aspectRatioHW: Float get() { if (width == 0) { return 1f } return height.toFloat() / width.toFloat() } var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR) var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR) var maxLevel by GLTexturePropertyTracker(GL_TEXTURE_MAX_LEVEL, 1000) var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT) var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT) fun bind(): GLTexture2D { state.texture2D = this return this } fun generateMips(): GLTexture2D { state.ensureSameThread() glGenerateTextureMipmap(pointer) checkForGLError("Generating texture mipmaps") return this } fun pixelToUV(x: Float, y: Float): UVCoord { check(uploaded) { "Texture is not uploaded to be used" } return UVCoord(x / width, y / height) } fun pixelToUV(x: Int, y: Int): UVCoord { check(uploaded) { "Texture is not uploaded to be used" } return UVCoord(x.toFloat() / width, y.toFloat() / height) } fun pixelToUV(pos: Vector2i) = pixelToUV(pos.x, pos.y) fun allocate(mipmap: Int, loadedFormat: Int, width: Int, height: Int): GLTexture2D { bind() require(width > 0) { "Invalid width $width" } require(height > 0) { "Invalid height $height" } this.width = width this.height = height glTexImage2D(GL_TEXTURE_2D, mipmap, loadedFormat, width, height, 0, loadedFormat, GL_UNSIGNED_BYTE, 0L) checkForGLError() uploaded = true return this } fun allocate(loadedFormat: Int, width: Int, height: Int): GLTexture2D { return allocate(0, loadedFormat, width, height) } private fun upload(mipmap: Int, loadedFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: IntArray): GLTexture2D { bind() require(width > 0) { "Invalid width $width" } require(height > 0) { "Invalid height $height" } this.width = width this.height = height glTexImage2D(GL_TEXTURE_2D, mipmap, loadedFormat, width, height, 0, bufferFormat, dataFormat, data) checkForGLError() uploaded = true return this } private fun upload(mipmap: Int, memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { bind() require(width > 0) { "Invalid width $width" } require(height > 0) { "Invalid height $height" } this.width = width this.height = height glTexImage2D(GL_TEXTURE_2D, mipmap, memoryFormat, width, height, 0, bufferFormat, dataFormat, data) checkForGLError() uploaded = true return this } fun upload(memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: IntArray): GLTexture2D { return upload(0, memoryFormat, width, height, bufferFormat, dataFormat, data) } fun upload(memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { return upload(0, memoryFormat, width, height, bufferFormat, dataFormat, data) } fun upload(path: File, memoryFormat: Int, bufferFormat: Int): GLTexture2D { state.ensureSameThread() if (!path.exists()) { throw FileNotFoundException("${path.absolutePath} does not exist") } if (!path.isFile) { throw FileNotFoundException("${path.absolutePath} is not a file") } val getwidth = intArrayOf(0) val getheight = intArrayOf(0) val getchannels = intArrayOf(0) val bytes = STBImage.stbi_load(path.absolutePath, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${path.absolutePath}. Is it a valid image?") require(getwidth[0] > 0) { "Image ${path.absolutePath} has bad width of ${getwidth[0]}" } require(getheight[0] > 0) { "Image ${path.absolutePath} has bad height of ${getheight[0]}" } upload(memoryFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) STBImage.stbi_image_free(bytes) return this } fun upload(path: File): GLTexture2D { state.ensureSameThread() if (!path.exists()) { throw FileNotFoundException("${path.absolutePath} does not exist") } if (!path.isFile) { throw FileNotFoundException("${path.absolutePath} is not a file") } val getwidth = intArrayOf(0) val getheight = intArrayOf(0) val getchannels = intArrayOf(0) val bytes = STBImage.stbi_load(path.absolutePath, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${path.absolutePath}. Is it a valid image?") require(getwidth[0] > 0) { "Image ${path.absolutePath} has bad width of ${getwidth[0]}" } require(getheight[0] > 0) { "Image ${path.absolutePath} has bad height of ${getheight[0]}" } val bufferFormat = when (val numChannels = getchannels[0]) { 1 -> GL_R 2 -> GL_RG 3 -> GL_RGB 4 -> GL_RGBA else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels") } upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) STBImage.stbi_image_free(bytes) return this } fun upload(buff: ByteBuffer, memoryFormat: Int, bufferFormat: Int): GLTexture2D { state.ensureSameThread() val getwidth = intArrayOf(0) val getheight = intArrayOf(0) val getchannels = intArrayOf(0) val bytes = STBImage.stbi_load_from_memory(buff, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${buff}. Is it a valid image?") require(getwidth[0] > 0) { "Image '$name' has bad width of ${getwidth[0]}" } require(getheight[0] > 0) { "Image '$name' has bad height of ${getheight[0]}" } upload(memoryFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) STBImage.stbi_image_free(bytes) return this } fun upload(buff: ByteBuffer): GLTexture2D { state.ensureSameThread() val getwidth = intArrayOf(0) val getheight = intArrayOf(0) val getchannels = intArrayOf(0) val bytes = STBImage.stbi_load_from_memory(buff, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${buff}. Is it a valid image?") require(getwidth[0] > 0) { "Image '$name' has bad width of ${getwidth[0]}" } require(getheight[0] > 0) { "Image '$name' has bad height of ${getheight[0]}" } val bufferFormat = when (val numChannels = getchannels[0]) { 1 -> GL_R 2 -> GL_RG 3 -> GL_RGB 4 -> GL_RGBA else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels") } upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) STBImage.stbi_image_free(bytes) return this } fun upload(data: Image): GLTexture2D { state.ensureSameThread() val bufferFormat = when (val numChannels = data.amountOfChannels) { 1 -> GL_R 2 -> GL_RG 3 -> GL_RGB 4 -> GL_RGBA else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels") } upload(bufferFormat, data.width, data.height, bufferFormat, GL_UNSIGNED_BYTE, data.data) return this } companion object { private val LOGGER = LogManager.getLogger() } }