Description
The demo stores its container scroll progress in a ref, then selects one of sixty transparent .webp frames for a full-screen TSL quad.
One sRGB texture is reused between frames, avoiding a canvas copy and keeping the GPU footprint near one full-HD texture. Use this pattern for scroll-driven product turns, reveals, and other pre-rendered motion.
Implementation
Source Code
ScrollDrivenImageSequence
'use client'
import { useFrame, useLoader, useLocalNodes } from '@react-three/fiber/webgpu'
import { type FC, type RefObject, useCallback, useEffect, useMemo } from 'react'
import {
Fn,
float,
positionGeometry,
screenSize,
texture,
varying,
vec2,
vec4,
} from 'three/tsl'
import {
BufferAttribute,
BufferGeometry,
ImageLoader,
LinearFilter,
SRGBColorSpace,
Sphere,
Texture,
Vector3,
} from 'three/webgpu'
type Props = {
progress: RefObject<number>
}
const FRAME_ASPECT = float(16 / 9).toConst()
const ScrollDrivenImageSequence: FC<Props> = ({ progress }) => {
const frameImages = useLoader(ImageLoader, FRAME_URLS)
const screenQuadGeometry = useMemo(() => createScreenQuadGeometry(), [])
const frameTexture = useMemo(() => {
const nextTexture = new Texture(frameImages[0])
nextTexture.colorSpace = SRGBColorSpace
nextTexture.generateMipmaps = false
nextTexture.magFilter = LinearFilter
nextTexture.minFilter = LinearFilter
nextTexture.needsUpdate = true
return nextTexture
}, [frameImages])
const createNodes = useCallback(() => {
const vUv = varying<'vec2'>(vec2(), 'vUv')
const vertexNode = Fn(() => {
const screenAspect = screenSize.x.div(screenSize.y)
const visibleArea = vec2(
screenAspect.div(FRAME_ASPECT).min(1),
FRAME_ASPECT.div(screenAspect).min(1),
)
vUv.assign(
positionGeometry.xy
.mul(0.5)
.add(0.5)
.mul(visibleArea)
.add(vec2(1).sub(visibleArea).mul(0.5)),
)
return vec4(positionGeometry.xy, 0, 1)
})()
return {
colorNode: texture(frameTexture, vUv),
setFrame: (progress: number) => {
const frameIndex = Math.min(
frameImages.length - 1,
Math.max(0, Math.floor(progress * frameImages.length)),
)
const nextImage = frameImages[frameIndex]
if (!nextImage || frameTexture.image === nextImage) return
frameTexture.image = nextImage
frameTexture.needsUpdate = true
},
vertexNode,
}
}, [frameImages, frameTexture])
const nodes = useLocalNodes(createNodes)
useEffect(() => () => frameTexture.dispose(), [frameTexture])
useFrame(() => {
nodes.setFrame(progress.current)
})
return (
<mesh geometry={screenQuadGeometry} frustumCulled={false}>
<meshBasicNodeMaterial
colorNode={nodes.colorNode}
depthTest={false}
depthWrite={false}
transparent={true}
vertexNode={nodes.vertexNode}
/>
</mesh>
)
}
function createScreenQuadGeometry(): BufferGeometry {
const geometry = new BufferGeometry()
const vertices = new Float32Array([-1, -1, 3, -1, -1, 3])
geometry.boundingSphere = new Sphere(new Vector3(), Infinity)
geometry.setAttribute('position', new BufferAttribute(vertices, 2))
return geometry
}
export default ScrollDrivenImageSequence
const FRAME_URLS: string[] = [
new URL('./assets/pragma10001.webp', import.meta.url).href,
new URL('./assets/pragma10002.webp', import.meta.url).href,
new URL('./assets/pragma10003.webp', import.meta.url).href,
new URL('./assets/pragma10004.webp', import.meta.url).href,
new URL('./assets/pragma10005.webp', import.meta.url).href,
new URL('./assets/pragma10006.webp', import.meta.url).href,
new URL('./assets/pragma10007.webp', import.meta.url).href,
new URL('./assets/pragma10008.webp', import.meta.url).href,
new URL('./assets/pragma10009.webp', import.meta.url).href,
new URL('./assets/pragma10010.webp', import.meta.url).href,
new URL('./assets/pragma10011.webp', import.meta.url).href,
new URL('./assets/pragma10012.webp', import.meta.url).href,
new URL('./assets/pragma10013.webp', import.meta.url).href,
new URL('./assets/pragma10014.webp', import.meta.url).href,
new URL('./assets/pragma10015.webp', import.meta.url).href,
new URL('./assets/pragma10016.webp', import.meta.url).href,
new URL('./assets/pragma10017.webp', import.meta.url).href,
new URL('./assets/pragma10018.webp', import.meta.url).href,
new URL('./assets/pragma10019.webp', import.meta.url).href,
new URL('./assets/pragma10020.webp', import.meta.url).href,
new URL('./assets/pragma10021.webp', import.meta.url).href,
new URL('./assets/pragma10022.webp', import.meta.url).href,
new URL('./assets/pragma10023.webp', import.meta.url).href,
new URL('./assets/pragma10024.webp', import.meta.url).href,
new URL('./assets/pragma10025.webp', import.meta.url).href,
new URL('./assets/pragma10026.webp', import.meta.url).href,
new URL('./assets/pragma10027.webp', import.meta.url).href,
new URL('./assets/pragma10028.webp', import.meta.url).href,
new URL('./assets/pragma10029.webp', import.meta.url).href,
new URL('./assets/pragma10030.webp', import.meta.url).href,
new URL('./assets/pragma10031.webp', import.meta.url).href,
new URL('./assets/pragma10032.webp', import.meta.url).href,
new URL('./assets/pragma10033.webp', import.meta.url).href,
new URL('./assets/pragma10034.webp', import.meta.url).href,
new URL('./assets/pragma10035.webp', import.meta.url).href,
new URL('./assets/pragma10036.webp', import.meta.url).href,
new URL('./assets/pragma10037.webp', import.meta.url).href,
new URL('./assets/pragma10038.webp', import.meta.url).href,
new URL('./assets/pragma10039.webp', import.meta.url).href,
new URL('./assets/pragma10040.webp', import.meta.url).href,
new URL('./assets/pragma10041.webp', import.meta.url).href,
new URL('./assets/pragma10042.webp', import.meta.url).href,
new URL('./assets/pragma10043.webp', import.meta.url).href,
new URL('./assets/pragma10044.webp', import.meta.url).href,
new URL('./assets/pragma10045.webp', import.meta.url).href,
new URL('./assets/pragma10046.webp', import.meta.url).href,
new URL('./assets/pragma10047.webp', import.meta.url).href,
new URL('./assets/pragma10048.webp', import.meta.url).href,
new URL('./assets/pragma10049.webp', import.meta.url).href,
new URL('./assets/pragma10050.webp', import.meta.url).href,
new URL('./assets/pragma10051.webp', import.meta.url).href,
new URL('./assets/pragma10052.webp', import.meta.url).href,
new URL('./assets/pragma10053.webp', import.meta.url).href,
new URL('./assets/pragma10054.webp', import.meta.url).href,
new URL('./assets/pragma10055.webp', import.meta.url).href,
new URL('./assets/pragma10056.webp', import.meta.url).href,
new URL('./assets/pragma10057.webp', import.meta.url).href,
new URL('./assets/pragma10058.webp', import.meta.url).href,
new URL('./assets/pragma10059.webp', import.meta.url).href,
new URL('./assets/pragma10060.webp', import.meta.url).href,
]