feat(render):重构 ModelGLPanel与 ModelRender 并增强渲染功能
- 重构 ModelGLPanel 支持动态尺寸调整和离屏渲染上下文重建 - 添加 GL 上下文任务队列机制,支持线程安全的 OpenGL 操作- 引入 SLF4J 日志系统替换原有 System.out 输出 - 优化像素读取逻辑,支持 ARGB 格式与图像缓冲复用- 增强错误处理与资源清理逻辑,提升稳定性 - 完善 Model2D与 ModelRender 类的文档注释与结构定义 - 新增 TestModelGLPanel 动画示例,展示模型部件控制与物理系统应用
This commit is contained in:
@@ -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 渲染面板
|
||||
*
|
||||
* <p>该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能,
|
||||
* 包含基本的 2D 图形绘制、模型显示和交互操作。</p>
|
||||
*
|
||||
* <p>具体使用示例请参考:{@code com.chuangzhou.vivid2D.test.TestModelGLPanel}</p>
|
||||
*
|
||||
* @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<Model2D> 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<Void> contextReady = new CompletableFuture<>();
|
||||
private final String modelPath;
|
||||
|
||||
// 任务队列,用于在 GL 上下文线程执行代码
|
||||
private final BlockingQueue<Runnable> 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<Void> executeInGLContext(Runnable task) {
|
||||
CompletableFuture<Void> 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 <T> CompletableFuture<T> executeInGLContext(Callable<T> task) {
|
||||
CompletableFuture<T> 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<Void> future = executeInGLContext(task);
|
||||
future.get(10, TimeUnit.SECONDS); // 设置超时时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成)
|
||||
* @param task 要在 GL 上下文线程中执行的有返回值的任务
|
||||
* @return 任务执行结果
|
||||
* @throws Exception 如果任务执行出错或超时
|
||||
*/
|
||||
public <T> T executeInGLContextSync(Callable<T> task) throws Exception {
|
||||
if (!running) {
|
||||
throw new IllegalStateException("渲染线程已停止");
|
||||
}
|
||||
|
||||
CompletableFuture<T> 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 资源已清理");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,31 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.lwjgl.opengl.GL20.glGetUniformLocation;
|
||||
|
||||
/**
|
||||
* 重构后的 ModelRender:更模块化、健壮的渲染子系统
|
||||
* (已修改以应用物理系统,并支持渲染碰撞箱)
|
||||
* @author tzdwindows 7
|
||||
* vivid2D 模型完整渲染系统
|
||||
*
|
||||
* <p>该系统提供了完整的 vivid2D 模型加载、渲染和显示功能,支持多种渲染模式和效果:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>基础模型渲染</li>
|
||||
* <li>光照效果渲染</li>
|
||||
* <li>纹理贴图渲染</li>
|
||||
* <li>模型加载与解析</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>使用示例:</h3>
|
||||
* <ul>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelLoadTest} - 模型加载测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelRenderLightingTest} - 光照渲染测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelRenderTest} - 基础渲染测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelRenderTest2} - 进阶渲染测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelRenderTextureTest} - 纹理渲染测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelTest} - 基础模型测试</li>
|
||||
* <li>{@link com.chuangzhou.vivid2D.test.ModelTest2} - 进阶模型测试</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author tzdwindows
|
||||
* @version 1.0
|
||||
* @since 2025-10-13
|
||||
*/
|
||||
public final class ModelRender {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ModelRender.class);
|
||||
|
||||
@@ -9,10 +9,28 @@ import org.joml.Matrix3f;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 2D模型核心数据结构
|
||||
* (已修改以配合 ModelRender 的物理系统应用)
|
||||
* 2D 模型核心数据结构
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
* <p>定义 vivid2D 模型系统中的核心数据结构和基础数据类型,包括:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>几何数据:顶点、边、面等基本几何元素</li>
|
||||
* <li>拓扑结构:模型的组织关系和连接信息</li>
|
||||
* <li>属性数据:颜色、纹理坐标、法向量等附加属性</li>
|
||||
* <li>层次结构:模型的父子关系和变换信息</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>主要包含:</h3>
|
||||
* <ul>
|
||||
* <li>基础几何类(点、向量、矩阵)</li>
|
||||
* <li>模型节点和组件类</li>
|
||||
* <li>数据容器和缓冲区</li>
|
||||
* <li>序列化和反序列化支持</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author tzdwindows
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class Model2D {
|
||||
// ==================== 基础属性 ====================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user