Models 44

Nemotron 3 Ultra (free)

by NVIDIAReasoningClean
Open in new window

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

How it went

Total time
1 min 27 s
Cost
$0
Output tokens
8,262
Spent thinking
0 tokens
Speed
95.8 tok/s
Console errors
0
First token after
426 ms
First visible text after
426 ms
Prompt tokens
448
Code size
24.4 KB
Finish reason
stop
Served by
Nvidia
Model id
nvidia/nemotron-3-ultra-550b-a55b:free
Reasoning sent
{"effort":"none"}
Max output
32,000 tokens
Generated
Nov 9, 2026

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 · 740 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: #000; font-family: system-ui, sans-serif; }
  canvas { display: block; }
  #ui {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    pointer-events: none; color: #fff; z-index: 10;
  }
  #title { font-size: clamp(1.5rem, 4vw, 3rem); font-weight: 300; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 1rem; opacity: 0.9; }
  #hint { font-size: clamp(0.875rem, 2vw, 1rem); opacity: 0.5; animation: pulse 2s ease-in-out infinite; }
  @keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.8; } }
  .fish-count { position: fixed; bottom: 1.5rem; right: 1.5rem; font-size: 0.875rem; opacity: 0.4; pointer-events: none; }
</style>
</head>
<body>
<div id="ui">
  <div id="title">Aquarium</div>
  <div id="hint">Move your mouse near the fish</div>
</div>
<div class="fish-count">Fish: <span id="count">0</span></div>
<script type="module">
import * as THREE from 'three';

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 200);
camera.position.set(0, 0, 25);

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// Water volume (fog)
scene.fog = new THREE.FogExp2(0x001a2e, 0.015);

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

const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
sunLight.position.set(20, 40, 20);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(2048, 2048);
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 100;
sunLight.shadow.camera.left = -40;
sunLight.shadow.camera.right = 40;
sunLight.shadow.camera.top = 40;
sunLight.shadow.camera.bottom = -40;
sunLight.shadow.bias = -0.001;
scene.add(sunLight);

const causticLight = new THREE.DirectionalLight(0x88ccff, 0.3);
causticLight.position.set(-15, 30, -10);
scene.add(causticLight);

// Tank boundaries
const TANK_SIZE = { x: 35, y: 18, z: 35 };
const MARGIN = 2;

// Geometry cache
const geometries = {
  body: new THREE.SphereGeometry(1, 16, 12),
  fin: new THREE.PlaneGeometry(1, 1, 8, 4),
  tail: new THREE.PlaneGeometry(1.2, 0.8, 6, 4),
  eye: new THREE.SphereGeometry(0.15, 8, 6),
  plantLeaf: new THREE.PlaneGeometry(1, 2, 4, 8),
  rock: new THREE.DodecahedronGeometry(1, 0),
  bubble: new THREE.SphereGeometry(0.08, 6, 4),
  particle: new THREE.SphereGeometry(0.03, 4, 3),
  coralBranch: new THREE.CylinderGeometry(0.08, 0.12, 1.5, 6, 4, true),
};

// Materials
const materials = {
  water: new THREE.MeshPhysicalMaterial({
    color: 0x003d5c,
    metalness: 0,
    roughness: 0.1,
    transmission: 0.98,
    thickness: 2,
    ior: 1.33,
    transparent: true,
    opacity: 0.15,
    side: THREE.DoubleSide,
  }),
  glass: new THREE.MeshPhysicalMaterial({
    color: 0x88ccff,
    metalness: 0.1,
    roughness: 0.05,
    transmission: 0.99,
    thickness: 0.5,
    ior: 1.5,
    transparent: true,
    opacity: 0.05,
    side: THREE.DoubleSide,
  }),
  sand: new THREE.MeshStandardMaterial({
    color: 0xe8d5b7,
    roughness: 0.9,
    metalness: 0.05,
  }),
  plant: new THREE.MeshStandardMaterial({
    color: 0x2d7d46,
    roughness: 0.8,
    metalness: 0.0,
    side: THREE.DoubleSide,
    transparent: true,
    opacity: 0.9,
  }),
  rock: new THREE.MeshStandardMaterial({
    color: 0x5a5a5a,
    roughness: 0.95,
    metalness: 0.1,
  }),
  coral: new THREE.MeshStandardMaterial({
    color: 0xff6b6b,
    roughness: 0.7,
    metalness: 0.1,
    side: THREE.DoubleSide,
  }),
  bubble: new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    metalness: 0.1,
    roughness: 0.05,
    transmission: 0.9,
    thickness: 0.1,
    ior: 1.33,
    transparent: true,
    opacity: 0.4,
  }),
  particle: new THREE.MeshBasicMaterial({
    color: 0xffffff,
    transparent: true,
    opacity: 0.3,
  }),
};

// Fish colors
const FISH_SPECIES = [
  { name: 'Neon Tetra', bodyColor: 0x00ffff, accentColor: 0xff0066, size: 0.6, speed: 1.8, schooling: true },
  { name: 'Betta', bodyColor: 0x8b00ff, accentColor: 0xff3366, size: 1.0, speed: 1.0, schooling: false },
  { name: 'Goldfish', bodyColor: 0xffaa00, accentColor: 0xff6600, size: 1.2, speed: 0.8, schooling: false },
  { name: 'Angelfish', bodyColor: 0xffffff, accentColor: 0x000000, size: 1.1, speed: 0.9, schooling: false },
  { name: 'Guppy', bodyColor: 0xff00aa, accentColor: 0xffff00, size: 0.5, speed: 1.5, schooling: true },
  { name: 'Discus', bodyColor: 0xff4444, accentColor: 0x00ffff, size: 1.3, speed: 0.7, schooling: false },
  { name: 'Zebra Danio', bodyColor: 0xffffff, accentColor: 0x000000, size: 0.55, speed: 2.0, schooling: true },
];

// Mouse tracking
const mouse = new THREE.Vector2(0, 0);
const mouseWorld = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
let mouseInScene = false;

window.addEventListener('mousemove', e => {
  mouse.x = (e.clientX / innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / innerHeight) * 2 + 1;
  mouseInScene = true;
});

window.addEventListener('mouseleave', () => { mouseInScene = false; });

function updateMouseWorld() {
  raycaster.setFromCamera(mouse, camera);
  const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -camera.position.z);
  raycaster.ray.intersectPlane(plane, mouseWorld);
}

// Fish class
class Fish {
  constructor(species, index) {
    this.species = species;
    this.index = index;
    this.size = species.size * (0.85 + Math.random() * 0.3);
    this.speed = species.speed * (0.8 + Math.random() * 0.4);
    this.turnSpeed = 0.03 + Math.random() * 0.02;
    
    // Position
    this.position = new THREE.Vector3(
      (Math.random() - 0.5) * (TANK_SIZE.x - 4),
      (Math.random() - 0.5) * (TANK_SIZE.y - 4),
      (Math.random() - 0.5) * (TANK_SIZE.z - 4)
    );
    this.velocity = new THREE.Vector3();
    this.targetVelocity = new THREE.Vector3();
    this.acceleration = new THREE.Vector3();
    
    // Animation
    this.swimPhase = Math.random() * Math.PI * 2;
    this.swimSpeed = 0.15 + Math.random() * 0.1;
    this.bodyRotation = new THREE.Euler(0, Math.random() * Math.PI * 2, 0);
    this.targetRotation = new THREE.Euler();
    
    // Behavior
    this.wanderTarget = new THREE.Vector3();
    this.wanderTimer = 0;
    this.fear = 0;
    this.schooling = species.schooling;
    this.schoolCenter = new THREE.Vector3();
    this.schoolVelocity = new THREE.Vector3();
    
    // Mesh
    this.group = new THREE.Group();
    this.createMesh();
    scene.add(this.group);
  }
  
  createMesh() {
    const { bodyColor, accentColor } = this.species;
    const s = this.size;
    
    // Body material with gradient
    const bodyMat = new THREE.MeshPhysicalMaterial({
      color: bodyColor,
      metalness: 0.3,
      roughness: 0.4,
      clearcoat: 0.8,
      clearcoatRoughness: 0.1,
      transmissive: 0.1,
    });
    
    const finMat = new THREE.MeshPhysicalMaterial({
      color: accentColor,
      metalness: 0.2,
      roughness: 0.3,
      clearcoat: 0.5,
      side: THREE.DoubleSide,
      transparent: true,
      opacity: 0.7,
    });
    
    const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
    
    // Body
    this.bodyMesh = new THREE.Mesh(geometries.body, bodyMat);
    this.bodyMesh.scale.set(s * 1.8, s * 0.9, s * 0.7);
    this.bodyMesh.castShadow = true;
    this.group.add(this.bodyMesh);
    
    // Tail
    this.tailMesh = new THREE.Mesh(geometries.tail, finMat);
    this.tailMesh.position.set(-s * 1.6, 0, 0);
    this.tailMesh.rotation.y = Math.PI / 2;
    this.group.add(this.tailMesh);
    
    // Dorsal fin
    this.dorsalMesh = new THREE.Mesh(geometries.fin, finMat);
    this.dorsalMesh.position.set(0, s * 0.55, 0);
    this.dorsalMesh.rotation.x = -Math.PI / 2;
    this.dorsalMesh.scale.set(s * 0.8, s * 0.6, 1);
    this.group.add(this.dorsalMesh);
    
    // Pectoral fins
    this.pectoralL = new THREE.Mesh(geometries.fin, finMat);
    this.pectoralL.position.set(s * 0.2, -s * 0.2, s * 0.5);
    this.pectoralL.rotation.y = Math.PI / 2;
    this.pectoralL.rotation.z = -0.3;
    this.pectoralL.scale.set(s * 0.5, s * 0.4, 1);
    this.group.add(this.pectoralL);
    
    this.pectoralR = new THREE.Mesh(geometries.fin, finMat);
    this.pectoralR.position.set(s * 0.2, -s * 0.2, -s * 0.5);
    this.pectoralR.rotation.y = Math.PI / 2;
    this.pectoralR.rotation.z = 0.3;
    this.pectoralR.scale.set(s * 0.5, s * 0.4, 1);
    this.group.add(this.pectoralR);
    
    // Eyes
    this.eyeL = new THREE.Mesh(geometries.eye, eyeMat);
    this.eyeL.position.set(s * 0.7, s * 0.15, s * 0.35);
    this.eyeL.scale.set(1, 0.7, 1);
    this.group.add(this.eyeL);
    
    this.eyeR = new THREE.Mesh(geometries.eye, eyeMat);
    this.eyeR.position.set(s * 0.7, s * 0.15, -s * 0.35);
    this.eyeR.scale.set(1, 0.7, 1);
    this.group.add(this.eyeR);
    
    // Accent stripe for some species
    if (this.species.name === 'Neon Tetra' || this.species.name === 'Zebra Danio') {
      const stripeMat = new THREE.MeshBasicMaterial({ color: accentColor, side: THREE.DoubleSide });
      const stripeGeo = new THREE.PlaneGeometry(s * 1.6, s * 0.3, 4, 2);
      this.stripeMesh = new THREE.Mesh(stripeGeo, stripeMat);
      this.stripeMesh.position.set(0, s * 0.35, s * 0.355);
      this.group.add(this.stripeMesh);
      this.stripeMesh2 = this.stripeMesh.clone();
      this.stripeMesh2.position.z = -s * 0.355;
      this.group.add(this.stripeMesh2);
    }
    
    // Angelfish stripes
    if (this.species.name === 'Angelfish') {
      const stripeMat = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });
      for (let i = 0; i < 4; i++) {
        const stripe = new THREE.Mesh(new THREE.PlaneGeometry(s * 1.8, s * 0.8, 2, 2), stripeMat);
        stripe.position.set((i - 1.5) * s * 0.4, 0, s * 0.355);
        this.group.add(stripe);
        const stripe2 = stripe.clone();
        stripe2.position.z = -s * 0.355;
        this.group.add(stripe2);
      }
    }
  }
  
  update(dt, allFish) {
    this.swimPhase += this.swimSpeed * this.speed * dt * 10;
    
    // Calculate forces
    this.acceleration.set(0, 0, 0);
    
    // Wandering
    this.wanderTimer -= dt;
    if (this.wanderTimer <= 0) {
      this.wanderTarget.set(
        (Math.random() - 0.5) * (TANK_SIZE.x - 4),
        (Math.random() - 0.5) * (TANK_SIZE.y - 4),
        (Math.random() - 0.5) * (TANK_SIZE.z - 4)
      );
      this.wanderTimer = 3 + Math.random() * 5;
    }
    
    const toWander = new THREE.Vector3().subVectors(this.wanderTarget, this.position);
    const wanderDist = toWander.length();
    if (wanderDist > 0.1) {
      toWander.normalize().multiplyScalar(0.5);
      this.acceleration.add(toWander);
    }
    
    // Tank boundaries
    const bounds = { x: TANK_SIZE.x / 2 - MARGIN, y: TANK_SIZE.y / 2 - MARGIN, z: TANK_SIZE.z / 2 - MARGIN };
    if (Math.abs(this.position.x) > bounds.x) this.acceleration.x -= Math.sign(this.position.x) * 2;
    if (Math.abs(this.position.y) > bounds.y) this.acceleration.y -= Math.sign(this.position.y) * 2;
    if (Math.abs(this.position.z) > bounds.z) this.acceleration.z -= Math.sign(this.position.z) * 2;
    
    // Mouse avoidance
    if (mouseInScene) {
      const toMouse = new THREE.Vector3().subVectors(this.position, mouseWorld);
      const dist = toMouse.length();
      const fearRadius = 6 + this.size * 2;
      if (dist < fearRadius && dist > 0.01) {
        const force = (fearRadius - dist) / fearRadius * 8;
        toMouse.normalize().multiplyScalar(force);
        this.acceleration.add(toMouse);
        this.fear = Math.min(1, this.fear + dt * 3);
      } else {
        this.fear = Math.max(0, this.fear - dt);
      }
    } else {
      this.fear = Math.max(0, this.fear - dt);
    }
    
    // Schooling behavior
    if (this.schooling && allFish.length > 1) {
      let neighbors = 0;
      this.schoolCenter.set(0, 0, 0);
      this.schoolVelocity.set(0, 0, 0);
      const schoolRadius = 8;
      
      for (const other of allFish) {
        if (other === this || !other.schooling) continue;
        const dist = this.position.distanceTo(other.position);
        if (dist < schoolRadius && dist > 0.01) {
          this.schoolCenter.add(other.position);
          this.schoolVelocity.add(other.velocity);
          neighbors++;
          
          // Separation
          if (dist < 1.5) {
            const sep = new THREE.Vector3().subVectors(this.position, other.position);
            sep.normalize().multiplyScalar((1.5 - dist) * 2);
            this.acceleration.add(sep);
          }
        }
      }
      
      if (neighbors > 0) {
        this.schoolCenter.divideScalar(neighbors);
        this.schoolVelocity.divideScalar(neighbors);
        
        // Cohesion
        const toCenter = new THREE.Vector3().subVectors(this.schoolCenter, this.position);
        if (toCenter.length() > 0.1) {
          toCenter.normalize().multiplyScalar(0.8);
          this.acceleration.add(toCenter);
        }
        
        // Alignment
        const align = this.schoolVelocity.clone().normalize().multiplyScalar(0.5);
        this.acceleration.add(align);
      }
    }
    
    // Collision avoidance with other fish
    for (const other of allFish) {
      if (other === this) continue;
      const dist = this.position.distanceTo(other.position);
      const minDist = (this.size + other.size) * 1.2;
      if (dist < minDist && dist > 0.01) {
        const push = new THREE.Vector3().subVectors(this.position, other.position);
        push.normalize().multiplyScalar((minDist - dist) * 3);
        this.acceleration.add(push);
      }
    }
    
    // Apply physics
    this.velocity.addScaledVector(this.acceleration, dt);
    const maxSpeed = this.speed * (1 + this.fear * 2);
    if (this.velocity.length() > maxSpeed) this.velocity.setLength(maxSpeed);
    this.velocity.multiplyScalar(0.98); // drag
    this.position.addScaledVector(this.velocity, dt);
    
    // Update rotation to face velocity
    if (this.velocity.length() > 0.01) {
      const targetY = Math.atan2(this.velocity.x, this.velocity.z);
      const targetX = Math.atan2(-this.velocity.y, Math.sqrt(this.velocity.x ** 2 + this.velocity.z ** 2));
      
      // Smooth rotation
      let currentY = this.bodyRotation.y;
      let diff = targetY - currentY;
      while (diff > Math.PI) diff -= Math.PI * 2;
      while (diff < -Math.PI) diff += Math.PI * 2;
      this.bodyRotation.y += diff * this.turnSpeed * dt * 10;
      
      let currentX = this.bodyRotation.x;
      diff = targetX - currentX;
      this.bodyRotation.x += diff * this.turnSpeed * dt * 10;
    }
    
    // Update mesh
    this.group.position.copy(this.position);
    this.group.rotation.y = this.bodyRotation.y;
    this.group.rotation.x = this.bodyRotation.x;
    
    // Swimming animation
    const tailSwing = Math.sin(this.swimPhase * 2) * 0.4 * (0.5 + this.velocity.length() / maxSpeed * 0.5);
    this.tailMesh.rotation.z = tailSwing;
    
    const bodyBend = Math.sin(this.swimPhase * 2) * 0.15 * (this.velocity.length() / maxSpeed);
    this.bodyMesh.rotation.z = bodyBend;
    
    const finFlap = Math.sin(this.swimPhase * 4) * 0.3;
    this.pectoralL.rotation.z = -0.3 + finFlap * 0.5;
    this.pectoralR.rotation.z = 0.3 - finFlap * 0.5;
    
    const dorsalFlap = Math.sin(this.swimPhase * 2) * 0.1;
    this.dorsalMesh.rotation.z = dorsalFlap;
  }
}

// Environment
function createEnvironment() {
  // Tank glass walls
  const glassGeo = new THREE.BoxGeometry(TANK_SIZE.x, TANK_SIZE.y, TANK_SIZE.z);
  const glassEdges = new THREE.EdgesGeometry(glassGeo);
  const glassLineMat = new THREE.LineBasicMaterial({ color: 0x88ccff, transparent: true, opacity: 0.15 });
  const glassLines = new THREE.LineSegments(glassEdges, glassLineMat);
  scene.add(glassLines);
  
  // Subtle glass panes for reflection
  const paneMat = materials.glass;
  const paneGeo = new THREE.PlaneGeometry(TANK_SIZE.x, TANK_SIZE.y);
  const frontPane = new THREE.Mesh(paneGeo, paneMat);
  frontPane.position.z = -TANK_SIZE.z / 2;
  frontPane.renderOrder = 1;
  scene.add(frontPane);
  
  const backPane = frontPane.clone();
  backPane.position.z = TANK_SIZE.z / 2;
  backPane.rotation.y = Math.PI;
  scene.add(backPane);
  
  const sidePaneGeo = new THREE.PlaneGeometry(TANK_SIZE.z, TANK_SIZE.y);
  const leftPane = new THREE.Mesh(sidePaneGeo, paneMat);
  leftPane.position.x = -TANK_SIZE.x / 2;
  leftPane.rotation.y = Math.PI / 2;
  scene.add(leftPane);
  
  const rightPane = leftPane.clone();
  rightPane.position.x = TANK_SIZE.x / 2;
  rightPane.rotation.y = -Math.PI / 2;
  scene.add(rightPane);
  
  // Sand floor
  const sandGeo = new THREE.PlaneGeometry(TANK_SIZE.x * 0.95, TANK_SIZE.z * 0.95, 40, 40);
  const sandPos = sandGeo.attributes.position;
  for (let i = 0; i < sandPos.count; i++) {
    const x = sandPos.getX(i);
    const z = sandPos.getY(i);
    sandPos.setZ(i, Math.sin(x * 0.5) * Math.cos(z * 0.5) * 0.15 + (Math.random() - 0.5) * 0.1);
  }
  sandGeo.computeVertexNormals();
  const sand = new THREE.Mesh(sandGeo, materials.sand);
  sand.rotation.x = -Math.PI / 2;
  sand.position.y = -TANK_SIZE.y / 2 + 0.3;
  sand.receiveShadow = true;
  scene.add(sand);
  
  // Rocks
  for (let i = 0; i < 12; i++) {
    const rock = new THREE.Mesh(geometries.rock, materials.rock);
    const scale = 0.5 + Math.random() * 1.5;
    rock.scale.setScalar(scale);
    rock.position.set(
      (Math.random() - 0.5) * (TANK_SIZE.x - 6),
      -TANK_SIZE.y / 2 + 0.3 + scale * 0.5,
      (Math.random() - 0.5) * (TANK_SIZE.z - 6)
    );
    rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
    rock.castShadow = true;
    rock.receiveShadow = true;
    scene.add(rock);
  }
  
  // Plants
  for (let i = 0; i < 20; i++) {
    createPlant(
      (Math.random() - 0.5) * (TANK_SIZE.x - 8),
      -TANK_SIZE.y / 2 + 0.3,
      (Math.random() - 0.5) * (TANK_SIZE.z - 8)
    );
  }
  
  // Coral
  for (let i = 0; i < 8; i++) {
    createCoral(
      (Math.random() - 0.5) * (TANK_SIZE.x - 8),
      -TANK_SIZE.y / 2 + 0.3,
      (Math.random() - 0.5) * (TANK_SIZE.z - 8)
    );
  }
}

function createPlant(x, y, z) {
  const group = new THREE.Group();
  const height = 3 + Math.random() * 4;
  const segments = Math.floor(height / 0.5);
  
  for (let i = 0; i < segments; i++) {
    const leaf = new THREE.Mesh(geometries.plantLeaf, materials.plant);
    leaf.position.y = i * 0.5;
    leaf.rotation.y = (i * 0.7 + Math.random() * 0.5);
    leaf.rotation.x = (Math.random() - 0.5) * 0.3;
    leaf.scale.setScalar(0.5 + Math.random() * 0.5);
    leaf.userData = { baseRotY: leaf.rotation.y, baseRotX: leaf.rotation.x, phase: Math.random() * Math.PI * 2, speed: 0.5 + Math.random() * 0.5 };
    group.add(leaf);
  }
  
  group.position.set(x, y, z);
  group.userData = { isPlant: true, swayPhase: Math.random() * Math.PI * 2 };
  scene.add(group);
  return group;
}

function createCoral(x, y, z) {
  const group = new THREE.Group();
  const branches = 3 + Math.floor(Math.random() * 4);
  
  for (let i = 0; i < branches; i++) {
    const branch = new THREE.Mesh(geometries.coralBranch, materials.coral);
    branch.position.set(
      (Math.random() - 0.5) * 1.5,
      0,
      (Math.random() - 0.5) * 1.5
    );
    branch.rotation.set(
      (Math.random() - 0.5) * 0.3,
      Math.random() * Math.PI * 2,
      (Math.random() - 0.5) * 0.3
    );
    branch.scale.y = 0.7 + Math.random() * 0.6;
    group.add(branch);
  }
  
  group.position.set(x, y, z);
  scene.add(group);
  return group;
}

// Bubbles
const bubbles = [];
for (let i = 0; i < 80; i++) {
  const bubble = new THREE.Mesh(geometries.bubble, materials.bubble);
  bubble.position.set(
    (Math.random() - 0.5) * TANK_SIZE.x * 0.8,
    -TANK_SIZE.y / 2 + Math.random() * TANK_SIZE.y,
    (Math.random() - 0.5) * TANK_SIZE.z * 0.8
  );
  bubble.userData = {
    speed: 0.3 + Math.random() * 0.7,
    wobble: Math.random() * Math.PI * 2,
    wobbleSpeed: 0.5 + Math.random(),
    size: 0.05 + Math.random() * 0.08
  };
  bubble.scale.setScalar(bubble.userData.size / 0.08);
  bubbles.push(bubble);
  scene.add(bubble);
}

// Floating particles
const particles = [];
const particleGeo = new THREE.BufferGeometry();
const particleCount = 500;
const positions = new Float32Array(particleCount * 3);
const alphas = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * TANK_SIZE.x;
  positions[i * 3 + 1] = (Math.random() - 0.5) * TANK_SIZE.y;
  positions[i * 3 + 2] = (Math.random() - 0.5) * TANK_SIZE.z;
  alphas[i] = Math.random() * 0.5;
}
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeo.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
const particleMat = new THREE.PointsMaterial({
  color: 0xffffff,
  size: 0.06,
  transparent: true,
  opacity: 0.4,
  sizeAttenuation: true,
  depthWrite: false,
});
const particleSystem = new THREE.Points(particleGeo, particleMat);
scene.add(particleSystem);

// Caustics simulation
const causticTexture = new THREE.DataTexture(
  new Uint8Array(128 * 128), 128, 128, THREE.RedFormat, THREE.UnsignedByteType
);
causticTexture.wrapS = causticTexture.wrapT = THREE.RepeatWrapping;

// Create fish
const fish = [];
const fishCount = 35;
for (let i = 0; i < fishCount; i++) {
  const species = FISH_SPECIES[Math.floor(Math.random() * FISH_SPECIES.length)];
  fish.push(new Fish(species, i));
}
document.getElementById('count').textContent = fish.length;

// Animation
let lastTime = performance.now();
const clock = new THREE.Clock();

function animate() {
  const now = performance.now();
  const dt = Math.min((now - lastTime) / 1000, 0.1);
  lastTime = now;
  
  const elapsed = clock.getElapsedTime();
  
  updateMouseWorld();
  
  // Update fish
  for (const f of fish) {
    f.update(dt, fish);
  }
  
  // Update bubbles
  for (const bubble of bubbles) {
    bubble.position.y += bubble.userData.speed * dt;
    bubble.position.x += Math.sin(elapsed * bubble.userData.wobbleSpeed + bubble.userData.wobble) * 0.02;
    bubble.position.z += Math.cos(elapsed * bubble.userData.wobbleSpeed + bubble.userData.wobble) * 0.02;
    
    if (bubble.position.y > TANK_SIZE.y / 2 - 1) {
      bubble.position.y = -TANK_SIZE.y / 2 + 0.5;
      bubble.position.x = (Math.random() - 0.5) * TANK_SIZE.x * 0.8;
      bubble.position.z = (Math.random() - 0.5) * TANK_SIZE.z * 0.8;
    }
  }
  
  // Update plants sway
  scene.traverse(obj => {
    if (obj.userData.isPlant) {
      obj.userData.swayPhase += dt * 0.3;
      const sway = Math.sin(obj.userData.swayPhase) * 0.15;
      obj.rotation.y = sway;
      obj.children.forEach((leaf, i) => {
        leaf.rotation.y = leaf.userData.baseRotY + Math.sin(elapsed * leaf.userData.speed + leaf.userData.phase) * 0.2;
        leaf.rotation.x = leaf.userData.baseRotX + Math.cos(elapsed * leaf.userData.speed + leaf.userData.phase) * 0.1;
      });
    }
  });
  
  // Update particles
  const particlePos = particleSystem.geometry.attributes.position;
  for (let i = 0; i < particleCount; i++) {
    particlePos.array[i * 3 + 1] -= dt * 0.15; // slow sink
    if (particlePos.array[i * 3 + 1] < -TANK_SIZE.y / 2) {
      particlePos.array[i * 3 + 1] = TANK_SIZE.y / 2;
      particlePos.array[i * 3] = (Math.random() - 0.5) * TANK_SIZE.x;
      particlePos.array[i * 3 + 2] = (Math.random() - 0.5) * TANK_SIZE.z;
    }
    // Subtle horizontal drift
    particlePos.array[i * 3] += Math.sin(elapsed + i) * 0.002;
    particlePos.array[i * 3 + 2] += Math.cos(elapsed + i) * 0.002;
  }
  particlePos.needsUpdate = true;
  
  // Camera subtle movement
  camera.position.x = Math.sin(elapsed * 0.1) * 1.5;
  camera.position.y = Math.cos(elapsed * 0.07) * 1;
  camera.lookAt(0, 0, 0);
  
  // Sun light animation
  sunLight.position.x = Math.sin(elapsed * 0.05) * 20;
  sunLight.position.z = Math.cos(elapsed * 0.05) * 20;
  
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

animate();

// Resize
window.addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>