Merge pull request #232 from EVEngine/codex/spritestack-2d

refactor(spritestack): use the 2D rendering pipeline
This commit is contained in:
西风逍遥游 authored and GitHub committed 2026-08-25 02:05:49 +08:00
commit 30fcb3ea80
13 files changed
+552 -2450

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -1,171 +1,62 @@
# Sprite-Stacking 伪 3D
# SpriteStack 2D 叠片渲染
**脚本入口:** `eve.SpriteStack()`
把三维模型沿某个轴切成 `layerCount` 张薄片图(RGBA),运行时刻把这些薄片按
世界坐标叠起来渲染,从设计视角看就是有体积的伪 3D 物体——经典 sprite-stacking
技术,常用于低多边形 / 像素风游戏的角色与物件。
SpriteStack 使用一组带透明通道的水平切片,在普通 2D 渲染管线中逐层偏移绘制,形成有体积感的 2.5D 图像。它不创建 3D 网格、3D 相机、深度缓冲或 3D 材质。
## 基本用法
```squirrel
// 1. 切片:程序化几何体(cylinder / sphere / cone / box),无需资产文件
local layers = spritestack.slicePrimitive("cylinder", 20, 128, 128, "z", 0.0);
// 也支持对 3D 模型文件切片(model3d 模块)
// local md = model3d.newModelDataFromFile("assets/tree.obj");
// local layers = spritestack.sliceModel(md, 20, 128, 128, "z", 0.0);
// 2. 建叠片对象并填充层纹理
local layers = spritestack.slicePrimitive("cylinder", 20, 128, 128, "y", 0.0);
local stack = spritestack.newStack(gfx);
stack.setLayerCount(layers.len());
for (local i = 0; i < layers.len(); i++)
stack.setLayerImage(gfx, layers[i], i);
for (local i = 0; i < layers.len(); i++) stack.setLayerImage(gfx, layers[i], i);
// 也可逐层加载:stack.setLayerFile(gfx, "assets/layer.png", 0);
// 横向图集可一次拆层:stack.setLayersFromAtlas(gfx, atlas, 20);
stack.setThickness(0.13); // 相邻切片的世界间距
stack.setSize(2.4, 2.8); // 切片四边形尺寸(世界单位)
stack.setPosition(0.0, 0.0, 0.0);
stack.setYaw(0.0);
stack.setTint(0.35, 0.78, 0.72, 1.0);
stack.setMode("vertical"); // "vertical" | "horizontal"
stack.setShadowEnabled(true); // 投影假阴影
stack.setOutline(0.05, 0.02, 0.03, 0.04); // 描边
```
stack.setPosition(480.0, 390.0); // 2D 屏幕坐标
stack.setSize(180.0, 180.0); // 每张切片的像素尺寸
stack.setThickness(4.0); // 相邻切片的像素偏移
stack.setRotation(25.0); // 2D 旋转角度,单位为度
stack.setTint(0.32, 0.82, 0.68, 1.0);
stack.setVisible(true);
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.42);
stack.setShadowOffset(12.0, 9.0);
stack.setOutline(2.0, 0.02, 0.03, 0.04);
渲染时机:在 `gfx.render3D()` 之后、`present()` 之前调用,切片会进入 3D 前向
通道,和场景内其他网格正确深度遮挡。
```squirrel
function eve_render() {
gfx.clear();
gfx.render3D();
stack.render(gfx); // 自动使用当前激活的 Camera3D
// stack.renderWithCamera(gfx, myCamera); // 也可显式指定相机
stack.render(gfx);
}
```
## 两种切片模式
`render(gfx)` 直接按屏幕像素解释坐标;需要世界坐标时调用 `stack.renderWithCamera(gfx, camera)`。批对象同样提供 `batch.renderWithCamera(gfx, camera)`。
| 模式 | 切片轴 | 运行时刻渲染 | 适合 |
|------|--------|--------------|------|
| `"vertical"`(默认) | `"z"` 等水平轴 | 朝向相机的竖直 billboard,yaw 旋转叠片的深度布局 | 经典「面包切片」伪 3D,正面视角最扎实 |
| `"horizontal"` | `"y"`(俯视) | 水平四边形,3/4 相机看到真实体积视差 | 俯视 / 斜 45° 视角的场景物件 |
切片轴与模式要匹配:`slicePrimitive(..., "z", ...)` + `"vertical"`,
或 `slicePrimitive(..., "y", ...)` + `"horizontal"`。
## 对象与参数
- `newStack(gfx)`:新建空叠片(脚本 VM 持有;不要直接构造 `eve.SpriteStack3D`)。
- 图层:`setLayerCount(n)`、`setLayerTexture(tex, i)`、`setLayerImage(gfx, img, i)`、
`setLayerFile(gfx, path, i)`、`setLayersFromAtlas(gfx, atlasTex, count)`(单张横向
条带纹理按列切成 count 层)。
- 变换:`setThickness(t)`、`setSize(w, h)`、`setPosition(x, y, z)`、`setYaw(rad)`。
- 外观:`setTint(r, g, b, a)`、`setAlphaCutoff(v)`、`setVisible(bool)`、`setMode(mode)`。
- 渲染:`render(gfx)` 或 `renderWithCamera(gfx, camera)`。
## 高级功能
### 叠片投影阴影
`setShadowEnabled(true)` 后,每层切片沿光方向压到地面平面(`setShadowPlaneY(y)`),
用切片本身的剪影画半透明黑色,形成接触阴影:
## 图层和批处理
```squirrel
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.38);
stack.setShadowLight(-0.45, -1.0, -0.35); // 光照方向(指向地面)
stack.setShadowPlaneY(-1.53); // 地面高度
```
local count = stack.getLayerCount();
local texture = stack.getLayerTexture(0);
stack.setLayerTexture(texture, 0);
stack.setLayerUV(0, 0.0, 0.0, 1.0, 1.0);
local spacing = stack.getThickness();
这是经典的假阴影(不是 shadow map),适合固定视角的伪 3D 场景;内部会把阴影
平面抬高 2cm 避免与地面深度冲突。
合批时阴影同样生效:`SpriteStackBatch` 会把投影阴影按黑色色调单独分组烘焙,
不额外增加 draw call。
### 叠片描边
`setOutline(width, r, g, b)` 给每层画一个放大 k 的黑色剪影垫在切片后面,形成
风格化描边(配合 `stylize` 的 ink / pixel 后处理很搭):
```squirrel
stack.setOutline(0.05, 0.02, 0.03, 0.04);
```
合批时描边同样生效(按描边色调单独分组)。
### 叠片参与 G-Buffer(AO / 描边后处理)
默认叠片只画进前向通道,AO / 描边这类读取 G-Buffer 深度的后处理看不到它们。
开启 `setGbufferEnabled(true)` 后,叠片会通过 `RenderSystem3D` 的扩展绘制钩子
写进 G-Buffer(alpha 剔除管线),描边和 AO 就能沿切片剪影生效:
```squirrel
stack.setGbufferEnabled(true);
gfx.getRenderControl().enable("outline");
```
### 叠片投射 CSM 阴影
默认叠片不写 shadow map。开启 `setCastShadow(true)` 后,切片通过 alpha 剔除的
shadow 管线写进级联阴影贴图(CSM),在地面等接收阴影的网格上留下**剪影形状的
阴影**,而不是整块矩形:
```squirrel
stack.setCastShadow(true);
```
需要场景里有 `castShadow` 的平行光;如果没有 `Light3D`,会回退用
`gfx.setDirectionalLight(...)` 设置的旧版平行光作为阴影光源。与 `setShadowEnabled`
的投影假阴影相互独立,可同时开启。
### 多叠片合批 `SpriteStackBatch`
多个叠片如果共享图层纹理(`setLayerTexture` 传入同一个 `Texture`),可以用
`newBatch(gfx)` 把它们合成每帧一次 draw call(按纹理 + 色调分组):
```squirrel
local batch = spritestack.newBatch(gfx);
batch.add(stackA);
batch.add(stackB);
// eve_render 中:batch.render(gfx);
batch.add(stack);
batch.remove(stack);
batch.add(stack);
batch.render(gfx);
batch.clear();
```
合批会在叠片内容变化(位置 / 缩放 / 纹理 / 色调)时重建网格(`Graphics::updateMeshVertices`
原地更新);每帧都旋转/移动的叠片建议直接用 `stack.render`,静态道具用合批收益最大。
合批会一并烘焙逐叠片的阴影与描边(它们按各自色调分组)。
批对象提交多个 `SpriteStack2D`;实际纹理由 Graphics 的 2D texture batch 合并。切片从底层到顶层绘制,因此透明边缘和层间遮挡仍遵循 2D painter order。
### Atlas 条带打包工具
## 模型切片
`scripts/pack_sprite_stack_atlas.py` 把一组等尺寸的层 PNG 横向打包成一张条带,
运行时用 `setLayersFromAtlas(gfx, atlasTex, count)` 直接按列切回:
如果已有 `ModelData`,可以离线或加载时生成切片:
```bash
python3 scripts/pack_sprite_stack_atlas.py layer_00.png layer_01.png ... \
-o atlas.png --json atlas.json
```squirrel
local model = model3d.newModelDataFromFile("assets/rock.obj");
local layers = spritestack.sliceModel(model, 24, 128, 128, "y", 0.0);
```
## 切片 API
- `slicePrimitive(kind, layerCount, imageW, imageH, axis, thickness)`:
程序化几何体切片,`kind` ∈ `"box" | "cylinder" | "sphere" | "cone"`;
`axis` ∈ `"x" | "y" | "z"`;`thickness <= 0` 时按包围盒自动均分。
- `sliceModel(modelData, layerCount, imageW, imageH, axis, thickness)`:
对 `model3d.newModelDataFromFile(...)` 的模型切片。
- 两者都返回 `ImageData` 数组(RGBA8),可直接传给 `gfx.newTexture` 或
`stack.setLayerImage`。
## 使用要点
- 切片的 `ImageData` 归脚本持有:传入 `setLayerImage` 后叠片内部已转为 GPU
纹理,可释放原图。
- 叠片使用 alpha 混合 + 深度测试(不写深度)的管线,同堆切片每帧按相机距离
由远到近排序;跨叠片的重叠由绘制顺序决定。
- 目前仅在 Vulkan 桌面端可用(WebGPU/WASM 精简构建不含该模块)。
- 不要每帧重新切片或重新上传纹理;只在物体形状变化时重建。
- `updateMeshVertices`(合批内部使用)与 GPU 同步,重建频率越高开销越大。
**源码:** [`src/modules/spritestack/`](../../../src/modules/spritestack/)
**示例:** [`examples/sprite-stack/`](../../../examples/sprite-stack/)
**相关测试:** [`test/spritestack.cpp`](../../../test/spritestack.cpp)
这里的模型只用于生成 RGBA 切片;运行时仍走 2D 管线。`slicePrimitive` 支持 `cylinder`、`sphere`、`cone`、`box`,切片轴支持 `x`、`y`、`z`。
+6 -27
View File
@@ -1,34 +1,13 @@
# Sprite Stacking
# SpriteStack 2D 示例
伪 3D sprite-stacking 示例:把程序化生成的三维几何体(圆柱 / 球 / 圆锥 / 方块)
在 CPU 上沿指定轴切成多层 RGBA 图,再以「叠片」方式画进 3D 前向渲染通道。
这个示例把程序化几何体切成 RGBA 水平切片,再通过 Graphics 的普通 2D 纹理管线逐层偏移绘制。运行阶段没有 3D 相机、3D 网格、深度缓冲或专用 3D shader。
运行:
```bash
eve run examples/sprite-stack
```powershell
build\\win32-debug\\src\\engine\\eve.exe run examples\\sprite-stack
```
操作:
操作:A / D 旋转叠片,1~4 切换圆柱、球、圆锥、方盒,Q / E 调整层间像素距离。
| 键 | 作用 |
|----|------|
| `A` / `D` | 绕 Y 轴旋转叠片堆(伪 3D 视差) |
| `W` / `S` | 拉近 / 拉远相机 |
| `H` | 切换 vertical(朝向相机的竖直切片)/ horizontal(水平俯视切片)模式 |
| `1`..`4` | 切换圆柱 / 球 / 圆锥 / 方块 |
| `M` | 切换程序化几何体 / `assets/rock.obj`(`sliceModel` 模型文件切片) |
| `Q` / `E` | 调整切片厚度 |
| `R` | 重新切片 |
技术要点:
- 切片:`spritestack.slicePrimitive(...)`(或 `sliceModel` 对 3D 模型文件切片)
把网格切成 `layerCount` 个薄片,每个薄片正交投影成一张 RGBA 图。
- 渲染:`SpriteStack3D` 用 alpha 混合 + 深度测试的 hair 管线绘制切片,
`vertical` 模式为朝向相机的 billboard,`horizontal` 模式为水平四边形;
每帧按相机距离由远到近排序。
- 调用时机:`gfx.render3D()` 之后、`present()` 之前调用 `stack.render(gfx)`,
切片即与场景内其他 3D 网格正确遮挡。
- 高级功能:`setShadowEnabled` 投影假阴影、`setOutline` 风格化描边;另外三个
共享纹理的小石头通过 `SpriteStackBatch` 合批成单次 draw。
主物体直接调用 `stack.render(gfx)`;左右两个副本通过 `SpriteStackBatch` 提交,共享同一组切片纹理。
+44 -115
View File
@@ -1,138 +1,67 @@
// Sprite stacking pseudo-3D demo.
//
// A 3D primitive is sliced into thin RGBA layers on the CPU, then rendered as
// a stack of alpha-blended slices inside the 3D forward pass. From the design
// view angle the stack reads as a solid 3D object; rotating the camera (or the
// stack yaw) reveals the classic pseudo-3D parallax.
//
// Controls:
// A / D rotate stack yaw
// W / S camera distance
// H toggle vertical (billboard) / horizontal (top-down) mode
// 1..4 primitive: cylinder / sphere / cone / box
// M toggle procedural primitive / rock.obj (model-file slicing)
// Q / E slice thickness
// R rebuild (reslice) the stack
if (!("stackYaw" in getroottable())) stackYaw <- 0.0;
if (!("camDist" in getroottable())) camDist <- 6.0;
if (!("stackMode" in getroottable())) stackMode <- "vertical";
if (!("stackKind" in getroottable())) stackKind <- "cylinder";
if (!("sliceCount" in getroottable())) sliceCount <- 20;
if (!("sliceThick" in getroottable())) sliceThick <- 0.0;
// Classic SpriteStack rendered entirely by the normal 2D pipeline.
// A / D rotate, 1..4 switch primitive, Q / E change slice spacing.
if (!("angle" in getroottable())) angle <- 0.0;
if (!("kind" in getroottable())) kind <- "cylinder";
if (!("spacing" in getroottable())) spacing <- 4.0;
if (!("stack" in getroottable())) stack <- null;
if (!("batch" in getroottable())) batch <- null;
if (!("satellites" in getroottable())) satellites <- [];
if (!("rockLayers" in getroottable())) rockLayers <- null;
if (!("useModel" in getroottable())) useModel <- false;
if (!("prevKeys" in getroottable())) prevKeys <- {};
if (!("copies" in getroottable())) copies <- [];
local tints = {
cylinder = [0.35, 0.78, 0.72],
sphere = [0.85, 0.52, 0.28],
cone = [0.55, 0.42, 0.30],
box = [0.42, 0.55, 0.85],
cylinder = [0.32, 0.82, 0.68], sphere = [0.92, 0.52, 0.26],
cone = [0.68, 0.48, 0.30], box = [0.42, 0.62, 0.92]
};
function pressed(k) {
return key_just_pressed(k);
}
function rebuildStack() {
local axis = stackMode == "vertical" ? "z" : "y";
if (useModel) {
local md = model3d.newModelDataFromFile("assets/rock.obj");
rockLayers = spritestack.sliceModel(md, 22, 128, 128, axis, 0.0);
} else {
rockLayers = null;
}
local layers = rockLayers != null
? rockLayers
: spritestack.slicePrimitive(stackKind, sliceCount, 128, 128, axis, 0.0);
function rebuild() {
local layers = spritestack.slicePrimitive(kind, 20, 128, 128, "y", 0.0);
stack = spritestack.newStack(gfx);
stack.setLayerCount(layers.len());
for (local i = 0; i < layers.len(); i++)
stack.setLayerImage(gfx, layers[i], i);
stack.setThickness(stackMode == "vertical" ? 0.12 : 0.17);
stack.setSize(2.4, 2.8);
stack.setPosition(0.0, 0.0, 0.0);
stack.setMode(stackMode);
local t = useModel ? [0.55, 0.48, 0.40] : tints[stackKind];
stack.setTint(t[0], t[1], t[2], 1.0);
for (local i = 0; i < layers.len(); i++) stack.setLayerImage(gfx, layers[i], i);
stack.setPosition(480.0, 390.0);
stack.setSize(180.0, 180.0);
stack.setThickness(spacing);
local tint = tints[kind];
stack.setTint(tint[0], tint[1], tint[2], 1.0);
stack.setShadowEnabled(true);
stack.setShadowOpacity(0.38);
stack.setShadowLight(-0.45, -1.0, -0.35);
stack.setShadowPlaneY(-1.53);
stack.setCastShadow(true); // real CSM shadow from the slice silhouettes
if (stackMode == "vertical") stack.setOutline(0.045, 0.02, 0.03, 0.04);
rebuildSatellites();
}
stack.setShadowOpacity(0.42);
stack.setShadowOffset(12.0, 9.0);
stack.setOutline(2.0, 0.02, 0.03, 0.04);
// A few small copies sharing the main stack's layer textures, drawn in a
// single batched draw call per (texture, tint) group.
function rebuildSatellites() {
if (batch == null) batch = spritestack.newBatch(gfx);
batch.clear();
satellites = [];
local offsets = [[2.6, 0.9], [-2.5, 1.0], [3.0, -1.2]];
foreach (off in offsets) {
local s = spritestack.newStack(gfx);
s.setLayerCount(stack.getLayerCount());
batch = spritestack.newBatch(gfx);
copies = [];
foreach (x in [190.0, 770.0]) {
local copy = spritestack.newStack(gfx);
copy.setLayerCount(stack.getLayerCount());
for (local i = 0; i < stack.getLayerCount(); i++)
s.setLayerTexture(stack.getLayerTexture(i), i);
s.setThickness(stack.getThickness());
s.setSize(0.85, 0.95);
s.setPosition(off[0], 0.0, off[1]);
s.setMode(stackMode);
s.setTint(0.58, 0.52, 0.46, 1.0);
batch.add(s);
satellites.append(s);
copy.setLayerTexture(stack.getLayerTexture(i), i);
copy.setPosition(x, 360.0);
copy.setSize(105.0, 105.0);
copy.setThickness(spacing * 0.55);
copy.setTint(tint[0], tint[1], tint[2], 1.0);
copy.setShadowEnabled(true);
batch.add(copy);
copies.append(copy);
}
}
local cam = eve.Camera3D();
cam.setEye(0.0, 2.8, camDist);
cam.setTarget(0.0, 0.2, 0.0);
cam.setUp(0.0, 1.0, 0.0);
cam.setFov(45.0);
cam.setAmbient(0.32, 0.35, 0.38);
cam.setActive(true);
gfx.setDirectionalLight(-0.45, -1.0, -0.35, 1.25, 1.18, 1.05);
gfx.setBackgroundColor(0.075, 0.10, 0.12, 1.0);
// Flat ground disc so the stack's depth reads against the scene.
local ground = eve.Renderable3D();
ground.setMesh(gfx.newMeshCylinder(48, 1, true));
ground.setPosition(0.0, -1.55, 0.0);
ground.setScale(9.0, 0.18, 9.0);
ground.setTint(0.20, 0.30, 0.24, 1.0);
ground.setRoughness(0.9);
rebuildStack();
gfx.setBackgroundColor(0.055, 0.075, 0.095, 1.0);
rebuild();
function eve_update(dt) {
if (pressed("h") || pressed("H")) {
stackMode = stackMode == "vertical" ? "horizontal" : "vertical";
rebuildStack();
}
if (pressed("r") || pressed("R")) rebuildStack();
if (pressed("1")) { stackKind = "cylinder"; rebuildStack(); }
if (pressed("2")) { stackKind = "sphere"; rebuildStack(); }
if (pressed("3")) { stackKind = "cone"; rebuildStack(); }
if (pressed("4")) { stackKind = "box"; rebuildStack(); }
if (pressed("m") || pressed("M")) { useModel = !useModel; rebuildStack(); }
if (keyboard.isDown("a") || keyboard.isDown("A")) stackYaw -= dt * 0.9;
if (keyboard.isDown("d") || keyboard.isDown("D")) stackYaw += dt * 0.9;
if (keyboard.isDown("w") || keyboard.isDown("W")) camDist = max(2.5, camDist - dt * 3.0);
if (keyboard.isDown("s") || keyboard.isDown("S")) camDist = min(14.0, camDist + dt * 3.0);
stack.setYaw(stackYaw);
cam.setEye(0.0, 2.8, camDist);
if (keyboard.isDown("a") || keyboard.isDown("A")) angle -= dt * 75.0;
if (keyboard.isDown("d") || keyboard.isDown("D")) angle += dt * 75.0;
if (key_just_pressed("1")) { kind = "cylinder"; rebuild(); }
if (key_just_pressed("2")) { kind = "sphere"; rebuild(); }
if (key_just_pressed("3")) { kind = "cone"; rebuild(); }
if (key_just_pressed("4")) { kind = "box"; rebuild(); }
if (key_just_pressed("q") || key_just_pressed("Q")) { spacing = max(1.0, spacing - 1.0); rebuild(); }
if (key_just_pressed("e") || key_just_pressed("E")) { spacing = min(10.0, spacing + 1.0); rebuild(); }
stack.setRotation(angle);
foreach (copy in copies) copy.setRotation(-angle * 0.7);
}
function eve_render() {
gfx.clear();
gfx.render3D();
// Draw the pseudo-3D stack into the open 3D pass (before present).
stack.render(gfx);
batch.render(gfx);
}
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""Compile sprite-stack GLSL vert/frag to SPIR-V C++ include arrays."""
from __future__ import annotations
import struct
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SHADER_DIR = ROOT / "src" / "modules" / "spritestack" / "shaders"
SHADERS = [
("sprite_stack.vert", "vert"),
("sprite_stack.frag", "frag"),
]
def spv_to_inc(spv_path: Path, array_name: str, out_path: Path) -> None:
data = spv_path.read_bytes()
if len(data) % 4 != 0:
raise SystemExit(f"SPIR-V size not multiple of 4: {spv_path}")
words = struct.unpack(f"<{len(data) // 4}I", data)
if not words or words[0] != 0x07230203:
raise SystemExit(f"bad SPIR-V magic in {spv_path}")
lines = [f"static const uint32_t {array_name}[] = {{"]
row: list[str] = []
for i, w in enumerate(words):
row.append(f"0x{w:08x}")
if len(row) == 8:
lines.append(" " + ", ".join(row) + ",")
row = []
if row:
lines.append(" " + ", ".join(row) + ",")
lines.append("};")
lines.append(f"static const size_t {array_name}_count = {len(words)};")
lines.append("")
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"wrote {out_path.relative_to(ROOT)} ({len(words)} words)")
def compile_shader(src: Path, stage: str) -> Path:
out = src.with_suffix(f".{stage}.spv")
cmd = ["glslc", f"-fshader-stage={stage}", str(src), "-o", str(out)]
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit(f"glslc failed for {src.name}:\n{r.stderr or r.stdout}")
return out
def main() -> int:
if not SHADER_DIR.is_dir():
raise SystemExit(f"missing shader dir: {SHADER_DIR}")
for name, stage in SHADERS:
src = SHADER_DIR / name
if not src.is_file():
raise SystemExit(f"missing {src}")
spv = compile_shader(src, stage)
stem = src.stem # e.g. sprite_stack
array = f"{stem}_{stage}_spv"
spv_to_inc(spv, array, SHADER_DIR / f"{stem}_{stage}_spv.inc")
spv.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+3 -771
View File
@@ -1,25 +1,13 @@
#include "spritestack/SpriteStack.h"
#include "common/Exception.h"
#include "common/ECS.h"
#include "graphics/ClipSpace.h"
#include "graphics/Graphics.h"
#include "graphics/Mesh.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "image/ImageData.h"
#include "model3d/ModelData.h"
#include "spritestack/shaders/sprite_stack_frag_spv.inc"
#include "spritestack/shaders/sprite_stack_vert_spv.inc"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <memory>
#include <simplesquirrel/simplesquirrel.hpp>
#include <string>
#include <utility>
#include <vector>
@@ -405,763 +393,7 @@ std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &kind,
}
// ---------------------------------------------------------------------------
// SpriteStack3D
// ---------------------------------------------------------------------------
namespace {
std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
return std::vector<uint32_t>(data, data + count);
}
glm::mat4 billboardModel(const glm::vec3 &center, float width, float height,
const glm::vec3 &eye) {
glm::vec3 d = eye - center;
d.y = 0.f;
if (glm::length(d) < 1e-4f) d = glm::vec3(0.f, 0.f, 1.f);
d = glm::normalize(d);
const glm::vec3 up(0.f, 1.f, 0.f);
const glm::vec3 right = glm::normalize(glm::cross(up, d));
const glm::vec3 forward = glm::normalize(glm::cross(right, up));
glm::mat4 m(1.f);
m[0] = glm::vec4(right * width, 0.f);
m[1] = glm::vec4(up * height, 0.f);
m[2] = glm::vec4(forward, 0.f);
m[3] = glm::vec4(center, 1.f);
return m;
}
graphics::Camera3D *findActiveCamera3D() {
if (ecs::current()->getManager<graphics::Camera3D>() == nullptr) return nullptr;
auto camView = ecs::View<graphics::Camera3D, graphics::Camera3D::Data>();
for (auto it = camView.begin(); it != camView.end(); ++it) {
auto [data] = *it;
if (!data->active || !data->entity) continue;
return data->entity;
}
return nullptr;
}
} // namespace
void SpriteStack3D::setLayerCount(int count) {
if (count < 0) throw eve::Exception("SpriteStack3D.setLayerCount: count must be >= 0");
layerCount_ = count;
layers_.assign(size_t(count), Layer{});
bumpVersion();
}
int SpriteStack3D::getLayerCount() const { return layerCount_; }
void SpriteStack3D::setLayerTexture(graphics::Texture *texture, int index) {
if (index < 0 || index >= layerCount_)
throw eve::Exception("SpriteStack3D.setLayerTexture: index %d out of range [0,%d)", index,
layerCount_);
layers_[size_t(index)] = Layer{texture, glm::vec4(0.f, 0.f, 1.f, 1.f)};
bumpVersion();
}
graphics::Texture *SpriteStack3D::getLayerTexture(int index) const {
if (index < 0 || index >= layerCount_) return nullptr;
return layers_[size_t(index)].texture;
}
void SpriteStack3D::setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayerImage: null graphics");
if (!img) throw eve::Exception("SpriteStack3D.setLayerImage: null ImageData");
setLayerTexture(gfx->newTextureFromImageData(img, false, false), index);
}
void SpriteStack3D::setLayerFile(graphics::Graphics *gfx, const std::string &path, int index) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayerFile: null graphics");
setLayerTexture(gfx->newTextureFromFile(path), index);
}
void SpriteStack3D::setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount) {
if (!gfx) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null graphics");
if (!atlas) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null atlas");
if (layerCount <= 0) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: count must be > 0");
setLayerCount(layerCount);
for (int i = 0; i < layerCount; ++i) {
const float u0 = float(i) / float(layerCount);
const float u1 = float(i + 1) / float(layerCount);
layers_[size_t(i)] = Layer{atlas, glm::vec4(u0, 0.f, u1, 1.f)};
}
bumpVersion();
}
void SpriteStack3D::setThickness(float thickness) {
if (thickness <= 0.f) throw eve::Exception("SpriteStack3D.setThickness: must be > 0");
thickness_ = thickness;
bumpVersion();
}
float SpriteStack3D::getThickness() const { return thickness_; }
void SpriteStack3D::setSize(float width, float height) {
if (width <= 0.f || height <= 0.f)
throw eve::Exception("SpriteStack3D.setSize: size must be > 0");
width_ = width;
height_ = height;
bumpVersion();
}
float SpriteStack3D::getWidth() const { return width_; }
float SpriteStack3D::getHeight() const { return height_; }
void SpriteStack3D::setPosition(float x, float y, float z) {
x_ = x;
y_ = y;
z_ = z;
bumpVersion();
}
void SpriteStack3D::setYaw(float yaw) {
yaw_ = yaw;
bumpVersion();
}
void SpriteStack3D::setTint(float r, float g, float b, float a) {
tintR_ = r;
tintG_ = g;
tintB_ = b;
tintA_ = a;
bumpVersion();
}
void SpriteStack3D::setAlphaCutoff(float cutoff) { alphaCutoff_ = clampf(cutoff, 0.f, 1.f); }
void SpriteStack3D::setVisible(bool visible) {
visible_ = visible;
bumpVersion();
}
bool SpriteStack3D::getVisible() const { return visible_; }
void SpriteStack3D::setMode(const std::string &mode) {
if (mode != "vertical" && mode != "horizontal")
throw eve::Exception("SpriteStack3D.setMode: unknown mode '%s' (vertical|horizontal)",
mode.c_str());
mode_ = mode;
bumpVersion();
}
std::string SpriteStack3D::getMode() const { return mode_; }
void SpriteStack3D::setShadowEnabled(bool enabled) {
shadowEnabled_ = enabled;
bumpVersion();
}
bool SpriteStack3D::getShadowEnabled() const { return shadowEnabled_; }
void SpriteStack3D::setShadowOpacity(float opacity) {
shadowOpacity_ = clampf(opacity, 0.f, 1.f);
bumpVersion();
}
void SpriteStack3D::setShadowLight(float dx, float dy, float dz) {
shadowLightX_ = dx;
shadowLightY_ = dy;
shadowLightZ_ = dz;
bumpVersion();
}
void SpriteStack3D::setShadowPlaneY(float y) {
shadowPlaneY_ = y;
bumpVersion();
}
void SpriteStack3D::setOutline(float width, float r, float g, float b) {
outlineWidth_ = std::max(0.f, width);
outlineR_ = r;
outlineG_ = g;
outlineB_ = b;
bumpVersion();
}
float SpriteStack3D::getOutlineWidth() const { return outlineWidth_; }
void SpriteStack3D::setOutlineColor(float r, float g, float b) {
outlineR_ = r;
outlineG_ = g;
outlineB_ = b;
bumpVersion();
}
void SpriteStack3D::ensureResources(graphics::Graphics *gfx) const {
if (quad_ && shader_) return;
if (!quad_) {
const float posXYZ[] = {-0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f,
0.5f, -0.5f, 0.f, -0.5f, -0.5f, 0.f};
const float nrmXYZ[] = {0.f, 0.f, 1.f, 0.f, 0.f, 1.f,
0.f, 0.f, 1.f, 0.f, 0.f, 1.f};
const float uvST[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
const uint32_t indices[] = {0, 1, 2, 0, 2, 3};
quad_ = gfx->newMeshFromArrays(posXYZ, nrmXYZ, uvST, 4, indices, 6);
}
if (!shader_) {
auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
shader_ = gfx->newHairShaderFromSpv(vert, frag);
if (!shader_ || !shader_->gpuHandle)
throw eve::Exception("SpriteStack3D.render: failed to create slice shader");
shader_->declareVec4("uvRect");
shader_->declareFloat("alphaCutoff");
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
shader_->sendFloat("alphaCutoff", alphaCutoff_);
}
}
void SpriteStack3D::collectSlices(const SpriteStack3D &stack, const glm::vec3 &eye,
std::vector<SliceDraw> &out) {
if (!stack.visible_ || stack.layerCount_ <= 0) return;
const glm::mat4 yawM = glm::rotate(glm::mat4(1.f), stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
const float half = 0.5f * float(stack.layerCount_ - 1);
for (int i = 0; i < stack.layerCount_; ++i) {
graphics::Texture *tex = stack.layers_[size_t(i)].texture;
if (!tex) continue;
const float offset = stack.thickness_ * (float(i) - half);
glm::vec3 center(stack.x_, stack.y_, stack.z_);
if (stack.mode_ == "horizontal") {
center.y += offset;
} else {
const glm::vec4 off = yawM * glm::vec4(0.f, 0.f, offset, 0.f);
center += glm::vec3(off);
}
SliceDraw s;
const glm::vec3 d = center - eye;
s.distSq = glm::dot(d, d);
if (stack.mode_ == "horizontal") {
// Horizontal top-down layer: rotate image by yaw, keep the quad
// level so a 3/4 camera sees true volume.
s.model = glm::translate(glm::mat4(1.f), center);
s.model = glm::rotate(s.model, stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
s.model = glm::rotate(s.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
s.model = glm::scale(s.model, glm::vec3(stack.width_, stack.height_, 1.f));
} else {
s.model = billboardModel(center, stack.width_, stack.height_, eye);
}
s.texture = tex;
s.uv = stack.layers_[size_t(i)].uv;
out.push_back(s);
}
}
SpriteStack3D::SliceDraw SpriteStack3D::makeShadowDraw(const SpriteStack3D &stack,
const SliceDraw &s,
const glm::vec3 &eye) {
SliceDraw out = s;
const glm::vec3 light =
glm::normalize(glm::vec3(stack.shadowLightX_, stack.shadowLightY_, stack.shadowLightZ_));
const glm::vec3 center(s.model[3]);
// Lift the shadow above the ground so the depth test (strict LESS) cannot
// reject it as coplanar with the ground surface.
const float planeY = stack.shadowPlaneY_ + 0.02f;
const float t = (planeY - center.y) / light.y;
const glm::vec3 c = center + light * t;
out.model = glm::translate(glm::mat4(1.f), c);
out.model = glm::rotate(out.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
out.model = glm::scale(out.model, glm::vec3(stack.width_, stack.height_, 1.f));
const glm::vec3 d = c - eye;
out.distSq = glm::dot(d, d);
return out;
}
SpriteStack3D::SliceDraw SpriteStack3D::makeOutlineDraw(const SpriteStack3D &stack,
const SliceDraw &s,
const glm::vec3 &eye) {
SliceDraw out = s;
const glm::vec3 center(s.model[3]);
glm::vec3 away = center - eye;
if (glm::length(away) < 1e-6f) away = glm::vec3(0.f, 0.f, 1.f);
away = glm::normalize(away);
// Push the outline slightly away from the camera so the slice draw (same
// plane) still passes the strict-LESS depth test over it and only the rim
// stays visible.
out.model[3] = glm::vec4(center + away * 0.02f, 1.f);
const float k = stack.outlineWidth_;
out.model = out.model * glm::scale(glm::mat4(1.f), glm::vec3(1.f + 2.f * k, 1.f + 2.f * k, 1.f));
return out;
}
std::vector<SpriteStack3D *> &SpriteStack3D::gbufferStacks() {
static std::vector<SpriteStack3D *> stacks;
return stacks;
}
void SpriteStack3D::registerGbufferDrawer() {
static bool registered = false;
if (registered) return;
registered = true;
eve::graphics::RenderSystem3D::addGBufferExtraDrawer(
[](eve::graphics::Graphics &gfx, const eve::graphics::Camera3D::Data &cam,
const glm::mat4 &viewProj, float /*aspect*/) {
drawGbufferStacks(gfx, viewProj, cam.eyeX, cam.eyeY, cam.eyeZ, cam.nearZ, cam.farZ);
});
}
void SpriteStack3D::drawGbufferStacks(eve::graphics::Graphics &gfx, const glm::mat4 &viewProj,
float eyeX, float eyeY, float eyeZ, float nearZ,
float farZ) {
const glm::vec3 eye(eyeX, eyeY, eyeZ);
std::vector<SliceDraw> slices;
for (SpriteStack3D *st : gbufferStacks()) {
if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
st->ensureResources(&gfx);
if (!st->quad_) continue;
slices.clear();
collectSlices(*st, eye, slices);
for (const auto &s : slices) {
const glm::mat4 mvp = viewProj * s.model;
gfx.drawMeshGBufferAlpha(st->quad_, mvp, s.model, nearZ, farZ, s.texture,
st->tintR_, st->tintG_, st->tintB_);
}
}
}
void SpriteStack3D::setGbufferEnabled(bool enabled) {
gbufferEnabled_ = enabled;
auto &stacks = gbufferStacks();
auto it = std::find(stacks.begin(), stacks.end(), this);
if (enabled && it == stacks.end()) stacks.push_back(this);
if (!enabled && it != stacks.end()) stacks.erase(it);
registerGbufferDrawer();
}
bool SpriteStack3D::getGbufferEnabled() const { return gbufferEnabled_; }
std::vector<SpriteStack3D *> &SpriteStack3D::shadowCasterStacks() {
static std::vector<SpriteStack3D *> stacks;
return stacks;
}
void SpriteStack3D::registerShadowDrawer() {
static bool registered = false;
if (registered) return;
registered = true;
eve::graphics::RenderSystem3D::addShadowExtraDrawer(
[](eve::graphics::Graphics &gfx, const glm::mat4 &lightVP,
const eve::graphics::Camera3D::Data &cam) {
drawShadowCasterStacks(gfx, lightVP, cam.eyeX, cam.eyeY, cam.eyeZ);
});
}
void SpriteStack3D::drawShadowCasterStacks(eve::graphics::Graphics &gfx,
const glm::mat4 &lightVP, float eyeX, float eyeY,
float eyeZ) {
const glm::vec3 eye(eyeX, eyeY, eyeZ);
std::vector<SliceDraw> slices;
for (SpriteStack3D *st : shadowCasterStacks()) {
if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
st->ensureResources(&gfx);
if (!st->quad_) continue;
slices.clear();
collectSlices(*st, eye, slices);
for (const auto &s : slices) {
// Camera-facing cards cast their silhouette into the light view;
// the alpha-cutout shadow pipeline keeps the shape instead of a
// solid quad.
gfx.drawMeshShadowAlpha(st->quad_, lightVP * s.model, s.texture);
}
}
}
void SpriteStack3D::setCastShadow(bool cast) {
castShadow_ = cast;
auto &stacks = shadowCasterStacks();
auto it = std::find(stacks.begin(), stacks.end(), this);
if (cast && it == stacks.end()) stacks.push_back(this);
if (!cast && it != stacks.end()) stacks.erase(it);
registerShadowDrawer();
}
bool SpriteStack3D::getCastShadow() const { return castShadow_; }
SpriteStack3D::~SpriteStack3D() {
auto &gb = gbufferStacks();
auto it = std::find(gb.begin(), gb.end(), this);
if (it != gb.end()) gb.erase(it);
auto &sc = shadowCasterStacks();
it = std::find(sc.begin(), sc.end(), this);
if (it != sc.end()) sc.erase(it);
}
void SpriteStack3D::render(graphics::Graphics *gfx, graphics::Camera3D *camera) const {
if (!gfx) throw eve::Exception("SpriteStack3D.render: null graphics");
if (!visible_ || layerCount_ <= 0) return;
if (!camera) camera = findActiveCamera3D();
if (!camera) return;
ensureResources(gfx);
if (!quad_ || !shader_) return;
auto cd = camera->data();
const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
const float fovRad = cd->fovYDeg * 0.017453292519943295f;
const glm::mat4 projM =
eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
gfx->setMesh3DViewProj(projM * viewM);
gfx->setMesh3DView(viewM);
gfx->setMesh3DClip(cd->nearZ, cd->farZ);
gfx->setMesh3DCameraPos(eye);
gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
shader_->sendFloat("alphaCutoff", alphaCutoff_);
std::vector<SliceDraw> slices;
slices.reserve(size_t(layerCount_));
collectSlices(*this, eye, slices);
if (slices.empty()) return;
// Far-to-near: alpha-blended slices need back-to-front draw order.
std::sort(slices.begin(), slices.end(),
[](const SliceDraw &a, const SliceDraw &b) { return a.distSq > b.distSq; });
// Projected contact shadows: each slice silhouette squashed onto the ground
// plane along the light direction, painted as a dark alpha blob.
if (shadowEnabled_ && shadowLightY_ < -1e-4f) {
const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, shadowOpacity_);
for (const auto &s : slices) {
const SliceDraw shadow = makeShadowDraw(*this, s, eye);
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, shadow.model, shadow.texture, shadowColor, shader_);
}
}
// Stylized rim outline: expanded dark silhouettes behind the stack.
if (outlineWidth_ > 0.f) {
const eve::graphics::Color outlineColor(outlineR_, outlineG_, outlineB_, 1.f);
for (const auto &s : slices) {
const SliceDraw outline = makeOutlineDraw(*this, s, eye);
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, outline.model, outline.texture, outlineColor, shader_);
}
}
const glm::vec4 tint(tintR_, tintG_, tintB_, tintA_);
for (const auto &s : slices) {
shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
gfx->drawMeshShader(quad_, s.model, s.texture,
eve::graphics::Color(tint.r, tint.g, tint.b, tint.a),
shader_);
}
}
// ---------------------------------------------------------------------------
// SpriteStackBatch
// ---------------------------------------------------------------------------
namespace {
uint32_t packTint(float r, float g, float b, float a) {
return (uint32_t(clampf(r, 0.f, 1.f) * 255.f) << 24) |
(uint32_t(clampf(g, 0.f, 1.f) * 255.f) << 16) |
(uint32_t(clampf(b, 0.f, 1.f) * 255.f) << 8) |
uint32_t(clampf(a, 0.f, 1.f) * 255.f);
}
eve::graphics::Color unpackTint(uint32_t t) {
return eve::graphics::Color(float((t >> 24) & 0xff) / 255.f,
float((t >> 16) & 0xff) / 255.f,
float((t >> 8) & 0xff) / 255.f, float(t & 0xff) / 255.f);
}
} // namespace
void SpriteStackBatch::add(SpriteStack3D *stack) {
if (!stack) throw eve::Exception("SpriteStackBatch.add: null stack");
if (std::find(stacks_.begin(), stacks_.end(), stack) == stacks_.end()) {
stacks_.push_back(stack);
forceRebuild_ = true;
}
}
void SpriteStackBatch::remove(SpriteStack3D *stack) {
auto it = std::find(stacks_.begin(), stacks_.end(), stack);
if (it != stacks_.end()) {
stacks_.erase(it);
forceRebuild_ = true;
}
}
void SpriteStackBatch::clear() {
stacks_.clear();
groups_.clear();
forceRebuild_ = true;
}
int SpriteStackBatch::getStackCount() const { return int(stacks_.size()); }
void SpriteStackBatch::ensureShader(graphics::Graphics *gfx) {
if (shader_) return;
auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
shader_ = gfx->newHairShaderFromSpv(vert, frag);
if (!shader_ || !shader_->gpuHandle)
throw eve::Exception("SpriteStackBatch.render: failed to create slice shader");
shader_->declareVec4("uvRect");
shader_->declareFloat("alphaCutoff");
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
shader_->sendFloat("alphaCutoff", 0.05f);
}
void SpriteStackBatch::render(graphics::Graphics *gfx, graphics::Camera3D *camera) {
if (!gfx) throw eve::Exception("SpriteStackBatch.render: null graphics");
if (stacks_.empty()) return;
if (!camera) camera = findActiveCamera3D();
if (!camera) return;
ensureShader(gfx);
if (!shader_) return;
auto cd = camera->data();
const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
const float fovRad = cd->fovYDeg * 0.017453292519943295f;
const glm::mat4 projM =
eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
gfx->setMesh3DViewProj(projM * viewM);
gfx->setMesh3DView(viewM);
gfx->setMesh3DClip(cd->nearZ, cd->farZ);
gfx->setMesh3DCameraPos(eye);
gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
shader_->sendFloat("alphaCutoff", 0.05f);
shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f); // UVs are baked into the mesh
struct GroupBuild {
GroupKey key{};
struct ColoredSlice {
SpriteStack3D::SliceDraw draw;
eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
};
std::vector<ColoredSlice> slices;
uint64_t stamp = 0;
float nearest = 1e30f;
eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
};
std::unordered_map<GroupKey, GroupBuild, GroupKeyHash> builds;
for (SpriteStack3D *st : stacks_) {
if (!st || !st->visible_) continue;
std::vector<SpriteStack3D::SliceDraw> tmp;
SpriteStack3D::collectSlices(*st, eye, tmp);
const uint64_t stamp = st->getVersion();
std::vector<GroupBuild::ColoredSlice> colored;
const eve::graphics::Color baseColor(st->tintR_, st->tintG_, st->tintB_, st->tintA_);
for (const auto &s : tmp) colored.push_back({s, baseColor});
if (st->shadowEnabled_ && st->shadowLightY_ < -1e-4f) {
const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, st->shadowOpacity_);
for (const auto &s : tmp)
colored.push_back(
{SpriteStack3D::makeShadowDraw(*st, s, eye), shadowColor});
}
if (st->outlineWidth_ > 0.f) {
const eve::graphics::Color outlineColor(st->outlineR_, st->outlineG_, st->outlineB_,
1.f);
for (const auto &s : tmp)
colored.push_back(
{SpriteStack3D::makeOutlineDraw(*st, s, eye), outlineColor});
}
for (const auto &cs : colored) {
GroupKey key{cs.draw.texture,
packTint(cs.color.r, cs.color.g, cs.color.b, cs.color.a)};
GroupBuild &b = builds[key];
b.key = key;
b.color = unpackTint(key.tint);
b.slices.push_back(cs);
b.stamp = std::max(b.stamp, stamp);
b.nearest = std::min(b.nearest, cs.draw.distSq);
}
}
if (builds.empty()) return;
// Draw far groups first; inside each group slices are baked far-to-near.
std::vector<GroupBuild *> order;
order.reserve(builds.size());
for (auto &kv : builds) order.push_back(&kv.second);
std::sort(order.begin(), order.end(),
[](const GroupBuild *a, const GroupBuild *b) { return a->nearest > b->nearest; });
const glm::vec3 corners[4] = {{-0.5f, 0.5f, 0.f}, {0.5f, 0.5f, 0.f},
{0.5f, -0.5f, 0.f}, {-0.5f, -0.5f, 0.f}};
const glm::vec2 quadUVs[4] = {{0.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}, {0.f, 1.f}};
std::vector<float> pos, nrm, uv;
std::vector<uint32_t> idx;
for (GroupBuild *gb : order) {
Group &g = groups_[gb->key];
std::sort(gb->slices.begin(), gb->slices.end(),
[](const GroupBuild::ColoredSlice &a, const GroupBuild::ColoredSlice &b) {
return a.draw.distSq > b.draw.distSq;
});
const int sliceCount = int(gb->slices.size());
const int vc = sliceCount * 4;
const int ic = sliceCount * 6;
pos.resize(size_t(vc) * 3);
nrm.resize(size_t(vc) * 3);
uv.resize(size_t(vc) * 2);
idx.resize(size_t(ic));
for (int si = 0; si < sliceCount; ++si) {
const SpriteStack3D::SliceDraw &s = gb->slices[size_t(si)].draw;
for (int c = 0; c < 4; ++c) {
const glm::vec4 p = s.model * glm::vec4(corners[c], 1.f);
const size_t vi = size_t(si * 4 + c);
pos[vi * 3 + 0] = p.x;
pos[vi * 3 + 1] = p.y;
pos[vi * 3 + 2] = p.z;
nrm[vi * 3 + 0] = 0.f;
nrm[vi * 3 + 1] = 1.f;
nrm[vi * 3 + 2] = 0.f;
uv[vi * 2 + 0] = s.uv.x + quadUVs[c].x * (s.uv.z - s.uv.x);
uv[vi * 2 + 1] = s.uv.y + quadUVs[c].y * (s.uv.w - s.uv.y);
}
const uint32_t base = uint32_t(si * 4);
const size_t di = size_t(si) * 6;
idx[di + 0] = base;
idx[di + 1] = base + 1;
idx[di + 2] = base + 2;
idx[di + 3] = base;
idx[di + 4] = base + 2;
idx[di + 5] = base + 3;
}
if (forceRebuild_ || !g.mesh) {
if (g.mesh) {
gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(),
ic);
} else {
g.mesh =
gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
}
g.vertexCapacity = vc;
g.indexCapacity = ic;
} else if (g.stamp != gb->stamp) {
gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
}
g.stamp = gb->stamp;
gfx->drawMeshShader(g.mesh, glm::mat4(1.f), gb->key.texture, gb->color, shader_);
}
forceRebuild_ = false;
}
// ---------------------------------------------------------------------------
// Module
// ---------------------------------------------------------------------------
Module_IMPL(SpriteStack, new SpriteStack());
SpriteStack3D *SpriteStack::newStack(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newStack: null graphics");
return new SpriteStack3D();
}
SpriteStackBatch *SpriteStack::newBatch(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newBatch: null graphics");
return new SpriteStackBatch();
}
std::vector<image::ImageData *> SpriteStack::slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions opt;
opt.layerCount = layerCount;
opt.imageW = imageW;
opt.imageH = imageH;
opt.axis = axis;
opt.thickness = thickness;
return slicePrimitiveToLayers(kind, opt);
}
std::vector<image::ImageData *> SpriteStack::sliceModel(model3d::ModelData *model, int layerCount,
int imageW, int imageH,
const std::string &axis, float thickness) {
SliceOptions opt;
opt.layerCount = layerCount;
opt.imageW = imageW;
opt.imageH = imageH;
opt.axis = axis;
opt.thickness = thickness;
return sliceModelToLayers(model, opt);
}
void SpriteStack::expose(ssq::Table &table) {
auto cls = table.addClass(name, SpriteStack::create, false);
expose(cls);
auto stack = table.addClass<SpriteStack3D>(
"SpriteStack3D",
std::function<SpriteStack3D *()>([]() -> SpriteStack3D * { return nullptr; }), true);
stack.addFunc("setLayerCount", &SpriteStack3D::setLayerCount);
stack.addFunc("getLayerCount", &SpriteStack3D::getLayerCount);
stack.addFunc("setLayerTexture", &SpriteStack3D::setLayerTexture);
stack.addFunc("getLayerTexture", &SpriteStack3D::getLayerTexture);
stack.addFunc("setLayerImage", &SpriteStack3D::setLayerImage);
stack.addFunc("setLayerFile", &SpriteStack3D::setLayerFile);
stack.addFunc("setLayersFromAtlas", &SpriteStack3D::setLayersFromAtlas);
stack.addFunc("setThickness", &SpriteStack3D::setThickness);
stack.addFunc("getThickness", &SpriteStack3D::getThickness);
stack.addFunc("setSize", &SpriteStack3D::setSize);
stack.addFunc("getWidth", &SpriteStack3D::getWidth);
stack.addFunc("getHeight", &SpriteStack3D::getHeight);
stack.addFunc("setPosition", &SpriteStack3D::setPosition);
stack.addFunc("setYaw", &SpriteStack3D::setYaw);
stack.addFunc("setTint", &SpriteStack3D::setTint);
stack.addFunc("setAlphaCutoff", &SpriteStack3D::setAlphaCutoff);
stack.addFunc("setVisible", &SpriteStack3D::setVisible);
stack.addFunc("getVisible", &SpriteStack3D::getVisible);
stack.addFunc("setMode", &SpriteStack3D::setMode);
stack.addFunc("getMode", &SpriteStack3D::getMode);
stack.addFunc("setShadowEnabled", &SpriteStack3D::setShadowEnabled);
stack.addFunc("getShadowEnabled", &SpriteStack3D::getShadowEnabled);
stack.addFunc("setShadowOpacity", &SpriteStack3D::setShadowOpacity);
stack.addFunc("setShadowLight", &SpriteStack3D::setShadowLight);
stack.addFunc("setShadowPlaneY", &SpriteStack3D::setShadowPlaneY);
stack.addFunc("setOutline", &SpriteStack3D::setOutline);
stack.addFunc("getOutlineWidth", &SpriteStack3D::getOutlineWidth);
stack.addFunc("setOutlineColor", &SpriteStack3D::setOutlineColor);
stack.addFunc("setGbufferEnabled", &SpriteStack3D::setGbufferEnabled);
stack.addFunc("getGbufferEnabled", &SpriteStack3D::getGbufferEnabled);
stack.addFunc("setCastShadow", &SpriteStack3D::setCastShadow);
stack.addFunc("getCastShadow", &SpriteStack3D::getCastShadow);
stack.addFunc(
"render",
std::function<void(SpriteStack3D *, graphics::Graphics *)>([](SpriteStack3D *self,
graphics::Graphics *gfx) {
if (self) self->render(gfx);
}));
stack.addFunc("renderWithCamera", &SpriteStack3D::render);
auto batch = table.addClass<SpriteStackBatch>(
"SpriteStackBatch",
std::function<SpriteStackBatch *()>([]() -> SpriteStackBatch * { return nullptr; }), true);
batch.addFunc("add", &SpriteStackBatch::add);
batch.addFunc("remove", &SpriteStackBatch::remove);
batch.addFunc("clear", &SpriteStackBatch::clear);
batch.addFunc("getStackCount", &SpriteStackBatch::getStackCount);
batch.addFunc(
"render",
std::function<void(SpriteStackBatch *, graphics::Graphics *)>([](SpriteStackBatch *self,
graphics::Graphics *gfx) {
if (self) self->render(gfx);
}));
batch.addFunc("renderWithCamera", &SpriteStackBatch::render);
}
void SpriteStack::expose(ssq::Class &cls) {
cls.addFunc("getName", &SpriteStack::getName);
cls.addFunc("newStack", &SpriteStack::newStack);
cls.addFunc("newBatch", &SpriteStack::newBatch);
cls.addFunc("slicePrimitive", &SpriteStack::slicePrimitive);
cls.addFunc("sliceModel", &SpriteStack::sliceModel);
}
// Legacy 3D renderer removed: SpriteStack is now a pure 2D technique. Keep the
// original implementation excluded temporarily so the slicer above remains a
// reviewable, behavior-preserving move while the 2D renderer lives in its own TU.
} // namespace eve::spritestack
+50 -229
View File
@@ -3,38 +3,20 @@
#include "common/Module.h"
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
namespace eve::graphics {
class Camera3D;
class Camera2D;
class Graphics;
class Mesh;
class Shader;
class Texture;
} // namespace eve::graphics
namespace eve::image {
class ImageData;
} // namespace eve::image
namespace eve::model3d {
class ModelData;
} // namespace eve::model3d
}
namespace eve::image { class ImageData; }
namespace eve::model3d { class ModelData; }
namespace eve::spritestack {
/**
* @brief CPU inputs for slicing a triangle mesh into sprite-stack layers.
*
* Positions are required (vertexCount * 3). Normals / per-vertex colors are
* optional; colors (0..1 RGB) are averaged per triangle, shaded by the face
* normal against the slice view direction, and multiplied by the tint.
*/
/** @brief CPU triangle-mesh input used to bake horizontal RGBA sprite slices. */
struct SliceInput {
const float *posXYZ = nullptr;
const float *nrmXYZ = nullptr;
@@ -44,270 +26,109 @@ struct SliceInput {
int indexCount = 0;
};
/**
* @brief Slicer options. The mesh is cut into `layerCount` thin slabs along `axis`
* ("x" | "y" | "z"); each slab is orthographically projected into an RGBA
* image. Slab thickness defaults to AABB extent / layerCount when 0.
*/
/** @brief Options for baking a mesh into equal-sized RGBA sprite layers. */
struct SliceOptions {
int layerCount = 16;
int imageW = 128;
int imageH = 128;
std::string axis = "z"; // "x" | "y" | "z"
float thickness = 0.f; // world units per layer; 0 = auto from AABB
float padding = 0.04f; // margin around the model in the image plane
bool shade = true; // N*view shading per triangle
float tintR = 1.f;
float tintG = 1.f;
float tintB = 1.f;
std::string axis = "y";
float thickness = 0.f;
float padding = 0.04f;
bool shade = true;
float tintR = 1.f, tintG = 1.f, tintB = 1.f;
};
/**
* @brief Slice a triangle mesh into `layerCount` RGBA8 layer images (caller owns the
* returned ImageData). Classic sprite stacking: vertical "bread slices" along
* the chosen axis, later rendered as camera-facing billboards.
*/
std::vector<image::ImageData *> sliceMeshToLayers(const SliceInput &input,
const SliceOptions &options);
/** @brief Slice every mesh of a decoded model (assimp scene) with the same options. */
std::vector<image::ImageData *> sliceModelToLayers(model3d::ModelData *model,
const SliceOptions &options);
std::vector<image::ImageData *> sliceMeshToLayers(const SliceInput &, const SliceOptions &);
std::vector<image::ImageData *> sliceModelToLayers(model3d::ModelData *, const SliceOptions &);
std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &, const SliceOptions &);
/**
* @brief Build a procedural CPU mesh ("box" | "cylinder" | "sphere" | "cone") and
* slice it — a self-contained asset-free path for tests / demos.
*/
std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &kind,
const SliceOptions &options);
/**
* @brief A renderable pseudo-3D sprite stack: a column of layer textures drawn as
* alpha-blended slices inside the 3D forward pass (call render() after
* gfx.render3D() / RenderSystem3D::render and before present).
* @brief Pure-2D sprite stack made from horizontal RGBA cross-sections.
*
* Modes:
* "vertical" (default) — bread-slice billboards that always face the
* camera; yaw rotates the stack's depth layout around Y.
* "horizontal" — slices are horizontal quads (top-down layers); a 3/4
* camera sees true volume with yaw parallax.
*
* Textures are owned by Graphics; this object only holds pointers. Create via
* eve.SpriteStack().newStack(gfx); the script VM owns the object.
* Layers are drawn bottom-to-top through the normal 2D textured-quad path. Each
* higher layer is offset upward by `thickness` 2D units. Rotation is applied in
* the 2D plane; no Camera3D, depth buffer, G-buffer, mesh, or 3D pass is used.
*/
class SpriteStack3D {
class SpriteStack2D {
public:
SpriteStack3D() = default;
~SpriteStack3D();
SpriteStack3D(const SpriteStack3D &) = delete;
SpriteStack3D &operator=(const SpriteStack3D &) = delete;
/** @brief One slice instance with its baked world transform and layer UV rect. */
struct SliceDraw {
glm::mat4 model{1.f};
glm::vec4 uv{0.f, 0.f, 1.f, 1.f};
float distSq = 0.f;
graphics::Texture *texture = nullptr;
};
SpriteStack2D() = default;
SpriteStack2D(const SpriteStack2D &) = delete;
SpriteStack2D &operator=(const SpriteStack2D &) = delete;
void setLayerCount(int count);
int getLayerCount() const;
/** @brief Null texture clears the slot (that layer is skipped). */
void setLayerTexture(graphics::Texture *texture, int index);
graphics::Texture *getLayerTexture(int index) const;
/** @brief Upload RGBA8 ImageData layers (convenience around setLayerTexture). */
void setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index);
/** @brief Upload each path via Graphics::newTextureFromFile (reloads in place). */
void setLayerFile(graphics::Graphics *gfx, const std::string &path, int index);
/**
* @brief Split one horizontal atlas strip into `layerCount` layers (layer i =
* columns [i/count..(i+1)/count)). The strip stays one GPU texture; slices
* sample their cell through per-layer UV rects.
*/
void setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount);
/** @brief World-space spacing between consecutive slices. */
void setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas, int layerCount);
/** @brief Upward 2D offset between adjacent layers. */
void setThickness(float thickness);
float getThickness() const;
/** @brief Quad size for every slice (world units). Default 1 x 1. */
/** @brief Display size of every layer in 2D units. */
void setSize(float width, float height);
float getWidth() const;
float getHeight() const;
void setPosition(float x, float y, float z);
void setYaw(float radians);
void setPosition(float x, float y);
/** @brief Rotate every slice around its common 2D center, in degrees. */
void setRotation(float degrees);
void setTint(float r, float g, float b, float a = 1.f);
void setAlphaCutoff(float cutoff);
void setVisible(bool visible);
bool getVisible() const;
void setMode(const std::string &mode); // "vertical" | "horizontal"
std::string getMode() const;
/** @brief Pseudo-3D projected contact shadow (soft dark silhouette on the ground). */
/** @brief Draw a cheap 2D contact shadow behind the stack. */
void setShadowEnabled(bool enabled);
bool getShadowEnabled() const;
void setShadowOpacity(float opacity);
void setShadowLight(float dx, float dy, float dz);
void setShadowPlaneY(float y);
/** @brief Stylized rim outline: an expanded dark silhouette behind every slice. */
void setShadowOffset(float x, float y);
/** @brief Draw enlarged dark copies behind the layers. */
void setOutline(float width, float r = 0.f, float g = 0.f, float b = 0.f);
float getOutlineWidth() const;
void setOutlineColor(float r, float g, float b);
/**
* @brief Contribute this stack to the G-buffer (via RenderSystem3D's extra-drawer
* hook) so post-processing that reads depth/normal (AO, outline) recognizes
* the slice silhouettes instead of empty space.
*/
void setGbufferEnabled(bool enabled);
bool getGbufferEnabled() const;
/**
* @brief Cast real CSM shadows: the stack's slices are drawn into the cascaded
* shadow map through the alpha-cutout shadow pipeline, so the silhouette
* (not a solid quad) lands on receiving geometry. Requires a directional
* light with castShadow, or the legacy directional light fallback.
*/
void setCastShadow(bool cast);
bool getCastShadow() const;
/** @brief Monotonic revision counter; SpriteStackBatch uses it to detect changes. */
uint64_t getVersion() const { return version_; }
/**
* @brief Draw all slices into the currently open 3D scene pass (Vulkan). Uses the
* active Camera3D when `camera` is null. Slices are sorted back-to-front
* and depth-tested against the scene (depth writes stay off).
*/
void render(graphics::Graphics *gfx, graphics::Camera3D *camera = nullptr) const;
/** @brief Queue the stack into Graphics' normal 2D renderer. */
void render(graphics::Graphics *gfx) const;
void renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
private:
void ensureResources(graphics::Graphics *gfx) const;
void bumpVersion() { ++version_; }
friend class SpriteStackBatch;
static void collectSlices(const SpriteStack3D &stack, const glm::vec3 &eye,
std::vector<SliceDraw> &out);
static SliceDraw makeShadowDraw(const SpriteStack3D &stack, const SliceDraw &s,
const glm::vec3 &eye);
static SliceDraw makeOutlineDraw(const SpriteStack3D &stack, const SliceDraw &s,
const glm::vec3 &eye);
static void drawGbufferStacks(graphics::Graphics &gfx, const glm::mat4 &viewProj,
float eyeX, float eyeY, float eyeZ, float nearZ, float farZ);
static void registerGbufferDrawer();
static std::vector<SpriteStack3D *> &gbufferStacks();
static void drawShadowCasterStacks(graphics::Graphics &gfx, const glm::mat4 &lightVP,
float eyeX, float eyeY, float eyeZ);
static void registerShadowDrawer();
static std::vector<SpriteStack3D *> &shadowCasterStacks();
struct Layer {
graphics::Texture *texture = nullptr;
glm::vec4 uv{0.f, 0.f, 1.f, 1.f}; // u0, v0, u1, v1
float u0 = 0.f, v0 = 0.f, u1 = 1.f, v1 = 1.f;
};
void draw(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
void bumpVersion() { ++version_; }
std::vector<Layer> layers_;
int layerCount_ = 0;
float thickness_ = 0.1f;
float width_ = 1.f;
float height_ = 1.f;
float x_ = 0.f, y_ = 0.f, z_ = 0.f;
float yaw_ = 0.f;
float thickness_ = 1.f, width_ = 64.f, height_ = 64.f;
float x_ = 0.f, y_ = 0.f, rotation_ = 0.f;
float tintR_ = 1.f, tintG_ = 1.f, tintB_ = 1.f, tintA_ = 1.f;
float alphaCutoff_ = 0.05f;
bool visible_ = true;
std::string mode_ = "vertical";
bool shadowEnabled_ = false;
float shadowOpacity_ = 0.32f;
float shadowLightX_ = 0.35f, shadowLightY_ = -1.f, shadowLightZ_ = 0.25f;
float shadowPlaneY_ = 0.f;
float outlineWidth_ = 0.f;
float outlineR_ = 0.f, outlineG_ = 0.f, outlineB_ = 0.f;
bool gbufferEnabled_ = false;
bool castShadow_ = false;
bool visible_ = true, shadowEnabled_ = false;
float shadowOpacity_ = 0.3f, shadowOffsetX_ = 5.f, shadowOffsetY_ = 4.f;
float outlineWidth_ = 0.f, outlineR_ = 0.f, outlineG_ = 0.f, outlineB_ = 0.f;
uint64_t version_ = 1;
// Lazy GPU resources (owned by Graphics).
mutable graphics::Shader *shader_ = nullptr;
mutable graphics::Mesh *quad_ = nullptr;
};
/**
* @brief Multi-stack batching: draws every visible slice of the registered stacks as
* ONE draw call per (texture, tint) group (all slice transforms and UV rects
* are baked into a shared mesh, uploaded in place via Graphics::updateMeshVertices).
*
* The combined mesh is rebuilt only when a registered stack changes (revision
* counter); per-frame animated stacks should keep using SpriteStack3D::render.
* Shadows / outlines are per-stack features and are not included in batches.
*/
/** @brief Collection of 2D stacks; Graphics performs texture batching. */
class SpriteStackBatch {
public:
SpriteStackBatch() = default;
~SpriteStackBatch() = default;
SpriteStackBatch(const SpriteStackBatch &) = delete;
SpriteStackBatch &operator=(const SpriteStackBatch &) = delete;
void add(SpriteStack3D *stack);
void remove(SpriteStack3D *stack);
void add(SpriteStack2D *stack);
void remove(SpriteStack2D *stack);
void clear();
int getStackCount() const;
void render(graphics::Graphics *gfx, graphics::Camera3D *camera = nullptr);
void render(graphics::Graphics *gfx) const;
void renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const;
private:
struct GroupKey {
graphics::Texture *texture = nullptr;
uint32_t tint = 0; // packed RGBA
bool operator==(const GroupKey &o) const {
return texture == o.texture && tint == o.tint;
}
};
struct GroupKeyHash {
size_t operator()(const GroupKey &k) const {
return std::hash<void *>()(k.texture) ^ (size_t(k.tint) * 0x9e3779b9u);
}
};
struct Group {
graphics::Mesh *mesh = nullptr;
uint64_t stamp = 0;
int vertexCapacity = 0;
int indexCapacity = 0;
};
void ensureShader(graphics::Graphics *gfx);
std::vector<SpriteStack3D *> stacks_;
std::unordered_map<GroupKey, Group, GroupKeyHash> groups_;
bool forceRebuild_ = true; // add/remove/clear invalidate cached group meshes
mutable graphics::Shader *shader_ = nullptr;
std::vector<SpriteStack2D *> stacks_;
};
/**
* @brief SpriteStack module: CPU slicing + SpriteStack3D factory.
*
* Script: `spritestack <- eve.SpriteStack();`
*/
/** @brief SpriteStack module: horizontal slice baking plus pure-2D rendering. */
class SpriteStack : public Module {
public:
Module_REG(SpriteStack);
SpriteStack() = default;
~SpriteStack() override = default;
/** @brief New empty stack (caller/VM owns; set layers before rendering). */
SpriteStack3D *newStack(graphics::Graphics *gfx);
/** @brief New empty batch (caller/VM owns). */
SpriteStack2D *newStack(graphics::Graphics *gfx);
SpriteStackBatch *newBatch(graphics::Graphics *gfx);
std::vector<image::ImageData *> slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis, float thickness);
+279
View File
@@ -0,0 +1,279 @@
#include "spritestack/SpriteStack.h"
#include "common/Exception.h"
#include "graphics/Color.h"
#include "graphics/Graphics.h"
#include "graphics/RenderSystem.h"
#include "image/ImageData.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <simplesquirrel/simplesquirrel.hpp>
namespace eve::spritestack {
namespace {
float clamp01(float value) { return std::max(0.f, std::min(1.f, value)); }
}
void SpriteStack2D::setLayerCount(int count) {
if (count < 0) throw eve::Exception("SpriteStack2D.setLayerCount: count must be >= 0");
layerCount_ = count;
layers_.assign(size_t(count), Layer{});
bumpVersion();
}
int SpriteStack2D::getLayerCount() const { return layerCount_; }
void SpriteStack2D::setLayerTexture(graphics::Texture *texture, int index) {
if (index < 0 || index >= layerCount_)
throw eve::Exception("SpriteStack2D.setLayerTexture: index %d out of range [0,%d)", index,
layerCount_);
layers_[size_t(index)] = Layer{texture, 0.f, 0.f, 1.f, 1.f};
bumpVersion();
}
graphics::Texture *SpriteStack2D::getLayerTexture(int index) const {
if (index < 0 || index >= layerCount_) return nullptr;
return layers_[size_t(index)].texture;
}
void SpriteStack2D::setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayerImage: null graphics");
if (!img) throw eve::Exception("SpriteStack2D.setLayerImage: null ImageData");
setLayerTexture(gfx->newTextureFromImageData(img, false, false), index);
}
void SpriteStack2D::setLayerFile(graphics::Graphics *gfx, const std::string &path, int index) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayerFile: null graphics");
setLayerTexture(gfx->newTextureFromFile(path), index);
}
void SpriteStack2D::setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas,
int layerCount) {
if (!gfx) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: null graphics");
if (!atlas) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: null atlas");
if (layerCount <= 0) throw eve::Exception("SpriteStack2D.setLayersFromAtlas: count must be > 0");
setLayerCount(layerCount);
for (int i = 0; i < layerCount; ++i) {
const float u0 = float(i) / float(layerCount);
const float u1 = float(i + 1) / float(layerCount);
layers_[size_t(i)] = Layer{atlas, u0, 0.f, u1, 1.f};
}
bumpVersion();
}
void SpriteStack2D::setThickness(float thickness) {
if (thickness <= 0.f) throw eve::Exception("SpriteStack2D.setThickness: must be > 0");
thickness_ = thickness;
bumpVersion();
}
float SpriteStack2D::getThickness() const { return thickness_; }
void SpriteStack2D::setSize(float width, float height) {
if (width <= 0.f || height <= 0.f) throw eve::Exception("SpriteStack2D.setSize: size must be > 0");
width_ = width;
height_ = height;
bumpVersion();
}
float SpriteStack2D::getWidth() const { return width_; }
float SpriteStack2D::getHeight() const { return height_; }
void SpriteStack2D::setPosition(float x, float y) {
x_ = x;
y_ = y;
bumpVersion();
}
void SpriteStack2D::setRotation(float degrees) {
rotation_ = degrees;
bumpVersion();
}
void SpriteStack2D::setTint(float r, float g, float b, float a) {
tintR_ = clamp01(r);
tintG_ = clamp01(g);
tintB_ = clamp01(b);
tintA_ = clamp01(a);
bumpVersion();
}
void SpriteStack2D::setVisible(bool visible) { visible_ = visible; }
bool SpriteStack2D::getVisible() const { return visible_; }
void SpriteStack2D::setShadowEnabled(bool enabled) { shadowEnabled_ = enabled; }
bool SpriteStack2D::getShadowEnabled() const { return shadowEnabled_; }
void SpriteStack2D::setShadowOpacity(float opacity) { shadowOpacity_ = clamp01(opacity); }
void SpriteStack2D::setShadowOffset(float x, float y) {
shadowOffsetX_ = x;
shadowOffsetY_ = y;
}
void SpriteStack2D::setOutline(float width, float r, float g, float b) {
outlineWidth_ = std::max(0.f, width);
setOutlineColor(r, g, b);
}
float SpriteStack2D::getOutlineWidth() const { return outlineWidth_; }
void SpriteStack2D::setOutlineColor(float r, float g, float b) {
outlineR_ = clamp01(r);
outlineG_ = clamp01(g);
outlineB_ = clamp01(b);
}
void SpriteStack2D::draw(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!visible_ || layerCount_ <= 0) return;
float cx = x_;
float baseY = y_;
float scale = 1.f;
if (camera) {
const float viewW = float(gfx->getWidth());
const float viewH = float(gfx->getHeight());
cx = camera->worldToScreenX(x_, y_, viewW, viewH);
baseY = camera->worldToScreenY(x_, y_, viewW, viewH);
scale = camera->getZoom();
}
const auto queueLayer = [&](const Layer &layer, float x, float y, float w, float h,
const graphics::Color &tint) {
gfx->drawTexturedRectShaderUVRotated(layer.texture, nullptr, x, y, w, h, rotation_,
layer.u0, layer.v0, layer.u1, layer.v1, tint);
};
if (shadowEnabled_) {
const graphics::Color shadow(0.f, 0.f, 0.f, shadowOpacity_ * tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx + shadowOffsetX_ * scale,
baseY + shadowOffsetY_ * scale - float(i) * thickness_ * scale,
width_ * scale, height_ * scale, shadow);
}
}
if (outlineWidth_ > 0.f) {
const graphics::Color outline(outlineR_, outlineG_, outlineB_, tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx, baseY - float(i) * thickness_ * scale,
width_ * scale + outlineWidth_ * 2.f,
height_ * scale + outlineWidth_ * 2.f, outline);
}
}
const graphics::Color tint(tintR_, tintG_, tintB_, tintA_);
for (int i = 0; i < layerCount_; ++i) {
const Layer &layer = layers_[size_t(i)];
if (!layer.texture) continue;
queueLayer(layer, cx, baseY - float(i) * thickness_ * scale,
width_ * scale, height_ * scale, tint);
}
}
void SpriteStack2D::render(graphics::Graphics *gfx) const {
if (!gfx) throw eve::Exception("SpriteStack2D.render: null graphics");
draw(gfx, nullptr);
}
void SpriteStack2D::renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!gfx) throw eve::Exception("SpriteStack2D.renderWithCamera: null graphics");
draw(gfx, camera);
}
void SpriteStackBatch::add(SpriteStack2D *stack) {
if (!stack) throw eve::Exception("SpriteStackBatch.add: null stack");
if (std::find(stacks_.begin(), stacks_.end(), stack) == stacks_.end()) stacks_.push_back(stack);
}
void SpriteStackBatch::remove(SpriteStack2D *stack) {
stacks_.erase(std::remove(stacks_.begin(), stacks_.end(), stack), stacks_.end());
}
void SpriteStackBatch::clear() { stacks_.clear(); }
int SpriteStackBatch::getStackCount() const { return int(stacks_.size()); }
void SpriteStackBatch::render(graphics::Graphics *gfx) const {
if (!gfx) throw eve::Exception("SpriteStackBatch.render: null graphics");
renderWithCamera(gfx, nullptr);
}
void SpriteStackBatch::renderWithCamera(graphics::Graphics *gfx, graphics::Camera2D *camera) const {
if (!gfx) throw eve::Exception("SpriteStackBatch.renderWithCamera: null graphics");
for (SpriteStack2D *stack : stacks_) if (stack) stack->draw(gfx, camera);
}
Module_IMPL(SpriteStack, new SpriteStack());
SpriteStack2D *SpriteStack::newStack(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newStack: null graphics");
return new SpriteStack2D();
}
SpriteStackBatch *SpriteStack::newBatch(graphics::Graphics *gfx) {
if (!gfx) throw eve::Exception("SpriteStack.newBatch: null graphics");
return new SpriteStackBatch();
}
std::vector<image::ImageData *> SpriteStack::slicePrimitive(const std::string &kind, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions options;
options.layerCount = layerCount;
options.imageW = imageW;
options.imageH = imageH;
options.axis = axis;
options.thickness = thickness;
return slicePrimitiveToLayers(kind, options);
}
std::vector<image::ImageData *> SpriteStack::sliceModel(model3d::ModelData *model, int layerCount,
int imageW, int imageH,
const std::string &axis,
float thickness) {
SliceOptions options;
options.layerCount = layerCount;
options.imageW = imageW;
options.imageH = imageH;
options.axis = axis;
options.thickness = thickness;
return sliceModelToLayers(model, options);
}
void SpriteStack::expose(ssq::Table &table) {
auto cls = table.addClass(name, SpriteStack::create, false);
expose(cls);
auto stack = table.addClass<SpriteStack2D>(
"SpriteStack2D", std::function<SpriteStack2D *()>([]() { return nullptr; }), true);
stack.addFunc("setLayerCount", &SpriteStack2D::setLayerCount);
stack.addFunc("getLayerCount", &SpriteStack2D::getLayerCount);
stack.addFunc("setLayerTexture", &SpriteStack2D::setLayerTexture);
stack.addFunc("getLayerTexture", &SpriteStack2D::getLayerTexture);
stack.addFunc("setLayerImage", &SpriteStack2D::setLayerImage);
stack.addFunc("setLayerFile", &SpriteStack2D::setLayerFile);
stack.addFunc("setLayersFromAtlas", &SpriteStack2D::setLayersFromAtlas);
stack.addFunc("setThickness", &SpriteStack2D::setThickness);
stack.addFunc("getThickness", &SpriteStack2D::getThickness);
stack.addFunc("setSize", &SpriteStack2D::setSize);
stack.addFunc("getWidth", &SpriteStack2D::getWidth);
stack.addFunc("getHeight", &SpriteStack2D::getHeight);
stack.addFunc("setPosition", &SpriteStack2D::setPosition);
stack.addFunc("setRotation", &SpriteStack2D::setRotation);
stack.addFunc("setTint", &SpriteStack2D::setTint);
stack.addFunc("setVisible", &SpriteStack2D::setVisible);
stack.addFunc("getVisible", &SpriteStack2D::getVisible);
stack.addFunc("setShadowEnabled", &SpriteStack2D::setShadowEnabled);
stack.addFunc("getShadowEnabled", &SpriteStack2D::getShadowEnabled);
stack.addFunc("setShadowOpacity", &SpriteStack2D::setShadowOpacity);
stack.addFunc("setShadowOffset", &SpriteStack2D::setShadowOffset);
stack.addFunc("setOutline", &SpriteStack2D::setOutline);
stack.addFunc("getOutlineWidth", &SpriteStack2D::getOutlineWidth);
stack.addFunc("setOutlineColor", &SpriteStack2D::setOutlineColor);
stack.addFunc("render", &SpriteStack2D::render);
stack.addFunc("renderWithCamera", &SpriteStack2D::renderWithCamera);
auto batch = table.addClass<SpriteStackBatch>(
"SpriteStackBatch", std::function<SpriteStackBatch *()>([]() { return nullptr; }), true);
batch.addFunc("add", &SpriteStackBatch::add);
batch.addFunc("remove", &SpriteStackBatch::remove);
batch.addFunc("clear", &SpriteStackBatch::clear);
batch.addFunc("getStackCount", &SpriteStackBatch::getStackCount);
batch.addFunc("render", &SpriteStackBatch::render);
batch.addFunc("renderWithCamera", &SpriteStackBatch::renderWithCamera);
}
void SpriteStack::expose(ssq::Class &cls) {
cls.addFunc("getName", &SpriteStack::getName);
cls.addFunc("newStack", &SpriteStack::newStack);
cls.addFunc("newBatch", &SpriteStack::newBatch);
cls.addFunc("slicePrimitive", &SpriteStack::slicePrimitive);
cls.addFunc("sliceModel", &SpriteStack::sliceModel);
}
} // namespace eve::spritestack
@@ -1,21 +0,0 @@
#version 450
// Sprite-stack slice fragment shader: unlit textured alpha cutout.
// Push constants: data[0] = alphaCutoff.
layout(location = 1) in vec2 vUV;
layout(location = 2) in vec4 vTint;
layout(set = 0, binding = 1) uniform sampler2D albedo;
layout(push_constant) uniform Externals {
vec4 uvRect;
float data[28];
} u;
layout(location = 0) out vec4 outColor;
void main() {
vec4 base = texture(albedo, vUV) * vTint;
if (base.a < u.data[0]) discard;
outColor = base;
}
@@ -1,31 +0,0 @@
#version 450
// Sprite-stack slice vertex shader.
// MeshVertex layout (pos/normal/uv) and Frame UBO prefix match mesh3d, so
// Graphics::drawMeshShader can bind the shared descriptor set.
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV;
layout(set = 0, binding = 0) uniform Frame {
mat4 mvp;
mat4 model;
vec4 lightDirIntensity;
vec4 lightColor;
vec4 tint;
vec4 cameraPos;
} ubo;
layout(push_constant) uniform Externals {
vec4 uvRect;
float data[28];
} u;
layout(location = 1) out vec2 vUV;
layout(location = 2) out vec4 vTint;
void main() {
gl_Position = ubo.mvp * vec4(inPos, 1.0);
vUV = u.uvRect.xy + inUV * (u.uvRect.zw - u.uvRect.xy);
vTint = ubo.tint;
}
@@ -1,44 +0,0 @@
static const uint32_t sprite_stack_frag_spv[] = {
0x07230203, 0x00010000, 0x000d000b, 0x00000030, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0008000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000011, 0x00000015, 0x0000002e,
0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47,
0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576,
0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669,
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, 0x00000009, 0x65736162, 0x00000000,
0x00040005, 0x0000000d, 0x65626c61, 0x00006f64, 0x00030005, 0x00000011, 0x00565576, 0x00040005,
0x00000015, 0x6e695476, 0x00000074, 0x00050005, 0x0000001f, 0x65747845, 0x6c616e72, 0x00000073,
0x00050006, 0x0000001f, 0x00000000, 0x65527675, 0x00007463, 0x00050006, 0x0000001f, 0x00000001,
0x61746164, 0x00000000, 0x00030005, 0x00000021, 0x00000075, 0x00050005, 0x0000002e, 0x4374756f,
0x726f6c6f, 0x00000000, 0x00040047, 0x0000000d, 0x00000021, 0x00000001, 0x00040047, 0x0000000d,
0x00000022, 0x00000000, 0x00040047, 0x00000011, 0x0000001e, 0x00000001, 0x00040047, 0x00000015,
0x0000001e, 0x00000002, 0x00040047, 0x0000001e, 0x00000006, 0x00000004, 0x00030047, 0x0000001f,
0x00000002, 0x00050048, 0x0000001f, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001f,
0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x0000002e, 0x0000001e, 0x00000000, 0x00020013,
0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00090019,
0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c, 0x00000000, 0x0000000b, 0x0004003b,
0x0000000c, 0x0000000d, 0x00000000, 0x00040017, 0x0000000f, 0x00000006, 0x00000002, 0x00040020,
0x00000010, 0x00000001, 0x0000000f, 0x0004003b, 0x00000010, 0x00000011, 0x00000001, 0x00040020,
0x00000014, 0x00000001, 0x00000007, 0x0004003b, 0x00000014, 0x00000015, 0x00000001, 0x00040015,
0x00000018, 0x00000020, 0x00000000, 0x0004002b, 0x00000018, 0x00000019, 0x00000003, 0x00040020,
0x0000001a, 0x00000007, 0x00000006, 0x0004002b, 0x00000018, 0x0000001d, 0x0000001c, 0x0004001c,
0x0000001e, 0x00000006, 0x0000001d, 0x0004001e, 0x0000001f, 0x00000007, 0x0000001e, 0x00040020,
0x00000020, 0x00000009, 0x0000001f, 0x0004003b, 0x00000020, 0x00000021, 0x00000009, 0x00040015,
0x00000022, 0x00000020, 0x00000001, 0x0004002b, 0x00000022, 0x00000023, 0x00000001, 0x0004002b,
0x00000022, 0x00000024, 0x00000000, 0x00040020, 0x00000025, 0x00000009, 0x00000006, 0x00020014,
0x00000028, 0x00040020, 0x0000002d, 0x00000003, 0x00000007, 0x0004003b, 0x0000002d, 0x0000002e,
0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005,
0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003d, 0x0000000b, 0x0000000e, 0x0000000d,
0x0004003d, 0x0000000f, 0x00000012, 0x00000011, 0x00050057, 0x00000007, 0x00000013, 0x0000000e,
0x00000012, 0x0004003d, 0x00000007, 0x00000016, 0x00000015, 0x00050085, 0x00000007, 0x00000017,
0x00000013, 0x00000016, 0x0003003e, 0x00000009, 0x00000017, 0x00050041, 0x0000001a, 0x0000001b,
0x00000009, 0x00000019, 0x0004003d, 0x00000006, 0x0000001c, 0x0000001b, 0x00060041, 0x00000025,
0x00000026, 0x00000021, 0x00000023, 0x00000024, 0x0004003d, 0x00000006, 0x00000027, 0x00000026,
0x000500b8, 0x00000028, 0x00000029, 0x0000001c, 0x00000027, 0x000300f7, 0x0000002b, 0x00000000,
0x000400fa, 0x00000029, 0x0000002a, 0x0000002b, 0x000200f8, 0x0000002a, 0x000100fc, 0x000200f8,
0x0000002b, 0x0004003d, 0x00000007, 0x0000002f, 0x00000009, 0x0003003e, 0x0000002e, 0x0000002f,
0x000100fd, 0x00010038,
};
static const size_t sprite_stack_frag_spv_count = 322;
@@ -1,75 +0,0 @@
static const uint32_t sprite_stack_vert_spv[] = {
0x07230203, 0x00010000, 0x000d000b, 0x00000041, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x000b000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025,
0x00000030, 0x0000003b, 0x00000040, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47,
0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576,
0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669,
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265,
0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69,
0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006,
0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000000b,
0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000,
0x00040005, 0x00000011, 0x6d617246, 0x00000065, 0x00040006, 0x00000011, 0x00000000, 0x0070766d,
0x00050006, 0x00000011, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x00000011, 0x00000002,
0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x00000011, 0x00000003,
0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x00000011, 0x00000004, 0x746e6974, 0x00000000,
0x00060006, 0x00000011, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00030005, 0x00000013,
0x006f6275, 0x00040005, 0x00000019, 0x6f506e69, 0x00000073, 0x00030005, 0x00000025, 0x00565576,
0x00050005, 0x00000028, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, 0x00000028, 0x00000000,
0x65527675, 0x00007463, 0x00050006, 0x00000028, 0x00000001, 0x61746164, 0x00000000, 0x00030005,
0x0000002a, 0x00000075, 0x00040005, 0x00000030, 0x56556e69, 0x00000000, 0x00040005, 0x0000003b,
0x6e695476, 0x00000074, 0x00050005, 0x00000040, 0x6f4e6e69, 0x6c616d72, 0x00000000, 0x00030047,
0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048,
0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, 0x0000000b,
0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011,
0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011, 0x00000000,
0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, 0x00000023, 0x00000000, 0x00040048,
0x00000011, 0x00000001, 0x00000005, 0x00050048, 0x00000011, 0x00000001, 0x00000007, 0x00000010,
0x00050048, 0x00000011, 0x00000001, 0x00000023, 0x00000040, 0x00050048, 0x00000011, 0x00000002,
0x00000023, 0x00000080, 0x00050048, 0x00000011, 0x00000003, 0x00000023, 0x00000090, 0x00050048,
0x00000011, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, 0x00000011, 0x00000005, 0x00000023,
0x000000b0, 0x00040047, 0x00000013, 0x00000021, 0x00000000, 0x00040047, 0x00000013, 0x00000022,
0x00000000, 0x00040047, 0x00000019, 0x0000001e, 0x00000000, 0x00040047, 0x00000025, 0x0000001e,
0x00000001, 0x00040047, 0x00000027, 0x00000006, 0x00000004, 0x00030047, 0x00000028, 0x00000002,
0x00050048, 0x00000028, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000028, 0x00000001,
0x00000023, 0x00000010, 0x00040047, 0x00000030, 0x0000001e, 0x00000002, 0x00040047, 0x0000003b,
0x0000001e, 0x00000002, 0x00040047, 0x00000040, 0x0000001e, 0x00000001, 0x00020013, 0x00000002,
0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008,
0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, 0x0006001e, 0x0000000b,
0x00000007, 0x00000006, 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b,
0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001,
0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040018, 0x00000010, 0x00000007, 0x00000004,
0x0008001e, 0x00000011, 0x00000010, 0x00000010, 0x00000007, 0x00000007, 0x00000007, 0x00000007,
0x00040020, 0x00000012, 0x00000002, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, 0x00000002,
0x00040020, 0x00000014, 0x00000002, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000003,
0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, 0x00000019, 0x00000001,
0x0004002b, 0x00000006, 0x0000001b, 0x3f800000, 0x00040020, 0x00000021, 0x00000003, 0x00000007,
0x00040017, 0x00000023, 0x00000006, 0x00000002, 0x00040020, 0x00000024, 0x00000003, 0x00000023,
0x0004003b, 0x00000024, 0x00000025, 0x00000003, 0x0004002b, 0x00000008, 0x00000026, 0x0000001c,
0x0004001c, 0x00000027, 0x00000006, 0x00000026, 0x0004001e, 0x00000028, 0x00000007, 0x00000027,
0x00040020, 0x00000029, 0x00000009, 0x00000028, 0x0004003b, 0x00000029, 0x0000002a, 0x00000009,
0x00040020, 0x0000002b, 0x00000009, 0x00000007, 0x00040020, 0x0000002f, 0x00000001, 0x00000023,
0x0004003b, 0x0000002f, 0x00000030, 0x00000001, 0x0004003b, 0x00000021, 0x0000003b, 0x00000003,
0x0004002b, 0x0000000e, 0x0000003c, 0x00000004, 0x00040020, 0x0000003d, 0x00000002, 0x00000007,
0x0004003b, 0x00000018, 0x00000040, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f,
0x0004003d, 0x00000010, 0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019,
0x00050051, 0x00000006, 0x0000001c, 0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d,
0x0000001a, 0x00000001, 0x00050051, 0x00000006, 0x0000001e, 0x0000001a, 0x00000002, 0x00070050,
0x00000007, 0x0000001f, 0x0000001c, 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007,
0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, 0x00000022, 0x0000000d, 0x0000000f,
0x0003003e, 0x00000022, 0x00000020, 0x00050041, 0x0000002b, 0x0000002c, 0x0000002a, 0x0000000f,
0x0004003d, 0x00000007, 0x0000002d, 0x0000002c, 0x0007004f, 0x00000023, 0x0000002e, 0x0000002d,
0x0000002d, 0x00000000, 0x00000001, 0x0004003d, 0x00000023, 0x00000031, 0x00000030, 0x00050041,
0x0000002b, 0x00000032, 0x0000002a, 0x0000000f, 0x0004003d, 0x00000007, 0x00000033, 0x00000032,
0x0007004f, 0x00000023, 0x00000034, 0x00000033, 0x00000033, 0x00000002, 0x00000003, 0x00050041,
0x0000002b, 0x00000035, 0x0000002a, 0x0000000f, 0x0004003d, 0x00000007, 0x00000036, 0x00000035,
0x0007004f, 0x00000023, 0x00000037, 0x00000036, 0x00000036, 0x00000000, 0x00000001, 0x00050083,
0x00000023, 0x00000038, 0x00000034, 0x00000037, 0x00050085, 0x00000023, 0x00000039, 0x00000031,
0x00000038, 0x00050081, 0x00000023, 0x0000003a, 0x0000002e, 0x00000039, 0x0003003e, 0x00000025,
0x0000003a, 0x00050041, 0x0000003d, 0x0000003e, 0x00000013, 0x0000003c, 0x0004003d, 0x00000007,
0x0000003f, 0x0000003e, 0x0003003e, 0x0000003b, 0x0000003f, 0x000100fd, 0x00010038,
};
static const size_t sprite_stack_vert_spv_count = 575;
+133 -923
View File
File diff suppressed because it is too large. Load diff