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 = '