Interactive 3D aquariumOne-line
Nemotron 3 Ultra (free)
by NVIDIAReasoning1 console error
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.