mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-01 01:17:13 +00:00
fix(ugc): transparent bricks drawn see-through in mixed models; dUgcServer in folders by role
- The client puts a shape in its sorted, blended pass only when its NiMaterialProperty alpha is under 0.99999 (ShaderCommon::GetAlphaFlags 0x0109f5a0; the NiAlphaProperty blend flag isn't read); at 1.0 it's drawn solid with blending off. Transparent (and transparent glitter) shapes now get a material with alpha 0.9999, as the S01_Alpha shapes of the game's own brick models (res/BrickModels/ndmade) do; opaque shapes keep 1.0. Models with a transparent brick change; the others are byte for byte the same. - dUgcServer's files move into Bricks/, Model/, Render/, Formats/ and Processing/ (the CMakeLists says what each holds); includes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
723
dUgcServer/Render/UgcHsr.cpp
Normal file
723
dUgcServer/Render/UgcHsr.cpp
Normal file
@@ -0,0 +1,723 @@
|
||||
#include "UgcHsr.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
|
||||
#include "UgcThrottle.h"
|
||||
|
||||
namespace {
|
||||
constexpr float INF = std::numeric_limits<float>::infinity();
|
||||
constexpr float PI = 3.14159265358979f;
|
||||
// Blender's default Glossy bounces (LU Toolbox leaves it)
|
||||
constexpr int GLOSSY_BOUNCES = 4;
|
||||
// Points on one triangle at most (very big triangles get their points further apart)
|
||||
constexpr size_t MAX_POINTS = 4096;
|
||||
|
||||
// PCG32 (O'Neill), seeded per path so every path is the same whatever order they're traced in
|
||||
class Random {
|
||||
public:
|
||||
explicit Random(uint64_t seed) {
|
||||
m_State = 0;
|
||||
Next();
|
||||
m_State += seed;
|
||||
Next();
|
||||
}
|
||||
|
||||
uint32_t Next() {
|
||||
const uint64_t old = m_State;
|
||||
m_State = old * 6364136223846793005ull + 1442695040888963407ull;
|
||||
const auto xorshifted = static_cast<uint32_t>(((old >> 18u) ^ old) >> 27u);
|
||||
const auto rot = static_cast<uint32_t>(old >> 59u);
|
||||
return (xorshifted >> rot) | (xorshifted << ((32u - rot) & 31u));
|
||||
}
|
||||
|
||||
// In [0, 1)
|
||||
float Float() { return static_cast<float>(Next() >> 8) * (1.0f / 16777216.0f); }
|
||||
|
||||
private:
|
||||
uint64_t m_State;
|
||||
};
|
||||
|
||||
uint64_t Mix(uint64_t h) {
|
||||
h ^= h >> 33;
|
||||
h *= 0xff51afd7ed558ccdull;
|
||||
h ^= h >> 33;
|
||||
h *= 0xc4ceb9fe1a85ec53ull;
|
||||
h ^= h >> 33;
|
||||
return h;
|
||||
}
|
||||
|
||||
uint64_t PathSeed(uint64_t seed, uint64_t triangle, uint64_t point, uint64_t sample) {
|
||||
return Mix(Mix(Mix(seed ^ 0x9E3779B97F4A7C15ull) ^ triangle) ^ (point << 16 | sample));
|
||||
}
|
||||
|
||||
// Directions around a unit normal (Duff et al., "Building an Orthonormal Basis, Revisited")
|
||||
void Basis(const glm::vec3& n, glm::vec3& t, glm::vec3& b) {
|
||||
const float sign = std::copysign(1.0f, n.z);
|
||||
const float a = -1.0f / (sign + n.z);
|
||||
const float c = n.x * n.y * a;
|
||||
t = glm::vec3(1.0f + sign * n.x * n.x * a, sign * c, -sign * n.x);
|
||||
b = glm::vec3(c, sign + n.y * n.y * a, -n.y);
|
||||
}
|
||||
|
||||
glm::vec3 CosineHemisphere(const glm::vec3& n, float u, float v) {
|
||||
glm::vec3 t, b;
|
||||
Basis(n, t, b);
|
||||
const float r = std::sqrt(u), phi = 2.0f * PI * v;
|
||||
return t * (r * std::cos(phi)) + b * (r * std::sin(phi)) + n * std::sqrt(std::max(0.0f, 1.0f - u));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles' ensure_valid_reflection, which the Principled BSDF applies to its normal: N turned towards Ng just
|
||||
* enough that the mirror reflection of I (towards where the light goes) stays above the surface.
|
||||
*/
|
||||
glm::vec3 EnsureValidReflection(const glm::vec3& ng, const glm::vec3& i, const glm::vec3& n) {
|
||||
const auto r = 2.0f * glm::dot(n, i) * n - i;
|
||||
const float threshold = std::min(0.9f * glm::dot(ng, i), 0.01f);
|
||||
if (glm::dot(ng, r) >= threshold) return n;
|
||||
const float ndotng = glm::dot(n, ng);
|
||||
const auto x = glm::normalize(n - ndotng * ng);
|
||||
const float ix = glm::dot(i, x), iz = glm::dot(i, ng);
|
||||
const float ix2 = ix * ix, iz2 = iz * iz;
|
||||
const float a = ix2 + iz2;
|
||||
const float b = std::sqrt(std::max(ix2 * (a - threshold * threshold), 0.0f));
|
||||
const float c = iz * threshold + a;
|
||||
const float fac = 0.5f / a;
|
||||
const float n1z2 = fac * (b + c), n2z2 = fac * (-b + c);
|
||||
bool valid1 = n1z2 > 1e-5f && n1z2 <= 1.0f + 1e-5f;
|
||||
bool valid2 = n2z2 > 1e-5f && n2z2 <= 1.0f + 1e-5f;
|
||||
const auto root = [](float v) { return std::sqrt(std::max(v, 0.0f)); };
|
||||
glm::vec2 chosen;
|
||||
if (valid1 && valid2) {
|
||||
const glm::vec2 n1(root(1.0f - n1z2), root(n1z2)), n2(root(1.0f - n2z2), root(n2z2));
|
||||
const float r1 = 2.0f * (n1.x * ix + n1.y * iz) * n1.y - iz;
|
||||
const float r2 = 2.0f * (n2.x * ix + n2.y * iz) * n2.y - iz;
|
||||
valid1 = r1 >= 1e-5f;
|
||||
valid2 = r2 >= 1e-5f;
|
||||
if (valid1 && valid2) chosen = r1 < r2 ? n1 : n2;
|
||||
else chosen = r1 > r2 ? n1 : n2;
|
||||
} else if (valid1 || valid2) {
|
||||
const float z2 = valid1 ? n1z2 : n2z2;
|
||||
chosen = glm::vec2(root(1.0f - z2), root(z2));
|
||||
} else {
|
||||
return ng;
|
||||
}
|
||||
return chosen.x * x + chosen.y * ng;
|
||||
}
|
||||
|
||||
float SchlickFresnel(float u) {
|
||||
const float m = std::clamp(1.0f - u, 0.0f, 1.0f);
|
||||
const float m2 = m * m;
|
||||
return m2 * m2 * m;
|
||||
}
|
||||
|
||||
float FresnelDielectricCos(float cosi, float eta) {
|
||||
const float c = std::abs(cosi);
|
||||
float g = eta * eta - 1.0f + c * c;
|
||||
if (!(g > 0.0f)) return 1.0f;
|
||||
g = std::sqrt(g);
|
||||
const float a = (g - c) / (g + c);
|
||||
const float b = (c * (g + c) - 1.0f) / (c * (g - c) + 1.0f);
|
||||
return 0.5f * a * a * (1.0f + b * b);
|
||||
}
|
||||
|
||||
/**
|
||||
* The bake material: a new material's Principled BSDF (Blender 3.1: base color 0.8, specular 0.5, roughness 0.5,
|
||||
* GGX) as Cycles 3.1 samples it, a diffuse closure (Burley, with retro-reflection) and a specular one (GGX with a
|
||||
* dielectric Fresnel, IOR 1.5) picked by their sample weights. Only the directions a path takes and its throughput
|
||||
* (for the Russian roulette) matter here.
|
||||
*/
|
||||
class Principled {
|
||||
public:
|
||||
static constexpr float BASE = 0.8f; // Base Color
|
||||
static constexpr float ROUGHNESS = 0.5f; // Roughness
|
||||
static constexpr float IOR = 1.5f; // (2 / (1 - sqrt(0.08 * Specular 0.5))) - 1
|
||||
static constexpr float CSPEC0 = 0.04f; // Specular 0.5 * 0.08
|
||||
|
||||
// n: the material's normal (after EnsureValidReflection), i: towards where the path came from, ng: the
|
||||
// geometric normal on the same side, sn: the shading normal. `first`: the bake point (Cycles' defensive
|
||||
// sampling there); minRayPdf: the smallest bounce pdf so far (Cycles' Filter Glossy 1 blurs the specular)
|
||||
Principled(const glm::vec3& n, const glm::vec3& i, const glm::vec3& ng, const glm::vec3& sn, bool first, float minRayPdf)
|
||||
: m_N(n), m_I(i), m_Ng(ng), m_Sn(sn) {
|
||||
m_F0 = FresnelDielectricCos(1.0f, IOR);
|
||||
m_WeightDiffuse = BASE;
|
||||
m_WeightSpecular = Fresnel(m_I, m_N); // the closure's weight 1 times its Fresnel color's average
|
||||
if (first) {
|
||||
const float sum = m_WeightDiffuse + m_WeightSpecular;
|
||||
m_WeightDiffuse = std::max(m_WeightDiffuse, 0.125f * sum);
|
||||
m_WeightSpecular = std::max(m_WeightSpecular, 0.125f * sum);
|
||||
}
|
||||
m_Alpha = std::clamp(ROUGHNESS * ROUGHNESS, 0.0f, 1.0f);
|
||||
if (minRayPdf < 1.0f) m_Alpha = std::max(std::sqrt(1.0f - minRayPdf) * 0.5f, m_Alpha);
|
||||
}
|
||||
|
||||
struct Sample {
|
||||
glm::vec3 direction{};
|
||||
float pdf{}; // of the mixture; 0: nothing sampled, the path ends
|
||||
float throughput{}; // eval / pdf
|
||||
bool glossy{};
|
||||
};
|
||||
|
||||
Sample Draw(Random& random) const {
|
||||
Sample sample;
|
||||
const float pick = random.Float() * (m_WeightDiffuse + m_WeightSpecular);
|
||||
const float u = random.Float(), v = random.Float();
|
||||
float pdfDiffuse = 0.0f, pdfSpecular = 0.0f;
|
||||
float eval = 0.0f;
|
||||
if (pick < m_WeightDiffuse) {
|
||||
sample.direction = CosineHemisphere(m_N, u, v);
|
||||
if (!(glm::dot(m_Ng, sample.direction) > 0.0f)) return sample;
|
||||
eval = EvalDiffuse(sample.direction, pdfDiffuse);
|
||||
if (!(pdfDiffuse > 0.0f) || !(eval > 0.0f)) return sample;
|
||||
if (!Transmission(sample.direction)) eval += EvalSpecular(sample.direction, pdfSpecular);
|
||||
} else {
|
||||
sample.glossy = true;
|
||||
if (!SampleSpecular(u, v, sample.direction, eval, pdfSpecular)) return sample;
|
||||
if (!Transmission(sample.direction)) eval += EvalDiffuse(sample.direction, pdfDiffuse);
|
||||
}
|
||||
sample.pdf = (pdfDiffuse * m_WeightDiffuse + pdfSpecular * m_WeightSpecular) / (m_WeightDiffuse + m_WeightSpecular);
|
||||
sample.throughput = sample.pdf > 0.0f ? eval / sample.pdf : 0.0f;
|
||||
return sample;
|
||||
}
|
||||
|
||||
private:
|
||||
// Cycles decides reflection or transmission by the shading normal; both closures only reflect
|
||||
bool Transmission(const glm::vec3& l) const { return glm::dot(m_Sn, l) < 0.0f; }
|
||||
|
||||
float Fresnel(const glm::vec3& l, const glm::vec3& h) const {
|
||||
const float fh = (FresnelDielectricCos(glm::dot(l, h), IOR) - m_F0) / (1.0f - m_F0);
|
||||
return CSPEC0 * (1.0f - fh) + fh;
|
||||
}
|
||||
|
||||
// bsdf_principled_diffuse_eval_reflect times the closure weight
|
||||
float EvalDiffuse(const glm::vec3& l, float& pdf) const {
|
||||
const float nl = glm::dot(m_N, l);
|
||||
if (!(nl > 0.0f)) {
|
||||
pdf = 0.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
pdf = nl / PI;
|
||||
const float nv = glm::dot(m_N, m_I);
|
||||
const float fv = SchlickFresnel(nv), fl = SchlickFresnel(nl);
|
||||
float f = (1.0f - 0.5f * fv) * (1.0f - 0.5f * fl);
|
||||
const float rr = ROUGHNESS * (glm::dot(l, m_I) + 1.0f);
|
||||
f += rr * (fl + fv + fl * fv * (rr - 1.0f));
|
||||
return BASE * nl / PI * f;
|
||||
}
|
||||
|
||||
// bsdf_microfacet_ggx_eval_reflect (isotropic, Fresnel) times the closure weight 1
|
||||
float EvalSpecular(const glm::vec3& l, float& pdf) const {
|
||||
pdf = 0.0f;
|
||||
const float cosNO = glm::dot(m_N, m_I), cosNI = glm::dot(m_N, l);
|
||||
if (!(cosNI > 0.0f && cosNO > 0.0f) || m_Alpha * m_Alpha <= 1e-7f) return 0.0f;
|
||||
const auto m = glm::normalize(l + m_I);
|
||||
const float alpha2 = m_Alpha * m_Alpha;
|
||||
const float cosThetaM = glm::dot(m_N, m);
|
||||
const float cosThetaM2 = cosThetaM * cosThetaM;
|
||||
const float tanThetaM2 = (1.0f - cosThetaM2) / cosThetaM2;
|
||||
const float d = alpha2 / (PI * cosThetaM2 * cosThetaM2 * (alpha2 + tanThetaM2) * (alpha2 + tanThetaM2));
|
||||
const float g1o = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNO * cosNO) / (cosNO * cosNO))));
|
||||
const float g1i = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNI * cosNI) / (cosNI * cosNI))));
|
||||
const float common = d * 0.25f / cosNO;
|
||||
pdf = g1o * common;
|
||||
return Fresnel(l, m) * g1o * g1i * common;
|
||||
}
|
||||
|
||||
// bsdf_microfacet_ggx_sample (isotropic): a visible normal (Heitz and d'Eon), the view reflected in it
|
||||
bool SampleSpecular(float randu, float randv, glm::vec3& l, float& eval, float& pdf) const {
|
||||
const float cosNO = glm::dot(m_N, m_I);
|
||||
if (!(cosNO > 0.0f)) return false;
|
||||
glm::vec3 x, y;
|
||||
Basis(m_N, x, y);
|
||||
// Stretch the view, sample the slopes, rotate and unstretch
|
||||
auto local = glm::normalize(glm::vec3(m_Alpha * glm::dot(x, m_I), m_Alpha * glm::dot(y, m_I), cosNO));
|
||||
float costheta = 1.0f, sintheta = 0.0f, cosphi = 1.0f, sinphi = 0.0f;
|
||||
if (local.z < 0.99999f) {
|
||||
costheta = local.z;
|
||||
sintheta = std::sqrt(std::max(0.0f, 1.0f - costheta * costheta));
|
||||
cosphi = local.x / sintheta;
|
||||
sinphi = local.y / sintheta;
|
||||
}
|
||||
float slopeX, slopeY, g1o;
|
||||
if (costheta >= 0.99999f) {
|
||||
const float r = std::sqrt(randu / (1.0f - randu));
|
||||
const float phi = 2.0f * PI * randv;
|
||||
slopeX = r * std::cos(phi);
|
||||
slopeY = r * std::sin(phi);
|
||||
g1o = 1.0f;
|
||||
} else {
|
||||
const float tanThetaI = sintheta / costheta;
|
||||
const float g1Inv = 0.5f * (1.0f + std::sqrt(std::max(0.0f, 1.0f + tanThetaI * tanThetaI)));
|
||||
g1o = 1.0f / g1Inv;
|
||||
const float a = 2.0f * randu * g1Inv - 1.0f;
|
||||
const float aa = a * a;
|
||||
const float tmp = 1.0f / (aa - 1.0f);
|
||||
const float b = tanThetaI, bb = b * b;
|
||||
const float dd = std::sqrt(std::max(0.0f, bb * (tmp * tmp) - (aa - bb) * tmp));
|
||||
const float x1 = b * tmp - dd, x2 = b * tmp + dd;
|
||||
slopeX = (a < 0.0f || x2 * tanThetaI > 1.0f) ? x1 : x2;
|
||||
float s;
|
||||
if (randv > 0.5f) {
|
||||
s = 1.0f;
|
||||
randv = 2.0f * (randv - 0.5f);
|
||||
} else {
|
||||
s = -1.0f;
|
||||
randv = 2.0f * (0.5f - randv);
|
||||
}
|
||||
const float z = (randv * (randv * (randv * 0.27385f - 0.73369f) + 0.46341f)) / (randv * (randv * (randv * 0.093073f + 0.309420f) - 1.0f) + 0.597999f);
|
||||
slopeY = s * z * std::sqrt(std::max(0.0f, 1.0f + slopeX * slopeX));
|
||||
}
|
||||
const float rotated = cosphi * slopeX - sinphi * slopeY;
|
||||
slopeY = sinphi * slopeX + cosphi * slopeY;
|
||||
slopeX = rotated * m_Alpha;
|
||||
slopeY *= m_Alpha;
|
||||
const auto localM = glm::normalize(glm::vec3(-slopeX, -slopeY, 1.0f));
|
||||
const auto m = x * localM.x + y * localM.y + m_N * localM.z;
|
||||
const float cosMO = glm::dot(m, m_I);
|
||||
if (!(cosMO > 0.0f)) return false;
|
||||
l = 2.0f * cosMO * m - m_I;
|
||||
if (!(glm::dot(m_Ng, l) > 0.0f)) return false;
|
||||
const float alpha2 = m_Alpha * m_Alpha;
|
||||
const float cosThetaM2 = localM.z * localM.z;
|
||||
const float tanThetaM2 = 1.0f / cosThetaM2 - 1.0f;
|
||||
const float d = alpha2 / (PI * cosThetaM2 * cosThetaM2 * (alpha2 + tanThetaM2) * (alpha2 + tanThetaM2));
|
||||
const float cosNI = glm::dot(m_N, l);
|
||||
const float g1i = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNI * cosNI) / (cosNI * cosNI))));
|
||||
const float common = g1o * d * 0.25f / cosNO;
|
||||
pdf = common;
|
||||
eval = g1i * common * Fresnel(l, m);
|
||||
return pdf > 0.0f && eval > 0.0f;
|
||||
}
|
||||
|
||||
glm::vec3 m_N, m_I, m_Ng, m_Sn;
|
||||
float m_F0{};
|
||||
float m_WeightDiffuse{}, m_WeightSpecular{};
|
||||
float m_Alpha{};
|
||||
};
|
||||
|
||||
struct Hit {
|
||||
float t{ INF };
|
||||
uint32_t triangle{ UINT32_MAX };
|
||||
float u{}, v{}; // weights of the triangle's second and third vertex
|
||||
};
|
||||
|
||||
/**
|
||||
* A bounding volume hierarchy (binned surface area heuristic) over a mesh's triangles, for the paths' rays: the
|
||||
* nearest hit. A ray never hits the triangle it leaves (`skip`), as in Cycles.
|
||||
*/
|
||||
class Bvh {
|
||||
public:
|
||||
explicit Bvh(const UgcModel::Mesh& mesh) {
|
||||
const size_t count = mesh.TriangleCount();
|
||||
std::vector<uint32_t> order(count);
|
||||
std::iota(order.begin(), order.end(), 0u);
|
||||
std::vector<glm::vec3> lo(count), hi(count), centre(count);
|
||||
for (size_t t = 0; t < count; t++) {
|
||||
const auto& a = mesh.positions[mesh.indices[t * 3]];
|
||||
const auto& b = mesh.positions[mesh.indices[t * 3 + 1]];
|
||||
const auto& c = mesh.positions[mesh.indices[t * 3 + 2]];
|
||||
lo[t] = glm::min(a, glm::min(b, c));
|
||||
hi[t] = glm::max(a, glm::max(b, c));
|
||||
centre[t] = (lo[t] + hi[t]) * 0.5f;
|
||||
}
|
||||
if (count > 0) Build(order, lo, hi, centre);
|
||||
m_Triangles.reserve(count);
|
||||
for (const auto t : order) {
|
||||
const auto& a = mesh.positions[mesh.indices[t * 3]];
|
||||
m_Triangles.push_back({ a, mesh.positions[mesh.indices[t * 3 + 1]] - a, mesh.positions[mesh.indices[t * 3 + 2]] - a, t });
|
||||
}
|
||||
}
|
||||
|
||||
// The nearest triangle along the ray (unit direction) before `maxT`
|
||||
Hit Closest(const glm::vec3& origin, const glm::vec3& direction, uint32_t skip, float maxT = INF) const {
|
||||
Hit hit;
|
||||
hit.t = maxT;
|
||||
if (m_Nodes.empty()) return hit;
|
||||
const auto inverse = Inverse(direction);
|
||||
uint32_t stack[128];
|
||||
int top = 0;
|
||||
uint32_t index = 0;
|
||||
while (true) {
|
||||
const auto& node = m_Nodes[index];
|
||||
if (node.count > 0) {
|
||||
for (uint32_t i = node.first; i < node.first + node.count; i++) Intersect(m_Triangles[i], origin, direction, skip, hit);
|
||||
} else {
|
||||
const uint32_t left = node.first, right = node.first + 1;
|
||||
const float tl = Enter(m_Nodes[left], origin, inverse, hit.t);
|
||||
const float tr = Enter(m_Nodes[right], origin, inverse, hit.t);
|
||||
if (tl <= tr) {
|
||||
if (tr != INF && top < 128) stack[top++] = right;
|
||||
if (tl != INF) { index = left; continue; }
|
||||
} else {
|
||||
if (tl != INF && top < 128) stack[top++] = left;
|
||||
index = right;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Next from the stack, skipping nodes now farther than the nearest hit
|
||||
bool found = false;
|
||||
while (top > 0) {
|
||||
index = stack[--top];
|
||||
if (Enter(m_Nodes[index], origin, inverse, hit.t) != INF) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) break;
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
glm::vec3 min{ INF };
|
||||
uint32_t first{}; // leaf: first triangle; inner: the left child (the right one follows it)
|
||||
glm::vec3 max{ -INF };
|
||||
uint32_t count{}; // triangles, 0 for inner nodes
|
||||
};
|
||||
|
||||
// Where the ray enters the node's box, INF when it misses it before `maxT`
|
||||
static float Enter(const Node& node, const glm::vec3& origin, const glm::vec3& inverse, float maxT) {
|
||||
const auto t0 = (node.min - origin) * inverse;
|
||||
const auto t1 = (node.max - origin) * inverse;
|
||||
const auto near = glm::min(t0, t1), far = glm::max(t0, t1);
|
||||
const float enter = std::max(std::max(near.x, near.y), std::max(near.z, 0.0f));
|
||||
const float exit = std::min(std::min(far.x, far.y), std::min(far.z, maxT));
|
||||
return enter <= exit ? enter : INF;
|
||||
}
|
||||
|
||||
struct Triangle {
|
||||
glm::vec3 a, e1, e2;
|
||||
uint32_t index;
|
||||
};
|
||||
|
||||
static glm::vec3 Inverse(const glm::vec3& d) {
|
||||
const auto safe = [](float x) { return 1.0f / (std::abs(x) > 1e-20f ? x : std::copysign(1e-20f, x)); };
|
||||
return { safe(d.x), safe(d.y), safe(d.z) };
|
||||
}
|
||||
|
||||
// Möller-Trumbore; keeps the hit when it's nearer than hit.t
|
||||
static bool Intersect(const Triangle& tri, const glm::vec3& origin, const glm::vec3& direction, uint32_t skip, Hit& hit) {
|
||||
if (tri.index == skip) return false;
|
||||
const auto p = glm::cross(direction, tri.e2);
|
||||
const float det = glm::dot(tri.e1, p);
|
||||
if (det == 0.0f) return false;
|
||||
const float inv = 1.0f / det;
|
||||
const auto s = origin - tri.a;
|
||||
const float u = glm::dot(s, p) * inv;
|
||||
if (u < 0.0f || u > 1.0f) return false;
|
||||
const auto q = glm::cross(s, tri.e1);
|
||||
const float v = glm::dot(direction, q) * inv;
|
||||
if (v < 0.0f || u + v > 1.0f) return false;
|
||||
const float t = glm::dot(tri.e2, q) * inv;
|
||||
if (!(t > 0.0f) || t >= hit.t) return false;
|
||||
hit.t = t;
|
||||
hit.triangle = tri.index;
|
||||
hit.u = u;
|
||||
hit.v = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Build(std::vector<uint32_t>& order, const std::vector<glm::vec3>& lo, const std::vector<glm::vec3>& hi, const std::vector<glm::vec3>& centre) {
|
||||
constexpr int BINS = 16;
|
||||
constexpr uint32_t LEAF = 4;
|
||||
struct Task { uint32_t node, first, count; };
|
||||
const auto area = [](const glm::vec3& min, const glm::vec3& max) {
|
||||
const auto d = glm::max(max - min, glm::vec3(0.0f));
|
||||
return d.x * d.y + d.y * d.z + d.z * d.x;
|
||||
};
|
||||
m_Nodes.reserve(order.size() * 2 / LEAF + 1);
|
||||
m_Nodes.push_back({});
|
||||
std::vector<Task> tasks{ { 0, 0, static_cast<uint32_t>(order.size()) } };
|
||||
while (!tasks.empty()) {
|
||||
const auto task = tasks.back();
|
||||
tasks.pop_back();
|
||||
Node node;
|
||||
glm::vec3 cmin(INF), cmax(-INF);
|
||||
for (uint32_t i = task.first; i < task.first + task.count; i++) {
|
||||
node.min = glm::min(node.min, lo[order[i]]);
|
||||
node.max = glm::max(node.max, hi[order[i]]);
|
||||
cmin = glm::min(cmin, centre[order[i]]);
|
||||
cmax = glm::max(cmax, centre[order[i]]);
|
||||
}
|
||||
node.first = task.first;
|
||||
node.count = task.count;
|
||||
int bestAxis = -1;
|
||||
int bestSplit = 0;
|
||||
float bestCost = static_cast<float>(task.count) * area(node.min, node.max); // not splitting
|
||||
if (task.count > LEAF) {
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
const float extent = cmax[axis] - cmin[axis];
|
||||
if (!(extent > 0.0f)) continue;
|
||||
struct Bin { glm::vec3 min{ INF }, max{ -INF }; uint32_t count{}; };
|
||||
std::array<Bin, BINS> bins{};
|
||||
const float scale = BINS / extent;
|
||||
for (uint32_t i = task.first; i < task.first + task.count; i++) {
|
||||
const auto t = order[i];
|
||||
const int b = std::min(BINS - 1, static_cast<int>((centre[t][axis] - cmin[axis]) * scale));
|
||||
bins[b].min = glm::min(bins[b].min, lo[t]);
|
||||
bins[b].max = glm::max(bins[b].max, hi[t]);
|
||||
bins[b].count++;
|
||||
}
|
||||
std::array<float, BINS - 1> leftCost{};
|
||||
glm::vec3 lmin(INF), lmax(-INF);
|
||||
uint32_t lcount = 0;
|
||||
for (int b = 0; b < BINS - 1; b++) {
|
||||
lmin = glm::min(lmin, bins[b].min);
|
||||
lmax = glm::max(lmax, bins[b].max);
|
||||
lcount += bins[b].count;
|
||||
leftCost[b] = lcount ? lcount * area(lmin, lmax) : 0.0f;
|
||||
}
|
||||
glm::vec3 rmin(INF), rmax(-INF);
|
||||
uint32_t rcount = 0;
|
||||
for (int b = BINS - 1; b > 0; b--) {
|
||||
rmin = glm::min(rmin, bins[b].min);
|
||||
rmax = glm::max(rmax, bins[b].max);
|
||||
rcount += bins[b].count;
|
||||
const float cost = leftCost[b - 1] + (rcount ? rcount * area(rmin, rmax) : 0.0f);
|
||||
if (rcount > 0 && rcount < task.count && cost < bestCost) {
|
||||
bestCost = cost;
|
||||
bestAxis = axis;
|
||||
bestSplit = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestAxis < 0) {
|
||||
m_Nodes[task.node] = node;
|
||||
continue;
|
||||
}
|
||||
const float extent = cmax[bestAxis] - cmin[bestAxis];
|
||||
const float scale = BINS / extent;
|
||||
auto* begin = order.data() + task.first;
|
||||
auto* middle = std::partition(begin, begin + task.count, [&](uint32_t t) {
|
||||
return std::min(BINS - 1, static_cast<int>((centre[t][bestAxis] - cmin[bestAxis]) * scale)) < bestSplit;
|
||||
});
|
||||
const auto leftCount = static_cast<uint32_t>(middle - begin);
|
||||
node.first = static_cast<uint32_t>(m_Nodes.size());
|
||||
node.count = 0;
|
||||
m_Nodes[task.node] = node;
|
||||
m_Nodes.push_back({});
|
||||
m_Nodes.push_back({});
|
||||
tasks.push_back({ node.first, task.first, leftCount });
|
||||
tasks.push_back({ node.first + 1, task.first + leftCount, task.count - leftCount });
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Node> m_Nodes;
|
||||
std::vector<Triangle> m_Triangles;
|
||||
};
|
||||
|
||||
// LU Toolbox's ground plane: a box 1000 x 1000 x 100 whose top is at LDD y 0, black (a path hitting it ends)
|
||||
float GroundHit(const glm::vec3& origin, const glm::vec3& direction, float maxT) {
|
||||
static const glm::vec3 MIN(-500.0f, -100.0f, -500.0f), MAX(500.0f, 0.0f, 500.0f);
|
||||
float enter = 0.0f, exit = maxT;
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (std::abs(direction[axis]) < 1e-20f) {
|
||||
if (origin[axis] < MIN[axis] || origin[axis] > MAX[axis]) return INF;
|
||||
continue;
|
||||
}
|
||||
const float inv = 1.0f / direction[axis];
|
||||
float t0 = (MIN[axis] - origin[axis]) * inv, t1 = (MAX[axis] - origin[axis]) * inv;
|
||||
if (t0 > t1) std::swap(t0, t1);
|
||||
enter = std::max(enter, t0);
|
||||
exit = std::min(exit, t1);
|
||||
if (enter > exit) return INF;
|
||||
}
|
||||
return enter;
|
||||
}
|
||||
|
||||
class Tracer {
|
||||
public:
|
||||
Tracer(const UgcModel::Mesh& mesh, const UgcHsr::Options& options) : m_Mesh(mesh), m_Bvh(mesh), m_Options(options) {
|
||||
m_Smooth = mesh.normals.size() == mesh.positions.size();
|
||||
}
|
||||
|
||||
// The triangle's normal from its winding (unit; zero when it has no area)
|
||||
glm::vec3 FaceNormal(uint32_t t) const {
|
||||
const auto& a = m_Mesh.positions[m_Mesh.indices[t * 3]];
|
||||
const auto n = glm::cross(m_Mesh.positions[m_Mesh.indices[t * 3 + 1]] - a, m_Mesh.positions[m_Mesh.indices[t * 3 + 2]] - a);
|
||||
const float length = glm::length(n);
|
||||
return length > 0.0f ? n / length : glm::vec3(0.0f);
|
||||
}
|
||||
|
||||
// The vertex normals at barycentric w (Cycles' smooth normal: the face's own when they sum to nothing)
|
||||
glm::vec3 ShadingNormal(uint32_t t, const glm::vec3& w, const glm::vec3& faceNormal) const {
|
||||
if (!m_Smooth) return faceNormal;
|
||||
const auto n = m_Mesh.normals[m_Mesh.indices[t * 3]] * w.x + m_Mesh.normals[m_Mesh.indices[t * 3 + 1]] * w.y + m_Mesh.normals[m_Mesh.indices[t * 3 + 2]] * w.z;
|
||||
const float length = glm::length(n);
|
||||
return length > 0.0f ? n / length : faceNormal;
|
||||
}
|
||||
|
||||
glm::vec3 Position(uint32_t t, const glm::vec3& w) const {
|
||||
return m_Mesh.positions[m_Mesh.indices[t * 3]] * w.x + m_Mesh.positions[m_Mesh.indices[t * 3 + 1]] * w.y + m_Mesh.positions[m_Mesh.indices[t * 3 + 2]] * w.z;
|
||||
}
|
||||
|
||||
/**
|
||||
* One path from point w (barycentric) of triangle t, as a Cycles diffuse bake traces it: whether it reaches the
|
||||
* sky. The path bounces off the model in the directions the bake material draws until a bounce ray hits
|
||||
* nothing, which is the sky. The sky isn't sampled directly: Cycles doesn't sample a world of one flat color as
|
||||
* a light, so only rays that happen to leave count. Up to `bounces` bounces (4 of them glossy), and from the
|
||||
* second bounce on the path may end early (Cycles' Russian roulette: it goes on with probability
|
||||
* sqrt(throughput)).
|
||||
*/
|
||||
bool Escapes(uint32_t t, const glm::vec3& w, Random& random) const {
|
||||
glm::vec3 ng = FaceNormal(t);
|
||||
// The bake looks at the point along its smooth normal (the ray comes from there): that side is lit, and
|
||||
// when the winding faces the other way Cycles treats it as the back of the face (both normals turned)
|
||||
glm::vec3 n = ShadingNormal(t, w, ng);
|
||||
glm::vec3 incoming = n;
|
||||
if (glm::dot(ng, incoming) < 0.0f) {
|
||||
ng = -ng;
|
||||
n = -n;
|
||||
}
|
||||
glm::vec3 p = Position(t, w);
|
||||
uint32_t self = t;
|
||||
float throughput = 1.0f;
|
||||
float minRayPdf = INF;
|
||||
int glossy = 0;
|
||||
for (int bounce = 0;; bounce++) {
|
||||
// The material's normal: the Principled BSDF keeps its mirror reflection above the surface
|
||||
const Principled material(EnsureValidReflection(ng, incoming, n), incoming, ng, n, bounce == 0, minRayPdf);
|
||||
const auto sample = material.Draw(random);
|
||||
// Nothing sampled (below the surface): the path ends
|
||||
if (!(sample.pdf > 0.0f) || !(sample.throughput > 0.0f)) return false;
|
||||
const auto& direction = sample.direction;
|
||||
throughput *= sample.throughput;
|
||||
minRayPdf = std::min(minRayPdf, sample.pdf);
|
||||
const auto hit = m_Bvh.Closest(p, direction, self);
|
||||
if (m_Options.groundPlane && GroundHit(p, direction, hit.t) < hit.t) return false;
|
||||
if (hit.triangle == UINT32_MAX) return true;
|
||||
// Past the bounce limits the next surface doesn't scatter (Cycles: Max Bounces, Glossy 4)
|
||||
if (bounce + 1 > m_Options.bounces) return false;
|
||||
if (sample.glossy && ++glossy > GLOSSY_BOUNCES) return false;
|
||||
if (bounce + 1 >= 2) {
|
||||
const float probability = std::min(std::sqrt(throughput), 1.0f);
|
||||
if (random.Float() >= probability) return false;
|
||||
throughput /= probability;
|
||||
}
|
||||
self = hit.triangle;
|
||||
ng = FaceNormal(self);
|
||||
n = ShadingNormal(self, glm::vec3(1.0f - hit.u - hit.v, hit.u, hit.v), ng);
|
||||
incoming = -direction;
|
||||
// Hit from behind: the back is lit (both normals turned towards the ray)
|
||||
if (glm::dot(ng, incoming) < 0.0f) {
|
||||
ng = -ng;
|
||||
n = -n;
|
||||
}
|
||||
p = p + direction * hit.t;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const UgcModel::Mesh& m_Mesh;
|
||||
Bvh m_Bvh;
|
||||
const UgcHsr::Options& m_Options;
|
||||
bool m_Smooth{};
|
||||
};
|
||||
}
|
||||
|
||||
namespace UgcHsr {
|
||||
std::vector<glm::vec3> SamplePoints(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float spacing, size_t minimum) {
|
||||
// Too small to lay out: the centre and one point towards each corner (the centres of its four halved-side
|
||||
// triangles)
|
||||
const std::vector<glm::vec3> fallback{ { 1.0f / 3, 1.0f / 3, 1.0f / 3 }, { 2.0f / 3, 1.0f / 6, 1.0f / 6 }, { 1.0f / 6, 2.0f / 3, 1.0f / 6 },
|
||||
{ 1.0f / 6, 1.0f / 6, 2.0f / 3 } };
|
||||
const std::array<glm::vec3, 3> corners{ a, b, c };
|
||||
// The longest side from corner `first` to the next; the third is the apex
|
||||
int first = 0;
|
||||
float longest = -1.0f;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const float length = glm::length(corners[(i + 1) % 3] - corners[i]);
|
||||
if (length > longest) {
|
||||
longest = length;
|
||||
first = i;
|
||||
}
|
||||
}
|
||||
const float area = 0.5f * glm::length(glm::cross(b - a, c - a));
|
||||
if (!(longest > 0.0f) || !(area > 0.0f) || !(spacing > 0.0f) || !std::isfinite(area)) return fallback;
|
||||
minimum = std::min(minimum, MAX_POINTS);
|
||||
// Very big triangles: points further apart, at most MAX_POINTS
|
||||
spacing = std::max(spacing, std::sqrt(area / static_cast<float>(MAX_POINTS)));
|
||||
const float height = 2.0f * area / longest;
|
||||
std::vector<glm::vec3> points;
|
||||
for (int attempt = 0; attempt < 16; attempt++) {
|
||||
points.clear();
|
||||
const int along = std::max(1, static_cast<int>(std::ceil(longest / spacing)));
|
||||
const int rows = std::max(1, static_cast<int>(std::ceil(height / spacing)));
|
||||
for (int row = 0; row < rows; row++) {
|
||||
// v: from the longest side (0) to the apex (1); the row is (1 - v) of the side's length
|
||||
const float v = (row + 0.5f) / rows;
|
||||
const int count = std::max(1, static_cast<int>(std::lround(along * (1.0f - v))));
|
||||
for (int j = 0; j < count; j++) {
|
||||
const float u = (j + 0.5f) / count;
|
||||
glm::vec3 w{};
|
||||
w[first] = (1.0f - u) * (1.0f - v);
|
||||
w[(first + 1) % 3] = u * (1.0f - v);
|
||||
w[(first + 2) % 3] = v;
|
||||
points.push_back(w);
|
||||
}
|
||||
}
|
||||
if (points.size() >= minimum) break;
|
||||
// Fewer than the minimum: closer together
|
||||
spacing *= 0.95f * std::sqrt(static_cast<float>(points.size()) / static_cast<float>(minimum));
|
||||
}
|
||||
return points.size() < fallback.size() ? fallback : points;
|
||||
}
|
||||
|
||||
std::vector<bool> Visible(const UgcModel::Mesh& mesh, const Options& options, uint64_t* pointCount, uint64_t* pathCount) {
|
||||
const size_t triangles = mesh.TriangleCount();
|
||||
std::vector<bool> visible(triangles, true);
|
||||
if (triangles == 0) return visible;
|
||||
const Tracer tracer(mesh, options);
|
||||
const int samples = std::max(options.samples, 1);
|
||||
uint64_t points = 0, paths = 0, sinceCheckpoint = 0;
|
||||
for (uint32_t t = 0; t < triangles; t++) {
|
||||
const auto& a = mesh.positions[mesh.indices[t * 3]];
|
||||
const auto& b = mesh.positions[mesh.indices[t * 3 + 1]];
|
||||
const auto& c = mesh.positions[mesh.indices[t * 3 + 2]];
|
||||
// A triangle without area draws nothing (LU Toolbox's bake leaves it dark too): removed
|
||||
if (tracer.FaceNormal(t) == glm::vec3(0.0f)) {
|
||||
visible[t] = false;
|
||||
continue;
|
||||
}
|
||||
const auto weights = SamplePoints(a, b, c, options.spacing, static_cast<size_t>(std::max(options.minPoints, 1)));
|
||||
points += weights.size();
|
||||
bool escaped = false;
|
||||
// A path from each point, then another from each, ...: a triangle that's seen is usually known at once
|
||||
for (int sample = 0; sample < samples && !escaped; sample++) {
|
||||
for (size_t i = 0; i < weights.size(); i++) {
|
||||
Random random(PathSeed(options.seed, t, i, static_cast<uint64_t>(sample)));
|
||||
paths++;
|
||||
if (tracer.Escapes(t, weights[i], random)) {
|
||||
escaped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sinceCheckpoint += weights.size();
|
||||
if (sinceCheckpoint >= 256) {
|
||||
UgcThrottle::Checkpoint();
|
||||
sinceCheckpoint = 0;
|
||||
}
|
||||
}
|
||||
visible[t] = escaped;
|
||||
}
|
||||
if (pointCount) *pointCount = points;
|
||||
if (pathCount) *pathCount = paths;
|
||||
return visible;
|
||||
}
|
||||
|
||||
Result RemoveHiddenFaces(UgcModel::Model& model, const Options& options) {
|
||||
Result result;
|
||||
auto& opaque = model.opaque;
|
||||
result.trianglesBefore = opaque.TriangleCount() + model.transparent.TriangleCount();
|
||||
if (opaque.Empty() || !options.enabled) return result;
|
||||
result.kept = Visible(opaque, options, &result.points, &result.paths);
|
||||
for (const bool kept : result.kept) result.trianglesRemoved += kept ? 0 : 1;
|
||||
UgcModel::KeepTriangles(opaque, result.kept);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
52
dUgcServer/Render/UgcHsr.h
Normal file
52
dUgcServer/Render/UgcHsr.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "UgcModel.h"
|
||||
|
||||
/**
|
||||
* Hidden surface removal as LU Toolbox's Remove Hidden Faces decides it, without its texture: paths are traced from
|
||||
* points on each opaque triangle under a sky of overwhelming brightness, bouncing off the model; a triangle none of
|
||||
* whose paths reaches the sky is removed. So faces seen only through openings, or lit only by light bounced in
|
||||
* (interiors, recesses), stay; faces sealed inside go. docs/UgcServer.md ("Hidden faces") has the details.
|
||||
*/
|
||||
namespace UgcHsr {
|
||||
struct Options {
|
||||
bool enabled{ true }; // remove_hidden_faces
|
||||
bool groundPlane{ false }; // hsr_ground_plane: LU Toolbox's black box under the model (y 0 down to -100)
|
||||
int samples{ 8 }; // hsr_samples: paths traced from each point (LU Toolbox's Samples)
|
||||
int bounces{ 8 }; // hsr_bounces: bounces a path may take (the Cycles bake's Max Bounces)
|
||||
float spacing{ 0.1143f }; // hsr_sample_spacing: LDD units between points (a stud is 0.8: 7 points along it)
|
||||
int minPoints{ 28 }; // hsr_min_points: points on a triangle at least (LU Toolbox bakes 28 texels a triangle)
|
||||
uint64_t seed{}; // of the paths' random numbers (the same seed gives the same result)
|
||||
};
|
||||
|
||||
struct Result {
|
||||
size_t trianglesBefore{}; // opaque and transparent
|
||||
size_t trianglesRemoved{};
|
||||
std::vector<bool> kept; // per opaque triangle before: whether it stayed
|
||||
uint64_t points{}; // sample points traced from
|
||||
uint64_t paths{}; // paths traced
|
||||
};
|
||||
|
||||
/**
|
||||
* Which triangles of `mesh` a path from them reaches the sky from (triangles without area: none). `mesh` is
|
||||
* everything that occludes (the opaque bricks; transparent ones hide nothing, as in LU Toolbox).
|
||||
*/
|
||||
std::vector<bool> Visible(const UgcModel::Mesh& mesh, const Options& options, uint64_t* points = nullptr, uint64_t* paths = nullptr);
|
||||
|
||||
// Removes the opaque triangles that aren't Visible (nothing when options.enabled is off); transparent ones stay
|
||||
Result RemoveHiddenFaces(UgcModel::Model& model, const Options& options);
|
||||
|
||||
/**
|
||||
* The points on triangle a, b, c paths start from, as barycentric weights (of a, b, c): rows parallel to its
|
||||
* longest side, `spacing` apart, with points `spacing` apart along each row (so about area / spacing^2 of them).
|
||||
* When that gives fewer than `minimum` the rows and points are closer together until there are that many; at
|
||||
* least 4 (its centre and one towards each corner), at most 4096 (big triangles get them further apart).
|
||||
*/
|
||||
std::vector<glm::vec3> SamplePoints(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float spacing, size_t minimum = 4);
|
||||
}
|
||||
91
dUgcServer/Render/UgcIconParams.cpp
Normal file
91
dUgcServer/Render/UgcIconParams.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "UgcIconParams.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
namespace {
|
||||
using Options = UgcRender::IconOptions;
|
||||
}
|
||||
|
||||
namespace UgcIconParams {
|
||||
const std::vector<Param>& List() {
|
||||
// Framing from LU Toolbox's icon renderer (its UGC render add-on's BrickBuild scene); the light matched to the
|
||||
// brightness of the game's own model icons (res/textures/ui/inventory/models), docs/UgcServer.md
|
||||
static const std::vector<Param> list = {
|
||||
{ "yaw", "camera", "icon_yaw", "Angle around", "degrees", -180, 180, 1, 53.36f, "The camera's angle around the model, from its front.", [](Options& o, float v) { o.yawDegrees = v; } },
|
||||
{ "pitch", "camera", "icon_pitch", "Angle above", "degrees", -89, 89, 1, 19.54f, "", [](Options& o, float v) { o.pitchDegrees = v; } },
|
||||
{ "fov", "camera", "icon_fov", "Field of view", "degrees", 5, 90, 0.5f, 39.6f, "", [](Options& o, float v) { o.fovDegrees = v; } },
|
||||
{ "margin", "framing", "icon_margin", "Border (zoom)", "", 0.5f, 3, 0.01f, 1.03f, "1 fills the icon, more leaves a border.", [](Options& o, float v) { o.margin = v; } },
|
||||
{ "offsetX", "framing", "icon_offset_x", "Shift right", "", -0.5f, 0.5f, 0.01f, 0.0f, "Share of the icon's width.", [](Options& o, float v) { o.offsetX = v; } },
|
||||
{ "offsetY", "framing", "icon_offset_y", "Shift up", "", -0.5f, 0.5f, 0.01f, 0.0f, "Share of the icon's height.", [](Options& o, float v) { o.offsetY = v; } },
|
||||
{ "modelYaw", "model", "icon_model_yaw", "Model turned around", "degrees", -180, 180, 1, 0.0f, "The model turned around its up axis before the camera looks at it.", [](Options& o, float v) { o.modelYawDegrees = v; } },
|
||||
{ "modelPitch", "model", "icon_model_pitch", "Model tipped forward", "degrees", -90, 90, 1, 0.0f, "Turned around its side axis, after the turn around.", [](Options& o, float v) { o.modelPitchDegrees = v; } },
|
||||
{ "modelRoll", "model", "icon_model_roll", "Model rolled", "degrees", -180, 180, 1, 0.0f, "Turned around its front axis, last.", [](Options& o, float v) { o.modelRollDegrees = v; } },
|
||||
{ "sunYaw", "sun", "icon_sun_yaw", "Sun around", "degrees", -180, 180, 1, 21.0f, "", [](Options& o, float v) { o.sunYawDegrees = v; } },
|
||||
{ "sunPitch", "sun", "icon_sun_pitch", "Sun above", "degrees", -10, 90, 1, 50.3f, "", [](Options& o, float v) { o.sunPitchDegrees = v; } },
|
||||
{ "sunStrength", "sun", "icon_sun_light", "Sun strength", "", 0, 10, 0.05f, 2.0f, "", [](Options& o, float v) { o.sunStrength = v; } },
|
||||
{ "ambient", "light", "icon_world_light", "World light", "", 0, 2, 0.01f, 1.0f, "What every face gets.", [](Options& o, float v) { o.ambient = v; } },
|
||||
{ "fill", "light", "icon_fill", "Fill light", "", 0, 5, 0.05f, 0.8f, "A light from the camera.", [](Options& o, float v) { o.fill = v; } },
|
||||
{ "specular", "light", "icon_specular", "Highlights", "", 0, 2, 0.05f, 0.2f, "The sun's highlight on the plastic.", [](Options& o, float v) { o.specular = v; } },
|
||||
{ "shininess", "light", "icon_shininess", "Highlight tightness", "", 1, 200, 1, 40.0f, "", [](Options& o, float v) { o.shininess = v; } },
|
||||
{ "exposure", "look", "icon_exposure", "Exposure", "", 0.1f, 4, 0.05f, 2.6f, "Brightness of everything.", [](Options& o, float v) { o.exposure = v; } },
|
||||
{ "contrast", "look", "icon_contrast", "Contrast", "", 0.5f, 1.5f, 0.01f, 1.0f, "", [](Options& o, float v) { o.contrast = v; } },
|
||||
{ "shadows", "sun", "icon_shadow_strength", "Shadows", "", 0, 1, 0.05f, 0.4f, "How much the sun's shadows darken.", [](Options& o, float v) { o.shadows = v; } },
|
||||
{ "aoStrength", "look", "icon_ao_strength", "Ambient occlusion", "", 0, 1, 0.05f, 0.0f,
|
||||
"Occlusion worked out for the icon. Player models' meshes have theirs baked in already.", [](Options& o, float v) { o.ao.strength = v; o.ao.enabled = v > 0.0f; } },
|
||||
};
|
||||
return list;
|
||||
}
|
||||
|
||||
const Param* Find(std::string_view key) {
|
||||
const auto& list = List();
|
||||
const auto it = std::find_if(list.begin(), list.end(), [key](const Param& p) { return p.key == key; });
|
||||
return it != list.end() ? &*it : nullptr;
|
||||
}
|
||||
|
||||
Values Parse(std::string_view text) {
|
||||
Values values;
|
||||
const auto json = nlohmann::json::parse(text, nullptr, false);
|
||||
if (!json.is_object()) return values;
|
||||
for (const auto& param : List()) {
|
||||
const auto it = json.find(param.key);
|
||||
if (it == json.end() || !it->is_number()) continue;
|
||||
const auto value = it->get<float>();
|
||||
if (std::isfinite(value)) values[param.key] = std::clamp(value, param.min, param.max);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::string ToJson(const Values& values) {
|
||||
nlohmann::json json = nlohmann::json::object();
|
||||
for (const auto& [key, value] : values) {
|
||||
if (Find(key)) json[key] = value;
|
||||
}
|
||||
return json.dump();
|
||||
}
|
||||
|
||||
void Apply(UgcRender::IconOptions& options, const Values& values) {
|
||||
for (const auto& [key, value] : values) {
|
||||
if (const auto* param = Find(key)) param->apply(options, std::clamp(value, param->min, param->max));
|
||||
}
|
||||
}
|
||||
|
||||
UgcRender::IconOptions FromSettings(const std::function<std::optional<std::string>(const std::string&)>& get) {
|
||||
UgcRender::IconOptions options;
|
||||
for (const auto& param : List()) {
|
||||
const auto text = get(param.setting);
|
||||
const auto value = text ? GeneralUtils::TryParse<float>(*text) : std::nullopt;
|
||||
param.apply(options, std::clamp(value.value_or(param.defaultValue), param.min, param.max));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string KindTarget(const std::string& kind) { return "kind:" + kind; }
|
||||
std::string ModelTarget(int64_t id) { return "model:" + std::to_string(id); }
|
||||
std::string CombinationTarget(const std::string& key) { return "combo:" + key; }
|
||||
std::string ModelKind() { return "model"; }
|
||||
std::string BuildKind(int32_t buildType) { return "build" + std::to_string(buildType); }
|
||||
}
|
||||
57
dUgcServer/Render/UgcIconParams.h
Normal file
57
dUgcServer/Render/UgcIconParams.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "UgcRender.h"
|
||||
|
||||
/**
|
||||
* Everything about an icon's framing and lighting that can be set, listed once: its name (in presets and overrides),
|
||||
* its setting (the shipped default, in ugcconfig.ini and the dashboard's settings), label, unit, range and default.
|
||||
* The UGC server reads the settings through it, the dashboard builds its settings entries and the icon editor's
|
||||
* controls from it, and presets and overrides are checked against it. Values for an icon come, each over the one
|
||||
* before: the settings (icon_*), the kind's preset (player models, or a car or rocket build type) and the item's or
|
||||
* module combination's own override, both stored in the database (ugc_icon_settings). Pure.
|
||||
*/
|
||||
namespace UgcIconParams {
|
||||
struct Param {
|
||||
std::string key; // in presets and overrides, e.g. "sunStrength"
|
||||
std::string group; // what it is part of, for the editor: camera, framing, model, sun, light, look
|
||||
std::string setting; // ugcconfig.ini, e.g. "icon_sun_strength"
|
||||
std::string label;
|
||||
std::string unit;
|
||||
float min{};
|
||||
float max{};
|
||||
float step{};
|
||||
float defaultValue{};
|
||||
std::string description;
|
||||
std::function<void(UgcRender::IconOptions&, float)> apply;
|
||||
};
|
||||
|
||||
const std::vector<Param>& List();
|
||||
const Param* Find(std::string_view key);
|
||||
|
||||
using Values = std::map<std::string, float>;
|
||||
|
||||
// Known keys only, numbers only, clamped to their ranges
|
||||
Values Parse(std::string_view json);
|
||||
std::string ToJson(const Values& values);
|
||||
|
||||
// Values over the options' own
|
||||
void Apply(UgcRender::IconOptions& options, const Values& values);
|
||||
|
||||
// The options from the settings: `get` gives a setting's value (nullopt or unparsable: the parameter's default)
|
||||
UgcRender::IconOptions FromSettings(const std::function<std::optional<std::string>(const std::string&)>& get);
|
||||
|
||||
// Where presets and overrides are stored (ugc_icon_settings.target)
|
||||
std::string KindTarget(const std::string& kind); // "kind:model", "kind:build6"
|
||||
std::string ModelTarget(int64_t id); // "model:<id>"
|
||||
std::string CombinationTarget(const std::string& key); // "combo:<key>"
|
||||
// The kind of a player model, and of a car or rocket of build type `buildType` (ModuleComponent.buildType)
|
||||
std::string ModelKind();
|
||||
std::string BuildKind(int32_t buildType);
|
||||
}
|
||||
99
dUgcServer/Render/UgcIconPose.cpp
Normal file
99
dUgcServer/Render/UgcIconPose.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "UgcIconPose.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace UgcIconPose {
|
||||
glm::vec3 CameraDirection(float yawDegrees, float pitchDegrees) {
|
||||
const float yaw = glm::radians(yawDegrees), pitch = glm::radians(pitchDegrees);
|
||||
return { std::sin(yaw) * std::cos(pitch), std::sin(pitch), std::cos(yaw) * std::cos(pitch) };
|
||||
}
|
||||
|
||||
glm::vec2 DirectionAngles(const glm::vec3& direction) {
|
||||
const float horizontal = std::sqrt(direction.x * direction.x + direction.z * direction.z);
|
||||
return { glm::degrees(std::atan2(direction.x, direction.z)), glm::degrees(std::atan2(direction.y, horizontal)) };
|
||||
}
|
||||
|
||||
glm::mat4 ModelRotation(float yawDegrees, float pitchDegrees, float rollDegrees) {
|
||||
auto rotation = glm::rotate(glm::mat4(1.0f), glm::radians(yawDegrees), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
rotation = glm::rotate(rotation, glm::radians(pitchDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
return glm::rotate(rotation, glm::radians(rollDegrees), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
glm::vec3 RotationAngles(const glm::mat4& m) {
|
||||
// Ry*Rx*Rz: m[2][1] (column 2, row 1) is -sin(pitch); as three.js's Euler.setFromRotationMatrix for 'YXZ'
|
||||
const float m13 = m[2][0], m23 = m[2][1], m33 = m[2][2];
|
||||
const float m21 = m[0][1], m22 = m[1][1], m11 = m[0][0], m31 = m[0][2];
|
||||
const float pitch = std::asin(std::clamp(-m23, -1.0f, 1.0f));
|
||||
float yaw, roll;
|
||||
if (std::abs(m23) < 0.9999999f) {
|
||||
yaw = std::atan2(m13, m33);
|
||||
roll = std::atan2(m21, m22);
|
||||
} else {
|
||||
yaw = std::atan2(-m31, m11);
|
||||
roll = 0.0f;
|
||||
}
|
||||
return { glm::degrees(yaw), glm::degrees(pitch), glm::degrees(roll) };
|
||||
}
|
||||
|
||||
glm::vec3 Frame::IconPoint(const glm::vec3& position) const {
|
||||
const auto clip = viewProjection * glm::vec4(position, 1.0f);
|
||||
const float w = clip.w > 1e-6f ? clip.w : 1e-6f;
|
||||
return { 0.5f + offset.x + (clip.x / w - centerX) * scale * 0.5f, 0.5f - offset.y - (clip.y / w - centerY) * scale * 0.5f, clip.z / w };
|
||||
}
|
||||
|
||||
glm::vec4 Frame::IconRect() const {
|
||||
// Inverse of IconPoint at the icon's edges (0 and 1)
|
||||
const float half = 2.0f / scale;
|
||||
return { centerX + (0.0f - 0.5f - offset.x) * half, centerY + (0.5f - offset.y - 1.0f) * half,
|
||||
centerX + (1.0f - 0.5f - offset.x) * half, centerY + (0.5f - offset.y) * half };
|
||||
}
|
||||
|
||||
Frame Compute(const std::vector<const std::vector<glm::vec3>*>& positions, const Camera& camera) {
|
||||
constexpr float INF = std::numeric_limits<float>::infinity();
|
||||
Frame frame;
|
||||
glm::vec3 min(INF), max(-INF);
|
||||
for (const auto* list : positions) {
|
||||
for (const auto& p : *list) {
|
||||
min = glm::min(min, p);
|
||||
max = glm::max(max, p);
|
||||
}
|
||||
}
|
||||
if (min.x > max.x) {
|
||||
frame.center = glm::vec3(0.0f);
|
||||
frame.radius = 1.0f;
|
||||
} else {
|
||||
frame.center = (min + max) * 0.5f;
|
||||
frame.radius = std::max(glm::length(max - min) * 0.5f, 0.01f);
|
||||
}
|
||||
const auto direction = CameraDirection(camera.yawDegrees, camera.pitchDegrees);
|
||||
frame.fov = glm::radians(std::clamp(camera.fovDegrees, 1.0f, 120.0f));
|
||||
frame.distance = frame.radius / std::sin(frame.fov * 0.5f);
|
||||
frame.eye = frame.center + direction * frame.distance;
|
||||
const float nearPlane = std::max(frame.distance - frame.radius * 1.5f, frame.distance * 0.01f);
|
||||
frame.viewProjection = glm::perspective(frame.fov, 1.0f, nearPlane, frame.distance + frame.radius * 1.5f) *
|
||||
glm::lookAt(frame.eye, frame.center, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
|
||||
float minX = INF, minY = INF, maxX = -INF, maxY = -INF;
|
||||
for (const auto* list : positions) {
|
||||
for (const auto& p : *list) {
|
||||
const auto clip = frame.viewProjection * glm::vec4(p, 1.0f);
|
||||
if (clip.w <= 0.0f) continue;
|
||||
minX = std::min(minX, clip.x / clip.w);
|
||||
maxX = std::max(maxX, clip.x / clip.w);
|
||||
minY = std::min(minY, clip.y / clip.w);
|
||||
maxY = std::max(maxY, clip.y / clip.w);
|
||||
}
|
||||
}
|
||||
if (minX > maxX) return frame;
|
||||
frame.centerX = (minX + maxX) * 0.5f;
|
||||
frame.centerY = (minY + maxY) * 0.5f;
|
||||
frame.scale = 2.0f / (std::max({ maxX - minX, maxY - minY, 1e-6f }) * std::max(camera.margin, 0.1f));
|
||||
frame.offset = { camera.offsetX, camera.offsetY };
|
||||
frame.ok = true;
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
56
dUgcServer/Render/UgcIconPose.h
Normal file
56
dUgcServer/Render/UgcIconPose.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
/**
|
||||
* Where the icon's camera is and how the model is turned for it, worked out the same way in the icon renderer and in
|
||||
* the dashboard's pose editor (static/js/ugc-pose.js mirrors these functions, so its 3D view shows what the icon
|
||||
* will). Angles are degrees. Pure.
|
||||
*
|
||||
* The model is turned first (ModelRotation, about its origin), then the camera looks at the centre of its bounds from
|
||||
* CameraDirection, as far away as makes the bounding sphere fill the field of view, and last the picture is cropped to
|
||||
* the model's projected bounds (Frame): scaled so the larger side fills the icon less the margin, then shifted.
|
||||
*/
|
||||
namespace UgcIconPose {
|
||||
// From the model towards the camera: yaw around +Y from +Z towards +X, pitch up from the ground
|
||||
glm::vec3 CameraDirection(float yawDegrees, float pitchDegrees);
|
||||
// The inverse: {yaw, pitch} of a direction (need not be unit length)
|
||||
glm::vec2 DirectionAngles(const glm::vec3& direction);
|
||||
|
||||
// The model's turn: yaw around +Y, then pitch around +X, then roll around +Z (R = Ry * Rx * Rz, three.js's 'YXZ' Euler order)
|
||||
glm::mat4 ModelRotation(float yawDegrees, float pitchDegrees, float rollDegrees);
|
||||
// The inverse: {yaw, pitch, roll} of a rotation (pitch in -90..90; at +-90 the roll is folded into the yaw)
|
||||
glm::vec3 RotationAngles(const glm::mat4& rotation);
|
||||
|
||||
struct Camera {
|
||||
float yawDegrees{};
|
||||
float pitchDegrees{};
|
||||
float fovDegrees{ 40.0f };
|
||||
float margin{ 1.0f }; // 1: the model's larger projected side fills the icon
|
||||
float offsetX{}; // share of the icon's width the model is moved right
|
||||
float offsetY{}; // and up
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
bool ok{};
|
||||
glm::vec3 center{}; // of the model's bounds
|
||||
float radius{}; // half their diagonal
|
||||
glm::vec3 eye{};
|
||||
float fov{}; // radians
|
||||
float distance{};
|
||||
glm::mat4 viewProjection{ 1.0f };
|
||||
float centerX{}, centerY{}; // centre of the projected bounds (NDC)
|
||||
float scale{ 1.0f }; // NDC -> icon: 2 / (larger projected side * margin)
|
||||
glm::vec2 offset{};
|
||||
|
||||
// A point's place in the icon: x right and y down, 0..1 across it, and its depth (NDC z)
|
||||
glm::vec3 IconPoint(const glm::vec3& position) const;
|
||||
// The icon's square in the camera's NDC: {minX, minY, maxX, maxY}
|
||||
glm::vec4 IconRect() const;
|
||||
};
|
||||
|
||||
// The frame of an already turned model's vertices (any number of lists)
|
||||
Frame Compute(const std::vector<const std::vector<glm::vec3>*>& positions, const Camera& camera);
|
||||
}
|
||||
473
dUgcServer/Render/UgcRender.cpp
Normal file
473
dUgcServer/Render/UgcRender.cpp
Normal file
@@ -0,0 +1,473 @@
|
||||
#include "UgcRender.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "UgcIconPose.h"
|
||||
#include "UgcPalette.h"
|
||||
#include "UgcThrottle.h"
|
||||
|
||||
namespace {
|
||||
constexpr float INF = std::numeric_limits<float>::infinity();
|
||||
|
||||
float Edge(const glm::vec3& a, const glm::vec3& b, float px, float py) {
|
||||
return (b.x - a.x) * (py - a.y) - (b.y - a.y) * (px - a.x);
|
||||
}
|
||||
|
||||
// Calls fragment(x, y, z, w0, w1, w2) for every pixel centre inside the screen-space triangle (x, y in pixels)
|
||||
template<typename Fragment>
|
||||
void Rasterize(int width, int height, const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, Fragment&& fragment) {
|
||||
const float area = Edge(a, b, c.x, c.y);
|
||||
if (!(std::abs(area) > 1e-9f)) return;
|
||||
const float inverse = 1.0f / area;
|
||||
const int minX = std::max(0, static_cast<int>(std::floor(std::min({ a.x, b.x, c.x }))));
|
||||
const int maxX = std::min(width - 1, static_cast<int>(std::ceil(std::max({ a.x, b.x, c.x }))));
|
||||
const int minY = std::max(0, static_cast<int>(std::floor(std::min({ a.y, b.y, c.y }))));
|
||||
const int maxY = std::min(height - 1, static_cast<int>(std::ceil(std::max({ a.y, b.y, c.y }))));
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
const float py = y + 0.5f;
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
const float px = x + 0.5f;
|
||||
const float w0 = Edge(b, c, px, py) * inverse;
|
||||
const float w1 = Edge(c, a, px, py) * inverse;
|
||||
const float w2 = 1.0f - w0 - w1;
|
||||
if (w0 < 0.0f || w1 < 0.0f || w2 < 0.0f) continue;
|
||||
fragment(x, y, w0 * a.z + w1 * b.z + w2 * c.z, w0, w1, w2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float ToLinear(float c) { return UgcPalette::SrgbToLinear(std::clamp(c, 0.0f, 1.0f)); }
|
||||
float ToSrgb(float c) { return UgcPalette::LinearToSrgb(std::clamp(c, 0.0f, 1.0f)); }
|
||||
|
||||
// A bounding volume hierarchy over a mesh's triangles, for the occlusion rays
|
||||
class Bvh {
|
||||
public:
|
||||
explicit Bvh(const UgcModel::Mesh& mesh) : m_Mesh(mesh) {
|
||||
const size_t count = mesh.TriangleCount();
|
||||
m_Order.resize(count);
|
||||
std::iota(m_Order.begin(), m_Order.end(), 0u);
|
||||
m_Centers.resize(count);
|
||||
for (size_t t = 0; t < count; t++) m_Centers[t] = (Vertex(t, 0) + Vertex(t, 1) + Vertex(t, 2)) / 3.0f;
|
||||
if (count > 0) Build(0, static_cast<uint32_t>(count));
|
||||
Flatten();
|
||||
}
|
||||
|
||||
// Whether a ray from `origin` along `direction` (unit) hits a triangle nearer than `maxDistance`
|
||||
bool Hits(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const {
|
||||
if (m_Nodes.empty()) return false;
|
||||
const glm::vec3 inverse(1.0f / (std::abs(direction.x) > 1e-12f ? direction.x : 1e-12f), 1.0f / (std::abs(direction.y) > 1e-12f ? direction.y : 1e-12f),
|
||||
1.0f / (std::abs(direction.z) > 1e-12f ? direction.z : 1e-12f));
|
||||
uint32_t stack[64];
|
||||
int top = 0;
|
||||
stack[top++] = 0;
|
||||
while (top > 0) {
|
||||
const auto& node = m_Nodes[stack[--top]];
|
||||
if (!BoxHit(node, origin, inverse, maxDistance)) continue;
|
||||
if (node.count > 0) {
|
||||
for (uint32_t i = node.first; i < node.first + node.count; i++) {
|
||||
if (TriangleHit(m_Triangles[i], origin, direction, maxDistance)) return true;
|
||||
}
|
||||
} else if (top < 62) {
|
||||
stack[top++] = node.first;
|
||||
stack[top++] = node.first + 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
glm::vec3 min{};
|
||||
glm::vec3 max{};
|
||||
uint32_t first{}; // leaf: first triangle in m_Order; inner: the first of two children
|
||||
uint32_t count{}; // triangles, 0 for inner nodes
|
||||
};
|
||||
|
||||
glm::vec3 Vertex(size_t t, int k) const { return m_Mesh.positions[m_Mesh.indices[t * 3 + k]]; }
|
||||
|
||||
void Build(uint32_t first, uint32_t count) {
|
||||
// Iterative, so deep trees don't use the stack
|
||||
struct Task { uint32_t node, first, count; };
|
||||
m_Nodes.push_back({});
|
||||
std::vector<Task> tasks{ { 0, first, count } };
|
||||
while (!tasks.empty()) {
|
||||
const auto task = tasks.back();
|
||||
tasks.pop_back();
|
||||
Node node;
|
||||
node.min = glm::vec3(INF);
|
||||
node.max = glm::vec3(-INF);
|
||||
glm::vec3 centerMin(INF), centerMax(-INF);
|
||||
for (uint32_t i = task.first; i < task.first + task.count; i++) {
|
||||
for (int k = 0; k < 3; k++) {
|
||||
node.min = glm::min(node.min, Vertex(m_Order[i], k));
|
||||
node.max = glm::max(node.max, Vertex(m_Order[i], k));
|
||||
}
|
||||
centerMin = glm::min(centerMin, m_Centers[m_Order[i]]);
|
||||
centerMax = glm::max(centerMax, m_Centers[m_Order[i]]);
|
||||
}
|
||||
const auto extent = centerMax - centerMin;
|
||||
const int axis = extent.x >= extent.y && extent.x >= extent.z ? 0 : extent.y >= extent.z ? 1 : 2;
|
||||
if (task.count <= 4 || extent[axis] <= 0.0f) {
|
||||
node.first = task.first;
|
||||
node.count = task.count;
|
||||
m_Nodes[task.node] = node;
|
||||
continue;
|
||||
}
|
||||
const uint32_t half = task.count / 2;
|
||||
auto* begin = m_Order.data() + task.first;
|
||||
std::nth_element(begin, begin + half, begin + task.count, [&](uint32_t a, uint32_t b) { return m_Centers[a][axis] < m_Centers[b][axis]; });
|
||||
node.first = static_cast<uint32_t>(m_Nodes.size());
|
||||
node.count = 0;
|
||||
m_Nodes[task.node] = node;
|
||||
m_Nodes.push_back({});
|
||||
m_Nodes.push_back({});
|
||||
tasks.push_back({ node.first, task.first, half });
|
||||
tasks.push_back({ node.first + 1, task.first + half, task.count - half });
|
||||
}
|
||||
}
|
||||
|
||||
static bool BoxHit(const Node& node, const glm::vec3& origin, const glm::vec3& inverse, float maxDistance) {
|
||||
const auto t0 = (node.min - origin) * inverse;
|
||||
const auto t1 = (node.max - origin) * inverse;
|
||||
const auto near = glm::min(t0, t1), far = glm::max(t0, t1);
|
||||
const float enter = std::max(std::max(near.x, near.y), std::max(near.z, 0.0f));
|
||||
const float exit = std::min(std::min(far.x, far.y), std::min(far.z, maxDistance));
|
||||
return enter <= exit;
|
||||
}
|
||||
|
||||
struct Triangle {
|
||||
glm::vec3 a, e1, e2;
|
||||
};
|
||||
|
||||
// The triangles in leaf order, edges worked out once (the rays read them far more often than the tree is built)
|
||||
void Flatten() {
|
||||
m_Triangles.reserve(m_Order.size());
|
||||
for (const auto t : m_Order) {
|
||||
const auto a = Vertex(t, 0);
|
||||
m_Triangles.push_back({ a, Vertex(t, 1) - a, Vertex(t, 2) - a });
|
||||
}
|
||||
}
|
||||
|
||||
static bool TriangleHit(const Triangle& triangle, const glm::vec3& origin, const glm::vec3& direction, float maxDistance) {
|
||||
const auto& a = triangle.a;
|
||||
const auto& e1 = triangle.e1;
|
||||
const auto& e2 = triangle.e2;
|
||||
const auto p = glm::cross(direction, e2);
|
||||
const float det = glm::dot(e1, p);
|
||||
if (std::abs(det) < 1e-12f) return false;
|
||||
const float inv = 1.0f / det;
|
||||
const auto s = origin - a;
|
||||
const float u = glm::dot(s, p) * inv;
|
||||
if (u < 0.0f || u > 1.0f) return false;
|
||||
const auto q = glm::cross(s, e1);
|
||||
const float v = glm::dot(direction, q) * inv;
|
||||
if (v < 0.0f || u + v > 1.0f) return false;
|
||||
const float distance = glm::dot(e2, q) * inv;
|
||||
return distance > 1e-4f && distance < maxDistance;
|
||||
}
|
||||
|
||||
const UgcModel::Mesh& m_Mesh;
|
||||
std::vector<Triangle> m_Triangles;
|
||||
std::vector<uint32_t> m_Order;
|
||||
std::vector<glm::vec3> m_Centers;
|
||||
std::vector<Node> m_Nodes;
|
||||
};
|
||||
|
||||
float RadicalInverse(uint32_t bits) {
|
||||
bits = (bits << 16u) | (bits >> 16u);
|
||||
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
|
||||
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
|
||||
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
|
||||
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
|
||||
return static_cast<float>(bits) * 2.3283064365386963e-10f;
|
||||
}
|
||||
|
||||
// Looking at a sphere (center, radius) from direction `dir` (towards the viewer), square orthographic view
|
||||
struct OrthoView {
|
||||
glm::vec3 center{};
|
||||
glm::vec3 dir{};
|
||||
glm::vec3 right{};
|
||||
glm::vec3 up{};
|
||||
float radius{};
|
||||
int resolution{};
|
||||
|
||||
OrthoView(const glm::vec3& center_, float radius_, const glm::vec3& dir_, int resolution_)
|
||||
: center(center_), dir(dir_), radius(radius_), resolution(resolution_) {
|
||||
const glm::vec3 worldUp = std::abs(dir.y) < 0.99f ? glm::vec3(0.0f, 1.0f, 0.0f) : glm::vec3(1.0f, 0.0f, 0.0f);
|
||||
right = glm::normalize(glm::cross(worldUp, dir));
|
||||
up = glm::cross(dir, right);
|
||||
}
|
||||
|
||||
// x, y in pixels; z the distance from the camera plane (smaller is nearer)
|
||||
glm::vec3 Project(const glm::vec3& p) const {
|
||||
const auto offset = p - center;
|
||||
const float scale = 0.5f * resolution / radius;
|
||||
return { resolution * 0.5f + glm::dot(offset, right) * scale, resolution * 0.5f - glm::dot(offset, up) * scale, radius - glm::dot(offset, dir) };
|
||||
}
|
||||
|
||||
float PixelSize() const { return 2.0f * radius / resolution; }
|
||||
};
|
||||
}
|
||||
|
||||
namespace UgcRender {
|
||||
std::vector<float> AmbientOcclusion(const UgcModel::Mesh& mesh, const UgcModel::Mesh& occluders, float distance, int samples) {
|
||||
std::vector<float> ao(mesh.positions.size(), 1.0f);
|
||||
if (occluders.Empty() || samples <= 0 || distance <= 0.0f || mesh.normals.size() != mesh.positions.size()) return ao;
|
||||
const Bvh bvh(occluders);
|
||||
const auto count = static_cast<uint32_t>(samples);
|
||||
// Vertices at the same place facing the same way (bricks' shared corners) are worked out once
|
||||
struct Key {
|
||||
int32_t p[3], n[3];
|
||||
bool operator==(const Key& o) const { return std::equal(p, p + 3, o.p) && std::equal(n, n + 3, o.n); }
|
||||
};
|
||||
struct KeyHash {
|
||||
size_t operator()(const Key& k) const {
|
||||
size_t h = 1469598103934665603ull;
|
||||
for (int i = 0; i < 3; i++) h = (h ^ static_cast<uint32_t>(k.p[i])) * 1099511628211ull ^ static_cast<uint32_t>(k.n[i]) * 0x9E3779B97F4A7C15ull;
|
||||
return h;
|
||||
}
|
||||
};
|
||||
std::unordered_map<Key, float, KeyHash> known;
|
||||
known.reserve(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) {
|
||||
if ((v & 0xFF) == 0) UgcThrottle::Checkpoint();
|
||||
const auto& normal = mesh.normals[v];
|
||||
const Key key{ { static_cast<int32_t>(std::lround(mesh.positions[v].x * 1000.0f)), static_cast<int32_t>(std::lround(mesh.positions[v].y * 1000.0f)),
|
||||
static_cast<int32_t>(std::lround(mesh.positions[v].z * 1000.0f)) }, { static_cast<int32_t>(std::lround(normal.x * 100.0f)),
|
||||
static_cast<int32_t>(std::lround(normal.y * 100.0f)), static_cast<int32_t>(std::lround(normal.z * 100.0f)) } };
|
||||
if (const auto it = known.find(key); it != known.end()) {
|
||||
ao[v] = it->second;
|
||||
continue;
|
||||
}
|
||||
if (glm::dot(normal, normal) < 0.5f) continue;
|
||||
// A frame around the normal
|
||||
const glm::vec3 helper = std::abs(normal.x) < 0.9f ? glm::vec3(1, 0, 0) : glm::vec3(0, 1, 0);
|
||||
const auto tangent = glm::normalize(glm::cross(helper, normal));
|
||||
const auto bitangent = glm::cross(normal, tangent);
|
||||
// Hammersley points, turned by an amount of the vertex's own (fixed) so neighbours don't band
|
||||
const float turn = static_cast<float>((v * 0x9E3779B9u) >> 8 & 0xFFFFFF) / 16777216.0f;
|
||||
const auto origin = mesh.positions[v] + normal * 1e-3f;
|
||||
uint32_t open = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
const float u = (i + 0.5f) / static_cast<float>(count);
|
||||
const float phi = 2.0f * 3.14159265f * std::fmod(RadicalInverse(i) + turn, 1.0f);
|
||||
const float r = std::sqrt(u), z = std::sqrt(std::max(0.0f, 1.0f - u));
|
||||
const auto direction = tangent * (r * std::cos(phi)) + bitangent * (r * std::sin(phi)) + normal * z;
|
||||
if (!bvh.Hits(origin, direction, distance)) open++;
|
||||
}
|
||||
ao[v] = static_cast<float>(open) / static_cast<float>(count);
|
||||
known.emplace(key, ao[v]);
|
||||
}
|
||||
return ao;
|
||||
}
|
||||
|
||||
std::vector<float> BakeAo(UgcModel::Model& model, const AoOptions& options) {
|
||||
auto& opaque = model.opaque;
|
||||
if (!options.enabled || opaque.Empty()) return {};
|
||||
auto ao = AmbientOcclusion(opaque, opaque, options.distance, options.samples);
|
||||
const float strength = std::clamp(options.strength, 0.0f, 1.0f);
|
||||
for (size_t v = 0; v < opaque.colors.size() && v < ao.size(); v++) {
|
||||
glm::vec3 lit(1.0f - strength * (1.0f - ao[v]));
|
||||
if (v < opaque.glow.size()) lit += opaque.glow[v] * options.glowStrength;
|
||||
lit = glm::clamp(lit, 0.0f, 1.0f);
|
||||
auto& color = opaque.colors[v];
|
||||
color.r = ToSrgb(ToLinear(color.r) * lit.r);
|
||||
color.g = ToSrgb(ToLinear(color.g) * lit.g);
|
||||
color.b = ToSrgb(ToLinear(color.b) * lit.b);
|
||||
}
|
||||
return ao;
|
||||
}
|
||||
|
||||
Image RenderIcon(const UgcModel::Model& source, const IconOptions& options, const std::vector<float>* opaqueAo) {
|
||||
const int size = std::clamp(options.size, 8, 1024);
|
||||
const int supersample = std::clamp(options.supersample, 1, 8);
|
||||
const int n = size * supersample;
|
||||
Image image{ size, size, std::vector<uint8_t>(static_cast<size_t>(size) * size * 4, 0) };
|
||||
if (source.Empty()) return image;
|
||||
|
||||
UgcModel::Model model = source;
|
||||
const auto rotation = UgcIconPose::ModelRotation(options.modelYawDegrees, options.modelPitchDegrees, options.modelRollDegrees) * options.modelRotation;
|
||||
model.opaque.Transform(rotation);
|
||||
model.transparent.Transform(rotation);
|
||||
|
||||
// The camera and the crop to the model's projected bounds (shared with the dashboard's pose editor)
|
||||
const auto frame = UgcIconPose::Compute({ &model.opaque.positions, &model.transparent.positions },
|
||||
{ options.yawDegrees, options.pitchDegrees, options.fovDegrees, options.margin, options.offsetX, options.offsetY });
|
||||
if (!frame.ok) return image;
|
||||
const glm::vec3 center = frame.center;
|
||||
const float radius = frame.radius;
|
||||
const glm::vec3 eye = frame.eye;
|
||||
const glm::vec3 dir = glm::normalize(eye - center);
|
||||
const auto project = [&](const glm::vec3& position) {
|
||||
const auto point = frame.IconPoint(position);
|
||||
return glm::vec3(point.x * n, point.y * n, point.z);
|
||||
};
|
||||
|
||||
// Linear, premultiplied
|
||||
std::vector<glm::vec4> color(static_cast<size_t>(n) * n, glm::vec4(0.0f));
|
||||
std::vector<float> depth(static_cast<size_t>(n) * n, INF);
|
||||
const float sunYaw = glm::radians(options.sunYawDegrees), sunPitch = glm::radians(options.sunPitchDegrees);
|
||||
const glm::vec3 light = glm::normalize(glm::vec3(std::sin(sunYaw) * std::cos(sunPitch), std::sin(sunPitch), std::cos(sunYaw) * std::cos(sunPitch)));
|
||||
|
||||
// Ambient occlusion darkens the world light (opaque bricks only, as they are what occludes)
|
||||
std::vector<float> ao;
|
||||
if (options.ao.enabled) {
|
||||
ao = opaqueAo && opaqueAo->size() == model.opaque.positions.size() ? *opaqueAo : AmbientOcclusion(model.opaque, model.opaque, options.ao.distance, options.ao.samples);
|
||||
}
|
||||
|
||||
// The sun's shadows: a depth map seen from the sun, looked up with a few taps for the sun's soft edge
|
||||
const int shadowSize = 1024;
|
||||
const OrthoView sunView(center, radius * 1.05f, light, shadowSize);
|
||||
std::vector<float> shadowDepth;
|
||||
if (options.shadows > 0.0f) {
|
||||
shadowDepth.assign(static_cast<size_t>(shadowSize) * shadowSize, INF);
|
||||
const auto& mesh = model.opaque;
|
||||
std::vector<glm::vec3> screen(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) screen[v] = sunView.Project(mesh.positions[v]);
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
if ((i & 0xFFFF) == 0) UgcThrottle::Checkpoint();
|
||||
Rasterize(shadowSize, shadowSize, screen[mesh.indices[i]], screen[mesh.indices[i + 1]], screen[mesh.indices[i + 2]], [&](int x, int y, float z, float, float, float) {
|
||||
auto& stored = shadowDepth[static_cast<size_t>(y) * shadowSize + x];
|
||||
stored = std::min(stored, z);
|
||||
});
|
||||
}
|
||||
}
|
||||
const float shadowBias = sunView.PixelSize() * 2.0f;
|
||||
const auto sunlit = [&](const glm::vec3& position) {
|
||||
if (shadowDepth.empty()) return 1.0f;
|
||||
const auto p = sunView.Project(position);
|
||||
float lit = 0.0f;
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
const int x = static_cast<int>(p.x) + dx, y = static_cast<int>(p.y) + dy;
|
||||
if (x < 0 || y < 0 || x >= shadowSize || y >= shadowSize || p.z <= shadowDepth[static_cast<size_t>(y) * shadowSize + x] + shadowBias) lit += 1.0f;
|
||||
}
|
||||
}
|
||||
const float shadowed = 1.0f - lit / 9.0f;
|
||||
return 1.0f - std::clamp(options.shadows, 0.0f, 1.0f) * shadowed;
|
||||
};
|
||||
const glm::vec3 toCamera = glm::normalize(dir);
|
||||
const glm::vec3 halfway = glm::normalize(light + toCamera);
|
||||
bool anyGlitter = false;
|
||||
for (const auto* mesh : { &model.opaque, &model.transparent }) {
|
||||
anyGlitter = anyGlitter || std::find(mesh->looks.begin(), mesh->looks.end(), UgcModel::eLook::GLITTER) != mesh->looks.end();
|
||||
}
|
||||
const auto glitterAlpha = anyGlitter ? UgcGlitter::FleckAlpha(options.glitter.flecks) : std::vector<uint8_t>{};
|
||||
|
||||
const auto shade = [&](const UgcModel::Mesh& mesh, bool isOpaque, uint32_t i0, uint32_t i1, uint32_t i2, float w0, float w1, float w2) {
|
||||
glm::vec3 normal(0.0f, 1.0f, 0.0f);
|
||||
if (mesh.normals.size() == mesh.positions.size()) {
|
||||
normal = mesh.normals[i0] * w0 + mesh.normals[i1] * w1 + mesh.normals[i2] * w2;
|
||||
const auto length = glm::length(normal);
|
||||
normal = length > 0.0f ? normal / length : glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
if (glm::dot(normal, dir) < 0.0f) normal = -normal; // the back of a face
|
||||
}
|
||||
glm::vec4 base(0.63f, 0.63f, 0.63f, 1.0f);
|
||||
if (mesh.colors.size() == mesh.positions.size()) base = mesh.colors[i0] * w0 + mesh.colors[i1] * w1 + mesh.colors[i2] * w2;
|
||||
const float occlusion = isOpaque && ao.size() == mesh.positions.size() ? ao[i0] * w0 + ao[i1] * w1 + ao[i2] * w2 : 1.0f;
|
||||
const float strength = std::clamp(options.ao.strength, 0.0f, 1.0f);
|
||||
const auto position = mesh.positions[i0] * w0 + mesh.positions[i1] * w1 + mesh.positions[i2] * w2;
|
||||
const float direct = std::max(0.0f, glm::dot(normal, light));
|
||||
const float sun = direct > 0.0f ? sunlit(position + normal * shadowBias) : 0.0f;
|
||||
// Diffuse: the world's light (radiance `ambient`), a fill from the camera and the sun's (irradiances, over pi)
|
||||
const float lighting = options.ambient * (1.0f - strength * (1.0f - occlusion)) +
|
||||
(options.fill * std::max(0.0f, glm::dot(normal, toCamera)) + options.sunStrength * direct * sun) / 3.14159265f;
|
||||
// The sun's highlight (Blinn-Phong), white, on top of the color
|
||||
const float highlight = direct > 0.0f ? options.specular * options.sunStrength / 3.14159265f * sun * std::pow(std::max(0.0f, glm::dot(normal, halfway)), std::max(options.shininess, 1.0f)) : 0.0f;
|
||||
const float exposure = std::max(options.exposure, 0.0f);
|
||||
const auto look = mesh.looks.size() == mesh.positions.size() && (isOpaque || mesh.looks[i0] == UgcModel::eLook::GLITTER) ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
|
||||
if (look == UgcModel::eLook::GLITTER) {
|
||||
// LEGO-AnimUV: lerp(vertex color, the texture's white, its alpha), then lit as plastic
|
||||
const float fleck = UgcGlitter::Sample(glitterAlpha, UgcGlitter::Uv(position, normal, options.glitter.tile));
|
||||
base = glm::vec4(glm::mix(glm::vec3(base), glm::vec3(1.0f), fleck), base.a);
|
||||
}
|
||||
if (look == UgcModel::eLook::PLASTIC || look == UgcModel::eLook::GLITTER) {
|
||||
return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure,
|
||||
(ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f));
|
||||
}
|
||||
const glm::vec3 linear(ToLinear(base.r), ToLinear(base.g), ToLinear(base.b));
|
||||
glm::vec3 shaded = (linear * lighting + glm::vec3(highlight)) * exposure;
|
||||
if (look == UgcModel::eLook::METAL || look == UgcModel::eLook::BRUSHED) {
|
||||
// A reflection of a bright sky over a dark ground, tinted by the color (polished: sharp; brushed: blurred
|
||||
// and duller), over a dimmed diffuse light, and the sun's highlight in the metal's color
|
||||
const bool polished = look == UgcModel::eLook::METAL;
|
||||
const auto reflected = glm::reflect(-toCamera, normal);
|
||||
const float up = polished ? glm::smoothstep(-0.15f, 0.5f, reflected.y) : 0.5f + 0.5f * reflected.y;
|
||||
const float environment = glm::mix(0.06f, polished ? 1.1f : 0.75f, up);
|
||||
const float spot = direct > 0.0f ? options.sunStrength / 3.14159265f * sun *
|
||||
std::pow(std::max(0.0f, glm::dot(normal, halfway)), polished ? 180.0f : 30.0f) * (polished ? 4.0f : 1.2f) : 0.0f;
|
||||
shaded = linear * (lighting * 0.35f + environment + spot) * exposure;
|
||||
} else if (look == UgcModel::eLook::GLOW) {
|
||||
// LEGO-Emissive: lerp(lit, vertex color, vertex alpha * the material's emissive red)
|
||||
shaded = glm::mix(shaded, linear, std::clamp(options.glowEmissive, 0.0f, 1.0f));
|
||||
}
|
||||
return glm::vec4(shaded, std::clamp(base.a, 0.0f, 1.0f));
|
||||
};
|
||||
|
||||
// Opaque first, with the depth buffer
|
||||
{
|
||||
const auto& mesh = model.opaque;
|
||||
std::vector<glm::vec3> screen(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) screen[v] = project(mesh.positions[v]);
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
const auto i0 = mesh.indices[i], i1 = mesh.indices[i + 1], i2 = mesh.indices[i + 2];
|
||||
Rasterize(n, n, screen[i0], screen[i1], screen[i2], [&](int x, int y, float z, float w0, float w1, float w2) {
|
||||
const size_t index = static_cast<size_t>(y) * n + x;
|
||||
if (z >= depth[index]) return;
|
||||
depth[index] = z;
|
||||
const auto shaded = shade(mesh, true, i0, i1, i2, w0, w1, w2);
|
||||
color[index] = glm::vec4(glm::vec3(shaded), 1.0f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Then transparent triangles, farthest first, blended over it
|
||||
{
|
||||
const auto& mesh = model.transparent;
|
||||
std::vector<glm::vec3> screen(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) screen[v] = project(mesh.positions[v]);
|
||||
std::vector<size_t> order(mesh.indices.size() / 3);
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
const auto depthOf = [&](size_t t) { return screen[mesh.indices[t * 3]].z + screen[mesh.indices[t * 3 + 1]].z + screen[mesh.indices[t * 3 + 2]].z; };
|
||||
std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { return depthOf(a) > depthOf(b); });
|
||||
for (const auto t : order) {
|
||||
const auto i0 = mesh.indices[t * 3], i1 = mesh.indices[t * 3 + 1], i2 = mesh.indices[t * 3 + 2];
|
||||
Rasterize(n, n, screen[i0], screen[i1], screen[i2], [&](int x, int y, float z, float w0, float w1, float w2) {
|
||||
const size_t index = static_cast<size_t>(y) * n + x;
|
||||
if (z >= depth[index]) return;
|
||||
const auto shaded = shade(mesh, false, i0, i1, i2, w0, w1, w2);
|
||||
const float alpha = shaded.a;
|
||||
auto& target = color[index];
|
||||
target = glm::vec4(glm::vec3(shaded) * alpha + glm::vec3(target) * (1.0f - alpha), alpha + target.a * (1.0f - alpha));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Box filter down to the icon's size
|
||||
const float samples = static_cast<float>(supersample * supersample);
|
||||
for (int y = 0; y < size; y++) {
|
||||
for (int x = 0; x < size; x++) {
|
||||
glm::vec4 sum(0.0f);
|
||||
for (int sy = 0; sy < supersample; sy++) {
|
||||
for (int sx = 0; sx < supersample; sx++) sum += color[static_cast<size_t>(y * supersample + sy) * n + (x * supersample + sx)];
|
||||
}
|
||||
sum /= samples;
|
||||
uint8_t* out = &image.rgba[(static_cast<size_t>(y) * size + x) * 4];
|
||||
if (sum.a <= 0.0f) continue;
|
||||
// sRGB, then the contrast around its middle grey
|
||||
const auto tone = [&options](float linear) { return std::clamp(0.5f + (ToSrgb(linear) - 0.5f) * options.contrast, 0.0f, 1.0f); };
|
||||
out[0] = static_cast<uint8_t>(std::lround(tone(sum.r / sum.a) * 255.0f));
|
||||
out[1] = static_cast<uint8_t>(std::lround(tone(sum.g / sum.a) * 255.0f));
|
||||
out[2] = static_cast<uint8_t>(std::lround(tone(sum.b / sum.a) * 255.0f));
|
||||
out[3] = static_cast<uint8_t>(std::lround(std::clamp(sum.a, 0.0f, 1.0f) * 255.0f));
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
84
dUgcServer/Render/UgcRender.h
Normal file
84
dUgcServer/Render/UgcRender.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "UgcGlitter.h"
|
||||
#include "UgcModel.h"
|
||||
|
||||
/**
|
||||
* A small software rasterizer (no GPU or display needed) for what the UGC server draws: the icons, and the occlusion
|
||||
* rays that bake ambient occlusion into the vertex colors.
|
||||
*/
|
||||
namespace UgcRender {
|
||||
// 8-bit RGBA, rows top to bottom, not premultiplied
|
||||
struct Image {
|
||||
int width{};
|
||||
int height{};
|
||||
std::vector<uint8_t> rgba;
|
||||
};
|
||||
|
||||
struct AoOptions {
|
||||
bool enabled{ true };
|
||||
float distance{ 5.0f }; // LU Toolbox's AO distance (Bake Lighting, AO Only)
|
||||
int samples{ 64 }; // rays per vertex (AO Samples)
|
||||
float strength{ 1.0f }; // 0 leaves the colors, 1 is the full bake
|
||||
float glowStrength{ 6.0f }; // what glowing colors add to the light (Glow Strength 3 x Glow Multiplier 2)
|
||||
};
|
||||
|
||||
/**
|
||||
* How an icon is drawn. The values that can be set (settings icon_*, presets and overrides) are listed once, with
|
||||
* their ranges and shipped defaults, in UgcIconParams; the ones here are only what the struct starts with.
|
||||
*/
|
||||
struct IconOptions {
|
||||
int size{ 128 };
|
||||
int supersample{ 4 };
|
||||
float yawDegrees{ 53.36f }; // camera around the model, from +Z towards +X
|
||||
float pitchDegrees{ 19.54f }; // camera above the model
|
||||
float fovDegrees{ 39.6f };
|
||||
float margin{ 1.03f }; // 1 fills the icon, more leaves a border
|
||||
float offsetX{}; // the model moved right by this share of the icon's width (after framing)
|
||||
float offsetY{}; // and up by this share of its height
|
||||
float modelYawDegrees{}; // the model turned (UgcIconPose::ModelRotation), after modelRotation
|
||||
float modelPitchDegrees{};
|
||||
float modelRollDegrees{};
|
||||
glm::mat4 modelRotation{ 1.0f }; // the model's own turn before that (a build type's AdditionalModelRotation)
|
||||
float sunYawDegrees{ 21.0f };
|
||||
float sunPitchDegrees{ 50.3f };
|
||||
float sunStrength{ 2.5f };
|
||||
float ambient{ 0.192f }; // the world light (radiance) every face gets
|
||||
float fill{ 0.0f }; // a light from the camera (irradiance), what lifts the sides facing the viewer
|
||||
float specular{ 0.0f }; // the sun's highlight on the plastic
|
||||
float shininess{ 60.0f }; // how tight the highlight is
|
||||
float exposure{ 1.0f }; // everything times this before it's written as sRGB
|
||||
float contrast{ 1.0f }; // around the middle grey of the sRGB result
|
||||
float shadows{ 1.0f }; // how much the sun's shadows darken, 0 to 1
|
||||
AoOptions ao{ false, 5.0f, 32, 1.0f, 0.0f };
|
||||
float glowEmissive{ 1.0f }; // how far glowing shapes go from lit to their plain color (the glow_emissive setting)
|
||||
UgcGlitter::Params glitter; // the glitter's flecks (glitter_size, glitter_density), drawn where they are at the start
|
||||
};
|
||||
|
||||
// The model drawn from the icon's camera, framed to fit, on a transparent background. `opaqueAo`: the opaque mesh's
|
||||
// ambient occlusion (AmbientOcclusion) when it is known already, else it is worked out when options.ao wants it.
|
||||
// Opaque vertices with a look (UgcModel::Mesh::looks) are drawn roughly as the game's shaders draw them: GLOW goes
|
||||
// from lit to its plain color by glowEmissive (LEGO-Emissive), METAL and BRUSHED dim the diffuse light and add a
|
||||
// sky-and-ground reflection tinted by the color and a highlight, sharp for polished metal and broad for brushed
|
||||
// steel (Polished Metal, Brushed Steel: an environment map tinted by the vertex color). GLITTER vertices (opaque or
|
||||
// transparent) get the glitter texture's white flecks over their color before the light (LEGO-AnimUV), still.
|
||||
Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector<float>* opaqueAo = nullptr);
|
||||
|
||||
/**
|
||||
* Ambient occlusion of each vertex of `mesh`: the share of `samples` rays (cosine weighted around the vertex
|
||||
* normal, the same pattern every time) that leave without hitting a triangle of `occluders` within `distance`.
|
||||
* 1 is open, 0 fully hidden.
|
||||
*/
|
||||
std::vector<float> AmbientOcclusion(const UgcModel::Mesh& mesh, const UgcModel::Mesh& occluders, float distance, int samples);
|
||||
|
||||
/**
|
||||
* LU Toolbox's Bake Lighting with AO Only (its defaults): the opaque mesh's occlusion (transparent bricks are hidden
|
||||
* while baking and not baked) plus the glow colors, multiplied into the vertex colors.
|
||||
*/
|
||||
std::vector<float> BakeAo(UgcModel::Model& model, const AoOptions& options); // the occlusion used, per opaque vertex
|
||||
}
|
||||
Reference in New Issue
Block a user