13#include <unordered_map>
19Result<ProceduralDecalRecipe> recipeFailure(std::string
message) {
24Result<ProceduralDecalBake> bakeFailure(std::string
message) {
29std::optional<std::string_view> attribute(std::string_view
element, std::string_view
name) {
30 const std::string needle = std::string(
name) +
"=\"";
32 if (
begin == std::string_view::npos)
return std::nullopt;
33 const auto valueBegin =
begin + needle.size();
34 const auto valueEnd =
element.find(
'"', valueBegin);
35 if (valueEnd == std::string_view::npos)
return std::nullopt;
36 return element.substr(valueBegin, valueEnd - valueBegin);
39std::optional<float>
number(std::string_view
text) {
40 if (
text.empty())
return std::nullopt;
41 if (
text.find_first_of(
" \t\n\r\f\v") != std::string_view::npos)
return std::nullopt;
42 const std::string copy(
text);
45 const float value = std::strtof(copy.c_str(), &
end);
46 if (
end != copy.c_str() + copy.size() || errno == ERANGE || !std::isfinite(
value))
return std::nullopt;
52 const auto parsed = std::from_chars(
text.data(),
text.data() +
text.size(),
value);
53 if (parsed.ec != std::errc{} || parsed.ptr !=
text.data() +
text.size())
return std::nullopt;
57std::optional<std::array<float, 3>> color3(std::string_view
text) {
58 std::array<float, 3>
color{};
59 for (std::size_t channel = 0; channel <
color.size(); ++channel) {
60 const auto comma =
text.find(
',');
61 const auto token = comma == std::string_view::npos ?
text :
text.substr(0, comma);
63 if (!
value)
return std::nullopt;
65 if (channel + 1 <
color.size()) {
66 if (comma == std::string_view::npos)
return std::nullopt;
67 text.remove_prefix(comma + 1);
68 }
else if (comma != std::string_view::npos) {
81 return families[std::size_t(magnitude % std::size(families))];
84float clamp01(
float value) {
return std::clamp(
value, 0.f, 1.f); }
87 const float t = clamp01((
value - edge0) / (edge1 - edge0));
88 return t *
t * (3.f - 2.f *
t);
99float random01(std::uint32_t
x, std::uint32_t
y, std::uint32_t
seed) {
100 return float(
hash(
x * 0x9e3779b9u ^
y * 0x85ebca6bu ^
seed)) / float(std::numeric_limits<std::uint32_t>::max());
103float valueNoise(
float x,
float y, std::uint32_t
seed) {
104 const int ix =
static_cast<int>(std::floor(
x));
105 const int iy =
static_cast<int>(std::floor(
y));
106 const float fx =
x - float(ix);
107 const float fy =
y - float(iy);
108 const float sx = fx * fx * (3.f - 2.f * fx);
109 const float sy = fy * fy * (3.f - 2.f * fy);
111 return random01(
static_cast<std::uint32_t
>(
px),
static_cast<std::uint32_t
>(
py),
seed);
114 const float b = std::lerp(
sample(ix, iy + 1),
sample(ix + 1, iy + 1),
sx);
115 return std::lerp(
a,
b,
sy);
118float fractal(
float x,
float y, std::uint32_t
seed) {
121 for (
int octave = 0; octave < 4; ++octave) {
127 return value / 1.03125f;
131 std::uint32_t
seed) {
132 const float x = (
u - 0.5f) *
scale;
133 const float y = (
v - 0.5f) *
scale;
134 const float radius = std::sqrt(
x *
x +
y *
y);
135 const float noise = fractal(
x + 17.1f,
y - 9.7f,
seed);
136 const int family = sourcePatternId >= 0 ? sourcePatternId % 12 :
static_cast<int>(
pattern);
139 const float cells = valueNoise(
x * 2.1f,
y * 2.1f,
seed + 71u);
140 return clamp01((
cells - 0.42f) * 2.8f) * clamp01(1.15f -
radius * 0.55f);
143 const float ridge = 1.f - std::abs(noise * 2.f - 1.f);
144 return clamp01((ridge - 0.76f) * 5.2f) * clamp01(1.2f -
radius * 0.45f);
147 const float streak = fractal(
x * 0.45f,
y * 3.8f,
seed + 191u);
148 return clamp01((streak - 0.38f) * 2.1f) * clamp01(1.1f - std::abs(
x) * 0.22f);
151 const float warpedRadius =
radius * (0.78f + 0.42f * noise);
152 return 1.f -
smoothStep(0.62f, 1.05f, warpedRadius);
154 case 4:
return clamp01((noise - 0.28f) * 1.45f) * clamp01(1.25f -
radius * 0.38f);
156 const float islands = fractal(
x * 1.45f + noise * 0.55f,
y * 1.45f - noise * 0.35f,
158 return clamp01((islands - 0.46f) * 2.65f) * clamp01(1.14f -
radius * 0.48f);
161 const float scratches = fractal((
x +
y) * 4.8f, (
y -
x) * 0.38f,
seed + 313u);
162 return clamp01((scratches - 0.56f) * 3.2f) * clamp01(1.18f -
radius * 0.42f);
165 const float blobs = valueNoise(
x * 1.35f,
y * 1.35f,
seed + 419u);
166 const float drops = valueNoise(
x * 4.7f,
y * 4.7f,
seed + 431u);
167 return clamp01((std::max(blobs, drops * 0.82f) - 0.48f) * 2.7f) *
168 clamp01(1.15f -
radius * 0.46f);
171 const float cells = valueNoise(
x * 2.6f,
y * 2.6f,
seed + 557u);
172 const float edge = 1.f - std::abs(
cells * 2.f - 1.f);
173 return clamp01((
edge - 0.68f) * 4.1f) * clamp01(1.2f -
radius * 0.44f);
176 const float smear = fractal(
x * 2.8f + noise,
y * 0.42f,
seed + 631u);
177 return clamp01((smear - 0.4f) * 2.25f) * clamp01(1.1f -
radius * 0.48f);
180 const float drops = valueNoise(
x * 5.3f,
y * 5.3f,
seed + 743u);
181 return clamp01((drops - 0.63f) * 4.4f) * clamp01(1.16f -
radius * 0.4f);
184 const float coarse = fractal(
x * 0.72f,
y * 0.72f,
seed + 829u);
185 const float fine = fractal(
x * 3.1f,
y * 3.1f,
seed + 853u);
186 return clamp01((coarse * 0.7f + fine * 0.3f - 0.38f) * 2.f) *
187 clamp01(1.22f -
radius * 0.4f);
193std::vector<float> layerMask(
const ProceduralDecalRecipe& recipe,
const ProceduralDecalLayer&
layer) {
194 const std::size_t
count = std::size_t(recipe.width) * std::size_t(recipe.height);
197 const float c = std::cos(
layer.rotation);
198 const float s = std::sin(
layer.rotation);
199 for (
int y = 0;
y < recipe.height; ++
y) {
200 for (
int x = 0;
x < recipe.width; ++
x) {
201 const float u = (float(
x) + 0.5f) /
float(recipe.width) - 0.5f;
202 const float v = (float(
y) + 0.5f) /
float(recipe.height) - 0.5f;
203 const float ru =
c *
u -
s *
v + 0.5f;
204 const float rv =
s *
u +
c *
v + 0.5f;
206 recipe.seed ^
layer.seed);
209 mask[std::size_t(
y) * std::size_t(recipe.width) + std::size_t(
x)] = clamp01(
value *
layer.amount);
212 const int radius = std::clamp(
static_cast<int>(std::round(
layer.blur * 12.f)), 0, 12);
214 std::vector<float> horizontal(
count, 0.f);
215 std::vector<float> result(
count, 0.f);
216 for (
int y = 0;
y < recipe.height; ++
y) {
217 for (
int x = 0;
x < recipe.width; ++
x) {
220 sum +=
mask[std::size_t(
y) * std::size_t(recipe.width) +
221 std::size_t(std::clamp(
x +
dx, 0, recipe.width - 1))];
222 horizontal[std::size_t(
y) * std::size_t(recipe.width) + std::size_t(
x)] = sum / float(
radius * 2 + 1);
225 for (
int y = 0;
y < recipe.height; ++
y) {
226 for (
int x = 0;
x < recipe.width; ++
x) {
229 sum += horizontal[std::size_t(std::clamp(
y +
dy, 0, recipe.height - 1)) * std::size_t(recipe.width) +
231 result[std::size_t(
y) * std::size_t(recipe.width) + std::size_t(
x)] = sum / float(
radius * 2 + 1);
237std::uint8_t byte(
float value) {
return static_cast<std::uint8_t
>(std::round(clamp01(
value) * 255.f)); }
239std::array<float, 3> rotateHue(
const std::array<float, 3>&
color,
float angle) {
240 const float c = std::cos(
angle);
241 const float s = std::sin(
angle);
243 clamp01(
color[0] * (0.299f + 0.701f *
c + 0.168f *
s) +
244 color[1] * (0.587f - 0.587f *
c + 0.330f *
s) +
245 color[2] * (0.114f - 0.114f *
c - 0.497f *
s)),
246 clamp01(
color[0] * (0.299f - 0.299f *
c - 0.328f *
s) +
247 color[1] * (0.587f + 0.413f *
c + 0.035f *
s) +
248 color[2] * (0.114f - 0.114f *
c + 0.292f *
s)),
249 clamp01(
color[0] * (0.299f - 0.300f *
c + 1.250f *
s) +
250 color[1] * (0.587f - 0.588f *
c - 1.050f *
s) +
251 color[2] * (0.114f + 0.886f *
c - 0.203f *
s)),
255bool finiteLayer(
const ProceduralDecalLayer&
layer) {
261 return std::all_of(std::begin(
values), std::end(
values), [](
float value) {
return std::isfinite(
value); });
264bool validLayer(
const ProceduralDecalLayer&
layer) {
268 return finiteLayer(
layer) && inRange(
layer.amount, 0.f, 1.f) &&
269 inRange(
layer.scale, 0.001f, 4096.f) && std::abs(
layer.rotation) <= 1000000.f &&
270 inRange(
layer.blur, 0.f, 1.f) && inRange(
layer.contrast, 0.f, 64.f) &&
271 std::all_of(
layer.color.begin(),
layer.color.end(),
272 [&](
float value) { return inRange(value, 0.f, 1.f); }) &&
273 inRange(
layer.colorVariation, 0.f, 1.f) && inRange(
layer.hueVariation, 0.f, 1.f) &&
274 inRange(
layer.roughness, 0.f, 1.f) && inRange(
layer.roughnessVariation, 0.f, 1.f) &&
275 inRange(
layer.metallic, 0.f, 1.f) && inRange(
layer.emissive, 0.f, 1.f) &&
276 inRange(
layer.normalStrength, 0.f, 64.f) && inRange(
layer.normalSoftness, 0.005f, 1.f) &&
277 inRange(
layer.normalTrim, 0.f, 1.f) && inRange(
layer.normalThickness, 0.1f, 16.f) &&
278 layer.normalStyle >= 0 &&
layer.normalStyle <= 16 && inRange(
layer.height, 0.f, 1.f);
283 ProceduralDecalLayer
value;
301 if (
name ==
"blood-wet") {
304 }
else if (
name ==
"blood-dried") {
307 }
else if (
name ==
"damage") {
311 }
else if (
name ==
"dirt") {
314 }
else if (
name ==
"rust") {
317 }
else if (
name ==
"puddle") {
320 }
else if (
name ==
"paint") {
323 }
else if (
name ==
"moss" ||
name ==
"mold" ||
name ==
"lichen") {
324 const std::array<float, 3> base =
325 name ==
"mold" ? std::array<float, 3>{0.08f, 0.11f, 0.075f} : std::array<float, 3>{0.12f, 0.25f, 0.06f};
329 return recipeFailure(
"Unknown procedural decal preset: " + std::string(
name));
337 constexpr std::size_t maxPresetBytes = 1024u * 1024u;
338 if (xml.empty())
return recipeFailure(
"Substance preset document is empty");
339 if (xml.size() > maxPresetBytes)
return recipeFailure(
"Substance preset document exceeds the 1 MiB limit");
340 if (xml.find(
"<sbspresets") == std::string_view::npos || xml.find(
"<sbspreset") == std::string_view::npos)
341 return recipeFailure(
"Input is not a Substance preset document");
343 std::unordered_map<std::string, std::string>
inputs;
345 while ((
cursor = xml.find(
"<presetinput",
cursor)) != std::string_view::npos) {
347 if (
end == std::string_view::npos)
return recipeFailure(
"Unterminated presetinput element");
349 const auto id = attribute(
element,
"identifier");
351 if (!
id || !
value)
return recipeFailure(
"presetinput requires identifier and value attributes");
352 inputs.insert_or_assign(std::string(*
id), std::string(*
value));
355 if (
inputs.empty())
return recipeFailure(
"Substance preset contains no inputs");
358 bool malformedKnownInput =
false;
359 const auto readFloat = [&](std::string_view
name) -> std::optional<float> {
363 malformedKnownInput = malformedKnownInput || !
value.has_value();
366 const auto readInt = [&](std::string_view
name) -> std::optional<int> {
370 malformedKnownInput = malformedKnownInput || !
value.has_value();
374 const std::string
key(prefix);
376 if (
const auto value = readInt(
key +
"pattern")) {
380 if (
const auto value = readFloat(
key +
"tweak"))
target.scale = 1.f + clamp01(*
value) * 11.f;
381 if (
const auto value = readFloat(
key +
"tile"))
target.scale *= std::clamp(*
value, 0.25f, 8.f);
383 if (
const auto value = readFloat(
key +
"blur"))
target.blur = std::max(0.f, *
value) * 0.15f;
384 if (
const auto value = readFloat(
key +
"contrast"))
target.contrast = 0.5f + std::max(0.f, *
value) * 2.f;
385 const auto colorIt =
inputs.find(
key +
"color");
386 if (colorIt !=
inputs.end()) {
387 auto value = color3(colorIt->second);
388 if (!
value)
return false;
391 if (
const auto value = readFloat(
key +
"color_value_variation"))
393 if (
const auto value = readFloat(
key +
"color_hue_variation"))
target.hueVariation = clamp01(*
value);
395 if (
const auto value = readFloat(
key +
"rough_variation"))
399 if (
const auto value = readFloat(
key +
"normal"))
target.normalStrength = std::abs(*
value);
400 if (
const auto value = readFloat(
key +
"normal_softness"))
401 target.normalSoftness = std::clamp(*
value, 0.005f, 1.f);
402 if (
const auto value = readFloat(
key +
"normal_trim"))
target.normalTrim = clamp01(*
value);
403 if (
const auto value = readFloat(
key +
"normal_thickness"))
404 target.normalThickness = std::clamp(*
value, 0.1f, 16.f);
411 if (
const auto seed = readInt(
"$randomseed")) recipe.
seed =
static_cast<std::uint32_t
>(*seed);
412 const auto outputSize =
inputs.find(
"$outputsize");
413 if (outputSize !=
inputs.end()) {
414 const auto comma = outputSize->second.find(
',');
415 if (comma == std::string::npos)
return recipeFailure(
"$outputsize must contain width,height exponents");
416 const auto x = integer(std::string_view(outputSize->second).substr(0, comma));
417 const auto y = integer(std::string_view(outputSize->second).substr(comma + 1));
418 if (!
x || !
y || *
x < 0 || *y < 0 || *x > 12 || *
y > 12)
419 return recipeFailure(
"$outputsize exponents must be in [0, 12]");
423 if (!applyLayer(
"a_", recipe.
layerA) || !applyLayer(
"b_", recipe.
layerB))
424 return recipeFailure(
"Substance preset contains an invalid color value");
425 if (
const auto value = readFloat(
"ab_height_masking"))
429 if (malformedKnownInput)
430 return recipeFailure(
"Substance preset contains a malformed recognized numeric value");
438 return bakeFailure(
"Unsupported procedural decal recipe schema version");
440 return bakeFailure(
"Procedural decal dimensions must be in [1, 4096]");
441 if (!validLayer(recipe.
layerA) || !validLayer(recipe.
layerB) ||
447 return bakeFailure(
"Procedural decal recipe contains values outside supported ranges");
449 const auto maskA = layerMask(recipe, recipe.
layerA);
450 const auto maskB = layerMask(recipe, recipe.
layerB);
451 const std::size_t
pixels = std::size_t(recipe.
width) * std::size_t(recipe.
height);
459 std::vector<float> normalHeight(
pixels, 0.f);
460 std::vector<float> coverage(
pixels, 0.f);
464 const float bWeight =
466 const float combined = recipe.
layerBlendMode == 2 ? std::max(
a, bWeight) :
a + bWeight * (1.f -
a);
467 const float alpha = clamp01(combined) * recipe.
opacity;
468 const float mixB = alpha > 1e-6f ? bWeight / std::max(
a + bWeight, 1e-6f) : 0.f;
469 const float variation =
470 random01(
static_cast<std::uint32_t
>(
index % std::size_t(recipe.
width)),
471 static_cast<std::uint32_t
>(
index / std::size_t(recipe.
width)), recipe.
seed + 991u) *
474 const float hueNoise = random01(
static_cast<std::uint32_t
>(
index % std::size_t(recipe.
width)),
475 static_cast<std::uint32_t
>(
index / std::size_t(recipe.
width)),
476 recipe.
seed + 1877u) * 2.f - 1.f;
479 for (
int channel = 0; channel < 3; ++channel) {
480 const float color = std::lerp(colorA[channel], colorB[channel], mixB);
482 bake.
albedo[
index * 4u + std::size_t(channel)] = byte(
color * (1.f + variation * colorVar));
493 const float edge0 = std::min(
layer.normalTrim * 0.9f, 0.95f);
494 float shaped = smoothStep(edge0, std::min(1.f, edge0 +
layer.normalSoftness), clamp01(
mask));
495 if (
layer.normalStyle > 0)
496 shaped = std::pow(shaped, 0.65f + 0.1f *
float(
layer.normalStyle % 5));
497 const float thickness = 0.5f + std::log2(1.f +
layer.normalThickness) * 0.75f;
500 normalHeight[
index] = clamp01(normalLayer(
a, recipe.
layerA) + normalLayer(bWeight, recipe.
layerB));
502 coverage[
index] = alpha;
504 for (
int y = 0;
y < recipe.
height; ++
y) {
505 for (
int x = 0;
x < recipe.
width; ++
x) {
506 const auto at = [&](
int px,
int py) {
507 return normalHeight[std::size_t(std::clamp(
py, 0, recipe.
height - 1)) * std::size_t(recipe.
width) +
508 std::size_t(std::clamp(
px, 0, recipe.
width - 1))];
510 const std::size_t
index = std::size_t(
y) * std::size_t(recipe.
width) + std::size_t(
x);
511 const float layerMix = maskB[
index] / std::max(maskA[
index] + maskB[
index], 1e-6f);
515 const float invLength = 1.f / std::sqrt(
nx *
nx +
ny *
ny + 1.f);
518 const float nz = invLength;
Stable, structured diagnostics shared by engine modules.
std::map< std::string, Var > values
std::array< std::uint8_t, 32 > hash
std::array< float, 3 > scale
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
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.
float smoothStep(float value) noexcept
Smooth step.
Result< ProceduralDecalBake > bakeProceduralDecal(const ProceduralDecalRecipe &recipe)
Bake a two-layer decal recipe into renderer-ready texture bytes.
Result< ProceduralDecalRecipe > proceduralDecalPreset(std::string_view name, std::uint32_t seed)
Build a deterministic preset recipe.
DecalPattern
Built-in procedural masks used by a procedural decal layer.
Result< ProceduralDecalRecipe > importProceduralDecalSbsprs(std::string_view xml)
Import one Substance .sbsprs preset into the native version-two recipe.
double sample(const Heightmap &map, double u, double v)
Sample.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
Owning, renderer-neutral output of one decal bake.
std::vector< std::uint8_t > albedo
std::vector< std::uint8_t > params
std::vector< std::uint8_t > normal
One independently art-directed layer of a procedural decal recipe.
std::array< float, 3 > color
Versioned two-layer authoring recipe analogous to Procedural Decal's Standard graph and material-inst...
ProceduralDecalLayer layerB
static constexpr std::uint32_t kSchemaVersion
ProceduralDecalLayer layerA
std::uint32_t schemaVersion