Interactive 3D aquariumOne-line
gpt-oss-120b
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 51.9 s
- Cost
- $0.0058
- Output tokens
- 7,710
- Spent thinking
- 5,701 tokens
- Speed
- 150.2 tok/s
- Console errors
- 0
- First token after
- 535 ms
- First visible text after
- 37.3 s
- Prompt tokens
- 487
- Code size
- 7.2 KB
- Finish reason
- stop
- Served by
- Parasail
- Model id
openai/gpt-oss-120b- Reasoning sent
{"effort":"high"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 273 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js Aquarium Demo</title>
<style>
html, body {
margin: 0;
overflow: hidden;
height: 100%;
background: #001d3d;
touch-action: none;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: #fff;
font-family: sans-serif;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 4px;
pointer-events: none;
user-select: none;
}
canvas { display: block; }
</style>
</head>
<body>
<div id="info">Move the mouse near fish to make them swim away</div>
<canvas id="canvas"></canvas>
<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>
<script type="module">
import * as THREE from "three";
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x001d3d);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
2000
);
camera.position.set(0, 0, 30);
camera.lookAt(0, 0, 0);
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
});
// Lights
scene.add(new THREE.AmbientLight(0x404040, 0.8));
const point = new THREE.PointLight(0xffffff, 1);
point.position.set(0, 10, 20);
scene.add(point);
// Aquarium dimensions
const AQUARIUM_WIDTH = 20;
const AQUARIUM_HEIGHT = 12;
const AQUARIUM_DEPTH = 20;
// Transparent walls (inside view)
const aquarium = new THREE.Mesh(
new THREE.BoxGeometry(AQUARIUM_WIDTH, AQUARIUM_HEIGHT, AQUARIUM_DEPTH),
new THREE.MeshPhongMaterial({
color: 0x44aaff,
transparent: true,
opacity: 0.15,
side: THREE.BackSide,
shininess: 100
})
);
scene.add(aquarium);
// Floor (optional visual cue)
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(AQUARIUM_WIDTH, AQUARIUM_DEPTH),
new THREE.MeshPhongMaterial({
color: 0x2a5c6c,
side: THREE.DoubleSide,
shininess: 30
})
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -AQUARIUM_HEIGHT / 2;
scene.add(floor);
// Fish configuration
const FISH_COUNT = 30;
const BASE_SPEED = 0.6; // world units per second
const MAX_SPEED = 3.0;
const FLEE_RADIUS = 1.2; // world units
const FLEE_FORCE = 5.0; // acceleration multiplier
const DRAG = 0.99; // per-frame damping factor
const fishArray = [];
// Utility to create a simple fish
function createFish() {
const fish = new THREE.Group();
// Body
const bodyGeom = new THREE.SphereGeometry(0.4, 12, 12);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x66aaff });
const body = new THREE.Mesh(bodyGeom, bodyMat);
fish.add(body);
// Tail
const tailGeom = new THREE.ConeGeometry(0.25, 0.6, 8);
const tailMat = new THREE.MeshStandardMaterial({ color: 0x66aaff });
const tail = new THREE.Mesh(tailGeom, tailMat);
tail.position.set(0, 0, -0.7);
tail.rotation.x = Math.PI; // point rearward
fish.add(tail);
// Eye
const eyeGeom = new THREE.SphereGeometry(0.07, 8, 8);
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x000000 });
const eye = new THREE.Mesh(eyeGeom, eyeMat);
eye.position.set(0.18, 0.12, 0.35);
fish.add(eye);
// Random start position inside the aquarium
fish.position.set(
(Math.random() - 0.5) * AQUARIUM_WIDTH,
(Math.random() - 0.5) * AQUARIUM_HEIGHT,
(Math.random() - 0.5) * AQUARIUM_DEPTH
);
// Random initial velocity
const vel = new THREE.Vector3(
(Math.random() - 0.5),
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5)
).normalize().multiplyScalar(BASE_SPEED);
fish.userData = { velocity: vel };
fish.lookAt(fish.position.clone().add(vel));
scene.add(fish);
return fish;
}
// Populate the aquarium with fish
for (let i = 0; i < FISH_COUNT; i++) {
fishArray.push(createFish());
}
// Mouse handling (NDC coordinates)
const mouse = new THREE.Vector2(0, 0);
let mouseMoved = false;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
mouseMoved = true;
});
// Raycaster for “danger” area
const raycaster = new THREE.Raycaster();
let raySet = false; // indicates whether we have a valid ray
const clock = new THREE.Clock();
animate();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// Update ray if mouse moved
if (mouseMoved) {
raycaster.setFromCamera(mouse, camera);
raySet = true;
mouseMoved = false;
}
// Update each fish
for (const fish of fishArray) {
const vel = fish.userData.velocity;
const pos = fish.position;
// Flee from mouse ray
if (raySet) {
const origin = raycaster.ray.origin;
const dir = raycaster.ray.direction;
// Compute closest point on ray to fish
const toFish = new THREE.Vector3().subVectors(pos, origin);
const t = toFish.dot(dir); // scalar projection
const closest = new THREE.Vector3()
.copy(origin)
.addScaledVector(dir, t);
const dist = pos.distanceTo(closest);
if (dist < FLEE_RADIUS) {
const away = new THREE.Vector3()
.subVectors(pos, closest)
.normalize();
vel.addScaledVector(away, FLEE_FORCE * delta);
}
}
// Apply drag to smoothly reduce speed
vel.multiplyScalar(DRAG);
// Clamp speed
if (vel.length() > MAX_SPEED) vel.setLength(MAX_SPEED);
if (vel.length() < BASE_SPEED) vel.setLength(BASE_SPEED);
// Move fish
pos.addScaledVector(vel, delta);
// Keep fish inside aquarium bounds (simple bounce)
const halfX = AQUARIUM_WIDTH / 2;
const halfY = AQUARIUM_HEIGHT / 2;
const halfZ = AQUARIUM_DEPTH / 2;
if (pos.x < -halfX) {
pos.x = -halfX;
vel.x = -vel.x;
} else if (pos.x > halfX) {
pos.x = halfX;
vel.x = -vel.x;
}
if (pos.y < -halfY) {
pos.y = -halfY;
vel.y = -vel.y;
} else if (pos.y > halfY) {
pos.y = halfY;
vel.y = -vel.y;
}
if (pos.z < -halfZ) {
pos.z = -halfZ;
vel.z = -vel.z;
} else if (pos.z > halfZ) {
pos.z = halfZ;
vel.z = -vel.z;
}
// Reorient fish to face its direction of travel
if (vel.lengthSq() > 1e-5) {
fish.lookAt(pos.clone().add(vel));
}
}
renderer.render(scene, camera);
}
</script>
</body>
</html>Reasoning trace (5,701 tokens)
We need to output a single HTML file with the complete code that meets the constraints. The user wants a 3D aquarium using Three.js where the fish swim away when the mouse moves near them.
We must load Three.js via importmap script that points to the correct module path. The import map: {"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/"}.
We'll write a small HTML page that includes an import map script and then a module script using the import map and loads three from 'three' and also any necessary extras like OrbitControls (maybe use "three/addons/controls/OrbitControls.js") and maybe GLTFLoader for fish models. We might just use simple fish geometries (for instance, we could use a sphere for the fish body and a cone for tail). But we could also use a simple fish shape with a set of geometry.
Simplify: We'll create a simple aquarium: a box with transparent sides representing water (a box geometry with transparent material). We'll add a floor (maybe a plane). We'll place a set of fish: each fish will be an object composed of a sphere (body) and a triangle or cone for tail and a small sphere for eye. We'll animate them swimming forward along a random direction, bouncing off the walls. The fish will have a flee behavior: when the mouse pointer gets near a fish (within a radius), the fish will accelerate away (like a vector from mouse to fish). We'll need to map mouse position to world coordinates: we can use a raycaster to compute a point on the aquarium plane (or in 3D). We could project the mouse onto a plane at a given depth (like near the fish's z coordinate). For simplicity, we could compute fish distances in screen space: using their projected positions onto screen and mouse coordinates; but better to compute world 3D distance: we can cast a ray into the scene and intersect with a plane (perhaps the aquarium's floor) and use the intersection point as the target for fish to flee. However fish can be in any x,y,z; but we could consider fish near the ray's point.
Simplify: For each animate frame, iterate over fish and compute distance from mouse ray to fish's position. Use THREE.Raycaster to get a point near fish? Actually we can find the distance from the ray to the fish's position: the distance from a point to a line.
Procedurally:
- Setup scene, camera, renderer.
- Maze: We'll add a box representing the aquarium: transparent walls using BoxGeometry and MeshBasicMaterial with opacity < 1.
- Add lights.
- Add fish: create maybe 20 fish.
- Each fish will have a userData: velocity (THREE.Vector3), speed, flee distance.
- Create fish geometry: combine sphere for body and cone for tail via Object3D.
- In animate loop, update fish positions based on velocity dt.
- Bounce off walls by reflecting if the fish goes beyond the aquarium bounds.
- For mouse tracking: We need to capture mouse coordinates relative to the canvas.
- On mousemove, we store mouse NDC coordinates (x,y) normalized between -1 and 1.
- On each frame, create a raycaster from camera using mouse NDC.
- For each fish, compute distance from fish.position to ray (distance from point to line). Use method: compute point from ray.origin + t * ray.direction (closest point), get distance. Compute t by projecting (fishPos - rayOrigin) onto rayDirection: t = (fishPos - origin).dot(direction). Then pointOnRay = origin + direction * t. distance = fishPos.distanceTo(pointOnRay). If distance < some threshold (like 2), then fish will accelerate away: fish.velocity.add(awayVec.clone().normalize().multiplyScalar(fleeForce)). Also limit speed.
- The fish will gradually return to normal speed.
- Render.
- Also add simple UI: "Move mouse near fish to make them swim away!"
- Provide HTML.
- Use importmap and a script type="module". All code in <script type="module"></script>.
Make sure we keep all inline.
Make sure page is visible on first paint: show aquarium with fish.
Phones: Should adapt to viewport. We'll use full-screen canvas (width: 100vw, height: 100vh). Use CSS to set body margin:0.
We must not use any external network request except for three module via importmap.
Potentially needed additional imports: maybe we need "OrbitControls" for camera control (but it's optional). For better view we can add a simple static camera that moves only.
Better to set camera globally at (0,0,15) looking at origin. That's fine.
Now implement fish.
Implementation details:
- We'll set aquarium dimensions: width=20, height=10, depth=20.
- Fish will be positioned randomly within aquarium.
- fishObject: a THREE.Group containing Body (sphere) and Tail (cone) and maybe Eye (small sphere). Add to scene.
- fish velocity: random direction normalized times speed.
- Avoid heavy memory per fish. Use an array to store fish meshes and some data.
Define fishCount=30.
Define fishMaterial: maybe blueish material.
Set body geometry: new THREE.SphereGeometry(0.5, 8, 8). Tail geometry: new THREE.ConeGeometry(0.3, 0.6, 8). The cone is positioned behind the sphere (negative z) and rotated.
Set fishGroup: add body at (0,0,0); tail at offset.
The fish orientation: we can align fish's forward direction to velocity direction, using quaternion. We'll set the fish to look in direction of velocity using lookAt.
Pseudo:
function createFish() {
const group = new THREE.Group();
const bodyGeom = new THREE.SphereGeometry(0.5, 12, 12);
const bodyMat = new THREE.MeshStandardMaterial({color:0x66aaff});
const body = new THREE.Mesh(bodyGeom, bodyMat);
group.add(body);
const tailGeom = new THREE.ConeGeometry(0.2, 0.5, 8);
const tailMat = new THREE.MeshStandardMaterial({color:0x66aaff});
const tail = new THREE.Mesh(tailGeom, tailMat);
tail.position.set(0, 0, -0.75);
tail.rotation.x = Math.PI; // point tail backward?
group.add(tail);
// optional eye
const eyeGeom = new THREE.SphereGeometry(0.1, 8, 8);
const eyeMat = new THREE.MeshStandardMaterial({color:0x000000});
const eye = new THREE.Mesh(eyeGeom, eyeMat);
eye.position.set(0.2, 0.1, 0.4);
group.add(eye);
// initial position
group.position.set(
(Math.random() - 0.5) * AQUARIUM_WIDTH,
(Math.random() - 0.5) * AQUARIUM_HEIGHT,
(Math.random() - 0.5) * AQUARIUM_DEPTH
);
// initial velocity
const velocity = new THREE.Vector3(
(Math.random() - 0.5),
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5)
).normalize().multiplyScalar(FISH_SPEED);
group.userData = {velocity, speed:FISH_SPEED};
// orientation: group.lookAt(group.position.clone().add(velocity));
return group;
}
Add to scene.
Define aquarium boundaries: minX = -AQUARIUM_WIDTH/2, maxX = AQUARIUM_WIDTH/2, etc.
In tick:
for each fish:
- const fish = fishArray[i];
- let vel = fish.userData.velocity;
- // Flee
// compute distance to mouse ray
const fishPos = fish.position;
// compute distance; overriding
if (mouseRay) compute distance as above.
// if distance < FLEE_DISTANCE then apply flee
// Compute vector from ray point to fish:
const pointOnRay = new THREE.Vector3();
const t = fishPos.clone().sub(rayOrigin).dot(rayDirection);
pointOnRay.copy(rayOrigin).addScaledVector(rayDirection, t);
const dist = fishPos.distanceTo(pointOnRay);
if (dist < FLEE_RADIUS) { // accelerate away
const away = fishPos.clone().sub(pointOnRay).normalize();
vel.addScaledVector(away, FLEE_FORCE);
}
// limit speed
if (vel.length() > MAX_SPEED) vel.setLength(MAX_SPEED);
// gently return to normal speed: maybe apply drag
vel.multiplyScalar(DRAG);
// update position
fish.position.addScaledVector(vel, delta);
// keep fish within bounds
// for each axis: reflect if out of bound.
// Also adjust orientation:
fish.lookAt(fish.position.clone().add(vel));
Add water effect: maybe some light and floor.
Add ambient light.
Add a point light.
Add a plane for bottom.
Add a transparent box: using BoxGeometry but we might need to make six separate walls as separate meshes to have inside-out visible faces. Use BoxGeometry with side: THREE.BackSide and material with opacity 0.1 and transparent.
Implementation:
const aquarium = new THREE.Mesh(
new THREE.BoxGeometry(AQUARIUM_WIDTH, AQUARIUM_HEIGHT, AQUARIUM_DEPTH),
new THREE.MeshPhongMaterial({
color:0x88ccee,
transparent:true,
opacity:0.2,
side:THREE.BackSide,
shininess:100
})
);
scene.add(aquarium);
Also a plane for floor: maybe not needed.
Set background: maybe a gradient - using CSS.
Add controls: maybe user can rotate camera using mouse drag (OrbitControls). But we want the user to move mouse near fish; need to have pointer on canvas. If we add OrbitControls, moving mouse will also rotate camera, interfering. However we can attach mousemove events to the canvas.
Moreover, we could keep camera static (centered). Then moving mouse coordinates can be used for ray. Simpler: no controls.
Thus fish will be observed from a fixed angle.
Set camera: new THREE.PerspectiveCamera(60, aspect, 0.1, 1000); camera.position.set(0, 0, 30).
Add small directional light or point light with spec.
Define constants:
const AQUARIUM_WIDTH = 20;
const AQUARIUM_HEIGHT = 10;
const AQUARIUM_DEPTH = 20;
const FISH_SPEED = 0.02; // per frame? Actually speed per second; we can adjust.
Will define speed in units per second.
For animation: compute delta = clock.getDelta(), update positions with vel multiplied by delta * speed factor.
Simplify: set velocity magnitude constant ~0.5 units per second. Use speed scaling: velocity.setLength(baseSpeed) each frame but allow temporary increase due to flee.
Implementation:
FISH_BASE_SPEED = 0.5; // units per second
FISH_MAX_SPEED = 2.0; // speed limit
FLEE_RADIUS = 1.5; // distance in world units from mouse ray
FLEE_FORCE = 5.0; // acceleration? We'll add scaled.
Maybe use vel.addScaledVector(away, FLEE_FORCE * delta). Then clamp speed.
Also some drag: vel.multiplyScalar(0.99) to slowly decelerate.
Now capturing mouse:
Add event listeners: canvas.addEventListener('mousemove', (e)=>{mouse.x = (e.clientX / canvas.clientWidth)*2-1; mouse.y = -(e.clientY / canvas.clientHeight)*2+1;});
Initialize mouse = new THREE.Vector2(0,0). And flag to indicate whether mouse is within canvas; but we can always compute.
On each frame, we set a raycaster with mouse coordinates:
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
Then get rayOrigin = raycaster.ray.origin.clone(); rayDirection = raycaster.ray.direction.clone();
Then pass to fish loop.
Alternatively compute distance without raycaster each fish.
Potential performance: fishCount ~30 - fine.
Now ensure the page is visible on first paint. We'll set some tags: <div id="info">Move mouse near fish to scare them!</div> position absolute top left.
Also set canvas full-screen.
Now we need to ensure the import map is placed before the module script.
Spec: In HTML, we add:
<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>
Then a <script type="module"> for our code.
Now we need to include the proper modules: import * as THREE from "three"; and optionally import {OrbitControls} from "three/addons/controls/OrbitControls.js"; but not needed. We'll create no external network request besides Three.
Now we think about all steps.
Potential pitfalls: The aquarium material side must be BackSide to see interior.
Since we have transparent walls, we might need to disable depthWrite to avoid weird artifacts? Usually no.
Also adding a floor with material to make it more "aquarium".
Now we write the final HTML.
Structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js Aquarium Demo</title>
<style>
html,body {margin:0; overflow:hidden; height:100%; background:#001d3d;}
#info {position:absolute; top:10px; left:10px; color:#fff; font-family:sans-serif; background:rgba(0,0,0,0.5); padding:5px 10px; border-radius:4px; pointer-events:none;}
canvas {display:block;}
</style>
</head>
<body>
<div id="info">Move the mouse near fish to make them swim away</div>
<canvas id="canvas"></canvas>
<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>
<script type="module">
import * as THREE from "three";
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x001d3d);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 30);
camera.lookAt(0,0,0);
window.addEventListener('resize', onWindowResize);
function onWindowResize(){
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
// Lights
const ambient = new THREE.AmbientLight(0x404040, 0.8);
scene.add(ambient);
const point = new THREE.PointLight(0xffffff, 1, 0, 2);
point.position.set(0, 10, 20);
scene.add(point);
// Aquarium dimensions
const AQUARIUM_WIDTH = 20;
const AQUARIUM_HEIGHT = 12;
const AQUARIUM_DEPTH = 20;
// Aquarium container (transparent walls)
const aquarium = new THREE.Mesh(
new THREE.BoxGeometry(AQUARIUM_WIDTH, AQUARIUM_HEIGHT, AQUARIUM_DEPTH),
new THREE.MeshPhongMaterial({
color: 0x44aaff,
transparent: true,
opacity: 0.15,
side: THREE.BackSide,
shininess: 100
})
);
scene.add(aquarium);
// Floor (optional)
const floormesh = new THREE.Mesh(
new THREE.PlaneGeometry(AQUARIUM_WIDTH, AQUARIUM_DEPTH),
new THREE.MeshPhongMaterial({color:0x2a5c6c, side:THREE.DoubleSide, shininess:30})
);
floormesh.rotation.x = -Math.PI/2;
floormesh.position.y = -AQUARIUM_HEIGHT/2;
scene.add(floormesh);
// Fish parameters
const FISH_COUNT = 30;
const FISH_BASE_SPEED = 0.6; // units per second
const FISH_MAX_SPEED = 3.0; // after flee
const FLEE_RADIUS = 1.2; // distance from ray to fish
const FLEE_FORCE = 5.0; // acceleration factor
const DRAG = 0.98; // per second
const fishArray = [];
// Helper to create fish
function createFish(){
const fish = new THREE.Group();
const bodyGeom = new THREE.SphereGeometry(0.4, 12, 12);
const bodyMat = new THREE.MeshStandardMaterial({color: 0x66aaff});
const body = new THREE.Mesh(bodyGeom, bodyMat);
fish.add(body);
const tailGeom = new THREE.ConeGeometry(0.25, 0.6, 8);
const tailMat = new THREE.MeshStandardMaterial({color: 0x66aaff});
const tail = new THREE.Mesh(tailGeom, tailMat);
tail.position.set(0,0,-0.7);
tail.rotation.x = Math.PI; // point backward
fish.add(tail);
const eyeGeom = new THREE.SphereGeometry(0.07, 8, 8);
const eyeMat = new THREE.MeshStandardMaterial({color: 0x000000});
const eye = new THREE.Mesh(eyeGeom, eyeMat);
eye.position.set(0.18,0.12,0.35);
fish.add(eye);
// Random starting position inside aquarium
fish.position.set(
(Math.random() - 0.5) * AQUARIUM_WIDTH,
(Math.random() - 0.5) * AQUARIUM_HEIGHT,
(Math.random() - 0.5) * AQUARIUM_DEPTH
);
// Random initial velocity
const vel = new THREE.Vector3(
(Math.random() - 0.5),
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5)
).normalize().multiplyScalar(FISH_BASE_SPEED);
fish.userData = {
velocity: vel,
baseSpeed: FISH_BASE_SPEED
};
// orient fish to velocity
fish.lookAt(fish.position.clone().add(vel));
scene.add(fish);
return fish;
}
// Create fish
for(let i=0;i<FISH_COUNT;i++){
fishArray.push(createFish());
}
// Mouse handling
const mouse = new THREE.Vector2(0,0);
let mouseMoved = false;
canvas.addEventListener('mousemove', e=>{
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
mouseMoved = true;
});
// Raycaster created once
const raycaster = new THREE.Raycaster();
const clock = new THREE.Clock();
animate();
function animate(){
requestAnimationFrame(animate);
const delta = clock.getDelta();
// Update ray (only if mouse moved)
if(mouseMoved){
raycaster.setFromCamera(mouse, camera);
mouseMoved = false;
}
// For each fish
for(let i=0;i<fishArray.length;i++){
const fish = fishArray[i];
const vel = fish.userData.velocity;
// Flee behavior
if(raycaster.ray){
const fishPos = fish.position;
const origin = raycaster.ray.origin;
const dir = raycaster.ray.direction;
const toFish = new THREE.Vector3().subVectors(fishPos, origin);
const t = toFish.dot(dir); // projection length
const closest = new THREE.Vector3().copy(origin).addScaledVector(dir, t);
const dist = fishPos.distanceTo(closest);
if(dist < FLEE_RADIUS){
const away = new THREE.Vector3().subVectors(fishPos, closest).normalize();
vel.addScaledVector(away, FLEE_FORCE * delta);
}
}
// Apply drag
vel.multiplyScalar(Math.pow(DRAG, delta * 60)); // approximate per-frame drag
// Clamp speed
const maxSpeed = Math.max(FISH_MAX_SPEED, vel.length());
if(vel.length() > maxSpeed){
vel.setLength(maxSpeed);
}
// Move fish
fish.position.addScaledVector(vel, delta);
// Bounce off aquarium walls
const halfX = AQUARIUM_WIDTH/2;
const halfY = AQUARIUM_HEIGHT/2;
const halfZ = AQUARIUM_DEPTH/2;
if(fish.position.x < -halfX || fish.position.x > halfX){
vel.x = -vel.x;
fish.position.x = THREE.MathUtils.clamp(fish.position.x, -halfX, halfX);
}
if(fish.position.y < -halfY || fish.position.y > halfY){
vel.y = -vel.y;
fish.position.y = THREE.MathUtils.clamp(fish.position.y, -halfY, halfY);
}
if(fish.position.z < -halfZ || fish.position.z > halfZ){
vel.z = -vel.z;
fish.position.z = THREE.MathUtils.clamp(fish.position.z, -halfZ, halfZ);
}
// Reorient fish to face moving direction
if(vel.lengthSq() > 1e-6){
fish.lookAt(fish.position.clone().add(vel));
}
}
// Slight rotation for demonstration (optional)
// aquarium.rotation.y += 0.001;
renderer.render(scene, camera);
}
</script>
</body>
</html>
Now check any errors.
Potential issues:
- We used AmbientLight and PointLight; these need to be imported? Three includes them in core.
- The imports only needed for THREE. No other modules used.
- raycaster.ray property is always present. It's fine.
- In animate, we call raycaster.setFromCamera only if mouseMoved. If no movement, the ray remains from previous frame; but after initial load, mouseMoved is false, thus ray wasn't set. That's okay. But we need initial ray value for fish to ignore.
- The code modifies vel.multiplyScalar(Math.pow(DRAG, delta * 60)). Actually we can just do vel.multiplyScalar(DRAG). The drag per frame is okay.
But many possibilities.
Simplify: apply drag as vel.multiplyScalar(DRAG) each frame (makes speed slowly decay). But this is per second factor; if delta is variable, using vel.multiplyScalar(Math.pow(DRAG, delta)) would be more correct. But we can just use vel.multiplyScalar(1 - delta * 0.1) or similar. Simpler: vel.multiplyScalar(0.99). That works but not dependent on delta. Use vel.multiplyScalar(0.98). Enough.
We'll adjust:
vel.multiplyScalar(0.99);
Thus speed decelerates slowly.
Will keep dRI.
Now careful about DRAG: currently defined but not used? It's used with exponent maybe.
Simplify: just after flee we apply drag: vel.multiplyScalar(DRAG). Make DRAG = 0.99
Thus speed decays over frames.
Define:
const DRAG = 0.99;
Now we need to compute maxSpeed: The max speed after flee should be limited.
We used:
const maxSpeed = Math.max(FISH_MAX_SPEED, vel.length());
if(vel.length() > maxSpeed) vel.setLength(maxSpeed);
Actually that is wrong: we want to clamp to FISH_MAX_SPEED, but if base speed is lower, and we want to let them accelerate a bit. So we should clamp to FISH_MAX_SPEED.
Simplify:
if(vel.length() > FISH_MAX_SPEED) vel.setLength(FISH_MAX_SPEED);
else if(vel.length() < fish.userData.baseSpeed) vel.setLength(fish.userData.baseSpeed);
But we could just let drag bring it down.
Simplify: After flee, limit speed:
if(vel.length() > FISH_MAX_SPEED) vel.setLength(FISH_MAX_SPEED);
Now if a fish's velocity becomes lower than base speed due to drag, we may want to gradually return to base speed. But it's okay.
Now we need to ensure we compute container boundaries correctly. In aquarium geometry, the box's interior coordinates extend from -width/2 to +width/2 in each axis. So halfX etc.
Now the fish group orientation: Using fish.lookAt(fish.position.clone().add(vel)) each frame is fine.
Potential performance: compute speed each frame using vel.lengthSq() etc.
Now check for variable collisions: in bounce section after flipping velocity, we clamp fish position inside bounds.
Now about calibrations: Flee distance relative to ray. Should we compute distance from fish to ray? That's done.
But we may want to ignore fish behind camera. But not needed.
Now see if any references missing.
We used THREE.MathUtils.clamp; we included MathUtils.
Now we need to define `raycaster.ray` but it's always set a previous setFromCamera. If never set, it will be the default from origin (0,0,0) direction (0,0,-1). That's okay.
Potential errors:
- call to 'raycaster.ray' is a property; we use it fine.
- The variable "mouseMoved" resets after setFromCamera.
- At start, if no mouse movement, ray may not be set or stays default. Flee will not trigger until mouse moves.
- The fish create function uses .lookAt which expects a Vector3; we used fish.position.clone().add(vel), okay.
Potential adjustments:
- The canvas should fill the page.
- Save reference to canvas.
- Ensure renderer.setSize correctly on resize.
I think the code is ready.
Now we need to only output the fenced code block with the entire HTML. Use ````html.
Make sure no text before or after.
Now final answer.