From 331d836d62577154ea6783cff45782987ad5f36a Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 25 Oct 2025 10:08:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E5=AE=9E=E7=8E=B0=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=96=87=E6=9C=AC=E6=B8=B2=E6=9F=93=E4=B8=8E=E6=82=AC?= =?UTF-8?q?=E5=81=9C=E6=8F=90=E7=A4=BA=E5=8A=9F=E8=83=BD-=20=E5=9C=A8=20Me?= =?UTF-8?q?sh2D=20=E4=B8=AD=E5=A2=9E=E5=8A=A0=E6=82=AC=E5=81=9C=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=94=AF=E6=8C=81=EF=BC=8C=E5=85=81=E8=AE=B8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=BA=A2=E8=89=B2=E8=BE=B9=E6=A1=86=E5=92=8C=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E6=A0=87=E7=AD=BE=20-=20=E6=B7=BB=E5=8A=A0=20splitLin?= =?UTF-8?q?es=20=E6=96=B9=E6=B3=95=E6=94=AF=E6=8C=81=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=8D=A2=E8=A1=8C=E6=98=BE=E7=A4=BA=20-=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20TextRenderer=20=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20ASCII=20=E5=92=8C=E4=B8=AD=E6=96=87=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E6=B7=B7=E5=90=88=E6=B8=B2=E6=9F=93=20-=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20getTextWidth=20=E6=96=B9=E6=B3=95=E7=94=A8=E4=BA=8E=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E6=96=87=E6=9C=AC=E5=AE=9E=E9=99=85=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=AE=BD=E5=BA=A6=20-=20=E4=BF=AE=E5=A4=8D=20RenderSystem=20?= =?UTF-8?q?=E4=B8=AD=E5=AD=97=E4=BD=93=E5=8A=A0=E8=BD=BD=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=80=E8=87=B4=E6=80=A7=E9=97=AE=E9=A2=98?= =?UTF-8?q?-=20=E8=B0=83=E6=95=B4=20ModelRenderPanel=20=E4=B8=AD=E5=9D=90?= =?UTF-8?q?=E6=A0=87=E8=BD=AC=E6=8D=A2=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=8B=BE=E5=8F=96=E5=87=86=E7=A1=AE=E6=80=A7=20-=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E7=9A=84=20Matrix3fUtils?= =?UTF-8?q?=20=E5=BC=95=E7=94=A8=EF=BC=8C=E4=BC=98=E5=8C=96=E5=8C=85?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E7=BB=93=E6=9E=84-=20=E5=AE=8C=E5=96=84=20Me?= =?UTF-8?q?sh2D=20=E7=BB=98=E5=88=B6=E6=B5=81=E7=A8=8B=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E7=BA=B9=E7=90=86=E7=BB=91=E5=AE=9A=E6=93=8D=E4=BD=9C-=20?= =?UTF-8?q?=E4=B8=BA=20Mesh2D=20=E5=92=8C=20ModelPart=20=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E5=8F=8C=E5=90=91=E5=85=B3=E8=81=94=EF=BC=8C=E4=BE=BF=E4=BA=8E?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=A8=A1=E5=9E=8B=E9=83=A8=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=20-=20=E4=BF=AE=E6=94=B9=E6=91=84=E5=83=8F=E6=9C=BA?= =?UTF-8?q?=E5=81=8F=E7=A7=BB=E8=AE=A1=E7=AE=97=E6=96=B9=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E6=B8=B2=E6=9F=93=E5=9D=90=E6=A0=87=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vivid2D/render/ModelRender.java | 50 ++- .../MultiSelectionBoxRenderer.java | 3 +- .../vivid2D/render/TextRenderer.java | 360 +++++++++++------- .../vivid2D/render/awt/ModelRenderPanel.java | 60 ++- .../vivid2D/render/model/ModelPart.java | 25 +- .../vivid2D/render/model/util/Mesh2D.java | 189 +++++++-- .../vivid2D/render/systems/Camera.java | 5 +- .../vivid2D/render/systems/RenderSystem.java | 2 +- .../render/systems/buffer/BufferBuilder.java | 3 + .../systems/sources/def/TextShader.java | 5 +- 10 files changed, 473 insertions(+), 229 deletions(-) rename src/main/java/com/chuangzhou/vivid2D/render/{systems => }/MultiSelectionBoxRenderer.java (99%) diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index bebd8ca..25888ad 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -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); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java similarity index 99% rename from src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java rename to src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java index 349a2a1..b8ca1d8 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/MultiSelectionBoxRenderer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java @@ -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; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java index 1e5f0aa..c771e22 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java @@ -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; - } -} \ No newline at end of file + public boolean isInitialized() { return initialized; } + public int getAsciiTextureId() { return asciiTextureId; } + public int getChineseTextureId() { return chineseTextureId; } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java index 1d02aaa..bd61b3c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -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 parts = model.getParts(); if (parts == null || parts.isEmpty()) { @@ -1935,6 +1932,7 @@ public class ModelRenderPanel extends JPanel { } + /** * 获取模型的边界框 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 6d27edc..227aeab 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -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; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index 4390f6c..59174cb 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -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 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 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 splitLines(String text, int maxCharsPerLine) { + List 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; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java index 06a9b2e..6107dc0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java @@ -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; } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java index 36ef84f..be8b2e6 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java @@ -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()); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java index d8b40e8..b43b659 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java @@ -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); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java index 03b933d..fa416b4 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java @@ -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); } """; }