diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 900832476..8e530b80c 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -480,6 +480,7 @@ namespace { c.Add(Float(UGC, "glitter_size", "Glitter tile size", "The fleck texture's tile in model units (a stud is 0.8): how far apart the flecks are, the same on every brick.", "1.6", 0.1f, 100)); c.Add(Int(UGC, "glitter_density", "Glitter flecks", "Flecks in one tile of the glitter texture.", "50", 0, 2000)); c.Add(Float(UGC, "glitter_speed", "Glitter speed", "How fast the flecks drift: 1 moves them a tile in 7 s one way and 11 s the other; 0 keeps them still.", "1", 0, 100)); + c.Add(Bool(UGC, "glitter_random", "Glitter placed per brick", "Each glitter brick gets its own fleck pattern (turned and moved by a number of the brick's own, the same every time the model is made); off: the same pattern on every brick.", true)); c.Add(Text(UGC, "satin_colors", "Satin colors", "Satin (opal) color ids, comma separated: they stay transparent plastic (the client has no satin shader) but are made milky and less see-through. By default LEGO's satin colors 360,362,363,364,365,366,367,376 (none: off)." + notLive, "360,362,363,364,365,366,367,376")); c.Add(Float(UGC, "satin_opacity", "Satin opacity", "Percent: the opacity of transparent satin bricks, instead of the transparent opacity.", "75", 0, 100)); c.Add(Float(UGC, "satin_whiten", "Satin whitening", "Percent: how far satin colors go towards white.", "20", 0, 100)); diff --git a/dUgcServer/Formats/UgcFormats.cpp b/dUgcServer/Formats/UgcFormats.cpp index 5114772cc..428d0ec4d 100644 --- a/dUgcServer/Formats/UgcFormats.cpp +++ b/dUgcServer/Formats/UgcFormats.cpp @@ -144,7 +144,8 @@ namespace { out.I32(-1); // collision object } - // `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv) + // `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) { Writer out; const auto count = static_cast(mesh.positions.size()); @@ -191,7 +192,8 @@ namespace { } if (uvs) { for (size_t v = 0; v < mesh.positions.size(); v++) { - const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter->tile); + 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); out.Float(uv.x); out.Float(uv.y); } diff --git a/dUgcServer/Model/UgcGlitter.cpp b/dUgcServer/Model/UgcGlitter.cpp index 165e033d0..b1063d2a3 100644 --- a/dUgcServer/Model/UgcGlitter.cpp +++ b/dUgcServer/Model/UgcGlitter.cpp @@ -53,12 +53,33 @@ namespace UgcGlitter { return levels; } - glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile) { + 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(bits >> 40) / static_cast(1ull << 24); } + } + + uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick) { + const auto hash = SplitMix(SplitMix(modelSeed ^ 0x676C6974746572ull) + brick); + return static_cast(hash >> 32) | 1u; + } + + glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed) { const auto a = glm::abs(normal); const float scale = 1.0f / std::max(tile, 1e-3f); - if (a.x >= a.y && a.x >= a.z) return glm::vec2(position.z, position.y) * scale; - if (a.y >= a.z) return glm::vec2(position.x, position.z) * scale; - return glm::vec2(position.x, position.y) * scale; + 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(seed) << 2) | static_cast(plane)); + 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; } float Sample(const std::vector& alpha, const glm::vec2& uv) { diff --git a/dUgcServer/Model/UgcGlitter.h b/dUgcServer/Model/UgcGlitter.h index f3d23c838..a3c5ba23d 100644 --- a/dUgcServer/Model/UgcGlitter.h +++ b/dUgcServer/Model/UgcGlitter.h @@ -9,7 +9,8 @@ * 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, drifting as the texture transform's translation loops. Pure. + * 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. */ namespace UgcGlitter { // The texture's side in pixels (a power of two, mipmapped down to 1) @@ -19,6 +20,7 @@ namespace UgcGlitter { float tile{ 1.6f }; // glitter_size: the 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 // 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; } @@ -33,8 +35,13 @@ namespace UgcGlitter { // The texture's mipmaps' alpha, from TEXTURE_SIZE down to 1 (each the mean of 2x2 of the one before) std::vector> Mipmaps(const std::vector& alpha); - // A vertex's UV: its position on the axis plane its normal faces most, in tiles - glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile); + // 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); // The texture's alpha (0..1) at `uv` (wrapping, bilinear) float Sample(const std::vector& alpha, const glm::vec2& uv); diff --git a/dUgcServer/Model/UgcModel.cpp b/dUgcServer/Model/UgcModel.cpp index 80b7cdb5a..9867bdb68 100644 --- a/dUgcServer/Model/UgcModel.cpp +++ b/dUgcServer/Model/UgcModel.cpp @@ -10,6 +10,7 @@ #include #include "NifFile.h" +#include "UgcGlitter.h" #include "UgcPalette.h" #include "tinyxml2.h" @@ -164,6 +165,16 @@ namespace UgcModel { if (other.looks.empty()) looks.resize(positions.size(), eLook::PLASTIC); else looks.insert(looks.end(), other.looks.begin(), other.looks.end()); } + if (!brickSeeds.empty() || !other.brickSeeds.empty()) { + brickSeeds.resize(base, 0); + if (other.brickSeeds.empty()) brickSeeds.resize(positions.size(), 0); + else brickSeeds.insert(brickSeeds.end(), other.brickSeeds.begin(), other.brickSeeds.end()); + } + if (!uvs.empty() || !other.uvs.empty()) { + uvs.resize(base, glm::vec2(0.0f)); + if (other.uvs.empty()) uvs.resize(positions.size(), glm::vec2(0.0f)); + else uvs.insert(uvs.end(), other.uvs.begin(), other.uvs.end()); + } indices.reserve(indices.size() + other.indices.size()); for (const auto index : other.indices) indices.push_back(base + index); } @@ -232,6 +243,7 @@ namespace UgcModel { continue; } model.bricks++; + const auto brickSeed = UgcGlitter::BrickSeed(options.seed, brick); const auto materialOf = [&part, &library](size_t index) { auto id = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]); // Unknown colors are black in LU Toolbox (its name included, so black's variation too). A color LU @@ -306,6 +318,7 @@ namespace UgcModel { mesh.colors.push_back(color); if (&mesh == &model.opaque) model.opaque.glow.push_back(glow); mesh.looks.push_back(look); + mesh.brickSeeds.push_back(brickSeed); } for (const auto i : geometry.indices) mesh.indices.push_back(base + i); } @@ -363,6 +376,7 @@ namespace UgcModel { glm::vec4 color = materialColor; if (vertexColors) color *= glm::vec4(source.colors[v * 4], source.colors[v * 4 + 1], source.colors[v * 4 + 2], source.colors[v * 4 + 3]) / 255.0f; mesh.colors.push_back(color); + if (source.uvs.size() == count * 2) mesh.uvs.emplace_back(source.uvs[v * 2], source.uvs[v * 2 + 1]); } mesh.indices.assign(source.indices.begin(), source.indices.end()); // Normals from the faces when the file has none @@ -406,6 +420,8 @@ namespace UgcModel { if (source < mesh.colors.size()) piece.colors.push_back(mesh.colors[source]); if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[source]); if (source < mesh.looks.size()) piece.looks.push_back(mesh.looks[source]); + if (source < mesh.brickSeeds.size()) piece.brickSeeds.push_back(mesh.brickSeeds[source]); + if (source < mesh.uvs.size()) piece.uvs.push_back(mesh.uvs[source]); } piece.indices.push_back(it->second); } @@ -428,6 +444,8 @@ namespace UgcModel { if (source < mesh.colors.size()) kept.colors.push_back(mesh.colors[source]); if (source < mesh.glow.size()) kept.glow.push_back(mesh.glow[source]); if (source < mesh.looks.size()) kept.looks.push_back(mesh.looks[source]); + if (source < mesh.brickSeeds.size()) kept.brickSeeds.push_back(mesh.brickSeeds[source]); + if (source < mesh.uvs.size()) kept.uvs.push_back(mesh.uvs[source]); } kept.indices.push_back(remap[source]); } @@ -461,6 +479,8 @@ namespace UgcModel { if (source < mesh.colors.size()) current.colors.push_back(mesh.colors[source]); if (source < mesh.glow.size()) current.glow.push_back(mesh.glow[source]); if (source < mesh.looks.size()) current.looks.push_back(mesh.looks[source]); + if (source < mesh.brickSeeds.size()) current.brickSeeds.push_back(mesh.brickSeeds[source]); + if (source < mesh.uvs.size()) current.uvs.push_back(mesh.uvs[source]); } current.indices.push_back(it->second); } @@ -514,6 +534,8 @@ namespace UgcModel { if (source < mesh.colors.size()) half.colors.push_back(mesh.colors[source]); if (source < mesh.glow.size()) half.glow.push_back(mesh.glow[source]); if (source < mesh.looks.size()) half.looks.push_back(mesh.looks[source]); + if (source < mesh.brickSeeds.size()) half.brickSeeds.push_back(mesh.brickSeeds[source]); + if (source < mesh.uvs.size()) half.uvs.push_back(mesh.uvs[source]); } half.indices.push_back(remap[source]); } diff --git a/dUgcServer/Model/UgcModel.h b/dUgcServer/Model/UgcModel.h index d2e26c766..23c2b23b2 100644 --- a/dUgcServer/Model/UgcModel.h +++ b/dUgcServer/Model/UgcModel.h @@ -50,6 +50,11 @@ namespace UgcModel { std::vector colors; // sRGB, 0..1, alpha is opacity std::vector glow; // linear glow color per vertex (LU Toolbox's "Glow" layer); empty when nothing glows std::vector looks; // per vertex; empty when everything is plastic (transparent meshes: plastic or glitter) + // Per vertex: its brick's UgcGlitter::BrickSeed, which places the brick's glitter; empty when not known (a + // mesh read from a .nif) + std::vector brickSeeds; + // Per vertex: the UV set of a mesh read from a .nif (its glitter's, placed when it was made); empty otherwise + std::vector uvs; std::vector indices; size_t TriangleCount() const { return indices.size() / 3; } diff --git a/dUgcServer/Render/UgcRender.cpp b/dUgcServer/Render/UgcRender.cpp index 8b2bcf4a1..2a1cd4bcd 100644 --- a/dUgcServer/Render/UgcRender.cpp +++ b/dUgcServer/Render/UgcRender.cpp @@ -384,7 +384,10 @@ namespace UgcRender { const auto look = mesh.looks.size() == mesh.positions.size() && (isOpaque || mesh.looks[i0] == UgcModel::eLook::GLITTER) ? mesh.looks[i0] : UgcModel::eLook::PLASTIC; if (look == UgcModel::eLook::GLITTER) { // LEGO-AnimUV: lerp(vertex color, the texture's white, its alpha), then lit as plastic - const float fleck = UgcGlitter::Sample(glitterAlpha, UgcGlitter::Uv(position, normal, options.glitter.tile)); + // On the mesh's own UVs (read from the .nif: each brick's pattern placed as it was made), else projected + const auto uv = mesh.uvs.size() == mesh.positions.size() ? mesh.uvs[i0] * w0 + mesh.uvs[i1] * w1 + mesh.uvs[i2] * w2 : + UgcGlitter::Uv(position, normal, options.glitter.tile); + const float fleck = UgcGlitter::Sample(glitterAlpha, uv); base = glm::vec4(glm::mix(glm::vec3(base), glm::vec3(1.0f), fleck), base.a); } if (look == UgcModel::eLook::PLASTIC || look == UgcModel::eLook::GLITTER) { diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 6cc43de8a..64dfd042c 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -122,6 +122,7 @@ namespace { settings.shaders.glitterParams.tile = std::clamp(Setting("glitter_size", 1.6f), 0.1f, 100.0f); settings.shaders.glitterParams.flecks = std::min(Setting("glitter_density", 50), 2000u); settings.shaders.glitterParams.speed = std::clamp(Setting("glitter_speed", 1.0f), 0.0f, 100.0f); + settings.shaders.glitterParams.random = Setting("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 }, std::pair{ "glitter_material_types", UgcModel::eLook::GLITTER } }) { diff --git a/docs/UgcServer.md b/docs/UgcServer.md index f44b9a350..59499dad4 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -306,6 +306,7 @@ all of its levels, so each look needs a group of its own. | `glitter_size` | 1.6 | The glitter texture's tile, in model units (a stud is 0.8): the flecks' spacing, the same on every brick. | | `glitter_density` | 50 | Flecks in one tile. | | `glitter_speed` | 1 | How fast the flecks drift: a tile in U in 7 s and in V in 11 s at 1; 0 keeps them still (no controllers). | +| `glitter_random` | 1 | Each glitter brick its own fleck pattern (turned and moved by the brick); 0: the same pattern on every brick. | | `satin_colors` | 360,362,363,364,365,366,367,376 | Satin (opal) colors, see Satin below. The default: LEGO's color data's "Satin Colors" category (the Transparent ... Opal colors). Empty: the default; `none`: off. | | `satin_opacity` | 75 | Percent: the vertex alpha of transparent satin bricks, instead of `transparent_opacity` or the Materials.xml alpha. | | `satin_whiten` | 20 | Percent: how far satin colors are moved towards white (in linear RGB, after the color variation). | @@ -352,7 +353,11 @@ flecks on a brick that is otherwise lit as plastic, and moving the texture trans What a glitter shape has, beside what plastic shapes have (white material, alpha, specular, vertex colors): - A UV set: each vertex's position on the axis plane its normal faces most, divided by `glitter_size` - (`UgcGlitter::Uv`), so the flecks are as dense on every brick and every side. + (`UgcGlitter::Uv`), so the flecks are as dense on every brick and every side, then turned by an angle and moved by + an offset under a tile that the brick picks for each plane (`glitter_random`, on by default): each brick's number + (`UgcGlitter::BrickSeed`, from the model's id and the brick's index, kept per vertex in `Mesh::brickSeeds`), so + no two bricks have the same pattern, every LOD of a brick has its own, and a model made again gets the same. The + icon draws the flecks on the UVs the .nif has (`Mesh::uvs`, read back by `UgcModel::FromNif`). - An `NiTexturingProperty` (one per file, shared by both glitter groups): apply mode decal (fixed function would do what the shader does), 9 slots, the base map only: wrap S and T, trilinear, UV set 0, a texture transform (translation 0, scale 1, Maya method, center 0.5). diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 866e3aae9..26af58a21 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -95,6 +95,7 @@ brushed_colors=298,300,1002,1004 # over the color and moves it. 0: off, glitter stays plastic. # glitter_size: the fleck texture's tile in model units (a stud is 0.8); glitter_density: flecks in a tile; # glitter_speed: 1 moves the flecks a tile in 7 s one way and 11 s the other, 0 keeps them still. +# glitter_random: 1 places each brick's flecks its own way (turned and moved by the brick), 0 the same on every brick. # glitter_colors: LEGO color ids that are glitter whatever their type (empty: the default, 114,117, which LEGO's color # data calls glitter; none: no colors) shader_glitter=21 @@ -103,6 +104,7 @@ glitter_colors=114,117 glitter_size=1.6 glitter_density=50 glitter_speed=1 +glitter_random=1 # Satin (opal) colors stay transparent plastic (the client has no satin shader) but are made milky: # satin_colors: color ids (empty: the default, LEGO's satin colors 360,362,363,364,365,366,367,376; none: off), # satin_opacity: their transparent bricks' opacity in percent (instead of transparent_opacity), diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 979bcc866..b40ee43ca 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -1551,6 +1551,74 @@ TEST(UgcShaders, GlitterGroups) { EXPECT_EQ(off.stats.find("groups"), std::string::npos); } +// Each glitter brick gets its own fleck pattern: bricks a whole number of tiles apart (whose projected UVs are the same +// but for whole tiles) get different ones, the same model made again the same ones, another model others; +// glitter_random 0 puts the same pattern on every brick as before +TEST(UgcShaders, GlitterIsPlacedPerBrick) { + // Brick seeds: never 0, different bricks and models different, the same brick the same + EXPECT_NE(UgcGlitter::BrickSeed(7, 0), 0u); + EXPECT_EQ(UgcGlitter::BrickSeed(7, 3), UgcGlitter::BrickSeed(7, 3)); + EXPECT_NE(UgcGlitter::BrickSeed(7, 3), UgcGlitter::BrickSeed(7, 4)); + EXPECT_NE(UgcGlitter::BrickSeed(7, 3), UgcGlitter::BrickSeed(8, 3)); + // Seed 0 is the plain projection; a seed turns and moves it + const glm::vec3 p(0.4f, 0.8f, 0.2f), q(1.2f, 0.8f, 0.2f); + EXPECT_EQ(UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f, 0), UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f)); + const auto a = UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f, 12345), b = UgcGlitter::Uv(q, { 0, 0, 1 }, 1.6f, 12345); + EXPECT_NE(a, UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f)); + EXPECT_NEAR(glm::length(b - a), 0.5f, 1e-5f); // turned and moved, not stretched: still 0.8 / 1.6 tiles apart + EXPECT_NE(UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f, 12345), UgcGlitter::Uv(p, { 0, 0, 1 }, 1.6f, 54321)); + + UgcBricks::BrickLibrary library(MakeRes(), 0); + library.SetMaterials({ { 5001, { 67, 84, 147, 150, "glitter" } } }); + // Three transparent glitter bricks 3.2 apart (two tiles of 1.6) + const std::string lxfml = R"( + + + + )"; + auto settings = SmallSettings(); + settings.shaders.glitter = 21; + // Each glitter shape's UVs' fractions (the texture wraps), LOD 0 + const auto patterns = [&](const UgcJobs::Outcome& outcome) { + std::string error; + const auto read = NifFile::Parse(*ZCompression::Gunzip(outcome.files.at("model.nif.gz")), 0, error); + EXPECT_TRUE(read) << error; + std::vector> out; + for (const auto& mesh : read->meshes) { + if (mesh.material.shaderTag != 21) continue; + std::vector fractions; + for (const auto uv : mesh.uvs) fractions.push_back(std::round((uv - std::floor(uv)) * 1000.0f) / 1000.0f); + out.push_back(fractions); + } + return out; + }; + const auto random = UgcJobs::ProcessModel(lxfml, library, settings, 7); + ASSERT_TRUE(random.ok) << random.error; + const auto perBrick = patterns(random); + ASSERT_EQ(perBrick.size(), 3u); // one shape per transparent brick + EXPECT_NE(perBrick[0], perBrick[1]); + EXPECT_NE(perBrick[1], perBrick[2]); + EXPECT_NE(perBrick[0], perBrick[2]); + // The same model made again: the same file; another model (seed) with the same bricks: other patterns + EXPECT_EQ(random.files.at("model.nif.checksum"), UgcJobs::ProcessModel(lxfml, library, settings, 7).files.at("model.nif.checksum")); + EXPECT_NE(patterns(UgcJobs::ProcessModel(lxfml, library, settings, 8)), perBrick); + // Off: the same pattern on every brick, as before + settings.shaders.glitterParams.random = false; + const auto off = UgcJobs::ProcessModel(lxfml, library, settings, 7); + const auto same = patterns(off); + ASSERT_EQ(same.size(), 3u); + EXPECT_EQ(same[0], same[1]); + EXPECT_EQ(same[1], same[2]); + + // The icon draws the flecks where the .nif has them (its UVs, read back), so it changes with the placement + std::string error; + const auto read = NifFile::Parse(*ZCompression::Gunzip(random.files.at("model.nif.gz")), 0, error); + ASSERT_TRUE(read) << error; + const auto back = UgcModel::FromNif(*read, settings.shaders.TagLooks()); + EXPECT_EQ(back.transparent.uvs.size(), back.transparent.positions.size()); + EXPECT_NE(random.files.at("icon.png"), off.files.at("icon.png")); +} + // Glitter in the icon: the texture's flecks over the color before the light, where they are at the start TEST(UgcShaders, IconsDrawGlitterFlecks) { UgcModel::Model model;