├── .github └── workflows │ └── ci-cd-production.yml ├── .gitignore ├── .htaccess ├── README.md ├── package-lock.json ├── package.json ├── patches └── three+0.164.1.patch ├── src ├── Experience │ ├── Camera.js │ ├── Experience.js │ ├── Materials │ │ └── Materials.js │ ├── Passes │ │ └── motionBlurPass │ │ │ └── src │ │ │ ├── CompositeShader.js │ │ │ ├── GeometryShader.js │ │ │ ├── MotionBlurPass.js │ │ │ ├── MotionBlurShaderChunks.js │ │ │ ├── VelocityShader.js │ │ │ └── utils.js │ ├── Patches │ │ └── compilePatch.js │ ├── Renderer.js │ ├── Shaders │ │ ├── Bloom │ │ │ ├── CompositeMaterial │ │ │ │ └── fragment.glsl │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ │ ├── Example │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ │ ├── Gpgpu │ │ │ ├── particles.glsl │ │ │ └── particlesVelocity.glsl │ │ ├── Includes │ │ │ ├── simplexNoise3d.glsl │ │ │ └── simplexNoise4d.glsl │ │ └── Particles │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ ├── State.js │ ├── Ui │ │ └── Ui.js │ ├── Utils │ │ ├── Debug.js │ │ ├── ElastickNumner.js │ │ ├── EventEmitter.js │ │ ├── FBO.js │ │ ├── Gizmo.js │ │ ├── Helpers.js │ │ ├── Input.js │ │ ├── MathHelper.js │ │ ├── PostProcess.js │ │ ├── PostProcessExternal.js │ │ ├── Resources.js │ │ ├── Sizes.js │ │ ├── Sound.js │ │ ├── Time.js │ │ ├── blue-noise-generation │ │ │ ├── README.md │ │ │ ├── images │ │ │ │ └── banner.png │ │ │ └── src │ │ │ │ ├── BlueNoiseGenerator.js │ │ │ │ ├── BlueNoiseSamples.js │ │ │ │ └── utils.js │ │ └── shader-replacement │ │ │ ├── README.md │ │ │ ├── docs │ │ │ └── image.png │ │ │ ├── exampleShaders.js │ │ │ ├── index.html │ │ │ └── src │ │ │ ├── ExtendedShaderMaterial.js │ │ │ ├── RendererState.js │ │ │ ├── ShaderReplacement.js │ │ │ ├── WrappedShaderMaterial.js │ │ │ ├── index.js │ │ │ ├── passes │ │ │ ├── DepthPass.js │ │ │ ├── NormalPass.js │ │ │ ├── VelocityPass.js │ │ │ └── VelocityShader.js │ │ │ └── utils.js │ ├── World │ │ ├── Abstracts │ │ │ └── Model.js │ │ ├── DebugHelpers.js │ │ ├── Environment.js │ │ ├── ExampleClass.js │ │ ├── Particles.js │ │ ├── ParticlesSimulation.js │ │ └── World.js │ └── sources.js ├── index.html ├── preloader.js ├── script.js └── style.css ├── static ├── .gitkeep ├── favicon.png ├── images │ └── icons │ │ ├── favicon--.png │ │ ├── favicon--.webp │ │ └── favicon.png ├── models │ └── .gitkeep └── textures │ └── displacement.jpg └── vite.config.js /.github/workflows/ci-cd-production.yml: -------------------------------------------------------------------------------- 1 | # git ftp init -u "${{ vars.PRODUCTION_FTP_USERNAME }}" -p "${{ secrets.PRODUCTION_FTP_PASSWORD }}" "${{ vars.PRODUCTION_FTP_HOST }}" 2 | # git ftp push --force -b main --user "${{ vars.PRODUCTION_FTP_USERNAME }}" --passwd "${{ secrets.PRODUCTION_FTP_PASSWORD }}" "${{ vars.PRODUCTION_FTP_HOST }}" 3 | # git ftp push -vv --auto-init --insecure --changed-only -f -b main --user "${{ vars.PRODUCTION_FTP_USERNAME }}" --passwd "${{ secrets.PRODUCTION_FTP_PASSWORD }}" "${{ vars.PRODUCTION_FTP_HOST }}" 4 | 5 | name: FTP Deploy Source Code 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | 11 | jobs: 12 | main: 13 | name: Deploy to Production 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: 📂 NPM Install and Build 21 | run: | 22 | npm install 23 | npm run build 24 | 25 | - name: 📂 Sync files 26 | uses: SamKirkland/FTP-Deploy-Action@v4.3.4 27 | with: 28 | server: ftp.misterprada.com 29 | local-dir: ./dist/ 30 | server-dir: /particles.misterprada.com/ 31 | username: ${{ vars.PRODUCTION_FTP_USERNAME }} 32 | password: ${{ secrets.PRODUCTION_FTP_PASSWORD }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,node 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | .pnpm-debug.log* 43 | 44 | # Diagnostic reports (https://nodejs.org/api/report.html) 45 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 46 | 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | 53 | # Directory for instrumented libs generated by jscoverage/JSCover 54 | lib-cov 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | *.lcov 59 | 60 | # nyc test coverage 61 | .nyc_output 62 | 63 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 64 | .grunt 65 | 66 | # Bower dependency directory (https://bower.io/) 67 | bower_components 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (https://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directories 76 | node_modules/ 77 | jspm_packages/ 78 | 79 | # Snowpack dependency directory (https://snowpack.dev/) 80 | web_modules/ 81 | 82 | # TypeScript cache 83 | *.tsbuildinfo 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Microbundle cache 92 | .rpt2_cache/ 93 | .rts2_cache_cjs/ 94 | .rts2_cache_es/ 95 | .rts2_cache_umd/ 96 | 97 | # Optional REPL history 98 | .node_repl_history 99 | 100 | # Output of 'npm pack' 101 | *.tgz 102 | 103 | # Yarn Integrity file 104 | .yarn-integrity 105 | 106 | # dotenv environment variables file 107 | .env 108 | .env.test 109 | .env.production 110 | 111 | # parcel-bundler cache (https://parceljs.org/) 112 | .cache 113 | .parcel-cache 114 | 115 | # Next.js build output 116 | .next 117 | out 118 | 119 | # Nuxt.js build / generate output 120 | .nuxt 121 | 122 | 123 | # Gatsby files 124 | .cache/ 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | # public 128 | 129 | # vuepress build output 130 | .vuepress/dist 131 | 132 | # Serverless directories 133 | .serverless/ 134 | 135 | # FuseBox cache 136 | .fusebox/ 137 | 138 | # DynamoDB Local files 139 | .dynamodb/ 140 | 141 | # TernJS port file 142 | .tern-port 143 | 144 | # Stores VSCode versions used for testing VSCode extensions 145 | .vscode-test 146 | 147 | # yarn v2 148 | .yarn/cache 149 | .yarn/unplugged 150 | .yarn/build-state.yml 151 | .yarn/install-state.gz 152 | .pnp.* 153 | 154 | # End of https://www.toptal.com/developers/gitignore/api/macos,node 155 | 156 | /dist 157 | .vite 158 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteRule ^$ dist/ [L] 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule ^(.*)$ dist/$1 [L] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | How to install and run this template? \ 2 | It's simple, just run the following commands: 3 | ``` 4 | npm i 5 | npm run dev 6 | ``` 7 | 8 | ![2024-07-07 23-43-45 (online-video-cutter com)](https://github.com/MisterPrada/particles/assets/8146111/aabdf1a8-f540-4627-849f-578d12f1c95b) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "particles", 3 | "author": "Mister&Prada", 4 | "private": true, 5 | "version": "1.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "postinstall": "patch-package" 11 | }, 12 | "devDependencies": { 13 | "@vitejs/plugin-basic-ssl": "^1.1.0", 14 | "gsap": "^3.12.5", 15 | "lil-gui": "^0.19.2", 16 | "min-signal": "^1.0.2", 17 | "normalize-wheel": "^1.0.1", 18 | "patch-package": "^8.0.0", 19 | "postprocessing": "^6.35.5", 20 | "simplex-noise": "^4.0.1", 21 | "stats.js": "^0.17.0", 22 | "three": "^0.164.1", 23 | "vite": "^4.3.1", 24 | "vite-plugin-glsl": "^1.2.1", 25 | "vite-plugin-terminal": "^1.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /patches/three+0.164.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/three/build/three.module.js b/node_modules/three/build/three.module.js 2 | index 8c431f6..9e1a9f1 100644 3 | --- a/node_modules/three/build/three.module.js 4 | +++ b/node_modules/three/build/three.module.js 5 | @@ -29201,7 +29201,7 @@ class WebGLRenderer { 6 | } ); 7 | 8 | renderStateStack.pop(); 9 | - currentRenderState = null; 10 | + //currentRenderState = null; 11 | 12 | return materials; 13 | 14 | diff --git a/node_modules/three/src/renderers/WebGLRenderer.js b/node_modules/three/src/renderers/WebGLRenderer.js 15 | index 6fe5a52..b42edaf 100644 16 | --- a/node_modules/three/src/renderers/WebGLRenderer.js 17 | +++ b/node_modules/three/src/renderers/WebGLRenderer.js 18 | @@ -988,7 +988,7 @@ class WebGLRenderer { 19 | } ); 20 | 21 | renderStateStack.pop(); 22 | - currentRenderState = null; 23 | + //currentRenderState = null; 24 | 25 | return materials; 26 | 27 | -------------------------------------------------------------------------------- /src/Experience/Camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from './Experience.js' 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 4 | import gsap from "gsap"; 5 | 6 | export default class Camera 7 | { 8 | constructor() 9 | { 10 | this.experience = new Experience() 11 | this.sizes = this.experience.sizes 12 | this.scene = this.experience.scene 13 | this.time = this.experience.time 14 | this.canvas = this.experience.canvas 15 | this.timeline = this.experience.timeline 16 | this.cursorEnabled = false 17 | 18 | this.lerpVector = new THREE.Vector3(); 19 | 20 | this.setInstance() 21 | this.setControls() 22 | } 23 | 24 | setInstance() 25 | { 26 | this.instance = new THREE.PerspectiveCamera(25, this.sizes.width / this.sizes.height, 0.1, 300) 27 | this.defaultCameraPosition = new THREE.Vector3(-0.5, 1.5, 3); 28 | 29 | this.instance.position.copy(this.defaultCameraPosition) 30 | this.instance.lookAt(new THREE.Vector3(0, 0, 0)); 31 | 32 | this.lerpVector.copy(this.instance.position); 33 | 34 | this.scene.add(this.instance) 35 | } 36 | 37 | setControls() 38 | { 39 | this.controls = new OrbitControls(this.instance, this.canvas) 40 | this.controls.enableDamping = true 41 | this.controls.minDistance = 0; 42 | this.controls.maxDistance = 500; 43 | this.controls.enabled = true; 44 | this.controls.target = new THREE.Vector3(0, 0, 0); 45 | 46 | 47 | // this.controls.mouseButtons = { 48 | // LEFT: THREE.MOUSE.ROTATE, 49 | // MIDDLE: null, 50 | // RIGHT: null, // Отключает действие для правой кнопки мыши 51 | // }; 52 | // 53 | // this.controls.enableZoom = false; 54 | } 55 | 56 | resize() 57 | { 58 | this.instance.aspect = this.sizes.width / this.sizes.height 59 | this.instance.updateProjectionMatrix() 60 | } 61 | 62 | update() 63 | { 64 | this.controls.update() 65 | 66 | //this.instance.updateMatrixWorld() // To be used in projection 67 | } 68 | 69 | animateCameraPosition() { 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Experience/Experience.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import EventEmitter from './Utils/EventEmitter.js' 3 | 4 | import Debug from './Utils/Debug.js' 5 | import Sizes from './Utils/Sizes.js' 6 | import Time from './Utils/Time.js' 7 | import Camera from './Camera.js' 8 | import Renderer from './Renderer.js' 9 | import World from './World/World.js' 10 | import Resources from './Utils/Resources.js' 11 | import Sound from "./Utils/Sound.js"; 12 | import Input from './Utils/Input.js' 13 | 14 | import sources from './sources.js' 15 | import gsap from "gsap"; 16 | import MotionPathPlugin from "gsap/MotionPathPlugin"; 17 | import State from './State.js' 18 | //import PostProcess from './Utils/PostProcess.js' 19 | import PostProcess from './Utils/PostProcessExternal.js' 20 | 21 | export default class Experience extends EventEmitter { 22 | 23 | static _instance = null 24 | 25 | appLoaded = false; 26 | firstRender = false; 27 | 28 | static getInstance() { 29 | return Experience._instance || new Experience() 30 | } 31 | 32 | constructor( _canvas ) { 33 | super() 34 | // Singleton 35 | if ( Experience._instance ) { 36 | return Experience._instance 37 | } 38 | Experience._instance = this 39 | 40 | // Global access 41 | window.experience = this 42 | 43 | // Html Elements 44 | this.html = {} 45 | this.html.preloader = document.getElementById( "preloader" ) 46 | this.html.playButton = document.getElementById( "play-button" ) 47 | 48 | // Options 49 | this.canvas = _canvas 50 | THREE.ColorManagement.enabled = false 51 | 52 | if ( !this.canvas ) { 53 | console.warn( 'Missing \'Canvas\' property' ) 54 | return 55 | } 56 | 57 | this.setDefaultCode(); 58 | 59 | // Start Loading Resources 60 | this.resources = new Resources( sources ) 61 | 62 | this.init() 63 | 64 | } 65 | 66 | init() { 67 | 68 | // Setup 69 | this.debug = new Debug() 70 | this.sizes = new Sizes() 71 | this.time = new Time() 72 | this.input = new Input() 73 | this.scene = new THREE.Scene() 74 | this.camera = new Camera() 75 | this.renderer = new Renderer() 76 | this.state = new State() 77 | this.sound = new Sound() 78 | this.world = new World() 79 | this.postProcess = new PostProcess( this.renderer.instance ) 80 | 81 | this.setListeners() 82 | this.trigger("classesReady"); 83 | } 84 | 85 | postInit() { 86 | 87 | } 88 | 89 | resize() { 90 | this.camera.resize() 91 | this.world.resize() 92 | this.renderer.resize() 93 | this.postProcess.resize() 94 | this.debug.resize() 95 | //this.sound.resize() 96 | } 97 | 98 | update() { 99 | this.camera.update( this.time.delta ) 100 | this.world.update( this.time.delta ) 101 | 102 | if ( this.state.postprocessing ) { 103 | this.postProcess.update( this.time.delta ) 104 | } else { 105 | this.renderer.update( this.time.delta ) 106 | } 107 | 108 | if ( this.debug.active ) { 109 | this.debug.update( this.time.delta ) 110 | } 111 | 112 | this.postUpdate() 113 | } 114 | 115 | postUpdate() { 116 | if ( this.firstRender === true ) { 117 | window.dispatchEvent( new CustomEvent( "app:first-render" ) ); 118 | this.firstRender = 'done'; 119 | } 120 | 121 | if ( this.resources.loadedAll && this.appLoaded && this.firstRender === false ) { 122 | this.firstRender = true; 123 | } 124 | } 125 | 126 | setListeners() { 127 | // Resize event 128 | this.sizes.on( 'resize', () => { 129 | this.resize() 130 | } ) 131 | 132 | // Time tick event 133 | this.time.on( 'tick', () => { 134 | this.update() 135 | this.debug?.stats?.update(); 136 | } ) 137 | } 138 | 139 | setDefaultCode() { 140 | document.ondblclick = function ( e ) { 141 | e.preventDefault() 142 | } 143 | 144 | gsap.registerPlugin( MotionPathPlugin ); 145 | } 146 | 147 | destroy() { 148 | this.sizes.off( 'resize' ) 149 | this.time.off( 'tick' ) 150 | 151 | // Traverse the whole scene 152 | this.scene.traverse( ( child ) => { 153 | // Test if it's a mesh 154 | if ( child instanceof THREE.Mesh ) { 155 | child.geometry.dispose() 156 | 157 | // Loop through the material properties 158 | for ( const key in child.material ) { 159 | const value = child.material[ key ] 160 | 161 | // Test if there is a dispose function 162 | if ( value && typeof value.dispose === 'function' ) { 163 | value.dispose() 164 | } 165 | } 166 | } 167 | } ) 168 | 169 | this.camera.controls.dispose() 170 | this.renderer.instance.dispose() 171 | 172 | if ( this.debug.active ) 173 | this.debug.ui.destroy() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Experience/Materials/Materials.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | import Debug from '../Utils/Debug.js' 4 | import State from "../State.js"; 5 | 6 | export default class Materials { 7 | static _instance = null 8 | 9 | static getInstance() { 10 | return Materials._instance || new Materials() 11 | } 12 | 13 | constructor() { 14 | if ( Materials._instance ) { 15 | return Materials._instance 16 | } 17 | Materials._instance = this 18 | 19 | this.experience = Experience.getInstance() 20 | this.debug = Debug.getInstance() 21 | this.state = State.getInstance() 22 | this.bloomLayer = this.state.bloomLayer 23 | 24 | this.materials = {} 25 | this.darkMaterial = new THREE.MeshBasicMaterial( { color: 'black' } ); 26 | } 27 | 28 | _darkenNonBloomed = ( obj ) => { 29 | if ( obj.isMesh && this.bloomLayer.test( obj.layers ) === false ) { 30 | this.materials[ obj.uuid ] = obj.material; 31 | obj.material = this.darkMaterial; 32 | } 33 | } 34 | 35 | _restoreMaterial = ( obj ) => { 36 | 37 | if ( this.materials[ obj.uuid ] ) { 38 | 39 | obj.material = this.materials[ obj.uuid ]; 40 | delete this.materials[ obj.uuid ]; 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/CompositeShader.js: -------------------------------------------------------------------------------- 1 | export const CompositeShader = { 2 | 3 | defines: { 4 | 5 | SAMPLES: 30, 6 | JITTER_STRATEGY: 1, 7 | BLUENOISE_SIZE: '32.0' 8 | 9 | }, 10 | 11 | uniforms: { 12 | 13 | sourceBuffer: { 14 | 15 | value: null 16 | 17 | }, 18 | 19 | velocityBuffer: { 20 | 21 | value: null 22 | 23 | }, 24 | 25 | jitter: { 26 | 27 | value: 1 28 | 29 | }, 30 | 31 | blueNoiseTex: { 32 | 33 | value: null 34 | 35 | } 36 | 37 | }, 38 | 39 | vertexShader: 40 | /* glsl */` 41 | varying vec2 vUv; 42 | void main() { 43 | vUv = uv; 44 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 45 | } 46 | `, 47 | 48 | fragmentShader: 49 | /* glsl */` 50 | varying vec2 vUv; 51 | uniform sampler2D sourceBuffer; 52 | uniform sampler2D velocityBuffer; 53 | uniform float jitter; 54 | 55 | #if JITTER_STRATEGY == 2 // blue noise 56 | uniform sampler2D blueNoiseTex; 57 | #endif 58 | 59 | #include 60 | void main() { 61 | 62 | vec2 vel = texture2D( velocityBuffer, vUv ).xy; 63 | 64 | #if JITTER_STRATEGY == 0 // Regular Jitter 65 | float jitterValue = mod( ( gl_FragCoord.x + gl_FragCoord.y ) * 0.25, 1.0 ); 66 | #elif JITTER_STRATEGY == 1 // Random Jitter 67 | float jitterValue = rand( gl_FragCoord.xy * 0.01 ); 68 | #elif JITTER_STRATEGY == 2 // Blue Noise Jitter 69 | float jitterValue = texture2D( blueNoiseTex, gl_FragCoord.xy / BLUENOISE_SIZE ).r; 70 | #endif 71 | 72 | vec2 jitterOffset = jitter * vel * vec2( jitterValue ) / float( SAMPLES ); 73 | vec4 result; 74 | 75 | vec2 startUv = clamp( vUv - vel * 0.5 + jitterOffset, 0.0, 1.0 ); 76 | vec2 endUv = clamp( vUv + vel * 0.5 + jitterOffset, 0.0, 1.0 ); 77 | for( int i = 0; i < SAMPLES; i ++ ) { 78 | 79 | vec2 sampleUv = mix( startUv, endUv, float( i ) / float( SAMPLES ) ); 80 | result += texture2D( sourceBuffer, sampleUv ); 81 | 82 | } 83 | 84 | result /= float( SAMPLES ); 85 | 86 | gl_FragColor = result; 87 | 88 | } 89 | ` 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/GeometryShader.js: -------------------------------------------------------------------------------- 1 | import { Matrix4, ShaderChunk } from 'three'; 2 | import { prev_skinning_pars_vertex, velocity_vertex } from './MotionBlurShaderChunks.js'; 3 | 4 | export const GeometryShader = { 5 | 6 | uniforms: { 7 | 8 | prevProjectionMatrix: { 9 | 10 | value: new Matrix4() 11 | 12 | }, 13 | 14 | prevModelViewMatrix: { 15 | 16 | value: new Matrix4() 17 | 18 | }, 19 | 20 | prevBoneTexture: { 21 | 22 | value: null 23 | 24 | }, 25 | 26 | expandGeometry: { 27 | 28 | value: 0 29 | 30 | }, 31 | 32 | interpolateGeometry: { 33 | 34 | value: 1 35 | 36 | }, 37 | 38 | smearIntensity: { 39 | 40 | value: 1 41 | 42 | } 43 | 44 | }, 45 | 46 | vertexShader: 47 | ` 48 | ${ ShaderChunk.skinning_pars_vertex } 49 | ${ prev_skinning_pars_vertex } 50 | 51 | uniform mat4 prevProjectionMatrix; 52 | uniform mat4 prevModelViewMatrix; 53 | uniform float expandGeometry; 54 | uniform float interpolateGeometry; 55 | varying vec4 prevPosition; 56 | varying vec4 newPosition; 57 | varying vec3 color; 58 | 59 | void main() { 60 | 61 | ${ velocity_vertex } 62 | 63 | color = (modelViewMatrix * vec4(normal.xyz, 0)).xyz; 64 | color = normalize(color); 65 | 66 | } 67 | `, 68 | 69 | fragmentShader: 70 | ` 71 | varying vec3 color; 72 | 73 | void main() { 74 | gl_FragColor = vec4(color, 1); 75 | } 76 | ` 77 | }; 78 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/MotionBlurPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Garrett Johnson / http://gkjohnson.github.io/ 3 | * 4 | * Approach from http://john-chapman-graphics.blogspot.com/2013/01/per-object-motion-blur.html 5 | */ 6 | import { 7 | Frustum, 8 | Color, 9 | WebGLRenderTarget, 10 | LinearFilter, 11 | HalfFloatType, 12 | Matrix4, 13 | DataTexture, 14 | RGBAFormat, 15 | FloatType, 16 | ShaderMaterial, 17 | RepeatWrapping, 18 | UniformsUtils, 19 | } from 'three'; 20 | import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js'; 21 | import { VelocityShader } from './VelocityShader.js'; 22 | import { GeometryShader } from './GeometryShader.js'; 23 | import { CompositeShader } from './CompositeShader.js'; 24 | import { BlueNoiseGenerator } from '@/Utils/blue-noise-generation/src/BlueNoiseGenerator.js'; 25 | import { RendererState } from '@/Utils/shader-replacement/src/RendererState.js'; 26 | import { traverseVisibleMeshes } from './utils.js'; 27 | import State from '@/State.js'; 28 | import Debug from '@/Utils/Debug.js'; 29 | 30 | const _blackColor = new Color( 0, 0, 0 ); 31 | const _defaultOverrides = {}; 32 | const _rendererState = new RendererState(); 33 | 34 | // Generate Blue Noise Textures 35 | const generator = new BlueNoiseGenerator(); 36 | generator.size = 32; 37 | 38 | const data = new Uint8Array( 32 ** 2 * 4 ); 39 | for ( let i = 0, l = 1; i < l; i ++ ) { 40 | 41 | const result = generator.generate(); 42 | const bin = result.data; 43 | const maxValue = result.maxValue; 44 | 45 | for ( let j = 0, l2 = bin.length; j < l2; j ++ ) { 46 | 47 | const value = 255 * ( bin[ j ] / maxValue ); 48 | data[ j * 3 + i ] = value; 49 | 50 | } 51 | 52 | } 53 | 54 | // TODO: Why won't RedFormat work here? 55 | const blueNoiseTex = new DataTexture( data, generator.size, generator.size, RGBAFormat ); 56 | blueNoiseTex.wrapS = RepeatWrapping; 57 | blueNoiseTex.wrapT = RepeatWrapping; 58 | blueNoiseTex.minFilter = LinearFilter; 59 | 60 | export class MotionBlurPass extends Pass { 61 | 62 | state = State.getInstance() 63 | debug = Debug.getInstance() 64 | 65 | get enabled() { 66 | 67 | return this._enabled; 68 | 69 | } 70 | 71 | set enabled( val ) { 72 | 73 | if ( val === false ) { 74 | 75 | this._prevPosMap.clear(); 76 | this._cameraMatricesNeedInitializing = true; 77 | 78 | } 79 | 80 | this._enabled = val; 81 | 82 | } 83 | 84 | constructor( scene, camera, options = {} ) { 85 | 86 | super(); 87 | 88 | this.enabled = true; 89 | this.needsSwap = true; 90 | 91 | // settings 92 | this.samples = 'samples' in options ? options.samples : 15; 93 | this.expandGeometry = 'expandGeometry' in options ? options.expandGeometry : 0; 94 | this.interpolateGeometry = 'interpolateGeometry' in options ? options.interpolateGeometry : 1; 95 | this.smearIntensity = 'smearIntensity' in options ? options.smearIntensity : 1; 96 | this.blurTransparent = 'blurTransparent' in options ? options.blurTransparent : false; 97 | this.renderCameraBlur = 'renderCameraBlur' in options ? options.renderCameraBlur : true; 98 | this.renderTargetScale = 'renderTargetScale' in options ? options.renderTargetScale : 1; 99 | this.jitter = 'jitter' in options ? options.jitter : 1; 100 | this.jitterStrategy = 'jitterStrategy' in options ? options.jitterStrategy : MotionBlurPass.RANDOM_JITTER; 101 | 102 | this.debug = { 103 | 104 | display: MotionBlurPass.DEFAULT, 105 | dontUpdateState: false 106 | 107 | }; 108 | 109 | this.scene = scene; 110 | this.camera = camera; 111 | 112 | // list of positions from previous frames 113 | this._prevPosMap = new Map(); 114 | this._currentFrameMod = 0; 115 | this._frustum = new Frustum(); 116 | this._projScreenMatrix = new Matrix4(); 117 | this._cameraMatricesNeedInitializing = true; 118 | 119 | this._prevCamProjection = new Matrix4(); 120 | this._prevCamWorldInverse = new Matrix4(); 121 | 122 | // render targets 123 | this._velocityBuffer = 124 | new WebGLRenderTarget( 256, 256, { 125 | minFilter: LinearFilter, 126 | magFilter: LinearFilter, 127 | format: RGBAFormat, 128 | type: HalfFloatType 129 | } ); 130 | this._velocityBuffer.texture.name = "MotionBlurPass.Velocity"; 131 | this._velocityBuffer.texture.generateMipmaps = false; 132 | 133 | this._compositeMaterial = new ShaderMaterial( CompositeShader ); 134 | this._compositeQuad = new FullScreenQuad( this._compositeMaterial ); 135 | 136 | } 137 | 138 | // Pass API 139 | dispose() { 140 | 141 | this._compositeQuad.dispose(); 142 | this._velocityBuffer.dispose(); 143 | this._prevPosMap.clear(); 144 | 145 | } 146 | 147 | setSize( width, height ) { 148 | 149 | const renderTargetScale = this.renderTargetScale; 150 | const velocityBuffer = this._velocityBuffer; 151 | velocityBuffer.setSize( width * renderTargetScale, height * renderTargetScale ); 152 | 153 | } 154 | 155 | render( renderer, writeBuffer, readBuffer ) { 156 | 157 | const debug = this.debug; 158 | const scene = this.scene; 159 | const camera = this.camera; 160 | const compositeQuad = this._compositeQuad; 161 | const finalBuffer = this.renderToScreen ? null : writeBuffer; 162 | 163 | _rendererState.copy( renderer, scene ); 164 | 165 | // Set the clear state 166 | renderer.autoClear = false; 167 | renderer.setClearColor( _blackColor, 0 ); 168 | 169 | // TODO: This is getting called just to set 'currentRenderState' in the renderer 170 | // NOTE -- why do we need this? 171 | renderer.compile( scene, camera ); 172 | this._ensurePrevCameraTransform(); 173 | 174 | switch ( debug.display ) { 175 | 176 | case MotionBlurPass.GEOMETRY: { 177 | 178 | renderer.setRenderTarget( finalBuffer ); 179 | renderer.clear(); 180 | this._drawAllMeshes( renderer, MotionBlurPass.GEOMETRY, ! debug.dontUpdateState ); 181 | break; 182 | 183 | } 184 | 185 | case MotionBlurPass.VELOCITY: { 186 | 187 | renderer.setRenderTarget( finalBuffer ); 188 | renderer.clear(); 189 | this._drawAllMeshes( renderer, MotionBlurPass.VELOCITY, ! debug.dontUpdateState ); 190 | break; 191 | 192 | } 193 | 194 | case MotionBlurPass.DEFAULT: { 195 | 196 | const velocityBuffer = this._velocityBuffer; 197 | renderer.setRenderTarget( velocityBuffer ); 198 | renderer.clear(); 199 | this._drawAllMeshes( renderer, MotionBlurPass.VELOCITY, ! debug.dontUpdateState ); 200 | 201 | const compositeMaterial = this._compositeMaterial; 202 | const uniforms = compositeMaterial.uniforms; 203 | uniforms.sourceBuffer.value = readBuffer.texture; 204 | uniforms.velocityBuffer.value = this._velocityBuffer.texture; 205 | uniforms.jitter.value = this.jitter; 206 | uniforms.blueNoiseTex.value = blueNoiseTex; 207 | 208 | if ( compositeMaterial.defines.SAMPLES !== this.samples ) { 209 | 210 | compositeMaterial.defines.SAMPLES = Math.max( 0, Math.floor( this.samples ) ); 211 | compositeMaterial.needsUpdate = true; 212 | 213 | } 214 | 215 | if ( compositeMaterial.defines.JITTER_STRATEGY !== this.jitterStrategy ) { 216 | 217 | compositeMaterial.defines.JITTER_STRATEGY = this.jitterStrategy; 218 | compositeMaterial.needsUpdate = true; 219 | 220 | } 221 | 222 | renderer.setRenderTarget( finalBuffer ); 223 | compositeQuad.render( renderer ); 224 | 225 | break; 226 | 227 | } 228 | 229 | } 230 | 231 | // Save the camera state for the next frame 232 | this._prevCamWorldInverse.copy( camera.matrixWorldInverse ); 233 | this._prevCamProjection.copy( camera.projectionMatrix ); 234 | 235 | // Restore renderer settings 236 | _rendererState.restore( renderer, scene ); 237 | 238 | } 239 | 240 | // Returns the set of previous frames data for object position and bone state. Creates 241 | // a new object this with frames state if it hasn't been created yet. 242 | _getPreviousFrameState( obj ) { 243 | 244 | const prevPosMap = this._prevPosMap; 245 | let data = prevPosMap.get( obj ); 246 | if ( data === undefined ) { 247 | 248 | data = { 249 | 250 | lastUsedFrame: - 1, 251 | matrixWorld: obj.matrixWorld.clone(), 252 | geometryMaterial: new ShaderMaterial( { 253 | uniforms: UniformsUtils.clone( GeometryShader.uniforms ), 254 | vertexShader: GeometryShader.vertexShader, 255 | fragmentShader: GeometryShader.fragmentShader, 256 | } ), 257 | velocityMaterial: new ShaderMaterial( { 258 | uniforms: UniformsUtils.clone( VelocityShader.uniforms ), 259 | vertexShader: VelocityShader.vertexShader, 260 | fragmentShader: VelocityShader.fragmentShader, 261 | } ), 262 | boneMatrices: null, 263 | boneTexture: null, 264 | 265 | }; 266 | 267 | 268 | prevPosMap.set( obj, data ); 269 | 270 | } 271 | 272 | const isSkinned = obj.type === 'SkinnedMesh' && obj.skeleton && obj.skeleton.bones && obj.skeleton.boneMatrices; 273 | 274 | data.geometryMaterial.skinning = isSkinned; 275 | data.velocityMaterial.skinning = isSkinned; 276 | 277 | // copy the skeleton state into the prevBoneTexture uniform 278 | const skeleton = obj.skeleton; 279 | const boneTextureNeedsUpdate = data.boneMatrices === null || data.boneMatrices.length !== skeleton.boneMatrices.length; 280 | if ( isSkinned && boneTextureNeedsUpdate ) { 281 | 282 | const boneMatrices = new Float32Array( skeleton.boneMatrices.length ); 283 | boneMatrices.set( skeleton.boneMatrices ); 284 | data.boneMatrices = boneMatrices; 285 | 286 | const size = Math.sqrt( skeleton.boneMatrices.length / 4 ); 287 | const boneTexture = new DataTexture( boneMatrices, size, size, RGBAFormat, FloatType ); 288 | boneTexture.needsUpdate = true; 289 | 290 | data.geometryMaterial.uniforms.prevBoneTexture.value = boneTexture; 291 | data.velocityMaterial.uniforms.prevBoneTexture.value = boneTexture; 292 | data.boneTexture = boneTexture; 293 | 294 | } 295 | 296 | return data; 297 | 298 | } 299 | 300 | // saves the current state to be used next frame 301 | _saveCurrentObjectState( obj ) { 302 | 303 | const prevPosMap = this._prevPosMap; 304 | const data = prevPosMap.get( obj ); 305 | 306 | if ( data.boneMatrices !== null ) { 307 | 308 | data.boneMatrices.set( obj.skeleton.boneMatrices ); 309 | data.boneTexture.needsUpdate = true; 310 | 311 | } 312 | 313 | data.matrixWorld.copy( obj.matrixWorld ); 314 | 315 | } 316 | 317 | // Draw all meshes in the scene and discard those that are no longer being used 318 | _drawAllMeshes( renderer, type, saveState ) { 319 | 320 | this._currentFrameMod = ( this._currentFrameMod + 1 ) % 2; 321 | const thisFrameId = this._currentFrameMod; 322 | const prevPosMap = this._prevPosMap; 323 | 324 | traverseVisibleMeshes( this.scene, mesh => { 325 | 326 | this._drawMesh( renderer, mesh, type, saveState ); 327 | if ( prevPosMap.has( mesh ) ) { 328 | 329 | prevPosMap.get( mesh ).lastUsedFrame = thisFrameId; 330 | 331 | } 332 | 333 | } ); 334 | 335 | prevPosMap.forEach( ( data, mesh ) => { 336 | 337 | if ( data.lastUsedFrame !== thisFrameId ) { 338 | 339 | data.geometryMaterial.dispose(); 340 | data.velocityMaterial.dispose(); 341 | if ( data.boneTexture ) { 342 | 343 | data.boneTexture.dispose(); 344 | 345 | } 346 | prevPosMap.delete( mesh ); 347 | 348 | } 349 | 350 | } ); 351 | 352 | } 353 | 354 | 355 | _drawMesh( renderer, mesh, type, saveState ) { 356 | 357 | const overrides = mesh.motionBlur || _defaultOverrides; 358 | let blurTransparent = this.blurTransparent; 359 | let renderCameraBlur = this.renderCameraBlur; 360 | let expandGeometry = this.expandGeometry; 361 | let interpolateGeometry = this.interpolateGeometry; 362 | let smearIntensity = this.smearIntensity; 363 | 364 | blurTransparent = 'blurTransparent' in overrides ? overrides.blurTransparent : this.blurTransparent; 365 | renderCameraBlur = 'renderCameraBlur' in overrides ? overrides.renderCameraBlur : this.renderCameraBlur; 366 | expandGeometry = 'expandGeometry' in overrides ? overrides.expandGeometry : this.expandGeometry; 367 | interpolateGeometry = 'interpolateGeometry' in overrides ? overrides.interpolateGeometry : this.interpolateGeometry; 368 | smearIntensity = 'smearIntensity' in overrides ? overrides.smearIntensity : this.smearIntensity; 369 | 370 | const isTransparent = mesh.material.transparent || mesh.material.alpha < 1; 371 | const isCulled = mesh.frustumCulled && ! this._frustum.intersectsObject( mesh ); 372 | let skip = blurTransparent === false && isTransparent || isCulled; 373 | 374 | if ( skip ) { 375 | 376 | if ( this._prevPosMap.has( mesh ) && saveState ) { 377 | 378 | this._saveCurrentObjectState( mesh ); 379 | 380 | } 381 | 382 | } else { 383 | 384 | const camera = this.camera; 385 | const data = this._getPreviousFrameState( mesh ); 386 | 387 | const material = type === MotionBlurPass.GEOMETRY ? data.geometryMaterial : data.velocityMaterial; 388 | const uniforms = material.uniforms; 389 | uniforms.expandGeometry.value = expandGeometry; 390 | uniforms.interpolateGeometry.value = interpolateGeometry; 391 | uniforms.smearIntensity.value = smearIntensity; 392 | 393 | const projMat = renderCameraBlur ? this._prevCamProjection : camera.projectionMatrix; 394 | const invMat = renderCameraBlur ? this._prevCamWorldInverse : camera.matrixWorldInverse; 395 | uniforms.prevProjectionMatrix.value.copy( projMat ); 396 | uniforms.prevModelViewMatrix.value.multiplyMatrices( invMat, data.matrixWorld ); 397 | 398 | // fix order static meshes 399 | mesh.matrixWorld.elements[13] += 0.000001; 400 | 401 | renderer.renderBufferDirect( camera, null, mesh.geometry, material, mesh, null ); 402 | 403 | if ( saveState ) { 404 | 405 | this._saveCurrentObjectState( mesh ); 406 | 407 | } 408 | 409 | } 410 | 411 | } 412 | 413 | _ensurePrevCameraTransform() { 414 | 415 | const camera = this.camera; 416 | const projScreenMatrix = this._projScreenMatrix; 417 | 418 | // reinitialize the camera matrices to the current transform because if 419 | // the pass has been disabled then the matrices will be out of date 420 | if ( this._cameraMatricesNeedInitializing ) { 421 | 422 | this._prevCamWorldInverse.copy( camera.matrixWorldInverse ); 423 | this._prevCamProjection.copy( camera.projectionMatrix ); 424 | this._cameraMatricesNeedInitializing = false; 425 | 426 | } 427 | 428 | 429 | projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); 430 | this._frustum.setFromProjectionMatrix( projScreenMatrix ); 431 | 432 | } 433 | } 434 | 435 | MotionBlurPass.DEFAULT = 0; 436 | MotionBlurPass.VELOCITY = 1; 437 | MotionBlurPass.GEOMETRY = 2; 438 | 439 | MotionBlurPass.REGULAR_JITTER = 0; 440 | MotionBlurPass.RANDOM_JITTER = 1; 441 | MotionBlurPass.BLUENOISE_JITTER = 2; 442 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/MotionBlurShaderChunks.js: -------------------------------------------------------------------------------- 1 | import { ShaderChunk } from 'three'; 2 | 3 | // Modified ShaderChunk.skinning_pars_vertex to handle 4 | // a second set of bone information from the previou frame 5 | export const prev_skinning_pars_vertex = 6 | ` 7 | #ifdef USE_SKINNING 8 | #ifdef BONE_TEXTURE 9 | uniform sampler2D prevBoneTexture; 10 | mat4 getPrevBoneMatrix( const in float i ) { 11 | float j = i * 4.0; 12 | float x = mod( j, float( boneTextureSize ) ); 13 | float y = floor( j / float( boneTextureSize ) ); 14 | float dx = 1.0 / float( boneTextureSize ); 15 | float dy = 1.0 / float( boneTextureSize ); 16 | y = dy * ( y + 0.5 ); 17 | vec4 v1 = texture2D( prevBoneTexture, vec2( dx * ( x + 0.5 ), y ) ); 18 | vec4 v2 = texture2D( prevBoneTexture, vec2( dx * ( x + 1.5 ), y ) ); 19 | vec4 v3 = texture2D( prevBoneTexture, vec2( dx * ( x + 2.5 ), y ) ); 20 | vec4 v4 = texture2D( prevBoneTexture, vec2( dx * ( x + 3.5 ), y ) ); 21 | mat4 bone = mat4( v1, v2, v3, v4 ); 22 | return bone; 23 | } 24 | #else 25 | uniform mat4 prevBoneMatrices[ MAX_BONES ]; 26 | mat4 getPrevBoneMatrix( const in float i ) { 27 | mat4 bone = prevBoneMatrices[ int(i) ]; 28 | return bone; 29 | } 30 | #endif 31 | #endif 32 | `; 33 | 34 | // Returns the body of the vertex shader for the velocity buffer and 35 | // outputs the position of the current and last frame positions 36 | export const velocity_vertex = 37 | ` 38 | vec3 transformed; 39 | 40 | // Get the normal 41 | ${ ShaderChunk.skinbase_vertex } 42 | ${ ShaderChunk.beginnormal_vertex } 43 | ${ ShaderChunk.skinnormal_vertex } 44 | ${ ShaderChunk.defaultnormal_vertex } 45 | 46 | // Get the current vertex position 47 | transformed = vec3( position ); 48 | ${ ShaderChunk.skinning_vertex } 49 | newPosition = modelViewMatrix * vec4(transformed, 1.0); 50 | 51 | // Get the previous vertex position 52 | transformed = vec3( position ); 53 | ${ ShaderChunk.skinbase_vertex.replace( /mat4 /g, '' ).replace( /getBoneMatrix/g, 'getPrevBoneMatrix' ) } 54 | ${ ShaderChunk.skinning_vertex.replace( /vec4 /g, '' ) } 55 | prevPosition = prevModelViewMatrix * vec4(transformed, 1.0); 56 | 57 | // The delta between frames 58 | vec3 delta = newPosition.xyz - prevPosition.xyz; 59 | vec3 direction = normalize(delta); 60 | 61 | // Stretch along the velocity axes 62 | // TODO: Can we combine the stretch and expand 63 | float stretchDot = dot(direction, transformedNormal); 64 | vec4 expandDir = vec4(direction, 0.0) * stretchDot * expandGeometry * length(delta); 65 | vec4 newPosition2 = projectionMatrix * (newPosition + expandDir); 66 | vec4 prevPosition2 = prevProjectionMatrix * (prevPosition + expandDir); 67 | 68 | newPosition = projectionMatrix * newPosition; 69 | prevPosition = prevProjectionMatrix * prevPosition; 70 | 71 | gl_Position = mix(newPosition2, prevPosition2, interpolateGeometry * (1.0 - step(0.0, stretchDot) ) ); 72 | 73 | `; 74 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/VelocityShader.js: -------------------------------------------------------------------------------- 1 | import { Matrix4, ShaderChunk } from 'three'; 2 | import { prev_skinning_pars_vertex, velocity_vertex } from './MotionBlurShaderChunks.js'; 3 | 4 | export const VelocityShader = { 5 | 6 | uniforms: { 7 | 8 | prevProjectionMatrix: { 9 | 10 | value: new Matrix4() 11 | 12 | }, 13 | 14 | prevModelViewMatrix: { 15 | 16 | value: new Matrix4() 17 | 18 | }, 19 | 20 | prevBoneTexture: { 21 | 22 | value: null 23 | 24 | }, 25 | 26 | expandGeometry: { 27 | 28 | value: 0 29 | 30 | }, 31 | 32 | interpolateGeometry: { 33 | 34 | value: 1 35 | 36 | }, 37 | 38 | smearIntensity: { 39 | 40 | value: 1 41 | 42 | } 43 | 44 | }, 45 | 46 | vertexShader: 47 | ` 48 | ${ ShaderChunk.skinning_pars_vertex } 49 | ${ prev_skinning_pars_vertex } 50 | 51 | uniform mat4 prevProjectionMatrix; 52 | uniform mat4 prevModelViewMatrix; 53 | uniform float expandGeometry; 54 | uniform float interpolateGeometry; 55 | varying vec4 prevPosition; 56 | varying vec4 newPosition; 57 | 58 | void main() { 59 | 60 | ${ velocity_vertex } 61 | 62 | } 63 | `, 64 | 65 | fragmentShader: 66 | ` 67 | uniform float smearIntensity; 68 | varying vec4 prevPosition; 69 | varying vec4 newPosition; 70 | 71 | void main() { 72 | 73 | // NOTE: It seems the velociyt is incorrectly calculated here -- see the velocity pass 74 | // in shader replacement to see how to compute velocities in screen uv space. 75 | vec3 vel; 76 | vel = (newPosition.xyz / newPosition.w) - (prevPosition.xyz / prevPosition.w); 77 | 78 | gl_FragColor = vec4(vel * smearIntensity, 1.0); 79 | } 80 | ` 81 | }; 82 | -------------------------------------------------------------------------------- /src/Experience/Passes/motionBlurPass/src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export function traverseVisibleMeshes( obj, callback ) { 3 | 4 | if ( obj.visible ) { 5 | 6 | if ( obj.isMesh || obj.isSkinnedMesh ) { 7 | 8 | callback( obj ); 9 | 10 | } 11 | 12 | const children = obj.children; 13 | for ( let i = 0, l = children.length; i < l; i ++ ) { 14 | 15 | traverseVisibleMeshes( children[ i ], callback ); 16 | 17 | } 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Experience/Patches/compilePatch.js: -------------------------------------------------------------------------------- 1 | export default function ( scene, camera, targetScene = null ) { 2 | 3 | if ( targetScene === null ) targetScene = scene; 4 | 5 | currentRenderState = renderStates.get( targetScene ); 6 | currentRenderState.init(); 7 | 8 | renderStateStack.push( currentRenderState ); 9 | 10 | // gather lights from both the target scene and the new object that will be added to the scene. 11 | 12 | targetScene.traverseVisible( function ( object ) { 13 | 14 | if ( object.isLight && object.layers.test( camera.layers ) ) { 15 | 16 | currentRenderState.pushLight( object ); 17 | 18 | if ( object.castShadow ) { 19 | 20 | currentRenderState.pushShadow( object ); 21 | 22 | } 23 | 24 | } 25 | 26 | } ); 27 | 28 | if ( scene !== targetScene ) { 29 | 30 | scene.traverseVisible( function ( object ) { 31 | 32 | if ( object.isLight && object.layers.test( camera.layers ) ) { 33 | 34 | currentRenderState.pushLight( object ); 35 | 36 | if ( object.castShadow ) { 37 | 38 | currentRenderState.pushShadow( object ); 39 | 40 | } 41 | 42 | } 43 | 44 | } ); 45 | 46 | } 47 | 48 | currentRenderState.setupLights( _this._useLegacyLights ); 49 | 50 | // Only initialize materials in the new scene, not the targetScene. 51 | 52 | const materials = new Set(); 53 | 54 | scene.traverse( function ( object ) { 55 | 56 | const material = object.material; 57 | 58 | if ( material ) { 59 | 60 | if ( Array.isArray( material ) ) { 61 | 62 | for ( let i = 0; i < material.length; i ++ ) { 63 | 64 | const material2 = material[ i ]; 65 | 66 | prepareMaterial( material2, targetScene, object ); 67 | materials.add( material2 ); 68 | 69 | } 70 | 71 | } else { 72 | 73 | prepareMaterial( material, targetScene, object ); 74 | materials.add( material ); 75 | 76 | } 77 | 78 | } 79 | 80 | } ); 81 | 82 | renderStateStack.pop(); 83 | //currentRenderState = null; 84 | 85 | return materials; 86 | 87 | }; 88 | -------------------------------------------------------------------------------- /src/Experience/Renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from './Experience.js' 3 | import compilePatch from "./Patches/compilePatch.js"; 4 | 5 | export default class Renderer { 6 | constructor() { 7 | this.experience = new Experience() 8 | this.canvas = this.experience.canvas 9 | this.sizes = this.experience.sizes 10 | this.scene = this.experience.scene 11 | this.camera = this.experience.camera 12 | this.debug = this.experience.debug 13 | this.resources = this.experience.resources 14 | this.html = this.experience.html 15 | 16 | this.setInstance() 17 | this.setDebug() 18 | } 19 | 20 | setInstance() { 21 | this.clearColor = '#010101' 22 | 23 | //console.log(THREE.WebGLRenderer.compile) 24 | 25 | //THREE.WebGLRenderer.prototype.compile = compilePatch.bind( THREE.WebGLRenderer.prototype.compile ) 26 | 27 | this.instance = new THREE.WebGLRenderer( { 28 | canvas: this.canvas, 29 | powerPreference: "high-performance", 30 | antialias: false, 31 | alpha: false, 32 | stencil: false, 33 | depth: true, 34 | useLegacyLights: false, 35 | physicallyCorrectLights: true, 36 | } ) 37 | 38 | 39 | //console.log( this.instance ) 40 | //this.instance.compile = compilePatch.bind( this.instance.compile ) 41 | 42 | this.instance.outputColorSpace = THREE.SRGBColorSpace 43 | this.instance.toneMapping = THREE.NeutralToneMapping 44 | 45 | this.instance.setSize( this.sizes.width, this.sizes.height ) 46 | this.instance.setPixelRatio( Math.min( this.sizes.pixelRatio, 2 ) ) 47 | 48 | this.instance.setClearColor( this.clearColor, 1 ) 49 | this.instance.setSize( this.sizes.width, this.sizes.height ) 50 | } 51 | 52 | setDebug() { 53 | if ( this.debug.active ) { 54 | if ( this.debug.panel ) { 55 | this.debugFolder = this.debug.panel.addFolder("Renderer"); 56 | 57 | this.debugFolder.add( this.instance, "toneMapping", { 58 | "No": THREE.NoToneMapping, 59 | "Linear": THREE.LinearToneMapping, 60 | "Reinhard": THREE.ReinhardToneMapping, 61 | "Cineon": THREE.CineonToneMapping, 62 | "ACESFilmic": THREE.ACESFilmicToneMapping, 63 | "AgXToneMapping": THREE.AgXToneMapping, 64 | "NeutralToneMapping": THREE.NeutralToneMapping 65 | } ).name( "Tone Mapping" ); 66 | 67 | this.debugFolder.add( this.instance, "toneMappingExposure" ) 68 | .min( 0 ).max( 2 ).step( 0.01 ).name( "Tone Mapping Exposure" ); 69 | 70 | } 71 | 72 | } 73 | } 74 | 75 | update() { 76 | if ( this.debug.active ) { 77 | this.debugRender() 78 | } else { 79 | this.productionRender() 80 | } 81 | } 82 | 83 | productionRender() { 84 | this.instance.render( this.scene, this.camera.instance ) 85 | } 86 | 87 | debugRender() { 88 | this.instance.autoClear = false 89 | this.instance.clearColor( this.clearColor ) 90 | this.instance.render( this.scene, this.camera.instance ) 91 | this.instance.clearDepth() 92 | } 93 | 94 | resize() { 95 | // Instance 96 | this.instance.setSize( this.sizes.width, this.sizes.height ) 97 | this.instance.setPixelRatio( this.sizes.pixelRatio ) 98 | } 99 | 100 | destroy() { 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Bloom/CompositeMaterial/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | uniform sampler2D blurTexture1; 3 | uniform sampler2D blurTexture2; 4 | uniform sampler2D blurTexture3; 5 | uniform sampler2D blurTexture4; 6 | uniform sampler2D blurTexture5; 7 | uniform sampler2D dirtTexture; 8 | uniform float bloomStrength; 9 | uniform float bloomRadius; 10 | uniform float bloomFactors[NUM_MIPS]; 11 | uniform vec3 bloomTintColors[NUM_MIPS]; 12 | uniform vec3 uTintColor; 13 | uniform float uTintStrength; 14 | 15 | float lerpBloomFactor(const in float factor) { 16 | float mirrorFactor = 1.2 - factor; 17 | return mix(factor, mirrorFactor, bloomRadius); 18 | } 19 | 20 | void main() { 21 | vec4 color = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + 22 | lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + 23 | lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + 24 | lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + 25 | lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); 26 | 27 | color.rgb = mix(color.rgb, uTintColor, uTintStrength); 28 | gl_FragColor = color; 29 | } 30 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Bloom/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D baseTexture; 2 | uniform sampler2D bloomTexture; 3 | 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | 8 | gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) ); 9 | 10 | // #include 11 | // #include 12 | } 13 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Bloom/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vUv = uv; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 6 | } 7 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Example/fragment.glsl: -------------------------------------------------------------------------------- 1 | void main() { 2 | gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 3 | } 4 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Example/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vUv = uv; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 6 | } 7 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Gpgpu/particles.glsl: -------------------------------------------------------------------------------- 1 | uniform float uTime; 2 | uniform float uDeltaTime; 3 | uniform sampler2D uBase; 4 | uniform float uFlowFieldInfluence; 5 | uniform float uFlowFieldStrength; 6 | uniform float uFlowFieldFrequency; 7 | 8 | #include ../Includes/simplexNoise4d.glsl 9 | 10 | void main() 11 | { 12 | float time = uTime * 0.2; 13 | vec2 uv = gl_FragCoord.xy / resolution.xy; 14 | vec4 particle = texture(uParticles, uv); 15 | vec4 base = texture(uBase, uv); 16 | 17 | // Dead 18 | if(particle.a >= 1.0) 19 | { 20 | particle.a = mod(particle.a, 1.0); 21 | particle.xyz = base.xyz; 22 | } 23 | 24 | // Alive 25 | else 26 | { 27 | // Strength 28 | float strength = simplexNoise4d(vec4(base.xyz * 0.2, time + 1.0)); 29 | float influence = (uFlowFieldInfluence - 0.5) * (- 2.0); 30 | strength = smoothstep(influence, 1.0, strength); 31 | 32 | // Flow field 33 | vec3 flowField = vec3( 34 | simplexNoise4d(vec4(particle.xyz * uFlowFieldFrequency + 0.0, time)), 35 | simplexNoise4d(vec4(particle.xyz * uFlowFieldFrequency + 1.0, time)), 36 | simplexNoise4d(vec4(particle.xyz * uFlowFieldFrequency + 2.0, time)) 37 | ); 38 | flowField = normalize(flowField); 39 | particle.xyz += flowField * uDeltaTime * uFlowFieldStrength /* * strength */; 40 | 41 | // Decay 42 | particle.a += uDeltaTime * 0.5; 43 | } 44 | 45 | gl_FragColor = particle; 46 | } 47 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Gpgpu/particlesVelocity.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uPrevPositions; 2 | uniform sampler2D uCurrPositions; 3 | uniform float uDeltaTime; 4 | 5 | void main() 6 | { 7 | vec2 uv = gl_FragCoord.xy / resolution.xy; 8 | vec4 particle = texture(uParticlesVelocity, uv); 9 | 10 | vec4 prevPositions = texture(uPrevPositions, uv); 11 | vec4 currPositions = texture(uCurrPositions, uv); 12 | 13 | vec4 velocity = ( prevPositions - currPositions ) / uDeltaTime; 14 | 15 | gl_FragColor = velocity; 16 | gl_FragColor.a = 1.0; 17 | } 18 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Includes/simplexNoise3d.glsl: -------------------------------------------------------------------------------- 1 | // Simplex 3D Noise 2 | // by Ian McEwan, Ashima Arts 3 | // 4 | vec4 permute_3d(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); } 5 | vec4 taylorInvSqrt3d(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; } 6 | 7 | float simplexNoise3d(vec3 v) 8 | { 9 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; 10 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); 11 | 12 | // First corner 13 | vec3 i = floor(v + dot(v, C.yyy) ); 14 | vec3 x0 = v - i + dot(i, C.xxx) ; 15 | 16 | // Other corners 17 | vec3 g = step(x0.yzx, x0.xyz); 18 | vec3 l = 1.0 - g; 19 | vec3 i1 = min( g.xyz, l.zxy ); 20 | vec3 i2 = max( g.xyz, l.zxy ); 21 | 22 | // x0 = x0 - 0. + 0.0 * C 23 | vec3 x1 = x0 - i1 + 1.0 * C.xxx; 24 | vec3 x2 = x0 - i2 + 2.0 * C.xxx; 25 | vec3 x3 = x0 - 1. + 3.0 * C.xxx; 26 | 27 | // Permutations 28 | i = mod(i, 289.0 ); 29 | vec4 p = permute_3d( permute_3d( permute_3d( i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); 30 | 31 | // Gradients 32 | // ( N*N points uniformly over a square, mapped onto an octahedron.) 33 | float n_ = 1.0/7.0; // N=7 34 | vec3 ns = n_ * D.wyz - D.xzx; 35 | 36 | vec4 j = p - 49.0 * floor(p * ns.z *ns.z); // mod(p,N*N) 37 | 38 | vec4 x_ = floor(j * ns.z); 39 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) 40 | 41 | vec4 x = x_ *ns.x + ns.yyyy; 42 | vec4 y = y_ *ns.x + ns.yyyy; 43 | vec4 h = 1.0 - abs(x) - abs(y); 44 | 45 | vec4 b0 = vec4( x.xy, y.xy ); 46 | vec4 b1 = vec4( x.zw, y.zw ); 47 | 48 | vec4 s0 = floor(b0)*2.0 + 1.0; 49 | vec4 s1 = floor(b1)*2.0 + 1.0; 50 | vec4 sh = -step(h, vec4(0.0)); 51 | 52 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; 53 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; 54 | 55 | vec3 p0 = vec3(a0.xy,h.x); 56 | vec3 p1 = vec3(a0.zw,h.y); 57 | vec3 p2 = vec3(a1.xy,h.z); 58 | vec3 p3 = vec3(a1.zw,h.w); 59 | 60 | // Normalise gradients 61 | vec4 norm = taylorInvSqrt3d(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); 62 | p0 *= norm.x; 63 | p1 *= norm.y; 64 | p2 *= norm.z; 65 | p3 *= norm.w; 66 | 67 | // Mix final noise value 68 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); 69 | m = m * m; 70 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) ); 71 | } 72 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Includes/simplexNoise4d.glsl: -------------------------------------------------------------------------------- 1 | // Simplex 4D Noise 2 | // by Ian McEwan, Ashima Arts 3 | // 4 | vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} 5 | float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));} 6 | vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} 7 | float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;} 8 | 9 | vec4 grad4(float j, vec4 ip){ 10 | const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0); 11 | vec4 p,s; 12 | 13 | p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0; 14 | p.w = 1.5 - dot(abs(p.xyz), ones.xyz); 15 | s = vec4(lessThan(p, vec4(0.0))); 16 | p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www; 17 | 18 | return p; 19 | } 20 | 21 | float simplexNoise4d(vec4 v){ 22 | const vec2 C = vec2( 0.138196601125010504, // (5 - sqrt(5))/20 G4 23 | 0.309016994374947451); // (sqrt(5) - 1)/4 F4 24 | // First corner 25 | vec4 i = floor(v + dot(v, C.yyyy) ); 26 | vec4 x0 = v - i + dot(i, C.xxxx); 27 | 28 | // Other corners 29 | 30 | // Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI) 31 | vec4 i0; 32 | 33 | vec3 isX = step( x0.yzw, x0.xxx ); 34 | vec3 isYZ = step( x0.zww, x0.yyz ); 35 | // i0.x = dot( isX, vec3( 1.0 ) ); 36 | i0.x = isX.x + isX.y + isX.z; 37 | i0.yzw = 1.0 - isX; 38 | 39 | // i0.y += dot( isYZ.xy, vec2( 1.0 ) ); 40 | i0.y += isYZ.x + isYZ.y; 41 | i0.zw += 1.0 - isYZ.xy; 42 | 43 | i0.z += isYZ.z; 44 | i0.w += 1.0 - isYZ.z; 45 | 46 | // i0 now contains the unique values 0,1,2,3 in each channel 47 | vec4 i3 = clamp( i0, 0.0, 1.0 ); 48 | vec4 i2 = clamp( i0-1.0, 0.0, 1.0 ); 49 | vec4 i1 = clamp( i0-2.0, 0.0, 1.0 ); 50 | 51 | // x0 = x0 - 0.0 + 0.0 * C 52 | vec4 x1 = x0 - i1 + 1.0 * C.xxxx; 53 | vec4 x2 = x0 - i2 + 2.0 * C.xxxx; 54 | vec4 x3 = x0 - i3 + 3.0 * C.xxxx; 55 | vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx; 56 | 57 | // Permutations 58 | i = mod(i, 289.0); 59 | float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x); 60 | vec4 j1 = permute( permute( permute( permute ( 61 | i.w + vec4(i1.w, i2.w, i3.w, 1.0 )) 62 | + i.z + vec4(i1.z, i2.z, i3.z, 1.0 )) 63 | + i.y + vec4(i1.y, i2.y, i3.y, 1.0 )) 64 | + i.x + vec4(i1.x, i2.x, i3.x, 1.0 )); 65 | // Gradients 66 | // ( 7*7*6 points uniformly over a cube, mapped onto a 4-octahedron.) 67 | // 7*7*6 = 294, which is close to the ring size 17*17 = 289. 68 | 69 | vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ; 70 | 71 | vec4 p0 = grad4(j0, ip); 72 | vec4 p1 = grad4(j1.x, ip); 73 | vec4 p2 = grad4(j1.y, ip); 74 | vec4 p3 = grad4(j1.z, ip); 75 | vec4 p4 = grad4(j1.w, ip); 76 | 77 | // Normalise gradients 78 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); 79 | p0 *= norm.x; 80 | p1 *= norm.y; 81 | p2 *= norm.z; 82 | p3 *= norm.w; 83 | p4 *= taylorInvSqrt(dot(p4,p4)); 84 | 85 | // Mix contributions from the five corners 86 | vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0); 87 | vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0); 88 | m0 = m0 * m0; 89 | m1 = m1 * m1; 90 | return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 ))) 91 | + dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ; 92 | 93 | } -------------------------------------------------------------------------------- /src/Experience/Shaders/Particles/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vColor; 2 | varying vec2 vUv; 3 | varying vec3 vBary; 4 | varying float vSize; 5 | varying vec2 vParticlesUv; 6 | varying mat4 vViewMatrix; 7 | 8 | uniform vec2 uResolution; 9 | uniform sampler2D uParticlesVelocityTexture; 10 | uniform float uTime; 11 | uniform float uSampleRadius; 12 | uniform float uVelocityScale; 13 | 14 | 15 | float directionalBlur(vec2 uv, vec2 direction, float radius) { 16 | float sum = 0.0; 17 | float total = 0.0; 18 | for (float i = -radius; i <= radius; i += 1.0) { 19 | vec2 offset = uv + direction * i / radius; 20 | float dist = length(offset - vec2(0.5, 0.5)); 21 | float circle = smoothstep(0.4, 0.5, dist); 22 | sum += circle; 23 | total += 1.0; 24 | } 25 | return sum / total; 26 | } 27 | 28 | vec2 scaleWithCenter(vec2 uv, vec2 scale, vec2 center) { 29 | return center + (uv - center) * scale; 30 | } 31 | 32 | void main() { 33 | vec4 particleVelocity = texture2D(uParticlesVelocityTexture, vParticlesUv); 34 | 35 | vec2 uvA = vec2(0.0, 0.0); 36 | vec2 uvB = vec2(1.0, 0.0); 37 | vec2 uvC = vec2(0.5, 1.0); 38 | 39 | vec2 uv = (vBary.x * uvA + vBary.y * uvB + vBary.z * uvC) * 1.7; 40 | uv.x = (uv.x * 1.2) - 0.501; 41 | uv.y -= 0.1; 42 | 43 | vec3 velocityInCameraSpace = (vViewMatrix * vec4(particleVelocity.xyz, 0.0)).xyz; 44 | 45 | vec2 direction = velocityInCameraSpace.xy * uVelocityScale; 46 | float radius = uSampleRadius; // Sample Radius 47 | 48 | // Calculate blurred circle 49 | float blurredCircle = directionalBlur(scaleWithCenter(uv, vec2(3.0), vec2(0.5, 0.5)), direction, radius); 50 | 51 | // Final color 52 | gl_FragColor = vec4(vec3(vColor), 1.0 - blurredCircle); 53 | 54 | if( gl_FragColor.a < 0.01 ) discard; 55 | 56 | 57 | #include 58 | #include 59 | } 60 | -------------------------------------------------------------------------------- /src/Experience/Shaders/Particles/vertex.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 uResolution; 2 | uniform float uSize; 3 | uniform float uTime; 4 | uniform vec2 uCursor; 5 | uniform vec3 uCursorDirection; 6 | uniform sampler2D uDisplacementTexture; 7 | uniform vec2 uResolutionDisplacement; 8 | uniform sampler2D uParticlesTexture; 9 | uniform sampler2D uSolarSystemTexture; 10 | uniform float uScroll; 11 | uniform float uTotalSections; 12 | uniform float uNormalizePoints[20]; 13 | 14 | varying vec2 vUv; 15 | varying vec3 vColor; 16 | varying vec3 vBary; 17 | varying float vSize; 18 | varying vec2 vParticlesUv; 19 | varying mat4 vViewMatrix; 20 | 21 | 22 | attribute vec3 color; 23 | attribute float a_size; 24 | attribute vec2 aParticlesUv; 25 | attribute vec2 aSolarSystemUv; 26 | attribute vec3 a_offset; 27 | attribute vec3 a_bary; 28 | attribute vec2 a_uv; 29 | 30 | #include ../Includes/simplexNoise4d.glsl 31 | #include ../Includes/simplexNoise3d.glsl 32 | 33 | 34 | float inOutProgress(vec3 position, vec3 target, float scrollProgress) { 35 | // Mixed position 36 | float noiseOrigin = simplexNoise3d(position * 0.2); 37 | float noiseTarget = simplexNoise3d(target * 0.2); 38 | float noise = mix(noiseOrigin, noiseTarget, scrollProgress); 39 | noise = smoothstep(-1.0, 1.0, noise); 40 | 41 | float duration = 0.3; 42 | float delay = (1.0 - duration) * noise; 43 | float end = delay + duration; 44 | float progress = smoothstep(delay, end, scrollProgress); 45 | 46 | return progress; 47 | } 48 | 49 | float remap(float value, float inputMin, float inputMax, float outputMin, float outputMax) { 50 | return outputMin + ((value - inputMin) / (inputMax - inputMin) * (outputMax - outputMin)); 51 | } 52 | 53 | vec2 rotate2d(vec2 _st, float _angle){ 54 | _st -= 0.5; 55 | _st = mat2(cos(_angle),-sin(_angle), 56 | sin(_angle),cos(_angle)) * _st; 57 | _st += 0.5; 58 | return _st; 59 | } 60 | 61 | void main() { 62 | 63 | vUv = a_uv; 64 | 65 | vParticlesUv = aParticlesUv; 66 | 67 | vec4 particleSim = texture(uParticlesTexture, aParticlesUv); 68 | 69 | float time = uTime * 0.2; 70 | 71 | // Transform start position 72 | //transformed.xyz = particleSim.xyz; 73 | 74 | // Rotate on Scroll 75 | //transformed.xz = rotate2d(transformed.xz, PI2 * uScroll); 76 | 77 | 78 | // Circle rotating 79 | // transformed.x += sin(uTime); 80 | // transformed.y += cos(uTime); 81 | 82 | 83 | 84 | // Final position 85 | //gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 86 | //vec4 modelPosition = modelMatrix * vec4(position, 1.0); 87 | vec4 modelPosition = modelMatrix * vec4(particleSim.xyz, 1.0); 88 | vec4 viewPosition = viewMatrix * modelPosition; 89 | 90 | 91 | // Point size 92 | float sizeIn = smoothstep(0.0, 0.1, particleSim.a); 93 | float sizeOut = 1.0 - smoothstep(0.7, 1.0, particleSim.a); 94 | float size = min(sizeIn, sizeOut); 95 | 96 | viewPosition.xyz += position / 25.0 * uSize * size * a_size * 10.0; 97 | vec4 projectedPosition = projectionMatrix * viewPosition; 98 | 99 | 100 | // // *** Cursor Displacement *** 101 | // // transformed to screen space x, y in range [0, 1] and z in range [0, 1] 102 | // vec2 displacementUV = (projectedPosition.xy / projectedPosition.w) * 0.5 + 0.5; 103 | // float displacementIntensity = texture(uDisplacementTexture, displacementUV).r; 104 | // displacementIntensity = clamp(displacementIntensity, 0.2, 1.0); 105 | // if (displacementIntensity <= 0.2) { 106 | // displacementIntensity = 0.0; 107 | // } 108 | // projectedPosition.xyz += displacementIntensity * (normal / 2.0) * cursorPointerActivation; 109 | // // *** Cursor Displacement END *** 110 | 111 | gl_Position = projectedPosition; 112 | 113 | // // Point size 114 | // float sizeIn = smoothstep(0.0, 0.1, particleSim.a); 115 | // float sizeOut = 1.0 - smoothstep(0.7, 1.0, particleSim.a); 116 | // float size = min(sizeIn, sizeOut); 117 | // 118 | // gl_PointSize = a_size * uSize * uResolution.y; 119 | // gl_PointSize *= (1.0 / - viewPosition.z); 120 | // 121 | // 122 | // #include 123 | // 124 | vColor = color; 125 | vBary = a_bary; 126 | vSize = size; 127 | vViewMatrix = viewMatrix; 128 | } 129 | -------------------------------------------------------------------------------- /src/Experience/State.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from './Experience.js' 3 | import Sizes from "./Utils/Sizes.js" 4 | 5 | export default class State { 6 | static _instance = null 7 | 8 | static getInstance() { 9 | return State._instance || new State() 10 | } 11 | 12 | experience = Experience.getInstance() 13 | renderer = this.experience.renderer.instance 14 | postprocessing = true; 15 | floatType = this.renderer.capabilities.isWebGL2 ? THREE.FloatType : THREE.HalfFloatType; 16 | 17 | unrealBloom = { 18 | enabled: false, 19 | strength: 0.6, 20 | radius: 1.9, 21 | threshold: 0.362, 22 | } 23 | 24 | motionBlur = { 25 | enabled: false, 26 | cameraBlur: true, 27 | animate: true, 28 | samples: 8, 29 | expandGeometry: 1, 30 | interpolateGeometry: 1, 31 | smearIntensity: 5, 32 | speed: 20, 33 | renderTargetScale: 1, 34 | jitter: 1, 35 | jitterStrategy: 2, 36 | }; 37 | 38 | bokeh = { 39 | enabled: true, 40 | focus: 91.902, 41 | aperture: 3.5, 42 | maxblur: 0 43 | } 44 | 45 | constructor() { 46 | // Singleton 47 | if ( State._instance ) { 48 | return State._instance 49 | } 50 | State._instance = this 51 | 52 | this.experience = Experience.getInstance() 53 | this.renderer = this.experience.renderer.instance 54 | this.scene = this.experience.scene 55 | this.camera = this.experience.camera.instance 56 | this.canvas = this.experience.canvas 57 | this.sizes = Sizes.getInstance() 58 | 59 | this.setLayers() 60 | } 61 | 62 | setLayers() { 63 | this.layersConst = { 64 | BLOOM_SCENE: 1, 65 | DEFAULT: 0, 66 | } 67 | this.bloomLayer = new THREE.Layers(); 68 | this.bloomLayer.set( this.layersConst.BLOOM_SCENE ); 69 | } 70 | 71 | resize() { 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Experience/Ui/Ui.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from '../Utils/EventEmitter.js' 2 | import Experience from '../Experience.js' 3 | import Sizes from '../Utils/Sizes.js' 4 | 5 | 6 | export default class Ui extends EventEmitter { 7 | static _instance = null 8 | 9 | static getInstance() { 10 | return Ui._instance || new Ui() 11 | } 12 | 13 | experience = Experience.getInstance() 14 | sizes = Sizes.getInstance() 15 | 16 | constructor() { 17 | 18 | // Singleton 19 | if ( Ui._instance ) { 20 | return Ui._instance 21 | } 22 | 23 | super() 24 | 25 | Ui._instance = this 26 | 27 | this.init() 28 | } 29 | 30 | init() { 31 | this.preloader = document.getElementById( "preloader" ) 32 | this.playButton = document.getElementById( "play-button" ) 33 | } 34 | 35 | hardRemovePreloader() { 36 | this.playButton.classList.replace( "fade-in", "fade-out" ); 37 | this.preloader.classList.add( "preloaded" ); 38 | this.preloader.remove(); 39 | this.playButton.remove(); 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Experience/Utils/Debug.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Stats from 'stats.js' 3 | import GUI from 'lil-gui'; 4 | import Experience from "../Experience.js"; 5 | import Sizes from "./Sizes.js"; 6 | 7 | export default class Debug { 8 | 9 | static _instance = null 10 | 11 | static getInstance() { 12 | return Debug._instance || new Debug() 13 | } 14 | 15 | experience = Experience.getInstance() 16 | sizes = Sizes.getInstance() 17 | 18 | constructor() { 19 | // Singleton 20 | if ( Debug._instance ) { 21 | return Debug._instance 22 | } 23 | Debug._instance = this 24 | 25 | this.active = window.location.hash === '#debug' 26 | 27 | if ( this.active ) { 28 | this.panel = new GUI(); 29 | 30 | // this.ui = new dat.GUI() 31 | this.stats = new Stats() 32 | this.stats.showPanel( 0 ); 33 | 34 | document.body.appendChild( this.stats.dom ) 35 | } 36 | } 37 | 38 | createDebugTexture( texture ) { 39 | this.debugTexture = texture; 40 | this.cameraOrtho = new THREE.OrthographicCamera( -this.sizes.width / 2, this.sizes.width / 2, this.sizes.height / 2, -this.sizes.height / 2, 1, 10 ); 41 | this.cameraOrtho.position.z = 10; 42 | 43 | this.sceneOrtho = new THREE.Scene(); 44 | 45 | texture.colorSpace = THREE.SRGBColorSpace; 46 | 47 | const material = new THREE.SpriteMaterial( { 48 | map: texture, 49 | //blending: THREE.NoBlending 50 | } ); 51 | 52 | const width = 128; 53 | const height = 128; 54 | 55 | //const width = material.map.image.width; 56 | //const height = material.map.image.height; 57 | 58 | this.sprite = new THREE.Sprite( material ); 59 | this.sprite.center.set( 0.0, 0.0 ); 60 | this.sprite.scale.set( width, height, 1 ); 61 | this.sceneOrtho.add( this.sprite ); 62 | 63 | this.updateSprite(); 64 | } 65 | 66 | updateSprite() { 67 | if ( !this.debugTexture ) return; 68 | 69 | this.cameraOrtho.left = -this.sizes.width / 2; 70 | this.cameraOrtho.right = this.sizes.width / 2; 71 | this.cameraOrtho.top = this.sizes.height / 2; 72 | this.cameraOrtho.bottom = -this.sizes.height / 2; 73 | this.cameraOrtho.updateProjectionMatrix(); 74 | 75 | const width = this.sizes.width / 2; 76 | const height = this.sizes.height / 2; 77 | 78 | this.sprite.position.set( -width, -height, 1 ); // bottom left 79 | } 80 | 81 | resize() { 82 | this.updateSprite(); 83 | } 84 | 85 | update( deltaTime ) { 86 | if ( this.debugTexture ) { 87 | this.experience?.renderer.instance.render( this.sceneOrtho, this.cameraOrtho ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Experience/Utils/ElastickNumner.js: -------------------------------------------------------------------------------- 1 | export default class ElasticNumber { 2 | constructor(initialValue) { 3 | this.value = initialValue; 4 | this.target = initialValue; 5 | this.speed = 3; 6 | } 7 | 8 | update(time) { 9 | let delta = this.target - this.value; 10 | 11 | this.value += delta * (this.speed * Math.min(time, 0.1)); 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Experience/Utils/EventEmitter.js: -------------------------------------------------------------------------------- 1 | export default class EventEmitter 2 | { 3 | constructor() 4 | { 5 | this.callbacks = {} 6 | this.callbacks.base = {} 7 | } 8 | 9 | on(_names, callback) 10 | { 11 | // Errors 12 | if(typeof _names === 'undefined' || _names === '') 13 | { 14 | console.warn('wrong names') 15 | return false 16 | } 17 | 18 | if(typeof callback === 'undefined') 19 | { 20 | console.warn('wrong callback') 21 | return false 22 | } 23 | 24 | // Resolve names 25 | const names = this.resolveNames(_names) 26 | 27 | // Each name 28 | names.forEach((_name) => 29 | { 30 | // Resolve name 31 | const name = this.resolveName(_name) 32 | 33 | // Create namespace if not exist 34 | if(!(this.callbacks[ name.namespace ] instanceof Object)) 35 | this.callbacks[ name.namespace ] = {} 36 | 37 | // Create callback if not exist 38 | if(!(this.callbacks[ name.namespace ][ name.value ] instanceof Array)) 39 | this.callbacks[ name.namespace ][ name.value ] = [] 40 | 41 | // Add callback 42 | this.callbacks[ name.namespace ][ name.value ].push(callback) 43 | }) 44 | 45 | return this 46 | } 47 | 48 | off(_names) 49 | { 50 | // Errors 51 | if(typeof _names === 'undefined' || _names === '') 52 | { 53 | console.warn('wrong name') 54 | return false 55 | } 56 | 57 | // Resolve names 58 | const names = this.resolveNames(_names) 59 | 60 | // Each name 61 | names.forEach((_name) => 62 | { 63 | // Resolve name 64 | const name = this.resolveName(_name) 65 | 66 | // Remove namespace 67 | if(name.namespace !== 'base' && name.value === '') 68 | { 69 | delete this.callbacks[ name.namespace ] 70 | } 71 | 72 | // Remove specific callback in namespace 73 | else 74 | { 75 | // Default 76 | if(name.namespace === 'base') 77 | { 78 | // Try to remove from each namespace 79 | for(const namespace in this.callbacks) 80 | { 81 | if(this.callbacks[ namespace ] instanceof Object && this.callbacks[ namespace ][ name.value ] instanceof Array) 82 | { 83 | delete this.callbacks[ namespace ][ name.value ] 84 | 85 | // Remove namespace if empty 86 | if(Object.keys(this.callbacks[ namespace ]).length === 0) 87 | delete this.callbacks[ namespace ] 88 | } 89 | } 90 | } 91 | 92 | // Specified namespace 93 | else if(this.callbacks[ name.namespace ] instanceof Object && this.callbacks[ name.namespace ][ name.value ] instanceof Array) 94 | { 95 | delete this.callbacks[ name.namespace ][ name.value ] 96 | 97 | // Remove namespace if empty 98 | if(Object.keys(this.callbacks[ name.namespace ]).length === 0) 99 | delete this.callbacks[ name.namespace ] 100 | } 101 | } 102 | }) 103 | 104 | return this 105 | } 106 | 107 | trigger(_name, _args) 108 | { 109 | // Errors 110 | if(typeof _name === 'undefined' || _name === '') 111 | { 112 | console.warn('wrong name') 113 | return false 114 | } 115 | 116 | let finalResult = null 117 | let result = null 118 | 119 | // Default args 120 | const args = !(_args instanceof Array) ? [] : _args 121 | 122 | // Resolve names (should on have one event) 123 | let name = this.resolveNames(_name) 124 | 125 | // Resolve name 126 | name = this.resolveName(name[ 0 ]) 127 | 128 | // Default namespace 129 | if(name.namespace === 'base') 130 | { 131 | // Try to find callback in each namespace 132 | for(const namespace in this.callbacks) 133 | { 134 | if(this.callbacks[ namespace ] instanceof Object && this.callbacks[ namespace ][ name.value ] instanceof Array) 135 | { 136 | this.callbacks[ namespace ][ name.value ].forEach(function(callback) 137 | { 138 | result = callback.apply(this, args) 139 | 140 | if(typeof finalResult === 'undefined') 141 | { 142 | finalResult = result 143 | } 144 | }) 145 | } 146 | } 147 | } 148 | 149 | // Specified namespace 150 | else if(this.callbacks[ name.namespace ] instanceof Object) 151 | { 152 | if(name.value === '') 153 | { 154 | console.warn('wrong name') 155 | return this 156 | } 157 | 158 | this.callbacks[ name.namespace ][ name.value ].forEach(function(callback) 159 | { 160 | result = callback.apply(this, args) 161 | 162 | if(typeof finalResult === 'undefined') 163 | finalResult = result 164 | }) 165 | } 166 | 167 | return finalResult 168 | } 169 | 170 | resolveNames(_names) 171 | { 172 | let names = _names 173 | names = names.replace(/[^a-zA-Z0-9 ,/.]/g, '') 174 | names = names.replace(/[,/]+/g, ' ') 175 | names = names.split(' ') 176 | 177 | return names 178 | } 179 | 180 | resolveName(name) 181 | { 182 | const newName = {} 183 | const parts = name.split('.') 184 | 185 | newName.original = name 186 | newName.value = parts[ 0 ] 187 | newName.namespace = 'base' // Base namespace 188 | 189 | // Specified namespace 190 | if(parts.length > 1 && parts[ 1 ] !== '') 191 | { 192 | newName.namespace = parts[ 1 ] 193 | } 194 | 195 | return newName 196 | } 197 | } -------------------------------------------------------------------------------- /src/Experience/Utils/FBO.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | import Sizes from "./Sizes.js" 4 | import State from "../State.js"; 5 | 6 | export default class FBO { 7 | 8 | static _instance = null 9 | 10 | static getInstance() { 11 | return FBO._instance || new FBO() 12 | } 13 | 14 | experience = Experience.getInstance() 15 | renderer = this.experience.renderer 16 | state = State.getInstance() 17 | floatType = this.state.floatType 18 | 19 | constructor() { 20 | 21 | if ( FBO._instance ) { 22 | return FBO._instance 23 | } 24 | 25 | FBO._instance = this 26 | 27 | this.experience = Experience.getInstance() 28 | this.sizes = Sizes.getInstance() 29 | this.renderer = this.experience.renderer 30 | this.debug = this.experience.debug 31 | } 32 | 33 | createRenderTarget( width, height, nearestFilter = false, floatType = false, samples = 0 ) { 34 | return new THREE.WebGLRenderTarget( width, height, { 35 | wrapS: THREE.ClampToEdgeWrapping, 36 | wrapT: THREE.ClampToEdgeWrapping, 37 | magFilter: nearestFilter ? THREE.NearestFilter : THREE.LinearFilter, 38 | minFilter: nearestFilter ? THREE.NearestFilter : THREE.LinearFilter, 39 | type: floatType ? this.floatType : THREE.UnsignedByteType, 40 | anisotropy: 0, 41 | colorSpace: THREE.SRGBColorSpace, 42 | depthBuffer: true, 43 | stencilBuffer: false, 44 | samples: this.sizes.pixelRatio <= 2 ? samples : 0 45 | } ) 46 | } 47 | 48 | 49 | setInstance() { 50 | 51 | } 52 | 53 | setDebug() { 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Experience/Utils/Gizmo.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class Gizmo extends HTMLElement{ 4 | constructor(camera, options) { 5 | super(); 6 | this.camera = camera; 7 | this.options = Object.assign({ 8 | size: 90, 9 | padding: 8, 10 | bubbleSizePrimary: 8, 11 | bubbleSizeSeconday: 6, 12 | showSecondary: true, 13 | lineWidth: 2, 14 | fontSize: "11px", 15 | fontFamily: "arial", 16 | fontWeight: "bold", 17 | fontColor: "#151515", 18 | fontYAdjust: 0, 19 | colors: { 20 | x: ["#f73c3c", "#942424"], 21 | y: ["#6ccb26", "#417a17"], 22 | z: ["#178cf0", "#0e5490"], 23 | } 24 | }, options); 25 | 26 | // Function called when axis is clicked 27 | this.onAxisSelected = null; 28 | 29 | // Generate list of axes 30 | this.bubbles = [ 31 | { axis: "x", direction: new THREE.Vector3(1, 0, 0), size: this.options.bubbleSizePrimary, color: this.options.colors.x, line: this.options.lineWidth, label: "X" }, 32 | { axis: "y", direction: new THREE.Vector3(0, 1, 0), size: this.options.bubbleSizePrimary, color: this.options.colors.y, line: this.options.lineWidth, label: "Y" }, 33 | { axis: "z", direction: new THREE.Vector3(0, 0, 1), size: this.options.bubbleSizePrimary, color: this.options.colors.z, line: this.options.lineWidth, label: "Z" }, 34 | { axis: "-x", direction: new THREE.Vector3(-1, 0, 0), size: this.options.bubbleSizeSeconday, color: this.options.colors.x }, 35 | { axis: "-y", direction: new THREE.Vector3(0, -1, 0), size: this.options.bubbleSizeSeconday, color: this.options.colors.y }, 36 | { axis: "-z", direction: new THREE.Vector3(0, 0, -1), size: this.options.bubbleSizeSeconday, color: this.options.colors.z }, 37 | ]; 38 | 39 | this.center = new THREE.Vector3(this.options.size / 2, this.options.size / 2, 0); 40 | this.selectedAxis = null; 41 | 42 | // All we need is a canvas 43 | this.innerHTML = ""; 44 | 45 | this.onMouseMove = this.onMouseMove.bind(this); 46 | this.onMouseOut = this.onMouseOut.bind(this); 47 | this.onMouseClick = this.onMouseClick.bind(this); 48 | } 49 | 50 | connectedCallback() { 51 | this.canvas = this.querySelector("canvas"); 52 | this.context = this.canvas.getContext("2d"); 53 | 54 | this.canvas.addEventListener('mousemove', this.onMouseMove, false); 55 | this.canvas.addEventListener('mouseout', this.onMouseOut, false); 56 | this.canvas.addEventListener('click', this.onMouseClick, false); 57 | } 58 | 59 | disconnectedCallback() { 60 | this.canvas.removeEventListener('mousemove', this.onMouseMove, false); 61 | this.canvas.removeEventListener('mouseout', this.onMouseOut, false); 62 | this.canvas.removeEventListener('click', this.onMouseClick, false); 63 | } 64 | 65 | onMouseMove(evt) { 66 | const rect = this.canvas.getBoundingClientRect(); 67 | this.mouse = new THREE.Vector3(evt.clientX - rect.left, evt.clientY - rect.top, 0); 68 | } 69 | 70 | onMouseOut(evt) { 71 | this.mouse = null; 72 | } 73 | 74 | onMouseClick(evt) { 75 | if (!!this.onAxisSelected && typeof this.onAxisSelected == "function") { 76 | this.onAxisSelected({ axis: this.selectedAxis.axis, direction: this.selectedAxis.direction.clone() }); 77 | } 78 | } 79 | 80 | clear() { 81 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 82 | } 83 | 84 | drawCircle(p, radius = 10, color = "#FF0000") { 85 | this.context.beginPath(); 86 | this.context.arc(p.x, p.y, radius, 0, 2 * Math.PI, false); 87 | this.context.fillStyle = color; 88 | this.context.fill(); 89 | this.context.closePath(); 90 | } 91 | 92 | drawLine(p1, p2, width = 1, color = "#FF0000") { 93 | this.context.beginPath(); 94 | this.context.moveTo(p1.x, p1.y); 95 | this.context.lineTo(p2.x, p2.y); 96 | this.context.lineWidth = width; 97 | this.context.strokeStyle = color; 98 | this.context.stroke(); 99 | this.context.closePath(); 100 | } 101 | 102 | update() { 103 | this.clear(); 104 | 105 | // Calculate the rotation matrix from the camera 106 | let rotMat = new THREE.Matrix4().makeRotationFromEuler(this.camera.rotation); 107 | let invRotMat = rotMat.clone().invert(); 108 | 109 | for (var bubble of this.bubbles) { 110 | bubble.position = this.getBubblePosition(bubble.direction.clone().applyMatrix4(invRotMat)); 111 | } 112 | 113 | // Generate a list of layers to draw 114 | const layers = []; 115 | for (let axis in this.bubbles) { 116 | // Check if the name starts with a negative and dont add it to the layer list if secondary axis is turned off 117 | if (this.options.showSecondary === true || axis[0] !== "-") { 118 | layers.push(this.bubbles[axis]); 119 | } 120 | } 121 | 122 | // Sort the layers where the +Z position is last so its drawn on top of anything below it 123 | layers.sort((a, b) => (a.position.z > b.position.z) ? 1 : -1); 124 | 125 | // If the mouse is over the gizmo, find the closest axis and highlight it 126 | this.selectedAxis = null; 127 | 128 | if (this.mouse) { 129 | let closestDist = Infinity; 130 | 131 | // Loop through each layer 132 | for (var bubble of layers) { 133 | const distance = this.mouse.distanceTo(bubble.position); 134 | 135 | // Only select the axis if its closer to the mouse than the previous or if its within its bubble circle 136 | if (distance < closestDist || distance < bubble.size) { 137 | closestDist = distance; 138 | this.selectedAxis = bubble; 139 | } 140 | } 141 | } 142 | 143 | // Draw the layers 144 | this.drawLayers(layers); 145 | } 146 | 147 | drawLayers(layers) { 148 | // For each layer, draw the bubble 149 | for (let bubble of layers) { 150 | let color = bubble.color; 151 | 152 | // Find the color 153 | if (this.selectedAxis === bubble) { 154 | color = "#FFFFFF"; 155 | } else if (bubble.position.z >= -0.01) { 156 | color = bubble.color[0] 157 | } else { 158 | color = bubble.color[1] 159 | } 160 | 161 | // Draw the circle for the bubbble 162 | this.drawCircle(bubble.position, bubble.size, color); 163 | 164 | // Draw the line that connects it to the center if enabled 165 | if (bubble.line) { 166 | this.drawLine(this.center, bubble.position, bubble.line, color); 167 | } 168 | 169 | // Write the axis label (X,Y,Z) if provided 170 | if (bubble.label) { 171 | this.context.font = [this.options.fontWeight, this.options.fontSize, this.options.fontFamily].join(" "); 172 | this.context.fillStyle = this.options.fontColor; 173 | this.context.textBaseline = 'middle'; 174 | this.context.textAlign = 'center'; 175 | this.context.fillText(bubble.label, bubble.position.x, bubble.position.y + this.options.fontYAdjust); 176 | } 177 | } 178 | } 179 | 180 | getBubblePosition(position) { 181 | return new THREE.Vector3((position.x * (this.center.x - (this.options.bubbleSizePrimary / 2) - this.options.padding)) + this.center.x, 182 | this.center.y - (position.y * (this.center.y - (this.options.bubbleSizePrimary / 2) - this.options.padding)), 183 | position.z); 184 | } 185 | } 186 | 187 | window.customElements.define('gizmo-helper', Gizmo); 188 | -------------------------------------------------------------------------------- /src/Experience/Utils/Helpers.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | // get position from min Y three attribute position array 4 | export function getMinPositionY( positions ) { 5 | let minPosition = new THREE.Vector3(positions[0], positions[1], positions[2]); 6 | 7 | let minY = Number.MAX_VALUE; 8 | 9 | for (let i = 0; i < positions.length / 3; i++) { 10 | 11 | if (positions[3 * i + 1] < minY) { 12 | minY = positions[3 * i + 1]; 13 | 14 | minPosition.set( 15 | positions[3 * i + 0], 16 | positions[3 * i + 1], 17 | positions[3 * i + 2] 18 | ); 19 | } 20 | } 21 | 22 | return minPosition; 23 | } 24 | 25 | // get centroid from three attribute position array 26 | export function getCentroid( positions ) { 27 | let centroid = new THREE.Vector3(); 28 | 29 | for (let i = 0; i < positions.length / 3; i++) { 30 | centroid.x += positions[3 * i + 0]; 31 | centroid.y += positions[3 * i + 1]; 32 | centroid.z += positions[3 * i + 2]; 33 | } 34 | 35 | centroid.x /= positions.length / 3; 36 | centroid.y /= positions.length / 3; 37 | centroid.z /= positions.length / 3; 38 | 39 | // // get nearest point to the centroid 40 | // let nearestPoint = new THREE.Vector3(positions[0], positions[1], positions[2]); 41 | // let nearestDistance = centroid.distanceTo(nearestPoint); 42 | // 43 | // for (let i = 0; i < positions.length / 3; i++) { 44 | // let point = new THREE.Vector3( 45 | // positions[3 * i + 0], 46 | // positions[3 * i + 1], 47 | // positions[3 * i + 2] 48 | // ); 49 | // 50 | // let distance = centroid.distanceTo(point); 51 | // 52 | // if (distance < nearestDistance) { 53 | // nearestDistance = distance; 54 | // nearestPoint = point; 55 | // } 56 | // } 57 | // 58 | // return nearestPoint; 59 | 60 | return centroid; 61 | } 62 | 63 | export function cloneAllMaterials( container ) { 64 | container.traverse( child => { 65 | if ( child.isMesh ) { 66 | 67 | const cloneMaterial = child.material.clone(); 68 | child.material.dispose(); 69 | child.material = cloneMaterial; 70 | } 71 | } ) 72 | } 73 | 74 | export function meshToBatchedMesh( mesh, container, batchedMeshesIds = {} ) { 75 | let materials = []; 76 | 77 | let maxGeometryCount = 0; 78 | let maxVertexCount = 0; 79 | let maxIndexCount = 0; 80 | 81 | mesh.traverse( ( child ) => { 82 | if ( child.isMesh ) { 83 | maxGeometryCount++; 84 | maxVertexCount += child.geometry.attributes.position.count; 85 | maxIndexCount += child.geometry.index.count; 86 | } 87 | } ) 88 | 89 | mesh.traverse( ( child ) => { 90 | if ( child.isMesh ) { 91 | child.updateMatrixWorld() 92 | // child.geometry.applyMatrix4( child.matrixWorld ) 93 | 94 | if ( materials[ child.material.uuid ] === undefined ) { 95 | const batchedMesh = new THREE.BatchedMesh( maxGeometryCount, maxVertexCount, maxIndexCount, child.material ); 96 | container.add( batchedMesh ); 97 | batchedMeshesIds[ batchedMesh.uuid ] = []; 98 | 99 | materials[ child.material.uuid ] = {}; 100 | materials[ child.material.uuid ].batchedMesh = batchedMesh; 101 | 102 | const geometry = child.geometry.clone(); 103 | geometry.applyMatrix4( child.matrixWorld ); 104 | 105 | const batchId = batchedMesh.addGeometry( geometry ); 106 | batchedMeshesIds[ batchedMesh.uuid ][ batchId ] = child; 107 | batchedMeshesIds[ batchedMesh.uuid ][ 'batchedMesh' ] = batchedMesh; 108 | 109 | } else { 110 | const geometry = child.geometry.clone(); 111 | geometry.applyMatrix4( child.matrixWorld ); 112 | const batchId = materials[ child.material.uuid ].batchedMesh.addGeometry( geometry ); 113 | batchedMeshesIds[ materials[ child.material.uuid ].batchedMesh.uuid ][ batchId ] = child; 114 | } 115 | } 116 | } ) 117 | } 118 | 119 | export function makeTexture( g ) { 120 | 121 | let vertAmount = g.attributes.position.count; 122 | let texWidth = Math.ceil( Math.sqrt( vertAmount ) ); 123 | let texHeight = Math.ceil( vertAmount / texWidth ); 124 | 125 | let data = new Float32Array( texWidth * texHeight * 4 ); 126 | 127 | function shuffleArrayByThree( array ) { 128 | const groupLength = 3; 129 | 130 | let numGroups = Math.floor( array.length / groupLength ); 131 | 132 | for ( let i = numGroups - 1; i > 0; i-- ) { 133 | const j = Math.floor( Math.random() * ( i + 1 ) ); 134 | 135 | for ( let k = 0; k < groupLength; k++ ) { 136 | let temp = array[ i * groupLength + k ]; 137 | array[ i * groupLength + k ] = array[ j * groupLength + k ]; 138 | array[ j * groupLength + k ] = temp; 139 | } 140 | } 141 | 142 | return array; 143 | } 144 | 145 | 146 | shuffleArrayByThree( g.attributes.position.array ); 147 | 148 | for ( let i = 0; i < vertAmount; i++ ) { 149 | //let f = Math.floor(Math.random() * (randomTemp.length / 3) ); 150 | 151 | const x = g.attributes.position.array[ i * 3 + 0 ] ?? 2; 152 | const y = g.attributes.position.array[ i * 3 + 1 ] ?? 0; 153 | const z = g.attributes.position.array[ i * 3 + 2 ] ?? 0; 154 | const w = 0; 155 | 156 | //randomTemp.splice(f * 3, 3); 157 | 158 | data[ i * 4 + 0 ] = x; 159 | data[ i * 4 + 1 ] = y; 160 | data[ i * 4 + 2 ] = z; 161 | data[ i * 4 + 3 ] = w; 162 | } 163 | 164 | let dataTexture = new THREE.DataTexture( data, texWidth, texHeight, THREE.RGBAFormat, THREE.FloatType ); 165 | dataTexture.needsUpdate = true; 166 | 167 | return dataTexture; 168 | } 169 | -------------------------------------------------------------------------------- /src/Experience/Utils/Input.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | 4 | import normalizeWheel from 'normalize-wheel' 5 | import Sizes from "./Sizes.js"; 6 | 7 | export default class Input { 8 | static _instance = null 9 | 10 | static getInstance() { 11 | return Input._instance || new Input() 12 | } 13 | 14 | constructor() { 15 | if ( Input._instance ) { 16 | return Input._instance 17 | } 18 | Input._instance = this 19 | 20 | this.experience = Experience.getInstance() 21 | this.sizes = Sizes.getInstance() 22 | 23 | // Wait for resources 24 | this.experience.on( 'classesReady', () => { 25 | this.camera = this.experience.camera.instance 26 | } ) 27 | 28 | this.cursor = { x: 0, y: 0, side: 'left'} 29 | this.cursor3D = new THREE.Vector3() 30 | this.cursorDirection = new THREE.Vector3() 31 | this.clientX = 0 32 | this.clientY = 0 33 | 34 | this.init() 35 | } 36 | 37 | init() { 38 | window.addEventListener( 'mousemove', this._onMouseMoved) 39 | window.addEventListener( 'touchstart', this._onTouchStart) 40 | window.addEventListener( 'touchmove', this._onTouchMoved) 41 | } 42 | 43 | postInit() { 44 | 45 | } 46 | 47 | projectNDCTo3D(x, y) { 48 | const vector = new THREE.Vector3(x, y, 0.5); 49 | vector.unproject(this.camera); 50 | 51 | const dir = vector.sub(this.camera.position).normalize(); // Direction from camera to point in NDC 52 | const cameraDirection = new THREE.Vector3(); 53 | this.camera.getWorldDirection(cameraDirection); // Camera view direction 54 | 55 | // Distance to the plane perpendicular to the camera view direction 56 | const distance = - this.camera.position.dot(cameraDirection) / dir.dot(cameraDirection); 57 | 58 | // Point in 3D space 59 | return this.camera.position.clone().add(dir.multiplyScalar(distance)); 60 | } 61 | 62 | getNDCFrom3d(x, y, z) { 63 | const vector = new THREE.Vector3( x, y, z ); 64 | vector.project( this.camera ); 65 | return vector; 66 | } 67 | 68 | 69 | _onMouseMoved = ( event ) =>{ 70 | this.clientX = event.clientX 71 | this.clientY = event.clientY 72 | 73 | this.cursor.x = event.clientX / this.sizes.width * 2 - 1 74 | this.cursor.y = -( event.clientY / this.sizes.height ) * 2 + 1 75 | this.cursor.side = event.clientX > this.sizes.width / 2 ? 'right' : 'left' 76 | 77 | this.previosCursor3D = this.cursor3D.clone() 78 | this.cursor3D = this.projectNDCTo3D(this.cursor.x, this.cursor.y) 79 | this.cursorDirection = this.cursor3D.clone().sub(this.previosCursor3D).normalize() 80 | } 81 | 82 | _onTouchStart = ( event ) => { 83 | this._onTouchMoved( event ) 84 | } 85 | 86 | _onTouchMoved = ( event ) => { 87 | this.cursor.x = event.touches[ 0 ].clientX / this.sizes.width * 2 - 1 88 | this.cursor.y = -( event.touches[ 0 ].clientY / this.sizes.height ) * 2 + 1 89 | this.cursor.side = event.touches[ 0 ].clientX > this.sizes.width / 2 ? 'right' : 'left' 90 | 91 | this.previosCursor3D = this.cursor3D.clone() 92 | this.cursor3D = this.projectNDCTo3D(this.cursor.x, this.cursor.y) 93 | this.cursorDirection = this.cursor3D.clone().sub(this.previosCursor3D).normalize() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Experience/Utils/MathHelper.js: -------------------------------------------------------------------------------- 1 | export function mix( x, y, a ) { 2 | return x * ( 1 - a ) + y * a; 3 | } 4 | 5 | export function remap( x, oMin, oMax, nMin, nMax ) { 6 | return mix( nMin, nMax, ( x - oMin ) / ( oMax - oMin ) ); 7 | } 8 | 9 | export function simplexNoise4d( x, y, z, w ) { 10 | return simplex.noise4d( x, y, z, w ); 11 | } 12 | -------------------------------------------------------------------------------- /src/Experience/Utils/PostProcess.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | import Debug from '../Utils/Debug.js' 4 | import State from "../State.js"; 5 | import Sizes from "./Sizes.js"; 6 | import Materials from "../Materials/Materials.js"; 7 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 8 | import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; 9 | import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; 10 | import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; 11 | import { MotionBlurPass } from '../Passes/motionBlurPass/src/MotionBlurPass.js'; 12 | import { BokehPass } from 'three/addons/postprocessing/BokehPass.js'; 13 | import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js'; 14 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 15 | import FBO from "./FBO.js"; 16 | 17 | import BloomVertex from '../Shaders/Bloom/vertex.glsl' 18 | import BloomFragment from '../Shaders/Bloom/fragment.glsl' 19 | 20 | import CompositeMaterialFragment from '../Shaders/Bloom/CompositeMaterial/fragment.glsl' 21 | 22 | export default class PostProcess { 23 | experience = Experience.getInstance() 24 | debug = Debug.getInstance() 25 | sizes = Sizes.getInstance() 26 | state = State.getInstance() 27 | materials = Materials.getInstance() 28 | fbo = FBO.getInstance() 29 | 30 | rendererClass = this.experience.renderer 31 | scene = experience.scene 32 | time = experience.time 33 | camera = experience.camera.instance 34 | resources = experience.resources 35 | timeline = experience.time.timeline; 36 | container = new THREE.Group(); 37 | 38 | constructor( renderer ) { 39 | this.renderer = renderer 40 | this.setComposer() 41 | this.setDebug() 42 | } 43 | 44 | setComposer() { 45 | /** 46 | * Passes 47 | */ 48 | // Render pass 49 | this.renderPass = new RenderPass( this.scene, this.camera ) 50 | 51 | this.bloomComposer = this._bloomComposer() 52 | this.mixPass = this._mixPass() 53 | this.outputPass = new OutputPass() 54 | //this.motionBlurPass = this._motionBlurPass() 55 | //this.bokehPass = this._bokehPass() 56 | this.afterimagePass = this._afterimagePass() 57 | 58 | this.renderTarget = this.fbo.createRenderTarget( this.sizes.width, this.sizes.height, false, false, 0 ) 59 | this.composer = new EffectComposer( this.renderer, this.renderTarget ) 60 | this.composer.setSize( this.sizes.width, this.sizes.height ) 61 | this.composer.setPixelRatio( this.sizes.pixelRatio ) 62 | 63 | this.composer.addPass( this.renderPass ) 64 | this.composer.addPass( this.mixPass ) 65 | this.composer.addPass( this.unrealBloomPass ) 66 | //this.composer.addPass( this.motionBlurPass ) 67 | //this.composer.addPass( this.bokehPass ) 68 | //this.composer.addPass( this.afterimagePass ); 69 | this.composer.addPass( this.outputPass ) 70 | } 71 | 72 | _bloomComposer() { 73 | 74 | this.renderTargetBloom = this.fbo.createRenderTarget( this.sizes.width, this.sizes.height, false, false, 0 ) 75 | 76 | this.unrealBloomPass = this._bloomPass() 77 | 78 | const bloomComposer = new EffectComposer( this.renderer, this.renderTargetBloom ); 79 | bloomComposer.renderToScreen = false; 80 | bloomComposer.addPass( this.renderPass ); 81 | bloomComposer.addPass( this.unrealBloomPass ) 82 | 83 | return bloomComposer 84 | } 85 | 86 | _bokehPass() { 87 | const bokehPass = new BokehPass( this.scene, this.camera, { 88 | focus: this.state.bokeh.focus, 89 | aperture: this.state.bokeh.aperture * 0.00001, 90 | maxblur: this.state.bokeh.maxblur, 91 | } ); 92 | 93 | 94 | 95 | return bokehPass; 96 | } 97 | 98 | _bloomPass() { 99 | const unrealBloomPass = new UnrealBloomPass( 100 | new THREE.Vector2( this.sizes.width, this.sizes.height ), 101 | this.state.unrealBloom.strength, 102 | this.state.unrealBloom.radius, 103 | this.state.unrealBloom.threshold 104 | ) 105 | 106 | unrealBloomPass.enabled = this.state.unrealBloom.enabled 107 | unrealBloomPass.renderToScreen = false 108 | 109 | unrealBloomPass.tintColor = {} 110 | unrealBloomPass.tintColor.value = '#000000' 111 | unrealBloomPass.tintColor.instance = new THREE.Color( unrealBloomPass.tintColor.value ) 112 | 113 | unrealBloomPass.compositeMaterial.uniforms.uTintColor = { value: unrealBloomPass.tintColor.instance } 114 | unrealBloomPass.compositeMaterial.uniforms.uTintStrength = { value: 0.15 } 115 | //unrealBloomPass.compositeMaterial.fragmentShader = CompositeMaterialFragment 116 | 117 | return unrealBloomPass 118 | } 119 | 120 | _mixPass() { 121 | const mixPass = new ShaderPass( 122 | new THREE.ShaderMaterial( { 123 | uniforms: { 124 | baseTexture: { value: null }, 125 | bloomTexture: { value: this.bloomComposer.renderTarget2.texture } 126 | }, 127 | vertexShader: BloomVertex, 128 | fragmentShader: BloomFragment, 129 | defines: {} 130 | } ), 'baseTexture' 131 | ); 132 | mixPass.needsSwap = true; 133 | 134 | return mixPass 135 | } 136 | 137 | _motionBlurPass() { 138 | const motionBlurPass = new MotionBlurPass( this.scene, this.camera ); 139 | 140 | motionBlurPass.enabled = this.state.motionBlur.enabled; 141 | motionBlurPass.samples = this.state.motionBlur.samples; 142 | motionBlurPass.expandGeometry = this.state.motionBlur.expandGeometry; 143 | motionBlurPass.interpolateGeometry = this.state.motionBlur.interpolateGeometry; 144 | motionBlurPass.renderCameraBlur = this.state.motionBlur.cameraBlur; 145 | motionBlurPass.smearIntensity = this.state.motionBlur.smearIntensity; 146 | motionBlurPass.jitter = this.state.motionBlur.jitter; 147 | motionBlurPass.jitterStrategy = this.state.motionBlur.jitterStrategy; 148 | 149 | return motionBlurPass; 150 | } 151 | 152 | _afterimagePass() { 153 | 154 | 155 | 156 | return new AfterimagePass() 157 | } 158 | 159 | resize() { 160 | this.composer.setSize( this.sizes.width, this.sizes.height ) 161 | this.composer.setPixelRatio( this.sizes.pixelRatio ) 162 | 163 | this.bloomComposer?.setSize( this.sizes.width, this.sizes.height ) 164 | this.bloomComposer?.setPixelRatio( this.sizes.pixelRatio ) 165 | } 166 | 167 | setDebug() { 168 | if ( !this.debug.active ) return 169 | 170 | if ( this.debug.panel ) { 171 | const PostProcessFolder = this.debug.panel.addFolder( 'PostProcess' ) 172 | PostProcessFolder.close() 173 | const bloomFolder = PostProcessFolder.addFolder( 'UnrealBloomPass' ) 174 | bloomFolder.close() 175 | 176 | bloomFolder.add( this.unrealBloomPass, 'enabled' ).name( 'Enabled' ) 177 | .onChange( () => { 178 | this.mixPass.enabled = this.unrealBloomPass.enabled 179 | } ) 180 | bloomFolder.add( this.unrealBloomPass, 'strength' ).min( 0 ).max( 5 ).step( 0.001 ).name( 'Strength' ) 181 | bloomFolder.add( this.unrealBloomPass, 'radius' ).min( -2 ).max( 1 ).step( 0.001 ).name( 'Radius' ) 182 | bloomFolder.add( this.unrealBloomPass, 'threshold' ).min( 0 ).max( 1 ).step( 0.001 ).name( 'Threshold' ) 183 | bloomFolder.addColor( this.unrealBloomPass.tintColor, 'value' ).name( 'Tint Color' ).onChange( () => { 184 | this.unrealBloomPass.tintColor.instance.set( this.unrealBloomPass.tintColor.value ) 185 | } ) 186 | bloomFolder.add( this.unrealBloomPass.compositeMaterial.uniforms.uTintStrength, 'value' ).name( 'Tint Strength' ).min( 0 ).max( 1 ).step( 0.001 ) 187 | 188 | 189 | if ( this.afterimagePass ) { 190 | const afterImageFolder = PostProcessFolder.addFolder( 'After Image' ); 191 | afterImageFolder.add( this.afterimagePass.uniforms[ 'damp' ], 'value', 0, 1 ).step( 0.001 ); 192 | } 193 | 194 | // const bokehFolder = PostProcessFolder.addFolder( 'Bokeh' ); 195 | 196 | // const matChanger = ( ) => { 197 | // 198 | // this.bokehPass.uniforms[ 'focus' ].value = this.state.bokeh.focus; 199 | // this.bokehPass.uniforms[ 'aperture' ].value = this.state.bokeh.aperture * 0.00001; 200 | // this.bokehPass.uniforms[ 'maxblur' ].value = this.state.bokeh.maxblur; 201 | // 202 | // }; 203 | // 204 | // bokehFolder.add( this.state.bokeh, 'enabled' ).name( 'Enabled' ) 205 | // .onChange( () => { 206 | // this.bokehPass.enabled = this.state.bokeh.enabled 207 | // } ); 208 | // 209 | // bokehFolder.add( this.state.bokeh, 'focus', 0, 300 ).step( 0.001 ) 210 | // .onChange( matChanger ); 211 | // 212 | // bokehFolder.add( this.state.bokeh, 'aperture', 0, 10 ).step( 0.1 ) 213 | // .onChange( matChanger ); 214 | // 215 | // bokehFolder.add( this.state.bokeh, 'maxblur', 0, 0.09 ).step( 0.001 ) 216 | // .onChange( matChanger ); 217 | 218 | 219 | // const motionFolder = PostProcessFolder.addFolder( 'Motion Blur' ); 220 | // motionFolder.add( this.state.motionBlur, 'enabled' ) 221 | // .onChange( () => { 222 | // this.motionBlurPass.enabled = this.state.motionBlur.enabled 223 | // } ); 224 | // motionFolder.add( this.state.motionBlur, 'cameraBlur' ) 225 | // .onChange( () => { 226 | // this.motionBlurPass.renderCameraBlur = this.state.motionBlur.cameraBlur 227 | // } ); 228 | // motionFolder.add( this.state.motionBlur, 'samples', 0, 50 ).step( 1 ) 229 | // .onChange( () => { 230 | // this.motionBlurPass.samples = this.state.motionBlur.samples 231 | // } ); 232 | // motionFolder.add( this.state.motionBlur, 'jitter', 0, 5 ).step( 0.01 ) 233 | // .onChange( () => { 234 | // this.motionBlurPass.jitter = this.state.motionBlur.jitter 235 | // } ); 236 | // motionFolder.add( this.state.motionBlur, 'jitterStrategy', { 237 | // REGULAR_JITTER: MotionBlurPass.REGULAR_JITTER, 238 | // RANDOM_JITTER: MotionBlurPass.RANDOM_JITTER, 239 | // BLUENOISE_JITTER: MotionBlurPass.BLUENOISE_JITTER, 240 | // } ) 241 | // .onChange( () => { 242 | // this.motionBlurPass.jitterStrategy = this.state.motionBlur.jitterStrategy 243 | // } ); 244 | // motionFolder.add( this.state.motionBlur, 'smearIntensity', 0, 10 ) 245 | // .onChange( () => { 246 | // this.motionBlurPass.smearIntensity = this.state.motionBlur.smearIntensity 247 | // } ); 248 | // motionFolder.add( this.state.motionBlur, 'expandGeometry', 0, 1 ) 249 | // .onChange( () => { 250 | // this.motionBlurPass.expandGeometry = this.state.motionBlur.expandGeometry 251 | // } ); 252 | // motionFolder.add( this.state.motionBlur, 'interpolateGeometry', 0, 1 ) 253 | // .onChange( () => { 254 | // this.motionBlurPass.interpolateGeometry = this.state.motionBlur.interpolateGeometry 255 | // } ); 256 | // motionFolder.add( this.state.motionBlur, 'renderTargetScale', 0, 1 ) 257 | // .onChange( v => { 258 | // MotionBlurPass.renderTargetScale = v; 259 | // window.resizeTo(); 260 | // } ); 261 | // 262 | // motionFolder.add( this.motionBlurPass.debug, 'display', { 263 | // 'Motion Blur': MotionBlurPass.DEFAULT, 264 | // 'Velocity': MotionBlurPass.VELOCITY, 265 | // 'Geometry': MotionBlurPass.GEOMETRY 266 | // } ).onChange( val => this.motionBlurPass.debug.display = parseFloat(val) ); 267 | // motionFolder.close() 268 | } 269 | } 270 | 271 | bloomRender() { 272 | if ( this.unrealBloomPass.enabled ) { 273 | //this.scene.traverse( this.materials._darkenNonBloomed ) 274 | this.bloomComposer.render() 275 | //this.scene.traverse( this.materials._restoreMaterial ) 276 | } 277 | 278 | } 279 | 280 | productionRender() { 281 | if ( this.state.postprocessing ) { 282 | this.bloomRender() 283 | 284 | this.composer.render() 285 | } else { 286 | this.renderer.render( this.scene, this.camera ) 287 | } 288 | } 289 | 290 | debugRender() { 291 | if ( this.state.postprocessing ) { 292 | this.bloomRender() 293 | 294 | this.renderer.autoClear = false 295 | this.composer.render() 296 | this.renderer.clearDepth() 297 | } else { 298 | this.renderer.autoClear = false 299 | this.renderer.clearColor( this.rendererClass.clearColor ) 300 | this.renderer.render( this.scene, this.camera ) 301 | this.renderer.clearDepth() 302 | } 303 | } 304 | 305 | update( deltaTime ) { 306 | if ( this.debug.active ) { 307 | this.debugRender() 308 | } else { 309 | this.productionRender() 310 | } 311 | 312 | } 313 | 314 | } 315 | -------------------------------------------------------------------------------- /src/Experience/Utils/PostProcessExternal.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | import Debug from '../Utils/Debug.js' 4 | import State from "../State.js"; 5 | import Sizes from "./Sizes.js"; 6 | import Materials from "../Materials/Materials.js"; 7 | import FBO from "./FBO.js"; 8 | 9 | import { 10 | BloomEffect, 11 | EffectComposer, 12 | EffectPass, 13 | RenderPass, 14 | BlendFunction, 15 | DepthOfFieldEffect, KernelSize 16 | } from "postprocessing"; 17 | 18 | 19 | export default class PostProcess { 20 | experience = Experience.getInstance() 21 | debug = Debug.getInstance() 22 | sizes = Sizes.getInstance() 23 | state = State.getInstance() 24 | materials = Materials.getInstance() 25 | fbo = FBO.getInstance() 26 | 27 | rendererClass = this.experience.renderer 28 | scene = experience.scene 29 | time = experience.time 30 | camera = experience.camera.instance 31 | resources = experience.resources 32 | timeline = experience.time.timeline; 33 | container = new THREE.Group(); 34 | 35 | 36 | constructor( renderer ) { 37 | this.renderer = renderer 38 | 39 | this.setComposer() 40 | this.setDebug() 41 | } 42 | 43 | setComposer() { 44 | const composer = this.composer = new EffectComposer( this.renderer, { 45 | //frameBufferType: THREE.HalfFloatType, 46 | powerPreference: "high-performance", 47 | // antialias: false, 48 | // stencil: false, 49 | // depth: false 50 | }); 51 | 52 | this.bloomPass = this._bloomPass(); 53 | 54 | composer.addPass( new RenderPass( this.scene, this.camera ) ); 55 | composer.addPass( new EffectPass( this.camera, this.bloomPass) ); 56 | 57 | // this.depthOfFieldEffect = new DepthOfFieldEffect( this.camera, { 58 | // focusDistance: 0.0, 59 | // focalLength: 0.048, 60 | // bokehScale: 2.0, 61 | // height: 240 62 | // }); 63 | 64 | // this.cocMaterial = this.depthOfFieldEffect.circleOfConfusionMaterial; 65 | // 66 | // this.params = { 67 | // "coc": { 68 | // "edge blur kernel": this.depthOfFieldEffect.blurPass.kernelSize, 69 | // "focus": this.cocMaterial.uniforms.focusDistance.value, 70 | // "focal length": this.cocMaterial.uniforms.focalLength.value 71 | // }, 72 | // "resolution": this.depthOfFieldEffect.resolution.height, 73 | // "bokeh scale": this.depthOfFieldEffect.bokehScale, 74 | // }; 75 | // 76 | // const effectPass = new EffectPass( 77 | // this.camera, 78 | // this.depthOfFieldEffect 79 | // ); 80 | 81 | //composer.addPass( effectPass ); 82 | 83 | } 84 | 85 | _bokehPass() { 86 | const bokehPass = new BokehPass( this.scene, this.camera, { 87 | focus: this.state.bokeh.focus, 88 | aperture: this.state.bokeh.aperture * 0.00001, 89 | maxblur: this.state.bokeh.maxblur, 90 | } ); 91 | 92 | 93 | return bokehPass; 94 | } 95 | 96 | 97 | _bloomPass() { 98 | this.bloomParams = { 99 | "intensity": 6., 100 | "radius": 0.493, 101 | "luminance": { 102 | "filter": true, 103 | "threshold": 0.0374, 104 | "smoothing": 1.0, 105 | } 106 | }; 107 | 108 | return new BloomEffect( 109 | { 110 | blendFunction: BlendFunction.ADD, 111 | mipmapBlur: this.bloomParams.luminance.filter, 112 | luminanceThreshold: this.bloomParams.luminance.threshold, 113 | luminanceSmoothing: this.bloomParams.luminance.smoothing, 114 | intensity: this.bloomParams.intensity, 115 | radius: this.bloomParams.radius 116 | } 117 | ) 118 | } 119 | 120 | resize() { 121 | this.composer.setSize( this.sizes.width, this.sizes.height ) 122 | //this.composer.setPixelRatio( this.sizes.pixelRatio ) 123 | 124 | this.bloomComposer?.setSize( this.sizes.width, this.sizes.height ) 125 | this.bloomComposer?.setPixelRatio( this.sizes.pixelRatio ) 126 | } 127 | 128 | setDebug() { 129 | if ( !this.debug.active ) return 130 | 131 | if ( this.debug.panel ) { 132 | const PostProcessFolder = this.debug.panel.addFolder( 'PostProcess' ) 133 | // PostProcessFolder.close() 134 | const bloomFolder = PostProcessFolder.addFolder( 'BloomPass' ) 135 | bloomFolder.close() 136 | 137 | 138 | // blendFunction: BlendFunction.ADD, 139 | // mipmapBlur: true, 140 | // luminanceThreshold: 0.001, 141 | // luminanceSmoothing: 0.2, 142 | // intensity: 100.0, 143 | // radius: 0.1 144 | 145 | bloomFolder.add(this.bloomParams.luminance, "filter").onChange((value) => { 146 | this.bloomPass.mipmapBlurPass.enabled = value; 147 | } ); 148 | 149 | bloomFolder.add(this.bloomParams, "intensity", 0.0, 200.0, 0.01).onChange( (value) => { 150 | this.bloomPass.intensity = value; 151 | }); 152 | 153 | bloomFolder.add(this.bloomParams, "radius", 0.0, 1.0, 0.001).onChange((value) => { 154 | this.bloomPass.mipmapBlurPass.radius = Number(value); 155 | }); 156 | 157 | 158 | bloomFolder.add(this.bloomParams.luminance, "threshold", 0.0, 1.0, 0.001) 159 | .onChange((value) => { 160 | this.bloomPass.luminanceMaterial.threshold = Number(value); 161 | }); 162 | 163 | bloomFolder.add(this.bloomParams.luminance, "smoothing", 0.0, 1.0, 0.001) 164 | .onChange((value) => { 165 | this.bloomPass.luminanceMaterial.smoothing = Number(value); 166 | }); 167 | 168 | 169 | 170 | 171 | 172 | 173 | // const bokehFolder = PostProcessFolder.addFolder( 'Bokeh' ); 174 | // 175 | // bokehFolder.add(this.params, "resolution", [240, 360, 480, 720, 1080]).onChange((value) => { 176 | // 177 | // this.depthOfFieldEffect.resolution.height = Number(value); 178 | // 179 | // }); 180 | // 181 | // bokehFolder.add(this.params, "bokeh scale", 1.0, 5.0, 0.001).onChange((value) => { 182 | // 183 | // this.depthOfFieldEffect.bokehScale = value; 184 | // 185 | // }); 186 | // 187 | // bokehFolder.add(this.params.coc, "edge blur kernel", KernelSize).onChange((value) => { 188 | // 189 | // this.depthOfFieldEffect.blurPass.kernelSize = Number(value); 190 | // 191 | // }); 192 | // 193 | // bokehFolder.add(this.params.coc, "focus", 0.0, 0.1, 0.001).onChange((value) => { 194 | // 195 | // this.cocMaterial.uniforms.focusDistance.value = value; 196 | // 197 | // }); 198 | // 199 | // bokehFolder.add(this.params.coc, "focal length", 0.0, 0.3, 0.0001) 200 | // .onChange((value) => { 201 | // 202 | // this.cocMaterial.uniforms.focalLength.value = value; 203 | // 204 | // }); 205 | 206 | // const matChanger = ( ) => { 207 | // 208 | // this.bokehPass.uniforms[ 'focus' ].value = this.state.bokeh.focus; 209 | // this.bokehPass.uniforms[ 'aperture' ].value = this.state.bokeh.aperture * 0.00001; 210 | // this.bokehPass.uniforms[ 'maxblur' ].value = this.state.bokeh.maxblur; 211 | // 212 | // }; 213 | // 214 | // bokehFolder.add( this.state.bokeh, 'enabled' ).name( 'Enabled' ) 215 | // .onChange( () => { 216 | // this.bokehPass.enabled = this.state.bokeh.enabled 217 | // } ); 218 | // 219 | // bokehFolder.add( this.state.bokeh, 'focus', 0, 300 ).step( 0.001 ) 220 | // .onChange( matChanger ); 221 | // 222 | // bokehFolder.add( this.state.bokeh, 'aperture', 0, 10 ).step( 0.1 ) 223 | // .onChange( matChanger ); 224 | // 225 | // bokehFolder.add( this.state.bokeh, 'maxblur', 0, 0.09 ).step( 0.001 ) 226 | // .onChange( matChanger ); 227 | 228 | 229 | // const motionFolder = PostProcessFolder.addFolder( 'Motion Blur' ); 230 | // motionFolder.add( this.state.motionBlur, 'enabled' ) 231 | // .onChange( () => { 232 | // this.motionBlurPass.enabled = this.state.motionBlur.enabled 233 | // } ); 234 | // motionFolder.add( this.state.motionBlur, 'cameraBlur' ) 235 | // .onChange( () => { 236 | // this.motionBlurPass.renderCameraBlur = this.state.motionBlur.cameraBlur 237 | // } ); 238 | // motionFolder.add( this.state.motionBlur, 'samples', 0, 50 ).step( 1 ) 239 | // .onChange( () => { 240 | // this.motionBlurPass.samples = this.state.motionBlur.samples 241 | // } ); 242 | // motionFolder.add( this.state.motionBlur, 'jitter', 0, 5 ).step( 0.01 ) 243 | // .onChange( () => { 244 | // this.motionBlurPass.jitter = this.state.motionBlur.jitter 245 | // } ); 246 | // motionFolder.add( this.state.motionBlur, 'jitterStrategy', { 247 | // REGULAR_JITTER: MotionBlurPass.REGULAR_JITTER, 248 | // RANDOM_JITTER: MotionBlurPass.RANDOM_JITTER, 249 | // BLUENOISE_JITTER: MotionBlurPass.BLUENOISE_JITTER, 250 | // } ) 251 | // .onChange( () => { 252 | // this.motionBlurPass.jitterStrategy = this.state.motionBlur.jitterStrategy 253 | // } ); 254 | // motionFolder.add( this.state.motionBlur, 'smearIntensity', 0, 10 ) 255 | // .onChange( () => { 256 | // this.motionBlurPass.smearIntensity = this.state.motionBlur.smearIntensity 257 | // } ); 258 | // motionFolder.add( this.state.motionBlur, 'expandGeometry', 0, 1 ) 259 | // .onChange( () => { 260 | // this.motionBlurPass.expandGeometry = this.state.motionBlur.expandGeometry 261 | // } ); 262 | // motionFolder.add( this.state.motionBlur, 'interpolateGeometry', 0, 1 ) 263 | // .onChange( () => { 264 | // this.motionBlurPass.interpolateGeometry = this.state.motionBlur.interpolateGeometry 265 | // } ); 266 | // motionFolder.add( this.state.motionBlur, 'renderTargetScale', 0, 1 ) 267 | // .onChange( v => { 268 | // MotionBlurPass.renderTargetScale = v; 269 | // window.resizeTo(); 270 | // } ); 271 | // 272 | // motionFolder.add( this.motionBlurPass.debug, 'display', { 273 | // 'Motion Blur': MotionBlurPass.DEFAULT, 274 | // 'Velocity': MotionBlurPass.VELOCITY, 275 | // 'Geometry': MotionBlurPass.GEOMETRY 276 | // } ).onChange( val => this.motionBlurPass.debug.display = parseFloat(val) ); 277 | // motionFolder.close() 278 | } 279 | } 280 | 281 | bloomRender() { 282 | if ( this.unrealBloomPass.enabled ) { 283 | this.scene.traverse( this.materials._darkenNonBloomed ) 284 | this.bloomComposer.render() 285 | this.scene.traverse( this.materials._restoreMaterial ) 286 | } 287 | 288 | } 289 | 290 | productionRender() { 291 | if ( this.state.postprocessing ) { 292 | this.composer.render() 293 | } else { 294 | this.renderer.render( this.scene, this.camera ) 295 | } 296 | } 297 | 298 | debugRender() { 299 | if ( this.state.postprocessing ) { 300 | //this.bloomRender() 301 | 302 | this.renderer.autoClear = false 303 | this.composer.render() 304 | this.renderer.clearDepth() 305 | } else { 306 | this.renderer.autoClear = false 307 | this.renderer.clearColor( this.rendererClass.clearColor ) 308 | this.renderer.render( this.scene, this.camera ) 309 | this.renderer.clearDepth() 310 | } 311 | } 312 | 313 | update( deltaTime ) { 314 | if ( this.debug.active ) { 315 | this.debugRender() 316 | } else { 317 | this.productionRender() 318 | } 319 | 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /src/Experience/Utils/Resources.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' 3 | import { OBJLoader } from "three/addons/loaders/OBJLoader.js"; 4 | import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js' 5 | import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' 6 | import EventEmitter from './EventEmitter.js' 7 | 8 | export default class Resources extends EventEmitter 9 | { 10 | constructor(sources) 11 | { 12 | super() 13 | 14 | this.sources = sources 15 | 16 | this.items = {} 17 | this.toLoad = this.sources.length 18 | this.loaded = 0 19 | this.loadedAll = false 20 | 21 | this.setLoaders() 22 | this.startLoading() 23 | } 24 | 25 | setLoaders() 26 | { 27 | this.loaders = {} 28 | this.loaders.gltfLoader = new GLTFLoader() 29 | this.loaders.objLoader = new OBJLoader() 30 | this.loaders.textureLoader = new THREE.TextureLoader() 31 | this.loaders.cubeTextureLoader = new THREE.CubeTextureLoader() 32 | this.loaders.RGBELoader = new RGBELoader() 33 | this.loaders.fontLoader = new FontLoader() 34 | this.loaders.AudioLoader = new THREE.AudioLoader() 35 | } 36 | 37 | startLoading() 38 | { 39 | // Load each source 40 | for(const source of this.sources) 41 | { 42 | if(source.type === 'gltfModel') 43 | { 44 | this.loaders.gltfLoader.load( 45 | source.path, 46 | (file) => 47 | { 48 | this.sourceLoaded(source, file) 49 | } 50 | ) 51 | } 52 | else if(source.type === 'objModel') 53 | { 54 | this.loaders.objLoader.load( 55 | source.path, 56 | (file) => 57 | { 58 | this.sourceLoaded(source, file) 59 | } 60 | ) 61 | } 62 | else if(source.type === 'texture') 63 | { 64 | this.loaders.textureLoader.load( 65 | source.path, 66 | (file) => 67 | { 68 | this.sourceLoaded(source, file) 69 | } 70 | ) 71 | } 72 | else if ( source.type === 'videoTexture' ) { 73 | let videoElement = document.createElement( 'video' ) 74 | videoElement.src = source.path 75 | videoElement.setAttribute( 'crossorigin', 'anonymous' ) 76 | videoElement.muted = true 77 | videoElement.loop = true 78 | videoElement.load() 79 | videoElement.setAttribute( 'playsinline', '' ) 80 | videoElement.setAttribute( 'webkit-playsinline', '' ) 81 | videoElement.play() 82 | 83 | const obj = { 84 | videoTexture : new THREE.VideoTexture( videoElement ), 85 | videoElement : videoElement 86 | } 87 | 88 | videoElement.addEventListener( 'canplaythrough', () => { 89 | this.sourceLoaded( source, obj ) 90 | } ) 91 | } 92 | else if(source.type === 'cubeTexture') 93 | { 94 | this.loaders.cubeTextureLoader.load( 95 | source.path, 96 | (file) => 97 | { 98 | this.sourceLoaded(source, file) 99 | } 100 | ) 101 | } 102 | else if(source.type === 'rgbeTexture') 103 | { 104 | this.loaders.RGBELoader.load( 105 | source.path, 106 | (file) => 107 | { 108 | this.sourceLoaded(source, file) 109 | } 110 | ) 111 | } 112 | else if(source.type === 'font') 113 | { 114 | this.loaders.fontLoader.load( 115 | source.path, 116 | (file) => 117 | { 118 | this.sourceLoaded(source, file) 119 | } 120 | ) 121 | } 122 | else if(source.type === 'audio') 123 | { 124 | this.loaders.AudioLoader.load( 125 | source.path, 126 | (file) => 127 | { 128 | this.sourceLoaded(source, file) 129 | } 130 | ) 131 | } 132 | } 133 | 134 | if(this.sources.length === 0) { 135 | setTimeout(() => { 136 | this.loadedAll = true 137 | this.trigger('ready') 138 | }); 139 | } 140 | } 141 | 142 | sourceLoaded(source, file) 143 | { 144 | this.items[source.name] = file 145 | 146 | this.loaded++ 147 | 148 | if(this.loaded === this.toLoad) 149 | { 150 | this.loadedAll = true 151 | this.trigger('ready') 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Experience/Utils/Sizes.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter.js' 2 | 3 | export default class Sizes extends EventEmitter { 4 | 5 | static _instance = null 6 | 7 | static getInstance() { 8 | return Sizes._instance || new Sizes() 9 | } 10 | 11 | constructor() { 12 | // Singleton 13 | if ( Sizes._instance ) { 14 | return Sizes._instance 15 | } 16 | 17 | super() 18 | 19 | Sizes._instance = this 20 | 21 | // Setup 22 | this.width = window.innerWidth 23 | this.height = window.innerHeight 24 | this.pixelRatio = Math.min( window.devicePixelRatio, 2 ) 25 | 26 | // Resize event 27 | window.addEventListener( 'resize', () => { 28 | this.width = window.innerWidth 29 | this.height = window.innerHeight 30 | this.pixelRatio = Math.min( window.devicePixelRatio, 2 ) 31 | 32 | this.trigger( 'resize' ) 33 | } ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Experience/Utils/Sound.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import EventEmitter from './EventEmitter.js' 3 | import Experience from '../Experience.js' 4 | import Debug from './Debug.js' 5 | import Sizes from './Sizes.js' 6 | 7 | export default class Sound extends EventEmitter 8 | { 9 | constructor() 10 | { 11 | super() 12 | 13 | this.experience = Experience.getInstance() 14 | this.camera = this.experience.camera.instance 15 | this.resources = this.experience.resources 16 | this.renderer = this.experience.renderer.instance 17 | this.debug = Debug.getInstance() 18 | this.sizes = Sizes.getInstance() 19 | 20 | this.soundsCreated = false; 21 | 22 | } 23 | 24 | isTabVisible() { 25 | return document.visibilityState === "visible"; 26 | } 27 | 28 | handleVisibilityChange() { 29 | if (this.isTabVisible()) { 30 | this.backgroundSound.play(); 31 | this.listener.setMasterVolume(1) 32 | } else { 33 | this.backgroundSound.pause(); 34 | this.listener.setMasterVolume(0) 35 | } 36 | } 37 | 38 | createSounds() { 39 | if ( this.soundsCreated === true ) 40 | return 41 | 42 | this.listener = new THREE.AudioListener(); 43 | this.camera.add( this.listener ); 44 | 45 | this.backgroundSound = new THREE.Audio( this.listener ); 46 | this.backgroundSound.setBuffer( this.resources.items.backgroundSound ); 47 | this.backgroundSound.setLoop( true ); 48 | this.backgroundSound.setVolume( 0.8 ); 49 | this.backgroundSound.play(); 50 | 51 | 52 | this.soundsCreated = true; 53 | 54 | document.addEventListener('visibilitychange', () => this.handleVisibilityChange(), false); 55 | 56 | // window.addEventListener('blur', () => this.backgroundSound.pause()); 57 | // window.addEventListener('focus', () => { 58 | // if (isTabVisible()) { 59 | // this.backgroundSound.play(); 60 | // } 61 | // }); 62 | 63 | } 64 | 65 | update() { 66 | 67 | } 68 | 69 | resize() { 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Experience/Utils/Time.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter.js' 2 | import gsap from "gsap"; 3 | 4 | export default class Time extends EventEmitter { 5 | static _instance = null 6 | 7 | static getInstance() { 8 | return Time._instance || new Time() 9 | } 10 | 11 | constructor() { 12 | if ( Time._instance ) { 13 | return Time._instance 14 | } 15 | 16 | super() 17 | 18 | Time._instance = this 19 | 20 | // Setup 21 | this.start = Date.now() 22 | this.current = this.start 23 | this.playing = true 24 | this.elapsed = 0 25 | this.delta = 0.016666666666666668 26 | this.timeline = gsap.timeline( { 27 | paused: true, 28 | } ); 29 | 30 | window.requestAnimationFrame( () => { 31 | this.tick() 32 | } ) 33 | } 34 | 35 | tick() { 36 | const currentTime = Date.now() 37 | this.delta = Math.min( ( currentTime - this.current ) * 0.001, 0.016 ) 38 | this.current = currentTime 39 | this.elapsed = ( this.current - this.start ) * 0.001 40 | 41 | if ( this.delta > 0.06 ) { 42 | this.delta = 0.06 43 | } 44 | 45 | this.timeline.time( this.elapsed ); 46 | 47 | this.trigger( 'tick' ) 48 | 49 | window.requestAnimationFrame( () => { 50 | this.tick() 51 | } ) 52 | } 53 | 54 | reset() { 55 | this.start = Date.now() 56 | this.current = this.start 57 | this.elapsed = 0 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Experience/Utils/blue-noise-generation/README.md: -------------------------------------------------------------------------------- 1 | # Blue Noise Generation 2 | 3 |

4 | 5 |

6 | 7 | _

1, 2, and 3 channel blue noise

_ 8 | 9 | Class for generating blue noise textures and example for generating monochrome or RGB variations. 10 | 11 | [Demo Here](https://gkjohnson.github.io/threejs-sandbox/blue-noise-generation/). 12 | 13 | ## TODO 14 | 15 | - Make a 3D blue noise generator. 16 | - Investigate Hilbert indices in shader (https://www.shadertoy.com/view/3tB3z3) 17 | 18 | ## References 19 | 20 | - http://cv.ulichney.com/papers/1993-void-cluster.pdf 21 | - http://momentsingraphics.de/BlueNoise.html (includes sample textures) 22 | - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ 23 | - https://gist.github.com/pixelmager/5d25fa32987273b9608a2d2c6cc74bfa 24 | - https://github.com/Atrix256/SampleZoo 25 | -------------------------------------------------------------------------------- /src/Experience/Utils/blue-noise-generation/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/src/Experience/Utils/blue-noise-generation/images/banner.png -------------------------------------------------------------------------------- /src/Experience/Utils/blue-noise-generation/src/BlueNoiseGenerator.js: -------------------------------------------------------------------------------- 1 | import { shuffleArray, fillWithOnes } from './utils.js'; 2 | import { BlueNoiseSamples } from './BlueNoiseSamples.js'; 3 | 4 | export class BlueNoiseGenerator { 5 | 6 | constructor() { 7 | 8 | this.random = Math.random; 9 | this.sigma = 1.5; 10 | this.size = 64; 11 | this.majorityPointsRatio = 0.1; 12 | 13 | this.samples = new BlueNoiseSamples( 1 ); 14 | this.savedSamples = new BlueNoiseSamples( 1 ); 15 | } 16 | 17 | generate() { 18 | 19 | // http://cv.ulichney.com/papers/1993-void-cluster.pdf 20 | 21 | const { 22 | samples, 23 | savedSamples, 24 | sigma, 25 | majorityPointsRatio, 26 | size, 27 | } = this; 28 | 29 | samples.resize( size ); 30 | samples.setSigma( sigma ); 31 | 32 | // 1. Randomly place the minority points. 33 | const pointCount = Math.floor( size * size * majorityPointsRatio ); 34 | const initialSamples = samples.binaryPattern; 35 | 36 | //console.time( 'Array Initialization' ); 37 | fillWithOnes( initialSamples, pointCount ); 38 | shuffleArray( initialSamples, this.random ); 39 | //console.timeEnd( 'Array Initialization' ); 40 | 41 | //console.time( 'Score Initialization' ); 42 | for ( let i = 0, l = initialSamples.length; i < l; i ++ ) { 43 | 44 | if ( initialSamples[ i ] === 1 ) { 45 | 46 | samples.addPointIndex( i ); 47 | 48 | } 49 | 50 | } 51 | //console.timeEnd( 'Score Initialization' ); 52 | 53 | // 2. Remove minority point that is in densest cluster and place it in the largest void. 54 | //console.time( 'Point Rearrangement' ); 55 | while ( true ) { 56 | 57 | const clusterIndex = samples.findCluster(); 58 | samples.removePointIndex( clusterIndex ); 59 | 60 | const voidIndex = samples.findVoid(); 61 | if ( clusterIndex === voidIndex ) { 62 | 63 | samples.addPointIndex( clusterIndex ); 64 | break; 65 | 66 | } 67 | 68 | samples.addPointIndex( voidIndex ); 69 | 70 | } 71 | //console.timeEnd( 'Point Rearrangement' ); 72 | 73 | 74 | // 3. PHASE I: Assign a rank to each progressively less dense cluster point and put it 75 | // in the dither array. 76 | const ditherArray = new Uint32Array( size * size ); 77 | savedSamples.copy( samples ); 78 | 79 | //console.time( 'Dither Array Phase 1' ); 80 | let rank; 81 | rank = samples.count - 1; 82 | while ( rank >= 0 ) { 83 | 84 | const clusterIndex = samples.findCluster(); 85 | samples.removePointIndex( clusterIndex ); 86 | 87 | ditherArray[ clusterIndex ] = rank; 88 | rank --; 89 | 90 | } 91 | //console.timeEnd( 'Dither Array Phase 1' ); 92 | 93 | // 4. PHASE II: Do the same thing for the largest voids up to half of the total pixels using 94 | // the initial binary pattern. 95 | //console.time( 'Dither Array Phase 2' ); 96 | const totalSize = size * size; 97 | rank = savedSamples.count; 98 | while ( rank < totalSize / 2 ) { 99 | 100 | const voidIndex = savedSamples.findVoid(); 101 | savedSamples.addPointIndex( voidIndex ); 102 | ditherArray[ voidIndex ] = rank; 103 | rank ++; 104 | 105 | } 106 | //console.timeEnd( 'Dither Array Phase 2' ); 107 | 108 | // 5. PHASE III: Invert the pattern and finish out by assigning a rank to the remaining 109 | // and iteratively removing them. 110 | //console.time( 'Samples Invert' ); 111 | savedSamples.invert(); 112 | //console.timeEnd( 'Samples Invert' ); 113 | 114 | //console.time( 'Dither Array Phase 3' ); 115 | while ( rank < totalSize ) { 116 | 117 | const clusterIndex = savedSamples.findCluster(); 118 | savedSamples.removePointIndex( clusterIndex ); 119 | ditherArray[ clusterIndex ] = rank; 120 | rank ++; 121 | 122 | } 123 | //console.timeEnd( 'Dither Array Phase 3' ); 124 | 125 | return { data: ditherArray, maxValue: totalSize }; 126 | 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/Experience/Utils/blue-noise-generation/src/BlueNoiseSamples.js: -------------------------------------------------------------------------------- 1 | export class BlueNoiseSamples { 2 | 3 | constructor( size ) { 4 | 5 | this.count = 0; 6 | this.size = - 1; 7 | this.sigma = - 1; 8 | this.radius = - 1; 9 | this.lookupTable = null; 10 | this.score = null; 11 | this.binaryPattern = null; 12 | 13 | this.resize( size ); 14 | this.setSigma( 1.5 ); 15 | 16 | } 17 | 18 | findVoid() { 19 | 20 | const { score, binaryPattern } = this; 21 | 22 | let currValue = Infinity; 23 | let currIndex = - 1; 24 | for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) { 25 | 26 | if ( binaryPattern[ i ] !== 0 ) { 27 | 28 | continue; 29 | 30 | } 31 | 32 | const pScore = score[ i ]; 33 | if ( pScore < currValue ) { 34 | 35 | currValue = pScore; 36 | currIndex = i; 37 | 38 | } 39 | 40 | } 41 | 42 | return currIndex; 43 | 44 | } 45 | 46 | findCluster() { 47 | 48 | const { score, binaryPattern } = this; 49 | 50 | let currValue = - Infinity; 51 | let currIndex = - 1; 52 | for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) { 53 | 54 | if ( binaryPattern[ i ] !== 1 ) { 55 | 56 | continue; 57 | 58 | } 59 | 60 | const pScore = score[ i ]; 61 | if ( pScore > currValue ) { 62 | 63 | currValue = pScore; 64 | currIndex = i; 65 | 66 | } 67 | 68 | } 69 | 70 | return currIndex; 71 | 72 | } 73 | 74 | setSigma( sigma ) { 75 | 76 | if ( sigma === this.sigma ) { 77 | 78 | return; 79 | 80 | } 81 | 82 | // generate a radius in which the score will be updated under the 83 | // assumption that e^-10 is insignificant enough to be the border at 84 | // which we drop off. 85 | const radius = ~ ~ ( Math.sqrt( 10 * 2 * ( sigma ** 2 ) ) + 1 ); 86 | const lookupWidth = 2 * radius + 1; 87 | const lookupTable = new Float32Array( lookupWidth * lookupWidth ); 88 | const sigma2 = sigma * sigma; 89 | for ( let x = - radius; x <= radius; x ++ ) { 90 | 91 | for ( let y = - radius; y <= radius; y ++ ) { 92 | 93 | const index = ( radius + y ) * lookupWidth + x + radius; 94 | const dist2 = x * x + y * y; 95 | lookupTable[ index ] = Math.E ** ( - dist2 / ( 2 * sigma2 ) ); 96 | 97 | } 98 | 99 | } 100 | 101 | this.lookupTable = lookupTable; 102 | this.sigma = sigma; 103 | this.radius = radius; 104 | 105 | } 106 | 107 | resize( size ) { 108 | 109 | if ( this.size !== size ) { 110 | 111 | this.size = size; 112 | this.score = new Float32Array( size * size ); 113 | this.binaryPattern = new Uint8Array( size * size ); 114 | 115 | } 116 | 117 | 118 | } 119 | 120 | invert() { 121 | 122 | const { binaryPattern, score, size } = this; 123 | 124 | score.fill( 0 ); 125 | 126 | for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) { 127 | 128 | if ( binaryPattern[ i ] === 0 ) { 129 | 130 | const y = ~ ~ ( i / size ); 131 | const x = i - y * size; 132 | this.updateScore( x, y, 1 ); 133 | binaryPattern[ i ] = 1; 134 | 135 | } else { 136 | 137 | binaryPattern[ i ] = 0; 138 | 139 | } 140 | 141 | } 142 | 143 | } 144 | 145 | updateScore( x, y, multiplier ) { 146 | 147 | // TODO: Is there a way to keep track of the highest and lowest scores here to avoid have to search over 148 | // everything in the buffer? 149 | const { size, score, lookupTable } = this; 150 | 151 | // const sigma2 = sigma * sigma; 152 | // const radius = Math.floor( size / 2 ); 153 | const radius = this.radius; 154 | const lookupWidth = 2 * radius + 1; 155 | for ( let px = - radius; px <= radius; px ++ ) { 156 | 157 | for ( let py = - radius; py <= radius; py ++ ) { 158 | 159 | // const dist2 = px * px + py * py; 160 | // const value = Math.E ** ( - dist2 / ( 2 * sigma2 ) ); 161 | 162 | const lookupIndex = ( radius + py ) * lookupWidth + px + radius; 163 | const value = lookupTable[ lookupIndex ]; 164 | 165 | let sx = ( x + px ); 166 | sx = sx < 0 ? size + sx : sx % size; 167 | 168 | let sy = ( y + py ); 169 | sy = sy < 0 ? size + sy : sy % size; 170 | 171 | const sindex = sy * size + sx; 172 | score[ sindex ] += multiplier * value; 173 | 174 | } 175 | 176 | } 177 | 178 | } 179 | 180 | addPointIndex( index ) { 181 | 182 | this.binaryPattern[ index ] = 1; 183 | 184 | const size = this.size; 185 | const y = ~ ~ ( index / size ); 186 | const x = index - y * size; 187 | this.updateScore( x, y, 1 ); 188 | this.count ++; 189 | 190 | } 191 | 192 | removePointIndex( index ) { 193 | 194 | this.binaryPattern[ index ] = 0; 195 | 196 | const size = this.size; 197 | const y = ~ ~ ( index / size ); 198 | const x = index - y * size; 199 | this.updateScore( x, y, - 1 ); 200 | this.count --; 201 | 202 | } 203 | 204 | copy( source ) { 205 | 206 | this.resize( source.size ); 207 | this.score.set( source.score ); 208 | this.binaryPattern.set( source.binaryPattern ); 209 | this.setSigma( source.sigma ); 210 | this.count = source.count; 211 | 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /src/Experience/Utils/blue-noise-generation/src/utils.js: -------------------------------------------------------------------------------- 1 | export function shuffleArray( array, random = Math.random ) { 2 | 3 | for ( let i = array.length - 1; i > 0; i -- ) { 4 | 5 | const replaceIndex = ~ ~ ( ( random() - 1e-6 ) * i ); 6 | const tmp = array[ i ]; 7 | array[ i ] = array[ replaceIndex ]; 8 | array[ replaceIndex ] = tmp; 9 | 10 | } 11 | 12 | } 13 | 14 | export function fillWithOnes( array, count ) { 15 | 16 | array.fill( 0 ); 17 | 18 | for ( let i = 0; i < count; i ++ ) { 19 | 20 | array[ i ] = 1; 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Shader Replacement 3 | 4 | ![](./docs/image.png) 5 | 6 | Utility for rendering a scene with a replacement shader. Similar to `Scene.overrideMaterial` but the uniforms of the original material uniforms are maintained. 7 | 8 | [Demo Here](https://gkjohnson.github.io/threejs-sandbox/shader-replacement/) 9 | 10 | # Use 11 | 12 | ```js 13 | import { ShaderReplacement } from './src/ShaderReplacement.js'; 14 | 15 | // ... 16 | 17 | const shader = ShaderLib.normal; 18 | shader.defines.USE_NORMALMAP = ''; 19 | shader.defines.TANGENTSPACE_NORMALMAP = ''; 20 | shader.defines.USE_UV = ''; 21 | 22 | const shaderReplacement = new ShaderReplacement( ShaderLib.normal ); 23 | shaderReplacement.replace( scene, true ); 24 | renderer.render( scene, camera ); 25 | shaderReplacement.reset( scene, true ); 26 | ``` 27 | 28 | Or with per-material logic 29 | 30 | ```js 31 | // ... 32 | const shader = ShaderLib.normal; 33 | const shaderReplacement = new ShaderReplacement( ShaderLib.normal ); 34 | shaderReplacement.updateUniforms = function( object, material, target ) { 35 | 36 | super.updateUniforms( object, material, target ); 37 | if ( ! target.uniforms.map.value ) { 38 | 39 | if ( 'USE_NORMALMAP' in target.defines ) { 40 | 41 | delete target.defines.USE_NORMALMAP; 42 | target.needsUpdate = true; 43 | 44 | } 45 | 46 | } else { 47 | 48 | if ( ! ( 'USE_NORMALMAP' in target.defines.USE_NORMALMAP ) ) { 49 | 50 | target.defines.USE_NORMALMAP = ''; 51 | target.needsUpdate = true; 52 | 53 | } 54 | 55 | } 56 | 57 | }; 58 | 59 | // ... 60 | 61 | shaderReplacement.replace( scene, true ); 62 | renderer.render( scene, camera ); 63 | shaderReplacement.reset( scene, true ); 64 | ``` 65 | 66 | # API 67 | 68 | ## ShaderReplacement 69 | 70 | ### .constructor 71 | 72 | ```js 73 | constructor( shader : Shader ) 74 | ``` 75 | 76 | Takes the shader to use on all materials when rendering the scene. 77 | 78 | ### .replace 79 | 80 | ```js 81 | replace( 82 | scene : Scene | Array< Object3D >, 83 | recursive = false : Boolean, 84 | cacheMaterial = true : Boolean 85 | ) : void 86 | ``` 87 | 88 | Replaces the material on all objects in the given scene. Recurses to all children if `recursive` is true. If `cacheMaterial` is true then all current materials are saved so they can be replaced on [reset](#reset). 89 | 90 | ### .reset 91 | 92 | ```js 93 | reset( 94 | scene : Scene | Array< Object3D >, 95 | recursive = false : Boolean 96 | ) : void 97 | ``` 98 | 99 | Resets all materials to the originally cached one. Recurses to all children if `recursive` is true. 100 | 101 | ### .dispose 102 | 103 | ```js 104 | dispose() : void 105 | ``` 106 | 107 | Disposes of all materials created by the object. 108 | 109 | _NOTE: Unimplemented for the moment._ 110 | 111 | ### updateUniforms 112 | 113 | _overrideable_ 114 | 115 | ```js 116 | updateUniforms( object : Object3D, material : Material, target : ShaderMaterial ) : void 117 | ``` 118 | 119 | Overrideable function intended to update the uniforms on the `target` material to match those on `material` for the given `object`. This can be overriden if special defines need to be set for a given material such as `USE_NORMALMAP` for a normal shader. 120 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/docs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/src/Experience/Utils/shader-replacement/docs/image.png -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/exampleShaders.js: -------------------------------------------------------------------------------- 1 | import { ShaderLib } from '//cdn.skypack.dev/three@0.130.1/build/three.module.js'; 2 | 3 | export const uvShader = { 4 | defines: { 5 | USE_UV: '' 6 | }, 7 | uniforms: ShaderLib.standard.uniforms, 8 | vertexShader: ShaderLib.normal.vertexShader, 9 | fragmentShader: 10 | ` 11 | varying vec2 vUv; 12 | void main() { 13 | gl_FragColor = vec4( vUv, 0.0, 1.0 ); 14 | } 15 | `, 16 | }; 17 | 18 | export const roughnessShader = { 19 | defines: { 20 | USE_UV: '', 21 | USE_ROUGHNESSMAP: '' 22 | }, 23 | uniforms: ShaderLib.standard.uniforms, 24 | vertexShader: ShaderLib.normal.vertexShader, 25 | fragmentShader: 26 | ` 27 | varying vec2 vUv; 28 | uniform float roughness; 29 | #include 30 | void main() { 31 | #include 32 | gl_FragColor = vec4( roughnessFactor ); 33 | } 34 | `, 35 | }; 36 | 37 | export const metalnessShader = { 38 | defines: { 39 | USE_UV: '', 40 | USE_ROUGHNESSMAP: '' 41 | }, 42 | uniforms: ShaderLib.standard.uniforms, 43 | vertexShader: ShaderLib.normal.vertexShader, 44 | fragmentShader: 45 | ` 46 | varying vec2 vUv; 47 | uniform float metalness; 48 | #include 49 | void main() { 50 | #include 51 | gl_FragColor = vec4( metalnessFactor ); 52 | } 53 | `, 54 | }; 55 | 56 | export const albedoShader = { 57 | defines: { 58 | USE_UV: '', 59 | USE_MAP: '' 60 | }, 61 | uniforms: ShaderLib.standard.uniforms, 62 | vertexShader: ShaderLib.normal.vertexShader, 63 | fragmentShader: 64 | ` 65 | varying vec2 vUv; 66 | uniform float metalness; 67 | uniform vec3 diffuse; 68 | uniform float opacity; 69 | #include 70 | void main() { 71 | vec4 diffuseColor = vec4( diffuse, opacity ); 72 | #include 73 | gl_FragColor = vec4( diffuseColor ); 74 | } 75 | `, 76 | }; 77 | 78 | export const opacityShader = { 79 | defines: { 80 | USE_UV: '', 81 | USE_MAP: '' 82 | }, 83 | uniforms: ShaderLib.standard.uniforms, 84 | vertexShader: ShaderLib.normal.vertexShader, 85 | fragmentShader: 86 | ` 87 | varying vec2 vUv; 88 | uniform vec3 diffuse; 89 | uniform float opacity; 90 | #include 91 | void main() { 92 | vec4 diffuseColor = vec4( diffuse, opacity ); 93 | #include 94 | gl_FragColor = vec4( diffuseColor.a ); 95 | } 96 | `, 97 | }; 98 | 99 | export const emissiveShader = { 100 | defines: { 101 | USE_UV: '', 102 | USE_EMISSIVEMAP: '' 103 | }, 104 | uniforms: ShaderLib.standard.uniforms, 105 | vertexShader: ShaderLib.normal.vertexShader, 106 | fragmentShader: 107 | ` 108 | varying vec2 vUv; 109 | uniform vec3 emissive; 110 | uniform float opacity; 111 | #include 112 | void main() { 113 | vec3 totalEmissiveRadiance = emissive; 114 | #include 115 | gl_FragColor = vec4( totalEmissiveRadiance, 1.0 ); 116 | } 117 | `, 118 | }; 119 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | threejs webgl - shader replacement 5 | 6 | 7 | 29 | 30 | 31 | 32 |
33 | three.js - Shader Replacement by Garrett Johnson 34 | 35 |
36 | 37 | Replace the materials used on all materials in the scene with new materials that 38 | 39 |
40 | 41 | have copied uniforms and defines from the original materials. 42 |
43 | 44 | 453 | 454 | 455 | 456 | 457 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/ExtendedShaderMaterial.js: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three'; 2 | export class ExtendedShaderMaterial extends ShaderMaterial { 3 | 4 | constructor( ...args ) { 5 | 6 | super( ...args ); 7 | [ 8 | 'opacity', 9 | 'map', 10 | 'emissiveMap', 11 | 'roughnessMap', 12 | 'metalnessMap', 13 | ].forEach( key => { 14 | 15 | Object.defineProperty( this, key, { 16 | 17 | get() { 18 | 19 | if ( ! ( key in this.uniforms ) ) return undefined; 20 | return this.uniforms[ key ].value; 21 | 22 | }, 23 | 24 | set( v ) { 25 | 26 | if ( ! ( key in this.uniforms ) ) return; 27 | this.uniforms[ key ].value = v; 28 | 29 | } 30 | 31 | } ); 32 | 33 | 34 | } ); 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/RendererState.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export class RendererState { 4 | 5 | constructor() { 6 | 7 | this.clearAlpha = 0; 8 | this.clearColor = new THREE.Color(); 9 | this.renderTarget = null; 10 | this.outputColorSpace = THREE.SRGBColorSpace 11 | this.overrideMaterial = null; 12 | this.shadowsEnabled = false; 13 | 14 | this.autoClear = true; 15 | this.autoClearColor = true; 16 | this.autoClearDepth = true; 17 | this.autoClearStencil = true; 18 | 19 | this.background = null; 20 | this.autoUpdate = true; 21 | 22 | } 23 | 24 | copy( renderer, scene ) { 25 | 26 | if ( renderer ) { 27 | 28 | this.clearAlpha = renderer.getClearAlpha(); 29 | this.clearColor = renderer.getClearColor( this.clearColor ); 30 | this.renderTarget = renderer.getRenderTarget(); 31 | 32 | this.shadowsEnabled = renderer.shadowMap.enabled; 33 | this.outputColorSpace = renderer.outputColorSpace; 34 | this.autoClear = renderer.autoClear; 35 | this.autoClearColor = renderer.autoClearColor; 36 | this.autoClearDepth = renderer.autoClearDepth; 37 | this.autoClearStencil = renderer.autoClearStencil; 38 | 39 | } 40 | 41 | if ( scene ) { 42 | 43 | this.overrideMaterial = scene.overrideMaterial; 44 | this.background = scene.background; 45 | this.autoUpdate = scene.autoUpdate; 46 | 47 | } 48 | 49 | } 50 | 51 | restore( renderer, scene ) { 52 | 53 | if ( renderer ) { 54 | 55 | renderer.setClearAlpha( this.clearAlpha ); 56 | renderer.setClearColor( this.clearColor ); 57 | renderer.setRenderTarget( this.renderTarget ); 58 | 59 | renderer.shadowMap.enabled = this.shadowsEnabled; 60 | renderer.outputColorSpace = this.outputColorSpace; 61 | renderer.autoClear = this.autoClear; 62 | renderer.autoClearColor = this.autoClearColor; 63 | renderer.autoClearDepth = this.autoClearDepth; 64 | renderer.autoClearStencil = this.autoClearStencil; 65 | 66 | } 67 | 68 | if ( scene ) { 69 | 70 | scene.overrideMaterial = this.overrideMaterial; 71 | scene.background = this.background; 72 | scene.autoUpdate = this.autoUpdate; 73 | 74 | } 75 | 76 | this.renderTarget = null; 77 | this.overrideMaterial = null; 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/ShaderReplacement.js: -------------------------------------------------------------------------------- 1 | import { WrappedShaderMaterial } from './WrappedShaderMaterial.js'; 2 | 3 | export function setMaterialDefine( material, define, value ) { 4 | 5 | if ( value === undefined ) { 6 | 7 | if ( define in material.defines ) { 8 | 9 | delete material.defines[ define ]; 10 | material.needsUpdate = true; 11 | 12 | } 13 | 14 | } else { 15 | 16 | if ( value !== material.defines[ define ] ) { 17 | 18 | material.defines[ define ] = value; 19 | material.needsUpdate = true; 20 | 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | const _originalMaterials = new WeakMap(); 28 | export class ShaderReplacement { 29 | 30 | constructor( shader ) { 31 | 32 | this._replacementMaterial = new WrappedShaderMaterial( shader ); 33 | this._replacementMaterials = new WeakMap(); 34 | 35 | this.overrideUniforms = {}; 36 | this.overrideDefines = {}; 37 | 38 | } 39 | 40 | replace( scene, recursive = false, cacheCurrentMaterial = true ) { 41 | 42 | const scope = this; 43 | function applyMaterial( object ) { 44 | 45 | if ( ! object.isMesh && ! object.isSkinnedMesh ) { 46 | 47 | return; 48 | 49 | } 50 | 51 | if ( ! replacementMaterials.has( object ) ) { 52 | 53 | const replacementMaterial = scope.createMaterial( object ); 54 | replacementMaterials.set( object, replacementMaterial ); 55 | 56 | } 57 | 58 | const replacementMaterial = replacementMaterials.get( object ); 59 | if ( replacementMaterial === null ) { 60 | 61 | return; 62 | 63 | } 64 | 65 | let originalMaterial = object.material; 66 | if ( cacheCurrentMaterial ) { 67 | 68 | originalMaterials.set( object, originalMaterial ); 69 | 70 | } else { 71 | 72 | originalMaterial = originalMaterials.get( object ); 73 | 74 | } 75 | 76 | if ( ! originalMaterial ) { 77 | 78 | console.error( 'ShaderReplacement : Material for object was not cached before replacing shader.', object ); 79 | 80 | } 81 | 82 | scope.updateUniforms( object, originalMaterial, replacementMaterial ); 83 | if ( replacementMaterial.finalize ) { 84 | 85 | replacementMaterial.finalize(); 86 | 87 | } 88 | 89 | object.material = replacementMaterial; 90 | 91 | } 92 | 93 | const replacementMaterials = this._replacementMaterials; 94 | const originalMaterials = _originalMaterials; 95 | if ( Array.isArray( scene ) ) { 96 | 97 | if ( recursive ) { 98 | 99 | for ( let i = 0, l = scene.length; i < l; i ++ ) { 100 | 101 | scene[ i ].traverse( applyMaterial ); 102 | 103 | } 104 | 105 | } else { 106 | 107 | for ( let i = 0, l = scene.length; i < l; i ++ ) { 108 | 109 | applyMaterial( scene[ i ] ); 110 | 111 | } 112 | 113 | } 114 | 115 | } else { 116 | 117 | if ( recursive ) { 118 | 119 | scene.traverse( applyMaterial ); 120 | 121 | } else { 122 | 123 | applyMaterial( scene ); 124 | 125 | } 126 | 127 | } 128 | 129 | } 130 | 131 | reset( scene, recursive ) { 132 | 133 | function resetMaterial( object ) { 134 | 135 | if ( originalMaterials.has( object ) ) { 136 | 137 | object.material = originalMaterials.get( object ); 138 | originalMaterials.delete( object ); 139 | 140 | } else if ( object.isSkinnedMesh || object.isMesh ) { 141 | 142 | console.error( 'ShaderReplacement : Material for object was not cached before resetting.', object ); 143 | 144 | } 145 | 146 | } 147 | 148 | const originalMaterials = _originalMaterials; 149 | if ( Array.isArray( scene ) ) { 150 | 151 | if ( recursive ) { 152 | 153 | for ( let i = 0, l = scene.length; i < l; i ++ ) { 154 | 155 | resetMaterial( scene[ i ] ); 156 | 157 | } 158 | 159 | } else { 160 | 161 | for ( let i = 0, l = scene.length; i < l; i ++ ) { 162 | 163 | scene[ i ].traverse( resetMaterial ); 164 | 165 | } 166 | 167 | } 168 | 169 | } else { 170 | 171 | if ( recursive ) { 172 | 173 | scene.traverse( resetMaterial ); 174 | 175 | } else { 176 | 177 | resetMaterial( scene ); 178 | 179 | } 180 | 181 | } 182 | 183 | } 184 | 185 | createMaterial( object ) { 186 | 187 | return this._replacementMaterial.clone(); 188 | 189 | } 190 | 191 | updateUniforms( object, material, target ) { 192 | 193 | const replacementMaterial = this._replacementMaterial; 194 | const originalDefines = replacementMaterial.defines; 195 | const materialDefines = material.defines; 196 | const targetDefines = target.defines; 197 | 198 | target.side = material.side; 199 | target.flatShading = material.flatShading; 200 | target.skinning = material.skinning; 201 | 202 | if ( materialDefines ) { 203 | 204 | for ( const key in materialDefines ) { 205 | 206 | target.setDefine( key, materialDefines[ key ] ); 207 | 208 | } 209 | 210 | for ( const key in targetDefines ) { 211 | 212 | if ( ! ( key in materialDefines ) ) { 213 | 214 | target.setDefine( key, originalDefines[ key ] ); 215 | 216 | } else { 217 | 218 | target.setDefine( key, materialDefines[ key ] ); 219 | 220 | } 221 | 222 | } 223 | 224 | } 225 | 226 | // NOTE: we shouldn't have to worry about using copy / equals on colors, vectors, or arrays here 227 | // because we promise not to change the values. 228 | const targetUniforms = target.uniforms; 229 | if ( material.isShaderMaterial ) { 230 | 231 | const materialUniforms = material.uniforms; 232 | for ( const key in targetUniforms ) { 233 | 234 | const materialUniform = materialUniforms[ key ]; 235 | const targetUniform = targetUniforms[ key ]; 236 | if ( materialUniform && materialUniform.value !== targetUniform.value ) { 237 | 238 | if ( 239 | targetUniform.value && targetUniform.value.isTexture || 240 | materialUniform.value && materialUniform.value.isTexture 241 | ) { 242 | 243 | target.setTextureUniform( key, materialUniform.value ); 244 | 245 | } else { 246 | 247 | targetUniform.value = materialUniform.value; 248 | 249 | } 250 | 251 | } 252 | 253 | } 254 | 255 | } else { 256 | 257 | for ( const key in targetUniforms ) { 258 | 259 | const targetUniform = targetUniforms[ key ]; 260 | if ( key in material && material[ key ] !== targetUniform.value ) { 261 | 262 | if ( 263 | targetUniform.value && targetUniform.value.isTexture || 264 | material[ key ] && material[ key ].isTexture 265 | ) { 266 | 267 | target.setTextureUniform( key, material[ key ] ); 268 | 269 | } else { 270 | 271 | targetUniform.value = material[ key ]; 272 | 273 | } 274 | 275 | } 276 | 277 | } 278 | 279 | } 280 | 281 | const { overrideDefines, overrideUniforms } = this; 282 | for ( const key in overrideDefines ) { 283 | 284 | if ( overrideDefines[ key ] === null || overrideDefines[ key ] === undefined ) { 285 | 286 | delete targetDefines[ key ]; 287 | 288 | } else { 289 | 290 | if ( targetDefines[ key ] !== overrideDefines[ key ] ) { 291 | 292 | targetDefines[ key ] = overrideDefines[ key ]; 293 | target.needsUpdate = true; 294 | 295 | } 296 | 297 | } 298 | 299 | } 300 | 301 | for ( const key in overrideUniforms ) { 302 | 303 | if ( key in targetUniforms ) { 304 | 305 | targetUniforms[ key ].value = overrideUniforms[ key ].value; 306 | 307 | } 308 | 309 | } 310 | 311 | } 312 | 313 | dispose() { 314 | 315 | // TODO: disposal needed? 316 | 317 | } 318 | 319 | } 320 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/WrappedShaderMaterial.js: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three'; 2 | import { ExtendedShaderMaterial } from './ExtendedShaderMaterial.js'; 3 | export class WrappedShaderMaterial extends ExtendedShaderMaterial { 4 | 5 | constructor( ...args ) { 6 | 7 | super( ...args ); 8 | this.modifiedDefines = {}; 9 | this.modifiedUniforms = {}; 10 | 11 | } 12 | 13 | // TODO: do the same for uniforms 14 | setDefine( name, value ) { 15 | 16 | const defines = this.defines; 17 | const modifiedDefines = this.modifiedDefines; 18 | 19 | const isDeleted = value === null || value === undefined; 20 | if ( isDeleted ) { 21 | 22 | if ( name in defines ) { 23 | 24 | if ( ! ( name in modifiedDefines ) ) { 25 | 26 | modifiedDefines[ name ] = defines[ name ]; 27 | 28 | } 29 | delete defines[ name ]; 30 | 31 | } 32 | 33 | } else if ( defines[ name ] !== value ) { 34 | 35 | if ( ! ( name in modifiedDefines ) ) { 36 | 37 | if ( name in defines ) { 38 | 39 | modifiedDefines[ name ] = defines[ name ]; 40 | 41 | } else { 42 | 43 | modifiedDefines[ name ] = undefined; 44 | 45 | } 46 | 47 | } 48 | defines[ name ] = value; 49 | 50 | } 51 | 52 | } 53 | 54 | setTextureUniform( name, value ) { 55 | 56 | const uniforms = this.uniforms; 57 | const modifiedUniforms = this.modifiedUniforms; 58 | 59 | if ( ! ( name in modifiedUniforms ) ) { 60 | 61 | modifiedUniforms[ name ] = uniforms[ name ].value; 62 | 63 | } 64 | uniforms[ name ].value = value; 65 | 66 | } 67 | 68 | finalize() { 69 | 70 | const modifiedDefines = this.modifiedDefines; 71 | const defines = this.defines; 72 | 73 | for ( const key in modifiedDefines ) { 74 | 75 | if ( modifiedDefines[ key ] === undefined ) { 76 | 77 | if ( key in defines ) { 78 | 79 | this.needsUpdate = true; 80 | 81 | } 82 | 83 | } else { 84 | 85 | if ( defines[ key ] !== modifiedDefines[ key ] ) { 86 | 87 | this.needsUpdate = true; 88 | 89 | } 90 | 91 | } 92 | 93 | delete modifiedDefines[ key ]; 94 | 95 | } 96 | 97 | const modifiedUniforms = this.modifiedUniforms; 98 | const uniforms = this.uniforms; 99 | for ( const key in modifiedUniforms ) { 100 | 101 | if ( modifiedUniforms[ key ] !== uniforms[ key ].value ) { 102 | 103 | this.needsUpdate = true; 104 | 105 | } 106 | 107 | delete modifiedUniforms[ key ]; 108 | 109 | } 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/index.js: -------------------------------------------------------------------------------- 1 | import { ShaderReplacement } from './ShaderReplacement.js'; 2 | import { gatherMeshes } from './utils.js'; 3 | 4 | export { ShaderReplacement, gatherMeshes }; 5 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/passes/DepthPass.js: -------------------------------------------------------------------------------- 1 | import { ShaderReplacement } from '../ShaderReplacement.js'; 2 | import { ShaderLib, BasicDepthPacking } from 'three'; 3 | export class DepthPass extends ShaderReplacement { 4 | 5 | constructor() { 6 | 7 | super( { 8 | defines: { 9 | DEPTH_PACKING: BasicDepthPacking 10 | }, 11 | uniforms: { 12 | ...ShaderLib.depth.uniforms, 13 | alphaMap: { value: null }, 14 | alphaTest: { value: 0 }, 15 | map: { value: null }, 16 | opacity: { value: 1.0 } 17 | }, 18 | vertexShader: ShaderLib.depth.vertexShader, 19 | fragmentShader: ShaderLib.depth.fragmentShader 20 | } ); 21 | 22 | } 23 | 24 | updateUniforms( object, material, target ) { 25 | 26 | super.updateUniforms( object, material, target ); 27 | 28 | // TODO: Handle displacement map 29 | // TODO: support packing 30 | 31 | target.setDefine( 'USE_UV', '' ); 32 | 33 | target.setDefine( 'ALPHATEST', target.uniforms.alphaTest.value ? target.uniforms.alphaTest.value : undefined ); 34 | 35 | target.setDefine( 'USE_ALPHAMAP', ( target.defines.ALPHATEST === 0 || ! target.uniforms.alphaMap.value ) ? undefined : '' ); 36 | 37 | target.setDefine( 'USE_MAP', ( target.defines.ALPHATEST === 0 || ! target.uniforms.map.value ) ? undefined : '' ); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/passes/NormalPass.js: -------------------------------------------------------------------------------- 1 | import { ShaderReplacement, setMaterialDefine } from '../ShaderReplacement.js'; 2 | import { ShaderLib } from 'three'; 3 | 4 | export class NormalPass extends ShaderReplacement { 5 | 6 | constructor() { 7 | 8 | super( { 9 | extensions: { 10 | derivatives: true 11 | }, 12 | defines: { 13 | // USE_NORMALMAP : '', 14 | // TANGENTSPACE_NORMALMAP : '', 15 | USE_UV: '' 16 | }, 17 | uniforms: { 18 | ...ShaderLib.normal.uniforms, 19 | alphaMap: { value: null }, 20 | alphaTest: { value: 0 }, 21 | map: { value: null }, 22 | opacity: { value: 1.0 } 23 | }, 24 | vertexShader: ShaderLib.normal.vertexShader, 25 | fragmentShader: /* glsl */` 26 | 27 | #define NORMAL 28 | uniform float opacity; 29 | #if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP ) 30 | varying vec3 vViewPosition; 31 | #endif 32 | #ifndef FLAT_SHADED 33 | varying vec3 vNormal; 34 | #ifdef USE_TANGENT 35 | varying vec3 vTangent; 36 | varying vec3 vBitangent; 37 | #endif 38 | #endif 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | void main() { 48 | vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | gl_FragColor = vec4( packNormalToRGB( normal ), opacity ); 57 | } 58 | ` 59 | } ); 60 | 61 | this.useNormalMaps = false; 62 | 63 | } 64 | 65 | createMaterial( ...args ) { 66 | 67 | const mat = super.createMaterial( ...args ); 68 | 69 | 70 | return mat; 71 | 72 | } 73 | 74 | updateUniforms( object, material, target ) { 75 | 76 | super.updateUniforms( object, material, target ); 77 | 78 | // TODO: Handle object space normal map 79 | // TODO: Handle displacement map 80 | 81 | target.setDefine( 'USE_NORMALMAP', this.useNormalMaps && target.uniforms.normalMap.value ? '' : undefined ); 82 | target.setDefine( 'TANGENTSPACE_NORMALMAP', this.useNormalMaps && target.uniforms.normalMap.value ? '' : undefined ); 83 | 84 | target.setDefine( 'ALPHATEST', target.uniforms.alphaTest.value ? target.uniforms.alphaTest.value : undefined ); 85 | 86 | target.setDefine( 'USE_ALPHAMAP', ( target.defines.ALPHATEST === 0 || ! target.uniforms.alphaMap.value ) ? undefined : '' ); 87 | 88 | target.setDefine( 'USE_MAP', ( target.defines.ALPHATEST === 0 || ! target.uniforms.map.value ) ? undefined : '' ); 89 | 90 | target.setDefine( 'USE_UV', ( 'USE_ALPHAMAP' in target.defines || 'USE_MAP' in target.defines ) ? '' : undefined ); 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/passes/VelocityPass.js: -------------------------------------------------------------------------------- 1 | import { ShaderReplacement } from '../ShaderReplacement.js'; 2 | import { Matrix4 } from 'three'; 3 | import { VelocityShader } from './VelocityShader.js'; 4 | export class VelocityPass extends ShaderReplacement { 5 | 6 | constructor() { 7 | 8 | super( VelocityShader ); 9 | 10 | this.initialized = false; 11 | this.includeCameraVelocity = true; 12 | this.includeObjectVelocity = true; 13 | this.prevProjectionMatrix = new Matrix4(); 14 | this.prevViewMatrix = new Matrix4(); 15 | this.prevInfo = new Map(); 16 | this.lastFrame = 0; 17 | this.autoUpdate = true; 18 | 19 | } 20 | 21 | replace( ...args ) { 22 | 23 | // NOTE: As it is this can only really be used for one scene because of how this frame 24 | // index works. Instead we'll need a different frame id per scene. 25 | this.lastFrame ++; 26 | 27 | if ( ! this.initialized || ! this.includeCameraVelocity ) { 28 | 29 | const camera = this.camera; 30 | this.prevProjectionMatrix.copy( camera.projectionMatrix ); 31 | this.prevViewMatrix.copy( camera.matrixWorldInverse ); 32 | this.initialized = true; 33 | 34 | } 35 | 36 | super.replace( ...args ); 37 | 38 | } 39 | 40 | reset( ...args ) { 41 | 42 | super.reset( ...args ); 43 | 44 | // NOTE: We expect that the camera and object transforms are all up to date here so we can cache them for the next frame. 45 | if ( this.autoUpdate ) { 46 | 47 | this.updateTransforms(); 48 | 49 | } 50 | 51 | } 52 | 53 | updateTransforms() { 54 | 55 | const camera = this.camera; 56 | this.prevProjectionMatrix.copy( camera.projectionMatrix ); 57 | this.prevViewMatrix.copy( camera.matrixWorldInverse ); 58 | 59 | const lastFrame = this.lastFrame; 60 | const prevInfo = this.prevInfo; 61 | prevInfo.forEach( ( info, object ) => { 62 | 63 | if ( info.lastFrame !== lastFrame ) { 64 | 65 | if ( info.boneTexture ) { 66 | 67 | info.boneTexture.dispose(); 68 | 69 | } 70 | prevInfo.delete( object ); 71 | 72 | } else { 73 | 74 | info.modelViewMatrix.multiplyMatrices( this.prevViewMatrix, object.matrixWorld ); 75 | 76 | if ( info.boneMatrices ) { 77 | 78 | info.boneMatrices.set( object.skeleton.boneMatrices ); 79 | info.boneTexture.needsUpdate = true; 80 | 81 | } 82 | 83 | } 84 | 85 | } ); 86 | 87 | } 88 | 89 | updateUniforms( object, material, target ) { 90 | 91 | super.updateUniforms( object, material, target ); 92 | 93 | // TODO: Handle alpha clip 94 | // TODO: Handle displacement map 95 | 96 | const prevInfo = this.prevInfo; 97 | let info; 98 | if ( ! prevInfo.has( object ) ) { 99 | 100 | info = { 101 | 102 | lastFrame: this.lastFrame, 103 | modelViewMatrix: new Matrix4().multiplyMatrices( this.prevViewMatrix, object.matrixWorld ), 104 | boneMatrices: null, 105 | boneTexture: null, 106 | 107 | }; 108 | 109 | prevInfo.set( object, info ); 110 | 111 | } else { 112 | 113 | info = prevInfo.get( object ); 114 | 115 | } 116 | 117 | if ( material.skinned ) { 118 | 119 | const skeleton = object.skeleton; 120 | const boneTextureNeedsUpdate = info.boneMatrices === null || info.boneMatrices.length !== skeleton.boneMatrices.length; 121 | if ( isSkinned && boneTextureNeedsUpdate ) { 122 | 123 | const boneMatrices = new Float32Array( skeleton.boneMatrices.length ); 124 | boneMatrices.set( skeleton.boneMatrices ); 125 | info.boneMatrices = boneMatrices; 126 | 127 | const size = Math.sqrt( skeleton.boneMatrices.length / 4 ); 128 | const boneTexture = new DataTexture( boneMatrices, size, size, RGBAFormat, FloatType ); 129 | boneTexture.needsUpdate = true; 130 | 131 | target.uniforms.prevBoneTexture.value = boneTexture; 132 | info.boneTexture = boneTexture; 133 | 134 | } 135 | 136 | } 137 | 138 | info.lastFrame = this.lastFrame; 139 | target.uniforms.prevProjectionMatrix.value.copy( this.prevProjectionMatrix ); 140 | target.uniforms.prevModelViewMatrix.value.copy( info.modelViewMatrix ); 141 | 142 | } 143 | 144 | dispose() { 145 | 146 | this.initialized = false; 147 | 148 | const prevInfo = this.prevInfo; 149 | prevInfo.forEach( ( info, object ) => { 150 | 151 | if ( info.boneTexture ) { 152 | 153 | info.boneTexture.dispose(); 154 | 155 | } 156 | prevInfo.delete( object ); 157 | 158 | } ); 159 | 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/passes/VelocityShader.js: -------------------------------------------------------------------------------- 1 | import { ShaderChunk, Matrix4 } from 'three'; 2 | 3 | // Modified ShaderChunk.skinning_pars_vertex to handle 4 | // a second set of bone information from the previou frame 5 | export const prev_skinning_pars_vertex = 6 | ` 7 | #ifdef USE_SKINNING 8 | #ifdef BONE_TEXTURE 9 | uniform sampler2D prevBoneTexture; 10 | mat4 getPrevBoneMatrix( const in float i ) { 11 | float j = i * 4.0; 12 | float x = mod( j, float( boneTextureSize ) ); 13 | float y = floor( j / float( boneTextureSize ) ); 14 | float dx = 1.0 / float( boneTextureSize ); 15 | float dy = 1.0 / float( boneTextureSize ); 16 | y = dy * ( y + 0.5 ); 17 | vec4 v1 = texture2D( prevBoneTexture, vec2( dx * ( x + 0.5 ), y ) ); 18 | vec4 v2 = texture2D( prevBoneTexture, vec2( dx * ( x + 1.5 ), y ) ); 19 | vec4 v3 = texture2D( prevBoneTexture, vec2( dx * ( x + 2.5 ), y ) ); 20 | vec4 v4 = texture2D( prevBoneTexture, vec2( dx * ( x + 3.5 ), y ) ); 21 | mat4 bone = mat4( v1, v2, v3, v4 ); 22 | return bone; 23 | } 24 | #else 25 | uniform mat4 prevBoneMatrices[ MAX_BONES ]; 26 | mat4 getPrevBoneMatrix( const in float i ) { 27 | mat4 bone = prevBoneMatrices[ int(i) ]; 28 | return bone; 29 | } 30 | #endif 31 | #endif 32 | `; 33 | 34 | // Returns the body of the vertex shader for the velocity buffer and 35 | // outputs the position of the current and last frame positions 36 | export const velocity_vertex = 37 | ` 38 | vec3 transformed; 39 | 40 | // Get the normal 41 | ${ ShaderChunk.skinbase_vertex } 42 | ${ ShaderChunk.beginnormal_vertex } 43 | ${ ShaderChunk.skinnormal_vertex } 44 | ${ ShaderChunk.defaultnormal_vertex } 45 | 46 | // Get the current vertex position 47 | transformed = vec3( position ); 48 | ${ ShaderChunk.skinning_vertex } 49 | newPosition = modelViewMatrix * vec4( transformed, 1.0 ); 50 | 51 | // Get the previous vertex position 52 | transformed = vec3( position ); 53 | ${ ShaderChunk.skinbase_vertex.replace( /mat4 /g, '' ).replace( /getBoneMatrix/g, 'getPrevBoneMatrix' ) } 54 | ${ ShaderChunk.skinning_vertex.replace( /vec4 /g, '' ) } 55 | prevPosition = prevModelViewMatrix * vec4( transformed, 1.0 ); 56 | 57 | newPosition = projectionMatrix * newPosition; 58 | prevPosition = prevProjectionMatrix * prevPosition; 59 | 60 | gl_Position = mix( newPosition, prevPosition, interpolateGeometry ); 61 | 62 | `; 63 | 64 | export const VelocityShader = { 65 | 66 | uniforms: { 67 | prevProjectionMatrix: { value: new Matrix4() }, 68 | prevModelViewMatrix: { value: new Matrix4() }, 69 | prevBoneTexture: { value: null }, 70 | interpolateGeometry: { value: 0 }, 71 | intensity: { value: 1 }, 72 | 73 | alphaTest: { value: 0.0 }, 74 | map: { value: null }, 75 | alphaMap: { value: null }, 76 | opacity: { value: 1.0 } 77 | }, 78 | 79 | vertexShader: 80 | ` 81 | ${ ShaderChunk.skinning_pars_vertex } 82 | ${ prev_skinning_pars_vertex } 83 | 84 | uniform mat4 prevProjectionMatrix; 85 | uniform mat4 prevModelViewMatrix; 86 | uniform float interpolateGeometry; 87 | varying vec4 prevPosition; 88 | varying vec4 newPosition; 89 | 90 | void main() { 91 | 92 | ${ velocity_vertex } 93 | 94 | } 95 | `, 96 | 97 | fragmentShader: 98 | ` 99 | uniform float intensity; 100 | varying vec4 prevPosition; 101 | varying vec4 newPosition; 102 | 103 | void main() { 104 | 105 | vec3 pos0 = prevPosition.xyz / prevPosition.w; 106 | pos0 += 1.0; 107 | pos0 /= 2.0; 108 | 109 | vec3 pos1 = newPosition.xyz / newPosition.w; 110 | pos1 += 1.0; 111 | pos1 /= 2.0; 112 | 113 | vec3 vel = pos1 - pos0; 114 | gl_FragColor = vec4( vel * intensity, 1.0 ); 115 | 116 | } 117 | ` 118 | }; 119 | -------------------------------------------------------------------------------- /src/Experience/Utils/shader-replacement/src/utils.js: -------------------------------------------------------------------------------- 1 | function gatherMeshes( scene, target ) { 2 | 3 | target.length = 0; 4 | scene.traverse( c => { 5 | 6 | if ( c.isMesh || c.isSkinnedMesh ) { 7 | 8 | target.push( c ); 9 | 10 | } 11 | 12 | } ); 13 | 14 | } 15 | 16 | export { gatherMeshes }; 17 | -------------------------------------------------------------------------------- /src/Experience/World/Abstracts/Model.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '@/Experience.js' 3 | import Resources from "@/Utils/Resources.js"; 4 | 5 | export default class Model { 6 | 7 | constructor() { 8 | 9 | } 10 | 11 | // loadModel( inputElement ) { 12 | // 13 | // if ( this[this.sources[0].name] || this.loadingModel ) { 14 | // return; 15 | // } 16 | // 17 | // this.loadingModel = true; 18 | // 19 | // const preloaderElement = document.createElement( 'div' ) 20 | // preloaderElement.classList.add( 'loader' ) 21 | // inputElement.parentElement.appendChild( preloaderElement ) 22 | // 23 | // this.localResources = new Resources( this.sources, this.sourcesReady ) 24 | // 25 | // this.localResources.on( this.sourcesReady, () => { 26 | // this.setModel() 27 | // this.setDebug() 28 | // inputElement.parentElement.removeChild( preloaderElement ) 29 | // this.loadingModel = false; 30 | // } ) 31 | // } 32 | 33 | setModel() { 34 | } 35 | 36 | setDebug() { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Experience/World/DebugHelpers.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 5 | import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper.js' 6 | import Gizmo from '../Utils/Gizmo.js' 7 | 8 | export default class DebugHelpers { 9 | experience = new Experience() 10 | debug = experience.debug 11 | scene = experience.scene 12 | time = experience.time 13 | camera = experience.camera.instance 14 | renderer = experience.renderer.instance 15 | resources = experience.resources 16 | cursor = experience.cursor 17 | timeline = experience.timeline; 18 | controls = experience.camera?.controls 19 | container = new THREE.Group(); 20 | 21 | constructor() { 22 | if ( !this.debug.active ) return 23 | 24 | this.setupDebugFeatures() 25 | } 26 | 27 | setupDebugFeatures() { 28 | //this.addGlobalAxes() 29 | this.addViewHelper() 30 | } 31 | 32 | addGlobalAxes() { 33 | const axesHelper = new THREE.AxesHelper( 5 ); 34 | this.scene.add( axesHelper ); 35 | } 36 | 37 | addViewHelper() { 38 | this.gizmo = new Gizmo(this.camera, { size: 100, padding: 8 }); 39 | document.body.appendChild(this.gizmo); 40 | this.gizmo.onAxisSelected = function(axis) { 41 | console.log(axis); // { axis: "x", direction: THREE.Vector3(1,0,0) } 42 | } 43 | } 44 | 45 | resize() { 46 | 47 | } 48 | 49 | update( deltaTime ) { 50 | this.gizmo?.update(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Experience/World/Environment.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from '../Experience.js' 3 | 4 | export default class Environment { 5 | constructor() { 6 | this.experience = new Experience() 7 | this.scene = this.experience.scene 8 | this.resources = this.experience.resources 9 | this.debug = this.experience.debug 10 | 11 | this.scene.colorSpace = THREE.SRGBColorSpace 12 | 13 | this.setAmbientLight() 14 | this.setDirectionalLight() 15 | 16 | this.setDebug() 17 | } 18 | 19 | setAmbientLight() { 20 | this.ambientLight = new THREE.AmbientLight( '#ffffff', 0.05 ) 21 | this.scene.add( this.ambientLight ) 22 | } 23 | 24 | setDirectionalLight() { 25 | this.directionalLight = new THREE.DirectionalLight( '#ffffff', 1 ) 26 | this.directionalLight.position.set( 0, 5, 5 ) 27 | this.scene.add( this.directionalLight ) 28 | } 29 | 30 | 31 | setEnvironmentMap() { 32 | 33 | } 34 | 35 | setDebug() { 36 | if ( this.debug.active ) { 37 | 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Experience/World/ExampleClass.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Model from './Abstracts/Model.js' 3 | import Experience from '../Experience.js' 4 | import Debug from '../Utils/Debug.js' 5 | import State from "../State.js"; 6 | import Materials from "../Materials/Materials.js"; 7 | 8 | export default class ExampleClass extends Model { 9 | experience = Experience.getInstance() 10 | debug = Debug.getInstance() 11 | state = State.getInstance() 12 | materials = Materials.getInstance() 13 | scene = experience.scene 14 | time = experience.time 15 | camera = experience.camera.instance 16 | renderer = experience.renderer.instance 17 | resources = experience.resources 18 | container = new THREE.Group(); 19 | 20 | constructor() { 21 | super() 22 | 23 | this.setModel() 24 | this.setDebug() 25 | } 26 | 27 | setModel() { 28 | const stencilRef = 1 29 | 30 | // // create plane 31 | // this.planeGeometry = new THREE.PlaneGeometry( 2, 2 ); 32 | // this.planeMaterial = new THREE.MeshBasicMaterial( { color: 0xffffff, side: THREE.DoubleSide } ); 33 | // this.planeMaterial.depthWrite = false; 34 | // this.planeMaterial.stencilWrite = true; 35 | // this.planeMaterial.stencilRef = stencilRef; 36 | // this.planeMaterial.stencilFunc = THREE.AlwaysStencilFunc; 37 | // this.planeMaterial.stencilZPass = THREE.ReplaceStencilOp; 38 | // 39 | // this.plane = new THREE.Mesh( this.planeGeometry, this.planeMaterial ); 40 | // this.plane.position.z += 1 41 | // this.plane.position.x += 1 42 | // 43 | // this.container.add( this.plane ); 44 | 45 | // texture 46 | this.displacementTexture = this.resources.items.displacementTexture 47 | 48 | // add example cube 49 | this.geometry = new THREE.BoxGeometry( 1, 1, 1 ); 50 | this.material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); 51 | this.material.map = this.displacementTexture 52 | // this.material.stencilRef = stencilRef; 53 | // this.material.stencilFunc = THREE.EqualStencilFunc; 54 | // this.material.stencilWrite = true; 55 | 56 | 57 | this.cube = new THREE.Mesh( this.geometry, this.material ); 58 | 59 | 60 | // add example cube 61 | this.geometry2 = new THREE.TorusKnotGeometry( 2.5, 1, 150, 40 ); 62 | this.material2 = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); 63 | this.material2.map = this.displacementTexture 64 | 65 | this.cube2 = new THREE.Mesh( this.geometry2, this.material2 ); 66 | this.cube2.scale.set(0.2, 0.2, 0.2) 67 | 68 | 69 | this.cube2.position.x = 2 70 | 71 | this.cube.layers.enable( this.state.layersConst.BLOOM_SCENE ); 72 | 73 | 74 | this.container.add( this.cube ); 75 | this.container.add( this.cube2 ); 76 | this.scene.add( this.container ); 77 | 78 | 79 | 80 | } 81 | 82 | resize() { 83 | 84 | } 85 | 86 | setDebug() { 87 | if ( !this.debug.active ) return 88 | 89 | this.debug.createDebugTexture( this.resources.items.displacementTexture ) 90 | } 91 | 92 | update( deltaTime ) { 93 | //this.cube2.rotation.y += deltaTime * 20 94 | //this.cube.rotation.y += deltaTime * 30 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/Experience/World/ParticlesSimulation.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../Experience.js"; 3 | import Debug from "../Utils/Debug.js"; 4 | import State from "../State.js"; 5 | import { GPUComputationRenderer } from "three/addons/misc/GPUComputationRenderer.js"; 6 | import gpgpuParticlesShader from "../Shaders/Gpgpu/particles.glsl"; 7 | import gpgpuParticlesVelocityShader from "../Shaders/Gpgpu/particlesVelocity.glsl"; 8 | import { createNoise4D } from 'simplex-noise'; 9 | import { MathUtils } from "three"; 10 | 11 | export default class ParticlesSimulation { 12 | experience = Experience.getInstance(); 13 | debug = Debug.getInstance(); 14 | state = State.getInstance(); 15 | scene = experience.scene; 16 | time = experience.time; 17 | camera = experience.camera.instance; 18 | renderer = experience.renderer.instance; 19 | resources = experience.resources; 20 | container = new THREE.Group(); 21 | 22 | points = experience.world.particles.points; 23 | 24 | prevPositionsTexture = null; 25 | currPositionsTexture = null; 26 | 27 | constructor() { 28 | this.setModel(); 29 | this.setDebug(); 30 | 31 | // disable block with class app-header 32 | // document.querySelector('.app-header').style.display = 'none'; 33 | } 34 | 35 | setModel() { 36 | /** 37 | * Base geometry 38 | */ 39 | 40 | const baseGeometry = {}; 41 | baseGeometry.instance = this.points.geometry; 42 | baseGeometry.count = baseGeometry.instance.instanceCount; 43 | 44 | /** 45 | * GPU Compute 46 | */ 47 | 48 | // Setup 49 | const gpgpu = {}; 50 | this.gpgpu = gpgpu; 51 | gpgpu.size = Math.ceil( Math.sqrt( baseGeometry.count ) ); 52 | gpgpu.computation = new GPUComputationRenderer( gpgpu.size, gpgpu.size, this.renderer ); 53 | 54 | // Base particles 55 | const baseParticlesTexture = gpgpu.computation.createTexture(); 56 | const baseParticlesVelocityTexture = gpgpu.computation.createTexture(); 57 | const noise4D = createNoise4D(); 58 | 59 | for(let i = 0; i < baseGeometry.count; i++) 60 | { 61 | const i3 = i * 3 62 | const i4 = i * 4 63 | 64 | 65 | // Position sphere 66 | const radius = 1.2 67 | const theta = Math.random() * Math.PI * 2 68 | const phi = Math.acos(2 * Math.random() - 1) 69 | 70 | const x = radius * Math.sin(phi) * Math.cos(theta) 71 | const y = radius * Math.sin(phi) * Math.sin(theta) 72 | const z = radius * Math.cos(phi) 73 | 74 | // Position based on geometry 75 | baseParticlesTexture.image.data[i4 + 0] = x + noise4D( x, y, z, this.time.elapsed) * 1 76 | baseParticlesTexture.image.data[i4 + 1] = y + noise4D( x, y, z, this.time.elapsed) * 1 77 | baseParticlesTexture.image.data[i4 + 2] = z + noise4D( x, y, z, this.time.elapsed) * 1 78 | baseParticlesTexture.image.data[i4 + 3] = Math.random() 79 | } 80 | 81 | // Particles variable 82 | gpgpu.particlesVariable = gpgpu.computation.addVariable('uParticles', gpgpuParticlesShader, baseParticlesTexture) 83 | gpgpu.computation.setVariableDependencies(gpgpu.particlesVariable, [ gpgpu.particlesVariable ]) 84 | 85 | // Particles variable Uniforms 86 | gpgpu.particlesVariable.material.uniforms.uTime = new THREE.Uniform(0) 87 | gpgpu.particlesVariable.material.uniforms.uDeltaTime = new THREE.Uniform(0) 88 | gpgpu.particlesVariable.material.uniforms.uBase = new THREE.Uniform(baseParticlesTexture) 89 | gpgpu.particlesVariable.material.uniforms.uFlowFieldInfluence = new THREE.Uniform(0.5) 90 | gpgpu.particlesVariable.material.uniforms.uFlowFieldStrength = new THREE.Uniform(2.522) 91 | gpgpu.particlesVariable.material.uniforms.uFlowFieldFrequency = new THREE.Uniform(0.5) 92 | 93 | // Velocity variable 94 | gpgpu.particlesVelocityVariable = gpgpu.computation.addVariable('uParticlesVelocity', gpgpuParticlesVelocityShader, baseParticlesVelocityTexture) 95 | gpgpu.computation.setVariableDependencies(gpgpu.particlesVelocityVariable, [ gpgpu.particlesVelocityVariable ]) 96 | 97 | // Velocity Particles variable Uniforms 98 | gpgpu.particlesVelocityVariable.material.uniforms.uTime = new THREE.Uniform(0) 99 | gpgpu.particlesVelocityVariable.material.uniforms.uDeltaTime = new THREE.Uniform(0) 100 | gpgpu.particlesVelocityVariable.material.uniforms.uPrevPositions = new THREE.Uniform() 101 | gpgpu.particlesVelocityVariable.material.uniforms.uCurrPositions = new THREE.Uniform() 102 | 103 | // Init 104 | gpgpu.computation.init() 105 | 106 | // Set Geometry UVs 107 | const particlesUvArray = new Float32Array(baseGeometry.count * 2) 108 | 109 | for(let i = 0; i < baseGeometry.count; i++) 110 | { 111 | const i2 = i * 2 112 | 113 | // Particles UV 114 | const y = Math.floor(i / gpgpu.size) 115 | const x = i - y * gpgpu.size 116 | 117 | particlesUvArray[i2 + 0] = (x + 0.5) / gpgpu.size 118 | particlesUvArray[i2 + 1] = (y + 0.5) / gpgpu.size 119 | } 120 | 121 | this.points.geometry.setAttribute('aParticlesUv', new THREE.InstancedBufferAttribute(particlesUvArray, 2)) 122 | this.points.geometry.needsUpdate = true 123 | 124 | } 125 | 126 | resize() { 127 | 128 | } 129 | 130 | setDebug() { 131 | if ( !this.debug.active ) return; 132 | 133 | this.debugFolder = this.debug.panel.addFolder( "Flow Field" ); 134 | 135 | this.debugFolder.add(this.gpgpu.particlesVariable.material.uniforms.uFlowFieldInfluence, 'value') 136 | .min(0).max(1).step(0.001).name('uFlowfieldInfluence') 137 | this.debugFolder.add(this.gpgpu.particlesVariable.material.uniforms.uFlowFieldStrength, 'value') 138 | .min(0).max(10).step(0.001).name('uFlowfieldStrength') 139 | this.debugFolder.add(this.gpgpu.particlesVariable.material.uniforms.uFlowFieldFrequency, 'value') 140 | .min(0).max(1).step(0.001).name('uFlowfieldFrequency') 141 | 142 | 143 | //this.debug.createDebugTexture( this.gpgpu.computation.getCurrentRenderTarget(this.gpgpu.particlesVariable).texture ) 144 | //this.debug.createDebugTexture( this.gpgpu.computation.getCurrentRenderTarget(this.gpgpu.particlesVelocityVariable).texture ) 145 | 146 | //this.debug.createDebugTexture( this.resources.items.displacementTexture ) 147 | } 148 | 149 | update( deltaTime ) { 150 | // GPGPU Update 151 | this.gpgpu.particlesVariable.material.uniforms.uTime.value = this.time.elapsed 152 | this.gpgpu.particlesVariable.material.uniforms.uDeltaTime.value = deltaTime 153 | 154 | this.points.material.uniforms.uParticlesTexture.value = this.gpgpu.computation.getCurrentRenderTarget(this.gpgpu.particlesVariable).texture 155 | 156 | this.prevPositionsTexture = this.currPositionsTexture 157 | this.currPositionsTexture = this.gpgpu.computation.getCurrentRenderTarget(this.gpgpu.particlesVariable).texture 158 | 159 | this.gpgpu.particlesVelocityVariable.material.uniforms.uTime.value = this.time.elapsed 160 | this.gpgpu.particlesVelocityVariable.material.uniforms.uDeltaTime.value = MathUtils.damp(this.gpgpu.particlesVelocityVariable.material.uniforms.uDeltaTime.value, deltaTime, 9, deltaTime) 161 | this.gpgpu.particlesVelocityVariable.material.uniforms.uPrevPositions.value = this.prevPositionsTexture 162 | this.gpgpu.particlesVelocityVariable.material.uniforms.uCurrPositions.value = this.currPositionsTexture 163 | 164 | this.points.material.uniforms.uParticlesVelocityTexture.value = this.gpgpu.computation.getCurrentRenderTarget(this.gpgpu.particlesVelocityVariable).texture 165 | 166 | this.gpgpu.computation.compute() 167 | 168 | //particles.material.uniforms.uParticlesTexture.value = gpgpu.computation.getCurrentRenderTarget(gpgpu.particlesVariable).texture 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/Experience/World/World.js: -------------------------------------------------------------------------------- 1 | import Experience from '../Experience.js' 2 | import Environment from './Environment.js' 3 | import ExampleClass from "./ExampleClass.js"; 4 | import DebugHelpers from "./DebugHelpers.js"; 5 | import Ui from "../Ui/Ui.js"; 6 | import State from "../State.js"; 7 | import Time from "../Utils/Time.js"; 8 | 9 | import Particles from "./Particles.js"; 10 | import ParticlesSimulation from "./ParticlesSimulation.js"; 11 | 12 | export default class World { 13 | constructor() { 14 | this.experience = Experience.getInstance() 15 | this.ui = new Ui() 16 | this.time = Time.getInstance() 17 | 18 | this.camera = this.experience.camera; 19 | this.scene = this.experience.scene 20 | 21 | this.resources = this.experience.resources 22 | this.html = this.experience.html 23 | this.sound = this.experience.sound 24 | this.debug = this.experience.debug.panel 25 | 26 | // Wait for resources 27 | this.resources.on( 'ready', () => { 28 | this.state = new State() 29 | 30 | //this.startWithPreloader() 31 | this.start() 32 | } ) 33 | } 34 | 35 | start() { 36 | setTimeout( () => { 37 | window.preloader.hidePreloader() 38 | }, 1000) 39 | 40 | this.time.reset() 41 | 42 | this.setupWorld() 43 | 44 | this.animationPipeline(); 45 | } 46 | 47 | setupWorld() { 48 | // Setup 49 | //this.cube = new ExampleClass() 50 | this.particles = new Particles() 51 | this.particlesSimulation = new ParticlesSimulation() 52 | this.environment = new Environment() 53 | 54 | // Add debug helpers 55 | this.debugHelpers = new DebugHelpers() 56 | 57 | // Animation timeline 58 | this.animationPipeline(); 59 | 60 | // Dispatch event 61 | this.experience.appLoaded = true 62 | } 63 | 64 | startWithPreloader() { 65 | this.ui.playButton.classList.add( "fade-in" ); 66 | this.ui.playButton.addEventListener( 'click', () => { 67 | 68 | this.ui.playButton.classList.replace( "fade-in", "fade-out" ); 69 | //this.sound.createSounds(); 70 | 71 | setTimeout( () => { 72 | this.time.reset() 73 | 74 | // Setup 75 | this.setupWorld() 76 | 77 | // Remove preloader 78 | this.ui.preloader.classList.add( "preloaded" ); 79 | setTimeout( () => { 80 | this.ui.preloader.remove(); 81 | this.ui.playButton.remove(); 82 | }, 2500 ); 83 | }, 100 ); 84 | }, { once: true } ); 85 | } 86 | 87 | animationPipeline() { 88 | // if ( this.text ) 89 | // this.text.animateTextShow() 90 | 91 | if ( this.camera ) 92 | this.camera.animateCameraPosition() 93 | } 94 | 95 | resize() { 96 | this.state.resize() 97 | } 98 | 99 | update( deltaTime ) { 100 | this.debugHelpers?.update( deltaTime ) 101 | this.cube?.update( deltaTime ) 102 | this.particles?.update( deltaTime ) 103 | this.particlesSimulation?.update( deltaTime ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Experience/sources.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | // { 3 | // name: 'backgroundSound', 4 | // type: 'audio', 5 | // path: 'sounds/background.mp3' 6 | // }, 7 | // { 8 | // name: 'predatorModel', 9 | // type: 'gltfModel', 10 | // path: 'models/predator.glb' 11 | // }, 12 | { 13 | name: 'displacementTexture', 14 | type: 'texture', 15 | path: 'textures/displacement.jpg' 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 3d-app-template 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/preloader.js: -------------------------------------------------------------------------------- 1 | class Star { 2 | ctx = window.preloader.ctx; 3 | preloader = window.preloader 4 | id = 0; 5 | 6 | params = { 7 | maxDistFromCursor: 50, 8 | dotsSpeed: 0, 9 | backgroundSpeed: 0 10 | }; 11 | 12 | constructor( id, x, y ) { 13 | this.id = id; 14 | this.x = x; 15 | this.y = y; 16 | this.r = Math.floor( Math.random() * 2 ) + 1; 17 | const alpha = ( Math.floor( Math.random() * 10 ) + 1 ) / 10 / 2; 18 | this.color = "rgba(255,255,255," + alpha + ")"; 19 | } 20 | 21 | draw() { 22 | this.ctx.fillStyle = this.color; 23 | this.ctx.shadowBlur = this.r * 2; 24 | this.ctx.beginPath(); 25 | this.ctx.arc( this.x, this.y, this.r, 0, 2 * Math.PI, false ); 26 | this.ctx.closePath(); 27 | this.ctx.fill(); 28 | } 29 | 30 | move() { 31 | this.y -= this.preloader.deltaTime * 15 + this.params.backgroundSpeed / 100; 32 | if ( this.y <= -10 ) this.y = this.preloader.ch + 10; 33 | 34 | this.draw(); 35 | } 36 | 37 | } 38 | 39 | class Preloader { 40 | preloader = document.getElementById( 'preloader' ); 41 | 42 | ctx = this.preloader.getContext( '2d' ); 43 | cw = this.preloader.width; 44 | ch = this.preloader.height; 45 | 46 | size = 20; 47 | centerX = this.preloader.width / 2; 48 | centerY = this.preloader.height / 2; 49 | strokeWidth = 4; 50 | angle = 0; 51 | nextTime = 0; 52 | delay = 1000 / 60 * 0.5; 53 | deltaTime = 0.016666666666666668; 54 | current = Date.now(); 55 | start = this.current; 56 | elapsed = 0; 57 | animationShow = true; 58 | activePreloader = true; 59 | animationState = 0; 60 | animationInterval = undefined; 61 | triangleState = 0; 62 | triangleStateNormalize = 0; 63 | resizeAction = false; 64 | aspectRatio = 1 / window.innerHeight 65 | 66 | stars = [] 67 | initStarsPopulation = 100 68 | dots = [] 69 | 70 | constructor() { 71 | window.preloader = this 72 | 73 | this.init() 74 | 75 | this.initStars() 76 | 77 | window.requestAnimationFrame( this.animate ); 78 | } 79 | 80 | init() { 81 | this.resize() 82 | 83 | window.addEventListener( 'resize', () => { 84 | this.resize() 85 | this.initStars() 86 | this.resizeAction = true 87 | }) 88 | } 89 | 90 | animate = ( time ) => { 91 | if ( !this.activePreloader ) { 92 | requestAnimationFrame( this.animate ); 93 | return 94 | } 95 | 96 | const currentTime = Date.now() 97 | this.deltaTime = Math.min( ( currentTime - this.current ) * 0.001, 0.016 ) 98 | this.current = currentTime 99 | this.elapsed = ( this.current - this.start ) * 0.001 100 | 101 | if ( this.deltaTime > 0.06 ) { 102 | this.deltaTime = 0.06 103 | } 104 | 105 | if ( time < this.nextTime && !this.resizeAction ) { 106 | requestAnimationFrame( this.animate ); 107 | return; 108 | } 109 | this.resizeAction = false; 110 | this.nextTime = time + this.delay * this.deltaTime; 111 | 112 | this.ctx.clearRect( 0, 0, this.cw, this.ch ); 113 | 114 | this.ctx.fillStyle = 'black'; 115 | this.ctx.globalCompositeOperation = 'xor'; 116 | this.ctx.fillRect( 0, 0, this.cw, this.ch ); 117 | 118 | this.ctx.fillStyle = 'black'; 119 | this.ctx.globalCompositeOperation = 'source-over'; 120 | this.ctx.fillRect( 0, 0, this.cw, this.ch ); 121 | 122 | // Создаем линейный градиент 123 | const gradient = this.ctx.createLinearGradient( 0, 0, 0, this.ch ); 124 | 125 | // Добавляем цвета 126 | gradient.addColorStop( 0, '#000000' ); // Черный цвет на начале 127 | gradient.addColorStop( 1, '#5788fe' ); // Синий цвет на конце 128 | 129 | // // Применяем градиент к заливке 130 | // this.ctx.fillStyle = gradient; 131 | // 132 | // // Рисуем прямоугольник, заполняя весь холст 133 | // this.ctx.fillRect( 0, 0, this.cw, this.ch ); 134 | 135 | 136 | this.drawStars(); 137 | 138 | this.drawTriangle() 139 | 140 | this.triangleState += 3 * this.deltaTime; 141 | 142 | if ( this.triangleState > 4.0 ) { 143 | this.triangleState = 0; 144 | } 145 | 146 | this.triangleStateNormalize = this.triangleState / 1.7; 147 | 148 | //this.angle += 60 * this.deltaTime; 149 | 150 | requestAnimationFrame( this.animate ); 151 | } 152 | 153 | drawTriangle() { 154 | //this.ctx.clearRect( 0, 0, this.cw, this.ch ); 155 | //this.ctx.globalAlpha = 1.0 - this.animationState 156 | 157 | this.ctx.save(); 158 | 159 | //const sideLength = this.ch / 3 * this.remap(this.animationState, 0, 1, 1, 0.4); // Length of the side of the big triangle 160 | const sideLength = this.ch / 3; 161 | const height = sideLength * Math.sqrt( 3 ) / 2 + 1; // Height of the big triangle 162 | const centroidY = height / 6; // Y-coordinate of the centroid of the big triangle 163 | 164 | 165 | // Offset for the center of mass 166 | this.ctx.translate( this.cw / 2, this.ch / 2 ); 167 | this.ctx.rotate( this.angle * Math.PI / 180 ); // Rotation of the big triangle 168 | 169 | const smallTriangleCount = 9; // Count of small triangles 170 | const smallSideLength = sideLength / smallTriangleCount; // Length of the side of the small triangle 171 | const smallHeight = smallSideLength * Math.sqrt( 3 ) / 2; // Height of the small triangle 172 | const smallCentroidY = smallHeight / 6; // Y-coordinate of the centroid of the small triangle 173 | 174 | // first minX from smallTriangleCount = 1 175 | const firstSmallHeight = sideLength * Math.sqrt( 3 ) / 2; 176 | const firstMinX = Math.abs(-sideLength / 2 + Math.abs( ( -height / 2 + firstSmallHeight / 2 + height / 2 ) / Math.sqrt( 3 ) )); 177 | 178 | 179 | // Draw small triangles 180 | for ( let y = -height / 2 + smallHeight / 2; y <= height / 2 - smallHeight / 2; y += smallHeight ) { 181 | const minX = -sideLength / 2 + Math.abs( ( y + height / 2 ) / Math.sqrt( 3 ) ); 182 | const maxX = sideLength / 2 - Math.abs( ( y + height / 2 ) / Math.sqrt( 3 ) ) + 1; 183 | 184 | let offsetWhite = 0.2; 185 | let offsetBlack = 0; 186 | 187 | let distanceToCenter = Math.abs( y ); 188 | offsetWhite = 2 * distanceToCenter / ( sideLength / 2 ); 189 | offsetBlack = 1 * distanceToCenter / ( sideLength / 2 ); 190 | 191 | for ( let x = minX; x <= maxX - smallSideLength / 2; x += smallSideLength ) { 192 | const offsetX = firstMinX / smallTriangleCount; 193 | const offsetY = firstMinX / 2 + 6; 194 | 195 | this.drawSmallTriangle( x + offsetX, y + offsetY, smallSideLength,'white', false, offsetWhite ); // Small triangles 196 | this.drawSmallTriangle( x + offsetX, y + offsetY, smallSideLength,`black`, false, offsetBlack ); // Small triangles 197 | 198 | if ( x + smallSideLength / 2 <= maxX - smallSideLength ) { 199 | this.drawSmallTriangle( x + offsetX + smallSideLength / 2, y + offsetY, smallSideLength, 'white', true, offsetWhite ); // Reverse small triangles 200 | this.drawSmallTriangle( x + offsetX + smallSideLength / 2, y + offsetY, smallSideLength, `black`, true, offsetBlack ); // Reverse small triangles 201 | } 202 | 203 | distanceToCenter = Math.abs( x + offsetX ); 204 | offsetWhite = 3 * distanceToCenter / ( sideLength / 2 ); 205 | offsetBlack = 2 * distanceToCenter / ( sideLength / 2 ); 206 | } 207 | } 208 | 209 | this.drawXorTriangle( sideLength ); 210 | 211 | 212 | this.ctx.restore(); 213 | 214 | //this.drawCenter() 215 | } 216 | 217 | drawSmallTriangle( x, y, length, fillStyle, flipped, offset = 0 ) { 218 | let scale = 1.0 - this.clamp( this.triangleState - offset, 0, 1 ); 219 | 220 | const newLength = length * scale; // New length of the small triangle 221 | const h = newLength * Math.sqrt( 3 ) / 2; // New height of the small triangle 222 | const oldHeight = length * Math.sqrt( 3 ) / 2; // Old height of the small triangle 223 | 224 | // Offset for the center of mass 225 | const offsetY = ( oldHeight - h ) / 3; 226 | 227 | this.ctx.beginPath(); 228 | if ( !flipped ) { 229 | // Basis triangle 230 | this.ctx.moveTo( x, y + h / 2 - offsetY ); 231 | this.ctx.lineTo( x + newLength / 2, y - h / 2 - offsetY ); 232 | this.ctx.lineTo( x - newLength / 2, y - h / 2 - offsetY ); 233 | } else { 234 | // Reversed triangle 235 | this.ctx.moveTo( x, y - h / 2 - offsetY ); 236 | this.ctx.lineTo( x + newLength / 2, y + h / 2 - offsetY ); 237 | this.ctx.lineTo( x - newLength / 2, y + h / 2 - offsetY ); 238 | } 239 | this.ctx.closePath(); 240 | this.ctx.fillStyle = fillStyle; // Color of the small triangle 241 | this.ctx.fill(); 242 | 243 | // add border 244 | this.ctx.strokeStyle = 'black'; 245 | this.ctx.lineWidth = 1; 246 | this.ctx.stroke(); 247 | 248 | } 249 | 250 | resize() { 251 | this.preloader.width = window.innerWidth; 252 | this.preloader.height = window.innerHeight; 253 | 254 | this.cw = this.preloader.width; 255 | this.ch = this.preloader.height; 256 | this.centerX = this.preloader.width / 2; 257 | this.centerY = this.preloader.height / 2; 258 | 259 | this.ctx.fillStyle = 'rgba(0, 0, 0, 1.0)'; 260 | this.ctx.fillRect( 0, 0, this.cw, this.ch ); 261 | 262 | this.preloader.setAttribute( "width", this.cw ); 263 | this.preloader.setAttribute( "height", this.ch ); 264 | 265 | this.aspectRatio = 1 / window.innerHeight 266 | } 267 | 268 | initStars() { 269 | this.ctx.strokeStyle = "white"; 270 | this.ctx.shadowColor = "white"; 271 | for ( let i = 0; i < this.initStarsPopulation; i++ ) { 272 | this.stars[ i ] = new Star( i, Math.floor( Math.random() * this.cw ), Math.floor( Math.random() * this.ch ) ); 273 | //stars[i].draw(); 274 | } 275 | this.ctx.shadowBlur = 0; 276 | } 277 | 278 | drawStars() { 279 | this.ctx.save() 280 | 281 | //ctx.clearRect(0, 0, WIDTH, HEIGHT); 282 | 283 | for ( let i in this.stars ) { 284 | this.stars[ i ].move(); 285 | } 286 | for ( let i in this.dots ) { 287 | this.dots[ i ].move(); 288 | } 289 | 290 | this.ctx.restore() 291 | } 292 | 293 | drawXorTriangle( sideLength ) { 294 | this.ctx.globalCompositeOperation = 'xor'; 295 | 296 | this.ctx.rotate( Math.PI * this.animationState ); // Rotation of the big triangle 297 | 298 | // Draw the big triangle 299 | let mainSideLength = sideLength * 100 * this.animationState; 300 | let mainHeight = mainSideLength * Math.sqrt( 3 ) / 2 - 4; 301 | let mainCentroidY = mainHeight / 6; 302 | this.ctx.beginPath(); 303 | this.ctx.moveTo( -mainSideLength / 2, -mainHeight / 2 + mainCentroidY ); 304 | this.ctx.lineTo( mainSideLength / 2, -mainHeight / 2 + mainCentroidY ); 305 | this.ctx.lineTo( 0, mainHeight / 2 + mainCentroidY ); 306 | this.ctx.closePath(); 307 | this.ctx.fillStyle = `rgba(0,0,0,${this.animationState + 1})`; // Color of the big triangle 308 | this.ctx.fill(); 309 | } 310 | 311 | drawCenter() { 312 | this.ctx.fillStyle = 'red'; 313 | this.ctx.beginPath(); 314 | this.ctx.arc( this.cw / 2, this.ch / 2, 1, 0, 2 * Math.PI ); 315 | this.ctx.fill(); 316 | } 317 | 318 | showPreloader() { 319 | this.preloader.style.pointerEvents = 'all'; 320 | this.preloader.style.display = 'block'; 321 | this.activePreloader = true; 322 | clearInterval( this.animationInterval ); 323 | this.animationState = 1; 324 | let startTime = Date.now(); 325 | let interval = this.animationInterval = setInterval( () => { 326 | if ( this.animationState <= 0.0 ) { 327 | clearInterval( interval ); 328 | return; 329 | } 330 | this.animationState = 1.0 - this.easeOutCubic( ( Date.now() - startTime ) / 1000 * 1.6 ); 331 | } ); 332 | } 333 | 334 | hidePreloader() { 335 | this.preloader.style.pointerEvents = 'none'; 336 | clearInterval( this.animationInterval ); 337 | this.animationState = 0; 338 | let startTime = Date.now(); 339 | let interval = this.animationInterval = setInterval( () => { 340 | if ( this.animationState >= 1 ) { 341 | clearInterval( interval ); 342 | this.activePreloader = false; 343 | this.preloader.style.display = 'none'; 344 | return; 345 | } 346 | this.animationState = this.expoInOut( ( Date.now() - startTime ) / 1000 ); 347 | } ); 348 | } 349 | 350 | 351 | expoInOut( t ) { 352 | return t === 0 || t === 1 ? t : t < 0.5 ? +0.5 * Math.pow( 2, ( 20 * t ) - 10 ) : -0.5 * Math.pow( 2, 10 - ( t * 20 ) ) + 1; 353 | } 354 | 355 | expoIn( t ) { 356 | return t === 0 ? 0 : Math.pow( 2, 10 * ( t - 1 ) ); 357 | } 358 | 359 | expoOut( t ) { 360 | return t === 1 ? t : 1 - Math.pow( 2, -10 * t ); 361 | } 362 | 363 | easeOutCubic( t ) { 364 | return ( --t ) * t * t + 1; 365 | } 366 | 367 | clamp( num, min, max ) { 368 | return num <= min ? min : num >= max ? max : num; 369 | } 370 | 371 | remap( value, low1, high1, low2, high2 ) { 372 | return low2 + ( value - low1 ) * ( high2 - low2 ) / ( high1 - low1 ); 373 | } 374 | } 375 | 376 | new Preloader() 377 | -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | import './preloader.js' 2 | 3 | import Experience from './Experience/Experience.js' 4 | 5 | const experience = new Experience(document.querySelector('canvas.webgl')) 6 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | * 2 | { 3 | margin: 0; 4 | padding: 0; 5 | 6 | -ms-touch-action: none; 7 | touch-action: none; 8 | } 9 | 10 | html, 11 | body 12 | { 13 | overflow: hidden; 14 | background-color: black; 15 | } 16 | 17 | .webgl 18 | { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | outline: none; 23 | } 24 | 25 | 26 | /********************* Gizmo *********************/ 27 | gizmo-helper { 28 | position: fixed; 29 | bottom: 0; 30 | left: 0; 31 | z-index: 100000; 32 | } 33 | 34 | gizmo-helper:hover { 35 | background: rgba(255, 255, 255, .2); 36 | border-radius: 100%; 37 | cursor: pointer; 38 | } 39 | /********************* End Gizmo *********************/ 40 | 41 | #preloader { 42 | z-index: 9999; 43 | width: 100%; 44 | height: 100%; 45 | display: block; 46 | position: absolute; 47 | } 48 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/.gitkeep -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/favicon.png -------------------------------------------------------------------------------- /static/images/icons/favicon--.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/images/icons/favicon--.png -------------------------------------------------------------------------------- /static/images/icons/favicon--.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/images/icons/favicon--.webp -------------------------------------------------------------------------------- /static/images/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/images/icons/favicon.png -------------------------------------------------------------------------------- /static/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/models/.gitkeep -------------------------------------------------------------------------------- /static/textures/displacement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterPrada/particles/baea4e2295556865d510b9b46ace887d9d630993/static/textures/displacement.jpg -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl' 2 | import basicSsl from '@vitejs/plugin-basic-ssl' 3 | import path from 'path' 4 | import Terminal from 'vite-plugin-terminal' 5 | 6 | 7 | const dirname = path.resolve() 8 | 9 | const isCodeSandbox = 'SANDBOX_URL' in process.env || 'CODESANDBOX_HOST' in process.env 10 | 11 | export default { 12 | root: 'src/', 13 | publicDir: '../static/', 14 | base: './', 15 | resolve: 16 | { 17 | alias: 18 | { 19 | '@' : path.resolve(dirname, './src/Experience/'), 20 | } 21 | }, 22 | server: 23 | { 24 | host: true, 25 | open: !isCodeSandbox // Open if it's not a CodeSandbox 26 | }, 27 | build: 28 | { 29 | outDir: '../dist', 30 | emptyOutDir: true, 31 | sourcemap: true 32 | }, 33 | plugins: 34 | [ 35 | glsl(), 36 | basicSsl(), 37 | // Terminal({ 38 | // console: 'terminal', 39 | // output: ['terminal', 'console'] 40 | // }) 41 | ] 42 | } 43 | --------------------------------------------------------------------------------