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();
}