feat(anim): 实现2D模型动画系统核心类
- 添加AnimationClip类用于管理动画剪辑和关键帧 - 添加AnimationLayer类支持动画层和混合模式 - 实现动画曲线采样和插值算法 - 支持事件标记和动画状态控制 - 添加参数覆盖和权重混合功能 - 实现动画轨道和关键帧管理- 添加多种插值类型支持(线性、步进、平滑、缓入缓出) - 实现动画事件系统和监听器模式 - 支持动画剪辑的深拷贝和合并功能 - 添加AnimationParameter类用于动画参数管理
This commit is contained in:
287
src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java
Normal file
287
src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java
Normal file
@@ -0,0 +1,287 @@
|
||||
package com.chuangzhou.vivid2D.test;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWErrorCallback;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 重写后的 ModelRender 测试示例:构造一个简单的人形(头、身体、左右手、左右腿)
|
||||
* 便于验证层级变换与渲染是否正确。
|
||||
*
|
||||
* 注意:依赖你工程里已有的 Model2D / ModelPart / Mesh2D / Texture API。
|
||||
*
|
||||
* @author tzdwindows 7(改)
|
||||
*/
|
||||
public class ModelRenderTest {
|
||||
|
||||
private static final int WINDOW_WIDTH = 800;
|
||||
private static final int WINDOW_HEIGHT = 600;
|
||||
private static final String WINDOW_TITLE = "Vivid2D ModelRender Test - Humanoid";
|
||||
|
||||
private long window;
|
||||
private boolean running = true;
|
||||
|
||||
private Model2D testModel;
|
||||
private Random random = new Random();
|
||||
|
||||
private float animationTime = 0f;
|
||||
private boolean animate = true;
|
||||
|
||||
public static void main(String[] args) {
|
||||
new ModelRenderTest().run();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
init();
|
||||
loop();
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
GLFWErrorCallback.createPrint(System.err).set();
|
||||
|
||||
if (!GLFW.glfwInit()) {
|
||||
throw new IllegalStateException("Unable to initialize GLFW");
|
||||
}
|
||||
|
||||
GLFW.glfwDefaultWindowHints();
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE);
|
||||
|
||||
window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL);
|
||||
if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window");
|
||||
|
||||
GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
|
||||
GLFW.glfwSetWindowPos(window,
|
||||
(vidMode.width() - WINDOW_WIDTH) / 2,
|
||||
(vidMode.height() - WINDOW_HEIGHT) / 2);
|
||||
|
||||
GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> {
|
||||
if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false;
|
||||
if (key == GLFW.GLFW_KEY_SPACE && action == GLFW.GLFW_RELEASE) {
|
||||
animate = !animate;
|
||||
System.out.println("Animation " + (animate ? "enabled" : "disabled"));
|
||||
}
|
||||
if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) randomizeModel();
|
||||
});
|
||||
|
||||
GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h));
|
||||
|
||||
GLFW.glfwMakeContextCurrent(window);
|
||||
GLFW.glfwSwapInterval(1);
|
||||
GLFW.glfwShowWindow(window);
|
||||
|
||||
GL.createCapabilities();
|
||||
|
||||
createTestModel();
|
||||
ModelRender.initialize();
|
||||
|
||||
System.out.println("Test initialized successfully");
|
||||
System.out.println("Controls: ESC exit | SPACE toggle anim | R randomize");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个简单的人形:body 为根,head、arms、legs 为 body 的子节点。
|
||||
* 使用 createPart 保证与 Model2D 管理一致。
|
||||
*/
|
||||
private void createTestModel() {
|
||||
testModel = new Model2D("Humanoid");
|
||||
|
||||
// body 放在屏幕中心
|
||||
ModelPart body = testModel.createPart("body");
|
||||
body.setPosition(400, 320);
|
||||
// 身体网格:宽 80 高 120
|
||||
Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120);
|
||||
bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); // 蓝衣
|
||||
body.addMesh(bodyMesh);
|
||||
|
||||
// head:相对于 body 在上方偏移
|
||||
ModelPart head = testModel.createPart("head");
|
||||
head.setPosition(0, -90); // 注意:如果 body 的坐标是屏幕位置,子部件的 position 是相对父节点(取决于你的实现);这里按常见习惯设负 y 向上
|
||||
Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60);
|
||||
headMesh.setTexture(createHeadTexture());
|
||||
head.addMesh(headMesh);
|
||||
|
||||
// left arm
|
||||
ModelPart leftArm = testModel.createPart("left_arm");
|
||||
leftArm.setPosition(-60, -20); // 在 body 左侧稍上位置
|
||||
Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90);
|
||||
leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); // 手臂颜色
|
||||
leftArm.addMesh(leftArmMesh);
|
||||
|
||||
// right arm
|
||||
ModelPart rightArm = testModel.createPart("right_arm");
|
||||
rightArm.setPosition(60, -20);
|
||||
Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90);
|
||||
rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED));
|
||||
rightArm.addMesh(rightArmMesh);
|
||||
|
||||
// left leg
|
||||
ModelPart leftLeg = testModel.createPart("left_leg");
|
||||
leftLeg.setPosition(-20, 90); // body 下方
|
||||
Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100);
|
||||
leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1));
|
||||
leftLeg.addMesh(leftLegMesh);
|
||||
|
||||
// right leg
|
||||
ModelPart rightLeg = testModel.createPart("right_leg");
|
||||
rightLeg.setPosition(20, 90);
|
||||
Mesh2D rightLegMesh = Mesh2D.createQuad("right_leg_mesh", 20, 100);
|
||||
rightLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1));
|
||||
rightLeg.addMesh(rightLegMesh);
|
||||
|
||||
// 建立层级:body 为根,其他作为 body 的子节点
|
||||
//testModel.addPart(body);
|
||||
body.addChild(head);
|
||||
body.addChild(leftArm);
|
||||
body.addChild(rightArm);
|
||||
body.addChild(leftLeg);
|
||||
body.addChild(rightLeg);
|
||||
|
||||
// 创建动画参数用于简单摆动
|
||||
testModel.createParameter("arm_swing", -1.0f, 1.0f, 0f);
|
||||
testModel.createParameter("leg_swing", -1.0f, 1.0f, 0f);
|
||||
testModel.createParameter("head_rotation", -0.5f, 0.5f, 0f);
|
||||
|
||||
System.out.println("Humanoid model created with parts: " + testModel.getParts().size());
|
||||
}
|
||||
|
||||
// 辅助:创建身体渐变/纯色纹理(ByteBuffer RGBA)
|
||||
private Texture createSolidTexture(int w, int h, int rgba) {
|
||||
ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4);
|
||||
byte a = (byte) ((rgba >> 24) & 0xFF);
|
||||
byte r = (byte) ((rgba >> 16) & 0xFF);
|
||||
byte g = (byte) ((rgba >> 8) & 0xFF);
|
||||
byte b = (byte) (rgba & 0xFF);
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
buf.put(r).put(g).put(b).put(a);
|
||||
}
|
||||
buf.flip();
|
||||
Texture t = new Texture("solid_" + rgba + "_" + w + "x" + h, w, h, Texture.TextureFormat.RGBA, buf);
|
||||
MemoryUtil.memFree(buf);
|
||||
return t;
|
||||
}
|
||||
|
||||
private Texture createHeadTexture() {
|
||||
int width = 64, height = 64;
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
float dx = (x - width / 2f) / (width / 2f);
|
||||
float dy = (y - height / 2f) / (height / 2f);
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
int alpha = dist > 1.0f ? 0 : 255;
|
||||
int r = (int) (240 * (1.0f - dist * 0.25f));
|
||||
int g = (int) (200 * (1.0f - dist * 0.25f));
|
||||
int b = (int) (180 * (1.0f - dist * 0.25f));
|
||||
pixels[y * width + x] = (alpha << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
return new Texture("head_tex", width, height, Texture.TextureFormat.RGBA, pixels);
|
||||
}
|
||||
|
||||
private void loop() {
|
||||
long last = System.nanoTime();
|
||||
double nsPerUpdate = 1_000_000_000.0 / 60.0;
|
||||
double accumulator = 0.0;
|
||||
|
||||
System.out.println("Entering main loop...");
|
||||
|
||||
while (running && !GLFW.glfwWindowShouldClose(window)) {
|
||||
long now = System.nanoTime();
|
||||
accumulator += (now - last) / nsPerUpdate;
|
||||
last = now;
|
||||
|
||||
while (accumulator >= 1.0) {
|
||||
update(1.0f / 60.0f);
|
||||
accumulator -= 1.0;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
GLFW.glfwSwapBuffers(window);
|
||||
GLFW.glfwPollEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private void update(float dt) {
|
||||
if (!animate) return;
|
||||
|
||||
animationTime += dt;
|
||||
float armSwing = (float) Math.sin(animationTime * 3.0f) * 0.7f; // -0.7 .. 0.7
|
||||
float legSwing = (float) Math.sin(animationTime * 3.0f + Math.PI) * 0.6f;
|
||||
float headRot = (float) Math.sin(animationTime * 1.4f) * 0.15f;
|
||||
|
||||
testModel.setParameterValue("arm_swing", armSwing);
|
||||
testModel.setParameterValue("leg_swing", legSwing);
|
||||
testModel.setParameterValue("head_rotation", headRot);
|
||||
|
||||
// 将参数应用到部件(直接通过 API 设置即可)
|
||||
ModelPart leftArm = testModel.getPart("left_arm");
|
||||
ModelPart rightArm = testModel.getPart("right_arm");
|
||||
ModelPart leftLeg = testModel.getPart("left_leg");
|
||||
ModelPart rightLeg = testModel.getPart("right_leg");
|
||||
ModelPart head = testModel.getPart("head");
|
||||
|
||||
if (leftArm != null) leftArm.setRotation(-0.8f * armSwing - 0.2f);
|
||||
if (rightArm != null) rightArm.setRotation(0.8f * armSwing + 0.2f);
|
||||
if (leftLeg != null) leftLeg.setRotation(0.6f * legSwing);
|
||||
if (rightLeg != null) rightLeg.setRotation(-0.6f * legSwing);
|
||||
if (head != null) head.setRotation(headRot);
|
||||
|
||||
testModel.update(dt);
|
||||
}
|
||||
|
||||
private void render() {
|
||||
ModelRender.setClearColor(0.18f, 0.18f, 0.25f, 1.0f);
|
||||
ModelRender.render(1.0f / 60.0f, testModel);
|
||||
|
||||
// 每 5 秒输出一次统计
|
||||
if ((int) (animationTime) % 5 == 0 && (animationTime - (int) animationTime) < 0.016) {
|
||||
//System.out.println("Render stats: meshes=" + ModelRender.getRenderStats());
|
||||
}
|
||||
}
|
||||
|
||||
private void randomizeModel() {
|
||||
System.out.println("Randomizing model...");
|
||||
ModelPart body = testModel.getPart("body");
|
||||
if (body != null) {
|
||||
body.setPosition(200 + random.nextInt(400), 200 + random.nextInt(200));
|
||||
}
|
||||
for (ModelPart p : testModel.getParts()) {
|
||||
p.setRotation((float) (random.nextFloat() * Math.PI * 2));
|
||||
if (p.getName().equals("head")) {
|
||||
p.setOpacity(0.6f + random.nextFloat() * 0.4f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
System.out.println("Cleaning up resources...");
|
||||
ModelRender.cleanup();
|
||||
Texture.cleanupAll();
|
||||
if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window);
|
||||
GLFW.glfwTerminate();
|
||||
GLFW.glfwSetErrorCallback(null).free();
|
||||
System.out.println("Test completed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user