feat(render): 实现摄像机系统和文字渲染功能

- 添加 Camera 类,支持位置、缩放、Z轴控制- 在 ModelRender 中集成摄像机投影矩阵计算
- 实现屏幕坐标到世界坐标的转换方法
- 添加默认文字渲染器和字体加载逻辑
- 在渲染面板中添加摄像机控制的鼠标手势支持
- 支持通过鼠标滚轮进行摄像机缩放操作
- 添加摄像机状态显示和调试信息渲染
- 实现多选框渲染逻辑的重构和优化
-修复坐标系变换相关的边界框计算问题
- 增加摄像机启用/禁用快捷键支持cyon 等- 添加对 Linux 和 macOS 的 LWJGL 原生库支持
- 将任务定义方式从 task 改为 tasks.register 以提高性能
- 更新部分 JavaFX 和其他图形库的版本
-优化依赖项排列顺序,增强可读性与逻辑分组
This commit is contained in:
tzdwindows 7
2025-10-24 20:05:40 +08:00
parent 2278c5d0c7
commit 210ac72a38
12 changed files with 1911 additions and 312 deletions

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