Skip to content

Scenes3D/three.js-lab

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

31 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

✨🌳 Three.js Lab

A living laboratory for experimenting with 3D graphics in the browser

Made with Three.js GSAP No Build Tools

View Demo Portfolio


Three.js Lab Preview

Table of Contents


About

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.


Why This Project Exists

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.


Features

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

Tech Stack

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.


Project Structure

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


Architecture

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.

Core Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   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        β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

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: 5 Groups, 5 Challenges

The scene is built on a 20Γ—20 plane divided into quadrants. Each quadrant hosts a different experiment.

πŸ”΄ Group 1 β€” Hierarchical Trees

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.

🟒 Group 2 β€” Particles and Fog

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.

πŸ”΅ Group 3 β€” Procedural Terrain

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%)

🟑 Group 4 β€” Room with Lights and Text

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.

⬛ Group 5 β€” Ceiling

Position: (5, 10, 5)

An inverted plane completing the room from Group 4. Simple, but essential for spatial coherence.


How It Works

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

Getting Started

Prerequisites

  • A modern browser (Chrome, Firefox, Safari, Edge)
  • A local server β€” file:// won't work due to ES modules

Installation

# Clone the repository
git clone https://github.com/sebastianvasquezechavarria1234/three.js-lab.git

# Navigate to the project
cd three.js-lab

Running Locally

Choose 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 extension

Then open http://localhost:8000 in your browser.

Environment Variables

None. This is a fully static project with no backend, no APIs, and no secrets.


Configuration

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

Performance

What Was Done Right

  • 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

Known Limitations

  • 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

Possible Optimizations

  • 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

Responsive Design

The scene adapts automatically to any window size:

  • Camera aspect ratio updates on resize
  • Renderer size matches the new viewport
  • Canvas uses position: fixed to 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.


Best Practices

  • 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 β€” MeshStandardMaterial for standard objects, ShaderMaterial for procedural terrain
  • Manual buffer manipulation β€” Direct position array updates for efficient particle movement
  • NDC-based raycasting β€” Normalized device coordinates ensure precise interaction detection

Technical Decisions

Why no bundler?

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.

Why GSAP for modal only?

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.

Why 15 octaves of noise?

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.

Why manual particle animation?

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.


FAQ

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.


Future Improvements

  • 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

Developer Notes

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-1 uses only basecolor, while pared-2 uses 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 .ttf for CSS (modal) and .json for 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.


Contributing

This is a personal learning project, but contributions are welcome:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Credits

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

License

This project is open source. Feel free to use it as a reference for your own experiments.


Some projects aren't built to solve a problem. They're built to ask questions.

And sometimes, on the way to finding answers, you end up creating something you didn't even know could exist.

This project is that.


SebastiÑn V ❀️

Portfolio Β· GitHub

About

✨🌳 A laboratory where you will explore everything that can be achieved with Three.js without relying on complex build tools. Just modern HTML, JavaScript, and a great deal of curiosity about how things work under the hood.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • JavaScript 87.9%
  • CSS 6.5%
  • HTML 5.6%