feat(model): 添加液化笔划数据的序列化与反序列化支持

- 在 ModelData.PartData 中新增 liquifyStrokes 字段用于保存液化笔划- 实现通过反射读取 ModelPart 的液化笔划数据(兼容旧版本)- 支持多种数据结构形式的液化点读取(Vector2f、自定义类、Map)
- 反序列化时自动重放液化笔划到 ModelPart- 添加 LiquifyStrokeData 和 LiquifyPointData 用于序列化存储
- 提供深度拷贝支持以确保 liquifyStrokes 数据完整复制
- 增加 ModelLoadTest 测试类用于验证模型加载与结构检查
This commit is contained in:
tzdwindows 7
2025-10-12 08:01:25 +08:00
parent 16af846e48
commit 22c3661d6e
6 changed files with 1436 additions and 56 deletions

View File

@@ -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<LightSource> lights = model.getLights();
int lightCount = Math.min(lights.size(), 8);
List<com.chuangzhou.vivid2D.render.model.util.LightSource> lights = model.getLights();
int idx = 0;
// 设置光源数量
setUniformIntInternal(sp, "uLightCount", lightCount);
for (int i = 0; i < lightCount; i++) {
LightSource l = lights.get(i);
// 只上传已启用的光源,最多 MAX_LIGHTS8
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));
}
}

View File

@@ -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<String> meshNames;
public Map<String, String> userData;
// 新增:保存变形器数据
// 保存变形器数据
public List<DeformerData> deformers;
// 保存液化笔划数据(可保存多个笔划)
public List<LiquifyStrokeData> 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<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) {
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<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;
}
}
/**
* 网格数据
*/

View File

@@ -39,12 +39,25 @@ public class ModelPart {
// ==================== 变形系统 ====================
private final List<Deformer> deformers;
private final List<LiquifyStroke> 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<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) {
// 先确保 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<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));
}
}
// ==================== 枚举和内部类 ====================
/**

View File

@@ -44,6 +44,7 @@ public class Texture {
// ==================== 状态管理 ====================
private boolean disposed = false;
private final long creationTime;
private int previousActiveTexture = -1;
// ==================== 静态管理 ====================
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);
}
// 安全地激活纹理单元
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;
}
}
// ==================== 资源管理 ====================
/**