Three.js to realize the 2022 Winter Olympics Theme 3D interesting page 🐼


Welcome the Winter Olympics and look forward to the future together! 2022 Winter Olympics will begin soon. This paper uses three JS + react technology stack realizes winter and Olympic elements, and makes a 3D page full of interesting and commemorative Winter Olympic theme. The knowledge Points involved in this paper mainly include TorusGeometry torus, MeshLambertMaterial matte surface material, MeshDepthMaterial depth mesh material, custromMaterial custom material, Points particles, PointsMaterial point material, etc.


The effect is as follows: 👇 Bing dwen dwen shows that the page is mainly composed of 2022 Winter Olympic mascots, ice pier, Olympic rings and dancing flags. 🚩, Trees 🌲 And snow effect ❄️ Etc. Hold down the left mouse button and move to change the camera position to obtain different views.

👀 Online preview: (deployed in GitHub, the loading speed may be a little slow 😓)


Introduce resources

Firstly, the library and external resources required for developing pages are introduced. OrbitControls is used for lens track control, TWEEN is used for gap animation implementation, GLTFLoader is used to load 3D models in glb or gltf format, as well as some other models, maps and other 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

The DOM structure of the page is very simple, with only the #container for rendering 3D elements and the container for displaying the loading progress olympic_loading element.

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

Scene initialization

Initialize render container, scene, camera. For the detailed knowledge of this part, you can refer to my previous articles, which will not be repeated in this article.

container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
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));

Add light

In this example, two kinds of light sources are mainly added: DirectionalLight is used to generate shadows, adjust the brightness of the page, and AmbientLight is used to render the environment atmosphere.

// Direct 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; = 40; = -40; = -40; = 40;
// Ambient light
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;

Loading schedule management

Use three The loading manager manages the loading progress of the page model and executes some methods related to the loading progress in its callback function. The page loading progress in this example is completed in onProgress. When the page loading progress is 100%, the TWEEN shot gap animation is executed.

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) });
    // Shot gap 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) });

Create ground

The bumpy ground in this example is created by using Blender to build the model, and then exporting glb format loading. Of course, you can only use three JS with its own plane mesh and bump map can also achieve a similar effect. The advantage of using Blender self built model is that it can freely and visually adjust the fluctuation effect of 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;
      // ground
      if ( === '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;

Bing dwen dwen Winter Olympic mascot

Now add the Bing dwen dwen Winter Olympic mascot. 🐼, Bing dwen dwen is also loaded by glb format. Its original model comes from here , after the current model is free from this website, the original model is built using 3D max. I found that it can not be directly used in the web page. I need to convert the model format in Blender and adjust the map normal of the model to restore the rendering effect.

Original model:

Map of Bing dwen dwen:

Convert to the model supported by Blender, adjust the model map normals in Blender, and add the map:

Export glb format:

📖 To add a mapping tutorial portal to a model in Blender: How to map the model in Blender

Bing dwen dwen 🐼 It can be found that there is a layer of transparent plastic or glass shell outside it. This effect can be achieved by modifying the transparency, metallicity, roughness and other material parameters of the model. Finally, it can be rendered as 👆 The effect shown in the banner diagram is shown in the following code.

loader.load(bingdundunModel, mesh => {
  mesh.scene.traverse(child => {
    if (child.isMesh) {
      // inside
      if ( === 'oldtiger001') {
        child.material.metalness = .5
        child.material.roughness = .8
      // Translucent shell
      if ( === '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);

Create the Olympic rings

The five Olympic rings are realized by TorusGeometry, a basic geometric model. Five circular rings are created, and their material, color and position are adjusted to form a five ring structure in the order of blue, black, red, yellow and green. The five rings material uses MeshLambertMaterial.

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 }}
]; => {
  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);
fiveCyclesGroup.scale.set(.036, .036, .036);
fiveCyclesGroup.position.set(0, 10, -8);

💡 TorusGeometry torus

TorusGeometry is a class used to generate torus geometry.


TorusGeometry(radius: Float, tube: Float, radialSegments: Integer, tubularSegments: Integer, arc: Float)
  • Radius: radius of the ring, from the center of the ring to the center of the pipe (cross section). The default value is 1.
  • tube: the radius of the pipe. The default value is 0.4.
  • radialSegments: the number of segments of the torus. The default value is 8.
  • tubularSegments: the number of pipe segments. The default value is 6.
  • arc: the center angle of the ring (in radians). The default value is math PI * 2.

💡 MeshLambertMaterial matte surface material

A non glossy surface material without specular highlights. The material uses a non physical based Lambertian model to calculate reflectivity. This can well simulate some surfaces (such as untreated wood or stone), but it cannot simulate shiny surfaces with specular highlights (such as painted wood).


MeshLambertMaterial(parameters : Object)
  • parameters: (optional) an object that defines the appearance of a material, with one or more attributes. Any property of the material can be passed in from here.

Create flag

The flag model is from sketchfab Download, also need a flagpole, you can add a cylindrical cube in Blender, and adjust the appropriate length, width and height to combine with the flag surface. Originally, I wanted to add the flag map to the flag model, but in order to avoid using it incorrectly and causing sensitive problems, I used the flag map of Beijing 2022 Winter Olympic Games 😂.

Flag map:

Animation has been added to the flag surface, and the animation frame playback needs to be performed in the code.

loader.load(flagModel, mesh => {
  mesh.scene.traverse(child => {
    if (child.isMesh) {
      child.castShadow = true;
      // flag
      if ( === 'mesh_0001') {
        child.material.metalness = .1;
        child.material.roughness = .1; = new THREE.TextureLoader().load(flagTexture);
      // flagpole
      if ( === 'Cylinder') {
        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();

Create trees

In order to enrich the picture and create a winter atmosphere, several pine trees were added 🌲 As a decoration. A skill is very important when adding pine trees: we know that because the tree model is very complex and has a lot of faces, too many faces will reduce the page performance and cause jamming. Two are used in this article, as shown in the figure below 👇 The two intersecting faces shown are used as the base of the tree. In this way, the tree has only two faces. Using this technique can greatly optimize the page performance, and the tree 🌲 It also looks 3D.

Texture mapping:

In order to make the tree transparent only in the transparent part of the map and opaque in other places, and can produce tree shadow instead of box shadow, you need to add the following two materials to the tree model: MeshPhysicalMaterial and MeshDepthMaterial. The two materials use the same texture map, in which MeshDepthMaterial is added to the custromMaterial attribute of the model.

 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);
  // Clone the other two trees
  let tree2 = mesh.scene.clone();
  tree2.position.set(10, -8, -15);
  tree2.scale.set(18, 18, 18);
  // ...

The effect can also be achieved from 👆 As can be seen from the Banner picture above, I canceled the shadow display of the tree in order to make the picture better.

📌 In 3D function development, some unimportant decoration models can adopt this strategy to optimize.

💡 MeshDepthMaterial depth mesh material

A material that draws geometry by depth. The depth is based on the near and far plane of the camera. White is the closest and black is the farthest.


MeshDepthMaterial(parameters: Object)
  • parameters: (optional) an object that defines the appearance of a material, with one or more attributes. Any property of the material can be passed in from here.

Special properties:

  • . depthPacking[Constant]: the code of depth packing. The default is BasicDepthPacking.
  • . displacementMap[Texture]: displacement maps affect the position of mesh vertices. Unlike other maps that only affect the lighting and shadows of materials, displaced vertices can cast shadows, block other objects, and act as real geometry.
  • . displacementScale[Float]: the degree of influence of the displacement map on the mesh (black is no displacement, white is the maximum displacement). If no displacement map is set, this value is not applied. The default value is 1.
  • . displacementBias[Float]: the offset of the displacement map on the mesh vertices. If no displacement map is set, this value is not applied. The default value is 0.

💡 custromMaterial custom material

Adding custromMaterial custom material attribute to the mesh can realize the shadow of the content area of the transparent peripheral png picture map.

Create snowflakes

Create snowflakes ❄️, You need particle knowledge. THREE.Points is a class used to create points. It is also used to manage particles in batch. In this example, 1500 snowflake particles are created, and they are set with random coordinates defining three-dimensional space and random moving speeds in horizontal and vertical directions.

// Snowflake map
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,
  // Background fusion
  blending: THREE.AdditiveBlending,
  // Depth of field attenuation
  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);
  // Longitudinal velocity
  vertice.velocityY = 0.1 + Math.random() / 3;
  // Lateral velocity
  vertice.velocityX = (Math.random() - 0.5) / 3;
  // Add to geometry
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;

💡 Points particles

Three.js, rain 🌧️, snow ❄️, Cloud ☁️, stars ✨ And other common particles in life can be simulated using Points.


new THREE.Points(geometry, material);
  • The constructor can accept two parameters, a geometry and a material. The geometry parameter is used to determine the position coordinates of particles, and the material parameter is used to format particles;
  • Simple geometry objects such as BoxGeometry and sphereometry can be used as parameters of particle system;
  • Generally speaking, you need to assign vertices to determine the position of particles.

💡 PointsMaterial point material

Through three Pointsmaterial can set the attribute parameters of particles. It is the default material used by Points.


PointsMaterial(parameters : Object)
  • parameters: (optional) an object that defines the appearance of a material, with one or more attributes. Any property of the material can be passed in from here.

💡 Material properties blending

Material Blending attribute mainly controls the superposition mode of texture fusion The values of the blending property include:

  • THREE.NormalBlending: default
  • THREE. Additive blending: additive blending mode
  • THREE.SubtractiveBlending: subtractive blending mode
  • THREE. Multiplicyblending: multiplicative fusion mode
  • THREE.CustomBlending: custom blending mode, and blendSrc, .blendDst or The blendEquation attribute is used in combination

💡 Material properties sizeAttenuation

Whether the size of particles will be attenuated by the camera depth. The default is true (perspective cameras only).

💡 Three.js vector

Several dimensional vectors have several components. Two dimensional vector 2 has two components X and y, three-dimensional vector 3 has three components x, y and z, and four-dimensional vector 4 has four components x, y, z and w.

Related API:

  • Vector2: 2D vector
  • Vector3: 3D vector
  • Vector4: four dimensional vector

Lens control, zoom adaptation, animation

controls = new OrbitControls(camera, renderer.domElement);, 0, 0);
controls.enableDamping = true;
// Disable translation
controls.enablePan = false;
// Disable scaling
controls.enableZoom = false;
// Vertical rotation angle limit
controls.minPolarAngle = 1.4;
controls.maxPolarAngle = 1.8;
// Horizontal rotation angle limit
controls.minAzimuthAngle = -.6;
controls.maxAzimuthAngle = .6;
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
function animate() {
  renderer.render(scene, camera);
  controls && controls.update();
  // Flag animation update
  mixer && mixer.update(new THREE.Clock().getDelta());
  // Lens animation
  TWEEN && TWEEN.update();
  // Five ring rotation
  fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
  // After the vertex changes, it needs to be updated, otherwise the raindrop effect cannot be realized
  points.geometry.verticesNeedUpdate = true;
  // Snowflake animation update
  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;

🔗 Full code:


💡 The new knowledge points mainly included in this paper include:

  • TorusGeometry torus
  • MeshLambertMaterial matte surface material
  • MeshDepthMaterial depth mesh material
  • custromMaterial custom material
  • Points particles
  • PointsMaterial point material
  • Material properties blending,. sizeAttenuation
  • Three.js vector

Space for further optimization:

  • Add more interactive functions and interface styles to further optimize;
  • Mascot Bing dwen dwen adds skeletal animation, and controls the movement and interaction through the mouse and keyboard.

Forecast for next period:

  • Metahuman! Three.js portrait optimization

Want to know about scene initialization, lighting, shadows, base geometry, meshes, materials and other three JS related knowledge, you can read my previous articles. If you think the article is helpful to you, don't forget to click three times 👍.


Author: dragonir address:

Added by BigMonkey on Thu, 03 Feb 2022 07:16:24 +0200