Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <title>Mermaids of Lake Minnetonka 🧜♀️🌊</title> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| font-family: 'Georgia', serif; | |
| } | |
| #blocker { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0,0,0,0.7); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 100; | |
| } | |
| #start-button { | |
| padding: 20px 40px; | |
| font-size: 24px; | |
| background: #1a3a5a; | |
| color: #cceeff; | |
| border: 2px solid #cceeff; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| text-shadow: 0 0 10px #cceeff; | |
| box-shadow: 0 0 20px rgba(135, 206, 250, 0.5); | |
| } | |
| #top-bar { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 10px; | |
| background: linear-gradient(to bottom, rgba(0, 15, 30, 0.8), rgba(0, 15, 30, 0)); | |
| box-sizing: border-box; | |
| color: #e0f7ff; | |
| text-shadow: 0 0 5px #66aaff; | |
| z-index: 10; | |
| } | |
| #lyric-ticker-container { | |
| width: 100%; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| } | |
| #lyric-ticker-text { | |
| display: inline-block; | |
| padding-left: 100%; | |
| animation: scrollText linear infinite; | |
| } | |
| @keyframes scrollText { | |
| from { transform: translateX(0%); } | |
| to { transform: translateX(-100%); } | |
| } | |
| #controls-ui { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 5px 20px; | |
| font-size: 14px; | |
| } | |
| #speed-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 150px; | |
| height: 5px; | |
| background: rgba(135, 206, 250, 0.3); | |
| border-radius: 5px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 15px; | |
| height: 15px; | |
| background: #87cefa; | |
| cursor: pointer; | |
| border-radius: 50%; | |
| border: 2px solid #e0f7ff; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="blocker"> | |
| <button id="start-button">Start Experience</button> | |
| </div> | |
| <div id="top-bar"> | |
| <div id="lyric-ticker-container"> | |
| <span id="lyric-ticker-text"></span> | |
| </div> | |
| <div id="controls-ui"> | |
| <span>W/S: Fwd/Back | A/D: Turn | Space: Ascend | Shift: Descend</span> | |
| <div id="speed-control"> | |
| <label for="speed-slider">Scroll Speed:</label> | |
| <input type="range" id="speed-slider" min="1" max="100" value="50"> | |
| </div> | |
| </div> | |
| </div> | |
| <audio id="song" loop> | |
| <source src="YOUR_SONG_FILE.mp3" type="audio/mpeg"> | |
| Your browser does not support the audio element. | |
| </audio> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { Water } from 'three/addons/objects/Water.js'; | |
| import { Sky } from 'three/addons/objects/Sky.js'; | |
| import { SimplexNoise } from 'three/addons/math/SimplexNoise.js'; | |
| let scene, camera, renderer, mermaid, water, sky, terrain; | |
| let controls = {}; | |
| const clock = new THREE.Clock(); | |
| const worldSize = 4000; | |
| const waterLevel = 100; | |
| let schools = []; | |
| let animationId; | |
| const mermaidVelocity = new THREE.Vector3(); | |
| const mermaidState = { | |
| turnSpeed: 0, | |
| forwardSpeed: 0, | |
| tailSegments: [], | |
| hairStrands: [], | |
| leftEye: null, | |
| rightEye: null | |
| }; | |
| const raycaster = new THREE.Raycaster(); | |
| const downVector = new THREE.Vector3(0, -1, 0); | |
| const rhymingWords = [ | |
| 'gold', 'told', 'blue', 'through', 'see', 'be', 'stone', 'throne', | |
| 'haze', 'ways', 'blue', 'new', 'remains', 'wanes', | |
| 'art', 'apart', 'hymn', 'whim', 'before', 'shore', 'eulogy', 'monarchy', | |
| 'historian', 'pre-diluvian', 'time', 'crime', 'rise', 'skies', 'feel', 'real', | |
| 'haze', 'ways', 'blue', 'new', 'remains', 'wanes', | |
| 'know', 'flow', 'keep', 'deep', 'drowned', 'Mound' | |
| ].join(' • '); | |
| // --- L-SYSTEM ENGINE --- | |
| function generateLSystem(axiom, rules, iterations) { | |
| let currentString = axiom; | |
| for (let i = 0; i < iterations; i++) { | |
| let nextString = ''; | |
| for (const char of currentString) { | |
| nextString += rules[char] || char; | |
| } | |
| currentString = nextString; | |
| } | |
| return currentString; | |
| } | |
| function createLSystemGeometry(lsystemString, angle, length) { | |
| const points = []; | |
| const turtle = { | |
| pos: new THREE.Vector3(0, 0, 0), | |
| dir: new THREE.Vector3(0, 1, 0) | |
| }; | |
| const stack = []; | |
| points.push(turtle.pos.clone()); | |
| for (const char of lsystemString) { | |
| switch (char) { | |
| case 'F': | |
| turtle.pos.addScaledVector(turtle.dir, length); | |
| points.push(turtle.pos.clone()); | |
| break; | |
| case '+': | |
| turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), angle); | |
| break; | |
| case '-': | |
| turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -angle); | |
| break; | |
| case '&': | |
| turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle); | |
| break; | |
| case '^': | |
| turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle); | |
| break; | |
| case '[': | |
| stack.push({ pos: turtle.pos.clone(), dir: turtle.dir.clone() }); | |
| break; | |
| case ']': | |
| const popped = stack.pop(); | |
| turtle.pos = popped.pos; | |
| turtle.dir = popped.dir; | |
| points.push(turtle.pos.clone()); // Create a gap in the tube | |
| points.push(turtle.pos.clone()); | |
| break; | |
| } | |
| } | |
| const curve = new THREE.CatmullRomCurve3(points); | |
| return new THREE.TubeGeometry(curve, Math.round(points.length * 1.5), 0.2, 5, false); | |
| } | |
| // --- BOIDS FLOCKING ENGINE --- | |
| class Boid { | |
| constructor(mesh) { | |
| this.mesh = mesh; | |
| this.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); | |
| this.maxSpeed = 10; | |
| this.maxForce = 0.3; | |
| } | |
| update(boids, school) { | |
| const separation = this.separate(boids); | |
| const alignment = this.align(boids); | |
| const cohesion = this.cohere(boids); | |
| const avoidance = this.avoid(mermaid.position); | |
| const wander = this.wander(school.target); | |
| separation.multiplyScalar(2.0); | |
| alignment.multiplyScalar(1.0); | |
| cohesion.multiplyScalar(1.0); | |
| avoidance.multiplyScalar(3.0); | |
| wander.multiplyScalar(0.5); | |
| this.velocity.add(separation).add(alignment).add(cohesion).add(avoidance).add(wander); | |
| this.velocity.clampLength(0, this.maxSpeed); | |
| this.mesh.position.addScaledVector(this.velocity, clock.getDelta()); | |
| this.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), this.velocity.clone().normalize()); | |
| } | |
| wander(target) { | |
| const desired = target.clone().sub(this.mesh.position); | |
| desired.setLength(this.maxSpeed); | |
| const steer = desired.sub(this.velocity); | |
| steer.clampLength(0, this.maxForce); | |
| return steer; | |
| } | |
| avoid(targetPos) { | |
| const steer = new THREE.Vector3(); | |
| const distance = this.mesh.position.distanceTo(targetPos); | |
| if (distance < 50) { | |
| const desired = this.mesh.position.clone().sub(targetPos); | |
| desired.setLength(this.maxSpeed); | |
| steer.subVectors(desired, this.velocity).clampLength(0, this.maxForce * 2); | |
| } | |
| return steer; | |
| } | |
| separate(boids) { | |
| const desiredSeparation = 10.0; | |
| const steer = new THREE.Vector3(); | |
| let count = 0; | |
| for (const other of boids) { | |
| const d = this.mesh.position.distanceTo(other.mesh.position); | |
| if ((d > 0) && (d < desiredSeparation)) { | |
| const diff = new THREE.Vector3().subVectors(this.mesh.position, other.mesh.position); | |
| diff.normalize(); | |
| diff.divideScalar(d); | |
| steer.add(diff); | |
| count++; | |
| } | |
| } | |
| if (count > 0) steer.divideScalar(count); | |
| if (steer.length() > 0) { | |
| steer.setLength(this.maxSpeed); | |
| steer.sub(this.velocity); | |
| steer.clampLength(0, this.maxForce); | |
| } | |
| return steer; | |
| } | |
| align(boids) { | |
| const neighborDist = 50; | |
| const sum = new THREE.Vector3(); | |
| let count = 0; | |
| for (const other of boids) { | |
| const d = this.mesh.position.distanceTo(other.mesh.position); | |
| if ((d > 0) && (d < neighborDist)) { | |
| sum.add(other.velocity); | |
| count++; | |
| } | |
| } | |
| if (count > 0) { | |
| sum.divideScalar(count); | |
| sum.setLength(this.maxSpeed); | |
| const steer = sum.sub(this.velocity); | |
| steer.clampLength(0, this.maxForce); | |
| return steer; | |
| } | |
| return new THREE.Vector3(); | |
| } | |
| cohere(boids) { | |
| const neighborDist = 50; | |
| const sum = new THREE.Vector3(); | |
| let count = 0; | |
| for (const other of boids) { | |
| const d = this.mesh.position.distanceTo(other.mesh.position); | |
| if ((d > 0) && (d < neighborDist)) { | |
| sum.add(other.mesh.position); | |
| count++; | |
| } | |
| } | |
| if (count > 0) { | |
| sum.divideScalar(count); | |
| const desired = sum.sub(this.mesh.position); | |
| desired.setLength(this.maxSpeed); | |
| const steer = desired.sub(this.velocity); | |
| steer.clampLength(0, this.maxForce); | |
| return steer; | |
| } | |
| return new THREE.Vector3(); | |
| } | |
| } | |
| class School { | |
| constructor(scene, count, modelFn, scale) { | |
| this.boids = []; | |
| this.target = new THREE.Vector3((Math.random() - 0.5) * worldSize, waterLevel - 50, (Math.random() - 0.5) * worldSize); | |
| const model = modelFn(); | |
| model.scale.set(scale, scale, scale); | |
| for(let i=0; i<count; i++){ | |
| const boidMesh = model.clone(); | |
| boidMesh.position.set( | |
| (Math.random() - 0.5) * 200, | |
| waterLevel - 50 + (Math.random() - 0.5) * 50, | |
| (Math.random() - 0.5) * 200 | |
| ); | |
| scene.add(boidMesh); | |
| this.boids.push(new Boid(boidMesh)); | |
| } | |
| } | |
| update() { | |
| if(this.boids[0].mesh.position.distanceTo(this.target) < 200){ | |
| this.target.set((Math.random() - 0.5) * worldSize, waterLevel - 50 - Math.random() * 50, (Math.random() - 0.5) * worldSize); | |
| } | |
| for (const boid of this.boids) { | |
| boid.update(this.boids, this); | |
| } | |
| } | |
| } | |
| function init() { | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x0a1429, 0.0035); | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, worldSize * 1.5); | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| document.body.appendChild(renderer.domElement); | |
| scene.add(new THREE.AmbientLight(0x6688aa, 2)); | |
| const sun = new THREE.DirectionalLight(0xffffff, 2.5); | |
| sun.position.set(100, 200, 100); | |
| scene.add(sun); | |
| const godrayLight = new THREE.DirectionalLight(0x8eadd4, 1.5); | |
| godrayLight.position.set(200, 300, 200); | |
| scene.add(godrayLight); | |
| // ... water and sky setup (no changes) | |
| const waterGeometry = new THREE.PlaneGeometry(worldSize * 2, worldSize * 2); | |
| water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load('https://cdn.jsdelivr.net/npm/[email protected]/examples/textures/waternormals.jpg', (t) => { t.wrapS = t.wrapT = THREE.RepeatWrapping; }), sunDirection: sun.position.clone().normalize(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog !== undefined }); | |
| water.rotation.x = -Math.PI / 2; | |
| water.position.y = waterLevel; | |
| scene.add(water); | |
| sky = new Sky(); | |
| sky.scale.setScalar(worldSize); | |
| scene.add(sky); | |
| const skyUniforms = sky.material.uniforms; | |
| skyUniforms['turbidity'].value = 10; skyUniforms['rayleigh'].value = 2; skyUniforms['mieCoefficient'].value = 0.005; skyUniforms['mieDirectionalG'].value = 0.8; | |
| const pmremGenerator = new THREE.PMREMGenerator(renderer); | |
| const phi = THREE.MathUtils.degToRad(88); const theta = THREE.MathUtils.degToRad(170); | |
| sun.position.setFromSphericalCoords(1, phi, theta); | |
| sky.material.uniforms['sunPosition'].value.copy(sun.position); | |
| scene.environment = pmremGenerator.fromScene(sky).texture; | |
| createTerrain(); | |
| createMermaid(); | |
| camera.position.set(mermaid.position.x, mermaid.position.y, mermaid.position.z + 15); | |
| createFlora(200); | |
| createMermaidCity(-worldSize/2 + 500, -worldSize/2 + 500); | |
| createSurfaceWildlife(50); | |
| createLilyPads(100); | |
| // Create fish schools | |
| schools.push(new School(scene, 50, createPikeModel, 1.0)); | |
| schools.push(new School(scene, 80, createSunfishModel, 1.5)); | |
| schools.push(new School(scene, 60, createBullheadModel, 1.2)); | |
| setupUI(); | |
| window.addEventListener('resize', onWindowResize, false); | |
| document.addEventListener('keydown', (e) => controls[e.code] = true); | |
| document.addEventListener('keyup', (e) => controls[e.code] = false); | |
| renderer.render(scene, camera); | |
| } | |
| function setupUI() { /* ... no changes ... */ } | |
| function createTerrain() { /* ... no changes ... */ } | |
| function createMermaid() { /* ... no changes ... */ } | |
| function onWindowResize() { /* ... no changes ... */ } | |
| // Copying unchanged functions for brevity | |
| setupUI = () => {const s=document.getElementById('start-button'),b=document.getElementById('blocker'),l=document.getElementById('lyric-ticker-text'),c=document.getElementById('speed-slider');l.textContent=rhymingWords;function u(){const n=20,t=200,i=t-((c.value-1)/99)*(t-n);l.style.animationDuration=`${i}s`}c.addEventListener('input',u);u();s.addEventListener('click',()=>{b.style.display='none';document.getElementById('song').play().catch(e=>console.error("Audio play failed:",e));animate()})}; | |
| createTerrain = () => {const s=worldSize,e=100,g=new THREE.PlaneGeometry(s,s,e,e);g.rotateX(-Math.PI/2);const n=new SimplexNoise(),p=g.attributes.position;for(let i=0;i<p.count;i++){const x=p.getX(i),z=p.getZ(i);let y=10*n.noise(x/500,z/500)-30*n.noise(x/800,z/800)-15*(Math.abs(n.noise(x/200,z/200))**2);p.setY(i,Math.max(y,-50))}g.computeVertexNormals();const m=new THREE.MeshStandardMaterial({color:0x3c322a,roughness:0.8,metalness:0.1});terrain=new THREE.Mesh(g,m);scene.add(terrain)}; | |
| createMermaid = () => {mermaid=new THREE.Group();const s=new THREE.MeshStandardMaterial({color:0x89CFF0,metalness:0.5,roughness:0.2}),t=new THREE.MeshStandardMaterial({color:0x008080,metalness:0.6,roughness:0.1,emissive:0x002222}),h=new THREE.MeshStandardMaterial({color:0xff4500,roughness:0.8});const o=new THREE.Mesh(new THREE.CapsuleGeometry(0.5,1.5,4,8),s);o.position.y=1.0;mermaid.add(o);const a=new THREE.Mesh(new THREE.SphereGeometry(0.7,16,12),s);a.position.y=2.5;mermaid.add(a);const e=new THREE.MeshBasicMaterial({color:0xffffff}),p=new THREE.MeshBasicMaterial({color:0x000000});mermaidState.leftEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);mermaidState.rightEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);const l=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p),r=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p);mermaidState.leftEye.add(l);mermaidState.rightEye.add(r);l.position.z=0.1;r.position.z=0.1;mermaidState.leftEye.position.set(-0.25,2.6,0.55);mermaidState.rightEye.position.set(0.25,2.6,0.55);mermaid.add(mermaidState.leftEye,mermaidState.rightEye);let c=mermaid;for(let i=0;i<8;i++){const g=new THREE.Mesh(new THREE.SphereGeometry(0.4-i*0.04,8,6),t);g.position.y=-0.4;c.add(g);mermaidState.tailSegments.push(g);c=g}const d=new THREE.ShapeGeometry(new THREE.Shape([new THREE.Vector2(0,0),new THREE.Vector2(1.5,0.5),new THREE.Vector2(1,1.5),new THREE.Vector2(0,1),new THREE.Vector2(-1,1.5),new THREE.Vector2(-1.5,0.5)]));const m=new THREE.Mesh(d,t);m.rotation.x=Math.PI/2;m.position.y=-0.5;c.add(m);for(let i=0;i<5;i++){const u=new THREE.CatmullRomCurve3([new THREE.Vector3(0,0,0),new THREE.Vector3(Math.random()-0.5,-1,Math.random()-0.5),new THREE.Vector3((Math.random()-0.5)*2,-3,(Math.random()-0.5)*2),new THREE.Vector3((Math.random()-0.5)*3,-5,(Math.random()-0.5)*3)]);const f=new THREE.TubeGeometry(u,20,0.05,5,false),w=new THREE.Mesh(f,h);w.position.y=2.8;mermaidState.hairStrands.push(w);mermaid.add(w)}mermaid.position.set(0,waterLevel-10,0);scene.add(mermaid)}; | |
| onWindowResize = () => {camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight)}; | |
| // --- NEW/UPDATED WORLD CREATION --- | |
| function createFlora(count) { | |
| const plantRule = { 'F': 'FF+[+F-F-F]-[-F+F+F]' }; | |
| const plantAxiom = 'F'; | |
| const plantLSystem = generateLSystem(plantAxiom, plantRule, 3); | |
| const plantGeom = createLSystemGeometry(plantLSystem, THREE.MathUtils.degToRad(25), 0.5); | |
| plantGeom.scale(2,2,2); | |
| const material = new THREE.MeshStandardMaterial({color: 0x228B22, emissive: 0x113311, side: THREE.DoubleSide}); | |
| for(let i=0; i < count; i++) { | |
| const plantMesh = new THREE.Mesh(plantGeom, material); | |
| const x = (Math.random() - 0.5) * worldSize; | |
| const z = (Math.random() - 0.5) * worldSize; | |
| raycaster.set(new THREE.Vector3(x, waterLevel, z), downVector); | |
| const intersects = raycaster.intersectObject(terrain); | |
| if(intersects.length > 0) { | |
| plantMesh.position.copy(intersects[0].point); | |
| plantMesh.rotation.set(0, Math.random() * Math.PI * 2, 0); | |
| scene.add(plantMesh); | |
| } | |
| } | |
| } | |
| function createMermaidCity(x_offset, z_offset) { | |
| const cityRule = {'F': 'F[+FF][-FF]F[-F][+F]F'}; | |
| const cityAxiom = 'F'; | |
| const cityLSystem = generateLSystem(cityAxiom, cityRule, 4); | |
| const cityGeom = createLSystemGeometry(cityLSystem, THREE.MathUtils.degToRad(20), 5); | |
| const material = new THREE.MeshStandardMaterial({ color: 0x77aaff, emissive: 0x88ccff, emissiveIntensity: 0.8, roughness: 0.6 }); | |
| for(let i = 0; i < 20; i++){ | |
| const building = new THREE.Mesh(cityGeom, material); | |
| building.position.set( | |
| x_offset + (Math.random() - 0.5) * 800, | |
| -50, | |
| z_offset + (Math.random() - 0.5) * 800 | |
| ); | |
| building.scale.setScalar(1 + Math.random() * 2); | |
| building.rotation.set(0, Math.random() * Math.PI * 2, 0); | |
| scene.add(building); | |
| } | |
| } | |
| function createPikeModel() { | |
| const group = new THREE.Group(); | |
| const bodyMat = new THREE.MeshStandardMaterial({ color: 0x90ee90, roughness: 0.5, metalness: 0.2}); | |
| const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.3, 2.5, 4, 8), bodyMat); | |
| body.rotation.z = Math.PI / 2; | |
| const tail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.8, 0.8), bodyMat); | |
| tail.position.x = -1.3; | |
| group.add(body, tail); | |
| return group; | |
| } | |
| function createSunfishModel() { | |
| const group = new THREE.Group(); | |
| const bodyMat = new THREE.MeshStandardMaterial({ color: 0x6495ed, roughness: 0.5, metalness: 0.2}); | |
| const body = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 6), bodyMat); | |
| body.scale.set(1.5, 1, 0.3); | |
| const tail = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.1), bodyMat); | |
| tail.position.x = -1; | |
| group.add(body, tail); | |
| return group; | |
| } | |
| function createBullheadModel() { | |
| const group = new THREE.Group(); | |
| const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8}); | |
| const body = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.8, 1), bodyMat); | |
| const tail = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1, 4), bodyMat); | |
| tail.rotation.z = -Math.PI / 2; | |
| tail.position.x = -1; | |
| group.add(body, tail); | |
| return group; | |
| } | |
| function createSurfaceWildlife(count) { | |
| const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 0.5, 2), new THREE.MeshLambertMaterial({color: 0xeeeeee}), count); | |
| const dummy = new THREE.Object3D(); | |
| for(let i=0; i<count; i++){ | |
| dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel, (Math.random() - 0.5) * worldSize); | |
| dummy.updateMatrix(); | |
| mesh.setMatrixAt(i, dummy.matrix); | |
| } | |
| scene.add(mesh); | |
| } | |
| function createLilyPads(count) { | |
| const g = new THREE.CircleGeometry(1, 8); | |
| g.rotateX(-Math.PI/2); | |
| const mesh = new THREE.InstancedMesh(g, new THREE.MeshLambertMaterial({color: 0x006400}), count); | |
| const dummy = new THREE.Object3D(); | |
| for(let i=0; i<count; i++){ | |
| dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel + 0.1, (Math.random() - 0.5) * worldSize); | |
| dummy.updateMatrix(); | |
| mesh.setMatrixAt(i, dummy.matrix); | |
| } | |
| scene.add(mesh); | |
| } | |
| function updateMermaid() { /* ... no changes ... */ } | |
| updateMermaid = () => {const d=clock.getDelta(),t=clock.getElapsedTime();const m=30.0,r=1.0;let a=0,v=0,f=0;if(controls['KeyW'])f=1.0;if(controls['KeyS'])f=-0.5;if(controls['KeyA'])a=r;if(controls['KeyD'])a=-r;if(controls['Space'])v=10.0;if(controls['ShiftLeft']||controls['ShiftRight'])v=-10.0;mermaidState.turnSpeed=THREE.MathUtils.lerp(mermaidState.turnSpeed,a,d*2.0);mermaid.rotation.y+=mermaidState.turnSpeed*d;mermaidState.forwardSpeed=THREE.MathUtils.lerp(mermaidState.forwardSpeed,m*f,d*1.5);const w=new THREE.Vector3(0,0,-1).applyQuaternion(mermaid.quaternion);mermaidVelocity.x=w.x*mermaidState.forwardSpeed;mermaidVelocity.z=w.z*mermaidState.forwardSpeed;const b=0.5;mermaidVelocity.y=THREE.MathUtils.lerp(mermaidVelocity.y,v||b,d*2.0);mermaid.position.addScaledVector(mermaidVelocity,d);raycaster.set(mermaid.position,downVector);const i=raycaster.intersectObject(terrain);if(i.length>0){const g=i[0].point.y;if(mermaid.position.y<g+3.0){mermaid.position.y=g+3.0;mermaidVelocity.y=Math.max(mermaidVelocity.y,2.0)}}mermaid.position.y=Math.min(mermaid.position.y,waterLevel-1);mermaid.rotation.z=THREE.MathUtils.lerp(mermaid.rotation.z,mermaidState.turnSpeed*-0.5,d*2.0);const p=-mermaidVelocity.y*0.05;mermaid.rotation.x=THREE.MathUtils.lerp(mermaid.rotation.x,p,d*2.5);const s=Math.abs(mermaidState.forwardSpeed/m);mermaidState.tailSegments.forEach((e,i)=>{const n=Math.sin(t*6.0-i*0.6)*(0.1+s*0.6);e.rotation.y=n;e.rotation.z=n*0.5});mermaidState.hairStrands.forEach((e,i)=>{e.rotation.x=Math.sin(t*1.5+i)*0.1-s*0.1;e.rotation.z=Math.sin(t*1.5+i)*0.1});const j=Math.sin(t*20)*0.02;mermaidState.leftEye.children[0].position.x=j;mermaidState.rightEye.children[0].position.x=-j;const o=new THREE.Vector3(0,4,18);o.applyQuaternion(mermaid.quaternion);camera.position.lerp(mermaid.position.clone().add(o),d*2.0);camera.lookAt(mermaid.position.clone().add(new THREE.Vector3(0,2,0)))}; | |
| function animate() { | |
| animationId = requestAnimationFrame(animate); | |
| updateMermaid(); | |
| for (const school of schools) { | |
| school.update(); | |
| } | |
| water.material.uniforms['time'].value += 1.0 / 60.0; | |
| renderer.render(scene, camera); | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |