diff options
Diffstat (limited to 'init.js')
-rw-r--r-- | init.js | 425 |
1 files changed, 425 insertions, 0 deletions
@@ -0,0 +1,425 @@ +import * as THREE from "three"; +import maplibregl from "maplibre-gl"; +import "maplibre-gl/dist/maplibre-gl.css"; +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { SVGLoader } from "three/addons/loaders/SVGLoader.js"; +import * as SunCalc from "suncalc"; +import { vec3, mat4 } from "gl-matrix"; + +let target; + +// if something fishy happens to the local storage, errors or load could render the game unplayable +// use try-catch to prevent this +try { + target = JSON.parse(localStorage.getItem("position")); +} catch {} +target = target || { lng: 9.142202119898826, lat: 49.97692244755174 }; + +const map = new maplibregl.Map({ + container: "map", + center: target, + minZoom: 16, + maxZoom: 21, + zoom: 20, + pitch: 45, + minPitch: 1, + antialias: true, + dragPan: false, + scrollZoom: { around: "center" }, + touchZoomRotate: { around: "center" }, + doubleClickZoom: false, + // key leakage is part of maptiler's ecosystem *shrug* + // their "fix" is to allow restricting keys to certain 'Origin' headers ("pinky promise uwu") + // honestly api keys are cringe anyway + style: + "https://api.maptiler.com/maps/streets/style.json?key=DOnvuOySyPyQM83lAx0a", + /*{ + version: 8, + sources: { + osm: { + type: "raster", + tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"], + tileSize: 256, + maxzoom: 20, + }, + }, + layers: [ + { + id: "osm", + type: "raster", + source: "osm", + }, + ], + },*/ +}); + +// hack. otherwise, zooming/rotating won't work while moving +map.stop = () => {}; + +// https://github.com/maplibre/maplibre-gl-js/discussions/1521 +map.getCameraPosition = () => { + const pitch = map.transform._pitch; + const altitude = Math.cos(pitch) * map.transform.cameraToCenterDistance; + const latOffset = Math.tan(pitch) * map.transform.cameraToCenterDistance; + const latPosPointInPixels = map.transform.centerPoint.add( + new maplibregl.Point(0, latOffset), + ); + const latLong = map.transform.pointLocation(latPosPointInPixels); + const verticalScaleConstant = + map.transform.worldSize / + (2 * Math.PI * 6378137 * Math.abs(Math.cos(latLong.lat * (Math.PI / 180)))); + const altitudeInMeters = altitude / verticalScaleConstant; + return { + lng: latLong.lng, + lat: latLong.lat, + altitude: altitudeInMeters, + pitch: (pitch * 180) / Math.PI, + }; +}; + +const clamp = (min, max, x) => Math.min(max, Math.max(min, x)); + +const camera = new THREE.PerspectiveCamera(); +const scene = new THREE.Scene(); + +const marker = new THREE.Group(); + +new SVGLoader().load("marker.svg", (data) => { + const material = new THREE.MeshBasicMaterial({ + color: new THREE.Color(0), + side: THREE.DoubleSide, + depthWrite: true, + transparent: false, + }); + + for (const shape of data.paths.flatMap(SVGLoader.createShapes)) { + const geometry = new THREE.ShapeGeometry(shape); + const mesh = new THREE.Mesh(geometry, material); + mesh.scale.setScalar(1 / 1792); + mesh.position.set(-0.5, (1536 - 118.237) / 1792, 0); + //mesh.position.set(-0.5, 0, (1536 - 118.237) / 1792); + mesh.rotateX(Math.PI); + //mesh.rotateX(-Math.PI/2); + marker.add(mesh); + } + + marker.scale.setScalar(50); + //marker.on("click", console.log); + //scene.add(marker); +}); + +const mapToMerc = maplibregl.MercatorCoordinate.fromLngLat; + +const mercToThree = (pos, center = mapToMerc(map.getCenter(), 0)) => { + return new THREE.Vector3( + pos.x - center.x, + pos.z - center.z, + pos.y - center.y, + ).divideScalar(center.meterInMercatorCoordinateUnits()); +}; + +const mapToThree = (lngLat, altitude) => { + return mercToThree(mapToMerc(lngLat, altitude)); +}; + +/*new THREE.FBXLoader().load( + "raphtalia.fbx", + ((model) => { + model.traverse((child) => { + if (child.isMesh) { + // child.material.color = new THREE.Color(0xffffff); + // delete child.material.color; + (child.material.isMaterial ? [child.material] : child.material) + .forEach(m => { + m.castShadow = true; + }) + + child.castShadow = true; + } + }); + this.scene.add(model); + player = model; + update(target); + }).bind(this) +);*/ + +const enableShadow = (model) => { + model.traverse((child) => { + if (child.isMesh) { + (child.material.isMaterial ? [child.material] : child.material).forEach( + (m) => { + m.castShadow = true; + }, + ); + + child.castShadow = true; + } + }); +}; + +let player; +{ + const path = "mei/"; // jasper/ + const scale = 3.0; // 1.5 + + new GLTFLoader() + .setPath(path) + .setResourcePath(path) + .load("scene.gltf", (gltf) => { + player = gltf; + + enableShadow(player.scene); + player.scene.scale.setScalar(scale); + + player.clock = new THREE.Clock(); + player.mixer = new THREE.AnimationMixer(player.scene); + player.walk = player.mixer.clipAction(player.animations[0]); + + scene.add(player.scene); + }); +} + +// shadow plane +{ + const geometry = new THREE.PlaneGeometry(60, 60); + geometry.lookAt(new THREE.Vector3(0, 1, 0)); + + const material = new THREE.ShadowMaterial(); + material.opacity = 0.3; + + const plane = new THREE.Mesh(geometry, material); + plane.receiveShadow = true; + // plane.position.set(0, 0, 0.01); + scene.add(plane); +} + +// animated circle around player +{ + const geometry = new THREE.CircleGeometry(7, 64); + geometry.lookAt(new THREE.Vector3(0, 1, 0)); + + const material = new THREE.MeshBasicMaterial({ color: 0xbebab6 }); + material.transparent = true; + + const circle = new THREE.Mesh(geometry, material); + circle.position.set(0, 0.01, 0); + scene.add(circle); + + const clock = new THREE.Clock(); + + let t = 0.0; + const animate = () => { + const ph = [0.75, 0.9, 1.25]; + t = (t + (clock.getDelta() * ph[2]) / 5) % ph[2]; + + const rlerp = (min, max, x) => clamp(0, 1, (x - min) / (max - min)); + const pow = Math.pow; + + circle.scale.setScalar(pow(rlerp(ph[0], ph[1], t), 2.0)); + material.opacity = pow(1 - rlerp(ph[1], ph[2], t), 2.0) * 0.8; + + requestAnimationFrame(animate); + }; + + animate(); +} + +class Celestial extends THREE.DirectionalLight { + constructor(color, intensity, positionFunc) { + super(color, intensity); + + this.castShadow = true; + this.shadow.mapSize.width = 1024; + this.shadow.mapSize.height = 1024; + + const frustumSize = 15; + this.shadow.camera = new THREE.OrthographicCamera( + -frustumSize / 2, + frustumSize / 2, + frustumSize / 2, + -frustumSize / 2, + 1, + 50, + ); + + this.positionFunc = positionFunc; + this.update(); + + //scene.add(new THREE.CameraHelper(this.shadow.camera)); + scene.add(this); + + this.time = 23; + addEventListener( + "keypress", + ((evt) => { + switch (evt.key) { + case "h": + this.time += 1; + break; + case "l": + this.time -= 1; + break; + default: + return; + } + evt.preventDefault(); + }).bind(this), + ); + } + + update() { + const pos = map.getCenter(); + const p = this.positionFunc( + new Date(this.time * 1000 * 60 * 10), + pos.lat, + pos.lng, + ); + + p.altitude = (p.altitude + Math.PI * 2) % (Math.PI * 2); + this.visible = + p.altitude > Math.PI * 0.05 && p.altitude < Math.PI * (1 - 0.05); + + this.position + .set(1, 0, 0) + .applyEuler(new THREE.Euler(0, p.altitude, p.azimuth)) + .multiplyScalar(5); + + this.shadow.camera.position.copy(this.position); + this.shadow.camera.lookAt(scene.position); + } +} + +scene.add(new THREE.AmbientLight(0xffffff, 0.8)); + +const sun = new Celestial(0xffffff, 0.4, SunCalc.getPosition); +// const moon = new Celestial(0x506886, 0.4, SunCalc.getMoonPosition); + +setInterval(() => { + sun.update(); + // moon.update(); +}, 10); + +const renderer = new THREE.WebGLRenderer({ + canvas: map.getCanvas(), + context: map.painter.context.gl, + antialias: true, +}); +renderer.shadowMap.enabled = true; +// renderer.shadowMap.type = THREE.PCFSoftShadowMap; +renderer.autoClear = false; + +const info = document.body.appendChild(document.createElement("span")); +info.style.position = "absolute"; +info.style.zIndex = 5; +info.style.color = "green"; + +const render = (gl, mercViewProj) => { + if (player) player.mixer.update(player.clock.getDelta()); + + const camMap = map.getCameraPosition(); + const camMerc = mapToMerc(camMap, camMap.altitude); + const cam = mercToThree(camMerc); + + const mercViewProjI = mat4.invert([], mercViewProj); + + const depthNCDtoThree = (depth) => { + const [x, y, z] = vec3.transformMat4([], [0, 0, depth], mercViewProjI); + return mercToThree({ x, y, z }, camMerc).length(); + }; + + camera.aspect = innerWidth / innerHeight; + camera.fov = map.transform.fov; + camera.near = depthNCDtoThree(-1); + camera.far = depthNCDtoThree(+1); + camera.updateProjectionMatrix(); + + camera.position.copy(cam); + camera.lookAt(scene.position); + + cam.y = 0; + marker.lookAt(cam); + + renderer.resetState(); + renderer.render(scene, camera); + map.triggerRepaint(); +}; + +map.on("style.load", () => { + map.addLayer( + { + id: "3d-model", + type: "custom", + renderingMode: "3d", + render, + }, + "building-3d", + ); +}); + +addEventListener("resize", () => { + renderer.setSize(innerWidth, innerHeight); +}); + +let playerAnimDuration = 0; +{ + const clock = new THREE.Clock(); + + const animate = () => { + requestAnimationFrame(animate); + + const dt = clock.getDelta(); + + if (playerAnimDuration <= 0) return; + + const lerp = (a, b, x) => a * (1 - x) + b * x; + const x = Math.min(dt / playerAnimDuration, 1); + + const center = map.getCenter(); + center.lng = lerp(center.lng, target.lng, x); + center.lat = lerp(center.lat, target.lat, x); + + playerAnimDuration -= dt; + + if (playerAnimDuration <= 0) player.walk.stop(); + + map.setCenter(center); + }; + + animate(); +} + +const clock = new THREE.Clock(); +clock.getDelta(); + +const setTarget = (pos) => { + const dt = clock.getDelta(); + + if (player) { + player.scene.lookAt(mapToThree(pos)); + player.walk.play(); + } + + playerAnimDuration = Math.min(dt, 1.5); + localStorage.setItem("position", JSON.stringify((target = pos))); +}; + +const watchGeo = navigator.geolocation.watchPosition( + ({ coords: { longitude: lng, latitude: lat } }) => { + const pos = { lng, lat }; + setTarget(pos); + }, + (err) => { + // todo: err.message; + navigator.geolocation.clearWatch(watchGeo); + + const click = (evt) => { + setTarget(evt.lngLat); + }; + + map.on("click", click); + map.on("touched", click); + }, + { + enableHighAccuracy: true, + }, +); |