mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-21 20:22:48 +00:00
feat(ugc): opt-in metal and glow shader groups in made models
Player models are multishader (RenderComponent shader 100): the client wraps each NiLODNode and draws it with the mapShaders id in its name. With shader_metal, shader_brushed or shader_glow set, the opaque bricks are split by look into S<id>_Metal_Model, S<id>_Brushed_Model and S<id>_Glow_Model beside S01_Opaque_Model and S01_Alpha_Model, each with every LOD level. Metal is LU Toolbox's metallic colors plus Materials.xml types (shinySteel; brushedSteel and matteSteel for brushed), glow its glow colors. Glow shapes get an emissive material (glow_emissive) and their plain color, not the baked one. Transparent glow stays in S01_Alpha. All off by default, which writes the same bytes as before (tested). Not how live looked; models already made change only when made again. The icon renderer reads the groups back by tag and draws glow at its plain color and metal with a tinted reflection and highlight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ namespace UgcBricks {
|
||||
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) };
|
||||
materials[id] = Material{ channel("Red", 160), channel("Green", 160), channel("Blue", 160), channel("Alpha", 255), element->Attribute("MaterialType") ? element->Attribute("MaterialType") : "" };
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
@@ -26,19 +26,20 @@ namespace UgcBricks {
|
||||
std::vector<uint32_t> indices;
|
||||
};
|
||||
|
||||
// An LDD material: sRGB color and opacity, 0-255
|
||||
// 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
|
||||
// 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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
|
||||
#include "MD5.h"
|
||||
#include "ZCompression.h"
|
||||
@@ -204,15 +205,7 @@ namespace {
|
||||
class SharedProperties {
|
||||
public:
|
||||
explicit SharedProperties(NifBuilder& nif) : m_Nif(nif) {
|
||||
Writer material;
|
||||
WriteNet(material, -1);
|
||||
for (int i = 0; i < 3; i++) material.Float(1.0f); // ambient
|
||||
for (int i = 0; i < 3; i++) material.Float(1.0f); // diffuse
|
||||
for (int i = 0; i < 3; i++) material.Float(0.0f); // specular
|
||||
for (int i = 0; i < 3; i++) material.Float(0.0f); // emissive
|
||||
material.Float(4.0f); // glossiness, as the game's brick models
|
||||
material.Float(1.0f); // alpha
|
||||
m_Material = nif.Add("NiMaterialProperty", std::move(material.Data()));
|
||||
m_Material = nif.Add("NiMaterialProperty", Material(0.0f));
|
||||
|
||||
Writer vertexColor;
|
||||
WriteNet(vertexColor, -1);
|
||||
@@ -220,8 +213,22 @@ namespace {
|
||||
m_VertexColor = nif.Add("NiVertexColorProperty", std::move(vertexColor.Data()));
|
||||
}
|
||||
|
||||
// An NiTriShape of `mesh` (-1 when it is empty or too big for the format)
|
||||
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent) {
|
||||
// A white NiMaterialProperty with this emissive color (grey)
|
||||
static std::string Material(float emissive) {
|
||||
Writer material;
|
||||
WriteNet(material, -1);
|
||||
for (int i = 0; i < 3; i++) material.Float(1.0f); // ambient
|
||||
for (int i = 0; i < 3; i++) material.Float(1.0f); // diffuse
|
||||
for (int i = 0; i < 3; i++) material.Float(0.0f); // specular
|
||||
for (int i = 0; i < 3; i++) material.Float(emissive);
|
||||
material.Float(4.0f); // glossiness, as the game's brick models
|
||||
material.Float(1.0f); // alpha
|
||||
return std::move(material.Data());
|
||||
}
|
||||
|
||||
// An NiTriShape of `mesh` (-1 when it is empty or too big for the format); `emissive`: its material's
|
||||
// emissive color, 0 for the shared material without one
|
||||
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f) {
|
||||
if (!mesh || mesh->Empty() || mesh->positions.size() > 65535 || mesh->TriangleCount() > 65535) return -1;
|
||||
// The properties every shape of the game's own brick models has, in their order: material, alpha (blending
|
||||
// by the vertex alpha: 1 on opaque bricks), specular (off) and vertex colors
|
||||
@@ -237,7 +244,13 @@ namespace {
|
||||
m_Specular = m_Nif.Add("NiSpecularProperty", std::move(specular.Data()));
|
||||
}
|
||||
(void)transparent;
|
||||
std::vector<int32_t> properties{ m_Material, m_Alpha, m_Specular, m_VertexColor };
|
||||
int32_t material = m_Material;
|
||||
if (emissive > 0.0f) {
|
||||
auto [it, added] = m_Emissive.try_emplace(emissive, -1);
|
||||
if (added) it->second = m_Nif.Add("NiMaterialProperty", Material(emissive));
|
||||
material = it->second;
|
||||
}
|
||||
std::vector<int32_t> properties{ material, m_Alpha, m_Specular, m_VertexColor };
|
||||
const auto shapeBlock = m_Nif.Reserve("NiTriShape");
|
||||
const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh));
|
||||
Writer tri;
|
||||
@@ -257,6 +270,7 @@ namespace {
|
||||
int32_t m_VertexColor{ -1 };
|
||||
int32_t m_Alpha{ -1 };
|
||||
int32_t m_Specular{ -1 };
|
||||
std::map<float, int32_t> m_Emissive; // emissive color -> its material
|
||||
};
|
||||
}
|
||||
|
||||
@@ -290,7 +304,7 @@ namespace UgcFormats {
|
||||
const auto level = nif.Reserve("NiNode");
|
||||
std::vector<int32_t> shapes;
|
||||
for (const auto* piece : lod.pieces) {
|
||||
const auto block = properties.Shape(group.name, piece, group.transparent);
|
||||
const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive);
|
||||
if (block >= 0) shapes.push_back(block);
|
||||
}
|
||||
nif.Fill(level, NodeData(nif.String(lod.name), shapes));
|
||||
|
||||
@@ -40,12 +40,16 @@ namespace UgcFormats {
|
||||
std::string name; // S01_Opaque_Model: the NiLODNode and its shapes
|
||||
bool transparent{};
|
||||
std::vector<NifLod> lods;
|
||||
// NiMaterialProperty's emissive color (grey) of the group's shapes; 0 the shared white material with none.
|
||||
// The client's emissive shader (S46) lerps from the lit color to the vertex color by vertex alpha times its red.
|
||||
float emissive{};
|
||||
};
|
||||
|
||||
/**
|
||||
* The layout LU Toolbox exports (setup_lod_data) and the game's own brick models (res/BrickModels/ndmade) have:
|
||||
* the root node, an NiLODNode per group with NiRangeLODData holding each level's distances, a node per level and
|
||||
* the level's shapes under it, named like the group. Properties as WriteNif.
|
||||
* the level's shapes under it, named like the group. Properties as WriteNif; a group with
|
||||
* an emissive color gets a material of its own.
|
||||
*/
|
||||
std::string WriteLodNif(const std::string& rootName, const std::vector<NifLodGroup>& groups);
|
||||
|
||||
|
||||
@@ -65,15 +65,51 @@ namespace UgcJobs {
|
||||
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
|
||||
std::string ShapeName(const std::string& shader, bool transparent) {
|
||||
return ("S" + (transparent ? std::string("01") : shader) + (transparent ? "_Alpha_" : "_Opaque_") + "Model").substr(0, 60);
|
||||
// An opaque mesh's pieces by look ([eLook]: UgcModel::Divide's pieces), the looks without a shader of their own
|
||||
// with the plastic ones
|
||||
using LookPieces = std::array<std::vector<UgcModel::Mesh>, UgcModel::LOOK_COUNT>;
|
||||
LookPieces DivideByLook(const UgcModel::Mesh& mesh, const std::array<bool, UgcModel::LOOK_COUNT>& separate) {
|
||||
LookPieces pieces;
|
||||
const auto split = UgcModel::SplitLooks(mesh, separate);
|
||||
if (!split) {
|
||||
pieces[0] = UgcModel::Divide(mesh);
|
||||
return pieces;
|
||||
}
|
||||
for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) pieces[look] = UgcModel::Divide((*split)[look]);
|
||||
return pieces;
|
||||
}
|
||||
}
|
||||
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error) {
|
||||
uint32_t Shaders::TagOf(UgcModel::eLook look) const {
|
||||
switch (look) {
|
||||
case UgcModel::eLook::METAL: return metal;
|
||||
case UgcModel::eLook::BRUSHED: return brushed;
|
||||
case UgcModel::eLook::GLOW: return glow;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<int32_t, UgcModel::eLook> Shaders::TagLooks() const {
|
||||
std::map<int32_t, UgcModel::eLook> looks{ { 88, UgcModel::eLook::METAL }, { 89, UgcModel::eLook::BRUSHED }, { 46, UgcModel::eLook::GLOW } };
|
||||
for (const auto look : { UgcModel::eLook::METAL, UgcModel::eLook::BRUSHED, UgcModel::eLook::GLOW }) {
|
||||
if (const auto tag = TagOf(look); tag != 0) looks[static_cast<int32_t>(tag)] = look;
|
||||
}
|
||||
return looks;
|
||||
}
|
||||
|
||||
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent) {
|
||||
if (transparent) return "S01_Alpha_Model";
|
||||
if (look == UgcModel::eLook::PLASTIC) return ("S" + settings.shaderOpaque + "_Opaque_Model").substr(0, 60);
|
||||
const auto tag = std::to_string(settings.shaders.TagOf(look));
|
||||
const char* name = look == UgcModel::eLook::METAL ? "_Metal_Model" : look == UgcModel::eLook::BRUSHED ? "_Brushed_Model" : "_Glow_Model";
|
||||
return ("S" + std::string(tag.size() < 2 ? "0" : "") + tag + name).substr(0, 60);
|
||||
}
|
||||
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks) {
|
||||
const auto readBack = NifFile::Parse(nif, 0, error);
|
||||
if (!readBack) return false;
|
||||
AddIcon(files, UgcModel::FromNif(*readBack), options);
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks), options);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,7 +149,12 @@ namespace UgcJobs {
|
||||
|
||||
// Every LOD made like LU Toolbox makes each LOD collection: colored, hidden faces removed, lighting baked, divided
|
||||
std::vector<UgcModel::Model> models;
|
||||
std::vector<std::vector<UgcModel::Mesh>> opaquePieces, transparentPieces;
|
||||
std::vector<LookPieces> opaquePieces;
|
||||
std::vector<std::vector<UgcModel::Mesh>> transparentPieces;
|
||||
// The looks with a shader of their own (UgcJobs::Shaders), each an NiLODNode apart from the plastic
|
||||
std::array<bool, UgcModel::LOOK_COUNT> separate{};
|
||||
for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast<UgcModel::eLook>(look)) != 0;
|
||||
const bool glowApart = separate[static_cast<size_t>(UgcModel::eLook::GLOW)];
|
||||
UgcModel::Model preview; // LOD 0 before the lighting bake, for the dashboard
|
||||
for (size_t i = 0; i < lods.size(); i++) {
|
||||
auto options = settings.build;
|
||||
@@ -144,27 +185,50 @@ namespace UgcJobs {
|
||||
outcome.note += "removed " + std::to_string(optimized.trianglesRemoved) + " of " + std::to_string(optimized.trianglesBefore) + " triangles";
|
||||
}
|
||||
if (i == 0) preview = model;
|
||||
// The emissive shader lerps from its own lighting to the vertex color, so glowing bricks keep their plain
|
||||
// color: no occlusion, and no glow added (it would glow twice)
|
||||
std::vector<glm::vec4> plainColors;
|
||||
if (glowApart && !model.opaque.looks.empty()) plainColors = model.opaque.colors;
|
||||
step = std::chrono::steady_clock::now();
|
||||
UgcRender::BakeAo(model, settings.ao);
|
||||
aoMs += Since(step);
|
||||
for (size_t v = 0; v < plainColors.size() && v < model.opaque.looks.size(); v++) {
|
||||
if (model.opaque.looks[v] == UgcModel::eLook::GLOW) model.opaque.colors[v] = glm::vec4(glm::vec3(plainColors[v]), 1.0f);
|
||||
}
|
||||
entry["opaqueAfter"] = model.opaque.TriangleCount();
|
||||
entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size();
|
||||
opaquePieces.push_back(UgcModel::Divide(model.opaque));
|
||||
opaquePieces.push_back(DivideByLook(model.opaque, separate));
|
||||
transparentPieces.push_back(settings.combineTransparent ? UgcModel::Divide(model.transparent) : UgcModel::SplitAt(model.transparent, model.transparentBricks));
|
||||
entry["shapes"] = opaquePieces.back().size() + transparentPieces.back().size();
|
||||
size_t shapes = transparentPieces.back().size();
|
||||
for (const auto& pieces : opaquePieces.back()) shapes += pieces.size();
|
||||
entry["shapes"] = shapes;
|
||||
// Triangles per group (NiLODNode) when metal or glow have groups of their own
|
||||
if (std::find(separate.begin(), separate.end(), true) != separate.end()) {
|
||||
auto& byGroup = entry["groups"] = nlohmann::json::object();
|
||||
for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) {
|
||||
size_t triangles = 0;
|
||||
for (const auto& piece : opaquePieces.back()[look]) triangles += piece.TriangleCount();
|
||||
if (triangles > 0) byGroup[ShapeName(settings, static_cast<UgcModel::eLook>(look), false)] = triangles;
|
||||
}
|
||||
if (!model.transparent.Empty()) byGroup[ShapeName(settings, UgcModel::eLook::PLASTIC, true)] = model.transparent.TriangleCount();
|
||||
}
|
||||
lodStats.push_back(entry);
|
||||
}
|
||||
outcome.aoBaked = settings.ao.enabled;
|
||||
|
||||
// An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them
|
||||
const auto groups = [&](size_t levels, const std::vector<std::vector<UgcModel::Mesh>>& opaque, const std::vector<std::vector<UgcModel::Mesh>>& transparent) {
|
||||
// An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them, and one for
|
||||
// each look with a shader of its own between them. Every group has every level (empty where it has nothing).
|
||||
const auto groups = [&](size_t levels, const std::vector<LookPieces>& opaque, const std::vector<std::vector<UgcModel::Mesh>>& transparent) {
|
||||
std::vector<UgcFormats::NifLodGroup> out;
|
||||
for (const bool isTransparent : { false, true }) {
|
||||
UgcFormats::NifLodGroup group{ ShapeName(settings.shaderOpaque, isTransparent), isTransparent, {} };
|
||||
for (size_t kind = 0; kind <= UgcModel::LOOK_COUNT; kind++) {
|
||||
const bool isTransparent = kind == UgcModel::LOOK_COUNT;
|
||||
const auto look = isTransparent ? UgcModel::eLook::PLASTIC : static_cast<UgcModel::eLook>(kind);
|
||||
UgcFormats::NifLodGroup group{ ShapeName(settings, look, isTransparent), isTransparent, {} };
|
||||
if (look == UgcModel::eLook::GLOW) group.emissive = std::max(settings.shaders.glowEmissive, 0.0f);
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < levels; i++) {
|
||||
UgcFormats::NifLod lod{ ranges[i].first, ranges[i].second, "LOD_" + std::to_string(lods[i]), {} };
|
||||
for (const auto& piece : (isTransparent ? transparent : opaque)[i]) lod.pieces.push_back(&piece);
|
||||
for (const auto& piece : (isTransparent ? transparent[i] : opaque[i][kind])) lod.pieces.push_back(&piece);
|
||||
any = any || !lod.pieces.empty();
|
||||
group.lods.push_back(std::move(lod));
|
||||
}
|
||||
@@ -177,7 +241,8 @@ namespace UgcJobs {
|
||||
// LXFML is served from the database.
|
||||
AddDownload(outcome.files, "model.nif", nif);
|
||||
{
|
||||
const std::vector<std::vector<UgcModel::Mesh>> opaque{ UgcModel::Divide(preview.opaque) }, transparent{ transparentPieces[0] };
|
||||
const std::vector<LookPieces> opaque{ DivideByLook(preview.opaque, separate) };
|
||||
const std::vector<std::vector<UgcModel::Mesh>> transparent{ transparentPieces[0] };
|
||||
outcome.files["model.noao.nif.gz"] = ZCompression::Gzip(UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent)));
|
||||
}
|
||||
|
||||
@@ -187,7 +252,7 @@ namespace UgcJobs {
|
||||
auto iconOptions = settings.icon;
|
||||
UgcIconParams::Apply(iconOptions, iconValues);
|
||||
std::string nifError;
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError)) {
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks())) {
|
||||
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,31 @@ namespace UgcJobs {
|
||||
* How models are made. The defaults are LU Toolbox's (Process Model, Bake Lighting and the icon renderer), see the
|
||||
* parity table in docs/UgcServer.md.
|
||||
*/
|
||||
/**
|
||||
* The shaders of the metal and glow colors (UgcModel::eLook): the mapShaders id each look's own NiLODNode names
|
||||
* (S88_Metal_Model, ...), 0 for none (the colors stay in S01_Opaque_Model, as on live). Not what live did: live's
|
||||
* models are all S01 (docs/UgcServer.md, "Metal and glow").
|
||||
*/
|
||||
struct Shaders {
|
||||
uint32_t metal{}; // shader_metal: 88 Polished Metal
|
||||
uint32_t brushed{}; // shader_brushed: 89 Brushed Steel
|
||||
uint32_t glow{}; // shader_glow: 46 LEGO-Emissive
|
||||
float glowEmissive{ 1.0f }; // glow_emissive: the glow shapes' NiMaterialProperty emissive (how much the vertex color shows unlit)
|
||||
|
||||
// The mapShaders id of a look's group, 0 for the plastic S01_Opaque_Model
|
||||
uint32_t TagOf(UgcModel::eLook look) const;
|
||||
// Multishader tag -> look, for reading the looks back out of a .nif (the icon): these settings' ids, and the
|
||||
// client's Polished Metal (88), Brushed Steel (89) and LEGO-Emissive (46) for .nifs made with other settings
|
||||
std::map<int32_t, UgcModel::eLook> TagLooks() const;
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
UgcModel::BuildOptions build; // palette, color variation, transparent opacity
|
||||
std::vector<uint32_t> lods{ 0, 2 }; // brickprimitives levels made (LU Toolbox imports LOD 0 and 2; the client has no 3)
|
||||
UgcModel::LodDistances lodDistances;
|
||||
std::string shaderOpaque{ "01" }; // S<shader>_Opaque_...; transparent shapes are always S01
|
||||
bool combineTransparent{ false }; // one shape for all transparent bricks, else one per brick (Combine Transparent)
|
||||
Shaders shaders; // metal and glow groups (all off by default)
|
||||
UgcRender::OptimizeOptions optimize; // hidden surface removal
|
||||
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
||||
UgcRender::IconOptions icon; // from the icon_* settings (UgcIconParams); presets and overrides go over it
|
||||
@@ -56,9 +75,14 @@ namespace UgcJobs {
|
||||
Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed = 0,
|
||||
const UgcIconParams::Values& iconValues = {});
|
||||
|
||||
// A model's icon files (icon.png, icon.dds download) drawn from its .nif (LOD 0); false (and `error`) when the .nif
|
||||
// can't be read
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error);
|
||||
// A model's icon files (icon.png, icon.dds download) drawn from its .nif (LOD 0), its metal and glow groups by
|
||||
// `tagLooks` (Shaders::TagLooks); false (and `error`) when the .nif can't be read
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks = {});
|
||||
|
||||
// The name of a group of shapes (its NiLODNode and shapes): S01_Opaque_Model, S01_Alpha_Model, S88_Metal_Model,
|
||||
// S89_Brushed_Model, S46_Glow_Model (the ids from the settings), at most 60 characters as LU Toolbox cuts them
|
||||
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent);
|
||||
|
||||
// How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts)
|
||||
size_t CountParts(std::string_view lxfml);
|
||||
|
||||
@@ -159,6 +159,11 @@ namespace UgcModel {
|
||||
if (other.glow.empty()) glow.resize(positions.size(), glm::vec3(0.0f));
|
||||
else glow.insert(glow.end(), other.glow.begin(), other.glow.end());
|
||||
}
|
||||
if (!looks.empty() || !other.looks.empty()) {
|
||||
looks.resize(base, eLook::PLASTIC);
|
||||
if (other.looks.empty()) looks.resize(positions.size(), eLook::PLASTIC);
|
||||
else looks.insert(looks.end(), other.looks.begin(), other.looks.end());
|
||||
}
|
||||
indices.reserve(indices.size() + other.indices.size());
|
||||
for (const auto index : other.indices) indices.push_back(base + index);
|
||||
}
|
||||
@@ -185,11 +190,39 @@ namespace UgcModel {
|
||||
return any;
|
||||
}
|
||||
|
||||
eLook LookOf(uint32_t id, const UgcBricks::Material& material, const LookRules& rules) {
|
||||
if (rules.paletteGlow && UgcPalette::Glow(id)) return eLook::GLOW;
|
||||
if (rules.paletteMetallic && UgcPalette::IsMetallic(id)) return eLook::METAL;
|
||||
const auto type = rules.materialTypes.find(material.type);
|
||||
return type != rules.materialTypes.end() ? type->second : eLook::PLASTIC;
|
||||
}
|
||||
|
||||
std::optional<std::array<Mesh, LOOK_COUNT>> SplitLooks(const Mesh& mesh, const std::array<bool, LOOK_COUNT>& separate) {
|
||||
if (mesh.looks.size() != mesh.positions.size()) return std::nullopt;
|
||||
const auto lookOf = [&](size_t triangle) {
|
||||
const auto look = mesh.looks[mesh.indices[triangle * 3]];
|
||||
return separate[static_cast<size_t>(look)] ? look : eLook::PLASTIC;
|
||||
};
|
||||
bool any = false;
|
||||
for (size_t t = 0; t < mesh.TriangleCount() && !any; t++) any = lookOf(t) != eLook::PLASTIC;
|
||||
if (!any) return std::nullopt;
|
||||
std::array<Mesh, LOOK_COUNT> out;
|
||||
for (size_t look = 0; look < LOOK_COUNT; look++) {
|
||||
std::vector<bool> keep(mesh.TriangleCount());
|
||||
bool some = false;
|
||||
for (size_t t = 0; t < keep.size(); t++) some = (keep[t] = static_cast<size_t>(lookOf(t)) == look) || some;
|
||||
if (!some) continue;
|
||||
out[look] = mesh;
|
||||
KeepTriangles(out[look], keep);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Model Build(const std::vector<Part>& parts, UgcBricks::BrickLibrary& library, const BuildOptions& options) {
|
||||
Model model;
|
||||
std::set<uint32_t> missing;
|
||||
const bool luToolbox = options.palette == ePalette::LU_TOOLBOX;
|
||||
bool anyGlow = false;
|
||||
bool anyGlow = false, anyLook = false;
|
||||
for (uint32_t brick = 0; brick < parts.size(); brick++) {
|
||||
const auto& part = parts[brick];
|
||||
const auto design = library.GetDesign(part.designId, options.lod);
|
||||
@@ -249,6 +282,8 @@ namespace UgcModel {
|
||||
}
|
||||
const glm::vec4 color(UgcPalette::LinearToSrgb(linear), alpha);
|
||||
anyGlow = anyGlow || glow != glm::vec3(0.0f);
|
||||
const auto look = transparent ? eLook::PLASTIC : LookOf(colorId, library.GetMaterial(colorId), options.looks);
|
||||
anyLook = anyLook || look != eLook::PLASTIC;
|
||||
const auto base = static_cast<uint32_t>(mesh.positions.size());
|
||||
const size_t vertexCount = geometry.positions.size() / 3;
|
||||
for (size_t v = 0; v < vertexCount; v++) {
|
||||
@@ -260,13 +295,17 @@ namespace UgcModel {
|
||||
mesh.positions.push_back(glm::vec3(part.transform * glm::vec4(position, 1.0f)));
|
||||
mesh.normals.push_back(normal);
|
||||
mesh.colors.push_back(color);
|
||||
if (&mesh == &model.opaque) model.opaque.glow.push_back(glow);
|
||||
if (&mesh == &model.opaque) {
|
||||
model.opaque.glow.push_back(glow);
|
||||
model.opaque.looks.push_back(look);
|
||||
}
|
||||
}
|
||||
for (const auto i : geometry.indices) mesh.indices.push_back(base + i);
|
||||
}
|
||||
}
|
||||
if (!anyGlow) model.opaque.glow.clear();
|
||||
else model.opaque.glow.resize(model.opaque.positions.size(), glm::vec3(0.0f));
|
||||
if (!anyLook) model.opaque.looks.clear();
|
||||
model.missingDesigns.assign(missing.begin(), missing.end());
|
||||
return model;
|
||||
}
|
||||
@@ -303,7 +342,7 @@ namespace UgcModel {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
Model FromNif(const NifFile::Model& nif) {
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks) {
|
||||
Model model;
|
||||
for (const auto& source : nif.meshes) {
|
||||
Mesh mesh;
|
||||
@@ -334,6 +373,9 @@ namespace UgcModel {
|
||||
// Blending only shows where something is see-through (the game's brick models blend every shape)
|
||||
bool seeThrough = source.material.alphaBlend && source.material.alpha < 0.99f;
|
||||
for (size_t v = 0; source.material.alphaBlend && !seeThrough && v < mesh.colors.size(); v++) seeThrough = mesh.colors[v].a < 0.99f;
|
||||
if (const auto look = tagLooks.find(source.material.shaderTag); !seeThrough && look != tagLooks.end() && look->second != eLook::PLASTIC) {
|
||||
mesh.looks.assign(mesh.positions.size(), look->second);
|
||||
}
|
||||
(seeThrough ? model.transparent : model.opaque).Append(mesh);
|
||||
}
|
||||
return model;
|
||||
@@ -355,6 +397,7 @@ namespace UgcModel {
|
||||
if (source < mesh.normals.size()) piece.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) piece.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) piece.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
piece.indices.push_back(it->second);
|
||||
}
|
||||
@@ -376,6 +419,7 @@ namespace UgcModel {
|
||||
if (source < mesh.normals.size()) kept.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) kept.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) kept.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) kept.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
kept.indices.push_back(remap[source]);
|
||||
}
|
||||
@@ -408,6 +452,7 @@ namespace UgcModel {
|
||||
if (source < mesh.normals.size()) current.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) current.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) current.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) current.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
current.indices.push_back(it->second);
|
||||
}
|
||||
@@ -460,6 +505,7 @@ namespace UgcModel {
|
||||
if (source < mesh.normals.size()) half.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) half.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) half.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) half.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
half.indices.push_back(remap[source]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -32,11 +35,19 @@ namespace UgcModel {
|
||||
// Whether an LXFML reads but has no bricks at all (nothing to make; not a failure)
|
||||
bool HasNoBricks(std::string_view lxfml);
|
||||
|
||||
/**
|
||||
* How an opaque color looks in the game when the UGC server's shader settings give it a shader of its own
|
||||
* (docs/UgcServer.md, "Metal and glow"): the LEGO plastic of S01_Opaque_Model, polished metal, brushed steel or glow.
|
||||
*/
|
||||
enum class eLook : uint8_t { PLASTIC = 0, METAL, BRUSHED, GLOW };
|
||||
constexpr size_t LOOK_COUNT = 4;
|
||||
|
||||
struct Mesh {
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<glm::vec3> normals;
|
||||
std::vector<glm::vec4> colors; // sRGB, 0..1, alpha is opacity
|
||||
std::vector<glm::vec3> glow; // linear glow color per vertex (LU Toolbox's "Glow" layer); empty when nothing glows
|
||||
std::vector<eLook> looks; // per vertex; empty when everything is plastic
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
size_t TriangleCount() const { return indices.size() / 3; }
|
||||
@@ -62,6 +73,19 @@ namespace UgcModel {
|
||||
BRICKDB, // the brick database's Materials.xml
|
||||
};
|
||||
|
||||
/**
|
||||
* Which colors have which look, from the client's data: a Materials.xml MaterialType (brickdb.zip) and LU Toolbox's
|
||||
* metallic and glow colors (UgcPalette). Glow wins over metal; transparent bricks are always plastic.
|
||||
*/
|
||||
struct LookRules {
|
||||
std::map<std::string, eLook> materialTypes{ { "shinySteel", eLook::METAL }, { "brushedSteel", eLook::BRUSHED }, { "matteSteel", eLook::BRUSHED } };
|
||||
bool paletteMetallic{ true }; // LU Toolbox's Metallic colors (UgcPalette::IsMetallic) are METAL
|
||||
bool paletteGlow{ true }; // its glow colors (UgcPalette::Glow) are GLOW
|
||||
};
|
||||
|
||||
// The look of an opaque material `id` whose Materials.xml entry is `material`
|
||||
eLook LookOf(uint32_t id, const UgcBricks::Material& material, const LookRules& rules);
|
||||
|
||||
struct BuildOptions {
|
||||
ePalette palette{ ePalette::LU_TOOLBOX };
|
||||
float colorVariation{ 5.0f }; // percent, 0: none (LU Toolbox: Apply Color Variation, 5%)
|
||||
@@ -69,6 +93,7 @@ namespace UgcModel {
|
||||
float transparentOpacity{ 58.82f }; // percent, transparent bricks' vertex alpha (LU Toolbox palette only)
|
||||
bool icon{}; // the icon renderer's color corrections
|
||||
uint32_t lod{}; // brickprimitives level
|
||||
LookRules looks; // which colors are metal and glow (Mesh::looks)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -100,8 +125,15 @@ namespace UgcModel {
|
||||
*/
|
||||
std::vector<Mesh> Divide(const Mesh& mesh, size_t maxVertices = 65535, size_t maxTriangles = 65535);
|
||||
|
||||
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended)
|
||||
Model FromNif(const NifFile::Model& nif);
|
||||
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended). `tagLooks`:
|
||||
// the look of the opaque shapes whose multishader tag (NifFile::ShaderTag, a mapShaders id) is listed
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {});
|
||||
|
||||
/**
|
||||
* The mesh's triangles by look ([eLook] -> its triangles; a triangle's look is its first vertex's), the looks not
|
||||
* in `separate` staying with PLASTIC. nullopt when nothing is separated: the mesh stays as it is.
|
||||
*/
|
||||
std::optional<std::array<Mesh, LOOK_COUNT>> SplitLooks(const Mesh& mesh, const std::array<bool, LOOK_COUNT>& separate);
|
||||
|
||||
// The mesh cut into pieces at these index offsets (each piece's triangles start at one), e.g. one per brick
|
||||
std::vector<Mesh> SplitAt(const Mesh& mesh, const std::vector<size_t>& starts);
|
||||
|
||||
@@ -209,7 +209,7 @@ void UgcProcessor::Worker() {
|
||||
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
|
||||
auto options = settings.icon;
|
||||
UgcIconParams::Apply(options, job.iconValues);
|
||||
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error);
|
||||
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks());
|
||||
if (!nif) outcome.error = "the model has no stored .nif yet";
|
||||
}
|
||||
if (outcome.ok && outcome.files.contains("assembly.nif")) {
|
||||
@@ -252,7 +252,7 @@ void UgcProcessor::Worker() {
|
||||
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
|
||||
auto options = settings.icon;
|
||||
UgcIconParams::Apply(options, job.iconValues);
|
||||
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error);
|
||||
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks());
|
||||
if (!nif) done.outcome.error = "no stored .nif";
|
||||
} else {
|
||||
done.outcome = job.kind == Kind::MODEL
|
||||
|
||||
@@ -483,8 +483,28 @@ namespace UgcRender {
|
||||
// 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));
|
||||
const auto look = isOpaque && mesh.looks.size() == mesh.positions.size() ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
|
||||
if (look == UgcModel::eLook::PLASTIC) {
|
||||
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
|
||||
|
||||
@@ -55,10 +55,15 @@ namespace UgcRender {
|
||||
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)
|
||||
};
|
||||
|
||||
// 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).
|
||||
Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector<float>* opaqueAo = nullptr);
|
||||
|
||||
struct OptimizeOptions {
|
||||
|
||||
@@ -88,6 +88,24 @@ namespace {
|
||||
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
|
||||
if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque");
|
||||
settings.combineTransparent = Setting<int32_t>("combine_transparent", 0) != 0;
|
||||
// Metal and glow colors in NiLODNodes of their own, drawn with those shaders (off by default: not how live looked)
|
||||
settings.shaders.metal = std::min(Setting<uint32_t>("shader_metal", 0), 9999u);
|
||||
settings.shaders.brushed = std::min(Setting<uint32_t>("shader_brushed", 0), 9999u);
|
||||
settings.shaders.glow = std::min(Setting<uint32_t>("shader_glow", 0), 9999u);
|
||||
settings.shaders.glowEmissive = std::clamp(Setting<float>("glow_emissive", 1.0f), 0.0f, 10.0f);
|
||||
settings.icon.glowEmissive = settings.shaders.glowEmissive;
|
||||
// Which Materials.xml MaterialTypes are metal and brushed steel
|
||||
for (const auto& [key, look] : { std::pair{ "metal_material_types", UgcModel::eLook::METAL }, std::pair{ "brushed_material_types", UgcModel::eLook::BRUSHED } }) {
|
||||
const auto value = Game::config->GetValue(key);
|
||||
if (value.empty()) continue;
|
||||
std::erase_if(settings.build.looks.materialTypes, [look](const auto& entry) { return entry.second == look; });
|
||||
std::stringstream stream(value);
|
||||
std::string type;
|
||||
while (std::getline(stream, type, ',')) {
|
||||
std::erase_if(type, [](unsigned char c) { return std::isspace(c); });
|
||||
if (!type.empty() && type != "none") settings.build.looks.materialTypes[type] = look;
|
||||
}
|
||||
}
|
||||
settings.optimize.removeHidden = Setting<int32_t>("remove_hidden_faces", 1) != 0;
|
||||
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
|
||||
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
|
||||
|
||||
Reference in New Issue
Block a user