Revert "feat(dashboard): the game's own shaders in the world and property 3D views"

This reverts commit acc4853756.
This commit is contained in:
Aaron Kimbrell
2026-09-28 13:53:43 -05:00
parent e0ccd3a016
commit c3b52545b5
7 changed files with 124 additions and 649 deletions

View File

@@ -1,528 +0,0 @@
/**
* The game client's shaders for the 3D views (ES module, three.js): one ShaderMaterial program per technique family
* (scenery-core.js gameLook says which a mesh uses, from the server's table NifFile::TechniqueFor), with the maths of
* the client's techniques in GLSL. Everything is worked out in the game's color space (sRGB values, as Direct3D 9
* did) and made linear at the end for three.js.
*
* Families: 'lego' (LEGOPPLighting: hemisphere lit sun, ambient, fresnel rim, specular and a faint reflection of the
* default environment cube), 'basic' (BasicShaders and AlphaAsAlpha: sun * N.L + ambient per vertex, or unlit),
* 'fixed' (fixed function: lit like basic with the material's colors), 'metal' (Metallic.fx polished metal and
* brushed steel), 'clearPlastic', 'ocean' (the Distortion shaders' warped texture layers), 'flatSurf', 'brickWater',
* 'darkling', 'terrain' (terrain meshes) and 'flair' (Flair.fx); the sky (Skydome.fx) is 'basic', unlit, with its
* texture moving. The shared uniforms (lights, fog, time, environment cubes) are updated in place, so every material
* follows the zone's lighting and its blends between scenes. Materials with the same defines share one program.
*/
import * as THREE from 'three';
import { TECHNIQUE, parseDdsCube } from '/js/scenery-core.js';
const VERTEX = /* glsl */ `
uniform vec3 gameLightColor;
uniform vec3 gameAmbient;
uniform vec3 gameLightVec;
uniform vec3 gameUpperHemi;
uniform vec3 gameSpecular;
uniform float gameTime;
uniform vec2 uvScroll;
uniform vec3 materialDiffuse;
uniform float materialAlpha;
#ifdef USE_DARK
attribute vec2 uvDark;
varying vec2 vUvDark;
#endif
varying vec2 vUv;
varying vec4 vColor;
varying vec3 vLight; // the lit color the technique's vertex shader outputs (clamped as a COLOR output is)
varying float vLdn; // N.L, clamped
varying vec3 vNormal; // world space
varying vec3 vWorld;
varying vec3 vObject; // object space position (brushed steel's noise, shiny glint's height)
vec3 hemi( vec3 n ) {
// CalculateHemiLightInfluence: the upper hemisphere light over the lower one (the client leaves that white)
float up = n.y + 1.0;
return ( gameUpperHemi * up + vec3( 1.0 ) * ( 2.0 - up ) ) * 0.5;
}
void main() {
mat4 world = modelMatrix;
#ifdef USE_INSTANCING
world = modelMatrix * instanceMatrix;
#endif
vec4 worldPosition = world * vec4( position, 1.0 );
vWorld = worldPosition.xyz;
vObject = position;
vec3 n = normalize( mat3( world ) * normal );
vNormal = n;
vUv = uv;
#ifdef UV_ANIM
vUv += uvScroll * gameTime;
#endif
#ifdef USE_DARK
vUvDark = uvDark;
#endif
// Vertex colors come as the file has them (sRGB bytes), which is what the game's shaders get
vec4 vcol = vec4( 1.0 );
#if defined( USE_COLOR_ALPHA )
vcol = color;
#elif defined( USE_COLOR )
vcol = vec4( color, 1.0 );
#endif
#ifdef MATERIAL_COLOR
// Fixed function and the "Material" techniques: NiMaterialProperty's color (times the vertex colors it reads)
vcol *= vec4( materialDiffuse, materialAlpha );
#endif
#ifdef USE_INSTANCING_COLOR
vcol.rgb *= instanceColor;
#endif
vColor = vcol;
float ldn = dot( gameLightVec, n );
vLdn = clamp( ldn, 0.0, 1.0 );
#if defined( FAMILY_LEGO ) || defined( FAMILY_BRICKWATER ) || defined( FAMILY_DARKLING_SPECULAR )
#ifdef NO_AMBIENT
vec3 ambient = vec3( 1.0 );
#else
vec3 ambient = gameAmbient;
#endif
vec3 light = max( 0.0, ldn ) * hemi( n ) * gameLightColor + ambient;
#ifdef LIGHT_TIMES_COLOR
light *= vcol.rgb;
#endif
vLight = clamp( light, 0.0, 1.0 );
#elif defined( FAMILY_METAL )
// g_metalDiffuse * N.L^4 + g_metalAmbient
vLight = clamp( vec3( 0.7 ) * pow( vLdn, 4.0 ) + vec3( 0.3 ), 0.0, 1.0 );
#elif defined( FAMILY_FLAIR )
vLight = clamp( 0.85 * gameLightColor + gameAmbient, 0.0, 1.0 );
#elif defined( UNLIT )
vLight = vec3( 1.0 );
#else
vLight = clamp( max( vec3( 0.0 ), gameLightColor * ldn ) + gameAmbient, 0.0, 1.0 );
#endif
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`;
const FRAGMENT = /* glsl */ `
uniform vec3 gameLightColor;
uniform vec3 gameAmbient;
uniform vec3 gameLightVec;
uniform vec3 gameSpecular;
uniform vec3 gameFogColor;
uniform float gameFogNear;
uniform float gameFogFar;
uniform float gameFogOn;
uniform float gameTime;
uniform vec3 materialEmissive;
uniform float materialAlpha;
uniform vec2 layerWeights;
uniform float alphaCutoff;
uniform float opacity;
uniform sampler2D map;
uniform sampler2D darkMap;
uniform sampler2D noiseMap;
uniform samplerCube envMap;
uniform vec3 glowColor;
varying vec2 vUv;
#ifdef USE_DARK
varying vec2 vUvDark;
#endif
varying vec4 vColor;
varying vec3 vLight;
varying float vLdn;
varying vec3 vNormal;
varying vec3 vWorld;
varying vec3 vObject;
// Texels come in linear (sRGB textures are decoded by the GPU); the game's maths is on sRGB values
vec3 toGame( vec3 c ) { return mix( c * 12.92, 1.055 * pow( c, vec3( 1.0 / 2.4 ) ) - 0.055, step( vec3( 0.0031308 ), c ) ); }
vec3 toLinear( vec3 c ) { return mix( c / 12.92, pow( ( c + 0.055 ) / 1.055, vec3( 2.4 ) ), step( vec3( 0.04045 ), c ) ); }
vec4 tex( sampler2D s, vec2 uv ) { vec4 t = texture2D( s, uv ); return vec4( toGame( t.rgb ), t.a ); }
vec4 cube( vec3 dir ) { vec4 t = textureCube( envMap, dir ); return vec4( toGame( t.rgb ), t.a ); }
float fresnel( float vdn ) { return ( 0.1 + 0.9 * pow( 1.0 - min( 1.0, abs( vdn ) ), 3.5 ) ) * 0.8; }
void main() {
vec3 n = normalize( vNormal );
if ( !gl_FrontFacing ) n = -n;
vec3 view = normalize( cameraPosition - vWorld );
float vdn = dot( view, n );
vec4 vc = vColor;
vec4 result;
#if defined( FAMILY_LEGO ) || defined( FAMILY_DARKLING_SPECULAR )
// LEGOPP_PixelCommon4: the base color by vertex color and texture as the technique's variant takes them
vec4 base = vec4( 1.0 );
#if defined( USE_MAP ) && defined( HAS_COLOR )
vec4 t = tex( map, vUv );
#ifdef MAP_TIMES_COLOR
base = vec4( t.rgb * vc.rgb, 1.0 );
#else
base = vec4( mix( vc.rgb, t.rgb, t.a ), vc.a );
#endif
#elif defined( USE_MAP )
base = vec4( tex( map, vUv ).rgb, 1.0 );
#elif defined( HAS_COLOR )
base = vc;
#endif
vec3 reflected = reflect( view, n );
vec3 refl = vdn * cube( vec3( reflected.x, -reflected.y, reflected.z ) ).rgb * 0.05;
float fres = fresnel( vdn );
vec3 spec = vLdn * pow( max( 0.0, dot( normalize( view + gameLightVec ), n ) ), 320.0 ) * gameSpecular;
#ifdef LIGHT_TIMES_COLOR
result = vec4( refl + spec + vLight + fres * base.rgb, base.a );
#else
result = vec4( refl + spec + ( vLight + fres ) * base.rgb, base.a );
#endif
#ifdef GRAYSCALE
float gray = 0.7 * ( 0.3 * result.r + 0.7 * result.g + 0.1 * result.b + 0.2 );
result.rgb = vec3( gray );
#endif
#ifdef GLOW
#ifdef IGNORE_VERTEX_ALPHA
result.a = 1.0;
#endif
result.rgb = glowColor * result.a;
#endif
#ifdef EMISSIVE
// The lit color goes to the base color by the vertex alpha times the material's emissive red (animated)
#if defined( HAS_COLOR )
float glow = vc.a * materialEmissive.r;
#else
float glow = materialEmissive.r;
#endif
#ifdef SUPER_EMISSIVE
result.rgb = mix( result.rgb, base.rgb * 10.0, glow );
#else
result.rgb = mix( result.rgb, base.rgb, glow );
#endif
#if defined( USE_MAP )
result.a = tex( map, vUv ).a;
#else
result.a = 1.0;
#endif
#endif
#ifdef SHINY_GLINT
// A glint sweeping up the object (the game moves attr_shinyGlintHeight itself)
float sweep = fract( gameTime * 0.25 ) * 12.0 - 2.0;
result.rgb += vec3( pow( clamp( 1.0 - abs( sweep - vObject.y ), 0.0, 1.0 ), 4.0 ) );
#endif
#ifdef FAMILY_DARKLING_SPECULAR
vec4 dark = tex( darkMap, vUvDark );
float centre = abs( materialEmissive.r - vc.a );
float window = 1.0 - clamp( ( min( centre, 1.0 - centre ) - materialEmissive.g * 0.5 ) * materialEmissive.b * 100.0, 0.0, 1.0 );
result = vec4( mix( result.rgb, dark.rgb, dark.a * window ), 1.0 );
#endif
#elif defined( FAMILY_DARKLING )
vec4 t = tex( map, vUv );
vec4 dark = tex( darkMap, vUvDark );
#ifdef NON_DECAL
vec3 combined = t.rgb * vc.rgb * vLight;
result = vec4( mix( combined, dark.rgb, dark.a * vc.a ), 1.0 );
#else
float centre = abs( materialEmissive.r - vc.a );
float window = 1.0 - clamp( ( min( centre, 1.0 - centre ) - materialEmissive.g * 0.5 ) * materialEmissive.b * 100.0, 0.0, 1.0 );
vec3 combined = mix( vc.rgb, t.rgb, t.a ) * vLight;
result = vec4( mix( combined, dark.rgb, dark.a * window ), 1.0 );
#endif
#elif defined( FAMILY_METAL )
// Lighting_PolishedMetal / Lighting_BrushedSteel: the lit tint plus the metal cube's reflection
vec4 tint = vc;
#ifdef USE_MAP
vec4 t = tex( map, vUv );
#ifdef HAS_COLOR
tint = vec4( vc.rgb * t.rgb, vc.a * t.a );
#else
tint = t;
#endif
#endif
vec3 reflected = reflect( view, n );
vec4 envColor = cube( reflected );
float reflIntensity = 1.0 - ( vLdn / 2.0 + 0.5 );
#ifdef BRUSHED
vec3 p = normalize( vObject );
vec4 noise = tex( noiseMap, vec2( p.x * ( p.x - p.z ), vObject.y / 5.0 ) );
float specIntensity = pow( max( 0.0, dot( normalize( view + gameLightVec ), n ) ), 100.0 * noise.a ) * 0.5;
vec3 coloured = envColor.rgb * noise.rgb * ( envColor.a - reflIntensity ) * tint.rgb * 2.0;
#else
float specIntensity = pow( max( 0.0, dot( normalize( view + gameLightVec ), n ) ), 50.0 );
vec3 coloured = envColor.rgb * ( envColor.a - reflIntensity ) * tint.rgb * 2.0;
#endif
result = vec4( vLight * tint.rgb + coloured + gameSpecular * specIntensity * vLdn, tint.a );
#elif defined( FAMILY_CLEAR_PLASTIC )
// ClearPlastic_PS: the reflection, a fresnel rim of the lit grey and a tight highlight; see-through facing the eye
float avdn = abs( vdn );
vec3 reflected = reflect( view, n );
vec3 refl = cube( vec3( reflected.x, -reflected.y, reflected.z ) ).rgb;
float ldn = dot( gameLightVec, n );
vec3 diffuse = clamp( vec3( max( 0.0, ldn ) ) + vec3( 0.7, 0.71, 0.75 ), 0.0, 1.0 );
float hdn = pow( max( 0.0, dot( normalize( view + gameLightVec ), n ) ), 120.0 ) * 4.19;
float fres = ( 0.1 + 0.9 * pow( 1.0 - avdn, 3.5 ) ) * 0.5;
result = clamp( vec4( refl + fres * diffuse * 2.5 + max( 0.0, ldn * hdn ), ( 1.0 - avdn ) * 0.5 ), 0.0, 1.0 );
#elif defined( FAMILY_OCEAN )
// Ocean_Distort: texture layers at 1/2, 3/4 and 1 scale, each warped by the one before, moving on their own
vec2 uv = vUv;
vec4 layer1 = tex( map, uv * 0.5 + vec2( gameTime * 0.011, gameTime * 0.004 ) );
vec2 warped = uv * 0.75 + vec2( -gameTime * 0.007, gameTime * 0.009 ) + layer1.rg * 0.2 - 0.5;
vec4 layer2 = tex( map, warped );
warped = uv + vec2( gameTime * 0.005, -gameTime * 0.006 ) + layer2.rg * 0.2 - 0.5;
vec4 layer3 = tex( map, warped );
vec4 water = ( layer1 + layer2 + layer3 ) * 0.3333;
#ifdef OCEAN_FX
float centre = abs( materialEmissive.r - water.a );
water.a *= 1.0 - clamp( ( min( centre, 1.0 - centre ) - materialEmissive.g * 0.5 ) * materialEmissive.b * 100.0, 0.0, 1.0 );
vec4 light = vc;
#elif defined( UNLIT )
vec4 light = vec4( vc.rgb * 2.0, vc.a );
#else
vec4 light = vec4( vLight * vc.rgb, vc.a );
#endif
result = water * light;
#elif defined( FAMILY_FLAT_SURF )
// Ocean_FlatSurf: the texture lit, its alpha from a second (differently moving) lookup
vec4 t = tex( map, vUv );
result = vec4( t.rgb * vLight * vc.rgb, tex( map, vUv * 0.5 + vec2( gameTime * 0.01 ) ).a );
#elif defined( FAMILY_BRICKWATER )
// BrickWater: hemisphere lit, fresnel and specular on the vertex color (the parallax normal map left out)
float fres = fresnel( vdn );
vec3 spec = vLdn * pow( max( 0.0, dot( normalize( view + gameLightVec ), n ) ), 320.0 ) * gameSpecular;
result = vec4( ( vLight + fres ) * vc.rgb + spec, 1.0 );
#elif defined( FAMILY_TERRAIN )
vec4 t = vec4( 1.0 );
#ifdef USE_MAP
t = tex( map, vUv );
#endif
#ifdef RIM_LIGHT
float rim = clamp( 2.0 * pow( 1.0 - clamp( 2.0 * vdn, 0.0, 1.0 ), 2.0 ), 0.0, 1.0 );
result = vec4( t.rgb * vc.rgb * ( vLight + rim ), 1.0 );
#elif defined( DIFFUSE_ONLY )
result = vec4( t.rgb * 2.0 * vLight, 1.0 );
#else
result = vec4( t.rgb * vc.rgb * vLight, 1.0 );
#endif
#else
// Basic, AlphaAsAlpha, fixed function, flairs, the sky: texture times the lit (or unlit) vertex color
vec4 light = vec4( vLight * vc.rgb, vc.a );
#ifdef LAYERS_BLENDED
result = vec4( mix( tex( darkMap, vUvDark ).rgb, tex( map, vUv ).rgb, vc.a ) * light.rgb, 1.0 );
#elif defined( LAYERS_ADDED )
result = ( tex( map, vUv ) * layerWeights.x + tex( darkMap, vUvDark ) * layerWeights.y ) * light;
#elif defined( BASIC_EMISSIVE )
vec4 t = tex( map, vUv );
result = vec4( vc.rgb * t.rgb * t.a + t.rgb * vc.rgb * ( gameLightColor * vLdn + gameAmbient ), vc.a * t.a );
#elif defined( USE_MAP )
vec4 t = tex( map, vUv );
#if defined( DECAL ) && defined( HAS_COLOR )
result = vec4( mix( vc.rgb, t.rgb, t.a ) * vLight, vc.a );
#elif defined( DECAL ) || defined( IGNORE_TEXTURE_ALPHA )
result = vec4( t.rgb, 1.0 ) * light;
#else
result = t * light;
#endif
#else
result = light;
#endif
#ifdef ANIM_ALPHA
result.a *= materialAlpha;
#endif
#endif
result = clamp( result, 0.0, 1.0 );
result.a *= opacity;
if ( result.a < alphaCutoff ) discard;
#ifdef USE_GAME_FOG
float fogAmount = gameFogOn * clamp( ( length( vWorld - cameraPosition ) - gameFogNear ) / max( 1.0, gameFogFar - gameFogNear ), 0.0, 1.0 );
result.rgb = mix( result.rgb, gameFogColor, fogAmount );
#endif
gl_FragColor = vec4( toLinear( result.rgb ), result.a );
#include <colorspace_fragment>
}
`;
// A 1x1 cube of one color, until the client's environment cube is loaded
function plainCube(value) {
const faces = [];
for (let i = 0; i < 6; i++) {
const face = new THREE.DataTexture(new Uint8Array([value, value, value, 255]), 1, 1, THREE.RGBAFormat);
face.needsUpdate = true;
faces.push(face);
}
const cube = new THREE.CubeTexture(faces);
cube.colorSpace = THREE.SRGBColorSpace;
cube.userData.shared = true;
cube.needsUpdate = true;
return cube;
}
const WHITE = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1, THREE.RGBAFormat);
WHITE.userData.shared = true;
WHITE.needsUpdate = true;
/**
* The shared state of the game's shaders: the zone's lights (setLights, blendTo), fog, time and the client's
* environment cubes, fetched from envUrl(name) ('reflection', 'polished', 'brushed', 'brushedNoise') when a material
* first needs them. material(look, mesh, maps) makes a mesh's material.
*/
export function createGameShading({ envUrl } = {}) {
const uniforms = {
gameLightColor: { value: new THREE.Vector3(1, 1, 1) }, gameAmbient: { value: new THREE.Vector3(0.4, 0.4, 0.4) },
gameLightVec: { value: new THREE.Vector3(0, 1, 0) }, gameUpperHemi: { value: new THREE.Vector3(1, 1, 1) },
gameSpecular: { value: new THREE.Vector3(0.3, 0.3, 0.3) },
gameLightOn: { value: 0 }, // 1 once a manifest brought the zone's lighting (the terrain uses its own light until then)
gameFogColor: { value: new THREE.Vector3(1, 1, 1) }, gameFogNear: { value: 0 }, gameFogFar: { value: 0 }, gameFogOn: { value: 0 },
gameTime: { value: 0 }
};
const envMaps = {
reflection: { value: plainCube(128), loading: null },
polished: { value: plainCube(160), loading: null },
brushed: { value: plainCube(140), loading: null },
brushedNoise: { value: WHITE, loading: null }
};
function loadEnv(name) {
const entry = envMaps[name];
if (entry.loading || !envUrl) return entry;
entry.loading = fetch(envUrl(name), { credentials: 'same-origin' }).then((r) => (r.ok ? r.arrayBuffer() : null)).then((buffer) => {
if (!buffer) return;
const cube = parseDdsCube(buffer, 256);
if (cube) {
const faces = cube.faces.map((face) => {
const texture = new THREE.DataTexture(face.data, face.width, face.height, THREE.RGBAFormat);
texture.needsUpdate = true;
return texture;
});
const texture = new THREE.CubeTexture(faces);
texture.colorSpace = THREE.SRGBColorSpace;
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.userData.shared = true;
texture.needsUpdate = true;
entry.value = texture;
} else {
// A plain DDS (the brushed noise)
const face = parseDdsCube(buffer, 256, true);
if (!face) return;
const texture = new THREE.DataTexture(face.data, face.width, face.height, THREE.RGBAFormat);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.colorSpace = THREE.SRGBColorSpace;
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.userData.shared = true;
texture.needsUpdate = true;
entry.value = texture;
}
for (const uniform of entry.users) uniform.value = entry.value;
}).catch(() => {});
return entry;
}
for (const entry of Object.values(envMaps)) entry.users = new Set();
// A uniform following an environment texture (swapped in place once it's loaded)
function envUniform(name) {
const entry = loadEnv(name);
const uniform = { value: entry.value };
entry.users.add(uniform);
return uniform;
}
/**
* The material for a mesh drawn with `look` (gameLook): maps {map, darkMap} (three.js textures or null), options
* {transparent, depthWrite, blending, side, alphaCutoff, opacity}.
*/
function material(look, mesh, maps, options) {
const defines = {};
// A darkling mesh without its dark texture is drawn as the LEGO shader it builds on
const family = look.family === 'darkling' && !maps.darkMap ? 'lego' : look.family;
const define = (name, on = true) => { if (on) defines[name] = ''; };
define('FAMILY_' + ({ lego: 'LEGO', metal: 'METAL', clearPlastic: 'CLEAR_PLASTIC', ocean: 'OCEAN', flatSurf: 'FLAT_SURF', brickWater: 'BRICKWATER',
darkling: look.flags & TECHNIQUE.SPECULAR ? 'DARKLING_SPECULAR' : 'DARKLING', terrain: 'TERRAIN', flair: 'FLAIR' }[family] || 'BASIC'));
const hasColor = !!look.vertexColors;
define('HAS_COLOR', hasColor);
define('USE_MAP', !!maps.map);
define('USE_DARK', !!maps.darkMap);
define('UNLIT', !look.lit);
// Metal without vertex colors takes the material's (Lighting_PolishedMetal_VS)
define('MATERIAL_COLOR', !!look.material || (family === 'metal' && !hasColor));
define('UV_ANIM', !!look.uvAnim && !!(mesh.uvScroll && (mesh.uvScroll[0] || mesh.uvScroll[1])));
define('NO_AMBIENT', !!(look.flags & TECHNIQUE.NO_AMBIENT));
// The LEGO "VertColor" variants multiply the light by the vertex color in the vertex shader
define('LIGHT_TIMES_COLOR', family === 'lego' && hasColor && !maps.map);
define('MAP_TIMES_COLOR', (!!look.emissive || !!(look.flags & TECHNIQUE.NON_DECAL)) && hasColor);
define('GRAYSCALE', !!(look.flags & TECHNIQUE.GRAYSCALE));
define('GLOW', !!(look.flags & TECHNIQUE.GLOW));
define('IGNORE_VERTEX_ALPHA', !!(look.flags & TECHNIQUE.IGNORE_VERTEX_ALPHA));
define('EMISSIVE', !!look.emissive);
define('SUPER_EMISSIVE', !!(look.flags & TECHNIQUE.SUPER_EMISSIVE));
define('SHINY_GLINT', !!(look.flags & TECHNIQUE.SHINY_GLINT));
define('NON_DECAL', !!(look.flags & TECHNIQUE.NON_DECAL));
define('BRUSHED', look.metal === 'brushed');
define('OCEAN_FX', !!(look.flags & TECHNIQUE.OCEAN_FX));
define('RIM_LIGHT', !!(look.flags & TECHNIQUE.RIM_LIGHT));
define('DIFFUSE_ONLY', !!(look.flags & TECHNIQUE.DIFFUSE_ONLY));
define('ANIM_ALPHA', !!(look.flags & TECHNIQUE.ANIM_ALPHA));
define('BASIC_EMISSIVE', !!(look.flags & TECHNIQUE.BASIC_EMISSIVE));
define('LAYERS_BLENDED', !!maps.darkMap && look.layers === 'blended');
define('LAYERS_ADDED', !!maps.darkMap && look.layers === 'added');
define('DECAL', look.textureAlpha === 'decal');
define('IGNORE_TEXTURE_ALPHA', look.textureAlpha === 'ignored');
define('USE_GAME_FOG', !(look.flags & TECHNIQUE.NO_FOG) && !look.sky);
const env = family === 'metal' ? envUniform(look.metal === 'brushed' ? 'brushed' : 'polished')
: family === 'lego' || family === 'clearPlastic' || (family === 'darkling' && look.flags & TECHNIQUE.SPECULAR) ? envUniform('reflection') : { value: envMaps.reflection.value };
const diffuse = mesh.diffuse || [1, 1, 1];
const emissive = mesh.emissive || [0, 0, 0];
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: VERTEX,
fragmentShader: FRAGMENT,
defines,
uniforms: {
...uniforms,
map: { value: maps.map || WHITE }, darkMap: { value: maps.darkMap || WHITE },
noiseMap: family === 'metal' && look.metal === 'brushed' ? envUniform('brushedNoise') : { value: WHITE },
envMap: env,
uvScroll: { value: new THREE.Vector2(...(mesh.uvScroll || [0, 0])) },
materialDiffuse: { value: new THREE.Vector3(...diffuse) }, materialAlpha: { value: mesh.alpha ?? 1 },
materialEmissive: { value: new THREE.Vector3(...emissive) },
layerWeights: { value: new THREE.Vector2(diffuse[0], diffuse[1]) },
glowColor: { value: new THREE.Vector3(0, 1, 1) },
alphaCutoff: { value: options.alphaCutoff || 0 },
opacity: { value: options.opacity ?? 1 }
},
vertexColors: hasColor,
transparent: !!options.transparent,
depthWrite: options.depthWrite !== false,
blending: options.blending ?? THREE.NormalBlending,
side: options.side ?? THREE.FrontSide
});
shaderMaterial.toneMapped = false;
shaderMaterial.userData.game = true;
return shaderMaterial;
}
return {
uniforms,
material,
/** The zone's lighting (manifest.lighting) at once. */
setLights(lighting) {
uniforms.gameLightOn.value = lighting ? 1 : 0;
if (!lighting) return;
uniforms.gameLightColor.value.fromArray(lighting.light);
uniforms.gameAmbient.value.fromArray(lighting.ambient);
uniforms.gameLightVec.value.fromArray(lighting.lightVec);
uniforms.gameUpperHemi.value.fromArray(lighting.upperHemi || [1, 1, 1]);
uniforms.gameSpecular.value.fromArray(lighting.specular || [0.3, 0.3, 0.3]);
uniforms.gameFogColor.value.fromArray(lighting.fogColor || [1, 1, 1]);
uniforms.gameFogNear.value = lighting.fogNear || 0;
uniforms.gameFogFar.value = lighting.fogFar || 0;
},
/** Fog as the zone's lighting has it (off by default: the views look from much further out than the game). */
setFog(on) { uniforms.gameFogOn.value = on ? 1 : 0; },
/** Seconds since the view started, for the moving textures. */
tick(dt) { uniforms.gameTime.value += dt; }
};
}

View File

@@ -305,11 +305,9 @@ export const TERRAIN_LOOKS = { textured: 0, colorMap: 1, blendMap: 2, scenes: 3
const TERRAIN_VERTEX = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorld;
void main() {
vUv = uv;
vNormal = normal;
vWorld = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`;
const TERRAIN_FRAGMENT = `
@@ -318,10 +316,6 @@ uniform vec3 lightDirection;
// The zone's lights as the game's TerrainDiffuse.fx gets them (scenery.js gameLights), when gameLightOn
uniform vec3 gameLightColor, gameAmbient, gameLightVec;
uniform float gameLightOn;
// ... and its fog, when the view turns it on (game-shaders.js)
uniform vec3 gameFogColor;
uniform float gameFogNear, gameFogFar, gameFogOn;
varying vec3 vWorld;
uniform int look;
uniform float sceneSize;
varying vec2 vUv;
@@ -348,9 +342,7 @@ void main() {
if (gameLightOn > 0.5) {
// TiledDetailDiffuse_4_PS: textures * diffuse map * 2 * (sun * N.L + ambient, clamped as a vertex color), * blend alpha
vec3 lit = clamp(gameLightColor * max(0.0, dot(normalize(vNormal), gameLightVec)) + gameAmbient, 0.0, 1.0);
vec3 terrain = min(color.rgb * tint * lit * blend.a, 1.0);
float fog = gameFogOn * clamp((length(vWorld - cameraPosition) - gameFogNear) / max(1.0, gameFogFar - gameFogNear), 0.0, 1.0);
gl_FragColor = vec4(mix(terrain, gameFogColor, fog), 1.0);
gl_FragColor = vec4(min(color.rgb * tint * lit * blend.a, 1.0), 1.0);
return;
}
gl_FragColor = vec4(min(color.rgb * tint, 1.0) * light * blend.a, 1.0);
@@ -411,10 +403,7 @@ export function buildTerrainChunks(data, loadTexture, lightDirection, gameLights
gameLightColor: gameLights ? gameLights.gameLightColor : { value: new THREE.Vector3() },
gameAmbient: gameLights ? gameLights.gameAmbient : { value: new THREE.Vector3() },
gameLightVec: gameLights ? gameLights.gameLightVec : { value: new THREE.Vector3(0, 1, 0) },
gameLightOn: gameLights ? gameLights.gameLightOn : { value: 0 },
gameFogColor: gameLights ? gameLights.gameFogColor : { value: new THREE.Vector3(1, 1, 1) },
gameFogNear: gameLights ? gameLights.gameFogNear : { value: 0 }, gameFogFar: gameLights ? gameLights.gameFogFar : { value: 0 },
gameFogOn: gameLights ? gameLights.gameFogOn : { value: 0 }
gameLightOn: gameLights ? gameLights.gameLightOn : { value: 0 }
}
});
const mesh = new THREE.Mesh(geometry, material);

View File

@@ -9,8 +9,7 @@
* follows the camera, far away, behind everything.
*/
import * as THREE from 'three';
import { parseModel, mergeMeshes, parseDds, decodeDxt, completeChain, linearColors, groupObjects, cellsOf, textureAlphaMode, gameLook, blendingOf, TECHNIQUE, decodeSceneMap, sceneAt, loadedScenes } from '/js/scenery-core.js';
import { createGameShading } from '/js/game-shaders.js';
import { parseModel, mergeMeshes, parseDds, decodeDxt, completeChain, linearColors, groupObjects, cellsOf, textureAlphaMode, gameLook, decodeSceneMap, sceneAt, loadedScenes } 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
@@ -135,11 +134,10 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
function geometryOf(mesh, look) {
const geometry = new THREE.BufferGeometry();
// The game's shaders take vertex colors as the file has them (game-shaders.js); three.js's own as linear
geometry.setAttribute('position', new THREE.BufferAttribute(mesh.positions, 3));
if (mesh.normals) geometry.setAttribute('normal', new THREE.BufferAttribute(mesh.normals, 3, true));
if (mesh.uvs) geometry.setAttribute('uv', new THREE.BufferAttribute(mesh.uvs, 2));
if (usesVertexColors(mesh, look)) geometry.setAttribute('color', new THREE.BufferAttribute(look ? mesh.colors : linearColors(mesh.colors), 4, true));
if (usesVertexColors(mesh, look)) geometry.setAttribute('color', new THREE.BufferAttribute(linearColors(mesh.colors), 4, true));
geometry.setIndex(new THREE.BufferAttribute(mesh.indices, 1));
if (!mesh.normals) geometry.computeVertexNormals();
geometry.computeBoundingSphere();
@@ -167,6 +165,23 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
#ifdef USE_MAP
diffuseColor.rgb *= texture2D( map, vMapUv ).rgb;
#endif
`;
// Two layer shaders: the dark texture (its own UV set) under the base one by the vertex alpha, or the two added
const TWO_LAYERS_BLENDED_FRAGMENT = `
#ifdef USE_MAP
#ifdef USE_COLOR_ALPHA
float layerMix = vColor.a;
#else
float layerMix = 1.0;
#endif
diffuseColor.rgb *= mix( texture2D( darkMap, vUvDark ).rgb, texture2D( map, vMapUv ).rgb, layerMix );
#endif
`;
const TWO_LAYERS_ADDED_FRAGMENT = `
#ifdef USE_MAP
diffuseColor *= texture2D( map, vMapUv ) * layerWeights.x + texture2D( darkMap, vUvDark ) * layerWeights.y;
#endif
`;
// The texture alpha mode's change to a fragment shader
@@ -185,90 +200,133 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
return material;
}
// The game's shaders (game-shaders.js) and the zone's lights as they get them (manifest.lighting), shared by every
// material and the terrain
const shading = createGameShading({ envUrl: (name) => '/api/scenery/env/' + name });
const gameLights = shading.uniforms;
// The zone's lights as the game's shaders get them (manifest.lighting), shared by every lit material
const gameLights = {
gameLightColor: { value: new THREE.Vector3(1, 1, 1) }, gameAmbient: { value: new THREE.Vector3() }, gameLightVec: { value: new THREE.Vector3(0, 1, 0) },
gameLightOn: { value: 0 } // 1 once a manifest brought the zone's lighting (the terrain uses its own light until then)
};
function setGameLights(lighting) {
shading.setLights(lighting);
gameLights.gameLightOn.value = lighting ? 1 : 0;
if (!lighting) return;
gameLights.gameLightColor.value.fromArray(lighting.light);
gameLights.gameAmbient.value.fromArray(lighting.ambient);
gameLights.gameLightVec.value.fromArray(lighting.lightVec);
lightBlend = null;
}
// A blend from the lights now to `lighting` over a second or two, as the client blends between scenes' lighting
const BLEND_SECONDS = 1.5;
let lightBlend = null;
const BLENDED = { light: 'gameLightColor', ambient: 'gameAmbient', lightVec: 'gameLightVec', upperHemi: 'gameUpperHemi', specular: 'gameSpecular', fogColor: 'gameFogColor' };
function blendLightsTo(lighting) {
if (!lighting) return;
const from = {}, to = {};
for (const [key, uniform] of Object.entries(BLENDED)) {
from[key] = gameLights[uniform].value.clone();
to[key] = lighting[key] ? new THREE.Vector3().fromArray(lighting[key]) : from[key].clone();
}
lightBlend = { from, to, fog: [gameLights.gameFogNear.value, gameLights.gameFogFar.value, lighting.fogNear || 0, lighting.fogFar || 0], t: 0 };
lightBlend = {
from: { light: gameLights.gameLightColor.value.clone(), ambient: gameLights.gameAmbient.value.clone(), lightVec: gameLights.gameLightVec.value.clone() },
to: { light: new THREE.Vector3().fromArray(lighting.light), ambient: new THREE.Vector3().fromArray(lighting.ambient), lightVec: new THREE.Vector3().fromArray(lighting.lightVec) },
t: 0
};
}
function stepLightBlend(dt) {
if (!lightBlend) return;
lightBlend.t = Math.min(1, lightBlend.t + dt / BLEND_SECONDS);
const { from, to, fog, t } = lightBlend;
for (const [key, uniform] of Object.entries(BLENDED)) gameLights[uniform].value.lerpVectors(from[key], to[key], t);
gameLights.gameLightVec.value.normalize();
gameLights.gameFogNear.value = fog[0] + (fog[2] - fog[0]) * t;
gameLights.gameFogFar.value = fog[1] + (fog[3] - fog[1]) * t;
const { from, to, t } = lightBlend;
gameLights.gameLightColor.value.lerpVectors(from.light, to.light, t);
gameLights.gameAmbient.value.lerpVectors(from.ambient, to.ambient, t);
gameLights.gameLightVec.value.lerpVectors(from.lightVec, to.lightVec, t).normalize();
if (t >= 1) lightBlend = null;
}
/**
* The material a mesh is drawn with: with the zone's lighting (look), the game's shader for it (game-shaders.js);
* without (older servers), a standard material lit by the view's own lights. The sky keeps its unlit look.
* Lighting per vertex as BasicShaders.fx and LEGOPPLighting.fx do it: sun * max(0, N.L) + ambient in the game's
* (sRGB) color space, which the vertex shader's color output clamps to 1, then made linear for three.js.
*/
const GAME_LIGHT_VERTEX = `#include <begin_vertex>
vec3 gameNormal = normal;
#ifdef USE_INSTANCING
gameNormal = mat3( instanceMatrix ) * gameNormal;
#endif
gameNormal = normalize( mat3( modelMatrix ) * gameNormal );
vGameLight = pow( clamp( gameLightColor * max( 0.0, dot( gameNormal, gameLightVec ) ) + gameAmbient, 0.0, 1.0 ), vec3( 2.2 ) );`;
// LEGO-Emissive: the lit color goes to the vertex color by the vertex alpha times the material's emissive red
const EMISSIVE_FRAGMENT = `
#ifdef USE_COLOR_ALPHA
outgoingLight = mix( outgoingLight, vColor.rgb, clamp( vColor.a * emissiveMix, 0.0, 1.0 ) );
#endif
#include <opaque_fragment>`;
/**
* A material that draws a mesh the way its game shader does (gameLook): unlit by the view's own lights and tone
* mapping, the zone's sun and ambient light per vertex when the shader is lit, the material's color only when the
* shader reads it. Metal is drawn lit like the rest (the game adds a reflection of its own textures); glowing
* meshes go to their vertex color as the emissive shader does.
*/
function gameMaterial(options, mesh, alphaMode, look, darkMap = null) {
const material = new THREE.MeshBasicMaterial({
...options,
color: look.material ? options.color : new THREE.Color(1, 1, 1)
});
material.toneMapped = false;
const layers = darkMap ? look.layers : null;
// TwoLayersAdded_PS: base * material diffuse red + dark * material diffuse green (their animations)
const weights = new THREE.Vector2(mesh.diffuse[0], mesh.diffuse[1]);
const emissive = !!look.emissive;
material.onBeforeCompile = (shader) => {
if (emissive) {
shader.uniforms.emissiveMix = { value: mesh.emissive[0] };
shader.fragmentShader = 'uniform float emissiveMix;\n' + shader.fragmentShader.replace('#include <opaque_fragment>', EMISSIVE_FRAGMENT);
}
if (layers) {
shader.uniforms.darkMap = { value: darkMap };
shader.uniforms.layerWeights = { value: weights };
shader.vertexShader = 'attribute vec2 uvDark;\nvarying vec2 vUvDark;\n' +
shader.vertexShader.replace('#include <uv_vertex>', '#include <uv_vertex>\n\tvUvDark = uvDark;');
shader.fragmentShader = 'uniform sampler2D darkMap;\nuniform vec2 layerWeights;\nvarying vec2 vUvDark;\n' + shader.fragmentShader
.replace('#include <map_fragment>', layers === 'blended' ? TWO_LAYERS_BLENDED_FRAGMENT : TWO_LAYERS_ADDED_FRAGMENT)
.replace('#include <color_fragment>', layers === 'blended' ? '#ifdef USE_COLOR_ALPHA\n\tdiffuseColor.rgb *= vColor.rgb;\n#endif' : '#include <color_fragment>');
} else if (options.map) {
textureAlphaPatch(shader, alphaMode);
}
if (!look.lit) return;
Object.assign(shader.uniforms, gameLights);
shader.vertexShader = 'uniform vec3 gameLightColor;\nuniform vec3 gameAmbient;\nuniform vec3 gameLightVec;\nvarying vec3 vGameLight;\n' +
shader.vertexShader.replace('#include <begin_vertex>', GAME_LIGHT_VERTEX);
shader.fragmentShader = 'varying vec3 vGameLight;\n' +
shader.fragmentShader.replace('#include <aomap_fragment>', '#include <aomap_fragment>\n\treflectedLight.indirectDiffuse *= vGameLight;');
};
material.customProgramCacheKey = () => 'game:' + (options.map ? alphaMode : '') + ':' + look.lit + ':' + layers + ':' + emissive;
return material;
}
function materialOf(mesh, map, forSky, alphaMode = 'opacity', look = null, darkMap = null) {
// 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 (only when the object's shader uses the texture's alpha as
// opacity). Blended meshes still write depth, as Gamebryo's default does.
const vertexColors = usesVertexColors(mesh, look);
let vertexAlpha = false;
// A two layer blend reads the vertex alpha as the mix of its textures, the emissive and darkling shaders as how
// much glows, not as opacity
const alphaIsOpacity = !(look && (look.emissive || look.family === 'darkling' || (darkMap && look.layers === 'blended')));
if (vertexColors && alphaIsOpacity) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250;
// A two layer blend reads the vertex alpha as the mix of its textures, not as opacity
const layersBlended = !!(darkMap && look.layers === 'blended');
// ... and the emissive shader as how much the vertex color glows
if (vertexColors && !layersBlended && !(look && look.emissive)) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250;
const textureAlpha = alphaMode === 'opacity' && !!(map && map.userData.alpha);
// The game's shaders take alpha from the vertex colors and texture only; NiMaterialProperty's is for fixed function
const materialAlpha = look && !look.material && !(look.flags & TECHNIQUE.ANIM_ALPHA) ? 1 : mesh.alpha;
const seeThrough = materialAlpha < 0.99 || vertexAlpha || textureAlpha || (look && (look.family === 'clearPlastic' || look.family === 'ocean' || look.family === 'flatSurf'));
const blending = blendingOf(look, mesh, seeThrough);
const side = blending.doubleSided ? THREE.DoubleSide : THREE.FrontSide;
if (look) {
return shading.material(look, mesh, { map, darkMap }, {
transparent: blending.transparent, depthWrite: blending.depthWrite && !forSky, blending: blending.additive ? THREE.AdditiveBlending : THREE.NormalBlending,
side, alphaCutoff: blending.alphaCutoff
});
}
const materialAlpha = look && !look.material ? 1 : mesh.alpha;
const seeThrough = mesh.blend && (materialAlpha < 0.99 || vertexAlpha || textureAlpha);
const options = {
color: new THREE.Color().setRGB(mesh.diffuse[0], mesh.diffuse[1], mesh.diffuse[2], THREE.SRGBColorSpace),
vertexColors,
transparent: blending.transparent,
transparent: seeThrough,
opacity: materialAlpha,
alphaTest: blending.alphaCutoff,
side,
alphaTest: mesh.test >= 0 ? Math.max(mesh.test / 255, 0.01) : 0,
side: mesh.doubleSided ? THREE.DoubleSide : THREE.FrontSide,
map: map || null
};
if (forSky) return useTextureAlpha(new THREE.MeshBasicMaterial({ ...options, depthWrite: false, fog: false }), alphaMode);
if (look) return gameMaterial(options, mesh, alphaMode, look, darkMap);
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 useTextureAlpha(material, alphaMode);
}
// A part's material and its own textures (a game shader's are in its uniforms; the shared ones stay)
function disposeMaterial(material) {
const own = [material.map, material.uniforms && material.uniforms.map.value, material.uniforms && material.uniforms.darkMap.value];
for (const texture of own) if (texture && !texture.userData.shared) texture.dispose();
material.dispose();
}
// The sky as Skydome.fx draws it: the texture times the vertex colors, unlit, moving as its texture transform says
function skyLook(mesh) {
return { family: 'basic', lit: false, texture: true, vertexColors: !!mesh.colors, material: false, layers: null, metal: null, emissive: false,
textureAlpha: 'opacity', uvAnim: true, flags: 0, blend: 'nif', doubleSided: false, hidden: false, sky: true };
}
async function buildParts(asset, lod, forSky) {
const buffer = await fetchBuffer(versioned(urls.mesh(manifest.zone, asset, lod)));
const model = parseModel(buffer);
@@ -276,9 +334,8 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
// The sky's layers keep their order; everything else has its look-alike pieces joined
for (const mesh of forSky ? model.meshes : mergeMeshes(model.meshes)) {
if (!mesh.vertices || !mesh.indices.length) continue;
const look = forSky ? (manifest.lighting ? skyLook(mesh) : null) : gameLook(manifest, asset, mesh);
// Post-processing and shadow shaders: the game draws nothing of these in the world
if (look && look.hidden) continue;
// The sky keeps its own unlit look
const look = forSky ? null : gameLook(manifest, asset, mesh);
const textureOf = async (slot, clampU, clampV) => {
const texture = await loadTexture(asset, slot, model.header.textures[slot], lod);
if (!texture) return null;
@@ -289,8 +346,8 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
return map;
};
const map = mesh.texture >= 0 && mesh.uv && (!look || look.texture) ? await textureOf(mesh.texture, mesh.clampU, mesh.clampV) : null;
// A two layer or darkling shader's second texture, on its own UV set
const darkMap = look && (look.layers || look.family === 'darkling') && map && mesh.darkTexture >= 0 && mesh.uvs2 ? await textureOf(mesh.darkTexture, false, false) : null;
// A two layer shader's second texture, on its own UV set
const darkMap = look && look.layers && map && mesh.darkTexture >= 0 && mesh.uvs2 ? await textureOf(mesh.darkTexture, false, false) : null;
const geometry = geometryOf(mesh, look);
if (darkMap) geometry.setAttribute('uvDark', new THREE.BufferAttribute(mesh.uvs2, 2));
parts.push({ geometry, material: materialOf(mesh, map, forSky, textureAlphaMode(manifest, asset, mesh), look, darkMap) });
@@ -309,7 +366,7 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
try {
const { parts, radius, center } = await buildParts(asset, detail.lod, false);
if (mine !== generation) {
parts.forEach((p) => { p.geometry.dispose(); disposeMaterial(p.material); });
parts.forEach((p) => { p.geometry.dispose(); if (p.material.map) p.material.map.dispose(); p.material.dispose(); });
return;
}
const instances = byAsset.get(asset) || [];
@@ -469,13 +526,14 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
for (const part of entry.parts || []) {
if (part.ghost) part.ghost.dispose();
part.geometry.dispose();
disposeMaterial(part.material);
if (part.material.map) part.material.map.dispose();
part.material.dispose();
}
}
assets.clear();
for (const promise of textures.values()) promise.then((t) => t && t.dispose());
textures.clear();
sky.children.slice().forEach((mesh) => { sky.remove(mesh); mesh.geometry.dispose(); disposeMaterial(mesh.material); });
sky.children.slice().forEach((mesh) => { sky.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); });
used = 0;
report();
}
@@ -542,7 +600,6 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
sky.position.copy(camera.position);
sky.scale.setScalar(camera.far * 0.8);
stepLightBlend(dt);
shading.tick(dt);
lastUpdate += dt;
if (lastUpdate < UPDATE_SECONDS) return;
lastUpdate = 0;
@@ -566,8 +623,6 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
sceneState() {
return { mode: sceneMode, scene: focusScene, shown: shownScenes, scenes: manifest && manifest.scenes ? manifest.scenes : [] };
},
/** Fog as the zone's lighting has it (off by default: the views look from much further out than the game). */
setFog(on) { shading.setFog(on); },
/** The zone's lights as uniforms (updated in place, blends too), for the terrain to be lit like the scenery. */
gameLights() { return gameLights; },
/** How many objects the loaded manifest places. */

View File

@@ -133,8 +133,6 @@ function showScenery() {
const on = $('sceneryToggle').checked, flairsOn = on && $('flairToggle').checked;
scenery.setEnabled(on);
scenery.setSky($('skyToggle').checked);
scenery.setFog($('fogToggle').checked);
flairs.setFog($('fogToggle').checked);
scenery.setShowHidden($('hiddenToggle').checked);
flairs.setEnabled(flairsOn);
applyMarkers();
@@ -723,7 +721,6 @@ $('sceneryToggle').addEventListener('change', showScenery);
$('sceneryDetail').addEventListener('change', showScenery);
$('flairToggle').addEventListener('change', showScenery);
$('skyToggle').addEventListener('change', showScenery);
$('fogToggle').addEventListener('change', showScenery);
$('hiddenToggle').addEventListener('change', showScenery);
$('modelMarkersToggle').addEventListener('change', applyMarkers);
$('pathsToggle').addEventListener('change', showPaths);

View File

@@ -105,7 +105,6 @@
<div class="form-check form-switch mb-0 small" id="boundaryWrap"><input class="form-check-input" type="checkbox" id="boundaryToggle" data-pref="property3d.boundary" checked><label class="form-check-label" for="boundaryToggle">Build area</label></div>
<div class="form-check form-switch mb-0 small"><input class="form-check-input" type="checkbox" id="shadowToggle" data-pref="property3d.shadows" checked><label class="form-check-label" for="shadowToggle">Shadows</label></div>
<div class="form-check form-switch mb-0 small" title="The zone's sky, as the game draws it (with Scenery on)"><input class="form-check-input" type="checkbox" id="skyToggle" data-pref="property3d.sky" checked><label class="form-check-label" for="skyToggle">Sky</label></div>
<div class="form-check form-switch mb-0 small" title="The zone's fog, as the game draws it around the player (with Scenery on)"><input class="form-check-input" type="checkbox" id="fogToggle" data-pref="property3d.fog"><label class="form-check-label" for="fogToggle">Fog</label></div>
<div class="form-check form-switch mb-0 small" title="The models placed on the property"><input class="form-check-input" type="checkbox" id="modelsToggle" data-pref="property3d.models" checked><label class="form-check-label" for="modelsToggle">Placed models</label></div>
{% if not exists("showcase") %}<div class="form-check form-switch mb-0 small d-none" id="generatedWrap" title="Draw player-built models as the UGC server made them for the game (on: generated models; off: built from their LXFML). Models it hasn't made yet are built from their LXFML either way"><input class="form-check-input" type="checkbox" id="generatedToggle" data-pref="property3d.generated" checked><label class="form-check-label" for="generatedToggle">Generated models</label></div>{% endif %}
<div class="form-check form-switch mb-0 small" title="Also draw, see-through, the models of objects the game doesn't draw: trigger and blocking volumes (with Scenery on)"><input class="form-check-input" type="checkbox" id="hiddenToggle" data-pref="property3d.hidden"><label class="form-check-label" for="hiddenToggle">Hidden objects</label></div>
@@ -388,7 +387,6 @@ async function showScenery() {
const toggle = document.getElementById('sceneryToggle');
scenery.setEnabled(toggle.checked);
scenery.setSky(document.getElementById('skyToggle').checked);
scenery.setFog(document.getElementById('fogToggle').checked);
scenery.setShowHidden(document.getElementById('hiddenToggle').checked);
if (!toggle.checked) return;
const lod = parseInt(document.getElementById('lodSelect').value, 10);
@@ -473,7 +471,6 @@ document.getElementById('terrainToggle').addEventListener('change', () => showTe
document.getElementById('boundaryToggle').addEventListener('change', showBoundary);
document.getElementById('sceneryToggle').addEventListener('change', showScenery);
document.getElementById('skyToggle').addEventListener('change', showScenery);
document.getElementById('fogToggle').addEventListener('change', showScenery);
document.getElementById('hiddenToggle').addEventListener('change', showScenery);
document.getElementById('modelsToggle').addEventListener('change', (e) => viewer.setModelsVisible(e.target.checked));
if (document.getElementById('generatedToggle')) document.getElementById('generatedToggle').addEventListener('change', () => { loadedLod = null; loadScene(); });

View File

@@ -151,7 +151,6 @@
</select>
<div class="form-check form-switch mb-0 small"><input class="form-check-input" type="checkbox" id="labelsToggle" data-pref="world3d.labels" checked><label class="form-check-label" for="labelsToggle">Names</label></div>
<div class="form-check form-switch mb-0 small" title="The zone's sky, as the game draws it (with Scenery on)"><input class="form-check-input" type="checkbox" id="skyToggle" data-pref="world3d.sky" checked><label class="form-check-label" for="skyToggle">Sky</label></div>
<div class="form-check form-switch mb-0 small" title="The zone's fog, as the game draws it around the player (with Scenery on)"><input class="form-check-input" type="checkbox" id="fogToggle" data-pref="world3d.fog"><label class="form-check-label" for="fogToggle">Fog</label></div>
<div class="form-check form-switch mb-0 small" title="Also draw, see-through, the models of objects the game doesn't draw: trigger and blocking volumes (with Scenery on)"><input class="form-check-input" type="checkbox" id="hiddenToggle" data-pref="world3d.hidden"><label class="form-check-label" for="hiddenToggle">Hidden objects</label></div>
</div>
<div class="fw-semibold mb-1">Scenes</div>