mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(ugc): glitter sparkles that move on placed models
Why the glitter never moved: player models (LOT 14) are wrapped in weeblewobble.kfm (RenderComponentWrapper 9845), so the client makes them an LWOSkinnedRenderComponent, whose Run (0x00d6d3d0) updates the scene graph (and so any NiTextureTransformController) only while animation is enabled, and LWOModelBehaviorComponent::EnableAnimation (0x00be2740) turns it off for modelType 2, which every placed property model is. The root flags 0x102 added earlier are only read by the base render component. Nothing in a placed model's .nif can move. What does move: shader classes set globals in their own per-frame Run. Distortion Directional (Ocean) (mapShaders 79, Run 0x010b90c0) slides its texture layers by fixed shares of a tile a second, as the game's own pond ripples (S79__pond_ripplesShape). Glitter bricks now get a sparkle group, S79_GlitterSparkle_Model: their triangles lifted 0.005 off the brick, vertex colors white tinted by the brick, UVs placed per brick, alpha tested (ShaderCommon's alpha test phase, GREATEREQUAL 127), with a stored texture of flat sparkles at alpha 230: one layer's sparkle alone averages under the test, two meeting pass, so sparkles flash and go out as the layers cross. The flecks stay (LEGO-AnimUV, now without the controllers and flags that never ran). The icon and the dashboard's 3D view leave the sparkles out. New settings: shader_glitter_sparkle (79, 0 off), glitter_sparkle_size, glitter_sparkle_amount, glitter_sparkle_tint, glitter_sparkle_brightness; glitter_speed is now how fast sparkles flash (the sparkle tile). Only glitter output changes; non-glitter models are byte-identical. Check in game: reprocess a property with glitter models, then look at them from a few angles and distances, on each graphics quality: - sparkles flash on and off all over the glitter bricks, continuously - no flickering fight between the sparkles and the brick surface - transparent glitter bricks still see-through, flecks still visible - nothing drawn where there is no glitter brick; icons unchanged apart from the flecks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,12 +118,6 @@ namespace {
|
||||
// NiAVObject flags as the game's own brick models (res/BrickModels/ndmade) have them: nodes 0x110, shapes 0x10
|
||||
constexpr uint16_t NODE_FLAGS = 0x110;
|
||||
constexpr uint16_t SHAPE_FLAGS = 0x10;
|
||||
// Nodes and shapes with controllers under them, as the client's own animated files (the AG ocean): the client
|
||||
// updates an object's scene graph every frame only when its root has the selective update bit (0x02;
|
||||
// LWOBaseRenderComponent::Run 0x00d5d770, NiAVObject::GetSelectiveUpdate 0x00413050). Without it the model is
|
||||
// updated once when it loads and its controllers never move.
|
||||
constexpr uint16_t ANIMATED_NODE_FLAGS = 0x102;
|
||||
constexpr uint16_t ANIMATED_SHAPE_FLAGS = 0x1A; // selective update, update property controllers, rigid
|
||||
|
||||
void WriteNet(Writer& out, int32_t name) {
|
||||
out.I32(name);
|
||||
@@ -144,9 +138,40 @@ namespace {
|
||||
out.I32(-1); // collision object
|
||||
}
|
||||
|
||||
// `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv), placed by each vertex's brick
|
||||
// (Mesh::brickSeeds) when the glitter is random
|
||||
std::string TriShapeData(const UgcModel::Mesh& mesh, const UgcGlitter::Params* glitter = nullptr) {
|
||||
// A glitter texture's UV set for `mesh` (UgcGlitter::Uv), placed by each vertex's brick (Mesh::brickSeeds) when
|
||||
// the glitter is random; empty without normals
|
||||
std::vector<glm::vec2> GlitterUvs(const UgcModel::Mesh& mesh, const UgcGlitter::Params& glitter, UgcGlitter::eLayer layer) {
|
||||
std::vector<glm::vec2> uvs;
|
||||
if (mesh.normals.size() != mesh.positions.size()) return uvs;
|
||||
const float tile = layer == UgcGlitter::eLayer::SPARKLES ? glitter.SparkleTile() : glitter.tile;
|
||||
uvs.reserve(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) {
|
||||
const uint32_t seed = glitter.random && v < mesh.brickSeeds.size() ? mesh.brickSeeds[v] : 0;
|
||||
uvs.push_back(UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], tile, seed, layer));
|
||||
}
|
||||
return uvs;
|
||||
}
|
||||
|
||||
// The sparkles over a glitter mesh: the same triangles lifted off it along the normals (UgcGlitter::SPARKLE_LIFT),
|
||||
// their vertex colors the sparkles' (UgcGlitter::SparkleColor)
|
||||
UgcModel::Mesh SparkleMesh(const UgcModel::Mesh& mesh, const UgcGlitter::Params& glitter) {
|
||||
UgcModel::Mesh out;
|
||||
out.positions = mesh.positions;
|
||||
out.normals = mesh.normals;
|
||||
out.brickSeeds = mesh.brickSeeds;
|
||||
out.indices = mesh.indices;
|
||||
if (out.normals.size() == out.positions.size()) {
|
||||
for (size_t v = 0; v < out.positions.size(); v++) out.positions[v] += out.normals[v] * UgcGlitter::SPARKLE_LIFT;
|
||||
}
|
||||
out.colors.reserve(mesh.positions.size());
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) {
|
||||
out.colors.push_back(UgcGlitter::SparkleColor(v < mesh.colors.size() ? mesh.colors[v] : glm::vec4(1.0f), glitter));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// `uvs`: a UV set (one per vertex), none when empty
|
||||
std::string TriShapeData(const UgcModel::Mesh& mesh, const std::vector<glm::vec2>& uvSet = {}) {
|
||||
Writer out;
|
||||
const auto count = static_cast<uint16_t>(mesh.positions.size());
|
||||
out.I32(0); // group ID
|
||||
@@ -163,7 +188,7 @@ namespace {
|
||||
max = glm::max(max, p);
|
||||
}
|
||||
const bool normals = mesh.normals.size() == mesh.positions.size();
|
||||
const bool uvs = glitter && normals;
|
||||
const bool uvs = !uvSet.empty() && uvSet.size() == mesh.positions.size();
|
||||
out.U16(uvs ? 1 : 0); // data flags: the number of UV sets, no tangents
|
||||
out.U8(normals ? 1 : 0);
|
||||
if (normals) {
|
||||
@@ -191,9 +216,7 @@ namespace {
|
||||
}
|
||||
}
|
||||
if (uvs) {
|
||||
for (size_t v = 0; v < mesh.positions.size(); v++) {
|
||||
const uint32_t seed = glitter->random && v < mesh.brickSeeds.size() ? mesh.brickSeeds[v] : 0;
|
||||
const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter->tile, seed);
|
||||
for (const auto& uv : uvSet) {
|
||||
out.Float(uv.x);
|
||||
out.Float(uv.y);
|
||||
}
|
||||
@@ -244,53 +267,11 @@ namespace {
|
||||
}
|
||||
|
||||
/**
|
||||
* The glitter groups' NiTexturingProperty (made once a file), as the client's own animated textures have it
|
||||
* (res/mesh/env/env_ag_ocean-maelstrom.nif): the base map wrapping, with a texture transform (Maya method,
|
||||
* center 0.5), its source stored in the file, and NiTextureTransformControllers on the property looping the
|
||||
* transform's translation (flags 0x48: active, looping, app time; frequency 1) through an NiFloatInterpolator
|
||||
* and linear NiFloatData from 0 to 1 tile. The client's NifHasAnimatedControllers (0x00bf4160) finds the
|
||||
* property's first controller and marks the object animated. Apply mode decal: what fixed function would do
|
||||
* with it is what the shader does (the texture over the vertex color by its alpha, the vertex alpha kept).
|
||||
* An NiSourceTexture stored in the file, as the client's own stored textures (res/mesh/env/env_ag_ocean-maelstrom.nif):
|
||||
* white, `alpha` its mipmaps' alpha (UgcGlitter::Mipmaps, the first the full size), 32-bit (B, G, R, A),
|
||||
* NiPersistentSrcTextureRendererData for DX9.
|
||||
*/
|
||||
int32_t GlitterTexturing(const UgcGlitter::Params& glitter) {
|
||||
if (m_Glitter >= 0) return m_Glitter;
|
||||
m_Glitter = m_Nif.Reserve("NiTexturingProperty");
|
||||
std::vector<std::pair<uint32_t, float>> motions; // TexTransform (0 translate U, 1 translate V), seconds a tile
|
||||
if (glitter.PeriodU() > 0.0f) motions.emplace_back(0, glitter.PeriodU());
|
||||
if (glitter.PeriodV() > 0.0f) motions.emplace_back(1, glitter.PeriodV());
|
||||
std::vector<int32_t> controllers;
|
||||
for (size_t i = 0; i < motions.size(); i++) controllers.push_back(m_Nif.Reserve("NiTextureTransformController"));
|
||||
for (size_t i = 0; i < motions.size(); i++) {
|
||||
const auto [operation, period] = motions[i];
|
||||
Writer data;
|
||||
data.U32(2); // keys
|
||||
data.U32(1); // linear
|
||||
data.Float(0.0f);
|
||||
data.Float(0.0f);
|
||||
data.Float(period);
|
||||
data.Float(1.0f);
|
||||
const auto interpolator = m_Nif.Reserve("NiFloatInterpolator");
|
||||
const auto dataBlock = m_Nif.Add("NiFloatData", std::move(data.Data()));
|
||||
Writer value;
|
||||
value.Float(0.0f);
|
||||
value.I32(dataBlock);
|
||||
m_Nif.Fill(interpolator, std::move(value.Data()));
|
||||
Writer controller;
|
||||
controller.I32(i + 1 < controllers.size() ? controllers[i + 1] : -1); // next controller
|
||||
controller.U16(0x48); // active, loop, app time (as the client's files)
|
||||
controller.Float(1.0f); // frequency
|
||||
controller.Float(0.0f); // phase
|
||||
controller.Float(0.0f); // start
|
||||
controller.Float(period); // stop
|
||||
controller.I32(m_Glitter); // target
|
||||
controller.I32(interpolator);
|
||||
controller.U8(0); // not a shader map
|
||||
controller.U32(0); // the base map
|
||||
controller.U32(operation);
|
||||
m_Nif.Fill(controllers[i], std::move(controller.Data()));
|
||||
}
|
||||
|
||||
// The texture: white, its alpha the flecks, mipmapped, 32-bit as the client's stored textures are (B, G, R, A)
|
||||
int32_t StoredTexture(const std::string& name, const std::vector<std::vector<uint8_t>>& mipmaps) {
|
||||
const auto source = m_Nif.Reserve("NiSourceTexture");
|
||||
Writer pixels;
|
||||
pixels.U32(1); // RGBA
|
||||
@@ -307,14 +288,13 @@ namespace {
|
||||
pixels.U8(0); // unsigned
|
||||
}
|
||||
pixels.I32(-1); // palette
|
||||
const auto mipmaps = UgcGlitter::Mipmaps(UgcGlitter::FleckAlpha(glitter.flecks));
|
||||
pixels.U32(static_cast<uint32_t>(mipmaps.size()));
|
||||
pixels.U32(4); // bytes per pixel
|
||||
uint32_t offset = 0;
|
||||
const auto side = static_cast<uint32_t>(std::lround(std::sqrt(static_cast<double>(mipmaps.empty() ? 0 : mipmaps[0].size()))));
|
||||
for (size_t level = 0; level < mipmaps.size(); level++) {
|
||||
const uint32_t side = static_cast<uint32_t>(UgcGlitter::TEXTURE_SIZE) >> level;
|
||||
pixels.U32(side);
|
||||
pixels.U32(side);
|
||||
pixels.U32(side >> level);
|
||||
pixels.U32(side >> level);
|
||||
pixels.U32(offset);
|
||||
offset += static_cast<uint32_t>(mipmaps[level].size()) * 4;
|
||||
}
|
||||
@@ -334,7 +314,7 @@ namespace {
|
||||
Writer texture;
|
||||
WriteNet(texture, -1);
|
||||
texture.U8(0); // stored in the file
|
||||
texture.I32(m_Nif.String("ugc_glitter.dds"));
|
||||
texture.I32(m_Nif.String(name));
|
||||
texture.I32(pixelData);
|
||||
texture.U32(6); // pixel layout: default
|
||||
texture.U32(2); // mipmaps: default
|
||||
@@ -343,35 +323,65 @@ namespace {
|
||||
texture.U8(0); // direct render
|
||||
texture.U8(1); // persist render data
|
||||
m_Nif.Fill(source, std::move(texture.Data()));
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* An NiTexturingProperty with only a base map, `source`: wrapping in S and T, trilinear, UV set 0; with an
|
||||
* identity texture transform (Maya method, center 0.5) when `transform` (what LEGO-AnimUV multiplies the UVs
|
||||
* by, TEXTRANSFORMBASE). No controllers: nothing updates a placed player model, see UgcGlitter.h.
|
||||
*/
|
||||
int32_t Texturing(int32_t source, uint16_t applyMode, bool transform) {
|
||||
Writer texturing;
|
||||
texturing.I32(-1); // name
|
||||
texturing.U32(0); // extra data
|
||||
texturing.I32(controllers.empty() ? -1 : controllers[0]);
|
||||
texturing.U16(1 << 1); // apply mode decal
|
||||
texturing.I32(-1); // controller
|
||||
texturing.U16(applyMode);
|
||||
texturing.U32(9); // texture slots
|
||||
texturing.U8(1); // base map
|
||||
texturing.I32(source);
|
||||
texturing.U16(0x3200); // wrap S and T, trilinear, UV set 0
|
||||
texturing.U8(1); // texture transform
|
||||
texturing.Float(0.0f); // translation
|
||||
texturing.Float(0.0f);
|
||||
texturing.Float(1.0f); // scale
|
||||
texturing.Float(1.0f);
|
||||
texturing.Float(0.0f); // rotation
|
||||
texturing.U32(2); // Maya
|
||||
texturing.Float(0.5f); // center
|
||||
texturing.Float(0.5f);
|
||||
texturing.U8(transform ? 1 : 0);
|
||||
if (transform) {
|
||||
texturing.Float(0.0f); // translation
|
||||
texturing.Float(0.0f);
|
||||
texturing.Float(1.0f); // scale
|
||||
texturing.Float(1.0f);
|
||||
texturing.Float(0.0f); // rotation
|
||||
texturing.U32(2); // Maya
|
||||
texturing.Float(0.5f); // center
|
||||
texturing.Float(0.5f);
|
||||
}
|
||||
for (int slot = 1; slot < 9; slot++) texturing.U8(0); // dark, detail, gloss, glow, bump, normal, parallax, decal
|
||||
texturing.U32(0); // shader maps
|
||||
m_Nif.Fill(m_Glitter, std::move(texturing.Data()));
|
||||
return m_Nif.Add("NiTexturingProperty", std::move(texturing.Data()));
|
||||
}
|
||||
|
||||
// The glitter groups' NiTexturingProperty (made once a file): the fleck texture, apply mode decal (what fixed
|
||||
// function would do with it is what LEGO-AnimUV does: the texture over the vertex color by its alpha, the
|
||||
// vertex alpha kept), with the texture transform LEGO-AnimUV reads
|
||||
int32_t GlitterTexturing(const UgcGlitter::Params& glitter) {
|
||||
if (m_Glitter < 0) m_Glitter = Texturing(StoredTexture("ugc_glitter.dds", UgcGlitter::Mipmaps(UgcGlitter::FleckAlpha(glitter.flecks))), 1 << 1, true);
|
||||
return m_Glitter;
|
||||
}
|
||||
|
||||
// The sparkle group's NiTexturingProperty (made once a file): the sparkle texture (its first two mipmaps keeping
|
||||
// the sparkles' alpha), apply mode replace and no transform, as the client's own Distortion Directional shapes
|
||||
// (S79__pond_ripplesShape, res/mesh/env/env_won_gnar_croc_pondfx.nif)
|
||||
int32_t SparkleTexturing(const UgcGlitter::Params& glitter) {
|
||||
if (m_Sparkle < 0) m_Sparkle = Texturing(StoredTexture("ugc_sparkle.dds", UgcGlitter::Mipmaps(UgcGlitter::SparkleAlpha(glitter), 2)), 0, false);
|
||||
return m_Sparkle;
|
||||
}
|
||||
|
||||
enum class eKind : uint8_t { PLAIN, GLITTER, SPARKLE };
|
||||
|
||||
// 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; `glitter`: with the glitter texture
|
||||
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f, const UgcGlitter::Params* glitter = nullptr) {
|
||||
// emissive color, 0 for the shared material without one; `glitter`: with the glitter texture (GLITTER) or
|
||||
// as sparkles over it (SPARKLE)
|
||||
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f, const UgcGlitter::Params* glitter = nullptr,
|
||||
eKind kind = eKind::PLAIN) {
|
||||
if (!mesh || mesh->Empty() || mesh->positions.size() > 65535 || mesh->TriangleCount() > 65535) return -1;
|
||||
if (!glitter) kind = eKind::PLAIN;
|
||||
// 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
|
||||
if (m_Alpha < 0) {
|
||||
@@ -390,7 +400,7 @@ namespace {
|
||||
// transparent brick is drawn solid, with blending off. The game's own brick models give their S01_Alpha
|
||||
// shapes 0.9999, and so do we (made once a file, only when there is a transparent shape).
|
||||
int32_t material = m_Material;
|
||||
if (transparent) {
|
||||
if (transparent && kind != eKind::SPARKLE) {
|
||||
if (m_MaterialAlpha < 0) m_MaterialAlpha = m_Nif.Add("NiMaterialProperty", Material(0.0f, 0.9999f));
|
||||
material = m_MaterialAlpha;
|
||||
}
|
||||
@@ -399,13 +409,31 @@ namespace {
|
||||
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 };
|
||||
if (glitter) properties.push_back(GlitterTexturing(*glitter));
|
||||
int32_t alpha = m_Alpha;
|
||||
if (kind == eKind::SPARKLE && m_AlphaTest < 0) {
|
||||
// Alpha tested: ShaderCommon::GetAlphaFlags puts a shape whose NiAlphaProperty has the test bit (0x200) in
|
||||
// the alpha test phase, whose own states test GREATEREQUAL 127 without blending (the flags and
|
||||
// threshold here say the same for anything else reading the file)
|
||||
Writer test;
|
||||
WriteNet(test, -1);
|
||||
test.U16(0x0200 | (6 << 10)); // test, GREATEREQUAL
|
||||
test.U8(127);
|
||||
m_AlphaTest = m_Nif.Add("NiAlphaProperty", std::move(test.Data()));
|
||||
}
|
||||
if (kind == eKind::SPARKLE) alpha = m_AlphaTest;
|
||||
std::vector<int32_t> properties{ material, alpha, m_Specular, m_VertexColor };
|
||||
if (kind == eKind::GLITTER) properties.push_back(GlitterTexturing(*glitter));
|
||||
if (kind == eKind::SPARKLE) properties.push_back(SparkleTexturing(*glitter));
|
||||
const auto shapeBlock = m_Nif.Reserve("NiTriShape");
|
||||
const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh, glitter));
|
||||
std::string data;
|
||||
if (kind == eKind::GLITTER) data = TriShapeData(*mesh, GlitterUvs(*mesh, *glitter, UgcGlitter::eLayer::FLECKS));
|
||||
else if (kind == eKind::SPARKLE) {
|
||||
const auto sparkles = SparkleMesh(*mesh, *glitter);
|
||||
data = TriShapeData(sparkles, GlitterUvs(*mesh, *glitter, UgcGlitter::eLayer::SPARKLES));
|
||||
} else data = TriShapeData(*mesh);
|
||||
const auto dataBlock = m_Nif.Add("NiTriShapeData", std::move(data));
|
||||
Writer tri;
|
||||
const bool animated = glitter && (glitter->PeriodU() > 0.0f || glitter->PeriodV() > 0.0f);
|
||||
WriteAv(tri, m_Nif.String(name), properties, animated ? ANIMATED_SHAPE_FLAGS : SHAPE_FLAGS);
|
||||
WriteAv(tri, m_Nif.String(name), properties, SHAPE_FLAGS);
|
||||
tri.I32(dataBlock);
|
||||
tri.I32(-1); // skin instance
|
||||
tri.U32(0); // materials
|
||||
@@ -421,9 +449,11 @@ namespace {
|
||||
int32_t m_MaterialAlpha{ -1 }; // transparent shapes' (alpha 0.9999)
|
||||
int32_t m_VertexColor{ -1 };
|
||||
int32_t m_Alpha{ -1 };
|
||||
int32_t m_AlphaTest{ -1 }; // the sparkles' (alpha tested)
|
||||
int32_t m_Specular{ -1 };
|
||||
std::map<float, int32_t> m_Emissive; // emissive color -> its material
|
||||
int32_t m_Glitter{ -1 }; // the glitter groups' NiTexturingProperty
|
||||
int32_t m_Sparkle{ -1 }; // the sparkle group's
|
||||
};
|
||||
}
|
||||
|
||||
@@ -446,12 +476,9 @@ namespace UgcFormats {
|
||||
const int32_t root = nif.Reserve("NiNode");
|
||||
SharedProperties properties(nif);
|
||||
std::vector<int32_t> groupBlocks;
|
||||
bool anyAnimated = false;
|
||||
for (const auto& group : groups) {
|
||||
if (group.lods.empty()) continue;
|
||||
const bool animated = group.glitter && (group.glitter->PeriodU() > 0.0f || group.glitter->PeriodV() > 0.0f);
|
||||
anyAnimated = anyAnimated || animated;
|
||||
const auto nodeFlags = animated ? ANIMATED_NODE_FLAGS : NODE_FLAGS;
|
||||
const auto kind = group.sparkle ? SharedProperties::eKind::SPARKLE : group.glitter ? SharedProperties::eKind::GLITTER : SharedProperties::eKind::PLAIN;
|
||||
const auto lodNode = nif.Reserve("NiLODNode");
|
||||
std::vector<int32_t> levels;
|
||||
Writer ranges;
|
||||
@@ -461,16 +488,16 @@ 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, group.emissive, group.glitter);
|
||||
const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive, group.glitter, kind);
|
||||
if (block >= 0) shapes.push_back(block);
|
||||
}
|
||||
nif.Fill(level, NodeData(nif.String(lod.name), shapes, nodeFlags));
|
||||
nif.Fill(level, NodeData(nif.String(lod.name), shapes));
|
||||
levels.push_back(level);
|
||||
ranges.Float(lod.nearDistance);
|
||||
ranges.Float(lod.farDistance);
|
||||
}
|
||||
const auto rangeData = nif.Add("NiRangeLODData", std::move(ranges.Data()));
|
||||
auto data = NodeData(nif.String(group.name), levels, nodeFlags);
|
||||
auto data = NodeData(nif.String(group.name), levels);
|
||||
Writer lod;
|
||||
lod.Raw(data);
|
||||
lod.U16(3); // switch flags: update only the active child, and controllers (as the game's own files)
|
||||
@@ -479,7 +506,7 @@ namespace UgcFormats {
|
||||
nif.Fill(lodNode, std::move(lod.Data()));
|
||||
groupBlocks.push_back(lodNode);
|
||||
}
|
||||
nif.Fill(root, NodeData(nif.String(rootName), groupBlocks, anyAnimated ? ANIMATED_NODE_FLAGS : NODE_FLAGS));
|
||||
nif.Fill(root, NodeData(nif.String(rootName), groupBlocks));
|
||||
return nif.Finish(root);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,13 @@ namespace UgcFormats {
|
||||
// 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 glitter of the UGC server's glitter groups (docs/UgcServer.md, "Metal and glow"): the shapes get UVs
|
||||
// The glitter of the UGC server's glitter groups (docs/UgcServer.md, "Glitter"): the shapes get UVs
|
||||
// (UgcGlitter::Uv) and an NiTexturingProperty whose base map is the fleck texture stored in the file
|
||||
// (NiSourceTexture, NiPersistentSrcTextureRendererData), its texture transform's translation looped by an
|
||||
// NiTextureTransformController for U and one for V. Null: none.
|
||||
// (NiSourceTexture, NiPersistentSrcTextureRendererData). Null: none.
|
||||
const UgcGlitter::Params* glitter{};
|
||||
// With `glitter`: the group is the sparkles over its pieces (the glitter bricks) instead: each piece lifted off
|
||||
// along its normals, the sparkles' vertex colors, UVs for the sparkle texture, alpha tested (UgcGlitter.h)
|
||||
bool sparkle{};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
#include <cmath>
|
||||
|
||||
namespace UgcGlitter {
|
||||
namespace {
|
||||
uint64_t SplitMix(uint64_t x) {
|
||||
x += 0x9E3779B97F4A7C15ull;
|
||||
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
|
||||
return x ^ (x >> 31);
|
||||
}
|
||||
// 0..1 from 24 bits of a hash
|
||||
float Unit(uint64_t bits) { return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24); }
|
||||
// The side of a square texture's alpha
|
||||
int Side(const std::vector<uint8_t>& alpha) { return static_cast<int>(std::lround(std::sqrt(static_cast<double>(alpha.size())))); }
|
||||
}
|
||||
|
||||
std::vector<uint8_t> FleckAlpha(uint32_t flecks) {
|
||||
constexpr int N = TEXTURE_SIZE;
|
||||
std::vector<float> alpha(static_cast<size_t>(N) * N, 0.0f);
|
||||
@@ -36,55 +49,89 @@ namespace UgcGlitter {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha) {
|
||||
float Params::SparkleTile() const {
|
||||
return 75.0f * std::max(sparkleSize, 0.001f) * std::max(speed, 0.01f);
|
||||
}
|
||||
|
||||
int Params::SparkleTextureSize() const {
|
||||
// A sparkle 3 pixels wide: side = 3 * tile / size (225 at speed 1)
|
||||
const float wanted = 3.0f * SparkleTile() / std::max(sparkleSize, 0.001f);
|
||||
int side = 128;
|
||||
while (side < 1024 && static_cast<float>(side) < wanted) side *= 2;
|
||||
return side;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> SparkleAlpha(const Params& params) {
|
||||
const int N = params.SparkleTextureSize();
|
||||
std::vector<uint8_t> alpha(static_cast<size_t>(N) * N, 0);
|
||||
const float radius = std::max(params.sparkleSize / params.SparkleTile() * static_cast<float>(N) * 0.5f, 0.75f);
|
||||
const float share = std::clamp(params.sparkleAmount, 0.0f, 100.0f) / 100.0f;
|
||||
const auto count = static_cast<uint32_t>(std::lround(share * static_cast<float>(N) * static_cast<float>(N) / (3.14159265f * radius * radius)));
|
||||
uint64_t state = 0x737061726B6C6500ull;
|
||||
const auto next = [&state] { return Unit(SplitMix(state++)); };
|
||||
const int reach = static_cast<int>(std::ceil(radius + 0.5f));
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
const float cx = next() * N, cy = next() * N;
|
||||
for (int dy = -reach; dy <= reach; dy++) {
|
||||
for (int dx = -reach; dx <= reach; dx++) {
|
||||
const int x = static_cast<int>(std::floor(cx)) + dx, y = static_cast<int>(std::floor(cy)) + dy;
|
||||
const float ddx = x + 0.5f - cx, ddy = y + 0.5f - cy;
|
||||
// Flat, with a pixel's worth of edge
|
||||
const float cover = std::clamp(radius + 0.5f - std::sqrt(ddx * ddx + ddy * ddy), 0.0f, 1.0f);
|
||||
if (cover <= 0.0f) continue;
|
||||
auto& value = alpha[static_cast<size_t>(((y % N) + N) % N) * N + ((x % N) + N) % N];
|
||||
value = std::max(value, static_cast<uint8_t>(std::lround(cover * SPARKLE_ALPHA)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha, int keepPeaks) {
|
||||
std::vector<std::vector<uint8_t>> levels{ alpha };
|
||||
for (int size = TEXTURE_SIZE / 2; size >= 1; size /= 2) {
|
||||
for (int size = Side(alpha) / 2, level = 0; size >= 1; size /= 2, level++) {
|
||||
const auto& above = levels.back();
|
||||
const int from = size * 2;
|
||||
std::vector<uint8_t> level(static_cast<size_t>(size) * size);
|
||||
std::vector<uint8_t> next(static_cast<size_t>(size) * size);
|
||||
for (int y = 0; y < size; y++) {
|
||||
for (int x = 0; x < size; x++) {
|
||||
const auto at = [&](int dx, int dy) { return static_cast<int>(above[static_cast<size_t>(y * 2 + dy) * from + x * 2 + dx]); };
|
||||
level[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>((at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
|
||||
next[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>(level < keepPeaks ? std::max({ at(0, 0), at(1, 0), at(0, 1), at(1, 1) }) :
|
||||
(at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
|
||||
}
|
||||
}
|
||||
levels.push_back(std::move(level));
|
||||
levels.push_back(std::move(next));
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
namespace {
|
||||
uint64_t SplitMix(uint64_t x) {
|
||||
x += 0x9E3779B97F4A7C15ull;
|
||||
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
|
||||
return x ^ (x >> 31);
|
||||
}
|
||||
// 0..1 from 24 bits of a hash
|
||||
float Unit(uint64_t bits) { return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24); }
|
||||
}
|
||||
|
||||
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick) {
|
||||
const auto hash = SplitMix(SplitMix(modelSeed ^ 0x676C6974746572ull) + brick);
|
||||
return static_cast<uint32_t>(hash >> 32) | 1u;
|
||||
}
|
||||
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed) {
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed, eLayer layer) {
|
||||
const auto a = glm::abs(normal);
|
||||
const float scale = 1.0f / std::max(tile, 1e-3f);
|
||||
const int plane = a.x >= a.y && a.x >= a.z ? 0 : a.y >= a.z ? 1 : 2;
|
||||
const glm::vec2 uv = (plane == 0 ? glm::vec2(position.z, position.y) : plane == 1 ? glm::vec2(position.x, position.z) : glm::vec2(position.x, position.y)) * scale;
|
||||
if (seed == 0) return uv;
|
||||
const auto hash = SplitMix((static_cast<uint64_t>(seed) << 2) | static_cast<uint64_t>(plane));
|
||||
const auto hash = SplitMix(((static_cast<uint64_t>(seed) << 2) | static_cast<uint64_t>(plane)) ^ (static_cast<uint64_t>(layer) << 40));
|
||||
const float angle = Unit(hash) * 6.28318530718f;
|
||||
const glm::vec2 offset(Unit(SplitMix(hash)), Unit(SplitMix(hash + 1)));
|
||||
const float c = std::cos(angle), s = std::sin(angle);
|
||||
return glm::vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y) + offset;
|
||||
}
|
||||
|
||||
glm::vec4 SparkleColor(const glm::vec4& brickColor, const Params& params) {
|
||||
const float tint = std::clamp(params.sparkleTint, 0.0f, 100.0f) / 100.0f;
|
||||
const float brightness = std::clamp(params.sparkleBrightness, 0.0f, 100.0f) / 100.0f;
|
||||
return glm::vec4(glm::clamp(glm::mix(glm::vec3(1.0f), glm::vec3(brickColor), tint) * brightness, 0.0f, 1.0f), 1.0f);
|
||||
}
|
||||
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv) {
|
||||
constexpr int N = TEXTURE_SIZE;
|
||||
if (alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
|
||||
const int N = Side(alpha);
|
||||
if (N == 0 || alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
|
||||
const float x = (uv.x - std::floor(uv.x)) * N - 0.5f, y = (uv.y - std::floor(uv.y)) * N - 0.5f;
|
||||
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
|
||||
const float fx = x - x0, fy = y - y0;
|
||||
|
||||
@@ -6,43 +6,81 @@
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
/**
|
||||
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Metal and glow"): a tileable texture of white
|
||||
* flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader (lerp(vertex color, texture,
|
||||
* texture alpha), then the LEGO lighting), on UVs projected from the model's own coordinates so every brick gets the
|
||||
* same density, turned and moved by a number of each brick's own (BrickSeed) so no two bricks have the same pattern,
|
||||
* drifting as the texture transform's translation loops. Pure.
|
||||
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Glitter"). Pure.
|
||||
*
|
||||
* Flecks: a tileable texture of white flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader
|
||||
* (lerp(vertex color, texture, texture alpha), then the LEGO lighting). They stay still: a placed player model is never
|
||||
* updated after it loads (LWOSkinnedRenderComponent::Run with animation off for modelType 2), so texture controllers in
|
||||
* its .nif never run.
|
||||
*
|
||||
* Sparkles: a second shape over each glitter brick drawn by the client's Distortion Directional (Ocean) shader, whose
|
||||
* texture layers the client moves every frame whatever the object does (the shader's Run sets
|
||||
* g_vDirectionalMotionLayer1..3). Its texture has flat sparkles at an alpha that one layer's alone keeps under the
|
||||
* alpha test, so a sparkle shows only where two moving layers' sparkles meet: points that flash and go out.
|
||||
*
|
||||
* Both are placed on each brick by UVs projected from the model's own coordinates, turned and moved by a number of the
|
||||
* brick's own (BrickSeed), so no two bricks have the same pattern and a model made again has the same one.
|
||||
*/
|
||||
namespace UgcGlitter {
|
||||
// The texture's side in pixels (a power of two, mipmapped down to 1)
|
||||
// The fleck texture's side in pixels (a power of two, mipmapped down to 1)
|
||||
constexpr int TEXTURE_SIZE = 128;
|
||||
|
||||
struct Params {
|
||||
float tile{ 1.6f }; // glitter_size: the texture's side in model units (LDD units: a stud is 0.8)
|
||||
// Flecks (LEGO-AnimUV)
|
||||
float tile{ 1.6f }; // glitter_size: the fleck texture's side in model units (LDD units: a stud is 0.8)
|
||||
uint32_t flecks{ 50 }; // glitter_density: flecks in one tile
|
||||
float speed{ 1.0f }; // glitter_speed: 1 moves the flecks a tile in U in 7 s and in V in 11 s; 0 keeps them still
|
||||
bool random{ true }; // glitter_random: each brick its own pattern (BrickSeed), else the same on every brick
|
||||
// Sparkles (Distortion Directional)
|
||||
float sparkleSize{ 0.1f }; // glitter_sparkle_size: a sparkle's diameter in model units
|
||||
float sparkleAmount{ 5.0f }; // glitter_sparkle_amount: percent of each moving layer covered by sparkles
|
||||
float speed{ 1.0f }; // glitter_speed: how fast sparkles flash and go out (1: about half a second)
|
||||
float sparkleTint{ 30.0f }; // glitter_sparkle_tint: percent, how far sparkles take their brick's color
|
||||
float sparkleBrightness{ 100.0f }; // glitter_sparkle_brightness: percent, the sparkles' vertex color
|
||||
|
||||
// Seconds the texture's translation takes to go one tile in U and in V (0: no animation)
|
||||
float PeriodU() const { return speed > 0.0f ? 7.0f / speed : 0.0f; }
|
||||
float PeriodV() const { return speed > 0.0f ? 11.0f / speed : 0.0f; }
|
||||
// The sparkle texture's side in model units. The client moves its layers a fixed share of a tile a second (a
|
||||
// tile in 24, 48 and 72 s), so the tile sets how fast they cross: 75 sparkle sizes times the speed.
|
||||
float SparkleTile() const;
|
||||
// The sparkle texture's side in pixels: the power of two (128 to 1024) that makes a sparkle 3 pixels wide
|
||||
int SparkleTextureSize() const;
|
||||
bool operator==(const Params&) const = default;
|
||||
};
|
||||
|
||||
// The texture's alpha (TEXTURE_SIZE squared, rows top to bottom): `flecks` soft dots at the same places every time,
|
||||
// wrapping around the edges so the texture tiles. Its color is white.
|
||||
// The sparkles' alpha in their texture. The alpha test of the client's alpha test phase keeps what reaches 127
|
||||
// (ShaderCommon__SetupPhaseRenderStates: GREATEREQUAL 0x7f); the Directional shader averages 2 layers or 3 (by the
|
||||
// graphics settings), so one sparkle alone is 115 or 77 and two meeting are 230 or 153
|
||||
constexpr uint8_t SPARKLE_ALPHA = 230;
|
||||
// How far the sparkle shapes stand off their brick, along its normals (model units): in front of its surface, so
|
||||
// the brick (drawn after them when it is transparent) doesn't cover them and they don't fight it for the depth
|
||||
constexpr float SPARKLE_LIFT = 0.005f;
|
||||
|
||||
// The fleck texture's alpha (TEXTURE_SIZE squared, rows top to bottom): `flecks` soft dots at the same places every
|
||||
// time, wrapping around the edges so the texture tiles. Its color is white.
|
||||
std::vector<uint8_t> FleckAlpha(uint32_t flecks);
|
||||
|
||||
// The texture's mipmaps' alpha, from TEXTURE_SIZE down to 1 (each the mean of 2x2 of the one before)
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha);
|
||||
// The sparkle texture's alpha (SparkleTextureSize() squared): flat discs of sparkleSize at SPARKLE_ALPHA covering
|
||||
// sparkleAmount percent of it, at the same places every time, tiling. Its color is white.
|
||||
std::vector<uint8_t> SparkleAlpha(const Params& params);
|
||||
|
||||
// A square texture's mipmaps' alpha, from its own size down to 1: each the mean of 2x2 of the one before, or for
|
||||
// the first `keepPeaks` the brightest of them (so sparkles keep their alpha at the next few distances)
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha, int keepPeaks = 0);
|
||||
|
||||
// A brick's number for placing its glitter (never 0), from the model's seed and the brick's index: the same for
|
||||
// the brick in every LOD and every time the model is made
|
||||
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick);
|
||||
|
||||
// A vertex's UV: its position on the axis plane its normal faces most, in tiles, turned by an angle and moved by
|
||||
// an offset (under a tile) that `seed` (the brick's BrickSeed) picks for each plane; seed 0 leaves it as it is
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed = 0);
|
||||
// Which texture a UV set is for: a brick's sparkles are placed apart from its flecks
|
||||
enum class eLayer : uint8_t { FLECKS = 0, SPARKLES };
|
||||
|
||||
// The texture's alpha (0..1) at `uv` (wrapping, bilinear)
|
||||
// A vertex's UV: its position on the axis plane its normal faces most, in tiles, turned by an angle and moved by
|
||||
// an offset (under a tile) that `seed` (the brick's BrickSeed) picks for each plane and layer; seed 0 leaves it
|
||||
// as it is
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed = 0, eLayer layer = eLayer::FLECKS);
|
||||
|
||||
// A sparkle's vertex color: white taking `sparkleTint` percent of its brick's color (sRGB), at `sparkleBrightness`,
|
||||
// alpha 1
|
||||
glm::vec4 SparkleColor(const glm::vec4& brickColor, const Params& params);
|
||||
|
||||
// A square texture's alpha (0..1) at `uv` (wrapping, bilinear)
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv);
|
||||
}
|
||||
|
||||
@@ -363,9 +363,10 @@ namespace UgcModel {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks) {
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks, const std::set<int32_t>& overlayTags) {
|
||||
Model model;
|
||||
for (const auto& source : nif.meshes) {
|
||||
if (source.material.alphaTest && overlayTags.contains(source.material.shaderTag)) continue;
|
||||
Mesh mesh;
|
||||
const size_t count = source.positions.size() / 3;
|
||||
const bool vertexColors = source.material.vertexColorMode == 2 && source.colors.size() == count * 4;
|
||||
|
||||
@@ -145,8 +145,9 @@ namespace UgcModel {
|
||||
|
||||
// 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, and of the
|
||||
// transparent ones when it is GLITTER
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {});
|
||||
// transparent ones when it is GLITTER. `overlayTags`: alpha tested shapes with these tags are left out (the UGC
|
||||
// server's glitter sparkles, drawn over the glitter bricks)
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {});
|
||||
|
||||
/**
|
||||
* The mesh's triangles by look ([eLook] -> its triangles; a triangle's look is its first vertex's), the looks not
|
||||
|
||||
@@ -128,6 +128,17 @@ namespace UgcJobs {
|
||||
return looks;
|
||||
}
|
||||
|
||||
std::set<int32_t> Shaders::OverlayTags() const {
|
||||
std::set<int32_t> tags{ 79 };
|
||||
if (sparkle != 0) tags.insert(static_cast<int32_t>(sparkle));
|
||||
return tags;
|
||||
}
|
||||
|
||||
std::string SparkleName(const Settings& settings) {
|
||||
const auto tag = std::to_string(settings.shaders.sparkle);
|
||||
return ("S" + std::string(tag.size() < 2 ? "0" : "") + tag + "_GlitterSparkle_Model").substr(0, 60);
|
||||
}
|
||||
|
||||
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent) {
|
||||
if (transparent && look != UgcModel::eLook::GLITTER) return "S01_Alpha_Model";
|
||||
if (look == UgcModel::eLook::PLASTIC) return ("S" + settings.shaderOpaque + "_Opaque_Model").substr(0, 60);
|
||||
@@ -138,10 +149,10 @@ namespace UgcJobs {
|
||||
}
|
||||
|
||||
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 std::map<int32_t, UgcModel::eLook>& tagLooks, const std::set<int32_t>& overlayTags) {
|
||||
const auto readBack = NifFile::Parse(nif, 0, error);
|
||||
if (!readBack) return false;
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks), options);
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks, overlayTags), options);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -189,6 +200,8 @@ namespace UgcJobs {
|
||||
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)];
|
||||
// The glitter bricks' sparkles, a group over both glitter groups
|
||||
const bool sparkles = separate[static_cast<size_t>(UgcModel::eLook::GLITTER)] && settings.shaders.sparkle != 0;
|
||||
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;
|
||||
@@ -251,6 +264,12 @@ namespace UgcJobs {
|
||||
for (const auto& piece : transparentPieces.back()[kind]) triangles += piece.TriangleCount();
|
||||
if (triangles > 0) byGroup[ShapeName(settings, kind ? UgcModel::eLook::GLITTER : UgcModel::eLook::PLASTIC, true)] = triangles;
|
||||
}
|
||||
if (sparkles) {
|
||||
size_t triangles = 0;
|
||||
for (const auto& piece : opaquePieces.back()[static_cast<size_t>(UgcModel::eLook::GLITTER)]) triangles += piece.TriangleCount();
|
||||
for (const auto& piece : transparentPieces.back()[1]) triangles += piece.TriangleCount();
|
||||
if (triangles > 0) byGroup[SparkleName(settings)] = triangles;
|
||||
}
|
||||
}
|
||||
lodStats.push_back(entry);
|
||||
}
|
||||
@@ -277,6 +296,21 @@ namespace UgcJobs {
|
||||
}
|
||||
if (any) out.push_back(std::move(group));
|
||||
}
|
||||
// Last, over everything: the sparkles over the opaque and the transparent glitter bricks
|
||||
if (sparkles) {
|
||||
UgcFormats::NifLodGroup group{ SparkleName(settings), false, {} };
|
||||
group.glitter = &settings.shaders.glitterParams;
|
||||
group.sparkle = true;
|
||||
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 : opaque[i][static_cast<size_t>(UgcModel::eLook::GLITTER)]) lod.pieces.push_back(&piece);
|
||||
for (const auto& piece : transparent[i][1]) lod.pieces.push_back(&piece);
|
||||
any = any || !lod.pieces.empty();
|
||||
group.lods.push_back(std::move(lod));
|
||||
}
|
||||
if (any) out.push_back(std::move(group));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", groups(lods.size(), opaquePieces, transparentPieces));
|
||||
@@ -295,7 +329,7 @@ namespace UgcJobs {
|
||||
auto iconOptions = settings.icon;
|
||||
UgcIconParams::Apply(iconOptions, iconValues);
|
||||
std::string nifError;
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks())) {
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks(), settings.shaders.OverlayTags())) {
|
||||
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -33,8 +34,11 @@ namespace UgcJobs {
|
||||
uint32_t brushed{}; // shader_brushed: 89 Brushed Steel
|
||||
uint32_t glow{}; // shader_glow: 46 LEGO-Emissive
|
||||
uint32_t glitter{}; // shader_glitter: 21 LEGO-AnimUV (opaque and transparent glitter, each a group)
|
||||
// shader_glitter_sparkle: 79 Distortion Directional (Ocean), the glitter bricks' sparkles, a group over both
|
||||
// glitter groups (so only with shader_glitter)
|
||||
uint32_t sparkle{};
|
||||
float glowEmissive{ 1.0f }; // glow_emissive: the glow shapes' NiMaterialProperty emissive (how much the vertex color shows unlit)
|
||||
UgcGlitter::Params glitterParams; // glitter_size, glitter_density, glitter_speed
|
||||
UgcGlitter::Params glitterParams; // glitter_* (flecks and sparkles)
|
||||
|
||||
// The mapShaders id of a look's group, 0 for the plastic S01_Opaque_Model
|
||||
uint32_t TagOf(UgcModel::eLook look) const;
|
||||
@@ -42,6 +46,9 @@ namespace UgcJobs {
|
||||
// client's Polished Metal (88), Brushed Steel (89), LEGO-Emissive (46) and LEGO-AnimUV (21) for .nifs made with
|
||||
// other settings
|
||||
std::map<int32_t, UgcModel::eLook> TagLooks() const;
|
||||
// The tags of groups drawn over others (the sparkles: this setting's id and the client's 79), which the icon
|
||||
// leaves out (UgcModel::FromNif)
|
||||
std::set<int32_t> OverlayTags() const;
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
@@ -81,14 +88,18 @@ namespace UgcJobs {
|
||||
|
||||
// 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
|
||||
// (the groups drawn over others, `overlayTags` (Shaders::OverlayTags), left out)
|
||||
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 std::map<int32_t, UgcModel::eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {});
|
||||
|
||||
// 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, S21_Glitter_Model and S21_GlitterAlpha_Model (transparent glitter; 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);
|
||||
|
||||
// The name of the glitter sparkles' group, S79_GlitterSparkle_Model (the id from the settings)
|
||||
std::string SparkleName(const Settings& settings);
|
||||
|
||||
// How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts)
|
||||
size_t CountParts(std::string_view lxfml);
|
||||
|
||||
|
||||
@@ -242,7 +242,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, settings.shaders.TagLooks());
|
||||
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
|
||||
if (!nif) outcome.error = "the model has no stored .nif yet";
|
||||
}
|
||||
if (outcome.ok && outcome.files.contains("assembly.nif")) {
|
||||
@@ -289,7 +289,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, settings.shaders.TagLooks());
|
||||
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
|
||||
if (!nif) done.outcome.error = "no stored .nif";
|
||||
} else {
|
||||
done.outcome = job.kind == Kind::MODEL
|
||||
|
||||
@@ -117,11 +117,17 @@ namespace {
|
||||
settings.shaders.brushed = std::min(Setting<uint32_t>("shader_brushed", 89), 9999u);
|
||||
settings.shaders.glow = std::min(Setting<uint32_t>("shader_glow", 46), 9999u);
|
||||
settings.shaders.glowEmissive = std::clamp(Setting<float>("glow_emissive", 1.0f), 0.0f, 10.0f);
|
||||
// Glitter colors in S<id>_Glitter_Model and S<id>_GlitterAlpha_Model with drifting flecks (LEGO-AnimUV)
|
||||
// Glitter colors in S<id>_Glitter_Model and S<id>_GlitterAlpha_Model with flecks (LEGO-AnimUV), and sparkles
|
||||
// over them in S<id>_GlitterSparkle_Model (Distortion Directional, which moves on its own)
|
||||
settings.shaders.glitter = std::min(Setting<uint32_t>("shader_glitter", 21), 9999u);
|
||||
settings.shaders.sparkle = std::min(Setting<uint32_t>("shader_glitter_sparkle", 79), 9999u);
|
||||
settings.shaders.glitterParams.tile = std::clamp(Setting<float>("glitter_size", 1.6f), 0.1f, 100.0f);
|
||||
settings.shaders.glitterParams.flecks = std::min(Setting<uint32_t>("glitter_density", 50), 2000u);
|
||||
settings.shaders.glitterParams.speed = std::clamp(Setting<float>("glitter_speed", 1.0f), 0.0f, 100.0f);
|
||||
settings.shaders.glitterParams.sparkleSize = std::clamp(Setting<float>("glitter_sparkle_size", 0.1f), 0.01f, 1.0f);
|
||||
settings.shaders.glitterParams.sparkleAmount = std::clamp(Setting<float>("glitter_sparkle_amount", 5.0f), 0.0f, 50.0f);
|
||||
settings.shaders.glitterParams.speed = std::clamp(Setting<float>("glitter_speed", 1.0f), 0.1f, 4.0f);
|
||||
settings.shaders.glitterParams.sparkleTint = std::clamp(Setting<float>("glitter_sparkle_tint", 30.0f), 0.0f, 100.0f);
|
||||
settings.shaders.glitterParams.sparkleBrightness = std::clamp(Setting<float>("glitter_sparkle_brightness", 100.0f), 0.0f, 100.0f);
|
||||
settings.shaders.glitterParams.random = Setting<int32_t>("glitter_random", 1) != 0;
|
||||
// Which Materials.xml MaterialTypes are metal, brushed steel and glitter
|
||||
for (const auto& [key, look] : { std::pair{ "metal_material_types", UgcModel::eLook::METAL }, std::pair{ "brushed_material_types", UgcModel::eLook::BRUSHED },
|
||||
|
||||
Reference in New Issue
Block a user