Skip to content
bgHub
Gallery

Sunset Ripples

GLSL ShaderMedium

A flowing ripples surface distorted by flowing noise. GPU shader.

#shader#glsl#fluid#ripple#sunset
Live preview
1 file
$npm i three @react-three/fiber
/*
 * Liquid (React + react-three-fiber, GLSL)
 *
 * Setup with Vite:
 *   npm create vite@latest my-app -- --template react-ts
 *   cd my-app
 *   npm i three @react-three/fiber
 *
 * Next.js App Router: add "use client" as the first line of the file.
 * Render <SceneBackground /> inside a container with position: relative.
 */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";

const vertexShader = `varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = vec4(position, 1.0);
}`;

const fragmentShader = `precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec2 uResolution;
uniform float uLight;
float hash11(float p){ return fract(sin(p*127.1)*43758.5453); }
float hash21(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
vec2 hash22(vec2 p){
  p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
  return fract(sin(p)*43758.5453);
}
mat2 rot(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}
vec3 permute(vec3 x){return mod289(((x*34.0)+1.0)*x);}
float snoise(vec2 v){
  const vec4 C=vec4(0.211324865,0.366025403,-0.577350269,0.024390243);
  vec2 i=floor(v+dot(v,C.yy));
  vec2 x0=v-i+dot(i,C.xx);
  vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0);
  vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1;
  i=mod289(i);
  vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));
  vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0);
  m=m*m; m=m*m;
  vec3 x=2.0*fract(p*C.www)-1.0;
  vec3 h=abs(x)-0.5;
  vec3 ox=floor(x+0.5);
  vec3 a0=x-ox;
  m*=1.79284291-0.85373472*(a0*a0+h*h);
  vec3 g;
  g.x=a0.x*x0.x+h.x*x0.y;
  g.yz=a0.yz*x12.xz+h.yz*x12.yw;
  return 130.0*dot(m,g);
}
float fbm(vec2 p){
  float v=0.0, a=0.5;
  for(int i=0;i<5;i++){ v+=a*snoise(p); p*=2.0; a*=0.5; }
  return v;
}
void main(){
  vec2 uv01=vUv;
  vec2 uv=(vUv-0.5);
  uv.x*=uResolution.x/uResolution.y;
  float t=uTime*0.1680;
  vec3 c0=mix(vec3(0.039,0.020,0.020),vec3(0.992,0.961,0.949),uLight);
  vec3 c1=mix(vec3(0.961,0.620,0.043),vec3(0.706,0.325,0.035),uLight);
  vec3 c2=mix(vec3(0.957,0.247,0.369),vec3(0.745,0.071,0.235),uLight);
  vec3 c3=mix(vec3(0.545,0.361,0.965),vec3(0.427,0.157,0.851),uLight);
  vec3 col=c0;
  float acc=0.0;
  for(int i=0;i<4;i++){
    float fi=float(i);
    vec2 c=vec2(sin(fi*2.1)*0.34, cos(fi*1.7)*0.26);
    float d=length(uv-c);
    acc+=sin(d*46.0-t*4.0-fi)*exp(-d*3.2);
  }
  float k=acc*0.5+0.5;
  vec3 cc=mix(c1,c2,k);
  col=mix(c0,cc,smoothstep(0.28,0.85,k));
  col+=c3*pow(smoothstep(0.6,1.0,k),3.0)*0.8;
  col=mix(col,c0,0.50*dot(uv,uv));
  gl_FragColor=vec4(col,1.0);
}`;

/** 1 in light mode, 0 in dark. The shader mixes both palettes on it. */
function useLightScheme() {
  const [light, setLight] = useState(false);
  useEffect(() => {
    const mq = matchMedia("(prefers-color-scheme: light)");
    const sync = () => setLight(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);
  return light;
}

function Plane() {
  const mat = useRef<THREE.ShaderMaterial>(null);
  const { size } = useThree();
  const light = useLightScheme();
  const uniforms = useMemo(
    () => ({
      uTime: { value: 0 },
      uResolution: { value: new THREE.Vector2(size.width, size.height) },
      uLight: { value: 0 },
    }),
    [],
  );

  useFrame((state) => {
    if (!mat.current) return;
    mat.current.uniforms.uTime.value = state.clock.elapsedTime;
    mat.current.uniforms.uResolution.value.set(size.width, size.height);
    // Eased rather than snapped, so a scheme flip crossfades.
    const u = mat.current.uniforms.uLight;
    u.value += ((light ? 1 : 0) - u.value) * 0.08;
  });

  return (
    <mesh>
      <planeGeometry args={[2, 2]} />
      <shaderMaterial
        ref={mat}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
        uniforms={uniforms}
      />
    </mesh>
  );
}

export function SceneBackground() {
  return (
    <Canvas
      style={{ position: "absolute", inset: 0 }}
      orthographic
      camera={{ position: [0, 0, 1], zoom: 1 }}
      dpr={[1, 2]}
    >
      <Plane />
    </Canvas>
  );
}

Copy or download any file. The HTML build loads Three.js from a CDN via an import map, so it runs by just opening index.html. The React build is react-three-fiber; setup notes live at the top of each file.

Variants

40 of this pattern · page 1 of 4