10bool valid(
const VegetationPresetImage& image, std::uint64_t maximumPixels) {
16float channel(
const VegetationPresetImage& image,
float u,
float v,
unsigned component) {
17 const float x = std::clamp(
u *
image.width - 0.5f, 0.f,
float(
image.width - 1));
18 const float y = std::clamp(
v *
image.height - 0.5f, 0.f,
float(
image.height - 1));
19 const auto x0 = std::uint32_t(
x), y0 = std::uint32_t(
y);
20 const auto x1 = std::min(x0 + 1,
image.width - 1), y1 = std::min(y0 + 1,
image.height - 1);
21 const auto at = [&](std::uint32_t
px, std::uint32_t
py) {
22 return image.pixels[(std::size_t(
py) *
image.width +
px) * 4 + component] / 255.f;
24 return std::lerp(std::lerp(
at(x0, y0),
at(x1, y0),
x - x0), std::lerp(
at(x0, y1),
at(x1, y1),
x - x0),
y - y0);
27Result<float>
sample(
const VegetationPresetImage& image,
const VegetationTextureChannelRecipe& recipe,
float u,
30 if (recipe.selector ==
"GET_RED")
31 value = channel(image,
u,
v, 0);
32 else if (recipe.selector ==
"GET_GREEN")
33 value = channel(image,
u,
v, 1);
34 else if (recipe.selector ==
"GET_BLUE")
35 value = channel(image,
u,
v, 2);
36 else if (recipe.selector ==
"GET_ALPHA")
37 value = channel(image,
u,
v, 3);
38 else if (recipe.selector ==
"GET_MAX")
39 value = std::max({channel(image,
u,
v, 0), channel(image,
u,
v, 1), channel(image,
u,
v, 2)});
40 else if (recipe.selector ==
"GET_GRAY" || recipe.selector ==
"GET_GREY")
41 value = (channel(image,
u,
v, 0) + channel(image,
u,
v, 1) + channel(image,
u,
v, 2)) / 3.f;
44 "unsupported texture channel selector: " + recipe.selector, {},
45 {},
"asset.import.vegetation-preset.texture"));
46 if (recipe.action ==
"ACTION_ONE_MINUS")
48 else if (!recipe.action.empty())
50 "unsupported texture channel action: " + recipe.action, {}, {},
51 "asset.import.vegetation-preset.texture"));
55float gammaToLinear(
float value) {
56 return value <= 0.04045f ?
value / 12.92f : std::pow((
value + 0.055f) / 1.055f, 2.4f);
58float linearToGamma(
float value) {
59 return value <= 0.0031308f ?
value * 12.92f : 1.055f * std::pow(
value, 1.f / 2.4f) - 0.055f;
62Result<void> transformNormalMap(VegetationPresetImage& image,
const asset::CanonicalMeshData&
mesh,
63 bool objectToTangent) {
64 const auto count =
mesh.positions.size() / 3;
65 const auto tangent =
mesh.attributes.find(
"TANGENT");
66 const auto uv =
mesh.texcoords.find(0);
69 uv->second.size() !=
count * 2 ||
mesh.indices.size() % 3)
72 "normal-space transform requires normals, tangent float4, UV0 and triangles", {}, {},
73 "asset.import.vegetation-preset.texture"));
74 auto norm = [](
float&
x,
float&
y,
float&
z) {
75 const float l = std::sqrt(
x *
x +
y *
y +
z *
z);
76 if (l < 1e-8f)
return false;
83 std::uint32_t
a = 0,
b = 0,
c = 0;
84 float w0 = 0, w1 = 0, w2 = 0;
88 std::uint32_t
a = 0,
b = 0,
c = 0;
89 float ax = 0,
ay = 0,
bx = 0,
by = 0,
cx = 0,
cy = 0, determinant = 0;
90 int minX = 0, maxX = -1, minY = 0, maxY = -1;
92 constexpr std::uint32_t tileSize = 16;
93 const auto tilesX = (
image.width + tileSize - 1) / tileSize;
94 const auto tilesY = (
image.height + tileSize - 1) / tileSize;
95 std::vector<Frame> frames(std::size_t(
image.width) *
image.height);
98 std::vector<std::vector<std::uint32_t>> bins(std::size_t(
tilesX) *
tilesY);
99 std::uint64_t rasterWork = 0, binReferences = 0;
100 for (std::size_t k = 0; k <
mesh.indices.size(); k += 3) {
101 const auto a =
mesh.indices[k],
b =
mesh.indices[k + 1],
c =
mesh.indices[k + 2];
104 "normal-space mesh index out of range", {}, {},
105 "asset.import.vegetation-preset.texture"));
106 const float ax =
uv->second[
a * 2],
ay =
uv->second[
a * 2 + 1],
bx =
uv->second[
b * 2],
107 by =
uv->second[
b * 2 + 1],
cx =
uv->second[
c * 2],
cy =
uv->second[
c * 2 + 1];
108 if (!std::isfinite(
ax) || !std::isfinite(
ay) || !std::isfinite(
bx) || !std::isfinite(
by) ||
109 !std::isfinite(
cx) || !std::isfinite(
cy))
111 "normal-space mesh contains nonfinite UVs", {}, {},
112 "asset.import.vegetation-preset.texture"));
114 if (std::abs(determinant) < 1e-10f)
continue;
115 const int minX = std::max(0,
int(std::ceil(std::min({
ax,
bx,
cx}) *
image.width - .5f)));
116 const int maxX = std::min(
int(
image.width) - 1,
int(std::floor(std::max({ax, bx, cx}) *
image.width - .5f)));
117 const int minY = std::max(0,
int(std::ceil(std::min({
ay,
by,
cy}) *
image.height - .5f)));
118 const int maxY = std::min(
int(
image.height) - 1,
int(std::floor(std::max({ay, by, cy}) *
image.height - .5f)));
119 if (minX > maxX || minY > maxY)
continue;
120 rasterWork += std::uint64_t(maxX - minX + 1) * std::uint64_t(maxY - minY + 1);
121 if (rasterWork > 512ull * 1024ull * 1024ull)
123 "normal-space raster work budget exceeded", {}, {},
124 "asset.import.vegetation-preset.texture"));
125 const auto triangleIndex = std::uint32_t(
triangles.size());
126 triangles.push_back({
a,
b,
c,
ax,
ay,
bx,
by,
cx,
cy, determinant, minX, maxX, minY, maxY});
127 const auto firstTileX = std::uint32_t(minX) / tileSize, lastTileX = std::uint32_t(maxX) / tileSize;
128 const auto firstTileY = std::uint32_t(minY) / tileSize, lastTileY = std::uint32_t(maxY) / tileSize;
129 binReferences += std::uint64_t(lastTileX - firstTileX + 1) * (lastTileY - firstTileY + 1);
130 if (binReferences > 32ull * 1024ull * 1024ull)
132 "normal-space triangle-bin budget exceeded", {}, {},
133 "asset.import.vegetation-preset.texture"));
134 for (
auto tileY = firstTileY; tileY <= lastTileY; ++tileY)
135 for (
auto tileX = firstTileX; tileX <= lastTileX; ++tileX)
136 bins[std::size_t(tileY) *
tilesX + tileX].push_back(triangleIndex);
138 for (std::uint32_t tileY = 0; tileY <
tilesY; ++tileY)
139 for (std::uint32_t tileX = 0; tileX <
tilesX; ++tileX) {
140 const int tileMinX = int(tileX * tileSize), tileMinY = int(tileY * tileSize);
141 const int tileMaxX = std::min(
int(
image.width) - 1, tileMinX +
int(tileSize) - 1);
142 const int tileMaxY = std::min(
int(
image.height) - 1, tileMinY +
int(tileSize) - 1);
143 for (
const auto triangleIndex : bins[
std::size_t(tileY) *
tilesX + tileX]) {
147 auto& frame = frames[std::size_t(
py) *
image.width +
px];
155 const float w2 = 1 - w0 - w1;
156 if (w0 >= -1e-5f && w1 >= -1e-5f && w2 >= -1e-5f)
161 for (std::uint32_t
py = 0;
py <
image.height; ++
py)
162 for (std::uint32_t
px = 0;
px <
image.width; ++
px) {
163 const auto& frame = frames[std::size_t(
py) *
image.width +
px];
164 if (!frame.covered)
continue;
165 auto interp = [&](
const std::vector<float>&
values,
unsigned stride,
unsigned component) {
166 return values[frame.a * stride + component] * frame.w0 +
167 values[frame.b * stride + component] * frame.w1 +
168 values[frame.c * stride + component] * frame.w2;
170 float nx = interp(
mesh.normals, 3, 0),
ny = interp(
mesh.normals, 3, 1),
nz = interp(
mesh.normals, 3, 2);
171 float tx = interp(
tangent->second.values, 4, 0), ty = interp(
tangent->second.values, 4, 1),
172 tz = interp(
tangent->second.values, 4, 2);
173 if (!norm(
nx,
ny,
nz) || !norm(tx, ty, tz))
175 "degenerate normal-space frame", {}, {},
176 "asset.import.vegetation-preset.texture"));
177 const float sign = interp(
tangent->second.values, 4, 3) >= 0 ? 1.f : -1.f;
178 float bx = (
ny * tz -
nz * ty) * sign,
by = (
nz * tx -
nx * tz) * sign,
bz = (
nx * ty -
ny * tx) * sign;
180 const auto at = (std::size_t(
py) *
image.width +
px) * 4;
181 float x =
image.pixels[
at] / 127.5f - 1,
y =
image.pixels[
at + 1] / 127.5f - 1,
182 z =
image.pixels[
at + 2] / 127.5f - 1;
184 if (objectToTangent) {
185 ox =
x * tx +
y * ty +
z * tz;
193 if (!norm(
ox,
oy, oz))
195 "zero normal-map direction", {}, {},
196 "asset.import.vegetation-preset.texture"));
197 image.pixels[
at] = std::uint8_t(std::clamp(
ox * .5f + .5f, 0.f, 1.f) * 255 + .5f);
198 image.pixels[
at + 1] = std::uint8_t(std::clamp(
oy * .5f + .5f, 0.f, 1.f) * 255 + .5f);
199 image.pixels[
at + 2] = std::uint8_t(std::clamp(oz * .5f + .5f, 0.f, 1.f) * 255 + .5f);
207 std::uint64_t maximumPixels) {
211 "asset.import.vegetation-preset.texture"));
213 std::map<std::string, VegetationPresetImage> outputs;
215 if (recipe.targetProperty.empty())
216 return Result<std::map<std::string, VegetationPresetImage>>::failure(
218 "asset.import.vegetation-preset.texture"));
219 if (recipe.transformSpace ==
"OBJECT_TO_TANGENT" || recipe.transformSpace ==
"TANGENT_TO_OBJECT")
222 {}, {},
"asset.import.vegetation-preset.texture"));
224 for (
const auto& instruction : recipe.channels) {
225 if (instruction.selector.empty() || instruction.selector ==
"NONE")
continue;
226 const auto found = sources.find(instruction.sourceProperty);
227 if (
found == sources.end())
continue;
229 return Result<std::map<std::string, VegetationPresetImage>>::failure(
231 "asset.import.vegetation-preset.texture"));
237 if (
count > maximumPixels ||
count > SIZE_MAX / 4)
240 "asset.import.vegetation-preset.texture"));
242 for (std::uint32_t
y = 0;
y <
height; ++
y) {
243 for (std::uint32_t
x = 0;
x <
width; ++
x) {
244 for (
unsigned c = 0;
c < 4; ++
c) {
245 const auto& instruction = recipe.channels[
c];
246 if (instruction.selector.empty() || instruction.selector ==
"NONE")
continue;
247 const auto found = sources.find(instruction.sourceProperty);
248 if (
found == sources.end())
continue;
252 float component =
value.value();
253 if (recipe.transformSpace ==
"GAMMA_TO_LINEAR")
254 component = gammaToLinear(component);
255 else if (recipe.transformSpace ==
"LINEAR_TO_GAMMA")
256 component = linearToGamma(component);
257 else if (!recipe.transformSpace.empty() && recipe.transformSpace !=
"NONE")
260 "unsupported texture transform space: " + recipe.transformSpace, {},
261 {},
"asset.import.vegetation-preset.texture"));
263 std::uint8_t(std::clamp(component, 0.f, 1.f) * 255.f + 0.5f);
267 outputs[recipe.targetProperty] = std::move(
output);
270 }
catch (
const std::bad_alloc&) {
273 "asset.import.vegetation-preset.texture"));
281 auto plain = candidate;
282 for (
auto& recipe : plain.texturePacks)
283 if (recipe.transformSpace ==
"OBJECT_TO_TANGENT" || recipe.transformSpace ==
"TANGENT_TO_OBJECT")
284 recipe.transformSpace =
"NONE";
286 if (!outputs)
return outputs;
288 if (recipe.transformSpace !=
"OBJECT_TO_TANGENT" && recipe.transformSpace !=
"TANGENT_TO_OBJECT")
continue;
289 auto found = outputs.value().find(recipe.targetProperty);
290 if (
found == outputs.value().end())
continue;
291 auto changed = transformNormalMap(
found->second,
mesh, recipe.transformSpace ==
"OBJECT_TO_TANGENT");
295 }
catch (
const std::bad_alloc&) {
298 "asset.import.vegetation-preset.texture"));
std::map< std::string, Var > values
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.
EVENGINE_API_PLATFORM Result< std::map< std::string, VegetationPresetImage > > executeVegetationTexturePacks(const VegetationConversionCandidate &candidate, const std::map< std::string, VegetationPresetImage > &sources, std::uint64_t maximumPixels=16ull *1024ull *1024ull)
Execute all texture recipes against detached RGBA8 source images.
double sample(const Heightmap &map, double u, double v)
Sample.
Owning CPU mesh snapshot; UV arrays are packed ST pairs indexed by source set number....
Owning unpublished material, mesh and texture candidate mutated transactionally by preset commands.
std::vector< VegetationTexturePackRecipe > texturePacks
Owning linear RGBA8 source or packed texture used by preset conversion.