feat(anim): 实现2D模型动画系统核心类
- 添加AnimationClip类用于管理动画剪辑和关键帧 - 添加AnimationLayer类支持动画层和混合模式 - 实现动画曲线采样和插值算法 - 支持事件标记和动画状态控制 - 添加参数覆盖和权重混合功能 - 实现动画轨道和关键帧管理- 添加多种插值类型支持(线性、步进、平滑、缓入缓出) - 实现动画事件系统和监听器模式 - 支持动画剪辑的深拷贝和合并功能 - 添加AnimationParameter类用于动画参数管理
This commit is contained in:
@@ -82,11 +82,14 @@ dependencies {
|
||||
implementation 'com.1stleg:jnativehook:2.1.0'
|
||||
implementation 'org.json:json:20230618'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-stb:3.3.3'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1:natives-windows'
|
||||
implementation 'com.badlogicgames.gdx:gdx:1.12.1'
|
||||
implementation 'org.joml:joml:1.10.7'
|
||||
implementation 'org.bytedeco:javacv-platform:1.5.7'
|
||||
implementation 'org.bytedeco:javacpp-platform:1.5.7'
|
||||
implementation 'com.madgag:animated-gif-lib:1.4'
|
||||
|
||||
@@ -1287,7 +1287,7 @@ public class RegistrationSettingsItem extends WindowsJDialog {
|
||||
applyLanguageSettings(settingsManager);
|
||||
|
||||
// 5. 应用CUDA设置
|
||||
applyCUDASettings(settingsManager);
|
||||
//applyCUDASettings(settingsManager);
|
||||
|
||||
logger.info("所有设置配置已成功应用");
|
||||
|
||||
|
||||
8
src/main/java/com/chuangzhou/vivid2D/Main.java
Normal file
8
src/main/java/com/chuangzhou/vivid2D/Main.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package com.chuangzhou.vivid2D;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
}
|
||||
527
src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java
Normal file
527
src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java
Normal file
@@ -0,0 +1,527 @@
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
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.joml.Matrix3f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.*;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 重构后的 ModelRender:更模块化、健壮的渲染子系统
|
||||
*/
|
||||
public final class ModelRender {
|
||||
|
||||
private ModelRender() { /* no instances */ }
|
||||
|
||||
// ================== 全局状态 ==================
|
||||
private static boolean initialized = false;
|
||||
private static int viewportWidth = 800;
|
||||
private static int viewportHeight = 600;
|
||||
private static final Vector4f CLEAR_COLOR = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
private static boolean enableDepthTest = false;
|
||||
private static boolean enableBlending = true;
|
||||
|
||||
// 着色器与资源
|
||||
private static final Map<String, ShaderProgram> shaderMap = new HashMap<>();
|
||||
private static ShaderProgram defaultProgram = null;
|
||||
|
||||
private static final Map<Mesh2D, MeshGLResources> meshResources = new HashMap<>();
|
||||
private static final AtomicInteger textureUnitAllocator = new AtomicInteger(0);
|
||||
|
||||
// 默认白色纹理
|
||||
private static int defaultTextureId = 0;
|
||||
|
||||
// ================== 内部类:ShaderProgram ==================
|
||||
private static class ShaderProgram {
|
||||
final int programId;
|
||||
final Map<String, Integer> uniformCache = new HashMap<>();
|
||||
|
||||
ShaderProgram(int programId) {
|
||||
this.programId = programId;
|
||||
}
|
||||
|
||||
void use() {
|
||||
GL20.glUseProgram(programId);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
GL20.glUseProgram(0);
|
||||
}
|
||||
|
||||
int getUniformLocation(String name) {
|
||||
return uniformCache.computeIfAbsent(name, k -> {
|
||||
int loc = GL20.glGetUniformLocation(programId, k);
|
||||
if (loc == -1) {
|
||||
// debug 时可以打开
|
||||
// System.err.println("Warning: uniform not found: " + k);
|
||||
}
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
|
||||
void delete() {
|
||||
if (GL20.glIsProgram(programId)) GL20.glDeleteProgram(programId);
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 内部类:MeshGLResources ==================
|
||||
private static class MeshGLResources {
|
||||
int vao = 0;
|
||||
int vbo = 0;
|
||||
int ebo = 0;
|
||||
int vertexCount = 0;
|
||||
boolean initialized = false;
|
||||
|
||||
void dispose() {
|
||||
if (ebo != 0) { GL15.glDeleteBuffers(ebo); ebo = 0; }
|
||||
if (vbo != 0) { GL15.glDeleteBuffers(vbo); vbo = 0; }
|
||||
if (vao != 0) { GL30.glDeleteVertexArrays(vao); vao = 0; }
|
||||
initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 着色器源 ==================
|
||||
private static final String VERTEX_SHADER_SRC =
|
||||
"#version 330 core\n" +
|
||||
"layout(location = 0) in vec2 aPosition;\n" +
|
||||
"layout(location = 1) in vec2 aTexCoord;\n" +
|
||||
"out vec2 vTexCoord;\n" +
|
||||
"uniform mat3 uModelMatrix;\n" +
|
||||
"uniform mat3 uViewMatrix;\n" +
|
||||
"uniform mat3 uProjectionMatrix;\n" +
|
||||
"void main() {\n" +
|
||||
" vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);\n" +
|
||||
" gl_Position = vec4(p.xy, 0.0, 1.0);\n" +
|
||||
" vTexCoord = aTexCoord;\n" +
|
||||
"}";
|
||||
|
||||
private static final String FRAGMENT_SHADER_SRC =
|
||||
"#version 330 core\n" +
|
||||
"in vec2 vTexCoord;\n" +
|
||||
"out vec4 FragColor;\n" +
|
||||
"uniform sampler2D uTexture;\n" +
|
||||
"uniform vec4 uColor;\n" +
|
||||
"uniform float uOpacity;\n" +
|
||||
"uniform int uBlendMode;\n" +
|
||||
"void main() {\n" +
|
||||
" vec4 tex = texture(uTexture, vTexCoord);\n" +
|
||||
" vec4 finalColor = tex * uColor;\n" +
|
||||
" if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb;\n" +
|
||||
" else if (uBlendMode == 2) finalColor.rgb = tex.rgb * uColor.rgb;\n" +
|
||||
" else if (uBlendMode == 3) finalColor.rgb = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb);\n" +
|
||||
" finalColor.a = tex.a * uOpacity;\n" +
|
||||
" if (finalColor.a <= 0.001) discard;\n" +
|
||||
" FragColor = finalColor;\n" +
|
||||
"}";
|
||||
|
||||
// ================== 初始化 / 清理 ==================
|
||||
public static synchronized void initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
System.out.println("Initializing ModelRender...");
|
||||
|
||||
// 需要在外部创建 OpenGL 上下文并调用 GL.createCapabilities()
|
||||
logGLInfo();
|
||||
|
||||
// 初始 GL 状态
|
||||
setupGLState();
|
||||
|
||||
// 创建默认 shader
|
||||
try {
|
||||
compileDefaultShader();
|
||||
} catch (RuntimeException ex) {
|
||||
System.err.println("Failed to compile default shader: " + ex.getMessage());
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// 创建默认纹理
|
||||
createDefaultTexture();
|
||||
|
||||
// 初始化视口
|
||||
GL11.glViewport(0, 0, viewportWidth, viewportHeight);
|
||||
|
||||
initialized = true;
|
||||
System.out.println("ModelRender initialized successfully");
|
||||
}
|
||||
|
||||
private static void logGLInfo() {
|
||||
System.out.println("OpenGL Vendor: " + GL11.glGetString(GL11.GL_VENDOR));
|
||||
System.out.println("OpenGL Renderer: " + GL11.glGetString(GL11.GL_RENDERER));
|
||||
System.out.println("OpenGL Version: " + GL11.glGetString(GL11.GL_VERSION));
|
||||
System.out.println("GLSL Version: " + GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION));
|
||||
}
|
||||
|
||||
private static void setupGLState() {
|
||||
GL11.glClearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w);
|
||||
|
||||
if (enableBlending) {
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
}
|
||||
|
||||
if (enableDepthTest) {
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
GL11.glDepthFunc(GL11.GL_LEQUAL);
|
||||
} else {
|
||||
GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
GL11.glDisable(GL11.GL_CULL_FACE);
|
||||
|
||||
checkGLError("setupGLState");
|
||||
}
|
||||
|
||||
private static void compileDefaultShader() {
|
||||
int vs = compileShader(GL20.GL_VERTEX_SHADER, VERTEX_SHADER_SRC);
|
||||
int fs = compileShader(GL20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_SRC);
|
||||
int prog = linkProgram(vs, fs);
|
||||
ShaderProgram sp = new ShaderProgram(prog);
|
||||
shaderMap.put("default", sp);
|
||||
defaultProgram = sp;
|
||||
|
||||
// 设置一些默认 uniform(需要先 use)
|
||||
sp.use();
|
||||
setUniformIntInternal(sp, "uTexture", 0);
|
||||
setUniformFloatInternal(sp, "uOpacity", 1.0f);
|
||||
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
|
||||
setUniformIntInternal(sp, "uBlendMode", 0);
|
||||
sp.stop();
|
||||
}
|
||||
|
||||
private static int compileShader(int type, String src) {
|
||||
int shader = GL20.glCreateShader(type);
|
||||
GL20.glShaderSource(shader, src);
|
||||
GL20.glCompileShader(shader);
|
||||
int status = GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS);
|
||||
if (status == GL11.GL_FALSE) {
|
||||
String log = GL20.glGetShaderInfoLog(shader);
|
||||
GL20.glDeleteShader(shader);
|
||||
throw new RuntimeException("Shader compilation failed: " + log);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
private static int linkProgram(int vs, int fs) {
|
||||
int prog = GL20.glCreateProgram();
|
||||
GL20.glAttachShader(prog, vs);
|
||||
GL20.glAttachShader(prog, fs);
|
||||
GL20.glLinkProgram(prog);
|
||||
int status = GL20.glGetProgrami(prog, GL20.GL_LINK_STATUS);
|
||||
if (status == GL11.GL_FALSE) {
|
||||
String log = GL20.glGetProgramInfoLog(prog);
|
||||
GL20.glDeleteProgram(prog);
|
||||
throw new RuntimeException("Program link failed: " + log);
|
||||
}
|
||||
// shaders can be deleted after linking
|
||||
GL20.glDetachShader(prog, vs);
|
||||
GL20.glDetachShader(prog, fs);
|
||||
GL20.glDeleteShader(vs);
|
||||
GL20.glDeleteShader(fs);
|
||||
return prog;
|
||||
}
|
||||
|
||||
private static void createDefaultTexture() {
|
||||
// 使用 GL11.glGenTextures() 获取单个 id(更直观,避免 IntBuffer 问题)
|
||||
defaultTextureId = GL11.glGenTextures();
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, defaultTextureId);
|
||||
ByteBuffer white = MemoryUtil.memAlloc(4);
|
||||
white.put((byte)255).put((byte)255).put((byte)255).put((byte)255).flip();
|
||||
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, white);
|
||||
MemoryUtil.memFree(white);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
|
||||
checkGLError("createDefaultTexture");
|
||||
}
|
||||
|
||||
public static synchronized void cleanup() {
|
||||
if (!initialized) return;
|
||||
|
||||
System.out.println("Cleaning up ModelRender...");
|
||||
|
||||
// mesh resources
|
||||
for (MeshGLResources r : meshResources.values()) r.dispose();
|
||||
meshResources.clear();
|
||||
|
||||
// shaders
|
||||
for (ShaderProgram sp : shaderMap.values()) sp.delete();
|
||||
shaderMap.clear();
|
||||
defaultProgram = null;
|
||||
|
||||
// textures
|
||||
if (defaultTextureId != 0) {
|
||||
GL11.glDeleteTextures(defaultTextureId);
|
||||
defaultTextureId = 0;
|
||||
}
|
||||
|
||||
initialized = false;
|
||||
System.out.println("ModelRender cleaned up");
|
||||
}
|
||||
|
||||
// ================== 渲染流程 ==================
|
||||
public static void render(float deltaTime, Model2D model) {
|
||||
if (!initialized) throw new IllegalStateException("ModelRender not initialized");
|
||||
if (model == null) return;
|
||||
|
||||
// 更新模型(确保 worldTransform 已经被计算)
|
||||
model.update(deltaTime);
|
||||
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0));
|
||||
|
||||
// 使用默认 shader(保持绑定直到完成渲染)
|
||||
defaultProgram.use();
|
||||
|
||||
// 设置投影与视图(3x3 正交投影用于 2D)
|
||||
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
|
||||
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
|
||||
setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity());
|
||||
|
||||
// 递归渲染所有根部件(使用 3x3 矩阵)
|
||||
Matrix3f identity = new Matrix3f().identity();
|
||||
for (ModelPart p : model.getParts()) {
|
||||
if (p.getParent() != null) continue;
|
||||
renderPartRecursive(p, identity);
|
||||
}
|
||||
|
||||
defaultProgram.stop();
|
||||
|
||||
checkGLError("render");
|
||||
}
|
||||
|
||||
private static void renderPartRecursive(ModelPart part, Matrix3f parentMat) {
|
||||
// 使用 part 内置的局部矩阵(localTransform),并与 parentMat 相乘得到 world 矩阵
|
||||
Matrix3f local = part.getLocalTransform(); // 返回 copy
|
||||
Matrix3f world = new Matrix3f(parentMat).mul(local); // world = parent * local
|
||||
|
||||
// 把 world 矩阵传入 shader(使用 3x3)
|
||||
setUniformMatrix3(defaultProgram, "uModelMatrix", world);
|
||||
|
||||
// 设置部件相关 uniform(opacity / blend / color)
|
||||
setPartUniforms(defaultProgram, part);
|
||||
|
||||
// 绘制该部件的所有网格(使用 ModelRender 的 renderMesh)
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
renderMesh(mesh);
|
||||
}
|
||||
|
||||
// 递归绘制子节点(传入当前 world 矩阵)
|
||||
for (ModelPart child : part.getChildren()) {
|
||||
renderPartRecursive(child, world);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderMesh(Mesh2D mesh) {
|
||||
// 确保 mesh 的 GL 资源已上传(ModelRender 管理 upload)
|
||||
MeshGLResources res = meshResources.computeIfAbsent(mesh, k -> new MeshGLResources());
|
||||
if (!res.initialized) uploadMeshData(mesh, res);
|
||||
|
||||
// 绑定纹理到单元0(我们使用 0 固定)
|
||||
Texture tex = mesh.getTexture();
|
||||
int texId = (tex != null && !tex.isDisposed()) ? tex.getTextureId() : defaultTextureId;
|
||||
|
||||
// active unit & bind — 确保 shader 已被 use()(调用者保证)
|
||||
GL13.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texId);
|
||||
// 将 sampler 设为 0(内部函数保证 program 绑定)
|
||||
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
||||
|
||||
// 绑定 VAO 并绘制
|
||||
GL30.glBindVertexArray(res.vao);
|
||||
int drawMode = getGLDrawMode(mesh.getDrawMode());
|
||||
if (mesh.getIndices().length > 0 &&
|
||||
(drawMode == GL11.GL_TRIANGLES || drawMode == GL11.GL_TRIANGLE_STRIP || drawMode == GL11.GL_TRIANGLE_FAN)) {
|
||||
GL11.glDrawElements(drawMode, mesh.getIndices().length, GL11.GL_UNSIGNED_INT, 0);
|
||||
} else {
|
||||
GL11.glDrawArrays(drawMode, 0, res.vertexCount);
|
||||
}
|
||||
GL30.glBindVertexArray(0);
|
||||
|
||||
// 解绑纹理(避免污染后续 state)
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
|
||||
checkGLError("renderMesh");
|
||||
}
|
||||
|
||||
private static int getGLDrawMode(int meshDrawMode) {
|
||||
switch (meshDrawMode) {
|
||||
case Mesh2D.POINTS: return GL11.GL_POINTS;
|
||||
case Mesh2D.LINES: return GL11.GL_LINES;
|
||||
case Mesh2D.LINE_STRIP: return GL11.GL_LINE_STRIP;
|
||||
case Mesh2D.TRIANGLES: return GL11.GL_TRIANGLES;
|
||||
case Mesh2D.TRIANGLE_STRIP: return GL11.GL_TRIANGLE_STRIP;
|
||||
case Mesh2D.TRIANGLE_FAN: return GL11.GL_TRIANGLE_FAN;
|
||||
default: return GL11.GL_TRIANGLES;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 上传数据 ==================
|
||||
private static void uploadMeshData(Mesh2D mesh, MeshGLResources res) {
|
||||
System.out.println("Uploading mesh data: " + mesh.getName());
|
||||
|
||||
res.vao = GL30.glGenVertexArrays();
|
||||
GL30.glBindVertexArray(res.vao);
|
||||
|
||||
res.vbo = GL15.glGenBuffers();
|
||||
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, res.vbo);
|
||||
|
||||
float[] verts = mesh.getVertices();
|
||||
float[] uvs = mesh.getUVs();
|
||||
int vertexCount = mesh.getVertexCount();
|
||||
if (verts == null || verts.length == 0) throw new IllegalStateException("Mesh has no vertices: " + mesh.getName());
|
||||
|
||||
FloatBuffer inter = MemoryUtil.memAllocFloat(vertexCount * 4);
|
||||
for (int i = 0; i < vertexCount; i++) {
|
||||
inter.put(verts[i*2]);
|
||||
inter.put(verts[i*2+1]);
|
||||
inter.put(uvs[i*2]);
|
||||
inter.put(uvs[i*2+1]);
|
||||
}
|
||||
inter.flip();
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, inter, GL15.GL_STATIC_DRAW);
|
||||
MemoryUtil.memFree(inter);
|
||||
|
||||
// 设置 attribute(位置 / uv),layout 已在 shader 中固定
|
||||
int stride = 4 * Float.BYTES;
|
||||
GL20.glEnableVertexAttribArray(0);
|
||||
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, stride, 0);
|
||||
GL20.glEnableVertexAttribArray(1);
|
||||
GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, stride, 2 * Float.BYTES);
|
||||
|
||||
int[] indices = mesh.getIndices();
|
||||
if (indices != null && indices.length > 0) {
|
||||
res.ebo = GL15.glGenBuffers();
|
||||
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, res.ebo);
|
||||
IntBuffer ib = MemoryUtil.memAllocInt(indices.length);
|
||||
ib.put(indices).flip();
|
||||
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, ib, GL15.GL_STATIC_DRAW);
|
||||
MemoryUtil.memFree(ib);
|
||||
res.vertexCount = indices.length; // drawElements 使用 count
|
||||
} else {
|
||||
res.vertexCount = vertexCount;
|
||||
}
|
||||
|
||||
// 不解绑 ELEMENT_ARRAY_BUFFER(它属于 VAO),解绑 ARRAY_BUFFER
|
||||
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GL30.glBindVertexArray(0);
|
||||
|
||||
res.initialized = true;
|
||||
checkGLError("uploadMeshData");
|
||||
System.out.println("Uploaded mesh: " + mesh.getName() + " (v=" + vertexCount + ")");
|
||||
}
|
||||
|
||||
// ================== uniform 设置辅助(内部使用,确保 program 已绑定) ==================
|
||||
private static void setUniformIntInternal(ShaderProgram sp, String name, int value) {
|
||||
int loc = sp.getUniformLocation(name);
|
||||
if (loc != -1) GL20.glUniform1i(loc, value);
|
||||
}
|
||||
|
||||
private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) {
|
||||
int loc = sp.getUniformLocation(name);
|
||||
if (loc != -1) GL20.glUniform1f(loc, value);
|
||||
}
|
||||
|
||||
private static void setUniformVec4Internal(ShaderProgram sp, String name, Vector4f vec) {
|
||||
int loc = sp.getUniformLocation(name);
|
||||
if (loc != -1) GL20.glUniform4f(loc, vec.x, vec.y, vec.z, vec.w);
|
||||
}
|
||||
|
||||
private static void setUniformMatrix3(ShaderProgram sp, String name, Matrix3f m) {
|
||||
int loc = sp.getUniformLocation(name);
|
||||
if (loc == -1) return;
|
||||
FloatBuffer fb = MemoryUtil.memAllocFloat(9);
|
||||
try {
|
||||
m.get(fb);
|
||||
GL20.glUniformMatrix3fv(loc, false, fb);
|
||||
} finally {
|
||||
MemoryUtil.memFree(fb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 外部可用的统一设置(会自动切换到默认程序)
|
||||
private static void setUniformInt(String name, int value) {
|
||||
defaultProgram.use();
|
||||
setUniformIntInternal(defaultProgram, name, value);
|
||||
defaultProgram.stop();
|
||||
}
|
||||
private static void setUniformFloat(String name, float value) {
|
||||
defaultProgram.use();
|
||||
setUniformFloatInternal(defaultProgram, name, value);
|
||||
defaultProgram.stop();
|
||||
}
|
||||
private static void setUniformVec4(String name, Vector4f v) {
|
||||
defaultProgram.use();
|
||||
setUniformVec4Internal(defaultProgram, name, v);
|
||||
defaultProgram.stop();
|
||||
}
|
||||
|
||||
// ================== 部件属性 ==================
|
||||
private static void setPartUniforms(ShaderProgram sp, ModelPart part) {
|
||||
setUniformFloatInternal(sp, "uOpacity", part.getOpacity());
|
||||
int blend = 0;
|
||||
switch (part.getBlendMode()) {
|
||||
case ADDITIVE: blend = 1; break;
|
||||
case MULTIPLY: blend = 2; break;
|
||||
case SCREEN: blend = 3; break;
|
||||
case NORMAL: default: blend = 0;
|
||||
}
|
||||
setUniformIntInternal(sp, "uBlendMode", blend);
|
||||
// 这里保留为白色,若需要部件 tint 请替换为 part 的 color 属性
|
||||
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
|
||||
}
|
||||
|
||||
// ================== 工具 ==================
|
||||
private static Matrix3f buildOrthoProjection(int width, int height) {
|
||||
Matrix3f m = new Matrix3f();
|
||||
m.set(
|
||||
2.0f / width, 0.0f, -1.0f,
|
||||
0.0f, -2.0f / height, 1.0f,
|
||||
0.0f, 0.0f, 1.0f
|
||||
);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static void setViewport(int width, int height) {
|
||||
viewportWidth = Math.max(1, width);
|
||||
viewportHeight = Math.max(1, height);
|
||||
GL11.glViewport(0, 0, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
public static void setClearColor(float r, float g, float b, float a) {
|
||||
GL11.glClearColor(r,g,b,a);
|
||||
}
|
||||
|
||||
private static void checkGLError(String op) {
|
||||
int e = GL11.glGetError();
|
||||
if (e != GL11.GL_NO_ERROR) {
|
||||
System.err.println("OpenGL error during " + op + ": " + getGLErrorString(e));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getGLErrorString(int err) {
|
||||
switch (err) {
|
||||
case GL11.GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||||
case GL11.GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||||
case GL11.GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||||
case GL11.GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||||
default: return "Unknown(0x" + Integer.toHexString(err) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 辅助:外部获取状态 ==================
|
||||
public static boolean isInitialized() { return initialized; }
|
||||
public static int getLoadedMeshCount() { return meshResources.size(); }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
public class AnimationParameter {
|
||||
private String id;
|
||||
private float value;
|
||||
private float defaultValue;
|
||||
private float minValue;
|
||||
private float maxValue;
|
||||
private boolean changed = false;
|
||||
|
||||
public AnimationParameter(String id, float min, float max, float defaultValue) {
|
||||
this.id = id;
|
||||
this.minValue = min;
|
||||
this.maxValue = max;
|
||||
this.defaultValue = defaultValue;
|
||||
this.value = defaultValue;
|
||||
}
|
||||
|
||||
public void setValue(float value) {
|
||||
float clamped = Math.max(minValue, Math.min(maxValue, value));
|
||||
if (this.value != clamped) {
|
||||
this.value = clamped;
|
||||
this.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasChanged() { return changed; }
|
||||
public void markClean() { this.changed = false; }
|
||||
|
||||
public float getValue() { return value; }
|
||||
|
||||
public String getId() { return id; }
|
||||
|
||||
public float getMinValue() { return minValue; }
|
||||
|
||||
public float getMaxValue() { return maxValue; }
|
||||
|
||||
public float getDefaultValue() { return defaultValue; }
|
||||
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取归一化值 [0, 1]
|
||||
*/
|
||||
public float getNormalizedValue() {
|
||||
return (value - minValue) / (maxValue - minValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置归一化值
|
||||
*/
|
||||
public void setNormalizedValue(float normalized) {
|
||||
this.value = minValue + normalized * (maxValue - minValue);
|
||||
}
|
||||
}
|
||||
347
src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java
Normal file
347
src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java
Normal file
@@ -0,0 +1,347 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
import org.joml.Matrix3f;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 2D模型核心数据结构
|
||||
* 支持层级变换、网格变形、参数驱动动画等功能
|
||||
*
|
||||
* 例子
|
||||
* // 创建模型
|
||||
* Model2D model = new Model2D("character");
|
||||
* model.setVersion("1.0.0");
|
||||
*
|
||||
* // 添加部件
|
||||
* ModelPart head = model.createPart("head");
|
||||
* ModelPart body = model.createPart("body");
|
||||
* ModelPart leftArm = model.createPart("left_arm");
|
||||
*
|
||||
* // 建立层级关系
|
||||
* body.addChild(head);
|
||||
* body.addChild(leftArm);
|
||||
*
|
||||
* // 创建网格
|
||||
* Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50);
|
||||
* Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80);
|
||||
* head.addMesh(headMesh);
|
||||
* body.addMesh(bodyMesh);
|
||||
*
|
||||
* // 添加参数
|
||||
* AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0);
|
||||
* AnimationParameter blinkParam = model.createParameter("blink", 0, 1, 0);
|
||||
*
|
||||
* // 保存模型
|
||||
* model.saveToFile("character.model");
|
||||
*
|
||||
* // 加载模型
|
||||
* Model2D loadedModel = Model2D.loadFromFile("character.model");
|
||||
*
|
||||
* // 使用加载的模型
|
||||
* loadedModel.setParameterValue("smile", 0.8f);
|
||||
* loadedModel.update(0.016f); // 更新模型状态
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Model2D {
|
||||
// ==================== 基础属性 ====================
|
||||
private String name;
|
||||
private String version = "1.0.0";
|
||||
private UUID uuid;
|
||||
private ModelMetadata metadata;
|
||||
|
||||
// ==================== 层级结构 ====================
|
||||
private final List<ModelPart> parts;
|
||||
private final Map<String, ModelPart> partMap; // 快速查找
|
||||
private ModelPart rootPart;
|
||||
|
||||
// ==================== 网格系统 ====================
|
||||
private final List<Mesh2D> meshes;
|
||||
private final Map<String, Texture> textures; // 纹理映射
|
||||
|
||||
// ==================== 动画系统 ====================
|
||||
private final Map<String, AnimationParameter> parameters;
|
||||
private final List<AnimationLayer> animationLayers;
|
||||
private final PhysicsSystem physics;
|
||||
|
||||
// ==================== 渲染状态 ====================
|
||||
private transient ModelPose currentPose;
|
||||
private transient boolean needsUpdate = true;
|
||||
private transient BoundingBox bounds;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
public Model2D() {
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.parts = new ArrayList<>();
|
||||
this.partMap = new HashMap<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.textures = new HashMap<>();
|
||||
this.parameters = new LinkedHashMap<>(); // 保持插入顺序
|
||||
this.animationLayers = new ArrayList<>();
|
||||
this.physics = new PhysicsSystem();
|
||||
this.currentPose = new ModelPose();
|
||||
this.metadata = new ModelMetadata();
|
||||
}
|
||||
|
||||
public Model2D(String name) {
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// ==================== 部件管理 ====================
|
||||
public ModelPart createPart(String name) {
|
||||
ModelPart part = new ModelPart(name);
|
||||
addPart(part);
|
||||
return part;
|
||||
}
|
||||
|
||||
public void addPart(ModelPart part) {
|
||||
if (partMap.containsKey(part.getName())) {
|
||||
throw new IllegalArgumentException("Part already exists: " + part.getName());
|
||||
}
|
||||
parts.add(part);
|
||||
partMap.put(part.getName(), part);
|
||||
|
||||
// 设置根部件(第一个添加的部件)
|
||||
if (rootPart == null) {
|
||||
rootPart = part;
|
||||
}
|
||||
}
|
||||
|
||||
public ModelPart getPart(String name) {
|
||||
return partMap.get(name);
|
||||
}
|
||||
|
||||
public List<ModelPart> getParts() {
|
||||
return Collections.unmodifiableList(parts);
|
||||
}
|
||||
|
||||
// ==================== 参数管理 ====================
|
||||
public AnimationParameter createParameter(String id, float min, float max, float defaultValue) {
|
||||
AnimationParameter param = new AnimationParameter(id, min, max, defaultValue);
|
||||
parameters.put(id, param);
|
||||
return param;
|
||||
}
|
||||
|
||||
public AnimationParameter getParameter(String id) {
|
||||
return parameters.get(id);
|
||||
}
|
||||
|
||||
public void addParameter(AnimationParameter param) {
|
||||
parameters.put(param.getId(), param);
|
||||
}
|
||||
|
||||
public void setParameterValue(String paramId, float value) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
if (param != null) {
|
||||
param.setValue(value);
|
||||
markNeedsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public float getParameterValue(String paramId) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
return param != null ? param.getValue() : 0.0f;
|
||||
}
|
||||
|
||||
// ==================== 网格管理 ====================
|
||||
public Mesh2D createMesh(String name, float[] vertices, float[] uvs, int[] indices) {
|
||||
Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices);
|
||||
meshes.add(mesh);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public void addMesh(Mesh2D mesh) {
|
||||
meshes.add(mesh);
|
||||
}
|
||||
|
||||
public Mesh2D getMesh(String name) {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh.getName().equals(name)) {
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 纹理管理 ====================
|
||||
public void addTexture(Texture texture) {
|
||||
textures.put(texture.getName(), texture);
|
||||
}
|
||||
|
||||
public Texture getTexture(String name) {
|
||||
return textures.get(name);
|
||||
}
|
||||
|
||||
public Map<String, Texture> getTextures() {
|
||||
return Collections.unmodifiableMap(textures);
|
||||
}
|
||||
|
||||
// ==================== 动画层管理 ====================
|
||||
public AnimationLayer createAnimationLayer(String name) {
|
||||
AnimationLayer layer = new AnimationLayer(name);
|
||||
animationLayers.add(layer);
|
||||
return layer;
|
||||
}
|
||||
|
||||
// ==================== 更新系统 ====================
|
||||
public void update(float deltaTime) {
|
||||
if (!needsUpdate && !physics.hasActivePhysics()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新物理系统
|
||||
physics.update(deltaTime, this);
|
||||
|
||||
// 更新所有参数驱动的变形
|
||||
updateParameterDeformations();
|
||||
|
||||
// 更新层级变换
|
||||
updateHierarchyTransforms();
|
||||
|
||||
// 更新包围盒
|
||||
updateBoundingBox();
|
||||
|
||||
needsUpdate = false;
|
||||
}
|
||||
|
||||
private void updateParameterDeformations() {
|
||||
for (AnimationParameter param : parameters.values()) {
|
||||
if (param.hasChanged()) {
|
||||
applyParameterDeformations(param);
|
||||
param.markClean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyParameterDeformations(AnimationParameter param) {
|
||||
// 这里将实现参数到具体变形的映射
|
||||
// 例如:参数"face_smile" -> 应用到嘴部网格的变形
|
||||
for (ModelPart part : parts) {
|
||||
part.applyParameter(param);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHierarchyTransforms() {
|
||||
if (rootPart != null) {
|
||||
Matrix3f matrix = new Matrix3f();
|
||||
matrix.identity();
|
||||
rootPart.updateWorldTransform(matrix, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBoundingBox() {
|
||||
if (bounds == null) {
|
||||
bounds = new BoundingBox();
|
||||
}
|
||||
bounds.reset();
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
bounds.expand(part.getWorldBounds());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
public void markNeedsUpdate() {
|
||||
this.needsUpdate = true;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return rootPart != null && rootPart.isVisible();
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
if (rootPart != null) {
|
||||
rootPart.setVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 序列化支持 ====================
|
||||
public ModelData serialize() {
|
||||
return new ModelData(this);
|
||||
}
|
||||
|
||||
public static Model2D deserialize(ModelData data) {
|
||||
return data.deserializeToModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模型到文件
|
||||
*/
|
||||
public void saveToFile(String filePath) {
|
||||
try {
|
||||
ModelData data = serialize();
|
||||
data.saveToFile(filePath);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to save model to: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载模型
|
||||
*/
|
||||
public static Model2D loadFromFile(String filePath) {
|
||||
try {
|
||||
ModelData data = ModelData.loadFromFile(filePath);
|
||||
return deserialize(data);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load model from: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模型到压缩文件
|
||||
*/
|
||||
public void saveToCompressedFile(String filePath) {
|
||||
try {
|
||||
ModelData data = serialize();
|
||||
data.saveToCompressedFile(filePath);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to save compressed model to: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从压缩文件加载模型
|
||||
*/
|
||||
public static Model2D loadFromCompressedFile(String filePath) {
|
||||
try {
|
||||
ModelData data = ModelData.loadFromCompressedFile(filePath);
|
||||
return deserialize(data);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load compressed model from: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public UUID getUuid() { return uuid; }
|
||||
public void setUuid(UUID uuid) { this.uuid = uuid; }
|
||||
|
||||
public ModelMetadata getMetadata() { return metadata; }
|
||||
public void setMetadata(ModelMetadata metadata) { this.metadata = metadata; }
|
||||
|
||||
public ModelPart getRootPart() { return rootPart; }
|
||||
public void setRootPart(ModelPart rootPart) { this.rootPart = rootPart; }
|
||||
|
||||
public List<Mesh2D> getMeshes() { return Collections.unmodifiableList(meshes); }
|
||||
|
||||
public Map<String, AnimationParameter> getParameters() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
public List<AnimationLayer> getAnimationLayers() {
|
||||
return Collections.unmodifiableList(animationLayers);
|
||||
}
|
||||
|
||||
public PhysicsSystem getPhysics() { return physics; }
|
||||
public ModelPose getCurrentPose() { return currentPose; }
|
||||
public BoundingBox getBounds() { return bounds; }
|
||||
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
}
|
||||
766
src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java
Normal file
766
src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java
Normal file
@@ -0,0 +1,766 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/**
|
||||
* 模型数据类,用于模型的序列化、反序列化和数据交换
|
||||
* 支持二进制和JSON格式的模型数据存储
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class ModelData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// ==================== 模型元数据 ====================
|
||||
private String name;
|
||||
private String version;
|
||||
private UUID uuid;
|
||||
private String author;
|
||||
private String description;
|
||||
private long creationTime;
|
||||
private long lastModifiedTime;
|
||||
|
||||
// ==================== 模型结构数据 ====================
|
||||
private List<PartData> parts;
|
||||
private List<MeshData> meshes;
|
||||
private List<TextureData> textures;
|
||||
private List<ParameterData> parameters;
|
||||
private List<AnimationData> animations;
|
||||
|
||||
// ==================== 模型设置 ====================
|
||||
private Vector2f pivotPoint;
|
||||
private float unitsPerMeter;
|
||||
private Map<String, String> userData;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public ModelData() {
|
||||
this("unnamed");
|
||||
}
|
||||
|
||||
public ModelData(String name) {
|
||||
this.name = name;
|
||||
this.version = "1.0.0";
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.creationTime = System.currentTimeMillis();
|
||||
this.lastModifiedTime = creationTime;
|
||||
|
||||
this.parts = new ArrayList<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.textures = new ArrayList<>();
|
||||
this.parameters = new ArrayList<>();
|
||||
this.animations = new ArrayList<>();
|
||||
|
||||
this.pivotPoint = new Vector2f();
|
||||
this.unitsPerMeter = 100.0f; // 默认100单位/米
|
||||
this.userData = new HashMap<>();
|
||||
}
|
||||
|
||||
public ModelData(Model2D model) {
|
||||
this(model.getName());
|
||||
serializeFromModel(model);
|
||||
}
|
||||
|
||||
// ==================== 序列化方法 ====================
|
||||
|
||||
/**
|
||||
* 从Model2D对象序列化数据
|
||||
*/
|
||||
public void serializeFromModel(Model2D model) {
|
||||
if (model == null) {
|
||||
throw new IllegalArgumentException("Model cannot be null");
|
||||
}
|
||||
|
||||
this.name = model.getName();
|
||||
this.version = model.getVersion();
|
||||
this.uuid = model.getUuid();
|
||||
|
||||
// 序列化元数据
|
||||
if (model.getMetadata() != null) {
|
||||
this.author = model.getMetadata().getAuthor();
|
||||
this.description = model.getMetadata().getDescription();
|
||||
}
|
||||
|
||||
// 序列化部件
|
||||
serializeParts(model);
|
||||
|
||||
// 序列化网格
|
||||
serializeMeshes(model);
|
||||
|
||||
// 序列化纹理
|
||||
serializeTextures(model);
|
||||
|
||||
// 序列化参数
|
||||
serializeParameters(model);
|
||||
|
||||
lastModifiedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void serializeParts(Model2D model) {
|
||||
parts.clear();
|
||||
for (ModelPart part : model.getParts()) {
|
||||
parts.add(new PartData(part));
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeMeshes(Model2D model) {
|
||||
meshes.clear();
|
||||
for (Mesh2D mesh : model.getMeshes()) {
|
||||
meshes.add(new MeshData(mesh));
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeTextures(Model2D model) {
|
||||
textures.clear();
|
||||
for (Texture texture : model.getTextures().values()) {
|
||||
textures.add(new TextureData(texture));
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeParameters(Model2D model) {
|
||||
parameters.clear();
|
||||
for (AnimationParameter param : model.getParameters().values()) {
|
||||
parameters.add(new ParameterData(param));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化到Model2D对象
|
||||
*/
|
||||
public Model2D deserializeToModel() {
|
||||
Model2D model = new Model2D(name);
|
||||
model.setVersion(version);
|
||||
model.setUuid(uuid);
|
||||
|
||||
// 设置元数据
|
||||
ModelMetadata metadata = new ModelMetadata();
|
||||
metadata.setAuthor(author);
|
||||
metadata.setDescription(description);
|
||||
model.setMetadata(metadata);
|
||||
|
||||
// 先创建所有纹理
|
||||
Map<String, Texture> textureMap = deserializeTextures();
|
||||
|
||||
// 然后创建所有网格(依赖纹理)
|
||||
Map<String, Mesh2D> meshMap = deserializeMeshes(textureMap);
|
||||
|
||||
// 然后创建部件(依赖网格)
|
||||
deserializeParts(model, meshMap);
|
||||
|
||||
// 最后创建参数
|
||||
deserializeParameters(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private Map<String, Texture> deserializeTextures() {
|
||||
Map<String, Texture> textureMap = new HashMap<>();
|
||||
for (TextureData textureData : textures) {
|
||||
Texture texture = textureData.toTexture();
|
||||
textureMap.put(texture.getName(), texture);
|
||||
}
|
||||
return textureMap;
|
||||
}
|
||||
|
||||
private Map<String, Mesh2D> deserializeMeshes(Map<String, Texture> textureMap) {
|
||||
Map<String, Mesh2D> meshMap = new HashMap<>();
|
||||
for (MeshData meshData : meshes) {
|
||||
Mesh2D mesh = meshData.toMesh2D();
|
||||
|
||||
// 设置纹理
|
||||
if (meshData.textureName != null) {
|
||||
Texture texture = textureMap.get(meshData.textureName);
|
||||
if (texture != null) {
|
||||
mesh.setTexture(texture);
|
||||
}
|
||||
}
|
||||
|
||||
meshMap.put(mesh.getName(), mesh);
|
||||
}
|
||||
return meshMap;
|
||||
}
|
||||
|
||||
private void deserializeParts(Model2D model, Map<String, Mesh2D> meshMap) {
|
||||
// 先创建所有部件
|
||||
Map<String, ModelPart> partMap = new HashMap<>();
|
||||
for (PartData partData : parts) {
|
||||
ModelPart part = partData.toModelPart(meshMap);
|
||||
partMap.put(part.getName(), part);
|
||||
model.addPart(part);
|
||||
}
|
||||
|
||||
// 然后建立父子关系
|
||||
for (PartData partData : parts) {
|
||||
if (partData.parentName != null && !partData.parentName.isEmpty()) {
|
||||
ModelPart child = partMap.get(partData.name);
|
||||
ModelPart parent = partMap.get(partData.parentName);
|
||||
if (parent != null && child != null) {
|
||||
parent.addChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置根部件
|
||||
for (PartData partData : parts) {
|
||||
if (partData.parentName == null || partData.parentName.isEmpty()) {
|
||||
model.setRootPart(partMap.get(partData.name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deserializeParameters(Model2D model) {
|
||||
for (ParameterData paramData : parameters) {
|
||||
AnimationParameter param = paramData.toAnimationParameter();
|
||||
model.addParameter(param);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 文件操作 ====================
|
||||
|
||||
/**
|
||||
* 保存到文件
|
||||
*/
|
||||
public void saveToFile(String filePath) throws IOException {
|
||||
saveToFile(new File(filePath));
|
||||
}
|
||||
|
||||
public void saveToFile(File file) throws IOException {
|
||||
// 确保目录存在
|
||||
File parentDir = file.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
|
||||
oos.writeObject(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存为压缩文件
|
||||
*/
|
||||
public void saveToCompressedFile(String filePath) throws IOException {
|
||||
saveToCompressedFile(new File(filePath));
|
||||
}
|
||||
|
||||
public void saveToCompressedFile(File file) throws IOException {
|
||||
// 确保目录存在
|
||||
File parentDir = file.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(
|
||||
new GZIPOutputStream(new FileOutputStream(file)))) {
|
||||
oos.writeObject(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载
|
||||
*/
|
||||
public static ModelData loadFromFile(String filePath) throws IOException, ClassNotFoundException {
|
||||
return loadFromFile(new File(filePath));
|
||||
}
|
||||
|
||||
public static ModelData loadFromFile(File file) throws IOException, ClassNotFoundException {
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Model file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
|
||||
return (ModelData) ois.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从压缩文件加载
|
||||
*/
|
||||
public static ModelData loadFromCompressedFile(String filePath) throws IOException, ClassNotFoundException {
|
||||
return loadFromCompressedFile(new File(filePath));
|
||||
}
|
||||
|
||||
public static ModelData loadFromCompressedFile(File file) throws IOException, ClassNotFoundException {
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Compressed model file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
new GZIPInputStream(new FileInputStream(file)))) {
|
||||
return (ModelData) ois.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 数据验证 ====================
|
||||
|
||||
/**
|
||||
* 验证模型数据的完整性
|
||||
*/
|
||||
public boolean validate() {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uuid == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查所有部件引用有效的网格
|
||||
for (PartData part : parts) {
|
||||
for (String meshName : part.meshNames) {
|
||||
if (!meshExists(meshName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean meshExists(String meshName) {
|
||||
return meshes.stream().anyMatch(mesh -> mesh.name.equals(meshName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证错误信息
|
||||
*/
|
||||
public List<String> getValidationErrors() {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
errors.add("Model name is required");
|
||||
}
|
||||
|
||||
if (uuid == null) {
|
||||
errors.add("Model UUID is required");
|
||||
}
|
||||
|
||||
// 检查网格引用
|
||||
for (PartData part : parts) {
|
||||
for (String meshName : part.meshNames) {
|
||||
if (!meshExists(meshName)) {
|
||||
errors.add("Part '" + part.name + "' references non-existent mesh: " + meshName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 创建深拷贝
|
||||
*/
|
||||
public ModelData copy() {
|
||||
ModelData copy = new ModelData(name + "_copy");
|
||||
copy.version = this.version;
|
||||
copy.uuid = UUID.randomUUID();
|
||||
copy.author = this.author;
|
||||
copy.description = this.description;
|
||||
copy.creationTime = System.currentTimeMillis();
|
||||
copy.lastModifiedTime = copy.creationTime;
|
||||
|
||||
// 深拷贝集合
|
||||
for (PartData part : this.parts) {
|
||||
copy.parts.add(part.copy());
|
||||
}
|
||||
for (MeshData mesh : this.meshes) {
|
||||
copy.meshes.add(mesh.copy());
|
||||
}
|
||||
for (TextureData texture : this.textures) {
|
||||
copy.textures.add(texture.copy());
|
||||
}
|
||||
for (ParameterData param : this.parameters) {
|
||||
copy.parameters.add(param.copy());
|
||||
}
|
||||
for (AnimationData anim : this.animations) {
|
||||
copy.animations.add(anim.copy());
|
||||
}
|
||||
|
||||
copy.pivotPoint = new Vector2f(this.pivotPoint);
|
||||
copy.unitsPerMeter = this.unitsPerMeter;
|
||||
copy.userData = new HashMap<>(this.userData);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并另一个模型数据
|
||||
*/
|
||||
public void merge(ModelData other) {
|
||||
if (other == null) return;
|
||||
|
||||
// 合并网格(避免名称冲突)
|
||||
for (MeshData mesh : other.meshes) {
|
||||
String originalName = mesh.name;
|
||||
int counter = 1;
|
||||
while (meshExists(mesh.name)) {
|
||||
mesh.name = originalName + "_" + counter++;
|
||||
}
|
||||
this.meshes.add(mesh);
|
||||
}
|
||||
|
||||
// 合并部件
|
||||
for (PartData part : other.parts) {
|
||||
String originalName = part.name;
|
||||
int counter = 1;
|
||||
while (partExists(part.name)) {
|
||||
part.name = originalName + "_" + counter++;
|
||||
}
|
||||
this.parts.add(part);
|
||||
}
|
||||
|
||||
// 合并参数
|
||||
for (ParameterData param : other.parameters) {
|
||||
this.parameters.add(param.copy());
|
||||
}
|
||||
|
||||
// 合并纹理
|
||||
for (TextureData texture : other.textures) {
|
||||
String originalName = texture.name;
|
||||
int counter = 1;
|
||||
while (textureExists(texture.name)) {
|
||||
texture.name = originalName + "_" + counter++;
|
||||
}
|
||||
this.textures.add(texture);
|
||||
}
|
||||
|
||||
lastModifiedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private boolean partExists(String partName) {
|
||||
return parts.stream().anyMatch(part -> part.name.equals(partName));
|
||||
}
|
||||
|
||||
private boolean textureExists(String textureName) {
|
||||
return textures.stream().anyMatch(texture -> texture.name.equals(textureName));
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
/**
|
||||
* 部件数据
|
||||
*/
|
||||
public static class PartData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String name;
|
||||
public String parentName;
|
||||
public Vector2f position;
|
||||
public float rotation;
|
||||
public Vector2f scale;
|
||||
public boolean visible;
|
||||
public float opacity;
|
||||
public List<String> meshNames;
|
||||
public Map<String, String> userData;
|
||||
|
||||
public PartData() {
|
||||
this.position = new Vector2f();
|
||||
this.rotation = 0.0f;
|
||||
this.scale = new Vector2f(1.0f, 1.0f);
|
||||
this.visible = true;
|
||||
this.opacity = 1.0f;
|
||||
this.meshNames = new ArrayList<>();
|
||||
this.userData = new HashMap<>();
|
||||
}
|
||||
|
||||
public PartData(ModelPart part) {
|
||||
this();
|
||||
this.name = part.getName();
|
||||
this.position = part.getPosition();
|
||||
this.rotation = part.getRotation();
|
||||
this.scale = part.getScale();
|
||||
this.visible = part.isVisible();
|
||||
this.opacity = part.getOpacity();
|
||||
|
||||
// 收集网格名称
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
this.meshNames.add(mesh.getName());
|
||||
}
|
||||
|
||||
// 设置父级名称
|
||||
if (part.getParent() != null) {
|
||||
this.parentName = part.getParent().getName();
|
||||
}
|
||||
}
|
||||
|
||||
public ModelPart toModelPart(Map<String, Mesh2D> meshMap) {
|
||||
ModelPart part = new ModelPart(name);
|
||||
part.setPosition(position);
|
||||
part.setRotation(rotation);
|
||||
part.setScale(scale);
|
||||
part.setVisible(visible);
|
||||
part.setOpacity(opacity);
|
||||
|
||||
// 添加网格
|
||||
for (String meshName : meshNames) {
|
||||
Mesh2D mesh = meshMap.get(meshName);
|
||||
if (mesh != null) {
|
||||
part.addMesh(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
public PartData copy() {
|
||||
PartData copy = new PartData();
|
||||
copy.name = this.name;
|
||||
copy.parentName = this.parentName;
|
||||
copy.position = new Vector2f(this.position);
|
||||
copy.rotation = this.rotation;
|
||||
copy.scale = new Vector2f(this.scale);
|
||||
copy.visible = this.visible;
|
||||
copy.opacity = this.opacity;
|
||||
copy.meshNames = new ArrayList<>(this.meshNames);
|
||||
copy.userData = new HashMap<>(this.userData);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格数据
|
||||
*/
|
||||
public static class MeshData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String name;
|
||||
public float[] vertices;
|
||||
public float[] uvs;
|
||||
public int[] indices;
|
||||
public String textureName;
|
||||
public boolean visible;
|
||||
public int drawMode;
|
||||
|
||||
public MeshData() {
|
||||
this.visible = true;
|
||||
this.drawMode = Mesh2D.TRIANGLES;
|
||||
}
|
||||
|
||||
public MeshData(Mesh2D mesh) {
|
||||
this();
|
||||
this.name = mesh.getName();
|
||||
this.vertices = mesh.getVertices();
|
||||
this.uvs = mesh.getUVs();
|
||||
this.indices = mesh.getIndices();
|
||||
this.visible = mesh.isVisible();
|
||||
this.drawMode = mesh.getDrawMode();
|
||||
|
||||
if (mesh.getTexture() != null) {
|
||||
this.textureName = mesh.getTexture().getName();
|
||||
}
|
||||
}
|
||||
|
||||
public Mesh2D toMesh2D() {
|
||||
Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices);
|
||||
mesh.setVisible(visible);
|
||||
mesh.setDrawMode(drawMode);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public MeshData copy() {
|
||||
MeshData copy = new MeshData();
|
||||
copy.name = this.name;
|
||||
copy.vertices = this.vertices != null ? this.vertices.clone() : null;
|
||||
copy.uvs = this.uvs != null ? this.uvs.clone() : null;
|
||||
copy.indices = this.indices != null ? this.indices.clone() : null;
|
||||
copy.textureName = this.textureName;
|
||||
copy.visible = this.visible;
|
||||
copy.drawMode = this.drawMode;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 纹理数据
|
||||
*/
|
||||
public static class TextureData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String name;
|
||||
public String filePath;
|
||||
public byte[] imageData;
|
||||
public int width;
|
||||
public int height;
|
||||
public Texture.TextureFormat format;
|
||||
|
||||
public TextureData() {}
|
||||
|
||||
public TextureData(Texture texture) {
|
||||
this.name = texture.getName();
|
||||
this.width = texture.getWidth();
|
||||
this.height = texture.getHeight();
|
||||
this.format = texture.getFormat();
|
||||
}
|
||||
|
||||
public Texture toTexture() {
|
||||
Texture texture = new Texture(name, width, height, format);
|
||||
// 注意:这里需要处理imageData的加载
|
||||
// 实际项目中可能需要从文件路径加载图像数据
|
||||
return texture;
|
||||
}
|
||||
|
||||
public TextureData copy() {
|
||||
TextureData copy = new TextureData();
|
||||
copy.name = this.name;
|
||||
copy.filePath = this.filePath;
|
||||
copy.imageData = this.imageData != null ? this.imageData.clone() : null;
|
||||
copy.width = this.width;
|
||||
copy.height = this.height;
|
||||
copy.format = this.format;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数数据
|
||||
*/
|
||||
public static class ParameterData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String id;
|
||||
public float value;
|
||||
public float defaultValue;
|
||||
public float minValue;
|
||||
public float maxValue;
|
||||
|
||||
public ParameterData() {}
|
||||
|
||||
public ParameterData(AnimationParameter param) {
|
||||
this.id = param.getId();
|
||||
this.value = param.getValue();
|
||||
this.defaultValue = param.getDefaultValue();
|
||||
this.minValue = param.getMinValue();
|
||||
this.maxValue = param.getMaxValue();
|
||||
}
|
||||
|
||||
public AnimationParameter toAnimationParameter() {
|
||||
AnimationParameter param = new AnimationParameter(id, minValue, maxValue, defaultValue);
|
||||
param.setValue(value); // 恢复保存时的值
|
||||
return param;
|
||||
}
|
||||
|
||||
public ParameterData copy() {
|
||||
ParameterData copy = new ParameterData();
|
||||
copy.id = this.id;
|
||||
copy.value = this.value;
|
||||
copy.defaultValue = this.defaultValue;
|
||||
copy.minValue = this.minValue;
|
||||
copy.maxValue = this.maxValue;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画数据
|
||||
*/
|
||||
public static class AnimationData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String name;
|
||||
public float duration;
|
||||
public boolean looping;
|
||||
public Map<String, List<KeyframeData>> tracks;
|
||||
|
||||
public AnimationData() {
|
||||
this.tracks = new HashMap<>();
|
||||
}
|
||||
|
||||
public AnimationData copy() {
|
||||
AnimationData copy = new AnimationData();
|
||||
copy.name = this.name;
|
||||
copy.duration = this.duration;
|
||||
copy.looping = this.looping;
|
||||
copy.tracks = new HashMap<>(this.tracks);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键帧数据
|
||||
*/
|
||||
public static class KeyframeData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public float time;
|
||||
public float value;
|
||||
public String interpolation;
|
||||
|
||||
public KeyframeData copy() {
|
||||
KeyframeData copy = new KeyframeData();
|
||||
copy.time = this.time;
|
||||
copy.value = this.value;
|
||||
copy.interpolation = this.interpolation;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
|
||||
public UUID getUuid() { return uuid; }
|
||||
public void setUuid(UUID uuid) { this.uuid = uuid; }
|
||||
|
||||
public String getAuthor() { return author; }
|
||||
public void setAuthor(String author) { this.author = author; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public long getCreationTime() { return creationTime; }
|
||||
public void setCreationTime(long creationTime) { this.creationTime = creationTime; }
|
||||
|
||||
public long getLastModifiedTime() { return lastModifiedTime; }
|
||||
public void setLastModifiedTime(long lastModifiedTime) { this.lastModifiedTime = lastModifiedTime; }
|
||||
|
||||
public List<PartData> getParts() { return parts; }
|
||||
public void setParts(List<PartData> parts) { this.parts = parts; }
|
||||
|
||||
public List<MeshData> getMeshes() { return meshes; }
|
||||
public void setMeshes(List<MeshData> meshes) { this.meshes = meshes; }
|
||||
|
||||
public List<TextureData> getTextures() { return textures; }
|
||||
public void setTextures(List<TextureData> textures) { this.textures = textures; }
|
||||
|
||||
public List<ParameterData> getParameters() { return parameters; }
|
||||
public void setParameters(List<ParameterData> parameters) { this.parameters = parameters; }
|
||||
|
||||
public List<AnimationData> getAnimations() { return animations; }
|
||||
public void setAnimations(List<AnimationData> animations) { this.animations = animations; }
|
||||
|
||||
public Vector2f getPivotPoint() { return pivotPoint; }
|
||||
public void setPivotPoint(Vector2f pivotPoint) { this.pivotPoint = pivotPoint; }
|
||||
|
||||
public float getUnitsPerMeter() { return unitsPerMeter; }
|
||||
public void setUnitsPerMeter(float unitsPerMeter) { this.unitsPerMeter = unitsPerMeter; }
|
||||
|
||||
public Map<String, String> getUserData() { return userData; }
|
||||
public void setUserData(Map<String, String> userData) { this.userData = userData; }
|
||||
|
||||
// ==================== Object方法 ====================
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModelData{" +
|
||||
"name='" + name + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", parts=" + parts.size() +
|
||||
", meshes=" + meshes.size() +
|
||||
", parameters=" + parameters.size() +
|
||||
", animations=" + animations.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
513
src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java
Normal file
513
src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java
Normal file
@@ -0,0 +1,513 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Matrix3fUtils;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 2D模型部件,支持层级变换和变形器
|
||||
* 使用 JOML 库进行数学计算
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class ModelPart {
|
||||
// ==================== 基础属性 ====================
|
||||
private String name;
|
||||
private ModelPart parent;
|
||||
private final List<ModelPart> children;
|
||||
private final List<Mesh2D> meshes;
|
||||
|
||||
// ==================== 变换属性 ====================
|
||||
private final Vector2f position;
|
||||
private float rotation;
|
||||
private final Vector2f scale;
|
||||
private final Matrix3f localTransform;
|
||||
private final Matrix3f worldTransform;
|
||||
|
||||
// ==================== 渲染属性 ====================
|
||||
private boolean visible;
|
||||
private BlendMode blendMode;
|
||||
private float opacity;
|
||||
|
||||
// ==================== 变形系统 ====================
|
||||
private final List<Deformer> deformers;
|
||||
|
||||
// ==================== 状态标记 ====================
|
||||
private boolean transformDirty;
|
||||
private boolean boundsDirty;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public ModelPart() {
|
||||
this("unnamed");
|
||||
}
|
||||
|
||||
public ModelPart(String name) {
|
||||
this.name = name;
|
||||
this.children = new ArrayList<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.deformers = new ArrayList<>();
|
||||
|
||||
// 初始化变换属性
|
||||
this.position = new Vector2f();
|
||||
this.rotation = 0.0f;
|
||||
this.scale = new Vector2f(1.0f, 1.0f);
|
||||
this.localTransform = new Matrix3f();
|
||||
this.worldTransform = new Matrix3f();
|
||||
|
||||
// 初始化渲染属性
|
||||
this.visible = true;
|
||||
this.blendMode = BlendMode.NORMAL;
|
||||
this.opacity = 1.0f;
|
||||
|
||||
// 标记需要更新
|
||||
this.transformDirty = true;
|
||||
this.boundsDirty = true;
|
||||
|
||||
updateLocalTransform();
|
||||
}
|
||||
|
||||
// ==================== 层级管理 ====================
|
||||
|
||||
/**
|
||||
* 添加子部件
|
||||
*/
|
||||
public void addChild(ModelPart child) {
|
||||
if (child == this) {
|
||||
throw new IllegalArgumentException("Cannot add self as child");
|
||||
}
|
||||
if (child.parent != null) {
|
||||
child.parent.removeChild(child);
|
||||
}
|
||||
children.add(child);
|
||||
child.parent = this;
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除子部件
|
||||
*/
|
||||
public boolean removeChild(ModelPart child) {
|
||||
boolean removed = children.remove(child);
|
||||
if (removed) {
|
||||
child.parent = null;
|
||||
markTransformDirty();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有子部件
|
||||
*/
|
||||
public List<ModelPart> getChildren() {
|
||||
return new ArrayList<>(children);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称查找子部件
|
||||
*/
|
||||
public ModelPart findChild(String name) {
|
||||
for (ModelPart child : children) {
|
||||
if (name.equals(child.getName())) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找子部件
|
||||
*/
|
||||
public ModelPart findChildRecursive(String name) {
|
||||
// 先检查直接子节点
|
||||
ModelPart result = findChild(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 递归检查子节点的子节点
|
||||
for (ModelPart child : children) {
|
||||
result = child.findChildRecursive(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 变换系统 ====================
|
||||
|
||||
/**
|
||||
* 更新世界变换
|
||||
*/
|
||||
public void updateWorldTransform(Matrix3f parentTransform, boolean recursive) {
|
||||
// 如果需要更新局部变换
|
||||
if (transformDirty) {
|
||||
updateLocalTransform();
|
||||
}
|
||||
|
||||
// 计算世界变换:parent * local
|
||||
parentTransform.mul(localTransform, worldTransform);
|
||||
|
||||
// 递归更新子部件
|
||||
if (recursive) {
|
||||
for (ModelPart child : children) {
|
||||
child.updateWorldTransform(worldTransform, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 标记边界需要更新
|
||||
boundsDirty = true;
|
||||
transformDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新局部变换矩阵
|
||||
*/
|
||||
private void updateLocalTransform() {
|
||||
float cos = (float) Math.cos(rotation);
|
||||
float sin = (float) Math.sin(rotation);
|
||||
|
||||
// 正确的 R * S 组合(先 scale 再 rotate,最终矩阵为 Translate * (Rotate * Scale))
|
||||
float m00 = cos * scale.x; // = cos * sx
|
||||
float m01 = -sin * scale.y; // = -sin * sy
|
||||
float m02 = position.x; // tx
|
||||
|
||||
float m10 = sin * scale.x; // = sin * sx
|
||||
float m11 = cos * scale.y; // = cos * sy
|
||||
float m12 = position.y; // ty
|
||||
|
||||
localTransform.set(
|
||||
m00, m01, m02,
|
||||
m10, m11, m12,
|
||||
0.0f, 0.0f, 1.0f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记变换需要更新
|
||||
*/
|
||||
public void markTransformDirty() {
|
||||
this.transformDirty = true;
|
||||
for (ModelPart child : children) {
|
||||
child.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置位置
|
||||
*/
|
||||
public void setPosition(float x, float y) {
|
||||
position.set(x, y);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
public void setPosition(Vector2f position) {
|
||||
this.position.set(position);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动部件
|
||||
*/
|
||||
public void translate(float dx, float dy) {
|
||||
position.add(dx, dy);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
public void translate(Vector2f delta) {
|
||||
position.add(delta);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置旋转(弧度)
|
||||
*/
|
||||
public void setRotation(float radians) {
|
||||
this.rotation = radians;
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转部件
|
||||
*/
|
||||
public void rotate(float deltaRadians) {
|
||||
this.rotation += deltaRadians;
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放
|
||||
*/
|
||||
public void setScale(float sx, float sy) {
|
||||
scale.set(sx, sy);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
public void setScale(float uniformScale) {
|
||||
scale.set(uniformScale, uniformScale);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
public void setScale(Vector2f scale) {
|
||||
this.scale.set(scale);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放部件
|
||||
*/
|
||||
public void scale(float sx, float sy) {
|
||||
scale.mul(sx, sy);
|
||||
markTransformDirty();
|
||||
}
|
||||
|
||||
// ==================== 网格管理 ====================
|
||||
|
||||
/**
|
||||
* 添加网格
|
||||
*/
|
||||
public void addMesh(Mesh2D mesh) {
|
||||
meshes.add(mesh);
|
||||
boundsDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除网格
|
||||
*/
|
||||
public boolean removeMesh(Mesh2D mesh) {
|
||||
boolean removed = meshes.remove(mesh);
|
||||
if (removed) {
|
||||
boundsDirty = true;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有网格
|
||||
*/
|
||||
public List<Mesh2D> getMeshes() {
|
||||
return new ArrayList<>(meshes);
|
||||
}
|
||||
|
||||
// ==================== 变形器管理 ====================
|
||||
|
||||
/**
|
||||
* 添加变形器
|
||||
*/
|
||||
public void addDeformer(Deformer deformer) {
|
||||
deformers.add(deformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除变形器
|
||||
*/
|
||||
public boolean removeDeformer(Deformer deformer) {
|
||||
return deformers.remove(deformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用参数到所有变形器
|
||||
*/
|
||||
public void applyParameter(AnimationParameter param) {
|
||||
for (Deformer deformer : deformers) {
|
||||
if (deformer.isDrivenBy(param.getId())) {
|
||||
deformer.apply(param.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 如果变形器改变了网格,需要更新边界
|
||||
if (!deformers.isEmpty()) {
|
||||
boundsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用所有变形器
|
||||
*/
|
||||
public void applyDeformers() {
|
||||
for (Deformer deformer : deformers) {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
deformer.applyToMesh(mesh);
|
||||
}
|
||||
}
|
||||
boundsDirty = true;
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 变换点从局部空间到世界空间
|
||||
*/
|
||||
public Vector2f localToWorld(Vector2f localPoint) {
|
||||
return Matrix3fUtils.transformPoint(worldTransform, localPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换点从世界空间到局部空间
|
||||
*/
|
||||
public Vector2f worldToLocal(Vector2f worldPoint) {
|
||||
return Matrix3fUtils.transformPointInverse(worldTransform, worldPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界空间中的包围盒
|
||||
*/
|
||||
public BoundingBox getWorldBounds() {
|
||||
if (boundsDirty) {
|
||||
updateBounds();
|
||||
}
|
||||
|
||||
BoundingBox worldBounds = new BoundingBox();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
BoundingBox meshBounds = mesh.getBounds();
|
||||
if (meshBounds != null) {
|
||||
// 变换到世界空间
|
||||
Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY()));
|
||||
Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY()));
|
||||
worldBounds.expand(min.x, min.y);
|
||||
worldBounds.expand(max.x, max.y);
|
||||
}
|
||||
}
|
||||
|
||||
return worldBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新边界
|
||||
*/
|
||||
private void updateBounds() {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
mesh.updateBounds();
|
||||
}
|
||||
boundsDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可见(考虑父级可见性)
|
||||
*/
|
||||
public boolean isEffectivelyVisible() {
|
||||
if (!visible) {
|
||||
return false;
|
||||
}
|
||||
if (parent != null) {
|
||||
return parent.isEffectivelyVisible();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public ModelPart getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public Vector2f getPosition() {
|
||||
return new Vector2f(position);
|
||||
}
|
||||
|
||||
public float getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public Vector2f getScale() {
|
||||
return new Vector2f(scale);
|
||||
}
|
||||
|
||||
public Matrix3f getLocalTransform() {
|
||||
return new Matrix3f(localTransform);
|
||||
}
|
||||
|
||||
public Matrix3f getWorldTransform() {
|
||||
return new Matrix3f(worldTransform);
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public BlendMode getBlendMode() {
|
||||
return blendMode;
|
||||
}
|
||||
|
||||
public void setBlendMode(BlendMode blendMode) {
|
||||
this.blendMode = blendMode;
|
||||
}
|
||||
|
||||
public float getOpacity() {
|
||||
return opacity;
|
||||
}
|
||||
|
||||
public void setOpacity(float opacity) {
|
||||
this.opacity = Math.max(0.0f, Math.min(1.0f, opacity));
|
||||
}
|
||||
|
||||
public List<Deformer> getDeformers() {
|
||||
return new ArrayList<>(deformers);
|
||||
}
|
||||
|
||||
// ==================== 枚举和内部类 ====================
|
||||
|
||||
/**
|
||||
* 混合模式枚举
|
||||
*/
|
||||
public enum BlendMode {
|
||||
NORMAL,
|
||||
ADDITIVE,
|
||||
MULTIPLY,
|
||||
SCREEN
|
||||
}
|
||||
|
||||
// ==================== Object 方法 ====================
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ModelPart modelPart = (ModelPart) o;
|
||||
return Float.compare(rotation, modelPart.rotation) == 0 &&
|
||||
visible == modelPart.visible &&
|
||||
Float.compare(opacity, modelPart.opacity) == 0 &&
|
||||
Objects.equals(name, modelPart.name) &&
|
||||
Objects.equals(position, modelPart.position) &&
|
||||
Objects.equals(scale, modelPart.scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, position, rotation, scale, visible, opacity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModelPart{" +
|
||||
"name='" + name + '\'' +
|
||||
", position=" + position +
|
||||
", rotation=" + rotation +
|
||||
", scale=" + scale +
|
||||
", visible=" + visible +
|
||||
", children=" + children.size() +
|
||||
", meshes=" + meshes.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 旋转变形器 - 围绕中心点旋转顶点
|
||||
*/
|
||||
public class RotationDeformer extends Deformer {
|
||||
private float baseAngle = 0.0f;
|
||||
private float angleRange = (float) Math.PI; // ±90度范围
|
||||
private float currentAngle = 0.0f;
|
||||
|
||||
public RotationDeformer(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
public RotationDeformer(String name, Vector2f center, float radius) {
|
||||
super(name);
|
||||
this.getRange().setCenter(center);
|
||||
this.getRange().setRadius(radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToMesh(Mesh2D mesh) {
|
||||
if (!enabled || weight <= 0.0f || currentAngle == 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] vertices = mesh.getVertices(); // 获取顶点数组副本
|
||||
Vector2f center = getRange().getCenter();
|
||||
float cos = (float) Math.cos(currentAngle);
|
||||
float sin = (float) Math.sin(currentAngle);
|
||||
boolean modified = false;
|
||||
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
float originalX = vertices[baseIndex];
|
||||
float originalY = vertices[baseIndex + 1];
|
||||
|
||||
// 计算相对于中心的坐标
|
||||
float dx = originalX - center.x;
|
||||
float dy = originalY - center.y;
|
||||
|
||||
// 应用旋转
|
||||
float rotatedX = dx * cos - dy * sin;
|
||||
float rotatedY = dx * sin + dy * cos;
|
||||
|
||||
float deformedX = center.x + rotatedX;
|
||||
float deformedY = center.y + rotatedY;
|
||||
|
||||
// 应用变形权重
|
||||
float deformationWeight = computeDeformationWeight(originalX, originalY);
|
||||
blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// 更新网格顶点数据
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float value) {
|
||||
// value 范围 [0, 1] 映射到 [baseAngle - angleRange/2, baseAngle + angleRange/2]
|
||||
this.currentAngle = baseAngle + (value - 0.5f) * angleRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.currentAngle = baseAngle;
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public float getBaseAngle() { return baseAngle; }
|
||||
public void setBaseAngle(float baseAngle) { this.baseAngle = baseAngle; }
|
||||
|
||||
public float getAngleRange() { return angleRange; }
|
||||
public void setAngleRange(float angleRange) { this.angleRange = angleRange; }
|
||||
|
||||
public float getCurrentAngle() { return currentAngle; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 缩放变形器 - 围绕中心点缩放顶点
|
||||
*/
|
||||
public class ScaleDeformer extends Deformer {
|
||||
private Vector2f baseScale = new Vector2f(1.0f, 1.0f);
|
||||
private Vector2f scaleRange = new Vector2f(0.5f, 0.5f); // 缩放范围
|
||||
private Vector2f currentScale = new Vector2f(1.0f, 1.0f);
|
||||
|
||||
public ScaleDeformer(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToMesh(Mesh2D mesh) {
|
||||
if (!enabled || weight <= 0.0f ||
|
||||
(currentScale.x == 1.0f && currentScale.y == 1.0f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] vertices = mesh.getVertices(); // 获取顶点数组副本
|
||||
Vector2f center = getRange().getCenter();
|
||||
boolean modified = false;
|
||||
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
float originalX = vertices[baseIndex];
|
||||
float originalY = vertices[baseIndex + 1];
|
||||
|
||||
// 计算相对于中心的坐标
|
||||
float dx = originalX - center.x;
|
||||
float dy = originalY - center.y;
|
||||
|
||||
// 应用缩放
|
||||
float deformedX = center.x + dx * currentScale.x;
|
||||
float deformedY = center.y + dy * currentScale.y;
|
||||
|
||||
// 应用变形权重
|
||||
float deformationWeight = computeDeformationWeight(originalX, originalY);
|
||||
blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// 更新网格顶点数据
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float value) {
|
||||
// value 范围 [0, 1] 映射到缩放范围
|
||||
float scaleX = baseScale.x + (value - 0.5f) * scaleRange.x;
|
||||
float scaleY = baseScale.y + (value - 0.5f) * scaleRange.y;
|
||||
|
||||
this.currentScale.set(scaleX, scaleY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.currentScale.set(baseScale);
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public Vector2f getBaseScale() { return new Vector2f(baseScale); }
|
||||
public void setBaseScale(Vector2f baseScale) { this.baseScale.set(baseScale); }
|
||||
|
||||
public Vector2f getScaleRange() { return new Vector2f(scaleRange); }
|
||||
public void setScaleRange(Vector2f scaleRange) { this.scaleRange.set(scaleRange); }
|
||||
|
||||
public Vector2f getCurrentScale() { return new Vector2f(currentScale); }
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 顶点位置变形器 - 直接修改顶点位置
|
||||
* 使用高效的数据结构存储变形数据
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class VertexDeformer extends Deformer {
|
||||
// 使用更高效的数据结构
|
||||
private final Map<Integer, VertexDeformation> vertexDeformations;
|
||||
private final List<Integer> vertexIndexList; // 用于快速迭代
|
||||
|
||||
private float currentValue = 0.0f;
|
||||
|
||||
public VertexDeformer(String name) {
|
||||
super(name);
|
||||
this.vertexDeformations = new HashMap<>();
|
||||
this.vertexIndexList = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶点变形数据内部类
|
||||
*/
|
||||
private static class VertexDeformation {
|
||||
final float originalX;
|
||||
final float originalY;
|
||||
final float targetX;
|
||||
final float targetY;
|
||||
|
||||
VertexDeformation(float originalX, float originalY, float targetX, float targetY) {
|
||||
this.originalX = originalX;
|
||||
this.originalY = originalY;
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加顶点变形目标
|
||||
*/
|
||||
public void addVertexDeformation(int vertexIndex, float originalX, float originalY, float targetX, float targetY) {
|
||||
VertexDeformation deformation = new VertexDeformation(originalX, originalY, targetX, targetY);
|
||||
|
||||
// 使用HashMap避免重复顶点索引
|
||||
if (!vertexDeformations.containsKey(vertexIndex)) {
|
||||
vertexIndexList.add(vertexIndex);
|
||||
}
|
||||
vertexDeformations.put(vertexIndex, deformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加顶点变形目标(使用Vector2f)
|
||||
*/
|
||||
public void addVertexDeformation(int vertexIndex, Vector2f originalPos, Vector2f targetPos) {
|
||||
addVertexDeformation(vertexIndex, originalPos.x, originalPos.y, targetPos.x, targetPos.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加顶点变形
|
||||
*/
|
||||
public void addVertexDeformations(Map<Integer, VertexDeformation> deformations) {
|
||||
for (Map.Entry<Integer, VertexDeformation> entry : deformations.entrySet()) {
|
||||
addVertexDeformation(entry.getKey(),
|
||||
entry.getValue().originalX, entry.getValue().originalY,
|
||||
entry.getValue().targetX, entry.getValue().targetY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除顶点变形
|
||||
*/
|
||||
public boolean removeVertexDeformation(int vertexIndex) {
|
||||
VertexDeformation removed = vertexDeformations.remove(vertexIndex);
|
||||
if (removed != null) {
|
||||
vertexIndexList.remove((Integer) vertexIndex); // 注意要移除对象而不是索引
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有顶点变形
|
||||
*/
|
||||
public void clearVertexDeformations() {
|
||||
vertexDeformations.clear();
|
||||
vertexIndexList.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含指定顶点的变形
|
||||
*/
|
||||
public boolean containsVertexDeformation(int vertexIndex) {
|
||||
return vertexDeformations.containsKey(vertexIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶点变形数量
|
||||
*/
|
||||
public int getVertexDeformationCount() {
|
||||
return vertexDeformations.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定顶点的变形数据
|
||||
*/
|
||||
public VertexDeformation getVertexDeformation(int vertexIndex) {
|
||||
return vertexDeformations.get(vertexIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有受影响的顶点索引
|
||||
*/
|
||||
public List<Integer> getAffectedVertexIndices() {
|
||||
return new ArrayList<>(vertexIndexList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToMesh(Mesh2D mesh) {
|
||||
if (!enabled || weight <= 0.0f || vertexDeformations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] vertices = mesh.getVertices(); // 获取顶点数组副本
|
||||
boolean modified = false;
|
||||
|
||||
// 使用预存的索引列表进行快速迭代
|
||||
for (int vertexIndex : vertexIndexList) {
|
||||
if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VertexDeformation deformation = vertexDeformations.get(vertexIndex);
|
||||
if (deformation == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取当前顶点位置
|
||||
int vertexBaseIndex = vertexIndex * 2;
|
||||
float currentX = vertices[vertexBaseIndex];
|
||||
float currentY = vertices[vertexBaseIndex + 1];
|
||||
|
||||
// 计算变形位置
|
||||
float deformedX = deformation.originalX +
|
||||
(deformation.targetX - deformation.originalX) * currentValue;
|
||||
float deformedY = deformation.originalY +
|
||||
(deformation.targetY - deformation.originalY) * currentValue;
|
||||
|
||||
// 应用变形权重
|
||||
float deformationWeight = computeDeformationWeight(currentX, currentY);
|
||||
blendVertexPosition(vertices, vertexIndex, currentX, currentY,
|
||||
deformedX, deformedY, deformationWeight);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// 批量更新网格顶点数据
|
||||
updateMeshVertices(mesh, vertices);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新网格顶点(优化性能)
|
||||
*/
|
||||
private void updateMeshVertices(Mesh2D mesh, float[] vertices) {
|
||||
// 只更新受影响的顶点,而不是全部顶点
|
||||
for (int vertexIndex : vertexIndexList) {
|
||||
if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) {
|
||||
continue;
|
||||
}
|
||||
int vertexBaseIndex = vertexIndex * 2;
|
||||
mesh.setVertex(vertexIndex, vertices[vertexBaseIndex], vertices[vertexBaseIndex + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float value) {
|
||||
this.currentValue = Math.max(0.0f, Math.min(1.0f, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.currentValue = 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前值并立即应用到指定网格
|
||||
*/
|
||||
public void applyToMesh(float value, Mesh2D mesh) {
|
||||
apply(value);
|
||||
applyToMesh(mesh);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值动画到目标值
|
||||
*/
|
||||
public void animateTo(float targetValue, float duration) {
|
||||
// 这里可以实现动画插值逻辑
|
||||
// 实际项目中可以使用动画系统
|
||||
this.currentValue = targetValue;
|
||||
}
|
||||
|
||||
public float getCurrentValue() {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建顶点变形器的深拷贝
|
||||
*/
|
||||
public VertexDeformer copy() {
|
||||
VertexDeformer copy = new VertexDeformer(this.name + "_copy");
|
||||
copy.enabled = this.enabled;
|
||||
copy.weight = this.weight;
|
||||
copy.currentValue = this.currentValue;
|
||||
|
||||
// 深拷贝变形数据
|
||||
for (Map.Entry<Integer, VertexDeformation> entry : this.vertexDeformations.entrySet()) {
|
||||
VertexDeformation deformation = entry.getValue();
|
||||
copy.addVertexDeformation(entry.getKey(),
|
||||
deformation.originalX, deformation.originalY,
|
||||
deformation.targetX, deformation.targetY);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从原始网格自动提取原始位置
|
||||
*/
|
||||
public void extractOriginalPositionsFromMesh(Mesh2D mesh) {
|
||||
for (int vertexIndex : vertexIndexList) {
|
||||
if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2f currentPos = mesh.getVertex(vertexIndex);
|
||||
VertexDeformation deformation = vertexDeformations.get(vertexIndex);
|
||||
|
||||
if (deformation != null) {
|
||||
// 更新原始位置为当前网格位置
|
||||
addVertexDeformation(vertexIndex,
|
||||
currentPos.x, currentPos.y,
|
||||
deformation.targetX, deformation.targetY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转变形方向(交换原始位置和目标位置)
|
||||
*/
|
||||
public void reverseDeformation() {
|
||||
Map<Integer, VertexDeformation> reversed = new HashMap<>();
|
||||
|
||||
for (Map.Entry<Integer, VertexDeformation> entry : vertexDeformations.entrySet()) {
|
||||
VertexDeformation original = entry.getValue();
|
||||
VertexDeformation reversedDeformation = new VertexDeformation(
|
||||
original.targetX, original.targetY,
|
||||
original.originalX, original.originalY
|
||||
);
|
||||
reversed.put(entry.getKey(), reversedDeformation);
|
||||
}
|
||||
|
||||
this.vertexDeformations.clear();
|
||||
this.vertexDeformations.putAll(reversed);
|
||||
this.currentValue = 1.0f - this.currentValue; // 反转当前值
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 波浪变形器 - 创建波浪效果的顶点变形
|
||||
*/
|
||||
public class WaveDeformer extends Deformer {
|
||||
private float amplitude = 10.0f; // 波幅
|
||||
private float frequency = 0.1f; // 频率
|
||||
private float phase = 0.0f; // 相位
|
||||
private float waveAngle = 0.0f; // 波传播方向角度
|
||||
private float currentTime = 0.0f;
|
||||
|
||||
public WaveDeformer(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToMesh(Mesh2D mesh) {
|
||||
if (!enabled || weight <= 0.0f || amplitude == 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] vertices = mesh.getVertices(); // 获取顶点数组副本
|
||||
Vector2f center = getRange().getCenter();
|
||||
float cosDir = (float) Math.cos(waveAngle);
|
||||
float sinDir = (float) Math.sin(waveAngle);
|
||||
boolean modified = false;
|
||||
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
float originalX = vertices[baseIndex];
|
||||
float originalY = vertices[baseIndex + 1];
|
||||
|
||||
// 计算在波传播方向上的投影距离
|
||||
float projDistance = (originalX - center.x) * cosDir +
|
||||
(originalY - center.y) * sinDir;
|
||||
|
||||
// 计算波浪偏移
|
||||
float waveOffset = amplitude *
|
||||
(float) Math.sin(frequency * projDistance + phase + currentTime);
|
||||
|
||||
// 垂直于波传播方向的偏移
|
||||
float deformedX = originalX - sinDir * waveOffset;
|
||||
float deformedY = originalY + cosDir * waveOffset;
|
||||
|
||||
// 应用变形权重
|
||||
float deformationWeight = computeDeformationWeight(originalX, originalY);
|
||||
blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// 更新网格顶点数据
|
||||
for (int i = 0; i < mesh.getVertexCount(); i++) {
|
||||
int baseIndex = i * 2;
|
||||
mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float value) {
|
||||
// 根据配置的驱动参数类型,决定如何应用value
|
||||
// 这里假设通过参数名称或配置来决定控制哪个波浪参数
|
||||
|
||||
// 方案1: 根据当前激活的驱动参数类型来应用
|
||||
if (!parameterValues.isEmpty()) {
|
||||
// 如果有多个参数,可以按优先级或特定逻辑处理
|
||||
// 这里简单取第一个参数的值
|
||||
String firstParam = drivenParameters.iterator().next();
|
||||
applyByParameterName(firstParam, value);
|
||||
} else {
|
||||
// 默认行为:控制波幅
|
||||
applyAmplitude(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数名称应用不同的波浪参数控制
|
||||
*/
|
||||
private void applyByParameterName(String paramName, float value) {
|
||||
paramName = paramName.toLowerCase();
|
||||
|
||||
if (paramName.contains("amplitude") || paramName.contains("amp")) {
|
||||
applyAmplitude(value);
|
||||
} else if (paramName.contains("frequency") || paramName.contains("freq")) {
|
||||
applyFrequency(value);
|
||||
} else if (paramName.contains("phase") || paramName.contains("offset")) {
|
||||
applyPhase(value);
|
||||
} else if (paramName.contains("angle") || paramName.contains("direction")) {
|
||||
applyWaveAngle(value);
|
||||
} else if (paramName.contains("time") || paramName.contains("speed")) {
|
||||
applyTimeSpeed(value);
|
||||
} else if (paramName.contains("weight") || paramName.contains("intensity")) {
|
||||
applyWeight(value);
|
||||
} else {
|
||||
// 默认控制波幅
|
||||
applyAmplitude(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制波幅 - value [0,1] 映射到 [0, maxAmplitude]
|
||||
*/
|
||||
private void applyAmplitude(float value) {
|
||||
float maxAmplitude = 50.0f; // 最大波幅
|
||||
this.amplitude = value * maxAmplitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制频率 - value [0,1] 映射到 [minFrequency, maxFrequency]
|
||||
*/
|
||||
private void applyFrequency(float value) {
|
||||
float minFrequency = 0.01f; // 最小频率
|
||||
float maxFrequency = 0.5f; // 最大频率
|
||||
this.frequency = minFrequency + value * (maxFrequency - minFrequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制相位 - value [0,1] 映射到 [0, 2π]
|
||||
*/
|
||||
private void applyPhase(float value) {
|
||||
this.phase = value * (float) (2.0f * Math.PI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制波传播方向 - value [0,1] 映射到 [0, 2π]
|
||||
*/
|
||||
private void applyWaveAngle(float value) {
|
||||
this.waveAngle = value * (float) (2.0f * Math.PI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制时间速度 - value [0,1] 映射到时间乘数 [0.1, 5.0]
|
||||
*/
|
||||
private void applyTimeSpeed(float value) {
|
||||
// 这个需要在外部update方法中使用timeMultiplier
|
||||
// 这里先存储,在update中使用
|
||||
this.timeMultiplier = 0.1f + value * 4.9f;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制变形器权重 - value [0,1] 直接设置权重
|
||||
*/
|
||||
private void applyWeight(float value) {
|
||||
setWeight(value);
|
||||
}
|
||||
|
||||
// 添加时间乘数字段
|
||||
private float timeMultiplier = 1.0f;
|
||||
|
||||
/**
|
||||
* 更新波浪动画(使用时间乘数)
|
||||
*/
|
||||
public void update(float deltaTime) {
|
||||
this.currentTime += deltaTime * timeMultiplier;
|
||||
}
|
||||
|
||||
// 添加参数配置方法,允许外部指定控制模式
|
||||
public enum ControlMode {
|
||||
AMPLITUDE, // 控制波幅
|
||||
FREQUENCY, // 控制频率
|
||||
PHASE, // 控制相位
|
||||
WAVE_ANGLE, // 控制波方向
|
||||
TIME_SPEED, // 控制动画速度
|
||||
WEIGHT // 控制变形器权重
|
||||
}
|
||||
|
||||
private ControlMode controlMode = ControlMode.AMPLITUDE;
|
||||
|
||||
/**
|
||||
* 设置控制模式
|
||||
*/
|
||||
public void setControlMode(ControlMode mode) {
|
||||
this.controlMode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设置的控制模式应用参数
|
||||
*/
|
||||
public void applyWithMode(float value, ControlMode mode) {
|
||||
switch (mode) {
|
||||
case AMPLITUDE:
|
||||
applyAmplitude(value);
|
||||
break;
|
||||
case FREQUENCY:
|
||||
applyFrequency(value);
|
||||
break;
|
||||
case PHASE:
|
||||
applyPhase(value);
|
||||
break;
|
||||
case WAVE_ANGLE:
|
||||
applyWaveAngle(value);
|
||||
break;
|
||||
case TIME_SPEED:
|
||||
applyTimeSpeed(value);
|
||||
break;
|
||||
case WEIGHT:
|
||||
applyWeight(value);
|
||||
break;
|
||||
default:
|
||||
applyAmplitude(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量应用多个参数
|
||||
*/
|
||||
public void applyParameters(float amplitudeValue, float frequencyValue, float phaseValue,
|
||||
float angleValue, float speedValue, float weightValue) {
|
||||
applyAmplitude(amplitudeValue);
|
||||
applyFrequency(frequencyValue);
|
||||
applyPhase(phaseValue);
|
||||
applyWaveAngle(angleValue);
|
||||
applyTimeSpeed(speedValue);
|
||||
applyWeight(weightValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用配置对象应用参数
|
||||
*/
|
||||
public void applyFromConfig(WaveConfig config) {
|
||||
this.amplitude = config.amplitude;
|
||||
this.frequency = config.frequency;
|
||||
this.phase = config.phase;
|
||||
this.waveAngle = config.waveAngle;
|
||||
this.timeMultiplier = config.timeMultiplier;
|
||||
setWeight(config.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 波浪配置类
|
||||
*/
|
||||
public static class WaveConfig {
|
||||
public float amplitude = 10.0f;
|
||||
public float frequency = 0.1f;
|
||||
public float phase = 0.0f;
|
||||
public float waveAngle = 0.0f;
|
||||
public float timeMultiplier = 1.0f;
|
||||
public float weight = 1.0f;
|
||||
|
||||
public WaveConfig() {}
|
||||
|
||||
public WaveConfig(float amplitude, float frequency, float phase,
|
||||
float waveAngle, float timeMultiplier, float weight) {
|
||||
this.amplitude = amplitude;
|
||||
this.frequency = frequency;
|
||||
this.phase = phase;
|
||||
this.waveAngle = waveAngle;
|
||||
this.timeMultiplier = timeMultiplier;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.currentTime = 0.0f;
|
||||
this.amplitude = 10.0f;
|
||||
}
|
||||
|
||||
public float getTimeMultiplier() { return timeMultiplier; }
|
||||
public void setTimeMultiplier(float timeMultiplier) { this.timeMultiplier = timeMultiplier; }
|
||||
|
||||
public ControlMode getControlMode() { return controlMode; }
|
||||
|
||||
// Getter/Setter
|
||||
public float getAmplitude() { return amplitude; }
|
||||
public void setAmplitude(float amplitude) { this.amplitude = amplitude; }
|
||||
|
||||
public float getFrequency() { return frequency; }
|
||||
public void setFrequency(float frequency) { this.frequency = frequency; }
|
||||
|
||||
public float getPhase() { return phase; }
|
||||
public void setPhase(float phase) { this.phase = phase; }
|
||||
|
||||
public float getWaveAngle() { return waveAngle; }
|
||||
public void setWaveAngle(float waveAngle) { this.waveAngle = waveAngle; }
|
||||
|
||||
public float getCurrentTime() { return currentTime; }
|
||||
public void setCurrentTime(float currentTime) { this.currentTime = currentTime; }
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 动画剪辑类,用于管理2D模型的完整动画序列
|
||||
* 支持关键帧动画、曲线编辑、事件标记和动画混合
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class AnimationClip {
|
||||
// ==================== 剪辑属性 ====================
|
||||
private final String name;
|
||||
private final UUID uuid;
|
||||
private float duration;
|
||||
private float framesPerSecond;
|
||||
private boolean looping;
|
||||
|
||||
// ==================== 动画数据 ====================
|
||||
private final Map<String, AnimationCurve> curves;
|
||||
private final List<AnimationEventMarker> eventMarkers;
|
||||
private final Map<String, Float> defaultValues;
|
||||
|
||||
// ==================== 元数据 ====================
|
||||
private String author;
|
||||
private String description;
|
||||
private long creationTime;
|
||||
private long lastModifiedTime;
|
||||
private Map<String, String> userData;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public AnimationClip(String name) {
|
||||
this(name, 1.0f, 60.0f);
|
||||
}
|
||||
|
||||
public AnimationClip(String name, float duration, float fps) {
|
||||
this.name = name;
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.duration = Math.max(0.0f, duration);
|
||||
this.framesPerSecond = Math.max(1.0f, fps);
|
||||
this.looping = true;
|
||||
|
||||
this.curves = new ConcurrentHashMap<>();
|
||||
this.eventMarkers = new ArrayList<>();
|
||||
this.defaultValues = new ConcurrentHashMap<>();
|
||||
|
||||
this.author = "Unknown";
|
||||
this.description = "";
|
||||
this.creationTime = System.currentTimeMillis();
|
||||
this.lastModifiedTime = creationTime;
|
||||
this.userData = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
// ==================== 曲线管理 ====================
|
||||
|
||||
/**
|
||||
* 添加动画曲线
|
||||
*/
|
||||
public AnimationCurve addCurve(String parameterId) {
|
||||
return addCurve(parameterId, 0.0f);
|
||||
}
|
||||
|
||||
public AnimationCurve addCurve(String parameterId, float defaultValue) {
|
||||
AnimationCurve curve = new AnimationCurve(parameterId, defaultValue);
|
||||
curves.put(parameterId, curve);
|
||||
defaultValues.put(parameterId, defaultValue);
|
||||
markModified();
|
||||
return curve;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动画曲线
|
||||
*/
|
||||
public AnimationCurve getCurve(String parameterId) {
|
||||
return curves.get(parameterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除动画曲线
|
||||
*/
|
||||
public boolean removeCurve(String parameterId) {
|
||||
boolean removed = curves.remove(parameterId) != null;
|
||||
defaultValues.remove(parameterId);
|
||||
if (removed) markModified();
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在曲线
|
||||
*/
|
||||
public boolean hasCurve(String parameterId) {
|
||||
return curves.containsKey(parameterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有曲线参数ID
|
||||
*/
|
||||
public Set<String> getCurveParameterIds() {
|
||||
return Collections.unmodifiableSet(curves.keySet());
|
||||
}
|
||||
|
||||
// ==================== 关键帧管理 ====================
|
||||
|
||||
/**
|
||||
* 添加关键帧
|
||||
*/
|
||||
public Keyframe addKeyframe(String parameterId, float time, float value) {
|
||||
return addKeyframe(parameterId, time, value, InterpolationType.LINEAR);
|
||||
}
|
||||
|
||||
public Keyframe addKeyframe(String parameterId, float time, float value,
|
||||
InterpolationType interpolation) {
|
||||
AnimationCurve curve = getOrCreateCurve(parameterId);
|
||||
Keyframe keyframe = curve.addKeyframe(time, value, interpolation);
|
||||
updateDurationIfNeeded(time);
|
||||
markModified();
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除关键帧
|
||||
*/
|
||||
public boolean removeKeyframe(String parameterId, float time) {
|
||||
AnimationCurve curve = curves.get(parameterId);
|
||||
if (curve != null) {
|
||||
boolean removed = curve.removeKeyframe(time);
|
||||
if (removed) markModified();
|
||||
return removed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关键帧
|
||||
*/
|
||||
public Keyframe getKeyframe(String parameterId, float time) {
|
||||
AnimationCurve curve = curves.get(parameterId);
|
||||
return curve != null ? curve.getKeyframe(time) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数在指定时间的所有关键帧
|
||||
*/
|
||||
public List<Keyframe> getKeyframes(String parameterId) {
|
||||
AnimationCurve curve = curves.get(parameterId);
|
||||
return curve != null ? curve.getKeyframes() : Collections.emptyList();
|
||||
}
|
||||
|
||||
// ==================== 采样系统 ====================
|
||||
|
||||
/**
|
||||
* 采样动画在指定时间的参数值
|
||||
*/
|
||||
public Map<String, Float> sample(float time) {
|
||||
Map<String, Float> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, AnimationCurve> entry : curves.entrySet()) {
|
||||
String paramId = entry.getKey();
|
||||
AnimationCurve curve = entry.getValue();
|
||||
float value = curve.sample(time);
|
||||
result.put(paramId, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样单个参数在指定时间的值
|
||||
*/
|
||||
public float sampleParameter(String parameterId, float time) {
|
||||
AnimationCurve curve = curves.get(parameterId);
|
||||
return curve != null ? curve.sample(time) : defaultValues.getOrDefault(parameterId, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样动画在指定时间的参数值(应用循环)
|
||||
*/
|
||||
public Map<String, Float> sampleLooped(float time) {
|
||||
float effectiveTime = time;
|
||||
if (looping && duration > 0) {
|
||||
effectiveTime = time % duration;
|
||||
} else {
|
||||
effectiveTime = Math.min(time, duration);
|
||||
}
|
||||
return sample(effectiveTime);
|
||||
}
|
||||
|
||||
// ==================== 事件标记管理 ====================
|
||||
|
||||
/**
|
||||
* 添加事件标记
|
||||
*/
|
||||
public AnimationEventMarker addEventMarker(String name, float time) {
|
||||
return addEventMarker(name, time, null);
|
||||
}
|
||||
|
||||
public AnimationEventMarker addEventMarker(String name, float time, Runnable action) {
|
||||
AnimationEventMarker marker = new AnimationEventMarker(name, time, action);
|
||||
|
||||
// 按时间排序插入
|
||||
int index = 0;
|
||||
while (index < eventMarkers.size() && eventMarkers.get(index).getTime() < time) {
|
||||
index++;
|
||||
}
|
||||
eventMarkers.add(index, marker);
|
||||
|
||||
updateDurationIfNeeded(time);
|
||||
markModified();
|
||||
return marker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件标记
|
||||
*/
|
||||
public boolean removeEventMarker(String name) {
|
||||
return eventMarkers.removeIf(marker -> marker.getName().equals(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间范围内的事件标记
|
||||
*/
|
||||
public List<AnimationEventMarker> getEventMarkersInRange(float startTime, float endTime) {
|
||||
List<AnimationEventMarker> result = new ArrayList<>();
|
||||
for (AnimationEventMarker marker : eventMarkers) {
|
||||
if (marker.getTime() >= startTime && marker.getTime() <= endTime) {
|
||||
result.add(marker);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发指定时间的事件标记
|
||||
*/
|
||||
public void triggerEventMarkers(float time, float tolerance) {
|
||||
for (AnimationEventMarker marker : eventMarkers) {
|
||||
if (Math.abs(marker.getTime() - time) <= tolerance && !marker.isTriggered()) {
|
||||
marker.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有事件标记状态
|
||||
*/
|
||||
public void resetEventMarkers() {
|
||||
for (AnimationEventMarker marker : eventMarkers) {
|
||||
marker.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取或创建曲线
|
||||
*/
|
||||
private AnimationCurve getOrCreateCurve(String parameterId) {
|
||||
return curves.computeIfAbsent(parameterId, k -> {
|
||||
float defaultValue = defaultValues.getOrDefault(parameterId, 0.0f);
|
||||
return new AnimationCurve(parameterId, defaultValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动画时长(如果需要)
|
||||
*/
|
||||
private void updateDurationIfNeeded(float time) {
|
||||
if (time > duration) {
|
||||
duration = time;
|
||||
markModified();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已修改
|
||||
*/
|
||||
private void markModified() {
|
||||
lastModifiedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算帧数
|
||||
*/
|
||||
public int getFrameCount() {
|
||||
return (int) Math.ceil(duration * framesPerSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间转换为帧索引
|
||||
*/
|
||||
public int timeToFrame(float time) {
|
||||
return (int) (time * framesPerSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧索引转换为时间
|
||||
*/
|
||||
public float frameToTime(int frame) {
|
||||
return frame / framesPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间是否在动画范围内
|
||||
*/
|
||||
public boolean isTimeInRange(float time) {
|
||||
return time >= 0 && time <= duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动画的边界值(最小/最大值)
|
||||
*/
|
||||
public Map<String, float[]> getValueBounds() {
|
||||
Map<String, float[]> bounds = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, AnimationCurve> entry : curves.entrySet()) {
|
||||
String paramId = entry.getKey();
|
||||
AnimationCurve curve = entry.getValue();
|
||||
float[] minMax = curve.getValueRange();
|
||||
bounds.put(paramId, minMax);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动画剪辑的深拷贝
|
||||
*/
|
||||
public AnimationClip copy() {
|
||||
AnimationClip copy = new AnimationClip(name + "_copy", duration, framesPerSecond);
|
||||
copy.looping = this.looping;
|
||||
copy.author = this.author;
|
||||
copy.description = this.description;
|
||||
|
||||
// 深拷贝曲线
|
||||
for (Map.Entry<String, AnimationCurve> entry : this.curves.entrySet()) {
|
||||
copy.curves.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
|
||||
// 深拷贝默认值
|
||||
copy.defaultValues.putAll(this.defaultValues);
|
||||
|
||||
// 深拷贝事件标记
|
||||
for (AnimationEventMarker marker : this.eventMarkers) {
|
||||
copy.eventMarkers.add(marker.copy());
|
||||
}
|
||||
|
||||
// 深拷贝用户数据
|
||||
copy.userData.putAll(this.userData);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并另一个动画剪辑
|
||||
*/
|
||||
public void merge(AnimationClip other) {
|
||||
if (other == null) return;
|
||||
|
||||
// 合并曲线
|
||||
for (Map.Entry<String, AnimationCurve> entry : other.curves.entrySet()) {
|
||||
String paramId = entry.getKey();
|
||||
AnimationCurve otherCurve = entry.getValue();
|
||||
|
||||
if (this.curves.containsKey(paramId)) {
|
||||
// 合并到现有曲线
|
||||
AnimationCurve thisCurve = this.curves.get(paramId);
|
||||
for (Keyframe keyframe : otherCurve.getKeyframes()) {
|
||||
thisCurve.addKeyframe(keyframe.getTime(), keyframe.getValue(),
|
||||
keyframe.getInterpolation());
|
||||
}
|
||||
} else {
|
||||
// 添加新曲线
|
||||
this.curves.put(paramId, otherCurve.copy());
|
||||
}
|
||||
}
|
||||
|
||||
// 合并事件标记
|
||||
for (AnimationEventMarker marker : other.eventMarkers) {
|
||||
this.addEventMarker(marker.getName() + "_merged", marker.getTime(),
|
||||
marker.getAction());
|
||||
}
|
||||
|
||||
// 更新时长
|
||||
this.duration = Math.max(this.duration, other.duration);
|
||||
|
||||
markModified();
|
||||
}
|
||||
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
/**
|
||||
* 动画曲线类
|
||||
*/
|
||||
public static class AnimationCurve {
|
||||
private final String parameterId;
|
||||
private final List<Keyframe> keyframes;
|
||||
private final float defaultValue;
|
||||
|
||||
public AnimationCurve(String parameterId, float defaultValue) {
|
||||
this.parameterId = parameterId;
|
||||
this.keyframes = new ArrayList<>();
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关键帧
|
||||
*/
|
||||
public Keyframe addKeyframe(float time, float value) {
|
||||
return addKeyframe(time, value, InterpolationType.LINEAR);
|
||||
}
|
||||
|
||||
public Keyframe addKeyframe(float time, float value, InterpolationType interpolation) {
|
||||
Keyframe keyframe = new Keyframe(time, value, interpolation);
|
||||
|
||||
// 移除相同时间的关键帧(如果有)
|
||||
removeKeyframe(time);
|
||||
|
||||
// 按时间排序插入
|
||||
int index = 0;
|
||||
while (index < keyframes.size() && keyframes.get(index).getTime() < time) {
|
||||
index++;
|
||||
}
|
||||
keyframes.add(index, keyframe);
|
||||
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除关键帧
|
||||
*/
|
||||
public boolean removeKeyframe(float time) {
|
||||
return keyframes.removeIf(kf -> Math.abs(kf.getTime() - time) < 0.0001f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关键帧
|
||||
*/
|
||||
public Keyframe getKeyframe(float time) {
|
||||
for (Keyframe kf : keyframes) {
|
||||
if (Math.abs(kf.getTime() - time) < 0.0001f) {
|
||||
return kf;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样曲线值
|
||||
*/
|
||||
public float sample(float time) {
|
||||
if (keyframes.isEmpty()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 在第一个关键帧之前
|
||||
if (time <= keyframes.get(0).getTime()) {
|
||||
return keyframes.get(0).getValue();
|
||||
}
|
||||
|
||||
// 在最后一个关键帧之后
|
||||
if (time >= keyframes.get(keyframes.size() - 1).getTime()) {
|
||||
return keyframes.get(keyframes.size() - 1).getValue();
|
||||
}
|
||||
|
||||
// 找到包围时间的关键帧
|
||||
for (int i = 0; i < keyframes.size() - 1; i++) {
|
||||
Keyframe kf1 = keyframes.get(i);
|
||||
Keyframe kf2 = keyframes.get(i + 1);
|
||||
|
||||
if (time >= kf1.getTime() && time <= kf2.getTime()) {
|
||||
return interpolate(kf1, kf2, time);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值计算
|
||||
*/
|
||||
private float interpolate(Keyframe kf1, Keyframe kf2, float time) {
|
||||
float t = (time - kf1.getTime()) / (kf2.getTime() - kf1.getTime());
|
||||
|
||||
switch (kf1.getInterpolation()) {
|
||||
case LINEAR:
|
||||
return lerp(kf1.getValue(), kf2.getValue(), t);
|
||||
case STEP:
|
||||
return kf1.getValue();
|
||||
case SMOOTH:
|
||||
return smoothLerp(kf1.getValue(), kf2.getValue(), t);
|
||||
case EASE_IN:
|
||||
return easeInLerp(kf1.getValue(), kf2.getValue(), t);
|
||||
case EASE_OUT:
|
||||
return easeOutLerp(kf1.getValue(), kf2.getValue(), t);
|
||||
case EASE_IN_OUT:
|
||||
return easeInOutLerp(kf1.getValue(), kf2.getValue(), t);
|
||||
default:
|
||||
return kf1.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
private float lerp(float a, float b, float t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
private float smoothLerp(float a, float b, float t) {
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
return a * (2 * t3 - 3 * t2 + 1) + b * (-2 * t3 + 3 * t2);
|
||||
}
|
||||
|
||||
private float easeInLerp(float a, float b, float t) {
|
||||
return a + (b - a) * (t * t);
|
||||
}
|
||||
|
||||
private float easeOutLerp(float a, float b, float t) {
|
||||
return a + (b - a) * (1 - (1 - t) * (1 - t));
|
||||
}
|
||||
|
||||
private float easeInOutLerp(float a, float b, float t) {
|
||||
return a + (b - a) * ((t < 0.5f) ? 2 * t * t : 1 - (2 * (1 - t) * (1 - t)) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值范围
|
||||
*/
|
||||
public float[] getValueRange() {
|
||||
if (keyframes.isEmpty()) {
|
||||
return new float[]{defaultValue, defaultValue};
|
||||
}
|
||||
|
||||
float min = Float.MAX_VALUE;
|
||||
float max = Float.MIN_VALUE;
|
||||
|
||||
for (Keyframe kf : keyframes) {
|
||||
min = Math.min(min, kf.getValue());
|
||||
max = Math.max(max, kf.getValue());
|
||||
}
|
||||
|
||||
return new float[]{min, max};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建曲线深拷贝
|
||||
*/
|
||||
public AnimationCurve copy() {
|
||||
AnimationCurve copy = new AnimationCurve(parameterId, defaultValue);
|
||||
for (Keyframe kf : this.keyframes) {
|
||||
copy.keyframes.add(kf.copy());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Getter方法
|
||||
public String getParameterId() { return parameterId; }
|
||||
public List<Keyframe> getKeyframes() { return Collections.unmodifiableList(keyframes); }
|
||||
public float getDefaultValue() { return defaultValue; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键帧类
|
||||
*/
|
||||
public static class Keyframe {
|
||||
private final float time;
|
||||
private final float value;
|
||||
private final InterpolationType interpolation;
|
||||
|
||||
public Keyframe(float time, float value, InterpolationType interpolation) {
|
||||
this.time = time;
|
||||
this.value = value;
|
||||
this.interpolation = interpolation;
|
||||
}
|
||||
|
||||
public Keyframe copy() {
|
||||
return new Keyframe(time, value, interpolation);
|
||||
}
|
||||
|
||||
// Getter方法
|
||||
public float getTime() { return time; }
|
||||
public float getValue() { return value; }
|
||||
public InterpolationType getInterpolation() { return interpolation; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Keyframe{time=%.2f, value=%.2f, interpolation=%s}",
|
||||
time, value, interpolation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件标记类
|
||||
*/
|
||||
public static class AnimationEventMarker {
|
||||
private final String name;
|
||||
private final float time;
|
||||
private final Runnable action;
|
||||
private boolean triggered;
|
||||
|
||||
public AnimationEventMarker(String name, float time, Runnable action) {
|
||||
this.name = name;
|
||||
this.time = time;
|
||||
this.action = action;
|
||||
this.triggered = false;
|
||||
}
|
||||
|
||||
public void trigger() {
|
||||
if (!triggered && action != null) {
|
||||
action.run();
|
||||
triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
triggered = false;
|
||||
}
|
||||
|
||||
public AnimationEventMarker copy() {
|
||||
return new AnimationEventMarker(name, time, action);
|
||||
}
|
||||
|
||||
// Getter方法
|
||||
public String getName() { return name; }
|
||||
public float getTime() { return time; }
|
||||
public Runnable getAction() { return action; }
|
||||
public boolean isTriggered() { return triggered; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("EventMarker{name='%s', time=%.2f}", name, time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值类型枚举
|
||||
*/
|
||||
public enum InterpolationType {
|
||||
LINEAR, // 线性插值
|
||||
STEP, // 步进插值
|
||||
SMOOTH, // 平滑插值(三次Hermite)
|
||||
EASE_IN, // 缓入
|
||||
EASE_OUT, // 缓出
|
||||
EASE_IN_OUT // 缓入缓出
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public UUID getUuid() { return uuid; }
|
||||
|
||||
public float getDuration() { return duration; }
|
||||
public void setDuration(float duration) {
|
||||
this.duration = Math.max(0.0f, duration);
|
||||
markModified();
|
||||
}
|
||||
|
||||
public float getFramesPerSecond() { return framesPerSecond; }
|
||||
public void setFramesPerSecond(float framesPerSecond) {
|
||||
this.framesPerSecond = Math.max(1.0f, framesPerSecond);
|
||||
markModified();
|
||||
}
|
||||
|
||||
public boolean isLooping() { return looping; }
|
||||
public void setLooping(boolean looping) { this.looping = looping; }
|
||||
|
||||
public Map<String, AnimationCurve> getCurves() {
|
||||
return Collections.unmodifiableMap(curves);
|
||||
}
|
||||
|
||||
public List<AnimationEventMarker> getEventMarkers() {
|
||||
return Collections.unmodifiableList(eventMarkers);
|
||||
}
|
||||
|
||||
public Map<String, Float> getDefaultValues() {
|
||||
return Collections.unmodifiableMap(defaultValues);
|
||||
}
|
||||
|
||||
public String getAuthor() { return author; }
|
||||
public void setAuthor(String author) { this.author = author; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public long getCreationTime() { return creationTime; }
|
||||
public long getLastModifiedTime() { return lastModifiedTime; }
|
||||
|
||||
public Map<String, String> getUserData() {
|
||||
return Collections.unmodifiableMap(userData);
|
||||
}
|
||||
public void setUserData(Map<String, String> userData) {
|
||||
this.userData = new ConcurrentHashMap<>(userData);
|
||||
}
|
||||
|
||||
// ==================== Object 方法 ====================
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"AnimationClip{name='%s', duration=%.2f, curves=%d, events=%d, looping=%s}",
|
||||
name, duration, curves.size(), eventMarkers.size(), looping
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AnimationClip that = (AnimationClip) o;
|
||||
return uuid.equals(that.uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 动画层类,用于管理2D模型的动画层和动画混合
|
||||
* 支持多层动画叠加、权重控制、混合模式等高级功能
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class AnimationLayer {
|
||||
// ==================== 层属性 ====================
|
||||
private final String name;
|
||||
private final UUID uuid;
|
||||
private float weight;
|
||||
private boolean enabled;
|
||||
private BlendMode blendMode;
|
||||
private int priority;
|
||||
|
||||
// ==================== 动画数据 ====================
|
||||
private final Map<String, AnimationTrack> tracks;
|
||||
private final List<AnimationClip> clips;
|
||||
private AnimationClip currentClip;
|
||||
private float playbackSpeed;
|
||||
private boolean looping;
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
private float currentTime;
|
||||
private boolean playing;
|
||||
private boolean paused;
|
||||
private Map<String, Float> parameterOverrides;
|
||||
|
||||
// ==================== 事件系统 ====================
|
||||
private final List<AnimationEventListener> eventListeners;
|
||||
private final Map<String, List<AnimationEvent>> events;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public AnimationLayer(String name) {
|
||||
this(name, 1.0f);
|
||||
}
|
||||
|
||||
public AnimationLayer(String name, float weight) {
|
||||
this.name = name;
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.weight = Math.max(0.0f, Math.min(1.0f, weight));
|
||||
this.enabled = true;
|
||||
this.blendMode = BlendMode.OVERRIDE;
|
||||
this.priority = 0;
|
||||
|
||||
this.tracks = new ConcurrentHashMap<>();
|
||||
this.clips = new ArrayList<>();
|
||||
this.playbackSpeed = 1.0f;
|
||||
this.looping = true;
|
||||
|
||||
this.currentTime = 0.0f;
|
||||
this.playing = false;
|
||||
this.paused = false;
|
||||
this.parameterOverrides = new ConcurrentHashMap<>();
|
||||
|
||||
this.eventListeners = new ArrayList<>();
|
||||
this.events = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
// ==================== 轨道管理 ====================
|
||||
|
||||
/**
|
||||
* 添加动画轨道
|
||||
*/
|
||||
public AnimationTrack addTrack(String parameterId) {
|
||||
AnimationTrack track = new AnimationTrack(parameterId);
|
||||
tracks.put(parameterId, track);
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动画轨道
|
||||
*/
|
||||
public AnimationTrack getTrack(String parameterId) {
|
||||
return tracks.get(parameterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除动画轨道
|
||||
*/
|
||||
public boolean removeTrack(String parameterId) {
|
||||
return tracks.remove(parameterId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在轨道
|
||||
*/
|
||||
public boolean hasTrack(String parameterId) {
|
||||
return tracks.containsKey(parameterId);
|
||||
}
|
||||
|
||||
// ==================== 剪辑管理 ====================
|
||||
|
||||
/**
|
||||
* 添加动画剪辑
|
||||
*/
|
||||
public void addClip(AnimationClip clip) {
|
||||
clips.add(clip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除动画剪辑
|
||||
*/
|
||||
public boolean removeClip(AnimationClip clip) {
|
||||
return clips.remove(clip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放指定剪辑
|
||||
*/
|
||||
public void playClip(String clipName) {
|
||||
for (AnimationClip clip : clips) {
|
||||
if (clip.getName().equals(clipName)) {
|
||||
playClip(clip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Animation clip not found: " + clipName);
|
||||
}
|
||||
|
||||
public void playClip(AnimationClip clip) {
|
||||
this.currentClip = clip;
|
||||
this.currentTime = 0.0f;
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
|
||||
notifyAnimationStarted(clip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止播放
|
||||
*/
|
||||
public void stop() {
|
||||
this.playing = false;
|
||||
this.paused = false;
|
||||
this.currentTime = 0.0f;
|
||||
|
||||
if (currentClip != null) {
|
||||
notifyAnimationStopped(currentClip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停播放
|
||||
*/
|
||||
public void pause() {
|
||||
if (playing && !paused) {
|
||||
paused = true;
|
||||
notifyAnimationPaused(currentClip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复播放
|
||||
*/
|
||||
public void resume() {
|
||||
if (playing && paused) {
|
||||
paused = false;
|
||||
notifyAnimationResumed(currentClip);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 更新系统 ====================
|
||||
|
||||
/**
|
||||
* 更新动画层
|
||||
*/
|
||||
public void update(float deltaTime, Model2D model) {
|
||||
if (!enabled || weight <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新播放时间
|
||||
if (playing && !paused) {
|
||||
currentTime += deltaTime * playbackSpeed;
|
||||
|
||||
// 检查循环
|
||||
if (currentClip != null && currentTime >= currentClip.getDuration()) {
|
||||
if (looping) {
|
||||
currentTime %= currentClip.getDuration();
|
||||
notifyAnimationLooped(currentClip);
|
||||
} else {
|
||||
stop();
|
||||
notifyAnimationCompleted(currentClip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查事件
|
||||
checkEvents();
|
||||
}
|
||||
|
||||
// 应用动画
|
||||
applyAnimation(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用动画到模型
|
||||
*/
|
||||
private void applyAnimation(Model2D model) {
|
||||
if (currentClip != null) {
|
||||
// 应用剪辑动画
|
||||
applyClipAnimation(model);
|
||||
} else {
|
||||
// 应用轨道动画
|
||||
applyTrackAnimation(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用剪辑动画
|
||||
*/
|
||||
private void applyClipAnimation(Model2D model) {
|
||||
Map<String, Float> animatedValues = currentClip.sample(currentTime);
|
||||
|
||||
for (Map.Entry<String, Float> entry : animatedValues.entrySet()) {
|
||||
String paramId = entry.getKey();
|
||||
float value = entry.getValue();
|
||||
|
||||
// 应用权重和混合模式
|
||||
float finalValue = applyBlending(model, paramId, value);
|
||||
|
||||
// 设置参数值
|
||||
model.setParameterValue(paramId, finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用轨道动画
|
||||
*/
|
||||
private void applyTrackAnimation(Model2D model) {
|
||||
for (AnimationTrack track : tracks.values()) {
|
||||
if (track.isEnabled()) {
|
||||
float value = track.sample(currentTime);
|
||||
String paramId = track.getParameterId();
|
||||
|
||||
// 应用权重和混合模式
|
||||
float finalValue = applyBlending(model, paramId, value);
|
||||
|
||||
// 设置参数值
|
||||
model.setParameterValue(paramId, finalValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用混合模式
|
||||
*/
|
||||
private float applyBlending(Model2D model, String paramId, float newValue) {
|
||||
float currentValue = model.getParameterValue(paramId);
|
||||
float overrideValue = parameterOverrides.getOrDefault(paramId, Float.NaN);
|
||||
|
||||
if (!Float.isNaN(overrideValue)) {
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
switch (blendMode) {
|
||||
case OVERRIDE:
|
||||
return blendOverride(currentValue, newValue);
|
||||
case ADDITIVE:
|
||||
return blendAdditive(currentValue, newValue);
|
||||
case MULTIPLICATIVE:
|
||||
return blendMultiplicative(currentValue, newValue);
|
||||
case AVERAGE:
|
||||
return blendAverage(currentValue, newValue);
|
||||
default:
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
private float blendOverride(float current, float target) {
|
||||
return current + (target - current) * weight;
|
||||
}
|
||||
|
||||
private float blendAdditive(float current, float target) {
|
||||
return current + target * weight;
|
||||
}
|
||||
|
||||
private float blendMultiplicative(float current, float target) {
|
||||
return current * (1.0f + (target - 1.0f) * weight);
|
||||
}
|
||||
|
||||
private float blendAverage(float current, float target) {
|
||||
return (current * (1.0f - weight)) + (target * weight);
|
||||
}
|
||||
|
||||
// ==================== 事件系统 ====================
|
||||
|
||||
/**
|
||||
* 添加动画事件
|
||||
*/
|
||||
public void addEvent(String eventName, float time, Runnable action) {
|
||||
AnimationEvent event = new AnimationEvent(eventName, time, action);
|
||||
events.computeIfAbsent(eventName, k -> new ArrayList<>()).add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并触发事件
|
||||
*/
|
||||
private void checkEvents() {
|
||||
if (currentClip == null) return;
|
||||
|
||||
for (List<AnimationEvent> eventList : events.values()) {
|
||||
for (AnimationEvent event : eventList) {
|
||||
if (!event.isTriggered() && currentTime >= event.getTime()) {
|
||||
event.trigger();
|
||||
notifyEventTriggered(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有事件状态
|
||||
*/
|
||||
public void resetEvents() {
|
||||
for (List<AnimationEvent> eventList : events.values()) {
|
||||
for (AnimationEvent event : eventList) {
|
||||
event.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 参数覆盖 ====================
|
||||
|
||||
/**
|
||||
* 设置参数覆盖值
|
||||
*/
|
||||
public void setParameterOverride(String parameterId, float value) {
|
||||
parameterOverrides.put(parameterId, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除参数覆盖
|
||||
*/
|
||||
public void clearParameterOverride(String parameterId) {
|
||||
parameterOverrides.remove(parameterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有参数覆盖
|
||||
*/
|
||||
public void clearAllOverrides() {
|
||||
parameterOverrides.clear();
|
||||
}
|
||||
|
||||
// ==================== 事件监听器 ====================
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
public void addEventListener(AnimationEventListener listener) {
|
||||
eventListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
public boolean removeEventListener(AnimationEventListener listener) {
|
||||
return eventListeners.remove(listener);
|
||||
}
|
||||
|
||||
private void notifyAnimationStarted(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationStarted(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAnimationStopped(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationStopped(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAnimationPaused(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationPaused(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAnimationResumed(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationResumed(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAnimationCompleted(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationCompleted(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAnimationLooped(AnimationClip clip) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onAnimationLooped(this, clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyEventTriggered(AnimationEvent event) {
|
||||
for (AnimationEventListener listener : eventListeners) {
|
||||
listener.onEventTriggered(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取当前播放进度(0-1)
|
||||
*/
|
||||
public float getProgress() {
|
||||
if (currentClip == null || currentClip.getDuration() == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
return currentTime / currentClip.getDuration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置播放进度
|
||||
*/
|
||||
public void setProgress(float progress) {
|
||||
if (currentClip != null) {
|
||||
currentTime = progress * currentClip.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定时间
|
||||
*/
|
||||
public void seek(float time) {
|
||||
currentTime = Math.max(0.0f, time);
|
||||
if (currentClip != null) {
|
||||
currentTime = Math.min(currentTime, currentClip.getDuration());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建层的深拷贝
|
||||
*/
|
||||
public AnimationLayer copy() {
|
||||
AnimationLayer copy = new AnimationLayer(name + "_copy", weight);
|
||||
copy.enabled = this.enabled;
|
||||
copy.blendMode = this.blendMode;
|
||||
copy.priority = this.priority;
|
||||
copy.playbackSpeed = this.playbackSpeed;
|
||||
copy.looping = this.looping;
|
||||
|
||||
// 拷贝轨道
|
||||
for (AnimationTrack track : this.tracks.values()) {
|
||||
copy.tracks.put(track.getParameterId(), track.copy());
|
||||
}
|
||||
|
||||
// 拷贝剪辑(引用,因为剪辑通常是共享的)
|
||||
copy.clips.addAll(this.clips);
|
||||
|
||||
// 拷贝参数覆盖
|
||||
copy.parameterOverrides.putAll(this.parameterOverrides);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public UUID getUuid() { return uuid; }
|
||||
|
||||
public float getWeight() { return weight; }
|
||||
public void setWeight(float weight) {
|
||||
this.weight = Math.max(0.0f, Math.min(1.0f, weight));
|
||||
}
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public BlendMode getBlendMode() { return blendMode; }
|
||||
public void setBlendMode(BlendMode blendMode) { this.blendMode = blendMode; }
|
||||
|
||||
public int getPriority() { return priority; }
|
||||
public void setPriority(int priority) { this.priority = priority; }
|
||||
|
||||
public Map<String, AnimationTrack> getTracks() {
|
||||
return Collections.unmodifiableMap(tracks);
|
||||
}
|
||||
|
||||
public List<AnimationClip> getClips() {
|
||||
return Collections.unmodifiableList(clips);
|
||||
}
|
||||
|
||||
public AnimationClip getCurrentClip() { return currentClip; }
|
||||
|
||||
public float getPlaybackSpeed() { return playbackSpeed; }
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
this.playbackSpeed = Math.max(0.0f, playbackSpeed);
|
||||
}
|
||||
|
||||
public boolean isLooping() { return looping; }
|
||||
public void setLooping(boolean looping) { this.looping = looping; }
|
||||
|
||||
public float getCurrentTime() { return currentTime; }
|
||||
|
||||
public boolean isPlaying() { return playing; }
|
||||
public boolean isPaused() { return paused; }
|
||||
|
||||
public Map<String, Float> getParameterOverrides() {
|
||||
return Collections.unmodifiableMap(parameterOverrides);
|
||||
}
|
||||
|
||||
// ==================== 枚举和内部类 ====================
|
||||
|
||||
/**
|
||||
* 混合模式枚举
|
||||
*/
|
||||
public enum BlendMode {
|
||||
OVERRIDE, // 覆盖混合
|
||||
ADDITIVE, // 叠加混合
|
||||
MULTIPLICATIVE, // 乘法混合
|
||||
AVERAGE // 平均混合
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画轨道类
|
||||
*/
|
||||
public static class AnimationTrack {
|
||||
private final String parameterId;
|
||||
private final List<Keyframe> keyframes;
|
||||
private boolean enabled;
|
||||
private InterpolationType interpolation;
|
||||
|
||||
public AnimationTrack(String parameterId) {
|
||||
this.parameterId = parameterId;
|
||||
this.keyframes = new ArrayList<>();
|
||||
this.enabled = true;
|
||||
this.interpolation = InterpolationType.LINEAR;
|
||||
}
|
||||
|
||||
public void addKeyframe(float time, float value) {
|
||||
addKeyframe(time, value, interpolation);
|
||||
}
|
||||
|
||||
public void addKeyframe(float time, float value, InterpolationType interpolation) {
|
||||
Keyframe keyframe = new Keyframe(time, value, interpolation);
|
||||
|
||||
// 按时间排序插入
|
||||
int index = 0;
|
||||
while (index < keyframes.size() && keyframes.get(index).getTime() < time) {
|
||||
index++;
|
||||
}
|
||||
keyframes.add(index, keyframe);
|
||||
}
|
||||
|
||||
public float sample(float time) {
|
||||
if (keyframes.isEmpty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// 在第一个关键帧之前
|
||||
if (time <= keyframes.get(0).getTime()) {
|
||||
return keyframes.get(0).getValue();
|
||||
}
|
||||
|
||||
// 在最后一个关键帧之后
|
||||
if (time >= keyframes.get(keyframes.size() - 1).getTime()) {
|
||||
return keyframes.get(keyframes.size() - 1).getValue();
|
||||
}
|
||||
|
||||
// 找到包围时间的关键帧
|
||||
for (int i = 0; i < keyframes.size() - 1; i++) {
|
||||
Keyframe kf1 = keyframes.get(i);
|
||||
Keyframe kf2 = keyframes.get(i + 1);
|
||||
|
||||
if (time >= kf1.getTime() && time <= kf2.getTime()) {
|
||||
return interpolate(kf1, kf2, time);
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
private float interpolate(Keyframe kf1, Keyframe kf2, float time) {
|
||||
float t = (time - kf1.getTime()) / (kf2.getTime() - kf1.getTime());
|
||||
|
||||
switch (kf1.getInterpolation()) {
|
||||
case LINEAR:
|
||||
return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * t;
|
||||
case STEP:
|
||||
return kf1.getValue();
|
||||
case SMOOTH:
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
return kf1.getValue() * (2 * t3 - 3 * t2 + 1) +
|
||||
kf2.getValue() * (-2 * t3 + 3 * t2);
|
||||
case EASE_IN:
|
||||
return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * (t * t);
|
||||
case EASE_OUT:
|
||||
return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * (1 - (1 - t) * (1 - t));
|
||||
default:
|
||||
return kf1.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
public AnimationTrack copy() {
|
||||
AnimationTrack copy = new AnimationTrack(parameterId);
|
||||
copy.enabled = this.enabled;
|
||||
copy.interpolation = this.interpolation;
|
||||
for (Keyframe kf : this.keyframes) {
|
||||
copy.keyframes.add(kf.copy());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public String getParameterId() { return parameterId; }
|
||||
public List<Keyframe> getKeyframes() { return Collections.unmodifiableList(keyframes); }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public InterpolationType getInterpolation() { return interpolation; }
|
||||
public void setInterpolation(InterpolationType interpolation) { this.interpolation = interpolation; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键帧类
|
||||
*/
|
||||
public static class Keyframe {
|
||||
private final float time;
|
||||
private final float value;
|
||||
private final InterpolationType interpolation;
|
||||
|
||||
public Keyframe(float time, float value, InterpolationType interpolation) {
|
||||
this.time = time;
|
||||
this.value = value;
|
||||
this.interpolation = interpolation;
|
||||
}
|
||||
|
||||
public Keyframe copy() {
|
||||
return new Keyframe(time, value, interpolation);
|
||||
}
|
||||
|
||||
// Getter
|
||||
public float getTime() { return time; }
|
||||
public float getValue() { return value; }
|
||||
public InterpolationType getInterpolation() { return interpolation; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值类型枚举
|
||||
*/
|
||||
public enum InterpolationType {
|
||||
LINEAR, // 线性插值
|
||||
STEP, // 步进插值
|
||||
SMOOTH, // 平滑插值
|
||||
EASE_IN, // 缓入
|
||||
EASE_OUT // 缓出
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画事件类
|
||||
*/
|
||||
public static class AnimationEvent {
|
||||
private final String name;
|
||||
private final float time;
|
||||
private final Runnable action;
|
||||
private boolean triggered;
|
||||
|
||||
public AnimationEvent(String name, float time, Runnable action) {
|
||||
this.name = name;
|
||||
this.time = time;
|
||||
this.action = action;
|
||||
this.triggered = false;
|
||||
}
|
||||
|
||||
public void trigger() {
|
||||
if (!triggered && action != null) {
|
||||
action.run();
|
||||
triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
triggered = false;
|
||||
}
|
||||
|
||||
// Getter
|
||||
public String getName() { return name; }
|
||||
public float getTime() { return time; }
|
||||
public boolean isTriggered() { return triggered; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画事件监听器接口
|
||||
*/
|
||||
public interface AnimationEventListener {
|
||||
void onAnimationStarted(AnimationLayer layer, AnimationClip clip);
|
||||
void onAnimationStopped(AnimationLayer layer, AnimationClip clip);
|
||||
void onAnimationPaused(AnimationLayer layer, AnimationClip clip);
|
||||
void onAnimationResumed(AnimationLayer layer, AnimationClip clip);
|
||||
void onAnimationCompleted(AnimationLayer layer, AnimationClip clip);
|
||||
void onAnimationLooped(AnimationLayer layer, AnimationClip clip);
|
||||
void onEventTriggered(AnimationLayer layer, AnimationEvent event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的动画事件监听器适配器
|
||||
*/
|
||||
public static abstract class AnimationEventAdapter implements AnimationEventListener {
|
||||
@Override public void onAnimationStarted(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onAnimationStopped(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onAnimationPaused(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onAnimationResumed(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onAnimationCompleted(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onAnimationLooped(AnimationLayer layer, AnimationClip clip) {}
|
||||
@Override public void onEventTriggered(AnimationLayer layer, AnimationEvent event) {}
|
||||
}
|
||||
|
||||
// ==================== Object 方法 ====================
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AnimationLayer{" +
|
||||
"name='" + name + '\'' +
|
||||
", weight=" + weight +
|
||||
", enabled=" + enabled +
|
||||
", blendMode=" + blendMode +
|
||||
", tracks=" + tracks.size() +
|
||||
", clips=" + clips.size() +
|
||||
", playing=" + playing +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 2D边界框类,用于表示和管理2D对象的轴对齐边界框(AABB)
|
||||
* 支持变换、合并、相交检测等操作
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BoundingBox {
|
||||
// ==================== 边界数据 ====================
|
||||
private float minX;
|
||||
private float minY;
|
||||
private float maxX;
|
||||
private float maxY;
|
||||
|
||||
// ==================== 状态标记 ====================
|
||||
private boolean valid;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
/**
|
||||
* 创建未初始化的边界框
|
||||
*/
|
||||
public BoundingBox() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从最小/最大值创建边界框
|
||||
*/
|
||||
public BoundingBox(float minX, float minY, float maxX, float maxY) {
|
||||
set(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从两个点创建边界框
|
||||
*/
|
||||
public BoundingBox(Vector2f point1, Vector2f point2) {
|
||||
set(point1, point2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝构造器
|
||||
*/
|
||||
public BoundingBox(BoundingBox other) {
|
||||
set(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从点数组创建边界框
|
||||
*/
|
||||
public BoundingBox(Vector2f[] points) {
|
||||
set(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从顶点数组创建边界框 [x0, y0, x1, y1, ...]
|
||||
*/
|
||||
public BoundingBox(float[] vertices) {
|
||||
set(vertices);
|
||||
}
|
||||
|
||||
// ==================== 设置方法 ====================
|
||||
|
||||
/**
|
||||
* 重置为无效状态
|
||||
*/
|
||||
public void reset() {
|
||||
minX = Float.MAX_VALUE;
|
||||
minY = Float.MAX_VALUE;
|
||||
maxX = -Float.MAX_VALUE;
|
||||
maxY = -Float.MAX_VALUE;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置边界值
|
||||
*/
|
||||
public void set(float minX, float minY, float maxX, float maxY) {
|
||||
if (minX > maxX || minY > maxY) {
|
||||
throw new IllegalArgumentException("Min values must be less than or equal to max values");
|
||||
}
|
||||
|
||||
this.minX = minX;
|
||||
this.minY = minY;
|
||||
this.maxX = maxX;
|
||||
this.maxY = maxY;
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从两个点设置边界框
|
||||
*/
|
||||
public void set(Vector2f point1, Vector2f point2) {
|
||||
reset();
|
||||
expand(point1);
|
||||
expand(point2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从另一个边界框设置
|
||||
*/
|
||||
public void set(BoundingBox other) {
|
||||
if (!other.isValid()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.minX = other.minX;
|
||||
this.minY = other.minY;
|
||||
this.maxX = other.maxX;
|
||||
this.maxY = other.maxY;
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从点数组设置边界框
|
||||
*/
|
||||
public void set(Vector2f[] points) {
|
||||
reset();
|
||||
if (points != null) {
|
||||
for (Vector2f point : points) {
|
||||
if (point != null) {
|
||||
expand(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从顶点数组设置边界框 [x0, y0, x1, y1, ...]
|
||||
*/
|
||||
public void set(float[] vertices) {
|
||||
reset();
|
||||
if (vertices != null) {
|
||||
if (vertices.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Vertices array must have even length");
|
||||
}
|
||||
for (int i = 0; i < vertices.length; i += 2) {
|
||||
expand(vertices[i], vertices[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 扩展方法 ====================
|
||||
|
||||
/**
|
||||
* 扩展边界框以包含点
|
||||
*/
|
||||
public void expand(float x, float y) {
|
||||
if (!valid) {
|
||||
minX = maxX = x;
|
||||
minY = maxY = y;
|
||||
valid = true;
|
||||
} else {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void expand(Vector2f point) {
|
||||
if (point != null) {
|
||||
expand(point.x, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展边界框以包含另一个边界框
|
||||
*/
|
||||
public void expand(BoundingBox other) {
|
||||
if (!other.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
set(other);
|
||||
} else {
|
||||
minX = Math.min(minX, other.minX);
|
||||
minY = Math.min(minY, other.minY);
|
||||
maxX = Math.max(maxX, other.maxX);
|
||||
maxY = Math.max(maxY, other.maxY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展边界框以包含点数组
|
||||
*/
|
||||
public void expand(Vector2f[] points) {
|
||||
if (points != null) {
|
||||
for (Vector2f point : points) {
|
||||
if (point != null) {
|
||||
expand(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展边界框以包含顶点数组 [x0, y0, x1, y1, ...]
|
||||
*/
|
||||
public void expand(float[] vertices) {
|
||||
if (vertices != null) {
|
||||
if (vertices.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Vertices array must have even length");
|
||||
}
|
||||
for (int i = 0; i < vertices.length; i += 2) {
|
||||
expand(vertices[i], vertices[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 变换方法 ====================
|
||||
|
||||
/**
|
||||
* 应用矩阵变换到边界框
|
||||
*/
|
||||
public BoundingBox transform(Matrix3f matrix) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
// 变换边界框的四个角点
|
||||
Vector2f[] corners = getCorners();
|
||||
BoundingBox result = new BoundingBox();
|
||||
|
||||
for (Vector2f corner : corners) {
|
||||
Vector2f transformed = Matrix3fUtils.transformPoint(matrix, corner);
|
||||
result.expand(transformed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用平移变换
|
||||
*/
|
||||
public BoundingBox translate(float dx, float dy) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
return new BoundingBox(
|
||||
minX + dx, minY + dy,
|
||||
maxX + dx, maxY + dy
|
||||
);
|
||||
}
|
||||
|
||||
public BoundingBox translate(Vector2f translation) {
|
||||
return translate(translation.x, translation.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用缩放变换
|
||||
*/
|
||||
public BoundingBox scale(float sx, float sy) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
return new BoundingBox(
|
||||
minX * sx, minY * sy,
|
||||
maxX * sx, maxY * sy
|
||||
);
|
||||
}
|
||||
|
||||
public BoundingBox scale(float scale) {
|
||||
return scale(scale, scale);
|
||||
}
|
||||
|
||||
public BoundingBox scale(Vector2f scale) {
|
||||
return scale(scale.x, scale.y);
|
||||
}
|
||||
|
||||
// ==================== 几何计算 ====================
|
||||
|
||||
/**
|
||||
* 获取边界框的四个角点
|
||||
*/
|
||||
public Vector2f[] getCorners() {
|
||||
if (!valid) {
|
||||
return new Vector2f[0];
|
||||
}
|
||||
|
||||
return new Vector2f[] {
|
||||
new Vector2f(minX, minY), // 左下
|
||||
new Vector2f(maxX, minY), // 右下
|
||||
new Vector2f(maxX, maxY), // 右上
|
||||
new Vector2f(minX, maxY) // 左上
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取边界框中心点
|
||||
*/
|
||||
public Vector2f getCenter() {
|
||||
if (!valid) {
|
||||
return new Vector2f();
|
||||
}
|
||||
|
||||
return new Vector2f(
|
||||
(minX + maxX) * 0.5f,
|
||||
(minY + maxY) * 0.5f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取边界框尺寸
|
||||
*/
|
||||
public Vector2f getSize() {
|
||||
if (!valid) {
|
||||
return new Vector2f();
|
||||
}
|
||||
|
||||
return new Vector2f(getWidth(), getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取边界框半尺寸(半径)
|
||||
*/
|
||||
public Vector2f getHalfSize() {
|
||||
if (!valid) {
|
||||
return new Vector2f();
|
||||
}
|
||||
|
||||
return new Vector2f(getWidth() * 0.5f, getHeight() * 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算边界框面积
|
||||
*/
|
||||
public float getArea() {
|
||||
if (!valid) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return getWidth() * getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算边界框周长
|
||||
*/
|
||||
public float getPerimeter() {
|
||||
if (!valid) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 2.0f * (getWidth() + getHeight());
|
||||
}
|
||||
|
||||
// ==================== 相交检测 ====================
|
||||
|
||||
/**
|
||||
* 检查是否包含点
|
||||
*/
|
||||
public boolean contains(float x, float y) {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return x >= minX && x <= maxX && y >= minY && y <= maxY;
|
||||
}
|
||||
|
||||
public boolean contains(Vector2f point) {
|
||||
if (point == null) return false;
|
||||
return contains(point.x, point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否完全包含另一个边界框
|
||||
*/
|
||||
public boolean contains(BoundingBox other) {
|
||||
if (!valid || !other.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return other.minX >= minX && other.maxX <= maxX &&
|
||||
other.minY >= minY && other.maxY <= maxY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否与另一个边界框相交
|
||||
*/
|
||||
public boolean intersects(BoundingBox other) {
|
||||
if (!valid || !other.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(other.maxX < minX || other.minX > maxX ||
|
||||
other.maxY < minY || other.minY > maxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算与另一个边界框的交集
|
||||
*/
|
||||
public BoundingBox intersection(BoundingBox other) {
|
||||
if (!intersects(other)) {
|
||||
return new BoundingBox(); // 返回无效边界框
|
||||
}
|
||||
|
||||
return new BoundingBox(
|
||||
Math.max(minX, other.minX),
|
||||
Math.max(minY, other.minY),
|
||||
Math.min(maxX, other.maxX),
|
||||
Math.min(maxY, other.maxY)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算与另一个边界框的并集
|
||||
*/
|
||||
public BoundingBox union(BoundingBox other) {
|
||||
BoundingBox result = new BoundingBox(this);
|
||||
result.expand(other);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个边界框的合并边界框
|
||||
*/
|
||||
public static BoundingBox merge(BoundingBox box1, BoundingBox box2) {
|
||||
return box1.union(box2);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 对边界框进行膨胀(扩展固定距离)
|
||||
*/
|
||||
public BoundingBox inflate(float amount) {
|
||||
return inflate(amount, amount);
|
||||
}
|
||||
|
||||
public BoundingBox inflate(float dx, float dy) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
return new BoundingBox(
|
||||
minX - dx, minY - dy,
|
||||
maxX + dx, maxY + dy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对边界框进行收缩(缩小固定距离)
|
||||
*/
|
||||
public BoundingBox deflate(float amount) {
|
||||
return deflate(amount, amount);
|
||||
}
|
||||
|
||||
public BoundingBox deflate(float dx, float dy) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
float newMinX = minX + dx;
|
||||
float newMinY = minY + dy;
|
||||
float newMaxX = maxX - dx;
|
||||
float newMaxY = maxY - dy;
|
||||
|
||||
// 检查收缩后是否仍然有效
|
||||
if (newMinX > newMaxX || newMinY > newMaxY) {
|
||||
return new BoundingBox(); // 返回无效边界框
|
||||
}
|
||||
|
||||
return new BoundingBox(newMinX, newMinY, newMaxX, newMaxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将边界框对齐到网格
|
||||
*/
|
||||
public BoundingBox alignToGrid(float gridSize) {
|
||||
if (!valid) {
|
||||
return new BoundingBox();
|
||||
}
|
||||
|
||||
float alignedMinX = (float) Math.floor(minX / gridSize) * gridSize;
|
||||
float alignedMinY = (float) Math.floor(minY / gridSize) * gridSize;
|
||||
float alignedMaxX = (float) Math.ceil(maxX / gridSize) * gridSize;
|
||||
float alignedMaxY = (float) Math.ceil(maxY / gridSize) * gridSize;
|
||||
|
||||
return new BoundingBox(alignedMinX, alignedMinY, alignedMaxX, alignedMaxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算到点的最近距离
|
||||
*/
|
||||
public float distanceTo(float x, float y) {
|
||||
if (!valid) {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
if (contains(x, y)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float dx = Math.max(Math.max(minX - x, 0), x - maxX);
|
||||
float dy = Math.max(Math.max(minY - y, 0), y - maxY);
|
||||
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
public float distanceTo(Vector2f point) {
|
||||
if (point == null) return Float.MAX_VALUE;
|
||||
return distanceTo(point.x, point.y);
|
||||
}
|
||||
|
||||
// ==================== Getter方法 ====================
|
||||
|
||||
public float getMinX() { return minX; }
|
||||
public float getMinY() { return minY; }
|
||||
public float getMaxX() { return maxX; }
|
||||
public float getMaxY() { return maxY; }
|
||||
|
||||
public float getWidth() {
|
||||
return valid ? maxX - minX : 0.0f;
|
||||
}
|
||||
|
||||
public float getHeight() {
|
||||
return valid ? maxY - minY : 0.0f;
|
||||
}
|
||||
|
||||
public float getLeft() { return minX; }
|
||||
public float getRight() { return maxX; }
|
||||
public float getBottom() { return minY; }
|
||||
public float getTop() { return maxY; }
|
||||
|
||||
public boolean isValid() { return valid; }
|
||||
|
||||
// ==================== 静态工厂方法 ====================
|
||||
|
||||
/**
|
||||
* 从点数组创建边界框
|
||||
*/
|
||||
public static BoundingBox fromPoints(Vector2f[] points) {
|
||||
return new BoundingBox(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从顶点数组创建边界框
|
||||
*/
|
||||
public static BoundingBox fromVertices(float[] vertices) {
|
||||
return new BoundingBox(vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含所有边界框的合并边界框
|
||||
*/
|
||||
public static BoundingBox mergeAll(BoundingBox... boxes) {
|
||||
BoundingBox result = new BoundingBox();
|
||||
for (BoundingBox box : boxes) {
|
||||
if (box != null && box.isValid()) {
|
||||
result.expand(box);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== Object方法 ====================
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
BoundingBox that = (BoundingBox) o;
|
||||
|
||||
if (valid != that.valid) return false;
|
||||
if (!valid) return true; // 两个无效边界框视为相等
|
||||
|
||||
return Float.compare(that.minX, minX) == 0 &&
|
||||
Float.compare(that.minY, minY) == 0 &&
|
||||
Float.compare(that.maxX, maxX) == 0 &&
|
||||
Float.compare(that.maxY, maxY) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (!valid) {
|
||||
return Objects.hash(valid);
|
||||
}
|
||||
return Objects.hash(minX, minY, maxX, maxY, valid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!valid) {
|
||||
return "BoundingBox{INVALID}";
|
||||
}
|
||||
|
||||
return String.format("BoundingBox{min=(%.2f, %.2f), max=(%.2f, %.2f), size=(%.2f, %.2f)}",
|
||||
minX, minY, maxX, maxY, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建边界框的深拷贝
|
||||
*/
|
||||
public BoundingBox copy() {
|
||||
return new BoundingBox(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 2D网格变形器基类
|
||||
* 支持顶点变形、参数驱动动画等特性
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public abstract class Deformer {
|
||||
// ==================== 基础属性 ====================
|
||||
protected String name;
|
||||
protected String id;
|
||||
protected boolean enabled = true;
|
||||
protected float weight = 1.0f;
|
||||
|
||||
// ==================== 驱动参数 ====================
|
||||
protected final Set<String> drivenParameters;
|
||||
protected final Map<String, Float> parameterValues;
|
||||
|
||||
// ==================== 变形范围 ====================
|
||||
protected DeformationRange range;
|
||||
protected BlendMode blendMode = BlendMode.REPLACE;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public Deformer() {
|
||||
this("unnamed");
|
||||
}
|
||||
|
||||
public Deformer(String name) {
|
||||
this.name = name;
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.drivenParameters = new HashSet<>();
|
||||
this.parameterValues = new HashMap<>();
|
||||
this.range = new DeformationRange();
|
||||
}
|
||||
|
||||
// ==================== 抽象方法 ====================
|
||||
|
||||
/**
|
||||
* 应用变形到指定网格
|
||||
*/
|
||||
public abstract void applyToMesh(Mesh2D mesh);
|
||||
|
||||
/**
|
||||
* 应用参数值到变形器
|
||||
*/
|
||||
public abstract void apply(float value);
|
||||
|
||||
/**
|
||||
* 重置变形器状态
|
||||
*/
|
||||
public abstract void reset();
|
||||
|
||||
// ==================== 参数驱动系统 ====================
|
||||
|
||||
/**
|
||||
* 检查是否由指定参数驱动
|
||||
*/
|
||||
public boolean isDrivenBy(String paramId) {
|
||||
return drivenParameters.contains(paramId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加驱动参数
|
||||
*/
|
||||
public void addDrivenParameter(String paramId) {
|
||||
drivenParameters.add(paramId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除驱动参数
|
||||
*/
|
||||
public void removeDrivenParameter(String paramId) {
|
||||
drivenParameters.remove(paramId);
|
||||
parameterValues.remove(paramId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数值
|
||||
*/
|
||||
public void setParameterValue(String paramId, float value) {
|
||||
if (drivenParameters.contains(paramId)) {
|
||||
parameterValues.put(paramId, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数值
|
||||
*/
|
||||
public float getParameterValue(String paramId) {
|
||||
return parameterValues.getOrDefault(paramId, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用所有参数到变形器
|
||||
*/
|
||||
public void applyAllParameters() {
|
||||
for (Map.Entry<String, Float> entry : parameterValues.entrySet()) {
|
||||
apply(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 计算变形权重(考虑全局权重和范围衰减)
|
||||
*/
|
||||
protected float computeDeformationWeight(float x, float y) {
|
||||
if (!enabled || weight <= 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float rangeWeight = range.computeWeight(x, y);
|
||||
return weight * rangeWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合顶点位置
|
||||
*/
|
||||
protected void blendVertexPosition(float[] vertices, int vertexIndex,
|
||||
float originalX, float originalY,
|
||||
float deformedX, float deformedY, float weight) {
|
||||
if (weight <= 0.0f) {
|
||||
return; // 保持原位置
|
||||
}
|
||||
|
||||
int baseIndex = vertexIndex * 2;
|
||||
|
||||
if (weight >= 1.0f) {
|
||||
vertices[baseIndex] = deformedX;
|
||||
vertices[baseIndex + 1] = deformedY;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (blendMode) {
|
||||
case ADDITIVE:
|
||||
vertices[baseIndex] += (deformedX - originalX) * weight;
|
||||
vertices[baseIndex + 1] += (deformedY - originalY) * weight;
|
||||
break;
|
||||
case MULTIPLY:
|
||||
vertices[baseIndex] *= (1.0f + (deformedX / originalX - 1.0f) * weight);
|
||||
vertices[baseIndex + 1] *= (1.0f + (deformedY / originalY - 1.0f) * weight);
|
||||
break;
|
||||
case REPLACE:
|
||||
default:
|
||||
vertices[baseIndex] = originalX + (deformedX - originalX) * weight;
|
||||
vertices[baseIndex + 1] = originalY + (deformedY - originalY) * weight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public float getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(float weight) {
|
||||
this.weight = Math.max(0.0f, Math.min(1.0f, weight));
|
||||
}
|
||||
|
||||
public Set<String> getDrivenParameters() {
|
||||
return new HashSet<>(drivenParameters);
|
||||
}
|
||||
|
||||
public DeformationRange getRange() {
|
||||
return range;
|
||||
}
|
||||
|
||||
public void setRange(DeformationRange range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public BlendMode getBlendMode() {
|
||||
return blendMode;
|
||||
}
|
||||
|
||||
public void setBlendMode(BlendMode blendMode) {
|
||||
this.blendMode = blendMode;
|
||||
}
|
||||
|
||||
// ==================== 枚举和内部类 ====================
|
||||
|
||||
/**
|
||||
* 变形混合模式
|
||||
*/
|
||||
public enum BlendMode {
|
||||
REPLACE, // 替换原始位置
|
||||
ADDITIVE, // 叠加变形
|
||||
MULTIPLY // 乘法变形
|
||||
}
|
||||
|
||||
/**
|
||||
* 变形范围控制
|
||||
*/
|
||||
public static class DeformationRange {
|
||||
private Vector2f center = new Vector2f(0, 0);
|
||||
private float radius = 100.0f;
|
||||
private float innerRadius = 0.0f;
|
||||
private float falloff = 2.0f;
|
||||
|
||||
public DeformationRange() {}
|
||||
|
||||
public DeformationRange(Vector2f center, float radius) {
|
||||
this.center.set(center);
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算顶点在变形范围内的权重
|
||||
*/
|
||||
public float computeWeight(float x, float y) {
|
||||
float dx = x - center.x;
|
||||
float dy = y - center.y;
|
||||
float distance = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance <= innerRadius) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
if (distance >= radius) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// 使用平滑衰减函数
|
||||
float normalized = (distance - innerRadius) / (radius - innerRadius);
|
||||
return (float) Math.pow(1.0f - normalized, falloff);
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public Vector2f getCenter() { return new Vector2f(center); }
|
||||
public void setCenter(Vector2f center) { this.center.set(center); }
|
||||
public void setCenter(float x, float y) { this.center.set(x, y); }
|
||||
|
||||
public float getRadius() { return radius; }
|
||||
public void setRadius(float radius) { this.radius = radius; }
|
||||
|
||||
public float getInnerRadius() { return innerRadius; }
|
||||
public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; }
|
||||
|
||||
public float getFalloff() { return falloff; }
|
||||
public void setFalloff(float falloff) { this.falloff = falloff; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Matrix3fUtils {
|
||||
public static Vector2f transformPoint(Matrix3f matrix, Vector2f point, Vector2f dest) {
|
||||
float x = matrix.m00() * point.x + matrix.m01() * point.y + matrix.m02();
|
||||
float y = matrix.m10() * point.x + matrix.m11() * point.y + matrix.m12();
|
||||
return dest.set(x, y);
|
||||
}
|
||||
|
||||
public static Vector2f transformPoint(Matrix3f matrix, Vector2f point) {
|
||||
return transformPoint(matrix, point, new Vector2f());
|
||||
}
|
||||
|
||||
public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point, Vector2f dest) {
|
||||
Matrix3f inverse = new Matrix3f(matrix).invert();
|
||||
return transformPoint(inverse, point, dest);
|
||||
}
|
||||
|
||||
public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point) {
|
||||
return transformPointInverse(matrix, point, new Vector2f());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Objects;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
/**
|
||||
* 2D网格类,用于存储和管理2D模型的几何数据
|
||||
* 支持顶点、UV坐标、索引和变形操作
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Mesh2D {
|
||||
// ==================== 网格数据 ====================
|
||||
private String name;
|
||||
private float[] vertices; // 顶点数据 [x0, y0, x1, y1, ...]
|
||||
private float[] uvs; // UV坐标 [u0, v0, u1, v1, ...]
|
||||
private int[] indices; // 索引数据
|
||||
private float[] originalVertices; // 原始顶点数据(用于变形恢复)
|
||||
|
||||
// ==================== 渲染属性 ====================
|
||||
private Texture texture;
|
||||
private boolean visible = true;
|
||||
private int drawMode = TRIANGLES; // 绘制模式
|
||||
private int vaoId = -1;
|
||||
private int vboId = -1;
|
||||
private int eboId = -1;
|
||||
private int indexCount = 0;
|
||||
private boolean uploaded = false;
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
private boolean dirty = true; // 数据是否已修改
|
||||
private BoundingBox bounds;
|
||||
private boolean boundsDirty = true;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
public static final int POINTS = 0;
|
||||
public static final int LINES = 1;
|
||||
public static final int LINE_STRIP = 2;
|
||||
public static final int TRIANGLES = 3;
|
||||
public static final int TRIANGLE_STRIP = 4;
|
||||
public static final int TRIANGLE_FAN = 5;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public Mesh2D() {
|
||||
this("unnamed");
|
||||
}
|
||||
|
||||
public Mesh2D(String name) {
|
||||
this.name = name;
|
||||
this.vertices = new float[0];
|
||||
this.uvs = new float[0];
|
||||
this.indices = new int[0];
|
||||
this.bounds = new BoundingBox();
|
||||
}
|
||||
|
||||
public Mesh2D(String name, float[] vertices, float[] uvs, int[] indices) {
|
||||
this(name);
|
||||
setMeshData(vertices, uvs, indices);
|
||||
}
|
||||
|
||||
// ==================== 网格数据设置 ====================
|
||||
|
||||
/**
|
||||
* 设置网格数据
|
||||
*/
|
||||
public void setMeshData(float[] vertices, float[] uvs, int[] indices) {
|
||||
if (vertices.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)");
|
||||
}
|
||||
if (uvs.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("UVs array must have even length (u,v pairs)");
|
||||
}
|
||||
if (vertices.length / 2 != uvs.length / 2) {
|
||||
throw new IllegalArgumentException("Vertices and UVs must have same number of points");
|
||||
}
|
||||
|
||||
this.vertices = vertices.clone();
|
||||
this.uvs = uvs.clone();
|
||||
this.indices = indices.clone();
|
||||
this.originalVertices = vertices.clone();
|
||||
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建矩形网格
|
||||
*/
|
||||
public static Mesh2D createQuad(String name, float width, float height) {
|
||||
float hw = width / 2.0f;
|
||||
float hh = height / 2.0f;
|
||||
|
||||
float[] vertices = {
|
||||
-hw, -hh, // 左下
|
||||
hw, -hh, // 右下
|
||||
hw, hh, // 右上
|
||||
-hw, hh // 左上
|
||||
};
|
||||
|
||||
float[] uvs = {
|
||||
0.0f, 1.0f, // 左下
|
||||
1.0f, 1.0f, // 右下
|
||||
1.0f, 0.0f, // 右上
|
||||
0.0f, 0.0f // 左上
|
||||
};
|
||||
|
||||
int[] indices = {
|
||||
0, 1, 2, // 第一个三角形
|
||||
0, 2, 3 // 第二个三角形
|
||||
};
|
||||
|
||||
return new Mesh2D(name, vertices, uvs, indices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形网格
|
||||
*/
|
||||
public static Mesh2D createCircle(String name, float radius, int segments) {
|
||||
if (segments < 3) {
|
||||
segments = 3;
|
||||
}
|
||||
|
||||
int vertexCount = segments + 1; // 中心点 + 边缘点
|
||||
float[] vertices = new float[vertexCount * 2];
|
||||
float[] uvs = new float[vertexCount * 2];
|
||||
int[] indices = new int[segments * 3];
|
||||
|
||||
// 中心点 (索引0)
|
||||
vertices[0] = 0.0f;
|
||||
vertices[1] = 0.0f;
|
||||
uvs[0] = 0.5f;
|
||||
uvs[1] = 0.5f;
|
||||
|
||||
// 边缘点
|
||||
float angleStep = (float) (2.0f * Math.PI / segments);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
float angle = i * angleStep;
|
||||
int vertexIndex = (i + 1) * 2;
|
||||
|
||||
vertices[vertexIndex] = (float) Math.cos(angle) * radius;
|
||||
vertices[vertexIndex + 1] = (float) Math.sin(angle) * radius;
|
||||
|
||||
uvs[vertexIndex] = (float) (Math.cos(angle) * 0.5f + 0.5f);
|
||||
uvs[vertexIndex + 1] = (float) (Math.sin(angle) * 0.5f + 0.5f);
|
||||
|
||||
// 三角形索引
|
||||
int triangleIndex = i * 3;
|
||||
indices[triangleIndex] = 0; // 中心点
|
||||
indices[triangleIndex + 1] = i + 1;
|
||||
indices[triangleIndex + 2] = (i + 1) % segments + 1;
|
||||
}
|
||||
|
||||
return new Mesh2D(name, vertices, uvs, indices);
|
||||
}
|
||||
|
||||
// ==================== 顶点操作 ====================
|
||||
|
||||
/**
|
||||
* 获取顶点数量
|
||||
*/
|
||||
public int getVertexCount() {
|
||||
return vertices.length / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶点位置
|
||||
*/
|
||||
public Vector2f getVertex(int index, Vector2f dest) {
|
||||
if (index < 0 || index >= getVertexCount()) {
|
||||
throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index);
|
||||
}
|
||||
int baseIndex = index * 2;
|
||||
return dest.set(vertices[baseIndex], vertices[baseIndex + 1]);
|
||||
}
|
||||
|
||||
public Vector2f getVertex(int index) {
|
||||
return getVertex(index, new Vector2f());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶点位置
|
||||
*/
|
||||
public void setVertex(int index, float x, float y) {
|
||||
if (index < 0 || index >= getVertexCount()) {
|
||||
throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index);
|
||||
}
|
||||
int baseIndex = index * 2;
|
||||
vertices[baseIndex] = x;
|
||||
vertices[baseIndex + 1] = y;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
public void setVertex(int index, Vector2f position) {
|
||||
setVertex(index, position.x, position.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取UV坐标
|
||||
*/
|
||||
public Vector2f getUV(int index, Vector2f dest) {
|
||||
if (index < 0 || index >= getVertexCount()) {
|
||||
throw new IndexOutOfBoundsException("UV index out of bounds: " + index);
|
||||
}
|
||||
int baseIndex = index * 2;
|
||||
return dest.set(uvs[baseIndex], uvs[baseIndex + 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置UV坐标
|
||||
*/
|
||||
public void setUV(int index, float u, float v) {
|
||||
if (index < 0 || index >= getVertexCount()) {
|
||||
throw new IndexOutOfBoundsException("UV index out of bounds: " + index);
|
||||
}
|
||||
int baseIndex = index * 2;
|
||||
uvs[baseIndex] = u;
|
||||
uvs[baseIndex + 1] = v;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// ==================== 变形支持 ====================
|
||||
|
||||
/**
|
||||
* 重置为原始顶点数据
|
||||
*/
|
||||
public void resetToOriginal() {
|
||||
if (originalVertices != null && originalVertices.length == vertices.length) {
|
||||
System.arraycopy(originalVertices, 0, vertices, 0, vertices.length);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前顶点为原始数据
|
||||
*/
|
||||
public void saveAsOriginal() {
|
||||
originalVertices = vertices.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用变形到所有顶点
|
||||
*/
|
||||
public void transformVertices(VertexTransformer transformer) {
|
||||
for (int i = 0; i < getVertexCount(); i++) {
|
||||
Vector2f vertex = getVertex(i);
|
||||
transformer.transform(vertex, i);
|
||||
setVertex(i, vertex);
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶点变换器接口
|
||||
*/
|
||||
public interface VertexTransformer {
|
||||
void transform(Vector2f vertex, int index);
|
||||
}
|
||||
|
||||
// ==================== 边界计算 ====================
|
||||
|
||||
/**
|
||||
* 更新边界框
|
||||
*/
|
||||
public void updateBounds() {
|
||||
bounds.reset();
|
||||
|
||||
for (int i = 0; i < vertices.length; i += 2) {
|
||||
bounds.expand(vertices[i], vertices[i + 1]);
|
||||
}
|
||||
|
||||
boundsDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取边界框
|
||||
*/
|
||||
public BoundingBox getBounds() {
|
||||
if (boundsDirty) {
|
||||
updateBounds();
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查点是否在网格内(使用边界框近似)
|
||||
*/
|
||||
public boolean containsPoint(float x, float y) {
|
||||
BoundingBox b = getBounds();
|
||||
return x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY();
|
||||
}
|
||||
|
||||
public boolean containsPoint(Vector2f point) {
|
||||
return containsPoint(point.x, point.y);
|
||||
}
|
||||
|
||||
// ==================== 缓冲区支持 ====================
|
||||
|
||||
/**
|
||||
* 获取顶点缓冲区数据
|
||||
*/
|
||||
public FloatBuffer getVertexBuffer(FloatBuffer buffer) {
|
||||
if (buffer == null || buffer.capacity() < vertices.length) {
|
||||
throw new IllegalArgumentException("Buffer is null or too small");
|
||||
}
|
||||
buffer.clear();
|
||||
buffer.put(vertices);
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取UV缓冲区数据
|
||||
*/
|
||||
public FloatBuffer getUVBuffer(FloatBuffer buffer) {
|
||||
if (buffer == null || buffer.capacity() < uvs.length) {
|
||||
throw new IllegalArgumentException("Buffer is null or too small");
|
||||
}
|
||||
buffer.clear();
|
||||
buffer.put(uvs);
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引缓冲区数据
|
||||
*/
|
||||
public IntBuffer getIndexBuffer(IntBuffer buffer) {
|
||||
if (buffer == null || buffer.capacity() < indices.length) {
|
||||
throw new IllegalArgumentException("Buffer is null or too small");
|
||||
}
|
||||
buffer.clear();
|
||||
buffer.put(indices);
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交错的顶点+UV数据(用于VBO)
|
||||
*/
|
||||
public FloatBuffer getInterleavedBuffer(FloatBuffer buffer) {
|
||||
int vertexCount = getVertexCount();
|
||||
int floatCount = vertexCount * 4; // 每个顶点:x, y, u, v
|
||||
|
||||
if (buffer == null || buffer.capacity() < floatCount) {
|
||||
throw new IllegalArgumentException("Buffer is null or too small");
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
for (int i = 0; i < vertexCount; i++) {
|
||||
buffer.put(vertices[i * 2]); // x
|
||||
buffer.put(vertices[i * 2 + 1]); // y
|
||||
buffer.put(uvs[i * 2]); // u
|
||||
buffer.put(uvs[i * 2 + 1]); // v
|
||||
}
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
|
||||
/**
|
||||
* 标记数据已修改
|
||||
*/
|
||||
public void markDirty() {
|
||||
deleteGPU();
|
||||
this.dirty = true;
|
||||
this.boundsDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除脏标记
|
||||
*/
|
||||
public void markClean() {
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据是否已修改
|
||||
*/
|
||||
public boolean isDirty() {
|
||||
return dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将网格数据上传到 GPU(生成 VAO/VBO/EBO)
|
||||
*/
|
||||
public void uploadToGPU() {
|
||||
if (uploaded) return;
|
||||
// 组织 interleaved buffer (x,y,u,v)
|
||||
int vertexCount = getVertexCount();
|
||||
int floatCount = vertexCount * 4; // x,y,u,v
|
||||
FloatBuffer interleaved = MemoryUtil.memAllocFloat(floatCount);
|
||||
try {
|
||||
getInterleavedBuffer(interleaved);
|
||||
IntBuffer ib = MemoryUtil.memAllocInt(indices.length);
|
||||
try {
|
||||
getIndexBuffer(ib);
|
||||
|
||||
vaoId = GL30.glGenVertexArrays();
|
||||
GL30.glBindVertexArray(vaoId);
|
||||
|
||||
vboId = GL15.glGenBuffers();
|
||||
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, interleaved, GL15.GL_STATIC_DRAW);
|
||||
|
||||
eboId = GL15.glGenBuffers();
|
||||
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, ib, GL15.GL_STATIC_DRAW);
|
||||
|
||||
int stride = 4 * Float.BYTES; // x,y,u,v
|
||||
|
||||
// position attrib (location 0) -> vec2
|
||||
GL20.glEnableVertexAttribArray(0);
|
||||
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, stride, 0);
|
||||
|
||||
// uv attrib (location 1) -> vec2
|
||||
GL20.glEnableVertexAttribArray(1);
|
||||
GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, stride, 2 * Float.BYTES);
|
||||
|
||||
// unbind VAO (keep EBO bound to VAO on unbind)
|
||||
GL30.glBindVertexArray(0);
|
||||
|
||||
indexCount = indices.length;
|
||||
uploaded = true;
|
||||
markClean();
|
||||
} finally {
|
||||
MemoryUtil.memFree(ib);
|
||||
}
|
||||
} finally {
|
||||
MemoryUtil.memFree(interleaved);
|
||||
// unbind array buffer
|
||||
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制网格(会在第一次绘制时自动上传到 GPU)
|
||||
*/
|
||||
public void draw() {
|
||||
if (!visible) return;
|
||||
if (indices == null || indices.length == 0) return;
|
||||
|
||||
if (!uploaded) {
|
||||
uploadToGPU();
|
||||
}
|
||||
|
||||
if (texture != null) {
|
||||
// 假设 Texture 提供 bind()/unbind() 方法
|
||||
texture.bind();
|
||||
}
|
||||
|
||||
GL30.glBindVertexArray(vaoId);
|
||||
GL11.glDrawElements(GL11.GL_TRIANGLES, indexCount, GL11.GL_UNSIGNED_INT, 0);
|
||||
GL30.glBindVertexArray(0);
|
||||
|
||||
if (texture != null) {
|
||||
texture.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 GPU 删除本网格相关的 VAO/VBO/EBO
|
||||
*/
|
||||
public void deleteGPU() {
|
||||
if (!uploaded) return;
|
||||
// 禁用属性并删除缓冲
|
||||
try {
|
||||
GL30.glBindVertexArray(0);
|
||||
if (vboId != -1) {
|
||||
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GL15.glDeleteBuffers(vboId);
|
||||
vboId = -1;
|
||||
}
|
||||
if (eboId != -1) {
|
||||
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
GL15.glDeleteBuffers(eboId);
|
||||
eboId = -1;
|
||||
}
|
||||
if (vaoId != -1) {
|
||||
GL30.glDeleteVertexArrays(vaoId);
|
||||
vaoId = -1;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 在某些上下文销毁阶段 GL 调用可能不可用
|
||||
} finally {
|
||||
uploaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public float[] getVertices() {
|
||||
return vertices.clone();
|
||||
}
|
||||
|
||||
public float[] getUVs() {
|
||||
return uvs.clone();
|
||||
}
|
||||
|
||||
public int[] getIndices() {
|
||||
return indices.clone();
|
||||
}
|
||||
|
||||
public Texture getTexture() {
|
||||
return texture;
|
||||
}
|
||||
|
||||
public void setTexture(Texture texture) {
|
||||
this.texture = texture;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public int getDrawMode() {
|
||||
return drawMode;
|
||||
}
|
||||
|
||||
public void setDrawMode(int drawMode) {
|
||||
if (drawMode < POINTS || drawMode > TRIANGLE_FAN) {
|
||||
throw new IllegalArgumentException("Invalid draw mode: " + drawMode);
|
||||
}
|
||||
this.drawMode = drawMode;
|
||||
}
|
||||
|
||||
public int getIndexCount() {
|
||||
return indices.length;
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 创建网格的深拷贝
|
||||
*/
|
||||
public Mesh2D copy() {
|
||||
Mesh2D copy = new Mesh2D(name + "_copy");
|
||||
copy.setMeshData(vertices, uvs, indices);
|
||||
copy.texture = texture;
|
||||
copy.visible = visible;
|
||||
copy.drawMode = drawMode;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绘制模式字符串
|
||||
*/
|
||||
public String getDrawModeString() {
|
||||
switch (drawMode) {
|
||||
case POINTS: return "POINTS";
|
||||
case LINES: return "LINES";
|
||||
case LINE_STRIP: return "LINE_STRIP";
|
||||
case TRIANGLES: return "TRIANGLES";
|
||||
case TRIANGLE_STRIP: return "TRIANGLE_STRIP";
|
||||
case TRIANGLE_FAN: return "TRIANGLE_FAN";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Object 方法 ====================
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Mesh2D mesh2D = (Mesh2D) o;
|
||||
return visible == mesh2D.visible &&
|
||||
drawMode == mesh2D.drawMode &&
|
||||
Objects.equals(name, mesh2D.name) &&
|
||||
Objects.deepEquals(vertices, mesh2D.vertices) &&
|
||||
Objects.deepEquals(uvs, mesh2D.uvs) &&
|
||||
Objects.deepEquals(indices, mesh2D.indices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name,
|
||||
java.util.Arrays.hashCode(vertices),
|
||||
java.util.Arrays.hashCode(uvs),
|
||||
java.util.Arrays.hashCode(indices),
|
||||
visible, drawMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mesh2D{" +
|
||||
"name='" + name + '\'' +
|
||||
", vertices=" + getVertexCount() +
|
||||
", indices=" + indices.length +
|
||||
", visible=" + visible +
|
||||
", drawMode=" + getDrawModeString() +
|
||||
", bounds=" + getBounds() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 模型元数据类
|
||||
* 用于存储模型的描述性信息、创建信息、版本信息等
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class ModelMetadata implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// ==================== 基础信息 ====================
|
||||
private String name;
|
||||
private String version;
|
||||
private UUID uuid;
|
||||
private String description;
|
||||
|
||||
// ==================== 创建信息 ====================
|
||||
private String author;
|
||||
private String creator;
|
||||
private String copyright;
|
||||
private String license;
|
||||
private long creationTime;
|
||||
private long lastModifiedTime;
|
||||
|
||||
// ==================== 技术信息 ====================
|
||||
private String fileFormatVersion;
|
||||
private int vertexCount;
|
||||
private int polygonCount;
|
||||
private int textureCount;
|
||||
private int parameterCount;
|
||||
private int partCount;
|
||||
|
||||
// ==================== 渲染设置 ====================
|
||||
private Vector2f pivotPoint;
|
||||
private float unitsPerMeter;
|
||||
private boolean visibleInScene;
|
||||
|
||||
// ==================== 用户数据 ====================
|
||||
private Map<String, String> userProperties;
|
||||
private List<String> tags;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public ModelMetadata() {
|
||||
this("unnamed", "1.0.0");
|
||||
}
|
||||
|
||||
public ModelMetadata(String name) {
|
||||
this(name, "1.0.0");
|
||||
}
|
||||
|
||||
public ModelMetadata(String name, String version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.creationTime = System.currentTimeMillis();
|
||||
this.lastModifiedTime = creationTime;
|
||||
|
||||
// 初始化默认值
|
||||
this.pivotPoint = new Vector2f();
|
||||
this.unitsPerMeter = 100.0f;
|
||||
this.visibleInScene = true;
|
||||
|
||||
this.userProperties = new HashMap<>();
|
||||
this.tags = new ArrayList<>();
|
||||
}
|
||||
|
||||
// ==================== 基础信息方法 ====================
|
||||
|
||||
/**
|
||||
* 验证元数据的基本完整性
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return name != null && !name.trim().isEmpty() &&
|
||||
version != null && !version.trim().isEmpty() &&
|
||||
uuid != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型的显示名称
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return "Unnamed Model";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的版本信息
|
||||
*/
|
||||
public String getFullVersion() {
|
||||
if (fileFormatVersion != null) {
|
||||
return version + " (Format: " + fileFormatVersion + ")";
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
// ==================== 时间管理 ====================
|
||||
|
||||
/**
|
||||
* 标记为已修改
|
||||
*/
|
||||
public void markModified() {
|
||||
this.lastModifiedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型年龄(以天为单位)
|
||||
*/
|
||||
public long getAgeInDays() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long ageMillis = currentTime - creationTime;
|
||||
return ageMillis / (1000 * 60 * 60 * 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后修改后的时间(以小时为单位)
|
||||
*/
|
||||
public long getHoursSinceLastModified() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long diffMillis = currentTime - lastModifiedTime;
|
||||
return diffMillis / (1000 * 60 * 60);
|
||||
}
|
||||
|
||||
// ==================== 标签管理 ====================
|
||||
|
||||
/**
|
||||
* 添加标签
|
||||
*/
|
||||
public void addTag(String tag) {
|
||||
if (tag != null && !tag.trim().isEmpty() && !tags.contains(tag)) {
|
||||
tags.add(tag);
|
||||
markModified();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除标签
|
||||
*/
|
||||
public boolean removeTag(String tag) {
|
||||
boolean removed = tags.remove(tag);
|
||||
if (removed) {
|
||||
markModified();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含标签
|
||||
*/
|
||||
public boolean hasTag(String tag) {
|
||||
return tags.contains(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含任何指定的标签
|
||||
*/
|
||||
public boolean hasAnyTag(String... searchTags) {
|
||||
for (String tag : searchTags) {
|
||||
if (tags.contains(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含所有指定的标签
|
||||
*/
|
||||
public boolean hasAllTags(String... searchTags) {
|
||||
for (String tag : searchTags) {
|
||||
if (!tags.contains(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 用户属性管理 ====================
|
||||
|
||||
/**
|
||||
* 设置用户属性
|
||||
*/
|
||||
public void setProperty(String key, String value) {
|
||||
if (key != null && !key.trim().isEmpty()) {
|
||||
userProperties.put(key, value);
|
||||
markModified();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户属性
|
||||
*/
|
||||
public String getProperty(String key) {
|
||||
return userProperties.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户属性,如果不存在则返回默认值
|
||||
*/
|
||||
public String getProperty(String key, String defaultValue) {
|
||||
return userProperties.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户属性
|
||||
*/
|
||||
public String removeProperty(String key) {
|
||||
String removed = userProperties.remove(key);
|
||||
if (removed != null) {
|
||||
markModified();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在属性
|
||||
*/
|
||||
public boolean hasProperty(String key) {
|
||||
return userProperties.containsKey(key);
|
||||
}
|
||||
|
||||
// ==================== 统计信息方法 ====================
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
public void updateStatistics(int vertexCount, int polygonCount, int textureCount,
|
||||
int parameterCount, int partCount) {
|
||||
this.vertexCount = vertexCount;
|
||||
this.polygonCount = polygonCount;
|
||||
this.textureCount = textureCount;
|
||||
this.parameterCount = parameterCount;
|
||||
this.partCount = partCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型复杂度评级
|
||||
*/
|
||||
public ComplexityRating getComplexityRating() {
|
||||
int totalComplexity = vertexCount + (polygonCount * 10) +
|
||||
(textureCount * 100) + (parameterCount * 5) +
|
||||
(partCount * 20);
|
||||
|
||||
if (totalComplexity < 1000) {
|
||||
return ComplexityRating.VERY_SIMPLE;
|
||||
} else if (totalComplexity < 5000) {
|
||||
return ComplexityRating.SIMPLE;
|
||||
} else if (totalComplexity < 20000) {
|
||||
return ComplexityRating.MEDIUM;
|
||||
} else if (totalComplexity < 50000) {
|
||||
return ComplexityRating.COMPLEX;
|
||||
} else {
|
||||
return ComplexityRating.VERY_COMPLEX;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取估计的文件大小(字节)
|
||||
*/
|
||||
public long getEstimatedFileSize() {
|
||||
// 粗略估算:顶点数据 + 纹理数据 + 其他开销
|
||||
long vertexDataSize = (long) vertexCount * 8 * 2; // 每个顶点8字节(float x,y),2份(原始+变形)
|
||||
long textureDataSize = (long) textureCount * 1024 * 1024; // 假设每个纹理1MB
|
||||
long otherDataSize = (long) (parameterCount * 16 + partCount * 64 + polygonCount * 12);
|
||||
|
||||
return vertexDataSize + textureDataSize + otherDataSize + 1024; // +1KB元数据
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 创建深拷贝
|
||||
*/
|
||||
@Override
|
||||
public ModelMetadata clone() {
|
||||
try {
|
||||
ModelMetadata clone = (ModelMetadata) super.clone();
|
||||
|
||||
// 深拷贝可变对象
|
||||
clone.pivotPoint = new Vector2f(this.pivotPoint);
|
||||
clone.userProperties = new HashMap<>(this.userProperties);
|
||||
clone.tags = new ArrayList<>(this.tags);
|
||||
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError("Clone should be supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带有新名称的拷贝
|
||||
*/
|
||||
public ModelMetadata copyWithName(String newName) {
|
||||
ModelMetadata copy = clone();
|
||||
copy.name = newName;
|
||||
copy.uuid = UUID.randomUUID();
|
||||
copy.creationTime = System.currentTimeMillis();
|
||||
copy.lastModifiedTime = copy.creationTime;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并另一个元数据(主要用于模型合并)
|
||||
*/
|
||||
public void merge(ModelMetadata other) {
|
||||
if (other == null) return;
|
||||
|
||||
// 合并描述
|
||||
if (this.description == null || this.description.isEmpty()) {
|
||||
this.description = other.description;
|
||||
} else if (other.description != null && !other.description.isEmpty()) {
|
||||
this.description += "; " + other.description;
|
||||
}
|
||||
|
||||
// 合并作者信息
|
||||
if (this.author == null || this.author.isEmpty()) {
|
||||
this.author = other.author;
|
||||
} else if (other.author != null && !other.author.isEmpty()) {
|
||||
this.author += ", " + other.author;
|
||||
}
|
||||
|
||||
// 合并标签(去重)
|
||||
for (String tag : other.tags) {
|
||||
if (!this.tags.contains(tag)) {
|
||||
this.tags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并用户属性(不覆盖现有属性)
|
||||
for (Map.Entry<String, String> entry : other.userProperties.entrySet()) {
|
||||
if (!this.userProperties.containsKey(entry.getKey())) {
|
||||
this.userProperties.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
markModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为简化的信息映射
|
||||
*/
|
||||
public Map<String, Object> toInfoMap() {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
|
||||
info.put("name", name);
|
||||
info.put("version", version);
|
||||
info.put("uuid", uuid.toString());
|
||||
info.put("author", author != null ? author : "Unknown");
|
||||
info.put("description", description != null ? description : "No description");
|
||||
info.put("creationTime", new Date(creationTime));
|
||||
info.put("lastModifiedTime", new Date(lastModifiedTime));
|
||||
info.put("vertexCount", vertexCount);
|
||||
info.put("polygonCount", polygonCount);
|
||||
info.put("textureCount", textureCount);
|
||||
info.put("parameterCount", parameterCount);
|
||||
info.put("partCount", partCount);
|
||||
info.put("complexity", getComplexityRating().toString());
|
||||
info.put("tags", String.join(", ", tags));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// ==================== 枚举和内部类 ====================
|
||||
|
||||
/**
|
||||
* 模型复杂度评级
|
||||
*/
|
||||
public enum ComplexityRating {
|
||||
VERY_SIMPLE("非常简单", "适合初学者"),
|
||||
SIMPLE("简单", "基础模型"),
|
||||
MEDIUM("中等", "标准模型"),
|
||||
COMPLEX("复杂", "高级模型"),
|
||||
VERY_COMPLEX("非常复杂", "专业级模型");
|
||||
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
||||
ComplexityRating(String displayName, String description) {
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName + " (" + description + ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
public void setCreator(String creator) {
|
||||
this.creator = creator;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getCopyright() {
|
||||
return copyright;
|
||||
}
|
||||
|
||||
public void setCopyright(String copyright) {
|
||||
this.copyright = copyright;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public String getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
public void setLicense(String license) {
|
||||
this.license = license;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public long getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
public void setCreationTime(long creationTime) {
|
||||
this.creationTime = creationTime;
|
||||
// 不标记修改,因为创建时间通常不应该改变
|
||||
}
|
||||
|
||||
public long getLastModifiedTime() {
|
||||
return lastModifiedTime;
|
||||
}
|
||||
|
||||
public void setLastModifiedTime(long lastModifiedTime) {
|
||||
this.lastModifiedTime = lastModifiedTime;
|
||||
// 不标记修改,避免循环调用
|
||||
}
|
||||
|
||||
public String getFileFormatVersion() {
|
||||
return fileFormatVersion;
|
||||
}
|
||||
|
||||
public void setFileFormatVersion(String fileFormatVersion) {
|
||||
this.fileFormatVersion = fileFormatVersion;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public int getVertexCount() {
|
||||
return vertexCount;
|
||||
}
|
||||
|
||||
public void setVertexCount(int vertexCount) {
|
||||
this.vertexCount = vertexCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public int getPolygonCount() {
|
||||
return polygonCount;
|
||||
}
|
||||
|
||||
public void setPolygonCount(int polygonCount) {
|
||||
this.polygonCount = polygonCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public int getTextureCount() {
|
||||
return textureCount;
|
||||
}
|
||||
|
||||
public void setTextureCount(int textureCount) {
|
||||
this.textureCount = textureCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public int getParameterCount() {
|
||||
return parameterCount;
|
||||
}
|
||||
|
||||
public void setParameterCount(int parameterCount) {
|
||||
this.parameterCount = parameterCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public int getPartCount() {
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public void setPartCount(int partCount) {
|
||||
this.partCount = partCount;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public Vector2f getPivotPoint() {
|
||||
return pivotPoint;
|
||||
}
|
||||
|
||||
public void setPivotPoint(Vector2f pivotPoint) {
|
||||
this.pivotPoint = pivotPoint;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public float getUnitsPerMeter() {
|
||||
return unitsPerMeter;
|
||||
}
|
||||
|
||||
public void setUnitsPerMeter(float unitsPerMeter) {
|
||||
this.unitsPerMeter = unitsPerMeter;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public boolean isVisibleInScene() {
|
||||
return visibleInScene;
|
||||
}
|
||||
|
||||
public void setVisibleInScene(boolean visibleInScene) {
|
||||
this.visibleInScene = visibleInScene;
|
||||
markModified();
|
||||
}
|
||||
|
||||
public Map<String, String> getUserProperties() {
|
||||
return Collections.unmodifiableMap(userProperties);
|
||||
}
|
||||
|
||||
public void setUserProperties(Map<String, String> userProperties) {
|
||||
this.userProperties = new HashMap<>(userProperties);
|
||||
markModified();
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return Collections.unmodifiableList(tags);
|
||||
}
|
||||
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags = new ArrayList<>(tags);
|
||||
markModified();
|
||||
}
|
||||
|
||||
// ==================== Object 方法 ====================
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModelMetadata{" +
|
||||
"name='" + name + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", uuid=" + uuid +
|
||||
", author='" + author + '\'' +
|
||||
", vertexCount=" + vertexCount +
|
||||
", polygonCount=" + polygonCount +
|
||||
", tags=" + tags.size() +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ModelMetadata that = (ModelMetadata) o;
|
||||
return creationTime == that.creationTime &&
|
||||
Objects.equals(uuid, that.uuid) &&
|
||||
Objects.equals(name, that.name) &&
|
||||
Objects.equals(version, that.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, version, uuid, creationTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
public class ModelPose {
|
||||
}
|
||||
@@ -0,0 +1,942 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 2D物理系统,用于处理模型的物理模拟
|
||||
* 支持弹簧系统、碰撞检测、重力等物理效果
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class PhysicsSystem {
|
||||
// ==================== 物理参数 ====================
|
||||
private final Vector2f gravity;
|
||||
private float airResistance;
|
||||
private float timeScale;
|
||||
private boolean enabled;
|
||||
|
||||
// ==================== 物理组件 ====================
|
||||
private final Map<String, PhysicsParticle> particles;
|
||||
private final List<PhysicsSpring> springs;
|
||||
private final List<PhysicsConstraint> constraints;
|
||||
private final List<PhysicsCollider> colliders;
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
private boolean initialized;
|
||||
private long lastUpdateTime;
|
||||
private float accumulatedTime;
|
||||
private final int maxSubSteps;
|
||||
private final float fixedTimeStep;
|
||||
|
||||
// ==================== 性能统计 ====================
|
||||
private int updateCount;
|
||||
private float averageUpdateTime;
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public PhysicsSystem() {
|
||||
this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力
|
||||
this.airResistance = 0.1f;
|
||||
this.timeScale = 1.0f;
|
||||
this.enabled = true;
|
||||
|
||||
this.particles = new ConcurrentHashMap<>();
|
||||
this.springs = new ArrayList<>();
|
||||
this.constraints = new ArrayList<>();
|
||||
this.colliders = new ArrayList<>();
|
||||
|
||||
this.initialized = false;
|
||||
this.lastUpdateTime = System.nanoTime();
|
||||
this.accumulatedTime = 0.0f;
|
||||
this.maxSubSteps = 5;
|
||||
this.fixedTimeStep = 1.0f / 60.0f; // 60 FPS物理更新
|
||||
|
||||
this.updateCount = 0;
|
||||
this.averageUpdateTime = 0.0f;
|
||||
}
|
||||
|
||||
// ==================== 初始化方法 ====================
|
||||
|
||||
/**
|
||||
* 初始化物理系统
|
||||
*/
|
||||
public void initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
reset();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置物理系统
|
||||
*/
|
||||
public void reset() {
|
||||
particles.clear();
|
||||
springs.clear();
|
||||
constraints.clear();
|
||||
colliders.clear();
|
||||
|
||||
lastUpdateTime = System.nanoTime();
|
||||
accumulatedTime = 0.0f;
|
||||
updateCount = 0;
|
||||
averageUpdateTime = 0.0f;
|
||||
}
|
||||
|
||||
// ==================== 粒子管理 ====================
|
||||
|
||||
/**
|
||||
* 添加物理粒子
|
||||
*/
|
||||
public PhysicsParticle addParticle(String id, Vector2f position, float mass) {
|
||||
PhysicsParticle particle = new PhysicsParticle(id, position, mass);
|
||||
particles.put(id, particle);
|
||||
return particle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从模型部件创建粒子
|
||||
*/
|
||||
public PhysicsParticle addParticleFromModelPart(String id, ModelPart part, float mass) {
|
||||
Vector2f position = part.getPosition();
|
||||
PhysicsParticle particle = addParticle(id, position, mass);
|
||||
particle.setUserData(part);
|
||||
return particle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除粒子
|
||||
*/
|
||||
public boolean removeParticle(String id) {
|
||||
// 移除相关的弹簧和约束
|
||||
springs.removeIf(spring ->
|
||||
spring.getParticleA().getId().equals(id) ||
|
||||
spring.getParticleB().getId().equals(id));
|
||||
|
||||
constraints.removeIf(constraint ->
|
||||
constraint.getParticle().getId().equals(id));
|
||||
|
||||
return particles.remove(id) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粒子
|
||||
*/
|
||||
public PhysicsParticle getParticle(String id) {
|
||||
return particles.get(id);
|
||||
}
|
||||
|
||||
// ==================== 弹簧管理 ====================
|
||||
|
||||
/**
|
||||
* 添加弹簧
|
||||
*/
|
||||
public PhysicsSpring addSpring(String id, PhysicsParticle a, PhysicsParticle b,
|
||||
float restLength, float stiffness, float damping) {
|
||||
PhysicsSpring spring = new PhysicsSpring(id, a, b, restLength, stiffness, damping);
|
||||
springs.add(spring);
|
||||
return spring;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加弹簧(自动计算自然长度)
|
||||
*/
|
||||
public PhysicsSpring addSpring(String id, PhysicsParticle a, PhysicsParticle b,
|
||||
float stiffness, float damping) {
|
||||
float restLength = a.getPosition().distance(b.getPosition());
|
||||
return addSpring(id, a, b, restLength, stiffness, damping);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除弹簧
|
||||
*/
|
||||
public boolean removeSpring(PhysicsSpring spring) {
|
||||
return springs.remove(spring);
|
||||
}
|
||||
|
||||
// ==================== 约束管理 ====================
|
||||
|
||||
/**
|
||||
* 添加位置约束
|
||||
*/
|
||||
public PhysicsConstraint addPositionConstraint(PhysicsParticle particle, Vector2f targetPosition) {
|
||||
PhysicsConstraint constraint = new PositionConstraint(particle, targetPosition);
|
||||
constraints.add(constraint);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加距离约束
|
||||
*/
|
||||
public PhysicsConstraint addDistanceConstraint(PhysicsParticle particle, PhysicsParticle target,
|
||||
float maxDistance) {
|
||||
PhysicsConstraint constraint = new DistanceConstraint(particle, target, maxDistance);
|
||||
constraints.add(constraint);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除约束
|
||||
*/
|
||||
public boolean removeConstraint(PhysicsConstraint constraint) {
|
||||
return constraints.remove(constraint);
|
||||
}
|
||||
|
||||
// ==================== 碰撞管理 ====================
|
||||
|
||||
/**
|
||||
* 添加圆形碰撞体
|
||||
*/
|
||||
public PhysicsCollider addCircleCollider(String id, Vector2f center, float radius) {
|
||||
PhysicsCollider collider = new CircleCollider(id, center, radius);
|
||||
colliders.add(collider);
|
||||
return collider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加矩形碰撞体
|
||||
*/
|
||||
public PhysicsCollider addRectangleCollider(String id, Vector2f center, float width, float height) {
|
||||
PhysicsCollider collider = new RectangleCollider(id, center, width, height);
|
||||
colliders.add(collider);
|
||||
return collider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除碰撞体
|
||||
*/
|
||||
public boolean removeCollider(PhysicsCollider collider) {
|
||||
return colliders.remove(collider);
|
||||
}
|
||||
|
||||
// ==================== 更新系统 ====================
|
||||
|
||||
/**
|
||||
* 更新物理系统
|
||||
*/
|
||||
public void update(float deltaTime, Model2D model) {
|
||||
if (!enabled || !initialized) return;
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
|
||||
// 应用时间缩放
|
||||
float scaledDeltaTime = deltaTime * timeScale;
|
||||
accumulatedTime += scaledDeltaTime;
|
||||
|
||||
// 固定时间步长更新
|
||||
int numSubSteps = 0;
|
||||
while (accumulatedTime >= fixedTimeStep && numSubSteps < maxSubSteps) {
|
||||
updatePhysics(fixedTimeStep);
|
||||
accumulatedTime -= fixedTimeStep;
|
||||
numSubSteps++;
|
||||
}
|
||||
|
||||
// 应用物理结果到模型
|
||||
applyToModel(model);
|
||||
|
||||
// 更新性能统计
|
||||
updatePerformanceStats(System.nanoTime() - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理模拟更新
|
||||
*/
|
||||
private void updatePhysics(float deltaTime) {
|
||||
// 清除所有力
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
particle.clearForces();
|
||||
}
|
||||
|
||||
// 应用重力
|
||||
applyGravity();
|
||||
|
||||
// 应用弹簧力
|
||||
for (PhysicsSpring spring : springs) {
|
||||
spring.applyForce(deltaTime);
|
||||
}
|
||||
|
||||
// 更新粒子运动
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
if (particle.isMovable()) {
|
||||
particle.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 应用约束
|
||||
for (PhysicsConstraint constraint : constraints) {
|
||||
constraint.apply(deltaTime);
|
||||
}
|
||||
|
||||
// 处理碰撞
|
||||
handleCollisions(deltaTime);
|
||||
|
||||
// 应用空气阻力
|
||||
applyAirResistance(deltaTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用重力
|
||||
*/
|
||||
private void applyGravity() {
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
if (particle.isMovable() && particle.isAffectedByGravity()) {
|
||||
Vector2f gravityForce = new Vector2f(gravity).mul(particle.getMass());
|
||||
particle.addForce(gravityForce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用空气阻力
|
||||
*/
|
||||
private void applyAirResistance(float deltaTime) {
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
if (particle.isMovable()) {
|
||||
Vector2f velocity = particle.getVelocity();
|
||||
Vector2f dragForce = new Vector2f(velocity).mul(-airResistance);
|
||||
particle.addForce(dragForce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理碰撞
|
||||
*/
|
||||
private void handleCollisions(float deltaTime) {
|
||||
// 粒子与碰撞体碰撞
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
if (!particle.isMovable()) continue;
|
||||
|
||||
for (PhysicsCollider collider : colliders) {
|
||||
if (collider.isEnabled() && collider.collidesWith(particle)) {
|
||||
collider.resolveCollision(particle, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 粒子间碰撞(简单实现)
|
||||
handleParticleCollisions(deltaTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粒子间碰撞
|
||||
*/
|
||||
private void handleParticleCollisions(float deltaTime) {
|
||||
List<PhysicsParticle> particleList = new ArrayList<>(particles.values());
|
||||
|
||||
for (int i = 0; i < particleList.size(); i++) {
|
||||
PhysicsParticle p1 = particleList.get(i);
|
||||
if (!p1.isMovable()) continue;
|
||||
|
||||
for (int j = i + 1; j < particleList.size(); j++) {
|
||||
PhysicsParticle p2 = particleList.get(j);
|
||||
if (!p2.isMovable()) continue;
|
||||
|
||||
// 简单圆形碰撞检测
|
||||
Vector2f delta = new Vector2f(p2.getPosition()).sub(p1.getPosition());
|
||||
float distance = delta.length();
|
||||
float minDistance = p1.getRadius() + p2.getRadius();
|
||||
|
||||
if (distance < minDistance && distance > 0.001f) {
|
||||
// 碰撞响应
|
||||
Vector2f normal = new Vector2f(delta).div(distance);
|
||||
float overlap = minDistance - distance;
|
||||
|
||||
// 分离粒子
|
||||
Vector2f separation = new Vector2f(normal).mul(overlap * 0.5f);
|
||||
p1.getPosition().sub(separation);
|
||||
p2.getPosition().add(separation);
|
||||
|
||||
// 简单的速度响应
|
||||
Vector2f relativeVelocity = new Vector2f(p2.getVelocity()).sub(p1.getVelocity());
|
||||
float velocityAlongNormal = relativeVelocity.dot(normal);
|
||||
|
||||
if (velocityAlongNormal > 0) continue; // 已经分离
|
||||
|
||||
float restitution = 0.5f; // 弹性系数
|
||||
float impulseMagnitude = -(1 + restitution) * velocityAlongNormal;
|
||||
impulseMagnitude /= p1.getInverseMass() + p2.getInverseMass();
|
||||
|
||||
Vector2f impulse = new Vector2f(normal).mul(impulseMagnitude);
|
||||
p1.getVelocity().sub(new Vector2f(impulse).mul(p1.getInverseMass()));
|
||||
p2.getVelocity().add(new Vector2f(impulse).mul(p2.getInverseMass()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用物理结果到模型
|
||||
*/
|
||||
private void applyToModel(Model2D model) {
|
||||
for (PhysicsParticle particle : particles.values()) {
|
||||
Object userData = particle.getUserData();
|
||||
if (userData instanceof ModelPart) {
|
||||
ModelPart part = (ModelPart) userData;
|
||||
part.setPosition(particle.getPosition());
|
||||
|
||||
// 可选:根据速度设置旋转
|
||||
if (particle.getVelocity().lengthSquared() > 0.1f) {
|
||||
float angle = (float) Math.atan2(particle.getVelocity().y, particle.getVelocity().x);
|
||||
part.setRotation(angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 性能统计 ====================
|
||||
|
||||
/**
|
||||
* 更新性能统计
|
||||
*/
|
||||
private void updatePerformanceStats(long nanoTime) {
|
||||
float millis = nanoTime / 1_000_000.0f;
|
||||
|
||||
// 指数移动平均
|
||||
if (updateCount == 0) {
|
||||
averageUpdateTime = millis;
|
||||
} else {
|
||||
averageUpdateTime = averageUpdateTime * 0.95f + millis * 0.05f;
|
||||
}
|
||||
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
public PhysicsPerformanceReport getPerformanceReport() {
|
||||
return new PhysicsPerformanceReport(
|
||||
particles.size(),
|
||||
springs.size(),
|
||||
constraints.size(),
|
||||
colliders.size(),
|
||||
averageUpdateTime,
|
||||
updateCount
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public Vector2f getGravity() {
|
||||
return new Vector2f(gravity);
|
||||
}
|
||||
|
||||
public void setGravity(float x, float y) {
|
||||
gravity.set(x, y);
|
||||
}
|
||||
|
||||
public void setGravity(Vector2f gravity) {
|
||||
this.gravity.set(gravity);
|
||||
}
|
||||
|
||||
public float getAirResistance() {
|
||||
return airResistance;
|
||||
}
|
||||
|
||||
public void setAirResistance(float airResistance) {
|
||||
this.airResistance = Math.max(0.0f, airResistance);
|
||||
}
|
||||
|
||||
public float getTimeScale() {
|
||||
return timeScale;
|
||||
}
|
||||
|
||||
public void setTimeScale(float timeScale) {
|
||||
this.timeScale = Math.max(0.0f, timeScale);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public Map<String, PhysicsParticle> getParticles() {
|
||||
return Collections.unmodifiableMap(particles);
|
||||
}
|
||||
|
||||
public List<PhysicsSpring> getSprings() {
|
||||
return Collections.unmodifiableList(springs);
|
||||
}
|
||||
|
||||
public List<PhysicsConstraint> getConstraints() {
|
||||
return Collections.unmodifiableList(constraints);
|
||||
}
|
||||
|
||||
public List<PhysicsCollider> getColliders() {
|
||||
return Collections.unmodifiableList(colliders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有活跃的物理效果
|
||||
* 返回true表示当前有物理效果正在影响模型
|
||||
*/
|
||||
public boolean hasActivePhysics() {
|
||||
if (!enabled || !initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有可移动的粒子
|
||||
boolean hasMovableParticles = particles.values().stream()
|
||||
.anyMatch(particle -> particle.isMovable() && particle.isAffectedByGravity());
|
||||
|
||||
if (!hasMovableParticles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有活跃的弹簧
|
||||
boolean hasActiveSprings = springs.stream()
|
||||
.anyMatch(spring -> spring.isEnabled() &&
|
||||
(spring.getParticleA().isMovable() || spring.getParticleB().isMovable()));
|
||||
|
||||
// 检查粒子是否有显著的运动
|
||||
boolean hasSignificantMotion = particles.values().stream()
|
||||
.anyMatch(particle -> {
|
||||
if (!particle.isMovable()) return false;
|
||||
|
||||
// 检查速度是否超过阈值
|
||||
float speedSquared = particle.getVelocity().lengthSquared();
|
||||
if (speedSquared > 0.1f) { // 速度阈值,可调整
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查位置是否显著变化(相对于前一帧)
|
||||
Vector2f positionDelta = new Vector2f(particle.getPosition())
|
||||
.sub(particle.getPreviousPosition()); // 现在可以正常使用了
|
||||
float positionDeltaSquared = positionDelta.lengthSquared();
|
||||
if (positionDeltaSquared > 0.001f) { // 位置变化阈值,可调整
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// 检查是否有活跃的约束
|
||||
boolean hasActiveConstraints = constraints.stream()
|
||||
.anyMatch(constraint -> constraint.isEnabled() &&
|
||||
constraint.getParticle().isMovable());
|
||||
|
||||
return hasActiveSprings || hasSignificantMotion || hasActiveConstraints;
|
||||
}
|
||||
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
/**
|
||||
* 物理粒子类
|
||||
*/
|
||||
public static class PhysicsParticle {
|
||||
private final String id;
|
||||
private final Vector2f position;
|
||||
private final Vector2f previousPosition;
|
||||
private final Vector2f velocity;
|
||||
private final Vector2f acceleration;
|
||||
private final Vector2f forceAccumulator;
|
||||
private final float mass;
|
||||
private final float inverseMass;
|
||||
private final float radius;
|
||||
private boolean movable;
|
||||
private boolean affectedByGravity;
|
||||
private Object userData;
|
||||
|
||||
public PhysicsParticle(String id, Vector2f position, float mass) {
|
||||
this.id = id;
|
||||
this.position = new Vector2f(position);
|
||||
this.previousPosition = new Vector2f(position);
|
||||
this.velocity = new Vector2f();
|
||||
this.acceleration = new Vector2f();
|
||||
this.forceAccumulator = new Vector2f();
|
||||
this.mass = Math.max(0.001f, mass);
|
||||
this.inverseMass = 1.0f / this.mass;
|
||||
this.radius = 2.0f; // 默认半径
|
||||
this.movable = true;
|
||||
this.affectedByGravity = true;
|
||||
}
|
||||
|
||||
public Vector2f getPreviousPosition() {
|
||||
return new Vector2f(previousPosition);
|
||||
}
|
||||
public void update(float deltaTime) {
|
||||
if (!movable) return;
|
||||
|
||||
// Verlet 积分法
|
||||
Vector2f temp = new Vector2f(position);
|
||||
|
||||
// 计算加速度: a = F / m
|
||||
acceleration.set(forceAccumulator).mul(inverseMass);
|
||||
|
||||
// Verlet 位置更新: x_{n+1} = 2x_n - x_{n-1} + a * dt^2
|
||||
position.set(2.0f * position.x - previousPosition.x + acceleration.x * deltaTime * deltaTime,
|
||||
2.0f * position.y - previousPosition.y + acceleration.y * deltaTime * deltaTime);
|
||||
|
||||
previousPosition.set(temp);
|
||||
|
||||
// 更新速度(用于显示和其他计算)
|
||||
velocity.set(position).sub(previousPosition).div(deltaTime);
|
||||
}
|
||||
|
||||
public void addForce(Vector2f force) {
|
||||
forceAccumulator.add(force);
|
||||
}
|
||||
|
||||
public void clearForces() {
|
||||
forceAccumulator.set(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
public String getId() { return id; }
|
||||
public Vector2f getPosition() { return new Vector2f(position); }
|
||||
public void setPosition(Vector2f position) { this.position.set(position); }
|
||||
public Vector2f getVelocity() { return new Vector2f(velocity); }
|
||||
public void setVelocity(Vector2f velocity) { this.velocity.set(velocity); }
|
||||
public Vector2f getAcceleration() { return new Vector2f(acceleration); }
|
||||
public float getMass() { return mass; }
|
||||
public float getInverseMass() { return inverseMass; }
|
||||
public float getRadius() { return radius; }
|
||||
public void setRadius(float radius) { /* this.radius = radius; */ } // 注意:半径在构造后不可变
|
||||
public boolean isMovable() { return movable; }
|
||||
public void setMovable(boolean movable) { this.movable = movable; }
|
||||
public boolean isAffectedByGravity() { return affectedByGravity; }
|
||||
public void setAffectedByGravity(boolean affectedByGravity) { this.affectedByGravity = affectedByGravity; }
|
||||
public Object getUserData() { return userData; }
|
||||
public void setUserData(Object userData) { this.userData = userData; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理弹簧类
|
||||
*/
|
||||
public static class PhysicsSpring {
|
||||
private final String id;
|
||||
private final PhysicsParticle particleA;
|
||||
private final PhysicsParticle particleB;
|
||||
private final float restLength;
|
||||
private final float stiffness;
|
||||
private final float damping;
|
||||
private boolean enabled;
|
||||
|
||||
public PhysicsSpring(String id, PhysicsParticle a, PhysicsParticle b,
|
||||
float restLength, float stiffness, float damping) {
|
||||
this.id = id;
|
||||
this.particleA = a;
|
||||
this.particleB = b;
|
||||
this.restLength = restLength;
|
||||
this.stiffness = stiffness;
|
||||
this.damping = damping;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public void applyForce(float deltaTime) {
|
||||
if (!enabled) return;
|
||||
|
||||
Vector2f delta = new Vector2f(particleB.getPosition()).sub(particleA.getPosition());
|
||||
float currentLength = delta.length();
|
||||
|
||||
if (currentLength < 0.001f) return; // 避免除以零
|
||||
|
||||
// 胡克定律: F = -k * (currentLength - restLength)
|
||||
float stretch = currentLength - restLength;
|
||||
Vector2f springForce = new Vector2f(delta).normalize().mul(stiffness * stretch);
|
||||
|
||||
// 阻尼力: F_damp = -damping * relativeVelocity
|
||||
Vector2f relativeVelocity = new Vector2f(particleB.getVelocity()).sub(particleA.getVelocity());
|
||||
float velocityAlongSpring = relativeVelocity.dot(delta) / currentLength;
|
||||
Vector2f dampingForce = new Vector2f(delta).normalize().mul(damping * velocityAlongSpring);
|
||||
|
||||
// 应用合力
|
||||
Vector2f totalForce = new Vector2f(springForce).sub(dampingForce);
|
||||
|
||||
if (particleA.isMovable()) {
|
||||
particleA.addForce(totalForce);
|
||||
}
|
||||
|
||||
if (particleB.isMovable()) {
|
||||
particleB.addForce(totalForce.negate());
|
||||
}
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
public String getId() { return id; }
|
||||
public PhysicsParticle getParticleA() { return particleA; }
|
||||
public PhysicsParticle getParticleB() { return particleB; }
|
||||
public float getRestLength() { return restLength; }
|
||||
public float getStiffness() { return stiffness; }
|
||||
public float getDamping() { return damping; }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理约束接口
|
||||
*/
|
||||
public interface PhysicsConstraint {
|
||||
void apply(float deltaTime);
|
||||
PhysicsParticle getParticle();
|
||||
boolean isEnabled();
|
||||
void setEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置约束
|
||||
*/
|
||||
public static class PositionConstraint implements PhysicsConstraint {
|
||||
private final PhysicsParticle particle;
|
||||
private final Vector2f targetPosition;
|
||||
private float strength;
|
||||
private boolean enabled;
|
||||
|
||||
public PositionConstraint(PhysicsParticle particle, Vector2f targetPosition) {
|
||||
this.particle = particle;
|
||||
this.targetPosition = new Vector2f(targetPosition);
|
||||
this.strength = 0.5f;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float deltaTime) {
|
||||
if (!enabled || !particle.isMovable()) return;
|
||||
|
||||
Vector2f currentPos = particle.getPosition();
|
||||
Vector2f delta = new Vector2f(targetPosition).sub(currentPos);
|
||||
Vector2f correction = new Vector2f(delta).mul(strength);
|
||||
|
||||
particle.setPosition(new Vector2f(currentPos).add(correction));
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
@Override public PhysicsParticle getParticle() { return particle; }
|
||||
@Override public boolean isEnabled() { return enabled; }
|
||||
@Override public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public Vector2f getTargetPosition() { return new Vector2f(targetPosition); }
|
||||
public void setTargetPosition(Vector2f targetPosition) { this.targetPosition.set(targetPosition); }
|
||||
public float getStrength() { return strength; }
|
||||
public void setStrength(float strength) { this.strength = Math.max(0.0f, Math.min(1.0f, strength)); }
|
||||
}
|
||||
|
||||
/**
|
||||
* 距离约束
|
||||
*/
|
||||
public static class DistanceConstraint implements PhysicsConstraint {
|
||||
private final PhysicsParticle particle;
|
||||
private final PhysicsParticle target;
|
||||
private final float maxDistance;
|
||||
private boolean enabled;
|
||||
|
||||
public DistanceConstraint(PhysicsParticle particle, PhysicsParticle target, float maxDistance) {
|
||||
this.particle = particle;
|
||||
this.target = target;
|
||||
this.maxDistance = maxDistance;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(float deltaTime) {
|
||||
if (!enabled || !particle.isMovable()) return;
|
||||
|
||||
Vector2f delta = new Vector2f(particle.getPosition()).sub(target.getPosition());
|
||||
float distance = delta.length();
|
||||
|
||||
if (distance > maxDistance) {
|
||||
Vector2f correction = new Vector2f(delta).normalize().mul(distance - maxDistance);
|
||||
particle.setPosition(new Vector2f(particle.getPosition()).sub(correction));
|
||||
}
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
@Override public PhysicsParticle getParticle() { return particle; }
|
||||
@Override public boolean isEnabled() { return enabled; }
|
||||
@Override public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public PhysicsParticle getTarget() { return target; }
|
||||
public float getMaxDistance() { return maxDistance; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理碰撞体接口
|
||||
*/
|
||||
public interface PhysicsCollider {
|
||||
boolean collidesWith(PhysicsParticle particle);
|
||||
void resolveCollision(PhysicsParticle particle, float deltaTime);
|
||||
String getId();
|
||||
boolean isEnabled();
|
||||
void setEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆形碰撞体
|
||||
*/
|
||||
public static class CircleCollider implements PhysicsCollider {
|
||||
private final String id;
|
||||
private final Vector2f center;
|
||||
private final float radius;
|
||||
private boolean enabled;
|
||||
|
||||
public CircleCollider(String id, Vector2f center, float radius) {
|
||||
this.id = id;
|
||||
this.center = new Vector2f(center);
|
||||
this.radius = radius;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collidesWith(PhysicsParticle particle) {
|
||||
float distance = particle.getPosition().distance(center);
|
||||
return distance < (radius + particle.getRadius());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveCollision(PhysicsParticle particle, float deltaTime) {
|
||||
Vector2f toParticle = new Vector2f(particle.getPosition()).sub(center);
|
||||
float distance = toParticle.length();
|
||||
float overlap = (radius + particle.getRadius()) - distance;
|
||||
|
||||
if (overlap > 0 && distance > 0.001f) {
|
||||
// 分离
|
||||
Vector2f normal = new Vector2f(toParticle).div(distance);
|
||||
particle.getPosition().add(new Vector2f(normal).mul(overlap));
|
||||
|
||||
// 反弹
|
||||
float dot = particle.getVelocity().dot(normal);
|
||||
if (dot < 0) {
|
||||
Vector2f reflection = new Vector2f(normal).mul(2.0f * dot);
|
||||
particle.getVelocity().sub(reflection).mul(0.8f); // 能量损失
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
@Override public String getId() { return id; }
|
||||
@Override public boolean isEnabled() { return enabled; }
|
||||
@Override public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public Vector2f getCenter() { return new Vector2f(center); }
|
||||
public void setCenter(Vector2f center) { this.center.set(center); }
|
||||
public float getRadius() { return radius; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形碰撞体
|
||||
*/
|
||||
public static class RectangleCollider implements PhysicsCollider {
|
||||
private final String id;
|
||||
private final Vector2f center;
|
||||
private final float width;
|
||||
private final float height;
|
||||
private boolean enabled;
|
||||
|
||||
public RectangleCollider(String id, Vector2f center, float width, float height) {
|
||||
this.id = id;
|
||||
this.center = new Vector2f(center);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collidesWith(PhysicsParticle particle) {
|
||||
Vector2f particlePos = particle.getPosition();
|
||||
float left = center.x - width / 2;
|
||||
float right = center.x + width / 2;
|
||||
float bottom = center.y - height / 2;
|
||||
float top = center.y + height / 2;
|
||||
|
||||
// 扩展边界考虑粒子半径
|
||||
left -= particle.getRadius();
|
||||
right += particle.getRadius();
|
||||
bottom -= particle.getRadius();
|
||||
top += particle.getRadius();
|
||||
|
||||
return particlePos.x >= left && particlePos.x <= right &&
|
||||
particlePos.y >= bottom && particlePos.y <= top;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveCollision(PhysicsParticle particle, float deltaTime) {
|
||||
Vector2f particlePos = particle.getPosition();
|
||||
float left = center.x - width / 2;
|
||||
float right = center.x + width / 2;
|
||||
float bottom = center.y - height / 2;
|
||||
float top = center.y + height / 2;
|
||||
|
||||
// 计算最近边界
|
||||
float closestX = Math.max(left, Math.min(particlePos.x, right));
|
||||
float closestY = Math.max(bottom, Math.min(particlePos.y, top));
|
||||
|
||||
Vector2f closestPoint = new Vector2f(closestX, closestY);
|
||||
Vector2f normal = new Vector2f(particlePos).sub(closestPoint);
|
||||
|
||||
if (normal.lengthSquared() > 0.001f) {
|
||||
normal.normalize();
|
||||
|
||||
// 分离粒子
|
||||
float overlap = particle.getRadius() - normal.length();
|
||||
if (overlap > 0) {
|
||||
particle.getPosition().add(new Vector2f(normal).mul(overlap));
|
||||
|
||||
// 反弹
|
||||
float dot = particle.getVelocity().dot(normal);
|
||||
if (dot < 0) {
|
||||
Vector2f reflection = new Vector2f(normal).mul(2.0f * dot);
|
||||
particle.getVelocity().sub(reflection).mul(0.8f); // 能量损失
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getter/Setter 方法
|
||||
@Override public String getId() { return id; }
|
||||
@Override public boolean isEnabled() { return enabled; }
|
||||
@Override public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public Vector2f getCenter() { return new Vector2f(center); }
|
||||
public void setCenter(Vector2f center) { this.center.set(center); }
|
||||
public float getWidth() { return width; }
|
||||
public float getHeight() { return height; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理性能报告
|
||||
*/
|
||||
public static class PhysicsPerformanceReport {
|
||||
private final int particleCount;
|
||||
private final int springCount;
|
||||
private final int constraintCount;
|
||||
private final int colliderCount;
|
||||
private final float averageUpdateTime;
|
||||
private final int totalUpdates;
|
||||
|
||||
public PhysicsPerformanceReport(int particleCount, int springCount, int constraintCount,
|
||||
int colliderCount, float averageUpdateTime, int totalUpdates) {
|
||||
this.particleCount = particleCount;
|
||||
this.springCount = springCount;
|
||||
this.constraintCount = constraintCount;
|
||||
this.colliderCount = colliderCount;
|
||||
this.averageUpdateTime = averageUpdateTime;
|
||||
this.totalUpdates = totalUpdates;
|
||||
}
|
||||
|
||||
// Getter 方法
|
||||
public int getParticleCount() { return particleCount; }
|
||||
public int getSpringCount() { return springCount; }
|
||||
public int getConstraintCount() { return constraintCount; }
|
||||
public int getColliderCount() { return colliderCount; }
|
||||
public float getAverageUpdateTime() { return averageUpdateTime; }
|
||||
public int getTotalUpdates() { return totalUpdates; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"Physics Performance: %d particles, %d springs, %d constraints, %d colliders, " +
|
||||
"Avg update: %.2fms, Total updates: %d",
|
||||
particleCount, springCount, constraintCount, colliderCount,
|
||||
averageUpdateTime, totalUpdates
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL14;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL45;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 纹理类,使用 LWJGL OpenGL API 实现完整的纹理管理
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Texture {
|
||||
// ==================== 纹理属性 ====================
|
||||
private final int textureId;
|
||||
private final String name;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final TextureFormat format;
|
||||
private final TextureType type;
|
||||
|
||||
// ==================== 纹理参数 ====================
|
||||
private TextureFilter minFilter = TextureFilter.LINEAR;
|
||||
private TextureFilter magFilter = TextureFilter.LINEAR;
|
||||
private TextureWrap wrapS = TextureWrap.CLAMP_TO_EDGE;
|
||||
private TextureWrap wrapT = TextureWrap.CLAMP_TO_EDGE;
|
||||
private boolean mipmapsEnabled = false;
|
||||
private boolean textureCreated = false;
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
private boolean disposed = false;
|
||||
private final long creationTime;
|
||||
|
||||
// ==================== 静态管理 ====================
|
||||
private static final Map<String, Texture> TEXTURE_CACHE = new HashMap<>();
|
||||
private static boolean openGLChecked = false;
|
||||
|
||||
// ==================== 枚举定义 ====================
|
||||
|
||||
public enum TextureFormat {
|
||||
RGB(3, GL11.GL_RGB, GL11.GL_RGB),
|
||||
RGBA(4, GL11.GL_RGBA, GL11.GL_RGBA),
|
||||
ALPHA(1, GL11.GL_ALPHA, GL11.GL_ALPHA),
|
||||
LUMINANCE(1, GL11.GL_LUMINANCE, GL11.GL_LUMINANCE),
|
||||
LUMINANCE_ALPHA(2, GL11.GL_LUMINANCE_ALPHA, GL11.GL_LUMINANCE_ALPHA),
|
||||
RED(1, GL30.GL_RED, GL30.GL_RED),
|
||||
RG(2, GL30.GL_RG, GL30.GL_RG);
|
||||
|
||||
private final int components;
|
||||
private final int glInternalFormat;
|
||||
private final int glFormat;
|
||||
|
||||
TextureFormat(int components, int glInternalFormat, int glFormat) {
|
||||
this.components = components;
|
||||
this.glInternalFormat = glInternalFormat;
|
||||
this.glFormat = glFormat;
|
||||
}
|
||||
|
||||
public int getComponents() { return components; }
|
||||
public int getGLInternalFormat() { return glInternalFormat; }
|
||||
public int getGLFormat() { return glFormat; }
|
||||
}
|
||||
|
||||
public enum TextureType {
|
||||
UNSIGNED_BYTE(GL11.GL_UNSIGNED_BYTE),
|
||||
BYTE(GL11.GL_BYTE),
|
||||
UNSIGNED_SHORT(GL11.GL_UNSIGNED_SHORT),
|
||||
SHORT(GL11.GL_SHORT),
|
||||
UNSIGNED_INT(GL11.GL_UNSIGNED_INT),
|
||||
INT(GL11.GL_INT),
|
||||
FLOAT(GL11.GL_FLOAT);
|
||||
|
||||
private final int glType;
|
||||
|
||||
TextureType(int glType) {
|
||||
this.glType = glType;
|
||||
}
|
||||
|
||||
public int getGLType() { return glType; }
|
||||
}
|
||||
|
||||
public enum TextureFilter {
|
||||
NEAREST(GL11.GL_NEAREST),
|
||||
LINEAR(GL11.GL_LINEAR),
|
||||
NEAREST_MIPMAP_NEAREST(GL11.GL_NEAREST_MIPMAP_NEAREST),
|
||||
LINEAR_MIPMAP_NEAREST(GL11.GL_LINEAR_MIPMAP_NEAREST),
|
||||
NEAREST_MIPMAP_LINEAR(GL11.GL_NEAREST_MIPMAP_LINEAR),
|
||||
LINEAR_MIPMAP_LINEAR(GL11.GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
private final int glFilter;
|
||||
|
||||
TextureFilter(int glFilter) {
|
||||
this.glFilter = glFilter;
|
||||
}
|
||||
|
||||
public int getGLFilter() { return glFilter; }
|
||||
}
|
||||
|
||||
public enum TextureWrap {
|
||||
REPEAT(GL11.GL_REPEAT),
|
||||
MIRRORED_REPEAT(GL14.GL_MIRRORED_REPEAT),
|
||||
CLAMP_TO_EDGE(GL12.GL_CLAMP_TO_EDGE),
|
||||
CLAMP_TO_BORDER(GL13.GL_CLAMP_TO_BORDER);
|
||||
|
||||
private final int glWrap;
|
||||
|
||||
TextureWrap(int glWrap) {
|
||||
this.glWrap = glWrap;
|
||||
}
|
||||
|
||||
public int getGLWrap() { return glWrap; }
|
||||
}
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
public Texture(String name, int width, int height, TextureFormat format) {
|
||||
this(name, width, height, format, TextureType.UNSIGNED_BYTE);
|
||||
}
|
||||
|
||||
public Texture(String name, int width, int height, TextureFormat format, TextureType type) {
|
||||
checkOpenGLCapabilities();
|
||||
this.textureId = generateTextureId();
|
||||
this.name = name;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.format = format;
|
||||
this.type = type;
|
||||
this.creationTime = System.currentTimeMillis();
|
||||
|
||||
// 创建空的纹理对象
|
||||
createTextureObject();
|
||||
applyTextureParameters();
|
||||
}
|
||||
|
||||
public Texture(String name, int width, int height, TextureFormat format, ByteBuffer pixelData) {
|
||||
this(name, width, height, format);
|
||||
uploadData(pixelData);
|
||||
}
|
||||
|
||||
public Texture(String name, int width, int height, TextureFormat format, int[] pixelData) {
|
||||
this(name, width, height, format);
|
||||
uploadData(pixelData);
|
||||
}
|
||||
|
||||
// ==================== OpenGL 能力检查 ====================
|
||||
|
||||
/**
|
||||
* 检查 OpenGL 能力
|
||||
*/
|
||||
private static void checkOpenGLCapabilities() {
|
||||
if (!openGLChecked) {
|
||||
if (!GL.getCapabilities().OpenGL11) {
|
||||
throw new RuntimeException("OpenGL 1.1 is required but not supported");
|
||||
}
|
||||
openGLChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 纹理数据管理 ====================
|
||||
|
||||
/**
|
||||
* 创建纹理对象
|
||||
*/
|
||||
private void createTextureObject() {
|
||||
if (textureCreated) return;
|
||||
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
||||
|
||||
// 分配纹理存储 - 使用兼容性更好的方法
|
||||
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format.getGLInternalFormat(),
|
||||
width, height, 0, format.getGLFormat(), type.getGLType(),
|
||||
(ByteBuffer) null);
|
||||
|
||||
textureCreated = true;
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
|
||||
checkGLError("createTextureObject");
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节缓冲区数据到纹理
|
||||
*/
|
||||
public void uploadData(ByteBuffer pixelData) {
|
||||
if (disposed) {
|
||||
throw new IllegalStateException("Cannot upload data to disposed texture: " + name);
|
||||
}
|
||||
|
||||
if (pixelData == null) {
|
||||
throw new IllegalArgumentException("Pixel data cannot be null");
|
||||
}
|
||||
|
||||
int expectedSize = width * height * format.getComponents();
|
||||
if (pixelData.remaining() < expectedSize) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Pixel data buffer too small for texture dimensions. Expected %d, got %d",
|
||||
expectedSize, pixelData.remaining()));
|
||||
}
|
||||
|
||||
bind(0);
|
||||
|
||||
if (!textureCreated) {
|
||||
createTextureObject();
|
||||
}
|
||||
|
||||
// 上传纹理数据
|
||||
GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height,
|
||||
format.getGLFormat(), type.getGLType(), pixelData);
|
||||
|
||||
// 检查OpenGL错误
|
||||
checkGLError("glTexSubImage2D");
|
||||
|
||||
unbind();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传整数数组数据到纹理
|
||||
*/
|
||||
public void uploadData(int[] pixelData) {
|
||||
if (pixelData == null) {
|
||||
throw new IllegalArgumentException("Pixel data cannot be null");
|
||||
}
|
||||
|
||||
if (pixelData.length < width * height) {
|
||||
throw new IllegalArgumentException("Pixel data array too small for texture dimensions");
|
||||
}
|
||||
|
||||
// 将int数组转换为ByteBuffer
|
||||
ByteBuffer buffer = MemoryUtil.memAlloc(pixelData.length * 4);
|
||||
buffer.asIntBuffer().put(pixelData);
|
||||
buffer.position(0);
|
||||
|
||||
try {
|
||||
uploadData(buffer);
|
||||
} finally {
|
||||
MemoryUtil.memFree(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成mipmaps
|
||||
*/
|
||||
public void generateMipmaps() {
|
||||
if (disposed) {
|
||||
throw new IllegalStateException("Cannot generate mipmaps for disposed texture: " + name);
|
||||
}
|
||||
|
||||
if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) {
|
||||
System.err.println("Warning: Cannot generate mipmaps for non-power-of-two texture: " + name);
|
||||
return;
|
||||
}
|
||||
|
||||
bind(0);
|
||||
|
||||
// 重新创建纹理为可变纹理
|
||||
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format.getGLInternalFormat(),
|
||||
width, height, 0, format.getGLFormat(), type.getGLType(),
|
||||
(ByteBuffer) null);
|
||||
textureCreated = true;
|
||||
|
||||
// 生成mipmaps
|
||||
GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D);
|
||||
|
||||
// 检查OpenGL错误
|
||||
checkGLError("glGenerateMipmap");
|
||||
|
||||
mipmapsEnabled = true;
|
||||
|
||||
// 更新过滤器以使用mipmaps
|
||||
if (minFilter == TextureFilter.LINEAR) {
|
||||
setMinFilter(TextureFilter.LINEAR_MIPMAP_LINEAR);
|
||||
} else if (minFilter == TextureFilter.NEAREST) {
|
||||
setMinFilter(TextureFilter.NEAREST_MIPMAP_NEAREST);
|
||||
}
|
||||
|
||||
unbind();
|
||||
}
|
||||
|
||||
// ==================== 纹理参数设置 ====================
|
||||
|
||||
/**
|
||||
* 设置最小化过滤器
|
||||
*/
|
||||
public void setMinFilter(TextureFilter filter) {
|
||||
if (this.minFilter != filter) {
|
||||
this.minFilter = filter;
|
||||
applyTextureParameters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大化过滤器
|
||||
*/
|
||||
public void setMagFilter(TextureFilter filter) {
|
||||
if (this.magFilter != filter) {
|
||||
this.magFilter = filter;
|
||||
applyTextureParameters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置S轴包装模式
|
||||
*/
|
||||
public void setWrapS(TextureWrap wrap) {
|
||||
if (this.wrapS != wrap) {
|
||||
this.wrapS = wrap;
|
||||
applyTextureParameters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置T轴包装模式
|
||||
*/
|
||||
public void setWrapT(TextureWrap wrap) {
|
||||
if (this.wrapT != wrap) {
|
||||
this.wrapT = wrap;
|
||||
applyTextureParameters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置双向包装模式
|
||||
*/
|
||||
public void setWrap(TextureWrap wrap) {
|
||||
setWrapS(wrap);
|
||||
setWrapT(wrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用纹理参数到GPU
|
||||
*/
|
||||
public void applyTextureParameters() {
|
||||
if (disposed) return;
|
||||
|
||||
bind(0);
|
||||
|
||||
// 设置纹理参数
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, minFilter.getGLFilter());
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, magFilter.getGLFilter());
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wrapS.getGLWrap());
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wrapT.getGLWrap());
|
||||
|
||||
// 检查OpenGL错误
|
||||
checkGLError("glTexParameteri");
|
||||
|
||||
unbind();
|
||||
}
|
||||
|
||||
// ==================== 绑定管理 ====================
|
||||
|
||||
/**
|
||||
* 绑定纹理到指定纹理单元
|
||||
*/
|
||||
public void bind(int textureUnit) {
|
||||
if (disposed) {
|
||||
throw new IllegalStateException("Cannot bind disposed texture: " + name);
|
||||
}
|
||||
|
||||
// 安全地激活纹理单元
|
||||
if (textureUnit >= 0 && textureUnit < 32) { // 合理的纹理单元范围
|
||||
try {
|
||||
GL13.glActiveTexture(GL13.GL_TEXTURE0 + textureUnit);
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
||||
checkGLError("glBindTexture");
|
||||
} catch (Exception e) {
|
||||
// 如果 GL13 不可用,回退到基本方法
|
||||
System.err.println("Warning: GL13 not available, using fallback texture binding");
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid texture unit: " + textureUnit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定纹理到默认纹理单元(0)
|
||||
*/
|
||||
public void bind() {
|
||||
bind(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑纹理
|
||||
*/
|
||||
public void unbind() {
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// ==================== 资源管理 ====================
|
||||
|
||||
/**
|
||||
* 释放纹理资源
|
||||
*/
|
||||
public void dispose() {
|
||||
if (!disposed) {
|
||||
try {
|
||||
IntBuffer textures = MemoryUtil.memAllocInt(1);
|
||||
textures.put(textureId);
|
||||
textures.flip();
|
||||
GL11.glDeleteTextures(textures);
|
||||
MemoryUtil.memFree(textures);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error disposing texture: " + e.getMessage());
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
TEXTURE_CACHE.values().removeIf(texture -> texture.textureId == this.textureId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查纹理是否已释放
|
||||
*/
|
||||
public boolean isDisposed() {
|
||||
return disposed;
|
||||
}
|
||||
|
||||
// ==================== 静态工厂方法 ====================
|
||||
|
||||
/**
|
||||
* 创建纯色纹理
|
||||
*/
|
||||
public static Texture createSolidColor(String name, int width, int height, int rgbaColor) {
|
||||
int[] pixels = new int[width * height];
|
||||
java.util.Arrays.fill(pixels, rgbaColor);
|
||||
|
||||
return new Texture(name, width, height, TextureFormat.RGBA, pixels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建棋盘格纹理(用于调试)
|
||||
*/
|
||||
public static Texture createCheckerboard(String name, int width, int height, int tileSize,
|
||||
int color1, int color2) {
|
||||
int[] pixels = new int[width * height];
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
boolean isColor1 = ((x / tileSize) + (y / tileSize)) % 2 == 0;
|
||||
pixels[y * width + x] = isColor1 ? color1 : color2;
|
||||
}
|
||||
}
|
||||
|
||||
return new Texture(name, width, height, TextureFormat.RGBA, pixels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存获取纹理,如果不存在则创建
|
||||
*/
|
||||
public static Texture getOrCreate(String name, int width, int height, TextureFormat format) {
|
||||
return TEXTURE_CACHE.computeIfAbsent(name, k ->
|
||||
new Texture(name, width, height, format));
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取纹理内存占用估算(字节)
|
||||
*/
|
||||
public long getEstimatedMemoryUsage() {
|
||||
int bytesPerPixel;
|
||||
switch (type) {
|
||||
case UNSIGNED_BYTE:
|
||||
case BYTE:
|
||||
bytesPerPixel = format.getComponents();
|
||||
break;
|
||||
case UNSIGNED_SHORT:
|
||||
case SHORT:
|
||||
bytesPerPixel = format.getComponents() * 2;
|
||||
break;
|
||||
case UNSIGNED_INT:
|
||||
case INT:
|
||||
case FLOAT:
|
||||
bytesPerPixel = format.getComponents() * 4;
|
||||
break;
|
||||
default:
|
||||
bytesPerPixel = 4;
|
||||
}
|
||||
|
||||
long baseMemory = (long) width * height * bytesPerPixel;
|
||||
|
||||
// 如果启用了mipmaps,加上mipmaps的内存
|
||||
if (mipmapsEnabled) {
|
||||
return baseMemory * 4L / 3L; // mipmaps大约增加1/3内存
|
||||
}
|
||||
|
||||
return baseMemory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查尺寸是否为2的幂
|
||||
*/
|
||||
private boolean isPowerOfTwo(int value) {
|
||||
return value > 0 && (value & (value - 1)) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算mipmap级别数量
|
||||
*/
|
||||
private int calculateMipmapLevels() {
|
||||
return (int) Math.floor(Math.log(Math.max(width, height)) / Math.log(2)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成纹理ID
|
||||
*/
|
||||
private int generateTextureId() {
|
||||
try {
|
||||
IntBuffer textures = MemoryUtil.memAllocInt(1);
|
||||
GL11.glGenTextures(textures);
|
||||
int textureId = textures.get(0);
|
||||
MemoryUtil.memFree(textures);
|
||||
|
||||
if (textureId == 0) {
|
||||
throw new RuntimeException("Failed to generate texture ID");
|
||||
}
|
||||
|
||||
return textureId;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to generate texture: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查OpenGL错误
|
||||
*/
|
||||
private void checkGLError(String operation) {
|
||||
int error = GL11.glGetError();
|
||||
if (error != GL11.GL_NO_ERROR) {
|
||||
String errorName = getGLErrorString(error);
|
||||
System.err.println("OpenGL error during " + operation + ": " + errorName);
|
||||
// 不再抛出异常,而是记录错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OpenGL 错误字符串
|
||||
*/
|
||||
private String getGLErrorString(int error) {
|
||||
switch (error) {
|
||||
case GL11.GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||||
case GL11.GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||||
case GL11.GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||||
case GL11.GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||||
case GL11.GL_STACK_OVERFLOW: return "GL_STACK_OVERFLOW";
|
||||
case GL11.GL_STACK_UNDERFLOW: return "GL_STACK_UNDERFLOW";
|
||||
default: return "Unknown Error (0x" + Integer.toHexString(error) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Getter方法 ====================
|
||||
|
||||
public int getTextureId() {
|
||||
return textureId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public TextureFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public TextureType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public TextureFilter getMinFilter() {
|
||||
return minFilter;
|
||||
}
|
||||
|
||||
public TextureFilter getMagFilter() {
|
||||
return magFilter;
|
||||
}
|
||||
|
||||
public TextureWrap getWrapS() {
|
||||
return wrapS;
|
||||
}
|
||||
|
||||
public TextureWrap getWrapT() {
|
||||
return wrapT;
|
||||
}
|
||||
|
||||
public boolean isMipmapsEnabled() {
|
||||
return mipmapsEnabled;
|
||||
}
|
||||
|
||||
public long getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
// ==================== Object方法 ====================
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Texture texture = (Texture) o;
|
||||
return textureId == texture.textureId &&
|
||||
width == texture.width &&
|
||||
height == texture.height &&
|
||||
disposed == texture.disposed &&
|
||||
Objects.equals(name, texture.name) &&
|
||||
format == texture.format &&
|
||||
type == texture.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(textureId, name, width, height, format, type, disposed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Texture{" +
|
||||
"id=" + textureId +
|
||||
", name='" + name + '\'' +
|
||||
", size=" + width + "x" + height +
|
||||
", format=" + format +
|
||||
", type=" + type +
|
||||
", memory=" + getEstimatedMemoryUsage() + " bytes" +
|
||||
", disposed=" + disposed +
|
||||
'}';
|
||||
}
|
||||
|
||||
// ==================== 静态清理方法 ====================
|
||||
|
||||
/**
|
||||
* 清理所有缓存的纹理
|
||||
*/
|
||||
public static void cleanupAll() {
|
||||
TEXTURE_CACHE.values().forEach(Texture::dispose);
|
||||
TEXTURE_CACHE.clear();
|
||||
}
|
||||
}
|
||||
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