Building Interactive 3D Hospital Navigation Systems with WebGL and A* Pathfinding
3D Architecture and Procedural Modeling
Implementing efficient navigation within a hospital facility requires a balance between visual fidelity and performance, particularly on mobile platforms. Instead of relying on heavy external model files, the 3D environment is generated procedurally using Three.js. This approach minimizes load times and optimizes frame rates. By defining geometries and materials at runtime, the system allows for flexible adaptation to different hospital layouts without significant resource overhead.
Dynamic Spatial Labeling
To assist users in identifying specific departments, a dynamic labeling system is employed. This involves attaching 3D coordinates to specific UI elements or markers that overlay the scene. When a user interacts with a department, the system calculates the world position of the target mesh and updates the label's position to float above it, ensuring visibility regardless of camera movement.
class LabelManager {
constructor(scene) {
this.scene = scene;
this.activeLabel = null;
}
updateLabelMarker(targetId, offsetHeight = 2.5) {
const targetMesh = this.scene.getObjectByName(targetId);
if (!targetMesh) return;
const labelMesh = this.scene.getObjectByName("nav_marker");
if (!labelMesh) return;
// Get global position of the target
const worldPos = new THREE.Vector3();
targetMesh.getWorldPosition(worldPos);
// Apply offset based on target type
const isFlatPanel = targetId.includes("panel");
labelMesh.position.copy(worldPos);
labelMesh.position.y += isFlatPanel ? -0.1 : offsetHeight;
}
}
3D Pathfinding Implementation
The core of the navigation system relies on calculating the most efficient route between the user's location and the destination. We utilize a modified A* (A-Star) search algorithm adapted for a three-dimensional grid. This algorithm traverses the navigable nodes in the 3D space, avoiding obstacles such as walls or restricted areas, to generate a precise path.
class Pathfinder {
constructor() {
this.directions = [];
// Generate 26 neighbor directions for 3D movement
for(let x=-1; x<=1; x++) {
for(let y=-1; y<=1; y++) {
for(let z=-1; z<=1; z++) {
if(x===0 && y===0 && z===0) continue;
this.directions.push({x, y, z});
}
}
}
}
calculatePath(startVec, endVec, barrierSet) {
const startNode = { pos: startVec, g: 0, h: 0, f: 0, parent: null };
const endNode = { pos: endVec };
let openList = [startNode];
let closedSet = new Set();
while (openList.length > 0) {
// Sort by lowest F cost
openList.sort((a, b) => a.f - b.f);
const current = openList.shift();
// Check if reached destination
if (this.isEqual(current.pos, endNode.pos)) {
return this.reconstructPath(current);
}
closedSet.add(this.getKey(current.pos));
// Explore neighbors
for (let dir of this.directions) {
const neighborPos = {
x: Math.round(current.pos.x + dir.x),
y: Math.round(current.pos.y + dir.y),
z: Math.round(current.pos.z + dir.z)
};
const neighborKey = this.getKey(neighborPos);
if (barrierSet.has(neighborKey) || closedSet.has(neighborKey)) continue;
const dist = Math.sqrt(dir.x**2 + dir.y**2 + dir.z**2);
const tentativeG = current.g + dist;
let neighborNode = openList.find(n => this.isEqual(n.pos, neighborPos));
if (!neighborNode) {
neighborNode = {
pos: neighborPos,
g: tentativeG,
h: this.heuristic(neighborPos, endNode.pos),
parent: current
};
neighborNode.f = neighborNode.g + neighborNode.h;
openList.push(neighborNode);
} else if (tentativeG < neighborNode.g) {
neighborNode.g = tentativeG;
neighborNode.f = neighborNode.g + neighborNode.h;
neighborNode.parent = current;
}
}
}
return null;
}
heuristic(posA, posB) {
return Math.sqrt(
Math.pow(posA.x - posB.x, 2) +
Math.pow(posA.y - posB.y, 2) +
Math.pow(posA.z - posB.z, 2)
);
}
getKey(pos) {
return `${pos.x},${pos.y},${pos.z}`;
}
isEqual(v1, v2) {
return v1.x === v2.x && v1.y === v2.y && v1.z === v2.z;
}
reconstructPath(node) {
const path = [];
let temp = node;
while (temp) {
path.unshift([temp.pos.x, temp.pos.y, temp.pos.z]);
temp = temp.parent;
}
return path;
}
}
Audio Guidance Integration
To enhance accessibility, the system includes text-to-speech capabilities. The solution prioritizes the browser's native Web Speech API for real-time feedback. If local synthesis is unavailable or specific voice quality is required, it falls back to cloud-based text-to-speech services. This ensures that users receive turn-by-turn audio instructions regardless of their device capabilities.
class AudioNavigator {
constructor() {
this.synth = window.speechSynthesis;
}
announce(message) {
if (!this.synth) {
console.warn("Speech Synthesis not supported");
return;
}
// Cancel any currently playing speech
this.synth.cancel();
const utterance = new SpeechSynthesisUtterance(message);
utterance.lang = 'en-US'; // Set locale dynamically as needed
utterance.rate = 1.0;
utterance.pitch = 1.0;
this.synth.speak(utterance);
}
}