feat(render): 实现摄像机系统和文字渲染功能
- 添加 Camera 类,支持位置、缩放、Z轴控制- 在 ModelRender 中集成摄像机投影矩阵计算 - 实现屏幕坐标到世界坐标的转换方法 - 添加默认文字渲染器和字体加载逻辑 - 在渲染面板中添加摄像机控制的鼠标手势支持 - 支持通过鼠标滚轮进行摄像机缩放操作 - 添加摄像机状态显示和调试信息渲染 - 实现多选框渲染逻辑的重构和优化 -修复坐标系变换相关的边界框计算问题 - 增加摄像机启用/禁用快捷键支持
This commit is contained in:
@@ -2,6 +2,7 @@ package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.systems.Camera;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.model.util.LightSource;
|
||||
@@ -18,6 +19,7 @@ import org.lwjgl.opengl.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -73,14 +75,14 @@ public final class ModelRender {
|
||||
* 默认值:800像素
|
||||
* @see #setViewport(int, int)
|
||||
*/
|
||||
private static int viewportWidth = 800;
|
||||
static int viewportWidth = 800;
|
||||
|
||||
/**
|
||||
* 视口高度(像素),定义渲染区域的大小
|
||||
* 默认值:600像素
|
||||
* @see #setViewport(int, int)
|
||||
*/
|
||||
private static int viewportHeight = 600;
|
||||
static int viewportHeight = 600;
|
||||
|
||||
/**
|
||||
* 清除颜色(RGBA),用于在每帧开始时清空颜色缓冲区
|
||||
@@ -179,8 +181,105 @@ public final class ModelRender {
|
||||
*/
|
||||
public static boolean renderLightPositions = true;
|
||||
|
||||
// ================== 内部类:ShaderProgram ==================
|
||||
// ================== 摄像机状态 ==================
|
||||
|
||||
/**
|
||||
* 默认摄像机,用于控制场景的视图和缩放
|
||||
* 默认位置:(0, 0)
|
||||
*/
|
||||
private static final Camera camera = new Camera();
|
||||
|
||||
// ================== 字体管理 ==================
|
||||
private static TextRenderer defaultTextRenderer = null;
|
||||
private static final int FONT_BITMAP_WIDTH = 512;
|
||||
private static final int FONT_BITMAP_HEIGHT = 512;
|
||||
private static final int FONT_FIRST_CHAR = 32;
|
||||
private static final int FONT_CHAR_COUNT = 96;
|
||||
// ================== 摄像机API方法 ==================
|
||||
|
||||
/**
|
||||
* 获取全局摄像机实例
|
||||
*/
|
||||
public static Camera getCamera() {
|
||||
return camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机位置
|
||||
*/
|
||||
public static void setCameraPosition(float x, float y) {
|
||||
camera.setPosition(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机缩放
|
||||
*/
|
||||
public static void setCameraZoom(float zoom) {
|
||||
camera.setZoom(zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机Z轴位置
|
||||
*/
|
||||
public static void setCameraZPosition(float z) {
|
||||
camera.setZPosition(z);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动摄像机
|
||||
*/
|
||||
public static void moveCamera(float dx, float dy) {
|
||||
camera.move(dx, dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放摄像机
|
||||
*/
|
||||
public static void zoomCamera(float factor) {
|
||||
camera.zoom(factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置摄像机
|
||||
*/
|
||||
public static void resetCamera() {
|
||||
camera.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用摄像机
|
||||
*/
|
||||
public static void setCameraEnabled(boolean enabled) {
|
||||
camera.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建考虑摄像机变换的投影矩阵
|
||||
*/
|
||||
private static Matrix3f buildCameraProjection(int width, int height) {
|
||||
Matrix3f m = new Matrix3f();
|
||||
|
||||
if (camera.isEnabled()) {
|
||||
// 考虑摄像机缩放和平移
|
||||
float zoom = camera.getZoom();
|
||||
Vector2f pos = camera.getPosition();
|
||||
|
||||
m.set(
|
||||
2.0f * zoom / width, 0.0f, -1.0f - (2.0f * zoom * pos.x / width),
|
||||
0.0f, -2.0f * zoom / height, 1.0f + (2.0f * zoom * pos.y / height),
|
||||
0.0f, 0.0f, 1.0f
|
||||
);
|
||||
} else {
|
||||
// 原始投影矩阵
|
||||
m.set(
|
||||
2.0f / width, 0.0f, -1.0f,
|
||||
0.0f, -2.0f / height, 1.0f,
|
||||
0.0f, 0.0f, 1.0f
|
||||
);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================== 内部类:MeshGLResources ==================
|
||||
private static class MeshGLResources {
|
||||
@@ -221,10 +320,47 @@ public final class ModelRender {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
|
||||
createDefaultTexture();
|
||||
RenderSystem.viewport(0, 0, viewportWidth, viewportHeight);
|
||||
RenderSystem.finishInitialization();
|
||||
|
||||
try {
|
||||
// 初始化默认字体(可替换为你自己的 TTF 数据)
|
||||
ByteBuffer fontData = null;
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("Arial.ttf");
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load Arial.ttf, trying fallback fonts", e);
|
||||
// 尝试其他字体
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("arial.ttf");
|
||||
} catch (Exception e2) {
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("times.ttf");
|
||||
} catch (Exception e3) {
|
||||
logger.error("All font loading attempts failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fontData != null && fontData.capacity() > 0) {
|
||||
defaultTextRenderer = new TextRenderer(FONT_BITMAP_WIDTH, FONT_BITMAP_HEIGHT, FONT_FIRST_CHAR, FONT_CHAR_COUNT);
|
||||
RenderSystem.checkGLError("TextRenderer constructor");
|
||||
|
||||
defaultTextRenderer.initialize(fontData, 32.0f); // 字体像素高度 32
|
||||
RenderSystem.checkGLError("defaultTextRenderer initialization");
|
||||
|
||||
if (!defaultTextRenderer.isInitialized()) {
|
||||
logger.error("TextRenderer failed to initialize properly");
|
||||
}
|
||||
} else {
|
||||
logger.error("No valid font data available for text rendering");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to initialize default text renderer", e);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
logger.info("ModelRender initialized successfully");
|
||||
}
|
||||
@@ -456,8 +592,8 @@ public final class ModelRender {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置投影与视图矩阵(所有着色器都需要)
|
||||
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
|
||||
// 设置投影与视图矩阵(使用摄像机变换)
|
||||
Matrix3f proj = buildCameraProjection(viewportWidth, viewportHeight);
|
||||
Matrix3f view = new Matrix3f().identity();
|
||||
|
||||
// 1. 首先设置默认着色器
|
||||
@@ -469,6 +605,10 @@ public final class ModelRender {
|
||||
setUniformMatrix3(defaultProgram, "uViewMatrix", view);
|
||||
RenderSystem.checkGLError("after_set_default_matrices");
|
||||
|
||||
// 设置摄像机Z轴位置(如果着色器支持)
|
||||
setUniformFloatInternal(defaultProgram, "uCameraZ", camera.getZPosition());
|
||||
RenderSystem.checkGLError("after_set_camera_z");
|
||||
|
||||
// 添加光源数据上传到默认着色器
|
||||
uploadLightsToShader(defaultProgram, model);
|
||||
RenderSystem.checkGLError("after_upload_lights");
|
||||
@@ -495,10 +635,22 @@ public final class ModelRender {
|
||||
RenderSystem.checkGLError("after_render_colliders");
|
||||
}
|
||||
|
||||
defaultProgram.stop();
|
||||
if (defaultTextRenderer != null) {
|
||||
String camInfo = String.format("Camera X: %.2f Y: %.2f Zoom: %.2f",
|
||||
camera.getPosition().x,
|
||||
camera.getPosition().y,
|
||||
camera.getZoom());
|
||||
float x = 10.0f;
|
||||
float y = viewportHeight - 30.0f;
|
||||
Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
renderText(camInfo, x, y, color);
|
||||
RenderSystem.checkGLError("renderText");
|
||||
}
|
||||
|
||||
RenderSystem.checkGLError("render_end");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置所有非默认着色器的顶点坐标相关uniform
|
||||
*/
|
||||
@@ -534,6 +686,9 @@ public final class ModelRender {
|
||||
// 设置基础模型矩阵为单位矩阵
|
||||
setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity());
|
||||
|
||||
// 设置摄像机Z轴位置
|
||||
setUniformFloatInternal(program, "uCameraZ", camera.getZPosition());
|
||||
|
||||
RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName());
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -840,6 +995,41 @@ public final class ModelRender {
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文字
|
||||
* @param text 文字内容
|
||||
* @param x 世界坐标 X
|
||||
* @param y 世界坐标 Y
|
||||
* @param color RGBA 颜色
|
||||
*/
|
||||
public static void renderText(String text, float x, float y, Vector4f color) {
|
||||
if (!initialized || defaultTextRenderer == null) return;
|
||||
RenderSystem.assertOnRenderThread();
|
||||
Vector2f offset = getCameraOffset();
|
||||
float px = x + offset.x;
|
||||
float py = y + offset.y;
|
||||
defaultTextRenderer.renderText(text, px, py, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认摄像机与当前摄像机之间的偏移量
|
||||
* @return Vector2f 偏移向量 (dx, dy)
|
||||
*/
|
||||
public static Vector2f getCameraOffset() {
|
||||
float width = viewportWidth;
|
||||
float height = viewportHeight;
|
||||
float zoom = camera.getZoom();
|
||||
Vector2f pos = camera.getPosition();
|
||||
float tx = -1.0f - (2.0f * zoom * pos.x / width);
|
||||
float ty = 1.0f + (2.0f * zoom * pos.y / height);
|
||||
float tx0 = -1.0f;
|
||||
float ty0 = 1.0f;
|
||||
float offsetX = tx - tx0;
|
||||
float offsetY = ty - ty0;
|
||||
offsetX = -offsetX * width / 2.0f / zoom;
|
||||
offsetY = offsetY * height / 2.0f / zoom;
|
||||
return new Vector2f(offsetX, offsetY);
|
||||
}
|
||||
public static void setViewport(int width, int height) {
|
||||
viewportWidth = Math.max(1, width);
|
||||
viewportHeight = Math.max(1, height);
|
||||
|
||||
237
src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java
Normal file
237
src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.*;
|
||||
import org.lwjgl.stb.STBTTAlignedQuad;
|
||||
import org.lwjgl.stb.STBTTBakedChar;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.lwjgl.stb.STBTruetype.*;
|
||||
|
||||
/**
|
||||
* OpenGL 文字渲染器实例类
|
||||
* 支持多字体、多实例管理,每个实例维护独立的字符数据与纹理
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public final class TextRenderer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TextRenderer.class);
|
||||
private final int bitmapWidth;
|
||||
private final int bitmapHeight;
|
||||
private final int firstChar;
|
||||
private final int charCount;
|
||||
|
||||
private STBTTBakedChar.Buffer charData;
|
||||
private int fontTextureId;
|
||||
private boolean initialized = false;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param bitmapWidth 字符纹理宽度
|
||||
* @param bitmapHeight 字符纹理高度
|
||||
* @param firstChar 字符起始码
|
||||
* @param charCount 字符数量
|
||||
*/
|
||||
public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) {
|
||||
this.bitmapWidth = bitmapWidth;
|
||||
this.bitmapHeight = bitmapHeight;
|
||||
this.firstChar = firstChar;
|
||||
this.charCount = charCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化字体渲染器
|
||||
*
|
||||
* @param fontData TTF 字体文件内容
|
||||
* @param fontHeight 字体像素高度
|
||||
*/
|
||||
public void initialize(ByteBuffer fontData, float fontHeight) {
|
||||
if (initialized) return;
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
shader.use();
|
||||
// 验证输入参数
|
||||
if (fontData == null || fontData.capacity() == 0) {
|
||||
logger.error("Invalid font data provided to TextRenderer");
|
||||
return;
|
||||
}
|
||||
if (fontHeight <= 0) {
|
||||
logger.error("Invalid font height: {}", fontHeight);
|
||||
return;
|
||||
}
|
||||
if (bitmapWidth <= 0 || bitmapHeight <= 0) {
|
||||
logger.error("Invalid bitmap dimensions: {}x{}", bitmapWidth, bitmapHeight);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
charData = STBTTBakedChar.malloc(charCount);
|
||||
|
||||
// 分配位图内存
|
||||
int bitmapSize = bitmapWidth * bitmapHeight;
|
||||
if (bitmapSize <= 0) {
|
||||
logger.error("Invalid bitmap size: {}", bitmapSize);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer bitmap = ByteBuffer.allocateDirect(bitmapSize);
|
||||
|
||||
// 烘焙字体位图
|
||||
int result = stbtt_BakeFontBitmap(fontData, fontHeight, bitmap, bitmapWidth, bitmapHeight, firstChar, charData);
|
||||
if (result <= 0) {
|
||||
logger.error("stbtt_BakeFontBitmap failed with result: {}", result);
|
||||
charData.free();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建纹理
|
||||
fontTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, bitmap);
|
||||
|
||||
if (fontTextureId == 0) {
|
||||
logger.error("Failed to create font texture");
|
||||
charData.free();
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
logger.debug("TextRenderer initialized successfully with texture ID: {}", fontTextureId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception during TextRenderer initialization: {}", e.getMessage());
|
||||
if (charData != null) {
|
||||
charData.free();
|
||||
charData = null;
|
||||
}
|
||||
}
|
||||
shader.stop();
|
||||
}
|
||||
|
||||
private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
|
||||
int textureId = RenderSystem.genTextures();
|
||||
RenderSystem.bindTexture(textureId);
|
||||
|
||||
// 使用更兼容的纹理格式
|
||||
RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_ALPHA,
|
||||
width, height, 0, GL11.GL_ALPHA, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
|
||||
RenderSystem.setTextureMinFilter(GL11.GL_LINEAR);
|
||||
RenderSystem.setTextureMagFilter(GL11.GL_LINEAR);
|
||||
RenderSystem.setTextureWrapS(GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.setTextureWrapT(GL12.GL_CLAMP_TO_EDGE);
|
||||
|
||||
RenderSystem.bindTexture(0);
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文字(使用 RenderSystem 封装,不使用 glBegin/glEnd)
|
||||
*
|
||||
* @param text 要显示的文字
|
||||
* @param x 世界坐标 X
|
||||
* @param y 世界坐标 Y
|
||||
* @param color 文字颜色
|
||||
*/
|
||||
public void renderText(String text, float x, float y, Vector4f color) {
|
||||
if (!initialized || text == null || text.isEmpty()) return;
|
||||
|
||||
RenderSystem.assertOnRenderThread();
|
||||
|
||||
// 保存当前状态
|
||||
RenderSystem.pushState();
|
||||
|
||||
try {
|
||||
// 检查文本着色器是否存在,如果不存在则创建默认的
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
|
||||
shader.use();
|
||||
ShaderManagement.setUniformVec4(shader, "uColor", color);
|
||||
ShaderManagement.setUniformInt(shader, "uTexture", 0);
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
RenderSystem.disableDepthTest();
|
||||
|
||||
RenderSystem.activeTexture(GL13.GL_TEXTURE0);
|
||||
RenderSystem.bindTexture(fontTextureId);
|
||||
|
||||
Vector2f offset = ModelRender.getCameraOffset();
|
||||
float px = x + offset.x;
|
||||
float py = y + offset.y;
|
||||
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
STBTTAlignedQuad q = STBTTAlignedQuad.mallocStack(stack);
|
||||
float[] xpos = {px};
|
||||
float[] ypos = {py};
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder builder = tesselator.getBuilder();
|
||||
|
||||
// 计算估计的顶点数量:每个字符6个顶点(2个三角形)
|
||||
int estimatedVertexCount = text.length() * 6;
|
||||
|
||||
// 修复:begin方法需要2个参数
|
||||
builder.begin(RenderSystem.DRAW_TRIANGLES, estimatedVertexCount);
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c < firstChar || c >= firstChar + charCount) continue;
|
||||
|
||||
stbtt_GetBakedQuad(charData, bitmapWidth, bitmapHeight, c - firstChar, xpos, ypos, q, true);
|
||||
|
||||
// 使用两个三角形组成一个四边形
|
||||
// 第一个三角形
|
||||
builder.vertex(q.x0(), q.y0(), q.s0(), q.t0());
|
||||
builder.vertex(q.x1(), q.y0(), q.s1(), q.t0());
|
||||
builder.vertex(q.x0(), q.y1(), q.s0(), q.t1());
|
||||
|
||||
// 第二个三角形
|
||||
builder.vertex(q.x1(), q.y0(), q.s1(), q.t0());
|
||||
builder.vertex(q.x1(), q.y1(), q.s1(), q.t1());
|
||||
builder.vertex(q.x0(), q.y1(), q.s0(), q.t1());
|
||||
}
|
||||
|
||||
tesselator.end();
|
||||
}
|
||||
|
||||
RenderSystem.checkGLError("renderText");
|
||||
|
||||
} finally {
|
||||
// 恢复之前的状态
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理字体资源
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (fontTextureId != 0) {
|
||||
RenderSystem.deleteTextures(fontTextureId);
|
||||
fontTextureId = 0;
|
||||
}
|
||||
if (charData != null) {
|
||||
charData.free();
|
||||
charData = null;
|
||||
}
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public int getFontTextureId() {
|
||||
return fontTextureId;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.systems.Camera;
|
||||
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.lwjgl.glfw.*;
|
||||
import org.lwjgl.opengl.GL;
|
||||
@@ -126,6 +129,75 @@ public class ModelRenderPanel extends JPanel {
|
||||
private Map<ModelPart, Float> dragStartRotations = new HashMap<>();
|
||||
private Map<ModelPart, Vector2f> dragStartPivots = new HashMap<>();
|
||||
|
||||
// 新增:鼠标手势相关字段
|
||||
private volatile Cursor currentCursor = Cursor.getDefaultCursor();
|
||||
private volatile DragMode hoverDragMode = DragMode.NONE;
|
||||
private volatile boolean isOverSelection = false;
|
||||
|
||||
// ================== 摄像机控制相关字段 ==================
|
||||
|
||||
private volatile boolean cameraDragging = false;
|
||||
private volatile int lastCameraDragX, lastCameraDragY;
|
||||
private volatile float cameraStartX, cameraStartY;
|
||||
private static final float CAMERA_ZOOM_STEP = 1.1f;
|
||||
private static final float CAMERA_ZOOM_MIN = 0.1f;
|
||||
private static final float CAMERA_ZOOM_MAX = 10.0f;
|
||||
private static final float CAMERA_Z_STEP = 0.1f;
|
||||
private static final float CAMERA_Z_MIN = -5.0f;
|
||||
private static final float CAMERA_Z_MAX = 5.0f;
|
||||
|
||||
// ================== 摄像机控制方法 ==================
|
||||
|
||||
/**
|
||||
* 获取摄像机实例
|
||||
*/
|
||||
public Camera getCamera() {
|
||||
return ModelRender.getCamera();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机位置
|
||||
*/
|
||||
public void setCameraPosition(float x, float y) {
|
||||
executeInGLContext(() -> ModelRender.setCameraPosition(x, y));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机缩放
|
||||
*/
|
||||
public void setCameraZoom(float zoom) {
|
||||
executeInGLContext(() -> ModelRender.setCameraZoom(zoom));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摄像机Z轴位置
|
||||
*/
|
||||
public void setCameraZPosition(float z) {
|
||||
executeInGLContext(() -> ModelRender.setCameraZPosition(z));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动摄像机
|
||||
*/
|
||||
public void moveCamera(float dx, float dy) {
|
||||
executeInGLContext(() -> ModelRender.moveCamera(dx, dy));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放摄像机
|
||||
*/
|
||||
public void zoomCamera(float factor) {
|
||||
executeInGLContext(() -> ModelRender.zoomCamera(factor));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置摄像机
|
||||
*/
|
||||
public void resetCamera() {
|
||||
executeInGLContext(() -> ModelRender.resetCamera());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构造函数:使用模型路径
|
||||
*/
|
||||
@@ -190,6 +262,32 @@ public class ModelRenderPanel extends JPanel {
|
||||
clearHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加摄像机重置快捷键:Ctrl+R
|
||||
KeyStroke resetCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK);
|
||||
inputMap.put(resetCameraKey, "resetCamera");
|
||||
actionMap.put("resetCamera", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
resetCamera();
|
||||
logger.info("重置摄像机");
|
||||
}
|
||||
});
|
||||
|
||||
// 添加摄像机启用/禁用快捷键:Ctrl+E
|
||||
KeyStroke toggleCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK);
|
||||
inputMap.put(toggleCameraKey, "toggleCamera");
|
||||
actionMap.put("toggleCamera", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
//executeInGLContext(() -> {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
boolean newState = !camera.isEnabled();
|
||||
camera.setEnabled(newState);
|
||||
logger.info("{}摄像机", newState ? "启用" : "禁用");
|
||||
//});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 新增:操作历史记录方法 ============
|
||||
@@ -371,28 +469,28 @@ public class ModelRenderPanel extends JPanel {
|
||||
*/
|
||||
public void setSelectedMesh(Mesh2D mesh) {
|
||||
//executeInGLContext(() -> {
|
||||
// 清除之前选中的所有网格
|
||||
for (Mesh2D selectedMesh : selectedMeshes) {
|
||||
selectedMesh.setSelected(false);
|
||||
// 清除多选列表
|
||||
selectedMesh.clearMultiSelection();
|
||||
}
|
||||
selectedMeshes.clear();
|
||||
// 清除之前选中的所有网格
|
||||
for (Mesh2D selectedMesh : selectedMeshes) {
|
||||
selectedMesh.setSelected(false);
|
||||
// 清除多选列表
|
||||
selectedMesh.clearMultiSelection();
|
||||
}
|
||||
selectedMeshes.clear();
|
||||
|
||||
// 设置新的选中网格
|
||||
if (mesh != null) {
|
||||
mesh.setSelected(true);
|
||||
selectedMeshes.add(mesh);
|
||||
lastSelectedMesh = mesh; // 更新最后选中的网格
|
||||
// 设置新的选中网格
|
||||
if (mesh != null) {
|
||||
mesh.setSelected(true);
|
||||
selectedMeshes.add(mesh);
|
||||
lastSelectedMesh = mesh; // 更新最后选中的网格
|
||||
|
||||
// 通知其他选中网格添加到多选列表
|
||||
updateMultiSelectionInMeshes();
|
||||
} else {
|
||||
lastSelectedMesh = null;
|
||||
}
|
||||
// 通知其他选中网格添加到多选列表
|
||||
updateMultiSelectionInMeshes();
|
||||
} else {
|
||||
lastSelectedMesh = null;
|
||||
}
|
||||
|
||||
logger.debug("设置选中网格: {}, 当前选中数量: {}",
|
||||
mesh != null ? mesh.getName() : "null", selectedMeshes.size());
|
||||
logger.debug("设置选中网格: {}, 当前选中数量: {}",
|
||||
mesh != null ? mesh.getName() : "null", selectedMeshes.size());
|
||||
//});
|
||||
}
|
||||
|
||||
@@ -631,8 +729,71 @@ public class ModelRenderPanel extends JPanel {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
handleMouseReleased(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
// 鼠标离开面板时恢复默认光标
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
});
|
||||
|
||||
addMouseWheelListener(new MouseWheelListener() {
|
||||
@Override
|
||||
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||
if (!contextInitialized) return;
|
||||
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
final int notches = e.getWheelRotation();
|
||||
final boolean fine = e.isShiftDown();
|
||||
|
||||
executeInGLContext(() -> {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float oldZoom = camera.getZoom();
|
||||
|
||||
// 1. 获取缩放前的世界坐标
|
||||
float[] worldPosBefore = screenToModelCoordinates(screenX, screenY);
|
||||
if (worldPosBefore == null) return;
|
||||
|
||||
// 2. 计算新缩放级别
|
||||
// 使用 CAMERA_ZOOM_STEP 或 ZOOM_STEP,这里沿用原有的 ZOOM_STEP 逻辑
|
||||
double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP;
|
||||
float newZoom = oldZoom;
|
||||
if (notches > 0) { // 滚轮向下,缩小
|
||||
newZoom /= Math.pow(step, notches);
|
||||
} else { // 滚轮向上,放大
|
||||
newZoom *= Math.pow(step, -notches);
|
||||
}
|
||||
// 限制范围,使用 CAMERA_ZOOM_MIN/MAX 或 ZOOM_MIN/MAX,这里沿用原有的 ZOOM_MIN/MAX
|
||||
newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom));
|
||||
|
||||
if (Math.abs(newZoom - oldZoom) < 1e-6f) {
|
||||
return; // 缩放级别无变化
|
||||
}
|
||||
|
||||
// 3. 应用新缩放并获取缩放后的世界坐标
|
||||
camera.setZoom(newZoom);
|
||||
float[] worldPosAfter = screenToModelCoordinates(screenX, screenY);
|
||||
if (worldPosAfter == null) {
|
||||
camera.setZoom(oldZoom); // 如果计算失败则恢复
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 计算相机需要平移的量,以保持鼠标指针下的点不变
|
||||
float panX = worldPosBefore[0] - worldPosAfter[0];
|
||||
float panY = worldPosBefore[1] - worldPosAfter[1];
|
||||
|
||||
// 5. 应用平移
|
||||
camera.move(panX, panY);
|
||||
|
||||
// 6. 更新面板的缩放状态变量,禁用平滑缩放以确保一致性
|
||||
displayScale = newZoom;
|
||||
targetScale = newZoom;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
@@ -742,6 +903,21 @@ public class ModelRenderPanel extends JPanel {
|
||||
final int screenY = e.getY();
|
||||
requestFocusInWindow();
|
||||
|
||||
if (SwingUtilities.isMiddleMouseButton(e)) {
|
||||
cameraDragging = true;
|
||||
lastCameraDragX = screenX;
|
||||
lastCameraDragY = screenY;
|
||||
|
||||
// 记录摄像机起始位置
|
||||
Camera camera = ModelRender.getCamera();
|
||||
cameraStartX = camera.getPosition().x;
|
||||
cameraStartY = camera.getPosition().y;
|
||||
|
||||
// 设置拖拽光标
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
|
||||
return;
|
||||
}
|
||||
|
||||
shiftDuringDrag = e.isShiftDown();
|
||||
|
||||
executeInGLContext(() -> {
|
||||
@@ -845,12 +1021,57 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新拖拽过程中的光标
|
||||
updateCursorForDragMode(currentDragMode);
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.error("处理鼠标按下时出错", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据拖拽模式更新光标
|
||||
*/
|
||||
private void updateCursorForDragMode(DragMode dragMode) {
|
||||
Cursor newCursor = getCursorForDragMode(dragMode);
|
||||
if (!newCursor.equals(currentCursor)) {
|
||||
currentCursor = newCursor;
|
||||
SwingUtilities.invokeLater(() -> setCursor(newCursor));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据拖拽模式获取对应的光标
|
||||
*/
|
||||
private Cursor getCursorForDragMode(DragMode dragMode) {
|
||||
switch (dragMode) {
|
||||
case MOVE:
|
||||
return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
|
||||
case RESIZE_LEFT:
|
||||
case RESIZE_RIGHT:
|
||||
return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
|
||||
case RESIZE_TOP:
|
||||
case RESIZE_BOTTOM:
|
||||
return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
|
||||
case RESIZE_TOP_LEFT:
|
||||
case RESIZE_BOTTOM_RIGHT:
|
||||
return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
|
||||
case RESIZE_TOP_RIGHT:
|
||||
case RESIZE_BOTTOM_LEFT:
|
||||
return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
|
||||
case ROTATE:
|
||||
// 使用手型光标表示旋转
|
||||
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||
case MOVE_PIVOT:
|
||||
// 使用十字光标表示移动中心点
|
||||
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
|
||||
case NONE:
|
||||
default:
|
||||
return Cursor.getDefaultCursor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理多选逻辑
|
||||
@@ -922,43 +1143,116 @@ public class ModelRenderPanel extends JPanel {
|
||||
fromMesh.getName(), toMesh.getName(), selectedMeshes.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 2D 向量 (Vector2f) 乘以 3x3 矩阵 (Matrix3f)。
|
||||
*
|
||||
* @param v 要变换的向量 (局部坐标)
|
||||
* @param m 变换矩阵 (世界变换)
|
||||
* @return 变换后的新向量 (世界坐标)
|
||||
*/
|
||||
private Vector2f transformVector2f(Vector2f v, Matrix3f m) {
|
||||
float x = v.x;
|
||||
float y = v.y;
|
||||
|
||||
// 计算新的 x' 和 y'
|
||||
// x' = m00*x + m01*y + m02
|
||||
// y' = m10*x + m11*y + m12
|
||||
float newX = m.m00() * x + m.m01() * y + m.m02();
|
||||
float newY = m.m10() * x + m.m11() * y + m.m12();
|
||||
|
||||
// 直接修改原向量并返回
|
||||
return v.set(newX, newY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算指定网格在世界坐标系下的边界框。
|
||||
*/
|
||||
private BoundingBox getWorldBounds(Mesh2D mesh) {
|
||||
ModelPart part = findPartByMesh(mesh);
|
||||
if (part == null) {
|
||||
mesh.updateBounds(); // 至少保证局部边界框是新的
|
||||
return mesh.getBounds();
|
||||
}
|
||||
|
||||
// 确保 ModelPart 的世界变换是最新的
|
||||
Matrix3f worldTransform = part.getWorldTransform();
|
||||
|
||||
// 获取局部边界框
|
||||
mesh.updateBounds();
|
||||
BoundingBox localBounds = mesh.getBounds();
|
||||
|
||||
// 转换四个角到世界坐标
|
||||
Vector2f min = new Vector2f(localBounds.getMinX(), localBounds.getMinY());
|
||||
Vector2f max = new Vector2f(localBounds.getMaxX(), localBounds.getMaxY());
|
||||
Vector2f p1 = new Vector2f(min.x, max.y); // 左上
|
||||
Vector2f p2 = new Vector2f(max.x, min.y); // 右下
|
||||
|
||||
// 应用世界变换:使用手动实现的 transformVector2f 方法
|
||||
transformVector2f(min, worldTransform);
|
||||
transformVector2f(max, worldTransform);
|
||||
transformVector2f(p1, worldTransform);
|
||||
transformVector2f(p2, worldTransform);
|
||||
|
||||
// 创建新的世界边界框并扩展
|
||||
BoundingBox worldBounds = new BoundingBox();
|
||||
worldBounds.expand(min.x, min.y);
|
||||
worldBounds.expand(max.x, max.y);
|
||||
worldBounds.expand(p1.x, p1.y);
|
||||
worldBounds.expand(p2.x, p2.y);
|
||||
|
||||
return worldBounds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查是否点击了选择框的调整手柄
|
||||
*/
|
||||
private DragMode checkResizeHandleHit(float modelX, float modelY, Mesh2D targetMesh) {
|
||||
if (targetMesh == null) return DragMode.NONE;
|
||||
if (targetMesh == null) {
|
||||
return DragMode.NONE;
|
||||
}
|
||||
|
||||
BoundingBox bounds;
|
||||
Vector2f center;
|
||||
|
||||
// 在多选状态下使用多选边界框
|
||||
// 多选状态下使用多选边界框
|
||||
if (targetMesh.isInMultiSelection()) {
|
||||
bounds = targetMesh.getMultiSelectionBounds();
|
||||
center = bounds.getCenter();
|
||||
} else {
|
||||
targetMesh.updateBounds();
|
||||
bounds = targetMesh.getBounds();
|
||||
center = targetMesh.getPivot();
|
||||
}
|
||||
|
||||
// 获取摄像机偏移
|
||||
Vector2f camOffset = ModelRender.getCameraOffset();
|
||||
|
||||
// 应用偏移,将 model 坐标转换到相对于摄像机的坐标系
|
||||
float checkX = modelX - camOffset.x;
|
||||
float checkY = modelY - camOffset.y;
|
||||
|
||||
// 将中心点也转换到相同坐标系
|
||||
center = new Vector2f(center).sub(camOffset);
|
||||
|
||||
float scaleFactor = calculateScaleFactor();
|
||||
float borderThickness = BORDER_THICKNESS / scaleFactor;
|
||||
float cornerSize = CORNER_SIZE / scaleFactor;
|
||||
|
||||
float minX = bounds.getMinX();
|
||||
float minY = bounds.getMinY();
|
||||
float maxX = bounds.getMaxX();
|
||||
float maxY = bounds.getMaxY();
|
||||
|
||||
// 动态计算检测阈值,基于面板缩放比例
|
||||
float scaleFactor = calculateScaleFactor();
|
||||
float borderThickness = BORDER_THICKNESS / scaleFactor;
|
||||
float cornerSize = CORNER_SIZE / scaleFactor;
|
||||
DragMode result = DragMode.NONE;
|
||||
|
||||
// 首先检查是否点击了中心点(移动中心点)
|
||||
if (isPointInCenterHandle(modelX, modelY, center.x, center.y, cornerSize)) {
|
||||
return DragMode.MOVE_PIVOT;
|
||||
// 检查中心点
|
||||
if (isPointInCenterHandle(checkX, checkY, center.x, center.y, cornerSize)) {
|
||||
result = DragMode.MOVE_PIVOT;
|
||||
}
|
||||
|
||||
// 检查是否点击了旋转手柄
|
||||
if (isPointInRotationHandle(modelX, modelY, center.x, center.y, minY, cornerSize)) {
|
||||
return DragMode.ROTATE;
|
||||
// 检查旋转手柄
|
||||
if (result == DragMode.NONE && isPointInRotationHandle(checkX, checkY, center.x, center.y, minY, cornerSize)) {
|
||||
result = DragMode.ROTATE;
|
||||
}
|
||||
|
||||
// 扩展边界以包含调整手柄区域
|
||||
@@ -967,31 +1261,44 @@ public class ModelRenderPanel extends JPanel {
|
||||
float expandedMaxX = maxX + borderThickness;
|
||||
float expandedMaxY = maxY + borderThickness;
|
||||
|
||||
// 检查是否在扩展边界内
|
||||
if (modelX < expandedMinX || modelX > expandedMaxX ||
|
||||
modelY < expandedMinY || modelY > expandedMaxY) {
|
||||
return DragMode.NONE;
|
||||
if (result == DragMode.NONE) {
|
||||
if (checkX < expandedMinX || checkX > expandedMaxX ||
|
||||
checkY < expandedMinY || checkY > expandedMaxY) {
|
||||
return DragMode.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查角点
|
||||
if (isPointInCorner(modelX, modelY, minX, minY, cornerSize)) return DragMode.RESIZE_TOP_LEFT;
|
||||
if (isPointInCorner(modelX, modelY, maxX, minY, cornerSize)) return DragMode.RESIZE_TOP_RIGHT;
|
||||
if (isPointInCorner(modelX, modelY, minX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_LEFT;
|
||||
if (isPointInCorner(modelX, modelY, maxX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_RIGHT;
|
||||
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, minY, cornerSize)) {
|
||||
result = DragMode.RESIZE_TOP_LEFT;
|
||||
}
|
||||
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, minY, cornerSize)) {
|
||||
result = DragMode.RESIZE_TOP_RIGHT;
|
||||
}
|
||||
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, maxY, cornerSize)) {
|
||||
result = DragMode.RESIZE_BOTTOM_LEFT;
|
||||
}
|
||||
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, maxY, cornerSize)) {
|
||||
result = DragMode.RESIZE_BOTTOM_RIGHT;
|
||||
}
|
||||
|
||||
// 检查边
|
||||
if (modelX >= minX - borderThickness && modelX <= minX + borderThickness)
|
||||
return DragMode.RESIZE_LEFT;
|
||||
if (modelX >= maxX - borderThickness && modelX <= maxX + borderThickness)
|
||||
return DragMode.RESIZE_RIGHT;
|
||||
if (modelY >= minY - borderThickness && modelY <= minY + borderThickness)
|
||||
return DragMode.RESIZE_TOP;
|
||||
if (modelY >= maxY - borderThickness && modelY <= maxY + borderThickness)
|
||||
return DragMode.RESIZE_BOTTOM;
|
||||
if (result == DragMode.NONE) {
|
||||
if (checkX >= minX - borderThickness && checkX <= minX + borderThickness) {
|
||||
result = DragMode.RESIZE_LEFT;
|
||||
} else if (checkX >= maxX - borderThickness && checkX <= maxX + borderThickness) {
|
||||
result = DragMode.RESIZE_RIGHT;
|
||||
} else if (checkY >= minY - borderThickness && checkY <= minY + borderThickness) {
|
||||
result = DragMode.RESIZE_TOP;
|
||||
} else if (checkY >= maxY - borderThickness && checkY <= maxY + borderThickness) {
|
||||
result = DragMode.RESIZE_BOTTOM;
|
||||
}
|
||||
}
|
||||
|
||||
return DragMode.NONE;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查点是否在中心点区域内
|
||||
*/
|
||||
@@ -1045,6 +1352,34 @@ public class ModelRenderPanel extends JPanel {
|
||||
* 处理鼠标拖拽事件
|
||||
*/
|
||||
private void handleMouseDragged(MouseEvent e) {
|
||||
if (cameraDragging) {
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
// 计算鼠标移动距离
|
||||
final int deltaX = screenX - lastCameraDragX;
|
||||
final int deltaY = screenY - lastCameraDragY;
|
||||
|
||||
// 更新最后拖拽位置
|
||||
lastCameraDragX = screenX;
|
||||
lastCameraDragY = screenY;
|
||||
|
||||
// 确保在 GL 上下文线程中执行摄像机移动
|
||||
executeInGLContext(() -> {
|
||||
try {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float zoom = camera.getZoom();
|
||||
// 计算世界坐标的移动量(反向移动)
|
||||
float worldDeltaX = -deltaX / zoom;
|
||||
float worldDeltaY = deltaY / zoom; // AWT/Swing 的 Y 轴与 OpenGL 相反
|
||||
|
||||
// 应用摄像机移动
|
||||
camera.move(worldDeltaX, worldDeltaY);
|
||||
} catch (Exception ex) {
|
||||
logger.error("处理摄像机拖拽时出错", ex);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (currentDragMode == DragMode.NONE) return;
|
||||
|
||||
final int screenX = e.getX();
|
||||
@@ -1241,15 +1576,16 @@ public class ModelRenderPanel extends JPanel {
|
||||
/**
|
||||
* 更新所有选中网格的多选边界框
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
private void updateMultiSelectionBoundsForSelectedMeshes() {
|
||||
if (selectedMeshes.size() <= 1) return;
|
||||
|
||||
for (Mesh2D mesh : selectedMeshes) {
|
||||
if (mesh.isInMultiSelection()) {
|
||||
mesh.updateBounds();
|
||||
mesh.forceUpdateMultiSelectionBounds();
|
||||
}
|
||||
}
|
||||
//if (selectedMeshes.size() <= 1) return;
|
||||
//
|
||||
//for (Mesh2D mesh : selectedMeshes) {
|
||||
// if (mesh.isInMultiSelection()) {
|
||||
// mesh.updateBounds();
|
||||
// mesh.forceUpdateMultiSelectionBounds();
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
@@ -1257,37 +1593,43 @@ public class ModelRenderPanel extends JPanel {
|
||||
* 处理鼠标释放事件(结束拖拽并记录操作历史)
|
||||
*/
|
||||
private void handleMouseReleased(MouseEvent e) {
|
||||
if (cameraDragging && SwingUtilities.isMiddleMouseButton(e)) {
|
||||
cameraDragging = false;
|
||||
// 恢复悬停状态的光标
|
||||
updateCursorForHoverState();
|
||||
return;
|
||||
}
|
||||
if (currentDragMode != DragMode.NONE) {
|
||||
// 记录操作历史
|
||||
//executeInGLContext(() -> {
|
||||
|
||||
try {
|
||||
List<ModelPart> selectedParts = getSelectedParts();
|
||||
switch (currentDragMode) {
|
||||
case MOVE:
|
||||
if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordDragEnd(selectedParts, new HashMap<>(dragStartPositions));
|
||||
}
|
||||
break;
|
||||
case ROTATE:
|
||||
if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations));
|
||||
}
|
||||
break;
|
||||
case MOVE_PIVOT:
|
||||
if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordResizeEnd(selectedParts, new HashMap<>(dragStartScales));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("记录操作历史时出错", ex);
|
||||
try {
|
||||
List<ModelPart> selectedParts = getSelectedParts();
|
||||
switch (currentDragMode) {
|
||||
case MOVE:
|
||||
if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordDragEnd(selectedParts, new HashMap<>(dragStartPositions));
|
||||
}
|
||||
break;
|
||||
case ROTATE:
|
||||
if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations));
|
||||
}
|
||||
break;
|
||||
case MOVE_PIVOT:
|
||||
if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) {
|
||||
recordResizeEnd(selectedParts, new HashMap<>(dragStartScales));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("记录操作历史时出错", ex);
|
||||
}
|
||||
//});
|
||||
}
|
||||
|
||||
@@ -1302,6 +1644,9 @@ public class ModelRenderPanel extends JPanel {
|
||||
dragStartScales.clear();
|
||||
dragStartRotations.clear();
|
||||
dragStartPivots.clear();
|
||||
|
||||
// 恢复悬停状态的光标
|
||||
updateCursorForHoverState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1390,6 +1735,10 @@ public class ModelRenderPanel extends JPanel {
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
|
||||
if (cameraDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 在 GL 上下文线程中执行悬停检测
|
||||
executeInGLContext(() -> {
|
||||
try {
|
||||
@@ -1417,45 +1766,116 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新鼠标手势
|
||||
updateCursorForHoverState(modelX, modelY, newHoveredMesh);
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.error("处理鼠标移动时出错", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据悬停状态更新光标(无坐标版本,用于鼠标释放后)
|
||||
*/
|
||||
private void updateCursorForHoverState() {
|
||||
Point mousePos = getMousePosition();
|
||||
if (mousePos != null) {
|
||||
float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y);
|
||||
if (modelCoords != null) {
|
||||
updateCursorForHoverState(modelCoords[0], modelCoords[1], hoveredMesh);
|
||||
}
|
||||
} else {
|
||||
// 鼠标不在面板内,恢复默认光标
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据悬停状态更新光标
|
||||
*/
|
||||
private void updateCursorForHoverState(float modelX, float modelY, Mesh2D hoveredMesh) {
|
||||
// 如果正在拖拽,不更新光标
|
||||
if (currentDragMode != DragMode.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor newCursor = Cursor.getDefaultCursor();
|
||||
isOverSelection = false;
|
||||
|
||||
// 检查是否在选中的网格上
|
||||
if (!selectedMeshes.isEmpty()) {
|
||||
// 多选时只对最后一个选中的网格进行操作
|
||||
Mesh2D targetMeshForHandle = lastSelectedMesh;
|
||||
if (targetMeshForHandle != null) {
|
||||
DragMode hoverMode = checkResizeHandleHit(modelX, modelY, targetMeshForHandle);
|
||||
if (hoverMode != DragMode.NONE) {
|
||||
newCursor = getCursorForDragMode(hoverMode);
|
||||
isOverSelection = true;
|
||||
} else {
|
||||
// 检查是否在选中网格的边界框内(但不是调整手柄)
|
||||
BoundingBox bounds;
|
||||
if (targetMeshForHandle.isInMultiSelection()) {
|
||||
bounds = targetMeshForHandle.getMultiSelectionBounds();
|
||||
} else {
|
||||
bounds = targetMeshForHandle.getBounds();
|
||||
}
|
||||
|
||||
if (modelX >= bounds.getMinX() && modelX <= bounds.getMaxX() &&
|
||||
modelY >= bounds.getMinY() && modelY <= bounds.getMaxY()) {
|
||||
newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
|
||||
isOverSelection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有在选中的网格上,检查是否在可悬停的网格上
|
||||
if (!isOverSelection && hoveredMesh != null) {
|
||||
newCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||
}
|
||||
|
||||
// 更新光标
|
||||
if (!newCursor.equals(currentCursor)) {
|
||||
currentCursor = newCursor;
|
||||
Cursor finalNewCursor = newCursor;
|
||||
SwingUtilities.invokeLater(() -> setCursor(finalNewCursor));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将屏幕坐标转换为模型坐标
|
||||
*/
|
||||
private float[] screenToModelCoordinates(int screenX, int screenY) {
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
public float[] screenToModelCoordinates(int screenX, int screenY) {
|
||||
if (!contextInitialized || this.width <= 0 || this.height <= 0) return null;
|
||||
|
||||
int panelWidth = getWidth();
|
||||
int panelHeight = getHeight();
|
||||
if (panelWidth <= 0 || panelHeight <= 0) return null;
|
||||
// 1. 将 Swing 坐标缩放到 GL 上下文坐标
|
||||
float glX = (float) screenX * this.width / getWidth();
|
||||
float glY = (float) screenY * this.height / getHeight();
|
||||
|
||||
// 1. 屏幕坐标转换为离屏缓冲坐标
|
||||
float scaleX = (float) width / panelWidth;
|
||||
float scaleY = (float) height / panelHeight;
|
||||
float bufferX = screenX * scaleX;
|
||||
float bufferY = screenY * scaleY; // Y轴不反转
|
||||
// 2. 转换为归一化设备坐标 (NDC)
|
||||
// NDC 范围 [-1, 1]。GL 坐标 (0, 0) -> NDC (-1, 1) [左上角]
|
||||
// 这里的 NDC 转换是基于 GL 上下文的尺寸 (this.width, this.height)
|
||||
float ndcX = (2.0f * glX) / this.width - 1.0f;
|
||||
// AWT/Swing 的 Y 轴向下,OpenGL 的 Y 轴向上,所以需要翻转 Y 轴
|
||||
float ndcY = 1.0f - (2.0f * glY) / this.height;
|
||||
|
||||
// 2. 缓冲坐标转换为标准化设备坐标 (NDC)
|
||||
float ndcX = (bufferX / width) * 2.0f - 1.0f;
|
||||
float ndcY = (bufferY / height) * 2.0f - 1.0f;
|
||||
// 3. 逆投影变换
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float zoom = camera.getZoom();
|
||||
Vector2f pos = camera.getPosition();
|
||||
|
||||
// 3. NDC 转换为模型坐标(考虑当前显示缩放)
|
||||
float modelX = ndcX * (width / 2.0f) / displayScale;
|
||||
float modelY = ndcY * (height / 2.0f) / displayScale;
|
||||
// 逆变换公式:
|
||||
// modelX = (ndcX / (2.0f / this.width)) / zoom + pos.x
|
||||
// modelY = (ndcY / (-2.0f / this.height)) / zoom + pos.y
|
||||
|
||||
float[] result = new float[]{modelX, modelY};
|
||||
float modelX = (ndcX * this.width / (2.0f * zoom)) + pos.x;
|
||||
float modelY = (ndcY * this.height / (-2.0f * zoom)) + pos.y;
|
||||
|
||||
// 调试日志
|
||||
logger.debug("坐标转换: 屏幕({}, {}) -> 缓冲({}, {}) -> NDC({}, {}) -> 模型({}, {}), 缩放: {}",
|
||||
screenX, screenY, bufferX, bufferY, ndcX, ndcY, modelX, modelY, displayScale);
|
||||
|
||||
return result;
|
||||
return new float[]{modelX, modelY};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在指定位置查找网格
|
||||
*/
|
||||
@@ -1467,40 +1887,54 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 getParts() 获取所有部件
|
||||
// 获取摄像机偏移
|
||||
Vector2f camOffset = ModelRender.getCameraOffset();
|
||||
|
||||
// 将输入坐标调整到相对于摄像机的坐标系
|
||||
float checkX = modelX - camOffset.x;
|
||||
float checkY = modelY - camOffset.y;
|
||||
|
||||
java.util.List<ModelPart> parts = model.getParts();
|
||||
if (parts == null || parts.isEmpty()) {
|
||||
logger.debug("模型没有部件列表");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 遍历所有部件和网格(从上到下)
|
||||
for (int i = parts.size() - 1; i >= 0; i--) {
|
||||
ModelPart part = parts.get(i);
|
||||
if (part != null && part.isVisible()) {
|
||||
java.util.List<Mesh2D> meshes = part.getMeshes();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh != null && mesh.isVisible()) {
|
||||
if (mesh.isDirty()) {
|
||||
mesh.updateBounds();
|
||||
}
|
||||
boolean contains = mesh.containsPoint(modelX, modelY);
|
||||
if (contains) {
|
||||
//logger.info("选中网格: {} (在部件 {})", mesh.getName(), part.getName());
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
if (part == null || !part.isVisible()) continue;
|
||||
|
||||
java.util.List<Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes == null || meshes.isEmpty()) continue;
|
||||
|
||||
for (int m = meshes.size() - 1; m >= 0; m--) {
|
||||
Mesh2D mesh = meshes.get(m);
|
||||
if (mesh == null || !mesh.isVisible()) continue;
|
||||
|
||||
if (mesh.isDirty()) {
|
||||
mesh.updateBounds();
|
||||
}
|
||||
|
||||
boolean contains = false;
|
||||
try {
|
||||
contains = mesh.containsPoint(checkX, checkY);
|
||||
} catch (Exception ex) {
|
||||
logger.warn("mesh.containsPoint 抛出异常: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
if (contains) {
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//logger.debug("未找到包含点的网格");
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("检测网格时出错", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型的边界框
|
||||
*/
|
||||
@@ -2013,7 +2447,7 @@ public class ModelRenderPanel extends JPanel {
|
||||
*/
|
||||
public void dispose() {
|
||||
running = false;
|
||||
|
||||
cameraDragging = false;
|
||||
// 停止任务执行器
|
||||
taskExecutor.shutdown();
|
||||
|
||||
|
||||
@@ -714,7 +714,7 @@ public class ModelPart {
|
||||
/**
|
||||
* 立即重新计算本节点的 worldTransform(并递归到子节点)
|
||||
*/
|
||||
private void recomputeWorldTransformRecursive() {
|
||||
public void recomputeWorldTransformRecursive() {
|
||||
if (transformDirty) {
|
||||
updateLocalTransform();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.systems.MultiSelectionBoxRenderer;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
@@ -417,17 +418,17 @@ public class Mesh2D {
|
||||
* 检查点是否在网格内(可选择精确检测)
|
||||
*/
|
||||
public boolean containsPoint(float x, float y, boolean precise) {
|
||||
if (isInMultiSelection()) {
|
||||
BoundingBox multiBounds = getMultiSelectionBounds();
|
||||
boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() &&
|
||||
y >= multiBounds.getMinY() && y <= multiBounds.getMaxY();
|
||||
|
||||
if (precise && inBounds) {
|
||||
// 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内
|
||||
return isPointInAnySelectedMesh(x, y);
|
||||
}
|
||||
return inBounds;
|
||||
}
|
||||
//if (isInMultiSelection()) {
|
||||
// BoundingBox multiBounds = getMultiSelectionBounds();
|
||||
// boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() &&
|
||||
// y >= multiBounds.getMinY() && y <= multiBounds.getMaxY();
|
||||
//
|
||||
// if (precise && inBounds) {
|
||||
// // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内
|
||||
// return isPointInAnySelectedMesh(x, y);
|
||||
// }
|
||||
// return inBounds;
|
||||
//}
|
||||
|
||||
BoundingBox b = getBounds();
|
||||
boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY();
|
||||
@@ -757,60 +758,9 @@ public class Mesh2D {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSelectBox(){
|
||||
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;
|
||||
|
||||
// 第1层:外发光边框
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
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);
|
||||
|
||||
// 新增:绘制中心点
|
||||
drawCenterPoint(bb, minX, minY, maxX, maxY);
|
||||
drawRotationHandle(bb, minX, minY, maxX, maxY);
|
||||
MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -922,25 +872,8 @@ public class Mesh2D {
|
||||
* 在多选状态下绘制组合边界框
|
||||
*/
|
||||
private void drawMultiSelectionBox() {
|
||||
if (!isInMultiSelection()) {
|
||||
drawSelectBox();
|
||||
return;
|
||||
}
|
||||
BoundingBox multiBounds = getMultiSelectionBounds();
|
||||
if (!multiBounds.isValid()) return;
|
||||
float minX = multiBounds.getMinX();
|
||||
float minY = multiBounds.getMinY();
|
||||
float maxX = multiBounds.getMaxX();
|
||||
float maxY = multiBounds.getMaxY();
|
||||
BufferBuilder bb = new BufferBuilder();
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
drawDashedBorder(bb, minX, minY, maxX, maxY);
|
||||
final float CORNER_SIZE = 8.0f;
|
||||
final float BORDER_THICKNESS = 6.0f;
|
||||
drawMultiSelectionResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS);
|
||||
drawMultiSelectionCenterPoint(bb, minX, minY, maxX, maxY);
|
||||
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
|
||||
MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -959,11 +892,7 @@ public class Mesh2D {
|
||||
|
||||
// 绘制右边虚线
|
||||
drawDashedLine(bb, maxX, minY, maxX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
|
||||
|
||||
// 绘制下边虚线
|
||||
drawDashedLine(bb, maxX, maxY, minX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
|
||||
|
||||
// 绘制左边虚线
|
||||
drawDashedLine(bb, minX, maxY, minX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.chuangzhou.vivid2D.render.systems;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
/**
|
||||
* 摄像机类
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Camera {
|
||||
private final Vector2f position = new Vector2f(0.0f, 0.0f);
|
||||
private float zoom = 1.0f;
|
||||
private float zPosition = 0.0f; // Z轴位置,影响深度
|
||||
private boolean enabled = true;
|
||||
|
||||
public Camera() {}
|
||||
|
||||
public void setPosition(float x, float y) {
|
||||
position.set(x, y);
|
||||
}
|
||||
|
||||
public void setPosition(Vector2f pos) {
|
||||
position.set(pos);
|
||||
}
|
||||
|
||||
public Vector2f getPosition() {
|
||||
return new Vector2f(position);
|
||||
}
|
||||
|
||||
public void setZoom(float zoom) {
|
||||
this.zoom = Math.max(0.1f, Math.min(10.0f, zoom));
|
||||
}
|
||||
|
||||
public float getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void setZPosition(float z) {
|
||||
this.zPosition = z;
|
||||
}
|
||||
|
||||
public float getZPosition() {
|
||||
return zPosition;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void move(float dx, float dy) {
|
||||
position.add(dx, dy);
|
||||
}
|
||||
|
||||
public void zoom(float factor) {
|
||||
zoom *= factor;
|
||||
zoom = Math.max(0.1f, Math.min(10.0f, zoom));
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
position.set(0.0f, 0.0f);
|
||||
zoom = 1.0f;
|
||||
zPosition = 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package com.chuangzhou.vivid2D.render.systems;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
* 现代化选择框渲染器(性能优化版)
|
||||
* 主要优化点:
|
||||
* 1) 复用 Tesselator 单例 BufferBuilder,减少频繁的 GPU 资源创建/销毁
|
||||
* 2) 批量提交顶点:把同一 primitive(LINES / TRIANGLES / LINE_LOOP)与同一颜色的顶点尽量合并到一次 begin/end
|
||||
* 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效
|
||||
* 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观
|
||||
*
|
||||
* 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。
|
||||
*/
|
||||
public class MultiSelectionBoxRenderer {
|
||||
|
||||
// 常量定义(视觉可调)
|
||||
public static final float DEFAULT_CORNER_SIZE = 10.0f;
|
||||
public static final float DEFAULT_BORDER_THICKNESS = 6.0f;
|
||||
public static final float DEFAULT_DASH_LENGTH = 10.0f;
|
||||
public static final float DEFAULT_GAP_LENGTH = 6.0f;
|
||||
public static final float ROTATION_HANDLE_DISTANCE = 28.0f;
|
||||
public static final float HANDLE_ROUNDNESS = 1.5f; // 保留,用于未来改进圆角手柄
|
||||
|
||||
// 颜色(更现代的配色)
|
||||
public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线
|
||||
public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.18f); // 轻微外发光
|
||||
public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框,青色
|
||||
public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内边框,接近白
|
||||
public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白
|
||||
public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); // 黄色手柄
|
||||
public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红
|
||||
public static final Vector4f ROTATION_HANDLE_COLOR = new Vector4f(0.14f, 0.95f, 0.3f, 1.0f); // 绿色旋转手柄
|
||||
public static final Vector4f SHADOW_COLOR = new Vector4f(0f, 0f, 0f, 0.18f); // 阴影/背板
|
||||
|
||||
/**
|
||||
* 绘制单选状态下的选择框(高效批处理)
|
||||
*/
|
||||
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
|
||||
if (!bounds.isValid()) return;
|
||||
|
||||
float minX = bounds.getMinX();
|
||||
float minY = bounds.getMinY();
|
||||
float maxX = bounds.getMaxX();
|
||||
float maxY = bounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// 1) 阴影底板(轻微偏移)
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6);
|
||||
bb.setColor(SHADOW_COLOR);
|
||||
addFilledQuadTriangles(bb, minX + 4f, minY + 4f, maxX + 4f, maxY + 4f);
|
||||
tesselator.end();
|
||||
|
||||
// 2) 外发光边框(更柔和)
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_OUTER);
|
||||
bb.vertex(minX - 6.0f, minY - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 6.0f, minY - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 6.0f, maxY + 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(minX - 6.0f, maxY + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 3) 主边框 + 内边框(两个 LINE_LOOP)
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_MAIN);
|
||||
bb.vertex(minX - 1.0f, minY - 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 1.0f, minY - 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 1.0f, maxY + 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(minX - 1.0f, maxY + 1.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_INNER);
|
||||
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);
|
||||
tesselator.end();
|
||||
|
||||
// 4) 手柄(一次性 TRIANGLES 批次绘制所有手柄)
|
||||
// 8 个手柄(四角 + 四边中点),每个 6 个顶点
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
|
||||
bb.setColor(HANDLE_COLOR);
|
||||
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); // 左上
|
||||
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); // 右上
|
||||
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); // 左下
|
||||
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); // 右下
|
||||
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); // 上中
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); // 下中
|
||||
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 左中
|
||||
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 右中
|
||||
tesselator.end();
|
||||
|
||||
// 5) 中心点(十字 + 圆环)
|
||||
// 十字:LINES
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
bb.vertex(pivot.x - 6.0f, pivot.y, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + 6.0f, pivot.y, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, pivot.y - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, pivot.y + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 圆环:LINE_LOOP
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
float radius = 6.0f * 0.85f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(pivot.x + (float) Math.cos(angle) * radius, pivot.y + (float) Math.sin(angle) * radius, 0.0f, 0.0f);
|
||||
}
|
||||
tesselator.end();
|
||||
|
||||
// 6) 旋转手柄(连线 + 圆 + 箭头),分三次提交但数量小
|
||||
float topY = bounds.getMinY();
|
||||
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
|
||||
|
||||
// 连线
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
bb.vertex(pivot.x, topY, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, rotationHandleY, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 圆
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float handleRadius = 6.0f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(pivot.x + (float) Math.cos(angle) * handleRadius, rotationHandleY + (float) Math.sin(angle) * handleRadius, 0.0f, 0.0f);
|
||||
}
|
||||
tesselator.end();
|
||||
|
||||
// 箭头(两条交叉线,提示旋转)
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float arrow = 4.0f;
|
||||
bb.vertex(pivot.x - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制多选框(现代化外观,批量提交)
|
||||
*/
|
||||
public static void drawMultiSelectionBox(BoundingBox multiBounds) {
|
||||
if (!multiBounds.isValid()) return;
|
||||
|
||||
float minX = multiBounds.getMinX();
|
||||
float minY = multiBounds.getMinY();
|
||||
float maxX = multiBounds.getMaxX();
|
||||
float maxY = multiBounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次
|
||||
int estimatedSegments = Math.max(4,
|
||||
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
|
||||
bb.begin(GL11.GL_LINES, estimatedSegments * 2);
|
||||
bb.setColor(DASHED_BORDER_COLOR);
|
||||
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
tesselator.end();
|
||||
|
||||
// 手柄(一次性 TRIANGLES 批次)
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
|
||||
bb.setColor(MULTI_SELECTION_HANDLE_COLOR);
|
||||
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
|
||||
tesselator.end();
|
||||
|
||||
// 中心点
|
||||
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
bb.vertex(center.x - 6.0f, center.y, 0.0f, 0.0f);
|
||||
bb.vertex(center.x + 6.0f, center.y, 0.0f, 0.0f);
|
||||
bb.vertex(center.x, center.y - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(center.x, center.y + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 旋转手柄(沿用单选逻辑)
|
||||
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
// ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder) ------
|
||||
|
||||
// 向当前 TRIANGLES 批次添加一个填充矩形(两三角形)
|
||||
private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) {
|
||||
// 三角形 1
|
||||
bb.vertex(x0, y0, 0.0f, 0.0f);
|
||||
bb.vertex(x1, y0, 0.0f, 0.0f);
|
||||
bb.vertex(x1, y1, 0.0f, 0.0f);
|
||||
// 三角形 2
|
||||
bb.vertex(x1, y1, 0.0f, 0.0f);
|
||||
bb.vertex(x0, y1, 0.0f, 0.0f);
|
||||
bb.vertex(x0, y0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// 向当前 TRIANGLES 批次添加一个手柄方块(中心在 cx,cy,边长 size)
|
||||
private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) {
|
||||
float half = size / 2f;
|
||||
addFilledQuadTriangles(bb, cx - half, cy - half, cx + half, cy + half);
|
||||
}
|
||||
|
||||
// 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin())
|
||||
private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY,
|
||||
float dashLen, float gapLen) {
|
||||
float dx = endX - startX;
|
||||
float dy = endY - startY;
|
||||
float len = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 0.001f) return;
|
||||
float dirX = dx / len, dirY = dy / len;
|
||||
float seg = dashLen + gapLen;
|
||||
int count = (int) Math.ceil(len / seg);
|
||||
for (int i = 0; i < count; i++) {
|
||||
float s = i * seg;
|
||||
if (s >= len) break;
|
||||
float e = Math.min(s + dashLen, len);
|
||||
float sx = startX + dirX * s;
|
||||
float sy = startY + dirY * s;
|
||||
float ex = startX + dirX * e;
|
||||
float ey = startY + dirY * e;
|
||||
bb.vertex(sx, sy, 0.0f, 0.0f);
|
||||
bb.vertex(ex, ey, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配:在 multi selection 中把旋转手柄渲染写入到传入的 bb(会在函数内部使用 tesselator.end())
|
||||
public static void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
|
||||
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
|
||||
drawRotationHandle(center, new BoundingBox(minX, minY, maxX, maxY));
|
||||
}
|
||||
|
||||
// 单独绘制旋转手柄(内部会 new / begin / end,因为包含多种 primitive)
|
||||
private static void drawRotationHandle(Vector2f pivot, BoundingBox bounds) {
|
||||
float centerX = pivot.x;
|
||||
float centerY = pivot.y;
|
||||
float topY = bounds.getMinY();
|
||||
|
||||
boolean pivotInBounds = (centerX >= bounds.getMinX() && centerX <= bounds.getMaxX() &&
|
||||
centerY >= bounds.getMinY() && centerY <= bounds.getMaxY());
|
||||
if (!pivotInBounds) {
|
||||
centerX = (bounds.getMinX() + bounds.getMaxX()) * 0.5f;
|
||||
centerY = (bounds.getMinY() + bounds.getMaxY()) * 0.5f;
|
||||
topY = bounds.getMinY();
|
||||
}
|
||||
|
||||
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
// 连线
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
bb.vertex(centerX, topY, 0.0f, 0.0f);
|
||||
bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f);
|
||||
t.end();
|
||||
|
||||
// 圆环
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float r = 6.0f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float ang = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(centerX + (float) Math.cos(ang) * r, rotationHandleY + (float) Math.sin(ang) * r, 0.0f, 0.0f);
|
||||
}
|
||||
t.end();
|
||||
|
||||
// 箭头
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float arrow = 4.0f;
|
||||
bb.vertex(centerX - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
t.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅绘制简化的多选虚线边框(保留单次批量绘制)
|
||||
*/
|
||||
public static void drawSimpleMultiSelectionBox(BoundingBox multiBounds) {
|
||||
if (!multiBounds.isValid()) return;
|
||||
|
||||
float minX = multiBounds.getMinX();
|
||||
float minY = multiBounds.getMinY();
|
||||
float maxX = multiBounds.getMaxX();
|
||||
float maxY = multiBounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
int est = Math.max(4,
|
||||
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
|
||||
bb.begin(GL11.GL_LINES, est * 2);
|
||||
bb.setColor(DASHED_BORDER_COLOR);
|
||||
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
tesselator.end();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,12 @@ import org.lwjgl.opengl.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.function.IntSupplier;
|
||||
@@ -101,7 +107,182 @@ public final class RenderSystem {
|
||||
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;
|
||||
private static final java.util.Deque<RenderState> stateStack = new java.util.ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* 渲染状态快照类
|
||||
*/
|
||||
private static class RenderState {
|
||||
private int currentProgram;
|
||||
private boolean blendEnabled;
|
||||
private boolean depthTestEnabled;
|
||||
private int blendSrcFactor;
|
||||
private int blendDstFactor;
|
||||
private int activeTexture;
|
||||
private int boundTexture;
|
||||
private float[] clearColor;
|
||||
private int[] viewport;
|
||||
|
||||
public RenderState() {
|
||||
setDefaults();
|
||||
|
||||
try {
|
||||
this.currentProgram = getCurrentProgram();
|
||||
this.blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND);
|
||||
this.depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST);
|
||||
|
||||
java.nio.IntBuffer blendFunc = org.lwjgl.system.MemoryUtil.memAllocInt(2);
|
||||
try {
|
||||
GL11.glGetIntegerv(GL11.GL_BLEND_SRC, blendFunc);
|
||||
this.blendSrcFactor = blendFunc.get(0);
|
||||
GL11.glGetIntegerv(GL11.GL_BLEND_DST, blendFunc);
|
||||
this.blendDstFactor = blendFunc.get(0);
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(blendFunc);
|
||||
}
|
||||
|
||||
java.nio.IntBuffer intBuf = org.lwjgl.system.MemoryUtil.memAllocInt(1);
|
||||
try {
|
||||
GL11.glGetIntegerv(GL13.GL_ACTIVE_TEXTURE, intBuf);
|
||||
this.activeTexture = intBuf.get(0);
|
||||
|
||||
GL11.glGetIntegerv(GL11.GL_TEXTURE_BINDING_2D, intBuf);
|
||||
this.boundTexture = intBuf.get(0);
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(intBuf);
|
||||
}
|
||||
|
||||
java.nio.FloatBuffer floatBuf = org.lwjgl.system.MemoryUtil.memAllocFloat(4);
|
||||
try {
|
||||
GL11.glGetFloatv(GL11.GL_COLOR_CLEAR_VALUE, floatBuf);
|
||||
this.clearColor = new float[] {
|
||||
floatBuf.get(0), floatBuf.get(1),
|
||||
floatBuf.get(2), floatBuf.get(3)
|
||||
};
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(floatBuf);
|
||||
}
|
||||
|
||||
java.nio.IntBuffer viewportBuf = org.lwjgl.system.MemoryUtil.memAllocInt(4);
|
||||
try {
|
||||
GL11.glGetIntegerv(GL11.GL_VIEWPORT, viewportBuf);
|
||||
this.viewport = new int[] {
|
||||
viewportBuf.get(0), viewportBuf.get(1),
|
||||
viewportBuf.get(2), viewportBuf.get(3)
|
||||
};
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(viewportBuf);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to get render state, using defaults: {}", e.getMessage());
|
||||
// 如果出现异常,我们使用默认值(已经在setDefaults中设置,所以不需要再次设置)
|
||||
}
|
||||
}
|
||||
|
||||
private void setDefaults() {
|
||||
this.currentProgram = 0;
|
||||
this.blendEnabled = false;
|
||||
this.depthTestEnabled = false;
|
||||
this.blendSrcFactor = GL11.GL_SRC_ALPHA;
|
||||
this.blendDstFactor = GL11.GL_ONE_MINUS_SRC_ALPHA;
|
||||
this.activeTexture = GL13.GL_TEXTURE0;
|
||||
this.boundTexture = 0;
|
||||
this.clearColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
this.viewport = new int[] {0, 0, viewportWidth, viewportHeight};
|
||||
}
|
||||
|
||||
public void restore() {
|
||||
try {
|
||||
// 恢复视口
|
||||
if (viewport != null && viewport.length == 4) {
|
||||
GL11.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
}
|
||||
|
||||
// 恢复清除颜色
|
||||
if (clearColor != null && clearColor.length == 4) {
|
||||
GL11.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
|
||||
}
|
||||
|
||||
// 恢复着色器程序
|
||||
if (GL20.glIsProgram(currentProgram)) {
|
||||
GL20.glUseProgram(currentProgram);
|
||||
} else {
|
||||
GL20.glUseProgram(0);
|
||||
}
|
||||
|
||||
// 恢复纹理状态 - 使用更安全的方式
|
||||
if (activeTexture >= GL13.GL_TEXTURE0 && activeTexture <= GL13.GL_TEXTURE31) {
|
||||
GL13.glActiveTexture(activeTexture);
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture);
|
||||
} else {
|
||||
// 使用默认纹理单元
|
||||
GL13.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture);
|
||||
}
|
||||
|
||||
// 恢复混合状态
|
||||
if (blendEnabled) {
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
} else {
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
}
|
||||
|
||||
// 使用安全的混合函数值
|
||||
if (isValidBlendFunc(blendSrcFactor) && isValidBlendFunc(blendDstFactor)) {
|
||||
GL11.glBlendFunc(blendSrcFactor, blendDstFactor);
|
||||
} else {
|
||||
// 使用默认混合函数
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
// 恢复深度测试状态
|
||||
if (depthTestEnabled) {
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
} else {
|
||||
GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during state restoration: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查混合函数值是否有效
|
||||
*/
|
||||
private boolean isValidBlendFunc(int func) {
|
||||
switch (func) {
|
||||
case GL11.GL_ZERO:
|
||||
case GL11.GL_ONE:
|
||||
case GL11.GL_SRC_COLOR:
|
||||
case GL11.GL_ONE_MINUS_SRC_COLOR:
|
||||
case GL11.GL_DST_COLOR:
|
||||
case GL11.GL_ONE_MINUS_DST_COLOR:
|
||||
case GL11.GL_SRC_ALPHA:
|
||||
case GL11.GL_ONE_MINUS_SRC_ALPHA:
|
||||
case GL11.GL_DST_ALPHA:
|
||||
case GL11.GL_ONE_MINUS_DST_ALPHA:
|
||||
case GL14.GL_SRC_ALPHA_SATURATE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RenderState{" +
|
||||
"currentProgram=" + currentProgram +
|
||||
", blendEnabled=" + blendEnabled +
|
||||
", depthTestEnabled=" + depthTestEnabled +
|
||||
", blendSrcFactor=" + blendSrcFactor +
|
||||
", blendDstFactor=" + blendDstFactor +
|
||||
", activeTexture=" + activeTexture +
|
||||
", boundTexture=" + boundTexture +
|
||||
", clearColor=" + Arrays.toString(clearColor) +
|
||||
", viewport=" + Arrays.toString(viewport);
|
||||
}
|
||||
}
|
||||
// ================== 初始化方法 ==================
|
||||
|
||||
public static void initRenderThread() {
|
||||
@@ -175,6 +356,52 @@ public final class RenderSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前渲染状态到栈中
|
||||
*/
|
||||
public static void pushState() {
|
||||
if (!isOnRenderThread()) {
|
||||
recordRenderCall(() -> _pushState());
|
||||
} else {
|
||||
_pushState();
|
||||
}
|
||||
}
|
||||
|
||||
private static void _pushState() {
|
||||
assertOnRenderThread();
|
||||
stateStack.push(new RenderState());
|
||||
checkGLError("pushState");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从栈中恢复之前的渲染状态
|
||||
*/
|
||||
public static void popState() {
|
||||
if (!isOnRenderThread()) {
|
||||
recordRenderCall(() -> _popState());
|
||||
} else {
|
||||
_popState();
|
||||
}
|
||||
}
|
||||
|
||||
private static void _popState() {
|
||||
assertOnRenderThread();
|
||||
if (!stateStack.isEmpty()) {
|
||||
RenderState state = stateStack.pop();
|
||||
state.restore();
|
||||
checkGLError("popState");
|
||||
} else {
|
||||
logger.warn("popState called with empty state stack");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态栈大小
|
||||
*/
|
||||
public static int getStateStackSize() {
|
||||
return stateStack.size();
|
||||
}
|
||||
|
||||
private static void _enable(int capability) {
|
||||
assertOnRenderThread();
|
||||
GL11.glEnable(capability);
|
||||
@@ -602,6 +829,18 @@ public final class RenderSystem {
|
||||
}
|
||||
}
|
||||
|
||||
public static ByteBuffer loadWindowsFont(String fontFileName) throws IOException {
|
||||
Path path = Path.of("C:/Windows/Fonts/" + fontFileName);
|
||||
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) {
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect((int) fc.size());
|
||||
while (buffer.hasRemaining()) {
|
||||
fc.read(buffer);
|
||||
}
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
// 完整的程序链接方法
|
||||
public static int linkProgram(int vertexShader, int fragmentShader) {
|
||||
assertOnRenderThread();
|
||||
@@ -914,29 +1153,52 @@ public final class RenderSystem {
|
||||
assertOnRenderThread();
|
||||
|
||||
int textureId = genTextures();
|
||||
bindTexture(textureId);
|
||||
|
||||
// 创建 1x1 白色纹理
|
||||
java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4);
|
||||
try {
|
||||
buffer.put((byte) 255)
|
||||
.put((byte) 255)
|
||||
.put((byte) 255)
|
||||
.put((byte) 255)
|
||||
.flip();
|
||||
|
||||
texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(buffer);
|
||||
if (textureId == 0) {
|
||||
logger.error("Failed to generate texture ID");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 设置纹理参数
|
||||
setTextureMinFilter(GL11.GL_NEAREST);
|
||||
setTextureMagFilter(GL11.GL_NEAREST);
|
||||
setTextureWrapS(GL11.GL_REPEAT);
|
||||
setTextureWrapT(GL11.GL_REPEAT);
|
||||
bindTexture(textureId);
|
||||
|
||||
try {
|
||||
// 创建 1x1 白色纹理 - 使用更兼容的格式
|
||||
java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4);
|
||||
try {
|
||||
// 填充 RGBA 数据:白色不透明
|
||||
buffer.put((byte) 0xFF) // R
|
||||
.put((byte) 0xFF) // G
|
||||
.put((byte) 0xFF) // B
|
||||
.put((byte) 0xFF) // A
|
||||
.flip();
|
||||
|
||||
// 使用更兼容的纹理格式组合
|
||||
// 注意:有些系统可能不支持 GL_RGBA8,使用 GL_RGBA 作为内部格式
|
||||
texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA,
|
||||
1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
|
||||
|
||||
checkGLError("texImage2D in createDefaultTexture");
|
||||
|
||||
} finally {
|
||||
org.lwjgl.system.MemoryUtil.memFree(buffer);
|
||||
}
|
||||
|
||||
// 设置纹理参数
|
||||
setTextureMinFilter(GL11.GL_NEAREST);
|
||||
setTextureMagFilter(GL11.GL_NEAREST);
|
||||
setTextureWrapS(GL12.GL_CLAMP_TO_EDGE);
|
||||
setTextureWrapT(GL12.GL_CLAMP_TO_EDGE);
|
||||
|
||||
checkGLError("texture parameters in createDefaultTexture");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error creating default texture: {}", e.getMessage());
|
||||
// 清理失败的纹理
|
||||
deleteTextures(textureId);
|
||||
return 0;
|
||||
} finally {
|
||||
bindTexture(0); // 解绑
|
||||
}
|
||||
|
||||
bindTexture(0); // 解绑
|
||||
return textureId;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 com.chuangzhou.vivid2D.render.systems.sources.def.TextShader;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
@@ -37,7 +38,8 @@ public class ShaderManagement {
|
||||
*/
|
||||
public static final List<CompleteShader> shaderList = List.of(
|
||||
new Shader2D(),
|
||||
new SolidColorShader()
|
||||
new SolidColorShader(),
|
||||
new TextShader()
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.chuangzhou.vivid2D.render.systems.sources.def;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.*;
|
||||
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/**
|
||||
* 文本着色器
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TextShader implements CompleteShader {
|
||||
|
||||
private final VertexShader vertexShader = new VertexShader();
|
||||
private final FragmentShader fragmentShader = new FragmentShader();
|
||||
private Vector4f color = new Vector4f(1,1,1,1);
|
||||
|
||||
public void setColor(Vector4f color) {
|
||||
this.color.set(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shader getVertexShader() {
|
||||
return vertexShader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shader getFragmentShader() {
|
||||
return fragmentShader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShaderName() {
|
||||
return "TextShader";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefaultShader() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultUniforms(ShaderProgram program) {
|
||||
// 传递颜色 uniform
|
||||
program.setUniform4f("uColor", color.x, color.y, color.z, color.w);
|
||||
// 纹理通常绑定到0号纹理单元
|
||||
program.setUniform1i("uTexture", 0);
|
||||
}
|
||||
|
||||
private static class VertexShader implements Shader {
|
||||
@Override
|
||||
public String getShaderCode() {
|
||||
return """
|
||||
#version 330 core
|
||||
layout(location = 0) in vec2 aPosition;
|
||||
layout(location = 1) in vec2 aTexCoord;
|
||||
|
||||
out vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vTexCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition.xy, 0.0, 1.0);
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShaderName() {
|
||||
return "TextVertexShader";
|
||||
}
|
||||
}
|
||||
|
||||
private static class FragmentShader implements Shader {
|
||||
@Override
|
||||
public String getShaderCode() {
|
||||
return """
|
||||
#version 330 core
|
||||
in vec2 vTexCoord;
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform sampler2D uTexture;
|
||||
uniform vec4 uColor;
|
||||
|
||||
void main() {
|
||||
// 使用 .r 通道读取单通道纹理
|
||||
float alpha = texture(uTexture, vTexCoord).r;
|
||||
FragColor = vec4(uColor.rgb, uColor.a * alpha);
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShaderName() {
|
||||
return "TextFragmentShader";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,15 @@ public class ModelTest {
|
||||
// Test 3: Test compressed file operations with textures
|
||||
testCompressedFileOperationsWithTexture();
|
||||
|
||||
//testModelSaveLoadIntegrity(model, "test_model.vmdl")
|
||||
|
||||
// Other existing tests...
|
||||
testAnimationSystem();
|
||||
testPhysicsSystem();
|
||||
testComplexTransformations();
|
||||
testPerformance();
|
||||
Model2D model = createTestModel();
|
||||
printModelState(model);
|
||||
//testAnimationSystem();
|
||||
//testPhysicsSystem();
|
||||
//testComplexTransformations();
|
||||
//testPerformance();
|
||||
//Model2D model = createTestModel();
|
||||
//printModelState(model);
|
||||
} finally {
|
||||
// Cleanup OpenGL
|
||||
cleanupOpenGL();
|
||||
@@ -311,7 +313,6 @@ public class ModelTest {
|
||||
System.out.println("OpenGL initialized successfully");
|
||||
System.out.println("OpenGL Version: " + org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION));
|
||||
glInitialized = true;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to initialize OpenGL: " + e.getMessage());
|
||||
// Continue without OpenGL for other tests
|
||||
@@ -581,7 +582,6 @@ public class ModelTest {
|
||||
try {
|
||||
// Load model
|
||||
Model2D model = Model2D.loadFromFile("test_character.model");
|
||||
|
||||
System.out.println("Testing animation system:");
|
||||
|
||||
// Test parameter-driven animation
|
||||
|
||||
Reference in New Issue
Block a user