28const std::array<const char *, 18> kParamSlots = {
29 "time",
"frameDuration",
"grassWidth",
"grassHeight",
"alphaCutoff",
30 "alwaysDark",
"lightGreenX",
"lightGreenY",
"lightGreenZ",
"darkGreenX",
31 "darkGreenY",
"darkGreenZ",
"frameCount",
"atlasCols",
"atlasRows",
32 "grassVariantCount",
"leafVariantCount",
"leafRowOffset"};
34std::vector<uint32_t> copySpv(
const uint32_t *
data,
size_t count) {
35 return std::vector<uint32_t>(
data,
data + count);
38float radicalInverse(uint32_t
n, uint32_t base) {
39 const float inv = 1.f / float(base);
43 val += float(
n % base) *
f;
50float wrap01(
float x) {
51 x =
x - std::floor(
x);
52 return x < 0.f ?
x + 1.f :
x;
55uint32_t mixSeed(uint32_t
seed, uint32_t i) {
56 seed ^= 0x9e3779b9u + (i << 6) + (i >> 2);
64 glm::vec3
n{0.f, 1.f, 0.f};
67glm::vec3 readPos(
const float *
pos,
int i) {
68 return glm::vec3(
pos[i * 3],
pos[i * 3 + 1],
pos[i * 3 + 2]);
71glm::vec3 readNrm(
const float *nrm,
int i) {
72 if (!nrm)
return glm::vec3(0.f, 1.f, 0.f);
73 return glm::vec3(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
76bool buildTriangles(
const float *posXYZ,
const float *nrmXYZ,
int vertexCount,
77 const uint32_t *indices,
int indexCount,
float minSlopeDot,
78 std::vector<Triangle> &tris, std::vector<float> &cdf) {
81 if (!posXYZ || !indices || vertexCount < 3 || indexCount < 3)
return false;
82 if (indexCount % 3 != 0)
return false;
85 for (
int t = 0; t + 2 < indexCount; t += 3) {
86 const uint32_t
i0 = indices[t];
87 const uint32_t
i1 = indices[t + 1];
88 const uint32_t
i2 = indices[t + 2];
89 if (
int(
i0) >= vertexCount ||
int(
i1) >= vertexCount ||
int(
i2) >= vertexCount)
continue;
90 const glm::vec3
a = readPos(posXYZ,
int(
i0));
91 const glm::vec3
b = readPos(posXYZ,
int(
i1));
92 const glm::vec3
c = readPos(posXYZ,
int(
i2));
93 glm::vec3
n = glm::cross(
b -
a,
c -
a);
94 const float twiceArea = glm::length(
n);
95 if (twiceArea < 1e-10f)
continue;
98 glm::vec3 ns = readNrm(nrmXYZ,
int(
i0)) + readNrm(nrmXYZ,
int(
i1)) + readNrm(nrmXYZ,
int(
i2));
99 if (glm::dot(ns, ns) > 1e-8f)
n = glm::normalize(ns);
101 if (
n.y < minSlopeDot)
continue;
106 tri.area = 0.5f * twiceArea;
110 cdf.push_back(total);
112 return total > 1e-12f && !tris.empty();
115int pickTriangle(
const std::vector<float> &cdf,
float u) {
116 const float target =
u * cdf.back();
117 auto it = std::lower_bound(cdf.begin(), cdf.end(), target);
118 int idx = int(it - cdf.begin());
119 if (
idx >=
int(cdf.size()))
idx = int(cdf.size()) - 1;
123void sampleOnTriangle(
const float *posXYZ,
const Triangle &tri,
float u,
float v, glm::vec3 &
p) {
128 const glm::vec3
a = readPos(posXYZ,
int(tri.i0));
129 const glm::vec3
b = readPos(posXYZ,
int(tri.i1));
130 const glm::vec3
c = readPos(posXYZ,
int(tri.i2));
136 bool operator==(
const GridKey &o)
const {
return x == o.x &&
y == o.y &&
z == o.z; }
140 size_t operator()(
const GridKey &k)
const {
141 return size_t(k.x) * 73856093u ^ size_t(k.y) * 19349663u ^ size_t(k.z) * 83492791u;
145GridKey toKey(
const glm::vec3 &
p,
float cell) {
146 return {int(std::floor(
p.x / cell)), int(std::floor(
p.y / cell)), int(std::floor(
p.z / cell))};
149bool tooClose(
const glm::vec3 &
p,
float radius,
150 const std::vector<Point> &accepted,
151 const std::unordered_map<GridKey, std::vector<int>, GridKeyHash> &grid,
float cell) {
152 const GridKey
c = toKey(
p, cell);
153 const float r2 = radius * radius;
154 for (
int dz = -1; dz <= 1; ++dz) {
155 for (
int dy = -1; dy <= 1; ++dy) {
156 for (
int dx = -1; dx <= 1; ++dx) {
157 GridKey k{
c.x + dx,
c.y + dy,
c.z + dz};
158 auto it = grid.find(k);
159 if (it == grid.end())
continue;
160 for (
int idx : it->second) {
161 const glm::vec3
d = accepted[size_t(
idx)].position -
p;
162 if (glm::dot(
d,
d) < r2)
return true;
170float hash01(uint32_t
id) {
171 float h = std::fmod(
float(
id) * 0.61803398875f, 1.f);
172 if (
h < 0.f)
h += 1.f;
176float clampf(
float x,
float lo,
float hi) {
return std::min(hi, std::max(lo,
x)); }
178float coverEdge(
float distPx,
float radiusPx) {
180 return clampf(radiusPx + 0.65f - distPx, 0.f, 1.f);
183void stampOver(std::vector<uint8_t> &rgba,
int w,
int h,
int x,
int y,
float luma,
float a) {
184 if (
x < 0 || y < 0 || x >=
w ||
y >=
h)
return;
186 luma =
clampf(luma, 0.f, 1.f);
187 if (
a < 0.02f)
return;
188 const size_t i = (size_t(
y) * size_t(
w) + size_t(
x)) * 4u;
189 const float oa = float(rgba[i + 3]) / 255.f;
190 const float or_ = float(rgba[i + 0]) / 255.f;
191 const float og = float(rgba[i + 1]) / 255.f;
192 const float ob = float(rgba[i + 2]) / 255.f;
193 const float outA =
a + oa * (1.f -
a);
194 if (outA < 1e-4f)
return;
195 const float nr = (luma *
a + or_ * oa * (1.f -
a)) / outA;
196 const float ng = (luma *
a + og * oa * (1.f -
a)) / outA;
197 const float nb = (luma *
a + ob * oa * (1.f -
a)) / outA;
198 rgba[i + 0] = uint8_t(std::round(
clampf(nr, 0.f, 1.f) * 255.f));
199 rgba[i + 1] = uint8_t(std::round(
clampf(ng, 0.f, 1.f) * 255.f));
200 rgba[i + 2] = uint8_t(std::round(
clampf(nb, 0.f, 1.f) * 255.f));
201 rgba[i + 3] = uint8_t(std::round(
clampf(outA, 0.f, 1.f) * 255.f));
204void stampBlade(std::vector<uint8_t> &rgba,
int atlasW,
int atlasH,
int ox,
int frameW,
int frameH,
207 const float fw = float(frameW);
208 const float fh = float(frameH);
210 for (
int y = 0;
y < frameH; ++
y) {
211 for (
int x = 0;
x < frameW; ++
x) {
212 const float u = (float(
x) + 0.5f) / fw;
213 const float v = 1.f - (float(
y) + 0.5f) / fh;
214 if (v < -0.02f || v >
height + 0.04f)
continue;
216 const float cx = baseU +
lean * t * t;
218 const float dist = std::abs(
u -
cx) * fw;
219 float cov = coverEdge(dist, half * fw);
221 cov *=
clampf((
v + 0.02f) / 0.05f, 0.f, 1.f);
222 if (cov > 0.5f) cov = 1.f;
223 else if (cov < 0.25f) cov = 0.f;
224 if (cov <= 0.f)
continue;
225 const float side = (
u -
cx) * fw;
227 if (side > 0.15f) luma *= 0.78f;
228 stampOver(rgba, atlasW, atlasH, ox +
x,
y, luma, cov);
243void stampTuft(std::vector<uint8_t> &rgba,
int atlasW,
int atlasH,
int ox,
int frameW,
int frameH,
246 const float fw = float(frameW);
247 const float fh = float(frameH);
248 for (
int y = 0;
y < frameH; ++
y) {
249 for (
int x = 0;
x < frameW; ++
x) {
250 const float u = (float(
x) + 0.5f) / fw;
251 const float v = 1.f - (float(
y) + 0.5f) / fh;
252 if (
v > 0.16f)
continue;
253 const float cx = 0.50f + wind * 0.02f;
254 const float hw = 0.16f * (1.f -
v / 0.16f);
255 float cov = coverEdge(std::abs(
u -
cx) * fw, hw * fw);
256 if (cov > 0.5f) cov = 1.f;
257 else if (cov < 0.25f) cov = 0.f;
258 if (cov > 0.f) stampOver(rgba, atlasW, atlasH, ox +
x,
y, 0.72f, cov);
263 const BladeDesc blades[] = {
264 {0.50f, 0.98f, 0.02f, 0.070f, 0.018f, 0.86f, 1.00f}, {0.38f, 0.90f, -0.14f, 0.062f, 0.016f, 0.82f, 0.97f},
265 {0.62f, 0.92f, 0.16f, 0.062f, 0.016f, 0.82f, 0.97f}, {0.28f, 0.74f, -0.26f, 0.056f, 0.016f, 0.78f, 0.93f},
266 {0.72f, 0.76f, 0.28f, 0.056f, 0.016f, 0.78f, 0.93f}, {0.44f, 0.84f, -0.06f, 0.050f, 0.014f, 0.84f, 0.98f},
267 {0.56f, 0.86f, 0.08f, 0.050f, 0.014f, 0.84f, 0.98f},
269 const float outline = 1.15f / fw;
270 for (
const BladeDesc &
b : blades) {
271 const float lean =
b.lean + wind * 0.32f;
272 const float u =
b.u + wind * 0.03f;
273 stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH,
u,
b.height,
lean,
b.baseHalf + outline,
274 b.tipHalf + outline * 0.5f, 0.42f, 0.55f);
275 stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH,
u,
b.height,
lean,
b.baseHalf,
b.tipHalf,
285 if (index < 0 || index >=
int(kParamSlots.size()))
return {};
286 return kParamSlots[size_t(index)];
291 shader->declareFloat(
"time");
292 shader->declareFloat(
"frameDuration");
293 shader->declareFloat(
"grassWidth");
294 shader->declareFloat(
"grassHeight");
295 shader->declareFloat(
"alphaCutoff");
296 shader->declareFloat(
"alwaysDark");
297 shader->declareVec3(
"lightGreen");
298 shader->declareVec3(
"darkGreen");
299 shader->declareFloat(
"frameCount");
300 shader->declareFloat(
"atlasCols");
301 shader->declareFloat(
"atlasRows");
302 shader->declareFloat(
"grassVariantCount");
303 shader->declareFloat(
"leafVariantCount");
304 shader->declareFloat(
"leafRowOffset");
305 shader->sendFloat(
"time", 0.f);
306 shader->sendFloat(
"frameDuration", 0.12f);
307 shader->sendFloat(
"grassWidth", 0.62f);
308 shader->sendFloat(
"grassHeight", 0.95f);
309 shader->sendFloat(
"alphaCutoff", 0.35f);
310 shader->sendFloat(
"alwaysDark", 0.f);
311 shader->sendVec3(
"lightGreen", 0.58f, 0.84f, 0.26f);
312 shader->sendVec3(
"darkGreen", 0.10f, 0.28f, 0.12f);
313 shader->sendFloat(
"frameCount", 4.f);
314 shader->sendFloat(
"atlasCols", 2.f);
315 shader->sendFloat(
"atlasRows", 2.f);
316 shader->sendFloat(
"grassVariantCount", 1.f);
317 shader->sendFloat(
"leafVariantCount", 1.f);
318 shader->sendFloat(
"leafRowOffset", 0.f);
323 shader->sendFloat(
"frameCount",
float(std::max(info.
frames, 1)));
333 shader->sendFloat(
"alwaysDark", alwaysDark ? 1.f : 0.f);
338 shader->sendFloat(
"time", seconds);
343 shader->sendFloat(
"frameDuration", seconds > 1e-4f ? seconds : 1e-4f);
347 if (!gfx)
throw eve::Exception(
"grass::createShader: null graphics");
348 auto vert = copySpv(mesh3d_grass_vert_spv, mesh3d_grass_vert_spv_count);
349 auto frag = copySpv(mesh3d_grass_frag_spv, mesh3d_grass_frag_spv_count);
352 throw eve::Exception(
"grass::createShader: failed to create grass shader");
357int swayFrame(
float time,
float frameDuration, uint32_t instanceId,
int frameCount) {
358 if (frameCount < 1) frameCount = 1;
359 const float dur = frameDuration > 1e-4f ? frameDuration : 1e-4f;
360 const float t = time / dur + hash01(instanceId) * float(frameCount);
361 int f = int(std::floor(t)) % frameCount;
362 if (
f < 0)
f += frameCount;
366int swayAtlasWidth(
int frameW,
int frames) {
return std::max(frameW, 1) * std::max(frames, 1); }
370 frameW = std::max(frameW, 8);
371 frameH = std::max(frameH, 8);
372 frames = std::max(frames, 1);
375 rgbaOut.assign(
size_t(
w *
h * 4), 0);
377 for (
int f = 0;
f < frames; ++
f) {
378 const float wind = (float(
f) - 1.5f) / 1.5f;
379 stampTuft(rgbaOut,
w,
h,
f * frameW, frameW, frameH, wind);
384 if (!gfx)
throw eve::Exception(
"grass::createSwayAtlas: null graphics");
385 std::vector<uint8_t> rgba;
395bool readWholeFile(
const std::string &path, std::vector<char> &out) {
396 std::ifstream in(path, std::ios::binary);
397 if (!in)
return false;
398 out.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
399 return in.good() || !out.empty();
402void maskToTintable(std::vector<uint8_t> &rgba) {
403 for (
size_t i = 0; i + 3 < rgba.size(); i += 4) {
404 const uint8_t luma = std::max(rgba[i], std::max(rgba[i + 1], rgba[i + 2]));
405 const uint8_t
a = std::max(luma, rgba[i + 3]);
413bool loadSwayMaskPng(
const std::string &path, std::vector<uint8_t> &rgba,
int &
w,
int &
h) {
414 std::vector<char> raw;
415 if (!readWholeFile(path, raw))
return false;
416 eve::image::Image::create();
418 std::unique_ptr<eve::image::ImageData> img(eve::image::Image::create()->newImageData(&bytes));
419 if (!img)
return false;
421 h = img->getHeight();
422 if (
w < 2 ||
h < 2)
return false;
423 const size_t n = size_t(
w) * size_t(
h) * 4u;
424 if (img->getSize() <
n)
return false;
426 std::memcpy(rgba.data(), img->getData(),
n);
427 maskToTintable(rgba);
431void blitRgba(std::vector<uint8_t> &dst,
int dw,
int dh,
int dx,
int dy,
432 const std::vector<uint8_t> &src,
int sw,
int sh) {
433 for (
int y = 0;
y < sh; ++
y) {
434 const int ty = dy +
y;
435 if (ty < 0 || ty >= dh)
continue;
436 for (
int x = 0;
x < sw; ++
x) {
437 const int tx = dx +
x;
438 if (tx < 0 || tx >= dw)
continue;
439 const size_t si = (size_t(
y) * size_t(sw) + size_t(
x)) * 4u;
440 const size_t di = (size_t(ty) * size_t(dw) + size_t(tx)) * 4u;
441 dst[di + 0] = src[si + 0];
442 dst[di + 1] = src[si + 1];
443 dst[di + 2] = src[si + 2];
444 dst[di + 3] = src[si + 3];
452 const std::vector<std::string> &leafFiles, std::vector<uint8_t> &rgbaOut,
454 if (grassFiles.empty())
throw eve::Exception(
"grass::packSwayAtlasRGBA: no grass atlas files");
457 std::vector<uint8_t> rgba;
461 auto loadOne = [](
const std::string &path) {
463 if (!loadSwayMaskPng(path, img.rgba, img.w, img.h))
464 throw eve::Exception(
"grass::packSwayAtlasRGBA: failed to load '%s'", path.c_str());
468 std::vector<Loaded> grass;
469 grass.reserve(grassFiles.size());
470 for (
const auto &
p : grassFiles) grass.push_back(loadOne(
p));
471 std::vector<Loaded> leaf;
472 leaf.reserve(leafFiles.size());
473 for (
const auto &
p : leafFiles) leaf.push_back(loadOne(
p));
475 const int tw = grass.front().w;
476 const int th = grass.front().h;
477 auto checkSize = [tw, th](
const Loaded &img,
const char *
kind) {
478 if (img.w != tw || img.h != th)
479 throw eve::Exception(
"grass::packSwayAtlasRGBA: %s atlas size mismatch (%dx%d vs %dx%d)",
480 kind, img.w, img.h, tw, th);
482 for (
const auto &img : grass) checkSize(img,
"grass");
483 for (
const auto &img : leaf) checkSize(img,
"leaf");
485 const int nGrass = int(grass.size());
486 const int nLeaf = int(leaf.size());
487 const int nX = std::max(nGrass, std::max(nLeaf, 1));
488 const int nY = nLeaf > 0 ? 2 : 1;
495 info.
width = nX * tw;
497 rgbaOut.assign(
size_t(info.
width) *
size_t(info.
height) * 4u, 0);
499 for (
int i = 0; i < nGrass; ++i)
500 blitRgba(rgbaOut, info.
width, info.
height, i * tw, 0, grass[
size_t(i)].rgba, tw, th);
501 for (
int i = 0; i < nLeaf; ++i)
502 blitRgba(rgbaOut, info.
width, info.
height, i * tw, th, leaf[
size_t(i)].rgba, tw, th);
506 const std::vector<std::string> &leafFiles,
508 if (!gfx)
throw eve::Exception(
"grass::createSwayAtlasFromFiles: null graphics");
510 std::vector<uint8_t> rgba;
512 if (infoOut) *infoOut = info;
518std::vector<Point>
sampleHalton(
const float *posXYZ,
const float *nrmXYZ,
int vertexCount,
519 const uint32_t *indices,
int indexCount,
int count, uint32_t
seed,
521 std::vector<Point> out;
522 if (count <= 0)
return out;
523 std::vector<Triangle> tris;
524 std::vector<float> cdf;
525 if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, minSlopeDot, tris, cdf))
528 out.reserve(
size_t(count));
529 for (
int i = 0; i < count; ++i) {
530 const uint32_t
n = mixSeed(
seed, uint32_t(i + 1));
531 const float uTri = wrap01(radicalInverse(
n, 2));
532 const float u = wrap01(radicalInverse(
n, 3));
533 const float v = wrap01(radicalInverse(
n, 5));
534 const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))];
536 sampleOnTriangle(posXYZ, tri,
u,
v,
p.position);
539 p.scale = 0.85f + 0.3f * hash01(
p.id +
seed);
545std::vector<Point>
samplePoisson(
const float *posXYZ,
const float *nrmXYZ,
int vertexCount,
546 const uint32_t *indices,
int indexCount,
548 std::vector<Point> accepted;
551 std::vector<Triangle> tris;
552 std::vector<float> cdf;
553 if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, params.
minSlopeDot, tris,
557 const float cell = params.
radius;
558 std::unordered_map<GridKey, std::vector<int>, GridKeyHash> grid;
561 for (
int i = 0; i < attempts && int(accepted.size()) < params.
maxPoints; ++i) {
562 const uint32_t
n = mixSeed(params.
seed, uint32_t(i + 1));
563 const float uTri = wrap01(radicalInverse(
n, 2));
564 const float u = wrap01(radicalInverse(
n, 3));
565 const float v = wrap01(radicalInverse(
n, 5));
566 const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))];
568 sampleOnTriangle(posXYZ, tri,
u,
v,
pos);
569 if (tooClose(
pos, params.
radius, accepted, grid, cell))
continue;
574 p.id = uint32_t(accepted.size());
575 p.scale = 0.85f + 0.3f * hash01(
p.id + params.
seed);
576 const int idx = int(accepted.size());
577 accepted.push_back(
p);
578 grid[toKey(
pos, cell)].push_back(
idx);
588 const size_t n =
points.size();
590 mesh.nrmXYZ.reserve(
n * 4 * 3);
591 mesh.uvST.reserve(
n * 4 * 2);
592 mesh.indices.reserve(
n * 6);
596 const float cu[4] = {0.f, 1.f, 1.f, 0.f};
597 const float cv[4] = {0.f, 0.f, 1.f, 1.f};
598 const uint32_t corners[6] = {0, 1, 2, 0, 2, 3};
600 for (
size_t i = 0; i <
n; ++i) {
602 const uint32_t base = uint32_t(i * 4);
603 for (
int c = 0;
c < 4; ++
c) {
604 mesh.posXYZ.push_back(
p.position.x);
605 mesh.posXYZ.push_back(
p.position.y);
606 mesh.posXYZ.push_back(
p.position.z);
607 mesh.nrmXYZ.push_back(
float(
p.id));
608 mesh.nrmXYZ.push_back(
p.scale > 1e-3f ?
p.scale : 1.f);
609 mesh.nrmXYZ.push_back(alwaysDark ? 1.f : 0.f);
610 mesh.uvST.push_back(cu[
c]);
611 mesh.uvST.push_back(cv[
c]);
613 for (uint32_t k : corners)
mesh.indices.push_back(base + k);
618void makePlane(
float sizeX,
float sizeZ,
int segX,
int segZ, std::vector<float> &posXYZ,
619 std::vector<float> &nrmXYZ, std::vector<uint32_t> &indices) {
620 segX = std::max(segX, 1);
621 segZ = std::max(segZ, 1);
622 sizeX = std::max(sizeX, 1e-3f);
623 sizeZ = std::max(sizeZ, 1e-3f);
624 const int nx = segX + 1;
625 const int nz = segZ + 1;
629 posXYZ.reserve(
size_t(nx * nz * 3));
630 nrmXYZ.reserve(
size_t(nx * nz * 3));
631 indices.reserve(
size_t(segX * segZ * 6));
633 for (
int z = 0;
z < nz; ++
z) {
634 for (
int x = 0;
x < nx; ++
x) {
635 const float px = (float(
x) / float(segX) - 0.5f) * sizeX;
636 const float pz = (float(
z) / float(segZ) - 0.5f) * sizeZ;
637 posXYZ.push_back(
px);
638 posXYZ.push_back(0.f);
639 posXYZ.push_back(pz);
640 nrmXYZ.push_back(0.f);
641 nrmXYZ.push_back(1.f);
642 nrmXYZ.push_back(0.f);
645 for (
int z = 0;
z < segZ; ++
z) {
646 for (
int x = 0;
x < segX; ++
x) {
647 const uint32_t
i0 = uint32_t(
z * nx +
x);
648 const uint32_t
i1 =
i0 + 1;
649 const uint32_t
i2 =
i0 + uint32_t(nx);
650 const uint32_t i3 =
i2 + 1;
651 indices.push_back(
i0);
652 indices.push_back(
i2);
653 indices.push_back(
i1);
654 indices.push_back(
i1);
655 indices.push_back(
i2);
656 indices.push_back(i3);