feat(model): 实现动画层数据序列化与纹理管理增强
- 添加 AnimationLayerData 类用于动画层的序列化支持- 增强 Model2D 的 addTexture 方法,添加空值检查和重复纹理处理 - 在 ModelData 中添加动画层序列化与反序列化逻辑 - 扩展 TextureData 结构以支持完整纹理参数和元数据- 改进纹理反序列化过程,添加错误处理和后备纹理创建- 更新模型测试用例以验证新功能和修复的问题 - 优化网格序列化逻辑,避免重复序列化相同网格- 添加日志记录支持以提高调试能力 重要 - 完全实现了模型的保存和加载(贴图待测试)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模型到压缩文件
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user