世界空间 UI 与自由光标输入¶
包:lib.kasuga.rendering.models.mc.backend.ui。所有方法在 Minecraft 客户端线程调用。
设计、坐标变换和输入所有权见 原理。
接入现有 Screen¶
Minecraft mc = Minecraft.getInstance();
UIBackend backend = Constants.UI_BACKEND; // client setup 后可用
Screen panel = new MyPanelScreen();
panel.init(mc, 320, 180); // 世界 UI 的逻辑尺寸,不随输入层窗口尺寸改变
// 面板中心放在相机前 2.5 格,宽 1.92 格;局部 UI 的 +Y 向下。
var camera = mc.gameRenderer.getMainCamera();
var center = camera.getPosition().add(
camera.getLookVector().x * 2.5,
camera.getLookVector().y * 2.5,
camera.getLookVector().z * 2.5);
Matrix4f localToWorld = new Matrix4f()
.translation((float) center.x, (float) center.y, (float) center.z)
.rotate(camera.rotation())
.scale(.006f, -.006f, 1)
.translate(-160, -90, 0);
backend.addRenderable(panel, localToWorld);
backend.openPointer();
Constants.UI_BACKEND 由内置 world_ui 管线在主视角 AFTER_LEVEL 渲染。
openPointer() 要求当前在世界内且没有其他 Screen;从按键绑定调用即可。
从聊天命令调用时先结束聊天 Screen,例如开发示例中的 mc.setScreen(null)。
方法返回已提交给 Minecraft.setScreen 的 WorldUiInputScreen;其他模组仍可取消 Screen opening。
输入层透明、不绘制背景、不暂停世界。原版 Screen 生命周期负责释放鼠标;Escape 关闭输入层,
回到鼠标控制视角。关闭后面板继续存在,恢复准心交互。调用 removeRenderable(panel) 移除面板。
注册项属于 backend,跨世界不会自动删除;世界绑定 UI 应由调用方在离开世界时移除。
正常视角下,用准心点击并聚焦 EditBox 或 MultiLineEditBox 后,backend 临时接管键盘,
字符交给文本框,游戏移动键和快捷键暂停响应;鼠标仍控制视角。Escape 清除文本焦点并退出,
本次 Escape 不打开暂停菜单。退出时仍按住的旧按键,其 repeat/release 会被消费,重新按下后恢复游戏输入。
按钮、禁用/隐藏或只读的文本框不会触发接管;离开世界、失焦、切换 Screen 或删除目标也会结束接管。
拥有自有控件树的面板可实现 WorldUiTextInput,通过 hasTextInputFocus() 报告当前是否有可编辑
文本焦点;收到 setFocused(false) 时必须清除内部焦点,使 Escape、目标移除和切换面板能正常退出。
通用后端不需要依赖第三方控件库。LDLib2 的 LdElement.attach(backend, ui, width, height, transform)
已实现这一适配,并管理世界面板的 tick、关闭与离开世界清理,接入与演示见
LDLib2 世界 UI。
Screen 可正常使用其子控件的焦点、键盘和文本输入。世界面板若需透明背景,应重写自身
renderBackground;输入层的透明与面板自身的外观分别控制。世界渲染期间只临时关闭面板的 blur,
退出渲染后恢复其原值。
控件先绘制到独立的透明 RGBA 画布,沿用原版 GUI 的正交深度范围;之后将画布一次绘制到
世界平面,避免把文字与阴影的微小 Z 间隔压缩到世界透视深度中。画布使用当前 GUI scale
决定像素密度,窗口缩放时重建目标;局部 scissor 按画布尺寸换算。合成使用预乘 alpha,
不会再次压暗透明文字边缘,也不会将面板矩形写入世界深度。
MIN_GUI_Z、MAX_GUI_Z 及边界处的文字阴影只参与画布内部排序;合成顶点始终为局部 Z=0,
不会把面板向玩家方向推出。画布裁剪范围在 GUI 深度端点外各预留 1 单位。
接口与约定¶
| 接口 | 行为 |
|---|---|
addRenderable(Renderable) |
默认在世界原点居中,以当前 GUI 高度作为每格像素数;重复注册不重复绘制 |
addRenderable(Renderable, Matrix4fc) |
注册并复制像素局部坐标到绝对世界坐标的变换 |
setTransform(Renderable, Matrix4fc) |
更新/预设变换;拒绝非有限、非仿射、不可逆矩阵 |
removeRenderable(Renderable) |
停止渲染,并为该目标补发捕获中的松键事件 |
openPointer() |
打开自由光标输入 Screen;无玩家、无世界或已有 Screen 时抛 IllegalStateException |
getInputRouter().setMaxDistance(float) |
最大命中距离,默认 5 格,从渲染近裁剪面起算;必须为有限正数 |
worldChanged() |
清理输入并释放临时 GPU 画布,保留面板注册;全局 backend 已接入世界切换钩子 |
close() |
清理输入、画布与全部注册;自有 backend 的所有者应在销毁时调用 |
resetInput() |
清理 hover、键盘焦点和输入捕获;为已按下的键/鼠标补发 release |
isKeyboardCaptured() |
是否正在正常视角的文本键盘接管模式中 |
WorldUiTextInput.hasTextInputFocus() |
第三方面板报告自身控件树的可编辑文本焦点;失焦时由面板清除内部状态 |
renderAllUis(GuiGraphics, float) |
自定义世界渲染入口;使用当前世界投影和有效 model-view,不能在 HUD 的正交投影阶段调用 |
renderWorld(WorldRenderPipelineContext) |
自有 backend 可从世界管线调用;负责投影/model-view 保存恢复,只接受主视角 |
Screen.width/height 与 LayoutElement 的位置/尺寸决定可点击范围。只有 Renderable、没有
GuiEventListener 或已知尺寸的对象只绘制。平面命中继续使用 RayTracingHelper.intersectRayWithPlane。
多个面板重叠时将事件交给最近的矩形;同距离按注册顺序。命中不检查方块遮挡,也不做纹理 alpha 测试。
透明层吞掉所有传入的鼠标、滚轮和键盘事件;未命中时不会触发世界操作。键盘输入交给最后点击的面板, 面板内部再分发给选中的控件。Escape 始终退出。鼠标按下被面板接受后,为该按钮捕获目标; 移出边界仍在原平面上计算局部坐标并拖拽,松键发回原目标。射线平行或转到背后时暂用最后有效坐标, 仍保证 release。窗口失焦、屏幕替换、目标移除、维度切换和断开连接都会取消捕获。
复用已有射线检测¶
若宿主已有世界命中求解器,可独立使用投影组件,不需要重写相交算法:
var projection = new ScreenPointerProjection(worldProjection, guiWidth, guiHeight);
Vector3f origin = new Vector3f(), direction = new Vector3f();
if (projection.ray(mouseGuiX, mouseGuiY, origin, direction)) {
// viewMatrix 包含相机平移时,inverseView 将输出还原为绝对世界坐标。
inverseView.transformPosition(origin);
inverseView.transformDirection(direction).normalize();
existingRaycast(origin, direction);
}
若 view 只含相机旋转,先用逆矩阵变换,再给 origin 加上相机世界位置。
ray 输出从近裁剪面出发的单位射线,兼容透视和正交投影。传入 GUI 逻辑像素,左上角为 (0,0);
原始 GLFW/window 光标坐标按 guiWidth / screenWidth、guiHeight / screenHeight 换算。
不要把 framebuffer 像素尺寸与 window 光标坐标混用。构造器复制投影并验证可逆性;无效投影或 viewport
抛 IllegalArgumentException,非有限光标返回 false。FOV、缩放和宽高比变化后重新采集渲染投影。
开发验收¶
./gradlew :modules:modelling:modelUnitTest --tests 'lib.kasuga.rendering.models.mc.backend.ui.*' --offline
./gradlew :modules:modelling:compileContentTestingJava --offline
./gradlew :modules:modelling:runClient
进入测试世界后执行 /ksglib debug world_ui:面板固定在开启时的相机前方,输入层立即打开。
移动光标点击面板左右两侧、在文本框中输入、滚动、按住鼠标拖出面板,再松开;检查计数和拖动增量。
Escape 后转动视角,确认准心交互仍可用;再次执行命令会重建面板并开启自由光标。
/ksglib debug world_ui stop 移除面板。
退出自由光标后,用准心点文本框并输入 WASD/中文:文字应变化,玩家不移动,鼠标仍能转动视角。
按住 W 时按 Escape,确认只退出编辑;松开再按 W 应恢复移动。按钮不应接管键盘。
分别执行 /ksglib debug world_ui min_z 和 /ksglib debug world_ui max_z,检查边界处文字/阴影完整,
面板距离不变;这两个预设与默认面板使用相同世界变换。
渲染验收时检查文字主体/阴影、按钮边框和输入框边框没有条纹或缺块,输入较长的中文/ASCII 文本检查裁剪;停止 demo 后确认世界、手部和 HUD 的颜色与深度正常。
窗口大小、GUI scale、FOV、第三人称和失焦切换需在真实客户端手动验收。 JVM 测试覆盖投影数值和输入路由状态,不替代游戏内渲染及 GLFW 验证。
已有隔离测试存档时,可自动检查真实键盘 Mixin、字符输入和极端 Z 的字体/控件像素:
./gradlew :modules:modelling:runClientHeadless --offline \
-PkasugaClientDirectory=run/world-ui-smoke -PkasugaQuickPlayWorld=TestWorld \
-PkasugaTestWorldUiInput=true
TestWorld 需位于该运行目录的 saves/ 下。测试不显示窗口,完成后退出;JSON 和 PNG 位于
该运行目录的 debug/world-ui-input/。此自动化不覆盖系统输入法候选框和玩家操作手感。