diff --git a/build.gradle.kts b/build.gradle.kts index 5785e134..8205bd6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ dependencies { implementation("com.github.jnr:jnr-ffi:2.2.13") implementation("ru.dbotthepony:kbox2d:2.4.1.6") - implementation("ru.dbotthepony:kvector:2.9.0") + implementation("ru.dbotthepony:kvector:2.9.2") implementation("com.github.ben-manes.caffeine:caffeine:3.1.5") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d425e712..65ba5c45 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -8,7 +8,6 @@ import org.lwjgl.glfw.Callbacks import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFWErrorCallback import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL45.* import org.lwjgl.opengl.GLCapabilities import org.lwjgl.system.MemoryStack @@ -30,6 +29,8 @@ import ru.dbotthepony.kstarbound.client.gl.properties.GLGenericTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLObjectTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker +import ru.dbotthepony.kstarbound.client.gl.properties.GLTexturesTracker +import ru.dbotthepony.kstarbound.client.gl.shader.FontProgram import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms import ru.dbotthepony.kstarbound.client.gl.shader.GLShader import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram @@ -72,6 +73,7 @@ import java.util.* import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.ReentrantLock import kotlin.collections.ArrayList +import kotlin.math.absoluteValue import kotlin.math.roundToInt class StarboundClient : Closeable { @@ -260,15 +262,7 @@ class StarboundClient : Closeable { .weakValues() .build() - private val missingTexture: GLTexture2D by lazy { - newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also { - it.textureMinFilter = GL_NEAREST - it.textureMagFilter = GL_NEAREST - } - } - - private val missingTexturePath = "/assetmissing.png" - private val regularShaderPrograms = ArrayList>() + private val fontShaderPrograms = ArrayList>() private val uberShaderPrograms = ArrayList>() val lightMapLocation = maxTextureBlocks - 1 @@ -278,8 +272,8 @@ class StarboundClient : Closeable { uberShaderPrograms.add(WeakReference(program)) } - if (program is GLShaderProgram.Regular) { - regularShaderPrograms.add(WeakReference(program)) + if (program is FontProgram) { + fontShaderPrograms.add(WeakReference(program)) } } @@ -339,23 +333,7 @@ class StarboundClient : Closeable { var framebuffer by GLObjectTracker(::glBindFramebuffer, GL_FRAMEBUFFER) var program by GLObjectTracker(::glUseProgram) - private val textures = Array(maxTextureBlocks) { GLObjectTracker(GL11::glBindTexture, GL_TEXTURE_2D) } - - var activeTexture = 0 - set(value) { - ensureSameThread() - - if (field != value) { - require(value in 0 until maxTextureBlocks) { "Texture block out of bounds: $value (max texture blocks is $maxTextureBlocks)" } - glActiveTexture(GL_TEXTURE0 + value) - checkForGLError() - field = value - } - } - - var texture2D: GLTexture2D? - get() = textures[activeTexture].get() - set(value) { textures[activeTexture].accept(value) } + val textures2D = GLTexturesTracker(maxTextureBlocks) var clearColor by GLGenericTracker(RGBAColor.WHITE) { val (r, g, b, a) = it @@ -376,7 +354,8 @@ class StarboundClient : Closeable { checkForGLError() } - val whiteTexture = GLTexture2D("white") + val whiteTexture = GLTexture2D(1, 1, GL_RGB8) + val missingTexture = GLTexture2D(8, 8, GL_RGB8) init { val buffer = ByteBuffer.allocateDirect(3) @@ -384,7 +363,42 @@ class StarboundClient : Closeable { buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte()) buffer.position(0) - whiteTexture.upload(GL_RGB, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, buffer) + whiteTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) + } + + init { + val buffer = ByteBuffer.allocateDirect(3 * 8 * 8) + + for (row in 0 until 4) { + for (x in 0 until 4) { + buffer.put(0x80.toByte()) + buffer.put(0x0.toByte()) + buffer.put(0x80.toByte()) + } + + for (x in 0 until 4) { + buffer.put(0x0.toByte()) + buffer.put(0x0.toByte()) + buffer.put(0x0.toByte()) + } + } + + for (row in 0 until 4) { + for (x in 0 until 4) { + buffer.put(0x0.toByte()) + buffer.put(0x0.toByte()) + buffer.put(0x0.toByte()) + } + + for (x in 0 until 4) { + buffer.put(0x80.toByte()) + buffer.put(0x0.toByte()) + buffer.put(0x80.toByte()) + } + } + + buffer.position() + missingTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) } fun setViewport(x: Int, y: Int, width: Int, height: Int) { @@ -454,7 +468,6 @@ class StarboundClient : Closeable { } fun isSameThread() = thread === Thread.currentThread() - fun newTexture(name: String = "") = GLTexture2D(name) fun loadTexture(path: String): GLTexture2D { ensureSameThread() @@ -464,13 +477,27 @@ class StarboundClient : Closeable { val data = Image.get(it) if (data == null) { - LOGGER.error("Texture {} is missing! Falling back to {}", it, missingTexturePath) + LOGGER.error("Texture $it is not found, falling back to missing texture") missingTexture } else { - newTexture(it).upload(data).also { - it.textureMinFilter = GL_NEAREST - it.textureMagFilter = GL_NEAREST - } + val tex = GLTexture2D(data.width, data.height, when (data.amountOfChannels) { + 1 -> GL_R8 + 3 -> GL_RGB8 + 4 -> GL_RGBA8 + else -> throw IllegalArgumentException("Unknown amount of channels in $it: ${data.amountOfChannels}") + }) + + tex.upload(when (data.amountOfChannels) { + 1 -> GL_RED + 3 -> GL_RGB + 4 -> GL_RGBA + else -> throw IllegalArgumentException("Unknown amount of channels in $it: ${data.amountOfChannels}") + }, GL_UNSIGNED_BYTE, data.data) + + tex.textureMinFilter = GL_NEAREST + tex.textureMagFilter = GL_NEAREST + + tex } } } @@ -489,7 +516,7 @@ class StarboundClient : Closeable { programs.position.use() programs.position.colorMultiplier = color - programs.position.worldMatrix = stack.last() + programs.position.modelMatrix = stack.last() builder.draw(GL_LINES) } @@ -632,7 +659,9 @@ class StarboundClient : Closeable { var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) private set - val viewportLightingTexture = newTexture("Viewport Lighting") + var viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8) + private set + private var viewportLightingMem: ByteBuffer? = null fun updateViewportParams() { @@ -643,8 +672,14 @@ class StarboundClient : Closeable { viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16 viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16 - viewportCellWidth = roundTowardsPositiveInfinity(viewportRectangle.width) + 32 - viewportCellHeight = roundTowardsPositiveInfinity(viewportRectangle.height) + 32 + val viewportCellWidth0 = roundTowardsPositiveInfinity(viewportRectangle.width) + 32 + val viewportCellHeight0 = roundTowardsPositiveInfinity(viewportRectangle.height) + 32 + + if ((viewportCellWidth0 - viewportCellWidth).absoluteValue > 1) + viewportCellWidth = viewportCellWidth0 + + if ((viewportCellHeight0 - viewportCellHeight).absoluteValue > 1) + viewportCellHeight = viewportCellHeight0 viewportTopRight = screenToWorld(viewportWidth, viewportHeight) viewportBottomLeft = screenToWorld(0, 0) @@ -655,6 +690,7 @@ class StarboundClient : Closeable { if (viewportCellWidth > 0 && viewportCellHeight > 0) { viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3) + viewportLightingTexture = GLTexture2D(viewportCellWidth.coerceAtMost(4096), viewportCellHeight.coerceAtMost(4096), GL_RGB8) } else { viewportLightingMem = null } @@ -681,6 +717,8 @@ class StarboundClient : Closeable { onPostDrawWorldOnce.add(lambda) } + private val layers = LayeredRenderer() + fun renderFrame(): Boolean { ensureSameThread() @@ -711,14 +749,14 @@ class StarboundClient : Closeable { cleanup() GLFW.glfwPollEvents() - if (world != null) { - if (Starbound.initialized) - world.think() - } + if (world != null && Starbound.initialized) + world.think() return true } + layers.clear() + uberShaderPrograms.forEachValid { if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) { it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat()) @@ -727,7 +765,6 @@ class StarboundClient : Closeable { if (world != null) { updateViewportParams() - val layers = LayeredRenderer() if (Starbound.initialized) world.think() @@ -741,7 +778,8 @@ class StarboundClient : Closeable { .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера .translate(-camera.pos.x.toFloat(), -camera.pos.y.toFloat()) // перемещаем вид к камере - regularShaderPrograms.forEachValid { it.viewMatrix = viewMatrix } + uberShaderPrograms.forEachValid { it.viewMatrix = viewMatrix } + fontShaderPrograms.forEachValid { it.viewMatrix = viewMatrix } for (lambda in onPreDrawWorld) { lambda.invoke(layers) @@ -763,16 +801,13 @@ class StarboundClient : Closeable { viewportLightingMem.position(0) BufferUtils.zeroBuffer(viewportLightingMem) viewportLightingMem.position(0) - viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096)) + viewportLighting.calculate(viewportLightingMem, viewportLightingTexture.width, viewportLightingTexture.height) viewportLightingMem.position(0) val old = textureUnpackAlignment - textureUnpackAlignment = if (viewportLighting.width.coerceAtMost(4096) % 4 == 0) 4 else 1 + textureUnpackAlignment = if (viewportLightingTexture.width.coerceAtMost(4096) % 4 == 0) 4 else 1 viewportLightingTexture.upload( - GL_RGB, - viewportLighting.width.coerceAtMost(4096), - viewportLighting.height.coerceAtMost(4096), GL_RGB, GL_UNSIGNED_BYTE, viewportLightingMem @@ -780,14 +815,11 @@ class StarboundClient : Closeable { textureUnpackAlignment = old } else { - viewportLightingTexture.upload(GL_RGBA, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, WHITE) + viewportLightingTexture = whiteTexture } viewportLightingTexture.textureMinFilter = GL_LINEAR - - activeTexture = lightMapLocation - texture2D = viewportLightingTexture - activeTexture = 0 + textures2D[lightMapLocation] = viewportLightingTexture val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f( ((viewportBottomLeft.x - viewportCellX) / viewportLighting.width).toFloat(), @@ -809,7 +841,8 @@ class StarboundClient : Closeable { } } - regularShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } + uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } + fontShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } val thisTime = System.currentTimeMillis() @@ -847,7 +880,7 @@ class StarboundClient : Closeable { font.render("Latency: ${(averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f) font.render("Frame: ${(averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f) font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f) - font.render("OGL A: ${objectsCreated - objectsCleaned} D: $objectsCleaned", y = font.lineHeight * 1.8f, scale = 0.4f) + font.render("OGL C: $objectsCreated D: $objectsCleaned A: ${objectsCreated - objectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt index 7d6d6b47..0b371841 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt @@ -9,10 +9,12 @@ sealed class BufferObject : GLObject() { final override val client = StarboundClient.current() abstract val glType: Int + @Deprecated("Has no effect", level = DeprecationLevel.ERROR) override fun bind() { // do nothing } + @Deprecated("Has no effect", level = DeprecationLevel.ERROR) override fun unbind() { // do nothing } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt index 26f62ec5..2cf209c8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt @@ -33,8 +33,7 @@ class GLFrameBuffer : GLObject() { throw IllegalStateException("Already has texture attached") } - val texture = GLTexture2D("framebuffer_$pointer") - texture.allocate(format, width, height) + val texture = GLTexture2D(width, height, format) val old = client.framebuffer bind() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt index 433a3176..974d4067 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.client.gl import org.apache.logging.log4j.LogManager +import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL45.* import org.lwjgl.stb.STBImage import ru.dbotthepony.kstarbound.client.StarboundClient @@ -30,24 +31,23 @@ private class GLTexturePropertyTracker(private val flag: Int, private var value: } @Suppress("SameParameterValue") -class GLTexture2D(val name: String = "") : GLObject() { +class GLTexture2D(val width: Int, val height: Int, val format: Int, val levels: Int = 1) : GLObject() { + init { + require(width > 0 && height > 0) { "Invalid image size: $width x $height" } + require(levels > 0) { "Invalid image level count: $levels" } + } + override val client = StarboundClient.current() override val pointer = glCreateTextures(GL_TEXTURE_2D) init { - checkForGLError() + checkForGLError("Creating texture") client.registerCleanable(this, ::glDeleteTextures, pointer) + + glTextureStorage2D(pointer, levels, format, width, height) + checkForGLError("Invalid image format: $format") } - var width = 0 - private set - - var height = 0 - private set - - var uploaded = false - private set - val aspectRatioWH: Float get() { if (height == 0) { return 1f @@ -71,13 +71,14 @@ class GLTexture2D(val name: String = "") : GLObject() { var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT) var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT) + @Deprecated("Has no effect", level = DeprecationLevel.ERROR) override fun bind() { - client.texture2D = this + // do nothing } + @Deprecated("Has no effect", level = DeprecationLevel.ERROR) override fun unbind() { - if (client.texture2D === this) - client.texture2D = null + // do nothing } fun generateMips(): GLTexture2D { @@ -88,186 +89,31 @@ class GLTexture2D(val name: String = "") : GLObject() { } 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 upload(level: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { + return upload(level, 0, 0, width, height, bufferFormat, dataFormat, data) } - fun allocate(loadedFormat: Int, width: Int, height: Int): GLTexture2D { - return allocate(0, loadedFormat, width, height) + fun upload(bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { + return upload(0, 0, 0, width, height, bufferFormat, dataFormat, data) } - 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 { - client.ensureSameThread() - - if (!path.exists()) { - throw FileNotFoundException("${path.absolutePath} does not exist") + fun upload(level: Int, x: Int, y: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { + require(x >= 0 && y >= 0 && width > 0 && height > 0 && x + width <= this.width && y + height <= this.height) { + "Bad texture rectangle specified: $x x $y -> ${x + width} x ${y + height}, which is out of bounds for texture ${this.width} x ${this.height}" } - 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 { - client.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 { - client.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 { - client.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 { - client.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) - + require(level in 0 until levels) { "Level out of bounds: $level" } + glTextureSubImage2D(pointer, level, x, y, width, height, bufferFormat, dataFormat, data) + checkForGLError("Uploading texture data") return this } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLTexturesTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLTexturesTracker.kt new file mode 100644 index 00000000..4dadee9e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLTexturesTracker.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.client.gl.properties + +import org.lwjgl.opengl.GL45 +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.gl.GLObject +import ru.dbotthepony.kstarbound.client.gl.checkForGLError + +class GLTexturesTracker(maxValue: Int) { + private val client = StarboundClient.current() + private val values = arrayOfNulls(maxValue) + + operator fun get(index: Int): T? { + return values[index] as T? + } + + operator fun set(index: Int, value: T?) { + client.ensureSameThread() + require(value == null || client === value.client) { "$value does not belong to $client" } + + if (values[index] === value) + return + + if (value == null) { + GL45.glBindTextureUnit(index, 0) + checkForGLError() + } else { + GL45.glBindTextureUnit(index, value.pointer) + checkForGLError() + } + + values[index] = value + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/TexturesTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/TexturesTracker.kt deleted file mode 100644 index 60f9e7af..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/TexturesTracker.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ru.dbotthepony.kstarbound.client.gl.properties - -import org.lwjgl.opengl.GL11.GL_TEXTURE_2D -import org.lwjgl.opengl.GL11.glBindTexture -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.client.gl.GLTexture2D -import ru.dbotthepony.kstarbound.client.gl.checkForGLError -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class TexturesTracker(maxValue: Int) : ReadWriteProperty { - private val values = arrayOfNulls(maxValue) - - override fun getValue(thisRef: StarboundClient, property: KProperty<*>): GLTexture2D? { - return values[thisRef.activeTexture] - } - - override fun setValue(thisRef: StarboundClient, property: KProperty<*>, value: GLTexture2D?) { - thisRef.ensureSameThread() - - require(value == null || thisRef === value.client) { "$value does not belong to $thisRef" } - - if (values[thisRef.activeTexture] === value) { - return - } - - values[thisRef.activeTexture] = value - - if (value == null) { - glBindTexture(GL_TEXTURE_2D, 0) - checkForGLError() - return - } - - glBindTexture(GL_TEXTURE_2D, value.pointer) - checkForGLError() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt index a6002a88..003f6620 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt @@ -28,13 +28,6 @@ open class GLShaderProgram( shaders: Iterable, val attributes: VertexAttributes ) : GLObject() { - interface Regular { - var viewMatrix: Matrix3f // set before rendering anything (camera and/or projection) - var worldMatrix: Matrix3f // global matrix stack - var modelMatrix: Matrix3f // should be set by drawing code itself - var colorMultiplier: IStruct4f - } - final override val client = StarboundClient.current() final override val pointer = glCreateProgram() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/Programs.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/Programs.kt index bea1c0f0..c3fe6756 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/Programs.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/Programs.kt @@ -22,38 +22,10 @@ private fun shaders1(name: String): List { return listOf(client.internalVertex("shaders/vertex/$name.vsh"), client.internalFragment("shaders/fragment/$name.fsh")) } -private fun fontShaders(): List { - val client = StarboundClient.current() - var read = StarboundClient.readInternal("shaders/fragment/font.fsh") - - val textures = ArrayList() - val cases = ArrayList() - val cases2 = ArrayList() - - for (i in 0 until client.maxUserTextureBlocks) { - cases.add("\tvec4 colorResult$i = colorMultiplier * texture(texture$i, uvOut).r;") - } - - for (i in 0 until client.maxUserTextureBlocks) { - textures.add("layout (binding=$i) uniform sampler2D texture$i;") - cases2.add("if (textureIndexOut < ${i + 0.1f}) { colorResult = colorResult$i; }") - } - - cases2.add(" { colorResult = colorMultiplier; }") - - read = read.replace("uniform sampler2D texture0;", textures.joinToString("\n")).replace("\tcolorResult = colorMultiplier * texture(texture0, uvOut).r;", cases.joinToString("\n") + cases2.joinToString("\n\telse ")) - - val fragment = GLShader(read, GL_FRAGMENT_SHADER) - val vertex = GLShader(StarboundClient.readInternal("shaders/vertex/font.vsh"), GL_VERTEX_SHADER) - - return listOf(fragment, vertex) -} - -class FontProgram : GLShaderProgram(shaders1("font"), FONT_VERTEX_FORMAT), GLShaderProgram.Regular { - override var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix") - override var worldMatrix: Matrix3f by F3x3Uniform("worldMatrix") - override var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix", true) - override var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier") +class FontProgram : GLShaderProgram(shaders1("font"), FONT_VERTEX_FORMAT) { + var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix") + var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix") + var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier") var texture by IUniform("texture0") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/UberShader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/UberShader.kt index 99ee3cf4..94d6c696 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/UberShader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/UberShader.kt @@ -2,33 +2,37 @@ package ru.dbotthepony.kstarbound.client.gl.shader import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.ObjectArraySet +import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.gl.GLTexture2D import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributeType import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes +import ru.dbotthepony.kstarbound.client.render.RenderConfig import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.arrays.Matrix3f import ru.dbotthepony.kvector.vector.RGBAColor import java.util.Collections import java.util.EnumSet +import java.util.function.Function class UberShader private constructor( fragment: GLShader, vertex: GLShader, attributes: VertexAttributes, val flags: Set, -) : GLShaderProgram(listOf(fragment, vertex), attributes), GLShaderProgram.Regular { +) : GLShaderProgram(listOf(fragment, vertex), attributes) { enum class Flag { LIGHT_MAP, NEEDS_SCREEN_SIZE; } - override var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix") - override var worldMatrix: Matrix3f by F3x3Uniform("worldMatrix") - override var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix") - override var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier") + var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix") + var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix") + var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier") var texture0 by IUniform("texture0", VertexAttributeType.UV !in attributes) @@ -40,11 +44,24 @@ class UberShader private constructor( init { viewMatrix = Matrix3f.identity() - worldMatrix = Matrix3f.identity() modelMatrix = Matrix3f.identity() colorMultiplier = RGBAColor.WHITE } + private val textureConfigs = Reference2ObjectOpenHashMap>() + + fun config(texture: GLTexture2D): RenderConfig { + return textureConfigs.computeIfAbsent(texture, Reference2ObjectFunction { + object : RenderConfig(this) { + override fun setup() { + super.setup() + + client.textures2D[0] = texture + } + } + }) + } + class Builder { private val directives = Object2ObjectArrayMap() private val attributes = ObjectArraySet() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt index d687aa04..478be315 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt @@ -12,7 +12,7 @@ import ru.dbotthepony.kstarbound.client.gl.checkForGLError class StreamVertexBuilder( attributes: VertexAttributes, type: GeometryType? = null, - initialCapacity: Int = 64, + initialCapacity: Int = 32, ) { val state = StarboundClient.current() val builder = VertexBuilder(attributes, type, initialCapacity) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt index af5d0d78..c0de4a3f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt @@ -51,7 +51,7 @@ private fun indexSize(type: Int): Int { class VertexBuilder( val attributes: VertexAttributes, val defaultMode: GeometryType? = null, - initialCapacity: Int = 64 + initialCapacity: Int = 32 ) { constructor(attributes: VertexAttributes, initialCapacity: Int) : this(attributes, null, initialCapacity) @@ -88,6 +88,14 @@ class VertexBuilder( var elementCount = 0 // GeometryType specific (quads, triangles, lines), less than or equal (points) to vertexCount private set + fun isEmpty(): Boolean { + return !inVertex && vertexCount == 0 + } + + fun isNotEmpty(): Boolean { + return inVertex || vertexCount != 0 + } + private fun ensureCapacity() { val mode = mode ?: return @@ -177,8 +185,12 @@ class VertexBuilder( } check(elementVertices == 0) { "Can't change buffer geometry type while not having fully built current element" } - this.mode = mode - ensureCapacity() + + if (this.mode !== mode) { + this.mode = mode + ensureCapacity() + } + return this } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt index 5479bac1..45c0e004 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt @@ -43,8 +43,7 @@ class Box2DRenderer : IDebugDraw { state.programs.position.use() state.programs.position.colorMultiplier = color - state.programs.position.worldMatrix = state.stack.last() - state.programs.position.modelMatrix = identity + state.programs.position.modelMatrix = state.stack.last() builder.draw(GL_LINES) } @@ -68,8 +67,7 @@ class Box2DRenderer : IDebugDraw { state.programs.position.use() state.programs.position.colorMultiplier = color - state.programs.position.worldMatrix = state.stack.last() - state.programs.position.modelMatrix = identity + state.programs.position.modelMatrix = state.stack.last() builder.draw(GL_TRIANGLES) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt index 12f234ce..cd0e4ac6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt @@ -98,16 +98,13 @@ class Font( Vector2i(roundTowardsPositiveInfinity(face.nativeMemory.bbox.xMax.toInt() / (12.0 * 48.0 / size)), roundTowardsPositiveInfinity(face.nativeMemory.bbox.yMax.toInt() / (12.0 * 48.0 / size))), ) - private val atlas = GLTexture2D() private val atlasWidth: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_WIDTH).coerceAtMost(4096) private val atlasHeight: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_HEIGHT).coerceAtMost(4096) private var nextAtlasX = 0 private var nextAtlasY = 0 + private val atlas = GLTexture2D(atlasWidth, atlasHeight, GL_R8) init { - glTextureStorage2D(atlas.pointer, 1, GL_R8, atlasWidth, atlasHeight) - checkForGLError() - atlas.textureMinFilter = GL_LINEAR atlas.textureMagFilter = GL_LINEAR atlas.textureWrapS = GL_CLAMP_TO_EDGE @@ -217,12 +214,11 @@ class Font( if (builder.elementCount != 0) { client.programs.font.use() client.programs.font.colorMultiplier = color - client.programs.font.worldMatrix = client.stack.last() + client.programs.font.modelMatrix = client.stack.last() builder.upload(vbo, ebo, GL_STREAM_DRAW) - client.activeTexture = 0 - atlas.bind() + client.textures2D[0] = atlas client.vao = vao glDrawElements(GL_TRIANGLES, builder.indexCount, builder.indexType, 0L) @@ -341,8 +337,7 @@ class Font( try { uv = Vector4f(nextAtlasX / atlasWidth.toFloat(), nextAtlasY / atlasHeight.toFloat(), (nextAtlasX + bitmap.width) / atlasWidth.toFloat(), (nextAtlasY + bitmap.rows) / atlasHeight.toFloat()) - glTextureSubImage2D(atlas.pointer, 0, nextAtlasX, nextAtlasY, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff) - checkForGLError("Uploading glyph texture data") + atlas.upload(0, nextAtlasX, nextAtlasY, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff) nextAtlasX += bbox.width diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt index d940cba1..65bf5919 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt @@ -1,28 +1,116 @@ package ru.dbotthepony.kstarbound.client.render -import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap -import ru.dbotthepony.kvector.arrays.Matrix3fStack -import ru.dbotthepony.kvector.arrays.Matrix4fStack +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap +import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder +import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import java.util.function.Function +import java.util.stream.Stream -/** - * Позволяет вызывать отрисовщики в определённой (послойной) последовательности - */ -class LayeredRenderer { - private val layers = Long2ObjectAVLTreeMap Unit>>() +interface IGeometryLayer { + fun add(renderer: () -> Unit) + fun getBuilder(config: RenderConfig<*>): VertexBuilder + fun render() + fun clear() + fun bakeIntoMeshes(): Stream, RenderLayer.Point>> +} - fun add(layer: Long, renderer: () -> Unit) { - layers.computeIfAbsent(layer, Function { ArrayList() }).add(renderer) +object OneShotGeometryLayer : IGeometryLayer { + override fun add(renderer: () -> Unit) { + TODO("Not yet implemented") } - fun render() { - for (list in layers.values) { - for (renderer in list) { - renderer.invoke() + override fun getBuilder(config: RenderConfig<*>): VertexBuilder { + TODO("Not yet implemented") + } + + override fun render() { + TODO("Not yet implemented") + } + + override fun clear() { + TODO("Not yet implemented") + } + + override fun bakeIntoMeshes(): Stream, RenderLayer.Point>> { + TODO("Not yet implemented") + } +} + +class LayeredRenderer { + class Layer(val layer: RenderLayer.Point) : IGeometryLayer { + private val meshes = ArrayList>() + private val callbacks = ArrayList<() -> Unit>() + private val builders = Reference2ObjectLinkedOpenHashMap, StreamVertexBuilder>() + + override fun add(renderer: () -> Unit) { + callbacks.add(renderer) + } + + override fun getBuilder(config: RenderConfig<*>): VertexBuilder { + return builders.computeIfAbsent(config, Function { + StreamVertexBuilder(config.program.attributes, initialCapacity = config.initialBuilderCapacity) + }).builder + } + + override fun render() { + builders.entries.forEach { + if (it.value.builder.isNotEmpty()) { + it.key.setup() + it.value.upload() + it.value.draw() + it.key.uninstall() + } + } + + meshes.forEach { + it.render() + } + + callbacks.forEach { + it.invoke() } } - layers.clear() + override fun clear() { + builders.values.forEach { it.builder.begin() } + meshes.clear() + callbacks.clear() + } + + override fun bakeIntoMeshes(): Stream, RenderLayer.Point>> { + if (meshes.isNotEmpty() || callbacks.isNotEmpty()) + throw IllegalStateException("This layer (index $layer) contains preconfigured meshes and/or callbacks") + + return builders.entries.stream() + .filter { it.value.builder.isNotEmpty() } + .map { ConfiguredMesh(it.key, Mesh(it.value.builder)) to layer } + } + } + + private val layers = Object2ObjectAVLTreeMap() + + fun getBuilder(layer: RenderLayer.Point, config: RenderConfig<*>): VertexBuilder { + return getLayer(layer).getBuilder(config) + } + + fun bakeIntoMeshes(): Stream, RenderLayer.Point>> { + return layers.values.stream().flatMap { it.bakeIntoMeshes() } + } + + fun add(layer: RenderLayer.Point, renderer: () -> Unit) { + getLayer(layer).add(renderer) + } + + fun getLayer(layer: RenderLayer.Point): Layer { + return layers.computeIfAbsent(layer, Function { Layer(it) }) + } + + fun render() { + layers.values.forEach { it.render() } + } + + fun clear() { + layers.values.forEach { it.clear() } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Mesh.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Mesh.kt index 18d213ee..19e5d32c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Mesh.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Mesh.kt @@ -7,6 +7,9 @@ import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import ru.dbotthepony.kvector.arrays.Matrix4f +/** + * mesh is container for paired VAO, VBO and EBO, and metadata for them + */ class Mesh() { constructor(builder: VertexBuilder) : this() { load(builder, GL45.GL_STATIC_DRAW) @@ -44,6 +47,9 @@ class Mesh() { } } +/** + * Mesh + RenderConfig + */ data class ConfiguredMesh(val config: RenderConfig, val mesh: Mesh = Mesh()) { fun render() { config.setup() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/MultiMeshBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/MultiMeshBuilder.kt deleted file mode 100644 index 08c506fe..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/MultiMeshBuilder.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ru.dbotthepony.kstarbound.client.render - -import it.unimi.dsi.fastutil.longs.Long2ObjectFunction -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction -import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap -import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder -import java.util.stream.Stream - -class MultiMeshBuilder { - data class Entry(val config: RenderConfig<*>, val builder: VertexBuilder, val layer: Long) - - private val meshes = Reference2ObjectOpenHashMap, Long2ObjectOpenHashMap>() - - fun get(config: RenderConfig<*>, layer: Long): VertexBuilder { - return meshes.computeIfAbsent(config, Reference2ObjectFunction { - Long2ObjectOpenHashMap() - }).computeIfAbsent(layer, Long2ObjectFunction { Entry(config, VertexBuilder(config.program.attributes, config.initialBuilderCapacity), layer) }).builder - } - - fun clear() = meshes.clear() - fun isEmpty() = meshes.isEmpty() - fun isNotEmpty() = meshes.isNotEmpty() - - fun meshes(): Stream { - return meshes.values.stream().flatMap { it.values.stream() } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderConfig.kt index 6c182abf..8c7ace3d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderConfig.kt @@ -5,7 +5,7 @@ import ru.dbotthepony.kvector.arrays.Matrix4f abstract class RenderConfig(val program: T) { val client get() = program.client - open val initialBuilderCapacity: Int get() = 64 + open val initialBuilderCapacity: Int get() = 32 open fun setup() { program.use() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayers.kt index 5c94792f..2dc4c6cf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayers.kt @@ -1,81 +1,112 @@ package ru.dbotthepony.kstarbound.client.render +import com.google.common.collect.ImmutableMap import org.apache.logging.log4j.LogManager -const val RenderLayerUpperBits = 5 -const val RenderLayerLowerBits = 32 - RenderLayerUpperBits -const val RenderLayerLowerMask = 0.inv() shr RenderLayerUpperBits +enum class RenderLayer { + BackgroundOverlay, + BackgroundTile, + BackgroundTileMod, + Platform, + Plant, + PlantDrop, + Object, + PreviewObject, + BackParticle, + Vehicle, + Effect, + Projectile, + Monster, + Npc, + Player, + ItemDrop, + Liquid, + MiddleParticle, + ForegroundTile, + ForegroundTileMod, + ForegroundEntity, + ForegroundOverlay, + FrontParticle, + Overlay; -enum class RenderLayer(val index: Long) { - BackgroundOverlay (1L shl RenderLayerLowerBits), - BackgroundTile (2L shl RenderLayerLowerBits), - Platform (3L shl RenderLayerLowerBits), - Plant (4L shl RenderLayerLowerBits), - PlantDrop (5L shl RenderLayerLowerBits), - Object (6L shl RenderLayerLowerBits), - PreviewObject (7L shl RenderLayerLowerBits), - BackParticle (8L shl RenderLayerLowerBits), - Vehicle (9L shl RenderLayerLowerBits), - Effect (10L shl RenderLayerLowerBits), - Projectile (11L shl RenderLayerLowerBits), - Monster (12L shl RenderLayerLowerBits), - Npc (13L shl RenderLayerLowerBits), - Player (14L shl RenderLayerLowerBits), - ItemDrop (15L shl RenderLayerLowerBits), - Liquid (16L shl RenderLayerLowerBits), - MiddleParticle (17L shl RenderLayerLowerBits), - ForegroundTile (18L shl RenderLayerLowerBits), - ForegroundEntity (19L shl RenderLayerLowerBits), - ForegroundOverlay (20L shl RenderLayerLowerBits), - FrontParticle (21L shl RenderLayerLowerBits), - Overlay (22L shl RenderLayerLowerBits); + val base = Point(this) + + fun point(offset: Long = 0L): Point { + return if (offset == 0L) + base + else + Point(this, offset) + } + + data class Point(val base: RenderLayer, val offset: Long = 0L) : Comparable { + override fun compareTo(other: Point): Int { + var cmp = base.compareTo(other.base) + if (cmp == 0) cmp = offset.compareTo(other.offset) + return cmp + } + } companion object { - private val logger = LogManager.getLogger() - - private inline fun perform(value: String, symbol: Char, operator: (Long, Long) -> Long): Long { - val split = value.split(symbol) - - if (split.size != 2) { - logger.error("Ambiguous render layer string: $value; assuming 0") - return 0 + fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L): Point { + if (isBackground && isModifier) { + return BackgroundTileMod.point(offset) + } else if (isBackground) { + return BackgroundTile.point(offset) + } else if (isModifier) { + return ForegroundTileMod.point(offset) } else { - val enum = entries.find { it.name == split[0] } - - if (enum == null) { - logger.error("Unknown render layer: ${split[0]} in $value; assuming 0") - return 0 - } - - val num = split[1].toLongOrNull() - - if (num == null) { - logger.error("Invalid render layer string: $value; assuming 0") - return 0 - } - - return operator(enum.index, num) + return ForegroundTile.point(offset) } } - fun parse(value: String): Long { + private val logger = LogManager.getLogger() + private val lowercase: ImmutableMap + + init { + val builder = ImmutableMap.Builder() + + for (value in entries) { + builder.put(value.name.lowercase(), value) + } + + lowercase = builder.build() + } + + private fun perform(value: String, symbol: Char): Point { + val before = value.substringBefore(symbol).lowercase() + val after = value.substring(before.length) + + val enum = lowercase[before] + + if (enum == null) { + logger.error("Unknown render layer: $before in $value; assuming BackgroundOverlay") + return BackgroundOverlay.base + } + + val num = after.toLongOrNull() + + if (num == null) { + logger.error("Invalid render layer string: $value; assuming BackgroundOverlay") + return BackgroundOverlay.base + } + + return enum.point(num) + } + + fun parse(value: String): Point { if ('+' in value) { - return perform(value, '+', Long::plus) + return perform(value, '+') } else if ('-' in value) { - return perform(value, '-', Long::minus) - } else if ('*' in value) { - return perform(value, '*', Long::times) - } else if ('/' in value) { - return perform(value, '/', Long::div) + return perform(value, '-') } else { - val enum = entries.find { it.name == value } + val enum = lowercase[value.lowercase()] if (enum == null) { - logger.error("Unknown render layer: $value; assuming 0") - return 0 + logger.error("Unknown render layer: $value; assuming BackgroundOverlay") + return BackgroundOverlay.base } - return enum.index + return enum.base } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index e19c405e..e20c1f1e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -27,7 +27,6 @@ class TileRenderers(val client: StarboundClient) { private val background = HashMap() private val matCache = HashMap() private val modCache = HashMap() - private val identity = Matrix3f.identity() fun getMaterialRenderer(defName: String): TileRenderer { return matCache.computeIfAbsent(defName) { @@ -49,15 +48,13 @@ class TileRenderers(val client: StarboundClient) { override fun setup() { super.setup() - client.activeTexture = 0 client.depthTest = false program.texture0 = 0 - texture.bind() + client.textures2D[0] = texture texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST - program.worldMatrix = client.stack.last() - program.modelMatrix = identity + program.modelMatrix = client.stack.last() program.colorMultiplier = color } @@ -97,8 +94,8 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { HALT } - val state get() = renderers.client - val texture = def.renderParameters.texture?.imagePath?.value?.let { state.loadTexture(it).also { it.textureMagFilter = GL_NEAREST }} + val client get() = renderers.client + val texture = def.renderParameters.texture?.imagePath?.value?.let { client.loadTexture(it).also { it.textureMagFilter = GL_NEAREST }} val equalityTester: EqualityRuleTester = when (def) { is TileDefinition -> TileEqualityTester(def) @@ -161,29 +158,32 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { self: ITileState, matchPiece: RenderMatch, getter: ITileAccess, - meshBuilder: MultiMeshBuilder, + meshBuilder: LayeredRenderer, pos: Vector2i, thisBuilder: VertexBuilder, - background: Boolean, + isBackground: Boolean, isModifier: Boolean, ): TestResult { if (matchPiece.test(getter, equalityTester, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.texture != null) { - val program = if (background) { - renderers.background(state.loadTexture(renderPiece.piece.texture!!)) + val program = if (isBackground) { + renderers.background(client.loadTexture(renderPiece.piece.texture!!)) } else { - renderers.foreground(state.loadTexture(renderPiece.piece.texture!!)) + renderers.foreground(client.loadTexture(renderPiece.piece.texture!!)) } - tesselateAt(self, renderPiece.piece, getter, meshBuilder.get(program, def.renderParameters.zLevel).mode(GeometryType.QUADS), pos, renderPiece.offset, isModifier) + tesselateAt( + self, renderPiece.piece, getter, + meshBuilder.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, def.renderParameters.zLevel), program).mode(GeometryType.QUADS), + pos, renderPiece.offset, isModifier) } else { tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier) } } for (subPiece in matchPiece.subMatches) { - val matched = tesselatePiece(self, subPiece, getter, meshBuilder, pos, thisBuilder, background, isModifier) + val matched = tesselatePiece(self, subPiece, getter, meshBuilder, pos, thisBuilder, isBackground, isModifier) if (matched == TestResult.HALT || matched == TestResult.CONTINUE && matchPiece.haltOnSubMatch) { return TestResult.HALT @@ -209,18 +209,20 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { * * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] */ - fun tesselate(self: ITileState, getter: ITileAccess, meshBuilder: MultiMeshBuilder, pos: Vector2i, background: Boolean = false, isModifier: Boolean = false) { + fun tesselate(self: ITileState, getter: ITileAccess, meshBuilder: LayeredRenderer, pos: Vector2i, isBackground: Boolean = false, isModifier: Boolean = false) { if (texture == null) return // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = def.renderTemplate.value ?: return - val vertexBuilder = meshBuilder.get(if (background) bakedBackgroundProgramState!! else bakedProgramState!!, def.renderParameters.zLevel).mode(GeometryType.QUADS) + val vertexBuilder = meshBuilder + .getBuilder(RenderLayer.tileLayer(isBackground, isModifier, def.renderParameters.zLevel), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!) + .mode(GeometryType.QUADS) for ((_, matcher) in template.matches) { for (matchPiece in matcher) { - val matched = tesselatePiece(self, matchPiece, getter, meshBuilder, pos, vertexBuilder, background, isModifier) + val matched = tesselatePiece(self, matchPiece, getter, meshBuilder, pos, vertexBuilder, isBackground, isModifier) if (matched == TestResult.HALT) { break diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 1074fa74..a1dde6bd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -10,7 +10,6 @@ import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.Mesh -import ru.dbotthepony.kstarbound.client.render.MultiMeshBuilder import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity @@ -72,7 +71,7 @@ class ClientWorld( inner class RenderRegion(val x: Int, val y: Int) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { - val bakedMeshes = ArrayList, Long>>() + val bakedMeshes = ArrayList, RenderLayer.Point>>() var isDirty = true fun bake() { @@ -81,7 +80,7 @@ class ClientWorld( bakedMeshes.clear() - val meshes = MultiMeshBuilder() + val meshes = LayeredRenderer() for (x in 0 until renderRegionWidth) { for (y in 0 until renderRegionHeight) { @@ -91,19 +90,23 @@ class ClientWorld( val material = tile.material if (!material.isMeta) { - client.tileRenderers.getMaterialRenderer(material.materialName).tesselate(tile, view, meshes, Vector2i(x, y), background = isBackground) + client.tileRenderers + .getMaterialRenderer(material.materialName) + .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground) } val modifier = tile.modifier if (modifier != null) { - client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, meshes, Vector2i(x, y), background = isBackground, isModifier = true) + client.tileRenderers + .getModifierRenderer(modifier.modName) + .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground, isModifier = true) } } } - for ((baked, builder, zLevel) in meshes.meshes()) { - bakedMeshes.add(ConfiguredMesh(baked, Mesh(builder)) to zLevel) + for ((baked, zLevel) in meshes.bakeIntoMeshes()) { + bakedMeshes.add(baked to zLevel) } } } @@ -155,7 +158,7 @@ class ClientWorld( } for ((baked, zLevel) in background.bakedMeshes) { - layers.add(zLevel + RenderLayer.BackgroundTile.index) { + layers.add(zLevel) { client.stack.push().last().translate(renderOrigin.x, renderOrigin.y) baked.render() client.stack.pop() @@ -163,7 +166,7 @@ class ClientWorld( } for ((baked, zLevel) in foreground.bakedMeshes) { - layers.add(zLevel + RenderLayer.ForegroundTile.index) { + layers.add(zLevel) { client.stack.push().last().translate(renderOrigin.x, renderOrigin.y) baked.render() client.stack.pop() @@ -268,23 +271,11 @@ class ClientWorld( for (obj in objects) { if (obj.pos.x in client.viewportCellX .. client.viewportCellX + client.viewportCellWidth && obj.pos.y in client.viewportCellY .. client.viewportCellY + client.viewportCellHeight) { - //layers.add(RenderLayer.Object.index) { - layers.add(obj.orientation?.renderLayer ?: continue) { - /*client.quadWireframe { - it.vertex(obj.pos.x.toFloat(), obj.pos.y.toFloat()) - it.vertex(obj.pos.x.toFloat() + 1f, obj.pos.y.toFloat()) - it.vertex(obj.pos.x.toFloat() + 1f, obj.pos.y.toFloat() + 1f) - it.vertex(obj.pos.x.toFloat(), obj.pos.y.toFloat() + 1f) - }*/ + val layer = layers.getLayer(obj.orientation?.renderLayer ?: continue) - obj.drawables.forEach { - val (x, y) = obj.imagePosition - - client.stack.push().last() - .translate(obj.pos.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, obj.pos.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) - it.render(client) - client.stack.pop() - } + obj.drawables.forEach { + val (x, y) = obj.imagePosition + it.render(client, layer, obj.pos.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, obj.pos.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) } obj.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt index 0da155b4..4de6c9b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt @@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType +import ru.dbotthepony.kstarbound.client.render.IGeometryLayer import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.consumeNull @@ -34,7 +35,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false ) : Drawable(position, color, fullbright) { - override fun render(client: StarboundClient, x: Float, y: Float) { + override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) { TODO("Not yet implemented") } @@ -49,7 +50,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false ) : Drawable(position, color, fullbright) { - override fun render(client: StarboundClient, x: Float, y: Float) { + override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) { TODO("Not yet implemented") } @@ -79,12 +80,12 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright) } - override fun render(client: StarboundClient, x: Float, y: Float) { + override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) { val sprite = path.sprite ?: return val texture = client.loadTexture(path.imagePath.value!!) val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap - val mat = transform.map({ it }, { + val mat = transform.map({ it.copy() }, { val mat = Matrix3f.identity() it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) }) @@ -105,27 +106,21 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig mat }) - //program.modelMatrix = mat - program.modelMatrix = Matrix3f.identity() + val builder = layer.getBuilder(program.config(texture)) - program.worldMatrix = client.stack.last() - program.use() - program.texture0 = 0 - client.activeTexture = 0 - texture.bind() + mat.preTranslate(x, y) + client.stack.last().mulIntoOther(mat) - program.builder.builder.begin(GeometryType.QUADS) - program.builder.builder.vertex(mat, x, y).uv(sprite.u0, sprite.v0) - program.builder.builder.vertex(mat, x + sprite.width, y).uv(sprite.u1, sprite.v0) - program.builder.builder.vertex(mat, x + sprite.width, y + sprite.height).uv(sprite.u1, sprite.v1) - program.builder.builder.vertex(mat, x, y + sprite.height).uv(sprite.u0, sprite.v1) - program.builder.upload() - program.builder.draw() + builder.mode(GeometryType.QUADS) + builder.vertex(mat, 0f, 0f).uv(sprite.u0, sprite.v0) + builder.vertex(mat, 0f + sprite.width, 0f).uv(sprite.u1, sprite.v0) + builder.vertex(mat, 0f + sprite.width, 0f + sprite.height).uv(sprite.u1, sprite.v1) + builder.vertex(mat, 0f, 0f + sprite.height).uv(sprite.u0, sprite.v1) } } class Empty(position: Vector2f = Vector2f.ZERO, color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false) : Drawable(position, color, fullbright) { - override fun render(client: StarboundClient, x: Float, y: Float) {} + override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {} override fun flop(): Drawable { return this @@ -136,7 +131,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig return this } - abstract fun render(client: StarboundClient = StarboundClient.current(), x: Float = 0f, y: Float = 0f) + abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f) /** * mirror along X axis diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index e3d441ae..25815f40 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -36,7 +36,7 @@ data class ObjectOrientation( val json: JsonObject, val flipImages: Boolean = false, val drawables: ImmutableList, - val renderLayer: Long, + val renderLayer: RenderLayer.Point, val imagePosition: Vector2f, val frames: Int, val animationCycle: Double, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt index 374941ca..1267d584 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt @@ -476,6 +476,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int) clusters.removeIf { val grid = it.grid + // wait until thread local caches are refreshed if (grid != null) { if (targetMem != null) { for (x in grid.minX - 1 .. grid.maxX) { diff --git a/src/main/resources/shaders/fragment/configurable.fsh b/src/main/resources/shaders/fragment/configurable.fsh index e13e9093..e04296e8 100644 --- a/src/main/resources/shaders/fragment/configurable.fsh +++ b/src/main/resources/shaders/fragment/configurable.fsh @@ -3,7 +3,7 @@ uniform vec4 colorMultiplier; out vec4 colorResult; #ifdef TEXTURE -uniform sampler2D texture0; +layout (binding = 0) uniform sampler2D texture0; in vec2 uvOut; #endif diff --git a/src/main/resources/shaders/vertex/configurable.vsh b/src/main/resources/shaders/vertex/configurable.vsh index 2acc7d93..90d96d90 100644 --- a/src/main/resources/shaders/vertex/configurable.vsh +++ b/src/main/resources/shaders/vertex/configurable.vsh @@ -17,11 +17,10 @@ out float hueShiftOut; #endif uniform mat3 viewMatrix; // projection (viewport) + camera -uniform mat3 worldMatrix; // matrix stack uniform mat3 modelMatrix; // local transformations void main() { - vec3 result = viewMatrix * worldMatrix * modelMatrix * pos; + vec3 result = viewMatrix * modelMatrix * pos; gl_Position = vec4(result.x, result.y, 0.0, result.z); #ifdef HUE_SHIFT diff --git a/src/main/resources/shaders/vertex/font.vsh b/src/main/resources/shaders/vertex/font.vsh index 34c9a0c8..4ae21909 100644 --- a/src/main/resources/shaders/vertex/font.vsh +++ b/src/main/resources/shaders/vertex/font.vsh @@ -5,12 +5,12 @@ layout (location = 0) in vec3 pos; layout (location = 1) in vec2 uvData; uniform mat3 viewMatrix; // projection (viewport) -uniform mat3 worldMatrix; // world position +uniform mat3 modelMatrix; // world position out vec2 uvOut; void main() { - vec3 result = viewMatrix * worldMatrix * pos; + vec3 result = viewMatrix * modelMatrix * pos; gl_Position = vec4(result.x, result.y, 0.0, result.z); uvOut = uvData;