Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<title>Mermaid of 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/three@0.165.0/build/three.module.js", | |
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/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/three@0.165.0/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> | |