From 71aa2b8699924b8a5a76fb21483dd69f28063a91 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sun, 26 Oct 2025 10:57:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E5=AE=9E=E7=8E=B0=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=9A=84=20OpenGL=20=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 GL 上下文管理从 ModelRenderPanel 抽离到独立的 GLContextManager 类- 实现离屏渲染上下文的创建、初始化和资源管理 - 支持动态调整渲染缓冲区大小和缩放功能 - 提供线程安全的任务队列机制用于在 GL 线程执行操作 - 实现像素数据读取和转换为 BufferedImage 的完整流程- 添加摄像机拖拽状态和缩放控制的支持 -重构 ModelRenderPanel以使用新的 GLContextManager- 更新所有 GL 相关操作的调用方式指向新的上下文管理器 - 修改 dispose 流程以正确释放所有 OpenGL 资源 - 优化渲染循环和平滑缩放逻辑实现 --- .../vivid2D/render/awt/ModelLayerPanel.java | 10 +- .../vivid2D/render/awt/ModelRenderPanel.java | 710 ++---------------- .../vivid2D/render/awt/TransformPanel.java | 12 +- .../render/awt/manager/GLContextManager.java | 586 +++++++++++++++ .../vivid2D/test/ModelLayerPanelTest.java | 4 +- .../vivid2D/test/TestModelGLPanel.java | 4 +- 6 files changed, 679 insertions(+), 647 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java index 2143bb9..2998e25 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -127,7 +127,7 @@ public class ModelLayerPanel extends JPanel { // 使用更可靠的方式在GL上下文中创建纹理 try { // 在GL上下文中同步执行所有图层的创建 - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { try { List createdParts = new ArrayList<>(); @@ -665,7 +665,7 @@ public class ModelLayerPanel extends JPanel { if (renderPanel != null) { final String texName = name + "_tex"; final String filePath = f.getAbsolutePath(); - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { try { Texture texture = Texture.createFromFile(texName, filePath); if (texture != null) { @@ -730,7 +730,7 @@ public class ModelLayerPanel extends JPanel { part.addMesh(mesh); if (renderPanel != null) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { try { Texture tex = createTextureFromBufferedImageInGL(img, name + "_tex"); if (tex != null) { @@ -799,7 +799,7 @@ public class ModelLayerPanel extends JPanel { final String texName = sel.getName() + "_tex"; if (renderPanel != null) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { try { Texture texture = Texture.createFromFile(texName, filePath); if (texture != null) { @@ -1087,7 +1087,7 @@ public class ModelLayerPanel extends JPanel { if (renderPanel == null) throw new IllegalStateException("需要 renderPanel 才能在 GL 上下文创建纹理"); try { - return renderPanel.executeInGLContext(() -> { + return renderPanel.getGlContextManager().executeInGLContext(() -> { // 静态工厂尝试 try { Method factory = findStaticMethod(Texture.class, "createFromBufferedImage", BufferedImage.class); 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 94c5a9a..41011b0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -1,6 +1,7 @@ package com.chuangzhou.vivid2D.render.awt; import com.chuangzhou.vivid2D.render.ModelRender; +import com.chuangzhou.vivid2D.render.awt.manager.GLContextManager; import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryManager; import com.chuangzhou.vivid2D.render.model.Model2D; @@ -10,14 +11,9 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PuppetPin; import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; import com.chuangzhou.vivid2D.render.systems.Camera; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.test.TestModelGLPanel; import org.joml.Matrix3f; import org.joml.Vector2f; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL11; -import org.lwjgl.system.MemoryUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,14 +23,10 @@ import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.IntBuffer; import java.util.List; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.LockSupport; /** * vivid2D 模型的 Java 渲染面板 @@ -49,27 +41,8 @@ import java.util.concurrent.locks.LockSupport; */ public class ModelRenderPanel extends JPanel { private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class); + private final GLContextManager glContextManager; private final AtomicReference modelRef = new AtomicReference<>(); - private long windowId; - private volatile boolean running = true; - private Thread renderThread; - // 改为可变的宽高以支持动态重建离屏上下文缓冲 - private volatile int width; - private volatile int height; - - private BufferedImage currentFrame; - private volatile boolean contextInitialized = false; - private final CompletableFuture contextReady = new CompletableFuture<>(); - private final String modelPath; - - // 任务队列,用于在 GL 上下文线程执行代码 - private final BlockingQueue glTaskQueue = new LinkedBlockingQueue<>(); - private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(); - - private BufferedImage lastFrame = null; - private ByteBuffer pixelBuffer = null; - private int[] pixelInts = null; - private int[] argbInts = null; private final CopyOnWriteArrayList clickListeners = new CopyOnWriteArrayList<>(); private volatile Mesh2D hoveredMesh = null; @@ -81,7 +54,7 @@ public class ModelRenderPanel extends JPanel { private volatile float partStartX, partStartY; private volatile boolean isDragging = false; - private enum DragMode { + public enum DragMode { NONE, // 无拖拽 MOVE, // 移动部件 RESIZE_LEFT, // 调整左边 @@ -101,26 +74,17 @@ public class ModelRenderPanel extends JPanel { // 新增:拖拽相关字段 private volatile DragMode currentDragMode = DragMode.NONE; private volatile float resizeStartWidth, resizeStartHeight; - private volatile float resizeStartX, resizeStartY; private volatile boolean shiftPressed = false; private volatile boolean ctrlPressed = false; // 新增:Ctrl键状态 // 新增:选择框边框厚度和角点大小 public static final float BORDER_THICKNESS = 6.0f; public static final float CORNER_SIZE = 12.0f; - - private final float partInitialScaleX = 1.0f; - private final float partInitialScaleY = 1.0f; - - private volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等) - private volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置) - private static final float ZOOM_SMOOTHING = 0.18f; // 0..1, 越大收敛越快(建议 0.12-0.25) private static final float ZOOM_STEP = 1.15f; // 每格滚轮的指数因子(>1 放大) private static final float ZOOM_MIN = 0.1f; private static final float ZOOM_MAX = 8.0f; private volatile boolean shiftDuringDrag = false; private volatile float rotationStartAngle = 0.0f; - private final float partInitialRotation = 0.0f; private final Vector2f rotationCenter = new Vector2f(); private static final float ROTATION_HANDLE_DISTANCE = 30.0f; private OperationHistoryManager historyManager; @@ -141,7 +105,7 @@ public class ModelRenderPanel extends JPanel { // ================== 摄像机控制相关字段 ================== - private volatile boolean cameraDragging = false; + private volatile int lastCameraDragX, lastCameraDragY; private volatile float cameraStartX, cameraStartY; private static final float CAMERA_ZOOM_STEP = 1.1f; @@ -176,7 +140,7 @@ public class ModelRenderPanel extends JPanel { private volatile PuppetPin hoveredPuppetPin = null; // 悬停的木偶控制点 private static final float PUPPET_PIN_TOLERANCE = 8.0f; // 木偶控制点选择容差 -// ================== 摄像机控制方法 ================== + // ================== 摄像机控制方法 ================== /** * 获取摄像机实例 @@ -185,55 +149,18 @@ public class ModelRenderPanel extends JPanel { 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()); + glContextManager.executeInGLContext(() -> ModelRender.resetCamera()); } /** * 构造函数:使用模型路径 */ public ModelRenderPanel(String modelPath, int width, int height) { - this.modelPath = modelPath; - this.width = width; - this.height = height; + this.glContextManager = new GLContextManager(modelPath, width, height); this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); initKeyboardShortcuts(); @@ -248,9 +175,7 @@ public class ModelRenderPanel extends JPanel { * 构造函数:使用已加载模型 */ public ModelRenderPanel(Model2D model, int width, int height) { - this.modelPath = null; - this.width = width; - this.height = height; + this.glContextManager = new GLContextManager(model, width, height); this.modelRef.set(model); this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); @@ -354,7 +279,7 @@ public class ModelRenderPanel extends JPanel { if (puppetMode) { exitPuppetMode(); } else { - executeInGLContext(this::enterPuppetMode); + glContextManager.executeInGLContext(this::enterPuppetMode); } } @@ -405,7 +330,7 @@ public class ModelRenderPanel extends JPanel { * 退出木偶工具模式 */ private void exitPuppetMode() { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { puppetMode = false; if (puppetTargetMesh != null) { puppetTargetMesh.setShowPuppetPins(false); @@ -499,7 +424,7 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -663,7 +588,7 @@ public class ModelRenderPanel extends JPanel { if (secondaryVertexMode) { exitSecondaryVertexMode(); } else { - executeInGLContext(this::enterSecondaryVertexMode); + glContextManager.executeInGLContext(this::enterSecondaryVertexMode); } } @@ -711,7 +636,7 @@ public class ModelRenderPanel extends JPanel { * 退出二级顶点变形模式 */ private void exitSecondaryVertexMode() { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { secondaryVertexMode = false; if (secondaryVertexTargetMesh != null) { secondaryVertexTargetMesh.setShowSecondaryVertices(false); @@ -805,7 +730,7 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -976,7 +901,7 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { // 转换屏幕坐标到模型坐标 float[] modelCoords = screenToModelCoordinates(screenX, screenY); @@ -1035,7 +960,7 @@ public class ModelRenderPanel extends JPanel { * 退出液化模式 */ private void exitLiquifyMode() { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { liquifyMode = false; if (liquifyTargetPart != null) { liquifyTargetPart.setStartLiquefy(false); @@ -1070,7 +995,7 @@ public class ModelRenderPanel extends JPanel { logger.debug("液化模式单击: {}", mousePos); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y); if (modelCoords == null) return; @@ -1305,7 +1230,7 @@ public class ModelRenderPanel extends JPanel { */ public void undo() { if (operationHistory != null && operationHistory.canUndo()) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { boolean success = operationHistory.undo(); if (success) { repaint(); @@ -1322,7 +1247,7 @@ public class ModelRenderPanel extends JPanel { */ public void redo() { if (operationHistory != null && operationHistory.canRedo()) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { boolean success = operationHistory.redo(); if (success) { repaint(); @@ -1404,7 +1329,7 @@ public class ModelRenderPanel extends JPanel { * 添加选中的网格(多选) */ public void addSelectedMesh(Mesh2D mesh) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { if (mesh != null && !selectedMeshes.contains(mesh)) { mesh.setSelected(true); selectedMeshes.add(mesh); @@ -1422,7 +1347,7 @@ public class ModelRenderPanel extends JPanel { * 移除选中的网格 */ public void removeSelectedMesh(Mesh2D mesh) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { if (mesh != null && selectedMeshes.contains(mesh)) { mesh.setSelected(false); selectedMeshes.remove(mesh); @@ -1444,7 +1369,7 @@ public class ModelRenderPanel extends JPanel { * 清空所有选中的网格 */ public void clearSelectedMeshes() { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { // 如果当前在液化模式,先退出液化模式 if (liquifyMode) { exitLiquifyMode(); @@ -1475,7 +1400,7 @@ public class ModelRenderPanel extends JPanel { * 全选所有网格 */ public void selectAllMeshes() { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { Model2D model = modelRef.get(); if (model == null) return; @@ -1606,18 +1531,13 @@ public class ModelRenderPanel extends JPanel { private void initialize() { setLayout(new BorderLayout()); - setPreferredSize(new Dimension(width, height)); - - // 初始化 GLFW - if (!GLFW.glfwInit()) { - throw new RuntimeException("无法初始化 GLFW"); - } + setPreferredSize(new Dimension(glContextManager.getWidth(), glContextManager.getHeight())); // 添加鼠标监听器 addMouseListeners(); // 创建渲染线程 - startRendering(); + glContextManager.startRendering(); this.addComponentListener(new ComponentAdapter() { @Override @@ -1625,10 +1545,12 @@ public class ModelRenderPanel extends JPanel { int w = getWidth(); int h = getHeight(); if (w <= 0 || h <= 0) return; - if (w == ModelRenderPanel.this.width && h == ModelRenderPanel.this.height) return; - ModelRenderPanel.this.resize(w, h); + if (w == glContextManager.getWidth() && h == glContextManager.getHeight()) return; + resize(w, h); } }); + + glContextManager.setRepaintCallback(this::repaint); } /** @@ -1661,14 +1583,14 @@ public class ModelRenderPanel extends JPanel { addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { - if (!contextInitialized) return; + if (!glContextManager.isContextInitialized()) return; final int screenX = e.getX(); final int screenY = e.getY(); final int notches = e.getWheelRotation(); final boolean fine = e.isShiftDown(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { Camera camera = ModelRender.getCamera(); float oldZoom = camera.getZoom(); @@ -1708,8 +1630,8 @@ public class ModelRenderPanel extends JPanel { camera.move(panX, panY); // 6. 更新面板的缩放状态变量,禁用平滑缩放以确保一致性 - displayScale = newZoom; - targetScale = newZoom; + glContextManager.setDisplayScale(newZoom); + glContextManager.setTargetScale(newZoom); }); } }); @@ -1804,14 +1726,14 @@ public class ModelRenderPanel extends JPanel { double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP; if (notches > 0) { // 滚轮下:缩小 - targetScale *= Math.pow(1.0 / step, notches); + glContextManager.targetScale *= Math.pow(1.0 / step, notches); } else if (notches < 0) { // 滚轮上:放大 - targetScale *= Math.pow(step, -notches); + glContextManager.targetScale *= Math.pow(step, -notches); } // 限制范围 - targetScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, targetScale)); + glContextManager.targetScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, glContextManager.targetScale)); } }); @@ -1824,14 +1746,14 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标按下事件(开始拖拽或调整大小) */ private void handleMousePressed(MouseEvent e) { - if (!contextInitialized) return; + if (!glContextManager.isContextInitialized()) return; final int screenX = e.getX(); final int screenY = e.getY(); requestFocusInWindow(); // 首先处理中键拖拽(摄像机控制),在任何模式下都可用 if (SwingUtilities.isMiddleMouseButton(e)) { - cameraDragging = true; + glContextManager.setCameraDragging(true); lastCameraDragX = screenX; lastCameraDragY = screenY; @@ -1847,7 +1769,7 @@ public class ModelRenderPanel extends JPanel { // 二级顶点模式下的左键处理 if (secondaryVertexMode && SwingUtilities.isLeftMouseButton(e)) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -1864,7 +1786,7 @@ public class ModelRenderPanel extends JPanel { } if (puppetMode && SwingUtilities.isLeftMouseButton(e)) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -1899,7 +1821,7 @@ public class ModelRenderPanel extends JPanel { shiftDuringDrag = e.isShiftDown(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { // 转换屏幕坐标到模型坐标 float[] modelCoords = screenToModelCoordinates(screenX, screenY); @@ -2303,19 +2225,19 @@ public class ModelRenderPanel extends JPanel { int panelWidth = getWidth(); int panelHeight = getHeight(); - if (panelWidth <= 0 || panelHeight <= 0 || width <= 0 || height <= 0) { + if (panelWidth <= 0 || panelHeight <= 0 || glContextManager.getHeight() <= 0 || glContextManager.getHeight() <= 0) { return 1.0f; } // 计算面板与离屏缓冲区的比例 - float scaleX = (float) panelWidth / width; - float scaleY = (float) panelHeight / height; + float scaleX = (float) panelWidth / glContextManager.getWidth(); + float scaleY = (float) panelHeight / glContextManager.getHeight(); // 基本面板缩放(保持与现有逻辑一致) float base = Math.min(scaleX, scaleY); // 乘以平滑的 displayScale,使视觉上缩放与检测区域一致 - return base * displayScale; + return base * glContextManager.displayScale; } /** @@ -2330,7 +2252,7 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标拖拽事件 */ private void handleMouseDragged(MouseEvent e) { - if (cameraDragging) { + if (glContextManager.isCameraDragging()) { final int screenX = e.getX(); final int screenY = e.getY(); // 计算鼠标移动距离 @@ -2342,7 +2264,7 @@ public class ModelRenderPanel extends JPanel { lastCameraDragY = screenY; // 确保在 GL 上下文线程中执行摄像机移动 - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { Camera camera = ModelRender.getCamera(); float zoom = camera.getZoom(); @@ -2363,7 +2285,7 @@ public class ModelRenderPanel extends JPanel { if (secondaryVertexMode && SwingUtilities.isLeftMouseButton(e)) { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -2383,7 +2305,7 @@ public class ModelRenderPanel extends JPanel { if (puppetMode && SwingUtilities.isLeftMouseButton(e)) { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -2410,7 +2332,7 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { if (currentDragMode == DragMode.NONE) { logger.debug("拖拽已取消,跳过处理"); @@ -2453,7 +2375,7 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { float[] modelCoords = screenToModelCoordinates(screenX, screenY); if (modelCoords == null) return; @@ -2698,8 +2620,8 @@ public class ModelRenderPanel extends JPanel { */ private void handleMouseReleased(MouseEvent e) { // 首先处理摄像机拖拽释放 - if (cameraDragging && (SwingUtilities.isMiddleMouseButton(e) || liquifyMode)) { - cameraDragging = false; + if (glContextManager.isCameraDragging() && (SwingUtilities.isMiddleMouseButton(e) || liquifyMode)) { + glContextManager.setCameraDragging(false); // 恢复悬停状态的光标 updateCursorForHoverState(); return; @@ -2812,7 +2734,7 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标点击事件 */ private void handleMouseClick(MouseEvent e) { - if (!contextInitialized) return; + if (!glContextManager.isContextInitialized()) return; final int screenX = e.getX(); final int screenY = e.getY(); @@ -2826,7 +2748,7 @@ public class ModelRenderPanel extends JPanel { doubleClickTimer.stop(); handleDoubleClick(e); } else { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { // 转换屏幕坐标到模型坐标 float[] modelCoords = screenToModelCoordinates(screenX, screenY); @@ -2864,17 +2786,17 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标移动事件 */ private void handleMouseMove(MouseEvent e) { - if (!contextInitialized) return; + if (!glContextManager.isContextInitialized()) return; final int screenX = e.getX(); final int screenY = e.getY(); - if (cameraDragging) { + if (glContextManager.isCameraDragging()) { return; } // 在 GL 上下文线程中执行悬停检测 - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { try { // 转换屏幕坐标到模型坐标 float[] modelCoords = screenToModelCoordinates(screenX, screenY); @@ -3033,24 +2955,15 @@ public class ModelRenderPanel extends JPanel { * 将屏幕坐标转换为模型坐标 */ public float[] screenToModelCoordinates(int screenX, int screenY) { - if (!contextInitialized || this.width <= 0 || this.height <= 0) return null; - - // 1. 将 Swing 坐标缩放到 GL 上下文坐标 - float glX = (float) screenX * this.width / getWidth(); - float glY = (float) screenY * this.height / getHeight(); - - // 2. 转换为归一化设备坐标 (NDC) - float ndcX = (2.0f * glX) / this.width - 1.0f; - float ndcY = 1.0f - (2.0f * glY) / this.height; // 翻转 Y 轴 - - // 3. 获取摄像机偏移 - Vector2f camOffset = ModelRender.getCameraOffset(); // 这里替换原来的 camera.getPosition() + if (!glContextManager.isContextInitialized() || glContextManager.getWidth() <= 0 || glContextManager.getHeight() <= 0) return null; + float glX = (float) screenX * glContextManager.getWidth() / getWidth(); + float glY = (float) screenY * glContextManager.getHeight() / getHeight(); + float ndcX = (2.0f * glX) / glContextManager.getWidth() - 1.0f; + float ndcY = 1.0f - (2.0f * glY) / glContextManager.getHeight(); + Vector2f camOffset = ModelRender.getCameraOffset(); float zoom = ModelRender.getCamera().getZoom(); - - // 4. 逆变换公式 - float modelX = (ndcX * this.width / (2.0f * zoom)) + camOffset.x; - float modelY = (ndcY * this.height / (-2.0f * zoom)) + camOffset.y; - + float modelX = (ndcX * glContextManager.getWidth() / (2.0f * zoom)) + camOffset.x; + float modelY = (ndcY * glContextManager.getHeight() / (-2.0f * zoom)) + camOffset.y; return new float[]{modelX, modelY}; } @@ -3065,12 +2978,6 @@ public class ModelRenderPanel extends JPanel { } try { - // 获取摄像机偏移 - Vector2f camOffset = ModelRender.getCameraOffset(); - - float checkX = modelX; - float checkY = modelY; - List parts = model.getParts(); if (parts == null || parts.isEmpty()) { return null; @@ -3094,7 +3001,7 @@ public class ModelRenderPanel extends JPanel { boolean contains = false; try { - contains = mesh.containsPoint(checkX, checkY); + contains = mesh.containsPoint(modelX, modelY); } catch (Exception ex) { logger.warn("mesh.containsPoint 抛出异常: {}", ex.getMessage()); } @@ -3126,79 +3033,6 @@ public class ModelRenderPanel extends JPanel { } } - /** - * 创建离屏 OpenGL 上下文 - */ - private void createOffscreenContext() throws Exception { - // 设置窗口提示 - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4); - - // 创建离屏窗口(像素尺寸以当前 width/height 为准) - windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL); - if (windowId == MemoryUtil.NULL) { - throw new Exception("无法创建离屏 OpenGL 上下文"); - } - - // 设置为当前上下文并初始化 - GLFW.glfwMakeContextCurrent(windowId); - GL.createCapabilities(); - - logger.info("OpenGL context created successfully"); - - // 然后初始化 RenderSystem - RenderSystem.beginInitialization(); - RenderSystem.initRenderThread(); - - // 使用 RenderSystem 设置视口 - RenderSystem.viewport(0, 0, width, height); - - // 分配像素读取缓冲 - int pixelCount = Math.max(1, width * height); - pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); - pixelBuffer.order(ByteOrder.nativeOrder()); - pixelInts = new int[pixelCount]; - argbInts = new int[pixelCount]; - - // 初始化 ModelRender - ModelRender.initialize(); - - RenderSystem.finishInitialization(); - - // 在正确的上下文中加载模型(可能会耗时) - loadModelInContext(); - - // 标记上下文已初始化并完成通知(只 complete 一次) - contextInitialized = true; - contextReady.complete(null); - - logger.info("Offscreen context initialization completed"); - } - - /** - * 在 OpenGL 上下文中加载模型 - */ - private void loadModelInContext() { - try { - if (modelPath != null) { - Model2D model = Model2D.loadFromFile(modelPath); - modelRef.set(model); - logger.info("模型加载成功: {}", modelPath); - } - } catch (Exception e) { - logger.error("模型加载失败: {}", e.getMessage(), e); - e.printStackTrace(); - - // 创建错误模型或使用默认模型 - createErrorModel(); - } - } - /** * 创建错误模型作为回退 */ @@ -3212,192 +3046,6 @@ public class ModelRenderPanel extends JPanel { } } - /** - * 启动渲染线程 - */ - private void startRendering() { - renderThread = new Thread(() -> { - try { - createOffscreenContext(); - - // 等待上下文就绪后再开始渲染循环(contextReady 由 createOffscreenContext 完成) - contextReady.get(); - - // 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent) - GLFW.glfwMakeContextCurrent(windowId); - - final long targetNs = 1_000_000_000L / 60L; // 60 FPS - while (running && !GLFW.glfwWindowShouldClose(windowId)) { - long start = System.nanoTime(); - - processGLTasks(); - displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING; - renderFrame(); - - long elapsed = System.nanoTime() - start; - long sleepNs = targetNs - elapsed; - if (sleepNs > 0) { - LockSupport.parkNanos(sleepNs); - } - } - } catch (Exception e) { - logger.error("渲染线程异常", e); - } finally { - cleanup(); - } - }); - - renderThread.setDaemon(true); - renderThread.setName("GL-Render-Thread"); - renderThread.start(); - } - - /** - * 处理 GL 上下文任务队列 - */ - private void processGLTasks() { - Runnable task; - while ((task = glTaskQueue.poll()) != null) { - try { - // 在渲染线程中执行,渲染线程已将上下文设为 current - task.run(); - } catch (Exception e) { - logger.error("执行 GL 任务时出错", e); - } - } - } - - /** - * 渲染单帧并读取到 BufferedImage - */ - private void renderFrame() { - if (!contextInitialized || windowId == 0) return; - - // 确保在当前上下文中 - GLFW.glfwMakeContextCurrent(windowId); - - Model2D currentModel = modelRef.get(); - if (currentModel != null) { - try { - // 使用 RenderSystem 清除缓冲区 - RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f); - RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); - - // 渲染模型 - ModelRender.render(1.0f / 60f, currentModel); - - // 读取像素数据到 BufferedImage - readPixelsToImage(); - } catch (Exception e) { - System.err.println("渲染错误: " + e.getMessage()); - renderErrorFrame(e.getMessage()); - } - } else { - // 没有模型时显示默认背景 - renderDefaultBackground(); - } - - // 在 Swing EDT 中更新显示 - SwingUtilities.invokeLater(this::repaint); - } - - /** - * 渲染错误帧 - */ - private void renderErrorFrame(String errorMessage) { - GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f); - GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); - readPixelsToImage(); - - // 创建错误图像 - BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB); - Graphics2D g2d = errorImage.createGraphics(); - g2d.setColor(Color.DARK_GRAY); - g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); - g2d.setColor(Color.RED); - g2d.drawString("渲染错误: " + errorMessage, 10, 20); - g2d.dispose(); - currentFrame = errorImage; - } - - /** - * 渲染默认背景 - */ - private void renderDefaultBackground() { - RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f); - RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); - readPixelsToImage(); - } - - /** - * 读取 OpenGL 像素数据到 BufferedImage - */ - private void readPixelsToImage() { - try { - final int w = Math.max(1, this.width); - final int h = Math.max(1, this.height); - final int pixelCount = w * h; - - // 确保缓冲区大小匹配(可能在 resize 后需要重建) - if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) { - if (pixelBuffer != null) { - try { - MemoryUtil.memFree(pixelBuffer); - } catch (Throwable ignored) { - } - } - pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); - pixelBuffer.order(ByteOrder.nativeOrder()); - pixelInts = new int[pixelCount]; - argbInts = new int[pixelCount]; - } - - pixelBuffer.clear(); - // 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem - RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer); - - // 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转 - IntBuffer ib = pixelBuffer.asIntBuffer(); - ib.get(pixelInts, 0, pixelCount); - - // 转换并翻转(RGBA -> ARGB) - for (int y = 0; y < h; y++) { - int srcRow = (h - y - 1) * w; - int dstRow = y * w; - for (int x = 0; x < w; x++) { - int rgba = pixelInts[srcRow + x]; - - // 提取字节(考虑 native order,按 RGBA 存放) - int r = (rgba >> 0) & 0xFF; - int g = (rgba >> 8) & 0xFF; - int b = (rgba >> 16) & 0xFF; - int a = (rgba >> 24) & 0xFF; - - // 组合为 ARGB (BufferedImage 使用 ARGB) - argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b; - } - } - - // 使用一次 setRGB 写入 BufferedImage(比逐像素 setRGB 快) - BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - image.setRGB(0, 0, w, h, argbInts, 0, w); - - currentFrame = image; - lastFrame = image; - } catch (Exception e) { - logger.error("读取像素数据错误", e); - // 创建错误图像(保持原逻辑) - BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB); - Graphics2D g2d = errorImage.createGraphics(); - g2d.setColor(Color.BLACK); - g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); - g2d.setColor(Color.RED); - g2d.drawString("像素读取失败", 10, 20); - g2d.dispose(); - currentFrame = errorImage; - } - } - @Override protected void paintComponent(Graphics g) { super.paintComponent(g); @@ -3405,21 +3053,18 @@ public class ModelRenderPanel extends JPanel { Graphics2D g2d = (Graphics2D) g.create(); try { // 选择要绘制的图像:优先 currentFrame(最新),其不存在则用 lastFrame(最后成功帧) - BufferedImage imgToDraw = currentFrame != null ? currentFrame : lastFrame; + BufferedImage imgToDraw = glContextManager.getCurrentFrame() + != null ? glContextManager.getCurrentFrame() : glContextManager.getLastFrame(); int panelW = getWidth(); int panelH = getHeight(); if (imgToDraw != null) { - // 绘制图像并拉伸以适应面板(保留最近一帧,避免闪烁) g2d.drawImage(imgToDraw, 0, 0, panelW, panelH, null); } else { - // 没有任何帧时,绘制静态背景(不会频繁切换) g2d.setColor(Color.DARK_GRAY); g2d.fillRect(0, 0, panelW, panelH); } - - // 如果模型为空,显示提示(绘制在最上层) if (modelRef.get() == null) { g2d.setColor(new Color(255, 255, 0, 200)); g2d.drawString("模型未加载", 10, 20); @@ -3431,108 +3076,11 @@ public class ModelRenderPanel extends JPanel { // ================== 新增:GL 上下文任务执行方法 ================== - /** - * 在 GL 上下文线程上异步执行任务 - * - * @param task 要在 GL 上下文线程中执行的任务 - * @return CompletableFuture 用于获取任务执行结果 - */ - public CompletableFuture executeInGLContext(Runnable task) { - CompletableFuture future = new CompletableFuture<>(); - - if (!running) { - future.completeExceptionally(new IllegalStateException("渲染线程已停止")); - return future; - } - - // 等待上下文就绪后再提交任务 - contextReady.thenRun(() -> { - try { - // 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队 - glTaskQueue.put(() -> { - try { - task.run(); - future.complete(null); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - - return future; - } - - /** - * 在 GL 上下文线程上异步执行任务并返回结果 - * - * @param task 要在 GL 上下文线程中执行的有返回值的任务 - * @return CompletableFuture 用于获取任务执行结果 - */ - public CompletableFuture executeInGLContext(Callable task) { - CompletableFuture future = new CompletableFuture<>(); - - if (!running) { - future.completeExceptionally(new IllegalStateException("渲染线程已停止")); - return future; - } - - contextReady.thenRun(() -> { - try { - glTaskQueue.put(() -> { - try { - T result = task.call(); - future.complete(result); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - - return future; - } - - /** - * 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成) - * - * @param task 要在 GL 上下文线程中执行的任务 - * @throws Exception 如果任务执行出错 - */ - public void executeInGLContextSync(Runnable task) throws Exception { - if (!running) { - throw new IllegalStateException("渲染线程已停止"); - } - - CompletableFuture future = executeInGLContext(task); - future.get(10, TimeUnit.SECONDS); // 设置超时时间 - } - - /** - * 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成) - * - * @param task 要在 GL 上下文线程中执行的有返回值的任务 - * @return 任务执行结果 - * @throws Exception 如果任务执行出错或超时 - */ - public T executeInGLContextSync(Callable task) throws Exception { - if (!running) { - throw new IllegalStateException("渲染线程已停止"); - } - - CompletableFuture future = executeInGLContext(task); - return future.get(10, TimeUnit.SECONDS); // 设置超时时间 - } - /** * 设置模型(线程安全)- 使用新的 GL 上下文执行方法 */ public void setModel(Model2D model) { - executeInGLContext(() -> { + glContextManager.executeInGLContext(() -> { modelRef.set(model); logger.info("模型已更新"); }); @@ -3555,116 +3103,7 @@ public class ModelRenderPanel extends JPanel { // 更新 Swing 尺寸 setPreferredSize(new Dimension(newWidth, newHeight)); revalidate(); - - // 在 GL 上下文线程中更新离屏窗口与缓冲 - executeInGLContext(() -> { - if (contextInitialized && windowId != 0) { - // 更新内部宽高字段 - this.width = Math.max(1, newWidth); - this.height = Math.max(1, newHeight); - - // 将离屏 GLFW 窗口也调整为新的像素尺寸 - GLFW.glfwMakeContextCurrent(windowId); - GLFW.glfwSetWindowSize(windowId, this.width, this.height); - - // 更新 OpenGL 视口与 ModelRender 的视口 - RenderSystem.viewport(0, 0, this.width, this.height); - ModelRender.setViewport(this.width, this.height); - - // 重新分配像素读取缓冲区(释放旧的) - try { - if (pixelBuffer != null) { - MemoryUtil.memFree(pixelBuffer); - pixelBuffer = null; - } - } catch (Throwable t) { - // 忽略释放错误,继续重分配 - } - int pixelCount = Math.max(1, this.width * this.height); - pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); - pixelBuffer.order(ByteOrder.nativeOrder()); - pixelInts = new int[pixelCount]; - argbInts = new int[pixelCount]; - - // 丢弃当前帧,下一帧会使用新尺寸重新生成 - currentFrame = null; - } else { - // 如果还没初始化 GL,上层改变 Swing 大小即可,实际缓冲会在 createOffscreenContext 时按最新宽高创建 - this.width = Math.max(1, newWidth); - this.height = Math.max(1, newHeight); - } - }); - } - - /** - * 等待渲染上下文准备就绪 - */ - public CompletableFuture waitForContext() { - return contextReady; - } - - /** - * 检查是否正在运行 - */ - public boolean isRunning() { - return running && contextInitialized; - } - - /** - * 清理资源 - */ - public void dispose() { - running = false; - cameraDragging = false; - // 停止任务执行器 - taskExecutor.shutdown(); - - if (renderThread != null) { - try { - renderThread.join(2000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - cleanup(); - } - - private void cleanup() { - // 清理 ModelRender - try { - if (ModelRender.isInitialized()) { - ModelRender.cleanup(); - logger.info("ModelRender 已清理"); - } - } catch (Exception e) { - logger.error("清理 ModelRender 时出错: {}", e.getMessage()); - } - - if (windowId != 0) { - try { - GLFW.glfwDestroyWindow(windowId); - } catch (Throwable ignored) { - } - windowId = 0; - } - - // 释放像素缓冲 - try { - if (pixelBuffer != null) { - MemoryUtil.memFree(pixelBuffer); - pixelBuffer = null; - } - } catch (Throwable t) { - logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage()); - } - - // 终止 GLFW(注意:如果应用中还有其他 GLFW 窗口,这里会影响它们) - try { - GLFW.glfwTerminate(); - } catch (Throwable ignored) { - } - - logger.info("OpenGL 资源已清理"); + glContextManager.resize(newWidth, newHeight); } /** @@ -3687,4 +3126,11 @@ public class ModelRenderPanel extends JPanel { public OperationHistoryGlobal getOperationHistory() { return operationHistory; } + + /** + * 获取 GL 上下文管理器 + */ + public GLContextManager getGlContextManager() { + return glContextManager; + } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java index 7d0d9ad..d2785c2 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java @@ -236,7 +236,7 @@ public class TransformPanel extends JPanel implements ModelEvent { // 旋转按钮监听器修改(支持多选) rotate90CWButton.addActionListener(e -> { if (!selectedParts.isEmpty()) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { Map oldRotations = new HashMap<>(); Map newRotations = new HashMap<>(); @@ -263,7 +263,7 @@ public class TransformPanel extends JPanel implements ModelEvent { rotate90CCWButton.addActionListener(e -> { if (!selectedParts.isEmpty()) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { Map oldRotations = new HashMap<>(); Map newRotations = new HashMap<>(); @@ -291,7 +291,7 @@ public class TransformPanel extends JPanel implements ModelEvent { // 翻转按钮监听器修改(支持多选) flipXButton.addActionListener(e -> { if (!selectedParts.isEmpty()) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { Map oldScales = new HashMap<>(); Map newScales = new HashMap<>(); @@ -318,7 +318,7 @@ public class TransformPanel extends JPanel implements ModelEvent { flipYButton.addActionListener(e -> { if (!selectedParts.isEmpty()) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { Map oldScales = new HashMap<>(); Map newScales = new HashMap<>(); @@ -346,7 +346,7 @@ public class TransformPanel extends JPanel implements ModelEvent { // 重置缩放按钮监听器修改(支持多选) resetScaleButton.addActionListener(e -> { if (!selectedParts.isEmpty()) { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { Map oldScales = new HashMap<>(); Map newScales = new HashMap<>(); @@ -495,7 +495,7 @@ public class TransformPanel extends JPanel implements ModelEvent { private void applyTransformChanges() { if (updatingUI || selectedParts.isEmpty()) return; - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { try { float posX = Float.parseFloat(positionXField.getText()); float posY = Float.parseFloat(positionYField.getText()); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java new file mode 100644 index 0000000..24a7f2c --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java @@ -0,0 +1,586 @@ +package com.chuangzhou.vivid2D.render.awt.manager; + +import com.chuangzhou.vivid2D.render.ModelRender; +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11; +import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +public class GLContextManager { + private static final Logger logger = LoggerFactory.getLogger(GLContextManager.class); + private long windowId; + private volatile boolean running = true; + private Thread renderThread; + // 改为可变的宽高以支持动态重建离屏上下文缓冲 + private volatile int width; + private volatile int height; + + private BufferedImage currentFrame; + private volatile boolean contextInitialized = false; + private final CompletableFuture contextReady = new CompletableFuture<>(); + private final String modelPath; + private final AtomicReference modelRef = new AtomicReference<>(); + + private BufferedImage lastFrame = null; + private ByteBuffer pixelBuffer = null; + private int[] pixelInts = null; + private int[] argbInts = null; + public volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等) + public volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置) + + // 任务队列,用于在 GL 上下文线程执行代码 + private final BlockingQueue glTaskQueue = new LinkedBlockingQueue<>(); + private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(); + + private volatile boolean cameraDragging = false; + private static final float ZOOM_SMOOTHING = 0.18f; // 0..1, 越大收敛越快(建议 0.12-0.25) + private RepaintCallback repaintCallback; + + public GLContextManager(String modelPath, int width, int height) { + this.modelPath = modelPath; + this.width = width; + this.height = height; + } + + public GLContextManager(Model2D model, int width, int height) { + this.modelPath = null; + this.width = width; + this.height = height; + this.modelRef.set(model); + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + /** + * 创建离屏 OpenGL 上下文 + */ + private void createOffscreenContext() throws Exception { + // 设置窗口提示 + GLFW.glfwDefaultWindowHints(); + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE); + GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4); + + // 创建离屏窗口(像素尺寸以当前 width/height 为准) + windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL); + if (windowId == MemoryUtil.NULL) { + throw new Exception("无法创建离屏 OpenGL 上下文"); + } + + // 设置为当前上下文并初始化 + GLFW.glfwMakeContextCurrent(windowId); + GL.createCapabilities(); + + logger.info("OpenGL context created successfully"); + + // 然后初始化 RenderSystem + RenderSystem.beginInitialization(); + RenderSystem.initRenderThread(); + + // 使用 RenderSystem 设置视口 + RenderSystem.viewport(0, 0, width, height); + + // 分配像素读取缓冲 + int pixelCount = Math.max(1, width * height); + pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); + pixelBuffer.order(ByteOrder.nativeOrder()); + pixelInts = new int[pixelCount]; + argbInts = new int[pixelCount]; + + // 初始化 ModelRender + ModelRender.initialize(); + + RenderSystem.finishInitialization(); + + // 在正确的上下文中加载模型(可能会耗时) + loadModelInContext(); + + // 标记上下文已初始化并完成通知(只 complete 一次) + contextInitialized = true; + contextReady.complete(null); + + logger.info("Offscreen context initialization completed"); + } + + public void setRepaintCallback(RepaintCallback callback) { + this.repaintCallback = callback; + } + + /** + * 在 OpenGL 上下文中加载模型 + */ + private void loadModelInContext() { + try { + if (modelPath != null) { + Model2D model = Model2D.loadFromFile(modelPath); + modelRef.set(model); + logger.info("模型加载成功: {}", modelPath); + } + } catch (Exception e) { + logger.error("模型加载失败: {}", e.getMessage(), e); + e.printStackTrace(); + } + } + + /** + * 启动渲染线程 + */ + public void startRendering() { + // 初始化 GLFW + if (!GLFW.glfwInit()) { + throw new RuntimeException("无法初始化 GLFW"); + } + renderThread = new Thread(() -> { + try { + createOffscreenContext(); + + // 等待上下文就绪后再开始渲染循环(contextReady 由 createOffscreenContext 完成) + contextReady.get(); + + // 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent) + GLFW.glfwMakeContextCurrent(windowId); + + final long targetNs = 1_000_000_000L / 60L; // 60 FPS + while (running && !GLFW.glfwWindowShouldClose(windowId)) { + long start = System.nanoTime(); + + processGLTasks(); + displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING; + renderFrame(); + + long elapsed = System.nanoTime() - start; + long sleepNs = targetNs - elapsed; + if (sleepNs > 0) { + LockSupport.parkNanos(sleepNs); + } + } + } catch (Exception e) { + logger.error("渲染线程异常", e); + } finally { + cleanup(); + } + }); + + renderThread.setDaemon(true); + renderThread.setName("GL-Render-Thread"); + renderThread.start(); + } + + /** + * 渲染单帧并读取到 BufferedImage + */ + private void renderFrame() { + if (!contextInitialized || windowId == 0) return; + // 确保在当前上下文中 + GLFW.glfwMakeContextCurrent(windowId); + Model2D currentModel = modelRef.get(); + if (currentModel != null) { + try { + // 使用 RenderSystem 清除缓冲区 + RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f); + RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); + // 渲染模型 + ModelRender.render(1.0f / 60f, currentModel); + // 读取像素数据到 BufferedImage + readPixelsToImage(); + } catch (Exception e) { + System.err.println("渲染错误: " + e.getMessage()); + renderErrorFrame(e.getMessage()); + } + } else { + // 没有模型时显示默认背景 + renderDefaultBackground(); + } + + // 在 Swing EDT 中更新显示 + if (repaintCallback != null) { + repaintCallback.repaint(); + } + } + + /** + * 渲染默认背景 + */ + private void renderDefaultBackground() { + RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f); + RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); + readPixelsToImage(); + } + + /** + * 渲染错误帧 + */ + private void renderErrorFrame(String errorMessage) { + GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + readPixelsToImage(); + BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = errorImage.createGraphics(); + g2d.setColor(Color.DARK_GRAY); + g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); + g2d.setColor(Color.RED); + g2d.drawString("渲染错误: " + errorMessage, 10, 20); + g2d.dispose(); + currentFrame = errorImage; + } + + /** + * 读取 OpenGL 像素数据到 BufferedImage + */ + private void readPixelsToImage() { + try { + final int w = Math.max(1, this.width); + final int h = Math.max(1, this.height); + final int pixelCount = w * h; + + // 确保缓冲区大小匹配(可能在 resize 后需要重建) + if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) { + if (pixelBuffer != null) { + try { + MemoryUtil.memFree(pixelBuffer); + } catch (Throwable ignored) { + } + } + pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); + pixelBuffer.order(ByteOrder.nativeOrder()); + pixelInts = new int[pixelCount]; + argbInts = new int[pixelCount]; + } + + pixelBuffer.clear(); + // 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem + RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer); + + // 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转 + IntBuffer ib = pixelBuffer.asIntBuffer(); + ib.get(pixelInts, 0, pixelCount); + + // 转换并翻转(RGBA -> ARGB) + for (int y = 0; y < h; y++) { + int srcRow = (h - y - 1) * w; + int dstRow = y * w; + for (int x = 0; x < w; x++) { + int rgba = pixelInts[srcRow + x]; + + // 提取字节(考虑 native order,按 RGBA 存放) + int r = (rgba >> 0) & 0xFF; + int g = (rgba >> 8) & 0xFF; + int b = (rgba >> 16) & 0xFF; + int a = (rgba >> 24) & 0xFF; + + // 组合为 ARGB (BufferedImage 使用 ARGB) + argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + + // 使用一次 setRGB 写入 BufferedImage(比逐像素 setRGB 快) + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + image.setRGB(0, 0, w, h, argbInts, 0, w); + + currentFrame = image; + lastFrame = image; + } catch (Exception e) { + logger.error("读取像素数据错误", e); + // 创建错误图像(保持原逻辑) + BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = errorImage.createGraphics(); + g2d.setColor(Color.BLACK); + g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); + g2d.setColor(Color.RED); + g2d.drawString("像素读取失败", 10, 20); + g2d.dispose(); + currentFrame = errorImage; + } + } + + /** + * 处理 GL 上下文任务队列 + */ + private void processGLTasks() { + Runnable task; + while ((task = glTaskQueue.poll()) != null) { + try { + // 在渲染线程中执行,渲染线程已将上下文设为 current + task.run(); + } catch (Exception e) { + logger.error("执行 GL 任务时出错", e); + } + } + } + + /** + * 重新设置面板大小 + *

+ * 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲, + * 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。 + */ + public void resize(int newWidth, int newHeight) { + executeInGLContext(() -> { + if (contextInitialized && windowId != 0) { + this.width = Math.max(1, newWidth); + this.height = Math.max(1, newHeight); + GLFW.glfwMakeContextCurrent(windowId); + GLFW.glfwSetWindowSize(windowId, this.width, this.height); + RenderSystem.viewport(0, 0, this.width, this.height); + ModelRender.setViewport(this.width, this.height); + try { + if (pixelBuffer != null) { + MemoryUtil.memFree(pixelBuffer); + pixelBuffer = null; + } + } catch (Throwable ignored) {} + int pixelCount = Math.max(1, this.width * this.height); + pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4); + pixelBuffer.order(ByteOrder.nativeOrder()); + pixelInts = new int[pixelCount]; + argbInts = new int[pixelCount]; + currentFrame = null; + } else { + this.width = Math.max(1, newWidth); + this.height = Math.max(1, newHeight); + } + }); + } + + /** + * 等待渲染上下文准备就绪 + */ + public CompletableFuture waitForContext() { + return contextReady; + } + + /** + * 检查渲染上下文是否已初始化 + * @return true 表示已初始化,false 表示未初始化 + */ + public boolean isContextInitialized() { + return contextInitialized; + } + + /** + * 检查是否正在运行 + */ + public boolean isRunning() { + return running && contextInitialized; + } + + /** + * 清理资源 + */ + public void dispose() { + running = false; + cameraDragging = false; + // 停止任务执行器 + taskExecutor.shutdown(); + + if (renderThread != null) { + try { + renderThread.join(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + cleanup(); + } + + private void cleanup() { + // 清理 ModelRender + try { + if (ModelRender.isInitialized()) { + ModelRender.cleanup(); + logger.info("ModelRender 已清理"); + } + } catch (Exception e) { + logger.error("清理 ModelRender 时出错: {}", e.getMessage()); + } + + if (windowId != 0) { + try { + GLFW.glfwDestroyWindow(windowId); + } catch (Throwable ignored) { + } + windowId = 0; + } + + // 释放像素缓冲 + try { + if (pixelBuffer != null) { + MemoryUtil.memFree(pixelBuffer); + pixelBuffer = null; + } + } catch (Throwable t) { + logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage()); + } + + // 终止 GLFW(注意:如果应用中还有其他 GLFW 窗口,这里会影响它们) + try { + GLFW.glfwTerminate(); + } catch (Throwable ignored) { + } + + logger.info("OpenGL 资源已清理"); + } + + /** + * 在 GL 上下文线程上异步执行任务 + * + * @param task 要在 GL 上下文线程中执行的任务 + * @return CompletableFuture 用于获取任务执行结果 + */ + public CompletableFuture executeInGLContext(Runnable task) { + CompletableFuture future = new CompletableFuture<>(); + + if (!running) { + future.completeExceptionally(new IllegalStateException("渲染线程已停止")); + return future; + } + + // 等待上下文就绪后再提交任务 + contextReady.thenRun(() -> { + try { + // 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队 + glTaskQueue.put(() -> { + try { + task.run(); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + + return future; + } + + /** + * 在 GL 上下文线程上异步执行任务并返回结果 + * + * @param task 要在 GL 上下文线程中执行的有返回值的任务 + * @return CompletableFuture 用于获取任务执行结果 + */ + public CompletableFuture executeInGLContext(Callable task) { + CompletableFuture future = new CompletableFuture<>(); + + if (!running) { + future.completeExceptionally(new IllegalStateException("渲染线程已停止")); + return future; + } + + contextReady.thenRun(() -> { + try { + glTaskQueue.put(() -> { + try { + T result = task.call(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + + return future; + } + + /** + * 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成) + * + * @param task 要在 GL 上下文线程中执行的任务 + * @throws Exception 如果任务执行出错 + */ + public void executeInGLContextSync(Runnable task) throws Exception { + if (!running) { + throw new IllegalStateException("渲染线程已停止"); + } + + CompletableFuture future = executeInGLContext(task); + future.get(10, TimeUnit.SECONDS); // 设置超时时间 + } + + /** + * 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成) + * + * @param task 要在 GL 上下文线程中执行的有返回值的任务 + * @return 任务执行结果 + * @throws Exception 如果任务执行出错或超时 + */ + public T executeInGLContextSync(Callable task) throws Exception { + if (!running) { + throw new IllegalStateException("渲染线程已停止"); + } + + CompletableFuture future = executeInGLContext(task); + return future.get(10, TimeUnit.SECONDS); // 设置超时时间 + } + + public void setDisplayScale(float scale) { + this.displayScale = scale; + } + + public void setTargetScale(float scale) { + this.targetScale = scale; + } + + public float getDisplayScale() { + return displayScale; + } + + public float getTargetScale() { + return targetScale; + } + + public interface RepaintCallback { + void repaint(); + } + + /** + * 获取当前帧 + * @return 当前帧 + */ + public BufferedImage getCurrentFrame() { + return currentFrame; + } + + /** + * 获取上一帧 + * @return 上一帧 + */ + public BufferedImage getLastFrame() { + return lastFrame; + } + + public boolean isCameraDragging() { + return cameraDragging; + } + + public void setCameraDragging(boolean cameraDragging) { + this.cameraDragging = cameraDragging; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java index f74e548..d62acee 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java @@ -96,7 +96,7 @@ public class ModelLayerPanelTest { // 添加选中部件更新按钮 JButton updateSelectionBtn = new JButton("更新选中部件"); updateSelectionBtn.addActionListener(e -> { - renderPanel.executeInGLContext(() -> { + renderPanel.getGlContextManager().executeInGLContext(() -> { List selectedPart = renderPanel.getSelectedParts(); transformPanel.setSelectedParts(selectedPart); }); @@ -134,7 +134,7 @@ public class ModelLayerPanelTest { public void windowClosed(java.awt.event.WindowEvent e) { // 进程退出(确保彻底关闭) try { - renderPanel.dispose(); + renderPanel.getGlContextManager().dispose(); } catch (Throwable ignored) { } model.saveToFile("C:\\Users\\Administrator\\Desktop\\testing.model"); diff --git a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java index a72efd6..5885187 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java @@ -41,7 +41,7 @@ public class TestModelGLPanel { // 在 GL 上下文中创建 mesh / part / physics 等资源 ModelRenderPanel finalGlPanel = glPanel; - glPanel.executeInGLContext(() -> { + glPanel.getGlContextManager().executeInGLContext(() -> { setupModelInGL(testModel); return null; }); @@ -53,7 +53,7 @@ public class TestModelGLPanel { if (!animate) return; float dt = 1.0f / fps; // 在 GL 上下文中更新模型状态(旋转、参数、物理更新等) - finalGlPanel.executeInGLContext(() -> { + finalGlPanel.getGlContextManager().executeInGLContext(() -> { updateAnimation(testModel, dt); return null; });