import { Color, LinearFilter, MathUtils, Matrix4, Mesh, PerspectiveCamera, Plane, Quaternion, RGBFormat, ShaderMaterial, UniformsUtils, Vector3, Vector4, WebGLRenderTarget } from "../../../build/three.module.js"; var Refractor = function ( geometry, options ) { Mesh.call( this, geometry ); this.type = 'Refractor'; var scope = this; options = options || {}; var color = ( options.color !== undefined ) ? new Color( options.color ) : new Color( 0x7F7F7F ); var textureWidth = options.textureWidth || 512; var textureHeight = options.textureHeight || 512; var clipBias = options.clipBias || 0; var shader = options.shader || Refractor.RefractorShader; // var virtualCamera = new PerspectiveCamera(); virtualCamera.matrixAutoUpdate = false; virtualCamera.userData.refractor = true; // var refractorPlane = new Plane(); var textureMatrix = new Matrix4(); // render target var parameters = { minFilter: LinearFilter, magFilter: LinearFilter, format: RGBFormat, stencilBuffer: false }; var renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, parameters ); if ( ! MathUtils.isPowerOfTwo( textureWidth ) || ! MathUtils.isPowerOfTwo( textureHeight ) ) { renderTarget.texture.generateMipmaps = false; } // material this.material = new ShaderMaterial( { uniforms: UniformsUtils.clone( shader.uniforms ), vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader, transparent: true // ensures, refractors are drawn from farthest to closest } ); this.material.uniforms[ "color" ].value = color; this.material.uniforms[ "tDiffuse" ].value = renderTarget.texture; this.material.uniforms[ "textureMatrix" ].value = textureMatrix; // functions var visible = ( function () { var refractorWorldPosition = new Vector3(); var cameraWorldPosition = new Vector3(); var rotationMatrix = new Matrix4(); var view = new Vector3(); var normal = new Vector3(); return function visible( camera ) { refractorWorldPosition.setFromMatrixPosition( scope.matrixWorld ); cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld ); view.subVectors( refractorWorldPosition, cameraWorldPosition ); rotationMatrix.extractRotation( scope.matrixWorld ); normal.set( 0, 0, 1 ); normal.applyMatrix4( rotationMatrix ); return view.dot( normal ) < 0; }; } )(); var updateRefractorPlane = ( function () { var normal = new Vector3(); var position = new Vector3(); var quaternion = new Quaternion(); var scale = new Vector3(); return function updateRefractorPlane() { scope.matrixWorld.decompose( position, quaternion, scale ); normal.set( 0, 0, 1 ).applyQuaternion( quaternion ).normalize(); // flip the normal because we want to cull everything above the plane normal.negate(); refractorPlane.setFromNormalAndCoplanarPoint( normal, position ); }; } )(); var updateVirtualCamera = ( function () { var clipPlane = new Plane(); var clipVector = new Vector4(); var q = new Vector4(); return function updateVirtualCamera( camera ) { virtualCamera.matrixWorld.copy( camera.matrixWorld ); virtualCamera.matrixWorldInverse.getInverse( virtualCamera.matrixWorld ); virtualCamera.projectionMatrix.copy( camera.projectionMatrix ); virtualCamera.far = camera.far; // used in WebGLBackground // The following code creates an oblique view frustum for clipping. // see: Lengyel, Eric. “Oblique View Frustum Depth Projection and Clipping”. // Journal of Game Development, Vol. 1, No. 2 (2005), Charles River Media, pp. 5–16 clipPlane.copy( refractorPlane ); clipPlane.applyMatrix4( virtualCamera.matrixWorldInverse ); clipVector.set( clipPlane.normal.x, clipPlane.normal.y, clipPlane.normal.z, clipPlane.constant ); // calculate the clip-space corner point opposite the clipping plane and // transform it into camera space by multiplying it by the inverse of the projection matrix var projectionMatrix = virtualCamera.projectionMatrix; q.x = ( Math.sign( clipVector.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ]; q.y = ( Math.sign( clipVector.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 clipVector.multiplyScalar( 2.0 / clipVector.dot( q ) ); // replacing the third row of the projection matrix projectionMatrix.elements[ 2 ] = clipVector.x; projectionMatrix.elements[ 6 ] = clipVector.y; projectionMatrix.elements[ 10 ] = clipVector.z + 1.0 - clipBias; projectionMatrix.elements[ 14 ] = clipVector.w; }; } )(); // This will update the texture matrix that is used for projective texture mapping in the shader. // see: http://developer.download.nvidia.com/assets/gamedev/docs/projective_texture_mapping.pdf function updateTextureMatrix( camera ) { // this matrix does range mapping to [ 0, 1 ] 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 ); // we use "Object Linear Texgen", so we need to multiply the texture matrix T // (matrix above) with the projection and view matrix of the virtual camera // and the model matrix of the refractor textureMatrix.multiply( camera.projectionMatrix ); textureMatrix.multiply( camera.matrixWorldInverse ); textureMatrix.multiply( scope.matrixWorld ); } // function render( renderer, scene, camera ) { scope.visible = false; var currentRenderTarget = renderer.getRenderTarget(); var currentXrEnabled = renderer.xr.enabled; var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; renderer.xr.enabled = false; // avoid camera modification renderer.shadowMap.autoUpdate = false; // avoid re-computing shadows renderer.setRenderTarget( renderTarget ); if ( renderer.autoClear === false ) renderer.clear(); renderer.render( scene, virtualCamera ); renderer.xr.enabled = currentXrEnabled; renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; renderer.setRenderTarget( currentRenderTarget ); // restore viewport var viewport = camera.viewport; if ( viewport !== undefined ) { renderer.state.viewport( viewport ); } scope.visible = true; } // this.onBeforeRender = function ( renderer, scene, camera ) { // Render renderTarget.texture.encoding = renderer.outputEncoding; // ensure refractors are rendered only once per frame if ( camera.userData.refractor === true ) return; // avoid rendering when the refractor is viewed from behind if ( ! visible( camera ) === true ) return; // update updateRefractorPlane(); updateTextureMatrix( camera ); updateVirtualCamera( camera ); render( renderer, scene, camera ); }; this.getRenderTarget = function () { return renderTarget; }; }; Refractor.prototype = Object.create( Mesh.prototype ); Refractor.prototype.constructor = Refractor; Refractor.RefractorShader = { uniforms: { 'color': { value: null }, 'tDiffuse': { value: null }, 'textureMatrix': { value: null } }, vertexShader: [ 'uniform mat4 textureMatrix;', 'varying vec4 vUv;', 'void main() {', ' vUv = textureMatrix * vec4( position, 1.0 );', ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', '}' ].join( '\n' ), fragmentShader: [ 'uniform vec3 color;', 'uniform sampler2D tDiffuse;', 'varying vec4 vUv;', 'float blendOverlay( float base, float blend ) {', ' return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );', '}', 'vec3 blendOverlay( vec3 base, vec3 blend ) {', ' return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );', '}', 'void main() {', ' vec4 base = texture2DProj( tDiffuse, vUv );', ' gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 );', '}' ].join( '\n' ) }; export { Refractor };