import * as THREE from 'three'; import Stats from 'three/addons/libs/stats.module.js'; import { Octree } from 'three/addons/math/Octree.js'; import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js'; import { Capsule } from 'three/addons/math/Capsule.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { Water } from 'three/addons/objects/Water.js'; import { Sky } from 'three/addons/objects/Sky.js'; import MazeMesh from './MazeMesh.js'; const mazeLength = 23 const mazeWidth = 23 const latitude = THREE.MathUtils.degToRad(35) const longitude = THREE.MathUtils.degToRad(25) let showGUI = window.location.search.includes("debug") let showStats = window.location.search.includes("stats") const ambiance = new Audio("snd/ambiance.mp3") ambiance.loop = true const piano = new Audio("snd/waves-and-tears.mp3") piano.loop = false const loadMngr = new THREE.LoadingManager(); const loader = new THREE.TextureLoader(loadMngr); const waterTexture = loader.load('img/waternormals.jpg'); const groundTexture = loader.load('img/pavement.jpg'); const wallTexture = loader.load('img/wall.jpg'); const woodTexture = loader.load('img/wood.jpg'); loadMngr.onLoad = () => { animate(); }; const clock = new THREE.Clock(); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.rotation.order = 'YXZ'; camera.position.set( 0, 0.7, 0); const worldOctree = new Octree(); const raftOctree = new Octree(); const container = document.getElementById( 'container' ); const renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap ; renderer.toneMapping = THREE.ACESFilmicToneMapping; container.appendChild( renderer.domElement ); // Water waterTexture.wrapS = waterTexture.wrapT = THREE.RepeatWrapping; const ocean = new Water( new THREE.PlaneGeometry( 1000, 1000 ), { textureWidth : 512, textureHeight : 512, waterNormals : waterTexture, sunDirection : new THREE.Vector3(), sunColor : 0xffffff, waterColor : 0x001e0f, distortionScale: 3.7, fog : scene.fog !== undefined, alpha : 0.7 } ); ocean.rotation.x = - Math.PI * 0.5; ocean.position.y = -.01 ocean.receiveShadow = true; ocean.material.transparent = true; scene.add( ocean ); // Lights const ambientLight = new THREE.AmbientLight( 0x404040 , 1 ); // soft white light scene.add( ambientLight ); const sunLight = new THREE.DirectionalLight( 0xffffff, 0.3 ); sunLight.castShadow = true; sunLight.shadow.camera.near = 20; sunLight.shadow.camera.far = 200; sunLight.shadow.camera.right = 10; sunLight.shadow.camera.left = -10; sunLight.shadow.camera.top = 10; sunLight.shadow.camera.bottom = -10; sunLight.shadow.mapSize.width = 4096; sunLight.shadow.mapSize.height = 4096; sunLight.shadow.radius = 4; sunLight.target = camera scene.add( sunLight ); const torchLight = new THREE.SpotLight(0xffffe8, 1, mazeLength/2, .45, 1) scene.add( torchLight ); scene.add( torchLight.target ); // Skybox const sun = new THREE.Vector3(); const sky = new Sky(); sky.scale.setScalar( 10000 ); 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 parameters = { elevation: 70, azimuth: 160 }; const pmremGenerator = new THREE.PMREMGenerator( renderer ); let renderTarget; const today = new Date() const startOfYear = new Date(today.getFullYear(), 0, 0); const diff = today - startOfYear; const oneDay = 1000 * 60 * 60 * 24; const dayOfYear = Math.floor(diff / oneDay); const declination = 0.40928 * Math.sin(2*Math.PI*(dayOfYear+284)/365) const startHour = 24 * Math.random() function updateSun() { let elevation, azimuth if ( showGUI ) { elevation = THREE.MathUtils.degToRad( parameters.elevation ); azimuth = THREE.MathUtils.degToRad( parameters.azimuth ); } else { const time = clock.elapsedTime ; const hour = ( startHour + time / 1440 ) % 24 const hourAngle = Math.PI * (1-hour/12) elevation = Math.asin( Math.sin(declination)*Math.sin(latitude) + Math.cos(declination)*Math.cos(latitude)*Math.cos(hourAngle) ) azimuth = -Math.PI/2 + Math.asin( Math.cos(declination)*Math.sin(hourAngle)/Math.cos(elevation) ) } const phi = Math.PI/2 - elevation const theta = azimuth sun.setFromSphericalCoords( 100, phi, theta ); sky.material.uniforms[ 'sunPosition' ].value.copy( sun ); ocean.material.uniforms[ 'sunDirection' ].value.copy( sun ).normalize(); ambientLight.intensity = 0.5 + Math.max( elevation, 0 )/Math.PI; if ( elevation >= 0 ) { sunLight.visible = true torchLight.visible = false } else { sunLight.visible = false torchLight.visible = true } if ( renderTarget !== undefined ) renderTarget.dispose(); renderTarget = pmremGenerator.fromScene( sky ); scene.environment = renderTarget.texture; } updateSun(); const updateSunIntervalId = setInterval( updateSun, 100 ); // Ground const groundGeometry = new THREE.PlaneGeometry(mazeLength, mazeWidth) groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping groundTexture.repeat.set(50, 50) const groundMaterial = new THREE.MeshPhongMaterial( { map : groundTexture, color : 0xFFFFFF, emissive : 0, specular : 0x000000, shininess : 5, bumpMap : groundTexture, bumpScale : .02, depthFunc : 3, depthTest : true, depthWrite: true } ) const ground = new THREE.Mesh( groundGeometry, groundMaterial ) ground.rotation.x = - Math.PI / 2; ground.receiveShadow = true; ground.matrixAutoUpdate = false ground.updateMatrix(); scene.add(ground) worldOctree.fromGraphNode( ground ) // Raft const raftGeometry = new THREE.BoxGeometry( 1.8, .1, .9, 1, 1, 8 ) const raftMaterial = new THREE.MeshPhongMaterial( { map : woodTexture, color : 0xFFFFFF, emissive : 0, specular : 0x505050, shininess : 1, bumpMap : woodTexture, bumpScale : .1, depthFunc : 3, depthTest : true, depthWrite : true, displacementMap : woodTexture, displacementScale: -0.08 } ) const raft = new THREE.Mesh( raftGeometry, raftMaterial ) raft.position.set( .2, 0, -1 - mazeWidth/2 ) raft.rotation.y = 1.4 raft.rotation.order = 'ZXY'; raft.castShadow = true; scene.add(raft) worldOctree.fromGraphNode( raft ) raftOctree.fromGraphNode( raft ) // Maze const wallMaterial = new THREE.MeshPhongMaterial( { map : wallTexture, color : 0xFCF8E5, emissive : 0, specular : 0x505050, shininess : 4, bumpMap : wallTexture, bumpScale : .01, depthFunc : 3, depthTest : true, depthWrite: true } ) const maze = new MazeMesh( mazeLength, mazeWidth, wallMaterial ); maze.castShadow = true; maze.receiveShadow = true; maze.matrixAutoUpdate = false scene.add(maze) console.log(String(maze)) const wall = new THREE.Mesh( maze.geometry ); let matrix = new THREE.Matrix4() for ( let i=0; i { keyStates[ event.code ] = true; } ); document.addEventListener( 'keyup', ( event ) => { keyStates[ event.code ] = false; if ( event.code == 'Space' ) jumping = false } ); container.addEventListener( 'mousedown', () => { document.body.requestPointerLock(); } ); function lockChangeAlert() { if (document.pointerLockElement === document.body) { ambiance.play() } else { ambiance.pause() } } document.addEventListener("pointerlockchange", lockChangeAlert, false); document.body.addEventListener( 'mousemove', ( event ) => { if ( document.pointerLockElement === document.body ) { camera.rotation.y -= event.movementX / 500; camera.rotation.x -= event.movementY / 500; } } ); window.addEventListener( 'resize', onWindowResize ); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function playerCollisions() { if ( !escaped && raftOctree.capsuleIntersect( playerCollider ) ) { gameEnd() } const result = worldOctree.capsuleIntersect( playerCollider ); playerOnFloor = false; if ( result ) { playerOnFloor = result.normal.y > 0; if ( ! playerOnFloor ) { playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) ); } playerCollider.translate( result.normal.multiplyScalar( result.depth ) ); } } function gameEnd() { escaped = true; message.className = "escaped"; piano.play(); } addEventListener("animationend", (event) => { clearInterval( updateSunIntervalId ); document.exitPointerLock(); container.style.cursor = "default"; }); function updatePlayer( deltaTime ) { let damping = Math.exp( - 4 * deltaTime ) - 1; if ( ! playerOnFloor ) { playerVelocity.y -= GRAVITY * deltaTime; // small air resistance damping *= 0.1; } playerVelocity.addScaledVector( playerVelocity, damping ); const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime ); playerCollider.translate( deltaPosition ); playerCollisions(); camera.position.copy( playerCollider.end ); } function getForwardVector() { camera.getWorldDirection( playerDirection ); playerDirection.y = 0; playerDirection.normalize(); return playerDirection; } function getSideVector() { camera.getWorldDirection( playerDirection ); playerDirection.y = 0; playerDirection.normalize(); playerDirection.cross( camera.up ); return playerDirection; } function controls( deltaTime ) { // gives a bit of air control const speedDelta = deltaTime * ( playerOnFloor ? 10 : 2 ); if ( keyStates["ArrowUp"] || keyStates[ 'KeyW' ] ) { playerVelocity.add( getForwardVector().multiplyScalar( speedDelta ) ); } if ( keyStates["ArrowDown"] || keyStates[ 'KeyS' ] ) { playerVelocity.add( getForwardVector().multiplyScalar( - speedDelta ) ); } if ( keyStates["ArrowLeft"] || keyStates[ 'KeyA' ] ) { playerVelocity.add( getSideVector().multiplyScalar( - speedDelta ) ); } if ( keyStates["ArrowRight"] || keyStates[ 'KeyD' ] ) { playerVelocity.add( getSideVector().multiplyScalar( speedDelta ) ); } if ( playerOnFloor && jumping == false ) { if ( keyStates[ 'Space' ] ) { playerVelocity.y = 9; jumping = true } } } function teleportPlayerIfOob() { if ( camera.position.y <= - 25 ) { playerCollider.start.set( 0, 0.3, 0 ); playerCollider.end.set( 0, 0.7, 0 ); playerCollider.radius = 0.3; camera.position.copy( playerCollider.end ); camera.rotation.set( 0, 0, 0 ); message.className= "" } } const waves = { A: { direction: 0, steepness: 0.015, wavelength: 10, }, B: { direction: 30, steepness: 0.015, wavelength: 5, }, C: { direction: 60, steepness: 0.015, wavelength: 3, }, } function getWaveInfo(x, z, time) { const pos = new THREE.Vector3() const tangent = new THREE.Vector3(1, 0, 0) const binormal = new THREE.Vector3(0, 0, 1) Object.keys(waves).forEach(function (wave) { const w = waves[wave] const k = (Math.PI * 2) / w.wavelength const c = Math.sqrt(9.8 / k) const d = new THREE.Vector2( Math.sin((w.direction * Math.PI) / 180), -Math.cos((w.direction * Math.PI) / 180) ) const f = k * (d.dot(new THREE.Vector2(x, z)) - c * time) const a = w.steepness / k pos.x += d.y * (a * Math.cos(f)) pos.y += a * Math.sin(f) pos.z += d.x * (a * Math.cos(f)) tangent.x += -d.x * d.x * (w.steepness * Math.sin(f)) tangent.y += d.x * (w.steepness * Math.cos(f)) tangent.z += -d.x * d.y * (w.steepness * Math.sin(f)) binormal.x += -d.x * d.y * (w.steepness * Math.sin(f)) binormal.y += d.y * (w.steepness * Math.cos(f)) binormal.z += -d.y * d.y * (w.steepness * Math.sin(f)) }) const normal = binormal.cross(tangent).normalize() return { position: pos, normal: normal, } } function animate() { const delta = Math.min( 0.05, clock.getDelta() ) const deltaTime = delta / STEPS_PER_FRAME; // we look for collisions in substeps to mitigate the risk of // an object traversing another too quickly for detection. for ( let i = 0; i < STEPS_PER_FRAME; i ++ ) { controls( deltaTime ); updatePlayer( deltaTime ); teleportPlayerIfOob(); } const time = clock.elapsedTime; ocean.material.uniforms[ 'time' ].value += 1.0 / 100.0; const waveInfo = getWaveInfo(raft.position.x, raft.position.z, time) raft.position.y = waveInfo.position.y const quat = new THREE.Quaternion().setFromEuler( new THREE.Euler(waveInfo.normal.x, waveInfo.normal.y, waveInfo.normal.z) ) raft.quaternion.rotateTowards(quat, delta * 0.5) if ( sunLight.visible ) { sunLight.position.copy( sun ).add( camera.position ) } if ( torchLight.visible ) { torchLight.position.copy(camera.position) torchLight.position.y -= .2 const targetDirection = camera.getWorldDirection(camera.up).add(camera.position) torchLight.target.position.copy(targetDirection) } renderer.render( scene, camera ); if ( showStats ) stats.update(); requestAnimationFrame( animate ); }