diff --git a/dDashboardServer/static/js/game-shaders.js b/dDashboardServer/static/js/game-shaders.js new file mode 100644 index 000000000..b20e7bd2c --- /dev/null +++ b/dDashboardServer/static/js/game-shaders.js @@ -0,0 +1,528 @@ +/** + * 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 +} +`; + +// 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; } + }; +} diff --git a/dDashboardServer/static/js/lddviewer.js b/dDashboardServer/static/js/lddviewer.js index b7eb5a30f..a43141630 100644 --- a/dDashboardServer/static/js/lddviewer.js +++ b/dDashboardServer/static/js/lddviewer.js @@ -305,9 +305,11 @@ 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 = ` @@ -316,6 +318,10 @@ 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; @@ -342,7 +348,9 @@ 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); - gl_FragColor = vec4(min(color.rgb * tint * lit * blend.a, 1.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); return; } gl_FragColor = vec4(min(color.rgb * tint, 1.0) * light * blend.a, 1.0); @@ -403,7 +411,10 @@ 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 } + 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 } } }); const mesh = new THREE.Mesh(geometry, material); diff --git a/dDashboardServer/static/js/scenery.js b/dDashboardServer/static/js/scenery.js index 8f265fe3f..3c8664df7 100644 --- a/dDashboardServer/static/js/scenery.js +++ b/dDashboardServer/static/js/scenery.js @@ -9,7 +9,8 @@ * follows the camera, far away, behind everything. */ import * as THREE from 'three'; -import { parseModel, mergeMeshes, parseDds, decodeDxt, completeChain, linearColors, groupObjects, cellsOf, textureAlphaMode, gameLook, decodeSceneMap, sceneAt, loadedScenes } from '/js/scenery-core.js'; +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'; // 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 @@ -134,10 +135,11 @@ 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(linearColors(mesh.colors), 4, true)); + if (usesVertexColors(mesh, look)) geometry.setAttribute('color', new THREE.BufferAttribute(look ? mesh.colors : linearColors(mesh.colors), 4, true)); geometry.setIndex(new THREE.BufferAttribute(mesh.indices, 1)); if (!mesh.normals) geometry.computeVertexNormals(); geometry.computeBoundingSphere(); @@ -165,23 +167,6 @@ 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 @@ -200,133 +185,90 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress return material; } - // 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) - }; + // 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; function setGameLights(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); + shading.setLights(lighting); 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; - 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 - }; + 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 }; } function stepLightBlend(dt) { if (!lightBlend) return; lightBlend.t = Math.min(1, lightBlend.t + dt / BLEND_SECONDS); - 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(); + 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; if (t >= 1) lightBlend = null; } /** - * 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. + * 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. */ - const GAME_LIGHT_VERTEX = `#include - 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 `; - - /** - * 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 ', 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 ', '#include \n\tvUvDark = uvDark;'); - shader.fragmentShader = 'uniform sampler2D darkMap;\nuniform vec2 layerWeights;\nvarying vec2 vUvDark;\n' + shader.fragmentShader - .replace('#include ', layers === 'blended' ? TWO_LAYERS_BLENDED_FRAGMENT : TWO_LAYERS_ADDED_FRAGMENT) - .replace('#include ', layers === 'blended' ? '#ifdef USE_COLOR_ALPHA\n\tdiffuseColor.rgb *= vColor.rgb;\n#endif' : '#include '); - } 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 ', GAME_LIGHT_VERTEX); - shader.fragmentShader = 'varying vec3 vGameLight;\n' + - shader.fragmentShader.replace('#include ', '#include \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, 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; + // 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; 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 ? 1 : mesh.alpha; - const seeThrough = mesh.blend && (materialAlpha < 0.99 || vertexAlpha || textureAlpha); + 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 options = { color: new THREE.Color().setRGB(mesh.diffuse[0], mesh.diffuse[1], mesh.diffuse[2], THREE.SRGBColorSpace), vertexColors, - transparent: seeThrough, + transparent: blending.transparent, opacity: materialAlpha, - alphaTest: mesh.test >= 0 ? Math.max(mesh.test / 255, 0.01) : 0, - side: mesh.doubleSided ? THREE.DoubleSide : THREE.FrontSide, + alphaTest: blending.alphaCutoff, + side, 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); @@ -334,8 +276,9 @@ 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; - // The sky keeps its own unlit look - const look = forSky ? null : gameLook(manifest, asset, mesh); + 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; const textureOf = async (slot, clampU, clampV) => { const texture = await loadTexture(asset, slot, model.header.textures[slot], lod); if (!texture) return null; @@ -346,8 +289,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 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; + // 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; 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) }); @@ -366,7 +309,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(); if (p.material.map) p.material.map.dispose(); p.material.dispose(); }); + parts.forEach((p) => { p.geometry.dispose(); disposeMaterial(p.material); }); return; } const instances = byAsset.get(asset) || []; @@ -526,14 +469,13 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress for (const part of entry.parts || []) { if (part.ghost) part.ghost.dispose(); part.geometry.dispose(); - if (part.material.map) part.material.map.dispose(); - part.material.dispose(); + disposeMaterial(part.material); } } 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(); mesh.material.dispose(); }); + sky.children.slice().forEach((mesh) => { sky.remove(mesh); mesh.geometry.dispose(); disposeMaterial(mesh.material); }); used = 0; report(); } @@ -600,6 +542,7 @@ 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; @@ -623,6 +566,8 @@ 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. */ diff --git a/dDashboardServer/static/js/world3d.js b/dDashboardServer/static/js/world3d.js index de785785c..41637b7ee 100644 --- a/dDashboardServer/static/js/world3d.js +++ b/dDashboardServer/static/js/world3d.js @@ -133,6 +133,8 @@ 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(); @@ -721,6 +723,7 @@ $('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); diff --git a/dDashboardServer/templates/property-3d.jinja2 b/dDashboardServer/templates/property-3d.jinja2 index 780dff560..4830491fc 100644 --- a/dDashboardServer/templates/property-3d.jinja2 +++ b/dDashboardServer/templates/property-3d.jinja2 @@ -105,6 +105,7 @@
+
{% if not exists("showcase") %}
{% endif %}
@@ -387,6 +388,7 @@ 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); @@ -471,6 +473,7 @@ 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(); }); diff --git a/dDashboardServer/templates/world3d.jinja2 b/dDashboardServer/templates/world3d.jinja2 index 60cbe6569..ddef7fb1d 100644 --- a/dDashboardServer/templates/world3d.jinja2 +++ b/dDashboardServer/templates/world3d.jinja2 @@ -151,6 +151,7 @@
+
Scenes
diff --git a/docs/Dashboard.md b/docs/Dashboard.md index 121e0d06a..b255e4375 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -717,7 +717,8 @@ The view fills the window (the page doesn't scroll; the wheel only zooms). View - **Scenery** (on by default) draws every object in the zone's scene files with its model, from the client's .nif files, as the property view does; **Sky** switches the zone's sky. Objects the game doesn't draw (trigger and blocking volumes: blocking-volume types, `renderDisabled`/`CreateNULLRender`, and `carver_only` objects, which the - client never loads) are left out; **Hidden objects** shows them see-through in pink for debugging. Models load nearest first around where the camera + client never loads) are left out; **Hidden objects** shows them see-through in pink for debugging. **Fog** (off by + default) adds the zone's fog. Models are drawn with the game's shaders (see *Game shaders* under Properties in 3D). Models load nearest first around where the camera looks; **High / Medium / Low detail** sets how far they're drawn and how sharp (Medium by default). Objects drawn with a model get no marker; **Markers on objects with a model** (Layers tab) brings the markers back. They can be clicked either way. @@ -1052,8 +1053,8 @@ changes); sounds, spawned enemies and drops are listed in the log. Actions the s red. Terrain needs `client_location` to be set. **Layers.** The property 3D view (and the showcase's) fills the window with its panels scrolling inside; its **Layers** -tab has the same switches as World 3D where they apply: detail, Terrain, Scenery, Sky, Placed models, Build area, -Shadows and Hidden objects (off by default), remembered per account. +tab has the same switches as World 3D where they apply: detail, Terrain, Scenery, Sky, Fog (off by default), Placed +models, Build area, Shadows and Hidden objects (off by default), remembered per account. **Generated models.** When the UGC server has made a player-built model, the property 3D view draws it from the model the UGC server made (the NIF the game client gets), and builds the rest from their LXFML as before; switch @@ -1071,6 +1072,39 @@ shader, or for a multishaded model the `S05__`-style tag in each part's name), n lay the texture over the vertex colors and terrain meshes and LEGO items ignore its alpha, so their textures' alpha channels (often gloss or leftover masks) don't punch holes in them. +**Game shaders.** Scenery, sky and flairs are drawn with ports of the client's shaders (`res/shaders/*.fx`) to WebGL +(`static/js/game-shaders.js`), one program per technique family and variant, shared by every mesh that uses it. Which +technique a shader (mapShaders `gameValue`) uses is one table, `NifFile::TechniqueFor` (dCommon); the manifests carry +it as `techniques` (gameValue -> family, eShaderLook bits, texture alpha, eTechniqueFlag bits). A gameValue the +table lacks is drawn as LEGO, as the client falls back to it. The math runs on sRGB values as Direct3D 9 did, with the +zone's sun, ambient, upper hemisphere and specular colors from the scene files, blended as the camera moves between +scenes. + +| Family | gameValues | Drawn as | +| --- | --- | --- | +| LEGO (LEGOPPLighting) | 4, 5, 12, 14, 19, 20, 22, 25-31, 48, 50, 53, 72, 88, 92 | hemisphere lit sun + ambient, fresnel rim, N.H^320 specular, default reflection cube x0.05; texture over vertex colors by its alpha (decal) or times them (Item, Masked NonDecal, FaceCreate); Emissive/SuperEmissive (vertex alpha x emissive red), Glow, Grayscale, NoAmbient, AnimUV | +| Basic (BasicShaders, AlphaAsAlpha) | 7-11, 13, 15-18, 23, 24, 32-39, 49, 52, 54-65, 68, 70, 73, 80-87, 91, 93, 94, 105-108 | sun x N.L + ambient per vertex (or unlit) x vertex color x texture; AlphaAsAlpha both sides; AlphaBlend without depth writes, AlphaTest cut out, Additive; material alpha for the AnimAlpha ones; two layer blends and adds | +| Metal (Metallic.fx) | 98, 99, 100 | 0.7 N.L^4 + 0.3, plus the client's metal cube (polished or brushed, with the brushed noise in object space) tinted by the vertex color, specular | +| Clear Plastic | 6 | default reflection cube, fresnel of the lit grey, tight highlight; see-through facing the eye | +| Ocean (Distortion) | 69, 89, 90, 95, 101 | three texture layers, each warped by the one before, lit (or unlit x2, or FX with the emissive alpha window) | +| Flat Surf | 78 | texture lit, alpha from a second lookup | +| BrickWater | 51 | hemisphere lit vertex color, fresnel, specular | +| Darkling | 75-77, 102-104 | dark texture on the second UV set over the base look by a window of vertex alphas (material emissive r/g/b); Specular uses LEGO lighting, Structure multiplies | +| Terrain meshes | 2, 3, 97 | texture x vertex color x (sun x N.L + ambient); Rim Light adds a rim; Diffuse Map Only doubles the texture | +| Flairs (Flair.fx) | the flair manifest | (0.85 sun + ambient) x tint | +| Sky (Skydome.fx) | the scene's sky | texture x vertex color, unlit, moving by its texture transform | +| Not drawn | 21, 71, 74, 79, 96 | model footprints, post-processing, drop shadows, Undefined | +| Fixed function | -1 | sun x N.L + ambient with the material color and NiVertexColorProperty | + +Moving textures (the AnimUV, ScrollingUV and Distortion shaders, the sky) scroll as the .nif's texture transform +controllers say. Fog (Layers, off by default: the views look from further out than the game's camera) mixes in the +zone's fog color from its near to far distance, terrain included. Approximated or left out: shadows, the LEGO parallax +and normal maps (BrickWater), Ocean's layer motion (the client moves its layers by timers of its own), the shiny glint +(a band moving up the object), Reveal and FadeUp masks (drawn fully shown), Glow's color (the shader's default cyan), +Powerups, Orb, TV Screen and Head Icon effects (drawn as LEGO or unlit), Flair's distance fade and movement. The +client's environment textures come from `/api/scenery/env/:name` (`reflection`, `polished`, `brushed`, +`brushedNoise`). Placed player models are drawn by the viewer's own lighting. + Converting a model the caches don't have yet (a big "glom" file takes a moment) happens on a few worker threads, so the dashboard keeps answering everything else meanwhile: the route hands the request to a worker (`Web::Defer`) and the web thread sends the answer when it is ready. Flairs and small models (up to 256 KB) go first, and one of the