From b501da0254672beab07325224f2b97f0f4d67b63 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sun, 12 Oct 2025 08:41:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E6=B7=BB=E5=8A=A0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=A7=BF=E6=80=81=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?-=20=E6=96=B0=E5=A2=9E=20ModelPose=20=E7=B1=BB=E7=94=A8?= =?UTF-8?q?=E4=BA=8E=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9E=8B=E9=83=A8=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=A7=BF=E6=80=81=E6=95=B0=E6=8D=AE=20-=20=E5=9C=A8?= =?UTF-8?q?=20Model2D=20=E4=B8=AD=E5=AE=9E=E7=8E=B0=E5=A7=BF=E6=80=81?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=80=81=E5=BA=94=E7=94=A8=E5=92=8C=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E5=8A=9F=E8=83=BD-=20=E6=94=AF=E6=8C=81=E5=A7=BF?= =?UTF-8?q?=E6=80=81=E7=9A=84=E5=BA=8F=E5=88=97=E5=8C=96=E5=92=8C=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=20-=20=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=E6=9B=BF=E4=BB=A3=E5=8E=9F=E6=9C=89?= =?UTF-8?q?=E7=9A=84=20System.out=20=E5=92=8C=20System.err=20=E8=BE=93?= =?UTF-8?q?=E5=87=BA-=E4=BC=98=E5=8C=96=E7=BD=91=E6=A0=BC=E5=92=8C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=83=A8=E4=BB=B6=E7=9A=84=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=BE=93=E5=87=BA=20-=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=20PartPoseData=20=E5=92=8C=20PoseData=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E5=A7=BF=E6=80=81=E6=95=B0=E6=8D=AE=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?-=20=E5=AE=9E=E7=8E=B0=E5=A7=BF=E6=80=81=E9=97=B4=E7=9A=84?= =?UTF-8?q?=E5=B9=B3=E6=BB=91=E8=BF=87=E6=B8=A1=E5=92=8C=E6=8F=92=E5=80=BC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=20-=20=E5=A2=9E=E5=8A=A0=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=A7=BF=E6=80=81=E5=88=9D=E5=A7=8B=E5=8C=96=E5=92=8C=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vivid2D/render/ModelRender.java | 30 +- .../vivid2D/render/model/Model2D.java | 200 ++++++++- .../vivid2D/render/model/ModelPart.java | 7 +- .../vivid2D/render/model/data/ModelData.java | 70 +++ .../render/model/data/PartPoseData.java | 66 +++ .../vivid2D/render/model/data/PoseData.java | 62 +++ .../vivid2D/render/model/util/Mesh2D.java | 8 +- .../vivid2D/render/model/util/ModelPose.java | 404 +++++++++++++++++- 8 files changed, 825 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 058724a..dd90b79 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -11,6 +11,8 @@ import org.joml.Vector2f; import org.joml.Vector4f; import org.lwjgl.opengl.*; import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.FloatBuffer; @@ -25,7 +27,7 @@ import static org.lwjgl.opengl.GL20.glGetUniformLocation; * @author tzdwindows 7 */ public final class ModelRender { - + private static final Logger logger = LoggerFactory.getLogger(ModelRender.class); private ModelRender() { /* no instances */ } // ================== 全局状态 ================== @@ -33,8 +35,8 @@ public final class ModelRender { private static int viewportWidth = 800; private static int viewportHeight = 600; private static final Vector4f CLEAR_COLOR = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f); - private static boolean enableDepthTest = false; - private static boolean enableBlending = true; + private static final boolean enableDepthTest = false; + private static final boolean enableBlending = true; // 着色器与资源 private static final Map shaderMap = new HashMap<>(); @@ -80,7 +82,7 @@ public final class ModelRender { int loc = glGetUniformLocation(programId, k); if (loc == -1) { // debug 时可以打开 - // System.err.println("Warning: uniform not found: " + k); + logger.warn("Warning: uniform not found: {}", k); } return loc; }); @@ -271,7 +273,7 @@ public final class ModelRender { public static synchronized void initialize() { if (initialized) return; - System.out.println("Initializing ModelRender..."); + logger.info("Initializing ModelRender..."); // 需要在外部创建 OpenGL 上下文并调用 GL.createCapabilities() logGLInfo(); @@ -283,7 +285,7 @@ public final class ModelRender { try { compileDefaultShader(); } catch (RuntimeException ex) { - System.err.println("Failed to compile default shader: " + ex.getMessage()); + logger.error("Failed to compile default shader: {}", ex.getMessage()); throw ex; } @@ -294,14 +296,14 @@ public final class ModelRender { GL11.glViewport(0, 0, viewportWidth, viewportHeight); initialized = true; - System.out.println("ModelRender initialized successfully"); + logger.info("ModelRender initialized successfully"); } private static void logGLInfo() { - System.out.println("OpenGL Vendor: " + GL11.glGetString(GL11.GL_VENDOR)); - System.out.println("OpenGL Renderer: " + GL11.glGetString(GL11.GL_RENDERER)); - System.out.println("OpenGL Version: " + GL11.glGetString(GL11.GL_VERSION)); - System.out.println("GLSL Version: " + GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION)); + logger.info("OpenGL Vendor: {}", GL11.glGetString(GL11.GL_VENDOR)); + logger.info("OpenGL Renderer: {}", GL11.glGetString(GL11.GL_RENDERER)); + logger.info("OpenGL Version: {}", GL11.glGetString(GL11.GL_VERSION)); + logger.info("GLSL Version: {}", GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION)); } private static void uploadLightsToShader(ShaderProgram sp, Model2D model) { @@ -438,7 +440,7 @@ public final class ModelRender { public static synchronized void cleanup() { if (!initialized) return; - System.out.println("Cleaning up ModelRender..."); + logger.info("Cleaning up ModelRender..."); // mesh resources for (MeshGLResources r : meshResources.values()) r.dispose(); @@ -456,7 +458,7 @@ public final class ModelRender { } initialized = false; - System.out.println("ModelRender cleaned up"); + logger.info("ModelRender cleaned up"); } // ================== 渲染流程 (已修改) ================== @@ -790,7 +792,7 @@ public final class ModelRender { private static void checkGLError(String op) { int e = GL11.glGetError(); if (e != GL11.GL_NO_ERROR) { - System.err.println("OpenGL error during " + op + ": " + getGLErrorString(e)); + logger.error("OpenGL error during {}: {}", op, getGLErrorString(e)); } } 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 f67cf5d..bfc9b82 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -35,10 +35,18 @@ public class Model2D { private final PhysicsSystem physics; // ==================== 渲染状态 ==================== - private transient ModelPose currentPose; private transient boolean needsUpdate = true; private transient BoundingBox bounds; + // ==================== 姿态系统 ==================== + private final Map poses; // 存储所有预设姿态 + private String currentPoseName = "default"; // 当前应用的姿态名称 + private transient ModelPose currentPose; // 当前姿态实例 + private transient ModelPose blendTargetPose; // 混合目标姿态 + private float blendProgress = 1.0f; // 混合进度 (0-1) + private float blendSpeed = 1.0f; // 混合速度 + + // ==================== 光源系统 ==================== private final List lights; @@ -55,6 +63,10 @@ public class Model2D { this.currentPose = new ModelPose(); this.metadata = new ModelMetadata(); this.lights = new ArrayList<>(); + + this.poses = new HashMap<>(); + this.currentPose = new ModelPose("default"); + initializeDefaultPose(); } public Model2D(String name) { @@ -62,6 +74,69 @@ public class Model2D { this.name = name; } + // ==================== 姿态管理 ==================== + /** + * 添加或更新姿态 + */ + public void addPose(ModelPose pose) { + if (pose == null) { + throw new IllegalArgumentException("Pose cannot be null"); + } + poses.put(pose.getName(), pose); + markNeedsUpdate(); + } + + /** + * 获取姿态 + */ + public ModelPose getPose(String name) { + return poses.get(name); + } + + /** + * 应用姿态(立即) + */ + public void applyPose(String poseName) { + ModelPose pose = poses.get(poseName); + if (pose != null) { + applyPoseInternal(pose); + this.currentPoseName = poseName; + this.currentPose = pose; + this.blendProgress = 1.0f; + this.blendTargetPose = null; + markNeedsUpdate(); + } + } + + /** + * 混合到目标姿态 + */ + public void blendToPose(String targetPoseName, float blendTime) { + ModelPose targetPose = poses.get(targetPoseName); + if (targetPose != null) { + this.blendTargetPose = targetPose; + this.blendProgress = 0.0f; + this.blendSpeed = blendTime > 0 ? 1.0f / blendTime : 10.0f; // 默认0.1秒 + markNeedsUpdate(); + } + } + + /** + * 保存当前状态为姿态 + */ + public void saveCurrentPose(String poseName) { + ModelPose pose = new ModelPose(poseName); + captureCurrentPose(pose); + addPose(pose); + } + + /** + * 获取当前姿态名称 + */ + public String getCurrentPoseName() { + return currentPoseName; + } + // ==================== 光源管理 ==================== public List getLights() { return Collections.unmodifiableList(lights); @@ -204,6 +279,9 @@ public class Model2D { // ==================== 更新系统 (已修改) ==================== public void update(float deltaTime) { + + updatePoseBlending(deltaTime); + // 物理系统更新已被移至渲染器(ModelRender)中,以确保它在渲染前被调用。 // 这里的 hasActivePhysics() 检查可以保留,用于决定是否需要更新变换,以优化性能。 if (!needsUpdate && !physics.hasActivePhysics()) { @@ -225,6 +303,79 @@ public class Model2D { needsUpdate = false; } + /** + * 更新姿态混合 + */ + private void updatePoseBlending(float deltaTime) { + if (blendTargetPose != null && blendProgress < 1.0f) { + blendProgress += deltaTime * blendSpeed; + if (blendProgress >= 1.0f) { + blendProgress = 1.0f; + currentPose = blendTargetPose; + currentPoseName = blendTargetPose.getName(); + blendTargetPose = null; + } + markNeedsUpdate(); + } + } + + /** + * 应用当前姿态到模型 + */ + private void applyCurrentPoseToModel() { + if (blendTargetPose != null && blendProgress < 1.0f) { + // 混合姿态 + ModelPose blendedPose = ModelPose.lerp(currentPose, blendTargetPose, blendProgress, "blended"); + applyPoseInternal(blendedPose); + } else { + // 直接应用当前姿态 + applyPoseInternal(currentPose); + } + } + + /** + * 内部姿态应用方法 + */ + private void applyPoseInternal(ModelPose pose) { + for (String partName : pose.getPartNames()) { + ModelPart part = partMap.get(partName); + if (part != null) { + ModelPose.PartPose partPose = pose.getPartPose(partName); + part.setPosition(partPose.getPosition()); + part.setRotation(partPose.getRotation()); + part.setScale(partPose.getScale()); + part.setOpacity(partPose.getOpacity()); + part.setVisible(partPose.isVisible()); + } + } + } + + /** + * 捕获当前模型状态到姿态 + */ + private void captureCurrentPose(ModelPose pose) { + for (ModelPart part : parts) { + ModelPose.PartPose partPose = new ModelPose.PartPose( + part.getPosition(), + part.getRotation(), + part.getScale(), + part.getOpacity(), + part.isVisible(), + new org.joml.Vector3f(1, 1, 1) // 默认颜色,可根据需要修改 + ); + pose.setPartPose(part.getName(), partPose); + } + } + + /** + * 初始化默认姿态 + */ + private void initializeDefaultPose() { + ModelPose defaultPose = ModelPose.createDefaultPose(); + captureCurrentPose(defaultPose); + poses.put("default", defaultPose); + } + private void updateParameterDeformations() { for (AnimationParameter param : parameters.values()) { if (param.hasChanged()) { @@ -276,6 +427,44 @@ public class Model2D { } } + /** + * 检查是否存在指定姿态 + */ + public boolean hasPose(String poseName) { + return poses.containsKey(poseName); + } + + /** + * 移除姿态 + */ + public void removePose(String poseName) { + if (!"default".equals(poseName)) { // 保护默认姿态 + poses.remove(poseName); + if (currentPoseName.equals(poseName)) { + applyPose("default"); // 回退到默认姿态 + } + } + } + + /** + * 获取所有姿态名称 + */ + public java.util.Set getPoseNames() { + return Collections.unmodifiableSet(poses.keySet()); + } + + /** + * 立即混合到姿态(指定混合系数) + */ + public void setPoseBlend(String poseName, float blendFactor) { + ModelPose targetPose = poses.get(poseName); + if (targetPose != null) { + ModelPose blendedPose = ModelPose.lerp(currentPose, targetPose, blendFactor, "manual_blend"); + applyPoseInternal(blendedPose); + markNeedsUpdate(); + } + } + // ==================== 序列化支持 ==================== public ModelData serialize() { return new ModelData(this); @@ -368,7 +557,14 @@ public class Model2D { } public PhysicsSystem getPhysics() { return physics; } - public ModelPose getCurrentPose() { return currentPose; } + public ModelPose getCurrentPose() { + return new ModelPose(currentPose); + } + public float getBlendProgress() { return blendProgress; } + public boolean isBlending() { return blendProgress < 1.0f; } + public Map getPoses() { + return Collections.unmodifiableMap(poses); + } public BoundingBox getBounds() { return bounds; } public String getVersion() { return version; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 630a0dc..b34035a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -6,6 +6,8 @@ import com.chuangzhou.vivid2D.render.model.util.Matrix3fUtils; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import org.joml.Matrix3f; import org.joml.Vector2f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -18,6 +20,7 @@ import java.util.Objects; * @author tzdwindows 7 */ public class ModelPart { + private static final Logger logger = LoggerFactory.getLogger(ModelPart.class); // ==================== 基础属性 ==================== private String name; private ModelPart parent; @@ -460,7 +463,7 @@ public class ModelPart { public void printWorldPosition() { float worldX = worldTransform.m02(); float worldY = worldTransform.m12(); - System.out.println("World position: " + worldX + ", " + worldY); + logger.info("World position: {}, {}", worldX, worldY); } /** @@ -590,7 +593,7 @@ public class ModelPart { if (!pivotInitialized) { // 确保第一次设置 pivot 的时候,必须是 (0,0) 因为这个为非0,0时后面如果想要热变换就会出问题 if (x != 0 || y != 0) { - System.out.println("The first time you set the pivot, it must be (0,0), which is automatically adjusted to (0,0)."); + logger.warn("The first time you set the pivot, it must be (0,0), which is automatically adjusted to (0,0)."); x = 0; y = 0; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java index 99b988d..0a26ec9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java @@ -44,6 +44,8 @@ public class ModelData implements Serializable { private List physicsColliders; private List physicsConstraints; private List lights; + private List poses; + private String currentPoseName; // 当前应用的姿态名称 // 全局物理参数(便于序列化) private float physicsGravityX; @@ -93,6 +95,9 @@ public class ModelData implements Serializable { this.physicsAirResistance = 0.1f; this.physicsTimeScale = 1.0f; this.physicsEnabled = true; + + this.poses = new ArrayList<>(); + this.currentPoseName = "default"; } public ModelData(Model2D model) { @@ -209,6 +214,40 @@ public class ModelData implements Serializable { } } + /** + * 序列化姿态数据 + */ + private void serializePoses(Model2D model) { + poses.clear(); + + // 序列化所有预设姿态 + for (ModelPose pose : model.getPoses().values()) { + poses.add(new PoseData(pose)); + } + + // 保存当前姿态名称 + this.currentPoseName = model.getCurrentPoseName(); + } + + /** + * 反序列化姿态数据 + */ + private void deserializePoses(Model2D model) { + if (poses != null) { + for (PoseData poseData : poses) { + ModelPose pose = poseData.toModelPose(); + if (pose != null) { + model.addPose(pose); + } + } + } + + // 恢复当前姿态 + if (currentPoseName != null && model.hasPose(currentPoseName)) { + model.applyPose(currentPoseName); + } + } + private void serializeLights(Model2D model) { lights.clear(); if (model.getLights() != null) { @@ -271,6 +310,9 @@ public class ModelData implements Serializable { // 序列化光源 serializeLights(model); + // 序列化姿态 + serializePoses(model); + lastModifiedTime = System.currentTimeMillis(); } @@ -371,6 +413,9 @@ public class ModelData implements Serializable { // 反序列化光源 deserializeLights(model); + + // 反序列化姿态 + deserializePoses(model); return model; } @@ -728,6 +773,8 @@ public class ModelData implements Serializable { 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()); + for (PoseData pose : this.poses) copy.poses.add(pose.copy()); + copy.currentPoseName = this.currentPoseName; copy.physicsGravityX = this.physicsGravityX; copy.physicsGravityY = this.physicsGravityY; @@ -783,9 +830,24 @@ public class ModelData implements Serializable { this.textures.add(texture); } + // 合并姿态 + for (PoseData pose : other.poses) { + String originalName = pose.name; + int counter = 1; + while (poseExists(pose.name)) { + pose.name = originalName + "_" + counter++; + } + this.poses.add(pose); + } + + lastModifiedTime = System.currentTimeMillis(); } + private boolean poseExists(String poseName) { + return poses.stream().anyMatch(pose -> pose.name.equals(poseName)); + } + private boolean partExists(String partName) { return parts.stream().anyMatch(part -> part.name.equals(partName)); } @@ -1030,6 +1092,14 @@ public class ModelData implements Serializable { this.animationLayers = animationLayers; } + public List getPoses() { return poses; } + public void setPoses(List poses) { this.poses = poses; } + + public String getCurrentPoseName() { return currentPoseName; } + public void setCurrentPoseName(String currentPoseName) { + this.currentPoseName = currentPoseName; + } + // ==================== Object方法 ==================== @Override diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java new file mode 100644 index 0000000..cb253c0 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java @@ -0,0 +1,66 @@ +package com.chuangzhou.vivid2D.render.model.data; + +import com.chuangzhou.vivid2D.render.model.util.ModelPose; + +import java.io.Serializable; + +/** + * 部件姿态数据序列化类 + * @author tzdwindows 7 + */ +public class PartPoseData implements Serializable { + private static final long serialVersionUID = 1L; + + public String partName; + public float posX, posY; + public float rotation; + public float scaleX, scaleY; + public float opacity; + public boolean visible; + public float colorR, colorG, colorB; + + public PartPoseData() {} + + public PartPoseData(String partName, ModelPose.PartPose partPose) { + this.partName = partName; + this.posX = partPose.getPosition().x; + this.posY = partPose.getPosition().y; + this.rotation = partPose.getRotation(); + this.scaleX = partPose.getScale().x; + this.scaleY = partPose.getScale().y; + this.opacity = partPose.getOpacity(); + this.visible = partPose.isVisible(); + + org.joml.Vector3f color = partPose.getColor(); + this.colorR = color.x; + this.colorG = color.y; + this.colorB = color.z; + } + + public ModelPose.PartPose toPartPose() { + return new ModelPose.PartPose( + new org.joml.Vector2f(posX, posY), + rotation, + new org.joml.Vector2f(scaleX, scaleY), + opacity, + visible, + new org.joml.Vector3f(colorR, colorG, colorB) + ); + } + + public PartPoseData copy() { + PartPoseData copy = new PartPoseData(); + copy.partName = this.partName; + copy.posX = this.posX; + copy.posY = this.posY; + copy.rotation = this.rotation; + copy.scaleX = this.scaleX; + copy.scaleY = this.scaleY; + copy.opacity = this.opacity; + copy.visible = this.visible; + copy.colorR = this.colorR; + copy.colorG = this.colorG; + copy.colorB = this.colorB; + return copy; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java new file mode 100644 index 0000000..12f568e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java @@ -0,0 +1,62 @@ +package com.chuangzhou.vivid2D.render.model.data; + +import com.chuangzhou.vivid2D.render.model.util.ModelPose; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 姿态数据序列化类 + * @author tzdwindows 7 + */ +public class PoseData implements Serializable { + private static final long serialVersionUID = 1L; + + public String name; + public float blendTime; + public boolean isDefaultPose; + public List partPoses; + + public PoseData() { + this.partPoses = new ArrayList<>(); + } + + public PoseData(ModelPose pose) { + this.name = pose.getName(); + this.blendTime = pose.getBlendTime(); + this.isDefaultPose = pose.isDefaultPose(); + this.partPoses = new ArrayList<>(); + + // 序列化所有部件姿态 + for (String partName : pose.getPartNames()) { + ModelPose.PartPose partPose = pose.getPartPose(partName); + partPoses.add(new PartPoseData(partName, partPose)); + } + } + + public ModelPose toModelPose() { + ModelPose pose = new ModelPose(name); + pose.setBlendTime(blendTime); + pose.setDefaultPose(isDefaultPose); + + // 反序列化部件姿态 + for (PartPoseData partPoseData : partPoses) { + pose.setPartPose(partPoseData.partName, partPoseData.toPartPose()); + } + + return pose; + } + + public PoseData copy() { + PoseData copy = new PoseData(); + copy.name = this.name; + copy.blendTime = this.blendTime; + copy.isDefaultPose = this.isDefaultPose; + copy.partPoses = new ArrayList<>(); + for (PartPoseData partPose : this.partPoses) { + copy.partPoses.add(partPose.copy()); + } + return copy; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index f758b9a..48beeb8 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.model.util; +import com.chuangzhou.vivid2D.render.model.ModelPart; import org.joml.Vector2f; import java.nio.FloatBuffer; @@ -10,6 +11,8 @@ import org.lwjgl.opengl.GL15; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL30; import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 2D网格类,用于存储和管理2D模型的几何数据 @@ -18,6 +21,7 @@ import org.lwjgl.system.MemoryUtil; * @author tzdwindows 7 */ public class Mesh2D { + private static final Logger logger = LoggerFactory.getLogger(Mesh2D.class); // ==================== 网格数据 ==================== private String name; private float[] vertices; // 顶点数据 [x0, y0, x1, y1, ...] @@ -369,8 +373,6 @@ public class Mesh2D { buffer.clear(); for (int i = 0; i < vertexCount; i++) { - // 明确使用定位方法,避免下标算错 - //System.out.println("x:"+ getX(i) + "y:"+ getY(i)); buffer.put(getX(i)); // x buffer.put(getY(i)); // y buffer.put(uvs[i * 2]); // u @@ -501,7 +503,7 @@ public class Mesh2D { org.lwjgl.system.MemoryUtil.memFree(fb); } } else { - System.err.println("警告: 着色器中未找到 uModelMatrix 或 uModel uniform"); + logger.warn("警告: 着色器中未找到 uModelMatrix 或 uModel uniform"); } // 绘制 diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java index 22ee098..59a36c3 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java @@ -1,4 +1,406 @@ package com.chuangzhou.vivid2D.render.model.util; +import org.joml.Vector2f; +import org.joml.Vector3f; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 模型姿态类 - 用于存储和管理2D模型的部件变换状态 + * 支持动画系统、姿态保存/恢复、姿态混合等功能 + * + * @author tzdwindows 7 + */ public class ModelPose { -} + + // ================== 内部类:部件姿态 ================== + + /** + * 单个部件的姿态数据 + */ + public static class PartPose { + private Vector2f position; + private float rotation; + private Vector2f scale; + private float opacity; + private boolean visible; + private Vector3f color; // RGB颜色乘数 + + public PartPose() { + this(new Vector2f(0, 0), 0.0f, new Vector2f(1, 1), 1.0f, true, new Vector3f(1, 1, 1)); + } + + public PartPose(Vector2f position, float rotation, Vector2f scale, + float opacity, boolean visible, Vector3f color) { + this.position = new Vector2f(position); + this.rotation = rotation; + this.scale = new Vector2f(scale); + this.opacity = opacity; + this.visible = visible; + this.color = new Vector3f(color); + } + + public PartPose(PartPose other) { + this.position = new Vector2f(other.position); + this.rotation = other.rotation; + this.scale = new Vector2f(other.scale); + this.opacity = other.opacity; + this.visible = other.visible; + this.color = new Vector3f(other.color); + } + + // ================== 线性插值方法 ================== + + /** + * 在两个部件姿态间进行线性插值 + */ + public static PartPose lerp(PartPose a, PartPose b, float alpha) { + alpha = Math.max(0.0f, Math.min(1.0f, alpha)); // 钳制到[0,1] + + Vector2f pos = new Vector2f(a.position).lerp(b.position, alpha); + float rot = a.rotation + (b.rotation - a.rotation) * alpha; + Vector2f scl = new Vector2f(a.scale).lerp(b.scale, alpha); + float opa = a.opacity + (b.opacity - a.opacity) * alpha; + Vector3f col = new Vector3f(a.color).lerp(b.color, alpha); + + // 可见性:当alpha>0.5时使用b的可见性 + boolean vis = alpha < 0.5f ? a.visible : b.visible; + + return new PartPose(pos, rot, scl, opa, vis, col); + } + + /** + * 带旋转正确插值的线性插值(处理360°边界) + */ + public static PartPose lerpWithRotation(PartPose a, PartPose b, float alpha) { + alpha = Math.max(0.0f, Math.min(1.0f, alpha)); + + Vector2f pos = new Vector2f(a.position).lerp(b.position, alpha); + + // 处理旋转插值的角度环绕问题 + float shortestAngle = ((b.rotation - a.rotation) % 360 + 540) % 360 - 180; + float rot = a.rotation + shortestAngle * alpha; + + Vector2f scl = new Vector2f(a.scale).lerp(b.scale, alpha); + float opa = a.opacity + (b.opacity - a.opacity) * alpha; + Vector3f col = new Vector3f(a.color).lerp(b.color, alpha); + boolean vis = alpha < 0.5f ? a.visible : b.visible; + + return new PartPose(pos, rot, scl, opa, vis, col); + } + + // ================== Getter和Setter ================== + + public Vector2f getPosition() { return new Vector2f(position); } + public void setPosition(Vector2f position) { this.position.set(position); } + + public float getRotation() { return rotation; } + public void setRotation(float rotation) { this.rotation = rotation; } + + public Vector2f getScale() { return new Vector2f(scale); } + public void setScale(Vector2f scale) { this.scale.set(scale); } + + public float getOpacity() { return opacity; } + public void setOpacity(float opacity) { this.opacity = opacity; } + + public boolean isVisible() { return visible; } + public void setVisible(boolean visible) { this.visible = visible; } + + public Vector3f getColor() { return new Vector3f(color); } + public void setColor(Vector3f color) { this.color.set(color); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PartPose partPose = (PartPose) o; + return Float.compare(partPose.rotation, rotation) == 0 && + Float.compare(partPose.opacity, opacity) == 0 && + visible == partPose.visible && + Objects.equals(position, partPose.position) && + Objects.equals(scale, partPose.scale) && + Objects.equals(color, partPose.color); + } + + @Override + public int hashCode() { + return Objects.hash(position, rotation, scale, opacity, visible, color); + } + + @Override + public String toString() { + return String.format("PartPose{pos=(%.2f,%.2f), rot=%.2f, scale=(%.2f,%.2f), opacity=%.2f, visible=%s, color=(%.2f,%.2f,%.2f)}", + position.x, position.y, rotation, scale.x, scale.y, opacity, visible, color.x, color.y, color.z); + } + } + + // ================== ModelPose 主体 ================== + + private String name; + private final Map partPoses; + private float blendTime = 0.3f; // 默认混合时间(秒) + private boolean isDefaultPose = false; + + // ================== 构造函数 ================== + + public ModelPose() { + this("Unnamed Pose"); + } + + public ModelPose(String name) { + this.name = name; + this.partPoses = new HashMap<>(); + } + + public ModelPose(ModelPose other) { + this.name = other.name + " (Copy)"; + this.partPoses = new HashMap<>(); + this.blendTime = other.blendTime; + this.isDefaultPose = other.isDefaultPose; + + // 深拷贝所有部件姿态 + for (Map.Entry entry : other.partPoses.entrySet()) { + this.partPoses.put(entry.getKey(), new PartPose(entry.getValue())); + } + } + + // ================== 姿态管理方法 ================== + + /** + * 设置指定部件的姿态 + */ + public void setPartPose(String partName, PartPose pose) { + partPoses.put(partName, new PartPose(pose)); + } + + /** + * 获取指定部件的姿态(如果不存在则创建默认姿态) + */ + public PartPose getPartPose(String partName) { + return partPoses.computeIfAbsent(partName, k -> new PartPose()); + } + + /** + * 检查是否包含指定部件的姿态 + */ + public boolean hasPartPose(String partName) { + return partPoses.containsKey(partName); + } + + /** + * 移除指定部件的姿态 + */ + public PartPose removePartPose(String partName) { + return partPoses.remove(partName); + } + + /** + * 获取所有部件名称 + */ + public java.util.Set getPartNames() { + return partPoses.keySet(); + } + + /** + * 清空所有部件姿态 + */ + public void clear() { + partPoses.clear(); + } + + /** + * 获取部件数量 + */ + public int getPartCount() { + return partPoses.size(); + } + + // ================== 便捷方法 ================== + + /** + * 设置部件位置 + */ + public void setPartPosition(String partName, Vector2f position) { + getPartPose(partName).setPosition(position); + } + + /** + * 设置部件旋转(角度) + */ + public void setPartRotation(String partName, float rotation) { + getPartPose(partName).setRotation(rotation); + } + + /** + * 设置部件缩放 + */ + public void setPartScale(String partName, Vector2f scale) { + getPartPose(partName).setScale(scale); + } + + /** + * 设置部件不透明度 + */ + public void setPartOpacity(String partName, float opacity) { + getPartPose(partName).setOpacity(opacity); + } + + /** + * 设置部件可见性 + */ + public void setPartVisible(String partName, boolean visible) { + getPartPose(partName).setVisible(visible); + } + + /** + * 设置部件颜色 + */ + public void setPartColor(String partName, Vector3f color) { + getPartPose(partName).setColor(color); + } + + // ================== 姿态混合 ================== + + /** + * 在两个姿态间进行线性插值 + */ + public static ModelPose lerp(ModelPose a, ModelPose b, float alpha, String resultName) { + ModelPose result = new ModelPose(resultName); + result.setBlendTime(a.blendTime + (b.blendTime - a.blendTime) * alpha); + + // 合并两个姿态的所有部件 + java.util.Set allParts = new java.util.HashSet<>(); + allParts.addAll(a.getPartNames()); + allParts.addAll(b.getPartNames()); + + for (String partName : allParts) { + PartPose poseA = a.partPoses.get(partName); + PartPose poseB = b.partPoses.get(partName); + + if (poseA != null && poseB != null) { + // 两个姿态都有该部件:插值 + result.setPartPose(partName, PartPose.lerpWithRotation(poseA, poseB, alpha)); + } else if (poseA != null) { + // 只有姿态A有该部件:根据alpha决定是否保留 + if (alpha < 0.5f) { + result.setPartPose(partName, new PartPose(poseA)); + } + } else if (poseB != null) { + // 只有姿态B有该部件:根据alpha决定是否保留 + if (alpha >= 0.5f) { + result.setPartPose(partName, new PartPose(poseB)); + } + } + } + + return result; + } + + /** + * 将当前姿态与另一个姿态混合 + */ + public void blendWith(ModelPose other, float alpha) { + Map newPoses = new HashMap<>(); + java.util.Set allParts = new java.util.HashSet<>(); + allParts.addAll(this.getPartNames()); + allParts.addAll(other.getPartNames()); + + for (String partName : allParts) { + PartPose thisPose = this.partPoses.get(partName); + PartPose otherPose = other.partPoses.get(partName); + + if (thisPose != null && otherPose != null) { + newPoses.put(partName, PartPose.lerpWithRotation(thisPose, otherPose, alpha)); + } else if (thisPose != null && alpha < 0.5f) { + newPoses.put(partName, new PartPose(thisPose)); + } else if (otherPose != null && alpha >= 0.5f) { + newPoses.put(partName, new PartPose(otherPose)); + } + } + + this.partPoses.clear(); + this.partPoses.putAll(newPoses); + } + + // ================== 预设姿态工厂方法 ================== + + /** + * 创建默认姿态(所有部件在原点,无旋转,正常缩放) + */ + public static ModelPose createDefaultPose() { + ModelPose pose = new ModelPose("Default Pose"); + pose.isDefaultPose = true; + return pose; + } + + /** + * 创建隐藏姿态(所有部件不可见) + */ + public static ModelPose createHiddenPose() { + ModelPose pose = new ModelPose("Hidden Pose"); + pose.isDefaultPose = false; + // 不预先添加任何部件,使用时自动创建为隐藏状态 + return pose; + } + + /** + * 创建缩放姿态(统一缩放所有部件) + */ + public static ModelPose createScaledPose(float scaleX, float scaleY) { + ModelPose pose = new ModelPose("Scaled Pose"); + pose.isDefaultPose = false; + // 不预先添加部件,使用时自动创建带缩放的姿态 + return pose; + } + + // ================== Getter和Setter ================== + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public float getBlendTime() { return blendTime; } + public void setBlendTime(float blendTime) { this.blendTime = Math.max(0, blendTime); } + + public boolean isDefaultPose() { return isDefaultPose; } + public void setDefaultPose(boolean defaultPose) { isDefaultPose = defaultPose; } + + // ================== 工具方法 ================== + + /** + * 检查姿态是否为空(不包含任何部件姿态) + */ + public boolean isEmpty() { + return partPoses.isEmpty(); + } + + /** + * 获取姿态的简要描述 + */ + public String getDescription() { + return String.format("ModelPose{name='%s', parts=%d, blendTime=%.2fs}", + name, partPoses.size(), blendTime); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ModelPose modelPose = (ModelPose) o; + return Float.compare(modelPose.blendTime, blendTime) == 0 && + isDefaultPose == modelPose.isDefaultPose && + Objects.equals(name, modelPose.name) && + Objects.equals(partPoses, modelPose.partPoses); + } + + @Override + public int hashCode() { + return Objects.hash(name, partPoses, blendTime, isDefaultPose); + } + + @Override + public String toString() { + return getDescription(); + } +} \ No newline at end of file