Interactive 3D aquariumOne-line
Nemotron 3 Ultra (free)
by NVIDIAReasoningClean
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>