More‎ > ‎

WebGL Experiment Demonstrates Barnes-Hut N-Body Simulation of a Growing Watermelon in a Box

Japan produces watermelons which have a cubic shape. USA Today reported in 2013 that these melons have been sold for up to 850$ in Moscow [1] - quite expensive for a simple melon. To produce this unusual shape the young melon fruits are put into a box. 
The growing melons are then forced into the cubic shape. 

In the following WebGL experiment this growth into a cubic shape is simulated with an N-Body-SimulationThis WebGL experiment demonstrates how to simulate a growing watermelon in a web browser. You may start the web application with a WebGL enabled browser here http://webgl-examples.appspot.com/square-melon/melon.html. The complete source code is available on GitHub

Expected Result

The WebGL simulation is not a simple geometrical transformation from a sphere to a cube. It uses an N-Body-Simulation which calculates the forces between the nodes of a sphere. 


Figure 1: Start of Simulation



Figure 2: End of 
Simulation


The underlying model for the melon has 400 nodes and 420 links between the nodes. All nodes repulse each other and the links work as springs to keep them together. 

Figure 3: Model of N-Body Simulation with 400 vertices/nodes 

Why Barnes-Hut-Algorithm is needed?

A simple three.js sphere with 400 nodes is used to render the melon. To calculate all repulsive forces between the nodes would need 400*(400-1)/2 or 79800 single calculations - a quite CPU intensive task.

One solution to improve performance is to reduce the number of calculations with an approximation of the repulsive forces. Groups of nodes which are far away can be handled as a single node. This can be done with an optimal data structure. One approach to do this, is the so called Barnes-Hut-Algorithm designed for astronomical problems. You may find an excellent description of two dimensional Barnes-Hut-Algorithm in the article http://arborjs.org/docs/barnes-hutFor the growing watermelon the Barnes-Hut-Algorithm is implemented for three dimensions in JavaScript. 

Implementation

In the file melon.simulator.js (line 1 - 469) the core of the N-Body Simulation is implemented.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
/**
 * Options for n-body simulation and rendering
 */

function SimulationOptions() {
	"use strict";
	return {
		RUN_SIMULATION : true,
		SPHERE_RADIUS : 1200.0,
		SPHERE_RADIUS_MINIMUM : 25.0,
		CHARGE : 25.0,
		THETA : 0.8,
		SPRING : 8.0,

		// Parameters for rendering
		SHOW_MELON_TEXTURE : true,
		DISPLAY_CUBE : true
	};
}

/**
 * Node element with information for simulation and rendering
 */

function NNode(node) {
	"use strict";
	this.id = node.id;
	this.masterNode = null;

	this.x = node.x;
	this.y = node.y;
	this.z = node.z;

	this.force_x = 0.0;
	this.force_y = 0.0;
	this.force_z = 0.0;

	// Rendering elements
	this.sphere = {};
	this.sphereCreated = false;
}

NNode.prototype.getRadius = function() {
	"use strict";
	return MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM;
};

function NLink(source, target) {
	"use strict";
	this.source = (source.masterNode == null) ? source : source.masterNode;
	this.target = (target.masterNode == null) ? target : target.masterNode;
	this.default_distance = this.getDistance();

	// Rendering elements
	this.threeElement = {};
	this.linkWebGLCreated = false;
}

NLink.prototype.getDistance = function() {
	"use strict";
	var deltaX = (this.source.x - this.target.x);
	var deltaY = (this.source.y - this.target.y);
	var deltaZ = (this.source.z - this.target.z);
	return Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
}

function initRandomPosition(node) {
	var gamma = 2 * Math.PI * Math.random();
	var delta = Math.PI * Math.random();
	var radius = MELONE_SimulationOptions.SPHERE_RADIUS * 0.95;
	node.x = radius * Math.sin(delta) * Math.cos(gamma);
	node.y = radius * Math.sin(delta) * Math.sin(gamma);
	node.z = radius * Math.cos(delta);
};

/**
 * Implementation of Barnes-Hut algorithm for a three-dimensional simulation of
 * charge
 */
BarnesHutAlgorithmOctTree = function(options) {
	"use strict";
	// Parameter needed for the simulation
	if (typeof (options) !== "undefined") {
		if (typeof (options.SPHERE_RADIUS) !== "undefined") {
			MELONE_SimulationOptions.SPHERE_RADIUS = options.SPHERE_RADIUS;
		}
		if (typeof (options.SPHERE_RADIUS_MINIMUM) !== "undefined") {
			MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM = options.SPHERE_RADIUS_MINIMUM;
		}
		if (typeof (options.CHARGE) !== "undefined") {
			MELONE_SimulationOptions.CHARGE = options.CHARGE;
		}
		if (typeof (options.THETA) !== "undefined") {
			MELONE_SimulationOptions.THETA = options.THETA;
		}
	}

	BarnesHutAlgorithmOctTree.prototype.run = function(nodes) {
		var size = MELONE_SimulationOptions.SPHERE_RADIUS;
		MELONE_OctTreeRoot = new BarnesHutAlgorithmOctNode(-size, size, -size,
				size, -size, size);
		var node;
		if (nodes.length > 1) {
			for ( var i = 0; i < nodes.length; i++) {
				node = nodes[i];
				MELONE_OctTreeRoot.addNode(node);
			}
			MELONE_OctTreeRoot.calculateAveragesAndSumOfMass();
			for (i = 0; i < nodes.length; i++) {
				node = nodes[i];
				MELONE_OctTreeRoot.calculateForces(node);
			}
		}
	};
};

BarnesHutAlgorithmOctNode = function(xMin, xMax, yMin, yMax, zMin, zMax) {
	"use strict";
	this.xMin = xMin;
	this.xMax = xMax;
	this.yMin = yMin;
	this.yMax = yMax;
	this.zMin = zMin;
	this.zMax = zMax;
	this.sum_mass = 0;
	this.sum_x = 0;
	this.sum_y = 0;
	this.sum_z = 0;
	this.node = null;
	this.children = null;
	this.diameter = (((xMax - xMin) + (yMax - yMin) + (zMax - zMin)) / 3);
};

BarnesHutAlgorithmOctNode.prototype.isFilled = function() {
	"use strict";
	return (this.node != null);
};

BarnesHutAlgorithmOctNode.prototype.isParent = function() {
	"use strict";
	return (this.children != null);
};

BarnesHutAlgorithmOctNode.prototype.isFitting = function(node) {
	"use strict";
	return ((node.x >= this.xMin) && (node.x <= this.xMax)
			&& (node.y >= this.yMin) && (node.y <= this.yMax)
			&& (node.z >= this.zMin) && (node.z <= this.zMax));
};

BarnesHutAlgorithmOctNode.prototype.addNode = function(new_node) {
	"use strict";
	if (this.isFilled() || this.isParent()) {
		var relocated_node;
		if (MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM > this.diameter) {
			var radius = Math.sqrt(new_node.x * new_node.x + new_node.y
					* new_node.y + new_node.z * new_node.z);
			var factor = (radius - MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM)
					/ radius;
			new_node.x *= factor;
			new_node.y *= factor;
			new_node.z *= factor;
			relocated_node = this.node;
			this.node = null;
			this.sum_mass = 0;
			this.sum_x = 0;
			this.sum_y = 0;
			this.sum_z = 0;
			MELONE_OctTreeRoot.addNode(relocated_node);
			return;
		}

		if (!this.isParent()) {
			var xMiddle = (this.xMin + this.xMax) / 2;
			var yMiddle = (this.yMin + this.yMax) / 2;
			var zMiddle = (this.zMin + this.zMax) / 2;

			// create children
			this.children = [];
			this.children.push(new BarnesHutAlgorithmOctNode(xMiddle,
					this.xMax, yMiddle, this.yMax, zMiddle, this.zMax));
			this.children.push(new BarnesHutAlgorithmOctNode(this.xMin,
					xMiddle, yMiddle, this.yMax, zMiddle, this.zMax));
			this.children.push(new BarnesHutAlgorithmOctNode(this.xMin,
					xMiddle, this.yMin, yMiddle, zMiddle, this.zMax));
			this.children.push(new BarnesHutAlgorithmOctNode(xMiddle,
					this.xMax, this.yMin, yMiddle, zMiddle, this.zMax));
			this.children.push(new BarnesHutAlgorithmOctNode(xMiddle,
					this.xMax, yMiddle, this.yMax, this.zMin, zMiddle));
			this.children.push(new BarnesHutAlgorithmOctNode(this.xMin,
					xMiddle, yMiddle, this.yMax, this.zMin, zMiddle));
			this.children.push(new BarnesHutAlgorithmOctNode(this.xMin,
					xMiddle, this.yMin, yMiddle, this.zMin, zMiddle));
			this.children.push(new BarnesHutAlgorithmOctNode(xMiddle,
					this.xMax, this.yMin, yMiddle, this.zMin, zMiddle));

			// re-locate old node (add into children)
			relocated_node = this.node;
			this.node = null;
			this.sum_mass = 0;
			this.sum_x = 0;
			this.sum_y = 0;
			this.sum_z = 0;

			this.addChildNode(relocated_node);
		}

		// now add new node into children
		if (this.isParent()) {
			this.addChildNode(new_node);
		}

	} else {
		this.node = new_node;
		this.sum_mass = 1.0;
		this.sum_x = this.node.x;
		this.sum_y = this.node.y;
		this.sum_z = this.node.z;
		this.node.force_x = 0.0;
		this.node.force_y = 0.0;
		this.node.force_z = 0.0;
	}
};

BarnesHutAlgorithmOctNode.prototype.addChildNode = function(node) {
	"use strict";
	if (this.isParent()) {
		for ( var index = 0; index < 8; index++) {
			var child = this.children[index];
			if (child.isFitting(node)) {
				child.addNode(node);
				return;
			}
		}
	}
	// Unable to add node -> has to be relocated
	initRandomPosition(node);
	MELONE_OctTreeRoot.addNode(node);
};

BarnesHutAlgorithmOctNode.prototype.calculateForces = function(new_node) {
	"use strict";
	if (this.sum_mass > 0.01 || this.isFilled()) {
		var deltaX, deltaY, deltaZ;
		if (this.isFilled()) {
			deltaX = (this.node.x - new_node.x);
			deltaY = (this.node.y - new_node.y);
			deltaZ = (this.node.z - new_node.z);
		} else {
			deltaX = (this.sum_x / this.sum_mass - new_node.x) * new_node.mass;
			deltaY = (this.sum_y / this.sum_mass - new_node.y) * new_node.mass;
			deltaZ = (this.sum_z / this.sum_mass - new_node.z) * new_node.mass;
		}

		var radius = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ
				* deltaZ);
		var radius_squared = Math.pow((radius > 1e-6) ? radius : 1e-6, 2);
		var treatInternalNodeAsSingleBody = this.diameter / radius < MELONE_SimulationOptions.THETA;
		if (this.isFilled() || treatInternalNodeAsSingleBody) {
			new_node.force_x -= (deltaX * MELONE_SimulationOptions.CHARGE)
					/ radius_squared;
			new_node.force_y -= (deltaY * MELONE_SimulationOptions.CHARGE)
					/ radius_squared;
			new_node.force_z -= (deltaZ * MELONE_SimulationOptions.CHARGE)
					/ radius_squared;
		} else if (this.isParent()) {
			for ( var index = 0; index < 8; index++) {
				var child = this.children[index];
				if (child.isFilled() || this.isParent()) {
					child.calculateForces(new_node);
				}
			}
		}
	}
};

BarnesHutAlgorithmOctNode.prototype.calculateAveragesAndSumOfMass = function() {
	"use strict";
	if (this.isParent()) {
		var child;
		for ( var index = 0; index < 8; index++) {
			child = this.children[index];
			child.calculateAveragesAndSumOfMass();
		}

		this.sum_mass = 0;
		this.sum_x = 0;
		this.sum_y = 0;
		this.sum_z = 0;
		for (index = 0; index < 8; index++) {
			child = this.children[index];
			if (child.isFilled() || this.isParent()) {
				this.sum_mass += child.sum_mass;
				this.sum_x += child.sum_x;
				this.sum_y += child.sum_y;
				this.sum_z += child.sum_z;
			}
		}
	}
};

/**
 * n-body simulator makes the Branes-Hut simulation and adds the link forces.
 */
var NBodySimulator = function() {
	"use strict";

	// all existing nodes and links
	this.node_list = [];
	this.node_list_simulate = [];
	this.link_list = [];

	this.octTree = new BarnesHutAlgorithmOctTree();
	

	NBodySimulator.prototype.simulateAllForces = function() {
		var me = this;

		this.node_list_simulate = [];
		this.node_list.forEach(function(node) {
			if (node.masterNode == null) {
				me.node_list_simulate.push(node);
			}
		});

		// Execute Barnes-Hut simulation
		this.octTree = new BarnesHutAlgorithmOctTree();
		this.octTree.run(this.node_list_simulate);

		// Calculate link forces
		this.link_list.forEach(function(link) {
			me.calcLinkForce(link);
		});
		this.node_list_simulate.forEach(function(node) {
			me.applyForces(node);
			me.scaleToBeInSphere(node);
			me.resetForces(node);
		});

		// Scale and apply all forces
		this.node_list.forEach(function(node) {
			if (node.masterNode != null) {
				node.x = node.masterNode.x;
				node.y = node.masterNode.y;
				node.z = node.masterNode.z;
			}
		});

	};

	NBodySimulator.prototype.updateMelon = function(sphere) {
		"use strict";
		var vertices = sphere.geometry.vertices;
		for ( var i = 0; i < vertices.length; i++) {
			vertices[i].x = this.node_list[i].x;
			vertices[i].y = this.node_list[i].y;
			vertices[i].z = this.node_list[i].z;
		}
		sphere.geometry.verticesNeedUpdate = true;
	}

	NBodySimulator.prototype.createModelFromSphere = function(sphere) {
		"use strict";
		var vertices = sphere.geometry.vertices;
		for ( var i = 0; i < vertices.length; i++) {
			var newNode = {
				"id" : i,
				"alias" : ('id' + i),
				"x" : vertices[i].x,
				"y" : vertices[i].y,
				"z" : vertices[i].z
			};
			this.node_list.push(new NNode(newNode));
		}

		// Merge double nodes
		for ( var k = 1; k <= sphere.geometry.widthSegments; k++) {
			this.node_list[k].masterNode = this.node_list[0];
		}

		for ( var k = 1; k < sphere.geometry.heightSegments; k++) {
			var indexNode1 = (sphere.geometry.widthSegments) + k
					* (sphere.geometry.widthSegments + 1);
			var indexNode2 = (sphere.geometry.widthSegments + 1) + (k - 1)
					* (sphere.geometry.widthSegments + 1);
			this.node_list[indexNode1].masterNode = this.node_list[indexNode2];
		}

		for ( var k = 1; k <= sphere.geometry.widthSegments; k++) {
			var indexNode1 = vertices.length - 1 - k;
			var indexNode2 = vertices.length - 1;
			this.node_list[indexNode1].masterNode = this.node_list[indexNode2];
		}

		var faces = sphere.geometry.faces;
		for ( var k = 0; k < faces.length; k++) {
			// Create links
			var sourceNode = this.node_list[faces[k].a];
			var targetNode = this.node_list[faces[k].b];
			this.link_list.push(new NLink(sourceNode, targetNode));

			// Create last row of links
			if (k >= faces.length - sphere.geometry.widthSegments) {
				sourceNode = this.node_list[faces[k].b];
				targetNode = this.node_list[faces[k].c];
				this.link_list.push(new NLink(sourceNode, targetNode));
			}
		}
	};

	/**
	 * Each link acts as simple spring. There are two types of nodes and links.
	 */
	NBodySimulator.prototype.calcLinkForce = function(link) {
		var deltaX = (link.source.x - link.target.x);
		var deltaY = (link.source.y - link.target.y);
		var deltaZ = (link.source.z - link.target.z);
		var radius = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ
				* deltaZ);
		if (radius > 1e-6) {
			var factor = (radius - link.default_distance) / radius / radius
					* MELONE_SimulationOptions.SPRING;
			link.source.force_x -= (deltaX) * factor;
			link.source.force_y -= (deltaY) * factor;
			link.source.force_z -= (deltaZ) * factor;
			link.target.force_x += (deltaX) * factor;
			link.target.force_y += (deltaY) * factor;
			link.target.force_z += (deltaZ) * factor;
		}
	};

	/**
	 * Ensure that the new position is in the sphere. Nodes which leave the
	 * sphere would be ignored by OctTree (Barnes-Hut-Algorithm).
	 */
	NBodySimulator.prototype.scaleToBeInSphere = function(node) {
		node.x = Math.min(Math.max(1 - MELONE_SimulationOptions.SPHERE_RADIUS,
				node.x), MELONE_SimulationOptions.SPHERE_RADIUS - 1);
		node.y = Math.min(Math.max(1 - MELONE_SimulationOptions.SPHERE_RADIUS,
				node.y), MELONE_SimulationOptions.SPHERE_RADIUS - 1);
		node.z = Math.min(Math.max(1 - MELONE_SimulationOptions.SPHERE_RADIUS,
				node.z), MELONE_SimulationOptions.SPHERE_RADIUS - 1);
	};

	/**
	 * Move the nodes depending of the forces
	 */
	NBodySimulator.prototype.applyForces = function(node) {
		node.x += node.force_x;
		node.y += node.force_y;
		node.z += node.force_z;
	};

	/**
	 * Reset all forces of the node to zero
	 */
	NBodySimulator.prototype.resetForces = function(node) {
		node.force_x = 0;
		node.force_y = 0;
		node.force_z = 0;
	};
};

/**
 * global variables
 */
var MELONE_SimulationOptions = new SimulationOptions();
var MELONE_OctTreeRoot = {};
var MELONE_NBodySimulator = new NBodySimulator();

In file melon.main.js (lines 1 - 328) all the rendering of the melon happens and a simple GUI is implemented.
 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/**
 * Global constants
 */
var BORDER_LEFT = 10;
var BORDER_TOP = 10;
var BORDER_RIGHT = 10;
var BORDER_BOTTOM = 60;

/**
 * Global variables for rendering
 */
var g_panelWidthWebGL;
var g_panelHeightWebGL;
var g_scene;
var g_cube_wireframe;
var g_camera;
var g_renderer;
var g_control;
var g_gui;
var g_melon;

/**
 * Initialize WebGL
 */
function initWebGL() {
	"use strict";
	// Container for WebGL rendering
	var container = document.getElementById('graphic-container');
	container.style.background = "#252525";
	initDatGui(container);
	
	// Size of drawing
	g_panelWidthWebGL = window.innerWidth - BORDER_RIGHT - BORDER_LEFT;
	g_panelHeightWebGL = window.innerHeight - BORDER_BOTTOM - BORDER_TOP;

	// Create g_camera
	g_camera = new THREE.PerspectiveCamera(40, g_panelWidthWebGL
			/ g_panelHeightWebGL, 1, 40000);
	resetCamera();

	// Create g_scene
	g_scene = new THREE.Scene();
	g_scene.add(g_camera);

	// Create g_renderer
	if (Detector.webgl) {
		g_renderer = new THREE.WebGLRenderer({
			antialias : true
		});
	} else {
		container.appendChild(Detector.getWebGLErrorMessage());
		return;
	}
	g_renderer.setSize(g_panelWidthWebGL, g_panelHeightWebGL);
	container.appendChild(g_renderer.domElement);


	// LIGHTS
	var light = new THREE.DirectionalLight( 0xffffff, 1.475 );
	light.position.set( -1000, 1000, 1000 );
	g_scene.add( light );

	var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.3 );
	hemiLight.position.set( 0, 5000, 0 );
	g_scene.add( hemiLight );

	// Support window resize
	var resizeCallback = function() {
		g_panelWidthWebGL = window.innerWidth - BORDER_RIGHT - BORDER_LEFT;
		g_panelHeightWebGL = window.innerHeight - BORDER_BOTTOM - BORDER_TOP;
		var devicePixelRatio = window.devicePixelRatio || 1;
		g_renderer.setSize(g_panelWidthWebGL * devicePixelRatio,
				g_panelHeightWebGL * devicePixelRatio);
		g_renderer.domElement.style.width = g_panelWidthWebGL + 'px';
		g_renderer.domElement.style.height = g_panelHeightWebGL + 'px';
		resetCamera();
		g_camera.updateProjectionMatrix();
		g_gui.domElement.style.position = 'absolute';
		g_gui.domElement.style.left = '' + (BORDER_LEFT) + 'px';
		g_gui.domElement.style.top = '' + (BORDER_TOP) + 'px';
	};
	window.addEventListener('resize', resizeCallback, false);
	resizeCallback();

	g_control = new THREE.TrackballControls(g_camera, g_renderer.domElement);
	g_control.target.set(0, 0, 0);
	g_control.rotateSpeed = 1.0;
	g_control.zoomSpeed = 1.2;
	g_control.panSpeed = 0.8;
	g_control.noZoom = false;
	g_control.noPan = false;
	g_control.staticMoving = false;
	g_control.dynamicDampingFactor = 0.15;
	g_control.keys = [ 65, 83, 68 ];
	g_control.addEventListener('change', renderer);

	// create melone with texture
	var loader = new THREE.TextureLoader();
	loader.load('melon.jpg', function(texture) {
		var geometry = new THREE.SphereGeometry(800, 16, 20);
		var material = new THREE.MeshLambertMaterial({
			map : texture,
			depthTest : true,
			overdraw : true,
			castShadow : true,
			shininess: 20,
			shading : THREE.SmoothShading
		});
		g_melon = new THREE.Mesh(geometry, material);
		g_melon.geometry.dynamic = true;
		if (MELONE_SimulationOptions.SHOW_MELON_TEXTURE) {
			g_scene.add(g_melon);
		}
		MELONE_NBodySimulator.createModelFromSphere(g_melon);
	});

	// Start animation
	animate();
}

/**
 * Render WebGL with about 60 fames per second if possible
 */

function animate() {
	"use strict";
	requestAnimationFrame(animate);

	g_control.update();
	
	// simulate forces
	if (MELONE_SimulationOptions.RUN_SIMULATION) {
		for (var i = 0; i < 3; i++) {
			MELONE_NBodySimulator.simulateAllForces();
		}
	}

	// update position of the melone
	if (null != g_melon) {
		MELONE_NBodySimulator.updateMelon(g_melon);
	}

	// re-draw the box
	renderCubeWithDottedHiddenLines();

	// update wire frame
	var nodes = MELONE_NBodySimulator.node_list;
	for ( var i = 0; i < nodes.length; i++) {
		renderNodeSphere(nodes[i]);
	}
	var links = MELONE_NBodySimulator.link_list;
	for (i = 0; i < links.length; i++) {
		renderLineElementForLink(links[i]);
	}

	renderer();
}

function renderer() {
	"use strict";
	g_renderer.render(g_scene, g_camera);
}

/**
 * Render a cube with hidden dotted lines. It is necessary to render the cube
 * several times to make all hidden lines dotted and the visible lines solid.
 */
function renderCubeWithDottedHiddenLines() {
	"use strict";
	if (typeof (g_cube_wireframe) !== "undefined") {
		g_scene.remove(g_cube_wireframe);
	}
	if (MELONE_SimulationOptions.DISPLAY_CUBE) {
		// Create geometries
		var a = (MELONE_SimulationOptions.SPHERE_RADIUS + MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM) * 2;
		var cube_geometry = new THREE.CubeGeometry(a, a, a);
		var cube_geometry_wire = convertCubeGeometry2LineGeometry(cube_geometry);
		// Create materials
		var material_solid_wireframe = new THREE.MeshBasicMaterial({
			color : 0x666666,
			depthTest : true,
			wireframe : true,
			polygonOffset : true,
			polygonOffsetFactor : 1,
			polygonOffsetUnits : 1
		});
		// Render four cubes with same geometry
		g_cube_wireframe = new THREE.Line(cube_geometry_wire,
				material_solid_wireframe, THREE.LinePieces);
		g_scene.add(g_cube_wireframe);
	}
}

/**
 * Helper to create a line-geometry from a cube-geometry
 */

function convertCubeGeometry2LineGeometry(input) {
	"use strict";
	var geometry = new THREE.Geometry();
	var vertices = geometry.vertices;
	for ( var i = 0; i < input.faces.length; i += 2) {
		var face1 = input.faces[i];
		var face2 = input.faces[i + 1];
		var c1 = input.vertices[face1.c].clone();
		var a1 = input.vertices[face1.a].clone();
		var a2 = input.vertices[face2.a].clone();
		var b2 = input.vertices[face2.b].clone();
		var c2 = input.vertices[face2.c].clone();
		vertices.push(c1, a1, a2, b2, b2, c2);
	}
	geometry.computeLineDistances();
	return geometry;
}

/**
 * Renders sphere for the node
 */

function renderNodeSphere(node) {
	"use strict";
	if (node.sphereCreated) {
		// Update position
		node.sphere.position.x = node.x;
		node.sphere.position.z = node.z;
		node.sphere.position.y = node.y;
		node.sphere.visible = !MELONE_SimulationOptions.SHOW_MELON_TEXTURE;
	} else {
		// Create sphere
		var material = new THREE.MeshLambertMaterial({
			reflectivity : 0.9,
			ambient : 0x3A3A3A,
			depthTest : true,
			transparent : true,
			color : 0xAAAAAA
		});
		node.sphere = new THREE.Mesh(new THREE.SphereGeometry(
				MELONE_SimulationOptions.SPHERE_RADIUS_MINIMUM), material);
		node.sphere.position.x = node.x;
		node.sphere.position.z = node.z;
		node.sphere.position.y = node.y;
		node.sphere.visible = false;
		g_scene.add(node.sphere);
		node.sphereCreated = true;
	}
}

/**
 * Renders a link - optional with arrow head
 */

function renderLineElementForLink(link) {
	"use strict";

	// Center position of the nodes
	var source_position = new THREE.Vector3(link.source.x, link.source.y,
			link.source.z);
	var target_position = new THREE.Vector3(link.target.x, link.target.y,
			link.target.z);

	if (link.linkWebGLCreated) {
		// Move existing line
		link.threeElement.geometry.vertices[0] = source_position;
		link.threeElement.geometry.vertices[1] = target_position;
		link.threeElement.geometry.verticesNeedUpdate = true;
		link.threeElement.visible = !MELONE_SimulationOptions.SHOW_MELON_TEXTURE;
	} else {
		// Create line
		var line_geometry = new THREE.Geometry();
		line_geometry.vertices.push(source_position);
		line_geometry.vertices.push(target_position);
		var line_material = new THREE.LineBasicMaterial({
			depthTest : true,
			transparent : true,
			opacity : 1.0,
			color: 0xAAAAAA
		});
		line_material.transparent = true;
		var line = new THREE.Line(line_geometry, line_material);
		line.visible = false;
		link.threeElement = line;
		link.linkWebGLCreated = true;
		g_scene.add(line);
	}
}

function resetCamera() {
	"use strict";
	g_camera.position.x = MELONE_SimulationOptions.SPHERE_RADIUS * 2.5;
	g_camera.position.y = MELONE_SimulationOptions.SPHERE_RADIUS * 2.5;
	g_camera.position.z = MELONE_SimulationOptions.SPHERE_RADIUS * 4;
	g_camera.lookAt(new THREE.Vector3(0, 0, 0));
}

/**
 * User interface to change parameters
 */
function initDatGui(container) {
	g_gui = new dat.GUI({
		autoPlace : false
	});

	f1 = g_gui.addFolder('Render Options');
	f1.add(MELONE_SimulationOptions, 'SHOW_MELON_TEXTURE').name('Show Texture')
			.onChange(function(value) {
				if (value) {
					g_scene.add(g_melon);
				} else {
					g_scene.remove(g_melon);
				}
			});
	f1.add(MELONE_SimulationOptions, 'DISPLAY_CUBE').name('Show Box');
	f1.open();
	
	f3 = g_gui.addFolder('N-Body Simulation');
	f3.add(MELONE_SimulationOptions, 'RUN_SIMULATION').name('Run');
	f3.add(MELONE_SimulationOptions, 'SPRING', 5.0, 20.0).step(1.0).name(
			'Spring Link');
	f3.add(MELONE_SimulationOptions, 'CHARGE', 5, 40).step(1.0).name('Charge');
	f3.open();
	
	container.appendChild(g_gui.domElement);
}

/**
 * Call initialization
 */
initWebGL();

File melon.html (lines 1 - 53) is the entry for the web application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<style type="text/css">
<!--
.main {
	background: #252525;
	padding: 0px;
}

.text {
	color: #CCC;
	font: 14px 'Lucida Grande', sans-serif;
	padding-left: 10px;
	padding-right: 10px;
	padding-top: 4px;
}

a {
	color: #009de9;
	font: 14px 'Lucida Grande', sans-serif;
	text-align: right;
}
-->
</style>
<title>Square Watermelon</title>
</head>

<body class="main">

	<div align="right" class='text'>
		N-Body Simulation of a Growing Watermelon in a Box, <a
			href="https://plus.google.com/u/0/117292523089281814301?rel=author">by
			Markus Sprunck</a>
	</div>

	<div id="graphic-container"></div>

	<script type="text/javascript" src="lib/dat.gui.min.js"></script>
	<script type="text/javascript" src="lib/detector.js"></script>
	<script type="text/javascript" src="lib/three.min.js"></script>
	<script type="text/javascript" src="lib/stats.min.js"></script>
	<script type="text/javascript" src="lib/trackballcontrols.js"></script>
	<script type="text/javascript" src="./melon.simulator.js"></script>
	<script type="text/javascript" src="./melon.main.js"></script>

	<div align="right" class='text'>
		find sources on <a href="https://github.com/SPM2OT/sw-engineering-candies-webgl-examples.git">GitHub</a>
	</div>

</body>
</html>

It is strongly recommended to use a modern browser like Google Chrome 39 and/or Firefox 34. The application also runs with MS Internet Explorer 11, but the performance is not satisfying with IE.

Find Code on GitHub

Sponsored Link