fix(ugc): transparent bricks drawn see-through in mixed models; dUgcServer in folders by role

- The client puts a shape in its sorted, blended pass only when its
  NiMaterialProperty alpha is under 0.99999 (ShaderCommon::GetAlphaFlags
  0x0109f5a0; the NiAlphaProperty blend flag isn't read); at 1.0 it's drawn
  solid with blending off. Transparent (and transparent glitter) shapes now
  get a material with alpha 0.9999, as the S01_Alpha shapes of the game's own
  brick models (res/BrickModels/ndmade) do; opaque shapes keep 1.0. Models
  with a transparent brick change; the others are byte for byte the same.
- dUgcServer's files move into Bricks/, Model/, Render/, Formats/ and
  Processing/ (the CMakeLists says what each holds); includes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 19:51:13 -05:00
parent 2277cbd7e2
commit 7c32779e89
32 changed files with 70 additions and 25 deletions

View File

@@ -1,25 +1,34 @@
# The UGC server (docs/UgcServer.md), in folders by role:
# Bricks/ the client's brick data: primitives, Materials.xml, LU Toolbox's palette, the CDClient's module data
# Model/ player models built from LXFML, car and rocket module combinations, glitter
# Render/ the software rasterizer, ambient occlusion, hidden-face removal, icon parameters and pose
# Formats/ the files written for the client and the dashboard (NIF, DDS, PNG, sd0)
# Processing/ the job queue and workers, the CPU budget, stored files
# UgcServer.cpp the server: settings, the master link, HTTP routes
# The pure part of the UGC server (brick data, meshes, rendering, file formats, storage), shared with the tests
set(DUGC_SOURCES
"UgcBricks.cpp"
"UgcFormats.cpp"
"UgcGlitter.cpp"
"UgcHsr.cpp"
"UgcIconParams.cpp"
"UgcIconPose.cpp"
"UgcJobs.cpp"
"UgcModel.cpp"
"UgcModular.cpp"
"UgcPalette.cpp"
"UgcRender.cpp"
"UgcStorage.cpp"
"UgcThrottle.cpp"
"Bricks/UgcBricks.cpp"
"Bricks/UgcPalette.cpp"
"Formats/UgcFormats.cpp"
"Model/UgcGlitter.cpp"
"Model/UgcModel.cpp"
"Model/UgcModular.cpp"
"Processing/UgcJobs.cpp"
"Processing/UgcStorage.cpp"
"Processing/UgcThrottle.cpp"
"Render/UgcHsr.cpp"
"Render/UgcIconParams.cpp"
"Render/UgcIconPose.cpp"
"Render/UgcRender.cpp"
)
add_library(dUgc STATIC ${DUGC_SOURCES})
target_include_directories(dUgc PUBLIC "." "${PROJECT_SOURCE_DIR}/thirdparty/MD5" "${PROJECT_SOURCE_DIR}/thirdparty/nlohmann")
target_include_directories(dUgc PUBLIC "." "Bricks" "Formats" "Model" "Processing" "Render"
"${PROJECT_SOURCE_DIR}/thirdparty/MD5" "${PROJECT_SOURCE_DIR}/thirdparty/nlohmann")
target_link_libraries(dUgc PUBLIC dCommon glm::glm tinyxml2 MD5)
add_executable(UgcServer "UgcServer.cpp" "UgcProcessor.cpp" "UgcCdClient.cpp")
add_executable(UgcServer "UgcServer.cpp" "Processing/UgcProcessor.cpp" "Bricks/UgcCdClient.cpp")
target_include_directories(UgcServer PRIVATE
"${PROJECT_SOURCE_DIR}/dCommon"
@@ -42,5 +51,5 @@ target_link_libraries(UgcServer ${COMMON_LIBRARIES} dWeb dServer dUgc)
# Rendering is far too slow unoptimized (minutes a model instead of seconds), so it is optimized in every build type
if(NOT MSVC)
set_source_files_properties("UgcRender.cpp" "UgcHsr.cpp" "UgcModel.cpp" "UgcPalette.cpp" "UgcGlitter.cpp" PROPERTIES COMPILE_OPTIONS "-O2")
set_source_files_properties("Render/UgcRender.cpp" "Render/UgcHsr.cpp" "Model/UgcModel.cpp" "Bricks/UgcPalette.cpp" "Model/UgcGlitter.cpp" PROPERTIES COMPILE_OPTIONS "-O2")
endif()

View File

@@ -228,8 +228,8 @@ namespace {
m_VertexColor = nif.Add("NiVertexColorProperty", std::move(vertexColor.Data()));
}
// A white NiMaterialProperty with this emissive color (grey)
static std::string Material(float emissive) {
// A white NiMaterialProperty with this emissive color (grey) and alpha
static std::string Material(float emissive, float alpha = 1.0f) {
Writer material;
WriteNet(material, -1);
for (int i = 0; i < 3; i++) material.Float(1.0f); // ambient
@@ -237,7 +237,7 @@ namespace {
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
material.Float(alpha);
return std::move(material.Data());
}
@@ -383,8 +383,15 @@ namespace {
specular.U16(0); // off
m_Specular = m_Nif.Add("NiSpecularProperty", std::move(specular.Data()));
}
(void)transparent;
// The client draws a shape in its sorted, blended pass only when the material's alpha is under 0.99999
// (ShaderCommon::GetAlphaFlags 0x0109f5a0; the NiAlphaProperty's blend flag isn't read): at 1.0 a
// 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 (m_MaterialAlpha < 0) m_MaterialAlpha = m_Nif.Add("NiMaterialProperty", Material(0.0f, 0.9999f));
material = m_MaterialAlpha;
}
if (emissive > 0.0f) {
auto [it, added] = m_Emissive.try_emplace(emissive, -1);
if (added) it->second = m_Nif.Add("NiMaterialProperty", Material(emissive));
@@ -409,6 +416,7 @@ namespace {
private:
NifBuilder& m_Nif;
int32_t m_Material{ -1 };
int32_t m_MaterialAlpha{ -1 }; // transparent shapes' (alpha 0.9999)
int32_t m_VertexColor{ -1 };
int32_t m_Alpha{ -1 };
int32_t m_Specular{ -1 };

View File

@@ -1097,8 +1097,8 @@ TEST(UgcShaders, OffIsByteIdenticalToBefore) {
// 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(nif), "7a7176731afd837da83450d260ed6832");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz"))), "19d8a2f748015e1f9d680a60fb7565d5");
EXPECT_EQ(UgcFormats::Md5Hex(outcome.files.at("icon.png")), "032ff7df236a636a4c609071d9b46181");
#endif
}
@@ -1624,10 +1624,10 @@ TEST(UgcHsr, OffIsByteIdenticalToBefore) {
const auto other = UgcJobs::ProcessModel(LXFML5, library, settings, 99);
ASSERT_TRUE(other.ok) << other.error;
#if defined(__linux__) && defined(__x86_64__) && defined(__GNUC__) && !defined(__clang__)
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.nif.gz"))), "f91b88e46a92e9472710854902b25c9a");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz"))), "58a779933695da04bf9b3459c8369528");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.nif.gz"))), "a8f6b7022a7c087d1659d67a16e9791f");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz"))), "2da0810cbd2470732e4732a8128da2ab");
EXPECT_EQ(UgcFormats::Md5Hex(outcome.files.at("icon.png")), "032ff7df236a636a4c609071d9b46181");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(other.files.at("model.nif.gz"))), "7eddc020a3e4bf22ed1df0183b042ced");
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(other.files.at("model.nif.gz"))), "289088a76248d35203cd961f2772ae21");
#endif
}
@@ -1764,3 +1764,31 @@ TEST(UgcThrottle, CancelStopsJobsAtTheirNextCheckpoint) {
UgcThrottle::Cancel(false);
EXPECT_NO_THROW(UgcThrottle::Checkpoint());
}
// Transparent shapes get a material alpha of 0.9999 as the game's own brick models' S01_Alpha shapes: the client only
// blends a shape whose material alpha is under 0.99999 (ShaderCommon::GetAlphaFlags); opaque shapes keep 1.0
TEST(UgcFormats, TransparentShapesHaveTheGamesAlphaMaterial) {
const auto mesh = Quad({ 0.2f, 0.4f, 0.8f, 0.6f });
const auto alphaOf = [](const std::string& nif) {
std::vector<float> alphas;
// Every NiMaterialProperty block ends with glossiness 4.0 and the alpha: find glossiness then read the alpha
for (size_t i = 0; i + 8 <= nif.size(); i++) {
float gloss = 0.0f;
std::memcpy(&gloss, nif.data() + i, 4);
if (gloss != 4.0f) continue;
float alpha = 0.0f;
std::memcpy(&alpha, nif.data() + i + 4, 4);
if (alpha > 0.9f && alpha <= 1.0f) alphas.push_back(alpha);
}
return alphas;
};
const auto mixed = UgcFormats::WriteLodNif("SceneNode_Model", {
{ "S01_Opaque_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } },
{ "S01_Alpha_Model", true, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } } });
const auto alphas = alphaOf(mixed);
EXPECT_NE(std::find(alphas.begin(), alphas.end(), 1.0f), alphas.end());
EXPECT_NE(std::find(alphas.begin(), alphas.end(), 0.9999f), alphas.end());
const auto opaque = UgcFormats::WriteLodNif("SceneNode_Model", { { "S01_Opaque_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } } });
const auto opaqueAlphas = alphaOf(opaque);
EXPECT_EQ(std::find(opaqueAlphas.begin(), opaqueAlphas.end(), 0.9999f), opaqueAlphas.end());
}