Geometry batching

This commit is contained in:
DBotThePony 2023-09-23 22:40:31 +07:00
parent 1479f7483a
commit 178953bce7
Signed by: DBot
GPG Key ID: DCC23B5715498507
27 changed files with 461 additions and 514 deletions

View File

@ -83,7 +83,7 @@ dependencies {
implementation("com.github.jnr:jnr-ffi:2.2.13") implementation("com.github.jnr:jnr-ffi:2.2.13")
implementation("ru.dbotthepony:kbox2d:2.4.1.6") 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") implementation("com.github.ben-manes.caffeine:caffeine:3.1.5")
} }

View File

@ -8,7 +8,6 @@ import org.lwjgl.glfw.Callbacks
import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW
import org.lwjgl.glfw.GLFWErrorCallback import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL45.* import org.lwjgl.opengl.GL45.*
import org.lwjgl.opengl.GLCapabilities import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.system.MemoryStack 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.GLObjectTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker 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.GLPrograms
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram 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.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
class StarboundClient : Closeable { class StarboundClient : Closeable {
@ -260,15 +262,7 @@ class StarboundClient : Closeable {
.weakValues() .weakValues()
.build() .build()
private val missingTexture: GLTexture2D by lazy { private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
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<WeakReference<GLShaderProgram.Regular>>()
private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>() private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()
val lightMapLocation = maxTextureBlocks - 1 val lightMapLocation = maxTextureBlocks - 1
@ -278,8 +272,8 @@ class StarboundClient : Closeable {
uberShaderPrograms.add(WeakReference(program)) uberShaderPrograms.add(WeakReference(program))
} }
if (program is GLShaderProgram.Regular) { if (program is FontProgram) {
regularShaderPrograms.add(WeakReference(program)) fontShaderPrograms.add(WeakReference(program))
} }
} }
@ -339,23 +333,7 @@ class StarboundClient : Closeable {
var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER) var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
var program by GLObjectTracker<GLShaderProgram>(::glUseProgram) var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
private val textures = Array(maxTextureBlocks) { GLObjectTracker<GLTexture2D>(GL11::glBindTexture, GL_TEXTURE_2D) } val textures2D = GLTexturesTracker<GLTexture2D>(maxTextureBlocks)
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) }
var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) { var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) {
val (r, g, b, a) = it val (r, g, b, a) = it
@ -376,7 +354,8 @@ class StarboundClient : Closeable {
checkForGLError() checkForGLError()
} }
val whiteTexture = GLTexture2D("white") val whiteTexture = GLTexture2D(1, 1, GL_RGB8)
val missingTexture = GLTexture2D(8, 8, GL_RGB8)
init { init {
val buffer = ByteBuffer.allocateDirect(3) val buffer = ByteBuffer.allocateDirect(3)
@ -384,7 +363,42 @@ class StarboundClient : Closeable {
buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte())
buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte())
buffer.position(0) 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) { fun setViewport(x: Int, y: Int, width: Int, height: Int) {
@ -454,7 +468,6 @@ class StarboundClient : Closeable {
} }
fun isSameThread() = thread === Thread.currentThread() fun isSameThread() = thread === Thread.currentThread()
fun newTexture(name: String = "<unknown>") = GLTexture2D(name)
fun loadTexture(path: String): GLTexture2D { fun loadTexture(path: String): GLTexture2D {
ensureSameThread() ensureSameThread()
@ -464,13 +477,27 @@ class StarboundClient : Closeable {
val data = Image.get(it) val data = Image.get(it)
if (data == null) { 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 missingTexture
} else { } else {
newTexture(it).upload(data).also { val tex = GLTexture2D(data.width, data.height, when (data.amountOfChannels) {
it.textureMinFilter = GL_NEAREST 1 -> GL_R8
it.textureMagFilter = GL_NEAREST 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.use()
programs.position.colorMultiplier = color programs.position.colorMultiplier = color
programs.position.worldMatrix = stack.last() programs.position.modelMatrix = stack.last()
builder.draw(GL_LINES) builder.draw(GL_LINES)
} }
@ -632,7 +659,9 @@ class StarboundClient : Closeable {
var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
private set private set
val viewportLightingTexture = newTexture("Viewport Lighting") var viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8)
private set
private var viewportLightingMem: ByteBuffer? = null private var viewportLightingMem: ByteBuffer? = null
fun updateViewportParams() { fun updateViewportParams() {
@ -643,8 +672,14 @@ class StarboundClient : Closeable {
viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16 viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16
viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16 viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16
viewportCellWidth = roundTowardsPositiveInfinity(viewportRectangle.width) + 32 val viewportCellWidth0 = roundTowardsPositiveInfinity(viewportRectangle.width) + 32
viewportCellHeight = roundTowardsPositiveInfinity(viewportRectangle.height) + 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) viewportTopRight = screenToWorld(viewportWidth, viewportHeight)
viewportBottomLeft = screenToWorld(0, 0) viewportBottomLeft = screenToWorld(0, 0)
@ -655,6 +690,7 @@ class StarboundClient : Closeable {
if (viewportCellWidth > 0 && viewportCellHeight > 0) { if (viewportCellWidth > 0 && viewportCellHeight > 0) {
viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3) viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3)
viewportLightingTexture = GLTexture2D(viewportCellWidth.coerceAtMost(4096), viewportCellHeight.coerceAtMost(4096), GL_RGB8)
} else { } else {
viewportLightingMem = null viewportLightingMem = null
} }
@ -681,6 +717,8 @@ class StarboundClient : Closeable {
onPostDrawWorldOnce.add(lambda) onPostDrawWorldOnce.add(lambda)
} }
private val layers = LayeredRenderer()
fun renderFrame(): Boolean { fun renderFrame(): Boolean {
ensureSameThread() ensureSameThread()
@ -711,14 +749,14 @@ class StarboundClient : Closeable {
cleanup() cleanup()
GLFW.glfwPollEvents() GLFW.glfwPollEvents()
if (world != null) { if (world != null && Starbound.initialized)
if (Starbound.initialized) world.think()
world.think()
}
return true return true
} }
layers.clear()
uberShaderPrograms.forEachValid { uberShaderPrograms.forEachValid {
if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) { if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) {
it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat()) it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat())
@ -727,7 +765,6 @@ class StarboundClient : Closeable {
if (world != null) { if (world != null) {
updateViewportParams() updateViewportParams()
val layers = LayeredRenderer()
if (Starbound.initialized) if (Starbound.initialized)
world.think() world.think()
@ -741,7 +778,8 @@ class StarboundClient : Closeable {
.scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера
.translate(-camera.pos.x.toFloat(), -camera.pos.y.toFloat()) // перемещаем вид к камере .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) { for (lambda in onPreDrawWorld) {
lambda.invoke(layers) lambda.invoke(layers)
@ -763,16 +801,13 @@ class StarboundClient : Closeable {
viewportLightingMem.position(0) viewportLightingMem.position(0)
BufferUtils.zeroBuffer(viewportLightingMem) BufferUtils.zeroBuffer(viewportLightingMem)
viewportLightingMem.position(0) viewportLightingMem.position(0)
viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096)) viewportLighting.calculate(viewportLightingMem, viewportLightingTexture.width, viewportLightingTexture.height)
viewportLightingMem.position(0) viewportLightingMem.position(0)
val old = textureUnpackAlignment 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( viewportLightingTexture.upload(
GL_RGB,
viewportLighting.width.coerceAtMost(4096),
viewportLighting.height.coerceAtMost(4096),
GL_RGB, GL_RGB,
GL_UNSIGNED_BYTE, GL_UNSIGNED_BYTE,
viewportLightingMem viewportLightingMem
@ -780,14 +815,11 @@ class StarboundClient : Closeable {
textureUnpackAlignment = old textureUnpackAlignment = old
} else { } else {
viewportLightingTexture.upload(GL_RGBA, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, WHITE) viewportLightingTexture = whiteTexture
} }
viewportLightingTexture.textureMinFilter = GL_LINEAR viewportLightingTexture.textureMinFilter = GL_LINEAR
textures2D[lightMapLocation] = viewportLightingTexture
activeTexture = lightMapLocation
texture2D = viewportLightingTexture
activeTexture = 0
val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f( val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f(
((viewportBottomLeft.x - viewportCellX) / viewportLighting.width).toFloat(), ((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() 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("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("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("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.glfwSwapBuffers(window)
GLFW.glfwPollEvents() GLFW.glfwPollEvents()

View File

@ -9,10 +9,12 @@ sealed class BufferObject : GLObject() {
final override val client = StarboundClient.current() final override val client = StarboundClient.current()
abstract val glType: Int abstract val glType: Int
@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
override fun bind() { override fun bind() {
// do nothing // do nothing
} }
@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
override fun unbind() { override fun unbind() {
// do nothing // do nothing
} }

View File

@ -33,8 +33,7 @@ class GLFrameBuffer : GLObject() {
throw IllegalStateException("Already has texture attached") throw IllegalStateException("Already has texture attached")
} }
val texture = GLTexture2D("framebuffer_$pointer") val texture = GLTexture2D(width, height, format)
texture.allocate(format, width, height)
val old = client.framebuffer val old = client.framebuffer
bind() bind()

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.client.gl package ru.dbotthepony.kstarbound.client.gl
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL45.* import org.lwjgl.opengl.GL45.*
import org.lwjgl.stb.STBImage import org.lwjgl.stb.STBImage
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
@ -30,24 +31,23 @@ private class GLTexturePropertyTracker(private val flag: Int, private var value:
} }
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
class GLTexture2D(val name: String = "<unknown>") : 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 client = StarboundClient.current()
override val pointer = glCreateTextures(GL_TEXTURE_2D) override val pointer = glCreateTextures(GL_TEXTURE_2D)
init { init {
checkForGLError() checkForGLError("Creating texture")
client.registerCleanable(this, ::glDeleteTextures, pointer) 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() { val aspectRatioWH: Float get() {
if (height == 0) { if (height == 0) {
return 1f return 1f
@ -71,13 +71,14 @@ class GLTexture2D(val name: String = "<unknown>") : GLObject() {
var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT) var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT)
var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT) var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)
@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
override fun bind() { override fun bind() {
client.texture2D = this // do nothing
} }
@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
override fun unbind() { override fun unbind() {
if (client.texture2D === this) // do nothing
client.texture2D = null
} }
fun generateMips(): GLTexture2D { fun generateMips(): GLTexture2D {
@ -88,186 +89,31 @@ class GLTexture2D(val name: String = "<unknown>") : GLObject() {
} }
fun pixelToUV(x: Float, y: Float): UVCoord { fun pixelToUV(x: Float, y: Float): UVCoord {
check(uploaded) { "Texture is not uploaded to be used" }
return UVCoord(x / width, y / height) return UVCoord(x / width, y / height)
} }
fun pixelToUV(x: Int, y: Int): UVCoord { fun pixelToUV(x: Int, y: Int): UVCoord {
check(uploaded) { "Texture is not uploaded to be used" }
return UVCoord(x.toFloat() / width, y.toFloat() / height) return UVCoord(x.toFloat() / width, y.toFloat() / height)
} }
fun pixelToUV(pos: Vector2i) = pixelToUV(pos.x, pos.y) fun pixelToUV(pos: Vector2i) = pixelToUV(pos.x, pos.y)
fun allocate(mipmap: Int, loadedFormat: Int, width: Int, height: Int): GLTexture2D { fun upload(level: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
bind() return upload(level, 0, 0, width, height, bufferFormat, dataFormat, data)
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 { fun upload(bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
return allocate(0, loadedFormat, width, height) 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 { fun upload(level: Int, x: Int, y: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
bind() 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}"
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")
} }
if (!path.isFile) { require(level in 0 until levels) { "Level out of bounds: $level" }
throw FileNotFoundException("${path.absolutePath} is not a file") glTextureSubImage2D(pointer, level, x, y, width, height, bufferFormat, dataFormat, data)
} checkForGLError("Uploading texture data")
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)
return this return this
} }

View File

@ -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<T : GLObject>(maxValue: Int) {
private val client = StarboundClient.current()
private val values = arrayOfNulls<GLObject>(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
}
}

View File

@ -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<StarboundClient, GLTexture2D?> {
private val values = arrayOfNulls<GLTexture2D>(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()
}
}

View File

@ -28,13 +28,6 @@ open class GLShaderProgram(
shaders: Iterable<GLShader>, shaders: Iterable<GLShader>,
val attributes: VertexAttributes val attributes: VertexAttributes
) : GLObject() { ) : 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 client = StarboundClient.current()
final override val pointer = glCreateProgram() final override val pointer = glCreateProgram()

View File

@ -22,38 +22,10 @@ private fun shaders1(name: String): List<GLShader> {
return listOf(client.internalVertex("shaders/vertex/$name.vsh"), client.internalFragment("shaders/fragment/$name.fsh")) return listOf(client.internalVertex("shaders/vertex/$name.vsh"), client.internalFragment("shaders/fragment/$name.fsh"))
} }
private fun fontShaders(): List<GLShader> { class FontProgram : GLShaderProgram(shaders1("font"), FONT_VERTEX_FORMAT) {
val client = StarboundClient.current() var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix")
var read = StarboundClient.readInternal("shaders/fragment/font.fsh") var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix")
var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier")
val textures = ArrayList<String>()
val cases = ArrayList<String>()
val cases2 = ArrayList<String>()
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")
var texture by IUniform("texture0") var texture by IUniform("texture0")

View File

@ -2,33 +2,37 @@ package ru.dbotthepony.kstarbound.client.gl.shader
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet 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_FRAGMENT_SHADER
import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER
import ru.dbotthepony.kstarbound.client.StarboundClient 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.StreamVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributeType import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributeType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes 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.api.IStruct4f
import ru.dbotthepony.kvector.arrays.Matrix3f import ru.dbotthepony.kvector.arrays.Matrix3f
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import java.util.function.Function
class UberShader private constructor( class UberShader private constructor(
fragment: GLShader, fragment: GLShader,
vertex: GLShader, vertex: GLShader,
attributes: VertexAttributes, attributes: VertexAttributes,
val flags: Set<Flag>, val flags: Set<Flag>,
) : GLShaderProgram(listOf(fragment, vertex), attributes), GLShaderProgram.Regular { ) : GLShaderProgram(listOf(fragment, vertex), attributes) {
enum class Flag { enum class Flag {
LIGHT_MAP, LIGHT_MAP,
NEEDS_SCREEN_SIZE; NEEDS_SCREEN_SIZE;
} }
override var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix") var viewMatrix: Matrix3f by F3x3Uniform("viewMatrix")
override var worldMatrix: Matrix3f by F3x3Uniform("worldMatrix") var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix")
override var modelMatrix: Matrix3f by F3x3Uniform("modelMatrix") var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier")
override var colorMultiplier: IStruct4f by F4Uniform("colorMultiplier")
var texture0 by IUniform("texture0", VertexAttributeType.UV !in attributes) var texture0 by IUniform("texture0", VertexAttributeType.UV !in attributes)
@ -40,11 +44,24 @@ class UberShader private constructor(
init { init {
viewMatrix = Matrix3f.identity() viewMatrix = Matrix3f.identity()
worldMatrix = Matrix3f.identity()
modelMatrix = Matrix3f.identity() modelMatrix = Matrix3f.identity()
colorMultiplier = RGBAColor.WHITE colorMultiplier = RGBAColor.WHITE
} }
private val textureConfigs = Reference2ObjectOpenHashMap<GLTexture2D, RenderConfig<UberShader>>()
fun config(texture: GLTexture2D): RenderConfig<UberShader> {
return textureConfigs.computeIfAbsent(texture, Reference2ObjectFunction {
object : RenderConfig<UberShader>(this) {
override fun setup() {
super.setup()
client.textures2D[0] = texture
}
}
})
}
class Builder { class Builder {
private val directives = Object2ObjectArrayMap<String, String>() private val directives = Object2ObjectArrayMap<String, String>()
private val attributes = ObjectArraySet<VertexAttributeType>() private val attributes = ObjectArraySet<VertexAttributeType>()

View File

@ -12,7 +12,7 @@ import ru.dbotthepony.kstarbound.client.gl.checkForGLError
class StreamVertexBuilder( class StreamVertexBuilder(
attributes: VertexAttributes, attributes: VertexAttributes,
type: GeometryType? = null, type: GeometryType? = null,
initialCapacity: Int = 64, initialCapacity: Int = 32,
) { ) {
val state = StarboundClient.current() val state = StarboundClient.current()
val builder = VertexBuilder(attributes, type, initialCapacity) val builder = VertexBuilder(attributes, type, initialCapacity)

View File

@ -51,7 +51,7 @@ private fun indexSize(type: Int): Int {
class VertexBuilder( class VertexBuilder(
val attributes: VertexAttributes, val attributes: VertexAttributes,
val defaultMode: GeometryType? = null, val defaultMode: GeometryType? = null,
initialCapacity: Int = 64 initialCapacity: Int = 32
) { ) {
constructor(attributes: VertexAttributes, initialCapacity: Int) : this(attributes, null, initialCapacity) 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 var elementCount = 0 // GeometryType specific (quads, triangles, lines), less than or equal (points) to vertexCount
private set private set
fun isEmpty(): Boolean {
return !inVertex && vertexCount == 0
}
fun isNotEmpty(): Boolean {
return inVertex || vertexCount != 0
}
private fun ensureCapacity() { private fun ensureCapacity() {
val mode = mode ?: return 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" } 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 return this
} }

View File

@ -43,8 +43,7 @@ class Box2DRenderer : IDebugDraw {
state.programs.position.use() state.programs.position.use()
state.programs.position.colorMultiplier = color state.programs.position.colorMultiplier = color
state.programs.position.worldMatrix = state.stack.last() state.programs.position.modelMatrix = state.stack.last()
state.programs.position.modelMatrix = identity
builder.draw(GL_LINES) builder.draw(GL_LINES)
} }
@ -68,8 +67,7 @@ class Box2DRenderer : IDebugDraw {
state.programs.position.use() state.programs.position.use()
state.programs.position.colorMultiplier = color state.programs.position.colorMultiplier = color
state.programs.position.worldMatrix = state.stack.last() state.programs.position.modelMatrix = state.stack.last()
state.programs.position.modelMatrix = identity
builder.draw(GL_TRIANGLES) builder.draw(GL_TRIANGLES)
} }

View File

@ -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))), 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 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 val atlasHeight: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_HEIGHT).coerceAtMost(4096)
private var nextAtlasX = 0 private var nextAtlasX = 0
private var nextAtlasY = 0 private var nextAtlasY = 0
private val atlas = GLTexture2D(atlasWidth, atlasHeight, GL_R8)
init { init {
glTextureStorage2D(atlas.pointer, 1, GL_R8, atlasWidth, atlasHeight)
checkForGLError()
atlas.textureMinFilter = GL_LINEAR atlas.textureMinFilter = GL_LINEAR
atlas.textureMagFilter = GL_LINEAR atlas.textureMagFilter = GL_LINEAR
atlas.textureWrapS = GL_CLAMP_TO_EDGE atlas.textureWrapS = GL_CLAMP_TO_EDGE
@ -217,12 +214,11 @@ class Font(
if (builder.elementCount != 0) { if (builder.elementCount != 0) {
client.programs.font.use() client.programs.font.use()
client.programs.font.colorMultiplier = color 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) builder.upload(vbo, ebo, GL_STREAM_DRAW)
client.activeTexture = 0 client.textures2D[0] = atlas
atlas.bind()
client.vao = vao client.vao = vao
glDrawElements(GL_TRIANGLES, builder.indexCount, builder.indexType, 0L) glDrawElements(GL_TRIANGLES, builder.indexCount, builder.indexType, 0L)
@ -341,8 +337,7 @@ class Font(
try { try {
uv = Vector4f(nextAtlasX / atlasWidth.toFloat(), nextAtlasY / atlasHeight.toFloat(), (nextAtlasX + bitmap.width) / atlasWidth.toFloat(), (nextAtlasY + bitmap.rows) / atlasHeight.toFloat()) 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) atlas.upload(0, nextAtlasX, nextAtlasY, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff)
checkForGLError("Uploading glyph texture data")
nextAtlasX += bbox.width nextAtlasX += bbox.width

View File

@ -1,28 +1,116 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap
import ru.dbotthepony.kvector.arrays.Matrix3fStack import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
import ru.dbotthepony.kvector.arrays.Matrix4fStack import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import java.util.function.Function import java.util.function.Function
import java.util.stream.Stream
/** interface IGeometryLayer {
* Позволяет вызывать отрисовщики в определённой (послойной) последовательности fun add(renderer: () -> Unit)
*/ fun getBuilder(config: RenderConfig<*>): VertexBuilder
class LayeredRenderer { fun render()
private val layers = Long2ObjectAVLTreeMap<ArrayList<() -> Unit>>() fun clear()
fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>>
}
fun add(layer: Long, renderer: () -> Unit) { object OneShotGeometryLayer : IGeometryLayer {
layers.computeIfAbsent(layer, Function { ArrayList() }).add(renderer) override fun add(renderer: () -> Unit) {
TODO("Not yet implemented")
} }
fun render() { override fun getBuilder(config: RenderConfig<*>): VertexBuilder {
for (list in layers.values) { TODO("Not yet implemented")
for (renderer in list) { }
renderer.invoke()
override fun render() {
TODO("Not yet implemented")
}
override fun clear() {
TODO("Not yet implemented")
}
override fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>> {
TODO("Not yet implemented")
}
}
class LayeredRenderer {
class Layer(val layer: RenderLayer.Point) : IGeometryLayer {
private val meshes = ArrayList<ConfiguredMesh<*>>()
private val callbacks = ArrayList<() -> Unit>()
private val builders = Reference2ObjectLinkedOpenHashMap<RenderConfig<*>, 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<Pair<ConfiguredMesh<*>, 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<RenderLayer.Point, Layer>()
fun getBuilder(layer: RenderLayer.Point, config: RenderConfig<*>): VertexBuilder {
return getLayer(layer).getBuilder(config)
}
fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, 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() }
} }
} }

View File

@ -7,6 +7,9 @@ import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kvector.arrays.Matrix4f import ru.dbotthepony.kvector.arrays.Matrix4f
/**
* mesh is container for paired VAO, VBO and EBO, and metadata for them
*/
class Mesh() { class Mesh() {
constructor(builder: VertexBuilder) : this() { constructor(builder: VertexBuilder) : this() {
load(builder, GL45.GL_STATIC_DRAW) load(builder, GL45.GL_STATIC_DRAW)
@ -44,6 +47,9 @@ class Mesh() {
} }
} }
/**
* Mesh + RenderConfig
*/
data class ConfiguredMesh<T : GLShaderProgram>(val config: RenderConfig<T>, val mesh: Mesh = Mesh()) { data class ConfiguredMesh<T : GLShaderProgram>(val config: RenderConfig<T>, val mesh: Mesh = Mesh()) {
fun render() { fun render() {
config.setup() config.setup()

View File

@ -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<RenderConfig<*>, Long2ObjectOpenHashMap<Entry>>()
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<Entry> {
return meshes.values.stream().flatMap { it.values.stream() }
}
}

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kvector.arrays.Matrix4f
abstract class RenderConfig<out T : GLShaderProgram>(val program: T) { abstract class RenderConfig<out T : GLShaderProgram>(val program: T) {
val client get() = program.client val client get() = program.client
open val initialBuilderCapacity: Int get() = 64 open val initialBuilderCapacity: Int get() = 32
open fun setup() { open fun setup() {
program.use() program.use()

View File

@ -1,81 +1,112 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import com.google.common.collect.ImmutableMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
const val RenderLayerUpperBits = 5 enum class RenderLayer {
const val RenderLayerLowerBits = 32 - RenderLayerUpperBits BackgroundOverlay,
const val RenderLayerLowerMask = 0.inv() shr RenderLayerUpperBits 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) { val base = Point(this)
BackgroundOverlay (1L shl RenderLayerLowerBits),
BackgroundTile (2L shl RenderLayerLowerBits), fun point(offset: Long = 0L): Point {
Platform (3L shl RenderLayerLowerBits), return if (offset == 0L)
Plant (4L shl RenderLayerLowerBits), base
PlantDrop (5L shl RenderLayerLowerBits), else
Object (6L shl RenderLayerLowerBits), Point(this, offset)
PreviewObject (7L shl RenderLayerLowerBits), }
BackParticle (8L shl RenderLayerLowerBits),
Vehicle (9L shl RenderLayerLowerBits), data class Point(val base: RenderLayer, val offset: Long = 0L) : Comparable<Point> {
Effect (10L shl RenderLayerLowerBits), override fun compareTo(other: Point): Int {
Projectile (11L shl RenderLayerLowerBits), var cmp = base.compareTo(other.base)
Monster (12L shl RenderLayerLowerBits), if (cmp == 0) cmp = offset.compareTo(other.offset)
Npc (13L shl RenderLayerLowerBits), return cmp
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);
companion object { companion object {
private val logger = LogManager.getLogger() fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L): Point {
if (isBackground && isModifier) {
private inline fun perform(value: String, symbol: Char, operator: (Long, Long) -> Long): Long { return BackgroundTileMod.point(offset)
val split = value.split(symbol) } else if (isBackground) {
return BackgroundTile.point(offset)
if (split.size != 2) { } else if (isModifier) {
logger.error("Ambiguous render layer string: $value; assuming 0") return ForegroundTileMod.point(offset)
return 0
} else { } else {
val enum = entries.find { it.name == split[0] } return ForegroundTile.point(offset)
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)
} }
} }
fun parse(value: String): Long { private val logger = LogManager.getLogger()
private val lowercase: ImmutableMap<String, RenderLayer>
init {
val builder = ImmutableMap.Builder<String, RenderLayer>()
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) { if ('+' in value) {
return perform(value, '+', Long::plus) return perform(value, '+')
} else if ('-' in value) { } else if ('-' in value) {
return perform(value, '-', Long::minus) return perform(value, '-')
} else if ('*' in value) {
return perform(value, '*', Long::times)
} else if ('/' in value) {
return perform(value, '/', Long::div)
} else { } else {
val enum = entries.find { it.name == value } val enum = lowercase[value.lowercase()]
if (enum == null) { if (enum == null) {
logger.error("Unknown render layer: $value; assuming 0") logger.error("Unknown render layer: $value; assuming BackgroundOverlay")
return 0 return BackgroundOverlay.base
} }
return enum.index return enum.base
} }
} }
} }

View File

@ -27,7 +27,6 @@ class TileRenderers(val client: StarboundClient) {
private val background = HashMap<GLTexture2D, Config>() private val background = HashMap<GLTexture2D, Config>()
private val matCache = HashMap<String, TileRenderer>() private val matCache = HashMap<String, TileRenderer>()
private val modCache = HashMap<String, TileRenderer>() private val modCache = HashMap<String, TileRenderer>()
private val identity = Matrix3f.identity()
fun getMaterialRenderer(defName: String): TileRenderer { fun getMaterialRenderer(defName: String): TileRenderer {
return matCache.computeIfAbsent(defName) { return matCache.computeIfAbsent(defName) {
@ -49,15 +48,13 @@ class TileRenderers(val client: StarboundClient) {
override fun setup() { override fun setup() {
super.setup() super.setup()
client.activeTexture = 0
client.depthTest = false client.depthTest = false
program.texture0 = 0 program.texture0 = 0
texture.bind() client.textures2D[0] = texture
texture.textureMagFilter = GL_NEAREST texture.textureMagFilter = GL_NEAREST
texture.textureMinFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST
program.worldMatrix = client.stack.last() program.modelMatrix = client.stack.last()
program.modelMatrix = identity
program.colorMultiplier = color program.colorMultiplier = color
} }
@ -97,8 +94,8 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
HALT HALT
} }
val state get() = renderers.client val client get() = renderers.client
val texture = def.renderParameters.texture?.imagePath?.value?.let { state.loadTexture(it).also { it.textureMagFilter = GL_NEAREST }} val texture = def.renderParameters.texture?.imagePath?.value?.let { client.loadTexture(it).also { it.textureMagFilter = GL_NEAREST }}
val equalityTester: EqualityRuleTester = when (def) { val equalityTester: EqualityRuleTester = when (def) {
is TileDefinition -> TileEqualityTester(def) is TileDefinition -> TileEqualityTester(def)
@ -161,29 +158,32 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
self: ITileState, self: ITileState,
matchPiece: RenderMatch, matchPiece: RenderMatch,
getter: ITileAccess, getter: ITileAccess,
meshBuilder: MultiMeshBuilder, meshBuilder: LayeredRenderer,
pos: Vector2i, pos: Vector2i,
thisBuilder: VertexBuilder, thisBuilder: VertexBuilder,
background: Boolean, isBackground: Boolean,
isModifier: Boolean, isModifier: Boolean,
): TestResult { ): TestResult {
if (matchPiece.test(getter, equalityTester, pos)) { if (matchPiece.test(getter, equalityTester, pos)) {
for (renderPiece in matchPiece.pieces) { for (renderPiece in matchPiece.pieces) {
if (renderPiece.piece.texture != null) { if (renderPiece.piece.texture != null) {
val program = if (background) { val program = if (isBackground) {
renderers.background(state.loadTexture(renderPiece.piece.texture!!)) renderers.background(client.loadTexture(renderPiece.piece.texture!!))
} else { } 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 { } else {
tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier) tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier)
} }
} }
for (subPiece in matchPiece.subMatches) { 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) { if (matched == TestResult.HALT || matched == TestResult.CONTINUE && matchPiece.haltOnSubMatch) {
return TestResult.HALT return TestResult.HALT
@ -209,18 +209,20 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
* *
* Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] * Тесселирует тайлы в нужный 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 if (texture == null) return
// если у нас нет renderTemplate // если у нас нет renderTemplate
// то мы просто не можем его отрисовать // то мы просто не можем его отрисовать
val template = def.renderTemplate.value ?: return 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 ((_, matcher) in template.matches) {
for (matchPiece in matcher) { 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) { if (matched == TestResult.HALT) {
break break

View File

@ -10,7 +10,6 @@ import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.Mesh 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.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
@ -72,7 +71,7 @@ class ClientWorld(
inner class RenderRegion(val x: Int, val y: Int) { inner class RenderRegion(val x: Int, val y: Int) {
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, Long>>() val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
var isDirty = true var isDirty = true
fun bake() { fun bake() {
@ -81,7 +80,7 @@ class ClientWorld(
bakedMeshes.clear() bakedMeshes.clear()
val meshes = MultiMeshBuilder() val meshes = LayeredRenderer()
for (x in 0 until renderRegionWidth) { for (x in 0 until renderRegionWidth) {
for (y in 0 until renderRegionHeight) { for (y in 0 until renderRegionHeight) {
@ -91,19 +90,23 @@ class ClientWorld(
val material = tile.material val material = tile.material
if (!material.isMeta) { 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 val modifier = tile.modifier
if (modifier != null) { 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()) { for ((baked, zLevel) in meshes.bakeIntoMeshes()) {
bakedMeshes.add(ConfiguredMesh(baked, Mesh(builder)) to zLevel) bakedMeshes.add(baked to zLevel)
} }
} }
} }
@ -155,7 +158,7 @@ class ClientWorld(
} }
for ((baked, zLevel) in background.bakedMeshes) { for ((baked, zLevel) in background.bakedMeshes) {
layers.add(zLevel + RenderLayer.BackgroundTile.index) { layers.add(zLevel) {
client.stack.push().last().translate(renderOrigin.x, renderOrigin.y) client.stack.push().last().translate(renderOrigin.x, renderOrigin.y)
baked.render() baked.render()
client.stack.pop() client.stack.pop()
@ -163,7 +166,7 @@ class ClientWorld(
} }
for ((baked, zLevel) in foreground.bakedMeshes) { for ((baked, zLevel) in foreground.bakedMeshes) {
layers.add(zLevel + RenderLayer.ForegroundTile.index) { layers.add(zLevel) {
client.stack.push().last().translate(renderOrigin.x, renderOrigin.y) client.stack.push().last().translate(renderOrigin.x, renderOrigin.y)
baked.render() baked.render()
client.stack.pop() client.stack.pop()
@ -268,23 +271,11 @@ class ClientWorld(
for (obj in objects) { 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) { 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) { val layer = layers.getLayer(obj.orientation?.renderLayer ?: continue)
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)
}*/
obj.drawables.forEach { obj.drawables.forEach {
val (x, y) = obj.imagePosition 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)
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.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) obj.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY)

View File

@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType 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.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.consumeNull 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, color: RGBAColor = RGBAColor.WHITE,
fullbright: Boolean = false fullbright: Boolean = false
) : Drawable(position, color, fullbright) { ) : 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") TODO("Not yet implemented")
} }
@ -49,7 +50,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
color: RGBAColor = RGBAColor.WHITE, color: RGBAColor = RGBAColor.WHITE,
fullbright: Boolean = false fullbright: Boolean = false
) : Drawable(position, color, fullbright) { ) : 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") 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) 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 sprite = path.sprite ?: return
val texture = client.loadTexture(path.imagePath.value!!) val texture = client.loadTexture(path.imagePath.value!!)
val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap 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() 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) }) 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 mat
}) })
//program.modelMatrix = mat val builder = layer.getBuilder(program.config(texture))
program.modelMatrix = Matrix3f.identity()
program.worldMatrix = client.stack.last() mat.preTranslate(x, y)
program.use() client.stack.last().mulIntoOther(mat)
program.texture0 = 0
client.activeTexture = 0
texture.bind()
program.builder.builder.begin(GeometryType.QUADS) builder.mode(GeometryType.QUADS)
program.builder.builder.vertex(mat, x, y).uv(sprite.u0, sprite.v0) builder.vertex(mat, 0f, 0f).uv(sprite.u0, sprite.v0)
program.builder.builder.vertex(mat, x + sprite.width, y).uv(sprite.u1, sprite.v0) builder.vertex(mat, 0f + sprite.width, 0f).uv(sprite.u1, sprite.v0)
program.builder.builder.vertex(mat, x + sprite.width, y + sprite.height).uv(sprite.u1, sprite.v1) builder.vertex(mat, 0f + sprite.width, 0f + sprite.height).uv(sprite.u1, sprite.v1)
program.builder.builder.vertex(mat, x, y + sprite.height).uv(sprite.u0, sprite.v1) builder.vertex(mat, 0f, 0f + sprite.height).uv(sprite.u0, sprite.v1)
program.builder.upload()
program.builder.draw()
} }
} }
class Empty(position: Vector2f = Vector2f.ZERO, color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false) : Drawable(position, color, fullbright) { 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 { override fun flop(): Drawable {
return this return this
@ -136,7 +131,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
return this 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 * mirror along X axis

View File

@ -36,7 +36,7 @@ data class ObjectOrientation(
val json: JsonObject, val json: JsonObject,
val flipImages: Boolean = false, val flipImages: Boolean = false,
val drawables: ImmutableList<Drawable>, val drawables: ImmutableList<Drawable>,
val renderLayer: Long, val renderLayer: RenderLayer.Point,
val imagePosition: Vector2f, val imagePosition: Vector2f,
val frames: Int, val frames: Int,
val animationCycle: Double, val animationCycle: Double,

View File

@ -476,6 +476,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int)
clusters.removeIf { clusters.removeIf {
val grid = it.grid val grid = it.grid
// wait until thread local caches are refreshed
if (grid != null) { if (grid != null) {
if (targetMem != null) { if (targetMem != null) {
for (x in grid.minX - 1 .. grid.maxX) { for (x in grid.minX - 1 .. grid.maxX) {

View File

@ -3,7 +3,7 @@ uniform vec4 colorMultiplier;
out vec4 colorResult; out vec4 colorResult;
#ifdef TEXTURE #ifdef TEXTURE
uniform sampler2D texture0; layout (binding = 0) uniform sampler2D texture0;
in vec2 uvOut; in vec2 uvOut;
#endif #endif

View File

@ -17,11 +17,10 @@ out float hueShiftOut;
#endif #endif
uniform mat3 viewMatrix; // projection (viewport) + camera uniform mat3 viewMatrix; // projection (viewport) + camera
uniform mat3 worldMatrix; // matrix stack
uniform mat3 modelMatrix; // local transformations uniform mat3 modelMatrix; // local transformations
void main() { void main() {
vec3 result = viewMatrix * worldMatrix * modelMatrix * pos; vec3 result = viewMatrix * modelMatrix * pos;
gl_Position = vec4(result.x, result.y, 0.0, result.z); gl_Position = vec4(result.x, result.y, 0.0, result.z);
#ifdef HUE_SHIFT #ifdef HUE_SHIFT

View File

@ -5,12 +5,12 @@ layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 uvData; layout (location = 1) in vec2 uvData;
uniform mat3 viewMatrix; // projection (viewport) uniform mat3 viewMatrix; // projection (viewport)
uniform mat3 worldMatrix; // world position uniform mat3 modelMatrix; // world position
out vec2 uvOut; out vec2 uvOut;
void main() { void main() {
vec3 result = viewMatrix * worldMatrix * pos; vec3 result = viewMatrix * modelMatrix * pos;
gl_Position = vec4(result.x, result.y, 0.0, result.z); gl_Position = vec4(result.x, result.y, 0.0, result.z);
uvOut = uvData; uvOut = uvData;