I am currently writing my first Three.js / WebGL application and it runs very well on my PC (Chrome). Sadly, on many other PCs, the framerate often drops bellow 30 frames per second.
Since the application actually isn't to complex, I wanted to ask for some tips related to the application to improve the performance.
A version of the app can be found here:
www.wrodewald.de/StackOverflowExample/
The application contains a dynamic (morphing) plane using 64² vertices. A matrix is used to store a static heightmap and a wavemap. The wavemap is updated every frame to recalculate itself, some filters are used to "even out" every vertex compared to their neightbors.
So every frame the plane has to be updated (colors and vertex position) which could be a reason for the performance problem
The second object (rhombus) shouldn't be a problem, static, moving around a bit but nothing special.
There are three lights (ambient, directional, spherical), no shadows, a tilt shift shader and a vignette shader.
Here are functions which are called per frame:
var render = function() {
requestAnimationFrame( render );
var time = clock.getDelta();
world.updateWorld(time);
diamond.rotate(time);
diamond.update(time);
control.updateCamera(camera, time);
composer.render();
stats.update();
}
this is what world.updateWorld(time) does
//in world.updateWorld(time) where
// accmap: stores acceleration and wavemap stores position
// this.mapSize stores the size of the plane in vertices (64)
// UPDATE ACCELERATION MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
for(var iY = 1; iY < (this.mapSize-1); iY++) {
accmap[iX][iY] -= dT * (wavemap[iX][iY]) * Math.abs(wavemap[iX][iY]);
}
}
// SMOOTH ACCELERATION MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
for(var iY = 1; iY < (this.mapSize-1); iY++) {
tempmap[iX][iY] = accmap[iX-1][iY-1] * 0.0625
+ accmap[iX-1][iY ] * 0.125
+ accmap[iX-1][iY+1] * 0.0625
+ accmap[iX ][iY-1] * 0.125
+ accmap[iX ][iY ] * 0.25
+ accmap[iX ][iY+1] * 0.125
+ accmap[iX+1][iY-1] * 0.0625
+ accmap[iX+1][iY ] * 0.125
+ accmap[iX+1][iY+1] * 0.0625;
accmap[iX][iY] = tempmap[iX][iY];
}
}
// UPDATE WAVE MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
for(var iY = 1; iY < (this.mapSize-1); iY++) {
wavemap[iX][iY] += dT * accmap[iX][iY];
}
}
for(var i = 0; i < this.mapSize; i++) {
for(var k = 0; k < this.mapSize; k++) {
geometry.vertices[ i * this.mapSize + k ].y = wavemap[i][k] + heightmap[i][k];
}
}
for(var i = 0; i < geometry.faces.length; i++) {
var vertexA = geometry.vertices[geometry.faces[i].a];
var vertexB = geometry.vertices[geometry.faces[i].b];
var vertexC = geometry.vertices[geometry.faces[i].c];
var val = (vertexA.y + vertexB.y + vertexC.y) / 3.0;
val = (val / 200.) + 0.5;
geometry.faces[i].color.r = val;
geometry.faces[i].color.g = val;
geometry.faces[i].color.b = val;
}
geometry.colorsNeedUpdate = true;
geometry.verticesNeedUpdate = true;
These are the "diamond"-functions
this.rotate = function(dT) {
counter += 0.5 * dT;
counter % 1;
var x = 0.0025 * (Math.sin((counter) * 2 * Math.PI));
var y = 0.0025 * (Math.cos((counter) * 2 * Math.PI));
this.mesh.rotateOnAxis(new THREE.Vector3(1,0,0), x);
this.mesh.rotateOnAxis(new THREE.Vector3(0,0,1), y);
}
this.update = function(dT) {
for(var i = 0; i < geometry.faces.length; i++) {
geometry.faces[i].color.lerp(color, dT*(0.9));
}
geometry.colorsNeedUpdate = true;
}
Do you spot any reason for the framerate to be so inconsistent?
EDIT:
I have found 2 major things you have to improve:
Planes updates with GPU
speedup: high
Lets pick your code from plane.js
timer += dT;
if(timer > 0.1) {
var x = 2 + Math.floor(Math.random() * (this.mapSize - 4));
var y = 2 + Math.floor(Math.random() * (this.mapSize - 4));
//accmap[x][y] += 30000 * Math.random () - 15000
}
// UPDATE ACCELERATION MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
for(var iY = 1; iY < (this.mapSize-1); iY++) {
accmap[iX][iY] -= dT * (wavemap[iX][iY]) * Math.abs(wavemap[iX][iY]);
}
}
So you have 4096 vertices you would like to update every 17 ms with CPU. Notice you didnt use any GPU advantage. How it should be done:
First you create buffers, for vertex position, for normals, for texture cordinates, indices ... . This together is called mesh.
Then you create a model. Model is composed from one or more meshes. And modelViewMatrix. This is super important part, matrix 4x4 is algebraic representation of position, rotation and scale of this model.
With each render you do exactly this in vertex shader:
"gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
it is from your cg/shaders/VerticalTiltShiftShader.js
If you want to rotate your plane, you don't multiply each vertex, but you only multiply your model matrix once (js with THREE.js function):
projectionMatrix.makeRotationY(dT);
Then each vertex is multiplied in vertex shader with this matrix, which is like much more faster.
Javascript style
speedup: none - medium, but it will allow you to code faster
Lets pick your plane.js as example.
// this is your interface
function PlaneWorld () {
this.init = function() {};
this.updateVertices = function() {};
this.updateWorld = function(dT) {};
// ... and more
}
// and somewhere else:
var world = new PlaneWorld();
In case you have only one plane in your project, you can consider this as singleton and implementation is ~ok. But if you would like to create 2 or more planes, all functions are recreated again for every instance (new PlaneWorld()). Correct way how to do this is:
function PlaneWorld () {
...
}
PlaneWorld.prototype.init = function() {};
PlaneWorld.prototype.updateVertices = function() {};
PlaneWorld.prototype.updateWorld = function(dT) {};
// ... and more
var world = new PlaneWorld();
// calling methods works same
world.updateVertices();
or more complicated version with anonymous function:
var PlaneWorld = (function() {
// something like private static variables here
var PlaneWorld = function () {
...
}
PlaneWorld.prototype = {
init: function() {},
updateVertices: function() {},
updateWorld: function(dT) {}
// ... and more
}
return PlaneWorld();
})();
var world = new PlaneWorld();
// calling methods works same
world.updateVertices();
Then new instance cost is lowered. Now the thing which might be connected, every instance should share same mesh, but has its own modelViewMatrix.
Related
Edited : Thanks to all for valuable time and effort. Finally I made this )) JSfiddle
I was just playing with canvas and made this. Fiddle link here.
... some code here ...
var cords = [];
for(var i = 50; i <= width; i += 100) {
for(var j = 50; j <= height; j += 100) {
cords.push({ cor: i+','+j});
}
}
console.log(cords);
var offset = 15,
speed = 0.01,
angle = 0.01;
cords.forEach(function(e1) {
e1.base = parseInt(Math.random()*25);
e1.rgb = 'rgb('+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+')';
});
setInterval(function() {
cords.forEach(function(e1) {
e1.base = parseInt(Math.random()*25);
e1.rgb = 'rgb('+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+')';
});
},5000);
function render() {
ctx.clearRect(0,0,width,height);
cords.forEach(function(e1) {
//console.log(e1);
ctx.fillStyle = e1.rgb;
ctx.beginPath();
var r = e1.base + Math.abs(Math.sin(angle)) * offset;
var v = e1.cor.split(',');
ctx.arc(v[0],v[1],r,0,Math.PI * 2, false);
ctx.fill();
});
angle += speed;
requestAnimationFrame(render);
}
render();
Was wondering if -
Coordinates can be made random, now they are fixed as you can see. After 5000 mil, balls will show up in various random cords but even at their fullest they won't touch each other.
Every ball has same speed for changing size, I want that to be different too. Meaning, After 5000 mil, they show up with different animation speeds as well.
Also any suggestion on improving code and making it better/quicker/lighter is much appreciated. Thank you !
TL;DR - See it running here.
Making the coordinates random:
This requires you to add some random displacement to the x and y coordinates. So I added a random value to the coordinates. But then a displacement of less than 1 is not noticeable. So you'd need to magnify that random number by a multiplier. That's where the randomizationFactor comes in. I have set it to 100 since that is the value by which you shift the coordinates in each iteration. So that gives a truly random look to the animation.
Making Speed Random:
This one took me a while to figure out, but the ideal way is to push a value of speed into the array of coordinates. This let's you ensure that for the duration of animation, the speed will remain constant and that gives you a smoother feel. But again multiplying the radius r with a value between 0 and 1 reduces the speed significantly for some of the circles. So I have added a multiplier to 3 to compensate slightly for that.
Ideally I'd put a 2, as the average value of Math.random() is 0.5, so a multiplier of 2 would be adequate to compensate for that. But a little experimentation showed that the multiplier of 3 was much better. You can choose the value as per your preference.
Your logic of generating the coordinates changes as follows:
for(var i = 50; i <= width;i += 100) {
for(var j = 51; j <= height;j += 100) {
var x = i + (Math.random() - 0.5)*randomizationFactor;
var y = j + (Math.random() - 0.5)*randomizationFactor;
cords.push({ cor: x+','+y, speed: Math.random()});
}
}
Your logic of enlarging the circles changes as follows:
function render() {
ctx.clearRect(0,0,width,height);
cords.forEach(function(e1) {
//console.log(e1);
ctx.fillStyle = e1.rgb;
ctx.beginPath();
var r = e1.base + Math.abs(Math.sin(angle)) * offset * e1.speed * 3;
var v = e1.cor.split(',');
ctx.arc(v[0],v[1],r,0,Math.PI * 2, false);
ctx.fill();
});
angle += speed ;
requestAnimationFrame(render);
}
Suggestion: Update the coordinates with color
I'd probably also update the location of circles every 5 seconds along with the colors. It's pretty simple to do as well. Here I've just created a function resetCoordinates that runs every 5 seconds along with the setBaseRgb function.
var cords = [];
function resetCoordinates() {
cords = [];
for(var i = 50; i <= width;i += 100) {
for(var j = 51; j <= height;j += 100) {
var x = i + (Math.random() - 0.5)*randomizationFactor;
var y = j + (Math.random() - 0.5)*randomizationFactor;
cords.push({ cor: x+','+y, speed: Math.random()});
}
}
}
UPDATE I did some fixes in your code that can make your animation more dynamic. Totally rewritten sample.
(sorry for variable name changing, imo now better)
Built in Math.random not really random, and becomes obvious when you meet animations. Try to use this random-js lib.
var randEngine = Random.engines.mt19937().autoSeed();
var rand = function(from, to){
return Random.integer(from, to)(randEngine)
}
Internal base properties to each circle would be better(more dynamic).
var circles = [];
// better to save coords as object neither as string
for(var i = 50; i <= width; i += 100)
for(var j = 50; j <= height; j += 100)
circles.push({
coords: {x:i,y:j}
});
We can adjust animation with new bouncing property.
var offset = 15,
speed = 0.005,
angle = 0.01,
bouncing = 25;
This is how setBaseRgb function may look like
function setBaseRgb(el){
el.base = rand(-bouncing, bouncing);
el.speed = rand(5, 10) * speed;
el.angle = 0;
el.rgb = 'rgb('+rand(0, 255)+','+rand(0, 255)+','+rand(0, 255)+')';
}
All your animations had fixed setInterval timeout. Better with random timeout.
cords.forEach(function(el){
// random timeout for each circle
setInterval(setBaseRgb.bind(null,el), rand(3000, 5000));
})
You forgot to add your base to your circle position
function render() {
ctx.clearRect(0,0,width,height);
circles.forEach(function(el) {
ctx.fillStyle = el.rgb;
ctx.beginPath();
var r = bouncing + el.base + Math.abs(Math.sin(el.angle)) * offset;
var coords = el.coords;
ctx.arc(
coords.x + el.base,
coords.y + el.base,
r, 0, Math.PI * 2, false
);
ctx.fill();
el.angle += el.speed;
});
requestAnimationFrame(render);
}
render();
Effect 1 JSFiddle
Adding this
if(el.angle > 1)
el.angle=0;
Results bubling effect
Effect 2 JSFiddle
Playing with formulas results this
Effect 3 JSFiddle
I am trying to write a script to place 100 circles of varying sizes onto a stage. I've outlined the concise requirements below.
Given the following:
var stage; // contains a "width" and "height" property.
var circle; // the circle class. contains x, y, radius & a unique id property.
var circleArray; // contains 100 circle instances
requirements:
write a function to place 100 circles of varying radius onto the stage.
placements must be random but evenly distributed (no clumping).
placement must be performant - this will be executing on a mobile web browser.
circles must not intersect/overlap other circles.
circle.x >= 0 must be true.
circle.y >= 0 && circle.y <= stage.height must be true.
circles may have any of the following radius sizes (assigned at creation):
150
120
90
80
65
My current attempt is a brute-force method, which does not operate efficiently. If I attempt to insert any more than ~10 circles, the browser hangs. Below is my current implementation, which I am completely OK with throwing away in favor of a more performant / better one.
Here is a live demo (NOTE: there is no actual drawing code, just the logic, but it will still lock up the browser so be warned!!) http://jsbin.com/muhiziduxu/2/edit?js,console
function adjustForOverlap (circleArray) {
// a reference to the circle that is invoking this function.
var _this = this;
// remove this circle from the array we are iterating over.
var arr = circleArray.filter(function (circle){
return circle.id !== _this.id;
});
// while repeat == true, the circle may be overlapping something.
var repeat = true;
while(repeat) {
var hasOverlap = false;
for (var i=0; i<arr.length; i++) {
var other = arr[i];
var dx = _self.x - other.x;
var dy = _self.y - other.y;
var rr = _self.radius + other.radius;
if (dx * dx + dy * dy < rr * rr) {
// if here, then an overlap was detected.
hit = true;
break;
}
}
// if hit is false, the circle didn't overlap anything, so break.
if (hit === false) {
repeat = false;
break;
} else {
// an overlap was detected, so randomize position.
_self.x = Math.random() * (stage.width*2);
_self.y = Math.random() * stage.height;
}
}
}
There are lots of efficient collision detection algorithms. Many of them work by dividing up the space into cells and maintaining a separate data structure with efficient lookup of other objects in the cell. The basic steps are:
Identify a random spot for your new circle
Determine which cells it's in
Look in each of those cells for a collision
If there's a collision, goto 1.
Else, add the new circle to each of the cells it overlaps.
You can use a simple square grid (i.e. a 2-d array) for the cell data structure, or something else like a quadtree. You can also in some cases get a bit of extra speed by trying a cheap-but-coarse collision check first (do the bounding boxes overlap), and if that returns true try the slightly more expensive and exact check.
Update
For quadtrees, check out d3-quadtree, which ought to give you a pretty good implementation, with examples.
For a (very quick, untested) 2-d array implementation:
function Grid(radius, width, height) {
// I'm not sure offhand how to find the optimum grid size.
// Let's use a radius as a starting point
this.gridX = Math.ceil(width / radius);
this.gridY = Math.ceil(height / radius);
// Determine cell size
this.cellWidth = width / this.gridX;
this.cellHeight = height / this.gridY;
// Create the grid structure
this.grid = [];
for (var i = 0; i < gridY; i++) {
// grid row
this.grid[i] = [];
for (var j = 0; j < gridX; j++) {
// Grid cell, holds refs to all circles
this.grid[i][j] = [];
}
}
}
Grid.prototype = {
// Return all cells the circle intersects. Each cell is an array
getCells: function(circle) {
var cells = [];
var grid = this.grid;
// For simplicity, just intersect the bounding boxes
var gridX1Index = Math.floor(
(circle.x - circle.radius) / this.cellWidth
);
var gridX2Index = Math.ceil(
(circle.x + circle.radius) / this.cellWidth
);
var gridY1Index = Math.floor(
(circle.y - circle.radius) / this.cellHeight
);
var gridY2Index = Math.ceil(
(circle.y + circle.radius) / this.cellHeight
);
for (var i = gridY1Index; i < gridY2Index; i++) {
for (var j = gridX1Index; j < gridX2Index; j++) {
// Add cell to list
cells.push(grid[i][j]);
}
}
return cells;
},
add: function(circle) {
this.getCells(circle).forEach(function(cell) {
cell.push(circle);
});
},
hasCollisions: function(circle) {
return this.getCells(circle).some(function(cell) {
return cell.some(function(other) {
return this.collides(circle, other);
}, this);
}, this);
},
collides: function (circle, other) {
if (circle === other) {
return false;
}
var dx = circle.x - other.x;
var dy = circle.y - other.y;
var rr = circle.radius + other.radius;
return (dx * dx + dy * dy < rr * rr);
}
};
var g = new Grid(150, 1000, 800);
g.add({x: 100, y: 100, radius: 50});
g.hasCollisions({x: 100, y:80, radius: 100});
Here's a fully-functional example: http://jsbin.com/cojoxoxufu/1/edit?js,output
Note that this only shows 30 circles. It looks like the problem is often unsolvable with your current radii, width, and height. This is set up to look for up to 500 locations for each circle before giving up and accepting a collision.
This is a bit complicated to describe, so please bear with me.
I'm using the HTML5 canvas to extend a diagramming tool (Diagramo). It implements multiple types of line, straight, jagged (right angle) and curved (cubic or quadratic). These lines can be solid, dotted or dashed.
The new feature I am implementing is a "squiggly" line, where instead of following a constant path, the line zigzags back and forth across the desired target path in smooth arcs.
Below is an example of this that is correct. This works in most cases, however, in certain edge cases it does not.
The implementation is to take the curve, use the quadratic or cubic functions to estimate equidistance points along the line, and draw squiggles along these straight lines by placing control points on either side of the straight line (alternating) and drawing multiple cubic curves.
The issues occur when the line is relatively short, and doubles back on itself close to the origin. An example is below, this happens on longer lines too - the critical point is that there is a very small sharp curve immediately after the origin. In this situation the algorithm picks the first point after the sharp curve, in some cases immediately next to the origin, and considers that the first segment.
Each squiggle has a minimum/maximum pixel length of 8px/14px (which I can change, but much below that and it becomes too sharp, and above becomes too wavy) the code tries to find the right sized squiggle for the line segment to fit with the minimum empty space, which is then filled by a straight line.
I'm hoping there is a solution to this that can account for sharply curved lines, if I know all points along a line can I choose control points that alternate either side of the line, perpendicular too it?
Would one option be to consider a point i and the points i-1 and i+1 and use that to determine the orientation of the line, and thus pick control points?
Code follows below
//fragment is either Cubic or Quadratic curve.
paint(fragment){
var length = fragment.getLength();
var points = Util.equidistancePoints(fragment, length < 100 ? (length < 50 ? 3: 5): 11);
points.splice(0, 1); //remove origin as that is the initial point of the delegate.
//points.splice(0, 1);
delegate.paint(context, points);
}
/**
*
* #param {QuadCurve} or {CubicCurbe} curve
* #param {Number} m the number of points
* #return [Point] a set of equidistance points along the polyline of points
* #author Zack
* #href http://math.stackexchange.com/questions/321293/find-coordinates-of-equidistant-points-in-bezier-curve
*/
equidistancePoints: function(curve, m){
var points = curve.getPoints(0.001);
// Get fractional arclengths along polyline
var n = points.length;
var s = 1.0/(n-1);
var dd = [];
var cc = [];
var QQ = [];
function findIndex(dd, d){
var i = 0;
for (var j = 0 ; j < dd.length ; j++){
if (d > dd[j]) {
i = j;
}
else{
return i;
}
}
return i;
};
dd.push(0);
cc.push(0);
for (var i = 0; i < n; i++){
if(i >0) {
cc.push(Util.distance(points[i], points[i - 1]));
}
}
for (var i = 1 ; i < n ; i++) {
dd.push(dd[i-1] + cc[i]);
}
for (var i = 1 ; i < n ; i++) {
dd[i] = dd[i]/dd[n-1];
}
var step = 1.0/(m-1);
for (var r = 0 ; r < m ; r++){
var d = parseFloat(r)*step;
var i = findIndex(dd, d);
var u = (d - dd[i]) / (dd[i+1] - dd[i]);
var t = (i + u)*s;
QQ[r] = curve.getPoint(t);
}
return QQ;
}
SquigglyLineDelegate.prototype = {
constructor: SquigglyLineDelegate,
paint: function(context, points){
var squiggles = 0;
var STEP = 0.1;
var useStart = false;
var bestSquiggles = -1;
var bestA = 0;
var distance = Util.distance(points[0], this.start);
for(var a = SquigglyLineDelegate.MIN_SQUIGGLE_LENGTH; a < SquigglyLineDelegate.MAX_SQUIGGLE_LENGTH; a += STEP){
squiggles = distance / a;
var diff = Math.abs(Math.floor(squiggles) - squiggles);
if(diff < bestSquiggles || bestSquiggles == -1){
bestA = a;
bestSquiggles = diff;
}
}
squiggles = distance / bestA;
for(var i = 0; i < points.length; i++){
context.beginPath();
var point = points[i];
for(var s = 0; s < squiggles-1; s++){
var start = Util.point_on_segment(this.start, point, s * bestA);
var end = Util.point_on_segment(this.start, point, (s + 1) * bestA);
var mid = Util.point_on_segment(this.start, point, (s + 0.5) * bestA);
end.style.lineWidth = 1;
var line1 = new Line(Util.point_on_segment(mid, end, -this.squiggleWidth), Util.point_on_segment(mid, end, this.squiggleWidth));
var mid1 = Util.getMiddle(line1.startPoint, line1.endPoint);
line1.transform(Matrix.translationMatrix(-mid1.x, -mid1.y));
line1.transform(Matrix.rotationMatrix(radians = 90 * (Math.PI/180)));
line1.transform(Matrix.translationMatrix(mid1.x, mid1.y));
var control1 = useStart ? line1.startPoint : line1.endPoint;
var curve = new QuadCurve(start, control1, end);
curve.style = null;
curve.paint(context);
useStart = !useStart;
}
this.start = point;
context.lineTo(point.x, point.y);
context.stroke();
}
}
}
I tried to play Oscillator (buffer -> FFT -> PeriodicWave -> Oscillator).
// PeriodicWave (-> Oscillator)
var wave = fft(buffer), periodicwave = ctx.createPeriodicWave(wave[0], wave[1]);
function fft(input) {
var n = input.length, theta = 2 * Math.PI / n,
ar = new Float32Array(n), ai = new Float32Array(n),
m, mh, i, j, k, irev,
wr, wi, xr, xi,
cos = Math.cos, sin = Math.sin;
for(i=0; i<n; ++i) {
ar[i] = input[i];
}
// scrambler
i=0;
for(j=1; j<n-1; ++j) {
for(k = n>>1; k>(i ^= k); k>>=1);
if(j<i) {
xr = ar[j];
xi = ai[j];
ar[j] = ar[i];
ai[j] = ai[i];
ar[i] = xr;
ai[i] = xi;
}
}
for(mh=1; (m = mh << 1) <= n; mh=m) {
irev = 0;
for(i=0; i<n; i+=m) {
wr = cos(theta * irev);
wi = sin(theta * irev);
for(k=n>>2; k > (irev ^= k); k>>=1);
for(j=i; j<mh+i; ++j) {
k = j + mh;
xr = ar[j] - ar[k];
xi = ai[j] - ai[k];
ar[j] += ar[k];
ai[j] += ai[k];
ar[k] = wr * xr - wi * xi;
ai[k] = wr * xi + wi * xr;
}
}
}
return [ar, ai];
}
This code is good however this Oscillator's gain is standardized automatically. I want to set gain as loud as BufferSource (buffer -> AudioBuffer -> BufferSource), but it doesn't work I have tried using AudioGainNode, see this test code.
createPeriodicWave automatically scales the output waveform so that it has a max amplitude of 1. This means the volume is lower than you might expect. You'll have to experiment a bit with the gain to make the oscillator output match the buffer output volume. The scaling depends on the wave used for createPeriodicWave.
However, I think the beta (or canary) versions of Chrome support the recently introduced parameter disableNormalization: true for createPeriodicWave to disable the normalization. Then the oscillator volume should match the buffer volume.
You can increase the gain on the Oscillator; but yes, PeriodicWave does normalize by default. createPeriodicWave now takes a constraints object that enables you to disable that normalization.
So, I am trying to create a N-Body Gravity simulation in JavaScript:
http://jsfiddle.net/4M94x/
var Circle = function(c, r, cor, cof) { // Fix CoR & CoF // Had to add code for JSFiddle link :P
this.c = c
this.r = r
this.m = r * r * Math.PI
this.v = new Vector()
this.cor = cor
this.cof = cof
}
The problem's that when you spawn (click) and put 2 balls (accidentally renamed "particles") next to each other they start to generate velocity and faster and faster push eachother.. How do I fix this, btw is my gravity implementation correct?
This is easy to explain: You are implementing Euler forward as solver for the ODE, and in mechanical systems the systematic error of Euler forward increases the energy. Euler backward decreases the energy, so a combination of alternating the explicit and implicit Euler methods would leave the energy a little more constant.
But then you can implement with the same or even less effort the second order symplectic methods which preserve energy and other physical invariants, either the (implicit) midpoint method or the Verlet-(Stoermer-Cromer-...-Newton-)method.
Or even higher order Runge-Kutta, which will also preserve the energy to a higher order, despite not being symplectic.
See Hairer on the Stoermer-Verlet-...-Newton method, postprint or preprint and the "Moving stars around" tutorial text using C++ or Ruby.
A note to the physics: All in all the implementation is very minimal and well readable. But the gravitational force is
g*m1*m2*(p2-p1)/norm(p2-p1)^3
as the negative gradient of
g*m1*m2/norm(p2-p1)
you are only using the square of the norm, where the force would be the negative gradient of the gravitational potential energy
g*m1*m2*ln(norm(p2-p1))
which is right for flatland physics, but not for a 2D section of 3D space.
Working code
with velocity Verlet and energy preservation:
Add a new field a=Vector() to the circle object and replace the kitchen sink which is the update() function with the following collection of dedicated functions
function compute_forces() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
p.a.set(0);
for (var j = 0; j < i; j++) {
var p2 = particles[j];
var d = p.c.sub(p2.c);
var norm = Math.sqrt(100.0 + d.lengthSq());
var mag = gravity / (norm * norm * norm);
p.a.set(p.a.sub(d.mul(mag * p2.m)));
p2.a.set(p2.a.add(d.mul(mag * p.m)));
}
}
}
function do_collisions() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
for (var j = 0; j < i; j++) {
var p2 = particles[j];
if (checkCCCol(p, p2)) {
resCCCol(p, p2);
}
}
}
}
function do_physics(dt) {
// do velocity Verlet
// with consistent state at interval half points
// x += 0.5*v*dt
for (var i1 = 0; i1 < particles.length; i1++) {
var p1 = particles[i1];
p1.c.set(p1.c.add(p1.v.mul(0.5 * dt)));
}
// a = A(x)
compute_forces();
// v += a*dt
for (var i2 = 0; i2 < particles.length; i2++) {
var p2 = particles[i2];
p2.v.set(p2.v.add(p2.a.mul(dt)));
}
// x += 0.5*v*dt
for (var i3 = 0; i3 < particles.length; i3++) {
var p3 = particles[i3];
p3.c.set(p3.c.add(p3.v.mul(0.5 * dt)));
}
do_collisions();
}
function update() {
for (var k = 0; k < 4; k++) {
do_physics(1.0 / 4);
}
render();
RAF(update);
}
See http://jsfiddle.net/4XVPH/
Altered example with particles coloured based on their mass(hopefully better displaying their interaction), one bug fixed, and some additional comments: http://jsfiddle.net/24mg6ctg/12/