Learn Creative Coding (#62) - Three.js: Your First 3D Scene

in StemSocial21 hours ago

Learn Creative Coding (#62) - Three.js: Your First 3D Scene

cc-banner

Everything we've done so far has been flat. Sixty-one episodes of pixels on a 2D plane. Canvas rectangles, particle systems, shader fragments, cellular grids, boids swimming across a flat surface. And it's been great -- we built some genuinely beautiful stuff. But the world has depth. Objects sit in front of other objects. Light hits surfaces at angles and casts shadows. Geometry has volume.

So we're stepping off the plane. Starting today, we're working in three dimensions.

The tool for the job is Three.js. It's the dominant 3D library for the web and has been for over a decade. It wraps WebGL (which is powerful but brutal to use directly -- like writing assembly when you want to write Python) into a scene graph that makes sense: scenes, cameras, meshes, lights, materials. You describe WHAT you want in 3D space and Three.js figures out HOW to render it with the GPU.

If you've done any of the shader episodes (ep021, ep032-045), you already have a mental model for how GPUs think about rendering. Three.js just gives you a higher-level API on top of that same pipeline. And the trig from episode 13? That's about to become very useful again -- except now it's in three dimensions instead of two :-)

Setting up

Three.js is a JavaScript library. You can use it with a bundler (Vite, Webpack) or load it from a CDN. For quick creative coding experiments, the CDN approach is simplest:

<!DOCTYPE html>
<html>
<head>
  <style>
    body { margin: 0; overflow: hidden; background: #000; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/[email protected]/build/three.module.js",
      "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
    }
  }
  </script>
  <script type="module" src="sketch.js"></script>
</body>
</html>

The import map lets you write clean import statements without a bundler. The type="module" on the script tag enables ES module syntax. This is the setup I use for all the Three.js sketches in this series -- one HTML file that never changes, and a sketch.js that holds the actual code.

The CSS is important: margin: 0 removes the default body margin that would leave a gap around the canvas. overflow: hidden prevents scrollbars. The canvas fills the entire browser window.

The three pillars: Scene, Camera, Renderer

Every Three.js program needs exactly three things to show anything on screen:

import * as THREE from 'three';

// 1. the scene: a container for everything
const scene = new THREE.Scene();

// 2. the camera: your viewpoint into the scene
const camera = new THREE.PerspectiveCamera(
  75,                             // field of view (degrees)
  window.innerWidth / window.innerHeight,  // aspect ratio
  0.1,                            // near clipping plane
  1000                            // far clipping plane
);

// 3. the renderer: turns the scene into pixels
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

The Scene is just a container. You add objects to it. The Camera defines where you're looking from and how the 3D world projects onto the 2D screen. The Renderer takes the scene and camera and actually draws everything into a <canvas> element.

PerspectiveCamera is the one you'll use 99% of the time. It mimics how human eyes see -- things farther away look smaller. The four parameters:

  • Field of view (FOV): how wide the camera sees, in degrees. 75 is a natural-ish default. Lower values (30-50) give a telephoto/compressed look. Higher values (90-120) give a wide-angle/fisheye feel
  • Aspect ratio: width divided by height. Match this to your canvas or things will look squished
  • Near plane: anything closer than this gets clipped. 0.1 is fine for most cases
  • Far plane: anything farther than this gets clipped. 1000 is plenty for most scenes

One gotcha: the camera starts at position (0, 0, 0) -- the center of the world. If you put an object at the origin too, the camera will be INSIDE the object and you'll see nothing. You need to move the camera back:

camera.position.z = 5;

Now the camera is 5 units back on the Z axis, looking toward the origin. Objects at (0,0,0) will be visible.

Your first mesh: a spinning cube

A mesh in Three.js is geometry (shape) + material (surface appearance). The simplest possible 3D object:

// geometry: the shape
const geometry = new THREE.BoxGeometry(1, 1, 1);

// material: the surface
const material = new THREE.MeshStandardMaterial({
  color: 0x4488cc,
  roughness: 0.4,
  metalness: 0.3
});

// mesh: geometry + material combined into a scene object
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

BoxGeometry(1, 1, 1) creates a unit cube -- 1 unit wide, 1 unit tall, 1 unit deep. MeshStandardMaterial is a physically-based rendering (PBR) material that responds to lighting. The color is a hex value (same as CSS but as a number, not a string). Roughness controls how shiny the surface is (0 = mirror, 1 = matte). Metalness controls whether it looks like metal or plastic.

If you render this right now, you'll see... a black rectangle. Why? Because MeshStandardMaterial needs light to be visible. Without any lights in the scene, the material renders as pure black. Makes sense when you think about it -- in a completely dark room, nothing is visible regardless of its color.

Lighting: making things visible

Two lights get you surprisingly far. An ambient light (uniform illumination from all directions) plus a directional light (parallel rays from a specific direction, like sunlight):

// ambient: soft fill light so nothing is completely black
const ambient = new THREE.AmbientLight(0x404040, 1.5);
scene.add(ambient);

// directional: main light source with direction
const dirLight = new THREE.DirectionalLight(0xffffff, 2.0);
dirLight.position.set(3, 5, 4);
scene.add(dirLight);

The ambient light color is a dim gray -- not white, because you want the directional light to create visible contrast between lit and unlit sides. If the ambient is too bright, everything looks flat. The directional light position at (3, 5, 4) means light comes from the upper-right-front. The position determines the direction of the rays (they point from that position toward the origin).

Now when you render, the cube appears! The face pointing toward the directional light is bright. The opposite face is dimmer (lit only by ambient). The transition between lit and unlit faces creates the visual cue that tells your brain "this is a 3D object, not a flat shape."

The render loop

Just like our 2D canvas sketches used requestAnimationFrame for animation, Three.js needs a render loop:

function animate() {
  requestAnimationFrame(animate);

  // rotate the cube
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.015;

  renderer.render(scene, camera);
}

animate();

Every frame: update the scene (rotate the cube a tiny bit), then render. The rotation values are in radians. 0.01 radians per frame at 60fps means about 0.6 radians per second -- a slow, gentle spin. The cube rotates around both X and Y axes simultaneously, which gives that classic "tumbling in space" look.

This is your complete first Three.js sketch. Scene, camera, renderer, one cube, two lights, animation loop. About 30 lines of actual code. Compare that to doing the same thing in raw WebGL (hundreds of lines of shader compilation, buffer management, matrix math) and you see why Three.js exists.

OrbitControls: exploring your scene

The cube spins but the camera is locked in place. For creative coding, you almost always want to be able to rotate around, zoom in, pan -- explore the scene with mouse and trackpad. Three.js provides OrbitControls for exactly this:

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

function animate() {
  requestAnimationFrame(animate);

  cube.rotation.x += 0.01;
  cube.rotation.y += 0.015;

  controls.update();  // must call this when damping is enabled
  renderer.render(scene, camera);
}

Left-click drag to orbit, right-click drag to pan, scroll to zoom. enableDamping adds inertia -- the view keeps sliding a bit after you release the mouse, which feels much more natural than abrupt stops. You need to call controls.update() in the animation loop for damping to work.

Orbit controls are not just convenient -- they're essential for understanding your 3D scenes. When something looks wrong (geometry clipping, lighting oddities, objects in the wrong position), being able to freely orbit around and inspect from all angles is how you debug 3D.

Geometry primitives

Three.js comes with a bunch of built-in geometries. Here are the ones you'll use most for creative coding:

// sphere
const sphere = new THREE.Mesh(
  new THREE.SphereGeometry(0.8, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0xcc6644 })
);
sphere.position.x = -3;
scene.add(sphere);

// cylinder
const cylinder = new THREE.Mesh(
  new THREE.CylinderGeometry(0.5, 0.5, 1.5, 32),
  new THREE.MeshStandardMaterial({ color: 0x44cc66 })
);
cylinder.position.x = -1;
scene.add(cylinder);

// torus (donut)
const torus = new THREE.Mesh(
  new THREE.TorusGeometry(0.7, 0.2, 16, 48),
  new THREE.MeshStandardMaterial({ color: 0x6644cc })
);
torus.position.x = 1;
scene.add(torus);

// torus knot (the interesting one)
const knot = new THREE.Mesh(
  new THREE.TorusKnotGeometry(0.5, 0.15, 100, 16),
  new THREE.MeshStandardMaterial({ color: 0xcc4466 })
);
knot.position.x = 3;
scene.add(knot);

Each geometry's constructor takes different arguments. SphereGeometry(radius, widthSegments, heightSegments) -- more segments means smoother. 32 is plenty smooth, 8 would give a visibly faceted look (which can be a deliberate aesthetic). TorusGeometry(radius, tube, radialSegments, tubularSegments) -- the donut shape. TorusKnotGeometry is a mathematical knot that looks amazing and is wildly popular in Three.js demos.

Position objects with .position.set(x, y, z) or by setting .position.x, .position.y, .position.z individually. Scale with .scale.set(sx, sy, sz). Rotate with .rotation.set(rx, ry, rz) (radians).

Materials: beyond the basics

We've been using MeshStandardMaterial but there are several others, each with diferent use cases:

// MeshBasicMaterial: ignores lighting, always fully visible
// good for wireframes, debug visualization, or unlit flat colors
const basic = new THREE.MeshBasicMaterial({
  color: 0xff8800,
  wireframe: true
});

// MeshNormalMaterial: colors based on surface direction
// fantastic for debugging geometry -- no light needed
const normal = new THREE.MeshNormalMaterial();

// MeshPhongMaterial: classic Phong shading (older, lighter)
const phong = new THREE.MeshPhongMaterial({
  color: 0x2266aa,
  shininess: 100,
  specular: 0x444444
});

// MeshStandardMaterial: PBR (physically based rendering)
// this is the default choice for anything that should look "real"
const standard = new THREE.MeshStandardMaterial({
  color: 0x88aa44,
  roughness: 0.3,
  metalness: 0.7
});

MeshBasicMaterial is underrated for creative coding. Since it ignores lighting entirely, you can use it for glowing, self-illuminated objects. Set wireframe: true and you get the skeletal wireframe view that's iconic in creative coding aesthetics. No need to set up lights at all.

MeshNormalMaterial maps each face's direction to a color (x=red, y=green, z=blue, like a normal map). It's mesmerizing on complex geometry -- a torus knot in normal material has this rainbow-oil-slick quality. And since it doesn't need lights, it's perfect for quick tests.

Material properties you'll reach for a lot:

material.transparent = true;
material.opacity = 0.5;       // see-through objects
material.side = THREE.DoubleSide;  // render both sides (needed for planes)
material.wireframe = true;    // skeletal view
material.flatShading = true;  // faceted look instead of smooth

flatShading is a creative coding favorite. It makes smooth geometry look faceted and geometric -- a sphere becomes an icosphere, a cylinder becomes a prism. Low-poly aesthetic with one boolean flag.

Animation: making things move

In the render loop, you can modify any property of any object. Position, rotation, scale, material properties -- anything. The trig from episode 13 is your best friend here:

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const t = clock.getElapsedTime();

  // rotate
  cube.rotation.x = t * 0.5;
  cube.rotation.y = t * 0.3;

  // bob up and down (sine wave on Y position)
  cube.position.y = Math.sin(t * 2) * 0.5;

  // pulse scale
  const s = 1 + Math.sin(t * 3) * 0.15;
  cube.scale.set(s, s, s);

  // change color over time
  const hue = (t * 0.1) % 1;
  cube.material.color.setHSL(hue, 0.7, 0.5);

  controls.update();
  renderer.render(scene, camera);
}

THREE.Clock gives you elapsed time in seconds -- cleaner than tracking frame counts manually. Using clock.getElapsedTime() instead of incrementing a counter means your animations run at the same speed regardless of frame rate. A cube at rotation.y = t * 0.3 rotates at 0.3 radians per second whether you're at 30fps or 144fps.

The setHSL method on colors is amazing for creative coding. Cycle the hue from 0 to 1 and you sweep through the entire rainbow. Way more useful than trying to animate RGB values directly. Same concept as the color wheel from episode 7, but now you can apply it to 3D materials.

A simple solar system

Time for something more interesting. Lets put our trig to work and build a mini solar system -- a central sphere with smaller spheres orbiting around it. This is episode 13's cos/sin orbital motion, but now in 3D space:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a14);

const camera = new THREE.PerspectiveCamera(
  60, window.innerWidth / window.innerHeight, 0.1, 100
);
camera.position.set(0, 8, 12);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// lights
scene.add(new THREE.AmbientLight(0x222233, 1.0));
const sunLight = new THREE.PointLight(0xffffcc, 3, 50);
scene.add(sunLight);  // light at the origin (inside the sun)

// sun
const sunGeo = new THREE.SphereGeometry(1.2, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffcc44 });
const sun = new THREE.Mesh(sunGeo, sunMat);
scene.add(sun);

// planets
const planets = [];
const colors = [0x4488cc, 0xcc6644, 0x44cc88, 0xaa44cc, 0xccaa44];

for (let i = 0; i < 5; i++) {
  const radius = 0.2 + Math.random() * 0.3;
  const orbitRadius = 3 + i * 1.8;
  const speed = 0.3 + Math.random() * 0.5;
  const tilt = (Math.random() - 0.5) * 0.4;  // orbit isn't perfectly flat

  const geo = new THREE.SphereGeometry(radius, 24, 24);
  const mat = new THREE.MeshStandardMaterial({
    color: colors[i],
    roughness: 0.6,
    metalness: 0.2
  });
  const mesh = new THREE.Mesh(geo, mat);
  scene.add(mesh);

  planets.push({ mesh, orbitRadius, speed, tilt, phase: Math.random() * Math.PI * 2 });
}

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const t = clock.getElapsedTime();

  // animate planets
  for (const p of planets) {
    const angle = t * p.speed + p.phase;
    p.mesh.position.x = Math.cos(angle) * p.orbitRadius;
    p.mesh.position.z = Math.sin(angle) * p.orbitRadius;
    p.mesh.position.y = Math.sin(angle * 2) * p.tilt;

    // spin on own axis
    p.mesh.rotation.y = t * 1.5;
  }

  // sun glow pulse
  const pulse = 1 + Math.sin(t * 2) * 0.05;
  sun.scale.set(pulse, pulse, pulse);

  controls.update();
  renderer.render(scene, camera);
}

animate();

See where trig comes back? Math.cos(angle) * radius and Math.sin(angle) * radius give circular motion in the XZ plane (the horizontal "floor" in Three.js). Adding a small Y offset with Math.sin(angle * 2) * tilt gives each orbit a slight vertical wobble, so they don't all sit perfectly flat. Each planet has a random phase offset so they don't start aligned.

The sun uses MeshBasicMaterial instead of MeshStandardMaterial -- it ignores lighting and renders as a solid bright color. This makes it look like it's glowing, because it's always the same brightness regardless of lights. We put a PointLight at the same position so the sun actually illuminates the planets around it.

Use orbit controls to fly around this. Zoom way out and you see the whole system. Zoom in on one planet and watch it spin. Tilt down and see the orbital paths from above. This is the power of 3D -- the same data (positions, rotations) creates completely different visual experiences depending on where you look from.

Responsive canvas

One thing you absolutely need: resize handling. When the browser window changes size, the canvas should adapt:

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Three lines. Update the camera's aspect ratio, tell it to recalculate its projection matrix, and resize the renderer. Without this, resizing the window distorts the image -- objects stretch horizontally or vertically because the aspect ratio is wrong.

Call camera.updateProjectionMatrix() whenever you change any camera parameter (FOV, aspect, near, far). It recomputes the internal matrix that transforms 3D coordinates to 2D screen coordinates. Forget this and your changes won't take effect.

Creative exercise: a generative 3D scene

Alright, time to actually make something. Lets build a field of randomly positioned and colored geometric shapes -- your first generative 3D composition:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0d1a);
scene.fog = new THREE.Fog(0x0d0d1a, 8, 25);

const camera = new THREE.PerspectiveCamera(
  65, window.innerWidth / window.innerHeight, 0.1, 50
);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// lighting
scene.add(new THREE.AmbientLight(0x333344, 1.2));
const light1 = new THREE.DirectionalLight(0xeeddcc, 1.8);
light1.position.set(5, 8, 3);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0x4466aa, 0.8);
light2.position.set(-4, 3, -5);
scene.add(light2);

Two directional lights from different angles with different colors. The main light is warm white, the fill light is cool blue. This two-tone lighting creates visual depth -- warm highlights, cool shadows. Film cinematography uses this technique all the time.

The fog is a nice touch. Fog(color, near, far) makes objects fade to the background color as they get farther from the camera. Distant shapes dissolve into the dark background. Gives the scene depth and atmosphere without any extra effort.

Now create the shapes:

const geometries = [
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.SphereGeometry(0.6, 24, 24),
  new THREE.OctahedronGeometry(0.6),
  new THREE.TorusGeometry(0.5, 0.15, 12, 32),
  new THREE.ConeGeometry(0.5, 1, 8),
  new THREE.TetrahedronGeometry(0.6)
];

const objects = [];

for (let i = 0; i < 60; i++) {
  const geo = geometries[Math.floor(Math.random() * geometries.length)];

  const hue = Math.random();
  const sat = 0.4 + Math.random() * 0.4;
  const light = 0.3 + Math.random() * 0.3;

  const mat = new THREE.MeshStandardMaterial({
    roughness: 0.3 + Math.random() * 0.5,
    metalness: Math.random() * 0.6,
    flatShading: Math.random() > 0.5
  });
  mat.color.setHSL(hue, sat, light);

  const mesh = new THREE.Mesh(geo, mat);

  // random position in a spherical region
  const r = 3 + Math.random() * 8;
  const theta = Math.random() * Math.PI * 2;
  const phi = (Math.random() - 0.5) * Math.PI * 0.6;

  mesh.position.x = r * Math.cos(phi) * Math.cos(theta);
  mesh.position.y = r * Math.sin(phi) + (Math.random() - 0.5) * 2;
  mesh.position.z = r * Math.cos(phi) * Math.sin(theta);

  // random rotation
  mesh.rotation.x = Math.random() * Math.PI * 2;
  mesh.rotation.y = Math.random() * Math.PI * 2;
  mesh.rotation.z = Math.random() * Math.PI * 2;

  // random scale
  const s = 0.5 + Math.random() * 1.0;
  mesh.scale.set(s, s, s);

  scene.add(mesh);
  objects.push({
    mesh,
    rotSpeed: {
      x: (Math.random() - 0.5) * 0.02,
      y: (Math.random() - 0.5) * 0.02,
      z: (Math.random() - 0.5) * 0.02
    },
    floatSpeed: 0.5 + Math.random() * 1.5,
    floatAmp: 0.1 + Math.random() * 0.3,
    baseY: mesh.position.y
  });
}

Sixty objects scattered in a roughly spherical arrangement. The spherical coordinate system (r, theta, phi) distributes them in 3D space more evenly than random XYZ would -- pure random XYZ tends to cluster objects near the corners of a box. Spherical coordinates give a natural, organic distribution.

Each object gets a random geometry from the pool, a random HSL color, random roughness and metalness, and a 50/50 chance of flat shading. Some will be shiny smooth spheres, others will be matte faceted octahedra. The variety makes the scene visually rich without any two objects looking exactly the same.

Now animate everything:

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const t = clock.getElapsedTime();

  for (const obj of objects) {
    // slow rotation
    obj.mesh.rotation.x += obj.rotSpeed.x;
    obj.mesh.rotation.y += obj.rotSpeed.y;
    obj.mesh.rotation.z += obj.rotSpeed.z;

    // gentle floating motion
    obj.mesh.position.y = obj.baseY + Math.sin(t * obj.floatSpeed) * obj.floatAmp;
  }

  controls.update();
  renderer.render(scene, camera);
}

animate();

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Each object rotates at its own rate and floats up and down gently on a sine wave. The combination of different rotation speeds and float frequencies means no two objects move in sync -- the whole scene has this dreamy, drifting quality. Orbit around it and you discover new compositions at every angle. The fog fades distant objects, creating natural layering. The two-tone lighting creates warm and cool areas across each surface.

This is your 3D creative coding playground. Fifty lines of setup, sixty randomized objects, a simple animation loop. Already more visually interesting than many gallery installations I've seen.

How Three.js connects to what we know

If you've been following along since episode 1, you'll notice that everything transfers over. The concepts are the same -- we're just adding a dimension.

Positions are now (x, y, z) instead of (x, y). Rotation has three axes instead of one. Colors work exactly the same way. Animation loops work exactly the same way. Trig functions from episode 13 do the same job but now you use spherical coordinates instead of polar. The requestAnimationFrame pattern is identical to what we've been doing since episode 3.

The biggest mental shift is that you don't control every pixel anymore. In 2D canvas, you decide exactly where every shape goes and exactly what color every pixel is. In 3D, you place objects in space and the renderer figures out what the camera sees. Lighting, perspective, occlusion -- all handled for you. You think in terms of objects and properties, not pixels. It's a higher level of abstraction.

For creative coding, this abstraction is a gift. You can focus on composition, motion, color, and form without worrying about the rendering pipeline. You describe the scene and Three.js makes it beautiful. The GPU does the heavy lifting.

Next time we'll dig into procedural geometry -- creating meshes from code instead of using built-in primitives. Custom shapes, terrain, organic forms. The real creative power of 3D comes from building geometry that doesn't exist in any library.

't Komt erop neer...

  • Three.js wraps WebGL into a scene graph with three core components: Scene (container for objects), Camera (your viewpoint), and Renderer (turns 3D into pixels). About 30 lines to get a spinning cube on screen versus hundreds in raw WebGL
  • PerspectiveCamera takes four parameters: field of view (degrees), aspect ratio, near clipping plane, and far clipping plane. Camera starts at the origin so you need to move it back (e.g. camera.position.z = 5) to see objects at the center
  • Meshes combine Geometry (shape) and Material (surface appearance). Built-in geometries include Box, Sphere, Cylinder, Torus, TorusKnot, Octahedron, Cone, Tetrahedron. MeshStandardMaterial is the default PBR choice, MeshBasicMaterial ignores lighting (good for glowing objects)
  • Lighting is required for MeshStandardMaterial -- without lights, everything renders black. An AmbientLight (uniform fill) plus a DirectionalLight (parallel rays like sunlight) is the simplest setup that looks good
  • OrbitControls from three/addons lets you orbit, pan, and zoom with mouse. Enable damping for smooth inertial movement. Essential for exploring and debugging 3D scenes
  • Animation uses the same requestAnimationFrame loop as 2D canvas. THREE.Clock gives elapsed time in seconds for frame-rate-independent animation. Trig functions (cos, sin) work the same way as episode 13 but now in 3D space -- cos(angle) * radius and sin(angle) * radius create orbital motion
  • Materials have useful creative properties: transparent + opacity for see-through objects, wireframe for skeletal views, flatShading for a faceted low-poly aesthetic, side: THREE.DoubleSide for rendering both faces
  • Fog (scene.fog = new THREE.Fog(color, near, far)) makes distant objects fade to the background color, adding depth and atmosphere. Two-tone directional lighting (warm main + cool fill) creates cinematic visual depth on 3D surfaces

Sallukes! Thanks for reading.

X

@femdev

Sort:  

Congratulations @femdev! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You received more than 2000 upvotes.
Your next target is to reach 2250 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP