diff --git a/src/assets/waternormals.jpg b/src/assets/waternormals.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9dfe03ce483a02fff31c35421094c53a333097bc Binary files /dev/null and b/src/assets/waternormals.jpg differ diff --git a/src/components/layers.js b/src/components/layers.js new file mode 100644 index 0000000000000000000000000000000000000000..2838cf754efd2c525cfa5508246f230449f5c895 --- /dev/null +++ b/src/components/layers.js @@ -0,0 +1,30 @@ +export const Layers = { + // Layers 0 - 2 reserverd by ThreeJS and AFrame. + reflection: 3 +}; + +AFRAME.registerComponent("layers", { + schema: { + reflection: { type: "boolean", default: false } + }, + init() { + this.update = this.update.bind(this); + this.el.addEventListener("model-loaded", this.update); + }, + update(oldData) { + if (this.data.reflection !== oldData.reflection) { + if (this.data.reflection) { + this.el.object3D.traverse(obj => { + obj.layers.enable(Layers.reflection); + }); + } else { + this.el.object3D.traverse(obj => { + obj.layers.disable(Layers.reflection); + }); + } + } + }, + remove() { + this.el.removeEventListener("model-loaded", this.update); + } +}); diff --git a/src/components/skybox.js b/src/components/skybox.js new file mode 100644 index 0000000000000000000000000000000000000000..e5f26b12fe93d006fa15920378223dc1d566243b --- /dev/null +++ b/src/components/skybox.js @@ -0,0 +1,280 @@ +/** + * @author zz85 / https://github.com/zz85 + * + * Based on "A Practical Analytic Model for Daylight" + * aka The Preetham Model, the de facto standard analytic skydome model + * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf + * + * First implemented by Simon Wallner + * http://www.simonwallner.at/projects/atmospheric-scattering + * + * Improved by Martin Upitis + * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR + * + * Three.js integration by zz85 http://twitter.com/blurspline + */ + +THREE.Sky = function() { + const shader = THREE.Sky.SkyShader; + + const material = new THREE.ShaderMaterial({ + fragmentShader: shader.fragmentShader, + vertexShader: shader.vertexShader, + uniforms: THREE.UniformsUtils.clone(shader.uniforms), + side: THREE.BackSide + }); + + THREE.Mesh.call(this, new THREE.BoxBufferGeometry(1, 1, 1), material); +}; + +THREE.Sky.prototype = Object.create(THREE.Mesh.prototype); +THREE.Sky.prototype.constructor = THREE.Sky; + +THREE.Sky.SkyShader = { + uniforms: { + luminance: { value: 1 }, + turbidity: { value: 2 }, + rayleigh: { value: 1 }, + mieCoefficient: { value: 0.005 }, + mieDirectionalG: { value: 0.8 }, + sunPosition: { value: new THREE.Vector3() } + }, + + vertexShader: [ + "uniform vec3 sunPosition;", + "uniform float rayleigh;", + "uniform float turbidity;", + "uniform float mieCoefficient;", + + "varying vec3 vWorldPosition;", + "varying vec3 vSunDirection;", + "varying float vSunfade;", + "varying vec3 vBetaR;", + "varying vec3 vBetaM;", + "varying float vSunE;", + + "const vec3 up = vec3( 0.0, 1.0, 0.0 );", + + // constants for atmospheric scattering + "const float e = 2.71828182845904523536028747135266249775724709369995957;", + "const float pi = 3.141592653589793238462643383279502884197169;", + + // wavelength of used primaries, according to preetham + "const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );", + // this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function: + // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn)) + "const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );", + + // mie stuff + // K coefficient for the primaries + "const float v = 4.0;", + "const vec3 K = vec3( 0.686, 0.678, 0.666 );", + // MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K + "const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );", + + // earth shadow hack + // cutoffAngle = pi / 1.95; + "const float cutoffAngle = 1.6110731556870734;", + "const float steepness = 1.5;", + "const float EE = 1000.0;", + + "float sunIntensity( float zenithAngleCos ) {", + " zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );", + " return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );", + "}", + + "vec3 totalMie( float T ) {", + " float c = ( 0.2 * T ) * 10E-18;", + " return 0.434 * c * MieConst;", + "}", + + "void main() {", + + " vec4 worldPosition = modelMatrix * vec4( position, 1.0 );", + " vWorldPosition = worldPosition.xyz;", + + " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", + + " vSunDirection = normalize( sunPosition );", + + " vSunE = sunIntensity( dot( vSunDirection, up ) );", + + " vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );", + + " float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );", + + // extinction (absorbtion + out scattering) + // rayleigh coefficients + " vBetaR = totalRayleigh * rayleighCoefficient;", + + // mie coefficients + " vBetaM = totalMie( turbidity ) * mieCoefficient;", + + "}" + ].join("\n"), + + fragmentShader: [ + "varying vec3 vWorldPosition;", + "varying vec3 vSunDirection;", + "varying float vSunfade;", + "varying vec3 vBetaR;", + "varying vec3 vBetaM;", + "varying float vSunE;", + + "uniform float luminance;", + "uniform float mieDirectionalG;", + + "const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );", + + // constants for atmospheric scattering + "const float pi = 3.141592653589793238462643383279502884197169;", + + "const float n = 1.0003;", // refractive index of air + "const float N = 2.545E25;", // number of molecules per unit volume for air at + // 288.15K and 1013mb (sea level -45 celsius) + + // optical length at zenith for molecules + "const float rayleighZenithLength = 8.4E3;", + "const float mieZenithLength = 1.25E3;", + "const vec3 up = vec3( 0.0, 1.0, 0.0 );", + // 66 arc seconds -> degrees, and the cosine of that + "const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;", + + // 3.0 / ( 16.0 * pi ) + "const float THREE_OVER_SIXTEENPI = 0.05968310365946075;", + // 1.0 / ( 4.0 * pi ) + "const float ONE_OVER_FOURPI = 0.07957747154594767;", + + "float rayleighPhase( float cosTheta ) {", + " return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );", + "}", + + "float hgPhase( float cosTheta, float g ) {", + " float g2 = pow( g, 2.0 );", + " float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );", + " return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );", + "}", + + // Filmic ToneMapping http://filmicgames.com/archives/75 + "const float A = 0.15;", + "const float B = 0.50;", + "const float C = 0.10;", + "const float D = 0.20;", + "const float E = 0.02;", + "const float F = 0.30;", + + "const float whiteScale = 1.0748724675633854;", // 1.0 / Uncharted2Tonemap(1000.0) + + "vec3 Uncharted2Tonemap( vec3 x ) {", + " return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;", + "}", + + "void main() {", + // optical length + // cutoff angle at 90 to avoid singularity in next formula. + " float zenithAngle = acos( max( 0.0, dot( up, normalize( vWorldPosition - cameraPos ) ) ) );", + " float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );", + " float sR = rayleighZenithLength * inverse;", + " float sM = mieZenithLength * inverse;", + + // combined extinction factor + " vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );", + + // in scattering + " float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );", + + " float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );", + " vec3 betaRTheta = vBetaR * rPhase;", + + " float mPhase = hgPhase( cosTheta, mieDirectionalG );", + " vec3 betaMTheta = vBetaM * mPhase;", + + " vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );", + " Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );", + + // nightsky + " vec3 direction = normalize( vWorldPosition - cameraPos );", + " float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]", + " float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]", + " vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );", + " vec3 L0 = vec3( 0.1 ) * Fex;", + + // composition + solar disc + " float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );", + " L0 += ( vSunE * 19000.0 * Fex ) * sundisk;", + + " vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );", + + " vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );", + " vec3 color = curr * whiteScale;", + + " vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );", + + " gl_FragColor = vec4( retColor, 1.0 );", + + "}" + ].join("\n") +}; + +AFRAME.registerComponent("skybox", { + schema: { + turbidity: { type: "number", default: 10 }, + rayleigh: { type: "number", default: 2 }, + luminance: { type: "number", default: 1 }, + mieCoefficient: { type: "number", default: 0.005 }, + mieDirectionalG: { type: "number", default: 0.8 }, + inclination: { type: "number", default: 0 }, + azimuth: { type: "number", default: 0 }, + distance: { type: "number", default: 8000 } + }, + + init() { + this.sky = new THREE.Sky(); + this.el.setObject3D("mesh", this.sky); + }, + + update(oldData) { + const uniforms = this.sky.material.uniforms; + + if (this.data.turbidity !== oldData.turbidity) { + uniforms.turbidity.value = this.data.turbidity; + } + + if (this.data.rayleigh !== oldData.rayleigh) { + uniforms.rayleigh.value = this.data.rayleigh; + } + + if (this.data.luminance !== oldData.luminance) { + uniforms.luminance.value = this.data.luminance; + } + + if (this.data.mieCoefficient !== oldData.mieCoefficient) { + uniforms.mieCoefficient.value = this.data.mieCoefficient; + } + + if (this.data.mieDirectionalG !== oldData.mieDirectionalG) { + uniforms.mieDirectionalG.value = this.data.mieDirectionalG; + } + + if ( + this.data.inclination !== oldData.inclination || + this.data.azimuth !== oldData.azimuth || + this.data.distance !== oldData.distance + ) { + const theta = Math.PI * (this.data.inclination - 0.5); + const phi = 2 * Math.PI * (this.data.azimuth - 0.5); + + const distance = this.data.distance; + + const x = distance * Math.cos(phi); + const y = distance * Math.sin(phi) * Math.sin(theta); + const z = distance * Math.sin(phi) * Math.cos(theta); + + uniforms.sunPosition.value.set(x, y, z).normalize(); + } + }, + + remove() { + this.el.removeObject3D("mesh"); + } +}); diff --git a/src/components/water.js b/src/components/water.js new file mode 100644 index 0000000000000000000000000000000000000000..9152e548435ea8b184e9d1eb0f8cd4c2371217cc --- /dev/null +++ b/src/components/water.js @@ -0,0 +1,237 @@ +import { Layers } from "./layers"; +import "../vendor/Water"; + +/** + * @author jbouny / https://github.com/jbouny + * + * Work based on : + * @author Slayvin / http://slayvin.net : Flat mirror for three.js + * @author Stemkoski / http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror + * @author Jonas Wagner / http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL + */ + +function MobileWater(geometry, options) { + THREE.Mesh.call(this, geometry); + + const scope = this; + + options = options || {}; + + const clipBias = options.clipBias !== undefined ? options.clipBias : 0.0; + const time = options.time !== undefined ? options.time : 0.0; + const normalSampler = + options.waterNormals !== undefined ? options.waterNormals : null; + const sunDirection = + options.sunDirection !== undefined + ? options.sunDirection + : new THREE.Vector3(0.70707, 0.70707, 0.0); + const sunColor = new THREE.Color( + options.sunColor !== undefined ? options.sunColor : 0xffffff + ); + const waterColor = new THREE.Color( + options.waterColor !== undefined ? options.waterColor : 0x7f7f7f + ); + const eye = + options.eye !== undefined ? options.eye : new THREE.Vector3(0, 0, 0); + const distortionScale = + options.distortionScale !== undefined ? options.distortionScale : 20.0; + const side = options.side !== undefined ? options.side : THREE.FrontSide; + const fog = options.fog !== undefined ? options.fog : false; + + const mirrorShader = { + uniforms: THREE.UniformsUtils.merge([ + THREE.UniformsLib["lights"], + { + normalSampler: { value: null }, + time: { value: 0.0 }, + size: { value: 1.0 }, + distortionScale: { value: 20.0 }, + sunColor: { value: new THREE.Color(0x7f7f7f) }, + sunDirection: { value: new THREE.Vector3(0.70707, 0.70707, 0) }, + eye: { value: new THREE.Vector3() }, + waterColor: { value: new THREE.Color(0x555555) } + } + ]), + + vertexShader: ` + uniform float time; + varying vec4 worldPosition; + + void main() { + worldPosition = modelMatrix * vec4( position, 1.0 ); + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + gl_Position = projectionMatrix * mvPosition; + + } + `, + + fragmentShader: ` + uniform float time; + uniform float size; + uniform float distortionScale; + uniform sampler2D normalSampler; + uniform vec3 sunColor; + uniform vec3 sunDirection; + uniform vec3 eye; + uniform vec3 waterColor; + + varying vec4 worldPosition; + + vec4 getNoise( vec2 uv ) { + vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0); + vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 ); + vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 ); + vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 ); + vec4 noise = texture2D( normalSampler, uv0 ) + + texture2D( normalSampler, uv1 ) + + texture2D( normalSampler, uv2 ) + + texture2D( normalSampler, uv3 ); + return noise * 0.5 - 1.0; + } + + void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) { + vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) ); + float direction = max( 0.0, dot( eyeDirection, reflection ) ); + specularColor += pow( direction, shiny ) * sunColor * spec; + diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse; + } + + ${THREE.ShaderChunk["common"]} + ${THREE.ShaderChunk["packing"]} + ${THREE.ShaderChunk["bsdfs"]} + ${THREE.ShaderChunk["lights_pars"]} + + void main() { + vec4 noise = getNoise( worldPosition.xz * size ); + vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) ); + + vec3 diffuseLight = vec3(0.0); + vec3 specularLight = vec3(0.0); + + vec3 worldToEye = eye-worldPosition.xyz; + vec3 eyeDirection = normalize( worldToEye ); + sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight ); + + float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 ); + float rf0 = 0.3; + float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 ); + vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor; + vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ), ( 0.5 + specularLight ), reflectance); + vec3 outgoingLight = albedo; + gl_FragColor = vec4( outgoingLight, 1 ); + } + ` + }; + + const material = new THREE.ShaderMaterial({ + fragmentShader: mirrorShader.fragmentShader, + vertexShader: mirrorShader.vertexShader, + uniforms: THREE.UniformsUtils.clone(mirrorShader.uniforms), + transparent: false, + lights: true, + side: side, + fog: fog + }); + + material.uniforms.time.value = time; + material.uniforms.normalSampler.value = normalSampler; + material.uniforms.sunColor.value = sunColor; + material.uniforms.waterColor.value = waterColor; + material.uniforms.sunDirection.value = sunDirection; + material.uniforms.distortionScale.value = distortionScale; + + material.uniforms.eye.value = eye; + + scope.material = material; +} + +MobileWater.prototype = Object.create(THREE.Mesh.prototype); +MobileWater.prototype.constructor = THREE.Water; + +AFRAME.registerComponent("water", { + schema: { + waterColor: { type: "color", default: "#001e0f" }, + distortionScale: { type: "number", default: 3.7 }, + sunColor: { type: "color", default: "#ffffff" }, + inclination: { type: "number", default: 0 }, + azimuth: { type: "number", default: 0 }, + distance: { type: "number", default: 1 }, + speed: { type: "number", default: 0.1 }, + forceMobile: { type: "boolean", default: false }, + normalMap: { type: "asset" } + }, + init() { + const waterGeometry = new THREE.PlaneBufferGeometry(800, 800); + + const waterNormals = new THREE.Texture(this.data.normalMap); + waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping; + waterNormals.needsUpdate = true; + + const waterConfig = { + textureWidth: 512, + textureHeight: 512, + waterNormals: waterNormals, + sunDirection: this.data.sunDirection, + sunColor: new THREE.Color(this.data.sunColor), + waterColor: new THREE.Color(this.data.waterColor), + distortionScale: this.data.distortionScale, + fog: false + }; + + if (AFRAME.utils.device.isMobile() || this.data.forceMobile) { + this.water = new MobileWater(waterGeometry, waterConfig); + } else { + this.water = new THREE.Water(waterGeometry, waterConfig); + this.water.mirrorCamera.layers.set(Layers.reflection); + } + + this.el.setObject3D("water", this.water); + }, + + update(oldData) { + const uniforms = this.water.material.uniforms; + + if (this.data.forceMobile !== oldData.forceMobile) { + this.el.removeObject3D("water"); + this.init(); + return; + } + + if (this.data.waterColor !== oldData.waterColor) { + uniforms.waterColor.value.setStyle(this.data.waterColor); + } + + if (this.data.distortionScale !== oldData.distortionScale) { + uniforms.distortionScale.value = this.data.distortionScale; + } + + if (this.data.sunColor !== oldData.sunColor) { + uniforms.sunColor.value.setStyle(this.data.sunColor); + } + + if ( + this.data.inclination !== oldData.inclination || + this.data.azimuth !== oldData.azimuth || + this.data.distance !== oldData.distance + ) { + const theta = Math.PI * (this.data.inclination - 0.5); + const phi = 2 * Math.PI * (this.data.azimuth - 0.5); + + const distance = this.data.distance; + + const x = distance * Math.cos(phi); + const y = distance * Math.sin(phi) * Math.sin(theta); + const z = distance * Math.sin(phi) * Math.cos(theta); + + uniforms.sunDirection.value.set(x, y, z); + } + }, + + tick(time) { + this.water.material.uniforms.time.value = time / 1000 * this.data.speed; + }, + + remove() { + this.el.removeObject3D("water"); + } +}); diff --git a/src/room.js b/src/room.js index 2330ffd76d063980a44352fb51430ea34ec35845..b79902452663c26a2c8b16e611f697ee67269107 100644 --- a/src/room.js +++ b/src/room.js @@ -23,6 +23,9 @@ import "./components/split-axis-events"; import "./components/networked-video-player"; import "./components/offset-relative-to"; import "./components/cached-gltf-model"; +import "./components/water"; +import "./components/skybox"; +import "./components/layers"; import "./components/spawn-controller"; import "./systems/personal-space-bubble"; diff --git a/src/vendor/Water.js b/src/vendor/Water.js new file mode 100644 index 0000000000000000000000000000000000000000..a3160a754e0b41ba59e9219350f988ae6e03901f --- /dev/null +++ b/src/vendor/Water.js @@ -0,0 +1,341 @@ +/** + * @author jbouny / https://github.com/jbouny + * + * Work based on : + * @author Slayvin / http://slayvin.net : Flat mirror for three.js + * @author Stemkoski / http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror + * @author Jonas Wagner / http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL + */ + +THREE.Water = function(geometry, options) { + THREE.Mesh.call(this, geometry); + + const scope = this; + + options = options || {}; + + const textureWidth = + options.textureWidth !== undefined ? options.textureWidth : 512; + const textureHeight = + options.textureHeight !== undefined ? options.textureHeight : 512; + + const clipBias = options.clipBias !== undefined ? options.clipBias : 0.0; + const alpha = options.alpha !== undefined ? options.alpha : 1.0; + const time = options.time !== undefined ? options.time : 0.0; + const normalSampler = + options.waterNormals !== undefined ? options.waterNormals : null; + const sunDirection = + options.sunDirection !== undefined + ? options.sunDirection + : new THREE.Vector3(0.70707, 0.70707, 0.0); + const sunColor = new THREE.Color( + options.sunColor !== undefined ? options.sunColor : 0xffffff + ); + const waterColor = new THREE.Color( + options.waterColor !== undefined ? options.waterColor : 0x7f7f7f + ); + const eye = + options.eye !== undefined ? options.eye : new THREE.Vector3(0, 0, 0); + const distortionScale = + options.distortionScale !== undefined ? options.distortionScale : 20.0; + const side = options.side !== undefined ? options.side : THREE.FrontSide; + const fog = options.fog !== undefined ? options.fog : false; + + // + + const mirrorPlane = new THREE.Plane(); + const normal = new THREE.Vector3(); + const mirrorWorldPosition = new THREE.Vector3(); + const cameraWorldPosition = new THREE.Vector3(); + const rotationMatrix = new THREE.Matrix4(); + const lookAtPosition = new THREE.Vector3(0, 0, -1); + const clipPlane = new THREE.Vector4(); + + const view = new THREE.Vector3(); + const target = new THREE.Vector3(); + const q = new THREE.Vector4(); + + const textureMatrix = new THREE.Matrix4(); + + const mirrorCamera = new THREE.PerspectiveCamera(); + + const parameters = { + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + format: THREE.RGBFormat, + stencilBuffer: false + }; + + const renderTarget = new THREE.WebGLRenderTarget( + textureWidth, + textureHeight, + parameters + ); + + if ( + !THREE.Math.isPowerOfTwo(textureWidth) || + !THREE.Math.isPowerOfTwo(textureHeight) + ) { + renderTarget.texture.generateMipmaps = false; + } + + const mirrorShader = { + uniforms: THREE.UniformsUtils.merge([ + THREE.UniformsLib["fog"], + THREE.UniformsLib["lights"], + { + normalSampler: { value: null }, + mirrorSampler: { value: null }, + alpha: { value: 1.0 }, + time: { value: 0.0 }, + size: { value: 1.0 }, + distortionScale: { value: 20.0 }, + textureMatrix: { value: new THREE.Matrix4() }, + sunColor: { value: new THREE.Color(0x7f7f7f) }, + sunDirection: { value: new THREE.Vector3(0.70707, 0.70707, 0) }, + eye: { value: new THREE.Vector3() }, + waterColor: { value: new THREE.Color(0x555555) } + } + ]), + + vertexShader: [ + "uniform mat4 textureMatrix;", + "uniform float time;", + + "varying vec4 mirrorCoord;", + "varying vec4 worldPosition;", + + THREE.ShaderChunk["fog_pars_vertex"], + THREE.ShaderChunk["shadowmap_pars_vertex"], + + "void main() {", + " mirrorCoord = modelMatrix * vec4( position, 1.0 );", + " worldPosition = mirrorCoord.xyzw;", + " mirrorCoord = textureMatrix * mirrorCoord;", + " vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", + " gl_Position = projectionMatrix * mvPosition;", + + THREE.ShaderChunk["fog_vertex"], + THREE.ShaderChunk["shadowmap_vertex"], + + "}" + ].join("\n"), + + fragmentShader: [ + "uniform sampler2D mirrorSampler;", + "uniform float alpha;", + "uniform float time;", + "uniform float size;", + "uniform float distortionScale;", + "uniform sampler2D normalSampler;", + "uniform vec3 sunColor;", + "uniform vec3 sunDirection;", + "uniform vec3 eye;", + "uniform vec3 waterColor;", + + "varying vec4 mirrorCoord;", + "varying vec4 worldPosition;", + + "vec4 getNoise( vec2 uv ) {", + " vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);", + " vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );", + " vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );", + " vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );", + " vec4 noise = texture2D( normalSampler, uv0 ) +", + " texture2D( normalSampler, uv1 ) +", + " texture2D( normalSampler, uv2 ) +", + " texture2D( normalSampler, uv3 );", + " return noise * 0.5 - 1.0;", + "}", + + "void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) {", + " vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) );", + " float direction = max( 0.0, dot( eyeDirection, reflection ) );", + " specularColor += pow( direction, shiny ) * sunColor * spec;", + " diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse;", + "}", + + THREE.ShaderChunk["common"], + THREE.ShaderChunk["packing"], + THREE.ShaderChunk["bsdfs"], + THREE.ShaderChunk["fog_pars_fragment"], + THREE.ShaderChunk["lights_pars"], + THREE.ShaderChunk["shadowmap_pars_fragment"], + THREE.ShaderChunk["shadowmask_pars_fragment"], + + "void main() {", + " vec4 noise = getNoise( worldPosition.xz * size );", + " vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );", + + " vec3 diffuseLight = vec3(0.0);", + " vec3 specularLight = vec3(0.0);", + + " vec3 worldToEye = eye-worldPosition.xyz;", + " vec3 eyeDirection = normalize( worldToEye );", + " sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );", + + " float distance = length(worldToEye);", + + " vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale;", + " vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.z + distortion ) );", + + " float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );", + " float rf0 = 0.3;", + " float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );", + " vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;", + " vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance);", + " vec3 outgoingLight = albedo;", + " gl_FragColor = vec4( outgoingLight, alpha );", + + THREE.ShaderChunk["tonemapping_fragment"], + THREE.ShaderChunk["fog_fragment"], + + "}" + ].join("\n") + }; + + const material = new THREE.ShaderMaterial({ + fragmentShader: mirrorShader.fragmentShader, + vertexShader: mirrorShader.vertexShader, + uniforms: THREE.UniformsUtils.clone(mirrorShader.uniforms), + transparent: true, + lights: true, + side: side, + fog: fog + }); + + material.uniforms.mirrorSampler.value = renderTarget.texture; + material.uniforms.textureMatrix.value = textureMatrix; + material.uniforms.alpha.value = alpha; + material.uniforms.time.value = time; + material.uniforms.normalSampler.value = normalSampler; + material.uniforms.sunColor.value = sunColor; + material.uniforms.waterColor.value = waterColor; + material.uniforms.sunDirection.value = sunDirection; + material.uniforms.distortionScale.value = distortionScale; + + material.uniforms.eye.value = eye; + + scope.material = material; + scope.mirrorCamera = mirrorCamera; + + scope.onBeforeRender = function(renderer, scene, camera) { + mirrorWorldPosition.setFromMatrixPosition(scope.matrixWorld); + cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld); + + rotationMatrix.extractRotation(scope.matrixWorld); + + normal.set(0, 0, 1); + normal.applyMatrix4(rotationMatrix); + + view.subVectors(mirrorWorldPosition, cameraWorldPosition); + + // Avoid rendering when mirror is facing away + + if (view.dot(normal) > 0) return; + + view.reflect(normal).negate(); + view.add(mirrorWorldPosition); + + rotationMatrix.extractRotation(camera.matrixWorld); + + lookAtPosition.set(0, 0, -1); + lookAtPosition.applyMatrix4(rotationMatrix); + lookAtPosition.add(cameraWorldPosition); + + target.subVectors(mirrorWorldPosition, lookAtPosition); + target.reflect(normal).negate(); + target.add(mirrorWorldPosition); + + mirrorCamera.position.copy(view); + mirrorCamera.up.set(0, 1, 0); + mirrorCamera.up.applyMatrix4(rotationMatrix); + mirrorCamera.up.reflect(normal); + mirrorCamera.lookAt(target); + + mirrorCamera.far = camera.far; // Used in WebGLBackground + + mirrorCamera.updateMatrixWorld(); + mirrorCamera.projectionMatrix.copy(camera.projectionMatrix); + + // Update the texture matrix + textureMatrix.set( + 0.5, + 0.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0 + ); + textureMatrix.multiply(mirrorCamera.projectionMatrix); + textureMatrix.multiply(mirrorCamera.matrixWorldInverse); + + // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html + // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf + mirrorPlane.setFromNormalAndCoplanarPoint(normal, mirrorWorldPosition); + mirrorPlane.applyMatrix4(mirrorCamera.matrixWorldInverse); + + clipPlane.set( + mirrorPlane.normal.x, + mirrorPlane.normal.y, + mirrorPlane.normal.z, + mirrorPlane.constant + ); + + const projectionMatrix = mirrorCamera.projectionMatrix; + + q.x = + (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / + projectionMatrix.elements[0]; + q.y = + (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / + projectionMatrix.elements[5]; + q.z = -1.0; + q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]; + + // Calculate the scaled plane vector + clipPlane.multiplyScalar(2.0 / clipPlane.dot(q)); + + // Replacing the third row of the projection matrix + projectionMatrix.elements[2] = clipPlane.x; + projectionMatrix.elements[6] = clipPlane.y; + projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias; + projectionMatrix.elements[14] = clipPlane.w; + + eye.setFromMatrixPosition(camera.matrixWorld); + + // + + const currentRenderTarget = renderer.getRenderTarget(); + + const currentVrEnabled = renderer.vr.enabled; + const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; + + scope.visible = false; + + renderer.vr.enabled = false; // Avoid camera modification and recursion + renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows + + renderer.render(scene, mirrorCamera, renderTarget, true); + + scope.visible = true; + + renderer.vr.enabled = currentVrEnabled; + renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; + + renderer.setRenderTarget(currentRenderTarget); + }; +}; + +THREE.Water.prototype = Object.create(THREE.Mesh.prototype); +THREE.Water.prototype.constructor = THREE.Water; diff --git a/templates/room.hbs b/templates/room.hbs index add5dff049b1731e33645724d2d848777e26d36f..0e90903456c0dda97f644339b5cc3eea899c90c1 100644 --- a/templates/room.hbs +++ b/templates/room.hbs @@ -55,6 +55,8 @@ <a-asset-item id="floor-nav-mesh" response-type="arraybuffer" src="{{asset "assets/environments/FloorNav_mesh.glb"}}"></a-asset-item> <a-asset-item id="cliff-vista-mesh" response-type="arraybuffer" src="{{asset "assets/environments/CliffVista_mesh.glb"}}"></a-asset-item> + <img id="water-normal-map" src="{{asset "assets/waternormals.jpg"}}"></a-asset-item> + <!-- Templates --> <script id="head-template" type="text/html"> <a-entity @@ -123,8 +125,6 @@ spawn-controller="radius: 4;" character-controller="pivot: #head" > - <a-sphere scale="0.1 0.1 0.1"></a-sphere> - <a-entity id="head" camera="userHeight: 1.6" @@ -174,26 +174,47 @@ ></a-entity> </a-entity> - <a-entity light="type: ambient; color: #FFF"></a-entity> - <!-- Environment --> <a-entity + id="meeting-space" cached-gltf-model="#meeting-space1-mesh" position="0 0 0" ></a-entity> <a-entity + id="outdoor-facade" cached-gltf-model="#outdoor-facade-mesh" position="0 0 0" ></a-entity> <a-entity + id="floor-nav" cached-gltf-model="#floor-nav-mesh" visible="false" position="0 0 0" ></a-entity> - <a-sky color="#DDFFD9"></a-sky> + <a-entity + id="cliff-vista" + cached-gltf-model="#cliff-vista-mesh" + layers="reflection:true" + position="0 0 0" + ></a-entity> + + <a-entity id="skybox" + id="skybox" + scale="8000 8000 8000" + skybox="azimuth:0.280; inclination:0.440" + light="type: ambient; color: #FFF" + layers="reflection:true" + ></a-entity> + + <a-entity + id="water" + water="normalMap:#water-normal-map" + rotation="-90 0 0" + position="0 -88.358 -332.424" + ></a-entity> </a-scene> <script>