Creating a Winter Olympics 3D Scene with Three.js and React
This article demonstrates how to build an interactive 3D Winter Olympics themed webpage using Three.js and React, covering resource import, scene setup, lighting, model loading, custom materials, particle effects, camera controls, and animation techniques.
Background
The article introduces a festive 3D page for the 2022 Winter Olympics, built with Three.js + React . It showcases the use of various Three.js components such as TorusGeometry , MeshLambertMaterial , MeshDepthMaterial , custom materials, Points , and PointsMaterial to create rings, the mascot Bing Dwen Dwen, flags, trees, and snowfall.
Effect
The final effect includes the Olympic mascot, the five rings, waving flags, trees, and falling snow. Users can hold the left mouse button to rotate the camera and view the scene from different angles.
Implementation
Importing 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';
// ...Page DOM Structure
<div>
<div id="container"></div>
{this.state.loadingProcess === 100 ? '' : (
<div className="olympic_loading">
<div className="box">{this.state.loadingProcess} %</div>
</div>
)}
</div>Scene Initialization
container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
scene = new THREE.Scene();
scene.background = new THREE.TextureLoader().load(skyTexture);
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));Adding Lights
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);
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;
scene.add(ambientLight);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: Math.floor(loaded / total * 100) });
// camera tween animation
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) });
}
};Creating the Ground
var loader = new THREE.GLTFLoader(manager);
loader.load(landModel, function (mesh) {
mesh.scene.traverse(function (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);
land = mesh.scene;
scene.add(land);
});Creating the Mascot Bing Dwen Dwen
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);
});Creating the 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 => {
let 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);
meshes.push(cycleMesh);
fiveCyclesGroup.add(cycleMesh);
});
fiveCyclesGroup.scale.set(.036, .036, .036);
fiveCyclesGroup.position.set(0, 10, -8);
scene.add(fiveCyclesGroup);Creating the 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);
// animation
let meshAnimation = mesh.animations[0];
mixer = new THREE.AnimationMixer(mesh.scene);
let animationClip = meshAnimation;
let clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
scene.add(mesh.scene);
});Creating Trees
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: 0.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);
// clone two more trees
let tree2 = mesh.scene.clone();
tree2.position.set(10, -8, -15);
tree2.scale.set(18, 18, 18);
scene.add(tree2);
});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();
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);Camera Controls, Resize Adaptation, and Animation
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 = -.6;
controls.maxAzimuthAngle = .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();
// flag animation
mixer && mixer.update(new THREE.Clock().getDelta());
// camera tween
TWEEN && TWEEN.update();
// rings rotation
fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
// update particles
points.geometry.verticesNeedUpdate = true;
// snow animation
let vertices = points.geometry.vertices;
vertices.forEach(function (v) {
v.y = v.y - v.velocityY;
v.x = v.x - v.velocityX;
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
});
}Summary
The tutorial covers the complete workflow of building an interactive 3D Winter Olympics webpage, from setting up the React component and loading external assets, to configuring the Three.js scene, lights, custom shaders, particle systems, and user interaction. It provides reusable code snippets and explains key concepts such as TorusGeometry , MeshLambertMaterial , MeshDepthMaterial , custom materials, Points particles, and camera controls, making it a valuable reference for frontend developers interested in WebGL and 3D graphics.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.