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("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")
}

View File

@ -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<WeakReference<GLShaderProgram.Regular>>()
private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()
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<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
private val textures = Array(maxTextureBlocks) { GLObjectTracker<GLTexture2D>(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<GLTexture2D>(maxTextureBlocks)
var clearColor by GLGenericTracker<IStruct4f>(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 = "<unknown>") = 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()

View File

@ -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
}

View File

@ -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()

View File

@ -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 = "<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 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 = "<unknown>") : 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 = "<unknown>") : 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
}

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>,
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()

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"))
}
private fun fontShaders(): List<GLShader> {
val client = StarboundClient.current()
var read = StarboundClient.readInternal("shaders/fragment/font.fsh")
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")
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")

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.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<Flag>,
) : 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<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 {
private val directives = Object2ObjectArrayMap<String, String>()
private val attributes = ObjectArraySet<VertexAttributeType>()

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

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))),
)
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

View File

@ -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<ArrayList<() -> Unit>>()
interface IGeometryLayer {
fun add(renderer: () -> Unit)
fun getBuilder(config: RenderConfig<*>): VertexBuilder
fun render()
fun clear()
fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, 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<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.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<T : GLShaderProgram>(val config: RenderConfig<T>, val mesh: Mesh = Mesh()) {
fun render() {
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) {
val client get() = program.client
open val initialBuilderCapacity: Int get() = 64
open val initialBuilderCapacity: Int get() = 32
open fun setup() {
program.use()

View File

@ -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<Point> {
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<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) {
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
}
}
}

View File

@ -27,7 +27,6 @@ class TileRenderers(val client: StarboundClient) {
private val background = HashMap<GLTexture2D, Config>()
private val matCache = HashMap<String, TileRenderer>()
private val modCache = HashMap<String, TileRenderer>()
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

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.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<Pair<ConfiguredMesh<*>, Long>>()
val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, 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)

View File

@ -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

View File

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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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;