Animated Flamingo

TRON-style animated flamingo

Created: March 4, 2024

View Source Code
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass";

let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: OrbitControls;
let mixer: THREE.AnimationMixer;
let clock: THREE.Clock;
let model: THREE.Group;
let animationFrameId: number;
let isInitialized = false;
let handleResizeFunction: (() => void) | null = null;
let composer: EffectComposer;
let bloomPass: UnrealBloomPass;

const vertexShader = `
#include <skinning_pars_vertex>
#include <morphtarget_pars_vertex>

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec3 vViewPosition;

void main() {
  vUv = uv;
  
  #include <beginnormal_vertex>
  #include <morphnormal_vertex>
  #include <skinnormal_vertex>
  
  vNormal = normalize(normalMatrix * objectNormal);
  
  #include <begin_vertex>
  #include <morphtarget_vertex>
  #include <skinning_vertex>
  
  vPosition = transformed;
  
  vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.0);
  vViewPosition = -mvPosition.xyz;
  
  gl_Position = projectionMatrix * mvPosition;
}
`;

const fragmentShader = `
uniform float time;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec3 vViewPosition;

void main() {
  vec3 viewDir = normalize(vViewPosition);
  vec3 normal = normalize(vNormal);
  
  float rimPower = 2.0;
  float rimStrength = 0.8;
  float rimAmount = pow(1.0 - max(0.0, dot(normal, viewDir)), rimPower) * rimStrength;
  
  float pulse = 0.5 + 0.5 * sin(time * 1.5);
  pulse = 0.8 + pulse * 0.2;
  
  float edge = rimAmount * pulse;
  
  vec3 glowColor = vec3(0.0, 0.6, 1.0);
  
  vec3 finalColor = vec3(0.0);
  
  if (edge > 0.1) {
    float edgeIntensity = edge * (0.8 + 0.2 * sin(vPosition.y * 8.0 + time * 2.0));
    finalColor = glowColor * edgeIntensity * 2.0;
  }
  
  if (edge > 0.3) {
    float gridX = mod(vUv.x * 50.0, 1.0);
    float gridY = mod(vUv.y * 50.0, 1.0);
    float gridPattern = 0.0;
    
    if (gridX < 0.05 || gridX > 0.95 || gridY < 0.05 || gridY > 0.95) {
      gridPattern = 0.3;
    }
    
    finalColor += glowColor * gridPattern * pulse;
  }
  
  gl_FragColor = vec4(finalColor, edge > 0.1 ? 0.9 : 0.0);
}
`;

function createCustomMaterial() {
  return new THREE.ShaderMaterial({
    uniforms: {
      time: { value: 0 },
    },
    vertexShader,
    fragmentShader,
    side: THREE.DoubleSide,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    depthTest: true,
    // Note: newer versions of Three.js handle these differently
    // These properties have been removed as they're not in ShaderMaterialParameters
  });
}

export function initialize(container: HTMLElement | null): void {
  if (!container || isInitialized) return;

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000000);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  directionalLight.position.set(2, 3, 4);
  scene.add(directionalLight);

  const secondLight = new THREE.DirectionalLight(0xffffff, 0.7);
  secondLight.position.set(-3, 2, -4);
  scene.add(secondLight);

  const pointLight = new THREE.PointLight(0x3677ff, 0.5, 10);
  pointLight.position.set(0, 2, 3);
  scene.add(pointLight);

  const aspectRatio = container.clientWidth / container.clientHeight;
  camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000);
  camera.position.set(0, 50, 150);
  camera.far = 2000;

  renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
  });
  renderer.setSize(container.clientWidth, container.clientHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.25;
  container.appendChild(renderer.domElement);

  composer = new EffectComposer(renderer);

  const renderPass = new RenderPass(scene, camera);
  composer.addPass(renderPass);

  const bloomParams = {
    exposure: 1,
    strength: 1.5,
    threshold: 0,
    radius: 0.5,
  };
  bloomPass = new UnrealBloomPass(
    new THREE.Vector2(container.clientWidth, container.clientHeight),
    bloomParams.strength,
    bloomParams.radius,
    bloomParams.threshold
  );
  composer.addPass(bloomPass);

  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;

  clock = new THREE.Clock();
  clock.start();

  const loadingText = document.createElement("div");
  loadingText.style.position = "absolute";
  loadingText.style.top = "50%";
  loadingText.style.left = "50%";
  loadingText.style.transform = "translate(-50%, -50%)";
  loadingText.style.color = "white";
  loadingText.style.fontSize = "16px";
  loadingText.style.padding = "10px";
  loadingText.style.backgroundColor = "rgba(0,0,0,0.5)";
  loadingText.style.borderRadius = "5px";
  loadingText.textContent = "Loading flamingo model...";
  container.appendChild(loadingText);

  const loader = new GLTFLoader();
  const modelUrl = "https://threejs.org/examples/models/gltf/Flamingo.glb";

  THREE.Cache.enabled = true;

  loader.load(
    modelUrl,
    (gltf) => {
      model = gltf.scene;
      scene.add(model);

      if (gltf.animations && gltf.animations.length) {
        // Use the whole model for the animation mixer
        mixer = new THREE.AnimationMixer(model);
        
        // Get the animation and ensure it loops
        const animation = gltf.animations[0];
        const action = mixer.clipAction(animation);
        action.setEffectiveTimeScale(1.0);
        action.setLoop(THREE.LoopRepeat, Infinity);
        action.play();
      }

      model.scale.set(1, 1, 1);
      model.position.set(0, 0, 0);
      model.rotation.y = Math.PI / 4;

      const customMaterial = createCustomMaterial();
      model.traverse((child) => {
        if (child instanceof THREE.Mesh) {
          (child as any).originalMaterial = child.material;
          child.material = customMaterial;
          child.castShadow = true;
          child.receiveShadow = true;
        }
      });

      if (loadingText.parentNode) {
        loadingText.parentNode.removeChild(loadingText);
      }

      const infoText = document.createElement("div");
      infoText.style.position = "absolute";
      infoText.style.bottom = "10px";
      infoText.style.left = "10px";
      infoText.style.color = "white";
      infoText.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
      infoText.style.padding = "10px";
      infoText.style.borderRadius = "5px";
      infoText.style.fontSize = "14px";
      infoText.style.pointerEvents = "none";
      infoText.innerHTML = "Mouse: Rotate | Scroll: Zoom | Drag: Pan";
      container.appendChild(infoText);
    },
    (progress) => {
      const percent = Math.round((progress.loaded / progress.total) * 100);
      loadingText.textContent = `Loading parrot model... ${percent}%`;
    },
    (error) => {
      console.error("Error loading model:", error);
      loadingText.textContent = "Error loading model. Please refresh.";
    }
  );

  handleResizeFunction = () => {
    if (!container) return;

    const width = container.clientWidth;
    const height = container.clientHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    composer.setSize(width, height);

    bloomPass.resolution.set(width, height);
  };

  window.addEventListener("resize", handleResizeFunction);

  isInitialized = true;
  animate();
}

function animate(): void {
  if (!isInitialized) return;

  animationFrameId = requestAnimationFrame(animate);

  controls.update();

  const delta = clock.getDelta();
  if (mixer) {
    mixer.update(delta);
  }

  if (model) {
    model.traverse((child) => {
      if (
        child instanceof THREE.Mesh &&
        child.material instanceof THREE.ShaderMaterial
      ) {
        child.material.uniforms.time.value = clock.elapsedTime;
      }
    });
  }

  composer.render();
}

export function teardown(): void {
  if (!isInitialized) return;

  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }

  if (handleResizeFunction) {
    window.removeEventListener("resize", handleResizeFunction);
    handleResizeFunction = null;
  }

  if (scene) {
    scene.traverse((object) => {
      if (object instanceof THREE.Mesh) {
        if (object.geometry) {
          object.geometry.dispose();
        }

        if (object.material) {
          if (Array.isArray(object.material)) {
            object.material.forEach((material) => {
              disposeMaterial(material);
            });
          } else {
            disposeMaterial(object.material);
          }
        }
      }
    });
  }

  if (renderer && renderer.domElement && renderer.domElement.parentNode) {
    renderer.domElement.parentNode.removeChild(renderer.domElement);
  }

  if (renderer) {
    renderer.dispose();
  }

  isInitialized = false;
}

function disposeMaterial(material: THREE.Material) {
  // Cast to MeshStandardMaterial to access texture properties
  const mat = material as THREE.MeshStandardMaterial;
  
  // Safely dispose of textures if they exist
  if (mat.map) mat.map.dispose();
  if (mat.lightMap) mat.lightMap.dispose();
  if (mat.aoMap) mat.aoMap.dispose();
  if (mat.emissiveMap) mat.emissiveMap.dispose();
  if (mat.bumpMap) mat.bumpMap.dispose();
  if (mat.normalMap) mat.normalMap.dispose();
  if (mat.displacementMap) mat.displacementMap.dispose();
  if (mat.roughnessMap) mat.roughnessMap.dispose();
  if (mat.metalnessMap) mat.metalnessMap.dispose();
  if (mat.alphaMap) mat.alphaMap.dispose();
  if (mat.envMap) mat.envMap.dispose();
  
  // Dispose the material itself
  material.dispose();
}