Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculating individual spheres position to create a sphere made of spheres

I'm trying to re-create an atom with THREE.js, and I'm running into my first issue - since every type of atom has a different amount of Protons/Neutrons, I'm trying to find a way to position them automatically so that there is no collisions, and so the final result of them all together will make something as close to a sphere as possible - see this image for an example

img
(source: alternativephysics.org)
.

Is there a way to calculate this and assign each Neutron/Protons position easily with a formula? Or will I have to get a physics engine involved to just squeeze the spheres together and hope for the best result with each run?

I don't have any code on this yet, since I'm just trying to figure out where to start with this part.

EDIT

I should also note, that I want the spheres to be squished together within the space of the larger sphere. I am NOT trying to just make all the spheres go on the radius of the larger sphere.

EDIT 2

I looked into using a physics engine to squish them all into a small area, but I can't find an engine that will allow me to move all of the objects in my scene to position (0,0,0) with a gravitational force. All of the engines just make gravity push down on an object. I'd still rather use a formula for positioning the spheres, rather than include an entire physics engine into my project.

EDIT 3, 04/06/06

I've done a bit of experimenting, but I still can't get it right. Here's what it looks like now:

enter image description here

But as you can see, looks really off. This is what happens when I make a Uranium atom instead of a Carbon one (more protons/neutrons/electrons)

enter image description here

It might just be me, but that's looking more like some fancy ratatouille than a Uranium atom.

How I got here:

I was attempting to make what I was looking for up above, and here's the premise:

(particleObject is the parent of particle, the particle will move relative to this object)

  1. I added all protons and neutrons lengths together, so that I could loop through them all.
  2. If the added number % 2 == 0, (which it is for my testing) I would set the rotate to (pi * 2) / 2 <- last two being there to represent the two above.
  3. Every iteration I would increment l variable. (hopefully) whenever i would equal the loopcount variable, it would mean that I've placed sphere's around in a sphere shape. I'd then multiply loopcount by 3 to find out how many sphere's would be needed for the next run. I'd set l to 0 so that the sphere's positioning would be reset, and the loop would be incremented, causing the next row of sphere's to be placed 1 unit out on the x axis.

(Sorry for the terminology here, it's very hard to explain. See code.)

    var PNamount = atomTypes[type].protons + atomTypes[type].neutrons;
    var loopcount = 1;
    if(PNamount % 2 == 0) {
        var rotate = (PI * 2) / 2;
        loopcount = 2;
    }
    var neutrons = 0,
        protons = 0,
        loop = 1,
        l = 0;
    for(var i = 0; i < PNamount; i++) {

        if(i == loopcount){
            loopcount = loopcount * 3;
            loop++;
            rotate = (PI * 2) / loopcount;
            l = 0;
        } else {
            l++;
        }

        particleObject.rotation.x = rotate * l;
        particleObject.rotation.y = rotate * l;
        particleObject.rotation.z = rotate * l;
        particle.position.x = loop;
    }

Honestly, I'm not that great at all with 3D math. So any help would be really helpful. Plus, it's very possible that my method of positioning them is absolutely wrong in every way. Thanks!

You can see the code live here.

like image 569
Darryl Huffman Avatar asked Mar 30 '16 19:03

Darryl Huffman


People also ask

What is the formula for spheres?

The formula for the volume of a sphere is V = 4/3 πr³. See the formula used in an example where we are given the diameter of the sphere. Created by Sal Khan and Monterey Institute for Technology and Education.

How do you determine if a sphere is in another sphere?

If you want to visualize why, draw the limiting case when the smallest sphere is tangential to the biggest sphere from within. That is when dist(C,c)=R−r. dist(C,c)≤R−r implies spheres are inside one another.

How do you calculate the area of a portion of a sphere?

We have a section of the sphere with height h. If we slice it into n slices, each with height h/n, then the surface area of each one will be about 2πrh/n, so in total, the area will be about n×2πrh/n=2πrh.


2 Answers

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();
like image 78
micnil Avatar answered Nov 03 '22 01:11

micnil


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.

like image 24
Freeman L Avatar answered Nov 02 '22 23:11

Freeman L