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:
Aaron Kimbrell
2026-09-28 09:55:00 -05:00
parent b65220b446
commit c39b7ee935
16 changed files with 560 additions and 45 deletions

View File

@@ -483,8 +483,28 @@ namespace UgcRender {
// The sun's highlight (Blinn-Phong), white, on top of the color
const float highlight = direct > 0.0f ? options.specular * options.sunStrength / 3.14159265f * sun * std::pow(std::max(0.0f, glm::dot(normal, halfway)), std::max(options.shininess, 1.0f)) : 0.0f;
const float exposure = std::max(options.exposure, 0.0f);
return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure,
(ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f));
const auto look = isOpaque && mesh.looks.size() == mesh.positions.size() ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
if (look == UgcModel::eLook::PLASTIC) {
return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure,
(ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f));
}
const glm::vec3 linear(ToLinear(base.r), ToLinear(base.g), ToLinear(base.b));
glm::vec3 shaded = (linear * lighting + glm::vec3(highlight)) * exposure;
if (look == UgcModel::eLook::METAL || look == UgcModel::eLook::BRUSHED) {
// A reflection of a bright sky over a dark ground, tinted by the color (polished: sharp; brushed: blurred
// and duller), over a dimmed diffuse light, and the sun's highlight in the metal's color
const bool polished = look == UgcModel::eLook::METAL;
const auto reflected = glm::reflect(-toCamera, normal);
const float up = polished ? glm::smoothstep(-0.15f, 0.5f, reflected.y) : 0.5f + 0.5f * reflected.y;
const float environment = glm::mix(0.06f, polished ? 1.1f : 0.75f, up);
const float spot = direct > 0.0f ? options.sunStrength / 3.14159265f * sun *
std::pow(std::max(0.0f, glm::dot(normal, halfway)), polished ? 180.0f : 30.0f) * (polished ? 4.0f : 1.2f) : 0.0f;
shaded = linear * (lighting * 0.35f + environment + spot) * exposure;
} else if (look == UgcModel::eLook::GLOW) {
// LEGO-Emissive: lerp(lit, vertex color, vertex alpha * the material's emissive red)
shaded = glm::mix(shaded, linear, std::clamp(options.glowEmissive, 0.0f, 1.0f));
}
return glm::vec4(shaded, std::clamp(base.a, 0.0f, 1.0f));
};
// Opaque first, with the depth buffer