Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@demo/flexbox-yoga-in-webgl": "^0.1.0",
"@demo/floating-diamonds": "^1.0.0",
"@demo/floating-instanced-shoes": "^1.0.0",
"@demo/flow-shield": "^1.0.0",
"@demo/floating-laptop": "^1.0.0",
"@demo/flying-bananas": "^1.0.0",
"@demo/frosted-glass": "^1.0.0",
Expand Down
10 changes: 10 additions & 0 deletions demos/flow-shield/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 140,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": true
}
9 changes: 9 additions & 0 deletions demos/flow-shield/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[![Static](https://img.shields.io/badge/demo-%23646CFF.svg?logo=html5&logoColor=white)](https://pmndrs.github.io/examples/basic-demo)
[![CodeSandbox](https://img.shields.io/badge/codesandbox-040404?logo=codesandbox&logoColor=DBDBDB)](https://codesandbox.io/s/github/pmndrs/examples/tree/main/demos/basic-demo)
[![Stackblitz](https://img.shields.io/badge/stackblitz-fff?logo=Stackblitz&logoColor=1389FD)](https://stackblitz.com/github/pmndrs/examples/tree/main/demos/basic-demo)

```sh
$ npx degit pmndrs/examples/demos/basic-demo
```

![](thumbnail.webp)
13 changes: 13 additions & 0 deletions demos/flow-shield/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions demos/flow-shield/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@demo/flow-shield",
"version": "1.0.0",
"description": "Interactive force-shield playground with hit reactions, presets, and model import.",
"homepage": "https://codesandbox.io/s/github/pmndrs/examples/tree/main/demos/flow-shield",
"keywords": [
"shader",
"shield",
"postprocessing",
"leva"
],
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/postprocessing": "^3.0.4",
"leva": "^0.10.1",
"postprocessing": "^6.38.2",
"react": "19.2.3",
"react-dom": "19.2.3",
"three": "^0.182.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.182.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.4",
"vite": "^5.4.0"
},
"scripts": {
"dev": "vite --host",
"dev3": "e2e-dev $npm_package_name",
"build": "tsc && vite build",
"build2": "tsc && e2e-build $npm_package_name",
"preview": "vite preview"
},
"browserslist": [
">1%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/pmndrs/examples/tree/main/demos/flow-shield"
}
}
Binary file added demos/flow-shield/public/droideka.glb
Binary file not shown.
1 change: 1 addition & 0 deletions demos/flow-shield/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions demos/flow-shield/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import PlaygroundCanvas from './components/playground/PlaygroundCanvas'

export default function App() {
return <PlaygroundCanvas />
}
54 changes: 54 additions & 0 deletions demos/flow-shield/src/components/ForceShield/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Preset } from "../../overlay/OverlayButtons";

export const MAX_HITS = 6;

export const SHIELD_PRESETS: Record<Preset, Record<string, unknown>> = {
default: {
color: "#26aeff",
opacity: 0.76,
showHex: true,
hexScale: 3.0,
hexOpacity: 0.13,
edgeWidth: 0.06,
fresnelPower: 1.8,
fresnelStrength: 1.75,
flashSpeed: 0.6,
flashIntensity: 0.11,
noiseScale: 1.3,
noiseEdgeColor: "#26aeff",
noiseEdgeWidth: 0.02,
noiseEdgeIntensity: 10.0,
noiseEdgeSmoothness: 0.5,
flowScale: 2.4,
flowSpeed: 1.13,
flowIntensity: 4,
hitRingSpeed: 1.75,
hitRingWidth: 0.12,
hitMaxRadius: 0.85,
fadeStart: -1,
},
droideka: {
hexScale: 3,
hexOpacity: 0.27,
showHex: false,
edgeWidth: 0.2,
fresnelPower: 1.8,
fadeStart:1,
fresnelStrength: 1.75,
flashSpeed: 0.6,
color:"#5992f7",
noiseEdgeColor:"#7faaf5",
noiseEdgeWidth: 0.1,
noiseEdgeIntensity: 0.6,
noiseEdgeSmoothness: 0.5,
noiseScale: 1,
opacity:0.29,
flashIntensity: 0.11,
flowScale: 6.2,
flowSpeed: 1.08,
flowIntensity: 4,
hitRingSpeed: 0.8,
hitRingWidth: 0.12,
hitMaxRadius: 2.1,
},
};
149 changes: 149 additions & 0 deletions demos/flow-shield/src/components/ForceShield/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { useRef, useMemo, useEffect, useCallback } from "react";
import { useFrame } from "@react-three/fiber";
import type { ThreeEvent } from "@react-three/fiber";
import * as THREE from "three";
import type { Preset } from "../overlay/OverlayButtons";
import { MAX_HITS, SHIELD_PRESETS } from "./consts";
import { useShieldControls } from "./useShieldControls";
import { createShieldMaterial } from "./shaderMaterial";

interface ShieldProps {
isActive?: boolean;
posYOverride?: number;
preset?: Preset;
}

function Shield({ isActive = false, posYOverride, preset }: ShieldProps) {
const materialRef = useRef<THREE.ShaderMaterial>(null!);
const groupRef = useRef<THREE.Group>(null!);
const revealRef = useRef(1);
const timeRef = useRef(0);
const hitIdxRef = useRef(0);
const lifeRef = useRef(1.0);
const hitDamageRef = useRef(10);

const [controls, setShield] = useShieldControls(lifeRef);
const {
debugAlwaysOn, manualReveal, revealProgress,
posX, posY, posZ, scale, color,
hexScale, hexOpacity, showHex, edgeWidth,
fresnelPower, fresnelStrength, opacity, fadeStart, revealSpeed,
flashSpeed, flashIntensity,
noiseScale, noiseEdgeColor, noiseEdgeWidth, noiseEdgeIntensity, noiseEdgeSmoothness,
flowScale, flowSpeed, flowIntensity,
hitRingSpeed, hitRingWidth, hitDuration, hitIntensity, hitImpactRadius, hitMaxRadius,
hitDamage,
} = controls;

const visible = isActive || debugAlwaysOn;

// Keep hitDamageRef in sync so the click handler always sees latest value
hitDamageRef.current = hitDamage;

// Apply preset values to Leva controls when preset changes
useEffect(() => {
if (preset) setShield(SHIELD_PRESETS[preset]);
}, [preset]);

// ── Shader material ───────────────────────────────────────────────────────
const shieldMaterial = useMemo(() => createShieldMaterial(), []);

if (shieldMaterial && materialRef.current !== shieldMaterial) {
materialRef.current = shieldMaterial;
}

// ── Sync Leva → uniforms ──────────────────────────────────────────────────
useEffect(() => {
if (!materialRef.current) return;
const u = materialRef.current.uniforms;
u.uColor.value.set(color);
u.uHexScale.value = hexScale;
u.uHexOpacity.value = hexOpacity;
u.uShowHex.value = showHex ? 1.0 : 0.0;
u.uEdgeWidth.value = edgeWidth;
u.uFresnelPower.value = fresnelPower;
u.uFresnelStrength.value = fresnelStrength;
u.uOpacity.value = opacity;
u.uFadeStart.value = fadeStart;
u.uFlashSpeed.value = flashSpeed;
u.uFlashIntensity.value = flashIntensity;
u.uNoiseScale.value = noiseScale;
u.uNoiseEdgeColor.value.set(noiseEdgeColor);
u.uNoiseEdgeWidth.value = noiseEdgeWidth;
u.uNoiseEdgeIntensity.value = noiseEdgeIntensity;
u.uNoiseEdgeSmoothness.value = noiseEdgeSmoothness;
u.uFlowScale.value = flowScale;
u.uFlowSpeed.value = flowSpeed;
u.uFlowIntensity.value = flowIntensity;
u.uHitRingSpeed.value = hitRingSpeed;
u.uHitRingWidth.value = hitRingWidth;
u.uHitMaxRadius.value = hitMaxRadius;
u.uHitDuration.value = hitDuration;
u.uHitIntensity.value = hitIntensity;
u.uHitImpactRadius.value = hitImpactRadius;
}, [
color, hexScale, hexOpacity, showHex, edgeWidth,
fresnelPower, fresnelStrength, opacity, fadeStart,
flashSpeed, flashIntensity,
noiseScale, noiseEdgeColor, noiseEdgeWidth, noiseEdgeIntensity, noiseEdgeSmoothness,
flowScale, flowSpeed, flowIntensity,
hitRingSpeed, hitRingWidth, hitMaxRadius, hitDuration, hitIntensity, hitImpactRadius,
]);

// ── Click → spawn hit in ring buffer ─────────────────────────────────────
const handleClick = useCallback((e: ThreeEvent<MouseEvent>) => {
e.stopPropagation();
if (!materialRef.current) return;

// e.point is world-space; worldToLocal gives object space,
// matching vObjPos (position attribute) in the vertex shader.
const localPoint = e.object.worldToLocal(e.point.clone());

const idx = hitIdxRef.current % MAX_HITS;
hitIdxRef.current++;

const u = materialRef.current.uniforms;
u.uHitPos.value[idx].copy(localPoint);
u.uHitTime.value[idx] = timeRef.current;

lifeRef.current = Math.max(0, lifeRef.current - hitDamageRef.current / 100);
}, []);

// ── Per-frame ─────────────────────────────────────────────────────────────
useFrame((state, delta) => {
if (!materialRef.current) return;

timeRef.current = state.clock.elapsedTime;
materialRef.current.uniforms.uTime.value = timeRef.current;
materialRef.current.uniforms.uLife.value = lifeRef.current;

if (manualReveal) {
revealRef.current = revealProgress;
} else {
const target = visible ? 0 : 1;
revealRef.current = THREE.MathUtils.lerp(
revealRef.current,
target,
1 - Math.exp(-revealSpeed * delta)
);
if ( visible && revealRef.current < 0.005) revealRef.current = 0;
if (!visible && revealRef.current > 0.995) revealRef.current = 1;
}

materialRef.current.uniforms.uReveal.value = revealRef.current;
materialRef.current.visible = revealRef.current < 1;
});

return (
<group ref={groupRef} position={[posX, posYOverride ?? posY, posZ]} scale={[scale, scale, scale]}>
<mesh onClick={handleClick}>
<sphereGeometry args={[1.8, 64, 64]} />
<primitive object={shieldMaterial} attach="material" />
</mesh>
</group>
);
}

export default Shield;
Loading
Loading