Boids simulation in three.js - spawn point looks weird?

Hi everyone,

I tried to simulate boids in 3D space and it kinda works, but also not (?).
My specific problem here is, that the boids behave a bit weird, if it comes to border/edge behaviour.
So I already specified the behaviour to spawn on the other side of the defined values back if it hits a specific spot in the coordinate system. So it actually works, they spawn on the other side and continue on flying.
But weirdly, it happens that there is one boid (?) in (0, 0, 0) that does not move and it looks a bit like as most of the boids spawn out of this spot (but they actually don’t if you have a closer look).

Does anyone know why this happens? I am really clueless and tried already a lot.

!!! Also if I try other conditions for the if-statements, there are other weird things happening around the (0, 0, 0). What’s wrong with this (0, 0, 0)?

Thanks in advance!


The JS File:
23.04.2024-test.js (8.8 KB)

The HTML:
index.html (310 Bytes)

The JS Code:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { TextureLoader } from 'three';
const flock = [];


// texture


// Erstellung der Szene + Kamera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const light = new THREE.PointLight(0xffffff, 1);

light.position.set(800, 300, 1000);
scene.add(light);

// Renderer, damit die Szene auch gezeigt wird
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );

document.body.appendChild( renderer.domElement );

const controls = new OrbitControls( camera, renderer.domElement );

// BG Farbe hier ändern
scene.background = new THREE.Color(808080);

// fog
scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );

// Kamera Position ändern
camera.position.set(800, 300, 1000);
camera.lookAt(60, 710, 10)
controls.update();

// Laden des Boid-Modells aus der Datei
async function loadBoidModel() {
    const loader = new GLTFLoader();
    return new Promise((resolve, reject) => {
        loader.load('3d_models/low_poly_bird/scene.gltf', gltf => {
            resolve(gltf.scene); // Lade die ganze Szene als Mesh
        }, undefined, reject);
    });
}

// Verfolgung der Mausposition in der Szene
const mouse = new THREE.Vector2();

function onMouseMove(event) {
    // Normalisiere die Mausposition auf das Fenster, um Werte zwischen -1 und 1 zu erhalten
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

// Eventlistener für Mausbewegung
window.addEventListener('mousemove', onMouseMove, false);

// Boid-Klasse
class Boid {
    constructor(){
        this.position = new THREE.Vector3(this.getRandomIntInclusive(0, 2000), this.getRandomIntInclusive(0, 2000), this.getRandomIntInclusive(0, 2000));
        this.velocity = new THREE.Vector3();
        this.velocity.randomDirection();
        this.velocity.setLength(this.getRandomIntInclusive(2, 4));
        this.acceleration = new THREE.Vector3();
        this.maxForce = 0.1;
        this.maxSpeed = 5;
        this.xBoundaries = 2000;
        this.yBoundaries = 2000;
        this.zBoundaries = 2000;        
        this.boidMesh = null;
        this.createBoidMesh(); // Erstellung eines Boids
    }


    // Methode zur Anpassung der Bewegung basierend auf der Mausposition
    steerAwayFromMouse() {
        const strength = 0.5; // Stärke der Abstoßung vom Cursor
        const target = new THREE.Vector3(mouse.x, mouse.y, 0); // Ziel ist die Mausposition auf der Ebene z=0

        // Berechne die Richtung und den Abstand zwischen dem Boid und der Maus
        const direction = new THREE.Vector3().subVectors(this.position, target);
        const distance = direction.length();

        // Wenn die Maus nahe genug ist, weiche dem Cursor aus
        if (distance < 100) {
            direction.normalize().multiplyScalar(strength);
            this.acceleration.add(direction);
        }
    }

    async createBoidMesh() {
        const boidModel = await loadBoidModel();
        this.boidMesh = boidModel.clone(); // Klone das geladene Modell
        this.boidMesh.scale.set(10, 10, 10); // Skalierung des Modells
        scene.add(this.boidMesh); // Füge das Modell der Szene hinzu
        await loadTexturesAndApplyToModel(this.boidMesh); // Lade Texturen und wende sie auf das Modell an
    }

    getRandomIntInclusive(min, max) {
        const minCeiled = Math.ceil(min);
        const maxFloored = Math.floor(max);
        return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); // The maximum is inclusive and the minimum is inclusive
    }

    edges(){
/*      this.position.x = this.boundaries(this.position.x, this.xBoundaries);
        this.position.y = this.boundaries(this.position.y, this.yBoundaries);
        this.position.z = this.boundaries(this.position.z, this.zBoundaries); */
        if (this.position.x > this.xBoundaries){
            this.position.x = 0;
        } else if(this.position.x < 0){
            this.position.x = this.xBoundaries;
        }

        if (this.position.y > this.yBoundaries){
            this.position.y = 0;
        } else if(this.position.y < 0){
            this.position.y = this.yBoundaries;
        }

        if (this.position.z > this.zBoundaries){
            this.position.z = 0;
        } else if(this.position.z < 0){
            this.position.z = this.zBoundaries;
        }


            // Überprüfen und umkehren, wenn die Boids die Grenzen erreichen
/*             if (this.position.x > this.xBoundaries || this.position.x < 0) {
                this.velocity.x *= -1;
            }
            if (this.position.y > this.yBoundaries || this.position.y < 0) {
                this.velocity.y *= -1;
            }
            if (this.position.z > this.zBoundaries || this.position.z < 0) {
                this.velocity.z *= -1;
            } */

    }

/*     boundaries(thisPosition, boundaries){
        if (thisPosition > boundaries) {
            thisPosition = 0;
        } else if (thisPosition < 0) {
            thisPosition = boundaries;
        }
        return thisPosition;
    }
 */
    align(boids) {
        let perceptionRadius = 50;
        let steering = new THREE.Vector3();
        let total = 0;
        for (let other of boids) {
            if (other != this && this.calculateDistance(boids, this.position, other.position) < perceptionRadius) {
                steering.add(other.velocity);
                total++;
            }
        }
        if (total > 0) {
            steering.divideScalar(total);
            steering.setLength(this.maxSpeed);
            steering.sub(this.velocity);
            steering.clampScalar(0, this.maxForce);
        }
        return steering;
    }

    cohesion(boids){
        let perceptionRadius = 70;
        let steering = new THREE.Vector3();
        let total = 0;
        for (let other of boids) {
            if (other != this && this.calculateDistance(boids, this.position, other.position) < perceptionRadius) {
                steering.add(other.position);
                total++;
            }
        }
        if (total > 0) {
            steering.divideScalar(total);
            steering.sub(this.position);
            steering.setLength(this.maxSpeed);
            steering.sub(this.velocity);
            steering.clampScalar(0, this.maxForce);
        }
        return steering;
    }

    separation(boids){
        let perceptionRadius = 30;
        let steering = new THREE.Vector3();
        let total = 0;
        for (let other of boids) {
            let distance = this.calculateDistance(boids, this.position, other.position);
            if (other != this && distance < perceptionRadius) {
                let diff = other.position.sub(this.position);
                diff.divideScalar(distance * distance);
                steering.add(diff);
                total++;
            }
        }
        if (total > 0) {
            steering.divideScalar(total);
            steering.setLength(this.maxSpeed);
            steering.sub(this.velocity);
            steering.clampScalar(0, this.maxForce);
        }
        return steering;
    }

    calculateDistance(boids, position, otherPosition){
        for(let other of boids){
            let distance = position.distanceTo(otherPosition);
            return distance;
        }
    }

    flock(boids){
        let alignment = this.align(boids);
        let cohesion = this.cohesion(boids);
        let separation = this.separation(boids);

        this.acceleration.add(alignment);
        this.acceleration.add(cohesion);
        this.acceleration.add(separation);
    }

    update(){
        this.position.add(this.velocity);
        this.velocity.add(this.acceleration);
        this.velocity.clampLength(0, this.maxSpeed);
        this.acceleration.multiplyScalar(0);
    }

    show () {
        if (this.boidMesh) {
            this.boidMesh.position.copy(this.position);
            this.boidMesh.lookAt(this.position.clone().add(this.velocity)); // Richtung des Boids ausrichten
        }
    }
}

// Erstellung der Boids
for (let i = 0; i < 300; i++) {
    flock.push(new Boid());
}

// Eventlistener für Orbit-Steuerung
controls.addEventListener( "change", event => {  
    console.log( controls.object.position ); 
});

const size = 1000;
const divisions = 100;

const gridHelper = new THREE.GridHelper( size, divisions );
gridHelper.position.set(0, 0, 0);
scene.add( gridHelper );

// Animationsloop
function animate() {
    requestAnimationFrame(animate);

    for (let boid of flock) {
        boid.edges();
        boid.flock(flock);
        boid.update();
        boid.show();
    }

    controls.update();
    renderer.render(scene, camera);
}

// Starte die Animation
animate();

If I change the edge()-function to this:

        if (this.position.x > this.xBoundaries || this.position.x < 0) {
            this.velocity.x *= -1; // Invertiere die x-Richtung
            this.position.x = Math.min(Math.max(this.position.x, 0), this.xBoundaries);
        }
        if (this.position.y > this.yBoundaries || this.position.y < 0) {
            this.velocity.y *= -1; // Invertiere die y-Richtung
            this.position.y = Math.min(Math.max(this.position.y, 0), this.yBoundaries); 
        }
        if (this.position.z > this.zBoundaries || this.position.z < 0) {
            this.velocity.z *= -1; // Invertiere die z-Richtung
            this.position.z = Math.min(Math.max(this.position.z, 0), this.zBoundaries); 
        }

Then it looks like this:

Most likely you experience side effects while calculating vectors. For example, in method separation(boids) I believe you do not want to modify other.position inside the loop. However, it is modified by sub(...). Instead of this line:

let diff = other.position.sub(this.position);

try this:

let diff = new THREE.Vector3().subVectors(other.position,this.position);

If it is working, then you can optimize the code by defining the vector once and reusing it every time.

Sure.

2 Likes

Wow, thank you very much! It really solved the problem! :heart_hands:

1 Like