diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8d4aaf40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.map \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..842598b8 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + HW7: Biocrowds + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..370fadd3 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "scripts": { + "start": "webpack-dev-server --hot --inline", + "build": "webpack", + "deploy": "gh-pages-deploy" + }, + "gh-pages-deploy": { + "prep": [ + "build" + ], + "noprompt": true + }, + "dependencies": { + "dat-gui": "^0.5.0", + "gl-matrix": "^2.3.2", + "stats-js": "^1.0.0-alpha1", + "three": "^0.82.1", + "three-orbit-controls": "^82.1.0" + }, + "devDependencies": { + "babel-core": "^6.18.2", + "babel-loader": "^6.2.8", + "babel-preset-es2015": "^6.18.0", + "gh-pages-deploy": "^0.4.2", + "webpack": "^1.13.3", + "webpack-dev-server": "^1.16.2", + "webpack-glsl-loader": "^1.0.1" + } +} diff --git a/src/framework.js b/src/framework.js new file mode 100644 index 00000000..9cfcd1b4 --- /dev/null +++ b/src/framework.js @@ -0,0 +1,75 @@ + +const THREE = require('three'); +const OrbitControls = require('three-orbit-controls')(THREE) +import Stats from 'stats-js' +import DAT from 'dat-gui' + +// when the scene is done initializing, the function passed as `callback` will be executed +// then, every frame, the function passed as `update` will be executed +function init(callback, update) { + var stats = new Stats(); + stats.setMode(1); + stats.domElement.style.position = 'absolute'; + stats.domElement.style.left = '0px'; + stats.domElement.style.top = '0px'; + document.body.appendChild(stats.domElement); + + var gui = new DAT.GUI(); + + var framework = { + gui: gui, + stats: stats + }; + + // run this function after the window loads + window.addEventListener('load', function() { + + var scene = new THREE.Scene(); + var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 ); + var renderer = new THREE.WebGLRenderer( { antialias: true } ); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0x020202, 0); + + var controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.enableZoom = true; + controls.target.set(0, 0, 0); + controls.rotateSpeed = 0.3; + controls.zoomSpeed = 1.0; + controls.panSpeed = 2.0; + + document.body.appendChild(renderer.domElement); + + // resize the canvas when the window changes + window.addEventListener('resize', function() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + }); + + // assign THREE.js objects to the object we will return + framework.scene = scene; + framework.camera = camera; + framework.renderer = renderer; + + // begin the animation loop + (function tick() { + stats.begin(); + update(framework); // perform any requested updates + renderer.render(scene, camera); // render the scene + stats.end(); + requestAnimationFrame(tick); // register to call this again when the browser renders a new frame + })(); + + // we will pass the scene, gui, renderer, camera, etc... to the callback function + return callback(framework); + }); +} + +export default { + init: init +} + +export const PI = 3.14159265 +export const e = 2.7181718 \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..06a421e0 --- /dev/null +++ b/src/main.js @@ -0,0 +1,327 @@ + +const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much +import Framework from './framework' + +var clock = new THREE.Clock(); +var t = 0.0; + +var grid = []; +var agents = []; +var markers = []; +var settings = { + minMarkersPerGrid: 1, + maxMarkersPerGrid: 1, + //markersResolution: 2, + gridResolution: 80, + agentRadius: 0.5, + numAgents: 20, + displayMarkers: true +}; + + +// returns a pair of ints to access grid +function coordToGrid(x, y) { + return new THREE.Vector2(Math.floor((x + 5.0) * settings.gridResolution / 10.0), Math.floor((y + 5.0) * settings.gridResolution / 10.0)); +} + +// returns a centerpoint of the grid +function gridToCoord(x, y) { + return new THREE.Vector2((x + 0.5) * 10.0 / settings.gridResolution - 5.0, (y + 0.5) * 10.0 / settings.gridResolution - 5.0) +} + +var Agent = function(pos, vel, tgt) { + return { + position: new THREE.Vector3(pos.x, pos.y, pos.z), + velocity: new THREE.Vector3(vel.x, vel.y, vel.z), + target: new THREE.Vector3(tgt.x, tgt.y, tgt.z), + markers: [] + } +} + +var Marker = function(pos, agent) { + return { + pos: new THREE.Vector3(pos.x, pos.y, pos.z), + agent: null + } +} + +function GridSquare(m, a) { + this.markers = m; + this.agents = a; +} + +function generateGrid() { + grid = []; // reset grid + + for (var i = 0; i < settings.gridResolution; i++) { + var row = []; + for (var j = 0; j < settings.gridResolution; j++) { + var square = new GridSquare([], []); + var numMarkers = Math.floor(Math.random() * + (1 + settings.maxMarkersPerGrid - settings.minMarkersPerGrid)) + settings.minMarkersPerGrid; + var center = gridToCoord(i, j); + for (var m = 0; m < numMarkers; m++) { + var shift = new THREE.Vector2((Math.random() - 0.5) * 10.0 / settings.gridResolution, + (Math.random() - 0.5) * 10.0 / settings.gridResolution); + shift = shift.addVectors(shift, center); + var marker = new Marker(new THREE.Vector3(shift.x, 0, shift.y), null); + square.markers.push(marker); + markers.push(marker); + } + + row.push(square); + } + grid.push(row); + + } +} + +function generateAgents(scene) { + agents = []; // reset agents + + var agentsMat = new THREE.MeshPhongMaterial( { + side: THREE.DoubleSide, + color: 0xaa1111 + }); + var agentsGeo = new THREE.SphereGeometry(0.1); + + for (var i = 0; i < settings.numAgents; i++) { + var theta = Math.PI * 2 * i / settings.numAgents; + var p = (new THREE.Vector3(Math.cos(theta), 0.01, Math.sin(theta))).multiplyScalar(4.5); + var t = (new THREE.Vector3(Math.cos(theta + Math.PI), 0.01, Math.sin(theta + Math.PI))).multiplyScalar(4.5); + var cell = coordToGrid(p.x, p.z); + + var agent = new Agent(p, new THREE.Vector3(0,0,0), t); + var aMesh = new THREE.Mesh(agentsGeo, agentsMat); + aMesh.userData.agentID = i; + aMesh.position.set(p.x, 0.1, p.z); + scene.add(aMesh); + agents.push(agent); + grid[cell.x][cell.y].agents.push(agent); + } +} + +function generateAgentsRows(scene) { + agents = []; // reset agents + + var agentsMat = new THREE.MeshPhongMaterial( { + side: THREE.DoubleSide, + color: 0xaa1111 + }); + var agentsGeo = new THREE.SphereGeometry(0.1); + + for (var i = 0; i < settings.numAgents; i++) { + var sign = i % 2 == 0; + var s = sign?1:-1; + var p = new THREE.Vector3(i / settings.numAgents * 9 - 4.5, 0.01, s * -4.5); + var t = new THREE.Vector3(i / settings.numAgents * 9 - 4.5, 0.01, s * 4.5); + var cell = coordToGrid(p.x, p.z); + + var agent = new Agent(p, new THREE.Vector3(0,0,0), t); + var aMesh = new THREE.Mesh(agentsGeo, agentsMat); + aMesh.userData.agentID = i; + aMesh.position.set(p.x, 0.1, p.z); + scene.add(aMesh); + agents.push(agent); + grid[cell.x][cell.y].agents.push(agent); + } +} + + + +function setWeightedVel(agent) { + var pos = agent.position; + var tgt = agent.target; + var toTarget = (new THREE.Vector3(0, 0, 0)).subVectors(tgt, pos); + toTarget.y = 0; + if (toTarget.length() < 0.01) { + return; + } + + // sum and record all weights + var totalWeight = 0.0; + var weights = []; + var displacements = []; + + for (var m = 0; m < agent.markers.length; m++) { + var mpos = agent.markers[m].pos; + var toMark = (new THREE.Vector3(0, 0, 0)).subVectors(mpos, pos); + toMark.y = 0.0; + var wf = 1.0 / (1.0 + toMark.length()); + + wf = wf * (1.0 + toMark.dot(toTarget) / toMark.length() / toTarget.length()); + totalWeight += wf; + weights.push(wf); + displacements.push(toMark); + } + + + var totalVelocity = new THREE.Vector3(0, 0, 0); + if (totalWeight == 0.0) { + return; + } + + for (var i = 0; i < weights.length; i++) { + displacements[i].multiplyScalar(weights[i] / totalWeight); + totalVelocity.add(displacements[i]); + } + + // clamp maximum to max radial influence + if (totalVelocity.length() > settings.agentRadius) { + totalVelocity = totalVelocity.normalize().multiplyScalar(settings.agentRadius); + } + + agent.velocity = totalVelocity; +} + +function updateSimulation(dt) { + + // clear all velocities and ownership + for (var a = 0; a < agents.length; a++) { + agents[a].velocity = new THREE.Vector3(0, 0, 0); + agents[a].markers = []; + } + for (var m = 0; m < markers.length; m++) { + markers[m].agent = null; + } + + // list of active markers for this iteration + var activeMarkers = []; + var gridRadius = Math.round(settings.agentRadius * settings.gridResolution / 10.0); + + for (var a = 0; a < agents.length; a++) { + var g = coordToGrid(agents[a].position.x, agents[a].position.z); + + for (var rx = -gridRadius; rx <= gridRadius; rx++) { + for (var ry = -gridRadius; ry <= gridRadius; ry++) { + if (rx + g.x < 0 || rx + g.x >= settings.gridResolution) continue; + if (ry + g.y < 0 || ry + g.y >= settings.gridResolution) continue; + var sq = grid[g.x + rx][g.y + ry]; + + for (var i = 0; i < sq.markers.length; i++) { + if (sq.markers[i].agent == null) { + sq.markers[i].agent = agents[a]; + activeMarkers.push(sq.markers[i]); + } else { + var currDist = sq.markers[i].pos.distanceToSquared(sq.markers[i].agent.position); + var newDist = sq.markers[i].pos.distanceToSquared(agents[a].position); + if (newDist < currDist) sq.markers[i].agent = agents[a]; + } + } + } + } + } + + // add all active markers to lists + for (var am = 0; am < activeMarkers.length; am++) { + var ag = activeMarkers[am].agent; + ag.markers.push(activeMarkers[am]); + } + + var min = new THREE.Vector3(-4.99, -5, -4.99); + var max = new THREE.Vector3(4.99, 5, 4.99); + + // update all positions + for (var a = 0; a < agents.length; a++) { + + // find velocity + setWeightedVel(agents[a]); + + if (agents[a].velocity.length > settings.agentRadius) { + agents[a].velocity = agents[a].velocity.normalize().multiplyScalar(settings.agentRadius); + } + agents[a].position.addScaledVector(agents[a].velocity, dt); + agents[a].position.clamp(min, max) + } + + // clear cell agents + for (var i = 0; i < settings.gridResolution; i++) { + for (var j = 0; j < settings.gridResolution; j++) { + grid[i][j].agents = []; + } + } + + // update cell agents + for (var a = 0; a < agents.length; a++) { + var g = coordToGrid(agents[a].position.x, agents[a].position.z); + grid[g.x][g.y].agents.push(agents[a]); + } +} + +// called after the scene loads +function onLoad(framework) { + var scene = framework.scene; + var camera = framework.camera; + var renderer = framework.renderer; + var gui = framework.gui; + var stats = framework.stats; + + camera.position.set(1, 1, 5); + camera.lookAt(new THREE.Vector3(0,0,0)); + + var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.9 ); + directionalLight.color.setHSL(0.1, 1, 0.95); + directionalLight.position.set(3, 2, 2); + directionalLight.position.multiplyScalar(10); + scene.add(directionalLight); + + var planeGeo = new THREE.PlaneGeometry(10, 10, 20, 20); + var planeMat = new THREE.MeshPhongMaterial( { + side: THREE.DoubleSide, + color: 0xdddddd + }); + planeGeo.applyMatrix( new THREE.Matrix4().makeRotationX(-Math.PI / 2.0)); + + var plane = new THREE.Mesh(planeGeo, planeMat); + scene.add(plane); + + // initialize sumulation data + generateGrid(); + generateAgents(scene); + + var marks = new THREE.Geometry(); + var marksMat = new THREE.PointsMaterial({ + color: 0xaaddff, + size: 0.01 + }); + for (var i = 0; i < settings.gridResolution; i++) { + for (var j = 0; j < settings.gridResolution; j++) { + var square = grid[i][j]; + for (var m = 0; m < square.markers.length; m++) { + marks.vertices.push(new THREE.Vector3(square.markers[m].pos.x, 0.0, square.markers[m].pos.z)); + } + } + } + var mMesh = new THREE.Points(marks, marksMat); + mMesh.name = "markers"; + scene.add(mMesh); + + var obj = { CircleScenario:function(){ generateAgents(scene)}, + RowsScenario:function(){ generateAgentsRows(scene)}}; + + gui.add(obj,'CircleScenario'); + gui.add(obj,'RowsScenario'); + gui.add(settings, 'numAgents', 10, 50); + +} + +// called on frame updates +function onUpdate(framework) { + var dt = clock.getDelta(); + + if (grid.length > 0 && agents.length > 0) { + updateSimulation(dt); + framework.scene.traverse(function(object) { + if (object instanceof THREE.Mesh) { + if ('agentID' in object.userData) { + var a = agents[object.userData.agentID]; + object.position.set(a.position.x, a.position.y, a.position.z); + } + } + }); + } + +} + +// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate +Framework.init(onLoad, onUpdate); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..57dce485 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,28 @@ +const path = require('path'); + +module.exports = { + entry: path.join(__dirname, "src/main"), + output: { + filename: "./bundle.js" + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel', + query: { + presets: ['es2015'] + } + }, + { + test: /\.glsl$/, + loader: "webpack-glsl" + }, + ] + }, + devtool: 'source-map', + devServer: { + port: 7000 + } +} \ No newline at end of file