Models 44

Nemotron 3 Ultra (free)

by NVIDIAReasoning1 console error
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 19 s
Cost
$0
Output tokens
10,434
Spent thinking
200 tokens
Speed
75.4 tok/s
Console errors
1
First token after
404 ms
First visible text after
2.4 s
Prompt tokens
448
Code size
29.9 KB
Finish reason
stop
Served by
Nvidia
Model id
nvidia/nemotron-3-ultra-550b-a55b:free
Reasoning sent
{"effort":"high"}
Max output
32,000 tokens
Generated
Nov 9, 2026

Console errors

Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.

THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false

Material Name: 
Material Type: PointsMaterial

Program Info Log: Vertex shader is not compiled.
VERTEX

ERROR: 0:77: 'size' : redefinition

  72: 
  73: 
  74:       attribute float size; attribute float alpha; attribute float speed;
  75:       varying float vAlpha; varying vec3 vPos;
  76:       uniform float uTime;
> 77:       uniform float size;
  78: uniform float scale;
  79: #define PI 3.141592653589793
  80: #define PI2 6.283185

What it was asked

The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 892 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Aquarium</title>
<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
  }
}
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a1a2f; font-family: system-ui, sans-serif; }
#canvas { display: block; width: 100vw; height: 100vh; }
#ui {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 10;
}
#title {
  color: #e8f4fd; font-size: clamp(1.5rem, 5vw, 3rem); font-weight: 300; letter-spacing: 0.1em;
  text-shadow: 0 2px 20px rgba(0,100,200,0.5); margin-bottom: 1rem; opacity: 0; animation: fadeIn 2s ease forwards;
}
#subtitle {
  color: #8ab4d8; font-size: clamp(0.9rem, 2.5vw, 1.2rem); opacity: 0; animation: fadeIn 2s ease 0.5s forwards;
  text-align: center; max-width: 80%; line-height: 1.6;
}
#hint {
  color: #5a8bc4; font-size: 0.85rem; margin-top: 2rem; opacity: 0; animation: pulse 2s ease-in-out 1.5s infinite;
}
@keyframes fadeIn { to { opacity: 1; } }
@keyframes pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
.fish-count { position: fixed; bottom: 1rem; right: 1rem; color: #5a8bc4; font-size: 0.8rem; opacity: 0.7; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="ui">
  <h1 id="title">AQUARIUM</h1>
  <p id="subtitle">Move your mouse near the fish — they'll swim away</p>
  <p id="hint">Click or press a key to start</p>
</div>
<div class="fish-count" id="fishCount">Fish: 0</div>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

// ── Config ──────────────────────────────────────────────────────────────
const CONFIG = {
  tankSize: { x: 40, y: 24, z: 24 },
  fishCount: 35,
  bubbleCount: 150,
  particleCount: 2000,
  causticSpeed: 0.15,
  mouseInfluenceRadius: 8,
  mouseForce: 0.8,
  wallAvoidanceDist: 3,
  fishMinSpeed: 0.02,
  fishMaxSpeed: 0.05,
  fishTurnSpeed: 0.03,
};

// ── Global State ────────────────────────────────────────────────────────
let renderer, scene, camera, composer, controls;
let fish = [], bubbles = [], particles = [], seaweed = [], rocks = [];
let causticTexture, time = 0, mouse3D = new THREE.Vector3(), mouseActive = false;
let raycaster = new THREE.Raycaster(), mouseNDC = new THREE.Vector2(-10, -10);
let started = false, animationId;

// ── Init ────────────────────────────────────────────────────────────────
function init() {
  // Renderer
  renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true, alpha: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.1;

  // Scene
  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x081830, 0.018);

  // Camera
  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
  camera.position.set(0, 2, 42);

  // Controls (disabled until start)
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;
  controls.enablePan = false;
  controls.minDistance = 15;
  controls.maxDistance = 60;
  controls.maxPolarAngle = Math.PI / 2 - 0.05;
  controls.minPolarAngle = 0.15;
  controls.autoRotate = true;
  controls.autoRotateSpeed = 0.3;
  controls.enabled = false;

  // Lighting
  setupLighting();
  // Environment
  createTank();
  createSubstrate();
  createSeaweed();
  createRocks();
  createFish();
  createBubbles();
  createParticles();
  createCaustics();
  // Post-processing
  setupPostProcessing();
  // Events
  window.addEventListener('resize', onResize);
  renderer.domElement.addEventListener('pointermove', onPointerMove);
  window.addEventListener('click', onStart);
  window.addEventListener('keydown', onStart);
  // Start loop
  animate();
}

function onStart() {
  if (started) return;
  started = true;
  controls.enabled = true;
  controls.autoRotate = false;
  document.getElementById('hint').style.display = 'none';
  document.getElementById('subtitle').textContent = 'Move your mouse near the fish — they\'ll swim away';
  window.removeEventListener('click', onStart);
  window.removeEventListener('keydown', onStart);
}

// ── Lighting ────────────────────────────────────────────────────────────
function setupLighting() {
  // Sunlight from top
  const sunLight = new THREE.DirectionalLight(0xffffff, 2.5);
  sunLight.position.set(10, 40, 15);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.set(2048, 2048);
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 100;
  sunLight.shadow.camera.left = -25;
  sunLight.shadow.camera.right = 25;
  sunLight.shadow.camera.top = 25;
  sunLight.shadow.camera.bottom = -25;
  sunLight.shadow.bias = -0.0005;
  scene.add(sunLight);

  // Ambient
  const ambient = new THREE.AmbientLight(0x4a7ab8, 0.6);
  scene.add(ambient);

  // Fill light
  const fillLight = new THREE.DirectionalLight(0x8ab4d8, 0.8);
  fillLight.position.set(-15, 10, -10);
  scene.add(fillLight);

  // Caustic projector (animated texture on tank bottom)
  // Handled in caustics section
}

// ── Tank ────────────────────────────────────────────────────────────────
function createTank() {
  const { x, y, z } = CONFIG.tankSize;
  const glassMat = new THREE.MeshPhysicalMaterial({
    color: 0x00ffff, metalness: 0, roughness: 0, transmission: 0.98,
    thickness: 0.5, ior: 1.33, clearcoat: 1, clearcoatRoughness: 0,
    opacity: 0.15, transparent: true, side: THREE.DoubleSide,
    envMapIntensity: 0.5,
  });

  // Back wall
  const backGeo = new THREE.BoxGeometry(x, y, 0.3);
  const back = new THREE.Mesh(backGeo, glassMat);
  back.position.z = -z / 2 - 0.15;
  back.receiveShadow = true;
  scene.add(back);

  // Side walls
  const sideGeo = new THREE.BoxGeometry(0.3, y, z);
  const left = new THREE.Mesh(sideGeo, glassMat);
  left.position.x = -x / 2 - 0.15;
  left.receiveShadow = true;
  scene.add(left);
  const right = left.clone();
  right.position.x = x / 2 + 0.15;
  scene.add(right);

  // Bottom (inside)
  const bottomGeo = new THREE.BoxGeometry(x - 0.6, 0.3, z - 0.6);
  const bottomMat = new THREE.MeshStandardMaterial({
    color: 0x1a2a3a, roughness: 0.9, metalness: 0,
  });
  const bottom = new THREE.Mesh(bottomGeo, bottomMat);
  bottom.position.y = -y / 2 + 0.15;
  bottom.receiveShadow = true;
  scene.add(bottom);

  // Top rim
  const rimGeo = new THREE.BoxGeometry(x + 0.6, 0.4, z + 0.6);
  const rimMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.7, metalness: 0.3 });
  const rim = new THREE.Mesh(rimGeo, rimMat);
  rim.position.y = y / 2 + 0.2;
  scene.add(rim);

  // Water volume (for refraction feel)
  const waterGeo = new THREE.BoxGeometry(x - 0.8, y - 0.8, z - 0.8);
  const waterMat = new THREE.MeshPhysicalMaterial({
    color: 0x003366, metalness: 0, roughness: 0, transmission: 0.9,
    thickness: 20, ior: 1.33, opacity: 0.1, transparent: true,
    depthWrite: false,
  });
  const water = new THREE.Mesh(waterGeo, waterMat);
  water.position.y = 0.2;
  scene.add(water);
}

// ── Substrate (Sand) ────────────────────────────────────────────────────
function createSubstrate() {
  const { x, z } = CONFIG.tankSize;
  const geo = new THREE.PlaneGeometry(x - 2, z - 2, 80, 80);
  // Add noise to vertices for natural look
  const pos = geo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    pos.setZ(i, (Math.random() - 0.5) * 0.3 + Math.sin(pos.getX(i) * 0.5) * Math.cos(pos.getY(i) * 0.5) * 0.15);
  }
  geo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    color: 0xd4c4a8, roughness: 0.95, metalness: 0,
    normalMap: createSandNormalMap(),
  });
  mat.normalMap.wrapS = mat.normalMap.wrapT = THREE.RepeatWrapping;
  mat.normalMap.repeat.set(20, 20);

  const sand = new THREE.Mesh(geo, mat);
  sand.rotation.x = -Math.PI / 2;
  sand.position.y = -CONFIG.tankSize.y / 2 + 0.35;
  sand.receiveShadow = true;
  scene.add(sand);
}

function createSandNormalMap() {
  const size = 256;
  const canvas = document.createElement('canvas');
  canvas.width = size; canvas.height = size;
  const ctx = canvas.getContext('2d');
  const imgData = ctx.createImageData(size, size);
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const n = Math.random() * 0.5 + 0.25;
      const idx = (y * size + x) * 4;
      imgData.data[idx] = 128 + (Math.random() - 0.5) * 30;     // R
      imgData.data[idx + 1] = 128 + (Math.random() - 0.5) * 30; // G
      imgData.data[idx + 2] = 255;                              // B
      imgData.data[idx + 3] = 255;
    }
  }
  ctx.putImageData(imgData, 0, 0);
  const tex = new THREE.CanvasTexture(canvas);
  return tex;
}

// ── Seaweed ─────────────────────────────────────────────────────────────
function createSeaweed() {
  const { x, z, y } = CONFIG.tankSize;
  const count = 40;
  const baseGeo = new THREE.CylinderGeometry(0.08, 0.2, 3, 6, 8, false);
  // Bend geometry
  const pos = baseGeo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const yPos = pos.getY(i);
    const t = (yPos + 1.5) / 3; // 0 to 1
    const bend = Math.sin(t * Math.PI * 1.5) * 0.3;
    pos.setX(i, pos.getX(i) + bend);
  }
  baseGeo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    color: 0x1a5a2a, roughness: 0.8, metalness: 0,
    side: THREE.DoubleSide, transparent: true, opacity: 0.9,
  });

  for (let i = 0; i < count; i++) {
    const mesh = new THREE.Mesh(baseGeo, mat);
    const rx = (Math.random() - 0.5) * (x - 6);
    const rz = (Math.random() - 0.5) * (z - 6);
    mesh.position.set(rx, -y / 2 + 1.8, rz);
    mesh.rotation.y = Math.random() * Math.PI * 2;
    mesh.scale.setScalar(0.7 + Math.random() * 0.8);
    mesh.userData = {
      swayPhase: Math.random() * Math.PI * 2,
      swaySpeed: 0.3 + Math.random() * 0.4,
      baseRotation: mesh.rotation.y,
    };
    seaweed.push(mesh);
    scene.add(mesh);
  }
}

// ── Rocks ───────────────────────────────────────────────────────────────
function createRocks() {
  const { x, z, y } = CONFIG.tankSize;
  const rockGeo = new THREE.DodecahedronGeometry(1, 0);
  const pos = rockGeo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const v = new THREE.Vector3().fromBufferAttribute(pos, i);
    v.multiplyScalar(0.7 + Math.random() * 0.6);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  rockGeo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    color: 0x3a3a4a, roughness: 0.95, metalness: 0.05,
  });

  const count = 18;
  for (let i = 0; i < count; i++) {
    const mesh = new THREE.Mesh(rockGeo, mat);
    const rx = (Math.random() - 0.5) * (x - 8);
    const rz = (Math.random() - 0.5) * (z - 8);
    mesh.position.set(rx, -y / 2 + 0.8, rz);
    mesh.rotation.set(
      Math.random() * 0.5, Math.random() * Math.PI * 2, Math.random() * 0.5
    );
    mesh.scale.setScalar(0.8 + Math.random() * 1.8);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    rocks.push(mesh);
    scene.add(mesh);
  }
}

// ── Fish ────────────────────────────────────────────────────────────────
function createFish() {
  const { x, y, z } = CONFIG.tankSize;
  const species = [
    { name: 'tetra', bodyColor: 0xff6b6b, finColor: 0xff4444, size: 0.6, speed: 1.0, school: true },
    { name: 'neon', bodyColor: 0x00ffff, finColor: 0x00cccc, size: 0.45, speed: 1.2, school: true },
    { name: 'gold', bodyColor: 0xffd700, finColor: 0xffaa00, size: 0.9, speed: 0.7, school: false },
    { name: 'angel', bodyColor: 0xffffff, finColor: 0xcccccc, size: 1.1, speed: 0.5, school: false, stripes: true },
    { name: 'guppy', bodyColor: 0xff69b4, finColor: 0xff1493, size: 0.5, speed: 1.3, school: true, tail: 'fan' },
    { name: 'betta', bodyColor: 0x4b0082, finColor: 0x800080, size: 0.8, speed: 0.6, school: false, tail: 'veil' },
  ];

  for (let i = 0; i < CONFIG.fishCount; i++) {
    const sp = species[Math.floor(Math.random() * species.length)];
    const f = createFishMesh(sp);
    f.position.set(
      (Math.random() - 0.5) * (x - 4),
      (Math.random() - 0.5) * (y - 4),
      (Math.random() - 0.5) * (z - 4)
    );
    f.rotation.y = Math.random() * Math.PI * 2;
    f.userData = {
      ...sp,
      velocity: new THREE.Vector3(),
      targetDir: new THREE.Vector3(),
      turnPhase: Math.random() * Math.PI * 2,
      swimPhase: Math.random() * Math.PI * 2,
      swimSpeed: 0.5 + Math.random() * 0.5,
      baseSpeed: (CONFIG.fishMinSpeed + Math.random() * (CONFIG.fishMaxSpeed - CONFIG.fishMinSpeed)) * sp.speed,
      panicLevel: 0,
      schoolCenter: new THREE.Vector3(),
    };
    fish.push(f);
    scene.add(f);
  }
  updateFishCount();
}

function createFishMesh(species) {
  const group = new THREE.Group();
  const s = species.size;

  // Body
  const bodyGeo = new THREE.CapsuleGeometry(0.35 * s, 1.0 * s, 8, 16);
  const bodyMat = new THREE.MeshStandardMaterial({
    color: species.bodyColor,
    roughness: 0.4, metalness: 0.3,
    emissive: species.bodyColor, emissiveIntensity: 0.1,
  });
  if (species.stripes) {
    // Add stripe pattern via vertex colors
    const pos = bodyGeo.attributes.position;
    const colors = [];
    for (let i = 0; i < pos.count; i++) {
      const y = pos.getY(i);
      const stripe = Math.sin(y * 8) > 0 ? 0.3 : 1.0;
      colors.push(stripe, stripe, stripe);
    }
    bodyGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    bodyMat.vertexColors = true;
  }
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.castShadow = true;
  group.add(body);

  // Tail fin
  const tailGeo = new THREE.PlaneGeometry(0.7 * s, 0.6 * s);
  tailGeo.translate(-0.8 * s, 0, 0);
  const tailMat = new THREE.MeshStandardMaterial({
    color: species.finColor, side: THREE.DoubleSide,
    transparent: true, opacity: 0.7, roughness: 0.6, metalness: 0.1,
  });
  const tail = new THREE.Mesh(tailGeo, tailMat);
  tail.userData.isFin = true;
  tail.userData.finType = 'tail';
  group.add(tail);

  // Dorsal fin
  const dorsalGeo = new THREE.PlaneGeometry(0.5 * s, 0.4 * s);
  dorsalGeo.translate(0, 0.35 * s, 0);
  dorsalGeo.rotateX(-Math.PI / 2);
  const dorsal = new THREE.Mesh(dorsalGeo, tailMat.clone());
  dorsal.userData.isFin = true;
  dorsal.userData.finType = 'dorsal';
  group.add(dorsal);

  // Pectoral fins (pair)
  for (let side of [-1, 1]) {
    const pGeo = new THREE.PlaneGeometry(0.35 * s, 0.25 * s);
    pGeo.translate(0.1 * s, -0.1 * s, side * 0.3 * s);
    pGeo.rotateY(side * Math.PI / 3);
    const pFin = new THREE.Mesh(pGeo, tailMat.clone());
    pFin.userData.isFin = true;
    pFin.userData.finType = 'pectoral';
    pFin.userData.side = side;
    group.add(pFin);
  }

  // Eye
  const eyeGeo = new THREE.SphereGeometry(0.08 * s, 8, 8);
  const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
  for (let side of [-1, 1]) {
    const eye = new THREE.Mesh(eyeGeo, eyeMat);
    eye.position.set(0.5 * s, 0.1 * s, side * 0.25 * s);
    group.add(eye);
  }

  group.userData.finMeshes = group.children.filter(c => c.userData.isFin);
  return group;
}

function updateFishCount() {
  document.getElementById('fishCount').textContent = `Fish: ${fish.length}`;
}

// ── Bubbles ─────────────────────────────────────────────────────────────
function createBubbles() {
  const { x, y, z } = CONFIG.tankSize;
  const geo = new THREE.IcosahedronGeometry(0.08, 1);
  const mat = new THREE.MeshPhysicalMaterial({
    color: 0xffffff, metalness: 0, roughness: 0, transmission: 0.9,
    thickness: 0.1, ior: 1.33, transparent: true, opacity: 0.4,
  });

  for (let i = 0; i < CONFIG.bubbleCount; i++) {
    const b = new THREE.Mesh(geo, mat);
    resetBubble(b);
    b.userData.speed = 0.02 + Math.random() * 0.04;
    b.userData.wobble = Math.random() * Math.PI * 2;
    b.userData.wobbleSpeed = 0.5 + Math.random();
    bubbles.push(b);
    scene.add(b);
  }
}

function resetBubble(bubble) {
  const { x, y, z } = CONFIG.tankSize;
  bubble.position.set(
    (Math.random() - 0.5) * (x - 4),
    -y / 2 + 1,
    (Math.random() - 0.5) * (z - 4)
  );
  bubble.scale.setScalar(0.3 + Math.random() * 0.7);
}

// ── Particles (floating debris) ─────────────────────────────────────────
function createParticles() {
  const { x, y, z } = CONFIG.tankSize;
  const geo = new THREE.BufferGeometry();
  const positions = new Float32Array(CONFIG.particleCount * 3);
  const sizes = new Float32Array(CONFIG.particleCount);
  const alphas = new Float32Array(CONFIG.particleCount);
  const speeds = new Float32Array(CONFIG.particleCount);

  for (let i = 0; i < CONFIG.particleCount; i++) {
    positions[i * 3] = (Math.random() - 0.5) * x;
    positions[i * 3 + 1] = (Math.random() - 0.5) * y;
    positions[i * 3 + 2] = (Math.random() - 0.5) * z;
    sizes[i] = Math.random() * 0.08 + 0.02;
    alphas[i] = Math.random() * 0.3 + 0.1;
    speeds[i] = Math.random() * 0.003 + 0.001;
  }
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  geo.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
  geo.setAttribute('speed', new THREE.BufferAttribute(speeds, 1));

  const mat = new THREE.PointsMaterial({
    color: 0x8ab4d8, size: 0.1, transparent: true, opacity: 0.4,
    vertexColors: false, sizeAttenuation: true, depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  // Custom shader for per-particle alpha/size
  mat.onBeforeCompile = (shader) => {
    shader.uniforms.uTime = { value: 0 };
    shader.vertexShader = `
      attribute float size; attribute float alpha; attribute float speed;
      varying float vAlpha; varying vec3 vPos;
      uniform float uTime;
      ${shader.vertexShader}
    `.replace(
      'void main() {',
      `void main() {
        vAlpha = alpha;
        vPos = position;
        vec3 pos = position;
        pos.y += mod(uTime * speed * 1000.0 + position.x * 10.0, ${y.toFixed(1)}) - ${(y/2).toFixed(1)};
        pos.x += sin(uTime * 0.5 + position.z) * 0.1;
        pos.z += cos(uTime * 0.3 + position.x) * 0.1;
      `
    ).replace(
      'gl_PointSize = size;',
      'gl_PointSize = size * (300.0 / -mvPosition.z);'
    );
    shader.fragmentShader = `
      varying float vAlpha; varying vec3 vPos;
      ${shader.fragmentShader}
    `.replace(
      'gl_FragColor = vec4( outgoingLight, diffuseColor.a );',
      `float dist = length(gl_PointCoord - 0.5);
       float a = smoothstep(0.5, 0.0, dist) * vAlpha;
       gl_FragColor = vec4(outgoingLight, a);`
    );
    particles.material = mat;
  };

  const pts = new THREE.Points(geo, mat);
  pts.userData.speeds = speeds;
  pts.userData.yRange = y;
  particles.push(pts);
  scene.add(pts);
}

// ── Caustics ────────────────────────────────────────────────────────────
function createCaustics() {
  // Generate caustic texture procedurally
  const size = 512;
  const canvas = document.createElement('canvas');
  canvas.width = size; canvas.height = size;
  const ctx = canvas.getContext('2d');
  const imgData = ctx.createImageData(size, size);

  // Multiple layers of noise for caustic pattern
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      let v = 0;
      // Layer 1
      v += Math.sin(x * 0.05 + y * 0.03) * Math.cos(x * 0.02 - y * 0.04);
      // Layer 2
      v += Math.sin(x * 0.08 + y * 0.06) * Math.cos(x * 0.04 - y * 0.07) * 0.5;
      // Layer 3
      v += Math.sin(x * 0.12) * Math.cos(y * 0.1) * 0.3;
      // Sharpen
      v = Math.pow(Math.max(0, v * 1.5 + 0.5), 2.5);
      const idx = (y * size + x) * 4;
      const val = Math.min(255, v * 255);
      imgData.data[idx] = val;
      imgData.data[idx + 1] = val;
      imgData.data[idx + 2] = val;
      imgData.data[idx + 3] = 255;
    }
  }
  ctx.putImageData(imgData, 0, 0);
  causticTexture = new THREE.CanvasTexture(canvas);
  causticTexture.wrapS = causticTexture.wrapT = THREE.RepeatWrapping;
  causticTexture.repeat.set(3, 3);

  // Projector light for caustics on bottom
  const causticLight = new THREE.SpotLight(0x4ab8ff, 1.5, 50, Math.PI / 4, 0.5, 1);
  causticLight.position.set(0, CONFIG.tankSize.y / 2 - 1, 0);
  causticLight.target.position.set(0, -CONFIG.tankSize.y / 2 + 0.5, 0);
  causticLight.map = causticTexture;
  scene.add(causticLight);
  scene.add(causticLight.target);
  causticLight.userData.isCaustic = true;
}

// ── Post Processing ─────────────────────────────────────────────────────
function setupPostProcessing() {
  composer = new EffectComposer(renderer);
  composer.addPass(new RenderPass(scene, camera));

  // Bloom for light rays
  const bloom = new UnrealBloomPass(
    new THREE.Vector2(window.innerWidth, window.innerHeight),
    0.3, 0.6, 0.85
  );
  bloom.threshold = 0.8;
  composer.addPass(bloom);

  // Underwater color grading / distortion
  const underwaterShader = {
    uniforms: {
      tDiffuse: { value: null },
      uTime: { value: 0 },
      uDepth: { value: 0 },
    },
    vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
    fragmentShader: `
      uniform sampler2D tDiffuse; uniform float uTime; uniform float uDepth;
      varying vec2 vUv;
      void main() {
        vec2 uv = vUv;
        // Subtle distortion
        uv += vec2(sin(uTime * 0.5 + vUv.y * 20.0), cos(uTime * 0.3 + vUv.x * 15.0)) * 0.0015;
        vec4 color = texture2D(tDiffuse, uv);
        // Color grading - deeper blue tint
        color.rgb *= vec3(0.95, 1.0, 1.15);
        // Vignette
        float vig = 1.0 - length(vUv - 0.5) * 0.6;
        color.rgb *= vig;
        gl_FragColor = color;
      }
    `,
  };
  const underwaterPass = new ShaderPass(underwaterShader);
  composer.addPass(underwaterPass);
  composer.underwaterPass = underwaterPass;
}

// ── Input ───────────────────────────────────────────────────────────────
function onPointerMove(e) {
  mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
  mouseActive = true;
}

function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
}

// ── Animation ───────────────────────────────────────────────────────────
function animate() {
  animationId = requestAnimationFrame(animate);
  time += 1/60; // Fixed timestep for physics
  const dt = 1/60;

  if (started) {
    updateMouse3D();
    updateFish(dt);
    updateSeaweed(dt);
    updateBubbles(dt);
    updateParticles(dt);
    updateCaustics(dt);
    controls.update();
  } else {
    // Demo mode: auto-rotate camera slowly
    const radius = 42;
    camera.position.x = Math.sin(time * 0.1) * radius;
    camera.position.z = Math.cos(time * 0.1) * radius;
    camera.lookAt(0, 0, 0);
  }

  // Update post-processing time
  if (composer.underwaterPass) {
    composer.underwaterPass.uniforms.uTime.value = time;
  }

  composer.render();
}

function updateMouse3D() {
  raycaster.setFromCamera(mouseNDC, camera);
  // Intersect with tank water volume (invisible plane at center)
  const plane = new THREE.Plane(new THREE.Vector3(0, 0, -1), 0);
  raycaster.ray.intersectPlane(plane, mouse3D);
  // Clamp to tank bounds
  const { x, y, z } = CONFIG.tankSize;
  mouse3D.x = THREE.MathUtils.clamp(mouse3D.x, -x/2 + 2, x/2 - 2);
  mouse3D.y = THREE.MathUtils.clamp(mouse3D.y, -y/2 + 2, y/2 - 2);
  mouse3D.z = THREE.MathUtils.clamp(mouse3D.z, -z/2 + 2, z/2 - 2);
}

function updateFish(dt) {
  const { x, y, z } = CONFIG.tankSize;
  const halfX = x/2 - 1.5, halfY = y/2 - 1.5, halfZ = z/2 - 1.5;

  // First pass: compute school centers
  const schools = new Map();
  fish.forEach(f => {
    if (f.userData.school) {
      const key = Math.floor(f.position.x / 10) * 1000 + Math.floor(f.position.z / 10);
      if (!schools.has(key)) schools.set(key, { center: new THREE.Vector3(), count: 0, vel: new THREE.Vector3() });
      const s = schools.get(key);
      s.center.add(f.position);
      s.vel.add(f.userData.velocity);
      s.count++;
    }
  });
  schools.forEach(s => {
    s.center.divideScalar(s.count);
    s.vel.divideScalar(s.count);
  });

  fish.forEach(f => {
    const ud = f.userData;
    const pos = f.position;
    const vel = ud.velocity;
    let steer = new THREE.Vector3();

    // 1. Wall avoidance
    const wallForce = new THREE.Vector3();
    if (pos.x > halfX) wallForce.x = -1;
    else if (pos.x < -halfX) wallForce.x = 1;
    if (pos.y > halfY) wallForce.y = -1;
    else if (pos.y < -halfY) wallForce.y = 1;
    if (pos.z > halfZ) wallForce.z = -1;
    else if (pos.z < -halfZ) wallForce.z = 1;
    if (wallForce.length() > 0) {
      wallForce.normalize().multiplyScalar(0.5);
      steer.add(wallForce);
    }

    // 2. Mouse avoidance
    if (mouseActive) {
      const toMouse = new THREE.Vector3().subVectors(pos, mouse3D);
      const dist = toMouse.length();
      if (dist < CONFIG.mouseInfluenceRadius && dist > 0.01) {
        const force = (1 - dist / CONFIG.mouseInfluenceRadius) * CONFIG.mouseForce;
        toMouse.normalize().multiplyScalar(force);
        steer.add(toMouse);
        ud.panicLevel = Math.min(1, ud.panicLevel + dt * 3);
      }
    }
    ud.panicLevel = Math.max(0, ud.panicLevel - dt * 0.5);

    // 3. Schooling behavior
    if (ud.school) {
      const key = Math.floor(pos.x / 10) * 1000 + Math.floor(pos.z / 10);
      const school = schools.get(key);
      if (school && school.count > 1) {
        // Cohesion
        const toCenter = new THREE.Vector3().subVectors(school.center, pos);
        if (toCenter.length() > 2) steer.add(toCenter.normalize().multiplyScalar(0.02));
        // Alignment
        steer.add(school.vel.clone().normalize().multiplyScalar(0.03));
        // Separation
        fish.forEach(other => {
          if (other !== f) {
            const diff = new THREE.Vector3().subVectors(pos, other.position);
            const d = diff.length();
            if (d < 2 && d > 0.01) {
              steer.add(diff.normalize().multiplyScalar((2 - d) * 0.05));
            }
          }
        });
      }
    }

    // 4. Random wandering
    ud.turnPhase += dt * 0.5;
    const wander = new THREE.Vector3(
      Math.sin(ud.turnPhase) * 0.02,
      Math.sin(ud.turnPhase * 0.7) * 0.01,
      Math.cos(ud.turnPhase) * 0.02
    );
    steer.add(wander);

    // Apply steering
    vel.add(steer.multiplyScalar(dt * 60));

    // Speed limiting
    const maxSpeed = ud.baseSpeed * (1 + ud.panicLevel * 2);
    const minSpeed = ud.baseSpeed * 0.3;
    const speed = vel.length();
    if (speed > maxSpeed) vel.setLength(maxSpeed);
    else if (speed < minSpeed && speed > 0.001) vel.setLength(minSpeed);

    // Update position
    pos.add(vel.clone().multiplyScalar(dt * 60));

    // Orient fish to velocity
    if (vel.length() > 0.001) {
      const targetQuat = new THREE.Quaternion().setFromUnitVectors(
        new THREE.Vector3(1, 0, 0), // Fish faces +X
        vel.clone().normalize()
      );
      f.quaternion.slerp(targetQuat, ud.fishTurnSpeed * (1 + ud.panicLevel * 3) * dt * 60);
    }

    // Fin animation
    ud.swimPhase += dt * 60 * ud.swimSpeed * (1 + ud.panicLevel * 2);
    f.userData.finMeshes.forEach(fin => {
      const ft = fin.userData.finType;
      const side = fin.userData.side || 1;
      if (ft === 'tail') {
        fin.rotation.z = Math.sin(ud.swimPhase * 1.5) * 0.6 * (0.7 + ud.panicLevel * 0.6);
      } else if (ft === 'dorsal') {
        fin.rotation.z = Math.sin(ud.swimPhase * 0.8) * 0.2;
      } else if (ft === 'pectoral') {
        fin.rotation.x = Math.sin(ud.swimPhase * 2 + side) * 0.4 * (0.5 + ud.panicLevel);
      }
    });

    // Body undulation (subtle)
    const body = f.children[0];
    if (body) {
      body.scale.y = 1 + Math.sin(ud.swimPhase * 2) * 0.02 * (1 + ud.panicLevel);
      body.scale.z = 1 - Math.sin(ud.swimPhase * 2) * 0.015;
    }
  });
}

function updateSeaweed(dt) {
  seaweed.forEach(sw => {
    const ud = sw.userData;
    ud.swayPhase += dt * 60 * ud.swaySpeed;
    const sway = Math.sin(ud.swayPhase) * 0.15;
    sw.rotation.y = ud.baseRotation + sway;
    // Bend segments (approximate by rotating whole mesh)
    sw.rotation.x = Math.sin(ud.swayPhase * 0.7) * 0.05;
  });
}

function updateBubbles(dt) {
  const { y, x, z } = CONFIG.tankSize;
  const halfY = y/2 - 0.5;
  bubbles.forEach(b => {
    b.position.y += b.userData.speed * dt * 60;
    b.userData.wobble += dt * 60 * b.userData.wobbleSpeed;
    b.position.x += Math.sin(b.userData.wobble) * 0.005;
    b.position.z += Math.cos(b.userData.wobble * 0.7) * 0.005;
    if (b.position.y > halfY) resetBubble(b);
  });
}

function updateParticles(dt) {
  particles.forEach(pts => {
    const pos = pts.geometry.attributes.position;
    const speeds = pts.userData.speeds;
    const yRange = pts.userData.yRange;
    const halfY = yRange / 2;
    for (let i = 0; i < pos.count; i++) {
      let py = pos.getY(i);
      py += speeds[i] * dt * 60;
      if (py > halfY) py = -halfY;
      pos.setY(i, py);
    }
    pos.needsUpdate = true;
    pts.material.uniforms.uTime.value = time;
  });
}

function updateCaustics(dt) {
  // Animate caustic texture offset
  if (causticTexture) {
    causticTexture.offset.x = time * 0.02;
    causticTexture.offset.y = time * 0.015;
  }
  // Rotate caustic light slightly
  scene.traverse(obj => {
    if (obj.userData.isCaustic) {
      obj.position.x = Math.sin(time * 0.1) * 3;
      obj.position.z = Math.cos(time * 0.1) * 3;
      obj.target.position.x = obj.position.x;
      obj.target.position.z = obj.position.z;
    }
  });
}

// ── Start ───────────────────────────────────────────────────────────────
init();
</script>
</body>
</html>
Reasoning trace (200 tokens)
The user wants a 3D aquarium with Three.js where fish swim away from the mouse cursor. Let me create a visually impressive demo with:

1. A 3D aquarium environment (glass walls, sand bottom, plants, rocks)
2. Multiple fish that swim naturally
3. Fish that react to mouse proximity by swimming away
4. Good lighting, caustics, bubbles, particles
5. Self-running demo mode

I'll use the import map provided for Three.js. Let me build this step by step.

Key features:
- Aquarium box with glass walls
- Sand bottom with procedural texture
- Seaweed/plants that sway
- Rocks and decorations
- Multiple fish with different colors/sizes
- Fish AI: flocking + avoidance of walls + mouse avoidance
- Mouse tracking in 3D space
- Bubbles rising
- Caustic light effects
- Post-processing for underwater look

Let me write the complete HTML file.