Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e6bb3fb
Implement md2 model animation (VAT) #117: restructuring & refactoring
demoth May 11, 2025
807c9d8
Implement md2 model animation (VAT) #117: fix glsl data types
demoth Jun 7, 2025
37a67fa
Implement md2 model animation (VAT) #117: wip vertex data restructuring
demoth Jun 7, 2025
a6c96cd
Implement md2 model animation (VAT) #117: wip vertex data restructuring
demoth Jun 7, 2025
4bdbe8f
Implement md2 model animation (VAT) #117: vertex data ready
demoth Jun 8, 2025
2baac70
Implement md2 model animation (VAT) #117: md2 loader
demoth Jun 8, 2025
0751c3b
Implement md2 model animation (VAT) #117: vat animation
demoth Jun 8, 2025
fe3503c
Implement md2 model animation (VAT) #117: vat animation
demoth Jun 10, 2025
7f4e324
Implement md2 model animation (VAT) #117: vat animation
demoth Jun 10, 2025
08b3f77
Implement md2 model animation (VAT) #117: vat animation
demoth Jun 10, 2025
83a6415
Implement md2 model animation (VAT) #117: vat animation
demoth Jun 10, 2025
17c2da7
Implement md2 model animation (VAT) #117: enable depth testing & culling
demoth Jun 10, 2025
2f760b5
Implement md2 model animation (VAT) #117: animated model!!!
demoth Jun 10, 2025
d81860d
feat: shader test - model viewer controls and camera fixes
demoth Jun 10, 2025
745668e
feat: shader test - add animation controls and frame stepping
demoth Jun 10, 2025
008a0dc
chore: fix md2 vertex data test: unpack vertices
demoth Jun 10, 2025
741a3cf
wip: integrating md2 shaders into libgdx pipeline
demoth Jun 14, 2025
e5d26ec
Implement md2 model animation (VAT) #117: wip md2 shader
demoth Jun 15, 2025
fd13286
use the freshly implemented shader, it works!! :exploding_head:
demoth Jun 15, 2025
41e06cf
cleanup of code from the initial md2 animation implementation
demoth Jun 15, 2025
3a5713d
documentation for md2 shader
demoth Jun 15, 2025
bf23dfb
use default libgdx fragment shader
demoth Jun 15, 2025
83417ea
Cake Model Viewer 1.4: Add MD2 animation
demoth Jun 15, 2025
89c8fbf
Incorporate MD2 animation improvements into the game
demoth Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions assets/shaders/md2-fragment.glsl

This file was deleted.

22 changes: 10 additions & 12 deletions assets/shaders/vat.glsl
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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;
}
37 changes: 31 additions & 6 deletions cake/core/src/main/java/org/demoth/cake/stages/Game3dScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Int, ClientCommands> = mutableMapOf(
Expand Down Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,16 +34,16 @@ class CakeModelViewer(val args: Array<String>) : ApplicationAdapter() {
private lateinit var camera: Camera
private lateinit var cameraInputController: CameraInputController
// rendered with the default libgdx shader
private val models: MutableList<ModelInstance> = mutableListOf()
private val instances: MutableList<ModelInstance> = 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() {

Expand All @@ -68,35 +68,48 @@ class CakeModelViewer(val args: Array<String>) : 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
Expand All @@ -113,33 +126,43 @@ class CakeModelViewer(val args: Array<String>) : 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)
Expand All @@ -160,11 +183,26 @@ class CakeModelViewer(val args: Array<String>) : 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
Expand Down
Loading