mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +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:
@@ -450,6 +450,14 @@ namespace {
|
|||||||
c.Add(Float(UGC, "lod_cull", "Drawn up to", "", "10000", 0, 1000000));
|
c.Add(Float(UGC, "lod_cull", "Drawn up to", "", "10000", 0, 1000000));
|
||||||
c.Add(Text(UGC, "shader_opaque", "Opaque shader", "S<shader>_Opaque_Model.", "01"));
|
c.Add(Text(UGC, "shader_opaque", "Opaque shader", "S<shader>_Opaque_Model.", "01"));
|
||||||
c.Add(Bool(UGC, "combine_transparent", "One shape for all transparent bricks", "Off: each transparent brick is its own shape, so the client can sort them.", false));
|
c.Add(Bool(UGC, "combine_transparent", "One shape for all transparent bricks", "Off: each transparent brick is its own shape, so the client can sort them.", false));
|
||||||
|
// Metal and glow groups (UgcJobs::Shaders): off keeps the files exactly as before, as live made them
|
||||||
|
const std::string notLive = " Not how live looked: live's models were all LEGO plastic (S01). Models already made keep their look until they are made again (Make everything again, or Reprocess).";
|
||||||
|
c.Add(Int(UGC, "shader_metal", "Metal shader", "mapShaders id for metal colors (Materials.xml shinySteel and LU Toolbox's metallic ones), in a group S<id>_Metal_Model: 88 is Polished Metal. 0: off, they stay LEGO plastic." + notLive, "0", 0, 9999));
|
||||||
|
c.Add(Int(UGC, "shader_brushed", "Brushed steel shader", "mapShaders id for brushed steel colors (Materials.xml brushedSteel and matteSteel; the client's has none) in S<id>_Brushed_Model: 89 is Brushed Steel. 0: off." + notLive, "0", 0, 9999));
|
||||||
|
c.Add(Int(UGC, "shader_glow", "Glow shader", "mapShaders id for opaque glowing colors (LU Toolbox's glow colors) in S<id>_Glow_Model, with their plain color and an emissive material: 46 is LEGO-Emissive. Transparent glow stays with the transparent bricks. 0: off." + notLive, "0", 0, 9999));
|
||||||
|
c.Add(Float(UGC, "glow_emissive", "Glow emissive strength", "With the glow shader on: the glow shapes' material emissive, how far the emissive shader goes from lit to the plain color (1: fully).", "1", 0, 10));
|
||||||
|
c.Add(Text(UGC, "metal_material_types", "Metal material types", "Materials.xml MaterialTypes drawn as metal, comma separated (none: only LU Toolbox's metallic colors).", "shinySteel"));
|
||||||
|
c.Add(Text(UGC, "brushed_material_types", "Brushed steel material types", "Materials.xml MaterialTypes drawn as brushed steel, comma separated (none: no such colors).", "brushedSteel,matteSteel"));
|
||||||
c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "", true));
|
c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "", true));
|
||||||
c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model.", false));
|
c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model.", false));
|
||||||
c.Add(Unit(Int(UGC, "optimize_resolution", "Detail of the visibility renders", "", "1024", 64, 4096), "pixels"));
|
c.Add(Unit(Int(UGC, "optimize_resolution", "Detail of the visibility renders", "", "1024", 64, 4096), "pixels"));
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace UgcBricks {
|
|||||||
const auto channel = [element](const char* name, uint32_t fallback) {
|
const auto channel = [element](const char* name, uint32_t fallback) {
|
||||||
return static_cast<uint8_t>(std::min<uint32_t>(element->UnsignedAttribute(name, fallback), 255));
|
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;
|
return materials;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,19 +26,20 @@ namespace UgcBricks {
|
|||||||
std::vector<uint32_t> indices;
|
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 {
|
struct Material {
|
||||||
uint8_t r{ 160 };
|
uint8_t r{ 160 };
|
||||||
uint8_t g{ 160 };
|
uint8_t g{ 160 };
|
||||||
uint8_t b{ 160 };
|
uint8_t b{ 160 };
|
||||||
uint8_t a{ 255 };
|
uint8_t a{ 255 };
|
||||||
|
std::string type;
|
||||||
bool Transparent() const { return a < 255; }
|
bool Transparent() const { return a < 255; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// A .g file ("10GB" magic, counts, positions, normals, texture coordinates for decorated parts, indices)
|
// A .g file ("10GB" magic, counts, positions, normals, texture coordinates for decorated parts, indices)
|
||||||
std::optional<Geometry> ParseGeometry(std::string_view data);
|
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);
|
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
|
// 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 <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
#include "MD5.h"
|
#include "MD5.h"
|
||||||
#include "ZCompression.h"
|
#include "ZCompression.h"
|
||||||
@@ -204,15 +205,7 @@ namespace {
|
|||||||
class SharedProperties {
|
class SharedProperties {
|
||||||
public:
|
public:
|
||||||
explicit SharedProperties(NifBuilder& nif) : m_Nif(nif) {
|
explicit SharedProperties(NifBuilder& nif) : m_Nif(nif) {
|
||||||
Writer material;
|
m_Material = nif.Add("NiMaterialProperty", Material(0.0f));
|
||||||
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()));
|
|
||||||
|
|
||||||
Writer vertexColor;
|
Writer vertexColor;
|
||||||
WriteNet(vertexColor, -1);
|
WriteNet(vertexColor, -1);
|
||||||
@@ -220,8 +213,22 @@ namespace {
|
|||||||
m_VertexColor = nif.Add("NiVertexColorProperty", std::move(vertexColor.Data()));
|
m_VertexColor = nif.Add("NiVertexColorProperty", std::move(vertexColor.Data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// An NiTriShape of `mesh` (-1 when it is empty or too big for the format)
|
// A white NiMaterialProperty with this emissive color (grey)
|
||||||
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent) {
|
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;
|
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
|
// 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
|
// 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()));
|
m_Specular = m_Nif.Add("NiSpecularProperty", std::move(specular.Data()));
|
||||||
}
|
}
|
||||||
(void)transparent;
|
(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 shapeBlock = m_Nif.Reserve("NiTriShape");
|
||||||
const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh));
|
const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh));
|
||||||
Writer tri;
|
Writer tri;
|
||||||
@@ -257,6 +270,7 @@ namespace {
|
|||||||
int32_t m_VertexColor{ -1 };
|
int32_t m_VertexColor{ -1 };
|
||||||
int32_t m_Alpha{ -1 };
|
int32_t m_Alpha{ -1 };
|
||||||
int32_t m_Specular{ -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");
|
const auto level = nif.Reserve("NiNode");
|
||||||
std::vector<int32_t> shapes;
|
std::vector<int32_t> shapes;
|
||||||
for (const auto* piece : lod.pieces) {
|
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);
|
if (block >= 0) shapes.push_back(block);
|
||||||
}
|
}
|
||||||
nif.Fill(level, NodeData(nif.String(lod.name), shapes));
|
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
|
std::string name; // S01_Opaque_Model: the NiLODNode and its shapes
|
||||||
bool transparent{};
|
bool transparent{};
|
||||||
std::vector<NifLod> lods;
|
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 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 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);
|
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();
|
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ShapeName(const std::string& shader, bool transparent) {
|
// An opaque mesh's pieces by look ([eLook]: UgcModel::Divide's pieces), the looks without a shader of their own
|
||||||
return ("S" + (transparent ? std::string("01") : shader) + (transparent ? "_Alpha_" : "_Opaque_") + "Model").substr(0, 60);
|
// 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);
|
const auto readBack = NifFile::Parse(nif, 0, error);
|
||||||
if (!readBack) return false;
|
if (!readBack) return false;
|
||||||
AddIcon(files, UgcModel::FromNif(*readBack), options);
|
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks), options);
|
||||||
return true;
|
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
|
// Every LOD made like LU Toolbox makes each LOD collection: colored, hidden faces removed, lighting baked, divided
|
||||||
std::vector<UgcModel::Model> models;
|
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
|
UgcModel::Model preview; // LOD 0 before the lighting bake, for the dashboard
|
||||||
for (size_t i = 0; i < lods.size(); i++) {
|
for (size_t i = 0; i < lods.size(); i++) {
|
||||||
auto options = settings.build;
|
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";
|
outcome.note += "removed " + std::to_string(optimized.trianglesRemoved) + " of " + std::to_string(optimized.trianglesBefore) + " triangles";
|
||||||
}
|
}
|
||||||
if (i == 0) preview = model;
|
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();
|
step = std::chrono::steady_clock::now();
|
||||||
UgcRender::BakeAo(model, settings.ao);
|
UgcRender::BakeAo(model, settings.ao);
|
||||||
aoMs += Since(step);
|
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["opaqueAfter"] = model.opaque.TriangleCount();
|
||||||
entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size();
|
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));
|
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);
|
lodStats.push_back(entry);
|
||||||
}
|
}
|
||||||
outcome.aoBaked = settings.ao.enabled;
|
outcome.aoBaked = settings.ao.enabled;
|
||||||
|
|
||||||
// An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them
|
// An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them, and one for
|
||||||
const auto groups = [&](size_t levels, const std::vector<std::vector<UgcModel::Mesh>>& opaque, const std::vector<std::vector<UgcModel::Mesh>>& transparent) {
|
// 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;
|
std::vector<UgcFormats::NifLodGroup> out;
|
||||||
for (const bool isTransparent : { false, true }) {
|
for (size_t kind = 0; kind <= UgcModel::LOOK_COUNT; kind++) {
|
||||||
UgcFormats::NifLodGroup group{ ShapeName(settings.shaderOpaque, isTransparent), isTransparent, {} };
|
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;
|
bool any = false;
|
||||||
for (size_t i = 0; i < levels; i++) {
|
for (size_t i = 0; i < levels; i++) {
|
||||||
UgcFormats::NifLod lod{ ranges[i].first, ranges[i].second, "LOD_" + std::to_string(lods[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();
|
any = any || !lod.pieces.empty();
|
||||||
group.lods.push_back(std::move(lod));
|
group.lods.push_back(std::move(lod));
|
||||||
}
|
}
|
||||||
@@ -177,7 +241,8 @@ namespace UgcJobs {
|
|||||||
// LXFML is served from the database.
|
// LXFML is served from the database.
|
||||||
AddDownload(outcome.files, "model.nif", nif);
|
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)));
|
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;
|
auto iconOptions = settings.icon;
|
||||||
UgcIconParams::Apply(iconOptions, iconValues);
|
UgcIconParams::Apply(iconOptions, iconValues);
|
||||||
std::string nifError;
|
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;
|
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
|
||||||
return outcome;
|
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
|
* 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.
|
* 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 {
|
struct Settings {
|
||||||
UgcModel::BuildOptions build; // palette, color variation, transparent opacity
|
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)
|
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;
|
UgcModel::LodDistances lodDistances;
|
||||||
std::string shaderOpaque{ "01" }; // S<shader>_Opaque_...; transparent shapes are always S01
|
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)
|
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::OptimizeOptions optimize; // hidden surface removal
|
||||||
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
||||||
UgcRender::IconOptions icon; // from the icon_* settings (UgcIconParams); presets and overrides go over it
|
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,
|
Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed = 0,
|
||||||
const UgcIconParams::Values& iconValues = {});
|
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
|
// A model's icon files (icon.png, icon.dds download) drawn from its .nif (LOD 0), its metal and glow groups by
|
||||||
// can't be read
|
// `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);
|
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)
|
// How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts)
|
||||||
size_t CountParts(std::string_view lxfml);
|
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));
|
if (other.glow.empty()) glow.resize(positions.size(), glm::vec3(0.0f));
|
||||||
else glow.insert(glow.end(), other.glow.begin(), other.glow.end());
|
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());
|
indices.reserve(indices.size() + other.indices.size());
|
||||||
for (const auto index : other.indices) indices.push_back(base + index);
|
for (const auto index : other.indices) indices.push_back(base + index);
|
||||||
}
|
}
|
||||||
@@ -185,11 +190,39 @@ namespace UgcModel {
|
|||||||
return any;
|
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 Build(const std::vector<Part>& parts, UgcBricks::BrickLibrary& library, const BuildOptions& options) {
|
||||||
Model model;
|
Model model;
|
||||||
std::set<uint32_t> missing;
|
std::set<uint32_t> missing;
|
||||||
const bool luToolbox = options.palette == ePalette::LU_TOOLBOX;
|
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++) {
|
for (uint32_t brick = 0; brick < parts.size(); brick++) {
|
||||||
const auto& part = parts[brick];
|
const auto& part = parts[brick];
|
||||||
const auto design = library.GetDesign(part.designId, options.lod);
|
const auto design = library.GetDesign(part.designId, options.lod);
|
||||||
@@ -249,6 +282,8 @@ namespace UgcModel {
|
|||||||
}
|
}
|
||||||
const glm::vec4 color(UgcPalette::LinearToSrgb(linear), alpha);
|
const glm::vec4 color(UgcPalette::LinearToSrgb(linear), alpha);
|
||||||
anyGlow = anyGlow || glow != glm::vec3(0.0f);
|
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 auto base = static_cast<uint32_t>(mesh.positions.size());
|
||||||
const size_t vertexCount = geometry.positions.size() / 3;
|
const size_t vertexCount = geometry.positions.size() / 3;
|
||||||
for (size_t v = 0; v < vertexCount; v++) {
|
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.positions.push_back(glm::vec3(part.transform * glm::vec4(position, 1.0f)));
|
||||||
mesh.normals.push_back(normal);
|
mesh.normals.push_back(normal);
|
||||||
mesh.colors.push_back(color);
|
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);
|
for (const auto i : geometry.indices) mesh.indices.push_back(base + i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!anyGlow) model.opaque.glow.clear();
|
if (!anyGlow) model.opaque.glow.clear();
|
||||||
else model.opaque.glow.resize(model.opaque.positions.size(), glm::vec3(0.0f));
|
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());
|
model.missingDesigns.assign(missing.begin(), missing.end());
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@@ -303,7 +342,7 @@ namespace UgcModel {
|
|||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
Model FromNif(const NifFile::Model& nif) {
|
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks) {
|
||||||
Model model;
|
Model model;
|
||||||
for (const auto& source : nif.meshes) {
|
for (const auto& source : nif.meshes) {
|
||||||
Mesh mesh;
|
Mesh mesh;
|
||||||
@@ -334,6 +373,9 @@ namespace UgcModel {
|
|||||||
// Blending only shows where something is see-through (the game's brick models blend every shape)
|
// 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;
|
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;
|
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);
|
(seeThrough ? model.transparent : model.opaque).Append(mesh);
|
||||||
}
|
}
|
||||||
return model;
|
return model;
|
||||||
@@ -355,6 +397,7 @@ namespace UgcModel {
|
|||||||
if (source < mesh.normals.size()) piece.normals.push_back(mesh.normals[source]);
|
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.colors.size()) piece.colors.push_back(mesh.colors[source]);
|
||||||
if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[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);
|
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.normals.size()) kept.normals.push_back(mesh.normals[source]);
|
||||||
if (source < mesh.colors.size()) kept.colors.push_back(mesh.colors[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.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]);
|
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.normals.size()) current.normals.push_back(mesh.normals[source]);
|
||||||
if (source < mesh.colors.size()) current.colors.push_back(mesh.colors[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.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);
|
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.normals.size()) half.normals.push_back(mesh.normals[source]);
|
||||||
if (source < mesh.colors.size()) half.colors.push_back(mesh.colors[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.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]);
|
half.indices.push_back(remap[source]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -32,11 +35,19 @@ namespace UgcModel {
|
|||||||
// Whether an LXFML reads but has no bricks at all (nothing to make; not a failure)
|
// Whether an LXFML reads but has no bricks at all (nothing to make; not a failure)
|
||||||
bool HasNoBricks(std::string_view lxfml);
|
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 {
|
struct Mesh {
|
||||||
std::vector<glm::vec3> positions;
|
std::vector<glm::vec3> positions;
|
||||||
std::vector<glm::vec3> normals;
|
std::vector<glm::vec3> normals;
|
||||||
std::vector<glm::vec4> colors; // sRGB, 0..1, alpha is opacity
|
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<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;
|
std::vector<uint32_t> indices;
|
||||||
|
|
||||||
size_t TriangleCount() const { return indices.size() / 3; }
|
size_t TriangleCount() const { return indices.size() / 3; }
|
||||||
@@ -62,6 +73,19 @@ namespace UgcModel {
|
|||||||
BRICKDB, // the brick database's Materials.xml
|
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 {
|
struct BuildOptions {
|
||||||
ePalette palette{ ePalette::LU_TOOLBOX };
|
ePalette palette{ ePalette::LU_TOOLBOX };
|
||||||
float colorVariation{ 5.0f }; // percent, 0: none (LU Toolbox: Apply Color Variation, 5%)
|
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)
|
float transparentOpacity{ 58.82f }; // percent, transparent bricks' vertex alpha (LU Toolbox palette only)
|
||||||
bool icon{}; // the icon renderer's color corrections
|
bool icon{}; // the icon renderer's color corrections
|
||||||
uint32_t lod{}; // brickprimitives level
|
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);
|
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)
|
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended). `tagLooks`:
|
||||||
Model FromNif(const NifFile::Model& nif);
|
// 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
|
// 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);
|
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");
|
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
|
||||||
auto options = settings.icon;
|
auto options = settings.icon;
|
||||||
UgcIconParams::Apply(options, job.iconValues);
|
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 (!nif) outcome.error = "the model has no stored .nif yet";
|
||||||
}
|
}
|
||||||
if (outcome.ok && outcome.files.contains("assembly.nif")) {
|
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");
|
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
|
||||||
auto options = settings.icon;
|
auto options = settings.icon;
|
||||||
UgcIconParams::Apply(options, job.iconValues);
|
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";
|
if (!nif) done.outcome.error = "no stored .nif";
|
||||||
} else {
|
} else {
|
||||||
done.outcome = job.kind == Kind::MODEL
|
done.outcome = job.kind == Kind::MODEL
|
||||||
|
|||||||
@@ -483,8 +483,28 @@ namespace UgcRender {
|
|||||||
// The sun's highlight (Blinn-Phong), white, on top of the color
|
// 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 highlight = direct > 0.0f ? options.specular * options.sunStrength / 3.14159265f * sun * std::pow(std::max(0.0f, glm::dot(normal, halfway)), std::max(options.shininess, 1.0f)) : 0.0f;
|
||||||
const float exposure = std::max(options.exposure, 0.0f);
|
const float exposure = std::max(options.exposure, 0.0f);
|
||||||
return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure,
|
const auto look = isOpaque && mesh.looks.size() == mesh.positions.size() ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
|
||||||
(ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f));
|
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
|
// 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 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
|
float shadows{ 1.0f }; // how much the sun's shadows darken, 0 to 1
|
||||||
AoOptions ao{ false, 5.0f, 32, 1.0f, 0.0f };
|
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
|
// 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.
|
// 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);
|
Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector<float>* opaqueAo = nullptr);
|
||||||
|
|
||||||
struct OptimizeOptions {
|
struct OptimizeOptions {
|
||||||
|
|||||||
@@ -88,6 +88,24 @@ namespace {
|
|||||||
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
|
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
|
||||||
if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque");
|
if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque");
|
||||||
settings.combineTransparent = Setting<int32_t>("combine_transparent", 0) != 0;
|
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.removeHidden = Setting<int32_t>("remove_hidden_faces", 1) != 0;
|
||||||
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
|
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
|
||||||
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
|
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ defaults. The table below goes through it step by step.
|
|||||||
| Remove Hidden Faces: Cycles bakes with an overexposed world (VC pre-pass 32 samples, tris to quads, 5 pixels between vertices, 8 samples, threshold 0.01), autoremove, transparent bricks hidden | Same result by other means: depth renders from 42 directions (`optimize_resolution`), transparent bricks hidden and untouched; the pre-pass, quads and samples are details of Blender's baking |
|
| Remove Hidden Faces: Cycles bakes with an overexposed world (VC pre-pass 32 samples, tris to quads, 5 pixels between vertices, 8 samples, threshold 0.01), autoremove, transparent bricks hidden | Same result by other means: depth renders from 42 directions (`optimize_resolution`), transparent bricks hidden and untouched; the pre-pass, quads and samples are details of Blender's baking |
|
||||||
| Use Ground Plane off | Same (`hsr_ground_plane=0`) |
|
| Use Ground Plane off | Same (`hsr_ground_plane=0`) |
|
||||||
| Split objects over 65536 vertices (divide_mesh, along the longest side, linked parts together) | Same, also keeping each shape under 65535 triangles (the format's limit) |
|
| Split objects over 65536 vertices (divide_mesh, along the longest side, linked parts together) | Same, also keeping each shape under 65535 triangles (the format's limit) |
|
||||||
| Setup LOD data: SceneNode, NiLODNode per shape name, LOD nodes, near/far by the levels there are, `S01_Opaque_`/`S01_Alpha_` names cut at 60 | Same (`lod_distance_0..3`, `lod_cull`, `shader_opaque`); the glow, metal and superemissive shader settings aren't used by LU Toolbox either |
|
| Setup LOD data: SceneNode, NiLODNode per shape name, LOD nodes, near/far by the levels there are, `S01_Opaque_`/`S01_Alpha_` names cut at 60 | Same (`lod_distance_0..3`, `lod_cull`, `shader_opaque`); LU Toolbox's glow, metal and superemissive shader settings are unused by it too; the UGC server's own metal and glow groups are opt in (see below) |
|
||||||
| Bake Lighting, AO Only: 64 AO samples, distance 5, transparent skipped, glow strength 3 x 2, smooth vertex colors | Same (`ao_samples`, `ao_distance`, `glow_strength`); smoothing averages a vertex's corners, and the occlusion is per vertex already |
|
| Bake Lighting, AO Only: 64 AO samples, distance 5, transparent skipped, glow strength 3 x 2, smooth vertex colors | Same (`ao_samples`, `ao_distance`, `glow_strength`); smoothing averages a vertex's corners, and the occlusion is per vertex already |
|
||||||
| NifTools export for LU: 20.3.0.9, user version 0 | Same |
|
| NifTools export for LU: 20.3.0.9, user version 0 | Same |
|
||||||
| Physics (`.hkx`) | Intentionally not done: `.hkx` requests answer 404, so the client makes its own |
|
| Physics (`.hkx`) | Intentionally not done: `.hkx` requests answer 404, so the client makes its own |
|
||||||
@@ -202,6 +202,57 @@ defaults. The table below goes through it step by step.
|
|||||||
| Icon scene BrickBuild / Car: 50 mm lens, camera 53.4 / 19.5 degrees, sun at 21 / 50.3, 128 px, framing 1.03, transparent film | Same framing (`icon_*`); the light is brighter, to match the game's icons |
|
| Icon scene BrickBuild / Car: 50 mm lens, camera 53.4 / 19.5 degrees, sun at 21 / 50.3, 128 px, framing 1.03, transparent film | Same framing (`icon_*`); the light is brighter, to match the game's icons |
|
||||||
| Icon scene Rocket: 35 mm lens, other angles, two suns | Not built in; a preset for the rocket build type can be set in the icon editor |
|
| Icon scene Rocket: 35 mm lens, other angles, two suns | Not built in; a preset for the rocket build type can be set in the icon editor |
|
||||||
|
|
||||||
|
### Metal and glow (opt in, not how live looked)
|
||||||
|
|
||||||
|
Live's models, LU Toolbox's exports and the client's own builder (`LUNifBuilder_BK`, which writes only `S01_Opaque`
|
||||||
|
and `S01_Alpha`) all draw every brick with the LEGO shader, so metal colors look like grey plastic and glowing colors
|
||||||
|
like bright plastic. The UGC server can instead give them the client's metal and emissive shaders. Everything is off
|
||||||
|
by default, and off writes exactly the files it wrote before these settings existed (the same bytes, tested), so
|
||||||
|
nothing is made again needlessly.
|
||||||
|
|
||||||
|
How the client picks the shader (checked in the 1.10.64 client; Ghidra bookmarks under "UGCShaders"): player models
|
||||||
|
(LOT 14, and 6662) have RenderComponent shader 100, mapShaders "Multishader" (gameValue 9999). For a downloaded model
|
||||||
|
`LWOBaseRenderComponent::WrapMultishaderNodes` (0x00c0d370) wraps each `NiLODNode` (or bare `NiGeometry`) of the
|
||||||
|
.nif, and `AddObjectToRenderPipe` (0x00cfbdb0) reads the wrapped node's name with `sscanf("S%d")` (else `"_S%d"`
|
||||||
|
after the first `_S`): the number is a mapShaders id, and its gameValue is the shader. A gameValue outside 3..108
|
||||||
|
falls back to 5 (LEGO) and logs "Multishaded node ... malformed name". There is one shader per `NiLODNode`, shared by
|
||||||
|
all of its levels, so each look needs a group of its own.
|
||||||
|
|
||||||
|
| Setting (`ugcconfig.ini`, dashboard: UGC models) | Default | What it writes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `shader_metal` | 0 (off) | `S<id>_Metal_Model` for metal colors: 88 is Polished Metal (gameValue 98). The client loads `textures/metal/metal_reflection_polished.dds` itself and tints it by the vertex color (`Metallic.fx`, `Technique_Lighting_PolishedMetal_VertColor`). |
|
||||||
|
| `shader_brushed` | 0 (off) | `S<id>_Brushed_Model` for brushed steel colors: 89 is Brushed Steel (gameValue 99; it loads `metal_reflection_brushed.dds` and `_noise.dds`, the noise in object space). The client's Materials.xml has no such colors, so this only does something with a Materials.xml that names them. |
|
||||||
|
| `shader_glow` | 0 (off) | `S<id>_Glow_Model` for opaque glowing colors: 46 is LEGO-Emissive (gameValue 53), which draws `lerp(lit, vertex color, vertex alpha * material emissive red)`, opaque. |
|
||||||
|
| `glow_emissive` | 1 | The glow shapes' `NiMaterialProperty` emissive (grey): how far the shader goes from lit to the plain color. |
|
||||||
|
| `metal_material_types` | `shinySteel` | Materials.xml `MaterialType`s that are metal (empty: the default; `none`: none). |
|
||||||
|
| `brushed_material_types` | `brushedSteel,matteSteel` | Materials.xml `MaterialType`s that are brushed steel. |
|
||||||
|
|
||||||
|
Which color has which look is data, not a list in the code (`UgcModel::LookOf`): glow is LU Toolbox's glow table
|
||||||
|
(`UgcPalette::Glow`: 50, 294, 329, 9000-9027), metal is LU Toolbox's metallic table (`UgcPalette::IsMetallic`) plus
|
||||||
|
the Materials.xml types above (the clients checked have 8 or 14 `shinySteel` colors, and 1 or 3 `glitter` ones, which stay
|
||||||
|
plastic, as does pearl: the client has no shader for them). Only opaque bricks get a look: a transparent glowing
|
||||||
|
color (294 with the brick database palette, alpha 150) stays in `S01_Alpha_Model`.
|
||||||
|
|
||||||
|
What is written with a group on: per LOD, the opaque bricks are split by look before being divided at 65535 vertices,
|
||||||
|
and the .nif gets, in order, `S01_Opaque_Model`, `S88_Metal_Model`, `S89_Brushed_Model`, `S46_Glow_Model` and
|
||||||
|
`S01_Alpha_Model`, each only when it has triangles, and each with every LOD level (an empty `LOD_<n>` node where it has
|
||||||
|
none there), like the plastic groups. Metal shapes are like plastic ones (white material, no textures, the brick color
|
||||||
|
as vertex color with the lighting baked in). Glow shapes get a material of their own with emissive `glow_emissive`,
|
||||||
|
vertex alpha 1 (the shader's mask) and their plain color, not the baked one: the shader lights them itself, and the
|
||||||
|
glow added by the bake would glow twice. `stats.json` lists each LOD's triangles per group. `model.noao.nif` has the
|
||||||
|
same groups.
|
||||||
|
|
||||||
|
Turning a setting on or off changes only models made afterwards: the ones made already keep their look until they are
|
||||||
|
made again, with the UGC page's **Make everything again** (or Reprocess on one model); nothing is remade on its own.
|
||||||
|
|
||||||
|
The icon renderer and the dashboard know the groups: the icon reads each shape's tag back (the settings' ids and the
|
||||||
|
client's 88, 89 and 46) and draws glow at its plain color (by `glow_emissive`, unlit) and metal with a dimmed diffuse
|
||||||
|
light, a sky over dark ground reflection tinted by its color and a sun highlight (sharp for polished, broad for
|
||||||
|
brushed). This is an approximation of the game's environment maps. `NifFile::ShaderLookFor` gives 98 `REFLECTIVE`,
|
||||||
|
99 `REFLECTIVE | BRUSHED` and 53 `EMISSIVE`; the UGC page's 3D view gets each mesh's look (`/api/ugc/mesh`, "look")
|
||||||
|
and draws metal as reflective (metalness 1, the view's environment) and glow unlit, and the zone views draw
|
||||||
|
LEGO-Emissive objects going to their vertex color by its alpha (metal there stays lit like the rest).
|
||||||
|
|
||||||
Modular builds (`ugc_modular_build` rows, `ldf_config` like `1:4713+1:4714+1:4715`):
|
Modular builds (`ugc_modular_build` rows, `ldf_config` like `1:4713+1:4714+1:4715`):
|
||||||
|
|
||||||
1. Each module LOT's `ModuleComponent` (component type 28) gives its part code and build
|
1. Each module LOT's `ModuleComponent` (component type 28) gives its part code and build
|
||||||
|
|||||||
@@ -64,6 +64,24 @@ lod_cull=10000
|
|||||||
shader_opaque=01
|
shader_opaque=01
|
||||||
combine_transparent=0
|
combine_transparent=0
|
||||||
|
|
||||||
|
# Metal and glow colors in NiLODNodes of their own, drawn with the client's metal and emissive shaders. Not how live
|
||||||
|
# looked: live's models were all LEGO plastic (S01), which is what 0 (off, the default) keeps, byte for byte.
|
||||||
|
# shader_metal: mapShaders id for metal colors (Materials.xml types in metal_material_types and LU Toolbox's metallic
|
||||||
|
# colors), S<id>_Metal_Model: 88 is Polished Metal.
|
||||||
|
# shader_brushed: for brushed steel colors (brushed_material_types; the client's Materials.xml has none): 89 is
|
||||||
|
# Brushed Steel.
|
||||||
|
# shader_glow: for opaque glowing colors (LU Toolbox's glow colors), with their plain color and an emissive material:
|
||||||
|
# 46 is LEGO-Emissive. Transparent glow stays with the transparent bricks.
|
||||||
|
# glow_emissive: the glow shapes' material emissive (how far the shader goes from lit to the plain color, 1 fully).
|
||||||
|
# *_material_types: Materials.xml MaterialTypes, comma separated (empty: the default, none: no types).
|
||||||
|
# Models already made keep their look until they're made again (the dashboard's Make everything again, or Reprocess).
|
||||||
|
shader_metal=0
|
||||||
|
shader_brushed=0
|
||||||
|
shader_glow=0
|
||||||
|
glow_emissive=1
|
||||||
|
metal_material_types=shinySteel
|
||||||
|
brushed_material_types=brushedSteel,matteSteel
|
||||||
|
|
||||||
# Remove faces that can't be seen from anywhere (optimize_resolution: detail of the renders that decide it;
|
# Remove faces that can't be seen from anywhere (optimize_resolution: detail of the renders that decide it;
|
||||||
# hsr_ground_plane: 1 also removes what can only be seen from below the model)
|
# hsr_ground_plane: 1 also removes what can only be seen from below the model)
|
||||||
remove_hidden_faces=1
|
remove_hidden_faces=1
|
||||||
|
|||||||
@@ -102,9 +102,11 @@ TEST(UgcBricks, ParsesGeometryAndRejectsBadData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(UgcBricks, ParsesMaterials) {
|
TEST(UgcBricks, ParsesMaterials) {
|
||||||
const auto materials = UgcBricks::ParseMaterials(R"(<Materials><Material MatID="21" Red="222" Green="0" Blue="13" Alpha="255"/><Material MatID="40" Red="238" Green="238" Blue="238" Alpha="150"/></Materials>)");
|
const auto materials = UgcBricks::ParseMaterials(R"(<Materials><Material MatID="21" Red="222" Green="0" Blue="13" Alpha="255"/><Material MatID="40" Red="238" Green="238" Blue="238" Alpha="150" MaterialType="shinySteel"/></Materials>)");
|
||||||
ASSERT_EQ(materials.size(), 2u);
|
ASSERT_EQ(materials.size(), 2u);
|
||||||
EXPECT_EQ(materials.at(21).r, 222);
|
EXPECT_EQ(materials.at(21).r, 222);
|
||||||
|
EXPECT_EQ(materials.at(21).type, "");
|
||||||
|
EXPECT_EQ(materials.at(40).type, "shinySteel");
|
||||||
EXPECT_FALSE(materials.at(21).Transparent());
|
EXPECT_FALSE(materials.at(21).Transparent());
|
||||||
EXPECT_TRUE(materials.at(40).Transparent());
|
EXPECT_TRUE(materials.at(40).Transparent());
|
||||||
}
|
}
|
||||||
@@ -1057,3 +1059,210 @@ TEST(UgcIconPose, MatchesTheEditorsFixture) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Plastic (21), LU Toolbox metallic (150), glow (329, and 294 which LU Toolbox's palette has opaque) and
|
||||||
|
// transparent (40)
|
||||||
|
const char* LOOKS_LXFML = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||||
|
<LXFML versionMajor="5" versionMinor="0"><Bricks>
|
||||||
|
<Brick><Part designID="3001" materials="21"><Bone transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="150"><Bone transformation="1,0,0,0,1,0,0,0,1,3,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="329"><Bone transformation="1,0,0,0,1,0,0,0,1,6,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="294"><Bone transformation="1,0,0,0,1,0,0,0,1,9,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="40"><Bone transformation="1,0,0,0,1,0,0,0,1,12,0,0"/></Part></Brick>
|
||||||
|
</Bricks></LXFML>)";
|
||||||
|
|
||||||
|
UgcJobs::Settings SmallSettings() {
|
||||||
|
UgcJobs::Settings settings;
|
||||||
|
settings.optimize.resolution = 128;
|
||||||
|
settings.ao.samples = 8;
|
||||||
|
settings.icon.size = 32;
|
||||||
|
settings.icon.supersample = 1;
|
||||||
|
settings.icon.ao.samples = 4;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, OffIsByteIdenticalToBefore) {
|
||||||
|
// With the shader settings off (the default) the files are exactly what the server made before they existed, so
|
||||||
|
// nothing is made again needlessly. The hashes are of the files made before the settings were added.
|
||||||
|
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||||
|
const auto outcome = UgcJobs::ProcessModel(LOOKS_LXFML, library, SmallSettings(), 7);
|
||||||
|
ASSERT_TRUE(outcome.ok) << outcome.error;
|
||||||
|
const auto nif = *ZCompression::Gunzip(outcome.files.at("model.nif.gz"));
|
||||||
|
std::string error;
|
||||||
|
const auto read = NifFile::Parse(nif, 0, error);
|
||||||
|
ASSERT_TRUE(read) << error;
|
||||||
|
EXPECT_EQ(read->nodes.size(), 4u); // the root, S01_Opaque_Model, S01_Alpha_Model and LOD_0
|
||||||
|
for (const auto& mesh : read->meshes) EXPECT_EQ(mesh.material.shaderTag, 1);
|
||||||
|
// The floating point results (color variation, occlusion) are the same on one platform and compiler; the hashes
|
||||||
|
// were taken with GCC on x86-64 Linux
|
||||||
|
#if defined(__linux__) && defined(__x86_64__) && defined(__GNUC__) && !defined(__clang__)
|
||||||
|
EXPECT_EQ(UgcFormats::Md5Hex(nif), "b0fcb707d36ccdb62e951bf50593e633");
|
||||||
|
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz"))), "db55bd2c8567a862b2c96942aa5a2617");
|
||||||
|
EXPECT_EQ(UgcFormats::Md5Hex(outcome.files.at("icon.png")), "032ff7df236a636a4c609071d9b46181");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, OnlyTheShaderIdsSwitchItOn) {
|
||||||
|
// The other shader settings change nothing while the groups are off
|
||||||
|
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||||
|
auto settings = SmallSettings();
|
||||||
|
const auto before = UgcJobs::ProcessModel(LOOKS_LXFML, library, settings, 7);
|
||||||
|
settings.shaders.glowEmissive = 0.5f;
|
||||||
|
settings.icon.glowEmissive = 0.5f;
|
||||||
|
settings.build.looks.materialTypes.clear();
|
||||||
|
const auto after = UgcJobs::ProcessModel(LOOKS_LXFML, library, settings, 7);
|
||||||
|
ASSERT_TRUE(before.ok && after.ok);
|
||||||
|
for (const auto* name : { "model.nif.checksum", "model.noao.nif.gz", "icon.png" }) EXPECT_EQ(before.files.at(name), after.files.at(name)) << name;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, NamesTheGroups) {
|
||||||
|
UgcJobs::Settings settings;
|
||||||
|
settings.shaders.metal = 88;
|
||||||
|
settings.shaders.brushed = 89;
|
||||||
|
settings.shaders.glow = 7;
|
||||||
|
EXPECT_EQ(UgcJobs::ShapeName(settings, UgcModel::eLook::PLASTIC, false), "S01_Opaque_Model");
|
||||||
|
EXPECT_EQ(UgcJobs::ShapeName(settings, UgcModel::eLook::PLASTIC, true), "S01_Alpha_Model");
|
||||||
|
EXPECT_EQ(UgcJobs::ShapeName(settings, UgcModel::eLook::METAL, false), "S88_Metal_Model");
|
||||||
|
EXPECT_EQ(UgcJobs::ShapeName(settings, UgcModel::eLook::BRUSHED, false), "S89_Brushed_Model");
|
||||||
|
EXPECT_EQ(UgcJobs::ShapeName(settings, UgcModel::eLook::GLOW, false), "S07_Glow_Model");
|
||||||
|
// The client reads the id back as the tag
|
||||||
|
EXPECT_EQ(NifFile::ShaderTag(UgcJobs::ShapeName(settings, UgcModel::eLook::GLOW, false)), 7);
|
||||||
|
EXPECT_EQ(NifFile::ShaderTag(UgcJobs::ShapeName(settings, UgcModel::eLook::METAL, false)), 88);
|
||||||
|
// The client's own ids read back to their looks whatever the settings, the settings' own too
|
||||||
|
const auto looks = settings.shaders.TagLooks();
|
||||||
|
EXPECT_EQ(looks.at(88), UgcModel::eLook::METAL);
|
||||||
|
EXPECT_EQ(looks.at(46), UgcModel::eLook::GLOW);
|
||||||
|
EXPECT_EQ(looks.at(7), UgcModel::eLook::GLOW);
|
||||||
|
EXPECT_FALSE(looks.contains(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, LooksComeFromTheColorData) {
|
||||||
|
const UgcModel::LookRules rules;
|
||||||
|
const UgcBricks::Material plastic{ 200, 0, 0, 255, "shinyPlastic" }, steel{ 150, 150, 150, 255, "shinySteel" }, brushed{ 150, 150, 150, 255, "brushedSteel" };
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(21, plastic, rules), UgcModel::eLook::PLASTIC);
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(5000, steel, rules), UgcModel::eLook::METAL); // a Materials.xml shinySteel
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(5000, brushed, rules), UgcModel::eLook::BRUSHED);
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(183, plastic, rules), UgcModel::eLook::METAL); // LU Toolbox's metallic, shinyPlastic in Materials.xml
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(329, plastic, rules), UgcModel::eLook::GLOW); // LU Toolbox's glow colors
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(50, plastic, rules), UgcModel::eLook::GLOW);
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(9016, plastic, rules), UgcModel::eLook::GLOW);
|
||||||
|
UgcModel::LookRules none;
|
||||||
|
none.materialTypes.clear();
|
||||||
|
none.paletteMetallic = false;
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(5000, steel, none), UgcModel::eLook::PLASTIC);
|
||||||
|
EXPECT_EQ(UgcModel::LookOf(150, steel, none), UgcModel::eLook::PLASTIC);
|
||||||
|
|
||||||
|
// Built: opaque vertices get their color's look, transparent bricks none (their glow stays with them)
|
||||||
|
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||||
|
library.SetMaterials({ { 5000, brushed } });
|
||||||
|
std::string error;
|
||||||
|
const auto model = UgcModel::Build(UgcModel::ParseLxfml(R"(<LXFML versionMajor="5"><Bricks>
|
||||||
|
<Brick><Part designID="3001" materials="150"><Bone transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="5000"><Bone transformation="1,0,0,0,1,0,0,0,1,3,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="21"><Bone transformation="1,0,0,0,1,0,0,0,1,6,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="40"><Bone transformation="1,0,0,0,1,0,0,0,1,9,0,0"/></Part></Brick>
|
||||||
|
</Bricks></LXFML>)", error), library);
|
||||||
|
ASSERT_EQ(model.opaque.looks.size(), 24u);
|
||||||
|
EXPECT_EQ(model.opaque.looks[0], UgcModel::eLook::METAL);
|
||||||
|
EXPECT_EQ(model.opaque.looks[8], UgcModel::eLook::BRUSHED);
|
||||||
|
EXPECT_EQ(model.opaque.looks[16], UgcModel::eLook::PLASTIC);
|
||||||
|
EXPECT_TRUE(model.transparent.looks.empty());
|
||||||
|
|
||||||
|
// Split by the looks that have groups; the rest stay plastic; nothing to split: the mesh as it is
|
||||||
|
const auto split = UgcModel::SplitLooks(model.opaque, { false, true, false, false });
|
||||||
|
ASSERT_TRUE(split);
|
||||||
|
EXPECT_EQ((*split)[0].TriangleCount(), 24u);
|
||||||
|
EXPECT_EQ((*split)[1].TriangleCount(), 12u);
|
||||||
|
EXPECT_TRUE((*split)[2].Empty());
|
||||||
|
EXPECT_FALSE(UgcModel::SplitLooks(model.opaque, { false, false, false, true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, WritesAGroupPerLookWithEveryLevel) {
|
||||||
|
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||||
|
auto settings = SmallSettings();
|
||||||
|
settings.build.colorVariation = 0.0f;
|
||||||
|
settings.shaders.metal = 88;
|
||||||
|
settings.shaders.brushed = 89;
|
||||||
|
settings.shaders.glow = 46;
|
||||||
|
settings.shaders.glowEmissive = 0.75f;
|
||||||
|
const auto outcome = UgcJobs::ProcessModel(LOOKS_LXFML, library, settings, 7);
|
||||||
|
ASSERT_TRUE(outcome.ok) << outcome.error;
|
||||||
|
const auto nif = *ZCompression::Gunzip(outcome.files.at("model.nif.gz"));
|
||||||
|
std::string error;
|
||||||
|
for (const uint32_t level : { 0u, 1u }) {
|
||||||
|
const auto read = NifFile::Parse(nif, level, error);
|
||||||
|
ASSERT_TRUE(read) << error;
|
||||||
|
// No brushed steel colors: no group for them. Every group has both levels.
|
||||||
|
for (const auto* name : { "S01_Opaque_Model", "S88_Metal_Model", "S46_Glow_Model", "S01_Alpha_Model" }) EXPECT_TRUE(read->nodes.contains(name)) << name;
|
||||||
|
EXPECT_FALSE(read->nodes.contains("S89_Brushed_Model"));
|
||||||
|
EXPECT_TRUE(read->nodes.contains(level == 0 ? "LOD_0" : "LOD_2"));
|
||||||
|
std::map<int32_t, size_t> triangles;
|
||||||
|
for (const auto& mesh : read->meshes) triangles[mesh.material.shaderTag] += mesh.indices.size() / 3;
|
||||||
|
EXPECT_EQ(triangles[1], 24u); // the plastic brick and the transparent one
|
||||||
|
EXPECT_EQ(triangles[88], 12u);
|
||||||
|
EXPECT_EQ(triangles[46], 24u); // 329 and 294
|
||||||
|
}
|
||||||
|
const auto read = NifFile::Parse(nif, 0, error);
|
||||||
|
const auto noao = NifFile::Parse(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz")), 0, error);
|
||||||
|
ASSERT_TRUE(read && noao);
|
||||||
|
size_t glowShapes = 0;
|
||||||
|
for (const auto& mesh : read->meshes) {
|
||||||
|
if (mesh.material.shaderTag == 46) {
|
||||||
|
glowShapes++;
|
||||||
|
// The emissive shader's material, the plain color (as before the lighting bake), opaque
|
||||||
|
for (const auto value : mesh.material.emissive) EXPECT_FLOAT_EQ(value, 0.75f);
|
||||||
|
const auto plain = std::find_if(noao->meshes.begin(), noao->meshes.end(), [&](const auto& other) { return other.material.shaderTag == 46 && other.positions == mesh.positions; });
|
||||||
|
ASSERT_NE(plain, noao->meshes.end());
|
||||||
|
EXPECT_EQ(mesh.colors, plain->colors);
|
||||||
|
for (size_t i = 3; i < mesh.colors.size(); i += 4) EXPECT_EQ(mesh.colors[i], 255);
|
||||||
|
} else {
|
||||||
|
for (const auto value : mesh.material.emissive) EXPECT_EQ(value, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(glowShapes, 1u);
|
||||||
|
EXPECT_NE(outcome.stats.find("\"S88_Metal_Model\":12"), std::string::npos) << outcome.stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UgcShaders, IconsDrawGlowUnlitAndMetalShiny) {
|
||||||
|
// One quad facing the camera, lit from behind: plastic is dark, glow its full color, metal shows a reflection
|
||||||
|
UgcModel::Model model;
|
||||||
|
model.opaque.positions = { { -1, -1, 0 }, { 1, -1, 0 }, { -1, 1, 0 }, { 1, 1, 0 } };
|
||||||
|
model.opaque.normals.assign(4, { 0, 0, 1 });
|
||||||
|
model.opaque.colors.assign(4, { 0.8f, 0.4f, 0.2f, 1.0f });
|
||||||
|
model.opaque.indices = { 0, 1, 2, 1, 3, 2 };
|
||||||
|
UgcRender::IconOptions options;
|
||||||
|
options.size = 16;
|
||||||
|
options.supersample = 1;
|
||||||
|
options.yawDegrees = 0.0f;
|
||||||
|
options.pitchDegrees = 0.0f;
|
||||||
|
options.sunYawDegrees = 180.0f;
|
||||||
|
options.sunPitchDegrees = 0.0f;
|
||||||
|
options.shadows = 0.0f;
|
||||||
|
const auto centre = [&](UgcModel::eLook look) {
|
||||||
|
auto copy = model;
|
||||||
|
if (look != UgcModel::eLook::PLASTIC) copy.opaque.looks.assign(4, look);
|
||||||
|
const auto image = UgcRender::RenderIcon(copy, options);
|
||||||
|
const size_t at = (8 * 16 + 8) * 4;
|
||||||
|
return glm::ivec3(image.rgba[at], image.rgba[at + 1], image.rgba[at + 2]);
|
||||||
|
};
|
||||||
|
const auto plastic = centre(UgcModel::eLook::PLASTIC), glow = centre(UgcModel::eLook::GLOW), metal = centre(UgcModel::eLook::METAL);
|
||||||
|
EXPECT_NEAR(glow.r, 204, 2);
|
||||||
|
EXPECT_NEAR(glow.g, 102, 2);
|
||||||
|
EXPECT_NEAR(glow.b, 51, 2);
|
||||||
|
EXPECT_LT(plastic.r, glow.r);
|
||||||
|
EXPECT_NE(metal, plastic);
|
||||||
|
EXPECT_GE(metal.r, metal.g); // tinted by its color
|
||||||
|
options.glowEmissive = 0.0f;
|
||||||
|
EXPECT_EQ(centre(UgcModel::eLook::GLOW), plastic);
|
||||||
|
|
||||||
|
// Read back from a .nif by the groups' tags
|
||||||
|
const UgcModel::Mesh mesh = model.opaque;
|
||||||
|
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", { { "S46_Glow_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 1.0f } });
|
||||||
|
std::string error;
|
||||||
|
const auto read = NifFile::Parse(nif, 0, error);
|
||||||
|
ASSERT_TRUE(read) << error;
|
||||||
|
EXPECT_EQ(UgcModel::FromNif(*read, UgcJobs::Shaders{}.TagLooks()).opaque.looks, std::vector<UgcModel::eLook>(4, UgcModel::eLook::GLOW));
|
||||||
|
EXPECT_TRUE(UgcModel::FromNif(*read).opaque.looks.empty());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user