├── 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 |
214 |
215 | >
216 | )
217 | }
218 |
219 | export default App
220 |
--------------------------------------------------------------------------------