├── src ├── vite-env.d.ts ├── App.css ├── main.tsx └── App.tsx ├── README.md ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json └── public └── vite.svg /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # interactive-shield 2 | Interactive shield shader using three js (r3f) 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interactive-shield", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^9.112.0", 14 | "@react-three/fiber": "^8.17.7", 15 | "leva": "^0.9.35", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "three": "^0.168.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.9.0", 22 | "@types/react": "^18.3.3", 23 | "@types/react-dom": "^18.3.0", 24 | "@vitejs/plugin-react": "^4.3.1", 25 | "eslint": "^9.9.0", 26 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 27 | "eslint-plugin-react-refresh": "^0.4.9", 28 | "globals": "^15.9.0", 29 | "typescript": "^5.5.3", 30 | "typescript-eslint": "^8.0.1", 31 | "vite": "^5.4.1" 32 | }, 33 | "overrides": { 34 | "leva": { 35 | "@radix-ui/react-portal": "1.0.2" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas, useThree } from '@react-three/fiber' 2 | import './App.css' 3 | import { BakeShadows, CameraControls, TransformControls, useDepthBuffer } from '@react-three/drei' 4 | import { useControls } from 'leva'; 5 | import { useEffect, useMemo, useRef } from 'react'; 6 | import { Color, DoubleSide, NormalBlending, ShaderMaterial, Vector2 } from 'three'; 7 | 8 | 9 | const vertexShader = ` 10 | varying vec2 vUv; 11 | varying vec3 vNormal; 12 | varying vec3 vPosition; 13 | void main() { 14 | vUv = uv; 15 | vec4 worldPos = modelMatrix * vec4(position, 1.0); 16 | vec4 modelNormal = modelMatrix * vec4(normal, 0.0); 17 | vec4 mvPosition = viewMatrix * worldPos; 18 | gl_Position = projectionMatrix * mvPosition; 19 | vNormal = modelNormal.xyz; 20 | vPosition = worldPos.xyz; 21 | } 22 | `; 23 | 24 | const fragmentShader = ` 25 | uniform sampler2D uDepthTexture; 26 | uniform vec2 uResolution; 27 | uniform float uNear; 28 | uniform float uFar; 29 | uniform vec3 uShieldColor; 30 | uniform vec3 uRimColor; 31 | 32 | varying vec2 vUv; 33 | varying vec3 vNormal; 34 | varying vec3 vPosition; 35 | 36 | #include 37 | 38 | 39 | float LinearizeDepth(float depth) { 40 | float zNdc = 2.0 * depth - 1.0; 41 | float zEye = (2.0 * uFar * uNear) / ((uFar + uNear) - zNdc * (uFar - uNear)); 42 | float linearDepth = (zEye - uNear) / (uFar - uNear); 43 | return linearDepth; 44 | } 45 | 46 | void main() { 47 | 48 | vec3 normal = normalize(vNormal); 49 | if(gl_FrontFacing) { 50 | normal *= -1.0; 51 | } 52 | 53 | vec3 viewDirection = normalize(cameraPosition - vPosition); 54 | float fresnel = 1. + dot(normal, viewDirection); 55 | fresnel = pow(fresnel, 4.0); 56 | 57 | vec2 worldCoords = gl_FragCoord.xy/uResolution; 58 | 59 | float sceneDepth = LinearizeDepth(texture2D(uDepthTexture, worldCoords).r); 60 | float bubbleDepth = LinearizeDepth(gl_FragCoord.z); 61 | 62 | float difference = abs( sceneDepth - bubbleDepth); 63 | float threshold = 0.0001; 64 | float normalizedDistance = clamp(difference / threshold, 0.0, 1.0); 65 | vec4 intersection = mix(vec4(1.0), vec4(0.0), normalizedDistance) ; 66 | intersection.rgb *= uRimColor; 67 | vec4 color = vec4(uShieldColor, 0.3); 68 | gl_FragColor = color + intersection + vec4(uRimColor, 1.0) * fresnel ; 69 | } 70 | `; 71 | 72 | const rimColorImpl = new Color("#fff"); 73 | const shieldColorImpl = new Color("#f00"); 74 | 75 | const Shield = () => { 76 | const db = useDepthBuffer({ size: 1024 }); 77 | 78 | const { radius, rimColor, shieldColor } = useControls({ 79 | radius: { 80 | value: 3, 81 | max: 5, 82 | min: 1, 83 | }, 84 | shieldColor: { 85 | value: "#ff0000", 86 | }, 87 | rimColor: { 88 | value: "#ffffff", 89 | } 90 | }); 91 | 92 | const size = useThree((state) => state.size); 93 | const dpr = useThree((state) => state.viewport.dpr); 94 | const camera = useThree((state) => state.camera); 95 | 96 | const uniforms = useMemo( 97 | () => ({ 98 | uDepthTexture: { 99 | value: db, 100 | }, 101 | uResolution: { 102 | value: new Vector2(size.width * dpr, size.height * dpr), 103 | }, 104 | uNear: { 105 | value: camera.near, 106 | }, 107 | uFar: { 108 | value: camera.far, 109 | }, 110 | uShieldColor: { 111 | value: shieldColorImpl.setStyle(shieldColor), 112 | }, 113 | uRimColor: { 114 | value: rimColorImpl.setStyle(rimColor), 115 | } 116 | }), 117 | [db, size, camera] 118 | ); 119 | 120 | const materialRef = useRef(null!); 121 | 122 | useEffect(() => { 123 | 124 | materialRef.current.uniforms.uShieldColor.value = shieldColorImpl.setStyle(shieldColor); 125 | materialRef.current.uniforms.uRimColor.value = rimColorImpl.setStyle(rimColor); 126 | }, [rimColor, shieldColor]); 127 | 128 | return ( 129 | <> 130 | 131 | 132 | 133 | 143 | 144 | 145 | 146 | ); 147 | }; 148 | 149 | 150 | const World = () => { 151 | const dist = 3; 152 | return ( 153 | <> 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | ) 176 | } 177 | 178 | 179 | function App() { 180 | 181 | return ( 182 | <> 183 |
188 | 200 | 201 | 202 | 209 | 210 | 211 | 212 | 213 | 214 |
215 | 216 | ) 217 | } 218 | 219 | export default App 220 | --------------------------------------------------------------------------------