feat(render): 实现中文文本渲染与悬停提示功能- 在 Mesh2D 中增加悬停状态支持,允许显示红色边框和名称标签
- 添加 splitLines 方法支持文本自动换行显示 - 重构 TextRenderer 以支持 ASCII 和中文字符混合渲染 - 增加 getTextWidth 方法用于计算文本实际渲染宽度 - 修复 RenderSystem 中字体加载方法命名一致性问题- 调整 ModelRenderPanel 中坐标转换逻辑,确保拾取准确性 - 移除冗余的 Matrix3fUtils 引用,优化包导入结构- 完善 Mesh2D 绘制流程中的程序状态管理和纹理绑定操作- 为 Mesh2D 和 ModelPart 建立双向关联,便于获取模型部件名称 - 修改摄像机偏移计算方式,提高渲染坐标一致性
This commit is contained in:
@@ -195,6 +195,7 @@ public final class ModelRender {
|
||||
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方法 ==================
|
||||
|
||||
/**
|
||||
@@ -329,15 +330,15 @@ public final class ModelRender {
|
||||
// 初始化默认字体(可替换为你自己的 TTF 数据)
|
||||
ByteBuffer fontData = null;
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("Arial.ttf");
|
||||
fontData = RenderSystem.loadFont("FZYTK.TTF");
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load Arial.ttf, trying fallback fonts", e);
|
||||
// 尝试其他字体
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("arial.ttf");
|
||||
fontData = RenderSystem.loadFont("arial.ttf");
|
||||
} catch (Exception e2) {
|
||||
try {
|
||||
fontData = RenderSystem.loadWindowsFont("times.ttf");
|
||||
fontData = RenderSystem.loadFont("times.ttf");
|
||||
} catch (Exception e3) {
|
||||
logger.error("All font loading attempts failed");
|
||||
}
|
||||
@@ -348,7 +349,7 @@ public final class ModelRender {
|
||||
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
|
||||
defaultTextRenderer.initialize(fontData, 20.0f);
|
||||
RenderSystem.checkGLError("defaultTextRenderer initialization");
|
||||
|
||||
if (!defaultTextRenderer.isInitialized()) {
|
||||
@@ -635,17 +636,17 @@ public final class ModelRender {
|
||||
RenderSystem.checkGLError("after_render_colliders");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
//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");
|
||||
}
|
||||
@@ -801,8 +802,12 @@ public final class ModelRender {
|
||||
if (!mesh.isVisible()) return;
|
||||
|
||||
// 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换)
|
||||
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix;
|
||||
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : new Matrix3f(modelMatrix);
|
||||
|
||||
// 手动应用摄像机偏移
|
||||
Vector2f offset = getCameraOffset();
|
||||
matToUse.m20(matToUse.m20() - offset.x);
|
||||
matToUse.m21(matToUse.m21() - offset.y);
|
||||
// 设置纹理相关的uniform
|
||||
if (mesh.getTexture() != null) {
|
||||
mesh.getTexture().bind(0); // 绑定到纹理单元0
|
||||
@@ -822,6 +827,7 @@ public final class ModelRender {
|
||||
RenderSystem.checkGLError("renderMesh");
|
||||
}
|
||||
|
||||
|
||||
// ================== 渲染碰撞箱相关实现 ==================
|
||||
|
||||
private static void renderPhysicsColliders(PhysicsSystem physics) {
|
||||
@@ -984,9 +990,14 @@ public final class ModelRender {
|
||||
setUniformVec4Internal(sp, "uColor", new Vector4f(1,1,1,1));
|
||||
}
|
||||
|
||||
public static TextRenderer getTextRenderer() {
|
||||
return defaultTextRenderer;
|
||||
}
|
||||
|
||||
// ================== 工具 ==================
|
||||
private static Matrix3f buildOrthoProjection(int width, int height) {
|
||||
Matrix3f m = new Matrix3f();
|
||||
// 这个投影把屏幕像素坐标(x in [0,width], y in [0,height])映射到 NDC [-1,1]x[1,-1]
|
||||
m.set(
|
||||
2.0f / width, 0.0f, -1.0f,
|
||||
0.0f, -2.0f / height, 1.0f,
|
||||
@@ -995,19 +1006,20 @@ public final class ModelRender {
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 渲染文字
|
||||
* @param text 文字内容
|
||||
* @param x 世界坐标 X
|
||||
* @param y 世界坐标 Y
|
||||
* @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;
|
||||
float px = x - offset.x;
|
||||
float py = y - offset.y;
|
||||
defaultTextRenderer.renderText(text, px, py, color);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.systems;
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import org.joml.Vector2f;
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -19,30 +18,28 @@ import java.nio.ByteBuffer;
|
||||
import static org.lwjgl.stb.STBTruetype.*;
|
||||
|
||||
/**
|
||||
* OpenGL 文字渲染器实例类
|
||||
* 支持多字体、多实例管理,每个实例维护独立的字符数据与纹理
|
||||
*
|
||||
* 支持 ASCII + 中文的 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 STBTTBakedChar.Buffer asciiCharData;
|
||||
private STBTTBakedChar.Buffer chineseCharData;
|
||||
private int asciiTextureId;
|
||||
private int chineseTextureId;
|
||||
|
||||
private boolean initialized = false;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param bitmapWidth 字符纹理宽度
|
||||
* @param bitmapHeight 字符纹理高度
|
||||
* @param firstChar 字符起始码
|
||||
* @param charCount 字符数量
|
||||
*/
|
||||
// 中文字符起始编码(选择一个不冲突的范围)
|
||||
private static final int CHINESE_FIRST_CHAR = 0x4E00; // CJK Unified Ideographs 常用汉字起始范围
|
||||
private static final int CHINESE_CHAR_COUNT = 20000;
|
||||
|
||||
public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) {
|
||||
this.bitmapWidth = bitmapWidth;
|
||||
this.bitmapHeight = bitmapHeight;
|
||||
@@ -52,108 +49,130 @@ public final class TextRenderer {
|
||||
|
||||
/**
|
||||
* 初始化字体渲染器
|
||||
*
|
||||
* @param fontData TTF 字体文件内容
|
||||
* @param fontHeight 字体像素高度
|
||||
*/
|
||||
public void initialize(ByteBuffer fontData, float fontHeight) {
|
||||
if (initialized) return;
|
||||
if (initialized) {
|
||||
logger.warn("TextRenderer already initialized");
|
||||
return;
|
||||
}
|
||||
if (fontData == null || fontData.capacity() == 0 || fontHeight <= 0) {
|
||||
logger.error("Invalid font data or font height");
|
||||
return;
|
||||
}
|
||||
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
if (shader == null) {
|
||||
logger.error("TextShader not found");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
// 烘焙 ASCII
|
||||
asciiCharData = STBTTBakedChar.malloc(charCount);
|
||||
ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight);
|
||||
int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap,
|
||||
bitmapWidth, bitmapHeight, firstChar, asciiCharData);
|
||||
if (asciiRes <= 0) {
|
||||
logger.error("ASCII font bake failed, result: {}", asciiRes);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
asciiTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, asciiBitmap);
|
||||
if (asciiTextureId == 0) {
|
||||
logger.error("Failed to create ASCII texture");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer bitmap = ByteBuffer.allocateDirect(bitmapSize);
|
||||
// 烘焙中文 - 使用更大的纹理和正确的字符范围
|
||||
int chineseTexSize = 4096; // 中文字符需要更大的纹理
|
||||
// 分配足够的空间来存储 CHINESE_CHAR_COUNT 个字符的数据
|
||||
chineseCharData = STBTTBakedChar.malloc(CHINESE_CHAR_COUNT);
|
||||
ByteBuffer chineseBitmap = ByteBuffer.allocateDirect(chineseTexSize * chineseTexSize);
|
||||
|
||||
// 烘焙字体位图
|
||||
int result = stbtt_BakeFontBitmap(fontData, fontHeight, bitmap, bitmapWidth, bitmapHeight, firstChar, charData);
|
||||
if (result <= 0) {
|
||||
logger.error("stbtt_BakeFontBitmap failed with result: {}", result);
|
||||
charData.free();
|
||||
// 关键:烘焙从 CHINESE_FIRST_CHAR 开始的 CHINESE_CHAR_COUNT 个连续字符
|
||||
int chineseRes = stbtt_BakeFontBitmap(fontData, fontHeight, chineseBitmap,
|
||||
chineseTexSize, chineseTexSize, CHINESE_FIRST_CHAR, chineseCharData);
|
||||
if (chineseRes <= 0) {
|
||||
logger.error("Chinese font bake failed, result: {}", chineseRes);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建纹理
|
||||
fontTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, bitmap);
|
||||
|
||||
if (fontTextureId == 0) {
|
||||
logger.error("Failed to create font texture");
|
||||
charData.free();
|
||||
chineseTextureId = createTextureFromBitmap(chineseTexSize, chineseTexSize, chineseBitmap);
|
||||
if (chineseTextureId == 0) {
|
||||
logger.error("Failed to create Chinese texture");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
logger.debug("TextRenderer initialized successfully with texture ID: {}", fontTextureId);
|
||||
logger.debug("TextRenderer initialized, ASCII tex={}, Chinese tex={}", asciiTextureId, chineseTextureId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception during TextRenderer initialization: {}", e.getMessage());
|
||||
if (charData != null) {
|
||||
charData.free();
|
||||
charData = null;
|
||||
}
|
||||
logger.error("Exception during TextRenderer init: {}", e.getMessage(), e);
|
||||
cleanup();
|
||||
} finally {
|
||||
shader.stop();
|
||||
}
|
||||
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) {
|
||||
renderText(text, x, y, color, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一行文字的宽度(单位:像素)
|
||||
*/
|
||||
public float getTextWidth(String text) {
|
||||
return getTextWidth(text, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一行文字的宽度(带缩放)
|
||||
*/
|
||||
public float getTextWidth(String text, float scale) {
|
||||
if (!initialized || text == null || text.isEmpty()) return 0f;
|
||||
|
||||
float width = 0f;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c >= firstChar && c < firstChar + charCount) {
|
||||
STBTTBakedChar bakedChar = asciiCharData.get(c - firstChar);
|
||||
width += bakedChar.xadvance() * scale;
|
||||
} else {
|
||||
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
|
||||
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
|
||||
int idx = c - CHINESE_FIRST_CHAR; // 关键:使用 Unicode 差值作为索引
|
||||
STBTTBakedChar bakedChar = chineseCharData.get(idx);
|
||||
width += bakedChar.xadvance() * scale;
|
||||
} else {
|
||||
// 对于未找到的字符,使用空格宽度
|
||||
width += 0.5f * scale; // 估计值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
public void renderText(String text, float x, float y, Vector4f color, float scale) {
|
||||
if (!initialized || text == null || text.isEmpty()) return;
|
||||
if (scale <= 0f) scale = 1.0f;
|
||||
|
||||
RenderSystem.assertOnRenderThread();
|
||||
|
||||
// 保存当前状态
|
||||
RenderSystem.pushState();
|
||||
|
||||
try {
|
||||
// 检查文本着色器是否存在,如果不存在则创建默认的
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
|
||||
if (shader == null) {
|
||||
logger.error("TextShader not found");
|
||||
return;
|
||||
}
|
||||
shader.use();
|
||||
ShaderManagement.setUniformVec4(shader, "uColor", color);
|
||||
ShaderManagement.setUniformInt(shader, "uTexture", 0);
|
||||
@@ -162,76 +181,143 @@ public final class TextRenderer {
|
||||
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};
|
||||
STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack);
|
||||
float[] xpos = {x};
|
||||
float[] ypos = {y};
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder builder = tesselator.getBuilder();
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder builder = t.getBuilder();
|
||||
|
||||
// 计算估计的顶点数量:每个字符6个顶点(2个三角形)
|
||||
int estimatedVertexCount = text.length() * 6;
|
||||
|
||||
// 修复:begin方法需要2个参数
|
||||
builder.begin(RenderSystem.DRAW_TRIANGLES, estimatedVertexCount);
|
||||
// 按字符类型分组渲染以减少纹理切换
|
||||
int currentTexture = -1;
|
||||
boolean batchStarted = false;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c < firstChar || c >= firstChar + charCount) continue;
|
||||
int targetTexture;
|
||||
STBTTBakedChar.Buffer charBuffer;
|
||||
int texWidth, texHeight;
|
||||
|
||||
stbtt_GetBakedQuad(charData, bitmapWidth, bitmapHeight, c - firstChar, xpos, ypos, q, true);
|
||||
if (c >= firstChar && c < firstChar + charCount) {
|
||||
targetTexture = asciiTextureId;
|
||||
charBuffer = asciiCharData;
|
||||
texWidth = bitmapWidth;
|
||||
texHeight = bitmapHeight;
|
||||
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, c - firstChar, xpos, ypos, q, true);
|
||||
} else {
|
||||
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
|
||||
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
|
||||
targetTexture = chineseTextureId;
|
||||
charBuffer = chineseCharData;
|
||||
texWidth = 4096;
|
||||
texHeight = 4096;
|
||||
// 关键修复:索引是字符的 Unicode 减去起始 Unicode
|
||||
int idx = c - CHINESE_FIRST_CHAR;
|
||||
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, idx, xpos, ypos, q, true);
|
||||
} else {
|
||||
continue; // 跳过不支持的字符
|
||||
}
|
||||
}
|
||||
|
||||
// 使用两个三角形组成一个四边形
|
||||
// 第一个三角形
|
||||
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());
|
||||
// 如果纹理改变,结束当前批次
|
||||
if (targetTexture != currentTexture) {
|
||||
if (batchStarted) {
|
||||
t.end();
|
||||
batchStarted = false;
|
||||
}
|
||||
RenderSystem.bindTexture(targetTexture);
|
||||
currentTexture = targetTexture;
|
||||
}
|
||||
|
||||
// 第二个三角形
|
||||
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());
|
||||
// 开始新批次(如果需要)
|
||||
if (!batchStarted) {
|
||||
builder.begin(RenderSystem.DRAW_TRIANGLES, (text.length() - i) * 6);
|
||||
batchStarted = true;
|
||||
}
|
||||
|
||||
// 应用缩放并计算顶点
|
||||
float sx0 = x + (q.x0() - x) * scale;
|
||||
float sx1 = x + (q.x1() - x) * scale;
|
||||
float sy0 = y + (q.y0() - y) * scale;
|
||||
float sy1 = y + (q.y1() - y) * scale;
|
||||
|
||||
builder.vertex(sx0, sy0, q.s0(), q.t0());
|
||||
builder.vertex(sx1, sy0, q.s1(), q.t0());
|
||||
builder.vertex(sx0, sy1, q.s0(), q.t1());
|
||||
|
||||
builder.vertex(sx1, sy0, q.s1(), q.t0());
|
||||
builder.vertex(sx1, sy1, q.s1(), q.t1());
|
||||
builder.vertex(sx0, sy1, q.s0(), q.t1());
|
||||
}
|
||||
|
||||
tesselator.end();
|
||||
// 结束最后一个批次
|
||||
if (batchStarted) {
|
||||
t.end();
|
||||
}
|
||||
}
|
||||
|
||||
RenderSystem.checkGLError("renderText");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error rendering text: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
// 恢复之前的状态
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理字体资源
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (fontTextureId != 0) {
|
||||
RenderSystem.deleteTextures(fontTextureId);
|
||||
fontTextureId = 0;
|
||||
private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
try {
|
||||
int textureId = RenderSystem.genTextures();
|
||||
RenderSystem.bindTexture(textureId);
|
||||
|
||||
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 1);
|
||||
RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, width, height, 0,
|
||||
GL11.GL_RED, 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);
|
||||
|
||||
// 设置纹理swizzle以便单通道纹理在着色器中显示为白色
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_G, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_A, GL11.GL_RED);
|
||||
|
||||
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 4);
|
||||
RenderSystem.bindTexture(0);
|
||||
|
||||
return textureId;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create texture from bitmap: {}", e.getMessage(), e);
|
||||
return 0;
|
||||
}
|
||||
if (charData != null) {
|
||||
charData.free();
|
||||
charData = null;
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (asciiTextureId != 0) {
|
||||
RenderSystem.deleteTextures(asciiTextureId);
|
||||
asciiTextureId = 0;
|
||||
}
|
||||
if (chineseTextureId != 0) {
|
||||
RenderSystem.deleteTextures(chineseTextureId);
|
||||
chineseTextureId = 0;
|
||||
}
|
||||
if (asciiCharData != null) {
|
||||
asciiCharData.free();
|
||||
asciiCharData = null;
|
||||
}
|
||||
if (chineseCharData != null) {
|
||||
chineseCharData.free();
|
||||
chineseCharData = null;
|
||||
}
|
||||
initialized = false;
|
||||
logger.debug("TextRenderer cleaned up");
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public int getFontTextureId() {
|
||||
return fontTextureId;
|
||||
}
|
||||
}
|
||||
public boolean isInitialized() { return initialized; }
|
||||
public int getAsciiTextureId() { return asciiTextureId; }
|
||||
public int getChineseTextureId() { return chineseTextureId; }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -1014,10 +1013,6 @@ public class ModelRenderPanel extends JPanel {
|
||||
logger.info("按住Ctrl/Shift点击空白区域,保持当前选择状态 - Ctrl: {}, Shift: {}, 当前选中数量: {}",
|
||||
e.isControlDown(), e.isShiftDown(), selectedMeshes.size());
|
||||
currentDragMode = DragMode.NONE;
|
||||
} else {
|
||||
currentDragMode = DragMode.NONE;
|
||||
logger.info("点击空白区域,保持当前选择状态 - 当前选中数量: {}, 点击位置: ({}, {})",
|
||||
selectedMeshes.size(), modelX, modelY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1228,11 +1223,11 @@ public class ModelRenderPanel extends JPanel {
|
||||
Vector2f camOffset = ModelRender.getCameraOffset();
|
||||
|
||||
// 应用偏移,将 model 坐标转换到相对于摄像机的坐标系
|
||||
float checkX = modelX - camOffset.x;
|
||||
float checkY = modelY - camOffset.y;
|
||||
float checkX = modelX;
|
||||
float checkY = modelY;
|
||||
|
||||
// 将中心点也转换到相同坐标系
|
||||
center = new Vector2f(center).sub(camOffset);
|
||||
//center = new Vector2f(center).sub(camOffset);
|
||||
|
||||
float scaleFactor = calculateScaleFactor();
|
||||
float borderThickness = BORDER_THICKNESS / scaleFactor;
|
||||
@@ -1368,9 +1363,9 @@ public class ModelRenderPanel extends JPanel {
|
||||
try {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float zoom = camera.getZoom();
|
||||
// 计算世界坐标的移动量(反向移动)
|
||||
|
||||
float worldDeltaX = -deltaX / zoom;
|
||||
float worldDeltaY = deltaY / zoom; // AWT/Swing 的 Y 轴与 OpenGL 相反
|
||||
float worldDeltaY = -deltaY / zoom;
|
||||
|
||||
// 应用摄像机移动
|
||||
camera.move(worldDeltaX, worldDeltaY);
|
||||
@@ -1712,6 +1707,11 @@ public class ModelRenderPanel extends JPanel {
|
||||
// 检测点击的网格
|
||||
Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY);
|
||||
|
||||
if (clickedMesh == null && !e.isControlDown() && !e.isShiftDown()) {
|
||||
clearSelectedMeshes();
|
||||
logger.debug("点击空白处,取消所有选择");
|
||||
}
|
||||
|
||||
// 触发点击事件
|
||||
for (ModelClickListener listener : clickListeners) {
|
||||
try {
|
||||
@@ -1752,11 +1752,17 @@ public class ModelRenderPanel extends JPanel {
|
||||
// 检测悬停的网格
|
||||
Mesh2D newHoveredMesh = findMeshAtPosition(modelX, modelY);
|
||||
|
||||
// 更新悬停状态
|
||||
// 如果悬停网格发生变化,更新状态
|
||||
if (newHoveredMesh != hoveredMesh) {
|
||||
hoveredMesh = newHoveredMesh;
|
||||
if (hoveredMesh != null) {
|
||||
hoveredMesh.setSuspension(false);
|
||||
}
|
||||
|
||||
hoveredMesh = newHoveredMesh;
|
||||
if (hoveredMesh != null) {
|
||||
hoveredMesh.setSuspension(true);
|
||||
}
|
||||
|
||||
// 触发悬停事件
|
||||
for (ModelClickListener listener : clickListeners) {
|
||||
try {
|
||||
listener.onModelHover(newHoveredMesh, modelX, modelY, screenX, screenY);
|
||||
@@ -1766,7 +1772,6 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新鼠标手势
|
||||
updateCursorForHoverState(modelX, modelY, newHoveredMesh);
|
||||
|
||||
} catch (Exception ex) {
|
||||
@@ -1854,23 +1859,16 @@ public class ModelRenderPanel extends JPanel {
|
||||
float glY = (float) screenY * this.height / getHeight();
|
||||
|
||||
// 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;
|
||||
float ndcY = 1.0f - (2.0f * glY) / this.height; // 翻转 Y 轴
|
||||
|
||||
// 3. 逆投影变换
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float zoom = camera.getZoom();
|
||||
Vector2f pos = camera.getPosition();
|
||||
// 3. 获取摄像机偏移
|
||||
Vector2f camOffset = ModelRender.getCameraOffset(); // 这里替换原来的 camera.getPosition()
|
||||
float zoom = ModelRender.getCamera().getZoom();
|
||||
|
||||
// 逆变换公式:
|
||||
// modelX = (ndcX / (2.0f / this.width)) / zoom + pos.x
|
||||
// modelY = (ndcY / (-2.0f / this.height)) / zoom + pos.y
|
||||
|
||||
float modelX = (ndcX * this.width / (2.0f * zoom)) + pos.x;
|
||||
float modelY = (ndcY * this.height / (-2.0f * zoom)) + pos.y;
|
||||
// 4. 逆变换公式
|
||||
float modelX = (ndcX * this.width / (2.0f * zoom)) + camOffset.x;
|
||||
float modelY = (ndcY * this.height / (-2.0f * zoom)) + camOffset.y;
|
||||
|
||||
return new float[]{modelX, modelY};
|
||||
}
|
||||
@@ -1890,9 +1888,8 @@ public class ModelRenderPanel extends JPanel {
|
||||
// 获取摄像机偏移
|
||||
Vector2f camOffset = ModelRender.getCameraOffset();
|
||||
|
||||
// 将输入坐标调整到相对于摄像机的坐标系
|
||||
float checkX = modelX - camOffset.x;
|
||||
float checkY = modelY - camOffset.y;
|
||||
float checkX = modelX;
|
||||
float checkY = modelY;
|
||||
|
||||
java.util.List<ModelPart> parts = model.getParts();
|
||||
if (parts == null || parts.isEmpty()) {
|
||||
@@ -1935,6 +1932,7 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型的边界框
|
||||
*/
|
||||
|
||||
@@ -1060,42 +1060,39 @@ public class ModelPart {
|
||||
public void addMesh(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
|
||||
// 创建独立副本,避免多个 Part 共享同一 Mesh 实例导致数据冲突
|
||||
Mesh2D m = mesh.copy();
|
||||
|
||||
// 确保拷贝保留原始的纹理引用(copy() 已处理)
|
||||
m.setTexture(mesh.getTexture());
|
||||
|
||||
//mesh.setTexture(mesh.getTexture());
|
||||
mesh.setModelPart(this);
|
||||
// 确保本节点的 worldTransform 是最新的
|
||||
recomputeWorldTransformRecursive();
|
||||
|
||||
// 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用
|
||||
float[] originalVertices = m.getVertices().clone();
|
||||
m.setOriginalVertices(originalVertices);
|
||||
float[] originalVertices = mesh.getVertices().clone();
|
||||
mesh.setOriginalVertices(originalVertices);
|
||||
// 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot)
|
||||
// 将每个顶点从本地空间变换到世界空间(烘焙到 world)
|
||||
int vc = m.getVertexCount();
|
||||
int vc = mesh.getVertexCount();
|
||||
for (int i = 0; i < vc; i++) {
|
||||
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);
|
||||
mesh.setVertex(i, worldPt.x, worldPt.y);
|
||||
}
|
||||
|
||||
// 同步 originalPivot -> world pivot(如果 originalPivot 有意义)
|
||||
try {
|
||||
Vector2f origPivot = m.getOriginalPivot();
|
||||
Vector2f origPivot = mesh.getOriginalPivot();
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
|
||||
m.setPivot(worldPivot.x, worldPivot.y);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
} catch (Exception ignored) { }
|
||||
|
||||
// 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新
|
||||
m.setBakedToWorld(true);
|
||||
mesh.setBakedToWorld(true);
|
||||
|
||||
// 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU)
|
||||
m.markDirty();
|
||||
mesh.markDirty();
|
||||
|
||||
// 将拷贝加入到本部件
|
||||
meshes.add(m);
|
||||
meshes.add(mesh);
|
||||
boundsDirty = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.systems.MultiSelectionBoxRenderer;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer;
|
||||
import com.chuangzhou.vivid2D.render.TextRenderer;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
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.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
@@ -14,10 +19,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.*;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -36,6 +38,7 @@ public class Mesh2D {
|
||||
private float[] uvs; // UV坐标 [u0, v0, u1, v1, ...]
|
||||
private int[] indices; // 索引数据
|
||||
private float[] originalVertices; // 原始顶点数据(用于变形恢复)
|
||||
private ModelPart modelPart;
|
||||
|
||||
// ==================== 渲染属性 ====================
|
||||
private Texture texture;
|
||||
@@ -55,6 +58,7 @@ public class Mesh2D {
|
||||
private volatile boolean selected = false;
|
||||
private Vector2f pivot = new Vector2f(0, 0);
|
||||
private Vector2f originalPivot = new Vector2f(0, 0);
|
||||
private boolean isSuspension = false;
|
||||
|
||||
// ==================== 多选支持 ====================
|
||||
private final List<Mesh2D> multiSelectedParts = new ArrayList<>();
|
||||
@@ -117,6 +121,10 @@ public class Mesh2D {
|
||||
markDirty();
|
||||
}
|
||||
|
||||
public void setModelPart(ModelPart modelPart) {
|
||||
this.modelPart = modelPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置中心点
|
||||
*/
|
||||
@@ -618,6 +626,13 @@ public class Mesh2D {
|
||||
this.multiSelectionDirty = true; // 新增:标记多选边界框需要更新
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前的“悬停状态”
|
||||
*/
|
||||
public void setSuspension(boolean suspension) {
|
||||
isSuspension = suspension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除脏标记
|
||||
*/
|
||||
@@ -693,44 +708,71 @@ public class Mesh2D {
|
||||
/**
|
||||
* 绘制网格(会在第一次绘制时自动上传到 GPU)
|
||||
*/
|
||||
public void draw(int shaderProgram, org.joml.Matrix3f modelMatrix) {
|
||||
public void draw(int shaderProgram, Matrix3f modelMatrix) {
|
||||
if (!visible) return;
|
||||
if (indices == null || indices.length == 0) return;
|
||||
|
||||
// 如果数据脏,强制删除旧 GPU 对象并重新上传
|
||||
if (dirty) {
|
||||
deleteGPU();
|
||||
uploadToGPU();
|
||||
}
|
||||
|
||||
if (!uploaded) {
|
||||
uploadToGPU();
|
||||
}
|
||||
|
||||
// 1. 绘制网格
|
||||
// 保存当前 program,必要时恢复
|
||||
int prevProgram = RenderSystem.getCurrentProgram();
|
||||
boolean switchedProgram = false;
|
||||
if (shaderProgram != 0 && prevProgram != shaderProgram) {
|
||||
RenderSystem.useProgram(shaderProgram);
|
||||
switchedProgram = true;
|
||||
}
|
||||
|
||||
// 确保纹理绑定到纹理单元0,并通知 shader 使用纹理单元0
|
||||
RenderSystem.activeTexture(GL13.GL_TEXTURE0);
|
||||
if (texture != null) {
|
||||
texture.bind();
|
||||
} else {
|
||||
RenderSystem.bindTexture(0);
|
||||
}
|
||||
if (shaderProgram != 0) {
|
||||
int texLoc = RenderSystem.getUniformLocation(shaderProgram, "uTexture");
|
||||
if (texLoc != -1) RenderSystem.uniform1i(texLoc, 0);
|
||||
}
|
||||
|
||||
if (shaderProgram != 0) {
|
||||
int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix");
|
||||
if (loc == -1) loc = RenderSystem.getUniformLocation(shaderProgram, "uModel");
|
||||
if (loc != -1) RenderSystem.uniformMatrix3(loc, modelMatrix);
|
||||
}
|
||||
|
||||
// 绑定 VAO 并绘制
|
||||
RenderSystem.glBindVertexArray(() -> vaoId);
|
||||
RenderSystem.useProgram(shaderProgram);
|
||||
|
||||
int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix");
|
||||
if (loc == -1) {
|
||||
loc = RenderSystem.getUniformLocation(shaderProgram, "uModel");
|
||||
}
|
||||
if (loc != -1) {
|
||||
RenderSystem.uniformMatrix3(loc, modelMatrix);
|
||||
}
|
||||
|
||||
RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount,
|
||||
RenderSystem.GL_UNSIGNED_INT, 0);
|
||||
|
||||
// 2. 解绑 VAO 和纹理,确保 overlay 绘制不受影响
|
||||
RenderSystem.glBindVertexArray(() -> 0);
|
||||
|
||||
// 解绑纹理(恢复到单元0的绑定0)
|
||||
if (texture != null) {
|
||||
texture.unbind();
|
||||
} else {
|
||||
RenderSystem.bindTexture(0);
|
||||
}
|
||||
|
||||
// 3. 如果选中,则绘制选中框
|
||||
// 恢复之前的 program(如果我们切换过)
|
||||
if (switchedProgram) {
|
||||
if (prevProgram != 0) RenderSystem.useProgram(prevProgram);
|
||||
else RenderSystem.useProgram(0);
|
||||
}
|
||||
|
||||
// 选中框绘制(需要切换到固色 shader)
|
||||
if (selected) {
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
int currentProgram = RenderSystem.getCurrentProgram();
|
||||
|
||||
RenderSystem.pushState();
|
||||
try {
|
||||
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
|
||||
if (solidShader != null && solidShader.programId != 0) {
|
||||
@@ -751,11 +793,107 @@ public class Mesh2D {
|
||||
drawSelectBox();
|
||||
}
|
||||
} finally {
|
||||
if (currentProgram != 0) {
|
||||
RenderSystem.useProgram(currentProgram);
|
||||
}
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuspension && !selected) {
|
||||
RenderSystem.pushState();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
BoundingBox bbox = getBounds();
|
||||
if (bbox != null && bbox.isValid()) {
|
||||
bb.begin(GL11.GL_LINE_LOOP, 4);
|
||||
bb.setColor(new Vector4f(1f, 0f, 0f, 1f));
|
||||
bb.vertex(bbox.getMinX(), bbox.getMinY(), 0f, 0f);
|
||||
bb.vertex(bbox.getMaxX(), bbox.getMinY(), 0f, 0f);
|
||||
bb.vertex(bbox.getMaxX(), bbox.getMaxY(), 0f, 0f);
|
||||
bb.vertex(bbox.getMinX(), bbox.getMaxY(), 0f, 0f);
|
||||
t.end();
|
||||
|
||||
String hoverText = getName();
|
||||
float textX = bbox.getMaxX() + 5f;
|
||||
float textY = bbox.getMaxY();
|
||||
Vector4f bgColor = new Vector4f(1f, 0f, 0f, 0.8f);
|
||||
Vector4f fgColor = new Vector4f(1f, 1f, 1f, 1f);
|
||||
|
||||
float lineHeight = 18f;
|
||||
|
||||
List<String> lines = splitLines(hoverText, 30);
|
||||
|
||||
float textHeight = lines.size() * lineHeight;
|
||||
float textWidth = 0f;
|
||||
for (String line : lines) {
|
||||
textWidth = Math.max(textWidth, ModelRender.getTextRenderer().getTextWidth(line));
|
||||
}
|
||||
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(bgColor);
|
||||
bb.vertex(textX, textY, 0f, 0f);
|
||||
bb.vertex(textX + textWidth, textY, 0f, 0f);
|
||||
bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f);
|
||||
bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f);
|
||||
bb.vertex(textX, textY + textHeight, 0f, 0f);
|
||||
bb.vertex(textX, textY, 0f, 0f);
|
||||
t.end();
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String line = lines.get(i);
|
||||
ModelRender.renderText(line, textX, textY + (i + 1) * lineHeight - 5, fgColor);
|
||||
}
|
||||
}
|
||||
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> splitLines(String text, int maxCharsPerLine) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
StringBuilder line = new StringBuilder();
|
||||
|
||||
for (String word : text.split(" ")) {
|
||||
if (line.length() + word.length() + 1 > maxCharsPerLine) {
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line.toString());
|
||||
line = new StringBuilder();
|
||||
}
|
||||
if (word.length() > maxCharsPerLine) {
|
||||
int start = 0;
|
||||
while (start < word.length()) {
|
||||
int end = Math.min(start + maxCharsPerLine, word.length());
|
||||
lines.add(word.substring(start, end));
|
||||
start = end;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!line.isEmpty()) {
|
||||
line.append(" ");
|
||||
}
|
||||
line.append(word);
|
||||
}
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line.toString());
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private void drawSelectBox() {
|
||||
@@ -1281,6 +1419,9 @@ public class Mesh2D {
|
||||
// ==================== Getter/Setter ====================
|
||||
|
||||
public String getName() {
|
||||
if (modelPart != null){
|
||||
return modelPart.getName();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.joml.Vector2f;
|
||||
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 float zPosition = 0.0f;
|
||||
private boolean enabled = true;
|
||||
|
||||
public Camera() {}
|
||||
@@ -46,6 +46,9 @@ public class Camera {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摄像机是否启用
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@@ -829,7 +829,7 @@ public final class RenderSystem {
|
||||
}
|
||||
}
|
||||
|
||||
public static ByteBuffer loadWindowsFont(String fontFileName) throws IOException {
|
||||
public static ByteBuffer loadFont(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());
|
||||
|
||||
@@ -203,6 +203,7 @@ public class BufferBuilder {
|
||||
int vao = RenderSystem.glGenVertexArrays();
|
||||
int vbo = RenderSystem.glGenBuffers();
|
||||
|
||||
// 使用 RenderSystem 要求的 Supplier/IntSupplier 风格来绑定(修复类型不匹配编译错误)
|
||||
RenderSystem.glBindVertexArray(() -> vao);
|
||||
RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> vbo);
|
||||
RenderSystem.glBufferData(RenderSystem.GL_ARRAY_BUFFER, fb, RenderSystem.GL_DYNAMIC_DRAW);
|
||||
@@ -217,6 +218,8 @@ public class BufferBuilder {
|
||||
COMPONENTS_PER_VERTEX * Float.BYTES, 2 * Float.BYTES);
|
||||
|
||||
MemoryUtil.memFree(fb);
|
||||
|
||||
// 解绑 VBO/VAO(同样使用 Supplier/IntSupplier)
|
||||
RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> 0);
|
||||
RenderSystem.glBindVertexArray(() -> 0);
|
||||
|
||||
|
||||
@@ -54,11 +54,14 @@ public class TextShader implements CompleteShader {
|
||||
layout(location = 0) in vec2 aPosition;
|
||||
layout(location = 1) in vec2 aTexCoord;
|
||||
|
||||
uniform mat3 uProjectionMatrix;
|
||||
|
||||
out vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vec3 p = uProjectionMatrix * vec3(aPosition, 1.0);
|
||||
gl_Position = vec4(p.xy, 0.0, 1.0);
|
||||
vTexCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition.xy, 0.0, 1.0);
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user