载入中...
搜索中...
未找到
Grass.cpp
浏览该文件的文档.
1#include "graphics/Grass.h"
2
3#include "common/Exception.h"
4#include "data/ByteData.h"
5#include "graphics/Graphics.h"
6#include "graphics/Mesh.h"
7#include "graphics/Shader.h"
8#include "graphics/Texture.h"
10#include "graphics/shaders/mesh3d_grass_frag_spv.inc"
11#include "graphics/shaders/mesh3d_grass_vert_spv.inc"
12#include "image/Image.h"
13#include "image/ImageData.h"
14
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <cstring>
19#include <fstream>
20#include <iterator>
21#include <memory>
22#include <unordered_map>
23#include <utility>
24
26namespace {
27
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"};
33
34std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
35 return std::vector<uint32_t>(data, data + count);
36}
37
38float radicalInverse(uint32_t n, uint32_t base) {
39 const float inv = 1.f / float(base);
40 float f = inv;
41 float val = 0.f;
42 while (n > 0) {
43 val += float(n % base) * f;
44 n /= base;
45 f *= inv;
46 }
47 return val;
48}
49
50float wrap01(float x) {
51 x = x - std::floor(x);
52 return x < 0.f ? x + 1.f : x;
53}
54
55uint32_t mixSeed(uint32_t seed, uint32_t i) {
56 seed ^= 0x9e3779b9u + (i << 6) + (i >> 2);
57 seed *= 0x85ebca6bu;
58 return seed ^ (seed >> 13);
59}
60
61struct Triangle {
62 uint32_t i0 = 0, i1 = 0, i2 = 0;
63 float area = 0.f;
64 glm::vec3 n{0.f, 1.f, 0.f};
65};
66
67glm::vec3 readPos(const float *pos, int i) {
68 return glm::vec3(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]);
69}
70
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]);
74}
75
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) {
79 tris.clear();
80 cdf.clear();
81 if (!posXYZ || !indices || vertexCount < 3 || indexCount < 3) return false;
82 if (indexCount % 3 != 0) return false;
83
84 float total = 0.f;
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;
96 n /= twiceArea;
97 if (nrmXYZ) {
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);
100 }
101 if (n.y < minSlopeDot) continue;
102 Triangle tri;
103 tri.i0 = i0;
104 tri.i1 = i1;
105 tri.i2 = i2;
106 tri.area = 0.5f * twiceArea;
107 tri.n = n;
108 total += tri.area;
109 tris.push_back(tri);
110 cdf.push_back(total);
111 }
112 return total > 1e-12f && !tris.empty();
113}
114
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;
120 return idx;
121}
122
123void sampleOnTriangle(const float *posXYZ, const Triangle &tri, float u, float v, glm::vec3 &p) {
124 if (u + v > 1.f) {
125 u = 1.f - u;
126 v = 1.f - v;
127 }
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));
131 p = a + u * (b - a) + v * (c - a);
132}
133
134struct GridKey {
135 int x = 0, y = 0, z = 0;
136 bool operator==(const GridKey &o) const { return x == o.x && y == o.y && z == o.z; }
137};
138
139struct GridKeyHash {
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;
142 }
143};
144
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))};
147}
148
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;
163 }
164 }
165 }
166 }
167 return false;
168}
169
170float hash01(uint32_t id) {
171 float h = std::fmod(float(id) * 0.61803398875f, 1.f);
172 if (h < 0.f) h += 1.f;
173 return h;
174}
175
176float clampf(float x, float lo, float hi) { return std::min(hi, std::max(lo, x)); }
177
178float coverEdge(float distPx, float radiusPx) {
179 // 1 inside the stroke, 0 outside, ~1px antialiased fringe for nearest upscale.
180 return clampf(radiusPx + 0.65f - distPx, 0.f, 1.f);
181}
182
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;
185 a = clampf(a, 0.f, 1.f);
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));
202}
203
204void stampBlade(std::vector<uint8_t> &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH,
205 float baseU, float height, float lean, float baseHalf, float tipHalf, float luma0,
206 float luma1) {
207 const float fw = float(frameW);
208 const float fh = float(frameH);
209 height = clampf(height, 0.12f, 1.f);
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;
215 const float t = clampf(v / height, 0.f, 1.f);
216 const float cx = baseU + lean * t * t;
217 const float half = baseHalf * (1.f - t * 0.82f) + tipHalf * t;
218 const float dist = std::abs(u - cx) * fw;
219 float cov = coverEdge(dist, half * fw);
220 cov *= 1.f - clampf((v - height) / 0.03f, 0.f, 1.f);
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; // +right
226 float luma = luma0 + (luma1 - luma0) * t;
227 if (side > 0.15f) luma *= 0.78f; // 1px-ish self-shadow on the right
228 stampOver(rgba, atlasW, atlasH, ox + x, y, luma, cov);
229 }
230 }
231}
232
233struct BladeDesc {
234 float u;
235 float height;
236 float lean;
237 float baseHalf;
238 float tipHalf;
239 float luma0;
240 float luma1;
241};
242
243void stampTuft(std::vector<uint8_t> &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH,
244 float wind) {
245 // Tiny root pad only — a solid mound turns overlapping cards into a flat lime slab.
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);
259 }
260 }
261
262 // 7 separated blades with gaps so later cards show through. Outline then fill.
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},
268 };
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,
276 b.luma0, b.luma1);
277 }
278}
279
280} // namespace
281
282int paramCount() { return int(kParamSlots.size()); }
283
284std::string paramName(int index) {
285 if (index < 0 || index >= int(kParamSlots.size())) return {};
286 return kParamSlots[size_t(index)];
287}
288
290 if (!shader) throw eve::Exception("grass::bindDefaults: null shader");
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);
319}
320
322 if (!shader) throw eve::Exception("grass::bindAtlasLayout: null shader");
323 shader->sendFloat("frameCount", float(std::max(info.frames, 1)));
324 shader->sendFloat("atlasCols", float(std::max(info.atlasCols, 1)));
325 shader->sendFloat("atlasRows", float(std::max(info.atlasRows, 1)));
326 shader->sendFloat("grassVariantCount", float(std::max(info.grassVariants, 1)));
327 shader->sendFloat("leafVariantCount", float(std::max(info.leafVariants, 1)));
328 shader->sendFloat("leafRowOffset", float(std::max(info.leafRowOffset, 0)));
329}
330
331void bindLayer(Shader *shader, bool alwaysDark) {
332 if (!shader) throw eve::Exception("grass::bindLayer: null shader");
333 shader->sendFloat("alwaysDark", alwaysDark ? 1.f : 0.f);
334}
335
336void setTime(Shader *shader, float seconds) {
337 if (!shader) throw eve::Exception("grass::setTime: null shader");
338 shader->sendFloat("time", seconds);
339}
340
341void setFrameDuration(Shader *shader, float seconds) {
342 if (!shader) throw eve::Exception("grass::setFrameDuration: null shader");
343 shader->sendFloat("frameDuration", seconds > 1e-4f ? seconds : 1e-4f);
344}
345
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);
351 if (!sh || !sh->gpuHandle)
352 throw eve::Exception("grass::createShader: failed to create grass shader");
353 bindDefaults(sh);
354 return sh;
355}
356
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;
363 return f;
364}
365
366int swayAtlasWidth(int frameW, int frames) { return std::max(frameW, 1) * std::max(frames, 1); }
367int swayAtlasHeight(int frameH) { return std::max(frameH, 1); }
368
369void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector<uint8_t> &rgbaOut) {
370 frameW = std::max(frameW, 8);
371 frameH = std::max(frameH, 8);
372 frames = std::max(frames, 1);
373 const int w = swayAtlasWidth(frameW, frames);
374 const int h = swayAtlasHeight(frameH);
375 rgbaOut.assign(size_t(w * h * 4), 0);
376
377 for (int f = 0; f < frames; ++f) {
378 const float wind = (float(f) - 1.5f) / 1.5f; // -1 .. +1 across 4 frames
379 stampTuft(rgbaOut, w, h, f * frameW, frameW, frameH, wind);
380 }
381}
382
383Texture *createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames) {
384 if (!gfx) throw eve::Exception("grass::createSwayAtlas: null graphics");
385 std::vector<uint8_t> rgba;
386 makeSwayAtlasRGBA(frameW, frameH, frames, rgba);
389 return gfx->newTexture(swayAtlasWidth(frameW, frames), swayAtlasHeight(frameH), rgba.data(),
390 info);
391}
392
393namespace {
394
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();
400}
401
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]);
406 rgba[i + 0] = 255;
407 rgba[i + 1] = 255;
408 rgba[i + 2] = 255;
409 rgba[i + 3] = a;
410 }
411}
412
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();
417 eve::data::ByteData bytes(raw.data(), raw.size());
418 std::unique_ptr<eve::image::ImageData> img(eve::image::Image::create()->newImageData(&bytes));
419 if (!img) return false;
420 w = img->getWidth();
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;
425 rgba.resize(n);
426 std::memcpy(rgba.data(), img->getData(), n);
427 maskToTintable(rgba);
428 return true;
429}
430
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];
445 }
446 }
447}
448
449} // namespace
450
451void packSwayAtlasRGBA(const std::vector<std::string> &grassFiles,
452 const std::vector<std::string> &leafFiles, std::vector<uint8_t> &rgbaOut,
453 PackedAtlasInfo &info) {
454 if (grassFiles.empty()) throw eve::Exception("grass::packSwayAtlasRGBA: no grass atlas files");
455
456 struct Loaded {
457 std::vector<uint8_t> rgba;
458 int w = 0;
459 int h = 0;
460 };
461 auto loadOne = [](const std::string &path) {
462 Loaded img;
463 if (!loadSwayMaskPng(path, img.rgba, img.w, img.h))
464 throw eve::Exception("grass::packSwayAtlasRGBA: failed to load '%s'", path.c_str());
465 return img;
466 };
467
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));
474
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);
481 };
482 for (const auto &img : grass) checkSize(img, "grass");
483 for (const auto &img : leaf) checkSize(img, "leaf");
484
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;
489 info.frames = 4;
490 info.grassVariants = nGrass;
491 info.leafVariants = nLeaf > 0 ? nLeaf : 1;
492 info.leafRowOffset = nLeaf > 0 ? 2 : 0;
493 info.atlasCols = nX * 2;
494 info.atlasRows = nY * 2;
495 info.width = nX * tw;
496 info.height = nY * th;
497 rgbaOut.assign(size_t(info.width) * size_t(info.height) * 4u, 0);
498
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);
503}
504
505Texture *createSwayAtlasFromFiles(Graphics *gfx, const std::vector<std::string> &grassFiles,
506 const std::vector<std::string> &leafFiles,
507 PackedAtlasInfo *infoOut) {
508 if (!gfx) throw eve::Exception("grass::createSwayAtlasFromFiles: null graphics");
509 PackedAtlasInfo info;
510 std::vector<uint8_t> rgba;
511 packSwayAtlasRGBA(grassFiles, leafFiles, rgba, info);
512 if (infoOut) *infoOut = info;
513 TextureCreateInfo texInfo;
515 return gfx->newTexture(info.width, info.height, rgba.data(), texInfo);
516}
517
518std::vector<Point> sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount,
519 const uint32_t *indices, int indexCount, int count, uint32_t seed,
520 float minSlopeDot) {
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))
526 return out;
527
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))];
535 Point p;
536 sampleOnTriangle(posXYZ, tri, u, v, p.position);
537 p.normal = tri.n;
538 p.id = uint32_t(i);
539 p.scale = 0.85f + 0.3f * hash01(p.id + seed);
540 out.push_back(p);
541 }
542 return out;
543}
544
545std::vector<Point> samplePoisson(const float *posXYZ, const float *nrmXYZ, int vertexCount,
546 const uint32_t *indices, int indexCount,
547 const SampleParams &params) {
548 std::vector<Point> accepted;
549 if (params.maxPoints <= 0 || params.radius <= 1e-6f) return accepted;
550
551 std::vector<Triangle> tris;
552 std::vector<float> cdf;
553 if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, params.minSlopeDot, tris,
554 cdf))
555 return accepted;
556
557 const float cell = params.radius;
558 std::unordered_map<GridKey, std::vector<int>, GridKeyHash> grid;
559 const int attempts = std::max(params.maxPoints * 24, params.maxPoints + 16);
560
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))];
567 glm::vec3 pos;
568 sampleOnTriangle(posXYZ, tri, u, v, pos);
569 if (tooClose(pos, params.radius, accepted, grid, cell)) continue;
570
571 Point p;
572 p.position = pos;
573 p.normal = tri.n;
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);
579 }
580 return accepted;
581}
582
583BillboardMesh buildBillboards(const std::vector<Point> &points, float width, float height,
584 bool alwaysDark) {
585 (void)width;
586 (void)height;
588 const size_t n = points.size();
589 mesh.posXYZ.reserve(n * 4 * 3);
590 mesh.nrmXYZ.reserve(n * 4 * 3);
591 mesh.uvST.reserve(n * 4 * 2);
592 mesh.indices.reserve(n * 6);
593
594 // Local corners: (0,0) bottom-left, (1,0) bottom-right, (1,1) top-right, (0,1) top-left.
595 // Root is the bottom-center of the rectangle, i.e. UV (0.5, 0).
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};
599
600 for (size_t i = 0; i < n; ++i) {
601 const Point &p = points[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]);
612 }
613 for (uint32_t k : corners) mesh.indices.push_back(base + k);
614 }
615 return mesh;
616}
617
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;
626 posXYZ.clear();
627 nrmXYZ.clear();
628 indices.clear();
629 posXYZ.reserve(size_t(nx * nz * 3));
630 nrmXYZ.reserve(size_t(nx * nz * 3));
631 indices.reserve(size_t(segX * segZ * 6));
632
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);
643 }
644 }
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);
657 }
658 }
659}
660
661} // namespace eve::graphics::grass
662
663namespace eve::graphics {
664
666 if (!gfx_) throw eve::Exception("GrassField: null graphics");
667}
668
669void GrassField::bake(const float *posXYZ, const float *nrmXYZ, int vertexCount,
670 const uint32_t *indices, int indexCount, const BakeParams &params) {
671 if (!gfx_) throw eve::Exception("GrassField::bake: null graphics");
672
673 grass::SampleParams denseP;
674 denseP.radius = params.denseRadius;
675 denseP.maxPoints = params.maxDense;
676 denseP.seed = params.seed;
677 denseP.minSlopeDot = params.minSlopeDot;
678
679 grass::SampleParams sparseP = denseP;
680 sparseP.radius = params.sparseRadius;
681 sparseP.maxPoints = params.maxSparse;
682 sparseP.seed = params.seed * 7477u + 13u;
683
684 const auto densePts =
685 grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, denseP);
686 const auto sparsePts =
687 grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, sparseP);
688 denseCount_ = int(densePts.size());
689 sparseCount_ = int(sparsePts.size());
690
691 const auto denseMesh = grass::buildBillboards(densePts, params.width, params.height, false);
692 const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height, true);
693
694 if (!shader_) shader_ = grass::createShader(gfx_);
696 if (!params.grassAtlasFiles.empty()) {
698 &layout);
699 } else {
700 if (!atlas_)
701 atlas_ = grass::createSwayAtlas(gfx_, params.atlasFrameW, params.atlasFrameH,
702 params.atlasFrames);
703 layout.frames = std::max(params.atlasFrames, 1);
704 layout.atlasCols = layout.frames;
705 layout.atlasRows = 1;
706 layout.grassVariants = 1;
707 layout.leafVariants = 1;
708 layout.leafRowOffset = 0;
709 }
710
711 grass::bindDefaults(shader_);
713 shader_->sendFloat("grassWidth", params.width);
714 shader_->sendFloat("grassHeight", params.height);
715 shader_->sendFloat("frameDuration", frameDuration_);
716 shader_->sendFloat("time", time_);
717
718 denseMesh_ = nullptr;
719 sparseMesh_ = nullptr;
720 if (!denseMesh.indices.empty())
721 denseMesh_ = gfx_->newMeshFromArrays(denseMesh.posXYZ.data(), denseMesh.nrmXYZ.data(),
722 denseMesh.uvST.data(), int(denseMesh.posXYZ.size() / 3),
723 denseMesh.indices.data(), int(denseMesh.indices.size()));
724 if (!sparseMesh.indices.empty())
725 sparseMesh_ =
726 gfx_->newMeshFromArrays(sparseMesh.posXYZ.data(), sparseMesh.nrmXYZ.data(),
727 sparseMesh.uvST.data(), int(sparseMesh.posXYZ.size() / 3),
728 sparseMesh.indices.data(), int(sparseMesh.indices.size()));
729}
730
731void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ) {
732 bakePlane(sizeX, sizeZ, segX, segZ, BakeParams{});
733}
734
735void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ, const BakeParams &params) {
736 std::vector<float> pos, nrm;
737 std::vector<uint32_t> idx;
738 grass::makePlane(sizeX, sizeZ, segX, segZ, pos, nrm, idx);
739 bake(pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), params);
740}
741
742void GrassField::update(float dt) {
743 time_ += dt;
744 if (shader_) grass::setTime(shader_, time_);
745}
746
747void GrassField::setTime(float seconds) {
748 time_ = seconds;
749 if (shader_) grass::setTime(shader_, time_);
750}
751
752void GrassField::setFrameDuration(float seconds) {
753 frameDuration_ = seconds > 1e-4f ? seconds : 1e-4f;
754 if (shader_) grass::setFrameDuration(shader_, frameDuration_);
755}
756
757void GrassField::draw() { draw(glm::mat4(1.f)); }
758
759void GrassField::draw(const glm::mat4 &model) {
760 if (!gfx_ || !shader_ || !atlas_) return;
761 const Color tint(1.f, 1.f, 1.f, 1.f);
762 grass::setTime(shader_, time_);
763 grass::setFrameDuration(shader_, frameDuration_);
764 if (denseMesh_) {
765 grass::bindLayer(shader_, false);
766 gfx_->drawMeshShader(denseMesh_, model, atlas_, tint, shader_);
767 }
768 if (sparseMesh_) {
769 grass::bindLayer(shader_, true);
770 gfx_->drawMeshShader(sparseMesh_, model, atlas_, tint, shader_);
771 }
772}
773
774} // namespace eve::graphics
float cx
Definition CardTypes.cpp:31
uint32_t seed
Tok kind
std::string layout
vk::ShaderModule vert
vk::ShaderModule frag
int y
Definition Grass.cpp:135
int z
Definition Grass.cpp:135
float tipHalf
Definition Grass.cpp:238
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
float luma0
Definition Grass.cpp:239
float area
Definition Grass.cpp:63
float baseHalf
Definition Grass.cpp:237
float luma1
Definition Grass.cpp:240
glm::vec3 n
Definition Grass.cpp:64
float lean
Definition Grass.cpp:236
int h
int w
std::vector< Colorf > px
uint32_t a
uint32_t b
uint32_t c
int width
int idx
float f
glm::vec4 p[6]
Mesh * mesh
Shader * shader
glm::mat4 model
Light2D::Data * data
int d
int v
std::vector< V3 > points
Definition TreeMesh.cpp:126
In-memory byte buffer implementing eve::Data (ref-counted).
Definition ByteData.h:11
virtual Shader * newMeshShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv)=0
Create a Mesh3D custom shader (MeshVertex + Frame UBO + albedo). Empty vert → default mesh3d....
virtual Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false)=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)....
void setFrameDuration(float seconds)
Definition Grass.cpp:752
void bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, int indexCount, const BakeParams &params)
Definition Grass.cpp:669
void bakePlane(float sizeX, float sizeZ, int segX, int segZ)
Definition Grass.cpp:731
void setTime(float seconds)
Definition Grass.cpp:747
void update(float dt)
Definition Grass.cpp:742
GrassField(Graphics *gfx)
Definition Grass.cpp:665
Custom GPU program.
Definition Shader.h:30
void sendFloat(const std::string &name, float x)
Definition Shader.cpp:82
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
float clampf(float v, float lo, float hi)
Definition AnimMath.h:31
t3ssel8r-style stylized grass.
Definition Grass.cpp:25
void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector< uint8_t > &rgbaOut)
Procedural 4-frame fallback atlas (horizontal strip). GPU paths should load authored 2x2 PNG masks vi...
Definition Grass.cpp:369
int swayAtlasWidth(int frameW, int frames)
Definition Grass.cpp:366
void setFrameDuration(Shader *shader, float seconds)
Definition Grass.cpp:341
void makePlane(float sizeX, float sizeZ, int segX, int segZ, std::vector< float > &posXYZ, std::vector< float > &nrmXYZ, std::vector< uint32_t > &indices)
Unit XZ plane (Y-up) for tests / demos.
Definition Grass.cpp:618
Shader * createShader(Graphics *gfx)
Definition Grass.cpp:346
void packSwayAtlasRGBA(const std::vector< std::string > &grassFiles, const std::vector< std::string > &leafFiles, std::vector< uint8_t > &rgbaOut, PackedAtlasInfo &info)
Load white-on-black (or RGBA) 2x2 sway PNGs and pack them into one atlas.
Definition Grass.cpp:451
std::string paramName(int index)
Definition Grass.cpp:284
void bindDefaults(Shader *shader)
Definition Grass.cpp:289
int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount)
Discrete 4-frame index with a per-instance phase offset.
Definition Grass.cpp:357
std::vector< Point > sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, int indexCount, int count, uint32_t seed, float minSlopeDot)
Area-weighted Halton samples (deterministic, evenly spread).
Definition Grass.cpp:518
Texture * createSwayAtlasFromFiles(Graphics *gfx, const std::vector< std::string > &grassFiles, const std::vector< std::string > &leafFiles, PackedAtlasInfo *infoOut)
Definition Grass.cpp:505
int swayAtlasHeight(int frameH)
Definition Grass.cpp:367
std::vector< Point > samplePoisson(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, int indexCount, const SampleParams &params)
Fast Poisson-disk / dart-throwing blue noise on a triangle mesh.
Definition Grass.cpp:545
void setTime(Shader *shader, float seconds)
Definition Grass.cpp:336
Texture * createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames)
Definition Grass.cpp:383
void bindLayer(Shader *shader, bool alwaysDark)
Definition Grass.cpp:331
BillboardMesh buildBillboards(const std::vector< Point > &points, float width, float height, bool alwaysDark)
Expand a unit rectangle per point. Vertex layout: pos = grass root uv = quad corner in [0,...
Definition Grass.cpp:583
void bindAtlasLayout(Shader *shader, const PackedAtlasInfo &info)
Definition Grass.cpp:321
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition AnimTrail.h:6
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< std::string > grassAtlasFiles
Authored 2x2 sway masks (4 grass + 2 leaf). Empty = procedural strip.
Definition Grass.h:140
std::vector< std::string > leafAtlasFiles
Definition Grass.h:141
float denseRadius
Poisson spacing. Keep this well below width so tufts overlap.
Definition Grass.h:128
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
static TextureSampler nearest()
static TextureSampler linear()
std::vector< float > posXYZ
Definition Grass.h:46
Layout of a packed 2x2-per-variant sway atlas (4 grass + 2 leaf typical).
Definition Grass.h:85
float minSlopeDot
Skip faces whose Y-up slope is below this (0 = keep walls).
Definition Grass.h:42