Skip to content

Instantly share code, notes, and snippets.

@phobon
Created April 8, 2024 03:39
Show Gist options
  • Save phobon/fa26c815989941cf4fa51902db55b8e0 to your computer and use it in GitHub Desktop.
Save phobon/fa26c815989941cf4fa51902db55b8e0 to your computer and use it in GitHub Desktop.
Pixellation / UV remapping grid trail
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D u_diffuse;
uniform sampler2D u_dataTexture;
uniform float u_amplitude;
varying vec2 v_uv;
vec3 correctGamma(vec3 color) {
return pow(color, vec3(1.0 / 2.2));
}
void main(void) {
vec4 offsetColor = texture2D(u_dataTexture, v_uv);
vec2 offsetUv = v_uv - u_amplitude * offsetColor.rg;
vec4 finalColor = texture2D(u_diffuse, offsetUv);
finalColor.rgb = correctGamma(finalColor.rgb);
gl_FragColor = finalColor;
}
import { useFrame, extend, useThree } from '@react-three/fiber'
import { useRef } from 'react'
import { shaderMaterial } from '@react-three/drei'
import { v4 as uuid } from 'uuid'
import vertexShader from './vertex.glsl'
import fragmentShader from './fragment.glsl'
import { useGridTrailTexture } from './use_grid_trail_texture'
const PixellationMaterial = shaderMaterial(
{
u_time: 0,
u_resolution: [0, 0],
u_diffuse: null,
u_dataTexture: null,
u_pageRatio: 0,
u_amplitude: 0.00001,
},
vertexShader,
fragmentShader,
)
PixellationMaterial.key = uuid()
extend({ PixellationMaterial })
const Pixellation = ({ texture }) => {
const meshRef = useRef<any>()
const { width, height } = useThree((state) => state.size)
useFrame(({ clock }) => {
const mesh = meshRef.current
if (!mesh) {
return
}
mesh.material.uniforms.u_time.value = clock.elapsedTime
mesh.material.uniforms.u_resolution.value.x = window.innerWidth
mesh.material.uniforms.u_resolution.value.y = window.innerWidth
})
const [dataTexture, onMove] = useGridTrailTexture({ grid: 32, radius: 0.05, strength: 0.02, decay: 0.9 })
return (
<mesh ref={meshRef} scale={[width, height, 1]} onPointerMove={onMove}>
<planeGeometry args={[1, 1]} />
{/* @ts-ignore */}
<pixellationMaterial uniforms-u_diffuse-value={texture} uniforms-u_dataTexture-value={dataTexture} />
</mesh>
)
}
export default Pixellation
import { ThreeEvent, useFrame } from '@react-three/fiber'
import { useCallback, useEffect, useRef, useState } from 'react'
import * as THREE from 'three'
const clamp = (min: number, input: number, max: number) => {
return Math.max(min, Math.min(input, max))
}
export type UseGridTrailTextureProps = {
grid?: number
radius?: number
strength?: number
decay?: number
}
export const useGridTrailTexture = (options?: UseGridTrailTextureProps): any => {
const { grid = 50, radius = 0.05, strength = 0.06, decay = 0.75 } = options || {}
const [dataTexture, setDataTexture] = useState<THREE.DataTexture>()
const previousPointerRef = useRef({ x: 0, y: 0 })
const pointerDeltaRef = useRef({ x: 0, y: 0 })
const pointerRef = useRef({ x: 0, y: 0 })
const viewportRef = useRef({ width: 0, height: 0 })
const size = grid
const width = size
const height = size
const dimensions = width * height
useEffect(() => {
// Regenerate grid on mount and on window resize
const regenerateGrid = () => {
viewportRef.current.width = window.innerWidth
viewportRef.current.height = window.innerHeight
// Populate data texture with initial values
const data = new Float32Array(4 * dimensions)
for (let stride = 0; stride < dimensions; stride++) {
const index = stride * 4
data[index] = 0
data[index + 1] = 0
data[index + 2] = 0
data[index + 3] = 0
}
const dataTexture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType)
dataTexture.minFilter = dataTexture.magFilter = THREE.NearestFilter
dataTexture.needsUpdate = true
setDataTexture(dataTexture)
}
window.addEventListener('resize', regenerateGrid)
regenerateGrid()
return () => {
window.removeEventListener('resize', regenerateGrid)
}
}, [dimensions, height, width])
const onMove = useCallback((e: ThreeEvent<PointerEvent>) => {
const { x, y } = e
pointerRef.current.x = x
pointerRef.current.y = y
// Pointer velocity
pointerDeltaRef.current.x = pointerRef.current.x - previousPointerRef.current.x
pointerDeltaRef.current.y = pointerRef.current.y - previousPointerRef.current.y
// Cache previous pointer position
previousPointerRef.current.x = pointerRef.current.x
previousPointerRef.current.y = pointerRef.current.y
}, [])
useFrame(({}) => {
if (!dataTexture) {
return
}
const data = dataTexture.image.data
const cellX = (size * pointerRef.current.x) / viewportRef.current.width
const cellY = size * (1 - pointerRef.current.y / viewportRef.current.height)
const mouseRadius = size * radius
const aspect = viewportRef.current.height / viewportRef.current.width
for (let stride = 0; stride < data.length; stride += 4) {
data[stride] *= decay
data[stride + 1] *= decay
for (let x = 0; x < size; x++)
for (let y = 0; y < size; y++) {
// Calculate squared euclidian distance between two points
const dist = (cellX - x) ** 2 / aspect + (cellY - y) ** 2
const distMax = mouseRadius ** 2
// If we're within the radius here, it's a hit think of this like a 2D raymarching calculation
if (dist < distMax) {
// Determine the strength and size of the force
const dataIndex = 4 * (x + size * y)
let force = mouseRadius / Math.sqrt(dist)
force = clamp(force, 0, 10)
data[dataIndex] += strength * Math.abs(pointerDeltaRef.current.x) * force
data[dataIndex + 1] += strength * Math.abs(pointerDeltaRef.current.y) * force
}
}
dataTexture.needsUpdate = true
}
// Decay pointer velocity
if (pointerDeltaRef.current.x > 0) {
pointerDeltaRef.current.x *= decay
}
if (pointerDeltaRef.current.y > 0) {
pointerDeltaRef.current.y *= decay
}
})
return [dataTexture, onMove]
}
varying vec2 v_uv;
void main() {
// gl_Position = vec4(position, 1.0);
vec3 localSpacePosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(localSpacePosition, 1.0);
v_uv = uv;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment