From e6bb3fbb19fa7eaf971d53ad096124ca8a021011 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 11 May 2025 10:06:25 +0200 Subject: [PATCH 01/24] Implement md2 model animation (VAT) #117: restructuring & refactoring --- .../org/demoth/cake/stages/Game3dScreen.kt | 4 +- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 83 ++++++++----------- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 5 ++ .../kotlin/jake2/qcommon/filesystem/md2.kt | 59 +++++++++++-- .../jake2/qcommon/filesystem/Md2ModelTest.kt | 4 +- 5 files changed, 97 insertions(+), 58 deletions(-) diff --git a/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt b/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt index 876d578d..3a11bae6 100644 --- a/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt +++ b/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt @@ -344,13 +344,13 @@ class Game3dScreen : KtxScreen, InputProcessor, ServerMessageProcessor { for (i in startIndex .. MAX_MODELS) { gameConfig[i]?.let { config -> config.value.let { - config.resource = Md2ModelLoader(locator).loadMd2Model(it, skinIndex = 0, frameIndex = 0) + config.resource = Md2ModelLoader(locator).loadStaticMd2Model(it, skinIndex = 0, frameIndex = 0) } } } // temporary: load one fixed player model - playerModel = Md2ModelLoader(locator).loadMd2Model( + playerModel = Md2ModelLoader(locator).loadStaticMd2Model( modelName = playerModelPath, playerSkin = playerSkinPath, skinIndex = 0, diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index b913d5e5..90c81f7e 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -1,14 +1,13 @@ package org.demoth.cake.modelviewer -import com.badlogic.gdx.graphics.GL20 import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES +import com.badlogic.gdx.graphics.Mesh import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.VertexAttribute import com.badlogic.gdx.graphics.VertexAttributes import com.badlogic.gdx.graphics.g3d.Material import com.badlogic.gdx.graphics.g3d.Model import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute -import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder import jake2.qcommon.filesystem.Md2Model import jake2.qcommon.filesystem.PCX @@ -17,7 +16,8 @@ import java.nio.ByteBuffer import java.nio.ByteOrder class Md2ModelLoader(private val locator: ResourceLocator) { - fun loadMd2Model( + + fun loadStaticMd2Model( modelName: String, playerSkin: String? = null, skinIndex: Int = 0, @@ -66,62 +66,51 @@ class Md2ModelLoader(private val locator: ResourceLocator) { playerSkin: String? = null, skinIndex: Int, ): Md2ShaderModel? { - val findModel = locator.findModel(modelName) - ?: return null + val findModel = locator.findModel(modelName) ?: return null val md2Model: Md2Model = readMd2Model(findModel) - val skins = md2Model.skinNames.map { + val embeddedSkins = md2Model.skinNames.map { locator.findSkin(it) } - -/* - val modelBuilder = ModelBuilder() - modelBuilder.begin() -*/ - val modelSkin: ByteArray = if (skins.isNotEmpty()) { - skins[skinIndex] + val modelSkin: ByteArray = if (embeddedSkins.isNotEmpty()) { + embeddedSkins[skinIndex] } else { if (playerSkin != null) { locator.findSkin(playerSkin) } else throw IllegalStateException("No skin found in the model, no player skin provided") } -/* - val meshBuilder = modelBuilder.part( - "part1", - GL_TRIANGLES, - VertexAttributes( - VertexAttribute.Position(), // 3 floats per vertex, unused by the shader but required by libgdx - VertexAttribute.TexCoords(0), // 2 floats per vertex - VertexAttribute(VertexAttributes.Usage.Generic, 1, "a_index") - ), - Material( - TextureAttribute( - TextureAttribute.Diffuse, - Texture(PCXTextureData(fromPCX(PCX(modelSkin)))), - ) - ) + + val diffuse = Texture(PCXTextureData(fromPCX(PCX(modelSkin)))) + + return Md2ShaderModel( + mesh = createMesh(md2Model), + vat = createVat(md2Model) to 0, + diffuse = diffuse to 1, ) -*/ -/* - val frameVertices = md2Model.getFrameVertices(0) - val size = frameVertices.size / 5 // 5 floats per vertex : fixme: not great - meshBuilder.addMesh(frameVertices, (0.. md2Model.getFrameVertices(i) } -*/ - val meshBuilder = MeshBuilder() - meshBuilder.begin( - VertexAttributes( - VertexAttribute(VertexAttributes.Usage.Generic, 1, "a_index"), - VertexAttribute.TexCoords(0) // 2 floats per vertex + } - ), - GL20.GL_TRIANGLES + /** + * The Mesh holds the vertex attributes, which in the VAT scenario are only texture coordinates. + * The indices are implicitly provided and normals are just skipped in this example. + * + */ + private fun createMesh(md2Model: Md2Model): Mesh { + val texCoords = md2Model.getVertexData() + val indices = ShortArray(md2Model.verticesCount) + // todo: calculate indices + val mesh = Mesh( + true, + texCoords.size, + indices.size, + VertexAttribute.TexCoords(1) ) - md2Model.glCommands.forEach { - it.vertices - } - return TODO() + mesh.setVertices(texCoords) + mesh.setIndices(indices) + return mesh + } + + private fun createVat(md2Model: Md2Model): Texture { + TODO() } } diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index 95ec720b..b24c6eb3 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -13,8 +13,11 @@ import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.utils.BufferUtils import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader +import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.modelviewer.CustomTextureData +import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2ShaderModel +import java.io.File import java.nio.FloatBuffer @@ -40,6 +43,8 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { cameraInputController = CameraInputController(camera) Gdx.input.inputProcessor = cameraInputController md2Shader = createShaderProgram() + //val locator = ModelViewerResourceLocator("/home/daniil/GameDev/quake/q2/quake2/baseq2/models/items/adrenal") + //val md2 = Md2ModelLoader(locator).loadAnimatedModel("/home/daniil/GameDev/quake/q2/quake2/baseq2/models/items/adrenal/tris.md2", null, 0)!! // vertex animation texture with all positional data for all vertices and frames val vat = createVatTexture() diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 3911e931..b41cc1aa 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -52,10 +52,31 @@ class Md2Model(buffer: ByteBuffer) { fun getFrameVertices(frame: Int): FloatArray { return glCommands.flatMap { - it.toFloats(frames[frame].points) + it.toVertexAttributes(frames[frame].points) }.toFloatArray() } + /** + * Get the vertex data for all frames as a single array. + * Every array is a set of + */ + fun getVertexData(): List { + // transform each gl command into a list of vertex data (float arrays) for all frames. + // each element of this list represents a single frame, first two elements are s, t, then vertex positions for all frames (x1, y1, z1, x2, y2, z2, ... + // combine vertex data for all frames into a single array + return glCommands.flatMap { glcmd -> + transformGlCmd(glcmd) + } + } + + fun transformGlCmd(glcmd: Md2GlCmd): List { + val result = mutableListOf() + val allFramesPositions = glcmd.toVertexAttributes(frames) + allFramesPositions.map { + listOf(s,t) + } + } + init { // // region: HEADER @@ -162,9 +183,22 @@ data class Md2VertexInfo(val index: Int, val s: Float, val t: Float) { /** * create a vertex buffer part for this particular vertex (x y z s t) */ - fun toFloats(points: List, usePosition: Boolean = true): List { + fun toFloats(points: List, returnTexCoords: Boolean): List { val p = points[index].position // todo: check bounds - return if (usePosition) listOf(p.x, p.y, p.z, s, t) else listOf(s, t) + return if(returnTexCoords) listOf(p.x, p.y, p.z, s, t) else listOf(p.x, p.y, p.z) + } +} +fun List>.transpose(): List> { + // Check if the list is empty or contains empty rows + if (this.isEmpty() || this.any { it.isEmpty() }) return emptyList() + + val rowCount = this.size + val colCount = this[0].size + + return List(colCount) { colIndex -> + List(rowCount) { rowIndex -> + this[rowIndex][colIndex] + } } } @@ -172,14 +206,25 @@ data class Md2GlCmd( val type: Md2GlCmdType, val vertices: List, ) { + + fun toVertexAttributes(frames: List): Pair, List>> { + // list of rows (vertex positions for each frame) + val framesCmdPositions = frames.map { frame -> + toVertexAttributes(frame.points, false) + } + val texCoords = vertices.map { listOf(it.s, it.t) } // not WORK because later we create more vertices that initially in the frame + return texCoords to framesCmdPositions.transpose() + } + /** * Convert indexed vertices into actual vertex buffer data. * * Also convert triangle strips and fans into sets of independent triangles. * It may waste a bit of VRAM, but makes it much easier to draw, * using a single drawElements(GL_TRIANGLES, ...) call. + * */ - fun toFloats(points: List, usePosition: Boolean = true): List { + fun toVertexAttributes(framePositions: List, returnTexCoords: Boolean = true): List { val result = when (type) { Md2GlCmdType.TRIANGLE_STRIP -> { // (0, 1, 2, 3, 4) -> (0, 1, 2), (1, 2, 3), (2, 3, 4) @@ -189,9 +234,9 @@ data class Md2GlCmd( vertices.windowed(3).flatMap { strip -> clockwise = !clockwise if (clockwise) { - strip[0].toFloats(points) + strip[1].toFloats(points) + strip[2].toFloats(points) + strip[0].toFloats(framePositions, returnTexCoords) + strip[1].toFloats(framePositions, returnTexCoords) + strip[2].toFloats(framePositions, returnTexCoords) } else { - strip[2].toFloats(points) + strip[1].toFloats(points) + strip[0].toFloats(points) + strip[2].toFloats(framePositions, returnTexCoords) + strip[1].toFloats(framePositions, returnTexCoords) + strip[0].toFloats(framePositions, returnTexCoords) } } } @@ -199,7 +244,7 @@ data class Md2GlCmd( Md2GlCmdType.TRIANGLE_FAN -> { // (0, 1, 2, 3, 4) -> (0, 1, 2), (0, 2, 3), (0, 3, 4) vertices.drop(1).windowed(2).flatMap { strip -> - strip[1].toFloats(points) + strip[0].toFloats(points) + vertices.first().toFloats(points) + strip[1].toFloats(framePositions, returnTexCoords) + strip[0].toFloats(framePositions, returnTexCoords) + vertices.first().toFloats(framePositions, returnTexCoords) } } } diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelTest.kt index d03da22f..f531c870 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelTest.kt @@ -43,7 +43,7 @@ class Md2ModelTest { Md2VertexInfo(3, 3.4f, 4.4f), ) val command = Md2GlCmd(Md2GlCmdType.TRIANGLE_FAN, vertices) - val actual = command.toFloats(points) + val actual = command.toVertexAttributes(points) val expected = listOf( 0.3f, 1.3f, 2.3f, 3.3f, 4.3f, 0.2f, 1.2f, 2.2f, 3.2f, 4.2f, @@ -71,7 +71,7 @@ class Md2ModelTest { Md2VertexInfo(3, 3.4f, 4.4f), ) val command = Md2GlCmd(Md2GlCmdType.TRIANGLE_STRIP, vertices) - val actual = command.toFloats(points) + val actual = command.toVertexAttributes(points) val expected = listOf( 0.3f, 1.3f, 2.3f, 3.3f, 4.3f, 0.2f, 1.2f, 2.2f, 3.2f, 4.2f, From 807c9d8b48a2c75e1b225bed25e5a0e4d3b6e347 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sat, 7 Jun 2025 21:50:36 +0200 Subject: [PATCH 02/24] Implement md2 model animation (VAT) #117: fix glsl data types --- assets/shaders/vat.glsl | 8 ++++---- .../org/demoth/cake/modelviewer/CustomTextureData.kt | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/assets/shaders/vat.glsl b/assets/shaders/vat.glsl index 52cc1107..d27436ea 100644 --- a/assets/shaders/vat.glsl +++ b/assets/shaders/vat.glsl @@ -6,10 +6,10 @@ uniform mat4 u_worldTrans; // World transformation matrix uniform mat4 u_projViewTrans; // View transformation matrix uniform sampler2D u_vertexAnimationTexture; // Texture containing animated vertex positions (float texture) -uniform float u_textureHeight; // Height of the vertex texture (number of animation frames) -uniform float u_textureWidth; // Width of the vertex texture (number of vertices) -uniform float u_frame1; // Index of the first frame in the animation texture -uniform float u_frame2; // Index of the second frame in the animation texture +uniform int u_textureHeight; // Height of the vertex texture (number of animation frames) +uniform int u_textureWidth; // Width of the vertex texture (number of vertices) +uniform int u_frame1; // Index of the first frame in the animation texture +uniform int u_frame2; // Index of the second frame in the animation texture uniform float u_interpolation; // Interpolation factor between two animation frames (0.0 to 1.0) varying vec2 v_texCoord; diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt index 253a0b65..5b7d42c7 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt @@ -96,14 +96,14 @@ class Md2ShaderModel( val entityTransform: Matrix4 = Matrix4() ): Disposable { - private val textureWidth = vat.first.width.toFloat() - private val textureHeight = vat.first.height.toFloat() + private val textureWidth = vat.first.width + private val textureHeight = vat.first.height fun render(shader: ShaderProgram, cameraTransform: Matrix4) { shader.bind() - shader.setUniformf("u_frame1", frame1.toFloat()) // not like! - shader.setUniformf("u_frame2", frame2.toFloat()) // not like! + shader.setUniformi("u_frame1", frame1) + shader.setUniformi("u_frame2", frame2) shader.setUniformf("u_interpolation", interpolation) shader.setUniformMatrix("u_projViewTrans", cameraTransform); @@ -112,8 +112,8 @@ class Md2ShaderModel( shader.setUniformi("u_vertexAnimationTexture", vat.second) shader.setUniformi("u_diffuseTexture", diffuse.second) - shader.setUniformf("u_textureHeight", textureHeight) // number of frames - shader.setUniformf("u_textureWidth", textureWidth) // number of vertices + shader.setUniformi("u_textureHeight", textureHeight) // number of frames + shader.setUniformi("u_textureWidth", textureWidth) // number of vertices vat.first.bind(vat.second) diffuse.first.bind(diffuse.second) From 37a67fa81f9204786ca0018c928d82b66a4a772e Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sat, 7 Jun 2025 21:52:04 +0200 Subject: [PATCH 03/24] Implement md2 model animation (VAT) #117: wip vertex data restructuring --- .../kotlin/jake2/qcommon/filesystem/md2.kt | 132 ++++++++++++------ .../filesystem/Md2ModelVertexDataTest.kt | 95 +++++++++++++ 2 files changed, 181 insertions(+), 46 deletions(-) create mode 100644 qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index b41cc1aa..35d8a134 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -3,6 +3,7 @@ package jake2.qcommon.filesystem import jake2.qcommon.math.Vector3f import java.lang.Float.intBitsToFloat import java.nio.ByteBuffer +import java.nio.FloatBuffer const val IDALIASHEADER: Int = (('2'.code shl 24) + ('P'.code shl 16) + ('D'.code shl 8) + 'I'.code) const val ALIAS_VERSION: Int = 8 @@ -60,21 +61,23 @@ class Md2Model(buffer: ByteBuffer) { * Get the vertex data for all frames as a single array. * Every array is a set of */ - fun getVertexData(): List { + fun getVertexData(): FloatArray { // transform each gl command into a list of vertex data (float arrays) for all frames. // each element of this list represents a single frame, first two elements are s, t, then vertex positions for all frames (x1, y1, z1, x2, y2, z2, ... // combine vertex data for all frames into a single array - return glCommands.flatMap { glcmd -> - transformGlCmd(glcmd) - } +// return glCommands.flatMap { glcmd -> +// transformGlCmd(glcmd) +// } + TODO() } fun transformGlCmd(glcmd: Md2GlCmd): List { val result = mutableListOf() val allFramesPositions = glcmd.toVertexAttributes(frames) - allFramesPositions.map { - listOf(s,t) - } +// allFramesPositions.map { +// listOf(s,t) +// } + TODO() } init { @@ -121,7 +124,7 @@ class Md2Model(buffer: ByteBuffer) { buffer.position(firstFrameOffset) repeat(framesCount) { // parse frame from the buffer - frames.add(Md2Frame(buffer, verticesCount)) + frames.add(Md2Frame.fromBuffer(buffer, verticesCount)) } // GL COMMANDS @@ -174,6 +177,43 @@ class Md2Model(buffer: ByteBuffer) { } } +fun getVertexData( + glCmds: List, + frames: List +): Md2VertexData { + // First, we need to reindex the vertices. + // In md2 format the vertices are indexed without the texture coordinates (which are part of gl commands). + // GL commands are shared between frames, therefore the uv don't change between frames. + // To make this index, we need to iterate over the gl commands by vertex index, and cache the vertex coordinates. + // If however, the same vertex has a different text coord, we need to make a new vertex, append it to the index, + // and (!most importantly!) reindex the positions in the frames. + + // map from (oldIndex, s,t ) to new index + val vertexMap = mutableMapOf, Int>() + var currentVertex = 0 + glCmds.forEach { glCmd -> + glCmd.vertices.forEach { vertex -> + val existingVertex = vertexMap[Triple(vertex.index, vertex.s, vertex.t)] + if (existingVertex != null) { + + } + } + } + + TODO() +} + +@Suppress("ArrayInDataClass") +data class Md2VertexData( + // indices to draw GL_TRIANGLES + val indices: ShortArray, + // indexed attributes (at the moment - only text coords) + val vertexData: FloatArray, + // vertex positions in a 2d array, should correspond to the indices, used to create VAT (Vertex Animation Texture) + // size is numVertices(width) * numFrames(height) * 3(rgb) + val vertexPositions: FloatBuffer +) + enum class Md2GlCmdType { TRIANGLE_STRIP, TRIANGLE_FAN, @@ -213,7 +253,8 @@ data class Md2GlCmd( toVertexAttributes(frame.points, false) } val texCoords = vertices.map { listOf(it.s, it.t) } // not WORK because later we create more vertices that initially in the frame - return texCoords to framesCmdPositions.transpose() +// return texCoords to framesCmdPositions.transpose() + TODO() } /** @@ -262,45 +303,44 @@ data class Md2GlCmd( * - 16 bytes: name * - number of vertices: * - 4 bytes: packed normal index + x, y, z position + * [name] - frame name from grabbing (size 16) */ -class Md2Frame(buffer: ByteBuffer, vertexCount: Int) { - val points: List - val name: String // frame name from grabbing (size 16) - - init { - val scale = Vector3f( - buffer.getFloat(), - buffer.getFloat(), - buffer.getFloat() - ) - val translate = Vector3f( - buffer.getFloat(), - buffer.getFloat(), - buffer.getFloat() - ) - val nameBuf = ByteArray(16) - buffer.get(nameBuf) - name = String(nameBuf).trim { it < ' '} - - points = ArrayList(vertexCount) - repeat(vertexCount) { - // vertices are all 8 bit, so no swapping needed - // 4 bytes: - // highest - normal index - // x y z - val vertexData = buffer.getInt() - // unpack vertex data - points.add( - Md2Point( - Vector3f( - scale.x * (vertexData ushr 0 and 0xFF), - scale.y * (vertexData ushr 8 and 0xFF), - scale.z * (vertexData ushr 16 and 0xFF) - ) + translate, - vertexData ushr 24 and 0xFF - ) +class Md2Frame(val name: String, val points: List) { + companion object { + fun fromBuffer(buffer: ByteBuffer, vertexCount: Int): Md2Frame { + val scale = Vector3f( + buffer.getFloat(), + buffer.getFloat(), + buffer.getFloat() ) - + val translate = Vector3f( + buffer.getFloat(), + buffer.getFloat(), + buffer.getFloat() + ) + val nameBuf = ByteArray(16) + buffer.get(nameBuf) + val name = String(nameBuf).trim { it < ' ' } + val points: ArrayList = ArrayList(vertexCount) + repeat(vertexCount) { + // vertices are all 8 bit, so no swapping needed + // 4 bytes: + // highest - normal index + // x y z + val vertexData = buffer.getInt() + // unpack vertex data + points.add( + Md2Point( + Vector3f( + scale.x * (vertexData ushr 0 and 0xFF), + scale.y * (vertexData ushr 8 and 0xFF), + scale.z * (vertexData ushr 16 and 0xFF) + ) + translate, + vertexData ushr 24 and 0xFF + ) + ) + } + return Md2Frame(name, points) } } diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt new file mode 100644 index 00000000..1ed3b5cf --- /dev/null +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -0,0 +1,95 @@ +package jake2.qcommon.filesystem + +import jake2.qcommon.math.Vector3f +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Create a simple geometry to test transformation of md2 packed data into shader suitable format. + * In this example there is a sinble square composed of two halfs: + * - bottom (triangle strip, grass texture) + * - top (triangle fan, wood texture). + * The actual texture has the opposite layout: the grass is on the top and the wood is on the bottom. + * This makes an interesting case, because the same vertices (same in a sense of the postitions) + * have different texture coordinates when are part of different triangles. + * + * Initially we have 6 vertices in the md2 format. But since the vertices, + * which are shared by different quads have different texture coordinates, we need to make new vertices instead. + * In our test case, vertex attributes will look like this: + * + * Initial vertex attributes + * index u v shared + * 0 0.0 0.5 + * 1 0.0 1.0 * + * 2 0.0 0.5 + * 3 1.0 0.5 + * 4 1.0 1.0 * + * 5 1.0 0.5 + * + * vertices 1 and 4 are shared and have different uv in different quads, so need to create new ones instead + * + * 6(prev 1)0.0 0.0 * + * 7(prev 4)1.0 0.0 * + */ +class Md2ModelVertexDataTest { + + fun createTestGlCmd(): List = listOf( + // bottom quad - grass + Md2GlCmd( + Md2GlCmdType.TRIANGLE_STRIP, + listOf( + Md2VertexInfo(0, 0.0f, 0.5f), + Md2VertexInfo(5, 1.0f, 0.5f), + Md2VertexInfo(4, 1.0f, 1.0f), + Md2VertexInfo(1, 0.0f, 1.0f), + ) + ), + // top quad - wood + Md2GlCmd( + Md2GlCmdType.TRIANGLE_FAN, + listOf( + Md2VertexInfo(1, 0.0f, 0.0f), + Md2VertexInfo(4, 1.0f, 0.0f), + Md2VertexInfo(3, 1.0f, 0.5f), + Md2VertexInfo(2, 0.0f, 0.5f), + ) + ), + ) + + // animate between square and hex shape + fun createTestFrames(): List = listOf( + Md2Frame( + "square", + listOf( + Md2Point(Vector3f(0.0f, 0.0f, 0.0f), 0), + Md2Point(Vector3f(0.0f, 0.5f, 0.0f), 0), + Md2Point(Vector3f(0.0f, 1f, 0.0f), 0), + Md2Point(Vector3f(1f, 1f, 0.0f), 0), + Md2Point(Vector3f(1f, 0.5f, 0.0f), 0), + Md2Point(Vector3f(1f, 0f, 0.0f), 0), + ) + ), + Md2Frame( + "hex", + listOf( + Md2Point(Vector3f(0.25f, 0.0f, 0.0f), 0), + Md2Point(Vector3f(0.0f, 0.5f, 0.0f), 0), + Md2Point(Vector3f(0.25f, 1f, 0.0f), 0), + Md2Point(Vector3f(0.75f, 1f, 0.0f), 0), + Md2Point(Vector3f(1f, 0.5f, 0.0f), 0), + Md2Point(Vector3f(0.75f, 0f, 0.0f), 0), + ) + ), + ) + + @Test + fun testSimpleVertexData() { + val testGlCmds: List = createTestGlCmd() + val testFrames: List = createTestFrames() + + val actual = getVertexData(testGlCmds, testFrames) + val expected = TODO() + + assertEquals(expected, actual) + } +} From a6c96cd03c9bca9e5c2bcf72ad82f67138afd94e Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 8 Jun 2025 00:53:50 +0200 Subject: [PATCH 04/24] Implement md2 model animation (VAT) #117: wip vertex data restructuring Finally, some working code! Add a test for transformation of vertex attributes (so far it's only tex coords). Transformation of vertex positions and VAT is still TODO --- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 17 ++--- .../kotlin/jake2/qcommon/filesystem/md2.kt | 71 +++++-------------- .../filesystem/Md2ModelVertexDataTest.kt | 65 ++++++++++++++--- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index 90c81f7e..fbb0fed5 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -11,9 +11,11 @@ import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder import jake2.qcommon.filesystem.Md2Model import jake2.qcommon.filesystem.PCX +import jake2.qcommon.filesystem.getVertexData import org.demoth.cake.ResourceLocator import java.nio.ByteBuffer import java.nio.ByteOrder +import java.nio.FloatBuffer class Md2ModelLoader(private val locator: ResourceLocator) { @@ -82,9 +84,10 @@ class Md2ModelLoader(private val locator: ResourceLocator) { val diffuse = Texture(PCXTextureData(fromPCX(PCX(modelSkin)))) + val vertexData = getVertexData(md2Model.glCommands, md2Model.frames) return Md2ShaderModel( - mesh = createMesh(md2Model), - vat = createVat(md2Model) to 0, + mesh = createMesh(vertexData.indices, vertexData.vertexAttributes), + vat = createVat(vertexData.vertexPositions!!) to 0, diffuse = diffuse to 1, ) } @@ -94,22 +97,20 @@ class Md2ModelLoader(private val locator: ResourceLocator) { * The indices are implicitly provided and normals are just skipped in this example. * */ - private fun createMesh(md2Model: Md2Model): Mesh { - val texCoords = md2Model.getVertexData() - val indices = ShortArray(md2Model.verticesCount) + private fun createMesh(indices: ShortArray, vertexAttributes: FloatArray): Mesh { // todo: calculate indices val mesh = Mesh( true, - texCoords.size, + vertexAttributes.size, indices.size, VertexAttribute.TexCoords(1) ) - mesh.setVertices(texCoords) + mesh.setVertices(vertexAttributes) mesh.setIndices(indices) return mesh } - private fun createVat(md2Model: Md2Model): Texture { + private fun createVat(data: FloatArray): Texture { TODO() } } diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 35d8a134..44dda177 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -57,29 +57,6 @@ class Md2Model(buffer: ByteBuffer) { }.toFloatArray() } - /** - * Get the vertex data for all frames as a single array. - * Every array is a set of - */ - fun getVertexData(): FloatArray { - // transform each gl command into a list of vertex data (float arrays) for all frames. - // each element of this list represents a single frame, first two elements are s, t, then vertex positions for all frames (x1, y1, z1, x2, y2, z2, ... - // combine vertex data for all frames into a single array -// return glCommands.flatMap { glcmd -> -// transformGlCmd(glcmd) -// } - TODO() - } - - fun transformGlCmd(glcmd: Md2GlCmd): List { - val result = mutableListOf() - val allFramesPositions = glcmd.toVertexAttributes(frames) -// allFramesPositions.map { -// listOf(s,t) -// } - TODO() - } - init { // // region: HEADER @@ -189,18 +166,29 @@ fun getVertexData( // and (!most importantly!) reindex the positions in the frames. // map from (oldIndex, s,t ) to new index - val vertexMap = mutableMapOf, Int>() + val vertexMap = mutableMapOf() var currentVertex = 0 glCmds.forEach { glCmd -> glCmd.vertices.forEach { vertex -> - val existingVertex = vertexMap[Triple(vertex.index, vertex.s, vertex.t)] - if (existingVertex != null) { - + val key = Md2VertexInfo(vertex.index, vertex.s, vertex.t) + val existingVertex = vertexMap[key] + if (existingVertex == null) { + vertexMap[key] = currentVertex + currentVertex++ } } } - TODO() + val indices = vertexMap.values.sorted() + // map new index -> vertex attributes + val reversedVertexMap = vertexMap.entries.associate { (k, v) -> v to k } + val vertexAttributes = indices.flatMap { listOf(reversedVertexMap[it]!!.s, reversedVertexMap[it]!!.t) } + return Md2VertexData( + indices = indices.map { it.toShort() }.toShortArray(), + vertexAttributes = vertexAttributes.toFloatArray(), + vertexPositions = null // todo + ) + } @Suppress("ArrayInDataClass") @@ -208,10 +196,10 @@ data class Md2VertexData( // indices to draw GL_TRIANGLES val indices: ShortArray, // indexed attributes (at the moment - only text coords) - val vertexData: FloatArray, + val vertexAttributes: FloatArray, // vertex positions in a 2d array, should correspond to the indices, used to create VAT (Vertex Animation Texture) // size is numVertices(width) * numFrames(height) * 3(rgb) - val vertexPositions: FloatBuffer + val vertexPositions: FloatArray? ) enum class Md2GlCmdType { @@ -228,35 +216,12 @@ data class Md2VertexInfo(val index: Int, val s: Float, val t: Float) { return if(returnTexCoords) listOf(p.x, p.y, p.z, s, t) else listOf(p.x, p.y, p.z) } } -fun List>.transpose(): List> { - // Check if the list is empty or contains empty rows - if (this.isEmpty() || this.any { it.isEmpty() }) return emptyList() - - val rowCount = this.size - val colCount = this[0].size - - return List(colCount) { colIndex -> - List(rowCount) { rowIndex -> - this[rowIndex][colIndex] - } - } -} data class Md2GlCmd( val type: Md2GlCmdType, val vertices: List, ) { - fun toVertexAttributes(frames: List): Pair, List>> { - // list of rows (vertex positions for each frame) - val framesCmdPositions = frames.map { frame -> - toVertexAttributes(frame.points, false) - } - val texCoords = vertices.map { listOf(it.s, it.t) } // not WORK because later we create more vertices that initially in the frame -// return texCoords to framesCmdPositions.transpose() - TODO() - } - /** * Convert indexed vertices into actual vertex buffer data. * diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt index 1ed3b5cf..49f0cc83 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -1,18 +1,41 @@ package jake2.qcommon.filesystem import jake2.qcommon.math.Vector3f -import org.junit.Assert.assertEquals +import org.junit.Assert.assertArrayEquals import org.junit.Test /** - * Create a simple geometry to test transformation of md2 packed data into shader suitable format. - * In this example there is a sinble square composed of two halfs: + * Create a simple geometry to test transformation of md2 packed data into shader-suitable format. + * In this example there is a single square composed of two halves: * - bottom (triangle strip, grass texture) * - top (triangle fan, wood texture). - * The actual texture has the opposite layout: the grass is on the top and the wood is on the bottom. - * This makes an interesting case, because the same vertices (same in a sense of the postitions) + * The actual texture has the opposite layout: the grass is on the top, and the wood is on the bottom. + * This makes an interesting case because the same vertices (same in the sense of the positions) * have different texture coordinates when are part of different triangles. * + * + * This is how I image the model (vertex indices are inside, vertex positions are outside) + * 0,1 1,1 + * ┌──────────────────────┐ + * │2 3│ + * │ wood texture │ + * Y │1 4│ + * 0,0.5┼──────────────────────┼1,0.5 + * │ grass texture │ + * │0 5│ + * └──────────────────────┘ + * 0,0 X 1,0 + * + * This is how the texture image looks + * 0,1┌──────────┐1,1 + * │ grass │ + * │ texture │ + * 0,0.5┼──────────┼1,0.5 + * Y │ wood │ + * │ texture │ + * └──────────┘ + * 0,0 X 1,0 + * * Initially we have 6 vertices in the md2 format. But since the vertices, * which are shared by different quads have different texture coordinates, we need to make new vertices instead. * In our test case, vertex attributes will look like this: @@ -26,10 +49,22 @@ import org.junit.Test * 4 1.0 1.0 * * 5 1.0 0.5 * - * vertices 1 and 4 are shared and have different uv in different quads, so need to create new ones instead + * Vertices 1 and 4 are shared and have different uv in different quads, so need to create new ones instead. + * + * 6(prev 1*)0.0 0.0 * + * 7(prev 4*)1.0 0.0 * * - * 6(prev 1)0.0 0.0 * - * 7(prev 4)1.0 0.0 * + * Vertices are re-indexed as they are used in the gl commands; we throw away previous indexing. + * Re-indexed vertices will look like: + * new index, old index, u v + * 0 0 0.0 0.5 + * 1 5 1.0 0.5 + * 2 4 1.0 1.0 + * 3 1 0.0 1.0 + * 4 1* 0.0 0.0 + * 5 4* 1.0 0.0 + * 6 3 1.0 0.5 + * 7 2 0.0 0.5 */ class Md2ModelVertexDataTest { @@ -88,8 +123,18 @@ class Md2ModelVertexDataTest { val testFrames: List = createTestFrames() val actual = getVertexData(testGlCmds, testFrames) - val expected = TODO() - assertEquals(expected, actual) + // these values are taken from the example in the Javadoc (very end) + val expectedVertexAttributes = floatArrayOf( + 0.0f, 0.5f, + 1.0f, 0.5f, + 1.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + 1.0f, 0.5f, + 0.0f, 0.5f, + ) + assertArrayEquals(expectedVertexAttributes, actual.vertexAttributes, 0.0001f) } } From 4bdbe8f35727671725f9601713685eb1cf3d360e Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 8 Jun 2025 23:24:44 +0200 Subject: [PATCH 05/24] Implement md2 model animation (VAT) #117: vertex data ready Implemented the vertex positional data conversion from md2 format. Turned out to be easier than expected - instead of reindexing all the vertices in the frames positions, the index in the VAT is passed as the vertex attribute (along with the texture coordinates). Not only this helps to avoid duplication of data, it also makes the code simpler. Added a test. --- .../kotlin/jake2/qcommon/filesystem/md2.kt | 20 +++++++++++++-- .../filesystem/Md2ModelVertexDataTest.kt | 25 ++++++++++++------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 44dda177..41f43db9 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -182,11 +182,27 @@ fun getVertexData( val indices = vertexMap.values.sorted() // map new index -> vertex attributes val reversedVertexMap = vertexMap.entries.associate { (k, v) -> v to k } - val vertexAttributes = indices.flatMap { listOf(reversedVertexMap[it]!!.s, reversedVertexMap[it]!!.t) } + val vertexAttributes = indices.flatMap { listOf( + reversedVertexMap[it]!!.index.toFloat(), + reversedVertexMap[it]!!.s, + reversedVertexMap[it]!!.t + ) } + + val vertexPositions = mutableListOf() + // todo: check the order of rows/columns + frames.forEach { frame -> + frame.points.forEach { point -> + vertexPositions.add(point.position.x) + vertexPositions.add(point.position.y) + vertexPositions.add(point.position.z) + // normal is unused so far + } + } + return Md2VertexData( indices = indices.map { it.toShort() }.toShortArray(), vertexAttributes = vertexAttributes.toFloatArray(), - vertexPositions = null // todo + vertexPositions = vertexPositions.toFloatArray() ) } diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt index 49f0cc83..5b2daf22 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -1,6 +1,7 @@ package jake2.qcommon.filesystem import jake2.qcommon.math.Vector3f +import junit.framework.TestCase.assertEquals import org.junit.Assert.assertArrayEquals import org.junit.Test @@ -54,7 +55,7 @@ import org.junit.Test * 6(prev 1*)0.0 0.0 * * 7(prev 4*)1.0 0.0 * * - * Vertices are re-indexed as they are used in the gl commands; we throw away previous indexing. + * Vertices are re-indexed as they are used in the gl commands. * Re-indexed vertices will look like: * new index, old index, u v * 0 0 0.0 0.5 @@ -65,6 +66,9 @@ import org.junit.Test * 5 4* 1.0 0.0 * 6 3 1.0 0.5 * 7 2 0.0 0.5 + * + * New index is just going incrementally (we always draw all vertices in a model), + * old index is used to locate the vertex position in the frame data (and VAT). */ class Md2ModelVertexDataTest { @@ -126,15 +130,18 @@ class Md2ModelVertexDataTest { // these values are taken from the example in the Javadoc (very end) val expectedVertexAttributes = floatArrayOf( - 0.0f, 0.5f, - 1.0f, 0.5f, - 1.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 0.0f, - 1.0f, 0.0f, - 1.0f, 0.5f, - 0.0f, 0.5f, + 0.0f, 0.0f, 0.5f, + 5.0f, 1.0f, 0.5f, + 4.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, + 4.0f, 1.0f, 0.0f, + 3.0f, 1.0f, 0.5f, + 2.0f, 0.0f, 0.5f, ) assertArrayEquals(expectedVertexAttributes, actual.vertexAttributes, 0.0001f) + + // todo: add proper test for texture coordinates + assertEquals(testFrames.size * testFrames.first().points.size * 3, actual.vertexPositions?.size) } } From 2baac708728c596b5578b6be5edddb83b175b3a5 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 8 Jun 2025 23:56:03 +0200 Subject: [PATCH 06/24] Implement md2 model animation (VAT) #117: md2 loader prepare md2 loader to load VAT --- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 45 ++++++++++++++----- .../kotlin/jake2/qcommon/filesystem/md2.kt | 14 +++--- .../filesystem/Md2ModelVertexDataTest.kt | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index fbb0fed5..0731d662 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -1,6 +1,8 @@ package org.demoth.cake.modelviewer +import com.badlogic.gdx.graphics.GL20 import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES +import com.badlogic.gdx.graphics.GL30 import com.badlogic.gdx.graphics.Mesh import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.VertexAttribute @@ -10,8 +12,9 @@ import com.badlogic.gdx.graphics.g3d.Model import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder import jake2.qcommon.filesystem.Md2Model +import jake2.qcommon.filesystem.Md2VertexData import jake2.qcommon.filesystem.PCX -import jake2.qcommon.filesystem.getVertexData +import jake2.qcommon.filesystem.buildVertexData import org.demoth.cake.ResourceLocator import java.nio.ByteBuffer import java.nio.ByteOrder @@ -84,10 +87,10 @@ class Md2ModelLoader(private val locator: ResourceLocator) { val diffuse = Texture(PCXTextureData(fromPCX(PCX(modelSkin)))) - val vertexData = getVertexData(md2Model.glCommands, md2Model.frames) + val vertexData = buildVertexData(md2Model.glCommands, md2Model.frames) return Md2ShaderModel( - mesh = createMesh(vertexData.indices, vertexData.vertexAttributes), - vat = createVat(vertexData.vertexPositions!!) to 0, + mesh = createMesh(vertexData), + vat = createVat(vertexData) to 0, diffuse = diffuse to 1, ) } @@ -97,24 +100,42 @@ class Md2ModelLoader(private val locator: ResourceLocator) { * The indices are implicitly provided and normals are just skipped in this example. * */ - private fun createMesh(indices: ShortArray, vertexAttributes: FloatArray): Mesh { - // todo: calculate indices + private fun createMesh(vertexData: Md2VertexData): Mesh { val mesh = Mesh( true, - vertexAttributes.size, - indices.size, + vertexData.vertexAttributes.size, + vertexData.indices.size, VertexAttribute.TexCoords(1) ) - mesh.setVertices(vertexAttributes) - mesh.setIndices(indices) + mesh.setVertices(vertexData.vertexAttributes) + mesh.setIndices(vertexData.indices) return mesh } - private fun createVat(data: FloatArray): Texture { - TODO() + private fun createVat(vertexData: Md2VertexData): Texture { + return Texture( + CustomTextureData( + vertexData.frames, + vertexData.vertices, + GL30.GL_RGB16F, + GL30.GL_RGB, + GL20.GL_FLOAT, + vertexData.vertexPositions.toFloatBuffer(), + ) + ) } } +private fun FloatArray.toFloatBuffer(): FloatBuffer { + val result = ByteBuffer + .allocateDirect(size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer() + result.put(this) + result.flip() + return result +} + private fun readMd2Model(modelData: ByteArray): Md2Model { val byteBuffer = ByteBuffer .wrap(modelData) diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 41f43db9..e2c95aff 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -3,7 +3,6 @@ package jake2.qcommon.filesystem import jake2.qcommon.math.Vector3f import java.lang.Float.intBitsToFloat import java.nio.ByteBuffer -import java.nio.FloatBuffer const val IDALIASHEADER: Int = (('2'.code shl 24) + ('P'.code shl 16) + ('D'.code shl 8) + 'I'.code) const val ALIAS_VERSION: Int = 8 @@ -154,7 +153,7 @@ class Md2Model(buffer: ByteBuffer) { } } -fun getVertexData( +fun buildVertexData( glCmds: List, frames: List ): Md2VertexData { @@ -163,7 +162,6 @@ fun getVertexData( // GL commands are shared between frames, therefore the uv don't change between frames. // To make this index, we need to iterate over the gl commands by vertex index, and cache the vertex coordinates. // If however, the same vertex has a different text coord, we need to make a new vertex, append it to the index, - // and (!most importantly!) reindex the positions in the frames. // map from (oldIndex, s,t ) to new index val vertexMap = mutableMapOf() @@ -188,8 +186,8 @@ fun getVertexData( reversedVertexMap[it]!!.t ) } + // flatten vertex positions in all frames val vertexPositions = mutableListOf() - // todo: check the order of rows/columns frames.forEach { frame -> frame.points.forEach { point -> vertexPositions.add(point.position.x) @@ -202,7 +200,9 @@ fun getVertexData( return Md2VertexData( indices = indices.map { it.toShort() }.toShortArray(), vertexAttributes = vertexAttributes.toFloatArray(), - vertexPositions = vertexPositions.toFloatArray() + vertexPositions = vertexPositions.toFloatArray(), + frames = frames.size, + vertices = frames.first().points.size, ) } @@ -215,7 +215,9 @@ data class Md2VertexData( val vertexAttributes: FloatArray, // vertex positions in a 2d array, should correspond to the indices, used to create VAT (Vertex Animation Texture) // size is numVertices(width) * numFrames(height) * 3(rgb) - val vertexPositions: FloatArray? + val vertexPositions: FloatArray, + val frames: Int, + val vertices: Int, ) enum class Md2GlCmdType { diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt index 5b2daf22..719793b5 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -126,7 +126,7 @@ class Md2ModelVertexDataTest { val testGlCmds: List = createTestGlCmd() val testFrames: List = createTestFrames() - val actual = getVertexData(testGlCmds, testFrames) + val actual = buildVertexData(testGlCmds, testFrames) // these values are taken from the example in the Javadoc (very end) val expectedVertexAttributes = floatArrayOf( From 0751c3b981670917e2edb9adb4691e4dbf7ace63 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Mon, 9 Jun 2025 00:12:32 +0200 Subject: [PATCH 07/24] Implement md2 model animation (VAT) #117: vat animation fix shader to accommodate for the vertex index in the VAT (passed instead of the built-in gl_VertexID) --- assets/shaders/vat.glsl | 10 +++++----- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 20 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/assets/shaders/vat.glsl b/assets/shaders/vat.glsl index d27436ea..1d9af2e2 100644 --- a/assets/shaders/vat.glsl +++ b/assets/shaders/vat.glsl @@ -1,6 +1,7 @@ -#version 130 // required by gl_VertexID +#version 130 -attribute vec2 a_texCoord1; // Texture coordinates +attribute float a_vat_index; +attribute vec2 a_texCoord1; // Diffuse Texture coordinates uniform mat4 u_worldTrans; // World transformation matrix uniform mat4 u_projViewTrans; // View transformation matrix @@ -15,10 +16,9 @@ uniform float u_interpolation; // Interpolation factor between two animation fra varying vec2 v_texCoord; void main() { - float index = float(gl_VertexID); vec2 texelSize = vec2(1.0 / u_textureWidth, 1.0 / u_textureHeight); - vec2 vertexTextureCoord1 = vec2((index + 0.5) * texelSize.x, (u_frame1 + 0.5) * texelSize.y); - vec2 vertexTextureCoord2 = vec2((index + 0.5) * texelSize.x, (u_frame2 + 0.5) * texelSize.y); + vec2 vertexTextureCoord1 = vec2((a_vat_index + 0.5) * texelSize.x, (u_frame1 + 0.5) * texelSize.y); + vec2 vertexTextureCoord2 = vec2((a_vat_index + 0.5) * texelSize.x, (u_frame2 + 0.5) * texelSize.y); // Sample the vertex texture to get the animated positions for the two frames // The texture stores vec3 positions in RGB channels (assuming float texture) diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index b24c6eb3..b1b749ca 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -1,23 +1,18 @@ package org.demoth.cake.lwjgl3 import com.badlogic.gdx.ApplicationAdapter -import com.badlogic.gdx.Game import com.badlogic.gdx.Gdx import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.g3d.utils.CameraInputController import com.badlogic.gdx.graphics.glutils.ShaderProgram -import com.badlogic.gdx.math.Matrix4 import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.utils.BufferUtils import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader -import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.modelviewer.CustomTextureData -import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2ShaderModel -import java.io.File import java.nio.FloatBuffer @@ -97,17 +92,20 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { true, numberOfVertices, iCount, - VertexAttribute.TexCoords(1) // in future, normals can also be added here + VertexAttributes( + VertexAttribute(VertexAttributes.Usage.Generic, 1, "a_vat_index"), + VertexAttribute.TexCoords(1) // in future, normals can also be added here + ) ) // v (vertical) component is flipped - val textureCoords = floatArrayOf( - 0.0f, 1.0f, // bottom left - 1.0f, 1.0f, // bottom right - 0.5f, 0.0f, // top + val attributes = floatArrayOf( + 0.0f, 0.0f, 1.0f, // bottom left + 1.0f, 1.0f, 1.0f, // bottom right + 2.0f, 0.5f, 0.0f, // top ) - mesh.setVertices(textureCoords) + mesh.setVertices(attributes) mesh.setIndices(indices) return mesh } From fe3503c3270c88bfdc80942ffc617c0a472da255 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 09:13:41 +0200 Subject: [PATCH 08/24] Implement md2 model animation (VAT) #117: vat animation fix missed vertex attribute in the md2 loader Finally, something showes up but clearly it is not correct --- .../kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt | 5 ++++- qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index 0731d662..d0081582 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -105,7 +105,10 @@ class Md2ModelLoader(private val locator: ResourceLocator) { true, vertexData.vertexAttributes.size, vertexData.indices.size, - VertexAttribute.TexCoords(1) + VertexAttributes( + VertexAttribute(VertexAttributes.Usage.Generic, 1, "a_vat_index"), + VertexAttribute.TexCoords(1) // in future, normals can also be added here + ) ) mesh.setVertices(vertexData.vertexAttributes) mesh.setIndices(vertexData.indices) diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index e2c95aff..b6b05f4e 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -202,7 +202,7 @@ fun buildVertexData( vertexAttributes = vertexAttributes.toFloatArray(), vertexPositions = vertexPositions.toFloatArray(), frames = frames.size, - vertices = frames.first().points.size, + vertices = frames.first().points.size, // assuming all frames have the same number of vertices ) } From 7f4e32469b1c769388b4ea686079eadf07e7d370 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 21:45:33 +0200 Subject: [PATCH 09/24] Implement md2 model animation (VAT) #117: vat animation shameful bugfix: incorrect order of width / height --- .../main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index d0081582..07559d49 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -118,8 +118,8 @@ class Md2ModelLoader(private val locator: ResourceLocator) { private fun createVat(vertexData: Md2VertexData): Texture { return Texture( CustomTextureData( - vertexData.frames, vertexData.vertices, + vertexData.frames, GL30.GL_RGB16F, GL30.GL_RGB, GL20.GL_FLOAT, From 08b3f77aad27c343c3cb85450113ebee38bfaa51 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 21:47:06 +0200 Subject: [PATCH 10/24] Implement md2 model animation (VAT) #117: vat animation completely forgot to unpack triangle strips and fans into independent triangles; fixed --- .../kotlin/jake2/qcommon/filesystem/md2.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index b6b05f4e..1bc3a2a2 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -240,6 +240,37 @@ data class Md2GlCmd( val vertices: List, ) { + /** + * Convert triangle strip and triangle fan into a list of independent triangles + */ + fun unpack(): List { + val result = when (type) { + Md2GlCmdType.TRIANGLE_STRIP -> { + // (0, 1, 2, 3, 4) -> (0, 1, 2), (1, 2, 3), (2, 3, 4) + // when converting a triangle strip into a set of separate triangles, + // need to alternate the winding direction + var clockwise = true + vertices.windowed(3).flatMap { strip -> + clockwise = !clockwise + if (clockwise) { + listOf(strip[0], strip[1], strip[2]) + } else { + listOf(strip[2], strip[1], strip[0]) + } + } + } + + Md2GlCmdType.TRIANGLE_FAN -> { + // (0, 1, 2, 3, 4) -> (0, 1, 2), (0, 2, 3), (0, 3, 4) + vertices.drop(1).windowed(2).flatMap { strip -> + listOf(strip[1], strip[0], vertices.first()) + } + + } + } + return result + } + /** * Convert indexed vertices into actual vertex buffer data. * From 83a641565482bbad388689cd7f34084a0bab4f55 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 21:49:37 +0200 Subject: [PATCH 11/24] Implement md2 model animation (VAT) #117: vat animation my previous idea about reindexing was flawed - after unpacking triangle strips and fans into the independent triangles we pass all resulting indices in the ascending order to the shader --- .../kotlin/jake2/qcommon/filesystem/md2.kt | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 1bc3a2a2..07793290 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -164,28 +164,12 @@ fun buildVertexData( // If however, the same vertex has a different text coord, we need to make a new vertex, append it to the index, // map from (oldIndex, s,t ) to new index - val vertexMap = mutableMapOf() - var currentVertex = 0 - glCmds.forEach { glCmd -> - glCmd.vertices.forEach { vertex -> - val key = Md2VertexInfo(vertex.index, vertex.s, vertex.t) - val existingVertex = vertexMap[key] - if (existingVertex == null) { - vertexMap[key] = currentVertex - currentVertex++ - } + val attributes = glCmds.flatMap { glCmd -> + glCmd.unpack().flatMap { vertex -> + listOf(vertex.index.toFloat(), vertex.s, vertex.t) } } - val indices = vertexMap.values.sorted() - // map new index -> vertex attributes - val reversedVertexMap = vertexMap.entries.associate { (k, v) -> v to k } - val vertexAttributes = indices.flatMap { listOf( - reversedVertexMap[it]!!.index.toFloat(), - reversedVertexMap[it]!!.s, - reversedVertexMap[it]!!.t - ) } - // flatten vertex positions in all frames val vertexPositions = mutableListOf() frames.forEach { frame -> @@ -198,8 +182,8 @@ fun buildVertexData( } return Md2VertexData( - indices = indices.map { it.toShort() }.toShortArray(), - vertexAttributes = vertexAttributes.toFloatArray(), + indices = attributes.indices.map { it.toShort() }.toShortArray(), + vertexAttributes = attributes.toFloatArray(), vertexPositions = vertexPositions.toFloatArray(), frames = frames.size, vertices = frames.first().points.size, // assuming all frames have the same number of vertices From 17c2da77ce5319acd6d01c48680a1632463061b2 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 21:53:22 +0200 Subject: [PATCH 12/24] Implement md2 model animation (VAT) #117: enable depth testing & culling --- .../src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index b1b749ca..00f631c2 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -34,6 +34,11 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { private lateinit var md2ShaderModel: Md2ShaderModel override fun create() { + Gdx.gl.glEnable(GL20.GL_DEPTH_TEST) + Gdx.gl.glDepthFunc(GL20.GL_LEQUAL) + Gdx.gl.glEnable(GL20.GL_CULL_FACE) + Gdx.gl.glCullFace(GL20.GL_BACK) + camera = PerspectiveCamera(67f, width.toFloat(), height.toFloat()) cameraInputController = CameraInputController(camera) Gdx.input.inputProcessor = cameraInputController From 2f760b50bd31ad5e17d708c72fb884da78d5444d Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 21:53:51 +0200 Subject: [PATCH 13/24] Implement md2 model animation (VAT) #117: animated model!!! finally smooth md2 animation! --- .../cake/modelviewer/CustomTextureData.kt | 3 +- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 149 +++--------------- .../filesystem/Md2ModelVertexDataTest.kt | 2 +- 3 files changed, 27 insertions(+), 127 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt index 5b7d42c7..f334a4b1 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt @@ -1,7 +1,6 @@ package org.demoth.cake.modelviewer import com.badlogic.gdx.Gdx -import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.GL20 import com.badlogic.gdx.graphics.Mesh import com.badlogic.gdx.graphics.Pixmap @@ -96,6 +95,8 @@ class Md2ShaderModel( val entityTransform: Matrix4 = Matrix4() ): Disposable { + val frames = vat.first.height + private val textureWidth = vat.first.width private val textureHeight = vat.first.height diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index 00f631c2..f44a7f4a 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -2,18 +2,17 @@ package org.demoth.cake.lwjgl3 import com.badlogic.gdx.ApplicationAdapter import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.g3d.utils.CameraInputController import com.badlogic.gdx.graphics.glutils.ShaderProgram -import com.badlogic.gdx.math.Vector3 -import com.badlogic.gdx.utils.BufferUtils import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader -import org.demoth.cake.modelviewer.CustomTextureData +import org.demoth.cake.ModelViewerResourceLocator +import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2ShaderModel -import java.nio.FloatBuffer class Md2ShaderTest : ApplicationAdapter(), Disposable { @@ -21,16 +20,11 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { private lateinit var md2Shader: ShaderProgram private var animationTime = 0f - // --- Model Data (Replace with your actual loaded data) --- - private val numberOfVertices = 3 // Example: 100 vertices - private val numberOfFrames = 2 // Example: 60 animation frames - private val animationDuration = 2.0f // Example: 2 seconds animation duration + private val animationDuration = 1f // Example: 2 seconds animation duration private lateinit var camera: Camera private lateinit var cameraInputController : CameraInputController - private var direction = 1f - private lateinit var md2ShaderModel: Md2ShaderModel override fun create() { @@ -40,24 +34,19 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { Gdx.gl.glCullFace(GL20.GL_BACK) camera = PerspectiveCamera(67f, width.toFloat(), height.toFloat()) + camera.near = 0.1f + camera.far = 1000f cameraInputController = CameraInputController(camera) Gdx.input.inputProcessor = cameraInputController - md2Shader = createShaderProgram() - //val locator = ModelViewerResourceLocator("/home/daniil/GameDev/quake/q2/quake2/baseq2/models/items/adrenal") - //val md2 = Md2ModelLoader(locator).loadAnimatedModel("/home/daniil/GameDev/quake/q2/quake2/baseq2/models/items/adrenal/tris.md2", null, 0)!! - - // vertex animation texture with all positional data for all vertices and frames - val vat = createVatTexture() - - val diffuse = Texture(Gdx.files.internal("triangloid.png")) - val mesh = createMesh() - - md2ShaderModel = Md2ShaderModel( - mesh, - vat to 0, - diffuse to 1 - ) + md2Shader = createShaderProgram() + val pathToFile = "berserk" + val locator = ModelViewerResourceLocator(pathToFile) + val md2 = Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)?.apply { + frame1 = 0 + frame2 = if (frames > 1) 1 else 0 + } + md2ShaderModel = md2!! } private fun createShaderProgram(): ShaderProgram { @@ -74,97 +63,6 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { return shaderProgram } - /** - * The Mesh holds the vertex attributes, which in the VAT scenario are only texture coordinates. - * The indices are implicitly provided and normals are just skipped in this example. - * - */ - private fun createMesh(): Mesh { - - val iCount = (numberOfVertices - 2) * 3 // one triangle fan as in the sample - - val indices = ShortArray(iCount) - - /* fill indices ----------------------------------------------------------- */ - var p = 0 - for (v in 0..?>(numberOfVertices) { - arrayOfNulls(numberOfFrames) - } - - val s = 0.5f // half size of the triangle - // sample vertex data: 2 frame - triangle with 3 vertices - vertexData[0]!![0] = Vector3(-s, -s, 0f) - vertexData[1]!![0] = Vector3(s, -s, 0f) - vertexData[2]!![0] = Vector3(s, s, 0f) - - // 1 frame - mirrored triangle - vertexData[0]!![1] = Vector3(-s, -s, 0f) - vertexData[1]!![1] = Vector3(s, -s, 0f) - vertexData[2]!![1] = Vector3(-s, s, 0f) - - // Create the VAT Texture buffer as a linear array - val vertexBuffer: FloatBuffer = - BufferUtils.newFloatBuffer(numberOfVertices * numberOfFrames * 3) // 3 floats per Vector3 - for (frameIndex in 0.. animationDuration) { - animationTime = animationDuration - direction = -1f - } - animationTime += Gdx.graphics.deltaTime * direction val interpolation = animationTime / animationDuration md2ShaderModel.interpolation = interpolation md2ShaderModel.render(md2Shader, camera.combined) + + animationTime += Gdx.graphics.deltaTime + + if (animationTime > animationDuration) { + animationTime = 0f + // advance animation frames: frame1++ frame2++, keep in mind number of frames + md2ShaderModel.frame1 = md2ShaderModel.frame2 + val nextFrame = (md2ShaderModel.frame2 + 1) % md2ShaderModel.frames + md2ShaderModel.frame2 = nextFrame + } } override fun dispose() { diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt index 719793b5..987a41da 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -142,6 +142,6 @@ class Md2ModelVertexDataTest { assertArrayEquals(expectedVertexAttributes, actual.vertexAttributes, 0.0001f) // todo: add proper test for texture coordinates - assertEquals(testFrames.size * testFrames.first().points.size * 3, actual.vertexPositions?.size) + assertEquals(testFrames.size * testFrames.first().points.size * 3, actual.vertexPositions.size) } } From d81860d52adc943a6df03a58c3f9b4fa3f8cec7e Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 22:15:53 +0200 Subject: [PATCH 14/24] feat: shader test - model viewer controls and camera fixes --- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index f44a7f4a..35ae3663 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -11,6 +11,7 @@ import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader import org.demoth.cake.ModelViewerResourceLocator +import org.demoth.cake.clientcommon.FlyingCameraController import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2ShaderModel @@ -20,12 +21,13 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { private lateinit var md2Shader: ShaderProgram private var animationTime = 0f - private val animationDuration = 1f // Example: 2 seconds animation duration + private val animationDuration = 0.1f // Example: 2 seconds animation duration private lateinit var camera: Camera private lateinit var cameraInputController : CameraInputController private lateinit var md2ShaderModel: Md2ShaderModel + private var playing = false override fun create() { Gdx.gl.glEnable(GL20.GL_DEPTH_TEST) @@ -33,10 +35,16 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { Gdx.gl.glEnable(GL20.GL_CULL_FACE) Gdx.gl.glCullFace(GL20.GL_BACK) - camera = PerspectiveCamera(67f, width.toFloat(), height.toFloat()) - camera.near = 0.1f - camera.far = 1000f - cameraInputController = CameraInputController(camera) + + // copied from model viewer + camera = PerspectiveCamera(90f, Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat()) + camera.position.set(0f, 0f, 0f); + camera.near = 1f + camera.far = 4096f + camera.up.set(0f, 0f, 1f) // make z up + camera.direction.set(0f, 1f, 0f) // make y forward + + cameraInputController = FlyingCameraController(camera) Gdx.input.inputProcessor = cameraInputController md2Shader = createShaderProgram() @@ -64,6 +72,12 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { } override fun render() { + if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { + playing = !playing + } else if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { + Gdx.app.exit() + } + camera.update() // Clear the screen @@ -75,14 +89,16 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { md2ShaderModel.interpolation = interpolation md2ShaderModel.render(md2Shader, camera.combined) - animationTime += Gdx.graphics.deltaTime + if (playing) { + animationTime += Gdx.graphics.deltaTime - if (animationTime > animationDuration) { - animationTime = 0f - // advance animation frames: frame1++ frame2++, keep in mind number of frames - md2ShaderModel.frame1 = md2ShaderModel.frame2 - val nextFrame = (md2ShaderModel.frame2 + 1) % md2ShaderModel.frames - md2ShaderModel.frame2 = nextFrame + if (animationTime > animationDuration) { + animationTime = 0f + // advance animation frames: frame1++ frame2++, keep in mind number of frames + md2ShaderModel.frame1 = md2ShaderModel.frame2 + val nextFrame = (md2ShaderModel.frame2 + 1) % md2ShaderModel.frames + md2ShaderModel.frame2 = nextFrame + } } } @@ -97,8 +113,9 @@ private const val height = 768 fun main() { val config = Lwjgl3ApplicationConfiguration() - config.setResizable(true) - config.setWindowedMode(width, height) + config.setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode()) +// config.setResizable(true) +// config.setWindowedMode(width, height) // fixme: didn't really quite get why it has to be explicitly loaded, // otherwise PerspectiveCamera(..) raises UnsatisfiedLinkError From 745668ec8e061fc8d89a74c31b9af53feaf8f9d9 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 22:26:41 +0200 Subject: [PATCH 15/24] feat: shader test - add animation controls and frame stepping --- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index 35ae3663..f0e8bae3 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -48,7 +48,7 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { Gdx.input.inputProcessor = cameraInputController md2Shader = createShaderProgram() - val pathToFile = "berserk" + val pathToFile = "/home/daniil/.steam/steam/steamapps/common/Quake 2/baseq2/models/monsters/infantry" val locator = ModelViewerResourceLocator(pathToFile) val md2 = Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)?.apply { frame1 = 0 @@ -76,6 +76,10 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { playing = !playing } else if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { Gdx.app.exit() + } else if (Gdx.input.isKeyJustPressed(Input.Keys.RIGHT)) { + changeFrame(1) + } else if (Gdx.input.isKeyJustPressed(Input.Keys.LEFT)) { + changeFrame(-1) } camera.update() @@ -93,15 +97,24 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { animationTime += Gdx.graphics.deltaTime if (animationTime > animationDuration) { - animationTime = 0f - // advance animation frames: frame1++ frame2++, keep in mind number of frames - md2ShaderModel.frame1 = md2ShaderModel.frame2 - val nextFrame = (md2ShaderModel.frame2 + 1) % md2ShaderModel.frames - md2ShaderModel.frame2 = nextFrame + changeFrame(1) } } } + private fun changeFrame(delta: Int) { + animationTime = 0f + // advance animation frames: frame1++ frame2++, keep in mind number of frames + md2ShaderModel.frame1 = (md2ShaderModel.frame1 + delta) % md2ShaderModel.frames + if (md2ShaderModel.frame1 < 0) { + md2ShaderModel.frame1 += md2ShaderModel.frames + } + if (md2ShaderModel.frame2 < 0) { + md2ShaderModel.frame2 += md2ShaderModel.frames + } + md2ShaderModel.frame2 = (md2ShaderModel.frame2 + delta) % md2ShaderModel.frames + } + override fun dispose() { md2Shader.dispose() md2ShaderModel.dispose() From 008a0dcd4792394d52651ee8cc1ffbf15d9c4a1d Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Tue, 10 Jun 2025 23:30:55 +0200 Subject: [PATCH 16/24] chore: fix md2 vertex data test: unpack vertices --- .../qcommon/filesystem/Md2ModelVertexDataTest.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt index 987a41da..01a1e5dc 100644 --- a/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -128,16 +128,23 @@ class Md2ModelVertexDataTest { val actual = buildVertexData(testGlCmds, testFrames) - // these values are taken from the example in the Javadoc (very end) + // these values are taken from the example in the Javadoc (strip and fan unpacked) val expectedVertexAttributes = floatArrayOf( + 4.0f, 1.0f, 1.0f, + 5.0f, 1.0f, 0.5f, 0.0f, 0.0f, 0.5f, + 5.0f, 1.0f, 0.5f, 4.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 0.0f, 0.0f, - 4.0f, 1.0f, 0.0f, + 3.0f, 1.0f, 0.5f, + 4.0f, 1.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 2.0f, 0.0f, 0.5f, + 3.0f, 1.0f, 0.5f, + 1.0f, 0.0f, 0.0f, ) assertArrayEquals(expectedVertexAttributes, actual.vertexAttributes, 0.0001f) From 741a3cfd21bb38fa759702d41adb03bc57b1db70 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sat, 14 Jun 2025 17:27:31 +0200 Subject: [PATCH 17/24] wip: integrating md2 shaders into libgdx pipeline --- .../cake/modelviewer/CustomTextureData.kt | 8 +- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 81 ++++++++++++++----- .../org/demoth/cake/modelviewer/Md2Shader.kt | 29 +++++++ .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 71 +++++++++++++--- 4 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt index f334a4b1..2120c17b 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt @@ -6,6 +6,8 @@ import com.badlogic.gdx.graphics.Mesh import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.TextureData +import com.badlogic.gdx.graphics.g3d.Material +import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.math.Matrix4 import com.badlogic.gdx.utils.Disposable @@ -85,9 +87,9 @@ class CustomTextureData( class Md2ShaderModel( // model related, managed resources, todo: extract into a model class - private val mesh: Mesh, - private val vat: Pair, - private val diffuse: Pair, + val mesh: Mesh, + val vat: Pair, + val diffuse: Pair, // instance related, mutable state var frame1: Int = 0, var frame2: Int = 1, diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index 07559d49..f72b0e63 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -1,16 +1,19 @@ package org.demoth.cake.modelviewer -import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES -import com.badlogic.gdx.graphics.GL30 -import com.badlogic.gdx.graphics.Mesh -import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.VertexAttribute -import com.badlogic.gdx.graphics.VertexAttributes +import com.badlogic.gdx.graphics.VertexAttribute.TexCoords +import com.badlogic.gdx.graphics.VertexAttributes.Usage.Generic +import com.badlogic.gdx.graphics.g3d.Attributes import com.badlogic.gdx.graphics.g3d.Material import com.badlogic.gdx.graphics.g3d.Model +import com.badlogic.gdx.graphics.g3d.Renderable import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute +import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute.Diffuse +import com.badlogic.gdx.graphics.g3d.shaders.BaseShader +import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder +import com.badlogic.gdx.graphics.glutils.ShaderProgram import jake2.qcommon.filesystem.Md2Model import jake2.qcommon.filesystem.Md2VertexData import jake2.qcommon.filesystem.PCX @@ -88,33 +91,29 @@ class Md2ModelLoader(private val locator: ResourceLocator) { val diffuse = Texture(PCXTextureData(fromPCX(PCX(modelSkin)))) val vertexData = buildVertexData(md2Model.glCommands, md2Model.frames) - return Md2ShaderModel( - mesh = createMesh(vertexData), - vat = createVat(vertexData) to 0, - diffuse = diffuse to 1, - ) - } - /** - * The Mesh holds the vertex attributes, which in the VAT scenario are only texture coordinates. - * The indices are implicitly provided and normals are just skipped in this example. - * - */ - private fun createMesh(vertexData: Md2VertexData): Mesh { + val instancedAttribute = VertexAttribute(Generic, 1, "a_vat_index", 0) val mesh = Mesh( - true, + false, vertexData.vertexAttributes.size, vertexData.indices.size, VertexAttributes( - VertexAttribute(VertexAttributes.Usage.Generic, 1, "a_vat_index"), - VertexAttribute.TexCoords(1) // in future, normals can also be added here + instancedAttribute, + TexCoords(1) // in future, normals can also be added here ) ) mesh.setVertices(vertexData.vertexAttributes) mesh.setIndices(vertexData.indices) - return mesh + return Md2ShaderModel( + mesh = mesh, + vat = createVat(vertexData) to 0, + diffuse = diffuse to 1, + ) } + + + private fun createVat(vertexData: Md2VertexData): Texture { return Texture( CustomTextureData( @@ -129,6 +128,44 @@ class Md2ModelLoader(private val locator: ResourceLocator) { } } +// animation related local (per renderable) uniforms +val u_vertexAnimationTexture = BaseShader.Uniform("u_vertexAnimationTexture") +val u_textureHeight = BaseShader.Uniform("u_textureHeight") +val u_textureWidth = BaseShader.Uniform("u_textureWidth") +val u_frame1 = BaseShader.Uniform("u_frame1") +val u_frame2 = BaseShader.Uniform("u_frame2") +val u_interpolation = BaseShader.Uniform("u_interpolation") + +class AnimationTextureAttribute(val texture: Texture): TextureAttribute(AnimationTexture, texture) { + companion object { + @property:JvmStatic val AnimationTextureAlias: String = "animationTexture" + @property:JvmStatic val AnimationTexture: Long = register(AnimationTextureAlias) + @JvmStatic fun init() { + // this is weird? + Mask = Mask or AnimationTexture + } + } +} + + + +fun createModel(mesh: Mesh, diffuseTexture: Texture, animationTexture: Texture): Model { + // need to call static init explicitly? + // without it, I get the error about an invalid attribute type from 'register' + AnimationTextureAttribute.init() + + // create the material with diffuse and an "animation" attribute + val material = Material( + TextureAttribute(Diffuse, diffuseTexture), + AnimationTextureAttribute(animationTexture) + ) + + return ModelBuilder().apply { + begin() + part("part1", mesh, GL_TRIANGLES, material) + }.end() +} + private fun FloatArray.toFloatBuffer(): FloatBuffer { val result = ByteBuffer .allocateDirect(size * 4) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt new file mode 100644 index 00000000..42379eff --- /dev/null +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -0,0 +1,29 @@ +package org.demoth.cake.modelviewer + +import com.badlogic.gdx.graphics.g3d.Attributes +import com.badlogic.gdx.graphics.g3d.Renderable +import com.badlogic.gdx.graphics.g3d.shaders.BaseShader +import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader +import com.badlogic.gdx.graphics.glutils.ShaderProgram + +class Md2Shader( + val renderable: Renderable, + val config: DefaultShader.Config, + val shaderProgram: ShaderProgram, +): DefaultShader(renderable, config, shaderProgram) { + val u_vertexAnimationTextureSetter = object : LocalSetter() { + override fun set(shader: BaseShader, inputID: Int, renderable: Renderable?, combinedAttributes: Attributes) { + val unit = shader.context.textureBinder.bind(((combinedAttributes.get(AnimationTextureAttribute.AnimationTexture) as TextureAttribute).textureDescription)); + shader.set(inputID, unit); + } + } + + val u_vertexAnimationTexturePos = register(u_vertexAnimationTexture, u_vertexAnimationTextureSetter) + val u_textureHeightPos = register(u_textureHeight) + val u_textureWidthPos = register(u_textureWidth) + val u_frame1Pos = register(u_frame1) + val u_frame2Pos = register(u_frame2) + val u_interpolationPos = register(u_interpolation) + + +} diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index f0e8bae3..d3f6a546 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -6,7 +6,16 @@ import com.badlogic.gdx.Input import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.graphics.* +import com.badlogic.gdx.graphics.g3d.Attributes +import com.badlogic.gdx.graphics.g3d.ModelBatch +import com.badlogic.gdx.graphics.g3d.ModelInstance +import com.badlogic.gdx.graphics.g3d.Renderable +import com.badlogic.gdx.graphics.g3d.Shader +import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute +import com.badlogic.gdx.graphics.g3d.shaders.BaseShader +import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.CameraInputController +import com.badlogic.gdx.graphics.g3d.utils.ShaderProvider import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader @@ -14,11 +23,12 @@ import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.clientcommon.FlyingCameraController import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2ShaderModel +import org.demoth.cake.modelviewer.createModel class Md2ShaderTest : ApplicationAdapter(), Disposable { - private lateinit var md2Shader: ShaderProgram + private lateinit var md2ShaderProgram: ShaderProgram private var animationTime = 0f private val animationDuration = 0.1f // Example: 2 seconds animation duration @@ -47,14 +57,51 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { cameraInputController = FlyingCameraController(camera) Gdx.input.inputProcessor = cameraInputController - md2Shader = createShaderProgram() + md2ShaderProgram = createShaderProgram() // ok + + + val md2 = loadMd2Format() + + val model = createModel(md2.mesh, md2.diffuse.first, md2.vat.first) // ok, but there are nuances + val modelInstance = ModelInstance(model) // ok + + val shaderRenderable = Renderable() + val shader = DefaultShader( + modelInstance.getRenderable(shaderRenderable), // I don't understand + DefaultShader.Config(), + md2ShaderProgram, + ) + + val md2shaderProvider = object : ShaderProvider { + override fun getShader(renderable: Renderable): Shader? { + return if (renderable.userData == "md2shader") // is it ok to just tag object with user data? + shader + else null // ? can it return null? + } + + override fun dispose() { + TODO("Not yet implemented") + } + } + + val modelBatch = ModelBatch(md2shaderProvider) + + // inside render() + modelBatch.begin(camera) + modelBatch.render(modelInstance) // where do I stick md2shaderProvider ? + modelBatch.end() + + md2ShaderModel = md2 + } + + private fun loadMd2Format(): Md2ShaderModel { val pathToFile = "/home/daniil/.steam/steam/steamapps/common/Quake 2/baseq2/models/monsters/infantry" val locator = ModelViewerResourceLocator(pathToFile) val md2 = Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)?.apply { frame1 = 0 frame2 = if (frames > 1) 1 else 0 - } - md2ShaderModel = md2!! + }!! + return md2 } private fun createShaderProgram(): ShaderProgram { @@ -65,9 +112,10 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { val shaderProgram = ShaderProgram(vertexShader, fragmentShader) if (!shaderProgram.isCompiled) { - Gdx.app.error("Shader Error", md2Shader.log) + Gdx.app.error("Shader Error", shaderProgram.log) Gdx.app.exit() } + return shaderProgram } @@ -91,7 +139,7 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { val interpolation = animationTime / animationDuration md2ShaderModel.interpolation = interpolation - md2ShaderModel.render(md2Shader, camera.combined) + md2ShaderModel.render(md2ShaderProgram, camera.combined) if (playing) { animationTime += Gdx.graphics.deltaTime @@ -106,17 +154,17 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { animationTime = 0f // advance animation frames: frame1++ frame2++, keep in mind number of frames md2ShaderModel.frame1 = (md2ShaderModel.frame1 + delta) % md2ShaderModel.frames + md2ShaderModel.frame2 = (md2ShaderModel.frame2 + delta) % md2ShaderModel.frames if (md2ShaderModel.frame1 < 0) { md2ShaderModel.frame1 += md2ShaderModel.frames } if (md2ShaderModel.frame2 < 0) { md2ShaderModel.frame2 += md2ShaderModel.frames } - md2ShaderModel.frame2 = (md2ShaderModel.frame2 + delta) % md2ShaderModel.frames } override fun dispose() { - md2Shader.dispose() + md2ShaderProgram.dispose() md2ShaderModel.dispose() } } @@ -124,11 +172,12 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { private const val width = 1024 private const val height = 768 + fun main() { val config = Lwjgl3ApplicationConfiguration() - config.setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode()) -// config.setResizable(true) -// config.setWindowedMode(width, height) +// config.setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode()) + config.setResizable(true) + config.setWindowedMode(width, height) // fixme: didn't really quite get why it has to be explicitly loaded, // otherwise PerspectiveCamera(..) raises UnsatisfiedLinkError From e5d26ec2432a090ca62a1b9466d91b8e3595c02e Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 15 Jun 2025 05:12:25 -0700 Subject: [PATCH 18/24] Implement md2 model animation (VAT) #117: wip md2 shader add the required setup with uniforms and shader initialization --- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 30 +------- .../org/demoth/cake/modelviewer/Md2Shader.kt | 75 ++++++++++++++++--- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 5 +- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index f72b0e63..51487232 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -4,16 +4,11 @@ import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES import com.badlogic.gdx.graphics.VertexAttribute.TexCoords import com.badlogic.gdx.graphics.VertexAttributes.Usage.Generic -import com.badlogic.gdx.graphics.g3d.Attributes import com.badlogic.gdx.graphics.g3d.Material import com.badlogic.gdx.graphics.g3d.Model -import com.badlogic.gdx.graphics.g3d.Renderable import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute.Diffuse -import com.badlogic.gdx.graphics.g3d.shaders.BaseShader -import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder -import com.badlogic.gdx.graphics.glutils.ShaderProgram import jake2.qcommon.filesystem.Md2Model import jake2.qcommon.filesystem.Md2VertexData import jake2.qcommon.filesystem.PCX @@ -92,13 +87,12 @@ class Md2ModelLoader(private val locator: ResourceLocator) { val vertexData = buildVertexData(md2Model.glCommands, md2Model.frames) - val instancedAttribute = VertexAttribute(Generic, 1, "a_vat_index", 0) val mesh = Mesh( false, vertexData.vertexAttributes.size, vertexData.indices.size, VertexAttributes( - instancedAttribute, + VertexAttribute(Generic, 1, "a_vat_index"), TexCoords(1) // in future, normals can also be added here ) ) @@ -111,9 +105,6 @@ class Md2ModelLoader(private val locator: ResourceLocator) { ) } - - - private fun createVat(vertexData: Md2VertexData): Texture { return Texture( CustomTextureData( @@ -128,24 +119,7 @@ class Md2ModelLoader(private val locator: ResourceLocator) { } } -// animation related local (per renderable) uniforms -val u_vertexAnimationTexture = BaseShader.Uniform("u_vertexAnimationTexture") -val u_textureHeight = BaseShader.Uniform("u_textureHeight") -val u_textureWidth = BaseShader.Uniform("u_textureWidth") -val u_frame1 = BaseShader.Uniform("u_frame1") -val u_frame2 = BaseShader.Uniform("u_frame2") -val u_interpolation = BaseShader.Uniform("u_interpolation") - -class AnimationTextureAttribute(val texture: Texture): TextureAttribute(AnimationTexture, texture) { - companion object { - @property:JvmStatic val AnimationTextureAlias: String = "animationTexture" - @property:JvmStatic val AnimationTexture: Long = register(AnimationTextureAlias) - @JvmStatic fun init() { - // this is weird? - Mask = Mask or AnimationTexture - } - } -} + diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt index 42379eff..84ee367d 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -1,29 +1,80 @@ package org.demoth.cake.modelviewer +import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g3d.Attributes import com.badlogic.gdx.graphics.g3d.Renderable +import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.shaders.BaseShader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.glutils.ShaderProgram +data class Md2CustomData(var frame1: Int, var frame2: Int, var interpolation: Float) + +class AnimationTextureAttribute(val texture: Texture): TextureAttribute(AnimationTexture, texture) { + companion object { + @property:JvmStatic val AnimationTextureAlias: String = "animationTexture" + @property:JvmStatic val AnimationTexture: Long = register(AnimationTextureAlias) + @JvmStatic fun init() { + // this is weird? + Mask = Mask or AnimationTexture + } + } +} + class Md2Shader( - val renderable: Renderable, - val config: DefaultShader.Config, - val shaderProgram: ShaderProgram, + renderable: Renderable, + config: Config, + shaderProgram: ShaderProgram, ): DefaultShader(renderable, config, shaderProgram) { - val u_vertexAnimationTextureSetter = object : LocalSetter() { + // uniform setters + private val vertexAnimationTextureSetter = object : LocalSetter() { override fun set(shader: BaseShader, inputID: Int, renderable: Renderable?, combinedAttributes: Attributes) { - val unit = shader.context.textureBinder.bind(((combinedAttributes.get(AnimationTextureAttribute.AnimationTexture) as TextureAttribute).textureDescription)); - shader.set(inputID, unit); + // identify which unit to bind to + val textureDescription = + (combinedAttributes.get(AnimationTextureAttribute.AnimationTexture) as TextureAttribute).textureDescription + val unit = shader.context.textureBinder.bind(textureDescription) + shader.set(inputID, unit) + // also set the animation texture height and width + shader.set(u_textureHeightPos, textureDescription.texture.height) + shader.set(u_textureWidthPos, textureDescription.texture.width) } } - val u_vertexAnimationTexturePos = register(u_vertexAnimationTexture, u_vertexAnimationTextureSetter) - val u_textureHeightPos = register(u_textureHeight) - val u_textureWidthPos = register(u_textureWidth) - val u_frame1Pos = register(u_frame1) - val u_frame2Pos = register(u_frame2) - val u_interpolationPos = register(u_interpolation) + // local setters (unique values for each object) + private val frame1Setter = object : LocalSetter() { + override fun set(shader: BaseShader, inputID: Int, renderable: Renderable, combinedAttributes: Attributes) { + val md2CustomData = renderable.userData as Md2CustomData + shader.set(inputID, md2CustomData.frame1) + } + } + private val frame2Setter = object : LocalSetter() { + override fun set(shader: BaseShader, inputID: Int, renderable: Renderable, combinedAttributes: Attributes) { + val md2CustomData = renderable.userData as Md2CustomData + shader.set(inputID, md2CustomData.frame2) + } + } + private val interpolationSetter = object : LocalSetter() { + override fun set(shader: BaseShader, inputID: Int, renderable: Renderable, combinedAttributes: Attributes) { + val md2CustomData = renderable.userData as Md2CustomData + shader.set(inputID, md2CustomData.interpolation) + } + } + + // animation related local (per renderable) uniforms + private val u_vertexAnimationTexture = Uniform("u_vertexAnimationTexture") + private val u_vertexAnimationTextureHeight = Uniform("u_textureHeight") + private val u_vertexAnimationTextureWidth = Uniform("u_textureWidth") + private val u_frame1 = Uniform("u_frame1") + private val u_frame2 = Uniform("u_frame2") + private val u_interpolation = Uniform("u_interpolation") + + // register additional uniforms + private val u_vertexAnimationTexturePos = register(u_vertexAnimationTexture, vertexAnimationTextureSetter) + private val u_textureHeightPos = register(u_vertexAnimationTextureHeight) + private val u_textureWidthPos = register(u_vertexAnimationTextureWidth) + private val u_frame1Pos = register(u_frame1, frame1Setter) + private val u_frame2Pos = register(u_frame2, frame2Setter) + private val u_interpolationPos = register(u_interpolation, interpolationSetter) } diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index d3f6a546..d352279e 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -22,6 +22,7 @@ import com.badlogic.gdx.utils.SharedLibraryLoader import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.clientcommon.FlyingCameraController import org.demoth.cake.modelviewer.Md2ModelLoader +import org.demoth.cake.modelviewer.Md2Shader import org.demoth.cake.modelviewer.Md2ShaderModel import org.demoth.cake.modelviewer.createModel @@ -66,7 +67,7 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { val modelInstance = ModelInstance(model) // ok val shaderRenderable = Renderable() - val shader = DefaultShader( + val shader = Md2Shader( modelInstance.getRenderable(shaderRenderable), // I don't understand DefaultShader.Config(), md2ShaderProgram, @@ -75,7 +76,7 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { val md2shaderProvider = object : ShaderProvider { override fun getShader(renderable: Renderable): Shader? { return if (renderable.userData == "md2shader") // is it ok to just tag object with user data? - shader + shader else null // ? can it return null? } From fd13286f372257bf44f7367b862f75a4053e686b Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 15 Jun 2025 19:47:37 +0200 Subject: [PATCH 19/24] use the freshly implemented shader, it works!! :exploding_head: --- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index d352279e..25afb60e 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -6,21 +6,19 @@ import com.badlogic.gdx.Input import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.graphics.* -import com.badlogic.gdx.graphics.g3d.Attributes import com.badlogic.gdx.graphics.g3d.ModelBatch import com.badlogic.gdx.graphics.g3d.ModelInstance import com.badlogic.gdx.graphics.g3d.Renderable import com.badlogic.gdx.graphics.g3d.Shader -import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute -import com.badlogic.gdx.graphics.g3d.shaders.BaseShader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.CameraInputController -import com.badlogic.gdx.graphics.g3d.utils.ShaderProvider +import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.clientcommon.FlyingCameraController +import org.demoth.cake.modelviewer.Md2CustomData import org.demoth.cake.modelviewer.Md2ModelLoader import org.demoth.cake.modelviewer.Md2Shader import org.demoth.cake.modelviewer.Md2ShaderModel @@ -39,6 +37,9 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { private lateinit var md2ShaderModel: Md2ShaderModel private var playing = false + private lateinit var modelBatch: ModelBatch + private lateinit var modelInstance: ModelInstance + override fun create() { Gdx.gl.glEnable(GL20.GL_DEPTH_TEST) @@ -64,34 +65,30 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { val md2 = loadMd2Format() val model = createModel(md2.mesh, md2.diffuse.first, md2.vat.first) // ok, but there are nuances - val modelInstance = ModelInstance(model) // ok + modelInstance = ModelInstance(model) // ok + modelInstance.userData = Md2CustomData(0, 0, 0f) // we can save the reference to update it later val shaderRenderable = Renderable() - val shader = Md2Shader( + val md2Shader = Md2Shader( modelInstance.getRenderable(shaderRenderable), // I don't understand DefaultShader.Config(), md2ShaderProgram, ) - - val md2shaderProvider = object : ShaderProvider { + md2Shader.init() + val md2shaderProvider = object : DefaultShaderProvider() { override fun getShader(renderable: Renderable): Shader? { - return if (renderable.userData == "md2shader") // is it ok to just tag object with user data? - shader - else null // ? can it return null? + return if (renderable.userData is Md2CustomData) { + md2Shader + } else super.getShader(renderable) } override fun dispose() { - TODO("Not yet implemented") + md2Shader.dispose() + md2ShaderProgram.dispose() } } - val modelBatch = ModelBatch(md2shaderProvider) - - // inside render() - modelBatch.begin(camera) - modelBatch.render(modelInstance) // where do I stick md2shaderProvider ? - modelBatch.end() - + modelBatch = ModelBatch(md2shaderProvider) md2ShaderModel = md2 } @@ -137,18 +134,25 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { Gdx.gl.glClearColor(0f, 0f, 0f, 1f) Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) + // inside render() + modelBatch.begin(camera) + modelBatch.render(modelInstance) // where do I stick md2shaderProvider ? + modelBatch.end() - val interpolation = animationTime / animationDuration - md2ShaderModel.interpolation = interpolation - md2ShaderModel.render(md2ShaderProgram, camera.combined) - if (playing) { - animationTime += Gdx.graphics.deltaTime + /* + val interpolation = animationTime / animationDuration + md2ShaderModel.interpolation = interpolation + md2ShaderModel.render(md2ShaderProgram, camera.combined) - if (animationTime > animationDuration) { - changeFrame(1) - } - } + if (playing) { + animationTime += Gdx.graphics.deltaTime + + if (animationTime > animationDuration) { + changeFrame(1) + } + } + */ } private fun changeFrame(delta: Int) { From 41e06cff8aa20443365719c55ea9c0e6d0250639 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 15 Jun 2025 20:37:12 +0200 Subject: [PATCH 20/24] cleanup of code from the initial md2 animation implementation --- .../cake/modelviewer/CakeModelViewer.kt | 2 +- .../cake/modelviewer/CustomTextureData.kt | 47 ++---------- .../demoth/cake/modelviewer/Md2ModelLoader.kt | 6 +- .../org/demoth/cake/modelviewer/Md2Shader.kt | 7 +- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 72 ++++++++----------- 5 files changed, 46 insertions(+), 88 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt index d3089a9c..b7c99f0e 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt @@ -138,7 +138,7 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } - md2ShaderModel?.render(md2Shader, camera.combined) + //fixme: md2ShaderModel?.render(md2Shader, camera.combined) batch.use { // draw frame time in the bottom left corner diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt index 2120c17b..fd4fc593 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt @@ -1,15 +1,10 @@ package org.demoth.cake.modelviewer import com.badlogic.gdx.Gdx -import com.badlogic.gdx.graphics.GL20 import com.badlogic.gdx.graphics.Mesh import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.TextureData -import com.badlogic.gdx.graphics.g3d.Material -import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute -import com.badlogic.gdx.graphics.glutils.ShaderProgram -import com.badlogic.gdx.math.Matrix4 import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.GdxRuntimeException import java.nio.Buffer @@ -86,48 +81,16 @@ class CustomTextureData( } class Md2ShaderModel( - // model related, managed resources, todo: extract into a model class val mesh: Mesh, - val vat: Pair, - val diffuse: Pair, - // instance related, mutable state - var frame1: Int = 0, - var frame2: Int = 1, - var interpolation: Float = 0.0f, - val entityTransform: Matrix4 = Matrix4() + val vat: Texture, + val diffuse: Texture, ): Disposable { - val frames = vat.first.height - - private val textureWidth = vat.first.width - private val textureHeight = vat.first.height - - fun render(shader: ShaderProgram, cameraTransform: Matrix4) { - shader.bind() - - shader.setUniformi("u_frame1", frame1) - shader.setUniformi("u_frame2", frame2) - shader.setUniformf("u_interpolation", interpolation) - - shader.setUniformMatrix("u_projViewTrans", cameraTransform); - shader.setUniformMatrix("u_worldTrans", entityTransform) - - shader.setUniformi("u_vertexAnimationTexture", vat.second) - shader.setUniformi("u_diffuseTexture", diffuse.second) - - shader.setUniformi("u_textureHeight", textureHeight) // number of frames - shader.setUniformi("u_textureWidth", textureWidth) // number of vertices - - vat.first.bind(vat.second) - diffuse.first.bind(diffuse.second) - - mesh.render(shader, GL20.GL_TRIANGLES) - } + val frames = vat.height override fun dispose() { mesh.dispose() - vat.first.dispose() - diffuse.first.dispose() + vat.dispose() + diffuse.dispose() } - } \ No newline at end of file diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index 51487232..f8d605b3 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -88,7 +88,7 @@ class Md2ModelLoader(private val locator: ResourceLocator) { val vertexData = buildVertexData(md2Model.glCommands, md2Model.frames) val mesh = Mesh( - false, + true, vertexData.vertexAttributes.size, vertexData.indices.size, VertexAttributes( @@ -100,8 +100,8 @@ class Md2ModelLoader(private val locator: ResourceLocator) { mesh.setIndices(vertexData.indices) return Md2ShaderModel( mesh = mesh, - vat = createVat(vertexData) to 0, - diffuse = diffuse to 1, + vat = createVat(vertexData), + diffuse = diffuse, ) } diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt index 84ee367d..b44c96b0 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -8,7 +8,12 @@ import com.badlogic.gdx.graphics.g3d.shaders.BaseShader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.glutils.ShaderProgram -data class Md2CustomData(var frame1: Int, var frame2: Int, var interpolation: Float) +data class Md2CustomData( + var frame1: Int, + var frame2: Int, + var interpolation: Float, + val frames: Int +) class AnimationTextureAttribute(val texture: Texture): TextureAttribute(AnimationTexture, texture) { companion object { diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index 25afb60e..59c971df 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -27,15 +27,12 @@ import org.demoth.cake.modelviewer.createModel class Md2ShaderTest : ApplicationAdapter(), Disposable { - private lateinit var md2ShaderProgram: ShaderProgram private var animationTime = 0f - - private val animationDuration = 0.1f // Example: 2 seconds animation duration + private val animationDuration = 0.2f // Example: 2 seconds animation duration private lateinit var camera: Camera private lateinit var cameraInputController : CameraInputController - private lateinit var md2ShaderModel: Md2ShaderModel private var playing = false private lateinit var modelBatch: ModelBatch private lateinit var modelInstance: ModelInstance @@ -59,20 +56,22 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { cameraInputController = FlyingCameraController(camera) Gdx.input.inputProcessor = cameraInputController - md2ShaderProgram = createShaderProgram() // ok - - val md2 = loadMd2Format() - val model = createModel(md2.mesh, md2.diffuse.first, md2.vat.first) // ok, but there are nuances + val model = createModel(md2.mesh, md2.diffuse, md2.vat) modelInstance = ModelInstance(model) // ok - modelInstance.userData = Md2CustomData(0, 0, 0f) // we can save the reference to update it later + modelInstance.userData = Md2CustomData( + 0, + if (md2.frames > 1) 1 else 0, + 0f, + md2.frames + ) val shaderRenderable = Renderable() val md2Shader = Md2Shader( modelInstance.getRenderable(shaderRenderable), // I don't understand DefaultShader.Config(), - md2ShaderProgram, + createShaderProgram(), ) md2Shader.init() val md2shaderProvider = object : DefaultShaderProvider() { @@ -84,22 +83,16 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { override fun dispose() { md2Shader.dispose() - md2ShaderProgram.dispose() } } modelBatch = ModelBatch(md2shaderProvider) - md2ShaderModel = md2 } private fun loadMd2Format(): Md2ShaderModel { val pathToFile = "/home/daniil/.steam/steam/steamapps/common/Quake 2/baseq2/models/monsters/infantry" val locator = ModelViewerResourceLocator(pathToFile) - val md2 = Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)?.apply { - frame1 = 0 - frame2 = if (frames > 1) 1 else 0 - }!! - return md2 + return Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)!! } private fun createShaderProgram(): ShaderProgram { @@ -123,9 +116,9 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { } else if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { Gdx.app.exit() } else if (Gdx.input.isKeyJustPressed(Input.Keys.RIGHT)) { - changeFrame(1) + changeFrame(1, modelInstance.getMd2CustomData()) } else if (Gdx.input.isKeyJustPressed(Input.Keys.LEFT)) { - changeFrame(-1) + changeFrame(-1, modelInstance.getMd2CustomData()) } camera.update() @@ -136,44 +129,41 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { // inside render() modelBatch.begin(camera) - modelBatch.render(modelInstance) // where do I stick md2shaderProvider ? + modelBatch.render(modelInstance) modelBatch.end() + modelInstance.getMd2CustomData().interpolation = animationTime / animationDuration - /* - val interpolation = animationTime / animationDuration - md2ShaderModel.interpolation = interpolation - md2ShaderModel.render(md2ShaderProgram, camera.combined) + if (playing) { + animationTime += Gdx.graphics.deltaTime - if (playing) { - animationTime += Gdx.graphics.deltaTime - - if (animationTime > animationDuration) { - changeFrame(1) - } - } - */ + if (animationTime > animationDuration) { + changeFrame(1, modelInstance.getMd2CustomData()) + } + } } - private fun changeFrame(delta: Int) { + private fun changeFrame(delta: Int, md2CustomData: Md2CustomData) { animationTime = 0f // advance animation frames: frame1++ frame2++, keep in mind number of frames - md2ShaderModel.frame1 = (md2ShaderModel.frame1 + delta) % md2ShaderModel.frames - md2ShaderModel.frame2 = (md2ShaderModel.frame2 + delta) % md2ShaderModel.frames - if (md2ShaderModel.frame1 < 0) { - md2ShaderModel.frame1 += md2ShaderModel.frames + md2CustomData.frame1 = (md2CustomData.frame1 + delta) % md2CustomData.frames + md2CustomData.frame2 = (md2CustomData.frame2 + delta) % md2CustomData.frames + if (md2CustomData.frame1 < 0) { + md2CustomData.frame1 += md2CustomData.frames } - if (md2ShaderModel.frame2 < 0) { - md2ShaderModel.frame2 += md2ShaderModel.frames + if (md2CustomData.frame2 < 0) { + md2CustomData.frame2 += md2CustomData.frames } + md2CustomData.interpolation = 0f } override fun dispose() { - md2ShaderProgram.dispose() - md2ShaderModel.dispose() + modelInstance.model.dispose() } } +fun ModelInstance.getMd2CustomData(): Md2CustomData = userData as Md2CustomData + private const val width = 1024 private const val height = 768 From 3a5713da6ebfd321bd99a841abc528ab64a78774 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 15 Jun 2025 21:54:27 +0200 Subject: [PATCH 21/24] documentation for md2 shader --- .../org/demoth/cake/modelviewer/Md2Shader.kt | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt index b44c96b0..8016087e 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -3,9 +3,11 @@ package org.demoth.cake.modelviewer import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g3d.Attributes import com.badlogic.gdx.graphics.g3d.Renderable +import com.badlogic.gdx.graphics.g3d.Shader import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.shaders.BaseShader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader +import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider import com.badlogic.gdx.graphics.glutils.ShaderProgram data class Md2CustomData( @@ -15,6 +17,10 @@ data class Md2CustomData( val frames: Int ) +/** + * The custom texture attribute, required to register and bind the animation texture. + * Practically, it's just another type of texture attribute. + */ class AnimationTextureAttribute(val texture: Texture): TextureAttribute(AnimationTexture, texture) { companion object { @property:JvmStatic val AnimationTextureAlias: String = "animationTexture" @@ -26,12 +32,18 @@ class AnimationTextureAttribute(val texture: Texture): TextureAttribute(Animatio } } +/** + * Shader for md2 vertex animations using a Vertex Animation Texture (or VAT) approach. + * + * This class doesn't have any custom logic apart from the registration of the required uniforms and attributes. + * The [DefaultShader] superclass is already flexible enough to handle the rest. + */ class Md2Shader( renderable: Renderable, config: Config, shaderProgram: ShaderProgram, ): DefaultShader(renderable, config, shaderProgram) { - // uniform setters + // uniform setters (local as they are different for each object) private val vertexAnimationTextureSetter = object : LocalSetter() { override fun set(shader: BaseShader, inputID: Int, renderable: Renderable?, combinedAttributes: Attributes) { // identify which unit to bind to @@ -39,13 +51,12 @@ class Md2Shader( (combinedAttributes.get(AnimationTextureAttribute.AnimationTexture) as TextureAttribute).textureDescription val unit = shader.context.textureBinder.bind(textureDescription) shader.set(inputID, unit) - // also set the animation texture height and width + + // since here we already know the size of the vat, we can also set its height and width uniforms shader.set(u_textureHeightPos, textureDescription.texture.height) shader.set(u_textureWidthPos, textureDescription.texture.width) } } - - // local setters (unique values for each object) private val frame1Setter = object : LocalSetter() { override fun set(shader: BaseShader, inputID: Int, renderable: Renderable, combinedAttributes: Attributes) { val md2CustomData = renderable.userData as Md2CustomData @@ -66,20 +77,30 @@ class Md2Shader( } // animation related local (per renderable) uniforms - private val u_vertexAnimationTexture = Uniform("u_vertexAnimationTexture") + protected val u_vertexAnimationTexture = Uniform("u_vertexAnimationTexture") private val u_vertexAnimationTextureHeight = Uniform("u_textureHeight") private val u_vertexAnimationTextureWidth = Uniform("u_textureWidth") private val u_frame1 = Uniform("u_frame1") private val u_frame2 = Uniform("u_frame2") private val u_interpolation = Uniform("u_interpolation") - // register additional uniforms + // register the uniforms private val u_vertexAnimationTexturePos = register(u_vertexAnimationTexture, vertexAnimationTextureSetter) + // no setter for these two as they are set inside the setter for u_vertexAnimationTexturePos private val u_textureHeightPos = register(u_vertexAnimationTextureHeight) private val u_textureWidthPos = register(u_vertexAnimationTextureWidth) private val u_frame1Pos = register(u_frame1, frame1Setter) private val u_frame2Pos = register(u_frame2, frame2Setter) private val u_interpolationPos = register(u_interpolation, interpolationSetter) - - } + +/** + * Shader provider for md2 animations, uses the user data to determine whether to use the [Md2Shader] or not. + */ +class Md2ShaderProvider(private val shader: Shader): DefaultShaderProvider() { + override fun getShader(renderable: Renderable): Shader? { + return if (renderable.userData is Md2CustomData) { + shader + } else super.getShader(renderable) + } +} \ No newline at end of file From bf23dfbe7a2d947deeb5461685e997cd82adfc81 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Sun, 15 Jun 2025 22:12:22 +0200 Subject: [PATCH 22/24] use default libgdx fragment shader --- assets/shaders/md2-fragment.glsl | 9 ---- assets/shaders/vat.glsl | 6 +-- .../org/demoth/cake/modelviewer/Md2Shader.kt | 8 ++- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 50 ++++--------------- 4 files changed, 19 insertions(+), 54 deletions(-) delete mode 100644 assets/shaders/md2-fragment.glsl diff --git a/assets/shaders/md2-fragment.glsl b/assets/shaders/md2-fragment.glsl deleted file mode 100644 index 52dedd69..00000000 --- a/assets/shaders/md2-fragment.glsl +++ /dev/null @@ -1,9 +0,0 @@ -#version 130 // same as vertex - -uniform sampler2D u_diffuseTexture; // the new colour texture – unit 1 - -varying vec2 v_texCoord; // real UVs from vertex shader - -void main() { - gl_FragColor = texture2D(u_diffuseTexture, v_texCoord); // doesn't work: I see a black screen -} \ No newline at end of file diff --git a/assets/shaders/vat.glsl b/assets/shaders/vat.glsl index 1d9af2e2..1c15c15b 100644 --- a/assets/shaders/vat.glsl +++ b/assets/shaders/vat.glsl @@ -1,5 +1,3 @@ -#version 130 - attribute float a_vat_index; attribute vec2 a_texCoord1; // Diffuse Texture coordinates @@ -13,7 +11,7 @@ uniform int u_frame1; // Index of the first frame in the animation texture uniform int u_frame2; // Index of the second frame in the animation texture uniform float u_interpolation; // Interpolation factor between two animation frames (0.0 to 1.0) -varying vec2 v_texCoord; +varying vec2 v_diffuseUV; void main() { vec2 texelSize = vec2(1.0 / u_textureWidth, 1.0 / u_textureHeight); @@ -30,5 +28,5 @@ void main() { // Apply the final interpolated position gl_Position = u_projViewTrans * u_worldTrans * vec4(finalPosition, 1.0); - v_texCoord = a_texCoord1; + v_diffuseUV = a_texCoord1; } diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt index 8016087e..24c4cff8 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -32,6 +32,11 @@ class AnimationTextureAttribute(val texture: Texture): TextureAttribute(Animatio } } +private const val md2ShaderPrefix = """ + #version 130 + #define diffuseTextureFlag +""" + /** * Shader for md2 vertex animations using a Vertex Animation Texture (or VAT) approach. * @@ -41,8 +46,7 @@ class AnimationTextureAttribute(val texture: Texture): TextureAttribute(Animatio class Md2Shader( renderable: Renderable, config: Config, - shaderProgram: ShaderProgram, -): DefaultShader(renderable, config, shaderProgram) { +): DefaultShader(renderable, config, md2ShaderPrefix) { // uniform setters (local as they are different for each object) private val vertexAnimationTextureSetter = object : LocalSetter() { override fun set(shader: BaseShader, inputID: Int, renderable: Renderable?, combinedAttributes: Attributes) { diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt index 59c971df..c767f0d5 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt @@ -5,24 +5,20 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration -import com.badlogic.gdx.graphics.* +import com.badlogic.gdx.graphics.Camera +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.PerspectiveCamera import com.badlogic.gdx.graphics.g3d.ModelBatch import com.badlogic.gdx.graphics.g3d.ModelInstance import com.badlogic.gdx.graphics.g3d.Renderable -import com.badlogic.gdx.graphics.g3d.Shader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.CameraInputController -import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.SharedLibraryLoader import org.demoth.cake.ModelViewerResourceLocator import org.demoth.cake.clientcommon.FlyingCameraController -import org.demoth.cake.modelviewer.Md2CustomData -import org.demoth.cake.modelviewer.Md2ModelLoader -import org.demoth.cake.modelviewer.Md2Shader -import org.demoth.cake.modelviewer.Md2ShaderModel -import org.demoth.cake.modelviewer.createModel +import org.demoth.cake.modelviewer.* class Md2ShaderTest : ApplicationAdapter(), Disposable { @@ -67,26 +63,17 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { md2.frames ) - val shaderRenderable = Renderable() + val tempRenderable = Renderable() val md2Shader = Md2Shader( - modelInstance.getRenderable(shaderRenderable), // I don't understand - DefaultShader.Config(), - createShaderProgram(), + modelInstance.getRenderable(tempRenderable), // may not be obvious, but it's required for the shader initialization, the renderable is not used after that + DefaultShader.Config( + Gdx.files.internal("shaders/vat.glsl").readString(), + null, // use default fragment shader + ) ) md2Shader.init() - val md2shaderProvider = object : DefaultShaderProvider() { - override fun getShader(renderable: Renderable): Shader? { - return if (renderable.userData is Md2CustomData) { - md2Shader - } else super.getShader(renderable) - } - - override fun dispose() { - md2Shader.dispose() - } - } - modelBatch = ModelBatch(md2shaderProvider) + modelBatch = ModelBatch(Md2ShaderProvider(md2Shader)) } private fun loadMd2Format(): Md2ShaderModel { @@ -95,21 +82,6 @@ class Md2ShaderTest : ApplicationAdapter(), Disposable { return Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)!! } - private fun createShaderProgram(): ShaderProgram { - //ShaderProgram.pedantic = false // Disable strict checking to keep the example simple. - - val vertexShader = Gdx.files.internal("shaders/vat.glsl").readString() // Assuming vat.vert contains the shader code above - val fragmentShader = Gdx.files.internal("shaders/md2-fragment.glsl").readString() - - val shaderProgram = ShaderProgram(vertexShader, fragmentShader) - if (!shaderProgram.isCompiled) { - Gdx.app.error("Shader Error", shaderProgram.log) - Gdx.app.exit() - } - - return shaderProgram - } - override fun render() { if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { playing = !playing From 83417eab3b5b2f90ad6a271c8d626258cecb0c23 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Mon, 16 Jun 2025 00:06:03 +0200 Subject: [PATCH 23/24] Cake Model Viewer 1.4: Add MD2 animation --- .../cake/modelviewer/CakeModelViewer.kt | 113 ++++++++----- .../org/demoth/cake/modelviewer/Md2Shader.kt | 1 - .../cake/lwjgl3/Lwjgl3ModelViewerLauncher.kt | 4 +- .../org/demoth/cake/lwjgl3/Md2ShaderTest.kt | 154 ------------------ 4 files changed, 76 insertions(+), 196 deletions(-) delete mode 100644 cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt index b7c99f0e..0797bc0f 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt @@ -10,9 +10,9 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g3d.* import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight +import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.CameraInputController import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder -import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.math.Vector3 import jake2.qcommon.filesystem.PCX import jake2.qcommon.filesystem.WAL @@ -34,16 +34,16 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { private lateinit var camera: Camera private lateinit var cameraInputController: CameraInputController // rendered with the default libgdx shader - private val models: MutableList = mutableListOf() + private val instances: MutableList = mutableListOf() private lateinit var environment: Environment private lateinit var font: BitmapFont - private var frameTime = 0f - - - private lateinit var md2Shader: ShaderProgram - private var md2ShaderModel: Md2ShaderModel? = null - private var md2AnimationTimer = 0.1f + private var frameTime = 0f // to have an idea of the fps + // md2 related stuff + private var md2Instance: ModelInstance? = null // to control the model animation + private var md2AnimationFrameTime = 0.1f + private var md2AnimationTime = 0.0f + private var playingMd2Animation = false override fun create() { @@ -68,35 +68,43 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } "md2" -> { - // Create the shader program - val vertexShader = - Gdx.files.internal("shaders/vat.glsl").readString() // Assuming vat.vert contains the shader code above - val fragmentShader = "void main() { gl_FragColor = vec4(1.0); }" // Simple dummy fragment shader - md2Shader = ShaderProgram(vertexShader, fragmentShader) - if (!md2Shader.isCompiled) { - Gdx.app.error("Shader Error", md2Shader.getLog()) - Gdx.app.exit() - } + val md2 = Md2ModelLoader(locator).loadAnimatedModel(file.absolutePath, null, 0)!! + val model = createModel(md2.mesh, md2.diffuse, md2.vat) + md2Instance = ModelInstance(model) // ok + md2Instance!!.userData = Md2CustomData( + 0, + if (md2.frames > 1) 1 else 0, + 0f, + md2.frames + ) - md2ShaderModel = Md2ModelLoader(locator).loadAnimatedModel( - modelName = file.path, // will be passed to the ResourceLocator - playerSkin = null, - skinIndex = 0 + val tempRenderable = Renderable() + val md2Shader = Md2Shader( + md2Instance!!.getRenderable(tempRenderable), // may not be obvious, but it's required for the shader initialization, the renderable is not used after that + DefaultShader.Config( + Gdx.files.internal("shaders/vat.glsl").readString(), + null, // use default fragment shader + ) ) - models.add(createOriginArrows(GRID_SIZE)) - models.add(createGrid(GRID_SIZE, GRID_DIVISIONS)) + md2Shader.init() + + modelBatch = ModelBatch(Md2ShaderProvider(md2Shader)) + + instances.add(md2Instance!!) + instances.add(createOriginArrows(GRID_SIZE)) + instances.add(createGrid(GRID_SIZE, GRID_DIVISIONS)) } "bsp" -> { + modelBatch = ModelBatch() // models.add(BspLoader().loadBSPModelWireFrame(file).transformQ2toLibgdx()) // models.addAll(BspLoader(gameDir).loadBspModels(file)) - models.add(createOriginArrows(GRID_SIZE)) - models.add(createGrid(GRID_SIZE, GRID_DIVISIONS)) + instances.add(createOriginArrows(GRID_SIZE)) + instances.add(createGrid(GRID_SIZE, GRID_DIVISIONS)) } } batch = SpriteBatch() - modelBatch = ModelBatch() camera = PerspectiveCamera(90f, Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat()) camera.position.set(0f, 0f, 0f); camera.near = 1f @@ -113,33 +121,43 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } override fun render() { - // advance the model animation (if any) each 0.1 seconds - md2AnimationTimer -= Gdx.graphics.deltaTime - if (md2AnimationTimer < 0f) { - md2AnimationTimer = 0.1f - //model?.nextFrame() + if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { + playingMd2Animation = !playingMd2Animation + } else if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { + Gdx.app.exit() + } else if (Gdx.input.isKeyJustPressed(Input.Keys.RIGHT)) { + md2AnimationTime = 0f + changeFrame(1, md2Instance!!.getMd2CustomData()) + } else if (Gdx.input.isKeyJustPressed(Input.Keys.LEFT)) { + md2AnimationTime = 0f + changeFrame(-1, md2Instance!!.getMd2CustomData()) } - frameTime = measureTimeMillis { - if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { - Gdx.app.exit() + if (playingMd2Animation) { + md2AnimationTime += Gdx.graphics.deltaTime + if (md2AnimationTime > md2AnimationFrameTime) { + md2AnimationTime = 0f + changeFrame(1, md2Instance!!.getMd2CustomData()) } + md2Instance!!.getMd2CustomData().interpolation = md2AnimationTime / md2AnimationFrameTime + + } + + frameTime = measureTimeMillis { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1f) Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) - camera.update(); + camera.update() - if (models.isNotEmpty()) { + if (instances.isNotEmpty()) { modelBatch.begin(camera) - models.forEach { + instances.forEach { modelBatch.render(it) } modelBatch.end() } - //fixme: md2ShaderModel?.render(md2Shader, camera.combined) - batch.use { // draw frame time in the bottom left corner font.draw(batch, "Frame time ms: $frameTime", 0f, font.lineHeight) @@ -160,11 +178,26 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { override fun dispose() { batch.dispose() image?.dispose() - models.forEach { it.model.dispose() } + instances.forEach { it.model.dispose() } modelBatch.dispose() } } +private fun changeFrame(delta: Int, md2CustomData: Md2CustomData) { + // advance animation frames: frame1++ frame2++, keep in mind number of frames + md2CustomData.frame1 = (md2CustomData.frame1 + delta) % md2CustomData.frames + md2CustomData.frame2 = (md2CustomData.frame2 + delta) % md2CustomData.frames + if (md2CustomData.frame1 < 0) { + md2CustomData.frame1 += md2CustomData.frames + } + if (md2CustomData.frame2 < 0) { + md2CustomData.frame2 += md2CustomData.frames + } + md2CustomData.interpolation = 0f +} + +fun ModelInstance.getMd2CustomData(): Md2CustomData = userData as Md2CustomData + fun createGrid(size: Float, divisions: Int): ModelInstance { val modelBuilder = ModelBuilder() // grid is in XZ plane diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt index 24c4cff8..37568770 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -8,7 +8,6 @@ import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.shaders.BaseShader import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider -import com.badlogic.gdx.graphics.glutils.ShaderProgram data class Md2CustomData( var frame1: Int, diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Lwjgl3ModelViewerLauncher.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Lwjgl3ModelViewerLauncher.kt index 2f0dbe9c..70506ec1 100644 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Lwjgl3ModelViewerLauncher.kt +++ b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Lwjgl3ModelViewerLauncher.kt @@ -18,7 +18,7 @@ private fun createApplication(args: Array): Lwjgl3Application { } private fun createConfiguration() = Lwjgl3ApplicationConfiguration().apply { - setTitle("Cake Model Viewer 1.3") + setTitle("Cake Model Viewer 1.4") useVsync(true) //// Limits FPS to the refresh rate of the currently active monitor. setForegroundFPS(Lwjgl3ApplicationConfiguration.getDisplayMode().refreshRate) @@ -29,4 +29,6 @@ private fun createConfiguration() = Lwjgl3ApplicationConfiguration().apply { //// You may also need to configure GPU drivers to fully disable Vsync; this can cause screen tearing. setWindowedMode(1280, 720) setWindowIcon("icons/logo.png", "icons/logo-32.png") +// setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode()) + } diff --git a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt b/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt deleted file mode 100644 index c767f0d5..00000000 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ /dev/null @@ -1,154 +0,0 @@ -package org.demoth.cake.lwjgl3 - -import com.badlogic.gdx.ApplicationAdapter -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration -import com.badlogic.gdx.graphics.Camera -import com.badlogic.gdx.graphics.GL20 -import com.badlogic.gdx.graphics.PerspectiveCamera -import com.badlogic.gdx.graphics.g3d.ModelBatch -import com.badlogic.gdx.graphics.g3d.ModelInstance -import com.badlogic.gdx.graphics.g3d.Renderable -import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader -import com.badlogic.gdx.graphics.g3d.utils.CameraInputController -import com.badlogic.gdx.graphics.glutils.ShaderProgram -import com.badlogic.gdx.utils.Disposable -import com.badlogic.gdx.utils.SharedLibraryLoader -import org.demoth.cake.ModelViewerResourceLocator -import org.demoth.cake.clientcommon.FlyingCameraController -import org.demoth.cake.modelviewer.* - - -class Md2ShaderTest : ApplicationAdapter(), Disposable { - - private var animationTime = 0f - private val animationDuration = 0.2f // Example: 2 seconds animation duration - - private lateinit var camera: Camera - private lateinit var cameraInputController : CameraInputController - - private var playing = false - private lateinit var modelBatch: ModelBatch - private lateinit var modelInstance: ModelInstance - - - override fun create() { - Gdx.gl.glEnable(GL20.GL_DEPTH_TEST) - Gdx.gl.glDepthFunc(GL20.GL_LEQUAL) - Gdx.gl.glEnable(GL20.GL_CULL_FACE) - Gdx.gl.glCullFace(GL20.GL_BACK) - - - // copied from model viewer - camera = PerspectiveCamera(90f, Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat()) - camera.position.set(0f, 0f, 0f); - camera.near = 1f - camera.far = 4096f - camera.up.set(0f, 0f, 1f) // make z up - camera.direction.set(0f, 1f, 0f) // make y forward - - cameraInputController = FlyingCameraController(camera) - Gdx.input.inputProcessor = cameraInputController - - val md2 = loadMd2Format() - - val model = createModel(md2.mesh, md2.diffuse, md2.vat) - modelInstance = ModelInstance(model) // ok - modelInstance.userData = Md2CustomData( - 0, - if (md2.frames > 1) 1 else 0, - 0f, - md2.frames - ) - - val tempRenderable = Renderable() - val md2Shader = Md2Shader( - modelInstance.getRenderable(tempRenderable), // may not be obvious, but it's required for the shader initialization, the renderable is not used after that - DefaultShader.Config( - Gdx.files.internal("shaders/vat.glsl").readString(), - null, // use default fragment shader - ) - ) - md2Shader.init() - - modelBatch = ModelBatch(Md2ShaderProvider(md2Shader)) - } - - private fun loadMd2Format(): Md2ShaderModel { - val pathToFile = "/home/daniil/.steam/steam/steamapps/common/Quake 2/baseq2/models/monsters/infantry" - val locator = ModelViewerResourceLocator(pathToFile) - return Md2ModelLoader(locator).loadAnimatedModel("$pathToFile/tris.md2", null, 0)!! - } - - override fun render() { - if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - playing = !playing - } else if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) { - Gdx.app.exit() - } else if (Gdx.input.isKeyJustPressed(Input.Keys.RIGHT)) { - changeFrame(1, modelInstance.getMd2CustomData()) - } else if (Gdx.input.isKeyJustPressed(Input.Keys.LEFT)) { - changeFrame(-1, modelInstance.getMd2CustomData()) - } - - camera.update() - - // Clear the screen - Gdx.gl.glClearColor(0f, 0f, 0f, 1f) - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) - - // inside render() - modelBatch.begin(camera) - modelBatch.render(modelInstance) - modelBatch.end() - - modelInstance.getMd2CustomData().interpolation = animationTime / animationDuration - - if (playing) { - animationTime += Gdx.graphics.deltaTime - - if (animationTime > animationDuration) { - changeFrame(1, modelInstance.getMd2CustomData()) - } - } - } - - private fun changeFrame(delta: Int, md2CustomData: Md2CustomData) { - animationTime = 0f - // advance animation frames: frame1++ frame2++, keep in mind number of frames - md2CustomData.frame1 = (md2CustomData.frame1 + delta) % md2CustomData.frames - md2CustomData.frame2 = (md2CustomData.frame2 + delta) % md2CustomData.frames - if (md2CustomData.frame1 < 0) { - md2CustomData.frame1 += md2CustomData.frames - } - if (md2CustomData.frame2 < 0) { - md2CustomData.frame2 += md2CustomData.frames - } - md2CustomData.interpolation = 0f - } - - override fun dispose() { - modelInstance.model.dispose() - } -} - -fun ModelInstance.getMd2CustomData(): Md2CustomData = userData as Md2CustomData - -private const val width = 1024 -private const val height = 768 - - -fun main() { - val config = Lwjgl3ApplicationConfiguration() -// config.setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode()) - config.setResizable(true) - config.setWindowedMode(width, height) - - // fixme: didn't really quite get why it has to be explicitly loaded, - // otherwise PerspectiveCamera(..) raises UnsatisfiedLinkError - SharedLibraryLoader().load("gdx") - - Lwjgl3Application(Md2ShaderTest(), config) -} \ No newline at end of file From 89c8fbfa1846e44fc3e25a70b866335c8a70b9f2 Mon Sep 17 00:00:00 2001 From: Daniil Bubnov Date: Mon, 16 Jun 2025 23:51:33 +0200 Subject: [PATCH 24/24] Incorporate MD2 animation improvements into the game --- .../org/demoth/cake/stages/Game3dScreen.kt | 37 ++++- .../cake/modelviewer/CakeModelViewer.kt | 11 +- .../cake/modelviewer/CustomTextureData.kt | 96 ------------ .../demoth/cake/modelviewer/Md2ModelLoader.kt | 146 +++++++++++------- 4 files changed, 127 insertions(+), 163 deletions(-) delete mode 100644 cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt diff --git a/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt b/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt index 3a11bae6..5d8b3764 100644 --- a/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt +++ b/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt @@ -11,8 +11,10 @@ import com.badlogic.gdx.graphics.g3d.Environment import com.badlogic.gdx.graphics.g3d.Model import com.badlogic.gdx.graphics.g3d.ModelBatch import com.badlogic.gdx.graphics.g3d.ModelInstance +import com.badlogic.gdx.graphics.g3d.Renderable import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight +import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader import com.badlogic.gdx.math.MathUtils.degRad import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.scenes.scene2d.ui.Skin @@ -145,7 +147,6 @@ class Game3dScreen : KtxScreen, InputProcessor, ServerMessageProcessor { ) // mappings for input command: which are sent on every client update frame - private val mouseSensitivity = 1f // todo cvar private var mouseWasMoved = false private val inputKeyMappings: MutableMap = mutableMapOf( @@ -200,7 +201,30 @@ class Game3dScreen : KtxScreen, InputProcessor, ServerMessageProcessor { // create camera camera.update() - modelBatch = ModelBatch() + + // fixme: make a free internal md2 model specifically for the shader initialization, don't use q2 resources + val md2 = Md2ModelLoader(locator).loadMd2ModelData("models/monsters/berserk/tris.md2", null, 0)!! + val model = createModel(md2.mesh, md2.material) + val md2Instance = ModelInstance(model) + + md2Instance.userData = Md2CustomData( + 0, + if (md2.frames > 1) 1 else 0, + 0f, + md2.frames + ) + + val tempRenderable = Renderable() + val md2Shader = Md2Shader( + md2Instance!!.getRenderable(tempRenderable), // may not be obvious, but it's required for the shader initialization, the renderable is not used after that + DefaultShader.Config( + Gdx.files.internal("shaders/vat.glsl").readString(), + null, // use default fragment shader + ) + ) + md2Shader.init() + + modelBatch = ModelBatch(Md2ShaderProvider(md2Shader)) ClientCommands.entries.forEach { commandsState[it] = false } } @@ -256,7 +280,7 @@ class Game3dScreen : KtxScreen, InputProcessor, ServerMessageProcessor { it.modelInstance.transform.setTranslation(x, y, z) - modelBatch.render(it.modelInstance, environment); + modelBatch.render(it.modelInstance, environment) } modelBatch.end() @@ -344,18 +368,19 @@ class Game3dScreen : KtxScreen, InputProcessor, ServerMessageProcessor { for (i in startIndex .. MAX_MODELS) { gameConfig[i]?.let { config -> config.value.let { - config.resource = Md2ModelLoader(locator).loadStaticMd2Model(it, skinIndex = 0, frameIndex = 0) + val md2 = Md2ModelLoader(locator).loadMd2ModelData(it, skinIndex = 0)!! + config.resource = createModel(md2.mesh, md2.material) } } } // temporary: load one fixed player model - playerModel = Md2ModelLoader(locator).loadStaticMd2Model( + val playerModelData = Md2ModelLoader(locator).loadMd2ModelData( modelName = playerModelPath, playerSkin = playerSkinPath, skinIndex = 0, - frameIndex = 0 )!! + playerModel = createModel(playerModelData.mesh, playerModelData.material) gameConfig.getSounds().forEach { config -> if (config != null) { diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt index 0797bc0f..3e15205c 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CakeModelViewer.kt @@ -68,8 +68,8 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } "md2" -> { - val md2 = Md2ModelLoader(locator).loadAnimatedModel(file.absolutePath, null, 0)!! - val model = createModel(md2.mesh, md2.diffuse, md2.vat) + val md2 = Md2ModelLoader(locator).loadMd2ModelData(file.absolutePath, null, 0)!! + val model = createModel(md2.mesh, md2.material) md2Instance = ModelInstance(model) // ok md2Instance!!.userData = Md2CustomData( 0, @@ -96,11 +96,16 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } "bsp" -> { modelBatch = ModelBatch() + // todo: fix bsp back! // models.add(BspLoader().loadBSPModelWireFrame(file).transformQ2toLibgdx()) // models.addAll(BspLoader(gameDir).loadBspModels(file)) instances.add(createOriginArrows(GRID_SIZE)) instances.add(createGrid(GRID_SIZE, GRID_DIVISIONS)) } + else -> { + println("Unsupported file format ${file.extension}") + Gdx.app.exit() + } } batch = SpriteBatch() @@ -144,7 +149,7 @@ class CakeModelViewer(val args: Array) : ApplicationAdapter() { } frameTime = measureTimeMillis { - Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1f) + Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1f) Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) camera.update() diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt deleted file mode 100644 index fd4fc593..00000000 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.demoth.cake.modelviewer - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.graphics.Mesh -import com.badlogic.gdx.graphics.Pixmap -import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.TextureData -import com.badlogic.gdx.utils.Disposable -import com.badlogic.gdx.utils.GdxRuntimeException -import java.nio.Buffer - -// Helper class to create TextureData from a vertex position data buffer -class CustomTextureData( - private val width: Int, - private val height: Int, - private val glInternalFormat: Int, - private val glFormat: Int, - private val glType: Int, - private val buffer: Buffer? -) : TextureData { - private var isPrepared = false - - override fun getType(): TextureData.TextureDataType { - // means it doesn't rely on the pixmap format - return TextureData.TextureDataType.Custom - } - - override fun isPrepared(): Boolean { - return isPrepared - } - - override fun prepare() { - if (isPrepared) throw GdxRuntimeException("Already prepared!") - isPrepared = true - } - - override fun consumePixmap(): Pixmap? { - throw GdxRuntimeException("This TextureData does not return a Pixmap") - } - - override fun disposePixmap(): Boolean { - return false - } - - - override fun consumeCustomData(target: Int) { - if (!isPrepared) throw GdxRuntimeException("Call prepare() first") - - Gdx.gl.glTexImage2D( - /* target = */ target, - /* level = */ 0, - /* internalformat = */ glInternalFormat, - /* width = */ width, - /* height = */ height, - /* border = */ 0, - /* format = */ glFormat, - /* type = */ glType, - /* pixels = */ buffer - ) - } - - override fun getWidth(): Int { - return width - } - - override fun getHeight(): Int { - return height - } - - override fun getFormat(): Pixmap.Format? { - throw GdxRuntimeException("This TextureData does not use a Pixmap") - } - - override fun useMipMaps(): Boolean { - return false - } - - override fun isManaged(): Boolean { - return true // LibGDX will manage this texture - } -} - -class Md2ShaderModel( - val mesh: Mesh, - val vat: Texture, - val diffuse: Texture, -): Disposable { - - val frames = vat.height - - override fun dispose() { - mesh.dispose() - vat.dispose() - diffuse.dispose() - } -} \ No newline at end of file diff --git a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt index f8d605b3..9fe4cac2 100644 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2ModelLoader.kt @@ -1,5 +1,6 @@ package org.demoth.cake.modelviewer +import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES import com.badlogic.gdx.graphics.VertexAttribute.TexCoords @@ -9,66 +10,24 @@ import com.badlogic.gdx.graphics.g3d.Model import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute.Diffuse import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder +import com.badlogic.gdx.utils.GdxRuntimeException import jake2.qcommon.filesystem.Md2Model import jake2.qcommon.filesystem.Md2VertexData import jake2.qcommon.filesystem.PCX import jake2.qcommon.filesystem.buildVertexData import org.demoth.cake.ResourceLocator +import java.nio.Buffer import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer class Md2ModelLoader(private val locator: ResourceLocator) { - fun loadStaticMd2Model( - modelName: String, - playerSkin: String? = null, - skinIndex: Int = 0, - frameIndex: Int = 0, - ): Model? { - val findModel = locator.findModel(modelName) - ?: return null - val md2Model: Md2Model = readMd2Model(findModel) - - val skins = md2Model.skinNames.map { - locator.findSkin(it) - } - - val modelBuilder = ModelBuilder() - modelBuilder.begin() - val modelSkin: ByteArray = if (skins.isNotEmpty()) { - skins[skinIndex] - } else { - if (playerSkin != null) { - locator.findSkin(playerSkin) - } else throw IllegalStateException("No skin found in the model, no player skin provided") - } - val meshBuilder = modelBuilder.part( - "part1", - GL_TRIANGLES, - VertexAttributes( - VertexAttribute.Position(), // 3 floats per vertex - VertexAttribute.TexCoords(0) // 2 floats per vertex - ), - Material( - TextureAttribute( - TextureAttribute.Diffuse, - Texture(PCXTextureData(fromPCX(PCX(modelSkin)))), - ) - ) - ) - val frameVertices = md2Model.getFrameVertices(frameIndex) - val size = frameVertices.size / 5 // 5 floats per vertex : fixme: not great - meshBuilder.addMesh(frameVertices, (0..