mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-19 03:02:49 +00:00
feat(dashboard): one table of the game's shader techniques for the 3D views
NifFile::TechniqueFor maps every mapShaders gameValue to the client's technique family (fixed function, LEGO, Basic/AlphaAsAlpha, metal, clear plastic, ocean distortion, flat surf, BrickWater, darkling, terrain mesh), its eShaderLook bits, texture alpha and eTechniqueFlag bits (moving texture, both sides, blend, alpha test, additive, no ambient, glow, super emissive, grayscale, shiny glint, not drawn, ...), from res/shaders and the verified technique setups. Values it lacks are the LEGO shader, as the client falls back to it. TextureAlphaFor and ShaderLookFor read it. The scenery manifests carry it as "techniques" (replacing textureAlpha and shaderLooks), the flairs' manifest a Flair.fx technique, the lighting its specular color. /api/scenery/env/:name serves the environment cubes the client's shaders load themselves (default reflection, polished and brushed metal, brushed noise). Conversion format 4. scenery-core.js: techniqueOf, gameLook with the family and flags, blendingOf, parseDdsCube; its test checks the flag and look bits against NifFile.h. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,29 +208,21 @@ namespace {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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).
|
||||
*/
|
||||
void AddShaders(nlohmann::json& manifest, const std::vector<int32_t>& assetShaders) {
|
||||
RenderInfos(); // reads g_ShaderValues
|
||||
manifest["shaders"] = assetShaders;
|
||||
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();
|
||||
nlohmann::json tags = nlohmann::json::object();
|
||||
std::set<int32_t> values{ -1, NifFile::LEGO_SHADER };
|
||||
for (const auto& [id, value] : g_ShaderValues) {
|
||||
if (const auto look = NifFile::ShaderLookFor(value)) looks[std::to_string(value)] = look;
|
||||
tags[std::to_string(id)] = value;
|
||||
values.insert(value);
|
||||
}
|
||||
manifest["shaderTags"] = std::move(tags);
|
||||
manifest["textureAlpha"] = std::move(modes);
|
||||
manifest["shaderLooks"] = std::move(looks);
|
||||
manifest["techniques"] = nlohmann::json::parse(NifFile::TechniquesJson({ values.begin(), values.end() }));
|
||||
manifest["multishader"] = NifFile::MULTISHADER;
|
||||
manifest["defaultShader"] = NifFile::LEGO_SHADER;
|
||||
}
|
||||
@@ -245,15 +237,16 @@ 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.
|
||||
* 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.
|
||||
*/
|
||||
constexpr uint32_t FORMAT_VERSION = 3;
|
||||
constexpr uint32_t FORMAT_VERSION = 4;
|
||||
|
||||
// A zone's lighting (WorldScene::Lighting) for the viewers' shaders
|
||||
nlohmann::json LightingJson(const WorldScene::Lighting& lighting) {
|
||||
const auto triple = [](const std::array<float, 3>& 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)},
|
||||
{"ambient", triple(lighting.ambient)}, {"light", triple(lighting.light)}, {"lightVec", triple(lighting.lightVec)}, {"specular", triple(lighting.specular)},
|
||||
{"upperHemi", triple(lighting.upperHemi)}, {"fogColor", triple(lighting.fogColor)},
|
||||
{"fogNear", Round(lighting.fogNear, 10.0)}, {"fogFar", Round(lighting.fogFar, 10.0)}
|
||||
};
|
||||
@@ -445,6 +438,8 @@ 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();
|
||||
}
|
||||
@@ -956,7 +951,46 @@ 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<std::string, std::string>& EnvironmentTextures() {
|
||||
static const std::map<std::string, std::string> 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<std::string, std::shared_ptr<const std::string>> cache;
|
||||
std::shared_ptr<const std::string> 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<const std::string>(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) {
|
||||
|
||||
@@ -40,18 +40,6 @@ 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
|
||||
@@ -112,22 +100,57 @@ 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 };
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
export function gameLook(manifest, asset, mesh) {
|
||||
if (!manifest || !manifest.lighting) return null;
|
||||
const shader = shaderOf(manifest, asset, mesh);
|
||||
const fixedFunction = shader === null || shader < 0;
|
||||
const bits = fixedFunction || !manifest.shaderLooks ? 0 : manifest.shaderLooks[shader] || 0;
|
||||
const technique = techniqueOf(manifest, asset, mesh);
|
||||
const fixedFunction = technique.family === 'fixed';
|
||||
const bits = technique.look || 0;
|
||||
const flags = technique.flags || 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
|
||||
@@ -135,10 +158,34 @@ 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)
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -262,6 +309,37 @@ 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];
|
||||
|
||||
Reference in New Issue
Block a user