refactor(render):重构网格变形引擎并优化顶点管理

- 重构 Mesh2D 的变形系统,采用基于三角剖分与重心坐标的映射方式
- 引入 BarycentricWeightInfo 类用于存储顶点的变形权重信息
- 新增 addControlPointAt 方法支持在指定坐标创建控制点- 实现 Delaunay 三角剖分算法用于构建控制点网格
-重写 applyDeformation 方法以提高变形计算效率
- 移除旧的基于多边形权重的变形逻辑- 优化顶点索引管理,将 indices 整合进 VertexList
- 增强网格创建方法,生成更利于变形的钻石拓扑结构- 添加顶点删除与索引重映射功能- 修复 FrameInterpolator 中顶点标签设置逻辑错误
- 完善网格拷贝与比较方法,确保正确处理顶点数据- 更新 GPU 上传逻辑以适配新的顶点存储结构
This commit is contained in:
tzdwindows 7
2025-11-12 20:45:08 +08:00
parent 8a01020cbe
commit 27f8ab11cf
10 changed files with 1087 additions and 873 deletions

View File

@@ -9,28 +9,32 @@ import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
/**
* MultiSelectionBoxRenderer — 修复摄像机缩放导致边框消失的问题(并保持美观)
* MultiSelectionBoxRenderer — 美观版完整实现
*
* 要点
* - drawSelectBox/drawMultiSelectionBox 接收 cameraScale 参数(世界单位 -> 像素的比例
* - 将需要“屏幕稳定”的厚度与尺寸除以 cameraScale保证缩放时视觉一致
* - 兼容不传 cameraScale 的调用(默认 1.0f
* 视觉设计目标
* - 细腻的三层边框(外发光 -> 主边框 -> 内描边
* - 小巧但可见的手柄(角点与边中点略有区分)
* - 精致的中心点(十字 + 细环
* - 虚线用于多选边框
* - 批处理绘制以减少 Draw Call尽量在一次 TRIANGLES 调用中绘制多数元素)
*
* 注BufferBuilder 的 vertex(...) 方法签名与项目实现有关,示例中使用 (x,y,z,u) 占位。
*/
public class MultiSelectionBoxRenderer {
// -------------------- 配置常量(可调) --------------------
// 尺寸(以世界坐标为基准,这些会根据 cameraScale 自动调整为屏幕稳定值)
// 尺寸
public static final float DEFAULT_CORNER_SIZE = 8.0f;
public static final float DEFAULT_BORDER_THICKNESS = 2.5f;
public static final float DEFAULT_DASH_LENGTH = 10.0f;
public static final float DEFAULT_GAP_LENGTH = 6.0f;
// 视觉厚度分层(这些是“世界坐标基准值”,实际绘制时会 / cameraScale
private static final float OUTER_BORDER_THICKNESS = 0.8f;
// 视觉厚度分层(更细腻
private static final float OUTER_BORDER_THICKNESS = 2.2f;
private static final float MAIN_BORDER_THICKNESS = 0.6f;
private static final float INNER_BORDER_THICKNESS = 0.2f;
// 手柄与中心点尺寸(世界坐标基准)
// 手柄与中心点尺寸
private static final float HANDLE_CORNER_SIZE = DEFAULT_CORNER_SIZE;
private static final float HANDLE_MID_SIZE = 2.8f;
private static final float CENTER_LINE_THICKNESS = 1.2f;
@@ -46,113 +50,79 @@ public class MultiSelectionBoxRenderer {
public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); // 黄色手柄
public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红
// 小的保护常量
private static final float MIN_CAMERA_SCALE = 1e-4f;
private static final float MIN_THICKNESS_PIXELS = 0.5f; // 最小像素视觉厚度保护
// -------------------- 公共绘制 API带 cameraScale 参数) --------------------
// -------------------- 公共绘制 API --------------------
/**
* 绘制单选的选择框(推荐:传入 cameraScale
* 绘制单选的选择框(主入口
*
* @param bounds 包围盒(世界坐标)
* @param pivot 旋转中心 / 中心点(世界坐标)
* @param cameraScale 世界单位 -> 像素 比例(通常来自 CameraManagement.calculateScaleFactor()
* @param bounds 包围盒(世界坐标)
* @param pivot 旋转中心 / 中心点(世界坐标)
*/
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot, float cameraScale) {
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
if (bounds == null || !bounds.isValid()) return;
if (cameraScale <= 0f) cameraScale = MIN_CAMERA_SCALE;
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
// 根据 cameraScale 计算“屏幕稳定”的厚度/尺寸
float outerThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, OUTER_BORDER_THICKNESS / cameraScale);
float mainThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, MAIN_BORDER_THICKNESS / cameraScale);
float innerThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, INNER_BORDER_THICKNESS / cameraScale);
float handleCornerSize = Math.max(2.0f / cameraScale, HANDLE_CORNER_SIZE / cameraScale);
float handleMidSize = Math.max(2.0f / cameraScale, HANDLE_MID_SIZE / cameraScale);
float centerLineThickness = Math.max(1.0f / cameraScale, CENTER_LINE_THICKNESS / cameraScale);
float centerRingThickness = Math.max(1.0f / cameraScale, CENTER_RING_THICKNESS / cameraScale);
float centerRingRadius = Math.max(3.0f / cameraScale, CENTER_RING_RADIUS / cameraScale);
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder bb = tesselator.getBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
// 估算顶点数并开始 TRIANGLES 绘制
int estimatedQuads = (3 * 4) + 8 + 12;
// 估算顶点数:三层边框 + 8 个手柄 + 中心点十字和圆环
// 每个四边形使用 6 个顶点(两个三角形)
int estimatedQuads = (3 * 4) + 8 + (6 * 4); // 近似估算(保守)
bb.begin(RenderSystem.GL_TRIANGLES, estimatedQuads * 6);
// 外层发光边框(较宽、透明)
bb.setColor(SOLID_BORDER_COLOR_OUTER);
addQuadLineLoop(bb, outerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
addQuadLineLoop(bb, OUTER_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
// 主边框(核心线)
bb.setColor(SOLID_BORDER_COLOR_MAIN);
addQuadLineLoop(bb, mainThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
addQuadLineLoop(bb, MAIN_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
// 内描边(细)
bb.setColor(SOLID_BORDER_COLOR_INNER);
addQuadLineLoop(bb, innerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
addQuadLineLoop(bb, INNER_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY);
// 手柄(角点与边中点)
bb.setColor(HANDLE_COLOR);
addHandleQuad(bb, minX, minY, handleCornerSize);
addHandleQuad(bb, maxX, minY, handleCornerSize);
addHandleQuad(bb, minX, maxY, handleCornerSize);
addHandleQuad(bb, maxX, maxY, handleCornerSize);
addHandleQuad(bb, minX, minY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, maxX, minY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, minX, maxY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, maxX, maxY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, (minX + maxX) * 0.5f, minY, handleMidSize);
addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, handleMidSize);
addHandleQuad(bb, minX, (minY + maxY) * 0.5f, handleMidSize);
addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, handleMidSize);
addHandleQuad(bb, (minX + maxX) * 0.5f, minY, HANDLE_MID_SIZE);
addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, HANDLE_MID_SIZE);
addHandleQuad(bb, minX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE);
addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE);
// 中心点:十字 + 环(圆环使用三角片段)
bb.setColor(CENTER_POINT_COLOR);
addQuadLine(bb, pivot.x - 6.0f / cameraScale, pivot.y, pivot.x + 6.0f / cameraScale, pivot.y, centerLineThickness);
addQuadLine(bb, pivot.x, pivot.y - 6.0f / cameraScale, pivot.x, pivot.y + 6.0f / cameraScale, centerLineThickness);
addRing(bb, pivot.x, pivot.y, centerRingRadius, centerRingThickness, 18);
addQuadLine(bb, pivot.x - 6.0f, pivot.y, pivot.x + 6.0f, pivot.y, CENTER_LINE_THICKNESS);
addQuadLine(bb, pivot.x, pivot.y - 6.0f, pivot.x, pivot.y + 6.0f, CENTER_LINE_THICKNESS);
addRing(bb, pivot.x, pivot.y, CENTER_RING_RADIUS, CENTER_RING_THICKNESS, 18);
tesselator.end();
}
/**
* 向后兼容:不传 cameraScale 时使用默认 1.0(即不做屏幕稳定
*/
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
drawSelectBox(bounds, pivot, 1.0f);
}
/**
* 绘制多选框(推荐传入 cameraScale
* 绘制多选框(虚线 + 手柄
*
* @param multiBounds 包围盒
* @param cameraScale 世界单位 -> 像素 比例
* @param multiBounds 多选包围盒
*/
public static void drawMultiSelectionBox(BoundingBox multiBounds, float cameraScale) {
public static void drawMultiSelectionBox(BoundingBox multiBounds) {
if (multiBounds == null || !multiBounds.isValid()) return;
if (cameraScale <= 0f) cameraScale = MIN_CAMERA_SCALE;
float minX = multiBounds.getMinX();
float minY = multiBounds.getMinY();
float maxX = multiBounds.getMaxX();
float maxY = multiBounds.getMaxY();
// 屏幕稳定尺寸
float handleCornerSize = Math.max(2.0f / cameraScale, HANDLE_CORNER_SIZE / cameraScale);
float handleMidSize = Math.max(2.0f / cameraScale, HANDLE_MID_SIZE / cameraScale);
float centerLineThickness = Math.max(1.0f / cameraScale, CENTER_LINE_THICKNESS / cameraScale);
float dashLen = Math.max(3.0f / cameraScale, DEFAULT_DASH_LENGTH / cameraScale);
float gapLen = Math.max(2.0f / cameraScale, DEFAULT_GAP_LENGTH / cameraScale);
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder bb = tesselator.getBuilder();
@@ -160,41 +130,37 @@ public class MultiSelectionBoxRenderer {
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
// 1) 虚线边框(使用 GL_LINES
int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (dashLen + gapLen)));
int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
bb.begin(GL11.GL_LINES, estimatedSegments * 2);
bb.setColor(DASHED_BORDER_COLOR);
addDashedLineVertices(bb, minX, minY, maxX, minY, dashLen, gapLen);
addDashedLineVertices(bb, maxX, minY, maxX, maxY, dashLen, gapLen);
addDashedLineVertices(bb, maxX, maxY, minX, maxY, dashLen, gapLen);
addDashedLineVertices(bb, minX, maxY, minX, minY, dashLen, gapLen);
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
tesselator.end();
// 2) 手柄与中心(合并为一次三角形绘制)
bb.begin(RenderSystem.GL_TRIANGLES, (8 + 2) * 6);
bb.setColor(MULTI_SELECTION_HANDLE_COLOR);
addHandleQuad(bb, minX, minY, handleCornerSize);
addHandleQuad(bb, maxX, minY, handleCornerSize);
addHandleQuad(bb, minX, maxY, handleCornerSize);
addHandleQuad(bb, maxX, maxY, handleCornerSize);
addHandleQuad(bb, minX, minY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, maxX, minY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, minX, maxY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, maxX, maxY, HANDLE_CORNER_SIZE);
addHandleQuad(bb, (minX + maxX) * 0.5f, minY, handleMidSize);
addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, handleMidSize);
addHandleQuad(bb, minX, (minY + maxY) * 0.5f, handleMidSize);
addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, handleMidSize);
addHandleQuad(bb, (minX + maxX) * 0.5f, minY, HANDLE_MID_SIZE);
addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, HANDLE_MID_SIZE);
addHandleQuad(bb, minX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE);
addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE);
Vector2f center = multiBounds.getCenter();
bb.setColor(CENTER_POINT_COLOR);
addQuadLine(bb, center.x - 6.0f / cameraScale, center.y, center.x + 6.0f / cameraScale, center.y, centerLineThickness);
addQuadLine(bb, center.x, center.y - 6.0f / cameraScale, center.x, center.y + 6.0f / cameraScale, centerLineThickness);
addQuadLine(bb, center.x - 6.0f, center.y, center.x + 6.0f, center.y, CENTER_LINE_THICKNESS);
addQuadLine(bb, center.x, center.y - 6.0f, center.x, center.y + 6.0f, CENTER_LINE_THICKNESS);
tesselator.end();
}
public static void drawMultiSelectionBox(BoundingBox multiBounds) {
drawMultiSelectionBox(multiBounds, 1.0f);
}
// -------------------- 辅助绘图方法(保持不变) --------------------
// -------------------- 辅助绘图方法 --------------------
/**
* 添加一个填充四边形(用两个三角形表示)
@@ -219,7 +185,7 @@ public class MultiSelectionBoxRenderer {
}
/**
* 绘制一条由四边形模拟的线段(厚度以输入值为准
* 绘制一条由四边形模拟的线段(厚度可控
*/
private static void addQuadLine(BufferBuilder bb, float x0, float y0, float x1, float y1, float thickness) {
float dx = x1 - x0;
@@ -249,7 +215,7 @@ public class MultiSelectionBoxRenderer {
/**
* 绘制一个闭合的四边形线环(用于边框三层绘制)
*
* @param thickness 厚度(以外部传入的值为准,通常已根据 cameraScale 调整
* @param thickness 厚度(世界坐标
* @param vertices 顶点序列 x1,y1,x2,y2,...
*/
private static void addQuadLineLoop(BufferBuilder bb, float thickness, float... vertices) {
@@ -269,8 +235,8 @@ public class MultiSelectionBoxRenderer {
*
* @param cx 中心 x
* @param cy 中心 y
* @param radius 半径(通常已按 cameraScale 调整)
* @param thickness 环厚度(通常已按 cameraScale 调整)
* @param radius 半径
* @param thickness 环厚度
* @param segments 分段数(建议 >= 8
*/
private static void addRing(BufferBuilder bb, float cx, float cy, float radius, float thickness, int segments) {
@@ -305,8 +271,8 @@ public class MultiSelectionBoxRenderer {
/**
* 在两点之间生成虚线段顶点(使用 GL_LINES
*
* @param dashLen 虚线长度(通常已按 cameraScale 调整
* @param gapLen 间隙长度(通常已按 cameraScale 调整
* @param dashLen 虚线长度(世界坐标
* @param gapLen 间隙长度(世界坐标
*/
private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY,
float dashLen, float gapLen) {

View File

@@ -458,35 +458,6 @@ public class ModelLayerPanel extends JPanel {
setSelectedLayers(selectedParts);
}
// 原始的单选拖拽逻辑 (为兼容老版本保留,但现在应主要使用 performBlockReorder)
public void performVisualReorder(int visualFrom, int visualTo) {
if (model == null) return;
try {
int size = listModel.getSize();
if (visualFrom < 0 || visualFrom >= size) return;
if (visualTo < 0) visualTo = 0;
if (visualTo > size - 1) visualTo = size - 1;
ModelPart moved = listModel.get(visualFrom);
if (!isDragging) {
isDragging = true;
draggedPart = moved;
dragStartPosition = new Vector2f(moved.getPosition());
}
List<ModelPart> visual = new ArrayList<>(size);
for (int i = 0; i < size; i++) visual.add(listModel.get(i));
moved = visual.remove(visualFrom);
visual.add(visualTo, moved);
// 使用新的辅助方法更新 UI 和模型
updateModelAndUIFromVisualList(visual, List.of(moved));
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* 【新增方法】执行多选拖拽后的图层块重排序操作。
* 供 CompositeLayerTransferHandler (原 LayerReorderTransferHandler) 调用。

View File

@@ -16,25 +16,21 @@ import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class VertexDeformationTool extends Tool {
private static final Logger logger = LoggerFactory.getLogger(VertexDeformationTool.class);
private Mesh2D targetMesh = null;
// [MODIFIED] 单选被替换为多选列表
private final List<Vertex> selectedVertices = new ArrayList<>();
private Vertex hoveredVertex = null;
private static final float VERTEX_TOLERANCE = 8.0f;
private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE;
private float savedCameraRotation = Float.NaN;
private Vector2f savedCameraScale = new Vector2f(1, 1);
private boolean cameraStateSaved = false;
// [MODIFIED] 恢复使用 orderedControlVertices 来维护控制点的顺序
private final List<Vertex> orderedControlVertices = new ArrayList<>();
// --- [新增] 用于“推/拉”模式的状态变量 ---
private boolean isPushPullMode = false;
private Vector2f dragStartPoint = null;
private List<Vertex> dragBaseState = null; // 存储拖动开始时的顶点快照
public VertexDeformationTool(ModelRenderPanel renderPanel) {
super(renderPanel, "顶点变形工具", "直接对网格顶点进行精细变形操作");
}
@@ -43,76 +39,37 @@ public class VertexDeformationTool extends Tool {
public void activate() {
if (isActive) return;
isActive = true;
orderedControlVertices.clear();
selectedVertices.clear();
hoveredVertex = null;
if (!renderPanel.getSelectedMeshes().isEmpty()) {
targetMesh = renderPanel.getSelectedMesh();
} else {
targetMesh = findFirstVisibleMesh();
}
// 关键:在激活时重置部件变换,确保鼠标坐标与顶点局部坐标一致,解决偏移问题
try {
if (renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
savedCameraRotation = targetMesh.getModelPart().getRotation();
savedCameraScale = new Vector2f(targetMesh.getModelPart().getScale());
cameraStateSaved = true;
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
targetMesh.getModelPart().setRotation(0f);
targetMesh.getModelPart().setScale(1f);
targetMesh.getModelPart().updateMeshVertices();
} catch (Throwable t) { logger.debug("设置部件默认状态时失败: {}", t.getMessage()); }
});
}
} catch (Throwable t) { logger.debug("无法备份/设置相机状态: {}", t.getMessage()); }
if (targetMesh != null) {
orderedControlVertices.clear();
orderedControlVertices.addAll(targetMesh.getDeformationControlVertices());
targetMesh.setStates("showDeformationVertices", true);
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
targetMesh.setRenderVertices(true);
targetMesh.updateBounds();
} catch (Throwable t) { logger.debug("激活顶点显示失败: {}", t.getMessage()); }
});
logger.info("激活顶点变形工具: {}", targetMesh.getName());
logger.info("激活顶点变形工具: {},已加载 {} 个控制点。", targetMesh.getName(), orderedControlVertices.size());
} else {
logger.warn("没有找到可用的网格用于顶点变形");
}
renderPanel.repaint();
}
@Override
public void deactivate() {
if (!isActive) return;
isActive = false;
try {
if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
targetMesh.getModelPart().setRotation(savedCameraRotation);
targetMesh.getModelPart().setScale(savedCameraScale);
targetMesh.getModelPart().updateMeshVertices();
targetMesh.saveAsOriginal();
} catch (Throwable t) { logger.debug("恢复部件状态失败: {}", t.getMessage()); }
});
}
} catch (Throwable t) { logger.debug("无法恢复相机状态: {}", t.getMessage()); } finally {
cameraStateSaved = false;
savedCameraRotation = Float.NaN;
savedCameraScale = new Vector2f(1, 1);
}
if (targetMesh != null) {
// 在停用前清除所有顶点的DEFORMATION标签
for (Vertex v : orderedControlVertices) {
v.setTag(VertexTag.DEFAULT);
v.setTag(VertexTag.DEFORMATION);
}
targetMesh.setDeformationControlVertices(new ArrayList<>());
orderedControlVertices.clear();
targetMesh.setStates("showDeformationVertices", false);
try {
targetMesh.setRenderVertices(false);
@@ -125,90 +82,145 @@ public class VertexDeformationTool extends Tool {
}
targetMesh = null;
selectedVertices.clear();
orderedControlVertices.clear();
hoveredVertex = null;
currentDragMode = ModelRenderPanel.DragMode.NONE;
logger.info("停用顶点变形工具");
}
/**
* [MODIFIED] onMousePressed 现在支持 Ctrl 多选
* [已修正] onMousePressed 现在会检查 Alt 键来决定进入“推/拉”模式还是“控制点选择”模式
*/
@Override
public void onMousePressed(MouseEvent e, float modelX, float modelY) {
if (!isActive || targetMesh == null) return;
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY);
if (clickedVertex != null) {
if (e.isControlDown()) {
// Ctrl 多选逻辑
if (selectedVertices.contains(clickedVertex)) {
selectedVertices.remove(clickedVertex); // 已选中的,再次点击则取消
} else {
selectedVertices.add(clickedVertex); // 未选中的,添加到列表
}
} else {
// 普通单击逻辑
if (!selectedVertices.contains(clickedVertex)) {
selectedVertices.clear();
selectedVertices.add(clickedVertex);
}
}
currentDragMode = ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX;
} else {
// 点击空白处,取消所有选择
if (!e.isControlDown()) {
selectedVertices.clear();
}
currentDragMode = ModelRenderPanel.DragMode.NONE;
}
} catch (Throwable t) {
logger.error("onMousePressed (VertexDeformationTool) 处理失败", t);
} finally {
renderPanel.repaint();
// [核心修正] 检查 Alt 键是否被按下
if (e.isAltDown()) {
// --- 进入“推/拉”模式 ---
isPushPullMode = true;
dragStartPoint = new Vector2f(modelX, modelY);
// 创建当前网格顶点状态的快照,这是计算位移的基准
dragBaseState = new ArrayList<>(targetMesh.getActiveVertexList().size());
for(Vertex v : targetMesh.getActiveVertexList()){
dragBaseState.add(v.copy()); // 必须是深拷贝
}
});
currentDragMode = ModelRenderPanel.DragMode.NONE; // 确保不触发控制点拖动
logger.debug("进入推/拉模式,起点: ({}, {})", modelX, modelY);
} else {
// --- 默认的“控制点选择”模式 ---
isPushPullMode = false;
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY);
if (clickedVertex != null) {
if (e.isControlDown()) {
if (selectedVertices.contains(clickedVertex)) {
selectedVertices.remove(clickedVertex);
} else {
selectedVertices.add(clickedVertex);
}
} else {
if (!selectedVertices.contains(clickedVertex)) {
selectedVertices.clear();
selectedVertices.add(clickedVertex);
}
}
currentDragMode = ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX;
} else {
if (!e.isControlDown()) {
selectedVertices.clear();
}
currentDragMode = ModelRenderPanel.DragMode.NONE;
}
} catch (Throwable t) {
logger.error("onMousePressed (控制点模式) 处理失败", t);
} finally {
renderPanel.repaint();
}
});
}
}
/**
* [已修正] onMouseDragged 现在会根据模式执行不同的拖动逻辑。
*/
@Override
public void onMouseDragged(MouseEvent e, float modelX, float modelY) {
if (!isActive || targetMesh == null) return;
if (isPushPullMode) {
// --- “推/拉”模式的逻辑 ---
if (dragStartPoint == null || dragBaseState == null) return;
// 计算从按下鼠标开始的总位移
Vector2f delta = new Vector2f(modelX, modelY).sub(dragStartPoint);
// 定义一个画笔半径 (可以设为可配置的)
float radius = 50.0f;
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
// 调用 Mesh2D 中已经存在的局部变形方法!
targetMesh.applyLocalizedPush(dragBaseState, dragStartPoint, delta, radius);
} catch (Throwable t) {
logger.error("onMouseDragged (推/拉模式) 处理失败", t);
} finally {
renderPanel.repaint();
}
});
} else {
// --- 默认的“控制点拖动”模式的逻辑 ---
if (selectedVertices.isEmpty() || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return;
Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1);
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
primaryVertex.position.set(modelX, modelY);
} catch (Throwable t) {
logger.error("onMouseDragged (控制点模式) 处理失败", t);
} finally {
renderPanel.repaint();
}
});
}
}
@Override
public void onMouseReleased(MouseEvent e, float modelX, float modelY) {
if (!isActive) return;
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX && !selectedVertices.isEmpty()) {
if (isPushPullMode) {
// --- 清理“推/拉”模式的状态 ---
isPushPullMode = false;
dragStartPoint = null;
dragBaseState = null;
logger.debug("退出推/拉模式");
}
// 无论是哪种模式,都在松开鼠标时固化变形
if (targetMesh != null) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
if (targetMesh != null) {
targetMesh.saveAsOriginal();
if (targetMesh.getModelPart() != null) {
targetMesh.getModelPart().updateMeshVertices();
}
targetMesh.saveAsOriginal();
if (targetMesh.getModelPart() != null) {
targetMesh.getModelPart().updateMeshVertices();
}
} catch (Throwable t) { logger.error("onMouseReleased 保存基准失败", t); }
});
}
currentDragMode = ModelRenderPanel.DragMode.NONE;
renderPanel.repaint();
}
@Override
public void onMouseDragged(MouseEvent e, float modelX, float modelY) {
if (!isActive || selectedVertices.isEmpty() || targetMesh == null || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return;
// 我们需要计算位移,以便批量移动
// (这里简化处理,只移动最后一个选中的点,完整的批量拖拽需要更复杂的逻辑)
Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1);
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
primaryVertex.position.set(modelX, modelY);
targetMesh.applyDeformation();
} catch (Throwable t) {
logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t);
} finally {
renderPanel.repaint();
}
});
}
// onMouseMoved, onMouseClicked, onMouseDoubleClicked, onKeyPressed, 等方法保持不变...
// ... (此处省略所有其他未修改的方法,请保留您文件中的原样)
@Override
public void onMouseMoved(MouseEvent e, float modelX, float modelY) {
if (!isActive || targetMesh == null) return;
@@ -220,25 +232,24 @@ public class VertexDeformationTool extends Tool {
}
@Override
public void onMouseClicked(MouseEvent e, float modelX, float modelY) {
// No action
}
public void onMouseClicked(MouseEvent e, float modelX, float modelY) { }
/**
* [MODIFIED] 双击逻辑现在是:双击已有控制点则删除,双击空白则添加。
*/
@Override
public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
if (!isActive || targetMesh == null) return;
if (!isActive || targetMesh == null || e.isAltDown()) return; // 在推拉模式下禁用双击
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY);
if (clickedVertex != null) {
// 双击已有的点 -> 删除
untagDeformationVertex(clickedVertex);
} else {
// 双击空白处 -> 添加
tagNearestVertexAsDeformation(modelX, modelY);
Vertex newVertex = targetMesh.addControlPointAt(modelX, modelY);
if (newVertex != null) {
orderedControlVertices.add(newVertex);
updateDeformationRegion();
} else {
logger.warn("在 ({}, {}) 添加控制点失败,可能点击位置在网格外部。", modelX, modelY);
}
}
} catch (Throwable t) {
logger.error("onMouseDoubleClicked 处理失败", t);
@@ -246,16 +257,12 @@ public class VertexDeformationTool extends Tool {
});
}
/**
* [MODIFIED] 按键逻辑现在支持批量删除。
*/
@Override
public void onKeyPressed(KeyEvent e) {
if (!isActive || selectedVertices.isEmpty()) return;
int kc = e.getKeyCode();
if (kc == KeyEvent.VK_BACK_SPACE || kc == KeyEvent.VK_DELETE) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
// 创建一个副本进行遍历,以避免在遍历时修改列表
List<Vertex> toDelete = new ArrayList<>(selectedVertices);
for (Vertex v : toDelete) {
untagDeformationVertex(v);
@@ -270,39 +277,15 @@ public class VertexDeformationTool extends Tool {
return createVertexCursor();
}
private void tagNearestVertexAsDeformation(float x, float y) {
if (targetMesh == null) return;
Vertex nearestVertex = null;
float minDistanceSq = Float.MAX_VALUE;
for (Vertex v : targetMesh.getActiveVertexList()) {
// 使用原始位置进行查找,因为它是稳定的
if (v.getTag() != VertexTag.DEFORMATION) {
float distSq = v.originalPosition.distanceSquared(x, y);
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
nearestVertex = v;
}
}
}
if (nearestVertex != null) {
nearestVertex.setTag(VertexTag.DEFORMATION);
if (!orderedControlVertices.contains(nearestVertex)) {
orderedControlVertices.add(nearestVertex);
}
logger.info("已添加控制点 {} (总数: {})", targetMesh.getActiveVertexList().vertices.indexOf(nearestVertex), orderedControlVertices.size());
updateDeformationRegion();
}
}
private void untagDeformationVertex(Vertex vertex) {
if (targetMesh == null || vertex == null) return;
vertex.setTag(VertexTag.DEFAULT);
orderedControlVertices.remove(vertex);
selectedVertices.remove(vertex); // 确保从选择列表中也移除
if (hoveredVertex == vertex) hoveredVertex = null;
logger.info("已移除一个控制点 (剩余: {})", orderedControlVertices.size());
selectedVertices.remove(vertex);
if (hoveredVertex == vertex) {
hoveredVertex = null;
}
vertex.setTag(VertexTag.DEFORMATION);
vertex.delete();
updateDeformationRegion();
}
@@ -316,7 +299,7 @@ public class VertexDeformationTool extends Tool {
if (targetMesh == null) return null;
float tolerance = VERTEX_TOLERANCE / calculateScaleFactor();
float toleranceSq = tolerance * tolerance;
for (Vertex v : orderedControlVertices) { // 优化:只在控制点中查找
for (Vertex v : orderedControlVertices) {
if (v.position.distanceSquared(x, y) < toleranceSq) {
return v;
}
@@ -338,9 +321,9 @@ public class VertexDeformationTool extends Tool {
private Cursor createVertexCursor() {
int size = 32; BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int center = size / 2; g2.setColor(Color.GREEN); g2.setStroke(new BasicStroke(2f));
g2.drawOval(center - 6, center - 6, 12, 12); g2.dispose();
return Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(center, center), "VertexCursor");
int center = size / 2; g2.setColor(Color.ORANGE); g2.setStroke(new BasicStroke(2f));
g2.drawRect(center - 5, center - 5, 10, 10); g2.dispose();
return Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(center, center), "VertexSelectCursor");
}
public Mesh2D getTargetMesh() {return targetMesh;}

View File

@@ -312,7 +312,7 @@ public class FrameInterpolator {
if (target.deleted) {
// "删除"操作意味着取消其变形资格
if (vertex.getTag() == VertexTag.DEFORMATION) {
vertex.setTag(VertexTag.DEFAULT);
vertex.setTag(VertexTag.DEFORMATION);
meshModified = true;
}
} else {

View File

@@ -19,168 +19,12 @@ public class MeshTextureUtil {
float h = img.getHeight();
try {
Mesh2D o = Mesh2D.createQuad(meshName, w, h);
return subdivideMeshForLiquify(o, 3);
} catch (Exception ignored) {
}
try {
return createSubdividedQuad(meshName, w, h, 3);
} catch (Exception ex) {
ex.printStackTrace();
}
return Mesh2D.createQuad(meshName, w, h);
} catch (Exception ignored) {}
throw new RuntimeException("无法创建 Mesh2D");
}
private static 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;
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;
uvs[vertexIndex * 2] = (float) x / segments;
uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments;
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;
indices[index++] = topLeft;
indices[index++] = topRight;
indices[index++] = bottomLeft;
indices[index++] = topRight;
indices[index++] = bottomRight;
indices[index++] = bottomLeft;
}
}
try {
Constructor<?> cons = null;
for (Constructor<?> c : Mesh2D.class.getDeclaredConstructors()) {
Class<?>[] params = c.getParameterTypes();
if (params.length >= 4 && params[0] == String.class) {
cons = c;
break;
}
}
if (cons != null) {
cons.setAccessible(true);
Object meshObj = cons.newInstance(name, vertices, uvs, indices);
if (meshObj instanceof Mesh2D mesh) {
mesh.setPivot(0, 0);
if (mesh.getOriginalPivot() != null) {
mesh.setOriginalPivot(new Vector2f(0, 0));
}
return mesh;
}
}
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("无法创建细分网格");
}
private static 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);
newIndices.add(i1); newIndices.add(mid12Idx); newIndices.add(mid31Idx);
newIndices.add(i2); newIndices.add(mid23Idx); newIndices.add(mid12Idx);
newIndices.add(i3); newIndices.add(mid31Idx); newIndices.add(mid23Idx);
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;
}
}
public static Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) {
try {

File diff suppressed because it is too large Load Diff

View File

@@ -1612,7 +1612,6 @@ public class ModelPart {
Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint);
vertex.position.set(worldPoint);
}
mesh.applyDeformation();
mesh.markDirty();
}
for (ModelPart child : children) {

View File

@@ -16,6 +16,9 @@ public class Vertex {
public Vector2f originalPosition; // 原始顶点位置 (用于变形)
private VertexTag tag;
private boolean selected;
private String name;
private boolean isDelete;
public int index;
/**
* 构造函数
@@ -45,6 +48,13 @@ public class Vertex {
this.tag = VertexTag.DEFAULT;
}
public Vertex(Vector2f position, Vector2f uv, VertexTag tag) {
this.position = new Vector2f(position);
this.uv = new Vector2f(uv);
this.originalPosition = new Vector2f(position);
this.tag = tag;
}
/**
* 构造函数(用于复制)
*
@@ -59,6 +69,20 @@ public class Vertex {
this.tag = VertexTag.DEFAULT;
}
/**
* 便捷构造函数,用于仅通过位置创建顶点
* UV坐标将默认为 (0, 0)。
*
* @param x 顶点 x 坐标
* @param y 顶点 y 坐标
*/
public Vertex(float x, float y) {
this.position = new Vector2f(x, y);
this.uv = new Vector2f(0, 0); // 为UV坐标提供一个默认值
this.originalPosition = new Vector2f(x, y);
this.tag = VertexTag.DEFAULT;
}
public VertexTag getTag() {
return tag;
}
@@ -67,6 +91,18 @@ public class Vertex {
this.tag = tag;
}
public void setIndex(int index) {
this.index = index;
}
public void delete(){
isDelete = true;
}
public int getIndex() {
return index;
}
/**
* 重置为原始位置
*/
@@ -97,6 +133,10 @@ public class Vertex {
return selected;
}
public boolean isDelete() {
return isDelete;
}
/**
* 创建此顶点的深拷贝
*/
@@ -109,22 +149,38 @@ public class Vertex {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Vertex vertex = (Vertex) o;
return Objects.equals(position, vertex.position) &&
return selected == vertex.selected &&
isDelete == vertex.isDelete &&
Objects.equals(position, vertex.position) &&
Objects.equals(uv, vertex.uv) &&
Objects.equals(originalPosition, vertex.originalPosition);
Objects.equals(originalPosition, vertex.originalPosition) &&
tag == vertex.tag &&
Objects.equals(name, vertex.name);
}
@Override
public int hashCode() {
return Objects.hash(position, uv, originalPosition);
return Objects.hash(position, uv, originalPosition, tag, selected, name, isDelete);
}
@Override
public String toString() {
return "Vertex{" +
"pos=" + position +
"name='" + name + '\'' +
", pos=" + position +
", uv=" + uv +
", orig=" + originalPosition +
", tag=" + tag +
", selected=" + selected +
", isDelete=" + isDelete +
'}';
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@@ -2,14 +2,11 @@ package com.chuangzhou.vivid2D.render.model.util;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.*;
/**
* 一个带有名称和标签的 Vertex 集合。
* 一个自包含的几何数据单元用于管理顶点vertices
* 以及定义它们之间拓扑结构三角形的索引indices
*
* @author Gemini
*/
@@ -17,79 +14,115 @@ public class VertexList implements Iterable<Vertex> {
public final List<Vertex> vertices;
private String name;
private int[] indices;
/**
* 默认构造函数。
* 名称默认为 "KongFuZi",标签默认为 "DEFAULT"。
*/
public VertexList() {
this.vertices = new ArrayList<>();
this.name = "KongFuZi";
this.indices = new int[0]; // 初始化为空数组
}
/**
* 构造函数
*
* @param name 此列表的名称
* @param tag 此列表的标签 (VertexTag)
*/
public VertexList(String name) {
this.vertices = new ArrayList<>();
this.name = Objects.requireNonNull(name, "Name cannot be null");
this.indices = new int[0]; // 初始化为空数组
}
/**
* 构造函数
*
* @param name 此列表的名称
* @param tag 此列表的标签
* @param name 此列表的名称
* @param initialVertices 用于初始化列表的顶点集合
* @param initialIndices 用于初始化列表的索引集合
*/
public VertexList(String name, Collection<Vertex> initialVertices) {
public VertexList(String name, Collection<Vertex> initialVertices, int[] initialIndices) {
this(name);
if (initialVertices != null) {
this.vertices.addAll(initialVertices);
}
setIndices(initialIndices); // 使用setter来安全地设置初始索引
}
// --- 列表管理 ---
// --- 列表管理 (已更新以支持索引) ---
/**
* 向列表末尾添加一个顶点。
* 注意:这不会自动更新索引。您需要手动调用 setIndices() 来使用这个新顶点。
*
* @param vertex 要添加的顶点
*/
public void add(Vertex vertex) {
if (vertex != null) {
vertex.setIndex(this.vertices.size());
this.vertices.add(vertex);
}
}
/**
* 移除列表中的指定顶点。
* 移除列表中的指定顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引
*
* @param vertex 要移除的顶点
* @return 如果成功移除则为 true
*/
public boolean remove(Vertex vertex) {
return this.vertices.remove(vertex);
public boolean remove(Object vertex) {
int index = this.vertices.indexOf(vertex);
if (index != -1) {
remove(index); // 委托给基于索引的移除方法
return true;
}
return false;
}
/**
* 移除指定索引处的顶点。
* [已重构] 移除指定索引处的顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引
*
* @param index 要移除的索引
* @return 被移除的顶点
*/
public Vertex remove(int index) {
if (index < 0 || index >= this.vertices.size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + this.vertices.size());
}
// 核心逻辑:在移除顶点前,必须重构索引数组
List<Integer> newIndicesList = new ArrayList<>();
for (int i = 0; i < this.indices.length; i += 3) {
int i1 = this.indices[i];
int i2 = this.indices[i + 1];
int i3 = this.indices[i + 2];
// 如果三角形包含被删除的顶点,则整个三角形都应该被丢弃
if (i1 == index || i2 == index || i3 == index) {
continue; // 跳过这个三角形
}
// 调整索引编号:任何大于被删除索引的索引都需要减一
if (i1 > index) i1--;
if (i2 > index) i2--;
if (i3 > index) i3--;
newIndicesList.add(i1);
newIndicesList.add(i2);
newIndicesList.add(i3);
}
// 用重构后的新索引数组替换旧的
this.indices = newIndicesList.stream().mapToInt(Integer::intValue).toArray();
// 最后,安全地从列表中移除顶点
return this.vertices.remove(index);
}
/**
* 获取指定索引处的顶点。
*
* @param index 索引
* @return 顶点
*/
public Vertex get(int index) {
return this.vertices.get(index);
@@ -97,8 +130,6 @@ public class VertexList implements Iterable<Vertex> {
/**
* 返回列表中的顶点数量。
*
* @return 顶点数量
*/
public int size() {
return this.vertices.size();
@@ -106,53 +137,124 @@ public class VertexList implements Iterable<Vertex> {
/**
* 检查列表是否为空。
*
* @return 如果为空则为 true
*/
public boolean isEmpty() {
return this.vertices.isEmpty();
}
/**
* 清空列表中的所有顶点。
* [已重构] 清空列表中的所有顶点和索引
*/
public void clear() {
this.vertices.clear();
this.indices = new int[0];
}
/**
* 返回内部列表的只读视图
* (注意:如果需要修改,请使用 add/remove 等方法)
* 返回内部顶点列表的副本
*
* @return 顶点的列表
* @return 顶点的列表副本
*/
public List<Vertex> getVertices() {
return new ArrayList<>(vertices); // 返回一个副本以防止外部修改
return new ArrayList<>(vertices);
}
// --- Getter 和 Setter ---
/**
* 获取此列表的名称
* 根据标签过滤并返回顶点列表
*
* @return 过滤后的顶点列表
*/
public List<Vertex> getVertices(VertexTag tag) {
return vertices.stream()
.filter(vertex -> vertex.getTag() == tag)
.toList();
}
// --- Getter 和 Setter (已更新以支持索引) ---
public String getName() {
return name;
}
/**
* 设置此列表的名称。
*
* @param name 新名称
*/
public void setName(String name) {
this.name = Objects.requireNonNull(name, "Name cannot be null");
}
// --- 迭代器 ---
/**
* [新增] 获取索引数组的副本。
*
* @return 索引数组的克隆
*/
public int[] getIndices() {
return indices;
}
/**
* 返回顶点的迭代器
* [新增] 获取仅由具有指定标签Tag的顶点组成的三角形索引
*
* <p>此方法会执行以下操作:
* <ol>
* <li>筛选出所有具有指定 {@code tag} 的顶点。</li>
* <li>遍历原始索引数组中的所有三角形。</li>
* <li>如果一个三角形的【全部三个顶点】都符合指定的 {@code tag},则保留该三角形。</li>
* <li>将这些保留下来的三角形的索引,重映射到仅包含筛选后顶点的新索引空间中。</li>
* </ol>
*
* @param tag 要筛选的顶点标签
* @return 一个新的索引数组,其中的索引值对应于通过 {@code getVertices(tag)} 获取的顶点列表。
*/
public int[] getIndices(VertexTag tag) {
if (tag == null) {
return new int[0];
}
// 1. 快速找到所有带标签的顶点的原始索引存入一个Set以便快速查找
Set<Integer> taggedOriginalIndices = new HashSet<>();
for (int i = 0; i < this.vertices.size(); i++) {
if (this.vertices.get(i).getTag() == tag) {
taggedOriginalIndices.add(i);
}
}
// 如果没有任何顶点带此标签,则不可能有相关三角形
if (taggedOriginalIndices.isEmpty()) {
return new int[0];
}
// 2. 遍历所有三角形只要有一个顶点的索引在Set中就保留该三角形
List<Integer> newIndices = new ArrayList<>();
for (int i = 0; i < this.indices.length; i += 3) {
int i1 = this.indices[i];
int i2 = this.indices[i + 1];
int i3 = this.indices[i + 2];
// 核心逻辑修改:从 && (AND) 改为 || (OR)
if (taggedOriginalIndices.contains(i1) ||
taggedOriginalIndices.contains(i2) ||
taggedOriginalIndices.contains(i3)) {
// 添加原始索引,因为它们将用于原始的、完整的顶点列表
newIndices.add(i1);
newIndices.add(i2);
newIndices.add(i3);
}
}
return newIndices.stream().mapToInt(Integer::intValue).toArray();
}
/**
* [新增] 设置索引数组。
*
* @param indices 新的索引数组
*/
public void setIndices(int[] indices) {
this.indices = (indices != null) ? indices.clone() : new int[0];
}
// --- 迭代器 ---
@Override
public @NotNull Iterator<Vertex> iterator() {
return this.vertices.iterator();
@@ -165,6 +267,7 @@ public class VertexList implements Iterable<Vertex> {
return "VertexList{" +
"name='" + name + '\'' +
", vertexCount=" + vertices.size() +
", indexCount=" + indices.length +
'}';
}
@@ -174,12 +277,15 @@ public class VertexList implements Iterable<Vertex> {
if (o == null || getClass() != o.getClass()) return false;
VertexList that = (VertexList) o;
return Objects.equals(vertices, that.vertices) &&
Objects.equals(name, that.name);
Objects.equals(name, that.name) &&
Arrays.equals(indices, that.indices);
}
@Override
public int hashCode() {
return Objects.hash(vertices, name);
int result = Objects.hash(vertices, name);
result = 31 * result + Arrays.hashCode(indices);
return result;
}
public void set(int index, Vertex vertex) {

View File

@@ -11,21 +11,24 @@ import org.joml.Vector2f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* [MODIFIED] 顶点变形渲染工具
* 该类现在渲染 Mesh2D 中被标记为 VertexTag.DEFORMATION 的一级顶点。
* [已修改] 顶点变形渲染工具 (支持外部控制笼)
* 该类现在渲染
* 1. 底层被变形网格的【三角剖分线框】。
* 2. 位于上层的【外部控制笼】(控制点和连线)。
* 这样可以直观地展示控制笼如何影响网格变形。
*/
public class VertexDeformationRander extends RanderTools {
// [NEW] 定义可变形顶点的默认渲染大小
private static final float DEFORMATION_VERTEX_SIZE = 8.0f;
private static final float DEFORMATION_VERTEX_SIZE = 6.0f;
@Override
public void init(Map<String, Boolean> algorithmEnabled) {
// [MODIFIED] 更新算法名称以反映新功能
algorithmEnabled.put("showDeformationVertices", false);
algorithmEnabled.put("showDeformationVertexInfluence", true);
}
@@ -33,9 +36,14 @@ public class VertexDeformationRander extends RanderTools {
@Override
public boolean render(Matrix3f modelMatrix, Object renderContext) {
if (renderContext instanceof Mesh2D mesh2D) {
// [MODIFIED] 检查新的算法开关并调用新的渲染方法
if (mesh2D.getStates("showDeformationVertices")) {
drawDeformationVertices(mesh2D);
RenderSystem.pushState();
try {
mesh2D.setSolidShader(modelMatrix);
drawControlCageAndMesh(mesh2D);
} finally {
RenderSystem.popState();
}
return true;
}
}
@@ -43,44 +51,28 @@ public class VertexDeformationRander extends RanderTools {
}
/**
* [MODIFIED] 重写此方法以渲染被标记为 DEFORMATION 的一级顶点
* @param mesh2D 要渲染的网格
* [已修改] 此方法现在会先渲染底层网格的线框,然后再渲染上层的控制笼
* @param mesh2D 要渲染的网格 (作为被变形的对象)
*/
private void drawDeformationVertices(Mesh2D mesh2D) {
// 1. 筛选出所有可变形的顶点
List<Vertex> deformationVertices = mesh2D.getActiveVertexList().vertices.stream()
.filter(v -> v.getTag() == VertexTag.DEFORMATION)
.collect(Collectors.toList());
if (deformationVertices.isEmpty()) {
return; // 没有可变形顶点,无需渲染
}
private void drawControlCageAndMesh(Mesh2D mesh2D) {
List<Vertex> controlCage = mesh2D.getDeformationControlVertices();
Tesselator t = Tesselator.getInstance();
BufferBuilder bb = t.getBuilder();
RenderSystem.pushState();
try {
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_DEPTH_TEST);
// 2. 绘制连接线
drawConnectionLines(bb, deformationVertices);
// 3. 绘制每个可变形顶点
for (Vertex vertex : deformationVertices) {
Vector2f p = vertex.position;
// 定义颜色
Vector4f centerCol = new Vector4f(0.1f, 0.9f, 0.2f, 0.7f); // 中心亮绿色
Vector4f outerCol = new Vector4f(0.0f, 0.5f, 0.1f, 0.3f); // 外部深绿色
Vector4f outlineCol = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 白色轮廓
// 绘制顶点视觉效果
drawCircleGradient(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, centerCol, outerCol, 16);
drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, outlineCol, 16);
drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE * 0.5f, new Vector4f(1.0f, 1.0f, 1.0f, 0.5f), 12);
drawMeshWireframe(bb, mesh2D);
if (controlCage != null && !controlCage.isEmpty()) {
List<Vertex> orderedCage = getOrderedVertices(controlCage);
drawConnectionLines(bb, orderedCage);
for (Vertex vertex : orderedCage) {
if (vertex == null){
continue;
}
drawStyledVertex(bb, vertex.position.x, vertex.position.y, vertex.isSelected());
}
}
} finally {
RenderSystem.popState();
@@ -88,127 +80,113 @@ public class VertexDeformationRander extends RanderTools {
}
/**
* [MODIFIED] 此方法现在接收一个 List<Vertex>
* 绘制连接可变形顶点的线
* 绘制网格的线框,以显示其三角剖分结构。
* 这就是“Vertex 的控制区域”的可视化
* @param bb BufferBuilder 实例
* @param mesh 目标网格
*/
private void drawConnectionLines(BufferBuilder bb, List<Vertex> verts) {
if (verts.size() < 2) return;
// 为了美观,我们对顶点进行排序以绘制一个简单的多边形轮廓
List<Vector2f> positions = verts.stream().map(v -> v.position).collect(Collectors.toList());
List<Vector2f> orderedPositions = buildOrderedPolygon(positions);
if (orderedPositions.size() < 2) return;
GL11.glLineWidth(1.5f);
bb.begin(GL11.GL_LINE_LOOP, orderedPositions.size());
bb.setColor(new Vector4f(1f, 1f, 1f, 0.25f));
for (Vector2f p : orderedPositions) {
bb.vertex(p.x, p.y, 0f, 0f);
private void drawMeshWireframe(BufferBuilder bb, Mesh2D mesh) {
List<Vertex> vertices = mesh.getActiveVertexList().getVertices();
int[] indices = mesh.getActiveVertexList().getIndices(VertexTag.DEFORMATION);
if (vertices == null || vertices.isEmpty() || indices == null || indices.length == 0) {
return;
}
bb.endImmediate();
Vector4f wireframeColor = new Vector4f(0.5f, 0.5f, 0.5f, 0.4f);
GL11.glLineWidth(1.0f);
for (int i = 0; i < orderedPositions.size(); i++) {
Vector2f a = orderedPositions.get(i);
Vector2f b = orderedPositions.get((i + 1) % orderedPositions.size());
drawDashedLine(bb, a.x, a.y, b.x, b.y, 6, 4, new Vector4f(1f,1f,1f,0.1f));
for (int i = 0; i < indices.length; i += 3) {
int i1 = indices[i];
int i2 = indices[i + 1];
int i3 = indices[i + 2];
Vertex v1 = vertices.get(i1);
Vertex v2 = vertices.get(i2);
Vertex v3 = vertices.get(i3);
bb.begin(GL11.GL_LINE_LOOP, 3);
bb.setColor(wireframeColor);
bb.vertex(v1.position.x, v1.position.y, 0f, 0f);
bb.vertex(v2.position.x, v2.position.y, 0f, 0f);
bb.vertex(v3.position.x, v3.position.y, 0f, 0f);
bb.endImmediate();
}
}
/**
* 根据输入点(任意顺序),生成一个“简单多边形”顺序:
* - 先计算重心
* - 按相对于重心的 atan2 排序(极角排序)
* 绘制连接控制顶点的虚线。
*/
private java.util.List<Vector2f> buildOrderedPolygon(java.util.List<Vector2f> pts) {
if (pts == null || pts.size() <= 1) return new ArrayList<>(pts);
private void drawConnectionLines(BufferBuilder bb, List<Vertex> verts) {
if (verts == null || verts.size() < 2) return;
Vector4f lineColor = new Vector4f(0.1f, 0.1f, 0.1f, 0.85f);
// 计算重心
Vector2f cen = new Vector2f(0f, 0f);
for (Vector2f v : pts) { cen.add(v); }
cen.div(pts.size());
// 创建副本并按角度排序
List<Vector2f> sortedPts = new ArrayList<>(pts);
sortedPts.sort(Comparator.comparingDouble(p -> Math.atan2(p.y - cen.y, p.x - cen.x)));
return sortedPts;
for (int i = 0; i < verts.size(); i++) {
Vector2f a = verts.get(i).position;
Vector2f b = verts.get((i + 1) % verts.size()).position; // 使用 % 来处理首尾相连
drawDashedLine(bb, a.x, a.y, b.x, b.y, 4, 4, lineColor);
}
}
// --- 以下是无需修改的通用绘图和几何辅助方法 ---
/**
* 绘制自定义样式的顶点,并根据 'isSelected' 状态改变颜色。
*/
private void drawStyledVertex(BufferBuilder bb, float cx, float cy, boolean isSelected) {
float halfSize = VertexDeformationRander.DEFORMATION_VERTEX_SIZE / 2.0f;
Vector4f fillColor = isSelected ? new Vector4f(1.0f, 0.3f, 0.3f, 1.0f) : new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
bb.begin(GL11.GL_QUADS, 4);
bb.setColor(fillColor);
bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f);
bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f);
bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f);
bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f);
bb.endImmediate();
GL11.glLineWidth(1.5f);
bb.begin(GL11.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 1.0f));
bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f);
bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f);
bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f);
bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f);
bb.endImmediate();
GL11.glLineWidth(1.0f);
}
/**
* 辅助方法,获取按极角排序后的顶点列表 (Vertex 对象)。
*/
private List<Vertex> getOrderedVertices(List<Vertex> verts) {
if (verts == null || verts.size() <= 1) return new ArrayList<>(verts);
Vector2f center = new Vector2f(0f, 0f);
for (Vertex v : verts) { center.add(v.position); }
center.div(verts.size());
List<Vertex> sortedVerts = new ArrayList<>(verts);
sortedVerts.sort(Comparator.comparingDouble(v -> Math.atan2(v.position.y - center.y, v.position.x - center.x)));
return sortedVerts;
}
/**
* 绘制虚线的底层实现。
*/
private void drawDashedLine(BufferBuilder bb, float x1, float y1, float x2, float y2, float segmentLen, float gapLen, Vector4f color) {
float dx = x2 - x1;
float dy = y2 - y1;
float dx = x2 - x1, dy = y2 - y1;
float total = (float)Math.sqrt(dx*dx + dy*dy);
if (total < 1e-4f) return;
float nx = dx / total;
float ny = dy / total;
float nx = dx / total, ny = dy / total;
float pos = 0f;
GL11.glLineWidth(1.5f); // 让虚线稍微粗一点
while (pos < total) {
float segStart = pos;
float segEnd = Math.min(total, pos + segmentLen);
if (segEnd > segStart) {
float sx = x1 + nx * segStart;
float sy = y1 + ny * segStart;
float ex = x1 + nx * segEnd;
float ey = y1 + ny * segEnd;
float sx = x1 + nx * segStart, sy = y1 + ny * segStart;
float ex = x1 + nx * segEnd, ey = y1 + ny * segEnd;
bb.begin(GL11.GL_LINES, 2);
bb.setColor(color);
bb.vertex(sx, sy, 0f, 0f);
bb.vertex(ex, ey, 0f, 0f);
bb.vertex(sx, sy, 0f, 0f); bb.vertex(ex, ey, 0f, 0f);
bb.endImmediate();
}
pos += segmentLen + gapLen;
}
}
private void drawCircleSolid(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) {
if (radius <= 0f) return;
segments = Math.max(6, segments);
bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2);
bb.setColor(color);
bb.vertex(cx, cy, 0f, 0f);
for (int i = 0; i <= segments; i++) {
double ang = 2.0 * Math.PI * i / segments;
float x = cx + (float) (Math.cos(ang) * radius);
float y = cy + (float) (Math.sin(ang) * radius);
bb.setColor(color);
bb.vertex(x, y, 0f, 0f);
}
bb.endImmediate();
}
private void drawCircleGradient(BufferBuilder bb, float cx, float cy, float radius, Vector4f centerColor, Vector4f outerColor, int segments) {
if (radius <= 0f) return;
segments = Math.max(8, segments);
bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2);
bb.setColor(centerColor);
bb.vertex(cx, cy, 0f, 0f);
for (int i = 0; i <= segments; i++) {
double ang = 2.0 * Math.PI * i / segments;
float x = cx + (float) (Math.cos(ang) * radius);
float y = cy + (float) (Math.sin(ang) * radius);
bb.setColor(outerColor);
bb.vertex(x, y, 0f, 0f);
}
bb.endImmediate();
}
private void drawCircleOutline(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) {
if (radius <= 0f) return;
segments = Math.max(8, segments);
bb.begin(GL11.GL_LINE_LOOP, segments);
bb.setColor(color);
for (int i = 0; i < segments; i++) {
double ang = 2.0 * Math.PI * i / segments;
float x = cx + (float) (Math.cos(ang) * radius);
float y = cy + (float) (Math.sin(ang) * radius);
bb.vertex(x, y, 0f, 0f);
}
bb.endImmediate();
GL11.glLineWidth(1.0f);
}
}