diff --git a/demos/flow-shield/public/droideka-5mb.glb b/demos/flow-shield/public/droideka-5mb.glb
new file mode 100644
index 00000000..d50f0d6e
Binary files /dev/null and b/demos/flow-shield/public/droideka-5mb.glb differ
diff --git a/demos/flow-shield/public/droideka.glb b/demos/flow-shield/public/droideka.glb
deleted file mode 100644
index f26e7bae..00000000
Binary files a/demos/flow-shield/public/droideka.glb and /dev/null differ
diff --git a/demos/flow-shield/src/App.jsx b/demos/flow-shield/src/App.jsx
index 69a9c66a..cb288ec6 100644
--- a/demos/flow-shield/src/App.jsx
+++ b/demos/flow-shield/src/App.jsx
@@ -1,5 +1,77 @@
-import PlaygroundCanvas from './components/playground/PlaygroundCanvas'
+import { useCallback, useRef, useState } from 'react'
+import { Canvas } from '@react-three/fiber'
+import { Leva, useControls } from 'leva'
+import SceneContent from './components/playground/SceneContent'
+import UIOverlay from './components/overlay/UIOverlay'
+import OverlayButtons from './components/overlay/OverlayButtons'
+import LoadingOverlay from './components/overlay/LoadingOverlay'
+import { LEVA_THEME } from './components/theme/theme'
export default function App() {
- return
+ const [showGrid, setShowGrid] = useState(true)
+ const [hideLeva, setHideLeva] = useState(false)
+ const [glbUrl, setGlbUrl] = useState(null)
+ const [preset, setPreset] = useState('default')
+ const [isLoadingModel, setIsLoadingModel] = useState(false)
+ const glbUrlRef = useRef(null)
+
+ const handleLoadGlb = useCallback((file) => {
+ if (glbUrlRef.current) URL.revokeObjectURL(glbUrlRef.current)
+ const url = URL.createObjectURL(file)
+ glbUrlRef.current = url
+ setIsLoadingModel(true)
+ setGlbUrl(url)
+ }, [])
+
+ const handleModelLoaded = useCallback(() => {
+ setIsLoadingModel(false)
+ }, [])
+
+ const handleClearGlb = useCallback(() => {
+ if (glbUrlRef.current) URL.revokeObjectURL(glbUrlRef.current)
+ glbUrlRef.current = null
+ setGlbUrl(null)
+ }, [])
+
+ const { mode } = useControls(
+ 'Scene',
+ {
+ mode: {
+ value: 'Background',
+ options: ['Background', 'Frame'],
+ label: 'Mode'
+ }
+ },
+ { collapsed: true }
+ )
+
+ return (
+ <>
+
+
+
+
+
+ setShowGrid((value) => !value)}
+ hideLeva={hideLeva}
+ onToggleLeva={() => setHideLeva((value) => !value)}
+ hasGlb={glbUrl !== null}
+ onLoadGlb={handleLoadGlb}
+ onClearGlb={handleClearGlb}
+ preset={preset}
+ onSetPreset={setPreset}
+ />
+
+ >
+ )
}
diff --git a/demos/flow-shield/src/components/ForceShield/index.tsx b/demos/flow-shield/src/components/ForceShield/index.tsx
index f4bf364e..412f6063 100644
--- a/demos/flow-shield/src/components/ForceShield/index.tsx
+++ b/demos/flow-shield/src/components/ForceShield/index.tsx
@@ -1,13 +1,15 @@
'use client'
-import { useRef, useMemo, useEffect, useCallback } from 'react'
-import { useFrame } from '@react-three/fiber'
+import { useRef, useEffect, useCallback } from 'react'
+import { extend, 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'
+import { ShieldMaterial } from './shaderMaterial'
+
+extend({ ShieldMaterial })
interface ShieldProps {
isActive?: boolean
@@ -16,7 +18,7 @@ interface ShieldProps {
}
function Shield({ isActive = false, posYOverride, preset }: ShieldProps) {
- const materialRef = useRef(null!)
+ const materialRef = useRef(null!)
const groupRef = useRef(null!)
const revealRef = useRef(1)
const timeRef = useRef(0)
@@ -72,13 +74,6 @@ function Shield({ isActive = false, posYOverride, preset }: ShieldProps) {
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
@@ -180,7 +175,7 @@ function Shield({ isActive = false, posYOverride, preset }: ShieldProps) {
-
+
)
diff --git a/demos/flow-shield/src/components/ForceShield/shaderMaterial.ts b/demos/flow-shield/src/components/ForceShield/shaderMaterial.ts
index 3ed1ded1..12454a41 100644
--- a/demos/flow-shield/src/components/ForceShield/shaderMaterial.ts
+++ b/demos/flow-shield/src/components/ForceShield/shaderMaterial.ts
@@ -239,49 +239,52 @@ export const fragmentShader = /* glsl */ `
}
`
-// ── Material factory ──────────────────────────────────────────────────────────
-export function createShieldMaterial(): THREE.ShaderMaterial {
- const hitPositions = Array.from({ length: MAX_HITS }, () => new THREE.Vector3(0, 1.8, 0))
- const hitTimes = new Array(MAX_HITS).fill(-999)
-
- return new THREE.ShaderMaterial({
- uniforms: {
- uTime: { value: 0 },
- uColor: { value: new THREE.Color('#26aeff') },
- uLife: { value: 1.0 },
- uHexScale: { value: 3.0 },
- uEdgeWidth: { value: 0.06 },
- uFresnelPower: { value: 1.8 },
- uFresnelStrength: { value: 1.75 },
- uOpacity: { value: 0.76 },
- uReveal: { value: 1 },
- uFlashSpeed: { value: 0.6 },
- uFlashIntensity: { value: 0.11 },
- uNoiseScale: { value: 1.3 },
- uNoiseEdgeColor: { value: new THREE.Color('#26aeff') },
- uNoiseEdgeWidth: { value: 0.02 },
- uNoiseEdgeIntensity: { value: 10.0 },
- uNoiseEdgeSmoothness: { value: 0.5 },
- uHexOpacity: { value: 0.13 },
- uShowHex: { value: 1.0 },
- uFlowScale: { value: 2.4 },
- uFlowSpeed: { value: 1.13 },
- uFlowIntensity: { value: 4 },
- uHitPos: { value: hitPositions },
- uHitTime: { value: hitTimes },
- uHitRingSpeed: { value: 1.75 },
- uHitRingWidth: { value: 0.12 },
- uHitMaxRadius: { value: 0.85 },
- uHitDuration: { value: 1.8 },
- uHitIntensity: { value: 4.1 },
- uHitImpactRadius: { value: 0.3 },
- uFadeStart: { value: 0.0 }
- },
- vertexShader,
- fragmentShader,
- transparent: true,
- depthWrite: false,
- side: THREE.FrontSide,
- blending: THREE.AdditiveBlending
- })
+// Each material instance gets its own hit ring buffers.
+function createUniforms() {
+ return {
+ uTime: { value: 0 },
+ uColor: { value: new THREE.Color('#26aeff') },
+ uLife: { value: 1.0 },
+ uHexScale: { value: 3.0 },
+ uEdgeWidth: { value: 0.06 },
+ uFresnelPower: { value: 1.8 },
+ uFresnelStrength: { value: 1.75 },
+ uOpacity: { value: 0.76 },
+ uReveal: { value: 1 },
+ uFlashSpeed: { value: 0.6 },
+ uFlashIntensity: { value: 0.11 },
+ uNoiseScale: { value: 1.3 },
+ uNoiseEdgeColor: { value: new THREE.Color('#26aeff') },
+ uNoiseEdgeWidth: { value: 0.02 },
+ uNoiseEdgeIntensity: { value: 10.0 },
+ uNoiseEdgeSmoothness: { value: 0.5 },
+ uHexOpacity: { value: 0.13 },
+ uShowHex: { value: 1.0 },
+ uFlowScale: { value: 2.4 },
+ uFlowSpeed: { value: 1.13 },
+ uFlowIntensity: { value: 4 },
+ uHitPos: { value: Array.from({ length: MAX_HITS }, () => new THREE.Vector3(0, 1.8, 0)) },
+ uHitTime: { value: new Array(MAX_HITS).fill(-999) },
+ uHitRingSpeed: { value: 1.75 },
+ uHitRingWidth: { value: 0.12 },
+ uHitMaxRadius: { value: 0.85 },
+ uHitDuration: { value: 1.8 },
+ uHitIntensity: { value: 4.1 },
+ uHitImpactRadius: { value: 0.3 },
+ uFadeStart: { value: 0.0 }
+ }
+}
+
+export class ShieldMaterial extends THREE.ShaderMaterial {
+ constructor() {
+ super({
+ uniforms: createUniforms(),
+ vertexShader,
+ fragmentShader,
+ transparent: true,
+ depthWrite: false,
+ side: THREE.FrontSide,
+ blending: THREE.AdditiveBlending
+ })
+ }
}
diff --git a/demos/flow-shield/src/components/playground/DemoSphere.tsx b/demos/flow-shield/src/components/playground/DemoSphere.tsx
deleted file mode 100644
index 8f856dd8..00000000
--- a/demos/flow-shield/src/components/playground/DemoSphere.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client'
-
-import { useRef } from 'react'
-import { useFrame } from '@react-three/fiber'
-import type { Mesh } from 'three'
-import type { SceneMode } from './SceneContent'
-
-export default function DemoSphere({ mode }: { mode: SceneMode }) {
- const meshRef = useRef(null!)
- const wireframe = mode === 'Frame'
-
- useFrame(() => {
- meshRef.current.position.y = 1 + Math.sin(Date.now() * 0.001) * 0.2
- })
-
- return (
-
-
- {wireframe ? (
-
- ) : (
-
- )}
-
- )
-}
diff --git a/demos/flow-shield/src/components/playground/Droideka.tsx b/demos/flow-shield/src/components/playground/Droideka.tsx
index 96862517..00576b9c 100644
--- a/demos/flow-shield/src/components/playground/Droideka.tsx
+++ b/demos/flow-shield/src/components/playground/Droideka.tsx
@@ -10,7 +10,7 @@ Title: Droideka
import { useGLTF } from '@react-three/drei'
-const DROIDEKA_URL = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}droideka.glb`
+const DROIDEKA_URL = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}droideka-5mb.glb`
export function Droideka() {
const { nodes, materials } = useGLTF(DROIDEKA_URL)
diff --git a/demos/flow-shield/src/components/playground/PlaygroundCanvas.tsx b/demos/flow-shield/src/components/playground/PlaygroundCanvas.tsx
deleted file mode 100644
index ac320d34..00000000
--- a/demos/flow-shield/src/components/playground/PlaygroundCanvas.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client'
-
-import { useState, useCallback, useRef } from 'react'
-import { Canvas } from '@react-three/fiber'
-import { Leva, useControls } from 'leva'
-import { LEVA_THEME } from '../theme/theme'
-import SceneContent from './SceneContent'
-import type { SceneMode } from './SceneContent'
-import UIOverlay from '../overlay/UIOverlay'
-import OverlayButtons, { type Preset } from '../overlay/OverlayButtons'
-import LoadingOverlay from '../overlay/LoadingOverlay'
-
-export default function PlaygroundCanvas() {
- const [showGrid, setShowGrid] = useState(true)
- const [hideLeva, setHideLeva] = useState(false)
- const [glbUrl, setGlbUrl] = useState(null)
- const [preset, setPreset] = useState('default')
- const [isLoadingModel, setIsLoadingModel] = useState(false)
- const glbUrlRef = useRef(null)
-
- const handleLoadGlb = useCallback((file: File) => {
- if (glbUrlRef.current) URL.revokeObjectURL(glbUrlRef.current)
- const url = URL.createObjectURL(file)
- glbUrlRef.current = url
- setIsLoadingModel(true)
- setGlbUrl(url)
- }, [])
-
- const handleModelLoaded = useCallback(() => {
- setIsLoadingModel(false)
- }, [])
-
- const handleClearGlb = useCallback(() => {
- if (glbUrlRef.current) URL.revokeObjectURL(glbUrlRef.current)
- glbUrlRef.current = null
- setGlbUrl(null)
- }, [])
-
- const { mode } = useControls(
- 'Scene',
- {
- mode: {
- value: 'Background' as SceneMode,
- options: ['Background', 'Frame'] as SceneMode[],
- label: 'Mode'
- }
- },
- { collapsed: true }
- )
-
- return (
- <>
-
-
-
-
-
- setShowGrid((v) => !v)}
- hideLeva={hideLeva}
- onToggleLeva={() => setHideLeva((v) => !v)}
- hasGlb={glbUrl !== null}
- onLoadGlb={handleLoadGlb}
- onClearGlb={handleClearGlb}
- preset={preset}
- onSetPreset={setPreset}
- />
-
- >
- )
-}
diff --git a/demos/flow-shield/src/react-three-fiber-jsx.d.ts b/demos/flow-shield/src/react-three-fiber-jsx.d.ts
index ffdb614d..0a758068 100644
--- a/demos/flow-shield/src/react-three-fiber-jsx.d.ts
+++ b/demos/flow-shield/src/react-three-fiber-jsx.d.ts
@@ -3,17 +3,26 @@ import type { ThreeElements } from '@react-three/fiber'
declare global {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
+ interface IntrinsicElements {
+ shieldMaterial: ThreeElements['shaderMaterial']
+ }
}
}
declare module 'react' {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
+ interface IntrinsicElements {
+ shieldMaterial: ThreeElements['shaderMaterial']
+ }
}
}
declare module 'react/jsx-runtime' {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
+ interface IntrinsicElements {
+ shieldMaterial: ThreeElements['shaderMaterial']
+ }
}
}