- 为 BoundingBox 类添加获取中心点坐标的便捷方法 - 重构 Mesh2D 悬停提示框绘制逻辑,支持基于摄像机缩放的动态尺寸计算 - 在 ModelRender 中新增带缩放参数的文本渲染方法 - 重写 MultiSelectionBoxRenderer 以适配动态缩放,统一使用像素单位配置 - 优化 ParametersManagement 日志记录方式 - 修复 TextRenderer 字体颜色传递问题 - 更新 TextShader 着色器代码以兼容新的渲染管线和透明度处理
337 lines
13 KiB
Java
337 lines
13 KiB
Java
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.Vector4f;
|
|
import org.lwjgl.opengl.GL11;
|
|
import org.lwjgl.opengl.GL12;
|
|
import org.lwjgl.opengl.GL30;
|
|
import org.lwjgl.opengl.GL33;
|
|
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.stbtt_BakeFontBitmap;
|
|
import static org.lwjgl.stb.STBTruetype.stbtt_GetBakedQuad;
|
|
|
|
/**
|
|
* 支持 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 asciiCharData;
|
|
private STBTTBakedChar.Buffer chineseCharData;
|
|
private int asciiTextureId;
|
|
private int chineseTextureId;
|
|
|
|
private boolean initialized = false;
|
|
|
|
// 中文字符起始编码(选择一个不冲突的范围)
|
|
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;
|
|
this.firstChar = firstChar;
|
|
this.charCount = charCount;
|
|
}
|
|
|
|
/**
|
|
* 初始化字体渲染器
|
|
*/
|
|
public void initialize(ByteBuffer fontData, float fontHeight) {
|
|
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();
|
|
|
|
try {
|
|
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;
|
|
}
|
|
|
|
// 烘焙中文 - 使用更大的纹理和正确的字符范围
|
|
int chineseTexSize = 4096; // 中文字符需要更大的纹理
|
|
// 分配足够的空间来存储 CHINESE_CHAR_COUNT 个字符的数据
|
|
chineseCharData = STBTTBakedChar.malloc(CHINESE_CHAR_COUNT);
|
|
ByteBuffer chineseBitmap = ByteBuffer.allocateDirect(chineseTexSize * chineseTexSize);
|
|
|
|
// 关键:烘焙从 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;
|
|
}
|
|
chineseTextureId = createTextureFromBitmap(chineseTexSize, chineseTexSize, chineseBitmap);
|
|
if (chineseTextureId == 0) {
|
|
logger.error("Failed to create Chinese texture");
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
initialized = true;
|
|
logger.debug("TextRenderer initialized, ASCII tex={}, Chinese tex={}", asciiTextureId, chineseTextureId);
|
|
|
|
} catch (Exception e) {
|
|
logger.error("Exception during TextRenderer init: {}", e.getMessage(), e);
|
|
cleanup();
|
|
} finally {
|
|
shader.stop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 渲染文字
|
|
*/
|
|
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);
|
|
|
|
RenderSystem.enableBlend();
|
|
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
|
RenderSystem.disableDepthTest();
|
|
|
|
try (MemoryStack stack = MemoryStack.stackPush()) {
|
|
STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack);
|
|
float[] xpos = {x};
|
|
float[] ypos = {y};
|
|
|
|
Tesselator t = Tesselator.getInstance();
|
|
BufferBuilder builder = t.getBuilder();
|
|
|
|
// 按字符类型分组渲染以减少纹理切换
|
|
int currentTexture = -1;
|
|
boolean batchStarted = false;
|
|
builder.setColor(color);
|
|
|
|
for (int i = 0; i < text.length(); i++) {
|
|
char c = text.charAt(i);
|
|
int targetTexture;
|
|
STBTTBakedChar.Buffer charBuffer;
|
|
int texWidth, texHeight;
|
|
|
|
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; // 跳过不支持的字符
|
|
}
|
|
}
|
|
|
|
// 如果纹理改变,结束当前批次
|
|
if (targetTexture != currentTexture) {
|
|
if (batchStarted) {
|
|
t.end();
|
|
batchStarted = false;
|
|
}
|
|
RenderSystem.bindTexture(targetTexture);
|
|
currentTexture = targetTexture;
|
|
}
|
|
|
|
// 开始新批次(如果需要)
|
|
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());
|
|
}
|
|
|
|
// 结束最后一个批次
|
|
if (batchStarted) {
|
|
t.end();
|
|
}
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
logger.error("Error rendering text: {}", e.getMessage(), e);
|
|
} finally {
|
|
RenderSystem.popState();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 getAsciiTextureId() {
|
|
return asciiTextureId;
|
|
}
|
|
|
|
public int getChineseTextureId() {
|
|
return chineseTextureId;
|
|
}
|
|
}
|