载入中...
搜索中...
未找到
SpriteStack.cpp
浏览该文件的文档.
2
3#include "common/Exception.h"
4#include "common/ECS.h"
6#include "graphics/Graphics.h"
7#include "graphics/Mesh.h"
9#include "graphics/Shader.h"
10#include "graphics/Texture.h"
11#include "image/ImageData.h"
12#include "model3d/ModelData.h"
13#include "spritestack/shaders/sprite_stack_frag_spv.inc"
14#include "spritestack/shaders/sprite_stack_vert_spv.inc"
15
16#include <algorithm>
17#include <cmath>
18#include <cstring>
19#include <glm/glm.hpp>
20#include <glm/gtc/matrix_transform.hpp>
21#include <memory>
22#include <simplesquirrel/simplesquirrel.hpp>
23#include <string>
24#include <utility>
25#include <vector>
26
27struct aiMesh;
28
30namespace {
31
32constexpr float kPi = 3.14159265358979323846f;
33
34float clampf(float x, float lo, float hi) { return std::min(hi, std::max(lo, x)); }
35
36struct Projection {
37 int u = 0; // model axis -> image column
38 int v = 0; // model axis -> image row
39 int d = 0; // model axis -> slice depth
40};
41
42Projection projectionForAxis(const std::string &axis) {
43 if (axis == "x") return {1, 2, 0};
44 if (axis == "y") return {0, 2, 1}; // top-down layers
45 return {0, 1, 2}; // vertical bread slices (default)
46}
47
48struct Tri2D {
49 glm::vec2 px[3]{};
50 float depth[3]{};
51 image::ImageData::Colorf color{1.f, 1.f, 1.f, 1.f};
52};
53
65void rasterizeCrossSection(const std::vector<Tri2D> &tris, float dc, int w, int h,
66 uint8_t *rgba) {
67 const size_t pixelCount = size_t(w) * size_t(h);
68 std::vector<std::vector<float>> hits(pixelCount);
69 std::vector<float> bestY(pixelCount, 1e30f);
70 std::vector<uint32_t> bestRGB(pixelCount, 0xffffffu);
71
72 for (const auto &tri : tris) {
73 const glm::vec2 &a = tri.px[0];
74 const glm::vec2 &b = tri.px[1];
75 const glm::vec2 &c = tri.px[2];
76 int x0 = int(std::floor(std::min(a.x, std::min(b.x, c.x))));
77 int x1 = int(std::ceil(std::max(a.x, std::max(b.x, c.x))));
78 int y0 = int(std::floor(std::min(a.y, std::min(b.y, c.y))));
79 int y1 = int(std::ceil(std::max(a.y, std::max(b.y, c.y))));
80 x0 = std::max(0, x0);
81 y0 = std::max(0, y0);
82 x1 = std::min(w - 1, x1);
83 y1 = std::min(h - 1, y1);
84
85 const glm::vec2 e0 = b - a;
86 const glm::vec2 e1 = c - a;
87 const float denom = e0.x * e1.y - e0.y * e1.x;
88 if (std::fabs(denom) < 1e-12f) continue;
89
90 for (int y = y0; y <= y1; ++y) {
91 for (int x = x0; x <= x1; ++x) {
92 const glm::vec2 p(float(x) + 0.5f, float(y) + 0.5f);
93 const glm::vec2 d = p - a;
94 const float u = (d.x * e1.y - d.y * e1.x) / denom;
95 const float v = (e0.x * d.y - e0.y * d.x) / denom;
96 if (u < -1e-4f || v < -1e-4f || u + v > 1.f + 1e-4f) continue;
97 const float w0 = 1.f - u - v;
98 const float depthAt = w0 * tri.depth[0] + u * tri.depth[1] + v * tri.depth[2];
99 if (depthAt <= dc) continue;
100
101 const size_t i = size_t(y) * size_t(w) + size_t(x);
102 hits[i].push_back(depthAt);
103 if (depthAt < bestY[i]) {
104 bestY[i] = depthAt;
105 bestRGB[i] = (uint32_t(clampf(tri.color.r, 0.f, 1.f) * 255.f) << 16) |
106 (uint32_t(clampf(tri.color.g, 0.f, 1.f) * 255.f) << 8) |
107 uint32_t(clampf(tri.color.b, 0.f, 1.f) * 255.f);
108 }
109 }
110 }
111 }
112
113 std::vector<float> uniqueHits;
114 for (size_t i = 0; i < pixelCount; ++i) {
115 if (hits[i].empty()) continue;
116 std::sort(hits[i].begin(), hits[i].end());
117 uniqueHits.clear();
118 for (float d : hits[i]) {
119 // Shared edges/vertices report the same surface with slightly
120 // different interpolated depths (float weight sums); group them.
121 if (uniqueHits.empty() ||
122 d - uniqueHits.back() > 1e-5f * std::max(1.f, std::fabs(d)))
123 uniqueHits.push_back(d);
124 }
125 if (uniqueHits.size() % 2 == 0) continue; // even = outside the solid
126 uint8_t *px = rgba + i * 4;
127 px[0] = uint8_t(bestRGB[i] >> 16);
128 px[1] = uint8_t(bestRGB[i] >> 8);
129 px[2] = uint8_t(bestRGB[i]);
130 px[3] = 255;
131 }
132}
133
134std::vector<image::ImageData *> sliceArrays(const float *pos, const float *nrm, const float *rgb,
135 int vertexCount, const uint32_t *indices, int indexCount,
136 const SliceOptions &opt) {
137 if (!pos || vertexCount < 3 || !indices || indexCount < 3)
138 throw eve::Exception("SpriteStack.sliceMesh: invalid mesh arrays");
139 if (opt.layerCount <= 0) throw eve::Exception("SpriteStack.sliceMesh: layerCount must be > 0");
140 if (opt.imageW <= 0 || opt.imageH <= 0)
141 throw eve::Exception("SpriteStack.sliceMesh: image size must be > 0");
142
143 const Projection proj = projectionForAxis(opt.axis);
144
145 glm::vec3 mn(1e30f), mx(-1e30f);
146 for (int i = 0; i < vertexCount; ++i) {
147 const glm::vec3 p(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]);
148 mn = glm::min(mn, p);
149 mx = glm::max(mx, p);
150 }
151 if (mn.x > mx.x) throw eve::Exception("SpriteStack.sliceMesh: empty mesh AABB");
152
153 const float d0 = mn[proj.d];
154 const float d1 = opt.thickness > 0.f ? d0 + opt.thickness * float(opt.layerCount - 1)
155 : mx[proj.d];
156 const float slab = opt.thickness > 0.f ? opt.thickness : (d1 - d0) / float(opt.layerCount);
157 if (slab <= 0.f) throw eve::Exception("SpriteStack.sliceMesh: zero slice thickness");
158
159 const float uMin = mn[proj.u] - opt.padding * std::max(1.f, mx[proj.u] - mn[proj.u]);
160 const float uMax = mx[proj.u] + opt.padding * std::max(1.f, mx[proj.u] - mn[proj.u]);
161 const float vMin = mn[proj.v] - opt.padding * std::max(1.f, mx[proj.v] - mn[proj.v]);
162 const float vMax = mx[proj.v] + opt.padding * std::max(1.f, mx[proj.v] - mn[proj.v]);
163 const float uSpan = uMax - uMin;
164 const float vSpan = vMax - vMin;
165 if (uSpan <= 0.f || vSpan <= 0.f)
166 throw eve::Exception("SpriteStack.sliceMesh: degenerate projected AABB");
167
168 const glm::vec3 viewDir =
169 proj.d == 0 ? glm::vec3(1.f, 0.f, 0.f)
170 : (proj.d == 1 ? glm::vec3(0.f, 1.f, 0.f) : glm::vec3(0.f, 0.f, 1.f));
171
172 std::vector<image::ImageData *> layers;
173 layers.reserve(size_t(opt.layerCount));
174 const int triangleCount = indexCount / 3;
175 std::vector<Tri2D> tris;
176 tris.reserve(size_t(triangleCount));
177
178 for (int t = 0; t < triangleCount; ++t) {
179 const uint32_t i0 = indices[t * 3];
180 const uint32_t i1 = indices[t * 3 + 1];
181 const uint32_t i2 = indices[t * 3 + 2];
182 if (int(i0) >= vertexCount || int(i1) >= vertexCount || int(i2) >= vertexCount) continue;
183
184 const glm::vec3 a(pos[i0 * 3], pos[i0 * 3 + 1], pos[i0 * 3 + 2]);
185 const glm::vec3 b(pos[i1 * 3], pos[i1 * 3 + 1], pos[i1 * 3 + 2]);
186 const glm::vec3 c(pos[i2 * 3], pos[i2 * 3 + 1], pos[i2 * 3 + 2]);
187
188 image::ImageData::Colorf color{opt.tintR, opt.tintG, opt.tintB, 1.f};
189 if (rgb) {
190 const float rr = (rgb[i0 * 3] + rgb[i1 * 3] + rgb[i2 * 3]) / 3.f;
191 const float gg = (rgb[i0 * 3 + 1] + rgb[i1 * 3 + 1] + rgb[i2 * 3 + 1]) / 3.f;
192 const float bb = (rgb[i0 * 3 + 2] + rgb[i1 * 3 + 2] + rgb[i2 * 3 + 2]) / 3.f;
193 color.r *= rr;
194 color.g *= gg;
195 color.b *= bb;
196 }
197 if (opt.shade) {
198 glm::vec3 n = glm::cross(b - a, c - a);
199 if (glm::dot(n, n) > 1e-12f) {
200 n = glm::normalize(n);
201 const float s = 0.72f + 0.28f * std::abs(glm::dot(n, viewDir));
202 color.r *= s;
203 color.g *= s;
204 color.b *= s;
205 }
206 }
207
208 Tri2D tri;
209 tri.px[0] = glm::vec2((a[proj.u] - uMin) / uSpan * float(opt.imageW - 1),
210 (1.f - (a[proj.v] - vMin) / vSpan) * float(opt.imageH - 1));
211 tri.px[1] = glm::vec2((b[proj.u] - uMin) / uSpan * float(opt.imageW - 1),
212 (1.f - (b[proj.v] - vMin) / vSpan) * float(opt.imageH - 1));
213 tri.px[2] = glm::vec2((c[proj.u] - uMin) / uSpan * float(opt.imageW - 1),
214 (1.f - (c[proj.v] - vMin) / vSpan) * float(opt.imageH - 1));
215 tri.depth[0] = a[proj.d];
216 tri.depth[1] = b[proj.d];
217 tri.depth[2] = c[proj.d];
218 tri.color = color;
219 tris.push_back(tri);
220 }
221
222 for (int li = 0; li < opt.layerCount; ++li) {
223 auto *img = new image::ImageData(opt.imageW, opt.imageH, "RGBA8");
224 auto *data = static_cast<uint8_t *>(img->getData());
225 std::memset(data, 0, size_t(opt.imageW) * size_t(opt.imageH) * 4);
226 const float dc = d0 + (float(li) + 0.5f) * slab;
227 rasterizeCrossSection(tris, dc, opt.imageW, opt.imageH, data);
228 layers.push_back(img);
229 }
230 return layers;
231}
232
233void makeBox(std::vector<float> &pos, std::vector<float> &nrm, std::vector<uint32_t> &idx) {
234 const float h = 0.5f;
235 const glm::vec3 corners[8] = {
236 {-h, -h, -h}, {h, -h, -h}, {h, h, -h}, {-h, h, -h},
237 {-h, -h, h}, {h, -h, h}, {h, h, h}, {-h, h, h},
238 };
239 const glm::vec3 faces[6] = {
240 {0.f, 0.f, -1.f}, {0.f, 0.f, 1.f}, {-1.f, 0.f, 0.f},
241 {1.f, 0.f, 0.f}, {0.f, -1.f, 0.f}, {0.f, 1.f, 0.f},
242 };
243 const int quads[6][4] = {
244 {0, 1, 2, 3}, {5, 4, 7, 6}, {4, 0, 3, 7},
245 {1, 5, 6, 2}, {4, 5, 1, 0}, {3, 2, 6, 7},
246 };
247 for (int f = 0; f < 6; ++f) {
248 for (int k = 0; k < 4; ++k) {
249 const glm::vec3 &p = corners[quads[f][k]];
250 pos.insert(pos.end(), {p.x, p.y, p.z});
251 nrm.insert(nrm.end(), {faces[f].x, faces[f].y, faces[f].z});
252 }
253 const uint32_t base = uint32_t(f * 4);
254 idx.insert(idx.end(), {base, base + 1, base + 2, base, base + 2, base + 3});
255 }
256}
257
258void makeLathe(const std::string &kind, int slices, std::vector<float> &pos,
259 std::vector<float> &nrm, std::vector<uint32_t> &idx) {
260 const bool sphere = kind == "sphere";
261 const bool cone = kind == "cone";
262 const int stacks = sphere ? 12 : 1;
263 auto ring = [&](float y, float radius, float nY) {
264 // Push `slices` vertices forming one horizontal ring.
265 const uint32_t base = uint32_t(pos.size() / 3);
266 for (int s = 0; s < slices; ++s) {
267 const float a = float(s) / float(slices) * 2.f * kPi;
268 const glm::vec3 n(std::cos(a), nY, std::sin(a));
269 pos.insert(pos.end(), {std::cos(a) * radius, y, std::sin(a) * radius});
270 nrm.insert(nrm.end(), {n.x, n.y, n.z});
271 }
272 return base;
273 };
274 auto connectRings = [&](uint32_t r0, uint32_t r1) {
275 for (int s = 0; s < slices; ++s) {
276 const uint32_t s0 = r0 + uint32_t(s);
277 const uint32_t s1 = r0 + uint32_t((s + 1) % slices);
278 const uint32_t t0 = r1 + uint32_t(s);
279 const uint32_t t1 = r1 + uint32_t((s + 1) % slices);
280 idx.insert(idx.end(), {s0, t0, t1, s0, t1, s1});
281 }
282 };
283 auto cap = [&](float y, float radius, float nY) {
284 const uint32_t center = uint32_t(pos.size() / 3);
285 pos.insert(pos.end(), {0.f, y, 0.f});
286 nrm.insert(nrm.end(), {0.f, nY, 0.f});
287 for (int s = 0; s < slices; ++s) {
288 const float a = float(s) / float(slices) * 2.f * kPi;
289 pos.insert(pos.end(), {std::cos(a) * radius, y, std::sin(a) * radius});
290 nrm.insert(nrm.end(), {0.f, nY, 0.f});
291 const uint32_t ring0 = center + 1;
292 const uint32_t a0 = ring0 + uint32_t(s);
293 const uint32_t b0 = ring0 + uint32_t((s + 1) % slices);
294 if (nY > 0.f)
295 idx.insert(idx.end(), {center, b0, a0});
296 else
297 idx.insert(idx.end(), {center, a0, b0});
298 }
299 };
300
301 if (sphere) {
302 std::vector<uint32_t> rings;
303 for (int st = 0; st < stacks; ++st) {
304 const float t0 = float(st) / float(stacks);
305 const float y0 = std::cos(t0 * kPi) * 0.5f;
306 const float r0 = std::sin(t0 * kPi) * 0.5f;
307 rings.push_back(ring(y0, r0, -std::sin(t0 * kPi)));
308 }
309 for (int st = 0; st + 1 < stacks; ++st)
310 connectRings(rings[size_t(st)], rings[size_t(st + 1)]);
311 return;
312 }
313
314 if (cone) {
315 const uint32_t apex = uint32_t(pos.size() / 3);
316 pos.insert(pos.end(), {0.f, 0.5f, 0.f});
317 nrm.insert(nrm.end(), {0.f, 1.f, 0.f});
318 for (int s = 0; s < slices; ++s) {
319 const float a0 = float(s) / float(slices) * 2.f * kPi;
320 const float a1 = float(s + 1) / float(slices) * 2.f * kPi;
321 const uint32_t b0 = uint32_t(pos.size() / 3);
322 pos.insert(pos.end(), {std::cos(a0) * 0.5f, -0.5f, std::sin(a0) * 0.5f});
323 nrm.insert(nrm.end(), {std::cos(a0) * 0.7071f, 0.7071f, std::sin(a0) * 0.7071f});
324 pos.insert(pos.end(), {std::cos(a1) * 0.5f, -0.5f, std::sin(a1) * 0.5f});
325 nrm.insert(nrm.end(), {std::cos(a1) * 0.7071f, 0.7071f, std::sin(a1) * 0.7071f});
326 idx.insert(idx.end(), {apex, b0, b0 + 1});
327 }
328 cap(-0.5f, 0.5f, -1.f);
329 return;
330 }
331
332 // cylinder
333 const uint32_t bottom = ring(-0.5f, 0.5f, 0.f);
334 const uint32_t top = ring(0.5f, 0.5f, 0.f);
335 connectRings(bottom, top);
336 cap(0.5f, 0.5f, 1.f);
337 cap(-0.5f, 0.5f, -1.f);
338}
339
340} // namespace
341
342std::vector<image::ImageData *> sliceMeshToLayers(const SliceInput &input, const SliceOptions &opt) {
343 return sliceArrays(input.posXYZ, input.nrmXYZ, input.rgb, input.vertexCount, input.indices,
344 input.indexCount, opt);
345}
346
347std::vector<image::ImageData *> sliceModelToLayers(model3d::ModelData *model,
348 const SliceOptions &opt) {
349 if (!model) throw eve::Exception("SpriteStack.sliceModel: null model");
350 std::vector<float> pos, nrm, rgb;
351 std::vector<uint32_t> idx;
352 const int meshCount = model->getMeshCount();
353 for (int m = 0; m < meshCount; ++m) {
354 const aiMesh *am = model->getMesh(m);
355 if (!am) continue;
356 const uint32_t base = uint32_t(pos.size() / 3);
357 for (unsigned v = 0; v < am->mNumVertices; ++v) {
358 const aiVector3D &p = am->mVertices[v];
359 pos.insert(pos.end(), {p.x, p.y, p.z});
360 if (am->mNormals) {
361 const aiVector3D &n = am->mNormals[v];
362 nrm.insert(nrm.end(), {n.x, n.y, n.z});
363 }
364 if (am->mColors && am->mColors[0]) {
365 const aiColor4D &c = am->mColors[0][v];
366 rgb.insert(rgb.end(), {c.r, c.g, c.b});
367 }
368 }
369 for (unsigned f = 0; f < am->mNumFaces; ++f) {
370 const auto &face = am->mFaces[f];
371 if (face.mNumIndices != 3) continue;
372 idx.insert(idx.end(), {base + face.mIndices[0], base + face.mIndices[1],
373 base + face.mIndices[2]});
374 }
375 }
376 SliceInput in{};
377 in.posXYZ = pos.data();
378 in.nrmXYZ = nrm.empty() ? nullptr : nrm.data();
379 in.rgb = rgb.empty() ? nullptr : rgb.data();
380 in.vertexCount = int(pos.size() / 3);
381 in.indices = idx.data();
382 in.indexCount = int(idx.size());
383 return sliceArrays(in.posXYZ, in.nrmXYZ, in.rgb, in.vertexCount, in.indices, in.indexCount, opt);
384}
385
386std::vector<image::ImageData *> slicePrimitiveToLayers(const std::string &kind,
387 const SliceOptions &opt) {
388 std::vector<float> pos, nrm;
389 std::vector<uint32_t> idx;
390 if (kind == "box") {
391 makeBox(pos, nrm, idx);
392 } else if (kind == "cylinder" || kind == "sphere" || kind == "cone") {
393 makeLathe(kind, 32, pos, nrm, idx);
394 } else {
395 throw eve::Exception("SpriteStack.slicePrimitive: unknown kind '%s' (box|cylinder|sphere|cone)",
396 kind.c_str());
397 }
398 SliceInput in{};
399 in.posXYZ = pos.data();
400 in.nrmXYZ = nrm.data();
401 in.vertexCount = int(pos.size() / 3);
402 in.indices = idx.data();
403 in.indexCount = int(idx.size());
404 return sliceArrays(in.posXYZ, in.nrmXYZ, nullptr, in.vertexCount, in.indices, in.indexCount, opt);
405}
406
407// ---------------------------------------------------------------------------
408// SpriteStack3D
409// ---------------------------------------------------------------------------
410
411namespace {
412
413std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
414 return std::vector<uint32_t>(data, data + count);
415}
416
417glm::mat4 billboardModel(const glm::vec3 &center, float width, float height,
418 const glm::vec3 &eye) {
419 glm::vec3 d = eye - center;
420 d.y = 0.f;
421 if (glm::length(d) < 1e-4f) d = glm::vec3(0.f, 0.f, 1.f);
422 d = glm::normalize(d);
423 const glm::vec3 up(0.f, 1.f, 0.f);
424 const glm::vec3 right = glm::normalize(glm::cross(up, d));
425 const glm::vec3 forward = glm::normalize(glm::cross(right, up));
426 glm::mat4 m(1.f);
427 m[0] = glm::vec4(right * width, 0.f);
428 m[1] = glm::vec4(up * height, 0.f);
429 m[2] = glm::vec4(forward, 0.f);
430 m[3] = glm::vec4(center, 1.f);
431 return m;
432}
433
434graphics::Camera3D *findActiveCamera3D() {
435 if (ecs::current()->getManager<graphics::Camera3D>() == nullptr) return nullptr;
436 auto camView = ecs::View<graphics::Camera3D, graphics::Camera3D::Data>();
437 for (auto it = camView.begin(); it != camView.end(); ++it) {
438 auto [data] = *it;
439 if (!data->active || !data->entity) continue;
440 return data->entity;
441 }
442 return nullptr;
443}
444
445} // namespace
446
448 if (count < 0) throw eve::Exception("SpriteStack3D.setLayerCount: count must be >= 0");
449 layerCount_ = count;
450 layers_.assign(size_t(count), Layer{});
451 bumpVersion();
452}
453
454int SpriteStack3D::getLayerCount() const { return layerCount_; }
455
457 if (index < 0 || index >= layerCount_)
458 throw eve::Exception("SpriteStack3D.setLayerTexture: index %d out of range [0,%d)", index,
459 layerCount_);
460 layers_[size_t(index)] = Layer{texture, glm::vec4(0.f, 0.f, 1.f, 1.f)};
461 bumpVersion();
462}
463
465 if (index < 0 || index >= layerCount_) return nullptr;
466 return layers_[size_t(index)].texture;
467}
468
470 if (!gfx) throw eve::Exception("SpriteStack3D.setLayerImage: null graphics");
471 if (!img) throw eve::Exception("SpriteStack3D.setLayerImage: null ImageData");
472 setLayerTexture(gfx->newTextureFromImageData(img, false, false), index);
473}
474
475void SpriteStack3D::setLayerFile(graphics::Graphics *gfx, const std::string &path, int index) {
476 if (!gfx) throw eve::Exception("SpriteStack3D.setLayerFile: null graphics");
477 setLayerTexture(gfx->newTextureFromFile(path), index);
478}
479
481 int layerCount) {
482 if (!gfx) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null graphics");
483 if (!atlas) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: null atlas");
484 if (layerCount <= 0) throw eve::Exception("SpriteStack3D.setLayersFromAtlas: count must be > 0");
485 setLayerCount(layerCount);
486 for (int i = 0; i < layerCount; ++i) {
487 const float u0 = float(i) / float(layerCount);
488 const float u1 = float(i + 1) / float(layerCount);
489 layers_[size_t(i)] = Layer{atlas, glm::vec4(u0, 0.f, u1, 1.f)};
490 }
491 bumpVersion();
492}
493
495 if (thickness <= 0.f) throw eve::Exception("SpriteStack3D.setThickness: must be > 0");
496 thickness_ = thickness;
497 bumpVersion();
498}
499
500float SpriteStack3D::getThickness() const { return thickness_; }
501
503 if (width <= 0.f || height <= 0.f)
504 throw eve::Exception("SpriteStack3D.setSize: size must be > 0");
505 width_ = width;
506 height_ = height;
507 bumpVersion();
508}
509
510float SpriteStack3D::getWidth() const { return width_; }
511float SpriteStack3D::getHeight() const { return height_; }
512
513void SpriteStack3D::setPosition(float x, float y, float z) {
514 x_ = x;
515 y_ = y;
516 z_ = z;
517 bumpVersion();
518}
519
520void SpriteStack3D::setYaw(float yaw) {
521 yaw_ = yaw;
522 bumpVersion();
523}
524
525void SpriteStack3D::setTint(float r, float g, float b, float a) {
526 tintR_ = r;
527 tintG_ = g;
528 tintB_ = b;
529 tintA_ = a;
530 bumpVersion();
531}
532
533void SpriteStack3D::setAlphaCutoff(float cutoff) { alphaCutoff_ = clampf(cutoff, 0.f, 1.f); }
534
535void SpriteStack3D::setVisible(bool visible) {
536 visible_ = visible;
537 bumpVersion();
538}
539
540bool SpriteStack3D::getVisible() const { return visible_; }
541
542void SpriteStack3D::setMode(const std::string &mode) {
543 if (mode != "vertical" && mode != "horizontal")
544 throw eve::Exception("SpriteStack3D.setMode: unknown mode '%s' (vertical|horizontal)",
545 mode.c_str());
546 mode_ = mode;
547 bumpVersion();
548}
549
550std::string SpriteStack3D::getMode() const { return mode_; }
551
553 shadowEnabled_ = enabled;
554 bumpVersion();
555}
556
557bool SpriteStack3D::getShadowEnabled() const { return shadowEnabled_; }
558
560 shadowOpacity_ = clampf(opacity, 0.f, 1.f);
561 bumpVersion();
562}
563
564void SpriteStack3D::setShadowLight(float dx, float dy, float dz) {
565 shadowLightX_ = dx;
566 shadowLightY_ = dy;
567 shadowLightZ_ = dz;
568 bumpVersion();
569}
570
572 shadowPlaneY_ = y;
573 bumpVersion();
574}
575
576void SpriteStack3D::setOutline(float width, float r, float g, float b) {
577 outlineWidth_ = std::max(0.f, width);
578 outlineR_ = r;
579 outlineG_ = g;
580 outlineB_ = b;
581 bumpVersion();
582}
583
584float SpriteStack3D::getOutlineWidth() const { return outlineWidth_; }
585
586void SpriteStack3D::setOutlineColor(float r, float g, float b) {
587 outlineR_ = r;
588 outlineG_ = g;
589 outlineB_ = b;
590 bumpVersion();
591}
592
593void SpriteStack3D::ensureResources(graphics::Graphics *gfx) const {
594 if (quad_ && shader_) return;
595 if (!quad_) {
596 const float posXYZ[] = {-0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f,
597 0.5f, -0.5f, 0.f, -0.5f, -0.5f, 0.f};
598 const float nrmXYZ[] = {0.f, 0.f, 1.f, 0.f, 0.f, 1.f,
599 0.f, 0.f, 1.f, 0.f, 0.f, 1.f};
600 const float uvST[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
601 const uint32_t indices[] = {0, 1, 2, 0, 2, 3};
602 quad_ = gfx->newMeshFromArrays(posXYZ, nrmXYZ, uvST, 4, indices, 6);
603 }
604 if (!shader_) {
605 auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
606 auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
607 shader_ = gfx->newHairShaderFromSpv(vert, frag);
608 if (!shader_ || !shader_->gpuHandle)
609 throw eve::Exception("SpriteStack3D.render: failed to create slice shader");
610 shader_->declareVec4("uvRect");
611 shader_->declareFloat("alphaCutoff");
612 shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
613 shader_->sendFloat("alphaCutoff", alphaCutoff_);
614 }
615}
616
617void SpriteStack3D::collectSlices(const SpriteStack3D &stack, const glm::vec3 &eye,
618 std::vector<SliceDraw> &out) {
619 if (!stack.visible_ || stack.layerCount_ <= 0) return;
620 const glm::mat4 yawM = glm::rotate(glm::mat4(1.f), stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
621 const float half = 0.5f * float(stack.layerCount_ - 1);
622 for (int i = 0; i < stack.layerCount_; ++i) {
623 graphics::Texture *tex = stack.layers_[size_t(i)].texture;
624 if (!tex) continue;
625 const float offset = stack.thickness_ * (float(i) - half);
626 glm::vec3 center(stack.x_, stack.y_, stack.z_);
627 if (stack.mode_ == "horizontal") {
628 center.y += offset;
629 } else {
630 const glm::vec4 off = yawM * glm::vec4(0.f, 0.f, offset, 0.f);
631 center += glm::vec3(off);
632 }
633 SliceDraw s;
634 const glm::vec3 d = center - eye;
635 s.distSq = glm::dot(d, d);
636 if (stack.mode_ == "horizontal") {
637 // Horizontal top-down layer: rotate image by yaw, keep the quad
638 // level so a 3/4 camera sees true volume.
639 s.model = glm::translate(glm::mat4(1.f), center);
640 s.model = glm::rotate(s.model, stack.yaw_, glm::vec3(0.f, 1.f, 0.f));
641 s.model = glm::rotate(s.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
642 s.model = glm::scale(s.model, glm::vec3(stack.width_, stack.height_, 1.f));
643 } else {
644 s.model = billboardModel(center, stack.width_, stack.height_, eye);
645 }
646 s.texture = tex;
647 s.uv = stack.layers_[size_t(i)].uv;
648 out.push_back(s);
649 }
650}
651
652SpriteStack3D::SliceDraw SpriteStack3D::makeShadowDraw(const SpriteStack3D &stack,
653 const SliceDraw &s,
654 const glm::vec3 &eye) {
655 SliceDraw out = s;
656 const glm::vec3 light =
657 glm::normalize(glm::vec3(stack.shadowLightX_, stack.shadowLightY_, stack.shadowLightZ_));
658 const glm::vec3 center(s.model[3]);
659 // Lift the shadow above the ground so the depth test (strict LESS) cannot
660 // reject it as coplanar with the ground surface.
661 const float planeY = stack.shadowPlaneY_ + 0.02f;
662 const float t = (planeY - center.y) / light.y;
663 const glm::vec3 c = center + light * t;
664 out.model = glm::translate(glm::mat4(1.f), c);
665 out.model = glm::rotate(out.model, -kPi * 0.5f, glm::vec3(1.f, 0.f, 0.f));
666 out.model = glm::scale(out.model, glm::vec3(stack.width_, stack.height_, 1.f));
667 const glm::vec3 d = c - eye;
668 out.distSq = glm::dot(d, d);
669 return out;
670}
671
672SpriteStack3D::SliceDraw SpriteStack3D::makeOutlineDraw(const SpriteStack3D &stack,
673 const SliceDraw &s,
674 const glm::vec3 &eye) {
675 SliceDraw out = s;
676 const glm::vec3 center(s.model[3]);
677 glm::vec3 away = center - eye;
678 if (glm::length(away) < 1e-6f) away = glm::vec3(0.f, 0.f, 1.f);
679 away = glm::normalize(away);
680 // Push the outline slightly away from the camera so the slice draw (same
681 // plane) still passes the strict-LESS depth test over it and only the rim
682 // stays visible.
683 out.model[3] = glm::vec4(center + away * 0.02f, 1.f);
684 const float k = stack.outlineWidth_;
685 out.model = out.model * glm::scale(glm::mat4(1.f), glm::vec3(1.f + 2.f * k, 1.f + 2.f * k, 1.f));
686 return out;
687}
688
689std::vector<SpriteStack3D *> &SpriteStack3D::gbufferStacks() {
690 static std::vector<SpriteStack3D *> stacks;
691 return stacks;
692}
693
694void SpriteStack3D::registerGbufferDrawer() {
695 static bool registered = false;
696 if (registered) return;
697 registered = true;
700 const glm::mat4 &viewProj, float /*aspect*/) {
701 drawGbufferStacks(gfx, viewProj, cam.eyeX, cam.eyeY, cam.eyeZ, cam.nearZ, cam.farZ);
702 });
703}
704
705void SpriteStack3D::drawGbufferStacks(eve::graphics::Graphics &gfx, const glm::mat4 &viewProj,
706 float eyeX, float eyeY, float eyeZ, float nearZ,
707 float farZ) {
708 const glm::vec3 eye(eyeX, eyeY, eyeZ);
709 std::vector<SliceDraw> slices;
710 for (SpriteStack3D *st : gbufferStacks()) {
711 if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
712 st->ensureResources(&gfx);
713 if (!st->quad_) continue;
714 slices.clear();
715 collectSlices(*st, eye, slices);
716 for (const auto &s : slices) {
717 const glm::mat4 mvp = viewProj * s.model;
718 gfx.drawMeshGBufferAlpha(st->quad_, mvp, s.model, nearZ, farZ, s.texture,
719 st->tintR_, st->tintG_, st->tintB_);
720 }
721 }
722}
723
725 gbufferEnabled_ = enabled;
726 auto &stacks = gbufferStacks();
727 auto it = std::find(stacks.begin(), stacks.end(), this);
728 if (enabled && it == stacks.end()) stacks.push_back(this);
729 if (!enabled && it != stacks.end()) stacks.erase(it);
730 registerGbufferDrawer();
731}
732
733bool SpriteStack3D::getGbufferEnabled() const { return gbufferEnabled_; }
734
735std::vector<SpriteStack3D *> &SpriteStack3D::shadowCasterStacks() {
736 static std::vector<SpriteStack3D *> stacks;
737 return stacks;
738}
739
740void SpriteStack3D::registerShadowDrawer() {
741 static bool registered = false;
742 if (registered) return;
743 registered = true;
745 [](eve::graphics::Graphics &gfx, const glm::mat4 &lightVP,
747 drawShadowCasterStacks(gfx, lightVP, cam.eyeX, cam.eyeY, cam.eyeZ);
748 });
749}
750
751void SpriteStack3D::drawShadowCasterStacks(eve::graphics::Graphics &gfx,
752 const glm::mat4 &lightVP, float eyeX, float eyeY,
753 float eyeZ) {
754 const glm::vec3 eye(eyeX, eyeY, eyeZ);
755 std::vector<SliceDraw> slices;
756 for (SpriteStack3D *st : shadowCasterStacks()) {
757 if (!st || !st->visible_ || st->layerCount_ <= 0) continue;
758 st->ensureResources(&gfx);
759 if (!st->quad_) continue;
760 slices.clear();
761 collectSlices(*st, eye, slices);
762 for (const auto &s : slices) {
763 // Camera-facing cards cast their silhouette into the light view;
764 // the alpha-cutout shadow pipeline keeps the shape instead of a
765 // solid quad.
766 gfx.drawMeshShadowAlpha(st->quad_, lightVP * s.model, s.texture);
767 }
768 }
769}
770
772 castShadow_ = cast;
773 auto &stacks = shadowCasterStacks();
774 auto it = std::find(stacks.begin(), stacks.end(), this);
775 if (cast && it == stacks.end()) stacks.push_back(this);
776 if (!cast && it != stacks.end()) stacks.erase(it);
777 registerShadowDrawer();
778}
779
780bool SpriteStack3D::getCastShadow() const { return castShadow_; }
781
783 auto &gb = gbufferStacks();
784 auto it = std::find(gb.begin(), gb.end(), this);
785 if (it != gb.end()) gb.erase(it);
786 auto &sc = shadowCasterStacks();
787 it = std::find(sc.begin(), sc.end(), this);
788 if (it != sc.end()) sc.erase(it);
789}
790
792 if (!gfx) throw eve::Exception("SpriteStack3D.render: null graphics");
793 if (!visible_ || layerCount_ <= 0) return;
794 if (!camera) camera = findActiveCamera3D();
795 if (!camera) return;
796
797 ensureResources(gfx);
798 if (!quad_ || !shader_) return;
799
800 auto cd = camera->data();
801 const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
802 const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
803 const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
804 const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
805 const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
806 const float fovRad = cd->fovYDeg * 0.017453292519943295f;
807 const glm::mat4 projM =
808 eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
809 gfx->setMesh3DViewProj(projM * viewM);
810 gfx->setMesh3DView(viewM);
811 gfx->setMesh3DClip(cd->nearZ, cd->farZ);
813 gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
814 shader_->sendFloat("alphaCutoff", alphaCutoff_);
815
816 std::vector<SliceDraw> slices;
817 slices.reserve(size_t(layerCount_));
818 collectSlices(*this, eye, slices);
819 if (slices.empty()) return;
820
821 // Far-to-near: alpha-blended slices need back-to-front draw order.
822 std::sort(slices.begin(), slices.end(),
823 [](const SliceDraw &a, const SliceDraw &b) { return a.distSq > b.distSq; });
824
825 // Projected contact shadows: each slice silhouette squashed onto the ground
826 // plane along the light direction, painted as a dark alpha blob.
827 if (shadowEnabled_ && shadowLightY_ < -1e-4f) {
828 const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, shadowOpacity_);
829 for (const auto &s : slices) {
830 const SliceDraw shadow = makeShadowDraw(*this, s, eye);
831 shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
832 gfx->drawMeshShader(quad_, shadow.model, shadow.texture, shadowColor, shader_);
833 }
834 }
835
836 // Stylized rim outline: expanded dark silhouettes behind the stack.
837 if (outlineWidth_ > 0.f) {
838 const eve::graphics::Color outlineColor(outlineR_, outlineG_, outlineB_, 1.f);
839 for (const auto &s : slices) {
840 const SliceDraw outline = makeOutlineDraw(*this, s, eye);
841 shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
842 gfx->drawMeshShader(quad_, outline.model, outline.texture, outlineColor, shader_);
843 }
844 }
845
846 const glm::vec4 tint(tintR_, tintG_, tintB_, tintA_);
847 for (const auto &s : slices) {
848 shader_->sendVec4("uvRect", s.uv.x, s.uv.y, s.uv.z, s.uv.w);
849 gfx->drawMeshShader(quad_, s.model, s.texture,
850 eve::graphics::Color(tint.r, tint.g, tint.b, tint.a),
851 shader_);
852 }
853}
854
855// ---------------------------------------------------------------------------
856// SpriteStackBatch
857// ---------------------------------------------------------------------------
858
859namespace {
860
861uint32_t packTint(float r, float g, float b, float a) {
862 return (uint32_t(clampf(r, 0.f, 1.f) * 255.f) << 24) |
863 (uint32_t(clampf(g, 0.f, 1.f) * 255.f) << 16) |
864 (uint32_t(clampf(b, 0.f, 1.f) * 255.f) << 8) |
865 uint32_t(clampf(a, 0.f, 1.f) * 255.f);
866}
867
868eve::graphics::Color unpackTint(uint32_t t) {
869 return eve::graphics::Color(float((t >> 24) & 0xff) / 255.f,
870 float((t >> 16) & 0xff) / 255.f,
871 float((t >> 8) & 0xff) / 255.f, float(t & 0xff) / 255.f);
872}
873
874} // namespace
875
877 if (!stack) throw eve::Exception("SpriteStackBatch.add: null stack");
878 if (std::find(stacks_.begin(), stacks_.end(), stack) == stacks_.end()) {
879 stacks_.push_back(stack);
880 forceRebuild_ = true;
881 }
882}
883
885 auto it = std::find(stacks_.begin(), stacks_.end(), stack);
886 if (it != stacks_.end()) {
887 stacks_.erase(it);
888 forceRebuild_ = true;
889 }
890}
891
893 stacks_.clear();
894 groups_.clear();
895 forceRebuild_ = true;
896}
897
898int SpriteStackBatch::getStackCount() const { return int(stacks_.size()); }
899
900void SpriteStackBatch::ensureShader(graphics::Graphics *gfx) {
901 if (shader_) return;
902 auto vert = copySpv(sprite_stack_vert_spv, sprite_stack_vert_spv_count);
903 auto frag = copySpv(sprite_stack_frag_spv, sprite_stack_frag_spv_count);
904 shader_ = gfx->newHairShaderFromSpv(vert, frag);
905 if (!shader_ || !shader_->gpuHandle)
906 throw eve::Exception("SpriteStackBatch.render: failed to create slice shader");
907 shader_->declareVec4("uvRect");
908 shader_->declareFloat("alphaCutoff");
909 shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f);
910 shader_->sendFloat("alphaCutoff", 0.05f);
911}
912
914 if (!gfx) throw eve::Exception("SpriteStackBatch.render: null graphics");
915 if (stacks_.empty()) return;
916 if (!camera) camera = findActiveCamera3D();
917 if (!camera) return;
918
919 ensureShader(gfx);
920 if (!shader_) return;
921
922 auto cd = camera->data();
923 const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
924 const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
925 const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
926 const float aspect = gfx->getHeight() > 0 ? float(gfx->getWidth()) / float(gfx->getHeight()) : 1.f;
927 const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
928 const float fovRad = cd->fovYDeg * 0.017453292519943295f;
929 const glm::mat4 projM =
930 eve::graphics::perspectiveVulkanRH_ZO(fovRad, aspect, cd->nearZ, cd->farZ);
931 gfx->setMesh3DViewProj(projM * viewM);
932 gfx->setMesh3DView(viewM);
933 gfx->setMesh3DClip(cd->nearZ, cd->farZ);
935 gfx->setMesh3DEnv(cd->envMap, cd->envIntensity);
936 shader_->sendFloat("alphaCutoff", 0.05f);
937 shader_->sendVec4("uvRect", 0.f, 0.f, 1.f, 1.f); // UVs are baked into the mesh
938
939 struct GroupBuild {
940 GroupKey key{};
941 struct ColoredSlice {
943 eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
944 };
945 std::vector<ColoredSlice> slices;
946 uint64_t stamp = 0;
947 float nearest = 1e30f;
948 eve::graphics::Color color{1.f, 1.f, 1.f, 1.f};
949 };
950 std::unordered_map<GroupKey, GroupBuild, GroupKeyHash> builds;
951 for (SpriteStack3D *st : stacks_) {
952 if (!st || !st->visible_) continue;
953 std::vector<SpriteStack3D::SliceDraw> tmp;
954 SpriteStack3D::collectSlices(*st, eye, tmp);
955 const uint64_t stamp = st->getVersion();
956 std::vector<GroupBuild::ColoredSlice> colored;
957 const eve::graphics::Color baseColor(st->tintR_, st->tintG_, st->tintB_, st->tintA_);
958 for (const auto &s : tmp) colored.push_back({s, baseColor});
959 if (st->shadowEnabled_ && st->shadowLightY_ < -1e-4f) {
960 const eve::graphics::Color shadowColor(0.f, 0.f, 0.f, st->shadowOpacity_);
961 for (const auto &s : tmp)
962 colored.push_back(
963 {SpriteStack3D::makeShadowDraw(*st, s, eye), shadowColor});
964 }
965 if (st->outlineWidth_ > 0.f) {
966 const eve::graphics::Color outlineColor(st->outlineR_, st->outlineG_, st->outlineB_,
967 1.f);
968 for (const auto &s : tmp)
969 colored.push_back(
970 {SpriteStack3D::makeOutlineDraw(*st, s, eye), outlineColor});
971 }
972 for (const auto &cs : colored) {
973 GroupKey key{cs.draw.texture,
974 packTint(cs.color.r, cs.color.g, cs.color.b, cs.color.a)};
975 GroupBuild &b = builds[key];
976 b.key = key;
977 b.color = unpackTint(key.tint);
978 b.slices.push_back(cs);
979 b.stamp = std::max(b.stamp, stamp);
980 b.nearest = std::min(b.nearest, cs.draw.distSq);
981 }
982 }
983 if (builds.empty()) return;
984
985 // Draw far groups first; inside each group slices are baked far-to-near.
986 std::vector<GroupBuild *> order;
987 order.reserve(builds.size());
988 for (auto &kv : builds) order.push_back(&kv.second);
989 std::sort(order.begin(), order.end(),
990 [](const GroupBuild *a, const GroupBuild *b) { return a->nearest > b->nearest; });
991
992 const glm::vec3 corners[4] = {{-0.5f, 0.5f, 0.f}, {0.5f, 0.5f, 0.f},
993 {0.5f, -0.5f, 0.f}, {-0.5f, -0.5f, 0.f}};
994 const glm::vec2 quadUVs[4] = {{0.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}, {0.f, 1.f}};
995 std::vector<float> pos, nrm, uv;
996 std::vector<uint32_t> idx;
997
998 for (GroupBuild *gb : order) {
999 Group &g = groups_[gb->key];
1000 std::sort(gb->slices.begin(), gb->slices.end(),
1001 [](const GroupBuild::ColoredSlice &a, const GroupBuild::ColoredSlice &b) {
1002 return a.draw.distSq > b.draw.distSq;
1003 });
1004 const int sliceCount = int(gb->slices.size());
1005 const int vc = sliceCount * 4;
1006 const int ic = sliceCount * 6;
1007
1008 pos.resize(size_t(vc) * 3);
1009 nrm.resize(size_t(vc) * 3);
1010 uv.resize(size_t(vc) * 2);
1011 idx.resize(size_t(ic));
1012 for (int si = 0; si < sliceCount; ++si) {
1013 const SpriteStack3D::SliceDraw &s = gb->slices[size_t(si)].draw;
1014 for (int c = 0; c < 4; ++c) {
1015 const glm::vec4 p = s.model * glm::vec4(corners[c], 1.f);
1016 const size_t vi = size_t(si * 4 + c);
1017 pos[vi * 3 + 0] = p.x;
1018 pos[vi * 3 + 1] = p.y;
1019 pos[vi * 3 + 2] = p.z;
1020 nrm[vi * 3 + 0] = 0.f;
1021 nrm[vi * 3 + 1] = 1.f;
1022 nrm[vi * 3 + 2] = 0.f;
1023 uv[vi * 2 + 0] = s.uv.x + quadUVs[c].x * (s.uv.z - s.uv.x);
1024 uv[vi * 2 + 1] = s.uv.y + quadUVs[c].y * (s.uv.w - s.uv.y);
1025 }
1026 const uint32_t base = uint32_t(si * 4);
1027 const size_t di = size_t(si) * 6;
1028 idx[di + 0] = base;
1029 idx[di + 1] = base + 1;
1030 idx[di + 2] = base + 2;
1031 idx[di + 3] = base;
1032 idx[di + 4] = base + 2;
1033 idx[di + 5] = base + 3;
1034 }
1035
1036 if (forceRebuild_ || !g.mesh) {
1037 if (g.mesh) {
1038 gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(),
1039 ic);
1040 } else {
1041 g.mesh =
1042 gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
1043 }
1044 g.vertexCapacity = vc;
1045 g.indexCapacity = ic;
1046 } else if (g.stamp != gb->stamp) {
1047 gfx->updateMeshVertices(g.mesh, pos.data(), nrm.data(), uv.data(), vc, idx.data(), ic);
1048 }
1049 g.stamp = gb->stamp;
1050 gfx->drawMeshShader(g.mesh, glm::mat4(1.f), gb->key.texture, gb->color, shader_);
1051 }
1052 forceRebuild_ = false;
1053}
1054
1055// ---------------------------------------------------------------------------
1056// Module
1057// ---------------------------------------------------------------------------
1058
1060
1061SpriteStack3D *SpriteStack::newStack(graphics::Graphics *gfx) {
1062 if (!gfx) throw eve::Exception("SpriteStack.newStack: null graphics");
1063 return new SpriteStack3D();
1064}
1065
1066SpriteStackBatch *SpriteStack::newBatch(graphics::Graphics *gfx) {
1067 if (!gfx) throw eve::Exception("SpriteStack.newBatch: null graphics");
1068 return new SpriteStackBatch();
1069}
1070
1071std::vector<image::ImageData *> SpriteStack::slicePrimitive(const std::string &kind, int layerCount,
1072 int imageW, int imageH,
1073 const std::string &axis,
1074 float thickness) {
1075 SliceOptions opt;
1076 opt.layerCount = layerCount;
1077 opt.imageW = imageW;
1078 opt.imageH = imageH;
1079 opt.axis = axis;
1080 opt.thickness = thickness;
1081 return slicePrimitiveToLayers(kind, opt);
1082}
1083
1084std::vector<image::ImageData *> SpriteStack::sliceModel(model3d::ModelData *model, int layerCount,
1085 int imageW, int imageH,
1086 const std::string &axis, float thickness) {
1087 SliceOptions opt;
1088 opt.layerCount = layerCount;
1089 opt.imageW = imageW;
1090 opt.imageH = imageH;
1091 opt.axis = axis;
1092 opt.thickness = thickness;
1093 return sliceModelToLayers(model, opt);
1094}
1095
1096void SpriteStack::expose(ssq::Table &table) {
1097 auto cls = table.addClass(name, SpriteStack::create, false);
1098 expose(cls);
1099
1100 auto stack = table.addClass<SpriteStack3D>(
1101 "SpriteStack3D",
1102 std::function<SpriteStack3D *()>([]() -> SpriteStack3D * { return nullptr; }), true);
1103 stack.addFunc("setLayerCount", &SpriteStack3D::setLayerCount);
1104 stack.addFunc("getLayerCount", &SpriteStack3D::getLayerCount);
1105 stack.addFunc("setLayerTexture", &SpriteStack3D::setLayerTexture);
1106 stack.addFunc("getLayerTexture", &SpriteStack3D::getLayerTexture);
1107 stack.addFunc("setLayerImage", &SpriteStack3D::setLayerImage);
1108 stack.addFunc("setLayerFile", &SpriteStack3D::setLayerFile);
1109 stack.addFunc("setLayersFromAtlas", &SpriteStack3D::setLayersFromAtlas);
1110 stack.addFunc("setThickness", &SpriteStack3D::setThickness);
1111 stack.addFunc("getThickness", &SpriteStack3D::getThickness);
1112 stack.addFunc("setSize", &SpriteStack3D::setSize);
1113 stack.addFunc("getWidth", &SpriteStack3D::getWidth);
1114 stack.addFunc("getHeight", &SpriteStack3D::getHeight);
1115 stack.addFunc("setPosition", &SpriteStack3D::setPosition);
1116 stack.addFunc("setYaw", &SpriteStack3D::setYaw);
1117 stack.addFunc("setTint", &SpriteStack3D::setTint);
1118 stack.addFunc("setAlphaCutoff", &SpriteStack3D::setAlphaCutoff);
1119 stack.addFunc("setVisible", &SpriteStack3D::setVisible);
1120 stack.addFunc("getVisible", &SpriteStack3D::getVisible);
1121 stack.addFunc("setMode", &SpriteStack3D::setMode);
1122 stack.addFunc("getMode", &SpriteStack3D::getMode);
1123 stack.addFunc("setShadowEnabled", &SpriteStack3D::setShadowEnabled);
1124 stack.addFunc("getShadowEnabled", &SpriteStack3D::getShadowEnabled);
1125 stack.addFunc("setShadowOpacity", &SpriteStack3D::setShadowOpacity);
1126 stack.addFunc("setShadowLight", &SpriteStack3D::setShadowLight);
1127 stack.addFunc("setShadowPlaneY", &SpriteStack3D::setShadowPlaneY);
1128 stack.addFunc("setOutline", &SpriteStack3D::setOutline);
1129 stack.addFunc("getOutlineWidth", &SpriteStack3D::getOutlineWidth);
1130 stack.addFunc("setOutlineColor", &SpriteStack3D::setOutlineColor);
1131 stack.addFunc("setGbufferEnabled", &SpriteStack3D::setGbufferEnabled);
1132 stack.addFunc("getGbufferEnabled", &SpriteStack3D::getGbufferEnabled);
1133 stack.addFunc("setCastShadow", &SpriteStack3D::setCastShadow);
1134 stack.addFunc("getCastShadow", &SpriteStack3D::getCastShadow);
1135 stack.addFunc(
1136 "render",
1137 std::function<void(SpriteStack3D *, graphics::Graphics *)>([](SpriteStack3D *self,
1138 graphics::Graphics *gfx) {
1139 if (self) self->render(gfx);
1140 }));
1141 stack.addFunc("renderWithCamera", &SpriteStack3D::render);
1142
1143 auto batch = table.addClass<SpriteStackBatch>(
1144 "SpriteStackBatch",
1145 std::function<SpriteStackBatch *()>([]() -> SpriteStackBatch * { return nullptr; }), true);
1146 batch.addFunc("add", &SpriteStackBatch::add);
1147 batch.addFunc("remove", &SpriteStackBatch::remove);
1148 batch.addFunc("clear", &SpriteStackBatch::clear);
1149 batch.addFunc("getStackCount", &SpriteStackBatch::getStackCount);
1150 batch.addFunc(
1151 "render",
1152 std::function<void(SpriteStackBatch *, graphics::Graphics *)>([](SpriteStackBatch *self,
1153 graphics::Graphics *gfx) {
1154 if (self) self->render(gfx);
1155 }));
1156 batch.addFunc("renderWithCamera", &SpriteStackBatch::render);
1157}
1158
1159void SpriteStack::expose(ssq::Class &cls) {
1160 cls.addFunc("getName", &SpriteStack::getName);
1161 cls.addFunc("newStack", &SpriteStack::newStack);
1162 cls.addFunc("newBatch", &SpriteStack::newBatch);
1163 cls.addFunc("slicePrimitive", &SpriteStack::slicePrimitive);
1164 cls.addFunc("sliceModel", &SpriteStack::sliceModel);
1165}
1166
1167} // namespace eve::spritestack
Tok kind
HSQOBJECT cls
Definition ECS.cpp:21
vk::ShaderModule vert
vk::ShaderModule frag
int y
Definition Grass.cpp:135
int z
Definition Grass.cpp:135
uint32_t i1
Definition Grass.cpp:62
uint32_t i2
Definition Grass.cpp:62
uint32_t i0
Definition Grass.cpp:62
float height
Definition Grass.cpp:235
float u
Definition Grass.cpp:234
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
std::vector< Colorf > px
float depth
float thickness
uint32_t a
uint32_t b
uint32_t c
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:24
int width
int idx
float f
glm::vec3 eye
glm::vec4 p[6]
float fovRad
glm::mat4 viewProj
glm::mat4 model
glm::mat4 proj
Light2D::Data * data
const char * name
Definition RockMesh.cpp:21
bool enabled
int d
int v
image::ImageData::Colorf color
float m[16]
uint32_t s
Definition Weather.cpp:28
virtual void drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f)=0
GBuffer fill with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): same ou...
virtual void setMesh3DClip(float nearZ, float farZ)=0
Near/far used to pack linear depth into scene color A (SSGI).
virtual void setMesh3DViewProj(const glm::mat4 &viewProj)=0
virtual void setMesh3DCameraPos(const glm::vec3 &eye)=0
Camera eye used by mesh shaders that need view/rim (stored in Mesh3DUBO).
virtual void setMesh3DEnv(Texture *cube, float intensity)=0
Specular IBL environment for subsequent default mesh draws. cube must be from newCubemap (or nullptr ...
virtual bool updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
In-place update of a mesh's vertex/index data (CPU -> host-visible VBO). Mirrors bakeMeshMorph: the u...
virtual void setMesh3DView(const glm::mat4 &view)=0
Camera view matrix for subsequent drawMesh (view-space depth / CSM select).
virtual Shader * newHairShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv)=0
Hair/fur card shader (alpha blend + Kajiya-Kay). Empty vert → mesh3d_hair.vert. Owned by Graphics.
virtual void drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo=nullptr)=0
Shadow pass draw with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): tra...
Texture * newTextureFromImageData(image::ImageData *data, bool repeatU=false, bool repeatV=false)
Definition Graphics.cpp:878
virtual Texture * newTextureFromFile(const std::string &filename)=0
virtual void drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, Shader *shader)=0
Draw mesh with an explicit Mesh3D Shader (nullptr = default PBR pipeline).
virtual Mesh * newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
Upload a triangle mesh from packed CPU arrays. Owned by Graphics. posXYZ required (vertexCount*3)....
int getHeight() const
Definition Graphics.h:118
static void addShadowExtraDrawer(ShadowExtraDrawer drawer)
static void addGBufferExtraDrawer(GBufferExtraDrawer drawer)
void sendFloat(const std::string &name, float x)
Definition Shader.cpp:82
int declareVec4(const std::string &name)
Definition Shader.cpp:33
int declareFloat(const std::string &name)
Reserve sequential float slots in the push-constant block. Returns start index.
Definition Shader.cpp:30
void sendVec4(const std::string &name, float x, float y, float z, float w)
Definition Shader.cpp:94
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
Represents raw pixel data.
Definition ImageData.h:26
medialoader::Colorf Colorf
Definition ImageData.h:29
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Definition ModelData.h:23
A renderable pseudo-3D sprite stack: a column of layer textures drawn as alpha-blended slices inside ...
Definition SpriteStack.h:98
void setOutlineColor(float r, float g, float b)
void setShadowLight(float dx, float dy, float dz)
void setLayerImage(graphics::Graphics *gfx, image::ImageData *img, int index)
Upload RGBA8 ImageData layers (convenience around setLayerTexture).
void setTint(float r, float g, float b, float a=1.f)
void render(graphics::Graphics *gfx, graphics::Camera3D *camera=nullptr) const
Draw all slices into the currently open 3D scene pass (Vulkan). Uses the active Camera3D when camera ...
void setSize(float width, float height)
Quad size for every slice (world units). Default 1 x 1.
void setLayerTexture(graphics::Texture *texture, int index)
Null texture clears the slot (that layer is skipped).
void setPosition(float x, float y, float z)
graphics::Texture * getLayerTexture(int index) const
void setMode(const std::string &mode)
void setLayerFile(graphics::Graphics *gfx, const std::string &path, int index)
Upload each path via Graphics::newTextureFromFile (reloads in place).
void setAlphaCutoff(float cutoff)
void setOutline(float width, float r=0.f, float g=0.f, float b=0.f)
Stylized rim outline: an expanded dark silhouette behind every slice.
void setGbufferEnabled(bool enabled)
Contribute this stack to the G-buffer (via RenderSystem3D's extra-drawer hook) so post-processing tha...
void setCastShadow(bool cast)
Cast real CSM shadows: the stack's slices are drawn into the cascaded shadow map through the alpha-cu...
void setShadowEnabled(bool enabled)
Pseudo-3D projected contact shadow (soft dark silhouette on the ground).
void setShadowOpacity(float opacity)
void setLayersFromAtlas(graphics::Graphics *gfx, graphics::Texture *atlas, int layerCount)
Split one horizontal atlas strip into layerCount layers (layer i = columns [i/count....
void setThickness(float thickness)
World-space spacing between consecutive slices.
Multi-stack batching: draws every visible slice of the registered stacks as ONE draw call per (textur...
void remove(SpriteStack3D *stack)
void render(graphics::Graphics *gfx, graphics::Camera3D *camera=nullptr)
void add(SpriteStack3D *stack)
SpriteStack module: CPU slicing + SpriteStack3D factory.
float clampf(float v, float lo, float hi)
Definition AnimMath.h:31
glm::mat4 perspectiveVulkanRH_ZO(float fovyRad, float aspect, float zNear, float zFar)
Right-handed, zero-to-one depth perspective for Vulkan swapchains.
Definition ClipSpace.h:20
glm::vec4 Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
std::vector< image::ImageData * > sliceMeshToLayers(const SliceInput &input, const SliceOptions &opt)
Slice a triangle mesh into layerCount RGBA8 layer images (caller owns the returned ImageData)....
std::vector< image::ImageData * > slicePrimitiveToLayers(const std::string &kind, const SliceOptions &opt)
Build a procedural CPU mesh ("box" | "cylinder" | "sphere" | "cone") and slice it — a self-contained ...
std::vector< image::ImageData * > sliceModelToLayers(model3d::ModelData *model, const SliceOptions &opt)
Slice every mesh of a decoded model (assimp scene) with the same options.
CPU inputs for slicing a triangle mesh into sprite-stack layers.
Definition SpriteStack.h:38
Slicer options. The mesh is cut into layerCount thin slabs along axis ("x" | "y" | "z"); each slab is...
Definition SpriteStack.h:52
One slice instance with its baked world transform and layer UV rect.