This medical simulation combines four core systems:
The result is a fully interactive first-person walkthrough where medical students can explore a virtual operating room, examine equipment placement, and practice spatial awareness — all in the browser with no plugins required.
Medical simulations require high visual fidelity. We configure the renderer with several quality settings:
// High-quality renderer for medical visualization
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ReinhardToneMapping; // Prevents blown-out highlights
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadow edges
The camera uses a 60° field of view (narrower than the typical 75° for games) to simulate a more natural human perspective appropriate for a professional training environment.
The operating room is a single GLTF/GLB file containing all geometry, materials, and textures. GLTFLoader handles the entire loading pipeline — parsing the binary data, creating Three.js meshes, applying materials, and building the scene hierarchy.
After loading, we position the model slightly above y=0 to align with the floor plane. The camera starts at a standing eye-height position (y=1.6, approximately 5'3" eye level) inside the room.
// Load the operating room GLTF model
var loader = new THREE.GLTFLoader();
loader.load('operating-room.glb', function(gltf) {
gltf.scene.position.y = 0.009; // Slight offset to align with floor
scene.add(gltf.scene);
});
// Camera at standing eye height inside the room
camera.position.set(0, 1.6, 0); // x=center, y=eye height, z=center
Where to get models: Free medical GLTF models are available on Sketchfab (search "operating room" or "hospital"). You can also export from Blender in GLTF format. The model used in this tutorial is a Charité University Hospital operating room.
PointerLockControls is what makes the experience feel like a real first-person walkthrough. When the user clicks "Play," the browser hides the mouse cursor and sends all mouse movement directly to the camera rotation — just like a PC game.
The controls provide two key methods: moveForward(distance) and moveRight(distance). These move the camera in the direction it's currently facing, which is exactly what you want for WASD movement — pressing W always moves you forward relative to where you're looking.
// PointerLockControls — captures mouse for FPS-style camera
var controls = new THREE.PointerLockControls(camera, document.body);
// Lock pointer on click (hides cursor, enables mouse-look)
playButton.addEventListener('click', function() { controls.lock(); });
// Show/hide UI based on lock state
controls.addEventListener('lock', function() {
blocker.style.display = 'none'; // Hide menu
crosshair.style.display = 'block'; // Show aiming reticle
});
controls.addEventListener('unlock', function() {
blocker.style.display = 'block'; // Show menu
crosshair.style.display = 'none'; // Hide reticle
});
// IMPORTANT: add the controls object to the scene
scene.add(controls.getObject());
Why add to scene? PointerLockControls creates an internal Object3D that holds the camera. Adding it to the scene is required for moveForward() and moveRight() to work correctly — they move this parent object, not the camera directly.
Instead of instant movement (which feels robotic), we implement smooth acceleration. When a key is pressed, speed ramps up gradually to a maximum. When released, speed decays quickly. This creates a natural, weighty feel appropriate for a professional simulation.
// Acceleration-based movement (feels natural, not robotic)
var acceleration = 0.003; // How fast speed ramps up
var maxSpeed = 0.08; // Maximum walking speed
var speed = 0; // Current speed (starts at 0)
// In the render loop:
if (moveForward || moveBackward || moveLeft || moveRight) {
speed = Math.min(speed + acceleration, maxSpeed); // Ramp up
} else {
speed *= 0.85; // Quick deceleration when no keys pressed
}
var moveAmount = speed * delta * 60; // Frame-rate independent
if (moveForward) controls.moveForward(moveAmount);
if (moveBackward) controls.moveForward(-moveAmount);
if (moveLeft) controls.moveRight(-moveAmount);
if (moveRight) controls.moveRight(moveAmount);
The delta * 60 multiplication ensures consistent speed regardless of frame rate. A 30fps machine and a 144fps machine will have the same walking speed.
Without collision detection, users would walk through walls and outside the operating room. We use the "move-then-undo" pattern: attempt the move, check if the new position is out of bounds, and if so, reverse the move.
// Boundary check — keeps user inside the room
function checkCollision(position) {
var bounds = 3.5; // Room extends 3.5 units from center in each direction
return (
position.x < -bounds || position.x > bounds ||
position.z < -bounds || position.z > bounds
);
}
// Move-then-undo pattern:
if (moveForward) {
controls.moveForward(moveAmount);
// If we're now outside bounds, undo the move
if (checkCollision(controls.getObject().position)) {
controls.moveForward(-moveAmount);
}
}
For more complex rooms: Replace the simple bounds check with raycasting against the room geometry. Cast a short ray in the movement direction — if it hits a wall within the step distance, block the move. This handles irregular room shapes and furniture.
Operating rooms have distinctive lighting — bright overhead surgical lights with softer ambient fill. We replicate this with:
The high ambient intensity (1.5 vs typical 0.5) is intentional — operating rooms are extremely well-lit environments. Dimmer lighting would feel unrealistic for this context.
This same architecture powers real medical training platforms:
To extend this into a full training platform, add: clickable equipment with info popups (raycasting), step-by-step procedure guides (UI overlays), multi-user support (WebSocket), and assessment scoring.
Copy this code into an HTML file. Replace the model path with your own GLTF/GLB operating room model: