From 9cde0192fda3f22556db299f2eb212b98d819827 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 11 Oct 2025 20:21:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E6=B7=BB=E5=8A=A0=E5=85=89?= =?UTF-8?q?=E6=BA=90=E4=B8=8E=E7=89=A9=E7=90=86=E7=B3=BB=E7=BB=9F=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BufferBuilder 工具类用于简化顶点数据提交 - 实现 LightSource 和 LightSourceData 类以支持光源管理- 在 Model2D 中集成光源系统,支持序列化与反序列化 - 扩展 ModelData 以支持物理系统数据的完整序列化 - 重构 ModelRender以支持物理系统应用及碰撞箱渲染 - 添加粒子、弹簧、约束与碰撞体的数据结构与序列化逻辑 - 实现变形器的序列化接口以支持参数驱动动画的持久化 --- .../vivid2D/render/ModelRender.java | 346 +++++++-- .../vivid2D/render/model/Model2D.java | 79 +- .../vivid2D/render/model/ModelData.java | 534 ++++++++++++-- .../model/transform/RotationDeformer.java | 18 + .../render/model/transform/ScaleDeformer.java | 21 + .../model/transform/VertexDeformer.java | 99 ++- .../render/model/transform/WaveDeformer.java | 22 + .../render/model/util/BufferBuilder.java | 119 +++ .../vivid2D/render/model/util/Deformer.java | 10 + .../render/model/util/LightSource.java | 36 + .../render/model/util/LightSourceData.java | 193 +++++ .../render/model/util/PhysicsSystem.java | 372 +++++++--- .../render/model/util/SaveVector2f.java | 70 ++ .../vivid2D/test/ModelRenderLightingTest.java | 234 ++++++ .../vivid2D/test/ModelRenderTest.java | 9 + .../chuangzhou/vivid2D/test/ModelTest.java | 238 +++++- .../chuangzhou/vivid2D/test/ModelTest2.java | 682 ++++++++++++++++++ vivid2DApi.md | 265 +++++++ 18 files changed, 3071 insertions(+), 276 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/BufferBuilder.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSourceData.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java create mode 100644 vivid2DApi.md diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index ff984df..9b1d8fb 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -2,17 +2,19 @@ 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.LightSource; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; // 引入 PhysicsSystem import com.chuangzhou.vivid2D.render.model.util.Texture; import org.joml.Matrix3f; import org.joml.Vector2f; +import org.joml.Vector3f; 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; @@ -20,6 +22,7 @@ import static org.lwjgl.opengl.GL20.glGetUniformLocation; /** * 重构后的 ModelRender:更模块化、健壮的渲染子系统 + * (已修改以应用物理系统,并支持渲染碰撞箱) * @author tzdwindows 7 */ public final class ModelRender { @@ -44,6 +47,18 @@ public final class ModelRender { // 默认白色纹理 private static int defaultTextureId = 0; + // ================== 碰撞箱渲染配置 ================== + // 是否在渲染时绘制碰撞箱(线框) + public static boolean renderColliders = false; + // 碰撞箱线宽 + public static float colliderLineWidth = 2.0f; + // 碰撞箱颜色(默认白色) + public static Vector4f colliderColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + // 圆形碰撞体绘制细分(越高越圆) + private static final int CIRCLE_SEGMENTS = 32; + // 是否在渲染时绘制碰撞箱 + public static boolean renderLightPositions = true; + // ================== 内部类:ShaderProgram ================== private static class ShaderProgram { final int programId; @@ -96,46 +111,84 @@ public final class ModelRender { // ================== 着色器源 ================== private static final String VERTEX_SHADER_SRC = """ - #version 330 core - layout(location = 0) in vec2 aPosition; - layout(location = 1) in vec2 aTexCoord; - out vec2 vTexCoord; - out vec2 vDebugPos; - uniform mat3 uModelMatrix; - uniform mat3 uViewMatrix; - uniform mat3 uProjectionMatrix; - void main() { - vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); - gl_Position = vec4(p.xy, 0.0, 1.0); - vTexCoord = aTexCoord; - vDebugPos = p.xy; - }"""; + #version 330 core + layout(location = 0) in vec2 aPosition; + layout(location = 1) in vec2 aTexCoord; + out vec2 vTexCoord; + out vec2 vWorldPos; + + uniform mat3 uModelMatrix; + uniform mat3 uViewMatrix; + uniform mat3 uProjectionMatrix; + + void main() { + vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); + gl_Position = vec4(p.xy, 0.0, 1.0); + vTexCoord = aTexCoord; + vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy; + } + """; private static final String FRAGMENT_SHADER_SRC = """ - #version 330 core - in vec2 vTexCoord; - in vec2 vDebugPos; - out vec4 FragColor; - uniform sampler2D uTexture; - uniform vec4 uColor; - uniform float uOpacity; - uniform int uBlendMode; - uniform int uDebugMode; - void main() { - if (uDebugMode == 1) { - FragColor = vec4(vDebugPos * 0.5 + 0.5, 0.0, 1.0); - return; - } - vec4 tex = texture(uTexture, vTexCoord); - vec4 finalColor = tex * uColor; - if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb; - else if (uBlendMode == 2) finalColor.rgb = tex.rgb * uColor.rgb; - else if (uBlendMode == 3) finalColor.rgb = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb); - finalColor.a = tex.a * uOpacity; - if (finalColor.a <= 0.001) discard; - FragColor = finalColor; - }"""; + #version 330 core + in vec2 vTexCoord; + in vec2 vWorldPos; + out vec4 FragColor; + + uniform sampler2D uTexture; + uniform vec4 uColor; + uniform float uOpacity; + uniform int uBlendMode; + uniform int uDebugMode; + + #define MAX_LIGHTS 8 + uniform vec2 uLightsPos[MAX_LIGHTS]; + uniform vec3 uLightsColor[MAX_LIGHTS]; + uniform float uLightsIntensity[MAX_LIGHTS]; + uniform int uLightsIsAmbient[MAX_LIGHTS]; + uniform int uLightCount; + + void main() { + if (uDebugMode == 1) { + FragColor = vec4(vWorldPos * 0.5 + 0.5, 0.0, 1.0); + return; + } + + vec4 tex = texture(uTexture, vTexCoord); + vec3 finalColor = tex.rgb * uColor.rgb; + vec3 lighting = vec3(0.0); + + for (int i = 0; i < uLightCount; i++) { + if (uLightsIsAmbient[i] == 1) { + lighting += uLightsColor[i] * uLightsIntensity[i]; + } + } + + for (int i = 0; i < uLightCount; i++) { + if (uLightsIsAmbient[i] == 1) continue; + + float intensity = uLightsIntensity[i]; + if (intensity <= 0.0) continue; + + vec2 lightDir = uLightsPos[i] - vWorldPos; + float dist = length(lightDir); + float atten = 1.0 / (1.0 + 0.1 * dist + 0.01 * dist * dist); + lighting += uLightsColor[i] * intensity * atten; + } + + finalColor *= min(lighting, vec3(2.0)); + + if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb; + else if (uBlendMode == 2) finalColor.rgb = tex.rgb * uColor.rgb; + else if (uBlendMode == 3) finalColor.rgb = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb); + + float alpha = tex.a * uOpacity; + if (alpha <= 0.001) discard; + + FragColor = vec4(finalColor, alpha); + } + """; // ================== 初始化 / 清理 ================== public static synchronized void initialize() { @@ -174,6 +227,35 @@ public final class ModelRender { System.out.println("GLSL Version: " + GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION)); } + private static void uploadLightsToShader(ShaderProgram sp, Model2D model) { + List lights = model.getLights(); + int lightCount = Math.min(lights.size(), 8); + + // 设置光源数量 + setUniformIntInternal(sp, "uLightCount", lightCount); + + for (int i = 0; i < lightCount; i++) { + LightSource l = lights.get(i); + if (!l.isEnabled()) continue; + + // 设置光源位置(环境光位置设为0) + Vector2f pos = l.isAmbient() ? new Vector2f(0, 0) : l.getPosition(); + setUniformVec2Internal(sp, "uLightsPos[" + i + "]", pos); + + setUniformVec3Internal(sp, "uLightsColor[" + i + "]", l.getColor()); + setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", l.getIntensity()); + + // 设置是否为环境光 + setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", l.isAmbient() ? 1 : 0); + } + + // 禁用未使用的光源 + for (int i = lightCount; i < 8; i++) { + setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); + setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0); + } + } + private static void setupGLState() { GL11.glClearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w); @@ -204,12 +286,13 @@ public final class ModelRender { 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); + setUniformIntInternal(sp, "uDebugMode", 0); + setUniformIntInternal(sp, "uLightCount", 0); // 默认没有光源 sp.stop(); } @@ -284,38 +367,70 @@ public final class ModelRender { 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 已经被计算) + // 物理系统更新 + PhysicsSystem physics = model.getPhysics(); + if (physics != null && physics.isEnabled()) { + physics.update(deltaTime, model); + } + model.update(deltaTime); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0)); - // 使用默认 shader(保持绑定直到完成渲染) defaultProgram.use(); - // setUniformIntInternal(defaultProgram, "uDebugMode", 0); 设置debug模式 - - // 设置投影与视图(3x3 正交投影用于 2D) + // 设置投影与视图 Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight); setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity()); - // 递归渲染所有根部件(使用 3x3 矩阵) + // 添加光源数据上传 + uploadLightsToShader(defaultProgram, model); + renderLightPositions(model); + + // 递归渲染所有根部件 Matrix3f identity = new Matrix3f().identity(); for (ModelPart p : model.getParts()) { if (p.getParent() != null) continue; renderPartRecursive(p, identity); } - defaultProgram.stop(); + if (renderColliders && physics != null) { + renderPhysicsColliders(physics); + } + + + defaultProgram.stop(); checkGLError("render"); } + private static void renderLightPositions(Model2D model) { + if (!renderLightPositions) return; + + GL11.glPointSize(10.0f); + setUniformIntInternal(defaultProgram, "uDebugMode", 1); + + for (LightSource light : model.getLights()) { + if (!light.isEnabled()) continue; + + // 绘制光源位置 + com.chuangzhou.vivid2D.render.util.BufferBuilder bb = + new com.chuangzhou.vivid2D.render.util.BufferBuilder(1 * 4); + bb.begin(GL11.GL_POINTS, 1); + bb.vertex(light.getPosition().x, light.getPosition().y, 0.5f, 0.5f); + bb.end(); + } + + setUniformIntInternal(defaultProgram, "uDebugMode", 0); + GL11.glPointSize(1.0f); + } + /** * 关键修改点:在渲染前确保更新 part 的 worldTransform, * 然后直接使用 part.getWorldTransform() 作为 uModelMatrix 传入 shader。 @@ -330,9 +445,8 @@ public final class ModelRender { // 先设置部件相关的 uniform(opacity / blendMode / color 等) setPartUniforms(defaultProgram, part); - // 把 world 矩阵传给 shader(兼容 uModelMatrix 和 可能的 uModel) + // 把 world 矩阵传给 shader(uModelMatrix) setUniformMatrix3(defaultProgram, "uModelMatrix", world); - setUniformMatrix3(defaultProgram, "uModel", world); // 绘制本节点的所有 mesh(将 world 传入 renderMesh) for (Mesh2D mesh : part.getMeshes()) { @@ -348,9 +462,6 @@ public final class ModelRender { private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) { if (!mesh.isVisible()) return; - // 使用默认 shader - defaultProgram.use(); - // 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换) Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix; @@ -364,27 +475,124 @@ public final class ModelRender { setUniformIntInternal(defaultProgram, "uTexture", 0); } + // 将模型矩阵设置为当前 mesh 使用的矩阵(shader 内名为 uModelMatrix) + setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse); + // 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵 mesh.draw(defaultProgram.programId, matToUse); 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 renderPhysicsColliders(PhysicsSystem physics) { + // 设置渲染状态 + GL11.glLineWidth(colliderLineWidth); + + // 绑定默认纹理(shader 依赖 uTexture)并设置颜色/opacity + GL13.glActiveTexture(GL13.GL_TEXTURE0); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, defaultTextureId); + setUniformIntInternal(defaultProgram, "uTexture", 0); + setUniformVec4Internal(defaultProgram, "uColor", colliderColor); + setUniformFloatInternal(defaultProgram, "uOpacity", 1.0f); + setUniformIntInternal(defaultProgram, "uBlendMode", 0); + setUniformIntInternal(defaultProgram, "uDebugMode", 0); + + // 使用单位矩阵作为 model(碰撞体顶点按世界坐标提供) + setUniformMatrix3(defaultProgram, "uModelMatrix", new Matrix3f().identity()); + + for (PhysicsSystem.PhysicsCollider collider : physics.getColliders()) { + if (!collider.isEnabled()) continue; + + if (collider instanceof PhysicsSystem.CircleCollider) { + PhysicsSystem.CircleCollider c = (PhysicsSystem.CircleCollider) collider; + drawCircleColliderWire(c.getCenter(), c.getRadius()); + } else if (collider instanceof PhysicsSystem.RectangleCollider) { + PhysicsSystem.RectangleCollider r = (PhysicsSystem.RectangleCollider) collider; + drawRectangleColliderWire(r.getCenter(), r.getWidth(), r.getHeight()); + } else { + // 未知类型:尝试调用 collidesWith 以获取位置(跳过) + } } + + // 恢复默认线宽 + GL11.glLineWidth(1.0f); } - // ================== 上传数据 ==================(被弃用) + /** + * 绘制圆形碰撞框(线框) + * 使用临时 VAO/VBO,每帧创建并删除(简单实现) + */ + private static void drawCircleColliderWire(Vector2f center, float radius) { + int segments = Math.max(8, CIRCLE_SEGMENTS); + com.chuangzhou.vivid2D.render.util.BufferBuilder bb = new com.chuangzhou.vivid2D.render.util.BufferBuilder(segments * 4); + bb.begin(GL11.GL_LINE_LOOP, segments); + for (int i = 0; i < segments; i++) { + double ang = 2.0 * Math.PI * i / segments; + float x = center.x + radius * (float) Math.cos(ang); + float y = center.y + radius * (float) Math.sin(ang); + // 给常量 texcoord + bb.vertex(x, y, 0.5f, 0.5f); + } + bb.end(); + } + + /** + * 绘制矩形碰撞框(线框) + */ + private static void drawRectangleColliderWire(Vector2f center, float width, float height) { + float halfW = width / 2.0f; + float halfH = height / 2.0f; + com.chuangzhou.vivid2D.render.util.BufferBuilder bb = new com.chuangzhou.vivid2D.render.util.BufferBuilder(4 * 4); + bb.begin(GL11.GL_LINE_LOOP, 4); + bb.vertex(center.x - halfW, center.y - halfH, 0.5f, 0.5f); + bb.vertex(center.x + halfW, center.y - halfH, 0.5f, 0.5f); + bb.vertex(center.x + halfW, center.y + halfH, 0.5f, 0.5f); + bb.vertex(center.x - halfW, center.y + halfH, 0.5f, 0.5f); + bb.end(); + } + + /** + * 从 float[] (x,y,u,v interleaved) 绘制 GL_LINE_LOOP + */ + private static void drawLineLoopFromFloatArray(float[] interleavedXYUV, int vertexCount) { + // 创建 VAO/VBO + int vao = GL30.glGenVertexArrays(); + int vbo = GL15.glGenBuffers(); + + GL30.glBindVertexArray(vao); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo); + + // 上传数据 + FloatBuffer fb = MemoryUtil.memAllocFloat(interleavedXYUV.length); + fb.put(interleavedXYUV).flip(); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, fb, GL15.GL_DYNAMIC_DRAW); + MemoryUtil.memFree(fb); + + // attrib 0 -> aPosition (vec2) + GL20.glEnableVertexAttribArray(0); + GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 4 * Float.BYTES, 0); + + // attrib 1 -> aTexCoord (vec2) (提供常量 texcoord) + GL20.glEnableVertexAttribArray(1); + GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, 4 * Float.BYTES, 2 * Float.BYTES); + + // 绘制线环 + GL11.glDrawArrays(GL11.GL_LINE_LOOP, 0, vertexCount); + + // 清理 + GL20.glDisableVertexAttribArray(0); + GL20.glDisableVertexAttribArray(1); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + + GL15.glDeleteBuffers(vbo); + GL30.glDeleteVertexArrays(vao); + + checkGLError("drawLineLoopFromFloatArray"); + } // ================== uniform 设置辅助(内部使用,确保 program 已绑定) ================== private static void setUniformIntInternal(ShaderProgram sp, String name, int value) { @@ -392,6 +600,20 @@ public final class ModelRender { if (loc != -1) GL20.glUniform1i(loc, value); } + private static void setUniformVec3Internal(ShaderProgram sp, String name, org.joml.Vector3f vec) { + int loc = sp.getUniformLocation(name); + if (loc != -1) { + GL20.glUniform3f(loc, vec.x, vec.y, vec.z); + } + } + + private static void setUniformVec2Internal(ShaderProgram sp, String name, org.joml.Vector2f vec) { + int loc = sp.getUniformLocation(name); + if (loc != -1) { + GL20.glUniform2f(loc, vec.x, vec.y); + } + } + private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) { int loc = sp.getUniformLocation(name); if (loc != -1) GL20.glUniform1f(loc, value); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index 69c6017..8eadc03 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -7,41 +7,7 @@ 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); // 更新模型状态 + * (已修改以配合 ModelRender 的物理系统应用) * * @author tzdwindows 7 */ @@ -71,6 +37,9 @@ public class Model2D { private transient boolean needsUpdate = true; private transient BoundingBox bounds; + // ==================== 光源系统 ==================== + private final List lights; + // ==================== 构造器 ==================== public Model2D() { this.uuid = UUID.randomUUID(); @@ -83,6 +52,7 @@ public class Model2D { this.physics = new PhysicsSystem(); this.currentPose = new ModelPose(); this.metadata = new ModelMetadata(); + this.lights = new ArrayList<>(); } public Model2D(String name) { @@ -90,6 +60,34 @@ public class Model2D { this.name = name; } + // ==================== 光源管理 ==================== + public List getLights() { + return Collections.unmodifiableList(lights); + } + + public void addLight(LightSource light) { + if (light == null) { + throw new IllegalArgumentException("LightSource cannot be null"); + } + lights.add(light); + markNeedsUpdate(); + } + + public void removeLight(LightSource light) { + if (lights.remove(light)) { + markNeedsUpdate(); + } + } + + public boolean isStartLight(LightSource light) { + return lights.isEmpty(); + } + + public void clearLights() { + lights.clear(); + markNeedsUpdate(); + } + // ==================== 部件管理 ==================== public ModelPart createPart(String name) { ModelPart part = new ModelPart(name); @@ -202,14 +200,16 @@ public class Model2D { return layer; } - // ==================== 更新系统 ==================== + // ==================== 更新系统 (已修改) ==================== public void update(float deltaTime) { + // 物理系统更新已被移至渲染器(ModelRender)中,以确保它在渲染前被调用。 + // 这里的 hasActivePhysics() 检查可以保留,用于决定是否需要更新变换,以优化性能。 if (!needsUpdate && !physics.hasActivePhysics()) { return; } - // 更新物理系统 - physics.update(deltaTime, this); + // 核心修改:移除了 physics.update(deltaTime, this); 这一行。 + // 该调用现在由 ModelRender.render() 方法负责。 // 更新所有参数驱动的变形 updateParameterDeformations(); @@ -327,6 +327,7 @@ public class Model2D { } } + /** * 从压缩文件加载模型 */ @@ -352,6 +353,8 @@ public class Model2D { public ModelPart getRootPart() { return rootPart; } public void setRootPart(ModelPart rootPart) { this.rootPart = rootPart; } + + public List getMeshes() { return Collections.unmodifiableList(meshes); } public Map getParameters() { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java index f1550d5..d3155ad 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java @@ -36,6 +36,19 @@ public class ModelData implements Serializable { private List parameters; private List animations; private List animationLayers; + private List physicsParticles; + private List physicsSprings; + private List physicsColliders; + private List physicsConstraints; + private List lights; + + // 全局物理参数(便于序列化) + private float physicsGravityX; + private float physicsGravityY; + private float physicsAirResistance; + private float physicsTimeScale; + private boolean physicsEnabled; + // ==================== 模型设置 ==================== private Vector2f pivotPoint; @@ -61,10 +74,22 @@ public class ModelData implements Serializable { this.parameters = new ArrayList<>(); this.animations = new ArrayList<>(); this.animationLayers = new ArrayList<>(); + this.lights = new ArrayList<>(); this.pivotPoint = new Vector2f(); this.unitsPerMeter = 100.0f; // 默认100单位/米 this.userData = new HashMap<>(); + + // 物理数据初始化 + this.physicsParticles = new ArrayList<>(); + this.physicsSprings = new ArrayList<>(); + this.physicsColliders = new ArrayList<>(); + this.physicsConstraints = new ArrayList<>(); + this.physicsGravityX = 0.0f; + this.physicsGravityY = -98.0f; + this.physicsAirResistance = 0.1f; + this.physicsTimeScale = 1.0f; + this.physicsEnabled = true; } public ModelData(Model2D model) { @@ -74,6 +99,133 @@ public class ModelData implements Serializable { // ==================== 序列化方法 ==================== + private void serializePhysics(Model2D model) { + physicsParticles.clear(); + physicsSprings.clear(); + physicsColliders.clear(); + physicsConstraints.clear(); + + if (model == null) return; + PhysicsSystem phys = model.getPhysics(); + if (phys == null) return; + + // 全局参数 + Vector2f g = phys.getGravity(); + this.physicsGravityX = g.x; + this.physicsGravityY = g.y; + this.physicsAirResistance = phys.getAirResistance(); + this.physicsTimeScale = phys.getTimeScale(); + this.physicsEnabled = phys.isEnabled(); + + // 粒子 + for (Map.Entry e : phys.getParticles().entrySet()) { + PhysicsSystem.PhysicsParticle p = e.getValue(); + ParticleData pd = new ParticleData(); + pd.id = p.getId(); + Vector2f pos = p.getPosition(); + pd.x = pos.x; + pd.y = pos.y; + pd.mass = p.getMass(); + pd.radius = p.getRadius(); + pd.movable = p.isMovable(); + pd.affectedByGravity = p.isAffectedByGravity(); + pd.affectedByWind = p.isAffectedByWind(); + // 如果 userData 是 ModelPart,则保存其 name 以便反序列化时恢复关联 + Object ud = p.getUserData(); + if (ud instanceof ModelPart) { + pd.userPartName = ((ModelPart) ud).getName(); + } else { + pd.userPartName = null; + } + physicsParticles.add(pd); + } + + // 弹簧 + for (PhysicsSystem.PhysicsSpring s : phys.getSprings()) { + SpringData sd = new SpringData(); + sd.id = s.getId(); + sd.aId = s.getParticleA().getId(); + sd.bId = s.getParticleB().getId(); + sd.restLength = s.getRestLength(); + sd.stiffness = s.getStiffness(); + sd.damping = s.getDamping(); + sd.enabled = s.isEnabled(); + physicsSprings.add(sd); + } + + // 约束(仅序列化常见两类) + for (PhysicsSystem.PhysicsConstraint c : phys.getConstraints()) { + if (c instanceof PhysicsSystem.PositionConstraint) { + PhysicsSystem.PositionConstraint pc = (PhysicsSystem.PositionConstraint) c; + ConstraintData cd = new ConstraintData(); + cd.type = "position"; + cd.particleId = pc.getParticle().getId(); + Vector2f tp = pc.getTargetPosition(); + cd.targetX = tp.x; + cd.targetY = tp.y; + cd.strength = pc.getStrength(); + cd.enabled = pc.isEnabled(); + physicsConstraints.add(cd); + } else if (c instanceof PhysicsSystem.DistanceConstraint) { + PhysicsSystem.DistanceConstraint dc = (PhysicsSystem.DistanceConstraint) c; + ConstraintData cd = new ConstraintData(); + cd.type = "distance"; + cd.particleId = dc.getParticle().getId(); + cd.targetParticleId = dc.getTarget().getId(); + cd.maxDistance = dc.getMaxDistance(); + cd.enabled = dc.isEnabled(); + physicsConstraints.add(cd); + } else { + // 忽略未知类型 + } + } + + // 碰撞体 + for (PhysicsSystem.PhysicsCollider collider : phys.getColliders()) { + ColliderData cd = new ColliderData(); + cd.id = collider.getId(); + cd.enabled = collider.isEnabled(); + if (collider instanceof PhysicsSystem.CircleCollider) { + PhysicsSystem.CircleCollider cc = (PhysicsSystem.CircleCollider) collider; + cd.type = "circle"; + cd.centerX = cc.getCenter().x; + cd.centerY = cc.getCenter().y; + cd.radius = cc.getRadius(); + } else if (collider instanceof PhysicsSystem.RectangleCollider) { + PhysicsSystem.RectangleCollider rc = (PhysicsSystem.RectangleCollider) collider; + cd.type = "rect"; + cd.centerX = rc.getCenter().x; + cd.centerY = rc.getCenter().y; + cd.width = rc.getWidth(); + cd.height = rc.getHeight(); + } else { + // 未知类型:跳过或扩展 + continue; + } + physicsColliders.add(cd); + } + } + + private void serializeLights(Model2D model) { + lights.clear(); + if (model.getLights() != null) { + for (LightSource light : model.getLights()) { + lights.add(new LightSourceData(light)); + } + } + } + + private void deserializeLights(Model2D model) { + if (lights != null) { + for (LightSourceData lightData : lights) { + LightSource light = lightData.toLightSource(); + if (light != null) { + model.addLight(light); + } + } + } + } + /** * 从Model2D对象序列化数据 */ @@ -90,6 +242,9 @@ public class ModelData implements Serializable { if (model.getMetadata() != null) { this.author = model.getMetadata().getAuthor(); this.description = model.getMetadata().getDescription(); + this.unitsPerMeter = model.getMetadata().getUnitsPerMeter(); + this.pivotPoint = new Vector2f(model.getMetadata().getPivotPoint()); + this.userData = new HashMap<>(model.getMetadata().getUserProperties()); } // 序列化部件 @@ -107,9 +262,16 @@ public class ModelData implements Serializable { // 序列化动画层 serializeAnimationLayers(model); + // 序列化物理系统 + serializePhysics(model); + + // 序列化光源 + serializeLights(model); + lastModifiedTime = System.currentTimeMillis(); } + private void serializeParts(Model2D model) { parts.clear(); for (ModelPart part : model.getParts()) { @@ -190,20 +352,145 @@ public class ModelData implements Serializable { model.addTexture(texture); } - // 然后创建所有网格(依赖纹理) Map meshMap = deserializeMeshes(textureMap); // 然后创建部件(依赖网格) - deserializeParts(model, meshMap); + Map partMap = deserializeParts(model, meshMap); // 最后创建参数 deserializeParameters(model); // 创建动画层 deserializeAnimationLayers(model); + + // 反序列化物理系统 + deserializePhysics(model, partMap); + + // 反序列化光源 + deserializeLights(model); return model; } + private void deserializePhysics(Model2D model, Map partMap) { + if (model == null) return; + PhysicsSystem phys = model.getPhysics(); + if (phys == null) return; + + phys.reset(); + phys.setGravity(this.physicsGravityX, this.physicsGravityY); + phys.setAirResistance(this.physicsAirResistance); + phys.setTimeScale(this.physicsTimeScale); + phys.setEnabled(this.physicsEnabled); + + // 安全处理列表,避免 null + List particles = physicsParticles != null ? physicsParticles : new ArrayList<>(); + List springs = physicsSprings != null ? physicsSprings : new ArrayList<>(); + List constraints = physicsConstraints != null ? physicsConstraints : new ArrayList<>(); + List colliders = physicsColliders != null ? physicsColliders : new ArrayList<>(); + + // 创建粒子 + Map idToParticle = new HashMap<>(); + for (ParticleData pd : particles) { + PhysicsSystem.PhysicsParticle p = phys.addParticle(pd.id, new Vector2f(pd.x, pd.y), pd.mass); + p.setRadius(pd.radius); + p.setMovable(pd.movable); + p.setAffectedByGravity(pd.affectedByGravity); + p.setAffectedByWind(pd.affectedByWind); + + if (pd.userPartName != null && partMap != null) { + ModelPart mp = partMap.get(pd.userPartName); + if (mp != null) p.setUserData(mp); + } + idToParticle.put(pd.id, p); + } + + // 创建弹簧 + for (SpringData sd : springs) { + PhysicsSystem.PhysicsParticle a = idToParticle.get(sd.aId); + PhysicsSystem.PhysicsParticle b = idToParticle.get(sd.bId); + if (a != null && b != null) { + PhysicsSystem.PhysicsSpring s = phys.addSpring(sd.id, a, b, sd.restLength, sd.stiffness, sd.damping); + s.setEnabled(sd.enabled); + } + } + + // 创建约束 + for (ConstraintData cd : constraints) { + if ("position".equals(cd.type)) { + PhysicsSystem.PhysicsParticle p = idToParticle.get(cd.particleId); + if (p != null) { + PhysicsSystem.PhysicsConstraint c = phys.addPositionConstraint(p, new Vector2f(cd.targetX, cd.targetY)); + if (c instanceof PhysicsSystem.PositionConstraint) { + ((PhysicsSystem.PositionConstraint) c).setStrength(cd.strength); + c.setEnabled(cd.enabled); + } + } + } else if ("distance".equals(cd.type)) { + PhysicsSystem.PhysicsParticle p = idToParticle.get(cd.particleId); + PhysicsSystem.PhysicsParticle target = idToParticle.get(cd.targetParticleId); + if (p != null && target != null) { + PhysicsSystem.PhysicsConstraint c = phys.addDistanceConstraint(p, target, cd.maxDistance); + c.setEnabled(cd.enabled); + } + } + } + + // 创建碰撞体 + for (ColliderData cd : colliders) { + if ("circle".equals(cd.type)) { + PhysicsSystem.PhysicsCollider coll = phys.addCircleCollider(cd.id, new Vector2f(cd.centerX, cd.centerY), cd.radius); + coll.setEnabled(cd.enabled); + } else if ("rect".equals(cd.type)) { + PhysicsSystem.PhysicsCollider coll = phys.addRectangleCollider(cd.id, new Vector2f(cd.centerX, cd.centerY), cd.width, cd.height); + coll.setEnabled(cd.enabled); + } + } + } + + + /** + * 反序列化部件并返回 name->ModelPart 的映射,供物理系统恢复 userData 使用 + */ + private Map deserializeParts(Model2D model, Map meshMap) { + Map partMap = new HashMap<>(); + + // 1. 创建所有部件对象 + for (PartData partData : parts) { + ModelPart part = partData.toModelPart(meshMap); + partMap.put(part.getName(), part); + } + + // 2. 建立父子关系 + 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); + } + } + } + + // 3. 找到根部件并设置 + ModelPart rootPart = null; + for (PartData partData : parts) { + if (partData.parentName == null || partData.parentName.isEmpty()) { + rootPart = partMap.get(partData.name); + model.setRootPart(rootPart); + break; + } + } + + // 4. 把所有部件加入 model(如果 addPart 是注册用,不会重复) + for (ModelPart part : partMap.values()) { + model.addPart(part); + } + + return partMap; + } + + + private Map deserializeTextures() { Map textureMap = new HashMap<>(); @@ -272,35 +559,6 @@ public class ModelData implements Serializable { 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(); @@ -454,24 +712,25 @@ public class ModelData implements Serializable { 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()); - } - for (AnimationLayerData layer : this.animationLayers) { - copy.animationLayers.add(layer.copy()); - } + // 拷贝核心数据 + for (PartData part : this.parts) copy.parts.add(part.copy()); + for (MeshData mesh : this.meshes) copy.meshes.add(mesh.copy()); + for (TextureData tex : this.textures) copy.textures.add(tex.copy()); + for (ParameterData param : this.parameters) copy.parameters.add(param.copy()); + for (AnimationLayerData layer : this.animationLayers) copy.animationLayers.add(layer.copy()); + for (LightSourceData light : this.lights) copy.lights.add(light.copy()); + + // 拷贝物理系统 + for (ParticleData p : this.physicsParticles) copy.physicsParticles.add(p.copy()); + for (SpringData s : this.physicsSprings) copy.physicsSprings.add(s.copy()); + for (ConstraintData c : this.physicsConstraints) copy.physicsConstraints.add(c.copy()); + for (ColliderData c : this.physicsColliders) copy.physicsColliders.add(c.copy()); + + copy.physicsGravityX = this.physicsGravityX; + copy.physicsGravityY = this.physicsGravityY; + copy.physicsAirResistance = this.physicsAirResistance; + copy.physicsTimeScale = this.physicsTimeScale; + copy.physicsEnabled = this.physicsEnabled; copy.pivotPoint = new Vector2f(this.pivotPoint); copy.unitsPerMeter = this.unitsPerMeter; @@ -534,9 +793,6 @@ public class ModelData implements Serializable { // ==================== 内部数据类 ==================== - /** - * 部件数据 - */ public static class PartData implements Serializable { private static final long serialVersionUID = 1L; @@ -550,6 +806,9 @@ public class ModelData implements Serializable { public List meshNames; public Map userData; + // 新增:保存变形器数据 + public List deformers; + public PartData() { this.position = new Vector2f(); this.rotation = 0.0f; @@ -558,6 +817,7 @@ public class ModelData implements Serializable { this.opacity = 1.0f; this.meshNames = new ArrayList<>(); this.userData = new HashMap<>(); + this.deformers = new ArrayList<>(); } public PartData(ModelPart part) { @@ -574,6 +834,22 @@ public class ModelData implements Serializable { this.meshNames.add(mesh.getName()); } + // 收集变形器(序列化每个变形器为键值表) + for (Deformer d : part.getDeformers()) { + try { + DeformerData dd = new DeformerData(); + dd.type = d.getClass().getName(); + dd.name = d.getName(); + Map map = new HashMap<>(); + d.serialization(map); // 让变形器把自己的状态写入 map + dd.properties = map; + this.deformers.add(dd); + } catch (Exception e) { + // 忽略单个变形器序列化错误,避免整个保存失败 + e.printStackTrace(); + } + } + // 设置父级名称 if (part.getParent() != null) { this.parentName = part.getParent().getName(); @@ -596,6 +872,40 @@ public class ModelData implements Serializable { } } + // 反序列化变形器(仅创建已知类型,其他类型可拓展) + if (deformers != null) { + for (DeformerData dd : deformers) { + try { + String className = dd.type; + + // 通过反射获取类并实例化(必须有 public 构造函数(String name)) + Class clazz = Class.forName(className); + + if (Deformer.class.isAssignableFrom(clazz)) { + Deformer deformer = (Deformer) clazz + .getConstructor(String.class) + .newInstance(dd.name); + + // 反序列化属性 + try { + deformer.deserialize(dd.properties != null ? dd.properties : new HashMap<>()); + } catch (Exception ex) { + ex.printStackTrace(); + } + + part.addDeformer(deformer); + } else { + System.err.println("跳过无效的变形器类型: " + className); + } + + } catch (Exception e) { + System.err.println("反序列化变形器失败: " + dd.type); + e.printStackTrace(); + } + } + } + + return part; } @@ -610,8 +920,31 @@ public class ModelData implements Serializable { copy.opacity = this.opacity; copy.meshNames = new ArrayList<>(this.meshNames); copy.userData = new HashMap<>(this.userData); + + // 深拷贝 deformers 列表 + copy.deformers = new ArrayList<>(); + if (this.deformers != null) { + for (DeformerData d : this.deformers) { + DeformerData cd = new DeformerData(); + cd.type = d.type; + cd.name = d.name; + cd.properties = (d.properties != null) ? new HashMap<>(d.properties) : new HashMap<>(); + copy.deformers.add(cd); + } + } + return copy; } + + /** + * 内部类:序列化变形器数据结构 + */ + public static class DeformerData implements Serializable { + private static final long serialVersionUID = 1L; + public String type; // 例如 "VertexDeformer" + public String name; + public Map properties; // 由 Deformer.serialization 填充 + } } /** @@ -1072,6 +1405,103 @@ public class ModelData implements Serializable { } } + // ---------- 物理数据的序列化类 ---------- + public static class ParticleData implements Serializable { + public String id; + public float x, y; + public float mass; + public float radius; + public boolean movable; + public boolean affectedByGravity; + public boolean affectedByWind; + public String userPartName; + + public ParticleData copy() { + ParticleData copy = new ParticleData(); + copy.id = this.id; + copy.x = this.x; + copy.y = this.y; + copy.mass = this.mass; + copy.radius = this.radius; + copy.movable = this.movable; + copy.affectedByGravity = this.affectedByGravity; + copy.affectedByWind = this.affectedByWind; + copy.userPartName = this.userPartName; + return copy; + } + } + + public static class SpringData implements Serializable { + public String id; + public String aId; + public String bId; + public float restLength; + public float stiffness; + public float damping; + public boolean enabled; + + public SpringData copy() { + SpringData copy = new SpringData(); + copy.id = this.id; + copy.aId = this.aId; + copy.bId = this.bId; + copy.restLength = this.restLength; + copy.stiffness = this.stiffness; + copy.damping = this.damping; + copy.enabled = this.enabled; + return copy; + } + } + + public static class ColliderData implements Serializable { + public String id; + public String type; // "circle" or "rect" + public float centerX, centerY; + public float radius; + public float width, height; + public boolean enabled; + + public ColliderData copy() { + ColliderData copy = new ColliderData(); + copy.id = this.id; + copy.type = this.type; + copy.centerX = this.centerX; + copy.centerY = this.centerY; + copy.radius = this.radius; + copy.width = this.width; + copy.height = this.height; + copy.enabled = this.enabled; + return copy; + } + } + + public static class ConstraintData implements Serializable { + public String type; // "position" or "distance" + public String particleId; + // position target + public float targetX, targetY; + public float strength; + // distance target + public String targetParticleId; + public float maxDistance; + public boolean enabled; + + public ConstraintData copy() { + ConstraintData copy = new ConstraintData(); + copy.type = this.type; + copy.particleId = this.particleId; + copy.targetX = this.targetX; + copy.targetY = this.targetY; + copy.strength = this.strength; + copy.targetParticleId = this.targetParticleId; + copy.maxDistance = this.maxDistance; + copy.enabled = this.enabled; + return copy; + } + } + + + /** * 参数数据 */ 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 index 3dbbc3e..8360059 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java @@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import org.joml.Vector2f; import java.util.List; +import java.util.Map; /** * 旋转变形器 - 围绕中心点旋转顶点 @@ -79,6 +80,23 @@ public class RotationDeformer extends Deformer { this.currentAngle = baseAngle; } + @Override + public void serialization(Map map) { + map.put("baseAngle", String.valueOf(baseAngle)); + map.put("angleRange", String.valueOf(angleRange)); + map.put("currentAngle", String.valueOf(currentAngle)); + } + + @Override + public void deserialize(Map map) { + if (map == null) { + return; + } + baseAngle = Float.parseFloat(map.get("baseAngle")); + angleRange = Float.parseFloat(map.get("angleRange")); + currentAngle = Float.parseFloat(map.get("currentAngle")); + } + // Getter/Setter public float getBaseAngle() { return baseAngle; } public void setBaseAngle(float baseAngle) { this.baseAngle = baseAngle; } 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 index 263de63..c693a6c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java @@ -2,12 +2,15 @@ package com.chuangzhou.vivid2D.render.model.transform; import com.chuangzhou.vivid2D.render.model.util.Deformer; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.SaveVector2f; import org.joml.Vector2f; import java.util.List; +import java.util.Map; /** * 缩放变形器 - 围绕中心点缩放顶点 + * @author tzdwindows 7 */ public class ScaleDeformer extends Deformer { private Vector2f baseScale = new Vector2f(1.0f, 1.0f); @@ -72,6 +75,24 @@ public class ScaleDeformer extends Deformer { this.currentScale.set(baseScale); } + @Override + public void serialization(Map map) { + map.put("baseScale", SaveVector2f.toString(baseScale)); + map.put("scaleRange", SaveVector2f.toString(scaleRange)); + map.put("currentScale", SaveVector2f.toString(currentScale)); + } + + + @Override + public void deserialize(Map map) { + if (map == null) { + return; + } + baseScale = SaveVector2f.fromString(map.get("baseScale")); + scaleRange = SaveVector2f.fromString(map.get("scaleRange")); + currentScale = SaveVector2f.fromString(map.get("currentScale")); + } + // Getter/Setter public Vector2f getBaseScale() { return new Vector2f(baseScale); } public void setBaseScale(Vector2f baseScale) { this.baseScale.set(baseScale); } 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 index 3cc5a05..2cea5b6 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java @@ -29,21 +29,9 @@ public class VertexDeformer extends Deformer { } /** - * 顶点变形数据内部类 - */ - 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; - } - } + * 顶点变形数据内部类 + */ + private record VertexDeformation(float originalX, float originalY, float targetX, float targetY) { } /** * 添加顶点变形目标 @@ -193,6 +181,87 @@ public class VertexDeformer extends Deformer { this.currentValue = 0.0f; } + @Override + public void serialization(Map map) { + // 保存基本属性 + map.put(name + ".enabled", Boolean.toString(this.enabled)); + map.put(name + ".weight", Float.toString(this.weight)); + map.put(name + ".currentValue", Float.toString(this.currentValue)); + + // 序列化顶点变形为紧凑字符串 + // 格式示例(每个条目用分号分隔): + // index|origX,origY|targetX,targetY;index2|... + StringBuilder sb = new StringBuilder(256); + for (int vertexIndex : vertexIndexList) { + VertexDeformation d = vertexDeformations.get(vertexIndex); + if (d == null) continue; + if (sb.length() > 0) sb.append(';'); + sb.append(vertexIndex).append('|') + .append(d.originalX()).append(',').append(d.originalY()).append('|') + .append(d.targetX()).append(',').append(d.targetY()); + } + map.put(name + ".vertexDeformations", sb.toString()); + } + + @Override + public void deserialize(Map map) { + if (map == null) { + return; + } + // 解析基本属性(容错) + try { + String enabledKey = map.get(name + ".enabled"); + if (enabledKey != null) this.enabled = Boolean.parseBoolean(enabledKey); + } catch (Exception ignored) {} + + try { + String weightKey = map.get(name + ".weight"); + if (weightKey != null) this.weight = Float.parseFloat(weightKey); + } catch (Exception ignored) {} + + try { + String curKey = map.get(name + ".currentValue"); + if (curKey != null) this.currentValue = Float.parseFloat(curKey); + } catch (Exception ignored) {} + + // 清空已有数据 + this.vertexDeformations.clear(); + this.vertexIndexList.clear(); + + String data = map.get(name + ".vertexDeformations"); + if (data == null || data.trim().isEmpty()) return; + + // 格式: index|origX,origY|targetX,targetY;... + String[] entries = data.split(";"); + for (String entry : entries) { + if (entry == null) continue; + entry = entry.trim(); + if (entry.isEmpty()) continue; + try { + // split into three parts by '|' + String[] parts = entry.split("\\|"); + if (parts.length != 3) continue; + + int index = Integer.parseInt(parts[0].trim()); + + String[] orig = parts[1].split(","); + String[] targ = parts[2].split(","); + if (orig.length != 2 || targ.length != 2) continue; + + float ox = Float.parseFloat(orig[0].trim()); + float oy = Float.parseFloat(orig[1].trim()); + float tx = Float.parseFloat(targ[0].trim()); + float ty = Float.parseFloat(targ[1].trim()); + + addVertexDeformation(index, ox, oy, tx, ty); + } catch (Exception ex) { + // 忽略单条解析错误,继续解析下一条 + // 可在调试时打印日志: ex.printStackTrace(); + } + } + } + + /** * 设置当前值并立即应用到指定网格 */ 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 index 7506a66..bdf4645 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java @@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import org.joml.Vector2f; import java.util.List; +import java.util.Map; /** * 波浪变形器 - 创建波浪效果的顶点变形 @@ -265,6 +266,27 @@ public class WaveDeformer extends Deformer { this.amplitude = 10.0f; } + @Override + public void serialization(Map map) { + if (map == null) { + return; + } + map.put("amplitude", String.valueOf(amplitude)); + map.put("frequency", String.valueOf(frequency)); + map.put("phase", String.valueOf(phase)); + map.put("waveAngle", String.valueOf(waveAngle)); + map.put("timeMultiplier", String.valueOf(timeMultiplier)); + } + + @Override + public void deserialize(Map map) { + amplitude = Float.parseFloat(map.get("amplitude")); + frequency = Float.parseFloat(map.get("frequency")); + phase = Float.parseFloat(map.get("phase")); + waveAngle = Float.parseFloat(map.get("waveAngle")); + timeMultiplier = Float.parseFloat(map.get("timeMultiplier")); + } + public float getTimeMultiplier() { return timeMultiplier; } public void setTimeMultiplier(float timeMultiplier) { this.timeMultiplier = timeMultiplier; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BufferBuilder.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BufferBuilder.java new file mode 100644 index 0000000..7ee2e73 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BufferBuilder.java @@ -0,0 +1,119 @@ +package com.chuangzhou.vivid2D.render.util; + +import org.lwjgl.BufferUtils; +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; + +/** + * 简化版 BufferBuilder,用于按顶点流构建并一次性绘制几何体。 + * 每个顶点格式: float x, float y, float u, float v (共4个 float) + * + * 用法: + * BufferBuilder bb = new BufferBuilder(); + * bb.begin(GL11.GL_LINE_LOOP, 16); + * bb.vertex(x,y,u,v); + * ... + * bb.end(); // 立即绘制并 cleanup + * + * 设计原则:简单、可靠、方便把临时多顶点数据提交到 GPU。 + */ +public class BufferBuilder { + private static final int COMPONENTS_PER_VERTEX = 4; // x,y,u,v + private float[] array; + private int size; // float 数量 + private int vertexCount; + private int mode; // GL mode + + public BufferBuilder() { + this(256); // 默认容量:256 floats -> 64 顶点 + } + + public BufferBuilder(int initialFloatCapacity) { + this.array = new float[Math.max(16, initialFloatCapacity)]; + this.size = 0; + this.vertexCount = 0; + this.mode = GL11.GL_TRIANGLES; + } + + private void ensureCapacity(int additionalFloats) { + int need = size + additionalFloats; + if (need > array.length) { + int newCap = array.length; + while (newCap < need) newCap <<= 1; + float[] na = new float[newCap]; + System.arraycopy(array, 0, na, 0, size); + array = na; + } + } + + /** + * 开始构建,传入要绘制的 GL 模式(例如 GL11.GL_LINE_LOOP) + * estimatedVertexCount 可传 0 表示不估计 + */ + public void begin(int glMode, int estimatedVertexCount) { + this.mode = glMode; + this.size = 0; + this.vertexCount = 0; + if (estimatedVertexCount > 0) { + ensureCapacity(estimatedVertexCount * COMPONENTS_PER_VERTEX); + } + } + + /** + * 添加顶点:x,y,u,v + */ + public void vertex(float x, float y, float u, float v) { + ensureCapacity(COMPONENTS_PER_VERTEX); + array[size++] = x; + array[size++] = y; + array[size++] = u; + array[size++] = v; + vertexCount++; + } + + /** + * 立即上传并绘制,然后释放临时 GPU 资源(方便、线程不复杂)。 + * 如果你想缓存 VAO/VBO 以便反复绘制,可以扩展本类。 + */ + public void end() { + if (vertexCount == 0) return; + + // upload buffer + FloatBuffer fb = MemoryUtil.memAllocFloat(size); + fb.put(array, 0, size).flip(); + + int vao = GL30.glGenVertexArrays(); + int vbo = GL15.glGenBuffers(); + + GL30.glBindVertexArray(vao); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, fb, GL15.GL_DYNAMIC_DRAW); + + // layout: attrib 0 -> vec2 position (x,y) + GL20.glEnableVertexAttribArray(0); + GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, COMPONENTS_PER_VERTEX * Float.BYTES, 0); + + // layout: attrib 1 -> vec2 texcoord (u,v) + GL20.glEnableVertexAttribArray(1); + GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, COMPONENTS_PER_VERTEX * Float.BYTES, 2 * Float.BYTES); + + // draw + GL11.glDrawArrays(mode, 0, vertexCount); + + // cleanup + GL20.glDisableVertexAttribArray(0); + GL20.glDisableVertexAttribArray(1); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + + GL15.glDeleteBuffers(vbo); + GL30.glDeleteVertexArrays(vao); + + MemoryUtil.memFree(fb); + } +} 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 index 01292f7..2428720 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java @@ -56,6 +56,16 @@ public abstract class Deformer { */ public abstract void reset(); + /** + * 序列化参数 + */ + public abstract void serialization(Map map); + + /** + * 序列化参数 + */ + public abstract void deserialize(Map map); + // ==================== 参数驱动系统 ==================== /** diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java new file mode 100644 index 0000000..d8ccd56 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java @@ -0,0 +1,36 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; +import org.joml.Vector3f; + +public class LightSource { + private Vector2f position; + private Vector3f color; + private float intensity; + private boolean enabled = true; + private boolean isAmbient = false; // 是否为环境光 + + public LightSource(Vector2f pos, Vector3f color, float intensity) { + this.position = pos; + this.color = color; + this.intensity = intensity; + } + + // 环境光构造函数 + public LightSource(Vector3f color, float intensity) { + this.position = new Vector2f(0, 0); + this.color = color; + this.intensity = intensity; + this.isAmbient = true; + } + + public Vector2f getPosition() { return position; } + public Vector3f getColor() { return color; } + public float getIntensity() { return intensity; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + // 判断是否为环境光 + public boolean isAmbient() { return isAmbient; } + public void setAmbient(boolean ambient) { this.isAmbient = ambient; } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSourceData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSourceData.java new file mode 100644 index 0000000..942832d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSourceData.java @@ -0,0 +1,193 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; +import org.joml.Vector3f; +import java.io.Serializable; + +/** + * LightSource 的序列化数据类 + * @author tzdwindows 7 + */ +public class LightSourceData implements Serializable { + private static final long serialVersionUID = 1L; + + // 光源属性 + private String id; + private String position; // 使用字符串格式存储 Vector2f + private String color; // 使用字符串格式存储 Vector3f + private float intensity; + private boolean enabled; + private boolean isAmbient; + + // 默认构造器 + public LightSourceData() { + this.id = "light_" + System.currentTimeMillis(); + this.position = "0,0"; + this.color = "1,1,1"; + this.intensity = 1.0f; + this.enabled = true; + this.isAmbient = false; + } + + // 从 LightSource 对象构造 + public LightSourceData(LightSource light) { + this(); + if (light != null) { + this.id = "light_" + System.currentTimeMillis() + "_" + light.hashCode(); + this.position = SaveVector2f.toString(light.getPosition()); + this.color = vector3fToString(light.getColor()); + this.intensity = light.getIntensity(); + this.enabled = light.isEnabled(); + this.isAmbient = light.isAmbient(); + } + } + + // 转换为 LightSource 对象 + public LightSource toLightSource() { + Vector2f pos = SaveVector2f.fromString(position); + Vector3f col = stringToVector3f(color); + + LightSource light; + if (isAmbient) { + light = new LightSource(col, intensity); + } else { + light = new LightSource(pos, col, intensity); + } + light.setEnabled(enabled); + return light; + } + + // 深拷贝 + public LightSourceData copy() { + LightSourceData copy = new LightSourceData(); + copy.id = this.id; + copy.position = this.position; + copy.color = this.color; + copy.intensity = this.intensity; + copy.enabled = this.enabled; + copy.isAmbient = this.isAmbient; + return copy; + } + + // ==================== 工具方法 ==================== + + private String vector3fToString(Vector3f vec) { + if (vec == null) { + return "1,1,1"; + } + return vec.x + "," + vec.y + "," + vec.z; + } + + private Vector3f stringToVector3f(String str) { + if (str == null || str.trim().isEmpty()) { + return new Vector3f(1, 1, 1); + } + + str = str.trim(); + String[] parts = str.split(","); + if (parts.length != 3) { + return new Vector3f(1, 1, 1); + } + + try { + float x = Float.parseFloat(parts[0].trim()); + float y = Float.parseFloat(parts[1].trim()); + float z = Float.parseFloat(parts[2].trim()); + return new Vector3f(x, y, z); + } catch (NumberFormatException e) { + return new Vector3f(1, 1, 1); + } + } + + // ==================== Getter/Setter ==================== + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPosition() { + return position; + } + + public void setPosition(String position) { + this.position = position; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public float getIntensity() { + return intensity; + } + + public void setIntensity(float intensity) { + this.intensity = intensity; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isAmbient() { + return isAmbient; + } + + public void setAmbient(boolean ambient) { + isAmbient = ambient; + } + + // ==================== 工具方法 ==================== + + /** + * 设置位置为 Vector2f + */ + public void setPosition(Vector2f position) { + this.position = SaveVector2f.toString(position); + } + + /** + * 获取位置为 Vector2f + */ + public Vector2f getPositionAsVector() { + return SaveVector2f.fromString(position); + } + + /** + * 设置颜色为 Vector3f + */ + public void setColor(Vector3f color) { + this.color = vector3fToString(color); + } + + /** + * 获取颜色为 Vector3f + */ + public Vector3f getColorAsVector() { + return stringToVector3f(color); + } + + @Override + public String toString() { + return "LightSourceData{" + + "id='" + id + '\'' + + ", position='" + position + '\'' + + ", color='" + color + '\'' + + ", intensity=" + intensity + + ", enabled=" + enabled + + ", isAmbient=" + isAmbient + + '}'; + } +} \ No newline at end of file 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 index edebb20..f300d64 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java @@ -11,7 +11,13 @@ import java.util.concurrent.ConcurrentHashMap; * 2D物理系统,用于处理模型的物理模拟 * 支持弹簧系统、碰撞检测、重力等物理效果 * - * @author tzdwindows 7 + * 调整点: + * - 使用半隐式(symplectic)Euler 积分,替代之前的纯 Verlet(更稳定且直观) + * - 在约束迭代后同步速度(根据位置变化计算),避免约束把位置推回后速度不一致导致僵化 + * - 在碰撞处理里同时更新位置与速度(冲量响应),并避免在碰撞后覆盖冲量 + * - 约束迭代次数可调,布料等需要多个迭代(默认 3) + * + * @author tzdwindows 7 (modified) */ public class PhysicsSystem { // ==================== 物理参数 ==================== @@ -37,10 +43,14 @@ public class PhysicsSystem { private int updateCount; private float averageUpdateTime; + // ==================== 风力参数 ==================== + private final Vector2f windForce; + private boolean windEnabled; + // ==================== 构造器 ==================== public PhysicsSystem() { - this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力 + this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力(单位可调) this.airResistance = 0.1f; this.timeScale = 1.0f; this.enabled = true; @@ -58,6 +68,9 @@ public class PhysicsSystem { this.updateCount = 0; this.averageUpdateTime = 0.0f; + + this.windForce = new Vector2f(0.0f, 0.0f); // 默认无风 + this.windEnabled = false; } // ==================== 初始化方法 ==================== @@ -213,6 +226,44 @@ public class PhysicsSystem { return colliders.remove(collider); } + // ==================== 风力方法 ==================== + + /** + * 设置风力 + */ + public void setWindForce(float x, float y) { + this.windForce.set(x, y); + this.windEnabled = (x != 0.0f || y != 0.0f); + } + + /** + * 设置风力 + */ + public void setWindForce(Vector2f windForce) { + setWindForce(windForce.x, windForce.y); + } + + /** + * 获取当前风力 + */ + public Vector2f getWindForce() { + return new Vector2f(windForce); + } + + /** + * 启用/禁用风力 + */ + public void setWindEnabled(boolean enabled) { + this.windEnabled = enabled; + } + + /** + * 检查风力是否启用 + */ + public boolean isWindEnabled() { + return windEnabled; + } + // ==================== 更新系统 ==================== /** @@ -243,41 +294,76 @@ public class PhysicsSystem { } /** - * 物理模拟更新 + * 物理模拟更新(主流程) */ private void updatePhysics(float deltaTime) { - // 清除所有力 + // 1) 清除上一帧的力累加器(将在本帧重新累加) for (PhysicsParticle particle : particles.values()) { particle.clearForces(); } - // 应用重力 + // 2) 应用全局力(重力、风) applyGravity(); + applyWind(); - // 应用弹簧力 + // 3) 弹簧加入力 for (PhysicsSpring spring : springs) { spring.applyForce(deltaTime); } - // 更新粒子运动 + // 4) 通过半隐式 Euler 更新速度与位置 for (PhysicsParticle particle : particles.values()) { if (particle.isMovable()) { particle.update(deltaTime); } } - // 应用约束 - for (PhysicsConstraint constraint : constraints) { - constraint.apply(deltaTime); + // 5) 对约束做若干次迭代(布料等需要多次投影) + int constraintIterations = 3; // 可调(1-5)——越多布料越不僵化,但耗时增加 + for (int iter = 0; iter < constraintIterations; iter++) { + for (PhysicsConstraint constraint : constraints) { + if (constraint.isEnabled()) { + constraint.apply(deltaTime); + } + } + + // 每次迭代后处理静态碰撞体的穿透(避免强穿透) + for (PhysicsCollider collider : colliders) { + if (!collider.isEnabled()) continue; + for (PhysicsParticle particle : particles.values()) { + if (!particle.isMovable()) continue; + if (collider.collidesWith(particle)) { + collider.resolveCollision(particle, deltaTime); + } + } + } } - // 处理碰撞 - handleCollisions(deltaTime); + // 6) 在约束迭代后,根据位置变化同步速度,避免约束把位置推回后速度不一致导致僵化 + syncVelocitiesFromPositions(deltaTime); - // 应用空气阻力 + // 7) 粒子间碰撞(位置修正 + 速度冲量) + handleParticleCollisions(deltaTime); + + // 8) 应用空气阻力(现在直接作用于速度) applyAirResistance(deltaTime); } + /** + * 根据当前位置与上一帧位置同步速度:v = (x - x_prev) / dt + * 此操作应在约束投影之后调用,且在不希望覆盖碰撞冲量的情况下不要在碰撞之后再次调用 + */ + private void syncVelocitiesFromPositions(float deltaTime) { + if (deltaTime <= 0.0f) return; + for (PhysicsParticle particle : particles.values()) { + if (!particle.isMovable()) continue; + Vector2f pos = particle.position; // package-private access within outer class + Vector2f prev = particle.previousPosition; + // velocity = (pos - prev) / dt + particle.velocity.set(pos).sub(prev).div(deltaTime); + } + } + /** * 应用重力 */ @@ -291,39 +377,38 @@ public class PhysicsSystem { } /** - * 应用空气阻力 + * 应用风力 */ - private void applyAirResistance(float deltaTime) { + private void applyWind() { + if (!windEnabled || windForce.lengthSquared() == 0.0f) { + return; + } + for (PhysicsParticle particle : particles.values()) { - if (particle.isMovable()) { - Vector2f velocity = particle.getVelocity(); - Vector2f dragForce = new Vector2f(velocity).mul(-airResistance); - particle.addForce(dragForce); + if (particle.isMovable() && particle.isAffectedByWind()) { + particle.addForce(new Vector2f(windForce)); } } } /** - * 处理碰撞 + * 应用空气阻力(对速度的阻尼) */ - private void handleCollisions(float deltaTime) { - // 粒子与碰撞体碰撞 + private void applyAirResistance(float deltaTime) { + if (airResistance <= 0.0f) return; + + // 简单阻尼: v *= 1 / (1 + k*dt) (数值稳定) + float factor = 1.0f / (1.0f + airResistance * deltaTime); + for (PhysicsParticle particle : particles.values()) { if (!particle.isMovable()) continue; - for (PhysicsCollider collider : colliders) { - if (collider.isEnabled() && collider.collidesWith(particle)) { - collider.resolveCollision(particle, deltaTime); - } - } + particle.velocity.mul(factor); } - - // 粒子间碰撞(简单实现) - handleParticleCollisions(deltaTime); } /** - * 处理粒子间碰撞 + * 粒子间碰撞(位置修正 + 速度冲量) */ private void handleParticleCollisions(float deltaTime) { List particleList = new ArrayList<>(particles.values()); @@ -336,34 +421,45 @@ public class PhysicsSystem { 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(); + Vector2f pos1 = p1.getPosition(); + Vector2f pos2 = p2.getPosition(); + Vector2f delta = new Vector2f(pos2).sub(pos1); + float dist = delta.length(); + float minDist = p1.getRadius() + p2.getRadius(); - if (distance < minDistance && distance > 0.001f) { - // 碰撞响应 - Vector2f normal = new Vector2f(delta).div(distance); - float overlap = minDistance - distance; + if (dist < minDist && dist > 1e-6f) { + Vector2f normal = new Vector2f(delta).div(dist); + float penetration = minDist - dist; - // 分离粒子 - Vector2f separation = new Vector2f(normal).mul(overlap * 0.5f); - p1.getPosition().sub(separation); - p2.getPosition().add(separation); + // 按逆质量比例分离位置(静态/不可移动的情况 inverseMass=0) + float invM1 = p1.getInverseMass(); + float invM2 = p2.getInverseMass(); + float invSum = invM1 + invM2; + if (invSum <= 0.0f) continue; - // 简单的速度响应 - Vector2f relativeVelocity = new Vector2f(p2.getVelocity()).sub(p1.getVelocity()); - float velocityAlongNormal = relativeVelocity.dot(normal); + Vector2f correction = new Vector2f(normal).mul(penetration / invSum); + p1.translatePosition(new Vector2f(correction).mul(-invM1)); + p2.translatePosition(new Vector2f(correction).mul(invM2)); - if (velocityAlongNormal > 0) continue; // 已经分离 + // 计算相对速度在法线方向上的分量 + Vector2f v1 = p1.getVelocity(); + Vector2f v2 = p2.getVelocity(); + Vector2f relVel = new Vector2f(v2).sub(v1); + float velAlongNormal = relVel.dot(normal); - float restitution = 0.5f; // 弹性系数 - float impulseMagnitude = -(1 + restitution) * velocityAlongNormal; - impulseMagnitude /= p1.getInverseMass() + p2.getInverseMass(); + // 如果朝向彼此(velAlongNormal < 0)才处理冲量 + if (velAlongNormal < 0.0f) { + float restitution = 0.5f; // 可调 + float j2 = -(1.0f + restitution) * velAlongNormal; + j2 /= invSum; - 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())); + Vector2f impulse = new Vector2f(normal).mul(j2); + // 更新速度 + v1.sub(new Vector2f(impulse).mul(invM1)); + v2.add(new Vector2f(impulse).mul(invM2)); + p1.setVelocity(v1); + p2.setVelocity(v2); + } } } } @@ -533,7 +629,7 @@ public class PhysicsSystem { // ==================== 内部类 ==================== /** - * 物理粒子类 + * 物理粒子类(半隐式 Euler) */ public static class PhysicsParticle { private final String id; @@ -544,9 +640,10 @@ public class PhysicsSystem { private final Vector2f forceAccumulator; private final float mass; private final float inverseMass; - private final float radius; + private float radius; private boolean movable; private boolean affectedByGravity; + private boolean affectedByWind; private Object userData; public PhysicsParticle(String id, Vector2f position, float mass) { @@ -556,36 +653,52 @@ public class PhysicsSystem { 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; + // 允许质量为 0 表示无限质量(不可移动),但为了避免除零,保留逆质量为 0 的逻辑 + if (mass <= 0.0f) { + this.mass = Float.POSITIVE_INFINITY; + this.inverseMass = 0.0f; + } else { + this.mass = mass; + this.inverseMass = 1.0f / this.mass; + } this.radius = 2.0f; // 默认半径 this.movable = true; this.affectedByGravity = true; + this.affectedByWind = true; } public Vector2f getPreviousPosition() { return new Vector2f(previousPosition); } + + /** + * 半隐式 Euler 更新: + * a = F / m + * v += a * dt + * x += v * dt + */ 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); + // v_{n+1} = v_n + a * dt + velocity.add(new Vector2f(acceleration).mul(deltaTime)); - previousPosition.set(temp); + // previousPosition 存放上一帧位置,用于 syncVelocities 时计算 + previousPosition.set(position); - // 更新速度(用于显示和其他计算) - velocity.set(position).sub(previousPosition).div(deltaTime); + // x_{n+1} = x_n + v_{n+1} * dt + position.add(new Vector2f(velocity).mul(deltaTime)); + + // 清除力累加器(下一帧会重新累加) + forceAccumulator.set(0.0f, 0.0f); } public void addForce(Vector2f force) { + // 如果是无限质量(inverseMass == 0),不累加力 + if (inverseMass == 0.0f) return; forceAccumulator.add(force); } @@ -593,23 +706,56 @@ public class PhysicsSystem { forceAccumulator.set(0.0f, 0.0f); } + // ----- 新增/保留的方法,辅助碰撞与约束使用 ----- + + /** + * 直接平移当前位置(用于位置修正) + */ + public void translatePosition(Vector2f delta) { + this.position.add(delta); + } + + /** + * 直接设置 previousPosition(用于在碰撞后根据新速度修正状态) + */ + public void setPreviousPosition(Vector2f prev) { + this.previousPosition.set(prev); + } + + /** + * 将新的速度应用到粒子(用于碰撞后的速度设置) + */ + public void applyNewVelocity(Vector2f newVelocity) { + this.velocity.set(newVelocity); + // 不直接修改 previousPosition,这样 syncVelocities 在下次迭代会根据位置更新速度 + } + // Getter/Setter 方法 public String getId() { return id; } public Vector2f getPosition() { return new Vector2f(position); } - public void setPosition(Vector2f position) { this.position.set(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 void setVelocity(Vector2f velocity) { + this.velocity.set(velocity); + } public Vector2f getAcceleration() { return new Vector2f(acceleration); } - public float getMass() { return mass; } + public float getMass() { + if (Float.isInfinite(mass)) return Float.POSITIVE_INFINITY; + return mass; + } public float getInverseMass() { return inverseMass; } public float getRadius() { return radius; } - public void setRadius(float radius) { /* this.radius = 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 boolean isAffectedByWind() { return affectedByWind; } + public void setAffectedByWind(boolean affectedByWind) { this.affectedByWind = affectedByWind; } } /** @@ -641,26 +787,29 @@ public class PhysicsSystem { Vector2f delta = new Vector2f(particleB.getPosition()).sub(particleA.getPosition()); float currentLength = delta.length(); - if (currentLength < 0.001f) return; // 避免除以零 + if (currentLength < 0.0001f) return; // 避免除以零 + + // 方向单位向量 + Vector2f dir = new Vector2f(delta).div(currentLength); // 胡克定律: F = -k * (currentLength - restLength) float stretch = currentLength - restLength; - Vector2f springForce = new Vector2f(delta).normalize().mul(stiffness * stretch); + Vector2f springForce = new Vector2f(dir).mul(stiffness * stretch); - // 阻尼力: F_damp = -damping * relativeVelocity + // 阻尼力: F_damp = -damping * relativeVelocity projected onto spring direction Vector2f relativeVelocity = new Vector2f(particleB.getVelocity()).sub(particleA.getVelocity()); - float velocityAlongSpring = relativeVelocity.dot(delta) / currentLength; - Vector2f dampingForce = new Vector2f(delta).normalize().mul(damping * velocityAlongSpring); + float velocityAlongSpring = relativeVelocity.dot(dir); + Vector2f dampingForce = new Vector2f(dir).mul(damping * velocityAlongSpring); - // 应用合力 + // 合力作用在两端(方向相反) Vector2f totalForce = new Vector2f(springForce).sub(dampingForce); if (particleA.isMovable()) { - particleA.addForce(totalForce); + particleA.addForce(new Vector2f(totalForce).mul(-1.0f)); } if (particleB.isMovable()) { - particleB.addForce(totalForce.negate()); + particleB.addForce(totalForce); } } @@ -709,6 +858,7 @@ public class PhysicsSystem { Vector2f delta = new Vector2f(targetPosition).sub(currentPos); Vector2f correction = new Vector2f(delta).mul(strength); + // 直接设置位置(后续会同步速度) particle.setPosition(new Vector2f(currentPos).add(correction)); } @@ -796,18 +946,28 @@ public class PhysicsSystem { 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; + float combined = (radius + particle.getRadius()); + float overlap = combined - distance; - if (overlap > 0 && distance > 0.001f) { - // 分离 - Vector2f normal = new Vector2f(toParticle).div(distance); - particle.getPosition().add(new Vector2f(normal).mul(overlap)); + if (overlap > 0.0001f) { + // 如果中心距离几乎为零,选一个任意法线 + Vector2f normal; + if (distance < 0.0001f) { + normal = new Vector2f(0.0f, 1.0f); + } else { + normal = new Vector2f(toParticle).div(distance); + } - // 反弹 - float dot = particle.getVelocity().dot(normal); + // 将粒子推动到边界外(静态碰撞体假定为不可移动 -> 全部位移分配给粒子) + particle.translatePosition(new Vector2f(normal).mul(overlap + 0.001f)); + + // 速度响应(与静态体碰撞) + Vector2f v = particle.getVelocity(); + float dot = v.dot(normal); if (dot < 0) { - Vector2f reflection = new Vector2f(normal).mul(2.0f * dot); - particle.getVelocity().sub(reflection).mul(0.8f); // 能量损失 + float restitution = 0.6f; + Vector2f vPrime = new Vector2f(v).sub(new Vector2f(normal).mul((1 + restitution) * dot)); + particle.setVelocity(vPrime); } } } @@ -822,7 +982,7 @@ public class PhysicsSystem { } /** - * 矩形碰撞体 + * 矩形碰撞体(轴对齐矩形) */ public static class RectangleCollider implements PhysicsCollider { private final String id; @@ -865,27 +1025,35 @@ public class PhysicsSystem { 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 dist = normal.length(); + float overlap = particle.getRadius() - dist; - // 分离粒子 - float overlap = particle.getRadius() - normal.length(); - if (overlap > 0) { - particle.getPosition().add(new Vector2f(normal).mul(overlap)); + if (dist < 0.0001f) { + // 粒子在矩形内部且中心重合,选 Y 方向 + normal.set(0.0f, 1.0f); + dist = 1.0f; + } else { + normal.div(dist); // 单位向量 + } - // 反弹 - 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); // 能量损失 - } + if (overlap > 0.0001f) { + // 将粒子推出矩形 + particle.translatePosition(new Vector2f(normal).mul(overlap + 0.001f)); + + // 速度响应(与静态矩形碰撞) + Vector2f v = particle.getVelocity(); + float dot = v.dot(normal); + if (dot < 0) { + float restitution = 0.6f; + Vector2f vPrime = new Vector2f(v).sub(new Vector2f(normal).mul((1 + restitution) * dot)); + particle.setVelocity(vPrime); } } } @@ -939,4 +1107,4 @@ public class PhysicsSystem { ); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java new file mode 100644 index 0000000..de6e6e8 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java @@ -0,0 +1,70 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; + +/** + * 工具类:用于在 Vector2f 和字符串之间进行转换。 + * 例如: + * - toString(new Vector2f(1.5f, -2.0f)) => "1.5,-2.0" + * - fromString("1.5,-2.0") => new Vector2f(1.5f, -2.0f) + * + * @author tzdwindows 7 + */ +public class SaveVector2f { + + /** + * 将 Vector2f 转换为字符串。 + * 格式: "x,y" + */ + public static String toString(Vector2f vec) { + if (vec == null) { + return "0,0"; + } + return vec.x + "," + vec.y; + } + + /** + * 从字符串解析为 Vector2f。 + * 允许的格式: + * - "x,y" + * - "(x,y)" + * - " x , y " + * 若格式错误则返回 (0,0) + */ + public static Vector2f fromString(String str) { + if (str == null || str.trim().isEmpty()) { + return new Vector2f(); + } + + str = str.trim(); + + // 去掉括号 + if (str.startsWith("(") && str.endsWith(")")) { + str = str.substring(1, str.length() - 1); + } + + String[] parts = str.split(","); + if (parts.length != 2) { + return new Vector2f(); + } + + try { + float x = Float.parseFloat(parts[0].trim()); + float y = Float.parseFloat(parts[1].trim()); + return new Vector2f(x, y); + } catch (NumberFormatException e) { + return new Vector2f(); + } + } + + /** + * 安全解析(带默认值) + */ + public static Vector2f fromString(String str, Vector2f defaultValue) { + Vector2f parsed = fromString(str); + if (parsed.equals(new Vector2f(0, 0)) && (str == null || str.isEmpty())) { + return new Vector2f(defaultValue); + } + return parsed; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java new file mode 100644 index 0000000..81c81ec --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java @@ -0,0 +1,234 @@ +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.LightSource; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.joml.Vector2f; +import org.joml.Vector3f; +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; + +/** + * ModelRenderLightingTest + * 测试使用 Model2D + 光源进行简单光照渲染 + */ +public class ModelRenderLightingTest { + + private static final int WINDOW_WIDTH = 800; + private static final int WINDOW_HEIGHT = 600; + private static final String WINDOW_TITLE = "Vivid2D ModelRender Lighting Test"; + + private long window; + private boolean running = true; + + private Model2D model; + private Random random = new Random(); + + private float animationTime = 0f; + + public static void main(String[] args) { + new ModelRenderLightingTest().run(); + } + + public void run() { + try { + init(); + loop(); + } 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; + }); + + GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); + + GLFW.glfwMakeContextCurrent(window); + GLFW.glfwSwapInterval(1); + GLFW.glfwShowWindow(window); + + GL.createCapabilities(); + + ModelRender.initialize(); + createModelWithLighting(); + + System.out.println("Lighting Test initialized"); + } + + private void createModelWithLighting() { + model = new Model2D("HumanoidLighting"); + + // 创建根部件 body + ModelPart body = model.createPart("body"); + body.setPosition(0, 0); + Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120); + bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); + body.addMesh(bodyMesh); + + // head + ModelPart head = model.createPart("head"); + head.setPosition(0, -90); + Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60); + headMesh.setTexture(createHeadTexture()); + head.addMesh(headMesh); + + // arms + ModelPart leftArm = model.createPart("left_arm"); + leftArm.setPosition(-60, -20); + Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90); + leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); + leftArm.addMesh(leftArmMesh); + + ModelPart rightArm = model.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); + + // legs + ModelPart leftLeg = model.createPart("left_leg"); + leftLeg.setPosition(-20, 90); + Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100); + leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); + leftLeg.addMesh(leftLegMesh); + + ModelPart rightLeg = model.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.addChild(head); + body.addChild(leftArm); + body.addChild(rightArm); + body.addChild(leftLeg); + body.addChild(rightLeg); + + LightSource ambientLight = new LightSource( + new Vector3f(0.5f, 0.5f, 0.5f), // 灰色 + 0.3f + ); + ambientLight.setAmbient(true); + model.addLight(ambientLight); + + // 添加光源 + model.addLight(new LightSource(new Vector2f(-100, -100), new Vector3f(1f, 0f, 0f), 20f)); + model.addLight(new LightSource(new Vector2f(150, 150), new Vector3f(0f, 0f, 1f), 20f)); + } + + 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;i1.0f?0:255; + int r = (int)(240*(1f - dist*0.25f)); + int g = (int)(200*(1f - dist*0.25f)); + int b = (int)(180*(1f - 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; + + 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) { + animationTime += dt; + float armSwing = (float)Math.sin(animationTime*3f)*0.7f; + float legSwing = (float)Math.sin(animationTime*3f + Math.PI)*0.6f; + float headRot = (float)Math.sin(animationTime*1.4f)*0.15f; + + ModelPart leftArm = model.getPart("left_arm"); + ModelPart rightArm = model.getPart("right_arm"); + ModelPart leftLeg = model.getPart("left_leg"); + ModelPart rightLeg = model.getPart("right_leg"); + ModelPart head = model.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); + + model.update(dt); + } + + private void render() { + ModelRender.setClearColor(0.18f,0.18f,0.25f,1.0f); + ModelRender.render(1f/60f, model); + } + + private void cleanup() { + ModelRender.cleanup(); + Texture.cleanupAll(); + if(window!= MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); + GLFW.glfwTerminate(); + GLFW.glfwSetErrorCallback(null).free(); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java index ddfb8c7..a5e9e73 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java @@ -4,7 +4,9 @@ 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.PhysicsSystem; import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.joml.Vector2f; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWErrorCallback; import org.lwjgl.glfw.GLFWVidMode; @@ -105,6 +107,13 @@ public class ModelRenderTest { private void createTestModel() { testModel = new Model2D("Humanoid"); + PhysicsSystem physics = testModel.getPhysics(); + physics.setGravity(new Vector2f(0, -98.0f)); + physics.setAirResistance(0.05f); + physics.setTimeScale(1.0f); + physics.setEnabled(true); + physics.initialize(); + // body 放在屏幕中心 ModelPart body = testModel.createPart("body"); body.setPosition(0, 0); diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java index 57d358d..cd53784 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java @@ -3,12 +3,8 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.AnimationLayer; -import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; -import com.chuangzhou.vivid2D.render.model.util.ModelPose; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.model.util.Texture; +import com.chuangzhou.vivid2D.render.model.transform.WaveDeformer; +import com.chuangzhou.vivid2D.render.model.util.*; import org.joml.Vector2f; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWErrorCallback; @@ -46,7 +42,8 @@ public class ModelTest { testPhysicsSystem(); testComplexTransformations(); testPerformance(); - + Model2D model = createTestModel(); + printModelState(model); } finally { // Cleanup OpenGL cleanupOpenGL(); @@ -55,6 +52,231 @@ public class ModelTest { System.out.println("=== Model2D Extended Save and Load Test Complete ==="); } + public static Model2D createTestModel() { + Model2D model = new Model2D("full_test_model"); + model.setVersion("1.0.0"); + + // ==================== 创建部件层级 ==================== + ModelPart root = model.createPart("root"); + ModelPart body = model.createPart("body"); + ModelPart head = model.createPart("head"); + ModelPart leftArm = model.createPart("left_arm"); + ModelPart rightArm = model.createPart("right_arm"); + + root.addChild(body); + body.addChild(head); + body.addChild(leftArm); + body.addChild(rightArm); + + // ==================== 设置本地变换 ==================== + root.setPosition(0, 0); + root.setRotation(0f); + root.setScale(1f, 1f); + + body.setPosition(0, -50); + body.setRotation(10f); // body稍微旋转 + body.setScale(1.1f, 1.0f); + + head.setPosition(0, -50); + head.setRotation(-5f); + head.setScale(1.0f, 1.0f); + + leftArm.setPosition(-30, -20); + leftArm.setRotation(20f); + leftArm.setScale(1.0f, 0.9f); + + rightArm.setPosition(30, -20); + rightArm.setRotation(-20f); + rightArm.setScale(1.0f, 0.9f); + + // ==================== 添加网格 ==================== + Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80); + Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50); + Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 15, 50); + Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 15, 50); + + model.addMesh(bodyMesh); + model.addMesh(headMesh); + model.addMesh(leftArmMesh); + model.addMesh(rightArmMesh); + + body.addMesh(bodyMesh); + head.addMesh(headMesh); + leftArm.addMesh(leftArmMesh); + rightArm.addMesh(rightArmMesh); + + // ==================== 添加纹理 ==================== + Texture bodyTex = Texture.createSolidColor("body_tex", 64, 64, 0xFFFF0000); + Texture headTex = Texture.createSolidColor("head_tex", 64, 64, 0xFF00FF00); + Texture armTex = Texture.createSolidColor("arm_tex", 32, 64, 0xFF0000FF); + + bodyTex.ensurePixelDataCached(); + headTex.ensurePixelDataCached(); + armTex.ensurePixelDataCached(); + + model.addTexture(bodyTex); + model.addTexture(headTex); + model.addTexture(armTex); + + bodyMesh.setTexture(bodyTex); + headMesh.setTexture(headTex); + leftArmMesh.setTexture(armTex); + rightArmMesh.setTexture(armTex); + + // ==================== 添加动画参数 ==================== + AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0.5f); + AnimationParameter walkParam = model.createParameter("walk", 0, 1, 0); + AnimationParameter waveParam = model.createParameter("wave", 0, 1, 0); + + // ==================== 添加 Deformer ==================== + root.addDeformer(new WaveDeformer("blink")); + root.addDeformer(new WaveDeformer("wave")); + root.addDeformer(new WaveDeformer("blink")); + + // ==================== 设置元数据 ==================== + model.getMetadata().setAuthor("Test Author"); + model.getMetadata().setDescription("This is a full-featured test model with transforms and deformers."); + model.getMetadata().setLicense("MIT"); + model.getMetadata().setFileFormatVersion("1.0.0"); + model.getMetadata().setUnitsPerMeter(100.0f); + model.getMetadata().setProperty("custom_prop1", "value1"); + + // ==================== 添加物理 ==================== + PhysicsSystem physics = model.getPhysics(); + if (physics != null) { + physics.initialize(); + PhysicsSystem.PhysicsParticle p1 = physics.addParticle("p1", new Vector2f(0, 0), 1f); + PhysicsSystem.PhysicsParticle p2 = physics.addParticle("p2", new Vector2f(10, 0), 1f); + physics.addSpring("spring1", p1, p2, 10f, 0.5f, 0.1f); + } + + return model; + } + + + public static void testModelSaveLoadIntegrity(Model2D model, String filePath) { + System.out.println("\n--- Test: Model Save and Load Integrity ---"); + try { + // 保存模型 + model.saveToFile(filePath); + + // 加载模型 + Model2D loaded = Model2D.loadFromFile(filePath); + + boolean integrityOk = true; + + // ==================== 基本属性 ==================== + if (!model.getName().equals(loaded.getName())) { + System.out.println("Name mismatch!"); + integrityOk = false; + } + if (!model.getVersion().equals(loaded.getVersion())) { + System.out.println("Version mismatch!"); + integrityOk = false; + } + + // ==================== 部件 ==================== + if (model.getParts().size() != loaded.getParts().size()) { + System.out.println("Parts count mismatch!"); + integrityOk = false; + } else { + for (int i = 0; i < model.getParts().size(); i++) { + ModelPart orig = model.getParts().get(i); + ModelPart loadPart = loaded.getParts().get(i); + if (!orig.getName().equals(loadPart.getName())) { + System.out.println("Part name mismatch: " + orig.getName()); + integrityOk = false; + } + // 检查变换 + if (!orig.getPosition().equals(loadPart.getPosition()) || + orig.getRotation() != loadPart.getRotation() || + !orig.getScale().equals(loadPart.getScale())) { + System.out.println("Part transform mismatch: " + orig.getName()); + integrityOk = false; + } + // 检查Deformer + if (orig.getDeformers().size() != loadPart.getDeformers().size()) { + System.out.println("Deformer count mismatch on part: " + orig.getName()); + integrityOk = false; + } + } + } + + // ==================== 网格 ==================== + if (model.getMeshes().size() != loaded.getMeshes().size()) { + System.out.println("Meshes count mismatch!"); + integrityOk = false; + } + + // ==================== 纹理 ==================== + if (model.getTextures().size() != loaded.getTextures().size()) { + System.out.println("Textures count mismatch!"); + integrityOk = false; + } + + // ==================== 参数 ==================== + if (model.getParameters().size() != loaded.getParameters().size()) { + System.out.println("Parameters count mismatch!"); + integrityOk = false; + } else { + for (String key : model.getParameters().keySet()) { + AnimationParameter origParam = model.getParameters().get(key); + AnimationParameter loadParam = loaded.getParameters().get(key); + if (origParam.getValue() != loadParam.getValue()) { + System.out.println("Parameter value mismatch: " + key); + integrityOk = false; + } + } + } + + // ==================== 物理 ==================== + PhysicsSystem origPhysics = model.getPhysics(); + PhysicsSystem loadPhysics = loaded.getPhysics(); + if ((origPhysics != null && loadPhysics == null) || (origPhysics == null && loadPhysics != null)) { + System.out.println("Physics system missing after load!"); + integrityOk = false; + } else if (origPhysics != null) { + if (origPhysics.getParticles().size() != loadPhysics.getParticles().size()) { + System.out.println("Physics particle count mismatch!"); + integrityOk = false; + } + if (origPhysics.getSprings().size() != loadPhysics.getSprings().size()) { + System.out.println("Physics spring count mismatch!"); + integrityOk = false; + } + } + + System.out.println("Integrity test " + (integrityOk ? "PASSED" : "FAILED")); + + } catch (Exception e) { + System.err.println("Error in testModelSaveLoadIntegrity: " + e.getMessage()); + e.printStackTrace(); + } + } + + + private static void printModelState(Model2D model) { + System.out.println(" - Name: " + model.getName()); + System.out.println(" - Version: " + model.getVersion()); + System.out.println(" - Parts: " + model.getParts().size()); + for (ModelPart part : model.getParts()) { + printPartHierarchy(part, 1); + } + System.out.println(" - Parameters:"); + for (AnimationParameter param : model.getParameters().values()) { + System.out.println(" * " + param.getId() + " = " + param.getValue()); + } + System.out.println(" - Textures:"); + model.getTextures().forEach((k, tex) -> { + System.out.println(" * " + tex.getName() + " (" + tex.getWidth() + "x" + tex.getHeight() + ")"); + }); + System.out.println(" - User Properties:"); + model.getMetadata().getUserProperties().forEach((k, v) -> + System.out.println(" * " + k + ": " + v) + ); + } + + /** * Initialize OpenGL context for texture testing */ @@ -586,6 +808,8 @@ public class ModelTest { } } + + /** * Utility method to print part hierarchy */ diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java new file mode 100644 index 0000000..781c77b --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java @@ -0,0 +1,682 @@ +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 com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; +import org.joml.Vector2f; +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.ArrayList; +import java.util.List; + +/** + * 物理系统使用实例 - 演示弹簧、重力和碰撞效果 + * @author tzdwindows 7 + */ +public class ModelTest2 { + + private static final int WINDOW_WIDTH = 1000; + private static final int WINDOW_HEIGHT = 700; + private static final String WINDOW_TITLE = "Physics System Demo"; + + private long window; + private boolean running = true; + private Model2D physicsModel; + private PhysicsSystem physics; + + // 测试用例控制 + private int testCase = 5; + private boolean gravityEnabled = true; + private boolean springsEnabled = true; + + // 存储部件引用,用于清理 + private List currentParts = new ArrayList<>(); + + // 所有测试基点(初始 xy = 0,0) + private final Vector2f initialOrigin = new Vector2f(0, 0); + + public static void main(String[] args) { + new ModelTest2().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) { + testCase = (testCase + 1) % 6; // 支持 0..5 共 6 个用例 + setupTestCase(); + } + if (key == GLFW.GLFW_KEY_G && action == GLFW.GLFW_RELEASE) { + gravityEnabled = !gravityEnabled; + physics.setGravity(gravityEnabled ? new Vector2f(0, -98.0f) : new Vector2f(0, 0)); + System.out.println("Gravity " + (gravityEnabled ? "ENABLED" : "DISABLED")); + } + if (key == GLFW.GLFW_KEY_S && action == GLFW.GLFW_RELEASE) { + springsEnabled = !springsEnabled; + toggleSprings(springsEnabled); + System.out.println("Springs " + (springsEnabled ? "ENABLED" : "DISABLED")); + } + if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) { + resetPhysics(); + } + if (key == GLFW.GLFW_KEY_C && action == GLFW.GLFW_RELEASE) { + applyRandomForce(); + } + }); + + GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); + + GLFW.glfwMakeContextCurrent(window); + GLFW.glfwSwapInterval(1); + GLFW.glfwShowWindow(window); + + GL.createCapabilities(); + + createPhysicsModel(); + ModelRender.initialize(); + + System.out.println("Physics System Demo Initialized"); + printControls(); + } + + private void printControls() { + System.out.println("\n=== Controls ==="); + System.out.println("ESC - Exit"); + System.out.println("SPACE - Change test case"); + System.out.println("G - Toggle gravity"); + System.out.println("S - Toggle springs"); + System.out.println("R - Reset physics"); + System.out.println("C - Apply random force"); + System.out.println("================\n"); + } + + /** + * 创建物理测试模型 + */ + private void createPhysicsModel() { + physicsModel = new Model2D("PhysicsDemo"); + physics = physicsModel.getPhysics(); + + // 配置物理系统 + physics.setGravity(new Vector2f(0, -98.0f)); + physics.setAirResistance(0.05f); + physics.setTimeScale(1.0f); + physics.setEnabled(true); + physics.initialize(); + + setupTestCase(); + } + + /** + * 设置不同的测试用例 + */ + private void setupTestCase() { + // 清理之前的设置 + clearCurrentParts(); + physics.reset(); + + switch (testCase) { + case 0: + setupSpringChain(); + break; + case 1: + setupClothSimulation(); + break; + case 2: + setupPendulum(); + break; + case 3: + setupSoftBody(); + break; + case 4: + setupWindTest(); + break; + case 5: + setupFreeFallTest(); + break; + } + + System.out.println("Test Case " + testCase + ": " + getTestCaseName(testCase)); + } + + /** + * 清理当前部件 + */ + private void clearCurrentParts() { + // 由于无法直接清除模型的parts列表,我们创建一个新模型 + physicsModel = new Model2D("PhysicsDemo"); + currentParts.clear(); + + // 重新配置物理系统 + physics = physicsModel.getPhysics(); + physics.setGravity(new Vector2f(0, -98.0f)); + physics.setAirResistance(0.05f); + physics.setTimeScale(1.0f); + physics.setEnabled(true); + physics.initialize(); + } + + /** + * 测试用例1: 弹簧链 + */ + private void setupSpringChain() { + // 创建5个连接的粒子,基于 initialOrigin(因此首个粒子是 (0,0)) + for (int i = 0; i < 5; i++) { + Vector2f position = new Vector2f(initialOrigin.x + i * 60, initialOrigin.y + i * 20); + PhysicsSystem.PhysicsParticle particle = physics.addParticle("particle_" + i, position, 1.0f); + + // 第一个粒子固定(位于 initialOrigin) + if (i == 0) { + particle.setMovable(false); + } + + // 创建对应的模型部件 + ModelPart part = physicsModel.createPart("part_" + i); + part.setPosition(position.x, position.y); + currentParts.add(part); + + // 创建圆形网格 + Mesh2D circleMesh = createCircleMesh("circle_" + i, 20, getColorForIndex(i)); + part.addMesh(circleMesh); + physicsModel.addMesh(circleMesh); + + // 将部件设置为粒子的用户数据,用于同步位置 + particle.setUserData(part); + + // 添加弹簧连接(除了第一个粒子) + if (i > 0) { + PhysicsSystem.PhysicsParticle prevParticle = physics.getParticle("particle_" + (i - 1)); + physics.addSpring("spring_" + (i - 1), prevParticle, particle, 60.0f, 0.3f, 0.1f); + } + } + } + + /** + * 测试用例2: 布料模拟 + */ + private void setupClothSimulation() { + int rows = 4; + int cols = 6; + float spacing = 40.0f; + + // 创建布料网格,基于 initialOrigin + for (int y = 0; y < rows; y++) { + for (int x = 0; x < cols; x++) { + int index = y * cols + x; + Vector2f position = new Vector2f(initialOrigin.x + x * spacing, initialOrigin.y + y * spacing); + + PhysicsSystem.PhysicsParticle particle = physics.addParticle("cloth_" + index, position, 0.8f); + + // 固定顶部行的粒子(y==0) + if (y == 0) { + particle.setMovable(false); + } + + ModelPart part = physicsModel.createPart("cloth_part_" + index); + part.setPosition(position.x, position.y); + currentParts.add(part); + + Mesh2D squareMesh = createSquareMesh("square_" + index, 15, getColorForIndex(index)); + part.addMesh(squareMesh); + physicsModel.addMesh(squareMesh); + + // 将部件设置为粒子的用户数据 + particle.setUserData(part); + + // 添加水平弹簧连接 + if (x > 0) { + PhysicsSystem.PhysicsParticle leftParticle = physics.getParticle("cloth_" + (index - 1)); + physics.addSpring("h_spring_" + index, leftParticle, particle, spacing, 0.4f, 0.05f); + } + + // 添加垂直弹簧连接 + if (y > 0) { + PhysicsSystem.PhysicsParticle topParticle = physics.getParticle("cloth_" + (index - cols)); + physics.addSpring("v_spring_" + index, topParticle, particle, spacing, 0.4f, 0.05f); + } + } + } + } + + /** + * 测试用例3: 钟摆系统 + */ + private void setupPendulum() { + // 创建钟摆锚点(位于 initialOrigin) + Vector2f anchorPos = new Vector2f(initialOrigin); + PhysicsSystem.PhysicsParticle anchor = physics.addParticle("anchor", anchorPos, 0.0f); + anchor.setMovable(false); // 固定锚点 + + // 创建钟摆摆锤(相对锚点水平分布) + for (int i = 0; i < 3; i++) { + Vector2f pendulumPos = new Vector2f(initialOrigin.x + (i - 1) * 120, initialOrigin.y + 200); + PhysicsSystem.PhysicsParticle particle = physics.addParticle("pendulum_" + i, pendulumPos, 2.0f); + + // 检查粒子是否成功创建 + if (particle == null) { + System.err.println("Failed to create pendulum particle: pendulum_" + i); + continue; + } + + ModelPart part = physicsModel.createPart("pendulum_part_" + i); + part.setPosition(pendulumPos.x, pendulumPos.y); + currentParts.add(part); + + Mesh2D ballMesh = createCircleMesh("ball_" + i, 25, getColorForIndex(i)); + part.addMesh(ballMesh); + physicsModel.addMesh(ballMesh); + + // 将部件设置为粒子的用户数据 + particle.setUserData(part); + + // 连接到锚点 - 确保anchor和particle都不为null + if (anchor != null && particle != null) { + float length = 200 + i * 50; + physics.addSpring("pendulum_spring_" + i, anchor, particle, length, 0.1f, 0.02f); + } + } + } + + /** + * 测试用例4: 软体模拟 + */ + private void setupSoftBody() { + // 创建软体圆形,中心在 initialOrigin + int points = 8; + float radius = 60.0f; + Vector2f center = new Vector2f(initialOrigin); + + // 第一步:先创建所有粒子 + List particlesList = new ArrayList<>(); + for (int i = 0; i < points; i++) { + float angle = (float) (i * 2 * Math.PI / points); + Vector2f position = new Vector2f( + center.x + radius * (float) Math.cos(angle), + center.y + radius * (float) Math.sin(angle) + ); + + PhysicsSystem.PhysicsParticle particle = physics.addParticle("soft_" + i, position, 0.5f); + particlesList.add(particle); + + ModelPart part = physicsModel.createPart("soft_part_" + i); + part.setPosition(position.x, position.y); + currentParts.add(part); + + Mesh2D pointMesh = createCircleMesh("point_" + i, 12, 0xFF00FFFF); + part.addMesh(pointMesh); + physicsModel.addMesh(pointMesh); + + // 将部件设置为粒子的用户数据 + particle.setUserData(part); + } + + // 第二步:再创建所有弹簧连接 + for (int i = 0; i < points; i++) { + PhysicsSystem.PhysicsParticle particle = particlesList.get(i); + + // 连接到相邻点 + int next = (i + 1) % points; + PhysicsSystem.PhysicsParticle nextParticle = particlesList.get(next); + physics.addSpring("soft_spring_" + i, particle, nextParticle, + radius * 2 * (float) Math.sin(Math.PI / points), 0.5f, 0.1f); + + // 连接到对面的点(增加稳定性) + if (i < points / 2) { + int opposite = (i + points / 2) % points; + PhysicsSystem.PhysicsParticle oppositeParticle = particlesList.get(opposite); + physics.addSpring("cross_spring_" + i, particle, oppositeParticle, + radius * 2, 0.2f, 0.05f); + } + } + } + + /** + * 测试用例5: 自由落体测试 + */ + private void setupFreeFallTest() { + // 创建地面(位于 initialOrigin) + Vector2f groundPos = new Vector2f(initialOrigin); + PhysicsSystem.PhysicsParticle ground = physics.addParticle("ground", groundPos, 0.0f); + ground.setMovable(false); + + // 创建多个不同质量的物体从不同高度掉落(相对于 initialOrigin) + for (int i = 0; i < 5; i++) { + Vector2f position = new Vector2f(initialOrigin.x + 300 + i * 100, initialOrigin.y + 600 - i * 50); + float mass = 1.0f + i * 0.5f; // 不同质量 + + PhysicsSystem.PhysicsParticle particle = physics.addParticle("fall_" + i, position, mass); + + ModelPart part = physicsModel.createPart("fall_part_" + i); + part.setPosition(position.x, position.y); + currentParts.add(part); + + Mesh2D ballMesh = createCircleMesh("fall_ball_" + i, 15 + i * 3, getColorForIndex(i)); + part.addMesh(ballMesh); + physicsModel.addMesh(ballMesh); + + particle.setUserData(part); + } + + // 添加地面碰撞体(基于 initialOrigin) + physics.addRectangleCollider("ground_collider", groundPos, 800, 20); + } + + /** + * 测试用例6: 风力测试 + */ + private void setupWindTest() { + // 创建布料用于测试风力,基于 initialOrigin + int rows = 6; + int cols = 8; + float spacing = 35.0f; + + for (int y = 0; y < rows; y++) { + for (int x = 0; x < cols; x++) { + int index = y * cols + x; + // 布料放在 initialOrigin.x + ..., initialOrigin.y - y*spacing + 500 以方便显示 + Vector2f position = new Vector2f(initialOrigin.x + x * spacing, initialOrigin.y - y * spacing + 500); + + PhysicsSystem.PhysicsParticle particle = physics.addParticle("wind_cloth_" + index, position, 0.6f); + + // 固定顶部行的粒子(y==0) + if (y == 0) { + particle.setMovable(false); + } + + ModelPart part = physicsModel.createPart("wind_part_" + index); + part.setPosition(position.x, position.y); + currentParts.add(part); + + Mesh2D squareMesh = createSquareMesh("wind_square_" + index, 12, getColorForIndex(index)); + part.addMesh(squareMesh); + physicsModel.addMesh(squareMesh); + + particle.setUserData(part); + + // 添加水平弹簧连接 + if (x > 0) { + PhysicsSystem.PhysicsParticle leftParticle = physics.getParticle("wind_cloth_" + (index - 1)); + physics.addSpring("wind_h_spring_" + index, leftParticle, particle, spacing, 0.3f, 0.05f); + } + + // 添加垂直弹簧连接 + if (y > 0) { + PhysicsSystem.PhysicsParticle topParticle = physics.getParticle("wind_cloth_" + (index - cols)); + physics.addSpring("wind_v_spring_" + index, topParticle, particle, spacing, 0.3f, 0.05f); + } + } + } + } + + /** + * 应用风力效果 + */ + private void applyWindEffect() { + // 随机风力方向 + float windStrength = 50.0f; + float windDirection = (float) (Math.random() * 2 * Math.PI); // 随机方向 + + Vector2f windForce = new Vector2f( + (float) Math.cos(windDirection) * windStrength, + (float) Math.sin(windDirection) * windStrength + ); + + // 对所有可移动粒子应用风力 + for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { + if (particle.isMovable()) { + // 风力随粒子高度变化(模拟真实风) + float heightFactor = particle.getPosition().y / 500.0f; + Vector2f adjustedWind = new Vector2f(windForce).mul(heightFactor); + particle.addForce(adjustedWind); + } + } + + System.out.println("Wind applied: " + windForce); + } + + /** + * 应用持续风力(周期性) + */ + private void applyContinuousWind(float deltaTime) { + // 模拟周期性风力 + float time = System.nanoTime() * 0.000000001f; + float windStrength = 30.0f + (float) Math.sin(time * 2) * 20.0f; // 周期性变化 + + Vector2f windForce = new Vector2f(windStrength, 0); // 主要水平方向 + + for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { + if (particle.isMovable()) { + particle.addForce(new Vector2f(windForce)); + } + } + } + + /** + * 创建圆形网格 - 修正版本 + */ + private Mesh2D createCircleMesh(String name, float radius, int color) { + int segments = 16; + 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; + vertices[1] = 0; + uvs[0] = 0.5f; + uvs[1] = 0.5f; + + // 圆周点 (索引1到segments) + for (int i = 0; i < segments; i++) { + float angle = (float) (i * 2 * Math.PI / segments); + int vertexIndex = (i + 1) * 2; + vertices[vertexIndex] = radius * (float) Math.cos(angle); + vertices[vertexIndex + 1] = radius * (float) Math.sin(angle); + uvs[vertexIndex] = (float) Math.cos(angle) * 0.5f + 0.5f; + uvs[vertexIndex + 1] = (float) Math.sin(angle) * 0.5f + 0.5f; + } + + // 三角形索引 - 每个三角形连接中心点和两个相邻的圆周点 + for (int i = 0; i < segments; i++) { + int triangleIndex = i * 3; + indices[triangleIndex] = 0; // 中心点 + indices[triangleIndex + 1] = i + 1; // 当前圆周点 + indices[triangleIndex + 2] = (i + 1) % segments + 1; // 下一个圆周点 + } + + Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); + mesh.setTexture(createSolidColorTexture(name + "_tex", color)); + return mesh; + } + + /** + * 创建方形网格 + */ + private Mesh2D createSquareMesh(String name, float size, int color) { + float halfSize = size / 2; + float[] vertices = { + -halfSize, -halfSize, + halfSize, -halfSize, + halfSize, halfSize, + -halfSize, halfSize + }; + float[] uvs = { + 0, 0, + 1, 0, + 1, 1, + 0, 1 + }; + int[] indices = {0, 1, 2, 0, 2, 3}; + + Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); + mesh.setTexture(createSolidColorTexture(name + "_tex", color)); + return mesh; + } + + /** + * 创建纯色纹理 + */ + private Texture createSolidColorTexture(String name, int color) { + int width = 64, height = 64; + ByteBuffer buf = MemoryUtil.memAlloc(width * height * 4); + + byte r = (byte) ((color >> 16) & 0xFF); + byte g = (byte) ((color >> 8) & 0xFF); + byte b = (byte) (color & 0xFF); + + for (int i = 0; i < width * height; i++) { + buf.put(r).put(g).put(b).put((byte) 255); + } + + buf.flip(); + Texture texture = new Texture(name, width, height, Texture.TextureFormat.RGBA, buf); + MemoryUtil.memFree(buf); + return texture; + } + + /** + * 根据索引获取不同颜色 + */ + private int getColorForIndex(int index) { + int[] colors = { + 0xFF00FF00, // 绿色 + 0xFFFF0000, // 红色 + 0xFF0000FF, // 蓝色 + 0xFFFFFF00, // 黄色 + 0xFFFF00FF, // 紫色 + 0xFF00FFFF // 青色 + }; + return colors[index % colors.length]; + } + + /** + * 获取测试用例名称 + */ + private String getTestCaseName(int testCase) { + switch (testCase) { + case 0: return "Spring Chain"; + case 1: return "Cloth Simulation"; + case 2: return "Pendulum System"; + case 3: return "Soft Body"; + case 4: return "Wind Test"; + case 5: return "Free Fall Test"; + default: return "Unknown"; + } + } + + /** + * 切换弹簧状态 + */ + private void toggleSprings(boolean enabled) { + for (PhysicsSystem.PhysicsSpring spring : physics.getSprings()) { + spring.setEnabled(enabled); + } + } + + /** + * 重置物理系统 + */ + private void resetPhysics() { + setupTestCase(); + System.out.println("Physics reset"); + } + + /** + * 施加随机力 + */ + private void applyRandomForce() { + for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { + if (particle.isMovable()) { + float forceX = (float) (Math.random() - 0.5) * 200f; + float forceY = (float) (Math.random() - 0.5) * 200f; + particle.addForce(new Vector2f(forceX, forceY)); + } + } + System.out.println("Random forces applied"); + } + + private void loop() { + long last = System.nanoTime(); + double nsPerUpdate = 1_000_000_000.0 / 60.0; + double accumulator = 0.0; + + 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(last); + + GLFW.glfwSwapBuffers(window); + GLFW.glfwPollEvents(); + } + } + + private void update(float dt) { + // 更新物理系统 - 会自动同步到模型部件 + physicsModel.update(dt); + } + + private void render(long last) { + ModelRender.setClearColor(0.1f, 0.1f, 0.15f, 1.0f); + ModelRender.render(last, physicsModel); + } + + private void cleanup() { + System.out.println("Cleaning up physics demo resources..."); + ModelRender.cleanup(); + Texture.cleanupAll(); + if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); + GLFW.glfwTerminate(); + GLFW.glfwSetErrorCallback(null).free(); + System.out.println("Physics demo finished"); + } +} diff --git a/vivid2DApi.md b/vivid2DApi.md new file mode 100644 index 0000000..6b7e360 --- /dev/null +++ b/vivid2DApi.md @@ -0,0 +1,265 @@ +## 🎯 vivid2D 核心操作方法 + +### 1. 模型创建与基础设置 + +```java +// 创建新模型 +Model2D model = new Model2D("character_name"); +model.setVersion("1.0.0"); + +// 设置元数据 +ModelMetadata metadata = model.getMetadata(); +metadata.setAuthor("Your Name"); +metadata.setDescription("Character model"); +``` + +### 2. 部件层级管理 + +```java +// 创建部件 +ModelPart body = model.createPart("body"); +ModelPart head = model.createPart("head"); +ModelPart leftArm = model.createPart("left_arm"); +ModelPart rightArm = model.createPart("right_arm"); + +// 建立层级关系 +body.addChild(head); +body.addChild(leftArm); +body.addChild(rightArm); + +// 设置部件变换 +body.setPosition(0, 0); +head.setPosition(0, -50); // 头部相对身体的位置 +leftArm.setPosition(-30, 0); +rightArm.setPosition(30, 0); + +// 设置旋转和缩放 +head.setRotation(15.0f); // 角度制 +body.setScale(1.2f, 1.0f); +``` + +### 3. 网格系统操作 + +```java +// 创建基本形状网格 +Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80); +Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50); +Mesh2D armMesh = Mesh2D.createQuad("arm_mesh", 20, 60); + +// 添加网格到模型和部件 +model.addMesh(bodyMesh); +model.addMesh(headMesh); +body.addMesh(bodyMesh); +head.addMesh(headMesh); +leftArm.addMesh(armMesh); +rightArm.addMesh(armMesh); + +// 自定义网格数据 +float[] vertices = { /* 顶点数据 */ }; +float[] uvs = { /* UV坐标 */ }; +int[] indices = { /* 索引数据 */ }; +Mesh2D customMesh = model.createMesh("custom_mesh", vertices, uvs, indices); +``` + +### 4. 纹理管理系统 + +```java +// 创建纹理 +Texture bodyTexture = Texture.createSolidColor("body_tex", 64, 64, 0xFFFF0000); // 红色 +Texture headTexture = Texture.createSolidColor("head_tex", 64, 64, 0xFF00FF00); // 绿色 + +// 创建棋盘格纹理 +Texture checkerTexture = Texture.createCheckerboard( + "checker_tex", 128, 128, 16, 0xFFFFFFFF, 0xFF0000FF +); + +// 关键:确保纹理数据缓存(序列化必需) +bodyTexture.ensurePixelDataCached(); +headTexture.ensurePixelDataCached(); + +// 添加纹理到模型 +model.addTexture(bodyTexture); +model.addTexture(headTexture); +model.addTexture(checkerTexture); + +// 为网格分配纹理 +bodyMesh.setTexture(bodyTexture); +headMesh.setTexture(headTexture); +``` + +### 5. 动画参数驱动系统 + +```java +// 创建动画参数 +AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0); +AnimationParameter blinkParam = model.createParameter("blink", 0, 1, 0); +AnimationParameter walkParam = model.createParameter("walk_cycle", 0, 1, 0); + +// 设置参数值 +model.setParameterValue("smile", 0.8f); +model.setParameterValue("blink", 1.0f); + +// 获取参数值 +float currentSmile = model.getParameterValue("smile"); + +// 动画循环示例 +for (int frame = 0; frame < 60; frame++) { + float walkValue = (float) Math.sin(frame * 0.1f) * 0.5f + 0.5f; + model.setParameterValue("walk_cycle", walkValue); + model.update(0.016f); // 60fps +} +``` + +### 6. 动画层系统 + +```java +// 创建动画层 +AnimationLayer baseLayer = model.createAnimationLayer("base_animation"); +AnimationLayer facialLayer = model.createAnimationLayer("facial_animation"); + +// 设置动画层属性 +baseLayer.setWeight(1.0f); +facialLayer.setWeight(0.8f); +``` + +### 7. 物理系统集成 + +```java +// 获取物理系统 +PhysicsSystem physics = model.getPhysics(); + +// 配置物理环境 +physics.setGravity(new Vector2f(0, -9.8f)); +physics.setAirResistance(0.1f); +physics.setTimeScale(1.0f); +physics.setEnabled(true); + +// 初始化物理系统 +physics.initialize(); + +// 添加物理粒子 +PhysicsSystem.PhysicsParticle particle1 = physics.addParticle( + "particle1", new Vector2f(0, 0), 1.0f +); +PhysicsSystem.PhysicsParticle particle2 = physics.addParticle( + "particle2", new Vector2f(10, 0), 1.0f +); + +// 添加弹簧连接 +physics.addSpring("spring1", particle1, particle2, 15.0f, 0.5f, 0.1f); +``` + +### 8. 模型更新与状态管理 + +```java +// 手动标记需要更新 +model.markNeedsUpdate(); + +// 更新模型状态(通常在游戏循环中调用) +model.update(deltaTime); + +// 控制可见性 +model.setVisible(true); +boolean isVisible = model.isVisible(); + +// 获取当前姿势 +ModelPose currentPose = model.getCurrentPose(); + +// 获取包围盒 +BoundingBox bounds = model.getBounds(); +if (bounds != null) { + float width = bounds.getWidth(); + float height = bounds.getHeight(); +} +``` + +### 9. 序列化与文件操作 + +```java +// 保存到普通文件 +model.saveToFile("character.model"); + +// 保存到压缩文件 +model.saveToCompressedFile("character.model.gz"); + +// 从文件加载 +Model2D loadedModel = Model2D.loadFromFile("character.model"); + +// 从压缩文件加载 +Model2D compressedModel = Model2D.loadFromCompressedFile("character.model.gz"); +``` + +### 10. 高级操作技巧 + +```java +// 遍历部件层级 +private void traverseHierarchy(ModelPart part, int depth) { + String indent = " ".repeat(depth); + System.out.println(indent + part.getName()); + + for (ModelPart child : part.getChildren()) { + traverseHierarchy(child, depth + 1); + } +} + +// 查找特定部件 +ModelPart findPartRecursive(ModelPart part, String name) { + if (part.getName().equals(name)) { + return part; + } + for (ModelPart child : part.getChildren()) { + ModelPart found = findPartRecursive(child, name); + if (found != null) { + return found; + } + } + return null; +} + +// 批量设置参数 +public void setMultipleParameters(Model2D model, Map paramValues) { + for (Map.Entry entry : paramValues.entrySet()) { + model.setParameterValue(entry.getKey(), entry.getValue()); + } + model.markNeedsUpdate(); +} +``` + +### 11. 性能优化建议 + +```java +// 批量更新参数后再调用更新 +public void efficientUpdate(Model2D model, float deltaTime) { + if (!model.needsUpdate && !model.getPhysics().hasActivePhysics()) { + return; + } + model.update(deltaTime); +} + +// 重用模型实例 +public class ModelManager { + private Map modelCache = new HashMap<>(); + + public Model2D getModel(String filePath) { + return modelCache.computeIfAbsent(filePath, + path -> Model2D.loadFromFile(path)); + } +} +``` + +### 12. 错误处理最佳实践 + +```java +try { + // 模型操作 + Model2D model = Model2D.loadFromFile("character.model"); + model.setParameterValue("smile", 0.5f); + model.update(0.016f); + +} catch (Exception e) { + System.err.println("模型操作失败: " + e.getMessage()); + e.printStackTrace(); +} +``` + +这套操作方法涵盖了 vivid2D 的核心功能,从基础模型创建到高级动画和物理系统,帮助您快速上手并有效使用这个 2D 渲染引擎。 \ No newline at end of file