载入中...
搜索中...
未找到
ProceduralDecal.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4
5#include <algorithm>
6#include <cerrno>
7#include <charconv>
8#include <cmath>
9#include <cstdlib>
10#include <limits>
11#include <optional>
12#include <string>
13#include <unordered_map>
14#include <utility>
15
16namespace eve::decal {
17namespace {
18
19Result<ProceduralDecalRecipe> recipeFailure(std::string message) {
21 Diagnostic::error(DiagnosticCode::InvalidArgument, std::move(message), "decal.procedural.preset"));
22}
23
24Result<ProceduralDecalBake> bakeFailure(std::string message) {
26 Diagnostic::error(DiagnosticCode::InvalidArgument, std::move(message), "decal.procedural.recipe"));
27}
28
29std::optional<std::string_view> attribute(std::string_view element, std::string_view name) {
30 const std::string needle = std::string(name) + "=\"";
31 const auto begin = element.find(needle);
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);
37}
38
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);
43 char* end = nullptr;
44 errno = 0;
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;
47 return value;
48}
49
50std::optional<int> integer(std::string_view text) {
51 int value = 0;
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;
54 return value;
55}
56
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);
62 auto value = number(token);
63 if (!value) return std::nullopt;
64 color[channel] = *value;
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) {
69 return std::nullopt;
70 }
71 }
72 return color;
73}
74
75DecalPattern importedPattern(int sourceId) {
76 // Substance exposes a larger pattern catalogue. Preserve deterministic variation while mapping it
77 // onto the five native pattern families rather than depending on Substance at runtime.
80 const auto magnitude = sourceId < 0 ? std::uint64_t(-std::int64_t(sourceId)) : std::uint64_t(sourceId);
81 return families[std::size_t(magnitude % std::size(families))];
82}
83
84float clamp01(float value) { return std::clamp(value, 0.f, 1.f); }
85
86float smoothStep(float edge0, float edge1, float value) {
87 const float t = clamp01((value - edge0) / (edge1 - edge0));
88 return t * t * (3.f - 2.f * t);
89}
90
91std::uint32_t hash(std::uint32_t value) {
92 value ^= value >> 16;
93 value *= 0x7feb352du;
94 value ^= value >> 15;
95 value *= 0x846ca68bu;
96 return value ^ (value >> 16);
97}
98
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());
101}
102
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);
110 const auto sample = [seed](int px, int py) {
111 return random01(static_cast<std::uint32_t>(px), static_cast<std::uint32_t>(py), seed);
112 };
113 const float a = std::lerp(sample(ix, iy), sample(ix + 1, iy), sx);
114 const float b = std::lerp(sample(ix, iy + 1), sample(ix + 1, iy + 1), sx);
115 return std::lerp(a, b, sy);
116}
117
118float fractal(float x, float y, std::uint32_t seed) {
119 float value = 0.f;
120 float weight = 0.55f;
121 for (int octave = 0; octave < 4; ++octave) {
122 value += valueNoise(x, y, seed + std::uint32_t(octave) * 1013u) * weight;
123 x *= 2.03f;
124 y *= 2.03f;
125 weight *= 0.5f;
126 }
127 return value / 1.03125f;
128}
129
130float patternValue(DecalPattern pattern, int sourcePatternId, float u, float v, float scale,
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);
137 switch (family) {
138 case 0: {
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);
141 }
142 case 1: {
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);
145 }
146 case 2: {
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);
149 }
150 case 3: {
151 const float warpedRadius = radius * (0.78f + 0.42f * noise);
152 return 1.f - smoothStep(0.62f, 1.05f, warpedRadius);
153 }
154 case 4: return clamp01((noise - 0.28f) * 1.45f) * clamp01(1.25f - radius * 0.38f);
155 case 5: {
156 const float islands = fractal(x * 1.45f + noise * 0.55f, y * 1.45f - noise * 0.35f,
157 seed + 257u);
158 return clamp01((islands - 0.46f) * 2.65f) * clamp01(1.14f - radius * 0.48f);
159 }
160 case 6: {
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);
163 }
164 case 7: {
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);
169 }
170 case 8: {
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);
174 }
175 case 9: {
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);
178 }
179 case 10: {
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);
182 }
183 case 11: {
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);
188 }
189 }
190 return 0.f;
191}
192
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);
195 std::vector<float> mask(count, 0.f);
196 if (!layer.enabled) return mask;
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;
205 float value = patternValue(layer.pattern, layer.sourcePatternId, ru, rv, layer.scale,
206 recipe.seed ^ layer.seed);
207 if (layer.invertMask) value = 1.f - value;
208 value = clamp01((value - 0.5f) * layer.contrast + 0.5f);
209 mask[std::size_t(y) * std::size_t(recipe.width) + std::size_t(x)] = clamp01(value * layer.amount);
210 }
211 }
212 const int radius = std::clamp(static_cast<int>(std::round(layer.blur * 12.f)), 0, 12);
213 if (radius == 0) return mask;
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) {
218 float sum = 0.f;
219 for (int dx = -radius; dx <= radius; ++dx)
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);
223 }
224 }
225 for (int y = 0; y < recipe.height; ++y) {
226 for (int x = 0; x < recipe.width; ++x) {
227 float sum = 0.f;
228 for (int dy = -radius; dy <= radius; ++dy)
229 sum += horizontal[std::size_t(std::clamp(y + dy, 0, recipe.height - 1)) * std::size_t(recipe.width) +
230 std::size_t(x)];
231 result[std::size_t(y) * std::size_t(recipe.width) + std::size_t(x)] = sum / float(radius * 2 + 1);
232 }
233 }
234 return result;
235}
236
237std::uint8_t byte(float value) { return static_cast<std::uint8_t>(std::round(clamp01(value) * 255.f)); }
238
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);
242 return {
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)),
252 };
253}
254
255bool finiteLayer(const ProceduralDecalLayer& layer) {
256 const float values[] = {layer.amount, layer.scale, layer.rotation, layer.blur,
257 layer.contrast, layer.color[0], layer.color[1], layer.color[2],
258 layer.colorVariation, layer.hueVariation, layer.roughness, layer.roughnessVariation,
259 layer.metallic, layer.emissive, layer.normalStrength, layer.normalSoftness,
260 layer.normalTrim, layer.normalThickness, layer.height};
261 return std::all_of(std::begin(values), std::end(values), [](float value) { return std::isfinite(value); });
262}
263
264bool validLayer(const ProceduralDecalLayer& layer) {
265 const auto inRange = [](float value, float minimum, float maximum) {
266 return value >= minimum && value <= maximum;
267 };
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);
279}
280
281ProceduralDecalLayer layer(DecalPattern pattern, std::array<float, 3> color, float roughness, float metallic,
282 float height, std::uint32_t seed) {
283 ProceduralDecalLayer value;
284 value.pattern = pattern;
285 value.color = color;
286 value.roughness = roughness;
287 value.metallic = metallic;
288 value.height = height;
289 value.seed = seed;
290 return value;
291}
292
293} // namespace
294
297 recipe.seed = seed;
298 recipe.layerB.amount = 0.45f;
299 recipe.layerB.scale = 9.f;
300 recipe.layerB.blur = 0.04f;
301 if (name == "blood-wet") {
302 recipe.layerA = layer(DecalPattern::Puddle, {0.34f, 0.002f, 0.001f}, 0.04f, 0.f, 0.65f, 11u);
303 recipe.layerB = layer(DecalPattern::Spots, {0.62f, 0.03f, 0.02f}, 0.12f, 0.f, 0.42f, 12u);
304 } else if (name == "blood-dried") {
305 recipe.layerA = layer(DecalPattern::Spots, {0.16f, 0.012f, 0.006f}, 0.82f, 0.f, 0.28f, 21u);
306 recipe.layerB = layer(DecalPattern::Cracks, {0.07f, 0.004f, 0.002f}, 0.94f, 0.f, 0.36f, 22u);
307 } else if (name == "damage") {
308 recipe.layerA = layer(DecalPattern::Cracks, {0.055f, 0.05f, 0.045f}, 0.88f, 0.f, 0.82f, 31u);
309 recipe.layerA.normalStrength = 5.f;
310 recipe.layerB = layer(DecalPattern::Spots, {0.12f, 0.1f, 0.08f}, 0.75f, 0.f, 0.35f, 32u);
311 } else if (name == "dirt") {
312 recipe.layerA = layer(DecalPattern::Grunge, {0.22f, 0.14f, 0.07f}, 0.92f, 0.f, 0.38f, 41u);
313 recipe.layerB = layer(DecalPattern::Streaks, {0.32f, 0.24f, 0.12f}, 0.86f, 0.f, 0.22f, 42u);
314 } else if (name == "rust") {
315 recipe.layerA = layer(DecalPattern::Grunge, {0.42f, 0.075f, 0.018f}, 0.91f, 0.55f, 0.5f, 51u);
316 recipe.layerB = layer(DecalPattern::Spots, {0.12f, 0.025f, 0.008f}, 0.98f, 0.25f, 0.68f, 52u);
317 } else if (name == "puddle") {
318 recipe.layerA = layer(DecalPattern::Puddle, {0.035f, 0.045f, 0.05f}, 0.025f, 0.f, 0.08f, 61u);
319 recipe.layerB.enabled = false;
320 } else if (name == "paint") {
321 recipe.layerA = layer(DecalPattern::Spots, {0.12f, 0.32f, 0.8f}, 0.32f, 0.f, 0.42f, 71u);
322 recipe.layerB = layer(DecalPattern::Cracks, {0.025f, 0.025f, 0.025f}, 0.9f, 0.f, 0.62f, 72u);
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};
326 recipe.layerA = layer(DecalPattern::Grunge, base, 0.96f, 0.f, 0.65f, 81u);
327 recipe.layerB = layer(DecalPattern::Spots, {0.24f, 0.36f, 0.09f}, 0.88f, 0.f, 0.32f, 82u);
328 } else {
329 return recipeFailure("Unknown procedural decal preset: " + std::string(name));
330 }
331 recipe.layerA.seed ^= seed;
332 recipe.layerB.seed ^= seed * 1664525u + 1013904223u;
333 return Result<ProceduralDecalRecipe>::success(std::move(recipe));
334}
335
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");
342
343 std::unordered_map<std::string, std::string> inputs;
344 std::size_t cursor = 0;
345 while ((cursor = xml.find("<presetinput", cursor)) != std::string_view::npos) {
346 const auto end = xml.find("/>", cursor);
347 if (end == std::string_view::npos) return recipeFailure("Unterminated presetinput element");
348 const auto element = xml.substr(cursor, end + 2 - cursor);
349 const auto id = attribute(element, "identifier");
350 const auto value = attribute(element, "value");
351 if (!id || !value) return recipeFailure("presetinput requires identifier and value attributes");
352 inputs.insert_or_assign(std::string(*id), std::string(*value));
353 cursor = end + 2;
354 }
355 if (inputs.empty()) return recipeFailure("Substance preset contains no inputs");
356
358 bool malformedKnownInput = false;
359 const auto readFloat = [&](std::string_view name) -> std::optional<float> {
360 const auto found = inputs.find(std::string(name));
361 if (found == inputs.end()) return std::nullopt;
362 auto value = number(found->second);
363 malformedKnownInput = malformedKnownInput || !value.has_value();
364 return value;
365 };
366 const auto readInt = [&](std::string_view name) -> std::optional<int> {
367 const auto found = inputs.find(std::string(name));
368 if (found == inputs.end()) return std::nullopt;
369 auto value = integer(found->second);
370 malformedKnownInput = malformedKnownInput || !value.has_value();
371 return value;
372 };
373 const auto applyLayer = [&](std::string_view prefix, ProceduralDecalLayer& target) -> bool {
374 const std::string key(prefix);
375 if (const auto value = readFloat(key + "amount")) target.amount = clamp01(*value);
376 if (const auto value = readInt(key + "pattern")) {
377 target.sourcePatternId = *value;
378 target.pattern = importedPattern(*value);
379 }
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);
382 if (const auto value = readInt(key + "mask_invert")) target.invertMask = *value != 0;
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;
389 target.color = *value;
390 }
391 if (const auto value = readFloat(key + "color_value_variation"))
392 target.colorVariation = clamp01(*value);
393 if (const auto value = readFloat(key + "color_hue_variation")) target.hueVariation = clamp01(*value);
394 if (const auto value = readFloat(key + "roughness")) target.roughness = clamp01(*value);
395 if (const auto value = readFloat(key + "rough_variation"))
396 target.roughnessVariation = clamp01(*value);
397 if (const auto value = readFloat(key + "metallic")) target.metallic = clamp01(*value);
398 if (const auto value = readFloat(key + "emissive")) target.emissive = clamp01(*value);
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);
405 if (const auto value = readInt(key + "normal_style")) target.normalStyle = *value;
406 if (const auto value = readFloat(key + "height")) target.height = clamp01(*value);
407 if (const auto value = readFloat(key + "color_opacity")) target.amount *= clamp01(*value);
408 return true;
409 };
410
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]");
420 recipe.width = 1 << *x;
421 recipe.height = 1 << *y;
422 }
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"))
426 recipe.layerHeightBlend = clamp01((*value + 1.f) * 0.5f);
427 if (const auto value = readFloat("ab_balance")) recipe.layerBalance = clamp01(*value);
428 if (const auto value = readInt("ab_blendmode")) recipe.layerBlendMode = *value;
429 if (malformedKnownInput)
430 return recipeFailure("Substance preset contains a malformed recognized numeric value");
431 recipe.layerA.seed = recipe.seed ^ 0x9e3779b9u;
432 recipe.layerB.seed = recipe.seed ^ 0x85ebca6bu;
433 return Result<ProceduralDecalRecipe>::success(std::move(recipe));
434}
435
438 return bakeFailure("Unsupported procedural decal recipe schema version");
439 if (recipe.width <= 0 || recipe.height <= 0 || recipe.width > 4096 || recipe.height > 4096)
440 return bakeFailure("Procedural decal dimensions must be in [1, 4096]");
441 if (!validLayer(recipe.layerA) || !validLayer(recipe.layerB) ||
442 !std::isfinite(recipe.layerHeightBlend) || recipe.layerHeightBlend < 0.f ||
443 recipe.layerHeightBlend > 1.f || !std::isfinite(recipe.layerBalance) ||
444 recipe.layerBalance < 0.f || recipe.layerBalance > 1.f || recipe.layerBlendMode < 0 ||
445 recipe.layerBlendMode > 2 || !std::isfinite(recipe.opacity) || recipe.opacity < 0.f ||
446 recipe.opacity > 1.f)
447 return bakeFailure("Procedural decal recipe contains values outside supported ranges");
448
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);
453 bake.width = recipe.width;
454 bake.height = recipe.height;
455 bake.albedo.resize(pixels * 4u);
456 bake.normal.resize(pixels * 4u);
457 bake.params.resize(pixels * 4u);
458 std::vector<float> height(pixels, 0.f);
459 std::vector<float> normalHeight(pixels, 0.f);
460 std::vector<float> coverage(pixels, 0.f);
461 for (std::size_t index = 0; index < pixels; ++index) {
462 const float a = maskA[index] * (2.f * (1.f - recipe.layerBalance));
463 const float b = maskB[index] * (2.f * recipe.layerBalance);
464 const float bWeight =
465 b * clamp01(0.5f + (recipe.layerB.height - recipe.layerA.height) * recipe.layerHeightBlend);
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) *
472 2.f -
473 1.f;
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;
477 const auto colorA = rotateHue(recipe.layerA.color, hueNoise * recipe.layerA.hueVariation * 3.14159265f);
478 const auto colorB = rotateHue(recipe.layerB.color, hueNoise * recipe.layerB.hueVariation * 3.14159265f);
479 for (int channel = 0; channel < 3; ++channel) {
480 const float color = std::lerp(colorA[channel], colorB[channel], mixB);
481 const float colorVar = std::lerp(recipe.layerA.colorVariation, recipe.layerB.colorVariation, mixB);
482 bake.albedo[index * 4u + std::size_t(channel)] = byte(color * (1.f + variation * colorVar));
483 }
484 bake.albedo[index * 4u + 3u] = byte(alpha);
485 const float roughness =
486 std::lerp(recipe.layerA.roughness, recipe.layerB.roughness, mixB) +
487 variation * std::lerp(recipe.layerA.roughnessVariation, recipe.layerB.roughnessVariation, mixB);
488 bake.params[index * 4u] = byte(roughness);
489 bake.params[index * 4u + 1u] = byte(std::lerp(recipe.layerA.metallic, recipe.layerB.metallic, mixB));
490 bake.params[index * 4u + 2u] = byte(std::lerp(recipe.layerA.emissive, recipe.layerB.emissive, mixB));
491 height[index] = clamp01(a * recipe.layerA.height + bWeight * recipe.layerB.height);
492 const auto normalLayer = [](float mask, const ProceduralDecalLayer& layer) {
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;
498 return shaped * layer.height * thickness;
499 };
500 normalHeight[index] = clamp01(normalLayer(a, recipe.layerA) + normalLayer(bWeight, recipe.layerB));
501 bake.params[index * 4u + 3u] = byte(height[index]);
502 coverage[index] = alpha;
503 }
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))];
509 };
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);
512 const float strength = std::lerp(recipe.layerA.normalStrength, recipe.layerB.normalStrength, layerMix);
513 float nx = (at(x - 1, y) - at(x + 1, y)) * strength;
514 float ny = (at(x, y - 1) - at(x, y + 1)) * strength;
515 const float invLength = 1.f / std::sqrt(nx * nx + ny * ny + 1.f);
516 nx *= invLength;
517 ny *= invLength;
518 const float nz = invLength;
519 bake.normal[index * 4u] = byte(nx * 0.5f + 0.5f);
520 bake.normal[index * 4u + 1u] = byte(ny * 0.5f + 0.5f);
521 bake.normal[index * 4u + 2u] = byte(nz * 0.5f + 0.5f);
522 bake.normal[index * 4u + 3u] = byte(coverage[index]);
523 }
524 }
525 return Result<ProceduralDecalBake>::success(std::move(bake));
526}
527
528} // namespace eve::decal
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
const std::string & s
int mask
float thickness
Definition CaveMesh.cpp:83
float py
float nx
float nz
float ny
Stable, structured diagnostics shared by engine modules.
std::map< std::string, Var > values
std::uint64_t sourceId
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
std::string pattern
std::string message
float maximum[3]
float minimum[3]
std::uint32_t key
float u
Definition Grass.cpp:233
int inputs
Definition GridGraph.cpp:23
float v
std::int32_t c
std::vector< Colorf > px
std::uint32_t height
std::string text
std::array< float, 3 > scale
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float roughness
float metallic
TileLayer * layer
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::uint32_t seed
Definition PointSet.cpp:807
float begin
float t
uint8_t * pixels
uint64_t token
const RoadEdge * edge
double number
bool found
float dy
float dx
std::uint32_t count
std::string element
std::size_t cursor
uint32_t index
std::size_t at
float angle
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
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.
Versioned two-layer authoring recipe analogous to Procedural Decal's Standard graph and material-inst...
static constexpr std::uint32_t kSchemaVersion
glm::vec4 color