Sapienza University of Rome - Interactive Graphics Course Final Project
| Student | Matricola |
|---|---|
| Davide Timperi | 1950722 |
| Alexandru Vivian Pita | 1948533 |
| Federico Mendiola | 1986026 |
Echoes in the Dark is a fully 3D, first-person psychological horror survival game developed entirely in WebGL and Three.js. Created as the final project for the Interactive Graphics course at Sapienza University of Rome, it pushes the boundaries of browser-based gaming without relying on heavy game engines like Unity or Unreal Engine.
The player is trapped in a procedurally styled, claustrophobic labyrinth. The ultimate goal is to locate a Hidden Key and escape through the Goal Door. However, the maze is actively patrolled by a relentless, procedurally-animated monster. Survival depends on resource management, stealth, and spatial awareness.
- First-Person Controls: Standard WASD movement with mouse-look controls.
- Stamina Management: Sprinting (holding
Spacebar) rapidly depletes the stamina bar. If stamina hits zero, the player is forced into a slow walk state until the bar partially recharges. - Immersive Head-Bobbing: Procedural camera motions using trigonometric functions (
Math.sin(),Math.cos()) simulate realistic walking and running gaits.
- Battery Drain: The flashlight is the player's only light source. Its battery drains over time.
- Visual Degradation: As battery decreases, the light intensity dims, the cone angle narrows, and it begins to flicker procedurally (randomised on/off thresholds whose frequency and depth increase as the battery gets lower), increasing tension.
- Doors: Players can approach doors and press
Eto interact. Doors act as physical barriers and break the monster's line of sight, allowing players to hide in rooms. - The Objective: The player must explore rooms to find the Key. Once acquired, the player must navigate to the Goal Door (distinguished by a green light) to escape.
- An invincible entity that stalks the player. You cannot fight it; you can only run and hide.
- If the monster catches the player, a procedural jump-scare animation plays, and the game is over.
The project strictly adheres to the course constraints, featuring custom-built systems for physics, AI, and animations rather than importing pre-made engine solutions.
- Entirely built using vanilla JavaScript and Three.js.
- Asset Management:
.glband.gltfmodels for architecture and props are dynamically loaded and cached (InteriorAssetManager.js). The flashlight prop is loaded viaFBXLoader, which relies onfflateinternally to decompress the format — not a compression step written by the team. - Dynamic Level Configuration: Levels (Easy, Medium, Hard) are defined by grid-based arrays (
MapEasy.js, etc.) parsed by the engine to construct walls, floors, and interactive objects on the fly.
The monster operates on a custom, highly responsive State Machine:
- Idle / Search State: While the player is outside the aggro radius, the monster waits and periodically teleports itself to a room or doorway near the player if too much time passes without contact, keeping the threat from being purely avoidable by distance.
- Chase State: Continuously casts rays towards the player. If the player enters the Line of Sight (and is not blocked by walls/doors), the monster speeds up and pursues aggressively.
- Dynamic Teleportation: To maintain constant psychological pressure, the AI periodically teleports to rooms adjacent to the player if they have been separated for too long. It also features failsafe emergency teleports if the monster becomes stuck in world geometry.
Instead of importing pre-rigged and pre-animated meshes, the monster's animations are 100% procedurally coded:
- Hierarchical Structure: The monster is assembled using Three.js
Object3Dparent-child relationships (Torso -> Arms, Legs, Head). - Trigonometric Gaits: Walking and running cycles are calculated using alternating sine and cosine waves applied to the limbs' rotation axes.
- Attack Animations: Lunges and jump-scares utilize custom Tweening (
TweenManager.js) to snap limbs into aggressive postures rapidly.
To maintain high performance in the browser, the game avoids heavy physics libraries (like Cannon.js):
- AABB & Raycasting: Implements Axis-Aligned Bounding Boxes (AABB) and Raycasters to prevent the player and the monster from clipping through walls, doors, and props.
- Sliding Mechanics: When colliding with a wall at an angle, the collision resolution allows the player to slide smoothly along the surface rather than stopping abruptly.
- Spotlights & Point Lights: Utilizes Three.js lighting models. Environmental lights flicker dynamically.
- Texture Mapping: Diffuse, Normal, and Specular maps (via
THREE.MeshPhongMaterial) are applied to world geometry to give depth and realism to the otherwise flat maze walls; procedural objects such as the key and battery pickups instead use PBRMeshStandardMaterial.
- 3D Positional Audio: Monster footsteps and roars are emitted from the monster's 3D coordinates, allowing the player to locate the threat entirely by sound.
- Dynamic Music: The background track smoothly cross-fades between an "ambience" mix and a more intense "pursuit" mix depending on whether the player is within the monster's aggro range, inducing panic.
To populate the rooms with realistic furniture layouts, the team utilized Large Language Models (LLMs like Claude/Codex) with a specific prompt structure.
The structural maps were provided to the AI, which acted as a level designer to inject 3D models from the team's own prop library (e.g., bed.glb, bookshelf.glb, cabinet.glb) into the rooms. The AI respected real-world proportions, prevented overlapping collisions, and ensured thematic consistency (e.g., grouping bathroom items together). This data is parsed by InteriorAssetManager.js.
📦 final-project-triomonnezza
├── 📂 assets/ # 3D Models (.glb), Textures, and Audio files (.wav)
├── 📂 lib/ # Downloaded external libraries (Three.js, etc.)
├── 📂 src/ # Main Source Code
│ ├── 📂 animations/ # Procedural animation logic (MonsterAnimator, TweenManager)
│ ├── 📂 core/ # Core systems (Player, AI, Audio, Flashlight, Doors)
│ ├── 📂 entities/ # Game entity classes (Monster.js)
│ ├── 📂 ui/ # User interface, Menus, HUD, Blood splatter effects
│ └── 📂 world/ # Map generation, Collision, Textures, Asset instantiation
│ └── 📂 maps/ # Grid-based level layouts (Easy, Medium, Hard)
├── 📄 index.html # Main entry point for the web application
├── 📄 index.css # UI styling and HUD elements
├── 📄 main.js # Game initialization and main render loop
├── 📄 download_libs.py # Script to fetch required libraries
└── 📄 README.md # Project documentation (You are here)
Since the game loads external assets (3D models, textures, sounds), it must be run via a local web server to bypass strict browser CORS policies.
- Clone the repository:
git clone https://github.com/SapienzaInteractiveGraphicsCourse/final-project-triomonnezza.git cd final-project-triomonnezza - Download libraries (if necessary):
python download_libs.py
- Start a local web server:
- Using Python 3:
python -m http.server 8000
- Using Node.js (http-server):
npx http-server -p 8000
- Using VS Code: Install the "Live Server" extension and click "Go Live" on
index.html.
- Using Python 3:
- Play: Open your browser and navigate to
http://localhost:8000.
This project successfully fulfills the core requirements of the Interactive Graphics exam:
- ✅ WebGL & Three.js Framework: Built entirely within the required technological stack.
- ✅ Hierarchical Modeling: The monster is a complex, multi-part hierarchical mesh built in code, not a pre-animated import.
- ✅ Lighting and Textures: Extensively uses Spotlights, Point lights, and multi-layered textures (diffuse/normal/specular via Phong materials, plus PBR materials for procedural objects).
- ✅ User Interaction: Features complex movement, stamina, flashlight management, and interactive doors.
- ✅ Custom Animation: Movement gaits and jump-scares are procedurally generated using mathematical functions and tweens.