From 194a7e479c83284e61bcc75ffad3f1ebea312e53 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 11 Sep 2022 01:10:40 +0700 Subject: [PATCH] Redo vertex builder --- .../kstarbound/client/ClientWorld.kt | 12 +- .../kstarbound/client/gl/GLAttributeList.kt | 133 ++-- .../kstarbound/client/gl/GLStateTracker.kt | 48 +- .../kstarbound/client/gl/GLType.kt | 27 + .../kstarbound/client/gl/VertexBuilder.kt | 601 ------------------ .../client/gl/vertex/AbstractVertexBuilder.kt | 262 ++++++++ .../client/gl/vertex/DirectVertexBuilder.kt | 50 ++ .../client/gl/vertex/HeapVertexBuilder.kt | 63 ++ .../client/gl/vertex/QuadTransformers.kt | 65 ++ .../client/gl/vertex/StreamVertexBuilder.kt | 52 ++ .../kstarbound/client/gl/vertex/VertexType.kt | 9 + .../kstarbound/client/render/Box2DRenderer.kt | 24 +- .../client/render/ConfiguredShaderProgram.kt | 8 +- .../client/render/EntityRenderer.kt | 12 +- .../kstarbound/client/render/Font.kt | 15 +- .../kstarbound/client/render/TileRenderer.kt | 14 +- .../kstarbound/util/ByteBufferOutputStream.kt | 27 + .../dbotthepony/kstarbound/util/LEStream.kt | 39 ++ 18 files changed, 688 insertions(+), 773 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLType.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/AbstractVertexBuilder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/DirectVertexBuilder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/HeapVertexBuilder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/ByteBufferOutputStream.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 31366f69..247e1945 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -2,7 +2,7 @@ package ru.dbotthepony.kstarbound.client import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf -import ru.dbotthepony.kstarbound.client.gl.VertexTransformers +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer import ru.dbotthepony.kstarbound.client.render.renderLayeredList import ru.dbotthepony.kstarbound.defs.ParallaxPrototype @@ -10,7 +10,6 @@ import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB import ru.dbotthepony.kstarbound.util.DoubleEdgeProgression import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.entities.Entity -import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.util2d.AABB class ClientWorld( @@ -49,8 +48,7 @@ class ClientWorld( client.gl.shaderVertexTexture.use() - val stateful = client.gl.flat2DTexturedQuads.statefulSmall - val builder = stateful.builder + val builder = client.gl.flat2DTexturedQuads.small client.gl.activeTexture = 0 client.gl.shaderVertexTexture["_texture"] = 0 @@ -80,7 +78,7 @@ class ClientWorld( x0 += diffx.toFloat() / PIXELS_IN_STARBOUND_UNITf x1 += diffx.toFloat() / PIXELS_IN_STARBOUND_UNITf - builder.quadZ(x0, diffy, x1, diffy + texture.height.toFloat() / PIXELS_IN_STARBOUND_UNITf, 1f, VertexTransformers.uv(0f, 1f, 1f, 0f)) + builder.quadZ(x0, diffy, x1, diffy + texture.height.toFloat() / PIXELS_IN_STARBOUND_UNITf, 1f, QuadTransformers.uv(0f, 1f, 1f, 0f)) /*if (x1 < size.mins.x) { break @@ -91,8 +89,8 @@ class ClientWorld( } } - stateful.upload() - stateful.draw() + builder.upload() + builder.draw() client.gl.matrixStack.pop() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt index ba85fb90..99ea5cb1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt @@ -1,45 +1,19 @@ package ru.dbotthepony.kstarbound.client.gl import com.google.common.collect.ImmutableList -import org.lwjgl.opengl.GL46.* -enum class GLType(val identity: Int, val typeIndentity: Int, val byteSize: Int, val logicalSize: Int) { - INT(GL_INT, GL_INT, 4, 1), - UINT(GL_UNSIGNED_INT, GL_UNSIGNED_INT, 4, 1), - FLOAT(GL_FLOAT, GL_FLOAT, 4, 1), - DOUBLE(GL_DOUBLE, GL_DOUBLE, 8, 1), - - VEC2F(GL_FLOAT_VEC2, GL_FLOAT, 8, 2), - VEC3F(GL_FLOAT_VEC3, GL_FLOAT, 12, 3), - VEC4F(GL_FLOAT_VEC4, GL_FLOAT, 16, 4), - - VEC2I(GL_INT_VEC2, GL_INT, 8, 2), - VEC3I(GL_INT_VEC3, GL_INT, 12, 3), - VEC4I(GL_INT_VEC4, GL_INT, 16, 4), - - MAT2F(GL_FLOAT_MAT2, GL_FLOAT, 2 * 2 * 4, 2 * 2), - MAT3F(GL_FLOAT_MAT3, GL_FLOAT, 3 * 3 * 4, 3 * 3), - MAT4F(GL_FLOAT_MAT4, GL_FLOAT, 4 * 4 * 4, 4 * 4), -} - -interface IGLAttributeList { - /** - * Применяет список атрибутов к заданному [GLVertexArrayObject] (попутно включая или отключая их через [enable]) - */ - fun apply(target: GLVertexArrayObject, enable: Boolean = false) -} - -data class AttributeListPosition(val name: String, val index: Int, val glType: GLType, val stride: Int, val offset: Long) /** * Хранит список аттрибутов для применения к Vertex Array Object * * Аттрибуты плотно упакованы и идут один за другим * - * Создаётся через [GLFlatAttributeListBuilder] + * Создаётся через [GLAttributeList.Builder] */ -class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList { - val attributes: List +class GLAttributeList(builder: Builder) { + data class Attribute(val name: String, val index: Int, val glType: GLType, val stride: Int, val offset: Long) + + val attributes: List val size get() = attributes.size /** @@ -49,10 +23,8 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis operator fun get(index: Int) = attributes[index] - fun vertexBuilder(vertexType: VertexType) = DynamicVertexBuilder(this, vertexType) - init { - val buildList = ArrayList() + val buildList = ArrayList() var offset = 0L var stride = 0 @@ -63,20 +35,19 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis this.stride = stride - // val counter = mutableMapOf() - for (i in builder.attributes.indices) { val value = builder.attributes[i].second - buildList.add(AttributeListPosition(builder.attributes[i].first, i, value, stride, offset)) + buildList.add(Attribute(builder.attributes[i].first, i, value, stride, offset)) offset += value.byteSize - - // counter[value.typeIndentity] = (counter[value.typeIndentity] ?: 0) + 1 } attributes = ImmutableList.copyOf(buildList) } - override fun apply(target: GLVertexArrayObject, enable: Boolean) { + /** + * Применяет список атрибутов к заданному [GLVertexArrayObject] (попутно включая или отключая их через [enable]) + */ + fun apply(target: GLVertexArrayObject, enable: Boolean) { for (i in attributes.indices) { val value = attributes[i] target.attribute(i, value.glType.logicalSize, value.glType.typeIndentity, false, value.stride, value.offset) @@ -87,66 +58,36 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis } } + class Builder { + val attributes = ArrayList>() + + fun push(type: GLType): Builder { + return push("$type#${attributes.size}", type) + } + + fun push(vararg types: GLType): Builder { + for (type in types) push(type) + return this + } + + fun push(name: String, type: GLType): Builder { + check(!attributes.any { it.first == name }) { "Already has named attribute $name!" } + attributes.add(name to type) + return this + } + + fun build() = GLAttributeList(this) + } + companion object { - val VEC2F = GLFlatAttributeListBuilder().push(GLType.VEC2F).build() - val VEC3F = GLFlatAttributeListBuilder().push(GLType.VEC3F).build() + val VEC2F = Builder().push(GLType.VEC2F).build() + val VEC3F = Builder().push(GLType.VEC3F).build() - val VERTEX_TEXTURE = GLFlatAttributeListBuilder().push(GLType.VEC3F).push(GLType.VEC2F).build() + val VERTEX_TEXTURE = Builder().push(GLType.VEC3F).push(GLType.VEC2F).build() - val VERTEX_HSV_TEXTURE = GLFlatAttributeListBuilder() + val VERTEX_HSV_TEXTURE = Builder() .push(GLType.VEC3F, GLType.VEC2F, GLType.FLOAT).build() - val VERTEX_2D_TEXTURE = GLFlatAttributeListBuilder().push(GLType.VEC2F).push(GLType.VEC2F).build() - } -} - -class GLFlatAttributeListBuilder : IGLAttributeList { - val attributes = ArrayList>() - - private fun findName(name: String): Boolean { - for (value in attributes) { - if (value.first == name) { - return true - } - } - - return false - } - - fun push(type: GLType): GLFlatAttributeListBuilder { - return push("$type#${attributes.size}", type) - } - - fun push(vararg types: GLType): GLFlatAttributeListBuilder { - for (type in types) push(type) - return this - } - - fun push(name: String, type: GLType): GLFlatAttributeListBuilder { - check(!findName(name)) { "Already has named attribute $name!" } - attributes.add(name to type) - return this - } - - fun build() = GLFlatAttributeList(this) - - @Deprecated("Используй build()", replaceWith = ReplaceWith("build()")) - override fun apply(target: GLVertexArrayObject, enable: Boolean) { - var offset = 0L - var stride = 0 - - for (i in attributes) { - stride += i.second.byteSize - } - - for (i in attributes.indices) { - val value = attributes[i].second - target.attribute(i, value.logicalSize, value.typeIndentity, false, stride, offset) - offset += value.byteSize - - if (enable) { - target.enableAttribute(i) - } - } + val VERTEX_2D_TEXTURE = Builder().push(GLType.VEC2F).push(GLType.VEC2F).build() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index 0b2ab8bd..6434f915 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -5,6 +5,8 @@ import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.freetype.FreeType +import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder +import ru.dbotthepony.kstarbound.client.gl.vertex.VertexType import ru.dbotthepony.kstarbound.client.render.Box2DRenderer import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kstarbound.client.render.TileRenderers @@ -86,7 +88,6 @@ interface GLCleanable : Cleaner.Cleanable { interface GLStreamBuilderList { val small: StreamVertexBuilder - val statefulSmall: StatefulStreamVertexBuilder } class GLStateTracker { @@ -403,61 +404,37 @@ class GLStateTracker { val flat2DLines = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.LINES, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.LINES, 1024) } } val flat2DTriangles = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.TRIANGLES, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.TRIANGLES, 1024) } } val flat2DQuads = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS, 1024) } } val flat2DTexturedQuads = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024) } } val flat2DQuadLines = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024) } } val flat2DQuadWireframe = object : GLStreamBuilderList { override val small by lazy { - return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024) - } - - override val statefulSmall by lazy { - return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024) } } @@ -467,20 +444,17 @@ class GLStateTracker { val font = Font(this) inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) { - val stateful = flat2DQuadWireframe.statefulSmall - val builder = stateful.builder + val builder = flat2DQuadWireframe.small builder.begin() - lambda.invoke(builder) - - stateful.upload() + builder.upload() flatProgram.use() flatProgram.color.set(color) flatProgram.transform.set(matrixStack.last) - stateful.draw(GL_LINES) + builder.draw(GL_LINES) } inline fun quadWireframe(value: AABB, color: Color = Color.WHITE, chain: (StreamVertexBuilder) -> Unit = {}) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLType.kt new file mode 100644 index 00000000..df194f72 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLType.kt @@ -0,0 +1,27 @@ +package ru.dbotthepony.kstarbound.client.gl + +import org.lwjgl.opengl.GL46.* + +enum class GLType( + val identity: Int, + val typeIndentity: Int, + val byteSize: Int, + val logicalSize: Int, +) { + INT(GL_INT, GL_INT, 4, 1), + UINT(GL_UNSIGNED_INT, GL_UNSIGNED_INT, 4, 1), + FLOAT(GL_FLOAT, GL_FLOAT, 4, 1), + DOUBLE(GL_DOUBLE, GL_DOUBLE, 8, 1), + + VEC2F(GL_FLOAT_VEC2, GL_FLOAT, 8, 2), + VEC3F(GL_FLOAT_VEC3, GL_FLOAT, 12, 3), + VEC4F(GL_FLOAT_VEC4, GL_FLOAT, 16, 4), + + VEC2I(GL_INT_VEC2, GL_INT, 8, 2), + VEC3I(GL_INT_VEC3, GL_INT, 12, 3), + VEC4I(GL_INT_VEC4, GL_INT, 16, 4), + + MAT2F(GL_FLOAT_MAT2, GL_FLOAT, 2 * 2 * 4, 2 * 2), + MAT3F(GL_FLOAT_MAT3, GL_FLOAT, 3 * 3 * 4, 3 * 3), + MAT4F(GL_FLOAT_MAT4, GL_FLOAT, 4 * 4 * 4, 4 * 4), +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt deleted file mode 100644 index f9c5933d..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt +++ /dev/null @@ -1,601 +0,0 @@ -package ru.dbotthepony.kstarbound.client.gl - -import org.lwjgl.opengl.GL46.* -import ru.dbotthepony.kvector.util2d.AABB -import java.io.Closeable -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.collections.ArrayList -import kotlin.math.cos -import kotlin.math.sin - -enum class VertexType(val elements: Int, val indicies: IntArray) { - LINES(2, intArrayOf(0, 1)), - TRIANGLES(3, intArrayOf(0, 1, 2)), - QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)), - QUADS_AS_LINES(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3)), - QUADS_AS_LINES_WIREFRAME(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3, 0, 3, 1, 2)), -} - -interface IVertexBuilder, VertexType : IVertex> { - val type: ru.dbotthepony.kstarbound.client.gl.VertexType - val indexCount: Int - - fun begin(): This - fun vertex(): VertexType - fun checkValid() - fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int = GL_DYNAMIC_DRAW) - - fun quad( - x0: Float, - y0: Float, - x1: Float, - y1: Float, - lambda: VertexTransformer = emptyTransform - ): This { - check(type.elements == 4) { "Currently building $type" } - - lambda(vertex().pushVec2f(x0, y0), 0).end() - lambda(vertex().pushVec2f(x1, y0), 1).end() - lambda(vertex().pushVec2f(x0, y1), 2).end() - lambda(vertex().pushVec2f(x1, y1), 3).end() - - return this as This - } - - fun quadRotated( - x0: Float, - y0: Float, - x1: Float, - y1: Float, - x: Float, - y: Float, - angle: Double, - lambda: VertexTransformer = emptyTransform - ): This { - check(type.elements == 4) { "Currently building $type" } - - val s = sin(angle).toFloat() - val c = cos(angle).toFloat() - - lambda(vertex().pushVec2f(x + x0 * c - s * y0, y + s * x0 + c * y0), 0).end() - lambda(vertex().pushVec2f(x + x1 * c - s * y0, y + s * x1 + c * y0), 1).end() - lambda(vertex().pushVec2f(x + x0 * c - s * y1, y + s * x0 + c * y1), 2).end() - lambda(vertex().pushVec2f(x + x1 * c - s * y1, y + s * x1 + c * y1), 3).end() - - return this as This - } - - fun quad(aabb: AABB, lambda: VertexTransformer = emptyTransform): This { - return quad( - aabb.mins.x.toFloat(), - aabb.mins.y.toFloat(), - aabb.maxs.x.toFloat(), - aabb.maxs.y.toFloat(), - lambda - ) - } - - fun quadZ( - x0: Float, - y0: Float, - x1: Float, - y1: Float, - z: Float, - lambda: VertexTransformer = emptyTransform - ): This { - check(type.elements == 4) { "Currently building $type" } - - lambda(vertex().pushVec3f(x0, y0, z), 0).end() - lambda(vertex().pushVec3f(x1, y0, z), 1).end() - lambda(vertex().pushVec3f(x0, y1, z), 2).end() - lambda(vertex().pushVec3f(x1, y1, z), 3).end() - - return this as This - } - - fun quadRotatedZ( - x0: Float, - y0: Float, - x1: Float, - y1: Float, - z: Float, - x: Float, - y: Float, - angle: Double, - lambda: VertexTransformer = emptyTransform - ): This { - check(type.elements == 4) { "Currently building $type" } - - val s = sin(angle).toFloat() - val c = cos(angle).toFloat() - - lambda(vertex().pushVec3f(x + x0 * c - s * y0, y + s * x0 + c * y0, z), 0).end() - lambda(vertex().pushVec3f(x + x1 * c - s * y0, y + s * x1 + c * y0, z), 1).end() - lambda(vertex().pushVec3f(x + x0 * c - s * y1, y + s * x0 + c * y1, z), 2).end() - lambda(vertex().pushVec3f(x + x1 * c - s * y1, y + s * x1 + c * y1, z), 3).end() - - return this as This - } -} - -interface IVertex, VertexBuilderType> { - fun checkValid() - fun expect(name: String): This - fun expect(type: GLType): This - fun pushVec3f(x: Float, y: Float, z: Float): This - fun pushVec2f(x: Float, y: Float): This - fun push(value: Float): This - fun end(): VertexBuilderType -} - -typealias VertexTransformer = (IVertex<*, *>, Int) -> IVertex<*, *> -private val emptyTransform: VertexTransformer = { it, _ -> it } -private val EMPTY_BUFFER = ByteBuffer.allocateDirect(0) - -fun VertexTransformer.before(other: VertexTransformer): VertexTransformer { - return { a, b -> - other.invoke(a, b) - this.invoke(a, b) - } -} - -fun VertexTransformer.after(other: VertexTransformer): VertexTransformer { - return { a, b -> - this.invoke(a, b) - other.invoke(a, b) - } -} - -object VertexTransformers { - fun uv(u0: Float, - v0: Float, - u1: Float, - v1: Float, - lambda: VertexTransformer = emptyTransform - ): VertexTransformer { - return transformer@{ it, index -> - when (index) { - 0 -> it.pushVec2f(u0, v0) - 1 -> it.pushVec2f(u1, v0) - 2 -> it.pushVec2f(u0, v1) - 3 -> it.pushVec2f(u1, v1) - } - - return@transformer lambda(it, index) - } - } - - fun uv(): VertexTransformer { - return transformer@{ it, index -> - when (index) { - 0 -> it.pushVec2f(0f, 0f) - 1 -> it.pushVec2f(1f, 0f) - 2 -> it.pushVec2f(0f, 1f) - 3 -> it.pushVec2f(1f, 1f) - } - - return@transformer it - } - } - - fun uv(lambda: VertexTransformer): VertexTransformer { - return transformer@{ it, index -> - when (index) { - 0 -> it.pushVec2f(0f, 0f) - 1 -> it.pushVec2f(1f, 0f) - 2 -> it.pushVec2f(0f, 1f) - 3 -> it.pushVec2f(1f, 1f) - } - - return@transformer lambda(it, index) - } - } -} - -class DynamicVertexBuilder(val attributes: GLFlatAttributeList, override val type: VertexType) : IVertexBuilder { - private val verticies = ArrayList() - override val indexCount get() = (verticies.size / type.elements) * type.indicies.size - - override fun begin(): DynamicVertexBuilder { - verticies.clear() - return this - } - - override fun vertex(): Vertex { - return Vertex() - } - - override fun checkValid() { - for (vertex in verticies) { - vertex.checkValid() - } - } - - /** - * Загружает (копирует) данные в указанные буферы, с их текущей позиции - */ - fun upload( - vertexBuffer: ByteBuffer, - elementBuffer: ByteBuffer, - ) { - check(verticies.size % type.elements == 0) { "Not fully built (expected ${type.elements} verticies to be present for each element, last element has only ${verticies.size % type.elements})" } - - require(vertexBuffer.order() == ByteOrder.nativeOrder()) { "Byte order of $vertexBuffer does not match native order" } - require(elementBuffer.order() == ByteOrder.nativeOrder()) { "Byte order of $elementBuffer does not match native order" } - - checkValid() - - for (vertex in verticies) { - vertex.upload(vertexBuffer) - } - - var offsetVertex = 0 - - for (i in 0 until verticies.size / type.elements) { - for (i2 in type.indicies.indices) { - elementBuffer.putInt(type.indicies[i2] + offsetVertex) - } - - offsetVertex += type.elements - } - } - - /** - * Загружает буфер в указанные VBO и EBO - * - * операция создаёт мусор вне кучи и довольно медленная - */ - override fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { - require(vbo.isArray) { "$vbo is not an array" } - require(ebo.isElementArray) { "$vbo is not an element array" } - - check(verticies.size % type.elements == 0) { "Not fully built (expected ${type.elements} verticies to be present for each element, last element has only ${verticies.size % type.elements})" } - - checkValid() - - if (verticies.size == 0) { - vbo.bufferData(EMPTY_BUFFER, drawType) - ebo.bufferData(EMPTY_BUFFER, drawType) - - return - } - - val vertexBuffer = ByteBuffer.allocateDirect(verticies.size * attributes.stride) - vertexBuffer.order(ByteOrder.nativeOrder()) - - val elementBuffer = ByteBuffer.allocateDirect((verticies.size / type.elements) * type.indicies.size * 4) - elementBuffer.order(ByteOrder.nativeOrder()) - - upload(vertexBuffer, elementBuffer) - - check(vertexBuffer.position() == vertexBuffer.capacity()) { "Vertex Buffer is not fully filled (position: ${vertexBuffer.position()}; capacity: ${vertexBuffer.capacity()})" } - check(elementBuffer.position() == elementBuffer.capacity()) { "Element Buffer is not fully filled (position: ${elementBuffer.position()}; capacity: ${elementBuffer.capacity()})" } - - vertexBuffer.position(0) - elementBuffer.position(0) - - vbo.bufferData(vertexBuffer, drawType) - ebo.bufferData(elementBuffer, drawType) - } - - inner class Vertex : IVertex { - init { - verticies.add(this) - } - - private val store = arrayOfNulls(attributes.size) - private var index = 0 - - fun upload(bytes: ByteBuffer) { - for (element in store) { - when (element) { - is FloatArray -> for (i in element) bytes.putFloat(i) - is IntArray -> for (i in element) bytes.putInt(i) - is ByteArray -> for (i in element) bytes.put(i) - is DoubleArray -> for (i in element) bytes.putDouble(i) - is Float -> bytes.putFloat(element) - else -> throw IllegalStateException("Unknown element $element") - } - } - } - - override fun toString(): String { - return "Vertex(${store.map { - return@map when (it) { - is FloatArray -> it.joinToString(", ") - is IntArray -> it.joinToString(", ") - is ByteArray -> it.joinToString(", ") - is DoubleArray -> it.joinToString(", ") - is Float -> it - else -> "null" - } }.joinToString("; ")})" - } - - override fun expect(name: String): Vertex { - if (index >= attributes.size) { - throw IllegalStateException("Reached end of attribute list early, expected $name") - } - - if (attributes[index].name != name) { - throw IllegalStateException("Expected $name, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") - } - - return this - } - - override fun expect(type: GLType): Vertex { - if (index >= attributes.size) { - throw IllegalStateException("Reached end of attribute list early, expected type $type") - } - - if (attributes[index].glType != type) { - throw IllegalStateException("Expected $type, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") - } - - return this - } - - override fun pushVec3f(x: Float, y: Float, z: Float): Vertex { - expect(GLType.VEC3F) - store[index++] = floatArrayOf(x, y, z) - return this - } - - override fun pushVec2f(x: Float, y: Float): Vertex { - expect(GLType.VEC2F) - store[index++] = floatArrayOf(x, y) - return this - } - - override fun push(value: Float): Vertex { - expect(GLType.FLOAT) - store[index++] = value - return this - } - - override fun checkValid() { - for (elem in store.indices) { - if (store[elem] == null) { - throw IllegalStateException("Vertex element at position $elem is null") - } - } - } - - override fun end(): DynamicVertexBuilder { - checkValid() - return this@DynamicVertexBuilder - } - } -} - -/** - * "Поточная" версия [DynamicVertexBuilder], ориентированная на скорость работы и имеющая фиксированный размер буфера - * - * Главные отличия: - * * Данный объект не желательно создавать каждый раз когда надо отрисовать какое либо количество геометрии, а использовать уже существующий, который - * удовлетворяет требованиям (формат вершин, и их потенциально максимальное количество) - * * Максимальное количество vertex'ов фиксированно и равняется [maxElements] * [VertexType.elements] - * * Имеет два встроенных DirectByteBuffer и НЕ позволяет загружать данные в другие ByteBuffer, только во внутренние ByteBuffer и только в VBO; EBO - */ -class StreamVertexBuilder( - val attributes: GLFlatAttributeList, - override val type: VertexType, - val maxElements: Int, -) : IVertexBuilder { - val maxVertexNum = maxElements * type.elements - var nextVertex = 0 - private set - - override val indexCount get() = (nextVertex / type.elements) * type.indicies.size - val maxIndexCount = maxElements * type.indicies.size - - val elementIndexType = when (maxIndexCount) { - // api performance issue 102: glDrawElements uses element index type 'GL_UNSIGNED_BYTE' that is not optimal for the current hardware configuration; consider using 'GL_UNSIGNED_SHORT' instead - // in 0 .. 255 -> GL_UNSIGNED_BYTE - in 0 .. 65535 -> GL_UNSIGNED_SHORT - else -> GL_UNSIGNED_INT - } - - private var head: Vertex? = null - - /** - * Буфер для VBO достаточного размера - */ - private val vertexBuffer = ByteBuffer.allocateDirect(maxVertexNum * attributes.stride) - - /** - * Буфер для EBO достаточного размера - */ - private val elementBuffer = ByteBuffer.allocateDirect(maxElements * type.indicies.size * 4) - - init { - vertexBuffer.order(ByteOrder.nativeOrder()) - elementBuffer.order(ByteOrder.nativeOrder()) - } - - private fun writeElementIndex(value: Int) { - when (elementIndexType) { - GL_UNSIGNED_BYTE -> elementBuffer.put(value.toByte()) - GL_UNSIGNED_SHORT -> elementBuffer.putShort(value.toShort()) - else -> elementBuffer.putInt(value) - } - } - - private var offsetElementIndex = 0 - - /** - * Устанавливает метку этого билдера в ноль. - * - * Не обнуляет память буферов! - */ - override fun begin(): StreamVertexBuilder { - nextVertex = 0 - offsetElementIndex = 0 - head = null - vertexBuffer.position(0) - elementBuffer.position(0) - return this - } - - override fun vertex(): Vertex { - return Vertex() - } - - override fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { - require(vbo.isArray) { "$vbo is not an array" } - require(ebo.isElementArray) { "$vbo is not an element array" } - - if (nextVertex == 0) { - vbo.bufferData(EMPTY_BUFFER, drawType) - ebo.bufferData(EMPTY_BUFFER, drawType) - - return - } - - checkValid() - - val a = vertexBuffer.position().toLong() - val b = elementBuffer.position().toLong() - - vertexBuffer.position(0) - elementBuffer.position(0) - - vbo.bufferData(vertexBuffer, drawType, length = a) - ebo.bufferData(elementBuffer, drawType, length = b) - } - - override fun checkValid() { - var vertex = head - - while (vertex != null) { - vertex.checkValid() - vertex = vertex.previous - } - } - - inner class Vertex : IVertex { - private val vertexIndex = nextVertex++ - val previous = head - private var bufferPosition = vertexIndex * attributes.stride - - init { - if (vertexIndex >= maxVertexNum) { - throw IndexOutOfBoundsException("Tried to push new vertex $vertexIndex, when already above limit of $maxVertexNum!") - } - - head = this - - for (i2 in type.indicies.indices) { - writeElementIndex(type.indicies[i2] + offsetElementIndex) - } - - offsetElementIndex += type.elements - } - - private var index = 0 - - override fun checkValid() { - check(index == attributes.size) { "Vertex $vertexIndex is not fully filled (only $index attributes provided, ${attributes.size} required)" } - } - - override fun expect(name: String): Vertex { - if (index >= attributes.size) { - throw IllegalStateException("Reached end of attribute list early, expected $name") - } - - if (attributes[index].name != name) { - throw IllegalStateException("Expected $name, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") - } - - return this - } - - override fun expect(type: GLType): Vertex { - if (index >= attributes.size) { - throw IllegalStateException("Reached end of attribute list early, expected type $type") - } - - if (attributes[index].glType != type) { - throw IllegalStateException("Expected $type, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") - } - - return this - } - - override fun pushVec3f(x: Float, y: Float, z: Float): Vertex { - expect(GLType.VEC3F) - vertexBuffer.position(bufferPosition) - vertexBuffer.putFloat(x) - vertexBuffer.putFloat(y) - vertexBuffer.putFloat(z) - index++ - bufferPosition += 12 - return this - } - - override fun pushVec2f(x: Float, y: Float): Vertex { - expect(GLType.VEC2F) - vertexBuffer.position(bufferPosition) - vertexBuffer.putFloat(x) - vertexBuffer.putFloat(y) - index++ - bufferPosition += 8 - return this - } - - override fun push(value: Float): Vertex { - expect(GLType.FLOAT) - vertexBuffer.position(bufferPosition) - vertexBuffer.putFloat(value) - index++ - bufferPosition += 4 - return this - } - - override fun end(): StreamVertexBuilder { - check(index == attributes.size) { "Vertex $vertexIndex is not fully filled (only $index attributes provided, ${attributes.size} required)" } - return this@StreamVertexBuilder - } - } -} - -class StatefulStreamVertexBuilder( - val state: GLStateTracker, - val builder: StreamVertexBuilder -) : Closeable, IVertexBuilder by builder { - private val vao = state.newVAO() - private val vbo = state.newVBO() - private val ebo = state.newEBO() - - init { - vao.bind() - vbo.bind() - ebo.bind() - - builder.attributes.apply(vao, true) - - vao.unbind() - vbo.unbind() - ebo.unbind() - } - - fun upload(drawType: Int = GL_DYNAMIC_DRAW) { - builder.upload(vbo, ebo, drawType) - } - - fun bind() = vao.bind() - fun unbind() = vao.unbind() - - fun draw(primitives: Int = GL_TRIANGLES) { - bind() - glDrawElements(primitives, builder.indexCount, builder.elementIndexType, 0L) - checkForGLError() - } - - override fun close() { - vao.close() - vbo.close() - ebo.close() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/AbstractVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/AbstractVertexBuilder.kt new file mode 100644 index 00000000..7d69d18c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/AbstractVertexBuilder.kt @@ -0,0 +1,262 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +import org.lwjgl.opengl.GL46 +import org.lwjgl.opengl.GL46.GL_UNSIGNED_INT +import org.lwjgl.opengl.GL46.GL_UNSIGNED_SHORT +import org.lwjgl.opengl.GL46.GL_UNSIGNED_BYTE +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.GLType +import ru.dbotthepony.kstarbound.client.gl.GLVertexBufferObject +import ru.dbotthepony.kstarbound.util.writeLEFloat +import ru.dbotthepony.kstarbound.util.writeLEInt +import ru.dbotthepony.kstarbound.util.writeLEShort +import ru.dbotthepony.kvector.util2d.AABB +import java.io.OutputStream +import kotlin.math.cos +import kotlin.math.sin + +private fun put(type: Int, memory: OutputStream, value: Int) { + when (type) { + GL_UNSIGNED_SHORT -> memory.writeLEShort(value) + GL_UNSIGNED_BYTE -> memory.write(value) + else -> memory.writeLEInt(value) + } +} + +/** + * Класс для построения геометрии для загрузки в память видеокарты. + */ +@Suppress("unchecked_cast") +abstract class AbstractVertexBuilder>( + val attributes: GLAttributeList, + val type: VertexType, +) { + protected abstract val vertexMemory: OutputStream + protected abstract val elementMemory: OutputStream + + /** + * [GL_UNSIGNED_BYTE], [GL_UNSIGNED_SHORT] или [GL_UNSIGNED_INT] + * + * Обязана быть статичным числом. + * + * Если нет, и его надо изменить, то это можно только делать внутри [ensureIndexCapacity]. + * + * Подкласс обязан самостоятельно изменить тип существующих элементов внутри [elementMemory] + */ + abstract val elementIndexType: Int + + protected abstract fun ensureIndexCapacity() + + protected abstract fun resetMemory() + + fun begin() { + inVertex = false + attributeIndex = 0 + elementIndexOffset = 0 + vertexCount = 0 + indexCount = 0 + resetMemory() + } + + private var inVertex = false + private var attributeIndex = 0 + protected var elementIndexOffset = 0 + private set + + var vertexCount = 0 + private set + + var indexCount = 0 + private set + + protected fun checkBounds() { + if (attributeIndex >= attributes.size) { + throw IndexOutOfBoundsException("Tried to add new attribute when already added all attributes") + } + } + + fun expect(type: GLType) { + checkBounds() + + if (attributes[attributeIndex].glType != type) { + throw IllegalStateException("Expected attribute type $type, got ${attributes[attributeIndex].name}[${attributes[attributeIndex].glType}] (at position $attributeIndex)") + } + } + + fun expect(name: String) { + checkBounds() + + if (attributes[attributeIndex].name != name) { + throw IllegalStateException("Expected attribute name $name, got ${attributes[attributeIndex].name}[${attributes[attributeIndex].glType}] (at position $attributeIndex)") + } + } + + protected abstract fun doUpload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) + + fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int = GL46.GL_DYNAMIC_DRAW) { + require(vbo.isArray) { "$vbo is not an array" } + require(ebo.isElementArray) { "$vbo is not an element array" } + + end() + + check(elementVertices == 0) { "Not fully built vertex element ($type requires ${type.elements} vertex points to be present, yet last strip has only $elementVertices elements)" } + + doUpload(vbo, ebo, drawType) + } + + private var elementVertices = 0 + + fun end() { + if (inVertex) { + inVertex = false + + if (attributeIndex != attributes.size) { + throw IllegalStateException("Unfinished vertex, we are at $attributeIndex, while we have ${attributes.size} attributes") + } + + vertexCount++ + elementVertices++ + + if (elementVertices == type.elements) { + ensureIndexCapacity() + + elementVertices = 0 + val elementMemory = elementMemory + val elementIndexType = elementIndexType + + for (index in type.indicies) { + put(elementIndexType, elementMemory, index + elementIndexOffset) + } + + elementIndexOffset += type.elements + indexCount += type.indicies.size + } + } + } + + fun vertex(): T { + end() + inVertex = true + attributeIndex = 0 + return this as T + } + + fun pushVec3f(x: Float, y: Float, z: Float): T { + expect(GLType.VEC3F) + val memory = vertexMemory + memory.writeLEFloat(x) + memory.writeLEFloat(y) + memory.writeLEFloat(z) + attributeIndex++ + return this as T + } + + fun pushVec2f(x: Float, y: Float): T { + expect(GLType.VEC2F) + val memory = vertexMemory + memory.writeLEFloat(x) + memory.writeLEFloat(y) + attributeIndex++ + return this as T + } + + fun push(value: Float): T { + expect(GLType.FLOAT) + vertexMemory.writeLEFloat(value) + attributeIndex++ + return this as T + } + + // Помощники + fun quad( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM + ): T { + check(type.elements == 4) { "Currently building $type" } + + lambda(vertex().pushVec2f(x0, y0), 0).end() + lambda(vertex().pushVec2f(x1, y0), 1).end() + lambda(vertex().pushVec2f(x0, y1), 2).end() + lambda(vertex().pushVec2f(x1, y1), 3).end() + + return this as T + } + + fun quadRotated( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + x: Float, + y: Float, + angle: Double, + lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM + ): T { + check(type.elements == 4) { "Currently building $type" } + + val s = sin(angle).toFloat() + val c = cos(angle).toFloat() + + lambda(vertex().pushVec2f(x + x0 * c - s * y0, y + s * x0 + c * y0), 0).end() + lambda(vertex().pushVec2f(x + x1 * c - s * y0, y + s * x1 + c * y0), 1).end() + lambda(vertex().pushVec2f(x + x0 * c - s * y1, y + s * x0 + c * y1), 2).end() + lambda(vertex().pushVec2f(x + x1 * c - s * y1, y + s * x1 + c * y1), 3).end() + + return this as T + } + + fun quad(aabb: AABB, lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM): T { + return quad( + aabb.mins.x.toFloat(), + aabb.mins.y.toFloat(), + aabb.maxs.x.toFloat(), + aabb.maxs.y.toFloat(), + lambda + ) + } + + fun quadZ( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + z: Float, + lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM + ): T { + check(type.elements == 4) { "Currently building $type" } + + lambda(vertex().pushVec3f(x0, y0, z), 0).end() + lambda(vertex().pushVec3f(x1, y0, z), 1).end() + lambda(vertex().pushVec3f(x0, y1, z), 2).end() + lambda(vertex().pushVec3f(x1, y1, z), 3).end() + + return this as T + } + + fun quadRotatedZ( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + z: Float, + x: Float, + y: Float, + angle: Double, + lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM + ): T { + check(type.elements == 4) { "Currently building $type" } + + val s = sin(angle).toFloat() + val c = cos(angle).toFloat() + + lambda(vertex().pushVec3f(x + x0 * c - s * y0, y + s * x0 + c * y0, z), 0).end() + lambda(vertex().pushVec3f(x + x1 * c - s * y0, y + s * x1 + c * y0, z), 1).end() + lambda(vertex().pushVec3f(x + x0 * c - s * y1, y + s * x0 + c * y1, z), 2).end() + lambda(vertex().pushVec3f(x + x1 * c - s * y1, y + s * x1 + c * y1, z), 3).end() + + return this as T + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/DirectVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/DirectVertexBuilder.kt new file mode 100644 index 00000000..e7b24364 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/DirectVertexBuilder.kt @@ -0,0 +1,50 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +import org.lwjgl.opengl.GL46 +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.GLVertexBufferObject +import ru.dbotthepony.kstarbound.util.ByteBufferOutputStream + +open class DirectVertexBuilder( + attributes: GLAttributeList, + type: VertexType, + val maxElements: Int, +) : AbstractVertexBuilder(attributes, type) { + val maxIndexCount = maxElements * type.indicies.size + val maxVertexCount = maxElements * type.elements + + override val vertexMemory = ByteBufferOutputStream.directLE(maxVertexCount) + override val elementMemory = ByteBufferOutputStream.directLE(maxIndexCount) + + override val elementIndexType: Int = when (maxIndexCount) { + // api performance issue 102: glDrawElements uses element index type 'GL_UNSIGNED_BYTE' that is not optimal for the current hardware configuration; consider using 'GL_UNSIGNED_SHORT' instead + // in 0 .. 255 -> GL_UNSIGNED_BYTE + in 0 .. 65535 -> GL46.GL_UNSIGNED_SHORT + else -> GL46.GL_UNSIGNED_INT + } + + override fun ensureIndexCapacity() { + if (vertexCount >= maxVertexCount) { + throw IndexOutOfBoundsException("Vertex count overflow (can hold max of $maxElements elements, that's $maxVertexCount vertexes)") + } + } + + override fun resetMemory() { + vertexMemory.position = 0 + elementMemory.position = 0 + } + + override fun doUpload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { + val vertexPos = vertexMemory.position + val elementPos = elementMemory.position + + vertexMemory.position = 0 + elementMemory.position = 0 + + vbo.bufferData(vertexMemory.buffer, drawType, length = vertexPos.toLong()) + ebo.bufferData(elementMemory.buffer, drawType, length = elementPos.toLong()) + + vertexMemory.position = vertexPos + elementMemory.position = elementPos + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/HeapVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/HeapVertexBuilder.kt new file mode 100644 index 00000000..2270c8b0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/HeapVertexBuilder.kt @@ -0,0 +1,63 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import org.lwjgl.opengl.GL11.GL_UNSIGNED_INT +import org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.GLVertexBufferObject +import ru.dbotthepony.kstarbound.util.readLEShort +import ru.dbotthepony.kstarbound.util.writeLEInt +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Vertex Builder который хранит все данные на куче, при [upload] создаётся [ByteBuffer] вне кучи, + * в него записываются данные из кучи, данные загружаются на видеокарту. + * + * Полезен в случаях, когда размер данных для загрузки заранее не известен. + */ +class HeapVertexBuilder( + attributes: GLAttributeList, + type: VertexType, +) : AbstractVertexBuilder(attributes, type) { + override val vertexMemory = FastByteArrayOutputStream() + override val elementMemory = FastByteArrayOutputStream() + + override var elementIndexType: Int = GL_UNSIGNED_SHORT + private set + + override fun ensureIndexCapacity() { + if (elementIndexType == GL_UNSIGNED_SHORT && elementMemory.length / 2 + type.indicies.size >= 30000) { + val backing = elementMemory.array + val copy = FastByteArrayInputStream(ByteArray(elementMemory.length) { backing[it] }) + + elementIndexType = GL_UNSIGNED_INT + val elementMemory = elementMemory + elementMemory.reset() + + for (i in 0 until (copy.length / 2)) { + elementMemory.writeLEInt(copy.readLEShort()) + } + } + } + + override fun resetMemory() { + vertexMemory.reset() + elementMemory.reset() + } + + override fun doUpload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { + val vboMemory = ByteBuffer.allocateDirect(vertexMemory.length) + vboMemory.put(vertexMemory.array, 0, vertexMemory.length) + + val eboMemory = ByteBuffer.allocateDirect(elementMemory.length) + eboMemory.put(elementMemory.array, 0, elementMemory.length) + + vboMemory.position(0) + eboMemory.position(0) + + vbo.bufferData(vboMemory, drawType) + ebo.bufferData(eboMemory, drawType) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt new file mode 100644 index 00000000..545439f2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt @@ -0,0 +1,65 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +typealias QuadVertexTransformer = (AbstractVertexBuilder<*>, Int) -> AbstractVertexBuilder<*> + +val EMPTY_VERTEX_TRANSFORM: QuadVertexTransformer = { it, _ -> it } + +fun QuadVertexTransformer.before(other: QuadVertexTransformer): QuadVertexTransformer { + return { a, b -> + other.invoke(a, b) + this.invoke(a, b) + } +} + +fun QuadVertexTransformer.after(other: QuadVertexTransformer): QuadVertexTransformer { + return { a, b -> + this.invoke(a, b) + other.invoke(a, b) + } +} + +object QuadTransformers { + fun uv(u0: Float, + v0: Float, + u1: Float, + v1: Float, + lambda: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM + ): QuadVertexTransformer { + return transformer@{ it, index -> + when (index) { + 0 -> it.pushVec2f(u0, v0) + 1 -> it.pushVec2f(u1, v0) + 2 -> it.pushVec2f(u0, v1) + 3 -> it.pushVec2f(u1, v1) + } + + return@transformer lambda(it, index) + } + } + + fun uv(): QuadVertexTransformer { + return transformer@{ it, index -> + when (index) { + 0 -> it.pushVec2f(0f, 0f) + 1 -> it.pushVec2f(1f, 0f) + 2 -> it.pushVec2f(0f, 1f) + 3 -> it.pushVec2f(1f, 1f) + } + + return@transformer it + } + } + + fun uv(lambda: QuadVertexTransformer): QuadVertexTransformer { + return transformer@{ it, index -> + when (index) { + 0 -> it.pushVec2f(0f, 0f) + 1 -> it.pushVec2f(1f, 0f) + 2 -> it.pushVec2f(0f, 1f) + 3 -> it.pushVec2f(1f, 1f) + } + + return@transformer lambda(it, index) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt new file mode 100644 index 00000000..bec45f20 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt @@ -0,0 +1,52 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +import org.lwjgl.opengl.GL46 +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.GLStateTracker +import ru.dbotthepony.kstarbound.client.gl.checkForGLError +import java.io.Closeable + +/** + * Класс-помощник для быстрого построения, загрузки и отрисовки геометрии + */ +class StreamVertexBuilder( + val state: GLStateTracker, + attributes: GLAttributeList, + type: VertexType, + maxElements: Int, +) : DirectVertexBuilder(attributes, type, maxElements), Closeable { + private val vao = state.newVAO() + private val vbo = state.newVBO() + private val ebo = state.newEBO() + + init { + vao.bind() + vbo.bind() + ebo.bind() + + attributes.apply(vao, true) + + vao.unbind() + vbo.unbind() + ebo.unbind() + } + + fun upload(drawType: Int = GL46.GL_DYNAMIC_DRAW) { + upload(vbo, ebo, drawType) + } + + fun bind() = vao.bind() + fun unbind() = vao.unbind() + + fun draw(primitives: Int = GL46.GL_TRIANGLES) { + bind() + GL46.glDrawElements(primitives, indexCount, elementIndexType, 0L) + checkForGLError() + } + + override fun close() { + vao.close() + vbo.close() + ebo.close() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexType.kt new file mode 100644 index 00000000..252d75ac --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexType.kt @@ -0,0 +1,9 @@ +package ru.dbotthepony.kstarbound.client.gl.vertex + +enum class VertexType(val elements: Int, val indicies: IntArray) { + LINES(2, intArrayOf(0, 1)), + TRIANGLES(3, intArrayOf(0, 1, 2)), + QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)), + QUADS_AS_LINES(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3)), + QUADS_AS_LINES_WIREFRAME(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3, 0, 3, 1, 2)), +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt index 225751a9..8d17286b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt @@ -20,32 +20,30 @@ class Box2DRenderer(val state: GLStateTracker) : IDebugDraw { override fun drawPolygon(vertices: List, color: Color) { require(vertices.size > 1) { "Vertex list had only ${vertices.size} namings in it" } - val stateful = state.flat2DLines.statefulSmall - val builder = stateful.builder + val builder = state.flat2DLines.small builder.begin() for (i in vertices.indices) { val current = vertices[i] val next = vertices[(i + 1) % vertices.size] - builder.Vertex().pushVec2f(current.x.toFloat(), current.y.toFloat()) - builder.Vertex().pushVec2f(next.x.toFloat(), next.y.toFloat()) + builder.vertex().pushVec2f(current.x.toFloat(), current.y.toFloat()) + builder.vertex().pushVec2f(next.x.toFloat(), next.y.toFloat()) } - stateful.upload() + builder.upload() state.flatProgram.use() state.flatProgram.color.set(color) state.flatProgram.transform.set(state.matrixStack.last) - stateful.draw(GL_LINES) + builder.draw(GL_LINES) } private fun drawSolid(vertices: List, color: Color) { require(vertices.size >= 3) { "Vertex list had only ${vertices.size} namings in it" } - val stateful = state.flat2DTriangles.statefulSmall - val builder = stateful.builder + val builder = state.flat2DTriangles.small builder.begin() @@ -54,18 +52,18 @@ class Box2DRenderer(val state: GLStateTracker) : IDebugDraw { for (i in 1 until vertices.size) { val current = vertices[i] val next = vertices[(i + 1) % vertices.size] - builder.Vertex().pushVec2f(zero.x.toFloat(), zero.y.toFloat()) - builder.Vertex().pushVec2f(current.x.toFloat(), current.y.toFloat()) - builder.Vertex().pushVec2f(next.x.toFloat(), next.y.toFloat()) + builder.vertex().pushVec2f(zero.x.toFloat(), zero.y.toFloat()) + builder.vertex().pushVec2f(current.x.toFloat(), current.y.toFloat()) + builder.vertex().pushVec2f(next.x.toFloat(), next.y.toFloat()) } - stateful.upload() + builder.upload() state.flatProgram.use() state.flatProgram.color.set(color) state.flatProgram.transform.set(state.matrixStack.last) - stateful.draw(GL_TRIANGLES) + builder.draw(GL_TRIANGLES) } override fun drawSolidPolygon(vertices: List, color: Color) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ConfiguredShaderProgram.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ConfiguredShaderProgram.kt index 0cad5192..d7e7dda8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ConfiguredShaderProgram.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ConfiguredShaderProgram.kt @@ -3,8 +3,8 @@ package ru.dbotthepony.kstarbound.client.render import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject -import ru.dbotthepony.kstarbound.client.gl.DynamicVertexBuilder import ru.dbotthepony.kstarbound.client.gl.checkForGLError +import ru.dbotthepony.kstarbound.client.gl.vertex.AbstractVertexBuilder import ru.dbotthepony.kvector.api.IFloatMatrix import ru.dbotthepony.kvector.matrix.Matrix4fStack @@ -43,13 +43,15 @@ class ConfiguredStaticMesh( val programState: ConfiguredShaderProgram, val indexCount: Int, val vao: GLVertexArrayObject, + val elementIndexType: Int, ) : AutoCloseable { private var onClose = {} - constructor(programState: ConfiguredShaderProgram, builder: DynamicVertexBuilder) : this( + constructor(programState: ConfiguredShaderProgram, builder: AbstractVertexBuilder<*>) : this( programState, builder.indexCount, programState.program.state.newVAO(), + builder.elementIndexType, ) { val vbo = programState.program.state.newVBO() val ebo = programState.program.state.newEBO() @@ -80,7 +82,7 @@ class ConfiguredStaticMesh( } vao.bind() - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0L) + glDrawElements(GL_TRIANGLES, indexCount, elementIndexType, 0L) checkForGLError() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt index b2f620af..fa2808d2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt @@ -1,11 +1,10 @@ package ru.dbotthepony.kstarbound.client.render import org.lwjgl.opengl.GL46.* -import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.gl.GLStateTracker -import ru.dbotthepony.kstarbound.client.gl.VertexTransformers +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile import ru.dbotthepony.kvector.matrix.Matrix4fStack @@ -70,8 +69,7 @@ open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: animator.advance() - val stateful = state.flat2DTexturedQuads.statefulSmall - val builder = stateful.builder + val builder = state.flat2DTexturedQuads.small builder.begin() @@ -81,9 +79,9 @@ open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: val width = (animator.frameObj.width / PIXELS_IN_STARBOUND_UNITf) / 2f val height = (animator.frameObj.height / PIXELS_IN_STARBOUND_UNITf) / 2f - builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, VertexTransformers.uv(u0, v0, u1, v1)) + builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, QuadTransformers.uv(u0, v0, u1, v1)) - stateful.upload() - stateful.draw() + builder.upload() + builder.draw() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt index cd7a24b3..99fe4414 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt @@ -6,6 +6,10 @@ import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.freetype.LoadFlag import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.freetype.struct.FT_Pixel_Mode +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.vertex.HeapVertexBuilder +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers +import ru.dbotthepony.kstarbound.client.gl.vertex.VertexType import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.vector.Color @@ -259,6 +263,8 @@ class Font( private val indexCount: Int + private val elementIndexType: Int + init { face.loadChar(char, LoadFlag.RENDER) val glyph = face.nativeMemory.glyph ?: throw IllegalStateException("Unable to load glyph data for $char (code ${char.code})") @@ -301,19 +307,22 @@ class Font( ebo.bind() vbo.bind() - val builder = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS) + val builder = HeapVertexBuilder(GLAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS) - builder.quad(0f, 0f, width, height, VertexTransformers.uv()) + builder.quad(0f, 0f, width, height, QuadTransformers.uv()) builder.upload(vbo, ebo, GL_STATIC_DRAW) builder.attributes.apply(vao, true) indexCount = builder.indexCount + elementIndexType = builder.elementIndexType + vao.unbind() ebo.unbind() vbo.unbind() } else { isEmpty = true indexCount = 0 + elementIndexType = 0 vao = null vbo = null @@ -335,7 +344,7 @@ class Font( texture!!.bind() state.fontProgram.transform.set(stack.last) - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0L) + glDrawElements(GL_TRIANGLES, indexCount, elementIndexType, 0L) checkForGLError() stack.translateWithMultiplication(advanceX - bearingX, bearingY) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index a1cacabf..0845fd1e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -7,6 +7,8 @@ import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.* +import ru.dbotthepony.kstarbound.client.gl.GLAttributeList +import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kstarbound.world.ITileChunk @@ -16,7 +18,7 @@ import kotlin.collections.HashMap data class TileLayer( val bakedProgramState: ConfiguredShaderProgram, - val vertexBuilder: DynamicVertexBuilder, + val vertexBuilder: AbstractVertexBuilder<*>, val zPos: Int ) @@ -28,7 +30,7 @@ class TileLayerList { * * Если такого слоя нет, вызывается [compute] и создаётся новый [TileLayer], затем возвращается результат [compute]. */ - fun computeIfAbsent(program: ConfiguredShaderProgram, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder { + fun computeIfAbsent(program: ConfiguredShaderProgram, zLevel: Int, compute: () -> AbstractVertexBuilder<*>): AbstractVertexBuilder<*> { return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction { return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel) }).vertexBuilder @@ -166,7 +168,7 @@ private enum class TileRenderTesselateResult { HALT } -private fun vertexTextureBuilder() = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_HSV_TEXTURE, VertexType.QUADS) +private fun vertexTextureBuilder() = HeapVertexBuilder(GLAttributeList.VERTEX_HSV_TEXTURE, VertexType.QUADS) private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester { override fun test(tile: TileState?): Boolean { @@ -194,7 +196,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { val bakedBackgroundProgramState = state.tileRenderers.background(texture) // private var notifiedDepth = false - private fun tesselateAt(self: TileState, piece: RenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO, isModifier: Boolean) { + private fun tesselateAt(self: TileState, piece: RenderPiece, getter: ITileChunk, builder: AbstractVertexBuilder<*>, pos: Vector2i, offset: Vector2i = Vector2i.ZERO, isModifier: Boolean) { val fx = pos.x.toFloat() val fy = pos.y.toFloat() @@ -232,7 +234,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { val (u0, v0) = texture.pixelToUV(mins) val (u1, v1) = texture.pixelToUV(maxs) - builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0).after { it, _ -> it.push(if (!isModifier || self.modifier?.grass == true) self.hueShift else 0f) }) + builder.quadZ(a, b, c, d, Z_LEVEL, QuadTransformers.uv(u0, v1, u1, v0).after { it, _ -> it.push(if (!isModifier || self.modifier?.grass == true) self.hueShift else 0f) }) } private fun tesselatePiece( @@ -241,7 +243,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { getter: ITileChunk, layers: TileLayerList, pos: Vector2i, - thisBuilder: DynamicVertexBuilder, + thisBuilder: AbstractVertexBuilder<*>, background: Boolean, isModifier: Boolean, ): TileRenderTesselateResult { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ByteBufferOutputStream.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ByteBufferOutputStream.kt new file mode 100644 index 00000000..0e34dd7f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ByteBufferOutputStream.kt @@ -0,0 +1,27 @@ +package ru.dbotthepony.kstarbound.util + +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class ByteBufferOutputStream(val buffer: ByteBuffer) : OutputStream() { + override fun write(b: Int) { + buffer.put(b.toByte()) + } + + override fun write(b: ByteArray) { + buffer.put(b) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + buffer.put(b, off, len) + } + + var position: Int + get() = buffer.position() + set(value) { buffer.position(value) } + + companion object { + fun directLE(capacity: Int) = ByteBufferOutputStream(ByteBuffer.allocateDirect(capacity).also { it.order(ByteOrder.LITTLE_ENDIAN) }) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt new file mode 100644 index 00000000..5811beb5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt @@ -0,0 +1,39 @@ +package ru.dbotthepony.kstarbound.util + +import java.io.InputStream +import java.io.OutputStream + +fun OutputStream.writeLEInt(value: Int) { + write(value) + write(value ushr 8) + write(value ushr 16) + write(value ushr 24) +} + +fun OutputStream.writeLEShort(value: Int) { + write(value) + write(value ushr 8) +} + +fun OutputStream.writeLELong(value: Long) { + writeLEInt(value.toInt()) + writeLEInt((value ushr 32).toInt()) +} + +fun OutputStream.writeLEFloat(value: Float) = writeLEInt(value.toBits()) +fun OutputStream.writeLEDouble(value: Double) = writeLELong(value.toBits()) + +fun InputStream.readLEShort(): Int { + return read() or (read() shl 8) +} + +fun InputStream.readLEInt(): Int { + return read() or (read() shl 8) or (read() shl 16) or (read() shl 24) +} + +fun InputStream.readLELong(): Long { + return readLEInt().toLong() or (readLEInt().toLong() shl 32) +} + +fun InputStream.readLEFloat() = Float.fromBits(readLEInt()) +fun InputStream.readLEDouble() = Double.fromBits(readLELong())