【Three.js】Chapter 18 Particles Particles

introduce

particle. They are very popular and can be used to achieve various effects such as stars, smoke, rain, dust, fire and many other things.
The nice thing about particles is that you can display hundreds of thousands of them on the screen at a reasonable frame rate. The disadvantage is that each particle consists of a plane (two triangles) that always faces the camera.
Creating particles is as easy as making a mesh . We need a BufferGeometry , a material ( PointsMaterial ) that can handle particles , and instead of generating a Mesh , we need to create a Points .

set up

The launcher consists of just a cube in the middle of the scene. The cube ensures everything is working properly.

first particle

Let's get rid of our cube and start creating a sphere made of particles.

geometry

You can use any basic Three.js geometry. For the same reasons as grids, it's better to use BufferGeometries . Each vertex of the geometry will become a particle:

/**
 * Particles
 */
// Geometry
const particlesGeometry = new THREE.SphereGeometry(1, 32, 32)

point material

We need a special type of material called PointsMaterial . There's already a lot you can do with this material, but we'll take it a step further by exploring how to create our own particle materials in future lessons.
PointsMaterial has several particle-specific properties, such as one for controlling the size of all particles sizeand one for specifying whether distant particles should be smaller than close ones sizeAttenuation:

// Material
const particlesMaterial = new THREE.PointsMaterial({
    
    
    size: 0.02,
    sizeAttenuation: true
})

As always, we can also change these properties after creating the material:

const particlesMaterial = new THREE.PointsMaterial()
particlesMaterial.size = 0.02
particlesMaterial.sizeAttenuation = true

integral

Finally, we can create the final particles just like we did the Mesh , but this time using the Points class (instead of the Mesh class). Don't forget to add this to the scene:

// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)


That's simple. Let's customize these particles.

custom geometry

To create a custom geometry, we can start with a BufferGeometry and add a positionproperty like we did in the geometry course. Replace the SphereGeometry with the custom geometry and add properties as before 'position':

// Geometry
const particlesGeometry = new THREE.BufferGeometry()
const count = 500

const positions = new Float32Array(count * 3) // Multiply by 3 because each position is composed of 3 values (x, y, z)

for(let i = 0; i < count * 3; i++) // Multiply by 3 for same reason
{
    
    
    positions[i] = (Math.random() - 0.5) * 10 // Math.random() - 0.5 to have a random value between -0.5 and +0.5
}

particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) // Create the Three.js BufferAttribute and specify that each information is composed of 3 values


Don't get frustrated if you can't extract this code yourself. It's a bit complicated and the variables use weird formats.
You should get a bunch of particles around the scene. Now is an excellent time to have fun and test the limits of your computer. try 5000, 50000maybe 500000. You can have millions of particles and still have a reasonable frame rate.
You can imagine the performance limitations. You won't be able to have a 60fps experience with millions of particles on a bad computer or smartphone. We continue to add particles that will greatly reduce the frame rate of the effect. However, it's still impressive.
Now, let's keep the count 5000and change the size to 0.1:

const count = 5000

// ...

particlesMaterial.size = 0.1

// ...

Colors, textures and alpha maps

We can.color change the color of all particles using the properties on PointsMaterial. If you change this property after instantiating the material, don't forget that you need to use the Color class:

particlesMaterial.color = new THREE.Color('#ff88cc')

We can also use this mapproperty to place textures on these particles. Use a TextureLoader already in your code to load /static/textures/particles/one of the textures located at:

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')

// ...

particlesMaterial.map = particleTexture


These textures are resized versions of the pack provided by Kenney, you can find the full pack here: //www.kenney.nl/assets/particle-pack . But you can also create your own packages.
As you can see, colorthe properties are changing the map, just like other materials.
If you look closely, you'll see that the particles in the front hide the particles behind.
We need to activate transparenttransparency and use the texture on the attribute alphaMapinstead map:

// particlesMaterial.map = particleTexture
particlesMaterial.transparent = true
particlesMaterial.alphaMap = particleTexture


It's much better now, but we can still randomly see some edges of the particles.
This is because the particles are drawn in the same order they were created, and WebGL doesn't know which one is in front of the other.
There are various ways to solve this problem.

Use alphaTest

alphaTestis a value between 0and 100that lets WebGL know when not to render a pixel based on its transparency. By default, this value means that the pixel will be rendered regardless. If we use a smaller value, for example if alphais 0.001, the pixel will not be rendered:

particlesMaterial.alphaTest = 0.001

This solution isn't perfect, you can still see glitches if you look closely, but it's already more satisfying.

use depth test

When drawing, WebGL tests whether what is being drawn is closer than what has already been drawn. This is called depth testing and can be deactivated (you can compare alphaTest):

// particlesMaterial.alphaTest = 0.001
particlesMaterial.depthTest = false

While this solution seems to completely solve our problem, disabling depth testing may produce errors if you have other objects in your scene or particles with different colors . Particles may be drawn as if they are on top of the rest of the scene.
Add a cube to the scene to see the effect:

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(),
    new THREE.MeshBasicMaterial()
)
scene.add(cube)

write in depth

As we said, WebGL is testing whether what is drawn is closer than what has already been drawn. The depth of what is drawn is stored in what we call a depth buffer. Instead of not testing if the particles are closer than the ones in the depth buffer we can tell WebGL not to write particles in that depth buffer (you can comment):

// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = false

In our case, this solution solved the problem with almost no drawbacks. Sometimes other objects may be drawn behind or in front of the particles, depending on many factors such as transparency, the order in which you added the objects to the scene, etc.
We see multiple technologies, and there is no perfect solution. You have to adjust according to the project and find the best combination.

mix

Currently, WebGL draws one pixel on top of another.
By changing blendingthe property, we can tell WebGL not only to draw the pixel, but also to add that pixel's color to the color of the already drawn pixel. This will have a saturated effect that looks amazing.
This way the particles will no longer appear on the cube
. To test this, just blendingchange the properties to THREE.AdditiveBlending(preserve depthWriteproperties):

// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = false
particlesMaterial.blending = THREE.AdditiveBlending


Add more particles (say 20000) to enjoy the effect better.

Be careful though, this effect will hit performance and at 60fps you won't have as many particles as before.
Now we can delete the cube.

different color

We can set different colors for each particle. We first need to add a new attribute with the colorsame name as we did for the position. A color consists of red, green, blue (3 values), so the code will be positionvery similar to the property. We can actually use the same loop for both properties:

const positions = new Float32Array(count * 3)
const colors = new Float32Array(count * 3)

for(let i = 0; i < count * 3; i++)
{
    
    
    positions[i] = (Math.random() - 0.5) * 10
    colors[i] = Math.random()
}

particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))

Be careful with singular and plural.
To activate these vertex colors, just vertexColorschange the properties to true:

particlesMaterial.vertexColors = true


The material's primary color still affects these vertex colors. Feel free to change that color or even contrast it.

// particlesMaterial.color = new THREE.Color('#ff88cc')

animation

There are various ways to animate particles.

By using points as objects

Because the Points class inherits from the Object3D class, you can move, rotate and scale the points as needed.
Rotate the particles in a function tick:

const tick = () =>
{
    
    
    const elapsedTime = clock.getElapsedTime()

    // Update particles
    particles.rotation.y = elapsedTime * 0.2

    // ...
}

While this is already cool, we wanted more control over each particle.

by changing the property

Another solution is to update each vertex position individually. This way, vertices can have different trajectories. We'll animate the particles as if they were floating on a wave, but first, let's see how to update the vertices.
First compare the overall rotation we did before particles:

const tick = () =>
{
    
    
    // ...

    // particles.rotation.y = elapsedTime * 0.2

    // ...
}

To update each vertex we have to update the right part in the properties because all vertices are stored in this 1D array where the first 3 values position​​correspond to the coordinates of the first vertex x``y``zand then the next 3 values at the second vertex x``y``z, and so on.
We only want the vertices to move up and down, which means we will update the y axis only. Because positionthe property is a one-dimensional array, we have to iterate over it 3 by 3 and only update the second value, which is the coordinate y.
Let's start by iterating over each vertex:

const tick = () =>
{
    
    
		// ...

    for(let i = 0; i < count; i++)
    {
    
    
        const i3 = i * 3
    }

    // ...
}

Here, we chose a simple loop forfrom 0to count, and internally we created a variable i3that simply imultiplied by 3 to get 0,3,6,9......the array index of .
The simplest way to simulate wave motion is to use a simple sinusoid. First, we'll update all vertices to move up and down at the same frequency.
Coordinates can i3 + 1be accessed in the array at index y:

const tick = () =>
{
    
    
    // ...

    for(let i = 0; i < count; i++)
    {
    
    
        const i3 = i * 3

        particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
    }

    // ...
}

Unfortunately, nothing is moving. The problem is that Three.js must be notified that the geometry has changed. To do this we have to set positionthe properties needsUpdateto true:

const tick = () =>
{
    
    
    // ...

    for(let i = 0; i < count; i++)
    {
    
    
        const i3 = i * 3

        particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
    }
    particlesGeometry.attributes.position.needsUpdate = true 

    // ...
}

All particles should move up and down like an airplane.
It's a good start and we're almost there. All we need to do now is apply an offset to the sinusoid between particles so that we get that waveform.
For this we can use xcoordinates. To get this value we can use ythe same technique we used for coordinates, but instead of i3 + 1coordinates xit's just i3:

const tick = () =>
{
    
    
    // ...

    for(let i = 0; i < count; i++)
    {
    
    
        let i3 = i * 3

        const x = particlesGeometry.attributes.position.array[i3]
        particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
    }
    particlesGeometry.attributes.position.needsUpdate = true

    // ...
}

You should get beautiful particle waves. Unfortunately, you should avoid this technique. If we have 20000particles, we will loop through each particle, calculate a new position, and update the entire properties every frame. This can handle a small number of particles, but we need millions of particles.

By using a custom shader

To update millions of particles on every frame at a good frame rate, we need to create our own material with our own shader. But shaders are for future lessons.

Guess you like

Origin blog.csdn.net/m0_68324632/article/details/132361841