In 3D graphics, adding dynamic environmental elements like clouds can enhance the realism and immersion of the scene. In this article, we will explore how to create a cloud system using Three.js. The cloud system will be based on particle systems, allowing for smooth movement and animation of clouds across the sky.
Step 1: Setting Up the Cloud System
We start by creating a class CloudSystem that will manage the clouds within our scene. This class will take the scene and a texture URL as input to generate cloud particles that move across the sky dynamically.
import * as THREE from "three";
class CloudSystem {
constructor(scene, textureUrl) {
this.scene = scene;
this.cloudParticles = [];
this.textureUrl = textureUrl;
this.initCloudSystem();
}
initCloudSystem() {
const loader = new THREE.TextureLoader();
loader.load(this.textureUrl, (texture) => {
this.createCloudPatches(texture);
});
}
}
Here, we initialize the cloud system by loading the texture for the clouds using the THREE.TextureLoader. Once the texture is loaded, we proceed to create cloud patches.
Step 2: Creating Cloud Patches
Cloud patches are groups of particles that simulate cloud formations. Each patch will contain a random number of cloud particles, each with its own position, velocity, size, and color.
createCloudPatches(texture) {
const numberOfPatches = 50; // Number of cloud patches
const planeSize = 20000;
const cloudHeightMin = 5000;
const cloudHeightMax = 15000;
for (let i = 0; i < numberOfPatches; i++) {
const patchSize = Math.random() * 800 + 800; // Random size for each patch
const patchDensity = Math.random() * 30 + 20; // Number of particles in each patch
const patchCenter = new THREE.Vector3(
-planeSize / 2 + Math.random() * planeSize,
cloudHeightMin + Math.random() * (cloudHeightMax - cloudHeightMin),
-planeSize / 2 + Math.random() * planeSize
);
this.createCloudPatch(texture, patchCenter, patchSize, patchDensity);
}
}
In this function, we define the number of patches, the size of the plane where clouds will move, and the range for cloud height. Each cloud patch is placed randomly within this space, with the size and density of particles varying for a natural look.
Step 3: Defining the Cloud Particles
Each cloud patch consists of particles, which are represented by points. We define attributes like positions, velocities, sizes, and colors for each particle.
createCloudPatch(texture, center, size, density) {
const cloudGeometry = new THREE.BufferGeometry();
const cloudPositions = [];
const cloudVelocities = [];
const cloudSizes = [];
const cloudColors = [];
for (let i = 0; i < density; i++) {
const position = new THREE.Vector3(
center.x + (Math.random() - 0.5) * size,
center.y + ((Math.random() - 0.5) * size) / 2,
center.z + (Math.random() - 0.5) * size
);
cloudPositions.push(position.x, position.y, position.z);
const velocity = new THREE.Vector3(10 + (Math.random() * 10 - 5), 0, 0);
cloudVelocities.push(velocity.x, velocity.y, velocity.z);
const cloudSize = 100 + Math.random() * 300;
cloudSizes.push(cloudSize);
const color = new THREE.Color(`hsl(0, 0%, ${Math.random() * 50 + 50}%)`);
cloudColors.push(color.r, color.g, color.b);
}
cloudGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cloudPositions, 3));
cloudGeometry.setAttribute("velocity", new THREE.Float32BufferAttribute(cloudVelocities, 3));
cloudGeometry.setAttribute("size", new THREE.Float32BufferAttribute(cloudSizes, 1));
cloudGeometry.setAttribute("color", new THREE.Float32BufferAttribute(cloudColors, 3));
const cloudMaterial = new THREE.PointsMaterial({
map: texture,
size: 1500.0, // Base size
blending: THREE.NormalBlending,
depthTest: true,
depthWrite: false,
transparent: true,
opacity: 0.2, // Adjust opacity
vertexColors: true,
});
const cloudParticleSystem = new THREE.Points(cloudGeometry, cloudMaterial);
this.scene.add(cloudParticleSystem);
cloudParticleSystem.frustumCulled = false;
this.cloudParticles.push(cloudParticleSystem);
}
Here, the THREE.PointsMaterial allows us to render each particle with a texture and configure the opacity, blending, and size. The position, velocity, and color of each particle are randomly generated, ensuring variety in the cloud patches.
Step 4: Updating Cloud Movement
Clouds need to move across the sky to simulate natural drift. We update the position of each cloud particle based on its velocity and ensure they wrap around the scene when they go out of bounds.
updateCloudParticles(delta) {
const speedFactor = 0.5; // Increase this factor to speed up the clouds
const boundary = 10000; // Half of the new plane size (20000 / 2)
this.cloudParticles.forEach((cloudParticleSystem) => {
const positions = cloudParticleSystem.geometry.attributes.position.array;
const velocities = cloudParticleSystem.geometry.attributes.velocity.array;
for (let j = 0; j < positions.length; j += 3) {
positions[j] += velocities[j] * delta * speedFactor; // x position update
positions[j + 1] += velocities[j + 1] * delta * speedFactor; // y position update
positions[j + 2] += velocities[j + 2] * delta * speedFactor; // z position update
// Wrap-around logic for continuous clouds based on the new boundary
if (positions[j] > boundary) {
positions[j] = -boundary;
} else if (positions[j] < -boundary) {
positions[j] = boundary;
}
if (positions[j + 2] > boundary) {
positions[j + 2] = -boundary;
} else if (positions[j + 2] < -boundary) {
positions[j + 2] = boundary;
}
}
cloudParticleSystem.geometry.attributes.position.needsUpdate = true;
});
}
The updateCloudParticles function continuously updates the positions of cloud particles, simulating their movement. We also handle wrap-around to make the clouds seamlessly reappear on the other side of the scene, ensuring an endless flow of cloud movement.