How It Works — Architecture Overview

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.

Scene & Renderer Setup

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.

Loading the Operating Room Model

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 — First-Person Camera

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.

WASD Movement with Acceleration

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.

Boundary Collision Detection

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.

Lighting for Medical Environments

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.

Real-World Applications

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.

Key Takeaways

COMPLETE SOURCE CODE

Copy this code into an HTML file. Replace the model path with your own GLTF/GLB operating room model:

HTML / Three.js
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Medical Simulation - Operating Room Walkthrough</title>
    <style>
        /* Full-screen, no margins */
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }

        /* Dark overlay shown when simulation is paused */
        #blocker {
            position: fixed; width: 100%; height: 100%;
            background: rgba(0,0,0,0.6); z-index: 10;
        }

        /* Centered play button */
        #instructions {
            position: absolute; top: 50%; left: 50%;
            transform: translate(-50%,-50%); text-align: center; color: white;
        }
        #playButton {
            font-size: 28px; cursor: pointer; font-family: Arial;
            padding: 16px 40px; background: #10b981; color: white;
            border: none; border-radius: 12px; font-weight: bold;
        }

        /* Crosshair — centered dot shown during walkthrough */
        #crosshair {
            position: fixed; top: 50%; left: 50%;
            transform: translate(-50%,-50%);
            width: 8px; height: 8px; background: rgba(16,185,129,0.8);
            border-radius: 50%; display: none; z-index: 20;
            box-shadow: 0 0 10px rgba(16,185,129,0.5);
        }
    </style>
</head>
<body>
    <!-- Pause overlay with play button -->
    <div id="blocker">
        <div id="instructions">
            <button id="playButton">Enter Operating Room</button>
            <p style="margin-top:16px;color:#aaa;font-family:Arial;">
                WASD - Walk | Mouse - Look | ESC - Pause
            </p>
        </div>
    </div>

    <!-- Small green dot crosshair (subtle, professional) -->
    <div id="crosshair"></div>

    <!-- Load Three.js, PointerLockControls, and GLTFLoader from CDN -->
    <script src="https://cdn.jsdelivr.net/npm/three@latest/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/controls/PointerLockControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/loaders/GLTFLoader.js"></script>

    <script>
        // ============================================================
        // SCENE SETUP
        // Create the 3D world, camera at standing eye height,
        // and a high-quality renderer with tone mapping for
        // realistic operating room lighting.
        // ============================================================
        var scene = new THREE.Scene();
        scene.background = new THREE.Color(0x1a1a2e); // Dark blue-gray

        // Camera at eye height (1.6m = average standing eye level)
        var camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 100);
        camera.position.set(0, 1.6, 0); // Standing in center of room

        // High-quality renderer with tone mapping
        var renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.toneMapping = THREE.ReinhardToneMapping; // Handles bright surgical lights
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        document.body.appendChild(renderer.domElement);

        // ============================================================
        // LIGHTING — Operating Room Style
        // High ambient (ORs are very bright) + focused surgical light
        // ============================================================
        // Strong ambient — no dark corners (safety requirement in real ORs)
        scene.add(new THREE.AmbientLight(0xffffff, 1.5));

        // Surgical overhead light — green-teal tint like modern LED panels
        var surgicalLight = new THREE.PointLight(0x10b981, 2, 20);
        surgicalLight.position.set(0, 3, 0); // Directly above operating table
        surgicalLight.castShadow = true;
        scene.add(surgicalLight);

        // Secondary fill light for depth
        var fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
        fillLight.position.set(2, 3, 2);
        scene.add(fillLight);

        // ============================================================
        // LOAD OPERATING ROOM MODEL (GLTF/GLB)
        // Replace this path with your own model file.
        // Free models available on Sketchfab (search "operating room")
        // ============================================================
        var loader = new THREE.GLTFLoader();
        loader.load(
            // REPLACE THIS with your model path:
            'https://www.shanebrumback.com/models/charite_university_hospital_-_operating_room.glb',
            function(gltf) {
                gltf.scene.position.y = 0.009; // Align with floor
                scene.add(gltf.scene);
                console.log('Operating room model loaded successfully');
            },
            function(progress) {
                // Loading progress (useful for loading bars)
                console.log('Loading: ' + Math.round(progress.loaded / progress.total * 100) + '%');
            },
            function(error) {
                console.error('Failed to load model:', error);
            }
        );

        // ============================================================
        // POINTER LOCK CONTROLS — First-Person Camera
        // Captures the mouse cursor for immersive look-around.
        // Click "Enter Operating Room" to lock, ESC to unlock.
        // ============================================================
        var controls = new THREE.PointerLockControls(camera, document.body);

        var blocker = document.getElementById('blocker');
        var instructions = document.getElementById('instructions');
        var crosshair = document.getElementById('crosshair');

        // Click play button to lock pointer (start walkthrough)
        document.getElementById('playButton').addEventListener('click', function() {
            controls.lock();
        });

        // When pointer locks: hide menu, show crosshair
        controls.addEventListener('lock', function() {
            blocker.style.display = 'none';
            crosshair.style.display = 'block';
        });

        // When pointer unlocks (ESC): show menu, hide crosshair
        controls.addEventListener('unlock', function() {
            blocker.style.display = '';
            crosshair.style.display = 'none';
        });

        // REQUIRED: add controls object to scene for movement to work
        scene.add(controls.getObject());

        // ============================================================
        // KEYBOARD MOVEMENT — WASD + Arrow Keys
        // Track which keys are currently held down.
        // Movement is processed in the render loop (not here).
        // ============================================================
        var moveForward = false, moveBackward = false;
        var moveLeft = false, moveRight = false;

        document.addEventListener('keydown', function(e) {
            switch (e.keyCode) {
                case 87: case 38: moveForward = true; break;   // W or Up
                case 83: case 40: moveBackward = true; break;  // S or Down
                case 65: case 37: moveLeft = true; break;      // A or Left
                case 68: case 39: moveRight = true; break;     // D or Right
            }
        });

        document.addEventListener('keyup', function(e) {
            switch (e.keyCode) {
                case 87: case 38: moveForward = false; break;
                case 83: case 40: moveBackward = false; break;
                case 65: case 37: moveLeft = false; break;
                case 68: case 39: moveRight = false; break;
            }
        });

        // ============================================================
        // BOUNDARY COLLISION
        // Prevents walking through walls. The room extends ~3.5 units
        // from center. Adjust this value based on your model's size.
        // ============================================================
        function checkCollision(position) {
            var bounds = 3.5; // Distance from center to walls
            return (
                position.x < -bounds || position.x > bounds ||
                position.z < -bounds || position.z > bounds
            );
        }

        // ============================================================
        // RENDER LOOP — Runs every frame
        // Handles smooth acceleration movement + collision detection.
        // Uses delta time for frame-rate independent speed.
        // ============================================================
        var clock = new THREE.Clock();
        var speed = 0;              // Current movement speed
        var acceleration = 0.003;   // How fast speed ramps up per frame
        var maxSpeed = 0.08;        // Maximum walking speed

        function animate() {
            requestAnimationFrame(animate);
            var delta = clock.getDelta();

            // Only process movement when pointer is locked (game active)
            if (controls.isLocked) {

                // Acceleration: ramp up when keys held, decay when released
                if (moveForward || moveBackward || moveLeft || moveRight) {
                    speed = Math.min(speed + acceleration, maxSpeed);
                } else {
                    speed *= 0.85; // Quick deceleration (0.85 = stops in ~10 frames)
                }

                var moveAmount = speed * delta * 60; // Frame-rate independent

                // Move-then-undo pattern for each direction:
                if (moveForward) {
                    controls.moveForward(moveAmount);
                    if (checkCollision(controls.getObject().position))
                        controls.moveForward(-moveAmount); // Undo if hitting wall
                }
                if (moveBackward) {
                    controls.moveForward(-moveAmount);
                    if (checkCollision(controls.getObject().position))
                        controls.moveForward(moveAmount);
                }
                if (moveLeft) {
                    controls.moveRight(-moveAmount);
                    if (checkCollision(controls.getObject().position))
                        controls.moveRight(moveAmount);
                }
                if (moveRight) {
                    controls.moveRight(moveAmount);
                    if (checkCollision(controls.getObject().position))
                        controls.moveRight(-moveAmount);
                }
            }

            renderer.render(scene, camera);
        }
        animate();

        // ============================================================
        // WINDOW RESIZE — Keep scene responsive
        // ============================================================
        window.addEventListener('resize', function() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

    </script>
</body>
</html>