载入中...
搜索中...
未找到
Grass.cpp
浏览该文件的文档.
1#include "graphics/Grass.h"
2
3#include "common/Exception.h"
4#include "common/Capability.h"
5#include "data/ByteData.h"
6#include "graphics/Graphics.h"
7#include "graphics/Mesh.h"
9#include "graphics/Shader.h"
10#include "graphics/Texture.h"
12#include "graphics/shaders/mesh3d_grass_frag_spv.inc"
13#include "graphics/shaders/mesh3d_grass_vert_spv.inc"
15#include "image/Image.h"
16#include "image/ImageData.h"
17
18#include <algorithm>
19#include <array>
20#include <cmath>
21#include <cstring>
22#include <fstream>
23#include <iterator>
24#include <memory>
25#include <unordered_map>
26#include <utility>
27
29namespace {
30
31
32
33std::vector<uint32_t> copySpv(const uint32_t *data, size_t count) {
34 return std::vector<uint32_t>(data, data + count);
35}
36
37float radicalInverse(uint32_t n, uint32_t base) {
38 const float inv = 1.f / float(base);
39 float f = inv;
40 float val = 0.f;
41 while (n > 0) {
42 val += float(n % base) * f;
43 n /= base;
44 f *= inv;
45 }
46 return val;
47}
48
49float wrap01(float x) {
50 x = x - std::floor(x);
51 return x < 0.f ? x + 1.f : x;
52}
53
54uint32_t mixSeed(uint32_t seed, uint32_t i) {
55 seed ^= 0x9e3779b9u + (i << 6) + (i >> 2);
56 seed *= 0x85ebca6bu;
57 return seed ^ (seed >> 13);
58}
59
60struct Triangle {
61 uint32_t i0 = 0, i1 = 0, i2 = 0;
62 float area = 0.f;
63 glm::vec3 n{0.f, 1.f, 0.f};
64};
65
66glm::vec3 readPos(const float *pos, int i) {
67 return glm::vec3(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]);
68}
69
70glm::vec3 readNrm(const float *nrm, int i) {
71 if (!nrm) return glm::vec3(0.f, 1.f, 0.f);
72 return glm::vec3(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
73}
74
75bool buildTriangles(const float *posXYZ, const float *nrmXYZ, int vertexCount,
76 const uint32_t *indices, int indexCount, float minSlopeDot,
77 std::vector<Triangle> &tris, std::vector<float> &cdf) {
78 tris.clear();
79 cdf.clear();
80 if (!posXYZ || !indices || vertexCount < 3 || indexCount < 3) return false;
81 if (indexCount % 3 != 0) return false;
82
83 float total = 0.f;
84 for (int t = 0; t + 2 < indexCount; t += 3) {
85 const uint32_t i0 = indices[t];
86 const uint32_t i1 = indices[t + 1];
87 const uint32_t i2 = indices[t + 2];
88 if (int(i0) >= vertexCount || int(i1) >= vertexCount || int(i2) >= vertexCount) continue;
89 const glm::vec3 a = readPos(posXYZ, int(i0));
90 const glm::vec3 b = readPos(posXYZ, int(i1));
91 const glm::vec3 c = readPos(posXYZ, int(i2));
92 glm::vec3 n = glm::cross(b - a, c - a);
93 const float twiceArea = glm::length(n);
94 if (twiceArea < 1e-10f) continue;
95 n /= twiceArea;
96 if (nrmXYZ) {
97 glm::vec3 ns = readNrm(nrmXYZ, int(i0)) + readNrm(nrmXYZ, int(i1)) + readNrm(nrmXYZ, int(i2));
98 if (glm::dot(ns, ns) > 1e-8f) n = glm::normalize(ns);
99 }
100 if (n.y < minSlopeDot) continue;
101 Triangle tri;
102 tri.i0 = i0;
103 tri.i1 = i1;
104 tri.i2 = i2;
105 tri.area = 0.5f * twiceArea;
106 tri.n = n;
107 total += tri.area;
108 tris.push_back(tri);
109 cdf.push_back(total);
110 }
111 return total > 1e-12f && !tris.empty();
112}
113
114int pickTriangle(const std::vector<float> &cdf, float u) {
115 const float target = u * cdf.back();
116 auto it = std::lower_bound(cdf.begin(), cdf.end(), target);
117 int idx = int(it - cdf.begin());
118 if (idx >= int(cdf.size())) idx = int(cdf.size()) - 1;
119 return idx;
120}
121
122void sampleOnTriangle(const float *posXYZ, const Triangle &tri, float u, float v, glm::vec3 &p) {
123 if (u + v > 1.f) {
124 u = 1.f - u;
125 v = 1.f - v;
126 }
127 const glm::vec3 a = readPos(posXYZ, int(tri.i0));
128 const glm::vec3 b = readPos(posXYZ, int(tri.i1));
129 const glm::vec3 c = readPos(posXYZ, int(tri.i2));
130 p = a + u * (b - a) + v * (c - a);
131}
132
133struct GridKey {
134 int x = 0, y = 0, z = 0;
135 bool operator==(const GridKey &o) const { return x == o.x && y == o.y && z == o.z; }
136};
137
138struct GridKeyHash {
139 size_t operator()(const GridKey &k) const {
140 return size_t(k.x) * 73856093u ^ size_t(k.y) * 19349663u ^ size_t(k.z) * 83492791u;
141 }
142};
143
144GridKey toKey(const glm::vec3 &p, float cell) {
145 return {int(std::floor(p.x / cell)), int(std::floor(p.y / cell)), int(std::floor(p.z / cell))};
146}
147
148bool tooClose(const glm::vec3 &p, float radius,
149 const std::vector<Point> &accepted,
150 const std::unordered_map<GridKey, std::vector<int>, GridKeyHash> &grid, float cell) {
151 const GridKey c = toKey(p, cell);
152 const float r2 = radius * radius;
153 for (int dz = -1; dz <= 1; ++dz) {
154 for (int dy = -1; dy <= 1; ++dy) {
155 for (int dx = -1; dx <= 1; ++dx) {
156 GridKey k{c.x + dx, c.y + dy, c.z + dz};
157 auto it = grid.find(k);
158 if (it == grid.end()) continue;
159 for (int idx : it->second) {
160 const glm::vec3 d = accepted[size_t(idx)].position - p;
161 if (glm::dot(d, d) < r2) return true;
162 }
163 }
164 }
165 }
166 return false;
167}
168
169float hash01(uint32_t id) {
170 float h = std::fmod(float(id) * 0.61803398875f, 1.f);
171 if (h < 0.f) h += 1.f;
172 return h;
173}
174
175float clampf(float x, float lo, float hi) { return std::min(hi, std::max(lo, x)); }
176
177float coverEdge(float distPx, float radiusPx) {
178 // 1 inside the stroke, 0 outside, ~1px antialiased fringe for nearest upscale.
179 return clampf(radiusPx + 0.65f - distPx, 0.f, 1.f);
180}
181
182void stampOver(std::vector<uint8_t> &rgba, int w, int h, int x, int y, float luma, float a) {
183 if (x < 0 || y < 0 || x >= w || y >= h) return;
184 a = clampf(a, 0.f, 1.f);
185 luma = clampf(luma, 0.f, 1.f);
186 if (a < 0.02f) return;
187 const size_t i = (size_t(y) * size_t(w) + size_t(x)) * 4u;
188 const float oa = float(rgba[i + 3]) / 255.f;
189 const float or_ = float(rgba[i + 0]) / 255.f;
190 const float og = float(rgba[i + 1]) / 255.f;
191 const float ob = float(rgba[i + 2]) / 255.f;
192 const float outA = a + oa * (1.f - a);
193 if (outA < 1e-4f) return;
194 const float nr = (luma * a + or_ * oa * (1.f - a)) / outA;
195 const float ng = (luma * a + og * oa * (1.f - a)) / outA;
196 const float nb = (luma * a + ob * oa * (1.f - a)) / outA;
197 rgba[i + 0] = uint8_t(std::round(clampf(nr, 0.f, 1.f) * 255.f));
198 rgba[i + 1] = uint8_t(std::round(clampf(ng, 0.f, 1.f) * 255.f));
199 rgba[i + 2] = uint8_t(std::round(clampf(nb, 0.f, 1.f) * 255.f));
200 rgba[i + 3] = uint8_t(std::round(clampf(outA, 0.f, 1.f) * 255.f));
201}
202
203void stampBlade(std::vector<uint8_t> &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH,
204 float baseU, float height, float lean, float baseHalf, float tipHalf, float luma0,
205 float luma1) {
206 const float fw = float(frameW);
207 const float fh = float(frameH);
208 height = clampf(height, 0.12f, 1.f);
209 for (int y = 0; y < frameH; ++y) {
210 for (int x = 0; x < frameW; ++x) {
211 const float u = (float(x) + 0.5f) / fw;
212 const float v = 1.f - (float(y) + 0.5f) / fh;
213 if (v < -0.02f || v > height + 0.04f) continue;
214 const float t = clampf(v / height, 0.f, 1.f);
215 const float cx = baseU + lean * t * t;
216 const float half = baseHalf * (1.f - t * 0.82f) + tipHalf * t;
217 const float dist = std::abs(u - cx) * fw;
218 float cov = coverEdge(dist, half * fw);
219 cov *= 1.f - clampf((v - height) / 0.03f, 0.f, 1.f);
220 cov *= clampf((v + 0.02f) / 0.05f, 0.f, 1.f);
221 if (cov > 0.5f) cov = 1.f;
222 else if (cov < 0.25f) cov = 0.f;
223 if (cov <= 0.f) continue;
224 const float side = (u - cx) * fw; // +right
225 float luma = luma0 + (luma1 - luma0) * t;
226 if (side > 0.15f) luma *= 0.78f; // 1px-ish self-shadow on the right
227 stampOver(rgba, atlasW, atlasH, ox + x, y, luma, cov);
228 }
229 }
230}
231
232struct BladeDesc {
233 float u;
234 float height;
235 float lean;
236 float baseHalf;
237 float tipHalf;
238 float luma0;
239 float luma1;
240};
241
242void stampTuft(std::vector<uint8_t> &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH,
243 float wind) {
244 // Tiny root pad only — a solid mound turns overlapping cards into a flat lime slab.
245 const float fw = float(frameW);
246 const float fh = float(frameH);
247 for (int y = 0; y < frameH; ++y) {
248 for (int x = 0; x < frameW; ++x) {
249 const float u = (float(x) + 0.5f) / fw;
250 const float v = 1.f - (float(y) + 0.5f) / fh;
251 if (v > 0.16f) continue;
252 const float cx = 0.50f + wind * 0.02f;
253 const float hw = 0.16f * (1.f - v / 0.16f);
254 float cov = coverEdge(std::abs(u - cx) * fw, hw * fw);
255 if (cov > 0.5f) cov = 1.f;
256 else if (cov < 0.25f) cov = 0.f;
257 if (cov > 0.f) stampOver(rgba, atlasW, atlasH, ox + x, y, 0.72f, cov);
258 }
259 }
260
261 // 7 separated blades with gaps so later cards show through. Outline then fill.
262 const BladeDesc blades[] = {
263 {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},
264 {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},
265 {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},
266 {0.56f, 0.86f, 0.08f, 0.050f, 0.014f, 0.84f, 0.98f},
267 };
268 const float outline = 1.15f / fw;
269 for (const BladeDesc &b : blades) {
270 const float lean = b.lean + wind * 0.32f;
271 const float u = b.u + wind * 0.03f;
272 stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH, u, b.height, lean, b.baseHalf + outline,
273 b.tipHalf + outline * 0.5f, 0.42f, 0.55f);
274 stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH, u, b.height, lean, b.baseHalf, b.tipHalf,
275 b.luma0, b.luma1);
276 }
277}
278
279} // namespace
280
282 if (!shader) throw eve::Exception("grass::bindAtlasLayout: null shader");
283 shader->sendFloat("frameCount", float(std::max(info.frames, 1)));
284 shader->sendFloat("atlasCols", float(std::max(info.atlasCols, 1)));
285 shader->sendFloat("atlasRows", float(std::max(info.atlasRows, 1)));
286 shader->sendFloat("grassVariantCount", float(std::max(info.grassVariants, 1)));
287 shader->sendFloat("leafVariantCount", float(std::max(info.leafVariants, 1)));
288 shader->sendFloat("leafRowOffset", float(std::max(info.leafRowOffset, 0)));
289}
290
291void bindLayer(Shader *shader, bool alwaysDark) {
292 if (!shader) throw eve::Exception("grass::bindLayer: null shader");
293 shader->sendFloat("alwaysDark", alwaysDark ? 1.f : 0.f);
294}
295
296void setTime(Shader *shader, float seconds) {
297 if (!shader) throw eve::Exception("grass::setTime: null shader");
298 shader->sendFloat("time", seconds);
299}
300
301void setFrameDuration(Shader *shader, float seconds) {
302 if (!shader) throw eve::Exception("grass::setFrameDuration: null shader");
303 shader->sendFloat("frameDuration", seconds > 1e-4f ? seconds : 1e-4f);
304}
305
307 if (!gfx) throw eve::Exception("grass::createShader: null graphics");
308 Shader *sh = nullptr;
309 if (gfx->getBackendName() == "webgpu") {
311 } else {
312 auto vert = copySpv(mesh3d_grass_vert_spv, mesh3d_grass_vert_spv_count);
313 auto frag = copySpv(mesh3d_grass_frag_spv, mesh3d_grass_frag_spv_count);
314 sh = gfx->newMeshShaderFromSpv(vert, frag);
315 }
316 if (!sh || !sh->gpuHandle)
317 throw eve::Exception("grass::createShader: failed to create grass shader");
318 bindDefaults(sh);
319 return sh;
320}
321
322int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount) {
323 if (frameCount < 1) frameCount = 1;
324 const float dur = frameDuration > 1e-4f ? frameDuration : 1e-4f;
325 const float t = time / dur + hash01(instanceId) * float(frameCount);
326 int f = int(std::floor(t)) % frameCount;
327 if (f < 0) f += frameCount;
328 return f;
329}
330
331int swayAtlasWidth(int frameW, int frames) { return std::max(frameW, 1) * std::max(frames, 1); }
332int swayAtlasHeight(int frameH) { return std::max(frameH, 1); }
333
334void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector<uint8_t> &rgbaOut) {
335 frameW = std::max(frameW, 8);
336 frameH = std::max(frameH, 8);
337 frames = std::max(frames, 1);
338 const int w = swayAtlasWidth(frameW, frames);
339 const int h = swayAtlasHeight(frameH);
340 rgbaOut.assign(size_t(w * h * 4), 0);
341
342 for (int f = 0; f < frames; ++f) {
343 const float wind = (float(f) - 1.5f) / 1.5f; // -1 .. +1 across 4 frames
344 stampTuft(rgbaOut, w, h, f * frameW, frameW, frameH, wind);
345 }
346}
347
348Texture *createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames) {
349 if (!gfx) throw eve::Exception("grass::createSwayAtlas: null graphics");
350 std::vector<uint8_t> rgba;
351 makeSwayAtlasRGBA(frameW, frameH, frames, rgba);
354 return gfx->newTexture(swayAtlasWidth(frameW, frames), swayAtlasHeight(frameH), rgba.data(),
355 info);
356}
357
358namespace {
359
360bool readWholeFile(const std::string &path, std::vector<char> &out) {
361 std::ifstream in(path, std::ios::binary);
362 if (!in) return false;
363 out.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
364 return in.good() || !out.empty();
365}
366
367void maskToTintable(std::vector<uint8_t> &rgba) {
368 for (size_t i = 0; i + 3 < rgba.size(); i += 4) {
369 const uint8_t luma = std::max(rgba[i], std::max(rgba[i + 1], rgba[i + 2]));
370 const uint8_t a = std::max(luma, rgba[i + 3]);
371 rgba[i + 0] = 255;
372 rgba[i + 1] = 255;
373 rgba[i + 2] = 255;
374 rgba[i + 3] = a;
375 }
376}
377
378bool loadSwayMaskPng(const std::string &path, std::vector<uint8_t> &rgba, int &w, int &h) {
379 std::vector<char> raw;
380 if (!readWholeFile(path, raw)) return false;
381 eve::image::Image *image = eve::image::Image::create();
382 if (!image) return false;
383 eve::data::ByteData bytes(raw.data(), raw.size());
384 std::unique_ptr<eve::image::ImageData> img(image->newImageData(&bytes));
385 if (!img) return false;
386 w = img->getWidth();
387 h = img->getHeight();
388 if (w < 2 || h < 2) return false;
389 const size_t n = size_t(w) * size_t(h) * 4u;
390 if (img->getSize() < n) return false;
391 rgba.resize(n);
392 std::memcpy(rgba.data(), img->getData(), n);
393 maskToTintable(rgba);
394 return true;
395}
396
397void blitRgba(std::vector<uint8_t> &dst, int dw, int dh, int dx, int dy,
398 const std::vector<uint8_t> &src, int sw, int sh) {
399 for (int y = 0; y < sh; ++y) {
400 const int ty = dy + y;
401 if (ty < 0 || ty >= dh) continue;
402 for (int x = 0; x < sw; ++x) {
403 const int tx = dx + x;
404 if (tx < 0 || tx >= dw) continue;
405 const size_t si = (size_t(y) * size_t(sw) + size_t(x)) * 4u;
406 const size_t di = (size_t(ty) * size_t(dw) + size_t(tx)) * 4u;
407 dst[di + 0] = src[si + 0];
408 dst[di + 1] = src[si + 1];
409 dst[di + 2] = src[si + 2];
410 dst[di + 3] = src[si + 3];
411 }
412 }
413}
414
415} // namespace
416
417void packSwayAtlasRGBA(const std::vector<std::string> &grassFiles,
418 const std::vector<std::string> &leafFiles, std::vector<uint8_t> &rgbaOut,
420 if (grassFiles.empty()) throw eve::Exception("grass::packSwayAtlasRGBA: no grass atlas files");
421
422 struct Loaded {
423 std::vector<uint8_t> rgba;
424 int w = 0;
425 int h = 0;
426 };
427 auto loadOne = [](const std::string &path) {
428 Loaded img;
429 if (!loadSwayMaskPng(path, img.rgba, img.w, img.h))
430 throw eve::Exception("grass::packSwayAtlasRGBA: failed to load '%s'", path.c_str());
431 return img;
432 };
433
434 std::vector<Loaded> grass;
435 grass.reserve(grassFiles.size());
436 for (const auto &p : grassFiles) grass.push_back(loadOne(p));
437 std::vector<Loaded> leaf;
438 leaf.reserve(leafFiles.size());
439 for (const auto &p : leafFiles) leaf.push_back(loadOne(p));
440
441 const int tw = grass.front().w;
442 const int th = grass.front().h;
443 auto checkSize = [tw, th](const Loaded &img, const char *kind) {
444 if (img.w != tw || img.h != th)
445 throw eve::Exception("grass::packSwayAtlasRGBA: %s atlas size mismatch (%dx%d vs %dx%d)",
446 kind, img.w, img.h, tw, th);
447 };
448 for (const auto &img : grass) checkSize(img, "grass");
449 for (const auto &img : leaf) checkSize(img, "leaf");
450
451 const int nGrass = int(grass.size());
452 const int nLeaf = int(leaf.size());
453 const int nX = std::max(nGrass, std::max(nLeaf, 1));
454 const int nY = nLeaf > 0 ? 2 : 1;
455 info.frames = 4;
456 info.grassVariants = nGrass;
457 info.leafVariants = nLeaf > 0 ? nLeaf : 1;
458 info.leafRowOffset = nLeaf > 0 ? 2 : 0;
459 info.atlasCols = nX * 2;
460 info.atlasRows = nY * 2;
461 info.width = nX * tw;
462 info.height = nY * th;
463 rgbaOut.assign(size_t(info.width) * size_t(info.height) * 4u, 0);
464
465 for (int i = 0; i < nGrass; ++i)
466 blitRgba(rgbaOut, info.width, info.height, i * tw, 0, grass[size_t(i)].rgba, tw, th);
467 for (int i = 0; i < nLeaf; ++i)
468 blitRgba(rgbaOut, info.width, info.height, i * tw, th, leaf[size_t(i)].rgba, tw, th);
469}
470
471Texture *createSwayAtlasFromFiles(Graphics *gfx, const std::vector<std::string> &grassFiles,
472 const std::vector<std::string> &leafFiles,
473 PackedAtlasInfo *infoOut) {
474 if (!gfx) throw eve::Exception("grass::createSwayAtlasFromFiles: null graphics");
476 std::vector<uint8_t> rgba;
477 packSwayAtlasRGBA(grassFiles, leafFiles, rgba, info);
478 if (infoOut) *infoOut = info;
479 TextureCreateInfo texInfo;
481 return gfx->newTexture(info.width, info.height, rgba.data(), texInfo);
482}
483
484std::vector<Point> sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount,
485 const uint32_t *indices, int indexCount, int count, uint32_t seed,
486 float minSlopeDot) {
487 std::vector<Point> out;
488 if (count <= 0) return out;
489 std::vector<Triangle> tris;
490 std::vector<float> cdf;
491 if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, minSlopeDot, tris, cdf))
492 return out;
493
494 out.reserve(size_t(count));
495 for (int i = 0; i < count; ++i) {
496 const uint32_t n = mixSeed(seed, uint32_t(i + 1));
497 const float uTri = wrap01(radicalInverse(n, 2));
498 const float u = wrap01(radicalInverse(n, 3));
499 const float v = wrap01(radicalInverse(n, 5));
500 const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))];
501 Point p;
502 sampleOnTriangle(posXYZ, tri, u, v, p.position);
503 p.normal = tri.n;
504 p.id = uint32_t(i);
505 p.scale = 0.85f + 0.3f * hash01(p.id + seed);
506 out.push_back(p);
507 }
508 return out;
509}
510
511std::vector<Point> samplePoisson(const float *posXYZ, const float *nrmXYZ, int vertexCount,
512 const uint32_t *indices, int indexCount,
513 const SampleParams &params) {
514 std::vector<Point> accepted;
515 if (params.maxPoints <= 0 || params.radius <= 1e-6f) return accepted;
516
517 std::vector<Triangle> tris;
518 std::vector<float> cdf;
519 if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, params.minSlopeDot, tris,
520 cdf))
521 return accepted;
522
523 const float cell = params.radius;
524 std::unordered_map<GridKey, std::vector<int>, GridKeyHash> grid;
525 const int attempts = std::max(params.maxPoints * 24, params.maxPoints + 16);
526
527 for (int i = 0; i < attempts && int(accepted.size()) < params.maxPoints; ++i) {
528 const uint32_t n = mixSeed(params.seed, uint32_t(i + 1));
529 const float uTri = wrap01(radicalInverse(n, 2));
530 const float u = wrap01(radicalInverse(n, 3));
531 const float v = wrap01(radicalInverse(n, 5));
532 const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))];
533 glm::vec3 pos;
534 sampleOnTriangle(posXYZ, tri, u, v, pos);
535 if (tooClose(pos, params.radius, accepted, grid, cell)) continue;
536
537 Point p;
538 p.position = pos;
539 p.normal = tri.n;
540 p.id = uint32_t(accepted.size());
541 p.scale = 0.85f + 0.3f * hash01(p.id + params.seed);
542 const int idx = int(accepted.size());
543 accepted.push_back(p);
544 grid[toKey(pos, cell)].push_back(idx);
545 }
546 return accepted;
547}
548
549
550void makePlane(float sizeX, float sizeZ, int segX, int segZ, std::vector<float> &posXYZ,
551 std::vector<float> &nrmXYZ, std::vector<uint32_t> &indices) {
552 segX = std::max(segX, 1);
553 segZ = std::max(segZ, 1);
554 sizeX = std::max(sizeX, 1e-3f);
555 sizeZ = std::max(sizeZ, 1e-3f);
556 const int nx = segX + 1;
557 const int nz = segZ + 1;
558 posXYZ.clear();
559 nrmXYZ.clear();
560 indices.clear();
561 posXYZ.reserve(size_t(nx * nz * 3));
562 nrmXYZ.reserve(size_t(nx * nz * 3));
563 indices.reserve(size_t(segX * segZ * 6));
564
565 for (int z = 0; z < nz; ++z) {
566 for (int x = 0; x < nx; ++x) {
567 const float px = (float(x) / float(segX) - 0.5f) * sizeX;
568 const float pz = (float(z) / float(segZ) - 0.5f) * sizeZ;
569 posXYZ.push_back(px);
570 posXYZ.push_back(0.f);
571 posXYZ.push_back(pz);
572 nrmXYZ.push_back(0.f);
573 nrmXYZ.push_back(1.f);
574 nrmXYZ.push_back(0.f);
575 }
576 }
577 for (int z = 0; z < segZ; ++z) {
578 for (int x = 0; x < segX; ++x) {
579 const uint32_t i0 = uint32_t(z * nx + x);
580 const uint32_t i1 = i0 + 1;
581 const uint32_t i2 = i0 + uint32_t(nx);
582 const uint32_t i3 = i2 + 1;
583 indices.push_back(i0);
584 indices.push_back(i2);
585 indices.push_back(i1);
586 indices.push_back(i1);
587 indices.push_back(i2);
588 indices.push_back(i3);
589 }
590 }
591}
592
593} // namespace eve::graphics::grass
594
595namespace eve::graphics {
596
598 if (!gfx_) throw eve::Exception("GrassField: null graphics");
599 captureDrawerToken_ = RenderSystem3D::addCaptureExtraDrawer(
600 0xffffffffu,
601 [this](Graphics &, const Camera3D::Data &, const glm::mat4 &, float, uint32_t mask) {
602 if (!reflectionCaptureEnabled_ || (reflectionCaptureMask_ & mask) == 0u) return;
603 draw(lastModel_);
604 });
605}
606
607GrassField::~GrassField() { if(photoModeAuthority_)cap::removeListener<IPhotoModeFieldSink>(this);RenderSystem3D::removeCaptureExtraDrawer(captureDrawerToken_); }
608
609void GrassField::setPhotoModeAuthority(bool enabled){if(enabled==photoModeAuthority_)return;if(enabled)cap::addListener<IPhotoModeFieldSink>(this);else cap::removeListener<IPhotoModeFieldSink>(this);photoModeAuthority_=enabled;}
612 auto fail=[&](const char*m){return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,m,a.field,{},"graphics.grass.photoMode"));};
613 auto f=std::get_if<float>(&a.value);auto i=std::get_if<int64_t>(&a.value);
614 if(a.field=="m_globalGrassDensity"){if(!f||!std::isfinite(*f)||*f<=0)return fail("grass density multiplier must be positive and finite");photoModeDensity_=*f;}
615 else if(a.field=="m_globalGrassDistance"){if(!f||!std::isfinite(*f)||*f<=0)return fail("grass distance multiplier must be positive and finite");photoModeDistance_=*f;}
616 else if(a.field=="m_cameraCellDistance"){if(!f||!std::isfinite(*f)||*f<=0)return fail("camera-cell distance multiplier must be positive and finite");photoModeCellDistance_=*f;}
617 else if(a.field=="m_cameraCellSubdivision"){if(!i||*i<-8||*i>8)return fail("camera-cell subdivision must be in [-8,8]");photoModeCellSubdivision_=int(*i);}
618 else return fail("unsupported grass photo-mode field");
619 applyPhotoModeShaderState();return Result<void>::success();
620}
621void GrassField::applyPhotoModeShaderState(){
622 const float cellScale=photoModeCellDistance_*std::exp2(float(-photoModeCellSubdivision_));
623 detailHardDistance_=baseDetailHardDistance_*photoModeDistance_*photoModeCellDistance_;
624 detailDensity_=std::clamp(baseDetailDensity_*photoModeDensity_,0.f,1.f);
625 if(!shader_||!foliageProfile_)return;
626 shader_->sendFloat("atlasCols",baseDetailRenderDistance_*photoModeDistance_*cellScale);
627 shader_->sendFloat("atlasRows",baseDetailFadeRange_*photoModeDistance_*cellScale);
628 shader_->sendFloat("leafRowOffset",std::floor(detailHardDistance_)+detailDensity_*0.5f);
629}
630
631void GrassField::bake(const float *posXYZ, const float *nrmXYZ, int vertexCount,
632 const uint32_t *indices, int indexCount, const BakeParams &params) {
633 if (!gfx_) throw eve::Exception("GrassField::bake: null graphics");
634
635 grass::SampleParams denseP;
636 denseP.radius = params.denseRadius;
637 denseP.maxPoints = params.maxDense;
638 denseP.seed = params.seed;
639 denseP.minSlopeDot = params.minSlopeDot;
640
641 grass::SampleParams sparseP = denseP;
642 sparseP.radius = params.sparseRadius;
643 sparseP.maxPoints = params.maxSparse;
644 sparseP.seed = params.seed * 7477u + 13u;
645
646 const auto densePts =
647 grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, denseP);
648 const auto sparsePts =
649 grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, sparseP);
650 denseCount_ = int(densePts.size());
651 sparseCount_ = int(sparsePts.size());
652
653 const auto denseMesh = grass::buildBillboards(densePts, params.width, params.height, false);
654 const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height, true);
655
656 if (!shader_) shader_ = grass::createShader(gfx_);
658 if (!params.grassAtlasFiles.empty()) {
659 atlas_ = grass::createSwayAtlasFromFiles(gfx_, params.grassAtlasFiles, params.leafAtlasFiles,
660 &layout);
661 } else {
662 if (!atlas_)
663 atlas_ = grass::createSwayAtlas(gfx_, params.atlasFrameW, params.atlasFrameH,
664 params.atlasFrames);
665 layout.frames = std::max(params.atlasFrames, 1);
666 layout.atlasCols = layout.frames;
667 layout.atlasRows = 1;
668 layout.grassVariants = 1;
669 layout.leafVariants = 1;
670 layout.leafRowOffset = 0;
671 }
672
673 grass::bindDefaults(shader_);
675 shader_->sendFloat("grassWidth", params.width);
676 shader_->sendFloat("grassHeight", params.height);
677 shader_->sendFloat("frameDuration", frameDuration_);
678 shader_->sendFloat("time", time_);
679
680 denseMesh_ = nullptr;
681 sparseMesh_ = nullptr;
682 if (!denseMesh.indices.empty())
683 denseMesh_ = gfx_->newMeshFromArrays(denseMesh.posXYZ.data(), denseMesh.nrmXYZ.data(),
684 denseMesh.uvST.data(), int(denseMesh.posXYZ.size() / 3),
685 denseMesh.indices.data(), int(denseMesh.indices.size()));
686 if (!sparseMesh.indices.empty())
687 sparseMesh_ =
688 gfx_->newMeshFromArrays(sparseMesh.posXYZ.data(), sparseMesh.nrmXYZ.data(),
689 sparseMesh.uvST.data(), int(sparseMesh.posXYZ.size() / 3),
690 sparseMesh.indices.data(), int(sparseMesh.indices.size()));
691}
692
693void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ) {
694 bakePlane(sizeX, sizeZ, segX, segZ, BakeParams{});
695}
696
697void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ, const BakeParams &params) {
698 std::vector<float> pos, nrm;
699 std::vector<uint32_t> idx;
700 grass::makePlane(sizeX, sizeZ, segX, segZ, pos, nrm, idx);
701 bake(pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), params);
702}
703
704void GrassField::update(float dt) {
705 time_ += dt;
706 if (shader_ && !foliageProfile_) grass::setTime(shader_, time_);
707}
708
709void GrassField::setTime(float seconds) {
710 time_ = seconds;
711 if (shader_ && !foliageProfile_) grass::setTime(shader_, time_);
712}
713
714void GrassField::setFrameDuration(float seconds) {
715 frameDuration_ = seconds > 1e-4f ? seconds : 1e-4f;
716 if (shader_ && !foliageProfile_) grass::setFrameDuration(shader_, frameDuration_);
717}
718
719void GrassField::draw() { draw(glm::mat4(1.f)); }
720
721void GrassField::draw(const glm::mat4 &model) {
722 lastModel_ = model;
723 if (!gfx_ || !shader_ || !atlas_) return;
724 const Color tint(1.f, 1.f, 1.f, 1.f);
725 if (!foliageProfile_) {
726 grass::setTime(shader_, time_);
727 grass::setFrameDuration(shader_, frameDuration_);
728 }
729 gfx_->setMesh3DNormalTexture(normal_);
730 gfx_->setMesh3DHeightTexture(mask_);
731 if (denseMesh_) {
732 grass::bindLayer(shader_, false);
733 gfx_->drawMeshShader(denseMesh_, model, atlas_, tint, shader_);
734 }
735 if (sparseMesh_) {
736 grass::bindLayer(shader_, true);
737 gfx_->drawMeshShader(sparseMesh_, model, atlas_, tint, shader_);
738 }
739 gfx_->setMesh3DNormalTexture(nullptr);
740 gfx_->setMesh3DHeightTexture(nullptr);
741}
742
743} // namespace eve::graphics
LogicalId target
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int mask
float cx
Definition CardTypes.cpp:33
float nx
float nz
float pz
glm::vec4 p[6]
std::string layout
std::uint32_t vertexCount
std::uint32_t indexCount
vk::ShaderModule vert
vk::ShaderModule frag
vk::UniqueImage image
glm::vec4 tint
float tipHalf
Definition Grass.cpp:237
uint32_t i1
Definition Grass.cpp:61
uint32_t i2
Definition Grass.cpp:61
uint32_t i0
Definition Grass.cpp:61
float u
Definition Grass.cpp:233
float luma0
Definition Grass.cpp:238
float area
Definition Grass.cpp:62
float baseHalf
Definition Grass.cpp:236
float luma1
Definition Grass.cpp:239
glm::vec3 n
Definition Grass.cpp:63
float lean
Definition Grass.cpp:235
std::vector< std::uint32_t > indices
float v
std::int32_t second
std::int32_t c
int h
std::vector< Colorf > px
std::uint32_t height
TokenKind kind
std::uint64_t bytes
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
int idx
float f
float radius
std::string path
Definition PlayHost.cpp:110
std::uint32_t seed
Definition PointSet.cpp:807
float d
float t
Shader * shader
glm::mat4 model
float dz
float dy
float dx
std::uint32_t count
Cell cell
ecs::EntityHandle side
double ox
float m[16]
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
In-memory byte buffer implementing eve::Data (ref-counted).
Definition ByteData.h:13
virtual std::string getBackendName() const =0
Renderer backend id used by sibling modules (e.g. Gpgpu).
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
Creates a texture. @ownership Caller deletes unless documented otherwise.
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)....
virtual Shader * newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl)=0
Create a Mesh3D custom shader from WGSL source (WebGPU backend). The WGSL must declare the engine's F...
virtual void setMesh3DNormalTexture(Texture *normal)=0
Optional normal map for the next drawMesh / drawMeshShader (nullptr = flat).
virtual void setMesh3DHeightTexture(Texture *height)=0
Optional height map for parallax (R channel; nullptr = flat / off).
void setFrameDuration(float seconds)
Definition Grass.cpp:714
PhotoModeFieldAcceptance acceptsPhotoModeField(const PhotoModeAssignment &assignment) const noexcept override
Return true only for fields owned by this provider.
Definition Grass.cpp:610
void bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, int indexCount, const BakeParams &params)
Definition Grass.cpp:631
void bakePlane(float sizeX, float sizeZ, int segX, int segZ)
Definition Grass.cpp:693
void setTime(float seconds)
Definition Grass.cpp:709
void update(float dt)
Definition Grass.cpp:704
GrassField(Graphics *gfx)
Definition Grass.cpp:597
Result< void > applyPhotoModeField(const PhotoModeAssignment &assignment) override
Apply one accepted assignment atomically on the game thread.
Definition Grass.cpp:611
void setPhotoModeAuthority(bool enabled)
Register or revoke this field as the unique Pcg photo-mode grass authority.
Definition Grass.cpp:609
static void removeCaptureExtraDrawer(uint64_t token)
Unregister an offscreen capture contributor.
static uint64_t addCaptureExtraDrawer(uint32_t reflectionCaptureMask, CaptureExtraDrawer drawer)
Register custom forward geometry for offscreen captures.
Custom GPU program.
Definition Shader.h:39
void sendFloat(const std::string &name, float x)
Definition Shader.cpp:95
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
This module is responsible for decoding files such as PNG, GIF, JPEG into raw pixel data,...
Definition Image.h:22
std::vector< ParamSpec > params
float clampf(float v, float lo, float hi)
Clampf.
Definition AnimMath.h:34
t3ssel8r-style stylized grass.
Definition Grass.cpp:28
void bindLayer(Shader *shader, bool alwaysDark)
Binds layer.
Definition Grass.cpp:291
void setFrameDuration(Shader *shader, float seconds)
Sets the frame duration.
Definition Grass.cpp:301
int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount)
Discrete 4-frame index with a per-instance phase offset.
Definition Grass.cpp:322
Texture * createSwayAtlasFromFiles(Graphics *gfx, const std::vector< std::string > &grassFiles, const std::vector< std::string > &leafFiles, PackedAtlasInfo *infoOut)
Creates sway atlas from files.
Definition Grass.cpp:471
Shader * createShader(Graphics *gfx)
Creates shader.
Definition Grass.cpp:306
int swayAtlasWidth(int frameW, int frames)
Sway atlas width.
Definition Grass.cpp:331
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:484
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:334
Texture * createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames)
Creates sway atlas.
Definition Grass.cpp:348
EVENGINE_API_BACKENDS void bindDefaults(Shader *shader)
Binds defaults.
int swayAtlasHeight(int frameH)
Sway atlas height.
Definition Grass.cpp:332
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:511
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:417
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:550
void setTime(Shader *shader, float seconds)
Sets the time.
Definition Grass.cpp:296
EVENGINE_API_BACKENDS BillboardMesh buildBillboards(const std::vector< Point > &points, float width=0.62f, float height=0.95f, bool alwaysDark=false)
Expand a unit rectangle per point. Vertex layout: pos = grass root uv = quad corner in [0,...
void bindAtlasLayout(Shader *shader, const PackedAtlasInfo &info)
Binds atlas layout.
Definition Grass.cpp:281
constexpr const char * kGrassVertWgsl
Definition GrassWgsl.h:6
constexpr const char * kGrassFragWgsl
Definition GrassWgsl.h:124
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition Animation.h:25
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
WidgetDesc grid(int columns, std::vector< WidgetDesc > children, std::string id)
Fixed-column grid; children are placed in source order.
Definition Widget.cpp:687
PhotoModeFieldAcceptance
Capability implemented by the composition root that routes assignments to domain owners.
bool enabled
One reversible field assignment in a photo-mode transaction.
BakeParams public API.
Definition Grass.h:196
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
static TextureSampler nearest()
Nearest.
static TextureSampler linear()
Linear.
Layout of a packed 2x2-per-variant sway atlas (4 grass + 2 leaf typical).
Definition Grass.h:144
Point public API.
Definition Grass.h:74
SampleParams public API.
Definition Grass.h:86
float minSlopeDot
Skip faces whose Y-up slope is below this (0 = keep walls).
Definition Grass.h:91
glm::uvec4 info