diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java index 62e0f96..5bfd0b7 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java @@ -6,31 +6,56 @@ import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; 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.CompletableFuture; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; /** - * 修复版高性能 OpenGL 渲染面板 + * 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 final int width; - private final int height; + // 改为可变的宽高以支持动态重建离屏上下文缓冲 + private volatile int width; + private volatile int height; private BufferedImage currentFrame; - private boolean contextInitialized = false; + 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; + /** * 构造函数:使用模型路径 */ @@ -63,6 +88,20 @@ public class ModelGLPanel extends JPanel { // 创建渲染线程 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); + } + }); } /** @@ -78,7 +117,7 @@ public class ModelGLPanel extends JPanel { 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 上下文"); @@ -88,27 +127,34 @@ public class ModelGLPanel extends JPanel { GLFW.glfwMakeContextCurrent(windowId); GL.createCapabilities(); + GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); // 初始化 OpenGL 状态 GL11.glEnable(GL11.GL_DEPTH_TEST); // 检查是否支持多重采样 if (GL.getCapabilities().OpenGL13) { GL11.glEnable(GL13.GL_MULTISAMPLE); - System.out.println("多重采样已启用"); + logger.info("多重采样已启用"); } else { - System.out.println("不支持多重采样,跳过启用"); + logger.info("不支持多重采样,跳过启用"); } GL11.glViewport(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(); - contextInitialized = true; - - // 在正确的上下文中加载模型 + // 在正确的上下文中加载模型(可能会耗时) loadModelInContext(); - // 通知上下文已准备就绪 + // 标记上下文已初始化并完成通知(只 complete 一次) + contextInitialized = true; contextReady.complete(null); } @@ -120,10 +166,10 @@ public class ModelGLPanel extends JPanel { if (modelPath != null) { Model2D model = Model2D.loadFromFile(modelPath); modelRef.set(model); - System.out.println("模型加载成功: " + modelPath); + logger.info("模型加载成功: {}", modelPath); } } catch (Exception e) { - System.err.println("模型加载失败: " + e.getMessage()); + logger.error("模型加载失败: {}", e.getMessage(), e); e.printStackTrace(); // 创建错误模型或使用默认模型 @@ -152,23 +198,28 @@ public class ModelGLPanel extends JPanel { 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(); - // 控制帧率 - try { - Thread.sleep(1000 / 60); // 60 FPS - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; + long elapsed = System.nanoTime() - start; + long sleepNs = targetNs - elapsed; + if (sleepNs > 0) { + LockSupport.parkNanos(sleepNs); } } } catch (Exception e) { - e.printStackTrace(); + logger.error("渲染线程异常", e); } finally { cleanup(); } @@ -179,6 +230,21 @@ public class ModelGLPanel extends JPanel { 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 */ @@ -222,10 +288,10 @@ public class ModelGLPanel extends JPanel { readPixelsToImage(); // 创建错误图像 - BufferedImage errorImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + 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, width, height); + g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); g2d.setColor(Color.RED); g2d.drawString("渲染错误: " + errorMessage, 10, 20); g2d.dispose(); @@ -246,33 +312,60 @@ public class ModelGLPanel extends JPanel { */ private void readPixelsToImage() { try { - ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 4); - GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); + final int w = Math.max(1, this.width); + final int h = Math.max(1, this.height); + final int pixelCount = w * h; - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + // 确保缓冲区大小匹配(可能在 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]; + } - // 转换像素数据 - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int i = (x + (height - y - 1) * width) * 4; // 翻转 Y 轴 + pixelBuffer.clear(); + // 从 GPU 读取 RGBA 字节到本地缓冲 + GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixelBuffer); - int r = buffer.get(i) & 0xFF; - int g = buffer.get(i + 1) & 0xFF; - int b = buffer.get(i + 2) & 0xFF; + // 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转 + IntBuffer ib = pixelBuffer.asIntBuffer(); + ib.get(pixelInts, 0, pixelCount); - int rgb = (r << 16) | (g << 8) | b; - image.setRGB(x, y, rgb); + // 转换并翻转(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) { - System.err.println("读取像素数据错误: " + e.getMessage()); - // 创建错误图像 - BufferedImage errorImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + 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, width, height); + g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight()); g2d.setColor(Color.RED); g2d.drawString("像素读取失败", 10, 20); g2d.dispose(); @@ -284,32 +377,135 @@ public class ModelGLPanel extends JPanel { protected void paintComponent(Graphics g) { super.paintComponent(g); - if (currentFrame != null) { - // 绘制当前帧到面板 - g.drawImage(currentFrame, 0, 0, getWidth(), getHeight(), null); - } else { - // 显示加载中信息 - g.setColor(Color.DARK_GRAY); - g.fillRect(0, 0, getWidth(), getHeight()); - g.setColor(Color.WHITE); - g.drawString("初始化中...", getWidth() / 2 - 30, getHeight() / 2); - } + Graphics2D g2d = (Graphics2D) g.create(); + try { + // 选择要绘制的图像:优先 currentFrame(最新),其不存在则用 lastFrame(最后成功帧) + BufferedImage imgToDraw = currentFrame != null ? currentFrame : lastFrame; - // 如果模型为空,显示提示 - if (modelRef.get() == null) { - g.setColor(Color.YELLOW); - g.drawString("模型未加载", 10, 20); + 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) { - // 等待上下文就绪后再设置模型 - contextReady.thenRun(() -> { + executeInGLContext(() -> { modelRef.set(model); - System.out.println("模型已更新"); + logger.info("模型已更新"); }); } @@ -322,19 +518,51 @@ public class ModelGLPanel extends JPanel { /** * 重新设置面板大小 + * + * 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲, + * 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。 */ public void resize(int newWidth, int newHeight) { + // 更新 Swing 尺寸 setPreferredSize(new Dimension(newWidth, newHeight)); revalidate(); - // 在渲染线程中更新视口 - contextReady.thenRun(() -> { + // 在 GL 上下文线程中更新离屏窗口与缓冲 + executeInGLContext(() -> { if (contextInitialized && windowId != 0) { - GLFW.glfwMakeContextCurrent(windowId); - GL11.glViewport(0, 0, newWidth, newHeight); + // 更新内部宽高字段 + 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 的视口 + GL11.glViewport(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); } }); } @@ -358,6 +586,10 @@ public class ModelGLPanel extends JPanel { */ public void dispose() { running = false; + + // 停止任务执行器 + taskExecutor.shutdown(); + if (renderThread != null) { try { renderThread.join(2000); @@ -369,11 +601,38 @@ public class ModelGLPanel extends JPanel { } private void cleanup() { + // 清理 ModelRender + try { + if (ModelRender.isInitialized()) { + ModelRender.cleanup(); + logger.info("ModelRender 已清理"); + } + } catch (Exception e) { + logger.error("清理 ModelRender 时出错: {}", e.getMessage()); + } + if (windowId != 0) { - GLFW.glfwDestroyWindow(windowId); + try { + GLFW.glfwDestroyWindow(windowId); + } catch (Throwable ignored) {} windowId = 0; } - GLFW.glfwTerminate(); - System.out.println("OpenGL 资源已清理"); + + // 释放像素缓冲 + 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 资源已清理"); } -} \ No newline at end of file +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index e40b45f..be58c7c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -22,9 +22,31 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.lwjgl.opengl.GL20.glGetUniformLocation; /** - * 重构后的 ModelRender:更模块化、健壮的渲染子系统 - * (已修改以应用物理系统,并支持渲染碰撞箱) - * @author tzdwindows 7 + * vivid2D 模型完整渲染系统 + * + *

该系统提供了完整的 vivid2D 模型加载、渲染和显示功能,支持多种渲染模式和效果:

+ * + *
    + *
  • 基础模型渲染
  • + *
  • 光照效果渲染
  • + *
  • 纹理贴图渲染
  • + *
  • 模型加载与解析
  • + *
+ * + *

使用示例:

+ *
    + *
  • {@link com.chuangzhou.vivid2D.test.ModelLoadTest} - 模型加载测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderLightingTest} - 光照渲染测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTest} - 基础渲染测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTest2} - 进阶渲染测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTextureTest} - 纹理渲染测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelTest} - 基础模型测试
  • + *
  • {@link com.chuangzhou.vivid2D.test.ModelTest2} - 进阶模型测试
  • + *
+ * + * @author tzdwindows + * @version 1.0 + * @since 2025-10-13 */ public final class ModelRender { private static final Logger logger = LoggerFactory.getLogger(ModelRender.class); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index 8eff6ec..eb3335f 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -9,10 +9,28 @@ import org.joml.Matrix3f; import java.util.*; /** - * 2D模型核心数据结构 - * (已修改以配合 ModelRender 的物理系统应用) + * 2D 模型核心数据结构 * - * @author tzdwindows 7 + *

定义 vivid2D 模型系统中的核心数据结构和基础数据类型,包括:

+ * + *
    + *
  • 几何数据:顶点、边、面等基本几何元素
  • + *
  • 拓扑结构:模型的组织关系和连接信息
  • + *
  • 属性数据:颜色、纹理坐标、法向量等附加属性
  • + *
  • 层次结构:模型的父子关系和变换信息
  • + *
+ * + *

主要包含:

+ *
    + *
  • 基础几何类(点、向量、矩阵)
  • + *
  • 模型节点和组件类
  • + *
  • 数据容器和缓冲区
  • + *
  • 序列化和反序列化支持
  • + *
+ * + * @author tzdwindows + * @version 1.0 + * @since 2024-01-01 */ public class Model2D { // ==================== 基础属性 ==================== diff --git a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java index 1d8e256..1d92e31 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java @@ -2,28 +2,209 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.ModelGLPanel; 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.PhysicsSystem; +import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.joml.Vector2f; +import org.lwjgl.system.MemoryUtil; import javax.swing.*; +import java.awt.event.ActionEvent; +import java.nio.ByteBuffer; +/** + * 在原 TestModelGLPanel 的基础上增加简单动画(手臂、腿、头部摆动) + * @author tzdwindows 7 + */ public class TestModelGLPanel { private static final String MODEL_PATH = "C:\\Users\\Administrator\\Desktop\\trump_texture.model"; + + // 使 testModel 与动画计时可访问 + private static Model2D testModel; + private static float animationTime = 0f; + private static boolean animate = true; + public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("ModelGLPanel Demo"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - //com.chuangzhou.vivid2D.render.model.Model2D model = com.chuangzhou.vivid2D.render.model.Model2D.loadFromFile(MODEL_PATH); + ModelGLPanel glPanel = null; try { - Model2D model2D = new Model2D("Hi"); - glPanel = new ModelGLPanel(MODEL_PATH, 800, 600); + // 先创建一个空的 Model2D 实例(将在 GL 上下文中初始化更详细内容) + testModel = new Model2D("Humanoid"); + + glPanel = new ModelGLPanel(testModel, 800, 600); + + // 在 GL 上下文中创建 mesh / part / physics 等资源 + ModelGLPanel finalGlPanel = glPanel; + glPanel.executeInGLContext(() -> { + setupModelInGL(testModel); + return null; + }); + + // 创建一个 Swing Timer,用于驱动动画(~60 FPS) + int fps = 60; + int delayMs = 1000 / fps; + Timer timer = new Timer(delayMs, (ActionEvent e) -> { + if (!animate) return; + float dt = 1.0f / fps; + // 在 GL 上下文中更新模型状态(旋转、参数、物理更新等) + finalGlPanel.executeInGLContext(() -> { + updateAnimation(testModel, dt); + return null; + }); + // 请求重绘(ModelGLPanel 应在其 paintGL 中处理渲染) + finalGlPanel.repaint(); + }); + timer.start(); + + // 可选:在窗口上添加键盘控制开关(Space 切换动画) + frame.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyReleased(java.awt.event.KeyEvent e) { + if (e.getKeyCode() == java.awt.event.KeyEvent.VK_SPACE) { + animate = !animate; + System.out.println("Animation " + (animate ? "enabled" : "disabled")); + } + } + }); } catch (Exception e) { throw new RuntimeException(e); } + + // 将 GL 面板加入窗体并显示 frame.add(glPanel); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); }); } + + private static void setupModelInGL(Model2D model) { + PhysicsSystem physics = model.getPhysics(); + physics.setGravity(new Vector2f(0, -98.0f)); + physics.setAirResistance(0.05f); + physics.setTimeScale(1.0f); + physics.setEnabled(true); + physics.initialize(); + + // body 放在屏幕中心 + ModelPart body = model.createPart("body"); + body.setPosition(0, 0); + // 身体网格:宽 80 高 120 + Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120); + bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); // 蓝衣 + body.addMesh(bodyMesh); + + // head:相对于 body 在上方偏移 + ModelPart head = model.createPart("head"); + head.setPosition(0, -90); + Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60); + headMesh.setTexture(createHeadTexture()); + head.addMesh(headMesh); + + // left arm + ModelPart leftArm = model.createPart("left_arm"); + leftArm.setPosition(-60, -20); + Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90); + leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); + leftArm.addMesh(leftArmMesh); + + // right arm + ModelPart rightArm = model.createPart("right_arm"); + rightArm.setPosition(60, -20); + Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90); + rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); + rightArm.addMesh(rightArmMesh); + + // left leg + ModelPart leftLeg = model.createPart("left_leg"); + leftLeg.setPosition(-20, 90); + Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100); + leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); + leftLeg.addMesh(leftLegMesh); + + // right leg + ModelPart rightLeg = model.createPart("right_leg"); + rightLeg.setPosition(20, 90); + Mesh2D rightLegMesh = Mesh2D.createQuad("right_leg_mesh", 20, 100); + rightLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); + rightLeg.addMesh(rightLegMesh); + + // 建立层级:body 为根 + body.addChild(head); + body.addChild(leftArm); + body.addChild(rightArm); + body.addChild(leftLeg); + body.addChild(rightLeg); + + // 创建动画参数用于简单摆动(可选,示例中也直接对 Part 旋转) + model.createParameter("arm_swing", -1.0f, 1.0f, 0f); + model.createParameter("leg_swing", -1.0f, 1.0f, 0f); + model.createParameter("head_rotation", -0.5f, 0.5f, 0f); + + System.out.println("Humanoid model created with parts: " + model.getParts().size()); + } + + private static void updateAnimation(Model2D model, float dt) { + animationTime += dt; + float armSwing = (float) Math.sin(animationTime * 3.0f) * 0.7f; // -0.7 .. 0.7 + float legSwing = (float) Math.sin(animationTime * 3.0f + Math.PI) * 0.6f; + float headRot = (float) Math.sin(animationTime * 1.4f) * 0.15f; + + model.setParameterValue("arm_swing", armSwing); + model.setParameterValue("leg_swing", legSwing); + model.setParameterValue("head_rotation", headRot); + + ModelPart leftArm = model.getPart("left_arm"); + ModelPart rightArm = model.getPart("right_arm"); + ModelPart leftLeg = model.getPart("left_leg"); + ModelPart rightLeg = model.getPart("right_leg"); + ModelPart head = model.getPart("head"); + + if (leftArm != null) leftArm.setRotation(-0.8f * armSwing - 0.2f); + if (rightArm != null) rightArm.setRotation(0.8f * armSwing + 0.2f); + if (leftLeg != null) leftLeg.setRotation(0.6f * legSwing); + if (rightLeg != null) rightLeg.setRotation(-0.6f * legSwing); + if (head != null) head.setRotation(headRot); + + // 更新物理与层级(如果 Model2D.update 会进行必要的矩阵/物理计算) + model.update(dt); + } + + private static Texture createSolidTexture(int w, int h, int rgba) { + ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4); + byte a = (byte) ((rgba >> 24) & 0xFF); + byte r = (byte) ((rgba >> 16) & 0xFF); + byte g = (byte) ((rgba >> 8) & 0xFF); + byte b = (byte) (rgba & 0xFF); + for (int i = 0; i < w * h; i++) { + buf.put(r).put(g).put(b).put(a); + } + buf.flip(); + Texture t = new Texture("solid_" + rgba + "_" + w + "x" + h, w, h, Texture.TextureFormat.RGBA, buf); + MemoryUtil.memFree(buf); + return t; + } + + private static Texture createHeadTexture() { + int width = 64, height = 64; + int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float dx = (x - width / 2f) / (width / 2f); + float dy = (y - height / 2f) / (height / 2f); + float dist = (float) Math.sqrt(dx * dx + dy * dy); + int alpha = dist > 1.0f ? 0 : 255; + int r = (int) (240 * (1.0f - dist * 0.25f)); + int g = (int) (200 * (1.0f - dist * 0.25f)); + int b = (int) (180 * (1.0f - dist * 0.25f)); + pixels[y * width + x] = (alpha << 24) | (r << 16) | (g << 8) | b; + } + } + return new Texture("head_tex", width, height, Texture.TextureFormat.RGBA, pixels); + } }