import * as THREE from 'three'; import { Octree } from 'three/addons/math/Octree.js'; import { Capsule } from 'three/addons/math/Capsule.js'; import { Water } from 'three/addons/objects/Water.js'; // import { Sky } from 'three/addons/objects/Sky.js'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js'; import Stats from 'three/addons/libs/stats.module.js'; import MazeMesh from './MazeMesh.js'; const mazeWidth = 23 const parameters = { elevation: 48, azimuth : 53, }; const waves = { A: { direction: 0, steepness: 0.05, wavelength: 3 }, B: { direction: 30, steepness: 0.10, wavelength: 6 }, C: { direction: 60, steepness: 0.05, wavelength: 1.5 }, }; const showParam = window.location.search.includes("param") const 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); loadMngr.onStart = function (url, itemsLoaded, itemsTotal) { progressCircle.innerText = "0%" progressCircle.style.setProperty("--progress", "0deg") } loadMngr.onProgress = function (url, itemsLoaded, itemsTotal) { progressCircle.innerText = Math.floor(100 * itemsLoaded / itemsTotal) + "%" progressCircle.style.setProperty("--progress", Math.floor(360 * itemsLoaded / itemsTotal)+"deg") } loadMngr.onError = function (url) { message.innerHTML = 'Erreur de chargement' } loadMngr.onLoad = () => { message.innerHTML = "" message.className = "" renderer.setAnimationLoop(animate) }; // const container = document.getElementById('container'); const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: true, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.toneMapping = THREE.ACESFilmicToneMapping; //renderer.toneMappingExposure = 0.5; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.physicallyCorrectLights = true; renderer.outputEncoding = THREE.sRGBEncoding; container.appendChild(renderer.domElement); const scene = new THREE.Scene(); scene.background = new THREE.CubeTextureLoader() .setPath( 'textures/calm-sea-skybox/' ) .load( [ 'ft.jpg', 'bk.jpg', 'up.jpg', 'dn.jpg', 'rt.jpg', 'lf.jpg', ] ); scene.backgroundBlurriness = 0.03; scene.environment = scene.background; window.scene = scene; const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000); camera.rotation.order = 'YXZ'; const collisionner = new THREE.Group(); // Maze const wallMaterial = new THREE.MeshStandardMaterial({ map : loader.load('textures/stonewall/albedo.png'), normalMap : loader.load('textures/stonewall/normal.png'), normalScale : new THREE.Vector2(0.6, 0.6), metalnessMap: loader.load('textures/stonewall/metalness.png'), aoMap : loader.load('textures/stonewall/ao.png'), roughnessMap: loader.load('textures/stonewall/roughness.png'), roughness : 1, envMapIntensity: 0.1 }) const maze = new MazeMesh(mazeWidth, mazeWidth, 1, wallMaterial); maze.castShadow = true; maze.receiveShadow = true; maze.matrixAutoUpdate = false scene.add(maze) console.log(String(maze)) const invisibleWall = new THREE.Mesh(new THREE.BoxGeometry( .9, 1.8, .9 )); invisibleWall.material.visible = false; let matrix = new THREE.Matrix4() for (let i = 0; i < maze.count; i++) { maze.getMatrixAt(i, matrix) const clone = invisibleWall.clone() clone.position.setFromMatrixPosition(matrix); clone.position.y = 1; collisionner.add(clone); } // Ground const groundGeometry = new THREE.BoxGeometry(mazeWidth, mazeWidth, 1) const groundMaterial = new THREE.MeshStandardMaterial({ map: loader.load( 'textures/angled-blocks-vegetation/albedo.png', texture => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } ), aoMap: loader.load( 'textures/angled-blocks-vegetation/ao.png', texture => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } ), metalnessMap: loader.load( 'textures/angled-blocks-vegetation/metallic.png', texture => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } ), normalMap: loader.load( 'textures/angled-blocks-vegetation/normal-dx.png', texture => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } ), roughnessMap: loader.load( 'textures/angled-blocks-vegetation/roughness.png', texture => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } ), }) const sideGroundMaterial = groundMaterial.clone() sideGroundMaterial.map = wallMaterial.map.clone() sideGroundMaterial.normalMap = wallMaterial.normalMap.clone() sideGroundMaterial.metalnessMap = wallMaterial.metalnessMap.clone() sideGroundMaterial.roughnessMap = wallMaterial.roughnessMap.clone() sideGroundMaterial.aoMap = wallMaterial.aoMap.clone() sideGroundMaterial.map.wrapS = sideGroundMaterial.map.wrapT = THREE.RepeatWrapping sideGroundMaterial.normalMap.wrapS = sideGroundMaterial.normalMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.metalnessMap.wrapS = sideGroundMaterial.metalnessMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.aoMap.wrapS = sideGroundMaterial.aoMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.roughnessMap.wrapS = sideGroundMaterial.roughnessMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.map.repeat.set(mazeWidth, 1) sideGroundMaterial.normalMap.repeat.set(mazeWidth, 1) sideGroundMaterial.metalnessMap.repeat.set(mazeWidth, 1) sideGroundMaterial.aoMap.repeat.set(mazeWidth, 1) sideGroundMaterial.roughnessMap.repeat.set(mazeWidth, 1) const ground = new THREE.Mesh( groundGeometry, [ sideGroundMaterial, sideGroundMaterial, sideGroundMaterial, sideGroundMaterial, groundMaterial, groundMaterial, ] ) ground.rotation.x = -Math.PI / 2; ground.position.y = -0.5 ground.receiveShadow = true; ground.matrixAutoUpdate = false ground.updateMatrix(); collisionner.add(ground) // Water const waterGeometry = new THREE.PlaneGeometry(1024, 1024, 512, 512); const ocean = new Water(waterGeometry, { textureWidth : 512, textureHeight: 512, waterNormals : loader.load( 'textures/waternormals.jpg', function (texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; } ), sunDirection : new THREE.Vector3(), sunColor : 0xffffff, waterColor : 0x001e0f, distortionScale: 3.7, fog : scene.fog !== undefined, alpha : 0.9 }); ocean.rotation.x = - Math.PI / 2; ocean.position.y = -0.2; ocean.material.transparent = true; ocean.material.onBeforeCompile = function (shader) { shader.uniforms.size = { value: 6 } shader.uniforms.waveA = { value: [ Math.sin((waves.A.direction * Math.PI) / 180), Math.cos((waves.A.direction * Math.PI) / 180), waves.A.steepness, waves.A.wavelength, ], }; shader.uniforms.waveB = { value: [ Math.sin((waves.B.direction * Math.PI) / 180), Math.cos((waves.B.direction * Math.PI) / 180), waves.B.steepness, waves.B.wavelength, ], }; shader.uniforms.waveC = { value: [ Math.sin((waves.C.direction * Math.PI) / 180), Math.cos((waves.C.direction * Math.PI) / 180), waves.C.steepness, waves.C.wavelength, ], }; shader.vertexShader = document.getElementById('vertexShader').textContent; shader.fragmentShader = document.getElementById('fragmentShader').textContent; }; scene.add(ocean); // Lights const sun = new THREE.Vector3(); //const ambientLight = new THREE.AmbientLight(0x404040, 7); //scene.add(ambientLight); const sunLight = new THREE.DirectionalLight(0xfffae8, 2); sunLight.castShadow = true; sunLight.shadow.camera.near = 0.1; sunLight.shadow.camera.far = 1.4 * mazeWidth; sunLight.shadow.camera.left = -1.4 * mazeWidth/2; sunLight.shadow.camera.right = 1.4 * mazeWidth/2; sunLight.shadow.camera.bottom = -1.4 * mazeWidth/2; sunLight.shadow.camera.top = 1.4 * mazeWidth/2; sunLight.shadow.mapSize.width = 1024; sunLight.shadow.mapSize.height = 1024; //sunLight.shadow.radius = 0.01; sunLight.shadow.bias = 0.0001; sunLight.target = maze scene.add(sunLight); updateSun(); function updateSun() { const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); const theta = THREE.MathUtils.degToRad(parameters.azimuth); sun.setFromSphericalCoords(1.4 * mazeWidth/2, phi, theta); ocean.material.uniforms['sunDirection'].value.copy(sun).normalize(); sunLight.position.copy(sun) //ambientLight.intensity = 5 + 5 * Math.sin(Math.max(THREE.MathUtils.degToRad(parameters.elevation), 0)); } // Raft const raftGeometry = new THREE.BoxGeometry(1.8, .1, 1.1, 1, 1, 8) const woodTexture = loader.load('textures/wood.jpg'); const raftFaceMaterial = new THREE.MeshStandardMaterial({ map: woodTexture, aoMap: woodTexture, roughnessMap: 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 raftSideMaterial = new THREE.MeshStandardMaterial({ map: woodTexture, aoMap: woodTexture, roughnessMap: woodTexture, color: 0xFFFFFF, emissive: 0, specular: 0x505050, shininess: 1, bumpMap: woodTexture, bumpScale: .1, depthFunc: 3, depthTest: true, depthWrite: true, }) const raft = new THREE.Mesh(raftGeometry, [ raftSideMaterial, raftSideMaterial, raftFaceMaterial, raftSideMaterial, raftFaceMaterial, raftFaceMaterial, ]) raft.position.set( .2, ocean.position.y, -mazeWidth/2 - 1 ); raft.rotation.y = 1.4 raft.castShadow = true; collisionner.add(raft); const raftOctree = new Octree(); raftOctree.fromGraphNode(raft) scene.add(collisionner); const worldOctree = new Octree(); worldOctree.fromGraphNode(collisionner); // const stats = new Stats(); if (showStats) container.appendChild(stats.dom); // GUI if (showParam) { const gui = new GUI(); const lightHelper = new THREE.DirectionalLightHelper(sunLight, .5) lightHelper.position.copy(maze.start) lightHelper.visible = false; const octreeHelper = new OctreeHelper(worldOctree); octreeHelper.visible = false; scene.add(octreeHelper); const showHelper = gui.add({ helpers: false }, "helpers") showHelper.onChange(function (value) { lightHelper.visible = value; octreeHelper.visible = value; }); const folderSky = gui.addFolder('Sky'); folderSky.add(parameters, 'elevation', 0, 90, 0.1).onChange(updateSun); folderSky.add(parameters, 'azimuth', - 180, 180, 0.1).onChange(updateSun); folderSky.open(); const waterUniforms = ocean.material.uniforms; const folderWater = gui.addFolder('Water'); folderWater .add(waterUniforms.distortionScale, 'value', 0, 8, 0.1) .name('distortionScale'); folderWater.add(waterUniforms.size, 'value', 0.1, 10, 0.1).name('size'); folderWater.add(ocean.material, 'wireframe'); folderWater.open(); const waveAFolder = gui.addFolder('Wave A'); waveAFolder .add(waves.A, 'direction', 0, 359) .name('Direction') .onChange((v) => { const x = (v * Math.PI) / 180; ocean.material.uniforms.waveA.value[0] = Math.sin(x); ocean.material.uniforms.waveA.value[1] = Math.cos(x); }); waveAFolder .add(waves.A, 'steepness', 0, 1, 0.01) .name('Steepness') .onChange((v) => { ocean.material.uniforms.waveA.value[2] = v; }); waveAFolder .add(waves.A, 'wavelength', 1, 100) .name('Wavelength') .onChange((v) => { ocean.material.uniforms.waveA.value[3] = v; }); waveAFolder.open(); const waveBFolder = gui.addFolder('Wave B'); waveBFolder .add(waves.B, 'direction', 0, 359) .name('Direction') .onChange((v) => { const x = (v * Math.PI) / 180; ocean.material.uniforms.waveB.value[0] = Math.sin(x); ocean.material.uniforms.waveB.value[1] = Math.cos(x); }); waveBFolder .add(waves.B, 'steepness', 0, 1, 0.01) .name('Steepness') .onChange((v) => { ocean.material.uniforms.waveB.value[2] = v; }); waveBFolder .add(waves.B, 'wavelength', 1, 100) .name('Wavelength') .onChange((v) => { ocean.material.uniforms.waveB.value[3] = v; }); waveBFolder.open(); const waveCFolder = gui.addFolder('Wave C'); waveCFolder .add(waves.C, 'direction', 0, 359) .name('Direction') .onChange((v) => { const x = (v * Math.PI) / 180; ocean.material.uniforms.waveC.value[0] = Math.sin(x); ocean.material.uniforms.waveC.value[1] = Math.cos(x); }); waveCFolder .add(waves.C, 'steepness', 0, 1, 0.01) .name('Steepness') .onChange((v) => { ocean.material.uniforms.waveC.value[2] = v; }); waveCFolder .add(waves.C, 'wavelength', 1, 100) .name('Wavelength') .onChange((v) => { ocean.material.uniforms.waveC.value[3] = v; }); waveCFolder.open(); } // const clock = new THREE.Clock(); // Controls const GRAVITY = 30; const STEPS_PER_FRAME = 10; const playerCollider = new Capsule( new THREE.Vector3(0, 25.0, 0), new THREE.Vector3(0, 25.5, 0), 0.3 ); const playerVelocity = new THREE.Vector3(); const playerDirection = new THREE.Vector3(); let playerOnFloor = false; let jumping = false; let escaped = false; const pointerLockControls = new PointerLockControls(camera, document.body); pointerLockControls.pointerSpeed = 0.7; container.addEventListener('click', function () { pointerLockControls.lock(); }); pointerLockControls.addEventListener('lock', function () { ambiance.play(); }); pointerLockControls.addEventListener('unlock', function () { ambiance.pause(); }); scene.add(pointerLockControls.getObject()); const keyStates = {}; document.addEventListener('keydown', (event) => { keyStates[event.code] = true; }); document.addEventListener('keyup', (event) => { keyStates[event.code] = false; if (event.code == 'Space') jumping = false }); function playerCollisions() { if (raftOctree.capsuleIntersect(playerCollider)) { camera.position.y = raft.position.y + 0.9; if (!escaped) 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.innerHTML = 'Libre !'; message.className = "escaped"; piano.play(); 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 ? 100 : 20) / STEPS_PER_FRAME; 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, 25, 0); playerCollider.end.set(0, 25.5, 0); playerCollider.radius = 0.3; camera.position.copy(playerCollider.end); camera.rotation.set(0, 0, 0); message.className = "" escaped = false; } } 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((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 updateRaft(delta) { const t = ocean.material.uniforms['time'].value; const waveInfo = getWaveInfo(raft.position.x, raft.position.z, t); raft.position.y = ocean.position.y + waveInfo.position.y; const quat = new THREE.Quaternion().setFromEuler( new THREE.Euler().setFromVector3(waveInfo.normal) ); raft.quaternion.rotateTowards(quat, delta * 0.5); } window.addEventListener('resize', onWindowResize); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function animate() { const delta = Math.min(0.05, clock.getDelta()) const deltaTime = delta / STEPS_PER_FRAME; ocean.material.uniforms['time'].value += delta; updateRaft(delta); // 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(); } if (camera.position.y > 3.5) camera.lookAt(raft.position.x, raft.position.y, raft.position.z); renderer.render(scene, camera); if (showStats) stats.update(); }