feat(render): 实现模型图层管理与选中高亮功能
- 添加 ModelLayerPanel 图层管理面板,支持图层增删、重排、重命名- 实现 Mesh2D 选中状态管理与可视化高亮边框绘制 - 添加模型点击与悬停事件监听接口 ModelClickListener - 引入完整着色器接口 CompleteShader 及默认片段着色器实现 - 改进 BufferUploader 支持颜色 uniform 传递- 完善 Mesh2D 复制逻辑与边界框计算方法 - 重构部分工具类包路径并增强矩阵工具功能 - 移除 LightSourceData 中冗余的构造逻辑 重要更新 - 更新了一个可视化界面可以控制图层顺序(ModelLayerPanel),并且给ModelRenderPanel增加了很多新功能,比如设置模型图层位置、大小 - 重写了逻辑着色器(Shader)、BufferUploader逻辑,让着色器能够规范的注册和使用
This commit is contained in:
@@ -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_LIGHTS(8)
|
||||
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();
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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 资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -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.parts(model.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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// 应用混合模式
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.chuangzhou.vivid2D.render.systems.sources;
|
||||
|
||||
/**
|
||||
* 着色器接口
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public interface Shader {
|
||||
String getShaderCode();
|
||||
String getShaderName();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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,再把它传给 layerPanel(ModelRenderPanel 构造需要尺寸)
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user