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:
Aaron Kimbrell
2026-09-28 19:51:13 -05:00
parent 2277cbd7e2
commit 7c32779e89
32 changed files with 70 additions and 25 deletions

View File

@@ -0,0 +1,228 @@
#include "UgcBricks.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
#include "tinyxml2.h"
#include "ZCompression.h"
namespace {
constexpr int32_t GEOMETRY_MAGIC = 0x42473031; // "10GB"
constexpr uint32_t MAX_GEOMETRY_PARTS = 64;
template<typename T>
bool ReadAt(std::string_view data, size_t offset, T& value) {
if (offset > data.size() || sizeof(T) > data.size() - offset) return false;
std::memcpy(&value, data.data() + offset, sizeof(T));
return true;
}
std::string Lower(std::string_view text) {
std::string out(text);
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return out;
}
}
namespace UgcBricks {
std::optional<Geometry> ParseGeometry(std::string_view data) {
int32_t magic{}, vertexCount{}, indexCount{}, options{};
if (!ReadAt(data, 0, magic) || magic != GEOMETRY_MAGIC || !ReadAt(data, 4, vertexCount) || !ReadAt(data, 8, indexCount) ||
!ReadAt(data, 12, options) || vertexCount < 0 || indexCount < 0 || indexCount % 3 != 0) {
return std::nullopt;
}
const auto vertices = static_cast<size_t>(vertexCount);
const auto indices = static_cast<size_t>(indexCount);
size_t offset = 16;
const bool hasUvs = (options & 3) == 3;
const size_t needed = offset + vertices * 24 + (hasUvs ? vertices * 8 : 0) + indices * 4;
if (needed > data.size()) return std::nullopt;
Geometry geometry;
geometry.positions.resize(vertices * 3);
std::memcpy(geometry.positions.data(), data.data() + offset, vertices * 12);
offset += vertices * 12;
geometry.normals.resize(vertices * 3);
std::memcpy(geometry.normals.data(), data.data() + offset, vertices * 12);
offset += vertices * 12;
if (hasUvs) offset += vertices * 8;
geometry.indices.resize(indices);
std::memcpy(geometry.indices.data(), data.data() + offset, indices * 4);
for (const auto index : geometry.indices) {
if (index >= vertices) return std::nullopt;
}
return geometry;
}
std::map<uint32_t, Material> ParseMaterials(std::string_view xml) {
std::map<uint32_t, Material> materials;
tinyxml2::XMLDocument doc;
if (doc.Parse(xml.data(), xml.size()) != tinyxml2::XML_SUCCESS) return materials;
const auto* root = doc.FirstChildElement("Materials");
if (!root) return materials;
for (const auto* element = root->FirstChildElement("Material"); element; element = element->NextSiblingElement("Material")) {
const auto id = element->UnsignedAttribute("MatID", 0);
if (id == 0) continue;
const auto channel = [element](const char* name, uint32_t fallback) {
return static_cast<uint8_t>(std::min<uint32_t>(element->UnsignedAttribute(name, fallback), 255));
};
materials[id] = Material{ channel("Red", 160), channel("Green", 160), channel("Blue", 160), channel("Alpha", 255), element->Attribute("MaterialType") ? element->Attribute("MaterialType") : "" };
}
return materials;
}
std::optional<std::string> ReadZipEntry(std::string_view zip, std::string_view name) {
// The end of central directory record is in the last 64 KiB + 22 bytes
constexpr uint32_t END_SIGNATURE = 0x06054b50;
constexpr uint32_t CENTRAL_SIGNATURE = 0x02014b50;
constexpr uint32_t LOCAL_SIGNATURE = 0x04034b50;
if (zip.size() < 22) return std::nullopt;
size_t end = std::string_view::npos;
const size_t lowest = zip.size() > 22 + 65535 ? zip.size() - 22 - 65535 : 0;
for (size_t i = zip.size() - 22 + 1; i-- > lowest;) {
uint32_t signature{};
if (ReadAt(zip, i, signature) && signature == END_SIGNATURE) {
end = i;
break;
}
}
if (end == std::string_view::npos) return std::nullopt;
uint16_t entries{};
uint32_t directoryOffset{};
if (!ReadAt(zip, end + 10, entries) || !ReadAt(zip, end + 16, directoryOffset)) return std::nullopt;
const auto wanted = Lower(name);
size_t offset = directoryOffset;
for (uint16_t i = 0; i < entries; i++) {
uint32_t signature{}, compressedSize{}, size{}, localOffset{};
uint16_t method{}, nameLength{}, extraLength{}, commentLength{};
if (!ReadAt(zip, offset, signature) || signature != CENTRAL_SIGNATURE || !ReadAt(zip, offset + 10, method) ||
!ReadAt(zip, offset + 20, compressedSize) || !ReadAt(zip, offset + 24, size) || !ReadAt(zip, offset + 28, nameLength) ||
!ReadAt(zip, offset + 30, extraLength) || !ReadAt(zip, offset + 32, commentLength) || !ReadAt(zip, offset + 42, localOffset) ||
offset + 46 + nameLength > zip.size()) {
return std::nullopt;
}
const auto entryName = zip.substr(offset + 46, nameLength);
offset += 46 + nameLength + extraLength + commentLength;
if (Lower(entryName) != wanted) continue;
uint16_t localNameLength{}, localExtraLength{};
if (!ReadAt(zip, localOffset, signature) || signature != LOCAL_SIGNATURE || !ReadAt(zip, localOffset + 26, localNameLength) ||
!ReadAt(zip, localOffset + 28, localExtraLength)) {
return std::nullopt;
}
const size_t dataStart = static_cast<size_t>(localOffset) + 30 + localNameLength + localExtraLength;
if (dataStart > zip.size() || compressedSize > zip.size() - dataStart) return std::nullopt;
const auto data = zip.substr(dataStart, compressedSize);
if (method == 0) return std::string(data);
if (method == 8) return ZCompression::InflateRaw(data, size);
return std::nullopt;
}
return std::nullopt;
}
std::optional<std::filesystem::path> ResolvePath(const std::filesystem::path& root, std::string_view relative) {
std::string normalized(relative);
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::filesystem::path current = root;
std::stringstream parts(normalized);
std::string part;
std::error_code error;
while (std::getline(parts, part, '/')) {
if (part.empty() || part == ".") continue;
if (part == "..") return std::nullopt;
if (std::filesystem::exists(current / part, error)) {
current /= part;
continue;
}
const auto wanted = Lower(part);
bool found = false;
for (std::filesystem::directory_iterator it(current, error), endIt; !error && it != endIt; it.increment(error)) {
if (Lower(it->path().filename().string()) == wanted) {
current = it->path();
found = true;
break;
}
}
if (!found) return std::nullopt;
}
if (!std::filesystem::is_regular_file(current, error)) return std::nullopt;
return current;
}
std::optional<std::string> ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) return std::nullopt;
std::ostringstream contents;
contents << file.rdbuf();
return contents.str();
}
BrickLibrary::BrickLibrary(std::filesystem::path res, uint32_t lod, FileReader reader)
: m_Res(std::move(res)), m_Lod(std::min<uint32_t>(lod, 2)), m_Reader(std::move(reader)) {}
std::optional<std::string> BrickLibrary::Read(const std::string& relative) const {
if (m_Reader) {
if (auto data = m_Reader(relative)) return data;
}
const auto path = ResolvePath(m_Res, relative);
return path ? ReadFile(*path) : std::nullopt;
}
bool BrickLibrary::LoadMaterials() {
const auto zip = Read("brickdb.zip");
const auto xml = zip ? ReadZipEntry(*zip, "Materials.xml") : std::nullopt;
if (!xml) return false;
auto materials = ParseMaterials(*xml);
if (materials.empty()) return false;
SetMaterials(std::move(materials));
return true;
}
void BrickLibrary::SetMaterials(std::map<uint32_t, Material> materials) {
std::lock_guard lock(m_Mutex);
m_Materials = std::move(materials);
}
Material BrickLibrary::GetMaterial(uint32_t id) const {
// Written once before the workers start, only read after
const auto it = m_Materials.find(id);
return it != m_Materials.end() ? it->second : Material{};
}
bool BrickLibrary::HasMaterial(uint32_t id) const {
// Written once before the workers start, only read after
return m_Materials.contains(id);
}
std::shared_ptr<const std::vector<Geometry>> BrickLibrary::GetDesign(uint32_t design, std::optional<uint32_t> lodLevel) {
const uint32_t lod = std::min<uint32_t>(lodLevel.value_or(m_Lod), 2);
const uint64_t key = (static_cast<uint64_t>(lod) << 32) | design;
{
std::lock_guard lock(m_Mutex);
if (const auto it = m_Designs.find(key); it != m_Designs.end()) return it->second;
}
// Loaded outside the lock; two threads loading the same design at once is harmless
auto parts = std::make_shared<std::vector<Geometry>>();
const auto folder = "brickprimitives/lod" + std::to_string(lod) + "/";
for (uint32_t index = 0; index < MAX_GEOMETRY_PARTS; index++) {
const auto name = std::to_string(design) + ".g" + (index == 0 ? "" : std::to_string(index));
const auto data = Read(folder + name);
if (!data) break;
auto geometry = ParseGeometry(*data);
if (!geometry) break;
parts->push_back(std::move(*geometry));
}
if (parts->empty() && lod > 0) return GetDesign(design, lod - 1);
std::lock_guard lock(m_Mutex);
return m_Designs.emplace(key, std::move(parts)).first->second;
}
size_t BrickLibrary::CachedDesigns() const {
std::lock_guard lock(m_Mutex);
return m_Designs.size();
}
}

View File

@@ -0,0 +1,98 @@
#pragma once
#include <array>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
/**
* The client's brick data the UGC server builds models from: LDD geometry (res/brickprimitives/lod<n>/<design>.g, .g1,
* ...) and the material colors (Materials.xml in res/brickdb.zip). The parsers are pure; BrickLibrary loads and caches
* files and may be used from any thread. Files are read through a FileReader (the server's reads packed and unpacked
* clients alike), falling back to loose files under the res folder.
*/
namespace UgcBricks {
// One LDD .g file: a triangle mesh with a normal per vertex
struct Geometry {
std::vector<float> positions; // x, y, z per vertex
std::vector<float> normals; // x, y, z per vertex
std::vector<uint32_t> indices;
};
// An LDD material: sRGB color and opacity, 0-255, and its MaterialType (shinyPlastic, shinySteel, glitter, ...)
struct Material {
uint8_t r{ 160 };
uint8_t g{ 160 };
uint8_t b{ 160 };
uint8_t a{ 255 };
std::string type;
bool Transparent() const { return a < 255; }
};
// A .g file ("10GB" magic, counts, positions, normals, texture coordinates for decorated parts, indices)
std::optional<Geometry> ParseGeometry(std::string_view data);
// Materials.xml: MatID -> color and type
std::map<uint32_t, Material> ParseMaterials(std::string_view xml);
// A file from a zip archive (stored or deflated), matched without regard to case; nullopt when it isn't there
std::optional<std::string> ReadZipEntry(std::string_view zip, std::string_view name);
// `relative` (either slash, any case) under `root`, matching each part without regard to case as the client's
// files are named on Windows; nullopt when there is no such file
std::optional<std::filesystem::path> ResolvePath(const std::filesystem::path& root, std::string_view relative);
// Reads a whole file; nullopt when it can't
std::optional<std::string> ReadFile(const std::filesystem::path& path);
// Reads a client file by its path under res/ ("brickdb.zip", "brickprimitives/lod0/3001.g"); nullopt when there is
// none. Called from any thread.
using FileReader = std::function<std::optional<std::string>(const std::string& relative)>;
class BrickLibrary {
public:
// `res` is the client's res folder; `lod` the brickprimitives level used (0 is the most detailed); `reader`
// the files' source, tried before loose files under `res`
BrickLibrary(std::filesystem::path res, uint32_t lod, FileReader reader = {});
// Loads the material colors; false when brickdb.zip or its Materials.xml can't be read
bool LoadMaterials();
// For tests and callers that have the colors already
void SetMaterials(std::map<uint32_t, Material> materials);
// A material's color (a grey when the id is unknown)
Material GetMaterial(uint32_t id) const;
// Whether the client's Materials.xml has the id
bool HasMaterial(uint32_t id) const;
// Every geometry file of a design, in order (.g, .g1, ...); empty when the design has none. Loaded once.
// `lod`: the brickprimitives level, the library's own when omitted; a design without that level uses the
// next more detailed one.
std::shared_ptr<const std::vector<Geometry>> GetDesign(uint32_t design, std::optional<uint32_t> lod = std::nullopt);
// Designs loaded (for the memory report)
size_t CachedDesigns() const;
const std::filesystem::path& GetResPath() const { return m_Res; }
private:
// A file from the reader, else loose under m_Res
std::optional<std::string> Read(const std::string& relative) const;
std::filesystem::path m_Res;
uint32_t m_Lod;
FileReader m_Reader;
std::map<uint32_t, Material> m_Materials;
mutable std::mutex m_Mutex;
std::map<uint64_t, std::shared_ptr<const std::vector<Geometry>>> m_Designs; // lod << 32 | design
};
}

View File

@@ -0,0 +1,59 @@
#include "UgcCdClient.h"
#include <optional>
#include "CDClientDatabase.h"
#include "UgcModular.h"
namespace {
constexpr int32_t MODULE_COMPONENT = 28;
constexpr int32_t RENDER_COMPONENT = 2;
}
namespace UgcCdClient {
bool GatherModular(const std::string& modules, UgcJobs::ModularInput& input, std::string& error) {
const auto lots = UgcModular::ParseModuleLots(modules);
if (lots.empty()) {
error = "no modules in \"" + modules + "\"";
return false;
}
std::optional<int32_t> buildType;
for (const auto lot : lots) {
auto moduleQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT m.partCode, m.buildType, m.xml FROM ComponentsRegistry cr JOIN ModuleComponent m ON m.id = cr.component_id "
"WHERE cr.id = ? AND cr.component_type = ? LIMIT 1;");
moduleQuery.bind(1, static_cast<int>(lot));
moduleQuery.bind(2, MODULE_COMPONENT);
auto moduleRow = moduleQuery.execQuery();
if (moduleRow.eof()) {
error = "LOT " + std::to_string(lot) + " is not a module";
return false;
}
UgcJobs::ModuleInput module;
module.lot = lot;
module.partCode = static_cast<uint32_t>(moduleRow.getIntField("partCode", 0));
module.moduleXml = moduleRow.getStringField("xml", "");
if (!buildType) buildType = moduleRow.getIntField("buildType", 0);
input.buildType = *buildType;
auto renderQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT rc.render_asset FROM ComponentsRegistry cr JOIN RenderComponent rc ON rc.id = cr.component_id "
"WHERE cr.id = ? AND cr.component_type = ? LIMIT 1;");
renderQuery.bind(1, static_cast<int>(lot));
renderQuery.bind(2, RENDER_COMPONENT);
auto renderRow = renderQuery.execQuery();
if (!renderRow.eof()) module.renderAsset = renderRow.getStringField("render_asset", "");
input.modules.push_back(std::move(module));
}
auto buildQuery = CDClientDatabase::CreatePreppedStmt("SELECT xml FROM ModularBuildComponent WHERE buildType = ? ORDER BY id LIMIT 1;");
buildQuery.bind(1, *buildType);
auto buildRow = buildQuery.execQuery();
if (buildRow.eof()) {
error = "no ModularBuildComponent for build type " + std::to_string(*buildType);
return false;
}
input.buildXml = buildRow.getStringField("xml", "");
return true;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include "UgcJobs.h"
// What the UGC server reads from the CDClient (main thread only: the CDClient connection isn't shared with workers)
namespace UgcCdClient {
// A modular build's modules (ugc_modular_build.ldf_config) with their render assets and module data, and the
// ModularBuildComponent of their build type; false and `error` when it can't be put together
bool GatherModular(const std::string& modules, UgcJobs::ModularInput& input, std::string& error);
}

View File

@@ -0,0 +1,219 @@
#include "UgcPalette.h"
#include <algorithm>
#include <cmath>
#include <unordered_map>
namespace {
using Table = std::unordered_map<uint32_t, glm::vec3>;
glm::vec3 FromSrgb(float r, float g, float b) {
return UgcPalette::SrgbToLinear(glm::vec3(r, g, b));
}
// Colors are linear, as LU Toolbox keeps them
const Table& Opaque() {
static const Table table = [] {
Table t = {
{ 1, { 0.904661f, 0.904661f, 0.904661f } },
{ 5, { 0.693872f, 0.491021f, 0.194618f } },
{ 18, { 0.672443f, 0.168269f, 0.051269f } },
{ 21, { 0.730461f, 0.0f, 0.004025f } },
{ 23, { 0.0f, 0.095307f, 0.391572f } },
{ 24, { 0.991102f, 0.552011f, 0.0f } },
{ 26, { 0.006f, 0.006f, 0.006f } },
{ 28, { 0.0f, 0.198069f, 0.021219f } },
{ 37, { 0.0f, 0.304987f, 0.017642f } },
{ 38, { 0.391572f, 0.046665f, 0.007499f } },
{ 50, { 0.7399f, 0.7399f, 0.7399f } },
{ 102, { 0.06301f, 0.262251f, 0.564712f } },
{ 106, { 0.799103f, 0.124772f, 0.009134f } },
{ 107, { 0.002732f, 0.462077f, 0.462077f } },
{ 119, { 0.296138f, 0.47932f, 0.003035f } },
{ 120, { 0.672444f, 0.768151f, 0.262251f } },
{ 124, { 0.332452f, 0.0f, 0.147027f } },
{ 135, { 0.111932f, 0.174647f, 0.262251f } },
{ 138, { 0.262251f, 0.177888f, 0.084376f } },
{ 140, { 0.0f, 0.0185f, 0.052861f } },
{ 141, { 0.0f, 0.03434f, 0.008023f } },
{ 151, { 0.114435f, 0.223228f, 0.130137f } },
{ 154, { 0.215861f, 0.002428f, 0.01096f } },
{ 191, { 0.887923f, 0.318547f, 0.0f } },
{ 192, { 0.104617f, 0.011612f, 0.003677f } },
{ 194, { 0.332452f, 0.283149f, 0.283149f } },
{ 199, { 0.072272f, 0.082283f, 0.093059f } },
{ 208, { 0.768151f, 0.768151f, 0.693872f } },
{ 212, { 0.242281f, 0.520996f, 0.83077f } },
{ 221, { 0.730461f, 0.038204f, 0.258183f } },
{ 222, { 0.846873f, 0.341914f, 0.53948f } },
{ 226, { 1.0f, 0.768151f, 0.141263f } },
{ 268, { 0.066626f, 0.011612f, 0.341914f } },
{ 283, { 0.913099f, 0.533276f, 0.250158f } },
{ 294, { 0.991102f, 0.973445f, 0.665388f } },
{ 308, { 0.03434f, 0.015209f, 0.0f } },
{ 329, { 1.0f, 1.0f, 1.0f } },
{ 330, { 0.184475f, 0.184475f, 0.076185f } },
{ 9013, { 1.0f, 0.009721f, 0.020289f } },
{ 9014, { 0.799103f, 0.088655f, 0.0f } },
{ 9015, { 1.0f, 0.630757f, 0.033105f } },
{ 9016, { 0.012983f, 1.0f, 0.090842f } },
{ 9017, { 0.059511f, 0.768152f, 0.913099f } },
{ 9018, { 0.0f, 0.226966f, 1.0f } },
{ 9019, { 0.40724f, 0.012983f, 1.0f } },
{ 9020, { 0.686686f, 0.000303f, 1.0f } },
};
// LDD colors LU doesn't have, as the nearest LU color
const std::pair<uint32_t, uint32_t> aliases[] = {
{ 0, 26 }, { 2, 194 }, { 3, 5 }, { 4, 106 }, { 6, 119 }, { 9, 222 }, { 11, 212 }, { 12, 106 }, { 13, 191 }, { 19, 191 },
{ 22, 221 }, { 25, 192 }, { 27, 199 }, { 29, 151 }, { 36, 283 }, { 39, 194 }, { 45, 212 }, { 100, 283 }, { 101, 106 },
{ 103, 194 }, { 104, 268 }, { 105, 191 }, { 110, 23 }, { 112, 23 }, { 115, 119 }, { 116, 107 }, { 118, 212 }, { 326, 120 },
{ 125, 283 }, { 128, 38 }, { 133, 106 }, { 134, 119 }, { 136, 135 }, { 153, 138 }, { 180, 191 }, { 195, 23 }, { 196, 23 },
{ 198, 124 }, { 216, 154 }, { 217, 138 }, { 218, 124 }, { 219, 268 }, { 223, 222 }, { 232, 212 }, { 233, 37 }, { 295, 222 },
{ 312, 138 }, { 321, 102 }, { 322, 212 }, { 323, 208 }, { 324, 124 }, { 325, 222 },
};
for (const auto& [id, target] : aliases) t[id] = t.at(target);
return t;
}();
return table;
}
const Table& Transparent() {
static const Table table = [] {
Table t = {
{ 20, { 0.930111f, 0.672443f, 0.250158f } },
{ 40, { 0.854993f, 0.854993f, 0.854993f } },
{ 41, FromSrgb(0.674509f, 0.0f, 0.0f) },
{ 42, FromSrgb(0.244106f, 0.720966f, 0.772058f) },
{ 43, FromSrgb(0.031372f, 0.285668f, 0.643137f) },
{ 44, FromSrgb(0.858f, 0.771375f, 0.0f) },
{ 47, FromSrgb(0.986f, 0.336526f, 0.120035f) },
{ 48, FromSrgb(0.0f, 0.391f, 0.0f) },
{ 49, FromSrgb(0.697f, 1.0f, 0.0f) },
{ 111, FromSrgb(0.741177f, 0.670588f, 0.639216f) },
{ 113, FromSrgb(0.754717f, 0.060520f, 0.541647f) },
{ 126, FromSrgb(0.267974f, 0.196078f, 0.627451f) },
{ 143, FromSrgb(0.325985f, 0.551358f, 0.821f) },
{ 182, FromSrgb(0.913726f, 0.524575f, 0.0156863f) },
{ 311, FromSrgb(0.454640f, 0.788235f, 0.0980392f) },
};
const std::pair<uint32_t, uint32_t> aliases[] = { { 157, 44 }, { 230, 113 }, { 231, 182 }, { 234, 44 }, { 284, 126 }, { 285, 111 }, { 293, 43 } };
for (const auto& [id, target] : aliases) t[id] = t.at(target);
return t;
}();
return table;
}
const Table& GlowColors() {
static const Table table = [] {
const auto& opaque = Opaque();
Table t = { { 50, { 0.401978f, 0.401978f, 0.401978f } } };
for (const uint32_t id : { 329u, 294u, 9013u, 9014u, 9015u, 9016u, 9017u, 9018u, 9019u, 9020u }) t[id] = opaque.at(id);
const std::pair<uint32_t, uint32_t> aliases[] = {
{ 9000, 9020 }, { 9002, 9016 }, { 9004, 9018 }, { 9008, 9013 }, { 9009, 9014 }, { 9010, 9016 }, { 9011, 9017 }, { 9012, 9019 },
{ 9021, 329 }, { 9022, 50 }, { 9023, 9013 }, { 9024, 9014 }, { 9025, 9016 }, { 9026, 9018 }, { 9027, 329 },
};
for (const auto& [id, target] : aliases) t[id] = t.at(target);
return t;
}();
return table;
}
const Table& Metallic() {
static const Table table = [] {
Table t;
const auto add = [&t](std::initializer_list<uint32_t> ids, glm::vec3 color) { for (const auto id : ids) t[id] = color; };
add({ 131, 150, 179, 298, 315 }, { 0.262251f, 0.296138f, 0.296138f });
add({ 139, 187, 300 }, { 0.174648f, 0.066626f, 0.029557f });
add({ 148 }, { 0.06301f, 0.051269f, 0.043735f });
add({ 149 }, { 0.006f, 0.006f, 0.006f });
add({ 184 }, { 0.238095f, 0.00907f, 0.00907f });
add({ 186, 200 }, { 0.081104f, 0.252379f, 0.045668f });
add({ 145, 185 }, { 0.104617f, 0.177888f, 0.278894f });
add({ 309, 183 }, { 0.617207f, 0.617207f, 0.617207f });
add({ 297, 147, 189 }, { 0.401978f, 0.212231f, 0.027321f });
add({ 310, 127 }, { 0.737911f, 0.533276f, 0.181164f });
return t;
}();
return table;
}
const std::unordered_map<uint32_t, float>& CustomVariation() {
static const std::unordered_map<uint32_t, float> table = {
{ 1, 1.3f }, { 21, 1.4f }, { 23, 1.25f }, { 24, 1.5f }, { 26, 0.4f }, { 28, 0.8f }, { 37, 0.8f }, { 135, 0.85f }, { 141, 0.7f },
{ 199, 0.7f }, { 192, 0.75f }, { 212, 1.25f }, { 222, 1.05f }, { 226, 1.75f }, { 283, 1.15f }, { 308, 0.85f }, { 323, 1.4f }, { 326, 1.75f },
};
return table;
}
uint64_t SplitMix(uint64_t x) {
x += 0x9E3779B97F4A7C15ull;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
return x ^ (x >> 31);
}
}
namespace UgcPalette {
std::optional<glm::vec3> Linear(uint32_t id, bool icon) {
if (icon) {
// ICON_MATERIALS_OPAQUE
if (id == 1) return FromSrgb(0.7f, 0.7f, 0.7f);
if (id == 26) return FromSrgb(0.01f, 0.01f, 0.01f);
}
// The importer loads opaque, then transparent, metallic and glow colors, the first one of an id winning
for (const auto* table : { &Opaque(), &Transparent(), &Metallic(), &GlowColors() }) {
if (const auto it = table->find(id); it != table->end()) return it->second;
}
return std::nullopt;
}
bool IsTransparent(uint32_t id) {
return Transparent().contains(id);
}
bool IsMetallic(uint32_t id) {
return Metallic().contains(id);
}
std::optional<glm::vec3> Glow(uint32_t id) {
const auto it = GlowColors().find(id);
return it != GlowColors().end() ? std::optional(it->second) : std::nullopt;
}
float VariationScale(uint32_t id) {
const auto it = CustomVariation().find(id);
return it != CustomVariation().end() ? it->second : 1.0f;
}
float SrgbToLinear(float srgb) {
return srgb <= 0.0404482362771082f ? srgb / 12.92f : std::pow((srgb + 0.055f) / 1.055f, 2.4f);
}
float LinearToSrgb(float linear) {
return linear > 0.0031308f ? 1.055f * std::pow(linear, 1.0f / 2.4f) - 0.055f : 12.92f * linear;
}
glm::vec3 SrgbToLinear(const glm::vec3& srgb) {
return { SrgbToLinear(srgb.r), SrgbToLinear(srgb.g), SrgbToLinear(srgb.b) };
}
glm::vec3 LinearToSrgb(const glm::vec3& linear) {
return { LinearToSrgb(linear.r), LinearToSrgb(linear.g), LinearToSrgb(linear.b) };
}
glm::vec3 ApplyVariation(const glm::vec3& color, float variationPercent, float random) {
const float value = std::max({ color.r, color.g, color.b });
float gamma = std::pow(std::max(value, 0.0f), 1.0f / 2.224f);
const float range = variationPercent / 200.0f;
gamma += -range + random * 2.0f * range;
const float newValue = std::pow(std::clamp(gamma, 0.0f, 1.0f), 2.224f);
// Setting an HSV value keeps hue and saturation: the channels scale together (a black color becomes grey)
if (value <= 0.0f) return glm::vec3(newValue);
return color * (newValue / value);
}
float BrickRandom(uint64_t seed, uint32_t brick, uint32_t material) {
const auto bits = SplitMix(SplitMix(seed ^ (static_cast<uint64_t>(brick) << 32)) ^ material);
return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24);
}
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <cstdint>
#include <optional>
#include <glm/glm.hpp>
/**
* LU Toolbox's color palette (its materials/__init__.py), which it colors imported models with instead of the brick
* database's Materials.xml: LU's colors in linear RGB, the LDD colors LU doesn't have mapped onto the nearest LU one,
* which colors are transparent, glow and metallic, how much each color varies from brick to brick, and the icon
* renderer's corrections. Also the brick to brick color variation itself (process_model.py apply_color_variation).
* Pure.
*/
namespace UgcPalette {
// A material id's color in linear RGB; nullopt when LU Toolbox doesn't know it (it uses black, 26, then).
// `icon`: with the icon renderer's corrections (white and black are toned down).
std::optional<glm::vec3> Linear(uint32_t id, bool icon = false);
bool IsTransparent(uint32_t id);
bool IsMetallic(uint32_t id);
// The glow color (linear) of a glowing material, nullopt for the others
std::optional<glm::vec3> Glow(uint32_t id);
// How much of the color variation a color gets (CUSTOM_VARIATION: black varies less, orange more); 1 by default
float VariationScale(uint32_t id);
// The color LU Toolbox falls back to for unknown ids
constexpr uint32_t FALLBACK_ID = 26;
// sRGB <-> linear, exactly as LU Toolbox converts (color_conversions.py)
float SrgbToLinear(float srgb);
float LinearToSrgb(float linear);
glm::vec3 SrgbToLinear(const glm::vec3& srgb);
glm::vec3 LinearToSrgb(const glm::vec3& linear);
/**
* LU Toolbox's color variation of one color: its HSV value, taken to a 1/2.224 gamma, shifted by
* `random` (0..1, mapped to -variation/200 .. +variation/200, variation in percent), clamped to 0..1 and taken back.
* Hue and saturation stay. `color` is linear RGB.
*/
glm::vec3 ApplyVariation(const glm::vec3& color, float variationPercent, float random);
/**
* The random number (0..1) of one brick's material: the same for the same seed, brick and material every time,
* so making a model again gives the same colors, and the same in every LOD (LU Toolbox restarts its random sequence
* for each LOD). `brick` is the brick's index in the LXFML.
*/
float BrickRandom(uint64_t seed, uint32_t brick, uint32_t material);
}