Download js/engine/WorldEngine.js from timfromhcs/3d-outdoor-map-explorer: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/spaces/timfromhcs/3d-outdoor-map-explorer/resolve/main/js/engine/WorldEngine.js
- Command line
-
hf download hf://spaces/timfromhcs/3d-outdoor-map-explorer/js/engine/WorldEngine.js
-
curl -L -o WorldEngine.js https://huggingface.co/spaces/timfromhcs/3d-outdoor-map-explorer/resolve/main/js/engine/WorldEngine.js
14.7 kB
| /** | |
| * WorldEngine.js | |
| * Core Three.js runtime engine coordinating render layers, dual-camera system | |
| * (POV / Orbit), asset loading, and animation loop. | |
| */ | |
| class WorldEngine { | |
| constructor(container) { | |
| this.container = container; | |
| this.scene = new THREE.Scene(); | |
| // Atmospheric daylight sky and subtle haze | |
| this.scene.background = new THREE.Color(0x8cb8e6); | |
| this.scene.fog = new THREE.FogExp2(0xa5cbf0, 0.008); | |
| this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.05, 2000); | |
| this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| this.renderer.outputEncoding = THREE.sRGBEncoding; | |
| this.renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| this.renderer.toneMappingExposure = 0.95; | |
| this.renderer.shadowMap.enabled = true; | |
| this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| this.container.appendChild(this.renderer.domElement); | |
| this.collisionSystem = new CollisionSystem(); | |
| this.player = new PlayerController(this.camera, this.renderer.domElement, this.collisionSystem); | |
| this.orbitControls = new THREE.OrbitControls(this.camera, this.renderer.domElement); | |
| this.orbitControls.enableDamping = true; | |
| this.orbitControls.dampingFactor = 0.08; | |
| this.orbitControls.maxPolarAngle = Math.PI / 2 + 0.05; // Prevent camera going below ground | |
| this.orbitControls.enabled = true; // Starts in Orbit Overview mode | |
| this.currentMode = "ORBIT"; // "ORBIT" or "POV" | |
| this.currentMapManifest = null; | |
| // Visual Layers | |
| this.layers = { | |
| render: null, | |
| collision: null, | |
| walkable: null, | |
| segmentation: null, | |
| navGraph: null, | |
| bbox: null, | |
| grid: null, | |
| playerHelper: null | |
| }; | |
| this.layerVisibility = { | |
| render: true, | |
| collision: false, | |
| walkable: false, | |
| segmentation: false, | |
| navGraph: false, | |
| wireframe: false, | |
| bbox: false, | |
| playerHelper: false | |
| }; | |
| this.clock = new THREE.Clock(); | |
| this.loader = new THREE.GLTFLoader(); | |
| this._setupLighting(); | |
| this._setupGroundGrid(); | |
| this._setupPlayerHelper(); | |
| window.addEventListener('resize', () => this.onResize()); | |
| } | |
| _setupLighting() { | |
| // Multi-angle realistic outdoor daylight lighting calibrated for vertex colors | |
| const ambient = new THREE.AmbientLight(0xffffff, 0.35); | |
| this.scene.add(ambient); | |
| // Sky & ground hemisphere bounce | |
| const hemiLight = new THREE.HemisphereLight(0xffffff, 0x556677, 0.45); | |
| hemiLight.position.set(0, 50, 0); | |
| this.scene.add(hemiLight); | |
| // Primary warm sun | |
| const sun = new THREE.DirectionalLight(0xfff5e6, 0.75); | |
| sun.position.set(25, 45, 30); | |
| sun.castShadow = true; | |
| this.scene.add(sun); | |
| // Secondary fill / ground bounce light | |
| const backSun = new THREE.DirectionalLight(0xddeeff, 0.25); | |
| backSun.position.set(-25, 20, -25); | |
| this.scene.add(backSun); | |
| } | |
| _setupGroundGrid() { | |
| const grid = new THREE.GridHelper(80, 40, 0x5588bb, 0x99bbee); | |
| grid.position.y = -0.01; | |
| this.layers.grid = grid; | |
| this.scene.add(grid); | |
| } | |
| _setupPlayerHelper() { | |
| const geom = new THREE.CylinderGeometry(0.35, 0.35, 1.7, 12); | |
| const mat = new THREE.MeshStandardMaterial({ color: 0x00e5ff, wireframe: true }); | |
| this.layers.playerHelper = new THREE.Mesh(geom, mat); | |
| this.layers.playerHelper.visible = false; | |
| this.scene.add(this.layers.playerHelper); | |
| } | |
| onResize() { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| /** | |
| * Toggles between First Person POV mode and Aerial Orbit mode. | |
| */ | |
| setMode(mode) { | |
| this.currentMode = mode; | |
| if (mode === "POV") { | |
| this.orbitControls.enabled = false; | |
| this.player.respawn("POV"); | |
| this.player.controls.lock(); | |
| this.layers.playerHelper.visible = false; | |
| } else { | |
| this.player.controls.unlock(); | |
| this.orbitControls.enabled = true; | |
| if (this.layers.render) { | |
| const box = new THREE.Box3().setFromObject(this.layers.render); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| const size = box.getSize(new THREE.Vector3()); | |
| const maxDim = Math.max(size.x, size.z, 15); | |
| this.orbitControls.target.copy(center); | |
| this.camera.position.set( | |
| center.x + maxDim * 0.85, | |
| center.y + maxDim * 0.75, | |
| center.z + maxDim * 0.85 | |
| ); | |
| } else { | |
| this.orbitControls.target.copy(this.player.position); | |
| this.camera.position.set( | |
| this.player.position.x + 10, | |
| this.player.position.y + 8, | |
| this.player.position.z + 10 | |
| ); | |
| } | |
| this.orbitControls.update(); | |
| this.layers.playerHelper.visible = this.layerVisibility.playerHelper; | |
| } | |
| } | |
| /** | |
| * Clears all layers of the currently loaded map. | |
| */ | |
| clearMap() { | |
| ['render', 'collision', 'walkable', 'segmentation', 'navGraph', 'bbox'].forEach(k => { | |
| if (this.layers[k]) { | |
| this.scene.remove(this.layers[k]); | |
| this.layers[k] = null; | |
| } | |
| }); | |
| } | |
| /** | |
| * Loads a map dynamically from its manifest. | |
| */ | |
| async loadMap(manifest) { | |
| this.currentMapManifest = manifest; | |
| this.clearMap(); | |
| const basePath = `/output/${manifest.id}`; | |
| // Helper to load GLB safely | |
| const loadGLB = (relPath) => { | |
| return new Promise((resolve) => { | |
| if (!relPath) return resolve(null); | |
| const url = `${basePath}/${relPath}`; | |
| this.loader.load( | |
| url, | |
| (gltf) => resolve(gltf.scene), | |
| undefined, | |
| (err) => { | |
| console.warn(`Asset not available: ${url}`); | |
| resolve(null); | |
| } | |
| ); | |
| }); | |
| }; | |
| // Load visual and physical layers concurrently | |
| const [renderScene, colScene, walkScene, segScene] = await Promise.all([ | |
| loadGLB(manifest.assets.render ? manifest.assets.render.path : null), | |
| loadGLB(manifest.assets.collision ? manifest.assets.collision.path : null), | |
| loadGLB(manifest.assets.walkable ? manifest.assets.walkable.path : null), | |
| loadGLB(manifest.assets.segmentation ? manifest.assets.segmentation.path : null) | |
| ]); | |
| if (renderScene) { | |
| this.layers.render = renderScene; | |
| this.layers.render.visible = this.layerVisibility.render; | |
| this.scene.add(renderScene); | |
| } | |
| if (colScene) { | |
| this.layers.collision = colScene; | |
| this.layers.collision.visible = this.layerVisibility.collision; | |
| this.scene.add(colScene); | |
| } | |
| if (walkScene) { | |
| this.layers.walkable = walkScene; | |
| this.layers.walkable.visible = this.layerVisibility.walkable; | |
| this.scene.add(walkScene); | |
| } | |
| if (segScene) { | |
| this.layers.segmentation = segScene; | |
| this.layers.segmentation.visible = this.layerVisibility.segmentation; | |
| this.scene.add(segScene); | |
| } | |
| // Configure high visual fidelity, double-sided rendering & smooth normals | |
| [renderScene, colScene, walkScene, segScene].forEach(scene => { | |
| if (!scene) return; | |
| scene.traverse(child => { | |
| if (child.isMesh) { | |
| if (child.material) { | |
| if (Array.isArray(child.material)) { | |
| child.material.forEach(m => { | |
| m.side = THREE.DoubleSide; | |
| m.roughness = 0.65; | |
| m.metalness = 0.05; | |
| m.needsUpdate = true; | |
| }); | |
| } else { | |
| child.material.side = THREE.DoubleSide; | |
| child.material.roughness = 0.65; | |
| child.material.metalness = 0.05; | |
| child.material.needsUpdate = true; | |
| } | |
| } | |
| if (child.geometry && !child.geometry.attributes.normal) { | |
| child.geometry.computeVertexNormals(); | |
| } | |
| } | |
| }); | |
| }); | |
| // Bounding Box Helper | |
| const mainMesh = renderScene || colScene || walkScene; | |
| if (mainMesh) { | |
| const box = new THREE.Box3().setFromObject(mainMesh); | |
| this.layers.bbox = new THREE.Box3Helper(box, 0x58a6ff); | |
| this.layers.bbox.visible = this.layerVisibility.bbox; | |
| this.scene.add(this.layers.bbox); | |
| // Auto-frame Orbit camera to center of map | |
| if (this.currentMode === 'ORBIT') { | |
| const center = box.getCenter(new THREE.Vector3()); | |
| const size = box.getSize(new THREE.Vector3()); | |
| const maxDim = Math.max(size.x, size.z, 15); | |
| this.orbitControls.target.copy(center); | |
| this.camera.position.set( | |
| center.x + maxDim * 0.85, | |
| center.y + maxDim * 0.75, | |
| center.z + maxDim * 0.85 | |
| ); | |
| this.orbitControls.update(); | |
| } | |
| } | |
| // Load Nav Graph if available | |
| if (manifest.assets.nav_graph) { | |
| this._loadNavGraph(`${basePath}/${manifest.assets.nav_graph.path}`); | |
| } | |
| // Extract primary mesh for collision detection | |
| const colMesh = this._extractMainTrimesh(this.layers.collision || this.layers.walkable || this.layers.render); | |
| const walkMesh = this._extractMainTrimesh(this.layers.walkable); | |
| this.collisionSystem.setCollisionMesh(colMesh, walkMesh, manifest.bounds); | |
| // Position player at safe spawn point | |
| const spawn = (manifest.spawn_points && manifest.spawn_points.length > 0) | |
| ? manifest.spawn_points[0] | |
| : { position: [0, 1.7, 0], lookYaw: 0 }; | |
| this.player.setSpawn(spawn, manifest.bounds.extents, this.currentMode); | |
| this.applyWireframe(this.layerVisibility.wireframe); | |
| return true; | |
| } | |
| _extractMainTrimesh(sceneGroup) { | |
| if (!sceneGroup) return null; | |
| let found = null; | |
| sceneGroup.traverse((child) => { | |
| if (child.isMesh && child.geometry && !found) { | |
| found = child; | |
| } | |
| }); | |
| return found; | |
| } | |
| async _loadNavGraph(url) { | |
| try { | |
| const res = await fetch(url); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| const graphGroup = new THREE.Group(); | |
| // Render nodes | |
| const nodeGeom = new THREE.SphereGeometry(0.008, 6, 6); | |
| const nodeMat = new THREE.MeshBasicMaterial({ color: 0x00e5ff }); | |
| const nodeInstanced = new THREE.InstancedMesh(nodeGeom, nodeMat, data.nodes.length); | |
| const matrix = new THREE.Matrix4(); | |
| data.nodes.forEach((n, i) => { | |
| matrix.setPosition(n.x, n.y + 0.01, n.z); | |
| nodeInstanced.setMatrixAt(i, matrix); | |
| }); | |
| nodeInstanced.instanceMatrix.needsUpdate = true; | |
| graphGroup.add(nodeInstanced); | |
| // Render edges | |
| const linePositions = []; | |
| const nodeMap = new Map(); | |
| data.nodes.forEach(n => nodeMap.set(n.id, [n.x, n.y + 0.01, n.z])); | |
| data.edges.forEach(e => { | |
| const p1 = nodeMap.get(e.source); | |
| const p2 = nodeMap.get(e.target); | |
| if (p1 && p2) { | |
| linePositions.push(...p1, ...p2); | |
| } | |
| }); | |
| if (linePositions.length > 0) { | |
| const lineGeom = new THREE.BufferGeometry(); | |
| lineGeom.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3)); | |
| const lineMat = new THREE.LineBasicMaterial({ color: 0x00bcd4, transparent: true, opacity: 0.6 }); | |
| const lines = new THREE.LineSegments(lineGeom, lineMat); | |
| graphGroup.add(lines); | |
| } | |
| graphGroup.visible = this.layerVisibility.navGraph; | |
| this.layers.navGraph = graphGroup; | |
| this.scene.add(graphGroup); | |
| } catch (e) { | |
| console.warn("Could not load nav graph visualizer:", e); | |
| } | |
| } | |
| setLayerVisible(name, visible) { | |
| this.layerVisibility[name] = visible; | |
| if (this.layers[name]) { | |
| this.layers[name].visible = visible; | |
| } | |
| } | |
| applyWireframe(wireframe) { | |
| this.layerVisibility.wireframe = wireframe; | |
| this.scene.traverse((obj) => { | |
| if (obj.isMesh && obj.material && obj !== this.layers.playerHelper) { | |
| if (Array.isArray(obj.material)) { | |
| obj.material.forEach(m => m.wireframe = wireframe); | |
| } else { | |
| obj.material.wireframe = wireframe; | |
| } | |
| } | |
| }); | |
| } | |
| update() { | |
| const delta = this.clock.getDelta(); | |
| if (this.currentMode === "POV") { | |
| this.player.update(delta); | |
| } else { | |
| this.orbitControls.update(); | |
| // Sync player helper position in orbit mode | |
| if (this.layers.playerHelper) { | |
| this.layers.playerHelper.position.set( | |
| this.player.position.x, | |
| this.player.position.y + this.player.height * 0.5, | |
| this.player.position.z | |
| ); | |
| } | |
| } | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| } | |
| window.WorldEngine = WorldEngine; | |