mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Brighter icons: a world light, a fill from the camera, a highlight, exposure and contrast; with the defaults the icons' mean luminance matches the game's own model icons (118 against 120 on a scratch set). Every icon parameter is listed once (UgcIconParams: key, setting, range, default); the settings, the dashboard's settings entries and the icon editor come from that list. Presets per kind (player models, each car or rocket build type from ModularBuildComponent) and overrides per model or module combination are in ugc_icon_settings. Saved models wait ugc_debounce_seconds (sharedconfig) after the owner's last save before they're made (ugc.process_after); a client asking for one, the owner leaving the world or a reset ends the wait. Cars and rockets: one icon per combination of modules (sorted LOTs), shared by every build of it; builds of a combination made already are marked made right away, the client's per-blueprint downloads serve the shared files. The dashboard can delete one item's files, purge by filter or all, preview icons with any values on the UGC server (/admin routes, master password), save presets and overrides, and draw a kind's icons again (icons only). Migrations dlu/mysql/86 and dlu/sqlite/69. Not done yet: the dashboard editor's lighting controls in the page script (routes are there), docs for it, the empty-model state, /ugc?item= links, the shared fetch helper; the storage size and property loading bugs are next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
568 lines
26 KiB
C++
568 lines
26 KiB
C++
#include "UgcRender.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <numeric>
|
|
#include <unordered_map>
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
#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; }
|
|
};
|
|
|
|
void Bounds(const UgcModel::Model& model, glm::vec3& center, float& radius) {
|
|
glm::vec3 min{}, max{};
|
|
if (!model.Bounds(min, max)) {
|
|
center = glm::vec3(0.0f);
|
|
radius = 1.0f;
|
|
return;
|
|
}
|
|
center = (min + max) * 0.5f;
|
|
radius = std::max(glm::length(max - min) * 0.5f, 0.01f);
|
|
}
|
|
}
|
|
|
|
namespace UgcRender {
|
|
std::vector<glm::vec3> SphereDirections() {
|
|
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
|
|
const std::vector<glm::vec3> corners = {
|
|
{ -1, t, 0 }, { 1, t, 0 }, { -1, -t, 0 }, { 1, -t, 0 },
|
|
{ 0, -1, t }, { 0, 1, t }, { 0, -1, -t }, { 0, 1, -t },
|
|
{ t, 0, -1 }, { t, 0, 1 }, { -t, 0, -1 }, { -t, 0, 1 },
|
|
};
|
|
std::vector<glm::vec3> directions;
|
|
for (const auto& corner : corners) directions.push_back(glm::normalize(corner));
|
|
// Edge centres: the pairs of corners that are neighbours (the shortest distance apart)
|
|
const float edge = glm::length(corners[0] - corners[1]);
|
|
for (size_t i = 0; i < corners.size(); i++) {
|
|
for (size_t j = i + 1; j < corners.size(); j++) {
|
|
if (std::abs(glm::length(corners[i] - corners[j]) - edge) < 1e-3f) directions.push_back(glm::normalize(corners[i] + corners[j]));
|
|
}
|
|
}
|
|
return directions;
|
|
}
|
|
|
|
OptimizeResult Optimize(UgcModel::Model& model, const OptimizeOptions& options) {
|
|
OptimizeResult result;
|
|
auto& opaque = model.opaque;
|
|
result.trianglesBefore = opaque.TriangleCount() + model.transparent.TriangleCount();
|
|
if (opaque.Empty() || !options.removeHidden) return result;
|
|
|
|
glm::vec3 center{};
|
|
float radius{};
|
|
Bounds(model, center, radius);
|
|
radius *= 1.02f;
|
|
const int resolution = std::clamp(options.resolution, 64, 4096);
|
|
const size_t pixels = static_cast<size_t>(resolution) * resolution;
|
|
std::vector<float> depth(pixels);
|
|
std::vector<uint32_t> ids(pixels);
|
|
std::vector<glm::vec3> screen(opaque.positions.size());
|
|
|
|
const size_t triangles = opaque.TriangleCount();
|
|
std::vector<bool> visible(triangles, false);
|
|
std::vector<bool> facing(triangles, true);
|
|
std::vector<glm::vec3> faceNormals(triangles, glm::vec3(0.0f));
|
|
for (size_t t = 0; t < triangles; t++) {
|
|
if (opaque.normals.size() != opaque.positions.size()) break;
|
|
faceNormals[t] = glm::normalize(opaque.normals[opaque.indices[t * 3]] + opaque.normals[opaque.indices[t * 3 + 1]] + opaque.normals[opaque.indices[t * 3 + 2]] + glm::vec3(1e-6f));
|
|
}
|
|
// Without normals nothing is culled
|
|
if (opaque.normals.size() != opaque.positions.size()) std::fill(faceNormals.begin(), faceNormals.end(), glm::vec3(0.0f));
|
|
|
|
for (const auto& direction : SphereDirections()) {
|
|
// A ground plane under the model hides everything from below
|
|
if (options.groundPlane && direction.y < -0.05f) continue;
|
|
UgcThrottle::Checkpoint();
|
|
const OrthoView view(center, radius, direction, resolution);
|
|
const float bias = view.PixelSize();
|
|
std::fill(depth.begin(), depth.end(), INF);
|
|
std::fill(ids.begin(), ids.end(), 0);
|
|
for (size_t v = 0; v < opaque.positions.size(); v++) screen[v] = view.Project(opaque.positions[v]);
|
|
// Faces turned away can't be seen from here (they're seen from the directions they face); their vertex
|
|
// normals say which way they face, which doesn't depend on the files' winding
|
|
for (size_t t = 0; t < triangles; t++) facing[t] = glm::dot(faceNormals[t], direction) > -0.1f;
|
|
for (size_t t = 0; t < triangles; t++) {
|
|
if (!facing[t]) continue;
|
|
if ((t & 0x3FFF) == 0) UgcThrottle::Checkpoint();
|
|
const auto id = static_cast<uint32_t>(t + 1);
|
|
Rasterize(resolution, resolution, screen[opaque.indices[t * 3]], screen[opaque.indices[t * 3 + 1]], screen[opaque.indices[t * 3 + 2]],
|
|
[&](int x, int y, float z, float, float, float) {
|
|
auto& stored = depth[static_cast<size_t>(y) * resolution + x];
|
|
if (z < stored) {
|
|
stored = z;
|
|
ids[static_cast<size_t>(y) * resolution + x] = id;
|
|
}
|
|
});
|
|
}
|
|
for (const auto id : ids) {
|
|
if (id != 0) visible[id - 1] = true;
|
|
}
|
|
|
|
// Triangles too small or thin to cover a pixel centre: kept when their centre isn't behind what was drawn.
|
|
// Bigger ones that show would have covered one.
|
|
const float smallArea = 2.0f; // pixels
|
|
for (size_t t = 0; t < triangles; t++) {
|
|
if (visible[t] || !facing[t]) continue;
|
|
const auto& a = screen[opaque.indices[t * 3]];
|
|
const auto& b = screen[opaque.indices[t * 3 + 1]];
|
|
const auto& c = screen[opaque.indices[t * 3 + 2]];
|
|
if (std::abs(Edge(a, b, c.x, c.y)) * 0.5f > smallArea) continue;
|
|
const auto centre = (a + b + c) / 3.0f;
|
|
const int x = static_cast<int>(centre.x), y = static_cast<int>(centre.y);
|
|
if (x < 0 || y < 0 || x >= resolution || y >= resolution || centre.z <= depth[static_cast<size_t>(y) * resolution + x] + bias) visible[t] = true;
|
|
}
|
|
}
|
|
|
|
for (size_t t = 0; t < triangles; t++) result.trianglesRemoved += visible[t] ? 0 : 1;
|
|
result.kept = visible;
|
|
UgcModel::KeepTriangles(opaque, visible);
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
model.opaque.Transform(options.modelRotation);
|
|
model.transparent.Transform(options.modelRotation);
|
|
glm::vec3 center{};
|
|
float radius{};
|
|
Bounds(model, center, radius);
|
|
|
|
const float yaw = glm::radians(options.yawDegrees), pitch = glm::radians(options.pitchDegrees);
|
|
const glm::vec3 dir(std::sin(yaw) * std::cos(pitch), std::sin(pitch), std::cos(yaw) * std::cos(pitch));
|
|
const float fov = glm::radians(std::clamp(options.fovDegrees, 1.0f, 120.0f));
|
|
const float distance = radius / std::sin(fov * 0.5f);
|
|
const glm::vec3 eye = center + dir * distance;
|
|
const glm::mat4 viewProjection = glm::perspective(fov, 1.0f, std::max(distance - radius * 1.5f, distance * 0.01f), distance + radius * 1.5f) *
|
|
glm::lookAt(eye, center, glm::vec3(0.0f, 1.0f, 0.0f));
|
|
|
|
// Frame the model: its projected bounds, scaled to fill the icon less the margin
|
|
float minX = INF, minY = INF, maxX = -INF, maxY = -INF;
|
|
for (const auto* mesh : { &model.opaque, &model.transparent }) {
|
|
for (const auto& position : mesh->positions) {
|
|
const auto clip = viewProjection * glm::vec4(position, 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 image;
|
|
const float centerX = (minX + maxX) * 0.5f, centerY = (minY + maxY) * 0.5f;
|
|
const float scale = 2.0f / (std::max({ maxX - minX, maxY - minY, 1e-6f }) * std::max(options.margin, 0.1f));
|
|
const auto project = [&](const glm::vec3& position) {
|
|
const auto clip = viewProjection * glm::vec4(position, 1.0f);
|
|
const float w = clip.w > 1e-6f ? clip.w : 1e-6f;
|
|
return glm::vec3((0.5f + options.offsetX + (clip.x / w - centerX) * scale * 0.5f) * n, (0.5f - options.offsetY - (clip.y / w - centerY) * scale * 0.5f) * n, clip.z / w);
|
|
};
|
|
|
|
// 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);
|
|
|
|
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);
|
|
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));
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
}
|