├── .gitignore ├── README.md ├── assets ├── aerodynamics_workshop.jpg ├── aerodynamics_workshop_blur3.jpg ├── floor5lr.jpg └── statue.glb ├── backup.rar ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── programs ├── blit.js ├── doubleDepthBuffer.js ├── skybox.js └── ssrtGlass.js └── screenshot.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /assets_backup -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SS-refraction-through-depth-peeling-in-threejs 2 | Screen space refraction through depth peeling in threejs 3 | 4 | [Live demo here](https://domenicobrz.github.io/webgl/projects/SSRefractionDepthPeeling/) 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/aerodynamics_workshop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/assets/aerodynamics_workshop.jpg -------------------------------------------------------------------------------- /assets/aerodynamics_workshop_blur3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/assets/aerodynamics_workshop_blur3.jpg -------------------------------------------------------------------------------- /assets/floor5lr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/assets/floor5lr.jpg -------------------------------------------------------------------------------- /assets/statue.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/assets/statue.glb -------------------------------------------------------------------------------- /backup.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/backup.rar -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "./node_modules/three/build/three.module.js"; 2 | import { OrbitControls } from "./node_modules/three/examples/jsm/controls/OrbitControls.js"; //'three/examples/jsm/controls/OrbitControls.js'; 3 | import { DoubleDepthBuffer } from "./programs/doubleDepthBuffer.js"; 4 | import { Blit } from "./programs/blit.js"; 5 | import { Skybox } from "./programs/skybox.js"; 6 | import { SSRTGlass } from "./programs/ssrtGlass.js"; 7 | import { GLTFLoader } from './node_modules/three/examples/jsm/loaders/GLTFLoader.js'; 8 | import * as dat from "../node_modules/dat.gui/build/dat.gui.module.js"; 9 | 10 | window.addEventListener("load", init); 11 | 12 | let scene; 13 | let camera; 14 | let controls; 15 | let renderer; 16 | 17 | 18 | let ddbProgram; 19 | let blitProgram; 20 | let skyboxProgram; 21 | let ssrtGlassProgram; 22 | 23 | let dlCount = 0; 24 | 25 | 26 | function init() { 27 | renderer = new THREE.WebGLRenderer({ antialias: true }); 28 | renderer.setSize( window.innerWidth, window.innerHeight ); 29 | renderer.toneMapping = THREE.ACESFilmicToneMapping; 30 | renderer.toneMappingExposure = 0.8; 31 | renderer.outputEncoding = THREE.sRGBEncoding; 32 | document.body.appendChild( renderer.domElement ); 33 | 34 | scene = new THREE.Scene(); 35 | camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 100 ); 36 | 37 | controls = new OrbitControls( camera, renderer.domElement ); 38 | controls.enableDamping = true; 39 | controls.dampingFactor = 0.0375; 40 | controls.enablePan = true; 41 | controls.panSpeed = 0.5; 42 | controls.screenSpacePanning = true; 43 | 44 | //controls.update() must be called after any manual changes to the camera's transform 45 | camera.position.set( 0, 5, -10 ); 46 | controls.target.set( 0, 2, 0 ); 47 | controls.update(); 48 | 49 | 50 | 51 | let path = "assets/aerodynamics_workshop.jpg"; 52 | // path = "assets/wooden_motel_4k.jpg"; 53 | // path = "assets/reading_room.jpg"; 54 | // path = "assets/birbeck_street_underpass.jpg"; 55 | // path = "assets/sculpture_exhibition.jpg"; 56 | path = "assets/aerodynamics_workshop_blur3.jpg"; 57 | 58 | let radpath = path; 59 | radpath = "assets/aerodynamics_workshop.jpg"; 60 | // radpath = "assets/sculpture_exhibition.jpg"; 61 | // radpath = "assets/birbeck_street_underpass.jpg"; 62 | 63 | // here's a list of good ones: 64 | let skybox = new THREE.TextureLoader().load(path, function(texture) { 65 | // ************ necessary to avoid seams in the equirectangular map !! *************** 66 | texture.generateMipmaps = false; 67 | texture.wrapS = THREE.ClampToEdgeWrapping; 68 | texture.wrapT = THREE.ClampToEdgeWrapping; 69 | texture.magFilter = THREE.LinearFilter; 70 | texture.minFilter = THREE.LinearFilter; 71 | // ************ necessary to avoid seams in the equirectangular map !! - END *************** 72 | 73 | skybox = texture; 74 | onDlComplete(); 75 | }); 76 | 77 | let radbox = new THREE.TextureLoader().load(radpath, function(texture) { 78 | radbox = texture; 79 | radbox.wrapS = THREE.ClampToEdgeWrapping; 80 | radbox.wrapT = THREE.ClampToEdgeWrapping; 81 | radbox.magFilter = THREE.LinearMipmapLinearFilter; 82 | radbox.minFilter = THREE.LinearMipmapLinearFilter; 83 | onDlComplete(); 84 | }); 85 | 86 | let mesh; 87 | new GLTFLoader().load("assets/statue.glb", 88 | function ( gltf ) { 89 | mesh = gltf.scene.children[0]; 90 | mesh.position.set(0,0,0); 91 | // mesh.rotation.x = Math.PI * 0.5; 92 | mesh.scale.set(2, 2, 2); 93 | // necessary since the mesh by default is rotated 94 | mesh.rotation.set(0,0,0); 95 | 96 | // manually recalculating normals & positions 97 | mesh.traverse((child) => { 98 | if (child instanceof THREE.Mesh) { 99 | let normals = child.geometry.attributes.normal.array; 100 | for(let i = 0; i < normals.length; i+=3) { 101 | let temp = normals[i+2]; 102 | normals[i+2] = normals[i+1]; 103 | normals[i+1] = -temp; 104 | } 105 | 106 | let positions = child.geometry.attributes.position.array; 107 | for(let i = 0; i < positions.length; i+=3) { 108 | let temp = positions[i+2]; 109 | positions[i+2] = positions[i+1]; 110 | positions[i+1] = -temp; 111 | } 112 | } 113 | }); 114 | 115 | onDlComplete(); 116 | } 117 | ); 118 | 119 | 120 | function onDlComplete() { 121 | dlCount++; 122 | if(dlCount < 3) return; 123 | 124 | // ************** THIS MESH IS CLONED INSIDE DDB & SSRT ************** 125 | // let mesh = new THREE.Mesh( 126 | // // new THREE.SphereBufferGeometry(0.5,15,15), 127 | // // new THREE.BoxBufferGeometry(2,2,2), 128 | // new THREE.TorusKnotBufferGeometry( 1, 0.45, 100, 16 ), 129 | // new THREE.MeshBasicMaterial({ color: 0xff0000 }) 130 | // ); 131 | scene.add(mesh); 132 | // ************** THIS MESH IS CLONED INSIDE DDB & SSRT - END ************** 133 | 134 | 135 | 136 | 137 | blitProgram = new Blit(renderer, "gl_FragColor = vec4(texture2D(uTexture, vUv).www, 1.0);"); 138 | ddbProgram = new DoubleDepthBuffer(mesh, camera, renderer); 139 | skyboxProgram = new Skybox(skybox, camera, renderer); 140 | ssrtGlassProgram = new SSRTGlass(mesh, radbox, camera, renderer); 141 | 142 | initGUI(); 143 | animate(); 144 | } 145 | } 146 | 147 | function animate(now) { 148 | now *= 0.001; 149 | 150 | requestAnimationFrame( animate ); 151 | 152 | controls.update(); 153 | 154 | skyboxProgram.render(); 155 | ddbProgram.compute(6); 156 | ssrtGlassProgram.render(now, ddbProgram.getBackFaceTexture(), ddbProgram.getFrontFaceTexture()); 157 | } 158 | 159 | 160 | function initGUI() { 161 | var FizzyText = function() { 162 | // this.extintionColor1 = [255 - Math.floor(0.25 * 255), 255 - Math.floor(0.7 * 255), 255 - Math.floor(0.9 * 255)]; 163 | this.extintionColor1 = [192, 123, 25]; 164 | this.extintionColor2 = [255 - Math.floor(0.9 * 255), 255 - Math.floor(0.35 * 255), 255 - Math.floor(0.25 * 255)]; 165 | this.extintionFactor = 5; 166 | this.reflectionFactor = 1; 167 | this.exposure = 0; 168 | this.extintionCol1Random = false; 169 | this.extintionCol2Random = false; 170 | this.waveRaymarch = false; 171 | this.targRadMult = 1.0; 172 | this.copy = () => { 173 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.x = (1 - this.extintionColor1[0] / 255); 174 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.y = (1 - this.extintionColor1[1] / 255); 175 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.z = (1 - this.extintionColor1[2] / 255); 176 | }; 177 | this.uncopy = () => { 178 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.x = (1 - this.extintionColor2[0] / 255); 179 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.y = (1 - this.extintionColor2[1] / 255); 180 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.z = (1 - this.extintionColor2[2] / 255); 181 | }; 182 | }; 183 | 184 | var text = new FizzyText(); 185 | 186 | var gui = new dat.GUI(); 187 | gui.add(text, 'extintionFactor', 0, 10).onChange((value) => { 188 | ssrtGlassProgram.material.uniforms.uExtintionFactor.value = value; 189 | }); 190 | gui.add(text, 'reflectionFactor', 0, 2).onChange((value) => { 191 | ssrtGlassProgram.material.uniforms.uReflectionFactor.value = value; 192 | }); 193 | gui.add(text, 'exposure', -1, 2).onChange((value) => { 194 | ssrtGlassProgram.material.uniforms.uExposure.value = value; 195 | }); 196 | gui.addColor(text, 'extintionColor1').onChange((value) => { 197 | ssrtGlassProgram.material.uniforms.uExtintionColor1.value.x = (1 - value[0] / 255); 198 | ssrtGlassProgram.material.uniforms.uExtintionColor1.value.y = (1 - value[1] / 255); 199 | ssrtGlassProgram.material.uniforms.uExtintionColor1.value.z = (1 - value[2] / 255); 200 | }); 201 | gui.addColor(text, 'extintionColor2').onChange((value) => { 202 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.x = (1 - value[0] / 255); 203 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.y = (1 - value[1] / 255); 204 | ssrtGlassProgram.material.uniforms.uExtintionColor2.value.z = (1 - value[2] / 255); 205 | }); 206 | gui.add(text, 'copy'); 207 | gui.add(text, 'uncopy'); 208 | var fx = gui.addFolder("FX"); 209 | fx.add(text, 'extintionCol1Random').onChange((value) => { 210 | ssrtGlassProgram.material.uniforms.uExtinctionFX1.value.x = value ? 1 : 0; 211 | }); 212 | fx.add(text, 'extintionCol2Random').onChange((value) => { 213 | ssrtGlassProgram.material.uniforms.uExtinctionFX1.value.y = value ? 1 : 0; 214 | }); 215 | fx.add(text, 'waveRaymarch').onChange((value) => { 216 | ssrtGlassProgram.material.uniforms.uExtinctionFX1.value.z = value ? 1 : 0; 217 | }); 218 | fx.add(text, 'targRadMult', 0, 2).onChange((value) => { 219 | ssrtGlassProgram.material.uniforms.uExtinctionFX1.value.w = value; 220 | }); 221 | fx.open(); 222 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Screen-space-RT-glass", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "dat.gui": { 8 | "version": "0.7.7", 9 | "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", 10 | "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==" 11 | }, 12 | "three": { 13 | "version": "0.115.0", 14 | "resolved": "https://registry.npmjs.org/three/-/three-0.115.0.tgz", 15 | "integrity": "sha512-mAV2Ky3RdcbdSbR9capI+tKLvRldWYxd4151PZTT/o7+U2jh9Is3a4KmnYwzyUAhB2ZA3pXSgCd2DOY4Tj5kow==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screen-space-rt-glass", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dat.gui": "^0.7.7", 14 | "three": "^0.115.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /programs/blit.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../node_modules/three/build/three.module.js"; 2 | 3 | class Blit { 4 | constructor(renderer, customFragment) { 5 | 6 | this.material = new THREE.ShaderMaterial({ 7 | uniforms: { 8 | uTexture: { type: "t", value: null } 9 | }, 10 | 11 | vertexShader: ` 12 | varying vec2 vUv; 13 | 14 | void main() { 15 | vUv = uv; 16 | gl_Position = vec4(position.xy, 0.0, 1.0); 17 | }`, 18 | 19 | fragmentShader: ` 20 | uniform sampler2D uTexture; 21 | 22 | varying vec2 vUv; 23 | 24 | void main() { 25 | ${ customFragment ? customFragment : "gl_FragColor = texture2D(uTexture, vUv);" } 26 | }`, 27 | 28 | depthTest: false, 29 | depthWrite: false, 30 | }); 31 | 32 | this.mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(2,2), this.material); 33 | this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 ); 34 | this.renderer = renderer; 35 | 36 | this.scene = new THREE.Scene(); 37 | this.scene.add(this.mesh); 38 | } 39 | 40 | blit(textureFrom, renderTargetDest) { 41 | this.renderer.setRenderTarget(renderTargetDest); 42 | 43 | this.material.uniforms.uTexture.value = textureFrom; 44 | this.renderer.render(this.scene, this.camera); 45 | 46 | this.renderer.setRenderTarget(null); 47 | } 48 | } 49 | 50 | export { Blit }; -------------------------------------------------------------------------------- /programs/doubleDepthBuffer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../node_modules/three/build/three.module.js"; 2 | import { Blit } from "../programs/blit.js"; 3 | 4 | 5 | class DoubleDepthBuffer { 6 | constructor(mesh, camera, renderer) { 7 | this.mesh = mesh.clone(); 8 | this.camera = camera; 9 | this.renderer = renderer; 10 | 11 | this.scene = new THREE.Scene(); 12 | this.scene.add(this.mesh); 13 | 14 | this.blitProgram = new Blit(this.renderer); 15 | 16 | 17 | this.ping = new THREE.WebGLRenderTarget(innerWidth, innerHeight, { 18 | type: THREE.FloatType, 19 | depthBuffer: false, 20 | stencilBuffer: false, 21 | }); 22 | 23 | this.pong = new THREE.WebGLRenderTarget(innerWidth, innerHeight, { 24 | type: THREE.FloatType, 25 | depthBuffer: false, 26 | stencilBuffer: false, 27 | }); 28 | 29 | this.frontFaceRT = new THREE.WebGLRenderTarget(innerWidth, innerHeight, { 30 | type: THREE.FloatType, 31 | }); 32 | 33 | this.frontFaceMaterial = new THREE.ShaderMaterial({ 34 | uniforms: { 35 | uCameraFarInverse: { value: 1 / this.camera.far }, 36 | }, 37 | 38 | vertexShader: ` 39 | varying vec3 vCameraSpacePos; 40 | varying vec3 vWorldSpaceNormal; 41 | 42 | void main() { 43 | vCameraSpacePos = (modelViewMatrix * vec4(position, 1.0)).xyz; 44 | vWorldSpaceNormal = normal; 45 | 46 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 47 | }`, 48 | 49 | fragmentShader: ` 50 | uniform float uCameraFarInverse; 51 | 52 | varying vec3 vWorldSpaceNormal; 53 | varying vec3 vCameraSpacePos; 54 | 55 | void main() { 56 | float currentDepth = abs(vCameraSpacePos.z) * uCameraFarInverse; 57 | gl_FragColor = vec4(vWorldSpaceNormal, currentDepth); 58 | }`, 59 | 60 | depthTest: true, 61 | depthWrite: true, 62 | side: THREE.FrontSide, 63 | }); 64 | 65 | 66 | this.material = new THREE.ShaderMaterial({ 67 | uniforms: { 68 | uScreenSize: { value: new THREE.Vector2(innerWidth, innerHeight) }, 69 | uPrevDepth: { type: "t", value: this.ping.texture }, 70 | uCameraFarInverse: { value: 1 / this.camera.far }, 71 | uSample: { value: 0 }, 72 | }, 73 | 74 | vertexShader: ` 75 | varying vec3 vCameraSpacePos; 76 | varying vec3 vWorldSpaceNormal; 77 | 78 | void main() { 79 | vCameraSpacePos = (modelViewMatrix * vec4(position, 1.0)).xyz; 80 | vWorldSpaceNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz); 81 | 82 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 83 | }`, 84 | 85 | fragmentShader: ` 86 | uniform sampler2D uPrevDepth; 87 | uniform float uCameraFarInverse; 88 | uniform float uSample; 89 | uniform vec2 uScreenSize; 90 | 91 | varying vec3 vWorldSpaceNormal; 92 | varying vec3 vCameraSpacePos; 93 | 94 | void main() { 95 | 96 | vec2 uv = gl_FragCoord.xy / uScreenSize; 97 | float prevRegisteredDepth = texture2D(uPrevDepth, uv).w; 98 | float currentDepth = abs(vCameraSpacePos.z) * uCameraFarInverse; 99 | 100 | if(currentDepth <= prevRegisteredDepth) { 101 | discard; 102 | } 103 | 104 | gl_FragColor = vec4(vWorldSpaceNormal, currentDepth); 105 | }`, 106 | 107 | depthTest: false, 108 | depthWrite: false, 109 | side: THREE.DoubleSide, 110 | }); 111 | 112 | 113 | this.mesh.traverse((child) => { 114 | if (child instanceof THREE.Mesh) { 115 | child.material = this.material; 116 | } 117 | }); 118 | } 119 | 120 | compute(samples) { 121 | // *********** render back face material *********** 122 | this.renderer.setRenderTarget(this.ping); 123 | this.renderer.clear(); 124 | this.renderer.setRenderTarget(this.pong); 125 | this.renderer.clear(); 126 | 127 | this.mesh.traverse((child) => { 128 | if (child instanceof THREE.Mesh) { 129 | child.material = this.material; 130 | } 131 | }); 132 | 133 | this.material.uniforms.uCameraFarInverse.value = 1 / this.camera.far; 134 | 135 | for(let i = 0; i < samples; i++) { 136 | let p1 = (i % 2) === 0 ? this.ping : this.pong; 137 | let p2 = (i % 2) === 0 ? this.pong : this.ping; 138 | 139 | this.material.uniforms.uPrevDepth.value = p1.texture; 140 | this.material.uniforms.uSample.value = i; 141 | 142 | this.renderer.autoClear = false; 143 | this.renderer.setRenderTarget(p2); 144 | this.renderer.render(this.scene, this.camera); 145 | this.renderer.autoClear = true; 146 | 147 | // this will make sure that if all fragments fail the depth test and get discarded, we at least have the fallback texture computed 148 | this.blitProgram.blit(p2.texture, p1); 149 | } 150 | 151 | if(samples % 2 === 0) { 152 | this.resultBuffer = this.ping; 153 | } else { 154 | this.resultBuffer = this.pong; 155 | } 156 | // *********** render back face material *********** 157 | 158 | 159 | 160 | 161 | // *********** render front face material *********** 162 | this.mesh.traverse((child) => { 163 | if (child instanceof THREE.Mesh) { 164 | child.material = this.frontFaceMaterial; 165 | } 166 | }); 167 | this.renderer.setRenderTarget(this.frontFaceRT); 168 | this.renderer.render(this.scene, this.camera); 169 | // *********** render front face material - END *********** 170 | } 171 | 172 | getBackFaceTexture() { 173 | return this.resultBuffer.texture; 174 | } 175 | getFrontFaceTexture() { 176 | return this.frontFaceRT.texture; 177 | } 178 | } 179 | 180 | export { DoubleDepthBuffer }; -------------------------------------------------------------------------------- /programs/skybox.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../node_modules/three/build/three.module.js"; 2 | 3 | class Skybox { 4 | constructor(texture, camera, renderer, args) { 5 | 6 | this.material = new THREE.ShaderMaterial({ 7 | uniforms: { 8 | uSkybox: { type: "t", value: texture } 9 | }, 10 | 11 | vertexShader: ` 12 | varying vec3 vFragPos; 13 | 14 | void main() { 15 | vFragPos = position.xyz; // v 0.0 v <-- we don't want translations 16 | // gl_Position = projectionMatrix * vec4((modelViewMatrix * vec4(position, 0.0)), 1.0); 17 | 18 | vec4 viewSpace = vec4(mat3(modelViewMatrix) * position, 0.0); 19 | viewSpace.w = 1.0; 20 | gl_Position = projectionMatrix * viewSpace; 21 | }`, 22 | 23 | fragmentShader: ` 24 | uniform sampler2D uSkybox; 25 | 26 | varying vec3 vFragPos; 27 | 28 | const float PI = 3.14159265359; 29 | 30 | void main() { 31 | 32 | vec3 dir = normalize(vFragPos); 33 | float v = (asin(dir.y) + PI * 0.5) / (PI); 34 | float u = (atan(dir.x, dir.z) + PI) / (PI * 2.0); 35 | 36 | gl_FragColor = texture2D(uSkybox, vec2(u, v)); 37 | }`, 38 | 39 | side: THREE.BackSide, 40 | depthWrite: false, 41 | }); 42 | 43 | this.mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(10,10,10), this.material); 44 | this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 100 ); 45 | this.mainCamera = camera; 46 | this.renderer = renderer; 47 | 48 | this.scene = new THREE.Scene(); 49 | this.scene.add(this.mesh); 50 | } 51 | 52 | render() { 53 | // I have to do it this way since camera.quaternion is read only 54 | var vector = new THREE.Vector3( 0, 0, - 1 ); 55 | vector.applyQuaternion( this.mainCamera.quaternion ); 56 | this.camera.lookAt( vector ); 57 | 58 | this.renderer.setRenderTarget(null); 59 | this.renderer.render(this.scene, this.camera); 60 | } 61 | } 62 | 63 | export { Skybox }; -------------------------------------------------------------------------------- /programs/ssrtGlass.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../node_modules/three/build/three.module.js"; 2 | 3 | class SSRTGlass { 4 | constructor(mesh, skybox, camera, renderer) { 5 | 6 | this.material = new THREE.ShaderMaterial({ 7 | uniforms: { 8 | uSkybox: { type: "t", value: skybox }, 9 | uBackFaceBuffer: { type: "t", value: null }, 10 | uFrontFaceBuffer: { type: "t", value: null }, 11 | uCameraFarInverse: { value: 1 / camera.far }, 12 | 13 | uScreenSizeInv: { value: new THREE.Vector2(1 / innerWidth, 1 / innerHeight) }, 14 | uCameraPos: { value: new THREE.Vector3(0,0,0) }, 15 | 16 | uTime: { value: 0 }, 17 | 18 | // uExtintionColor1: { value: new THREE.Vector3(0.25, 0.7, 0.9) }, 19 | uExtintionColor1: { value: new THREE.Vector3(1 - 192/255, 1 - 123/255, 1 - 25/255) }, 20 | uExtintionColor2: { value: new THREE.Vector3(0.9, 0.35, 0.25) }, 21 | uExtintionFactor: { value: 5 }, 22 | uExposure: { value: 0 }, 23 | uReflectionFactor: { value: 1 }, 24 | uExtinctionFX1: { value: new THREE.Vector4(0, 0, 0, 1) }, 25 | }, 26 | 27 | vertexShader: ` 28 | varying vec3 vWorldSpaceFragPos; 29 | varying vec3 vWorldSpaceNormal; 30 | // NOTE: we don't need the projViewModel matrix, because vWorldSpaceFragPos is already multiplied by the model matrix 31 | // I'm repeating this comment 5 times because I've lost 2 hours of my life debugging this thing 32 | varying mat4 vProjViewMatrix; 33 | varying mat4 vViewMatrix; 34 | 35 | void main() { 36 | // NOTE: the multiplication with modelMatrix is required otherwise viewDir in the fragment shader would be incorrect 37 | vWorldSpaceFragPos = (modelMatrix * vec4(position, 1.0)).xyz; 38 | vWorldSpaceNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz); 39 | 40 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 41 | vProjViewMatrix = projectionMatrix * viewMatrix; 42 | vViewMatrix = viewMatrix; 43 | }`, 44 | 45 | fragmentShader: ` 46 | uniform sampler2D uSkybox; 47 | uniform sampler2D uBackFaceBuffer; 48 | uniform sampler2D uFrontFaceBuffer; 49 | 50 | uniform vec3 uExtintionColor1; 51 | uniform vec3 uExtintionColor2; 52 | uniform float uExtintionFactor; 53 | uniform float uExposure; 54 | uniform float uReflectionFactor; 55 | uniform vec4 uExtinctionFX1; 56 | 57 | uniform float uTime; 58 | 59 | uniform vec3 uCameraPos; 60 | uniform vec2 uScreenSizeInv; 61 | uniform float uCameraFarInverse; 62 | 63 | varying vec3 vWorldSpaceFragPos; 64 | varying vec3 vWorldSpaceNormal; 65 | varying mat4 vProjViewMatrix; 66 | varying mat4 vViewMatrix; 67 | 68 | const float PI = 3.14159265359; 69 | const float e = 2.7182818284590; 70 | 71 | const float planeSize = 3.0; 72 | const vec3 planeColor = pow(vec3(202.0 / 255.0, 205.0 / 255.0, 185.0 / 255.0), vec3(3.0)); 73 | 74 | float mod289(float x){return x - floor(x * (1.0 / 289.0)) * 289.0;} 75 | vec4 mod289(vec4 x){return x - floor(x * (1.0 / 289.0)) * 289.0;} 76 | vec4 perm(vec4 x){return mod289(((x * 34.0) + 1.0) * x);} 77 | 78 | float noise(vec3 p){ 79 | vec3 a = floor(p); 80 | vec3 d = p - a; 81 | d = d * d * (3.0 - 2.0 * d); 82 | 83 | vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); 84 | vec4 k1 = perm(b.xyxy); 85 | vec4 k2 = perm(k1.xyxy + b.zzww); 86 | 87 | vec4 c = k2 + a.zzzz; 88 | vec4 k3 = perm(c); 89 | vec4 k4 = perm(c + 1.0); 90 | 91 | vec4 o1 = fract(k3 * (1.0 / 41.0)); 92 | vec4 o2 = fract(k4 * (1.0 / 41.0)); 93 | 94 | vec4 o3 = o2 * d.z + o1 * (1.0 - d.z); 95 | vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x); 96 | 97 | return o4.y * d.y + o4.x * (1.0 - d.y); 98 | } 99 | 100 | vec3 acesFilm(const vec3 x) { 101 | const float a = 2.51; 102 | const float b = 0.03; 103 | const float c = 2.43; 104 | const float d = 0.59; 105 | const float e = 0.14; 106 | return clamp((x * (a * x + b)) / (x * (c * x + d ) + e), 0.0, 1.0); 107 | } 108 | 109 | 110 | // gets the skybox color from a given view direction 111 | vec3 getSkyboxColor(vec3 viewDir) { 112 | // skybox coordinates 113 | vec2 skyboxUV = vec2( 114 | (atan(viewDir.x, viewDir.z) + PI) / (PI * 2.0), 115 | (asin(viewDir.y) + PI * 0.5) / (PI) 116 | ); 117 | 118 | vec3 col = texture2D(uSkybox, skyboxUV).xyz; 119 | col = pow(col, vec3(2.2)); 120 | return col; 121 | } 122 | 123 | bool refract2(vec3 v, vec3 n, float ni_over_nt, inout vec3 refracted) { 124 | vec3 uv = normalize(v); 125 | float dt = dot(uv, n); 126 | float discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt*dt); 127 | if (discriminant > 0.0) { 128 | refracted = ni_over_nt * (v - n * dt) - n * sqrt(discriminant); 129 | return true; 130 | } 131 | 132 | return false; 133 | } 134 | 135 | 136 | 137 | vec3 binarySearchHitPoint(vec3 lastP, vec3 hitP, vec3 rayDir) { 138 | 139 | for(int i = 0; i < 10; i++) { 140 | vec3 midP = (lastP + hitP) * 0.5; 141 | 142 | // project midP in uv space 143 | vec4 projCoord = vProjViewMatrix * vec4(midP, 1.0); 144 | projCoord.xyz /= projCoord.w; 145 | 146 | vec2 midpNDC = projCoord.xy; 147 | vec2 midpUV = midpNDC * 0.5 + 0.5; 148 | 149 | // get depth at point 150 | vec4 backBuffer = texture2D(uBackFaceBuffer, midpUV); 151 | float depth = backBuffer.w; 152 | 153 | float midpDepth = abs((vViewMatrix * vec4(midP, 1.0)).z) * uCameraFarInverse; 154 | if(midpDepth > depth) { 155 | hitP = midP; 156 | } else { 157 | lastP = midP; 158 | } 159 | } 160 | 161 | return hitP; 162 | } 163 | 164 | 165 | 166 | vec3 getRefractedColor(vec3 refractionDir, vec3 hitPoint, float refractionIndex) { 167 | // move the hitpoint inside the mesh with epsilon 168 | hitPoint += refractionDir * 0.0001; 169 | 170 | // raymarch! 171 | float stepSize = 0.02; 172 | float stepMult = 1.5; 173 | 174 | vec3 lastP = hitPoint; 175 | vec3 p = hitPoint; 176 | vec3 hitPNormal; 177 | float currStepSize = stepSize; 178 | float transmissionDistance = 0.0; 179 | for(int i = 0; i < 20; i++) { 180 | p += currStepSize * refractionDir; 181 | 182 | // project p in uv space 183 | vec4 projCoord = vProjViewMatrix * vec4(p, 1.0); 184 | projCoord.xyz /= projCoord.w; 185 | 186 | vec2 pNDC = projCoord.xy; 187 | vec2 pUV = pNDC * 0.5 + 0.5; 188 | 189 | // get depth at point 190 | vec4 backBuffer = texture2D(uBackFaceBuffer, pUV); 191 | float depth = backBuffer.w; 192 | vec3 norm = backBuffer.xyz; 193 | 194 | // get p depth 195 | float pDepth = abs((vViewMatrix * vec4(p,1.0)).z) * uCameraFarInverse; 196 | 197 | 198 | if(pDepth > depth) { 199 | 200 | vec3 hitp = binarySearchHitPoint(lastP, p, refractionDir); 201 | p = hitp; 202 | 203 | // ************ get the hitpoint normal 204 | vec4 projCoord = vProjViewMatrix * vec4(p, 1.0); 205 | projCoord.xyz /= projCoord.w; 206 | 207 | vec2 pNDC = projCoord.xy; 208 | vec2 pUV = pNDC * 0.5 + 0.5; 209 | 210 | // get depth at point 211 | hitPNormal = texture2D(uBackFaceBuffer, pUV).xyz; 212 | // ************ get the hitpoint normal - END 213 | 214 | break; 215 | } 216 | 217 | lastP = p; 218 | currStepSize *= stepMult; 219 | } 220 | 221 | transmissionDistance = length(hitPoint - p); 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | // ******************** recalc directions 233 | vec3 outward_normal; 234 | vec3 reflected = reflect(refractionDir, hitPNormal); 235 | float ni_over_nt; 236 | vec3 refracted; 237 | float reflect_prob; 238 | float cosine; 239 | 240 | if (dot(refractionDir, hitPNormal) > 0.0) { 241 | outward_normal = -hitPNormal; 242 | ni_over_nt = refractionIndex; 243 | cosine = refractionIndex * dot(refractionDir, hitPNormal); 244 | } else { 245 | outward_normal = hitPNormal; 246 | ni_over_nt = 1.0 / refractionIndex; 247 | cosine = -dot(refractionDir, hitPNormal); 248 | } 249 | 250 | 251 | if (refract2(refractionDir, outward_normal, ni_over_nt, refracted)) { 252 | float r0 = (1.0 - refractionIndex) / (1.0 + refractionIndex); 253 | r0 *= r0; 254 | reflect_prob = r0 + (1.0 - r0) * pow((1.0 - cosine), 5.0); 255 | } else { 256 | reflect_prob = 1.0; 257 | } 258 | // ******************** recalc directions - END 259 | 260 | 261 | // ******************** get colors 262 | vec3 col; 263 | vec3 colrefl; 264 | vec3 colrefr; 265 | if(refracted.y < 0.0) { 266 | float t = p.y / abs(refracted.y); 267 | vec3 planeHitP = p + refracted * t; 268 | if(abs(planeHitP.x) < planeSize && abs(planeHitP.z) < planeSize) { 269 | colrefr = planeColor; 270 | } else { 271 | colrefr = getSkyboxColor(refracted); 272 | } 273 | } else { 274 | colrefr = getSkyboxColor(refracted); 275 | } 276 | 277 | if(reflected.y < 0.0) { 278 | float t = p.y / abs(reflected.y); 279 | vec3 planeHitP = p + reflected * t; 280 | if(abs(planeHitP.x) < planeSize && abs(planeHitP.z) < planeSize) { 281 | colrefl = planeColor; 282 | } else { 283 | colrefl = getSkyboxColor(reflected); 284 | } 285 | } else { 286 | colrefl = getSkyboxColor(reflected); 287 | } 288 | 289 | col = colrefl * (reflect_prob * uReflectionFactor) + colrefr * (1.0 - reflect_prob); 290 | // ******************** get colors 291 | 292 | 293 | 294 | 295 | vec3 transm = vec3(1.0); 296 | const int steps = 15; 297 | float step = transmissionDistance / float(steps); 298 | // raymarching transmission color 299 | for(int i = 0; i < steps; i++) { 300 | vec3 np = hitPoint + refractionDir * float(i) * step; 301 | 302 | float noiseStrength = 0.8; 303 | float noiseSpeed = 1.0; 304 | float noiseTimeSpeed = 1.5; 305 | 306 | vec3 nnp = np; 307 | vec3 w = normalize(np - vec3(0.75, 1.5, 0.0)); 308 | vec3 u = vec3(0.0,0.0,1.0); 309 | // vec3 timeOffset = uTime * normalize(np - vec3(0.75, 1.5, 0.0)); 310 | vec3 timeOffset = cos(uTime) * w + sin(uTime) * u; 311 | float colorNoiseX = noise(np * noiseSpeed + timeOffset * noiseTimeSpeed); 312 | float colorNoiseY = noise(np * noiseSpeed + timeOffset * noiseTimeSpeed + vec3(15.3278, 125.19879, 0.0)); 313 | float colorNoiseZ = noise(np * noiseSpeed + timeOffset * noiseTimeSpeed + vec3(2.6008, 78.19879, 543.12993)); 314 | 315 | float targ = length(nnp * 0.8 * uExtinctionFX1.w - vec3(0.75, 1.5, 0.0)); 316 | float targAperture = 0.25; 317 | 318 | // wave raymarch 319 | if(uExtinctionFX1.z > 0.5) { 320 | nnp = np + sin(np.x * 2.5 + uTime * 1.5) * 0.3; 321 | targ = nnp.y - 0.85 * uExtinctionFX1.w; 322 | } else { 323 | nnp = np + vec3(colorNoiseX, colorNoiseY, colorNoiseZ) * 1.05; 324 | vec3 diff = nnp - vec3(3.3, 4.5, 0.0); 325 | float angle = (atan(diff.x, diff.y) + PI) / (PI * 2.0); 326 | targ = length(diff) + sin(angle * 32.0 * PI + uTime * 1.5) * 0.4; 327 | targ *= 0.475; 328 | targAperture = 0.5 + colorNoiseX * 0.75; 329 | } 330 | 331 | // what's the color at np? 332 | vec3 col1 = uExtintionColor1; 333 | vec3 col2 = uExtintionColor2; 334 | if(uExtinctionFX1.x > 0.5) { 335 | col1 = vec3(colorNoiseX, colorNoiseY, colorNoiseZ) * 0.85; 336 | } 337 | if(uExtinctionFX1.y > 0.5) { 338 | col2 = vec3(colorNoiseX, colorNoiseY, colorNoiseZ) * 0.85; 339 | } 340 | if(targ < 1.0) { 341 | transm *= exp(-step * col2 * uExtintionFactor); 342 | } else if (targ > 1.0 && targ < 1.0 + targAperture) { 343 | float t = (targ - 1.0) / targAperture; 344 | // transm *= exp(-step * col1 * uExtintionFactor * t -step * col2 * uExtintionFactor * (1.0 - t)); 345 | transm *= exp(-step * (col1 * t + col2 * (1.0 - t)) * uExtintionFactor); 346 | } else { 347 | transm *= exp(-step * col1 * uExtintionFactor); 348 | } 349 | } 350 | col = col * transm; 351 | 352 | return col; 353 | } 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | void main() { 362 | vec2 screenUV = gl_FragCoord.xy * uScreenSizeInv; 363 | 364 | vec3 viewDir = normalize(vWorldSpaceFragPos - uCameraPos); 365 | vec3 normal = vWorldSpaceNormal; 366 | float refractionIndex = 1.5; 367 | 368 | 369 | vec3 outward_normal; 370 | vec3 reflected = reflect(viewDir, normal); 371 | float ni_over_nt; 372 | vec3 refracted; 373 | float reflect_prob; 374 | float cosine; 375 | 376 | if (dot(viewDir, normal) > 0.0) { 377 | outward_normal = -normal; 378 | ni_over_nt = refractionIndex; 379 | cosine = refractionIndex * dot(viewDir, normal); 380 | } else { 381 | outward_normal = normal; 382 | ni_over_nt = 1.0 / refractionIndex; 383 | cosine = -dot(viewDir, normal); 384 | } 385 | 386 | 387 | if (refract2(viewDir, outward_normal, ni_over_nt, refracted)) { 388 | float r0 = (1.0 - refractionIndex) / (1.0 + refractionIndex); 389 | r0 *= r0; 390 | reflect_prob = r0 + (1.0 - r0) * pow((1.0 - cosine), 5.0); 391 | } else { 392 | reflect_prob = 1.0; 393 | } 394 | 395 | 396 | 397 | 398 | vec3 reflectedCol; 399 | if(reflected.y < 0.0) { 400 | float t = vWorldSpaceFragPos.y / abs(reflected.y); 401 | vec3 planeHitP = vWorldSpaceFragPos + reflected * t; 402 | if(abs(planeHitP.x) < planeSize && abs(planeHitP.z) < planeSize) { 403 | reflectedCol = planeColor; 404 | } else { 405 | reflectedCol = getSkyboxColor(reflected); 406 | } 407 | } else { 408 | reflectedCol = getSkyboxColor(reflected); 409 | } 410 | 411 | vec3 col = reflectedCol * reflect_prob * uReflectionFactor + getRefractedColor(refracted, vWorldSpaceFragPos, refractionIndex) * (1.0 - reflect_prob); 412 | // getRefractedColor(normalize(refracted + vec3(0.0, 0.0, 0.0)), vWorldSpaceFragPos) * (1.0 - reflect_prob) * 0.333 + 413 | // getRefractedColor(normalize(refracted + vec3(0.0, 0.15, 0.0)), vWorldSpaceFragPos) * (1.0 - reflect_prob) * 0.333 + 414 | // getRefractedColor(normalize(refracted + vec3(0.0, 0.35, 0.0)), vWorldSpaceFragPos) * (1.0 - reflect_prob) * 0.333; 415 | 416 | 417 | // col = getRefractedColor(refracted, vWorldSpaceFragPos) * (1.0 - reflect_prob); 418 | // vec3 col = getRefractedColor(refracted, vWorldSpaceFragPos); 419 | // col = getSkyboxColor(reflected) * reflect_prob * 1.0; 420 | 421 | // vec3 col = viewDir; 422 | // gl_FragColor = vec4(col, 1.0); 423 | // return; 424 | 425 | col *= pow(2.0, uExposure); 426 | col = acesFilm(col); 427 | col = pow(col, vec3(1.0 / 2.2)); 428 | 429 | 430 | gl_FragColor = vec4(col, 1.0); 431 | // gl_FragColor = vec4(getSkyboxColor(viewDir), 1.0) * 0.5 + vec4(viewDir * 0.5 + 0.5, 1.0); 432 | }`, 433 | }); 434 | 435 | this.mesh = mesh.clone(); 436 | this.camera = camera; 437 | this.renderer = renderer; 438 | 439 | this.mesh.traverse((child) => { 440 | if (child instanceof THREE.Mesh) { 441 | child.material = this.material; 442 | child.material.side = THREE.FrontSide; 443 | } 444 | }); 445 | 446 | this.scene = new THREE.Scene(); 447 | this.scene.add(this.mesh); 448 | 449 | 450 | new THREE.TextureLoader().load("assets/floor5lr.jpg", (texture) => { 451 | // texture.encoding = THREE.LinearEncoding; 452 | 453 | let planeMesh = new THREE.Mesh( 454 | // new THREE.PlaneBufferGeometry(60, 60), // original size 455 | // new THREE.PlaneBufferGeometry(60 * 0.35, 60 * 0.35), 456 | new THREE.PlaneBufferGeometry(20, 20), 457 | new THREE.ShaderMaterial({ 458 | uniforms: { 459 | uTexture: { type: "t", value: null }, 460 | }, 461 | 462 | vertexShader: ` 463 | varying vec2 vUv; 464 | void main() { 465 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 466 | vUv = uv; 467 | }`, 468 | 469 | fragmentShader: ` 470 | varying vec2 vUv; 471 | uniform sampler2D uTexture; 472 | 473 | float smoothstep(float t) { 474 | return t * t * (3.0 - 2.0 * t); 475 | } 476 | 477 | void main() { 478 | vec4 col = texture2D(uTexture, vUv * 1.0 + (1.0 - 1.0) * 0.5); 479 | // col.rgb *= vec3(1.3, 1.15, 1.0) * 1.2; 480 | col.rgb *= vec3(0.97, 0.95, 0.9) * 1.2; 481 | 482 | float alpha = 1.0; 483 | float d = length(vUv - vec2(0.5)); 484 | if(d > 0.35) { 485 | alpha = 1.0 - smoothstep( clamp( (d - 0.35) / 0.15, 0.0, 1.0) ); 486 | } 487 | 488 | gl_FragColor = vec4(col.rgb, alpha); 489 | }`, 490 | 491 | transparent: true, 492 | }) 493 | ); 494 | 495 | planeMesh.rotation.x = -Math.PI * 0.5; 496 | planeMesh.rotation.z = -Math.PI * 1.0; 497 | planeMesh.material.uniforms.uTexture.value = texture; 498 | this.scene.add(planeMesh); 499 | }); 500 | } 501 | 502 | render(now, backFaceBuffer, frontFaceBuffer) { 503 | 504 | this.material.uniforms.uBackFaceBuffer.value = backFaceBuffer; 505 | this.material.uniforms.uFrontFaceBuffer.value = frontFaceBuffer; 506 | this.material.uniforms.uCameraPos.value = this.camera.position.clone(); 507 | 508 | this.material.uniforms.uTime.value = now; 509 | 510 | this.renderer.setRenderTarget(null); 511 | this.renderer.autoClear = false; 512 | this.renderer.render(this.scene, this.camera); 513 | this.renderer.autoClear = true; 514 | 515 | this.renderer.setRenderTarget(null); 516 | } 517 | } 518 | 519 | export { SSRTGlass }; -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Domenicobrz/SS-refraction-through-depth-peeling-in-threejs/13399a0589c421677bcd1a51e0a944f7c7845fa8/screenshot.jpg --------------------------------------------------------------------------------