diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 1f6bb65..2860085 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -8,7 +8,9 @@ import com.chuangzhou.vivid2D.render.model.util.LightSource; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.ShaderSources; +import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; import org.joml.Matrix3f; import org.joml.Vector2f; import org.joml.Vector4f; @@ -19,8 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import static org.lwjgl.opengl.GL20.glGetUniformLocation; - /** * vivid2D 模型完整渲染系统 * @@ -106,24 +106,20 @@ public final class ModelRender { */ private static final boolean enableBlending = true; - private static final int SHADER_MAX_LIGHTS = 8; + /** + * 最大光源数量,用于限制同时启用的光源数量 + * 默认值:80 + */ + private static final int MAX_LIGHTS = 80; // ================== 着色器与资源管理 ================== - /** - * 着色器程序缓存映射,按名称存储已编译的着色器程序 - * 键:着色器名称(如 "default") - * 值:对应的着色器程序对象 - * @see ShaderSources.ShaderProgram - */ - private static final Map shaderMap = new HashMap<>(); - /** * 默认着色器程序,用于大多数模型的渲染 * 包含基础的光照、纹理和变换功能 * @see #compileDefaultShader() */ - private static ShaderSources.ShaderProgram defaultProgram = null; + private static ShaderProgram defaultProgram = null; /** * 网格GPU资源缓存,管理已上传到GPU的网格数据 @@ -183,7 +179,7 @@ public final class ModelRender { */ public static boolean renderLightPositions = true; - // ================== 内部类:ShaderSources.ShaderProgram ================== + // ================== 内部类:ShaderProgram ================== // ================== 内部类:MeshGLResources ================== @@ -216,6 +212,10 @@ public final class ModelRender { try { compileDefaultShader(); + + // 初始化所有非默认着色器的基础信息 + initNonDefaultShaders(); + } catch (RuntimeException ex) { logger.error("Failed to compile default shader: {}", ex.getMessage()); throw ex; @@ -229,6 +229,76 @@ public final class ModelRender { logger.info("ModelRender initialized successfully"); } + /** + * 初始化所有非默认着色器的基础信息(顶点坐标等) + */ + private static void initNonDefaultShaders() { + List shaderList = ShaderManagement.getShaderList(); + if (shaderList == null || shaderList.isEmpty()) { + logger.info("No shaders found to initialize"); + return; + } + + int nonDefaultCount = 0; + for (CompleteShader shader : shaderList) { + // 跳过默认着色器,只初始化非默认的 + if (shader.isDefaultShader()) { + continue; + } + + try { + // 获取着色器程序 + ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName()); + if (program == null) { + logger.warn("Shader program not found for: {}", shader.getShaderName()); + continue; + } + + // 设置着色器的基础uniforms(主要是顶点坐标相关的) + initShaderBasicUniforms(program, shader); + nonDefaultCount++; + + logger.debug("Initialized non-default shader: {}", shader.getShaderName()); + + } catch (Exception e) { + logger.error("Failed to initialize non-default shader: {}", shader.getShaderName(), e); + } + } + + logger.info("Initialized {} non-default shaders", nonDefaultCount); + } + + /** + * 初始化着色器的基础uniforms(顶点坐标相关) + */ + private static void initShaderBasicUniforms(ShaderProgram program, CompleteShader shader) { + program.use(); + + try { + // 设置基础的变换矩阵为单位矩阵 + setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity()); + setUniformMatrix3(program, "uViewMatrix", new Matrix3f().identity()); + + // 设置投影矩阵(使用当前视口尺寸) + Matrix3f projection = buildOrthoProjection(viewportWidth, viewportHeight); + setUniformMatrix3(program, "uProjectionMatrix", projection); + + // 设置基础颜色为白色 + setUniformVec4Internal(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + + // 设置基础不透明度 + setUniformFloatInternal(program, "uOpacity", 1.0f); + + // 设置纹理单元(如果有纹理的话) + setUniformIntInternal(program, "uTexture", 0); + + RenderSystem.checkGLError("initShaderBasicUniforms_" + shader.getShaderName()); + + } finally { + program.stop(); + } + } + private static void logGLInfo() { logger.info("OpenGL Vendor: {}", RenderSystem.getVendor()); logger.info("OpenGL Renderer: {}", RenderSystem.getRenderer()); @@ -238,12 +308,11 @@ public final class ModelRender { } - private static void uploadLightsToShader(ShaderSources.ShaderProgram sp, Model2D model) { + private static void uploadLightsToShader(ShaderProgram sp, Model2D model) { List lights = model.getLights(); int idx = 0; - // 只上传已启用的光源,最多 MAX_LIGHTS(8) - for (int i = 0; i < lights.size() && idx < 8; i++) { + for (int i = 0; i < lights.size() && idx < MAX_LIGHTS; i++) { com.chuangzhou.vivid2D.render.model.util.LightSource l = lights.get(i); if (!l.isEnabled()) continue; @@ -267,7 +336,7 @@ public final class ModelRender { setUniformIntInternal(sp, "uLightCount", idx); // 禁用剩余槽位(确保 shader 中不会读取到垃圾值) - for (int i = idx; i < 8; i++) { + for (int i = idx; i < MAX_LIGHTS; i++) { setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0); setUniformVec3Internal(sp, "uLightsColor[" + i + "]", new org.joml.Vector3f(0f, 0f, 0f)); @@ -284,53 +353,48 @@ public final class ModelRender { private static void setupGLState() { + RenderSystem.checkGLError("setupGLState_start"); + RenderSystem.clearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w); + RenderSystem.checkGLError("after_clearColor"); if (enableBlending) { RenderSystem.enableBlend(); + RenderSystem.checkGLError("after_enableBlend"); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + RenderSystem.checkGLError("after_blendFunc"); } else { RenderSystem.disableBlend(); + RenderSystem.checkGLError("after_disableBlend"); } if (enableDepthTest) { RenderSystem.enableDepthTest(); + RenderSystem.checkGLError("after_enableDepthTest"); + RenderSystem.depthFunc(GL11.GL_LEQUAL); + RenderSystem.checkGLError("after_depthFunc"); + RenderSystem.depthMask(true); + RenderSystem.checkGLError("after_depthMask"); + RenderSystem.clearDepth(1.0); + RenderSystem.checkGLError("after_clearDepth"); } else { RenderSystem.disableDepthTest(); + RenderSystem.checkGLError("after_disableDepthTest"); } - RenderSystem.checkGLError("setupGLState"); + RenderSystem.checkGLError("after_disableCullFace"); } private static void compileDefaultShader() { - int vs = compileShader(GL20.GL_VERTEX_SHADER, ShaderSources.VERTEX_SHADER_SRC); - int fs = compileShader(GL20.GL_FRAGMENT_SHADER, ShaderSources.FRAGMENT_SHADER_SRC); - int prog = linkProgram(vs, fs); - ShaderSources.ShaderProgram sp = new ShaderSources.ShaderProgram(prog); - shaderMap.put("default", sp); - defaultProgram = sp; - - sp.use(); - setUniformIntInternal(sp, "uTexture", 0); - setUniformFloatInternal(sp, "uOpacity", 1.0f); - setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1)); - setUniformIntInternal(sp, "uBlendMode", 0); - setUniformIntInternal(sp, "uDebugMode", 0); - setUniformIntInternal(sp, "uLightCount", 0); // 默认没有光源 - sp.stop(); - } - - private static int compileShader(int type, String src) { - RenderSystem.assertOnRenderThread(); - return RenderSystem.compileShader(type, src); - } - - private static int linkProgram(int vs, int fs) { - RenderSystem.assertOnRenderThread(); - return RenderSystem.linkProgram(vs, fs); + ShaderManagement.compileAllShaders(); + defaultProgram = ShaderManagement.getDefaultProgram(); + if (defaultProgram == null) { + throw new RuntimeException("Failed to compile default shader: no default shader found"); + } } private static void createDefaultTexture() { @@ -348,9 +412,8 @@ public final class ModelRender { for (MeshGLResources r : meshResources.values()) r.dispose(); meshResources.clear(); - // shaders - for (ShaderSources.ShaderProgram sp : shaderMap.values()) sp.delete(); - shaderMap.clear(); + // 使用新的着色器管理系统清理着色器 + ShaderManagement.cleanup(); defaultProgram = null; // textures @@ -393,25 +456,33 @@ public final class ModelRender { return; } - defaultProgram.use(); - RenderSystem.checkGLError("after_use_program"); - - // 设置投影与视图 + // 设置投影与视图矩阵(所有着色器都需要) Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight); - setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); - setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity()); - RenderSystem.checkGLError("after_set_matrices"); + Matrix3f view = new Matrix3f().identity(); - // 添加光源数据上传 + // 1. 首先设置默认着色器 + defaultProgram.use(); + RenderSystem.checkGLError("after_use_default_program"); + + // 设置默认着色器的投影与视图 + setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); + setUniformMatrix3(defaultProgram, "uViewMatrix", view); + RenderSystem.checkGLError("after_set_default_matrices"); + + // 添加光源数据上传到默认着色器 uploadLightsToShader(defaultProgram, model); RenderSystem.checkGLError("after_upload_lights"); + // 2. 设置非默认着色器的顶点坐标相关uniform + setupNonDefaultShaders(proj, view); + RenderSystem.checkGLError("after_setup_non_default_shaders"); + // 在渲染光源位置前检查 RenderSystem.checkGLError("before_render_light_positions"); renderLightPositions(model); RenderSystem.checkGLError("after_render_light_positions"); - // 递归渲染所有根部件 + // 递归渲染所有根部件(使用默认着色器) Matrix3f identity = new Matrix3f().identity(); for (ModelPart p : model.getParts()) { if (p.getParent() != null) continue; @@ -428,6 +499,55 @@ public final class ModelRender { RenderSystem.checkGLError("render_end"); } + /** + * 设置所有非默认着色器的顶点坐标相关uniform + */ + private static void setupNonDefaultShaders(Matrix3f projection, Matrix3f view) { + List shaderList = ShaderManagement.getShaderList(); + if (shaderList == null || shaderList.isEmpty()) { + return; + } + + // 保存当前绑定的着色器程序 + int currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); + + try { + for (CompleteShader shader : shaderList) { + // 跳过默认着色器 + if (shader.isDefaultShader()) { + continue; + } + + try { + // 获取着色器程序 + ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName()); + if (program == null || program.programId == 0) { + continue; + } + + program.use(); + + // 只设置顶点坐标相关的uniform + setUniformMatrix3(program, "uProjectionMatrix", projection); + setUniformMatrix3(program, "uViewMatrix", view); + + // 设置基础模型矩阵为单位矩阵 + setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity()); + + RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName()); + + } catch (Exception e) { + logger.warn("Failed to setup non-default shader: {}", shader.getShaderName(), e); + } + } + } finally { + // 恢复之前绑定的着色器程序 + if (currentProgram != 0) { + GL20.glUseProgram(currentProgram); + } + } + } + private static void renderLightPositions(Model2D model) { if (!renderLightPositions) return; // 设置灯泡颜色为光源的颜色 @@ -658,39 +778,39 @@ public final class ModelRender { } // ================== uniform 设置辅助(内部使用,确保 program 已绑定) ================== - private static void setUniformIntInternal(ShaderSources.ShaderProgram sp, String name, int value) { + private static void setUniformIntInternal(ShaderProgram sp, String name, int value) { int loc = sp.getUniformLocation(name); if (loc != -1) RenderSystem.uniform1i(loc, value); } - private static void setUniformVec3Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector3f vec) { + private static void setUniformVec3Internal(ShaderProgram sp, String name, org.joml.Vector3f vec) { int loc = sp.getUniformLocation(name); if (loc != -1) RenderSystem.uniform3f(loc, vec); } - private static void setUniformVec2Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector2f vec) { + private static void setUniformVec2Internal(ShaderProgram sp, String name, org.joml.Vector2f vec) { int loc = sp.getUniformLocation(name); if (loc != -1) RenderSystem.uniform2f(loc, vec); } - private static void setUniformFloatInternal(ShaderSources.ShaderProgram sp, String name, float value) { + private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) { int loc = sp.getUniformLocation(name); if (loc != -1) RenderSystem.uniform1f(loc, value); } - private static void setUniformVec4Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector4f vec) { + private static void setUniformVec4Internal(ShaderProgram sp, String name, org.joml.Vector4f vec) { int loc = sp.getUniformLocation(name); if (loc != -1) RenderSystem.uniform4f(loc, vec); } - private static void setUniformMatrix3(ShaderSources.ShaderProgram sp, String name, org.joml.Matrix3f m) { + private static void setUniformMatrix3(ShaderProgram sp, String name, org.joml.Matrix3f m) { int loc = sp.getUniformLocation(name); if (loc == -1) return; RenderSystem.uniformMatrix3(loc, m); } // ================== 部件属性 ================== - private static void setPartUniforms(ShaderSources.ShaderProgram sp, ModelPart part) { + private static void setPartUniforms(ShaderProgram sp, ModelPart part) { setUniformFloatInternal(sp, "uOpacity", part.getOpacity()); int blend = 0; ModelPart.BlendMode bm = part.getBlendMode(); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java new file mode 100644 index 0000000..618954e --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java @@ -0,0 +1,30 @@ +package com.chuangzhou.vivid2D.render.awt; + +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; + +/** + * 模型点击事件监听器接口 + * + * @author tzdwindows 7 + */ +public interface ModelClickListener { + /** + * 当点击模型时触发 + * @param mesh 被点击的网格,如果点击在空白处则为 null + * @param modelX 模型坐标系中的 X 坐标 + * @param modelY 模型坐标系中的 Y 坐标 + * @param screenX 屏幕坐标系中的 X 坐标 + * @param screenY 屏幕坐标系中的 Y 坐标 + */ + void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY); + + /** + * 当鼠标在模型上移动时触发 + * @param mesh 鼠标下方的网格,如果不在任何网格上则为 null + * @param modelX 模型坐标系中的 X 坐标 + * @param modelY 模型坐标系中的 Y 坐标 + * @param screenX 屏幕坐标系中的 X 坐标 + * @param screenY 屏幕坐标系中的 Y 坐标 + */ + default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {} +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelGLPanel.java deleted file mode 100644 index fcdf305..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelGLPanel.java +++ /dev/null @@ -1,646 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.lwjgl.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.nio.IntBuffer; -import java.nio.ByteOrder; -import java.util.concurrent.locks.LockSupport; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; - -/** - * vivid2D 模型的 Java 渲染面板 - * - *

该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能, - * 包含基本的 2D 图形绘制、模型显示和交互操作。

- * - *

具体使用示例请参考:{@code com.chuangzhou.vivid2D.test.TestModelGLPanel}

- * - * @author tzdwindows - * @version 1.0 - * @since 2025-10-13 - * @see com.chuangzhou.vivid2D.test.TestModelGLPanel - */ -public class ModelGLPanel extends JPanel { - private static final Logger logger = LoggerFactory.getLogger(ModelGLPanel.class); - 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; - - /** - * 构造函数:使用模型路径 - */ - public ModelGLPanel(String modelPath, int width, int height) { - this.modelPath = modelPath; - this.width = width; - this.height = height; - initialize(); - } - - /** - * 构造函数:使用已加载模型 - */ - public ModelGLPanel(Model2D model, int width, int height) { - this.modelPath = null; - this.width = width; - this.height = height; - this.modelRef.set(model); - initialize(); - } - - private void initialize() { - setLayout(new BorderLayout()); - setPreferredSize(new Dimension(width, height)); - - // 初始化 GLFW - if (!GLFW.glfwInit()) { - throw new RuntimeException("无法初始化 GLFW"); - } - - // 创建渲染线程 - startRendering(); - - this.addComponentListener(new java.awt.event.ComponentAdapter() { - @Override - public void componentResized(java.awt.event.ComponentEvent e) { - int w = getWidth(); - int h = getHeight(); - // 忽略无效尺寸或未变化的情况 - if (w <= 0 || h <= 0) return; - if (w == ModelGLPanel.this.width && h == ModelGLPanel.this.height) return; - // 调用本类的 resize 方法(会在 GL 上下文线程中执行实际的 GL 更新) - - ModelGLPanel.this.resize(w, h); - } - }); - } - - /** - * 创建离屏 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(); - - // 使用 RenderSystem 初始化 OpenGL 状态 - RenderSystem.beginInitialization(); - RenderSystem.initRenderThread(); - - RenderSystem.pixelStore(RenderSystem.GL_PACK_ALIGNMENT, 1); - - // 初始化 OpenGL 状态 - RenderSystem.enableDepthTest(); - - // 检查是否支持多重采样 - if (RenderSystem.isExtensionSupported("GL_ARB_multisample")) { - RenderSystem.enable(RenderSystem.GL_MULTISAMPLE); - logger.info("多重采样已启用"); - } else { - logger.info("不支持多重采样,跳过启用"); - } - - 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.initialize(); - - RenderSystem.finishInitialization(); - - // 在正确的上下文中加载模型(可能会耗时) - loadModelInContext(); - - // 标记上下文已初始化并完成通知(只 complete 一次) - contextInitialized = true; - contextReady.complete(null); - } - - /** - * 在 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(); - } - } - - /** - * 创建错误模型作为回退 - */ - private void createErrorModel() { - try { - // 这里可以创建一个简单的默认模型 - // 或者保持 modelRef 为 null,在渲染时显示错误信息 - System.out.println("使用默认错误模型"); - } catch (Exception e) { - System.err.println("创建错误模型失败: " + e.getMessage()); - } - } - - /** - * 启动渲染线程 - */ - 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(); - - 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); - - Graphics2D g2d = (Graphics2D) g.create(); - try { - // 选择要绘制的图像:优先 currentFrame(最新),其不存在则用 lastFrame(最后成功帧) - BufferedImage imgToDraw = currentFrame != null ? currentFrame : lastFrame; - - 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); - } - } finally { - g2d.dispose(); - } - } - - // ================== 新增: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(() -> { - modelRef.set(model); - logger.info("模型已更新"); - }); - } - - /** - * 获取当前渲染的模型 - */ - public Model2D getModel() { - return modelRef.get(); - } - - /** - * 重新设置面板大小 - * - * 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲, - * 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。 - */ - public void resize(int newWidth, int newHeight) { - // 更新 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; - - // 停止任务执行器 - 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 资源已清理"); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java new file mode 100644 index 0000000..afcc738 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -0,0 +1,988 @@ +package com.chuangzhou.vivid2D.render.awt; + +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.lwjgl.system.MemoryUtil; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.plaf.basic.BasicListUI; +import java.awt.*; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * ModelLayerPanel(完整实现) + * + * - 列表显示“从上到下”的图层(listModel[0] 为最上层) + * - 在任何修改后都会把 model.parts 同步为列表的反序(保证渲染顺序与 UI 一致) + * - 支持添加空层 / 从文件创建带贴图的层(在有 renderPanel 时在 GL 线程使用 Texture.createFromFile) + * - 支持为选中部件绑定贴图、创建透明图层 + * - 支持拖拽重排、上下按钮移动,并在重排后正确恢复选中与不触发滑块事件 + * + * 使用: + * new ModelLayerPanel(model, optionalModelRenderPanel) + */ +public class ModelLayerPanel extends JPanel { + private Model2D model; + private ModelRenderPanel renderPanel; // 可选 GL 渲染面板(用于在其 GL 上下文创建纹理) + + private DefaultListModel listModel; + private JList layerList; + + private JButton addButton; + private JButton removeButton; + private JButton upButton; + private JButton downButton; + private JButton bindTextureButton; + + private JSlider opacitySlider; + private JLabel opacityValueLabel; + + // 程序性设置滑块时忽略事件,避免错误写回 + private volatile boolean ignoreSliderEvents = false; + + public ModelLayerPanel(Model2D model) { + this(model, null); + } + + public ModelLayerPanel(Model2D model, ModelRenderPanel renderPanel) { + this.model = model; + this.renderPanel = renderPanel; + initComponents(); + reloadFromModel(); + } + + public void setModel(Model2D model) { + this.model = model; + reloadFromModel(); + } + + public void setRenderPanel(ModelRenderPanel panel) { + this.renderPanel = panel; + } + + private void initComponents() { + setLayout(new BorderLayout()); + listModel = new DefaultListModel<>(); + layerList = new JList<>(listModel); + layerList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + layerList.setCellRenderer(new LayerCellRenderer()); + layerList.setDragEnabled(true); + layerList.setTransferHandler(new LayerReorderTransferHandler()); + layerList.setDropMode(DropMode.INSERT); + + // 双击重命名 + layerList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int idx = layerList.locationToIndex(e.getPoint()); + if (idx >= 0) { + ModelPart part = listModel.get(idx); + String newName = JOptionPane.showInputDialog( + ModelLayerPanel.this, + "输入新名称:", + part.getName() + ); + if (newName != null && !newName.trim().isEmpty()) { + renamePart(part, newName); + reloadFromModel(); + } + } + } + } + }); + + // 选择变更 -> 更新滑块显示(但程序性更新时要忽略事件) + layerList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + ModelPart sel = layerList.getSelectedValue(); + if (sel != null) { + float op = 1.0f; + try { + Method gm = sel.getClass().getMethod("getOpacity"); + Object v = gm.invoke(sel); + if (v instanceof Float) op = (Float) v; + } catch (Exception ex) { + try { + Field f = sel.getClass().getDeclaredField("opacity"); + f.setAccessible(true); + Object v = f.get(sel); + if (v instanceof Float) op = (Float) v; + } catch (Exception ignored) {} + } + int val = Math.round(op * 100); + + // 程序性更新滑块时阻止 ChangeListener 响应 + ignoreSliderEvents = true; + try { + opacitySlider.setValue(val); + opacityValueLabel.setText(val + "%"); + } finally { + ignoreSliderEvents = false; + } + + removeButton.setEnabled(true); + upButton.setEnabled(true); + downButton.setEnabled(true); + bindTextureButton.setEnabled(true); + } else { + removeButton.setEnabled(false); + upButton.setEnabled(false); + downButton.setEnabled(false); + bindTextureButton.setEnabled(false); + } + } + }); + + JScrollPane scroll = new JScrollPane(layerList); + add(scroll, BorderLayout.CENTER); + + // 按钮区 + JPanel controls = new JPanel(new BorderLayout()); + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); + + addButton = new JButton("+"); + addButton.setToolTipText("添加图层(点击箭头选择创建方式)"); + JPopupMenu addMenu = new JPopupMenu(); + JMenuItem addBlank = new JMenuItem("创建空图层 (无贴图)"); + JMenuItem addWithTexture = new JMenuItem("从文件选择贴图并创建图层"); + JMenuItem addTransparent = new JMenuItem("创建透明贴图图层"); + addMenu.add(addBlank); + addMenu.add(addWithTexture); + addMenu.add(addTransparent); + addButton.addActionListener(e -> addMenu.show(addButton, 0, addButton.getHeight())); + + addBlank.addActionListener(e -> createEmptyPart()); + addWithTexture.addActionListener(e -> createPartWithTextureFromFile()); + addTransparent.addActionListener(e -> createPartWithTransparentTexture()); + + removeButton = new JButton("-"); + removeButton.setToolTipText("删除选中图层"); + removeButton.addActionListener(e -> onRemoveLayer()); + removeButton.setEnabled(false); + + upButton = new JButton("\u25B2"); + upButton.setToolTipText("上移图层"); + upButton.addActionListener(e -> moveSelectedUp()); + upButton.setEnabled(false); + + downButton = new JButton("\u25BC"); + downButton.setToolTipText("下移图层"); + downButton.addActionListener(e -> moveSelectedDown()); + downButton.setEnabled(false); + + bindTextureButton = new JButton("绑定贴图"); + bindTextureButton.setToolTipText("为选中部件绑定贴图(选择文件)"); + bindTextureButton.addActionListener(e -> bindTextureToSelectedPart()); + bindTextureButton.setEnabled(false); + + btnPanel.add(addButton); + btnPanel.add(removeButton); + btnPanel.add(upButton); + btnPanel.add(downButton); + btnPanel.add(bindTextureButton); + controls.add(btnPanel, BorderLayout.NORTH); + + // 不透明度面板 + JPanel opacityPanel = new JPanel(new BorderLayout(6, 6)); + opacityPanel.setBorder(BorderFactory.createTitledBorder("不透明度")); + opacitySlider = new JSlider(0, 100, 100); + opacityValueLabel = new JLabel("100%"); + opacitySlider.addChangeListener(e -> { + if (ignoreSliderEvents) return; + + ModelPart sel = layerList.getSelectedValue(); + int val = opacitySlider.getValue(); + opacityValueLabel.setText(val + "%"); + if (sel != null) { + try { + Method sm = sel.getClass().getMethod("setOpacity", float.class); + sm.invoke(sel, val / 100.0f); + } catch (Exception ex) { + try { + Field f = sel.getClass().getDeclaredField("opacity"); + f.setAccessible(true); + f.setFloat(sel, val / 100.0f); + } catch (Exception ignored) {} + } + if (model != null) model.markNeedsUpdate(); + layerList.repaint(); + } + }); + + opacityPanel.add(opacitySlider, BorderLayout.CENTER); + opacityPanel.add(opacityValueLabel, BorderLayout.EAST); + + controls.add(opacityPanel, BorderLayout.SOUTH); + + add(controls, BorderLayout.SOUTH); + } + + // ============== 部件创建 / 贴图绑定 ============== + + private void createEmptyPart() { + String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层"); + if (name == null || name.trim().isEmpty()) return; + + // 使用 model.createPart 创建(会加入 model.parts 的末尾 -> 视为底层) + ModelPart part = model.createPart(name); + model.markNeedsUpdate(); + + // reload 并把新创建的部件选中(列表显示从上到下,所以新部件在底部/最后,需要在 reload 后定位) + reloadFromModel(); + selectPart(part); + } + + private void createPartWithTextureFromFile() { + JFileChooser chooser = new JFileChooser(); + int r = chooser.showOpenDialog(this); + if (r != JFileChooser.APPROVE_OPTION) return; + File f = chooser.getSelectedFile(); + try { + BufferedImage img = ImageIO.read(f); + if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath()); + String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName()); + if (name == null || name.trim().isEmpty()) name = f.getName(); + + // 先创建部件与 Mesh(基于图片尺寸) + ModelPart part = model.createPart(name); + Mesh2D mesh = createQuadForImage(img, name + "_mesh"); + part.addMesh(mesh); + + // 在有 GL 上下文时优先使用 Texture.createFromFile 在 GL 线程创建 + if (renderPanel != null) { + final String texName = name + "_tex"; + final String filePath = f.getAbsolutePath(); + renderPanel.executeInGLContext(() -> { + try { + Texture texture = Texture.createFromFile(texName, filePath); + if (texture != null) { + // 找到实际被加入到 part 的 mesh(通常为最后一个) + java.util.List partMeshes = part.getMeshes(); + Mesh2D actualMesh = null; + if (partMeshes != null && !partMeshes.isEmpty()) { + actualMesh = partMeshes.get(partMeshes.size() - 1); + } + + if (actualMesh != null) { + actualMesh.setTexture(texture); + } else { + // 兜底:如果没找到(极少数情况),仍然设置在原始 mesh 上以避免丢失 + mesh.setTexture(texture); + } + + model.addTexture(texture); + model.markNeedsUpdate(); + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + }); + } else { + // 无 GL:尝试内存构造 + Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex"); + if (memTex != null) { + mesh.setTexture(memTex); + model.addTexture(memTex); + model.markNeedsUpdate(); + } else { + System.err.println("未找到可用的 GL 上下文,也无法创建内存纹理: " + f.getAbsolutePath()); + } + } + + reloadFromModel(); + selectPart(part); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } + } + + private void createPartWithTransparentTexture() { + String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层"); + if (name == null || name.trim().isEmpty()) return; + int w = 128, h = 128; + try { + String wh = JOptionPane.showInputDialog(this, "输入尺寸(宽x高,例如 128x128)或留空使用 128x128:", "128x128"); + if (wh != null && wh.contains("x")) { + String[] sp = wh.split("x"); + w = Math.max(1, Integer.parseInt(sp[0].trim())); + h = Math.max(1, Integer.parseInt(sp[1].trim())); + } + } catch (Exception ignored) {} + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + + ModelPart part = model.createPart(name); + Mesh2D mesh = createQuadForImage(img, name + "_mesh"); + part.addMesh(mesh); + + if (renderPanel != null) { + renderPanel.executeInGLContext(() -> { + try { + Texture tex = createTextureFromBufferedImageInGL(img, name + "_tex"); + if (tex != null) { + mesh.setTexture(tex); + model.addTexture(tex); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } else { + Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex"); + if (memTex != null) { + mesh.setTexture(memTex); + model.addTexture(memTex); + } + } + + model.markNeedsUpdate(); + reloadFromModel(); + selectPart(part); + } + + private void bindTextureToSelectedPart() { + ModelPart sel = layerList.getSelectedValue(); + if (sel == null) return; + + JFileChooser chooser = new JFileChooser(); + int r = chooser.showOpenDialog(this); + if (r != JFileChooser.APPROVE_OPTION) return; + File f = chooser.getSelectedFile(); + try { + BufferedImage img = null; + try { img = ImageIO.read(f); } catch (Exception ignored) {} + + // 获取第一个 mesh + Mesh2D targetMesh = null; + try { + Method getMeshes = sel.getClass().getMethod("getMeshes"); + Object list = getMeshes.invoke(sel); + if (list instanceof List) { + List meshes = (List) list; + if (!meshes.isEmpty() && meshes.get(0) instanceof Mesh2D) { + targetMesh = (Mesh2D) meshes.get(0); + } + } + } catch (Exception ignored) {} + + if (targetMesh == null) { + if (img == null) { + img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); + } + targetMesh = createQuadForImage(img, sel.getName() + "_mesh"); + try { + sel.getClass().getMethod("addMesh", Mesh2D.class).invoke(sel, targetMesh); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + final Mesh2D meshToBind = targetMesh; + final String filePath = f.getAbsolutePath(); + final String texName = sel.getName() + "_tex"; + + if (renderPanel != null) { + renderPanel.executeInGLContext(() -> { + try { + Texture texture = Texture.createFromFile(texName, filePath); + if (texture != null) { + meshToBind.setTexture(texture); + model.addTexture(texture); + model.markNeedsUpdate(); + } else { + System.err.println("Texture.createFromFile 返回 null: " + filePath); + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + }); + } else { + if (img == null) img = ImageIO.read(f); + Texture mem = tryCreateTextureFromImageMemory(img, texName); + if (mem != null) { + meshToBind.setTexture(mem); + model.addTexture(mem); + model.markNeedsUpdate(); + } else { + System.err.println("无法在无 GL 上下文中创建纹理: " + filePath); + } + } + + reloadFromModel(); + selectPart(sel); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } + } + + // ============== 辅助:Mesh/Texture 创建 ============== + + private Mesh2D createQuadForImage(BufferedImage img, String meshName) { + float w = img.getWidth(); + float h = img.getHeight(); + try { + Method m = Mesh2D.class.getMethod("createQuad", String.class, float.class, float.class); + Object o = m.invoke(null, meshName, w, h); + if (o instanceof Mesh2D) return (Mesh2D) o; + } catch (Exception ignored) {} + + try { + Constructor cons = null; + for (Constructor c : Mesh2D.class.getDeclaredConstructors()) { + Class[] params = c.getParameterTypes(); + if (params.length >= 4 && params[0] == String.class) { + cons = c; + break; + } + } + if (cons != null) { + cons.setAccessible(true); + float[] vertices = new float[]{ + -w / 2f, -h / 2f, + w / 2f, -h / 2f, + w / 2f, h / 2f, + -w / 2f, h / 2f + }; + float[] uvs = new float[]{ + 0f, 1f, + 1f, 1f, + 1f, 0f, + 0f, 0f + }; + int[] indices = new int[]{0, 1, 2, 2, 3, 0}; + Object meshObj = cons.newInstance(meshName, vertices, uvs, indices); + if (meshObj instanceof Mesh2D) return (Mesh2D) meshObj; + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + throw new RuntimeException("无法创建 Mesh2D(没有合适的工厂或构造函数)"); + } + + /** + * 在 GL 上下文中创建并上传 Texture(返回已上传的 Texture) + * 该方法仅在 renderPanel 可用时被调用(renderPanel.executeInGLContext) + */ + private Texture createTextureFromBufferedImageInGL(BufferedImage img, String texName) { + if (renderPanel == null) throw new IllegalStateException("需要 renderPanel 才能在 GL 上下文创建纹理"); + + try { + return renderPanel.executeInGLContext((Callable) () -> { + // 静态工厂尝试 + try { + Method factory = findStaticMethod(Texture.class, "createFromBufferedImage", BufferedImage.class); + if (factory == null) factory = findStaticMethod(Texture.class, "createFromImage", BufferedImage.class); + if (factory != null) { + Object texObj = factory.invoke(null, img); + if (texObj instanceof Texture) { + tryCallTextureUpload((Texture) texObj); + return (Texture) texObj; + } + } + } catch (Throwable ignored) {} + + // 构造 ByteBuffer 并尝试构造器 + try { + int w = img.getWidth(); + int h = img.getHeight(); + ByteBuffer buf = imageToRGBAByteBuffer(img); + Constructor suit = null; + for (Constructor c : Texture.class.getDeclaredConstructors()) { + Class[] ps = c.getParameterTypes(); + if (ps.length >= 4 && ps[0] == String.class) { + suit = c; + break; + } + } + if (suit != null) { + suit.setAccessible(true); + Object texObj = null; + Class[] ps = suit.getParameterTypes(); + if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { + Object formatEnum = null; + try { + Class formatCls = null; + for (Class inner : Texture.class.getDeclaredClasses()) { + if (inner.getSimpleName().toLowerCase().contains("format")) { + formatCls = inner; + break; + } + } + if (formatCls != null) { + for (Field f : formatCls.getFields()) { + if (f.getName().toUpperCase().contains("RGBA")) { + formatEnum = f.get(null); + break; + } + } + } + } catch (Throwable ignored) {} + if (formatEnum != null) { + try { + texObj = suit.newInstance(texName, w, h, formatEnum, buf); + } catch (Exception ignored) {} + } + } + if (texObj == null) { + try { texObj = suit.newInstance(texName, img.getWidth(), img.getHeight(), buf); } catch (Exception ignored) {} + } + if (texObj instanceof Texture) { + tryCallTextureUpload((Texture) texObj); + return (Texture) texObj; + } + } + } catch (Throwable t) { + t.printStackTrace(); + } + + throw new RuntimeException("无法在 GL 上下文中创建 Texture(缺少兼容的构造器/工厂)"); + }).get(); + } catch (Exception e) { + throw new RuntimeException("创建 GL 纹理失败: " + e.getMessage(), e); + } + } + + private Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) { + try { + int w = img.getWidth(); + int h = img.getHeight(); + ByteBuffer buf = imageToRGBAByteBuffer(img); + + Constructor suit = null; + for (Constructor c : Texture.class.getDeclaredConstructors()) { + Class[] ps = c.getParameterTypes(); + if (ps.length >= 4 && ps[0] == String.class) { + suit = c; + break; + } + } + if (suit != null) { + suit.setAccessible(true); + Object texObj = null; + Class[] ps = suit.getParameterTypes(); + if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { + Object formatEnum = null; + try { + Class formatCls = null; + for (Class inner : Texture.class.getDeclaredClasses()) { + if (inner.getSimpleName().toLowerCase().contains("format")) { + formatCls = inner; + break; + } + } + if (formatCls != null) { + for (Field f : formatCls.getFields()) { + if (f.getName().toUpperCase().contains("RGBA")) { + formatEnum = f.get(null); + break; + } + } + } + } catch (Throwable ignored) {} + if (formatEnum != null) { + try { texObj = suit.newInstance(texName, w, h, formatEnum, buf); } catch (Throwable ignored) {} + } + } + if (texObj == null) { + try { texObj = suit.newInstance(texName, w, h, buf); } catch (Throwable ignored) {} + } + if (texObj instanceof Texture) return (Texture) texObj; + } + } catch (Throwable t) { + t.printStackTrace(); + } + return null; + } + + private ByteBuffer imageToRGBAByteBuffer(BufferedImage img) { + final int w = img.getWidth(); + final int h = img.getHeight(); + final int[] pixels = new int[w * h]; + img.getRGB(0, 0, w, h, pixels, 0, w); + ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder()); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int argb = pixels[y * w + x]; + int a = (argb >> 24) & 0xFF; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = (argb) & 0xFF; + buffer.put((byte) r); + buffer.put((byte) g); + buffer.put((byte) b); + buffer.put((byte) a); + } + } + buffer.flip(); + return buffer; + } + + private void tryCallTextureUpload(Texture tex) { + if (tex == null) return; + String[] candidates = new String[]{"upload", "uploadToGPU", "initGL", "initTexture", "createGLTexture", "bind"}; + for (String name : candidates) { + try { + Method m = tex.getClass().getMethod(name); + if (m != null) { + m.invoke(tex); + return; + } + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + private static Method findStaticMethod(Class cls, String name, Class param) { + try { + Method m = cls.getMethod(name, param); + if (Modifier.isStatic(m.getModifiers())) return m; + } catch (Exception ignored) {} + try { + Method m = cls.getDeclaredMethod(name, param); + m.setAccessible(true); + if (Modifier.isStatic(m.getModifiers())) return m; + } catch (Exception ignored) {} + return null; + } + + // ============== 列表操作(核心:保持 model.parts 与 listModel 一致) ============== + + private void onRemoveLayer() { + ModelPart sel = layerList.getSelectedValue(); + if (sel == null) return; + int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION); + if (r != JOptionPane.YES_OPTION) return; + + try { + List parts = getModelPartsList(); + if (parts != null) parts.remove(sel); + Map partMap = getModelPartMap(); + if (partMap != null) partMap.remove(sel.getName()); + try { + ModelPart root = model.getRootPart(); + if (root != null && root == sel) { + List remaining = getModelPartsList(); + if (remaining != null && !remaining.isEmpty()) { + model.setRootPart(remaining.get(0)); + } else { + model.setRootPart(null); + } + } + } catch (Exception ignored) {} + model.markNeedsUpdate(); + reloadFromModel(); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } + } + + private void moveSelectedUp() { + int idx = layerList.getSelectedIndex(); + if (idx <= 0) return; + performVisualReorder(idx, idx - 1); + } + + private void moveSelectedDown() { + int idx = layerList.getSelectedIndex(); + if (idx < 0 || idx >= listModel.getSize() - 1) return; + performVisualReorder(idx, idx + 1); + } + + /** + * 重新加载列表(列表显示顺序为从上到下) + * 列表中的顺序与用户看到的顺序一致(listModel[0] = 最上层) + */ + private void reloadFromModel() { + // 记录对象选中以便恢复 + ModelPart selected = layerList.getSelectedValue(); + + listModel.clear(); + if (model == null) return; + try { + List parts = model.getParts(); + // 我们希望列表从上到下显示,因此把 model.parts 反序加入 listModel + if (parts != null) { + for (int i = parts.size() - 1; i >= 0; i--) { + listModel.addElement(parts.get(i)); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + // 恢复选中(按对象引用) + if (selected != null) { + for (int i = 0; i < listModel.getSize(); i++) { + if (listModel.get(i) == selected) { + layerList.setSelectedIndex(i); + break; + } + } + } + } + + /** + * 执行视觉(列表)层级的重排:先在 visualList 上进行操作,然后把 model.parts 重建为 visualList 的反序, + * 保证 model.parts 与 UI 显示顺序一致(rendering 与 UI 保持一致)。 + * + * @param visualFrom 源 visual 索引(listModel) + * @param visualTo 目标 visual 索引(listModel) + */ + private void performVisualReorder(int visualFrom, int visualTo) { + if (model == null) return; + try { + int size = listModel.getSize(); + if (visualFrom < 0 || visualFrom >= size) return; + if (visualTo < 0) visualTo = 0; + if (visualTo > size - 1) visualTo = size - 1; + + // 构造新的视觉顺序(arraylist) + List visual = new ArrayList<>(size); + for (int i = 0; i < size; i++) visual.add(listModel.get(i)); + + // 移动元素 + ModelPart moved = visual.remove(visualFrom); + visual.add(visualTo, moved); + + // 更新 listModel(程序性更新,期间设置 ignoreSliderEvents 防止滑块回写) + ignoreSliderEvents = true; + try { + listModel.clear(); + for (ModelPart p : visual) listModel.addElement(p); + } finally { + ignoreSliderEvents = false; + } + + // 根据视觉顺序重建 model.parts(model.parts = reverse(visual)) + List newModelParts = new ArrayList<>(visual.size()); + for (int i = visual.size() - 1; i >= 0; i--) newModelParts.add(visual.get(i)); + // 替换 model.parts 字段(通过反射) + replaceModelPartsList(newModelParts); + + model.markNeedsUpdate(); + + // 恢复选中:按对象引用找到索引 + selectPart(moved); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + // ============== 反射读写 Model2D 内部 ============== + + @SuppressWarnings("unchecked") + private List getModelPartsList() { + if (model == null) return null; + try { + Field partsField = model.getClass().getDeclaredField("parts"); + partsField.setAccessible(true); + Object o = partsField.get(model); + if (o instanceof List) return (List) o; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @SuppressWarnings("unchecked") + private Map getModelPartMap() { + if (model == null) return null; + try { + Field mapField = model.getClass().getDeclaredField("partMap"); + mapField.setAccessible(true); + Object o = mapField.get(model); + if (o instanceof Map) return (Map) o; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 用新的 parts 列表替换 model.parts(保持同一 List 对象或直接 set) + */ + private void replaceModelPartsList(List newParts) { + if (model == null) return; + try { + Field partsField = model.getClass().getDeclaredField("parts"); + partsField.setAccessible(true); + Object old = partsField.get(model); + if (old instanceof java.util.List) { + @SuppressWarnings("rawtypes") + java.util.List rawList = (java.util.List) old; + rawList.clear(); + rawList.addAll(newParts); + } else { + partsField.set(model, newParts); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // ============== 列表渲染/拖拽辅助 ============== + + private class LayerCellRenderer extends JPanel implements ListCellRenderer { + private JCheckBox visibleBox = new JCheckBox(); + private JLabel nameLabel = new JLabel(); + private JLabel opacityLabel = new JLabel(); + + LayerCellRenderer() { + setLayout(new BorderLayout(6, 6)); + JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + left.setOpaque(false); + visibleBox.setOpaque(false); + left.add(visibleBox); + left.add(nameLabel); + add(left, BorderLayout.CENTER); + add(opacityLabel, BorderLayout.EAST); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + int idx = layerList.locationToIndex(e.getPoint()); + if (idx >= 0) { + ModelPart part = listModel.get(idx); + Rectangle cbBounds = new Rectangle(0, 0, 20, getHeight()); + if (cbBounds.contains(e.getPoint())) { + boolean newVis = !part.isVisible(); + part.setVisible(newVis); + if (model != null) model.markNeedsUpdate(); + reloadFromModel(); + } else { + layerList.setSelectedIndex(idx); + } + } + } + }); + } + + @Override + public Component getListCellRendererComponent(JList list, ModelPart value, int index, boolean isSelected, boolean cellHasFocus) { + nameLabel.setText(value.getName()); + try { + Method gm = value.getClass().getMethod("getOpacity"); + Object v = gm.invoke(value); + if (v instanceof Float) opacityLabel.setText(((int) (((Float) v) * 100)) + "%"); + } catch (Exception ex) { + try { + Field f = value.getClass().getDeclaredField("opacity"); + f.setAccessible(true); + Object v = f.get(value); + if (v instanceof Float) opacityLabel.setText(Math.round((Float) v * 100) + "%"); + else opacityLabel.setText(""); + } catch (Exception ignored) { + opacityLabel.setText(""); + } + } + visibleBox.setSelected(value.isVisible()); + + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + nameLabel.setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + nameLabel.setForeground(list.getForeground()); + } + setOpaque(true); + setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + return this; + } + } + + private class LayerReorderTransferHandler extends TransferHandler { + @Override + protected Transferable createTransferable(JComponent c) { + int src = layerList.getSelectedIndex(); + if (src < 0) return null; + return new StringSelection(Integer.toString(src)); + } + + @Override + public int getSourceActions(JComponent c) { return MOVE; } + + @Override + public boolean canImport(TransferSupport support) { + return support.isDrop() && support.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.stringFlavor); + } + + @Override + public boolean importData(TransferSupport support) { + if (!canImport(support)) return false; + try { + javax.swing.JList.DropLocation dl = (javax.swing.JList.DropLocation) support.getDropLocation(); + int dropIndex = dl.getIndex(); + String s = (String) support.getTransferable().getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor); + int srcIdx = Integer.parseInt(s); + if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false; + performVisualReorder(srcIdx, dropIndex); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + } + return false; + } + } + + // ============== 小工具 ============== + + private void selectPart(ModelPart part) { + if (part == null) return; + for (int i = 0; i < listModel.getSize(); i++) { + if (listModel.get(i) == part) { + layerList.setSelectedIndex(i); + layerList.ensureIndexIsVisible(i); + return; + } + } + } + + private void renamePart(ModelPart part, String newName) { + if (part == null) return; + try { + try { + Method m = part.getClass().getMethod("setName", String.class); + m.invoke(part, newName); + } catch (NoSuchMethodException ex) { + Field nameField = part.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + String oldName = (String) nameField.get(part); + nameField.set(part, newName); + Map partMap = getModelPartMap(); + if (partMap != null) { + partMap.remove(oldName); + partMap.put(newName, part); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java new file mode 100644 index 0000000..3da7f49 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -0,0 +1,1319 @@ +package com.chuangzhou.vivid2D.render.awt; + +import com.chuangzhou.vivid2D.render.ModelRender; +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.RenderSystem; +import org.joml.Vector2f; +import org.lwjgl.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.event.*; +import java.nio.IntBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.locks.LockSupport; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +/** + * vivid2D 模型的 Java 渲染面板 + * + *

该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能, + * 包含基本的 2D 图形绘制、模型显示和交互操作。

+ * + * + * @author tzdwindows 7 + * @version 1.0 + * @since 2025-10-13 + * @see com.chuangzhou.vivid2D.test.TestModelGLPanel + */ +public class ModelRenderPanel extends JPanel { + private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class); + 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; + private volatile Mesh2D selectedMesh = null; + private volatile ModelPart draggedPart = null; + private volatile float dragStartX, dragStartY; + private volatile float partStartX, partStartY; + private volatile boolean isDragging = false; + private enum DragMode { + NONE, // 无拖拽 + MOVE, // 移动部件 + RESIZE_LEFT, // 调整左边 + RESIZE_RIGHT, // 调整右边 + RESIZE_TOP, // 调整上边 + RESIZE_BOTTOM, // 调整下边 + RESIZE_TOP_LEFT, // 调整左上角 + RESIZE_TOP_RIGHT, // 调整右上角 + RESIZE_BOTTOM_LEFT, // 调整左下角 + RESIZE_BOTTOM_RIGHT // 调整右下角 + } + + // 新增:拖拽相关字段 + private volatile DragMode currentDragMode = DragMode.NONE; + private volatile float resizeStartWidth, resizeStartHeight; + private volatile float resizeStartX, resizeStartY; + private volatile boolean shiftPressed = false; + + // 新增:选择框边框厚度和角点大小 + public static final float BORDER_THICKNESS = 6.0f; + public static final float CORNER_SIZE = 12.0f; + + private volatile float partInitialScaleX = 1.0f; + private volatile 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; + + /** + * 构造函数:使用模型路径 + */ + public ModelRenderPanel(String modelPath, int width, int height) { + this.modelPath = modelPath; + this.width = width; + this.height = height; + initialize(); + } + + /** + * 构造函数:使用已加载模型 + */ + public ModelRenderPanel(Model2D model, int width, int height) { + this.modelPath = null; + this.width = width; + this.height = height; + this.modelRef.set(model); + initialize(); + } + + /** + * 添加模型点击监听器 + */ + public void addModelClickListener(ModelClickListener listener) { + clickListeners.add(listener); + } + + /** + * 移除模型点击监听器 + */ + public void removeModelClickListener(ModelClickListener listener) { + clickListeners.remove(listener); + } + + /** + * 获取当前选中的网格 + */ + public Mesh2D getSelectedMesh() { + return selectedMesh; + } + + /** + * 设置选中的网格 + */ + public void setSelectedMesh(Mesh2D mesh) { + executeInGLContext(() -> { + // 清除之前选中的网格 + if (selectedMesh != null) { + selectedMesh.setSelected(false); + } + + // 设置新的选中网格 + selectedMesh = mesh; + if (selectedMesh != null) { + selectedMesh.setSelected(true); + } + }); + } + + /** + * 获取鼠标悬停的网格 + */ + public Mesh2D getHoveredMesh() { + return hoveredMesh; + } + + private void initialize() { + setLayout(new BorderLayout()); + setPreferredSize(new Dimension(width, height)); + + // 初始化 GLFW + if (!GLFW.glfwInit()) { + throw new RuntimeException("无法初始化 GLFW"); + } + + // 添加鼠标监听器 + addMouseListeners(); + + // 创建渲染线程 + startRendering(); + + this.addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + 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); + } + }); + } + + /** + * 添加鼠标事件监听器 + */ + private void addMouseListeners() { + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + handleMouseClick(e); + } + + @Override + public void mousePressed(MouseEvent e) { + handleMousePressed(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + handleMouseReleased(e); + } + }); + + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + handleMouseMove(e); + } + + @Override + public void mouseDragged(MouseEvent e) { + handleMouseDragged(e); + } + }); + + // 新增:键盘监听器用于检测Shift键 + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SHIFT) { + shiftPressed = true; + } + } + + @Override + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SHIFT) { + shiftPressed = false; + } + } + }); + + addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + // 如果没有初始化上下文也允许修改 targetScale(视觉效果会在渲染线程平滑完成) + int notches = e.getWheelRotation(); // 向后为正 + boolean fine = (e.isShiftDown() || shiftPressed); // 支持 Shift 更精细控制 + + // 指数步长:每格滚轮缩放 ZOOM_STEP 的幂 + double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP; + if (notches > 0) { + // 滚轮下:缩小 + targetScale *= Math.pow(1.0 / step, notches); + } else if (notches < 0) { + // 滚轮上:放大 + targetScale *= Math.pow(step, -notches); + } + + // 限制范围 + targetScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, targetScale)); + } + }); + + // 确保面板可以获得焦点以接收键盘事件 + setFocusable(true); + requestFocusInWindow(); + } + + /** + * 处理鼠标按下事件(开始拖拽或调整大小) + */ + private void handleMousePressed(MouseEvent e) { + if (!contextInitialized) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + requestFocusInWindow(); + + shiftDuringDrag = e.isShiftDown(); + + executeInGLContext(() -> { + try { + // 转换屏幕坐标到模型坐标 + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + // 首先检查是否点击了选择框的调整手柄 + DragMode dragMode = checkResizeHandleHit(modelX, modelY); + + if (dragMode != DragMode.NONE && selectedMesh != null) { + // 开始调整大小 + currentDragMode = dragMode; + dragStartX = modelX; // 记录拖拽起始位置 + dragStartY = modelY; + + BoundingBox bounds = selectedMesh.getBounds(); + resizeStartWidth = bounds.getWidth(); + resizeStartHeight = bounds.getHeight(); + Vector2f center = bounds.getCenter(); + resizeStartX = center.x; + resizeStartY = center.y; + + // 记录被调整部件的当前缩放(不使用反射,直接调用 ModelPart 的 getter) + ModelPart selPart = findPartByMesh(selectedMesh); + if (selPart != null) { + partInitialScaleX = selPart.getScaleX(); + partInitialScaleY = selPart.getScaleY(); + } else { + partInitialScaleX = 1.0f; + partInitialScaleY = 1.0f; + } + + //logger.info("开始调整大小,模式: {}", dragMode); + } else { + // 检查是否点击了网格(移动操作) + Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); + if (clickedMesh != null) { + setSelectedMesh(clickedMesh); + ModelPart clickedPart = findPartByMesh(clickedMesh); + if (clickedPart != null) { + draggedPart = clickedPart; + dragStartX = modelX; + dragStartY = modelY; + partStartX = clickedPart.getPosition().x; + partStartY = clickedPart.getPosition().y; + currentDragMode = DragMode.MOVE; + //logger.info("开始移动部件: {}", clickedPart.getName()); + } + } else { + // 点击空白区域,取消选中 + setSelectedMesh(null); + currentDragMode = DragMode.NONE; + } + } + + } catch (Exception ex) { + logger.error("处理鼠标按下时出错", ex); + } + }); + } + + /** + * 检查是否点击了选择框的调整手柄 + */ + private DragMode checkResizeHandleHit(float modelX, float modelY) { + if (selectedMesh == null) return DragMode.NONE; + + selectedMesh.updateBounds(); + BoundingBox bounds = selectedMesh.getBounds(); + 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; + + //logger.info("检测阈值 - 缩放因子: {}, 边框: {}, 角点: {}", + // scaleFactor, borderThickness, cornerSize); + + // 扩展边界以包含调整手柄区域 + float expandedMinX = minX - borderThickness; + float expandedMinY = minY - borderThickness; + float expandedMaxX = maxX + borderThickness; + float expandedMaxY = maxY + borderThickness; + + // 检查是否在扩展边界内 + if (modelX < expandedMinX || modelX > expandedMaxX || + modelY < expandedMinY || modelY > 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 (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; + + return DragMode.NONE; + } + + /** + * 计算当前缩放因子(模型单位与屏幕像素的比例) + */ + private float calculateScaleFactor() { + int panelWidth = getWidth(); + int panelHeight = getHeight(); + + if (panelWidth <= 0 || panelHeight <= 0 || width <= 0 || height <= 0) { + return 1.0f; + } + + // 计算面板与离屏缓冲区的比例 + float scaleX = (float) panelWidth / width; + float scaleY = (float) panelHeight / height; + + // 基本面板缩放(保持与现有逻辑一致) + float base = Math.min(scaleX, scaleY); + + // 乘以平滑的 displayScale,使视觉上缩放与检测区域一致 + return base * displayScale; + } + + + /** + * 检查点是否在角点区域内 + */ + private boolean isPointInCorner(float x, float y, float cornerX, float cornerY, float cornerSize) { + return x >= cornerX - cornerSize && x <= cornerX + cornerSize && + y >= cornerY - cornerSize && y <= cornerY + cornerSize; + } + + /** + * 处理鼠标拖拽事件 + */ + private void handleMouseDragged(MouseEvent e) { + if (currentDragMode == DragMode.NONE) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + + executeInGLContext(() -> { + try { + if (currentDragMode == DragMode.NONE) { + logger.debug("拖拽已取消,跳过处理"); + return; + } + + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + if (currentDragMode == DragMode.MOVE) { + // 原有的移动逻辑 + handleMoveDrag(modelX, modelY); + } else { + // 新的调整大小逻辑 + handleResizeDrag(modelX, modelY); + } + + } catch (Exception ex) { + logger.error("处理鼠标拖拽时出错", ex); + } + }); + } + + /** + * 处理移动拖拽 + */ + private void handleMoveDrag(float modelX, float modelY) { + if (draggedPart == null) return; + + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + float newX = partStartX + deltaX; + float newY = partStartY + deltaY; + + draggedPart.setPosition(newX, newY); + } + + /** + * 处理调整大小拖拽 + */ + private void handleResizeDrag(float modelX, float modelY) { + if (selectedMesh == null) return; + + ModelPart selectedPart = findPartByMesh(selectedMesh); + if (selectedPart == null) return; + + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + float relScaleX = 1.0f; + float relScaleY = 1.0f; + + // 根据拖拽模式计算相对缩放比例(基于 resizeStartWidth/resizeStartHeight) + switch (currentDragMode) { + case RESIZE_LEFT: + relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); + break; + case RESIZE_RIGHT: + relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); + break; + case RESIZE_TOP: + relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + case RESIZE_BOTTOM: + relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + case RESIZE_TOP_LEFT: + relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); + relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + case RESIZE_TOP_RIGHT: + relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); + relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + case RESIZE_BOTTOM_LEFT: + relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); + relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + case RESIZE_BOTTOM_RIGHT: + relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); + relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + break; + } + + // 如果按住Shift键,等比例缩放 + if (shiftPressed || shiftDuringDrag) { + float uniform = (relScaleX + relScaleY) * 0.5f; + relScaleX = uniform; + relScaleY = uniform; + } + + // 将相对缩放转换为基于部件初始缩放的绝对缩放值(不使用反射) + float finalScaleX = partInitialScaleX * relScaleX; + float finalScaleY = partInitialScaleY * relScaleY; + + // 防止缩放变为零或负值 + finalScaleX = Math.max(finalScaleX, 0.01f); + finalScaleY = Math.max(finalScaleY, 0.01f); + + // 应用缩放(ModelPart 提供 setScale 方法) + selectedPart.setScale(finalScaleX, finalScaleY); + + // 可选:实时更新选择框基准(使得在同一次拖拽中感觉更自然) + // 注意:如果开启此项,dragStartX/dragStartY 与 resizeStartWidth/height 将被更新, + // 这样拖拽的 delta 计算会以当前帧为基础(增量更新),而不是始终相对于按下时的初始值。 + // 取消注释下面三行以启用增量模式: + // dragStartX = modelX; + // dragStartY = modelY; + // resizeStartWidth = Math.max(1e-6f, resizeStartWidth * relScaleX); resizeStartHeight = Math.max(1e-6f, resizeStartHeight * relScaleY); + } + + /** + * 处理鼠标释放事件(结束拖拽) + */ + private void handleMouseReleased(MouseEvent e) { + isDragging = false; + draggedPart = null; + currentDragMode = DragMode.NONE; + shiftDuringDrag = false; + } + + /** + * 通过网格查找对应的 ModelPart + */ + private ModelPart findPartByMesh(Mesh2D mesh) { + Model2D model = modelRef.get(); + if (model == null) return null; + for (int i = 0; i < model.getParts().size(); i++) { + ModelPart part = model.getParts().get(i); + ModelPart found = findPartByMeshRecursive(part, mesh); + if (found != null) { + return found; + } + } + return null; + } + + /** + * 递归查找包含指定网格的部件 + */ + private ModelPart findPartByMeshRecursive(ModelPart part, Mesh2D targetMesh) { + if (part == null || targetMesh == null) return null; + + // 检查当前部件的网格 + for (Mesh2D mesh : part.getMeshes()) { + if (mesh == targetMesh) { + return part; + } + } + + // 递归检查子部件 + for (ModelPart child : part.getChildren()) { + ModelPart found = findPartByMeshRecursive(child, targetMesh); + if (found != null) { + return found; + } + } + + return null; + } + + /** + * 处理鼠标点击事件 + */ + private void handleMouseClick(MouseEvent e) { + if (!contextInitialized) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + + // 在 GL 上下文线程中执行点击检测 + executeInGLContext(() -> { + try { + // 转换屏幕坐标到模型坐标 + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + logger.debug("点击位置:({}, {})", modelX, modelY); + // 检测点击的网格 + Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); + + // 更新选中状态 + setSelectedMesh(clickedMesh); + + // 触发点击事件 + for (ModelClickListener listener : clickListeners) { + try { + listener.onModelClicked(clickedMesh, modelX, modelY, screenX, screenY); + } catch (Exception ex) { + logger.error("点击事件监听器执行出错", ex); + } + } + + } catch (Exception ex) { + logger.error("处理鼠标点击时出错", ex); + } + }); + } + + /** + * 处理鼠标移动事件 + */ + private void handleMouseMove(MouseEvent e) { + if (!contextInitialized) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + + // 在 GL 上下文线程中执行悬停检测 + executeInGLContext(() -> { + try { + // 转换屏幕坐标到模型坐标 + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + // 检测悬停的网格 + Mesh2D newHoveredMesh = findMeshAtPosition(modelX, modelY); + + // 更新悬停状态 + if (newHoveredMesh != hoveredMesh) { + hoveredMesh = newHoveredMesh; + + // 触发悬停事件 + for (ModelClickListener listener : clickListeners) { + try { + listener.onModelHover(newHoveredMesh, modelX, modelY, screenX, screenY); + } catch (Exception ex) { + logger.error("悬停事件监听器执行出错", ex); + } + } + } + + } catch (Exception ex) { + logger.error("处理鼠标移动时出错", ex); + } + }); + } + + /** + * 将屏幕坐标转换为模型坐标 + */ + private float[] screenToModelCoordinates(int screenX, int screenY) { + if (width <= 0 || height <= 0) return null; + int panelWidth = getWidth(); + int panelHeight = getHeight(); + if (panelWidth <= 0 || panelHeight <= 0) return null; + float scaleX = (float) width / panelWidth; + float scaleY = (float) height / panelHeight; + float bufferX = screenX * scaleX; + float bufferY = screenY * scaleY; + float ndcX = (bufferX / width) * 2.0f - 1.0f; + float ndcY = (bufferY / height) * 2.0f - 1.0f; + float modelX = ndcX * (width / 2.0f); + float modelY = ndcY * (height / 2.0f); + return new float[]{modelX, modelY}; + } + + /** + * 在指定位置查找网格 + */ + private Mesh2D findMeshAtPosition(float modelX, float modelY) { + Model2D model = modelRef.get(); + if (model == null) { + logger.debug("模型未加载"); + return null; + } + + try { + // 使用 getParts() 获取所有部件 + 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; + } + } + } + } + } + + //logger.debug("未找到包含点的网格"); + return null; + + } catch (Exception e) { + logger.error("检测网格时出错", e); + return null; + } + } + + /** + * 获取模型的边界框 + */ + private BoundingBox getModelBounds(Model2D model) { + if (model == null) return null; + + try { + java.lang.reflect.Method getBoundsMethod = model.getClass().getMethod("getBounds"); + return (BoundingBox) getBoundsMethod.invoke(model); + } catch (Exception e) { + logger.debug("无法获取模型边界", e); + return null; + } + } + + /** + * 查找模型中的第一个网格 + */ + private Mesh2D findFirstMesh(Model2D model) { + if (model == null) return null; + + try { + java.lang.reflect.Method getMeshesMethod = model.getClass().getMethod("getMeshes"); + java.util.List meshes = (java.util.List) getMeshesMethod.invoke(model); + return meshes.isEmpty() ? null : meshes.get(0); + } catch (Exception e) { + return null; + } + } + + /** + * 创建离屏 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(); + } + } + + /** + * 创建错误模型作为回退 + */ + private void createErrorModel() { + try { + // 这里可以创建一个简单的默认模型 + // 或者保持 modelRef 为 null,在渲染时显示错误信息 + System.out.println("使用默认错误模型"); + } catch (Exception e) { + System.err.println("创建错误模型失败: " + e.getMessage()); + } + } + + /** + * 启动渲染线程 + */ + 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); + + Graphics2D g2d = (Graphics2D) g.create(); + try { + // 选择要绘制的图像:优先 currentFrame(最新),其不存在则用 lastFrame(最后成功帧) + BufferedImage imgToDraw = currentFrame != null ? currentFrame : lastFrame; + + 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); + } + } finally { + g2d.dispose(); + } + } + + // ================== 新增: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(() -> { + modelRef.set(model); + logger.info("模型已更新"); + }); + } + + /** + * 获取当前渲染的模型 + */ + public Model2D getModel() { + return modelRef.get(); + } + + /** + * 重新设置面板大小 + * + * 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲, + * 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。 + */ + public void resize(int newWidth, int newHeight) { + // 更新 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; + + // 停止任务执行器 + 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 资源已清理"); + } +} 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 6427c65..3768e85 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -2,7 +2,7 @@ package com.chuangzhou.vivid2D.render.model; import com.chuangzhou.vivid2D.render.model.util.BoundingBox; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Matrix3fUtils; +import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import org.joml.Matrix3f; import org.joml.Vector2f; @@ -35,6 +35,8 @@ public class ModelPart { private final Matrix3f localTransform; private final Matrix3f worldTransform; private final Vector2f pivot = new Vector2f(0, 0); + private float scaleX = 1.0f; + private float scaleY = 1.0f; // ==================== 渲染属性 ==================== private boolean visible; @@ -403,21 +405,6 @@ public class ModelPart { return new Vector2f(center.x + rx, center.y + ry); } - public void draw(int shaderProgram, org.joml.Matrix3f parentTransform) { - // 先确保 worldTransform 是最新的 - updateWorldTransform(parentTransform, false); - - // 绘制本节点的所有 mesh(将 worldTransform 作为 model 矩阵传入) - for (Mesh2D mesh : meshes) { - mesh.draw(shaderProgram, worldTransform); - } - - // 递归绘制子节点 - for (ModelPart child : children) { - child.draw(shaderProgram, worldTransform); - } - } - // 更新局部矩阵 private void updateLocalTransform() { float cos = (float) Math.cos(rotation); @@ -493,6 +480,60 @@ public class ModelPart { markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + + // 更新网格顶点位置 + updateMeshVertices(); + } + + /** + * 更新所有网格的顶点位置以反映当前变换 + */ + public void updateMeshVertices() { + // 确保世界变换是最新的 + if (transformDirty) { + updateLocalTransform(); + recomputeWorldTransformRecursive(); + } + + // 对每个网格应用当前的世界变换 + for (Mesh2D mesh : meshes) { + updateMeshVertices(mesh); + } + + // 递归更新子部件的网格 + for (ModelPart child : children) { + child.updateMeshVertices(); + } + } + + /** + * 更新单个网格的顶点位置 + */ + private void updateMeshVertices(Mesh2D mesh) { + if (mesh == null) return; + + // 获取原始顶点数据(局部坐标) + float[] originalVertices = mesh.getOriginalVertices(); + if (originalVertices == null || originalVertices.length == 0) { + logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName()); + return; + } + + // 确保世界变换是最新的 + if (transformDirty) { + updateLocalTransform(); + recomputeWorldTransformRecursive(); + } + + // 应用当前世界变换到每个顶点 + for (int i = 0; i < originalVertices.length; i += 2) { + Vector2f localPoint = new Vector2f(originalVertices[i], originalVertices[i + 1]); + Vector2f worldPoint = localToWorld(localPoint); + mesh.setVertex(i / 2, worldPoint.x, worldPoint.y); + } + + // 标记网格需要更新 + mesh.markDirty(); } public void setPosition(Vector2f position) { @@ -500,6 +541,9 @@ public class ModelPart { markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + + // 更新网格顶点位置 + updateMeshVertices(); } /** @@ -543,10 +587,14 @@ public class ModelPart { * 设置缩放 */ public void setScale(float sx, float sy) { + this.scaleX = sx; + this.scaleY = sy; scale.set(sx, sy); markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + + updateMeshVertices(); } public void setScale(float uniformScale) { @@ -554,6 +602,8 @@ public class ModelPart { markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + + updateMeshVertices(); } public void setScale(Vector2f scale) { @@ -561,6 +611,8 @@ public class ModelPart { markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + + updateMeshVertices(); } /** @@ -581,20 +633,46 @@ public class ModelPart { public void addMesh(Mesh2D mesh) { if (mesh == null) return; - // 确保本节点的 worldTransform 是最新的(会递归更新子节点) + // 创建独立副本,避免多个 Part 共享同一 Mesh 实例导致数据冲突 + Mesh2D m = mesh.copy(); + + // 确保拷贝保留原始的纹理引用(copy() 应该已经赋值,但显式赋值可避免遗漏) + m.setTexture(mesh.getTexture()); + + // 确保本节点的 worldTransform 是最新的 recomputeWorldTransformRecursive(); - // 将 mesh 的每个顶点从本地空间变换到世界空间(烘焙) - int vc = mesh.getVertexCount(); + // 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用 + float[] originalVertices = m.getVertices().clone(); + m.setOriginalVertices(originalVertices); + logger.info("addMesh: texture={} for mesh={}", m.getTexture(), m.getName()); + // 保证 UV 不被篡改(通常 copy() 已经处理) + // float[] uvs = m.getUVs(); // 如果需要可以在此处检查 + + // 将拷贝的 mesh 的每个顶点从本地空间变换到世界空间(烘焙到 world) + int vc = m.getVertexCount(); for (int i = 0; i < vc; i++) { - org.joml.Vector2f local = mesh.getVertex(i); - org.joml.Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local); - mesh.setVertex(i, worldPt.x, worldPt.y); + Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); + Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local); + m.setVertex(i, worldPt.x, worldPt.y); } - meshes.add(mesh); + + // 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新 + m.setBakedToWorld(true); + + // 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU) + m.markDirty(); + + // 如果你确定此处正在 GL 渲染线程并且想要立刻创建 VAO/VBO(可取消下面注释) + // m.uploadToGPU(); + + // 将拷贝加入到本部件 + meshes.add(m); boundsDirty = true; } + + /** * 设置中心点 */ @@ -811,6 +889,9 @@ public class ModelPart { return opacity; } + public float getScaleX() { return scaleX; } + public float getScaleY() { return scaleY; } + public void setOpacity(float opacity) { this.opacity = Math.max(0.0f, Math.min(1.0f, opacity)); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java index 0fd5caa..0e8336c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java @@ -72,22 +72,7 @@ public class LightSourceData implements Serializable { Vector3f col = stringToVector3f(color); LightSource light; - if (isAmbient) { - // 使用环境光构造器 - light = new LightSource(LightSource.vector3fToColor(col), intensity); - } else { - // 使用包含辉光参数的构造器(即便 isGlow 为 false 也可以传入) - light = new LightSource( - pos, - LightSource.vector3fToColor(col), - intensity, - isGlow, - SaveVector2f.fromString(glowDirection), - glowIntensity, - glowRadius, - glowAmount - ); - } + light = new LightSource(LightSource.vector3fToColor(col), intensity); light.setEnabled(enabled); light.setAmbient(isAmbient); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java index 15d4df0..a857f52 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.model.util; +import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import org.joml.Matrix3f; import org.joml.Vector2f; 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 2ccaac5..70f86e7 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,11 +1,16 @@ package com.chuangzhou.vivid2D.render.model.util; import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; import org.joml.Vector2f; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.Objects; + +import org.joml.Vector4f; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL15; import org.lwjgl.opengl.GL20; @@ -44,6 +49,7 @@ public class Mesh2D { private BoundingBox bounds; private boolean boundsDirty = true; private boolean bakedToWorld = false; + private volatile boolean selected = false; // ==================== 常量 ==================== public static final int POINTS = 0; @@ -125,6 +131,17 @@ public class Mesh2D { return new Mesh2D(name, vertices, uvs, indices); } + public float[] getOriginalVertices() { + return originalVertices != null ? originalVertices.clone() : vertices.clone(); + } + + /** + * 设置原始顶点数据 + */ + public void setOriginalVertices(float[] originalVertices) { + this.originalVertices = originalVertices != null ? originalVertices.clone() : null; + } + /** * 创建圆形网格 */ @@ -206,6 +223,20 @@ public class Mesh2D { setVertex(index, position.x, position.y); } + /** + * 设置该 Mesh 的选中状态(线程安全) + */ + public void setSelected(boolean sel) { + this.selected = sel; + } + + /** + * 查询选中状态 + */ + public boolean isSelected() { + return this.selected; + } + /** * 获取UV坐标 */ @@ -476,40 +507,202 @@ public class Mesh2D { uploadToGPU(); } + // 1. 绘制网格 if (texture != null) { texture.bind(); } - // 绑定 VAO - 使用 RenderSystem RenderSystem.glBindVertexArray(() -> vaoId); - - // 使用着色器程序 - 使用 RenderSystem RenderSystem.useProgram(shaderProgram); - // 将 modelMatrix 上传到 shader 的 uniform int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix"); if (loc == -1) { loc = RenderSystem.getUniformLocation(shaderProgram, "uModel"); } - if (loc != -1) { RenderSystem.uniformMatrix3(loc, modelMatrix); - } else { - //logger.warn("警告: 着色器中未找到 uModelMatrix 或 uModel uniform"); } - // 绘制 - 使用 RenderSystem RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount, RenderSystem.GL_UNSIGNED_INT, 0); - // 解绑 VAO + // 2. 解绑 VAO 和纹理,确保 overlay 绘制不受影响 RenderSystem.glBindVertexArray(() -> 0); - if (texture != null) { - texture.unbind(); // 需要检查 texture.unbind() 是否也需要封装 + texture.unbind(); + } + + // 3. 如果选中,则绘制选中框 + if (selected) { + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + int currentProgram = RenderSystem.getCurrentProgram(); + try { + ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); + if (solidShader != null && solidShader.programId != 0) { + solidShader.use(); + int modelLoc = solidShader.getUniformLocation("uModelMatrix"); + if (modelLoc != -1) { + RenderSystem.uniformMatrix3(modelLoc, modelMatrix); + } + int colorLoc = solidShader.getUniformLocation("uColor"); + if (colorLoc != -1) { + RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + } + } + drawSelectBox(); + } finally { + if (currentProgram != 0) { + RenderSystem.useProgram(currentProgram); + } + } } } + 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; + + 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)); // 青色,100%不透明 + + 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)); // 白色,100%不透明 + + 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); + } + + /** + * 绘制调整大小的手柄 + */ + private void drawResizeHandles(BufferBuilder bb, float minX, float minY, float maxX, float maxY, + float cornerSize, float borderThickness) { + Vector4f handleColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + + // 绘制四个角点 + drawCornerHandle(bb, minX, minY, handleColor, cornerSize); // 左上 + drawCornerHandle(bb, maxX, minY, handleColor, cornerSize); // 右上 + drawCornerHandle(bb, minX, maxY, handleColor, cornerSize); // 左下 + drawCornerHandle(bb, maxX, maxY, handleColor, cornerSize); // 右下 + + // 绘制边线中点(可选) + drawEdgeHandle(bb, (minX + maxX) / 2, minY, handleColor, borderThickness); // 上边中点 + drawEdgeHandle(bb, (minX + maxX) / 2, maxY, handleColor, borderThickness); // 下边中点 + drawEdgeHandle(bb, minX, (minY + maxY) / 2, handleColor, borderThickness); // 左边中点 + drawEdgeHandle(bb, maxX, (minY + maxY) / 2, handleColor, borderThickness); // 右边中点 + } + + private void drawCornerHandle(BufferBuilder bb, float x, float y, Vector4f color, float cornerSize) { + float halfSize = cornerSize / 2; + // 使用 RenderSystem 的常量 + bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN + bb.setColor(color); + bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f); + bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f); + bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f); + bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f); + bb.endImmediate(); + } + + private void drawEdgeHandle(BufferBuilder bb, float x, float y, Vector4f color, float borderThickness) { + float halfSize = borderThickness / 2; + // 使用 RenderSystem 的常量 + bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN + bb.setColor(color); + bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f); + bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f); + bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f); + bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f); + bb.endImmediate(); + } + + // 角点标记 + private void drawCornerPoints(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + float cornerSize = 8.0f; // 定义局部常量 + Vector4f cornerColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + + Vector2f[] corners = { + new Vector2f(minX, minY), + new Vector2f(maxX, minY), + new Vector2f(maxX, maxY), + new Vector2f(minX, maxY) + }; + + for (Vector2f corner : corners) { + // 使用 RenderSystem 的常量 + bb.begin(RenderSystem.GL_LINE_STRIP, 3); // 改为 RenderSystem.GL_LINE_STRIP + bb.setColor(cornerColor); + bb.vertex(corner.x - cornerSize, corner.y, 0.0f, 0.0f); + bb.vertex(corner.x, corner.y, 0.0f, 0.0f); + bb.vertex(corner.x, corner.y + cornerSize, 0.0f, 0.0f); + bb.endImmediate(); + } + } + + /** + * 计算模型的边界框 [minX, minY, maxX, maxY] + */ + public float[] calculateBoundingBox() { + // 使用现有的边界计算功能 + BoundingBox bounds = getBounds(); + return new float[]{ + bounds.getMinX(), + bounds.getMinY(), + bounds.getMaxX(), + bounds.getMaxY() + }; + } + + /** + * 计算带扩展的边界框 [minX, minY, maxX, maxY] + */ + public float[] calculateBoundingBox(float expand) { + float[] bounds = calculateBoundingBox(); + return new float[]{ + bounds[0] - expand, + bounds[1] - expand, + bounds[2] + expand, + bounds[3] + expand + }; + } public void draw() { if (!visible) return; if (indices == null || indices.length == 0) return; @@ -616,13 +809,38 @@ public class Mesh2D { */ public Mesh2D copy() { Mesh2D copy = new Mesh2D(name + "_copy"); - copy.setMeshData(vertices, uvs, indices); - copy.texture = texture; - copy.visible = visible; - copy.drawMode = drawMode; + + // 深拷贝数组(保证互不影响) + copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0]; + copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0]; + copy.indices = this.indices != null ? this.indices.clone() : new int[0]; + + // 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据 + copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone(); + + // 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄) + copy.texture = this.texture; + copy.visible = this.visible; + copy.drawMode = this.drawMode; + copy.bakedToWorld = this.bakedToWorld; + + // 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行 + copy.vaoId = -1; + copy.vboId = -1; + copy.eboId = -1; + copy.indexCount = this.indices != null ? this.indices.length : 0; + copy.uploaded = false; + + // 状态标记 + copy.dirty = true; + copy.boundsDirty = true; + copy.bounds = new BoundingBox(); + copy.selected = this.selected; + return copy; } + public int getVaoId() { return vaoId; } @@ -677,13 +895,30 @@ public class Mesh2D { @Override public String toString() { - return "Mesh2D{" + - "name='" + name + '\'' + - ", vertices=" + getVertexCount() + - ", indices=" + indices.length + - ", visible=" + visible + - ", drawMode=" + getDrawModeString() + - ", bounds=" + getBounds() + - '}'; + StringBuilder sb = new StringBuilder(); + sb.append("Mesh2D{") + .append("name='").append(name).append('\'') + .append(", vertices=").append(getVertexCount()) + .append(", indices=").append(indices.length) + .append(", visible=").append(visible) + .append(", drawMode=").append(getDrawModeString()) + .append(", bounds=").append(getBounds()); + + if (vertices != null && vertices.length > 0) { + sb.append(", coordinates=["); + for (int i = 0; i < vertices.length; i += 2) { + if (i > 0) sb.append(", "); + sb.append("(") + .append(String.format("%.2f", vertices[i])) + .append(", ") + .append(String.format("%.2f", vertices[i + 1])) + .append(")"); + } + sb.append("]"); + } + + sb.append('}'); + return sb.toString(); } + } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java similarity index 94% rename from src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java rename to src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java index 1d76445..017c81e 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Matrix3fUtils.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java @@ -1,4 +1,4 @@ -package com.chuangzhou.vivid2D.render.model.util; +package com.chuangzhou.vivid2D.render.systems; import org.joml.Matrix3f; import org.joml.Vector2f; 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 1cba732..178dbc1 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java @@ -67,6 +67,11 @@ public final class RenderSystem { public static final int DRAW_TRIANGLE_STRIP = GL11.GL_TRIANGLE_STRIP; public static final int DRAW_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN; public static final int DRAW_QUADS = GL11.GL_QUADS; + public static final int GL_LINE_LOOP = GL11.GL_LINE_LOOP; + public static final int GL_LINE_STRIP = GL11.GL_LINE_STRIP; + public static final int GL_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN; + public static final int GL_QUADS = GL11.GL_QUADS; + public static final int GL_TRIANGLES = GL11.GL_TRIANGLES; // ================== 索引类型常量 ================== public static final int GL_UNSIGNED_BYTE = GL11.GL_UNSIGNED_BYTE; @@ -92,6 +97,11 @@ public final class RenderSystem { public static final int GL_SHORT = GL11.GL_SHORT; public static final int GL_INT = GL11.GL_INT; + public static final int GL_TRUE = org.lwjgl.opengl.GL11.GL_TRUE; + 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; + // ================== 初始化方法 ================== public static void initRenderThread() { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java index 5edf0dc..59aecdf 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java @@ -1,12 +1,14 @@ package com.chuangzhou.vivid2D.render.systems.buffer; import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; /** * 缓冲区上传器 * - * @version 1.0 - * @author tzdwindows + * @version 1.1 - 添加颜色支持 + * @author tzdwindows 7 */ public class BufferUploader { @@ -54,8 +56,21 @@ public class BufferUploader { } // 应用着色器 + int currentProgram = 0; if (state.shaderProgram != 0) { + currentProgram = state.shaderProgram; RenderSystem.useProgram(state.shaderProgram); + } else { + currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); + } + if (currentProgram != 0) { + int colorLoc = RenderSystem.getUniformLocation(currentProgram, "uColor"); + if (colorLoc == -1) {} else { + RenderSystem.uniform4f(colorLoc, + state.color.x, state.color.y, state.color.z, state.color.w); + } + } else { + System.err.println("DEBUG: No shader program available for color setting"); } // 应用混合模式 diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java new file mode 100644 index 0000000..7da84c8 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java @@ -0,0 +1,14 @@ +package com.chuangzhou.vivid2D.render.systems.sources; + +/** + * 完整着色器接口 + * 一个完整的着色器程序需要顶点着色器和片段着色器 + * @author tzdwindows 7 + */ +public interface CompleteShader { + Shader getVertexShader(); + Shader getFragmentShader(); + String getShaderName(); + boolean isDefaultShader(); + default void setDefaultUniforms(ShaderProgram program) {} +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java new file mode 100644 index 0000000..ea422b5 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java @@ -0,0 +1,10 @@ +package com.chuangzhou.vivid2D.render.systems.sources; + +/** + * 着色器接口 + * @author tzdwindows 7 + */ +public interface Shader { + String getShaderCode(); + String getShaderName(); +} 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 new file mode 100644 index 0000000..d9827ae --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java @@ -0,0 +1,284 @@ +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 org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL20; +import org.lwjgl.system.MemoryStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.lwjgl.system.MemoryStack.stackPush; + +/** + * 着色器管理器 - 负责着色器的编译、链接和管理 + * + * @author tzdwindows 7 + * @version 1.0 + * @since 2025-10-16 + */ +public class ShaderManagement { + private static final Logger logger = LoggerFactory.getLogger(ShaderManagement.class); + + /** + * 着色器程序缓存映射,按名称存储已编译的着色器程序 + */ + public static final Map shaderMap = new HashMap<>(); + + /** + * 着色器列表,按顺序存储所有着色器源代码 + */ + public static final List shaderList = List.of( + new Shader2D(), + new SolidColorShader() + ); + + /** + * 默认着色器程序 + */ + private static ShaderProgram defaultProgram; + + /** + * 编译所有注册的着色器 + */ + public static void compileAllShaders() { + // 确保在渲染线程 + RenderSystem.assertOnRenderThread(); + + for (CompleteShader completeShader : shaderList) { + compileShaderProgram(completeShader); + } + + // 设置默认着色器 + if (defaultProgram == null && !shaderMap.isEmpty()) { + defaultProgram = shaderMap.values().iterator().next(); + } + } + + /** + * 编译单个完整的着色器程序 + */ + private static void compileShaderProgram(CompleteShader completeShader) { + String shaderName = completeShader.getShaderName(); + + try { + // 编译顶点着色器 + Shader vertexShader = completeShader.getVertexShader(); + int vsId = compileShader(GL20.GL_VERTEX_SHADER, vertexShader.getShaderCode(), + vertexShader.getShaderName()); + + // 编译片段着色器 + Shader fragmentShader = completeShader.getFragmentShader(); + int fsId = compileShader(GL20.GL_FRAGMENT_SHADER, fragmentShader.getShaderCode(), + fragmentShader.getShaderName()); + + // 链接程序 + int programId = linkProgram(vsId, fsId, shaderName); + + // 创建着色器程序对象 + ShaderProgram shaderProgram = new ShaderProgram(programId); + shaderMap.put(shaderName, shaderProgram); + + // 如果是默认着色器,设置为默认程序 + if (completeShader.isDefaultShader()) { + defaultProgram = shaderProgram; + setupDefaultUniforms(shaderProgram); + } + + // 清理单独的着色器对象 + RenderSystem.deleteShader(vsId); + RenderSystem.deleteShader(fsId); + + logger.info("成功编译着色器: {}", shaderName); + + } catch (Exception e) { + logger.error("编译着色器失败: {}", shaderName); + e.printStackTrace(); + throw new RuntimeException("Shader compilation failed: " + shaderName, e); + } + } + + /** + * 设置默认着色器的uniform值 + */ + private static void setupDefaultUniforms(ShaderProgram program) { + program.use(); + + // 设置纹理单元 + setUniformInt(program, "uTexture", 0); + setUniformFloat(program, "uOpacity", 1.0f); + setUniformVec4(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + setUniformInt(program, "uBlendMode", 0); + setUniformInt(program, "uDebugMode", 0); + setUniformInt(program, "uLightCount", 0); + + program.stop(); + + RenderSystem.checkGLError("setupDefaultUniforms"); + } + + /** + * 编译着色器 + */ + private static int compileShader(int type, String source, String shaderName) { + int shaderId = RenderSystem.createShader(type); + RenderSystem.shaderSource(shaderId, source); + RenderSystem.compileShader(shaderId); + + // 检查编译状态 + if (RenderSystem.getShaderi(shaderId, RenderSystem.GL_COMPILE_STATUS) != RenderSystem.GL_TRUE) { + String log = RenderSystem.getShaderInfoLog(shaderId); + RenderSystem.deleteShader(shaderId); + throw new RuntimeException("着色器编译失败 [" + shaderName + "]:\n" + log); + } + + return shaderId; + } + + /** + * 链接着色器程序 + */ + private static int linkProgram(int vertexShaderId, int fragmentShaderId, String programName) { + int programId = RenderSystem.createProgram(); + RenderSystem.attachShader(programId, vertexShaderId); + RenderSystem.attachShader(programId, fragmentShaderId); + RenderSystem.linkProgram(programId); + + // 检查链接状态 + if (RenderSystem.getProgrami(programId, RenderSystem.GL_LINK_STATUS) != RenderSystem.GL_TRUE) { + String log = RenderSystem.getProgramInfoLog(programId); + RenderSystem.deleteProgram(programId); + throw new RuntimeException("着色器程序链接失败 [" + programName + "]:\n" + log); + } + + // 验证程序(使用自定义验证方法) + validateProgram(programId, programName); + + return programId; + } + + /** + * 自定义程序验证方法 + */ + private static void validateProgram(int programId, String programName) { + int validateStatus = RenderSystem.getProgrami(programId, RenderSystem.GL_VALIDATE_STATUS); + if (validateStatus != RenderSystem.GL_TRUE) { + String log = RenderSystem.getProgramInfoLog(programId); + logger.warn("着色器程序验证警告 [{}]: {}", programName, log); + } + } + + /** + * 获取默认着色器程序 + */ + public static ShaderProgram getDefaultProgram() { + return defaultProgram; + } + + /** + * 按名称获取着色器程序 + */ + public static ShaderProgram getShaderProgram(String name) { + return shaderMap.get(name); + } + + public static List getShaderList() { + return shaderList; + } + + /** + * 清理所有着色器资源 + */ + public static void cleanup() { + RenderSystem.assertOnRenderThread(); + + for (ShaderProgram program : shaderMap.values()) { + program.delete(); + } + shaderMap.clear(); + defaultProgram = null; + } + + // Uniform设置方法 + public static void setUniformInt(ShaderProgram program, String name, int value) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniform1i(location, value); + } + } + + public static void setUniformFloat(ShaderProgram program, String name, float value) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniform1f(location, value); + } + } + + public static void setUniformVec2(ShaderProgram program, String name, float x, float y) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniform2f(location, x, y); + } + } + + public static void setUniformVec2(ShaderProgram program, String name, org.joml.Vector2f vec) { + setUniformVec2(program, name, vec.x, vec.y); + } + + public static void setUniformVec3(ShaderProgram program, String name, float x, float y, float z) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniform3f(location, x, y, z); + } + } + + public static void setUniformVec3(ShaderProgram program, String name, Vector3f vec) { + setUniformVec3(program, name, vec.x, vec.y, vec.z); + } + + public static void setUniformVec4(ShaderProgram program, String name, float[] values) { + if (values.length != 4) { + throw new IllegalArgumentException("Vec4 uniform requires 4 values"); + } + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + try (MemoryStack stack = stackPush()) { + FloatBuffer buffer = stack.mallocFloat(4); + buffer.put(values).flip(); + RenderSystem.uniform4f(location, values[0], values[1], values[2], values[3]); + } + } + } + + public static void setUniformVec4(ShaderProgram program, String name, Vector4f vec) { + setUniformVec4(program, name, new float[]{vec.x, vec.y, vec.z, vec.w}); + } + + public static void setUniformMat3(ShaderProgram program, String name, FloatBuffer matrix) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniformMatrix3(location, false, matrix); + } + } + + public static void setUniformMat3(ShaderProgram program, String name, org.joml.Matrix3f matrix) { + program.use(); + int location = program.getUniformLocation(name); + if (location != -1) { + RenderSystem.uniformMatrix3(location, matrix); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java new file mode 100644 index 0000000..411c7dc --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java @@ -0,0 +1,117 @@ +package com.chuangzhou.vivid2D.render.systems.sources; + +import org.joml.Matrix3f; +import org.lwjgl.opengl.GL20; + +import java.util.HashMap; +import java.util.Map; + +import static org.lwjgl.opengl.GL20.*; + +/** + * @author tzdwindows 7 + */ +public class ShaderProgram { + public final int programId; + public final Map uniformCache = new HashMap<>(); + + public ShaderProgram(int programId) { + this.programId = programId; + } + + public void use() { + GL20.glUseProgram(programId); + } + + public void stop() { + GL20.glUseProgram(0); + } + + public int getUniformLocation(String name) { + return uniformCache.computeIfAbsent(name, k -> { + return glGetUniformLocation(programId, k); + }); + } + + // 添加 uniform 设置方法 + public void setUniform1i(String name, int value) { + int location = getUniformLocation(name); + if (location != -1) { + glUniform1i(location, value); + } + } + + public void setUniform1f(String name, float value) { + int location = getUniformLocation(name); + if (location != -1) { + glUniform1f(location, value); + } + } + + public void setUniform2f(String name, float x, float y) { + int location = getUniformLocation(name); + if (location != -1) { + glUniform2f(location, x, y); + } + } + + public void setUniform3f(String name, float x, float y, float z) { + int location = getUniformLocation(name); + if (location != -1) { + glUniform3f(location, x, y, z); + } + } + + public void setUniform4f(String name, float x, float y, float z, float w) { + int location = getUniformLocation(name); + if (location != -1) { + glUniform4f(location, x, y, z, w); + } + } + + public void setUniformMatrix3(String name, Matrix3f matrix) { + int location = getUniformLocation(name); + if (location != -1) { + float[] matrixArray = new float[9]; + matrix.get(matrixArray); + glUniformMatrix3fv(location, false, matrixArray); + } + } + + // 重载方法,直接使用 location + public void setUniform1i(int location, int value) { + if (location != -1) { + glUniform1i(location, value); + } + } + + public void setUniform1f(int location, float value) { + if (location != -1) { + glUniform1f(location, value); + } + } + + public void setUniform4f(int location, float x, float y, float z, float w) { + if (location != -1) { + glUniform4f(location, x, y, z, w); + } + } + + public void setUniformMatrix3(int location, Matrix3f matrix) { + if (location != -1) { + float[] matrixArray = new float[9]; + matrix.get(matrixArray); + glUniformMatrix3fv(location, false, matrixArray); + } + } + + public void delete() { + if (GL20.glIsProgram(programId)) { + GL20.glDeleteProgram(programId); + } + } + + public int getProgramId() { + return programId; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/ShaderSources.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java similarity index 71% rename from src/main/java/com/chuangzhou/vivid2D/render/systems/ShaderSources.java rename to src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java index d196255..45ada41 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/ShaderSources.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java @@ -1,42 +1,11 @@ -package com.chuangzhou.vivid2D.render.systems; +package com.chuangzhou.vivid2D.render.systems.sources.def; -import org.lwjgl.opengl.GL20; - -import java.util.HashMap; -import java.util.Map; - -import static org.lwjgl.opengl.GL20.glGetUniformLocation; +import com.chuangzhou.vivid2D.render.systems.sources.Shader; /** - * 着色器源代码 - * * @author tzdwindows 7 - * @version 1.0 - * @since 2025-10-16 */ -public class ShaderSources { - public static final String VERTEX_SHADER_SRC = - """ - #version 330 core - layout(location = 0) in vec2 aPosition; - layout(location = 1) in vec2 aTexCoord; - out vec2 vTexCoord; - out vec2 vWorldPos; - - uniform mat3 uModelMatrix; - uniform mat3 uViewMatrix; - uniform mat3 uProjectionMatrix; - - void main() { - // 使用 3x3 矩阵链计算屏幕位置(假设矩阵是二维仿射) - vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); - gl_Position = vec4(p.xy, 0.0, 1.0); - vTexCoord = aTexCoord; - // 输出 world-space 位置供 fragment shader 使用(仅 xy) - vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy; - } - """; - +public class FragmentShaders implements Shader { public static final String FRAGMENT_SHADER_SRC = """ #version 330 core @@ -137,32 +106,13 @@ public class ShaderSources { FragColor = vec4(finalColor, alpha); } """; + @Override + public String getShaderCode() { + return FRAGMENT_SHADER_SRC; + } - - public static class ShaderProgram { - public final int programId; - public final Map uniformCache = new HashMap<>(); - - public ShaderProgram(int programId) { - this.programId = programId; - } - - public void use() { - GL20.glUseProgram(programId); - } - - public void stop() { - GL20.glUseProgram(0); - } - - public int getUniformLocation(String name) { - return uniformCache.computeIfAbsent(name, k -> { - return glGetUniformLocation(programId, k); - }); - } - - public void delete() { - if (GL20.glIsProgram(programId)) GL20.glDeleteProgram(programId); - } + @Override + public String getShaderName() { + return "Fragment shaders"; } } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java new file mode 100644 index 0000000..56c6513 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java @@ -0,0 +1,33 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; +import com.chuangzhou.vivid2D.render.systems.sources.Shader; + +/** + * 默认着色器实现 + * + * @author tzdwindows 7 + * @version 1.0 + * @since 2025-10-17 + */ +public class Shader2D implements CompleteShader { + @Override + public Shader getVertexShader() { + return new VertexShaders(); + } + + @Override + public Shader getFragmentShader() { + return new FragmentShaders(); + } + + @Override + public String getShaderName() { + return "Vivid2d Shader"; + } + + @Override + public boolean isDefaultShader() { + return true; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java new file mode 100644 index 0000000..2c3c3a1 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java @@ -0,0 +1,40 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.Shader; + +/** + * 纯色着色器的片段着色器 + * 只使用颜色,忽略纹理 + * @author tzdwindows 7 + */ +public class SolidColorFragmentShader implements Shader { + public static final String FRAGMENT_SHADER_SRC = + """ + #version 330 core + out vec4 FragColor; + + uniform vec4 uColor; + uniform float uOpacity; + + void main() { + // 直接使用颜色,忽略纹理 + vec4 finalColor = uColor; + finalColor.a *= uOpacity; + + // 如果透明度太低则丢弃片段 + if (finalColor.a <= 0.001) discard; + + FragColor = finalColor; + } + """; + + @Override + public String getShaderCode() { + return FRAGMENT_SHADER_SRC; + } + + @Override + public String getShaderName() { + return "Solid Color Fragment Shader"; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java new file mode 100644 index 0000000..1def1e1 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java @@ -0,0 +1,82 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; +import com.chuangzhou.vivid2D.render.systems.sources.Shader; +import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; + +/** + * 纯色着色器程序 + * 专门用于绘制纯色几何体,如选中框、调试图形等 + * @author tzdwindows 7 + */ +public class SolidColorShader implements CompleteShader { + private final SolidColorVertexShader vertexShader; + private final SolidColorFragmentShader fragmentShader; + + public SolidColorShader() { + this.vertexShader = new SolidColorVertexShader(); + this.fragmentShader = new SolidColorFragmentShader(); + } + + @Override + public Shader getVertexShader() { + return vertexShader; + } + + @Override + public Shader getFragmentShader() { + return fragmentShader; + } + + @Override + public String getShaderName() { + return "Solid Color Shader"; + } + + @Override + public boolean isDefaultShader() { + return false; // 这不是默认着色器,是专门用途的着色器 + } + + @Override + public void setDefaultUniforms(ShaderProgram program) { + // 设置默认的uniform值 + if (program != null) { + // 设置默认颜色为白色 + int colorLoc = program.getUniformLocation("uColor"); + if (colorLoc != -1) { + program.setUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + } + + // 设置默认不透明度 + int opacityLoc = program.getUniformLocation("uOpacity"); + if (opacityLoc != -1) { + program.setUniform1f(opacityLoc, 1.0f); + } + } + } + + /** + * 设置着色器颜色 + */ + public void setColor(ShaderProgram program, float r, float g, float b, float a) { + if (program != null) { + int colorLoc = program.getUniformLocation("uColor"); + if (colorLoc != -1) { + program.setUniform4f(colorLoc, r, g, b, a); + } + } + } + + /** + * 设置着色器不透明度 + */ + public void setOpacity(ShaderProgram program, float opacity) { + if (program != null) { + int opacityLoc = program.getUniformLocation("uOpacity"); + if (opacityLoc != -1) { + program.setUniform1f(opacityLoc, opacity); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java new file mode 100644 index 0000000..dcc5280 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java @@ -0,0 +1,36 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.Shader; + +/** + * 纯色着色器的顶点着色器 + * @author tzdwindows 7 + */ +public class SolidColorVertexShader implements Shader { + public static final String VERTEX_SHADER_SRC = + """ + #version 330 core + layout(location = 0) in vec2 aPosition; + layout(location = 1) in vec2 aTexCoord; + + uniform mat3 uModelMatrix; + uniform mat3 uViewMatrix; + uniform mat3 uProjectionMatrix; + + void main() { + // 使用 3x3 矩阵链计算屏幕位置 + vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); + gl_Position = vec4(p.xy, 0.0, 1.0); + } + """; + + @Override + public String getShaderCode() { + return VERTEX_SHADER_SRC; + } + + @Override + public String getShaderName() { + return "Solid Color Vertex Shader"; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java new file mode 100644 index 0000000..7b4cbe0 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java @@ -0,0 +1,44 @@ +package com.chuangzhou.vivid2D.render.systems.sources.def; + +import com.chuangzhou.vivid2D.render.systems.sources.Shader; + +/** + * 顶点着色器 + * + * @author tzdwindows 7 + * @version 1.0 + * @since 2025-10-17 + */ +public class VertexShaders implements Shader { + public static final String VERTEX_SHADER_SRC = + """ + #version 330 core + layout(location = 0) in vec2 aPosition; + layout(location = 1) in vec2 aTexCoord; + out vec2 vTexCoord; + out vec2 vWorldPos; + + uniform mat3 uModelMatrix; + uniform mat3 uViewMatrix; + uniform mat3 uProjectionMatrix; + + void main() { + // 使用 3x3 矩阵链计算屏幕位置(假设矩阵是二维仿射) + vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); + gl_Position = vec4(p.xy, 0.0, 1.0); + vTexCoord = aTexCoord; + // 输出 world-space 位置供 fragment shader 使用(仅 xy) + vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy; + } + """; + + @Override + public String getShaderCode() { + return VERTEX_SHADER_SRC; + } + + @Override + public String getShaderName() { + return "Vertex Shaders"; + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java new file mode 100644 index 0000000..92564ef --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java @@ -0,0 +1,112 @@ +package com.chuangzhou.vivid2D.test; + +import com.chuangzhou.vivid2D.render.awt.ModelClickListener; +import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; +import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; + +import javax.swing.*; +import java.awt.*; + +/** + * 简单的测试示例:创建一个 Model2D,添加几层(部件), + * 然后在 JFrame 中展示 ModelLayerPanel(左侧)、ModelRenderPanel(中间渲染区) + * 和模型树(右侧)以便观察变化。 + */ +public class ModelLayerPanelTest { + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + // 创建示例模型并添加图层 + Model2D model = new Model2D("示例模型"); + + // 调整一些初始属性(可选) + ModelPart person = model.getPart("人物"); + if (person != null) { + try { + person.setOpacity(0.85f); + } catch (Exception ignored) {} + } + + // 创建 UI + JFrame frame = new JFrame("ModelLayerPanel 测试(含渲染面板)"); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.setLayout(new BorderLayout()); + + // 左侧:图层面板(传入 renderPanel 后可在面板中绑定贴图到 GL 上下文) + // 先创建一个占位 renderPanel,再把它传给 layerPanel(ModelRenderPanel 构造需要尺寸) + ModelRenderPanel renderPanel = new ModelRenderPanel(model, 640, 480); + //renderPanel.addModelClickListener(new ModelClickListener() { + // @Override + // public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { + // if (mesh == null) return; + // System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY); + // } + //}); + ModelLayerPanel layerPanel = new ModelLayerPanel(model, renderPanel); + layerPanel.setPreferredSize(new Dimension(260, 600)); + frame.add(layerPanel, BorderLayout.WEST); + + // 中间:渲染面板 + renderPanel.setPreferredSize(new Dimension(640, 480)); + frame.add(renderPanel, BorderLayout.CENTER); + + // 右侧:显示模型树(用于观察当前模型部件结构) + JTree tree = new JTree(model.toTreeNode()); + JScrollPane treeScroll = new JScrollPane(tree); + treeScroll.setPreferredSize(new Dimension(240, 600)); + frame.add(treeScroll, BorderLayout.EAST); + + // 底部:演示按钮(刷新树以反映面板中对模型的更改) + JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton refreshBtn = new JButton("刷新模型树"); + refreshBtn.addActionListener(e -> { + tree.setModel(new javax.swing.tree.DefaultTreeModel(model.toTreeNode())); + for (int i = 0; i < tree.getRowCount(); i++) tree.expandRow(i); + // 同步通知渲染面板(如果需要)去刷新模型 + try { + renderPanel.setModel(model); + } catch (Exception ignored) {} + }); + bottom.add(refreshBtn); + + JButton printOrderBtn = new JButton("打印部件顺序(控制台)"); + printOrderBtn.addActionListener(e -> { + System.out.println("当前模型部件顺序:"); + for (ModelPart p : model.getParts()) { + System.out.println(" - " + p.getName() + " (可见=" + p.isVisible() + ", 不透明度=" + p.getOpacity() + ")"); + } + }); + bottom.add(printOrderBtn); + + frame.add(bottom, BorderLayout.SOUTH); + + // 监听窗口关闭,确保释放 GL 资源 + frame.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(java.awt.event.WindowEvent e) { + // 先释放渲染面板相关 GL 资源与线程 + try { + renderPanel.dispose(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + @Override + public void windowClosed(java.awt.event.WindowEvent e) { + // 进程退出(确保彻底关闭) + try { + renderPanel.dispose(); + } catch (Throwable ignored) {} + System.exit(0); + } + }); + + frame.setSize(1200, 700); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java index 46e856a..f836338 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java @@ -22,6 +22,7 @@ import java.util.Random; /** * ModelRenderLightingTest * 测试使用 Model2D + 光源进行简单光照渲染 + * @author tzdwindows 7 */ public class ModelRenderLightingTest { @@ -114,6 +115,7 @@ public class ModelRenderLightingTest { rightArm.setPosition(60, -20); Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90); rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); + rightArmMesh.setSelected( true); rightArm.addMesh(rightArmMesh); // legs diff --git a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java index 341a5c8..2c0e79e 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java @@ -1,6 +1,6 @@ package com.chuangzhou.vivid2D.test; -import com.chuangzhou.vivid2D.render.awt.ModelGLPanel; +import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; @@ -31,15 +31,15 @@ public class TestModelGLPanel { JFrame frame = new JFrame("ModelGLPanel Demo"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - ModelGLPanel glPanel = null; + ModelRenderPanel glPanel = null; try { // 先创建一个空的 Model2D 实例(将在 GL 上下文中初始化更详细内容) testModel = new Model2D("Humanoid"); - glPanel = new ModelGLPanel(testModel, 800, 600); + glPanel = new ModelRenderPanel(testModel, 800, 600); // 在 GL 上下文中创建 mesh / part / physics 等资源 - ModelGLPanel finalGlPanel = glPanel; + ModelRenderPanel finalGlPanel = glPanel; glPanel.executeInGLContext(() -> { setupModelInGL(testModel); return null;