Simple Shader Demo

Jeremy Atkinson
Developer

This post has a short demo showing how a custom shader can be implemented in R3F.

Learning Shaders with Interactive Controls

Shaders can be intimidating when you're starting out. Static code examples don't help you understand what each parameter actually does. This demo makes shader learning hands-on.

import React, { useRef, useMemo } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Sky } from '@react-three/drei';
import { useControls } from 'leva';
import * as THREE from 'three';

function ShaderMesh() {
  const meshRef = useRef();

  const { thresholdX, thresholdY, displacement, baseColor, highlightColor, wireframe, transparent, autoRotate
  } = useControls({
    thresholdX: { value: 0.5, min: 0, max: 1, step: 0.01 },
    thresholdY: { value: 0.5, min: 0, max: 1, step: 0.01 },
    displacement: { value: 0.1, min: 0, max: 1, step: 0.01 },
    baseColor: '#d8d5d5',
    highlightColor: '#00ff00',
    wireframe: true,
    transparent: true,
    autoRotate: false
  });

  const material = useMemo(() => {
    return new THREE.ShaderMaterial({
      vertexShader: `
        uniform float uThresholdX;
        uniform float uThresholdY;
        uniform float uDisplacement;
        varying vec2 vUv; 
        
        void main() {
          vec3 newPosition = position;
          
          if (uv.x > uThresholdX && uv.y > uThresholdY) {
            newPosition.z += uDisplacement;
          }
        
          gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
          vUv = uv;
        }
      `,
      fragmentShader: `
        uniform float uThresholdX;
        uniform float uThresholdY;
        uniform vec3 uBaseColor;
        uniform vec3 uHighlightColor;
        varying vec2 vUv; 
      
        void main() {
          vec3 color = uBaseColor;
          
          if (vUv.x > uThresholdX && vUv.y > uThresholdY) {
            color = uHighlightColor;
          }
        
          gl_FragColor = vec4(color, 1.0);
        }
      `,
      uniforms: {
        uThresholdX: { value: thresholdX },
        uThresholdY: { value: thresholdY },
        uDisplacement: { value: displacement },
        uBaseColor: { value: new THREE.Color(baseColor) },
        uHighlightColor: { value: new THREE.Color(highlightColor) }
      },
      side: THREE.DoubleSide,
      wireframe,
      transparent
    });
  }, [thresholdX, thresholdY, displacement, baseColor, highlightColor, wireframe, transparent]);

  useFrame((state) => {
    if (meshRef.current && autoRotate) {
      meshRef.current.rotation.x = state.clock.elapsedTime * 0.3;
      meshRef.current.rotation.y = state.clock.elapsedTime * 0.2;
    }

    // Update uniforms
    if (material.uniforms) {
      material.uniforms.uThresholdX.value = thresholdX;
      material.uniforms.uThresholdY.value = thresholdY;
      material.uniforms.uDisplacement.value = displacement;
      material.uniforms.uBaseColor.value.set(baseColor);
      material.uniforms.uHighlightColor.value.set(highlightColor);
    }
  });

  return (
    <mesh ref={meshRef} material={material}>
      <planeGeometry args={[3, 3, 32, 32]} />
    </mesh>
  );
}

function Scene() {
  return (
    <Canvas camera={{ position: [0, 0, 4], fov: 50 }} style={{ height: '90vh' }}>
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      <Sky sunPosition={[0, 1, 0]} />
      <OrbitControls enableDamping dampingFactor={0.05} />
      <ShaderMesh />
    </Canvas>
  );
}

export default function App() {
  return (
    <div className="w-full h-screen bg-gray-900 relative">
      <Scene />
      <div className="absolute top-4 left-4 text-white">
        <h1 className="text-xl font-bold mb-2">Interactive Shader Demo</h1>
      </div>
    </div>
  );
}

What It Shows

The shader demonstrates two core concepts:

Vertex displacement: Push vertices forward based on their UV coordinates. The uThresholdX and uThresholdY uniforms control where this happens.

Fragment coloring: Color pixels differently in the same UV region using the same threshold values.

Learning Through Interaction

Instead of guessing what changing 0.5 to 0.7 does, you move a slider and see it happen. Want to understand how displacement amount affects the geometry? Drag the displacement control.

The Leva controls expose the key parameters:

  • Threshold values (where effects trigger)
  • Displacement amount (how much vertices move)
  • Colors (base and highlight)
  • Material properties (wireframe, transparency)

Why This Helps

When you can immediately see how parameters affect the output, shader concepts click faster. You understand the relationship between UV coordinates and effects. You see how vertex and fragment shaders work together.

It's the difference between reading about shaders and actually playing with them.

Next Steps

Once you understand these basics, you can apply the same patterns to more complex effects. The interactive approach works for any shader - just expose the key parameters as uniforms and hook them up to controls.

Start simple, experiment freely, and build up your shader intuition.

Jeremy Atkinson

Jeremy is a structural engineer, researcher, and developer from BC. He works on Calcs.app and writes at Kinson.io

Comments (0)

Loading comments...