Merge remote-tracking branch 'origin/dev' into codex/test-suite-representative-tests
# Conflicts: # test/dap.cpp
This commit is contained in:
commit
42d9be4eed
29 files changed
+1465
-223
No files matched your search
@@ -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 与画布快照
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -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);
|
||||
|
||||
@@ -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 的跳变会被忽略;指针不在视口上时不转视角。
|
||||
|
||||
|
||||
## 地形数据引用
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
Reference in new issue
Block a user