feat(render): 实现网格液化变形功能

- 添加向量变换工具方法,支持旋转和缩放变换
- 实现网格顶点的动态增删改功能
- 添加液化状态可视化渲染,包括顶点显示和状态指示器
- 支持创建细分网格以提高液化精度- 实现液化模式的交互控制,包括双击进入和快捷键操作- 添加液化画笔效果,支持推动、膨胀等多种变形模式- 完善网格数据结构,支持顶点数量动态变化时的UV和索引自动调整-优化选中框绘制逻辑,避免与顶点渲染冲突
This commit is contained in:
tzdwindows 7
2025-10-25 14:20:36 +08:00
parent 331d836d62
commit cdc0843174
8 changed files with 2182 additions and 157 deletions

View File

@@ -1,5 +1,6 @@
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
/**
@@ -27,4 +28,7 @@ public interface ModelClickListener {
* @param screenY 屏幕坐标系中的 Y 坐标
*/
default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {}
default void onLiquifyModeExited(){};
default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart){};
}

View File

@@ -824,12 +824,84 @@ public class ModelLayerPanel extends JPanel {
private Mesh2D createQuadForImage(BufferedImage img, String meshName) {
float w = img.getWidth();
float h = img.getHeight();
try {
Method m = Mesh2D.class.getMethod("createQuad", String.class, float.class, float.class);
Object o = m.invoke(null, meshName, w, h);
if (o instanceof Mesh2D) return (Mesh2D) o;
if (o instanceof Mesh2D) {
Mesh2D mesh = (Mesh2D) o;
// 对基础四边形进行细分以增加顶点密度
return subdivideMeshForLiquify(mesh, 3); // 3级细分
}
} catch (Exception ignored) {}
try {
// 创建高密度网格(细分网格)
return createSubdividedQuad(meshName, w, h, 3); // 3级细分
} catch (Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException("无法创建 Mesh2D没有合适的工厂或构造函数");
}
/**
* 创建细分四边形网格以支持更好的液化效果
*/
private Mesh2D createSubdividedQuad(String name, float width, float height, int subdivisionLevel) {
// 计算细分后的网格参数
int segments = (int) Math.pow(2, subdivisionLevel); // 每边分段数
int vertexCount = (segments + 1) * (segments + 1); // 顶点总数
int triangleCount = segments * segments * 2; // 三角形总数
float[] vertices = new float[vertexCount * 2];
float[] uvs = new float[vertexCount * 2];
int[] indices = new int[triangleCount * 3];
float halfW = width / 2f;
float halfH = height / 2f;
// 生成顶点和UV坐标
int vertexIndex = 0;
for (int y = 0; y <= segments; y++) {
for (int x = 0; x <= segments; x++) {
// 顶点坐标(从中心点开始)
float xPos = -halfW + (x * width) / segments;
float yPos = -halfH + (y * height) / segments;
vertices[vertexIndex * 2] = xPos;
vertices[vertexIndex * 2 + 1] = yPos;
// UV坐标
uvs[vertexIndex * 2] = (float) x / segments;
uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments; // 翻转V坐标
vertexIndex++;
}
}
// 生成三角形索引
int index = 0;
for (int y = 0; y < segments; y++) {
for (int x = 0; x < segments; x++) {
int topLeft = y * (segments + 1) + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * (segments + 1) + x;
int bottomRight = bottomLeft + 1;
// 第一个三角形 (topLeft -> topRight -> bottomLeft)
indices[index++] = topLeft;
indices[index++] = topRight;
indices[index++] = bottomLeft;
// 第二个三角形 (topRight -> bottomRight -> bottomLeft)
indices[index++] = topRight;
indices[index++] = bottomRight;
indices[index++] = bottomLeft;
}
}
// 使用反射创建Mesh2D实例
try {
Constructor<?> cons = null;
for (Constructor<?> c : Mesh2D.class.getDeclaredConstructors()) {
@@ -841,27 +913,157 @@ public class ModelLayerPanel extends JPanel {
}
if (cons != null) {
cons.setAccessible(true);
float[] vertices = new float[]{
-w / 2f, -h / 2f,
w / 2f, -h / 2f,
w / 2f, h / 2f,
-w / 2f, h / 2f
};
float[] uvs = new float[]{
0f, 1f,
1f, 1f,
1f, 0f,
0f, 0f
};
int[] indices = new int[]{0, 1, 2, 2, 3, 0};
Object meshObj = cons.newInstance(meshName, vertices, uvs, indices);
if (meshObj instanceof Mesh2D) return (Mesh2D) meshObj;
Object meshObj = cons.newInstance(name, vertices, uvs, indices);
if (meshObj instanceof Mesh2D) {
Mesh2D mesh = (Mesh2D) meshObj;
// 设置合适的pivot中心点
mesh.setPivot(0, 0);
if (mesh.getOriginalPivot() != null) {
mesh.setOriginalPivot(new Vector2f(0, 0));
}
return mesh;
}
}
} catch (Exception ex) {
ex.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("无法创建 Mesh2D没有合适的工厂或构造函数");
throw new RuntimeException("无法创建细分网格");
}
/**
* 对现有网格进行细分以增加顶点密度
*/
private Mesh2D subdivideMeshForLiquify(Mesh2D originalMesh, int subdivisionLevel) {
if (subdivisionLevel <= 0) return originalMesh;
try {
// 获取原始网格数据
float[] origVertices = originalMesh.getVertices();
float[] origUVs = originalMesh.getUVs();
int[] origIndices = originalMesh.getIndices();
// 简单的循环细分算法
List<Vector2f> newVertices = new ArrayList<>();
List<Vector2f> newUVs = new ArrayList<>();
List<Integer> newIndices = new ArrayList<>();
// 添加原始顶点
for (int i = 0; i < origVertices.length / 2; i++) {
newVertices.add(new Vector2f(origVertices[i * 2], origVertices[i * 2 + 1]));
newUVs.add(new Vector2f(origUVs[i * 2], origUVs[i * 2 + 1]));
}
// 对每个三角形进行细分
for (int i = 0; i < origIndices.length; i += 3) {
int i1 = origIndices[i];
int i2 = origIndices[i + 1];
int i3 = origIndices[i + 2];
Vector2f v1 = newVertices.get(i1);
Vector2f v2 = newVertices.get(i2);
Vector2f v3 = newVertices.get(i3);
Vector2f uv1 = newUVs.get(i1);
Vector2f uv2 = newUVs.get(i2);
Vector2f uv3 = newUVs.get(i3);
// 计算边的中点
Vector2f mid12 = new Vector2f(v1).add(v2).mul(0.5f);
Vector2f mid23 = new Vector2f(v2).add(v3).mul(0.5f);
Vector2f mid31 = new Vector2f(v3).add(v1).mul(0.5f);
Vector2f uvMid12 = new Vector2f(uv1).add(uv2).mul(0.5f);
Vector2f uvMid23 = new Vector2f(uv2).add(uv3).mul(0.5f);
Vector2f uvMid31 = new Vector2f(uv3).add(uv1).mul(0.5f);
// 添加新顶点
int mid12Idx = newVertices.size();
newVertices.add(mid12);
newUVs.add(uvMid12);
int mid23Idx = newVertices.size();
newVertices.add(mid23);
newUVs.add(uvMid23);
int mid31Idx = newVertices.size();
newVertices.add(mid31);
newUVs.add(uvMid31);
// 创建4个小三角形
// 三角形1: v1, mid12, mid31
newIndices.add(i1);
newIndices.add(mid12Idx);
newIndices.add(mid31Idx);
// 三角形2: v2, mid23, mid12
newIndices.add(i2);
newIndices.add(mid23Idx);
newIndices.add(mid12Idx);
// 三角形3: v3, mid31, mid23
newIndices.add(i3);
newIndices.add(mid31Idx);
newIndices.add(mid23Idx);
// 三角形4: mid12, mid23, mid31
newIndices.add(mid12Idx);
newIndices.add(mid23Idx);
newIndices.add(mid31Idx);
}
// 转换回数组
float[] finalVertices = new float[newVertices.size() * 2];
float[] finalUVs = new float[newUVs.size() * 2];
int[] finalIndices = new int[newIndices.size()];
for (int i = 0; i < newVertices.size(); i++) {
finalVertices[i * 2] = newVertices.get(i).x;
finalVertices[i * 2 + 1] = newVertices.get(i).y;
finalUVs[i * 2] = newUVs.get(i).x;
finalUVs[i * 2 + 1] = newUVs.get(i).y;
}
for (int i = 0; i < newIndices.size(); i++) {
finalIndices[i] = newIndices.get(i);
}
// 创建新的细分网格
Mesh2D subdividedMesh = originalMesh.copy();
subdividedMesh.setMeshData(finalVertices, finalUVs, finalIndices);
// 递归细分直到达到指定级别
if (subdivisionLevel > 1) {
return subdivideMeshForLiquify(subdividedMesh, subdivisionLevel - 1);
}
return subdividedMesh;
} catch (Exception e) {
e.printStackTrace();
return originalMesh; // 如果细分失败,返回原始网格
}
}
/**
* 根据图像尺寸智能计算细分级别
*/
private int calculateOptimalSubdivisionLevel(float width, float height) {
float area = width * height;
// 根据面积决定细分级别
if (area < 10000) { // 小图像
return 2;
} else if (area < 50000) { // 中等图像
return 3;
} else if (area < 200000) { // 大图像
return 4;
} else { // 超大图像
return 5;
}
}
/**

View File

@@ -9,6 +9,7 @@ import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.systems.Camera;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.test.TestModelGLPanel;
import org.joml.Matrix3f;
import org.joml.Vector2f;
import org.lwjgl.glfw.*;
@@ -19,11 +20,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.event.*;
import java.lang.reflect.Method;
import java.nio.IntBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.locks.LockSupport;
import javax.swing.*;
import javax.swing.Timer;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
@@ -42,7 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
* @author tzdwindows 7
* @version 1.1
* @since 2025-10-13
* @see com.chuangzhou.vivid2D.test.TestModelGLPanel
* @see TestModelGLPanel
*/
public class ModelRenderPanel extends JPanel {
private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class);
@@ -145,6 +148,17 @@ public class ModelRenderPanel extends JPanel {
private static final float CAMERA_Z_MIN = -5.0f;
private static final float CAMERA_Z_MAX = 5.0f;
private volatile boolean liquifyMode = false;
private volatile ModelPart liquifyTargetPart = null;
private volatile Mesh2D liquifyTargetMesh = null;
private final Timer doubleClickTimer;
private volatile long lastClickTime = 0;
private static final int DOUBLE_CLICK_INTERVAL = 300; // 双击间隔(毫秒)
private float liquifyBrushSize = 50.0f;
private float liquifyBrushStrength = 2.0f;
private ModelPart.LiquifyMode currentLiquifyMode = ModelPart.LiquifyMode.PUSH;
// ================== 摄像机控制方法 ==================
/**
@@ -207,6 +221,11 @@ public class ModelRenderPanel extends JPanel {
this.operationHistory = OperationHistoryGlobal.getInstance();
initialize();
initKeyboardShortcuts();
doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> {
handleSingleClick();
});
doubleClickTimer.setRepeats(false);
}
/**
@@ -220,6 +239,229 @@ public class ModelRenderPanel extends JPanel {
this.operationHistory = OperationHistoryGlobal.getInstance();
initialize();
initKeyboardShortcuts();
doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> {
// 单单击超时处理
handleSingleClick();
});
doubleClickTimer.setRepeats(false);
}
/**
* 处理双击事件
*/
private void handleDoubleClick(MouseEvent e) {
if (liquifyMode) {
// 如果在液化模式下双击,退出液化模式
exitLiquifyMode();
return;
}
final int screenX = e.getX();
final int screenY = e.getY();
executeInGLContext(() -> {
try {
// 转换屏幕坐标到模型坐标
float[] modelCoords = screenToModelCoordinates(screenX, screenY);
if (modelCoords == null) return;
float modelX = modelCoords[0];
float modelY = modelCoords[1];
// 检测双击的网格
Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY);
if (clickedMesh != null) {
enterLiquifyMode(clickedMesh);
}
} catch (Exception ex) {
logger.error("处理双击时出错", ex);
}
});
}
/**
* 进入液化模式
*/
private void enterLiquifyMode(Mesh2D targetMesh) {
liquifyMode = true;
liquifyTargetMesh = targetMesh;
liquifyTargetPart = findPartByMesh(targetMesh);
if (liquifyTargetPart != null) {
liquifyTargetPart.setStartLiquefy(true);
}
// 设置液化模式光标
setCursor(createLiquifyCursor());
logger.info("进入液化模式: {}", targetMesh.getName());
// 通知监听器
for (ModelClickListener listener : clickListeners) {
try {
listener.onLiquifyModeEntered(targetMesh, liquifyTargetPart);
} catch (Exception ex) {
logger.error("液化模式进入事件监听器执行出错", ex);
}
}
repaint();
}
/**
* 退出液化模式
*/
private void exitLiquifyMode() {
executeInGLContext(()->{
liquifyMode = false;
if (liquifyTargetPart != null) {
liquifyTargetPart.setStartLiquefy(false);
}
liquifyTargetMesh = null;
liquifyTargetPart = null;
// 恢复默认光标
updateCursorForHoverState();
logger.info("退出液化模式");
// 通知监听器
for (ModelClickListener listener : clickListeners) {
try {
listener.onLiquifyModeExited();
} catch (Exception ex) {
logger.error("液化模式退出事件监听器执行出错", ex);
}
}
repaint();
});
}
/**
* 液化模式下的单击处理(应用液化效果)
*/
private void handleLiquifyClick() {
Point mousePos = getMousePosition();
if (mousePos == null || liquifyTargetPart == null) return;
logger.debug("液化模式单击: {}", mousePos);
executeInGLContext(() -> {
try {
float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y);
if (modelCoords == null) return;
float modelX = modelCoords[0];
float modelY = modelCoords[1];
// 应用液化效果
Vector2f brushCenter = new Vector2f(modelX, modelY);
// 判断是否按住Ctrl键决定是否创建顶点
boolean createVertices = ctrlPressed;
liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize,
liquifyBrushStrength, currentLiquifyMode, 1, createVertices);
//logger.info("应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}",
// modelX, modelY, liquifyBrushSize, liquifyBrushStrength,
// currentLiquifyMode, createVertices);
} catch (Exception ex) {
logger.error("应用液化效果时出错", ex);
}
});
}
/**
* 创建液化模式光标
*/
private Cursor createLiquifyCursor() {
// 创建自定义液化光标(圆圈)
int size = 32;
BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = cursorImg.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制透明背景
g2d.setColor(new Color(0, 0, 0, 0));
g2d.fillRect(0, 0, size, size);
// 绘制圆圈
int center = size / 2;
int radius = (int)(liquifyBrushSize * 0.1f); // 根据画笔大小缩放光标
// 外圈
g2d.setColor(Color.RED);
g2d.setStroke(new BasicStroke(2f));
g2d.drawOval(center - radius, center - radius, radius * 2, radius * 2);
// 内圈
g2d.setColor(new Color(255, 100, 100, 150));
g2d.setStroke(new BasicStroke(1f));
g2d.drawOval(center - radius / 2, center - radius / 2, radius, radius);
// 中心点
g2d.setColor(Color.RED);
g2d.fillOval(center - 2, center - 2, 4, 4);
g2d.dispose();
return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "LiquifyCursor");
}
/**
* 设置液化画笔大小
*/
public void setLiquifyBrushSize(float size) {
this.liquifyBrushSize = Math.max(1.0f, Math.min(500.0f, size));
if (liquifyMode) {
setCursor(createLiquifyCursor());
}
}
/**
* 设置液化画笔强度
*/
public void setLiquifyBrushStrength(float strength) {
this.liquifyBrushStrength = Math.max(0.0f, Math.min(2.0f, strength));
}
/**
* 设置液化模式
*/
public void setLiquifyMode(ModelPart.LiquifyMode mode) {
this.currentLiquifyMode = mode;
}
/**
* 获取当前液化状态
*/
public boolean isInLiquifyMode() {
return liquifyMode;
}
/**
* 获取液化目标网格
*/
public Mesh2D getLiquifyTargetMesh() {
return liquifyTargetMesh;
}
/**
* 处理单单击事件
*/
private void handleSingleClick() {
if (liquifyMode) {
handleLiquifyClick();
}
}
/**
@@ -538,8 +780,14 @@ public class ModelRenderPanel extends JPanel {
*/
public void clearSelectedMeshes() {
executeInGLContext(() -> {
// 如果当前在液化模式,先退出液化模式
if (liquifyMode) {
exitLiquifyMode();
}
for (Mesh2D mesh : selectedMeshes) {
mesh.setSelected(false);
mesh.setSuspension(false);
mesh.clearMultiSelection();
}
selectedMeshes.clear();
@@ -616,7 +864,7 @@ public class ModelRenderPanel extends JPanel {
if (model == null) return allMeshes;
try {
java.util.List<ModelPart> parts = model.getParts();
List<ModelPart> parts = model.getParts();
if (parts == null) return allMeshes;
for (ModelPart part : parts) {
@@ -638,7 +886,7 @@ public class ModelRenderPanel extends JPanel {
if (part == null) return;
// 添加当前部件的网格
java.util.List<Mesh2D> meshes = part.getMeshes();
List<Mesh2D> meshes = part.getMeshes();
if (meshes != null) {
for (Mesh2D mesh : meshes) {
if (mesh != null && mesh.isVisible()) {
@@ -697,9 +945,9 @@ public class ModelRenderPanel extends JPanel {
// 创建渲染线程
startRendering();
this.addComponentListener(new java.awt.event.ComponentAdapter() {
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(java.awt.event.ComponentEvent e) {
public void componentResized(ComponentEvent e) {
int w = getWidth();
int h = getHeight();
if (w <= 0 || h <= 0) return;
@@ -813,6 +1061,10 @@ public class ModelRenderPanel extends JPanel {
shiftPressed = true;
} else if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = true;
if (liquifyMode && liquifyTargetMesh != null) {
liquifyTargetMesh.setRenderVertices(true);
logger.debug("液化模式下按住Ctrl开启顶点渲染");
}
} else if (e.getKeyCode() == KeyEvent.VK_A && ctrlPressed) {
// Ctrl+A 全选
e.consume(); // 阻止默认行为
@@ -826,6 +1078,10 @@ public class ModelRenderPanel extends JPanel {
shiftPressed = false;
} else if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = false;
if (liquifyMode && liquifyTargetMesh != null) {
liquifyTargetMesh.setRenderVertices(false);
logger.debug("液化模式下松开Ctrl关闭顶点渲染");
}
}
}
});
@@ -897,11 +1153,11 @@ public class ModelRenderPanel extends JPanel {
*/
private void handleMousePressed(MouseEvent e) {
if (!contextInitialized) return;
final int screenX = e.getX();
final int screenY = e.getY();
requestFocusInWindow();
// 首先处理中键拖拽(摄像机控制),在任何模式下都可用
if (SwingUtilities.isMiddleMouseButton(e)) {
cameraDragging = true;
lastCameraDragX = screenX;
@@ -917,6 +1173,23 @@ public class ModelRenderPanel extends JPanel {
return;
}
// 液化模式下的左键处理
if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) {
// 在液化模式下,左键按下直接开始液化操作
currentDragMode = DragMode.NONE; // 液化模式使用特殊拖拽逻辑
logger.debug("液化模式下开始拖拽");
// 立即应用一次液化效果
handleLiquifyClick();
return;
}
// 非液化模式的正常处理
if (liquifyMode) {
// 液化模式下只处理左键和中键,其他按钮忽略
return;
}
shiftDuringDrag = e.isShiftDown();
executeInGLContext(() -> {
@@ -1375,6 +1648,14 @@ public class ModelRenderPanel extends JPanel {
});
return;
}
// 液化模式下的拖拽处理(优先于其他模式)
if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) {
handleLiquifyDrag(e);
return;
}
// 普通模式下的拖拽处理
if (currentDragMode == DragMode.NONE) return;
final int screenX = e.getX();
@@ -1413,6 +1694,41 @@ public class ModelRenderPanel extends JPanel {
});
}
/**
* 液化模式下的拖拽处理(连续应用液化效果)
*/
private void handleLiquifyDrag(MouseEvent e) {
if (!liquifyMode || liquifyTargetPart == null) return;
final int screenX = e.getX();
final int screenY = e.getY();
executeInGLContext(() -> {
try {
float[] modelCoords = screenToModelCoordinates(screenX, screenY);
if (modelCoords == null) return;
float modelX = modelCoords[0];
float modelY = modelCoords[1];
// 应用液化效果
Vector2f brushCenter = new Vector2f(modelX, modelY);
// 判断是否按住Ctrl键决定是否创建顶点
boolean createVertices = ctrlPressed;
liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize,
liquifyBrushStrength, currentLiquifyMode, 1, createVertices);
//logger.info("拖拽应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}",
// modelX, modelY, liquifyBrushSize, liquifyBrushStrength,
// currentLiquifyMode, createVertices);
} catch (Exception ex) {
logger.error("应用液化拖拽效果时出错", ex);
}
});
}
/**
* 处理移动中心点拖拽
@@ -1588,12 +1904,22 @@ public class ModelRenderPanel extends JPanel {
* 处理鼠标释放事件(结束拖拽并记录操作历史)
*/
private void handleMouseReleased(MouseEvent e) {
if (cameraDragging && SwingUtilities.isMiddleMouseButton(e)) {
// 首先处理摄像机拖拽释放
if (cameraDragging && (SwingUtilities.isMiddleMouseButton(e) || liquifyMode)) {
cameraDragging = false;
// 恢复悬停状态的光标
updateCursorForHoverState();
return;
}
// 液化模式下的释放处理
if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) {
// 液化模式下不需要记录操作历史,直接重置状态
currentDragMode = DragMode.NONE;
logger.debug("液化拖拽结束");
return;
}
if (currentDragMode != DragMode.NONE) {
// 记录操作历史
//executeInGLContext(() -> {
@@ -1693,37 +2019,47 @@ public class ModelRenderPanel extends JPanel {
final int screenX = e.getX();
final int screenY = e.getY();
// 在 GL 上下文线程中执行点击检测
executeInGLContext(() -> {
try {
// 转换屏幕坐标到模型坐标
float[] modelCoords = screenToModelCoordinates(screenX, screenY);
if (modelCoords == null) return;
long currentTime = System.currentTimeMillis();
boolean isDoubleClick = (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL;
lastClickTime = currentTime;
float modelX = modelCoords[0];
float modelY = modelCoords[1];
if (isDoubleClick) {
// 取消单单击计时器
doubleClickTimer.stop();
handleDoubleClick(e);
} else {
executeInGLContext(() -> {
try {
// 转换屏幕坐标到模型坐标
float[] modelCoords = screenToModelCoordinates(screenX, screenY);
if (modelCoords == null) return;
logger.debug("点击位置:({}, {})", modelX, modelY);
// 检测点击的网格
Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY);
float modelX = modelCoords[0];
float modelY = modelCoords[1];
if (clickedMesh == null && !e.isControlDown() && !e.isShiftDown()) {
clearSelectedMeshes();
logger.debug("点击空白处,取消所有选择");
}
logger.debug("点击位置:({}, {})", modelX, modelY);
// 检测点击的网格
Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY);
// 触发点击事件
for (ModelClickListener listener : clickListeners) {
try {
listener.onModelClicked(clickedMesh, modelX, modelY, screenX, screenY);
} catch (Exception ex) {
logger.error("点击事件监听器执行出错", ex);
if (clickedMesh == null && !e.isControlDown() && !e.isShiftDown()) {
clearSelectedMeshes();
logger.debug("点击空白处,取消所有选择");
}
// 触发点击事件
for (ModelClickListener listener : clickListeners) {
try {
listener.onModelClicked(clickedMesh, modelX, modelY, screenX, screenY);
} catch (Exception ex) {
logger.error("点击事件监听器执行出错", ex);
}
}
} catch (Exception ex) {
logger.error("处理鼠标点击时出错", ex);
}
} catch (Exception ex) {
logger.error("处理鼠标点击时出错", ex);
}
});
});
doubleClickTimer.restart();
}
}
/**
@@ -1756,6 +2092,7 @@ public class ModelRenderPanel extends JPanel {
if (newHoveredMesh != hoveredMesh) {
if (hoveredMesh != null) {
hoveredMesh.setSuspension(false);
hoveredMesh.setRenderVertices(false);
}
hoveredMesh = newHoveredMesh;
@@ -1891,7 +2228,7 @@ public class ModelRenderPanel extends JPanel {
float checkX = modelX;
float checkY = modelY;
java.util.List<ModelPart> parts = model.getParts();
List<ModelPart> parts = model.getParts();
if (parts == null || parts.isEmpty()) {
return null;
}
@@ -1901,7 +2238,7 @@ public class ModelRenderPanel extends JPanel {
ModelPart part = parts.get(i);
if (part == null || !part.isVisible()) continue;
java.util.List<Mesh2D> meshes = part.getMeshes();
List<Mesh2D> meshes = part.getMeshes();
if (meshes == null || meshes.isEmpty()) continue;
for (int m = meshes.size() - 1; m >= 0; m--) {
@@ -1940,7 +2277,7 @@ public class ModelRenderPanel extends JPanel {
if (model == null) return null;
try {
java.lang.reflect.Method getBoundsMethod = model.getClass().getMethod("getBounds");
Method getBoundsMethod = model.getClass().getMethod("getBounds");
return (BoundingBox) getBoundsMethod.invoke(model);
} catch (Exception e) {
logger.debug("无法获取模型边界", e);
@@ -1955,8 +2292,8 @@ public class ModelRenderPanel extends JPanel {
if (model == null) return null;
try {
java.lang.reflect.Method getMeshesMethod = model.getClass().getMethod("getMeshes");
java.util.List<Mesh2D> meshes = (java.util.List<Mesh2D>) getMeshesMethod.invoke(model);
Method getMeshesMethod = model.getClass().getMethod("getMeshes");
List<Mesh2D> meshes = (List<Mesh2D>) getMeshesMethod.invoke(model);
return meshes.isEmpty() ? null : meshes.get(0);
} catch (Exception e) {
return null;

View File

@@ -589,6 +589,9 @@ public class OperationHistoryGlobal {
case "BATCH_TRANSFORM":
executeBatchTransform(params);
break;
case "LIQUIFY":
executeLiquify(params);
break;
default:
LOGGER.debug("执行操作: {}", operationType);
break;
@@ -631,6 +634,9 @@ public class OperationHistoryGlobal {
case "BATCH_TRANSFORM":
undoBatchTransform(params);
break;
case "LIQUIFY":
undoLiquify(params);
break;
default:
//System.out.println("撤回操作: " + operationType);
break;
@@ -639,6 +645,64 @@ public class OperationHistoryGlobal {
// ============ 批量变换操作方法 ============
private void executeLiquify(Object... params) {
// 重做液化操作:应用液化后的状态
if (params.length >= 2) {
@SuppressWarnings("unchecked")
Map<Mesh2D, OperationHistoryGlobal.MeshState> afterStates =
(Map<Mesh2D, OperationHistoryGlobal.MeshState>) params[1];
applyLiquifyStates(afterStates);
}
}
private void undoLiquify(Object... params) {
// 撤回液化操作:恢复液化前的状态
if (params.length >= 1) {
@SuppressWarnings("unchecked")
Map<Mesh2D, OperationHistoryGlobal.MeshState> beforeStates =
(Map<Mesh2D, OperationHistoryGlobal.MeshState>) params[0];
applyLiquifyStates(beforeStates);
}
}
private void applyLiquifyStates(Map<Mesh2D, OperationHistoryGlobal.MeshState> states) {
if (states == null || states.isEmpty()) {
return;
}
for (Map.Entry<Mesh2D, OperationHistoryGlobal.MeshState> entry : states.entrySet()) {
Mesh2D mesh = entry.getKey();
OperationHistoryGlobal.MeshState state = entry.getValue();
if (mesh == null) {
continue;
}
try {
// 恢复顶点数据
if (state.vertices != null && state.vertices.length > 0) {
mesh.setVertices(state.vertices, true); // true表示同时更新originalVertices
} else {
}
// 恢复原始顶点数据(作为备份)
if (state.originalVertices != null && state.originalVertices.length > 0) {
mesh.setOriginalVertices(state.originalVertices);
}
// 恢复原始中心点
if (state.originalPivot != null) {
mesh.setOriginalPivot(state.originalPivot);
}
// 强制更新边界
mesh.updateBounds();
} catch (Exception e) {
}
}
}
private void handleBatchTransformRecord(Object... params) {
if (params.length >= 3 && params[0] instanceof ModelPart) {
ModelPart part = (ModelPart) params[0];
@@ -1126,14 +1190,14 @@ public class OperationHistoryGlobal {
}
public static class MeshState {
String name;
float[] vertices;
float[] originalVertices;
Vector2f originalPivot;
public String name;
public float[] vertices;
public float[] originalVertices;
public Vector2f originalPivot;
Object texture;
MeshState(String name, float[] vertices, float[] originalVertices,
Vector2f originalPivot, Object texture) {
public MeshState(String name, float[] vertices, float[] originalVertices,
Vector2f originalPivot, Object texture) {
this.name = name;
this.vertices = vertices != null ? vertices.clone() : null;
this.originalVertices = originalVertices != null ? originalVertices.clone() : null;

View File

@@ -242,19 +242,8 @@ public class PartData implements Serializable {
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);
part.applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations, true);
} 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;
}
}
}
}

View File

@@ -59,12 +59,17 @@ public class Mesh2D {
private Vector2f pivot = new Vector2f(0, 0);
private Vector2f originalPivot = new Vector2f(0, 0);
private boolean isSuspension = false;
private boolean isRenderVertices = false;
// ==================== 多选支持 ====================
private final List<Mesh2D> multiSelectedParts = new ArrayList<>();
private final BoundingBox multiSelectionBounds = new BoundingBox();
private boolean multiSelectionDirty = true;
// ==================== 液化状态渲染 ====================
private boolean showLiquifyOverlay = false;
private Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色
// ==================== 常量 ====================
public static final int POINTS = 0;
public static final int LINES = 1;
@@ -121,10 +126,347 @@ public class Mesh2D {
markDirty();
}
/**
* 设置是否为渲染顶点
*/
public void setRenderVertices(boolean renderVertices) {
isRenderVertices = renderVertices;
}
public void setModelPart(ModelPart modelPart) {
this.modelPart = modelPart;
}
/**
* 设置是否显示液化覆盖层
*/
public void setShowLiquifyOverlay(boolean show) {
this.showLiquifyOverlay = show;
markDirty();
}
/**
* 绘制液化状态覆盖层 - 渲染所有顶点
*/
private void drawLiquifyOverlay(int shaderProgram, Matrix3f modelMatrix) {
if (!showLiquifyOverlay) return;
RenderSystem.pushState();
try {
// 使用固色着色器
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
if (solidShader != null && solidShader.programId != 0) {
solidShader.use();
// 设置模型矩阵
int modelLoc = solidShader.getUniformLocation("uModelMatrix");
if (modelLoc != -1) {
RenderSystem.uniformMatrix3(modelLoc, modelMatrix);
}
// 设置液化覆盖层颜色
int colorLoc = solidShader.getUniformLocation("uColor");
if (colorLoc != -1) {
RenderSystem.uniform4f(colorLoc, liquifyOverlayColor);
}
}
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
Tesselator t = Tesselator.getInstance();
BufferBuilder bb = t.getBuilder();
if (isRenderVertices && vertices != null && vertices.length >= 6) {
// ============ 显示顶点模式 ============
// 1. 绘制所有顶点组成的多边形填充
bb.begin(GL11.GL_TRIANGLES, vertices.length / 2 * 3);
bb.setColor(liquifyOverlayColor);
// 使用三角形扇绘制填充
Vector2f center = new Vector2f(vertices[0], vertices[1]);
for (int i = 1; i < vertices.length / 2 - 1; i++) {
int baseIndex1 = i * 2;
int baseIndex2 = (i + 1) * 2;
bb.vertex(center.x, center.y, 0f, 0f);
bb.vertex(vertices[baseIndex1], vertices[baseIndex1 + 1], 0f, 0f);
bb.vertex(vertices[baseIndex2], vertices[baseIndex2 + 1], 0f, 0f);
}
t.end();
// 2. 绘制顶点连线(边框)
drawLiquifyVertexLines(bb);
// 3. 绘制顶点标记
drawLiquifyVertexPoints(bb);
// 4. 绘制液化提示文字
drawLiquifyTextAtCenter(bb);
} else {
// 4. 绘制液化状态指示器
drawLiquifyStatusIndicator(bb);
}
} finally {
RenderSystem.popState();
}
}
/**
* 绘制液化状态指示器(在不显示顶点时)
*/
private void drawLiquifyStatusIndicator(BufferBuilder bb) {
BoundingBox bounds = getBounds();
if (bounds == null || !bounds.isValid()) return;
float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f;
float centerY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f;
float width = bounds.getWidth();
float height = bounds.getHeight();
// 计算指示器位置(放在网格右上方,不遮挡内容)
float indicatorX = bounds.getMaxX() + Math.max(width, height) * 0.2f;
float indicatorY = bounds.getMaxY() + Math.max(width, height) * 0.1f;
// 1. 绘制简洁的液化状态圆点
float dotRadius = Math.max(width, height) * 0.08f;
// 外圈圆点
bb.begin(GL11.GL_TRIANGLE_FAN, 16);
bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.8f)); // 橙色圆点
bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点
for (int i = 0; i <= 16; i++) {
float angle = (float) (i * 2 * Math.PI / 16);
float x = indicatorX + (float) Math.cos(angle) * dotRadius;
float y = indicatorY + (float) Math.sin(angle) * dotRadius;
bb.vertex(x, y, 0f, 0f);
}
bb.endImmediate();
// 内圈白色圆点
float innerRadius = dotRadius * 0.5f;
bb.begin(GL11.GL_TRIANGLE_FAN, 12);
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); // 白色内圆
bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点
for (int i = 0; i <= 12; i++) {
float angle = (float) (i * 2 * Math.PI / 12);
float x = indicatorX + (float) Math.cos(angle) * innerRadius;
float y = indicatorY + (float) Math.sin(angle) * innerRadius;
bb.vertex(x, y, 0f, 0f);
}
bb.endImmediate();
// 2. 绘制简洁的画笔图标
drawSimpleBrushIcon(bb, indicatorX, indicatorY, dotRadius);
// 3. 绘制简洁的提示文字
String liquifyText = "Liquify";
String hintText = "Ctrl: Show Vertices";
TextRenderer textRenderer = ModelRender.getTextRenderer();
if (textRenderer != null) {
float textY = indicatorY + dotRadius + 15f;
// 主标题
float titleWidth = textRenderer.getTextWidth(liquifyText);
float titleX = indicatorX - titleWidth / 2.0f;
// 绘制主标题背景(简洁的圆角效果)
bb.begin(GL11.GL_TRIANGLES, 6);
bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 0.6f)); // 半透明黑色背景
bb.vertex(titleX - 6, textY - 12, 0f, 0f);
bb.vertex(titleX + titleWidth + 6, textY - 12, 0f, 0f);
bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f);
bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f);
bb.vertex(titleX - 6, textY + 2, 0f, 0f);
bb.vertex(titleX - 6, textY - 12, 0f, 0f);
bb.endImmediate();
// 绘制主标题
ModelRender.renderText(liquifyText, titleX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f));
// 提示文字(小字号)
float hintY = textY + 15f;
float hintWidth = textRenderer.getTextWidth(hintText);
float hintX = indicatorX - hintWidth / 2.0f;
ModelRender.renderText(hintText, hintX, hintY, new Vector4f(0.8f, 0.8f, 0.8f, 0.7f));
}
}
/**
* 绘制简洁的画笔图标
*/
private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) {
float iconSize = size * 0.6f;
// 画笔柄(简单的线条)
bb.begin(GL11.GL_LINES, 2);
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f));
bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f);
bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f);
bb.endImmediate();
// 画笔头(小三角形)
bb.begin(GL11.GL_TRIANGLES, 3);
bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f));
bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f);
bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f);
bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f);
bb.endImmediate();
}
/**
* 绘制顶点连线(边框)
*/
private void drawLiquifyVertexLines(BufferBuilder bb) {
final Vector4f LINE_COLOR = new Vector4f(1.0f, 0.8f, 0.0f, 1.0f); // 黄色边框
if (vertices == null || vertices.length < 4) return;
// 绘制闭合的多边形边框
bb.begin(GL11.GL_LINE_LOOP, vertices.length / 2);
bb.setColor(LINE_COLOR);
for (int i = 0; i < vertices.length / 2; i++) {
int baseIndex = i * 2;
bb.vertex(vertices[baseIndex], vertices[baseIndex + 1], 0f, 0f);
}
bb.endImmediate();
// 如果网格有三角形索引,也绘制内部连线
if (indices != null && indices.length >= 3) {
bb.begin(GL11.GL_LINES, indices.length * 2);
bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.6f)); // 半透明橙色内部线
for (int i = 0; i < indices.length; i += 3) {
int i1 = indices[i];
int i2 = indices[i + 1];
int i3 = indices[i + 2];
// 三条边
drawLineBetweenVertices(bb, i1, i2);
drawLineBetweenVertices(bb, i2, i3);
drawLineBetweenVertices(bb, i3, i1);
}
bb.endImmediate();
}
}
/**
* 在两个顶点之间绘制线段
*/
private void drawLineBetweenVertices(BufferBuilder bb, int index1, int index2) {
if (index1 < 0 || index1 >= vertices.length / 2 ||
index2 < 0 || index2 >= vertices.length / 2) {
return;
}
int base1 = index1 * 2;
int base2 = index2 * 2;
bb.vertex(vertices[base1], vertices[base1 + 1], 0f, 0f);
bb.vertex(vertices[base2], vertices[base2 + 1], 0f, 0f);
}
/**
* 绘制顶点标记
*/
private void drawLiquifyVertexPoints(BufferBuilder bb) {
final float POINT_SIZE = 4.0f;
final Vector4f POINT_COLOR = new Vector4f(1.0f, 0.4f, 0.0f, 1.0f); // 橙色顶点
if (vertices == null) return;
// 绘制所有顶点
for (int i = 0; i < vertices.length / 2; i++) {
int baseIndex = i * 2;
float x = vertices[baseIndex];
float y = vertices[baseIndex + 1];
// 绘制小方块表示顶点
bb.begin(GL11.GL_TRIANGLES, 6);
bb.setColor(POINT_COLOR);
float halfSize = POINT_SIZE / 2;
bb.vertex(x - halfSize, y - halfSize, 0f, 0f);
bb.vertex(x + halfSize, y - halfSize, 0f, 0f);
bb.vertex(x + halfSize, y + halfSize, 0f, 0f);
bb.vertex(x + halfSize, y + halfSize, 0f, 0f);
bb.vertex(x - halfSize, y + halfSize, 0f, 0f);
bb.vertex(x - halfSize, y - halfSize, 0f, 0f);
bb.endImmediate();
// 在顶点旁边显示编号(可选)
drawVertexNumber(bb, i, x, y);
}
}
/**
* 在顶点旁边显示编号
*/
private void drawVertexNumber(BufferBuilder bb, int vertexIndex, float x, float y) {
String numberText = String.valueOf(vertexIndex);
TextRenderer textRenderer = ModelRender.getTextRenderer();
if (textRenderer != null) {
float textWidth = textRenderer.getTextWidth(numberText);
float textX = x + 6.0f; // 在顶点右侧显示
float textY = y - 4.0f;
// 绘制文字背景
bb.begin(GL11.GL_TRIANGLES, 6);
bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f));
bb.vertex(textX - 2, textY - 8, 0f, 0f);
bb.vertex(textX + textWidth + 2, textY - 8, 0f, 0f);
bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f);
bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f);
bb.vertex(textX - 2, textY + 2, 0f, 0f);
bb.vertex(textX - 2, textY - 8, 0f, 0f);
bb.endImmediate();
// 绘制文字
ModelRender.renderText(numberText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
}
}
/**
* 在网格中心绘制液化提示文字
*/
private void drawLiquifyTextAtCenter(BufferBuilder bb) {
String liquifyText = "LIQUIFY MODE";
BoundingBox bounds = getBounds();
if (bounds == null || !bounds.isValid()) return;
float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f;
float textY = bounds.getMaxY() + 20.0f;
// 使用文本渲染器绘制提示文字
Vector4f textColor = new Vector4f(1.0f, 0.8f, 0.0f, 1.0f);
TextRenderer textRenderer = ModelRender.getTextRenderer();
if (textRenderer != null) {
float textWidth = textRenderer.getTextWidth(liquifyText);
float textX = centerX - textWidth / 2.0f;
// 绘制文字背景
bb.begin(GL11.GL_TRIANGLES, 6);
bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f));
bb.vertex(textX - 5, textY - 15, 0f, 0f);
bb.vertex(textX + textWidth + 5, textY - 15, 0f, 0f);
bb.vertex(textX + textWidth + 5, textY + 5, 0f, 0f);
bb.vertex(textX + textWidth + 5, textY + 5, 0f, 0f);
bb.vertex(textX - 5, textY + 5, 0f, 0f);
bb.vertex(textX - 5, textY - 15, 0f, 0f);
bb.endImmediate();
// 绘制文字
ModelRender.renderText(liquifyText, textX, textY, textColor);
}
}
/**
* 设置中心点
*/
@@ -233,6 +575,106 @@ public class Mesh2D {
this.originalVertices = originalVertices != null ? originalVertices.clone() : null;
}
/**
* 设置顶点数据(支持顶点数量变化)
*/
public void setVertices(float[] vertices) {
if (vertices == null) {
throw new IllegalArgumentException("Vertices array cannot be null");
}
if (vertices.length % 2 != 0) {
throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)");
}
// 允许顶点数量变化但需要相应地调整UV数组
if (this.vertices.length != vertices.length) {
// 顶点数量发生了变化需要调整UV数组
adjustUVsForNewVertexCount(vertices.length / 2);
}
this.vertices = vertices.clone();
markDirty();
}
/**
* 根据新的顶点数量调整UV数组
*/
private void adjustUVsForNewVertexCount(int newVertexCount) {
int currentVertexCount = getVertexCount();
if (newVertexCount == currentVertexCount) {
return; // 顶点数量没有变化
}
float[] newUVs = new float[newVertexCount * 2];
if (newVertexCount > currentVertexCount) {
// 顶点数量增加复制现有UV并估算新增顶点的UV
System.arraycopy(uvs, 0, newUVs, 0, uvs.length);
// 为新增的顶点估算UV坐标
for (int i = currentVertexCount; i < newVertexCount; i++) {
int uvIndex = i * 2;
// 使用边界框来估算UV
BoundingBox bounds = getBounds();
if (bounds != null && bounds.isValid()) {
float u = 0.5f; // 默认值
float v = 0.5f; // 默认值
newUVs[uvIndex] = u;
newUVs[uvIndex + 1] = v;
} else {
newUVs[uvIndex] = 0.5f;
newUVs[uvIndex + 1] = 0.5f;
}
}
} else {
// 顶点数量减少截断UV数组
System.arraycopy(uvs, 0, newUVs, 0, newUVs.length);
}
this.uvs = newUVs;
// 如果顶点数量变化很大,可能需要重新生成索引
if (Math.abs(newVertexCount - currentVertexCount) > 2) {
regenerateIndicesForNewVertexCount(newVertexCount);
}
}
/**
* 为新的顶点数量重新生成索引
*/
private void regenerateIndicesForNewVertexCount(int newVertexCount) {
// 简单的三角形扇形索引生成
if (newVertexCount >= 3) {
List<Integer> newIndices = new ArrayList<>();
// 使用三角形扇形(适用于凸多边形)
for (int i = 1; i < newVertexCount - 1; i++) {
newIndices.add(0);
newIndices.add(i);
newIndices.add(i + 1);
}
this.indices = new int[newIndices.size()];
for (int i = 0; i < newIndices.size(); i++) {
this.indices[i] = newIndices.get(i);
}
} else {
// 顶点太少,清空索引
this.indices = new int[0];
}
}
/**
* 设置顶点数据(带原始顶点同步)
*/
public void setVertices(float[] vertices, boolean updateOriginal) {
setVertices(vertices);
if (updateOriginal && originalVertices != null && originalVertices.length == vertices.length) {
this.originalVertices = vertices.clone();
}
}
/**
* 创建圆形网格
*/
@@ -276,6 +718,138 @@ public class Mesh2D {
// ==================== 顶点操作 ====================
/**
* 添加新顶点到网格末尾
*/
public void addVertex(float x, float y, float u, float v) {
// 扩展顶点数组
float[] newVertices = new float[vertices.length + 2];
System.arraycopy(vertices, 0, newVertices, 0, vertices.length);
newVertices[vertices.length] = x;
newVertices[vertices.length + 1] = y;
// 扩展UV数组
float[] newUVs = new float[uvs.length + 2];
System.arraycopy(uvs, 0, newUVs, 0, uvs.length);
newUVs[uvs.length] = u;
newUVs[uvs.length + 1] = v;
// 更新数据
this.vertices = newVertices;
this.uvs = newUVs;
// 注意:需要手动更新索引数组来包含新顶点
markDirty();
}
/**
* 在指定位置插入顶点
*/
public void insertVertex(int index, float x, float y, float u, float v) {
if (index < 0 || index > getVertexCount()) {
throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index);
}
// 扩展顶点数组
float[] newVertices = new float[vertices.length + 2];
System.arraycopy(vertices, 0, newVertices, 0, index * 2);
newVertices[index * 2] = x;
newVertices[index * 2 + 1] = y;
System.arraycopy(vertices, index * 2, newVertices, index * 2 + 2, vertices.length - index * 2);
// 扩展UV数组
float[] newUVs = new float[uvs.length + 2];
System.arraycopy(uvs, 0, newUVs, 0, index * 2);
newUVs[index * 2] = u;
newUVs[index * 2 + 1] = v;
System.arraycopy(uvs, index * 2, newUVs, index * 2 + 2, uvs.length - index * 2);
// 更新数据
this.vertices = newVertices;
this.uvs = newUVs;
// 注意:需要更新索引数组来反映顶点位置的变化
updateIndicesAfterVertexInsertion(index);
markDirty();
}
/**
* 移除指定顶点
*/
public void removeVertex(int index) {
if (index < 0 || index >= getVertexCount()) {
throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index);
}
// 缩减顶点数组
float[] newVertices = new float[vertices.length - 2];
System.arraycopy(vertices, 0, newVertices, 0, index * 2);
System.arraycopy(vertices, index * 2 + 2, newVertices, index * 2, vertices.length - index * 2 - 2);
// 缩减UV数组
float[] newUVs = new float[uvs.length - 2];
System.arraycopy(uvs, 0, newUVs, 0, index * 2);
System.arraycopy(uvs, index * 2 + 2, newUVs, index * 2, uvs.length - index * 2 - 2);
// 更新数据
this.vertices = newVertices;
this.uvs = newUVs;
// 更新索引数组
updateIndicesAfterVertexRemoval(index);
markDirty();
}
/**
* 在插入顶点后更新索引数组
*/
private void updateIndicesAfterVertexInsertion(int insertedIndex) {
if (indices == null) return;
for (int i = 0; i < indices.length; i++) {
if (indices[i] >= insertedIndex) {
indices[i]++;
}
}
}
/**
* 在移除顶点后更新索引数组
*/
private void updateIndicesAfterVertexRemoval(int removedIndex) {
if (indices == null) return;
// 创建新索引数组,移除引用被删除顶点的三角形
List<Integer> newIndicesList = new ArrayList<>();
for (int i = 0; i < indices.length; i += 3) {
int i1 = indices[i];
int i2 = indices[i + 1];
int i3 = indices[i + 2];
// 如果三角形包含被删除的顶点,跳过这个三角形
if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) {
continue;
}
// 调整索引编号
if (i1 > removedIndex) i1--;
if (i2 > removedIndex) i2--;
if (i3 > removedIndex) i3--;
newIndicesList.add(i1);
newIndicesList.add(i2);
newIndicesList.add(i3);
}
// 转换回数组
this.indices = new int[newIndicesList.size()];
for (int i = 0; i < newIndicesList.size(); i++) {
this.indices[i] = newIndicesList.get(i);
}
}
/**
* 获取顶点数量
*/
@@ -768,7 +1342,7 @@ public class Mesh2D {
}
// 选中框绘制(需要切换到固色 shader
if (selected) {
if (selected && !isRenderVertices) {
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
@@ -797,6 +1371,10 @@ public class Mesh2D {
}
}
if (showLiquifyOverlay) {
drawLiquifyOverlay(shaderProgram, modelMatrix);
}
if (isSuspension && !selected) {
RenderSystem.pushState();

View File

@@ -25,4 +25,46 @@ public class Matrix3fUtils {
public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point) {
return transformPointInverse(matrix, point, new Vector2f());
}
}
/**
* 变换向量(不考虑平移,只考虑旋转和缩放)
*/
public static Vector2f transformVector(Matrix3f matrix, Vector2f vector, Vector2f dest) {
float x = matrix.m00() * vector.x + matrix.m01() * vector.y;
float y = matrix.m10() * vector.x + matrix.m11() * vector.y;
return dest.set(x, y);
}
public static Vector2f transformVector(Matrix3f matrix, Vector2f vector) {
return transformVector(matrix, vector, new Vector2f());
}
/**
* 逆变换向量(不考虑平移,只考虑旋转和缩放的逆)
*/
public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector, Vector2f dest) {
// 计算2x2子矩阵的行列式
float det = matrix.m00() * matrix.m11() - matrix.m01() * matrix.m10();
if (Math.abs(det) < 1e-6f) {
return dest.set(vector);
}
float invDet = 1.0f / det;
// 计算2x2子矩阵的逆
float m00 = matrix.m11() * invDet;
float m01 = -matrix.m01() * invDet;
float m10 = -matrix.m10() * invDet;
float m11 = matrix.m00() * invDet;
float x = vector.x * m00 + vector.y * m01;
float y = vector.x * m10 + vector.y * m11;
return dest.set(x, y);
}
public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector) {
return transformVectorInverse(matrix, vector, new Vector2f());
}
}