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 { 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' //import 'three-hex-tiling' // LOADING const loadingMazeWidth = 23 const loadingMazeHeight = 23 for(let y=0; y < loadingMazeHeight; y++) { let tr = document.createElement("tr") loadingMazeTable.appendChild(tr) for(let x=0; x < loadingMazeWidth; x++) { let td = document.createElement("td") tr.appendChild(td) } } let walls function dig(x, y) { walls[y][x] = false loadingMazeTable.children[y].children[x].className = "ground" } const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]] function* build(x, y) { for (let direction of Array.from(directions).sort(x => .5 - Math.random())) { let [dx, dy] = direction let x1 = x + dx let y1 = y + dy let x2 = x1 + dx let y2 = y1 + dy if (0 <= x2 && x2 < loadingMazeWidth && 0 <= y2 && y2 < loadingMazeHeight && walls[y2][x2]) { dig(x1, y1) yield x1, y1 dig(x2, y2) yield x2, y2 yield* build(x2, y2) } } } function* endlessLoadingMaze() { while (true) { for (const tr of loadingMazeTable.children) { for (const td of tr.children) { td.className = "wall" } } walls = Array(loadingMazeHeight).fill(true).map(row => Array(loadingMazeWidth).fill(true)) let x0 = Math.floor(loadingMazeWidth / 2) let y0 = Math.floor(loadingMazeHeight / 2) dig(x0, y0) yield* build(x0, y0) } } let interval const loadMngr = new THREE.LoadingManager() const loader = new THREE.TextureLoader(loadMngr) loader.setPath("textures/") loadMngr.onStart = function (url, itemsLoaded, itemsTotal) { progress.innerText = "0" let loadingMazeIterator = endlessLoadingMaze() interval = window.setInterval(() => loadingMazeIterator.next(), 200) } loadMngr.onProgress = function (url, itemsLoaded, itemsTotal) { progress.innerText = Math.floor(100 * itemsLoaded / itemsTotal) } loadMngr.onError = function (url) { loadingMessage.innerHTML = `Erreur de chargement :
${url}` } loadMngr.onLoad = function (url, itemsLoaded, itemsTotal) { loading.style.display = "none" window.clearInterval(interval) renderer.setAnimationLoop(animate) setInterval(() => { let x = Math.floor(8 + camera.position.x * 16 / mazeWidth) let y = Math.floor(8 + camera.position.z * 16 / mazeWidth) favicon.href = `favicon.php?x=${x}&y=${y}` }, 1000) } // GAME const playerHeight = 0.5 const mazeWidth = 23 const parameters = { elevation: 48, azimuth : 53, } const waves = { A: { direction: 0, steepness: 0.06, wavelength: 4 }, B: { direction: 30, steepness: 0.10, wavelength: 6 }, C: { direction: 60, steepness: 0.05, wavelength: 1.5 }, } const ambiance = new Audio("snd/ambiance.mp3") ambiance.loop = true const piano = new Audio("snd/waves-and-tears.mp3") piano.loop = false const container = document.getElementById('container') const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", }) renderer.setPixelRatio(window.devicePixelRatio) renderer.setSize(window.innerWidth, window.innerHeight) renderer.toneMapping = THREE.ACESFilmicToneMapping renderer.shadowMap.enabled = true renderer.shadowMap.type = THREE.PCFSoftShadowMap container.appendChild(renderer.domElement) const scene = new THREE.Scene() scene.background = new THREE.CubeTextureLoader(loadMngr) .setPath( 'textures/calm-sea-skybox/' ) .load( [ 'ft.webp', 'bk.webp', 'up.webp', 'dn.webp', 'rt.webp', 'lf.webp', ] ) scene.backgroundBlurriness = 0.03 scene.backgroundIntensity = 1.4 scene.environment = scene.background window.scene = scene const camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.1, 1000) camera.rotation.order = 'YXZ' camera.position.set(0, 25 + playerHeight, 0) const mazeCollisionner = new THREE.Group() // Maze const wallMaterial = new THREE.MeshStandardMaterial({ map : loader.load('Poly-cobblestone-wall/color_map.webp'), normalMap : loader.load('Poly-cobblestone-wall/normal_map_opengl.webp'), aoMap : loader.load('Poly-cobblestone-wall/ao_map.webp'), roughnessMap : loader.load('Poly-cobblestone-wall/roughness_map.webp'), roughness : 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 dev = window.location.search.includes("dev") if (!dev) { 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 mazeCollisionner.add(clone) } } // Ground const groundGeometry = new THREE.BoxGeometry(mazeWidth, mazeWidth, 20) function repeatGroundMaterial (texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(mazeWidth / 4, mazeWidth / 4) } const groundMaterial = new THREE.MeshStandardMaterial({ map : loader.load('angled-blocks-vegetation/albedo.webp', repeatGroundMaterial), aoMap : loader.load('angled-blocks-vegetation/ao-roughness-metalness.webp', repeatGroundMaterial), metalnessMap: loader.load('angled-blocks-vegetation/ao-roughness-metalness.webp', repeatGroundMaterial), normalMap : loader.load('angled-blocks-vegetation/normal-dx.webp', repeatGroundMaterial), roughnessMap: loader.load('angled-blocks-vegetation/ao-roughness-metalness.webp', repeatGroundMaterial), /*hexTiling : { patchScale: 1, useContrastCorrectedBlending: true, lookupSkipThreshold: 0.01, textureSampleCoefficientExponent: 32, }*/ }) const sideGroundMaterial = new THREE.MeshStandardMaterial({ map : wallMaterial.map.clone(), normalMap : wallMaterial.normalMap.clone(), normalScale : new THREE.Vector2(0.6, 0.6), aoMap : wallMaterial.aoMap.clone(), roughnessMap : wallMaterial.roughnessMap.clone(), roughness : 1, }) sideGroundMaterial.map.wrapS = sideGroundMaterial.map.wrapT = THREE.RepeatWrapping sideGroundMaterial.map.repeat.set(mazeWidth, 20) sideGroundMaterial.map.rotation = Math.PI sideGroundMaterial.normalMap.wrapS = sideGroundMaterial.normalMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.normalMap.repeat.set(mazeWidth, 20) sideGroundMaterial.normalMap.rotation = Math.PI sideGroundMaterial.aoMap.wrapS = sideGroundMaterial.aoMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.aoMap.repeat.set(mazeWidth, 20) sideGroundMaterial.aoMap.rotation = Math.PI sideGroundMaterial.roughnessMap.wrapS = sideGroundMaterial.roughnessMap.wrapT = THREE.RepeatWrapping sideGroundMaterial.roughnessMap.repeat.set(mazeWidth, 20) sideGroundMaterial.roughnessMap.rotation = Math.PI const ground = new THREE.Mesh( groundGeometry, [ sideGroundMaterial, sideGroundMaterial, sideGroundMaterial, sideGroundMaterial, groundMaterial, groundMaterial, ] ) ground.rotation.x = -Math.PI / 2 ground.position.y = -10 ground.receiveShadow = true ground.matrixAutoUpdate = false ground.updateMatrix() mazeCollisionner.add(ground) scene.add(mazeCollisionner) const mazeOctree = new Octree().fromGraphNode(mazeCollisionner) // Water const waterGeometry = new THREE.PlaneGeometry(512, 512, 128, 128) const ocean = new Water(waterGeometry, { textureWidth : 256, textureHeight: 256, waterNormals : loader.load( 'waternormals.webp', 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.7 }) 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) const oceanOctree = new Octree().fromGraphNode(ocean) // Lights const sun = new THREE.Vector3() const ambientLight = new THREE.AmbientLight(0x404040, 5) scene.add(ambientLight) const sunLight = new THREE.DirectionalLight(0xffffff, 1) 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, 16) function repeatRaftMaterial(texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping texture.repeat.set(2, 1) } const raftMaterial = new THREE.MeshStandardMaterial({ map: loader.load("Poly-wood/color_map.webp", repeatRaftMaterial), aoMap: loader.load("Poly-wood/ao_map.webp", repeatRaftMaterial), normalMap: loader.load("Poly-wood/normal_map_opengl.webp", repeatRaftMaterial), normalScale : new THREE.Vector2(2, 2), roughnessMap: loader.load("Poly-wood/roughness_map.webp", repeatRaftMaterial), depthFunc: 3, depthTest: true, depthWrite: true, displacementMap: loader.load("Poly-wood/displacement_map.webp", repeatRaftMaterial), displacementScale: -0.3, displacementBias: 0.15, }) const raft = new THREE.Mesh(raftGeometry, raftMaterial) raft.position.set( .25, ocean.position.y, -mazeWidth/2 - 1.1 ) raft.castShadow = true scene.add(raft) const raftOctree = new Octree().fromGraphNode(raft) // GUI const stats = new Stats() if (dev) { container.appendChild(stats.dom) const gui = new GUI() const lightHelper = new THREE.DirectionalLightHelper(sunLight, .5) lightHelper.position.copy(maze.start) lightHelper.visible = false const octreeHelper = new OctreeHelper(mazeOctree) octreeHelper.visible = false scene.add(octreeHelper) const showHelper = gui.add({ helpers: false }, "helpers") showHelper.onChange(function (value) { lightHelper.visible = value octreeHelper.visible = value }) const cameraFolder = gui.addFolder("camera") cameraFolder.add(camera, "focus", 0, 200).onChange(() => camera.updateProjectionMatrix()) cameraFolder.add(camera, "fov", 0, 200).onChange(() => camera.updateProjectionMatrix()) cameraFolder.add(camera, "filmGauge", 0, 200).onChange(() => camera.updateProjectionMatrix()) const raftFolder = gui.addFolder("Raft") const raftPositionFolder = raftFolder.addFolder("position") raftPositionFolder.add(raft.position, "x") raftPositionFolder.add(raft.position, "y") raftPositionFolder.add(raft.position, "z") const raftRotationFolder = raftFolder.addFolder("rotation") raftRotationFolder.add(raft.rotation, "x") raftRotationFolder.add(raft.rotation, "y") raftRotationFolder.add(raft.rotation, "z") raftFolder.close() const skyFolder = gui.addFolder('Sky') skyFolder.add(parameters, 'elevation', 0, 90, 0.1).onChange(updateSun) skyFolder.add(parameters, 'azimuth', - 180, 180, 0.1).onChange(updateSun) skyFolder.close() const waterUniforms = ocean.material.uniforms const waterFolder = gui.addFolder('Water') waterFolder .add(waterUniforms.distortionScale, 'value', 0, 8, 0.1) .name('distortionScale') waterFolder.add(waterUniforms.size, 'value', 0.1, 10, 0.1).name('size') waterFolder.add(ocean.material, 'wireframe') waterFolder.close() const waveAFolder = waterFolder.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 = waterFolder.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 = waterFolder.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 hexTilingFolder = gui.addFolder('Hex Tiling') if (wallMaterial?.hexTiling?.patchScale) { const wallMaterialFolder = hexTilingFolder.addFolder("wall") wallMaterialFolder.add(wallMaterial.hexTiling, "patchScale", 0, 10) wallMaterialFolder.add(wallMaterial.hexTiling, "useContrastCorrectedBlending") wallMaterialFolder.add(wallMaterial.hexTiling, "lookupSkipThreshold", 0, 1) wallMaterialFolder.add(wallMaterial.hexTiling, "textureSampleCoefficientExponent", 0, 64).name("SampleCoefExp") } if (groundMaterial?.hexTiling?.patchScale) { const groundMaterialFolder = hexTilingFolder.addFolder("ground") groundMaterialFolder.add(groundMaterial.hexTiling, "patchScale", 0, 10) groundMaterialFolder.add(groundMaterial.hexTiling, "useContrastCorrectedBlending") groundMaterialFolder.add(groundMaterial.hexTiling, "lookupSkipThreshold", 0, 1) groundMaterialFolder.add(groundMaterial.hexTiling, "textureSampleCoefficientExponent", 0, 64).name("SampleCoefExp") } hexTilingFolder.close() } // 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 + playerHeight, 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 const keyStates = {} document.addEventListener('keydown', (event) => { keyStates[event.code] = true }) document.addEventListener('keyup', (event) => { keyStates[event.code] = false if (event.code == 'Space') jumping = false }) let mouseButtonsStates = [] function onMouseChange(event) { for(let i=0; i < mouseButtonsStates.length || i <= Math.log2(event.buttons); i++) { mouseButtonsStates[i] = (event.buttons & (1 << i)) > 0 } } container.addEventListener('click', function () { pointerLockControls.lock() }) pointerLockControls.addEventListener('lock', function () { ambiance.play() document.addEventListener('mousedown', onMouseChange) document.addEventListener('mouseup', onMouseChange) }) pointerLockControls.addEventListener('unlock', function () { ambiance.pause() document.removeEventListener('mousedown', onMouseChange) document.removeEventListener('mouseup', onMouseChange) }) scene.add(pointerLockControls.getObject()) function playerCollisions() { const playerOnMaze = mazeOctree.capsuleIntersect(playerCollider) const playerOnRaft = raftOctree.capsuleIntersect(playerCollider) const playerOnWater = oceanOctree.capsuleIntersect(playerCollider) const result = playerOnMaze || playerOnRaft || playerOnWater 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)) if (playerOnRaft) { camera.position.y = playerCollider.end.y + raft.position.y if (!escaped) gameEnd() } else if (playerOnWater) { const t = ocean.material.uniforms['time'].value const waveInfo = getWaveInfo(playerCollider.end.x, playerCollider.end.z, t) camera.position.y = ocean.position.y + waveInfo.position.y + 0.2 } } } function gameEnd() { escaped = true message.innerHTML = '

Libre !

Rejouer' 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 damping *= 0.1; // small air resistance } playerVelocity.addScaledVector(playerVelocity, damping) const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime) playerCollider.translate(deltaPosition) camera.position.copy(playerCollider.end) playerCollisions() } 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'] || mouseButtonsStates[0]) { playerVelocity.add(getForwardVector().multiplyScalar(speedDelta)) } if (keyStates["ArrowDown"] || keyStates['KeyS'] || mouseButtonsStates[1]) { 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 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) } if (camera.position.y > 3.5) camera.lookAt(raft.position.x, raft.position.y, raft.position.z) renderer.render(scene, camera) if (dev) stats.update() }