diff --git a/build.gradle b/build.gradle index b22e124..923e960 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java index 831fc27..0d69b64 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java @@ -1287,7 +1287,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { applyLanguageSettings(settingsManager); // 5. 应用CUDA设置 - applyCUDASettings(settingsManager); + //applyCUDASettings(settingsManager); logger.info("所有设置配置已成功应用"); diff --git a/src/main/java/com/chuangzhou/vivid2D/Main.java b/src/main/java/com/chuangzhou/vivid2D/Main.java new file mode 100644 index 0000000..944a222 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/Main.java @@ -0,0 +1,8 @@ +package com.chuangzhou.vivid2D; + +public class Main { + + public static void main(String[] args) { + + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java new file mode 100644 index 0000000..c5b9791 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -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 shaderMap = new HashMap<>(); + private static ShaderProgram defaultProgram = null; + + private static final Map 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 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(); } + +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java b/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java new file mode 100644 index 0000000..cbbfccf --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java new file mode 100644 index 0000000..b1d0701 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -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 parts; + private final Map partMap; // 快速查找 + private ModelPart rootPart; + + // ==================== 网格系统 ==================== + private final List meshes; + private final Map textures; // 纹理映射 + + // ==================== 动画系统 ==================== + private final Map parameters; + private final List 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 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 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 getMeshes() { return Collections.unmodifiableList(meshes); } + + public Map getParameters() { + return Collections.unmodifiableMap(parameters); + } + + public List 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; } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java new file mode 100644 index 0000000..dd8e4fb --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java @@ -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 parts; + private List meshes; + private List textures; + private List parameters; + private List animations; + + // ==================== 模型设置 ==================== + private Vector2f pivotPoint; + private float unitsPerMeter; + private Map 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 textureMap = deserializeTextures(); + + // 然后创建所有网格(依赖纹理) + Map meshMap = deserializeMeshes(textureMap); + + // 然后创建部件(依赖网格) + deserializeParts(model, meshMap); + + // 最后创建参数 + deserializeParameters(model); + + return model; + } + + private Map deserializeTextures() { + Map textureMap = new HashMap<>(); + for (TextureData textureData : textures) { + Texture texture = textureData.toTexture(); + textureMap.put(texture.getName(), texture); + } + return textureMap; + } + + private Map deserializeMeshes(Map textureMap) { + Map 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 meshMap) { + // 先创建所有部件 + Map 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 getValidationErrors() { + List 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 meshNames; + public Map 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 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> 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 getParts() { return parts; } + public void setParts(List parts) { this.parts = parts; } + + public List getMeshes() { return meshes; } + public void setMeshes(List meshes) { this.meshes = meshes; } + + public List getTextures() { return textures; } + public void setTextures(List textures) { this.textures = textures; } + + public List getParameters() { return parameters; } + public void setParameters(List parameters) { this.parameters = parameters; } + + public List getAnimations() { return animations; } + public void setAnimations(List 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 getUserData() { return userData; } + public void setUserData(Map 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() + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java new file mode 100644 index 0000000..7d16db9 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -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 children; + private final List 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 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 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 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 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() + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java new file mode 100644 index 0000000..3dbbc3e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java @@ -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; } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java new file mode 100644 index 0000000..263de63 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java @@ -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); } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java new file mode 100644 index 0000000..3cc5a05 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java @@ -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 vertexDeformations; + private final List 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 deformations) { + for (Map.Entry 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 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 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 reversed = new HashMap<>(); + + for (Map.Entry 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; // 反转当前值 + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java new file mode 100644 index 0000000..7506a66 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java @@ -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; } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java new file mode 100644 index 0000000..1dbbd6d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java @@ -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 curves; + private final List eventMarkers; + private final Map defaultValues; + + // ==================== 元数据 ==================== + private String author; + private String description; + private long creationTime; + private long lastModifiedTime; + private Map 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 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 getKeyframes(String parameterId) { + AnimationCurve curve = curves.get(parameterId); + return curve != null ? curve.getKeyframes() : Collections.emptyList(); + } + + // ==================== 采样系统 ==================== + + /** + * 采样动画在指定时间的参数值 + */ + public Map sample(float time) { + Map result = new HashMap<>(); + + for (Map.Entry 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 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 getEventMarkersInRange(float startTime, float endTime) { + List 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 getValueBounds() { + Map bounds = new HashMap<>(); + + for (Map.Entry 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 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 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 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 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 getCurves() { + return Collections.unmodifiableMap(curves); + } + + public List getEventMarkers() { + return Collections.unmodifiableList(eventMarkers); + } + + public Map 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 getUserData() { + return Collections.unmodifiableMap(userData); + } + public void setUserData(Map 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java new file mode 100644 index 0000000..58b3091 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java @@ -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 tracks; + private final List clips; + private AnimationClip currentClip; + private float playbackSpeed; + private boolean looping; + + // ==================== 状态管理 ==================== + private float currentTime; + private boolean playing; + private boolean paused; + private Map parameterOverrides; + + // ==================== 事件系统 ==================== + private final List eventListeners; + private final Map> 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 animatedValues = currentClip.sample(currentTime); + + for (Map.Entry 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 eventList : events.values()) { + for (AnimationEvent event : eventList) { + if (!event.isTriggered() && currentTime >= event.getTime()) { + event.trigger(); + notifyEventTriggered(event); + } + } + } + } + + /** + * 重置所有事件状态 + */ + public void resetEvents() { + for (List 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 getTracks() { + return Collections.unmodifiableMap(tracks); + } + + public List 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 getParameterOverrides() { + return Collections.unmodifiableMap(parameterOverrides); + } + + // ==================== 枚举和内部类 ==================== + + /** + * 混合模式枚举 + */ + public enum BlendMode { + OVERRIDE, // 覆盖混合 + ADDITIVE, // 叠加混合 + MULTIPLICATIVE, // 乘法混合 + AVERAGE // 平均混合 + } + + /** + * 动画轨道类 + */ + public static class AnimationTrack { + private final String parameterId; + private final List 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 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 + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java new file mode 100644 index 0000000..15d4df0 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java new file mode 100644 index 0000000..01292f7 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java @@ -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 drivenParameters; + protected final Map 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 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 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; } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java new file mode 100644 index 0000000..1d76445 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java @@ -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()); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java new file mode 100644 index 0000000..c2bc913 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -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() + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelMetadata.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelMetadata.java new file mode 100644 index 0000000..9683976 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelMetadata.java @@ -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 userProperties; + private List 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 entry : other.userProperties.entrySet()) { + if (!this.userProperties.containsKey(entry.getKey())) { + this.userProperties.put(entry.getKey(), entry.getValue()); + } + } + + markModified(); + } + + /** + * 转换为简化的信息映射 + */ + public Map toInfoMap() { + Map 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 getUserProperties() { + return Collections.unmodifiableMap(userProperties); + } + + public void setUserProperties(Map userProperties) { + this.userProperties = new HashMap<>(userProperties); + markModified(); + } + + public List getTags() { + return Collections.unmodifiableList(tags); + } + + public void setTags(List 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java new file mode 100644 index 0000000..22ee098 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java @@ -0,0 +1,4 @@ +package com.chuangzhou.vivid2D.render.model.util; + +public class ModelPose { +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java new file mode 100644 index 0000000..edebb20 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java @@ -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 particles; + private final List springs; + private final List constraints; + private final List 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 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 getParticles() { + return Collections.unmodifiableMap(particles); + } + + public List getSprings() { + return Collections.unmodifiableList(springs); + } + + public List getConstraints() { + return Collections.unmodifiableList(constraints); + } + + public List 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 + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java new file mode 100644 index 0000000..21d3a74 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java new file mode 100644 index 0000000..773161e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java @@ -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"); + } +}