Frontend Development 19 min read

Building a Winter Olympics 3D Web Page with Three.js and React

This article demonstrates how to build an interactive Winter Olympics-themed 3D web page using Three.js and React, covering model loading, custom materials, lighting, particle effects, and camera controls, with detailed code snippets and explanations for each component.

IT Xianyu
IT Xianyu
IT Xianyu
Building a Winter Olympics 3D Web Page with Three.js and React

Background: The article introduces a project to create a winter Olympic-themed 3D webpage using the Three.js and React technology stack. It aims to showcase the mascot Bing Dwen Dwen, Olympic rings, flags, trees, and snow effects.

Implementation Steps:

1. Import Resources

import React from 'react';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import bingdundunModel from './models/bingdundun.glb';
// ...

2. Page DOM Structure

<div id="container"></div>
{ this.state.loadingProcess === 100 ? '' : (
  <div className="olympic_loading">
    <div className="box">{this.state.loadingProcess}%</div>
  </div>
)}

3. Scene Initialization

const container = document.getElementById('container');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.TextureLoader().load(skyTexture);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));

4. Adding Lights

// Directional Light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(16, 16, 8);
light.castShadow = true;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 40;
light.shadow.camera.bottom = -40;
light.shadow.camera.left = -40;
light.shadow.camera.right = 40;
scene.add(light);
// Ambient Light
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;
scene.add(ambientLight);

5. Loading Progress Management

const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {};
manager.onLoad = () => { console.log('Loading complete!'); };
manager.onProgress = (url, loaded, total) => {
  if (Math.floor(loaded / total * 100) === 100) {
    this.setState({ loadingProcess: 100 });
    Animations.animateCamera(camera, controls, { x: 0, y: -1, z: 20 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
  } else {
    this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
  }
};

6. Creating Ground

const loader = new THREE.GLTFLoader(manager);
loader.load(landModel, (mesh) => {
  mesh.scene.traverse((child) => {
    if (child.isMesh) {
      child.material.metalness = .1;
      child.material.roughness = .8;
      if (child.name === 'Mesh_2') {
        child.material.metalness = .5;
        child.receiveShadow = true;
      }
    }
  });
  mesh.scene.rotation.y = Math.PI / 4;
  mesh.scene.position.set(15, -20, 0);
  mesh.scene.scale.set(.9, .9, .9);
  scene.add(mesh.scene);
});

7. Adding the Bing Dwen Dwen Mascot

loader.load(bingdundunModel, (mesh) => {
  mesh.scene.traverse((child) => {
    if (child.isMesh) {
      if (child.name === 'oldtiger001') {
        child.material.metalness = .5;
        child.material.roughness = .8;
      }
      if (child.name === 'oldtiger002') {
        child.material.transparent = true;
        child.material.opacity = .5;
        child.material.metalness = .2;
        child.material.roughness = 0;
        child.material.refractionRatio = 1;
        child.castShadow = true;
      }
    }
  });
  mesh.scene.rotation.y = Math.PI / 24;
  mesh.scene.position.set(-8, -12, 0);
  mesh.scene.scale.set(24, 24, 24);
  scene.add(mesh.scene);
});

8. Creating Olympic Rings

const fiveCycles = [
  { key: 'cycle_0', color: 0x0885c2, position: { x: -250, y: 0, z: 0 } },
  { key: 'cycle_1', color: 0x000000, position: { x: -10, y: 0, z: 5 } },
  { key: 'cycle_2', color: 0xed334e, position: { x: 230, y: 0, z: 0 } },
  { key: 'cycle_3', color: 0xfbb132, position: { x: -125, y: -100, z: -5 } },
  { key: 'cycle_4', color: 0x1c8b3c, position: { x: 115, y: -100, z: 10 } }
];
fiveCycles.map(item => {
  const cycleMesh = new THREE.Mesh(
    new THREE.TorusGeometry(100, 10, 10, 50),
    new THREE.MeshLambertMaterial({ color: new THREE.Color(item.color), side: THREE.DoubleSide })
  );
  cycleMesh.castShadow = true;
  cycleMesh.position.set(item.position.x, item.position.y, item.position.z);
  fiveCyclesGroup.add(cycleMesh);
});
scene.add(fiveCyclesGroup);

9. Adding Flag

loader.load(flagModel, (mesh) => {
  mesh.scene.traverse((child) => {
    if (child.isMesh) {
      child.castShadow = true;
      if (child.name === 'mesh_0001') {
        child.material.metalness = .1;
        child.material.roughness = .1;
        child.material.map = new THREE.TextureLoader().load(flagTexture);
      }
      if (child.name === '柱体') {
        child.material.metalness = .6;
        child.material.roughness = 0;
        child.material.refractionRatio = 1;
        child.material.color = new THREE.Color(0xeeeeee);
      }
    }
  });
  mesh.scene.rotation.y = Math.PI / 24;
  mesh.scene.position.set(2, -7, -1);
  mesh.scene.scale.set(4, 4, 4);
  const meshAnimation = mesh.animations[0];
  const mixer = new THREE.AnimationMixer(mesh.scene);
  const clipAction = mixer.clipAction(meshAnimation).play();
  scene.add(mesh.scene);
});

10. Adding Trees with Custom Materials

let treeMaterial = new THREE.MeshPhysicalMaterial({
  map: new THREE.TextureLoader().load(treeTexture),
  transparent: true,
  side: THREE.DoubleSide,
  metalness: .2,
  roughness: .8,
  depthTest: true,
  depthWrite: false,
  skinning: false,
  fog: false,
  reflectivity: .1,
  refractionRatio: 0
});
let treeCustomDepthMaterial = new THREE.MeshDepthMaterial({
  depthPacking: THREE.RGBADepthPacking,
  map: new THREE.TextureLoader().load(treeTexture),
  alphaTest: 0.5
});
loader.load(treeModel, (mesh) => {
  mesh.scene.traverse((child) => {
    if (child.isMesh) {
      child.material = treeMaterial;
      child.custromMaterial = treeCustomDepthMaterial;
    }
  });
  mesh.scene.position.set(14, -9, 0);
  mesh.scene.scale.set(16, 16, 16);
  scene.add(mesh.scene);
  const tree2 = mesh.scene.clone();
  tree2.position.set(10, -8, -15);
  tree2.scale.set(18, 18, 18);
  scene.add(tree2);
});

11. Creating Snow Particles

let texture = new THREE.TextureLoader().load(snowTexture);
let geometry = new THREE.Geometry();
let range = 100;
let pointsMaterial = new THREE.PointsMaterial({
  size: 1,
  transparent: true,
  opacity: 0.8,
  map: texture,
  blending: THREE.AdditiveBlending,
  sizeAttenuation: true,
  depthTest: false
});
for (let i = 0; i < 1500; i++) {
  let vertice = new THREE.Vector3(
    Math.random() * range - range / 2,
    Math.random() * range * 1.5,
    Math.random() * range - range / 2
  );
  vertice.velocityY = 0.1 + Math.random() / 3;
  vertice.velocityX = (Math.random() - 0.5) / 3;
  geometry.vertices.push(vertice);
}
geometry.center();
const points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);

12. Camera Controls, Resize Handling, and Animation Loop

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enablePan = false;
controls.enableZoom = false;
controls.minPolarAngle = 1.4;
controls.maxPolarAngle = 1.8;
controls.minAzimuthAngle = -0.6;
controls.maxAzimuthAngle = 0.6;
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  controls && controls.update();
  mixer && mixer.update(new THREE.Clock().getDelta());
  TWEEN && TWEEN.update();
  fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
  points.geometry.verticesNeedUpdate = true;
  points.geometry.vertices.forEach(v => {
    v.y -= v.velocityY;
    v.x -= v.velocityX;
    if (v.y <= 0) v.y = 60;
    if (v.x <= -20 || v.x >= 20) v.velocityX *= -1;
  });
}
animate();

Conclusion: The tutorial covers key Three.js concepts such as TorusGeometry, MeshLambertMaterial, MeshDepthMaterial, custom materials, particle systems with Points, material blending, size attenuation, and vector usage, providing a complete example that can be further optimized with more interactivity and animations.

reactThree.jsWebGLWinter Olympics3D graphics
IT Xianyu
Written by

IT Xianyu

We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.