Files
window-axis-innovators-box/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java
tzdwindows 7 fe4142902c feat(render): 实现动态缩放支持与文本渲染优化
- 为 BoundingBox 类添加获取中心点坐标的便捷方法
- 重构 Mesh2D 悬停提示框绘制逻辑,支持基于摄像机缩放的动态尺寸计算
- 在 ModelRender 中新增带缩放参数的文本渲染方法
- 重写 MultiSelectionBoxRenderer 以适配动态缩放,统一使用像素单位配置
- 优化 ParametersManagement 日志记录方式
- 修复 TextRenderer 字体颜色传递问题
- 更新 TextShader 着色器代码以兼容新的渲染管线和透明度处理
2025-11-21 16:46:37 +08:00

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;
}
}