diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index b1d0701..69c6017 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -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 animationLayers) { + this.animationLayers.clear(); + this.animationLayers.addAll(animationLayers); + } + /** * 保存模型到压缩文件 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java index dd8e4fb..f1550d5 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java @@ -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 textures; private List parameters; private List animations; + private List 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 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 textureMap = deserializeTextures(); + // === 关键修复:将纹理添加到模型中 === + for (Texture texture : textureMap.values()) { + model.addTexture(texture); + } + // 然后创建所有网格(依赖纹理) Map meshMap = deserializeMeshes(textureMap); @@ -156,32 +199,75 @@ public class ModelData implements Serializable { // 最后创建参数 deserializeParameters(model); + // 创建动画层 + deserializeAnimationLayers(model); return model; } private Map deserializeTextures() { Map 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 deserializeMeshes(Map textureMap) { Map 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 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 getUserData() { return userData; } public void setUserData(Map userData) { this.userData = userData; } + public List getAnimationLayers() { return animationLayers; } + public void setAnimationLayers(List animationLayers) { + this.animationLayers = animationLayers; + } + // ==================== Object方法 ==================== @Override diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayerData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayerData.java new file mode 100644 index 0000000..db577e5 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayerData.java @@ -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 tracks; + public List 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 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 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; + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java index 21d3a74..47baea5 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java @@ -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 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); + } + } + // ==================== 工具方法 ==================== /** diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java index e487a3c..ddfb8c7 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java @@ -18,9 +18,8 @@ import java.util.Random; * 重写后的 ModelRender 测试示例:构造一个简单的人形(头、身体、左右手、左右腿) * 便于验证层级变换与渲染是否正确。 * - * 注意:依赖你工程里已有的 Model2D / ModelPart / Mesh2D / Texture API。 * - * @author tzdwindows 7(改) + * @author tzdwindows 7 */ public class ModelRenderTest { diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java new file mode 100644 index 0000000..57d358d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java @@ -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); + } + } +} \ No newline at end of file