feat(render): 添加光源与物理系统支持
- 新增 BufferBuilder 工具类用于简化顶点数据提交 - 实现 LightSource 和 LightSourceData 类以支持光源管理- 在 Model2D 中集成光源系统,支持序列化与反序列化 - 扩展 ModelData 以支持物理系统数据的完整序列化 - 重构 ModelRender以支持物理系统应用及碰撞箱渲染 - 添加粒子、弹簧、约束与碰撞体的数据结构与序列化逻辑 - 实现变形器的序列化接口以支持参数驱动动画的持久化
This commit is contained in:
@@ -2,17 +2,19 @@ package com.chuangzhou.vivid2D.render;
|
|||||||
|
|
||||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
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.Mesh2D;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; // 引入 PhysicsSystem
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||||
import org.joml.Matrix3f;
|
import org.joml.Matrix3f;
|
||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
import org.joml.Vector3f;
|
||||||
import org.joml.Vector4f;
|
import org.joml.Vector4f;
|
||||||
import org.lwjgl.opengl.*;
|
import org.lwjgl.opengl.*;
|
||||||
import org.lwjgl.system.MemoryUtil;
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
import java.nio.IntBuffer;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ import static org.lwjgl.opengl.GL20.glGetUniformLocation;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 重构后的 ModelRender:更模块化、健壮的渲染子系统
|
* 重构后的 ModelRender:更模块化、健壮的渲染子系统
|
||||||
|
* (已修改以应用物理系统,并支持渲染碰撞箱)
|
||||||
* @author tzdwindows 7
|
* @author tzdwindows 7
|
||||||
*/
|
*/
|
||||||
public final class ModelRender {
|
public final class ModelRender {
|
||||||
@@ -44,6 +47,18 @@ public final class ModelRender {
|
|||||||
// 默认白色纹理
|
// 默认白色纹理
|
||||||
private static int defaultTextureId = 0;
|
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 ==================
|
// ================== 内部类:ShaderProgram ==================
|
||||||
private static class ShaderProgram {
|
private static class ShaderProgram {
|
||||||
final int programId;
|
final int programId;
|
||||||
@@ -100,42 +115,80 @@ public final class ModelRender {
|
|||||||
layout(location = 0) in vec2 aPosition;
|
layout(location = 0) in vec2 aPosition;
|
||||||
layout(location = 1) in vec2 aTexCoord;
|
layout(location = 1) in vec2 aTexCoord;
|
||||||
out vec2 vTexCoord;
|
out vec2 vTexCoord;
|
||||||
out vec2 vDebugPos;
|
out vec2 vWorldPos;
|
||||||
|
|
||||||
uniform mat3 uModelMatrix;
|
uniform mat3 uModelMatrix;
|
||||||
uniform mat3 uViewMatrix;
|
uniform mat3 uViewMatrix;
|
||||||
uniform mat3 uProjectionMatrix;
|
uniform mat3 uProjectionMatrix;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);
|
vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);
|
||||||
gl_Position = vec4(p.xy, 0.0, 1.0);
|
gl_Position = vec4(p.xy, 0.0, 1.0);
|
||||||
vTexCoord = aTexCoord;
|
vTexCoord = aTexCoord;
|
||||||
vDebugPos = p.xy;
|
vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy;
|
||||||
}""";
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
private static final String FRAGMENT_SHADER_SRC =
|
private static final String FRAGMENT_SHADER_SRC =
|
||||||
"""
|
"""
|
||||||
#version 330 core
|
#version 330 core
|
||||||
in vec2 vTexCoord;
|
in vec2 vTexCoord;
|
||||||
in vec2 vDebugPos;
|
in vec2 vWorldPos;
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
uniform sampler2D uTexture;
|
uniform sampler2D uTexture;
|
||||||
uniform vec4 uColor;
|
uniform vec4 uColor;
|
||||||
uniform float uOpacity;
|
uniform float uOpacity;
|
||||||
uniform int uBlendMode;
|
uniform int uBlendMode;
|
||||||
uniform int uDebugMode;
|
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() {
|
void main() {
|
||||||
if (uDebugMode == 1) {
|
if (uDebugMode == 1) {
|
||||||
FragColor = vec4(vDebugPos * 0.5 + 0.5, 0.0, 1.0);
|
FragColor = vec4(vWorldPos * 0.5 + 0.5, 0.0, 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec4 tex = texture(uTexture, vTexCoord);
|
vec4 tex = texture(uTexture, vTexCoord);
|
||||||
vec4 finalColor = tex * uColor;
|
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;
|
if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb;
|
||||||
else if (uBlendMode == 2) 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);
|
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;
|
float alpha = tex.a * uOpacity;
|
||||||
FragColor = finalColor;
|
if (alpha <= 0.001) discard;
|
||||||
}""";
|
|
||||||
|
FragColor = vec4(finalColor, alpha);
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
// ================== 初始化 / 清理 ==================
|
// ================== 初始化 / 清理 ==================
|
||||||
public static synchronized void initialize() {
|
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));
|
System.out.println("GLSL Version: " + GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void uploadLightsToShader(ShaderProgram sp, Model2D model) {
|
||||||
|
List<LightSource> 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() {
|
private static void setupGLState() {
|
||||||
GL11.glClearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w);
|
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);
|
shaderMap.put("default", sp);
|
||||||
defaultProgram = sp;
|
defaultProgram = sp;
|
||||||
|
|
||||||
// 设置一些默认 uniform(需要先 use)
|
|
||||||
sp.use();
|
sp.use();
|
||||||
setUniformIntInternal(sp, "uTexture", 0);
|
setUniformIntInternal(sp, "uTexture", 0);
|
||||||
setUniformFloatInternal(sp, "uOpacity", 1.0f);
|
setUniformFloatInternal(sp, "uOpacity", 1.0f);
|
||||||
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
|
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
|
||||||
setUniformIntInternal(sp, "uBlendMode", 0);
|
setUniformIntInternal(sp, "uBlendMode", 0);
|
||||||
|
setUniformIntInternal(sp, "uDebugMode", 0);
|
||||||
|
setUniformIntInternal(sp, "uLightCount", 0); // 默认没有光源
|
||||||
sp.stop();
|
sp.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,38 +367,70 @@ public final class ModelRender {
|
|||||||
System.out.println("ModelRender cleaned up");
|
System.out.println("ModelRender cleaned up");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== 渲染流程 ==================
|
// ================== 渲染流程 (已修改) ==================
|
||||||
public static void render(float deltaTime, Model2D model) {
|
public static void render(float deltaTime, Model2D model) {
|
||||||
if (!initialized) throw new IllegalStateException("ModelRender not initialized");
|
if (!initialized) throw new IllegalStateException("ModelRender not initialized");
|
||||||
if (model == null) return;
|
if (model == null) return;
|
||||||
|
|
||||||
// 更新模型(确保 worldTransform 已经被计算)
|
// 物理系统更新
|
||||||
|
PhysicsSystem physics = model.getPhysics();
|
||||||
|
if (physics != null && physics.isEnabled()) {
|
||||||
|
physics.update(deltaTime, model);
|
||||||
|
}
|
||||||
|
|
||||||
model.update(deltaTime);
|
model.update(deltaTime);
|
||||||
|
|
||||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0));
|
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0));
|
||||||
|
|
||||||
// 使用默认 shader(保持绑定直到完成渲染)
|
|
||||||
defaultProgram.use();
|
defaultProgram.use();
|
||||||
|
|
||||||
// setUniformIntInternal(defaultProgram, "uDebugMode", 0); 设置debug模式
|
// 设置投影与视图
|
||||||
|
|
||||||
// 设置投影与视图(3x3 正交投影用于 2D)
|
|
||||||
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
|
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
|
||||||
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
|
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
|
||||||
setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity());
|
setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity());
|
||||||
|
|
||||||
// 递归渲染所有根部件(使用 3x3 矩阵)
|
// 添加光源数据上传
|
||||||
|
uploadLightsToShader(defaultProgram, model);
|
||||||
|
renderLightPositions(model);
|
||||||
|
|
||||||
|
// 递归渲染所有根部件
|
||||||
Matrix3f identity = new Matrix3f().identity();
|
Matrix3f identity = new Matrix3f().identity();
|
||||||
for (ModelPart p : model.getParts()) {
|
for (ModelPart p : model.getParts()) {
|
||||||
if (p.getParent() != null) continue;
|
if (p.getParent() != null) continue;
|
||||||
renderPartRecursive(p, identity);
|
renderPartRecursive(p, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultProgram.stop();
|
if (renderColliders && physics != null) {
|
||||||
|
renderPhysicsColliders(physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
defaultProgram.stop();
|
||||||
checkGLError("render");
|
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 的 worldTransform,
|
||||||
* 然后直接使用 part.getWorldTransform() 作为 uModelMatrix 传入 shader。
|
* 然后直接使用 part.getWorldTransform() 作为 uModelMatrix 传入 shader。
|
||||||
@@ -330,9 +445,8 @@ public final class ModelRender {
|
|||||||
// 先设置部件相关的 uniform(opacity / blendMode / color 等)
|
// 先设置部件相关的 uniform(opacity / blendMode / color 等)
|
||||||
setPartUniforms(defaultProgram, part);
|
setPartUniforms(defaultProgram, part);
|
||||||
|
|
||||||
// 把 world 矩阵传给 shader(兼容 uModelMatrix 和 可能的 uModel)
|
// 把 world 矩阵传给 shader(uModelMatrix)
|
||||||
setUniformMatrix3(defaultProgram, "uModelMatrix", world);
|
setUniformMatrix3(defaultProgram, "uModelMatrix", world);
|
||||||
setUniformMatrix3(defaultProgram, "uModel", world);
|
|
||||||
|
|
||||||
// 绘制本节点的所有 mesh(将 world 传入 renderMesh)
|
// 绘制本节点的所有 mesh(将 world 传入 renderMesh)
|
||||||
for (Mesh2D mesh : part.getMeshes()) {
|
for (Mesh2D mesh : part.getMeshes()) {
|
||||||
@@ -348,9 +462,6 @@ public final class ModelRender {
|
|||||||
private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) {
|
private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) {
|
||||||
if (!mesh.isVisible()) return;
|
if (!mesh.isVisible()) return;
|
||||||
|
|
||||||
// 使用默认 shader
|
|
||||||
defaultProgram.use();
|
|
||||||
|
|
||||||
// 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换)
|
// 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换)
|
||||||
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix;
|
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix;
|
||||||
|
|
||||||
@@ -364,34 +475,145 @@ public final class ModelRender {
|
|||||||
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将模型矩阵设置为当前 mesh 使用的矩阵(shader 内名为 uModelMatrix)
|
||||||
|
setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse);
|
||||||
|
|
||||||
// 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵
|
// 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵
|
||||||
mesh.draw(defaultProgram.programId, matToUse);
|
mesh.draw(defaultProgram.programId, matToUse);
|
||||||
|
|
||||||
checkGLError("renderMesh");
|
checkGLError("renderMesh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================== 渲染碰撞箱相关实现 ==================
|
||||||
|
|
||||||
private static int getGLDrawMode(int meshDrawMode) {
|
private static void renderPhysicsColliders(PhysicsSystem physics) {
|
||||||
switch (meshDrawMode) {
|
// 设置渲染状态
|
||||||
case Mesh2D.POINTS: return GL11.GL_POINTS;
|
GL11.glLineWidth(colliderLineWidth);
|
||||||
case Mesh2D.LINES: return GL11.GL_LINES;
|
|
||||||
case Mesh2D.LINE_STRIP: return GL11.GL_LINE_STRIP;
|
// 绑定默认纹理(shader 依赖 uTexture)并设置颜色/opacity
|
||||||
case Mesh2D.TRIANGLES: return GL11.GL_TRIANGLES;
|
GL13.glActiveTexture(GL13.GL_TEXTURE0);
|
||||||
case Mesh2D.TRIANGLE_STRIP: return GL11.GL_TRIANGLE_STRIP;
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, defaultTextureId);
|
||||||
case Mesh2D.TRIANGLE_FAN: return GL11.GL_TRIANGLE_FAN;
|
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
||||||
default: return GL11.GL_TRIANGLES;
|
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 已绑定) ==================
|
// ================== uniform 设置辅助(内部使用,确保 program 已绑定) ==================
|
||||||
private static void setUniformIntInternal(ShaderProgram sp, String name, int value) {
|
private static void setUniformIntInternal(ShaderProgram sp, String name, int value) {
|
||||||
int loc = sp.getUniformLocation(name);
|
int loc = sp.getUniformLocation(name);
|
||||||
if (loc != -1) GL20.glUniform1i(loc, value);
|
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) {
|
private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) {
|
||||||
int loc = sp.getUniformLocation(name);
|
int loc = sp.getUniformLocation(name);
|
||||||
if (loc != -1) GL20.glUniform1f(loc, value);
|
if (loc != -1) GL20.glUniform1f(loc, value);
|
||||||
|
|||||||
@@ -7,41 +7,7 @@ import java.util.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 2D模型核心数据结构
|
* 2D模型核心数据结构
|
||||||
* 支持层级变换、网格变形、参数驱动动画等功能
|
* (已修改以配合 ModelRender 的物理系统应用)
|
||||||
*
|
|
||||||
* 例子
|
|
||||||
* // 创建模型
|
|
||||||
* Model2D model = new Model2D("character");
|
|
||||||
* model.setVersion("1.0.0");
|
|
||||||
*
|
|
||||||
* // 添加部件
|
|
||||||
* ModelPart head = model.createPart("head");
|
|
||||||
* ModelPart body = model.createPart("body");
|
|
||||||
* ModelPart leftArm = model.createPart("left_arm");
|
|
||||||
*
|
|
||||||
* // 建立层级关系
|
|
||||||
* body.addChild(head);
|
|
||||||
* body.addChild(leftArm);
|
|
||||||
*
|
|
||||||
* // 创建网格
|
|
||||||
* Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50);
|
|
||||||
* Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80);
|
|
||||||
* head.addMesh(headMesh);
|
|
||||||
* body.addMesh(bodyMesh);
|
|
||||||
*
|
|
||||||
* // 添加参数
|
|
||||||
* AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0);
|
|
||||||
* AnimationParameter blinkParam = model.createParameter("blink", 0, 1, 0);
|
|
||||||
*
|
|
||||||
* // 保存模型
|
|
||||||
* model.saveToFile("character.model");
|
|
||||||
*
|
|
||||||
* // 加载模型
|
|
||||||
* Model2D loadedModel = Model2D.loadFromFile("character.model");
|
|
||||||
*
|
|
||||||
* // 使用加载的模型
|
|
||||||
* loadedModel.setParameterValue("smile", 0.8f);
|
|
||||||
* loadedModel.update(0.016f); // 更新模型状态
|
|
||||||
*
|
*
|
||||||
* @author tzdwindows 7
|
* @author tzdwindows 7
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +37,9 @@ public class Model2D {
|
|||||||
private transient boolean needsUpdate = true;
|
private transient boolean needsUpdate = true;
|
||||||
private transient BoundingBox bounds;
|
private transient BoundingBox bounds;
|
||||||
|
|
||||||
|
// ==================== 光源系统 ====================
|
||||||
|
private final List<LightSource> lights;
|
||||||
|
|
||||||
// ==================== 构造器 ====================
|
// ==================== 构造器 ====================
|
||||||
public Model2D() {
|
public Model2D() {
|
||||||
this.uuid = UUID.randomUUID();
|
this.uuid = UUID.randomUUID();
|
||||||
@@ -83,6 +52,7 @@ public class Model2D {
|
|||||||
this.physics = new PhysicsSystem();
|
this.physics = new PhysicsSystem();
|
||||||
this.currentPose = new ModelPose();
|
this.currentPose = new ModelPose();
|
||||||
this.metadata = new ModelMetadata();
|
this.metadata = new ModelMetadata();
|
||||||
|
this.lights = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Model2D(String name) {
|
public Model2D(String name) {
|
||||||
@@ -90,6 +60,34 @@ public class Model2D {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 光源管理 ====================
|
||||||
|
public List<LightSource> 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) {
|
public ModelPart createPart(String name) {
|
||||||
ModelPart part = new ModelPart(name);
|
ModelPart part = new ModelPart(name);
|
||||||
@@ -202,14 +200,16 @@ public class Model2D {
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 更新系统 ====================
|
// ==================== 更新系统 (已修改) ====================
|
||||||
public void update(float deltaTime) {
|
public void update(float deltaTime) {
|
||||||
|
// 物理系统更新已被移至渲染器(ModelRender)中,以确保它在渲染前被调用。
|
||||||
|
// 这里的 hasActivePhysics() 检查可以保留,用于决定是否需要更新变换,以优化性能。
|
||||||
if (!needsUpdate && !physics.hasActivePhysics()) {
|
if (!needsUpdate && !physics.hasActivePhysics()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新物理系统
|
// 核心修改:移除了 physics.update(deltaTime, this); 这一行。
|
||||||
physics.update(deltaTime, this);
|
// 该调用现在由 ModelRender.render() 方法负责。
|
||||||
|
|
||||||
// 更新所有参数驱动的变形
|
// 更新所有参数驱动的变形
|
||||||
updateParameterDeformations();
|
updateParameterDeformations();
|
||||||
@@ -327,6 +327,7 @@ public class Model2D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从压缩文件加载模型
|
* 从压缩文件加载模型
|
||||||
*/
|
*/
|
||||||
@@ -352,6 +353,8 @@ public class Model2D {
|
|||||||
public ModelPart getRootPart() { return rootPart; }
|
public ModelPart getRootPart() { return rootPart; }
|
||||||
public void setRootPart(ModelPart rootPart) { this.rootPart = rootPart; }
|
public void setRootPart(ModelPart rootPart) { this.rootPart = rootPart; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public List<Mesh2D> getMeshes() { return Collections.unmodifiableList(meshes); }
|
public List<Mesh2D> getMeshes() { return Collections.unmodifiableList(meshes); }
|
||||||
|
|
||||||
public Map<String, AnimationParameter> getParameters() {
|
public Map<String, AnimationParameter> getParameters() {
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ public class ModelData implements Serializable {
|
|||||||
private List<ParameterData> parameters;
|
private List<ParameterData> parameters;
|
||||||
private List<AnimationData> animations;
|
private List<AnimationData> animations;
|
||||||
private List<AnimationLayerData> animationLayers;
|
private List<AnimationLayerData> animationLayers;
|
||||||
|
private List<ParticleData> physicsParticles;
|
||||||
|
private List<SpringData> physicsSprings;
|
||||||
|
private List<ColliderData> physicsColliders;
|
||||||
|
private List<ConstraintData> physicsConstraints;
|
||||||
|
private List<LightSourceData> lights;
|
||||||
|
|
||||||
|
// 全局物理参数(便于序列化)
|
||||||
|
private float physicsGravityX;
|
||||||
|
private float physicsGravityY;
|
||||||
|
private float physicsAirResistance;
|
||||||
|
private float physicsTimeScale;
|
||||||
|
private boolean physicsEnabled;
|
||||||
|
|
||||||
|
|
||||||
// ==================== 模型设置 ====================
|
// ==================== 模型设置 ====================
|
||||||
private Vector2f pivotPoint;
|
private Vector2f pivotPoint;
|
||||||
@@ -61,10 +74,22 @@ public class ModelData implements Serializable {
|
|||||||
this.parameters = new ArrayList<>();
|
this.parameters = new ArrayList<>();
|
||||||
this.animations = new ArrayList<>();
|
this.animations = new ArrayList<>();
|
||||||
this.animationLayers = new ArrayList<>();
|
this.animationLayers = new ArrayList<>();
|
||||||
|
this.lights = new ArrayList<>();
|
||||||
|
|
||||||
this.pivotPoint = new Vector2f();
|
this.pivotPoint = new Vector2f();
|
||||||
this.unitsPerMeter = 100.0f; // 默认100单位/米
|
this.unitsPerMeter = 100.0f; // 默认100单位/米
|
||||||
this.userData = new HashMap<>();
|
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) {
|
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<String, PhysicsSystem.PhysicsParticle> 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对象序列化数据
|
* 从Model2D对象序列化数据
|
||||||
*/
|
*/
|
||||||
@@ -90,6 +242,9 @@ public class ModelData implements Serializable {
|
|||||||
if (model.getMetadata() != null) {
|
if (model.getMetadata() != null) {
|
||||||
this.author = model.getMetadata().getAuthor();
|
this.author = model.getMetadata().getAuthor();
|
||||||
this.description = model.getMetadata().getDescription();
|
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);
|
serializeAnimationLayers(model);
|
||||||
|
|
||||||
|
// 序列化物理系统
|
||||||
|
serializePhysics(model);
|
||||||
|
|
||||||
|
// 序列化光源
|
||||||
|
serializeLights(model);
|
||||||
|
|
||||||
lastModifiedTime = System.currentTimeMillis();
|
lastModifiedTime = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void serializeParts(Model2D model) {
|
private void serializeParts(Model2D model) {
|
||||||
parts.clear();
|
parts.clear();
|
||||||
for (ModelPart part : model.getParts()) {
|
for (ModelPart part : model.getParts()) {
|
||||||
@@ -190,20 +352,145 @@ public class ModelData implements Serializable {
|
|||||||
model.addTexture(texture);
|
model.addTexture(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 然后创建所有网格(依赖纹理)
|
|
||||||
Map<String, Mesh2D> meshMap = deserializeMeshes(textureMap);
|
Map<String, Mesh2D> meshMap = deserializeMeshes(textureMap);
|
||||||
|
|
||||||
// 然后创建部件(依赖网格)
|
// 然后创建部件(依赖网格)
|
||||||
deserializeParts(model, meshMap);
|
Map<String, ModelPart> partMap = deserializeParts(model, meshMap);
|
||||||
|
|
||||||
// 最后创建参数
|
// 最后创建参数
|
||||||
deserializeParameters(model);
|
deserializeParameters(model);
|
||||||
|
|
||||||
// 创建动画层
|
// 创建动画层
|
||||||
deserializeAnimationLayers(model);
|
deserializeAnimationLayers(model);
|
||||||
|
|
||||||
|
// 反序列化物理系统
|
||||||
|
deserializePhysics(model, partMap);
|
||||||
|
|
||||||
|
// 反序列化光源
|
||||||
|
deserializeLights(model);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void deserializePhysics(Model2D model, Map<String, ModelPart> 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<ParticleData> particles = physicsParticles != null ? physicsParticles : new ArrayList<>();
|
||||||
|
List<SpringData> springs = physicsSprings != null ? physicsSprings : new ArrayList<>();
|
||||||
|
List<ConstraintData> constraints = physicsConstraints != null ? physicsConstraints : new ArrayList<>();
|
||||||
|
List<ColliderData> colliders = physicsColliders != null ? physicsColliders : new ArrayList<>();
|
||||||
|
|
||||||
|
// 创建粒子
|
||||||
|
Map<String, PhysicsSystem.PhysicsParticle> 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<String, ModelPart> deserializeParts(Model2D model, Map<String, Mesh2D> meshMap) {
|
||||||
|
Map<String, ModelPart> 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<String, Texture> deserializeTextures() {
|
private Map<String, Texture> deserializeTextures() {
|
||||||
Map<String, Texture> textureMap = new HashMap<>();
|
Map<String, Texture> textureMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -272,35 +559,6 @@ public class ModelData implements Serializable {
|
|||||||
return meshMap;
|
return meshMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deserializeParts(Model2D model, Map<String, Mesh2D> meshMap) {
|
|
||||||
// 先创建所有部件
|
|
||||||
Map<String, ModelPart> partMap = new HashMap<>();
|
|
||||||
for (PartData partData : parts) {
|
|
||||||
ModelPart part = partData.toModelPart(meshMap);
|
|
||||||
partMap.put(part.getName(), part);
|
|
||||||
model.addPart(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 然后建立父子关系
|
|
||||||
for (PartData partData : parts) {
|
|
||||||
if (partData.parentName != null && !partData.parentName.isEmpty()) {
|
|
||||||
ModelPart child = partMap.get(partData.name);
|
|
||||||
ModelPart parent = partMap.get(partData.parentName);
|
|
||||||
if (parent != null && child != null) {
|
|
||||||
parent.addChild(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置根部件
|
|
||||||
for (PartData partData : parts) {
|
|
||||||
if (partData.parentName == null || partData.parentName.isEmpty()) {
|
|
||||||
model.setRootPart(partMap.get(partData.name));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deserializeParameters(Model2D model) {
|
private void deserializeParameters(Model2D model) {
|
||||||
for (ParameterData paramData : parameters) {
|
for (ParameterData paramData : parameters) {
|
||||||
AnimationParameter param = paramData.toAnimationParameter();
|
AnimationParameter param = paramData.toAnimationParameter();
|
||||||
@@ -454,24 +712,25 @@ public class ModelData implements Serializable {
|
|||||||
copy.creationTime = System.currentTimeMillis();
|
copy.creationTime = System.currentTimeMillis();
|
||||||
copy.lastModifiedTime = copy.creationTime;
|
copy.lastModifiedTime = copy.creationTime;
|
||||||
|
|
||||||
for (PartData part : this.parts) {
|
// 拷贝核心数据
|
||||||
copy.parts.add(part.copy());
|
for (PartData part : this.parts) copy.parts.add(part.copy());
|
||||||
}
|
for (MeshData mesh : this.meshes) copy.meshes.add(mesh.copy());
|
||||||
for (MeshData mesh : this.meshes) {
|
for (TextureData tex : this.textures) copy.textures.add(tex.copy());
|
||||||
copy.meshes.add(mesh.copy());
|
for (ParameterData param : this.parameters) copy.parameters.add(param.copy());
|
||||||
}
|
for (AnimationLayerData layer : this.animationLayers) copy.animationLayers.add(layer.copy());
|
||||||
for (TextureData texture : this.textures) {
|
for (LightSourceData light : this.lights) copy.lights.add(light.copy());
|
||||||
copy.textures.add(texture.copy());
|
|
||||||
}
|
// 拷贝物理系统
|
||||||
for (ParameterData param : this.parameters) {
|
for (ParticleData p : this.physicsParticles) copy.physicsParticles.add(p.copy());
|
||||||
copy.parameters.add(param.copy());
|
for (SpringData s : this.physicsSprings) copy.physicsSprings.add(s.copy());
|
||||||
}
|
for (ConstraintData c : this.physicsConstraints) copy.physicsConstraints.add(c.copy());
|
||||||
for (AnimationData anim : this.animations) {
|
for (ColliderData c : this.physicsColliders) copy.physicsColliders.add(c.copy());
|
||||||
copy.animations.add(anim.copy());
|
|
||||||
}
|
copy.physicsGravityX = this.physicsGravityX;
|
||||||
for (AnimationLayerData layer : this.animationLayers) {
|
copy.physicsGravityY = this.physicsGravityY;
|
||||||
copy.animationLayers.add(layer.copy());
|
copy.physicsAirResistance = this.physicsAirResistance;
|
||||||
}
|
copy.physicsTimeScale = this.physicsTimeScale;
|
||||||
|
copy.physicsEnabled = this.physicsEnabled;
|
||||||
|
|
||||||
copy.pivotPoint = new Vector2f(this.pivotPoint);
|
copy.pivotPoint = new Vector2f(this.pivotPoint);
|
||||||
copy.unitsPerMeter = this.unitsPerMeter;
|
copy.unitsPerMeter = this.unitsPerMeter;
|
||||||
@@ -534,9 +793,6 @@ public class ModelData implements Serializable {
|
|||||||
|
|
||||||
// ==================== 内部数据类 ====================
|
// ==================== 内部数据类 ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* 部件数据
|
|
||||||
*/
|
|
||||||
public static class PartData implements Serializable {
|
public static class PartData implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@@ -550,6 +806,9 @@ public class ModelData implements Serializable {
|
|||||||
public List<String> meshNames;
|
public List<String> meshNames;
|
||||||
public Map<String, String> userData;
|
public Map<String, String> userData;
|
||||||
|
|
||||||
|
// 新增:保存变形器数据
|
||||||
|
public List<DeformerData> deformers;
|
||||||
|
|
||||||
public PartData() {
|
public PartData() {
|
||||||
this.position = new Vector2f();
|
this.position = new Vector2f();
|
||||||
this.rotation = 0.0f;
|
this.rotation = 0.0f;
|
||||||
@@ -558,6 +817,7 @@ public class ModelData implements Serializable {
|
|||||||
this.opacity = 1.0f;
|
this.opacity = 1.0f;
|
||||||
this.meshNames = new ArrayList<>();
|
this.meshNames = new ArrayList<>();
|
||||||
this.userData = new HashMap<>();
|
this.userData = new HashMap<>();
|
||||||
|
this.deformers = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartData(ModelPart part) {
|
public PartData(ModelPart part) {
|
||||||
@@ -574,6 +834,22 @@ public class ModelData implements Serializable {
|
|||||||
this.meshNames.add(mesh.getName());
|
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<String, String> map = new HashMap<>();
|
||||||
|
d.serialization(map); // 让变形器把自己的状态写入 map
|
||||||
|
dd.properties = map;
|
||||||
|
this.deformers.add(dd);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略单个变形器序列化错误,避免整个保存失败
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置父级名称
|
// 设置父级名称
|
||||||
if (part.getParent() != null) {
|
if (part.getParent() != null) {
|
||||||
this.parentName = part.getParent().getName();
|
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;
|
return part;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,8 +920,31 @@ public class ModelData implements Serializable {
|
|||||||
copy.opacity = this.opacity;
|
copy.opacity = this.opacity;
|
||||||
copy.meshNames = new ArrayList<>(this.meshNames);
|
copy.meshNames = new ArrayList<>(this.meshNames);
|
||||||
copy.userData = new HashMap<>(this.userData);
|
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;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:序列化变形器数据结构
|
||||||
|
*/
|
||||||
|
public static class DeformerData implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
public String type; // 例如 "VertexDeformer"
|
||||||
|
public String name;
|
||||||
|
public Map<String, String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参数数据
|
* 参数数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
|||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 旋转变形器 - 围绕中心点旋转顶点
|
* 旋转变形器 - 围绕中心点旋转顶点
|
||||||
@@ -79,6 +80,23 @@ public class RotationDeformer extends Deformer {
|
|||||||
this.currentAngle = baseAngle;
|
this.currentAngle = baseAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialization(Map<String, String> map) {
|
||||||
|
map.put("baseAngle", String.valueOf(baseAngle));
|
||||||
|
map.put("angleRange", String.valueOf(angleRange));
|
||||||
|
map.put("currentAngle", String.valueOf(currentAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(Map<String, String> 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
|
// Getter/Setter
|
||||||
public float getBaseAngle() { return baseAngle; }
|
public float getBaseAngle() { return baseAngle; }
|
||||||
public void setBaseAngle(float baseAngle) { this.baseAngle = baseAngle; }
|
public void setBaseAngle(float baseAngle) { this.baseAngle = baseAngle; }
|
||||||
|
|||||||
@@ -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.Deformer;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.util.SaveVector2f;
|
||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缩放变形器 - 围绕中心点缩放顶点
|
* 缩放变形器 - 围绕中心点缩放顶点
|
||||||
|
* @author tzdwindows 7
|
||||||
*/
|
*/
|
||||||
public class ScaleDeformer extends Deformer {
|
public class ScaleDeformer extends Deformer {
|
||||||
private Vector2f baseScale = new Vector2f(1.0f, 1.0f);
|
private Vector2f baseScale = new Vector2f(1.0f, 1.0f);
|
||||||
@@ -72,6 +75,24 @@ public class ScaleDeformer extends Deformer {
|
|||||||
this.currentScale.set(baseScale);
|
this.currentScale.set(baseScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialization(Map<String, String> map) {
|
||||||
|
map.put("baseScale", SaveVector2f.toString(baseScale));
|
||||||
|
map.put("scaleRange", SaveVector2f.toString(scaleRange));
|
||||||
|
map.put("currentScale", SaveVector2f.toString(currentScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(Map<String, String> 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
|
// Getter/Setter
|
||||||
public Vector2f getBaseScale() { return new Vector2f(baseScale); }
|
public Vector2f getBaseScale() { return new Vector2f(baseScale); }
|
||||||
public void setBaseScale(Vector2f baseScale) { this.baseScale.set(baseScale); }
|
public void setBaseScale(Vector2f baseScale) { this.baseScale.set(baseScale); }
|
||||||
|
|||||||
@@ -31,19 +31,7 @@ public class VertexDeformer extends Deformer {
|
|||||||
/**
|
/**
|
||||||
* 顶点变形数据内部类
|
* 顶点变形数据内部类
|
||||||
*/
|
*/
|
||||||
private static class VertexDeformation {
|
private record VertexDeformation(float originalX, float originalY, float targetX, float targetY) { }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加顶点变形目标
|
* 添加顶点变形目标
|
||||||
@@ -193,6 +181,87 @@ public class VertexDeformer extends Deformer {
|
|||||||
this.currentValue = 0.0f;
|
this.currentValue = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialization(Map<String, String> 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<String, String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置当前值并立即应用到指定网格
|
* 设置当前值并立即应用到指定网格
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
|||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 波浪变形器 - 创建波浪效果的顶点变形
|
* 波浪变形器 - 创建波浪效果的顶点变形
|
||||||
@@ -265,6 +266,27 @@ public class WaveDeformer extends Deformer {
|
|||||||
this.amplitude = 10.0f;
|
this.amplitude = 10.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialization(Map<String, String> 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<String, String> 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 float getTimeMultiplier() { return timeMultiplier; }
|
||||||
public void setTimeMultiplier(float timeMultiplier) { this.timeMultiplier = timeMultiplier; }
|
public void setTimeMultiplier(float timeMultiplier) { this.timeMultiplier = timeMultiplier; }
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,16 @@ public abstract class Deformer {
|
|||||||
*/
|
*/
|
||||||
public abstract void reset();
|
public abstract void reset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化参数
|
||||||
|
*/
|
||||||
|
public abstract void serialization(Map<String, String> map);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化参数
|
||||||
|
*/
|
||||||
|
public abstract void deserialize(Map<String, String> map);
|
||||||
|
|
||||||
// ==================== 参数驱动系统 ====================
|
// ==================== 参数驱动系统 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,13 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* 2D物理系统,用于处理模型的物理模拟
|
* 2D物理系统,用于处理模型的物理模拟
|
||||||
* 支持弹簧系统、碰撞检测、重力等物理效果
|
* 支持弹簧系统、碰撞检测、重力等物理效果
|
||||||
*
|
*
|
||||||
* @author tzdwindows 7
|
* 调整点:
|
||||||
|
* - 使用半隐式(symplectic)Euler 积分,替代之前的纯 Verlet(更稳定且直观)
|
||||||
|
* - 在约束迭代后同步速度(根据位置变化计算),避免约束把位置推回后速度不一致导致僵化
|
||||||
|
* - 在碰撞处理里同时更新位置与速度(冲量响应),并避免在碰撞后覆盖冲量
|
||||||
|
* - 约束迭代次数可调,布料等需要多个迭代(默认 3)
|
||||||
|
*
|
||||||
|
* @author tzdwindows 7 (modified)
|
||||||
*/
|
*/
|
||||||
public class PhysicsSystem {
|
public class PhysicsSystem {
|
||||||
// ==================== 物理参数 ====================
|
// ==================== 物理参数 ====================
|
||||||
@@ -37,10 +43,14 @@ public class PhysicsSystem {
|
|||||||
private int updateCount;
|
private int updateCount;
|
||||||
private float averageUpdateTime;
|
private float averageUpdateTime;
|
||||||
|
|
||||||
|
// ==================== 风力参数 ====================
|
||||||
|
private final Vector2f windForce;
|
||||||
|
private boolean windEnabled;
|
||||||
|
|
||||||
// ==================== 构造器 ====================
|
// ==================== 构造器 ====================
|
||||||
|
|
||||||
public PhysicsSystem() {
|
public PhysicsSystem() {
|
||||||
this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力
|
this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力(单位可调)
|
||||||
this.airResistance = 0.1f;
|
this.airResistance = 0.1f;
|
||||||
this.timeScale = 1.0f;
|
this.timeScale = 1.0f;
|
||||||
this.enabled = true;
|
this.enabled = true;
|
||||||
@@ -58,6 +68,9 @@ public class PhysicsSystem {
|
|||||||
|
|
||||||
this.updateCount = 0;
|
this.updateCount = 0;
|
||||||
this.averageUpdateTime = 0.0f;
|
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);
|
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) {
|
private void updatePhysics(float deltaTime) {
|
||||||
// 清除所有力
|
// 1) 清除上一帧的力累加器(将在本帧重新累加)
|
||||||
for (PhysicsParticle particle : particles.values()) {
|
for (PhysicsParticle particle : particles.values()) {
|
||||||
particle.clearForces();
|
particle.clearForces();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用重力
|
// 2) 应用全局力(重力、风)
|
||||||
applyGravity();
|
applyGravity();
|
||||||
|
applyWind();
|
||||||
|
|
||||||
// 应用弹簧力
|
// 3) 弹簧加入力
|
||||||
for (PhysicsSpring spring : springs) {
|
for (PhysicsSpring spring : springs) {
|
||||||
spring.applyForce(deltaTime);
|
spring.applyForce(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新粒子运动
|
// 4) 通过半隐式 Euler 更新速度与位置
|
||||||
for (PhysicsParticle particle : particles.values()) {
|
for (PhysicsParticle particle : particles.values()) {
|
||||||
if (particle.isMovable()) {
|
if (particle.isMovable()) {
|
||||||
particle.update(deltaTime);
|
particle.update(deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用约束
|
// 5) 对约束做若干次迭代(布料等需要多次投影)
|
||||||
|
int constraintIterations = 3; // 可调(1-5)——越多布料越不僵化,但耗时增加
|
||||||
|
for (int iter = 0; iter < constraintIterations; iter++) {
|
||||||
for (PhysicsConstraint constraint : constraints) {
|
for (PhysicsConstraint constraint : constraints) {
|
||||||
|
if (constraint.isEnabled()) {
|
||||||
constraint.apply(deltaTime);
|
constraint.apply(deltaTime);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理碰撞
|
// 每次迭代后处理静态碰撞体的穿透(避免强穿透)
|
||||||
handleCollisions(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 应用空气阻力
|
// 6) 在约束迭代后,根据位置变化同步速度,避免约束把位置推回后速度不一致导致僵化
|
||||||
|
syncVelocitiesFromPositions(deltaTime);
|
||||||
|
|
||||||
|
// 7) 粒子间碰撞(位置修正 + 速度冲量)
|
||||||
|
handleParticleCollisions(deltaTime);
|
||||||
|
|
||||||
|
// 8) 应用空气阻力(现在直接作用于速度)
|
||||||
applyAirResistance(deltaTime);
|
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()) {
|
for (PhysicsParticle particle : particles.values()) {
|
||||||
if (particle.isMovable()) {
|
if (particle.isMovable() && particle.isAffectedByWind()) {
|
||||||
Vector2f velocity = particle.getVelocity();
|
particle.addForce(new Vector2f(windForce));
|
||||||
Vector2f dragForce = new Vector2f(velocity).mul(-airResistance);
|
|
||||||
particle.addForce(dragForce);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理碰撞
|
* 应用空气阻力(对速度的阻尼)
|
||||||
*/
|
*/
|
||||||
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()) {
|
for (PhysicsParticle particle : particles.values()) {
|
||||||
if (!particle.isMovable()) continue;
|
if (!particle.isMovable()) continue;
|
||||||
|
|
||||||
for (PhysicsCollider collider : colliders) {
|
particle.velocity.mul(factor);
|
||||||
if (collider.isEnabled() && collider.collidesWith(particle)) {
|
|
||||||
collider.resolveCollision(particle, deltaTime);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 粒子间碰撞(简单实现)
|
|
||||||
handleParticleCollisions(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理粒子间碰撞
|
* 粒子间碰撞(位置修正 + 速度冲量)
|
||||||
*/
|
*/
|
||||||
private void handleParticleCollisions(float deltaTime) {
|
private void handleParticleCollisions(float deltaTime) {
|
||||||
List<PhysicsParticle> particleList = new ArrayList<>(particles.values());
|
List<PhysicsParticle> particleList = new ArrayList<>(particles.values());
|
||||||
@@ -336,34 +421,45 @@ public class PhysicsSystem {
|
|||||||
PhysicsParticle p2 = particleList.get(j);
|
PhysicsParticle p2 = particleList.get(j);
|
||||||
if (!p2.isMovable()) continue;
|
if (!p2.isMovable()) continue;
|
||||||
|
|
||||||
// 简单圆形碰撞检测
|
Vector2f pos1 = p1.getPosition();
|
||||||
Vector2f delta = new Vector2f(p2.getPosition()).sub(p1.getPosition());
|
Vector2f pos2 = p2.getPosition();
|
||||||
float distance = delta.length();
|
Vector2f delta = new Vector2f(pos2).sub(pos1);
|
||||||
float minDistance = p1.getRadius() + p2.getRadius();
|
float dist = delta.length();
|
||||||
|
float minDist = p1.getRadius() + p2.getRadius();
|
||||||
|
|
||||||
if (distance < minDistance && distance > 0.001f) {
|
if (dist < minDist && dist > 1e-6f) {
|
||||||
// 碰撞响应
|
Vector2f normal = new Vector2f(delta).div(dist);
|
||||||
Vector2f normal = new Vector2f(delta).div(distance);
|
float penetration = minDist - dist;
|
||||||
float overlap = minDistance - distance;
|
|
||||||
|
|
||||||
// 分离粒子
|
// 按逆质量比例分离位置(静态/不可移动的情况 inverseMass=0)
|
||||||
Vector2f separation = new Vector2f(normal).mul(overlap * 0.5f);
|
float invM1 = p1.getInverseMass();
|
||||||
p1.getPosition().sub(separation);
|
float invM2 = p2.getInverseMass();
|
||||||
p2.getPosition().add(separation);
|
float invSum = invM1 + invM2;
|
||||||
|
if (invSum <= 0.0f) continue;
|
||||||
|
|
||||||
// 简单的速度响应
|
Vector2f correction = new Vector2f(normal).mul(penetration / invSum);
|
||||||
Vector2f relativeVelocity = new Vector2f(p2.getVelocity()).sub(p1.getVelocity());
|
p1.translatePosition(new Vector2f(correction).mul(-invM1));
|
||||||
float velocityAlongNormal = relativeVelocity.dot(normal);
|
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; // 弹性系数
|
// 如果朝向彼此(velAlongNormal < 0)才处理冲量
|
||||||
float impulseMagnitude = -(1 + restitution) * velocityAlongNormal;
|
if (velAlongNormal < 0.0f) {
|
||||||
impulseMagnitude /= p1.getInverseMass() + p2.getInverseMass();
|
float restitution = 0.5f; // 可调
|
||||||
|
float j2 = -(1.0f + restitution) * velAlongNormal;
|
||||||
|
j2 /= invSum;
|
||||||
|
|
||||||
Vector2f impulse = new Vector2f(normal).mul(impulseMagnitude);
|
Vector2f impulse = new Vector2f(normal).mul(j2);
|
||||||
p1.getVelocity().sub(new Vector2f(impulse).mul(p1.getInverseMass()));
|
// 更新速度
|
||||||
p2.getVelocity().add(new Vector2f(impulse).mul(p2.getInverseMass()));
|
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 {
|
public static class PhysicsParticle {
|
||||||
private final String id;
|
private final String id;
|
||||||
@@ -544,9 +640,10 @@ public class PhysicsSystem {
|
|||||||
private final Vector2f forceAccumulator;
|
private final Vector2f forceAccumulator;
|
||||||
private final float mass;
|
private final float mass;
|
||||||
private final float inverseMass;
|
private final float inverseMass;
|
||||||
private final float radius;
|
private float radius;
|
||||||
private boolean movable;
|
private boolean movable;
|
||||||
private boolean affectedByGravity;
|
private boolean affectedByGravity;
|
||||||
|
private boolean affectedByWind;
|
||||||
private Object userData;
|
private Object userData;
|
||||||
|
|
||||||
public PhysicsParticle(String id, Vector2f position, float mass) {
|
public PhysicsParticle(String id, Vector2f position, float mass) {
|
||||||
@@ -556,36 +653,52 @@ public class PhysicsSystem {
|
|||||||
this.velocity = new Vector2f();
|
this.velocity = new Vector2f();
|
||||||
this.acceleration = new Vector2f();
|
this.acceleration = new Vector2f();
|
||||||
this.forceAccumulator = new Vector2f();
|
this.forceAccumulator = new Vector2f();
|
||||||
this.mass = Math.max(0.001f, 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.inverseMass = 1.0f / this.mass;
|
||||||
|
}
|
||||||
this.radius = 2.0f; // 默认半径
|
this.radius = 2.0f; // 默认半径
|
||||||
this.movable = true;
|
this.movable = true;
|
||||||
this.affectedByGravity = true;
|
this.affectedByGravity = true;
|
||||||
|
this.affectedByWind = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2f getPreviousPosition() {
|
public Vector2f getPreviousPosition() {
|
||||||
return new Vector2f(previousPosition);
|
return new Vector2f(previousPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 半隐式 Euler 更新:
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
public void update(float deltaTime) {
|
public void update(float deltaTime) {
|
||||||
if (!movable) return;
|
if (!movable) return;
|
||||||
|
|
||||||
// Verlet 积分法
|
|
||||||
Vector2f temp = new Vector2f(position);
|
|
||||||
|
|
||||||
// 计算加速度: a = F / m
|
// 计算加速度: a = F / m
|
||||||
acceleration.set(forceAccumulator).mul(inverseMass);
|
acceleration.set(forceAccumulator).mul(inverseMass);
|
||||||
|
|
||||||
// Verlet 位置更新: x_{n+1} = 2x_n - x_{n-1} + a * dt^2
|
// v_{n+1} = v_n + a * dt
|
||||||
position.set(2.0f * position.x - previousPosition.x + acceleration.x * deltaTime * deltaTime,
|
velocity.add(new Vector2f(acceleration).mul(deltaTime));
|
||||||
2.0f * position.y - previousPosition.y + acceleration.y * deltaTime * deltaTime);
|
|
||||||
|
|
||||||
previousPosition.set(temp);
|
// previousPosition 存放上一帧位置,用于 syncVelocities 时计算
|
||||||
|
previousPosition.set(position);
|
||||||
|
|
||||||
// 更新速度(用于显示和其他计算)
|
// x_{n+1} = x_n + v_{n+1} * dt
|
||||||
velocity.set(position).sub(previousPosition).div(deltaTime);
|
position.add(new Vector2f(velocity).mul(deltaTime));
|
||||||
|
|
||||||
|
// 清除力累加器(下一帧会重新累加)
|
||||||
|
forceAccumulator.set(0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addForce(Vector2f force) {
|
public void addForce(Vector2f force) {
|
||||||
|
// 如果是无限质量(inverseMass == 0),不累加力
|
||||||
|
if (inverseMass == 0.0f) return;
|
||||||
forceAccumulator.add(force);
|
forceAccumulator.add(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,23 +706,56 @@ public class PhysicsSystem {
|
|||||||
forceAccumulator.set(0.0f, 0.0f);
|
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 方法
|
// Getter/Setter 方法
|
||||||
public String getId() { return id; }
|
public String getId() { return id; }
|
||||||
public Vector2f getPosition() { return new Vector2f(position); }
|
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 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 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 getInverseMass() { return inverseMass; }
|
||||||
public float getRadius() { return radius; }
|
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 boolean isMovable() { return movable; }
|
||||||
public void setMovable(boolean movable) { this.movable = movable; }
|
public void setMovable(boolean movable) { this.movable = movable; }
|
||||||
public boolean isAffectedByGravity() { return affectedByGravity; }
|
public boolean isAffectedByGravity() { return affectedByGravity; }
|
||||||
public void setAffectedByGravity(boolean affectedByGravity) { this.affectedByGravity = affectedByGravity; }
|
public void setAffectedByGravity(boolean affectedByGravity) { this.affectedByGravity = affectedByGravity; }
|
||||||
public Object getUserData() { return userData; }
|
public Object getUserData() { return userData; }
|
||||||
public void setUserData(Object userData) { this.userData = 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());
|
Vector2f delta = new Vector2f(particleB.getPosition()).sub(particleA.getPosition());
|
||||||
float currentLength = delta.length();
|
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)
|
// 胡克定律: F = -k * (currentLength - restLength)
|
||||||
float stretch = 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());
|
Vector2f relativeVelocity = new Vector2f(particleB.getVelocity()).sub(particleA.getVelocity());
|
||||||
float velocityAlongSpring = relativeVelocity.dot(delta) / currentLength;
|
float velocityAlongSpring = relativeVelocity.dot(dir);
|
||||||
Vector2f dampingForce = new Vector2f(delta).normalize().mul(damping * velocityAlongSpring);
|
Vector2f dampingForce = new Vector2f(dir).mul(damping * velocityAlongSpring);
|
||||||
|
|
||||||
// 应用合力
|
// 合力作用在两端(方向相反)
|
||||||
Vector2f totalForce = new Vector2f(springForce).sub(dampingForce);
|
Vector2f totalForce = new Vector2f(springForce).sub(dampingForce);
|
||||||
|
|
||||||
if (particleA.isMovable()) {
|
if (particleA.isMovable()) {
|
||||||
particleA.addForce(totalForce);
|
particleA.addForce(new Vector2f(totalForce).mul(-1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (particleB.isMovable()) {
|
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 delta = new Vector2f(targetPosition).sub(currentPos);
|
||||||
Vector2f correction = new Vector2f(delta).mul(strength);
|
Vector2f correction = new Vector2f(delta).mul(strength);
|
||||||
|
|
||||||
|
// 直接设置位置(后续会同步速度)
|
||||||
particle.setPosition(new Vector2f(currentPos).add(correction));
|
particle.setPosition(new Vector2f(currentPos).add(correction));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,18 +946,28 @@ public class PhysicsSystem {
|
|||||||
public void resolveCollision(PhysicsParticle particle, float deltaTime) {
|
public void resolveCollision(PhysicsParticle particle, float deltaTime) {
|
||||||
Vector2f toParticle = new Vector2f(particle.getPosition()).sub(center);
|
Vector2f toParticle = new Vector2f(particle.getPosition()).sub(center);
|
||||||
float distance = toParticle.length();
|
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) {
|
if (overlap > 0.0001f) {
|
||||||
// 分离
|
// 如果中心距离几乎为零,选一个任意法线
|
||||||
Vector2f normal = new Vector2f(toParticle).div(distance);
|
Vector2f normal;
|
||||||
particle.getPosition().add(new Vector2f(normal).mul(overlap));
|
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) {
|
if (dot < 0) {
|
||||||
Vector2f reflection = new Vector2f(normal).mul(2.0f * dot);
|
float restitution = 0.6f;
|
||||||
particle.getVelocity().sub(reflection).mul(0.8f); // 能量损失
|
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 {
|
public static class RectangleCollider implements PhysicsCollider {
|
||||||
private final String id;
|
private final String id;
|
||||||
@@ -865,27 +1025,35 @@ public class PhysicsSystem {
|
|||||||
float bottom = center.y - height / 2;
|
float bottom = center.y - height / 2;
|
||||||
float top = center.y + height / 2;
|
float top = center.y + height / 2;
|
||||||
|
|
||||||
// 计算最近边界
|
// 计算最近点
|
||||||
float closestX = Math.max(left, Math.min(particlePos.x, right));
|
float closestX = Math.max(left, Math.min(particlePos.x, right));
|
||||||
float closestY = Math.max(bottom, Math.min(particlePos.y, top));
|
float closestY = Math.max(bottom, Math.min(particlePos.y, top));
|
||||||
|
|
||||||
Vector2f closestPoint = new Vector2f(closestX, closestY);
|
Vector2f closestPoint = new Vector2f(closestX, closestY);
|
||||||
Vector2f normal = new Vector2f(particlePos).sub(closestPoint);
|
Vector2f normal = new Vector2f(particlePos).sub(closestPoint);
|
||||||
|
|
||||||
if (normal.lengthSquared() > 0.001f) {
|
float dist = normal.length();
|
||||||
normal.normalize();
|
float overlap = particle.getRadius() - dist;
|
||||||
|
|
||||||
// 分离粒子
|
if (dist < 0.0001f) {
|
||||||
float overlap = particle.getRadius() - normal.length();
|
// 粒子在矩形内部且中心重合,选 Y 方向
|
||||||
if (overlap > 0) {
|
normal.set(0.0f, 1.0f);
|
||||||
particle.getPosition().add(new Vector2f(normal).mul(overlap));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;i<w*h;i++) buf.put(r).put(g).put(b).put(a);
|
||||||
|
buf.flip();
|
||||||
|
Texture t = new Texture("solid_" + rgba, w, h, Texture.TextureFormat.RGBA, buf);
|
||||||
|
MemoryUtil.memFree(buf);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Texture createHeadTexture() {
|
||||||
|
int width = 64, height = 64;
|
||||||
|
int[] pixels = new int[width * height];
|
||||||
|
for(int y=0;y<height;y++) {
|
||||||
|
for(int x=0;x<width;x++) {
|
||||||
|
float dx = (x - width/2f)/(width/2f);
|
||||||
|
float dy = (y - height/2f)/(height/2f);
|
||||||
|
float dist = (float)Math.sqrt(dx*dx + dy*dy);
|
||||||
|
int alpha = dist>1.0f?0:255;
|
||||||
|
int r = (int)(240*(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ import com.chuangzhou.vivid2D.render.ModelRender;
|
|||||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
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 com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||||
|
import org.joml.Vector2f;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.lwjgl.glfw.GLFWErrorCallback;
|
import org.lwjgl.glfw.GLFWErrorCallback;
|
||||||
import org.lwjgl.glfw.GLFWVidMode;
|
import org.lwjgl.glfw.GLFWVidMode;
|
||||||
@@ -105,6 +107,13 @@ public class ModelRenderTest {
|
|||||||
private void createTestModel() {
|
private void createTestModel() {
|
||||||
testModel = new Model2D("Humanoid");
|
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 放在屏幕中心
|
// body 放在屏幕中心
|
||||||
ModelPart body = testModel.createPart("body");
|
ModelPart body = testModel.createPart("body");
|
||||||
body.setPosition(0, 0);
|
body.setPosition(0, 0);
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ package com.chuangzhou.vivid2D.test;
|
|||||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
import com.chuangzhou.vivid2D.render.model.transform.WaveDeformer;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.AnimationLayer;
|
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||||
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 org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.lwjgl.glfw.GLFWErrorCallback;
|
import org.lwjgl.glfw.GLFWErrorCallback;
|
||||||
@@ -46,7 +42,8 @@ public class ModelTest {
|
|||||||
testPhysicsSystem();
|
testPhysicsSystem();
|
||||||
testComplexTransformations();
|
testComplexTransformations();
|
||||||
testPerformance();
|
testPerformance();
|
||||||
|
Model2D model = createTestModel();
|
||||||
|
printModelState(model);
|
||||||
} finally {
|
} finally {
|
||||||
// Cleanup OpenGL
|
// Cleanup OpenGL
|
||||||
cleanupOpenGL();
|
cleanupOpenGL();
|
||||||
@@ -55,6 +52,231 @@ public class ModelTest {
|
|||||||
System.out.println("=== Model2D Extended Save and Load Test Complete ===");
|
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
|
* Initialize OpenGL context for texture testing
|
||||||
*/
|
*/
|
||||||
@@ -586,6 +808,8 @@ public class ModelTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to print part hierarchy
|
* Utility method to print part hierarchy
|
||||||
*/
|
*/
|
||||||
|
|||||||
682
src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java
Normal file
682
src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java
Normal file
@@ -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<ModelPart> 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<PhysicsSystem.PhysicsParticle> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
265
vivid2DApi.md
Normal file
265
vivid2DApi.md
Normal file
@@ -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<String, Float> paramValues) {
|
||||||
|
for (Map.Entry<String, Float> 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<String, Model2D> 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 渲染引擎。
|
||||||
Reference in New Issue
Block a user