feat(render):重构 ModelGLPanel与 ModelRender 并增强渲染功能

- 重构 ModelGLPanel 支持动态尺寸调整和离屏渲染上下文重建
- 添加 GL 上下文任务队列机制,支持线程安全的 OpenGL 操作- 引入 SLF4J 日志系统替换原有 System.out 输出
- 优化像素读取逻辑,支持 ARGB 格式与图像缓冲复用- 增强错误处理与资源清理逻辑,提升稳定性
- 完善 Model2D与 ModelRender 类的文档注释与结构定义
- 新增 TestModelGLPanel 动画示例,展示模型部件控制与物理系统应用
This commit is contained in:
tzdwindows 7
2025-10-13 22:12:30 +08:00
parent 082478cdb6
commit 1bc2634afb
4 changed files with 558 additions and 78 deletions

View File

@@ -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 资源已清理");
}
}
}

View File

@@ -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);

View File

@@ -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 {
// ==================== 基础属性 ====================

View File

@@ -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);
}
}