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

@@ -1,3 +1,5 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
plugins {
id 'java'
id 'application'
@@ -31,114 +33,159 @@ repositories {
mavenCentral()
}
//tasks.named("bootJar") {
// enabled = false
//}
dependencies {
// === 构建工具 ===
proguardLib files('libs/proguard.jar')
// === 测试框架 ===
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.commonmark:commonmark:0.24.0'
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
implementation 'com.google.code.gson:gson:2.8.9'
// === 开发工具 ===
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// === 本地库文件 ===
implementation files('libs/JNC-1.0-jnc.jar')
implementation files('libs/dog api 1.3.jar')
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
// === 核心工具库 ===
implementation 'com.google.code.gson:gson:2.10.1' // 统一版本
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.20.0'
implementation 'commons-io:commons-io:2.18.0' // 统一版本
implementation 'com.google.guava:guava:31.1-jre'
implementation 'net.java.dev.jna:jna:5.13.0'
implementation 'net.java.dev.jna:jna-platform:5.13.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-compress:1.23.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
// === 字节码操作 ===
implementation 'org.ow2.asm:asm:9.7.1'
implementation 'org.ow2.asm:asm-commons:9.7.1'
implementation 'org.ow2.asm:asm-analysis:9.7.1'
implementation 'org.ow2.asm:asm-util:9.7.1'
implementation 'org.ow2.asm:asm-tree:9.7.1'
implementation 'net.bytebuddy:byte-buddy:1.17.6'
// === 反编译工具 ===
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' // 统一版本
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' // 统一版本
implementation 'org.benf:cfr:0.152'
// === Java 解析与分析 ===
implementation 'com.github.javaparser:javaparser-core:3.25.1'
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
// === 文本处理 ===
implementation 'org.commonmark:commonmark:0.24.0'
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
// === Web 和网络 ===
implementation 'org.jsoup:jsoup:1.17.2'
implementation 'commons-io:commons-io:2.14.0'
implementation 'org.json:json:20231013' // 统一版本
implementation 'org.openjfx:javafx-web:17'
// === UI 框架 ===
implementation 'com.formdev:flatlaf:3.2.1'
implementation 'com.formdev:flatlaf-extras:3.2.1'
implementation 'com.formdev:flatlaf-intellij-themes:3.2.1'
implementation 'io.github.vincenzopalazzo:material-ui-swing:1.1.2'
implementation 'org.python:jython-standalone:2.7.3'
implementation 'org.graalvm.python:python-embedding:24.2.1'
implementation files('libs/JNC-1.0-jnc.jar')
implementation files('libs/dog api 1.3.jar')
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
// JavaFX
implementation 'org.openjfx:javafx-controls:21'
implementation 'org.openjfx:javafx-graphics:21'
implementation 'org.fxmisc.richtext:richtextfx:0.11.0'
implementation 'org.bitbucket.mstrobel:procyon-core:0.5.36'
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.5.36'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
// === 代码编辑器 ===
implementation 'com.fifesoft:rsyntaxtextarea:3.5.4'
implementation 'com.fifesoft:rstaui:3.3.1'
implementation 'com.fifesoft:languagesupport:3.3.0'
implementation 'com.fifesoft:autocomplete:3.3.2'
implementation 'org.apache.commons:commons-compress:1.23.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
implementation 'net.sourceforge.plantuml:plantuml:8059'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.openjfx:javafx-controls:21'
implementation 'org.benf:cfr:0.152'
implementation 'com.github.javaparser:javaparser-core:3.25.1'
implementation 'com.1stleg:jnativehook:2.1.0'
implementation 'org.json:json:20230618'
// === 图形和游戏引擎 ===
// LWJGL
implementation 'org.lwjgl:lwjgl:3.3.6'
implementation 'org.lwjgl:lwjgl-stb:3.3.6'
implementation 'org.lwjgl:lwjgl-glfw:3.3.6'
implementation 'org.lwjgl:lwjgl-opengl:3.3.6'
implementation 'org.lwjgl:lwjgl-jawt:3.3.5'
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows'
// Lwjgl natives
if (DefaultNativePlatform.currentOperatingSystem.isWindows()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows'
} else if (DefaultNativePlatform.currentOperatingSystem.isLinux()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-linux'
} else if (DefaultNativePlatform.currentOperatingSystem.isMacOsX()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-macos'
}
// 其他图形库
implementation 'com.badlogicgames.gdx:gdx:1.12.1'
implementation 'org.joml:joml:1.10.7'
implementation 'com.kitfox.svg:svg-salamander:1.0'
implementation 'net.sourceforge.plantuml:plantuml:8059'
implementation 'com.twelvemonkeys.imageio:imageio-psd:3.12.0'
// === 图像处理 ===
implementation 'com.madgag:animated-gif-lib:1.4'
implementation 'org.bytedeco:javacv-platform:1.5.7'
implementation 'org.bytedeco:javacpp-platform:1.5.7'
implementation 'com.madgag:animated-gif-lib:1.4'
implementation 'org.openjfx:javafx-web:17'
runtimeOnly 'com.mysql:mysql-connector-j'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'com.kitfox.svg:svg-salamander:1.0'
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
// === 编程语言支持 ===
implementation 'org.python:jython-standalone:2.7.3'
implementation 'org.graalvm.python:python-embedding:24.2.1'
// === 系统交互 ===
implementation 'com.github.kwhat:jnativehook:2.2.2'
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
implementation 'org.openjfx:javafx-graphics:21'
implementation 'me.friwi:jcefmaven:122.1.10'
implementation 'com.alphacephei:vosk:0.3.45'
implementation 'net.java.dev.jna:jna:5.13.0'
implementation 'com.1stleg:jnativehook:2.1.0'
// === 数据库 ===
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'com.h2database:h2:2.2.220'
implementation 'org.xerial:sqlite-jdbc:3.41.2.1'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.postgresql:postgresql:42.6.0'
implementation 'net.java.dev.jna:jna-platform:5.13.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0'
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0'
implementation 'com.belerweb:pinyin4j:2.5.1'
implementation 'commons-io:commons-io:2.18.0'
// === 音频处理 ===
implementation 'jflac:jflac:1.3'
implementation 'com.github.axet:TarsosDSP:2.4'
implementation 'org.json:json:20231013'
implementation 'org.casbin:casdoor-java-sdk:1.37.0'
implementation 'com.googlecode.soundlibs:mp3spi:1.9.5-1'
implementation 'com.googlecode.soundlibs:vorbisspi:1.0.3-2'
implementation 'com.googlecode.soundlibs:jorbis:0.0.17-2'
//implementation 'com.googlecode.soundlibs:tritonus-share:0.3.6-2'
implementation 'com.googlecode.soundlibs:mp3spi:1.9.5-1' // mp3 支持
implementation 'com.googlecode.soundlibs:vorbisspi:1.0.3-2' // ogg 支持
//implementation 'com.googlecode.soundlibs:flac:1.3.3-1' // flac 支持(示例)
implementation 'com.googlecode.soundlibs:jorbis:0.0.17-2' // ogg 依赖
// === 语音识别 ===
implementation 'com.alphacephei:vosk:0.3.45'
// === 浏览器引擎 ===
implementation 'me.friwi:jcefmaven:122.1.10'
// === 中文处理 ===
implementation 'com.belerweb:pinyin4j:2.5.1'
// === 安全认证 ===
implementation 'cn.dev33:sa-token-spring-boot-starter:1.44.0'
implementation 'com.twelvemonkeys.imageio:imageio-psd:3.12.0'
implementation 'org.casbin:casdoor-java-sdk:1.37.0'
}
configurations.all {
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
@@ -190,7 +237,7 @@ application {
mainClass = 'com.axis.innovators.box.Main'
}
task runClient(type: JavaExec) {
tasks.register('runClient', JavaExec) {
group = "run-toolboxProgram"
description = "执行工具箱程序"
classpath = sourceSets.main.runtimeClasspath
@@ -201,7 +248,7 @@ task runClient(type: JavaExec) {
]
}
task test2DModelLayerPanel(type: JavaExec) {
tasks.register('test2DModelLayerPanel', JavaExec) {
group = "test-model"
description = "运行 2D Model Layer Panel 测试"
classpath = sourceSets.main.runtimeClasspath
@@ -211,7 +258,7 @@ task test2DModelLayerPanel(type: JavaExec) {
]
}
task testModelRenderLightingTest(type: JavaExec) {
tasks.register('testModelRenderLightingTest', JavaExec) {
group = "test-model"
description = "运行 2D Model 高亮灯光测试"
classpath = sourceSets.main.runtimeClasspath
@@ -221,7 +268,7 @@ task testModelRenderLightingTest(type: JavaExec) {
]
}
task testModelTest(type: JavaExec) {
tasks.register('testModelTest', JavaExec) {
group = "test-model"
description = "运行 2D Model 保存和完整性测试"
classpath = sourceSets.main.runtimeClasspath
@@ -231,7 +278,7 @@ task testModelTest(type: JavaExec) {
]
}
task testModelTest2(type: JavaExec) {
tasks.register('testModelTest2', JavaExec) {
group = "test-model"
description = "运行 2D Model 物理基准测试"
classpath = sourceSets.main.runtimeClasspath

View File

@@ -2,6 +2,7 @@ package com.chuangzhou.vivid2D.render;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.systems.Camera;
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
import com.chuangzhou.vivid2D.render.model.util.LightSource;
@@ -18,6 +19,7 @@ import org.lwjgl.opengl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -73,14 +75,14 @@ public final class ModelRender {
* 默认值800像素
* @see #setViewport(int, int)
*/
private static int viewportWidth = 800;
static int viewportWidth = 800;
/**
* 视口高度(像素),定义渲染区域的大小
* 默认值600像素
* @see #setViewport(int, int)
*/
private static int viewportHeight = 600;
static int viewportHeight = 600;
/**
* 清除颜色RGBA用于在每帧开始时清空颜色缓冲区
@@ -179,8 +181,105 @@ public final class ModelRender {
*/
public static boolean renderLightPositions = true;
// ================== 内部类ShaderProgram ==================
// ================== 摄像机状态 ==================
/**
* 默认摄像机,用于控制场景的视图和缩放
* 默认位置:(0, 0)
*/
private static final Camera camera = new Camera();
// ================== 字体管理 ==================
private static TextRenderer defaultTextRenderer = null;
private static final int FONT_BITMAP_WIDTH = 512;
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方法 ==================
/**
* 获取全局摄像机实例
*/
public static Camera getCamera() {
return camera;
}
/**
* 设置摄像机位置
*/
public static void setCameraPosition(float x, float y) {
camera.setPosition(x, y);
}
/**
* 设置摄像机缩放
*/
public static void setCameraZoom(float zoom) {
camera.setZoom(zoom);
}
/**
* 设置摄像机Z轴位置
*/
public static void setCameraZPosition(float z) {
camera.setZPosition(z);
}
/**
* 移动摄像机
*/
public static void moveCamera(float dx, float dy) {
camera.move(dx, dy);
}
/**
* 缩放摄像机
*/
public static void zoomCamera(float factor) {
camera.zoom(factor);
}
/**
* 重置摄像机
*/
public static void resetCamera() {
camera.reset();
}
/**
* 启用/禁用摄像机
*/
public static void setCameraEnabled(boolean enabled) {
camera.setEnabled(enabled);
}
/**
* 构建考虑摄像机变换的投影矩阵
*/
private static Matrix3f buildCameraProjection(int width, int height) {
Matrix3f m = new Matrix3f();
if (camera.isEnabled()) {
// 考虑摄像机缩放和平移
float zoom = camera.getZoom();
Vector2f pos = camera.getPosition();
m.set(
2.0f * zoom / width, 0.0f, -1.0f - (2.0f * zoom * pos.x / width),
0.0f, -2.0f * zoom / height, 1.0f + (2.0f * zoom * pos.y / height),
0.0f, 0.0f, 1.0f
);
} else {
// 原始投影矩阵
m.set(
2.0f / width, 0.0f, -1.0f,
0.0f, -2.0f / height, 1.0f,
0.0f, 0.0f, 1.0f
);
}
return m;
}
// ================== 内部类MeshGLResources ==================
private static class MeshGLResources {
@@ -221,10 +320,47 @@ public final class ModelRender {
throw ex;
}
createDefaultTexture();
RenderSystem.viewport(0, 0, viewportWidth, viewportHeight);
RenderSystem.finishInitialization();
try {
// 初始化默认字体(可替换为你自己的 TTF 数据)
ByteBuffer fontData = null;
try {
fontData = RenderSystem.loadWindowsFont("Arial.ttf");
} catch (Exception e) {
logger.warn("Failed to load Arial.ttf, trying fallback fonts", e);
// 尝试其他字体
try {
fontData = RenderSystem.loadWindowsFont("arial.ttf");
} catch (Exception e2) {
try {
fontData = RenderSystem.loadWindowsFont("times.ttf");
} catch (Exception e3) {
logger.error("All font loading attempts failed");
}
}
}
if (fontData != null && fontData.capacity() > 0) {
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
RenderSystem.checkGLError("defaultTextRenderer initialization");
if (!defaultTextRenderer.isInitialized()) {
logger.error("TextRenderer failed to initialize properly");
}
} else {
logger.error("No valid font data available for text rendering");
}
} catch (Exception e) {
logger.warn("Failed to initialize default text renderer", e);
}
initialized = true;
logger.info("ModelRender initialized successfully");
}
@@ -456,8 +592,8 @@ public final class ModelRender {
return;
}
// 设置投影与视图矩阵(所有着色器都需要
Matrix3f proj = buildOrthoProjection(viewportWidth, viewportHeight);
// 设置投影与视图矩阵(使用摄像机变换
Matrix3f proj = buildCameraProjection(viewportWidth, viewportHeight);
Matrix3f view = new Matrix3f().identity();
// 1. 首先设置默认着色器
@@ -469,6 +605,10 @@ public final class ModelRender {
setUniformMatrix3(defaultProgram, "uViewMatrix", view);
RenderSystem.checkGLError("after_set_default_matrices");
// 设置摄像机Z轴位置如果着色器支持
setUniformFloatInternal(defaultProgram, "uCameraZ", camera.getZPosition());
RenderSystem.checkGLError("after_set_camera_z");
// 添加光源数据上传到默认着色器
uploadLightsToShader(defaultProgram, model);
RenderSystem.checkGLError("after_upload_lights");
@@ -495,10 +635,22 @@ public final class ModelRender {
RenderSystem.checkGLError("after_render_colliders");
}
defaultProgram.stop();
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");
}
/**
* 设置所有非默认着色器的顶点坐标相关uniform
*/
@@ -534,6 +686,9 @@ public final class ModelRender {
// 设置基础模型矩阵为单位矩阵
setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity());
// 设置摄像机Z轴位置
setUniformFloatInternal(program, "uCameraZ", camera.getZPosition());
RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName());
} catch (Exception e) {
@@ -840,6 +995,41 @@ public final class ModelRender {
return m;
}
/**
* 渲染文字
* @param text 文字内容
* @param x 世界坐标 X
* @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;
defaultTextRenderer.renderText(text, px, py, color);
}
/**
* 获取默认摄像机与当前摄像机之间的偏移量
* @return Vector2f 偏移向量 (dx, dy)
*/
public static Vector2f getCameraOffset() {
float width = viewportWidth;
float height = viewportHeight;
float zoom = camera.getZoom();
Vector2f pos = camera.getPosition();
float tx = -1.0f - (2.0f * zoom * pos.x / width);
float ty = 1.0f + (2.0f * zoom * pos.y / height);
float tx0 = -1.0f;
float ty0 = 1.0f;
float offsetX = tx - tx0;
float offsetY = ty - ty0;
offsetX = -offsetX * width / 2.0f / zoom;
offsetY = offsetY * height / 2.0f / zoom;
return new Vector2f(offsetX, offsetY);
}
public static void setViewport(int width, int height) {
viewportWidth = Math.max(1, width);
viewportHeight = Math.max(1, height);

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

View File

@@ -7,7 +7,10 @@ import com.chuangzhou.vivid2D.render.model.Model2D;
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;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
@@ -126,6 +129,75 @@ public class ModelRenderPanel extends JPanel {
private Map<ModelPart, Float> dragStartRotations = new HashMap<>();
private Map<ModelPart, Vector2f> dragStartPivots = new HashMap<>();
// 新增:鼠标手势相关字段
private volatile Cursor currentCursor = Cursor.getDefaultCursor();
private volatile DragMode hoverDragMode = DragMode.NONE;
private volatile boolean isOverSelection = false;
// ================== 摄像机控制相关字段 ==================
private volatile boolean cameraDragging = false;
private volatile int lastCameraDragX, lastCameraDragY;
private volatile float cameraStartX, cameraStartY;
private static final float CAMERA_ZOOM_STEP = 1.1f;
private static final float CAMERA_ZOOM_MIN = 0.1f;
private static final float CAMERA_ZOOM_MAX = 10.0f;
private static final float CAMERA_Z_STEP = 0.1f;
private static final float CAMERA_Z_MIN = -5.0f;
private static final float CAMERA_Z_MAX = 5.0f;
// ================== 摄像机控制方法 ==================
/**
* 获取摄像机实例
*/
public Camera getCamera() {
return ModelRender.getCamera();
}
/**
* 设置摄像机位置
*/
public void setCameraPosition(float x, float y) {
executeInGLContext(() -> ModelRender.setCameraPosition(x, y));
}
/**
* 设置摄像机缩放
*/
public void setCameraZoom(float zoom) {
executeInGLContext(() -> ModelRender.setCameraZoom(zoom));
}
/**
* 设置摄像机Z轴位置
*/
public void setCameraZPosition(float z) {
executeInGLContext(() -> ModelRender.setCameraZPosition(z));
}
/**
* 移动摄像机
*/
public void moveCamera(float dx, float dy) {
executeInGLContext(() -> ModelRender.moveCamera(dx, dy));
}
/**
* 缩放摄像机
*/
public void zoomCamera(float factor) {
executeInGLContext(() -> ModelRender.zoomCamera(factor));
}
/**
* 重置摄像机
*/
public void resetCamera() {
executeInGLContext(() -> ModelRender.resetCamera());
}
/**
* 构造函数:使用模型路径
*/
@@ -190,6 +262,32 @@ public class ModelRenderPanel extends JPanel {
clearHistory();
}
});
// 添加摄像机重置快捷键Ctrl+R
KeyStroke resetCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK);
inputMap.put(resetCameraKey, "resetCamera");
actionMap.put("resetCamera", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
resetCamera();
logger.info("重置摄像机");
}
});
// 添加摄像机启用/禁用快捷键Ctrl+E
KeyStroke toggleCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK);
inputMap.put(toggleCameraKey, "toggleCamera");
actionMap.put("toggleCamera", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
//executeInGLContext(() -> {
Camera camera = ModelRender.getCamera();
boolean newState = !camera.isEnabled();
camera.setEnabled(newState);
logger.info("{}摄像机", newState ? "启用" : "禁用");
//});
}
});
}
// ============ 新增:操作历史记录方法 ============
@@ -371,28 +469,28 @@ public class ModelRenderPanel extends JPanel {
*/
public void setSelectedMesh(Mesh2D mesh) {
//executeInGLContext(() -> {
// 清除之前选中的所有网格
for (Mesh2D selectedMesh : selectedMeshes) {
selectedMesh.setSelected(false);
// 清除多选列表
selectedMesh.clearMultiSelection();
}
selectedMeshes.clear();
// 清除之前选中的所有网格
for (Mesh2D selectedMesh : selectedMeshes) {
selectedMesh.setSelected(false);
// 清除多选列表
selectedMesh.clearMultiSelection();
}
selectedMeshes.clear();
// 设置新的选中网格
if (mesh != null) {
mesh.setSelected(true);
selectedMeshes.add(mesh);
lastSelectedMesh = mesh; // 更新最后选中的网格
// 设置新的选中网格
if (mesh != null) {
mesh.setSelected(true);
selectedMeshes.add(mesh);
lastSelectedMesh = mesh; // 更新最后选中的网格
// 通知其他选中网格添加到多选列表
updateMultiSelectionInMeshes();
} else {
lastSelectedMesh = null;
}
// 通知其他选中网格添加到多选列表
updateMultiSelectionInMeshes();
} else {
lastSelectedMesh = null;
}
logger.debug("设置选中网格: {}, 当前选中数量: {}",
mesh != null ? mesh.getName() : "null", selectedMeshes.size());
logger.debug("设置选中网格: {}, 当前选中数量: {}",
mesh != null ? mesh.getName() : "null", selectedMeshes.size());
//});
}
@@ -631,8 +729,71 @@ public class ModelRenderPanel extends JPanel {
public void mouseReleased(MouseEvent e) {
handleMouseReleased(e);
}
@Override
public void mouseExited(MouseEvent e) {
// 鼠标离开面板时恢复默认光标
setCursor(Cursor.getDefaultCursor());
}
});
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (!contextInitialized) return;
final int screenX = e.getX();
final int screenY = e.getY();
final int notches = e.getWheelRotation();
final boolean fine = e.isShiftDown();
executeInGLContext(() -> {
Camera camera = ModelRender.getCamera();
float oldZoom = camera.getZoom();
// 1. 获取缩放前的世界坐标
float[] worldPosBefore = screenToModelCoordinates(screenX, screenY);
if (worldPosBefore == null) return;
// 2. 计算新缩放级别
// 使用 CAMERA_ZOOM_STEP 或 ZOOM_STEP这里沿用原有的 ZOOM_STEP 逻辑
double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP;
float newZoom = oldZoom;
if (notches > 0) { // 滚轮向下,缩小
newZoom /= Math.pow(step, notches);
} else { // 滚轮向上,放大
newZoom *= Math.pow(step, -notches);
}
// 限制范围,使用 CAMERA_ZOOM_MIN/MAX 或 ZOOM_MIN/MAX这里沿用原有的 ZOOM_MIN/MAX
newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom));
if (Math.abs(newZoom - oldZoom) < 1e-6f) {
return; // 缩放级别无变化
}
// 3. 应用新缩放并获取缩放后的世界坐标
camera.setZoom(newZoom);
float[] worldPosAfter = screenToModelCoordinates(screenX, screenY);
if (worldPosAfter == null) {
camera.setZoom(oldZoom); // 如果计算失败则恢复
return;
}
// 4. 计算相机需要平移的量,以保持鼠标指针下的点不变
float panX = worldPosBefore[0] - worldPosAfter[0];
float panY = worldPosBefore[1] - worldPosAfter[1];
// 5. 应用平移
camera.move(panX, panY);
// 6. 更新面板的缩放状态变量,禁用平滑缩放以确保一致性
displayScale = newZoom;
targetScale = newZoom;
});
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
@@ -742,6 +903,21 @@ public class ModelRenderPanel extends JPanel {
final int screenY = e.getY();
requestFocusInWindow();
if (SwingUtilities.isMiddleMouseButton(e)) {
cameraDragging = true;
lastCameraDragX = screenX;
lastCameraDragY = screenY;
// 记录摄像机起始位置
Camera camera = ModelRender.getCamera();
cameraStartX = camera.getPosition().x;
cameraStartY = camera.getPosition().y;
// 设置拖拽光标
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
return;
}
shiftDuringDrag = e.isShiftDown();
executeInGLContext(() -> {
@@ -845,12 +1021,57 @@ public class ModelRenderPanel extends JPanel {
}
}
}
// 更新拖拽过程中的光标
updateCursorForDragMode(currentDragMode);
} catch (Exception ex) {
logger.error("处理鼠标按下时出错", ex);
}
});
}
/**
* 根据拖拽模式更新光标
*/
private void updateCursorForDragMode(DragMode dragMode) {
Cursor newCursor = getCursorForDragMode(dragMode);
if (!newCursor.equals(currentCursor)) {
currentCursor = newCursor;
SwingUtilities.invokeLater(() -> setCursor(newCursor));
}
}
/**
* 根据拖拽模式获取对应的光标
*/
private Cursor getCursorForDragMode(DragMode dragMode) {
switch (dragMode) {
case MOVE:
return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
case RESIZE_LEFT:
case RESIZE_RIGHT:
return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
case RESIZE_TOP:
case RESIZE_BOTTOM:
return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
case RESIZE_TOP_LEFT:
case RESIZE_BOTTOM_RIGHT:
return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
case RESIZE_TOP_RIGHT:
case RESIZE_BOTTOM_LEFT:
return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
case ROTATE:
// 使用手型光标表示旋转
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
case MOVE_PIVOT:
// 使用十字光标表示移动中心点
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
case NONE:
default:
return Cursor.getDefaultCursor();
}
}
/**
* 处理多选逻辑
@@ -922,43 +1143,116 @@ public class ModelRenderPanel extends JPanel {
fromMesh.getName(), toMesh.getName(), selectedMeshes.size());
}
/**
* 将 2D 向量 (Vector2f) 乘以 3x3 矩阵 (Matrix3f)。
*
* @param v 要变换的向量 (局部坐标)
* @param m 变换矩阵 (世界变换)
* @return 变换后的新向量 (世界坐标)
*/
private Vector2f transformVector2f(Vector2f v, Matrix3f m) {
float x = v.x;
float y = v.y;
// 计算新的 x' 和 y'
// x' = m00*x + m01*y + m02
// y' = m10*x + m11*y + m12
float newX = m.m00() * x + m.m01() * y + m.m02();
float newY = m.m10() * x + m.m11() * y + m.m12();
// 直接修改原向量并返回
return v.set(newX, newY);
}
/**
* 计算指定网格在世界坐标系下的边界框。
*/
private BoundingBox getWorldBounds(Mesh2D mesh) {
ModelPart part = findPartByMesh(mesh);
if (part == null) {
mesh.updateBounds(); // 至少保证局部边界框是新的
return mesh.getBounds();
}
// 确保 ModelPart 的世界变换是最新的
Matrix3f worldTransform = part.getWorldTransform();
// 获取局部边界框
mesh.updateBounds();
BoundingBox localBounds = mesh.getBounds();
// 转换四个角到世界坐标
Vector2f min = new Vector2f(localBounds.getMinX(), localBounds.getMinY());
Vector2f max = new Vector2f(localBounds.getMaxX(), localBounds.getMaxY());
Vector2f p1 = new Vector2f(min.x, max.y); // 左上
Vector2f p2 = new Vector2f(max.x, min.y); // 右下
// 应用世界变换:使用手动实现的 transformVector2f 方法
transformVector2f(min, worldTransform);
transformVector2f(max, worldTransform);
transformVector2f(p1, worldTransform);
transformVector2f(p2, worldTransform);
// 创建新的世界边界框并扩展
BoundingBox worldBounds = new BoundingBox();
worldBounds.expand(min.x, min.y);
worldBounds.expand(max.x, max.y);
worldBounds.expand(p1.x, p1.y);
worldBounds.expand(p2.x, p2.y);
return worldBounds;
}
/**
* 检查是否点击了选择框的调整手柄
*/
private DragMode checkResizeHandleHit(float modelX, float modelY, Mesh2D targetMesh) {
if (targetMesh == null) return DragMode.NONE;
if (targetMesh == null) {
return DragMode.NONE;
}
BoundingBox bounds;
Vector2f center;
// 多选状态下使用多选边界框
// 多选状态下使用多选边界框
if (targetMesh.isInMultiSelection()) {
bounds = targetMesh.getMultiSelectionBounds();
center = bounds.getCenter();
} else {
targetMesh.updateBounds();
bounds = targetMesh.getBounds();
center = targetMesh.getPivot();
}
// 获取摄像机偏移
Vector2f camOffset = ModelRender.getCameraOffset();
// 应用偏移,将 model 坐标转换到相对于摄像机的坐标系
float checkX = modelX - camOffset.x;
float checkY = modelY - camOffset.y;
// 将中心点也转换到相同坐标系
center = new Vector2f(center).sub(camOffset);
float scaleFactor = calculateScaleFactor();
float borderThickness = BORDER_THICKNESS / scaleFactor;
float cornerSize = CORNER_SIZE / scaleFactor;
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
// 动态计算检测阈值,基于面板缩放比例
float scaleFactor = calculateScaleFactor();
float borderThickness = BORDER_THICKNESS / scaleFactor;
float cornerSize = CORNER_SIZE / scaleFactor;
DragMode result = DragMode.NONE;
// 首先检查是否点击了中心点(移动中心点)
if (isPointInCenterHandle(modelX, modelY, center.x, center.y, cornerSize)) {
return DragMode.MOVE_PIVOT;
// 检查中心点
if (isPointInCenterHandle(checkX, checkY, center.x, center.y, cornerSize)) {
result = DragMode.MOVE_PIVOT;
}
// 检查是否点击了旋转手柄
if (isPointInRotationHandle(modelX, modelY, center.x, center.y, minY, cornerSize)) {
return DragMode.ROTATE;
// 检查旋转手柄
if (result == DragMode.NONE && isPointInRotationHandle(checkX, checkY, center.x, center.y, minY, cornerSize)) {
result = DragMode.ROTATE;
}
// 扩展边界以包含调整手柄区域
@@ -967,31 +1261,44 @@ public class ModelRenderPanel extends JPanel {
float expandedMaxX = maxX + borderThickness;
float expandedMaxY = maxY + borderThickness;
// 检查是否在扩展边界内
if (modelX < expandedMinX || modelX > expandedMaxX ||
modelY < expandedMinY || modelY > expandedMaxY) {
return DragMode.NONE;
if (result == DragMode.NONE) {
if (checkX < expandedMinX || checkX > expandedMaxX ||
checkY < expandedMinY || checkY > expandedMaxY) {
return DragMode.NONE;
}
}
// 检查角点
if (isPointInCorner(modelX, modelY, minX, minY, cornerSize)) return DragMode.RESIZE_TOP_LEFT;
if (isPointInCorner(modelX, modelY, maxX, minY, cornerSize)) return DragMode.RESIZE_TOP_RIGHT;
if (isPointInCorner(modelX, modelY, minX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_LEFT;
if (isPointInCorner(modelX, modelY, maxX, maxY, cornerSize)) return DragMode.RESIZE_BOTTOM_RIGHT;
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, minY, cornerSize)) {
result = DragMode.RESIZE_TOP_LEFT;
}
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, minY, cornerSize)) {
result = DragMode.RESIZE_TOP_RIGHT;
}
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, minX, maxY, cornerSize)) {
result = DragMode.RESIZE_BOTTOM_LEFT;
}
if (result == DragMode.NONE && isPointInCorner(checkX, checkY, maxX, maxY, cornerSize)) {
result = DragMode.RESIZE_BOTTOM_RIGHT;
}
// 检查边
if (modelX >= minX - borderThickness && modelX <= minX + borderThickness)
return DragMode.RESIZE_LEFT;
if (modelX >= maxX - borderThickness && modelX <= maxX + borderThickness)
return DragMode.RESIZE_RIGHT;
if (modelY >= minY - borderThickness && modelY <= minY + borderThickness)
return DragMode.RESIZE_TOP;
if (modelY >= maxY - borderThickness && modelY <= maxY + borderThickness)
return DragMode.RESIZE_BOTTOM;
if (result == DragMode.NONE) {
if (checkX >= minX - borderThickness && checkX <= minX + borderThickness) {
result = DragMode.RESIZE_LEFT;
} else if (checkX >= maxX - borderThickness && checkX <= maxX + borderThickness) {
result = DragMode.RESIZE_RIGHT;
} else if (checkY >= minY - borderThickness && checkY <= minY + borderThickness) {
result = DragMode.RESIZE_TOP;
} else if (checkY >= maxY - borderThickness && checkY <= maxY + borderThickness) {
result = DragMode.RESIZE_BOTTOM;
}
}
return DragMode.NONE;
return result;
}
/**
* 检查点是否在中心点区域内
*/
@@ -1045,6 +1352,34 @@ public class ModelRenderPanel extends JPanel {
* 处理鼠标拖拽事件
*/
private void handleMouseDragged(MouseEvent e) {
if (cameraDragging) {
final int screenX = e.getX();
final int screenY = e.getY();
// 计算鼠标移动距离
final int deltaX = screenX - lastCameraDragX;
final int deltaY = screenY - lastCameraDragY;
// 更新最后拖拽位置
lastCameraDragX = screenX;
lastCameraDragY = screenY;
// 确保在 GL 上下文线程中执行摄像机移动
executeInGLContext(() -> {
try {
Camera camera = ModelRender.getCamera();
float zoom = camera.getZoom();
// 计算世界坐标的移动量(反向移动)
float worldDeltaX = -deltaX / zoom;
float worldDeltaY = deltaY / zoom; // AWT/Swing 的 Y 轴与 OpenGL 相反
// 应用摄像机移动
camera.move(worldDeltaX, worldDeltaY);
} catch (Exception ex) {
logger.error("处理摄像机拖拽时出错", ex);
}
});
return;
}
if (currentDragMode == DragMode.NONE) return;
final int screenX = e.getX();
@@ -1241,15 +1576,16 @@ public class ModelRenderPanel extends JPanel {
/**
* 更新所有选中网格的多选边界框
*/
@Deprecated(forRemoval = true)
private void updateMultiSelectionBoundsForSelectedMeshes() {
if (selectedMeshes.size() <= 1) return;
for (Mesh2D mesh : selectedMeshes) {
if (mesh.isInMultiSelection()) {
mesh.updateBounds();
mesh.forceUpdateMultiSelectionBounds();
}
}
//if (selectedMeshes.size() <= 1) return;
//
//for (Mesh2D mesh : selectedMeshes) {
// if (mesh.isInMultiSelection()) {
// mesh.updateBounds();
// mesh.forceUpdateMultiSelectionBounds();
// }
//}
}
@@ -1257,37 +1593,43 @@ public class ModelRenderPanel extends JPanel {
* 处理鼠标释放事件(结束拖拽并记录操作历史)
*/
private void handleMouseReleased(MouseEvent e) {
if (cameraDragging && SwingUtilities.isMiddleMouseButton(e)) {
cameraDragging = false;
// 恢复悬停状态的光标
updateCursorForHoverState();
return;
}
if (currentDragMode != DragMode.NONE) {
// 记录操作历史
//executeInGLContext(() -> {
try {
List<ModelPart> selectedParts = getSelectedParts();
switch (currentDragMode) {
case MOVE:
if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) {
recordDragEnd(selectedParts, new HashMap<>(dragStartPositions));
}
break;
case ROTATE:
if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) {
recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations));
}
break;
case MOVE_PIVOT:
if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) {
recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots));
}
break;
default:
if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) {
recordResizeEnd(selectedParts, new HashMap<>(dragStartScales));
}
break;
}
} catch (Exception ex) {
logger.error("记录操作历史时出错", ex);
try {
List<ModelPart> selectedParts = getSelectedParts();
switch (currentDragMode) {
case MOVE:
if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) {
recordDragEnd(selectedParts, new HashMap<>(dragStartPositions));
}
break;
case ROTATE:
if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) {
recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations));
}
break;
case MOVE_PIVOT:
if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) {
recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots));
}
break;
default:
if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) {
recordResizeEnd(selectedParts, new HashMap<>(dragStartScales));
}
break;
}
} catch (Exception ex) {
logger.error("记录操作历史时出错", ex);
}
//});
}
@@ -1302,6 +1644,9 @@ public class ModelRenderPanel extends JPanel {
dragStartScales.clear();
dragStartRotations.clear();
dragStartPivots.clear();
// 恢复悬停状态的光标
updateCursorForHoverState();
}
/**
@@ -1390,6 +1735,10 @@ public class ModelRenderPanel extends JPanel {
final int screenX = e.getX();
final int screenY = e.getY();
if (cameraDragging) {
return;
}
// 在 GL 上下文线程中执行悬停检测
executeInGLContext(() -> {
try {
@@ -1417,45 +1766,116 @@ public class ModelRenderPanel extends JPanel {
}
}
// 更新鼠标手势
updateCursorForHoverState(modelX, modelY, newHoveredMesh);
} catch (Exception ex) {
logger.error("处理鼠标移动时出错", ex);
}
});
}
/**
* 根据悬停状态更新光标(无坐标版本,用于鼠标释放后)
*/
private void updateCursorForHoverState() {
Point mousePos = getMousePosition();
if (mousePos != null) {
float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y);
if (modelCoords != null) {
updateCursorForHoverState(modelCoords[0], modelCoords[1], hoveredMesh);
}
} else {
// 鼠标不在面板内,恢复默认光标
setCursor(Cursor.getDefaultCursor());
}
}
/**
* 根据悬停状态更新光标
*/
private void updateCursorForHoverState(float modelX, float modelY, Mesh2D hoveredMesh) {
// 如果正在拖拽,不更新光标
if (currentDragMode != DragMode.NONE) {
return;
}
Cursor newCursor = Cursor.getDefaultCursor();
isOverSelection = false;
// 检查是否在选中的网格上
if (!selectedMeshes.isEmpty()) {
// 多选时只对最后一个选中的网格进行操作
Mesh2D targetMeshForHandle = lastSelectedMesh;
if (targetMeshForHandle != null) {
DragMode hoverMode = checkResizeHandleHit(modelX, modelY, targetMeshForHandle);
if (hoverMode != DragMode.NONE) {
newCursor = getCursorForDragMode(hoverMode);
isOverSelection = true;
} else {
// 检查是否在选中网格的边界框内(但不是调整手柄)
BoundingBox bounds;
if (targetMeshForHandle.isInMultiSelection()) {
bounds = targetMeshForHandle.getMultiSelectionBounds();
} else {
bounds = targetMeshForHandle.getBounds();
}
if (modelX >= bounds.getMinX() && modelX <= bounds.getMaxX() &&
modelY >= bounds.getMinY() && modelY <= bounds.getMaxY()) {
newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
isOverSelection = true;
}
}
}
}
// 如果没有在选中的网格上,检查是否在可悬停的网格上
if (!isOverSelection && hoveredMesh != null) {
newCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
// 更新光标
if (!newCursor.equals(currentCursor)) {
currentCursor = newCursor;
Cursor finalNewCursor = newCursor;
SwingUtilities.invokeLater(() -> setCursor(finalNewCursor));
}
}
/**
* 将屏幕坐标转换为模型坐标
*/
private float[] screenToModelCoordinates(int screenX, int screenY) {
if (width <= 0 || height <= 0) return null;
public float[] screenToModelCoordinates(int screenX, int screenY) {
if (!contextInitialized || this.width <= 0 || this.height <= 0) return null;
int panelWidth = getWidth();
int panelHeight = getHeight();
if (panelWidth <= 0 || panelHeight <= 0) return null;
// 1. 将 Swing 坐标缩放到 GL 上下文坐标
float glX = (float) screenX * this.width / getWidth();
float glY = (float) screenY * this.height / getHeight();
// 1. 屏幕坐标转换为离屏缓冲坐标
float scaleX = (float) width / panelWidth;
float scaleY = (float) height / panelHeight;
float bufferX = screenX * scaleX;
float bufferY = screenY * scaleY; // Y轴不反转
// 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;
// 2. 缓冲坐标转换为标准化设备坐标 (NDC)
float ndcX = (bufferX / width) * 2.0f - 1.0f;
float ndcY = (bufferY / height) * 2.0f - 1.0f;
// 3. 逆投影变换
Camera camera = ModelRender.getCamera();
float zoom = camera.getZoom();
Vector2f pos = camera.getPosition();
// 3. NDC 转换为模型坐标(考虑当前显示缩放)
float modelX = ndcX * (width / 2.0f) / displayScale;
float modelY = ndcY * (height / 2.0f) / displayScale;
// 逆变换公式:
// modelX = (ndcX / (2.0f / this.width)) / zoom + pos.x
// modelY = (ndcY / (-2.0f / this.height)) / zoom + pos.y
float[] result = new float[]{modelX, modelY};
float modelX = (ndcX * this.width / (2.0f * zoom)) + pos.x;
float modelY = (ndcY * this.height / (-2.0f * zoom)) + pos.y;
// 调试日志
logger.debug("坐标转换: 屏幕({}, {}) -> 缓冲({}, {}) -> NDC({}, {}) -> 模型({}, {}), 缩放: {}",
screenX, screenY, bufferX, bufferY, ndcX, ndcY, modelX, modelY, displayScale);
return result;
return new float[]{modelX, modelY};
}
/**
* 在指定位置查找网格
*/
@@ -1467,40 +1887,54 @@ public class ModelRenderPanel extends JPanel {
}
try {
// 使用 getParts() 获取所有部件
// 获取摄像机偏移
Vector2f camOffset = ModelRender.getCameraOffset();
// 将输入坐标调整到相对于摄像机的坐标系
float checkX = modelX - camOffset.x;
float checkY = modelY - camOffset.y;
java.util.List<ModelPart> parts = model.getParts();
if (parts == null || parts.isEmpty()) {
logger.debug("模型没有部件列表");
return null;
}
// 遍历所有部件和网格(从上到下)
for (int i = parts.size() - 1; i >= 0; i--) {
ModelPart part = parts.get(i);
if (part != null && part.isVisible()) {
java.util.List<Mesh2D> meshes = part.getMeshes();
for (Mesh2D mesh : meshes) {
if (mesh != null && mesh.isVisible()) {
if (mesh.isDirty()) {
mesh.updateBounds();
}
boolean contains = mesh.containsPoint(modelX, modelY);
if (contains) {
//logger.info("选中网格: {} (在部件 {})", mesh.getName(), part.getName());
return mesh;
}
}
if (part == null || !part.isVisible()) continue;
java.util.List<Mesh2D> meshes = part.getMeshes();
if (meshes == null || meshes.isEmpty()) continue;
for (int m = meshes.size() - 1; m >= 0; m--) {
Mesh2D mesh = meshes.get(m);
if (mesh == null || !mesh.isVisible()) continue;
if (mesh.isDirty()) {
mesh.updateBounds();
}
boolean contains = false;
try {
contains = mesh.containsPoint(checkX, checkY);
} catch (Exception ex) {
logger.warn("mesh.containsPoint 抛出异常: {}", ex.getMessage());
}
if (contains) {
return mesh;
}
}
}
//logger.debug("未找到包含点的网格");
return null;
} catch (Exception e) {
logger.error("检测网格时出错", e);
return null;
}
}
/**
* 获取模型的边界框
*/
@@ -2013,7 +2447,7 @@ public class ModelRenderPanel extends JPanel {
*/
public void dispose() {
running = false;
cameraDragging = false;
// 停止任务执行器
taskExecutor.shutdown();

View File

@@ -714,7 +714,7 @@ public class ModelPart {
/**
* 立即重新计算本节点的 worldTransform并递归到子节点
*/
private void recomputeWorldTransformRecursive() {
public void recomputeWorldTransformRecursive() {
if (transformDirty) {
updateLocalTransform();
}

View File

@@ -1,5 +1,6 @@
package com.chuangzhou.vivid2D.render.model.util;
import com.chuangzhou.vivid2D.render.systems.MultiSelectionBoxRenderer;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
@@ -417,17 +418,17 @@ public class Mesh2D {
* 检查点是否在网格内(可选择精确检测)
*/
public boolean containsPoint(float x, float y, boolean precise) {
if (isInMultiSelection()) {
BoundingBox multiBounds = getMultiSelectionBounds();
boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() &&
y >= multiBounds.getMinY() && y <= multiBounds.getMaxY();
if (precise && inBounds) {
// 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内
return isPointInAnySelectedMesh(x, y);
}
return inBounds;
}
//if (isInMultiSelection()) {
// BoundingBox multiBounds = getMultiSelectionBounds();
// boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() &&
// y >= multiBounds.getMinY() && y <= multiBounds.getMaxY();
//
// if (precise && inBounds) {
// // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内
// return isPointInAnySelectedMesh(x, y);
// }
// return inBounds;
//}
BoundingBox b = getBounds();
boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY();
@@ -757,60 +758,9 @@ public class Mesh2D {
}
}
private void drawSelectBox(){
private void drawSelectBox() {
BoundingBox bounds = getBounds();
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
BufferBuilder bb = new BufferBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
final float CORNER_SIZE = 8.0f;
final float BORDER_THICKNESS = 6.0f;
float expand = 4.0f * 2.0f;
// 第1层外发光边框
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 0.4f));
bb.vertex(minX - expand, minY - expand, 0.0f, 0.0f);
bb.vertex(maxX + expand, minY - expand, 0.0f, 0.0f);
bb.vertex(maxX + expand, maxY + expand, 0.0f, 0.0f);
bb.vertex(minX - expand, maxY + expand, 0.0f, 0.0f);
bb.endImmediate();
// 第2层主边框实心粗边框- 使用明亮的青色
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 1.0f));
float mainExpand = 1.0f;
bb.vertex(minX - mainExpand, minY - mainExpand, 0.0f, 0.0f);
bb.vertex(maxX + mainExpand, minY - mainExpand, 0.0f, 0.0f);
bb.vertex(maxX + mainExpand, maxY + mainExpand, 0.0f, 0.0f);
bb.vertex(minX - mainExpand, maxY + mainExpand, 0.0f, 0.0f);
bb.endImmediate();
// 第3层内边框 - 使用白色增加对比度
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
bb.vertex(minX, minY, 0.0f, 0.0f);
bb.vertex(maxX, minY, 0.0f, 0.0f);
bb.vertex(maxX, maxY, 0.0f, 0.0f);
bb.vertex(minX, maxY, 0.0f, 0.0f);
bb.endImmediate();
// 第4层绘制角点标记和边线
drawResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS);
// 新增:绘制中心点
drawCenterPoint(bb, minX, minY, maxX, maxY);
drawRotationHandle(bb, minX, minY, maxX, maxY);
MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot);
}
/**
@@ -922,25 +872,8 @@ public class Mesh2D {
* 在多选状态下绘制组合边界框
*/
private void drawMultiSelectionBox() {
if (!isInMultiSelection()) {
drawSelectBox();
return;
}
BoundingBox multiBounds = getMultiSelectionBounds();
if (!multiBounds.isValid()) return;
float minX = multiBounds.getMinX();
float minY = multiBounds.getMinY();
float maxX = multiBounds.getMaxX();
float maxY = multiBounds.getMaxY();
BufferBuilder bb = new BufferBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
drawDashedBorder(bb, minX, minY, maxX, maxY);
final float CORNER_SIZE = 8.0f;
final float BORDER_THICKNESS = 6.0f;
drawMultiSelectionResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS);
drawMultiSelectionCenterPoint(bb, minX, minY, maxX, maxY);
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds);
}
/**
@@ -959,11 +892,7 @@ public class Mesh2D {
// 绘制右边虚线
drawDashedLine(bb, maxX, minY, maxX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
// 绘制下边虚线
drawDashedLine(bb, maxX, maxY, minX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
// 绘制左边虚线
drawDashedLine(bb, minX, maxY, minX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR);
}

View File

@@ -0,0 +1,67 @@
package com.chuangzhou.vivid2D.render.systems;
import org.joml.Vector2f;
/**
* 摄像机类
* @author tzdwindows 7
*/
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 boolean enabled = true;
public Camera() {}
public void setPosition(float x, float y) {
position.set(x, y);
}
public void setPosition(Vector2f pos) {
position.set(pos);
}
public Vector2f getPosition() {
return new Vector2f(position);
}
public void setZoom(float zoom) {
this.zoom = Math.max(0.1f, Math.min(10.0f, zoom));
}
public float getZoom() {
return zoom;
}
public void setZPosition(float z) {
this.zPosition = z;
}
public float getZPosition() {
return zPosition;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
public void move(float dx, float dy) {
position.add(dx, dy);
}
public void zoom(float factor) {
zoom *= factor;
zoom = Math.max(0.1f, Math.min(10.0f, zoom));
}
public void reset() {
position.set(0.0f, 0.0f);
zoom = 1.0f;
zPosition = 0.0f;
}
}

View File

@@ -0,0 +1,335 @@
package com.chuangzhou.vivid2D.render.systems;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
import org.joml.Vector2f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
/**
* 现代化选择框渲染器(性能优化版)
* 主要优化点:
* 1) 复用 Tesselator 单例 BufferBuilder减少频繁的 GPU 资源创建/销毁
* 2) 批量提交顶点:把同一 primitiveLINES / TRIANGLES / LINE_LOOP与同一颜色的顶点尽量合并到一次 begin/end
* 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效
* 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观
*
* 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。
*/
public class MultiSelectionBoxRenderer {
// 常量定义(视觉可调)
public static final float DEFAULT_CORNER_SIZE = 10.0f;
public static final float DEFAULT_BORDER_THICKNESS = 6.0f;
public static final float DEFAULT_DASH_LENGTH = 10.0f;
public static final float DEFAULT_GAP_LENGTH = 6.0f;
public static final float ROTATION_HANDLE_DISTANCE = 28.0f;
public static final float HANDLE_ROUNDNESS = 1.5f; // 保留,用于未来改进圆角手柄
// 颜色(更现代的配色)
public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线
public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.18f); // 轻微外发光
public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框,青色
public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内边框,接近白
public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白
public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); // 黄色手柄
public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红
public static final Vector4f ROTATION_HANDLE_COLOR = new Vector4f(0.14f, 0.95f, 0.3f, 1.0f); // 绿色旋转手柄
public static final Vector4f SHADOW_COLOR = new Vector4f(0f, 0f, 0f, 0.18f); // 阴影/背板
/**
* 绘制单选状态下的选择框(高效批处理)
*/
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
if (!bounds.isValid()) return;
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder bb = tesselator.getBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
// 1) 阴影底板(轻微偏移)
bb.begin(RenderSystem.GL_TRIANGLES, 6);
bb.setColor(SHADOW_COLOR);
addFilledQuadTriangles(bb, minX + 4f, minY + 4f, maxX + 4f, maxY + 4f);
tesselator.end();
// 2) 外发光边框(更柔和)
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(SOLID_BORDER_COLOR_OUTER);
bb.vertex(minX - 6.0f, minY - 6.0f, 0.0f, 0.0f);
bb.vertex(maxX + 6.0f, minY - 6.0f, 0.0f, 0.0f);
bb.vertex(maxX + 6.0f, maxY + 6.0f, 0.0f, 0.0f);
bb.vertex(minX - 6.0f, maxY + 6.0f, 0.0f, 0.0f);
tesselator.end();
// 3) 主边框 + 内边框(两个 LINE_LOOP
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(SOLID_BORDER_COLOR_MAIN);
bb.vertex(minX - 1.0f, minY - 1.0f, 0.0f, 0.0f);
bb.vertex(maxX + 1.0f, minY - 1.0f, 0.0f, 0.0f);
bb.vertex(maxX + 1.0f, maxY + 1.0f, 0.0f, 0.0f);
bb.vertex(minX - 1.0f, maxY + 1.0f, 0.0f, 0.0f);
tesselator.end();
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
bb.setColor(SOLID_BORDER_COLOR_INNER);
bb.vertex(minX, minY, 0.0f, 0.0f);
bb.vertex(maxX, minY, 0.0f, 0.0f);
bb.vertex(maxX, maxY, 0.0f, 0.0f);
bb.vertex(minX, maxY, 0.0f, 0.0f);
tesselator.end();
// 4) 手柄(一次性 TRIANGLES 批次绘制所有手柄)
// 8 个手柄(四角 + 四边中点),每个 6 个顶点
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
bb.setColor(HANDLE_COLOR);
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); // 左上
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); // 右上
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); // 左下
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); // 右下
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); // 上中
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); // 下中
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 左中
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 右中
tesselator.end();
// 5) 中心点(十字 + 圆环)
// 十字LINES
bb.begin(GL11.GL_LINES, 4);
bb.setColor(CENTER_POINT_COLOR);
bb.vertex(pivot.x - 6.0f, pivot.y, 0.0f, 0.0f);
bb.vertex(pivot.x + 6.0f, pivot.y, 0.0f, 0.0f);
bb.vertex(pivot.x, pivot.y - 6.0f, 0.0f, 0.0f);
bb.vertex(pivot.x, pivot.y + 6.0f, 0.0f, 0.0f);
tesselator.end();
// 圆环LINE_LOOP
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
bb.setColor(CENTER_POINT_COLOR);
float radius = 6.0f * 0.85f;
for (int i = 0; i < 16; i++) {
float angle = (float) (i * 2f * Math.PI / 16f);
bb.vertex(pivot.x + (float) Math.cos(angle) * radius, pivot.y + (float) Math.sin(angle) * radius, 0.0f, 0.0f);
}
tesselator.end();
// 6) 旋转手柄(连线 + 圆 + 箭头),分三次提交但数量小
float topY = bounds.getMinY();
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
// 连线
bb.begin(GL11.GL_LINES, 2);
bb.setColor(ROTATION_HANDLE_COLOR);
bb.vertex(pivot.x, topY, 0.0f, 0.0f);
bb.vertex(pivot.x, rotationHandleY, 0.0f, 0.0f);
tesselator.end();
// 圆
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
bb.setColor(ROTATION_HANDLE_COLOR);
float handleRadius = 6.0f;
for (int i = 0; i < 16; i++) {
float angle = (float) (i * 2f * Math.PI / 16f);
bb.vertex(pivot.x + (float) Math.cos(angle) * handleRadius, rotationHandleY + (float) Math.sin(angle) * handleRadius, 0.0f, 0.0f);
}
tesselator.end();
// 箭头(两条交叉线,提示旋转)
bb.begin(GL11.GL_LINES, 4);
bb.setColor(ROTATION_HANDLE_COLOR);
float arrow = 4.0f;
bb.vertex(pivot.x - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
bb.vertex(pivot.x + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
bb.vertex(pivot.x + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
bb.vertex(pivot.x - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
tesselator.end();
}
/**
* 绘制多选框(现代化外观,批量提交)
*/
public static void drawMultiSelectionBox(BoundingBox multiBounds) {
if (!multiBounds.isValid()) return;
float minX = multiBounds.getMinX();
float minY = multiBounds.getMinY();
float maxX = multiBounds.getMaxX();
float maxY = multiBounds.getMaxY();
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder bb = tesselator.getBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
// 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次
int estimatedSegments = Math.max(4,
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
bb.begin(GL11.GL_LINES, estimatedSegments * 2);
bb.setColor(DASHED_BORDER_COLOR);
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
tesselator.end();
// 手柄(一次性 TRIANGLES 批次)
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
bb.setColor(MULTI_SELECTION_HANDLE_COLOR);
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE);
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE);
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE);
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE);
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS);
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS);
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
tesselator.end();
// 中心点
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
bb.begin(GL11.GL_LINES, 4);
bb.setColor(CENTER_POINT_COLOR);
bb.vertex(center.x - 6.0f, center.y, 0.0f, 0.0f);
bb.vertex(center.x + 6.0f, center.y, 0.0f, 0.0f);
bb.vertex(center.x, center.y - 6.0f, 0.0f, 0.0f);
bb.vertex(center.x, center.y + 6.0f, 0.0f, 0.0f);
tesselator.end();
// 旋转手柄(沿用单选逻辑)
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
}
// ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder ------
// 向当前 TRIANGLES 批次添加一个填充矩形(两三角形)
private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) {
// 三角形 1
bb.vertex(x0, y0, 0.0f, 0.0f);
bb.vertex(x1, y0, 0.0f, 0.0f);
bb.vertex(x1, y1, 0.0f, 0.0f);
// 三角形 2
bb.vertex(x1, y1, 0.0f, 0.0f);
bb.vertex(x0, y1, 0.0f, 0.0f);
bb.vertex(x0, y0, 0.0f, 0.0f);
}
// 向当前 TRIANGLES 批次添加一个手柄方块(中心在 cx,cy边长 size
private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) {
float half = size / 2f;
addFilledQuadTriangles(bb, cx - half, cy - half, cx + half, cy + half);
}
// 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin()
private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY,
float dashLen, float gapLen) {
float dx = endX - startX;
float dy = endY - startY;
float len = (float) Math.sqrt(dx * dx + dy * dy);
if (len < 0.001f) return;
float dirX = dx / len, dirY = dy / len;
float seg = dashLen + gapLen;
int count = (int) Math.ceil(len / seg);
for (int i = 0; i < count; i++) {
float s = i * seg;
if (s >= len) break;
float e = Math.min(s + dashLen, len);
float sx = startX + dirX * s;
float sy = startY + dirY * s;
float ex = startX + dirX * e;
float ey = startY + dirY * e;
bb.vertex(sx, sy, 0.0f, 0.0f);
bb.vertex(ex, ey, 0.0f, 0.0f);
}
}
// 适配:在 multi selection 中把旋转手柄渲染写入到传入的 bb会在函数内部使用 tesselator.end()
public static void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
drawRotationHandle(center, new BoundingBox(minX, minY, maxX, maxY));
}
// 单独绘制旋转手柄(内部会 new / begin / end因为包含多种 primitive
private static void drawRotationHandle(Vector2f pivot, BoundingBox bounds) {
float centerX = pivot.x;
float centerY = pivot.y;
float topY = bounds.getMinY();
boolean pivotInBounds = (centerX >= bounds.getMinX() && centerX <= bounds.getMaxX() &&
centerY >= bounds.getMinY() && centerY <= bounds.getMaxY());
if (!pivotInBounds) {
centerX = (bounds.getMinX() + bounds.getMaxX()) * 0.5f;
centerY = (bounds.getMinY() + bounds.getMaxY()) * 0.5f;
topY = bounds.getMinY();
}
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
Tesselator t = Tesselator.getInstance();
BufferBuilder bb = t.getBuilder();
// 连线
bb.begin(GL11.GL_LINES, 2);
bb.setColor(ROTATION_HANDLE_COLOR);
bb.vertex(centerX, topY, 0.0f, 0.0f);
bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f);
t.end();
// 圆环
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
bb.setColor(ROTATION_HANDLE_COLOR);
float r = 6.0f;
for (int i = 0; i < 16; i++) {
float ang = (float) (i * 2f * Math.PI / 16f);
bb.vertex(centerX + (float) Math.cos(ang) * r, rotationHandleY + (float) Math.sin(ang) * r, 0.0f, 0.0f);
}
t.end();
// 箭头
bb.begin(GL11.GL_LINES, 4);
bb.setColor(ROTATION_HANDLE_COLOR);
float arrow = 4.0f;
bb.vertex(centerX - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
bb.vertex(centerX + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
bb.vertex(centerX + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
bb.vertex(centerX - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
t.end();
}
/**
* 仅绘制简化的多选虚线边框(保留单次批量绘制)
*/
public static void drawSimpleMultiSelectionBox(BoundingBox multiBounds) {
if (!multiBounds.isValid()) return;
float minX = multiBounds.getMinX();
float minY = multiBounds.getMinY();
float maxX = multiBounds.getMaxX();
float maxY = multiBounds.getMaxY();
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder bb = tesselator.getBuilder();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
int est = Math.max(4,
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
bb.begin(GL11.GL_LINES, est * 2);
bb.setColor(DASHED_BORDER_COLOR);
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
tesselator.end();
}
}

View File

@@ -4,6 +4,12 @@ import org.lwjgl.opengl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.IntSupplier;
@@ -101,7 +107,182 @@ public final class RenderSystem {
public static final int GL_COMPILE_STATUS = org.lwjgl.opengl.GL20.GL_COMPILE_STATUS;
public static final int GL_LINK_STATUS = org.lwjgl.opengl.GL20.GL_LINK_STATUS;
public static final int GL_VALIDATE_STATUS = org.lwjgl.opengl.GL20.GL_VALIDATE_STATUS;
private static final java.util.Deque<RenderState> stateStack = new java.util.ArrayDeque<>();
/**
* 渲染状态快照类
*/
private static class RenderState {
private int currentProgram;
private boolean blendEnabled;
private boolean depthTestEnabled;
private int blendSrcFactor;
private int blendDstFactor;
private int activeTexture;
private int boundTexture;
private float[] clearColor;
private int[] viewport;
public RenderState() {
setDefaults();
try {
this.currentProgram = getCurrentProgram();
this.blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND);
this.depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST);
java.nio.IntBuffer blendFunc = org.lwjgl.system.MemoryUtil.memAllocInt(2);
try {
GL11.glGetIntegerv(GL11.GL_BLEND_SRC, blendFunc);
this.blendSrcFactor = blendFunc.get(0);
GL11.glGetIntegerv(GL11.GL_BLEND_DST, blendFunc);
this.blendDstFactor = blendFunc.get(0);
} finally {
org.lwjgl.system.MemoryUtil.memFree(blendFunc);
}
java.nio.IntBuffer intBuf = org.lwjgl.system.MemoryUtil.memAllocInt(1);
try {
GL11.glGetIntegerv(GL13.GL_ACTIVE_TEXTURE, intBuf);
this.activeTexture = intBuf.get(0);
GL11.glGetIntegerv(GL11.GL_TEXTURE_BINDING_2D, intBuf);
this.boundTexture = intBuf.get(0);
} finally {
org.lwjgl.system.MemoryUtil.memFree(intBuf);
}
java.nio.FloatBuffer floatBuf = org.lwjgl.system.MemoryUtil.memAllocFloat(4);
try {
GL11.glGetFloatv(GL11.GL_COLOR_CLEAR_VALUE, floatBuf);
this.clearColor = new float[] {
floatBuf.get(0), floatBuf.get(1),
floatBuf.get(2), floatBuf.get(3)
};
} finally {
org.lwjgl.system.MemoryUtil.memFree(floatBuf);
}
java.nio.IntBuffer viewportBuf = org.lwjgl.system.MemoryUtil.memAllocInt(4);
try {
GL11.glGetIntegerv(GL11.GL_VIEWPORT, viewportBuf);
this.viewport = new int[] {
viewportBuf.get(0), viewportBuf.get(1),
viewportBuf.get(2), viewportBuf.get(3)
};
} finally {
org.lwjgl.system.MemoryUtil.memFree(viewportBuf);
}
} catch (Exception e) {
logger.warn("Failed to get render state, using defaults: {}", e.getMessage());
// 如果出现异常我们使用默认值已经在setDefaults中设置所以不需要再次设置
}
}
private void setDefaults() {
this.currentProgram = 0;
this.blendEnabled = false;
this.depthTestEnabled = false;
this.blendSrcFactor = GL11.GL_SRC_ALPHA;
this.blendDstFactor = GL11.GL_ONE_MINUS_SRC_ALPHA;
this.activeTexture = GL13.GL_TEXTURE0;
this.boundTexture = 0;
this.clearColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f};
this.viewport = new int[] {0, 0, viewportWidth, viewportHeight};
}
public void restore() {
try {
// 恢复视口
if (viewport != null && viewport.length == 4) {
GL11.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
}
// 恢复清除颜色
if (clearColor != null && clearColor.length == 4) {
GL11.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
}
// 恢复着色器程序
if (GL20.glIsProgram(currentProgram)) {
GL20.glUseProgram(currentProgram);
} else {
GL20.glUseProgram(0);
}
// 恢复纹理状态 - 使用更安全的方式
if (activeTexture >= GL13.GL_TEXTURE0 && activeTexture <= GL13.GL_TEXTURE31) {
GL13.glActiveTexture(activeTexture);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture);
} else {
// 使用默认纹理单元
GL13.glActiveTexture(GL13.GL_TEXTURE0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, boundTexture);
}
// 恢复混合状态
if (blendEnabled) {
GL11.glEnable(GL11.GL_BLEND);
} else {
GL11.glDisable(GL11.GL_BLEND);
}
// 使用安全的混合函数值
if (isValidBlendFunc(blendSrcFactor) && isValidBlendFunc(blendDstFactor)) {
GL11.glBlendFunc(blendSrcFactor, blendDstFactor);
} else {
// 使用默认混合函数
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
}
// 恢复深度测试状态
if (depthTestEnabled) {
GL11.glEnable(GL11.GL_DEPTH_TEST);
} else {
GL11.glDisable(GL11.GL_DEPTH_TEST);
}
} catch (Exception e) {
logger.error("Error during state restoration: {}", e.getMessage());
}
}
/**
* 检查混合函数值是否有效
*/
private boolean isValidBlendFunc(int func) {
switch (func) {
case GL11.GL_ZERO:
case GL11.GL_ONE:
case GL11.GL_SRC_COLOR:
case GL11.GL_ONE_MINUS_SRC_COLOR:
case GL11.GL_DST_COLOR:
case GL11.GL_ONE_MINUS_DST_COLOR:
case GL11.GL_SRC_ALPHA:
case GL11.GL_ONE_MINUS_SRC_ALPHA:
case GL11.GL_DST_ALPHA:
case GL11.GL_ONE_MINUS_DST_ALPHA:
case GL14.GL_SRC_ALPHA_SATURATE:
return true;
default:
return false;
}
}
@Override
public String toString() {
return "RenderState{" +
"currentProgram=" + currentProgram +
", blendEnabled=" + blendEnabled +
", depthTestEnabled=" + depthTestEnabled +
", blendSrcFactor=" + blendSrcFactor +
", blendDstFactor=" + blendDstFactor +
", activeTexture=" + activeTexture +
", boundTexture=" + boundTexture +
", clearColor=" + Arrays.toString(clearColor) +
", viewport=" + Arrays.toString(viewport);
}
}
// ================== 初始化方法 ==================
public static void initRenderThread() {
@@ -175,6 +356,52 @@ public final class RenderSystem {
}
}
/**
* 保存当前渲染状态到栈中
*/
public static void pushState() {
if (!isOnRenderThread()) {
recordRenderCall(() -> _pushState());
} else {
_pushState();
}
}
private static void _pushState() {
assertOnRenderThread();
stateStack.push(new RenderState());
checkGLError("pushState");
}
/**
* 从栈中恢复之前的渲染状态
*/
public static void popState() {
if (!isOnRenderThread()) {
recordRenderCall(() -> _popState());
} else {
_popState();
}
}
private static void _popState() {
assertOnRenderThread();
if (!stateStack.isEmpty()) {
RenderState state = stateStack.pop();
state.restore();
checkGLError("popState");
} else {
logger.warn("popState called with empty state stack");
}
}
/**
* 获取当前状态栈大小
*/
public static int getStateStackSize() {
return stateStack.size();
}
private static void _enable(int capability) {
assertOnRenderThread();
GL11.glEnable(capability);
@@ -602,6 +829,18 @@ public final class RenderSystem {
}
}
public static ByteBuffer loadWindowsFont(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());
while (buffer.hasRemaining()) {
fc.read(buffer);
}
buffer.flip();
return buffer;
}
}
// 完整的程序链接方法
public static int linkProgram(int vertexShader, int fragmentShader) {
assertOnRenderThread();
@@ -914,29 +1153,52 @@ public final class RenderSystem {
assertOnRenderThread();
int textureId = genTextures();
bindTexture(textureId);
// 创建 1x1 白色纹理
java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4);
try {
buffer.put((byte) 255)
.put((byte) 255)
.put((byte) 255)
.put((byte) 255)
.flip();
texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
} finally {
org.lwjgl.system.MemoryUtil.memFree(buffer);
if (textureId == 0) {
logger.error("Failed to generate texture ID");
return 0;
}
// 设置纹理参数
setTextureMinFilter(GL11.GL_NEAREST);
setTextureMagFilter(GL11.GL_NEAREST);
setTextureWrapS(GL11.GL_REPEAT);
setTextureWrapT(GL11.GL_REPEAT);
bindTexture(textureId);
try {
// 创建 1x1 白色纹理 - 使用更兼容的格式
java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4);
try {
// 填充 RGBA 数据:白色不透明
buffer.put((byte) 0xFF) // R
.put((byte) 0xFF) // G
.put((byte) 0xFF) // B
.put((byte) 0xFF) // A
.flip();
// 使用更兼容的纹理格式组合
// 注意:有些系统可能不支持 GL_RGBA8使用 GL_RGBA 作为内部格式
texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA,
1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
checkGLError("texImage2D in createDefaultTexture");
} finally {
org.lwjgl.system.MemoryUtil.memFree(buffer);
}
// 设置纹理参数
setTextureMinFilter(GL11.GL_NEAREST);
setTextureMagFilter(GL11.GL_NEAREST);
setTextureWrapS(GL12.GL_CLAMP_TO_EDGE);
setTextureWrapT(GL12.GL_CLAMP_TO_EDGE);
checkGLError("texture parameters in createDefaultTexture");
} catch (Exception e) {
logger.error("Error creating default texture: {}", e.getMessage());
// 清理失败的纹理
deleteTextures(textureId);
return 0;
} finally {
bindTexture(0); // 解绑
}
bindTexture(0); // 解绑
return textureId;
}

View File

@@ -3,6 +3,7 @@ package com.chuangzhou.vivid2D.render.systems.sources;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.sources.def.Shader2D;
import com.chuangzhou.vivid2D.render.systems.sources.def.SolidColorShader;
import com.chuangzhou.vivid2D.render.systems.sources.def.TextShader;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
@@ -37,7 +38,8 @@ public class ShaderManagement {
*/
public static final List<CompleteShader> shaderList = List.of(
new Shader2D(),
new SolidColorShader()
new SolidColorShader(),
new TextShader()
);
/**

View File

@@ -0,0 +1,96 @@
package com.chuangzhou.vivid2D.render.systems.sources.def;
import com.chuangzhou.vivid2D.render.systems.sources.*;
import org.joml.Vector4f;
/**
* 文本着色器
* @author tzdwindows 7
*/
public class TextShader implements CompleteShader {
private final VertexShader vertexShader = new VertexShader();
private final FragmentShader fragmentShader = new FragmentShader();
private Vector4f color = new Vector4f(1,1,1,1);
public void setColor(Vector4f color) {
this.color.set(color);
}
@Override
public Shader getVertexShader() {
return vertexShader;
}
@Override
public Shader getFragmentShader() {
return fragmentShader;
}
@Override
public String getShaderName() {
return "TextShader";
}
@Override
public boolean isDefaultShader() {
return false;
}
@Override
public void setDefaultUniforms(ShaderProgram program) {
// 传递颜色 uniform
program.setUniform4f("uColor", color.x, color.y, color.z, color.w);
// 纹理通常绑定到0号纹理单元
program.setUniform1i("uTexture", 0);
}
private static class VertexShader implements Shader {
@Override
public String getShaderCode() {
return """
#version 330 core
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = vec4(aPosition.xy, 0.0, 1.0);
}
""";
}
@Override
public String getShaderName() {
return "TextVertexShader";
}
}
private static class FragmentShader implements Shader {
@Override
public String getShaderCode() {
return """
#version 330 core
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D uTexture;
uniform vec4 uColor;
void main() {
// 使用 .r 通道读取单通道纹理
float alpha = texture(uTexture, vTexCoord).r;
FragColor = vec4(uColor.rgb, uColor.a * alpha);
}
""";
}
@Override
public String getShaderName() {
return "TextFragmentShader";
}
}
}

View File

@@ -37,13 +37,15 @@ public class ModelTest {
// Test 3: Test compressed file operations with textures
testCompressedFileOperationsWithTexture();
//testModelSaveLoadIntegrity(model, "test_model.vmdl")
// Other existing tests...
testAnimationSystem();
testPhysicsSystem();
testComplexTransformations();
testPerformance();
Model2D model = createTestModel();
printModelState(model);
//testAnimationSystem();
//testPhysicsSystem();
//testComplexTransformations();
//testPerformance();
//Model2D model = createTestModel();
//printModelState(model);
} finally {
// Cleanup OpenGL
cleanupOpenGL();
@@ -311,7 +313,6 @@ public class ModelTest {
System.out.println("OpenGL initialized successfully");
System.out.println("OpenGL Version: " + org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION));
glInitialized = true;
} catch (Exception e) {
System.err.println("Failed to initialize OpenGL: " + e.getMessage());
// Continue without OpenGL for other tests
@@ -581,7 +582,6 @@ public class ModelTest {
try {
// Load model
Model2D model = Model2D.loadFromFile("test_character.model");
System.out.println("Testing animation system:");
// Test parameter-driven animation