feat(model): 实现动画层数据序列化与纹理管理增强

- 添加 AnimationLayerData 类用于动画层的序列化支持- 增强 Model2D 的 addTexture 方法,添加空值检查和重复纹理处理
- 在 ModelData 中添加动画层序列化与反序列化逻辑
- 扩展 TextureData 结构以支持完整纹理参数和元数据- 改进纹理反序列化过程,添加错误处理和后备纹理创建- 更新模型测试用例以验证新功能和修复的问题
- 优化网格序列化逻辑,避免重复序列化相同网格- 添加日志记录支持以提高调试能力

重要
- 完全实现了模型的保存和加载(贴图待测试)
This commit is contained in:
tzdwindows 7
2025-10-08 21:02:46 +08:00
parent 424c00ede9
commit 22af92cd84
6 changed files with 1773 additions and 24 deletions

View File

@@ -168,7 +168,23 @@ public class Model2D {
// ==================== 纹理管理 ====================
public void addTexture(Texture texture) {
textures.put(texture.getName(), texture);
if (texture == null) {
throw new IllegalArgumentException("Texture cannot be null");
}
String textureName = texture.getName();
if (textureName == null || textureName.trim().isEmpty()) {
throw new IllegalArgumentException("Texture name cannot be null or empty");
}
if (textures.containsKey(textureName)) {
Texture oldTexture = textures.get(textureName);
if (oldTexture != null && oldTexture != texture) {
oldTexture.dispose();
}
}
textures.put(textureName, texture);
}
public Texture getTexture(String name) {
@@ -291,6 +307,14 @@ public class Model2D {
}
}
/**
* 设置动画层列表(用于反序列化)
*/
public void setAnimationLayers(List<AnimationLayer> animationLayers) {
this.animationLayers.clear();
this.animationLayers.addAll(animationLayers);
}
/**
* 保存模型到压缩文件
*/

View File

@@ -2,6 +2,8 @@ package com.chuangzhou.vivid2D.render.model;
import com.chuangzhou.vivid2D.render.model.util.*;
import org.joml.Vector2f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
@@ -15,6 +17,7 @@ import java.util.zip.GZIPOutputStream;
* @author tzdwindows 7
*/
public class ModelData implements Serializable {
private static Logger logger = LoggerFactory.getLogger(ModelData.class);
private static final long serialVersionUID = 1L;
// ==================== 模型元数据 ====================
@@ -32,6 +35,7 @@ public class ModelData implements Serializable {
private List<TextureData> textures;
private List<ParameterData> parameters;
private List<AnimationData> animations;
private List<AnimationLayerData> animationLayers;
// ==================== 模型设置 ====================
private Vector2f pivotPoint;
@@ -56,6 +60,7 @@ public class ModelData implements Serializable {
this.textures = new ArrayList<>();
this.parameters = new ArrayList<>();
this.animations = new ArrayList<>();
this.animationLayers = new ArrayList<>();
this.pivotPoint = new Vector2f();
this.unitsPerMeter = 100.0f; // 默认100单位/米
@@ -99,6 +104,9 @@ public class ModelData implements Serializable {
// 序列化参数
serializeParameters(model);
// 序列化动画层
serializeAnimationLayers(model);
lastModifiedTime = System.currentTimeMillis();
}
@@ -111,8 +119,22 @@ public class ModelData implements Serializable {
private void serializeMeshes(Model2D model) {
meshes.clear();
// 首先序列化模型级别的所有网格
for (Mesh2D mesh : model.getMeshes()) {
meshes.add(new MeshData(mesh));
if (!meshExists(mesh.getName())) {
meshes.add(new MeshData(mesh));
}
}
// 然后序列化所有部件中的网格
for (ModelPart part : model.getParts()) {
for (Mesh2D mesh : part.getMeshes()) {
// 检查是否已经序列化过(避免重复)
if (!meshExists(mesh.getName())) {
meshes.add(new MeshData(mesh));
}
}
}
}
@@ -130,6 +152,22 @@ public class ModelData implements Serializable {
}
}
private void serializeAnimationLayers(Model2D model) {
animationLayers.clear();
for (AnimationLayer layer : model.getAnimationLayers()) {
animationLayers.add(new AnimationLayerData(layer));
}
}
private void deserializeAnimationLayers(Model2D model) {
List<AnimationLayer> layers = new ArrayList<>();
for (AnimationLayerData layerData : animationLayers) {
AnimationLayer layer = layerData.toAnimationLayer();
layers.add(layer);
}
model.setAnimationLayers(layers);
}
/**
* 反序列化到Model2D对象
*/
@@ -147,6 +185,11 @@ public class ModelData implements Serializable {
// 先创建所有纹理
Map<String, Texture> textureMap = deserializeTextures();
// === 关键修复:将纹理添加到模型中 ===
for (Texture texture : textureMap.values()) {
model.addTexture(texture);
}
// 然后创建所有网格(依赖纹理)
Map<String, Mesh2D> meshMap = deserializeMeshes(textureMap);
@@ -156,32 +199,75 @@ public class ModelData implements Serializable {
// 最后创建参数
deserializeParameters(model);
// 创建动画层
deserializeAnimationLayers(model);
return model;
}
private Map<String, Texture> deserializeTextures() {
Map<String, Texture> textureMap = new HashMap<>();
for (TextureData textureData : textures) {
Texture texture = textureData.toTexture();
textureMap.put(texture.getName(), texture);
try {
Texture texture = textureData.toTexture();
if (texture != null) {
textureMap.put(texture.getName(), texture);
}
} catch (Exception e) {
logger.error("Error creating texture '{}': {}", textureData.name, e.getMessage());
e.printStackTrace();
Texture fallbackTexture = createFallbackTexture(textureData.name, textureData.width, textureData.height);
if (fallbackTexture != null) {
textureMap.put(textureData.name, fallbackTexture);
}
}
}
return textureMap;
}
/**
* 创建后备纹理
*/
private Texture createFallbackTexture(String name, int width, int height) {
try {
int color;
if (name.contains("body")) {
color = 0xFFFF0000;
} else if (name.contains("head")) {
color = 0xFF00FF00;
} else if (name.contains("checker")) {
return Texture.createCheckerboard(name + "_fallback", width, height, 16, 0xFFFFFFFF, 0xFF0000FF);
} else {
color = 0xFF0000FF;
}
return Texture.createSolidColor(name + "_fallback", width, height, color);
} catch (Exception e) {
System.err.println("Failed to create fallback texture: " + e.getMessage());
return null;
}
}
private Map<String, Mesh2D> deserializeMeshes(Map<String, Texture> textureMap) {
Map<String, Mesh2D> meshMap = new HashMap<>();
for (MeshData meshData : meshes) {
Mesh2D mesh = meshData.toMesh2D();
try {
Mesh2D mesh = meshData.toMesh2D();
// 设置纹理
if (meshData.textureName != null) {
Texture texture = textureMap.get(meshData.textureName);
if (texture != null) {
mesh.setTexture(texture);
// 设置纹理
if (meshData.textureName != null) {
Texture texture = textureMap.get(meshData.textureName);
if (texture != null) {
mesh.setTexture(texture);
} else {
logger.error("Texture not found for mesh '{}': {}", meshData.name, meshData.textureName);
}
}
meshMap.put(mesh.getName(), mesh);
} catch (Exception e) {
logger.error("Error creating mesh '{}': {}", meshData.name, e.getMessage());
}
meshMap.put(mesh.getName(), mesh);
}
return meshMap;
}
@@ -368,7 +454,6 @@ public class ModelData implements Serializable {
copy.creationTime = System.currentTimeMillis();
copy.lastModifiedTime = copy.creationTime;
// 深拷贝集合
for (PartData part : this.parts) {
copy.parts.add(part.copy());
}
@@ -384,6 +469,9 @@ public class ModelData implements Serializable {
for (AnimationData anim : this.animations) {
copy.animations.add(anim.copy());
}
for (AnimationLayerData layer : this.animationLayers) {
copy.animationLayers.add(layer.copy());
}
copy.pivotPoint = new Vector2f(this.pivotPoint);
copy.unitsPerMeter = this.unitsPerMeter;
@@ -591,21 +679,368 @@ public class ModelData implements Serializable {
public int width;
public int height;
public Texture.TextureFormat format;
public Texture.TextureFilter minFilter;
public Texture.TextureFilter magFilter;
public Texture.TextureWrap wrapS;
public Texture.TextureWrap wrapT;
public boolean mipmapsEnabled;
public Map<String, String> metadata;
public TextureData() {}
public TextureData() {
this.minFilter = Texture.TextureFilter.LINEAR;
this.magFilter = Texture.TextureFilter.LINEAR;
this.wrapS = Texture.TextureWrap.CLAMP_TO_EDGE;
this.wrapT = Texture.TextureWrap.CLAMP_TO_EDGE;
this.mipmapsEnabled = false;
this.metadata = new HashMap<>();
}
public TextureData(Texture texture) {
this();
this.name = texture.getName();
this.width = texture.getWidth();
this.height = texture.getHeight();
this.format = texture.getFormat();
this.minFilter = texture.getMinFilter();
this.magFilter = texture.getMagFilter();
this.wrapS = texture.getWrapS();
this.wrapT = texture.getWrapT();
this.mipmapsEnabled = texture.isMipmapsEnabled();
if (texture.hasPixelData()) {
this.imageData = texture.getPixelData();
//System.out.println("Using cached pixel data for texture: " + texture.getName());
} else {
//System.out.println("No cached data for texture: " + texture.getName() + ", extracting from GPU");
this.imageData = extractTextureData(texture);
}
}
private byte[] extractFromTextureInternal(Texture texture) {
if (texture.hasPixelData()) {
return texture.getPixelData();
}
throw new RuntimeException("No OpenGL context and no internal pixel data available");
}
/**
* 从纹理提取图像数据
*/
private byte[] extractTextureData(Texture texture) {
try {
// 确保有OpenGL上下文
if (!org.lwjgl.opengl.GL.getCapabilities().OpenGL45) {
System.err.println("OpenGL context not available for texture extraction");
// 尝试使用纹理的内部数据(如果有)
return extractFromTextureInternal(texture);
}
java.nio.ByteBuffer pixelData = texture.extractTextureData();
if (pixelData == null || pixelData.remaining() == 0) {
System.err.println("Texture data extraction returned null or empty buffer");
throw new RuntimeException("Failed to extract texture data");
}
// 验证数据大小
int expectedSize = width * height * format.getComponents();
if (pixelData.remaining() != expectedSize) {
System.err.println("Texture data size mismatch. Expected: " + expectedSize +
", Got: " + pixelData.remaining());
throw new RuntimeException("Texture data size mismatch");
}
byte[] data = new byte[pixelData.remaining()];
pixelData.get(data);
// 释放Native Memory
org.lwjgl.system.MemoryUtil.memFree(pixelData);
return data;
} catch (Exception e) {
logger.error("Critical error extracting texture data: {}", e.getMessage());
throw new RuntimeException("Failed to extract texture data for serialization", e);
}
}
/**
* 创建占位符纹理数据
*/
private byte[] createPlaceholderTextureData() {
int components = format.getComponents();
int dataSize = width * height * components;
byte[] data = new byte[dataSize];
// 创建简单的渐变纹理作为占位符
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int index = (y * width + x) * components;
if (components >= 1) data[index] = (byte)((x * 255) / width); // R
if (components >= 2) data[index + 1] = (byte)((y * 255) / height); // G
if (components >= 3) data[index + 2] = (byte)128; // B
if (components >= 4) data[index + 3] = (byte)255; // A
}
}
return data;
}
public Texture toTexture() {
Texture texture = new Texture(name, width, height, format);
// 注意这里需要处理imageData的加载
// 实际项目中可能需要从文件路径加载图像数据
return texture;
try {
Texture texture = null;
if (imageData != null && imageData.length > 0) {
try {
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocateDirect(imageData.length);
buffer.put(imageData);
buffer.flip();
texture = new Texture(name, width, height, format, buffer);
} catch (Exception e) {
logger.error("Failed to create texture from image data: {}", e.getMessage());
}
}
// 如果从图像数据创建失败,尝试从文件创建
if (texture == null && filePath != null && !filePath.isEmpty()) {
try {
texture = loadTextureFromFile(filePath);
} catch (Exception e) {
logger.error("Failed to create texture from file: {}", e.getMessage());
}
}
// 如果以上方法都失败,创建空纹理
if (texture == null) {
try {
texture = new Texture(name, width, height, format);
} catch (Exception e) {
logger.error("Failed to create empty texture: {}", e.getMessage());
throw e;
}
}
// 应用纹理参数
if (texture != null) {
try {
texture.setMinFilter(minFilter);
texture.setMagFilter(magFilter);
texture.setWrapS(wrapS);
texture.setWrapT(wrapT);
if (mipmapsEnabled) {
texture.generateMipmaps();
}
} catch (Exception e) {
logger.error("Failed to apply texture parameters: {}", e.getMessage());
}
}
return texture;
} catch (Exception e) {
logger.error("Critical error in toTexture() for '{}': {}", name, e.getMessage());
e.printStackTrace();
return createSimpleFallbackTexture();
}
}
/**
* 创建简单的后备纹理
*/
private Texture createSimpleFallbackTexture() {
try {
// 创建一个非常简单的纯色纹理
return Texture.createSolidColor(name + "_simple_fallback", 64, 64, 0xFFFFFF00); // 黄色
} catch (Exception e) {
logger.error("Even fallback texture creation failed: {}", e.getMessage());
return null;
}
}
/**
* 从文件加载纹理
*/
private Texture loadTextureFromFile(String filePath) {
try {
// 使用Texture类的静态方法从文件加载
Texture texture = Texture.createFromFile(name, filePath);
// 应用保存的纹理参数
texture.setMinFilter(minFilter);
texture.setMagFilter(magFilter);
texture.setWrapS(wrapS);
texture.setWrapT(wrapT);
if (mipmapsEnabled) {
texture.generateMipmaps();
}
return texture;
} catch (Exception e) {
logger.error("Failed to load texture from file: {} - {}", filePath, e.getMessage());
return createFallbackTexture();
}
}
/**
* 获取纹理数据的估计内存使用量(字节)
*/
public long getEstimatedMemoryUsage() {
long baseMemory = (long) width * height * format.getComponents();
// 如果启用了mipmaps加上mipmaps的内存
if (mipmapsEnabled) {
return baseMemory * 4L / 3L; // mipmaps大约增加1/3内存
}
return baseMemory;
}
/**
* 将纹理数据保存到图像文件
*/
public boolean saveToFile(String filePath, String format) {
if (imageData == null) {
logger.error("No image data to save");
return false;
}
try {
// 创建临时纹理并保存
Texture tempTexture = this.toTexture();
boolean success = tempTexture.saveToFile(filePath, format);
tempTexture.dispose(); // 清理临时纹理
return success;
} catch (Exception e) {
logger.error("Failed to save texture data to file: {}", e.getMessage());
return false;
}
}
public boolean saveToFile(String filePath) {
return saveToFile(filePath, "png"); // 默认保存为PNG
}
/**
* 从文件路径创建纹理数据
*/
public static TextureData fromFile(String name, String filePath) {
TextureData data = new TextureData();
data.name = name;
data.filePath = filePath;
// 预加载图像信息
try {
Texture.ImageInfo info = Texture.getImageInfo(filePath);
data.width = info.width;
data.height = info.height;
data.format = Texture.getTextureFormat(info.components);
} catch (Exception e) {
System.err.println("Failed to get image info: " + e.getMessage());
// 设置默认值
data.width = 64;
data.height = 64;
data.format = Texture.TextureFormat.RGBA;
}
return data;
}
/**
* 从内存数据创建纹理数据
*/
public static TextureData fromMemory(String name, byte[] imageData, int width, int height, Texture.TextureFormat format) {
TextureData data = new TextureData();
data.name = name;
data.setImageData(imageData, width, height, format);
return data;
}
/**
* 验证纹理数据的完整性
*/
public boolean validate() {
if (name == null || name.trim().isEmpty()) {
return false;
}
if (width <= 0 || height <= 0) {
return false;
}
if (format == null) {
return false;
}
// 检查图像数据大小是否匹配
if (imageData != null) {
int expectedSize = width * height * format.getComponents();
if (imageData.length != expectedSize) {
System.err.println("Texture data size mismatch. Expected: " + expectedSize + ", Got: " + imageData.length);
return false;
}
}
return true;
}
/**
* 创建后备纹理(当主要方法失败时使用)
*/
private Texture createFallbackTexture() {
try {
// 创建一个棋盘格纹理作为后备
return Texture.createCheckerboard(
name + "_fallback",
Math.max(32, width),
Math.max(32, height),
8,
0xFFFF0000, // 红色
0xFF0000FF // 蓝色
);
} catch (Exception e) {
// 如果连后备纹理都创建失败,抛出异常
logger.error("Failed to create fallback texture: {}", e.getMessage());
throw new RuntimeException("Failed to create fallback texture", e);
}
}
/**
* 设置文件路径(用于从文件加载纹理)
*/
public void setFilePath(String filePath) {
this.filePath = filePath;
// 清除imageData因为我们将从文件加载
this.imageData = null;
}
/**
* 设置图像数据(用于从内存数据创建纹理)
*/
public void setImageData(byte[] imageData, int width, int height, Texture.TextureFormat format) {
this.imageData = imageData;
this.width = width;
this.height = height;
this.format = format;
// 清除filePath因为我们将使用内存数据
this.filePath = null;
}
/**
* 添加元数据
*/
public void addMetadata(String key, String value) {
this.metadata.put(key, value);
}
/**
* 获取元数据
*/
public String getMetadata(String key) {
return this.metadata.get(key);
}
public TextureData copy() {
@@ -616,8 +1051,25 @@ public class ModelData implements Serializable {
copy.width = this.width;
copy.height = this.height;
copy.format = this.format;
copy.minFilter = this.minFilter;
copy.magFilter = this.magFilter;
copy.wrapS = this.wrapS;
copy.wrapT = this.wrapT;
copy.mipmapsEnabled = this.mipmapsEnabled;
copy.metadata = new HashMap<>(this.metadata);
return copy;
}
@Override
public String toString() {
return "TextureData{" +
"name='" + name + '\'' +
", size=" + width + "x" + height +
", format=" + format +
", hasImageData=" + (imageData != null) +
", filePath=" + (filePath != null ? "'" + filePath + "'" : "null") +
'}';
}
}
/**
@@ -750,6 +1202,11 @@ public class ModelData implements Serializable {
public Map<String, String> getUserData() { return userData; }
public void setUserData(Map<String, String> userData) { this.userData = userData; }
public List<AnimationLayerData> getAnimationLayers() { return animationLayers; }
public void setAnimationLayers(List<AnimationLayerData> animationLayers) {
this.animationLayers = animationLayers;
}
// ==================== Object方法 ====================
@Override

View File

@@ -0,0 +1,167 @@
package com.chuangzhou.vivid2D.render.model.util;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 动画层数据
* @author tzdwindows 7
*/
public class AnimationLayerData implements Serializable {
private static final long serialVersionUID = 1L;
public String name;
public float weight;
public boolean enabled;
public AnimationLayer.BlendMode blendMode;
public int priority;
public float playbackSpeed;
public boolean looping;
public Map<String, AnimationTrackData> tracks;
public List<String> clipNames;
public AnimationLayerData() {
this.tracks = new HashMap<>();
this.clipNames = new ArrayList<>();
}
public AnimationLayerData(AnimationLayer layer) {
this();
this.name = layer.getName();
this.weight = layer.getWeight();
this.enabled = layer.isEnabled();
this.blendMode = layer.getBlendMode();
this.priority = layer.getPriority();
this.playbackSpeed = layer.getPlaybackSpeed();
this.looping = layer.isLooping();
// 序列化轨道
for (AnimationLayer.AnimationTrack track : layer.getTracks().values()) {
this.tracks.put(track.getParameterId(), new AnimationTrackData(track));
}
// 序列化剪辑名称(剪辑对象本身需要单独序列化)
for (AnimationClip clip : layer.getClips()) {
this.clipNames.add(clip.getName());
}
}
public AnimationLayer toAnimationLayer() {
AnimationLayer layer = new AnimationLayer(name, weight);
layer.setEnabled(enabled);
layer.setBlendMode(blendMode);
layer.setPriority(priority);
layer.setPlaybackSpeed(playbackSpeed);
layer.setLooping(looping);
// 反序列化轨道
for (AnimationTrackData trackData : tracks.values()) {
AnimationLayer.AnimationTrack track = trackData.toAnimationTrack();
layer.getTracks().put(track.getParameterId(), track);
}
// 注意:剪辑对象需要在外部设置
return layer;
}
public AnimationLayerData copy() {
AnimationLayerData copy = new AnimationLayerData();
copy.name = this.name;
copy.weight = this.weight;
copy.enabled = this.enabled;
copy.blendMode = this.blendMode;
copy.priority = this.priority;
copy.playbackSpeed = this.playbackSpeed;
copy.looping = this.looping;
copy.tracks = new HashMap<>();
for (Map.Entry<String, AnimationTrackData> entry : this.tracks.entrySet()) {
copy.tracks.put(entry.getKey(), entry.getValue().copy());
}
copy.clipNames = new ArrayList<>(this.clipNames);
return copy;
}
/**
* 动画轨道数据
*/
public static class AnimationTrackData implements Serializable {
private static final long serialVersionUID = 1L;
public String parameterId;
public boolean enabled;
public AnimationLayer.InterpolationType interpolation;
public List<KeyframeData> keyframes;
public AnimationTrackData() {
this.keyframes = new ArrayList<>();
}
public AnimationTrackData(AnimationLayer.AnimationTrack track) {
this();
this.parameterId = track.getParameterId();
this.enabled = track.isEnabled();
this.interpolation = track.getInterpolation();
// 序列化关键帧
for (AnimationLayer.Keyframe keyframe : track.getKeyframes()) {
this.keyframes.add(new KeyframeData(keyframe));
}
}
public AnimationLayer.AnimationTrack toAnimationTrack() {
AnimationLayer.AnimationTrack track = new AnimationLayer.AnimationTrack(parameterId);
track.setEnabled(enabled);
track.setInterpolation(interpolation);
// 反序列化关键帧
for (KeyframeData kfData : keyframes) {
track.addKeyframe(kfData.time, kfData.value, kfData.interpolation);
}
return track;
}
public AnimationTrackData copy() {
AnimationTrackData copy = new AnimationTrackData();
copy.parameterId = this.parameterId;
copy.enabled = this.enabled;
copy.interpolation = this.interpolation;
copy.keyframes = new ArrayList<>();
for (KeyframeData kf : this.keyframes) {
copy.keyframes.add(kf.copy());
}
return copy;
}
}
/**
* 关键帧数据(重用现有的 KeyframeData
*/
public static class KeyframeData implements Serializable {
private static final long serialVersionUID = 1L;
public float time;
public float value;
public AnimationLayer.InterpolationType interpolation;
public KeyframeData() {}
public KeyframeData(AnimationLayer.Keyframe keyframe) {
this.time = keyframe.getTime();
this.value = keyframe.getValue();
this.interpolation = keyframe.getInterpolation();
}
public KeyframeData copy() {
KeyframeData copy = new KeyframeData();
copy.time = this.time;
copy.value = this.value;
copy.interpolation = this.interpolation;
return copy;
}
}
}

View File

@@ -7,8 +7,11 @@ import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL45;
import org.lwjgl.stb.STBImage;
import org.lwjgl.stb.STBImageWrite;
import org.lwjgl.system.MemoryUtil;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -46,6 +49,9 @@ public class Texture {
private static final Map<String, Texture> TEXTURE_CACHE = new HashMap<>();
private static boolean openGLChecked = false;
// ==================== 像素数据缓存支持 ====================
private byte[] pixelDataCache = null;
// ==================== 枚举定义 ====================
public enum TextureFormat {
@@ -124,6 +130,14 @@ public class Texture {
// ==================== 构造器 ====================
// ==================== STB 图像加载 ====================
static {
STBImage.stbi_set_flip_vertically_on_load(true);
}
public Texture(String name, int width, int height, TextureFormat format) {
this(name, width, height, format, TextureType.UNSIGNED_BYTE);
}
@@ -220,9 +234,27 @@ public class Texture {
// 检查OpenGL错误
checkGLError("glTexSubImage2D");
// 缓存像素数据
cachePixelDataFromBuffer(pixelData);
unbind();
}
/**
* 从ByteBuffer缓存像素数据
*/
private void cachePixelDataFromBuffer(ByteBuffer buffer) {
try {
int originalPosition = buffer.position();
pixelDataCache = new byte[buffer.remaining()];
buffer.get(pixelDataCache);
buffer.position(originalPosition);
} catch (Exception e) {
System.err.println("Failed to cache pixel data from buffer: " + e.getMessage());
pixelDataCache = null;
}
}
/**
* 上传整数数组数据到纹理
*/
@@ -413,6 +445,8 @@ public class Texture {
System.err.println("Error disposing texture: " + e.getMessage());
}
pixelDataCache = null;
disposed = true;
TEXTURE_CACHE.values().removeIf(texture -> texture.textureId == this.textureId);
}
@@ -433,8 +467,9 @@ public class Texture {
public static Texture createSolidColor(String name, int width, int height, int rgbaColor) {
int[] pixels = new int[width * height];
java.util.Arrays.fill(pixels, rgbaColor);
return new Texture(name, width, height, TextureFormat.RGBA, pixels);
Texture texture = new Texture(name, width, height, TextureFormat.RGBA, pixels);
texture.ensurePixelDataCached();
return texture;
}
/**
@@ -450,8 +485,9 @@ public class Texture {
pixels[y * width + x] = isColor1 ? color1 : color2;
}
}
return new Texture(name, width, height, TextureFormat.RGBA, pixels);
Texture texture = new Texture(name, width, height, TextureFormat.RGBA, pixels);
texture.ensurePixelDataCached();
return texture;
}
/**
@@ -462,6 +498,471 @@ public class Texture {
new Texture(name, width, height, format));
}
/**
* 从文件系统加载图像数据
*/
public static ImageData loadImageFromFile(String filePath) {
if (filePath == null || filePath.trim().isEmpty()) {
throw new IllegalArgumentException("File path cannot be null or empty");
}
File file = new File(filePath);
if (!file.exists()) {
throw new RuntimeException("Texture file not found: " + filePath);
}
IntBuffer width = MemoryUtil.memAllocInt(1);
IntBuffer height = MemoryUtil.memAllocInt(1);
IntBuffer components = MemoryUtil.memAllocInt(1);
try {
// 使用 STB 加载图像
ByteBuffer imageData = STBImage.stbi_load(filePath, width, height, components, 0);
if (imageData == null) {
String error = STBImage.stbi_failure_reason();
throw new RuntimeException("Failed to load image: " + filePath + " - " + error);
}
// 确定纹理格式
TextureFormat format = getTextureFormat(components.get(0));
return new ImageData(
imageData,
width.get(0),
height.get(0),
format,
filePath
);
} finally {
MemoryUtil.memFree(width);
MemoryUtil.memFree(height);
MemoryUtil.memFree(components);
}
}
/**
* 从文件创建纹理
*/
public static Texture createFromFile(String name, String filePath) {
return createFromFile(name, filePath, TextureFilter.LINEAR, TextureFilter.LINEAR);
}
public static Texture createFromFile(String name, String filePath, TextureFilter minFilter, TextureFilter magFilter) {
ImageData imageData = loadImageFromFile(filePath);
try {
// 创建纹理
Texture texture = new Texture(
name,
imageData.width,
imageData.height,
imageData.format
);
// 上传数据(这会自动缓存)
texture.uploadData(imageData.data);
// 设置纹理参数
texture.setMinFilter(minFilter);
texture.setMagFilter(magFilter);
// 如果是2的幂次方尺寸生成mipmaps
if (texture.isPowerOfTwo(imageData.width) && texture.isPowerOfTwo(imageData.height)) {
texture.generateMipmaps();
}
return texture;
} finally {
// 释放STB图像数据
STBImage.stbi_image_free(imageData.data);
}
}
/**
* 从纹理提取图像数据
*/
public ByteBuffer extractTextureData() {
if (disposed) {
throw new IllegalStateException("Cannot extract data from disposed texture");
}
bind(0);
try {
// 计算数据大小
int dataSize = width * height * format.getComponents();
ByteBuffer pixelData = MemoryUtil.memAlloc(dataSize);
// 从GPU读取纹理数据
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, format.getGLFormat(), type.getGLType(), pixelData);
// 检查OpenGL错误
checkGLError("glGetTexImage");
pixelData.flip();
return pixelData;
} finally {
unbind();
}
}
/**
* 将纹理保存到文件支持PNG、JPG等格式
*/
public boolean saveToFile(String filePath) {
return saveToFile(filePath, "png");
}
public boolean saveToFile(String filePath, String format) {
if (disposed) {
throw new IllegalStateException("Cannot save disposed texture");
}
ByteBuffer pixelData = extractTextureData();
try {
// 根据格式保存图像
boolean success = false;
int components = this.format.getComponents(); // 使用纹理的格式组件数量
int stride = width * components; // 每行的字节数
switch (format.toLowerCase()) {
case "png":
success = STBImageWrite.stbi_write_png(
filePath, width, height, components, pixelData, stride
);
break;
case "jpg":
case "jpeg":
success = STBImageWrite.stbi_write_jpg(
filePath, width, height, components, pixelData, 90 // 质量 0-100
);
break;
case "bmp":
success = STBImageWrite.stbi_write_bmp(filePath, width, height, components, pixelData);
break;
case "tga":
success = STBImageWrite.stbi_write_tga(filePath, width, height, components, pixelData);
break;
default:
throw new IllegalArgumentException("Unsupported image format: " + format);
}
if (!success) {
System.err.println("Failed to save texture to: " + filePath);
return false;
}
return true;
} finally {
MemoryUtil.memFree(pixelData);
}
}
/**
* 检查是否有可用的像素数据缓存
*/
public boolean hasPixelData() {
return this.pixelDataCache != null && this.pixelDataCache.length > 0;
}
/**
* 获取内部像素数据缓存
*/
public byte[] getPixelData() {
if (pixelDataCache == null) {
// 如果没有缓存从GPU提取并缓存
cachePixelDataFromGPU();
}
return pixelDataCache != null ? pixelDataCache.clone() : null;
}
/**
* 设置像素数据缓存
*/
public void setPixelData(byte[] pixelData) {
this.pixelDataCache = pixelData != null ? pixelData.clone() : null;
}
/**
* 从GPU提取像素数据并缓存
*/
private void cachePixelDataFromGPU() {
if (disposed) {
return;
}
try {
ByteBuffer gpuData = extractTextureData();
if (gpuData != null && gpuData.remaining() > 0) {
pixelDataCache = new byte[gpuData.remaining()];
gpuData.get(pixelDataCache);
gpuData.rewind();
MemoryUtil.memFree(gpuData);
}
} catch (Exception e) {
System.err.println("Failed to cache pixel data from GPU: " + e.getMessage());
pixelDataCache = null;
}
}
/**
* 清除像素数据缓存
*/
public void clearPixelDataCache() {
this.pixelDataCache = null;
}
/**
* 确保像素数据缓存可用如果不存在则从GPU提取
*/
public void ensurePixelDataCached() {
if (!hasPixelData()) {
cachePixelDataFromGPU();
}
}
/**
* 获取像素数据缓存大小(字节)
*/
public int getPixelDataCacheSize() {
return pixelDataCache != null ? pixelDataCache.length : 0;
}
/**
* 创建纹理的深拷贝(包括图像数据)
*/
public Texture copy() {
return copy(this.name + "_copy");
}
public Texture copy(String newName) {
if (disposed) {
throw new IllegalStateException("Cannot copy disposed texture");
}
// 确保缓存数据可用
ensurePixelDataCached();
ByteBuffer pixelData = null;
try {
if (hasPixelData()) {
// 使用缓存数据创建新纹理
pixelData = MemoryUtil.memAlloc(pixelDataCache.length);
pixelData.put(pixelDataCache);
pixelData.flip();
} else {
// 回退到GPU提取
pixelData = extractTextureData();
}
// 创建新纹理
Texture copy = new Texture(newName, width, height, format, pixelData);
// 复制纹理参数
copy.setMinFilter(this.minFilter);
copy.setMagFilter(this.magFilter);
copy.setWrapS(this.wrapS);
copy.setWrapT(this.wrapT);
// 复制像素数据缓存
if (hasPixelData()) {
copy.setPixelData(this.pixelDataCache);
}
if (this.mipmapsEnabled) {
copy.generateMipmaps();
}
return copy;
} finally {
if (pixelData != null) {
MemoryUtil.memFree(pixelData);
}
}
}
// ==================== 辅助方法和内部类 ====================
/**
* 根据组件数量确定纹理格式
*/
public static TextureFormat getTextureFormat(int components) {
switch (components) {
case 1: return TextureFormat.RED;
case 2: return TextureFormat.RG;
case 3: return TextureFormat.RGB;
case 4: return TextureFormat.RGBA;
default:
throw new IllegalArgumentException("Unsupported number of components: " + components);
}
}
/**
* 图像数据容器类
*/
public static class ImageData {
public final ByteBuffer data;
public final int width;
public final int height;
public final TextureFormat format;
public final String sourcePath;
public ImageData(ByteBuffer data, int width, int height, TextureFormat format, String sourcePath) {
this.data = data;
this.width = width;
this.height = height;
this.format = format;
this.sourcePath = sourcePath;
}
/**
* 创建图像数据的拷贝
*/
public ImageData copy() {
ByteBuffer copyBuffer = MemoryUtil.memAlloc(data.capacity());
copyBuffer.put(data);
copyBuffer.flip();
data.rewind();
return new ImageData(copyBuffer, width, height, format, sourcePath);
}
/**
* 释放图像数据内存
*/
public void free() {
if (data != null) {
STBImage.stbi_image_free(data);
}
}
}
/**
* 支持的图像格式检查
*/
public static boolean isSupportedImageFormat(String filePath) {
if (filePath == null) return false;
String lowerPath = filePath.toLowerCase();
return lowerPath.endsWith(".png") ||
lowerPath.endsWith(".jpg") ||
lowerPath.endsWith(".jpeg") ||
lowerPath.endsWith(".bmp") ||
lowerPath.endsWith(".tga") ||
lowerPath.endsWith(".psd") ||
lowerPath.endsWith(".gif") ||
lowerPath.endsWith(".hdr") ||
lowerPath.endsWith(".pic");
}
/**
* 获取图像文件信息(不加载完整图像)
*/
public static ImageInfo getImageInfo(String filePath) {
if (!isSupportedImageFormat(filePath)) {
throw new IllegalArgumentException("Unsupported image format: " + filePath);
}
IntBuffer width = MemoryUtil.memAllocInt(1);
IntBuffer height = MemoryUtil.memAllocInt(1);
IntBuffer components = MemoryUtil.memAllocInt(1);
try {
boolean success = STBImage.stbi_info(filePath, width, height, components);
if (!success) {
String error = STBImage.stbi_failure_reason();
throw new RuntimeException("Failed to get image info: " + filePath + " - " + error);
}
return new ImageInfo(
width.get(0),
height.get(0),
components.get(0),
filePath
);
} finally {
MemoryUtil.memFree(width);
MemoryUtil.memFree(height);
MemoryUtil.memFree(components);
}
}
/**
* 图像信息类
*/
public static class ImageInfo {
public final int width;
public final int height;
public final int components;
public final String filePath;
public ImageInfo(int width, int height, int components, String filePath) {
this.width = width;
this.height = height;
this.components = components;
this.filePath = filePath;
}
@Override
public String toString() {
return String.format("ImageInfo{size=%dx%d, components=%d, path='%s'}",
width, height, components, filePath);
}
}
// ==================== 新的静态工厂方法 ====================
/**
* 从字节数组创建纹理
*/
public static Texture createFromBytes(String name, byte[] imageData, TextureFilter minFilter, TextureFilter magFilter) {
ByteBuffer buffer = MemoryUtil.memAlloc(imageData.length);
buffer.put(imageData);
buffer.flip();
// 使用STB从内存加载图像
IntBuffer width = MemoryUtil.memAllocInt(1);
IntBuffer height = MemoryUtil.memAllocInt(1);
IntBuffer components = MemoryUtil.memAllocInt(1);
try {
ByteBuffer pixelData = STBImage.stbi_load_from_memory(buffer, width, height, components, 0);
if (pixelData == null) {
String error = STBImage.stbi_failure_reason();
throw new RuntimeException("Failed to load image from bytes: " + error);
}
TextureFormat format = getTextureFormat(components.get(0));
Texture texture = new Texture(name, width.get(0), height.get(0), format, pixelData);
texture.setMinFilter(minFilter);
texture.setMagFilter(magFilter);
STBImage.stbi_image_free(pixelData);
return texture;
} finally {
MemoryUtil.memFree(buffer);
MemoryUtil.memFree(width);
MemoryUtil.memFree(height);
MemoryUtil.memFree(components);
}
}
// ==================== 工具方法 ====================
/**

View File

@@ -18,9 +18,8 @@ import java.util.Random;
* 重写后的 ModelRender 测试示例:构造一个简单的人形(头、身体、左右手、左右腿)
* 便于验证层级变换与渲染是否正确。
*
* 注意:依赖你工程里已有的 Model2D / ModelPart / Mesh2D / Texture API。
*
* @author tzdwindows 7(改)
* @author tzdwindows 7
*/
public class ModelRenderTest {

View File

@@ -0,0 +1,601 @@
package com.chuangzhou.vivid2D.test;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.AnimationLayer;
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem;
import com.chuangzhou.vivid2D.render.model.util.ModelPose;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.model.util.Texture;
import org.joml.Vector2f;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryUtil;
/**
* 用于测试Model2D模型的保存和加载功能
*
* @author tzdwindows 7
*/
public class ModelTest {
private static long window;
private static boolean glInitialized = false;
public static void main(String[] args) {
System.out.println("=== Model2D Extended Save and Load Test Start ===");
try {
// Initialize OpenGL context for texture testing
initializeOpenGL();
// Test 1: Create model and save (with texture)
testCreateAndSaveModelWithTexture();
// Test 2: Load model and verify data including textures
testLoadAndVerifyModelWithTexture();
// Test 3: Test compressed file operations with textures
testCompressedFileOperationsWithTexture();
// Other existing tests...
testAnimationSystem();
testPhysicsSystem();
testComplexTransformations();
testPerformance();
} finally {
// Cleanup OpenGL
cleanupOpenGL();
}
System.out.println("=== Model2D Extended Save and Load Test Complete ===");
}
/**
* Initialize OpenGL context for texture testing
*/
private static void initializeOpenGL() {
try {
// Setup error callback
GLFWErrorCallback.createPrint(System.err).set();
// Initialize GLFW
if (!GLFW.glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
// Configure GLFW
GLFW.glfwDefaultWindowHints();
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); // Hide window
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
// Create window
window = GLFW.glfwCreateWindow(100, 100, "Texture Test", MemoryUtil.NULL, MemoryUtil.NULL);
if (window == MemoryUtil.NULL) {
throw new RuntimeException("Failed to create GLFW window");
}
// Make OpenGL context current
GLFW.glfwMakeContextCurrent(window);
GLFW.glfwSwapInterval(1); // Enable v-sync
// Initialize OpenGL capabilities
GL.createCapabilities();
System.out.println("OpenGL initialized successfully");
System.out.println("OpenGL Version: " + org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION));
glInitialized = true;
} catch (Exception e) {
System.err.println("Failed to initialize OpenGL: " + e.getMessage());
// Continue without OpenGL for other tests
}
}
/**
* Cleanup OpenGL resources
*/
private static void cleanupOpenGL() {
if (window != MemoryUtil.NULL) {
GLFW.glfwDestroyWindow(window);
}
GLFW.glfwTerminate();
GLFW.glfwSetErrorCallback(null).free();
}
/**
* Test 1: Create model with textures and save to file
*/
public static void testCreateAndSaveModelWithTexture() {
System.out.println("\n--- Test 1: Create and Save Model with Textures ---");
if (!glInitialized) {
System.out.println("Skipping texture test - OpenGL not available");
return;
}
try {
// Create model
Model2D model = new Model2D("textured_character");
model.setVersion("1.0.0");
// Create parts
ModelPart body = model.createPart("body");
ModelPart head = model.createPart("head");
// Build hierarchy
body.addChild(head);
// Set part properties
body.setPosition(0, 0);
head.setPosition(0, -50);
// Create test textures
System.out.println("Creating test textures...");
// Create solid color texture
Texture bodyTexture = Texture.createSolidColor("body_texture", 64, 64, 0xFFFF0000); // Red
Texture headTexture = Texture.createSolidColor("head_texture", 64, 64, 0xFF00FF00); // Green
// Create checkerboard texture
Texture checkerTexture = Texture.createCheckerboard("checker_texture", 128, 128, 16,
0xFFFFFFFF, 0xFF0000FF); // White and Blue
// === 关键修复:确保纹理数据被缓存 ===
System.out.println("Ensuring texture data is cached...");
bodyTexture.ensurePixelDataCached();
headTexture.ensurePixelDataCached();
checkerTexture.ensurePixelDataCached();
// 验证缓存状态
System.out.println("Texture cache status:");
System.out.println(" - body_texture: " + (bodyTexture.hasPixelData() ? "CACHED" : "MISSING"));
System.out.println(" - head_texture: " + (headTexture.hasPixelData() ? "CACHED" : "MISSING"));
System.out.println(" - checker_texture: " + (checkerTexture.hasPixelData() ? "CACHED" : "MISSING"));
// Add textures to model
model.addTexture(bodyTexture);
model.addTexture(headTexture);
model.addTexture(checkerTexture);
// Create meshes and assign textures
Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80);
Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50);
// Set textures for meshes
bodyMesh.setTexture(bodyTexture);
headMesh.setTexture(headTexture);
// Add meshes to model and parts
model.addMesh(bodyMesh);
model.addMesh(headMesh);
body.addMesh(bodyMesh);
head.addMesh(headMesh);
// Create animation parameters
AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0);
model.setParameterValue("smile", 0.5f);
// Update model
model.update(0.016f);
// Save to regular file
String regularFilePath = "textured_character.model";
model.saveToFile(regularFilePath);
System.out.println("Textured model saved to regular file: " + regularFilePath);
// Save to compressed file
String compressedFilePath = "textured_character.model.gz";
model.saveToCompressedFile(compressedFilePath);
System.out.println("Textured model saved to compressed file: " + compressedFilePath);
// Verify model state before saving
System.out.println("Textured model created successfully:");
System.out.println(" - Name: " + model.getName());
System.out.println(" - Textures: " + model.getTextures().size());
System.out.println(" - Meshes: " + model.getMeshes().size());
// Print texture information
for (Texture texture : model.getTextures().values()) {
System.out.println(" - Texture: " + texture.getName() +
" (" + texture.getWidth() + "x" + texture.getHeight() +
", format: " + texture.getFormat() +
", cached: " + texture.hasPixelData() + ")");
}
} catch (Exception e) {
System.err.println("Error in testCreateAndSaveModelWithTexture: " + e.getMessage());
e.printStackTrace();
// 提供更详细的错误信息
if (e.getCause() != null) {
System.err.println("Caused by: " + e.getCause().getMessage());
}
}
}
/**
* Test 2: Load model with textures and verify data integrity
*/
public static void testLoadAndVerifyModelWithTexture() {
System.out.println("\n--- Test 2: Load and Verify Model with Textures ---");
if (!glInitialized) {
System.out.println("Skipping texture test - OpenGL not available");
return;
}
try {
// Load from regular file
String filePath = "textured_character.model";
Model2D loadedModel = Model2D.loadFromFile(filePath);
System.out.println("Textured model loaded successfully from: " + filePath);
// Verify basic properties
System.out.println("Basic properties:");
System.out.println(" - Name: " + loadedModel.getName());
System.out.println(" - Version: " + loadedModel.getVersion());
// Verify textures
System.out.println("Textures verification:");
System.out.println(" - Total textures: " + loadedModel.getTextures().size());
for (Texture texture : loadedModel.getTextures().values()) {
System.out.println(" - Texture '" + texture.getName() + "': " +
texture.getWidth() + "x" + texture.getHeight() +
", format: " + texture.getFormat() +
", disposed: " + texture.isDisposed());
}
// Verify parts and meshes
System.out.println("Parts and meshes verification:");
for (ModelPart part : loadedModel.getParts()) {
System.out.println(" - Part '" + part.getName() + "': " +
part.getMeshes().size() + " meshes");
for (Mesh2D mesh : part.getMeshes()) {
Texture meshTexture = mesh.getTexture();
System.out.println(" * Mesh '" + mesh.getName() + "': " +
(meshTexture != null ? "has texture '" + meshTexture.getName() + "'" : "no texture"));
}
}
// Test texture functionality
System.out.println("Texture functionality test:");
Texture bodyTexture = loadedModel.getTexture("body_texture");
if (bodyTexture != null) {
System.out.println(" - Body texture validation:");
System.out.println(" * Width: " + bodyTexture.getWidth());
System.out.println(" * Height: " + bodyTexture.getHeight());
System.out.println(" * Format: " + bodyTexture.getFormat());
System.out.println(" * Memory usage: " + bodyTexture.getEstimatedMemoryUsage() + " bytes");
// Test texture binding (if OpenGL context is available)
try {
bodyTexture.bind(0);
System.out.println(" * Texture binding: SUCCESS");
bodyTexture.unbind();
} catch (Exception e) {
System.out.println(" * Texture binding: FAILED - " + e.getMessage());
}
}
// Test parameter modification
System.out.println("Parameter modification test:");
loadedModel.setParameterValue("smile", 0.8f);
float newSmileValue = loadedModel.getParameterValue("smile");
System.out.println(" - Modified smile parameter to: " + newSmileValue);
// Test model update
loadedModel.update(0.016f);
System.out.println(" - Model update completed successfully");
} catch (Exception e) {
System.err.println("Error in testLoadAndVerifyModelWithTexture: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Test 3: Test compressed file operations with textures
*/
public static void testCompressedFileOperationsWithTexture() {
System.out.println("\n--- Test 3: Compressed File Operations with Textures ---");
if (!glInitialized) {
System.out.println("Skipping texture test - OpenGL not available");
return;
}
try {
// Load from compressed file
String compressedFilePath = "textured_character.model.gz";
Model2D compressedModel = Model2D.loadFromCompressedFile(compressedFilePath);
System.out.println("Textured model loaded successfully from compressed file: " + compressedFilePath);
System.out.println(" - Name: " + compressedModel.getName());
System.out.println(" - Textures: " + compressedModel.getTextures().size());
System.out.println(" - Parts: " + compressedModel.getParts().size());
// Verify textures in compressed model
System.out.println("Compressed model texture verification:");
for (Texture texture : compressedModel.getTextures().values()) {
System.out.println(" - Texture '" + texture.getName() + "': " +
texture.getWidth() + "x" + texture.getHeight());
}
// Modify and re-save
compressedModel.setName("modified_textured_character");
compressedModel.setParameterValue("smile", 0.9f);
String newCompressedPath = "modified_textured_character.model.gz";
compressedModel.saveToCompressedFile(newCompressedPath);
System.out.println("Modified textured model saved to new compressed file: " + newCompressedPath);
// Verify the new compressed file can be loaded
Model2D reloadedModel = Model2D.loadFromCompressedFile(newCompressedPath);
System.out.println("Reloaded modified textured model verification:");
System.out.println(" - Name: " + reloadedModel.getName());
System.out.println(" - Smile parameter value: " + reloadedModel.getParameterValue("smile"));
System.out.println(" - Textures: " + reloadedModel.getTextures().size());
} catch (Exception e) {
System.err.println("Error in testCompressedFileOperationsWithTexture: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Test 4: Test animation system
*/
public static void testAnimationSystem() {
System.out.println("\n--- Test 4: Animation System Test ---");
try {
// Load model
Model2D model = Model2D.loadFromFile("test_character.model");
System.out.println("Testing animation system:");
// Test parameter-driven animation
System.out.println("Parameter-driven animation test:");
for (int frame = 0; frame < 10; frame++) {
float walkValue = (float) Math.sin(frame * 0.2f) * 0.5f + 0.5f;
float waveValue = (float) Math.sin(frame * 0.3f);
float blinkValue = frame % 20 == 0 ? 1.0f : 0.0f; // Blink every 20 frames
model.setParameterValue("walk_cycle", walkValue);
model.setParameterValue("wave", waveValue);
model.setParameterValue("blink", blinkValue);
model.update(0.016f);
System.out.println(" - Frame " + frame +
": walk=" + String.format("%.2f", walkValue) +
", wave=" + String.format("%.2f", waveValue) +
", blink=" + String.format("%.2f", blinkValue));
}
// Test pose system
System.out.println("Pose system test:");
ModelPose currentPose = model.getCurrentPose();
if (currentPose != null) {
System.out.println(" - Current pose: " + currentPose);
}
// Test animation layer blending
System.out.println("Animation layer test:");
for (AnimationLayer layer : model.getAnimationLayers()) {
System.out.println(" - Layer: " + layer.getName());
}
} catch (Exception e) {
System.err.println("Error in testAnimationSystem: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Test 5: Test physics system
*/
public static void testPhysicsSystem() {
System.out.println("\n--- Test 5: Physics System Test ---");
try {
// Load model
Model2D model = Model2D.loadFromFile("test_character.model");
System.out.println("Testing physics system:");
PhysicsSystem physics = model.getPhysics();
System.out.println(" - Physics system: " +
(physics != null ? "available" : "not available"));
if (physics != null) {
Vector2f gravity = physics.getGravity();
System.out.println(" - Gravity: (" + gravity.x + ", " + gravity.y + ")");
System.out.println(" - Air resistance: " + physics.getAirResistance());
System.out.println(" - Time scale: " + physics.getTimeScale());
System.out.println(" - Enabled: " + physics.isEnabled());
}
// Test physics simulation
System.out.println("Physics simulation test:");
// 初始化物理系统
physics.initialize();
// 添加一些物理粒子
PhysicsSystem.PhysicsParticle particle1 = physics.addParticle("test_particle1", new Vector2f(0, 0), 1.0f);
PhysicsSystem.PhysicsParticle particle2 = physics.addParticle("test_particle2", new Vector2f(10, 0), 1.0f);
// 添加弹簧连接
physics.addSpring("test_spring", particle1, particle2, 15.0f, 0.5f, 0.1f);
for (int step = 0; step < 15; step++) {
model.update(0.016f); // Simulate physics
if (step % 5 == 0) {
System.out.println(" - Step " + step + ": model updated with physics");
Vector2f pos1 = particle1.getPosition();
System.out.println(" Particle1 position: (" +
String.format("%.2f", pos1.x) + ", " +
String.format("%.2f", pos1.y) + ")");
}
}
// Test physics properties
System.out.println("Physics properties verification:");
System.out.println(" - Active physics: " + physics.hasActivePhysics());
System.out.println(" - Particle count: " + physics.getParticles().size());
System.out.println(" - Spring count: " + physics.getSprings().size());
} catch (Exception e) {
System.err.println("Error in testPhysicsSystem: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Test 6: Test complex transformations
*/
public static void testComplexTransformations() {
System.out.println("\n--- Test 6: Complex Transformations Test ---");
try {
// Load model
Model2D model = Model2D.loadFromFile("test_character.model");
System.out.println("Testing complex transformations:");
// Test nested transformations
ModelPart root = model.getRootPart();
if (root != null) {
Vector2f position = root.getPosition();
Vector2f scale = root.getScale();
System.out.println("Root transformation:");
System.out.println(" - Local position: (" + position.x + ", " + position.y + ")");
System.out.println(" - Rotation: " + root.getRotation() + " degrees");
System.out.println(" - Scale: (" + scale.x + ", " + scale.y + ")");
// 获取世界变换矩阵中的位置
float worldX = root.getWorldTransform().m02();
float worldY = root.getWorldTransform().m12();
System.out.println(" - World position (from matrix): (" + worldX + ", " + worldY + ")");
}
// Test transformation inheritance
System.out.println("Transformation inheritance test:");
ModelPart head = model.getPart("head");
if (head != null) {
Vector2f headPos = head.getPosition();
float headWorldX = head.getWorldTransform().m02();
float headWorldY = head.getWorldTransform().m12();
System.out.println("Head transformation (relative to body):");
System.out.println(" - Local position: (" + headPos.x + ", " + headPos.y + ")");
System.out.println(" - World position (from matrix): (" + headWorldX + ", " + headWorldY + ")");
}
// Test bounds calculation
BoundingBox bounds = model.getBounds();
if (bounds != null) {
System.out.println("Bounds calculation:");
System.out.println(" - Min: (" + bounds.getMinX() + ", " + bounds.getMinY() + ")");
System.out.println(" - Max: (" + bounds.getMaxX() + ", " + bounds.getMaxY() + ")");
System.out.println(" - Width: " + bounds.getWidth());
System.out.println(" - Height: " + bounds.getHeight());
}
// Test visibility system
System.out.println("Visibility system test:");
model.setVisible(false);
System.out.println(" - Model visible: " + model.isVisible());
model.setVisible(true);
System.out.println(" - Model visible: " + model.isVisible());
} catch (Exception e) {
System.err.println("Error in testComplexTransformations: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Test 7: Test performance with large model
*/
public static void testPerformance() {
System.out.println("\n--- Test 7: Performance Test ---");
try {
// Create a more complex model for performance testing
Model2D complexModel = new Model2D("complex_character");
// Add many parts
ModelPart root = complexModel.createPart("root");
for (int i = 0; i < 10; i++) {
ModelPart part = complexModel.createPart("part_" + i);
root.addChild(part);
part.setPosition(i * 10, i * 5);
part.setRotation(i * 5);
// Add mesh
Mesh2D mesh = Mesh2D.createQuad("mesh_" + i, 20, 20);
complexModel.addMesh(mesh);
part.addMesh(mesh);
}
// Add multiple parameters
for (int i = 0; i < 8; i++) {
complexModel.createParameter("param_" + i, 0, 1, 0);
}
System.out.println("Performance test with complex model:");
System.out.println(" - Parts: " + complexModel.getParts().size());
System.out.println(" - Parameters: " + complexModel.getParameters().size());
System.out.println(" - Meshes: " + complexModel.getMeshes().size());
// Performance test: multiple updates
long startTime = System.currentTimeMillis();
int frameCount = 100;
for (int i = 0; i < frameCount; i++) {
// Animate parameters
for (int j = 0; j < 8; j++) {
float value = (float) Math.sin(i * 0.1f + j * 0.5f) * 0.5f + 0.5f;
complexModel.setParameterValue("param_" + j, value);
}
complexModel.update(0.016f);
}
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
double avgTimePerFrame = (double) totalTime / frameCount;
System.out.println("Performance results:");
System.out.println(" - Total time for " + frameCount + " frames: " + totalTime + "ms");
System.out.println(" - Average time per frame: " + String.format("%.2f", avgTimePerFrame) + "ms");
System.out.println(" - Estimated FPS: " + String.format("%.1f", 1000.0 / avgTimePerFrame));
} catch (Exception e) {
System.err.println("Error in testPerformance: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Utility method to print part hierarchy
*/
private static void printPartHierarchy(ModelPart part, int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + "- " + part.getName() +
" (children: " + part.getChildren().size() + ")");
for (ModelPart child : part.getChildren()) {
printPartHierarchy(child, depth + 1);
}
}
}