mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-10 23:03:44 +00:00
Revert "feat(dashboard): the game's own shaders in the world and property 3D views"
This reverts commit acc4853756.
This commit is contained in:
@@ -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; }
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -717,8 +717,7 @@ 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. **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
|
||||
client never loads) are left out; **Hidden objects** shows them see-through in pink for debugging. 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.
|
||||
@@ -1053,8 +1052,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, Fog (off by default), 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, 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
|
||||
@@ -1072,39 +1071,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user