Recently, I read the “Three.js development Guide”, and realized that just watching without practicing is almost the same as not watching, so I practiced writing this little animation.

Project Address:Github.com/alasolala/t…

Front knowledge

WebGL allows us to develop 3D applications in the browser, but programming directly with WebGL is complicated. Developers need to know the low-level details of WebGL and learn the complex coloring language to get most of the functionality of WebGL. Three. Js provides a set of very simple JavaScript apis for WebGL features, making it easy for developers to create beautiful 3D graphics. There are a lot of cool 3D effects on three. js.

Developing 3D applications with three. js usually includes Renderer, Scene, Camera, objects you create in the Scene, and lighting.

Imagine the situation of taking photos. We need a Scene. In this Scene, we set up the object to be taken, set the lighting environment, and set the position and orientation of the Camera, then we can take photos. The Renderer is probably more like a photographer, giving orders to take pictures and generating images.

Copy and run the following code to create a very simple 3D scene.

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width, Initial-scale =1.0"> <title>room</title> </head> <body> <div id="webgl-output"></div> <script SRC ="https://unpkg.com/[email protected]/build/three.js"></script> <script> function init () {const scene = new Scene() const camera = new THREE.PerspectiveCamera(45, window.innerwidth/window.innerheight, 0.1, 1000 ) camera.position.set(-30, 40, LookAt (0,0,0) scene. Add (camera) const planeGeometry = new THREE.PlaneGeometry(60,20) const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xAAAAAA }) const plane = new THREE.Mesh(planeGeometry, planeMaterial) plane.rotation.x = -Math.PI / 2 plane.position.set(15, 0, 0) scene.add(plane) const sphereGeometry = new THREE.SphereGeometry(4, 20, 20) const sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }) const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial) sphere.position.set(20, 4, 2) scene.add(sphere) const spotLight = new THREE.SpotLight(0xffffff) spotLight.position.set(-20, 30, -15) scene.add(spotLight) const renderer = new THREE.WebGLRenderer() renderer.setClearColor(new THREE.Color(0x000000)) renderer.setSize(window.innerWidth, window.innerHeight) document.getElementById('webgl-output').appendChild(renderer.domElement) renderer.render(scene, camera) } init() </script> </body> </html>Copy the code

There is a Scene.

Scene object is a container for all the different objects, but the object itself does not have very complex operations. We usually instantiate a Scene at the beginning of the program, and then add the camera, object, and light source to the Scene.

Const scene = new three.scene () scene.add(camera) // Add camera scene.add(plane) // Add gray plane scene.add(sphere) // Add yellow sphere Scene.add (spotLight) // Add lightCopy the code

Camera

The three.js library provides two different types of cameras: perspective projection cameras and orthogonal projection cameras.

The effect of a perspective projection camera is similar to what the human eye sees in the real world, with the effect of "larger near and smaller far away," where parallel lines in the vertical plane of view intersect at a distance. The effect of the orthogonal projection camera is similar to the effect that we were taught to draw in math and geometry class, where parallel lines in three dimensions never meet on the screen.Copy the code

We’re using perspective projection cameras here, and we’ll focus on that, but we’ll use it later for orthographic projection cameras.

Const camera = new THREE.PerspectiveCamera(45, window.innerwidth/window.innerheight, 0.1, Set (-30, 40, 30) camera. LookAt (0,0,0) scene. Add (camera)Copy the code

There are three steps to setting up a camera: determine the field of vision, determine the camera coordinates, and determine the camera’s focus.

We are innew THREE.PerspectiveCameraAccording to the figure above, 45 is FOV, which is the Angle between the upper and lower edges of the field of vision.window.innerWidth / window.innerHeightIs the ratio of the horizontal and vertical lengths of the field of view. 0.1 (near) and 1000 (far) are the closest and furthest distances from the camera to the field of view respectively. These parameters determine the range of 3d space to be displayed, namely the gray area in the figure above.

Camera.position. set(-30, 40, 30) determines the coordinates of the camera in space.

Camera. lookAt(0,0,0) determines the focus of the camera, and the line between this point and the camera coordinates is the shooting direction.

The display effect of gray area in the figure above on the screen, that is, the projection of 3d space coordinates onto the screen 2d coordinates are completed by WebGL, we only need to care about the 3d space coordinates.

Coordinate system

Unlike the 3D CSS coordinate system we talked about earlier, the WebGL coordinate system is a right-handed coordinate system, with X-axis pointing to the right, Y-axis pointing up, and z-axis pointing to “me”.

Stretch out your right hand so that your thumb and forefinger form an "L", thumb to the right and forefinger up. The rest of your fingers point towards yourself, thus establishing a right-handed coordinate system. The thumb, index finger and other fingers represent the positive direction of the X, Y and Z axes respectivelyCopy the code

Positioning and translation in space is a little bit easier to understand, but let’s look at rotation.

Sometimes, we’ll set the object’s rotation to object.rotation. X = -math.pi / 2, which means -90 degrees about the X-axis. Specifically how to rotate, it is necessary to compare with the above coordinate system, expand the right hand, the thumb points to the positive direction of the X axis, the bending direction of the other fingers is the positive direction of rotation; The thumb points in the negative direction of the x axis, and the rest of the fingers bend in the negative direction of rotation. The rotation direction of y axis and Z axis is judged in the same way.

object

In three.js, creating an object requires two parameters: Geometry and Material. In layman’s terms, geometry determines the shape of an object, and materials determine the color of the surface, texture mapping, response to light, and so on.

// create a planeGeometry with the parameters of Width and height const planeGeometry = new THREE.PlaneGeometry(60,20) MeshLambertMaterial is a diffuse account and don't consider the material of mirror reflection const planeMaterial = new THREE. MeshLambertMaterial ({color: 0xAAAAAA}) // Create object based on geometry and material const plane = new THREE.Mesh(planeGeometry, planeMaterial) Plane.rotation. X = -math.pi / 2 plane.position.set(15, 0, 0) scene.add(plane)Copy the code

light

Without light, the rendered scene will not be visible (unless you use base materials or wireframes, which of course are rarely used when building 3D applications).

WebGL itself does not support light sources. If you don’t use three.js, you’ll need to write your own WebGL shader to simulate the light source. Three.js makes it easy to use light sources.

const spotLight = new THREE.SpotLight(0xffffff)
spotLight.position.set(0, 0, 100)
scene.add(spotLight)
Copy the code

As shown above, all we need to do is create a light source and add it to the scene. Three. js will calculate the display effect of each object in the scene according to the type and position of the light source.

The most common sources of light are AmbientLight, PointLight, SpotLight, and DirectionalLight.

Renderer

When the camera, objects, lighting, etc. is ready for the scene, it’s time for the renderer to step in.

In the small example above, we use the renderer like this:

Const renderer = new three. WebGLRenderer() // Set canvas background color, Renderer. setClearColor(new three.color (0x000000)) // Set the canvas size renderer.setSize(window.innerWidth, Window.innerheight) // Add the canvas element (renderer.domElement, AppendChild (renderer.domElement) // Perform render operations, The parameters are scene and camera defined above and renderer.render(scene, camera)Copy the code

It can be seen that using Three.js to develop 3D applications, we only need to care about the layout and movement of objects, cameras and lighting in the scene in 3D space, and the specific rendering is completed by Three.js. Of course, it would be nice to know some of the basics of WebGL, as some applications will be too complex for the Three.js API.

Implement rain animation

Initialization Scenario

Since every 3D application has scene, camera, and render initialization, we encapsulate the initialization of the three into a class Template, which can be subclassed for later application initialization to quickly build the framework.

import { Scene, PerspectiveCamera, WebGLRenderer, Vector3, } from 'three' export default class Template {constructor () {this.el = document.body this.pcamera = { fov: 45, aspect: window.innerWidth / window.innerHeight, near: 1, far: 1000 } this.cameraPostion = new Vector3(0, 0, RendererColor = new Color(0x000000) This.rendererWidth = new Color(0x000000 Window.innerwidth this.rendererheight = window.innerheight} initPerspectiveCamera () { PerspectiveCamera = new PerspectiveCamera(this.pcamera. Fov, this.pcamera. Aspect, this.pcamera. this.PCamera.far, ) camera.position.copy(this.cameraPostion) camera.lookAt(this.cameraLookAt) this.camera = camera this.scene.add(camera) } initScene () {this.scene = new scene ()} initRenderer () {const renderer = new WebGLRenderer() renderer.setClearColor(this.rendererColor) renderer.setSize(this.rendererWidth, this.rendererHeight) this.el.appendChild(renderer.domElement) this.renderer = renderer } init () { this.initScene() this.initPerspectiveCamera() this.initRenderer() } }Copy the code

In our rain animation, create a Director class to manage the animation, which inherits from the Template class. As you can see, what it does is pretty clear: initialize the frame, modify the default configuration of the parent class, add objects (clouds and raindrops), add lighting (lightning is also caused by lighting), add atomization, and loop rendering.

//director.js export default class Director extends Template{ constructor () { super() //set params //camera //init camera/scene/render this.init() this.camera.rotation. X = 1.16 Y = -0.12 this.camera.rotation. Z = 0.27 // Add object this.addcloud () // Add clouds and raindrops This.addraindrop () //add light this.initlight () // Add light, This.addfog () // Add fog near the camera. The farther away you are from the camera, //animate this.animate() //requestAnimationFrame implement animation}}Copy the code

Create ever-changing clouds

We first create a plane and use a small cloud as the material to create a cloud object. Then add many cloud objects on top of each other to create a cloud.

// cloud.js const texture = new TextureLoader().load('/images/ smok.png ') // Load Cloud material const cloudGeo = new Const cloudMaterial = new MeshLambertMaterial({// Image as texture map, generate texture map: texture, transparent: true }) export default class Cloud { constructor () { const cloud = new Mesh(cloudGeo, Opacity = 0.6 this.instance = cloud} setPosition (x,y,z) { this.instance.position.set(x,y,z) } setRotation (x,y,z) { this.instance.rotation.x = x this.instance.rotation.y = y {this.instance.rotation. Z = z} animate () {this.instance.rotation. Z -= 0.003}}Copy the code

In the Director class, generate 30 cloud objects and randomly set their position and rotation to create a spread and cascade effect. Invoke the animate method of cloud objects during loop rendering.

//director.js addCloud () { this.clouds = [] for(let i = 0; i < 30; i++){ const cloud = new Cloud() this.clouds.push(cloud) cloud.setPosition(Math.random() * 1000 - 460, 600, Math.random() * 500-400) cloud.setrotation (1.16, -0.12, Math.random() * 360) this.scene.add(cloud.instance) } } animate () { //cloud move this.clouds.forEach((cloud) => { // Call the animate method of each cloud object to create an animate effect for the entire cloud. Cloud.animate ()})... this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.animate.bind(this)) }Copy the code

Ambient light and lightning

At the same time, AmbientLight and DirectionalLight are used as the stable light source of the whole scene to enhance the simulation of the real scene.

//director.js initLight () { const ambientLight = new AmbientLight(0x555555) this.scene.add(ambientLight) const DirectionLight = new DirectionalLight (0 xffeedd) directionLight. Position. Set (0, 1). This scene. The add (directionLight)}Copy the code

Simulate lightning with PointLight, starting with an initial PointLight.

//director.js addLightning () {const lightning = new PointLight(0x062d89, 30, 500, 1.7) lightning.position.set(200, 300, 100) this.lightning = lightning this.scene.add(lightning) }Copy the code

During cyclic rendering, the PointLight intensity (power) is constantly randomly changed to create a flickering effect. When the intensity is low, i.e. when the light is low, the position of the PointLight is “quietly” changed so that lightning randomly appears at various locations in the cloud without being abrupt.

//director.js animate () { ... / / from the if (Math. The random () > 0.93 | | this. 20. The power > 100) {if (this. 20. Power < 100) { this.lightning.position.set( Math.random() * 400, 300 + Math.random() * 200, 100 ) } this.lightning.power = 50 + Math.random() * 500 } this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.animate.bind(this)) }Copy the code

Create a raindrop

The particle effect used to create raindrops. To create a group of particles, the intuitive way is to create a particle object, and then copy N of it, defining their positions and rotations.

This works well when you use a small number of objects, but when you want to use a large number of three.sprite objects, you can quickly run into performance issues because each object needs to be managed separately by three.js.

Three.js provides another way to handle a large number of particles, which requires the use of three.points. With three. Points, three. js you no longer need to manage a large number of single three. Sprite objects, but only THREE.

Using THREE.Points, it is very easy to create many tiny objects that simulate raindrops, snowflakes, smoke, and other interesting effects.

The core idea of THREE Points is to first declare a geometry GEom and then determine the positions of each vertex in the geometry. The positions of these vertices will be the positions of each particle. Determine the material material of the vertex by PointsMaterial. Then new Points(GEom, Material) generates a particle system based on the incoming geometry and vertex material.

Movement of particles: The particle’s position coordinates are determined by a set of digital const positions = this. The java.awt.geom. Attributes. The position. The array, this group of Numbers, each number three determine a coordinate point (x, y, z), so to change the x coordinate of particles, Change the positions[3n] (n is the ordinal number of a particle); Similarly, the y-coordinate corresponds to positions[3n+1] and the Z-coordinate corresponds to positions[3n+2].

//RainDrop.js export default class RainDrop { constructor () { const texture = new TextureLoader().load('/images/rain-drop.png') const Material = new PointsMaterial({// Initialize vertex material with image size: 0.8, map: texture, transparent: true }) const positions = [] this.drops = 8000 this.geom = new BufferGeometry() this.velocityY = [] for(let i = 0; i < this.drops; i++){ positions.push( Math.random() * 400 - 200 ) positions.push( Math.random() * 500 - 250 ) positions.push( Math.random() * 400-200) this.velocityy.push (0.5 + math.random () / 2) // initialize the coordinates of each particle and the velocity of the particle in the Y direction} // Determine the position coordinates of each vertex this.geom.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ) this.instance = new Points(this.geom, Material) / / initialize the particle system} the animate () {const positions = enclosing the java.awt.geom. Attributes. The position. The array; for(let i=0; i<this.drops * 3; I +=3){// change Y coordinates, This. VelocityY [I /3] += Math.random() * 0.05 positions[I + 1] -= this. VelocityY [I /3] if(positions[I + 1] < -200){positions[I + 1] = 200 this.velocityy [I /3] = 0.5 + math.random () / 2}} this.instance.rotation. Y += 0.002 this.geom.attributes.position.needsUpdate = true } }Copy the code

Add RainDrop particles to the scene and call RainDrop’s animate method when rendering in a loop:

//director.js
addRainDrop () {
  this.rainDrop = new RainDrop()
  this.scene.add(this.rainDrop.instance)
}
animate () {
  //rain drop move
  this.rainDrop.animate() 
  ...
  this.renderer.render(this.scene, this.camera)
  requestAnimationFrame(this.animate.bind(this))
}
Copy the code