Faking an Infinite Runner in Three.js with a Moving Ground Plane and Fog
Implementation Effect Description
- Call the built-in running skeletal animation of the character model and loop the running motion in place.
- A giant grass plane continuously moves backward, visually achieving the effect of the character running forward infinitely.
- Linear white fog is added to the scene, gradually fogging the distant grass, eliminating the hard boundary of the plane and enhancing spatial depth.
- Complete engineering code for window adaptation, lighting and shadows, texture tiling, and memory release.
The dynamic demo effect is as follows:
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
- Use
GLTFLoaderto load the model, and parse the built-in animation clips through theTHREE.AnimationMixer. clipAction()gets the running action, and callingplay()starts a looped playback. The character's coordinates are fixed throughout, visually running in place.
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.
- Create a super-large plane as the ground to avoid boundaries appearing during short-distance movement.
- Modify the ground's
position.zto shift backward in each frame of the rendering loop. - When the ground's offset reaches the threshold of
-500, directly reset the position to 0 for a seamless loop, achieving an infinite runway. - The grass texture uses
RepeatWrappingfor repeated tiling, ensuring the large-area ground texture is not stretched.
3. Scene Fog Depth of Field: Linear Fog Enhances Realism
Add linear white fog to the scene using THREE.Fog:
- Parameter description:
new THREE.Fog(fog color, near clipping distance, far clipping distance). - Distance less than 280: Completely fog-free, clearly showing the nearby grass.
- Distance 280 ~ 1050: Fog gradually thickens.
- Distance over 1050: Completely covered by white fog, obscuring the distant boundary, softening the hard edge of the plane, and enhancing spatial depth.
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.