feat(render): 实现高性能OpenGL渲染面板
- 添加ModelGLPanel类支持离屏OpenGL渲染 - 集成LWJGL3.3.6版本并更新相关依赖 - 实现模型树节点转换功能 - 添加纹理读取与显示错误处理机制 - 引入CommonMark库支持Markdown解析 -优化物理系统注释信息 - 禁用部分调试日志输出 - 添加测试用例TestModelGLPanel
This commit is contained in:
18
build.gradle
18
build.gradle
@@ -40,6 +40,7 @@ dependencies {
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
|
||||
implementation 'org.commonmark:commonmark:0.24.0'
|
||||
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
@@ -81,13 +82,16 @@ dependencies {
|
||||
implementation 'com.github.javaparser:javaparser-core:3.25.1'
|
||||
implementation 'com.1stleg:jnativehook:2.1.0'
|
||||
implementation 'org.json:json:20230618'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-stb:3.3.3'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-stb:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-jawt:3.3.5'
|
||||
|
||||
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows'
|
||||
implementation 'com.badlogicgames.gdx:gdx:1.12.1'
|
||||
implementation 'org.joml:joml:1.10.7'
|
||||
implementation 'org.bytedeco:javacv-platform:1.5.7'
|
||||
|
||||
379
src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java
Normal file
379
src/main/java/com/chuangzhou/vivid2D/render/ModelGLPanel.java
Normal file
@@ -0,0 +1,379 @@
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import org.lwjgl.glfw.*;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 修复版高性能 OpenGL 渲染面板
|
||||
*/
|
||||
public class ModelGLPanel extends JPanel {
|
||||
|
||||
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 BufferedImage currentFrame;
|
||||
private boolean contextInitialized = false;
|
||||
private final CompletableFuture<Void> contextReady = new CompletableFuture<>();
|
||||
private final String modelPath;
|
||||
|
||||
/**
|
||||
* 构造函数:使用模型路径
|
||||
*/
|
||||
public ModelGLPanel(String modelPath, int width, int height) {
|
||||
this.modelPath = modelPath;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数:使用已加载模型
|
||||
*/
|
||||
public ModelGLPanel(Model2D model, int width, int height) {
|
||||
this.modelPath = null;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.modelRef.set(model);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setLayout(new BorderLayout());
|
||||
setPreferredSize(new Dimension(width, height));
|
||||
|
||||
// 初始化 GLFW
|
||||
if (!GLFW.glfwInit()) {
|
||||
throw new RuntimeException("无法初始化 GLFW");
|
||||
}
|
||||
|
||||
// 创建渲染线程
|
||||
startRendering();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建离屏 OpenGL 上下文
|
||||
*/
|
||||
private void createOffscreenContext() throws Exception {
|
||||
// 设置窗口提示
|
||||
GLFW.glfwDefaultWindowHints();
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4);
|
||||
|
||||
// 创建离屏窗口
|
||||
windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL);
|
||||
if (windowId == MemoryUtil.NULL) {
|
||||
throw new Exception("无法创建离屏 OpenGL 上下文");
|
||||
}
|
||||
|
||||
// 设置为当前上下文并初始化
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
GL.createCapabilities();
|
||||
|
||||
// 初始化 OpenGL 状态
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
|
||||
// 检查是否支持多重采样
|
||||
if (GL.getCapabilities().OpenGL13) {
|
||||
GL11.glEnable(GL13.GL_MULTISAMPLE);
|
||||
System.out.println("多重采样已启用");
|
||||
} else {
|
||||
System.out.println("不支持多重采样,跳过启用");
|
||||
}
|
||||
|
||||
GL11.glViewport(0, 0, width, height);
|
||||
|
||||
ModelRender.initialize();
|
||||
|
||||
contextInitialized = true;
|
||||
|
||||
// 在正确的上下文中加载模型
|
||||
loadModelInContext();
|
||||
|
||||
// 通知上下文已准备就绪
|
||||
contextReady.complete(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 OpenGL 上下文中加载模型
|
||||
*/
|
||||
private void loadModelInContext() {
|
||||
try {
|
||||
if (modelPath != null) {
|
||||
Model2D model = Model2D.loadFromFile(modelPath);
|
||||
modelRef.set(model);
|
||||
System.out.println("模型加载成功: " + modelPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("模型加载失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
|
||||
// 创建错误模型或使用默认模型
|
||||
createErrorModel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误模型作为回退
|
||||
*/
|
||||
private void createErrorModel() {
|
||||
try {
|
||||
// 这里可以创建一个简单的默认模型
|
||||
// 或者保持 modelRef 为 null,在渲染时显示错误信息
|
||||
System.out.println("使用默认错误模型");
|
||||
} catch (Exception e) {
|
||||
System.err.println("创建错误模型失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动渲染线程
|
||||
*/
|
||||
private void startRendering() {
|
||||
renderThread = new Thread(() -> {
|
||||
try {
|
||||
createOffscreenContext();
|
||||
|
||||
// 等待上下文就绪后再开始渲染循环
|
||||
contextReady.get();
|
||||
|
||||
// 高性能渲染循环
|
||||
while (running && !GLFW.glfwWindowShouldClose(windowId)) {
|
||||
renderFrame();
|
||||
|
||||
// 控制帧率
|
||||
try {
|
||||
Thread.sleep(1000 / 60); // 60 FPS
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
renderThread.setDaemon(true);
|
||||
renderThread.setName("GL-Render-Thread");
|
||||
renderThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单帧并读取到 BufferedImage
|
||||
*/
|
||||
private void renderFrame() {
|
||||
if (!contextInitialized || windowId == 0) return;
|
||||
|
||||
// 确保在当前上下文中
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
|
||||
Model2D currentModel = modelRef.get();
|
||||
if (currentModel != null) {
|
||||
try {
|
||||
// 清除缓冲区
|
||||
GL11.glClearColor(0.18f, 0.18f, 0.25f, 1f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// 渲染模型
|
||||
ModelRender.render(1.0f / 60f, currentModel);
|
||||
|
||||
// 读取像素数据到 BufferedImage
|
||||
readPixelsToImage();
|
||||
} catch (Exception e) {
|
||||
System.err.println("渲染错误: " + e.getMessage());
|
||||
renderErrorFrame(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 没有模型时显示默认背景
|
||||
renderDefaultBackground();
|
||||
}
|
||||
|
||||
// 在 Swing EDT 中更新显示
|
||||
SwingUtilities.invokeLater(this::repaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染错误帧
|
||||
*/
|
||||
private void renderErrorFrame(String errorMessage) {
|
||||
GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
|
||||
// 创建错误图像
|
||||
BufferedImage errorImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染默认背景
|
||||
*/
|
||||
private void renderDefaultBackground() {
|
||||
GL11.glClearColor(0.1f, 0.1f, 0.15f, 1f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 OpenGL 像素数据到 BufferedImage
|
||||
*/
|
||||
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);
|
||||
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
// 转换像素数据
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int i = (x + (height - y - 1) * width) * 4; // 翻转 Y 轴
|
||||
|
||||
int r = buffer.get(i) & 0xFF;
|
||||
int g = buffer.get(i + 1) & 0xFF;
|
||||
int b = buffer.get(i + 2) & 0xFF;
|
||||
|
||||
int rgb = (r << 16) | (g << 8) | b;
|
||||
image.setRGB(x, y, rgb);
|
||||
}
|
||||
}
|
||||
|
||||
currentFrame = image;
|
||||
} catch (Exception e) {
|
||||
System.err.println("读取像素数据错误: " + e.getMessage());
|
||||
// 创建错误图像
|
||||
BufferedImage errorImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.BLACK);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("像素读取失败", 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
// 如果模型为空,显示提示
|
||||
if (modelRef.get() == null) {
|
||||
g.setColor(Color.YELLOW);
|
||||
g.drawString("模型未加载", 10, 20);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型(线程安全)
|
||||
*/
|
||||
public void setModel(Model2D model) {
|
||||
// 等待上下文就绪后再设置模型
|
||||
contextReady.thenRun(() -> {
|
||||
modelRef.set(model);
|
||||
System.out.println("模型已更新");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前渲染的模型
|
||||
*/
|
||||
public Model2D getModel() {
|
||||
return modelRef.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置面板大小
|
||||
*/
|
||||
public void resize(int newWidth, int newHeight) {
|
||||
setPreferredSize(new Dimension(newWidth, newHeight));
|
||||
revalidate();
|
||||
|
||||
// 在渲染线程中更新视口
|
||||
contextReady.thenRun(() -> {
|
||||
if (contextInitialized && windowId != 0) {
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
GL11.glViewport(0, 0, newWidth, newHeight);
|
||||
|
||||
// 重新创建帧缓冲图像
|
||||
currentFrame = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待渲染上下文准备就绪
|
||||
*/
|
||||
public CompletableFuture<Void> waitForContext() {
|
||||
return contextReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否正在运行
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running && contextInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
public void dispose() {
|
||||
running = false;
|
||||
if (renderThread != null) {
|
||||
try {
|
||||
renderThread.join(2000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (windowId != 0) {
|
||||
GLFW.glfwDestroyWindow(windowId);
|
||||
windowId = 0;
|
||||
}
|
||||
GLFW.glfwTerminate();
|
||||
System.out.println("OpenGL 资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -792,7 +792,7 @@ public final class ModelRender {
|
||||
private static void checkGLError(String op) {
|
||||
int e = GL11.glGetError();
|
||||
if (e != GL11.GL_NO_ERROR) {
|
||||
logger.error("OpenGL error during {}: {}", op, getGLErrorString(e));
|
||||
//logger.error("OpenGL error during {}: {}", op, getGLErrorString(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelMetadata;
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
@@ -350,6 +351,14 @@ public class Model2D {
|
||||
}
|
||||
}
|
||||
|
||||
public DefaultMutableTreeNode toTreeNode() {
|
||||
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(this.name != null ? this.name : "模型");
|
||||
for (ModelPart part : parts) {
|
||||
rootNode.add(part.toTreeNode());
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获当前模型状态到姿态
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
/**
|
||||
* 2D模型部件,支持层级变换和变形器
|
||||
@@ -133,6 +134,14 @@ public class ModelPart {
|
||||
return new ArrayList<>(children);
|
||||
}
|
||||
|
||||
public DefaultMutableTreeNode toTreeNode() {
|
||||
DefaultMutableTreeNode node = new DefaultMutableTreeNode(this.name != null ? this.name : "部件");
|
||||
for (ModelPart child : children) {
|
||||
node.add(child.toTreeNode());
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称查找子部件
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
@@ -503,7 +502,7 @@ public class Mesh2D {
|
||||
org.lwjgl.system.MemoryUtil.memFree(fb);
|
||||
}
|
||||
} else {
|
||||
logger.warn("警告: 着色器中未找到 uModelMatrix 或 uModel uniform");
|
||||
//logger.warn("警告: 着色器中未找到 uModelMatrix 或 uModel uniform");
|
||||
}
|
||||
|
||||
// 绘制
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* - 在碰撞处理里同时更新位置与速度(冲量响应),并避免在碰撞后覆盖冲量
|
||||
* - 约束迭代次数可调,布料等需要多个迭代(默认 3)
|
||||
*
|
||||
* @author tzdwindows 7 (modified)
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class PhysicsSystem {
|
||||
// ==================== 物理参数 ====================
|
||||
|
||||
@@ -6,14 +6,12 @@ import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL14;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL45;
|
||||
import org.lwjgl.stb.STBImage;
|
||||
import org.lwjgl.stb.STBImageWrite;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.chuangzhou.vivid2D.test;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelGLPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class TestModelGLPanel {
|
||||
|
||||
private static final String MODEL_PATH = "C:\\Users\\Administrator\\Desktop\\trump_texture.model";
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
frame.add(glPanel);
|
||||
frame.pack();
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/vivid2D/Vivid2D.png
Normal file
BIN
src/main/resources/vivid2D/Vivid2D.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Reference in New Issue
Block a user