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 52cc1107..1c15c15b 100644 --- a/assets/shaders/vat.glsl +++ b/assets/shaders/vat.glsl @@ -1,24 +1,22 @@ -#version 130 // required by gl_VertexID - -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 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; +varying vec2 v_diffuseUV; 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) @@ -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/java/org/demoth/cake/stages/Game3dScreen.kt b/cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt index 876d578d..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).loadMd2Model(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).loadMd2Model( + 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 d3089a9c..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 @@ -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,48 @@ 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).loadMd2ModelData(file.absolutePath, null, 0)!! + val model = createModel(md2.mesh, md2.material) + 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() + // todo: fix bsp back! // 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)) + } + else -> { + println("Unsupported file format ${file.extension}") + Gdx.app.exit() } } 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 +126,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()) } - Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1f) + md2Instance!!.getMd2CustomData().interpolation = md2AnimationTime / md2AnimationFrameTime + + } + + frameTime = measureTimeMillis { + 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(); + camera.update() - if (models.isNotEmpty()) { + if (instances.isNotEmpty()) { modelBatch.begin(camera) - models.forEach { + instances.forEach { modelBatch.render(it) } modelBatch.end() } - 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 +183,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/CustomTextureData.kt b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt deleted file mode 100644 index 253a0b65..00000000 --- a/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/CustomTextureData.kt +++ /dev/null @@ -1,130 +0,0 @@ -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 -import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.TextureData -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 - -// 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( - // model related, managed resources, todo: extract into a model class - private val mesh: Mesh, - private val vat: Pair, - private val diffuse: Pair, - // instance related, mutable state - var frame1: Int = 0, - var frame2: Int = 1, - var interpolation: Float = 0.0f, - val entityTransform: Matrix4 = Matrix4() -): Disposable { - - private val textureWidth = vat.first.width.toFloat() - private val textureHeight = vat.first.height.toFloat() - - 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.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.setUniformf("u_textureHeight", textureHeight) // number of frames - shader.setUniformf("u_textureWidth", textureWidth) // number of vertices - - vat.first.bind(vat.second) - diffuse.first.bind(diffuse.second) - - mesh.render(shader, GL20.GL_TRIANGLES) - } - - override fun dispose() { - mesh.dispose() - vat.first.dispose() - diffuse.first.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 b913d5e5..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,128 +1,183 @@ package org.demoth.cake.modelviewer -import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.* import com.badlogic.gdx.graphics.GL20.GL_TRIANGLES -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.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.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 loadMd2Model( + + fun loadMd2ModelData( modelName: String, playerSkin: String? = null, - skinIndex: Int = 0, - frameIndex: Int = 0, - ): Model? { - val findModel = locator.findModel(modelName) - ?: return null + skinIndex: Int, + ): Md2ModelData? { + 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, + + val diffuse = Texture(PCXTextureData(fromPCX(PCX(modelSkin)))) + + val vertexData = buildVertexData(md2Model.glCommands, md2Model.frames) + + val mesh = Mesh( + true, + vertexData.vertexAttributes.size, + vertexData.indices.size, VertexAttributes( - VertexAttribute.Position(), // 3 floats per vertex - VertexAttribute.TexCoords(0) // 2 floats per vertex - ), - Material( - TextureAttribute( - TextureAttribute.Diffuse, - Texture(PCXTextureData(fromPCX(PCX(modelSkin)))), - ) + VertexAttribute(Generic, 1, "a_vat_index"), + TexCoords(1) // in future, normals can also be added here ) ) - val frameVertices = md2Model.getFrameVertices(frameIndex) - 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 +class Md2ModelData(val mesh: Mesh, val material: Material, val frames: Int) + +// Helper class to create TextureData from a vertex position data buffer +private 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 ) - md2Model.glCommands.forEach { - it.vertices - } - return TODO() } + + 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 + } +} + +fun createModel(mesh: Mesh, material: Material): Model { + return ModelBuilder().apply { + begin() + part("part1", mesh, GL_TRIANGLES, material) + }.end() +} + +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 { 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..37568770 --- /dev/null +++ b/cake/core/src/main/kotlin/org/demoth/cake/modelviewer/Md2Shader.kt @@ -0,0 +1,109 @@ +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 + +data class Md2CustomData( + var frame1: Int, + var frame2: Int, + var interpolation: Float, + 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" + @property:JvmStatic val AnimationTexture: Long = register(AnimationTextureAlias) + @JvmStatic fun init() { + // this is weird? + Mask = Mask or AnimationTexture + } + } +} + +private const val md2ShaderPrefix = """ + #version 130 + #define diffuseTextureFlag +""" + +/** + * 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, +): 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) { + // 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) + + // 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) + } + } + 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 + 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 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 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 95ec720b..00000000 --- a/cake/lwjgl3/src/main/kotlin/org/demoth/cake/lwjgl3/Md2ShaderTest.kt +++ /dev/null @@ -1,201 +0,0 @@ -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.modelviewer.CustomTextureData -import org.demoth.cake.modelviewer.Md2ShaderModel -import java.nio.FloatBuffer - - -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 lateinit var camera: Camera - private lateinit var cameraInputController : CameraInputController - - private var direction = 1f - - private lateinit var md2ShaderModel: Md2ShaderModel - - override fun create() { - camera = PerspectiveCamera(67f, width.toFloat(), height.toFloat()) - cameraInputController = CameraInputController(camera) - Gdx.input.inputProcessor = cameraInputController - md2Shader = createShaderProgram() - - // 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 - ) - } - - 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", md2Shader.log) - Gdx.app.exit() - } - 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) - } - - override fun dispose() { - md2Shader.dispose() - md2ShaderModel.dispose() - } -} - -private const val width = 1024 -private const val height = 768 - -fun main() { - val config = Lwjgl3ApplicationConfiguration() - 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 diff --git a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt index 3911e931..07793290 100644 --- a/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt +++ b/qcommon/src/main/kotlin/jake2/qcommon/filesystem/md2.kt @@ -52,7 +52,7 @@ class Md2Model(buffer: ByteBuffer) { fun getFrameVertices(frame: Int): FloatArray { return glCommands.flatMap { - it.toFloats(frames[frame].points) + it.toVertexAttributes(frames[frame].points) }.toFloatArray() } @@ -100,7 +100,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 @@ -153,6 +153,57 @@ class Md2Model(buffer: ByteBuffer) { } } +fun buildVertexData( + 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, + + // map from (oldIndex, s,t ) to new index + val attributes = glCmds.flatMap { glCmd -> + glCmd.unpack().flatMap { vertex -> + listOf(vertex.index.toFloat(), vertex.s, vertex.t) + } + } + + // flatten vertex positions in all frames + val vertexPositions = mutableListOf() + 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 = 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 + ) + +} + +@Suppress("ArrayInDataClass") +data class Md2VertexData( + // indices to draw GL_TRIANGLES + val indices: ShortArray, + // indexed attributes (at the moment - only text coords) + 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 frames: Int, + val vertices: Int, +) + enum class Md2GlCmdType { TRIANGLE_STRIP, TRIANGLE_FAN, @@ -162,9 +213,9 @@ 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) } } @@ -172,14 +223,47 @@ data class Md2GlCmd( val type: Md2GlCmdType, 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. * * 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 +273,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 +283,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) } } } @@ -217,45 +301,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/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, 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..01a1e5dc --- /dev/null +++ b/qcommon/src/test/kotlin/jake2/qcommon/filesystem/Md2ModelVertexDataTest.kt @@ -0,0 +1,154 @@ +package jake2.qcommon.filesystem + +import jake2.qcommon.math.Vector3f +import junit.framework.TestCase.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 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 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: + * + * 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 * + * + * 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 + * 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 + * + * 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 { + + 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 = buildVertexData(testGlCmds, testFrames) + + // 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, + + 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) + + // todo: add proper test for texture coordinates + assertEquals(testFrames.size * testFrames.first().points.size * 3, actual.vertexPositions.size) + } +}