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:
Aaron Kimbrell
2026-09-29 00:27:55 -05:00
parent 63bfaf545e
commit fb7850fb9f
19 changed files with 562 additions and 248 deletions

View File

@@ -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;