feat(render): 实现模型图层管理与选中高亮功能

- 添加 ModelLayerPanel 图层管理面板,支持图层增删、重排、重命名- 实现 Mesh2D 选中状态管理与可视化高亮边框绘制
- 添加模型点击与悬停事件监听接口 ModelClickListener
- 引入完整着色器接口 CompleteShader 及默认片段着色器实现
- 改进 BufferUploader 支持颜色 uniform 传递- 完善 Mesh2D 复制逻辑与边界框计算方法
- 重构部分工具类包路径并增强矩阵工具功能
- 移除 LightSourceData 中冗余的构造逻辑

重要更新
- 更新了一个可视化界面可以控制图层顺序(ModelLayerPanel),并且给ModelRenderPanel增加了很多新功能,比如设置模型图层位置、大小
- 重写了逻辑着色器(Shader)、BufferUploader逻辑,让着色器能够规范的注册和使用
This commit is contained in:
tzdwindows 7
2025-10-17 18:16:24 +08:00
parent 27744d4b5c
commit 879069a9f4
25 changed files with 3700 additions and 838 deletions

View File

@@ -8,7 +8,9 @@ import com.chuangzhou.vivid2D.render.model.util.LightSource;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.ShaderSources;
import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
import org.joml.Matrix3f;
import org.joml.Vector2f;
import org.joml.Vector4f;
@@ -19,8 +21,6 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.lwjgl.opengl.GL20.glGetUniformLocation;
/**
* vivid2D 模型完整渲染系统
*
@@ -106,24 +106,20 @@ public final class ModelRender {
*/
private static final boolean enableBlending = true;
private static final int SHADER_MAX_LIGHTS = 8;
/**
* 最大光源数量,用于限制同时启用的光源数量
* 默认值80
*/
private static final int MAX_LIGHTS = 80;
// ================== 着色器与资源管理 ==================
/**
* 着色器程序缓存映射,按名称存储已编译的着色器程序
* 键:着色器名称(如 "default"
* 值:对应的着色器程序对象
* @see ShaderSources.ShaderProgram
*/
private static final Map<String, ShaderSources.ShaderProgram> shaderMap = new HashMap<>();
/**
* 默认着色器程序,用于大多数模型的渲染
* 包含基础的光照、纹理和变换功能
* @see #compileDefaultShader()
*/
private static ShaderSources.ShaderProgram defaultProgram = null;
private static ShaderProgram defaultProgram = null;
/**
* 网格GPU资源缓存管理已上传到GPU的网格数据
@@ -183,7 +179,7 @@ public final class ModelRender {
*/
public static boolean renderLightPositions = true;
// ================== 内部类:ShaderSources.ShaderProgram ==================
// ================== 内部类ShaderProgram ==================
// ================== 内部类MeshGLResources ==================
@@ -216,6 +212,10 @@ public final class ModelRender {
try {
compileDefaultShader();
// 初始化所有非默认着色器的基础信息
initNonDefaultShaders();
} catch (RuntimeException ex) {
logger.error("Failed to compile default shader: {}", ex.getMessage());
throw ex;
@@ -229,6 +229,76 @@ public final class ModelRender {
logger.info("ModelRender initialized successfully");
}
/**
* 初始化所有非默认着色器的基础信息(顶点坐标等)
*/
private static void initNonDefaultShaders() {
List<CompleteShader> shaderList = ShaderManagement.getShaderList();
if (shaderList == null || shaderList.isEmpty()) {
logger.info("No shaders found to initialize");
return;
}
int nonDefaultCount = 0;
for (CompleteShader shader : shaderList) {
// 跳过默认着色器,只初始化非默认的
if (shader.isDefaultShader()) {
continue;
}
try {
// 获取着色器程序
ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName());
if (program == null) {
logger.warn("Shader program not found for: {}", shader.getShaderName());
continue;
}
// 设置着色器的基础uniforms主要是顶点坐标相关的
initShaderBasicUniforms(program, shader);
nonDefaultCount++;
logger.debug("Initialized non-default shader: {}", shader.getShaderName());
} catch (Exception e) {
logger.error("Failed to initialize non-default shader: {}", shader.getShaderName(), e);
}
}
logger.info("Initialized {} non-default shaders", nonDefaultCount);
}
/**
* 初始化着色器的基础uniforms顶点坐标相关
*/
private static void initShaderBasicUniforms(ShaderProgram program, CompleteShader shader) {
program.use();
try {
// 设置基础的变换矩阵为单位矩阵
setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity());
setUniformMatrix3(program, "uViewMatrix", new Matrix3f().identity());
// 设置投影矩阵(使用当前视口尺寸)
Matrix3f projection = buildOrthoProjection(viewportWidth, viewportHeight);
setUniformMatrix3(program, "uProjectionMatrix", projection);
// 设置基础颜色为白色
setUniformVec4Internal(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
// 设置基础不透明度
setUniformFloatInternal(program, "uOpacity", 1.0f);
// 设置纹理单元(如果有纹理的话)
setUniformIntInternal(program, "uTexture", 0);
RenderSystem.checkGLError("initShaderBasicUniforms_" + shader.getShaderName());
} finally {
program.stop();
}
}
private static void logGLInfo() {
logger.info("OpenGL Vendor: {}", RenderSystem.getVendor());
logger.info("OpenGL Renderer: {}", RenderSystem.getRenderer());
@@ -238,12 +308,11 @@ public final class ModelRender {
}
private static void uploadLightsToShader(ShaderSources.ShaderProgram sp, Model2D model) {
private static void uploadLightsToShader(ShaderProgram sp, Model2D model) {
List<com.chuangzhou.vivid2D.render.model.util.LightSource> lights = model.getLights();
int idx = 0;
// 只上传已启用的光源,最多 MAX_LIGHTS8
for (int i = 0; i < lights.size() && idx < 8; i++) {
for (int i = 0; i < lights.size() && idx < MAX_LIGHTS; i++) {
com.chuangzhou.vivid2D.render.model.util.LightSource l = lights.get(i);
if (!l.isEnabled()) continue;
@@ -267,7 +336,7 @@ public final class ModelRender {
setUniformIntInternal(sp, "uLightCount", idx);
// 禁用剩余槽位(确保 shader 中不会读取到垃圾值)
for (int i = idx; i < 8; i++) {
for (int i = idx; i < MAX_LIGHTS; i++) {
setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f);
setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0);
setUniformVec3Internal(sp, "uLightsColor[" + i + "]", new org.joml.Vector3f(0f, 0f, 0f));
@@ -284,53 +353,48 @@ public final class ModelRender {
private static void setupGLState() {
RenderSystem.checkGLError("setupGLState_start");
RenderSystem.clearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w);
RenderSystem.checkGLError("after_clearColor");
if (enableBlending) {
RenderSystem.enableBlend();
RenderSystem.checkGLError("after_enableBlend");
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
RenderSystem.checkGLError("after_blendFunc");
} else {
RenderSystem.disableBlend();
RenderSystem.checkGLError("after_disableBlend");
}
if (enableDepthTest) {
RenderSystem.enableDepthTest();
RenderSystem.checkGLError("after_enableDepthTest");
RenderSystem.depthFunc(GL11.GL_LEQUAL);
RenderSystem.checkGLError("after_depthFunc");
RenderSystem.depthMask(true);
RenderSystem.checkGLError("after_depthMask");
RenderSystem.clearDepth(1.0);
RenderSystem.checkGLError("after_clearDepth");
} else {
RenderSystem.disableDepthTest();
RenderSystem.checkGLError("after_disableDepthTest");
}
RenderSystem.checkGLError("setupGLState");
RenderSystem.checkGLError("after_disableCullFace");
}
private static void compileDefaultShader() {
int vs = compileShader(GL20.GL_VERTEX_SHADER, ShaderSources.VERTEX_SHADER_SRC);
int fs = compileShader(GL20.GL_FRAGMENT_SHADER, ShaderSources.FRAGMENT_SHADER_SRC);
int prog = linkProgram(vs, fs);
ShaderSources.ShaderProgram sp = new ShaderSources.ShaderProgram(prog);
shaderMap.put("default", sp);
defaultProgram = sp;
sp.use();
setUniformIntInternal(sp, "uTexture", 0);
setUniformFloatInternal(sp, "uOpacity", 1.0f);
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
setUniformIntInternal(sp, "uBlendMode", 0);
setUniformIntInternal(sp, "uDebugMode", 0);
setUniformIntInternal(sp, "uLightCount", 0); // 默认没有光源
sp.stop();
}
private static int compileShader(int type, String src) {
RenderSystem.assertOnRenderThread();
return RenderSystem.compileShader(type, src);
}
private static int linkProgram(int vs, int fs) {
RenderSystem.assertOnRenderThread();
return RenderSystem.linkProgram(vs, fs);
ShaderManagement.compileAllShaders();
defaultProgram = ShaderManagement.getDefaultProgram();
if (defaultProgram == null) {
throw new RuntimeException("Failed to compile default shader: no default shader found");
}
}
private static void createDefaultTexture() {
@@ -348,9 +412,8 @@ public final class ModelRender {
for (MeshGLResources r : meshResources.values()) r.dispose();
meshResources.clear();
// shaders
for (ShaderSources.ShaderProgram sp : shaderMap.values()) sp.delete();
shaderMap.clear();
// 使用新的着色器管理系统清理着色器
ShaderManagement.cleanup();
defaultProgram = null;
// textures
@@ -393,25 +456,33 @@ public final class ModelRender {
return;
}
defaultProgram.use();
RenderSystem.checkGLError("after_use_program");
// 设置投影与视图
// 设置投影与视图矩阵(所有着色器都需要)
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
setUniformMatrix3(defaultProgram, "uViewMatrix", new Matrix3f().identity());
RenderSystem.checkGLError("after_set_matrices");
Matrix3f view = new Matrix3f().identity();
// 添加光源数据上传
// 1. 首先设置默认着色器
defaultProgram.use();
RenderSystem.checkGLError("after_use_default_program");
// 设置默认着色器的投影与视图
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
setUniformMatrix3(defaultProgram, "uViewMatrix", view);
RenderSystem.checkGLError("after_set_default_matrices");
// 添加光源数据上传到默认着色器
uploadLightsToShader(defaultProgram, model);
RenderSystem.checkGLError("after_upload_lights");
// 2. 设置非默认着色器的顶点坐标相关uniform
setupNonDefaultShaders(proj, view);
RenderSystem.checkGLError("after_setup_non_default_shaders");
// 在渲染光源位置前检查
RenderSystem.checkGLError("before_render_light_positions");
renderLightPositions(model);
RenderSystem.checkGLError("after_render_light_positions");
// 递归渲染所有根部件
// 递归渲染所有根部件(使用默认着色器)
Matrix3f identity = new Matrix3f().identity();
for (ModelPart p : model.getParts()) {
if (p.getParent() != null) continue;
@@ -428,6 +499,55 @@ public final class ModelRender {
RenderSystem.checkGLError("render_end");
}
/**
* 设置所有非默认着色器的顶点坐标相关uniform
*/
private static void setupNonDefaultShaders(Matrix3f projection, Matrix3f view) {
List<CompleteShader> shaderList = ShaderManagement.getShaderList();
if (shaderList == null || shaderList.isEmpty()) {
return;
}
// 保存当前绑定的着色器程序
int currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
try {
for (CompleteShader shader : shaderList) {
// 跳过默认着色器
if (shader.isDefaultShader()) {
continue;
}
try {
// 获取着色器程序
ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName());
if (program == null || program.programId == 0) {
continue;
}
program.use();
// 只设置顶点坐标相关的uniform
setUniformMatrix3(program, "uProjectionMatrix", projection);
setUniformMatrix3(program, "uViewMatrix", view);
// 设置基础模型矩阵为单位矩阵
setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity());
RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName());
} catch (Exception e) {
logger.warn("Failed to setup non-default shader: {}", shader.getShaderName(), e);
}
}
} finally {
// 恢复之前绑定的着色器程序
if (currentProgram != 0) {
GL20.glUseProgram(currentProgram);
}
}
}
private static void renderLightPositions(Model2D model) {
if (!renderLightPositions) return;
// 设置灯泡颜色为光源的颜色
@@ -658,39 +778,39 @@ public final class ModelRender {
}
// ================== uniform 设置辅助(内部使用,确保 program 已绑定) ==================
private static void setUniformIntInternal(ShaderSources.ShaderProgram sp, String name, int value) {
private static void setUniformIntInternal(ShaderProgram sp, String name, int value) {
int loc = sp.getUniformLocation(name);
if (loc != -1) RenderSystem.uniform1i(loc, value);
}
private static void setUniformVec3Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector3f vec) {
private static void setUniformVec3Internal(ShaderProgram sp, String name, org.joml.Vector3f vec) {
int loc = sp.getUniformLocation(name);
if (loc != -1) RenderSystem.uniform3f(loc, vec);
}
private static void setUniformVec2Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector2f vec) {
private static void setUniformVec2Internal(ShaderProgram sp, String name, org.joml.Vector2f vec) {
int loc = sp.getUniformLocation(name);
if (loc != -1) RenderSystem.uniform2f(loc, vec);
}
private static void setUniformFloatInternal(ShaderSources.ShaderProgram sp, String name, float value) {
private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) {
int loc = sp.getUniformLocation(name);
if (loc != -1) RenderSystem.uniform1f(loc, value);
}
private static void setUniformVec4Internal(ShaderSources.ShaderProgram sp, String name, org.joml.Vector4f vec) {
private static void setUniformVec4Internal(ShaderProgram sp, String name, org.joml.Vector4f vec) {
int loc = sp.getUniformLocation(name);
if (loc != -1) RenderSystem.uniform4f(loc, vec);
}
private static void setUniformMatrix3(ShaderSources.ShaderProgram sp, String name, org.joml.Matrix3f m) {
private static void setUniformMatrix3(ShaderProgram sp, String name, org.joml.Matrix3f m) {
int loc = sp.getUniformLocation(name);
if (loc == -1) return;
RenderSystem.uniformMatrix3(loc, m);
}
// ================== 部件属性 ==================
private static void setPartUniforms(ShaderSources.ShaderProgram sp, ModelPart part) {
private static void setPartUniforms(ShaderProgram sp, ModelPart part) {
setUniformFloatInternal(sp, "uOpacity", part.getOpacity());
int blend = 0;
ModelPart.BlendMode bm = part.getBlendMode();

View File

@@ -0,0 +1,30 @@
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
/**
* 模型点击事件监听器接口
*
* @author tzdwindows 7
*/
public interface ModelClickListener {
/**
* 当点击模型时触发
* @param mesh 被点击的网格,如果点击在空白处则为 null
* @param modelX 模型坐标系中的 X 坐标
* @param modelY 模型坐标系中的 Y 坐标
* @param screenX 屏幕坐标系中的 X 坐标
* @param screenY 屏幕坐标系中的 Y 坐标
*/
void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY);
/**
* 当鼠标在模型上移动时触发
* @param mesh 鼠标下方的网格,如果不在任何网格上则为 null
* @param modelX 模型坐标系中的 X 坐标
* @param modelY 模型坐标系中的 Y 坐标
* @param screenX 屏幕坐标系中的 X 坐标
* @param screenY 屏幕坐标系中的 Y 坐标
*/
default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {}
}

View File

@@ -1,646 +0,0 @@
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.ModelRender;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
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.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* 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 volatile int width;
private volatile int height;
private BufferedImage currentFrame;
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;
/**
* 构造函数:使用模型路径
*/
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();
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);
}
});
}
/**
* 创建离屏 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);
// 创建离屏窗口(像素尺寸以当前 width/height 为准)
windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL);
if (windowId == MemoryUtil.NULL) {
throw new Exception("无法创建离屏 OpenGL 上下文");
}
// 设置为当前上下文并初始化
GLFW.glfwMakeContextCurrent(windowId);
GL.createCapabilities();
// 使用 RenderSystem 初始化 OpenGL 状态
RenderSystem.beginInitialization();
RenderSystem.initRenderThread();
RenderSystem.pixelStore(RenderSystem.GL_PACK_ALIGNMENT, 1);
// 初始化 OpenGL 状态
RenderSystem.enableDepthTest();
// 检查是否支持多重采样
if (RenderSystem.isExtensionSupported("GL_ARB_multisample")) {
RenderSystem.enable(RenderSystem.GL_MULTISAMPLE);
logger.info("多重采样已启用");
} else {
logger.info("不支持多重采样,跳过启用");
}
RenderSystem.viewport(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();
RenderSystem.finishInitialization();
// 在正确的上下文中加载模型(可能会耗时)
loadModelInContext();
// 标记上下文已初始化并完成通知(只 complete 一次)
contextInitialized = true;
contextReady.complete(null);
}
/**
* 在 OpenGL 上下文中加载模型
*/
private void loadModelInContext() {
try {
if (modelPath != null) {
Model2D model = Model2D.loadFromFile(modelPath);
modelRef.set(model);
logger.info("模型加载成功: {}", modelPath);
}
} catch (Exception e) {
logger.error("模型加载失败: {}", e.getMessage(), e);
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 由 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();
long elapsed = System.nanoTime() - start;
long sleepNs = targetNs - elapsed;
if (sleepNs > 0) {
LockSupport.parkNanos(sleepNs);
}
}
} catch (Exception e) {
logger.error("渲染线程异常", e);
} finally {
cleanup();
}
});
renderThread.setDaemon(true);
renderThread.setName("GL-Render-Thread");
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
*/
private void renderFrame() {
if (!contextInitialized || windowId == 0) return;
// 确保在当前上下文中
GLFW.glfwMakeContextCurrent(windowId);
Model2D currentModel = modelRef.get();
if (currentModel != null) {
try {
// 使用 RenderSystem 清除缓冲区
RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.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(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, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
/**
* 渲染默认背景
*/
private void renderDefaultBackground() {
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
readPixelsToImage();
}
/**
* 读取 OpenGL 像素数据到 BufferedImage
*/
private void readPixelsToImage() {
try {
final int w = Math.max(1, this.width);
final int h = Math.max(1, this.height);
final int pixelCount = w * h;
// 确保缓冲区大小匹配(可能在 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];
}
pixelBuffer.clear();
// 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem
RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer);
// 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转
IntBuffer ib = pixelBuffer.asIntBuffer();
ib.get(pixelInts, 0, pixelCount);
// 转换并翻转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) {
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, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("像素读取失败", 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
try {
// 选择要绘制的图像:优先 currentFrame最新其不存在则用 lastFrame最后成功帧
BufferedImage imgToDraw = currentFrame != null ? currentFrame : lastFrame;
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) {
executeInGLContext(() -> {
modelRef.set(model);
logger.info("模型已更新");
});
}
/**
* 获取当前渲染的模型
*/
public Model2D getModel() {
return modelRef.get();
}
/**
* 重新设置面板大小
*
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
*/
public void resize(int newWidth, int newHeight) {
// 更新 Swing 尺寸
setPreferredSize(new Dimension(newWidth, newHeight));
revalidate();
// 在 GL 上下文线程中更新离屏窗口与缓冲
executeInGLContext(() -> {
if (contextInitialized && windowId != 0) {
// 更新内部宽高字段
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 的视口
RenderSystem.viewport(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);
}
});
}
/**
* 等待渲染上下文准备就绪
*/
public CompletableFuture<Void> waitForContext() {
return contextReady;
}
/**
* 检查是否正在运行
*/
public boolean isRunning() {
return running && contextInitialized;
}
/**
* 清理资源
*/
public void dispose() {
running = false;
// 停止任务执行器
taskExecutor.shutdown();
if (renderThread != null) {
try {
renderThread.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
cleanup();
}
private void cleanup() {
// 清理 ModelRender
try {
if (ModelRender.isInitialized()) {
ModelRender.cleanup();
logger.info("ModelRender 已清理");
}
} catch (Exception e) {
logger.error("清理 ModelRender 时出错: {}", e.getMessage());
}
if (windowId != 0) {
try {
GLFW.glfwDestroyWindow(windowId);
} catch (Throwable ignored) {}
windowId = 0;
}
// 释放像素缓冲
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

@@ -0,0 +1,988 @@
package com.chuangzhou.vivid2D.render.awt;
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.Texture;
import org.lwjgl.system.MemoryUtil;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.basic.BasicListUI;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* ModelLayerPanel完整实现
*
* - 列表显示“从上到下”的图层listModel[0] 为最上层)
* - 在任何修改后都会把 model.parts 同步为列表的反序(保证渲染顺序与 UI 一致)
* - 支持添加空层 / 从文件创建带贴图的层(在有 renderPanel 时在 GL 线程使用 Texture.createFromFile
* - 支持为选中部件绑定贴图、创建透明图层
* - 支持拖拽重排、上下按钮移动,并在重排后正确恢复选中与不触发滑块事件
*
* 使用:
* new ModelLayerPanel(model, optionalModelRenderPanel)
*/
public class ModelLayerPanel extends JPanel {
private Model2D model;
private ModelRenderPanel renderPanel; // 可选 GL 渲染面板(用于在其 GL 上下文创建纹理)
private DefaultListModel<ModelPart> listModel;
private JList<ModelPart> layerList;
private JButton addButton;
private JButton removeButton;
private JButton upButton;
private JButton downButton;
private JButton bindTextureButton;
private JSlider opacitySlider;
private JLabel opacityValueLabel;
// 程序性设置滑块时忽略事件,避免错误写回
private volatile boolean ignoreSliderEvents = false;
public ModelLayerPanel(Model2D model) {
this(model, null);
}
public ModelLayerPanel(Model2D model, ModelRenderPanel renderPanel) {
this.model = model;
this.renderPanel = renderPanel;
initComponents();
reloadFromModel();
}
public void setModel(Model2D model) {
this.model = model;
reloadFromModel();
}
public void setRenderPanel(ModelRenderPanel panel) {
this.renderPanel = panel;
}
private void initComponents() {
setLayout(new BorderLayout());
listModel = new DefaultListModel<>();
layerList = new JList<>(listModel);
layerList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
layerList.setCellRenderer(new LayerCellRenderer());
layerList.setDragEnabled(true);
layerList.setTransferHandler(new LayerReorderTransferHandler());
layerList.setDropMode(DropMode.INSERT);
// 双击重命名
layerList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int idx = layerList.locationToIndex(e.getPoint());
if (idx >= 0) {
ModelPart part = listModel.get(idx);
String newName = JOptionPane.showInputDialog(
ModelLayerPanel.this,
"输入新名称:",
part.getName()
);
if (newName != null && !newName.trim().isEmpty()) {
renamePart(part, newName);
reloadFromModel();
}
}
}
}
});
// 选择变更 -> 更新滑块显示(但程序性更新时要忽略事件)
layerList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
ModelPart sel = layerList.getSelectedValue();
if (sel != null) {
float op = 1.0f;
try {
Method gm = sel.getClass().getMethod("getOpacity");
Object v = gm.invoke(sel);
if (v instanceof Float) op = (Float) v;
} catch (Exception ex) {
try {
Field f = sel.getClass().getDeclaredField("opacity");
f.setAccessible(true);
Object v = f.get(sel);
if (v instanceof Float) op = (Float) v;
} catch (Exception ignored) {}
}
int val = Math.round(op * 100);
// 程序性更新滑块时阻止 ChangeListener 响应
ignoreSliderEvents = true;
try {
opacitySlider.setValue(val);
opacityValueLabel.setText(val + "%");
} finally {
ignoreSliderEvents = false;
}
removeButton.setEnabled(true);
upButton.setEnabled(true);
downButton.setEnabled(true);
bindTextureButton.setEnabled(true);
} else {
removeButton.setEnabled(false);
upButton.setEnabled(false);
downButton.setEnabled(false);
bindTextureButton.setEnabled(false);
}
}
});
JScrollPane scroll = new JScrollPane(layerList);
add(scroll, BorderLayout.CENTER);
// 按钮区
JPanel controls = new JPanel(new BorderLayout());
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4));
addButton = new JButton("+");
addButton.setToolTipText("添加图层(点击箭头选择创建方式)");
JPopupMenu addMenu = new JPopupMenu();
JMenuItem addBlank = new JMenuItem("创建空图层 (无贴图)");
JMenuItem addWithTexture = new JMenuItem("从文件选择贴图并创建图层");
JMenuItem addTransparent = new JMenuItem("创建透明贴图图层");
addMenu.add(addBlank);
addMenu.add(addWithTexture);
addMenu.add(addTransparent);
addButton.addActionListener(e -> addMenu.show(addButton, 0, addButton.getHeight()));
addBlank.addActionListener(e -> createEmptyPart());
addWithTexture.addActionListener(e -> createPartWithTextureFromFile());
addTransparent.addActionListener(e -> createPartWithTransparentTexture());
removeButton = new JButton("-");
removeButton.setToolTipText("删除选中图层");
removeButton.addActionListener(e -> onRemoveLayer());
removeButton.setEnabled(false);
upButton = new JButton("\u25B2");
upButton.setToolTipText("上移图层");
upButton.addActionListener(e -> moveSelectedUp());
upButton.setEnabled(false);
downButton = new JButton("\u25BC");
downButton.setToolTipText("下移图层");
downButton.addActionListener(e -> moveSelectedDown());
downButton.setEnabled(false);
bindTextureButton = new JButton("绑定贴图");
bindTextureButton.setToolTipText("为选中部件绑定贴图(选择文件)");
bindTextureButton.addActionListener(e -> bindTextureToSelectedPart());
bindTextureButton.setEnabled(false);
btnPanel.add(addButton);
btnPanel.add(removeButton);
btnPanel.add(upButton);
btnPanel.add(downButton);
btnPanel.add(bindTextureButton);
controls.add(btnPanel, BorderLayout.NORTH);
// 不透明度面板
JPanel opacityPanel = new JPanel(new BorderLayout(6, 6));
opacityPanel.setBorder(BorderFactory.createTitledBorder("不透明度"));
opacitySlider = new JSlider(0, 100, 100);
opacityValueLabel = new JLabel("100%");
opacitySlider.addChangeListener(e -> {
if (ignoreSliderEvents) return;
ModelPart sel = layerList.getSelectedValue();
int val = opacitySlider.getValue();
opacityValueLabel.setText(val + "%");
if (sel != null) {
try {
Method sm = sel.getClass().getMethod("setOpacity", float.class);
sm.invoke(sel, val / 100.0f);
} catch (Exception ex) {
try {
Field f = sel.getClass().getDeclaredField("opacity");
f.setAccessible(true);
f.setFloat(sel, val / 100.0f);
} catch (Exception ignored) {}
}
if (model != null) model.markNeedsUpdate();
layerList.repaint();
}
});
opacityPanel.add(opacitySlider, BorderLayout.CENTER);
opacityPanel.add(opacityValueLabel, BorderLayout.EAST);
controls.add(opacityPanel, BorderLayout.SOUTH);
add(controls, BorderLayout.SOUTH);
}
// ============== 部件创建 / 贴图绑定 ==============
private void createEmptyPart() {
String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层");
if (name == null || name.trim().isEmpty()) return;
// 使用 model.createPart 创建(会加入 model.parts 的末尾 -> 视为底层)
ModelPart part = model.createPart(name);
model.markNeedsUpdate();
// reload 并把新创建的部件选中(列表显示从上到下,所以新部件在底部/最后,需要在 reload 后定位)
reloadFromModel();
selectPart(part);
}
private void createPartWithTextureFromFile() {
JFileChooser chooser = new JFileChooser();
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
try {
BufferedImage img = ImageIO.read(f);
if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath());
String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName());
if (name == null || name.trim().isEmpty()) name = f.getName();
// 先创建部件与 Mesh基于图片尺寸
ModelPart part = model.createPart(name);
Mesh2D mesh = createQuadForImage(img, name + "_mesh");
part.addMesh(mesh);
// 在有 GL 上下文时优先使用 Texture.createFromFile 在 GL 线程创建
if (renderPanel != null) {
final String texName = name + "_tex";
final String filePath = f.getAbsolutePath();
renderPanel.executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
if (texture != null) {
// 找到实际被加入到 part 的 mesh通常为最后一个
java.util.List<Mesh2D> partMeshes = part.getMeshes();
Mesh2D actualMesh = null;
if (partMeshes != null && !partMeshes.isEmpty()) {
actualMesh = partMeshes.get(partMeshes.size() - 1);
}
if (actualMesh != null) {
actualMesh.setTexture(texture);
} else {
// 兜底:如果没找到(极少数情况),仍然设置在原始 mesh 上以避免丢失
mesh.setTexture(texture);
}
model.addTexture(texture);
model.markNeedsUpdate();
}
} catch (Throwable ex) {
ex.printStackTrace();
}
});
} else {
// 无 GL尝试内存构造
Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex");
if (memTex != null) {
mesh.setTexture(memTex);
model.addTexture(memTex);
model.markNeedsUpdate();
} else {
System.err.println("未找到可用的 GL 上下文,也无法创建内存纹理: " + f.getAbsolutePath());
}
}
reloadFromModel();
selectPart(part);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
private void createPartWithTransparentTexture() {
String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层");
if (name == null || name.trim().isEmpty()) return;
int w = 128, h = 128;
try {
String wh = JOptionPane.showInputDialog(this, "输入尺寸宽x高例如 128x128或留空使用 128x128", "128x128");
if (wh != null && wh.contains("x")) {
String[] sp = wh.split("x");
w = Math.max(1, Integer.parseInt(sp[0].trim()));
h = Math.max(1, Integer.parseInt(sp[1].trim()));
}
} catch (Exception ignored) {}
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
ModelPart part = model.createPart(name);
Mesh2D mesh = createQuadForImage(img, name + "_mesh");
part.addMesh(mesh);
if (renderPanel != null) {
renderPanel.executeInGLContext(() -> {
try {
Texture tex = createTextureFromBufferedImageInGL(img, name + "_tex");
if (tex != null) {
mesh.setTexture(tex);
model.addTexture(tex);
}
} catch (Exception e) {
e.printStackTrace();
}
});
} else {
Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex");
if (memTex != null) {
mesh.setTexture(memTex);
model.addTexture(memTex);
}
}
model.markNeedsUpdate();
reloadFromModel();
selectPart(part);
}
private void bindTextureToSelectedPart() {
ModelPart sel = layerList.getSelectedValue();
if (sel == null) return;
JFileChooser chooser = new JFileChooser();
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
try {
BufferedImage img = null;
try { img = ImageIO.read(f); } catch (Exception ignored) {}
// 获取第一个 mesh
Mesh2D targetMesh = null;
try {
Method getMeshes = sel.getClass().getMethod("getMeshes");
Object list = getMeshes.invoke(sel);
if (list instanceof List) {
List<?> meshes = (List<?>) list;
if (!meshes.isEmpty() && meshes.get(0) instanceof Mesh2D) {
targetMesh = (Mesh2D) meshes.get(0);
}
}
} catch (Exception ignored) {}
if (targetMesh == null) {
if (img == null) {
img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
}
targetMesh = createQuadForImage(img, sel.getName() + "_mesh");
try {
sel.getClass().getMethod("addMesh", Mesh2D.class).invoke(sel, targetMesh);
} catch (Exception ex) {
ex.printStackTrace();
}
}
final Mesh2D meshToBind = targetMesh;
final String filePath = f.getAbsolutePath();
final String texName = sel.getName() + "_tex";
if (renderPanel != null) {
renderPanel.executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
if (texture != null) {
meshToBind.setTexture(texture);
model.addTexture(texture);
model.markNeedsUpdate();
} else {
System.err.println("Texture.createFromFile 返回 null: " + filePath);
}
} catch (Throwable ex) {
ex.printStackTrace();
}
});
} else {
if (img == null) img = ImageIO.read(f);
Texture mem = tryCreateTextureFromImageMemory(img, texName);
if (mem != null) {
meshToBind.setTexture(mem);
model.addTexture(mem);
model.markNeedsUpdate();
} else {
System.err.println("无法在无 GL 上下文中创建纹理: " + filePath);
}
}
reloadFromModel();
selectPart(sel);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
// ============== 辅助Mesh/Texture 创建 ==============
private Mesh2D createQuadForImage(BufferedImage img, String meshName) {
float w = img.getWidth();
float h = img.getHeight();
try {
Method m = Mesh2D.class.getMethod("createQuad", String.class, float.class, float.class);
Object o = m.invoke(null, meshName, w, h);
if (o instanceof Mesh2D) return (Mesh2D) o;
} catch (Exception ignored) {}
try {
Constructor<?> cons = null;
for (Constructor<?> c : Mesh2D.class.getDeclaredConstructors()) {
Class<?>[] params = c.getParameterTypes();
if (params.length >= 4 && params[0] == String.class) {
cons = c;
break;
}
}
if (cons != null) {
cons.setAccessible(true);
float[] vertices = new float[]{
-w / 2f, -h / 2f,
w / 2f, -h / 2f,
w / 2f, h / 2f,
-w / 2f, h / 2f
};
float[] uvs = new float[]{
0f, 1f,
1f, 1f,
1f, 0f,
0f, 0f
};
int[] indices = new int[]{0, 1, 2, 2, 3, 0};
Object meshObj = cons.newInstance(meshName, vertices, uvs, indices);
if (meshObj instanceof Mesh2D) return (Mesh2D) meshObj;
}
} catch (Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException("无法创建 Mesh2D没有合适的工厂或构造函数");
}
/**
* 在 GL 上下文中创建并上传 Texture返回已上传的 Texture
* 该方法仅在 renderPanel 可用时被调用renderPanel.executeInGLContext
*/
private Texture createTextureFromBufferedImageInGL(BufferedImage img, String texName) {
if (renderPanel == null) throw new IllegalStateException("需要 renderPanel 才能在 GL 上下文创建纹理");
try {
return renderPanel.executeInGLContext((Callable<Texture>) () -> {
// 静态工厂尝试
try {
Method factory = findStaticMethod(Texture.class, "createFromBufferedImage", BufferedImage.class);
if (factory == null) factory = findStaticMethod(Texture.class, "createFromImage", BufferedImage.class);
if (factory != null) {
Object texObj = factory.invoke(null, img);
if (texObj instanceof Texture) {
tryCallTextureUpload((Texture) texObj);
return (Texture) texObj;
}
}
} catch (Throwable ignored) {}
// 构造 ByteBuffer 并尝试构造器
try {
int w = img.getWidth();
int h = img.getHeight();
ByteBuffer buf = imageToRGBAByteBuffer(img);
Constructor<?> suit = null;
for (Constructor<?> c : Texture.class.getDeclaredConstructors()) {
Class<?>[] ps = c.getParameterTypes();
if (ps.length >= 4 && ps[0] == String.class) {
suit = c;
break;
}
}
if (suit != null) {
suit.setAccessible(true);
Object texObj = null;
Class<?>[] ps = suit.getParameterTypes();
if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) {
Object formatEnum = null;
try {
Class<?> formatCls = null;
for (Class<?> inner : Texture.class.getDeclaredClasses()) {
if (inner.getSimpleName().toLowerCase().contains("format")) {
formatCls = inner;
break;
}
}
if (formatCls != null) {
for (Field f : formatCls.getFields()) {
if (f.getName().toUpperCase().contains("RGBA")) {
formatEnum = f.get(null);
break;
}
}
}
} catch (Throwable ignored) {}
if (formatEnum != null) {
try {
texObj = suit.newInstance(texName, w, h, formatEnum, buf);
} catch (Exception ignored) {}
}
}
if (texObj == null) {
try { texObj = suit.newInstance(texName, img.getWidth(), img.getHeight(), buf); } catch (Exception ignored) {}
}
if (texObj instanceof Texture) {
tryCallTextureUpload((Texture) texObj);
return (Texture) texObj;
}
}
} catch (Throwable t) {
t.printStackTrace();
}
throw new RuntimeException("无法在 GL 上下文中创建 Texture缺少兼容的构造器/工厂)");
}).get();
} catch (Exception e) {
throw new RuntimeException("创建 GL 纹理失败: " + e.getMessage(), e);
}
}
private Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) {
try {
int w = img.getWidth();
int h = img.getHeight();
ByteBuffer buf = imageToRGBAByteBuffer(img);
Constructor<?> suit = null;
for (Constructor<?> c : Texture.class.getDeclaredConstructors()) {
Class<?>[] ps = c.getParameterTypes();
if (ps.length >= 4 && ps[0] == String.class) {
suit = c;
break;
}
}
if (suit != null) {
suit.setAccessible(true);
Object texObj = null;
Class<?>[] ps = suit.getParameterTypes();
if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) {
Object formatEnum = null;
try {
Class<?> formatCls = null;
for (Class<?> inner : Texture.class.getDeclaredClasses()) {
if (inner.getSimpleName().toLowerCase().contains("format")) {
formatCls = inner;
break;
}
}
if (formatCls != null) {
for (Field f : formatCls.getFields()) {
if (f.getName().toUpperCase().contains("RGBA")) {
formatEnum = f.get(null);
break;
}
}
}
} catch (Throwable ignored) {}
if (formatEnum != null) {
try { texObj = suit.newInstance(texName, w, h, formatEnum, buf); } catch (Throwable ignored) {}
}
}
if (texObj == null) {
try { texObj = suit.newInstance(texName, w, h, buf); } catch (Throwable ignored) {}
}
if (texObj instanceof Texture) return (Texture) texObj;
}
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
private ByteBuffer imageToRGBAByteBuffer(BufferedImage img) {
final int w = img.getWidth();
final int h = img.getHeight();
final int[] pixels = new int[w * h];
img.getRGB(0, 0, w, h, pixels, 0, w);
ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder());
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int argb = pixels[y * w + x];
int a = (argb >> 24) & 0xFF;
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = (argb) & 0xFF;
buffer.put((byte) r);
buffer.put((byte) g);
buffer.put((byte) b);
buffer.put((byte) a);
}
}
buffer.flip();
return buffer;
}
private void tryCallTextureUpload(Texture tex) {
if (tex == null) return;
String[] candidates = new String[]{"upload", "uploadToGPU", "initGL", "initTexture", "createGLTexture", "bind"};
for (String name : candidates) {
try {
Method m = tex.getClass().getMethod(name);
if (m != null) {
m.invoke(tex);
return;
}
} catch (NoSuchMethodException ignored) {
} catch (Throwable t) {
t.printStackTrace();
}
}
}
private static Method findStaticMethod(Class<?> cls, String name, Class<?> param) {
try {
Method m = cls.getMethod(name, param);
if (Modifier.isStatic(m.getModifiers())) return m;
} catch (Exception ignored) {}
try {
Method m = cls.getDeclaredMethod(name, param);
m.setAccessible(true);
if (Modifier.isStatic(m.getModifiers())) return m;
} catch (Exception ignored) {}
return null;
}
// ============== 列表操作(核心:保持 model.parts 与 listModel 一致) ==============
private void onRemoveLayer() {
ModelPart sel = layerList.getSelectedValue();
if (sel == null) return;
int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
if (r != JOptionPane.YES_OPTION) return;
try {
List<ModelPart> parts = getModelPartsList();
if (parts != null) parts.remove(sel);
Map<String, ModelPart> partMap = getModelPartMap();
if (partMap != null) partMap.remove(sel.getName());
try {
ModelPart root = model.getRootPart();
if (root != null && root == sel) {
List<ModelPart> remaining = getModelPartsList();
if (remaining != null && !remaining.isEmpty()) {
model.setRootPart(remaining.get(0));
} else {
model.setRootPart(null);
}
}
} catch (Exception ignored) {}
model.markNeedsUpdate();
reloadFromModel();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
private void moveSelectedUp() {
int idx = layerList.getSelectedIndex();
if (idx <= 0) return;
performVisualReorder(idx, idx - 1);
}
private void moveSelectedDown() {
int idx = layerList.getSelectedIndex();
if (idx < 0 || idx >= listModel.getSize() - 1) return;
performVisualReorder(idx, idx + 1);
}
/**
* 重新加载列表(列表显示顺序为从上到下)
* 列表中的顺序与用户看到的顺序一致listModel[0] = 最上层)
*/
private void reloadFromModel() {
// 记录对象选中以便恢复
ModelPart selected = layerList.getSelectedValue();
listModel.clear();
if (model == null) return;
try {
List<ModelPart> parts = model.getParts();
// 我们希望列表从上到下显示,因此把 model.parts 反序加入 listModel
if (parts != null) {
for (int i = parts.size() - 1; i >= 0; i--) {
listModel.addElement(parts.get(i));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
// 恢复选中(按对象引用)
if (selected != null) {
for (int i = 0; i < listModel.getSize(); i++) {
if (listModel.get(i) == selected) {
layerList.setSelectedIndex(i);
break;
}
}
}
}
/**
* 执行视觉(列表)层级的重排:先在 visualList 上进行操作,然后把 model.parts 重建为 visualList 的反序,
* 保证 model.parts 与 UI 显示顺序一致rendering 与 UI 保持一致)。
*
* @param visualFrom 源 visual 索引listModel
* @param visualTo 目标 visual 索引listModel
*/
private void performVisualReorder(int visualFrom, int visualTo) {
if (model == null) return;
try {
int size = listModel.getSize();
if (visualFrom < 0 || visualFrom >= size) return;
if (visualTo < 0) visualTo = 0;
if (visualTo > size - 1) visualTo = size - 1;
// 构造新的视觉顺序arraylist
List<ModelPart> visual = new ArrayList<>(size);
for (int i = 0; i < size; i++) visual.add(listModel.get(i));
// 移动元素
ModelPart moved = visual.remove(visualFrom);
visual.add(visualTo, moved);
// 更新 listModel程序性更新期间设置 ignoreSliderEvents 防止滑块回写)
ignoreSliderEvents = true;
try {
listModel.clear();
for (ModelPart p : visual) listModel.addElement(p);
} finally {
ignoreSliderEvents = false;
}
// 根据视觉顺序重建 model.partsmodel.parts = reverse(visual)
List<ModelPart> newModelParts = new ArrayList<>(visual.size());
for (int i = visual.size() - 1; i >= 0; i--) newModelParts.add(visual.get(i));
// 替换 model.parts 字段(通过反射)
replaceModelPartsList(newModelParts);
model.markNeedsUpdate();
// 恢复选中:按对象引用找到索引
selectPart(moved);
} catch (Exception ex) {
ex.printStackTrace();
}
}
// ============== 反射读写 Model2D 内部 ==============
@SuppressWarnings("unchecked")
private List<ModelPart> getModelPartsList() {
if (model == null) return null;
try {
Field partsField = model.getClass().getDeclaredField("parts");
partsField.setAccessible(true);
Object o = partsField.get(model);
if (o instanceof List) return (List<ModelPart>) o;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@SuppressWarnings("unchecked")
private Map<String, ModelPart> getModelPartMap() {
if (model == null) return null;
try {
Field mapField = model.getClass().getDeclaredField("partMap");
mapField.setAccessible(true);
Object o = mapField.get(model);
if (o instanceof Map) return (Map<String, ModelPart>) o;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 用新的 parts 列表替换 model.parts保持同一 List 对象或直接 set
*/
private void replaceModelPartsList(List<ModelPart> newParts) {
if (model == null) return;
try {
Field partsField = model.getClass().getDeclaredField("parts");
partsField.setAccessible(true);
Object old = partsField.get(model);
if (old instanceof java.util.List) {
@SuppressWarnings("rawtypes")
java.util.List rawList = (java.util.List) old;
rawList.clear();
rawList.addAll(newParts);
} else {
partsField.set(model, newParts);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// ============== 列表渲染/拖拽辅助 ==============
private class LayerCellRenderer extends JPanel implements ListCellRenderer<ModelPart> {
private JCheckBox visibleBox = new JCheckBox();
private JLabel nameLabel = new JLabel();
private JLabel opacityLabel = new JLabel();
LayerCellRenderer() {
setLayout(new BorderLayout(6, 6));
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
left.setOpaque(false);
visibleBox.setOpaque(false);
left.add(visibleBox);
left.add(nameLabel);
add(left, BorderLayout.CENTER);
add(opacityLabel, BorderLayout.EAST);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int idx = layerList.locationToIndex(e.getPoint());
if (idx >= 0) {
ModelPart part = listModel.get(idx);
Rectangle cbBounds = new Rectangle(0, 0, 20, getHeight());
if (cbBounds.contains(e.getPoint())) {
boolean newVis = !part.isVisible();
part.setVisible(newVis);
if (model != null) model.markNeedsUpdate();
reloadFromModel();
} else {
layerList.setSelectedIndex(idx);
}
}
}
});
}
@Override
public Component getListCellRendererComponent(JList<? extends ModelPart> list, ModelPart value, int index, boolean isSelected, boolean cellHasFocus) {
nameLabel.setText(value.getName());
try {
Method gm = value.getClass().getMethod("getOpacity");
Object v = gm.invoke(value);
if (v instanceof Float) opacityLabel.setText(((int) (((Float) v) * 100)) + "%");
} catch (Exception ex) {
try {
Field f = value.getClass().getDeclaredField("opacity");
f.setAccessible(true);
Object v = f.get(value);
if (v instanceof Float) opacityLabel.setText(Math.round((Float) v * 100) + "%");
else opacityLabel.setText("");
} catch (Exception ignored) {
opacityLabel.setText("");
}
}
visibleBox.setSelected(value.isVisible());
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
nameLabel.setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
nameLabel.setForeground(list.getForeground());
}
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
return this;
}
}
private class LayerReorderTransferHandler extends TransferHandler {
@Override
protected Transferable createTransferable(JComponent c) {
int src = layerList.getSelectedIndex();
if (src < 0) return null;
return new StringSelection(Integer.toString(src));
}
@Override
public int getSourceActions(JComponent c) { return MOVE; }
@Override
public boolean canImport(TransferSupport support) {
return support.isDrop() && support.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.stringFlavor);
}
@Override
public boolean importData(TransferSupport support) {
if (!canImport(support)) return false;
try {
javax.swing.JList.DropLocation dl = (javax.swing.JList.DropLocation) support.getDropLocation();
int dropIndex = dl.getIndex();
String s = (String) support.getTransferable().getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor);
int srcIdx = Integer.parseInt(s);
if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false;
performVisualReorder(srcIdx, dropIndex);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
// ============== 小工具 ==============
private void selectPart(ModelPart part) {
if (part == null) return;
for (int i = 0; i < listModel.getSize(); i++) {
if (listModel.get(i) == part) {
layerList.setSelectedIndex(i);
layerList.ensureIndexIsVisible(i);
return;
}
}
}
private void renamePart(ModelPart part, String newName) {
if (part == null) return;
try {
try {
Method m = part.getClass().getMethod("setName", String.class);
m.invoke(part, newName);
} catch (NoSuchMethodException ex) {
Field nameField = part.getClass().getDeclaredField("name");
nameField.setAccessible(true);
String oldName = (String) nameField.get(part);
nameField.set(part, newName);
Map<String, ModelPart> partMap = getModelPartMap();
if (partMap != null) {
partMap.remove(oldName);
partMap.put(newName, part);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ package com.chuangzhou.vivid2D.render.model;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.model.util.Deformer;
import com.chuangzhou.vivid2D.render.model.util.Matrix3fUtils;
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import org.joml.Matrix3f;
import org.joml.Vector2f;
@@ -35,6 +35,8 @@ public class ModelPart {
private final Matrix3f localTransform;
private final Matrix3f worldTransform;
private final Vector2f pivot = new Vector2f(0, 0);
private float scaleX = 1.0f;
private float scaleY = 1.0f;
// ==================== 渲染属性 ====================
private boolean visible;
@@ -403,21 +405,6 @@ public class ModelPart {
return new Vector2f(center.x + rx, center.y + ry);
}
public void draw(int shaderProgram, org.joml.Matrix3f parentTransform) {
// 先确保 worldTransform 是最新的
updateWorldTransform(parentTransform, false);
// 绘制本节点的所有 mesh将 worldTransform 作为 model 矩阵传入)
for (Mesh2D mesh : meshes) {
mesh.draw(shaderProgram, worldTransform);
}
// 递归绘制子节点
for (ModelPart child : children) {
child.draw(shaderProgram, worldTransform);
}
}
// 更新局部矩阵
private void updateLocalTransform() {
float cos = (float) Math.cos(rotation);
@@ -493,6 +480,60 @@ public class ModelPart {
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
// 更新网格顶点位置
updateMeshVertices();
}
/**
* 更新所有网格的顶点位置以反映当前变换
*/
public void updateMeshVertices() {
// 确保世界变换是最新的
if (transformDirty) {
updateLocalTransform();
recomputeWorldTransformRecursive();
}
// 对每个网格应用当前的世界变换
for (Mesh2D mesh : meshes) {
updateMeshVertices(mesh);
}
// 递归更新子部件的网格
for (ModelPart child : children) {
child.updateMeshVertices();
}
}
/**
* 更新单个网格的顶点位置
*/
private void updateMeshVertices(Mesh2D mesh) {
if (mesh == null) return;
// 获取原始顶点数据(局部坐标)
float[] originalVertices = mesh.getOriginalVertices();
if (originalVertices == null || originalVertices.length == 0) {
logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName());
return;
}
// 确保世界变换是最新的
if (transformDirty) {
updateLocalTransform();
recomputeWorldTransformRecursive();
}
// 应用当前世界变换到每个顶点
for (int i = 0; i < originalVertices.length; i += 2) {
Vector2f localPoint = new Vector2f(originalVertices[i], originalVertices[i + 1]);
Vector2f worldPoint = localToWorld(localPoint);
mesh.setVertex(i / 2, worldPoint.x, worldPoint.y);
}
// 标记网格需要更新
mesh.markDirty();
}
public void setPosition(Vector2f position) {
@@ -500,6 +541,9 @@ public class ModelPart {
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
// 更新网格顶点位置
updateMeshVertices();
}
/**
@@ -543,10 +587,14 @@ public class ModelPart {
* 设置缩放
*/
public void setScale(float sx, float sy) {
this.scaleX = sx;
this.scaleY = sy;
scale.set(sx, sy);
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
updateMeshVertices();
}
public void setScale(float uniformScale) {
@@ -554,6 +602,8 @@ public class ModelPart {
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
updateMeshVertices();
}
public void setScale(Vector2f scale) {
@@ -561,6 +611,8 @@ public class ModelPart {
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
updateMeshVertices();
}
/**
@@ -581,20 +633,46 @@ public class ModelPart {
public void addMesh(Mesh2D mesh) {
if (mesh == null) return;
// 确保本节点的 worldTransform 是最新的(会递归更新子节点)
// 创建独立副本,避免多个 Part 共享同一 Mesh 实例导致数据冲突
Mesh2D m = mesh.copy();
// 确保拷贝保留原始的纹理引用copy() 应该已经赋值,但显式赋值可避免遗漏)
m.setTexture(mesh.getTexture());
// 确保本节点的 worldTransform 是最新的
recomputeWorldTransformRecursive();
// 将 mesh 的每个顶点从本地空间变换到世界空间(烘焙)
int vc = mesh.getVertexCount();
// 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用
float[] originalVertices = m.getVertices().clone();
m.setOriginalVertices(originalVertices);
logger.info("addMesh: texture={} for mesh={}", m.getTexture(), m.getName());
// 保证 UV 不被篡改(通常 copy() 已经处理)
// float[] uvs = m.getUVs(); // 如果需要可以在此处检查
// 将拷贝的 mesh 的每个顶点从本地空间变换到世界空间(烘焙到 world
int vc = m.getVertexCount();
for (int i = 0; i < vc; i++) {
org.joml.Vector2f local = mesh.getVertex(i);
org.joml.Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local);
mesh.setVertex(i, worldPt.x, worldPt.y);
Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local);
m.setVertex(i, worldPt.x, worldPt.y);
}
meshes.add(mesh);
// 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新
m.setBakedToWorld(true);
// 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU
m.markDirty();
// 如果你确定此处正在 GL 渲染线程并且想要立刻创建 VAO/VBO可取消下面注释
// m.uploadToGPU();
// 将拷贝加入到本部件
meshes.add(m);
boundsDirty = true;
}
/**
* 设置中心点
*/
@@ -811,6 +889,9 @@ public class ModelPart {
return opacity;
}
public float getScaleX() { return scaleX; }
public float getScaleY() { return scaleY; }
public void setOpacity(float opacity) {
this.opacity = Math.max(0.0f, Math.min(1.0f, opacity));
}

View File

@@ -72,22 +72,7 @@ public class LightSourceData implements Serializable {
Vector3f col = stringToVector3f(color);
LightSource light;
if (isAmbient) {
// 使用环境光构造器
light = new LightSource(LightSource.vector3fToColor(col), intensity);
} else {
// 使用包含辉光参数的构造器(即便 isGlow 为 false 也可以传入)
light = new LightSource(
pos,
LightSource.vector3fToColor(col),
intensity,
isGlow,
SaveVector2f.fromString(glowDirection),
glowIntensity,
glowRadius,
glowAmount
);
}
light = new LightSource(LightSource.vector3fToColor(col), intensity);
light.setEnabled(enabled);
light.setAmbient(isAmbient);

View File

@@ -1,5 +1,6 @@
package com.chuangzhou.vivid2D.render.model.util;
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
import org.joml.Matrix3f;
import org.joml.Vector2f;

View File

@@ -1,11 +1,16 @@
package com.chuangzhou.vivid2D.render.model.util;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
import org.joml.Vector2f;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Objects;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
@@ -44,6 +49,7 @@ public class Mesh2D {
private BoundingBox bounds;
private boolean boundsDirty = true;
private boolean bakedToWorld = false;
private volatile boolean selected = false;
// ==================== 常量 ====================
public static final int POINTS = 0;
@@ -125,6 +131,17 @@ public class Mesh2D {
return new Mesh2D(name, vertices, uvs, indices);
}
public float[] getOriginalVertices() {
return originalVertices != null ? originalVertices.clone() : vertices.clone();
}
/**
* 设置原始顶点数据
*/
public void setOriginalVertices(float[] originalVertices) {
this.originalVertices = originalVertices != null ? originalVertices.clone() : null;
}
/**
* 创建圆形网格
*/
@@ -206,6 +223,20 @@ public class Mesh2D {
setVertex(index, position.x, position.y);
}
/**
* 设置该 Mesh 的选中状态(线程安全)
*/
public void setSelected(boolean sel) {
this.selected = sel;
}
/**
* 查询选中状态
*/
public boolean isSelected() {
return this.selected;
}
/**
* 获取UV坐标
*/
@@ -476,40 +507,202 @@ public class Mesh2D {
uploadToGPU();
}
// 1. 绘制网格
if (texture != null) {
texture.bind();
}
// 绑定 VAO - 使用 RenderSystem
RenderSystem.glBindVertexArray(() -> vaoId);
// 使用着色器程序 - 使用 RenderSystem
RenderSystem.useProgram(shaderProgram);
// 将 modelMatrix 上传到 shader 的 uniform
int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix");
if (loc == -1) {
loc = RenderSystem.getUniformLocation(shaderProgram, "uModel");
}
if (loc != -1) {
RenderSystem.uniformMatrix3(loc, modelMatrix);
} else {
//logger.warn("警告: 着色器中未找到 uModelMatrix 或 uModel uniform");
}
// 绘制 - 使用 RenderSystem
RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount,
RenderSystem.GL_UNSIGNED_INT, 0);
// 解绑 VAO
// 2. 解绑 VAO 和纹理,确保 overlay 绘制不受影响
RenderSystem.glBindVertexArray(() -> 0);
if (texture != null) {
texture.unbind(); // 需要检查 texture.unbind() 是否也需要封装
texture.unbind();
}
// 3. 如果选中,则绘制选中框
if (selected) {
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
int currentProgram = RenderSystem.getCurrentProgram();
try {
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
if (solidShader != null && solidShader.programId != 0) {
solidShader.use();
int modelLoc = solidShader.getUniformLocation("uModelMatrix");
if (modelLoc != -1) {
RenderSystem.uniformMatrix3(modelLoc, modelMatrix);
}
int colorLoc = solidShader.getUniformLocation("uColor");
if (colorLoc != -1) {
RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
}
}
drawSelectBox();
} finally {
if (currentProgram != 0) {
RenderSystem.useProgram(currentProgram);
}
}
}
}
private void drawSelectBox(){
BoundingBox bounds = getBounds();
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
BufferBuilder bb = new BufferBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
final float CORNER_SIZE = 8.0f;
final float BORDER_THICKNESS = 6.0f;
float expand = 4.0f * 2.0f;
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 0.4f));
bb.vertex(minX - expand, minY - expand, 0.0f, 0.0f);
bb.vertex(maxX + expand, minY - expand, 0.0f, 0.0f);
bb.vertex(maxX + expand, maxY + expand, 0.0f, 0.0f);
bb.vertex(minX - expand, maxY + expand, 0.0f, 0.0f);
bb.endImmediate();
// 第2层主边框实心粗边框- 使用明亮的青色
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 1.0f)); // 青色100%不透明
float mainExpand = 1.0f;
bb.vertex(minX - mainExpand, minY - mainExpand, 0.0f, 0.0f);
bb.vertex(maxX + mainExpand, minY - mainExpand, 0.0f, 0.0f);
bb.vertex(maxX + mainExpand, maxY + mainExpand, 0.0f, 0.0f);
bb.vertex(minX - mainExpand, maxY + mainExpand, 0.0f, 0.0f);
bb.endImmediate();
// 第3层内边框 - 使用白色增加对比度
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); // 白色100%不透明
bb.vertex(minX, minY, 0.0f, 0.0f);
bb.vertex(maxX, minY, 0.0f, 0.0f);
bb.vertex(maxX, maxY, 0.0f, 0.0f);
bb.vertex(minX, maxY, 0.0f, 0.0f);
bb.endImmediate();
// 第4层绘制角点标记和边线
drawResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS);
}
/**
* 绘制调整大小的手柄
*/
private void drawResizeHandles(BufferBuilder bb, float minX, float minY, float maxX, float maxY,
float cornerSize, float borderThickness) {
Vector4f handleColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
// 绘制四个角点
drawCornerHandle(bb, minX, minY, handleColor, cornerSize); // 左上
drawCornerHandle(bb, maxX, minY, handleColor, cornerSize); // 右上
drawCornerHandle(bb, minX, maxY, handleColor, cornerSize); // 左下
drawCornerHandle(bb, maxX, maxY, handleColor, cornerSize); // 右下
// 绘制边线中点(可选)
drawEdgeHandle(bb, (minX + maxX) / 2, minY, handleColor, borderThickness); // 上边中点
drawEdgeHandle(bb, (minX + maxX) / 2, maxY, handleColor, borderThickness); // 下边中点
drawEdgeHandle(bb, minX, (minY + maxY) / 2, handleColor, borderThickness); // 左边中点
drawEdgeHandle(bb, maxX, (minY + maxY) / 2, handleColor, borderThickness); // 右边中点
}
private void drawCornerHandle(BufferBuilder bb, float x, float y, Vector4f color, float cornerSize) {
float halfSize = cornerSize / 2;
// 使用 RenderSystem 的常量
bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN
bb.setColor(color);
bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f);
bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f);
bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f);
bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f);
bb.endImmediate();
}
private void drawEdgeHandle(BufferBuilder bb, float x, float y, Vector4f color, float borderThickness) {
float halfSize = borderThickness / 2;
// 使用 RenderSystem 的常量
bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN
bb.setColor(color);
bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f);
bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f);
bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f);
bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f);
bb.endImmediate();
}
// 角点标记
private void drawCornerPoints(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
float cornerSize = 8.0f; // 定义局部常量
Vector4f cornerColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
Vector2f[] corners = {
new Vector2f(minX, minY),
new Vector2f(maxX, minY),
new Vector2f(maxX, maxY),
new Vector2f(minX, maxY)
};
for (Vector2f corner : corners) {
// 使用 RenderSystem 的常量
bb.begin(RenderSystem.GL_LINE_STRIP, 3); // 改为 RenderSystem.GL_LINE_STRIP
bb.setColor(cornerColor);
bb.vertex(corner.x - cornerSize, corner.y, 0.0f, 0.0f);
bb.vertex(corner.x, corner.y, 0.0f, 0.0f);
bb.vertex(corner.x, corner.y + cornerSize, 0.0f, 0.0f);
bb.endImmediate();
}
}
/**
* 计算模型的边界框 [minX, minY, maxX, maxY]
*/
public float[] calculateBoundingBox() {
// 使用现有的边界计算功能
BoundingBox bounds = getBounds();
return new float[]{
bounds.getMinX(),
bounds.getMinY(),
bounds.getMaxX(),
bounds.getMaxY()
};
}
/**
* 计算带扩展的边界框 [minX, minY, maxX, maxY]
*/
public float[] calculateBoundingBox(float expand) {
float[] bounds = calculateBoundingBox();
return new float[]{
bounds[0] - expand,
bounds[1] - expand,
bounds[2] + expand,
bounds[3] + expand
};
}
public void draw() {
if (!visible) return;
if (indices == null || indices.length == 0) return;
@@ -616,13 +809,38 @@ public class Mesh2D {
*/
public Mesh2D copy() {
Mesh2D copy = new Mesh2D(name + "_copy");
copy.setMeshData(vertices, uvs, indices);
copy.texture = texture;
copy.visible = visible;
copy.drawMode = drawMode;
// 深拷贝数组(保证互不影响)
copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0];
copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0];
copy.indices = this.indices != null ? this.indices.clone() : new int[0];
// 保留 originalVertices如果有否则把当前 vertices 作为原始数据
copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone();
// 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄)
copy.texture = this.texture;
copy.visible = this.visible;
copy.drawMode = this.drawMode;
copy.bakedToWorld = this.bakedToWorld;
// 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行
copy.vaoId = -1;
copy.vboId = -1;
copy.eboId = -1;
copy.indexCount = this.indices != null ? this.indices.length : 0;
copy.uploaded = false;
// 状态标记
copy.dirty = true;
copy.boundsDirty = true;
copy.bounds = new BoundingBox();
copy.selected = this.selected;
return copy;
}
public int getVaoId() {
return vaoId;
}
@@ -677,13 +895,30 @@ public class Mesh2D {
@Override
public String toString() {
return "Mesh2D{" +
"name='" + name + '\'' +
", vertices=" + getVertexCount() +
", indices=" + indices.length +
", visible=" + visible +
", drawMode=" + getDrawModeString() +
", bounds=" + getBounds() +
'}';
StringBuilder sb = new StringBuilder();
sb.append("Mesh2D{")
.append("name='").append(name).append('\'')
.append(", vertices=").append(getVertexCount())
.append(", indices=").append(indices.length)
.append(", visible=").append(visible)
.append(", drawMode=").append(getDrawModeString())
.append(", bounds=").append(getBounds());
if (vertices != null && vertices.length > 0) {
sb.append(", coordinates=[");
for (int i = 0; i < vertices.length; i += 2) {
if (i > 0) sb.append(", ");
sb.append("(")
.append(String.format("%.2f", vertices[i]))
.append(", ")
.append(String.format("%.2f", vertices[i + 1]))
.append(")");
}
sb.append("]");
}
sb.append('}');
return sb.toString();
}
}

View File

@@ -1,4 +1,4 @@
package com.chuangzhou.vivid2D.render.model.util;
package com.chuangzhou.vivid2D.render.systems;
import org.joml.Matrix3f;
import org.joml.Vector2f;

View File

@@ -67,6 +67,11 @@ public final class RenderSystem {
public static final int DRAW_TRIANGLE_STRIP = GL11.GL_TRIANGLE_STRIP;
public static final int DRAW_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN;
public static final int DRAW_QUADS = GL11.GL_QUADS;
public static final int GL_LINE_LOOP = GL11.GL_LINE_LOOP;
public static final int GL_LINE_STRIP = GL11.GL_LINE_STRIP;
public static final int GL_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN;
public static final int GL_QUADS = GL11.GL_QUADS;
public static final int GL_TRIANGLES = GL11.GL_TRIANGLES;
// ================== 索引类型常量 ==================
public static final int GL_UNSIGNED_BYTE = GL11.GL_UNSIGNED_BYTE;
@@ -92,6 +97,11 @@ public final class RenderSystem {
public static final int GL_SHORT = GL11.GL_SHORT;
public static final int GL_INT = GL11.GL_INT;
public static final int GL_TRUE = org.lwjgl.opengl.GL11.GL_TRUE;
public static final int GL_COMPILE_STATUS = org.lwjgl.opengl.GL20.GL_COMPILE_STATUS;
public static final int GL_LINK_STATUS = org.lwjgl.opengl.GL20.GL_LINK_STATUS;
public static final int GL_VALIDATE_STATUS = org.lwjgl.opengl.GL20.GL_VALIDATE_STATUS;
// ================== 初始化方法 ==================
public static void initRenderThread() {

View File

@@ -1,12 +1,14 @@
package com.chuangzhou.vivid2D.render.systems.buffer;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
/**
* 缓冲区上传器
*
* @version 1.0
* @author tzdwindows
* @version 1.1 - 添加颜色支持
* @author tzdwindows 7
*/
public class BufferUploader {
@@ -54,8 +56,21 @@ public class BufferUploader {
}
// 应用着色器
int currentProgram = 0;
if (state.shaderProgram != 0) {
currentProgram = state.shaderProgram;
RenderSystem.useProgram(state.shaderProgram);
} else {
currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
}
if (currentProgram != 0) {
int colorLoc = RenderSystem.getUniformLocation(currentProgram, "uColor");
if (colorLoc == -1) {} else {
RenderSystem.uniform4f(colorLoc,
state.color.x, state.color.y, state.color.z, state.color.w);
}
} else {
System.err.println("DEBUG: No shader program available for color setting");
}
// 应用混合模式

View File

@@ -0,0 +1,14 @@
package com.chuangzhou.vivid2D.render.systems.sources;
/**
* 完整着色器接口
* 一个完整的着色器程序需要顶点着色器和片段着色器
* @author tzdwindows 7
*/
public interface CompleteShader {
Shader getVertexShader();
Shader getFragmentShader();
String getShaderName();
boolean isDefaultShader();
default void setDefaultUniforms(ShaderProgram program) {}
}

View File

@@ -0,0 +1,10 @@
package com.chuangzhou.vivid2D.render.systems.sources;
/**
* 着色器接口
* @author tzdwindows 7
*/
public interface Shader {
String getShaderCode();
String getShaderName();
}

View File

@@ -0,0 +1,284 @@
package com.chuangzhou.vivid2D.render.systems.sources;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.sources.def.Shader2D;
import com.chuangzhou.vivid2D.render.systems.sources.def.SolidColorShader;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
import org.lwjgl.system.MemoryStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.lwjgl.system.MemoryStack.stackPush;
/**
* 着色器管理器 - 负责着色器的编译、链接和管理
*
* @author tzdwindows 7
* @version 1.0
* @since 2025-10-16
*/
public class ShaderManagement {
private static final Logger logger = LoggerFactory.getLogger(ShaderManagement.class);
/**
* 着色器程序缓存映射,按名称存储已编译的着色器程序
*/
public static final Map<String, ShaderProgram> shaderMap = new HashMap<>();
/**
* 着色器列表,按顺序存储所有着色器源代码
*/
public static final List<CompleteShader> shaderList = List.of(
new Shader2D(),
new SolidColorShader()
);
/**
* 默认着色器程序
*/
private static ShaderProgram defaultProgram;
/**
* 编译所有注册的着色器
*/
public static void compileAllShaders() {
// 确保在渲染线程
RenderSystem.assertOnRenderThread();
for (CompleteShader completeShader : shaderList) {
compileShaderProgram(completeShader);
}
// 设置默认着色器
if (defaultProgram == null && !shaderMap.isEmpty()) {
defaultProgram = shaderMap.values().iterator().next();
}
}
/**
* 编译单个完整的着色器程序
*/
private static void compileShaderProgram(CompleteShader completeShader) {
String shaderName = completeShader.getShaderName();
try {
// 编译顶点着色器
Shader vertexShader = completeShader.getVertexShader();
int vsId = compileShader(GL20.GL_VERTEX_SHADER, vertexShader.getShaderCode(),
vertexShader.getShaderName());
// 编译片段着色器
Shader fragmentShader = completeShader.getFragmentShader();
int fsId = compileShader(GL20.GL_FRAGMENT_SHADER, fragmentShader.getShaderCode(),
fragmentShader.getShaderName());
// 链接程序
int programId = linkProgram(vsId, fsId, shaderName);
// 创建着色器程序对象
ShaderProgram shaderProgram = new ShaderProgram(programId);
shaderMap.put(shaderName, shaderProgram);
// 如果是默认着色器,设置为默认程序
if (completeShader.isDefaultShader()) {
defaultProgram = shaderProgram;
setupDefaultUniforms(shaderProgram);
}
// 清理单独的着色器对象
RenderSystem.deleteShader(vsId);
RenderSystem.deleteShader(fsId);
logger.info("成功编译着色器: {}", shaderName);
} catch (Exception e) {
logger.error("编译着色器失败: {}", shaderName);
e.printStackTrace();
throw new RuntimeException("Shader compilation failed: " + shaderName, e);
}
}
/**
* 设置默认着色器的uniform值
*/
private static void setupDefaultUniforms(ShaderProgram program) {
program.use();
// 设置纹理单元
setUniformInt(program, "uTexture", 0);
setUniformFloat(program, "uOpacity", 1.0f);
setUniformVec4(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
setUniformInt(program, "uBlendMode", 0);
setUniformInt(program, "uDebugMode", 0);
setUniformInt(program, "uLightCount", 0);
program.stop();
RenderSystem.checkGLError("setupDefaultUniforms");
}
/**
* 编译着色器
*/
private static int compileShader(int type, String source, String shaderName) {
int shaderId = RenderSystem.createShader(type);
RenderSystem.shaderSource(shaderId, source);
RenderSystem.compileShader(shaderId);
// 检查编译状态
if (RenderSystem.getShaderi(shaderId, RenderSystem.GL_COMPILE_STATUS) != RenderSystem.GL_TRUE) {
String log = RenderSystem.getShaderInfoLog(shaderId);
RenderSystem.deleteShader(shaderId);
throw new RuntimeException("着色器编译失败 [" + shaderName + "]:\n" + log);
}
return shaderId;
}
/**
* 链接着色器程序
*/
private static int linkProgram(int vertexShaderId, int fragmentShaderId, String programName) {
int programId = RenderSystem.createProgram();
RenderSystem.attachShader(programId, vertexShaderId);
RenderSystem.attachShader(programId, fragmentShaderId);
RenderSystem.linkProgram(programId);
// 检查链接状态
if (RenderSystem.getProgrami(programId, RenderSystem.GL_LINK_STATUS) != RenderSystem.GL_TRUE) {
String log = RenderSystem.getProgramInfoLog(programId);
RenderSystem.deleteProgram(programId);
throw new RuntimeException("着色器程序链接失败 [" + programName + "]:\n" + log);
}
// 验证程序(使用自定义验证方法)
validateProgram(programId, programName);
return programId;
}
/**
* 自定义程序验证方法
*/
private static void validateProgram(int programId, String programName) {
int validateStatus = RenderSystem.getProgrami(programId, RenderSystem.GL_VALIDATE_STATUS);
if (validateStatus != RenderSystem.GL_TRUE) {
String log = RenderSystem.getProgramInfoLog(programId);
logger.warn("着色器程序验证警告 [{}]: {}", programName, log);
}
}
/**
* 获取默认着色器程序
*/
public static ShaderProgram getDefaultProgram() {
return defaultProgram;
}
/**
* 按名称获取着色器程序
*/
public static ShaderProgram getShaderProgram(String name) {
return shaderMap.get(name);
}
public static List<CompleteShader> getShaderList() {
return shaderList;
}
/**
* 清理所有着色器资源
*/
public static void cleanup() {
RenderSystem.assertOnRenderThread();
for (ShaderProgram program : shaderMap.values()) {
program.delete();
}
shaderMap.clear();
defaultProgram = null;
}
// Uniform设置方法
public static void setUniformInt(ShaderProgram program, String name, int value) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniform1i(location, value);
}
}
public static void setUniformFloat(ShaderProgram program, String name, float value) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniform1f(location, value);
}
}
public static void setUniformVec2(ShaderProgram program, String name, float x, float y) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniform2f(location, x, y);
}
}
public static void setUniformVec2(ShaderProgram program, String name, org.joml.Vector2f vec) {
setUniformVec2(program, name, vec.x, vec.y);
}
public static void setUniformVec3(ShaderProgram program, String name, float x, float y, float z) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniform3f(location, x, y, z);
}
}
public static void setUniformVec3(ShaderProgram program, String name, Vector3f vec) {
setUniformVec3(program, name, vec.x, vec.y, vec.z);
}
public static void setUniformVec4(ShaderProgram program, String name, float[] values) {
if (values.length != 4) {
throw new IllegalArgumentException("Vec4 uniform requires 4 values");
}
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
try (MemoryStack stack = stackPush()) {
FloatBuffer buffer = stack.mallocFloat(4);
buffer.put(values).flip();
RenderSystem.uniform4f(location, values[0], values[1], values[2], values[3]);
}
}
}
public static void setUniformVec4(ShaderProgram program, String name, Vector4f vec) {
setUniformVec4(program, name, new float[]{vec.x, vec.y, vec.z, vec.w});
}
public static void setUniformMat3(ShaderProgram program, String name, FloatBuffer matrix) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniformMatrix3(location, false, matrix);
}
}
public static void setUniformMat3(ShaderProgram program, String name, org.joml.Matrix3f matrix) {
program.use();
int location = program.getUniformLocation(name);
if (location != -1) {
RenderSystem.uniformMatrix3(location, matrix);
}
}
}

View File

@@ -0,0 +1,117 @@
package com.chuangzhou.vivid2D.render.systems.sources;
import org.joml.Matrix3f;
import org.lwjgl.opengl.GL20;
import java.util.HashMap;
import java.util.Map;
import static org.lwjgl.opengl.GL20.*;
/**
* @author tzdwindows 7
*/
public class ShaderProgram {
public final int programId;
public final Map<String, Integer> uniformCache = new HashMap<>();
public ShaderProgram(int programId) {
this.programId = programId;
}
public void use() {
GL20.glUseProgram(programId);
}
public void stop() {
GL20.glUseProgram(0);
}
public int getUniformLocation(String name) {
return uniformCache.computeIfAbsent(name, k -> {
return glGetUniformLocation(programId, k);
});
}
// 添加 uniform 设置方法
public void setUniform1i(String name, int value) {
int location = getUniformLocation(name);
if (location != -1) {
glUniform1i(location, value);
}
}
public void setUniform1f(String name, float value) {
int location = getUniformLocation(name);
if (location != -1) {
glUniform1f(location, value);
}
}
public void setUniform2f(String name, float x, float y) {
int location = getUniformLocation(name);
if (location != -1) {
glUniform2f(location, x, y);
}
}
public void setUniform3f(String name, float x, float y, float z) {
int location = getUniformLocation(name);
if (location != -1) {
glUniform3f(location, x, y, z);
}
}
public void setUniform4f(String name, float x, float y, float z, float w) {
int location = getUniformLocation(name);
if (location != -1) {
glUniform4f(location, x, y, z, w);
}
}
public void setUniformMatrix3(String name, Matrix3f matrix) {
int location = getUniformLocation(name);
if (location != -1) {
float[] matrixArray = new float[9];
matrix.get(matrixArray);
glUniformMatrix3fv(location, false, matrixArray);
}
}
// 重载方法,直接使用 location
public void setUniform1i(int location, int value) {
if (location != -1) {
glUniform1i(location, value);
}
}
public void setUniform1f(int location, float value) {
if (location != -1) {
glUniform1f(location, value);
}
}
public void setUniform4f(int location, float x, float y, float z, float w) {
if (location != -1) {
glUniform4f(location, x, y, z, w);
}
}
public void setUniformMatrix3(int location, Matrix3f matrix) {
if (location != -1) {
float[] matrixArray = new float[9];
matrix.get(matrixArray);
glUniformMatrix3fv(location, false, matrixArray);
}
}
public void delete() {
if (GL20.glIsProgram(programId)) {
GL20.glDeleteProgram(programId);
}
}
public int getProgramId() {
return programId;
}
}

View File

@@ -1,42 +1,11 @@
package com.chuangzhou.vivid2D.render.systems;
package com.chuangzhou.vivid2D.render.systems.sources.def;
import org.lwjgl.opengl.GL20;
import java.util.HashMap;
import java.util.Map;
import static org.lwjgl.opengl.GL20.glGetUniformLocation;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
/**
* 着色器源代码
*
* @author tzdwindows 7
* @version 1.0
* @since 2025-10-16
*/
public class ShaderSources {
public static final String VERTEX_SHADER_SRC =
"""
#version 330 core
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
out vec2 vWorldPos;
uniform mat3 uModelMatrix;
uniform mat3 uViewMatrix;
uniform mat3 uProjectionMatrix;
void main() {
// 使用 3x3 矩阵链计算屏幕位置假设矩阵是二维仿射
vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);
gl_Position = vec4(p.xy, 0.0, 1.0);
vTexCoord = aTexCoord;
// 输出 world-space 位置供 fragment shader 使用 xy
vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy;
}
""";
public class FragmentShaders implements Shader {
public static final String FRAGMENT_SHADER_SRC =
"""
#version 330 core
@@ -137,32 +106,13 @@ public class ShaderSources {
FragColor = vec4(finalColor, alpha);
}
""";
@Override
public String getShaderCode() {
return FRAGMENT_SHADER_SRC;
}
public static class ShaderProgram {
public final int programId;
public final Map<String, Integer> uniformCache = new HashMap<>();
public ShaderProgram(int programId) {
this.programId = programId;
}
public void use() {
GL20.glUseProgram(programId);
}
public void stop() {
GL20.glUseProgram(0);
}
public int getUniformLocation(String name) {
return uniformCache.computeIfAbsent(name, k -> {
return glGetUniformLocation(programId, k);
});
}
public void delete() {
if (GL20.glIsProgram(programId)) GL20.glDeleteProgram(programId);
}
@Override
public String getShaderName() {
return "Fragment shaders";
}
}

View File

@@ -0,0 +1,33 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
/**
* 默认着色器实现
*
* @author tzdwindows 7
* @version 1.0
* @since 2025-10-17
*/
public class Shader2D implements CompleteShader {
@Override
public Shader getVertexShader() {
return new VertexShaders();
}
@Override
public Shader getFragmentShader() {
return new FragmentShaders();
}
@Override
public String getShaderName() {
return "Vivid2d Shader";
}
@Override
public boolean isDefaultShader() {
return true;
}
}

View File

@@ -0,0 +1,40 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
/**
* 纯色着色器的片段着色器
* 只使用颜色,忽略纹理
* @author tzdwindows 7
*/
public class SolidColorFragmentShader implements Shader {
public static final String FRAGMENT_SHADER_SRC =
"""
#version 330 core
out vec4 FragColor;
uniform vec4 uColor;
uniform float uOpacity;
void main() {
// 直接使用颜色,忽略纹理
vec4 finalColor = uColor;
finalColor.a *= uOpacity;
// 如果透明度太低则丢弃片段
if (finalColor.a <= 0.001) discard;
FragColor = finalColor;
}
""";
@Override
public String getShaderCode() {
return FRAGMENT_SHADER_SRC;
}
@Override
public String getShaderName() {
return "Solid Color Fragment Shader";
}
}

View File

@@ -0,0 +1,82 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
/**
* 纯色着色器程序
* 专门用于绘制纯色几何体,如选中框、调试图形等
* @author tzdwindows 7
*/
public class SolidColorShader implements CompleteShader {
private final SolidColorVertexShader vertexShader;
private final SolidColorFragmentShader fragmentShader;
public SolidColorShader() {
this.vertexShader = new SolidColorVertexShader();
this.fragmentShader = new SolidColorFragmentShader();
}
@Override
public Shader getVertexShader() {
return vertexShader;
}
@Override
public Shader getFragmentShader() {
return fragmentShader;
}
@Override
public String getShaderName() {
return "Solid Color Shader";
}
@Override
public boolean isDefaultShader() {
return false; // 这不是默认着色器,是专门用途的着色器
}
@Override
public void setDefaultUniforms(ShaderProgram program) {
// 设置默认的uniform值
if (program != null) {
// 设置默认颜色为白色
int colorLoc = program.getUniformLocation("uColor");
if (colorLoc != -1) {
program.setUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
}
// 设置默认不透明度
int opacityLoc = program.getUniformLocation("uOpacity");
if (opacityLoc != -1) {
program.setUniform1f(opacityLoc, 1.0f);
}
}
}
/**
* 设置着色器颜色
*/
public void setColor(ShaderProgram program, float r, float g, float b, float a) {
if (program != null) {
int colorLoc = program.getUniformLocation("uColor");
if (colorLoc != -1) {
program.setUniform4f(colorLoc, r, g, b, a);
}
}
}
/**
* 设置着色器不透明度
*/
public void setOpacity(ShaderProgram program, float opacity) {
if (program != null) {
int opacityLoc = program.getUniformLocation("uOpacity");
if (opacityLoc != -1) {
program.setUniform1f(opacityLoc, opacity);
}
}
}
}

View File

@@ -0,0 +1,36 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
/**
* 纯色着色器的顶点着色器
* @author tzdwindows 7
*/
public class SolidColorVertexShader implements Shader {
public static final String VERTEX_SHADER_SRC =
"""
#version 330 core
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTexCoord;
uniform mat3 uModelMatrix;
uniform mat3 uViewMatrix;
uniform mat3 uProjectionMatrix;
void main() {
// 使用 3x3 矩阵链计算屏幕位置
vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);
gl_Position = vec4(p.xy, 0.0, 1.0);
}
""";
@Override
public String getShaderCode() {
return VERTEX_SHADER_SRC;
}
@Override
public String getShaderName() {
return "Solid Color Vertex Shader";
}
}

View File

@@ -0,0 +1,44 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.Shader;
/**
* 顶点着色器
*
* @author tzdwindows 7
* @version 1.0
* @since 2025-10-17
*/
public class VertexShaders implements Shader {
public static final String VERTEX_SHADER_SRC =
"""
#version 330 core
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
out vec2 vWorldPos;
uniform mat3 uModelMatrix;
uniform mat3 uViewMatrix;
uniform mat3 uProjectionMatrix;
void main() {
// 使用 3x3 矩阵链计算屏幕位置(假设矩阵是二维仿射)
vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0);
gl_Position = vec4(p.xy, 0.0, 1.0);
vTexCoord = aTexCoord;
// 输出 world-space 位置供 fragment shader 使用(仅 xy
vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy;
}
""";
@Override
public String getShaderCode() {
return VERTEX_SHADER_SRC;
}
@Override
public String getShaderName() {
return "Vertex Shaders";
}
}

View File

@@ -0,0 +1,112 @@
package com.chuangzhou.vivid2D.test;
import com.chuangzhou.vivid2D.render.awt.ModelClickListener;
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import javax.swing.*;
import java.awt.*;
/**
* 简单的测试示例:创建一个 Model2D添加几层部件
* 然后在 JFrame 中展示 ModelLayerPanel左侧、ModelRenderPanel中间渲染区
* 和模型树(右侧)以便观察变化。
*/
public class ModelLayerPanelTest {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// 创建示例模型并添加图层
Model2D model = new Model2D("示例模型");
// 调整一些初始属性(可选)
ModelPart person = model.getPart("人物");
if (person != null) {
try {
person.setOpacity(0.85f);
} catch (Exception ignored) {}
}
// 创建 UI
JFrame frame = new JFrame("ModelLayerPanel 测试(含渲染面板)");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 左侧:图层面板(传入 renderPanel 后可在面板中绑定贴图到 GL 上下文)
// 先创建一个占位 renderPanel再把它传给 layerPanelModelRenderPanel 构造需要尺寸)
ModelRenderPanel renderPanel = new ModelRenderPanel(model, 640, 480);
//renderPanel.addModelClickListener(new ModelClickListener() {
// @Override
// public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {
// if (mesh == null) return;
// System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY);
// }
//});
ModelLayerPanel layerPanel = new ModelLayerPanel(model, renderPanel);
layerPanel.setPreferredSize(new Dimension(260, 600));
frame.add(layerPanel, BorderLayout.WEST);
// 中间:渲染面板
renderPanel.setPreferredSize(new Dimension(640, 480));
frame.add(renderPanel, BorderLayout.CENTER);
// 右侧:显示模型树(用于观察当前模型部件结构)
JTree tree = new JTree(model.toTreeNode());
JScrollPane treeScroll = new JScrollPane(tree);
treeScroll.setPreferredSize(new Dimension(240, 600));
frame.add(treeScroll, BorderLayout.EAST);
// 底部:演示按钮(刷新树以反映面板中对模型的更改)
JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton refreshBtn = new JButton("刷新模型树");
refreshBtn.addActionListener(e -> {
tree.setModel(new javax.swing.tree.DefaultTreeModel(model.toTreeNode()));
for (int i = 0; i < tree.getRowCount(); i++) tree.expandRow(i);
// 同步通知渲染面板(如果需要)去刷新模型
try {
renderPanel.setModel(model);
} catch (Exception ignored) {}
});
bottom.add(refreshBtn);
JButton printOrderBtn = new JButton("打印部件顺序(控制台)");
printOrderBtn.addActionListener(e -> {
System.out.println("当前模型部件顺序:");
for (ModelPart p : model.getParts()) {
System.out.println(" - " + p.getName() + " (可见=" + p.isVisible() + ", 不透明度=" + p.getOpacity() + ")");
}
});
bottom.add(printOrderBtn);
frame.add(bottom, BorderLayout.SOUTH);
// 监听窗口关闭,确保释放 GL 资源
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
// 先释放渲染面板相关 GL 资源与线程
try {
renderPanel.dispose();
} catch (Throwable t) {
t.printStackTrace();
}
}
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
// 进程退出(确保彻底关闭)
try {
renderPanel.dispose();
} catch (Throwable ignored) {}
System.exit(0);
}
});
frame.setSize(1200, 700);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Random;
/**
* ModelRenderLightingTest
* 测试使用 Model2D + 光源进行简单光照渲染
* @author tzdwindows 7
*/
public class ModelRenderLightingTest {
@@ -114,6 +115,7 @@ public class ModelRenderLightingTest {
rightArm.setPosition(60, -20);
Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90);
rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED));
rightArmMesh.setSelected( true);
rightArm.addMesh(rightArmMesh);
// legs

View File

@@ -1,6 +1,6 @@
package com.chuangzhou.vivid2D.test;
import com.chuangzhou.vivid2D.render.awt.ModelGLPanel;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
@@ -31,15 +31,15 @@ public class TestModelGLPanel {
JFrame frame = new JFrame("ModelGLPanel Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ModelGLPanel glPanel = null;
ModelRenderPanel glPanel = null;
try {
// 先创建一个空的 Model2D 实例(将在 GL 上下文中初始化更详细内容)
testModel = new Model2D("Humanoid");
glPanel = new ModelGLPanel(testModel, 800, 600);
glPanel = new ModelRenderPanel(testModel, 800, 600);
// 在 GL 上下文中创建 mesh / part / physics 等资源
ModelGLPanel finalGlPanel = glPanel;
ModelRenderPanel finalGlPanel = glPanel;
glPanel.executeInGLContext(() -> {
setupModelInGL(testModel);
return null;