├── .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
--------------------------------------------------------------------------------