From 7ac960be5edeaddec22a09f71094c99fe62ef104 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Fri, 24 Oct 2025 20:05:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E5=AE=9E=E7=8E=B0=E6=91=84?= =?UTF-8?q?=E5=83=8F=E6=9C=BA=E7=B3=BB=E7=BB=9F=E5=92=8C=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Camera 类,支持位置、缩放、Z轴控制- 在 ModelRender 中集成摄像机投影矩阵计算 - 实现屏幕坐标到世界坐标的转换方法 - 添加默认文字渲染器和字体加载逻辑 - 在渲染面板中添加摄像机控制的鼠标手势支持 - 支持通过鼠标滚轮进行摄像机缩放操作 - 添加摄像机状态显示和调试信息渲染 - 实现多选框渲染逻辑的重构和优化 -修复坐标系变换相关的边界框计算问题 - 增加摄像机启用/禁用快捷键支持 --- .../vivid2D/render/ModelRender.java | 202 +++++- .../vivid2D/render/TextRenderer.java | 237 ++++++ .../vivid2D/render/awt/ModelRenderPanel.java | 682 ++++++++++++++---- .../vivid2D/render/model/ModelPart.java | 2 +- .../vivid2D/render/model/util/Mesh2D.java | 101 +-- .../vivid2D/render/systems/Camera.java | 67 ++ .../systems/MultiSelectionBoxRenderer.java | 335 +++++++++ .../vivid2D/render/systems/RenderSystem.java | 302 +++++++- .../systems/sources/ShaderManagement.java | 4 +- .../systems/sources/def/TextShader.java | 96 +++ .../chuangzhou/vivid2D/test/ModelTest.java | 16 +- 11 files changed, 1798 insertions(+), 246 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 2860085..bebd8ca 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -2,6 +2,7 @@ package com.chuangzhou.vivid2D.render; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.systems.Camera; import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; import com.chuangzhou.vivid2D.render.model.util.LightSource; @@ -18,6 +19,7 @@ import org.lwjgl.opengl.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -73,14 +75,14 @@ public final class ModelRender { * 默认值:800像素 * @see #setViewport(int, int) */ - private static int viewportWidth = 800; + static int viewportWidth = 800; /** * 视口高度(像素),定义渲染区域的大小 * 默认值:600像素 * @see #setViewport(int, int) */ - private static int viewportHeight = 600; + static int viewportHeight = 600; /** * 清除颜色(RGBA),用于在每帧开始时清空颜色缓冲区 @@ -179,8 +181,105 @@ public final class ModelRender { */ public static boolean renderLightPositions = true; - // ================== 内部类:ShaderProgram ================== +// ================== 摄像机状态 ================== + /** + * 默认摄像机,用于控制场景的视图和缩放 + * 默认位置:(0, 0) + */ + private static final Camera camera = new Camera(); + +// ================== 字体管理 ================== + private static TextRenderer defaultTextRenderer = null; + private static final int FONT_BITMAP_WIDTH = 512; + private static final int FONT_BITMAP_HEIGHT = 512; + private static final int FONT_FIRST_CHAR = 32; + private static final int FONT_CHAR_COUNT = 96; + // ================== 摄像机API方法 ================== + + /** + * 获取全局摄像机实例 + */ + public static Camera getCamera() { + return camera; + } + + /** + * 设置摄像机位置 + */ + public static void setCameraPosition(float x, float y) { + camera.setPosition(x, y); + } + + /** + * 设置摄像机缩放 + */ + public static void setCameraZoom(float zoom) { + camera.setZoom(zoom); + } + + /** + * 设置摄像机Z轴位置 + */ + public static void setCameraZPosition(float z) { + camera.setZPosition(z); + } + + /** + * 移动摄像机 + */ + public static void moveCamera(float dx, float dy) { + camera.move(dx, dy); + } + + /** + * 缩放摄像机 + */ + public static void zoomCamera(float factor) { + camera.zoom(factor); + } + + /** + * 重置摄像机 + */ + public static void resetCamera() { + camera.reset(); + } + + /** + * 启用/禁用摄像机 + */ + public static void setCameraEnabled(boolean enabled) { + camera.setEnabled(enabled); + } + + /** + * 构建考虑摄像机变换的投影矩阵 + */ + private static Matrix3f buildCameraProjection(int width, int height) { + Matrix3f m = new Matrix3f(); + + if (camera.isEnabled()) { + // 考虑摄像机缩放和平移 + float zoom = camera.getZoom(); + Vector2f pos = camera.getPosition(); + + m.set( + 2.0f * zoom / width, 0.0f, -1.0f - (2.0f * zoom * pos.x / width), + 0.0f, -2.0f * zoom / height, 1.0f + (2.0f * zoom * pos.y / height), + 0.0f, 0.0f, 1.0f + ); + } else { + // 原始投影矩阵 + m.set( + 2.0f / width, 0.0f, -1.0f, + 0.0f, -2.0f / height, 1.0f, + 0.0f, 0.0f, 1.0f + ); + } + + return m; + } // ================== 内部类:MeshGLResources ================== private static class MeshGLResources { @@ -221,10 +320,47 @@ public final class ModelRender { throw ex; } + createDefaultTexture(); RenderSystem.viewport(0, 0, viewportWidth, viewportHeight); RenderSystem.finishInitialization(); + try { + // 初始化默认字体(可替换为你自己的 TTF 数据) + ByteBuffer fontData = null; + try { + fontData = RenderSystem.loadWindowsFont("Arial.ttf"); + } catch (Exception e) { + logger.warn("Failed to load Arial.ttf, trying fallback fonts", e); + // 尝试其他字体 + try { + fontData = RenderSystem.loadWindowsFont("arial.ttf"); + } catch (Exception e2) { + try { + fontData = RenderSystem.loadWindowsFont("times.ttf"); + } catch (Exception e3) { + logger.error("All font loading attempts failed"); + } + } + } + + if (fontData != null && fontData.capacity() > 0) { + defaultTextRenderer = new TextRenderer(FONT_BITMAP_WIDTH, FONT_BITMAP_HEIGHT, FONT_FIRST_CHAR, FONT_CHAR_COUNT); + RenderSystem.checkGLError("TextRenderer constructor"); + + defaultTextRenderer.initialize(fontData, 32.0f); // 字体像素高度 32 + RenderSystem.checkGLError("defaultTextRenderer initialization"); + + if (!defaultTextRenderer.isInitialized()) { + logger.error("TextRenderer failed to initialize properly"); + } + } else { + logger.error("No valid font data available for text rendering"); + } + } catch (Exception e) { + logger.warn("Failed to initialize default text renderer", e); + } + initialized = true; logger.info("ModelRender initialized successfully"); } @@ -456,8 +592,8 @@ public final class ModelRender { return; } - // 设置投影与视图矩阵(所有着色器都需要) - Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight); + // 设置投影与视图矩阵(使用摄像机变换) + Matrix3f proj = buildCameraProjection(viewportWidth, viewportHeight); Matrix3f view = new Matrix3f().identity(); // 1. 首先设置默认着色器 @@ -469,6 +605,10 @@ public final class ModelRender { setUniformMatrix3(defaultProgram, "uViewMatrix", view); RenderSystem.checkGLError("after_set_default_matrices"); + // 设置摄像机Z轴位置(如果着色器支持) + setUniformFloatInternal(defaultProgram, "uCameraZ", camera.getZPosition()); + RenderSystem.checkGLError("after_set_camera_z"); + // 添加光源数据上传到默认着色器 uploadLightsToShader(defaultProgram, model); RenderSystem.checkGLError("after_upload_lights"); @@ -495,10 +635,22 @@ public final class ModelRender { RenderSystem.checkGLError("after_render_colliders"); } - defaultProgram.stop(); + if (defaultTextRenderer != null) { + String camInfo = String.format("Camera X: %.2f Y: %.2f Zoom: %.2f", + camera.getPosition().x, + camera.getPosition().y, + camera.getZoom()); + float x = 10.0f; + float y = viewportHeight - 30.0f; + Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + renderText(camInfo, x, y, color); + RenderSystem.checkGLError("renderText"); + } + RenderSystem.checkGLError("render_end"); } + /** * 设置所有非默认着色器的顶点坐标相关uniform */ @@ -534,6 +686,9 @@ public final class ModelRender { // 设置基础模型矩阵为单位矩阵 setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity()); + // 设置摄像机Z轴位置 + setUniformFloatInternal(program, "uCameraZ", camera.getZPosition()); + RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName()); } catch (Exception e) { @@ -840,6 +995,41 @@ public final class ModelRender { return m; } + /** + * 渲染文字 + * @param text 文字内容 + * @param x 世界坐标 X + * @param y 世界坐标 Y + * @param color RGBA 颜色 + */ + public static void renderText(String text, float x, float y, Vector4f color) { + if (!initialized || defaultTextRenderer == null) return; + RenderSystem.assertOnRenderThread(); + Vector2f offset = getCameraOffset(); + float px = x + offset.x; + float py = y + offset.y; + defaultTextRenderer.renderText(text, px, py, color); + } + + /** + * 获取默认摄像机与当前摄像机之间的偏移量 + * @return Vector2f 偏移向量 (dx, dy) + */ + public static Vector2f getCameraOffset() { + float width = viewportWidth; + float height = viewportHeight; + float zoom = camera.getZoom(); + Vector2f pos = camera.getPosition(); + float tx = -1.0f - (2.0f * zoom * pos.x / width); + float ty = 1.0f + (2.0f * zoom * pos.y / height); + float tx0 = -1.0f; + float ty0 = 1.0f; + float offsetX = tx - tx0; + float offsetY = ty - ty0; + offsetX = -offsetX * width / 2.0f / zoom; + offsetY = offsetY * height / 2.0f / zoom; + return new Vector2f(offsetX, offsetY); + } public static void setViewport(int width, int height) { viewportWidth = Math.max(1, width); viewportHeight = Math.max(1, height); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java new file mode 100644 index 0000000..1e5f0aa --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java @@ -0,0 +1,237 @@ +package com.chuangzhou.vivid2D.render; + +import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; +import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; +import org.joml.Vector2f; +import org.joml.Vector4f; +import org.lwjgl.opengl.*; +import org.lwjgl.stb.STBTTAlignedQuad; +import org.lwjgl.stb.STBTTBakedChar; +import org.lwjgl.system.MemoryStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; + +import static org.lwjgl.stb.STBTruetype.*; + +/** + * OpenGL 文字渲染器实例类 + * 支持多字体、多实例管理,每个实例维护独立的字符数据与纹理 + * + * @author tzdwindows 7 + */ +public final class TextRenderer { + private static final Logger logger = LoggerFactory.getLogger(TextRenderer.class); + private final int bitmapWidth; + private final int bitmapHeight; + private final int firstChar; + private final int charCount; + + private STBTTBakedChar.Buffer charData; + private int fontTextureId; + private boolean initialized = false; + + /** + * 构造函数 + * + * @param bitmapWidth 字符纹理宽度 + * @param bitmapHeight 字符纹理高度 + * @param firstChar 字符起始码 + * @param charCount 字符数量 + */ + public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) { + this.bitmapWidth = bitmapWidth; + this.bitmapHeight = bitmapHeight; + this.firstChar = firstChar; + this.charCount = charCount; + } + + /** + * 初始化字体渲染器 + * + * @param fontData TTF 字体文件内容 + * @param fontHeight 字体像素高度 + */ + public void initialize(ByteBuffer fontData, float fontHeight) { + if (initialized) return; + ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader"); + shader.use(); + // 验证输入参数 + if (fontData == null || fontData.capacity() == 0) { + logger.error("Invalid font data provided to TextRenderer"); + return; + } + if (fontHeight <= 0) { + logger.error("Invalid font height: {}", fontHeight); + return; + } + if (bitmapWidth <= 0 || bitmapHeight <= 0) { + logger.error("Invalid bitmap dimensions: {}x{}", bitmapWidth, bitmapHeight); + return; + } + try { + charData = STBTTBakedChar.malloc(charCount); + + // 分配位图内存 + int bitmapSize = bitmapWidth * bitmapHeight; + if (bitmapSize <= 0) { + logger.error("Invalid bitmap size: {}", bitmapSize); + return; + } + + ByteBuffer bitmap = ByteBuffer.allocateDirect(bitmapSize); + + // 烘焙字体位图 + int result = stbtt_BakeFontBitmap(fontData, fontHeight, bitmap, bitmapWidth, bitmapHeight, firstChar, charData); + if (result <= 0) { + logger.error("stbtt_BakeFontBitmap failed with result: {}", result); + charData.free(); + return; + } + + // 创建纹理 + fontTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, bitmap); + + if (fontTextureId == 0) { + logger.error("Failed to create font texture"); + charData.free(); + return; + } + + initialized = true; + logger.debug("TextRenderer initialized successfully with texture ID: {}", fontTextureId); + + } catch (Exception e) { + logger.error("Exception during TextRenderer initialization: {}", e.getMessage()); + if (charData != null) { + charData.free(); + charData = null; + } + } + shader.stop(); + } + + private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) { + RenderSystem.assertOnRenderThread(); + + int textureId = RenderSystem.genTextures(); + RenderSystem.bindTexture(textureId); + + // 使用更兼容的纹理格式 + RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_ALPHA, + width, height, 0, GL11.GL_ALPHA, GL11.GL_UNSIGNED_BYTE, pixels); + + RenderSystem.setTextureMinFilter(GL11.GL_LINEAR); + RenderSystem.setTextureMagFilter(GL11.GL_LINEAR); + RenderSystem.setTextureWrapS(GL12.GL_CLAMP_TO_EDGE); + RenderSystem.setTextureWrapT(GL12.GL_CLAMP_TO_EDGE); + + RenderSystem.bindTexture(0); + return textureId; + } + + /** + * 渲染文字(使用 RenderSystem 封装,不使用 glBegin/glEnd) + * + * @param text 要显示的文字 + * @param x 世界坐标 X + * @param y 世界坐标 Y + * @param color 文字颜色 + */ + public void renderText(String text, float x, float y, Vector4f color) { + if (!initialized || text == null || text.isEmpty()) return; + + RenderSystem.assertOnRenderThread(); + + // 保存当前状态 + RenderSystem.pushState(); + + try { + // 检查文本着色器是否存在,如果不存在则创建默认的 + ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader"); + + shader.use(); + ShaderManagement.setUniformVec4(shader, "uColor", color); + ShaderManagement.setUniformInt(shader, "uTexture", 0); + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + RenderSystem.disableDepthTest(); + + RenderSystem.activeTexture(GL13.GL_TEXTURE0); + RenderSystem.bindTexture(fontTextureId); + + Vector2f offset = ModelRender.getCameraOffset(); + float px = x + offset.x; + float py = y + offset.y; + + try (MemoryStack stack = MemoryStack.stackPush()) { + STBTTAlignedQuad q = STBTTAlignedQuad.mallocStack(stack); + float[] xpos = {px}; + float[] ypos = {py}; + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder builder = tesselator.getBuilder(); + + // 计算估计的顶点数量:每个字符6个顶点(2个三角形) + int estimatedVertexCount = text.length() * 6; + + // 修复:begin方法需要2个参数 + builder.begin(RenderSystem.DRAW_TRIANGLES, estimatedVertexCount); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c < firstChar || c >= firstChar + charCount) continue; + + stbtt_GetBakedQuad(charData, bitmapWidth, bitmapHeight, c - firstChar, xpos, ypos, q, true); + + // 使用两个三角形组成一个四边形 + // 第一个三角形 + builder.vertex(q.x0(), q.y0(), q.s0(), q.t0()); + builder.vertex(q.x1(), q.y0(), q.s1(), q.t0()); + builder.vertex(q.x0(), q.y1(), q.s0(), q.t1()); + + // 第二个三角形 + builder.vertex(q.x1(), q.y0(), q.s1(), q.t0()); + builder.vertex(q.x1(), q.y1(), q.s1(), q.t1()); + builder.vertex(q.x0(), q.y1(), q.s0(), q.t1()); + } + + tesselator.end(); + } + + RenderSystem.checkGLError("renderText"); + + } finally { + // 恢复之前的状态 + RenderSystem.popState(); + } + } + + /** + * 清理字体资源 + */ + public void cleanup() { + if (fontTextureId != 0) { + RenderSystem.deleteTextures(fontTextureId); + fontTextureId = 0; + } + if (charData != null) { + charData.free(); + charData = null; + } + initialized = false; + } + + public boolean isInitialized() { + return initialized; + } + + public int getFontTextureId() { + return fontTextureId; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java index e410d71..1d02aaa 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -7,7 +7,10 @@ import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; 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.Matrix3fUtils; import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import org.joml.Matrix3f; import org.joml.Vector2f; import org.lwjgl.glfw.*; import org.lwjgl.opengl.GL; @@ -126,6 +129,75 @@ public class ModelRenderPanel extends JPanel { private Map dragStartRotations = new HashMap<>(); private Map dragStartPivots = new HashMap<>(); + // 新增:鼠标手势相关字段 + private volatile Cursor currentCursor = Cursor.getDefaultCursor(); + private volatile DragMode hoverDragMode = DragMode.NONE; + private volatile boolean isOverSelection = false; + + // ================== 摄像机控制相关字段 ================== + + private volatile boolean cameraDragging = false; + private volatile int lastCameraDragX, lastCameraDragY; + private volatile float cameraStartX, cameraStartY; + private static final float CAMERA_ZOOM_STEP = 1.1f; + private static final float CAMERA_ZOOM_MIN = 0.1f; + private static final float CAMERA_ZOOM_MAX = 10.0f; + private static final float CAMERA_Z_STEP = 0.1f; + private static final float CAMERA_Z_MIN = -5.0f; + private static final float CAMERA_Z_MAX = 5.0f; + +// ================== 摄像机控制方法 ================== + + /** + * 获取摄像机实例 + */ + public Camera getCamera() { + return ModelRender.getCamera(); + } + + /** + * 设置摄像机位置 + */ + public void setCameraPosition(float x, float y) { + executeInGLContext(() -> ModelRender.setCameraPosition(x, y)); + } + + /** + * 设置摄像机缩放 + */ + public void setCameraZoom(float zoom) { + executeInGLContext(() -> ModelRender.setCameraZoom(zoom)); + } + + /** + * 设置摄像机Z轴位置 + */ + public void setCameraZPosition(float z) { + executeInGLContext(() -> ModelRender.setCameraZPosition(z)); + } + + /** + * 移动摄像机 + */ + public void moveCamera(float dx, float dy) { + executeInGLContext(() -> ModelRender.moveCamera(dx, dy)); + } + + /** + * 缩放摄像机 + */ + public void zoomCamera(float factor) { + executeInGLContext(() -> ModelRender.zoomCamera(factor)); + } + + /** + * 重置摄像机 + */ + public void resetCamera() { + executeInGLContext(() -> ModelRender.resetCamera()); + } + + /** * 构造函数:使用模型路径 */ @@ -190,6 +262,32 @@ public class ModelRenderPanel extends JPanel { clearHistory(); } }); + + // 添加摄像机重置快捷键:Ctrl+R + KeyStroke resetCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(resetCameraKey, "resetCamera"); + actionMap.put("resetCamera", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + resetCamera(); + logger.info("重置摄像机"); + } + }); + + // 添加摄像机启用/禁用快捷键:Ctrl+E + KeyStroke toggleCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(toggleCameraKey, "toggleCamera"); + actionMap.put("toggleCamera", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + //executeInGLContext(() -> { + Camera camera = ModelRender.getCamera(); + boolean newState = !camera.isEnabled(); + camera.setEnabled(newState); + logger.info("{}摄像机", newState ? "启用" : "禁用"); + //}); + } + }); } // ============ 新增:操作历史记录方法 ============ @@ -371,28 +469,28 @@ public class ModelRenderPanel extends JPanel { */ public void setSelectedMesh(Mesh2D mesh) { //executeInGLContext(() -> { - // 清除之前选中的所有网格 - for (Mesh2D selectedMesh : selectedMeshes) { - selectedMesh.setSelected(false); - // 清除多选列表 - selectedMesh.clearMultiSelection(); - } - selectedMeshes.clear(); + // 清除之前选中的所有网格 + for (Mesh2D selectedMesh : selectedMeshes) { + selectedMesh.setSelected(false); + // 清除多选列表 + selectedMesh.clearMultiSelection(); + } + selectedMeshes.clear(); - // 设置新的选中网格 - if (mesh != null) { - mesh.setSelected(true); - selectedMeshes.add(mesh); - lastSelectedMesh = mesh; // 更新最后选中的网格 + // 设置新的选中网格 + if (mesh != null) { + mesh.setSelected(true); + selectedMeshes.add(mesh); + lastSelectedMesh = mesh; // 更新最后选中的网格 - // 通知其他选中网格添加到多选列表 - updateMultiSelectionInMeshes(); - } else { - lastSelectedMesh = null; - } + // 通知其他选中网格添加到多选列表 + updateMultiSelectionInMeshes(); + } else { + lastSelectedMesh = null; + } - logger.debug("设置选中网格: {}, 当前选中数量: {}", - mesh != null ? mesh.getName() : "null", selectedMeshes.size()); + logger.debug("设置选中网格: {}, 当前选中数量: {}", + mesh != null ? mesh.getName() : "null", selectedMeshes.size()); //}); } @@ -631,8 +729,71 @@ public class ModelRenderPanel extends JPanel { public void mouseReleased(MouseEvent e) { handleMouseReleased(e); } + + @Override + public void mouseExited(MouseEvent e) { + // 鼠标离开面板时恢复默认光标 + setCursor(Cursor.getDefaultCursor()); + } }); + addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (!contextInitialized) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + final int notches = e.getWheelRotation(); + final boolean fine = e.isShiftDown(); + + executeInGLContext(() -> { + Camera camera = ModelRender.getCamera(); + float oldZoom = camera.getZoom(); + + // 1. 获取缩放前的世界坐标 + float[] worldPosBefore = screenToModelCoordinates(screenX, screenY); + if (worldPosBefore == null) return; + + // 2. 计算新缩放级别 + // 使用 CAMERA_ZOOM_STEP 或 ZOOM_STEP,这里沿用原有的 ZOOM_STEP 逻辑 + double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP; + float newZoom = oldZoom; + if (notches > 0) { // 滚轮向下,缩小 + newZoom /= Math.pow(step, notches); + } else { // 滚轮向上,放大 + newZoom *= Math.pow(step, -notches); + } + // 限制范围,使用 CAMERA_ZOOM_MIN/MAX 或 ZOOM_MIN/MAX,这里沿用原有的 ZOOM_MIN/MAX + newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom)); + + if (Math.abs(newZoom - oldZoom) < 1e-6f) { + return; // 缩放级别无变化 + } + + // 3. 应用新缩放并获取缩放后的世界坐标 + camera.setZoom(newZoom); + float[] worldPosAfter = screenToModelCoordinates(screenX, screenY); + if (worldPosAfter == null) { + camera.setZoom(oldZoom); // 如果计算失败则恢复 + return; + } + + // 4. 计算相机需要平移的量,以保持鼠标指针下的点不变 + float panX = worldPosBefore[0] - worldPosAfter[0]; + float panY = worldPosBefore[1] - worldPosAfter[1]; + + // 5. 应用平移 + camera.move(panX, panY); + + // 6. 更新面板的缩放状态变量,禁用平滑缩放以确保一致性 + displayScale = newZoom; + targetScale = newZoom; + }); + } + }); + + addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { @@ -742,6 +903,21 @@ public class ModelRenderPanel extends JPanel { final int screenY = e.getY(); requestFocusInWindow(); + if (SwingUtilities.isMiddleMouseButton(e)) { + cameraDragging = true; + lastCameraDragX = screenX; + lastCameraDragY = screenY; + + // 记录摄像机起始位置 + Camera camera = ModelRender.getCamera(); + cameraStartX = camera.getPosition().x; + cameraStartY = camera.getPosition().y; + + // 设置拖拽光标 + setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + return; + } + shiftDuringDrag = e.isShiftDown(); executeInGLContext(() -> { @@ -845,12 +1021,57 @@ public class ModelRenderPanel extends JPanel { } } } + + // 更新拖拽过程中的光标 + updateCursorForDragMode(currentDragMode); + } catch (Exception ex) { logger.error("处理鼠标按下时出错", ex); } }); } + /** + * 根据拖拽模式更新光标 + */ + private void updateCursorForDragMode(DragMode dragMode) { + Cursor newCursor = getCursorForDragMode(dragMode); + if (!newCursor.equals(currentCursor)) { + currentCursor = newCursor; + SwingUtilities.invokeLater(() -> setCursor(newCursor)); + } + } + + /** + * 根据拖拽模式获取对应的光标 + */ + private Cursor getCursorForDragMode(DragMode dragMode) { + switch (dragMode) { + case MOVE: + return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); + case RESIZE_LEFT: + case RESIZE_RIGHT: + return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR); + case RESIZE_TOP: + case RESIZE_BOTTOM: + return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR); + case RESIZE_TOP_LEFT: + case RESIZE_BOTTOM_RIGHT: + return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR); + case RESIZE_TOP_RIGHT: + case RESIZE_BOTTOM_LEFT: + return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR); + case ROTATE: + // 使用手型光标表示旋转 + return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + case MOVE_PIVOT: + // 使用十字光标表示移动中心点 + return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); + case NONE: + default: + return Cursor.getDefaultCursor(); + } + } /** * 处理多选逻辑 @@ -922,43 +1143,116 @@ public class ModelRenderPanel extends JPanel { fromMesh.getName(), toMesh.getName(), selectedMeshes.size()); } + /** + * 将 2D 向量 (Vector2f) 乘以 3x3 矩阵 (Matrix3f)。 + * + * @param v 要变换的向量 (局部坐标) + * @param m 变换矩阵 (世界变换) + * @return 变换后的新向量 (世界坐标) + */ + private Vector2f transformVector2f(Vector2f v, Matrix3f m) { + float x = v.x; + float y = v.y; + + // 计算新的 x' 和 y' + // x' = m00*x + m01*y + m02 + // y' = m10*x + m11*y + m12 + float newX = m.m00() * x + m.m01() * y + m.m02(); + float newY = m.m10() * x + m.m11() * y + m.m12(); + + // 直接修改原向量并返回 + return v.set(newX, newY); + } + + /** + * 计算指定网格在世界坐标系下的边界框。 + */ + private BoundingBox getWorldBounds(Mesh2D mesh) { + ModelPart part = findPartByMesh(mesh); + if (part == null) { + mesh.updateBounds(); // 至少保证局部边界框是新的 + return mesh.getBounds(); + } + + // 确保 ModelPart 的世界变换是最新的 + Matrix3f worldTransform = part.getWorldTransform(); + + // 获取局部边界框 + mesh.updateBounds(); + BoundingBox localBounds = mesh.getBounds(); + + // 转换四个角到世界坐标 + Vector2f min = new Vector2f(localBounds.getMinX(), localBounds.getMinY()); + Vector2f max = new Vector2f(localBounds.getMaxX(), localBounds.getMaxY()); + Vector2f p1 = new Vector2f(min.x, max.y); // 左上 + Vector2f p2 = new Vector2f(max.x, min.y); // 右下 + + // 应用世界变换:使用手动实现的 transformVector2f 方法 + transformVector2f(min, worldTransform); + transformVector2f(max, worldTransform); + transformVector2f(p1, worldTransform); + transformVector2f(p2, worldTransform); + + // 创建新的世界边界框并扩展 + BoundingBox worldBounds = new BoundingBox(); + worldBounds.expand(min.x, min.y); + worldBounds.expand(max.x, max.y); + worldBounds.expand(p1.x, p1.y); + worldBounds.expand(p2.x, p2.y); + + return worldBounds; + } + + /** * 检查是否点击了选择框的调整手柄 */ private DragMode checkResizeHandleHit(float modelX, float modelY, Mesh2D targetMesh) { - if (targetMesh == null) return DragMode.NONE; + if (targetMesh == null) { + return DragMode.NONE; + } BoundingBox bounds; Vector2f center; - // 在多选状态下使用多选边界框 + // 多选状态下使用多选边界框 if (targetMesh.isInMultiSelection()) { bounds = targetMesh.getMultiSelectionBounds(); center = bounds.getCenter(); } else { - targetMesh.updateBounds(); bounds = targetMesh.getBounds(); center = targetMesh.getPivot(); } + // 获取摄像机偏移 + Vector2f camOffset = ModelRender.getCameraOffset(); + + // 应用偏移,将 model 坐标转换到相对于摄像机的坐标系 + float checkX = modelX - camOffset.x; + float checkY = modelY - camOffset.y; + + // 将中心点也转换到相同坐标系 + center = new Vector2f(center).sub(camOffset); + + float scaleFactor = calculateScaleFactor(); + float borderThickness = BORDER_THICKNESS / scaleFactor; + float cornerSize = CORNER_SIZE / scaleFactor; + float minX = bounds.getMinX(); float minY = bounds.getMinY(); float maxX = bounds.getMaxX(); float maxY = bounds.getMaxY(); - // 动态计算检测阈值,基于面板缩放比例 - float scaleFactor = calculateScaleFactor(); - float borderThickness = BORDER_THICKNESS / scaleFactor; - float cornerSize = CORNER_SIZE / scaleFactor; + DragMode result = DragMode.NONE; - // 首先检查是否点击了中心点(移动中心点) - if (isPointInCenterHandle(modelX, modelY, center.x, center.y, cornerSize)) { - return DragMode.MOVE_PIVOT; + // 检查中心点 + if (isPointInCenterHandle(checkX, checkY, center.x, center.y, cornerSize)) { + result = DragMode.MOVE_PIVOT; } - // 检查是否点击了旋转手柄 - if (isPointInRotationHandle(modelX, modelY, center.x, center.y, minY, cornerSize)) { - return DragMode.ROTATE; + // 检查旋转手柄 + if (result == DragMode.NONE && isPointInRotationHandle(checkX, checkY, center.x, center.y, minY, cornerSize)) { + result = DragMode.ROTATE; } // 扩展边界以包含调整手柄区域 @@ -967,31 +1261,44 @@ public class ModelRenderPanel extends JPanel { float expandedMaxX = maxX + borderThickness; float expandedMaxY = maxY + borderThickness; - // 检查是否在扩展边界内 - if (modelX < expandedMinX || modelX > expandedMaxX || - modelY < expandedMinY || modelY > expandedMaxY) { - return DragMode.NONE; + if (result == DragMode.NONE) { + if (checkX < expandedMinX || checkX > expandedMaxX || + checkY < expandedMinY || checkY > expandedMaxY) { + return DragMode.NONE; + } } // 检查角点 - if (isPointInCorner(modelX, modelY, minX, minY, cornerSize)) return DragMode.RESIZE_TOP_LEFT; - if (isPointInCorner(modelX, modelY, maxX, minY, cornerSize)) return DragMode.RESIZE_TOP_RIGHT; - if (isPointInCorner(modelX, modelY, minX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_LEFT; - if (isPointInCorner(modelX, modelY, maxX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_RIGHT; + if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, minY, cornerSize)) { + result = DragMode.RESIZE_TOP_LEFT; + } + if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, minY, cornerSize)) { + result = DragMode.RESIZE_TOP_RIGHT; + } + if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, maxY, cornerSize)) { + result = DragMode.RESIZE_BOTTOM_LEFT; + } + if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, maxY, cornerSize)) { + result = DragMode.RESIZE_BOTTOM_RIGHT; + } // 检查边 - if (modelX >= minX - borderThickness && modelX <= minX + borderThickness) - return DragMode.RESIZE_LEFT; - if (modelX >= maxX - borderThickness && modelX <= maxX + borderThickness) - return DragMode.RESIZE_RIGHT; - if (modelY >= minY - borderThickness && modelY <= minY + borderThickness) - return DragMode.RESIZE_TOP; - if (modelY >= maxY - borderThickness && modelY <= maxY + borderThickness) - return DragMode.RESIZE_BOTTOM; + if (result == DragMode.NONE) { + if (checkX >= minX - borderThickness && checkX <= minX + borderThickness) { + result = DragMode.RESIZE_LEFT; + } else if (checkX >= maxX - borderThickness && checkX <= maxX + borderThickness) { + result = DragMode.RESIZE_RIGHT; + } else if (checkY >= minY - borderThickness && checkY <= minY + borderThickness) { + result = DragMode.RESIZE_TOP; + } else if (checkY >= maxY - borderThickness && checkY <= maxY + borderThickness) { + result = DragMode.RESIZE_BOTTOM; + } + } - return DragMode.NONE; + return result; } + /** * 检查点是否在中心点区域内 */ @@ -1045,6 +1352,34 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标拖拽事件 */ private void handleMouseDragged(MouseEvent e) { + if (cameraDragging) { + final int screenX = e.getX(); + final int screenY = e.getY(); + // 计算鼠标移动距离 + final int deltaX = screenX - lastCameraDragX; + final int deltaY = screenY - lastCameraDragY; + + // 更新最后拖拽位置 + lastCameraDragX = screenX; + lastCameraDragY = screenY; + + // 确保在 GL 上下文线程中执行摄像机移动 + executeInGLContext(() -> { + try { + Camera camera = ModelRender.getCamera(); + float zoom = camera.getZoom(); + // 计算世界坐标的移动量(反向移动) + float worldDeltaX = -deltaX / zoom; + float worldDeltaY = deltaY / zoom; // AWT/Swing 的 Y 轴与 OpenGL 相反 + + // 应用摄像机移动 + camera.move(worldDeltaX, worldDeltaY); + } catch (Exception ex) { + logger.error("处理摄像机拖拽时出错", ex); + } + }); + return; + } if (currentDragMode == DragMode.NONE) return; final int screenX = e.getX(); @@ -1241,15 +1576,16 @@ public class ModelRenderPanel extends JPanel { /** * 更新所有选中网格的多选边界框 */ + @Deprecated(forRemoval = true) private void updateMultiSelectionBoundsForSelectedMeshes() { - if (selectedMeshes.size() <= 1) return; - - for (Mesh2D mesh : selectedMeshes) { - if (mesh.isInMultiSelection()) { - mesh.updateBounds(); - mesh.forceUpdateMultiSelectionBounds(); - } - } + //if (selectedMeshes.size() <= 1) return; +// + //for (Mesh2D mesh : selectedMeshes) { + // if (mesh.isInMultiSelection()) { + // mesh.updateBounds(); + // mesh.forceUpdateMultiSelectionBounds(); + // } + //} } @@ -1257,37 +1593,43 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标释放事件(结束拖拽并记录操作历史) */ private void handleMouseReleased(MouseEvent e) { + if (cameraDragging && SwingUtilities.isMiddleMouseButton(e)) { + cameraDragging = false; + // 恢复悬停状态的光标 + updateCursorForHoverState(); + return; + } if (currentDragMode != DragMode.NONE) { // 记录操作历史 //executeInGLContext(() -> { - try { - List selectedParts = getSelectedParts(); - switch (currentDragMode) { - case MOVE: - if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) { - recordDragEnd(selectedParts, new HashMap<>(dragStartPositions)); - } - break; - case ROTATE: - if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) { - recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations)); - } - break; - case MOVE_PIVOT: - if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) { - recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots)); - } - break; - default: - if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) { - recordResizeEnd(selectedParts, new HashMap<>(dragStartScales)); - } - break; - } - } catch (Exception ex) { - logger.error("记录操作历史时出错", ex); + try { + List selectedParts = getSelectedParts(); + switch (currentDragMode) { + case MOVE: + if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) { + recordDragEnd(selectedParts, new HashMap<>(dragStartPositions)); + } + break; + case ROTATE: + if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) { + recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations)); + } + break; + case MOVE_PIVOT: + if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) { + recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots)); + } + break; + default: + if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) { + recordResizeEnd(selectedParts, new HashMap<>(dragStartScales)); + } + break; } + } catch (Exception ex) { + logger.error("记录操作历史时出错", ex); + } //}); } @@ -1302,6 +1644,9 @@ public class ModelRenderPanel extends JPanel { dragStartScales.clear(); dragStartRotations.clear(); dragStartPivots.clear(); + + // 恢复悬停状态的光标 + updateCursorForHoverState(); } /** @@ -1390,6 +1735,10 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); + if (cameraDragging) { + return; + } + // 在 GL 上下文线程中执行悬停检测 executeInGLContext(() -> { try { @@ -1417,45 +1766,116 @@ public class ModelRenderPanel extends JPanel { } } + // 更新鼠标手势 + updateCursorForHoverState(modelX, modelY, newHoveredMesh); + } catch (Exception ex) { logger.error("处理鼠标移动时出错", ex); } }); } + /** + * 根据悬停状态更新光标(无坐标版本,用于鼠标释放后) + */ + private void updateCursorForHoverState() { + Point mousePos = getMousePosition(); + if (mousePos != null) { + float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y); + if (modelCoords != null) { + updateCursorForHoverState(modelCoords[0], modelCoords[1], hoveredMesh); + } + } else { + // 鼠标不在面板内,恢复默认光标 + setCursor(Cursor.getDefaultCursor()); + } + } + + /** + * 根据悬停状态更新光标 + */ + private void updateCursorForHoverState(float modelX, float modelY, Mesh2D hoveredMesh) { + // 如果正在拖拽,不更新光标 + if (currentDragMode != DragMode.NONE) { + return; + } + + Cursor newCursor = Cursor.getDefaultCursor(); + isOverSelection = false; + + // 检查是否在选中的网格上 + if (!selectedMeshes.isEmpty()) { + // 多选时只对最后一个选中的网格进行操作 + Mesh2D targetMeshForHandle = lastSelectedMesh; + if (targetMeshForHandle != null) { + DragMode hoverMode = checkResizeHandleHit(modelX, modelY, targetMeshForHandle); + if (hoverMode != DragMode.NONE) { + newCursor = getCursorForDragMode(hoverMode); + isOverSelection = true; + } else { + // 检查是否在选中网格的边界框内(但不是调整手柄) + BoundingBox bounds; + if (targetMeshForHandle.isInMultiSelection()) { + bounds = targetMeshForHandle.getMultiSelectionBounds(); + } else { + bounds = targetMeshForHandle.getBounds(); + } + + if (modelX >= bounds.getMinX() && modelX <= bounds.getMaxX() && + modelY >= bounds.getMinY() && modelY <= bounds.getMaxY()) { + newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); + isOverSelection = true; + } + } + } + } + + // 如果没有在选中的网格上,检查是否在可悬停的网格上 + if (!isOverSelection && hoveredMesh != null) { + newCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + } + + // 更新光标 + if (!newCursor.equals(currentCursor)) { + currentCursor = newCursor; + Cursor finalNewCursor = newCursor; + SwingUtilities.invokeLater(() -> setCursor(finalNewCursor)); + } + } + /** * 将屏幕坐标转换为模型坐标 */ - private float[] screenToModelCoordinates(int screenX, int screenY) { - if (width <= 0 || height <= 0) return null; + public float[] screenToModelCoordinates(int screenX, int screenY) { + if (!contextInitialized || this.width <= 0 || this.height <= 0) return null; - int panelWidth = getWidth(); - int panelHeight = getHeight(); - if (panelWidth <= 0 || panelHeight <= 0) return null; + // 1. 将 Swing 坐标缩放到 GL 上下文坐标 + float glX = (float) screenX * this.width / getWidth(); + float glY = (float) screenY * this.height / getHeight(); - // 1. 屏幕坐标转换为离屏缓冲坐标 - float scaleX = (float) width / panelWidth; - float scaleY = (float) height / panelHeight; - float bufferX = screenX * scaleX; - float bufferY = screenY * scaleY; // Y轴不反转 + // 2. 转换为归一化设备坐标 (NDC) + // NDC 范围 [-1, 1]。GL 坐标 (0, 0) -> NDC (-1, 1) [左上角] + // 这里的 NDC 转换是基于 GL 上下文的尺寸 (this.width, this.height) + float ndcX = (2.0f * glX) / this.width - 1.0f; + // AWT/Swing 的 Y 轴向下,OpenGL 的 Y 轴向上,所以需要翻转 Y 轴 + float ndcY = 1.0f - (2.0f * glY) / this.height; - // 2. 缓冲坐标转换为标准化设备坐标 (NDC) - float ndcX = (bufferX / width) * 2.0f - 1.0f; - float ndcY = (bufferY / height) * 2.0f - 1.0f; + // 3. 逆投影变换 + Camera camera = ModelRender.getCamera(); + float zoom = camera.getZoom(); + Vector2f pos = camera.getPosition(); - // 3. NDC 转换为模型坐标(考虑当前显示缩放) - float modelX = ndcX * (width / 2.0f) / displayScale; - float modelY = ndcY * (height / 2.0f) / displayScale; + // 逆变换公式: + // modelX = (ndcX / (2.0f / this.width)) / zoom + pos.x + // modelY = (ndcY / (-2.0f / this.height)) / zoom + pos.y - float[] result = new float[]{modelX, modelY}; + float modelX = (ndcX * this.width / (2.0f * zoom)) + pos.x; + float modelY = (ndcY * this.height / (-2.0f * zoom)) + pos.y; - // 调试日志 - logger.debug("坐标转换: 屏幕({}, {}) -> 缓冲({}, {}) -> NDC({}, {}) -> 模型({}, {}), 缩放: {}", - screenX, screenY, bufferX, bufferY, ndcX, ndcY, modelX, modelY, displayScale); - - return result; + return new float[]{modelX, modelY}; } + /** * 在指定位置查找网格 */ @@ -1467,40 +1887,54 @@ public class ModelRenderPanel extends JPanel { } try { - // 使用 getParts() 获取所有部件 + // 获取摄像机偏移 + Vector2f camOffset = ModelRender.getCameraOffset(); + + // 将输入坐标调整到相对于摄像机的坐标系 + float checkX = modelX - camOffset.x; + float checkY = modelY - camOffset.y; + java.util.List parts = model.getParts(); if (parts == null || parts.isEmpty()) { - logger.debug("模型没有部件列表"); return null; } + + // 遍历所有部件和网格(从上到下) for (int i = parts.size() - 1; i >= 0; i--) { ModelPart part = parts.get(i); - if (part != null && part.isVisible()) { - java.util.List meshes = part.getMeshes(); - for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.isVisible()) { - if (mesh.isDirty()) { - mesh.updateBounds(); - } - boolean contains = mesh.containsPoint(modelX, modelY); - if (contains) { - //logger.info("选中网格: {} (在部件 {})", mesh.getName(), part.getName()); - return mesh; - } - } + if (part == null || !part.isVisible()) continue; + + java.util.List meshes = part.getMeshes(); + if (meshes == null || meshes.isEmpty()) continue; + + for (int m = meshes.size() - 1; m >= 0; m--) { + Mesh2D mesh = meshes.get(m); + if (mesh == null || !mesh.isVisible()) continue; + + if (mesh.isDirty()) { + mesh.updateBounds(); + } + + boolean contains = false; + try { + contains = mesh.containsPoint(checkX, checkY); + } catch (Exception ex) { + logger.warn("mesh.containsPoint 抛出异常: {}", ex.getMessage()); + } + + if (contains) { + return mesh; } } } - - //logger.debug("未找到包含点的网格"); return null; - } catch (Exception e) { logger.error("检测网格时出错", e); return null; } } + /** * 获取模型的边界框 */ @@ -2013,7 +2447,7 @@ public class ModelRenderPanel extends JPanel { */ public void dispose() { running = false; - + cameraDragging = false; // 停止任务执行器 taskExecutor.shutdown(); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index e453391..6d27edc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -714,7 +714,7 @@ public class ModelPart { /** * 立即重新计算本节点的 worldTransform(并递归到子节点) */ - private void recomputeWorldTransformRecursive() { + public void recomputeWorldTransformRecursive() { if (transformDirty) { updateLocalTransform(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index ddbd818..4390f6c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.model.util; +import com.chuangzhou.vivid2D.render.systems.MultiSelectionBoxRenderer; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; @@ -417,17 +418,17 @@ public class Mesh2D { * 检查点是否在网格内(可选择精确检测) */ public boolean containsPoint(float x, float y, boolean precise) { - if (isInMultiSelection()) { - BoundingBox multiBounds = getMultiSelectionBounds(); - boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && - y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); - - if (precise && inBounds) { - // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 - return isPointInAnySelectedMesh(x, y); - } - return inBounds; - } + //if (isInMultiSelection()) { + // BoundingBox multiBounds = getMultiSelectionBounds(); + // boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && + // y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); +// + // if (precise && inBounds) { + // // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 + // return isPointInAnySelectedMesh(x, y); + // } + // return inBounds; + //} BoundingBox b = getBounds(); boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); @@ -757,60 +758,9 @@ public class Mesh2D { } } - private void drawSelectBox(){ + private void drawSelectBox() { BoundingBox bounds = getBounds(); - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - - BufferBuilder bb = new BufferBuilder(); - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - final float CORNER_SIZE = 8.0f; - final float BORDER_THICKNESS = 6.0f; - - float expand = 4.0f * 2.0f; - - // 第1层:外发光边框 - bb.begin(RenderSystem.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 0.4f)); - - bb.vertex(minX - expand, minY - expand, 0.0f, 0.0f); - bb.vertex(maxX + expand, minY - expand, 0.0f, 0.0f); - bb.vertex(maxX + expand, maxY + expand, 0.0f, 0.0f); - bb.vertex(minX - expand, maxY + expand, 0.0f, 0.0f); - bb.endImmediate(); - - // 第2层:主边框(实心粗边框)- 使用明亮的青色 - bb.begin(RenderSystem.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 1.0f)); - - float mainExpand = 1.0f; - bb.vertex(minX - mainExpand, minY - mainExpand, 0.0f, 0.0f); - bb.vertex(maxX + mainExpand, minY - mainExpand, 0.0f, 0.0f); - bb.vertex(maxX + mainExpand, maxY + mainExpand, 0.0f, 0.0f); - bb.vertex(minX - mainExpand, maxY + mainExpand, 0.0f, 0.0f); - bb.endImmediate(); - - // 第3层:内边框 - 使用白色增加对比度 - bb.begin(RenderSystem.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - - bb.vertex(minX, minY, 0.0f, 0.0f); - bb.vertex(maxX, minY, 0.0f, 0.0f); - bb.vertex(maxX, maxY, 0.0f, 0.0f); - bb.vertex(minX, maxY, 0.0f, 0.0f); - bb.endImmediate(); - - // 第4层:绘制角点标记和边线 - drawResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS); - - // 新增:绘制中心点 - drawCenterPoint(bb, minX, minY, maxX, maxY); - drawRotationHandle(bb, minX, minY, maxX, maxY); + MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot); } /** @@ -922,25 +872,8 @@ public class Mesh2D { * 在多选状态下绘制组合边界框 */ private void drawMultiSelectionBox() { - if (!isInMultiSelection()) { - drawSelectBox(); - return; - } BoundingBox multiBounds = getMultiSelectionBounds(); - if (!multiBounds.isValid()) return; - float minX = multiBounds.getMinX(); - float minY = multiBounds.getMinY(); - float maxX = multiBounds.getMaxX(); - float maxY = multiBounds.getMaxY(); - BufferBuilder bb = new BufferBuilder(); - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - drawDashedBorder(bb, minX, minY, maxX, maxY); - final float CORNER_SIZE = 8.0f; - final float BORDER_THICKNESS = 6.0f; - drawMultiSelectionResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS); - drawMultiSelectionCenterPoint(bb, minX, minY, maxX, maxY); - drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY); + MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds); } /** @@ -959,11 +892,7 @@ public class Mesh2D { // 绘制右边虚线 drawDashedLine(bb, maxX, minY, maxX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - - // 绘制下边虚线 drawDashedLine(bb, maxX, maxY, minX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - - // 绘制左边虚线 drawDashedLine(bb, minX, maxY, minX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java new file mode 100644 index 0000000..06a9b2e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java @@ -0,0 +1,67 @@ +package com.chuangzhou.vivid2D.render.systems; + +import org.joml.Vector2f; + +/** + * 摄像机类 + * @author tzdwindows 7 + */ +public class Camera { + private final Vector2f position = new Vector2f(0.0f, 0.0f); + private float zoom = 1.0f; + private float zPosition = 0.0f; // Z轴位置,影响深度 + private boolean enabled = true; + + public Camera() {} + + public void setPosition(float x, float y) { + position.set(x, y); + } + + public void setPosition(Vector2f pos) { + position.set(pos); + } + + public Vector2f getPosition() { + return new Vector2f(position); + } + + public void setZoom(float zoom) { + this.zoom = Math.max(0.1f, Math.min(10.0f, zoom)); + } + + public float getZoom() { + return zoom; + } + + public void setZPosition(float z) { + this.zPosition = z; + } + + public float getZPosition() { + return zPosition; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public void move(float dx, float dy) { + position.add(dx, dy); + } + + public void zoom(float factor) { + zoom *= factor; + zoom = Math.max(0.1f, Math.min(10.0f, zoom)); + } + + public void reset() { + position.set(0.0f, 0.0f); + zoom = 1.0f; + zPosition = 0.0f; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java new file mode 100644 index 0000000..349a2a1 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java @@ -0,0 +1,335 @@ +package com.chuangzhou.vivid2D.render.systems; + +import com.chuangzhou.vivid2D.render.model.util.BoundingBox; +import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; +import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; +import org.joml.Vector2f; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL11; + +/** + * 现代化选择框渲染器(性能优化版) + * 主要优化点: + * 1) 复用 Tesselator 单例 BufferBuilder,减少频繁的 GPU 资源创建/销毁 + * 2) 批量提交顶点:把同一 primitive(LINES / TRIANGLES / LINE_LOOP)与同一颜色的顶点尽量合并到一次 begin/end + * 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效 + * 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观 + * + * 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。 + */ +public class MultiSelectionBoxRenderer { + + // 常量定义(视觉可调) + public static final float DEFAULT_CORNER_SIZE = 10.0f; + public static final float DEFAULT_BORDER_THICKNESS = 6.0f; + public static final float DEFAULT_DASH_LENGTH = 10.0f; + public static final float DEFAULT_GAP_LENGTH = 6.0f; + public static final float ROTATION_HANDLE_DISTANCE = 28.0f; + public static final float HANDLE_ROUNDNESS = 1.5f; // 保留,用于未来改进圆角手柄 + + // 颜色(更现代的配色) + public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线 + public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.18f); // 轻微外发光 + public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框,青色 + public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内边框,接近白 + public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白 + 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); // 中心点红 + public static final Vector4f ROTATION_HANDLE_COLOR = new Vector4f(0.14f, 0.95f, 0.3f, 1.0f); // 绿色旋转手柄 + public static final Vector4f SHADOW_COLOR = new Vector4f(0f, 0f, 0f, 0.18f); // 阴影/背板 + + /** + * 绘制单选状态下的选择框(高效批处理) + */ + public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) { + if (!bounds.isValid()) return; + + float minX = bounds.getMinX(); + float minY = bounds.getMinY(); + float maxX = bounds.getMaxX(); + float maxY = bounds.getMaxY(); + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bb = tesselator.getBuilder(); + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // 1) 阴影底板(轻微偏移) + bb.begin(RenderSystem.GL_TRIANGLES, 6); + bb.setColor(SHADOW_COLOR); + addFilledQuadTriangles(bb, minX + 4f, minY + 4f, maxX + 4f, maxY + 4f); + tesselator.end(); + + // 2) 外发光边框(更柔和) + bb.begin(RenderSystem.GL_LINE_LOOP, 4); + bb.setColor(SOLID_BORDER_COLOR_OUTER); + bb.vertex(minX - 6.0f, minY - 6.0f, 0.0f, 0.0f); + bb.vertex(maxX + 6.0f, minY - 6.0f, 0.0f, 0.0f); + bb.vertex(maxX + 6.0f, maxY + 6.0f, 0.0f, 0.0f); + bb.vertex(minX - 6.0f, maxY + 6.0f, 0.0f, 0.0f); + tesselator.end(); + + // 3) 主边框 + 内边框(两个 LINE_LOOP) + bb.begin(RenderSystem.GL_LINE_LOOP, 4); + bb.setColor(SOLID_BORDER_COLOR_MAIN); + bb.vertex(minX - 1.0f, minY - 1.0f, 0.0f, 0.0f); + bb.vertex(maxX + 1.0f, minY - 1.0f, 0.0f, 0.0f); + bb.vertex(maxX + 1.0f, maxY + 1.0f, 0.0f, 0.0f); + bb.vertex(minX - 1.0f, maxY + 1.0f, 0.0f, 0.0f); + tesselator.end(); + + bb.begin(RenderSystem.GL_LINE_LOOP, 4); + bb.setColor(SOLID_BORDER_COLOR_INNER); + bb.vertex(minX, minY, 0.0f, 0.0f); + bb.vertex(maxX, minY, 0.0f, 0.0f); + bb.vertex(maxX, maxY, 0.0f, 0.0f); + bb.vertex(minX, maxY, 0.0f, 0.0f); + tesselator.end(); + + // 4) 手柄(一次性 TRIANGLES 批次绘制所有手柄) + // 8 个手柄(四角 + 四边中点),每个 6 个顶点 + bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8); + bb.setColor(HANDLE_COLOR); + addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); // 左上 + addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); // 右上 + addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); // 左下 + addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); // 右下 + + addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); // 上中 + addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); // 下中 + addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 左中 + addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 右中 + tesselator.end(); + + // 5) 中心点(十字 + 圆环) + // 十字:LINES + bb.begin(GL11.GL_LINES, 4); + bb.setColor(CENTER_POINT_COLOR); + bb.vertex(pivot.x - 6.0f, pivot.y, 0.0f, 0.0f); + bb.vertex(pivot.x + 6.0f, pivot.y, 0.0f, 0.0f); + bb.vertex(pivot.x, pivot.y - 6.0f, 0.0f, 0.0f); + bb.vertex(pivot.x, pivot.y + 6.0f, 0.0f, 0.0f); + tesselator.end(); + + // 圆环:LINE_LOOP + bb.begin(RenderSystem.GL_LINE_LOOP, 16); + bb.setColor(CENTER_POINT_COLOR); + float radius = 6.0f * 0.85f; + for (int i = 0; i < 16; i++) { + float angle = (float) (i * 2f * Math.PI / 16f); + bb.vertex(pivot.x + (float) Math.cos(angle) * radius, pivot.y + (float) Math.sin(angle) * radius, 0.0f, 0.0f); + } + tesselator.end(); + + // 6) 旋转手柄(连线 + 圆 + 箭头),分三次提交但数量小 + float topY = bounds.getMinY(); + float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE; + + // 连线 + bb.begin(GL11.GL_LINES, 2); + bb.setColor(ROTATION_HANDLE_COLOR); + bb.vertex(pivot.x, topY, 0.0f, 0.0f); + bb.vertex(pivot.x, rotationHandleY, 0.0f, 0.0f); + tesselator.end(); + + // 圆 + bb.begin(RenderSystem.GL_LINE_LOOP, 16); + bb.setColor(ROTATION_HANDLE_COLOR); + float handleRadius = 6.0f; + for (int i = 0; i < 16; i++) { + float angle = (float) (i * 2f * Math.PI / 16f); + bb.vertex(pivot.x + (float) Math.cos(angle) * handleRadius, rotationHandleY + (float) Math.sin(angle) * handleRadius, 0.0f, 0.0f); + } + tesselator.end(); + + // 箭头(两条交叉线,提示旋转) + bb.begin(GL11.GL_LINES, 4); + bb.setColor(ROTATION_HANDLE_COLOR); + float arrow = 4.0f; + bb.vertex(pivot.x - arrow, rotationHandleY - arrow, 0.0f, 0.0f); + bb.vertex(pivot.x + arrow, rotationHandleY + arrow, 0.0f, 0.0f); + bb.vertex(pivot.x + arrow, rotationHandleY - arrow, 0.0f, 0.0f); + bb.vertex(pivot.x - arrow, rotationHandleY + arrow, 0.0f, 0.0f); + tesselator.end(); + } + + /** + * 绘制多选框(现代化外观,批量提交) + */ + public static void drawMultiSelectionBox(BoundingBox multiBounds) { + if (!multiBounds.isValid()) return; + + float minX = multiBounds.getMinX(); + float minY = multiBounds.getMinY(); + float maxX = multiBounds.getMaxX(); + float maxY = multiBounds.getMaxY(); + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bb = tesselator.getBuilder(); + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次 + int estimatedSegments = Math.max(4, + (int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); + bb.begin(GL11.GL_LINES, estimatedSegments * 2); + bb.setColor(DASHED_BORDER_COLOR); + 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(); + + // 手柄(一次性 TRIANGLES 批次) + bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8); + bb.setColor(MULTI_SELECTION_HANDLE_COLOR); + addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); + addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); + addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); + addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); + addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); + addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); + addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); + addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); + tesselator.end(); + + // 中心点 + Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f); + bb.begin(GL11.GL_LINES, 4); + bb.setColor(CENTER_POINT_COLOR); + bb.vertex(center.x - 6.0f, center.y, 0.0f, 0.0f); + bb.vertex(center.x + 6.0f, center.y, 0.0f, 0.0f); + bb.vertex(center.x, center.y - 6.0f, 0.0f, 0.0f); + bb.vertex(center.x, center.y + 6.0f, 0.0f, 0.0f); + tesselator.end(); + + // 旋转手柄(沿用单选逻辑) + drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY); + } + + // ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder) ------ + + // 向当前 TRIANGLES 批次添加一个填充矩形(两三角形) + private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) { + // 三角形 1 + bb.vertex(x0, y0, 0.0f, 0.0f); + bb.vertex(x1, y0, 0.0f, 0.0f); + bb.vertex(x1, y1, 0.0f, 0.0f); + // 三角形 2 + bb.vertex(x1, y1, 0.0f, 0.0f); + bb.vertex(x0, y1, 0.0f, 0.0f); + bb.vertex(x0, y0, 0.0f, 0.0f); + } + + // 向当前 TRIANGLES 批次添加一个手柄方块(中心在 cx,cy,边长 size) + private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) { + float half = size / 2f; + addFilledQuadTriangles(bb, cx - half, cy - half, cx + half, cy + half); + } + + // 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin()) + private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY, + float dashLen, float gapLen) { + float dx = endX - startX; + float dy = endY - startY; + float len = (float) Math.sqrt(dx * dx + dy * dy); + if (len < 0.001f) return; + float dirX = dx / len, dirY = dy / len; + float seg = dashLen + gapLen; + int count = (int) Math.ceil(len / seg); + for (int i = 0; i < count; i++) { + float s = i * seg; + if (s >= len) break; + float e = Math.min(s + dashLen, len); + float sx = startX + dirX * s; + float sy = startY + dirY * s; + float ex = startX + dirX * e; + float ey = startY + dirY * e; + bb.vertex(sx, sy, 0.0f, 0.0f); + bb.vertex(ex, ey, 0.0f, 0.0f); + } + } + + // 适配:在 multi selection 中把旋转手柄渲染写入到传入的 bb(会在函数内部使用 tesselator.end()) + public static void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f); + drawRotationHandle(center, new BoundingBox(minX, minY, maxX, maxY)); + } + + // 单独绘制旋转手柄(内部会 new / begin / end,因为包含多种 primitive) + private static void drawRotationHandle(Vector2f pivot, BoundingBox bounds) { + float centerX = pivot.x; + float centerY = pivot.y; + float topY = bounds.getMinY(); + + boolean pivotInBounds = (centerX >= bounds.getMinX() && centerX <= bounds.getMaxX() && + centerY >= bounds.getMinY() && centerY <= bounds.getMaxY()); + if (!pivotInBounds) { + centerX = (bounds.getMinX() + bounds.getMaxX()) * 0.5f; + centerY = (bounds.getMinY() + bounds.getMaxY()) * 0.5f; + topY = bounds.getMinY(); + } + + float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE; + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + + // 连线 + bb.begin(GL11.GL_LINES, 2); + bb.setColor(ROTATION_HANDLE_COLOR); + bb.vertex(centerX, topY, 0.0f, 0.0f); + bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f); + t.end(); + + // 圆环 + bb.begin(RenderSystem.GL_LINE_LOOP, 16); + bb.setColor(ROTATION_HANDLE_COLOR); + float r = 6.0f; + for (int i = 0; i < 16; i++) { + float ang = (float) (i * 2f * Math.PI / 16f); + bb.vertex(centerX + (float) Math.cos(ang) * r, rotationHandleY + (float) Math.sin(ang) * r, 0.0f, 0.0f); + } + t.end(); + + // 箭头 + bb.begin(GL11.GL_LINES, 4); + bb.setColor(ROTATION_HANDLE_COLOR); + float arrow = 4.0f; + bb.vertex(centerX - arrow, rotationHandleY - arrow, 0.0f, 0.0f); + bb.vertex(centerX + arrow, rotationHandleY + arrow, 0.0f, 0.0f); + bb.vertex(centerX + arrow, rotationHandleY - arrow, 0.0f, 0.0f); + bb.vertex(centerX - arrow, rotationHandleY + arrow, 0.0f, 0.0f); + t.end(); + } + + /** + * 仅绘制简化的多选虚线边框(保留单次批量绘制) + */ + public static void drawSimpleMultiSelectionBox(BoundingBox multiBounds) { + if (!multiBounds.isValid()) return; + + float minX = multiBounds.getMinX(); + float minY = multiBounds.getMinY(); + float maxX = multiBounds.getMaxX(); + float maxY = multiBounds.getMaxY(); + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bb = tesselator.getBuilder(); + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + int est = Math.max(4, + (int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); + bb.begin(GL11.GL_LINES, est * 2); + bb.setColor(DASHED_BORDER_COLOR); + 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(); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java index 178dbc1..36ef84f 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java @@ -4,6 +4,12 @@ import org.lwjgl.opengl.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.IntSupplier; @@ -101,7 +107,182 @@ public final class RenderSystem { public static final int GL_COMPILE_STATUS = org.lwjgl.opengl.GL20.GL_COMPILE_STATUS; public static final int GL_LINK_STATUS = org.lwjgl.opengl.GL20.GL_LINK_STATUS; public static final int GL_VALIDATE_STATUS = org.lwjgl.opengl.GL20.GL_VALIDATE_STATUS; + private static final java.util.Deque stateStack = new java.util.ArrayDeque<>(); + /** + * 渲染状态快照类 + */ + private static class RenderState { + private int currentProgram; + private boolean blendEnabled; + private boolean depthTestEnabled; + private int blendSrcFactor; + private int blendDstFactor; + private int activeTexture; + private int boundTexture; + private float[] clearColor; + private int[] viewport; + + public RenderState() { + setDefaults(); + + try { + this.currentProgram = getCurrentProgram(); + this.blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND); + this.depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST); + + java.nio.IntBuffer blendFunc = org.lwjgl.system.MemoryUtil.memAllocInt(2); + try { + GL11.glGetIntegerv(GL11.GL_BLEND_SRC, blendFunc); + this.blendSrcFactor = blendFunc.get(0); + GL11.glGetIntegerv(GL11.GL_BLEND_DST, blendFunc); + this.blendDstFactor = blendFunc.get(0); + } finally { + org.lwjgl.system.MemoryUtil.memFree(blendFunc); + } + + java.nio.IntBuffer intBuf = org.lwjgl.system.MemoryUtil.memAllocInt(1); + try { + GL11.glGetIntegerv(GL13.GL_ACTIVE_TEXTURE, intBuf); + this.activeTexture = intBuf.get(0); + + GL11.glGetIntegerv(GL11.GL_TEXTURE_BINDING_2D, intBuf); + this.boundTexture = intBuf.get(0); + } finally { + org.lwjgl.system.MemoryUtil.memFree(intBuf); + } + + java.nio.FloatBuffer floatBuf = org.lwjgl.system.MemoryUtil.memAllocFloat(4); + try { + GL11.glGetFloatv(GL11.GL_COLOR_CLEAR_VALUE, floatBuf); + this.clearColor = new float[] { + floatBuf.get(0), floatBuf.get(1), + floatBuf.get(2), floatBuf.get(3) + }; + } finally { + org.lwjgl.system.MemoryUtil.memFree(floatBuf); + } + + java.nio.IntBuffer viewportBuf = org.lwjgl.system.MemoryUtil.memAllocInt(4); + try { + GL11.glGetIntegerv(GL11.GL_VIEWPORT, viewportBuf); + this.viewport = new int[] { + viewportBuf.get(0), viewportBuf.get(1), + viewportBuf.get(2), viewportBuf.get(3) + }; + } finally { + org.lwjgl.system.MemoryUtil.memFree(viewportBuf); + } + } catch (Exception e) { + logger.warn("Failed to get render state, using defaults: {}", e.getMessage()); + // 如果出现异常,我们使用默认值(已经在setDefaults中设置,所以不需要再次设置) + } + } + + private void setDefaults() { + this.currentProgram = 0; + this.blendEnabled = false; + this.depthTestEnabled = false; + this.blendSrcFactor = GL11.GL_SRC_ALPHA; + this.blendDstFactor = GL11.GL_ONE_MINUS_SRC_ALPHA; + this.activeTexture = GL13.GL_TEXTURE0; + this.boundTexture = 0; + this.clearColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f}; + this.viewport = new int[] {0, 0, viewportWidth, viewportHeight}; + } + + public void restore() { + try { + // 恢复视口 + if (viewport != null && viewport.length == 4) { + GL11.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); + } + + // 恢复清除颜色 + if (clearColor != null && clearColor.length == 4) { + GL11.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + } + + // 恢复着色器程序 + if (GL20.glIsProgram(currentProgram)) { + GL20.glUseProgram(currentProgram); + } else { + GL20.glUseProgram(0); + } + + // 恢复纹理状态 - 使用更安全的方式 + if (activeTexture >= GL13.GL_TEXTURE0 && activeTexture <= GL13.GL_TEXTURE31) { + GL13.glActiveTexture(activeTexture); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture); + } else { + // 使用默认纹理单元 + GL13.glActiveTexture(GL13.GL_TEXTURE0); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture); + } + + // 恢复混合状态 + if (blendEnabled) { + GL11.glEnable(GL11.GL_BLEND); + } else { + GL11.glDisable(GL11.GL_BLEND); + } + + // 使用安全的混合函数值 + if (isValidBlendFunc(blendSrcFactor) && isValidBlendFunc(blendDstFactor)) { + GL11.glBlendFunc(blendSrcFactor, blendDstFactor); + } else { + // 使用默认混合函数 + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + } + + // 恢复深度测试状态 + if (depthTestEnabled) { + GL11.glEnable(GL11.GL_DEPTH_TEST); + } else { + GL11.glDisable(GL11.GL_DEPTH_TEST); + } + + } catch (Exception e) { + logger.error("Error during state restoration: {}", e.getMessage()); + } + } + + /** + * 检查混合函数值是否有效 + */ + private boolean isValidBlendFunc(int func) { + switch (func) { + case GL11.GL_ZERO: + case GL11.GL_ONE: + case GL11.GL_SRC_COLOR: + case GL11.GL_ONE_MINUS_SRC_COLOR: + case GL11.GL_DST_COLOR: + case GL11.GL_ONE_MINUS_DST_COLOR: + case GL11.GL_SRC_ALPHA: + case GL11.GL_ONE_MINUS_SRC_ALPHA: + case GL11.GL_DST_ALPHA: + case GL11.GL_ONE_MINUS_DST_ALPHA: + case GL14.GL_SRC_ALPHA_SATURATE: + return true; + default: + return false; + } + } + + @Override + public String toString() { + return "RenderState{" + + "currentProgram=" + currentProgram + + ", blendEnabled=" + blendEnabled + + ", depthTestEnabled=" + depthTestEnabled + + ", blendSrcFactor=" + blendSrcFactor + + ", blendDstFactor=" + blendDstFactor + + ", activeTexture=" + activeTexture + + ", boundTexture=" + boundTexture + + ", clearColor=" + Arrays.toString(clearColor) + + ", viewport=" + Arrays.toString(viewport); + } + } // ================== 初始化方法 ================== public static void initRenderThread() { @@ -175,6 +356,52 @@ public final class RenderSystem { } } + /** + * 保存当前渲染状态到栈中 + */ + public static void pushState() { + if (!isOnRenderThread()) { + recordRenderCall(() -> _pushState()); + } else { + _pushState(); + } + } + + private static void _pushState() { + assertOnRenderThread(); + stateStack.push(new RenderState()); + checkGLError("pushState"); + } + + /** + * 从栈中恢复之前的渲染状态 + */ + public static void popState() { + if (!isOnRenderThread()) { + recordRenderCall(() -> _popState()); + } else { + _popState(); + } + } + + private static void _popState() { + assertOnRenderThread(); + if (!stateStack.isEmpty()) { + RenderState state = stateStack.pop(); + state.restore(); + checkGLError("popState"); + } else { + logger.warn("popState called with empty state stack"); + } + } + + /** + * 获取当前状态栈大小 + */ + public static int getStateStackSize() { + return stateStack.size(); + } + private static void _enable(int capability) { assertOnRenderThread(); GL11.glEnable(capability); @@ -602,6 +829,18 @@ public final class RenderSystem { } } + public static ByteBuffer loadWindowsFont(String fontFileName) throws IOException { + Path path = Path.of("C:/Windows/Fonts/" + fontFileName); + try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) { + ByteBuffer buffer = ByteBuffer.allocateDirect((int) fc.size()); + while (buffer.hasRemaining()) { + fc.read(buffer); + } + buffer.flip(); + return buffer; + } + } + // 完整的程序链接方法 public static int linkProgram(int vertexShader, int fragmentShader) { assertOnRenderThread(); @@ -914,29 +1153,52 @@ public final class RenderSystem { assertOnRenderThread(); int textureId = genTextures(); - bindTexture(textureId); - - // 创建 1x1 白色纹理 - java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4); - try { - buffer.put((byte) 255) - .put((byte) 255) - .put((byte) 255) - .put((byte) 255) - .flip(); - - texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); - } finally { - org.lwjgl.system.MemoryUtil.memFree(buffer); + if (textureId == 0) { + logger.error("Failed to generate texture ID"); + return 0; } - // 设置纹理参数 - setTextureMinFilter(GL11.GL_NEAREST); - setTextureMagFilter(GL11.GL_NEAREST); - setTextureWrapS(GL11.GL_REPEAT); - setTextureWrapT(GL11.GL_REPEAT); + bindTexture(textureId); + + try { + // 创建 1x1 白色纹理 - 使用更兼容的格式 + java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4); + try { + // 填充 RGBA 数据:白色不透明 + buffer.put((byte) 0xFF) // R + .put((byte) 0xFF) // G + .put((byte) 0xFF) // B + .put((byte) 0xFF) // A + .flip(); + + // 使用更兼容的纹理格式组合 + // 注意:有些系统可能不支持 GL_RGBA8,使用 GL_RGBA 作为内部格式 + texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, + 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); + + checkGLError("texImage2D in createDefaultTexture"); + + } finally { + org.lwjgl.system.MemoryUtil.memFree(buffer); + } + + // 设置纹理参数 + setTextureMinFilter(GL11.GL_NEAREST); + setTextureMagFilter(GL11.GL_NEAREST); + setTextureWrapS(GL12.GL_CLAMP_TO_EDGE); + setTextureWrapT(GL12.GL_CLAMP_TO_EDGE); + + checkGLError("texture parameters in createDefaultTexture"); + + } catch (Exception e) { + logger.error("Error creating default texture: {}", e.getMessage()); + // 清理失败的纹理 + deleteTextures(textureId); + return 0; + } finally { + bindTexture(0); // 解绑 + } - bindTexture(0); // 解绑 return textureId; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java index d9827ae..fbcb9c9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java @@ -3,6 +3,7 @@ package com.chuangzhou.vivid2D.render.systems.sources; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.render.systems.sources.def.Shader2D; import com.chuangzhou.vivid2D.render.systems.sources.def.SolidColorShader; +import com.chuangzhou.vivid2D.render.systems.sources.def.TextShader; import org.joml.Vector3f; import org.joml.Vector4f; import org.lwjgl.opengl.GL20; @@ -37,7 +38,8 @@ public class ShaderManagement { */ public static final List shaderList = List.of( new Shader2D(), - new SolidColorShader() + new SolidColorShader(), + new TextShader() ); /** diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java new file mode 100644 index 0000000..03b933d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java @@ -0,0 +1,96 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.*; + +import org.joml.Vector4f; + +/** + * 文本着色器 + * @author tzdwindows 7 + */ +public class TextShader implements CompleteShader { + + private final VertexShader vertexShader = new VertexShader(); + private final FragmentShader fragmentShader = new FragmentShader(); + private Vector4f color = new Vector4f(1,1,1,1); + + public void setColor(Vector4f color) { + this.color.set(color); + } + + @Override + public Shader getVertexShader() { + return vertexShader; + } + + @Override + public Shader getFragmentShader() { + return fragmentShader; + } + + @Override + public String getShaderName() { + return "TextShader"; + } + + @Override + public boolean isDefaultShader() { + return false; + } + + @Override + public void setDefaultUniforms(ShaderProgram program) { + // 传递颜色 uniform + program.setUniform4f("uColor", color.x, color.y, color.z, color.w); + // 纹理通常绑定到0号纹理单元 + program.setUniform1i("uTexture", 0); + } + + private static class VertexShader implements Shader { + @Override + public String getShaderCode() { + return """ + #version 330 core + layout(location = 0) in vec2 aPosition; + layout(location = 1) in vec2 aTexCoord; + + out vec2 vTexCoord; + + void main() { + vTexCoord = aTexCoord; + gl_Position = vec4(aPosition.xy, 0.0, 1.0); + } + """; + } + + @Override + public String getShaderName() { + return "TextVertexShader"; + } + } + + private static class FragmentShader implements Shader { + @Override + public String getShaderCode() { + return """ + #version 330 core + in vec2 vTexCoord; + out vec4 FragColor; + + uniform sampler2D uTexture; + uniform vec4 uColor; + + void main() { + // 使用 .r 通道读取单通道纹理 + float alpha = texture(uTexture, vTexCoord).r; + FragColor = vec4(uColor.rgb, uColor.a * alpha); + } + """; + } + + @Override + public String getShaderName() { + return "TextFragmentShader"; + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java index cd53784..581d2fe 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java @@ -37,13 +37,15 @@ public class ModelTest { // Test 3: Test compressed file operations with textures testCompressedFileOperationsWithTexture(); + //testModelSaveLoadIntegrity(model, "test_model.vmdl") + // Other existing tests... - testAnimationSystem(); - testPhysicsSystem(); - testComplexTransformations(); - testPerformance(); - Model2D model = createTestModel(); - printModelState(model); + //testAnimationSystem(); + //testPhysicsSystem(); + //testComplexTransformations(); + //testPerformance(); + //Model2D model = createTestModel(); + //printModelState(model); } finally { // Cleanup OpenGL cleanupOpenGL(); @@ -311,7 +313,6 @@ public class ModelTest { System.out.println("OpenGL initialized successfully"); System.out.println("OpenGL Version: " + org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION)); glInitialized = true; - } catch (Exception e) { System.err.println("Failed to initialize OpenGL: " + e.getMessage()); // Continue without OpenGL for other tests @@ -581,7 +582,6 @@ public class ModelTest { try { // Load model Model2D model = Model2D.loadFromFile("test_character.model"); - System.out.println("Testing animation system:"); // Test parameter-driven animation