Making rain drop collision

I’m trying to make a scene, with rain drops and a 3D model of a roof. For now, i’m trying to detect the collision of the rain drops to the roof, then make a splash effect when there’s a collision. I read that you can use a bound box, but my bound box for the 3D model looks like this

Here is my code for loading the 3D model and creating bound box:

const loader = new GLTFLoader();

loader.load('./simple_roof_model/scene.gltf', function (gltf) {
  gltf.scene.scale.set(0.01, 0.01, 0.01);
  gltf.scene.position.set(0, -3, 0);
  gltf.scene.rotation.set.y = 1.5;
  const model = gltf.scene;
  // Update the model's matrix to apply transformations

  // Get the bounding box of the model
  const modelBoundingBox = new THREE.Box3().setFromObject(model);

  // Create a wireframe geometry based on the bounding box
  const wireframeGeometry = new THREE.BoxGeometry().setFromPoints([

  // Create a wireframe material
  const wireframeMaterial = new THREE.MeshBasicMaterial({
      color: 0x00ff00, // Green color
      wireframe: true // Render as wireframe

  // Create the wireframe mesh
  const wireframeMesh = new THREE.Mesh(wireframeGeometry, wireframeMaterial);

  // Add the wireframe mesh to the scene

How do i fix the bound box to accurately cover the model. And do you have any suggestion on how to make the splash effect?

Technically, if you don’t spawn too many drops per second, it shouldn’t be crazy expensive to just do a raycast from rain drop spawn point towards its falling direction - and just measure the distance to the collision (then despawn the raindrop as soon as it crosses that distance.)

But if you’re creating 1000s of these raindrops and want something quicker:

  1. Create an InstancedMesh of the rain drop particles
  2. Create a spawn surface (plane that limits the area at which the rain drops can spawn and start falling down from.)
  3. Place a camera at the center of that plane, point it downwards and render DepthTexture from that camera perspective to a RenderTarget texture. That way you’ve just did all the raycasting in a single RT.
  4. Whenever you spawn a new raindrop, just sample the RT at the position at which you create the drop (or at the position it will fall at, if it’s falling at an angle.), and calculate the maximum distance drop can travel before splashing.

Ain’t no need to base your rain / particle systems on manually set bounding boxes.


Does make the plane to limit the area where the rain can spawn and fall down make a square where there’s no rain at all? I want to make a 3D scene where a can move the camera around.
Or do you mean like this, i’m trying to make a plane that when the drop get in contact with the plane, the drop will reset/ make spalsh effect (but i’m struggling to write the logic for that)

Had the same thought.
Seems, it’s working :handshake: (50k raindrops)


Can you guys share the code or related document or just anything in general to be able to implement this? I have no idea at all how to do it.

How to setup a WebGLRenderTarget with DepthTexture: three.js examples

How to render to the render target with OrthographicCamera, this is what I could recall quickly: Running in the field of instances

Also, used layers for cameras and objects. For example, when you render with the orthographic camera to the render target, you don’t need it to “see” the rain, so the camera “sees” objects at the layer 1.


Three.js is like an illusionist’s hat. You can pull out real magic. If your browser supports WebGPU, here is something to tease you:


I’m trying to follow your first way, implementing this function to update the rain and detect collision. My logic is that if the y coordinate of the rain drop is close to the y coordinate of the collision point, it\s should be the collision, then it will print out “met”. However, even though the particle met the plane in the scene, no message was printed out. Could you take a look and see what is the problem?

function updateRainPositions() {
    intersectionPoints.length = 0; // Clear intersection points array

    raindrops.forEach((raindrop, index) => {
        // Update position
        raindrop.y -= raindrop.velocity*0.1; // Move the raindrop upward

        // Check if raindrop is out of screen
        if (raindrop.y < -3.5) {
            raindrop.y = 10; // Reset position above the screen
            raindrop.velocity = THREE.MathUtils.randFloat(0.1, 0.5); // Reset velocity

        // Raycast to check for collision with the plane
        const raycaster = new THREE.Raycaster(new THREE.Vector3(raindrop.x, raindrop.y, raindrop.z), new THREE.Vector3(0, -1, 0));
        const intersects = raycaster.intersectObject(planeMesh);

        // Raycast to check for collision with the second plane
        const intersects2 = raycaster.intersectObject(roofMesh);

        let collided = false; // Flag to track collision
        let intersectionPoint = null; // Store intersection point

        // Check for intersection with the plane
        if (intersects.length > 0) {
            intersectionPoint = intersects[0].point; // Get the intersection point
            collided = intersectionPoint.y >= raindrop.y; // Check if collision happened at or below the raindrop
            // console.log(intersectionPoint.y);

        // Check for intersection with the second plane
        if (!collided && intersects2.length > 0) {
            intersectionPoint = intersects2[0].point;
            collided = intersectionPoint.y >= raindrop.y;
            // console.log("Intersection point:")
            // console.log(intersectionPoint.y);
            // console.log("rain drop:")
            // console.log(raindrop.y);


        // Reset raindrop if collision detected
        if (collided) {
            // Store the intersection point for visualization
            // console.log(raindrop.x);
            // console.log(raindrop.y);
            // console.log(raindrop.z);
            const tolerance = 0.1 * raindrop.velocity;
            if (intersectionPoint.y - raindrop.y <= tolerance) {
                // Reset raindrop position and velocity
            // raindrop.y -= raindrop.velocity; // Move the raindrop upward
            // // Reset raindrop position and velocity
            // raindrop.y = 10; // Reset position above the screen
            // raindrop.velocity = THREE.MathUtils.randFloat(0.1, 0.5); // Reset velocity

    // Update rainMesh positions
    for (let i = 0; i < gCount; i++) {
        const raindrop = raindrops[i];
        const matrix = new THREE.Matrix4().makeTranslation(raindrop.x, raindrop.y, raindrop.z);
        rainMesh.setMatrixAt(i, matrix);

    rainMesh.instanceMatrix.needsUpdate = true; // Update instance matrix

    // Visualize intersection points

You shouldn’t do the raycasting on every frame - it’s expensive and the result will always be the same (for collisions with static world.)

Only when you spawn a raindrop do the raycast, one time per drop for the entire lifetime of the raindrop - read the length of the vector rainDropStart.sub(intersections[0].point). Then on every frame move the raindrop along it’s path and calculate current distance of the raindrop from it’s starting position. Is the distance between raindrop and the starting position longer than between starting position and the raycaster intersection? Then splash the raindrop.


Demo, if somebody’s interested :slight_smile: :


Genius! This makes me think, water, earth, wind, fire are all in the same category for a reason… Surfaced based fire could essentially be the inverted time scale here with some noise growth :thinking: following suit with both wind and earth / ‘dust’

1 Like

I tried to emulate it once :slight_smile:: Noisy Mountains
It wasn’t the approach with DepthTexture though.

1 Like