载入中...
搜索中...
未找到
VoxelWorld.cpp
浏览该文件的文档.
1#include "voxel/VoxelWorld.h"
2
4
5#include "data/ByteData.h"
6#include "graphics/Graphics.h"
8#include "thread/Thread.h"
9
10#include <algorithm>
11#include <cmath>
12#include <cstring>
13#include <limits>
14#include <thread>
15
16namespace eve::voxel {
17
18VoxelWorld::VoxelWorld() = default;
19VoxelWorld::VoxelWorld(const CubeTypeRegistry &types) : types_(types) {}
20VoxelWorld::~VoxelWorld() { waitStreamJobs(); }
21
22void VoxelWorld::setTerrainParams(uint32_t seed, uint8_t top, uint8_t sub, uint8_t stone, float baseHeight,
23 float amplitude, float scale) {
24 waitStreamJobs();
25 if (!terrainSampler_) terrainSampler_ = std::make_unique<procgen::TerrainSampler>();
26 terrainSampler_->setSeed(seed);
27 terrainSampler_->setBase(0.f);
28 terrainSampler_->setAmplitude(1.f);
29 terrainSampler_->setClamp(true, 0.f, 1.f);
30 terrainSampler_->setFrequency(scale > 0.f ? scale : 1.f / 32.f);
31 terrainTop_ = top;
32 terrainSub_ = sub;
33 terrainStone_ = stone;
34 terrainBase_ = baseHeight;
35 terrainAmplitude_ = amplitude < 0.f ? 0.f : amplitude;
36 terrainEnabled_ = true;
37}
38
39void VoxelWorld::setTerrainParam(const std::string &key, float value) {
40 waitStreamJobs();
41 if (!terrainSampler_) terrainSampler_ = std::make_unique<procgen::TerrainSampler>();
42 if (key == "seed") {
43 terrainSampler_->setSeed(uint32_t(value));
44 } else if (key == "top") {
45 terrainTop_ = uint8_t(value);
46 } else if (key == "sub") {
47 terrainSub_ = uint8_t(value);
48 } else if (key == "stone") {
49 terrainStone_ = uint8_t(value);
50 } else if (key == "sand") {
51 terrainSand_ = uint8_t(value);
52 } else if (key == "base") {
53 terrainBase_ = value;
54 } else if (key == "amplitude") {
55 terrainAmplitude_ = value < 0.f ? 0.f : value;
56 } else if (key == "scale" || key == "frequency") {
57 terrainSampler_->setFrequency(value > 0.f ? value : 1.f / 32.f);
58 } else if (key == "octaves") {
59 terrainSampler_->setOctaves(int(value));
60 } else if (key == "lacunarity") {
61 terrainSampler_->setLacunarity(value);
62 } else if (key == "gain") {
63 terrainSampler_->setGain(value);
64 } else if (key == "ridge") {
65 terrainSampler_->setRidge(value);
66 } else if (key == "warp") {
67 terrainSampler_->setWarp(value);
68 } else if (key == "exponent") {
69 terrainSampler_->setExponent(value);
70 } else if (key == "continent") {
71 terrainSampler_->setContinent(value);
72 } else if (key == "island") {
73 terrainSampler_->setIsland(value);
74 } else if (key == "coast") {
75 terrainSampler_->setCoastSoftness(value);
76 } else if (key == "worldWidth") {
77 terrainSampler_->setWorldSize(int(value), terrainSampler_->getWorldHeight());
78 } else if (key == "worldHeight") {
79 terrainSampler_->setWorldSize(terrainSampler_->getWorldWidth(), int(value));
80 } else if (key == "sandLevel") {
81 sandLevel_ = value;
82 } else if (key == "enable") {
83 terrainEnabled_ = value != 0.f;
84 }
85}
86
88 waitStreamJobs();
89 terrainEnabled_ = false;
90 terrainAssetEnabled_ = false;
91 terrainAsset_.clear();
92}
93
94namespace {
95
97void sampleTerrainColumn(const procgen::TerrainSampler *sampler, float base, float amp, uint8_t topTexDefault,
98 uint8_t sandTex, float sandLevel, int wx, int wz, int &height, uint8_t &topTex) {
99 if (!sampler) {
100 height = int(base);
101 topTex = topTexDefault;
102 return;
103 }
104 const float e = sampler->sample(float(wx), float(wz));
105 height = int(std::floor(base + amp * e));
106 topTex = (sandLevel > 0.f && sandTex != 0 && e <= sandLevel) ? sandTex : topTexDefault;
107}
108
109} // namespace
110
111namespace {
112// Cap remesh worker count: enough to parallelize chunk meshing without
113// oversubscribing small devices / browser pthread pools.
114constexpr int kMaxRemeshWorkers = 4;
115constexpr int kMaxStreamJobs = 8;
116} // namespace
117
119 const uint64_t k = key(cx, cy, cz);
120 auto it = chunks_.find(k);
121 if (it != chunks_.end()) return it->second.get();
122 auto chunk = std::make_unique<Chunk>(cx, cy, cz);
123 Chunk *raw = chunk.get();
124 chunks_.emplace(k, std::move(chunk));
125 return raw;
126}
127
128Chunk *VoxelWorld::getChunk(int cx, int cy, int cz) {
129 auto it = chunks_.find(key(cx, cy, cz));
130 return it == chunks_.end() ? nullptr : it->second.get();
131}
132
133const Chunk *VoxelWorld::getChunk(int cx, int cy, int cz) const {
134 auto it = chunks_.find(key(cx, cy, cz));
135 return it == chunks_.end() ? nullptr : it->second.get();
136}
137
138bool VoxelWorld::hasChunk(int cx, int cy, int cz) const {
139 return chunks_.find(key(cx, cy, cz)) != chunks_.end();
140}
141
142void VoxelWorld::removeChunk(int cx, int cy, int cz) {
143 if (chunks_.erase(key(cx, cy, cz)) > 0) ++revision_;
144}
145
147 waitStreamJobs();
148 const bool changed = !chunks_.empty();
149 chunks_.clear();
150 visible_.clear();
151 visibleChunkKeys_.clear();
152 if (changed) ++revision_;
153}
154
155bool VoxelWorld::loadTerrainAsset(data::ByteData *bytes, float heightOffset, float heightScale) {
156 waitStreamJobs();
157 if (!bytes || !std::isfinite(heightOffset) || !std::isfinite(heightScale)) return false;
158 std::string error;
159 if (!terrainAsset_.open(static_cast<const uint8_t *>(bytes->getData()), bytes->getSize(),
160 &error)) return false;
161 terrainAssetOffset_ = heightOffset;
162 terrainAssetScale_ = heightScale;
163 terrainAssetEnabled_ = true;
164 return true;
165}
166
168 int radiusChunks,
169 int maxLoads) {
170 if (!terrainAssetEnabled_) return {};
171 return terrainAsset_.streamAround(worldX, worldZ, radiusChunks, maxLoads);
172}
173
174void VoxelWorld::setTerrainAssetMaterials(uint8_t vegetation, uint8_t sand, uint8_t snow,
175 uint8_t alpine, uint8_t riverbed) {
176 terrainVegetation_ = vegetation;
177 terrainAssetSand_ = sand;
178 terrainSnow_ = snow;
179 terrainAlpine_ = alpine;
180 terrainRiverbed_ = riverbed;
181}
182
183int VoxelWorld::terrainHeightAt(int wx, int wz) const {
184 if (terrainAssetEnabled_) {
185 float storedHeight = 0.f;
186 if (terrainAsset_.sampleHeight(float(wx), float(wz), storedHeight))
187 return int(std::floor(terrainAssetOffset_ + terrainAssetScale_ * storedHeight));
188 }
189 if (terrainEnabled_) {
190 if (!terrainSampler_) return int(std::floor(terrainBase_));
191 const float e = terrainSampler_->sample(float(wx), float(wz));
192 return int(std::floor(terrainBase_ + terrainAmplitude_ * e));
193 }
194 return int(std::floor(terrainAssetOffset_));
195}
196
197uint8_t VoxelWorld::terrainSurfaceAt(int wx, int wz) const {
198 if (!terrainAssetEnabled_) return terrainTop_;
200 if (!terrainAsset_.sampleCell(wx, wz, sample)) return terrainTop_;
201 switch (sample.biome) {
205 return terrainAssetSand_;
208 return terrainSnow_;
210 return terrainAlpine_;
212 return terrainRiverbed_;
213 default:
214 return terrainVegetation_;
215 }
216}
217
218void VoxelWorld::setStreamCacheChunks(int extraChunks) {
219 streamCacheChunks_ = extraChunks < 0 ? 0 : extraChunks;
220}
221
222int VoxelWorld::unloadChunksOutside(int centerX, int centerY, int centerZ, int radiusChunks) {
223 if (radiusChunks < 0) return 0;
224 const int64_t r2 = int64_t(radiusChunks) * int64_t(radiusChunks);
225 std::vector<uint64_t> evict;
226 evict.reserve(chunks_.size() / 4);
227 for (auto &kv : chunks_) {
228 int cx, cy, cz;
229 unpackKey(kv.first, cx, cy, cz);
230 const int64_t dx = int64_t(cx) - centerX;
231 const int64_t dy = int64_t(cy) - centerY;
232 const int64_t dz = int64_t(cz) - centerZ;
233 if (dx * dx + dy * dy + dz * dz > r2) evict.push_back(kv.first);
234 }
235 for (uint64_t k : evict) chunks_.erase(k);
236 if (!evict.empty()) {
237 // Batch pointers may dangle after eviction; force re-selection.
238 visible_.clear();
239 visibleChunkKeys_.clear();
240 ++revision_;
241 }
242 return int(evict.size());
243}
244
245void VoxelWorld::fillChunkTerrain(Chunk &c, int nx, int ny, int nz) const {
246 const int wy0 = ny * kChunkSize;
247 for (int lz = 0; lz < kChunkSize; ++lz)
248 for (int lx = 0; lx < kChunkSize; ++lx) {
249 int h = 0;
250 uint8_t surface = terrainTop_;
251 const int wx = nx * kChunkSize + lx;
252 const int wz = nz * kChunkSize + lz;
253 if (terrainAssetEnabled_) {
255 surface = terrainSurfaceAt(wx, wz);
256 } else {
257 sampleTerrainColumn(terrainSampler_.get(), terrainBase_, terrainAmplitude_,
258 terrainTop_, terrainSand_, sandLevel_, wx, wz, h, surface);
259 }
260 for (int ly = 0; ly < kChunkSize; ++ly) {
261 const int wy = wy0 + ly;
262 if (wy <= h - 4)
263 c.set(lx, ly, lz, terrainStone_);
264 else if (wy <= h - 1)
265 c.set(lx, ly, lz, terrainSub_);
266 else if (wy == h)
267 c.set(lx, ly, lz, surface);
268 }
269 }
270}
271
272StreamStats VoxelWorld::streamAround(int centerX, int centerY, int centerZ, int radiusChunks,
273 const std::function<void(Chunk &, int, int, int)> &generator,
274 int maxCreates) {
276 if (radiusChunks < 0) return stats;
277 if (maxCreates < 0) maxCreates = 0;
278
279 const int keepRadius = radiusChunks + streamCacheChunks_;
280
281 if (!generator && terrainAssetEnabled_) {
282 const int assetChunk = terrainAsset_.asset().getChunkSize();
283 const int worldRadius = (keepRadius + 1) * kChunkSize;
284 const int assetRadius = assetChunk > 0
285 ? int(std::ceil(float(worldRadius) * 1.41421356f / float(assetChunk))) + 1 : 0;
286 const int assetLoads = maxCreates > 0 ? maxCreates : 0;
287 terrainAsset_.streamAround(centerX * kChunkSize + kChunkSize / 2,
288 centerZ * kChunkSize + kChunkSize / 2, assetRadius, assetLoads);
289 }
290
291 // Evict only past the keep radius so recently left chunks stay resident.
292 stats.evicted = unloadChunksOutside(centerX, centerY, centerZ, keepRadius);
293 reapRetiredStreamJobs();
294 restoreRetiredInside(centerX, centerY, centerZ, keepRadius);
295 dropStreamJobsOutside(centerX, centerY, centerZ, keepRadius);
296 const bool asyncTerrain = maxCreates > 0 && !generator && terrainEnabled_ && !terrainAssetEnabled_;
297 if (asyncTerrain)
298 stats.created = harvestStreamJobs(centerX, centerY, centerZ, keepRadius, maxCreates);
299
300 struct Missing {
301 int d2 = 0;
302 int nx = 0;
303 int ny = 0;
304 int nz = 0;
305 };
306 std::vector<Missing> missing;
307 const int64_t r2 = int64_t(radiusChunks) * int64_t(radiusChunks);
308 for (int dz = -radiusChunks; dz <= radiusChunks; ++dz)
309 for (int dy = -radiusChunks; dy <= radiusChunks; ++dy)
310 for (int dx = -radiusChunks; dx <= radiusChunks; ++dx) {
311 const int64_t d2 = int64_t(dx) * dx + int64_t(dy) * dy + int64_t(dz) * dz;
312 if (d2 > r2) continue;
313 const int nx = centerX + dx;
314 const int ny = centerY + dy;
315 const int nz = centerZ + dz;
316 if (hasChunk(nx, ny, nz) || isStreamJob(nx, ny, nz)) continue;
317 missing.push_back({int(d2), nx, ny, nz});
318 }
319 std::sort(missing.begin(), missing.end(), [](const Missing &a, const Missing &b) {
320 if (a.d2 != b.d2) return a.d2 < b.d2;
321 if (a.nx != b.nx) return a.nx < b.nx;
322 if (a.ny != b.ny) return a.ny < b.ny;
323 return a.nz < b.nz;
324 });
325
326 if (asyncTerrain) {
327 thread::JobSystem *jobs = nullptr;
328 try {
329 jobs = thread::Thread::create()->getJobSystem();
330 } catch (...) {
331 jobs = nullptr;
332 }
333 if (jobs && jobs->isRunning()) {
334 int submitted = 0;
335 for (const Missing &m : missing) {
336 if (int(inflight_.size()) >= kMaxStreamJobs) break;
337 if (submitted >= maxCreates) break;
338 StreamJob job;
339 job.cx = m.nx;
340 job.cy = m.ny;
341 job.cz = m.nz;
342 job.chunk = std::make_unique<Chunk>(m.nx, m.ny, m.nz);
343 Chunk *raw = job.chunk.get();
344 try {
345 CubeTypeRegistry typesCopy = types_;
346 job.job = jobs->submit([this, raw, nx = m.nx, ny = m.ny, nz = m.nz,
347 typesCopy = std::move(typesCopy)]() {
348 fillChunkTerrain(*raw, nx, ny, nz);
349 raw->remesh(typesCopy, nullptr, nullptr);
350 });
351 } catch (...) {
352 job.job = nullptr;
353 }
354 if (!job.job) {
355 fillChunkTerrain(*raw, m.nx, m.ny, m.nz);
356 raw->remesh(types_, nullptr, nullptr);
357 chunks_.emplace(key(m.nx, m.ny, m.nz), std::move(job.chunk));
358 ++stats.created;
359 ++revision_;
360 continue;
361 }
362 inflight_.push_back(std::move(job));
363 ++submitted;
364 }
365 stats.pending = int(missing.size() - size_t(submitted)) + int(inflight_.size());
366 return stats;
367 }
368 }
369
370 const size_t budget = maxCreates > 0 ? size_t(maxCreates) : missing.size();
371 const size_t createCount = std::min(budget, missing.size());
372 std::vector<Chunk *> created;
373 created.reserve(createCount);
374 for (size_t i = 0; i < createCount; ++i) {
375 const Missing &m = missing[i];
376 Chunk *c = getOrCreateChunk(m.nx, m.ny, m.nz);
377 if (generator)
378 generator(*c, m.nx, m.ny, m.nz);
379 else if (terrainEnabled_ || terrainAssetEnabled_)
380 fillChunkTerrain(*c, m.nx, m.ny, m.nz);
381 created.push_back(c);
382 ++stats.created;
383 }
384 stats.pending = int(missing.size() - createCount) + getInflightStreamCount();
385
386 if (stats.created > 0) {
387 ++revision_;
388 remeshChunks(created, 0);
389 if (maxCreates > 0) {
390 static const int nbs[6][3] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0},
391 {0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
392 for (Chunk *c : created) {
393 for (const auto &d : nbs) {
394 if (Chunk *n = getChunk(c->cx() + d[0], c->cy() + d[1], c->cz() + d[2]))
395 n->markDirty();
396 }
397 }
398 remeshDirty(1, 0);
399 }
400 }
401 return stats;
402}
403
404namespace {
405
406void putU32(std::vector<uint8_t> &out, uint32_t v) {
407 out.push_back(uint8_t(v));
408 out.push_back(uint8_t(v >> 8));
409 out.push_back(uint8_t(v >> 16));
410 out.push_back(uint8_t(v >> 24));
411}
412
413void putI32(std::vector<uint8_t> &out, int32_t v) { putU32(out, uint32_t(v)); }
414
415bool getU32(const uint8_t *&p, const uint8_t *end, uint32_t &out) {
416 if (end - p < 4) return false;
417 out = uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) |
418 (uint32_t(p[3]) << 24);
419 p += 4;
420 return true;
421}
422
423bool getI32(const uint8_t *&p, const uint8_t *end, int32_t &out) {
424 uint32_t v;
425 if (!getU32(p, end, v)) return false;
426 out = int32_t(v);
427 return true;
428}
429
430} // namespace
431
432void VoxelWorld::serializeWorld(std::vector<uint8_t> &out) const {
433 out.clear();
434 const char magic[4] = {'E', 'V', 'V', 'X'};
435 out.insert(out.end(), magic, magic + 4);
436 out.push_back(1); // version
437 putU32(out, uint32_t(chunks_.size()));
438
439 // Deterministic output: sort chunk keys so saves are byte-stable.
440 std::vector<uint64_t> keys;
441 keys.reserve(chunks_.size());
442 for (auto &kv : chunks_) keys.push_back(kv.first);
443 std::sort(keys.begin(), keys.end());
444 for (uint64_t key : keys) {
445 const auto &chunk = chunks_.at(key);
446 int cx, cy, cz;
447 unpackKey(key, cx, cy, cz);
448 putI32(out, int32_t(cx));
449 putI32(out, int32_t(cy));
450 putI32(out, int32_t(cz));
451 const uint8_t *raw = chunk->rawVoxels();
452 out.insert(out.end(), raw, raw + kChunkSize * kChunkSize * kChunkSize);
453 }
454}
455
456bool VoxelWorld::deserializeWorld(const uint8_t *data, size_t size) {
457 const uint8_t *p = data;
458 const uint8_t *end = data + size;
459 if (size < 9 || std::memcmp(p, "EVVX", 4) != 0 || p[4] != 1) return false;
460 p += 5;
461 uint32_t count = 0;
462 if (!getU32(p, end, count)) return false;
463 const size_t voxelBytes = size_t(kChunkSize) * kChunkSize * kChunkSize;
464 if (uint64_t(count) > (uint64_t(end - p)) / (12 + voxelBytes)) return false;
465
466 clear();
467 for (uint32_t i = 0; i < count; ++i) {
468 int32_t cx = 0, cy = 0, cz = 0;
469 if (!getI32(p, end, cx) || !getI32(p, end, cy) || !getI32(p, end, cz)) return false;
470 if (size_t(end - p) < voxelBytes) return false;
471 Chunk *c = getOrCreateChunk(cx, cy, cz);
472 c->setVoxelData(p);
473 p += voxelBytes;
474 }
475 ++revision_;
476 return true;
477}
478
480 std::vector<uint8_t> bytes;
482 return new data::ByteData(bytes.data(), bytes.size());
483}
484
486 if (!bytes) return false;
487 return deserializeWorld(static_cast<const uint8_t *>(bytes->getData()), bytes->getSize());
488}
489
491 int n = 0;
492 for (auto &kv : chunks_) {
493 if (kv.second->isDirty()) ++n;
494 }
495 return n;
496}
497
499 return int(inflight_.size() + retired_.size());
500}
501
502void VoxelWorld::waitStreamJobs() {
503 auto waitAll = [](std::vector<StreamJob> &jobs) {
504 for (StreamJob &job : jobs) {
505 if (!job.job) continue;
506 job.job->wait();
507 delete job.job;
508 job.job = nullptr;
509 }
510 jobs.clear();
511 };
512 waitAll(inflight_);
513 waitAll(retired_);
514}
515
516void VoxelWorld::reapRetiredStreamJobs() {
517 std::vector<StreamJob> keep;
518 keep.reserve(retired_.size());
519 for (StreamJob &job : retired_) {
520 if (job.job && !job.job->isDone()) {
521 keep.push_back(std::move(job));
522 continue;
523 }
524 if (job.job) {
525 job.job->wait();
526 delete job.job;
527 }
528 }
529 retired_ = std::move(keep);
530}
531
532bool VoxelWorld::isStreamJob(int cx, int cy, int cz) const {
533 auto match = [cx, cy, cz](const StreamJob &job) {
534 return job.cx == cx && job.cy == cy && job.cz == cz;
535 };
536 for (const StreamJob &job : inflight_) {
537 if (match(job)) return true;
538 }
539 for (const StreamJob &job : retired_) {
540 if (match(job)) return true;
541 }
542 return false;
543}
544
545void VoxelWorld::restoreRetiredInside(int centerX, int centerY, int centerZ, int radiusChunks) {
546 const int64_t r2 = int64_t(radiusChunks) * int64_t(radiusChunks);
547 std::vector<StreamJob> keep;
548 keep.reserve(retired_.size());
549 for (StreamJob &job : retired_) {
550 const int64_t dx = int64_t(job.cx) - centerX;
551 const int64_t dy = int64_t(job.cy) - centerY;
552 const int64_t dz = int64_t(job.cz) - centerZ;
553 if (dx * dx + dy * dy + dz * dz <= r2)
554 inflight_.push_back(std::move(job));
555 else
556 keep.push_back(std::move(job));
557 }
558 retired_ = std::move(keep);
559}
560
561void VoxelWorld::dropStreamJobsOutside(int centerX, int centerY, int centerZ, int radiusChunks) {
562 const int64_t r2 = int64_t(radiusChunks) * int64_t(radiusChunks);
563 std::vector<StreamJob> keep;
564 keep.reserve(inflight_.size());
565 for (StreamJob &job : inflight_) {
566 const int64_t dx = int64_t(job.cx) - centerX;
567 const int64_t dy = int64_t(job.cy) - centerY;
568 const int64_t dz = int64_t(job.cz) - centerZ;
569 if (dx * dx + dy * dy + dz * dz <= r2) {
570 keep.push_back(std::move(job));
571 continue;
572 }
573 retired_.push_back(std::move(job));
574 }
575 inflight_ = std::move(keep);
576}
577
578int VoxelWorld::harvestStreamJobs(int centerX, int centerY, int centerZ, int radiusChunks,
579 int maxHarvest) {
580 const int64_t r2 = int64_t(radiusChunks) * int64_t(radiusChunks);
581 int harvested = 0;
582 std::vector<StreamJob> keep;
583 keep.reserve(inflight_.size());
584 for (StreamJob &job : inflight_) {
585 if (!job.job || !job.job->isDone() || (maxHarvest > 0 && harvested >= maxHarvest)) {
586 keep.push_back(std::move(job));
587 continue;
588 }
589 job.job->wait();
590 delete job.job;
591 job.job = nullptr;
592 const int64_t dx = int64_t(job.cx) - centerX;
593 const int64_t dy = int64_t(job.cy) - centerY;
594 const int64_t dz = int64_t(job.cz) - centerZ;
595 const bool inside = dx * dx + dy * dy + dz * dz <= r2;
596 if (!inside || hasChunk(job.cx, job.cy, job.cz) || !job.chunk) continue;
597 chunks_.emplace(key(job.cx, job.cy, job.cz), std::move(job.chunk));
598 ++harvested;
599 }
600 inflight_ = std::move(keep);
601 if (harvested > 0) ++revision_;
602 return harvested;
603}
604
605void VoxelWorld::remeshChunks(const std::vector<Chunk *> &chunks, int maxThreads) {
606 const int count = int(chunks.size());
607 if (count == 0) return;
608
609 const auto remeshOne = [this](Chunk *c) {
610 if (!c) return;
611 c->remesh(types_, &VoxelWorld::chunkNeighborSampler, this);
612 };
613
614 int workers = maxThreads;
615 if (workers <= 0) {
616 workers = static_cast<int>(std::thread::hardware_concurrency());
617 if (workers < 1) workers = 1;
618 if (workers > kMaxRemeshWorkers) workers = kMaxRemeshWorkers;
619 }
620
621 if (workers <= 1 || count <= 1) {
622 for (Chunk *c : chunks) remeshOne(c);
623 return;
624 }
625
626 auto *jobs = thread::Thread::create()->getJobSystem();
627 thread::Job *loop = nullptr;
628 try {
629 loop = jobs->parallelFor(0, count,
630 [this, &chunks, &remeshOne](int first, int last) {
631 for (int k = first; k < last; ++k) remeshOne(chunks[size_t(k)]);
632 },
633 (count + workers - 1) / workers);
634 } catch (...) {
635 for (Chunk *c : chunks) remeshOne(c);
636 return;
637 }
638 loop->wait();
639 delete loop;
640}
641
642int VoxelWorld::remeshDirty(int maxThreads, int maxChunks) {
643 std::vector<Chunk *> dirty;
644 dirty.reserve(chunks_.size());
645 for (auto &kv : chunks_) {
646 if (!kv.second->isDirty()) continue;
647 dirty.push_back(kv.second.get());
648 if (maxChunks > 0 && int(dirty.size()) >= maxChunks) break;
649 }
650 remeshChunks(dirty, maxThreads);
651 return int(dirty.size());
652}
653
654void VoxelWorld::selectVisible(const float *viewProj16, float eyeX, float eyeY, float eyeZ,
655 float viewRange, bool faceCull) {
656 visible_.clear();
657 visibleChunkKeys_.clear();
658 if (!viewProj16) return;
659
661 const float rangeSq = viewRange > 0.f ? viewRange * viewRange : 0.f;
662
663 for (auto &kv : chunks_) {
664 Chunk *chunk = kv.second.get();
665
666 float minX, minY, minZ, maxX, maxY, maxZ;
667 chunk->worldAABB(minX, minY, minZ, maxX, maxY, maxZ);
668 const float cx = (minX + maxX) * 0.5f;
669 const float cy = (minY + maxY) * 0.5f;
670 const float cz = (minZ + maxZ) * 0.5f;
671
672 if (viewRange > 0.f) {
673 const float dx = cx - eyeX;
674 const float dy = cy - eyeY;
675 const float dz = cz - eyeZ;
676 if (dx * dx + dy * dy + dz * dz > rangeSq) continue;
677 }
678
679 if (!frustum.intersectsAABB(minX, minY, minZ, maxX, maxY, maxZ)) continue;
680
681 // Mesh only after range/frustum culling so edits far outside the view
682 // are not remeshed every frame.
683 chunk->ensureMeshed(types_, &VoxelWorld::chunkNeighborSampler, this);
684
685 visibleChunkKeys_.push_back(kv.first);
686
687 for (int i = 0; i < faceDirCount(); ++i) {
688 const FaceDir dir = FaceDir(i);
689 const int count = chunk->faceRectCount(dir);
690 if (count <= 0) continue;
691
692 if (faceCull) {
693 // A perspective camera has a different view vector at every surface point.
694 // Reject a direction only when every possible face plane in the chunk faces
695 // away from the eye. Testing against the chunk centre is not conservative:
696 // when the eye crosses that centre plane it can discard faces on the far half
697 // of the chunk that are still front-facing and visible (notably cave walls).
698 bool allBackFacing = false;
699 switch (dir) {
700 case FaceDir::PosX: allBackFacing = eyeX <= minX; break;
701 case FaceDir::NegX: allBackFacing = eyeX >= maxX; break;
702 case FaceDir::PosY: allBackFacing = eyeY <= minY; break;
703 case FaceDir::NegY: allBackFacing = eyeY >= maxY; break;
704 case FaceDir::PosZ: allBackFacing = eyeZ <= minZ; break;
705 case FaceDir::NegZ: allBackFacing = eyeZ >= maxZ; break;
706 case FaceDir::Count: break;
707 }
708 if (allBackFacing) continue;
709 }
710
711 DrawBatch batch;
712 batch.chunk = chunk;
713 batch.dir = dir;
714 batch.packed = chunk->facePackedData(dir);
715 batch.ao = chunk->faceAOPackedData(dir);
716 batch.count = count;
717 visible_.push_back(batch);
718 }
719 }
720}
721
722void VoxelWorld::getVisibleChunkCoord(int index, int &cx, int &cy, int &cz) const {
723 if (index < 0 || index >= int(visibleChunkKeys_.size())) {
724 cx = cy = cz = 0;
725 return;
726 }
727 unpackKey(visibleChunkKeys_[size_t(index)], cx, cy, cz);
728}
729
731 int n = 0;
732 for (const auto &b : visible_) n += b.count;
733 return n;
734}
735
737 if (!gfx) return;
738 for (const auto &b : visible_) {
739 if (!b.chunk || !b.packed || b.count <= 0) continue;
740 gfx->drawVoxelFaceInstances(b.packed, b.count, b.chunk->originX(), b.chunk->originY(),
741 b.chunk->originZ(), faceDirName(b.dir), atlas, tilesPerRow,
742 b.ao);
743 }
744}
745
746uint8_t VoxelWorld::getVoxel(int wx, int wy, int wz) const {
747 const int cx = floorDiv(wx);
748 const int cy = floorDiv(wy);
749 const int cz = floorDiv(wz);
750 const Chunk *c = getChunk(cx, cy, cz);
751 if (!c) return 0;
752 return c->get(wx - cx * kChunkSize, wy - cy * kChunkSize, wz - cz * kChunkSize);
753}
754
755void VoxelWorld::setVoxel(int wx, int wy, int wz, uint8_t texId) {
756 const int cx = floorDiv(wx);
757 const int cy = floorDiv(wy);
758 const int cz = floorDiv(wz);
759 const int lx = wx - cx * kChunkSize;
760 const int ly = wy - cy * kChunkSize;
761 const int lz = wz - cz * kChunkSize;
762
763 Chunk *c = getChunk(cx, cy, cz);
764 if (c && c->get(lx, ly, lz) == texId) return;
765 if (texId == 0) {
766 // Clearing an unallocated chunk is a no-op (air needs no storage).
767 if (!c) return;
768 c->set(lx, ly, lz, 0);
769 } else {
770 if (!c) c = getOrCreateChunk(cx, cy, cz);
771 c->set(lx, ly, lz, texId);
772 }
773 ++revision_;
774 markNeighborChunksDirty(cx, cy, cz, lx, ly, lz);
775}
776
777void VoxelWorld::markNeighborChunksDirty(int cx, int cy, int cz, int lx, int ly, int lz) {
778 auto mark = [this](int nx, int ny, int nz) {
779 if (Chunk *n = getChunk(nx, ny, nz)) n->markDirty();
780 };
781 if (lx == 0) mark(cx - 1, cy, cz);
782 if (lx == kChunkSize - 1) mark(cx + 1, cy, cz);
783 if (ly == 0) mark(cx, cy - 1, cz);
784 if (ly == kChunkSize - 1) mark(cx, cy + 1, cz);
785 if (lz == 0) mark(cx, cy, cz - 1);
786 if (lz == kChunkSize - 1) mark(cx, cy, cz + 1);
787}
788
789uint8_t VoxelWorld::chunkNeighborSampler(void *userData, int chunkX, int chunkY, int chunkZ,
790 int localX, int localY, int localZ) {
791 const auto *self = static_cast<const VoxelWorld *>(userData);
792 const int wx = chunkX * kChunkSize + localX;
793 const int wy = chunkY * kChunkSize + localY;
794 const int wz = chunkZ * kChunkSize + localZ;
795 return self->getVoxel(wx, wy, wz);
796}
797
798bool VoxelWorld::raycast(float ox, float oy, float oz, float dx, float dy, float dz,
799 float maxDist, int &hitX, int &hitY, int &hitZ, int &prevX,
800 int &prevY, int &prevZ, int &faceX, int &faceY, int &faceZ) const {
801 const float lenSq = dx * dx + dy * dy + dz * dz;
802 if (lenSq <= 1e-12f || maxDist <= 0.f) return false;
803 const float invLen = 1.f / std::sqrt(lenSq);
804 const float rx = dx * invLen;
805 const float ry = dy * invLen;
806 const float rz = dz * invLen;
807
808 int ix = int(std::floor(ox));
809 int iy = int(std::floor(oy));
810 int iz = int(std::floor(oz));
811
812 if (getVoxel(ix, iy, iz) != 0) {
813 hitX = prevX = ix;
814 hitY = prevY = iy;
815 hitZ = prevZ = iz;
816 faceX = faceY = faceZ = 0;
817 return true;
818 }
819
820 const float inf = std::numeric_limits<float>::infinity();
821 const int stepX = rx > 0.f ? 1 : -1;
822 const int stepY = ry > 0.f ? 1 : -1;
823 const int stepZ = rz > 0.f ? 1 : -1;
824 const float absInvX = rx != 0.f ? std::fabs(1.f / rx) : inf;
825 const float absInvY = ry != 0.f ? std::fabs(1.f / ry) : inf;
826 const float absInvZ = rz != 0.f ? std::fabs(1.f / rz) : inf;
827 float tMaxX = rx != 0.f ? (rx > 0.f ? float(ix + 1) - ox : ox - float(ix)) * absInvX : inf;
828 float tMaxY = ry != 0.f ? (ry > 0.f ? float(iy + 1) - oy : oy - float(iy)) * absInvY : inf;
829 float tMaxZ = rz != 0.f ? (rz > 0.f ? float(iz + 1) - oz : oz - float(iz)) * absInvZ : inf;
830
831 prevX = ix;
832 prevY = iy;
833 prevZ = iz;
834 faceX = faceY = faceZ = 0;
835
836 constexpr int kMaxSteps = 4096;
837 for (int iter = 0; iter < kMaxSteps; ++iter) {
838 float t;
839 if (tMaxX <= tMaxY && tMaxX <= tMaxZ) {
840 ix += stepX;
841 t = tMaxX;
842 tMaxX += absInvX;
843 faceX = -stepX;
844 faceY = 0;
845 faceZ = 0;
846 } else if (tMaxY <= tMaxZ) {
847 iy += stepY;
848 t = tMaxY;
849 tMaxY += absInvY;
850 faceX = 0;
851 faceY = -stepY;
852 faceZ = 0;
853 } else {
854 iz += stepZ;
855 t = tMaxZ;
856 tMaxZ += absInvZ;
857 faceX = 0;
858 faceY = 0;
859 faceZ = -stepZ;
860 }
861 if (t > maxDist) return false;
862 if (getVoxel(ix, iy, iz) != 0) {
863 hitX = ix;
864 hitY = iy;
865 hitZ = iz;
866 return true;
867 }
868 prevX = ix;
869 prevY = iy;
870 prevZ = iz;
871 }
872 return false;
873}
874
875bool VoxelWorld::raycastScript(float ox, float oy, float oz, float dx, float dy, float dz,
876 float maxDist) {
877 raycastHit_ = raycast(ox, oy, oz, dx, dy, dz, maxDist, raycastHitX_, raycastHitY_,
878 raycastHitZ_, raycastPrevX_, raycastPrevY_, raycastPrevZ_,
879 raycastFaceX_, raycastFaceY_, raycastFaceZ_);
880 return raycastHit_;
881}
882
883void VoxelWorld::setVoxelByName(int wx, int wy, int wz, const std::string &name, int orientation) {
884 const CubeType *t = types_.find(name);
885 uint8_t id = 0;
886 if (t) id = types_.variantId(name, orientation);
887 setVoxel(wx, wy, wz, id);
888}
889
890std::string VoxelWorld::getCubeTypeName(int wx, int wy, int wz) const {
891 const CubeType *t = types_.find(getVoxel(wx, wy, wz));
892 return t ? t->name : std::string{};
893}
894
895uint8_t VoxelWorld::getCubeTypeTex(int wx, int wy, int wz, const std::string &faceDir) const {
896 FaceDir d;
897 if (!faceDirFromName(faceDir, d)) return 0;
898 const uint8_t id = getVoxel(wx, wy, wz);
899 return resolveFaceTex(types_, id, d);
900}
901
902} // namespace eve::voxel
double value
SQInteger top
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float nx
float nz
float ny
glm::vec4 p[6]
std::uint32_t key
vk::UniqueSampler sampler
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t c
std::int32_t first
int h
std::uint32_t height
std::array< float, 3 > scale
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
OnnxTransferStats stats
Definition OnnxGpgpu.cpp:56
std::string error
Definition Package.cpp:60
bool dirty
std::uint32_t seed
Definition PointSet.cpp:807
float d
float t
FrustumPlanes frustum
int created
float dz
float dy
float dx
std::uint32_t count
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
uint32_t index
double oy
std::vector< char > inside
double ox
float m[16]
float wz
float wx
float wy
In-memory byte buffer implementing eve::Data (ref-counted).
Definition ByteData.h:13
virtual void drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, float originY, float originZ, const std::string &faceDir, Texture *atlas, int tilesPerRow=16, const uint32_t *ao=nullptr)=0
Instanced voxel face rectangles (32-bit packed instances). ao: optional per-instance ambient-occlusio...
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
int getChunkSize() const
Returns the chunk size.
Deterministic terrain height sampling (Red Blob Games / Perlin fBm recipe).
bool open(const uint8_t *data, size_t size, std::string *error=nullptr)
Compatibility operation that opens EVTR and clears decoded chunks.
bool sampleHeight(float worldX, float worldY, float &out) const
Compatibility operation for cross-chunk bilinear height sampling.
TerrainStreamStats streamAround(int worldX, int worldY, int radiusChunks, int maxLoads=0, std::string *error=nullptr)
Stream chunks around a world-space sample coordinate.
bool sampleCell(int worldX, int worldY, TerrainSample &out) const
Compatibility operation that reads a resident integer world cell.
const TerrainAsset & asset() const
Asset.
Abstract job scheduler: dependencies, parallel_for, task_group, arena.
Definition JobSystem.h:157
virtual Job * submit(JobFunc body)=0
Create a job and schedule it immediately.
virtual bool isRunning() const =0
Whether the system is still accepting new jobs.
One 32³ voxel chunk with six direction-sorted packed-rect instance buffers. Coordinates: chunk (cx,...
Definition Chunk.h:18
const uint32_t * faceAOPackedData(FaceDir dir) const
Face ao packed data.
Definition Chunk.h:126
void ensureMeshed(const CubeTypeRegistry &types=CubeTypeRegistry::empty(), ChunkSampler sampler=nullptr, void *samplerUserData=nullptr)
Ensure mesh is up to date; remesh if dirty.
Definition Chunk.h:101
void worldAABB(float &minX, float &minY, float &minZ, float &maxX, float &maxY, float &maxZ) const
World aabb.
Definition Chunk.h:41
const uint32_t * facePackedData(FaceDir dir) const
Face packed data.
Definition Chunk.h:116
int faceRectCount(FaceDir dir) const
Face rect count.
Definition Chunk.h:113
uint8_t get(int x, int y, int z) const
Returns the value.
Definition Chunk.h:52
void remesh(const CubeTypeRegistry &types=CubeTypeRegistry::empty(), ChunkSampler sampler=nullptr, void *samplerUserData=nullptr)
Rebuild six face instance buffers via greedy meshing.
Definition Chunk.h:91
EVENGINE_API_DOMAINS public API.
const CubeType * find(const std::string &name) const
按名字返回 0 度变体;未找到返回 nullptr。
uint8_t variantId(const std::string &name, int orientation) const
名字 + orientation(0..3) 对应的具体类型 id;非方向性类型忽略 orientation。
int remeshDirty(int maxThreads=0, int maxChunks=0)
Remesh dirty chunks.
bool loadTerrainAsset(data::ByteData *bytes, float heightOffset=0.f, float heightScale=1.f)
Compatibility operation that opens baked EVTR terrain data.
data::ByteData * saveWorld() const
Script-facing wrappers around serialize/deserialize.
int terrainHeightAt(int wx, int wz) const
Terrain height (world blocks) at a column for the configured seed.
void setVoxelByName(int wx, int wy, int wz, const std::string &name, int orientation=0)
按方块名 + orientation(0..3) 设置体素;内部解析为具体类型 id 后写 Chunk。 未注册的名字按空气(0)处理。
void setTerrainParams(uint32_t seed, uint8_t top, uint8_t sub, uint8_t stone, float baseHeight, float amplitude, float scale)
Configure the built-in terrain generator used by streamAround(). The sampling itself lives in procgen...
uint8_t getCubeTypeTex(int wx, int wy, int wz, const std::string &faceDir) const
该体素在某面方向上的纹理 id(faceDir 如 "posX"/"+y"/"negZ")。
uint8_t getVoxel(int wx, int wy, int wz) const
World-space voxel get/set. Air (0) never allocates a chunk; a border edit also invalidates the adjace...
void drawVisible(graphics::Graphics *gfx, graphics::Texture *atlas, int tilesPerRow=16)
Issue Graphics::drawVoxelFaceInstances for every visible batch. Requires begin3DFrame + setMesh3DView...
~VoxelWorld()
Voxel world.
Chunk * getOrCreateChunk(int cx, int cy, int cz)
Returns the or create chunk.
procgen::TerrainStreamStats streamTerrainAssetAround(int worldX, int worldZ, int radiusChunks, int maxLoads=0)
Decode/evict EVTR chunks around a world-space column with an optional load budget.
bool raycast(float ox, float oy, float oz, float dx, float dy, float dz, float maxDist, int &hitX, int &hitY, int &hitZ, int &prevX, int &prevY, int &prevZ, int &faceX, int &faceY, int &faceZ) const
Voxel raycast (Amanatides & Woo DDA). Returns true when a solid voxel is found within maxDist world u...
bool deserializeWorld(const uint8_t *data, size_t size)
Deserialize world.
void setVoxel(int wx, int wy, int wz, uint8_t texId)
Sets the voxel.
bool raycastScript(float ox, float oy, float oz, float dx, float dy, float dz, float maxDist)
Script-facing raycast: stores the last result, returns hit/miss.
int getDirtyCount() const
Number of chunks whose mesh is stale.
std::string getCubeTypeName(int wx, int wy, int wz) const
该体素所属方块类型名(未注册或空气返回空串)。
bool loadWorld(data::ByteData *bytes)
Loads world.
void disableTerrain()
Disables terrain.
VoxelWorld()
Voxel world.
void selectVisible(const float *viewProj16, float eyeX, float eyeY, float eyeZ, float viewRange, bool faceCull=true)
Select chunks/faces to draw.
int getInflightStreamCount() const
Terrain jobs still running (not yet inserted into the world).
void removeChunk(int cx, int cy, int cz)
Removes chunk.
Chunk * getChunk(int cx, int cy, int cz)
Returns the chunk.
void getVisibleChunkCoord(int index, int &cx, int &cy, int &cz) const
Returns the visible chunk coord.
bool hasChunk(int cx, int cy, int cz) const
True when chunk.
StreamStats streamAround(int centerX, int centerY, int centerZ, int radiusChunks, const std::function< void(Chunk &, int, int, int)> &generator={}, int maxCreates=0)
Player-centered streaming: fill missing chunks inside radiusChunks, unload only past radius + getStre...
int getVisibleRectCount() const
Returns the visible rect count.
void setStreamCacheChunks(int extraChunks)
Extra chunk radius kept after leaving the stream sphere. streamAround still creates only inside radiu...
void setTerrainAssetMaterials(uint8_t vegetation, uint8_t sand, uint8_t snow, uint8_t alpine, uint8_t riverbed)
Configure biome surface ids: vegetation, sand, snow, alpine rock, river bed.
int unloadChunksOutside(int centerX, int centerY, int centerZ, int radiusChunks)
Streaming eviction: drop chunks whose center is farther than radiusChunks chunk units from (centerX,...
void setTerrainParam(const std::string &key, float value)
细粒度地形参数配置(脚本用)。key 支持: seed/top/sub/stone/sand/base/amplitude/scale(=frequency)/ octaves/lacunarity/g...
void serializeWorld(std::vector< uint8_t > &out) const
Persistence: serialize every chunk (coords + raw voxels) into out. Format is portable little-endian: ...
方块类型定义:名字、各面图集纹理、方向性、组合声明。 方向性方块在注册时按 orientation 绕 Y 轴展开成多个"具体类型"变体, 每个变体持有旋转后的各面纹理;渲染端只消费纹理 id,不接触 ...
Definition Chunk.h:12
constexpr int kChunkSize
Chunk edge length in voxels (fixed).
Definition VoxelPack.h:8
const char * faceDirName(FaceDir d)
Face dir name.
Definition FaceDir.h:31
uint8_t resolveFaceTex(const CubeTypeRegistry &types, uint8_t id, FaceDir dir)
求某个体素在某面方向上的实际图集纹理 id。 未注册的 id 退化为“所有面 = 原 id”(向后兼容:体素值即纹理 id)。
constexpr int faceDirCount()
Face dir count.
Definition FaceDir.h:28
bool faceDirFromName(const std::string &name, FaceDir &out)
Face dir from name.
Definition FaceDir.h:44
FaceDir
Six axis-aligned face directions. Each chunk keeps a separate instance buffer per direction so camera...
Definition FaceDir.h:17
TerrainSample public API.
TerrainStreamStats public API.
CubeType public API.
Definition CubeType.h:15
std::string name
Definition CubeType.h:16
One draw batch: all packed rects of one face direction for one chunk.
Definition VoxelWorld.h:47
const uint32_t * packed
Definition VoxelWorld.h:50
const Chunk * chunk
Definition VoxelWorld.h:48
const uint32_t * ao
Definition VoxelWorld.h:51
Six frustum planes in ax+by+cz+d >= 0 form (normals point inward).
Definition Frustum.h:8
static Frustum fromViewProjColumnMajor(const float *m16)
Extract from a column-major 4x4 view-projection matrix (RH + Vulkan ZO). Layout matches glm::mat4 mem...
Definition Frustum.h:15
Result of a streaming pass.
Definition VoxelWorld.h:40