Merge remote-tracking branch 'origin/dev' into codex/editor-p0-p1-mcp

# Conflicts:
#	src/modules/editor/Editor.cpp
#	src/modules/editor/Editor.h
This commit is contained in:
西风逍遥游 committed 2026-08-25 02:19:38 +08:00
commit d3cb623ddb
33 files changed
+1626 -2512

No files matched your search

+4 -4
View File
@@ -84,9 +84,9 @@ eve_declare_module(NAME crowd LAYER 0 SCRIPT Crowd SLOT crowd
GROUP 2d 3d web)
eve_declare_module(NAME ik LIB EVIK LAYER 0 SCRIPT IK
GROUP 2d 3d web)
eve_declare_module(NAME editor LAYER 6 SCRIPT Editor SLOT editor
GROUP 3d web
OPTIONAL_DEPS procgen)
eve_declare_module(NAME editor LAYER 6 SCRIPT Editor SLOT editor
GROUP 3d web
OPTIONAL_DEPS procgen map)
eve_declare_module(NAME plugins LAYER 0 SCRIPT Plugins
GROUP 3d)
eve_declare_module(NAME database LAYER 0 SCRIPT Database
@@ -262,7 +262,7 @@ eve_declare_module(NAME voxel LAYER 5 SCRIPT Voxel
GROUP 3d)
eve_declare_module(NAME spritestack LIB EVSpriteStack LAYER 4 SCRIPT SpriteStack SLOT spritestack
DEPS graphics image model3d
GROUP 3d)
GROUP 2d)
eve_declare_module(NAME housegen LIB EVHouseGen LAYER 4 SCRIPT HouseGen
DEPS data graphics image model3d)
eve_declare_module(NAME card LAYER 4 SCRIPT Card
@@ -0,0 +1,64 @@
# Map production-readiness design
## Goal
Move `map` from a capable small-map runtime to a production tilemap stack without
breaking existing Squirrel games. The target is cost proportional to visible or
dirty chunks, a versioned TileSet data model, automatic derived-data invalidation,
and a level-authoring loop that does not require hand-editing JSON.
## Compatibility boundary
- `TileLayer` remains the script facade and fixed maps keep their current coordinates.
- GID 0 remains empty and Tiled transform bits remain preserved in storage.
- Rendering, physics and editor integrations cross module boundaries through
capabilities; `map` must not add upward includes.
- Existing `eve.tileset/1` manifests remain loadable. Version 2 fields are optional.
## Stage 0: measurable baseline
The regression fixture uses 1024 x 1024 maps in sparse and dense forms. Required
counters are total chunks, non-empty chunks, visited chunks, visited cells and
emitted tiles. CPU timing is informative locally; CI gates deterministic visit
counts so machine variance cannot create flakes.
Acceptance criteria:
- an empty 1024 x 1024 layer scans zero cells;
- a sparse layer scans only its occupied 32 x 32 chunks;
- a camera-bound layer rejects chunks outside the viewport;
- bulk edits publish one monotonic tile revision.
## Stage 1: chunked runtime
`TileLayer::Tiles` owns a 32 x 32 spatial index beside the compatibility GID array.
All mutations update chunk occupancy and a monotonic revision. Rendering iterates
non-empty chunks and performs conservative chunk/AABB viewport rejection before
cell collection. A later sparse/infinite backend can replace the dense array behind
the same facade.
## Stage 2: TileSet v2
Tile definitions gain typed custom data, animation alternatives, terrain/Wang
connectivity and optional collision/navigation/occlusion shapes. Runtime terrain
painting resolves a region plus its one-cell neighborhood deterministically.
## Stage 3: derived systems
Pathfinder and FOV remember the bound layer revision and resync automatically before
queries or compute. Physics integration is supplied by a capability implemented by
the physics module; map emits dirty tile regions and never includes physics headers.
## Stage 4: authoring
The editor supplies palette, brush, rectangle, fill, erase, eyedropper and undo/redo
commands over a shared `TileEditCommand`. Tiled import expands to infinite chunks,
external tilesets, groups, typed properties, Wang sets and animation with actionable
diagnostics and schema migration.
## Delivery and gates
Each stage is independently reviewable but the public interface, backends, consumers,
documentation and tests ship together. Required gates are the focused map tests,
`check_bindings.py --strict`, module layering, changed-line formatting and an actual
`iso-grid-walk` runtime smoke with a captured frame.
+59 -1
View File
@@ -26,7 +26,48 @@ map.render(gfx);
### 运行时修改瓦片
先用地图坐标换算接口把世界位置转成格子,再 `setTile(x, y, gid)`;批量生成地图时先 resize,再填充,避免重复重建图层。0 通常表示空瓦片。改瓦片后若已创建 Pathfinder,调用 `syncFromLayer()`。
先用地图坐标换算接口把世界位置转成格子,再 `setTile(x, y, gid)`;批量生成地图时先 resize,再填充,避免重复重建图层。0 通常表示空瓦片。
`fillRect(x, y, width, height, gid)` 会裁剪到地图边界,并把整次区域修改发布为一个 revision。绑定到图层的 Pathfinder 与 Fov 会在下一次查询或 `compute()` 时根据 revision 自动同步;通常不再需要手动调用 `syncFromLayer()`。
```squirrel
layer.fillRect(8, 8, 16, 12, 4);
print(layer.getRevision() + " chunks=" + layer.getNonEmptyChunkCount() +
"/" + layer.getChunkCount() + " size=" + layer.getChunkSize() + "\n");
```
地图内部以 32×32 chunk 建立空间索引。带 Camera2D 的正常 `map.render(gfx)` 会先按视口剔除 chunk;可通过 `map.getLastVisitedChunkCount()`、`map.getLastVisitedCellCount()` 和现有的可见瓦片统计检查本帧工作量。
### TileSet v2 数据、动画与 terrain
运行时可以逐帧建立动画,并为 GID 写入保留类型的自定义数据:
```squirrel
layer.clearTileAnimation(20);
layer.addTileAnimationFrame(20, 21, 100);
layer.addTileAnimationFrame(20, 22, 140);
print(layer.getTileAnimationFrameCount(20) + "\n");
layer.setTileDataString(20, "biome", "marsh");
layer.setTileDataNumber(20, "damage", 2.5);
layer.setTileDataBool(20, "wet", true);
print(layer.getTileDataType(20, "damage") + " " +
layer.getTileDataString(20, "biome") + " " +
layer.getTileDataNumber(20, "damage") + " " +
layer.getTileDataBool(20, "wet") + "\n");
```
terrain rule 使用 8 位邻接掩码(从西北开始顺时针)。绘制一个 terrain 会重新解析目标及其一格邻域:
```squirrel
layer.clearTerrainRules();
layer.setTerrainRule(30, 1, 0); // 孤立草地
layer.setTerrainRule(31, 1, 1 << 3); // 东侧相连
layer.paintTerrain(4, 4, 1);
print(layer.getTerrain(4, 4) + "\n");
```
Tiled JSON 的 `animation`、typed `properties` 与无限地图 `chunks` 可直接导入。无限地图当前会按已存在 chunk 的包围盒规范化为运行时图层,并把负 chunk 坐标折算进图层 origin。
### 组合项目自己的 2.5D 资产工作流
@@ -141,6 +182,22 @@ map.resolveDualGrid(logic, display); // 半步偏移 + 15 片选瓦
`resolveDualGridFilled(logic, display, filledGid)` 只把指定 GID 当作填充。逻辑层可继续用于碰撞 / 寻路;默认会 `setVisible(false)`。
### 碰撞几何
`publishCollision(layer)` 会把 `setTileMetadata(..., walkable=false)` 标记的正交瓦片贪心合并成世界坐标矩形,减少静态碰撞体数量;已注册的物理/项目适配器会通过 `ITileCollisionSink` 一次性收到替换后的几何。没有适配器时仍可读取矩形,自行创建物理夹具:
```squirrel
local count = map.publishCollision(layer);
for (local i = 0; i < count; ++i) {
local x = map.getCollisionRectX(i);
local y = map.getCollisionRectY(i);
local w = map.getCollisionRectWidth(i);
local h = map.getCollisionRectHeight(i);
}
```
当前自动合并只支持正交层;等距、交错和六角层返回 0,项目应使用多边形适配器,避免把菱形误近似成轴对齐矩形。
## 常见问题
- GID 与图集编号混淆:0 为空,其余值遵循 tileset 映射;默认空瓦片不可走(`setBlockEmpty`)。
@@ -156,6 +213,7 @@ map.resolveDualGrid(logic, display); // 半步偏移 + 15 片选瓦
- `applyConfig()`、`clear()`、`depthYAt()`、`fill()`、`getAutoReload()`、`getConfigPath()`、`getLayer()`、`getLayerCount()`
- `getMapHeight()`、`getMapWidth()`、`getName()`、`getObjectCount()`、`getObjectGid()`、`getObjectHeight()`、`getObjectName()`、`getObjectType()`
- `getLastVisibleTileCount()`、`getLastCustomVisualCount()`、`getLastAtlasCount()`
- `publishCollision()`、`getCollisionRectCount()`、`getCollisionRectX()`、`getCollisionRectY()`、`getCollisionRectWidth()`、`getCollisionRectHeight()`
- `getObjectWidth()`、`getObjectX()`、`getObjectY()`、`getTile()`、`getTileHeight()`、`getTileWidth()`、`getTilesetColumns()`、`getTilesetFirstGid()`
- `getTilesetTexture()`、`getX()`、`getY()`、`isVisible()`、`loadConfig()`、`loadFromFile()`、`newLayer()`、`newLayerFromFile()`
- `newPathfinder()`、`newPathfinderSize()`、`newFov()`、`newFovSize()`、`newFovVolume()`
+36 -145
View File
@@ -1,171 +1,62 @@
# Sprite-Stacking 伪 3D
# SpriteStack 2D 叠片渲染
**脚本入口:** `eve.SpriteStack()`
把三维模型沿某个轴切成 `layerCount` 张薄片图(RGBA),运行时刻把这些薄片按
世界坐标叠起来渲染,从设计视角看就是有体积的伪 3D 物体——经典 sprite-stacking
技术,常用于低多边形 / 像素风游戏的角色与物件。
SpriteStack 使用一组带透明通道的水平切片,在普通 2D 渲染管线中逐层偏移绘制,形成有体积感的 2.5D 图像。它不创建 3D 网格、3D 相机、深度缓冲或 3D 材质。
## 基本用法
```squirrel
// 1. 切片:程序化几何体(cylinder / sphere / cone / box),无需资产文件
local layers = spritestack.slicePrimitive("cylinder", 20, 128, 128, "z", 0.0);
// 也支持对 3D 模型文件切片(model3d 模块)
// local md = model3d.newModelDataFromFile("assets/tree.obj");
// local layers = spritestack.sliceModel(md, 20, 128, 128, "z", 0.0);
// 2. 建叠片对象并填充层纹理
local layers = spritestack.slicePrimitive("cylinder", 20, 128, 128, "y", 0.0);
local stack = spritestack.newStack(gfx);
stack.setLayerCount(layers.len());
for (local i = 0; i < layers.len(); i++)
stack.setLayerImage(gfx, layers[i], i);
for (local i = 0; i < layers.len(); i++) stack.setLayerImage(gfx, layers[i], i);
// 也可逐层加载:stack.setLayerFile(gfx, "assets/layer.png", 0);
// 横向图集可一次拆层:stack.setLayersFromAtlas(gfx, atlas, 20);
stack.setThickness(0.13); // 相邻切片的世界间距
stack.setSize(2.4, 2.8); // 切片四边形尺寸(世界单位)
stack.setPosition(0.0, 0.0, 0.0);
stack.setYaw(0.0);
stack.setTint(0.35, 0.78, 0.72, 1.0);
stack.setMode("vertical"); // "vertical" | "horizontal"
stack.setShadowEnabled(true); // 投影假阴影
stack.setOutline(0.05, 0.02, 0.03, 0.04); // 描边
```
stack.setPosition(480.0, 390.0); // 2D 屏幕坐标
stack.setSize(180.0, 180.0); // 每张切片的像素尺寸
stack.setThickness(4.0); // 相邻切片的像素偏移
stack.setRotation(25.0); // 2D 旋转角度,单位为度
stack.setTint(0.32, 0.82, 0.68, 1.0);
stack.setVisible(true);
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.42);
stack.setShadowOffset(12.0, 9.0);
stack.setOutline(2.0, 0.02, 0.03, 0.04);
渲染时机:在 `gfx.render3D()` 之后、`present()` 之前调用,切片会进入 3D 前向
通道,和场景内其他网格正确深度遮挡。
```squirrel
function eve_render() {
gfx.clear();
gfx.render3D();
stack.render(gfx); // 自动使用当前激活的 Camera3D
// stack.renderWithCamera(gfx, myCamera); // 也可显式指定相机
stack.render(gfx);
}
```
## 两种切片模式
`render(gfx)` 直接按屏幕像素解释坐标;需要世界坐标时调用 `stack.renderWithCamera(gfx, camera)`。批对象同样提供 `batch.renderWithCamera(gfx, camera)`。
| 模式 | 切片轴 | 运行时刻渲染 | 适合 |
|------|--------|--------------|------|
| `"vertical"`(默认) | `"z"` 等水平轴 | 朝向相机的竖直 billboard,yaw 旋转叠片的深度布局 | 经典「面包切片」伪 3D,正面视角最扎实 |
| `"horizontal"` | `"y"`(俯视) | 水平四边形,3/4 相机看到真实体积视差 | 俯视 / 斜 45° 视角的场景物件 |
切片轴与模式要匹配:`slicePrimitive(..., "z", ...)` + `"vertical"`,
或 `slicePrimitive(..., "y", ...)` + `"horizontal"`。
## 对象与参数
- `newStack(gfx)`:新建空叠片(脚本 VM 持有;不要直接构造 `eve.SpriteStack3D`)。
- 图层:`setLayerCount(n)`、`setLayerTexture(tex, i)`、`setLayerImage(gfx, img, i)`、
`setLayerFile(gfx, path, i)`、`setLayersFromAtlas(gfx, atlasTex, count)`(单张横向
条带纹理按列切成 count 层)。
- 变换:`setThickness(t)`、`setSize(w, h)`、`setPosition(x, y, z)`、`setYaw(rad)`。
- 外观:`setTint(r, g, b, a)`、`setAlphaCutoff(v)`、`setVisible(bool)`、`setMode(mode)`。
- 渲染:`render(gfx)` 或 `renderWithCamera(gfx, camera)`。
## 高级功能
### 叠片投影阴影
`setShadowEnabled(true)` 后,每层切片沿光方向压到地面平面(`setShadowPlaneY(y)`),
用切片本身的剪影画半透明黑色,形成接触阴影:
## 图层和批处理
```squirrel
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.38);
stack.setShadowLight(-0.45, -1.0, -0.35); // 光照方向(指向地面)
stack.setShadowPlaneY(-1.53); // 地面高度
```
local count = stack.getLayerCount();
local texture = stack.getLayerTexture(0);
stack.setLayerTexture(texture, 0);
stack.setLayerUV(0, 0.0, 0.0, 1.0, 1.0);
local spacing = stack.getThickness();
这是经典的假阴影(不是 shadow map),适合固定视角的伪 3D 场景;内部会把阴影
平面抬高 2cm 避免与地面深度冲突。
合批时阴影同样生效:`SpriteStackBatch` 会把投影阴影按黑色色调单独分组烘焙,
不额外增加 draw call。
### 叠片描边
`setOutline(width, r, g, b)` 给每层画一个放大 k 的黑色剪影垫在切片后面,形成
风格化描边(配合 `stylize` 的 ink / pixel 后处理很搭):
```squirrel
stack.setOutline(0.05, 0.02, 0.03, 0.04);
```
合批时描边同样生效(按描边色调单独分组)。
### 叠片参与 G-Buffer(AO / 描边后处理)
默认叠片只画进前向通道,AO / 描边这类读取 G-Buffer 深度的后处理看不到它们。
开启 `setGbufferEnabled(true)` 后,叠片会通过 `RenderSystem3D` 的扩展绘制钩子
写进 G-Buffer(alpha 剔除管线),描边和 AO 就能沿切片剪影生效:
```squirrel
stack.setGbufferEnabled(true);
gfx.getRenderControl().enable("outline");
```
### 叠片投射 CSM 阴影
默认叠片不写 shadow map。开启 `setCastShadow(true)` 后,切片通过 alpha 剔除的
shadow 管线写进级联阴影贴图(CSM),在地面等接收阴影的网格上留下**剪影形状的
阴影**,而不是整块矩形:
```squirrel
stack.setCastShadow(true);
```
需要场景里有 `castShadow` 的平行光;如果没有 `Light3D`,会回退用
`gfx.setDirectionalLight(...)` 设置的旧版平行光作为阴影光源。与 `setShadowEnabled`
的投影假阴影相互独立,可同时开启。
### 多叠片合批 `SpriteStackBatch`
多个叠片如果共享图层纹理(`setLayerTexture` 传入同一个 `Texture`),可以用
`newBatch(gfx)` 把它们合成每帧一次 draw call(按纹理 + 色调分组):
```squirrel
local batch = spritestack.newBatch(gfx);
batch.add(stackA);
batch.add(stackB);
// eve_render 中:batch.render(gfx);
batch.add(stack);
batch.remove(stack);
batch.add(stack);
batch.render(gfx);
batch.clear();
```
合批会在叠片内容变化(位置 / 缩放 / 纹理 / 色调)时重建网格(`Graphics::updateMeshVertices`
原地更新);每帧都旋转/移动的叠片建议直接用 `stack.render`,静态道具用合批收益最大。
合批会一并烘焙逐叠片的阴影与描边(它们按各自色调分组)。
批对象提交多个 `SpriteStack2D`;实际纹理由 Graphics 的 2D texture batch 合并。切片从底层到顶层绘制,因此透明边缘和层间遮挡仍遵循 2D painter order。
### Atlas 条带打包工具
## 模型切片
`scripts/pack_sprite_stack_atlas.py` 把一组等尺寸的层 PNG 横向打包成一张条带,
运行时用 `setLayersFromAtlas(gfx, atlasTex, count)` 直接按列切回:
如果已有 `ModelData`,可以离线或加载时生成切片:
```bash
python3 scripts/pack_sprite_stack_atlas.py layer_00.png layer_01.png ... \
-o atlas.png --json atlas.json
```squirrel
local model = model3d.newModelDataFromFile("assets/rock.obj");
local layers = spritestack.sliceModel(model, 24, 128, 128, "y", 0.0);
```
## 切片 API
- `slicePrimitive(kind, layerCount, imageW, imageH, axis, thickness)`:
程序化几何体切片,`kind` ∈ `"box" | "cylinder" | "sphere" | "cone"`;
`axis` ∈ `"x" | "y" | "z"`;`thickness <= 0` 时按包围盒自动均分。
- `sliceModel(modelData, layerCount, imageW, imageH, axis, thickness)`:
对 `model3d.newModelDataFromFile(...)` 的模型切片。
- 两者都返回 `ImageData` 数组(RGBA8),可直接传给 `gfx.newTexture` 或
`stack.setLayerImage`。
## 使用要点
- 切片的 `ImageData` 归脚本持有:传入 `setLayerImage` 后叠片内部已转为 GPU
纹理,可释放原图。
- 叠片使用 alpha 混合 + 深度测试(不写深度)的管线,同堆切片每帧按相机距离
由远到近排序;跨叠片的重叠由绘制顺序决定。
- 目前仅在 Vulkan 桌面端可用(WebGPU/WASM 精简构建不含该模块)。
- 不要每帧重新切片或重新上传纹理;只在物体形状变化时重建。
- `updateMeshVertices`(合批内部使用)与 GPU 同步,重建频率越高开销越大。
**源码:** [`src/modules/spritestack/`](../../../src/modules/spritestack/)
**示例:** [`examples/sprite-stack/`](../../../examples/sprite-stack/)
**相关测试:** [`test/spritestack.cpp`](../../../test/spritestack.cpp)
这里的模型只用于生成 RGBA 切片;运行时仍走 2D 管线。`slicePrimitive` 支持 `cylinder`、`sphere`、`cone`、`box`,切片轴支持 `x`、`y`、`z`。
+6 -27
View File
@@ -1,34 +1,13 @@
# Sprite Stacking
# SpriteStack 2D 示例
伪 3D sprite-stacking 示例:把程序化生成的三维几何体(圆柱 / 球 / 圆锥 / 方块)
在 CPU 上沿指定轴切成多层 RGBA 图,再以「叠片」方式画进 3D 前向渲染通道。
这个示例把程序化几何体切成 RGBA 水平切片,再通过 Graphics 的普通 2D 纹理管线逐层偏移绘制。运行阶段没有 3D 相机、3D 网格、深度缓冲或专用 3D shader。
运行:
```bash
eve run examples/sprite-stack
```powershell
build\\win32-debug\\src\\engine\\eve.exe run examples\\sprite-stack
```
操作:
操作:A / D 旋转叠片,1~4 切换圆柱、球、圆锥、方盒,Q / E 调整层间像素距离。
| 键 | 作用 |
|----|------|
| `A` / `D` | 绕 Y 轴旋转叠片堆(伪 3D 视差) |
| `W` / `S` | 拉近 / 拉远相机 |
| `H` | 切换 vertical(朝向相机的竖直切片)/ horizontal(水平俯视切片)模式 |
| `1`..`4` | 切换圆柱 / 球 / 圆锥 / 方块 |
| `M` | 切换程序化几何体 / `assets/rock.obj`(`sliceModel` 模型文件切片) |
| `Q` / `E` | 调整切片厚度 |
| `R` | 重新切片 |
技术要点:
- 切片:`spritestack.slicePrimitive(...)`(或 `sliceModel` 对 3D 模型文件切片)
把网格切成 `layerCount` 个薄片,每个薄片正交投影成一张 RGBA 图。
- 渲染:`SpriteStack3D` 用 alpha 混合 + 深度测试的 hair 管线绘制切片,
`vertical` 模式为朝向相机的 billboard,`horizontal` 模式为水平四边形;
每帧按相机距离由远到近排序。
- 调用时机:`gfx.render3D()` 之后、`present()` 之前调用 `stack.render(gfx)`,
切片即与场景内其他 3D 网格正确遮挡。
- 高级功能:`setShadowEnabled` 投影假阴影、`setOutline` 风格化描边;另外三个
共享纹理的小石头通过 `SpriteStackBatch` 合批成单次 draw。
主物体直接调用 `stack.render(gfx)`;左右两个副本通过 `SpriteStackBatch` 提交,共享同一组切片纹理。
+44 -115
View File
@@ -1,138 +1,67 @@
// Sprite stacking pseudo-3D demo.
//
// A 3D primitive is sliced into thin RGBA layers on the CPU, then rendered as
// a stack of alpha-blended slices inside the 3D forward pass. From the design
// view angle the stack reads as a solid 3D object; rotating the camera (or the
// stack yaw) reveals the classic pseudo-3D parallax.
//
// Controls:
// A / D rotate stack yaw
// W / S camera distance
// H toggle vertical (billboard) / horizontal (top-down) mode
// 1..4 primitive: cylinder / sphere / cone / box
// M toggle procedural primitive / rock.obj (model-file slicing)
// Q / E slice thickness
// R rebuild (reslice) the stack
if (!("stackYaw" in getroottable())) stackYaw <- 0.0;
if (!("camDist" in getroottable())) camDist <- 6.0;
if (!("stackMode" in getroottable())) stackMode <- "vertical";
if (!("stackKind" in getroottable())) stackKind <- "cylinder";
if (!("sliceCount" in getroottable())) sliceCount <- 20;
if (!("sliceThick" in getroottable())) sliceThick <- 0.0;
// Classic SpriteStack rendered entirely by the normal 2D pipeline.
// A / D rotate, 1..4 switch primitive, Q / E change slice spacing.
if (!("angle" in getroottable())) angle <- 0.0;
if (!("kind" in getroottable())) kind <- "cylinder";
if (!("spacing" in getroottable())) spacing <- 4.0;
if (!("stack" in getroottable())) stack <- null;
if (!("batch" in getroottable())) batch <- null;
if (!("satellites" in getroottable())) satellites <- [];
if (!("rockLayers" in getroottable())) rockLayers <- null;
if (!("useModel" in getroottable())) useModel <- false;
if (!("prevKeys" in getroottable())) prevKeys <- {};
if (!("copies" in getroottable())) copies <- [];
local tints = {
cylinder = [0.35, 0.78, 0.72],
sphere = [0.85, 0.52, 0.28],
cone = [0.55, 0.42, 0.30],
box = [0.42, 0.55, 0.85],
cylinder = [0.32, 0.82, 0.68], sphere = [0.92, 0.52, 0.26],
cone = [0.68, 0.48, 0.30], box = [0.42, 0.62, 0.92]
};
function pressed(k) {
return key_just_pressed(k);
}
function rebuildStack() {
local axis = stackMode == "vertical" ? "z" : "y";
if (useModel) {
local md = model3d.newModelDataFromFile("assets/rock.obj");
rockLayers = spritestack.sliceModel(md, 22, 128, 128, axis, 0.0);
} else {
rockLayers = null;
}
local layers = rockLayers != null
? rockLayers
: spritestack.slicePrimitive(stackKind, sliceCount, 128, 128, axis, 0.0);
function rebuild() {
local layers = spritestack.slicePrimitive(kind, 20, 128, 128, "y", 0.0);
stack = spritestack.newStack(gfx);
stack.setLayerCount(layers.len());
for (local i = 0; i < layers.len(); i++)
stack.setLayerImage(gfx, layers[i], i);
stack.setThickness(stackMode == "vertical" ? 0.12 : 0.17);
stack.setSize(2.4, 2.8);
stack.setPosition(0.0, 0.0, 0.0);
stack.setMode(stackMode);
local t = useModel ? [0.55, 0.48, 0.40] : tints[stackKind];
stack.setTint(t[0], t[1], t[2], 1.0);
for (local i = 0; i < layers.len(); i++) stack.setLayerImage(gfx, layers[i], i);
stack.setPosition(480.0, 390.0);
stack.setSize(180.0, 180.0);
stack.setThickness(spacing);
local tint = tints[kind];
stack.setTint(tint[0], tint[1], tint[2], 1.0);
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.38);
stack.setShadowLight(-0.45, -1.0, -0.35);
stack.setShadowPlaneY(-1.53);
stack.setCastShadow(true); // real CSM shadow from the slice silhouettes
if (stackMode == "vertical") stack.setOutline(0.045, 0.02, 0.03, 0.04);
rebuildSatellites();
}
stack.setShadowOpacity(0.42);
stack.setShadowOffset(12.0, 9.0);
stack.setOutline(2.0, 0.02, 0.03, 0.04);
// A few small copies sharing the main stack's layer textures, drawn in a
// single batched draw call per (texture, tint) group.
function rebuildSatellites() {
if (batch == null) batch = spritestack.newBatch(gfx);
batch.clear();
satellites = [];
local offsets = [[2.6, 0.9], [-2.5, 1.0], [3.0, -1.2]];
foreach (off in offsets) {
local s = spritestack.newStack(gfx);
s.setLayerCount(stack.getLayerCount());
batch = spritestack.newBatch(gfx);
copies = [];
foreach (x in [190.0, 770.0]) {
local copy = spritestack.newStack(gfx);
copy.setLayerCount(stack.getLayerCount());
for (local i = 0; i < stack.getLayerCount(); i++)
s.setLayerTexture(stack.getLayerTexture(i), i);
s.setThickness(stack.getThickness());
s.setSize(0.85, 0.95);
s.setPosition(off[0], 0.0, off[1]);
s.setMode(stackMode);
s.setTint(0.58, 0.52, 0.46, 1.0);
batch.add(s);
satellites.append(s);
copy.setLayerTexture(stack.getLayerTexture(i), i);
copy.setPosition(x, 360.0);
copy.setSize(105.0, 105.0);
copy.setThickness(spacing * 0.55);
copy.setTint(tint[0], tint[1], tint[2], 1.0);
copy.setShadowEnabled(true);
batch.add(copy);
copies.append(copy);
}
}
local cam = eve.Camera3D();
cam.setEye(0.0, 2.8, camDist);
cam.setTarget(0.0, 0.2, 0.0);
cam.setUp(0.0, 1.0, 0.0);
cam.setFov(45.0);
cam.setAmbient(0.32, 0.35, 0.38);
cam.setActive(true);
gfx.setDirectionalLight(-0.45, -1.0, -0.35, 1.25, 1.18, 1.05);
gfx.setBackgroundColor(0.075, 0.10, 0.12, 1.0);
// Flat ground disc so the stack's depth reads against the scene.
local ground = eve.Renderable3D();
ground.setMesh(gfx.newMeshCylinder(48, 1, true));
ground.setPosition(0.0, -1.55, 0.0);
ground.setScale(9.0, 0.18, 9.0);
ground.setTint(0.20, 0.30, 0.24, 1.0);
ground.setRoughness(0.9);
rebuildStack();
gfx.setBackgroundColor(0.055, 0.075, 0.095, 1.0);
rebuild();
function eve_update(dt) {
if (pressed("h") || pressed("H")) {
stackMode = stackMode == "vertical" ? "horizontal" : "vertical";
rebuildStack();
}
if (pressed("r") || pressed("R")) rebuildStack();
if (pressed("1")) { stackKind = "cylinder"; rebuildStack(); }
if (pressed("2")) { stackKind = "sphere"; rebuildStack(); }
if (pressed("3")) { stackKind = "cone"; rebuildStack(); }
if (pressed("4")) { stackKind = "box"; rebuildStack(); }
if (pressed("m") || pressed("M")) { useModel = !useModel; rebuildStack(); }
if (keyboard.isDown("a") || keyboard.isDown("A")) stackYaw -= dt * 0.9;
if (keyboard.isDown("d") || keyboard.isDown("D")) stackYaw += dt * 0.9;
if (keyboard.isDown("w") || keyboard.isDown("W")) camDist = max(2.5, camDist - dt * 3.0);
if (keyboard.isDown("s") || keyboard.isDown("S")) camDist = min(14.0, camDist + dt * 3.0);
stack.setYaw(stackYaw);
cam.setEye(0.0, 2.8, camDist);
if (keyboard.isDown("a") || keyboard.isDown("A")) angle -= dt * 75.0;
if (keyboard.isDown("d") || keyboard.isDown("D")) angle += dt * 75.0;
if (key_just_pressed("1")) { kind = "cylinder"; rebuild(); }
if (key_just_pressed("2")) { kind = "sphere"; rebuild(); }
if (key_just_pressed("3")) { kind = "cone"; rebuild(); }
if (key_just_pressed("4")) { kind = "box"; rebuild(); }
if (key_just_pressed("q") || key_just_pressed("Q")) { spacing = max(1.0, spacing - 1.0); rebuild(); }
if (key_just_pressed("e") || key_just_pressed("E")) { spacing = min(10.0, spacing + 1.0); rebuild(); }
stack.setRotation(angle);
foreach (copy in copies) copy.setRotation(-angle * 0.7);
}
function eve_render() {
gfx.clear();
gfx.render3D();
// Draw the pseudo-3D stack into the open 3D pass (before present).
stack.render(gfx);
batch.render(gfx);
}
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""Compile sprite-stack GLSL vert/frag to SPIR-V C++ include arrays."""
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" / "spritestack" / "shaders"
SHADERS = [
("sprite_stack.vert", "vert"),
("sprite_stack.frag", "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_shader(src: Path, stage: str) -> Path:
out = src.with_suffix(f".{stage}.spv")
cmd = ["glslc", f"-fshader-stage={stage}", 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, stage in SHADERS:
src = SHADER_DIR / name
if not src.is_file():
raise SystemExit(f"missing {src}")
spv = compile_shader(src, stage)
stem = src.stem # e.g. sprite_stack
array = f"{stem}_{stage}_spv"
spv_to_inc(spv, array, SHADER_DIR / f"{stem}_{stage}_spv.inc")
spv.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+6
View File
@@ -578,6 +578,12 @@ TileBufferTarget* Editor::newTileBufferTarget(const std::string& id, TileBuffer*
return new TileBufferTarget(id, buffer);
}
#ifdef EVENGINE_HAS_MAP
TileLayerTarget* Editor::newTileLayerTarget(const std::string& id, map::TileLayer* layer) {
return new TileLayerTarget(id, layer);
}
#endif
ScriptEditorTool* Editor::newScriptTool(const std::string& id, const std::string& label) {
return new ScriptEditorTool(id, label);
}
+12
View File
@@ -16,6 +16,11 @@ namespace procgen {
class Heightmap;
}
#endif
#ifdef EVENGINE_HAS_MAP
namespace map {
class TileLayer;
}
#endif
} // namespace eve
namespace eve::editor {
@@ -30,6 +35,9 @@ class EditorDock;
class EditorHistory;
class EditorSession;
class TileBufferTarget;
#ifdef EVENGINE_HAS_MAP
class TileLayerTarget;
#endif
class HeightmapTarget;
class ScriptEditorTool;
class EditorAutomationProvider;
@@ -65,6 +73,10 @@ public:
const EditorCommandService& commandService() const { return commandService_; }
/** @brief Adapt existing fields to capability-based editor targets. */
TileBufferTarget* newTileBufferTarget(const std::string& id, TileBuffer* buffer);
#ifdef EVENGINE_HAS_MAP
/** @brief Adapt a live map layer to the editor command/undo/brush protocol. */
TileLayerTarget* newTileLayerTarget(const std::string& id, map::TileLayer* layer);
#endif
/** @brief Create a script-backed implementation of the IEditorTool protocol. */
ScriptEditorTool* newScriptTool(const std::string& id, const std::string& label);
#ifdef EVENGINE_HAS_PROCGEN
+24
View File
@@ -1,6 +1,9 @@
#include "editor/FieldTargets.h"
#include "editor/TileBuffer.h"
#ifdef EVENGINE_HAS_MAP
#include "map/TileLayer.h"
#endif
#ifdef EVENGINE_HAS_PROCGEN
#include "procgen/heightmap/Heightmap.h"
#endif
@@ -23,6 +26,27 @@ bool TileBufferTarget::writeInt(int x, int y, int value) {
return true;
}
#ifdef EVENGINE_HAS_MAP
TileLayerTarget::TileLayerTarget(std::string id, map::TileLayer *layer)
: id_(std::move(id)), layer_(layer) {}
unsigned long long TileLayerTarget::revision() const {
return layer_ ? layer_->tiles()->revision : 0;
}
int TileLayerTarget::width() const { return layer_ ? layer_->getMapWidth() : 0; }
int TileLayerTarget::height() const { return layer_ ? layer_->getMapHeight() : 0; }
bool TileLayerTarget::inBounds(int x, int y) const {
return layer_ && x >= 0 && y >= 0 && x < width() && y < height();
}
int TileLayerTarget::readInt(int x, int y) const { return inBounds(x, y) ? layer_->getTile(x, y) : 0; }
bool TileLayerTarget::writeInt(int x, int y, int value) {
if (!inBounds(x, y)) return false;
if (layer_->getTile(x, y) == value) return true;
layer_->setTile(x, y, value);
dirty_.include(x, y);
return true;
}
#endif
#ifdef EVENGINE_HAS_PROCGEN
HeightmapTarget::HeightmapTarget(std::string id, procgen::Heightmap *heightmap)
: id_(std::move(id)), heightmap_(heightmap) {}
+25
View File
@@ -5,6 +5,9 @@
#include <string>
namespace eve::editor { class TileBuffer; }
#ifdef EVENGINE_HAS_MAP
namespace eve::map { class TileLayer; }
#endif
#ifdef EVENGINE_HAS_PROCGEN
namespace eve::procgen { class Heightmap; }
#endif
@@ -32,6 +35,28 @@ private:
EditRegion dirty_;
};
#ifdef EVENGINE_HAS_MAP
/** @brief Non-owning editable adapter for a live map::TileLayer. */
class TileLayerTarget final : public IEditableTarget, public IIntFieldTarget {
public:
TileLayerTarget(std::string id, map::TileLayer *layer);
const std::string &targetId() const override { return id_; }
unsigned long long revision() const override;
EditRegion dirtyRegion() const override { return dirty_; }
void clearDirtyRegion() override { dirty_.clear(); }
int width() const override;
int height() const override;
bool inBounds(int x, int y) const override;
int readInt(int x, int y) const override;
bool writeInt(int x, int y, int value) override;
map::TileLayer *layer() const { return layer_; }
private:
std::string id_;
map::TileLayer *layer_ = nullptr;
EditRegion dirty_;
};
#endif
#ifdef EVENGINE_HAS_PROCGEN
/** @brief Non-owning IScalarFieldTarget adapter for procgen::Heightmap. */
class HeightmapTarget final : public IEditableTarget, public IScalarFieldTarget {
+3
View File
@@ -220,6 +220,7 @@ struct Fov::Impl {
int height = 0;
int depth = 1;
TileLayer *layer = nullptr;
uint64_t layerRevision = 0;
Algorithm algorithm = Algorithm::Shadowcast;
RadiusMetric metric = RadiusMetric::Euclidean;
Mode mode = Mode::Grid2D;
@@ -316,6 +317,7 @@ struct Fov::Impl {
if (opaqueGids.count(gid)) isOpaque = true;
opaque[size_t(i)] = isOpaque ? 1u : 0u;
}
layerRevision = tiles->revision;
dirty = true;
}
@@ -894,6 +896,7 @@ struct Fov::Impl {
}
void compute() {
if (layer && layerRevision != layer->tiles()->revision) syncFromLayer();
if (!dirty) return;
resetVisibleKeepExplored();
for (const auto &r : revealers) computeRevealer(r);
+102 -2
View File
@@ -9,9 +9,12 @@
#include "graphics/Graphics.h"
#include "graphics/RenderSystem.h"
#include "common/Module.h"
#include "common/Capability.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <algorithm>
namespace eve::map {
Module_IMPL(Map, new Map());
@@ -62,7 +65,7 @@ int Map::loadFromFile(const std::string &path) {
}
void Map::update(float dt) {
(void)dt;
TileRenderSystem::update(dt);
TileConfigSystem::poll();
}
@@ -70,7 +73,7 @@ void Map::render(graphics::Graphics *gfx) {
if (!gfx) return;
std::vector<graphics::DrawItem2D> items;
graphics::RenderSystem::collectSprites(items);
TileRenderSystem::collect(items);
TileRenderSystem::collect(items, gfx->getWidth(), gfx->getHeight());
graphics::RenderSystem::drawItems(*gfx, items, false);
}
@@ -79,6 +82,76 @@ int Map::pollConfigs() { return TileConfigSystem::poll(); }
int Map::getLastVisibleTileCount() const { return TileRenderSystem::lastVisibleTileCount(); }
int Map::getLastCustomVisualCount() const { return TileRenderSystem::lastCustomVisualCount(); }
int Map::getLastAtlasCount() const { return TileRenderSystem::lastAtlasCount(); }
int Map::getLastVisitedChunkCount() const { return TileRenderSystem::lastVisitedChunkCount(); }
int Map::getLastVisitedCellCount() const { return TileRenderSystem::lastVisitedCellCount(); }
int Map::publishCollision(TileLayer *layer) {
collisionRects_.clear();
if (!layer) return 0;
const auto config = layer->config();
const auto tiles = layer->tiles();
const auto tileset = layer->tileset();
if (config->orientation != MapOrientation::Orthogonal) return 0;
std::vector<uint8_t> solid(size_t(config->mapW * config->mapH), 0);
for (int y = 0; y < config->mapH; ++y) {
for (int x = 0; x < config->mapW; ++x) {
const int gid = int(tileGid(tiles->gids[size_t(y * config->mapW + x)]));
const auto it = std::find_if(tileset->visuals.begin(), tileset->visuals.end(),
[gid](const auto &v) { return v.gid == gid; });
solid[size_t(y * config->mapW + x)] =
gid != 0 && it != tileset->visuals.end() && !it->walkable;
}
}
for (int y = 0; y < config->mapH; ++y) {
for (int x = 0; x < config->mapW; ++x) {
if (!solid[size_t(y * config->mapW + x)]) continue;
int width = 1;
while (x + width < config->mapW && solid[size_t(y * config->mapW + x + width)])
++width;
int height = 1;
bool canGrow = true;
while (y + height < config->mapH && canGrow) {
for (int dx = 0; dx < width; ++dx)
if (!solid[size_t((y + height) * config->mapW + x + dx)]) canGrow = false;
if (canGrow) ++height;
}
for (int dy = 0; dy < height; ++dy)
for (int dx = 0; dx < width; ++dx)
solid[size_t((y + dy) * config->mapW + x + dx)] = 0;
collisionRects_.push_back({config->originX + x * (config->tileW + config->cellGapX),
config->originY + y * (config->tileH + config->cellGapY),
width * (config->tileW + config->cellGapX),
height * (config->tileH + config->cellGapY)});
}
}
cap::forEach<ITileCollisionSink>([&](ITileCollisionSink *sink) {
sink->replaceTileCollision(layer, collisionRects_.data(), collisionRects_.size());
});
return int(collisionRects_.size());
}
int Map::getCollisionRectCount() const { return int(collisionRects_.size()); }
float Map::getCollisionRectX(int index) const {
return index >= 0 && index < int(collisionRects_.size())
? collisionRects_[size_t(index)].x
: 0.f;
}
float Map::getCollisionRectY(int index) const {
return index >= 0 && index < int(collisionRects_.size())
? collisionRects_[size_t(index)].y
: 0.f;
}
float Map::getCollisionRectWidth(int index) const {
return index >= 0 && index < int(collisionRects_.size())
? collisionRects_[size_t(index)].width
: 0.f;
}
float Map::getCollisionRectHeight(int index) const {
return index >= 0 && index < int(collisionRects_.size())
? collisionRects_[size_t(index)].height
: 0.f;
}
int Map::getLayerCount() const {
if (ecs::current()->getManager<TileLayer>() == nullptr) return 0;
@@ -191,8 +264,27 @@ void Map::expose(ssq::Table &table) {
layer.addFunc("resize", &TileLayer::resize);
layer.addFunc("setTile", &TileLayer::setTile);
layer.addFunc("getTile", &TileLayer::getTile);
layer.addFunc("fillRect", &TileLayer::fillRect);
layer.addFunc("fill", &TileLayer::fill);
layer.addFunc("clear", &TileLayer::clear);
layer.addFunc("getRevision", &TileLayer::getRevision);
layer.addFunc("getChunkSize", &TileLayer::getChunkSize);
layer.addFunc("getChunkCount", &TileLayer::getChunkCount);
layer.addFunc("getNonEmptyChunkCount", &TileLayer::getNonEmptyChunkCount);
layer.addFunc("clearTileAnimation", &TileLayer::clearTileAnimation);
layer.addFunc("addTileAnimationFrame", &TileLayer::addTileAnimationFrame);
layer.addFunc("getTileAnimationFrameCount", &TileLayer::getTileAnimationFrameCount);
layer.addFunc("setTileDataString", &TileLayer::setTileDataString);
layer.addFunc("setTileDataNumber", &TileLayer::setTileDataNumber);
layer.addFunc("setTileDataBool", &TileLayer::setTileDataBool);
layer.addFunc("getTileDataType", &TileLayer::getTileDataType);
layer.addFunc("getTileDataString", &TileLayer::getTileDataString);
layer.addFunc("getTileDataNumber", &TileLayer::getTileDataNumber);
layer.addFunc("getTileDataBool", &TileLayer::getTileDataBool);
layer.addFunc("setTerrainRule", &TileLayer::setTerrainRule);
layer.addFunc("clearTerrainRules", &TileLayer::clearTerrainRules);
layer.addFunc("paintTerrain", &TileLayer::paintTerrain);
layer.addFunc("getTerrain", &TileLayer::getTerrain);
layer.addFunc("setTileset", &TileLayer::setTileset);
layer.addFunc("setTilesetTileSize", &TileLayer::setTilesetTileSize);
layer.addFunc("setTileVisual", &TileLayer::setTileVisual);
@@ -350,6 +442,14 @@ void Map::expose(ssq::Class &cls) {
cls.addFunc("getLastVisibleTileCount", &Map::getLastVisibleTileCount);
cls.addFunc("getLastCustomVisualCount", &Map::getLastCustomVisualCount);
cls.addFunc("getLastAtlasCount", &Map::getLastAtlasCount);
cls.addFunc("getLastVisitedChunkCount", &Map::getLastVisitedChunkCount);
cls.addFunc("getLastVisitedCellCount", &Map::getLastVisitedCellCount);
cls.addFunc("publishCollision", &Map::publishCollision);
cls.addFunc("getCollisionRectCount", &Map::getCollisionRectCount);
cls.addFunc("getCollisionRectX", &Map::getCollisionRectX);
cls.addFunc("getCollisionRectY", &Map::getCollisionRectY);
cls.addFunc("getCollisionRectWidth", &Map::getCollisionRectWidth);
cls.addFunc("getCollisionRectHeight", &Map::getCollisionRectHeight);
cls.addFunc("getLayerCount", &Map::getLayerCount);
cls.addFunc("getLayer", &Map::getLayer);
cls.addFunc("getObjectCount", &Map::getObjectCount);
+12
View File
@@ -5,6 +5,7 @@
#include "map/MapObject.h"
#include "map/Pathfinder.h"
#include "map/Fov.h"
#include "map/TileCollision.h"
#include <string>
#include <vector>
@@ -58,6 +59,16 @@ public:
int getLastVisibleTileCount() const;
int getLastCustomVisualCount() const;
int getLastAtlasCount() const;
int getLastVisitedChunkCount() const;
int getLastVisitedCellCount() const;
/** @brief Generate merged collision from non-walkable tile metadata and notify adapters. */
int publishCollision(TileLayer *layer);
int getCollisionRectCount() const;
float getCollisionRectX(int index) const;
float getCollisionRectY(int index) const;
float getCollisionRectWidth(int index) const;
float getCollisionRectHeight(int index) const;
int getLayerCount() const;
/** @brief Layer created by the most recent loadFromFile/newLayerFromFile call. */
@@ -95,6 +106,7 @@ private:
std::vector<MapObject> objects_;
std::vector<TileLayer *> loadedLayers_;
std::string dualGridError_;
std::vector<TileCollisionRect> collisionRects_;
};
} // namespace eve::map
+4 -1
View File
@@ -144,6 +144,7 @@ struct Grid {
int width = 0;
int height = 0;
TileLayer *layer = nullptr;
uint64_t layerRevision = 0;
Topology topology = Topology::Ortho4;
bool topologyManual = false;
bool diagonal = false;
@@ -199,6 +200,7 @@ struct Grid {
if (blockedGids.count(gid)) blocked = true;
cost[size_t(i)] = blocked ? 0.f : 1.f;
}
layerRevision = tiles->revision;
dirty = true;
}
@@ -385,7 +387,8 @@ void Pathfinder::invalidateCache() {
namespace {
bool ensureSynced(Grid &grid) {
if (grid.layer && grid.dirty) grid.syncFromLayer();
if (grid.layer && (grid.dirty || grid.layerRevision != grid.layer->tiles()->revision))
grid.syncFromLayer();
return grid.width > 0 && grid.height > 0;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "common/Export.h"
#include <cstddef>
namespace eve::map {
/** @brief One greedily merged solid tile rectangle in world coordinates. */
struct EVENGINE_API TileCollisionRect {
float x = 0.f;
float y = 0.f;
float width = 0.f;
float height = 0.f;
};
/** @brief Optional sink implemented by a physics or project adapter. */
class EVENGINE_API ITileCollisionSink {
public:
static constexpr const char* capabilityName = "ITileCollisionSink";
virtual ~ITileCollisionSink() = default;
/** @brief Atomically replace collision geometry for a stable layer identity. */
virtual void replaceTileCollision(const void* layer, const TileCollisionRect* rects, size_t count) = 0;
};
} // namespace eve::map
+103 -2
View File
@@ -116,6 +116,9 @@ struct TilesetInfo {
int margin = 0;
int spacing = 0;
std::vector<TileLayer::Tileset::Visual> visuals;
std::vector<TileLayer::Tileset::Animation> animations;
std::vector<TileLayer::Tileset::TerrainRule> terrainRules;
std::vector<TileLayer::Tileset::CustomData> customData;
};
TileLayer::Tileset::Visual readTileVisual(Poco::JSON::Object::Ptr o, int fallbackGid) {
@@ -165,9 +168,56 @@ TilesetInfo readTilesetObject(Poco::JSON::Object::Ptr o) {
if (arr) {
for (size_t i = 0; i < arr->size(); ++i) {
auto tile = arr->getObject(static_cast<unsigned int>(i));
auto visual = readTileVisual(tile, info.firstGid + int(i));
const int localId = tile && tile->has("id") ? asInt(tile->get("id"), int(i))
: int(i);
const int gid = tile && tile->has("gid")
? asInt(tile->get("gid"), info.firstGid + localId)
: info.firstGid + localId;
auto visual = readTileVisual(tile, gid);
if (visual.gid > 0 && visual.width > 0 && visual.height > 0)
info.visuals.push_back(visual);
if (!tile) continue;
if (tile->has("animation")) {
auto frames = tile->getArray("animation");
TileLayer::Tileset::Animation animation;
animation.gid = gid;
if (frames) {
for (size_t frameIndex = 0; frameIndex < frames->size(); ++frameIndex) {
auto frame = frames->getObject(static_cast<unsigned int>(frameIndex));
if (!frame) continue;
const int frameLocal = asInt(frame->get("tileid"), localId);
const int duration = frame->has("duration")
? asInt(frame->get("duration"), 100)
: 100;
animation.frames.push_back(
{info.firstGid + frameLocal, std::max(1, duration)});
}
}
if (!animation.frames.empty()) info.animations.push_back(std::move(animation));
}
if (tile->has("terrain") && tile->has("neighborMask")) {
info.terrainRules.push_back(
{gid, asInt(tile->get("terrain"), 0),
asInt(tile->get("neighborMask"), 0) & 0xff});
}
if (tile->has("properties")) {
auto properties = tile->getArray("properties");
if (properties) {
for (size_t propertyIndex = 0; propertyIndex < properties->size();
++propertyIndex) {
auto property =
properties->getObject(static_cast<unsigned int>(propertyIndex));
if (!property || !property->has("name") || !property->has("value"))
continue;
const std::string type = property->has("type")
? asString(property->get("type"))
: "string";
info.customData.push_back(
{gid, asString(property->get("name")), type,
asString(property->get("value"))});
}
}
}
}
}
} catch (...) {
@@ -183,6 +233,9 @@ void applyTileset(TileLayer *layer, const TilesetInfo &info) {
layer->resource()->texturePath = info.image;
layer->setTileset(tex, info.firstGid, info.columns, info.margin, info.spacing);
layer->tileset()->visuals = info.visuals;
layer->tileset()->animations = info.animations;
layer->tileset()->terrainRules = info.terrainRules;
layer->tileset()->customData = info.customData;
}
bool decodeLayerData(Poco::JSON::Object::Ptr layerObj, size_t expectedCount,
@@ -299,7 +352,7 @@ bool isTileLayerObject(Poco::JSON::Object::Ptr o) {
const std::string t = asString(o->get("type"));
if (!t.empty() && t != "tilelayer") return false;
}
return o->has("data");
return o->has("data") || o->has("chunks");
}
bool isObjectGroup(Poco::JSON::Object::Ptr o) {
@@ -435,12 +488,59 @@ bool applyFlatLayerData(TileLayer *layer, Poco::JSON::Object::Ptr root, std::str
gids.assign(need, 0u);
const size_t n = std::min(need, data.size());
for (size_t i = 0; i < n; ++i) gids[i] = data[i];
layer->rebuildSpatialIndex();
return true;
}
bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int mapW, int mapH,
std::string *error) {
if (!layer || !layerObj) return false;
if (layerObj->has("chunks")) {
try {
auto chunks = layerObj->getArray("chunks");
if (!chunks || chunks->size() == 0) return false;
int minX = 0, minY = 0, maxX = 0, maxY = 0;
bool first = true;
for (size_t i = 0; i < chunks->size(); ++i) {
auto chunk = chunks->getObject(static_cast<unsigned int>(i));
if (!chunk) continue;
const int x = asInt(chunk->get("x"), 0), y = asInt(chunk->get("y"), 0);
const int w = asInt(chunk->get("width"), 0), h = asInt(chunk->get("height"), 0);
if (w <= 0 || h <= 0) continue;
if (first) {
minX = x; minY = y; maxX = x + w; maxY = y + h; first = false;
} else {
minX = std::min(minX, x); minY = std::min(minY, y);
maxX = std::max(maxX, x + w); maxY = std::max(maxY, y + h);
}
}
if (first) return false;
float shiftX = 0.f, shiftY = 0.f;
layer->tileToWorld(minX, minY, shiftX, shiftY);
layer->resize(maxX - minX, maxY - minY);
layer->setOrigin(shiftX, shiftY);
auto &gids = layer->tiles()->gids;
for (size_t i = 0; i < chunks->size(); ++i) {
auto chunk = chunks->getObject(static_cast<unsigned int>(i));
if (!chunk) continue;
const int x = asInt(chunk->get("x"), 0), y = asInt(chunk->get("y"), 0);
const int w = asInt(chunk->get("width"), 0), h = asInt(chunk->get("height"), 0);
if (w <= 0 || h <= 0) continue;
std::vector<uint32_t> data;
if (!decodeLayerData(chunk, size_t(w * h), data, error)) return false;
for (int cy = 0; cy < h; ++cy)
for (int cx = 0; cx < w; ++cx)
gids[size_t((y - minY + cy) * layer->getMapWidth() + x - minX + cx)] =
data[size_t(cy * w + cx)];
}
layer->rebuildSpatialIndex();
applyLayerDraw(layer, layerObj);
return true;
} catch (...) {
if (error) *error = "invalid chunk layer";
return false;
}
}
int w = mapW, h = mapH;
if (layerObj->has("width")) w = asInt(layerObj->get("width"), w);
if (layerObj->has("height")) h = asInt(layerObj->get("height"), h);
@@ -454,6 +554,7 @@ bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int
gids.assign(need, 0u);
const size_t n = std::min(need, data.size());
for (size_t i = 0; i < n; ++i) gids[i] = data[i];
layer->rebuildSpatialIndex();
applyLayerDraw(layer, layerObj);
return true;
+219 -2
View File
@@ -6,6 +6,9 @@
#include "graphics/Texture.h"
#include <algorithm>
#include <bit>
#include <cstdlib>
#include <sstream>
namespace eve::map {
@@ -47,12 +50,24 @@ void TileLayer::resize(int mapW, int mapH) {
c->mapH = mapH > 0 ? mapH : 0;
const size_t n = size_t(std::max(0, c->mapW) * std::max(0, c->mapH));
tiles()->gids.assign(n, 0u);
tiles()->terrainIds.assign(n, -1);
rebuildSpatialIndex();
}
void TileLayer::setTile(int tx, int ty, int gid) {
auto c = config();
if (tx < 0 || ty < 0 || tx >= c->mapW || ty >= c->mapH) return;
tiles()->gids[size_t(ty * c->mapW + tx)] = uint32_t(gid < 0 ? 0 : gid);
auto t = tiles();
const size_t index = size_t(ty * c->mapW + tx);
const uint32_t next = uint32_t(gid < 0 ? 0 : gid);
const bool wasEmpty = tileGid(t->gids[index]) == 0;
const bool isEmpty = tileGid(next) == 0;
if (t->gids[index] == next) return;
t->gids[index] = next;
const int chunkIndex = (ty / Tiles::kChunkSize) * t->chunkColumns + tx / Tiles::kChunkSize;
if (chunkIndex >= 0 && chunkIndex < int(t->chunks.size()) && wasEmpty != isEmpty)
t->chunks[size_t(chunkIndex)].nonEmpty += isEmpty ? -1 : 1;
++t->revision;
}
int TileLayer::getTile(int tx, int ty) {
@@ -61,17 +76,219 @@ int TileLayer::getTile(int tx, int ty) {
return int(tiles()->gids[size_t(ty * c->mapW + tx)]);
}
void TileLayer::fillRect(int x, int y, int width, int height, int gid) {
auto c = config();
if (width <= 0 || height <= 0 || c->mapW <= 0 || c->mapH <= 0) return;
const int x0 = std::max(0, x);
const int y0 = std::max(0, y);
const int x1 = std::min(c->mapW, x + width);
const int y1 = std::min(c->mapH, y + height);
if (x0 >= x1 || y0 >= y1) return;
const uint32_t next = uint32_t(gid < 0 ? 0 : gid);
bool changed = false;
auto t = tiles();
for (int ty = y0; ty < y1; ++ty) {
for (int tx = x0; tx < x1; ++tx) {
const size_t index = size_t(ty * c->mapW + tx);
if (t->gids[index] == next) continue;
t->gids[index] = next;
changed = true;
}
}
if (changed) {
rebuildSpatialIndex();
}
}
void TileLayer::fill(int gid) {
const uint32_t g = uint32_t(gid < 0 ? 0 : gid);
for (auto &v : tiles()->gids) v = g;
auto t = tiles();
bool changed = false;
for (auto &v : t->gids) {
if (v != g) changed = true;
v = g;
}
if (changed) {
rebuildSpatialIndex();
}
}
void TileLayer::clear() { fill(0); }
int TileLayer::getRevision() { return int(tiles()->revision & 0x7fffffffu); }
int TileLayer::getChunkSize() { return Tiles::kChunkSize; }
int TileLayer::getChunkCount() { return int(tiles()->chunks.size()); }
int TileLayer::getNonEmptyChunkCount() {
int count = 0;
for (const auto &chunk : tiles()->chunks)
if (chunk.nonEmpty > 0) ++count;
return count;
}
void TileLayer::rebuildSpatialIndex() {
auto c = config();
auto t = tiles();
t->chunkColumns = (c->mapW + Tiles::kChunkSize - 1) / Tiles::kChunkSize;
t->chunkRows = (c->mapH + Tiles::kChunkSize - 1) / Tiles::kChunkSize;
t->chunks.assign(size_t(t->chunkColumns * t->chunkRows), {});
for (int y = 0; y < c->mapH; ++y) {
for (int x = 0; x < c->mapW; ++x) {
const size_t index = size_t(y * c->mapW + x);
if (index >= t->gids.size() || tileGid(t->gids[index]) == 0) continue;
const int chunkIndex = (y / Tiles::kChunkSize) * t->chunkColumns + x / Tiles::kChunkSize;
++t->chunks[size_t(chunkIndex)].nonEmpty;
}
}
++t->revision;
}
void TileLayer::clearTileAnimation(int gid) {
auto &animations = tileset()->animations;
animations.erase(std::remove_if(animations.begin(), animations.end(),
[gid](const Tileset::Animation &a) { return a.gid == gid; }),
animations.end());
}
void TileLayer::addTileAnimationFrame(int gid, int frameGid, int durationMs) {
if (gid <= 0 || frameGid <= 0) return;
auto &animations = tileset()->animations;
auto it = std::find_if(animations.begin(), animations.end(),
[gid](const Tileset::Animation &a) { return a.gid == gid; });
if (it == animations.end()) {
animations.push_back({});
it = animations.end() - 1;
it->gid = gid;
}
it->frames.push_back({frameGid, std::max(1, durationMs)});
}
int TileLayer::getTileAnimationFrameCount(int gid) {
for (const auto &animation : tileset()->animations)
if (animation.gid == gid) return int(animation.frames.size());
return 0;
}
namespace {
TileLayer::Tileset::CustomData *findCustomData(TileLayer::Tileset &tileset, int gid,
const std::string &name) {
for (auto &entry : tileset.customData)
if (entry.gid == gid && entry.name == name) return &entry;
tileset.customData.push_back({gid, name, {}, {}});
return &tileset.customData.back();
}
const TileLayer::Tileset::CustomData *findCustomData(const TileLayer::Tileset &tileset, int gid,
const std::string &name) {
for (const auto &entry : tileset.customData)
if (entry.gid == gid && entry.name == name) return &entry;
return nullptr;
}
} // namespace
void TileLayer::setTileDataString(int gid, const std::string &name, const std::string &value) {
if (gid <= 0 || name.empty()) return;
auto *entry = findCustomData(*tileset(), gid, name);
entry->type = "string";
entry->value = value;
}
void TileLayer::setTileDataNumber(int gid, const std::string &name, float value) {
if (gid <= 0 || name.empty()) return;
auto *entry = findCustomData(*tileset(), gid, name);
entry->type = "number";
std::ostringstream out;
out << value;
entry->value = out.str();
}
void TileLayer::setTileDataBool(int gid, const std::string &name, bool value) {
if (gid <= 0 || name.empty()) return;
auto *entry = findCustomData(*tileset(), gid, name);
entry->type = "bool";
entry->value = value ? "true" : "false";
}
std::string TileLayer::getTileDataType(int gid, const std::string &name) {
const auto *entry = findCustomData(*tileset(), gid, name);
return entry ? entry->type : std::string();
}
std::string TileLayer::getTileDataString(int gid, const std::string &name) {
const auto *entry = findCustomData(*tileset(), gid, name);
return entry ? entry->value : std::string();
}
float TileLayer::getTileDataNumber(int gid, const std::string &name) {
const auto *entry = findCustomData(*tileset(), gid, name);
return entry && (entry->type == "number" || entry->type == "float" || entry->type == "int")
? std::strtof(entry->value.c_str(), nullptr)
: 0.f;
}
bool TileLayer::getTileDataBool(int gid, const std::string &name) {
const auto *entry = findCustomData(*tileset(), gid, name);
return entry && entry->type == "bool" && entry->value == "true";
}
void TileLayer::setTerrainRule(int gid, int terrain, int neighborMask) {
if (gid <= 0 || terrain < 0) return;
auto &rules = tileset()->terrainRules;
auto it = std::find_if(rules.begin(), rules.end(), [=](const Tileset::TerrainRule &rule) {
return rule.terrain == terrain && rule.neighborMask == (neighborMask & 0xff);
});
if (it == rules.end()) rules.push_back({gid, terrain, neighborMask & 0xff});
else it->gid = gid;
}
void TileLayer::clearTerrainRules() { tileset()->terrainRules.clear(); }
int TileLayer::getTerrain(int x, int y) {
auto c = config();
auto t = tiles();
if (x < 0 || y < 0 || x >= c->mapW || y >= c->mapH) return -1;
const size_t index = size_t(y * c->mapW + x);
return index < t->terrainIds.size() ? t->terrainIds[index] : -1;
}
void TileLayer::paintTerrain(int x, int y, int terrain) {
auto c = config();
auto t = tiles();
if (x < 0 || y < 0 || x >= c->mapW || y >= c->mapH || terrain < 0) return;
if (t->terrainIds.size() != t->gids.size()) t->terrainIds.assign(t->gids.size(), -1);
t->terrainIds[size_t(y * c->mapW + x)] = terrain;
constexpr int dx[8] = {-1, 0, 1, 1, 1, 0, -1, -1};
constexpr int dy[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
const int minX = std::max(0, x - 1), maxX = std::min(c->mapW - 1, x + 1);
const int minY = std::max(0, y - 1), maxY = std::min(c->mapH - 1, y + 1);
for (int cy = minY; cy <= maxY; ++cy) {
for (int cx = minX; cx <= maxX; ++cx) {
const int cellTerrain = getTerrain(cx, cy);
if (cellTerrain < 0) continue;
int mask = 0;
for (int i = 0; i < 8; ++i)
if (getTerrain(cx + dx[i], cy + dy[i]) == cellTerrain) mask |= 1 << i;
const Tileset::TerrainRule *best = nullptr;
int bestMismatch = 1000;
for (const auto &rule : tileset()->terrainRules) {
if (rule.terrain != cellTerrain) continue;
const int mismatch = std::popcount(unsigned(rule.neighborMask ^ mask));
if (mismatch < bestMismatch) {
best = &rule;
bestMismatch = mismatch;
}
}
if (best) setTile(cx, cy, best->gid);
}
}
}
void TileLayer::setTileset(graphics::Texture *texture, int firstGid, int columns, int margin,
int spacing) {
auto ts = tileset();
ts->visuals.clear();
ts->animations.clear();
ts->terrainRules.clear();
ts->customData.clear();
ts->texture = texture;
ts->firstGid = firstGid > 0 ? firstGid : 1;
ts->columns = columns > 0 ? columns : 1;
+66
View File
@@ -48,10 +48,43 @@ public:
};
struct Tiles {
struct Chunk {
int nonEmpty = 0;
};
static constexpr int kChunkSize = 32;
std::vector<uint32_t> gids;
std::vector<Chunk> chunks;
int chunkColumns = 0;
int chunkRows = 0;
uint64_t revision = 1;
std::vector<int> terrainIds;
};
struct Tileset {
struct AnimationFrame {
int gid = 0;
int durationMs = 100;
};
struct Animation {
int gid = 0;
std::vector<AnimationFrame> frames;
};
struct TerrainRule {
int gid = 0;
int terrain = 0;
int neighborMask = 0;
};
struct CustomData {
int gid = 0;
std::string name;
std::string type;
std::string value;
};
/** @brief Per-GID visual metadata emitted by a project-defined asset pipeline. */
struct Visual {
int gid = 0;
@@ -76,6 +109,9 @@ public:
int margin = 0;
int spacing = 0;
std::vector<Visual> visuals;
std::vector<Animation> animations;
std::vector<TerrainRule> terrainRules;
std::vector<CustomData> customData;
};
struct Draw {
@@ -130,11 +166,41 @@ public:
/** @brief Tile GID access; 0 = empty. */
void setTile(int tx, int ty, int gid);
int getTile(int tx, int ty);
/** @brief Fill a clipped rectangular region and publish one tile revision. */
void fillRect(int x, int y, int width, int height, int gid);
/** @brief Fills the whole layer with one GID. */
void fill(int gid);
/** @brief Clears the layer to empty. */
void clear();
/** @brief Monotonic tile-data revision used by derived navigation/FOV caches. */
int getRevision();
/** @brief Runtime spatial-index diagnostics. */
int getChunkSize();
int getChunkCount();
int getNonEmptyChunkCount();
/** @brief Rebuild chunk occupancy after bulk loader writes directly to gids. */
void rebuildSpatialIndex();
/** @brief TileSet v2 animation authoring; frames are selected during map update/render. */
void clearTileAnimation(int gid);
void addTileAnimationFrame(int gid, int frameGid, int durationMs);
int getTileAnimationFrameCount(int gid);
/** @brief TileSet v2 typed custom data (stored losslessly as canonical text). */
void setTileDataString(int gid, const std::string &name, const std::string &value);
void setTileDataNumber(int gid, const std::string &name, float value);
void setTileDataBool(int gid, const std::string &name, bool value);
std::string getTileDataType(int gid, const std::string &name);
std::string getTileDataString(int gid, const std::string &name);
float getTileDataNumber(int gid, const std::string &name);
bool getTileDataBool(int gid, const std::string &name);
/** @brief Register an 8-neighbor terrain rule and paint with local re-resolution. */
void setTerrainRule(int gid, int terrain, int neighborMask);
void clearTerrainRules();
void paintTerrain(int x, int y, int terrain);
int getTerrain(int x, int y);
/** @brief Binds the atlas texture and tile table. */
void setTileset(graphics::Texture *texture, int firstGid, int columns, int margin = 0,
int spacing = 0);
+135 -51
View File
@@ -10,6 +10,8 @@
#include <unordered_set>
#include <vector>
#include <algorithm>
#include <cmath>
namespace eve::map {
namespace {
@@ -17,6 +19,9 @@ namespace {
int gLastVisibleTileCount = 0;
int gLastCustomVisualCount = 0;
int gLastAtlasCount = 0;
int gLastVisitedChunkCount = 0;
int gLastVisitedCellCount = 0;
double gAnimationTimeMs = 0.0;
struct ViewCam {
float x = 0.f;
@@ -92,12 +97,66 @@ int64_t fileModtime(const std::string &path) {
return info.modtime;
}
uint32_t animatedGid(const TileLayer::Tileset &ts, uint32_t gid) {
for (const auto &animation : ts.animations) {
if (animation.gid != int(gid) || animation.frames.empty()) continue;
int total = 0;
for (const auto &frame : animation.frames) total += std::max(1, frame.durationMs);
if (total <= 0) return gid;
int cursor = int(std::fmod(gAnimationTimeMs, double(total)));
for (const auto &frame : animation.frames) {
cursor -= std::max(1, frame.durationMs);
if (cursor < 0) return uint32_t(frame.gid);
}
}
return gid;
}
bool chunkVisible(const TileLayer::Config &cfg, const TileLayer::Tileset &ts, const ViewCam &cam,
int viewWidth, int viewHeight, int x0, int y0, int x1, int y1) {
if (!cam.valid || viewWidth <= 0 || viewHeight <= 0) return true;
float minX = 1e30f, minY = 1e30f, maxX = -1e30f, maxY = -1e30f;
const int xs[] = {x0, std::max(x0, x1 - 1)};
const int ys[] = {y0, std::max(y0, y1 - 1)};
for (int y : ys) {
for (int x : xs) {
float wx = 0.f, wy = 0.f;
tileToWorld(cfg, x, y, wx, wy);
minX = std::min(minX, wx);
minY = std::min(minY, wy);
maxX = std::max(maxX, wx + cfg.tileW);
maxY = std::max(maxY, wy + cfg.tileH);
}
}
float expandX = cfg.tileW;
float expandY = cfg.tileH;
for (const auto &visual : ts.visuals) {
expandX = std::max(expandX, float(visual.width) + std::fabs(visual.pivotX));
expandY = std::max(expandY, float(visual.height) + std::fabs(visual.pivotY));
}
minX -= expandX;
minY -= expandY;
maxX += expandX;
maxY += expandY;
const float zoom = cam.zoom > 0.f ? cam.zoom : 0.0001f;
const float left = cam.x - float(viewWidth) * 0.5f / zoom;
const float right = cam.x + float(viewWidth) * 0.5f / zoom;
const float top = cam.y - float(viewHeight) * 0.5f / zoom;
const float bottom = cam.y + float(viewHeight) * 0.5f / zoom;
return maxX >= left && minX <= right && maxY >= top && minY <= bottom;
}
} // namespace
void TileRenderSystem::collect(std::vector<graphics::DrawItem2D> &out) {
void TileRenderSystem::collect(std::vector<graphics::DrawItem2D> &out) { collect(out, 0, 0); }
void TileRenderSystem::collect(std::vector<graphics::DrawItem2D> &out, int viewWidth,
int viewHeight) {
gLastVisibleTileCount = 0;
gLastCustomVisualCount = 0;
gLastAtlasCount = 0;
gLastVisitedChunkCount = 0;
gLastVisitedCellCount = 0;
if (ecs::current()->getManager<TileLayer>() == nullptr) return;
std::unordered_set<graphics::Texture *> atlases;
@@ -112,58 +171,77 @@ void TileRenderSystem::collect(std::vector<graphics::DrawItem2D> &out) {
const Color &tint = draw->tint;
const ViewCam cam = fromEntity(draw->camera);
for (int ty = 0; ty < cfg->mapH; ++ty) {
for (int tx = 0; tx < cfg->mapW; ++tx) {
const uint32_t raw = tiles->gids[size_t(ty * cfg->mapW + tx)];
const uint32_t gid = tileGid(raw);
if (gid == 0) continue;
const int chunkColumns = std::max(1, tiles->chunkColumns);
const int chunkRows = std::max(1, tiles->chunkRows);
for (int cy = 0; cy < chunkRows; ++cy) {
for (int cx = 0; cx < chunkColumns; ++cx) {
const int chunkIndex = cy * chunkColumns + cx;
if (!tiles->chunks.empty() &&
(chunkIndex >= int(tiles->chunks.size()) ||
tiles->chunks[size_t(chunkIndex)].nonEmpty == 0))
continue;
const int x0 = cx * TileLayer::Tiles::kChunkSize;
const int y0 = cy * TileLayer::Tiles::kChunkSize;
const int x1 = std::min(cfg->mapW, x0 + TileLayer::Tiles::kChunkSize);
const int y1 = std::min(cfg->mapH, y0 + TileLayer::Tiles::kChunkSize);
if (!chunkVisible(*cfg, *ts, cam, viewWidth, viewHeight, x0, y0, x1, y1))
continue;
++gLastVisitedChunkCount;
for (int ty = y0; ty < y1; ++ty) {
for (int tx = x0; tx < x1; ++tx) {
++gLastVisitedCellCount;
const uint32_t raw = tiles->gids[size_t(ty * cfg->mapW + tx)];
const uint32_t gid = animatedGid(*ts, tileGid(raw));
if (gid == 0) continue;
graphics::DrawItem2D item;
tileToWorld(*cfg, tx, ty, item.x, item.y);
item.w = cfg->tileW;
item.h = cfg->tileH;
item.depthY = tileToDepthY(*cfg, tx, ty);
item.layer = draw->layer;
item.canvas = draw->canvas;
item.camera = draw->camera;
item.camValid = cam.valid;
item.camX = cam.x;
item.camY = cam.y;
item.camZoom = cam.zoom;
item.receiveLight = false;
item.litPath = false;
graphics::DrawItem2D item;
tileToWorld(*cfg, tx, ty, item.x, item.y);
item.w = cfg->tileW;
item.h = cfg->tileH;
item.depthY = tileToDepthY(*cfg, tx, ty);
item.layer = draw->layer;
item.canvas = draw->canvas;
item.camera = draw->camera;
item.camValid = cam.valid;
item.camX = cam.x;
item.camY = cam.y;
item.camZoom = cam.zoom;
item.receiveLight = false;
item.litPath = false;
float u0, v0, u1, v1;
const auto *visual = visualForGid(*ts, gid);
if (visual && visualUV(*ts, *visual, u0, v0, u1, v1)) {
++gLastCustomVisualCount;
item.x -= visual->pivotX;
item.y -= visual->pivotY;
item.w = float(visual->width);
item.h = float(visual->height);
item.depthY += visual->sortBias;
item.texture = ts->texture;
item.hasUV = true;
item.u0 = u0;
item.v0 = v0;
item.u1 = u1;
item.v1 = v1;
item.color = tint;
} else if (atlasUV(*ts, gid, u0, v0, u1, v1)) {
item.texture = ts->texture;
item.hasUV = true;
item.u0 = u0;
item.v0 = v0;
item.u1 = u1;
item.v1 = v1;
item.color = tint;
} else {
item.texture = nullptr;
item.color = solidForGid(gid, tint);
float u0, v0, u1, v1;
const auto* visual = visualForGid(*ts, gid);
if (visual && visualUV(*ts, *visual, u0, v0, u1, v1)) {
++gLastCustomVisualCount;
item.x -= visual->pivotX;
item.y -= visual->pivotY;
item.w = float(visual->width);
item.h = float(visual->height);
item.depthY += visual->sortBias;
item.texture = ts->texture;
item.hasUV = true;
item.u0 = u0;
item.v0 = v0;
item.u1 = u1;
item.v1 = v1;
item.color = tint;
} else if (atlasUV(*ts, gid, u0, v0, u1, v1)) {
item.texture = ts->texture;
item.hasUV = true;
item.u0 = u0;
item.v0 = v0;
item.u1 = u1;
item.v1 = v1;
item.color = tint;
} else {
item.texture = nullptr;
item.color = solidForGid(gid, tint);
}
out.push_back(item);
++gLastVisibleTileCount;
if (item.texture) atlases.insert(item.texture);
}
}
out.push_back(item);
++gLastVisibleTileCount;
if (item.texture) atlases.insert(item.texture);
}
}
}
@@ -173,13 +251,19 @@ void TileRenderSystem::collect(std::vector<graphics::DrawItem2D> &out) {
void TileRenderSystem::render(graphics::Graphics *gfx) {
if (!gfx) return;
std::vector<graphics::DrawItem2D> items;
collect(items);
collect(items, gfx->getWidth(), gfx->getHeight());
graphics::RenderSystem::drawItems(*gfx, items, false);
}
int TileRenderSystem::lastVisibleTileCount() { return gLastVisibleTileCount; }
int TileRenderSystem::lastCustomVisualCount() { return gLastCustomVisualCount; }
int TileRenderSystem::lastAtlasCount() { return gLastAtlasCount; }
int TileRenderSystem::lastVisitedChunkCount() { return gLastVisitedChunkCount; }
int TileRenderSystem::lastVisitedCellCount() { return gLastVisitedCellCount; }
void TileRenderSystem::update(float dt) {
if (dt > 0.f && std::isfinite(dt)) gAnimationTimeMs += double(dt) * 1000.0;
}
int TileConfigSystem::poll() {
if (ecs::current()->getManager<TileLayer>() == nullptr) return 0;
+7
View File
@@ -18,8 +18,12 @@ namespace eve::map {
/** @brief Draws all visible TileLayer entities via Graphics batch path (UV atlas quads). */
class TileRenderSystem {
public:
/** @brief Advance TileSet animation clocks once per map update. */
static void update(float dt);
/** @brief Append non-empty visible tiles into a shared 2D draw queue. */
static void collect(std::vector<graphics::DrawItem2D> &out);
/** @brief Viewport-aware collection that rejects off-screen chunks before scanning cells. */
static void collect(std::vector<graphics::DrawItem2D> &out, int viewWidth, int viewHeight);
/** @brief Collect + draw tiles only (no present). Null gfx is a no-op. */
static void render(graphics::Graphics *gfx);
@@ -30,6 +34,9 @@ public:
static int lastCustomVisualCount();
/** @brief Number of distinct atlas textures referenced by the last pass. */
static int lastAtlasCount();
/** @brief Spatial-index work counters for deterministic performance regressions. */
static int lastVisitedChunkCount();
static int lastVisitedCellCount();
};
/**
+3 -771
View File
@@ -1,25 +1,13 @@
#include "spritestack/SpriteStack.h"
#include "common/Exception.h"
#include "common/ECS.h"
#include "graphics/ClipSpace.h"
#include "graphics/Graphics.h"
#include "graphics/Mesh.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "image/ImageData.h"
#include "model3d/ModelData.h"
#include "spritestack/shaders/sprite_stack_frag_spv.inc"
#include "spritestack/shaders/sprite_stack_vert_spv.inc"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <memory>
#include <simplesquirrel/simplesquirrel.hpp>
#include <string>
#include <utility>
#include <vector>
@@ -405,763 +393,7 @@ std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &kind,
}
// ---------------------------------------------------------------------------
// SpriteStack3D
// ---------------------------------------------------------------------------
namespace {
std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
return std::vector<uint32_t>(data, data + count);
}
glm::mat4 billboardModel(const glm::vec3 &center, float width, float height,
const glm::vec3 &eye) {
glm::vec3 d = eye - center;
d.y = 0.f;
if (glm::length(d) < 1e-4f) d = glm::vec3(0.f, 0.f, 1.f);
d = glm::normalize(d);
const glm::vec3 up(0.f, 1.f, 0.f);
const glm::vec3 right = glm::normalize(glm::cross(up, d));
const glm::vec3 forward = glm::normalize(glm::cross(right, up));
glm::mat4 m(1.f);
m[0] = glm::vec4(right * width, 0.f);
m[1] = glm::vec4(up * height, 0.f);
m[2] = glm::vec4(forward, 0.f);
m[3] = glm::vec4(center, 1.f);
return m;
}
graphics::Camera3D *findActiveCamera3D() {
if (ecs::current()->getManager<graphics::Camera3D>() == nullptr) return nullptr;
auto camView = ecs::View<graphics::Camera3D, graphics::Camera3D::Data>();
for (auto it = camView.begin(); it != camView.end(); ++it) {
auto [data] = *it;
if (!data->active || !data->entity) continue;
return data->entity;
}
return nullptr;
}
} // namespace
void SpriteStack3D::setLayerCount(int count) {
if (count < 0) throw eve::Exception("SpriteStack3D.setLayerCount: count must be >= 0");
layerCount_ = count;
layers_.assign(size_t(count), Layer{});
bumpVersion();
}
int SpriteStack3D::getLayerCount() const { return layerCount_; }
void SpriteStack3D::setLayerTexture(graphics::Texture *texture, int index) {
if (index < 0 || index >= layerCount_)
throw eve::Exception("SpriteStack3D.setLayerTexture: index %d out of range [0,%d)", index,
layerCount_);
layers_[size_t(index)] = Layer{texture, glm::vec4(0.f, 0.f, 1.f, 1.f)};
bumpVersion();
}
graphics::Texture *SpriteStack3D::getLayerTexture(int index) const {
if (index < 0 || index >= layerCount_) return nullptr;
return layers_[size_t(index)].texture;
}
void SpriteStack3D::setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayerImage: null graphics");
if (!img) throw eve::Exception("SpriteStack3D.setLayerImage: null ImageData");
setLayerTexture(gfx->newTextureFromImageData(img, false, false), index);
}
void SpriteStack3D::setLayerFile(graphics::Graphics *gfx, const std::string &path, int index) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayerFile: null graphics");
setLayerTexture(gfx->newTextureFromFile(path), index);
}
void SpriteStack3D::setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null graphics");
if (!atlas) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null atlas");
if (layerCount <= 0) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: count must be > 0");
setLayerCount(layerCount);
for (int i = 0; i < layerCount; ++i) {
const float u0 = float(i) / float(layerCount);
const float u1 = float(i + 1) / float(layerCount);
layers_[size_t(i)] = Layer{atlas, glm::vec4(u0, 0.f, u1, 1.f)};
}
bumpVersion();
}
void SpriteStack3D::setThickness(float thickness) {
if (thickness <= 0.f) throw eve::Exception("SpriteStack3D.setThickness: must be > 0");
thickness_ = thickness;
bumpVersion();
}
float SpriteStack3D::getThickness() const { return thickness_; }
void SpriteStack3D::setSize(float width, float height) {
if (width <= 0.f || height <= 0.f)
throw eve::Exception("SpriteStack3D.setSize: size must be > 0");
width_ = width;
height_ = height;
bumpVersion();
}
float SpriteStack3D::getWidth() const { return width_; }
float SpriteStack3D::getHeight() const { return height_; }
void SpriteStack3D::setPosition(float x, float y, float z) {
x_ = x;
y_ = y;
z_ = z;
bumpVersion();
}
void SpriteStack3D::setYaw(float yaw) {
yaw_ = yaw;
bumpVersion();
}
void SpriteStack3D::setTint(float r, float g, float b, float a) {
tintR_ = r;
tintG_ = g;
tintB_ = b;
tintA_ = a;
bumpVersion();
}
void SpriteStack3D::setAlphaCutoff(float cutoff) { alphaCutoff_ = clampf(cutoff, 0.f, 1.f); }
void SpriteStack3D::setVisible(bool visible) {
visible_ = visible;
bumpVersion();
}
bool SpriteStack3D::getVisible() const { return visible_; }
void SpriteStack3D::setMode(const std::string &mode) {
if (mode != "vertical" && mode != "horizontal")
throw eve::Exception("SpriteStack3D.setMode: unknown mode '%s' (vertical|horizontal)",
mode.c_str());
mode_ = mode;
bumpVersion();
}
std::string SpriteStack3D::getMode() const { return mode_; }
void SpriteStack3D::setShadowEnabled(bool enabled) {
shadowEnabled_ = enabled;
bumpVersion();
}
bool SpriteStack3D::getShadowEnabled() const { return shadowEnabled_; }
void SpriteStack3D::setShadowOpacity(float opacity) {
shadowOpacity_ = clampf(opacity, 0.f, 1.f);
bumpVersion();
}
void SpriteStack3D::setShadowLight(float dx, float dy, float dz) {
shadowLightX_ = dx;
shadowLightY_ = dy;
shadowLightZ_ = dz;
bumpVersion();
}
void SpriteStack3D::setShadowPlaneY(float y) {
shadowPlaneY_ = y;
bumpVersion();
}
void SpriteStack3D::setOutline(float width, float r, float g, float b) {
outlineWidth_ = std::max(0.f, width);
outlineR_ = r;
outlineG_ = g;
outlineB_ = b;
bumpVersion();
}
float SpriteStack3D::getOutlineWidth() const { return outlineWidth_; }
void SpriteStack3D::setOutlineColor(float r, float g, float b) {
outlineR_ = r;
outlineG_ = g;
outlineB_ = b;
bumpVersion();
}
void SpriteStack3D::ensureResources(graphics::Graphics *gfx) const {
if (quad_ && shader_) return;
if (!quad_) {
const float posXYZ[] = {-0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f,
0.5f, -0.5f, 0.f, -0.5f, -0.5f, 0.f};
const float nrmXYZ[] = {0.f, 0.f, 1.f, 0.f, 0.f, 1.f,
0.f, 0.f, 1.f, 0.f, 0.f, 1.f};
const float uvST[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
const uint32_t indices[] = {0, 1, 2, 0, 2, 3};
quad_ = gfx->newMeshFromArrays(posXYZ, nrmXYZ, uvST, 4, indices, 6);
}
if (!shader_) {
auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
shader_ = gfx->newHairShaderFromSpv(vert, frag);
if (!shader_ || !shader_->gpuHandle)
throw eve::Exception("SpriteStack3D.render: failed to create slice shader");
shader_->declareVec4("uvRect");
shader_->declareFloat("alphaCutoff");
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
shader_->sendFloat("alphaCutoff", alphaCutoff_);
}
}
void SpriteStack3D::collectSlices(const SpriteStack3D &stack, const glm::vec3 &eye,
std::vector<SliceDraw> &out) {
if (!stack.visible_ || stack.layerCount_ <= 0) return;
const glm::mat4 yawM = glm::rotate(glm::mat4(1.f), stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
const float half = 0.5f * float(stack.layerCount_ - 1);
for (int i = 0; i < stack.layerCount_; ++i) {
graphics::Texture *tex = stack.layers_[size_t(i)].texture;
if (!tex) continue;
const float offset = stack.thickness_ * (float(i) - half);
glm::vec3 center(stack.x_, stack.y_, stack.z_);
if (stack.mode_ == "horizontal") {
center.y += offset;
} else {
const glm::vec4 off = yawM * glm::vec4(0.f, 0.f, offset, 0.f);
center += glm::vec3(off);
}
SliceDraw s;
const glm::vec3 d = center - eye;
s.distSq = glm::dot(d, d);
if (stack.mode_ == "horizontal") {
// Horizontal top-down layer: rotate image by yaw, keep the quad
// level so a 3/4 camera sees true volume.
s.model = glm::translate(glm::mat4(1.f), center);
s.model = glm::rotate(s.model, stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
s.model = glm::rotate(s.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
s.model = glm::scale(s.model, glm::vec3(stack.width_, stack.height_, 1.f));
} else {
s.model = billboardModel(center, stack.width_, stack.height_, eye);
}
s.texture = tex;
s.uv = stack.layers_[size_t(i)].uv;
out.push_back(s);
}
}
SpriteStack3D::SliceDraw SpriteStack3D::makeShadowDraw(const SpriteStack3D &stack,
const SliceDraw &s,
const glm::vec3 &eye) {
SliceDraw out = s;
const glm::vec3 light =
glm::normalize(glm::vec3(stack.shadowLightX_, stack.shadowLightY_, stack.shadowLightZ_));
const glm::vec3 center(s.model[3]);
// Lift the shadow above the ground so the depth test (strict LESS) cannot
// reject it as coplanar with the ground surface.
const float planeY = stack.shadowPlaneY_ + 0.02f;
const float t = (planeY - center.y) / light.y;
const glm::vec3 c = center + light * t;
out.model = glm::translate(glm::mat4(1.f), c);
out.model = glm::rotate(out.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
out.model = glm::scale(out.model, glm::vec3(stack.width_, stack.height_, 1.f));
const glm::vec3 d = c - eye;
out.distSq = glm::dot(d, d);
return out;
}
SpriteStack3D::SliceDraw SpriteStack3D::makeOutlineDraw(const SpriteStack3D &stack,
const SliceDraw &s,
const glm::vec3 &eye) {
SliceDraw out = s;
const glm::vec3 center(s.model[3]);
glm::vec3 away = center - eye;
if (glm::length(away) < 1e-6f) away = glm::vec3(0.f, 0.f, 1.f);
away = glm::normalize(away);
// Push the outline slightly away from the camera so the slice draw (same
// plane) still passes the strict-LESS depth test over it and only the rim
// stays visible.
out.model[3] = glm::vec4(center + away * 0.02f, 1.f);
const float k = stack.outlineWidth_;
out.model = out.model * glm::scale(glm::mat4(1.f), glm::vec3(1.f + 2.f * k, 1.f + 2.f * k, 1.f));
return out;
}
std::vector<SpriteStack3D *> &SpriteStack3D::gbufferStacks() {
static std::vector<SpriteStack3D *> stacks;
return stacks;
}
void SpriteStack3D::registerGbufferDrawer() {
static bool registered = false;
if (registered) return;
registered = true;
eve::graphics::RenderSystem3D::addGBufferExtraDrawer(
[](eve::graphics::Graphics &gfx, const eve::graphics::Camera3D::Data &cam,
const glm::mat4 &viewProj, float /*aspect*/) {
drawGbufferStacks(gfx, viewProj, cam.eyeX, cam.eyeY, cam.eyeZ, cam.nearZ, cam.farZ);
});
}
void SpriteStack3D::drawGbufferStacks(eve::graphics::Graphics &gfx, const glm::mat4 &viewProj,
float eyeX, float eyeY, float eyeZ, float nearZ,
float farZ) {
const glm::vec3 eye(eyeX, eyeY, eyeZ);
std::vector<SliceDraw> slices;
for (SpriteStack3D *st : gbufferStacks()) {
if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
st->ensureResources(&gfx);
if (!st->quad_) continue;
slices.clear();
collectSlices(*st, eye, slices);
for (const auto &s : slices) {
const glm::mat4 mvp = viewProj * s.model;
gfx.drawMeshGBufferAlpha(st->quad_, mvp, s.model, nearZ, farZ, s.texture,
st->tintR_, st->tintG_, st->tintB_);
}
}
}
void SpriteStack3D::setGbufferEnabled(bool enabled) {
gbufferEnabled_ = enabled;
auto &stacks = gbufferStacks();
auto it = std::find(stacks.begin(), stacks.end(), this);
if (enabled && it == stacks.end()) stacks.push_back(this);
if (!enabled && it != stacks.end()) stacks.erase(it);
registerGbufferDrawer();
}
bool SpriteStack3D::getGbufferEnabled() const { return gbufferEnabled_; }
std::vector<SpriteStack3D *> &SpriteStack3D::shadowCasterStacks() {
static std::vector<SpriteStack3D *> stacks;
return stacks;
}
void SpriteStack3D::registerShadowDrawer() {
static bool registered = false;
if (registered) return;
registered = true;
eve::graphics::RenderSystem3D::addShadowExtraDrawer(
[](eve::graphics::Graphics &gfx, const glm::mat4 &lightVP,
const eve::graphics::Camera3D::Data &cam) {
drawShadowCasterStacks(gfx, lightVP, cam.eyeX, cam.eyeY, cam.eyeZ);
});
}
void SpriteStack3D::drawShadowCasterStacks(eve::graphics::Graphics &gfx,
const glm::mat4 &lightVP, float eyeX, float eyeY,
float eyeZ) {
const glm::vec3 eye(eyeX, eyeY, eyeZ);
std::vector<SliceDraw> slices;
for (SpriteStack3D *st : shadowCasterStacks()) {
if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
st->ensureResources(&gfx);
if (!st->quad_) continue;
slices.clear();
collectSlices(*st, eye, slices);
for (const auto &s : slices) {
// Camera-facing cards cast their silhouette into the light view;
// the alpha-cutout shadow pipeline keeps the shape instead of a
// solid quad.
gfx.drawMeshShadowAlpha(st->quad_, lightVP * s.model, s.texture);
}
}
}
void SpriteStack3D::setCastShadow(bool cast) {
castShadow_ = cast;
auto &stacks = shadowCasterStacks();
auto it = std::find(stacks.begin(), stacks.end(), this);
if (cast && it == stacks.end()) stacks.push_back(this);
if (!cast && it != stacks.end()) stacks.erase(it);
registerShadowDrawer();
}
bool SpriteStack3D::getCastShadow() const { return castShadow_; }
SpriteStack3D::~SpriteStack3D() {
auto &gb = gbufferStacks();
auto it = std::find(gb.begin(), gb.end(), this);
if (it != gb.end()) gb.erase(it);
auto &sc = shadowCasterStacks();
it = std::find(sc.begin(), sc.end(), this);
if (it != sc.end()) sc.erase(it);
}
void SpriteStack3D::render(graphics::Graphics *gfx, graphics::Camera3D *camera) const {
if (!gfx) throw eve::Exception("SpriteStack3D.render: null graphics");
if (!visible_ || layerCount_ <= 0) return;
if (!camera) camera = findActiveCamera3D();
if (!camera) return;
ensureResources(gfx);
if (!quad_ || !shader_) return;
auto cd = camera->data();
const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
const float fovRad = cd->fovYDeg * 0.017453292519943295f;
const glm::mat4 projM =
eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
gfx->setMesh3DViewProj(projM * viewM);
gfx->setMesh3DView(viewM);
gfx->setMesh3DClip(cd->nearZ, cd->farZ);
gfx->setMesh3DCameraPos(eye);
gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
shader_->sendFloat("alphaCutoff", alphaCutoff_);
std::vector<SliceDraw> slices;
slices.reserve(size_t(layerCount_));
collectSlices(*this, eye, slices);
if (slices.empty()) return;
// Far-to-near: alpha-blended slices need back-to-front draw order.
std::sort(slices.begin(), slices.end(),
[](const SliceDraw &a, const SliceDraw &b) { return a.distSq > b.distSq; });
// Projected contact shadows: each slice silhouette squashed onto the ground
// plane along the light direction, painted as a dark alpha blob.
if (shadowEnabled_ && shadowLightY_ < -1e-4f) {
const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, shadowOpacity_);
for (const auto &s : slices) {
const SliceDraw shadow = makeShadowDraw(*this, s, eye);
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, shadow.model, shadow.texture, shadowColor, shader_);
}
}
// Stylized rim outline: expanded dark silhouettes behind the stack.
if (outlineWidth_ > 0.f) {
const eve::graphics::Color outlineColor(outlineR_, outlineG_, outlineB_, 1.f);
for (const auto &s : slices) {
const SliceDraw outline = makeOutlineDraw(*this, s, eye);
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, outline.model, outline.texture, outlineColor, shader_);
}
}
const glm::vec4 tint(tintR_, tintG_, tintB_, tintA_);
for (const auto &s : slices) {
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, s.model, s.texture,
eve::graphics::Color(tint.r, tint.g, tint.b, tint.a),
shader_);
}
}
// ---------------------------------------------------------------------------
// SpriteStackBatch
// ---------------------------------------------------------------------------
namespace {
uint32_t packTint(float r, float g, float b, float a) {
return (uint32_t(clampf(r, 0.f, 1.f) * 255.f) << 24) |
(uint32_t(clampf(g, 0.f, 1.f) * 255.f) << 16) |
(uint32_t(clampf(b, 0.f, 1.f) * 255.f) << 8) |
uint32_t(clampf(a, 0.f, 1.f) * 255.f);
}
eve::graphics::Color unpackTint(uint32_t t) {
return eve::graphics::Color(float((t >> 24) & 0xff) / 255.f,
float((t >> 16) & 0xff) / 255.f,
float((t >> 8) & 0xff) / 255.f, float(t & 0xff) / 255.f);
}
} // namespace
void SpriteStackBatch::add(SpriteStack3D *stack) {
if (!stack) throw eve::Exception("SpriteStackBatch.add: null stack");
if (std::find(stacks_.begin(), stacks_.end(), stack) == stacks_.end()) {
stacks_.push_back(stack);
forceRebuild_ = true;
}
}
void SpriteStackBatch::remove(SpriteStack3D *stack) {
auto it = std::find(stacks_.begin(), stacks_.end(), stack);
if (it != stacks_.end()) {
stacks_.erase(it);
forceRebuild_ = true;
}
}
void SpriteStackBatch::clear() {
stacks_.clear();
groups_.clear();
forceRebuild_ = true;
}
int SpriteStackBatch::getStackCount() const { return int(stacks_.size()); }
void SpriteStackBatch::ensureShader(graphics::Graphics *gfx) {
if (shader_) return;
auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
shader_ = gfx->newHairShaderFromSpv(vert, frag);
if (!shader_ || !shader_->gpuHandle)
throw eve::Exception("SpriteStackBatch.render: failed to create slice shader");
shader_->declareVec4("uvRect");
shader_->declareFloat("alphaCutoff");
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
shader_->sendFloat("alphaCutoff", 0.05f);
}
void SpriteStackBatch::render(graphics::Graphics *gfx, graphics::Camera3D *camera) {
if (!gfx) throw eve::Exception("SpriteStackBatch.render: null graphics");
if (stacks_.empty()) return;
if (!camera) camera = findActiveCamera3D();
if (!camera) return;
ensureShader(gfx);
if (!shader_) return;
auto cd = camera->data();
const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
const float fovRad = cd->fovYDeg * 0.017453292519943295f;
const glm::mat4 projM =
eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
gfx->setMesh3DViewProj(projM * viewM);
gfx->setMesh3DView(viewM);
gfx->setMesh3DClip(cd->nearZ, cd->farZ);
gfx->setMesh3DCameraPos(eye);
gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
shader_->sendFloat("alphaCutoff", 0.05f);
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f); // UVs are baked into the mesh
struct GroupBuild {
GroupKey key{};
struct ColoredSlice {
SpriteStack3D::SliceDraw draw;
eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
};
std::vector<ColoredSlice> slices;
uint64_t stamp = 0;
float nearest = 1e30f;
eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
};
std::unordered_map<GroupKey, GroupBuild, GroupKeyHash> builds;
for (SpriteStack3D *st : stacks_) {
if (!st || !st->visible_) continue;
std::vector<SpriteStack3D::SliceDraw> tmp;
SpriteStack3D::collectSlices(*st, eye, tmp);
const uint64_t stamp = st->getVersion();
std::vector<GroupBuild::ColoredSlice> colored;
const eve::graphics::Color baseColor(st->tintR_, st->tintG_, st->tintB_, st->tintA_);
for (const auto &s : tmp) colored.push_back({s, baseColor});
if (st->shadowEnabled_ && st->shadowLightY_ < -1e-4f) {
const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, st->shadowOpacity_);
for (const auto &s : tmp)
colored.push_back(
{SpriteStack3D::makeShadowDraw(*st, s, eye), shadowColor});
}
if (st->outlineWidth_ > 0.f) {
const eve::graphics::Color outlineColor(st->outlineR_, st->outlineG_, st->outlineB_,
1.f);
for (const auto &s : tmp)
colored.push_back(
{SpriteStack3D::makeOutlineDraw(*st, s, eye), outlineColor});
}
for (const auto &cs : colored) {
GroupKey key{cs.draw.texture,
packTint(cs.color.r, cs.color.g, cs.color.b, cs.color.a)};
GroupBuild &b = builds[key];
b.key = key;
b.color = unpackTint(key.tint);
b.slices.push_back(cs);
b.stamp = std::max(b.stamp, stamp);
b.nearest = std::min(b.nearest, cs.draw.distSq);
}
}
if (builds.empty()) return;
// Draw far groups first; inside each group slices are baked far-to-near.
std::vector<GroupBuild *> order;
order.reserve(builds.size());
for (auto &kv : builds) order.push_back(&kv.second);
std::sort(order.begin(), order.end(),
[](const GroupBuild *a, const GroupBuild *b) { return a->nearest > b->nearest; });
const glm::vec3 corners[4] = {{-0.5f, 0.5f, 0.f}, {0.5f, 0.5f, 0.f},
{0.5f, -0.5f, 0.f}, {-0.5f, -0.5f, 0.f}};
const glm::vec2 quadUVs[4] = {{0.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}, {0.f, 1.f}};
std::vector<float> pos, nrm, uv;
std::vector<uint32_t> idx;
for (GroupBuild *gb : order) {
Group &g = groups_[gb->key];
std::sort(gb->slices.begin(), gb->slices.end(),
[](const GroupBuild::ColoredSlice &a, const GroupBuild::ColoredSlice &b) {
return a.draw.distSq > b.draw.distSq;
});
const int sliceCount = int(gb->slices.size());
const int vc = sliceCount * 4;
const int ic = sliceCount * 6;
pos.resize(size_t(vc) * 3);
nrm.resize(size_t(vc) * 3);
uv.resize(size_t(vc) * 2);
idx.resize(size_t(ic));
for (int si = 0; si < sliceCount; ++si) {
const SpriteStack3D::SliceDraw &s = gb->slices[size_t(si)].draw;
for (int c = 0; c < 4; ++c) {
const glm::vec4 p = s.model * glm::vec4(corners[c], 1.f);
const size_t vi = size_t(si * 4 + c);
pos[vi * 3 + 0] = p.x;
pos[vi * 3 + 1] = p.y;
pos[vi * 3 + 2] = p.z;
nrm[vi * 3 + 0] = 0.f;
nrm[vi * 3 + 1] = 1.f;
nrm[vi * 3 + 2] = 0.f;
uv[vi * 2 + 0] = s.uv.x + quadUVs[c].x * (s.uv.z - s.uv.x);
uv[vi * 2 + 1] = s.uv.y + quadUVs[c].y * (s.uv.w - s.uv.y);
}
const uint32_t base = uint32_t(si * 4);
const size_t di = size_t(si) * 6;
idx[di + 0] = base;
idx[di + 1] = base + 1;
idx[di + 2] = base + 2;
idx[di + 3] = base;
idx[di + 4] = base + 2;
idx[di + 5] = base + 3;
}
if (forceRebuild_ || !g.mesh) {
if (g.mesh) {
gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(),
ic);
} else {
g.mesh =
gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
}
g.vertexCapacity = vc;
g.indexCapacity = ic;
} else if (g.stamp != gb->stamp) {
gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
}
g.stamp = gb->stamp;
gfx->drawMeshShader(g.mesh, glm::mat4(1.f), gb->key.texture, gb->color, shader_);
}
forceRebuild_ = false;
}
// ---------------------------------------------------------------------------
// Module
// ---------------------------------------------------------------------------
Module_IMPL(SpriteStack, new SpriteStack());
SpriteStack3D *SpriteStack::newStack(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newStack: null graphics");
return new SpriteStack3D();
}
SpriteStackBatch *SpriteStack::newBatch(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newBatch: null graphics");
return new SpriteStackBatch();
}
std::vector<image::ImageData *> SpriteStack::slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions opt;
opt.layerCount = layerCount;
opt.imageW = imageW;
opt.imageH = imageH;
opt.axis = axis;
opt.thickness = thickness;
return slicePrimitiveToLayers(kind, opt);
}
std::vector<image::ImageData *> SpriteStack::sliceModel(model3d::ModelData *model, int layerCount,
int imageW, int imageH,
const std::string &axis, float thickness) {
SliceOptions opt;
opt.layerCount = layerCount;
opt.imageW = imageW;
opt.imageH = imageH;
opt.axis = axis;
opt.thickness = thickness;
return sliceModelToLayers(model, opt);
}
void SpriteStack::expose(ssq::Table &table) {
auto cls = table.addClass(name, SpriteStack::create, false);
expose(cls);
auto stack = table.addClass<SpriteStack3D>(
"SpriteStack3D",
std::function<SpriteStack3D *()>([]() -> SpriteStack3D * { return nullptr; }), true);
stack.addFunc("setLayerCount", &SpriteStack3D::setLayerCount);
stack.addFunc("getLayerCount", &SpriteStack3D::getLayerCount);
stack.addFunc("setLayerTexture", &SpriteStack3D::setLayerTexture);
stack.addFunc("getLayerTexture", &SpriteStack3D::getLayerTexture);
stack.addFunc("setLayerImage", &SpriteStack3D::setLayerImage);
stack.addFunc("setLayerFile", &SpriteStack3D::setLayerFile);
stack.addFunc("setLayersFromAtlas", &SpriteStack3D::setLayersFromAtlas);
stack.addFunc("setThickness", &SpriteStack3D::setThickness);
stack.addFunc("getThickness", &SpriteStack3D::getThickness);
stack.addFunc("setSize", &SpriteStack3D::setSize);
stack.addFunc("getWidth", &SpriteStack3D::getWidth);
stack.addFunc("getHeight", &SpriteStack3D::getHeight);
stack.addFunc("setPosition", &SpriteStack3D::setPosition);
stack.addFunc("setYaw", &SpriteStack3D::setYaw);
stack.addFunc("setTint", &SpriteStack3D::setTint);
stack.addFunc("setAlphaCutoff", &SpriteStack3D::setAlphaCutoff);
stack.addFunc("setVisible", &SpriteStack3D::setVisible);
stack.addFunc("getVisible", &SpriteStack3D::getVisible);
stack.addFunc("setMode", &SpriteStack3D::setMode);
stack.addFunc("getMode", &SpriteStack3D::getMode);
stack.addFunc("setShadowEnabled", &SpriteStack3D::setShadowEnabled);
stack.addFunc("getShadowEnabled", &SpriteStack3D::getShadowEnabled);
stack.addFunc("setShadowOpacity", &SpriteStack3D::setShadowOpacity);
stack.addFunc("setShadowLight", &SpriteStack3D::setShadowLight);
stack.addFunc("setShadowPlaneY", &SpriteStack3D::setShadowPlaneY);
stack.addFunc("setOutline", &SpriteStack3D::setOutline);
stack.addFunc("getOutlineWidth", &SpriteStack3D::getOutlineWidth);
stack.addFunc("setOutlineColor", &SpriteStack3D::setOutlineColor);
stack.addFunc("setGbufferEnabled", &SpriteStack3D::setGbufferEnabled);
stack.addFunc("getGbufferEnabled", &SpriteStack3D::getGbufferEnabled);
stack.addFunc("setCastShadow", &SpriteStack3D::setCastShadow);
stack.addFunc("getCastShadow", &SpriteStack3D::getCastShadow);
stack.addFunc(
"render",
std::function<void(SpriteStack3D *, graphics::Graphics *)>([](SpriteStack3D *self,
graphics::Graphics *gfx) {
if (self) self->render(gfx);
}));
stack.addFunc("renderWithCamera", &SpriteStack3D::render);
auto batch = table.addClass<SpriteStackBatch>(
"SpriteStackBatch",
std::function<SpriteStackBatch *()>([]() -> SpriteStackBatch * { return nullptr; }), true);
batch.addFunc("add", &SpriteStackBatch::add);
batch.addFunc("remove", &SpriteStackBatch::remove);
batch.addFunc("clear", &SpriteStackBatch::clear);
batch.addFunc("getStackCount", &SpriteStackBatch::getStackCount);
batch.addFunc(
"render",
std::function<void(SpriteStackBatch *, graphics::Graphics *)>([](SpriteStackBatch *self,
graphics::Graphics *gfx) {
if (self) self->render(gfx);
}));
batch.addFunc("renderWithCamera", &SpriteStackBatch::render);
}
void SpriteStack::expose(ssq::Class &cls) {
cls.addFunc("getName", &SpriteStack::getName);
cls.addFunc("newStack", &SpriteStack::newStack);
cls.addFunc("newBatch", &SpriteStack::newBatch);
cls.addFunc("slicePrimitive", &SpriteStack::slicePrimitive);
cls.addFunc("sliceModel", &SpriteStack::sliceModel);
}
// Legacy 3D renderer removed: SpriteStack is now a pure 2D technique. Keep the
// original implementation excluded temporarily so the slicer above remains a
// reviewable, behavior-preserving move while the 2D renderer lives in its own TU.
} // namespace eve::spritestack
+50 -229
View File
@@ -3,38 +3,20 @@
#include "common/Module.h"
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
namespace eve::graphics {
class Camera3D;
class Camera2D;
class Graphics;
class Mesh;
class Shader;
class Texture;
} // namespace eve::graphics
namespace eve::image {
class ImageData;
} // namespace eve::image
namespace eve::model3d {
class ModelData;
} // namespace eve::model3d
}
namespace eve::image { class ImageData; }
namespace eve::model3d { class ModelData; }
namespace eve::spritestack {
/**
* @brief CPU inputs for slicing a triangle mesh into sprite-stack layers.
*
* Positions are required (vertexCount * 3). Normals / per-vertex colors are
* optional; colors (0..1 RGB) are averaged per triangle, shaded by the face
* normal against the slice view direction, and multiplied by the tint.
*/
/** @brief CPU triangle-mesh input used to bake horizontal RGBA sprite slices. */
struct SliceInput {
const float *posXYZ = nullptr;
const float *nrmXYZ = nullptr;
@@ -44,270 +26,109 @@ struct SliceInput {
int indexCount = 0;
};
/**
* @brief Slicer options. The mesh is cut into `layerCount` thin slabs along `axis`
* ("x" | "y" | "z"); each slab is orthographically projected into an RGBA
* image. Slab thickness defaults to AABB extent / layerCount when 0.
*/
/** @brief Options for baking a mesh into equal-sized RGBA sprite layers. */
struct SliceOptions {
int layerCount = 16;
int imageW = 128;
int imageH = 128;
std::string axis = "z"; // "x" | "y" | "z"
float thickness = 0.f; // world units per layer; 0 = auto from AABB
float padding = 0.04f; // margin around the model in the image plane
bool shade = true; // N*view shading per triangle
float tintR = 1.f;
float tintG = 1.f;
float tintB = 1.f;
std::string axis = "y";
float thickness = 0.f;
float padding = 0.04f;
bool shade = true;
float tintR = 1.f, tintG = 1.f, tintB = 1.f;
};
/**
* @brief Slice a triangle mesh into `layerCount` RGBA8 layer images (caller owns the
* returned ImageData). Classic sprite stacking: vertical "bread slices" along
* the chosen axis, later rendered as camera-facing billboards.
*/
std::vector<image::ImageData *> sliceMeshToLayers(const SliceInput &input,
const SliceOptions &options);
/** @brief Slice every mesh of a decoded model (assimp scene) with the same options. */
std::vector<image::ImageData *> sliceModelToLayers(model3d::ModelData *model,
const SliceOptions &options);
std::vector<image::ImageData *> sliceMeshToLayers(const SliceInput &, const SliceOptions &);
std::vector<image::ImageData *> sliceModelToLayers(model3d::ModelData *, const SliceOptions &);
std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &, const SliceOptions &);
/**
* @brief Build a procedural CPU mesh ("box" | "cylinder" | "sphere" | "cone") and
* slice it — a self-contained asset-free path for tests / demos.
*/
std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &kind,
const SliceOptions &options);
/**
* @brief A renderable pseudo-3D sprite stack: a column of layer textures drawn as
* alpha-blended slices inside the 3D forward pass (call render() after
* gfx.render3D() / RenderSystem3D::render and before present).
* @brief Pure-2D sprite stack made from horizontal RGBA cross-sections.
*
* Modes:
* "vertical" (default) — bread-slice billboards that always face the
* camera; yaw rotates the stack's depth layout around Y.
* "horizontal" — slices are horizontal quads (top-down layers); a 3/4
* camera sees true volume with yaw parallax.
*
* Textures are owned by Graphics; this object only holds pointers. Create via
* eve.SpriteStack().newStack(gfx); the script VM owns the object.
* Layers are drawn bottom-to-top through the normal 2D textured-quad path. Each
* higher layer is offset upward by `thickness` 2D units. Rotation is applied in
* the 2D plane; no Camera3D, depth buffer, G-buffer, mesh, or 3D pass is used.
*/
class SpriteStack3D {
class SpriteStack2D {
public:
SpriteStack3D() = default;
~SpriteStack3D();
SpriteStack3D(const SpriteStack3D &) = delete;
SpriteStack3D &operator=(const SpriteStack3D &) = delete;
/** @brief One slice instance with its baked world transform and layer UV rect. */
struct SliceDraw {
glm::mat4 model{1.f};
glm::vec4 uv{0.f, 0.f, 1.f, 1.f};
float distSq = 0.f;
graphics::Texture *texture = nullptr;
};
SpriteStack2D() = default;
SpriteStack2D(const SpriteStack2D &) = delete;
SpriteStack2D &operator=(const SpriteStack2D &) = delete;
void setLayerCount(int count);
int getLayerCount() const;
/** @brief Null texture clears the slot (that layer is skipped). */
void setLayerTexture(graphics::Texture *texture, int index);
graphics::Texture *getLayerTexture(int index) const;
/** @brief Upload RGBA8 ImageData layers (convenience around setLayerTexture). */
void setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index);
/** @brief Upload each path via Graphics::newTextureFromFile (reloads in place). */
void setLayerFile(graphics::Graphics *gfx, const std::string &path, int index);
/**
* @brief Split one horizontal atlas strip into `layerCount` layers (layer i =
* columns [i/count..(i+1)/count)). The strip stays one GPU texture; slices
* sample their cell through per-layer UV rects.
*/
void setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount);
/** @brief World-space spacing between consecutive slices. */
void setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas, int layerCount);
/** @brief Upward 2D offset between adjacent layers. */
void setThickness(float thickness);
float getThickness() const;
/** @brief Quad size for every slice (world units). Default 1 x 1. */
/** @brief Display size of every layer in 2D units. */
void setSize(float width, float height);
float getWidth() const;
float getHeight() const;
void setPosition(float x, float y, float z);
void setYaw(float radians);
void setPosition(float x, float y);
/** @brief Rotate every slice around its common 2D center, in degrees. */
void setRotation(float degrees);
void setTint(float r, float g, float b, float a = 1.f);
void setAlphaCutoff(float cutoff);
void setVisible(bool visible);
bool getVisible() const;
void setMode(const std::string &mode); // "vertical" | "horizontal"
std::string getMode() const;
/** @brief Pseudo-3D projected contact shadow (soft dark silhouette on the ground). */
/** @brief Draw a cheap 2D contact shadow behind the stack. */
void setShadowEnabled(bool enabled);
bool getShadowEnabled() const;
void setShadowOpacity(float opacity);
void setShadowLight(float dx, float dy, float dz);
void setShadowPlaneY(float y);
/** @brief Stylized rim outline: an expanded dark silhouette behind every slice. */
void setShadowOffset(float x, float y);
/** @brief Draw enlarged dark copies behind the layers. */
void setOutline(float width, float r = 0.f, float g = 0.f, float b = 0.f);
float getOutlineWidth() const;
void setOutlineColor(float r, float g, float b);
/**
* @brief Contribute this stack to the G-buffer (via RenderSystem3D's extra-drawer
* hook) so post-processing that reads depth/normal (AO, outline) recognizes
* the slice silhouettes instead of empty space.
*/
void setGbufferEnabled(bool enabled);
bool getGbufferEnabled() const;
/**
* @brief Cast real CSM shadows: the stack's slices are drawn into the cascaded
* shadow map through the alpha-cutout shadow pipeline, so the silhouette
* (not a solid quad) lands on receiving geometry. Requires a directional
* light with castShadow, or the legacy directional light fallback.
*/
void setCastShadow(bool cast);
bool getCastShadow() const;
/** @brief Monotonic revision counter; SpriteStackBatch uses it to detect changes. */
uint64_t getVersion() const { return version_; }
/**
* @brief Draw all slices into the currently open 3D scene pass (Vulkan). Uses the
* active Camera3D when `camera` is null. Slices are sorted back-to-front
* and depth-tested against the scene (depth writes stay off).
*/
void render(graphics::Graphics *gfx, graphics::Camera3D *camera = nullptr) const;
/** @brief Queue the stack into Graphics' normal 2D renderer. */
void render(graphics::Graphics *gfx) const;
void renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
private:
void ensureResources(graphics::Graphics *gfx) const;
void bumpVersion() { ++version_; }
friend class SpriteStackBatch;
static void collectSlices(const SpriteStack3D &stack, const glm::vec3 &eye,
std::vector<SliceDraw> &out);
static SliceDraw makeShadowDraw(const SpriteStack3D &stack, const SliceDraw &s,
const glm::vec3 &eye);
static SliceDraw makeOutlineDraw(const SpriteStack3D &stack, const SliceDraw &s,
const glm::vec3 &eye);
static void drawGbufferStacks(graphics::Graphics &gfx, const glm::mat4 &viewProj,
float eyeX, float eyeY, float eyeZ, float nearZ, float farZ);
static void registerGbufferDrawer();
static std::vector<SpriteStack3D *> &gbufferStacks();
static void drawShadowCasterStacks(graphics::Graphics &gfx, const glm::mat4 &lightVP,
float eyeX, float eyeY, float eyeZ);
static void registerShadowDrawer();
static std::vector<SpriteStack3D *> &shadowCasterStacks();
struct Layer {
graphics::Texture *texture = nullptr;
glm::vec4 uv{0.f, 0.f, 1.f, 1.f}; // u0, v0, u1, v1
float u0 = 0.f, v0 = 0.f, u1 = 1.f, v1 = 1.f;
};
void draw(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
void bumpVersion() { ++version_; }
std::vector<Layer> layers_;
int layerCount_ = 0;
float thickness_ = 0.1f;
float width_ = 1.f;
float height_ = 1.f;
float x_ = 0.f, y_ = 0.f, z_ = 0.f;
float yaw_ = 0.f;
float thickness_ = 1.f, width_ = 64.f, height_ = 64.f;
float x_ = 0.f, y_ = 0.f, rotation_ = 0.f;
float tintR_ = 1.f, tintG_ = 1.f, tintB_ = 1.f, tintA_ = 1.f;
float alphaCutoff_ = 0.05f;
bool visible_ = true;
std::string mode_ = "vertical";
bool shadowEnabled_ = false;
float shadowOpacity_ = 0.32f;
float shadowLightX_ = 0.35f, shadowLightY_ = -1.f, shadowLightZ_ = 0.25f;
float shadowPlaneY_ = 0.f;
float outlineWidth_ = 0.f;
float outlineR_ = 0.f, outlineG_ = 0.f, outlineB_ = 0.f;
bool gbufferEnabled_ = false;
bool castShadow_ = false;
bool visible_ = true, shadowEnabled_ = false;
float shadowOpacity_ = 0.3f, shadowOffsetX_ = 5.f, shadowOffsetY_ = 4.f;
float outlineWidth_ = 0.f, outlineR_ = 0.f, outlineG_ = 0.f, outlineB_ = 0.f;
uint64_t version_ = 1;
// Lazy GPU resources (owned by Graphics).
mutable graphics::Shader *shader_ = nullptr;
mutable graphics::Mesh *quad_ = nullptr;
};
/**
* @brief Multi-stack batching: draws every visible slice of the registered stacks as
* ONE draw call per (texture, tint) group (all slice transforms and UV rects
* are baked into a shared mesh, uploaded in place via Graphics::updateMeshVertices).
*
* The combined mesh is rebuilt only when a registered stack changes (revision
* counter); per-frame animated stacks should keep using SpriteStack3D::render.
* Shadows / outlines are per-stack features and are not included in batches.
*/
/** @brief Collection of 2D stacks; Graphics performs texture batching. */
class SpriteStackBatch {
public:
SpriteStackBatch() = default;
~SpriteStackBatch() = default;
SpriteStackBatch(const SpriteStackBatch &) = delete;
SpriteStackBatch &operator=(const SpriteStackBatch &) = delete;
void add(SpriteStack3D *stack);
void remove(SpriteStack3D *stack);
void add(SpriteStack2D *stack);
void remove(SpriteStack2D *stack);
void clear();
int getStackCount() const;
void render(graphics::Graphics *gfx, graphics::Camera3D *camera = nullptr);
void render(graphics::Graphics *gfx) const;
void renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
private:
struct GroupKey {
graphics::Texture *texture = nullptr;
uint32_t tint = 0; // packed RGBA
bool operator==(const GroupKey &o) const {
return texture == o.texture && tint == o.tint;
}
};
struct GroupKeyHash {
size_t operator()(const GroupKey &k) const {
return std::hash<void *>()(k.texture) ^ (size_t(k.tint) * 0x9e3779b9u);
}
};
struct Group {
graphics::Mesh *mesh = nullptr;
uint64_t stamp = 0;
int vertexCapacity = 0;
int indexCapacity = 0;
};
void ensureShader(graphics::Graphics *gfx);
std::vector<SpriteStack3D *> stacks_;
std::unordered_map<GroupKey, Group, GroupKeyHash> groups_;
bool forceRebuild_ = true; // add/remove/clear invalidate cached group meshes
mutable graphics::Shader *shader_ = nullptr;
std::vector<SpriteStack2D *> stacks_;
};
/**
* @brief SpriteStack module: CPU slicing + SpriteStack3D factory.
*
* Script: `spritestack <- eve.SpriteStack();`
*/
/** @brief SpriteStack module: horizontal slice baking plus pure-2D rendering. */
class SpriteStack : public Module {
public:
Module_REG(SpriteStack);
SpriteStack() = default;
~SpriteStack() override = default;
/** @brief New empty stack (caller/VM owns; set layers before rendering). */
SpriteStack3D *newStack(graphics::Graphics *gfx);
/** @brief New empty batch (caller/VM owns). */
SpriteStack2D *newStack(graphics::Graphics *gfx);
SpriteStackBatch *newBatch(graphics::Graphics *gfx);
std::vector<image::ImageData *> slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis, float thickness);
+279
View File
@@ -0,0 +1,279 @@
#include "spritestack/SpriteStack.h"
#include "common/Exception.h"
#include "graphics/Color.h"
#include "graphics/Graphics.h"
#include "graphics/RenderSystem.h"
#include "image/ImageData.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <simplesquirrel/simplesquirrel.hpp>
namespace eve::spritestack {
namespace {
float clamp01(float value) { return std::max(0.f, std::min(1.f, value)); }
}
void SpriteStack2D::setLayerCount(int count) {
if (count < 0) throw eve::Exception("SpriteStack2D.setLayerCount: count must be >= 0");
layerCount_ = count;
layers_.assign(size_t(count), Layer{});
bumpVersion();
}
int SpriteStack2D::getLayerCount() const { return layerCount_; }
void SpriteStack2D::setLayerTexture(graphics::Texture *texture, int index) {
if (index < 0 || index >= layerCount_)
throw eve::Exception("SpriteStack2D.setLayerTexture: index %d out of range [0,%d)", index,
layerCount_);
layers_[size_t(index)] = Layer{texture, 0.f, 0.f, 1.f, 1.f};
bumpVersion();
}
graphics::Texture *SpriteStack2D::getLayerTexture(int index) const {
if (index < 0 || index >= layerCount_) return nullptr;
return layers_[size_t(index)].texture;
}
void SpriteStack2D::setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayerImage: null graphics");
if (!img) throw eve::Exception("SpriteStack2D.setLayerImage: null ImageData");
setLayerTexture(gfx->newTextureFromImageData(img, false, false), index);
}
void SpriteStack2D::setLayerFile(graphics::Graphics *gfx, const std::string &path, int index) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayerFile: null graphics");
setLayerTexture(gfx->newTextureFromFile(path), index);
}
void SpriteStack2D::setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: null graphics");
if (!atlas) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: null atlas");
if (layerCount <= 0) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: count must be > 0");
setLayerCount(layerCount);
for (int i = 0; i < layerCount; ++i) {
const float u0 = float(i) / float(layerCount);
const float u1 = float(i + 1) / float(layerCount);
layers_[size_t(i)] = Layer{atlas, u0, 0.f, u1, 1.f};
}
bumpVersion();
}
void SpriteStack2D::setThickness(float thickness) {
if (thickness <= 0.f) throw eve::Exception("SpriteStack2D.setThickness: must be > 0");
thickness_ = thickness;
bumpVersion();
}
float SpriteStack2D::getThickness() const { return thickness_; }
void SpriteStack2D::setSize(float width, float height) {
if (width <= 0.f || height <= 0.f) throw eve::Exception("SpriteStack2D.setSize: size must be > 0");
width_ = width;
height_ = height;
bumpVersion();
}
float SpriteStack2D::getWidth() const { return width_; }
float SpriteStack2D::getHeight() const { return height_; }
void SpriteStack2D::setPosition(float x, float y) {
x_ = x;
y_ = y;
bumpVersion();
}
void SpriteStack2D::setRotation(float degrees) {
rotation_ = degrees;
bumpVersion();
}
void SpriteStack2D::setTint(float r, float g, float b, float a) {
tintR_ = clamp01(r);
tintG_ = clamp01(g);
tintB_ = clamp01(b);
tintA_ = clamp01(a);
bumpVersion();
}
void SpriteStack2D::setVisible(bool visible) { visible_ = visible; }
bool SpriteStack2D::getVisible() const { return visible_; }
void SpriteStack2D::setShadowEnabled(bool enabled) { shadowEnabled_ = enabled; }
bool SpriteStack2D::getShadowEnabled() const { return shadowEnabled_; }
void SpriteStack2D::setShadowOpacity(float opacity) { shadowOpacity_ = clamp01(opacity); }
void SpriteStack2D::setShadowOffset(float x, float y) {
shadowOffsetX_ = x;
shadowOffsetY_ = y;
}
void SpriteStack2D::setOutline(float width, float r, float g, float b) {
outlineWidth_ = std::max(0.f, width);
setOutlineColor(r, g, b);
}
float SpriteStack2D::getOutlineWidth() const { return outlineWidth_; }
void SpriteStack2D::setOutlineColor(float r, float g, float b) {
outlineR_ = clamp01(r);
outlineG_ = clamp01(g);
outlineB_ = clamp01(b);
}
void SpriteStack2D::draw(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!visible_ || layerCount_ <= 0) return;
float cx = x_;
float baseY = y_;
float scale = 1.f;
if (camera) {
const float viewW = float(gfx->getWidth());
const float viewH = float(gfx->getHeight());
cx = camera->worldToScreenX(x_, y_, viewW, viewH);
baseY = camera->worldToScreenY(x_, y_, viewW, viewH);
scale = camera->getZoom();
}
const auto queueLayer = [&](const Layer &layer, float x, float y, float w, float h,
const graphics::Color &tint) {
gfx->drawTexturedRectShaderUVRotated(layer.texture, nullptr, x, y, w, h, rotation_,
layer.u0, layer.v0, layer.u1, layer.v1, tint);
};
if (shadowEnabled_) {
const graphics::Color shadow(0.f, 0.f, 0.f, shadowOpacity_ * tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx + shadowOffsetX_ * scale,
baseY + shadowOffsetY_ * scale - float(i) * thickness_ * scale,
width_ * scale, height_ * scale, shadow);
}
}
if (outlineWidth_ > 0.f) {
const graphics::Color outline(outlineR_, outlineG_, outlineB_, tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx, baseY - float(i) * thickness_ * scale,
width_ * scale + outlineWidth_ * 2.f,
height_ * scale + outlineWidth_ * 2.f, outline);
}
}
const graphics::Color tint(tintR_, tintG_, tintB_, tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx, baseY - float(i) * thickness_ * scale,
width_ * scale, height_ * scale, tint);
}
}
void SpriteStack2D::render(graphics::Graphics *gfx) const {
if (!gfx) throw eve::Exception("SpriteStack2D.render: null graphics");
draw(gfx, nullptr);
}
void SpriteStack2D::renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!gfx) throw eve::Exception("SpriteStack2D.renderWithCamera: null graphics");
draw(gfx, camera);
}
void SpriteStackBatch::add(SpriteStack2D *stack) {
if (!stack) throw eve::Exception("SpriteStackBatch.add: null stack");
if (std::find(stacks_.begin(), stacks_.end(), stack) == stacks_.end()) stacks_.push_back(stack);
}
void SpriteStackBatch::remove(SpriteStack2D *stack) {
stacks_.erase(std::remove(stacks_.begin(), stacks_.end(), stack), stacks_.end());
}
void SpriteStackBatch::clear() { stacks_.clear(); }
int SpriteStackBatch::getStackCount() const { return int(stacks_.size()); }
void SpriteStackBatch::render(graphics::Graphics *gfx) const {
if (!gfx) throw eve::Exception("SpriteStackBatch.render: null graphics");
renderWithCamera(gfx, nullptr);
}
void SpriteStackBatch::renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!gfx) throw eve::Exception("SpriteStackBatch.renderWithCamera: null graphics");
for (SpriteStack2D *stack : stacks_) if (stack) stack->draw(gfx, camera);
}
Module_IMPL(SpriteStack, new SpriteStack());
SpriteStack2D *SpriteStack::newStack(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newStack: null graphics");
return new SpriteStack2D();
}
SpriteStackBatch *SpriteStack::newBatch(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newBatch: null graphics");
return new SpriteStackBatch();
}
std::vector<image::ImageData *> SpriteStack::slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions options;
options.layerCount = layerCount;
options.imageW = imageW;
options.imageH = imageH;
options.axis = axis;
options.thickness = thickness;
return slicePrimitiveToLayers(kind, options);
}
std::vector<image::ImageData *> SpriteStack::sliceModel(model3d::ModelData *model, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions options;
options.layerCount = layerCount;
options.imageW = imageW;
options.imageH = imageH;
options.axis = axis;
options.thickness = thickness;
return sliceModelToLayers(model, options);
}
void SpriteStack::expose(ssq::Table &table) {
auto cls = table.addClass(name, SpriteStack::create, false);
expose(cls);
auto stack = table.addClass<SpriteStack2D>(
"SpriteStack2D", std::function<SpriteStack2D *()>([]() { return nullptr; }), true);
stack.addFunc("setLayerCount", &SpriteStack2D::setLayerCount);
stack.addFunc("getLayerCount", &SpriteStack2D::getLayerCount);
stack.addFunc("setLayerTexture", &SpriteStack2D::setLayerTexture);
stack.addFunc("getLayerTexture", &SpriteStack2D::getLayerTexture);
stack.addFunc("setLayerImage", &SpriteStack2D::setLayerImage);
stack.addFunc("setLayerFile", &SpriteStack2D::setLayerFile);
stack.addFunc("setLayersFromAtlas", &SpriteStack2D::setLayersFromAtlas);
stack.addFunc("setThickness", &SpriteStack2D::setThickness);
stack.addFunc("getThickness", &SpriteStack2D::getThickness);
stack.addFunc("setSize", &SpriteStack2D::setSize);
stack.addFunc("getWidth", &SpriteStack2D::getWidth);
stack.addFunc("getHeight", &SpriteStack2D::getHeight);
stack.addFunc("setPosition", &SpriteStack2D::setPosition);
stack.addFunc("setRotation", &SpriteStack2D::setRotation);
stack.addFunc("setTint", &SpriteStack2D::setTint);
stack.addFunc("setVisible", &SpriteStack2D::setVisible);
stack.addFunc("getVisible", &SpriteStack2D::getVisible);
stack.addFunc("setShadowEnabled", &SpriteStack2D::setShadowEnabled);
stack.addFunc("getShadowEnabled", &SpriteStack2D::getShadowEnabled);
stack.addFunc("setShadowOpacity", &SpriteStack2D::setShadowOpacity);
stack.addFunc("setShadowOffset", &SpriteStack2D::setShadowOffset);
stack.addFunc("setOutline", &SpriteStack2D::setOutline);
stack.addFunc("getOutlineWidth", &SpriteStack2D::getOutlineWidth);
stack.addFunc("setOutlineColor", &SpriteStack2D::setOutlineColor);
stack.addFunc("render", &SpriteStack2D::render);
stack.addFunc("renderWithCamera", &SpriteStack2D::renderWithCamera);
auto batch = table.addClass<SpriteStackBatch>(
"SpriteStackBatch", std::function<SpriteStackBatch *()>([]() { return nullptr; }), true);
batch.addFunc("add", &SpriteStackBatch::add);
batch.addFunc("remove", &SpriteStackBatch::remove);
batch.addFunc("clear", &SpriteStackBatch::clear);
batch.addFunc("getStackCount", &SpriteStackBatch::getStackCount);
batch.addFunc("render", &SpriteStackBatch::render);
batch.addFunc("renderWithCamera", &SpriteStackBatch::renderWithCamera);
}
void SpriteStack::expose(ssq::Class &cls) {
cls.addFunc("getName", &SpriteStack::getName);
cls.addFunc("newStack", &SpriteStack::newStack);
cls.addFunc("newBatch", &SpriteStack::newBatch);
cls.addFunc("slicePrimitive", &SpriteStack::slicePrimitive);
cls.addFunc("sliceModel", &SpriteStack::sliceModel);
}
} // namespace eve::spritestack
@@ -1,21 +0,0 @@
#version 450
// Sprite-stack slice fragment shader: unlit textured alpha cutout.
// Push constants: data[0] = alphaCutoff.
layout(location = 1) in vec2 vUV;
layout(location = 2) in vec4 vTint;
layout(set = 0, binding = 1) uniform sampler2D albedo;
layout(push_constant) uniform Externals {
vec4 uvRect;
float data[28];
} u;
layout(location = 0) out vec4 outColor;
void main() {
vec4 base = texture(albedo, vUV) * vTint;
if (base.a < u.data[0]) discard;
outColor = base;
}
@@ -1,31 +0,0 @@
#version 450
// Sprite-stack slice vertex shader.
// MeshVertex layout (pos/normal/uv) and Frame UBO prefix match mesh3d, so
// Graphics::drawMeshShader can bind the shared descriptor set.
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV;
layout(set = 0, binding = 0) uniform Frame {
mat4 mvp;
mat4 model;
vec4 lightDirIntensity;
vec4 lightColor;
vec4 tint;
vec4 cameraPos;
} ubo;
layout(push_constant) uniform Externals {
vec4 uvRect;
float data[28];
} u;
layout(location = 1) out vec2 vUV;
layout(location = 2) out vec4 vTint;
void main() {
gl_Position = ubo.mvp * vec4(inPos, 1.0);
vUV = u.uvRect.xy + inUV * (u.uvRect.zw - u.uvRect.xy);
vTint = ubo.tint;
}
@@ -1,44 +0,0 @@
static const uint32_t sprite_stack_frag_spv[] = {
0x07230203, 0x00010000, 0x000d000b, 0x00000030, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0008000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000011, 0x00000015, 0x0000002e,
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, 0x00040005, 0x00000009, 0x65736162, 0x00000000,
0x00040005, 0x0000000d, 0x65626c61, 0x00006f64, 0x00030005, 0x00000011, 0x00565576, 0x00040005,
0x00000015, 0x6e695476, 0x00000074, 0x00050005, 0x0000001f, 0x65747845, 0x6c616e72, 0x00000073,
0x00050006, 0x0000001f, 0x00000000, 0x65527675, 0x00007463, 0x00050006, 0x0000001f, 0x00000001,
0x61746164, 0x00000000, 0x00030005, 0x00000021, 0x00000075, 0x00050005, 0x0000002e, 0x4374756f,
0x726f6c6f, 0x00000000, 0x00040047, 0x0000000d, 0x00000021, 0x00000001, 0x00040047, 0x0000000d,
0x00000022, 0x00000000, 0x00040047, 0x00000011, 0x0000001e, 0x00000001, 0x00040047, 0x00000015,
0x0000001e, 0x00000002, 0x00040047, 0x0000001e, 0x00000006, 0x00000004, 0x00030047, 0x0000001f,
0x00000002, 0x00050048, 0x0000001f, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001f,
0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x0000002e, 0x0000001e, 0x00000000, 0x00020013,
0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00090019,
0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c, 0x00000000, 0x0000000b, 0x0004003b,
0x0000000c, 0x0000000d, 0x00000000, 0x00040017, 0x0000000f, 0x00000006, 0x00000002, 0x00040020,
0x00000010, 0x00000001, 0x0000000f, 0x0004003b, 0x00000010, 0x00000011, 0x00000001, 0x00040020,
0x00000014, 0x00000001, 0x00000007, 0x0004003b, 0x00000014, 0x00000015, 0x00000001, 0x00040015,
0x00000018, 0x00000020, 0x00000000, 0x0004002b, 0x00000018, 0x00000019, 0x00000003, 0x00040020,
0x0000001a, 0x00000007, 0x00000006, 0x0004002b, 0x00000018, 0x0000001d, 0x0000001c, 0x0004001c,
0x0000001e, 0x00000006, 0x0000001d, 0x0004001e, 0x0000001f, 0x00000007, 0x0000001e, 0x00040020,
0x00000020, 0x00000009, 0x0000001f, 0x0004003b, 0x00000020, 0x00000021, 0x00000009, 0x00040015,
0x00000022, 0x00000020, 0x00000001, 0x0004002b, 0x00000022, 0x00000023, 0x00000001, 0x0004002b,
0x00000022, 0x00000024, 0x00000000, 0x00040020, 0x00000025, 0x00000009, 0x00000006, 0x00020014,
0x00000028, 0x00040020, 0x0000002d, 0x00000003, 0x00000007, 0x0004003b, 0x0000002d, 0x0000002e,
0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005,
0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003d, 0x0000000b, 0x0000000e, 0x0000000d,
0x0004003d, 0x0000000f, 0x00000012, 0x00000011, 0x00050057, 0x00000007, 0x00000013, 0x0000000e,
0x00000012, 0x0004003d, 0x00000007, 0x00000016, 0x00000015, 0x00050085, 0x00000007, 0x00000017,
0x00000013, 0x00000016, 0x0003003e, 0x00000009, 0x00000017, 0x00050041, 0x0000001a, 0x0000001b,
0x00000009, 0x00000019, 0x0004003d, 0x00000006, 0x0000001c, 0x0000001b, 0x00060041, 0x00000025,
0x00000026, 0x00000021, 0x00000023, 0x00000024, 0x0004003d, 0x00000006, 0x00000027, 0x00000026,
0x000500b8, 0x00000028, 0x00000029, 0x0000001c, 0x00000027, 0x000300f7, 0x0000002b, 0x00000000,
0x000400fa, 0x00000029, 0x0000002a, 0x0000002b, 0x000200f8, 0x0000002a, 0x000100fc, 0x000200f8,
0x0000002b, 0x0004003d, 0x00000007, 0x0000002f, 0x00000009, 0x0003003e, 0x0000002e, 0x0000002f,
0x000100fd, 0x00010038,
};
static const size_t sprite_stack_frag_spv_count = 322;
@@ -1,75 +0,0 @@
static const uint32_t sprite_stack_vert_spv[] = {
0x07230203, 0x00010000, 0x000d000b, 0x00000041, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x000b000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025,
0x00000030, 0x0000003b, 0x00000040, 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, 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265,
0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69,
0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006,
0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000000b,
0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000,
0x00040005, 0x00000011, 0x6d617246, 0x00000065, 0x00040006, 0x00000011, 0x00000000, 0x0070766d,
0x00050006, 0x00000011, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x00000011, 0x00000002,
0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x00000011, 0x00000003,
0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x00000011, 0x00000004, 0x746e6974, 0x00000000,
0x00060006, 0x00000011, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00030005, 0x00000013,
0x006f6275, 0x00040005, 0x00000019, 0x6f506e69, 0x00000073, 0x00030005, 0x00000025, 0x00565576,
0x00050005, 0x00000028, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, 0x00000028, 0x00000000,
0x65527675, 0x00007463, 0x00050006, 0x00000028, 0x00000001, 0x61746164, 0x00000000, 0x00030005,
0x0000002a, 0x00000075, 0x00040005, 0x00000030, 0x56556e69, 0x00000000, 0x00040005, 0x0000003b,
0x6e695476, 0x00000074, 0x00050005, 0x00000040, 0x6f4e6e69, 0x6c616d72, 0x00000000, 0x00030047,
0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048,
0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, 0x0000000b,
0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011,
0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011, 0x00000000,
0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, 0x00000023, 0x00000000, 0x00040048,
0x00000011, 0x00000001, 0x00000005, 0x00050048, 0x00000011, 0x00000001, 0x00000007, 0x00000010,
0x00050048, 0x00000011, 0x00000001, 0x00000023, 0x00000040, 0x00050048, 0x00000011, 0x00000002,
0x00000023, 0x00000080, 0x00050048, 0x00000011, 0x00000003, 0x00000023, 0x00000090, 0x00050048,
0x00000011, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, 0x00000011, 0x00000005, 0x00000023,
0x000000b0, 0x00040047, 0x00000013, 0x00000021, 0x00000000, 0x00040047, 0x00000013, 0x00000022,
0x00000000, 0x00040047, 0x00000019, 0x0000001e, 0x00000000, 0x00040047, 0x00000025, 0x0000001e,
0x00000001, 0x00040047, 0x00000027, 0x00000006, 0x00000004, 0x00030047, 0x00000028, 0x00000002,
0x00050048, 0x00000028, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000028, 0x00000001,
0x00000023, 0x00000010, 0x00040047, 0x00000030, 0x0000001e, 0x00000002, 0x00040047, 0x0000003b,
0x0000001e, 0x00000002, 0x00040047, 0x00000040, 0x0000001e, 0x00000001, 0x00020013, 0x00000002,
0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008,
0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, 0x0006001e, 0x0000000b,
0x00000007, 0x00000006, 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b,
0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001,
0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040018, 0x00000010, 0x00000007, 0x00000004,
0x0008001e, 0x00000011, 0x00000010, 0x00000010, 0x00000007, 0x00000007, 0x00000007, 0x00000007,
0x00040020, 0x00000012, 0x00000002, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, 0x00000002,
0x00040020, 0x00000014, 0x00000002, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000003,
0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, 0x00000019, 0x00000001,
0x0004002b, 0x00000006, 0x0000001b, 0x3f800000, 0x00040020, 0x00000021, 0x00000003, 0x00000007,
0x00040017, 0x00000023, 0x00000006, 0x00000002, 0x00040020, 0x00000024, 0x00000003, 0x00000023,
0x0004003b, 0x00000024, 0x00000025, 0x00000003, 0x0004002b, 0x00000008, 0x00000026, 0x0000001c,
0x0004001c, 0x00000027, 0x00000006, 0x00000026, 0x0004001e, 0x00000028, 0x00000007, 0x00000027,
0x00040020, 0x00000029, 0x00000009, 0x00000028, 0x0004003b, 0x00000029, 0x0000002a, 0x00000009,
0x00040020, 0x0000002b, 0x00000009, 0x00000007, 0x00040020, 0x0000002f, 0x00000001, 0x00000023,
0x0004003b, 0x0000002f, 0x00000030, 0x00000001, 0x0004003b, 0x00000021, 0x0000003b, 0x00000003,
0x0004002b, 0x0000000e, 0x0000003c, 0x00000004, 0x00040020, 0x0000003d, 0x00000002, 0x00000007,
0x0004003b, 0x00000018, 0x00000040, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f,
0x0004003d, 0x00000010, 0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019,
0x00050051, 0x00000006, 0x0000001c, 0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d,
0x0000001a, 0x00000001, 0x00050051, 0x00000006, 0x0000001e, 0x0000001a, 0x00000002, 0x00070050,
0x00000007, 0x0000001f, 0x0000001c, 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007,
0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, 0x00000022, 0x0000000d, 0x0000000f,
0x0003003e, 0x00000022, 0x00000020, 0x00050041, 0x0000002b, 0x0000002c, 0x0000002a, 0x0000000f,
0x0004003d, 0x00000007, 0x0000002d, 0x0000002c, 0x0007004f, 0x00000023, 0x0000002e, 0x0000002d,
0x0000002d, 0x00000000, 0x00000001, 0x0004003d, 0x00000023, 0x00000031, 0x00000030, 0x00050041,
0x0000002b, 0x00000032, 0x0000002a, 0x0000000f, 0x0004003d, 0x00000007, 0x00000033, 0x00000032,
0x0007004f, 0x00000023, 0x00000034, 0x00000033, 0x00000033, 0x00000002, 0x00000003, 0x00050041,
0x0000002b, 0x00000035, 0x0000002a, 0x0000000f, 0x0004003d, 0x00000007, 0x00000036, 0x00000035,
0x0007004f, 0x00000023, 0x00000037, 0x00000036, 0x00000036, 0x00000000, 0x00000001, 0x00050083,
0x00000023, 0x00000038, 0x00000034, 0x00000037, 0x00050085, 0x00000023, 0x00000039, 0x00000031,
0x00000038, 0x00050081, 0x00000023, 0x0000003a, 0x0000002e, 0x00000039, 0x0003003e, 0x00000025,
0x0000003a, 0x00050041, 0x0000003d, 0x0000003e, 0x00000013, 0x0000003c, 0x0004003d, 0x00000007,
0x0000003f, 0x0000003e, 0x0003003e, 0x0000003b, 0x0000003f, 0x000100fd, 0x00010038,
};
static const size_t sprite_stack_vert_spv_count = 575;
+25
View File
@@ -16,6 +16,10 @@
#include "editor/FieldBrushTool.h"
#include "editor/GizmoManager.h"
#include "editor/TileBuffer.h"
#ifdef EVENGINE_HAS_MAP
#include "map/Map.h"
#include "map/TileLayer.h"
#endif
#include "editor/TransformGizmo.h"
#include "common/Exception.h"
@@ -329,6 +333,27 @@ TEST_CASE("editor.transactions.undo_redo_and_rollback_any_field") {
CHECK_EQ(buffer.getGid(0, 0), 0);
}
#ifdef EVENGINE_HAS_MAP
TEST_CASE("editor.map.tileLayerTargetUsesLiveRevision") {
auto *map = eve::map::Map::create();
auto *layer = map->newLayer(4, 3, 8.f, 8.f);
Editor editor;
std::unique_ptr<TileLayerTarget> target(editor.newTileLayerTarget("ground", layer));
const auto before = target->revision();
CHECK(target->writeInt(2, 1, 9));
CHECK_EQ(layer->getTile(2, 1), 9);
CHECK_GT(target->revision(), before);
CHECK_EQ(target->dirtyRegion().minX, 2);
CHECK_EQ(target->dirtyRegion().maxY, 1);
IntFieldEditCommand command("paint", target.get());
CHECK(command.record(2, 1, 4));
CHECK(command.apply());
CHECK_EQ(layer->getTile(2, 1), 4);
command.revert();
CHECK_EQ(layer->getTile(2, 1), 9);
}
#endif
TEST_CASE("editor.constraints.accept_warn_and_reject_without_core_types") {
TileBuffer buffer(4, 2);
TileBufferTarget target("tiles", &buffer);
+142
View File
@@ -31,6 +31,8 @@
#include "map/TileProjection.h"
#include "map/TileSystem.h"
#include "window/Window.h"
#include "common/Capability.h"
#include "map/TileCollision.h"
#include <SDL2/SDL.h>
#include <algorithm>
@@ -40,6 +42,19 @@
using namespace eve::map;
namespace {
class CollisionSinkMock final : public ITileCollisionSink {
public:
void replaceTileCollision(const void *source, const TileCollisionRect *rects,
size_t count) override {
layer = source;
values.assign(rects, rects + count);
}
const void *layer = nullptr;
std::vector<TileCollisionRect> values;
};
} // namespace
namespace {
void hideAllTileLayers() {
@@ -91,6 +106,52 @@ TEST_CASE("map.layer.resizeClears") {
CHECK_EQ(layer->getTile(0, 0), 0);
}
TEST_CASE("map.layer.chunkIndexAndRevision") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(96, 65, 8.f, 8.f);
CHECK_EQ(layer->getChunkSize(), 32);
CHECK_EQ(layer->getChunkCount(), 9);
CHECK_EQ(layer->getNonEmptyChunkCount(), 0);
const int revision = layer->getRevision();
layer->setTile(0, 0, 1);
layer->setTile(64, 64, 2);
CHECK_EQ(layer->getNonEmptyChunkCount(), 2);
CHECK_GT(layer->getRevision(), revision);
const int unchanged = layer->getRevision();
layer->setTile(64, 64, 2);
CHECK_EQ(layer->getRevision(), unchanged);
layer->setTile(0, 0, 0);
CHECK_EQ(layer->getNonEmptyChunkCount(), 1);
}
TEST_CASE("map.layer.fillRectClipsAndPublishesOnce") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(64, 64, 8.f, 8.f);
const int before = layer->getRevision();
layer->fillRect(-2, -2, 5, 5, 7);
CHECK_EQ(layer->getTile(0, 0), 7);
CHECK_EQ(layer->getTile(2, 2), 7);
CHECK_EQ(layer->getTile(3, 3), 0);
CHECK_EQ(layer->getNonEmptyChunkCount(), 1);
CHECK_EQ(layer->getRevision(), before + 1);
layer->fillRect(31, 31, 2, 2, 8);
CHECK_EQ(layer->getNonEmptyChunkCount(), 4);
}
TEST_CASE("map.render.sparseChunksBoundCollectionWork") {
hideAllTileLayers();
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(1024, 1024, 8.f, 8.f);
layer->setTile(1, 1, 1);
layer->setTile(900, 900, 2);
std::vector<eve::graphics::DrawItem2D> items;
TileRenderSystem::collect(items);
CHECK_EQ(int(items.size()), 2);
CHECK_EQ(TileRenderSystem::lastVisitedChunkCount(), 2);
CHECK_EQ(TileRenderSystem::lastVisitedCellCount(), 2 * 32 * 32);
layer->setVisible(false);
}
TEST_CASE("map.layer.applyConfigFlat") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(2, 2, 16.f, 16.f);
@@ -188,6 +249,87 @@ TEST_CASE("map.tileset.manifestShapeInMapConfig") {
layer->setVisible(false);
}
TEST_CASE("map.layer.applyTiledInfiniteChunks") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(1, 1, 16.f, 16.f);
const char *json = R"({
"infinite":true,"width":0,"height":0,"tilewidth":16,"tileheight":16,
"layers":[{"type":"tilelayer","chunks":[
{"x":-2,"y":-1,"width":2,"height":2,"data":[1,2,3,4]},
{"x":0,"y":-1,"width":2,"height":2,"data":[5,6,7,8]}
]}]
})";
CHECK(layer->applyConfig(json));
CHECK_EQ(layer->getMapWidth(), 4);
CHECK_EQ(layer->getMapHeight(), 2);
CHECK_EQ(layer->getTile(0, 0), 1);
CHECK_EQ(layer->getTile(3, 1), 8);
CHECK_EQ(layer->getX(), -32.f);
CHECK_EQ(layer->getY(), -16.f);
}
TEST_CASE("map.tileset.v2AnimationCustomDataAndTerrain") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(3, 3, 16.f, 16.f);
layer->addTileAnimationFrame(10, 11, 100);
layer->addTileAnimationFrame(10, 12, 150);
CHECK_EQ(layer->getTileAnimationFrameCount(10), 2);
layer->setTileDataString(10, "biome", "forest");
layer->setTileDataNumber(10, "damage", 2.5f);
layer->setTileDataBool(10, "wet", true);
CHECK_EQ(layer->getTileDataType(10, "damage"), std::string("number"));
CHECK_EQ(layer->getTileDataString(10, "biome"), std::string("forest"));
CHECK_EQ(layer->getTileDataNumber(10, "damage"), 2.5f);
CHECK(layer->getTileDataBool(10, "wet"));
layer->setTerrainRule(20, 1, 0);
layer->setTerrainRule(21, 1, 1 << 3);
layer->paintTerrain(1, 1, 1);
CHECK_EQ(layer->getTerrain(1, 1), 1);
CHECK_EQ(layer->getTile(1, 1), 20);
layer->paintTerrain(2, 1, 1);
CHECK_EQ(layer->getTile(1, 1), 21);
layer->clearTileAnimation(10);
CHECK_EQ(layer->getTileAnimationFrameCount(10), 0);
}
TEST_CASE("map.collision.greedyMergeAndCapabilityPublish") {
Map map;
TileLayer *layer = map.newLayer(5, 4, 16.f, 8.f);
layer->setTileMetadata(9, 1, 1, false);
layer->fillRect(1, 1, 3, 2, 9);
CollisionSinkMock sink;
eve::cap::addListener<ITileCollisionSink>(&sink);
CHECK(map.publishCollision(layer) == 1);
CHECK(map.getCollisionRectCount() == 1);
CHECK(map.getCollisionRectX(0) == 16.f);
CHECK(map.getCollisionRectY(0) == 8.f);
CHECK(map.getCollisionRectWidth(0) == 48.f);
CHECK(map.getCollisionRectHeight(0) == 16.f);
CHECK(sink.layer == layer);
REQUIRE(sink.values.size() == 1);
CHECK(sink.values[0].width == 48.f);
eve::cap::removeListener<ITileCollisionSink>(&sink);
}
TEST_CASE("map.tileset.v2ManifestReadsTiledFields") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(1, 1, 16.f, 16.f);
const char *json = R"({
"width":1,"height":1,"data":[1],
"tileset":{"firstGid":1,"columns":4,"tileWidth":16,"tileHeight":16,
"tiles":[{"id":0,"animation":[{"tileid":1,"duration":80},{"tileid":2,"duration":120}],
"terrain":2,"neighborMask":0,
"properties":[{"name":"damage","type":"float","value":3.5}]}]}
})";
CHECK(layer->applyConfig(json));
CHECK_EQ(layer->getTileAnimationFrameCount(1), 2);
CHECK_EQ(layer->getTileDataType(1, "damage"), std::string("float"));
CHECK_EQ(layer->getTileDataString(1, "damage"), std::string("3.5"));
layer->paintTerrain(0, 0, 2);
CHECK_EQ(layer->getTile(0, 0), 1);
}
TEST_CASE("map.render.nullSafe") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(2, 2, 8.f, 8.f);
+16
View File
@@ -219,6 +219,22 @@ TEST_CASE("map.fov.layerOpaqueGid") {
delete fov;
}
TEST_CASE("map.fov.boundLayerAutoSyncsRevision") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(7, 1, 8.f, 8.f);
layer->fill(2);
Fov *fov = mod->newFov(layer);
fov->setBlockEmpty(false);
fov->blockOpaqueGid(1);
fov->addRevealer(0, 0, 7);
fov->compute();
CHECK(fov->isVisible(6, 0));
layer->setTile(3, 0, 1);
fov->compute();
CHECK(!fov->isVisible(6, 0));
delete fov;
}
TEST_CASE("map.fov.removeAndDisable") {
auto *mod = Map::create();
Fov *fov = mod->newFovSize(5, 5);
+20
View File
@@ -436,3 +436,23 @@ TEST_CASE("map.path.accessorRoundTrips") {
delete pf;
}
TEST_CASE("map.path.boundLayerAutoSyncsRevision") {
auto *mod = Map::create();
TileLayer *layer = mod->newLayer(5, 1, 8.f, 8.f);
layer->fill(2);
Pathfinder *pf = mod->newPathfinder(layer);
pf->blockGid(1);
Path *initial = pf->findPath(0, 0, 4, 0);
CHECK_GT(initial->getLength(), 0);
delete initial;
layer->setTile(2, 0, 1);
Path *blocked = pf->findPath(0, 0, 4, 0);
CHECK_EQ(blocked->getLength(), 0);
delete blocked;
layer->setTile(2, 0, 2);
Path *open = pf->findPath(0, 0, 4, 0);
CHECK_GT(open->getLength(), 0);
delete open;
delete pf;
}
+133 -923
View File
File diff suppressed because it is too large. Load diff