├── .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 | 
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 |
4 |
--------------------------------------------------------------------------------
/public/assets/svg/threejs.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
--------------------------------------------------------------------------------