mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-23 05:02:48 +00:00
fix(dashboard): texture alpha in 3D scenery follows the game's shaders
The viewer treated every blended texture's alpha as opacity, so models whose shader uses the alpha for something else rendered see-through. The scenery manifest now carries each model's shader (RenderComponent. shader_id via mapShaders) and the multishader tag table; meshes carry their S##__ tag, read the way LWOBaseRenderComponent::AddObjectToRenderPipe does (S%d, else _S%d, outside 3..108 the LEGO shader). Per res/shaders: the LEGO lighting shaders lerp the texture over the vertex colors (decal), LEGO items and terrain meshes ignore its alpha, everything else keeps opacity. Pure rules unit tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,23 @@ 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.shaders || !manifest.textureAlpha) return 'opacity';
|
||||
let shader = manifest.shaders[asset];
|
||||
if (shader === undefined || shader === null) return 'opacity';
|
||||
if (shader === manifest.multishader) {
|
||||
const tagged = mesh && mesh.shaderTag >= 0 && manifest.shaderTags ? manifest.shaderTags[mesh.shaderTag] : undefined;
|
||||
shader = tagged !== undefined && tagged >= 3 && tagged <= 108 ? tagged : manifest.defaultShader;
|
||||
}
|
||||
return manifest.textureAlpha[shader] || 'opacity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Meshes of a model that look the same (texture, colors, blending, sides, attributes) joined into one, so a model
|
||||
* made of many pieces (the zones' "glom" files have over a hundred) costs a few draw calls instead of one per piece.
|
||||
@@ -46,7 +63,7 @@ export function mergeMeshes(meshes) {
|
||||
for (const mesh of meshes) {
|
||||
if (!mesh.vertices || !mesh.indices.length) continue;
|
||||
const key = JSON.stringify([mesh.texture, mesh.diffuse, mesh.emissive, mesh.alpha, mesh.blend, mesh.test, mesh.doubleSided,
|
||||
mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors]);
|
||||
mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors, mesh.shaderTag]);
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(mesh);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* follows the camera, far away, behind everything.
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { parseModel, mergeMeshes, parseDds, decodeDxt, completeChain, linearColors, groupObjects, cellsOf } from '/js/scenery-core.js';
|
||||
import { parseModel, mergeMeshes, parseDds, decodeDxt, completeChain, linearColors, groupObjects, cellsOf, textureAlphaMode } from '/js/scenery-core.js';
|
||||
|
||||
// Per detail level (the property view's 0 high, 1 medium, 2 low): the model LOD, how far objects are drawn, the
|
||||
// largest texture side and a memory budget for geometry and textures
|
||||
@@ -126,12 +126,50 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function materialOf(mesh, map, forSky) {
|
||||
// The game's LEGO shaders lay the texture over the vertex colors by its alpha instead of letting it show through
|
||||
const DECAL_FRAGMENT = `
|
||||
#if defined( USE_COLOR_ALPHA )
|
||||
diffuseColor *= vColor;
|
||||
#elif defined( USE_COLOR )
|
||||
diffuseColor.rgb *= vColor;
|
||||
#endif
|
||||
#ifdef USE_MAP
|
||||
vec4 sampledDiffuseColor = texture2D( map, vMapUv );
|
||||
#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )
|
||||
diffuseColor.rgb = mix( diffuseColor.rgb, sampledDiffuseColor.rgb, sampledDiffuseColor.a );
|
||||
#else
|
||||
diffuseColor.rgb *= sampledDiffuseColor.rgb;
|
||||
#endif
|
||||
#endif
|
||||
`;
|
||||
// ... or leave its alpha out altogether (LEGO items, terrain meshes)
|
||||
const OPAQUE_MAP_FRAGMENT = `
|
||||
#ifdef USE_MAP
|
||||
diffuseColor.rgb *= texture2D( map, vMapUv ).rgb;
|
||||
#endif
|
||||
`;
|
||||
|
||||
function useTextureAlpha(material, mode) {
|
||||
if (mode === 'opacity' || !material.map) return material;
|
||||
material.onBeforeCompile = (shader) => {
|
||||
if (mode === 'decal') {
|
||||
shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', '').replace('#include <color_fragment>', DECAL_FRAGMENT);
|
||||
} else {
|
||||
shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>', OPAQUE_MAP_FRAGMENT);
|
||||
}
|
||||
};
|
||||
material.customProgramCacheKey = () => 'textureAlpha:' + mode;
|
||||
return material;
|
||||
}
|
||||
|
||||
function materialOf(mesh, map, forSky, alphaMode = 'opacity') {
|
||||
// Nearly everything in the game's files has alpha blending switched on; it only shows where something is see-
|
||||
// through: the material, a vertex or the texture. Blended meshes still write depth, as Gamebryo's default does.
|
||||
// through: the material, a vertex or the texture (only when the object's shader uses the texture's alpha as
|
||||
// opacity). Blended meshes still write depth, as Gamebryo's default does.
|
||||
let vertexAlpha = false;
|
||||
if (mesh.colors && mesh.vertexColors !== 0) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250;
|
||||
const seeThrough = mesh.blend && (mesh.alpha < 0.99 || vertexAlpha || !!(map && map.userData.alpha));
|
||||
const textureAlpha = alphaMode === 'opacity' && !!(map && map.userData.alpha);
|
||||
const seeThrough = mesh.blend && (mesh.alpha < 0.99 || vertexAlpha || textureAlpha);
|
||||
const options = {
|
||||
color: new THREE.Color().setRGB(mesh.diffuse[0], mesh.diffuse[1], mesh.diffuse[2], THREE.SRGBColorSpace),
|
||||
vertexColors: !!(mesh.colors && mesh.vertexColors !== 0),
|
||||
@@ -141,10 +179,10 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
||||
side: mesh.doubleSided ? THREE.DoubleSide : THREE.FrontSide,
|
||||
map: map || null
|
||||
};
|
||||
if (forSky) return new THREE.MeshBasicMaterial({ ...options, depthWrite: false, fog: false });
|
||||
if (forSky) return useTextureAlpha(new THREE.MeshBasicMaterial({ ...options, depthWrite: false, fog: false }), alphaMode);
|
||||
const material = new THREE.MeshStandardMaterial({ ...options, roughness: 0.85, metalness: 0 });
|
||||
material.emissive.setRGB(mesh.emissive[0], mesh.emissive[1], mesh.emissive[2], THREE.SRGBColorSpace);
|
||||
return material;
|
||||
return useTextureAlpha(material, alphaMode);
|
||||
}
|
||||
|
||||
async function buildParts(asset, lod, forSky) {
|
||||
@@ -165,7 +203,7 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
||||
}
|
||||
}
|
||||
const geometry = geometryOf(mesh);
|
||||
parts.push({ geometry, material: materialOf(mesh, map, forSky) });
|
||||
parts.push({ geometry, material: materialOf(mesh, map, forSky, textureAlphaMode(manifest, asset, mesh)) });
|
||||
}
|
||||
const min = model.header.min, max = model.header.max;
|
||||
const radius = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2;
|
||||
|
||||
Reference in New Issue
Block a user