14std::optional<std::string> match(
const std::string&
s,
const std::string&
pattern) {
15 const std::regex expression(
pattern);
16 auto it = std::sregex_iterator(
s.begin(),
s.end(), expression);
17 if (it == std::sregex_iterator())
return {};
18 const auto result = (*it)[1].str();
19 if (++it != std::sregex_iterator())
throw Invalid{};
22double numeric(
const Value&
value) {
25 if (!std::isfinite(
n) || std::abs(
n) > std::numeric_limits<float>::max())
throw Invalid{};
28double scalar(
const std::string&
text,
const std::string&
name,
double fallback) {
29 const auto raw = match(
text,
"(?:^|\\n)\\s*- " +
name +
": *([^\\r\\n]+)");
30 if (!raw)
return fallback;
33 return numeric(decoded.value());
35double importerScalar(
const std::string&
text,
const std::string&
name,
double fallback) {
36 const auto raw = match(
text,
"(?:^|\\n)[ \\t]*" +
name +
": *([^\\r\\n]+)");
37 if (!raw)
return fallback;
40 return numeric(decoded.value());
42std::array<double, 4> vector(
const std::string&
text,
const std::string&
name, std::array<double, 4> fallback) {
43 auto raw = match(
text,
"(?:^|\\n)\\s*- " +
name +
": *(\\{[^\\r\\n]+\\})");
44 if (!raw)
return fallback;
49 for (
const auto key : {
"r",
"g",
"b",
"a"}) {
50 if (!
object.contains(
key))
throw Invalid{};
51 fallback[i++] = numeric(
object.
at(
key));
55std::optional<std::string> texture(
const std::string&
text,
const std::string&
name) {
56 auto block = match(
text,
"- " +
name +
":([\\s\\S]*?)(?:\\n - |\\n m_|$)");
57 if (!block)
return {};
58 if (match(*block, R
"(m_Texture: *\{fileID: *(0)\})")) return {};
59 auto guid = match(*block, R
"(m_Texture: *\{fileID: *2800000, guid: *([0-9a-fA-F]{32}), type: *[23]\})");
63Result<std::string> texturePath(
const UnityProjectImportRequest&
request,
const std::string& guid) {
66 if (
path.ends_with(
".meta")) {
80Result<Value::Object> textureSampler(
const UnityProjectImportRequest&
request,
const std::string&
path) {
81 const bool native =
path.ends_with(
".asset");
84 const auto u = importerScalar(
settings, native ?
"m_WrapU" :
"wrapU", -1);
85 const auto v = importerScalar(
settings, native ?
"m_WrapV" :
"wrapV", -1);
86 for (
const auto wrap : {
u,
v}) {
87 if (wrap != -1 && wrap != 0 && wrap != 1 && wrap != 2 && wrap != 3)
throw Invalid{};
91 path, {},
"asset.import"));
93 auto convert = [](
double wrap) {
return wrap == 1 ? 33071 : wrap == 2 ? 33648 : 10497; };
94 auto filter = importerScalar(
settings, native ?
"m_FilterMode" :
"filterMode", -1);
98 const auto heightNormal = importerScalar(
settings,
"convertToNormalMap", 0);
99 if (heightNormal != 0 && heightNormal != 1)
throw Invalid{};
100 filter = heightNormal == 1 ? 2 : 1;
102 const auto mipSetting = importerScalar(
settings, native ?
"m_MipCount" :
"enableMipMap", 1);
103 if (native ? (mipSetting < 1 || mipSetting != std::floor(mipSetting)) : (mipSetting != 0 && mipSetting != 1))
105 const bool mipmaps = native ? mipSetting > 1 : mipSetting == 1;
106 const int mag =
filter == 0 ? 9728 : 9729;
107 const int min = !mipmaps ? mag :
filter == 0 ? 9984 :
filter == 2 ? 9987 : 9985;
109 {{
"wrapS", convert(
u)}, {
"wrapT", convert(
v)}, {
"minFilter",
min}, {
"magFilter", mag}});
111Result<PreparedAssetImport> ormImage(
const UnityProjectImportRequest&
request,
const std::string&
text,
112 const PersistentId& materialId,
double occlusion,
double smoothness) {
113 std::vector<std::uint8_t> rgba{255, 255, 255, 255};
117 if (
auto guid = texture(
text,
"_MainMaskTex")) {
123 const auto definition =
imported.value().manifest.assets.front().definition;
125 if (entry.
path == definition) {
129 auto read32 = [&](std::size_t
p) {
130 const auto&
b = cooked.value().bulk;
131 return std::uint32_t(
b[
p]) | (std::uint32_t(
b[
p + 1]) << 8) | (std::uint32_t(
b[
p + 2]) << 16) |
132 (std::uint32_t(
b[
p + 3]) << 24);
136 rgba.assign(cooked.value().bulk.begin() + 28, cooked.value().bulk.end());
139 for (std::size_t
p = 0;
p < rgba.size();
p += 4) {
140 const auto ao = 255.0 + (double(rgba[
p + 1]) - 255.0) * occlusion;
141 const auto roughness = 255.0 - double(rgba[
p + 3]) * smoothness;
142 const auto mask = rgba[
p + 2];
143 rgba[
p] = std::uint8_t(std::lround(ao));
144 rgba[
p + 1] = std::uint8_t(std::lround(
roughness));
153enum class NormalSource { Authored, Height };
154uint32_t normalExtent(uint32_t extent,
int mode) {
155 if (mode == 0 || (extent & (extent - 1)) == 0)
return extent;
157 while (lower <= extent / 2) lower *= 2;
158 if (lower > UINT32_MAX / 2)
throw Invalid{};
159 const auto upper = lower * 2;
160 return mode == 3 || (mode == 1 && extent - lower < upper - extent) ? lower : upper;
162struct NormalResizeTap {
166std::vector<NormalResizeTap> normalResizeTaps(uint32_t
source, uint32_t
target, uint32_t coordinate,
int algorithm) {
170 std::vector<NormalResizeTap> taps;
172 taps.push_back({uint32_t(std::clamp(
index, int64_t(0), int64_t(
source - 1))),
weight});
174 if (algorithm == 1) {
188 x < 1 ? ((12 - 9 *
b - 6 *
c) *
x *
x *
x + (-18 + 12 *
b + 6 *
c) *
x *
x + 6 - 2 *
b) / 6
190 ? ((-
b - 6 *
c) *
x *
x *
x + (6 *
b + 30 *
c) *
x *
x + (-12 *
b - 48 *
c) *
x + 8 *
b + 24 *
c) / 6
195 for (
auto& tap : taps) tap.
weight /= sum;
198std::vector<double> normalResizeRgb(
const std::vector<uint8_t>& bulk, uint32_t
width, uint32_t
height,
199 uint32_t targetWidth, uint32_t targetHeight,
int algorithm) {
200 std::vector<double> rgb(
size_t(targetWidth) * targetHeight * 3);
201 for (uint32_t
y = 0;
y < targetHeight; ++
y) {
202 const auto vertical = normalResizeTaps(
height, targetHeight,
y, algorithm);
203 for (uint32_t
x = 0;
x < targetWidth; ++
x) {
204 const auto horizontal = normalResizeTaps(
width, targetWidth,
x, algorithm);
205 std::array<double, 3>
value{};
206 for (
const auto&
sy : vertical)
207 for (const auto&
sx : horizontal)
208 for (size_t
c = 0;
c < 3; ++
c)
210 for (
size_t c = 0;
c < 3; ++
c) {
211 const auto clamped = std::clamp(
value[
c], 0.0, 255.0);
214 rgb[(size_t(
y) * targetWidth +
x) * 3 +
c] =
215 algorithm == 0 ?
double(std::lround(
float(clamped))) / 255.0 : clamped / 255.0;
221Result<PreparedAssetImport> normalImage(
const UnityProjectImportRequest&
request,
const std::string&
path,
223 bool flipGreen, NormalSource
source) {
224 const bool fromHeight =
source == NormalSource::Height;
225 const size_t channels = fromHeight ? 1 : 2;
227 identity.
packageId = materialId.
child(fromHeight ?
"main-height-normal" :
"main-authored-normal");
230 if (!image)
return image;
231 const auto definition =
image.value().manifest.assets.front().definition;
233 if (entry.path != definition)
continue;
237 const auto& bulk = cooked.value().bulk;
238 auto read32 = [&](std::size_t
p) {
239 return std::uint32_t(bulk[
p]) | (std::uint32_t(bulk[
p + 1]) << 8) | (std::uint32_t(bulk[
p + 2]) << 16) |
240 (std::uint32_t(bulk[
p + 3]) << 24);
243 const auto& metaBytes =
request.files.at(
path +
".meta");
244 const std::string meta(metaBytes.begin(), metaBytes.end());
245 if (mipmaps && importerScalar(meta,
"mipMapMode", 0) != 0)
248 const auto npot = importerScalar(meta,
"nPOTScale", 1);
249 if (npot < 0 || npot > 3 || npot != std::floor(npot))
throw Invalid{};
250 const auto targetWidth = normalExtent(
width,
int(npot)), targetHeight = normalExtent(
height,
int(npot));
251 std::vector<double> resized;
252 int resizeAlgorithm = 0;
253 if (targetWidth !=
width || targetHeight !=
height) {
254 const std::regex algorithms(R
"((?:^|\n)[ \t]*resizeAlgorithm: *([^\r\n]+))");
255 std::optional<int> selected;
256 for (
auto it = std::sregex_iterator(meta.begin(), meta.end(), algorithms); it != std::sregex_iterator();
260 const auto algorithm = numeric(
value.value());
261 if (algorithm != 0 && algorithm != 1)
throw Invalid{};
262 if (selected && *selected !=
int(algorithm))
265 "platform-specific normal resize algorithms require target-specific image variants",
path, {},
267 selected = int(algorithm);
269 resizeAlgorithm = selected.value_or(0);
273 path, {},
"asset.import"));
274 resized = normalResizeRgb(bulk,
width,
height, targetWidth, targetHeight, resizeAlgorithm);
281 const uint64_t
bytes = uint64_t(
w) *
h * 4;
288 if (!mipmaps || (
w == 1 &&
h == 1))
break;
289 w = std::max(
w / 2, 1u);
290 h = std::max(
h / 2, 1u);
296 path, {},
"asset.import"));
298 for (
size_t i = 0; i < size_t(
width) *
height; ++i) {
299 if (!resized.empty()) {
301 heights[i] = (resized[i * 3] + resized[i * 3 + 1] + resized[i * 3 + 2]) / 3;
303 heights[i * 2] = resized[i * 3];
304 heights[i * 2 + 1] = resized[i * 3 + 1];
309 heights[i] = (double(bulk[28 + i * 4]) + bulk[29 + i * 4] + bulk[30 + i * 4]) / 765.0;
311 heights[i * 2] = double(bulk[28 + i * 4]) / 255.0;
312 heights[i * 2 + 1] = double(bulk[29 + i * 4]) / 255.0;
315 std::vector<uint8_t> rgba;
316 rgba.reserve(
size_t(total));
320 auto sample = [&](int64_t
x, int64_t
y) {
323 return heights[size_t(
y) *
w + size_t(
x)];
325 const double factor = .04 * (double(
w) +
h) * (std::pow(10.0,
strength) - 1.0);
326 for (int64_t
y = 0;
y <
h; ++
y) {
327 for (int64_t
x = 0;
x <
w; ++
x) {
329 const auto at = (size_t(
y) *
w + size_t(
x)) * 2;
330 const auto green = flipGreen ? 1.0 - heights[
at + 1] : heights[
at + 1];
331 rgba.insert(rgba.end(), {uint8_t(std::lround(heights[
at] * 255)),
332 uint8_t(std::lround(green * 255)), 255, 255});
346 dy *= flipGreen ? -factor : factor;
348 rgba.insert(rgba.end(), {uint8_t(std::lround((dx / length * .5 + .5) * 255)),
349 uint8_t(std::lround((dy / length * .5 + .5) * 255)), 255, 255});
352 if (
level + 1 == levels)
break;
353 const uint32_t nextW = std::max(
w / 2, 1u), nextH = std::max(
h / 2, 1u);
354 std::vector<double>
next(
size_t(nextW) * nextH *
channels);
355 for (uint32_t
y = 0;
y < nextH; ++
y) {
356 for (uint32_t
x = 0;
x < nextW; ++
x) {
357 const double sx = (
x + .5) *
w / nextW - .5,
sy = (
y + .5) *
h / nextH - .5;
358 const auto ix = uint32_t(std::floor(
sx)), iy = uint32_t(std::floor(
sy));
359 const double fx =
sx - ix, fy =
sy - iy;
360 const auto x1 = std::min(ix + 1,
w - 1), y1 = std::min(iy + 1,
h - 1);
363 (heights[(size_t(iy) *
w + ix) *
channels +
c] * (1 - fx) +
364 heights[(size_t(iy) *
w + x1) *
channels +
c] * fx) *
366 (heights[(
size_t(y1) *
w + ix) *
channels +
c] * (1 - fx) +
367 heights[(size_t(y1) *
w + x1) *
channels +
c] * fx) *
371 heights = std::move(next);
375 auto parsed =
Value::fromJson(std::string(entry.bytes.begin(), entry.bytes.end()));
378 const auto oldBlob =
object.at(
"blob").asString();
379 const auto newBlob = definition.substr(0, definition.rfind(
'/') + 1) +
"normal.rgba8-mips";
380 object[
"encoding"] =
Value(
"rgba8-mips");
381 object[
"blob"] =
Value(newBlob);
382 object[
"mipCount"] =
Value(int64_t(levels));
385 object[
"usage"] =
Value(
"normal");
386 auto encoded = parsed.value().toJson();
388 auto result = std::move(image).takeValue();
390 if (
output.path == definition)
391 output.bytes.assign(encoded.value().begin(), encoded.value().end());
392 else if (
output.path == oldBlob) {
394 output.bytes = std::move(rgba);
397 auto& asset = result.manifest.assets.front();
399 detail::sha256({
reinterpret_cast<const uint8_t*
>(encoded.value().data()), encoded.value().size()});
400 asset.tags = {
"image",
"usage:normal"};
401 std::erase_if(result.entries, [](
const auto&
output) { return output.path ==
"reports/import.json"; });
403 {
path, fromHeight ?
"TextureImporter.HeightNormal" :
"TextureImporter.AuthoredNormal",
405 fromHeight ?
"height resampled and normals regenerated independently per mip"
406 :
"linear source RG resampled independently per mip before green flip and quantization"}};
408 {{
"fromHeight",
Value(fromHeight)},
409 {
"npotScale",
Value(int64_t(npot))},
410 {
"resizeAlgorithm",
Value(int64_t(resizeAlgorithm))},
412 {
"sobel",
Value(sobel)},
413 {
"flipGreen",
Value(flipGreen)},
414 {
"mipCount",
Value(int64_t(levels))}});
428 const auto shader = match(
text, R
"(m_Shader: *\{fileID: *4800000, guid: *([0-9a-fA-F]{32}), type: *3\})");
430 (*
shader !=
"7befaa6f41d00a6478d5f4af21d66518" && *
shader !=
"a933075b367f9b24981408633f72ff34" &&
431 *
shader !=
"6e6307b56f9201d40ad738f21cf03495" && *
shader !=
"d9a724745053dee46bf301b216cdd348"))
434 {},
"asset.import"));
435 auto unit = [](
double v) {
436 if (v < 0 || v > 1)
throw Invalid{};
439 const auto mode = scalar(
text,
"_RenderMode", 0), cull = scalar(
text,
"_RenderCull", 0),
440 clip = scalar(
text,
"_RenderClip", 1), coverage = scalar(
text,
"_RenderCoverage", 0),
441 specular = scalar(
text,
"_RenderSpecular", 1);
442 if ((mode != 0 && mode != 1) || cull < 0 || cull > 2 || cull != std::floor(cull) ||
443 (
clip != 0 &&
clip != 1) || (coverage != 0 && coverage != 1))
447 auto color = vector(
text,
"_MainColor", {1, 1, 1, 1});
448 auto second = vector(
text,
"_MainColorTwo", {1, 1, 1, 1});
449 const auto uv = vector(
text,
"_MainUVs", {1, 1, 0, 0});
450 for (
unsigned c = 0;
c < 4; ++
c)
454 const auto dual = scalar(
text,
"_MainColorMode", 0);
455 if (dual != 0 && dual != 1)
throw Invalid{};
456 const auto occlusion =
unit(scalar(
text,
"_MainOcclusionValue", 0)),
457 smoothness =
unit(scalar(
text,
"_MainSmoothnessValue", 0));
459 cutoff =
unit(scalar(
text,
"_AlphaClipValue", .5));
462 if (
float(
maximum) -
float(
minimum) + .0001f == 0)
throw Invalid{};
466 out.
manifest = std::move(manifest).takeValue();
471 {
"schemaVersion", 15},
472 {
"shadingModel",
"pbr"},
473 {
"surfaceMode", mode == 1 ?
"transparent"
474 :
clip == 1 ?
"masked"
476 {
"doubleSided", cull == 0},
477 {
"cullMode", cull == 0 ?
"none" : (cull == 1 ?
"back" :
"front")},
478 {
"alphaToCoverage", coverage == 1 &&
clip == 1},
482 {
"specularFactor",
unit(specular)},
483 {
"alphaCutoff", cutoff},
484 {
"albedoTextureStrength",
albedo}};
485 const auto extrasLayer = scalar(
text,
"_LayerExtrasValue", 0);
486 if (extrasLayer < 0 || extrasLayer > 8 || extrasLayer != std::floor(extrasLayer))
throw Invalid{};
487 const auto colorsLayer = scalar(
text,
"_LayerColorsValue", 0);
488 if (colorsLayer < 0 || colorsLayer > 8 || colorsLayer != std::floor(colorsLayer))
throw Invalid{};
489 const auto motionLayer = scalar(
text,
"_LayerMotionValue", 0);
490 if (motionLayer < 0 || motionLayer > 8 || motionLayer != std::floor(motionLayer))
throw Invalid{};
491 const auto vertexLayer = scalar(
text,
"_LayerVertexValue", 0);
492 if (vertexLayer < 0 || vertexLayer > 8 || vertexLayer != std::floor(vertexLayer))
throw Invalid{};
493 auto binary = [&](
const char*
name,
double fallback) {
495 if (
value != 0 &&
value != 1)
throw Invalid{};
499 {
"variation",
unit(scalar(
text,
"_AlphaVariationValue", .5))},
500 {
"detailFade", binary(
"_DetailFadeMode", 0) == 1},
501 {
"glancing",
unit(scalar(
text,
"_FadeGlancingValue", 0))},
502 {
"camera",
unit(scalar(
text,
"_FadeCameraValue", 1))},
503 {
"constant",
unit(scalar(
text,
"_FadeConstantValue", 0))}};
504 const auto emissiveMode = binary(
"_EmissiveMode", 0);
505 if (emissiveMode == 1) {
506 const auto emissiveColor = vector(
text,
"_EmissiveColor", {0, 0, 0, 0});
507 for (
size_t c = 0;
c < 3; ++
c)
508 if (emissiveColor[
c] < 0)
throw Invalid{};
509 const auto intensityMode = binary(
"_EmissiveIntensityMode", 0);
510 const auto authoredPower = scalar(
text,
"_EmissiveIntensityValue", 1);
511 const auto fallbackPower = intensityMode == 0 ? authoredPower : .125 * std::pow(2., authoredPower);
512 const auto intensity = scalar(
text,
"_emissive_intensity_value", fallbackPower);
513 if (intensity < 0 || intensity > 1000000)
throw Invalid{};
514 const auto emissionMinimum =
unit(scalar(
text,
"_EmissiveTexMinValue", 0));
515 const auto emissionMaximum =
unit(scalar(
text,
"_EmissiveTexMaxValue", 1));
516 if (
float(emissionMaximum) -
float(emissionMinimum) + .0001f == 0)
throw Invalid{};
517 definition[
"emissive"] =
Value::Array{emissiveColor[0], emissiveColor[1], emissiveColor[2]};
518 definition[
"emissiveStrength"] = intensity;
519 definition[
"vegetationEmission"] =
Value::Object{{
"minimum", emissionMinimum},
520 {
"maximum", emissionMaximum},
521 {
"phase",
unit(scalar(
text,
"_EmissivePhaseValue", 1))},
522 {
"global",
unit(scalar(
text,
"_GlobalEmissive", 1))}};
524 const auto gradientOne = vector(
text,
"_GradientColorOne", {1, 1, 1, 1});
525 const auto gradientTwo = vector(
text,
"_GradientColorTwo", {1, 1, 1, 1});
526 for (
size_t c = 0;
c < 3; ++
c)
527 if (gradientOne[
c] < 0 || gradientTwo[
c] < 0)
throw Invalid{};
528 const auto gradientMinimum =
unit(scalar(
text,
"_GradientMinValue", 0));
529 const auto gradientMaximum =
unit(scalar(
text,
"_GradientMaxValue", 1));
530 if (
float(gradientMaximum) -
float(gradientMinimum) + .0001f == 0)
throw Invalid{};
531 definition[
"vegetationGradient"] =
533 {
"colorTwo",
Value::Array{gradientTwo[0], gradientTwo[1], gradientTwo[2]}},
534 {
"minimum", gradientMinimum},
535 {
"maximum", gradientMaximum}};
536 definition[
"vegetationFields"] =
538 {
"colorsUsePivotPosition", binary(
"_ColorsPositionMode", 0) == 1},
539 {
"extrasLayer", int64_t(extrasLayer)},
540 {
"extrasUsePivotPosition", binary(
"_ExtrasPositionMode", 0) == 1},
541 {
"motionLayer", int64_t(motionLayer)},
542 {
"vertexLayer", int64_t(vertexLayer)},
543 {
"globalSize",
unit(scalar(
text,
"_GlobalSize", 1))},
544 {
"sizeFadeStart", scalar(
text,
"_SizeFadeStartValue", 0)},
545 {
"sizeFadeEnd", scalar(
text,
"_SizeFadeEndValue", 100)}};
546 auto nonnegative = [&](
const char*
name,
double fallback) {
548 if (value < 0 || value > 1000000)
throw Invalid{};
551 definition[
"vegetationMotion"] =
553 {
"rigidity",
unit(scalar(
text,
"_MotionPosition_10", .5))},
554 {
"facing",
unit(scalar(
text,
"_MotionFacingValue", .5))},
555 {
"bending", nonnegative(
"_MotionAmplitude_10", .2)},
556 {
"bendingSpeed", nonnegative(
"_MotionSpeed_10", 2)},
557 {
"bendingScale", nonnegative(
"_MotionScale_10", 1)},
558 {
"bendingVariation", nonnegative(
"_MotionVariation_10", 0)},
559 {
"branch", nonnegative(
"_MotionAmplitude_20", .2)},
560 {
"rolling", nonnegative(
"_MotionAmplitude_22", .2)},
561 {
"branchSpeed", nonnegative(
"_MotionSpeed_20", 6)},
562 {
"branchScale", nonnegative(
"_MotionScale_20", 3)},
563 {
"branchVariation", nonnegative(
"_MotionVariation_20", 0)},
564 {
"flutter", nonnegative(
"_MotionAmplitude_32", .2)},
565 {
"flutterSpeed", nonnegative(
"_MotionSpeed_32", 20)},
566 {
"flutterScale", nonnegative(
"_MotionScale_32", 10)},
567 {
"flutterVariation", nonnegative(
"_MotionVariation_32", 0)},
568 {
"interaction", nonnegative(
"_InteractionAmplitude", 1)},
569 {
"interactionMask",
unit(scalar(
text,
"_InteractionMaskValue", 1))},
570 {
"perspectivePush", nonnegative(
"_PerspectivePushValue", 0)},
571 {
"perspectiveNoise", nonnegative(
"_PerspectiveNoiseValue", 0)},
572 {
"perspectiveAngle", nonnegative(
"_PerspectiveAngleValue", 1)}};
573 const auto occlusionColor = vector(
text,
"_VertexOcclusionColor", {1, 1, 1, .5019608});
574 for (
size_t c = 0;
c < 3; ++
c)
575 if (occlusionColor[
c] < 0)
throw Invalid{};
576 const auto colorsOcclusionMode = scalar(
text,
"_VertexOcclusionColorsMode", 0);
577 if (colorsOcclusionMode != 0 && colorsOcclusionMode != 1)
throw Invalid{};
578 const auto colorsOcclusionMinimum =
unit(scalar(
text,
"_VertexOcclusionMinValue", 0));
579 const auto colorsOcclusionMaximum =
unit(scalar(
text,
"_VertexOcclusionMaxValue", 1));
580 if (
float(colorsOcclusionMaximum) -
float(colorsOcclusionMinimum) + .0001f == 0)
throw Invalid{};
581 const auto colorsIntensity = scalar(
text,
"_ColorsIntensityValue", 1);
582 if (colorsIntensity < 0 || colorsIntensity > 2)
throw Invalid{};
583 const auto backfaceNormalMode = scalar(
text,
"_RenderNormals", 0);
584 if (backfaceNormalMode < 0 || backfaceNormalMode > 2 || backfaceNormalMode != std::floor(backfaceNormalMode))
587 {
"sourceFamily",
"tve-12"},
588 {
"overlay",
unit(scalar(
text,
"_GlobalOverlay", 1))},
589 {
"wetness",
unit(scalar(
text,
"_GlobalWetness", 1))},
590 {
"overlayVariation",
unit(scalar(
text,
"_OverlayVariationValue", .5))},
591 {
"overlayProjection",
unit(scalar(
text,
"_OverlayProjectionValue", .5))},
592 {
"vertexOcclusionAlpha",
unit(occlusionColor[3])},
593 {
"vertexOcclusionColor",
Value::Array{occlusionColor[0], occlusionColor[1], occlusionColor[2]}},
594 {
"invertVertexOcclusion", scalar(
text,
"_VertexOcclusionOverlayMode", 0) == 1},
595 {
"colors",
unit(scalar(
text,
"_GlobalColors", 1))},
596 {
"colorsIntensity", colorsIntensity},
597 {
"colorsMask",
unit(scalar(
text,
"_ColorsMaskValue", 1))},
598 {
"colorsVariation",
unit(scalar(
text,
"_ColorsVariationValue", .5))},
599 {
"vertexOcclusionMinimum", colorsOcclusionMinimum},
600 {
"vertexOcclusionMaximum", colorsOcclusionMaximum},
601 {
"invertVertexOcclusionColors", colorsOcclusionMode == 1},
602 {
"backfaceNormalMode", int64_t(backfaceNormalMode)}};
603 const auto detailMode = scalar(
text,
"_DetailMode", 0);
604 if (detailMode != 0 && detailMode != 1)
throw Invalid{};
605 if (detailMode == 1) {
606 const auto detailUv = vector(
text,
"_SecondUVs", {1, 1, 0, 0});
607 const auto detailColor = vector(
text,
"_SecondColor", {1, 1, 1, 1});
608 const auto detailColorTwo = vector(
text,
"_SecondColorTwo", {1, 1, 1, 1});
609 const auto uvMode = scalar(
text,
"_SecondUVsMode", 0);
610 if (uvMode < 0 || uvMode > 2 || uvMode != std::floor(uvMode))
throw Invalid{};
611 auto signedNormal = scalar(
text,
"_SecondNormalValue", 1);
612 if (signedNormal < -8 || signedNormal > 8 || detailUv[0] == 0 || detailUv[1] == 0)
throw Invalid{};
614 {
"value",
unit(scalar(
text,
"_DetailValue", 1))},
615 {
"uvMode", int64_t(uvMode)},
616 {
"inverseUvScale", binary(
"_SecondUVsScaleMode", 0) == 1},
618 {
"uvOffset",
Value::Array{detailUv[2], 1.0 - detailUv[1] - detailUv[3]}},
619 {
"color",
Value::Array{detailColor[0], detailColor[1], detailColor[2], detailColor[3]}},
620 {
"colorTwo",
Value::Array{detailColorTwo[0], detailColorTwo[1], detailColorTwo[2], detailColorTwo[3]}},
621 {
"colorMode", int64_t(binary(
"_SecondColorMode", 0))},
622 {
"albedoValue",
unit(scalar(
text,
"_SecondAlbedoValue", 1))},
623 {
"normalValue", signedNormal},
624 {
"normalBlendValue",
unit(scalar(
text,
"_DetailNormalValue", 1))},
625 {
"metallicValue",
unit(scalar(
text,
"_SecondMetallicValue", 0))},
626 {
"occlusionValue",
unit(scalar(
text,
"_SecondOcclusionValue", 1))},
627 {
"smoothnessValue",
unit(scalar(
text,
"_SecondSmoothnessValue", 1))},
628 {
"blendMode", int64_t(binary(
"_DetailBlendMode", 0))},
629 {
"alphaMode", int64_t(binary(
"_DetailAlphaMode", 1))},
630 {
"maskMode", int64_t(binary(
"_DetailMaskMode", 0))},
631 {
"meshMode", int64_t(binary(
"_DetailMeshMode", 0))},
632 {
"blendMinimum",
unit(scalar(
text,
"_DetailBlendMinValue", 0))},
633 {
"blendMaximum",
unit(scalar(
text,
"_DetailBlendMaxValue", 1))},
634 {
"maskMinimum",
unit(scalar(
text,
"_DetailMaskMinValue", 0))},
635 {
"maskMaximum",
unit(scalar(
text,
"_DetailMaskMaxValue", 1))},
636 {
"meshMinimum",
unit(scalar(
text,
"_DetailMeshMinValue", 0))},
637 {
"meshMaximum",
unit(scalar(
text,
"_DetailMeshMaxValue", 1))}};
639 auto bindWithUv = [&](
const std::string& role,
const AssetRef&
image,
const std::array<double, 4>& transform) {
640 definition[role] =
image.format();
641 definition[role +
"Transform"] =
643 {
"offset",
Value::Array{transform[2], 1.0 - transform[1] - transform[3]}}};
647 auto bind = [&](
const std::string& role,
const AssetRef&
image) { bindWithUv(role,
image,
uv); };
648 if (
auto guid = texture(
text,
"_MainAlbedoTex")) {
653 bind(
"baseColorTexture",
image.value());
656 definition[
"baseColorTextureSampler"] = std::move(
sampler).takeValue();
658 if (emissiveMode == 1) {
659 auto guid = texture(
text,
"_EmissiveTex");
660 if (!guid)
throw Invalid{};
665 const auto emissiveUv = vector(
text,
"_EmissiveUVs", {1, 1, 0, 0});
666 if (emissiveUv[0] == 0 || emissiveUv[1] == 0)
throw Invalid{};
667 bindWithUv(
"emissiveTexture",
image.value(), emissiveUv);
670 definition[
"emissiveTextureSampler"] = std::move(
sampler).takeValue();
672 if (
auto guid = texture(
text,
"_MainNormalTex")) {
676 const std::string meta(
bytes.begin(),
bytes.end());
677 const auto normalType = importerScalar(meta,
"textureType", 0);
678 const auto fromHeight = importerScalar(meta,
"convertToNormalMap", 0);
679 const auto flipGreen = importerScalar(meta,
"flipGreenChannel", 0);
680 if (normalType == 1 && (fromHeight == 0 || fromHeight == 1) && (flipGreen == 0 || flipGreen == 1)) {
683 definition[
"normalTextureSampler"] = std::move(
sampler).takeValue();
684 const auto strength = scalar(
text,
"_MainNormalValue", 1);
685 if (strength < -8 || strength > 8)
throw Invalid{};
689 const auto scale = importerScalar(meta,
"heightScale", .25);
690 const auto filter = importerScalar(meta,
"normalMapFilter", 0);
691 if (scale < 0 || scale > 1 || (
filter != 0 &&
filter != 1))
throw Invalid{};
693 importerScalar(meta,
"enableMipMap", 1) == 1, flipGreen == 1,
694 fromHeight == 1 ? NormalSource::Height : NormalSource::Authored);
695 if (!generated)
return generated;
697 for (
auto&
a : generated.value().manifest.assets) out.
manifest.
assets.push_back(std::move(
a));
698 for (
auto& e : generated.value().entries)
699 if (e.path.starts_with(
"assets/")) out.
entries.push_back(std::move(e));
700 for (
auto& finding : generated.value().findings) out.
findings.push_back(std::move(finding));
702 bind(
"normalTexture",
image.value());
703 definition[
"normalEncoding"] =
"tve-rg";
704 definition[
"normalScale"] =
strength;
707 "authored or height-generated RG linked as linear data; signed strength and fixed-Z "
708 "reconstruction run after filtering"});
712 "normal source retained; invalid settings or non-normal importer encoding requires conversion"});
715 if (*
shader ==
"a933075b367f9b24981408633f72ff34" || *
shader ==
"d9a724745053dee46bf301b216cdd348") {
716 auto tint = vector(
text,
"_SubsurfaceColor", {1, 1, 1, 1});
717 for (
size_t c = 0;
c < 3; ++
c) {
718 if (
tint[
c] < 0)
throw Invalid{};
720 const auto power = scalar(
text,
"_SubsurfaceScatteringValue", 2);
721 const auto angle = scalar(
text,
"_SubsurfaceAngleValue", 8);
722 if (power < 0 || power > 16 || angle < 1 || angle > 16)
throw Invalid{};
725 {
"intensity",
unit(scalar(
text,
"_SubsurfaceValue", 1))},
727 {
"scattering",
angle},
728 {
"normalDistortion",
unit(scalar(
text,
"_SubsurfaceNormalValue", 0))},
729 {
"direct",
unit(scalar(
text,
"_SubsurfaceDirectValue", 1))},
730 {
"ambient",
unit(scalar(
text,
"_SubsurfaceAmbientValue", .2))},
731 {
"shadow",
unit(scalar(
text,
"_SubsurfaceShadowValue", 1))},
733 *
shader ==
"a933075b367f9b24981408633f72ff34" ?
unit(scalar(
text,
"_SubsurfaceMaskValue", 1)) : 0},
737 "visible subsurface controls mapped to per-light translucency; field wetness is "
738 "composed by the vegetation runtime"});
740 if (*
shader ==
"a933075b367f9b24981408633f72ff34" || *
shader ==
"7befaa6f41d00a6478d5f4af21d66518") {
741 auto highlight = vector(
text,
"_MotionHighlightColor", {0, 0, 0, 0});
742 for (
size_t c = 0;
c < 3; ++
c) {
748 "Plant motion highlight RGB retained; runtime requires evaluated per-vertex highlight stream"});
750 if (detailMode == 1) {
751 for (
const auto& [property, role] :
752 std::array<std::pair<const char*, const char*>, 3>{{{
"_SecondAlbedoTex",
"detailAlbedoTexture"},
753 {
"_SecondNormalTex",
"detailNormalTexture"},
754 {
"_SecondMaskTex",
"detailMaskTexture"}}}) {
755 auto guid = texture(
text, property);
761 definition[role] =
image.value().format();
764 definition[std::string(role) +
"Sampler"] = std::move(
sampler).takeValue();
774 auto orm = ormImage(
request,
text,
id, occlusion, smoothness);
775 if (!orm)
return orm;
776 const auto mask = orm.value().manifest.assets.front().asset;
777 bind(
"metallicRoughnessTexture",
mask);
778 bind(
"occlusionTexture",
mask);
779 if (
auto guid = texture(
text,
"_MainMaskTex")) {
784 definition[
"metallicRoughnessTextureSampler"] =
sampler.value();
785 definition[
"occlusionTextureSampler"] = std::move(
sampler).takeValue();
787 for (
auto&
a : orm.value().manifest.assets) out.
manifest.
assets.push_back(std::move(
a));
788 for (
auto& e : orm.value().entries)
789 if (e.path.starts_with(
"assets/")) out.
entries.push_back(std::move(e));
794 const auto path =
"assets/" +
id.format() +
"/asset.json";
797 std::vector<std::uint8_t> encoded(json.value().begin(), json.value().end());
803 {
"material",
"source:unity",
"vegetation"}});
808 "TVE 12.6.0 primary RGB, albedo, UV transform, AO/smoothness and RGB mask converted"});
810 "specular, facing normal, culling and alpha-to-coverage state translated; "
811 "Direct/Ambient/Shadow and generated noise/rim declarations are inactive in the "
812 "inspected 12.6 shader calculations"});
814 }
catch (
const Invalid&) {
std::map< std::string, std::vector< Key >, std::less<> > channels
std::unordered_map< std::string, QuestRuntime > entries
Safe source-image conversion to runtime EVIMG payloads.
graphics::Texture * albedo
const GltfImportRequest & request
vk::UniqueSampler sampler
wgpu::PopErrorScopeStatus status
std::array< float, 3 > scale
TerrainThermalSettings settings
Direct Unity text-serialization adapter for TerrainData and Prefab assets.
const UnitySourceAsset & source
const AssetImportLimits & limits
Stable asset identity backed by PersistentId.
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
The canonical owning dynamic value used by data-facing protocols.
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
std::vector< Value > Array
Id128 child(std::string_view role) const noexcept
Derives a deterministic child UUID for a hierarchical identity.
Result< std::vector< std::uint8_t > > encodeSourcePng(std::uint32_t width, std::uint32_t height, std::span< const std::uint8_t > pixels, std::uint64_t maximumBytes)
Encode top-down RGBA8 source pixels without transfer conversion.
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< asset::EvaManifest > baseManifest(const ImportPackageIdentity &package, std::string_view importer)
Base manifest.
Result< AssetRef > assetRef(PersistentId id)
Asset ref.
std::string foldAscii(std::string value)
Fold ascii.
Result< std::string > metaScalar(std::span< const std::uint8_t > bytes, std::string_view key, std::string_view path)
Read an unindented scalar from .meta; rejects duplicate keys and binary metadata.
Result< PreparedAssetImport > prepareUnityVegetationMaterial(const UnityProjectImportRequest &request, const UnitySourceAsset &source)
Convert admitted TVE primary material data; remaining source features are reported explicitly.
EVENGINE_API_PLATFORM Result< PreparedAssetImport > prepareImageImport(const ImageImportRequest &request)
Decode image metadata and prepare one canonical eve.image source asset.
Result< CookedCanonicalImage > cookCanonicalImageRgba8(std::span< const std::uint8_t > definition, std::span< const std::uint8_t > encodedSource, std::uint64_t maximumDecodedBytes)
Decode image/2 base images or image/3 explicit linear RGBA8 mip chains into bounded EVIMG.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
double sample(const Heightmap &map, double u, double v)
Sample.
StrongUuid< detail::PersistentIdTag > PersistentId
Stable instance identity for persistence, networking and process boundaries.
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
std::vector< EvaAssetEntry > assets
std::vector< EvaDependency > dependencies
std::map< std::string, AssetRef > entrypoints
std::uint64_t maximumDecodedBytes
std::uint64_t maximumSourceBytes
ImportPackageIdentity package
Owning, validated source archive candidate not yet published to disk/database.
std::vector< ImportFinding > findings
std::vector< asset::EvaArchiveEntry > entries
std::vector< ImportSourceMapping > sourceMappings
asset::EvaManifest manifest
In-memory Unity project slice; keys are normalized project-relative paths.
Immutable-by-convention owning source index entry; no borrowed source addresses.