├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── icons │ │ └── github.svg │ ├── svg │ │ └── threejs.svg │ └── textures │ │ ├── SurfaceImperfections003_1K_Normal.jpg │ │ └── SurfaceImperfections003_1K_var1.jpg ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── components │ ├── App.tsx │ ├── LinkIconButton.tsx │ └── three │ │ ├── TCanvas.tsx │ │ └── postprocessing │ │ ├── BloomPass.tsx │ │ ├── Effects.tsx │ │ ├── FXAAPass.tsx │ │ ├── FlowmapPass.tsx │ │ └── simulator.ts ├── index.css ├── index.tsx ├── modules │ └── gui.ts ├── react-app-env.d.ts ├── reportWebVitals.ts └── types │ └── postprocessing.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | This application was inspired by the [Alien.js](https://github.com/pschroen/alien.js) [flowmap](https://alien.js.org/examples/shader_flowmap_rgbshift.html) by [Patrick Schroen](https://twitter.com/pschroen). 3 | 4 | https://nemutas.github.io/r3f-flowmap/ 5 | 6 | ![output(video-cutter-js com) (2)](https://user-images.githubusercontent.com/46724121/162637521-d3bff08f-3b1f-42fa-99b3-ab8ab185c8e8.gif) 7 | 8 | # Technology 9 | 10 | - TypeScript 11 | - React(Create React App) 12 | - React Three Fiber(Three.js) 13 | - Postprocessing 14 | - GPGPU 15 | 16 | # Reference 17 | The scene was created using the following CodeSandbox as a reference.
18 | However, the description method is out of date. For the latest description method, please check the document. 19 | 20 | * [Reflectorplanes and bloom](https://codesandbox.io/s/reflectorplanes-and-bloom-jflps) 21 | * [MeshReflectorMaterial](https://github.com/pmndrs/drei#meshreflectormaterial) 22 | 23 | # License 24 | 25 | This source code is not MIT License. 26 | 27 | ❌ Commercial use is prohibited.
28 | ❌ Redistribution is prohibited.
29 | ❌ Diversion is prohibited.(Incorporate all of the code into the project, etc.)
30 | ✅ You can look at the application and reproduce the representation.
31 | ✅ You can use parts of the code. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r3f-flowmap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://nemutas.github.io/r3f-flowmap/", 6 | "dependencies": { 7 | "@emotion/css": "^11.9.0", 8 | "@react-three/drei": "^8.20.2", 9 | "@react-three/fiber": "^7.0.27", 10 | "@testing-library/jest-dom": "^5.16.4", 11 | "@testing-library/react": "^12.1.4", 12 | "@testing-library/user-event": "^13.5.0", 13 | "@types/jest": "^27.4.1", 14 | "@types/node": "^16.11.26", 15 | "@types/react": "^17.0.44", 16 | "@types/react-dom": "^17.0.14", 17 | "lil-gui": "^0.16.1", 18 | "react": "^18.0.0", 19 | "react-dom": "^18.0.0", 20 | "react-scripts": "5.0.0", 21 | "three": "^0.139.2", 22 | "typescript": "^4.6.3", 23 | "web-vitals": "^2.1.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "deploy": "npm run build && gh-pages -d build" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/three": "^0.139.0", 50 | "gh-pages": "^3.2.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/svg/threejs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/textures/SurfaceImperfections003_1K_Normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemutas/r3f-flowmap/77a3212c2cd7552daf1cce685b31418cbcd3e4e1/public/assets/textures/SurfaceImperfections003_1K_Normal.jpg -------------------------------------------------------------------------------- /public/assets/textures/SurfaceImperfections003_1K_var1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemutas/r3f-flowmap/77a3212c2cd7552daf1cce685b31418cbcd3e4e1/public/assets/textures/SurfaceImperfections003_1K_var1.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemutas/r3f-flowmap/77a3212c2cd7552daf1cce685b31418cbcd3e4e1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemutas/r3f-flowmap/77a3212c2cd7552daf1cce685b31418cbcd3e4e1/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemutas/r3f-flowmap/77a3212c2cd7552daf1cce685b31418cbcd3e4e1/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { VFC } from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { LinkIconButton } from './LinkIconButton'; 4 | import { TCanvas } from './three/TCanvas'; 5 | 6 | export const App: VFC = () => { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | 15 | const styles = { 16 | container: css` 17 | position: relative; 18 | width: 100vw; 19 | height: 100vh; 20 | ` 21 | } 22 | -------------------------------------------------------------------------------- /src/components/LinkIconButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, VFC } from 'react'; 2 | 3 | type LinkIconButtonProps = { 4 | /** 5 | * Resource path directly under the public folder. 6 | * @example '/assets/icons/github.svg' 7 | */ 8 | imagePath: string 9 | /** 10 | * @example 'https://github.com' 11 | */ 12 | linkPath: string 13 | /** 14 | * @default 'bottom-right' 15 | */ 16 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 17 | /** 18 | * @default [50, 50] - width:50px, height:50px 19 | */ 20 | size?: [number, number] 21 | } 22 | 23 | export const LinkIconButton: VFC = props => { 24 | const { imagePath, linkPath, position = 'bottom-right', size = [50, 50] } = props 25 | const [hover, setHover] = useState(false) 26 | 27 | const publicImagePath = process.env.PUBLIC_URL + imagePath 28 | 29 | let positionStyle 30 | switch (position) { 31 | case 'top-left': 32 | positionStyle = styles.topLeft 33 | break 34 | case 'top-right': 35 | positionStyle = styles.topRight 36 | break 37 | case 'bottom-left': 38 | positionStyle = styles.bottomLeft 39 | break 40 | default: 41 | positionStyle = styles.bottomRight 42 | } 43 | 44 | return ( 45 | setHover(true)} 51 | onMouseLeave={() => setHover(false)}> 52 | 53 | 54 | ) 55 | } 56 | 57 | // ======================================================== 58 | // styles 59 | 60 | type Styles = { [key in string]: React.CSSProperties } 61 | 62 | const temp: Styles = { 63 | container: { 64 | position: 'fixed', 65 | bottom: '0', 66 | right: '0', 67 | fontSize: '0' 68 | } 69 | } 70 | 71 | const styles: Styles = { 72 | topLeft: { 73 | ...temp.container, 74 | top: '10px', 75 | left: '10px' 76 | }, 77 | topRight: { 78 | ...temp.container, 79 | top: '10px', 80 | right: '10px' 81 | }, 82 | bottomLeft: { 83 | ...temp.container, 84 | bottom: '10px', 85 | left: '10px' 86 | }, 87 | bottomRight: { 88 | ...temp.container, 89 | bottom: '10px', 90 | right: '10px' 91 | }, 92 | img: { 93 | objectFit: 'cover', 94 | opacity: '0.5', 95 | transform: 'rotate(0deg)', 96 | transition: 'all 0.3s' 97 | } 98 | } 99 | 100 | const hoverStyles: Styles = { 101 | img: { 102 | ...styles.img, 103 | opacity: '1', 104 | transform: 'rotate(360deg)' 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/three/TCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useMemo, useRef, VFC } from 'react'; 2 | import * as THREE from 'three'; 3 | import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader'; 4 | import { MeshReflectorMaterial, OrbitControls, useTexture } from '@react-three/drei'; 5 | import { Canvas, useFrame, useLoader } from '@react-three/fiber'; 6 | import { BloomPass } from './postprocessing/BloomPass'; 7 | import { Effects } from './postprocessing/Effects'; 8 | import { FlowmapPass } from './postprocessing/FlowmapPass'; 9 | import { FXAAPass } from './postprocessing/FXAAPass'; 10 | 11 | export const TCanvas: VFC = () => { 12 | return ( 13 | 23 | {/* scene */} 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {/* helper */} 43 | {/* */} 44 | {/* */} 45 | 46 | ) 47 | } 48 | 49 | const SVGMesh: VFC = () => { 50 | const ref = useRef(null) 51 | const { 52 | paths: [path] 53 | } = useLoader(SVGLoader, process.env.PUBLIC_URL + '/assets/svg/threejs.svg') 54 | 55 | const geometries = useMemo(() => { 56 | const geometries: THREE.BufferGeometry[] = [] 57 | const bX = [Number.MAX_VALUE, Number.MIN_VALUE] 58 | const bY = [Number.MAX_VALUE, Number.MIN_VALUE] 59 | const bZ = [Number.MAX_VALUE, Number.MIN_VALUE] 60 | path.subPaths.forEach(p => { 61 | const geometry = SVGLoader.pointsToStroke(p.getPoints(), path.userData!.style) 62 | geometry.computeBoundingBox() 63 | const { min, max } = geometry.boundingBox! 64 | bX[0] = Math.min(bX[0], min.x) 65 | bY[0] = Math.min(bY[0], min.y) 66 | bZ[0] = Math.min(bZ[0], min.z) 67 | bX[1] = Math.max(bX[1], max.x) 68 | bY[1] = Math.max(bY[1], max.y) 69 | bZ[1] = Math.max(bZ[1], max.z) 70 | geometries.push(geometry) 71 | }) 72 | 73 | const [offsetX, offsetY, offsetZ] = [(bX[1] + bX[0]) / 2, (bY[1] + bY[0]) / 2, (bZ[1] + bZ[0]) / 2] 74 | geometries.forEach(geometry => { 75 | geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-offsetX, -offsetY, -offsetZ)) 76 | geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(Math.PI)) 77 | geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(Math.PI)) 78 | }) 79 | 80 | return geometries 81 | }, [path]) 82 | 83 | useFrame(({ clock }) => { 84 | ref.current!.position.y = 0.7 * Math.sin(clock.getElapsedTime() * 0.3) + 1.0 85 | }) 86 | 87 | return ( 88 | 89 | {geometries.map((geometry, i) => ( 90 | 91 | 92 | 93 | ))} 94 | 95 | ) 96 | } 97 | 98 | const Floor: VFC = () => { 99 | const filePath = (name: string) => process.env.PUBLIC_URL + `/assets/textures/SurfaceImperfections003_1K_${name}.jpg` 100 | const [roughness] = useTexture([filePath('var1')]) 101 | 102 | return ( 103 | 104 | 105 | 121 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/components/three/postprocessing/BloomPass.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, VFC } from 'react'; 2 | import * as THREE from 'three'; 3 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'; 4 | import { extend, useFrame } from '@react-three/fiber'; 5 | import { GUIController } from '../../../modules/gui'; 6 | 7 | extend({ UnrealBloomPass }) 8 | 9 | const datas = { 10 | enabled: true, 11 | exposure: 0.8, 12 | strength: 0.5, 13 | radius: 1.45, 14 | threshold: 0.15 15 | } 16 | 17 | export const BloomPass: VFC = () => { 18 | const passRef = useRef(null) 19 | 20 | // overwrite default 21 | const gui = GUIController.instance.setFolder('Bloom').open(false) 22 | gui.addCheckBox(datas, 'enabled') 23 | gui.addNumericSlider(datas, 'exposure', 0.1, 2, 0.01) 24 | gui.addNumericSlider(datas, 'strength', 0, 10, 0.1) 25 | gui.addNumericSlider(datas, 'radius', 0, 2, 0.01) 26 | gui.addNumericSlider(datas, 'threshold', 0, 1, 0.01) 27 | 28 | const update = (gl: THREE.WebGLRenderer) => { 29 | passRef.current!.enabled = datas.enabled 30 | gl.toneMappingExposure = datas.enabled ? Math.pow(datas.exposure, 4.0) : 1 31 | 32 | if (datas.enabled) { 33 | passRef.current!.strength = datas.strength 34 | passRef.current!.radius = datas.radius 35 | passRef.current!.threshold = datas.threshold 36 | } 37 | } 38 | 39 | useFrame(({ gl }) => { 40 | update(gl) 41 | }) 42 | 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /src/components/three/postprocessing/Effects.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, VFC } from 'react'; 2 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'; 3 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'; 4 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; 5 | import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader'; 6 | import { extend, useFrame, useThree } from '@react-three/fiber'; 7 | 8 | extend({ EffectComposer, RenderPass, ShaderPass }) 9 | 10 | type EffectsProps = { 11 | children: React.ReactNode 12 | sRGBCorrection?: boolean 13 | } 14 | 15 | export const Effects: VFC = props => { 16 | const { children, sRGBCorrection } = props 17 | 18 | const composerRef = useRef(null) 19 | const { gl, scene, camera, size } = useThree() 20 | 21 | useEffect(() => { 22 | composerRef.current!.setSize(size.width, size.height) 23 | }, [size]) 24 | 25 | useFrame(() => { 26 | composerRef.current!.render() 27 | }, 1) 28 | 29 | return ( 30 | 31 | 32 | {sRGBCorrection && } 33 | {children} 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/three/postprocessing/FXAAPass.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, VFC } from 'react'; 2 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; 3 | import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader'; 4 | import { extend, useFrame, useThree } from '@react-three/fiber'; 5 | import { GUIController } from '../../../modules/gui'; 6 | 7 | extend({ ShaderPass }) 8 | 9 | const datas = { 10 | enabled: true 11 | } 12 | 13 | export const FXAAPass: VFC = () => { 14 | const passRef = useRef(null) 15 | const { size } = useThree() 16 | 17 | const gui = GUIController.instance.setFolder('FXAA').open(false) 18 | gui.addCheckBox(datas, 'enabled') 19 | 20 | const update = () => { 21 | const pass = passRef.current! 22 | pass.enabled = datas.enabled 23 | 24 | if (datas.enabled) { 25 | } 26 | } 27 | 28 | useFrame(() => { 29 | update() 30 | }) 31 | 32 | return ( 33 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/three/postprocessing/FlowmapPass.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef, VFC } from 'react'; 2 | import * as THREE from 'three'; 3 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; 4 | import { useFrame, useThree } from '@react-three/fiber'; 5 | import { GUIController } from '../../../modules/gui'; 6 | import { Simulator } from './simulator'; 7 | 8 | const datas = { 9 | enabled: true, 10 | power: 0.3, 11 | range: 0.1, 12 | viscosity: 0.04, 13 | isPixel: false, 14 | pixel: 20, 15 | rgbShift: true 16 | } 17 | 18 | export const FlowmapPass: VFC = () => { 19 | const passRef = useRef(null) 20 | 21 | const gui = GUIController.instance.setFolder('Flowmap') 22 | gui.addCheckBox(datas, 'enabled') 23 | gui.addNumericSlider(datas, 'power', 0.1, 0.5, 0.01) 24 | gui.addNumericSlider(datas, 'range', 0.1, 0.2, 0.01) 25 | gui.addNumericSlider(datas, 'viscosity', 0.01, 0.1, 0.01) 26 | gui.addCheckBox(datas, 'isPixel', 'Pixel Mode') 27 | gui.addNumericSlider(datas, 'pixel', 10, 50, 10) 28 | gui.addCheckBox(datas, 'rgbShift', 'RGB Shift') 29 | 30 | const { gl, size, viewport } = useThree() 31 | const simulator = useMemo(() => new Simulator(gl, size.width, size.height), [gl, size]) 32 | // pixel effect 33 | // const simulator = useMemo(() => new Simulator(gl, 100, 50), [gl]) 34 | 35 | const shader: THREE.Shader = { 36 | uniforms: { 37 | tDiffuse: { value: null }, 38 | u_motionTexture: { value: null }, 39 | u_powar: { value: datas.power }, 40 | u_aspect: { value: viewport.aspect }, 41 | u_pixelMode: { value: datas.isPixel }, 42 | u_pixel: { value: datas.pixel }, 43 | u_shift: { value: datas.rgbShift } 44 | }, 45 | vertexShader: vertexShader, 46 | fragmentShader: fragmentShader 47 | } 48 | 49 | const normalizedMouse = new THREE.Vector2() 50 | const defPos = new THREE.Vector2(0, 0) 51 | useFrame(({ mouse }) => { 52 | passRef.current!.enabled = datas.enabled 53 | 54 | if (datas.enabled) { 55 | // Normalize to 0 ~ 1 56 | if (mouse.equals(defPos)) { 57 | normalizedMouse.set(0, 0) 58 | } else { 59 | normalizedMouse.set((mouse.x + 1) / 2, (mouse.y + 1) / 2) 60 | } 61 | simulator.compute(normalizedMouse, datas.range, datas.viscosity) 62 | passRef.current!.uniforms.u_motionTexture.value = simulator.texture 63 | passRef.current!.uniforms.u_powar.value = datas.power 64 | passRef.current!.uniforms.u_pixelMode.value = datas.isPixel 65 | passRef.current!.uniforms.u_pixel.value = datas.pixel 66 | passRef.current!.uniforms.u_shift.value = datas.rgbShift 67 | } 68 | }) 69 | 70 | return 71 | } 72 | 73 | const vertexShader = ` 74 | varying vec2 v_uv; 75 | 76 | void main() { 77 | v_uv = uv; 78 | 79 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 80 | } 81 | ` 82 | 83 | const fragmentShader = ` 84 | uniform sampler2D tDiffuse; 85 | uniform sampler2D u_motionTexture; 86 | uniform float u_powar; 87 | uniform float u_aspect; 88 | uniform bool u_pixelMode; 89 | uniform float u_pixel; 90 | uniform bool u_shift; 91 | 92 | varying vec2 v_uv; 93 | 94 | void main() { 95 | vec2 st = v_uv; 96 | if (u_pixelMode) { 97 | vec2 pixel = vec2(u_aspect * u_pixel, u_pixel); 98 | st = floor(v_uv * pixel) / pixel; 99 | } 100 | vec4 motionTexture = texture2D(u_motionTexture, st); 101 | 102 | vec2 distortion = -motionTexture.xy * u_powar; 103 | vec2 uv = v_uv + distortion; 104 | vec4 tex = texture2D(tDiffuse, uv); 105 | 106 | if (u_shift) { 107 | vec2 uv_r = v_uv + distortion * 0.5; 108 | vec2 uv_g = v_uv + distortion * 0.75; 109 | vec2 uv_b = v_uv + distortion * 1.0; 110 | float tex_r = texture2D(tDiffuse, uv_r).r; 111 | float tex_g = texture2D(tDiffuse, uv_g).g; 112 | float tex_b = texture2D(tDiffuse, uv_b).b; 113 | tex = vec4(tex_r, tex_g, tex_b, tex.a); 114 | } 115 | 116 | gl_FragColor = tex; 117 | // gl_FragColor = vec4(motionTexture.xy, 0.0, 1.0); 118 | } 119 | ` 120 | -------------------------------------------------------------------------------- /src/components/three/postprocessing/simulator.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GPUComputationRenderer, Variable } from 'three/examples/jsm/misc/GPUComputationRenderer'; 3 | 4 | export class Simulator { 5 | private _gpuCompute 6 | private _variables: Variable[] = [] 7 | private _material = new THREE.ShaderMaterial() 8 | 9 | constructor(gl: THREE.WebGLRenderer, private _width: number, private _height: number) { 10 | this._gpuCompute = new GPUComputationRenderer(this._width, this._height, gl) 11 | this._setMotionTexture() 12 | this._setVariableDependencies() 13 | this._gpuCompute.init() 14 | } 15 | 16 | private _setMotionTexture = () => { 17 | // set the default position to texture 18 | const dataTexture = this._gpuCompute.createTexture() 19 | const theArray = dataTexture.image.data 20 | 21 | for (let i = 0; i < theArray.length; i += 4) { 22 | theArray[i + 0] = 0 23 | theArray[i + 1] = 0 24 | theArray[i + 2] = 0 25 | theArray[i + 3] = 0 26 | } 27 | 28 | // set fragment shader 29 | const variable = this._gpuCompute.addVariable('motionTexture', fragmentShader, dataTexture) 30 | variable.wrapS = THREE.RepeatWrapping 31 | variable.wrapT = THREE.RepeatWrapping 32 | 33 | // set uniforms 34 | this._material = variable.material 35 | this._material.uniforms['u_defaultTexture'] = { value: dataTexture.clone() } 36 | this._material.uniforms['u_mouse_pos'] = { value: new THREE.Vector2() } 37 | this._material.uniforms['u_range'] = { value: 0 } 38 | this._material.uniforms['u_viscosity'] = { value: 0 } 39 | 40 | // add variable 41 | this._variables.push(variable) 42 | } 43 | 44 | private _setVariableDependencies = () => { 45 | this._variables.forEach(variable => { 46 | this._gpuCompute.setVariableDependencies(variable, this._variables) 47 | }) 48 | // it means. 49 | // this._gpuCompute.setVariableDependencies(positionVariable, [positionVariable, ...]) 50 | } 51 | 52 | compute = (mouse: THREE.Vector2, range: number, viscosity: number) => { 53 | this._material.uniforms.u_mouse_pos.value.copy(mouse) 54 | this._material.uniforms.u_range.value = range 55 | this._material.uniforms.u_viscosity.value = viscosity 56 | this._gpuCompute.compute() 57 | } 58 | 59 | get texture() { 60 | const variable = this._variables.find(v => v.name === 'motionTexture')! 61 | const target = this._gpuCompute.getCurrentRenderTarget(variable) as THREE.WebGLRenderTarget 62 | return target.texture 63 | } 64 | } 65 | 66 | const fragmentShader = ` 67 | uniform sampler2D u_defaultTexture; 68 | uniform vec2 u_mouse_pos; 69 | uniform float u_range; 70 | uniform float u_viscosity; 71 | 72 | vec2 lerp(vec2 original, vec2 target, float alpha) { 73 | return original * alpha + target * (1.0 - alpha); 74 | } 75 | 76 | void main() { 77 | vec2 uv = gl_FragCoord.xy / resolution.xy; 78 | vec4 tmp = texture2D(motionTexture, uv); 79 | vec4 defTmp = texture2D(u_defaultTexture, uv); 80 | 81 | float dist = 1.0 - smoothstep(0.0, u_range, distance(u_mouse_pos, uv)); 82 | 83 | if(0.0 < dist) { 84 | vec2 speed = u_mouse_pos - tmp.zw; 85 | vec2 distortion = speed * dist * 5.0; 86 | tmp.xy += distortion; 87 | } 88 | 89 | vec4 result; 90 | result.xy = lerp(defTmp.xy, tmp.xy, u_viscosity); 91 | result.xy = clamp(result.xy, -1.0, 1.0); 92 | result.zw = u_mouse_pos; 93 | 94 | gl_FragColor = result; 95 | } 96 | ` 97 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import React from 'react'; 3 | import * as ReactDOMClient from 'react-dom/client'; 4 | import { App } from './components/App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOMClient.createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals() 18 | -------------------------------------------------------------------------------- /src/modules/gui.ts: -------------------------------------------------------------------------------- 1 | import GUI from 'lil-gui'; 2 | 3 | export class GUIController { 4 | private static _instance: GUIController | null 5 | private _gui 6 | private _currentFolderName: string | undefined 7 | 8 | private constructor() { 9 | this._gui = new GUI() 10 | } 11 | 12 | static get instance() { 13 | if (!this._instance) { 14 | this._instance = new GUIController() 15 | } 16 | this._instance._currentFolderName = undefined 17 | return this._instance 18 | } 19 | 20 | private _getGui = (folderName: string | undefined) => { 21 | let gui = this._gui 22 | if (folderName) { 23 | gui = this._folder(folderName) 24 | } else if (this._currentFolderName) { 25 | gui = this._folder(this._currentFolderName) 26 | } 27 | return gui 28 | } 29 | 30 | private _folder = (title: string) => { 31 | let folder = this._gui.folders.find(f => f._title === title) 32 | if (!folder) folder = this._gui.addFolder(title) 33 | return folder 34 | } 35 | 36 | private _controller = (gui: GUI, name: string) => { 37 | return gui.controllers.find(c => c._name === name) 38 | } 39 | 40 | setFolder = (name: string) => { 41 | this._currentFolderName = name 42 | return this 43 | } 44 | 45 | open = (open: boolean) => { 46 | this._getGui(this._currentFolderName).open(open) 47 | return this 48 | } 49 | 50 | /** 51 | * add color controls 52 | * @reference https://lil-gui.georgealways.com/#Guide#Colors 53 | */ 54 | addColor = ( 55 | obj: object, 56 | propertyName: string, 57 | rgbScale?: number | undefined, 58 | displayName?: string | undefined, 59 | folderName?: string | undefined 60 | ) => { 61 | const controllerName = displayName ? displayName : propertyName 62 | const gui = this._getGui(folderName) 63 | 64 | let controller = this._controller(gui, controllerName) 65 | if (!controller) { 66 | controller = gui.addColor(obj, propertyName, rgbScale).name(controllerName) 67 | } 68 | return controller 69 | } 70 | 71 | /** 72 | * add numeric slider controls 73 | * @reference https://lil-gui.georgealways.com/#Guide#Numbers-and-Sliders 74 | */ 75 | addNumericSlider = ( 76 | obj: object, 77 | propertyName: string, 78 | min: number, 79 | max: number, 80 | step: number, 81 | displayName?: string | undefined, 82 | folderName?: string | undefined 83 | ) => { 84 | const controllerName = displayName ? displayName : propertyName 85 | const gui = this._getGui(folderName) 86 | 87 | let controller = this._controller(gui, controllerName) 88 | if (!controller) { 89 | controller = gui.add(obj, propertyName, min, max, step).name(controllerName) 90 | } 91 | return controller 92 | } 93 | 94 | /** 95 | * add dropdown controls 96 | * @reference https://lil-gui.georgealways.com/#Guide#Dropdowns 97 | */ 98 | addDropdown = ( 99 | obj: object, 100 | propertyName: string, 101 | list: string[] | { [key: string]: number }, 102 | displayName?: string | undefined, 103 | folderName?: string | undefined 104 | ) => { 105 | const controllerName = displayName ? displayName : propertyName 106 | const gui = this._getGui(folderName) 107 | 108 | let controller = this._controller(gui, controllerName) 109 | if (!controller) { 110 | controller = gui.add(obj, propertyName, list).name(controllerName) 111 | } 112 | return controller 113 | } 114 | 115 | /** 116 | * add Button controls 117 | * @description property given by its property name is a callback method. 118 | * @reference https://lil-gui.georgealways.com/#Guide#Saving 119 | */ 120 | addButton = ( 121 | obj: object, 122 | propertyName: string, 123 | displayName?: string | undefined, 124 | folderName?: string | undefined 125 | ) => { 126 | const controllerName = displayName ? displayName : propertyName 127 | const gui = this._getGui(folderName) 128 | 129 | let controller = this._controller(gui, controllerName) 130 | if (!controller) { 131 | controller = gui.add(obj, propertyName).name(controllerName) 132 | } 133 | return controller 134 | } 135 | 136 | /** 137 | * add CheckBox controls 138 | * @description property given by its property name is type of boolean. 139 | * @reference https://lil-gui.georgealways.com/#Guide#Adding-Controllers 140 | */ 141 | addCheckBox = ( 142 | obj: object, 143 | propertyName: string, 144 | displayName?: string | undefined, 145 | folderName?: string | undefined 146 | ) => { 147 | const controllerName = displayName ? displayName : propertyName 148 | const gui = this._getGui(folderName) 149 | 150 | let controller = this._controller(gui, controllerName) 151 | if (!controller) { 152 | controller = gui.add(obj, propertyName).name(controllerName) 153 | } 154 | return controller 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/types/postprocessing.d.ts: -------------------------------------------------------------------------------- 1 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'; 2 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'; 3 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; 4 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'; 5 | import { ReactThreeFiber } from '@react-three/fiber'; 6 | 7 | declare global { 8 | namespace JSX { 9 | interface IntrinsicElements { 10 | effectComposer: ReactThreeFiber.Node 11 | renderPass: ReactThreeFiber.Node 12 | shaderPass: ReactThreeFiber.Node 13 | unrealBloomPass: ReactThreeFiber.Node 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------