diff --git a/cmake/module_manifest.cmake b/cmake/module_manifest.cmake index 6a8d6082..92f9777f 100644 --- a/cmake/module_manifest.cmake +++ b/cmake/module_manifest.cmake @@ -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 diff --git a/docs/usr/modules/spritestack.md b/docs/usr/modules/spritestack.md index 56bab1d0..10f5c4b4 100644 --- a/docs/usr/modules/spritestack.md +++ b/docs/usr/modules/spritestack.md @@ -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`。 diff --git a/examples/sprite-stack/README.md b/examples/sprite-stack/README.md index 1d1fd24e..a9e88ffd 100644 --- a/examples/sprite-stack/README.md +++ b/examples/sprite-stack/README.md @@ -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` 提交,共享同一组切片纹理。 diff --git a/examples/sprite-stack/main.nut b/examples/sprite-stack/main.nut index c8ff34f9..6e33eafe 100644 --- a/examples/sprite-stack/main.nut +++ b/examples/sprite-stack/main.nut @@ -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); } diff --git a/scripts/compile_spritestack_shaders.py b/scripts/compile_spritestack_shaders.py deleted file mode 100644 index 9f89da1b..00000000 --- a/scripts/compile_spritestack_shaders.py +++ /dev/null @@ -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()) diff --git a/src/modules/spritestack/SpriteStack.cpp b/src/modules/spritestack/SpriteStack.cpp index cace5c2b..10296bd2 100644 --- a/src/modules/spritestack/SpriteStack.cpp +++ b/src/modules/spritestack/SpriteStack.cpp @@ -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 #include #include #include -#include -#include -#include #include #include #include @@ -405,763 +393,7 @@ std::vector slicePrimitiveToLayers(const std::string &kind, } // --------------------------------------------------------------------------- -// SpriteStack3D -// --------------------------------------------------------------------------- - -namespace { - -std::vector copySpv(const uint32_t *data, size_t count) { - return std::vector(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() == nullptr) return nullptr; - auto camView = ecs::View(); - 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 &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::gbufferStacks() { - static std::vector 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 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::shadowCasterStacks() { - static std::vector 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 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 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 slices; - uint64_t stamp = 0; - float nearest = 1e30f; - eve::graphics::Color color{1.f, 1.f, 1.f, 1.f}; - }; - std::unordered_map builds; - for (SpriteStack3D *st : stacks_) { - if (!st || !st->visible_) continue; - std::vector tmp; - SpriteStack3D::collectSlices(*st, eye, tmp); - const uint64_t stamp = st->getVersion(); - std::vector 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 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 pos, nrm, uv; - std::vector 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 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 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", - std::function([]() -> 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([](SpriteStack3D *self, - graphics::Graphics *gfx) { - if (self) self->render(gfx); - })); - stack.addFunc("renderWithCamera", &SpriteStack3D::render); - - auto batch = table.addClass( - "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", - std::function([](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 diff --git a/src/modules/spritestack/SpriteStack.h b/src/modules/spritestack/SpriteStack.h index 673a201a..291db301 100644 --- a/src/modules/spritestack/SpriteStack.h +++ b/src/modules/spritestack/SpriteStack.h @@ -3,38 +3,20 @@ #include "common/Module.h" #include -#include #include -#include #include -#include - 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 sliceMeshToLayers(const SliceInput &input, - const SliceOptions &options); - -/** @brief Slice every mesh of a decoded model (assimp scene) with the same options. */ -std::vector sliceModelToLayers(model3d::ModelData *model, - const SliceOptions &options); +std::vector sliceMeshToLayers(const SliceInput &, const SliceOptions &); +std::vector sliceModelToLayers(model3d::ModelData *, const SliceOptions &); +std::vector 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 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 &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 &gbufferStacks(); - static void drawShadowCasterStacks(graphics::Graphics &gfx, const glm::mat4 &lightVP, - float eyeX, float eyeY, float eyeZ); - static void registerShadowDrawer(); - static std::vector &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 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()(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 stacks_; - std::unordered_map groups_; - bool forceRebuild_ = true; // add/remove/clear invalidate cached group meshes - mutable graphics::Shader *shader_ = nullptr; + std::vector 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 slicePrimitive(const std::string &kind, int layerCount, int imageW, int imageH, const std::string &axis, float thickness); diff --git a/src/modules/spritestack/SpriteStack2D.cpp b/src/modules/spritestack/SpriteStack2D.cpp new file mode 100644 index 00000000..85d96737 --- /dev/null +++ b/src/modules/spritestack/SpriteStack2D.cpp @@ -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 +#include +#include +#include + +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 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 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", std::function([]() { 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", std::function([]() { 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 diff --git a/src/modules/spritestack/shaders/sprite_stack.frag b/src/modules/spritestack/shaders/sprite_stack.frag deleted file mode 100644 index d15c0477..00000000 --- a/src/modules/spritestack/shaders/sprite_stack.frag +++ /dev/null @@ -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; -} diff --git a/src/modules/spritestack/shaders/sprite_stack.vert b/src/modules/spritestack/shaders/sprite_stack.vert deleted file mode 100644 index 789486ba..00000000 --- a/src/modules/spritestack/shaders/sprite_stack.vert +++ /dev/null @@ -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; -} diff --git a/src/modules/spritestack/shaders/sprite_stack_frag_spv.inc b/src/modules/spritestack/shaders/sprite_stack_frag_spv.inc deleted file mode 100644 index 2186bbb7..00000000 --- a/src/modules/spritestack/shaders/sprite_stack_frag_spv.inc +++ /dev/null @@ -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; diff --git a/src/modules/spritestack/shaders/sprite_stack_vert_spv.inc b/src/modules/spritestack/shaders/sprite_stack_vert_spv.inc deleted file mode 100644 index 5b6a7583..00000000 --- a/src/modules/spritestack/shaders/sprite_stack_vert_spv.inc +++ /dev/null @@ -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; diff --git a/test/spritestack.cpp b/test/spritestack.cpp index 9ca47ee0..090df494 100644 --- a/test/spritestack.cpp +++ b/test/spritestack.cpp @@ -1,967 +1,177 @@ #include "zeroerr/assert.h" #include "zeroerr/unittest.h" -#include +#include "common/Exception.h" +#include "filesystem/FileData.h" +#include "graphics/Color.h" +#include "graphics/Graphics.h" +#include "image/Image.h" +#include "image/ImageData.h" +#include "spritestack/SpriteStack.h" +#include "window/Window.h" + #include #include #include #include #include -#include -#include +#include -#include "common/Exception.h" -#include "filesystem/FileData.h" -#include "graphics/AmbientOcclusion.h" -#include "graphics/AntiAliasing.h" -#include "graphics/Canvas.h" -#include "graphics/DrawItem2D.h" -#include "graphics/Font.h" -#include "graphics/GBuffer.h" -#include "graphics/GlobalIllumination.h" -#include "graphics/Graphics.h" -#include "graphics/Grass.h" -#include "graphics/Light.h" -#include "graphics/Material.h" -#include "graphics/Mesh.h" -#include "graphics/Outline.h" -#include "graphics/Quad.h" -#include "graphics/RenderControl.h" -#include "graphics/RenderSystem.h" -#include "graphics/RenderSystem3D.h" -#include "graphics/ScreenSpaceReflection.h" -#include "graphics/Shader.h" -#include "graphics/Texture.h" -#include "graphics/Volumetric.h" -#include "graphics/Water.h" -#include "graphics/Waterfall.h" -#include "image/Image.h" -#include "image/ImageData.h" -#include "spritestack/SpriteStack.h" -#include "window/Window.h" -// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form. using eve::graphics::Color; - -using eve::graphics::Camera3D; using eve::graphics::Graphics; -using eve::graphics::Light3D; -using eve::graphics::Renderable2D; -using eve::graphics::Renderable3D; -using eve::graphics::RenderSystem; -using eve::graphics::RenderSystem3D; -using eve::graphics::Texture; using eve::image::ImageData; using eve::spritestack::SpriteStack; +using eve::spritestack::SpriteStack2D; using eve::spritestack::SpriteStackBatch; -using eve::spritestack::SpriteStack3D; using Colorf = ImageData::Colorf; namespace { - int countAlpha(const ImageData *img) { - int n = 0; - const int w = img->getWidth(); - const int h = img->getHeight(); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (img->getPixel(x, y).a > 0.5f) ++n; - return n; -} - -bool centerCovered(const ImageData *img) { - const int w = img->getWidth(); - const int h = img->getHeight(); - return img->getPixel(w / 2, h / 2).a > 0.5f; + int count = 0; + for (int y = 0; y < img->getHeight(); ++y) + for (int x = 0; x < img->getWidth(); ++x) + if (img->getPixel(x, y).a > 0.5f) ++count; + return count; } std::string testOutDir() { return std::string(EVENGINE_TEST_BINARY_DIR) + "/out"; } -void saveImageDataPng(ImageData *frame, const std::string &path) { +void savePng(ImageData *frame, const std::string &path) { REQUIRE(frame != nullptr); eve::image::Image::create(); - eve::filesystem::FileData *png = - frame->encode(eve::image::ImageData::FormatHandler::ENCODED_PNG, "spritestack.png", false); - REQUIRE(png != nullptr); - REQUIRE(png->getSize() > 0); - + std::unique_ptr png( + frame->encode(ImageData::FormatHandler::ENCODED_PNG, "spritestack2d.png", false)); + REQUIRE(png.get() != nullptr); std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - { - std::ofstream out(path, std::ios::binary); - REQUIRE(out.good()); - out.write(static_cast(png->getData()), - static_cast(png->getSize())); - REQUIRE(out.good()); - } - delete png; - std::printf("spritestack render saved: %s\n", path.c_str()); + std::ofstream out(path, std::ios::binary); + out.write(static_cast(png->getData()), std::streamsize(png->getSize())); + REQUIRE(out.good()); } -void resetScene3D() { - if (ecs::current()->getManager() != nullptr) { - auto view = ecs::View(); - for (auto it = view.begin(); it != view.end(); ++it) { - auto [mr] = *it; - mr->visible = false; - } +struct WindowFixture { + eve::window::Window *win = eve::window::Window::create(); + Graphics *gfx = Graphics::create(); + WindowFixture() { + REQUIRE(win != nullptr); + REQUIRE(gfx != nullptr); + eve::window::WindowSettings settings; + settings.width = 320; + settings.height = 240; + settings.centered = true; + REQUIRE(win->setWindowSettings(settings)); + gfx->setScreenReadbackEnabled(true); + gfx->setBackgroundColor(Color(0.07f, 0.09f, 0.12f, 1.f)); } - if (ecs::current()->getManager() != nullptr) { - auto camView = ecs::View(); - for (auto it = camView.begin(); it != camView.end(); ++it) { - auto [data] = *it; - data->active = false; - } - } - if (ecs::current()->getManager() != nullptr) { - auto view = ecs::View(); - for (auto it = view.begin(); it != view.end(); ++it) { - auto [sp] = *it; - sp->visible = false; - } - } -} + ~WindowFixture() { win->close(); } +}; -} // namespace +std::unique_ptr renderStack(Graphics *gfx, const SpriteStack2D &stack) { + for (int i = 0; i < 3; ++i) { + gfx->clear(std::nullopt, std::nullopt, std::nullopt); + stack.render(gfx); + gfx->present(); + } + return std::unique_ptr(gfx->newImageData()); +} +} TEST_CASE("spritestack.slice.boxLayers") { - auto *mod = SpriteStack::create(); - REQUIRE(mod != nullptr); - CHECK_EQ(mod->getName(), std::string("SpriteStack")); - - std::vector layers = - mod->slicePrimitive("box", 6, 48, 48, "z", 0.f); + auto layers = SpriteStack::create()->slicePrimitive("box", 6, 48, 48, "y", 0.f); REQUIRE_EQ(layers.size(), size_t(6)); - for (auto *img : layers) { - REQUIRE(img != nullptr); - CHECK_EQ(img->getWidth(), 48); - CHECK_EQ(img->getHeight(), 48); - CHECK_EQ(img->getFormat(), std::string("RGBA8")); - delete img; + for (ImageData *layer : layers) { + REQUIRE(layer != nullptr); + CHECK_GT(countAlpha(layer), 100); + delete layer; } } TEST_CASE("spritestack.slice.sphereVariesAcrossLayers") { - // A sphere sliced along Z: middle slabs are wide, outer slabs are thin. - std::vector layers = - SpriteStack::create()->slicePrimitive("sphere", 8, 64, 64, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(8)); - - const int middle = countAlpha(layers[3]); - const int outer = countAlpha(layers[0]); - CHECK_GT(middle, 0); - CHECK_GT(outer, 0); - CHECK_GT(middle, outer); // center slab covers more area than the cap slab - CHECK(centerCovered(layers[3])); - - for (auto *img : layers) delete img; + auto layers = SpriteStack::create()->slicePrimitive("sphere", 9, 64, 64, "y", 0.f); + REQUIRE_EQ(layers.size(), size_t(9)); + const int edge = countAlpha(layers.front()); + const int middle = countAlpha(layers[layers.size() / 2]); + CHECK_GT(middle, edge); + for (ImageData *layer : layers) delete layer; } -TEST_CASE("spritestack.slice.axisYTopDown") { - // Top-down slicing ("y" axis) of a cylinder yields circular layers. - std::vector layers = - SpriteStack::create()->slicePrimitive("cylinder", 6, 64, 64, "y", 0.f); - REQUIRE_EQ(layers.size(), size_t(6)); - for (int i = 0; i < 6; ++i) { - REQUIRE(layers[i] != nullptr); - // Every horizontal cut of a full cylinder covers the center. - CHECK(centerCovered(layers[i])); - delete layers[i]; - } -} - -TEST_CASE("spritestack.slice.meshArrays") { - // 2x2x2 box built from raw arrays (unit corners), sliced along Z. - const float h = 1.f; - const float pos[] = { - -h, -h, -h, h, -h, -h, h, h, -h, -h, h, -h, - -h, -h, h, h, -h, h, h, h, h, -h, h, h, - }; - const uint32_t idx[] = {0, 1, 2, 0, 2, 3, 5, 4, 7, 5, 7, 6, - 4, 0, 3, 4, 3, 7, 1, 5, 6, 1, 6, 2, - 4, 5, 1, 4, 1, 0, 3, 2, 6, 3, 6, 7}; - eve::spritestack::SliceInput in{}; - in.posXYZ = pos; - in.vertexCount = 8; - in.indices = idx; - in.indexCount = int(sizeof(idx) / sizeof(idx[0])); - - eve::spritestack::SliceOptions opt; - opt.layerCount = 4; - opt.imageW = 32; - opt.imageH = 32; - opt.thickness = 0.5f; // box depth 2 / 4 slabs - std::vector layers = eve::spritestack::sliceMeshToLayers(in, opt); - REQUIRE_EQ(layers.size(), size_t(4)); - // Every slab of a full box covers its center column. - for (auto *img : layers) { - REQUIRE(img != nullptr); - CHECK(centerCovered(img)); - delete img; - } - - bool threw = false; - try { - eve::spritestack::SliceInput bad{}; - bad.posXYZ = pos; - bad.vertexCount = 0; - bad.indices = idx; - bad.indexCount = 3; - (void)eve::spritestack::sliceMeshToLayers(bad, opt); - } catch (const eve::Exception &) { - threw = true; - } - CHECK(threw); -} - -TEST_CASE("spritestack.render.verticalStack") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.0f, 2.2f, 4.0f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - cam->data()->ambientR = 0.30f; - cam->data()->ambientG = 0.32f; - cam->data()->ambientB = 0.36f; - - // Slices are self-contained: no Renderable3D needed in the scene. - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->r = 0.f; - hud->sprite()->g = 0.f; - hud->sprite()->b = 0.f; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.14f, 0.16f, 0.20f, 1.f)); - RenderSystem3D::setDirectionalLight(0.55f, 0.95f, 1.15f, 1.05f, 1.0f, 0.95f); - std::vector layers = - SpriteStack::create()->slicePrimitive("cylinder", 16, 96, 96, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(16)); - - SpriteStack3D stack; - stack.setLayerCount(16); - for (int i = 0; i < 16; ++i) stack.setLayerImage(gfx, layers[i], i); - stack.setThickness(0.16f); - stack.setSize(1.6f, 1.6f); - stack.setPosition(0.f, 0.f, 0.f); - stack.setTint(0.35f, 0.78f, 0.72f, 1.f); - auto renderFrame = [&](float yaw) -> ImageData * { - resetScene3D(); - cam->data()->active = true; - stack.setYaw(yaw); - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - return gfx->newImageData(); - }; - - std::unique_ptr frameA(renderFrame(0.0f)); - REQUIRE(frameA.get() != nullptr); - saveImageDataPng(frameA.get(), testOutDir() + "/sprite_stack_vertical.png"); - - // The stacked cylinder should paint a visible blob near the screen center. - int lit = 0; - const int w = frameA->getWidth(); - const int h = frameA->getHeight(); - for (int y = h / 4; y < 3 * h / 4; ++y) { - for (int x = w / 4; x < 3 * w / 4; ++x) { - Colorf c = frameA->getPixel(x, y); - if (c.r + c.g + c.b > 0.25f && c.a > 0.5f) ++lit; - } - } - CHECK_GT(lit, 400); - - // Rotating the stack around Y must change the rendered pixels. - std::unique_ptr frameB(renderFrame(1.3f)); - REQUIRE(frameB.get() != nullptr); - int diff = 0; - for (int y = 0; y < h; ++y) { - for (int x = 0; x < w; ++x) { - Colorf a = frameA->getPixel(x, y); - Colorf b = frameB->getPixel(x, y); - if (std::fabs(a.r - b.r) + std::fabs(a.g - b.g) + std::fabs(a.b - b.b) > 0.08f) ++diff; - } - } - CHECK_GT(diff, 64); - - for (auto *img : layers) delete img; - win->close(); -} - -TEST_CASE("spritestack.render.gbufferOutline") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.2f, 2.8f, 4.2f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - // A mesh so the GBuffer pass has content and the outline pass runs. - auto *ground = Renderable3D::create(); - ground->meshRenderer()->mesh = gfx->newMeshCylinder(32, 1, true); - ground->transform()->x = 0.f; - ground->transform()->y = -1.5f; - ground->transform()->sx = 6.f; - ground->transform()->sy = 0.1f; - ground->transform()->sz = 6.f; - ground->setTint(0.20f, 0.26f, 0.22f, 1.f); - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.12f, 0.13f, 0.16f, 1.f)); - gfx->getRenderControl()->enable("outline"); - - std::vector layers = - SpriteStack::create()->slicePrimitive("cone", 12, 96, 96, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(12)); - - SpriteStack3D stack; - stack.setLayerCount(12); - for (int i = 0; i < 12; ++i) stack.setLayerImage(gfx, layers[i], i); - stack.setThickness(0.13f); - stack.setSize(1.5f, 1.6f); - stack.setPosition(0.f, 0.2f, 0.f); - stack.setTint(0.45f, 0.55f, 0.35f, 1.f); - - auto renderFrame = [&]() -> ImageData * { - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - return gfx->newImageData(); - }; - - // Without G-buffer participation the outline pass cannot see the slices. - std::unique_ptr noG(renderFrame()); - REQUIRE(noG.get() != nullptr); - - stack.setGbufferEnabled(true); - std::unique_ptr withG(renderFrame()); - REQUIRE(withG.get() != nullptr); - saveImageDataPng(withG.get(), testOutDir() + "/sprite_stack_gbuffer_outline.png"); - - // Outline pass paints a dark blue-ish rim (0.05,0.04,0.07) around the stack. - int rimNoG = 0, rimG = 0; - const int w = withG->getWidth(); - const int h = withG->getHeight(); - auto countRim = [&](ImageData *f) { - int n = 0; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - Colorf c = f->getPixel(x, y); - if (std::fabs(c.r - 0.05f) < 0.05f && std::fabs(c.g - 0.04f) < 0.05f && - std::fabs(c.b - 0.07f) < 0.05f && c.r + c.g + c.b < 0.3f) - ++n; - } - return n; - }; - rimNoG = countRim(noG.get()); - rimG = countRim(withG.get()); - CHECK_GT(rimG, rimNoG + 20); - - stack.setGbufferEnabled(false); - for (auto *img : layers) delete img; - win->close(); -} - -TEST_CASE("spritestack.render.batchShadow") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.2f, 2.8f, 4.2f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - auto *ground = Renderable3D::create(); - ground->meshRenderer()->mesh = gfx->newMeshCylinder(32, 1, true); - ground->transform()->x = 0.f; - ground->transform()->y = -1.5f; - ground->transform()->sx = 6.f; - ground->transform()->sy = 0.1f; - ground->transform()->sz = 6.f; - ground->setTint(0.20f, 0.26f, 0.22f, 1.f); - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.12f, 0.13f, 0.16f, 1.f)); - - std::vector layers = - SpriteStack::create()->slicePrimitive("sphere", 10, 64, 64, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(10)); - std::vector texs; - for (auto *img : layers) { - texs.push_back(gfx->newTexture(img)); - delete img; - } - - auto *a = new SpriteStack3D(); - auto *b = new SpriteStack3D(); - for (auto *st : {a, b}) { - st->setLayerCount(10); - for (int i = 0; i < 10; ++i) st->setLayerTexture(texs[size_t(i)], i); - st->setThickness(0.14f); - st->setSize(0.8f, 0.8f); - } - a->setPosition(-1.0f, 0.f, 0.f); - a->setTint(0.8f, 0.45f, 0.25f, 1.f); - b->setPosition(1.0f, 0.f, 0.f); - b->setTint(0.3f, 0.65f, 0.85f, 1.f); - - SpriteStackBatch batch; - batch.add(a); - batch.add(b); - b->setShadowEnabled(true); - b->setShadowOpacity(0.45f); - b->setShadowLight(0.35f, -1.f, 0.25f); - b->setShadowPlaneY(-1.45f); - - auto renderFrame = [&]() -> ImageData * { - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - batch.render(gfx); - RenderSystem::render(*gfx); - } - return gfx->newImageData(); - }; - - std::unique_ptr plain(renderFrame()); - REQUIRE(plain.get() != nullptr); - b->setShadowEnabled(false); - std::unique_ptr noShadow(renderFrame()); - REQUIRE(noShadow.get() != nullptr); - - // With the shadow batched in, dark pixels appear on the ground. - int dark = 0; - const int w = plain->getWidth(); - const int h = plain->getHeight(); - for (int y = 0; y < h; ++y) { - for (int x = 0; x < w; ++x) { - Colorf a = noShadow->getPixel(x, y); - Colorf b = plain->getPixel(x, y); - if ((a.r + a.g + a.b) - (b.r + b.g + b.b) > 0.12f) ++dark; - } - } - CHECK_GT(dark, 50); - - delete a; - delete b; - win->close(); -} - -TEST_CASE("spritestack.render.atlasStrip") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.0f, 2.2f, 4.0f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.14f, 0.16f, 0.20f, 1.f)); - - // Build a horizontal atlas strip: 6 cells, each a filled circle whose - // radius shrinks toward the last cell (a cone cross-section set). - const int cell = 64; - const int layers = 6; - auto *strip = new ImageData(cell * layers, cell, "RGBA8"); - for (int li = 0; li < layers; ++li) { - const float t = float(li) / float(layers - 1); - const float radius = 0.42f * (1.f - 0.75f * t); - for (int y = 0; y < cell; ++y) { - for (int x = 0; x < cell; ++x) { - const float nx = (float(x) + 0.5f) / float(cell) - 0.5f; - const float ny = (float(y) + 0.5f) / float(cell) - 0.5f; - Colorf c{0.f, 0.f, 0.f, 0.f}; - if (nx * nx + ny * ny <= radius * radius) - c = Colorf{0.5f + 0.4f * t, 0.7f - 0.35f * t, 0.35f + 0.3f * t, 1.f}; - strip->setPixel(li * cell + x, y, c); - } - } - } - Texture *atlas = gfx->newTexture(strip); - delete strip; - REQUIRE(atlas != nullptr); - - SpriteStack3D stack; - stack.setLayersFromAtlas(gfx, atlas, layers); - stack.setThickness(0.16f); - stack.setSize(1.6f, 1.6f); - stack.setPosition(0.f, 0.f, 0.f); - stack.setTint(1.f, 1.f, 1.f, 1.f); - - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - std::unique_ptr frame(gfx->newImageData()); - REQUIRE(frame.get() != nullptr); - saveImageDataPng(frame.get(), testOutDir() + "/sprite_stack_atlas.png"); - - int lit = 0; - const int w = frame->getWidth(); - const int h = frame->getHeight(); - for (int y = h / 4; y < 3 * h / 4; ++y) { - for (int x = w / 4; x < 3 * w / 4; ++x) { - Colorf c = frame->getPixel(x, y); - if (c.r + c.g + c.b > 0.25f && c.a > 0.5f) ++lit; - } - } - CHECK_GT(lit, 300); - - win->close(); -} - -TEST_CASE("spritestack.render.csmShadow") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 320; - s.height = 240; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setEye(0.f, 3.5f, 5.5f); - cam->setTarget(0.f, 0.f, 0.f); - cam->setAmbient(0.06f, 0.06f, 0.07f); - - // Receiving ground + a directional CSM light (mirrors Shadow3D setup). - auto *ground = Renderable3D::create(); - ground->meshRenderer()->mesh = gfx->newMeshSphere(16, 8); - const uint8_t gray[4] = {219, 219, 219, 255}; - ground->meshRenderer()->texture = gfx->newTexture(1, 1, gray); - ground->transform()->x = 0.f; - ground->transform()->y = -1.2f; - ground->transform()->sx = 4.f; - ground->transform()->sy = 0.15f; - ground->transform()->sz = 4.f; - ground->setCastShadow(false); - ground->setReceiveShadow(true); - ground->setRoughness(0.85f); - - auto *sun = Light3D::createLight("dir"); - sun->setDirection(0.55f, 1.f, 0.35f); - sun->setColor(1.f, 1.f, 1.f, 2.5f); - sun->setCastShadow(true); - sun->setShadowStrength(1.f); - sun->setShadowBias(0.003f); - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.10f, 0.11f, 0.14f, 1.f)); - - std::vector layers = - SpriteStack::create()->slicePrimitive("cone", 12, 96, 96, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(12)); - - SpriteStack3D stack; - stack.setLayerCount(12); - for (int i = 0; i < 12; ++i) stack.setLayerImage(gfx, layers[i], i); - stack.setThickness(0.13f); - stack.setSize(1.4f, 1.5f); - stack.setPosition(0.f, 0.2f, 0.f); - stack.setTint(0.45f, 0.55f, 0.35f, 1.f); - stack.setCastShadow(true); - - auto renderFrame = [&]() -> ImageData * { - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - return gfx->newImageData(); - }; - - auto lumaGrid = [&](ImageData *f) { - std::vector out; - const int w = f->getWidth(); - const int h = f->getHeight(); - out.reserve(size_t(w * h)); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - Colorf c = f->getPixel(x, y); - out.push_back((c.r + c.g + c.b) / 3.f); - } - return out; - }; - - // Casting on -> CSM umbra darkens some ground texels. - std::unique_ptr on(renderFrame()); - REQUIRE(on.get() != nullptr); - stack.setCastShadow(false); - std::unique_ptr off(renderFrame()); - REQUIRE(off.get() != nullptr); - saveImageDataPng(on.get(), testOutDir() + "/sprite_stack_csm_shadow.png"); - - const auto L_on = lumaGrid(on.get()); - const auto L_off = lumaGrid(off.get()); - float best = -1.f; - const int w = on->getWidth(); - const int h = on->getHeight(); - for (int y = 0; y < h; y += 4) { - for (int x = 0; x < w; x += 4) { - const size_t i = size_t(y * w + x); - best = std::max(best, L_off[i] - L_on[i]); - } - } - CHECK_GT(best, 0.02f); - - for (auto *img : layers) delete img; - win->close(); -} - -TEST_CASE("spritestack.render.horizontalMode") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.2f, 2.8f, 4.2f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.12f, 0.13f, 0.16f, 1.f)); - - // Top-down slices of a cone viewed from a 3/4 camera read as a 3D cone. - std::vector layers = - SpriteStack::create()->slicePrimitive("cone", 12, 96, 96, "y", 0.f); - REQUIRE_EQ(layers.size(), size_t(12)); - - SpriteStack3D stack; - stack.setLayerCount(12); - for (int i = 0; i < 12; ++i) stack.setLayerImage(gfx, layers[i], i); - stack.setMode("horizontal"); - stack.setThickness(0.18f); - stack.setSize(2.2f, 2.2f); - stack.setPosition(0.f, -1.0f, 0.f); - stack.setTint(0.55f, 0.42f, 0.30f, 1.f); - - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - std::unique_ptr frame(gfx->newImageData()); - REQUIRE(frame.get() != nullptr); - saveImageDataPng(frame.get(), testOutDir() + "/sprite_stack_horizontal.png"); - - int lit = 0; - const int w = frame->getWidth(); - const int h = frame->getHeight(); - for (int y = h / 4; y < 3 * h / 4; ++y) { - for (int x = w / 4; x < 3 * w / 4; ++x) { - Colorf c = frame->getPixel(x, y); - if (c.r + c.g + c.b > 0.25f && c.a > 0.5f) ++lit; - } - } - CHECK_GT(lit, 300); - - for (auto *img : layers) delete img; - win->close(); -} - -TEST_CASE("spritestack.render.batch") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.0f, 2.2f, 4.0f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.14f, 0.16f, 0.20f, 1.f)); - - // Both stacks share the same layer textures so the batch groups them into - // one combined mesh per texture (single draw call). - std::vector layers = - SpriteStack::create()->slicePrimitive("sphere", 10, 64, 64, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(10)); - std::vector texs; - for (auto *img : layers) { - texs.push_back(gfx->newTexture(img)); - delete img; - } - - auto makeStack = [&](float x, float z) { - auto *st = new SpriteStack3D(); - st->setLayerCount(10); - for (int i = 0; i < 10; ++i) st->setLayerTexture(texs[size_t(i)], i); - st->setThickness(0.14f); - st->setSize(0.8f, 0.8f); - st->setPosition(x, 0.f, z); - return st; - }; - - std::unique_ptr a(makeStack(-1.0f, 0.f)); - std::unique_ptr b(makeStack(1.0f, 0.f)); - a->setTint(0.8f, 0.45f, 0.25f, 1.f); - b->setTint(0.3f, 0.65f, 0.85f, 1.f); - - SpriteStackBatch batch; - batch.add(a.get()); - batch.add(b.get()); - CHECK_EQ(batch.getStackCount(), 2); - - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - batch.render(gfx); - RenderSystem::render(*gfx); - } - std::unique_ptr frame(gfx->newImageData()); - REQUIRE(frame.get() != nullptr); - saveImageDataPng(frame.get(), testOutDir() + "/sprite_stack_batch.png"); - - // Two batched spheres should paint two distinct lit blobs. - int litLeft = 0, litRight = 0; - const int w = frame->getWidth(); - const int h = frame->getHeight(); - for (int y = h / 3; y < 2 * h / 3; ++y) { - for (int x = w / 4; x < w / 2; ++x) { - Colorf c = frame->getPixel(x, y); - if (c.r + c.g + c.b > 0.25f && c.a > 0.5f) ++litLeft; - } - for (int x = w / 2; x < 3 * w / 4; ++x) { - Colorf c = frame->getPixel(x, y); - if (c.r + c.g + c.b > 0.25f && c.a > 0.5f) ++litRight; - } - } - CHECK_GT(litLeft, 120); - CHECK_GT(litRight, 120); - - batch.remove(b.get()); - CHECK_EQ(batch.getStackCount(), 1); - win->close(); -} - -TEST_CASE("spritestack.render.shadowAndOutline") { - auto *win = eve::window::Window::create(); - auto *gfx = Graphics::create(); - REQUIRE(win != nullptr); - REQUIRE(gfx != nullptr); - eve::window::WindowSettings s; - s.width = 256; - s.height = 192; - s.centered = true; - REQUIRE(win->setWindowSettings(s)); - - resetScene3D(); - - auto *cam = Camera3D::createCamera(); - cam->setTarget(0.f, 0.f, 0.f); - cam->setEye(3.2f, 2.8f, 4.2f); - cam->data()->nearZ = 0.1f; - cam->data()->farZ = 50.f; - - auto *hud = Renderable2D::create(); - hud->transform()->x = 0; - hud->transform()->y = 0; - hud->sprite()->width = 2; - hud->sprite()->height = 2; - hud->sprite()->a = 0.f; - hud->sprite()->visible = true; - - // Ground disc so the projected shadow has a surface to land on. - auto *ground = Renderable3D::create(); - ground->meshRenderer()->mesh = gfx->newMeshCylinder(32, 1, true); - ground->transform()->x = 0.f; - ground->transform()->y = -1.5f; - ground->transform()->sx = 6.f; - ground->transform()->sy = 0.1f; - ground->transform()->sz = 6.f; - ground->setTint(0.20f, 0.26f, 0.22f, 1.f); - - gfx->setScreenReadbackEnabled(true); - gfx->setBackgroundColor(::Color(0.12f, 0.13f, 0.16f, 1.f)); - - std::vector layers = - SpriteStack::create()->slicePrimitive("cone", 12, 96, 96, "z", 0.f); - REQUIRE_EQ(layers.size(), size_t(12)); - - SpriteStack3D stack; - stack.setLayerCount(12); - for (int i = 0; i < 12; ++i) stack.setLayerImage(gfx, layers[i], i); - stack.setThickness(0.13f); - stack.setSize(1.5f, 1.6f); - stack.setPosition(0.f, 0.2f, 0.f); - stack.setTint(0.45f, 0.55f, 0.35f, 1.f); - - auto renderFrame = [&]() -> ImageData * { - for (int i = 0; i < 3; ++i) { - RenderSystem3D::render(*gfx); - stack.render(gfx); - RenderSystem::render(*gfx); - } - return gfx->newImageData(); - }; - - std::unique_ptr plain(renderFrame()); - REQUIRE(plain.get() != nullptr); - saveImageDataPng(plain.get(), testOutDir() + "/sprite_stack_plain.png"); - - // Shadow: dark blob projected onto the ground below/right of the cone. +TEST_CASE("spritestack.render.pure2DStack") { + WindowFixture fx; + auto layers = SpriteStack::create()->slicePrimitive("cone", 14, 64, 64, "y", 0.f); + SpriteStack2D stack; + stack.setLayerCount(int(layers.size())); + for (int i = 0; i < int(layers.size()); ++i) stack.setLayerImage(fx.gfx, layers[size_t(i)], i); + stack.setPosition(160.f, 155.f); + stack.setSize(96.f, 96.f); + stack.setThickness(3.f); + stack.setRotation(25.f); + stack.setTint(0.44f, 0.78f, 0.58f, 1.f); stack.setShadowEnabled(true); - stack.setShadowOpacity(0.42f); - stack.setShadowLight(0.35f, -1.f, 0.25f); - stack.setShadowPlaneY(-1.45f); - std::unique_ptr shadow(renderFrame()); - REQUIRE(shadow.get() != nullptr); - saveImageDataPng(shadow.get(), testOutDir() + "/sprite_stack_shadow.png"); + stack.setShadowOffset(8.f, 6.f); + stack.setOutline(1.5f, 0.02f, 0.03f, 0.04f); - int dark = 0; - const int w = shadow->getWidth(); - const int h = shadow->getHeight(); - for (int y = 0; y < h; ++y) { - for (int x = 0; x < w; ++x) { - Colorf a = plain->getPixel(x, y); - Colorf b = shadow->getPixel(x, y); - const float diff = (a.r + a.g + a.b) - (b.r + b.g + b.b); - if (diff > 0.12f) ++dark; + auto frame = renderStack(fx.gfx, stack); + REQUIRE(frame.get() != nullptr); + savePng(frame.get(), testOutDir() + "/sprite_stack_2d.png"); + int changed = 0; + for (int y = 30; y < 210; ++y) { + for (int x = 60; x < 260; ++x) { + const Colorf pixel = frame->getPixel(x, y); + if (pixel.g > 0.20f && pixel.g > pixel.r * 1.15f) ++changed; } } - CHECK_GT(dark, 60); - - // Outline: expanded dark silhouette grows the cone's footprint. - stack.setShadowEnabled(false); - stack.setOutline(0.12f, 0.0f, 0.0f, 0.0f); - std::unique_ptr outlined(renderFrame()); - REQUIRE(outlined.get() != nullptr); - saveImageDataPng(outlined.get(), testOutDir() + "/sprite_stack_outline.png"); - - auto countDarkRim = [&](ImageData *f) { - int n = 0; - for (int y = h / 4; y < 3 * h / 4; ++y) - for (int x = w / 4; x < 3 * w / 4; ++x) { - Colorf c = f->getPixel(x, y); - if (c.r + c.g + c.b < 0.15f) ++n; - } - return n; - }; - // The outline is an opaque black rim; count those pixels appearing around - // the cone's silhouette only when the outline is enabled. - CHECK_GT(countDarkRim(outlined.get()), countDarkRim(plain.get()) + 30); - - for (auto *img : layers) delete img; - win->close(); + CHECK_GT(changed, 500); + for (ImageData *layer : layers) delete layer; +} + +TEST_CASE("spritestack.render.rotationChangesPixels") { + WindowFixture fx; + auto layers = SpriteStack::create()->slicePrimitive("box", 8, 48, 48, "y", 0.f); + SpriteStack2D stack; + stack.setLayerCount(int(layers.size())); + for (int i = 0; i < int(layers.size()); ++i) stack.setLayerImage(fx.gfx, layers[size_t(i)], i); + stack.setPosition(160.f, 140.f); + stack.setSize(100.f, 68.f); + stack.setThickness(3.f); + auto before = renderStack(fx.gfx, stack); + stack.setRotation(37.f); + auto after = renderStack(fx.gfx, stack); + int diff = 0; + for (int y = 0; y < before->getHeight(); ++y) + for (int x = 0; x < before->getWidth(); ++x) { + const Colorf a = before->getPixel(x, y); + const Colorf b = after->getPixel(x, y); + if (std::fabs(a.r - b.r) + std::fabs(a.g - b.g) + std::fabs(a.b - b.b) > 0.08f) + ++diff; + } + CHECK_GT(diff, 300); + for (ImageData *layer : layers) delete layer; +} + +TEST_CASE("spritestack.render.batchUses2DStacks") { + WindowFixture fx; + auto layers = SpriteStack::create()->slicePrimitive("sphere", 8, 48, 48, "y", 0.f); + SpriteStack2D left; + SpriteStack2D right; + for (SpriteStack2D *stack : {&left, &right}) { + stack->setLayerCount(int(layers.size())); + for (int i = 0; i < int(layers.size()); ++i) stack->setLayerImage(fx.gfx, layers[size_t(i)], i); + stack->setSize(70.f, 70.f); + stack->setThickness(2.f); + } + left.setPosition(105.f, 145.f); + right.setPosition(215.f, 145.f); + right.setRotation(30.f); + SpriteStackBatch batch; + batch.add(&left); + batch.add(&right); + CHECK_EQ(batch.getStackCount(), 2); + for (int i = 0; i < 3; ++i) { + fx.gfx->clear(std::nullopt, std::nullopt, std::nullopt); + batch.render(fx.gfx); + fx.gfx->present(); + } + std::unique_ptr frame(fx.gfx->newImageData()); + savePng(frame.get(), testOutDir() + "/sprite_stack_2d_batch.png"); + for (ImageData *layer : layers) delete layer; }