#include <whiteout/textures/pbr_bake.h>
#include <algorithm>
#include <cmath>
#include <numbers>
#include <vector>
namespace whiteout::textures::pbr {
namespace {
struct Plane {
u32 width = 0;
u32 height = 0;
std::vector<u8> rgba;
bool empty() const {
return width == 0 || height == 0 || rgba.empty();
}
};
Plane decode(const Texture* texture) {
Plane plane;
if (texture == nullptr || texture->width() == 0 || texture->height() == 0) {
return plane;
}
const Texture rgba8 = texture->copyAsFormat(PixelFormat::RGBA8);
const std::span<const u8> pixels = rgba8.mipData(0);
if (pixels.empty()) {
return plane;
}
plane.width = rgba8.width();
plane.height = rgba8.height();
plane.rgba.assign(pixels.begin(), pixels.end());
return plane;
}
void sample(const Plane& plane, f32 u, f32 v, f32 out[4]) {
out[0] = out[1] = out[2] = out[3] = 0.0f;
if (plane.empty()) {
return;
}
const f32 x = u * static_cast<f32>(plane.width) - 0.5f;
const f32 y = v * static_cast<f32>(plane.height) - 0.5f;
const i32 x0 = static_cast<i32>(std::floor(x));
const i32 y0 = static_cast<i32>(std::floor(y));
const f32 fx = x - static_cast<f32>(x0);
const f32 fy = y - static_cast<f32>(y0);
const auto clampX = [&](i32 value) {
return std::clamp(value, 0, static_cast<i32>(plane.width) - 1);
};
const auto clampY = [&](i32 value) {
return std::clamp(value, 0, static_cast<i32>(plane.height) - 1);
};
const i32 xs[2] = {clampX(x0), clampX(x0 + 1)};
const i32 ys[2] = {clampY(y0), clampY(y0 + 1)};
const f32 wx[2] = {1.0f - fx, fx};
const f32 wy[2] = {1.0f - fy, fy};
for (i32 j = 0; j < 2; ++j) {
for (i32 i = 0; i < 2; ++i) {
const std::size_t offset =
(static_cast<std::size_t>(ys[j]) * plane.width + static_cast<std::size_t>(xs[i])) *
4;
const f32 weight = wx[i] * wy[j];
for (i32 c = 0; c < 4; ++c) {
out[c] += weight *
static_cast<f32>(plane.rgba[offset + static_cast<std::size_t>(c)]) /
255.0f;
}
}
}
}
f32 srgbToLinear(f32 value) {
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
}
f32 linearToSrgb(f32 value) {
return value <= 0.0031308f ? value * 12.92f : 1.055f * std::pow(value, 1.0f / 2.4f) - 0.055f;
}
f32 luminance(const f32 rgb[3]) {
return 0.2126f * rgb[0] + 0.7152f * rgb[1] + 0.0722f * rgb[2];
}
void colorAt(const ColorInput& input, const Plane& plane, f32 u, f32 v, f32 out[3]) {
f32 texel[4] = {input.constant[0], input.constant[1], input.constant[2], 0.0f};
if (input.present() && !plane.empty()) {
sample(plane, u, v, texel);
}
if (input.srgb) {
for (i32 c = 0; c < 3; ++c) {
texel[c] = srgbToLinear(texel[c]);
}
}
const f32 splat =
input.splat.has_value() ? texel[static_cast<u32>(*input.splat)] : 0.0f;
for (i32 c = 0; c < 3; ++c) {
const f32 value = input.splat.has_value() ? splat : texel[c];
out[c] = value * input.scale[c] + input.bias;
}
}
f32 colorWithAlphaAt(const ColorInput& input, const Plane& plane, f32 u, f32 v, f32 out[3]) {
colorAt(input, plane, u, v, out);
if (!input.present() || plane.empty()) {
return 1.0f;
}
f32 texel[4];
sample(plane, u, v, texel);
return texel[3];
}
void foldDecal(const ColorInput& decal, const Plane& plane, DecalOp op, f32 u, f32 v,
f32 albedo[3]) {
if (!decal.present() || plane.empty()) {
return;
}
f32 d[3];
const f32 a = colorWithAlphaAt(decal, plane, u, v, d);
switch (op) {
case DecalOp::Mod:
for (i32 c = 0; c < 3; ++c) {
albedo[c] *= d[c];
}
break;
case DecalOp::Mod2x:
for (i32 c = 0; c < 3; ++c) {
albedo[c] *= d[c] * 2.0f;
}
break;
case DecalOp::AddScaled:
for (i32 c = 0; c < 3; ++c) {
albedo[c] += d[c] * a;
}
break;
case DecalOp::Add:
for (i32 c = 0; c < 3; ++c) {
albedo[c] += d[c];
}
break;
case DecalOp::Lerp:
for (i32 c = 0; c < 3; ++c) {
albedo[c] += (d[c] - albedo[c]) * a;
}
break;
}
}
u8 quantise(f32 value) {
return static_cast<u8>(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f));
}
f32 scalarAt(const ScalarInput& input, const Plane& plane, f32 u, f32 v) {
f32 value = input.constant;
if (input.present() && !plane.empty()) {
f32 texel[4];
sample(plane, u, v, texel);
if (input.srgb) {
for (i32 c = 0; c < 3; ++c) {
texel[c] = srgbToLinear(texel[c]);
}
}
value = input.luminance ? luminance(texel) : texel[static_cast<u32>(input.channel)];
if (input.alphaWeighted) {
value *= texel[3];
}
}
value *= input.scale;
if (input.invert) {
value = 1.0f - value;
}
return value * input.postScale + input.bias;
}
f32 teamAt(const ScalarInput& primary, const Plane& primaryPlane, const ScalarInput& secondary,
const Plane& secondaryPlane, f32 u, f32 v) {
return std::max(scalarAt(primary, primaryPlane, u, v),
scalarAt(secondary, secondaryPlane, u, v));
}
void teamAlbedoAt(const f32 albedo[3], f32 team, const f32 teamColor[3], f32 out[3]) {
const f32 keep = 1.0f - team;
for (i32 c = 0; c < 3; ++c) {
out[c] = teamColor[c] + (albedo[c] - teamColor[c]) * keep;
}
}
struct Reflectance {
f32 f0[3] = {0.0f, 0.0f, 0.0f};
f32 exponent = 20.0f;
f32 roughness = 0.5f;
f32 env = 0.0f;
bool envModulates = false;
};
struct MetalSplit {
f32 gain = 1.0f;
f32 metallic = 0.0f;
};
MetalSplit metalSplit(f32 albedoLum, f32 specLum) {
MetalSplit split;
const f32 total = albedoLum + specLum;
if (total <= 1e-6f) {
return split;
}
split.metallic = specLum / total;
split.gain = albedoLum > 1e-4f ? total / albedoLum : 0.0f;
return split;
}
Reflectance reflectanceAt(const SpecularReflectance& source, const Plane& specular,
const Plane& exponentScale, const Plane& envMask, f32 u, f32 v) {
Reflectance out;
const f32 g = scalarAt(source.exponentScale, exponentScale, u, v);
const bool perceptualGloss = source.simulateRoughness && source.exponentScale.present();
out.exponent = std::max(1.0f, source.exponent * g * g);
out.roughness = perceptualGloss ? std::clamp(1.0f - g, 0.0f, 1.0f)
: RoughnessFromExponent(out.exponent);
const bool anySpecular = source.specular.present() ||
source.specular.constant[0] != 0.0f ||
source.specular.constant[1] != 0.0f ||
source.specular.constant[2] != 0.0f || source.specular.bias != 0.0f;
if (anySpecular) {
f32 spec[3];
colorAt(source.specular, specular, u, v, spec);
const f32 scale = source.factor * ReflectanceScale(out.exponent, source.energyConserving);
for (i32 c = 0; c < 3; ++c) {
out.f0[c] = std::max(0.0f, spec[c]) * scale;
}
}
if (source.envReflectance > 0.0f) {
out.env = source.envReflectance * std::max(0.0f, scalarAt(source.envMask, envMask, u, v));
out.envModulates = source.envModulates;
if (!out.envModulates) {
for (i32 c = 0; c < 3; ++c) {
out.f0[c] += out.env;
}
}
if (!perceptualGloss && out.env > 1e-3f) {
out.roughness = std::min(out.roughness, source.envRoughnessCap);
}
}
return out;
}
void modulatedAlbedo(const Reflectance& reflectance, const f32 albedo[3], f32 out[3]) {
for (i32 c = 0; c < 3; ++c) {
out[c] = std::max(0.0f, albedo[c]) * reflectance.env;
}
}
}
f32 RoughnessFromExponent(f32 exponent) {
const f32 n = std::max(1.0f, exponent);
return std::clamp(std::pow(2.0f / (n + 2.0f), 0.25f), 0.0f, 1.0f);
}
f32 ExponentFromRoughness(f32 roughness) {
const f32 r = std::clamp(roughness, 0.05f, 1.0f);
const f32 alpha2 = r * r * r * r;
return std::clamp(2.0f / alpha2 - 2.0f, 1.0f, 4096.0f);
}
f32 GlossFromRoughness(f32 roughness) {
return 1.0f - std::clamp(roughness, 0.0f, 1.0f);
}
f32 GlossCeilingExponent(f32 roughness) {
const f32 gloss = std::max(GlossFromRoughness(roughness), 0.05f);
return std::clamp(ExponentFromRoughness(roughness) / (gloss * gloss), 20.0f, 2048.0f);
}
f32 ReflectanceScale(f32 exponent, bool energyConserving) {
const f32 n = std::max(1.0f, exponent);
f32 dim = 1.0f;
if (energyConserving) {
const f32 p = std::clamp(n, 1.0f, 512.0f);
dim = std::clamp(-0.000004444f * p * p + 0.004333f * p + 0.0020834f, 0.0f, 1.0f);
}
const f32 normalisation = 8.0f / (n + 2.0f);
return dim * normalisation;
}
std::optional<Texture> BakeOrm(const OrmRecipe& recipe) {
const Plane specular = decode(recipe.reflectance.specular.texture);
const Plane exponentScale = decode(recipe.reflectance.exponentScale.texture);
const Plane envMask = decode(recipe.reflectance.envMask.texture);
const Plane occlusion = decode(recipe.occlusion.texture);
const Plane teamMask = decode(recipe.teamMask.texture);
const Plane teamMaskAlt = decode(recipe.teamMaskAlt.texture);
const Plane baseColor = decode(recipe.baseColor.texture);
const Plane decal = decode(recipe.decal.texture);
u32 width = recipe.width;
u32 height = recipe.height;
if (width == 0 || height == 0) {
for (const Plane* plane :
{&specular, &exponentScale, &envMask, &occlusion, &teamMask, &teamMaskAlt}) {
width = std::max(width, plane->width);
height = std::max(height, plane->height);
}
}
if (width == 0 || height == 0) {
const bool constantSignal =
recipe.teamMask.constant > 0.0f || recipe.teamMaskAlt.constant > 0.0f ||
recipe.reflectance.specular.constant[0] > 0.0f ||
recipe.reflectance.specular.constant[1] > 0.0f ||
recipe.reflectance.specular.constant[2] > 0.0f ||
recipe.reflectance.specular.bias > 0.0f || recipe.reflectance.envReflectance > 0.0f;
if (!constantSignal) {
return std::nullopt;
}
width = height = 4;
}
Texture out = Texture::create2D(PixelFormat::RGBA8, width, height, 1);
out.setSrgb(false);
out.setKind(TextureKind::Multikind);
out.setChannelKind(Channel::R, TextureKind::AmbientOcclusion);
out.setChannelKind(Channel::G, TextureKind::Roughness);
out.setChannelKind(Channel::B, TextureKind::Metalness);
out.setChannelKind(Channel::A, TextureKind::AlphaMask);
const std::span<u8> pixels = out.mipData(0);
for (u32 y = 0; y < height; ++y) {
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(height);
for (u32 x = 0; x < width; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(width);
const Reflectance reflectance =
reflectanceAt(recipe.reflectance, specular, exponentScale, envMask, u, v);
const f32 roughness = reflectance.roughness;
f32 share = teamAt(recipe.teamMask, teamMask, recipe.teamMaskAlt, teamMaskAlt, u, v);
f32 metallic = 0.0f;
if (recipe.baseColor.present() && !baseColor.empty()) {
f32 albedo[3];
colorAt(recipe.baseColor, baseColor, u, v, albedo);
foldDecal(recipe.decal, decal, recipe.decalOp, u, v, albedo);
f32 teamAlbedo[3];
teamAlbedoAt(albedo, share, recipe.teamColor, teamAlbedo);
if (reflectance.envModulates) {
f32 metal[3];
modulatedAlbedo(reflectance, teamAlbedo, metal);
metallic = luminance(metal) > 1e-6f ? 1.0f : 0.0f;
} else {
metallic = metalSplit(std::max(0.0f, luminance(teamAlbedo)),
luminance(reflectance.f0))
.metallic;
}
const f32 paint = (1.0f - share) * luminance(albedo) * 0.8f;
share = share + paint > 1e-5f ? share / (share + paint) : 0.0f;
}
const std::size_t offset =
(static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4;
pixels[offset + 0] = quantise(scalarAt(recipe.occlusion, occlusion, u, v));
pixels[offset + 1] = quantise(roughness);
pixels[offset + 2] = quantise(metallic);
pixels[offset + 3] = quantise(share);
}
}
return out;
}
std::optional<Texture> BakeBaseColor(const BaseColorRecipe& recipe) {
const Plane baseColor = decode(recipe.baseColor.texture);
if (baseColor.empty()) {
return std::nullopt;
}
const Plane teamMask = decode(recipe.teamMask.texture);
const Plane teamMaskAlt = decode(recipe.teamMaskAlt.texture);
const Plane decal = decode(recipe.decal.texture);
const Plane coverage1 = decode(recipe.coverage1.texture);
const Plane coverage2 = decode(recipe.coverage2.texture);
const Plane specular = decode(recipe.reflectance.specular.texture);
const Plane exponentScale = decode(recipe.reflectance.exponentScale.texture);
const Plane envMask = decode(recipe.reflectance.envMask.texture);
u32 width = baseColor.width;
u32 height = baseColor.height;
for (const Plane* plane : {&coverage1, &coverage2}) {
width = std::max(width, plane->width);
height = std::max(height, plane->height);
}
Texture out = Texture::create2D(PixelFormat::RGBA8, width, height, 1);
out.setSrgb(recipe.baseColor.srgb);
out.setKind(TextureKind::Multikind);
out.setChannelKind(Channel::R, TextureKind::Diffuse);
out.setChannelKind(Channel::G, TextureKind::Diffuse);
out.setChannelKind(Channel::B, TextureKind::Diffuse);
out.setChannelKind(Channel::A, recipe.coverageCutoff > 0.0f ? TextureKind::BinaryMask
: TextureKind::AlphaMask);
const std::span<u8> pixels = out.mipData(0);
for (u32 y = 0; y < height; ++y) {
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(height);
for (u32 x = 0; x < width; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(width);
const f32 team =
teamAt(recipe.teamMask, teamMask, recipe.teamMaskAlt, teamMaskAlt, u, v);
f32 albedo[3];
colorAt(recipe.baseColor, baseColor, u, v, albedo);
foldDecal(recipe.decal, decal, recipe.decalOp, u, v, albedo);
f32 mixed[3];
teamAlbedoAt(albedo, team, recipe.teamColor, mixed);
const Reflectance reflectance =
reflectanceAt(recipe.reflectance, specular, exponentScale, envMask, u, v);
f32 written[3];
if (reflectance.envModulates) {
modulatedAlbedo(reflectance, mixed, written);
} else {
const MetalSplit split =
metalSplit(std::max(0.0f, luminance(mixed)), luminance(reflectance.f0));
for (i32 c = 0; c < 3; ++c) {
written[c] = split.gain > 0.0f ? mixed[c] * split.gain
: mixed[c] + reflectance.f0[c];
}
}
const std::size_t offset =
(static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4;
for (i32 c = 0; c < 3; ++c) {
const f32 value = std::clamp(written[c], 0.0f, 1.0f);
pixels[offset + static_cast<std::size_t>(c)] =
quantise(recipe.baseColor.srgb ? linearToSrgb(value) : value);
}
f32 coverage = scalarAt(recipe.coverage1, coverage1, u, v) *
scalarAt(recipe.coverage2, coverage2, u, v);
if (recipe.coverageCutoff > 0.0f) {
coverage = coverage >= recipe.coverageCutoff ? 1.0f : 0.0f;
}
pixels[offset + 3] = quantise(coverage);
}
}
return out;
}
f32 TeamReplaceFromBlend(const f32 albedo[3], f32 weight, bool srgb, f32 out[3]) {
f32 base[3];
f32 peak = 0.0f;
for (i32 c = 0; c < 3; ++c) {
base[c] = std::clamp(albedo[c], 0.0f, 1.0f);
if (srgb) {
base[c] = srgbToLinear(base[c]);
}
peak = std::max(peak, base[c]);
}
const f32 onset = std::sqrt(std::clamp(weight, 0.0f, 1.0f));
const f32 alpha = 1.0f - onset * peak;
for (i32 c = 0; c < 3; ++c) {
f32 value = alpha > 1e-4f ? (1.0f - onset) * base[c] / alpha : base[c];
value = std::clamp(value, 0.0f, 1.0f);
out[c] = srgb ? linearToSrgb(value) : value;
}
return alpha;
}
void DiffuseFromMetalness(const f32 albedo[3], f32 metalness, bool srgb, f32 out[3]) {
const f32 keep = 1.0f - std::clamp(metalness, 0.0f, 1.0f);
for (i32 c = 0; c < 3; ++c) {
const f32 value = std::clamp(albedo[c], 0.0f, 1.0f);
out[c] = srgb ? linearToSrgb(srgbToLinear(value) * keep) : value * keep;
}
}
std::optional<Texture> BakeEmissiveSum(const ColorInput& first, bool firstWeightByAlpha,
const ColorInput& second, bool secondWeightByAlpha) {
const Plane a = decode(first.texture);
const Plane b = decode(second.texture);
const u32 width = std::max(a.width, b.width);
const u32 height = std::max(a.height, b.height);
if (width == 0 || height == 0) {
return std::nullopt;
}
Texture out = Texture::create2D(PixelFormat::RGBA8, width, height, 1);
out.setSrgb(first.srgb || second.srgb);
out.setKind(TextureKind::Emissive);
const bool srgb = first.srgb || second.srgb;
const std::span<u8> pixels = out.mipData(0);
for (u32 y = 0; y < height; ++y) {
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(height);
for (u32 x = 0; x < width; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(width);
f32 ca[3];
const f32 aa = colorWithAlphaAt(first, a, u, v, ca);
f32 cb[3];
const f32 ab = colorWithAlphaAt(second, b, u, v, cb);
const f32 wa = firstWeightByAlpha ? aa : 1.0f;
const f32 wb = secondWeightByAlpha ? ab : 1.0f;
const std::size_t offset =
(static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4;
for (i32 c = 0; c < 3; ++c) {
const f32 value = std::clamp(ca[c] * wa + cb[c] * wb, 0.0f, 1.0f);
pixels[offset + static_cast<std::size_t>(c)] =
quantise(srgb ? linearToSrgb(value) : value);
}
pixels[offset + 3] = 255;
}
}
return out;
}
std::optional<Texture> ConvertNormalXInAlpha(const Texture& source,
const NormalRestatement& options) {
if (source.width() == 0 || source.height() == 0) {
return std::nullopt;
}
Texture out = source.copyAsFormat(PixelFormat::RGBA8);
const std::span<u8> pixels = out.mipData(0);
if (pixels.empty()) {
return std::nullopt;
}
Texture moved = Texture::create2D(PixelFormat::RGBA8, out.width(), out.height(), 1);
const std::span<u8> dst = moved.mipData(0);
for (std::size_t i = 0; i + 3 < pixels.size() && i + 3 < dst.size(); i += 4) {
const u8 sourceX = pixels[i + 3]; const u8 sourceY = pixels[i + 1]; const u8 outX = options.swapXY ? sourceY : sourceX;
const u8 outY = options.swapXY ? sourceX : sourceY;
dst[i + 0] = outX;
dst[i + 1] = options.invertY ? static_cast<u8>(255 - outY) : outY;
dst[i + 2] = 0;
dst[i + 3] = 255;
}
moved.setSrgb(false);
moved.setKind(TextureKind::Normal);
moved.expandNormal(Channel::R, Channel::G, Channel::B);
return moved;
}
}