Merge remote-tracking branch 'origin/dev' into codex/test-suite-representative-tests

# Conflicts:
#	test/dap.cpp
This commit is contained in:
西风逍遥游 committed 2026-09-13 09:10:03 +08:00
commit 42d9be4eed
29 files changed
+1465 -223

No files matched your search

+7 -3
View File
@@ -295,11 +295,15 @@ WebGPU 使用带 origin 的 `WriteTexture`;两者都不重建 Texture、采样
**源码:** [`src/modules/graphics/`](../../../src/modules/graphics/)
**相关测试:** 在 [`test/`](../../../test/) 中搜索 `graphics`、`Camera2D`、`Camera3D`。
### HDR 与反射探针绑定
### HDR、景深与反射探针绑定
下列绑定用于 HDR 图像、反射探针采集/注册、天空环境、曝光、Bloom 与反射链质量控制:
下列绑定用于 HDR 图像、反射探针采集/注册、天空环境、曝光、Bloom、Gaussian 景深(DOF)与反射链质量控制。
`Camera3D.setDepthOfField(focusDistance, maxBlurPx, focusRange)` 配置最终 HDR resolve 上的可分离 Gaussian 景深;
`maxBlurPx <= 0` 关闭效果。焦点平面用视空间距离表示;`getDofFocusDistance` / `getDofMaxBlur` /
`getDofFocusRange` 读取当前参数。HD-2D 缩微感可配合 `hd2d.Hd2dLook` 一键写入(见 [HD2D](hd2d.md))。
Cutout/masked 精灵需写入深度,DOF 才能正确对焦角色。
`applyConfiguredToCamera`、`applyToCamera`、`clearReflectionProbe`、`configureInfluence`、`filterAndPublish`、`getActiveCubemap`、`getAdaptiveFaceBudget`、`getAdaptiveFilterSamples`、`getBloomIntensity`、`getBloomThreshold`、`getCaptureClusteredLighting`、`getCaptureFarDistance`、`getCaptureLodDistanceScale`、`getCaptureMask`、`getCaptureTransparent`、`getCenterX`、`getCenterY`、`getCenterZ`、`getCount`、`getEnvProbeCenterX`、`getEnvProbeCenterY`、`getEnvProbeCenterZ`、`getEnvProbeExtentX`、`getEnvProbeExtentY`、`getEnvProbeExtentZ`、`getEnvironmentLighting`、`getEnvironmentLightingIntensity`、`getExposure`、`getFaceCanvas`、`getGpuBudgetMs`、`getInfluenceBlendDistance`、`getInfluenceExtentX`、`getInfluenceExtentY`、`getInfluenceExtentZ`、`getInfluenceIntensity`、`getInfluencePriority`、`getLastCandidateCount`、`getLastCapturedFaceCount`、`getLastFilterSampleCount`、`getLastPublishedCount`、`getLastSelectedCount`、`getMaxRoughness`、`getPendingFaceCount`、`getPostProcessQuality`、`getPublishedRevision`、`getReflectionCaptureEnabled`、`getReflectionCaptureMask`、`getReflectionProbeCount`、`getReflectionQuality`、`getRefreshInterval`、`getResolution`、`getResolutionScale`、`getRevision`、`getSelectionHysteresis`、`getSkyB`、`getSkyFaceColor`、`getSkyFaceTexture`、`getSkyFaceTextureScale`、`getSkyG`、`getSkyIntensity`、`getSkyR`、`getSmoothedGpuDurationMs`、`getStagedRevision`、`getStagingCubemap`、`getThickness`、`getTotalCapturedFaceCount`、`getUpdateMode`、`hasEnvProbe`、`isAutoExposure`、`isCaptureComplete`、`isCapturePending`、`isRecaptureQueued`、`newHDRImageData`、`newReflectionProbeCapture`、`newReflectionProbeRegistry`、`queueCapture`、`queueCaptureAABB`、`remove`、`reportGpuDurationMs`、`requestCapture`、`setAutoExposure`、`setBloom`、`setCaptureClusteredLighting`、`setCaptureLodDistanceScale`、`setCaptureMask`、`setCaptureTransparent`、`setEnvironmentLighting`、`setExposure`、`setGpuBudgetMs`、`setMaxRoughness`、`setReflectionCaptureEnabled`、`setReflectionCaptureMask`、`setReflectionProbe`、`setReflectionQuality`、`setRefreshInterval`、`setResolutionScale`、`setSelectionHysteresis`、`setSkyColor`、`setSkyFaceColor`、`setSkyFaceTexture`、`setSkyFaceTextureScale`、`setUpdateMode`、`stageCapturedFaces`、`tick`、`tickAdaptive`、`updateCamera`。
`applyConfiguredToCamera`、`applyToCamera`、`clearReflectionProbe`、`configureInfluence`、`filterAndPublish`、`getActiveCubemap`、`getAdaptiveFaceBudget`、`getAdaptiveFilterSamples`、`getBloomIntensity`、`getBloomThreshold`、`getCaptureClusteredLighting`、`getCaptureFarDistance`、`getCaptureLodDistanceScale`、`getCaptureMask`、`getCaptureTransparent`、`getCenterX`、`getCenterY`、`getCenterZ`、`getCount`、`getDofFocusDistance`、`getDofFocusRange`、`getDofMaxBlur`、`getEnvProbeCenterX`、`getEnvProbeCenterY`、`getEnvProbeCenterZ`、`getEnvProbeExtentX`、`getEnvProbeExtentY`、`getEnvProbeExtentZ`、`getEnvironmentLighting`、`getEnvironmentLightingIntensity`、`getExposure`、`getFaceCanvas`、`getGpuBudgetMs`、`getInfluenceBlendDistance`、`getInfluenceExtentX`、`getInfluenceExtentY`、`getInfluenceExtentZ`、`getInfluenceIntensity`、`getInfluencePriority`、`getLastCandidateCount`、`getLastCapturedFaceCount`、`getLastFilterSampleCount`、`getLastPublishedCount`、`getLastSelectedCount`、`getMaxRoughness`、`getPendingFaceCount`、`getPostProcessQuality`、`getPublishedRevision`、`getReflectionCaptureEnabled`、`getReflectionCaptureMask`、`getReflectionProbeCount`、`getReflectionQuality`、`getRefreshInterval`、`getResolution`、`getResolutionScale`、`getRevision`、`getSelectionHysteresis`、`getSkyB`、`getSkyFaceColor`、`getSkyFaceTexture`、`getSkyFaceTextureScale`、`getSkyG`、`getSkyIntensity`、`getSkyR`、`getSmoothedGpuDurationMs`、`getStagedRevision`、`getStagingCubemap`、`getThickness`、`getTotalCapturedFaceCount`、`getUpdateMode`、`hasEnvProbe`、`isAutoExposure`、`isCaptureComplete`、`isCapturePending`、`isRecaptureQueued`、`newHDRImageData`、`newReflectionProbeCapture`、`newReflectionProbeRegistry`、`queueCapture`、`queueCaptureAABB`、`remove`、`reportGpuDurationMs`、`requestCapture`、`setAutoExposure`、`setBloom`、`setCaptureClusteredLighting`、`setCaptureLodDistanceScale`、`setCaptureMask`、`setCaptureTransparent`、`setDepthOfField`、`setEnvironmentLighting`、`setExposure`、`setGpuBudgetMs`、`setMaxRoughness`、`setReflectionCaptureEnabled`、`setReflectionCaptureMask`、`setReflectionProbe`、`setReflectionQuality`、`setRefreshInterval`、`setResolutionScale`、`setSelectionHysteresis`、`setSkyColor`、`setSkyFaceColor`、`setSkyFaceTexture`、`setSkyFaceTextureScale`、`setUpdateMode`、`stageCapturedFaces`、`tick`、`tickAdaptive`、`updateCamera`。
### 预编译网格 Shader 与画布快照
+29 -2
View File
@@ -4,6 +4,7 @@ HD-2D 模块把 2D 内容叠加到现有 3D 渲染管线,实现「2D tilemap
- `TileMap3D`:把 2D tilemap(`map` 模块的 `TileLayer`)挤成 3D 地形网格。
- `Sprite3D`:把 2D 精灵图/角色帧作为始终正对相机的 billboard 渲染进 3D 场景,支持精灵表帧动画。
- `Hd2dLook`:参考 [HD2DURP](https://github.com/dulong-lab/HD2DURP) 的景深(DopFix)+ Bloom + 像素采样组合,一键写到 `Camera3D`。
模块绑定在 `eve.Hd2D`,脚本槽位为 `hd2d`。
@@ -47,6 +48,11 @@ local scale = tile.getHeightScale(); // 读取当前高度缩放
Pivot 使用可见图像左上角为 (0,0)、右下角为 (1,1),旋转、缩放及 UV 翻转不改变世界锚点。
相机必须在解除绑定前保持有效;退化相机基向量保留最后一次有效朝向。
精灵默认使用 **masked cutout** 材质并开启深度写入(对齐 HD2DURP 的 DopFix /
`TransparentCutout`):透明像素丢弃,不透明像素写入深度,景深才能正确对焦角色。
可用 `setAlphaCutoff` / `setDepthWrite` / `setDoubleSided` 调整;
`setBillboardMode("yaw")` 切换为仅绕 Y 轴的圆柱 billboard。
```squirrel
local hero = hd2d.newSprite(gfx);
hero.setTexture(gfx.newTextureFromFile("assets/hero.png"));
@@ -55,6 +61,8 @@ hero.setSize(28.0, 56.0); // billboard 尺寸
hero.setTint(1.0, 1.0, 1.0, 1.0); // 颜色倍率(alpha 参与裁切)
hero.setVisible(true);
hero.setCamera(cam); // 绑定相机后 update() 每帧转向相机
hero.setAlphaCutoff(0.5);
hero.setDepthWrite(true);
gfx.setTextureSampler(hero.getTexture(), "nearest", "none", 1.0, 0.0); // 清晰像素采样
local tx = hero.getPositionX(); local ty = hero.getPositionY(); local tz = hero.getPositionZ();
@@ -100,8 +108,27 @@ Sprite、Camera 与纹理的调用在渲染线程进行,Graphics 及相关 ECS
固定步长、无随机数,足底锚点由图集透明边距确定。示例验收说明和素材
生成记录见该示例的 README。
## Hd2dLook:景深 + Bloom + 像素采样
`Hd2dLook` 把 HD2DURP 风格的后处理参数写到 `Camera3D`:Gaussian DOF(`Camera3D.setDepthOfField`)
与 Bloom。DOF 在最终 HDR resolve 中读取 GBuffer 硬件深度;因此角色必须用 cutout
深度写入(Sprite3D 默认已开启)。
```squirrel
local look = hd2d.newMiniatureLook(); // 或 hd2d.newLook()
look.setFocusDistance(18.0);
look.setFocusRange(12.0);
look.setMaxBlur(6.0);
look.apply(camera);
look.applyPixelSampler(gfx, hero.getTexture());
```
也可用 `camera.setDepthOfField(focusDistance, maxBlurPx, focusRange)` /
`camera.setBloom(intensity, threshold)` 直接配置。
## 模块方法与对象
- `hd2d.getName()`、`hd2d.newTileMap3D()`、`hd2d.newSprite(gfx)`
- `hd2d.getName()`、`hd2d.newTileMap3D()`、`hd2d.newSprite(gfx)`、`hd2d.newLook()`、`hd2d.newMiniatureLook()`
- `TileMap3D`:`setSideDepth`/`getSideDepth`、`setHeightScale`/`getHeightScale`、`setWallUV`、`setTint`、`buildMesh`、`buildRenderable`、`getTileCount`
- `Sprite3D`:`setTexture`/`getTexture`、`setFrame`、`setFlipX`/`setFlipY`、`setFrameGrid`、`getFrameGridColumns`/`getFrameGridRows`、`setFrameIndex`/`getFrameIndex`/`getFrameCount`、`play`/`stop`/`isPlaying`/`update`、`setPosition`/`getPositionX`/`getPositionY`/`getPositionZ`、`setSize`/`getWidth`/`getHeight`、`setTint`、`setVisible`/`getVisible`
- `Sprite3D`:`setTexture`/`getTexture`、`setFrame`、`setFlipX`/`setFlipY`、`setFrameGrid`、`getFrameGridColumns`/`getFrameGridRows`、`setFrameIndex`/`getFrameIndex`/`getFrameCount`、`play`/`stop`/`isPlaying`/`update`、`setPosition`/`getPositionX`/`getPositionY`/`getPositionZ`、`setSize`/`getWidth`/`getHeight`、`setTint`、`setVisible`/`getVisible`、`setAlphaCutoff`/`getAlphaCutoff`、`setDepthWrite`/`getDepthWrite`、`setDoubleSided`/`getDoubleSided`、`setBillboardMode`/`getBillboardMode`
- `Hd2dLook`:`setFocusDistance`/`getFocusDistance`、`setMaxBlur`/`getMaxBlur`、`setFocusRange`/`getFocusRange`、`setBloomIntensity`/`getBloomIntensity`、`setBloomThreshold`/`getBloomThreshold`、`apply`、`applyPixelSampler`
+13 -1
View File
@@ -131,6 +131,16 @@ eve_init = function() {
camera.setClipPlanes(1.0, 3000.0);
camera.setActive(true);
camera.setAmbient(0.25, 0.28, 0.34);
// HD2DURP-style miniature look: shallow DOF + mild bloom. Sprites keep
// depth write via masked cutout so characters stay sharp in the focus band.
local look = hd2d.newLook();
look.setFocusDistance(900.0);
look.setFocusRange(420.0);
look.setMaxBlur(5.0);
look.setBloomIntensity(0.28);
look.setBloomThreshold(1.25);
look.apply(camera);
look.applyPixelSampler(gfx, atlas);
sun = eve.Light3D();
sun.setType("dir");
sun.setDirection(-0.6, 1.0, 0.5);
@@ -157,7 +167,9 @@ eve_init = function() {
foreach (spawn in riversideData.spawns) {
local sprite = hd2d.newSprite(gfx);
sprite.setTexture(gfx.newTextureFromFile("assets/" + spawn.id + "/64/final/walk-sheet-clean.png"));
gfx.setTextureSampler(sprite.getTexture(), "nearest", "none", 1.0, 0.0);
look.applyPixelSampler(gfx, sprite.getTexture());
sprite.setAlphaCutoff(0.5);
sprite.setDepthWrite(true);
sprite.setFrameGrid(6, 4);
sprite.setSize(64.0, 64.0);
sprite.setPivot(0.5, 60.0/64.0);
+26 -9
View File
@@ -7,19 +7,35 @@
这个例子把已有的可组合组件装配成一个 3D 关卡设计工具:引用地形数据 → 放置白盒快速模型 →
调整模型 → 放置玩家出生点 → 用 Motion Matching 控制器在关卡里自由跑动。
## 编辑器布局
界面按常见关卡 / 场景编辑器壳(Unity / Godot / Unreal 一类)拆成五个区域,由
`editor.newWorkspace` 排布、每个区域一个 UI host(与 `examples/composable-editor` 同壳):
| 区域 | 面板 | 内容 |
| --- | --- | --- |
| 顶栏 | Toolbar | New / Save / Load · Select / Whitebox / Spawn · Move / Rotate / Scale · Undo / Redo · Play · Frame / Focus |
| 左侧 | Outliner | 地形引用与 Rebuild · 场景层级 |
| 中央 | Viewport | 嵌入式 3D 视口(`ui.viewport` + `renderScene3DToCanvas`)· 状态行 |
| 右侧 | Inspector | 选中物体的名字 / TRS · 白盒 recipe 参数或出生点朝向/人物 |
| 底栏 | Assets | 白盒调色板(Whitebox 工具)或当前工具说明 |
选中物体时会画出可见的 Transform Gizmo(旧版只有拾取、没有绘制)。
## 操作
| 输入 | 作用 |
| --- | --- |
| `1` / `2` / `3` | 切换 选择 / 白盒 / 出生点 工具 |
| 左键 | 选择工具:拾取物体并开始 Gizmo 拖动;白盒与出生点工具:在鼠标射线与地形交点处放置 |
| 右键拖动 | 环绕相机(绕当前焦点旋转) |
| 中键拖动 | **平移相机焦点**(在关卡里横向移动视角) |
| `Q` / `E` | 拉远 / 拉近 |
| `W` / `R` / `T` | Gizmo:移动 / 旋转 / 缩放 |
| 左键(视口内) | 选择工具:拾取物体并开始 Gizmo 拖动;白盒与出生点工具:在鼠标射线与地形交点处放置 |
| 右键拖动(视口内) | 环绕相机(绕当前焦点旋转) |
| 中键拖动(视口内) | **平移相机焦点**(在关卡里横向移动视角) |
| 滚轮 / `Q` / `E` | 拉远 / 拉近 |
| `F` | 聚焦当前选中物体;没有选中时框住整块地形 |
| `Delete` | 删除当前选中节点 |
| `F5` | 进入 / 退出 Play |
| Play 中移动鼠标 | **视角转动**(第三人称跟随相机绕角色旋转) |
| Play 中在视口内移动鼠标 | **视角转动**(第三人称跟随相机绕角色旋转) |
| Play 中 `WASD` + `Shift` | 走 / 跑(方向相对当前视角) |
| Play 中 `Q` / `E` | 拉远 / 拉近跟随距离 |
| Play 中 `F1` / `F2` | 切换玩家人物 / motion 集合 |
@@ -30,15 +46,16 @@
编辑视角是「绕一个可平移焦点旋转」的轨道相机:`focusX/Y/Z` 是唯一权威,眼位由
`focus + distance * (cosPitch*sinYaw, sinPitch, cosPitch*cosYaw)` 推出。右键绕焦点转,
中键沿视线的地面基(`forward = (-sinYaw, -cosYaw)`、`right = (cosYaw, -sinYaw)`)平移焦点,
`Q/E` 改距离;平移步长按当前距离缩放,因此在任意缩放下手感一致。`F` 把焦点与距离框到选中物体
(按缩放估算距离),未选中时退回框住整块地形。
滚轮与 `Q/E` 改距离;平移步长按当前距离缩放,因此在任意缩放下手感一致。`F` 把焦点与距离框到选中物体
(按缩放估算距离),未选中时退回框住整块地形。射线与相机交互都相对**中央视口**的本地坐标
(`ui.viewportMouse*` + `canvas` 尺寸),侧栏不会挡住拾取。
Play 视角是第三人称跟随:`CameraController` 的 `follow` 模式求值 `eye = target + offset`,
所以视角完全用一个 offset 表达:
`offset = (-sinYaw * cosPitch, sinPitch, -cosYaw * cosPitch) * distance`。
`cameraYaw` 同时是**移动方向的基准**(`levelPlayInputYaw`),因此转视角就是转 WASD 的前方;
角色自身仍朝向实际移动方向。鼠标用绝对光标位置做差分(引擎没有提供 delta API),
超过 200px 的跳变(进出窗口、切换程序)会被忽略以免视角被甩飞;指针悬停在编辑面板上时不转视角。
角色自身仍朝向实际移动方向。鼠标用视口内绝对光标差分(引擎没有提供 delta API),
超过 200px 的跳变会被忽略;指针不在视口上时不转视角。
## 地形数据引用
+21 -3
View File
@@ -53,11 +53,29 @@ function terrainRayPick(originX, originY, originZ, dirX, dirY, dirZ, maxDistance
return null;
}
/** @brief Terrain point under the current mouse position, or null. */
/**
* @brief Viewport-local mouse / canvas size for ray picks, or null when the
* pointer is not over the center viewport.
*/
function levelViewportPointer() {
ui.select("viewport");
if (!ui.viewportHovered("level-vp")) return null;
local canvas = ui.viewportCanvas("level-vp");
if (canvas == null) return null;
return {
x = ui.viewportMouseX("level-vp"),
y = ui.viewportMouseY("level-vp"),
w = canvas.getWidth().tofloat(),
h = canvas.getHeight().tofloat()
};
}
/** @brief Terrain point under the current viewport mouse, or null. */
function terrainPickAtMouse() {
if (level.camera == null) return null;
level.camera.screenToRay(mouse.getX(), mouse.getY(),
config.width.tofloat(), config.height.tofloat());
local pointer = levelViewportPointer();
if (pointer == null) return null;
level.camera.screenToRay(pointer.x, pointer.y, pointer.w, pointer.h);
return terrainRayPick(level.camera.getScreenRayOriginX(), level.camera.getScreenRayOriginY(),
level.camera.getScreenRayOriginZ(), level.camera.getScreenRayDirX(),
level.camera.getScreenRayDirY(), level.camera.getScreenRayDirZ(), 6000.0);
+7
View File
@@ -176,6 +176,13 @@ function levelStartPlay() {
level.play.focusZ = level.play.positionZ;
level.play.lastMouseX = mouse.getX();
level.play.lastMouseY = mouse.getY();
try {
ui.select("viewport");
if (ui.viewportHovered("level-vp")) {
level.play.lastMouseX = ui.viewportMouseX("level-vp");
level.play.lastMouseY = ui.viewportMouseY("level-vp");
}
} catch (error) {}
levelApplyPlayCameraOffset();
level.controller.setTarget(level.play.focusX, level.play.focusY, level.play.focusZ);
level.controller.snap();
+9 -3
View File
@@ -16,16 +16,18 @@ function levelInit() {
// module handles
session = null, camera = null, controller = null, gizmo = null,
model3d = null, animation = null, physics = null, heightmapTargets = null,
world = null,
world = null, workspace = null,
mode = "edit", // "edit" | "play"
tool = "select", // select | whitebox | spawn
gizmoMode = "translate", // translate | rotate | scale
piece = "prototype.cube",
recipes = [], // whitebox palette (ProcgenKit prototype recipes)
selected = "",
objects = [],
pieces = {}, // objectId -> {recipe, values, entity, half}
spawns = {}, // objectId -> {yaw, character, motionSet, entity, facing}
gizmoPrims = [], // live TransformGizmo overlays (Primitive3D)
rigCache = {}, // "character|motionSet" -> rigged character
characterName = "Mannequin",
motionSetName = "Locomotion",
@@ -51,8 +53,11 @@ function levelInit() {
function levelStatus(text) {
level.status = text;
if (level.session != null) ui.select(levelHost());
ui.setText("status", text);
// Status lives on the center viewport panel after the workspace redesign.
try {
ui.select("viewport");
ui.setText("status", text);
} catch (error) {}
}
// Every scene mutation goes through the session command registry.
@@ -94,6 +99,7 @@ function levelSyncGizmo() {
level.gizmo.setPosition(t.x, t.y, t.z);
level.gizmo.setRotationEuler(t.rotationX, t.rotationY, t.rotationZ);
level.gizmo.setScale(t.scaleX, t.scaleY, t.scaleZ);
if (level.gizmoMode != "") level.gizmo.setMode(level.gizmoMode);
}
function levelNextId(prefix) {
+260 -151
View File
@@ -1,12 +1,43 @@
// Editor panels. All of this is host-owned presentation: replacing it does not
// change the session, the level document or any domain operation.
// Classic level-editor chrome: top toolbar, left outliner, center viewport,
// right inspector, bottom asset palette. Presentation only — session /
// document / domain ops stay in the other files.
//
// The panel tree is rebuilt only when its structure changes (selection, tool,
// palette, mode). Value edits are applied in place so dragging a slider does not
// destroy the widget under the pointer.
// Pattern matches examples/composable-editor and the domain-editor workspace
// shell (docs/dev/2026-09-04-domain-editor-workspace-ui.md): one UI host per
// region, layout from editor.newWorkspace.
function levelMarkPanelsDirty() { level.panelsDirty = true; }
function levelUiHosts() {
return ["toolbar", "hierarchy", "viewport", "inspector", "palette"];
}
function levelEventParts(path) {
local slash = path.find("/");
return slash == null ? ["", path] : [path.slice(0, slash), path.slice(slash + 1)];
}
function levelConfigureWorkspace() {
level.workspace = editor.newWorkspace("level-designer.ui", "Level Designer");
// Top is a single toolbar row (Unity-style); bottom holds the asset strip.
level.workspace.setRegionSize("top", 64.0);
level.workspace.setRegionSize("left", 260.0);
level.workspace.setRegionSize("right", 340.0);
level.workspace.setRegionSize("bottom", 200.0);
level.workspace.layout(config.width.tofloat(), config.height.tofloat());
level.workspace.registerPanel("toolbar", "Toolbar", "top", 0);
level.workspace.registerPanel("hierarchy", "Outliner", "left", 10);
level.workspace.registerPanel("viewport", "Viewport", "center", 20);
level.workspace.registerPanel("inspector", "Inspector", "right", 30);
level.workspace.registerPanel("palette", "Assets", "bottom", 40);
level.workspace.setPanelCapability("viewport", "scene.viewport.3d");
level.workspace.setPanelCapability("inspector", "property.selection");
level.workspace.setPanelContext("hierarchy", "list");
level.workspace.setPanelContext("viewport", "preview");
level.workspace.setPanelContext("inspector", "inspector");
level.workspace.setPanelContext("palette", "asset");
}
function levelHierarchyOrder() {
local ordered = [];
function walk(parentId, depth) {
@@ -22,177 +53,245 @@ function levelHierarchyOrder() {
return ordered;
}
function levelToolButton(label, tool) {
ui.button(level.tool == tool ? "[" + label + "]" : label, "tool:" + tool);
function levelToolLabel(tool) {
if (tool == "select") return "Select";
if (tool == "whitebox") return "Whitebox";
if (tool == "spawn") return "Spawn";
return tool;
}
function levelMountPanels() {
// Cleared up front, not at the end: if a field in any section throws, the
// panel must degrade to "stale" rather than re-running the whole build on
// every subsequent frame.
level.panelsDirty = false;
ui.beginBuild();
ui.beginWindow("Level Designer", "root");
ui.textWrapped(level.status, 352.0, "status");
function levelGizmoModeLabel() {
if (level.gizmoMode == "rotate") return "Rotate";
if (level.gizmoMode == "scale") return "Scale";
return "Move";
}
ui.beginRow("tools", 4.0);
levelToolButton("Select", "select");
levelToolButton("Whitebox", "whitebox");
levelToolButton("Spawn", "spawn");
ui.end();
ui.beginRow("transport", 4.0);
ui.button(level.mode == "play" ? "Stop (Esc)" : "Play (F5)", "play");
ui.button("Undo", "undo");
ui.button("Redo", "redo");
ui.end();
ui.beginRow("file-actions", 4.0);
function levelPanelToolbar() {
// One horizontal strip — stacked rows get clipped by the top region height.
ui.beginRow("toolbar-row", 6.0);
ui.button("New", "new");
ui.button("Save", "save");
ui.button("Load", "load");
ui.button("New", "new");
ui.button("Frame all", "frame");
ui.button("Focus selection", "focus");
ui.separator("tb-sep-file");
ui.button(level.tool == "select" ? "[Select]" : "Select", "tool:select");
ui.button(level.tool == "whitebox" ? "[Whitebox]" : "Whitebox", "tool:whitebox");
ui.button(level.tool == "spawn" ? "[Spawn]" : "Spawn", "tool:spawn");
ui.separator("tb-sep-gizmo");
ui.button(level.gizmoMode == "translate" ? "[Move]" : "Move", "gizmo:translate");
ui.button(level.gizmoMode == "rotate" ? "[Rotate]" : "Rotate", "gizmo:rotate");
ui.button(level.gizmoMode == "scale" ? "[Scale]" : "Scale", "gizmo:scale");
ui.separator("tb-sep-hist");
ui.button("Undo", "undo");
ui.button("Redo", "redo");
ui.button(level.mode == "play" ? "Stop" : "Play", "play");
ui.button("Frame", "frame");
ui.button("Focus", "focus");
ui.spacer("tb-fill", 1.0);
ui.badge(level.mode == "play" ? "PLAY" : "EDIT", "mode-badge");
ui.end();
}
// Terrain reference: either regenerate deterministically from the sampler
// parameters, or reference a terrain asset that already exists on disk.
function levelPanelHierarchy() {
ui.sectionHeader("Terrain", "terrain-header");
local reference = level.terrain != null ? level.terrain.reference : terrainDefaultReference();
ui.separator("terrain-sep");
ui.text("Terrain", "terrain-title");
ui.combo("Source", "Generated\nAsset file", reference.mode == "asset" ? 1 : 0, "terrainMode");
if (reference.mode == "asset") ui.inputText("Asset path", reference.path, "terrainPath");
ui.beginRow("terrain-actions", 4.0);
ui.button("Rebuild terrain", "terrainRebuild");
if (level.terrain != null)
ui.text(level.terrain.width + "x" + level.terrain.height + " @ " +
level.terrain.spacingX + "m", "terrain-info");
ui.end();
ui.separator("hierarchy-sep");
ui.text("Hierarchy", "hierarchy-title");
ui.beginScrollList("hierarchy", 190.0, 0.0);
ui.sectionHeader("Hierarchy", "hierarchy-header");
ui.text(level.objects.len() + " objects · rev " + level.revision, "hierarchy-meta");
local listH = level.workspace.getRegionH("left") - 210.0;
if (listH < 120.0) listH = 120.0;
ui.beginScrollList("hierarchy", listH, 0.0);
foreach (entry in levelHierarchyOrder()) {
local indent = "";
for (local i = 0; i < entry.depth; ++i) indent += " ";
local tag = entry.spawn ? " *" : (entry.piece ? " #" : "");
ui.button(indent + (entry.id == level.selected ? "> " : "") + entry.name + tag,
"select:" + entry.id);
for (local i = 0; i < entry.depth; ++i) indent += " ";
local tag = entry.spawn ? " [spawn]" : (entry.piece ? " [box]" : "");
local mark = entry.id == level.selected ? "> " : " ";
ui.button(indent + mark + entry.name + tag, "select:" + entry.id);
}
ui.end();
}
function levelPanelViewport() {
local help = level.mode == "play"
? "Mouse look · WASD+Shift · Q/E zoom · F1/F2 · Esc"
: "LMB select/place · RMB orbit · MMB pan · wheel zoom · F focus · Del · 1/2/3";
ui.text(help, "viewport-help");
local vpW = level.workspace.getRegionW("center") - 16.0;
local vpH = level.workspace.getRegionH("center") - 78.0;
if (vpW < 64.0) vpW = 64.0;
if (vpH < 64.0) vpH = 64.0;
ui.viewport("level-vp", vpW, vpH);
ui.text(level.status, "status");
if (level.mode == "play" && level.play != null) {
local play = level.play;
local speed = sqrt(play.rig.matcher.getDesiredVelocityX() * play.rig.matcher.getDesiredVelocityX() +
play.rig.matcher.getDesiredVelocityZ() * play.rig.matcher.getDesiredVelocityZ());
ui.progress(levelClamp(speed / 4.4, 0.0, 1.0), "play-speed", "Speed");
ui.text((play.grounded ? "Grounded" : "Airborne") + " · " + play.characterName +
" / " + play.motionSetName, "play-state");
}
}
function levelPanelInspector() {
ui.sectionHeader("Selection", "selection-header");
local object = levelObject(level.selected);
if (object != null) {
ui.separator("inspector-sep");
ui.text("Selection", "selection-title");
ui.inputText("Name", object.name, "name");
ui.beginRow("position-row", 4.0);
ui.inputText("x", object.transform.x.tostring(), "px");
ui.inputText("y", object.transform.y.tostring(), "py");
ui.inputText("z", object.transform.z.tostring(), "pz");
ui.end();
ui.beginRow("rot-scale-row", 4.0);
ui.inputText("yaw", object.transform.rotationY.tostring(), "ry");
ui.inputText("scale", object.transform.scaleX.tostring(), "sx");
ui.end();
ui.beginRow("object-actions", 4.0);
ui.button("Apply", "apply");
ui.button("Drop to terrain", "drop");
ui.button("Delete", "delete");
ui.end();
if (object == null) {
ui.textWrapped("Nothing selected. Click an object in the viewport or hierarchy, or switch to Whitebox / Spawn and click the terrain.",
268.0, "selection-empty");
ui.separator("tool-context-sep");
ui.text("Active tool: " + levelToolLabel(level.tool), "tool-context");
ui.text("Gizmo: " + levelGizmoModeLabel(), "gizmo-context");
return;
}
ui.inputText("Name", object.name, "name");
ui.text("id: " + object.id, "object-id");
ui.separator("trs-sep");
ui.sectionHeader("Transform", "trs-header");
ui.beginRow("position-row", 4.0);
ui.inputText("X", object.transform.x.tostring(), "px");
ui.inputText("Y", object.transform.y.tostring(), "py");
ui.inputText("Z", object.transform.z.tostring(), "pz");
ui.end();
ui.beginRow("rot-scale-row", 4.0);
ui.inputText("Yaw", object.transform.rotationY.tostring(), "ry");
ui.inputText("Scale", object.transform.scaleX.tostring(), "sx");
ui.end();
ui.beginRow("object-actions", 4.0);
ui.button("Apply", "apply");
ui.button("Drop to terrain", "drop");
ui.button("Delete", "delete");
ui.end();
if (object.id in level.pieces) {
local piece = level.pieces[object.id];
ui.separator("piece-params-sep");
ui.sectionHeader("Whitebox parameters", "piece-title");
ui.text(piece.recipe.slice("prototype.".len()), "piece-recipe");
local schema = whiteboxSchema(piece.recipe);
if (schema != null) {
local paramsH = level.workspace.getRegionH("right") - 340.0;
if (paramsH < 100.0) paramsH = 100.0;
ui.beginScrollList("piece-params", paramsH, 0.0);
for (local i = 0; i < schema.getParamCount(); ++i) {
local key = schema.getParamKey(i);
if (!(key in piece.values)) continue;
local kind = schema.getParamKind(i);
local id = "pp:" + key;
if (kind == "float" || kind == "int") {
local minimum = schema.paramHasMinimum(i) ? schema.getParamMinimum(i) : 0.0;
local maximum = schema.paramHasMaximum(i) ? schema.getParamMaximum(i) : 100.0;
ui.slider(schema.getParamLabel(i), piece.values[key].tofloat(),
minimum, maximum, id);
} else if (kind == "bool") {
ui.checkbox(schema.getParamLabel(i), piece.values[key].tointeger() != 0, id);
} else if (kind == "choice") {
local choices = "", selected = 0;
for (local c = 0; c < schema.getParamChoiceCount(i); ++c) {
local choice = schema.getParamChoice(i, c);
if (choice == piece.values[key]) selected = c;
choices += (c == 0 ? "" : "\n") + choice;
}
ui.combo(schema.getParamLabel(i), choices, selected, id);
} else {
ui.inputText(schema.getParamLabel(i), piece.values[key], id);
}
}
ui.end();
}
}
if (object.id in level.spawns) {
local spawn = level.spawns[object.id];
ui.separator("spawn-sep");
ui.sectionHeader("Spawn point", "spawn-title");
ui.slider("Facing yaw (deg)", spawn.yaw * 57.2957795, -180.0, 180.0, "spawnYaw");
local spawnCharacter = spawn.character != "" ? spawn.character : levelActiveCharacterName();
if (levelCharacterNames().len() > 0) {
local options = "", selected = 0, index = 0;
foreach (name in levelCharacterNames()) {
if (name == spawnCharacter) selected = index;
options += (index == 0 ? "" : "\n") + name;
index += 1;
}
ui.combo("Character", options, selected, "spawnCharacter");
}
local spawnMotionSet = spawn.motionSet != "" ? spawn.motionSet : levelActiveMotionSetName();
if (levelMotionSetNames().len() > 0) {
local options = "", selected = 0, index = 0;
foreach (name in levelMotionSetNames()) {
if (name == spawnMotionSet) selected = index;
options += (index == 0 ? "" : "\n") + name;
index += 1;
}
ui.combo("Motion set", options, selected, "spawnMotionSet");
}
}
}
function levelPanelPalette() {
if (level.tool == "whitebox") {
ui.separator("palette-sep");
ui.text("Whitebox palette (" + level.recipes.len() + " pieces)", "palette-title");
ui.beginScrollList("palette", 210.0, 0.0);
ui.sectionHeader("Whitebox palette · " + level.recipes.len() + " recipes", "palette-header");
ui.text("Active: " + level.piece.slice("prototype.".len()) + " · click terrain to place",
"palette-active");
local listH = level.workspace.getRegionH("bottom") - 70.0;
if (listH < 80.0) listH = 80.0;
// Flat scroll list — nested beginRow wrapping clipped most recipes.
ui.beginScrollList("palette", listH, 0.0);
foreach (recipe in level.recipes) {
local label = recipe.slice("prototype.".len());
ui.button(level.piece == recipe ? "> " + label : label, "piece:" + recipe);
}
ui.end();
local piece = (level.selected in level.pieces) ? level.pieces[level.selected] : null;
if (piece != null) {
ui.separator("piece-params-sep");
ui.text("Piece parameters", "piece-title");
local schema = whiteboxSchema(piece.recipe);
if (schema != null) {
ui.beginScrollList("piece-params", 240.0, 0.0);
for (local i = 0; i < schema.getParamCount(); ++i) {
local key = schema.getParamKey(i);
if (!(key in piece.values)) continue;
local kind = schema.getParamKind(i);
local id = "pp:" + key;
if (kind == "float" || kind == "int") {
local minimum = schema.paramHasMinimum(i) ? schema.getParamMinimum(i) : 0.0;
local maximum = schema.paramHasMaximum(i) ? schema.getParamMaximum(i) : 100.0;
ui.slider(schema.getParamLabel(i), piece.values[key].tofloat(),
minimum, maximum, id);
} else if (kind == "bool") {
ui.checkbox(schema.getParamLabel(i), piece.values[key].tointeger() != 0, id);
} else if (kind == "choice") {
local choices = "", selected = 0;
for (local c = 0; c < schema.getParamChoiceCount(i); ++c) {
local choice = schema.getParamChoice(i, c);
if (choice == piece.values[key]) selected = c;
choices += (c == 0 ? "" : "\n") + choice;
}
ui.combo(schema.getParamLabel(i), choices, selected, id);
} else {
ui.inputText(schema.getParamLabel(i), piece.values[key], id);
}
}
ui.end();
}
}
} else if (level.tool == "spawn") {
ui.sectionHeader("Spawn tool", "palette-header");
ui.textWrapped("Click the terrain in the viewport to drop a player start. Select a spawn in the outliner to edit facing, character, and motion set in the inspector.",
level.workspace.getRegionW("bottom") - 24.0, "palette-spawn-help");
ui.text("Spawn points: " + spawnCount(), "palette-spawn-count");
} else {
ui.sectionHeader("Select tool", "palette-header");
ui.textWrapped("Click objects in the viewport or outliner. Drag the visible gizmo to move / rotate / scale. Use the inspector for precise transforms.",
level.workspace.getRegionW("bottom") - 24.0, "palette-select-help");
local sel = levelObject(level.selected);
ui.text(sel == null ? "No selection" : ("Selected: " + sel.name), "palette-selection");
}
}
if (level.tool == "spawn") {
ui.separator("spawn-sep");
ui.text("Spawn points (" + spawnCount() + ")", "spawn-title");
local spawn = (level.selected in level.spawns) ? level.spawns[level.selected] : null;
if (spawn == null) {
ui.textWrapped("Select or place a spawn point to edit its facing and start character.",
340.0, "spawn-help");
} else {
ui.slider("Facing yaw (deg)", spawn.yaw * 57.2957795, -180.0, 180.0, "spawnYaw");
// The combo shows this spawn point's own start character, falling back to
// the level default only while the spawn has not chosen one.
local spawnCharacter = spawn.character != "" ? spawn.character : levelActiveCharacterName();
if (levelCharacterNames().len() > 0) {
local options = "", selected = 0, index = 0;
foreach (name in levelCharacterNames()) {
if (name == spawnCharacter) selected = index;
options += (index == 0 ? "" : "\n") + name;
index += 1;
}
ui.combo("Character", options, selected, "spawnCharacter");
}
local spawnMotionSet = spawn.motionSet != "" ? spawn.motionSet : levelActiveMotionSetName();
if (levelMotionSetNames().len() > 0) {
local options = "", selected = 0, index = 0;
foreach (name in levelMotionSetNames()) {
if (name == spawnMotionSet) selected = index;
options += (index == 0 ? "" : "\n") + name;
index += 1;
}
ui.combo("Motion set", options, selected, "spawnMotionSet");
}
}
levelPanelBuilders <- {
toolbar = levelPanelToolbar,
hierarchy = levelPanelHierarchy,
viewport = levelPanelViewport,
inspector = levelPanelInspector,
palette = levelPanelPalette
};
function levelMountPanels() {
// Cleared up front: if a field throws, degrade to stale instead of
// rebuilding every frame.
level.panelsDirty = false;
if (level.workspace == null) levelConfigureWorkspace();
level.workspace.layout(config.width.tofloat(), config.height.tofloat());
for (local i = 0; i < level.workspace.getPanelCount(); ++i) {
if (!level.workspace.getPanelVisible(i)) continue;
local id = level.workspace.getPanelId(i);
if (!(id in levelPanelBuilders)) continue;
ui.beginBuild();
ui.beginWindow(level.workspace.getPanelTitle(i), "root");
levelPanelBuilders[id]();
ui.end();
ui.mountBuildAs(id);
ui.select(id);
local region = level.workspace.getPanelRegion(i);
ui.setHostPos(level.workspace.getRegionX(region), level.workspace.getRegionY(region), 0.0, 0.0);
ui.setHostSize(level.workspace.getRegionW(region), level.workspace.getRegionH(region));
ui.setHostOverlay(false);
}
ui.separator("help-sep");
ui.textWrapped(level.mode == "play"
? "Mouse look turns the view (and with it WASD forward). Q/E zoom | WASD + Shift run | F1 character | F2 motion set | Esc stop."
: "Whitebox: pick a piece, click the terrain to place it. Spawn: click the terrain to drop a player start. LMB select/drag gizmo, RMB orbit, MMB pan, Q/E zoom, F focus, Del deletes.",
352.0, "help");
ui.end();
ui.mountBuildAs(levelHost());
ui.select(levelHost());
ui.setHostPos(14.0, 14.0, 0.0, 0.0);
ui.setHostSize(400.0, config.height - 28.0);
ui.setHostOverlay(true);
}
function levelRefreshPanels() {
@@ -205,6 +304,7 @@ function levelApplySelectionFields() {
if (object == null) return;
local px, py, pz, ry, sx;
try {
ui.select("inspector");
px = ui.getValueText("px").tofloat();
py = ui.getValueText("py").tofloat();
pz = ui.getValueText("pz").tofloat();
@@ -245,6 +345,7 @@ function levelUpdatePieceParam(id, key) {
if (piece == null) return;
local schema = whiteboxSchema(piece.recipe);
if (schema == null) return;
ui.select("inspector");
for (local i = 0; i < schema.getParamCount(); ++i) {
if (schema.getParamKey(i) != key) continue;
local kind = schema.getParamKind(i);
@@ -258,41 +359,43 @@ function levelUpdatePieceParam(id, key) {
}
function levelHandleUi() {
ui.select(levelHost());
// Global click/change queues carry "host/widget" paths — route by prefix.
local click = ui.consumeClick();
while (click != "") {
local slash = click.find("/");
local id = slash == null ? click : click.slice(slash + 1);
levelHandleAction(id);
ui.select(levelHost());
local parts = levelEventParts(click);
levelHandleAction(parts[1]);
click = ui.consumeClick();
}
local change = ui.consumeChange();
while (change != "") {
local slash = change.find("/");
local id = slash == null ? change : change.slice(slash + 1);
local parts = levelEventParts(change);
local id = parts[1];
if (id.find("pp:") == 0) levelUpdatePieceParam(id, id.slice(3));
else if (id == "terrainMode") {
ui.select("hierarchy");
if (level.terrain != null) {
level.terrain.reference.mode = ui.getValue("terrainMode").tointeger() == 1 ? "asset" : "generated";
level.status = "terrain source: " + level.terrain.reference.mode + " (press Rebuild terrain)";
}
levelMarkPanelsDirty();
} else if (id == "terrainPath") {
ui.select("hierarchy");
if (level.terrain != null) level.terrain.reference.path = ui.getValueText("terrainPath");
} else if (id == "spawnYaw") {
ui.select("inspector");
local spawn = (level.selected in level.spawns) ? level.spawns[level.selected] : null;
if (spawn != null) spawn.yaw = ui.getValue("spawnYaw") / 57.2957795;
} else if (id == "spawnCharacter") {
ui.select("inspector");
local spawn = (level.selected in level.spawns) ? level.spawns[level.selected] : null;
local names = levelCharacterNames();
if (spawn != null && names.len() > 0) spawn.character = names[ui.getValue("spawnCharacter").tointeger()];
} else if (id == "spawnMotionSet") {
ui.select("inspector");
local spawn = (level.selected in level.spawns) ? level.spawns[level.selected] : null;
local names = levelMotionSetNames();
if (spawn != null && names.len() > 0) spawn.motionSet = names[ui.getValue("spawnMotionSet").tointeger()];
}
ui.select(levelHost());
change = ui.consumeChange();
}
}
@@ -300,6 +403,12 @@ function levelHandleUi() {
function levelHandleAction(id) {
if (id == "tool:select" || id == "tool:whitebox" || id == "tool:spawn") {
level.tool = id.slice(5);
level.status = "tool: " + levelToolLabel(level.tool);
levelMarkPanelsDirty();
} else if (id == "gizmo:translate" || id == "gizmo:rotate" || id == "gizmo:scale") {
level.gizmoMode = id.slice(6);
if (level.gizmo != null) level.gizmo.setMode(level.gizmoMode);
level.status = "gizmo: " + levelGizmoModeLabel();
levelMarkPanelsDirty();
} else if (id == "play") {
levelTogglePlay();
+126 -35
View File
@@ -4,6 +4,9 @@
// undo/redo and conflict detection behave exactly as in examples/scene-editor.
// Play mode (F5) builds a runtime world from the authored data and drives a
// Motion Matching character; it never mutates authored state.
//
// UI is a classic editor workspace (toolbar / outliner / viewport / inspector /
// asset palette) — see level_ui.nut.
dofile("level_state.nut");
dofile("level_json.nut");
@@ -123,6 +126,88 @@ function levelPreviewSelection() {
if (level.selected in level.spawns) spawnApplyTransform(level.spawns[level.selected], preview);
}
/** @brief Parent-local gizmo point → world, for overlay drawing. */
function levelGizmoWorldPoint(object, lx, ly, lz) {
if (object == null || object.parent == "") return [lx, ly, lz];
return scene.localToWorldAt(levelHost(), object.parent, lx, ly, lz);
}
function levelClearGizmoPrims() {
foreach (prim in level.gizmoPrims) {
if (prim == null) continue;
try {
local removed = prim.remove();
// Best-effort: a stale proxy after hot-reload is fine to drop.
if (removed != null && !removed.ok) {}
} catch (error) {}
}
level.gizmoPrims = [];
}
/**
* @brief Draw the TransformGizmo as world primitives (missing in the old UI —
* pick/drag worked but nothing was visible).
*
* Primitive factories return Result{ok,value}; keep the owned proxy and set
* depth to ignore so the gizmo stays readable over terrain.
*/
function levelDrawGizmo() {
levelClearGizmoPrims();
if (level.mode == "play") return;
local object = levelObject(level.selected);
if (object == null || level.gizmo == null) return;
local gizmo = level.gizmo;
function keep(result) {
if (result == null || !result.ok) return;
local prim = result.value;
local depth = prim.setDepthMode("ignore");
if (depth != null && !depth.ok) {}
level.gizmoPrims.push(prim);
}
for (local i = 0; i < gizmo.getPartCount(); ++i) {
local ox = gizmo.getPartOriginX(i), oy = gizmo.getPartOriginY(i), oz = gizmo.getPartOriginZ(i);
local length = gizmo.getPartLength(i);
local a = levelGizmoWorldPoint(object, ox, oy, oz);
local b = levelGizmoWorldPoint(object,
ox + gizmo.getPartDirX(i) * length,
oy + gizmo.getPartDirY(i) * length,
oz + gizmo.getPartDirZ(i) * length);
local kind = gizmo.getPartKind(i);
local r = gizmo.getPartColorR(i), g = gizmo.getPartColorG(i), bl = gizmo.getPartColorB(i);
if (kind == "axis") {
if (gizmo.getMode() == "translate")
keep(gfx.newPrimitiveArrow3D(a[0], a[1], a[2], b[0], b[1], b[2],
length * 0.22, length * 0.08, r, g, bl, 1.0, 3.0));
else
keep(gfx.newPrimitiveLine3D(a[0], a[1], a[2], b[0], b[1], b[2], r, g, bl, 1.0, 3.0));
} else if (kind == "center") {
local rad = gizmo.getPartRadius(i) * 0.75;
keep(gfx.newPrimitiveLine3D(a[0] - rad, a[1], a[2], a[0] + rad, a[1], a[2], r, g, bl, 1.0, 2.0));
} else if (kind == "ring") {
local normal = [gizmo.getPartDirX(i), gizmo.getPartDirY(i), gizmo.getPartDirZ(i)];
local u = fabs(normal[1]) < 0.9 ? [-normal[2], 0.0, normal[0]] : [0.0, normal[2], -normal[1]];
local un = sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
if (un < 0.0001) continue;
for (local k = 0; k < 3; ++k) u[k] /= un;
local v = [normal[1] * u[2] - normal[2] * u[1],
normal[2] * u[0] - normal[0] * u[2],
normal[0] * u[1] - normal[1] * u[0]];
local points = [], radius = gizmo.getPartRadius(i);
for (local k = 0; k < 48; ++k) {
local angle = k * 6.28318530718 / 48.0;
local p = levelGizmoWorldPoint(object,
ox + radius * (u[0] * cos(angle) + v[0] * sin(angle)),
oy + radius * (u[1] * cos(angle) + v[1] * sin(angle)),
oz + radius * (u[2] * cos(angle) + v[2] * sin(angle)));
points.push(p[0]); points.push(p[1]); points.push(p[2]);
}
keep(gfx.newPrimitivePolyline3D(points, true, r, g, bl, 1.0, 3.0));
}
}
}
function levelPointer() {
local down = mouse.isDown(1);
local pressed = down && !level.mouseDown;
@@ -130,13 +215,13 @@ function levelPointer() {
level.mouseDown = down;
local gizmo = level.gizmo;
if (ui.wantCaptureMouse()) {
local pointer = levelViewportPointer();
if (pointer == null) {
if (gizmo.isDragging()) gizmo.endDrag();
return;
}
local object = levelObject(level.selected);
level.camera.screenToRay(mouse.getX(), mouse.getY(),
config.width.tofloat(), config.height.tofloat());
level.camera.screenToRay(pointer.x, pointer.y, pointer.w, pointer.h);
local ox = level.camera.getScreenRayOriginX();
local oy = level.camera.getScreenRayOriginY();
local oz = level.camera.getScreenRayOriginZ();
@@ -215,9 +300,23 @@ function levelEditInput() {
if (levelKeyPressed("1")) levelHandleAction("tool:select");
if (levelKeyPressed("2")) levelHandleAction("tool:whitebox");
if (levelKeyPressed("3")) levelHandleAction("tool:spawn");
if (levelKeyPressed("w") || levelKeyPressed("W")) levelHandleAction("gizmo:translate");
if (levelKeyPressed("r") || levelKeyPressed("R")) levelHandleAction("gizmo:rotate");
if (levelKeyPressed("t") || levelKeyPressed("T")) levelHandleAction("gizmo:scale");
local mouseX = mouse.getX(), mouseY = mouse.getY();
local orbit = mouse.isDown(2) && !ui.wantCaptureMouse();
ui.select("viewport");
local hovered = ui.viewportHovered("level-vp");
local mouseX = hovered ? ui.viewportMouseX("level-vp") : mouse.getX();
local mouseY = hovered ? ui.viewportMouseY("level-vp") : mouse.getY();
if (hovered) {
local wheel = ui.viewportWheel("level-vp");
if (wheel != 0.0) {
if (wheel > 0.0) level.distance = levelClamp(level.distance / 1.12, 3.0, 3000.0);
else level.distance = levelClamp(level.distance * 1.12, 3.0, 3000.0);
levelUpdateOrbitCamera();
}
}
local orbit = hovered && mouse.isDown(2);
if (orbit && level.orbit) {
level.yaw += (mouseX - level.lastX) * 0.008;
level.pitch += (mouseY - level.lastY) * 0.006;
@@ -225,7 +324,7 @@ function levelEditInput() {
levelUpdateOrbitCamera();
}
// Middle-drag pans the focus across the level.
local pan = mouse.isDown(3) && !ui.wantCaptureMouse();
local pan = hovered && mouse.isDown(3);
if (pan && level.pan) levelPanCamera(mouseX - level.lastX, mouseY - level.lastY);
level.orbit = orbit;
level.pan = pan;
@@ -252,16 +351,13 @@ function levelPlayInput() {
local play = level.play;
if (play == null) return;
// Mouse look. The engine exposes absolute cursor positions and no delta API,
// so the delta is tracked here and the panel area is excluded so hovering the
// editor UI does not spin the view.
local mouseX = mouse.getX(), mouseY = mouse.getY();
// Mouse look inside the viewport only, so docking chrome never spins the view.
ui.select("viewport");
local hovered = ui.viewportHovered("level-vp");
local mouseX = hovered ? ui.viewportMouseX("level-vp") : play.lastMouseX;
local mouseY = hovered ? ui.viewportMouseY("level-vp") : play.lastMouseY;
local deltaX = mouseX - play.lastMouseX, deltaY = mouseY - play.lastMouseY;
// Absolute cursor polling has no notion of a discontinuity, so a jump larger
// than a deliberate flick (leaving/entering the window, alt-tab) is ignored
// instead of whipping the view around.
if (!ui.wantCaptureMouse() && (deltaX < 200.0 && deltaX > -200.0) &&
(deltaY < 200.0 && deltaY > -200.0))
if (hovered && (deltaX < 200.0 && deltaX > -200.0) && (deltaY < 200.0 && deltaY > -200.0))
levelPlayLook(deltaX, deltaY);
play.lastMouseX = mouseX;
play.lastMouseY = mouseY;
@@ -272,20 +368,6 @@ function levelPlayInput() {
play.cameraDistance = levelClamp(play.cameraDistance / 1.15, 2.0, 60.0);
}
function levelDrawOverlay() {
if (level.mode != "play") return;
local play = level.play;
gfx.drawSolidRect(18.0, 18.0, 470.0, 84.0, 0.02, 0.035, 0.06, 0.88);
gfx.drawSolidRect(30.0, 30.0, 20.0, 20.0, 0.20, 0.85, 0.45, 1.0);
gfx.drawSolidRect(62.0, 30.0, 390.0, 8.0, 0.18, 0.24, 0.32, 1.0);
local speed = sqrt(play.rig.matcher.getDesiredVelocityX() * play.rig.matcher.getDesiredVelocityX() +
play.rig.matcher.getDesiredVelocityZ() * play.rig.matcher.getDesiredVelocityZ());
gfx.drawSolidRect(62.0, 30.0, 390.0 * levelClamp(speed / 4.4, 0.0, 1.0), 8.0, 0.25, 0.86, 0.48, 1.0);
gfx.drawSolidRect(62.0, 52.0, play.grounded ? 180.0 : 55.0, 8.0,
play.grounded ? 0.30 : 0.85, play.grounded ? 0.80 : 0.45, 0.50, 1.0);
gfx.drawSolidRect(62.0, 70.0, 120.0 * levelClamp(play.lastCost / 6.0, 0.0, 1.0), 8.0, 0.95, 0.60, 0.20, 1.0);
}
eve_init = function() {
levelInit();
level.model3d = eve.Model3D();
@@ -300,11 +382,13 @@ eve_init = function() {
level.gizmo = eve.Editor().newGizmo();
level.gizmo.setSize(2.4);
level.gizmo.setSnapTranslate(0.5, 0.5, 0.5);
level.gizmo.setMode(level.gizmoMode);
gfx.setBackgroundColor(0.055, 0.065, 0.085, 1.0);
gfx.setBackgroundColor(0.045, 0.052, 0.068, 1.0);
gfx.setDirectionalLight(-0.42, -1.0, -0.30, 1.25, 1.18, 1.05);
ui.setTheme("dark");
levelSetupCamera();
levelConfigureWorkspace();
if (terrainBuild(terrainDefaultReference()) == null) throw level.status;
levelFrameTerrain();
@@ -313,9 +397,11 @@ eve_init = function() {
print("level-designer: terrain " + level.terrain.width + "x" + level.terrain.height +
" spacing=" + level.terrain.spacingX + " whiteboxRecipes=" + level.recipes.len() + "\n");
print("level-designer: 1/2/3 tools | LMB place/select | RMB orbit | MMB pan | Q/E zoom | F focus | F5 play\n");
print("level-designer: in play: mouse look | WASD+Shift | Q/E zoom | F1/F2 character/motion | Esc stop\n");
level.status = "ready: pick a piece, then click the terrain";
print("level-designer: workspace panels=" + level.workspace.getPanelCount() +
" (toolbar/outliner/viewport/inspector/palette)\n");
print("level-designer: 1/2/3 tools | W/R/T gizmo | LMB place/select | RMB orbit | MMB pan | wheel/Q/E zoom | F focus | F5 play\n");
print("level-designer: in play: mouse look (viewport) | WASD+Shift | Q/E zoom | F1/F2 character/motion | Esc stop\n");
level.status = "ready: pick Whitebox, choose a piece, click the terrain";
};
eve_update = function(dt) {
@@ -327,6 +413,8 @@ eve_update = function(dt) {
levelPlayInput();
if (level.mode == "play") levelUpdatePlay(dt);
levelHandleUi();
if (level.panelsDirty) levelMountPanels();
levelClearGizmoPrims();
return;
}
levelHandleUi();
@@ -335,12 +423,14 @@ eve_update = function(dt) {
levelPointer();
whiteboxSyncTransforms();
spawnSyncTransforms();
levelDrawGizmo();
};
eve_render = function() {
gfx.clear();
gfx.render3D();
levelDrawOverlay();
ui.select("viewport");
local canvas = ui.viewportCanvas("level-vp");
if (canvas != null) gfx.renderScene3DToCanvas(canvas, level.camera);
ui.beginFrameAndRender();
};
@@ -348,6 +438,7 @@ eve_reload <- function() {
// The level state holds live module handles (session, gizmo, camera, procgen
// module, camera controller) that a hot-reload snapshot cannot capture, so the
// designer rebuilds its session from scratch instead of using stale handles.
levelClearGizmoPrims();
level = null;
eve_init();
level.status = "hot reloaded";
+2
View File
@@ -131,6 +131,8 @@ foreach (tool in ["select", "whitebox", "spawn"]) {
smokeCheck("panels build in every tool and selection state", panelFailure == "");
if (panelFailure != "") print(" panel failure: " + panelFailure + "\n");
smokeCheck("panel build clears the dirty flag", !level.panelsDirty);
smokeCheck("workspace shell mounted", level.workspace != null && level.workspace.getPanelCount() >= 5);
level.tool = "whitebox";
level.selected = spawnObject;
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Compile the graphics depth-of-field GLSL fragment to a SPIR-V C++ include array."""
from __future__ import annotations
import struct
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SHADER_DIR = ROOT / "src" / "modules" / "graphics" / "shaders"
FRAGS = [
"dof_gaussian.frag",
]
def spv_to_inc(spv_path: Path, array_name: str, out_path: Path) -> None:
data = spv_path.read_bytes()
if len(data) % 4 != 0:
raise SystemExit(f"SPIR-V size not multiple of 4: {spv_path}")
words = struct.unpack(f"<{len(data) // 4}I", data)
if not words or words[0] != 0x07230203:
raise SystemExit(f"bad SPIR-V magic in {spv_path}")
lines = [f"static const uint32_t {array_name}[] = {{"]
row: list[str] = []
for i, w in enumerate(words):
row.append(f"0x{w:08x}")
if len(row) == 8:
lines.append(" " + ", ".join(row) + ",")
row = []
if row:
lines.append(" " + ", ".join(row) + ",")
lines.append("};")
lines.append(f"static const size_t {array_name}_count = {len(words)};")
lines.append("")
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"wrote {out_path.relative_to(ROOT)} ({len(words)} words)")
def compile_frag(src: Path) -> Path:
out = src.with_suffix(".spv")
cmd = ["glslc", "-fshader-stage=frag", str(src), "-o", str(out)]
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit(f"glslc failed for {src.name}:\n{r.stderr or r.stdout}")
return out
def main() -> int:
if not SHADER_DIR.is_dir():
raise SystemExit(f"missing shader dir: {SHADER_DIR}")
for name in FRAGS:
src = SHADER_DIR / name
if not src.is_file():
raise SystemExit(f"missing {src}")
spv = compile_frag(src)
stem = src.stem
array = f"{stem}_frag_spv"
spv_to_inc(spv, array, SHADER_DIR / f"{stem}_frag_spv.inc")
spv.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+9 -1
View File
@@ -135,6 +135,10 @@ void DevTool::attach(ssq::VM& vm, bool sampleLocals) { attach(vm.getHandle(), sa
void DevTool::attach(eve::Runtime& runtime, bool sampleLocals) {
attach(runtime.vm(), sampleLocals);
runtime_ = &runtime;
// Capture throw-site stacks for try/catch so reportError / lastScriptError
// can surface the original site (load.nut pattern). Safe here because the
// Runtime-bound error hook only stashes context and does not notify/pause.
sq_notifyallexceptions(runtime.handle(), SQTrue);
// Route Runtime-boundary errors into the slicer/report. The VM error hook
// only captures the stack when this Runtime sink is bound; this handler
// reports compile / reflect / unload failures and uncaught execute errors
@@ -182,7 +186,11 @@ void DevTool::attach(HSQUIRRELVM vm, bool sampleLocals) {
Debugger::instance().setPump([this]() { pumpWhilePaused(); });
sq_enabledebuginfo(vm_, SQTrue);
sq_notifyallexceptions(vm_, SQTrue);
// Do not enable sq_notifyallexceptions here. With notify-all + break-on-error,
// this hook would pause on every caught throw before script catch/reportError
// runs (double pause → hung script thread → flaky teardown SEGFAULT in
// devtools.dap.caughtErrorPausesAtReportSite). Runtime::installErrorHandler
// already turns notify-all on for production VMs; attach(Runtime&) keeps it.
sq_setnativedebughook(vm_, nativeDebugHook);
// Route uncaught script errors into the debugger (break-on-error aware).
sq_newclosure(vm_, runtimeErrorHook, 0);
+102
View File
@@ -0,0 +1,102 @@
#include "graphics/DepthOfField.h"
#include <algorithm>
#include <string>
#include <vector>
#include "common/Exception.h"
#include "graphics/BlendMode.h"
#include "graphics/Canvas.h"
#include "graphics/Color.h"
#include "graphics/Graphics.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "graphics/shaders/PostProcessWgsl.h"
#include "graphics/shaders/dof_gaussian_frag_spv.inc"
namespace eve::graphics {
namespace {
std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
return std::vector<uint32_t>(data, data + count);
}
Shader *newDofShader(Graphics *gfx) {
Shader *shader =
gfx->getBackendName() == "webgpu"
? gfx->newShaderFromWgsl({}, std::string(shaders::kPostCommon) + shaders::kDofGaussian)
: gfx->newShaderFromSpv({},
copySpv(dof_gaussian_frag_spv, dof_gaussian_frag_spv_count));
if (!shader || !shader->gpuHandle) throw eve::Exception("DepthOfField: failed to create shader");
shader->declareFloat("texelW");
shader->declareFloat("texelH");
shader->declareFloat("directionX");
shader->declareFloat("directionY");
shader->declareFloat("focusDistance");
shader->declareFloat("focusRange");
shader->declareFloat("maxBlurPx");
shader->declareFloat("nearZ");
shader->declareFloat("farZ");
return shader;
}
} // namespace
DepthOfField::DepthOfField(Graphics *gfx) : gfx_(gfx) {
if (!gfx_) throw eve::Exception("DepthOfField: null graphics");
shader_ = newDofShader(gfx_);
}
void DepthOfField::ensureTargets(int width, int height) {
width = std::max(width, 1);
height = std::max(height, 1);
if (temp_ && width_ == width && height_ == height) return;
width_ = width;
height_ = height;
temp_ = gfx_->newHDRCanvas(width, height);
output_ = gfx_->newHDRCanvas(width, height);
if (!temp_ || !output_) throw eve::Exception("DepthOfField: failed to allocate HDR targets");
}
void DepthOfField::configure(float dirX, float dirY, float focusDistance, float maxBlurPx,
float focusRange, float nearZ, float farZ, int width, int height) {
shader_->sendFloat("texelW", 1.f / float(std::max(width, 1)));
shader_->sendFloat("texelH", 1.f / float(std::max(height, 1)));
shader_->sendFloat("directionX", dirX);
shader_->sendFloat("directionY", dirY);
shader_->sendFloat("focusDistance", std::max(focusDistance, 0.f));
shader_->sendFloat("focusRange", std::max(focusRange, 1e-3f));
shader_->sendFloat("maxBlurPx", std::max(maxBlurPx, 0.f));
const float nearClip = nearZ > 1e-4f ? nearZ : 0.1f;
const float farClip = farZ > nearClip ? farZ : nearClip + 1.f;
shader_->sendFloat("nearZ", nearClip);
shader_->sendFloat("farZ", farClip);
}
Texture *DepthOfField::apply(Texture *source, Texture *hwDepth, float focusDistance,
float maxBlurPx, float focusRange, float nearZ, float farZ) {
if (!source) throw eve::Exception("DepthOfField.apply: null source");
if (!hwDepth || maxBlurPx <= 0.f) return source;
ensureTargets(source->getWidth(), source->getHeight());
Canvas *previous = gfx_->getCanvas();
temp_->clear(Color(0.f, 0.f, 0.f, 0.f), {}, {});
gfx_->setCanvas(temp_);
configure(1.f, 0.f, focusDistance, maxBlurPx, focusRange, nearZ, farZ, width_, height_);
gfx_->drawTexturedRectShaderDepth(source, hwDepth, shader_, 0.f, 0.f, float(width_),
float(height_), Color(1.f, 1.f, 1.f, 1.f));
output_->clear(Color(0.f, 0.f, 0.f, 0.f), {}, {});
gfx_->setCanvas(output_);
configure(0.f, 1.f, focusDistance, maxBlurPx, focusRange, nearZ, farZ, width_, height_);
gfx_->drawTexturedRectShaderDepth(temp_->getTexture(), hwDepth, shader_, 0.f, 0.f, float(width_),
float(height_), Color(1.f, 1.f, 1.f, 1.f));
gfx_->setCanvas(previous);
Texture *result = output_->getTexture();
if (!result) throw eve::Exception("DepthOfField: output texture missing");
return result;
}
} // namespace eve::graphics
+51
View File
@@ -0,0 +1,51 @@
#pragma once
namespace eve::graphics {
class Canvas;
class Graphics;
class Shader;
class Texture;
/**
* @brief Separable Gaussian depth-of-field for the final HDR scene.
*
* Reads linear HDR color plus hardware depth (Vulkan NDC z). Blur radius grows
* with view-space distance from `focusDistance` over `focusRange`, capped at
* `maxBlurPx`. Zero max blur is a no-op. Used by HD-2D miniature looks; sprites
* must write depth (masked/cutout) so characters stay sharp in the focus band.
*/
class DepthOfField {
public:
/** @brief Create the cross-backend DOF shaders. */
explicit DepthOfField(Graphics *gfx);
/**
* @brief Apply separable Gaussian DOF.
* @param source Linear HDR scene color.
* @param hwDepth Hardware depth (NDC z in .r). Null disables the effect.
* @param focusDistance View-space focus plane distance.
* @param maxBlurPx Maximum blur radius in source texels; <= 0 disables.
* @param focusRange Distance from focus at which blur reaches maxBlurPx.
* @param nearZ Camera near clip.
* @param farZ Camera far clip.
* @return Blurred HDR texture owned by this DepthOfField, or `source` when disabled.
* @lifetime Returned texture remains valid until dimensions change or this object is destroyed.
*/
Texture *apply(Texture *source, Texture *hwDepth, float focusDistance, float maxBlurPx,
float focusRange, float nearZ, float farZ);
private:
void ensureTargets(int width, int height);
void configure(float dirX, float dirY, float focusDistance, float maxBlurPx, float focusRange,
float nearZ, float farZ, int width, int height);
Graphics *gfx_ = nullptr;
Shader *shader_ = nullptr;
Canvas *temp_ = nullptr;
Canvas *output_ = nullptr;
int width_ = 0;
int height_ = 0;
};
} // namespace eve::graphics
+23
View File
@@ -1,5 +1,6 @@
#include "graphics/Graphics.h"
#include "graphics/Bloom.h"
#include "graphics/DepthOfField.h"
#include "graphics/Exposure.h"
#include "graphics/DepthPyramid.h"
#include "common/Capability.h"
@@ -22,6 +23,7 @@
#include "graphics/CanvasScriptBindings.h"
#include "graphics/FogVolume.h"
#include "graphics/Font.h"
#include "graphics/GBuffer.h"
#include "graphics/GlobalIllumination.h"
#include "graphics/Light.h"
#include "graphics/MapFog.h"
@@ -418,6 +420,10 @@ void Graphics::expose(ssq::Table& table) {
cam.addFunc("setBloom", &Camera3D::setBloom);
cam.addFunc("getBloomIntensity", &Camera3D::getBloomIntensity);
cam.addFunc("getBloomThreshold", &Camera3D::getBloomThreshold);
cam.addFunc("setDepthOfField", &Camera3D::setDepthOfField);
cam.addFunc("getDofFocusDistance", &Camera3D::getDofFocusDistance);
cam.addFunc("getDofMaxBlur", &Camera3D::getDofMaxBlur);
cam.addFunc("getDofFocusRange", &Camera3D::getDofFocusRange);
cam.addFunc("setEnvProbe", &Camera3D::setEnvProbe);
cam.addFunc("clearEnvProbe", &Camera3D::clearEnvProbe);
cam.addFunc("hasEnvProbe", &Camera3D::hasEnvProbe);
@@ -1183,6 +1189,11 @@ Bloom* Graphics::pipelineBloom() {
return pipelineBloom_.get();
}
DepthOfField* Graphics::pipelineDepthOfField() {
if (!pipelineDof_) pipelineDof_ = std::make_unique<DepthOfField>(this);
return pipelineDof_.get();
}
Exposure* Graphics::pipelineExposure() {
if (!pipelineExposure_) pipelineExposure_ = std::make_unique<Exposure>(this);
return pipelineExposure_.get();
@@ -1218,6 +1229,18 @@ Texture* Graphics::prepareFinalSceneTexture(Texture *scene, Texture *motion) {
aa->applyTo(this, scene, spatialAAResolve_);
resolved = spatialAAResolve_->getTexture();
}
if (getSceneDofMaxBlur() > 0.f && renderControl_) {
GBuffer *gb = renderControl_->getGBuffer();
if (gb && gb->isValid()) {
Texture *depth = getBackendName() == "webgpu" ? gb->getDepthTexture()
: gb->getHwDepthTexture();
if (depth) {
resolved = pipelineDepthOfField()->apply(
resolved, depth, getSceneDofFocusDistance(), getSceneDofMaxBlur(),
getSceneDofFocusRange(), getSceneDofNearZ(), getSceneDofFarZ());
}
}
}
Texture *meterSource = resolved;
resolved = pipelineBloom()->apply(resolved, getSceneBloomIntensity(),
getSceneBloomThreshold());
+21 -1
View File
@@ -43,6 +43,7 @@ struct ShaderResourceInputs;
class AmbientOcclusion;
class AntiAliasing;
class Bloom;
class DepthOfField;
class Exposure;
class DepthPyramid;
class Camera3D;
@@ -1217,6 +1218,21 @@ public:
virtual float getSceneBloomIntensity() const = 0;
/** @brief Current linear HDR bloom threshold. */
virtual float getSceneBloomThreshold() const = 0;
/**
* @brief Configure final-scene Gaussian depth-of-field.
* @param focusDistance View-space focus plane.
* @param maxBlurPx Max blur radius in texels; <= 0 disables.
* @param focusRange Distance from focus where blur reaches the max.
* @param nearZ Camera near clip used to linearize hardware depth.
* @param farZ Camera far clip used to linearize hardware depth.
*/
virtual void setSceneDepthOfField(float focusDistance, float maxBlurPx, float focusRange,
float nearZ, float farZ) = 0;
virtual float getSceneDofFocusDistance() const = 0;
virtual float getSceneDofMaxBlur() const = 0;
virtual float getSceneDofFocusRange() const = 0;
virtual float getSceneDofNearZ() const = 0;
virtual float getSceneDofFarZ() const = 0;
/** @brief Upload CSM constants for subsequent default mesh draws (active=false disables). */
virtual void setMesh3DShadows(const ShadowUpload &upload) = 0;
@@ -1724,13 +1740,16 @@ public:
/** @brief Pipeline-owned linear-HDR bloom pyramid, created on first use.
* @lifetime Returned effect remains valid until Graphics shutdown. */
Bloom *pipelineBloom();
/** @brief Pipeline-owned Gaussian depth-of-field, created on first use.
* @lifetime Returned effect remains valid until Graphics shutdown. */
DepthOfField *pipelineDepthOfField();
/** @brief Pipeline-owned GPU exposure metering and eye adaptation.
* @lifetime Returned effect remains valid until Graphics shutdown. */
Exposure *pipelineExposure();
/** @brief Pipeline-owned shared min/max depth hierarchy for screen-space effects.
* @lifetime Returned effect remains valid until Graphics shutdown. */
DepthPyramid *pipelineDepthPyramid();
/** @brief Build the shared linear-HDR AA, bloom, and exposure result for final ACES.
/** @brief Build the shared linear-HDR AA, DOF, bloom, and exposure result for final ACES.
* @lifetime Returned texture is Graphics-owned and valid for the current target allocation. */
Texture *prepareFinalSceneTexture(Texture *scene, Texture *motion = nullptr);
/** @brief Return a reusable HDR target for composing screen-space lighting.
@@ -1910,6 +1929,7 @@ protected:
std::unique_ptr<ScreenSpaceReflection> pipelineSSR_;
std::unique_ptr<AntiAliasing> pipelineAA_;
std::unique_ptr<Bloom> pipelineBloom_;
std::unique_ptr<DepthOfField> pipelineDof_;
std::unique_ptr<Exposure> pipelineExposure_;
std::unique_ptr<DepthPyramid> pipelineDepthPyramid_;
Canvas *spatialAAResolve_ = nullptr;
+15
View File
@@ -233,6 +233,17 @@ void Camera3D::setBloom(float intensity, float threshold) {
float Camera3D::getBloomIntensity() { return data()->bloomIntensity; }
float Camera3D::getBloomThreshold() { return data()->bloomThreshold; }
void Camera3D::setDepthOfField(float focusDistance, float maxBlurPx, float focusRange) {
auto d = data();
d->dofFocusDistance = std::max(0.f, focusDistance);
d->dofMaxBlurPx = std::clamp(maxBlurPx, 0.f, 32.f);
d->dofFocusRange = std::max(1e-3f, focusRange);
}
float Camera3D::getDofFocusDistance() { return data()->dofFocusDistance; }
float Camera3D::getDofMaxBlur() { return data()->dofMaxBlurPx; }
float Camera3D::getDofFocusRange() { return data()->dofFocusRange; }
void Camera3D::setEnvProbe(float centerX, float centerY, float centerZ, float extentX,
float extentY, float extentZ) {
data()->envProbeCenter = glm::vec3(centerX, centerY, centerZ);
@@ -857,6 +868,8 @@ void RenderSystem3D::render(Graphics &gfx) {
gfx.setSceneAutoExposure(cd->autoExposure, cd->autoExposureMinEV,
cd->autoExposureMaxEV);
gfx.setSceneBloom(cd->bloomIntensity, cd->bloomThreshold);
gfx.setSceneDepthOfField(cd->dofFocusDistance, cd->dofMaxBlurPx, cd->dofFocusRange,
cd->nearZ, cd->farZ);
}
gfx.begin3DFrame();
if (!gfx.had3DThisFrame()) return;
@@ -1330,6 +1343,8 @@ void RenderSystem3D::renderToCanvas(Graphics &gfx, Canvas *target, Camera3D *cam
gfx.setSceneAutoExposure(cd->autoExposure, cd->autoExposureMinEV,
cd->autoExposureMaxEV);
gfx.setSceneBloom(cd->bloomIntensity, cd->bloomThreshold);
gfx.setSceneDepthOfField(cd->dofFocusDistance, cd->dofMaxBlurPx, cd->dofFocusRange, cd->nearZ,
cd->farZ);
// Lighting: real lights (if any) + camera ambient; shadows/clustered off.
std::vector<PackedLight3D> packed;
+16
View File
@@ -55,6 +55,9 @@ public:
float autoExposureMaxEV = 8.f;
float bloomIntensity = 0.f;
float bloomThreshold = 1.f;
float dofFocusDistance = 0.f;
float dofMaxBlurPx = 0.f; // <= 0 disables DOF
float dofFocusRange = 8.f;
bool active = false;
Camera3D *entity = nullptr;
// Last screenToRay() result (origin = eye, dir normalized).
@@ -111,6 +114,19 @@ public:
float getBloomIntensity();
/** @brief Return bloom threshold in linear HDR units. */
float getBloomThreshold();
/**
* @brief Configure Gaussian depth-of-field on the final HDR resolve.
* @param focusDistance View-space distance of the sharp plane.
* @param maxBlurPx Maximum blur radius in texels; <= 0 disables DOF.
* @param focusRange Distance from focus at which blur reaches maxBlurPx.
*/
void setDepthOfField(float focusDistance, float maxBlurPx, float focusRange = 8.f);
/** @brief Return DOF focus distance. */
float getDofFocusDistance();
/** @brief Return DOF max blur in texels (0 when disabled). */
float getDofMaxBlur();
/** @brief Return DOF focus range. */
float getDofFocusRange();
/** @brief Enable box-projected IBL for the camera environment cubemap. */
void setEnvProbe(float centerX, float centerY, float centerZ, float extentX, float extentY,
float extentZ);
@@ -122,4 +122,21 @@ fn viewz(z:f32)->f32{let n=max(p(10),.0001);let f=max(p(11),n+.001);return n*f/m
let nt=clamp(p(3),0,2);let edge=max(de,smoothstep(nt*(1-clamp(p(4),0,1)),nt+clamp(p(4),0,1),ne));return vec4f(color,clamp(edge,0,1))*i.color;}
)wgsl";
/** @lifetime Shader source has static storage for the process lifetime. */
inline constexpr const char *kDofGaussian = R"wgsl(
fn viewz(z:f32)->f32{let n=max(p(7),.0001);let f=max(p(8),n+.001);return n*f/max(f-z*(f-n),.000001);}
fn coc(viewZ:f32)->f32{let focus=max(p(4),0);let range=max(p(5),.001);let maxBlur=max(p(6),0);var t=clamp(abs(viewZ-focus)/range,0,1);t=t*t*(3-2*t);return t*maxBlur;}
@fragment fn fs_main(i:FSIn)->@location(0) vec4f{
let maxBlur=max(p(6),0);if maxBlur<=.0001{return tex(i.uv)*i.color;}
let ndc=aux_load(i.uv).r;let vz=viewz(ndc);let radius=coc(vz);
if radius<.35||ndc>=.9999{return tex(i.uv)*i.color;}
let stepUv=vec2f(p(2),p(3))*vec2f(p(0),p(1))*radius;
let kw=array<f32,5>(.227027,.1945946,.1216216,.054054,.016216);
var color=tex(i.uv).rgb*kw[0];var weight=kw[0];
for(var s=1;s<5;s++){let off=stepUv*f32(s);let uvA=clamp(i.uv+off,vec2f(0),vec2f(1));let uvB=clamp(i.uv-off,vec2f(0),vec2f(1));
let wA=kw[s]*select(0.0,1.0,coc(viewz(aux_load(uvA).r))>=radius*.35);let wB=kw[s]*select(0.0,1.0,coc(viewz(aux_load(uvB).r))>=radius*.35);
color+=tex(uvA).rgb*wA;color+=tex(uvB).rgb*wB;weight+=wA+wB;}
return vec4f(color/max(weight,.0001),1)*i.color;}
)wgsl";
} // namespace eve::graphics::shaders
@@ -0,0 +1,72 @@
#version 450
// Separable Gaussian depth-of-field for HD-2D miniature look.
//
// MainTex (binding 0) = linear HDR scene color.
// DepthTex(binding 1) = hardware D32 depth, .r = Vulkan NDC z (0 near, 1 far).
//
// Push constants (declareFloat order):
// 0 texelW 1 texelH 2 directionX 3 directionY
// 4 focusDistance 5 focusRange 6 maxBlurPx
// 7 nearZ 8 farZ
layout(location = 0) in vec4 fragColor;
layout(location = 1) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(binding = 0) uniform sampler2D MainTex;
layout(binding = 1) uniform sampler2D DepthTex;
layout(push_constant) uniform Externals { float data[32]; } u;
float toViewZ(float ndcZ) {
float nearZ = max(u.data[7], 1e-4);
float farZ = max(u.data[8], nearZ + 1e-3);
return (nearZ * farZ) / max(farZ - ndcZ * (farZ - nearZ), 1e-6);
}
float cocRadius(float viewZ) {
float focus = max(u.data[4], 0.0);
float range = max(u.data[5], 1e-3);
float maxBlur = max(u.data[6], 0.0);
float t = clamp(abs(viewZ - focus) / range, 0.0, 1.0);
// Smoothstep keeps the in-focus band readable for pixel characters.
t = t * t * (3.0 - 2.0 * t);
return t * maxBlur;
}
void main() {
float maxBlur = max(u.data[6], 0.0);
if (maxBlur <= 1e-4) {
outColor = texture(MainTex, fragUV) * fragColor;
return;
}
float ndcZ = texture(DepthTex, fragUV).r;
float viewZ = toViewZ(ndcZ);
float radius = cocRadius(viewZ);
if (radius < 0.35 || ndcZ >= 0.9999) {
outColor = texture(MainTex, fragUV) * fragColor;
return;
}
vec2 dir = vec2(u.data[2], u.data[3]);
vec2 stepUv = dir * vec2(u.data[0], u.data[1]) * radius;
// 9-tap Gaussian (sigma ~= 2) along the separable axis.
const float kW[5] = float[5](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
vec3 color = texture(MainTex, fragUV).rgb * kW[0];
float weight = kW[0];
for (int i = 1; i < 5; ++i) {
float fi = float(i);
vec2 off = stepUv * fi;
vec2 uvA = clamp(fragUV + off, vec2(0.0), vec2(1.0));
vec2 uvB = clamp(fragUV - off, vec2(0.0), vec2(1.0));
float zA = toViewZ(texture(DepthTex, uvA).r);
float zB = toViewZ(texture(DepthTex, uvB).r);
// Reject samples whose CoC is much smaller (keeps sharp sprites from bleeding).
float wA = kW[i] * step(radius * 0.35, max(cocRadius(zA), 0.35));
float wB = kW[i] * step(radius * 0.35, max(cocRadius(zB), 0.35));
color += texture(MainTex, uvA).rgb * wA;
color += texture(MainTex, uvB).rgb * wB;
weight += wA + wB;
}
outColor = vec4(color / max(weight, 1e-4), 1.0) * fragColor;
}
@@ -0,0 +1,213 @@
static const uint32_t dof_gaussian_frag_spv[] = {
0x07230203, 0x00010000, 0x000d000b, 0x00000121, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0008000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000067, 0x0000006f, 0x00000073,
0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47,
0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576,
0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669,
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x0000000a, 0x69566f74, 0x285a7765,
0x003b3166, 0x00040005, 0x00000009, 0x5a63646e, 0x00000000, 0x00060005, 0x0000000d, 0x52636f63,
0x75696461, 0x31662873, 0x0000003b, 0x00040005, 0x0000000c, 0x77656976, 0x0000005a, 0x00040005,
0x0000000f, 0x7261656e, 0x0000005a, 0x00050005, 0x00000013, 0x65747845, 0x6c616e72, 0x00000073,
0x00050006, 0x00000013, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x00000015, 0x00000075,
0x00040005, 0x0000001e, 0x5a726166, 0x00000000, 0x00040005, 0x00000035, 0x75636f66, 0x00000073,
0x00040005, 0x0000003b, 0x676e6172, 0x00000065, 0x00040005, 0x00000040, 0x4278616d, 0x0072756c,
0x00030005, 0x00000045, 0x00000074, 0x00040005, 0x0000005c, 0x4278616d, 0x0072756c, 0x00050005,
0x00000067, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00040005, 0x0000006b, 0x6e69614d, 0x00786554,
0x00040005, 0x0000006f, 0x67617266, 0x00005655, 0x00050005, 0x00000073, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040005, 0x00000077, 0x5a63646e, 0x00000000, 0x00050005, 0x00000078, 0x74706544,
0x78655468, 0x00000000, 0x00040005, 0x0000007e, 0x77656976, 0x0000005a, 0x00040005, 0x0000007f,
0x61726170, 0x0000006d, 0x00040005, 0x00000082, 0x69646172, 0x00007375, 0x00040005, 0x00000083,
0x61726170, 0x0000006d, 0x00030005, 0x00000096, 0x00726964, 0x00040005, 0x0000009e, 0x70657473,
0x00007655, 0x00040005, 0x000000ab, 0x6f6c6f63, 0x00000072, 0x00040005, 0x000000b2, 0x67696577,
0x00007468, 0x00030005, 0x000000b4, 0x00000069, 0x00030005, 0x000000bc, 0x00006966, 0x00030005,
0x000000bf, 0x0066666f, 0x00030005, 0x000000c3, 0x00417675, 0x00030005, 0x000000ca, 0x00427675,
0x00030005, 0x000000cf, 0x0000417a, 0x00040005, 0x000000d3, 0x61726170, 0x0000006d, 0x00030005,
0x000000d6, 0x0000427a, 0x00040005, 0x000000da, 0x61726170, 0x0000006d, 0x00030005, 0x000000dd,
0x00004177, 0x00050005, 0x000000e7, 0x65646e69, 0x6c626178, 0x00000065, 0x00040005, 0x000000ec,
0x61726170, 0x0000006d, 0x00030005, 0x000000f2, 0x00004277, 0x00050005, 0x000000f4, 0x65646e69,
0x6c626178, 0x00000065, 0x00040005, 0x000000f9, 0x61726170, 0x0000006d, 0x00040047, 0x00000012,
0x00000006, 0x00000004, 0x00050048, 0x00000013, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
0x00000013, 0x00000002, 0x00040047, 0x00000067, 0x0000001e, 0x00000000, 0x00040047, 0x0000006b,
0x00000022, 0x00000000, 0x00040047, 0x0000006b, 0x00000021, 0x00000000, 0x00040047, 0x0000006f,
0x0000001e, 0x00000001, 0x00040047, 0x00000073, 0x0000001e, 0x00000000, 0x00040047, 0x00000078,
0x00000022, 0x00000000, 0x00040047, 0x00000078, 0x00000021, 0x00000001, 0x00020013, 0x00000002,
0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040020, 0x00000007,
0x00000007, 0x00000006, 0x00040021, 0x00000008, 0x00000006, 0x00000007, 0x00040015, 0x00000010,
0x00000020, 0x00000000, 0x0004002b, 0x00000010, 0x00000011, 0x00000020, 0x0004001c, 0x00000012,
0x00000006, 0x00000011, 0x0003001e, 0x00000013, 0x00000012, 0x00040020, 0x00000014, 0x00000009,
0x00000013, 0x0004003b, 0x00000014, 0x00000015, 0x00000009, 0x00040015, 0x00000016, 0x00000020,
0x00000001, 0x0004002b, 0x00000016, 0x00000017, 0x00000000, 0x0004002b, 0x00000016, 0x00000018,
0x00000007, 0x00040020, 0x00000019, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x0000001c,
0x38d1b717, 0x0004002b, 0x00000016, 0x0000001f, 0x00000008, 0x0004002b, 0x00000006, 0x00000023,
0x3a83126f, 0x0004002b, 0x00000006, 0x00000030, 0x358637bd, 0x0004002b, 0x00000016, 0x00000036,
0x00000004, 0x0004002b, 0x00000006, 0x00000039, 0x00000000, 0x0004002b, 0x00000016, 0x0000003c,
0x00000005, 0x0004002b, 0x00000016, 0x00000041, 0x00000006, 0x0004002b, 0x00000006, 0x0000004c,
0x3f800000, 0x0004002b, 0x00000006, 0x00000051, 0x40400000, 0x0004002b, 0x00000006, 0x00000052,
0x40000000, 0x00020014, 0x00000061, 0x00040017, 0x00000065, 0x00000006, 0x00000004, 0x00040020,
0x00000066, 0x00000003, 0x00000065, 0x0004003b, 0x00000066, 0x00000067, 0x00000003, 0x00090019,
0x00000068, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
0x0003001b, 0x00000069, 0x00000068, 0x00040020, 0x0000006a, 0x00000000, 0x00000069, 0x0004003b,
0x0000006a, 0x0000006b, 0x00000000, 0x00040017, 0x0000006d, 0x00000006, 0x00000002, 0x00040020,
0x0000006e, 0x00000001, 0x0000006d, 0x0004003b, 0x0000006e, 0x0000006f, 0x00000001, 0x00040020,
0x00000072, 0x00000001, 0x00000065, 0x0004003b, 0x00000072, 0x00000073, 0x00000001, 0x0004003b,
0x0000006a, 0x00000078, 0x00000000, 0x0004002b, 0x00000010, 0x0000007c, 0x00000000, 0x0004002b,
0x00000006, 0x00000087, 0x3eb33333, 0x0004002b, 0x00000006, 0x0000008a, 0x3f7ff972, 0x00040020,
0x00000095, 0x00000007, 0x0000006d, 0x0004002b, 0x00000016, 0x00000097, 0x00000002, 0x0004002b,
0x00000016, 0x0000009a, 0x00000003, 0x0004002b, 0x00000016, 0x000000a2, 0x00000001, 0x00040017,
0x000000a9, 0x00000006, 0x00000003, 0x00040020, 0x000000aa, 0x00000007, 0x000000a9, 0x0004002b,
0x00000006, 0x000000b0, 0x3e6879c4, 0x00040020, 0x000000b3, 0x00000007, 0x00000016, 0x0005002c,
0x0000006d, 0x000000c7, 0x00000039, 0x00000039, 0x0005002c, 0x0000006d, 0x000000c8, 0x0000004c,
0x0000004c, 0x0004002b, 0x00000010, 0x000000de, 0x00000005, 0x0004001c, 0x000000df, 0x00000006,
0x000000de, 0x0004002b, 0x00000006, 0x000000e0, 0x3e4743cf, 0x0004002b, 0x00000006, 0x000000e1,
0x3df914bf, 0x0004002b, 0x00000006, 0x000000e2, 0x3d5d67ba, 0x0004002b, 0x00000006, 0x000000e3,
0x3c84d76b, 0x0008002c, 0x000000df, 0x000000e4, 0x000000b0, 0x000000e0, 0x000000e1, 0x000000e2,
0x000000e3, 0x00040020, 0x000000e6, 0x00000007, 0x000000df, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000007, 0x0000005c, 0x00000007,
0x0004003b, 0x00000007, 0x00000077, 0x00000007, 0x0004003b, 0x00000007, 0x0000007e, 0x00000007,
0x0004003b, 0x00000007, 0x0000007f, 0x00000007, 0x0004003b, 0x00000007, 0x00000082, 0x00000007,
0x0004003b, 0x00000007, 0x00000083, 0x00000007, 0x0004003b, 0x00000095, 0x00000096, 0x00000007,
0x0004003b, 0x00000095, 0x0000009e, 0x00000007, 0x0004003b, 0x000000aa, 0x000000ab, 0x00000007,
0x0004003b, 0x00000007, 0x000000b2, 0x00000007, 0x0004003b, 0x000000b3, 0x000000b4, 0x00000007,
0x0004003b, 0x00000007, 0x000000bc, 0x00000007, 0x0004003b, 0x00000095, 0x000000bf, 0x00000007,
0x0004003b, 0x00000095, 0x000000c3, 0x00000007, 0x0004003b, 0x00000095, 0x000000ca, 0x00000007,
0x0004003b, 0x00000007, 0x000000cf, 0x00000007, 0x0004003b, 0x00000007, 0x000000d3, 0x00000007,
0x0004003b, 0x00000007, 0x000000d6, 0x00000007, 0x0004003b, 0x00000007, 0x000000da, 0x00000007,
0x0004003b, 0x00000007, 0x000000dd, 0x00000007, 0x0004003b, 0x000000e6, 0x000000e7, 0x00000007,
0x0004003b, 0x00000007, 0x000000ec, 0x00000007, 0x0004003b, 0x00000007, 0x000000f2, 0x00000007,
0x0004003b, 0x000000e6, 0x000000f4, 0x00000007, 0x0004003b, 0x00000007, 0x000000f9, 0x00000007,
0x00060041, 0x00000019, 0x0000005d, 0x00000015, 0x00000017, 0x00000041, 0x0004003d, 0x00000006,
0x0000005e, 0x0000005d, 0x0007000c, 0x00000006, 0x0000005f, 0x00000001, 0x00000028, 0x0000005e,
0x00000039, 0x0003003e, 0x0000005c, 0x0000005f, 0x0004003d, 0x00000006, 0x00000060, 0x0000005c,
0x000500bc, 0x00000061, 0x00000062, 0x00000060, 0x0000001c, 0x000300f7, 0x00000064, 0x00000000,
0x000400fa, 0x00000062, 0x00000063, 0x00000064, 0x000200f8, 0x00000063, 0x0004003d, 0x00000069,
0x0000006c, 0x0000006b, 0x0004003d, 0x0000006d, 0x00000070, 0x0000006f, 0x00050057, 0x00000065,
0x00000071, 0x0000006c, 0x00000070, 0x0004003d, 0x00000065, 0x00000074, 0x00000073, 0x00050085,
0x00000065, 0x00000075, 0x00000071, 0x00000074, 0x0003003e, 0x00000067, 0x00000075, 0x000100fd,
0x000200f8, 0x00000064, 0x0004003d, 0x00000069, 0x00000079, 0x00000078, 0x0004003d, 0x0000006d,
0x0000007a, 0x0000006f, 0x00050057, 0x00000065, 0x0000007b, 0x00000079, 0x0000007a, 0x00050051,
0x00000006, 0x0000007d, 0x0000007b, 0x00000000, 0x0003003e, 0x00000077, 0x0000007d, 0x0004003d,
0x00000006, 0x00000080, 0x00000077, 0x0003003e, 0x0000007f, 0x00000080, 0x00050039, 0x00000006,
0x00000081, 0x0000000a, 0x0000007f, 0x0003003e, 0x0000007e, 0x00000081, 0x0004003d, 0x00000006,
0x00000084, 0x0000007e, 0x0003003e, 0x00000083, 0x00000084, 0x00050039, 0x00000006, 0x00000085,
0x0000000d, 0x00000083, 0x0003003e, 0x00000082, 0x00000085, 0x0004003d, 0x00000006, 0x00000086,
0x00000082, 0x000500b8, 0x00000061, 0x00000088, 0x00000086, 0x00000087, 0x0004003d, 0x00000006,
0x00000089, 0x00000077, 0x000500be, 0x00000061, 0x0000008b, 0x00000089, 0x0000008a, 0x000500a6,
0x00000061, 0x0000008c, 0x00000088, 0x0000008b, 0x000300f7, 0x0000008e, 0x00000000, 0x000400fa,
0x0000008c, 0x0000008d, 0x0000008e, 0x000200f8, 0x0000008d, 0x0004003d, 0x00000069, 0x0000008f,
0x0000006b, 0x0004003d, 0x0000006d, 0x00000090, 0x0000006f, 0x00050057, 0x00000065, 0x00000091,
0x0000008f, 0x00000090, 0x0004003d, 0x00000065, 0x00000092, 0x00000073, 0x00050085, 0x00000065,
0x00000093, 0x00000091, 0x00000092, 0x0003003e, 0x00000067, 0x00000093, 0x000100fd, 0x000200f8,
0x0000008e, 0x00060041, 0x00000019, 0x00000098, 0x00000015, 0x00000017, 0x00000097, 0x0004003d,
0x00000006, 0x00000099, 0x00000098, 0x00060041, 0x00000019, 0x0000009b, 0x00000015, 0x00000017,
0x0000009a, 0x0004003d, 0x00000006, 0x0000009c, 0x0000009b, 0x00050050, 0x0000006d, 0x0000009d,
0x00000099, 0x0000009c, 0x0003003e, 0x00000096, 0x0000009d, 0x0004003d, 0x0000006d, 0x0000009f,
0x00000096, 0x00060041, 0x00000019, 0x000000a0, 0x00000015, 0x00000017, 0x00000017, 0x0004003d,
0x00000006, 0x000000a1, 0x000000a0, 0x00060041, 0x00000019, 0x000000a3, 0x00000015, 0x00000017,
0x000000a2, 0x0004003d, 0x00000006, 0x000000a4, 0x000000a3, 0x00050050, 0x0000006d, 0x000000a5,
0x000000a1, 0x000000a4, 0x00050085, 0x0000006d, 0x000000a6, 0x0000009f, 0x000000a5, 0x0004003d,
0x00000006, 0x000000a7, 0x00000082, 0x0005008e, 0x0000006d, 0x000000a8, 0x000000a6, 0x000000a7,
0x0003003e, 0x0000009e, 0x000000a8, 0x0004003d, 0x00000069, 0x000000ac, 0x0000006b, 0x0004003d,
0x0000006d, 0x000000ad, 0x0000006f, 0x00050057, 0x00000065, 0x000000ae, 0x000000ac, 0x000000ad,
0x0008004f, 0x000000a9, 0x000000af, 0x000000ae, 0x000000ae, 0x00000000, 0x00000001, 0x00000002,
0x0005008e, 0x000000a9, 0x000000b1, 0x000000af, 0x000000b0, 0x0003003e, 0x000000ab, 0x000000b1,
0x0003003e, 0x000000b2, 0x000000b0, 0x0003003e, 0x000000b4, 0x000000a2, 0x000200f9, 0x000000b5,
0x000200f8, 0x000000b5, 0x000400f6, 0x000000b7, 0x000000b8, 0x00000000, 0x000200f9, 0x000000b9,
0x000200f8, 0x000000b9, 0x0004003d, 0x00000016, 0x000000ba, 0x000000b4, 0x000500b1, 0x00000061,
0x000000bb, 0x000000ba, 0x0000003c, 0x000400fa, 0x000000bb, 0x000000b6, 0x000000b7, 0x000200f8,
0x000000b6, 0x0004003d, 0x00000016, 0x000000bd, 0x000000b4, 0x0004006f, 0x00000006, 0x000000be,
0x000000bd, 0x0003003e, 0x000000bc, 0x000000be, 0x0004003d, 0x0000006d, 0x000000c0, 0x0000009e,
0x0004003d, 0x00000006, 0x000000c1, 0x000000bc, 0x0005008e, 0x0000006d, 0x000000c2, 0x000000c0,
0x000000c1, 0x0003003e, 0x000000bf, 0x000000c2, 0x0004003d, 0x0000006d, 0x000000c4, 0x0000006f,
0x0004003d, 0x0000006d, 0x000000c5, 0x000000bf, 0x00050081, 0x0000006d, 0x000000c6, 0x000000c4,
0x000000c5, 0x0008000c, 0x0000006d, 0x000000c9, 0x00000001, 0x0000002b, 0x000000c6, 0x000000c7,
0x000000c8, 0x0003003e, 0x000000c3, 0x000000c9, 0x0004003d, 0x0000006d, 0x000000cb, 0x0000006f,
0x0004003d, 0x0000006d, 0x000000cc, 0x000000bf, 0x00050083, 0x0000006d, 0x000000cd, 0x000000cb,
0x000000cc, 0x0008000c, 0x0000006d, 0x000000ce, 0x00000001, 0x0000002b, 0x000000cd, 0x000000c7,
0x000000c8, 0x0003003e, 0x000000ca, 0x000000ce, 0x0004003d, 0x00000069, 0x000000d0, 0x00000078,
0x0004003d, 0x0000006d, 0x000000d1, 0x000000c3, 0x00050057, 0x00000065, 0x000000d2, 0x000000d0,
0x000000d1, 0x00050051, 0x00000006, 0x000000d4, 0x000000d2, 0x00000000, 0x0003003e, 0x000000d3,
0x000000d4, 0x00050039, 0x00000006, 0x000000d5, 0x0000000a, 0x000000d3, 0x0003003e, 0x000000cf,
0x000000d5, 0x0004003d, 0x00000069, 0x000000d7, 0x00000078, 0x0004003d, 0x0000006d, 0x000000d8,
0x000000ca, 0x00050057, 0x00000065, 0x000000d9, 0x000000d7, 0x000000d8, 0x00050051, 0x00000006,
0x000000db, 0x000000d9, 0x00000000, 0x0003003e, 0x000000da, 0x000000db, 0x00050039, 0x00000006,
0x000000dc, 0x0000000a, 0x000000da, 0x0003003e, 0x000000d6, 0x000000dc, 0x0004003d, 0x00000016,
0x000000e5, 0x000000b4, 0x0003003e, 0x000000e7, 0x000000e4, 0x00050041, 0x00000007, 0x000000e8,
0x000000e7, 0x000000e5, 0x0004003d, 0x00000006, 0x000000e9, 0x000000e8, 0x0004003d, 0x00000006,
0x000000ea, 0x00000082, 0x00050085, 0x00000006, 0x000000eb, 0x000000ea, 0x00000087, 0x0004003d,
0x00000006, 0x000000ed, 0x000000cf, 0x0003003e, 0x000000ec, 0x000000ed, 0x00050039, 0x00000006,
0x000000ee, 0x0000000d, 0x000000ec, 0x0007000c, 0x00000006, 0x000000ef, 0x00000001, 0x00000028,
0x000000ee, 0x00000087, 0x0007000c, 0x00000006, 0x000000f0, 0x00000001, 0x00000030, 0x000000eb,
0x000000ef, 0x00050085, 0x00000006, 0x000000f1, 0x000000e9, 0x000000f0, 0x0003003e, 0x000000dd,
0x000000f1, 0x0004003d, 0x00000016, 0x000000f3, 0x000000b4, 0x0003003e, 0x000000f4, 0x000000e4,
0x00050041, 0x00000007, 0x000000f5, 0x000000f4, 0x000000f3, 0x0004003d, 0x00000006, 0x000000f6,
0x000000f5, 0x0004003d, 0x00000006, 0x000000f7, 0x00000082, 0x00050085, 0x00000006, 0x000000f8,
0x000000f7, 0x00000087, 0x0004003d, 0x00000006, 0x000000fa, 0x000000d6, 0x0003003e, 0x000000f9,
0x000000fa, 0x00050039, 0x00000006, 0x000000fb, 0x0000000d, 0x000000f9, 0x0007000c, 0x00000006,
0x000000fc, 0x00000001, 0x00000028, 0x000000fb, 0x00000087, 0x0007000c, 0x00000006, 0x000000fd,
0x00000001, 0x00000030, 0x000000f8, 0x000000fc, 0x00050085, 0x00000006, 0x000000fe, 0x000000f6,
0x000000fd, 0x0003003e, 0x000000f2, 0x000000fe, 0x0004003d, 0x00000069, 0x000000ff, 0x0000006b,
0x0004003d, 0x0000006d, 0x00000100, 0x000000c3, 0x00050057, 0x00000065, 0x00000101, 0x000000ff,
0x00000100, 0x0008004f, 0x000000a9, 0x00000102, 0x00000101, 0x00000101, 0x00000000, 0x00000001,
0x00000002, 0x0004003d, 0x00000006, 0x00000103, 0x000000dd, 0x0005008e, 0x000000a9, 0x00000104,
0x00000102, 0x00000103, 0x0004003d, 0x000000a9, 0x00000105, 0x000000ab, 0x00050081, 0x000000a9,
0x00000106, 0x00000105, 0x00000104, 0x0003003e, 0x000000ab, 0x00000106, 0x0004003d, 0x00000069,
0x00000107, 0x0000006b, 0x0004003d, 0x0000006d, 0x00000108, 0x000000ca, 0x00050057, 0x00000065,
0x00000109, 0x00000107, 0x00000108, 0x0008004f, 0x000000a9, 0x0000010a, 0x00000109, 0x00000109,
0x00000000, 0x00000001, 0x00000002, 0x0004003d, 0x00000006, 0x0000010b, 0x000000f2, 0x0005008e,
0x000000a9, 0x0000010c, 0x0000010a, 0x0000010b, 0x0004003d, 0x000000a9, 0x0000010d, 0x000000ab,
0x00050081, 0x000000a9, 0x0000010e, 0x0000010d, 0x0000010c, 0x0003003e, 0x000000ab, 0x0000010e,
0x0004003d, 0x00000006, 0x0000010f, 0x000000dd, 0x0004003d, 0x00000006, 0x00000110, 0x000000f2,
0x00050081, 0x00000006, 0x00000111, 0x0000010f, 0x00000110, 0x0004003d, 0x00000006, 0x00000112,
0x000000b2, 0x00050081, 0x00000006, 0x00000113, 0x00000112, 0x00000111, 0x0003003e, 0x000000b2,
0x00000113, 0x000200f9, 0x000000b8, 0x000200f8, 0x000000b8, 0x0004003d, 0x00000016, 0x00000114,
0x000000b4, 0x00050080, 0x00000016, 0x00000115, 0x00000114, 0x000000a2, 0x0003003e, 0x000000b4,
0x00000115, 0x000200f9, 0x000000b5, 0x000200f8, 0x000000b7, 0x0004003d, 0x000000a9, 0x00000116,
0x000000ab, 0x0004003d, 0x00000006, 0x00000117, 0x000000b2, 0x0007000c, 0x00000006, 0x00000118,
0x00000001, 0x00000028, 0x00000117, 0x0000001c, 0x00060050, 0x000000a9, 0x00000119, 0x00000118,
0x00000118, 0x00000118, 0x00050088, 0x000000a9, 0x0000011a, 0x00000116, 0x00000119, 0x00050051,
0x00000006, 0x0000011b, 0x0000011a, 0x00000000, 0x00050051, 0x00000006, 0x0000011c, 0x0000011a,
0x00000001, 0x00050051, 0x00000006, 0x0000011d, 0x0000011a, 0x00000002, 0x00070050, 0x00000065,
0x0000011e, 0x0000011b, 0x0000011c, 0x0000011d, 0x0000004c, 0x0004003d, 0x00000065, 0x0000011f,
0x00000073, 0x00050085, 0x00000065, 0x00000120, 0x0000011e, 0x0000011f, 0x0003003e, 0x00000067,
0x00000120, 0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x0000000a, 0x00000000, 0x00000008,
0x00030037, 0x00000007, 0x00000009, 0x000200f8, 0x0000000b, 0x0004003b, 0x00000007, 0x0000000f,
0x00000007, 0x0004003b, 0x00000007, 0x0000001e, 0x00000007, 0x00060041, 0x00000019, 0x0000001a,
0x00000015, 0x00000017, 0x00000018, 0x0004003d, 0x00000006, 0x0000001b, 0x0000001a, 0x0007000c,
0x00000006, 0x0000001d, 0x00000001, 0x00000028, 0x0000001b, 0x0000001c, 0x0003003e, 0x0000000f,
0x0000001d, 0x00060041, 0x00000019, 0x00000020, 0x00000015, 0x00000017, 0x0000001f, 0x0004003d,
0x00000006, 0x00000021, 0x00000020, 0x0004003d, 0x00000006, 0x00000022, 0x0000000f, 0x00050081,
0x00000006, 0x00000024, 0x00000022, 0x00000023, 0x0007000c, 0x00000006, 0x00000025, 0x00000001,
0x00000028, 0x00000021, 0x00000024, 0x0003003e, 0x0000001e, 0x00000025, 0x0004003d, 0x00000006,
0x00000026, 0x0000000f, 0x0004003d, 0x00000006, 0x00000027, 0x0000001e, 0x00050085, 0x00000006,
0x00000028, 0x00000026, 0x00000027, 0x0004003d, 0x00000006, 0x00000029, 0x0000001e, 0x0004003d,
0x00000006, 0x0000002a, 0x00000009, 0x0004003d, 0x00000006, 0x0000002b, 0x0000001e, 0x0004003d,
0x00000006, 0x0000002c, 0x0000000f, 0x00050083, 0x00000006, 0x0000002d, 0x0000002b, 0x0000002c,
0x00050085, 0x00000006, 0x0000002e, 0x0000002a, 0x0000002d, 0x00050083, 0x00000006, 0x0000002f,
0x00000029, 0x0000002e, 0x0007000c, 0x00000006, 0x00000031, 0x00000001, 0x00000028, 0x0000002f,
0x00000030, 0x00050088, 0x00000006, 0x00000032, 0x00000028, 0x00000031, 0x000200fe, 0x00000032,
0x00010038, 0x00050036, 0x00000006, 0x0000000d, 0x00000000, 0x00000008, 0x00030037, 0x00000007,
0x0000000c, 0x000200f8, 0x0000000e, 0x0004003b, 0x00000007, 0x00000035, 0x00000007, 0x0004003b,
0x00000007, 0x0000003b, 0x00000007, 0x0004003b, 0x00000007, 0x00000040, 0x00000007, 0x0004003b,
0x00000007, 0x00000045, 0x00000007, 0x00060041, 0x00000019, 0x00000037, 0x00000015, 0x00000017,
0x00000036, 0x0004003d, 0x00000006, 0x00000038, 0x00000037, 0x0007000c, 0x00000006, 0x0000003a,
0x00000001, 0x00000028, 0x00000038, 0x00000039, 0x0003003e, 0x00000035, 0x0000003a, 0x00060041,
0x00000019, 0x0000003d, 0x00000015, 0x00000017, 0x0000003c, 0x0004003d, 0x00000006, 0x0000003e,
0x0000003d, 0x0007000c, 0x00000006, 0x0000003f, 0x00000001, 0x00000028, 0x0000003e, 0x00000023,
0x0003003e, 0x0000003b, 0x0000003f, 0x00060041, 0x00000019, 0x00000042, 0x00000015, 0x00000017,
0x00000041, 0x0004003d, 0x00000006, 0x00000043, 0x00000042, 0x0007000c, 0x00000006, 0x00000044,
0x00000001, 0x00000028, 0x00000043, 0x00000039, 0x0003003e, 0x00000040, 0x00000044, 0x0004003d,
0x00000006, 0x00000046, 0x0000000c, 0x0004003d, 0x00000006, 0x00000047, 0x00000035, 0x00050083,
0x00000006, 0x00000048, 0x00000046, 0x00000047, 0x0006000c, 0x00000006, 0x00000049, 0x00000001,
0x00000004, 0x00000048, 0x0004003d, 0x00000006, 0x0000004a, 0x0000003b, 0x00050088, 0x00000006,
0x0000004b, 0x00000049, 0x0000004a, 0x0008000c, 0x00000006, 0x0000004d, 0x00000001, 0x0000002b,
0x0000004b, 0x00000039, 0x0000004c, 0x0003003e, 0x00000045, 0x0000004d, 0x0004003d, 0x00000006,
0x0000004e, 0x00000045, 0x0004003d, 0x00000006, 0x0000004f, 0x00000045, 0x00050085, 0x00000006,
0x00000050, 0x0000004e, 0x0000004f, 0x0004003d, 0x00000006, 0x00000053, 0x00000045, 0x00050085,
0x00000006, 0x00000054, 0x00000052, 0x00000053, 0x00050083, 0x00000006, 0x00000055, 0x00000051,
0x00000054, 0x00050085, 0x00000006, 0x00000056, 0x00000050, 0x00000055, 0x0003003e, 0x00000045,
0x00000056, 0x0004003d, 0x00000006, 0x00000057, 0x00000045, 0x0004003d, 0x00000006, 0x00000058,
0x00000040, 0x00050085, 0x00000006, 0x00000059, 0x00000057, 0x00000058, 0x000200fe, 0x00000059,
0x00010038,
};
static const size_t dof_gaussian_frag_spv_count = 1673;
+18
View File
@@ -609,6 +609,19 @@ public:
}
float getSceneBloomIntensity() const override { return sceneBloomIntensity; }
float getSceneBloomThreshold() const override { return sceneBloomThreshold; }
void setSceneDepthOfField(float focusDistance, float maxBlurPx, float focusRange, float nearZ,
float farZ) override {
sceneDofFocusDistance = focusDistance;
sceneDofMaxBlurPx = maxBlurPx;
sceneDofFocusRange = focusRange;
sceneDofNearZ = nearZ;
sceneDofFarZ = farZ;
}
float getSceneDofFocusDistance() const override { return sceneDofFocusDistance; }
float getSceneDofMaxBlur() const override { return sceneDofMaxBlurPx; }
float getSceneDofFocusRange() const override { return sceneDofFocusRange; }
float getSceneDofNearZ() const override { return sceneDofNearZ; }
float getSceneDofFarZ() const override { return sceneDofFarZ; }
void setMesh3DShadows(const ShadowUpload &upload) override;
void setMesh3DShadowReceive(bool receive) override;
void beginShadowPass(int cascadeIndex) override;
@@ -945,6 +958,11 @@ private:
float sceneAutoExposureMaxEV = 8.f;
float sceneBloomIntensity = 0.f;
float sceneBloomThreshold = 1.f;
float sceneDofFocusDistance = 0.f;
float sceneDofMaxBlurPx = 0.f;
float sceneDofFocusRange = 8.f;
float sceneDofNearZ = 0.1f;
float sceneDofFarZ = 100.f;
vk::PipelineLayout texPipelineLayout;
vk::PipelineLayout shaderPipelineLayout; // tex set + push constants
vk::CommandPool uploadPool;
+18
View File
@@ -408,6 +408,19 @@ public:
}
float getSceneBloomIntensity() const override { return sceneBloomIntensity; }
float getSceneBloomThreshold() const override { return sceneBloomThreshold; }
void setSceneDepthOfField(float focusDistance, float maxBlurPx, float focusRange, float nearZ,
float farZ) override {
sceneDofFocusDistance = focusDistance;
sceneDofMaxBlurPx = maxBlurPx;
sceneDofFocusRange = focusRange;
sceneDofNearZ = nearZ;
sceneDofFarZ = farZ;
}
float getSceneDofFocusDistance() const override { return sceneDofFocusDistance; }
float getSceneDofMaxBlur() const override { return sceneDofMaxBlurPx; }
float getSceneDofFocusRange() const override { return sceneDofFocusRange; }
float getSceneDofNearZ() const override { return sceneDofNearZ; }
float getSceneDofFarZ() const override { return sceneDofFarZ; }
void setMesh3DShadows(const ShadowUpload &upload) override;
void setMesh3DShadowReceive(bool receive) override;
void beginShadowPass(int cascadeIndex) override;
@@ -800,6 +813,11 @@ private:
float sceneAutoExposureMaxEV = 8.f;
float sceneBloomIntensity = 0.f;
float sceneBloomThreshold = 1.f;
float sceneDofFocusDistance = 0.f;
float sceneDofMaxBlurPx = 0.f;
float sceneDofFocusRange = 8.f;
float sceneDofNearZ = 0.1f;
float sceneDofFarZ = 100.f;
wgpu::RenderPipeline mesh3dPipeline;
wgpu::RenderPipeline mesh3dTransparentPipeline;
static constexpr size_t kMeshPipelineVariants = 20;
+29
View File
@@ -22,6 +22,10 @@ Sprite3D *Hd2D::newSprite(graphics::Graphics *gfx) {
return sprite;
}
Hd2dLook *Hd2D::newLook() { return new Hd2dLook(); }
Hd2dLook *Hd2D::newMiniatureLook() { return new Hd2dLook(Hd2dLook::miniature()); }
void Hd2D::expose(ssq::Table &table) {
auto cls = table.addClass(name, Hd2D::create, false);
expose(cls);
@@ -67,12 +71,37 @@ void Hd2D::expose(ssq::Table &table) {
sprite.addFunc("setTint", &Sprite3D::setTint);
sprite.addFunc("setVisible", &Sprite3D::setVisible);
sprite.addFunc("getVisible", &Sprite3D::getVisible);
sprite.addFunc("setAlphaCutoff", &Sprite3D::setAlphaCutoff);
sprite.addFunc("getAlphaCutoff", &Sprite3D::getAlphaCutoff);
sprite.addFunc("setDepthWrite", &Sprite3D::setDepthWrite);
sprite.addFunc("getDepthWrite", &Sprite3D::getDepthWrite);
sprite.addFunc("setDoubleSided", &Sprite3D::setDoubleSided);
sprite.addFunc("getDoubleSided", &Sprite3D::getDoubleSided);
sprite.addFunc("setBillboardMode", &Sprite3D::setBillboardMode);
sprite.addFunc("getBillboardMode", &Sprite3D::getBillboardMode);
auto look = table.addClass<Hd2dLook>(
"Hd2dLook", std::function<Hd2dLook *()>([]() { return nullptr; }), true);
look.addFunc("setFocusDistance", &Hd2dLook::setFocusDistance);
look.addFunc("getFocusDistance", &Hd2dLook::getFocusDistance);
look.addFunc("setMaxBlur", &Hd2dLook::setMaxBlur);
look.addFunc("getMaxBlur", &Hd2dLook::getMaxBlur);
look.addFunc("setFocusRange", &Hd2dLook::setFocusRange);
look.addFunc("getFocusRange", &Hd2dLook::getFocusRange);
look.addFunc("setBloomIntensity", &Hd2dLook::setBloomIntensity);
look.addFunc("getBloomIntensity", &Hd2dLook::getBloomIntensity);
look.addFunc("setBloomThreshold", &Hd2dLook::setBloomThreshold);
look.addFunc("getBloomThreshold", &Hd2dLook::getBloomThreshold);
look.addFunc("apply", &Hd2dLook::apply);
look.addFunc("applyPixelSampler", &Hd2dLook::applyPixelSampler);
}
void Hd2D::expose(ssq::Class &cls) {
cls.addFunc("getName", &Hd2D::getName);
cls.addFunc("newTileMap3D", &Hd2D::newTileMap3D);
cls.addFunc("newSprite", &Hd2D::newSprite);
cls.addFunc("newLook", &Hd2D::newLook);
cls.addFunc("newMiniatureLook", &Hd2D::newMiniatureLook);
}
} // namespace eve::hd2d
+96
View File
@@ -195,6 +195,28 @@ public:
void setVisible(bool visible);
bool getVisible() const;
/**
* @brief Alpha cutout threshold for the masked material (DopFix-style depth write).
* @param cutoff Finite value clamped to [0,1]; default 0.5.
*/
void setAlphaCutoff(float cutoff);
float getAlphaCutoff() const;
/**
* @brief Whether opaque/cutout texels write depth (required for DOF focus).
* Default true — matches HD2DURP DopFix / TransparentCutout materials.
*/
void setDepthWrite(bool enabled);
bool getDepthWrite() const;
/** @brief Render both faces of the billboard quad (default true). */
void setDoubleSided(bool enabled);
bool getDoubleSided() const;
/**
* @brief Billboard orientation mode.
* @param mode "screen" (default, full camera basis) or "yaw" (Y-up cylindrical).
*/
void setBillboardMode(const std::string &mode);
std::string getBillboardMode() const;
graphics::Mesh *quadMesh() const { return quad_; }
private:
@@ -209,6 +231,7 @@ private:
void buildQuad(graphics::Graphics *gfx);
void updateFrameUv();
void orientToCamera();
void syncMaterial();
graphics::Graphics *gfx_ = nullptr;
graphics::Texture *texture_ = nullptr;
@@ -231,6 +254,67 @@ private:
float pivotX_ = 0.5f, pivotY_ = 0.5f;
float tintR_ = 1.f, tintG_ = 1.f, tintB_ = 1.f, tintA_ = 1.f;
bool visible_ = true;
float alphaCutoff_ = 0.5f;
bool depthWrite_ = true;
bool doubleSided_ = true;
bool yawBillboard_ = false;
};
/**
* @brief HD-2D presentation look inspired by HD2DURP (DOF + bloom + pixel sampling).
*
* Applies camera post settings for the miniature look and nearest-neighbor
* filtering on sprite/terrain atlases. Depth-of-field needs Sprite3D masked
* cutout / DopFix depth writes so the focus plane can resolve.
*/
class Hd2dLook {
public:
Hd2dLook() = default;
/** @brief View-space focus plane distance (default 18). */
void setFocusDistance(float distance);
float getFocusDistance() const { return focusDistance_; }
/** @brief Max Gaussian blur radius in texels; 0 disables DOF (default 5). */
void setMaxBlur(float blurPx);
float getMaxBlur() const { return maxBlurPx_; }
/** @brief Distance from focus where blur reaches the max (default 14). */
void setFocusRange(float range);
float getFocusRange() const { return focusRange_; }
/** @brief HDR bloom intensity (default 0.35). */
void setBloomIntensity(float intensity);
float getBloomIntensity() const { return bloomIntensity_; }
/** @brief HDR bloom threshold (default 1.2). */
void setBloomThreshold(float threshold);
float getBloomThreshold() const { return bloomThreshold_; }
/**
* @brief Write DOF + bloom onto a Camera3D (render-thread).
* @param camera Non-null active scene camera.
* @ownership `camera` is borrowed; Hd2dLook does not retain it.
* @lifetime `camera` must remain valid only for this call.
*/
void apply(graphics::Camera3D *camera) const;
/**
* @brief Point-filter a texture for crisp pixel art (nearest / no mip).
* @param gfx Active Graphics.
* @param texture Atlas or sprite sheet.
* @ownership `gfx` and `texture` are borrowed; Hd2dLook does not retain them.
* @lifetime Both arguments must remain valid only for this call.
*/
void applyPixelSampler(graphics::Graphics *gfx, graphics::Texture *texture) const;
/** @brief Strong miniature preset matching HD2DURP-style shallow DOF. */
static Hd2dLook miniature();
/** @brief Mild look with light bloom and modest DOF. */
static Hd2dLook soft();
private:
float focusDistance_ = 18.f;
float maxBlurPx_ = 5.f;
float focusRange_ = 14.f;
float bloomIntensity_ = 0.35f;
float bloomThreshold_ = 1.2f;
};
/**
@@ -246,6 +330,18 @@ public:
TileMap3D *newTileMap3D();
/** @brief Create a 3D billboard sprite bound to the given Graphics. */
Sprite3D *newSprite(graphics::Graphics *gfx);
/**
* @brief Create an HD-2D look preset with soft defaults.
* @ownership Caller owns the returned object and must delete it.
* @lifetime Valid until the caller deletes it; independent of Hd2D.
*/
Hd2dLook *newLook();
/**
* @brief Create the miniature (strong DOF) look preset.
* @ownership Caller owns the returned object and must delete it.
* @lifetime Valid until the caller deletes it; independent of Hd2D.
*/
Hd2dLook *newMiniatureLook();
};
} // namespace eve::hd2d
+75
View File
@@ -0,0 +1,75 @@
#include "hd2d/Hd2d.h"
#include "common/Exception.h"
#include "graphics/Graphics.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Texture.h"
#include <algorithm>
#include <cmath>
namespace eve::hd2d {
void Hd2dLook::setFocusDistance(float distance) {
if (!std::isfinite(distance) || distance < 0.f)
throw eve::Exception("Hd2dLook.setFocusDistance: distance must be finite and >= 0");
focusDistance_ = distance;
}
void Hd2dLook::setMaxBlur(float blurPx) {
if (!std::isfinite(blurPx) || blurPx < 0.f)
throw eve::Exception("Hd2dLook.setMaxBlur: blur must be finite and >= 0");
maxBlurPx_ = std::min(blurPx, 32.f);
}
void Hd2dLook::setFocusRange(float range) {
if (!std::isfinite(range) || range <= 0.f)
throw eve::Exception("Hd2dLook.setFocusRange: range must be finite and > 0");
focusRange_ = range;
}
void Hd2dLook::setBloomIntensity(float intensity) {
if (!std::isfinite(intensity) || intensity < 0.f)
throw eve::Exception("Hd2dLook.setBloomIntensity: intensity must be finite and >= 0");
bloomIntensity_ = std::min(intensity, 8.f);
}
void Hd2dLook::setBloomThreshold(float threshold) {
if (!std::isfinite(threshold) || threshold < 0.f)
throw eve::Exception("Hd2dLook.setBloomThreshold: threshold must be finite and >= 0");
bloomThreshold_ = std::min(threshold, 16.f);
}
void Hd2dLook::apply(graphics::Camera3D *camera) const {
if (!camera) throw eve::Exception("Hd2dLook.apply: null camera");
camera->setDepthOfField(focusDistance_, maxBlurPx_, focusRange_);
camera->setBloom(bloomIntensity_, bloomThreshold_);
}
void Hd2dLook::applyPixelSampler(graphics::Graphics *gfx, graphics::Texture *texture) const {
if (!gfx) throw eve::Exception("Hd2dLook.applyPixelSampler: null graphics");
if (!texture) throw eve::Exception("Hd2dLook.applyPixelSampler: null texture");
gfx->setTextureSampler(texture, "nearest", "none", 1.f, 0.f);
}
Hd2dLook Hd2dLook::miniature() {
Hd2dLook look;
look.focusDistance_ = 16.f;
look.maxBlurPx_ = 7.f;
look.focusRange_ = 12.f;
look.bloomIntensity_ = 0.45f;
look.bloomThreshold_ = 1.1f;
return look;
}
Hd2dLook Hd2dLook::soft() {
Hd2dLook look;
look.focusDistance_ = 22.f;
look.maxBlurPx_ = 3.f;
look.focusRange_ = 18.f;
look.bloomIntensity_ = 0.2f;
look.bloomThreshold_ = 1.4f;
return look;
}
} // namespace eve::hd2d
+72 -12
View File
@@ -3,6 +3,7 @@
#include "common/Assert.h"
#include "common/Exception.h"
#include "graphics/Graphics.h"
#include "graphics/Material.h"
#include "graphics/Mesh.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Texture.h"
@@ -13,6 +14,7 @@
#include <glm/glm.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <limits>
#include <string>
#include <vector>
namespace eve::hd2d {
@@ -50,11 +52,14 @@ void Sprite3D::buildQuad(graphics::Graphics *gfx) {
renderable_ = graphics::Renderable3D::create();
if (!renderable_) throw eve::Exception("Sprite3D: Renderable3D create failed");
renderable_->setMesh(quad_);
auto* material = gfx->newMaterial();
auto *material = gfx->newMaterial();
material->setSurfaceMode("masked");
material->setReceiveLight(false);
material->setAlbedoTexture(texture_);
material->setTint(tintR_, tintG_, tintB_, tintA_);
material->setAlphaCutoff(alphaCutoff_);
material->setDepthWrite(depthWrite_);
material->setDoubleSided(doubleSided_);
renderable_->setMaterial(material);
// 2D sprites are self-lit (HD-2D characters), not shaded by 3D lights.
renderable_->setReceiveLight(false);
@@ -65,6 +70,17 @@ void Sprite3D::buildQuad(graphics::Graphics *gfx) {
updateFrameUv();
}
void Sprite3D::syncMaterial() {
if (!renderable_ || !renderable_->getMaterial()) return;
auto *material = renderable_->getMaterial();
material->setSurfaceMode("masked");
material->setAlphaCutoff(alphaCutoff_);
material->setDepthWrite(depthWrite_);
material->setDoubleSided(doubleSided_);
material->setTint(tintR_, tintG_, tintB_, tintA_);
material->setAlbedoTexture(texture_);
}
void Sprite3D::updateFrameUv() {
if (!gfx_ || !quad_) return;
std::vector<float> uv(8);
@@ -91,7 +107,7 @@ void Sprite3D::setTexture(graphics::Texture *texture) {
texture_ = texture;
if (renderable_) {
renderable_->setTexture(texture);
renderable_->getMaterial()->setAlbedoTexture(texture);
syncMaterial();
}
}
graphics::Texture *Sprite3D::getTexture() const { return texture_; }
@@ -183,16 +199,29 @@ void Sprite3D::orientToCamera() {
const float length2 = glm::dot(forward, forward);
if (!std::isfinite(length2) || length2 < 1e-12f) return;
forward /= std::sqrt(length2);
glm::vec3 right = glm::cross(forward, glm::vec3(cam.upX, cam.upY, cam.upZ));
const float rightLength2 = glm::dot(right, right);
if (!std::isfinite(rightLength2) || rightLength2 < 1e-12f) return;
right /= std::sqrt(rightLength2);
// Use the camera basis, not eye-to-sprite: all sprites remain parallel to
// the image plane, including off-center sprites and rolled cameras.
glm::mat4 rotation(1.f);
rotation[0] = glm::vec4(-right, 0.f);
rotation[1] = glm::vec4(glm::cross(right, forward), 0.f);
rotation[2] = glm::vec4(forward, 0.f);
if (yawBillboard_) {
// Cylindrical (Y-up) billboard: face the camera on XZ only.
glm::vec3 flat(-forward.x, 0.f, -forward.z);
const float flatLen2 = glm::dot(flat, flat);
if (!std::isfinite(flatLen2) || flatLen2 < 1e-12f) return;
flat /= std::sqrt(flatLen2);
const glm::vec3 right = glm::normalize(glm::cross(glm::vec3(0.f, 1.f, 0.f), flat));
rotation[0] = glm::vec4(-right, 0.f);
rotation[1] = glm::vec4(0.f, 1.f, 0.f, 0.f);
rotation[2] = glm::vec4(flat, 0.f);
} else {
glm::vec3 right = glm::cross(forward, glm::vec3(cam.upX, cam.upY, cam.upZ));
const float rightLength2 = glm::dot(right, right);
if (!std::isfinite(rightLength2) || rightLength2 < 1e-12f) return;
right /= std::sqrt(rightLength2);
// Use the camera basis, not eye-to-sprite: all sprites remain parallel to
// the image plane, including off-center sprites and rolled cameras.
rotation[0] = glm::vec4(-right, 0.f);
rotation[1] = glm::vec4(glm::cross(right, forward), 0.f);
rotation[2] = glm::vec4(forward, 0.f);
}
float yaw, pitch, roll;
glm::extractEulerAngleYXZ(rotation, yaw, pitch, roll);
renderable_->setRotation(yaw, pitch, roll);
@@ -235,7 +264,7 @@ void Sprite3D::setTint(float r, float g, float b, float a) {
tintA_ = std::max(0.f, std::min(1.f, a));
if (renderable_) {
renderable_->setTint(tintR_, tintG_, tintB_, tintA_);
renderable_->getMaterial()->setTint(tintR_, tintG_, tintB_, tintA_);
syncMaterial();
}
}
void Sprite3D::setVisible(bool visible) {
@@ -244,4 +273,35 @@ void Sprite3D::setVisible(bool visible) {
}
bool Sprite3D::getVisible() const { return visible_; }
void Sprite3D::setAlphaCutoff(float cutoff) {
if (!std::isfinite(cutoff)) throw eve::Exception("Sprite3D.setAlphaCutoff: cutoff must be finite");
alphaCutoff_ = std::max(0.f, std::min(1.f, cutoff));
syncMaterial();
}
float Sprite3D::getAlphaCutoff() const { return alphaCutoff_; }
void Sprite3D::setDepthWrite(bool enabled) {
depthWrite_ = enabled;
syncMaterial();
}
bool Sprite3D::getDepthWrite() const { return depthWrite_; }
void Sprite3D::setDoubleSided(bool enabled) {
doubleSided_ = enabled;
syncMaterial();
}
bool Sprite3D::getDoubleSided() const { return doubleSided_; }
void Sprite3D::setBillboardMode(const std::string &mode) {
if (mode == "yaw" || mode == "cylindrical" || mode == "y") {
yawBillboard_ = true;
} else if (mode == "screen" || mode == "camera" || mode.empty()) {
yawBillboard_ = false;
} else {
throw eve::Exception("Sprite3D.setBillboardMode: expected 'screen' or 'yaw'");
}
orientToCamera();
}
std::string Sprite3D::getBillboardMode() const { return yawBillboard_ ? "yaw" : "screen"; }
} // namespace eve::hd2d
+4 -2
View File
@@ -882,9 +882,11 @@ TEST_CASE("devtools.dap.caughtErrorPausesAtReportSite") {
// Never detach a thread that captures this test's stack. Release a
// possible late debugger stop and wait for deterministic teardown.
dbg.setBreakOnError(false);
dbg.resume();
for (int i = 0; i < 200 && !scriptDone.load(); ++i)
if (dbg.isPaused()) dbg.resume();
for (int i = 0; i < 200 && !scriptDone.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
dap.poll();
}
}
if (scriptThread.joinable()) scriptThread.join();
CHECK(scriptDone.load());
+47
View File
@@ -0,0 +1,47 @@
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <string>
#include "graphics/RenderSystem3D.h"
#include "hd2d/Hd2d.h"
TEST_CASE("hd2d.look.miniatureWritesCameraDofAndBloom") {
eve::hd2d::Hd2dLook look = eve::hd2d::Hd2dLook::miniature();
CHECK(look.getMaxBlur() > 0.f);
CHECK(look.getBloomIntensity() > 0.f);
eve::graphics::Camera3D *cam = eve::graphics::Camera3D::createCamera();
REQUIRE(cam != nullptr);
look.apply(cam);
CHECK_EQ(cam->getDofFocusDistance(), look.getFocusDistance());
CHECK_EQ(cam->getDofMaxBlur(), look.getMaxBlur());
CHECK_EQ(cam->getDofFocusRange(), look.getFocusRange());
CHECK_EQ(cam->getBloomIntensity(), look.getBloomIntensity());
CHECK_EQ(cam->getBloomThreshold(), look.getBloomThreshold());
}
TEST_CASE("hd2d.look.rejectsInvalidFocusRange") {
eve::hd2d::Hd2dLook look;
bool threw = false;
try {
look.setFocusRange(0.f);
} catch (...) {
threw = true;
}
CHECK(threw);
}
TEST_CASE("hd2d.sprite.dopFixMaterialDefaults") {
eve::hd2d::Sprite3D sprite;
CHECK(sprite.getDepthWrite());
CHECK(sprite.getDoubleSided());
CHECK_EQ(sprite.getAlphaCutoff(), 0.5f);
CHECK_EQ(sprite.getBillboardMode(), std::string("screen"));
sprite.setBillboardMode("yaw");
CHECK_EQ(sprite.getBillboardMode(), std::string("yaw"));
sprite.setAlphaCutoff(0.7f);
CHECK_EQ(sprite.getAlphaCutoff(), 0.7f);
sprite.setDepthWrite(false);
CHECK(!sprite.getDepthWrite());
}