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.
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.
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.
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.