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)
I added all protons and neutrons lengths together, so that I could
loop through them all.
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.
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.
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.
one solution would be to use the icosphere algorithm to calculate the position of a Neutrons/Protons using the vertex point of generated sphere.
You can find ad usefoul algorithm here
the distance between the points remains equal over the entire surface
Related
I want to create a grid of, let's say 500x500, and I want it to be able to be curved in certain places (think of it as space-time plane that's curved due to gravity). I'm stuck at the start.
This is what I found on the documentation:
// Create a sine-like wave
const curve = new THREE.SplineCurve( [
new THREE.Vector2( -10, 0 ),
new THREE.Vector2( -5, 5 ),
new THREE.Vector2( 0, 0 ),
new THREE.Vector2( 5, -5 ),
new THREE.Vector2( 10, 0 )
] );
const points = curve.getPoints( 50 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color : 0xff0000 } );
// Create the final object to add to the scene
const splineObject = new THREE.Line( geometry, material );
I think this should be working but it doesn't. I don't know how to create a multiple lines from here. I tried to handle some array but I didn't know where or how. I have done my research but I can't make any headway.
I have another question: Is that this is a Vector2 I need to make it 3d for that work? There a lot of other classes like CatmullRomCurve3, CubicBezierCurve3, etc, but the problem still is that i need to make an array to create all the vectors and lines.
--
EDIT i created a code for the grid lines but the curve probleme still the same
let size = 12, step = 1;
const geometry = new THREE.BufferGeometry();
// create a simple square shape. We duplicate the top left and bottom right
// vertices because each vertex needs to appear once per triangle.
const vertices = [];
for(var i = - size; i <= size; i += step) {
vertices.push( - size, - 0.4, i);
vertices.push( size, - 0.4, i);
vertices.push( i, - 0.4, - size);
vertices.push( i, - 0.4, size);
}
let positionAttribute = new THREE.Float32BufferAttribute(vertices, 3);
geometry.setAttribute("position", positionAttribute);
let lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial());
I wanted to make a "Thick Arrow" mesh i.e. an arrow like the standard Arrow Helper but with the shaft made out of a cylinder instead of a line.
tldr; do not copy the Arrow Helper design; see the Epilogue section at end of the question.
So I copied and modified the code for my needs (dispensed with constructor and methods) and made the changes and now it works OK:-
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//... START of ARROWMAKER SET of FUNCTIONS
// adapted from https://github.com/mrdoob/three.js/blob/master/src/helpers/ArrowHelper.js
//====================================
function F_Arrow_Fat_noDoesLookAt_Make ( dir, origin, length, shaftBaseWidth, shaftTopWidth, color, headLength, headBaseWidth, headTopWidth )
{
//... dir is assumed to be normalized
var thisArrow = new THREE.Object3D();////SW
if ( dir === undefined ) dir = new THREE.Vector3( 0, 0, 1 );
if ( origin === undefined ) origin = new THREE.Vector3( 0, 0, 0 );
if ( length === undefined ) length = 1;
if ( shaftBaseWidth === undefined ) shaftBaseWidth = 0.02 * length;
if ( shaftTopWidth === undefined ) shaftTopWidth = 0.02 * length;
if ( color === undefined ) color = 0xffff00;
if ( headLength === undefined ) headLength = 0.2 * length;
if ( headBaseWidth === undefined ) headBaseWidth = 0.4 * headLength;
if ( headTopWidth === undefined ) headTopWidth = 0.2 * headLength;//... 0.0 for a point.
/* CylinderBufferGeometry parameters from:-
// https://threejs.org/docs/index.html#api/en/geometries/CylinderBufferGeometry
* radiusTop — Radius of the cylinder at the top. Default is 1.
* radiusBottom — Radius of the cylinder at the bottom. Default is 1.
* height — Height of the cylinder. Default is 1.
* radialSegments — Number of segmented faces around the circumference of the cylinder. Default is 8
* heightSegments — Number of rows of faces along the height of the cylinder. Default is 1.
* openEnded — A Boolean indicating whether the ends of the cylinder are open or capped. Default is false, meaning capped.
* thetaStart — Start angle for first segment, default = 0 (three o'clock position).
* thetaLength — The central angle, often called theta, of the circular sector. The default is 2*Pi, which makes for a complete cylinder.
*/
//var shaftGeometry = new THREE.CylinderBufferGeometry( 0.0, 0.5, 1, 8, 1 );//for strongly tapering, pointed shaft
var shaftGeometry = new THREE.CylinderBufferGeometry( 0.1, 0.1, 1, 8, 1 );//shaft is cylindrical
//shaftGeometry.translate( 0, - 0.5, 0 );
shaftGeometry.translate( 0, + 0.5, 0 );
//... for partial doesLookAt capability
//shaftGeometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
var headGeometry = new THREE.CylinderBufferGeometry( 0, 0.5, 1, 5, 1 ); //for strongly tapering, pointed head
headGeometry.translate( 0, - 0.5, 0 );
//... for partial doesLookAt capability
//headGeometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
thisArrow.position.copy( origin );
/*thisArrow.line = new Line( _lineGeometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
thisArrow.line.matrixAutoUpdate = false;
thisArrow.add( thisArrow.line ); */
thisArrow.shaft = new THREE.Mesh( shaftGeometry, new THREE.MeshLambertMaterial( { color: color } ) );
thisArrow.shaft.matrixAutoUpdate = false;
thisArrow.add( thisArrow.shaft );
thisArrow.head = new THREE.Mesh( headGeometry, new THREE.MeshLambertMaterial( { color: color } ) );
thisArrow.head.matrixAutoUpdate = false;
thisArrow.add( thisArrow.head );
//thisArrow.setDirection( dir );
//thisArrow.setLength( length, headLength, headTopWidth );
var arkle = new THREE.AxesHelper (2 * length);
thisArrow.add (arkle);
F_Arrow_Fat_noDoesLookAt_setDirection( thisArrow, dir ) ;////SW
F_Arrow_Fat_noDoesLookAt_setLength ( thisArrow, length, headLength, headBaseWidth ) ;////SW
F_Arrow_Fat_noDoesLookAt_setColor ( thisArrow, color ) ;////SW
scene.add ( thisArrow );
//... this screws up for the F_Arrow_Fat_noDoesLookAt kind of Arrow
//thisArrow.lookAt(0,0,0);//...makes the arrow's blue Z axis lookAt Point(x,y,z).
}
//... EOFn F_Arrow_Fat_noDoesLookAt_Make().
//=============================================
function F_Arrow_Fat_noDoesLookAt_setDirection( thisArrow, dir )
{
// dir is assumed to be normalized
if ( dir.y > 0.99999 )
{
thisArrow.quaternion.set( 0, 0, 0, 1 );
} else if ( dir.y < - 0.99999 )
{
thisArrow.quaternion.set( 1, 0, 0, 0 );
} else
{
const _axis = /*#__PURE__*/ new THREE.Vector3();
_axis.set( dir.z, 0, - dir.x ).normalize();
const radians = Math.acos( dir.y );
thisArrow.quaternion.setFromAxisAngle( _axis, radians );
}
}
//... EOFn F_Arrow_Fat_noDoesLookAt_setDirection().
//=========================================
function F_Arrow_Fat_noDoesLookAt_setLength( thisArrow, length, headLength, headBaseWidth )
{
if ( headLength === undefined ) headLength = 0.2 * length;
if ( headBaseWidth === undefined ) headBaseWidth = 0.2 * headLength;
thisArrow.shaft.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458
//x&z the same, y as per length-headLength
//thisArrow.shaft.position.y = length;//SW ???????
thisArrow.shaft.updateMatrix();
thisArrow.head.scale.set( headBaseWidth, headLength, headBaseWidth ); //x&z the same, y as per length
thisArrow.head.position.y = length;
thisArrow.head.updateMatrix();
}
//...EOFn F_Arrow_Fat_noDoesLookAt_setLength().
//========================================
function F_Arrow_Fat_noDoesLookAt_setColor( thisArrow, color )
{
thisArrow.shaft.material.color.set( color );
thisArrow.head.material.color.set( color );
}
//...EOFn F_Arrow_Fat_noDoesLookAt_setColor().
//... END of ARROWMAKER SET of FUNCTIONS
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
This works OK for a fixed-direction arrow where the arrow direction can be supplied at time of construction.
But now I need to change the arrow orientation over time (for tracking a moving target). Currently the Object3D.lookAt() function is not sufficient because the arrow points along its Object3D y-axis, whereas lookAt() orients the Object3D z-axis to look at the given target position.
With experimentation I have gotten part-way there by using:-
geometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
on the shaft and head geometries (the 2 lines are commented out in the above code extract). This seems to get the cylinder meshes pointing in the correct direction. But the problem is that the meshes are mis-shaped and the head mesh is displaced away from the shaft mesh.
With trial and error I might be able to adjust the code to get the arrow to work for my present example. But (given my weak understanding of quaternions) I am not confident that it would (a) be general enough to apply in all situations or (b) be sufficiently future-proof against evolution of THREE.js.
So I would be grateful for any solutions/recommendations on how to achieve the lookAt() capability for this "Thick Arrow".
Epilogue
My main takeaway is NOT to follow the design of the Helper Arrow.
As TheJim01's and somethinghere's answers indicate, there is an easier approach using the Object3D.add() "nesting" function.
For example:-
(1) create two cylinder meshes (for arrowshaft and arrowhead) which by default will point in the Y-direction; make geometry length =1.0 to assist future re-scaling.
(2) Add the meshes to a parent Object3D object.
(3) Rotate the parent +90 degrees around the X-axis using parent.rotateX(Math.PI/2).
(4) Add the parent to a grandparent object.
(5) Subsequently use grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z).
N.B. lookAt() will not work properly if parent or grandparent have scaling other than (n,n,n).
The parent and grandparent object types may be plain THREE.Object3D, or THREE.Group, or THREE.Mesh (made invisible if required e.g. by setting small dimensions or .visibility=false)
Arrow Helper can be used dynamically but only if the internal direction is set to (0,0,1) before using lookAt().
You can apply lookAt to any Object3D. Object3D.lookAt( ... )
You have already discovered that lookAt causes the shapes to point in the +Z direction, and are compensating for that. But it can be taken a step further with the introduction of a Group. Groups are also derived from Object3D, so they also support the lookAt method.
let W = window.innerWidth;
let H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(10, 10, 50);
camera.lookAt(scene.position);
scene.add(camera);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);
const group = new THREE.Group();
scene.add(group);
const arrowMat = new THREE.MeshLambertMaterial({color:"green"});
const arrowGeo = new THREE.ConeBufferGeometry(2, 5, 32);
const arrowMesh = new THREE.Mesh(arrowGeo, arrowMat);
arrowMesh.rotation.x = Math.PI / 2;
arrowMesh.position.z = 2.5;
group.add(arrowMesh);
const cylinderGeo = new THREE.CylinderBufferGeometry(1, 1, 5, 32);
const cylinderMesh = new THREE.Mesh(cylinderGeo, arrowMat);
cylinderMesh.rotation.x = Math.PI / 2;
cylinderMesh.position.z = -2.5;
group.add(cylinderMesh);
function render() {
renderer.render(scene, camera);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
render();
}
window.addEventListener("resize", resize);
resize();
let rad = 0;
function animate() {
rad += 0.05;
group.lookAt(Math.sin(rad) * 100, Math.cos(rad) * 100, 100);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>
The key here is that the cone/shaft are made to point in the +Z direction, and then added to the Group. This means their orientations are now local to the group. When the group's lookAt changes, the shapes follow suit. And because the "arrow" shapes point in the group's local +Z direction, that means they also point at whatever position was given to group.lookAt(...);.
Further work
This is just a starting point. You'll need to adapt this to how you want it to work with constructing the arrow at the correct position, with the correct length, etc. Still, the grouping pattern should make lookAt easier to work with.
All you require is some more understanding of nesting, which allows you to place objects relative to their parents. As mentioned in the answer above, you could use Group or Object3D, but you don't have to. You can just nest your arrowhead on your cylinder and point your cylinder into the z-direction, then use the built-in, dont-overcomplicate-things methods lookAt.
Try not to use matrices or quaternions for simple things like this, as it makes for a way harder time figuring things out. Since THREE.js allows for nested frames, make use of that!
const renderer = new THREE.WebGLRenderer;
const camera = new THREE.PerspectiveCamera;
const scene = new THREE.Scene;
const mouse = new THREE.Vector2;
const raycaster = new THREE.Raycaster;
const quaternion = new THREE.Quaternion;
const sphere = new THREE.Mesh(
new THREE.SphereGeometry( 10, 10, 10 ),
new THREE.MeshBasicMaterial({ transparent: true, opacity: .1 })
);
const arrow = new THREE.Group;
const arrowShaft = new THREE.Mesh(
// We want to ensure our arrow is completely offset into one direction
// So the translation ensure every bit of it is in Y+
new THREE.CylinderGeometry( .1, .3, 3 ).translate( 0, 1.5, 0 ),
new THREE.MeshBasicMaterial({ color: 'blue' })
);
const arrowPoint = new THREE.Mesh(
// Same thing, translate to all vertices or +Y
new THREE.ConeGeometry( 1, 2, 10 ).translate( 0, 1, 0 ),
new THREE.MeshBasicMaterial({ color: 'red' })
);
const trackerPoint = new THREE.Mesh(
new THREE.SphereGeometry( .2 ),
new THREE.MeshBasicMaterial({ color: 'green' })
);
const clickerPoint = new THREE.Mesh(
trackerPoint.geometry,
new THREE.MeshBasicMaterial({ color: 'yellow' })
);
camera.position.set( 10, 10, 10 );
camera.lookAt( scene.position );
// Place the point at the top of the shaft
arrowPoint.position.y = 3;
// Point the shaft into the z-direction
arrowShaft.rotation.x = Math.PI / 2;
// Attach the point to the shaft
arrowShaft.add( arrowPoint );
// Add the shaft to the global arrow group
arrow.add( arrowShaft );
// Add the arrow to the scene
scene.add( arrow );
scene.add( sphere );
scene.add( trackerPoint );
scene.add( clickerPoint );
renderer.domElement.addEventListener( 'mousemove', mouseMove );
renderer.domElement.addEventListener( 'click', mouseClick );
renderer.domElement.addEventListener( 'wheel', mouseWheel );
render();
document.body.appendChild( renderer.domElement );
function render(){
renderer.setSize( innerWidth, innerHeight );
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.render( scene, camera );
}
function mouseMove( event ){
mouse.set(
event.clientX / event.target.clientWidth * 2 - 1,
-event.clientY / event.target.clientHeight * 2 + 1
);
raycaster.setFromCamera( mouse, camera );
const hit = raycaster.intersectObject( sphere ).shift();
if( hit ){
trackerPoint.position.copy( hit.point );
render();
}
document.body.classList.toggle( 'tracking', !!hit );
}
function mouseClick( event ){
clickerPoint.position.copy( trackerPoint.position );
arrow.lookAt( trackerPoint.position );
render();
}
function mouseWheel( event ){
const angle = Math.PI * event.wheelDeltaX / innerWidth;
camera.position.applyQuaternion(
quaternion.setFromAxisAngle( scene.up, angle )
);
camera.lookAt( scene.position );
render();
}
body { padding: 0; margin: 0; }
body.tracking { cursor: none; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"></script>
You can wheel around using your mouse (if it has horizontal scroll, should be on trackpads) and click to point the arrow. I also added some tracking points so you can see that `lookAt' does work without overcomplicating it, and that is is pointing at the point you clicked on the wrapping sphere.
And with that, I definitely typed the word shaft too often. It's starting to sound weird.
I am trying to draw a curved road in three.js from some beziers I get with previous calculations, the problem is that I can't find the way to convert the sequence of curved lines (one starting at the end of the previous one) to a curved plane.
I have a 3D scene where there are some cars, a road created with a plane and the path of the coming road is painted. I use that Bezier curves I said to represent the path as a Line with
function createAdasisBezier(initx, inity, cp1x, cp1y, cp2x, cp2y, finalx, finaly) {
bezier = new THREE.CubicBezierCurve3(
new THREE.Vector3(initx, inity, 0),
new THREE.Vector3(cp1x, cp1y, 0),
new THREE.Vector3( cp2x, cp2y, 0),
new THREE.Vector3(finalx, finaly, 0)
);
curvePath = new THREE.CurvePath();
curvePath.add(bezier);
var geoPath = curvePath.createPointsGeometry( 5 );
var lineMat = new THREE.LineBasicMaterial({color: 0xff0000});
curveLine = new THREE.Line(geoPath, lineMat);
curveLine.rotation.set(-Math.PI/2,0,0);
curveLine.position.y = 0.1;
scene.add(curveLine);
}
First, I tried extruding the line, but then I realized that it might not be the solution because I wanted to do a road, and although I could move top vertices on X and Y to place them near the bezier in order to be the external part of the curve, the result was not only unfavourable, it also made impossible to preserve a relation between a left and a right curve.
To move vertices (once identified) I did a loop and move them manually:
for (var i = 0; i < geoPath.vertices.length; ++i) {
geoPath.vertices[i].y += 10;
}
Bevel is not enabled in the extude.
Then I tried to draw a plane over each bezier (as a child of them) and rotate it to face the path, but the result was not as I expected, and it if it were, it would spoil the arcs of the curves.
To do it, I created a copy of every bezier, and place it aside the original ones, then I created the plane.
var plane = new THREE.PlaneBufferGeometry(10,25,1,1);
var planemesh = new THREE.Mesh(plane, material);
planemesh.position.set(copy.geometry.vertices[0].x, copy.geometry.vertices[0].y, 0);
Last thing I was trying to do is creating a clone of the line, separate it some meters and "connect" the first vertex from one, to the first of the other, so I get a closed geometry, and I can create a Face, but I don't find how to "connect" vertices from 2 different geometries. I tried adding the vertex from one to the other, but it did not work.
Does anybody have an idea how could I convert the line into a curved road? Thanks in adcance.
You should try looking at the Geometry > Extrude > Shapes example. As you can see, all extruded shapes maintain their width and direction, despite turning left/right or looping completely.
Instead of using bezier curves, they're using a CatmullRomCurve3 to define the extrusion. If you look at the source code, the essential code to make the red extruded shape begins in line 69:
// Define the curve
var closedSpline = new THREE.CatmullRomCurve3( [
new THREE.Vector3( -60, -100, 60 ),
new THREE.Vector3( -60, 20, 60 ),
new THREE.Vector3( -60, 120, 60 ),
new THREE.Vector3( 60, 20, -60 ),
new THREE.Vector3( 60, -100, -60 )
] );
closedSpline.type = 'catmullrom';
closedSpline.closed = true;
// Set up settings for later extrusion
var extrudeSettings = {
steps : 100,
bevelEnabled : false,
extrudePath : closedSpline
};
// Define a triangle
var pts = [], count = 3;
for ( var i = 0; i < count; i ++ ) {
var l = 20;
var a = 2 * i / count * Math.PI;
pts.push( new THREE.Vector2 ( Math.cos( a ) * l, Math.sin( a ) * l ) );
}
var shape = new THREE.Shape( pts );
// Extrude the triangle along the CatmullRom curve
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
var material = new THREE.MeshLambertMaterial( { color: 0xb00000, wireframe: false } );
// Create mesh with the resulting geometry
var mesh = new THREE.Mesh( geometry, material );
From here, it should only be a matter of small tweaks to these parameters to get the specific road shape you want.
So I'm working with Three.js and jQuery to create a small visual application. At the moment all I want is for all the meshes I have, to appear on screen.
The Problem: None of the meshes appear on screen whatsoever.
Exceptions: The renderer's clear color appears (0x00bfff) and console.log(scene) confirms that all the meshes are in the scene.
Attempts to Fix: Use THREE.Projector, THREE.Raycaster, change camera positioning, and many more attempts.
I'm still very new to Three.js and programming in general so please be very critical of my work. Anything helps! Thanks!
WORLD.JS
$(document).ready(function() {
initialize();
animate();
});
var initialize = function() {
clock = new THREE.Clock(); // timer used to calculate time between rendering frames
scene = new THREE.Scene(); // list of objects that are to be "read" (rendered)
camera = new THREE.PerspectiveCamera(35, // FOV
window.innerWidth / window.innerHeight, // Aspect Ratio
.1, // Near
10000); // Far
camera.position.set( 25, 25, 125 );
camera.lookAt( scene.position );
setupEnvironment();
setupAI();
renderer = new THREE.WebGLRenderer(); // renderer will draw as WebGL rather than HTML5 Canvas
renderer.setSize( window.innerWidth, window.innerHeight ); // size of the canvas that renderer will draw on
renderer.setClearColor( 0x00bfff, 1 );
document.body.appendChild( renderer.domElement ); // adds the canvas to the document
};
var animate = function() { // animates the scene with frames
requestAnimationFrame(animate); // works recursively
render(); // update and display
}
var render = function() {
var delta = clock.getDelta() // gets the seconds passed since the last call to this method
// AI collision needed
// AI update needed
renderer.render( scene, camera ) // repaint
}
var setupEnvironment = function() {
ground = new BoxMesh( 10, 0.1, 10, 0x6C4319, 1 );
positionThenAdd( ground, [[ 0, 0 ]] );
light1 = new THREE.PointLight( 0xFFFFFF, .5 );
light1.position.set( 10, 10, 10 );
scene.add( light1 );
light2 = new THREE.PointLight( 0xFFFFFF, 1 );
light2.position.set( -10, -10, 10 );
scene.add( light2 );
};
var setupAI = function() {
sheep = new BoxMesh( 1, 1, 1, 0xFFFFFF, 3 );
positionThenAdd( sheep, [[ 0, 0 ],
[ 4.5, 0 ],
[ 9.5, 0 ]]);
sheepHerder = new BoxMesh( 1, 1, 1, 0x996633, 1 );
positionThenAdd( sheepHerder, [[ 4.5, 7.5 ]] );
};
function BoxMesh( width, height, depth, hexColor, amount ) { // creates one or more box meshes
this.width = width;
this.height = height;
this.depth = depth;
this.hexColor = hexColor;
this.amount = amount; // amount of box meshes to be made
boxSize = new THREE.BoxGeometry( width, height, depth );
boxMaterial = new THREE.MeshLambertMaterial( { color: hexColor } );
var all = []; // will contain all of the box meshes
for(var n = 1; n <= amount; n++) { // adds a new box mesh to the end of the all array
all.push(new THREE.Mesh( boxSize, boxMaterial )); // uses the attributes given by the BoxMesh constructor's parameters
}
return all; // returns all of the created box meshes as an array;
}
var positionThenAdd = function( varMesh, posArrXByZ ) { // positions an object and then adds it to the scene
this.varMesh = varMesh; // variable name of the mesh(es) array
this.posArrXByZ = posArrXByZ; // posArrXByZ stands for "array of positions in the format of X-by-Z"
// posArrXByZ is a 2 dimensional array where the first dimension is for the specific mesh to be positioned...
// and the second dimension is the positional coordinates.
// posArrXByZ = [ [x0,z0], [x1,z1], ...[xn,zn] ]
for(var mesh = 0; mesh < varMesh.length; mesh++) { // mesh accesses the varMesh array
varMesh[mesh].position.set( varMesh[mesh].geometry.parameters.width/2 + posArrXByZ[mesh][0], // the x coordinate, varMesh[mesh].width/2 makes the x coordinate act upon the closest side
varMesh[mesh].geometry.parameters.height/2 + ground.height, // the y coordinate, which is pre-set to rest on top of the ground
varMesh[mesh].geometry.parameters.depth/2 + posArrXByZ[mesh][1] ); // the z coordinate, varMesh[mesh].height/2 makes the y coordinate act upon the closest side
scene.add( varMesh[mesh] ); // adds the specific mesh that was just positioned
}
};
HTML FILE
<!DOCTYPE html>
<html>
<head>
<title>Taro's World</title>
<style>
body {
margin: 0;
padding: 0;
border: 0;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="mrdoob-three.js-d6384d2/build/Three.js"></script>
<script src="mrdoob-three.js-d6384d2/examples/js/renderers/Projector.js"></script>
<script src="world.js"></script>
</head>
<body></body>
</html>
Two things are broken in your script :
in your positionThenAdd function, at position.set(...), you wrote somewhere ground.height. ground is an array, you probably meant varMesh[mesh].geometry.parameters.height.
your console should print that positionThenAdd is not a function. While you declared previous functions writing function myFunction(){....} you declared this one that way : var positionThenAdd = function () { ... };. The difference in javascript is that, as any variable, positionThenAdd will then be reachable in the script order. Since you write it at the end, nothing can reach it. You just have to modify its declaration to function positionThenAdd(){...}. See var functionName = function() {} vs function functionName() {}
Your scene : http://jsfiddle.net/ba8vvkyg/1/
Hi folks,
I've got a question belongig surfaces in Three.js:
I got a bunch of Vec3 Points and want want to interpolate a surface through them. While searching, I stumbeled across beziers (three.js bezier - only as lines) and what looked more like I was searching : three.js Nurbs. I've tried to reconstruct the code, but the documentation was terrible (pages like this) and I didn't get how everything worked by reconstructing the code...
So here's the question:
Is there any easy way to get a shape out of my calculated points? (I would still be happy, if it's not interpolated).
Thank you guys!
Mat
Edit: What I want to acchieve is a surface plot. I stumbeled across http://acko.net/blog/making-mathbox/ but it's way too big for my needs...
After some try and error I found a solution: add a plane and than transform the single vertices.
// need to setup 'step', 'xStart', 'xEnd', 'yStart', 'yEnd'
// calc the variables
var width = Math.abs(-xStart+xEnd),
height = Math.abs(-yStart+yEnd);
var stepsX = width*step, stepsY = height*step;
var posX = (xStart+xEnd)/2;
var posZ = (yStart+yEnd)/2;
// add a plane and morph it to a function
var geometry = new THREE.PlaneGeometry( width, height, stepsX - 1, stepsY - 1 );
geometry.applyMatrix( new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) );
var size = stepsX * (stepsY),
data = new Float32Array( size );
var count = 0, scope = {};
mesh = new THREE.Mesh( geometry, new THREE.MeshNormalMaterial( {
side : THREE.DoubleSide,
transparent: true,
shading: THREE.SmoothShading,
opacity : _opacity }));
mesh.updateMatrixWorld();
// calc y value for every vertice
for ( var i = 0; i < size; i ++ ) {
// calculate the current values
// http://stackoverflow.com/questions/11495089/how-to-get-the-absolute-position-of-a-vertex-in-three-js
var vector = mesh.geometry.vertices[i].clone();
vector.applyMatrix4(
mesh.matrixWorld
);
// set them into the scope
scope.x = vector.x + posX;
scope.y = vector.z + posZ;
// calculate point and write it in a temp array
data[i] = math.eval(term, scope);
}
// push the new vertice data
for ( var i = 0, l = geometry.vertices.length; i < l; i ++ ) {
geometry.vertices[ i ].y = data[ i ];
}
// update the new normals
geometry.computeFaceNormals();
geometry.computeVertexNormals();
// add to scene
scene.add( mesh );
Only issue is that it is not working for non static functions like tan(x). This snippet is using math.js to calc the term.
Greetings Mat