From 22c3661d6e5c7c26d0591c2cc6c0c065c11a323a Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sun, 12 Oct 2025 08:01:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E6=B7=BB=E5=8A=A0=E6=B6=B2?= =?UTF-8?q?=E5=8C=96=E7=AC=94=E5=88=92=E6=95=B0=E6=8D=AE=E7=9A=84=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=E4=B8=8E=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ModelData.PartData 中新增 liquifyStrokes 字段用于保存液化笔划- 实现通过反射读取 ModelPart 的液化笔划数据(兼容旧版本)- 支持多种数据结构形式的液化点读取(Vector2f、自定义类、Map) - 反序列化时自动重放液化笔划到 ModelPart- 添加 LiquifyStrokeData 和 LiquifyPointData 用于序列化存储 - 提供深度拷贝支持以确保 liquifyStrokes 数据完整复制 - 增加 ModelLoadTest 测试类用于验证模型加载与结构检查 --- .../vivid2D/render/ModelRender.java | 128 +++-- .../vivid2D/render/model/ModelData.java | 190 +++++++- .../vivid2D/render/model/ModelPart.java | 267 +++++++++++ .../vivid2D/render/model/util/Texture.java | 109 ++++- .../vivid2D/test/ModelLoadTest.java | 450 ++++++++++++++++++ .../vivid2D/test/ModelRenderTextureTest.java | 348 ++++++++++++++ 6 files changed, 1436 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 9b1d8fb..42930f9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -149,43 +149,83 @@ public final class ModelRender { uniform int uLightsIsAmbient[MAX_LIGHTS]; uniform int uLightCount; + // 常用衰减系数(可在 shader 内微调) + const float ATT_CONST = 1.0; + const float ATT_LINEAR = 0.09; + const float ATT_QUAD = 0.032; + 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; } - vec4 tex = texture(uTexture, vTexCoord); - vec3 finalColor = tex.rgb * uColor.rgb; - vec3 lighting = vec3(0.0); + // 基础颜色(纹理 * 部件颜色) + vec3 baseColor = tex.rgb * uColor.rgb; - 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) { lighting += uLightsColor[i] * uLightsIntensity[i]; } } - - for (int i = 0; i < uLightCount; i++) { + // 加上基线环境光 + lighting += ambient; + + // 对每个非环境光计算基于距离的衰减与简单高光 + for (int i = 0; i < uLightCount; ++i) { if (uLightsIsAmbient[i] == 1) continue; - - float intensity = uLightsIntensity[i]; - if (intensity <= 0.0) continue; - - vec2 lightDir = uLightsPos[i] - vWorldPos; - float dist = length(lightDir); - float atten = 1.0 / (1.0 + 0.1 * dist + 0.01 * dist * dist); - lighting += uLightsColor[i] * intensity * atten; + + vec2 toLight = uLightsPos[i] - vWorldPos; + float dist = length(toLight); + // 标准物理式衰减 + float attenuation = ATT_CONST / (ATT_CONST + ATT_LINEAR * dist + ATT_QUAD * dist * dist); + + // 强度受光源强度和衰减影响 + float radiance = uLightsIntensity[i] * attenuation; + + // 漫反射:在纯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)); - - if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb; - else if (uBlendMode == 2) finalColor.rgb = tex.rgb * uColor.rgb; - else if (uBlendMode == 3) finalColor.rgb = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb); - - float alpha = tex.a * uOpacity; - if (alpha <= 0.001) discard; - + + // 限制光照的最大值以避免过曝(可根据场景调整) + vec3 totalLighting = min(lighting + specularAccum, vec3(2.0)); + + // 将光照应用到基础颜色 + vec3 finalColor = baseColor * totalLighting; + + // 支持简单混合模式(保留原有行为) + 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); } """; @@ -228,31 +268,33 @@ public final class ModelRender { } private static void uploadLightsToShader(ShaderProgram sp, Model2D model) { - List lights = model.getLights(); - int lightCount = Math.min(lights.size(), 8); + List lights = model.getLights(); + int idx = 0; - // 设置光源数量 - setUniformIntInternal(sp, "uLightCount", lightCount); - - for (int i = 0; i < lightCount; i++) { - LightSource l = lights.get(i); + // 只上传已启用的光源,最多 MAX_LIGHTS(8) + for (int i = 0; i < lights.size() && idx < 8; i++) { + com.chuangzhou.vivid2D.render.model.util.LightSource l = lights.get(i); if (!l.isEnabled()) continue; - // 设置光源位置(环境光位置设为0) - Vector2f pos = l.isAmbient() ? new Vector2f(0, 0) : l.getPosition(); - setUniformVec2Internal(sp, "uLightsPos[" + i + "]", pos); + // 环境光的 position 在 shader 中不会用于距离计算,但我们也上传(安全) + setUniformVec2Internal(sp, "uLightsPos[" + idx + "]", l.isAmbient() ? new org.joml.Vector2f(0f, 0f) : l.getPosition()); + 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()); - setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", l.getIntensity()); - - // 设置是否为环境光 - setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", l.isAmbient() ? 1 : 0); + idx++; } - // 禁用未使用的光源 - for (int i = lightCount; i < 8; i++) { + // 上传实际有效光源数量 + setUniformIntInternal(sp, "uLightCount", idx); + + // 禁用剩余槽位(确保 shader 中不会读取到垃圾值) + for (int i = idx; i < 8; i++) { setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); 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)); } } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java index d3155ad..3d6c2f0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelData.java @@ -793,6 +793,7 @@ public class ModelData implements Serializable { // ==================== 内部数据类 ==================== + // ====== 修改后的 PartData(包含液化数据的序列化/反序列化) ====== public static class PartData implements Serializable { private static final long serialVersionUID = 1L; @@ -806,9 +807,12 @@ public class ModelData implements Serializable { public List meshNames; public Map userData; - // 新增:保存变形器数据 + // 保存变形器数据 public List deformers; + // 保存液化笔划数据(可保存多个笔划) + public List liquifyStrokes; + public PartData() { this.position = new Vector2f(); this.rotation = 0.0f; @@ -818,6 +822,7 @@ public class ModelData implements Serializable { this.meshNames = new ArrayList<>(); this.userData = new HashMap<>(); this.deformers = new ArrayList<>(); + this.liquifyStrokes = new ArrayList<>(); } public PartData(ModelPart part) { @@ -850,6 +855,105 @@ public class ModelData implements Serializable { } } + // 尝试通过反射收集液化笔划数据(兼容性:如果 ModelPart 没有对应 API,则跳过) + try { + // 期望的方法签名: public List 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) { 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; } @@ -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; } @@ -945,8 +1103,38 @@ public class ModelData implements Serializable { public String name; public Map 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 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; + } } + /** * 网格数据 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 274a8ce..630a0dc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -39,12 +39,25 @@ public class ModelPart { // ==================== 变形系统 ==================== private final List deformers; + private final List liquifyStrokes = new ArrayList<>(); // ==================== 状态标记 ==================== private boolean transformDirty; private boolean boundsDirty; private boolean pivotInitialized; + // ====== 液化模式枚举 ====== + public enum LiquifyMode { + PUSH, // 推开(从画笔中心向外推) + PULL, // 拉近(向画笔中心吸) + SWIRL_CW, // 顺时针旋转 + SWIRL_CCW, // 逆时针旋转 + BLOAT, // 鼓起(放大) + PINCH, // 收缩(缩小) + SMOOTH, // 平滑(邻域平均) + TURBULENCE // 湍流(噪声扰动) + } + // ==================== 构造器 ==================== public ModelPart() { @@ -176,6 +189,208 @@ public class ModelPart { transformDirty = false; } + /** + * 获取当前部件记录的所有液化笔划(用于序列化 / 导出) + */ + public List 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) { // 先确保 worldTransform 是最新的 updateWorldTransform(parentTransform, false); @@ -592,6 +807,58 @@ public class ModelPart { 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 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 getPoints() { return points; } + + public void addPoint(float x, float y, float pressure) { + this.points.add(new LiquifyPoint(x, y, pressure)); + } + } + // ==================== 枚举和内部类 ==================== /** diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java index 47baea5..b6d160c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java @@ -44,6 +44,7 @@ public class Texture { // ==================== 状态管理 ==================== private boolean disposed = false; private final long creationTime; + private int previousActiveTexture = -1; // ==================== 静态管理 ==================== private static final Map 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); } - // 安全地激活纹理单元 - 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 { + if (textureUnit < 0 || textureUnit >= 32) { // 合理的纹理单元范围 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,9 +497,22 @@ public class Texture { * 解绑纹理 */ public void unbind() { + // 解绑当前纹理目标 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; + } } + // ==================== 资源管理 ==================== /** diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java new file mode 100644 index 0000000..2a5cff9 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java @@ -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 seen) { + if (obj == null) { + printIndent(indent); System.out.println("null"); return; + } + if (seen.contains(obj)) { + printIndent(indent); System.out.println("<>"); 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) : ""; + 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"); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java new file mode 100644 index 0000000..136df7e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java @@ -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"); + } +} \ No newline at end of file