跪拜 Guibai
← Back to the summary

Faking an Infinite Runner in Three.js with a Moving Ground Plane and Fog

Implementation Effect Description

  1. Call the built-in running skeletal animation of the character model and loop the running motion in place.
  2. A giant grass plane continuously moves backward, visually achieving the effect of the character running forward infinitely.
  3. Linear white fog is added to the scene, gradually fogging the distant grass, eliminating the hard boundary of the plane and enhancing spatial depth.
  4. Complete engineering code for window adaptation, lighting and shadows, texture tiling, and memory release.

The dynamic demo effect is as follows:

run.gif

Complete Code

1. template structure:

<template>
  <div class="three-container" ref="content" id="three"></div>
</template>

2. script code:

<script>
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

export default {
  beforeDestroy() {
    // Method to release ThreeJS resources before component destruction to prevent memory leaks
    this.leaveDestory()
  },
  mounted() {
    this.$nextTick(() => {
      // Initialization method
      this.init()
      // Listen for browser window size changes to adapt canvas size
      window.onresize = () => {
        this.resize()
      }
    })
  },
  data() {
    return {
      isLoading: false
    }
  },
  methods: {
    // Resource destruction function: destroys animation frames, geometries, materials, renderers, etc., to prevent memory overflow from repeated mounting/unmounting of the page
    leaveDestory() {
      // Cancel animation frame request to stop the rendering loop
      if (window.myMar) {
        cancelAnimationFrame(window.myMar)
        window.myMar = null
      }
      if (window.scene) {
        // Traverse all meshes in the scene to release geometry and material video memory resources
        window.scene.traverse(function (v) {
            if (v.type === 'Mesh') {
                if (v.geometry && v.geometry.dispose) {
                    v.geometry.dispose()
                }
                if (v.material && v.material.dispose) {
                    v.material.dispose()
                }
            }
        })
        if (window.scene.children.length > 0) {
          window.scene.remove(window.scene.children[0])
        }
        window.renderer.dispose()  // Release renderer resources
        window.renderer.forceContextLoss()  // Force loss of WebGL context to completely free video memory
        window.renderer.domElement = null
        window.renderer = null
        window.camera = null
        window.clock = null
        if (window.mixer) {
          this.stopAnimation()
        }
        window.gltf = null
        window.model = null
        window.ground = null
        window.onresize = null
        window.scene.clear()
        window.scene = null
      }
    },
    // Method to stop the character's running animation
    stopAnimation() {
      if (window.mixer) {
        window.animationAction.stop()  // Stop the animation clip playback
        window.mixer = null
        window.animationAction = null
        window.ground.position.z = 0  // Reset ground position
      }
    },
    // Method to start the model's built-in running animation
    startAnimation() {
      // Create an animation mixer, bind the character model, used to parse the model's built-in skeletal animation
      window.mixer = new THREE.AnimationMixer(window.model)
      // Get the model's first animation clip (running animation)
      window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
      // Start looping the running animation
      window.animationAction.play()
      // Method to start the rendering loop
      this.animate()
    },
    addGround() {
      let textureLoader = new THREE.TextureLoader()
      textureLoader.load('images/grasslight-big.jpg', (texture) => {
        // Create a super-large plane geometry, 16000x16000, reserving enough movement space
        const gg = new THREE.PlaneGeometry(16000, 16000)
        const gm = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture })
        window.ground = new THREE.Mesh(gg, gm)
        // Rotate the plane -90 degrees around the x-axis to lay it flat as the ground
        ground.rotation.x = -Math.PI / 2
        // Repeat the texture 64 times horizontally and vertically to achieve a fine grass effect
        ground.material.map.repeat.set(64, 64)
        // Set the texture to repeat wrapping when out of bounds
        ground.material.map.wrapS = THREE.RepeatWrapping
        ground.material.map.wrapT = THREE.RepeatWrapping
        // Set the texture color space to ensure correct color display
        ground.material.map.colorSpace = THREE.SRGBColorSpace
        // Ground receives shadows
        ground.receiveShadow = true
        window.scene.add(window.ground)
      })
    },
    resize() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      window.camera.aspect = width / height
      // Update the camera projection matrix
      window.camera.updateProjectionMatrix()
      window.renderer.setSize(width, height)
    },
    async init() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      // Create a 3D scene container
      window.scene = new THREE.Scene()
      // Create a perspective camera with a 45-degree field of view
      window.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
      // Initialize the WebGL renderer with anti-aliasing enabled
      window.renderer = new THREE.WebGLRenderer({ antialias: true })
      // Adapt to device pixel ratio to solve blurriness on high-DPI screens
      window.renderer.setPixelRatio(window.devicePixelRatio)
      // Set the canvas background to white
      window.renderer.setClearColor(0xffffff, 1.0)
      window.renderer.setSize(width, height)
      // Enable shadows
      window.renderer.shadowMapEnabled = true

      // Set camera position and look at the scene origin
      window.camera.position.x = -72.93
      window.camera.position.y = 126.06
      window.camera.position.z = 323.85
      window.camera.lookAt(window.scene.position)

      // Add a hemispheric ambient light to softly illuminate the overall scene
      let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.3)
      hemiLight.position.set(0, 500, 0)
      window.scene.add(hemiLight)

      // Add a spotlight to produce shadows and enhance the 3D effect
      let spotLight = new THREE.SpotLight(0xffffff)
      spotLight.position.set(-40, 650, -10)
      spotLight.castShadow = true
      window.scene.add(spotLight)

      // Timer used to get the frame interval and drive skeletal animation updates
      window.clock = new THREE.Clock()
      // Method to create the grass ground
      this.addGround()
      // Add white fog to the scene, clear near and blurry far, enhancing visual realism, corresponding to the fog effect in the demo
      window.scene.fog = new THREE.Fog(0xffffff, 280, 1050)
      // Append the renderer's canvas to the DOM container
      document.getElementById('three').appendChild(window.renderer.domElement)
      // Call the method to load the running character model
      this.addModel()
    },
    // Method to load the running character model
    addModel() {
      this.isLoading = true
      let loader = new GLTFLoader()
      loader.load('static/models/workman.gltf', (gltf) => {
        window.gltf = gltf
        window.model = gltf.scene
        // Scale the model size
        window.model.scale.set(0.55, 0.55, 0.55)
        // Adjust the model's y-coordinate to fit the ground
        window.model.position.y = -2
        window.scene.add(window.model)
        window.renderer.render(window.scene, window.camera)
        this.isLoading = false
        // Start the running animation after the model is loaded
        this.startAnimation();
      })
    },
    animate() {
      window.myMar = requestAnimationFrame(this.animate)
      // Update the skeletal animation state
      if (window.mixer) {
        const delta = window.clock.getDelta()
        window.mixer.update(delta)
      }
      // Ground loop movement: continuously decrease the z-coordinate to move backward; reset the position when a threshold is reached to achieve infinite scrolling
      if (window.ground.position.z <= -500) {
        window.ground.position.z = 0
      } else {
        window.ground.position.z = window.ground.position.z - 5
      }
      window.renderer.render(window.scene, window.camera)
    }
  }
}
</script>

3. CSS style code:

.three-container {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
}

Breakdown of the Three Core Implementation Principles

1. Character Running in Place: Driven by GLTF Skeletal Animation

2. Infinite Scrolling Grass: Relative Motion Optical Illusion

Core idea: The character stays still while the ground continuously moves backward, using a human visual illusion to simulate forward running.

3. Scene Fog Depth of Field: Linear Fog Enhances Realism

Add linear white fog to the scene using THREE.Fog:

Model Note: The 3D model used in this demonstration is sourced from the internet, and the copyright belongs to the original author. This article is solely for the purpose of learning and demonstrating Three.js front-end technology, and no modifications have been made to the model itself.