From d6dd5737419f83f7117c781ca198b3ecffed3719 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Mon, 28 Sep 2026 13:53:44 -0500 Subject: [PATCH] Revert "feat(dashboard): one table of the game's shader techniques for the 3D views" This reverts commit ee9135437f32f8b5cb63a1e90b23366e8c0f443c. --- dCommon/NifFile.cpp | 235 ++++++--------------- dCommon/NifFile.h | 62 +----- dDashboardServer/routes/Scenery.cpp | 72 ++----- dDashboardServer/static/js/scenery-core.js | 126 +++-------- tests/dWebTests/CMakeLists.txt | 4 +- tests/dWebTests/NifFileTests.cpp | 52 ----- tests/dWebTests/scenery-core.test.mjs | 87 +------- 7 files changed, 123 insertions(+), 515 deletions(-) diff --git a/dCommon/NifFile.cpp b/dCommon/NifFile.cpp index 131408d85..dace685af 100644 --- a/dCommon/NifFile.cpp +++ b/dCommon/NifFile.cpp @@ -719,182 +719,79 @@ namespace NifFile { return tagShader && *tagShader >= 3 && *tagShader <= 0x6C ? *tagShader : LEGO_SHADER; } - namespace { - using F = eShaderFamily; - constexpr auto OPACITY = eTextureAlpha::OPACITY, DECAL = eTextureAlpha::DECAL, IGNORED = eTextureAlpha::IGNORED; - - struct TechniqueRow { - int32_t shader; - ShaderTechnique technique; - }; - - /** - * Every mapShaders gameValue, by the technique its shader class sets up (ShaderManager's factory table at - * 0x01889608, indexed by gameValue; the class's technique setup names it) and the client's res/shaders/*.fx. - * Checked in the client: 33 and 82 Technique_Basic_NoLighting_VertColor_NoTexture, 35 and 84 - * Technique_Basic_NoLighting_VertColor, 37 Technique_Basic_Lighting_VertColor_NoTexture, 38 and 94 - * Technique_Basic_Lighting_VertColor (94 "Basic": the vtable slot at +0x90 of the class made at 0x0045f240 names - * it), 70 Technique_AlphaAsAlpha_UVScrolling_SimpleV_NoLighting_AlphaAnim, 105 - * Technique_TwoLayersBlended_NoLighting_VertColor_UVScrolling. The rest follow their mapShaders labels ("NL" no - * lighting, "NT" no texture, "VC" vertex colors, "AnimUV"/"ScrollingUV" the texture transform, "OneSidedAlpha" - * AlphaAsAlpha culled) and the technique of that name in res/shaders. - */ - constexpr TechniqueRow TECHNIQUES[] = { - { -1, { F::FIXED_FUNCTION, 0, OPACITY, 0 } }, - // TerrainDiffuse.fx's mesh techniques: the texture times the vertex colors and the light, alpha only the fade - { 2, { F::TERRAIN, 0, OPACITY, NO_BLEND } }, // Terrain Mesh - { 3, { F::TERRAIN, 0, IGNORED, RIM_LIGHT | NO_BLEND } }, // Terrain Mesh Rim Light - { 97, { F::TERRAIN, 0, OPACITY, DIFFUSE_ONLY | NO_BLEND } }, // Terrain Diffuse Map Only + eTextureAlpha TextureAlphaFor(int32_t shader) { + switch (shader) { // LEGOPPLighting: textured alone the texture's alpha is forced to 1; with vertex colors it only lays the // texture over them (lerp by its alpha) and the vertex alpha is what shows through - { 4, { F::LEGO, 0, DECAL, 0 } }, // LEGO (No LOD) - { 5, { F::LEGO, 0, DECAL, 0 } }, // LEGO - { 12, { F::LEGO, 0, DECAL, 0 } }, // LEGO-Reveal (the reveal mask left out) - { 14, { F::LEGO, 0, OPACITY, NON_DECAL } }, // LEGO Masked NonDecal (the specular mask left out) - { 19, { F::LEGO, 0, DECAL, 0 } }, // Powerups (their own effect, drawn as LEGO) - { 20, { F::LEGO, 0, DECAL, 0 } }, // Orb (Powerups.fx, drawn as LEGO) - { 22, { F::LEGO, EMISSIVE, OPACITY, SUPER_EMISSIVE } }, // LEGO-SuperEmissive - { 25, { F::LEGO, 0, DECAL, 0 } }, // LEGO_FrontEnd - { 26, { F::LEGO, 0, OPACITY, NON_DECAL } }, // LEGO_FaceCreate - { 27, { F::LEGO, 0, DECAL, GLOW } }, // LEGO-Glow - { 28, { F::LEGO, 0, DECAL, GRAYSCALE } }, // LEGO-Grayscale - { 29, { F::LEGO, 0, DECAL, GLOW | IGNORE_VERTEX_ALPHA } }, // LEGO-Glow-IgnoreVertAlpha - { 30, { F::LEGO, 0, DECAL, UV_ANIM } }, // LEGO-AnimUV + case 4: case 5: case 12: case 25: case 27: case 28: case 29: case 30: case 50: case 72: case 88: + // Darkling: the same lay-over; alpha from lighting or the fade + case 75: case 76: case 77: case 102: case 103: case 104: + return eTextureAlpha::DECAL; // LEGOPPLighting_Item: texture alpha forced to 1, multiplied by the vertex colors - { 31, { F::LEGO, 0, IGNORED, NON_DECAL } }, // LEGO-Item - { 48, { F::LEGO, 0, IGNORED, NON_DECAL | GLOW } }, // LEGO-ItemGlow - { 50, { F::LEGO, 0, DECAL, 0 } }, // LEGO-FadeUp (as when faded in) - { 53, { F::LEGO, EMISSIVE, OPACITY, 0 } }, // LEGO-Emissive - { 72, { F::LEGO, 0, DECAL, SHINY_GLINT } }, // ShinyGlint - { 88, { F::LEGO, 0, DECAL, NO_AMBIENT } }, // LEGO NoAmbient - { 92, { F::LEGO, 0, DECAL, 0 } }, // Pet Taming LEGO In Cloud - // LEGOPPLighting's NL pixel shaders: the vertex color or texture as it is - { 52, { F::BASIC, UNLIT, OPACITY, ANIM_ALPHA } }, // LEGO-No Light - // Darkling: the same lay-over; the dark texture on the second UV set through a window of vertex alphas - { 75, { F::DARKLING, 0, DECAL, 0 } }, // Darkling - { 76, { F::DARKLING, 0, DECAL, SPECULAR } }, // Darkling /w Specular - { 77, { F::DARKLING, 0, DECAL, NON_DECAL } }, // Darkling Structure - { 102, { F::DARKLING, 0, DECAL, SHINY_GLINT } }, // Darking Shiny Glint - { 103, { F::DARKLING, 0, DECAL, SPECULAR | SHINY_GLINT } }, // Darkling /w Specular Shiny Glint - { 104, { F::DARKLING, 0, DECAL, NON_DECAL | SHINY_GLINT } }, // Darkling Structure Shiny Glint - // AlphaAsAlpha: texture times the (lit) vertex color, both sides - { 7, { F::BASIC, 0, OPACITY, DOUBLE_SIDED } }, // VertColor_Alpha - { 8, { F::BASIC, UNLIT, OPACITY, DOUBLE_SIDED | ANIM_ALPHA } }, // VertColor_NoLighting_Alpha - { 9, { F::BASIC, 0, OPACITY, DOUBLE_SIDED } }, // VertColor_Alpha_Fade - { 10, { F::BASIC, UNLIT, OPACITY, DOUBLE_SIDED | ANIM_ALPHA | BLEND } }, // VertColorTex_NoLight_AlphaBlend - { 54, { F::BASIC, UNLIT, OPACITY, DOUBLE_SIDED | ANIM_ALPHA | ALPHA_TEST } }, // VertColorTex_NoLight_AlphaTest - { 13, { F::BASIC, 0, OPACITY, UV_ANIM } }, // ScrollingUV - { 70, { F::BASIC, UNLIT, OPACITY, UV_ANIM | ANIM_ALPHA } }, // ScrollingUV_NoLight_AnimAlpha - { 73, { F::BASIC, UNLIT, OPACITY, UV_ANIM | ANIM_ALPHA } }, // ScrollingUV_NoLight_AimAlpha_Post - { 81, { F::BASIC, UNLIT, OPACITY, UV_ANIM | ANIM_ALPHA | NO_FOG } }, // ScrollingUV NL AnimAlpha NoFog - // OneSidedAlpha: the same, culled - { 55, { F::BASIC, 0, OPACITY, 0 } }, // OneSidedAlpha VC - { 56, { F::BASIC, UNLIT | NO_VERTEX_COLORS, OPACITY, 0 } }, // OneSidedAlpha NL - { 57, { F::BASIC, UNLIT, OPACITY, ANIM_ALPHA } }, // OneSidedAlpha NL VC - { 58, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, ANIM_ALPHA } }, // OneSidedAlpha NL VC NT - { 59, { F::BASIC, 0, OPACITY, UV_ANIM } }, // OneSidedAlpha AnimUV V Skinned - { 60, { F::BASIC, 0, OPACITY, 0 } }, // OneSidedAlpha VC Skinned - { 61, { F::BASIC, UNLIT | NO_VERTEX_COLORS, OPACITY, 0 } }, // OneSidedAlpha NL Skinned - { 62, { F::BASIC, UNLIT, OPACITY, ANIM_ALPHA } }, // OneSidedAlpha NL VC Skinned - { 63, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, ANIM_ALPHA } }, // OneSidedAlpha NL VC NT Skinned - { 64, { F::BASIC, 0, OPACITY, UV_ANIM } }, // OneSidedAlpha AnimUV V - { 68, { F::BASIC, UNLIT, OPACITY, ANIM_ALPHA } }, // OneSidedAlpha NL AnimAlpha - // BasicShaders - { 11, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, ANIM_ALPHA } }, // VertColor_NoLight_NoTex_AnimAlpha - { 15, { F::BASIC, UNLIT, OPACITY, 0 } }, // VC_NoLighting_2D - { 16, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, 0 } }, // VC_NL_NoTex_2D - { 17, { F::BASIC, UNLIT, OPACITY, 0 } }, // TV Screen (its static and flicker left out) - { 18, { F::BASIC, UNLIT, OPACITY, 0 } }, // Head Icon - { 23, { F::BASIC, UNLIT, OPACITY, 0 } }, // Over Everything (Unlit) - { 24, { F::BASIC, UNLIT, OPACITY, NO_FOG | BLEND } }, // Fogless GrayBubble - { 32, { F::BASIC, UNLIT | NO_VERTEX_COLORS | MATERIAL_COLOR, OPACITY, 0 } }, // Basic NL Material - { 33, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, ANIM_ALPHA } }, // Basic NL VC NT - { 34, { F::BASIC, UNLIT | NO_VERTEX_COLORS, OPACITY, 0 } }, // Basic NL - { 35, { F::BASIC, UNLIT, OPACITY, 0 } }, // Basic NL VC - { 36, { F::BASIC, UNLIT | NO_VERTEX_COLORS, OPACITY, UV_ANIM } }, // Basic NL UVAnim - { 37, { F::BASIC, NO_TEXTURE, OPACITY, 0 } }, // Basic VC NT - { 38, { F::BASIC, 0, OPACITY, 0 } }, // Basic VC - { 39, { F::BASIC, 0, OPACITY, UV_ANIM } }, // Basic VC UVAnim - { 49, { F::BASIC, 0, OPACITY, 0 } }, // Experimental Stub - { 65, { F::BASIC, 0, OPACITY, BASIC_EMISSIVE } }, // VC_Texture_Emissive - { 80, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, 0 } }, // Basic NL NT - { 82, { F::BASIC, UNLIT | NO_TEXTURE, OPACITY, NO_BLEND | NO_FOG } }, // Opaque NL VC NT NoFog - { 83, { F::BASIC, UNLIT | NO_VERTEX_COLORS, OPACITY, NO_BLEND | NO_FOG } }, // Opaque NL NoFog - { 84, { F::BASIC, UNLIT, OPACITY, NO_BLEND | NO_FOG } }, // Opaque NL VC NoFog - { 85, { F::BASIC, NO_TEXTURE, OPACITY, NO_BLEND | NO_FOG } }, // Opaque VC NT NoFog - { 86, { F::BASIC, 0, OPACITY, NO_BLEND | NO_FOG } }, // Opaque VC NoFog - { 87, { F::BASIC, UNLIT, OPACITY, ADDITIVE } }, // Additive NoLight VertColor - { 91, { F::BASIC, UNLIT, OPACITY, BLEND } }, // Pet Taming Imagination Cloud - { 94, { F::BASIC, 0, OPACITY, 0 } }, // Basic - { 108, { F::BASIC, UNLIT | NO_VERTEX_COLORS | MATERIAL_COLOR, OPACITY, 0 } }, // Over Everything Material Unlit - // Two layers (TwoLayersAdded_PS in BasicShaders.fx; the client ships no Technique_TwoLayersBlended_* shader, - // so the blended ones follow the meshes' data: the dark texture under the base one by the vertex alpha, as - // Avant Gardens' snow caps and grass fade into rock by it) - { 93, { F::BASIC, UNLIT | TWO_LAYERS_ADDED, OPACITY, UV_ANIM } }, // Two Textures Added NL VC AnimUV - { 105, { F::BASIC, UNLIT | TWO_LAYERS_BLENDED, OPACITY, UV_ANIM } }, // Two Layers Blended NL VC AnimUV - { 106, { F::BASIC, TWO_LAYERS_BLENDED, OPACITY, UV_ANIM } }, // Two Layers Blended VC AnimUV - { 107, { F::BASIC, TWO_LAYERS_ADDED, OPACITY, UV_ANIM } }, // Two Layers Added VC AnimUV - // Metallic.fx: both load their reflection cubes themselves (textures/metal) - { 98, { F::METAL, REFLECTIVE, OPACITY, 0 } }, // Polished Metal - { 99, { F::METAL, REFLECTIVE | BRUSHED, OPACITY, 0 } }, // Brushed Steel - { 100, { F::METAL, REFLECTIVE | BRUSHED, OPACITY, 0 } }, // Brushed Steel Item - { 6, { F::CLEAR_PLASTIC, 0, OPACITY, BLEND } }, // Clear Plastic - { 51, { F::BRICK_WATER, 0, OPACITY, 0 } }, // BrickWater - // Ocean.fx - { 69, { F::OCEAN, 0, OPACITY, UV_ANIM } }, // Distortion (Ocean) - { 89, { F::OCEAN, 0, OPACITY, UV_ANIM } }, // Distortion Directional (Ocean) - { 90, { F::OCEAN, UNLIT, OPACITY, UV_ANIM | OCEAN_FX } }, // Distortion FX (Ocean) - { 95, { F::OCEAN, 0, OPACITY, UV_ANIM | BLEND } }, // Distortion NoDepth (Ocean) (Alpha) - { 101, { F::OCEAN, UNLIT, OPACITY, UV_ANIM } }, // Distortion (Ocean) Unlit - { 78, { F::FLAT_SURF, 0, OPACITY, UV_ANIM } }, // Flat Surf - // Drawn by other passes, not in the world: footprints, post-processing, drop shadows, Technique_Undefined - { 21, { F::BASIC, 0, OPACITY, NOT_DRAWN } }, // Model Footprint - { 71, { F::BASIC, 0, OPACITY, NOT_DRAWN } }, // PostProcess Gray Bubble - { 74, { F::BASIC, 0, OPACITY, NOT_DRAWN } }, // Drop Shadow - { 79, { F::BASIC, 0, OPACITY, NOT_DRAWN } }, // Post Process Gray Bubble Interior Ghost - { 96, { F::BASIC, 0, OPACITY, NOT_DRAWN } }, // Undefined - }; - } - - const char* FamilyName(eShaderFamily family) { - switch (family) { - case eShaderFamily::FIXED_FUNCTION: return "fixed"; - case eShaderFamily::LEGO: return "lego"; - case eShaderFamily::BASIC: return "basic"; - case eShaderFamily::METAL: return "metal"; - case eShaderFamily::CLEAR_PLASTIC: return "clearPlastic"; - case eShaderFamily::OCEAN: return "ocean"; - case eShaderFamily::FLAT_SURF: return "flatSurf"; - case eShaderFamily::BRICK_WATER: return "brickWater"; - case eShaderFamily::DARKLING: return "darkling"; - case eShaderFamily::TERRAIN: return "terrain"; + case 31: case 48: + // TerrainMeshLighting_Rim: texture times vertex colors, alpha only the fade + case 3: + return eTextureAlpha::IGNORED; + default: + return eTextureAlpha::OPACITY; } - return "lego"; - } - - ShaderTechnique TechniqueFor(int32_t shader) { - for (const auto& row : TECHNIQUES) { - if (row.shader == shader) return row.technique; - } - return ShaderTechnique{}; // the LEGO shader, as the client falls back to - } - - std::string TechniquesJson(const std::vector& shaders) { - nlohmann::json out = nlohmann::json::object(); - for (const auto shader : shaders) { - const auto technique = TechniqueFor(shader); - const char* alpha = technique.textureAlpha == eTextureAlpha::DECAL ? "decal" : technique.textureAlpha == eTextureAlpha::IGNORED ? "ignored" : "opacity"; - out[std::to_string(shader)] = { {"family", FamilyName(technique.family)}, {"look", technique.look}, {"alpha", alpha}, {"flags", technique.flags} }; - } - return out.dump(); - } - - eTextureAlpha TextureAlphaFor(int32_t shader) { - return TechniqueFor(shader).textureAlpha; } uint16_t ShaderLookFor(int32_t shader) { - return TechniqueFor(shader).look; + // By the technique each shader class sets up (ShaderManager's factory table at 0x01889608, indexed by gameValue; + // the class's technique setup names it). Checked in the client: 33 and 82 Technique_Basic_NoLighting_VertColor_ + // NoTexture, 35 and 84 Technique_Basic_NoLighting_VertColor, 37 Technique_Basic_Lighting_VertColor_NoTexture, + // 38 and 94 Technique_Basic_Lighting_VertColor, 70 Technique_AlphaAsAlpha_UVScrolling_SimpleV_NoLighting_ + // AlphaAnim, 105 Technique_TwoLayersBlended_NoLighting_VertColor_UVScrolling. The rest follow their mapShaders + // labels ("NL" no lighting, "NT" no texture, "VC" vertex colors) + switch (shader) { + // Basic NL Material, Over Everything Material Unlit + case 32: case 108: + return UNLIT | NO_VERTEX_COLORS | MATERIAL_COLOR; + // Basic NL, Basic NL UVAnim, OneSidedAlpha NL (and skinned), Opaque NL NoFog + case 34: case 36: case 56: case 61: case 83: + return UNLIT | NO_VERTEX_COLORS; + // (94 "Basic" is Technique_Basic_Lighting_VertColor like "Basic VC": its shader's technique setup, the vtable + // slot at +0x90 of the class made at 0x0045f240, names that technique, so it's the usual look) + // VertColor_NoLight_NoTex_AnimAlpha, VC_NL_NoTex_2D, Basic NL VC NT, OneSidedAlpha NL VC NT (and skinned), + // Basic NL NT, Opaque NL VC NT NoFog + case 11: case 16: case 33: case 58: case 63: case 80: case 82: + return UNLIT | NO_TEXTURE; + // Basic VC NT, Opaque VC NT NoFog + case 37: case 85: + return NO_TEXTURE; + // Two Textures Added NL VC AnimUV (TwoLayersAdded_PS in BasicShaders.fx), Two Layers Added VC AnimUV + case 93: + return UNLIT | TWO_LAYERS_ADDED; + case 107: + return TWO_LAYERS_ADDED; + // Two Layers Blended NL VC AnimUV and Two Layers Blended VC AnimUV. The client names techniques for them + // (Technique_TwoLayersBlended_*) that no shader it ships has, so how the game draws them is a guess: the dark + // texture under the base one by the vertex alpha, as the meshes' data suggests (Avant Gardens' snow caps + // and grass fade into rock by it) + case 105: + return UNLIT | TWO_LAYERS_BLENDED; + case 106: + return TWO_LAYERS_BLENDED; + // VertColor_NoLighting_Alpha, VertColorTex_NoLight_AlphaBlend and _AlphaTest, VC_NoLighting_2D, Over + // Everything (Unlit), Basic NL VC, LEGO-No Light, OneSidedAlpha NL VC (and skinned), OneSidedAlpha NL + // AnimAlpha, the NoLight scrolling UVs, Opaque NL VC NoFog, Additive NoLight VertColor, Distortion (Ocean) + // Unlit + case 8: case 10: case 54: case 15: case 23: case 35: case 52: case 57: case 62: case 68: case 70: case 73: case 81: + case 84: case 87: case 101: + return UNLIT; + // Polished Metal (Technique_Lighting_PolishedMetal_VertColor in Metallic.fx) and Brushed Steel (its noise + // in object space); both load their reflection textures themselves + case 98: + return REFLECTIVE; + case 99: + return REFLECTIVE | BRUSHED; + // LEGO-Emissive + case 53: + return EMISSIVE; + default: + return 0; + } } std::optional Parse(std::string_view data, uint32_t lod, std::string& error) { diff --git a/dCommon/NifFile.h b/dCommon/NifFile.h index 98bab8e8f..220acb03c 100644 --- a/dCommon/NifFile.h +++ b/dCommon/NifFile.h @@ -78,7 +78,7 @@ namespace NifFile { // The shader (gameValue) a multishader part is drawn with, from its tag's gameValue: outside 3..108 the LEGO shader int32_t MultishaderPart(std::optional tagShader); - // What a texture's alpha does under a shader (mapShaders.gameValue, TechniqueFor); -1 is fixed function (opacity) + // What a texture's alpha does under a shader (mapShaders.gameValue); -1 is fixed function (opacity) eTextureAlpha TextureAlphaFor(int32_t shader); /** @@ -112,66 +112,6 @@ namespace NifFile { // eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function uint16_t ShaderLookFor(int32_t shader); - /** - * The client's shader families (res/shaders/*.fx), each drawn by one program in the dashboard's 3D views - * (static/js/game-shaders.js): FIXED_FUNCTION Gamebryo's own lighting with the material; LEGO LEGOPPLighting (hemisphere - * lit sun, fresnel rim, specular, a faint environment reflection); BASIC BasicShaders and AlphaAsAlpha (sun * N.L + - * ambient per vertex, or unlit, times vertex color and texture); METAL Metallic.fx; CLEAR_PLASTIC ClearPlastic.fx; - * OCEAN Ocean.fx's Distortion techniques (warped texture layers); FLAT_SURF Ocean.fx's Flat Surf; BRICK_WATER - * BrickWater.fx; DARKLING LEGOPPLighting's Darkling techniques (a second, dark texture on its own UV set); TERRAIN - * TerrainDiffuse.fx's mesh techniques. - */ - enum class eShaderFamily : uint8_t { FIXED_FUNCTION, LEGO, BASIC, METAL, CLEAR_PLASTIC, OCEAN, FLAT_SURF, BRICK_WATER, DARKLING, TERRAIN }; - - // Its name in the scenery manifest ("fixed", "lego", "basic", "metal", "clearPlastic", "ocean", "flatSurf", "brickWater", "darkling", "terrain") - const char* FamilyName(eShaderFamily family); - - // What a technique does besides its family and eShaderLook bits (static/js/scenery-core.js TECHNIQUE has the same bits) - enum eTechniqueFlag : uint32_t { - UV_ANIM = 1, // the base texture moves by the .nif's texture transform (g_textureMotion) - DOUBLE_SIDED = 2, // Cullmode none (AlphaAsAlpha) - BLEND = 4, // alpha blended without depth writes - ALPHA_TEST = 8, // cut out by the alpha - ADDITIVE = 16, // added onto what's behind - NO_AMBIENT = 32, // lit as if the ambient light were white - GLOW = 64, // LEGOPP_ApplyGlow: the glow color by the alpha - IGNORE_VERTEX_ALPHA = 128, // ... with the vertex alpha taken as 1 - SUPER_EMISSIVE = 256, // LEGO-Emissive going to ten times the color - GRAYSCALE = 512, // LEGOPP_ApplyGrayscale - SHINY_GLINT = 1024, // a highlight band moving up the object - SPECULAR = 2048, // Darkling with LEGO lighting - NON_DECAL = 4096, // the texture multiplies the vertex colors instead of being laid over them - OCEAN_FX = 8192, // Distortion FX: unlit, the alpha a window set by the material's emissive - RIM_LIGHT = 16384, // Terrain Mesh Rim Light - DIFFUSE_ONLY = 32768, // Terrain Diffuse Map Only: the texture doubled, lit - ANIM_ALPHA = 65536, // NiMaterialProperty's alpha (animated) multiplies the output alpha - BASIC_EMISSIVE = 131072, // Basic_Emissive_PS - NO_FOG = 262144, // the "NoFog" techniques - NOT_DRAWN = 524288, // nothing of it is drawn in the world (post-processing, footprints, drop shadows) - NO_BLEND = 1048576 // no alpha blending ("Opaque" techniques, terrain meshes) - }; - - struct ShaderTechnique { - eShaderFamily family{ eShaderFamily::LEGO }; - uint16_t look{}; // eShaderLook bits - eTextureAlpha textureAlpha{ eTextureAlpha::DECAL }; - uint32_t flags{}; // eTechniqueFlag bits - }; - - /** - * How the client draws with a shader (mapShaders.gameValue): the one table of the game's shaders for the views. - * Every mapShaders gameValue has a row; -1 is fixed function; any other value is the LEGO shader, which the client - * falls back to. - */ - ShaderTechnique TechniqueFor(int32_t shader); - - /** - * The techniques of these shaders (gameValues) as the scenery manifest's "techniques": a JSON object of gameValue -> - * {"family": FamilyName, "look": eShaderLook bits, "alpha": "opacity"|"decal"|"ignored", "flags": eTechniqueFlag - * bits}. - */ - std::string TechniquesJson(const std::vector& shaders); - // Where a node is in the model's space: p' = rotation * p + translation (row-major, scale folded in) struct NodeTransform { std::array rotation{ 1, 0, 0, 0, 1, 0, 0, 0, 1 }; diff --git a/dDashboardServer/routes/Scenery.cpp b/dDashboardServer/routes/Scenery.cpp index 68c822d5e..5308c3f5a 100644 --- a/dDashboardServer/routes/Scenery.cpp +++ b/dDashboardServer/routes/Scenery.cpp @@ -208,21 +208,29 @@ namespace { } /** - * How the client draws each model, for the viewers' game shaders (static/js/game-shaders.js): "shaders" gives each - * asset's shader (the first object drawing it wins; -1 fixed function or not an object), "shaderTags" a multishader - * part's tag -> shader, and "techniques" every shader's technique (NifFile::TechniquesJson, the one table of them). + * How the client's shaders use each model's texture alpha, for the viewer: "shaders" gives each asset's shader + * (the first object drawing it wins; -1 fixed function or not an object), "shaderTags" a multishader part's tag -> + * shader, and "textureAlpha" the shaders whose texture alpha isn't opacity (NifFile::TextureAlphaFor). */ void AddShaders(nlohmann::json& manifest, const std::vector& assetShaders) { RenderInfos(); // reads g_ShaderValues manifest["shaders"] = assetShaders; - nlohmann::json tags = nlohmann::json::object(); - std::set values{ -1, NifFile::LEGO_SHADER }; + nlohmann::json tags = nlohmann::json::object(), modes = nlohmann::json::object(); + for (const auto& [id, value] : g_ShaderValues) tags[std::to_string(id)] = value; + auto mode = [&modes](int32_t value) { + const auto alpha = NifFile::TextureAlphaFor(value); + if (alpha != NifFile::eTextureAlpha::OPACITY) modes[std::to_string(value)] = alpha == NifFile::eTextureAlpha::DECAL ? "decal" : "ignored"; + }; + for (const auto& [id, value] : g_ShaderValues) mode(value); + mode(NifFile::LEGO_SHADER); + // What each shader leaves out of the lit look (NifFile::eShaderLook bits), for the ones that do + nlohmann::json looks = nlohmann::json::object(); for (const auto& [id, value] : g_ShaderValues) { - tags[std::to_string(id)] = value; - values.insert(value); + if (const auto look = NifFile::ShaderLookFor(value)) looks[std::to_string(value)] = look; } manifest["shaderTags"] = std::move(tags); - manifest["techniques"] = nlohmann::json::parse(NifFile::TechniquesJson({ values.begin(), values.end() })); + manifest["textureAlpha"] = std::move(modes); + manifest["shaderLooks"] = std::move(looks); manifest["multishader"] = NifFile::MULTISHADER; manifest["defaultShader"] = NifFile::LEGO_SHADER; } @@ -237,16 +245,15 @@ namespace { * Bump when NifFile's output changes: converted models kept on disk are made again, and the manifests' "format" * goes into the viewers' model and texture URLs so browsers don't keep drawing the old ones (they're cached for * a week). 2: meshes carry their multishader tag; conversions without it drew glom parts with the LEGO shader. - * 3: dark textures and the UV set each texture names. 4: the game's shaders draw the models (manifest - * "techniques"), vertex colors go to them as stored. + * 3: dark textures and the UV set each texture names. */ - constexpr uint32_t FORMAT_VERSION = 4; + constexpr uint32_t FORMAT_VERSION = 3; // A zone's lighting (WorldScene::Lighting) for the viewers' shaders nlohmann::json LightingJson(const WorldScene::Lighting& lighting) { const auto triple = [](const std::array& value) { return nlohmann::json{ Round(value[0], 1000.0), Round(value[1], 1000.0), Round(value[2], 1000.0) }; }; return { - {"ambient", triple(lighting.ambient)}, {"light", triple(lighting.light)}, {"lightVec", triple(lighting.lightVec)}, {"specular", triple(lighting.specular)}, + {"ambient", triple(lighting.ambient)}, {"light", triple(lighting.light)}, {"lightVec", triple(lighting.lightVec)}, {"upperHemi", triple(lighting.upperHemi)}, {"fogColor", triple(lighting.fogColor)}, {"fogNear", Round(lighting.fogNear, 10.0)}, {"fogFar", Round(lighting.fogFar, 10.0)} }; @@ -438,8 +445,6 @@ namespace { } return nlohmann::json{ {"zone", zoneId}, {"sky", -1}, {"assets", assets}, {"distance", FLAIR_DISTANCE}, {"colorScale", 1.0 / 63.0}, {"lighting", scenery.lighting}, {"format", FORMAT_VERSION}, - // Flair.fx for all of them: (0.85 * sun + ambient) * the flair's tint, whatever their facing - {"technique", { {"family", "flair"}, {"look", 0}, {"alpha", "opacity"}, {"flags", 0} }}, {"objects", { {"asset", assetOf}, {"pos", positions}, {"rot", rotations}, {"scale", scales}, {"color", colors} }} }.dump(); } @@ -951,46 +956,7 @@ namespace Scenery { }); } - /** - * The environment textures the client's shaders load themselves, by the name the viewers ask for them: the default - * reflection cube (LEGOPPLighting, ClearPlastic) and Metallic.fx's cubes and noise. - */ - static const std::map& EnvironmentTextures() { - static const std::map textures{ - { "reflection", "textures/env/default_reflection.dds" }, - { "polished", "textures/metal/metal_reflection_polished.dds" }, - { "brushed", "textures/metal/metal_reflection_brushed.dds" }, - { "brushedNoise", "textures/metal/metal_reflection_brushed_noise.dds" } - }; - return textures; - } - void RegisterRoutes() { - Route(eHTTPMethod::GET, "/api/scenery/env/:name", 0, - "An environment texture the client's shaders load themselves, as a DDS file: reflection (the default reflection cube), polished, brushed (the metal cubes) or brushedNoise", - [](HTTPReply& reply, const HTTPContext& context) { - const std::string name(PathSegment(context.path, 3)); - const auto& textures = EnvironmentTextures(); - const auto it = textures.find(name); - if (it == textures.end()) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "No such environment texture"); - // Read once: a few megabytes the views ask for with every zone - static std::mutex mutex; - static std::map> cache; - std::shared_ptr bytes; - { - std::lock_guard lock(mutex); - if (const auto cached = cache.find(name); cached != cache.end()) bytes = cached->second; - } - if (!bytes) { - auto read = ClientAssets::ReadResFile(it->second); - if (!read) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "The client has no such texture"); - bytes = std::make_shared(std::move(*read)); - std::lock_guard lock(mutex); - cache.try_emplace(name, bytes); - } - Binary(reply, *bytes); - }); - Route(eHTTPMethod::GET, "/api/scenery/:zone/mesh/:asset", 0, "Model `asset` of a zone's scenery (see the scenery routes of properties and /world3d), converted from the client's .nif. Query: ?lod=0 (most detailed) to 3", [](HTTPReply& reply, const HTTPContext& context) { diff --git a/dDashboardServer/static/js/scenery-core.js b/dDashboardServer/static/js/scenery-core.js index a6c73e0ce..de4a2bf2c 100644 --- a/dDashboardServer/static/js/scenery-core.js +++ b/dDashboardServer/static/js/scenery-core.js @@ -40,6 +40,18 @@ export function parseModel(buffer) { return { header, meshes }; } +/** + * What a mesh's texture alpha does in the game, from the manifest's shader data (Scenery.cpp AddShaders): the + * client's shader decides, not the .nif. 'opacity' see-through where the alpha is; 'decal' the texture is laid over + * the vertex colors by its alpha (LEGO shaders); 'ignored' the alpha does nothing. A multishader model's parts name + * their shader in their node names (mesh.shaderTag); a tag the client can't use falls back to the LEGO shader. + */ +export function textureAlphaMode(manifest, asset, mesh) { + if (!manifest || !manifest.textureAlpha) return 'opacity'; + const shader = shaderOf(manifest, asset, mesh); + return shader === null ? 'opacity' : manifest.textureAlpha[shader] || 'opacity'; +} + /** * The shader (mapShaders.gameValue) the game draws a mesh of a model with, or null when the manifest doesn't say * (-1 is fixed function). A multishader model's parts name theirs in their node names (mesh.shaderTag, a mapShaders @@ -100,57 +112,22 @@ float glitterFleck(vec2 uv) { return { update(seconds) { uniforms.glitterTime.value = seconds; } }; } -// NifFile::eTechniqueFlag bits (what a technique does besides its family and eShaderLook bits) -export const TECHNIQUE = { - UV_ANIM: 1, DOUBLE_SIDED: 2, BLEND: 4, ALPHA_TEST: 8, ADDITIVE: 16, NO_AMBIENT: 32, GLOW: 64, IGNORE_VERTEX_ALPHA: 128, - SUPER_EMISSIVE: 256, GRAYSCALE: 512, SHINY_GLINT: 1024, SPECULAR: 2048, NON_DECAL: 4096, OCEAN_FX: 8192, RIM_LIGHT: 16384, - DIFFUSE_ONLY: 32768, ANIM_ALPHA: 65536, BASIC_EMISSIVE: 131072, NO_FOG: 262144, NOT_DRAWN: 524288, NO_BLEND: 1048576 -}; - -// A technique the manifest doesn't name: the LEGO shader's, as the client falls back to it -const LEGO_TECHNIQUE = { family: 'lego', look: 0, alpha: 'decal', flags: 0 }; -const FIXED_TECHNIQUE = { family: 'fixed', look: 0, alpha: 'opacity', flags: 0 }; - /** - * The technique (NifFile::TechniqueFor, the manifest's "techniques" by gameValue) a mesh of a model is drawn with: - * {shader, family, look, alpha, flags}. A manifest-wide "technique" (the flairs') is every mesh's; without a shader the - * mesh is fixed function; a shader the table lacks is drawn as LEGO. - */ -export function techniqueOf(manifest, asset, mesh) { - if (manifest && manifest.technique) return { shader: null, ...manifest.technique }; - const shader = shaderOf(manifest, asset, mesh); - if (shader === null || shader < 0) return { shader, ...FIXED_TECHNIQUE }; - const known = manifest.techniques && manifest.techniques[shader]; - return { shader, ...(known || LEGO_TECHNIQUE) }; -} - -/** - * What a texture's alpha does in the game (the technique's, NifFile::eTextureAlpha): 'opacity' see-through where the - * alpha is; 'decal' the texture is laid over the vertex colors by its alpha (LEGO shaders); 'ignored' it does nothing. - */ -export function textureAlphaMode(manifest, asset, mesh) { - return techniqueOf(manifest, asset, mesh).alpha || 'opacity'; -} - -/** - * How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {family (game-shaders.js), - * lit, texture, vertexColors, material, layers, metal, emissive, textureAlpha, uvAnim, flags, blend, doubleSided, - * hidden} — whether the scene's sun and ambient light it, its texture and vertex colors are used, whether its - * NiMaterialProperty colors are (only fixed function and the "Material" shaders use them), how a two layer shader puts - * its dark texture with the base one ('blended', 'added' or null), whether it is metal ('polished', 'brushed' or null), - * whether it glows (LEGO-Emissive: the vertex alpha is then no opacity), whether its texture moves as the .nif's - * texture transform says, its blending ('nif': as NiAlphaProperty says; 'blend': see-through without depth writes; - * 'test': cut out; 'additive'; 'opaque') and whether the game draws it in the world at all (hidden: post-processing - * and shadow shaders). Null without lighting in the manifest (older servers), for the viewer's own lights. + * How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {lit, texture, + * vertexColors, material, layers} — whether the scene's sun and ambient light it, its texture and vertex colors are + * used, whether its NiMaterialProperty colors are (only fixed function and the "Material" shaders use them), and how + * a two layer shader puts its dark texture with the base one ('blended', 'added' or null), whether it is metal + * ('polished', 'brushed' or null: an environment reflection tinted by the vertex color) and whether it glows + * (LEGO-Emissive: the lit color goes to the vertex color by the vertex alpha times the material's emissive red, so + * the vertex alpha is no opacity). Null without + * lighting in the manifest (older servers), for the viewer's own lights. */ export function gameLook(manifest, asset, mesh) { if (!manifest || !manifest.lighting) return null; - const technique = techniqueOf(manifest, asset, mesh); - const fixedFunction = technique.family === 'fixed'; - const bits = technique.look || 0; - const flags = technique.flags || 0; + const shader = shaderOf(manifest, asset, mesh); + const fixedFunction = shader === null || shader < 0; + const bits = fixedFunction || !manifest.shaderLooks ? 0 : manifest.shaderLooks[shader] || 0; return { - family: technique.family, lit: !(bits & SHADER_LOOK.UNLIT), texture: !(bits & SHADER_LOOK.NO_TEXTURE), // Fixed function reads them as NiVertexColorProperty says; the shaders always do, unless they have none @@ -158,34 +135,10 @@ export function gameLook(manifest, asset, mesh) { material: fixedFunction || !!(bits & SHADER_LOOK.MATERIAL_COLOR), layers: bits & SHADER_LOOK.TWO_LAYERS_BLENDED ? 'blended' : bits & SHADER_LOOK.TWO_LAYERS_ADDED ? 'added' : null, metal: metalOf(bits), - emissive: !!(bits & SHADER_LOOK.EMISSIVE), - textureAlpha: technique.alpha || 'opacity', - uvAnim: !!(flags & TECHNIQUE.UV_ANIM), - flags, - blend: flags & TECHNIQUE.ADDITIVE ? 'additive' : flags & TECHNIQUE.BLEND ? 'blend' : flags & TECHNIQUE.ALPHA_TEST ? 'test' : flags & TECHNIQUE.NO_BLEND ? 'opaque' : 'nif', - doubleSided: !!(flags & TECHNIQUE.DOUBLE_SIDED), - hidden: !!(flags & TECHNIQUE.NOT_DRAWN) + emissive: !!(bits & SHADER_LOOK.EMISSIVE) }; } -/** - * How a mesh drawn with `look` is blended: {transparent, depthWrite, additive, alphaCutoff, doubleSided}. Most of the - * game's files have alpha blending switched on; it only shows where something is see-through (`seeThrough`: the - * material, a vertex or the texture's alpha as opacity). Blended meshes still write depth, as Gamebryo's default does, - * unless the technique turns that off. - */ -export function blendingOf(look, mesh, seeThrough) { - const cutoff = mesh.test >= 0 ? Math.max(mesh.test / 255, 0.01) : 0; - const doubleSided = !!mesh.doubleSided || !!(look && look.doubleSided); - switch (look ? look.blend : 'nif') { - case 'additive': return { transparent: true, depthWrite: false, additive: true, alphaCutoff: 0, doubleSided }; - case 'blend': return { transparent: true, depthWrite: false, additive: false, alphaCutoff: cutoff, doubleSided }; - case 'test': return { transparent: false, depthWrite: true, additive: false, alphaCutoff: cutoff || 0.5, doubleSided }; - case 'opaque': return { transparent: false, depthWrite: true, additive: false, alphaCutoff: 0, doubleSided }; - default: return { transparent: !!(mesh.blend && seeThrough), depthWrite: true, additive: false, alphaCutoff: cutoff, doubleSided }; - } -} - // A shader's metal from its eShaderLook bits: 'polished', 'brushed' or null export function metalOf(bits) { if (!(bits & SHADER_LOOK.REFLECTIVE)) return null; @@ -309,37 +262,6 @@ export function parseDds(buffer, maxSize = 4096) { return image; } -/** - * A DDS cube map (the client's environment cubes, textures/env and textures/metal) as six RGBA faces no larger than - * maxSize: {faces: [{format: 'RGBA', width, height, data}]} in the file's order (+X, -X, +Y, -Y, +Z, -Z, as WebGL - * takes them). With `plain`, a DDS that isn't a cube comes back as its one face instead; null otherwise. - */ -export function parseDdsCube(buffer, maxSize = 256, plain = false) { - if (buffer.byteLength < 128) return null; - const view = new DataView(buffer); - if (view.getUint32(0, true) !== 0x20534444) return null; - const isCube = (view.getUint32(112, true) & 0x200) !== 0; - if (isCube === plain) return null; - const height = view.getUint32(12, true), width = view.getUint32(16, true); - const mipCount = Math.max(1, view.getUint32(28, true)); - const pfFlags = view.getUint32(80, true); - const format = FOURCC[view.getUint32(84, true)]; - if (!(pfFlags & 0x4) || !format || !width || !height || width > 4096 || height > 4096) return null; - const blockBytes = format === 'DXT1' ? 8 : 16; - const levelBytes = (w, h) => Math.max(1, (w + 3) >> 2) * Math.max(1, (h + 3) >> 2) * blockBytes; - let faceBytes = 0; - for (let i = 0, w = width, h = height; i < mipCount; i++, w = Math.max(1, w >> 1), h = Math.max(1, h >> 1)) faceBytes += levelBytes(w, h); - const count = isCube ? 6 : 1; - if (128 + faceBytes * count > buffer.byteLength) return null; - const faces = []; - for (let f = 0; f < count; f++) { - const data = new Uint8Array(buffer, 128 + f * faceBytes, levelBytes(width, height)); - const rgba = decodeDxt(format, width, height, data); - faces.push(downscale({ format: 'RGBA', width, height, levels: [{ width, height, data: rgba }] }, maxSize).levels[0]); - } - return plain ? { format: 'RGBA', ...faces[0] } : { faces: faces.map((face) => ({ format: 'RGBA', ...face })) }; -} - // Halve an RGBA image (box filter) until it fits maxSize function downscale(image, maxSize) { let { width, height, data } = image.levels[0]; diff --git a/tests/dWebTests/CMakeLists.txt b/tests/dWebTests/CMakeLists.txt index d918b9486..0dab41635 100644 --- a/tests/dWebTests/CMakeLists.txt +++ b/tests/dWebTests/CMakeLists.txt @@ -83,9 +83,9 @@ gtest_discover_tests(dWebTests) target_compile_definitions(dWebTests PRIVATE DLU_SOURCE_DIR="${PROJECT_SOURCE_DIR}") -# The 3D views' shader lookups (static/js/scenery-core.js) against the server's bits (NifFile.h), when node is there +# The 3D views' shader lookups (static/js/scenery-core.js), when node is there find_program(NODE_EXECUTABLE node) if(NODE_EXECUTABLE) add_test(NAME SceneryCoreJs COMMAND ${NODE_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/scenery-core.test.mjs" - "${PROJECT_SOURCE_DIR}/dDashboardServer/static/js/scenery-core.js" "${PROJECT_SOURCE_DIR}/dCommon/NifFile.h") + "${PROJECT_SOURCE_DIR}/dDashboardServer/static/js/scenery-core.js") endif() diff --git a/tests/dWebTests/NifFileTests.cpp b/tests/dWebTests/NifFileTests.cpp index c82bae71d..84aafa745 100644 --- a/tests/dWebTests/NifFileTests.cpp +++ b/tests/dWebTests/NifFileTests.cpp @@ -587,58 +587,6 @@ TEST(NifFileTests, KnowsWhichShadersUseTextureAlphaAsOpacity) { EXPECT_EQ(NifFile::TextureAlphaFor(-1), eTextureAlpha::OPACITY); // fixed function: NiAlphaProperty as Gamebryo does } -TEST(NifFileTests, KnowsEachShadersTechniqueFamily) { - using NifFile::eShaderFamily; - const auto family = [](int32_t shader) { return NifFile::TechniqueFor(shader).family; }; - const auto flags = [](int32_t shader) { return NifFile::TechniqueFor(shader).flags; }; - EXPECT_EQ(family(-1), eShaderFamily::FIXED_FUNCTION); - // The ones most of the zones' objects use: LEGO, Basic VC, "Basic", VertColor_Alpha, LEGO NoAmbient - EXPECT_EQ(family(NifFile::LEGO_SHADER), eShaderFamily::LEGO); - EXPECT_EQ(family(38), eShaderFamily::BASIC); - EXPECT_EQ(family(94), eShaderFamily::BASIC); - EXPECT_EQ(family(7), eShaderFamily::BASIC); - EXPECT_TRUE(flags(7) & NifFile::DOUBLE_SIDED); // AlphaAsAlpha: Cullmode none - EXPECT_EQ(family(88), eShaderFamily::LEGO); - EXPECT_TRUE(flags(88) & NifFile::NO_AMBIENT); - EXPECT_EQ(family(3), eShaderFamily::TERRAIN); - EXPECT_TRUE(flags(3) & NifFile::RIM_LIGHT); - // Moving textures, water, metal, glass, darklings - EXPECT_TRUE(flags(30) & NifFile::UV_ANIM); // LEGO-AnimUV - EXPECT_TRUE(flags(70) & NifFile::UV_ANIM); // ScrollingUV_NoLight_AnimAlpha - EXPECT_FALSE(flags(38) & NifFile::UV_ANIM); - EXPECT_EQ(family(69), eShaderFamily::OCEAN); - EXPECT_TRUE(flags(90) & NifFile::OCEAN_FX); - EXPECT_EQ(family(98), eShaderFamily::METAL); - EXPECT_EQ(family(99), eShaderFamily::METAL); - EXPECT_EQ(family(6), eShaderFamily::CLEAR_PLASTIC); - EXPECT_TRUE(flags(6) & NifFile::BLEND); - EXPECT_EQ(family(75), eShaderFamily::DARKLING); - EXPECT_TRUE(flags(76) & NifFile::SPECULAR); - EXPECT_TRUE(flags(22) & NifFile::SUPER_EMISSIVE); - EXPECT_TRUE(flags(87) & NifFile::ADDITIVE); - EXPECT_TRUE(flags(74) & NifFile::NOT_DRAWN); // Drop Shadow - // A value the table lacks is the LEGO shader, as the client falls back to it - EXPECT_EQ(family(4242), eShaderFamily::LEGO); - EXPECT_EQ(NifFile::TextureAlphaFor(4242), NifFile::eTextureAlpha::DECAL); -} - -TEST(NifFileTests, WritesTechniquesForTheManifest) { - const auto json = nlohmann::json::parse(NifFile::TechniquesJson({ -1, 5, 99, 87 })); - ASSERT_EQ(json.size(), 4u); - EXPECT_EQ(json["-1"]["family"], "fixed"); - EXPECT_EQ(json["5"]["family"], "lego"); - EXPECT_EQ(json["5"]["alpha"], "decal"); - EXPECT_EQ(json["99"]["family"], "metal"); - EXPECT_EQ(json["99"]["look"], NifFile::REFLECTIVE | NifFile::BRUSHED); - EXPECT_EQ(json["87"]["flags"], NifFile::ADDITIVE); - EXPECT_EQ(json["87"]["look"], NifFile::UNLIT); - for (const auto family : { "fixed", "lego", "basic", "metal", "clearPlastic", "ocean", "flatSurf", "brickWater", "darkling", "terrain" }) { - bool named = false; - for (int i = 0; i <= static_cast(NifFile::eShaderFamily::TERRAIN); i++) named |= std::string(NifFile::FamilyName(static_cast(i))) == family; - EXPECT_TRUE(named) << family; - } -} - TEST(NifFileTests, PassesMultishaderTagsDownToMeshes) { NifBuilder nif; auto rootAv = Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {}); diff --git a/tests/dWebTests/scenery-core.test.mjs b/tests/dWebTests/scenery-core.test.mjs index 3a8874230..e5552511e 100644 --- a/tests/dWebTests/scenery-core.test.mjs +++ b/tests/dWebTests/scenery-core.test.mjs @@ -1,9 +1,8 @@ -// The 3D views' shader lookups (static/js/scenery-core.js): which technique draws a mesh and what it uses. -// Run by ctest: node scenery-core.test.mjs [NifFile.h] +// The 3D views' shader lookups (static/js/scenery-core.js): which shader draws a mesh and what it uses. +// Run by ctest: node scenery-core.test.mjs import { pathToFileURL } from 'node:url'; -import { readFileSync } from 'node:fs'; -const [modulePath, nifHeader] = process.argv.slice(2); +const [modulePath] = process.argv.slice(2); const S = await import(pathToFileURL(modulePath).href); let failures = 0; const same = (actual, expected, what) => { @@ -13,24 +12,16 @@ const same = (actual, expected, what) => { } }; -// A manifest as Scenery.cpp writes it: "techniques" from NifFile::TechniquesJson -const L = S.SHADER_LOOK, T = S.TECHNIQUE; const manifest = { shaders: [38, 9999, -1, 33], shaderTags: { 1: 5, 30: 38, 25: 33, 2: 2 }, - techniques: { - '-1': { family: 'fixed', look: 0, alpha: 'opacity', flags: 0 }, - 5: { family: 'lego', look: 0, alpha: 'decal', flags: 0 }, - 33: { family: 'basic', look: L.UNLIT | L.NO_TEXTURE, alpha: 'opacity', flags: T.ANIM_ALPHA }, - 38: { family: 'basic', look: 0, alpha: 'opacity', flags: 0 } - }, + textureAlpha: { 5: 'decal' }, + shaderLooks: { 33: S.SHADER_LOOK.UNLIT | S.SHADER_LOOK.NO_TEXTURE }, multishader: 9999, defaultShader: 5, lighting: { ambient: [0.4, 0.6, 0.7], light: [1, 1, 1], lightVec: [0, 1, 0] } }; const colored = { colors: new Uint8Array(4), vertexColors: 2 }; -const pick = (look, keys) => Object.fromEntries(keys.map((k) => [k, look[k]])); -const BASICS = ['family', 'lit', 'texture', 'vertexColors', 'material', 'layers', 'metal', 'emissive']; // A multishader part's tag names its shader; an unusable or missing tag is the LEGO shader same(S.shaderOf(manifest, 1, { shaderTag: 30 }), 38, 'tagged part'); @@ -41,41 +32,17 @@ same(S.shaderOf({}, 0, {}), null, 'no shaders in the manifest'); same(S.textureAlphaMode(manifest, 1, { shaderTag: 1 }), 'decal', 'LEGO part texture alpha'); same(S.textureAlphaMode(manifest, 1, { shaderTag: 30 }), 'opacity', 'Basic VC part texture alpha'); -// Techniques: the manifest's table, fixed function without a shader, the LEGO shader for one it lacks -same(S.techniqueOf(manifest, 0, {}).family, 'basic', 'Basic VC technique'); -same(S.techniqueOf(manifest, 2, {}).family, 'fixed', 'fixed function technique'); -same(S.techniqueOf({ ...manifest, shaders: [77] }, 0, {}), { shader: 77, family: 'lego', look: 0, alpha: 'decal', flags: 0 }, 'unknown shader is LEGO'); -same(S.techniqueOf({ ...manifest, technique: { family: 'flair', look: 0, alpha: 'opacity', flags: 0 } }, 0, {}).family, 'flair', 'manifest-wide technique'); - // Lit, textured, vertex colors, no material colors: Basic VC -same(pick(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), BASICS), { family: 'basic', lit: true, texture: true, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic VC'); +same(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), { lit: true, texture: true, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic VC'); // Vertex colors are read even when NiVertexColorProperty ignores them, but only if the mesh has some same(S.gameLook(manifest, 0, { ...colored, vertexColors: 0 }).vertexColors, true, 'shader reads vertex colors'); same(S.gameLook(manifest, 0, {}).vertexColors, false, 'mesh without vertex colors'); -same(pick(S.gameLook(manifest, 3, colored), BASICS), { family: 'basic', lit: false, texture: false, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic NL VC NT'); +same(S.gameLook(manifest, 3, colored), { lit: false, texture: false, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic NL VC NT'); // Fixed function: NiVertexColorProperty and the material decide -same(pick(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), BASICS), { family: 'fixed', lit: true, texture: true, vertexColors: false, material: true, layers: null, metal: null, emissive: false }, 'fixed function'); +same(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), { lit: true, texture: true, vertexColors: false, material: true, layers: null, metal: null, emissive: false }, 'fixed function'); // Without the zone's lighting the viewer lights scenery itself same(S.gameLook({ ...manifest, lighting: null }, 0, colored), null, 'no lighting'); -// What the flags turn into: moving textures, both sides, blending, not drawn -const flagged = (flags, family = 'basic') => S.gameLook({ ...manifest, shaders: [1], techniques: { 1: { family, look: 0, alpha: 'opacity', flags } } }, 0, colored); -same(flagged(T.UV_ANIM).uvAnim, true, 'UV animation'); -same(flagged(0).uvAnim, false, 'still texture'); -same(flagged(T.DOUBLE_SIDED).doubleSided, true, 'AlphaAsAlpha both sides'); -same([flagged(0).blend, flagged(T.BLEND).blend, flagged(T.ALPHA_TEST).blend, flagged(T.ADDITIVE).blend, flagged(T.NO_BLEND).blend], ['nif', 'blend', 'test', 'additive', 'opaque'], 'blend modes'); -same(flagged(T.NOT_DRAWN).hidden, true, 'not drawn'); -same(flagged(T.ANIM_ALPHA).flags & T.ANIM_ALPHA, T.ANIM_ALPHA, 'flags kept'); - -// Blending as the look and the mesh say -same(S.blendingOf(null, { blend: true, test: -1 }, true), { transparent: true, depthWrite: true, additive: false, alphaCutoff: 0, doubleSided: false }, 'NiAlphaProperty blend where see-through'); -same(S.blendingOf(flagged(0), { blend: true, test: -1 }, false).transparent, false, 'blend on but nothing see-through'); -same(S.blendingOf(flagged(T.BLEND), { blend: false, test: -1 }, false), { transparent: true, depthWrite: false, additive: false, alphaCutoff: 0, doubleSided: false }, 'technique blends'); -same(S.blendingOf(flagged(T.ADDITIVE), { blend: false, test: -1 }, false).additive, true, 'additive'); -same(S.blendingOf(flagged(T.ALPHA_TEST), { blend: true, test: -1 }, true), { transparent: false, depthWrite: true, additive: false, alphaCutoff: 0.5, doubleSided: false }, 'alpha test'); -same(S.blendingOf(flagged(T.NO_BLEND), { blend: true, test: 128 }, true).transparent, false, 'opaque technique'); -same(S.blendingOf(flagged(T.DOUBLE_SIDED), { blend: false, test: -1, doubleSided: false }, false).doubleSided, true, 'Cullmode none'); - // Scene maps: the same terrain as ZoneScenesTests.FindsTheSceneUnderAPosition, as runs of [length, scene] const runs = (bytes) => Buffer.from(bytes).toString('base64'); const map = S.decodeSceneMap({ chunks: [ @@ -91,51 +58,19 @@ same([...S.loadedScenes(scenes, 0)], [0], 'loaded in the global scene'); same(S.decodeSceneMap({ chunks: [{ x: 0, z: 0, maxX: 1, maxZ: 1, size: 1, runs: runs([9, 4]) }] }).chunks[0].cells.length, 1, 'runs clipped'); // Two layer shaders -const layered = { ...manifest, shaders: [106, 107], techniques: { 106: { family: 'basic', look: L.TWO_LAYERS_BLENDED, alpha: 'opacity', flags: T.UV_ANIM }, 107: { family: 'basic', look: L.TWO_LAYERS_ADDED, alpha: 'opacity', flags: T.UV_ANIM } } }; +const layered = { ...manifest, shaders: [106, 107], shaderLooks: { 106: S.SHADER_LOOK.TWO_LAYERS_BLENDED, 107: S.SHADER_LOOK.TWO_LAYERS_ADDED } }; same(S.gameLook(layered, 0, colored).layers, 'blended', 'two layers blended'); same(S.gameLook(layered, 1, colored).layers, 'added', 'two layers added'); same(S.gameLook(manifest, 0, colored).layers, null, 'one layer'); // A player model's metal and glow groups (UGC server shader settings): Polished Metal, Brushed Steel, LEGO-Emissive const shiny = { ...manifest, shaders: [9999], shaderTags: { 1: 5, 88: 98, 89: 99, 46: 53 }, - techniques: { ...manifest.techniques, 98: { family: 'metal', look: L.REFLECTIVE, alpha: 'opacity', flags: 0 }, 99: { family: 'metal', look: L.REFLECTIVE | L.BRUSHED, alpha: 'opacity', flags: 0 }, - 53: { family: 'lego', look: L.EMISSIVE, alpha: 'opacity', flags: 0 } } }; + shaderLooks: { 98: S.SHADER_LOOK.REFLECTIVE, 99: S.SHADER_LOOK.REFLECTIVE | S.SHADER_LOOK.BRUSHED, 53: S.SHADER_LOOK.EMISSIVE } }; same(S.gameLook(shiny, 0, { ...colored, shaderTag: 88 }).metal, 'polished', 'polished metal'); same(S.gameLook(shiny, 0, { ...colored, shaderTag: 89 }).metal, 'brushed', 'brushed steel'); -same(S.gameLook(shiny, 0, { ...colored, shaderTag: 89 }).family, 'metal', 'metal family'); same(S.gameLook(shiny, 0, { ...colored, shaderTag: 46 }).emissive, true, 'emissive'); same(S.gameLook(shiny, 0, { ...colored, shaderTag: 1 }).metal, null, 'plastic'); -same([S.metalOf(0), S.metalOf(L.REFLECTIVE), S.metalOf(L.REFLECTIVE | L.BRUSHED)], [null, 'polished', 'brushed'], 'metal of look bits'); - -// Environment cubes: a DXT1 cube of six 4x4 faces, one color each, made RGBA -const cube = new Uint8Array(128 + 6 * 8); -const header = new DataView(cube.buffer); -header.setUint32(0, 0x20534444, true); -header.setUint32(12, 4, true); header.setUint32(16, 4, true); header.setUint32(28, 1, true); -header.setUint32(80, 0x4, true); header.setUint32(84, 0x31545844, true); header.setUint32(112, 0xfe00, true); -const faceColors = [0xf800, 0x07e0, 0x001f, 0xffff, 0x0000, 0x8410]; -faceColors.forEach((c, f) => { const at = 128 + f * 8; cube[at] = c & 255; cube[at + 1] = c >> 8; cube[at + 2] = c & 255; cube[at + 3] = c >> 8; }); -const parsed = S.parseDdsCube(cube.buffer, 256); -same(parsed.faces.length, 6, 'six faces'); -same([...parsed.faces[0].data.slice(0, 4)], [255, 0, 0, 255], '+X red'); -same([...parsed.faces[2].data.slice(0, 4)], [0, 0, 255, 255], '+Y blue'); -same([parsed.faces[5].width, parsed.faces[5].height], [4, 4], 'face size'); -same(S.parseDdsCube(cube.buffer, 2).faces[0].width, 2, 'faces made smaller'); -same(S.parseDdsCube(cube.buffer, 256, true), null, 'a cube is no plain texture'); -header.setUint32(112, 0, true); -same(S.parseDdsCube(cube.buffer, 256), null, 'a plain texture is no cube'); -same(S.parseDdsCube(cube.buffer, 256, true).width, 4, 'plain texture'); - -// The flag and look bits are the server's (NifFile.h eTechniqueFlag, eShaderLook) -if (nifHeader) { - const text = readFileSync(nifHeader, 'utf8'); - const bitsOf = (name) => { - const block = text.slice(text.indexOf('enum ' + name)).split('};')[0]; - return Object.fromEntries([...block.matchAll(/^\s*([A-Z_]+) = (\d+)/gm)].map((m) => [m[1], Number(m[2])])); - }; - same(bitsOf('eTechniqueFlag'), T, 'TECHNIQUE matches eTechniqueFlag'); - same(bitsOf('eShaderLook'), L, 'SHADER_LOOK matches eShaderLook'); -} +same([S.metalOf(0), S.metalOf(S.SHADER_LOOK.REFLECTIVE), S.metalOf(S.SHADER_LOOK.REFLECTIVE | S.SHADER_LOOK.BRUSHED)], [null, 'polished', 'brushed'], 'metal of look bits'); // The near plane grows with the distance, within limits same([S.nearPlaneFor(10), S.nearPlaneFor(2000), S.nearPlaneFor(100000)], [0.5, 5, 20], 'near plane');