载入中...
搜索中...
未找到
TerrainFile.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
6
7#include <algorithm>
8#include <bit>
9#include <cmath>
10#include <cstring>
11#include <limits>
12#include <vector>
13
14namespace eve::procgen {
15namespace {
16
17constexpr std::uint8_t kEvtrnMagic[8] = {'E', 'V', 'T', 'R', 'N', 0, 1, 0};
18constexpr std::size_t kEvtrnHeader = 24;
20constexpr std::uint32_t kMaximumDimension = 32768;
21
23 std::string path) {
25 eve::Diagnostic::error(code, message, path, {}, "procgen.terrain-file"));
26}
27
28std::uint32_t littleU32(const std::uint8_t *bytes) {
29 return std::uint32_t(bytes[0]) | (std::uint32_t(bytes[1]) << 8) | (std::uint32_t(bytes[2]) << 16) |
30 (std::uint32_t(bytes[3]) << 24);
31}
32
33float littleF32(const std::uint8_t *bytes) {
34 return std::bit_cast<float>(littleU32(bytes));
35}
36
37bool startsWith(std::span<const std::uint8_t> bytes, const std::uint8_t *magic, std::size_t size) {
38 return bytes.size() >= size && std::memcmp(bytes.data(), magic, size) == 0;
39}
40
41eve::Result<DecodedTerrainFile> decodePcgRaw(std::span<const std::uint8_t> bytes, TerrainFileFormat format) {
42 const std::size_t bytesPerSample = format == TerrainFileFormat::Raw8 ? 1u : 2u;
43 if (bytes.empty() || bytes.size() % bytesPerSample != 0)
44 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument,
45 "Pcg RAW byte length does not match its bit depth", "raw");
46 const std::size_t samples = bytes.size() / bytesPerSample;
47 const std::size_t resolution = std::size_t(std::sqrt(double(samples)));
48 if (resolution == 0 || resolution > kMaximumDimension || resolution * resolution != samples)
49 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument,
50 "Pcg RAW payload must contain an exact square sample count", "raw");
51 DecodedTerrainFile decoded;
52 decoded.heightmap.resize(int(resolution), int(resolution));
53 decoded.hasSpacing = false;
54 decoded.format = format == TerrainFileFormat::Raw8 ? "raw8" :
55 format == TerrainFileFormat::Raw16LE ? "raw16le" : "raw16be";
56 float minimum = 1.F, maximum = 0.F;
57 std::size_t cursor = 0;
58 for (std::size_t x = 0; x < resolution; ++x)
59 for (std::size_t y = 0; y < resolution; ++y) {
60 float value;
61 if (bytesPerSample == 1) value = float(bytes[cursor++]) / 255.F;
62 else {
63 const std::uint16_t raw = format == TerrainFileFormat::Raw16LE
64 ? std::uint16_t(bytes[cursor] | std::uint16_t(bytes[cursor + 1]) << 8)
65 : std::uint16_t(std::uint16_t(bytes[cursor]) << 8 | bytes[cursor + 1]);
66 cursor += 2;
67 value = float(raw) / 65535.F;
68 }
69 decoded.heightmap.setHeight(int(x), int(y), value);
70 minimum = std::min(minimum, value);
71 maximum = std::max(maximum, value);
72 }
73 decoded.minHeight = minimum;
74 decoded.maxHeight = maximum;
75 return eve::Result<DecodedTerrainFile>::success(std::move(decoded));
76}
77
79eve::Result<DecodedTerrainFile> decodeEvtrn(std::span<const std::uint8_t> bytes) {
80 if (bytes.size() < kEvtrnHeader)
81 return terrainFileFailure(eve::DiagnosticCode::ParseError, "EVTRN payload is truncated", "evtrn");
82 const std::uint32_t width = littleU32(bytes.data() + 8);
83 const std::uint32_t height = littleU32(bytes.data() + 12);
84 const float spacingX = littleF32(bytes.data() + 16);
85 const float spacingZ = littleF32(bytes.data() + 20);
86 const std::uint64_t samples = std::uint64_t(width) * std::uint64_t(height);
87 if (width == 0 || height == 0 || width > kMaximumDimension || height > kMaximumDimension)
88 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument, "EVTRN dimensions are invalid", "evtrn");
89 if (!std::isfinite(spacingX) || !std::isfinite(spacingZ) || spacingX <= 0.f || spacingZ <= 0.f)
90 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument, "EVTRN spacing is invalid", "evtrn");
91 if (samples > (std::numeric_limits<std::uint64_t>::max() - kEvtrnHeader) / 4 ||
92 samples * 4 + kEvtrnHeader != bytes.size())
93 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument,
94 "EVTRN payload size does not match its dimensions", "evtrn");
95
96 DecodedTerrainFile decoded;
97 decoded.heightmap.resize(int(width), int(height));
98 decoded.spacingX = spacingX;
99 decoded.spacingZ = spacingZ;
100 decoded.hasSpacing = true;
101 decoded.format = "evtrn";
102 float minimum = std::numeric_limits<float>::max();
103 float maximum = std::numeric_limits<float>::lowest();
104 std::size_t cursor = kEvtrnHeader;
105 for (std::uint32_t y = 0; y < height; ++y) {
106 for (std::uint32_t x = 0; x < width; ++x) {
107 const float value = littleF32(bytes.data() + cursor);
108 cursor += 4;
109 if (!std::isfinite(value))
110 return terrainFileFailure(eve::DiagnosticCode::ParseError,
111 "EVTRN contains a non-finite height", "evtrn");
112 decoded.heightmap.setHeight(int(x), int(y), value);
113 minimum = std::min(minimum, value);
114 maximum = std::max(maximum, value);
115 }
116 }
117 decoded.minHeight = minimum;
118 decoded.maxHeight = maximum;
119 return eve::Result<DecodedTerrainFile>::success(std::move(decoded));
120}
121
123eve::Result<DecodedTerrainFile> decodeEvtr(std::span<const std::uint8_t> bytes) {
124 TerrainAsset asset;
125 std::string error;
126 if (!asset.open(bytes.data(), bytes.size(), &error))
127 return terrainFileFailure(eve::DiagnosticCode::ParseError,
128 error.empty() ? "EVTR archive is invalid" : error, "evtr");
129
130 DecodedTerrainFile decoded;
131 decoded.heightmap.resize(asset.getWidth(), asset.getHeight());
132 decoded.minHeight = asset.getMinHeight();
133 decoded.maxHeight = asset.getMaxHeight();
134 // EVTR stores no metres-per-cell, so the referencing level owns the spacing.
135 decoded.hasSpacing = false;
136 decoded.format = "evtr";
137
138 const int chunkSize = asset.getChunkSize();
139 for (const TerrainChunkEntry &entry : asset.chunks()) {
140 TerrainChunkData chunk;
141 error.clear();
142 if (!asset.loadChunk(entry.chunkX, entry.chunkY, chunk, &error))
143 return terrainFileFailure(eve::DiagnosticCode::ParseError,
144 error.empty() ? "EVTR chunk failed to decode" : error, "evtr");
145 for (int y = 0; y < chunk.height; ++y) {
146 for (int x = 0; x < chunk.width; ++x) {
147 const int gx = entry.chunkX * chunkSize + x;
148 const int gy = entry.chunkY * chunkSize + y;
149 if (!decoded.heightmap.inBounds(gx, gy)) continue;
150 decoded.heightmap.setHeight(gx, gy, chunk.heights.height(x, y));
151 }
152 }
153 }
154 return eve::Result<DecodedTerrainFile>::success(std::move(decoded));
155}
156
157} // namespace
158
160 if (name.empty()) return TerrainFileFormat::Auto;
161 std::string lowered(name);
162 std::transform(lowered.begin(), lowered.end(), lowered.begin(),
163 [](unsigned char c) { return char(std::tolower(c)); });
164 if (lowered == "evtr") return TerrainFileFormat::Evtr;
165 if (lowered == "evtrn") return TerrainFileFormat::Evtrn;
166 if (lowered == "raw8") return TerrainFileFormat::Raw8;
167 if (lowered == "raw16le") return TerrainFileFormat::Raw16LE;
168 if (lowered == "raw16be") return TerrainFileFormat::Raw16BE;
170}
171
173 TerrainFileFormat format) {
174 if (bytes.empty())
175 return terrainFileFailure(eve::DiagnosticCode::InvalidArgument, "terrain file is empty", "terrain");
176
177 if (format == TerrainFileFormat::Auto) {
178 if (startsWith(bytes, kEvtrnMagic, sizeof(kEvtrnMagic))) format = TerrainFileFormat::Evtrn;
179 else if (startsWith(bytes, reinterpret_cast<const std::uint8_t *>("EVTR"), 4)) format = TerrainFileFormat::Evtr;
180 else
181 return terrainFileFailure(eve::DiagnosticCode::Unsupported,
182 "unrecognised terrain file magic (expected EVTR or EVTRN)", "terrain");
183 }
184 if (format == TerrainFileFormat::Evtrn) {
185 if (!startsWith(bytes, kEvtrnMagic, sizeof(kEvtrnMagic)))
186 return terrainFileFailure(eve::DiagnosticCode::ParseError,
187 "EVTRN magic does not match the requested format", "evtrn");
188 return decodeEvtrn(bytes);
189 }
190 if (format == TerrainFileFormat::Raw8 || format == TerrainFileFormat::Raw16LE ||
192 return decodePcgRaw(bytes, format);
193 return decodeEvtr(bytes);
194}
195
197 auto *filesystem = eve::cap::query<eve::service::IFileSystem>();
198 if (filesystem == nullptr)
199 return terrainFileFailure(eve::DiagnosticCode::Unsupported,
200 "no filesystem service is registered in this build", "terrain");
201
202 std::vector<std::uint8_t> bytes;
203 if (!filesystem->readFile(path, bytes))
204 return terrainFileFailure(eve::DiagnosticCode::NotFound, "terrain file could not be read: " + path, path);
205 return decodeTerrainFile(std::span<const std::uint8_t>(bytes.data(), bytes.size()), format);
206}
207
208} // namespace eve::procgen
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string message
DiagnosticCode code
float maximum[3]
float minimum[3]
std::int32_t c
std::uint32_t height
std::uint32_t width
std::uint64_t bytes
std::string name
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::size_t cursor
double spacingZ
double spacingX
float size
Definition TreeMesh.cpp:156
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
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
TerrainFileFormat parseTerrainFileFormat(std::string_view name) noexcept
Parse a format selector from a script-facing string.
eve::Result< DecodedTerrainFile > decodeTerrainFile(std::span< const std::uint8_t > bytes, TerrainFileFormat format)
Decode a terrain height field from in-memory file bytes.
eve::Result< DecodedTerrainFile > loadTerrainFile(const std::string &path, TerrainFileFormat format)
Read and decode a terrain height field through the filesystem capability.
TerrainFileFormat
Persisted terrain encodings the runtime can decode.
Definition TerrainFile.h:22
@ Auto
Detect from the leading magic bytes.
@ Evtrn
Raw EVTRN heightfield blob.
@ Raw16BE
Headerless square 16-bit big-endian (Mac) Pcg RAW samples.
@ Raw8
Headerless square 8-bit Pcg RAW samples.
@ Evtr
Chunked EVTR archive.
@ Raw16LE
Headerless square 16-bit little-endian (IBM) Pcg RAW samples.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47