- About
- Why This Project Exists
- Features
- Tech Stack
- Project Structure
- Architecture
- The Scene: 5 Groups, 5 Challenges
- How It Works
- Getting Started
- Configuration
- Performance
- Responsive Design
- Best Practices
- Technical Decisions
- FAQ
- Future Improvements
- Developer Notes
- Contributing
- Credits
- License
This is not a copied tutorial. Not a downloaded template that was slightly modified. Every line of code here was written with a clear purpose: to learn, to experiment, and to create something that feels alive.
Three.js Lab is a personal laboratory where I explored everything that can be achieved with Three.js without relying on complicated build tools or heavy frameworks. Just modern HTML, vanilla JavaScript, and a genuine curiosity to understand how things work from the inside.
Each group in the scene represents a different challenge. Every texture, every shadow, every particle was placed there intentionally. There is no dead code. No leftover "just in case" features.
This project is the sum of hours of trial and error, of searching for answers, of understanding why something wasn't working, and of the deep satisfaction of finally seeing it come to life.
The best way to learn something is to build something with it.
Rather than following a course or reading documentation passively, I chose to answer a series of technical questions by actually building the answers:
- How do you generate procedural terrain with custom shaders?
- How do you load a 3D model and clone it efficiently across a scene?
- How do you create particle systems that move organically over time?
- How do you make 3D objects interactive with raycasting?
- How do you structure a complex scene without everything becoming chaos?
Each group in the scene is a practical, working answer to one of these questions. Together, they form a complete playground for 3D experimentation on the web.
| Feature | Description |
|---|---|
| π³ Hierarchical 3D Models | Tree.glb loaded and cloned within nested sub-groups |
| π«οΈ Particle System | 40 orbiting petals + fog particle with webp texture |
| ποΈ Procedural Terrain | Custom GLSL shader with 15 octaves of 3D Simplex noise |
| π‘ Animated Lights | 6 luminous spheres with PointLight moving sinusoidally |
| π§ PBR Textures | Wood (2K) and marble (1K) with full material sets |
| π€ Interactive 3D Text | "sebastian v." in Luckiest Guy, orange hover + click modal |
| πͺ Animated Modal | Smooth GSAP transitions: scale, rotation, blur and fade |
| π±οΈ Orbital Controls | Free camera with damping to explore the scene |
| π± Responsive Design | Adapts to any window size with automatic camera updates |
| Category | Technology | Version |
|---|---|---|
| 3D Engine | Three.js | 0.170.0 |
| Animation | GSAP | 3.12.5 |
| Camera Controls | OrbitControls | β |
| Model Loading | GLTFLoader | β |
| 3D Text | FontLoader + TextGeometry | β |
| Hosting | Vercel | β |
No bundlers. No transpilers. No complexity.
The entire project runs on native ES modules with import maps, loading dependencies directly from CDN. This was a deliberate choice β not a limitation.
pratica-threejs/
β
βββ index.html β Entry point: canvas, modal, links, importmap
βββ main.js β Core logic: scene, groups, shaders, animations
βββ style.css β Styles: font, modal, canvas, footer buttons
β
βββ font/
β βββ LuckiestGuy-Regular.ttf β Font for CSS (modal)
β βββ Luckiest Guy_Regular.json β Font for Three.js (3D text)
β
βββ img/
β βββ fog-5.webp β Fog particle texture
β βββ petal.webp β Petal texture for orbiting particles
β βββ preview.jpg β Project preview image
β
βββ models/
β βββ Tree.glb β 3D tree model (GLTF Binary)
β
βββ textures/
βββ pared/ β Marble textures for walls
β βββ marble_108_basecolor-1K.png
β βββ marble_108_height-1K.png
β βββ marble_108_normal-1K.png
β βββ marble_108_roughness-1K.png
β
βββ plane/ β Wood textures for base floor
βββ woodplank_39_AmbientOcclusion-2K.jpg
βββ woodplank_39_BaseColor-2K.jpg
βββ woodplank_39_Height-2K.jpg
βββ woodplank_39_Normal-2K.jpg
βββ woodplank_39_Roughness-2K.jpg
17 files Β· ~4.5 MB of assets Β· Zero build steps
The project follows a single-file architecture for the core logic (main.js), with HTML and CSS separated into their own files. This was intentional β for a project of this scope, over-engineering the file structure would add complexity without value.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β index.html β
β Canvas (#webgl) Β· Modal Β· Footer Β· Importmap β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββ
β main.js β
β β
β ββββββββββββββββ ββββββββββββββββ β
β β Asset Loader β β Scene Setup β β
β β Promise.all ββββ Renderer β β
β β 9 textures β β Camera β β
β β 1 model β β Lights β β
β β 2 particles β β Controls β β
β ββββββββββββββββ ββββββββ¬ββββββββ β
β β β
β ββββββββββββββββββββββββββΌββββββββββββββββββ β
β β Group Construction β β
β β βββββββββββ βββββββββββ βββββββββββ β β
β β β Group 1 β β Group 2 β β Group 3 β β β
β β β Trees β βParticlesβ β Terrain β β β
β β βββββββββββ βββββββββββ βββββββββββ β β
β β βββββββββββ βββββββββββ β β
β β β Group 4 β β Group 5 β β β
β β β Room β β Ceiling β β β
β β βββββββββββ βββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ β
β β Raycaster β β Animation β β
β β Interactionsβ β Loop β β
β β Hover/Click β β rAF β β
β ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Assets load first via Promise.all. Only after everything is ready does the scene construction begin. This eliminates race conditions and ensures no texture or model is undefined when needed.
The scene is built on a 20Γ20 plane divided into quadrants. Each quadrant hosts a different experiment.
Position: (-5, 0.01, -5)
A 10Γ10 plane with 4 sub-groups of 5Γ5 each. Every sub-group has a distinct color β orange, purple, cyan, pink β and a cloned Tree.glb model at half scale.
This group explores scene graph hierarchy: how nested groups inherit transformations, how to clone models without duplicating geometry, and how to organize complex compositions cleanly.
Position: (5, 0.01, -5)
A large fog particle (size 12) using fog-5.webp as texture, surrounded by 40 petals orbiting an invisible center at radius 3. The petals update every frame by directly manipulating the position buffer.
This group taught me that particles in Three.js aren't magic β they're arrays of numbers you move manually. Understanding that changed everything.
Position: (-5, 0.01, 5)
The most technically demanding piece. A ShaderMaterial with a vertex shader implementing 15 octaves of 3D Simplex noise. Each octave has a different frequency and amplitude, creating natural-looking elevation that ranges from green grass to brown earth to white snow.
The fragment shader colors purely by height. No textures. No tricks. Just mathematics.
Elevation: max(0, simplex_noise Γ total_amplitude)
Color map: green (0β40%) β brown (40β60%) β white (60β100%)
Position: (5, 0.01, 5)
Two marble walls β pared-1 (back) and pared-2 (side) β with PBR material sets. Six luminous spheres of different colors move with sinusoidal functions, each casting light via PointLight at intensity 150.
At the center sits the 3D text "sebastian v." rendered with the Luckiest Guy font. Hover to turn it orange. Click to open an animated modal.
Position: (5, 10, 5)
An inverted plane completing the room from Group 4. Simple, but essential for spatial coherence.
1. Browser loads index.html
βββ GSAP loads from CDN
βββ Import map resolves Three.js
βββ main.js executes as ES module
2. Assets load via Promise.all()
βββ 9 PBR textures (wood + marble)
βββ 2 particle textures
βββ 1 GLB model
βββ 1 font (separate load)
3. Scene constructs after all assets are ready
βββ WebGL renderer with antialiasing
βββ Perspective camera (FOV 60)
βββ Orbital controls with damping
βββ Lighting: ambient + directional + 6 point lights
4. Five groups create their objects
5. Animation loop starts (requestAnimationFrame)
βββ Particles orbit
βββ Light spheres drift
βββ Controls update
βββ Scene renders
6. Interactions listen
βββ Raycaster detects text hover/click
βββ GSAP animates the modal
βββ Window resize updates camera
- A modern browser (Chrome, Firefox, Safari, Edge)
- A local server β
file://won't work due to ES modules
# Clone the repository
git clone https://github.com/sebastianvasquezechavarria1234/three.js-lab.git
# Navigate to the project
cd three.js-labChoose any of these options:
# Python
python -m http.server 8000
# Node.js (if http-server is installed)
npx http-server -p 8000
# Or use VS Code's Live Server extensionThen open http://localhost:8000 in your browser.
None. This is a fully static project with no backend, no APIs, and no secrets.
| Parameter | Value | Notes |
|---|---|---|
| Canvas resolution | Full window | Updates on resize |
| Max pixel ratio | 2 | Prevents HiDPI overload |
| Background color | Black (#000000) |
β |
| Camera position | (0, 10, 20) |
Looking at origin |
| Camera FOV | 60Β° | β |
| Near / Far planes | 0.1 / 1000 | β |
| OrbitControls damping | 0.05 | Smooth camera feel |
| Terrain segments | 128Γ128 | High detail |
| Noise octaves | 15 | Maximum complexity |
| Petal particles | 40 | β |
| Orbit radius | 3.0 | β |
| Point lights | 6 | Intensity 150, distance 25 |
| Modal animation | 0.4β0.6s | GSAP spring easing |
- Pixel ratio capping β
Math.min(devicePixelRatio, 2)prevents overrendering on high-DPI displays - Promise.all preloading β All textures load before scene construction, avoiding runtime hitches
- BufferGeometry for particles β Direct position array manipulation avoids object creation per frame
- Reasonable segment counts β 128Γ128 terrain is detailed without being excessive
- Single render call β No multi-pass rendering, no post-processing overhead
- The 15-octave Simplex noise shader is computationally expensive on low-end GPUs
- All 6 point lights render every frame without frustum culling optimization
- No LOD system β all geometry renders at full detail regardless of distance
- Particle positions update on the CPU, not GPU
- Reduce noise octaves on mobile devices
- Implement light culling or baking
- Move particle animation to a GPU compute shader
- Add frustum culling for off-screen objects
The scene adapts automatically to any window size:
- Camera aspect ratio updates on resize
- Renderer size matches the new viewport
- Canvas uses
position: fixedto fill the screen - Modal uses CSS transforms that scale relative to viewport
No media queries. No breakpoints. The 3D scene itself is inherently responsive β it just works at any size.
- Promise.all for asset loading β Everything loads before scene construction, eliminating race conditions
- Pixel ratio capping β Protects mobile GPUs from overrendering
- ES modules without bundler β Import maps load Three.js directly from CDN, zero build step
- Hierarchical scene graph β Nested groups manage relative transformations cleanly
- Mixed material strategy β
MeshStandardMaterialfor standard objects,ShaderMaterialfor procedural terrain - Manual buffer manipulation β Direct position array updates for efficient particle movement
- NDC-based raycasting β Normalized device coordinates ensure precise interaction detection
For a project of this scope, Webpack or Vite would add configuration overhead without meaningful benefit. ES modules with import maps load Three.js directly from CDN β faster to start, zero build time, and completely transparent.
GSAP excels at DOM animation with its spring easings and timeline control. Three.js handles all 3D animation internally. Mixing them would create unnecessary coupling.
More octaves = more detail. With 15 layers, the terrain has both large-scale mountain shapes and fine-grain surface detail. It's computationally expensive, but this is a learning project β clarity over optimization.
Using BufferGeometry with direct position updates gives full control over particle behavior. It's more verbose than a particle system library, but it teaches you exactly what's happening under the hood.
Why can't I open the project with file://?
ES modules require a server context. The browser blocks import statements from file:// for security reasons. Use any local server.
Why are some textures loaded but not visible?
The wood textures (normal, roughness, AO) are loaded and available but only the basecolor is currently applied to the base plane. This was intentional β they're ready for future enhancement.
Why is the terrain static?
The terrain shader generates elevation once. Adding animation would require updating the vertex buffer every frame, which is possible but wasn't the goal for this experiment.
Can I use this in my own project?
Yes. The code is open source. Study it, learn from it, adapt it. That's exactly why it exists.
- Dynamic shadows with
ShadowMap - Post-processing pipeline (bloom, SSAO)
- 3D positional audio
- More procedural shaders (water, fire, clouds)
- Animation mixer integration
- LOD (Level of Detail) for distant models
- GPU compute-based particle system
- Terrain animation (wind, erosion)
- Interactive light color picker
- Mobile touch controls optimization
Wood textures (
textures/plane/) are loaded but only the basecolor is applied. The other layers β normal, roughness, AO β are available for a more realistic finish.
Marble textures are applied differently per wall:
pared-1uses only basecolor, whilepared-2uses the full PBR package.
Terrain clamping uses
max(0, elevation)to prevent negative values that would create geometry below the plane.
Dual font loading β Luckiest Guy loads as
.ttffor CSS (modal) and.jsonfor Three.js (3D text).
GSAP scope β Only used for DOM animations (modal), never for 3D object transformations.
Light timings are randomly generated with
Math.random()when creating each sphere, ensuring no two bulbs move identically.
This is a personal learning project, but contributions are welcome:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
| Resource | Source |
|---|---|
| 3D Engine | Three.js |
| Animations | GSAP |
| Typography | Luckiest Guy |
| PBR Textures | ambientCG |
| Simplex Noise | Based on work by Stefan Gustavson & Ashima Arts |
| Hosting | Vercel |
This project is open source. Feel free to use it as a reference for your own experiments.
