feat(model): 添加液化笔划数据的序列化与反序列化支持
- 在 ModelData.PartData 中新增 liquifyStrokes 字段用于保存液化笔划- 实现通过反射读取 ModelPart 的液化笔划数据(兼容旧版本)- 支持多种数据结构形式的液化点读取(Vector2f、自定义类、Map) - 反序列化时自动重放液化笔划到 ModelPart- 添加 LiquifyStrokeData 和 LiquifyPointData 用于序列化存储 - 提供深度拷贝支持以确保 liquifyStrokes 数据完整复制 - 增加 ModelLoadTest 测试类用于验证模型加载与结构检查
This commit is contained in:
@@ -149,43 +149,83 @@ public final class ModelRender {
|
|||||||
uniform int uLightsIsAmbient[MAX_LIGHTS];
|
uniform int uLightsIsAmbient[MAX_LIGHTS];
|
||||||
uniform int uLightCount;
|
uniform int uLightCount;
|
||||||
|
|
||||||
|
// 常用衰减系数(可在 shader 内微调)
|
||||||
|
const float ATT_CONST = 1.0;
|
||||||
|
const float ATT_LINEAR = 0.09;
|
||||||
|
const float ATT_QUAD = 0.032;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
if (uDebugMode == 1) {
|
// 先采样纹理
|
||||||
FragColor = vec4(vWorldPos * 0.5 + 0.5, 0.0, 1.0);
|
vec4 tex = texture(uTexture, vTexCoord);
|
||||||
|
float alpha = tex.a * uOpacity;
|
||||||
|
if (alpha <= 0.001) discard;
|
||||||
|
|
||||||
|
// 如果没有光源,跳过光照计算(性能更好并且保持原始贴图色)
|
||||||
|
if (uLightCount == 0) {
|
||||||
|
vec3 base = tex.rgb * uColor.rgb;
|
||||||
|
// 简单的色调映射(防止数值过大)
|
||||||
|
base = clamp(base, 0.0, 1.0);
|
||||||
|
FragColor = vec4(base, alpha);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec4 tex = texture(uTexture, vTexCoord);
|
// 基础颜色(纹理 * 部件颜色)
|
||||||
vec3 finalColor = tex.rgb * uColor.rgb;
|
vec3 baseColor = tex.rgb * uColor.rgb;
|
||||||
vec3 lighting = vec3(0.0);
|
|
||||||
|
|
||||||
for (int i = 0; i < uLightCount; i++) {
|
// 全局环境光基线(可以适度提高以避免全黑)
|
||||||
|
vec3 ambient = vec3(0.06); // 小环境光补偿
|
||||||
|
vec3 lighting = vec3(0.0);
|
||||||
|
vec3 specularAccum = vec3(0.0);
|
||||||
|
|
||||||
|
// 累积环境光(来自被标记为环境光的光源)
|
||||||
|
for (int i = 0; i < uLightCount; ++i) {
|
||||||
if (uLightsIsAmbient[i] == 1) {
|
if (uLightsIsAmbient[i] == 1) {
|
||||||
lighting += uLightsColor[i] * uLightsIntensity[i];
|
lighting += uLightsColor[i] * uLightsIntensity[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 加上基线环境光
|
||||||
|
lighting += ambient;
|
||||||
|
|
||||||
for (int i = 0; i < uLightCount; i++) {
|
// 对每个非环境光计算基于距离的衰减与简单高光
|
||||||
|
for (int i = 0; i < uLightCount; ++i) {
|
||||||
if (uLightsIsAmbient[i] == 1) continue;
|
if (uLightsIsAmbient[i] == 1) continue;
|
||||||
|
|
||||||
float intensity = uLightsIntensity[i];
|
vec2 toLight = uLightsPos[i] - vWorldPos;
|
||||||
if (intensity <= 0.0) continue;
|
float dist = length(toLight);
|
||||||
|
// 标准物理式衰减
|
||||||
|
float attenuation = ATT_CONST / (ATT_CONST + ATT_LINEAR * dist + ATT_QUAD * dist * dist);
|
||||||
|
|
||||||
vec2 lightDir = uLightsPos[i] - vWorldPos;
|
// 强度受光源强度和衰减影响
|
||||||
float dist = length(lightDir);
|
float radiance = uLightsIntensity[i] * attenuation;
|
||||||
float atten = 1.0 / (1.0 + 0.1 * dist + 0.01 * dist * dist);
|
|
||||||
lighting += uLightsColor[i] * intensity * atten;
|
// 漫反射:在纯2D情景下,法线与视线近似固定(Z向),
|
||||||
|
// 所以漫反射对所有片元是恒定的。我们用一个基于距离的柔和因子来模拟明暗变化。
|
||||||
|
float diffuseFactor = clamp(1.0 - (dist * 0.0015), 0.0, 1.0); // 通过调节常数控制半径感觉
|
||||||
|
vec3 diff = uLightsColor[i] * radiance * diffuseFactor;
|
||||||
|
lighting += diff;
|
||||||
|
|
||||||
|
// 简单高光(基于视向与反射的大致模拟,产生亮点)
|
||||||
|
vec3 lightDir3 = normalize(vec3(toLight, 0.0));
|
||||||
|
vec3 viewDir = vec3(0.0, 0.0, 1.0);
|
||||||
|
vec3 normal = vec3(0.0, 0.0, 1.0);
|
||||||
|
vec3 reflectDir = reflect(-lightDir3, normal);
|
||||||
|
float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), 16.0); // 16 为高光粗糙度,可调
|
||||||
|
float specIntensity = 0.2; // 高光强度系数
|
||||||
|
specularAccum += uLightsColor[i] * radiance * specFactor * specIntensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalColor *= min(lighting, vec3(2.0));
|
// 限制光照的最大值以避免过曝(可根据场景调整)
|
||||||
|
vec3 totalLighting = min(lighting + specularAccum, vec3(2.0));
|
||||||
|
|
||||||
if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb;
|
// 将光照应用到基础颜色
|
||||||
else if (uBlendMode == 2) finalColor.rgb = tex.rgb * uColor.rgb;
|
vec3 finalColor = baseColor * totalLighting;
|
||||||
else if (uBlendMode == 3) finalColor.rgb = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb);
|
|
||||||
|
|
||||||
float alpha = tex.a * uOpacity;
|
// 支持简单混合模式(保留原有行为)
|
||||||
if (alpha <= 0.001) discard;
|
if (uBlendMode == 1) finalColor = tex.rgb + uColor.rgb;
|
||||||
|
else if (uBlendMode == 2) finalColor = tex.rgb * uColor.rgb;
|
||||||
|
else if (uBlendMode == 3) finalColor = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb);
|
||||||
|
|
||||||
|
finalColor = clamp(finalColor, 0.0, 1.0);
|
||||||
FragColor = vec4(finalColor, alpha);
|
FragColor = vec4(finalColor, alpha);
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
@@ -228,31 +268,33 @@ public final class ModelRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void uploadLightsToShader(ShaderProgram sp, Model2D model) {
|
private static void uploadLightsToShader(ShaderProgram sp, Model2D model) {
|
||||||
List<LightSource> lights = model.getLights();
|
List<com.chuangzhou.vivid2D.render.model.util.LightSource> lights = model.getLights();
|
||||||
int lightCount = Math.min(lights.size(), 8);
|
int idx = 0;
|
||||||
|
|
||||||
// 设置光源数量
|
// 只上传已启用的光源,最多 MAX_LIGHTS(8)
|
||||||
setUniformIntInternal(sp, "uLightCount", lightCount);
|
for (int i = 0; i < lights.size() && idx < 8; i++) {
|
||||||
|
com.chuangzhou.vivid2D.render.model.util.LightSource l = lights.get(i);
|
||||||
for (int i = 0; i < lightCount; i++) {
|
|
||||||
LightSource l = lights.get(i);
|
|
||||||
if (!l.isEnabled()) continue;
|
if (!l.isEnabled()) continue;
|
||||||
|
|
||||||
// 设置光源位置(环境光位置设为0)
|
// 环境光的 position 在 shader 中不会用于距离计算,但我们也上传(安全)
|
||||||
Vector2f pos = l.isAmbient() ? new Vector2f(0, 0) : l.getPosition();
|
setUniformVec2Internal(sp, "uLightsPos[" + idx + "]", l.isAmbient() ? new org.joml.Vector2f(0f, 0f) : l.getPosition());
|
||||||
setUniformVec2Internal(sp, "uLightsPos[" + i + "]", pos);
|
setUniformVec3Internal(sp, "uLightsColor[" + idx + "]", l.getColor());
|
||||||
|
setUniformFloatInternal(sp, "uLightsIntensity[" + idx + "]", l.getIntensity());
|
||||||
|
setUniformIntInternal(sp, "uLightsIsAmbient[" + idx + "]", l.isAmbient() ? 1 : 0);
|
||||||
|
|
||||||
setUniformVec3Internal(sp, "uLightsColor[" + i + "]", l.getColor());
|
idx++;
|
||||||
setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", l.getIntensity());
|
|
||||||
|
|
||||||
// 设置是否为环境光
|
|
||||||
setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", l.isAmbient() ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用未使用的光源
|
// 上传实际有效光源数量
|
||||||
for (int i = lightCount; i < 8; i++) {
|
setUniformIntInternal(sp, "uLightCount", idx);
|
||||||
|
|
||||||
|
// 禁用剩余槽位(确保 shader 中不会读取到垃圾值)
|
||||||
|
for (int i = idx; i < 8; i++) {
|
||||||
setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f);
|
setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f);
|
||||||
setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0);
|
setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0);
|
||||||
|
// color/pos 不严格必要,但清零更稳健
|
||||||
|
setUniformVec3Internal(sp, "uLightsColor[" + i + "]", new org.joml.Vector3f(0f, 0f, 0f));
|
||||||
|
setUniformVec2Internal(sp, "uLightsPos[" + i + "]", new org.joml.Vector2f(0f, 0f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -793,6 +793,7 @@ public class ModelData implements Serializable {
|
|||||||
|
|
||||||
// ==================== 内部数据类 ====================
|
// ==================== 内部数据类 ====================
|
||||||
|
|
||||||
|
// ====== 修改后的 PartData(包含液化数据的序列化/反序列化) ======
|
||||||
public static class PartData implements Serializable {
|
public static class PartData implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@@ -806,9 +807,12 @@ public class ModelData implements Serializable {
|
|||||||
public List<String> meshNames;
|
public List<String> meshNames;
|
||||||
public Map<String, String> userData;
|
public Map<String, String> userData;
|
||||||
|
|
||||||
// 新增:保存变形器数据
|
// 保存变形器数据
|
||||||
public List<DeformerData> deformers;
|
public List<DeformerData> deformers;
|
||||||
|
|
||||||
|
// 保存液化笔划数据(可保存多个笔划)
|
||||||
|
public List<LiquifyStrokeData> liquifyStrokes;
|
||||||
|
|
||||||
public PartData() {
|
public PartData() {
|
||||||
this.position = new Vector2f();
|
this.position = new Vector2f();
|
||||||
this.rotation = 0.0f;
|
this.rotation = 0.0f;
|
||||||
@@ -818,6 +822,7 @@ public class ModelData implements Serializable {
|
|||||||
this.meshNames = new ArrayList<>();
|
this.meshNames = new ArrayList<>();
|
||||||
this.userData = new HashMap<>();
|
this.userData = new HashMap<>();
|
||||||
this.deformers = new ArrayList<>();
|
this.deformers = new ArrayList<>();
|
||||||
|
this.liquifyStrokes = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartData(ModelPart part) {
|
public PartData(ModelPart part) {
|
||||||
@@ -850,6 +855,105 @@ public class ModelData implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试通过反射收集液化笔划数据(兼容性:如果 ModelPart 没有对应 API,则跳过)
|
||||||
|
try {
|
||||||
|
// 期望的方法签名: public List<YourStrokeClass> getLiquifyStrokes()
|
||||||
|
java.lang.reflect.Method m = part.getClass().getMethod("getLiquifyStrokes");
|
||||||
|
Object strokesObj = m.invoke(part);
|
||||||
|
if (strokesObj instanceof List<?>) {
|
||||||
|
List<?> strokes = (List<?>) strokesObj;
|
||||||
|
for (Object s : strokes) {
|
||||||
|
// 支持两种情况:存储为自定义类型(有 getMode/getRadius/getStrength/getIterations/getPoints 方法)
|
||||||
|
// 或者直接存储为通用 Map/POJO。我们做宽松处理:通过反射尽可能读取常见字段。
|
||||||
|
LiquifyStrokeData strokeData = new LiquifyStrokeData();
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method gm = s.getClass().getMethod("getMode");
|
||||||
|
Object modeObj = gm.invoke(s);
|
||||||
|
if (modeObj != null) strokeData.mode = modeObj.toString();
|
||||||
|
} catch (NoSuchMethodException ignored) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method gr = s.getClass().getMethod("getRadius");
|
||||||
|
Object r = gr.invoke(s);
|
||||||
|
if (r instanceof Number) strokeData.radius = ((Number) r).floatValue();
|
||||||
|
} catch (NoSuchMethodException ignored) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method gs = s.getClass().getMethod("getStrength");
|
||||||
|
Object st = gs.invoke(s);
|
||||||
|
if (st instanceof Number) strokeData.strength = ((Number) st).floatValue();
|
||||||
|
} catch (NoSuchMethodException ignored) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method gi = s.getClass().getMethod("getIterations");
|
||||||
|
Object it = gi.invoke(s);
|
||||||
|
if (it instanceof Number) strokeData.iterations = ((Number) it).intValue();
|
||||||
|
} catch (NoSuchMethodException ignored) {}
|
||||||
|
|
||||||
|
// 读取点列表
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method gp = s.getClass().getMethod("getPoints");
|
||||||
|
Object ptsObj = gp.invoke(s);
|
||||||
|
if (ptsObj instanceof List<?>) {
|
||||||
|
List<?> pts = (List<?>) ptsObj;
|
||||||
|
for (Object p : pts) {
|
||||||
|
// 支持 Vector2f 或自定义点类型(有 getX/getY/getPressure)
|
||||||
|
LiquifyPointData pd = new LiquifyPointData();
|
||||||
|
if (p instanceof org.joml.Vector2f) {
|
||||||
|
org.joml.Vector2f v = (org.joml.Vector2f) p;
|
||||||
|
pd.x = v.x;
|
||||||
|
pd.y = v.y;
|
||||||
|
pd.pressure = 1.0f;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method px = p.getClass().getMethod("getX");
|
||||||
|
java.lang.reflect.Method py = p.getClass().getMethod("getY");
|
||||||
|
Object ox = px.invoke(p);
|
||||||
|
Object oy = py.invoke(p);
|
||||||
|
if (ox instanceof Number && oy instanceof Number) {
|
||||||
|
pd.x = ((Number) ox).floatValue();
|
||||||
|
pd.y = ((Number) oy).floatValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method pp = p.getClass().getMethod("getPressure");
|
||||||
|
Object op = pp.invoke(p);
|
||||||
|
if (op instanceof Number) pd.pressure = ((Number) op).floatValue();
|
||||||
|
} catch (NoSuchMethodException ignored2) {
|
||||||
|
pd.pressure = 1.0f;
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException ex) {
|
||||||
|
// 最后尝试 Map 形式(键 x,y)
|
||||||
|
if (p instanceof Map<?, ?>) {
|
||||||
|
Map<?, ?> mapP = (Map<?, ?>) p;
|
||||||
|
Object ox = mapP.get("x");
|
||||||
|
Object oy = mapP.get("y");
|
||||||
|
if (ox instanceof Number && oy instanceof Number) {
|
||||||
|
pd.x = ((Number) ox).floatValue();
|
||||||
|
pd.y = ((Number) oy).floatValue();
|
||||||
|
}
|
||||||
|
Object op = mapP.get("pressure");
|
||||||
|
if (op instanceof Number) pd.pressure = ((Number) op).floatValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strokeData.points.add(pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException ignored) {}
|
||||||
|
|
||||||
|
// 如果没有 mode,则用默认 PUSH
|
||||||
|
if (strokeData.mode == null) strokeData.mode = ModelPart.LiquifyMode.PUSH.name();
|
||||||
|
|
||||||
|
this.liquifyStrokes.add(strokeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
// ModelPart 没有 getLiquifyStrokes 方法,跳过(向后兼容)
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
// 设置父级名称
|
// 设置父级名称
|
||||||
if (part.getParent() != null) {
|
if (part.getParent() != null) {
|
||||||
this.parentName = part.getParent().getName();
|
this.parentName = part.getParent().getName();
|
||||||
@@ -905,6 +1009,37 @@ public class ModelData implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 反序列化液化笔划:如果 PartData 中存在 liquifyStrokes,尝试在新创建的 part 上重放这些笔划
|
||||||
|
if (liquifyStrokes != null && !liquifyStrokes.isEmpty()) {
|
||||||
|
for (LiquifyStrokeData stroke : liquifyStrokes) {
|
||||||
|
// 尝试将 mode 转换为 ModelPart.LiquifyMode
|
||||||
|
ModelPart.LiquifyMode modeEnum = ModelPart.LiquifyMode.PUSH;
|
||||||
|
try {
|
||||||
|
modeEnum = ModelPart.LiquifyMode.valueOf(stroke.mode);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// 对每个点进行重放:调用 applyLiquify(存在于 ModelPart)
|
||||||
|
if (stroke.points != null) {
|
||||||
|
for (LiquifyPointData p : stroke.points) {
|
||||||
|
try {
|
||||||
|
part.applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果 applyLiquify 不存在或签名不匹配,则尝试通过反射调用名为 applyLiquify 的方法
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method am = part.getClass().getMethod("applyLiquify", Vector2f.class, float.class, float.class, ModelPart.LiquifyMode.class, int.class);
|
||||||
|
am.invoke(part, new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations);
|
||||||
|
} catch (NoSuchMethodException nsme) {
|
||||||
|
// 无法恢复液化(该 ModelPart 可能不支持液化存储/播放),跳过
|
||||||
|
break;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
@@ -933,6 +1068,29 @@ public class ModelData implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 深拷贝 liquifyStrokes
|
||||||
|
copy.liquifyStrokes = new ArrayList<>();
|
||||||
|
if (this.liquifyStrokes != null) {
|
||||||
|
for (LiquifyStrokeData s : this.liquifyStrokes) {
|
||||||
|
LiquifyStrokeData cs = new LiquifyStrokeData();
|
||||||
|
cs.mode = s.mode;
|
||||||
|
cs.radius = s.radius;
|
||||||
|
cs.strength = s.strength;
|
||||||
|
cs.iterations = s.iterations;
|
||||||
|
cs.points = new ArrayList<>();
|
||||||
|
if (s.points != null) {
|
||||||
|
for (LiquifyPointData p : s.points) {
|
||||||
|
LiquifyPointData cp = new LiquifyPointData();
|
||||||
|
cp.x = p.x;
|
||||||
|
cp.y = p.y;
|
||||||
|
cp.pressure = p.pressure;
|
||||||
|
cs.points.add(cp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy.liquifyStrokes.add(cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,8 +1103,38 @@ public class ModelData implements Serializable {
|
|||||||
public String name;
|
public String name;
|
||||||
public Map<String, String> properties; // 由 Deformer.serialization 填充
|
public Map<String, String> properties; // 由 Deformer.serialization 填充
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:液化笔划数据(Serializable)
|
||||||
|
* 每个笔划有若干点以及笔划级别参数(mode/radius/strength/iterations)
|
||||||
|
*/
|
||||||
|
public static class LiquifyStrokeData implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// LiquifyMode 的 name(),例如 "PUSH", "PULL" 等
|
||||||
|
public String mode = ModelPart.LiquifyMode.PUSH.name();
|
||||||
|
|
||||||
|
// 画笔半径与强度(用于重放)
|
||||||
|
public float radius = 50.0f;
|
||||||
|
public float strength = 0.5f;
|
||||||
|
public int iterations = 1;
|
||||||
|
|
||||||
|
// 笔划包含的点序列(世界坐标)
|
||||||
|
public List<LiquifyPointData> points = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:单个液化点数据
|
||||||
|
*/
|
||||||
|
public static class LiquifyPointData implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
public float x;
|
||||||
|
public float y;
|
||||||
|
public float pressure = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网格数据
|
* 网格数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -39,12 +39,25 @@ public class ModelPart {
|
|||||||
|
|
||||||
// ==================== 变形系统 ====================
|
// ==================== 变形系统 ====================
|
||||||
private final List<Deformer> deformers;
|
private final List<Deformer> deformers;
|
||||||
|
private final List<LiquifyStroke> liquifyStrokes = new ArrayList<>();
|
||||||
|
|
||||||
// ==================== 状态标记 ====================
|
// ==================== 状态标记 ====================
|
||||||
private boolean transformDirty;
|
private boolean transformDirty;
|
||||||
private boolean boundsDirty;
|
private boolean boundsDirty;
|
||||||
private boolean pivotInitialized;
|
private boolean pivotInitialized;
|
||||||
|
|
||||||
|
// ====== 液化模式枚举 ======
|
||||||
|
public enum LiquifyMode {
|
||||||
|
PUSH, // 推开(从画笔中心向外推)
|
||||||
|
PULL, // 拉近(向画笔中心吸)
|
||||||
|
SWIRL_CW, // 顺时针旋转
|
||||||
|
SWIRL_CCW, // 逆时针旋转
|
||||||
|
BLOAT, // 鼓起(放大)
|
||||||
|
PINCH, // 收缩(缩小)
|
||||||
|
SMOOTH, // 平滑(邻域平均)
|
||||||
|
TURBULENCE // 湍流(噪声扰动)
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 构造器 ====================
|
// ==================== 构造器 ====================
|
||||||
|
|
||||||
public ModelPart() {
|
public ModelPart() {
|
||||||
@@ -176,6 +189,208 @@ public class ModelPart {
|
|||||||
transformDirty = false;
|
transformDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前部件记录的所有液化笔划(用于序列化 / 导出)
|
||||||
|
*/
|
||||||
|
public List<LiquifyStroke> getLiquifyStrokes() {
|
||||||
|
return liquifyStrokes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加并记录一个完整的液化笔划(不会自动在调用时 apply,除非你在调用后显式重放)
|
||||||
|
*/
|
||||||
|
public void addLiquifyStroke(LiquifyStroke stroke) {
|
||||||
|
if (stroke != null) liquifyStrokes.add(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除已记录的所有液化笔划
|
||||||
|
*/
|
||||||
|
public void clearLiquifyStrokes() {
|
||||||
|
liquifyStrokes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重放并应用某个记录的液化笔划(将对每个点调用 applyLiquify)
|
||||||
|
* 兼容 PartData 在反序列化时逐点调用 applyLiquify 的方式。
|
||||||
|
*/
|
||||||
|
public void replayLiquifyStroke(LiquifyStroke stroke) {
|
||||||
|
if (stroke == null || stroke.points == null) return;
|
||||||
|
LiquifyMode mode = stroke.mode != null ? stroke.mode : LiquifyMode.PUSH;
|
||||||
|
for (LiquifyPoint p : stroke.points) {
|
||||||
|
applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对当前部件下所有网格应用液化笔效果(类似 Photoshop 的液化工具)。
|
||||||
|
* 请在使用前注册在使用addLiquifyStroke方法注册LiquifyStroke
|
||||||
|
*
|
||||||
|
* 注意:
|
||||||
|
* - brushCenter 使用世界坐标(与 ModelPart 的世界坐标体系一致)。
|
||||||
|
* - radius 为画笔半径(像素),strength 为强度(建议范围 0.0 - 1.0,数值越大效果越强)。
|
||||||
|
* - mode 选择液化操作类型。
|
||||||
|
* - iterations 为迭代次数(>0),可用来让效果更平滑,默认 1 次即可。
|
||||||
|
*
|
||||||
|
* 该方法会直接修改 mesh 的顶点并更新其边界(mesh.updateBounds)。
|
||||||
|
*/
|
||||||
|
public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations) {
|
||||||
|
if (radius <= 0f || strength == 0f || iterations <= 0) return;
|
||||||
|
|
||||||
|
// 限制 strength 到合理范围,避免过大造成畸变
|
||||||
|
float s = Math.max(-5f, Math.min(5f, strength));
|
||||||
|
|
||||||
|
// 随机用于 Turbulence
|
||||||
|
java.util.Random rand = new java.util.Random();
|
||||||
|
|
||||||
|
// 迭代多个小步以获得平滑结果
|
||||||
|
for (int iter = 0; iter < iterations; iter++) {
|
||||||
|
// 对每个网格执行液化
|
||||||
|
for (Mesh2D mesh : meshes) {
|
||||||
|
int vc = mesh.getVertexCount();
|
||||||
|
if (vc <= 0) continue;
|
||||||
|
|
||||||
|
// 预取顶点副本,以便在单次迭代中使用原始邻域数据(避免串联影响)
|
||||||
|
Vector2f[] original = new Vector2f[vc];
|
||||||
|
for (int i = 0; i < vc; i++) {
|
||||||
|
Vector2f v = mesh.getVertex(i);
|
||||||
|
original[i] = new Vector2f(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对每个顶点计算影响
|
||||||
|
for (int i = 0; i < vc; i++) {
|
||||||
|
Vector2f vOrig = original[i];
|
||||||
|
float dx = vOrig.x - brushCenter.x;
|
||||||
|
float dy = vOrig.y - brushCenter.y;
|
||||||
|
float dist = (float) Math.hypot(dx, dy);
|
||||||
|
|
||||||
|
if (dist > radius) continue;
|
||||||
|
|
||||||
|
// falloff 使用平滑步进(smoothstep)
|
||||||
|
float t = 1.0f - (dist / radius); // 0..1, 1 在中心
|
||||||
|
float falloff = t * t * (3f - 2f * t); // smoothstep
|
||||||
|
|
||||||
|
// 基本影响量
|
||||||
|
float influence = falloff * s;
|
||||||
|
|
||||||
|
// 目标点(工作副本)
|
||||||
|
Vector2f vNew = new Vector2f(vOrig);
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case PUSH -> {
|
||||||
|
// 推开:沿着从中心到点的方向推动(与 PS push 含义一致)
|
||||||
|
if (dist < 1e-6f) {
|
||||||
|
// 随机方向避免除0
|
||||||
|
float ang = rand.nextFloat() * (float) (2.0 * Math.PI);
|
||||||
|
vNew.x += (float) Math.cos(ang) * influence;
|
||||||
|
vNew.y += (float) Math.sin(ang) * influence;
|
||||||
|
} else {
|
||||||
|
vNew.x += (dx / dist) * influence * (radius * 0.02f);
|
||||||
|
vNew.y += (dy / dist) * influence * (radius * 0.02f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PULL -> {
|
||||||
|
// 拉近:沿着从点到中心的方向拉动(和 PUSH 相反)
|
||||||
|
if (dist < 1e-6f) {
|
||||||
|
float ang = rand.nextFloat() * (float) (2.0 * Math.PI);
|
||||||
|
vNew.x -= (float) Math.cos(ang) * influence;
|
||||||
|
vNew.y -= (float) Math.sin(ang) * influence;
|
||||||
|
} else {
|
||||||
|
vNew.x -= (dx / dist) * influence * (radius * 0.02f);
|
||||||
|
vNew.y -= (dy / dist) * influence * (radius * 0.02f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SWIRL_CW, SWIRL_CCW -> {
|
||||||
|
// 旋转:绕画笔中心旋转一定角度,距离越近角度越大
|
||||||
|
float dir = (mode == LiquifyMode.SWIRL_CW) ? -1f : 1f;
|
||||||
|
// 角度基于 influence(建议 strength 为接近 1 时产生 0.5-1 弧度级别)
|
||||||
|
float maxAngle = 1.0f * s; // 可调:s 控制总体角度
|
||||||
|
float angle = dir * maxAngle * falloff;
|
||||||
|
vNew = rotateAround(vOrig, brushCenter, angle);
|
||||||
|
}
|
||||||
|
case BLOAT -> {
|
||||||
|
// 鼓起:远离中心(按比例放大)
|
||||||
|
if (dist < 1e-6f) {
|
||||||
|
// 随机方向扩大
|
||||||
|
float ang = rand.nextFloat() * (float) (2.0 * Math.PI);
|
||||||
|
vNew.x += (float) Math.cos(ang) * Math.abs(influence);
|
||||||
|
vNew.y += (float) Math.sin(ang) * Math.abs(influence);
|
||||||
|
} else {
|
||||||
|
float factor = 1.0f + Math.abs(influence) * 0.08f; // scale multiplier
|
||||||
|
vNew.x = brushCenter.x + (vOrig.x - brushCenter.x) * factor * (1.0f * falloff + (1.0f - falloff));
|
||||||
|
vNew.y = brushCenter.y + (vOrig.y - brushCenter.y) * factor * (1.0f * falloff + (1.0f - falloff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PINCH -> {
|
||||||
|
// 收缩:向中心缩放
|
||||||
|
if (dist < 1e-6f) {
|
||||||
|
float ang = rand.nextFloat() * (float) (2.0 * Math.PI);
|
||||||
|
vNew.x -= (float) Math.cos(ang) * Math.abs(influence);
|
||||||
|
vNew.y -= (float) Math.sin(ang) * Math.abs(influence);
|
||||||
|
} else {
|
||||||
|
float factor = 1.0f - Math.abs(influence) * 0.08f;
|
||||||
|
factor = Math.max(0.01f, factor);
|
||||||
|
vNew.x = brushCenter.x + (vOrig.x - brushCenter.x) * factor * (1.0f * falloff + (1.0f - falloff));
|
||||||
|
vNew.y = brushCenter.y + (vOrig.y - brushCenter.y) * factor * (1.0f * falloff + (1.0f - falloff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SMOOTH -> {
|
||||||
|
// 平滑:计算邻域点的平均并向平均点移动
|
||||||
|
Vector2f avg = new Vector2f(0f, 0f);
|
||||||
|
int count = 0;
|
||||||
|
float neighborRadius = Math.max(1.0f, radius * 0.25f);
|
||||||
|
for (int j = 0; j < vc; j++) {
|
||||||
|
Vector2f vj = original[j];
|
||||||
|
if (vOrig.distance(vj) <= neighborRadius) {
|
||||||
|
avg.add(vj);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
avg.mul(1.0f / count);
|
||||||
|
// 向平均位置移动,强度受 influence 控制
|
||||||
|
vNew.x = vOrig.x + (avg.x - vOrig.x) * (Math.abs(influence) * 0.5f * falloff);
|
||||||
|
vNew.y = vOrig.y + (avg.y - vOrig.y) * (Math.abs(influence) * 0.5f * falloff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case TURBULENCE -> {
|
||||||
|
// 湍流:基于噪声/随机的小位移叠加
|
||||||
|
float jitter = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * radius * 0.005f;
|
||||||
|
float jitter2 = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * radius * 0.005f;
|
||||||
|
vNew.x += jitter * falloff;
|
||||||
|
vNew.y += jitter2 * falloff;
|
||||||
|
}
|
||||||
|
default -> { /* no-op */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写回顶点(直接覆盖)
|
||||||
|
mesh.setVertex(i, vNew.x, vNew.y);
|
||||||
|
} // end for vertices
|
||||||
|
|
||||||
|
// 更新网格边界
|
||||||
|
try {
|
||||||
|
mesh.updateBounds();
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
} // end for meshes
|
||||||
|
|
||||||
|
// 标记需要重新计算边界
|
||||||
|
this.boundsDirty = true;
|
||||||
|
} // end iterations
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助:绕中心旋转点
|
||||||
|
*/
|
||||||
|
private static Vector2f rotateAround(Vector2f point, Vector2f center, float angleRad) {
|
||||||
|
float cos = (float) Math.cos(angleRad);
|
||||||
|
float sin = (float) Math.sin(angleRad);
|
||||||
|
float x = point.x - center.x;
|
||||||
|
float y = point.y - center.y;
|
||||||
|
float rx = x * cos - y * sin;
|
||||||
|
float ry = x * sin + y * cos;
|
||||||
|
return new Vector2f(center.x + rx, center.y + ry);
|
||||||
|
}
|
||||||
|
|
||||||
public void draw(int shaderProgram, org.joml.Matrix3f parentTransform) {
|
public void draw(int shaderProgram, org.joml.Matrix3f parentTransform) {
|
||||||
// 先确保 worldTransform 是最新的
|
// 先确保 worldTransform 是最新的
|
||||||
updateWorldTransform(parentTransform, false);
|
updateWorldTransform(parentTransform, false);
|
||||||
@@ -592,6 +807,58 @@ public class ModelPart {
|
|||||||
return new ArrayList<>(deformers);
|
return new ArrayList<>(deformers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== 新增:单个液化点数据结构(Serializable 友好,并提供 getter) ======
|
||||||
|
public static class LiquifyPoint {
|
||||||
|
public float x;
|
||||||
|
public float y;
|
||||||
|
public float pressure = 1.0f;
|
||||||
|
|
||||||
|
public LiquifyPoint() {}
|
||||||
|
|
||||||
|
public LiquifyPoint(float x, float y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiquifyPoint(float x, float y, float pressure) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.pressure = pressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getX() { return x; }
|
||||||
|
public float getY() { return y; }
|
||||||
|
public float getPressure() { return pressure; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 液化笔划数据结构(包含点序列与笔划参数),提供 getter 以便反射读取 ======
|
||||||
|
public static class LiquifyStroke {
|
||||||
|
public LiquifyMode mode = LiquifyMode.PUSH;
|
||||||
|
public float radius = 50.0f;
|
||||||
|
public float strength = 0.5f;
|
||||||
|
public int iterations = 1;
|
||||||
|
public List<LiquifyPoint> points = new ArrayList<>();
|
||||||
|
|
||||||
|
public LiquifyStroke() {}
|
||||||
|
|
||||||
|
public LiquifyStroke(LiquifyMode mode, float radius, float strength, int iterations) {
|
||||||
|
this.mode = mode;
|
||||||
|
this.radius = radius;
|
||||||
|
this.strength = strength;
|
||||||
|
this.iterations = iterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMode() { return mode.name(); } // PartData 反射时读取字符串也可
|
||||||
|
public float getRadius() { return radius; }
|
||||||
|
public float getStrength() { return strength; }
|
||||||
|
public int getIterations() { return iterations; }
|
||||||
|
public List<LiquifyPoint> getPoints() { return points; }
|
||||||
|
|
||||||
|
public void addPoint(float x, float y, float pressure) {
|
||||||
|
this.points.add(new LiquifyPoint(x, y, pressure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 枚举和内部类 ====================
|
// ==================== 枚举和内部类 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class Texture {
|
|||||||
// ==================== 状态管理 ====================
|
// ==================== 状态管理 ====================
|
||||||
private boolean disposed = false;
|
private boolean disposed = false;
|
||||||
private final long creationTime;
|
private final long creationTime;
|
||||||
|
private int previousActiveTexture = -1;
|
||||||
|
|
||||||
// ==================== 静态管理 ====================
|
// ==================== 静态管理 ====================
|
||||||
private static final Map<String, Texture> TEXTURE_CACHE = new HashMap<>();
|
private static final Map<String, Texture> TEXTURE_CACHE = new HashMap<>();
|
||||||
@@ -320,6 +321,68 @@ public class Texture {
|
|||||||
|
|
||||||
// ==================== 纹理参数设置 ====================
|
// ==================== 纹理参数设置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从当前纹理裁剪出一个子纹理并返回新的 Texture。
|
||||||
|
* 仅支持 UNSIGNED_BYTE 类型的纹理(常见的 8-bit per component 图像)。
|
||||||
|
*
|
||||||
|
* @param x 裁剪区域左上角 X(像素)
|
||||||
|
* @param y 裁剪区域左上角 Y(像素)
|
||||||
|
* @param w 裁剪区域宽度(像素)
|
||||||
|
* @param h 裁剪区域高度(像素)
|
||||||
|
* @param newName 新纹理名称
|
||||||
|
* @return 新创建的子纹理
|
||||||
|
*/
|
||||||
|
public Texture crop(int x, int y, int w, int h, String newName) {
|
||||||
|
if (disposed) {
|
||||||
|
throw new IllegalStateException("Cannot crop disposed texture: " + name);
|
||||||
|
}
|
||||||
|
if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > width || y + h > height) {
|
||||||
|
throw new IllegalArgumentException("Crop rectangle out of bounds");
|
||||||
|
}
|
||||||
|
if (type != TextureType.UNSIGNED_BYTE) {
|
||||||
|
throw new UnsupportedOperationException("Crop currently only supported for UNSIGNED_BYTE textures");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保有像素缓存(若没有则尝试从 GPU 提取)
|
||||||
|
ensurePixelDataCached();
|
||||||
|
if (!hasPixelData()) {
|
||||||
|
throw new RuntimeException("No pixel data available for cropping texture: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
int comps = format.getComponents();
|
||||||
|
int rowSrcBytes = width * comps;
|
||||||
|
int rowDstBytes = w * comps;
|
||||||
|
byte[] cropped = new byte[w * h * comps];
|
||||||
|
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
int srcPos = ((y + row) * width + x) * comps;
|
||||||
|
int dstPos = row * rowDstBytes;
|
||||||
|
System.arraycopy(this.pixelDataCache, srcPos, cropped, dstPos, rowDstBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将裁剪数据上传到新纹理
|
||||||
|
ByteBuffer buffer = MemoryUtil.memAlloc(cropped.length);
|
||||||
|
buffer.put(cropped);
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Texture newTex = new Texture(newName, w, h, this.format, buffer);
|
||||||
|
// 复制参数
|
||||||
|
newTex.setMinFilter(this.minFilter);
|
||||||
|
newTex.setMagFilter(this.magFilter);
|
||||||
|
newTex.setWrapS(this.wrapS);
|
||||||
|
newTex.setWrapT(this.wrapT);
|
||||||
|
if (this.mipmapsEnabled && newTex.isPowerOfTwo(w) && newTex.isPowerOfTwo(h)) {
|
||||||
|
newTex.generateMipmaps();
|
||||||
|
}
|
||||||
|
// 缓存像素数据以便后续使用
|
||||||
|
newTex.ensurePixelDataCached();
|
||||||
|
return newTex;
|
||||||
|
} finally {
|
||||||
|
MemoryUtil.memFree(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置最小化过滤器
|
* 设置最小化过滤器
|
||||||
*/
|
*/
|
||||||
@@ -398,20 +461,29 @@ public class Texture {
|
|||||||
throw new IllegalStateException("Cannot bind disposed texture: " + name);
|
throw new IllegalStateException("Cannot bind disposed texture: " + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全地激活纹理单元
|
if (textureUnit < 0 || textureUnit >= 32) { // 合理的纹理单元范围
|
||||||
if (textureUnit >= 0 && textureUnit < 32) { // 合理的纹理单元范围
|
|
||||||
try {
|
|
||||||
GL13.glActiveTexture(GL13.GL_TEXTURE0 + textureUnit);
|
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
|
||||||
checkGLError("glBindTexture");
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 如果 GL13 不可用,回退到基本方法
|
|
||||||
System.err.println("Warning: GL13 not available, using fallback texture binding");
|
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid texture unit: " + textureUnit);
|
throw new IllegalArgumentException("Invalid texture unit: " + textureUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果支持 GL13,保存当前活动纹理单元并激活目标单元,绑定纹理后保持可以恢复
|
||||||
|
boolean hasGL13 = GL.getCapabilities().OpenGL13;
|
||||||
|
if (hasGL13) {
|
||||||
|
try {
|
||||||
|
// 保存之前的活动纹理单元(返回值是 GL_TEXTUREi 的枚举值)
|
||||||
|
previousActiveTexture = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE);
|
||||||
|
GL13.glActiveTexture(GL13.GL_TEXTURE0 + textureUnit);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果查询/激活失败,重置标志,不影响后续绑定(仍尝试绑定)
|
||||||
|
previousActiveTexture = -1;
|
||||||
|
System.err.println("Warning: failed to change active texture unit: " + e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previousActiveTexture = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定纹理到当前(已激活的)纹理单元
|
||||||
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
|
||||||
|
checkGLError("glBindTexture");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -425,8 +497,21 @@ public class Texture {
|
|||||||
* 解绑纹理
|
* 解绑纹理
|
||||||
*/
|
*/
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
|
// 解绑当前纹理目标
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
// 如果之前保存了活动纹理单元并且支持 GL13,尝试恢复它
|
||||||
|
try {
|
||||||
|
if (previousActiveTexture != -1 && GL.getCapabilities().OpenGL13) {
|
||||||
|
GL13.glActiveTexture(previousActiveTexture);
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Warning: failed to restore previous active texture unit: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
previousActiveTexture = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==================== 资源管理 ====================
|
// ==================== 资源管理 ====================
|
||||||
|
|
||||||
|
|||||||
450
src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java
Normal file
450
src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
package com.chuangzhou.vivid2D.test;
|
||||||
|
|
||||||
|
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
|
import org.lwjgl.glfw.GLFWVidMode;
|
||||||
|
import org.lwjgl.opengl.GL;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.lwjgl.glfw.GLFW.*;
|
||||||
|
import static org.lwjgl.opengl.GL11.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModelLoadTest - enhanced debug loader
|
||||||
|
*
|
||||||
|
* - 加载模型后会自动检查 model 内部结构并打印(parts, meshes, textures)
|
||||||
|
* - 尝试把第一个 part 放到窗口中心以确保在可视范围内
|
||||||
|
* - 每帧确保 ModelRender 的 viewport 与窗口大小一致
|
||||||
|
*
|
||||||
|
* 运行前请确保 MODEL_PATH 指向你保存的 model 文件
|
||||||
|
*/
|
||||||
|
public class ModelLoadTest {
|
||||||
|
|
||||||
|
private long window;
|
||||||
|
private Model2D model;
|
||||||
|
|
||||||
|
// Window dimensions
|
||||||
|
private static int WINDOW_WIDTH = 800;
|
||||||
|
private static int WINDOW_HEIGHT = 600;
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
private boolean enableWireframe = false;
|
||||||
|
|
||||||
|
// 要加载的 model 文件路径(请根据实际保存位置修改)
|
||||||
|
private static final String MODEL_PATH = "C:\\Users\\Administrator\\Desktop\\trump_texture.model";
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
new ModelLoadTest().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
init();
|
||||||
|
loop();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() throws Exception {
|
||||||
|
if (!glfwInit()) {
|
||||||
|
throw new IllegalStateException("Unable to initialize GLFW");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure GLFW
|
||||||
|
glfwDefaultWindowHints();
|
||||||
|
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||||
|
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
|
|
||||||
|
// Create window
|
||||||
|
window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Model Load Test - DEBUG", MemoryUtil.NULL, MemoryUtil.NULL);
|
||||||
|
if (window == MemoryUtil.NULL) {
|
||||||
|
throw new RuntimeException("Failed to create the GLFW window");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center window
|
||||||
|
GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
|
||||||
|
if (vidMode != null) {
|
||||||
|
glfwSetWindowPos(
|
||||||
|
window,
|
||||||
|
(vidMode.width() - WINDOW_WIDTH) / 2,
|
||||||
|
(vidMode.height() - WINDOW_HEIGHT) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key callback
|
||||||
|
glfwSetKeyCallback(window, (w, key, scancode, action, mods) -> {
|
||||||
|
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
|
||||||
|
glfwSetWindowShouldClose(window, true);
|
||||||
|
}
|
||||||
|
if (key == GLFW_KEY_SPACE && action == GLFW_RELEASE) {
|
||||||
|
enableWireframe = !enableWireframe;
|
||||||
|
System.out.println("Wireframe: " + (enableWireframe ? "ON" : "OFF"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create OpenGL context
|
||||||
|
glfwMakeContextCurrent(window);
|
||||||
|
GL.createCapabilities();
|
||||||
|
|
||||||
|
// Show window
|
||||||
|
glfwShowWindow(window);
|
||||||
|
|
||||||
|
// 初始化渲染系统
|
||||||
|
ModelRender.initialize();
|
||||||
|
|
||||||
|
// 尝试加载 model
|
||||||
|
loadModelFromFile(MODEL_PATH);
|
||||||
|
|
||||||
|
// 一些额外检查/尝试修复
|
||||||
|
postLoadSanityChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试多种方式加载 Model2D:优先尝试静态方法 loadFromFile/fromFile/load,
|
||||||
|
* 其次尝试创建空实例并调用实例方法 loadFromFile(String)
|
||||||
|
*/
|
||||||
|
private void loadModelFromFile(String path) {
|
||||||
|
inspectSerializedFileStructure( path);
|
||||||
|
File f = new File(path);
|
||||||
|
if (!f.exists()) {
|
||||||
|
System.err.println("Model file not found: " + path);
|
||||||
|
model = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试静态工厂方法
|
||||||
|
try {
|
||||||
|
Method m;
|
||||||
|
String[] names = {"loadFromFile", "fromFile", "load"};
|
||||||
|
for (String name : names) {
|
||||||
|
try {
|
||||||
|
m = Model2D.class.getMethod(name, String.class);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
m = null;
|
||||||
|
}
|
||||||
|
if (m != null) {
|
||||||
|
Object res = m.invoke(null, path);
|
||||||
|
if (res instanceof Model2D) {
|
||||||
|
model = (Model2D) res;
|
||||||
|
System.out.println("Model loaded via static method: " + name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 继续尝试实例方法
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试实例方法
|
||||||
|
try {
|
||||||
|
Model2D inst = null;
|
||||||
|
try {
|
||||||
|
inst = Model2D.class.getConstructor().newInstance();
|
||||||
|
} catch (NoSuchMethodException nsme) {
|
||||||
|
try {
|
||||||
|
inst = Model2D.class.getConstructor(String.class).newInstance("loaded_model");
|
||||||
|
} catch (NoSuchMethodException ex) {
|
||||||
|
inst = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inst != null) {
|
||||||
|
try {
|
||||||
|
Method im = Model2D.class.getMethod("loadFromFile", String.class);
|
||||||
|
im.invoke(inst, path);
|
||||||
|
model = inst;
|
||||||
|
System.out.println("Model loaded via instance method loadFromFile");
|
||||||
|
return;
|
||||||
|
} catch (NoSuchMethodException ignored) { }
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后尝试反序列化 ObjectInputStream
|
||||||
|
try {
|
||||||
|
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream(f));
|
||||||
|
Object obj = ois.readObject();
|
||||||
|
ois.close();
|
||||||
|
if (obj instanceof Model2D) {
|
||||||
|
model = (Model2D) obj;
|
||||||
|
System.out.println("Model deserialized from file via ObjectInputStream");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
System.err.println("Deserialized object is not Model2D: " + (obj != null ? obj.getClass() : "null"));
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println("Failed to load model from file using known methods: " + path);
|
||||||
|
model = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 追加方法:反序列化内容深度检测(帮助诊断保存文件里到底有什么) ======
|
||||||
|
private void inspectSerializedFileStructure(String path) {
|
||||||
|
File f = new File(path);
|
||||||
|
if (!f.exists()) {
|
||||||
|
System.err.println("File not found: " + path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.out.println("Inspecting serialized object structure in file: " + path);
|
||||||
|
try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream(f))) {
|
||||||
|
Object obj = ois.readObject();
|
||||||
|
printObjectStructure(obj, 0, new java.util.HashSet<>());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.err.println("Failed to read/inspect serialized file: " + t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printObjectStructure(Object obj, int indent, java.util.Set<Object> seen) {
|
||||||
|
if (obj == null) {
|
||||||
|
printIndent(indent); System.out.println("null"); return;
|
||||||
|
}
|
||||||
|
if (seen.contains(obj)) {
|
||||||
|
printIndent(indent); System.out.println("<<already seen " + obj.getClass().getName() + ">>"); return;
|
||||||
|
}
|
||||||
|
seen.add(obj);
|
||||||
|
Class<?> cls = obj.getClass();
|
||||||
|
printIndent(indent); System.out.println(cls.getName());
|
||||||
|
// 如果是集合,列出元素类型/数目(不深入过深避免堆栈)
|
||||||
|
if (obj instanceof java.util.Collection) {
|
||||||
|
java.util.Collection col = (java.util.Collection) obj;
|
||||||
|
printIndent(indent+1); System.out.println("size=" + col.size());
|
||||||
|
int i = 0;
|
||||||
|
for (Object e : col) {
|
||||||
|
if (i++ > 20) { printIndent(indent+1); System.out.println("... (truncated)"); break; }
|
||||||
|
printObjectStructure(e, indent+1, seen);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (obj instanceof java.util.Map) {
|
||||||
|
java.util.Map map = (java.util.Map) obj;
|
||||||
|
printIndent(indent+1); System.out.println("size=" + map.size());
|
||||||
|
int i = 0;
|
||||||
|
for (Object k : map.keySet()) {
|
||||||
|
if (i++ > 20) { printIndent(indent+1); System.out.println("... (truncated)"); break; }
|
||||||
|
printIndent(indent+1); System.out.println("Key:");
|
||||||
|
printObjectStructure(k, indent+2, seen);
|
||||||
|
printIndent(indent+1); System.out.println("Value:");
|
||||||
|
printObjectStructure(map.get(k), indent+2, seen);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 打印字段(反射),但避免进入 Java 内部类和基本类型
|
||||||
|
java.lang.reflect.Field[] fields = cls.getDeclaredFields();
|
||||||
|
for (java.lang.reflect.Field f : fields) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object val = null;
|
||||||
|
try { val = f.get(obj); } catch (Throwable ignored) {}
|
||||||
|
printIndent(indent+1); System.out.println(f.getName() + " : " + (val == null ? "null" : val.getClass().getName()));
|
||||||
|
// 对常见自定义类型深入一层
|
||||||
|
if (val != null && !val.getClass().getName().startsWith("java.") && !val.getClass().isPrimitive()) {
|
||||||
|
if (val instanceof Number || val instanceof String) continue;
|
||||||
|
printObjectStructure(val, indent+2, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printIndent(int n) {
|
||||||
|
for (int i = 0; i < n; i++) System.out.print(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载后的一些检查与尝试性修复:
|
||||||
|
* - 打印 parts/meshes/textures 的信息
|
||||||
|
* - 如果没有 parts/meshes,提醒并退出
|
||||||
|
* - 如果有 part,尝试把第一个 part 放到窗口中心
|
||||||
|
*/
|
||||||
|
private void postLoadSanityChecks() {
|
||||||
|
if (model == null) {
|
||||||
|
System.err.println("Model is null after loading. Nothing to render.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Model loaded: " + model.getClass().getName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 通过反射尝试读取 parts / meshes / textures 等
|
||||||
|
Method getParts = tryGetMethod(Model2D.class, "getParts");
|
||||||
|
Method getMeshes = tryGetMethod(Model2D.class, "getMeshes");
|
||||||
|
Method getTextures = tryGetMethod(Model2D.class, "getTextures");
|
||||||
|
|
||||||
|
if (getParts != null) {
|
||||||
|
Object partsObj = getParts.invoke(model);
|
||||||
|
if (partsObj instanceof List) {
|
||||||
|
List<?> parts = (List<?>) partsObj;
|
||||||
|
System.out.println("Parts count: " + parts.size());
|
||||||
|
for (int i = 0; i < parts.size(); i++) {
|
||||||
|
Object p = parts.get(i);
|
||||||
|
System.out.println(" Part[" + i + "]: " + p.getClass().getName());
|
||||||
|
// 尝试获取 name / position via reflection
|
||||||
|
try {
|
||||||
|
Method getName = tryGetMethod(p.getClass(), "getName");
|
||||||
|
Method getPosition = tryGetMethod(p.getClass(), "getPosition");
|
||||||
|
Object name = getName != null ? getName.invoke(p) : "<no-name>";
|
||||||
|
Object pos = getPosition != null ? getPosition.invoke(p) : null;
|
||||||
|
System.out.println(" name=" + name + ", pos=" + pos);
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 parts 不为空,尝试把第一个 part 放到窗口中心(如果有 setPosition)
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
Object first = parts.get(0);
|
||||||
|
try {
|
||||||
|
Method setPosition = tryGetMethod(first.getClass(), "setPosition", float.class, float.class);
|
||||||
|
if (setPosition != null) {
|
||||||
|
setPosition.invoke(first, (float)WINDOW_WIDTH/2f, (float)WINDOW_HEIGHT/2f);
|
||||||
|
System.out.println("Moved first part to window center.");
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getMeshes != null) {
|
||||||
|
Object meshesObj = getMeshes.invoke(model);
|
||||||
|
if (meshesObj instanceof List) {
|
||||||
|
List<?> meshes = (List<?>) meshesObj;
|
||||||
|
System.out.println("Meshes count: " + meshes.size());
|
||||||
|
for (int i = 0; i < Math.min(meshes.size(), 10); i++) {
|
||||||
|
Object m = meshes.get(i);
|
||||||
|
System.out.println(" Mesh[" + i + "]: " + m.getClass().getName());
|
||||||
|
// try to print vertex count / texture
|
||||||
|
try {
|
||||||
|
Method getVertexCount = tryGetMethod(m.getClass(), "getVertexCount");
|
||||||
|
Method getTexture = tryGetMethod(m.getClass(), "getTexture");
|
||||||
|
Object vc = getVertexCount != null ? getVertexCount.invoke(m) : null;
|
||||||
|
Object tex = getTexture != null ? getTexture.invoke(m) : null;
|
||||||
|
System.out.println(" vertexCount=" + vc + ", texture=" + tex);
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTextures != null) {
|
||||||
|
Object texObj = getTextures.invoke(model);
|
||||||
|
if (texObj instanceof List) {
|
||||||
|
List<?> texs = (List<?>) texObj;
|
||||||
|
System.out.println("Textures count: " + texs.size());
|
||||||
|
for (int i = 0; i < Math.min(texs.size(), 10); i++) {
|
||||||
|
Object t = texs.get(i);
|
||||||
|
System.out.println(" Texture[" + i + "]: " + t.getClass().getName());
|
||||||
|
try {
|
||||||
|
Method getW = tryGetMethod(t.getClass(), "getWidth");
|
||||||
|
Method getH = tryGetMethod(t.getClass(), "getHeight");
|
||||||
|
Method getId = tryGetMethod(t.getClass(), "getTextureId");
|
||||||
|
Object w = getW != null ? getW.invoke(t) : "?";
|
||||||
|
Object h = getH != null ? getH.invoke(t) : "?";
|
||||||
|
Object id = getId != null ? getId.invoke(t) : "?";
|
||||||
|
System.out.println(" size=" + w + "x" + h + ", id=" + id);
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.err.println("Failed to introspect model: " + t.getMessage());
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method tryGetMethod(Class<?> cls, String name, Class<?>... params) {
|
||||||
|
try {
|
||||||
|
return cls.getMethod(name, params);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loop() {
|
||||||
|
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
|
||||||
|
|
||||||
|
float last = (float) glfwGetTime();
|
||||||
|
|
||||||
|
while (!glfwWindowShouldClose(window)) {
|
||||||
|
float now = (float) glfwGetTime();
|
||||||
|
float delta = now - last;
|
||||||
|
last = now;
|
||||||
|
|
||||||
|
// 每帧确保 viewport 与窗口大小一致(避免投影错位)
|
||||||
|
IntBuffer w = MemoryUtil.memAllocInt(1);
|
||||||
|
IntBuffer h = MemoryUtil.memAllocInt(1);
|
||||||
|
glfwGetFramebufferSize(window, w, h);
|
||||||
|
int ww = Math.max(1, w.get(0));
|
||||||
|
int hh = Math.max(1, h.get(0));
|
||||||
|
MemoryUtil.memFree(w);
|
||||||
|
MemoryUtil.memFree(h);
|
||||||
|
if (ww != WINDOW_WIDTH || hh != WINDOW_HEIGHT) {
|
||||||
|
WINDOW_WIDTH = ww;
|
||||||
|
WINDOW_HEIGHT = hh;
|
||||||
|
ModelRender.setViewport(WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
|
System.out.println("Viewport updated to: " + WINDOW_WIDTH + "x" + WINDOW_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render
|
||||||
|
render(delta);
|
||||||
|
|
||||||
|
glfwSwapBuffers(window);
|
||||||
|
glfwPollEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(float deltaTime) {
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
if (enableWireframe) {
|
||||||
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||||
|
} else {
|
||||||
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model == null) {
|
||||||
|
// Nothing to render, 提示并返回
|
||||||
|
//(仅打印一次以免刷屏)
|
||||||
|
System.err.println("No model to render (model == null).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ModelRender.render(deltaTime, model);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外检查 glGetError
|
||||||
|
int err = glGetError();
|
||||||
|
if (err != GL_NO_ERROR) {
|
||||||
|
System.err.println("OpenGL error after render: 0x" + Integer.toHexString(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
System.out.println("Cleaning up...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
ModelRender.cleanup();
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (window != MemoryUtil.NULL) {
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
}
|
||||||
|
glfwTerminate();
|
||||||
|
System.out.println("Finished cleanup");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
package com.chuangzhou.vivid2D.test;
|
||||||
|
|
||||||
|
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||||
|
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
import org.lwjgl.glfw.GLFWVidMode;
|
||||||
|
import org.lwjgl.opengl.GL;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import static org.lwjgl.glfw.GLFW.*;
|
||||||
|
import static org.lwjgl.opengl.GL11.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texture Render Test Class - Debug Version
|
||||||
|
* @author tzdwindows 7
|
||||||
|
*/
|
||||||
|
public class ModelRenderTextureTest {
|
||||||
|
|
||||||
|
private long window;
|
||||||
|
private Model2D model;
|
||||||
|
|
||||||
|
// Window dimensions
|
||||||
|
private static final int WINDOW_WIDTH = 800;
|
||||||
|
private static final int WINDOW_HEIGHT = 600;
|
||||||
|
|
||||||
|
// Debug flags
|
||||||
|
private int frameCount = 0;
|
||||||
|
private double lastFpsTime = 0;
|
||||||
|
private boolean enableWireframe = false;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
new ModelRenderTextureTest().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
init();
|
||||||
|
loop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
// Initialize GLFW
|
||||||
|
if (!glfwInit()) {
|
||||||
|
throw new IllegalStateException("Unable to initialize GLFW");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure GLFW
|
||||||
|
glfwDefaultWindowHints();
|
||||||
|
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||||
|
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
|
|
||||||
|
// Create window
|
||||||
|
window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "ModelRender Texture Test - DEBUG", MemoryUtil.NULL, MemoryUtil.NULL);
|
||||||
|
if (window == MemoryUtil.NULL) {
|
||||||
|
throw new RuntimeException("Failed to create the GLFW window");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center window
|
||||||
|
GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
|
||||||
|
glfwSetWindowPos(
|
||||||
|
window,
|
||||||
|
(vidMode.width() - WINDOW_WIDTH) / 2,
|
||||||
|
(vidMode.height() - WINDOW_HEIGHT) / 2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set callbacks
|
||||||
|
glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
|
||||||
|
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
|
||||||
|
glfwSetWindowShouldClose(window, true);
|
||||||
|
}
|
||||||
|
if (key == GLFW_KEY_SPACE && action == GLFW_RELEASE) {
|
||||||
|
enableWireframe = !enableWireframe;
|
||||||
|
System.out.println("Wireframe mode: " + (enableWireframe ? "ON" : "OFF"));
|
||||||
|
}
|
||||||
|
if (key == GLFW_KEY_R && action == GLFW_RELEASE) {
|
||||||
|
recreateModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create OpenGL context
|
||||||
|
glfwMakeContextCurrent(window);
|
||||||
|
GL.createCapabilities();
|
||||||
|
|
||||||
|
// Show window
|
||||||
|
glfwShowWindow(window);
|
||||||
|
|
||||||
|
// Initialize ModelRender
|
||||||
|
ModelRender.initialize();
|
||||||
|
|
||||||
|
// Create test model
|
||||||
|
createTestModel();
|
||||||
|
|
||||||
|
System.out.println("=== DEBUG INFO ===");
|
||||||
|
System.out.println("Press SPACE to toggle wireframe mode");
|
||||||
|
System.out.println("Press R to recreate model");
|
||||||
|
System.out.println("Press ESC to exit");
|
||||||
|
System.out.println("==================");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTestModel() {
|
||||||
|
if (model != null) {
|
||||||
|
// Clean up previous model if exists
|
||||||
|
System.out.println("Cleaning up previous model...");
|
||||||
|
}
|
||||||
|
|
||||||
|
model = new Model2D("TextureTestModel");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load Trump image texture
|
||||||
|
String texturePath = "G:\\鬼畜素材\\川普\\图片\\7(DJ0MH9}`)GJYHHADQDHYN.png";
|
||||||
|
System.out.println("Loading texture: " + texturePath);
|
||||||
|
|
||||||
|
Texture texture = Texture.createFromFile("trump_texture", texturePath);
|
||||||
|
model.addTexture(texture);
|
||||||
|
|
||||||
|
System.out.println("Texture loaded: " + texture.getWidth() + "x" + texture.getHeight());
|
||||||
|
System.out.println("Texture ID: " + texture.getTextureId());
|
||||||
|
System.out.println("Texture format: " + texture.getFormat());
|
||||||
|
|
||||||
|
// 使用与工作示例相同的方式创建网格
|
||||||
|
// 根据纹理尺寸创建合适大小的四边形
|
||||||
|
float width = texture.getWidth() / 2.0f; // 缩小以适应屏幕
|
||||||
|
float height = texture.getHeight() / 2.0f;
|
||||||
|
|
||||||
|
// 使用 Mesh2D.createQuad 方法创建网格(如果可用)
|
||||||
|
Mesh2D mesh;
|
||||||
|
try {
|
||||||
|
// 尝试使用 createQuad 方法
|
||||||
|
mesh = Mesh2D.createQuad("trump_mesh", width, height);
|
||||||
|
System.out.println("Using Mesh2D.createQuad method");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 回退到手动创建顶点
|
||||||
|
System.out.println("Using manual vertex creation");
|
||||||
|
float[] vertices = {
|
||||||
|
-width/2, -height/2, // bottom left
|
||||||
|
width/2, -height/2, // bottom right
|
||||||
|
width/2, height/2, // top right
|
||||||
|
-width/2, height/2 // top left
|
||||||
|
};
|
||||||
|
|
||||||
|
float[] uvs = {
|
||||||
|
0.0f, 1.0f, // bottom left
|
||||||
|
1.0f, 1.0f, // bottom right
|
||||||
|
1.0f, 0.0f, // top right
|
||||||
|
0.0f, 0.0f // top left
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] indices = {
|
||||||
|
0, 1, 2, // first triangle
|
||||||
|
2, 3, 0 // second triangle
|
||||||
|
};
|
||||||
|
|
||||||
|
mesh = model.createMesh("trump_mesh", vertices, uvs, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.setTexture(texture);
|
||||||
|
|
||||||
|
// Create part and add mesh
|
||||||
|
ModelPart part = model.createPart("trump_part");
|
||||||
|
|
||||||
|
part.addMesh(mesh);
|
||||||
|
part.setVisible(true);
|
||||||
|
|
||||||
|
// 设置部件位置到屏幕中心(使用像素坐标)
|
||||||
|
part.setPosition(0, 0); // 800x600 窗口的中心
|
||||||
|
|
||||||
|
System.out.println("Model created:");
|
||||||
|
System.out.println(" - Part count: " + model.getParts().size());
|
||||||
|
System.out.println(" - Mesh count: " + model.getMeshes().size());
|
||||||
|
System.out.println(" - Mesh dimensions: " + width + "x" + height);
|
||||||
|
System.out.println(" - Part position: " + part.getPosition());
|
||||||
|
System.out.println(" - Mesh vertex count: " + mesh.getVertexCount());
|
||||||
|
// 测试模型保存
|
||||||
|
model.saveToFile("C:\\Users\\Administrator\\Desktop\\trump_texture.model");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to create test model: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
createFallbackModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFallbackModel() {
|
||||||
|
System.out.println("Creating fallback checkerboard model...");
|
||||||
|
|
||||||
|
// 使用与工作示例相同的模式
|
||||||
|
float width = 200;
|
||||||
|
float height = 200;
|
||||||
|
|
||||||
|
Mesh2D mesh;
|
||||||
|
try {
|
||||||
|
mesh = Mesh2D.createQuad("fallback_mesh", width, height);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 手动创建
|
||||||
|
float[] vertices = {
|
||||||
|
-width/2, -height/2,
|
||||||
|
width/2, -height/2,
|
||||||
|
width/2, height/2,
|
||||||
|
-width/2, height/2
|
||||||
|
};
|
||||||
|
|
||||||
|
float[] uvs = {
|
||||||
|
0.0f, 1.0f,
|
||||||
|
1.0f, 1.0f,
|
||||||
|
1.0f, 0.0f,
|
||||||
|
0.0f, 0.0f
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] indices = {
|
||||||
|
0, 1, 2,
|
||||||
|
2, 3, 0
|
||||||
|
};
|
||||||
|
|
||||||
|
mesh = model.createMesh("fallback_mesh", vertices, uvs, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checkerboard texture
|
||||||
|
Texture fallbackTexture = Texture.createCheckerboard(
|
||||||
|
"fallback_texture",
|
||||||
|
512, 512, 32,
|
||||||
|
0xFFFF0000, // red
|
||||||
|
0xFF0000FF // blue
|
||||||
|
);
|
||||||
|
model.addTexture(fallbackTexture);
|
||||||
|
mesh.setTexture(fallbackTexture);
|
||||||
|
|
||||||
|
ModelPart part = model.createPart("fallback_part");
|
||||||
|
part.addMesh(mesh);
|
||||||
|
part.setVisible(true);
|
||||||
|
part.setPosition(400, 300);
|
||||||
|
|
||||||
|
System.out.println("Fallback model created with size: " + width + "x" + height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recreateModel() {
|
||||||
|
System.out.println("Recreating model...");
|
||||||
|
createTestModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loop() {
|
||||||
|
// Set clear color (light gray for better visibility)
|
||||||
|
glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||||
|
|
||||||
|
double lastTime = glfwGetTime();
|
||||||
|
|
||||||
|
while (!glfwWindowShouldClose(window)) {
|
||||||
|
double currentTime = glfwGetTime();
|
||||||
|
float deltaTime = (float) (currentTime - lastTime);
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
// Calculate FPS
|
||||||
|
frameCount++;
|
||||||
|
if (currentTime - lastFpsTime >= 1.0) {
|
||||||
|
System.out.printf("FPS: %d, Delta: %.3fms\n", frameCount, deltaTime * 1000);
|
||||||
|
frameCount = 0;
|
||||||
|
lastFpsTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
render(deltaTime);
|
||||||
|
|
||||||
|
// Swap buffers and poll events
|
||||||
|
glfwSwapBuffers(window);
|
||||||
|
glfwPollEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(float deltaTime) {
|
||||||
|
// Clear screen
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Set wireframe mode if enabled
|
||||||
|
if (enableWireframe) {
|
||||||
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||||
|
} else {
|
||||||
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print debug info occasionally
|
||||||
|
if (frameCount % 120 == 0) {
|
||||||
|
System.out.println("=== RENDER DEBUG ===");
|
||||||
|
System.out.println("Model null: " + (model == null));
|
||||||
|
if (model != null) {
|
||||||
|
System.out.println("Parts: " + model.getParts().size());
|
||||||
|
System.out.println("Meshes: " + model.getMeshes().size());
|
||||||
|
if (!model.getParts().isEmpty()) {
|
||||||
|
ModelPart part = model.getParts().get(0);
|
||||||
|
System.out.println("First part visible: " + part.isVisible());
|
||||||
|
System.out.println("First part position: " + part.getPosition());
|
||||||
|
System.out.println("First part meshes: " + part.getMeshes().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("===================");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render using ModelRender
|
||||||
|
try {
|
||||||
|
ModelRender.render(deltaTime, model);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Rendering error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check OpenGL errors
|
||||||
|
int error = glGetError();
|
||||||
|
if (error != GL_NO_ERROR) {
|
||||||
|
System.err.println("OpenGL error: " + getGLErrorString(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getGLErrorString(int error) {
|
||||||
|
switch (error) {
|
||||||
|
case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||||||
|
case GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||||||
|
case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||||||
|
case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||||||
|
default: return "Unknown Error (0x" + Integer.toHexString(error) + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
System.out.println("Cleaning up resources...");
|
||||||
|
|
||||||
|
// Cleanup ModelRender
|
||||||
|
ModelRender.cleanup();
|
||||||
|
|
||||||
|
// Cleanup texture cache
|
||||||
|
Texture.cleanupAll();
|
||||||
|
|
||||||
|
// Destroy window and terminate GLFW
|
||||||
|
if (window != MemoryUtil.NULL) {
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
}
|
||||||
|
glfwTerminate();
|
||||||
|
|
||||||
|
System.out.println("Cleanup completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user