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
(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:
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)
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)
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. 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.
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.
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.
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.
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();
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With