From 1bc2634afb02b18c590334f9ac5f8b3ac2f5281e Mon Sep 17 00:00:00 2001
From: tzdwindows 7 <3076584115@qq.com>
Date: Mon, 13 Oct 2025 22:12:30 +0800
Subject: [PATCH] =?UTF-8?q?feat(render):=E9=87=8D=E6=9E=84=20ModelGLPanel?=
=?UTF-8?q?=E4=B8=8E=20ModelRender=20=E5=B9=B6=E5=A2=9E=E5=BC=BA=E6=B8=B2?=
=?UTF-8?q?=E6=9F=93=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重构 ModelGLPanel 支持动态尺寸调整和离屏渲染上下文重建
- 添加 GL 上下文任务队列机制,支持线程安全的 OpenGL 操作- 引入 SLF4J 日志系统替换原有 System.out 输出
- 优化像素读取逻辑,支持 ARGB 格式与图像缓冲复用- 增强错误处理与资源清理逻辑,提升稳定性
- 完善 Model2D与 ModelRender 类的文档注释与结构定义
- 新增 TestModelGLPanel 动画示例,展示模型部件控制与物理系统应用
---
.../vivid2D/render/ModelGLPanel.java | 397 +++++++++++++++---
.../vivid2D/render/ModelRender.java | 28 +-
.../vivid2D/render/model/Model2D.java | 24 +-
.../vivid2D/test/TestModelGLPanel.java | 187 ++++++++-
4 files changed, 558 insertions(+), 78 deletions(-)
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);
+ }
}