Revert "feat(dashboard): one table of the game's shader techniques for the 3D views"

This reverts commit ee9135437f.
This commit is contained in:
Aaron Kimbrell
2026-09-28 13:53:44 -05:00
parent c3b52545b5
commit d6dd573741
7 changed files with 123 additions and 515 deletions

View File

@@ -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()

View File

@@ -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<int>(NifFile::eShaderFamily::TERRAIN); i++) named |= std::string(NifFile::FamilyName(static_cast<NifFile::eShaderFamily>(i))) == family;
EXPECT_TRUE(named) << family;
}
}
TEST(NifFileTests, PassesMultishaderTagsDownToMeshes) {
NifBuilder nif;
auto rootAv = Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {});

View File

@@ -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 <scenery-core.js> [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 <scenery-core.js>
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');