Merge pull request #232 from EVEngine/codex/spritestack-2d
refactor(spritestack): use the 2D rendering pipeline
This commit is contained in:
13 files changed
+552
-2450
No files matched your search
@@ -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
|
||||
|
||||
+36
-145
@@ -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`。
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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 ¢er, 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
+133
-923
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user