Calculating individual spheres position to create a sphere made of spheres

I have been facing the same problem, and also made a solution using Cannon.js. However, when rendering heavier elements this might cause a considerable load, especially on mobile.

I came up with an idea to capture the final position of the nucleons after they have settled and save that in a json file for all the elements.

Then the nucleons can be made to orbit the nucleus linearly without physics.


I would definitely say that this is a perfect use case of a physics engine. Making this simulation without a physics engine sounds like a real hassle, so "including an entire physics engine" doesn't seam like such a big cost to me. Most of the JavaScript physics engines that i've found are leight weight anyway. It will however demand some extra CPU power for the physics calculations!

I sat down and tried to create something similar to what you describe with the physics engine CANNON.js. It was quite easy to get a basic simulation working, but to get the parameters just right took is what seems a bit tricky, and will need more adjusting.

You mentioned that you tried this already but couldn't get the particles to gravitate towards a point, with CANNON.js (and probably most other physic engines) this can be achieved be applying a force to the object in the negative position direction:

function pullOrigin(body){
    body.force.set(
        -body.position.x,
        -body.position.y,
        -body.position.z
    );
}

It is also easy to achieve behaviours where bodies are pulled towards a certain parent object, which in its turn is pull towards the average position of all other parent objects. This way you can create whole molecules.

One tricky thing was to let the electrons circulate the protons and neutrons at a distance. To achieve this I give them a slight force towards the origin, and then a slight force away from all the protons and neutrons at the same time. On top of that I also give them a small push sideways in the beginning of the simulation so that they start circulating the center.

Please let me know if you want me to clarify any particular part.

let scene = new THREE.Scene();
let world = new CANNON.World();
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 5;

let camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

let renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

function Proton(){
	let radius = 1;

	return {
		// Cannon
		body: new CANNON.Body({
			mass: 1, // kg
			position: randomPosition(6),
			shape: new CANNON.Sphere(radius)
		}),
		// THREE
		mesh: new THREE.Mesh(
			new THREE.SphereGeometry( radius, 32, 32 ),
			new THREE.MeshPhongMaterial( { color: 0xdd5555, specular: 0x999999, shininess: 13} )
		)
	}
}

function Neutron(){
	let radius = 1;

	return {
		// Cannon
		body: new CANNON.Body({
			mass: 1, // kg
			position: randomPosition(6),
			shape: new CANNON.Sphere(radius)
		}),
		// THREE
		mesh: new THREE.Mesh(
			new THREE.SphereGeometry( radius, 32, 32 ),
			new THREE.MeshPhongMaterial( { color: 0x55dddd, specular: 0x999999, shininess: 13} )
		)
	}
}

function Electron(){
	let radius = 0.2;

	return {
		// Cannon
		body: new CANNON.Body({
			mass: 0.5, // kg
			position: randomPosition(10),
			shape: new CANNON.Sphere(radius)
		}),
		// THREE
		mesh: new THREE.Mesh(
			new THREE.SphereGeometry( radius, 32, 32 ),
			new THREE.MeshPhongMaterial( { color: 0xdddd55, specular: 0x999999, shininess: 13} )
		)
	}
}

function randomPosition(outerRadius){
	let x = (2 * Math.random() - 1 ) * outerRadius,
		y = (2 * Math.random() - 1 ) * outerRadius,
		z = (2 * Math.random() - 1 ) * outerRadius
	return new CANNON.Vec3(x, y, z);
}

function addToWorld(object){
	world.add(object.body);
	scene.add(object.mesh);
}

// create our Atom
let protons = Array(5).fill(0).map( () => Proton() );
let neutrons = Array(5).fill(0).map( () => Neutron() );
let electrons = Array(15).fill(0).map( () => Electron() );

protons.forEach(addToWorld);
neutrons.forEach(addToWorld);
electrons.forEach(addToWorld);


let light = new THREE.AmbientLight( 0x202020 ); // soft white light
scene.add( light );

let directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( -1, 1, 1 );
scene.add( directionalLight );

camera.position.z = 18;

const timeStep = 1/60;

//Small impulse on the electrons to get them moving in the start
electrons.forEach((electron) => {
	let centerDir = electron.body.position.vsub(new CANNON.Vec3(0, 0, 0));
	centerDir.normalize();
	let impulse = centerDir.cross(new CANNON.Vec3(0, 0, 1));
	impulse.scale(2, impulse);
	electron.body.applyLocalImpulse(impulse, new CANNON.Vec3(0, 0, 0));
});

function render () {
	requestAnimationFrame( render );

	// all particles pull towards the center
	protons.forEach(pullOrigin);
	neutrons.forEach(pullOrigin);
	electrons.forEach(pullOrigin);

	// electrons should also be pushed by protons and neutrons
	electrons.forEach( (electron) => {
		let pushForce = new CANNON.Vec3(0, 0, 0 );

		protons.forEach((proton) => {
			let f = electron.body.position.vsub(proton.body.position);
			pushForce.vadd(f, pushForce);
		});

		neutrons.forEach((neutron) => {
			let f = electron.body.position.vsub(neutron.body.position);
			pushForce.vadd(f, pushForce);
		});

		pushForce.scale(0.07, pushForce);
		electron.body.force.vadd(pushForce, electron.body.force);
	})

	// protons and neutrons slows down (like wind resistance)
	neutrons.forEach((neutron) => resistance(neutron, 0.95));
	protons.forEach((proton) => resistance(proton, 0.95));

	// Electrons have a max velocity
	electrons.forEach((electron) => {maxVelocity(electron, 5)});

	// Step the physics world
	world.step(timeStep);
	// Copy coordinates from Cannon.js to Three.js
	protons.forEach(updateMeshState);
	neutrons.forEach(updateMeshState);
	electrons.forEach(updateMeshState);

	renderer.render(scene, camera);
};

function updateMeshState(object){
	object.mesh.position.copy(object.body.position);
	object.mesh.quaternion.copy(object.body.quaternion);
}

function pullOrigin(object){
	object.body.force.set(
		-object.body.position.x,
		-object.body.position.y,
		-object.body.position.z
	);
}

function maxVelocity(object, vel){
	if(object.body.velocity.length() > vel)
		object.body.force.set(0, 0, 0);
}

function resistance(object, val) {
	if(object.body.velocity.length() > 0)
		object.body.velocity.scale(val, object.body.velocity);
}
render();
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.min.js"></script>

EDIT

I have modularized the particles into a Atom object that can be retrieved from the Atom function. Also added some more comments in the code if your unsure about anything. I would advise you to really study the code, and check the CANNON.js documentation (it is really thourogh). The force related stuff is in the Body class of Cannon.js. All i've done is to combine a THREE.Mesh and a CANNON.Body into a single object (for each particle). Then I simulate all movements on the CANNON.Body, and right before I render the THREE.Mesh, I copy the positions and rotations from CANNON.Body to THREE.Mesh.

This is the Atom function (changed some of the electron physics aswell):

function Atom(nProtons, nNeutrons, nElectrons, pos = new CANNON.Vec3(0, 0, 0)){

    //variable to move the atom, which att the particles will pull towards
    let position = pos;

    // create our Atom
    let protons = Array(nProtons).fill(0).map( () => Proton() );
    let neutrons = Array(nNeutrons).fill(0).map( () => Neutron() );
    let electrons = Array(nElectrons).fill(0).map( () => Electron() );

    // Public Functions
    //=================
    // add to a three.js and CANNON scene/world
    function addToWorld(world, scene) {
        protons.forEach((proton) => {
            world.add(proton.body);
            scene.add(proton.mesh);
        });
        neutrons.forEach((neutron) => {
            world.add(neutron.body);
            scene.add(neutron.mesh);
        });
        electrons.forEach((electron) => {
            world.add(electron.body);
            scene.add(electron.mesh);
        });
    }

    function simulate() {

        protons.forEach(pullParticle);
        neutrons.forEach(pullParticle);

        //pull electrons if they are further than 5 away
        electrons.forEach((electron) => { pullParticle(electron, 5) });
        //push electrons if they are closer than 6 away
        electrons.forEach((electron) => { pushParticle(electron, 6) });

        // give the particles some friction/wind resistance
        //electrons.forEach((electron) => resistance(electron, 0.95));
        neutrons.forEach((neutron) => resistance(neutron, 0.95));
        protons.forEach((proton) => resistance(proton, 0.95));

    }

    function electronStartingVelocity(vel) {
        electrons.forEach((electron) => {
            let centerDir = electron.body.position.vsub(position);
            centerDir.normalize();
            let impulse = centerDir.cross(new CANNON.Vec3(0, 0, 1));
            impulse.scale(vel, impulse);
            electron.body.applyLocalImpulse(impulse, new CANNON.Vec3(0, 0, 0));
        });
    }

    // Should be called after CANNON has simulated a frame and before THREE renders.
    function updateAtomMeshState(){
        protons.forEach(updateMeshState);
        neutrons.forEach(updateMeshState);
        electrons.forEach(updateMeshState);
    }


    // Private Functions
    // =================

    // pull a particale towards the atom position (if it is more than distance away)
    function pullParticle(particle, distance = 0){

        // if particle is close enough, dont pull more
        if(particle.body.position.distanceTo(position) < distance)
            return false;

        //create vector pointing from particle to atom position
        let pullForce = position.vsub(particle.body.position);

        // same as: particle.body.force = particle.body.force.vadd(pullForce)
        particle.body.force.vadd(   // add particle force
            pullForce,              // to pullForce
            particle.body.force);   // and put it in particle force
    }

    // Push a particle from the atom position (if it is less than distance away)
    function pushParticle(particle, distance = 0){

        // if particle is far enough, dont push more
        if(particle.body.position.distanceTo(position) > distance)
            return false;

        //create vector pointing from particle to atom position
        let pushForce = particle.body.position.vsub(position);

        particle.body.force.vadd(   // add particle force
            pushForce,              // to pushForce
            particle.body.force);   // and put it in particle force
    }

    // give a partile some friction
    function resistance(particle, val) {
        if(particle.body.velocity.length() > 0)
            particle.body.velocity.scale(val, particle.body.velocity);
    }

    // Call this on a particle if you want to limit its velocity
    function limitVelocity(particle, vel){
        if(particle.body.velocity.length() > vel)
            particle.body.force.set(0, 0, 0);
    }

    // copy ratation and position from CANNON to THREE
    function updateMeshState(particle){
        particle.mesh.position.copy(particle.body.position);
        particle.mesh.quaternion.copy(particle.body.quaternion);
    }


    // public API
    return {
        "simulate":                 simulate,
        "electrons":                electrons,
        "neutrons":                 neutrons,
        "protons":                  protons,
        "position":                 position,
        "updateAtomMeshState":      updateAtomMeshState,
        "electronStartingVelocity": electronStartingVelocity,
        "addToWorld":               addToWorld

    }
}

function Proton(){
    let radius = 1;

    return {
        // Cannon
        body: new CANNON.Body({
            mass: 1, // kg
            position: randomPosition(0, 6), // random pos from radius 0-6
            shape: new CANNON.Sphere(radius)
        }),
        // THREE
        mesh: new THREE.Mesh(
            new THREE.SphereGeometry( radius, 32, 32 ),
            new THREE.MeshPhongMaterial( { color: 0xdd5555, specular: 0x999999, shininess: 13} )
        )
    }
}

function Neutron(){
    let radius = 1;

    return {
        // Cannon
        body: new CANNON.Body({
            mass: 1, // kg
            position: randomPosition(0, 6), // random pos from radius 0-6
            shape: new CANNON.Sphere(radius)
        }),
        // THREE
        mesh: new THREE.Mesh(
            new THREE.SphereGeometry( radius, 32, 32 ),
            new THREE.MeshPhongMaterial( { color: 0x55dddd, specular: 0x999999, shininess: 13} )
        )
    }
}

function Electron(){
    let radius = 0.2;

    return {
        // Cannon
        body: new CANNON.Body({
            mass: 0.5, // kg
            position: randomPosition(3, 7), // random pos from radius 3-8
            shape: new CANNON.Sphere(radius)
        }),
        // THREE
        mesh: new THREE.Mesh(
            new THREE.SphereGeometry( radius, 32, 32 ),
            new THREE.MeshPhongMaterial( { color: 0xdddd55, specular: 0x999999, shininess: 13} )
        )
    }
}


function randomPosition(innerRadius, outerRadius){

    // get random direction
    let x = (2 * Math.random() - 1 ),
        y = (2 * Math.random() - 1 ),
        z = (2 * Math.random() - 1 )

    // create vector
    let randVec = new CANNON.Vec3(x, y, z);

    // normalize
    randVec.normalize();
    // scale it to the right radius
    randVec = randVec.scale( Math.random() * (outerRadius - innerRadius) + innerRadius); //from inner to outer
    return randVec;
}

And to use it:

let scene = new THREE.Scene();
let world = new CANNON.World();
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 5;

let camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

let renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// create a Atom with 3 protons and neutrons, and 5 electrons
// all circulating position (-4, 0, 0)
let atom = Atom(3, 3, 5, new CANNON.Vec3(-4, 0, 0));

// move atom (will not be instant)
//atom.position.x = -2;

// add to THREE scene and CANNON world
atom.addToWorld(world, scene);

let light = new THREE.AmbientLight( 0x202020 ); // soft white light
scene.add( light );

let directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( -1, 1, 1 );
scene.add( directionalLight );

camera.position.z = 18;

const timeStep = 1/60;

// give the atoms electrons some starting velocity
atom.electronStartingVelocity(2);

function render () {
    requestAnimationFrame( render );

    // calculate all the particles positions
    atom.simulate();

    // Step the physics world
    world.step(timeStep);

    //update the THREE mesh
    atom.updateAtomMeshState();

    renderer.render(scene, camera);
};


render();