载入中...
搜索中...
未找到
UnrealImporter.cpp
浏览该文件的文档.
2
4
5#include <cmath>
6#include <set>
7
8namespace eve::asset_import {
9namespace {
10
11const Value* member(const Value::Object& object, std::string_view key) {
12 const auto found = object.find(std::string(key));
13 return found == object.end() ? nullptr : &found->second;
14}
15
16Result<std::string> requiredString(const Value::Object& object, std::string_view key, std::string path) {
17 const Value* value = member(object, key);
18 if (!value || !value->isString() || value->asString().empty())
20 "Unreal adapter field must be a non-empty string",
21 path, {}, "asset.import"));
22 return Result<std::string>::success(value->asString());
23}
24
25Result<std::uint32_t> requiredUint32(const Value::Object& object, std::string_view key, std::string path) {
26 const Value* value = member(object, key);
27 if (!value || !value->isInt64() || value->asInt() <= 0 ||
28 value->asInt() > std::numeric_limits<std::uint32_t>::max())
30 "Unreal adapter field must be a positive uint32",
31 path, {}, "asset.import"));
32 return Result<std::uint32_t>::success(static_cast<std::uint32_t>(value->asInt()));
33}
34
35Result<float> number(const Value& value, std::string path) {
36 double result = 0;
37 if (value.isDouble()) result = value.asDouble();
38 else if (value.isInt64()) result = static_cast<double>(value.asInt());
39 else
40 return Result<float>::failure(Diagnostic::error(DiagnosticCode::ParseError, "Unreal numeric field is invalid",
41 path, {}, "asset.import"));
42 if (!std::isfinite(result) || result < -std::numeric_limits<float>::max() ||
43 result > std::numeric_limits<float>::max())
45 DiagnosticCode::ParseError, "Unreal numeric field is non-finite", path, {}, "asset.import"));
46 return Result<float>::success(static_cast<float>(result));
47}
48
49template <std::size_t Count>
50Result<std::array<float, Count>> numberArray(const Value* value, std::string path) {
51 const auto* array = value ? value->getIf<Value::Array>() : nullptr;
52 if (!array || array->size() != Count)
53 return Result<std::array<float, Count>>::failure(Diagnostic::error(DiagnosticCode::ParseError,
54 "Unreal vector field has the wrong shape",
55 path, {}, "asset.import"));
56 std::array<float, Count> result{};
57 for (std::size_t index = 0; index < Count; ++index) {
58 auto parsed = number((*array)[index], path + "[" + std::to_string(index) + "]");
59 if (!parsed) return Result<std::array<float, Count>>::failure(parsed.status());
60 result[index] = parsed.value();
61 }
62 return Result<std::array<float, Count>>::success(result);
63}
64
65bool safeProjectPath(std::string_view path, const AssetImportLimits& limits) {
66 if (path.empty() || path.size() > limits.maximumStringBytes || path.front() == '/' ||
67 path.find('\\') != std::string_view::npos || path.find(':') != std::string_view::npos) return false;
68 std::size_t start = 0;
69 for (std::size_t index = 0; index <= path.size(); ++index) {
70 if (index != path.size() && path[index] != '/') continue;
71 const auto segment = path.substr(start, index - start);
72 if (segment.empty() || segment == "." || segment == "..") return false;
73 start = index + 1;
74 }
75 return true;
76}
77
78std::array<float, 4> unrealQuaternionToCanonical(const std::array<float, 4>& source) {
79 const float x = source[0], y = source[1], z = source[2], w = source[3];
80 const float r[3][3] = {
81 {1 - 2*y*y - 2*z*z, 2*x*y - 2*z*w, 2*x*z + 2*y*w},
82 {2*x*y + 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z - 2*x*w},
83 {2*x*z - 2*y*w, 2*y*z + 2*x*w, 1 - 2*x*x - 2*y*y}
84 };
85 // C maps UE (X forward,Y right,Z up) to canonical (X right,Y up,-Z forward).
86 const float c[3][3] = {{0,1,0},{0,0,1},{-1,0,0}};
87 float cr[3][3]{};
88 float out[3][3]{};
89 for (int row = 0; row < 3; ++row) for (int col = 0; col < 3; ++col)
90 for (int k = 0; k < 3; ++k) cr[row][col] += c[row][k] * r[k][col];
91 for (int row = 0; row < 3; ++row) for (int col = 0; col < 3; ++col)
92 for (int k = 0; k < 3; ++k) out[row][col] += cr[row][k] * c[col][k];
93 std::array<float, 4> q{};
94 const float trace = out[0][0] + out[1][1] + out[2][2];
95 if (trace > 0) {
96 const float s = std::sqrt(trace + 1.0f) * 2;
97 q = {(out[2][1] - out[1][2]) / s, (out[0][2] - out[2][0]) / s,
98 (out[1][0] - out[0][1]) / s, 0.25f * s};
99 } else if (out[0][0] > out[1][1] && out[0][0] > out[2][2]) {
100 const float s = std::sqrt(1 + out[0][0] - out[1][1] - out[2][2]) * 2;
101 q = {0.25f*s, (out[0][1]+out[1][0])/s, (out[0][2]+out[2][0])/s,
102 (out[2][1]-out[1][2])/s};
103 } else if (out[1][1] > out[2][2]) {
104 const float s = std::sqrt(1 + out[1][1] - out[0][0] - out[2][2]) * 2;
105 q = {(out[0][1]+out[1][0])/s, 0.25f*s, (out[1][2]+out[2][1])/s,
106 (out[0][2]-out[2][0])/s};
107 } else {
108 const float s = std::sqrt(1 + out[2][2] - out[0][0] - out[1][1]) * 2;
109 q = {(out[0][2]+out[2][0])/s, (out[1][2]+out[2][1])/s, 0.25f*s,
110 (out[1][0]-out[0][1])/s};
111 }
112 return q;
113}
114
115Result<void> preserveSource(PreparedAssetImport& output, const UnrealProjectImportRequest& request,
116 std::string_view path) {
117 if (!safeProjectPath(path, request.limits))
119 "Unreal source path is unsafe", std::string(path), {},
120 "asset.import.unreal"));
121 const auto found = request.files.find(std::string(path));
122 if (found == request.files.end())
124 "referenced Unreal source file was not supplied",
125 std::string(path), {}, "asset.import.unreal"));
126 output.entries.push_back({"sources/unreal/" + std::string(path), found->second});
127 return Result<void>::success();
128}
129
130} // namespace
131
133 if (!safeProjectPath(request.descriptorPath, request.limits))
135 DiagnosticCode::InvalidArgument, "Unreal adapter descriptor path is unsafe", {}, {}, "asset.import"));
136 const auto descriptorFile = request.files.find(request.descriptorPath);
137 if (descriptorFile == request.files.end() || descriptorFile->second.size() > request.limits.maximumSourceBytes)
139 "Unreal adapter descriptor was not supplied",
140 request.descriptorPath, {}, "asset.import"));
141 std::uint64_t sourceTotal = 0;
142 for (const auto& [path, bytes] : request.files) {
143 if (!safeProjectPath(path, request.limits) || bytes.size() > request.limits.maximumSourceBytes ||
144 sourceTotal > request.limits.maximumSourceBytes - bytes.size())
146 Diagnostic::error(DiagnosticCode::InvalidArgument, "Unreal source path or total budget is invalid",
147 path, {}, "asset.import"));
148 sourceTotal += bytes.size();
149 }
150 const std::string json(descriptorFile->second.begin(), descriptorFile->second.end());
151 auto parsed = Value::fromJson(json);
152 if (!parsed) return Result<PreparedAssetImport>::failure(parsed.status());
153 const auto* root = parsed.value().getIf<Value::Object>();
154 if (!root)
156 DiagnosticCode::ParseError, "Unreal adapter descriptor root must be an object", {}, {}, "asset.import"));
157 auto schema = requiredString(*root, "schema", "$.schema");
158 auto version = requiredUint32(*root, "schemaVersion", "$.schemaVersion");
159 if (!schema || !version)
161 DiagnosticCode::ParseError, "Unreal descriptor envelope is invalid", {}, {}, "asset.import"));
162 if (schema.value() != "eve.unreal-landscape-import" || version.value() != 1)
164 Diagnostic::error(DiagnosticCode::UnknownVersion, "unsupported Unreal adapter descriptor schema/version",
165 {}, {}, "asset.import"));
166 const auto* landscapeValue = member(*root, "landscape");
167 const auto* landscape = landscapeValue ? landscapeValue->getIf<Value::Object>() : nullptr;
168 if (!landscape)
170 DiagnosticCode::ParseError, "Unreal descriptor landscape is required", {}, {}, "asset.import"));
171 auto sourceWidth = requiredUint32(*landscape, "width", "$.landscape.width");
172 auto sourceHeight = requiredUint32(*landscape, "height", "$.landscape.height");
173 auto heightmap = requiredString(*landscape, "heightmap", "$.landscape.heightmap");
174 auto scale = numberArray<3>(member(*landscape, "scaleCm"), "$.landscape.scaleCm");
175 if (!sourceWidth || !sourceHeight || !heightmap || !scale)
177 DiagnosticCode::ParseError, "Unreal Landscape fields are invalid", {}, {}, "asset.import"));
178 if (sourceWidth.value() < 2 || sourceHeight.value() < 2 || scale.value()[0] <= 0 ||
179 scale.value()[1] <= 0 || scale.value()[2] <= 0 || !safeProjectPath(heightmap.value(), request.limits))
181 Diagnostic::error(DiagnosticCode::InvalidArgument, "Unreal Landscape dimensions/scale/path are invalid", {},
182 {}, "asset.import"));
183 const auto heightFile = request.files.find(heightmap.value());
184 const std::uint64_t sampleCount = std::uint64_t(sourceWidth.value()) * sourceHeight.value();
185 if (heightFile == request.files.end() || sampleCount > request.limits.maximumDecodedBytes / sizeof(float) ||
186 heightFile->second.size() != sampleCount * 2)
188 "Unreal Landscape R16 byte count is invalid",
189 heightmap.value(), {}, "asset.import"));
191 // UE X rows become canonical -Z rows; UE Y columns become canonical +X columns.
192 terrain.width = sourceWidth.value();
193 terrain.height = sourceHeight.value();
194 terrain.spacingX = scale.value()[1] / 100.0f;
195 terrain.spacingZ = scale.value()[0] / 100.0f;
196 terrain.heightsMeters.resize(sampleCount);
197 for (std::uint32_t sourceX = 0; sourceX < sourceHeight.value(); ++sourceX) {
198 for (std::uint32_t sourceY = 0; sourceY < sourceWidth.value(); ++sourceY) {
199 const std::size_t sourceIndex = std::size_t(sourceX) * sourceWidth.value() + sourceY;
200 const std::uint16_t encoded = std::uint16_t(heightFile->second[sourceIndex * 2]) |
201 (std::uint16_t(heightFile->second[sourceIndex * 2 + 1]) << 8);
202 const std::size_t canonicalRow = sourceHeight.value() - 1 - sourceX;
203 const std::size_t canonicalColumn = sourceY;
204 terrain.heightsMeters[canonicalRow * terrain.width + canonicalColumn] =
205 (float(encoded) - 32768.0f) / 128.0f * scale.value()[2] / 100.0f;
206 }
207 }
208 terrain.sourceTransform = {{"source", Value("ue-left-handed-x-forward-y-right-z-up-centimeters")},
209 {"conversion", Value("canonical=(ueY,ueZ,-ueX)/100")},
210 {"landscapeHeight", Value("(r16-32768)/128*zScaleCm/100")}};
211
212 std::set<std::string> preserved;
213 const Value* layersValue = member(*root, "layers");
214 const auto* layers = layersValue ? layersValue->getIf<Value::Array>() : nullptr;
215 if (layers) for (std::size_t index = 0; index < layers->size(); ++index) {
216 const auto* layer = (*layers)[index].getIf<Value::Object>();
217 if (!layer)
219 Diagnostic::error(DiagnosticCode::ParseError, "M4 layer must be an object", {}, {}, "asset.import"));
220 auto name = requiredString(*layer, "name", "$.layers[].name");
221 if (!name) return Result<PreparedAssetImport>::failure(name.status());
223 output.name = name.value(); output.normalConvention = "directx";
224 if (const Value* diffuse = member(*layer, "diffuse")) {
225 if (!diffuse->isString() || !safeProjectPath(diffuse->asString(), request.limits))
227 DiagnosticCode::InvalidArgument, "M4 diffuse path is invalid", {}, {}, "asset.import"));
228 output.diffuseSource = "sources/unreal/" + diffuse->asString(); preserved.emplace(diffuse->asString());
229 }
230 if (const Value* normal = member(*layer, "normal")) {
231 if (!normal->isString() || !safeProjectPath(normal->asString(), request.limits))
233 DiagnosticCode::InvalidArgument, "M4 normal path is invalid", {}, {}, "asset.import"));
234 output.normalSource = "sources/unreal/" + normal->asString(); preserved.emplace(normal->asString());
235 }
236 if (const Value* weight = member(*layer, "weight")) {
237 if (!weight->isString() || !safeProjectPath(weight->asString(), request.limits))
239 DiagnosticCode::InvalidArgument, "M4 weight path is invalid", {}, {}, "asset.import"));
240 output.weightSource = "sources/unreal/" + weight->asString(); preserved.emplace(weight->asString());
241 }
242 if (const Value* tile = member(*layer, "tileSizeCm")) {
243 auto parsedTile = number(*tile, "$.layers[].tileSizeCm");
244 if (!parsedTile || parsedTile.value() <= 0)
246 DiagnosticCode::InvalidArgument, "M4 tile size is invalid", {}, {}, "asset.import"));
247 output.tileSizeMeters = parsedTile.value() / 100.0f;
248 }
249 terrain.layers.push_back(std::move(output));
250 }
251 const Value* grassValue = member(*root, "grass");
252 const auto* grass = grassValue ? grassValue->getIf<Value::Array>() : nullptr;
253 if (grass) for (std::size_t index = 0; index < grass->size(); ++index) {
254 const auto* rule = (*grass)[index].getIf<Value::Object>();
255 if (!rule)
257 DiagnosticCode::ParseError, "M4 grass rule must be an object", {}, {}, "asset.import"));
258 auto id = requiredString(*rule, "id", "$.grass[].id");
259 auto prototype = requiredString(*rule, "prototype", "$.grass[].prototype");
260 auto layer = requiredString(*rule, "layer", "$.grass[].layer");
261 const Value* densityValue = member(*rule, "densityPerSquareMeter");
262 if (!id || !prototype || !layer || !densityValue)
264 DiagnosticCode::ParseError, "M4 grass fields are incomplete", {}, {}, "asset.import"));
265 auto density = number(*densityValue, "$.grass[].densityPerSquareMeter");
266 if (!density) return Result<PreparedAssetImport>::failure(density.status());
267 CanonicalScatterRule output{id.value(), prototype.value(), layer.value(), density.value(), 0, 1.57079632679f, 0};
268 if (const Value* seed = member(*rule, "seed")) {
269 if (!seed->isInt64() || seed->asInt() < 0)
271 Diagnostic::error(DiagnosticCode::ParseError, "M4 grass seed is invalid", {}, {}, "asset.import"));
272 output.seed = static_cast<std::uint64_t>(seed->asInt());
273 }
274 terrain.scatterRules.push_back(std::move(output));
275 }
276 const Value* instancesValue = member(*root, "instances");
277 const auto* instances = instancesValue ? instancesValue->getIf<Value::Array>() : nullptr;
278 if (instances) for (const auto& value : *instances) {
279 const auto* instance = value.getIf<Value::Object>();
280 if (!instance)
282 Diagnostic::error(DiagnosticCode::ParseError, "UE instance must be an object", {}, {}, "asset.import"));
283 auto prototype = requiredString(*instance, "prototype", "$.instances[].prototype");
284 auto position = numberArray<3>(member(*instance, "positionCm"), "$.instances[].positionCm");
285 auto rotation = numberArray<4>(member(*instance, "rotationQuat"), "$.instances[].rotationQuat");
286 auto scaleValue = numberArray<3>(member(*instance, "scale"), "$.instances[].scale");
287 if (!prototype || !position || !rotation || !scaleValue)
289 DiagnosticCode::ParseError, "UE instance transform is invalid", {}, {}, "asset.import"));
291 output.prototype = prototype.value();
292 output.position[0] = position.value()[1] / 100.0f;
293 output.position[1] = position.value()[2] / 100.0f;
294 output.position[2] = -position.value()[0] / 100.0f;
295 const auto q = unrealQuaternionToCanonical(rotation.value());
296 std::copy(q.begin(), q.end(), output.rotation);
297 output.scale[0] = scaleValue.value()[1]; output.scale[1] = scaleValue.value()[2];
298 output.scale[2] = scaleValue.value()[0];
299 terrain.instances.push_back(std::move(output));
300 }
301
302 auto prepared = prepareCanonicalTerrainImport(request.package, terrain, "eve.unreal-m4/1", request.limits);
303 if (!prepared) return Result<PreparedAssetImport>::failure(prepared.status());
304 PreparedAssetImport output = std::move(prepared).takeValue();
305 output.manifest.provenance["sourceEngine"] = Value("unreal-engine-5");
306 output.manifest.provenance["adapterDescriptor"] = Value(request.descriptorPath);
307 output.manifest.provenance["sourceHash"] = Value(detail::sha256(descriptorFile->second));
308 output.entries.push_back({"sources/unreal/adapter.json", descriptorFile->second});
309 preserved.emplace(heightmap.value());
310 for (const auto& path : preserved) {
311 auto stored = preserveSource(output, request, path);
313 }
314 output.findings.push_back({heightmap.value(), "Landscape.heightfield", ImportDisposition::Translated,
315 "UE R16 height and centimetre scale converted to canonical metres"});
316 if (layers) output.findings.push_back({request.descriptorPath, "M4.MaterialInstance.layers",
318 "M4 layer textures, tiling and DirectX normal convention mapped"});
319 if (grass) output.findings.push_back({request.descriptorPath, "LandscapeGrassType",
321 "grass density/layer rules mapped to deterministic PCG"});
322 if (const Value* unsupportedValue = member(*root, "unsupported")) {
323 const auto* unsupported = unsupportedValue->getIf<Value::Array>();
324 if (!unsupported)
326 Diagnostic::error(DiagnosticCode::ParseError, "unsupported must be an array", {}, {}, "asset.import"));
327 for (const auto& feature : *unsupported) {
328 if (!feature.isString())
330 DiagnosticCode::ParseError, "unsupported feature must be a string", {}, {}, "asset.import"));
331 output.findings.push_back({request.descriptorPath, feature.asString(), ImportDisposition::Unsupported,
332 "native Unreal behavior is not executed by the data-only adapter"});
333 }
334 }
335 for (const auto& asset : output.manifest.assets) {
336 std::string sourceObject = request.descriptorPath + "#" + asset.type;
337 if (asset.type == "eve.terrain") sourceObject = heightmap.value();
338 else if (asset.type == "eve.terrain-material") sourceObject = request.descriptorPath + "#layers";
339 else if (asset.type == "eve.pcg-graph") sourceObject = request.descriptorPath + "#grass";
340 else if (asset.type == "eve.instance-set") sourceObject = request.descriptorPath + "#instances";
341 output.sourceMappings.push_back({std::move(sourceObject), asset.asset});
342 }
343 auto report = detail::finalizeImportReport(output, request.package, "unreal-engine", "5.x",
344 {{"descriptorSchema", Value("eve.unreal-landscape-import/1")}});
345 if (!report) return Result<PreparedAssetImport>::failure(report.status());
347}
348
349} // namespace eve::asset_import
double value
Duration start
Trace trace
Definition Agent.cpp:49
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 root
Definition AnimSmr.cpp:119
std::string output
std::string terrain
const std::string & s
std::vector< BuildingInstanceSnapshot > instances
std::vector< std::uint8_t > stored
Definition Evpack.cpp:171
const GltfImportRequest & request
std::uint32_t key
std::array< double, 10 > q
double r
std::int32_t c
std::array< float, 4 > rotation
std::array< float, 3 > position
std::array< float, 3 > scale
std::uint64_t bytes
std::string name
Texture * normal
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
std::uint32_t seed
Definition PointSet.cpp:807
double number
bool found
float sourceX
float(ui::Theme::* member)[4]
uint32_t index
const UnitySourceAsset & source
const AssetImportLimits & limits
UE5/M⁴ landscape adapter that does not execute or link Unreal Engine.
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
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
std::string sha256(std::span< const std::uint8_t > bytes)
Sha 256.
EVENGINE_API_PLATFORM Result< void > finalizeImportReport(PreparedAssetImport &prepared, const ImportPackageIdentity &package, std::string_view sourceEngine, std::string_view sourceVersion, Value::Object options={})
Append the deterministic mandatory import report and bind it from provenance.
Result< PreparedAssetImport > prepareCanonicalTerrainImport(const ImportPackageIdentity &package, const CanonicalTerrainInput &terrain, std::string_view importer, const AssetImportLimits &limits)
Build eve.terrain, terrain material, PCG and instance assets from canonical input.
Result< PreparedAssetImport > prepareUnrealM4Import(const UnrealProjectImportRequest &request)
Import an M⁴-style Landscape/material/grass slice without launching Unreal.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
Value::Object provenance
Definition EvaManifest.h:72
One deterministic terrain scatter rule, independent of source engine nodes.
Fully parsed source terrain ready for canonical archive construction.
One baked instance in canonical right-handed metre coordinates.
One canonical terrain material layer after source-engine mapping.
Owning, validated source archive candidate not yet published to disk/database.
Uncooked Unreal content slice plus a versioned data-only adapter descriptor.