فهرست منبع

Add SSRPass (screen space reflection) (#20156)

* SSRPass init

* a

* a

* a

* a

* a

* a

* a

* a

* a

* use resolution & delete depthRenderMaterial

* SSRPassPerspective

* a

* a

* a

* a

* a

* a

* a

* use pointToLineDistance, maybe ok

* a

* almost OK

* increase FOV

* a

* a

* a

* A

* A

* a

* clean

* clean

* screenshot

* a

* gui

* a

* new pointToLineDistance function

* #define MAX_STEP & onresize replace MAX_STEP

* bug fix

* a

* mobile bug fix

* surfDist affected by clipW

* performance improvement

* a

* SSRPassSelective

* a

* selective ok

* selective ok

* a

* a

* use traverse for selective reflect first, try other methods after

* Configurable encoding

* support OrthographicCamera

* traverseVisible

* SSRPass_bouncing

* a

* a

* performance improvement

* a

* No need to calculate clipW separately

* performance: Use the same skip strategy as viewZ for vZ

* clean

* use lineLineIntersection instead of pointToLineDistance, can get accurate relfectRayLen. Add DistanceAttenuation feature.

* lineLineIntersection & DistanceAttenuation.
Use lineLineIntersection instead of pointToLineDistance, can get accurate relfectRayLen. Add DistanceAttenuation feature.

* default DistanceAttenuation on

* bugfix: OrthographicCamera support

* infiniteThick

* normal noise

* a

* a

* fit morphTargets

* Separate blur setting

* performance: use #ifdef instead of if

* a

* New d1viewPosition calc method, seems same performance, but more concise and clear

* Use Perspective-Correct Interpolation instead of lineLineIntersection

* performance & priliminary angleCompensation

* NormalBlending

* screenshot

* a

* Resolve conflict of examples/files.js

* bugfix: maxDistance and attenuation should compared by distance perpendicular to reflectNormal (use pointPlaneDistance function)

* fresnel

* calculated maxReflectRayLen

* bugfix: Character morphTarget normalMaterial.

* bugfix: Character morphTarget normalMaterial.

* change default scene to bunny

* surfDist compensation by angle

* With Refelector.js, default off. Now just add a reflector to hide the apparent flaw of SSR, need more integration afterwards.

* Let Reflector.js can handle opacity by depthTexture.

* fix the "jumpiness" of the reflection

* 1. Exclude Reflector from SSRPass if Reflector is on. 2. Increase reflector distanceAttenuation. 3. Change isOrbitControls to autoRotate.

* Decrease the vertical offset between SSR and Reflector.

* Revert plane.depthWrite = false & Revert to reflector.position.y = plane.position.y + .0001;

* Remove the noise setting.

* Use pointToLineDistance instead of viewReflectRayZ to calculate away, thus become stride irrelavent.

* Performance: perform pointToLineDistance only if viewReflectRayZ-sD<=vZ.

* 1. Reduce the size of the reflector to prevent glitch caused by low precision.
2. reflectorRenderTarget resize.
3. Clean up.

* Fix: .getClearColor() now requires a Color as an argument.

* postprocessing with reflector performance problem quick hack.

https://discourse.threejs.org/t/hows-the-reflection-made-in-this-example/23597/8

* Turn on GroundReflector by default & some clean up.

* Let groundReflector also follow settings.

* Fix zigzag problem between ground and objects.

* gui folder
Vis 4 سال پیش
والد
کامیت
05cc046c6a

+ 1 - 0
examples/files.json

@@ -261,6 +261,7 @@
 		"webgl_postprocessing_smaa",
 		"webgl_postprocessing_sobel",
 		"webgl_postprocessing_ssao",
+		"webgl_postprocessing_ssr",
 		"webgl_postprocessing_taa",
 		"webgl_postprocessing_unreal_bloom",
 		"webgl_postprocessing_unreal_bloom_selective"

+ 333 - 0
examples/jsm/objects/ReflectorForSSRPass.js

@@ -0,0 +1,333 @@
+import {
+	Color,
+	LinearFilter,
+	MathUtils,
+	Matrix4,
+	Mesh,
+	PerspectiveCamera,
+	Plane,
+	RGBFormat,
+	ShaderMaterial,
+	UniformsUtils,
+	Vector3,
+	Vector4,
+	WebGLRenderTarget,
+	DepthTexture,
+	UnsignedShortType,
+	NearestFilter
+} from '../../../build/three.module.js';
+
+var Reflector = function ( geometry, options ) {
+
+	Mesh.call( this, geometry );
+
+	this.type = 'Reflector';
+
+	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 || Reflector.ReflectorShader;
+	var useDepthTexture = options.useDepthTexture
+	var yAxis = new Vector3(0, 1, 0);
+	var vecTemp0 = new Vector3();
+	var vecTemp1 = new Vector3();
+
+	//
+
+	scope.needsUpdate = false;
+	scope.maxDistance = Reflector.ReflectorShader.uniforms.maxDistance.value
+	scope.opacity = Reflector.ReflectorShader.uniforms.opacity.value
+
+  scope._isDistanceAttenuation = Reflector.ReflectorShader.defines.isDistanceAttenuation
+  Object.defineProperty(scope, 'isDistanceAttenuation', {
+    get() {
+      return scope._isDistanceAttenuation
+    },
+    set(val) {
+      if (scope._isDistanceAttenuation === val) return
+      scope._isDistanceAttenuation = val
+      scope.material.defines.isDistanceAttenuation = val
+      scope.material.needsUpdate = true
+    }
+	})
+
+  scope._isFresnel = Reflector.ReflectorShader.defines.isFresnel
+  Object.defineProperty(scope, 'isFresnel', {
+    get() {
+      return scope._isFresnel
+    },
+    set(val) {
+      if (scope._isFresnel === val) return
+      scope._isFresnel = val
+      scope.material.defines.isFresnel = val
+      scope.material.needsUpdate = true
+    }
+	})
+
+	var reflectorPlane = new Plane();
+	var normal = new Vector3();
+	var reflectorWorldPosition = new Vector3();
+	var cameraWorldPosition = new Vector3();
+	var rotationMatrix = new Matrix4();
+	var lookAtPosition = new Vector3( 0, 0, - 1 );
+	var clipPlane = new Vector4();
+
+	var view = new Vector3();
+	var target = new Vector3();
+	var q = new Vector4();
+
+	var textureMatrix = new Matrix4();
+	var virtualCamera = new PerspectiveCamera();
+
+	if( useDepthTexture ){
+		var depthTexture = new DepthTexture();
+		depthTexture.type = UnsignedShortType;
+		depthTexture.minFilter = NearestFilter;
+		depthTexture.maxFilter = NearestFilter;
+	}
+
+	var parameters = {
+		minFilter: LinearFilter,
+		magFilter: LinearFilter,
+		format: RGBFormat,
+    depthTexture: useDepthTexture ? depthTexture : null,
+	};
+
+	var renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, parameters );
+
+	if ( ! MathUtils.isPowerOfTwo( textureWidth ) || ! MathUtils.isPowerOfTwo( textureHeight ) ) {
+
+		renderTarget.texture.generateMipmaps = false;
+
+	}
+
+	var material = new ShaderMaterial( {
+		transparent: useDepthTexture,
+    defines: Object.assign({
+      useDepthTexture: useDepthTexture
+    }, Reflector.ReflectorShader.defines),
+		uniforms: UniformsUtils.clone( shader.uniforms ),
+		fragmentShader: shader.fragmentShader,
+		vertexShader: shader.vertexShader
+	} );
+
+	material.uniforms[ 'tDiffuse' ].value = renderTarget.texture;
+	material.uniforms[ 'color' ].value = color;
+	material.uniforms[ 'textureMatrix' ].value = textureMatrix;
+	if (useDepthTexture) {
+		material.uniforms[ 'tDepth' ].value = renderTarget.depthTexture;
+	}
+
+	this.material = material;
+
+	this.doRender = function ( renderer, scene, camera ) {
+
+		material.uniforms['maxDistance'].value = scope.maxDistance * (camera.position.length() / camera.position.y);
+		///todo: Temporary hack,
+		// need precise calculation like this https://github.com/mrdoob/three.js/pull/20156/commits/8181946068e386d14a283cbd4f8877bc7ae066d3 ,
+		// after fully understand http://www.terathon.com/lengyel/Lengyel-Oblique.pdf .
+
+		material.uniforms['opacity'].value = scope.opacity;
+
+		vecTemp0.copy(camera.position).normalize();
+		vecTemp1.copy(vecTemp0).reflect(yAxis);
+		material.uniforms['fresnel'].value = (vecTemp0.dot( vecTemp1 ) + 1.) / 2.; ///todo: Also need to use glsl viewPosition and viewNormal per pixel.
+		// console.log(material.uniforms['fresnel'].value)
+
+		reflectorWorldPosition.setFromMatrixPosition( scope.matrixWorld );
+		cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
+
+		rotationMatrix.extractRotation( scope.matrixWorld );
+
+		normal.set( 0, 0, 1 );
+		normal.applyMatrix4( rotationMatrix );
+
+		view.subVectors( reflectorWorldPosition, cameraWorldPosition );
+
+		// Avoid rendering when reflector is facing away
+
+		if ( view.dot( normal ) > 0 ) return;
+
+		view.reflect( normal ).negate();
+		view.add( reflectorWorldPosition );
+
+		rotationMatrix.extractRotation( camera.matrixWorld );
+
+		lookAtPosition.set( 0, 0, - 1 );
+		lookAtPosition.applyMatrix4( rotationMatrix );
+		lookAtPosition.add( cameraWorldPosition );
+
+		target.subVectors( reflectorWorldPosition, lookAtPosition );
+		target.reflect( normal ).negate();
+		target.add( reflectorWorldPosition );
+
+		virtualCamera.position.copy( view );
+		virtualCamera.up.set( 0, 1, 0 );
+		virtualCamera.up.applyMatrix4( rotationMatrix );
+		virtualCamera.up.reflect( normal );
+		virtualCamera.lookAt( target );
+
+		virtualCamera.far = camera.far; // Used in WebGLBackground
+
+		virtualCamera.updateMatrixWorld();
+		virtualCamera.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( virtualCamera.projectionMatrix );
+		textureMatrix.multiply( virtualCamera.matrixWorldInverse );
+		textureMatrix.multiply( scope.matrixWorld );
+
+		// 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
+		reflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition );
+		reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse );
+
+		clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant );
+
+		var projectionMatrix = virtualCamera.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;
+
+		// Render
+
+		renderTarget.texture.encoding = renderer.outputEncoding;
+
+		// 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 );
+
+		renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
+
+		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.getRenderTarget = function () {
+
+		return renderTarget;
+
+	};
+
+};
+
+Reflector.prototype = Object.create( Mesh.prototype );
+Reflector.prototype.constructor = Reflector;
+
+Reflector.ReflectorShader = { ///todo: Will conflict with Reflector.js?
+
+  defines: {
+    isDistanceAttenuation: true,
+    isFresnel: true,
+  },
+
+	uniforms: {
+
+		color: { value: null },
+		tDiffuse: { value: null },
+		tDepth: { value: null },
+		textureMatrix: { value: null },
+    maxDistance: { value: 180 },
+    opacity: { value: .5 },
+    fresnel: { 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;
+		uniform sampler2D tDepth;
+		uniform float maxDistance;
+		uniform float opacity;
+		uniform float fresnel;
+		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 );
+			#ifdef useDepthTexture
+				float op=opacity;
+				float depth = texture2DProj( tDepth, vUv ).r;
+				if(depth>maxDistance) discard;
+				#ifdef isDistanceAttenuation
+					float ratio=1.-(depth/maxDistance);
+					float attenuation=ratio*ratio;
+					op=opacity*attenuation;
+				#endif
+				#ifdef isFresnel
+					op*=fresnel;
+				#endif
+				gl_FragColor = vec4( blendOverlay( base.rgb, color ), op );
+			#else
+				gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 );
+			#endif
+		}
+	`,
+};
+
+export { Reflector };

+ 623 - 0
examples/jsm/postprocessing/SSRPass.js

@@ -0,0 +1,623 @@
+import {
+  AddEquation,
+  Color,
+  NormalBlending,
+  DepthTexture,
+	SrcAlphaFactor,
+	OneMinusSrcAlphaFactor,
+  LinearFilter,
+  MeshNormalMaterial,
+  MeshBasicMaterial,
+  NearestFilter,
+  NoBlending,
+  RGBAFormat,
+  ShaderMaterial,
+  UniformsUtils,
+  UnsignedShortType,
+  WebGLRenderTarget,
+	HalfFloatType,
+} from "../../../build/three.module.js";
+import { Pass } from "../postprocessing/Pass.js";
+import { SSRShader } from "../shaders/SSRShader.js";
+import { SSRBlurShader } from "../shaders/SSRShader.js";
+import { SSRDepthShader } from "../shaders/SSRShader.js";
+import { CopyShader } from "../shaders/CopyShader.js";
+
+var SSRPass = function({ renderer, scene, camera, width, height, selects, encoding, isPerspectiveCamera = true, isBouncing = false, morphTargets = false, groundReflector }) {
+
+  Pass.call(this);
+
+  this.width = (width !== undefined) ? width : 512;
+  this.height = (height !== undefined) ? height : 512;
+
+  this.clear = true;
+
+	this.renderer = renderer;
+	this.scene = scene;
+  this.camera = camera;
+	this.groundReflector = groundReflector;
+
+  this.opacity = SSRShader.uniforms.opacity.value;;
+  this.output = 0;
+
+  this.maxDistance = SSRShader.uniforms.maxDistance.value;
+  this.surfDist = SSRShader.uniforms.surfDist.value;
+
+	this.encoding = encoding
+
+	this.tempColor = new Color()
+
+	this._selects = selects
+  this.isSelective = Array.isArray(this._selects)
+	Object.defineProperty(this, 'selects', {
+		get() {
+			return this._selects
+		},
+		set(val) {
+      if (this._selects === val) return
+      this._selects = val
+			if (Array.isArray(val)) {
+				this.isSelective = true
+				this.ssrMaterial.defines.isSelective = true
+				this.ssrMaterial.needsUpdate = true
+			} else {
+				this.isSelective = false
+				this.ssrMaterial.defines.isSelective = false
+				this.ssrMaterial.needsUpdate = true
+			}
+		}
+	})
+
+  this._isBouncing = isBouncing ///todo: don't need defineProperty
+  Object.defineProperty(this, 'isBouncing', {
+    get() {
+      return this._isBouncing
+    },
+    set(val) {
+      if (this._isBouncing === val) return
+      this._isBouncing = val
+      if (val) {
+        this.ssrMaterial.uniforms['tDiffuse'].value = this.prevRenderTarget.texture;
+      } else {
+        this.ssrMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+      }
+    }
+  })
+
+  this.isBlur = true
+
+  this._isDistanceAttenuation = SSRShader.defines.isDistanceAttenuation
+  Object.defineProperty(this, 'isDistanceAttenuation', {
+    get() {
+      return this._isDistanceAttenuation
+    },
+    set(val) {
+      if (this._isDistanceAttenuation === val) return
+      this._isDistanceAttenuation = val
+      this.ssrMaterial.defines.isDistanceAttenuation = val
+      this.ssrMaterial.needsUpdate = true
+    }
+	})
+
+
+  this._isFresnel = SSRShader.defines.isFresnel
+  Object.defineProperty(this, 'isFresnel', {
+    get() {
+      return this._isFresnel
+    },
+    set(val) {
+      if (this._isFresnel === val) return
+      this._isFresnel = val
+      this.ssrMaterial.defines.isFresnel = val
+      this.ssrMaterial.needsUpdate = true
+    }
+	})
+
+  this._isInfiniteThick = SSRShader.defines.isInfiniteThick
+  Object.defineProperty(this, 'isInfiniteThick', {
+    get() {
+      return this._isInfiniteThick
+    },
+    set(val) {
+      if (this._isInfiniteThick === val) return
+      this._isInfiniteThick = val
+      this.ssrMaterial.defines.isInfiniteThick = val
+      this.ssrMaterial.needsUpdate = true
+    }
+  })
+  this.thickTolerance = SSRShader.uniforms.thickTolerance.value;
+
+  // beauty render target with depth buffer
+
+  var depthTexture = new DepthTexture();
+  depthTexture.type = UnsignedShortType;
+  depthTexture.minFilter = NearestFilter;
+  depthTexture.maxFilter = NearestFilter;
+
+  this.beautyRenderTarget = new WebGLRenderTarget(this.width, this.height, {
+    minFilter: LinearFilter,
+    magFilter: LinearFilter,
+    format: RGBAFormat,
+    depthTexture: depthTexture,
+    depthBuffer: true
+  });
+
+  //for bouncing
+  this.prevRenderTarget = new WebGLRenderTarget(this.width, this.height, {
+    minFilter: LinearFilter,
+    magFilter: LinearFilter,
+    format: RGBAFormat,
+  });
+
+  // normal render target
+
+  this.normalRenderTarget = new WebGLRenderTarget(this.width, this.height, {
+    minFilter: NearestFilter,
+    magFilter: NearestFilter,
+		format: RGBAFormat,
+		type: HalfFloatType,
+  });
+
+  // metalness render target
+
+  // if (this.isSelective) {
+    this.metalnessRenderTarget = new WebGLRenderTarget(this.width, this.height, {
+      minFilter: NearestFilter,
+      magFilter: NearestFilter,
+      format: RGBAFormat
+    });
+  // }
+
+
+
+  // ssr render target
+
+  this.ssrRenderTarget = new WebGLRenderTarget(this.width, this.height, {
+    minFilter: LinearFilter,
+    magFilter: LinearFilter,
+    format: RGBAFormat
+  });
+
+  this.blurRenderTarget = this.ssrRenderTarget.clone();
+  this.blurRenderTarget2 = this.ssrRenderTarget.clone();
+  // this.blurRenderTarget3 = this.ssrRenderTarget.clone();
+
+  // ssr material
+
+  if (SSRShader === undefined) {
+
+    console.error('THREE.SSRPass: The pass relies on SSRShader.');
+
+  }
+
+  this.ssrMaterial = new ShaderMaterial({
+    defines: Object.assign({
+      MAX_STEP: Math.sqrt(window.innerWidth * window.innerWidth + window.innerHeight * window.innerHeight)
+    }, SSRShader.defines),
+    uniforms: UniformsUtils.clone(SSRShader.uniforms),
+    vertexShader: SSRShader.vertexShader,
+    fragmentShader: SSRShader.fragmentShader,
+    blending: NoBlending
+  });
+  if (!isPerspectiveCamera) {
+    this.ssrMaterial.defines.isPerspectiveCamera = isPerspectiveCamera
+    this.ssrMaterial.needsUpdate = true
+  }
+
+  this.ssrMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+  this.ssrMaterial.uniforms['tNormal'].value = this.normalRenderTarget.texture;
+  // if (this.isSelective) {
+    this.ssrMaterial.defines.isSelective = this.isSelective
+    this.ssrMaterial.needsUpdate = true
+    this.ssrMaterial.uniforms['tMetalness'].value = this.metalnessRenderTarget.texture;
+  // }
+  this.ssrMaterial.uniforms['tDepth'].value = this.beautyRenderTarget.depthTexture;
+  this.ssrMaterial.uniforms['cameraNear'].value = this.camera.near;
+  this.ssrMaterial.uniforms['cameraFar'].value = this.camera.far;
+  this.ssrMaterial.uniforms['surfDist'].value = this.surfDist;
+  this.ssrMaterial.uniforms['resolution'].value.set(this.width, this.height);
+  this.ssrMaterial.uniforms['cameraProjectionMatrix'].value.copy(this.camera.projectionMatrix);
+  this.ssrMaterial.uniforms['cameraInverseProjectionMatrix'].value.copy(this.camera.projectionMatrixInverse);
+
+  // normal material
+
+  this.normalMaterial = new MeshNormalMaterial({ morphTargets });
+  this.normalMaterial.blending = NoBlending;
+
+  // if (this.isSelective) {
+    // metalnessOn material
+
+    this.metalnessOnMaterial = new MeshBasicMaterial({
+      color: 'white'
+    });
+
+    // metalnessOff material
+
+    this.metalnessOffMaterial = new MeshBasicMaterial({
+      color: 'black'
+    });
+  // }
+
+  // blur material
+
+  this.blurMaterial = new ShaderMaterial({
+    defines: Object.assign({}, SSRBlurShader.defines),
+    uniforms: UniformsUtils.clone(SSRBlurShader.uniforms),
+    vertexShader: SSRBlurShader.vertexShader,
+    fragmentShader: SSRBlurShader.fragmentShader
+  });
+  this.blurMaterial.uniforms['tDiffuse'].value = this.ssrRenderTarget.texture;
+  this.blurMaterial.uniforms['resolution'].value.set(this.width, this.height);
+
+  // blur material 2
+
+  this.blurMaterial2 = new ShaderMaterial({
+    defines: Object.assign({}, SSRBlurShader.defines),
+    uniforms: UniformsUtils.clone(SSRBlurShader.uniforms),
+    vertexShader: SSRBlurShader.vertexShader,
+    fragmentShader: SSRBlurShader.fragmentShader
+  });
+  this.blurMaterial2.uniforms['tDiffuse'].value = this.blurRenderTarget.texture;
+  this.blurMaterial2.uniforms['resolution'].value.set(this.width, this.height);
+
+  // // blur material 3
+
+  // this.blurMaterial3 = new ShaderMaterial({
+  //   defines: Object.assign({}, SSRBlurShader.defines),
+  //   uniforms: UniformsUtils.clone(SSRBlurShader.uniforms),
+  //   vertexShader: SSRBlurShader.vertexShader,
+  //   fragmentShader: SSRBlurShader.fragmentShader
+  // });
+  // this.blurMaterial3.uniforms['tDiffuse'].value = this.blurRenderTarget2.texture;
+  // this.blurMaterial3.uniforms['resolution'].value.set(this.width, this.height);
+
+  // material for rendering the depth
+
+  this.depthRenderMaterial = new ShaderMaterial({
+    defines: Object.assign({}, SSRDepthShader.defines),
+    uniforms: UniformsUtils.clone(SSRDepthShader.uniforms),
+    vertexShader: SSRDepthShader.vertexShader,
+    fragmentShader: SSRDepthShader.fragmentShader,
+    blending: NoBlending
+  });
+  this.depthRenderMaterial.uniforms['tDepth'].value = this.beautyRenderTarget.depthTexture;
+  this.depthRenderMaterial.uniforms['cameraNear'].value = this.camera.near;
+  this.depthRenderMaterial.uniforms['cameraFar'].value = this.camera.far;
+
+  // material for rendering the content of a render target
+
+  this.copyMaterial = new ShaderMaterial({
+    uniforms: UniformsUtils.clone(CopyShader.uniforms),
+    vertexShader: CopyShader.vertexShader,
+    fragmentShader: CopyShader.fragmentShader,
+    transparent: true,
+    depthTest: false,
+    depthWrite: false,
+    blendSrc: SrcAlphaFactor,
+    blendDst: OneMinusSrcAlphaFactor,
+    blendEquation: AddEquation,
+    blendSrcAlpha: SrcAlphaFactor,
+    blendDstAlpha: OneMinusSrcAlphaFactor,
+    blendEquationAlpha: AddEquation,
+    // premultipliedAlpha:true,
+  });
+
+  this.fsQuad = new Pass.FullScreenQuad(null);
+
+  this.originalClearColor = new Color();
+
+};
+
+SSRPass.prototype = Object.assign(Object.create(Pass.prototype), {
+
+  constructor: SSRPass,
+
+  dispose: function() {
+
+    // dispose render targets
+
+    this.beautyRenderTarget.dispose();
+    this.prevRenderTarget.dispose();
+    this.normalRenderTarget.dispose();
+		// if (this.isSelective)
+			this.metalnessRenderTarget.dispose();
+    this.ssrRenderTarget.dispose();
+    this.blurRenderTarget.dispose();
+    this.blurRenderTarget2.dispose();
+    // this.blurRenderTarget3.dispose();
+
+    // dispose materials
+
+    this.normalMaterial.dispose();
+    // if (this.isSelective) {
+      this.metalnessOnMaterial.dispose();
+      this.metalnessOffMaterial.dispose();
+    // }
+    this.blurMaterial.dispose();
+    this.blurMaterial2.dispose();
+    this.copyMaterial.dispose();
+    this.depthRenderMaterial.dispose();
+
+    // dipsose full screen quad
+
+    this.fsQuad.dispose();
+
+  },
+
+  render: function(renderer, writeBuffer /*, readBuffer, deltaTime, maskActive */ ) {
+
+    // render beauty and depth
+
+    if (this.encoding) this.beautyRenderTarget.texture.encoding = this.encoding
+    renderer.setRenderTarget(this.beautyRenderTarget);
+		renderer.clear();
+		if (this.groundReflector) {
+			this.groundReflector.doRender(this.renderer, this.scene, this.camera);
+			this.groundReflector.visible = true
+		}
+    renderer.render(this.scene, this.camera);
+		if(this.groundReflector) this.groundReflector.visible=false
+
+    // render normals
+
+    this.renderOverride(renderer, this.normalMaterial, this.normalRenderTarget, 0, 0);
+
+    // render metalnesses
+
+    if (this.isSelective) {
+      this.renderMetalness(renderer, this.metalnessOnMaterial, this.metalnessRenderTarget, 0, 0);
+    }
+
+    // render SSR
+
+    this.ssrMaterial.uniforms['opacity'].value = this.opacity;
+    this.ssrMaterial.uniforms['maxDistance'].value = this.maxDistance;
+    this.ssrMaterial.uniforms['surfDist'].value = this.surfDist;
+    this.ssrMaterial.uniforms['thickTolerance'].value = this.thickTolerance
+    this.renderPass(renderer, this.ssrMaterial, this.ssrRenderTarget);
+
+
+    // render blur
+
+    if (this.isBlur) {
+      this.renderPass(renderer, this.blurMaterial, this.blurRenderTarget);
+      this.renderPass(renderer, this.blurMaterial2, this.blurRenderTarget2);
+      // this.renderPass(renderer, this.blurMaterial3, this.blurRenderTarget3);
+    }
+
+    // output result to screen
+
+    switch (this.output) {
+
+      case SSRPass.OUTPUT.Default:
+
+        if (this.isBouncing) {
+          this.copyMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+          this.copyMaterial.blending = NoBlending;
+          this.renderPass(renderer, this.copyMaterial, this.prevRenderTarget);
+
+          if (this.isBlur)
+            this.copyMaterial.uniforms['tDiffuse'].value = this.blurRenderTarget2.texture;
+          else
+            this.copyMaterial.uniforms['tDiffuse'].value = this.ssrRenderTarget.texture;
+          this.copyMaterial.blending = NormalBlending;
+          this.renderPass(renderer, this.copyMaterial, this.prevRenderTarget);
+
+          this.copyMaterial.uniforms['tDiffuse'].value = this.prevRenderTarget.texture;
+          this.copyMaterial.blending = NoBlending;
+          this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+        } else {
+          this.copyMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+          this.copyMaterial.blending = NoBlending;
+          this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+
+          if (this.isBlur)
+            this.copyMaterial.uniforms['tDiffuse'].value = this.blurRenderTarget2.texture;
+          else
+            this.copyMaterial.uniforms['tDiffuse'].value = this.ssrRenderTarget.texture;
+          this.copyMaterial.blending = NormalBlending;
+          this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+        }
+
+        break;
+      case SSRPass.OUTPUT.SSR:
+
+        if (this.isBlur)
+          this.copyMaterial.uniforms['tDiffuse'].value = this.blurRenderTarget2.texture;
+        else
+          this.copyMaterial.uniforms['tDiffuse'].value = this.ssrRenderTarget.texture;
+        this.copyMaterial.blending = NoBlending;
+        this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+
+        if (this.isBouncing) {
+          if (this.isBlur)
+            this.copyMaterial.uniforms['tDiffuse'].value = this.blurRenderTarget2.texture;
+          else
+            this.copyMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+          this.copyMaterial.blending = NoBlending;
+          this.renderPass(renderer, this.copyMaterial, this.prevRenderTarget);
+
+          this.copyMaterial.uniforms['tDiffuse'].value = this.ssrRenderTarget.texture;
+          this.copyMaterial.blending = NormalBlending;
+          this.renderPass(renderer, this.copyMaterial, this.prevRenderTarget);
+        }
+
+        break;
+
+      case SSRPass.OUTPUT.Beauty:
+
+        this.copyMaterial.uniforms['tDiffuse'].value = this.beautyRenderTarget.texture;
+        this.copyMaterial.blending = NoBlending;
+        this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+
+        break;
+
+      case SSRPass.OUTPUT.Depth:
+
+        this.renderPass(renderer, this.depthRenderMaterial, this.renderToScreen ? null : writeBuffer);
+
+        break;
+
+      case SSRPass.OUTPUT.Normal:
+
+        this.copyMaterial.uniforms['tDiffuse'].value = this.normalRenderTarget.texture;
+        this.copyMaterial.blending = NoBlending;
+        this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+
+        break;
+
+      case SSRPass.OUTPUT.Metalness:
+
+        this.copyMaterial.uniforms['tDiffuse'].value = this.metalnessRenderTarget.texture;
+        this.copyMaterial.blending = NoBlending;
+        this.renderPass(renderer, this.copyMaterial, this.renderToScreen ? null : writeBuffer);
+
+        break;
+
+      default:
+        console.warn('THREE.SSRPass: Unknown output type.');
+
+    }
+
+  },
+
+  renderPass: function(renderer, passMaterial, renderTarget, clearColor, clearAlpha) {
+
+    // save original state
+    this.originalClearColor.copy(renderer.getClearColor(this.tempColor));
+    var originalClearAlpha = renderer.getClearAlpha(this.tempColor);
+    var originalAutoClear = renderer.autoClear;
+
+    renderer.setRenderTarget(renderTarget);
+
+    // setup pass state
+    renderer.autoClear = false;
+    if ((clearColor !== undefined) && (clearColor !== null)) {
+
+      renderer.setClearColor(clearColor);
+      renderer.setClearAlpha(clearAlpha || 0.0);
+      renderer.clear();
+
+    }
+
+    this.fsQuad.material = passMaterial;
+    this.fsQuad.render(renderer);
+
+    // restore original state
+    renderer.autoClear = originalAutoClear;
+    renderer.setClearColor(this.originalClearColor);
+    renderer.setClearAlpha(originalClearAlpha);
+
+  },
+
+  renderOverride: function(renderer, overrideMaterial, renderTarget, clearColor, clearAlpha) {
+
+    this.originalClearColor.copy(renderer.getClearColor(this.tempColor));
+    var originalClearAlpha = renderer.getClearAlpha(this.tempColor);
+    var originalAutoClear = renderer.autoClear;
+
+    renderer.setRenderTarget(renderTarget);
+    renderer.autoClear = false;
+
+    clearColor = overrideMaterial.clearColor || clearColor;
+    clearAlpha = overrideMaterial.clearAlpha || clearAlpha;
+
+    if ((clearColor !== undefined) && (clearColor !== null)) {
+
+      renderer.setClearColor(clearColor);
+      renderer.setClearAlpha(clearAlpha || 0.0);
+      renderer.clear();
+
+    }
+
+    this.scene.overrideMaterial = overrideMaterial;
+    renderer.render(this.scene, this.camera);
+    this.scene.overrideMaterial = null;
+
+    // restore original state
+
+    renderer.autoClear = originalAutoClear;
+    renderer.setClearColor(this.originalClearColor);
+    renderer.setClearAlpha(originalClearAlpha);
+
+  },
+
+  renderMetalness: function(renderer, overrideMaterial, renderTarget, clearColor, clearAlpha) {
+
+    this.originalClearColor.copy(renderer.getClearColor(this.tempColor));
+    var originalClearAlpha = renderer.getClearAlpha(this.tempColor);
+    var originalAutoClear = renderer.autoClear;
+
+    renderer.setRenderTarget(renderTarget);
+    renderer.autoClear = false;
+
+    clearColor = overrideMaterial.clearColor || clearColor;
+    clearAlpha = overrideMaterial.clearAlpha || clearAlpha;
+
+    if ((clearColor !== undefined) && (clearColor !== null)) {
+
+      renderer.setClearColor(clearColor);
+      renderer.setClearAlpha(clearAlpha || 0.0);
+      renderer.clear();
+
+    }
+
+    this.scene.traverseVisible(child => {
+      child._SSRPassMaterialBack = child.material
+      if (this._selects.includes(child)) {
+        child.material = this.metalnessOnMaterial
+      } else {
+        child.material = this.metalnessOffMaterial
+      }
+    })
+    renderer.render(this.scene, this.camera);
+    this.scene.traverseVisible(child => {
+      child.material = child._SSRPassMaterialBack
+    })
+
+    // restore original state
+
+    renderer.autoClear = originalAutoClear;
+    renderer.setClearColor(this.originalClearColor);
+    renderer.setClearAlpha(originalClearAlpha);
+
+  },
+
+  setSize: function(width, height) {
+
+    this.width = width;
+    this.height = height;
+
+    this.ssrMaterial.defines.MAX_STEP = Math.sqrt(width * width + height * height)
+    this.ssrMaterial.needsUpdate = true
+    this.beautyRenderTarget.setSize(width, height);
+    this.prevRenderTarget.setSize(width, height);
+    this.ssrRenderTarget.setSize(width, height);
+    this.normalRenderTarget.setSize(width, height);
+		// if (this.isSelective)
+			this.metalnessRenderTarget.setSize(width, height);
+    this.blurRenderTarget.setSize(width, height);
+    this.blurRenderTarget2.setSize(width, height);
+    // this.blurRenderTarget3.setSize(width, height);
+
+    this.ssrMaterial.uniforms['resolution'].value.set(width, height);
+    this.ssrMaterial.uniforms['cameraProjectionMatrix'].value.copy(this.camera.projectionMatrix);
+    this.ssrMaterial.uniforms['cameraInverseProjectionMatrix'].value.copy(this.camera.projectionMatrixInverse);
+
+    this.blurMaterial.uniforms['resolution'].value.set(width, height);
+    this.blurMaterial2.uniforms['resolution'].value.set(width, height);
+
+  },
+
+});
+
+SSRPass.OUTPUT = {
+  'Default': 0,
+  'SSR': 1,
+  'Beauty': 3,
+  'Depth': 4,
+  'Normal': 5,
+  'Metalness': 7,
+};
+
+export { SSRPass };

+ 352 - 0
examples/jsm/shaders/SSRShader.js

@@ -0,0 +1,352 @@
+import {
+  Matrix4,
+  Vector2
+} from "../../../build/three.module.js";
+/**
+ * References:
+ * https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html
+ */
+
+var SSRShader = {
+
+  defines: {
+    MAX_STEP: 0,
+    isPerspectiveCamera: true,
+    isDistanceAttenuation: true,
+    isFresnel: true,
+    isInfiniteThick: false,
+    isSelective: false,
+  },
+
+  uniforms: {
+
+    "tDiffuse": { value: null },
+    "tNormal": { value: null },
+    "tMetalness": { value: null },
+    "tDepth": { value: null },
+    "cameraNear": { value: null },
+    "cameraFar": { value: null },
+    "resolution": { value: new Vector2() },
+    "cameraProjectionMatrix": { value: new Matrix4() },
+    "cameraInverseProjectionMatrix": { value: new Matrix4() },
+    "opacity": { value: .5 },
+    "maxDistance": { value: 180 },
+    "cameraRange": { value: 0 },
+    "surfDist": { value: .007 },
+    "thickTolerance": { value: .03 },
+
+  },
+
+  vertexShader: /* glsl */`
+
+    varying vec2 vUv;
+
+    void main() {
+
+			vUv = uv;
+
+			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+    }
+
+  `,
+
+  fragmentShader: /* glsl */`
+		// precision highp float;
+		precision highp sampler2D;
+		varying vec2 vUv;
+		uniform sampler2D tDepth;
+		uniform sampler2D tNormal;
+		uniform sampler2D tMetalness;
+		uniform sampler2D tDiffuse;
+		uniform float cameraRange;
+		uniform vec2 resolution;
+		uniform float opacity;
+		uniform float cameraNear;
+		uniform float cameraFar;
+		uniform float maxDistance;
+		uniform float surfDist;
+		uniform mat4 cameraProjectionMatrix;
+		uniform mat4 cameraInverseProjectionMatrix;
+		uniform float thickTolerance;
+		#include <packing>
+		float pointToLineDistance(vec3 x0, vec3 x1, vec3 x2) {
+			//x0: point, x1: linePointA, x2: linePointB
+			//https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
+			return length(cross(x0-x1,x0-x2))/length(x2-x1);
+		}
+		float pointPlaneDistance(vec3 point,vec3 planePoint,vec3 planeNormal){
+			// https://mathworld.wolfram.com/Point-PlaneDistance.html
+			//// https://en.wikipedia.org/wiki/Plane_(geometry)
+			//// http://paulbourke.net/geometry/pointlineplane/
+			float a=planeNormal.x,b=planeNormal.y,c=planeNormal.z;
+			float x0=point.x,y0=point.y,z0=point.z;
+			float x=planePoint.x,y=planePoint.y,z=planePoint.z;
+			float d=-(a*x+b*y+c*z);
+			float distance=(a*x0+b*y0+c*z0+d)/sqrt(a*a+b*b+c*c);
+			return distance;
+		}
+		float getDepth( const in vec2 uv ) {
+			return texture2D( tDepth, uv ).x;
+		}
+		float getViewZ( const in float depth ) {
+			#ifdef isPerspectiveCamera
+				return perspectiveDepthToViewZ( depth, cameraNear, cameraFar );
+			#else
+				return orthographicDepthToViewZ( depth, cameraNear, cameraFar );
+			#endif
+		}
+		vec3 getViewPosition( const in vec2 uv, const in float depth/*clip space*/, const in float clipW ) {
+			vec4 clipPosition = vec4( ( vec3( uv, depth ) - 0.5 ) * 2.0, 1.0 );//ndc
+			clipPosition *= clipW; //clip
+			return ( cameraInverseProjectionMatrix * clipPosition ).xyz;//view
+		}
+		vec3 getViewNormal( const in vec2 uv ) {
+			return unpackRGBToNormal( texture2D( tNormal, uv ).xyz );
+		}
+		vec2 viewPositionToXY(vec3 viewPosition){
+			vec2 xy;
+			vec4 clip=cameraProjectionMatrix*vec4(viewPosition,1);
+			xy=clip.xy;//clip
+			float clipW=clip.w;
+			xy/=clipW;//NDC
+			xy=(xy+1.)/2.;//uv
+			xy*=resolution;//screen
+			return xy;
+		}
+		void main(){
+			#ifdef isSelective
+				float metalness=texture2D(tMetalness,vUv).r;
+				if(metalness==0.) return;
+			#endif
+
+			float depth = getDepth( vUv );
+			float viewZ = getViewZ( depth );
+			if(-viewZ>=cameraFar) return;
+
+			float clipW = cameraProjectionMatrix[2][3] * viewZ+cameraProjectionMatrix[3][3];
+			vec3 viewPosition=getViewPosition( vUv, depth, clipW );
+
+			vec2 d0=gl_FragCoord.xy;
+			vec2 d1;
+
+			vec3 viewNormal=getViewNormal( vUv );
+
+			#ifdef isPerspectiveCamera
+				vec3 viewIncidenceDir=normalize(viewPosition);
+				vec3 viewReflectDir=reflect(viewIncidenceDir,viewNormal);
+			#else
+				vec3 viewIncidenceDir=vec3(0,0,-1);
+				vec3 viewReflectDir=reflect(viewIncidenceDir,viewNormal);
+			#endif
+
+			float maxReflectRayLen=maxDistance/dot(-viewIncidenceDir,viewNormal);
+			// dot(a,b)==length(a)*length(b)*cos(theta) // https://www.mathsisfun.com/algebra/vectors-dot-product.html
+			// if(a.isNormalized&&b.isNormalized) dot(a,b)==cos(theta)
+			// maxDistance/maxReflectRayLen=cos(theta)
+			// maxDistance/maxReflectRayLen==dot(a,b)
+			// maxReflectRayLen==maxDistance/dot(a,b)
+
+			vec3 d1viewPosition=viewPosition+viewReflectDir*maxReflectRayLen;
+			#ifdef isPerspectiveCamera
+				if(d1viewPosition.z>-cameraNear){
+					//https://tutorial.math.lamar.edu/Classes/CalcIII/EqnsOfLines.aspx
+					float t=(-cameraNear-viewPosition.z)/viewReflectDir.z;
+					d1viewPosition=viewPosition+viewReflectDir*t;
+				}
+			#endif
+			d1=viewPositionToXY(d1viewPosition);
+
+			float totalLen=length(d1-d0);
+			float xLen=d1.x-d0.x;
+			float yLen=d1.y-d0.y;
+			float totalStep=max(abs(xLen),abs(yLen));
+			float xSpan=xLen/totalStep;
+			float ySpan=yLen/totalStep;
+			for(float i=0.;i<MAX_STEP;i++){
+				if(i>=totalStep) break;
+				vec2 xy=vec2(d0.x+i*xSpan,d0.y+i*ySpan);
+				if(xy.x<0.||xy.x>resolution.x||xy.y<0.||xy.y>resolution.y) break;
+				float s=length(xy-d0)/totalLen;
+				vec2 uv=xy/resolution;
+
+				float d = getDepth(uv);
+				float vZ = getViewZ( d );
+				if(-vZ>=cameraFar) continue;
+				float cW = cameraProjectionMatrix[2][3] * vZ+cameraProjectionMatrix[3][3];
+				vec3 vP=getViewPosition( uv, d, cW );
+
+				#ifdef isPerspectiveCamera
+					// https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf
+					float recipVPZ=1./viewPosition.z;
+					float viewReflectRayZ=1./(recipVPZ+s*(1./d1viewPosition.z-recipVPZ));
+					float sD=surfDist*cW;
+				#else
+					float viewReflectRayZ=viewPosition.z+s*(d1viewPosition.z-viewPosition.z);
+					float sD=surfDist;
+				#endif
+				if(viewReflectRayZ-sD>vZ) continue;
+
+				#ifdef isInfiniteThick
+					if(viewReflectRayZ+thickTolerance*clipW<vP.z) break;
+				#endif
+				float away=pointToLineDistance(vP,viewPosition,d1viewPosition);
+
+				float op=opacity;
+
+				if(away<sD){
+					vec3 vN=getViewNormal( uv );
+					if(dot(viewReflectDir,vN)>=0.) continue;
+					float distance=pointPlaneDistance(vP,viewPosition,viewNormal);
+					if(distance>maxDistance) break;
+					#ifdef isDistanceAttenuation
+						float ratio=1.-(distance/maxDistance);
+						float attenuation=ratio*ratio;
+						op=opacity*attenuation;
+					#endif
+					#ifdef isFresnel
+						float fresnel=(dot(viewIncidenceDir,viewReflectDir)+1.)/2.;
+						op*=fresnel;
+					#endif
+					vec4 reflectColor=texture2D(tDiffuse,uv);
+					gl_FragColor.xyz=reflectColor.xyz;
+					gl_FragColor.a=op;
+					break;
+				}
+			}
+		}
+	`
+
+};
+
+var SSRDepthShader = {
+
+  defines: {
+    "PERSPECTIVE_CAMERA": 1
+  },
+
+  uniforms: {
+
+    "tDepth": { value: null },
+    "cameraNear": { value: null },
+    "cameraFar": { value: null },
+
+  },
+
+  vertexShader: /* glsl */`
+
+    varying vec2 vUv;
+
+    void main() {
+
+    	vUv = uv;
+    	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+    }
+
+  `,
+
+  fragmentShader: /* glsl */`
+
+    uniform sampler2D tDepth;
+
+    uniform float cameraNear;
+    uniform float cameraFar;
+
+    varying vec2 vUv;
+
+    #include <packing>
+
+		float getLinearDepth( const in vec2 uv ) {
+
+			#if PERSPECTIVE_CAMERA == 1
+
+				float fragCoordZ = texture2D( tDepth, uv ).x;
+				float viewZ = perspectiveDepthToViewZ( fragCoordZ, cameraNear, cameraFar );
+				return viewZToOrthographicDepth( viewZ, cameraNear, cameraFar );
+
+			#else
+
+				return texture2D( tDepth, uv ).x;
+
+			#endif
+
+		}
+
+    void main() {
+
+    	float depth = getLinearDepth( vUv );
+			float d = 1.0 - depth;
+			// d=(d-.999)*1000.;
+    	gl_FragColor = vec4( vec3( d ), 1.0 );
+
+    }
+
+  `
+
+};
+
+var SSRBlurShader = {
+
+  uniforms: {
+
+    "tDiffuse": { value: null },
+    "resolution": { value: new Vector2() },
+    "opacity": { value: .5 },
+
+  },
+
+  vertexShader: /* glsl */`
+
+    varying vec2 vUv;
+
+    void main() {
+
+    	vUv = uv;
+    	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+    }
+
+  `,
+
+  fragmentShader: /* glsl */`
+
+    uniform sampler2D tDiffuse;
+    uniform vec2 resolution;
+    varying vec2 vUv;
+    void main() {
+			//reverse engineering from PhotoShop blur filter, then change coefficient
+
+    	vec2 texelSize = ( 1.0 / resolution );
+
+			vec4 c=texture2D(tDiffuse,vUv);
+
+			vec2 offset;
+
+			offset=(vec2(-1,0))*texelSize;
+			vec4 cl=texture2D(tDiffuse,vUv+offset);
+
+			offset=(vec2(1,0))*texelSize;
+			vec4 cr=texture2D(tDiffuse,vUv+offset);
+
+			offset=(vec2(0,-1))*texelSize;
+			vec4 cb=texture2D(tDiffuse,vUv+offset);
+
+			offset=(vec2(0,1))*texelSize;
+			vec4 ct=texture2D(tDiffuse,vUv+offset);
+
+			// float coeCenter=.5;
+			// float coeSide=.125;
+			float coeCenter=.2;
+			float coeSide=.2;
+			float a=c.a*coeCenter+cl.a*coeSide+cr.a*coeSide+cb.a*coeSide+ct.a*coeSide;
+			vec3 rgb=(c.rgb*c.a*coeCenter+cl.rgb*cl.a*coeSide+cr.rgb*cr.a*coeSide+cb.rgb*cb.a*coeSide+ct.rgb*ct.a*coeSide)/a;
+			gl_FragColor=vec4(rgb,a);
+
+		}
+	`
+
+
+};
+
+export { SSRShader, SSRDepthShader, SSRBlurShader };

BIN
examples/screenshots/webgl_postprocessing_ssr.jpg


+ 307 - 0
examples/webgl_postprocessing_ssr.html

@@ -0,0 +1,307 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+
+	<head>
+		<title>three.js webgl - postprocessing - Screen Space Reflection</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+
+<body>
+	<div id="container"></div>
+	<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> -
+		<a href="https://github.com/mrdoob/three.js/pull/20156" target="_blank" rel="noopener">SSRPass</a> demo by <a href="https://github.com/gonnavis" target="_blank">Vis</a>.<br />
+	</div>
+
+	<script type="module">
+		import * as THREE from '../build/three.module.js';
+
+		import Stats from './jsm/libs/stats.module.js';
+
+		import { OrbitControls } from './jsm/controls/OrbitControls.js'
+
+		import { GUI } from './jsm/libs/dat.gui.module.js';
+		import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';
+		import { SSRPass } from './jsm/postprocessing/SSRPass.js';
+		import { Reflector } from './jsm/objects/ReflectorForSSRPass.js';
+
+		import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
+
+		let params = {
+			enableSSR: true,
+			autoRotate: true,
+			isOtherMeshes: true,
+			isGroundReflector: true,
+		}
+		let composer
+		let ssrPass
+		let gui
+		let isPerspectiveCamera = true
+		let stats;
+		let mesh;
+		let controls;
+		let camera, scene, renderer;
+		let otherMeshes = []
+		let groundReflector
+		let selects=[]
+
+		const container = document.querySelector('#container');
+
+		// Configure and create Draco decoder.
+		const dracoLoader = new DRACOLoader();
+		dracoLoader.setDecoderPath('js/libs/draco/');
+		dracoLoader.setDecoderConfig({ type: 'js' });
+
+		init();
+		animate();
+
+		function init() {
+
+			camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 15);
+			camera.position.set(0.13271600513224902, 0.3489546826045913, 0.43921296427927076);
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color(0x443333);
+			scene.fog = new THREE.Fog(0x443333, 1, 4);
+
+			// Ground
+			const plane = new THREE.Mesh(
+				new THREE.PlaneGeometry(8, 8),
+				new THREE.MeshPhongMaterial({ color: 0x999999, specular: 0x101010 })
+			);
+			plane.rotation.x = - Math.PI / 2;
+			plane.position.y = 0.0365;
+			// plane.receiveShadow = true;
+			scene.add(plane);
+
+			// Lights
+			const hemiLight = new THREE.HemisphereLight(0x443333, 0x111122);
+			scene.add(hemiLight);
+
+			const spotLight = new THREE.SpotLight();
+			spotLight.angle = Math.PI / 16;
+			spotLight.penumbra = 0.5;
+			// spotLight.castShadow = true;
+			spotLight.position.set(- 1, 1, 1);
+			scene.add(spotLight);
+
+			dracoLoader.load('models/draco/bunny.drc', function (geometry) {
+
+				geometry.computeVertexNormals();
+
+				const material = new THREE.MeshStandardMaterial({ color: 0x606060 });
+				const mesh = new THREE.Mesh(geometry, material);
+				// mesh.castShadow = true;
+				// mesh.receiveShadow = true;
+				scene.add(mesh);
+				selects.push(mesh)
+
+				// Release decoder resources.
+				dracoLoader.dispose();
+
+			});
+
+			{
+				const geometry = new THREE.BoxBufferGeometry(.05, .05, .05);
+				const material = new THREE.MeshStandardMaterial({ color: 'green' })
+				const mesh = new THREE.Mesh(geometry, material)
+				mesh.position.set(-.12, plane.position.y + .025, .015)
+				scene.add(mesh)
+				otherMeshes.push(mesh)
+				selects.push(mesh)
+			}
+			{
+				const geometry = new THREE.IcosahedronBufferGeometry(.025, 4);
+				const material = new THREE.MeshStandardMaterial({ color: 'cyan' })
+				const mesh = new THREE.Mesh(geometry, material)
+				mesh.position.set(-.05, plane.position.y + .025, .08)
+				scene.add(mesh)
+				otherMeshes.push(mesh)
+				selects.push(mesh)
+			}
+			{
+				const geometry = new THREE.ConeBufferGeometry(.025, .05, 64);
+				const material = new THREE.MeshStandardMaterial({ color: 'yellow' })
+				const mesh = new THREE.Mesh(geometry, material)
+				mesh.position.set(-.05, plane.position.y + .025, -.055)
+				scene.add(mesh)
+				otherMeshes.push(mesh)
+				selects.push(mesh)
+			}
+			{
+				const geometry = new THREE.PlaneBufferGeometry(8, 8);
+				groundReflector = new Reflector(geometry, {
+					clipBias: 0.003,
+					textureWidth: window.innerWidth,
+					textureHeight: window.innerHeight,
+					color: 0x888888,
+					useDepthTexture: true,
+				});
+				groundReflector.material.depthWrite = false;
+				groundReflector.position.y = plane.position.y + .0001;
+				groundReflector.rotation.x = -Math.PI / 2
+				groundReflector.visible = false
+				scene.add(groundReflector);
+			}
+
+			// renderer
+			renderer = new THREE.WebGLRenderer({ antialias: false });
+			// renderer.setPixelRatio(window.devicePixelRatio);
+			renderer.setSize(window.innerWidth, window.innerHeight);
+			renderer.outputEncoding = THREE.sRGBEncoding;
+			// renderer.shadowMap.enabled = true;
+			container.appendChild(renderer.domElement);
+
+			//
+
+			controls = new OrbitControls(camera, renderer.domElement);
+			controls.enableDamping = true
+			controls.target.set(0, 0.1, 0);
+			controls.update()
+			controls.enabled = !params.autoRotate
+
+			// STATS
+
+			stats = new Stats();
+			container.appendChild(stats.dom);
+
+			window.addEventListener('resize', onWindowResize, false);
+
+			// composer
+
+			composer = new EffectComposer(renderer);
+			ssrPass = new SSRPass({
+				renderer,
+				scene,
+				camera,
+				width: innerWidth,
+				height: innerHeight,
+				encoding: THREE.sRGBEncoding,
+				isPerspectiveCamera: isPerspectiveCamera,
+				groundReflector: params.isGroundReflector ? groundReflector : null,
+				selects: params.isGroundReflector ? selects : null,
+				// morphTargets: true,
+			});
+			// window.ssrPass = ssrPass
+			composer.addPass(ssrPass);
+
+			// GUI
+
+			gui = new GUI();
+			gui.add(params, 'enableSSR').name('Enable SSR');
+			gui.add(params, 'isGroundReflector').onChange(() => {
+				if (params.isGroundReflector) {
+					ssrPass.groundReflector = groundReflector,
+					ssrPass.selects = selects
+				} else {
+					ssrPass.groundReflector = null,
+					ssrPass.selects = null
+				}
+			})
+			gui.add(params, 'autoRotate').onChange(() => {
+				controls.enabled = !params.autoRotate
+			})
+
+			let folder = gui.addFolder( 'more settings' )
+			folder.add(ssrPass, 'isFresnel').onChange(()=>{
+				groundReflector.isFresnel=ssrPass.isFresnel
+			});
+			folder.add(ssrPass, 'isDistanceAttenuation').onChange(()=>{
+				groundReflector.isDistanceAttenuation=ssrPass.isDistanceAttenuation
+			});
+			ssrPass.maxDistance = .1
+			groundReflector.maxDistance = ssrPass.maxDistance
+			folder.add(ssrPass, 'maxDistance').min(0).max(.5).step(.001).onChange(()=>{
+				groundReflector.maxDistance=ssrPass.maxDistance
+			});
+			folder.add(params, 'isOtherMeshes').onChange(() => {
+				if (params.isOtherMeshes) {
+					otherMeshes.forEach(mesh => mesh.visible = true)
+				} else {
+					otherMeshes.forEach(mesh => mesh.visible = false)
+				}
+			})
+			folder.add(ssrPass, 'isBouncing')
+			folder.add(ssrPass, 'output', {
+				'Default': SSRPass.OUTPUT.Default,
+				'SSR Only': SSRPass.OUTPUT.SSR,
+				'Beauty': SSRPass.OUTPUT.Beauty,
+				'Depth': SSRPass.OUTPUT.Depth,
+				'Normal': SSRPass.OUTPUT.Normal,
+				'Metalness': SSRPass.OUTPUT.Metalness,
+			}).onChange(function (value) {
+
+				ssrPass.output = parseInt(value);
+
+			});
+			ssrPass.opacity = 1
+			groundReflector.opacity=ssrPass.opacity
+			folder.add(ssrPass, 'opacity').min(0).max(1).onChange(()=>{
+				groundReflector.opacity=ssrPass.opacity
+			});
+			if (isPerspectiveCamera) {
+				ssrPass.surfDist = 0.0015
+				folder.add(ssrPass, 'surfDist').min(0).max(.005).step(.0001);
+			} else {
+				ssrPass.surfDist = 2
+				folder.add(ssrPass, 'surfDist').min(0).max(7).step(.01);
+			}
+			folder.add(ssrPass, 'isInfiniteThick');
+			folder.add(ssrPass, 'thickTolerance').min(0).max(.05).step(.0001);
+			folder.add(ssrPass, 'isBlur');
+			// gui.close()
+
+		}
+
+
+		function onWindowResize() {
+
+			camera.aspect = window.innerWidth / window.innerHeight;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize(window.innerWidth, window.innerHeight);
+			composer.setSize(window.innerWidth, window.innerHeight);
+			groundReflector.getRenderTarget().setSize(window.innerWidth, window.innerHeight);
+
+		}
+
+		function animate() {
+
+			requestAnimationFrame(animate);
+
+			stats.begin();
+			render();
+			stats.end();
+
+		}
+
+		function render() {
+
+			if(params.autoRotate){
+				const timer = Date.now() * 0.0003;
+
+				camera.position.x = Math.sin(timer) * 0.5;
+				camera.position.y = 0.25;
+				camera.position.z = Math.cos(timer) * 0.5;
+				camera.lookAt(0, 0.1, 0);
+			}else {
+				controls.update()
+			}
+
+			if (params.enableSSR) {
+				///todo: groundReflector has full ground info, need use it to solve reflection gaps problem on objects when camera near ground.
+				///todo: the normal info where groundReflector reflected need to be changed.
+				composer.render();
+			} else {
+				renderer.render(scene, camera)
+			}
+
+		}
+	</script>
+</body>
+
+</html>