├── .gitignore ├── README.md ├── app ├── effect1 │ └── page.tsx ├── effect2 │ └── page.tsx ├── effect3 │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── assets ├── depth-1.png ├── depth-2.png ├── depth-3.png ├── edge-2.png ├── raw-1.png ├── raw-2.png └── raw-3.jpg ├── components ├── canvas.tsx ├── layout.tsx └── post-processing.tsx ├── context └── index.tsx ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── preview.mp4 └── 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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGPU Scanning Effect with Depth Maps 2 | 3 | A WebGPU-powered visual experiment that explores a scanning effect using a depth map and procedural masking, rendered with custom shaders and animated in real time. 4 | 5 | ![Image Title](https://tympanus.net/codrops/wp-content/uploads/2025/03/scaneffect_featured.jpg) 6 | 7 | 10 | 11 | [Article on Codrops](https://tympanus.net/codrops/?p=90674) 12 | 13 | [Demo](https://tympanus.net/Development/ScanEffect/) 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | yarn dev 21 | # or 22 | pnpm dev 23 | # or 24 | bun dev 25 | ``` 26 | 27 | ## License 28 | 29 | [MIT](LICENSE) 30 | -------------------------------------------------------------------------------- /app/effect1/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { WebGPUCanvas } from '@/components/canvas'; 4 | import { useAspect, useTexture } from '@react-three/drei'; 5 | import { useFrame } from '@react-three/fiber'; 6 | import { useContext, useMemo } from 'react'; 7 | import { Tomorrow } from 'next/font/google'; 8 | import gsap from 'gsap'; 9 | 10 | import { 11 | abs, 12 | blendScreen, 13 | float, 14 | mod, 15 | mx_cell_noise_float, 16 | oneMinus, 17 | smoothstep, 18 | texture, 19 | uniform, 20 | uv, 21 | vec2, 22 | vec3, 23 | } from 'three/tsl'; 24 | 25 | import * as THREE from 'three/webgpu'; 26 | import { useGSAP } from '@gsap/react'; 27 | import { GlobalContext, ContextProvider } from '@/context'; 28 | import { PostProcessing } from '@/components/post-processing'; 29 | import TEXTUREMAP from '@/assets/raw-1.png'; 30 | import DEPTHMAP from '@/assets/depth-1.png'; 31 | 32 | const tomorrow = Tomorrow({ 33 | weight: '600', 34 | subsets: ['latin'], 35 | }); 36 | 37 | const WIDTH = 1600; 38 | const HEIGHT = 900; 39 | 40 | const Scene = () => { 41 | const { setIsLoading } = useContext(GlobalContext); 42 | 43 | const [rawMap, depthMap] = useTexture([TEXTUREMAP.src, DEPTHMAP.src], () => { 44 | setIsLoading(false); 45 | rawMap.colorSpace = THREE.SRGBColorSpace; 46 | }); 47 | 48 | const { material, uniforms } = useMemo(() => { 49 | const uPointer = uniform(new THREE.Vector2(0)); 50 | const uProgress = uniform(0); 51 | 52 | const strength = 0.01; 53 | 54 | const tDepthMap = texture(depthMap); 55 | 56 | const tMap = texture( 57 | rawMap, 58 | uv().add(tDepthMap.r.mul(uPointer).mul(strength)) 59 | ); 60 | 61 | const aspect = float(WIDTH).div(HEIGHT); 62 | const tUv = vec2(uv().x.mul(aspect), uv().y); 63 | 64 | const tiling = vec2(120.0); 65 | const tiledUv = mod(tUv.mul(tiling), 2.0).sub(1.0); 66 | 67 | const brightness = mx_cell_noise_float(tUv.mul(tiling).div(2)); 68 | 69 | const dist = float(tiledUv.length()); 70 | const dot = float(smoothstep(0.5, 0.49, dist)).mul(brightness); 71 | 72 | const depth = tDepthMap; 73 | 74 | const flow = oneMinus(smoothstep(0, 0.02, abs(depth.sub(uProgress)))); 75 | 76 | const mask = dot.mul(flow).mul(vec3(10, 0, 0)); 77 | 78 | const final = blendScreen(tMap, mask); 79 | 80 | const material = new THREE.MeshBasicNodeMaterial({ 81 | colorNode: final, 82 | }); 83 | 84 | return { 85 | material, 86 | uniforms: { 87 | uPointer, 88 | uProgress, 89 | }, 90 | }; 91 | }, [rawMap, depthMap]); 92 | 93 | const [w, h] = useAspect(WIDTH, HEIGHT); 94 | 95 | useGSAP(() => { 96 | gsap.to(uniforms.uProgress, { 97 | value: 1, 98 | repeat: -1, 99 | duration: 3, 100 | ease: 'power1.out', 101 | }); 102 | }, [uniforms.uProgress]); 103 | 104 | useFrame(({ pointer }) => { 105 | uniforms.uPointer.value = pointer; 106 | }); 107 | 108 | return ( 109 | 110 | 111 | 112 | ); 113 | }; 114 | 115 | const Html = () => { 116 | const { isLoading } = useContext(GlobalContext); 117 | 118 | useGSAP(() => { 119 | if (!isLoading) { 120 | gsap 121 | .timeline() 122 | .to('[data-loader]', { 123 | opacity: 0, 124 | }) 125 | .from('[data-title]', { 126 | yPercent: -100, 127 | stagger: { 128 | each: 0.15, 129 | }, 130 | ease: 'power1.out', 131 | }) 132 | .from('[data-desc]', { 133 | opacity: 0, 134 | yPercent: 100, 135 | }); 136 | } 137 | }, [isLoading]); 138 | 139 | return ( 140 |
141 |
145 |
146 |
147 |
148 |
149 |
155 |
156 | {'Crown of Fire'.split(' ').map((word, index) => { 157 | return ( 158 |
159 | {word} 160 |
161 | ); 162 | })} 163 |
164 |
165 | 166 |
167 |
The Majesty and Glory of the Young King
168 |
169 |
170 | 171 | 172 | 173 | 174 | 175 |
176 |
177 | ); 178 | }; 179 | 180 | export default function Home() { 181 | return ( 182 | 183 | 184 | 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /app/effect2/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { WebGPUCanvas } from '@/components/canvas'; 4 | import { useAspect, useTexture } from '@react-three/drei'; 5 | import { useFrame } from '@react-three/fiber'; 6 | import { useContext, useMemo } from 'react'; 7 | import { Tomorrow } from 'next/font/google'; 8 | import gsap from 'gsap'; 9 | 10 | import { 11 | abs, 12 | blendScreen, 13 | oneMinus, 14 | smoothstep, 15 | sub, 16 | texture, 17 | uniform, 18 | uv, 19 | vec3, 20 | } from 'three/tsl'; 21 | 22 | import * as THREE from 'three/webgpu'; 23 | import { useGSAP } from '@gsap/react'; 24 | import { PostProcessing } from '@/components/post-processing'; 25 | import { ContextProvider, GlobalContext } from '@/context'; 26 | 27 | import TEXTUREMAP from '@/assets/raw-2.png'; 28 | import DEPTHMAP from '@/assets/depth-2.png'; 29 | import EDGEMAP from '@/assets/edge-2.png'; 30 | 31 | const tomorrow = Tomorrow({ 32 | weight: '600', 33 | subsets: ['latin'], 34 | }); 35 | 36 | const WIDTH = 1600; 37 | const HEIGHT = 900; 38 | 39 | const Scene = () => { 40 | const { setIsLoading } = useContext(GlobalContext); 41 | 42 | const [rawMap, depthMap, edgeMap] = useTexture( 43 | [TEXTUREMAP.src, DEPTHMAP.src, EDGEMAP.src], 44 | () => { 45 | setIsLoading(false); 46 | rawMap.colorSpace = THREE.SRGBColorSpace; 47 | } 48 | ); 49 | 50 | const { material, uniforms } = useMemo(() => { 51 | const uPointer = uniform(new THREE.Vector2(0)); 52 | const uProgress = uniform(0); 53 | 54 | const strength = 0.01; 55 | 56 | const tDepthMap = texture(depthMap); 57 | const tEdgeMap = texture(edgeMap); 58 | 59 | const tMap = texture( 60 | rawMap, 61 | uv().add(tDepthMap.r.mul(uPointer).mul(strength)) 62 | ).mul(0.5); 63 | 64 | const depth = tDepthMap; 65 | 66 | const flow = sub(1, smoothstep(0, 0.02, abs(depth.sub(uProgress)))); 67 | 68 | const mask = oneMinus(tEdgeMap).mul(flow).mul(vec3(10, 0.4, 10)); 69 | 70 | const final = blendScreen(tMap, mask); 71 | 72 | const material = new THREE.MeshBasicNodeMaterial({ 73 | colorNode: final, 74 | }); 75 | 76 | return { 77 | material, 78 | uniforms: { 79 | uPointer, 80 | uProgress, 81 | }, 82 | }; 83 | }, [rawMap, depthMap, edgeMap]); 84 | 85 | const [w, h] = useAspect(WIDTH, HEIGHT); 86 | 87 | useGSAP(() => { 88 | gsap.to(uniforms.uProgress, { 89 | value: 1, 90 | repeat: -1, 91 | duration: 3, 92 | ease: 'power1.out', 93 | }); 94 | }, [uniforms.uProgress]); 95 | 96 | useFrame(({ pointer }) => { 97 | uniforms.uPointer.value = pointer; 98 | }); 99 | 100 | return ( 101 | 102 | 103 | 104 | ); 105 | }; 106 | 107 | const Html = () => { 108 | const { isLoading } = useContext(GlobalContext); 109 | 110 | useGSAP(() => { 111 | if (!isLoading) { 112 | gsap 113 | .timeline() 114 | .to('[data-loader]', { 115 | opacity: 0, 116 | }) 117 | .from('[data-title]', { 118 | yPercent: -100, 119 | stagger: { 120 | each: 0.15, 121 | }, 122 | ease: 'power1.out', 123 | }) 124 | .from('[data-desc]', { 125 | opacity: 0, 126 | yPercent: 100, 127 | }); 128 | } 129 | }, [isLoading]); 130 | 131 | return ( 132 |
133 |
137 |
138 |
139 |
140 |
141 |
147 |
148 | {'Neon Horizon'.split(' ').map((word, index) => { 149 | return ( 150 |
151 | {word} 152 |
153 | ); 154 | })} 155 |
156 |
157 | 158 |
159 |
160 |
A city consumed by light and shadow,
161 |
where one endless road leads to an uncertain future.
162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 | 170 |
171 |
172 | ); 173 | }; 174 | 175 | export default function Page() { 176 | return ( 177 | 178 | 179 | 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /app/effect3/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { WebGPUCanvas } from '@/components/canvas'; 4 | import { useAspect, useTexture } from '@react-three/drei'; 5 | import { useFrame } from '@react-three/fiber'; 6 | import { useContext, useMemo } from 'react'; 7 | import { Tomorrow } from 'next/font/google'; 8 | import gsap from 'gsap'; 9 | 10 | import { 11 | abs, 12 | blendScreen, 13 | float, 14 | Fn, 15 | max, 16 | mod, 17 | oneMinus, 18 | select, 19 | ShaderNodeObject, 20 | smoothstep, 21 | sub, 22 | texture, 23 | uniform, 24 | uv, 25 | vec2, 26 | vec3, 27 | } from 'three/tsl'; 28 | 29 | import * as THREE from 'three/webgpu'; 30 | import { useGSAP } from '@gsap/react'; 31 | import { PostProcessing } from '@/components/post-processing'; 32 | import { ContextProvider, GlobalContext } from '@/context'; 33 | 34 | import TEXTUREMAP from '@/assets/raw-3.jpg'; 35 | import DEPTHMAP from '@/assets/depth-3.png'; 36 | 37 | const tomorrow = Tomorrow({ 38 | weight: '600', 39 | subsets: ['latin'], 40 | }); 41 | 42 | const WIDTH = 1226; 43 | const HEIGHT = 650; 44 | 45 | const sdCross = Fn( 46 | ([p_immutable, b_immutable, r_immutable]: ShaderNodeObject[]) => { 47 | const r = float(r_immutable).toVar(); 48 | const b = vec2(b_immutable).toVar(); 49 | const p = vec2(p_immutable).toVar(); 50 | p.assign(abs(p)); 51 | p.assign(select(p.y.greaterThan(p.x), p.yx, p.xy)); 52 | const q = vec2(p.sub(b)).toVar(); 53 | const k = float(max(q.y, q.x)).toVar(); 54 | const w = vec2( 55 | select(k.greaterThan(0.0), q, vec2(b.y.sub(p.x), k.negate())) 56 | ).toVar(); 57 | const d = float(max(w, 0.0).length()).toVar(); 58 | 59 | return select(k.greaterThan(0.0), d, d.negate()).add(r); 60 | } 61 | ); 62 | 63 | const Scene = () => { 64 | const { setIsLoading } = useContext(GlobalContext); 65 | 66 | const [rawMap, depthMap] = useTexture([TEXTUREMAP.src, DEPTHMAP.src], () => { 67 | setIsLoading(false); 68 | rawMap.colorSpace = THREE.SRGBColorSpace; 69 | }); 70 | 71 | const { material, uniforms } = useMemo(() => { 72 | const uPointer = uniform(new THREE.Vector2(0)); 73 | const uProgress = uniform(0); 74 | 75 | const strength = 0.01; 76 | 77 | const tDepthMap = texture(depthMap); 78 | 79 | const tMap = texture( 80 | rawMap, 81 | uv().add(tDepthMap.r.mul(uPointer).mul(strength)) 82 | ).mul(0.5); 83 | 84 | const aspect = float(WIDTH).div(HEIGHT); 85 | const tUv = vec2(uv().x.mul(aspect), uv().y); 86 | 87 | const tiling = vec2(50.0); 88 | const tiledUv = mod(tUv.mul(tiling), 2.0).sub(1.0); 89 | 90 | const dist = sdCross(tiledUv, vec2(0.3, 0.02), 0.0); 91 | const cross = vec3(smoothstep(0.0, 0.02, dist)); 92 | 93 | const depth = oneMinus(tDepthMap); 94 | 95 | const flow = sub(1, smoothstep(0, 0.02, abs(depth.sub(uProgress)))); 96 | 97 | const mask = oneMinus(cross).mul(flow).mul(vec3(10, 10, 10)); 98 | 99 | const final = blendScreen(tMap, mask); 100 | 101 | const material = new THREE.MeshBasicNodeMaterial({ 102 | colorNode: final, 103 | }); 104 | 105 | return { 106 | material, 107 | uniforms: { 108 | uPointer, 109 | uProgress, 110 | }, 111 | }; 112 | }, [rawMap, depthMap]); 113 | 114 | const [w, h] = useAspect(WIDTH, HEIGHT); 115 | 116 | useGSAP(() => { 117 | gsap.to(uniforms.uProgress, { 118 | value: 0.9, 119 | repeat: -1, 120 | duration: 3, 121 | ease: 'power1.out', 122 | }); 123 | }, [uniforms.uProgress]); 124 | 125 | useFrame(({ pointer }) => { 126 | uniforms.uPointer.value = pointer; 127 | }); 128 | 129 | return ( 130 | 131 | 132 | 133 | ); 134 | }; 135 | 136 | const Html = () => { 137 | const { isLoading } = useContext(GlobalContext); 138 | 139 | useGSAP(() => { 140 | if (!isLoading) { 141 | gsap 142 | .timeline() 143 | .to('[data-loader]', { 144 | opacity: 0, 145 | }) 146 | .from('[data-title]', { 147 | yPercent: -100, 148 | stagger: { 149 | each: 0.15, 150 | }, 151 | ease: 'power1.out', 152 | }) 153 | .from('[data-desc]', { 154 | opacity: 0, 155 | yPercent: 100, 156 | }); 157 | } 158 | }, [isLoading]); 159 | 160 | return ( 161 |
162 |
166 |
167 |
168 |
169 |
170 |
176 |
177 | {'Embrace Nature’s Rhythm'.split(' ').map((word, index) => { 178 | return ( 179 |
180 | {word} 181 |
182 | ); 183 | })} 184 |
185 |
186 | 187 |
188 |
189 |
where one endless road leads to an uncertain future.
190 |
191 |
192 |
193 | 194 | 195 | 196 | 197 | 198 |
199 |
200 | ); 201 | }; 202 | 203 | export default function Page() { 204 | return ( 205 | 206 | 207 | 208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3adrabbit/ScanningEffectWithDepthMap/e15817cb790c71694cfbc5926b80159d45c0cd2f/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | body { 4 | font-family: Arial, Helvetica, sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script'; 2 | import type { Metadata } from 'next'; 3 | import './globals.css'; 4 | import { Layout } from '@/components/layout'; 5 | 6 | export const metadata: Metadata = { 7 | title: 'Scanning effect with depth map | Codrops', 8 | description: 'Scanning effect with depth map', 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | 19 |