diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..3c3629e6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.vscode/.browse.VC.db b/.vscode/.browse.VC.db
new file mode 100644
index 00000000..1903eae8
Binary files /dev/null and b/.vscode/.browse.VC.db differ
diff --git a/README.md b/README.md
index 71a9296d..8cc4e7f5 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,4 @@
# CIS700 Procedural Graphics: Final Project
-Time to show off your new bag of procedural tricks by creating one polished final project. For this assignment you will have four weeks to create and document a portfolio piece that demonstrates your mastery of procedural thinking and implementation. You may work in groups of up to three (working alone is fine too). You may use any language / platform you choose for this assignment (given our approval if it’s not JavaScript/WebGL or C++/OpenGL).
-
-As usual with this class, we want to encourage you to take this opportunity to explore and experiment. To get you started, however, we’ve provided a few open-ended prompts below. Interesting and complex visuals are the goal in all of these prompts, but they encourage focus on different aspects of proceduralism.
-
-## Prompts:
-
-- ### A classic 4k demo
- * In the spirit of the demo scene, create an animation that fits into a 4k executable that runs in real-time. Feel free to take inspiration from the many existing demos. Focus on efficiency and elegance in your implementation.
- * Examples: [cdak by Quite & orange](https://www.youtube.com/watch?v=RCh3Q08HMfs&list=PLA5E2FF8E143DA58C)
-
-- ### A forgery
- * Taking inspiration from a particular natural phenomenon or distinctive set of visuals, implement a detailed, procedural recreation of that aesthetic. This includes modeling, texturing and object placement within your scene. Does not need to be real-time. Focus on detail and visual accuracy in your implementation.
- * Examples:
- - [Snail](https://www.shadertoy.com/view/ld3Gz2), [Journey](https://www.shadertoy.com/view/ldlcRf), Big Hero 6 Wormhole: [Image 1](http://2.bp.blogspot.com/-R-6AN2cWjwg/VTyIzIQSQfI/AAAAAAAABLA/GC0yzzz4wHw/s1600/big-hero-6-disneyscreencaps.com-10092.jpg) , [Image 2](https://i.stack.imgur.com/a9RGL.jpg)
-
-- ### A game level
- * Like generations of game makers before us, create a game which generates an navigable environment (eg. a roguelike dungeon, platforms) and some sort of goal or conflict (eg. enemy agents to avoid or items to collect). Must run in real-time. Aim to create an experience that will challenge players and vary noticeably in different playthroughs, whether that means complex dungeon generation, careful resource management or a sophisticated AI model. Focus on designing a system that will generate complex challenges and goals.
- * Examples: Spore, Dwarf Fortress, Minecraft, Rogue
-
-- ### An animated environment / music visualizer
- * Create an environment full of interactive procedural animation. The goal of this project is to create an environment that feels responsive and alive. Whether or not animations are musically-driven, sound should be an important component. Focus on user interactions, motion design and experimental interfaces.
- * Examples: [Panoramical](https://www.youtube.com/watch?v=gBTTMNFXHTk), [Bound](https://www.youtube.com/watch?v=aE37l6RvF-c)
-- ### Own proposal
- * You are of course **welcome to propose your own topic**. Regardless of what you choose, you and your team must research your topic and relevant techniques and come up with a detailed plan of execution. You will meet with some subset of the procedural staff before starting implementation for approval.
-
-**Final grading will be individual** and will be based on both the final product and how well you were able to achieve your intended effect according to your execution plan. Plans change of course, and we don’t expect you to follow your execution plan to a T, but if your final project looks pretty good, but you cut corners and only did 30% of what you outlined in your design doc, you will be penalized.
-
-But overall, have fun! This is your opportunity to work on whatever procedural project inspires you. The best way to ensure a good result is to pick something you’re passionate about. :)
-
-## Timeline
-
-- 4/08 Design doc due / Have met with procedural staff
-- 4/18 Milestone 1 (short write-up + demo)
-- 4/25 Milestone 2 (short write-up + demo)
-- 5/3 Final presentations (3-5 pm, Siglab), final reports due
-
-## Design Doc
-
-Your design doc should follow the following template. Note, each section can be pretty short, but cover them all! This will serve as valuable documentation when showing this project off in the future AND doing a good job will make it much easier for you to succeed, so please take this seriously.
-
-### Design Doc Template:
-
-- #### Introduction
- * What motivates this project?
-- #### Goal
- * What do you intend to achieve with this project?
-- #### Inspiration/reference:
- * Attach some materials, visual or otherwise you intend as reference
-- #### Specification:
- * Outline the main features of your project
-- #### Techniques:
- * What are the main technical/algorithmic tools you’ll be using? Give an overview, citing specific papers/articles
-- #### Design:
- * How will your program fit together? Make a simple free-body diagram illustrating the pieces.
-- #### Timeline:
- * Create a week-by-week set of milestones for each person in your group.
-
-
-Along with your final project demo, you will submit a final report, in which you will update correct your original design doc as needed and add a few post-mortem items.
-
-## Milestones
-
-To keep you honest / on-track, we will be checking on your progress at weekly intervals, according to milestones you’ll define at the outset (pending our approval). For each of the two milestones prior to the final submission, you will submit a short write up explaining whether or not you individually achieved your goals (specifying the files where the work happened), along with a link to a demo / images. These don’t have to be super polished -- we just want to see that you’re getting things done.
-
-Example:
-
-“Milestone 1:
- Adam:
-Made some procedural terrain code in src/terrain.js. Implemented 3D simplex noise to do it. Also applied coloring via custom shader based on this cool paper X (see src/shaders/dirt.glsl). IMAGE
-
-Austin:
-I managed to set up my voronoi diagram shader (see src/shaders/voronoi.glsl).
-Experimented with different scattering techniques. It’s working with the euclidean distance metric. I’m using it in src/main.js to color stones. IMAGE
-
-Rachel:
-I tried really hard to make my toon shader work (src/shaders/toon.glsl), but I still have a bug! T_T BUGGY IMAGE. DEMO LINK”
-
-## Final Report
-
-In addition to your demo, you will create a final report documenting your project overall. This document should be clear enough to explain the value and details of your project to a random computer graphics person with no knowledge of this class.
-
-### Final Report Template:
-
-- #### Updated design doc:
- * All the sections of your original design doc, corrected if necessary
-- #### Results:
- * Provide images of your finished project
-- #### Evaluation (this is a big one!):
- * How well did you do? What parameters did you tune along the way? Include some WIP shots that compare intermediate results to your final. Explain why you made the decisions you did.
-- #### Future work:
- * Given more time, what would you add/improve
-- #### Acknowledgements:
- * Cite _EVERYTHING_. Implemented a paper? Used some royalty-free music? Talked to classmates / a professor in a way that influenced your project? Attribute everything!
-
-## Logistics
-
-Like every prior project, your code will be submitted via github. Fork the empty final project repo and start your code base from there. Take this as an opportunity to practice using git properly in a team setting if you’re a new user. For each weekly submission, provide a link to your pull request. Your repo will contain all the code and documentation associated with your project. The readme for your repo will eventually be your final report. At the top level, include a folder called “documentation”, where you’ll put your design doc and milestone write-ups.
-
-Don’t wait to merge your code! Seriously, there be dragons. Try to have a working version including all your code so that compatibility and merge issues don’t sneak up on you near the end.
-
-## Grading
-
-- 15% Design Doc (graded as a group)
-- 15% Milestone 1 (graded as a group)
-- 15% Milestone 2 (graded as a group)
-- 55% Final demo + report (graded individually)
-
-NOTE: We’ve been pretty lax about our late policy throughout the semester, but our margins on the final project are tight, therefore late submissions will NOT be accepted. If you have a significant reason for being unable to complete your goals, talk to us, and we’ll discuss getting you an incomplete and figure out an adjusted work plan with your group.
-
-
+Design Doc: `./writeups/design_doc.md`
+Milestone #1 Doc: `./writeups/milestone1_doc.md`
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100755
index 00000000..f775186a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+ HW1: Noise
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..35d3ff47
--- /dev/null
+++ b/package.json
@@ -0,0 +1,34 @@
+{
+ "scripts": {
+ "start": "webpack-dev-server --hot --inline",
+ "build": "webpack",
+ "deploy": "gh-pages-deploy"
+ },
+ "gh-pages-deploy": {
+ "prep": [
+ "build"
+ ],
+ "noprompt": true
+ },
+ "dependencies": {
+ "chroma-js": "^1.3.3",
+ "dat-gui": "^0.5.0",
+ "gl-matrix": "^2.3.2",
+ "lodash": "^4.17.4",
+ "noisejs": "^2.1.0",
+ "seedrandom": "^2.4.3",
+ "stats-js": "^1.0.0-alpha1",
+ "three": "^0.82.1",
+ "three-orbit-controls": "^82.1.0",
+ "voronoi": "^1.0.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 100755
index 00000000..1657541c
--- /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({ width: 400 });
+
+ 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/geography-manager/geography-manager.js b/src/geography-manager/geography-manager.js
new file mode 100644
index 00000000..03c06584
--- /dev/null
+++ b/src/geography-manager/geography-manager.js
@@ -0,0 +1,325 @@
+const THREE = require('three');
+const _ = require('lodash');
+const NOISEJS = require('noisejs');
+const CHROMA = require('chroma-js');
+const SEEDRANDOM = require('seedrandom');
+
+export default class GeographyManager {
+ constructor(options, map) {
+ this.map = map;
+ this.seedElevation = options.seedElevation;
+ this.seedMoisture = options.seedMoisture;
+ this.seedErosion = options.seedErosion;
+ this.elevationNoisiness = options.elevationNoisiness;
+ this.moistureNoisiness = options.moistureNoisiness;
+ this.erosionSteps = options.erosionSteps;
+ this.rainDistribution = options.rainDistribution;
+ this.rainFrequency = options.rainFrequency;
+ this.biomes = {
+ SNOW: 'SNOW',
+ TUNDRA: 'TUNDRA',
+ BARE: 'BARE',
+ SCORCHED: 'SCORCHED',
+ TAIGA: 'TAIGA',
+ SHRUBLAND: 'SHRUBLAND',
+ TEMPERATE_DESERT: 'TEMPERATE_DESERT',
+ TEMPERATE_RAIN_FOREST: 'TEMPERATE_RAIN_FOREST',
+ TEMPERATE_DECIDUOUS_FOREST: "TEMPERATE_DECIDUOUS_FOREST",
+ GRASSLAND: 'GRASSLAND',
+ TROPICAL_RAIN_FOREST: 'TROPICAL_RAIN_FOREST',
+ TROPICAL_SEASONAL_FOREST: 'TROPICAL_SEASONAL_FOREST',
+ SUBTROPICAL_DESERT: 'SUBTROPICAL_DESERT',
+ BEACH: 'BEACH',
+ OCEAN: 'OCEAN'
+ };
+ this.biomeColors = {
+ SNOW: CHROMA(248, 248, 248),
+ TUNDRA: CHROMA(221, 222, 185),
+ BARE: CHROMA(187, 187, 187),
+ SCORCHED: CHROMA(153, 153, 153),
+ TAIGA: CHROMA(204, 212, 186),
+ SHRUBLAND: CHROMA(196, 204, 186),
+ TEMPERATE_DESERT: CHROMA(205, 215, 166),
+ TEMPERATE_RAIN_FOREST: CHROMA(96, 154, 111),
+ TEMPERATE_DECIDUOUS_FOREST: CHROMA(179, 202, 168),
+ GRASSLAND: CHROMA(152, 181, 109),
+ TROPICAL_RAIN_FOREST: CHROMA(83, 140, 111),
+ TROPICAL_SEASONAL_FOREST: CHROMA(110, 167, 95),
+ SUBTROPICAL_DESERT: CHROMA(213, 193, 153),
+ BEACH: CHROMA(172, 159, 138),
+ OCEAN: CHROMA(54, 53, 98)
+ };
+ }
+
+ generateGeography() {
+ this._generateElevationMap();
+ this._generateMoistureMap();
+ this._generateErosion();
+ this._generateCoastline();
+ this._distributeBiomes();
+ }
+
+ _generateCoastline() {
+ var nodes = this.map.graphManager.nodes;
+
+ nodes.forEach(function(node) {
+ var adjToLand = false;
+ var adjToOcean = false;
+
+ node.cells.forEach(function(cell) {
+ if (cell.elevation <= 0) adjToOcean = true;
+ if (cell.elevation > 0) adjToLand = true;
+ });
+
+ if (adjToOcean && adjToLand) {
+ node.isCoastal = true;
+ }
+ });
+ }
+
+ _generateElevationMap() {
+ var nodes = this.map.graphManager.nodes;
+ var cells = this.map.graphManager.cells;
+ var numCells = this.map.graphManager.numCells;
+ var seed = this.seedElevation;
+ var noise = new NOISEJS.Noise(seed);
+ var f = CHROMA.scale(['008ae5', 'yellow']).domain([-1, 1]);
+
+ nodes.forEach(function(node) {
+ var elevation = noise.simplex2(node.pos.x / numCells * this.elevationNoisiness,
+ node.pos.y / numCells * this.elevationNoisiness);
+
+ node.elevationColor = f(elevation);
+ node.elevation = elevation;
+ }, this);
+
+ cells.forEach(function(cell) {
+ var colors = [];
+ var elevations = [];
+
+ cell.corners.forEach(function(corner) {
+ colors.push(corner.elevationColor);
+ elevations.push(corner.elevation);
+ });
+
+ cell.elevationColor = CHROMA.average(colors);
+ cell.elevation = _.mean(elevations);
+ });
+ }
+
+ _generateMoistureMap() {
+ var nodes = this.map.graphManager.nodes;
+ var cells = this.map.graphManager.cells;
+ var numCells = this.map.graphManager.numCells;
+ var seed = this.seedMoisture;
+ var noise = new NOISEJS.Noise(seed);
+ var f = CHROMA.scale(['fba271', '5070ff']).domain([-1, 1]);
+
+ nodes.forEach(function(node) {
+ var moisture = noise.simplex2(node.pos.x / numCells * this.moistureNoisiness,
+ node.pos.y / numCells * this.moistureNoisiness);
+
+ node.moistureColor = f(moisture);
+ node.moisture = moisture;
+ }, this);
+
+ cells.forEach(function(cell) {
+ var colors = [];
+ var moistures = [];
+
+ cell.corners.forEach(function(corner) {
+ colors.push(corner.moistureColor);
+ moistures.push(corner.moisture);
+ });
+
+ cell.moistureColor = CHROMA.average(colors);
+ cell.moisture = _.mean(moistures);
+ });
+ }
+
+ _generateErosion() {
+ for (var i = 0; i < this.erosionSteps; i++) {
+ var droplets = this._spawnDroplets();
+ var maxLimit = 1000;
+ var l = 0;
+
+ while (l < maxLimit) {
+ var nextDroplets = [];
+ var dropletMaxSpeed = 1.0;
+
+ if (droplets.length === 0) l = maxLimit;
+
+ droplets.forEach(function(droplet) {
+ var node = droplet.node;
+ var elevation = node.elevation;
+ var lowestNeighbor = node.getLowestNeighbor();
+ var lowestElevation = lowestNeighbor.elevation;
+
+ var elevationDelta = elevation - lowestElevation;
+ var lowestNeighborMoisture = lowestNeighbor.moisture;
+ var sedimentChange = (elevationDelta / 10.0) * droplet.speed;
+
+ // Lowest elevation is higher than current elevation
+ if (elevation < lowestElevation) {
+ sedimentChange = -1 * Math.min(droplet.sediment, -1 * elevationDelta);
+ }
+
+ node.elevation -= sedimentChange
+ droplet.sediment += sedimentChange;
+ droplet.node = lowestNeighbor;
+ droplet.speed = Math.max(dropletMaxSpeed, droplet.speed + elevationDelta);
+
+ // If new node is above sea level, add it
+ if (lowestElevation > 0.0) {
+ nextDroplets.push(droplet);
+ }
+ });
+
+ droplets = nextDroplets;
+ l++;
+ }
+ }
+
+ this._updateCellElevations();
+ this._updateElevationColors();
+ }
+
+ _spawnDroplets() {
+ var nodes = this.map.graphManager.nodes;
+ var rng = SEEDRANDOM(this.seedErosion);
+ var droplets = [];
+
+ class Droplet {
+ constructor(node) {
+ this.speed = 0.0;
+ this.sediment = 0.0;
+ this.node = node;
+ }
+ }
+
+ nodes.forEach(function(node) {
+
+ var rand = rng() / this.rainFrequency;
+ var dropletCondition = (this.rainDistribution === 'moisture map') ? (rand < node.moisture) :
+ (this.rainDistribution === 'uniform') ? (rand < 0.5) :
+ (rand < 0.5);
+
+ if (dropletCondition && node.elevation > 0.0) {
+ var droplet = new Droplet(node);
+
+ droplets.push(droplet);
+ node.spawnedDroplet = true;
+ }
+
+ rng = SEEDRANDOM(rng());
+ }, this);
+
+ return droplets;
+ }
+
+ _updateCellElevations() {
+ var cells = this.map.graphManager.cells;
+
+ cells.forEach(function(cell) {
+ var cellElevation = 0;
+ var corners = cell.corners;
+
+ corners.forEach(function(node) {
+ cellElevation += node.elevation;
+ });
+
+ cell.elevation = cellElevation / corners.length;
+ });
+ }
+
+ _updateElevationColors() {
+ var nodes = this.map.graphManager.nodes;
+ var cells = this.map.graphManager.cells;
+ var f = CHROMA.scale(['008ae5', 'yellow']).domain([-1, 1]);
+
+ nodes.forEach(function(node) {
+ node.elevationColor = f(node.elevation);
+ }, this);
+
+ cells.forEach(function(cell) {
+ var colors = [];
+
+ cell.corners.forEach(function(corner) {
+ colors.push(corner.elevationColor);
+ });
+
+ cell.elevationColor = CHROMA.average(colors);
+ });
+ }
+
+ _distributeBiomes() {
+ var nodes = this.map.graphManager.nodes;
+ var cells = this.map.graphManager.cells;
+
+ cells.forEach(function(cell) {
+ var elevation = cell.elevation;
+ var moisture = cell.moisture;
+ var biome = this._getBiome(elevation, moisture);
+
+ if (cell.isCoastal()) {
+ biome = this.biomes.BEACH;
+ }
+
+ cell.biome = biome;
+ cell.biomeColor = this.biomeColors[biome];
+ }, this);
+
+ nodes.forEach(function(node) {
+ var elevation = node.elevation;
+ var moisture = node.moisture;
+ var biome = this._getBiome(elevation, moisture);
+
+ if (node.isCoastal) {
+ biome = this.biomes.BEACH;
+ }
+
+ node.biome = biome;
+ node.biomeColor = this.biomeColors[biome];
+ }, this);
+ }
+
+ _getBiome(elevation, moisture) {
+ if (elevation <= 0) return this.biomes.OCEAN;
+
+ if (elevation <= 0.25) {
+ if (moisture <= -0.66) return this.biomes.TROPICAL_RAIN_FOREST;
+ if (moisture <= -0.33) return this.biomes.TROPICAL_RAIN_FOREST;
+ if (moisture <= 0) return this.biomes.TROPICAL_SEASONAL_FOREST;
+ if (moisture <= 0.33) return this.biomes.TROPICAL_SEASONAL_FOREST;
+ if (moisture <= 0.66) return this.biomes.GRASSLAND;
+ if (moisture <= 1) return this.biomes.SUBTROPICAL_DESERT;
+ }
+
+ if (elevation <= 0.5) {
+ if (moisture <= -0.66) return this.biomes.TEMPERATE_RAIN_FOREST;
+ if (moisture <= -0.33) return this.biomes.TEMPERATE_DECIDUOUS_FOREST;
+ if (moisture <= 0) return this.biomes.TEMPERATE_DECIDUOUS_FOREST;
+ if (moisture <= 0.33) return this.biomes.GRASSLAND;
+ if (moisture <= 0.66) return this.biomes.GRASSLAND;
+ if (moisture <= 1) return this.biomes.TEMPERATE_DESERT;
+ }
+
+ if (elevation <= 0.75) {
+ if (moisture <= -0.66) return this.biomes.TAIGA;
+ if (moisture <= -0.33) return this.biomes.TAIGA;
+ if (moisture <= 0) return this.biomes.SHRUBLAND;
+ if (moisture <= 0.33) return this.biomes.SHRUBLAND;
+ if (moisture <= 0.66) return this.biomes.TEMPERATE_DESERT;
+ if (moisture <= 1) return this.biomes.TEMPERATE_DESERT;
+ }
+
+ if (elevation <= 1) {
+ if (moisture <= -0.66) return this.biomes.SNOW;
+ if (moisture <= -0.33) return this.biomes.SNOW;
+ if (moisture <= 0) return this.biomes.SNOW;
+ if (moisture <= 0.33) return this.biomes.TUNDRA;
+ if (moisture <= 0.66) return this.biomes.BARE;
+ if (moisture <= 1) return this.biomes.SCORCHED;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/graph-manager/cell.js b/src/graph-manager/cell.js
new file mode 100644
index 00000000..ad1c982f
--- /dev/null
+++ b/src/graph-manager/cell.js
@@ -0,0 +1,30 @@
+export default class Cell {
+ constructor() {
+ this.center = {};
+ this.halfedges = [];
+ this.corners = [];
+ this.neighbors = [];
+ }
+
+ // getElevation() {
+ // var elevation = 0;
+
+ // _.forEach(this.corners, function(corner) {
+ // elevation += corner.elevation;
+ // });
+
+ // return elevation / this.corners.length;
+ // }
+
+ isCoastal() {
+ var adjToOcean = false;
+
+ if (this.elevation <= 0) return false;
+
+ this.neighbors.forEach(function(cell) {
+ if (cell.elevation <= 0) adjToOcean = true;
+ });
+
+ return adjToOcean;
+ }
+}
\ No newline at end of file
diff --git a/src/graph-manager/edge.js b/src/graph-manager/edge.js
new file mode 100644
index 00000000..6d8f01ad
--- /dev/null
+++ b/src/graph-manager/edge.js
@@ -0,0 +1,6 @@
+export default class Edge {
+ constructor(nodeA, nodeB) {
+ this.nodeA = nodeA;
+ this.nodeB = nodeB;
+ }
+}
\ No newline at end of file
diff --git a/src/graph-manager/graph-manager.js b/src/graph-manager/graph-manager.js
new file mode 100644
index 00000000..9e8d05b5
--- /dev/null
+++ b/src/graph-manager/graph-manager.js
@@ -0,0 +1,429 @@
+const _ = require('lodash');
+const VORONOI = require('voronoi');
+const SEEDRANDOM = require('seedrandom');
+
+import Cell from './cell'
+import Edge from './edge'
+import Node from './node'
+import HalfEdge from './halfedge'
+
+export default class GraphManager {
+ constructor(options, map) {
+ this.seedVoronoi = options.seedVoronoi;
+ this.numCells = options.numCells;
+ this.cellType = options.cellType;
+ this.map = map;
+ this.nodes = [];
+ this.edges = [];
+ this.cells = [];
+ }
+
+ generateGrid() {
+ if (this.cellType === 'square') {
+ this._generateFromSquareGrid();
+ } else if (this.cellType === 'hex') {
+ this._generateFromHexGrid();
+ } else if (this.cellType === 'voronoi') {
+ this._generateFromVoronoiGrid();
+ } else {
+ // Unrecognized cell type
+ }
+ }
+
+ _generateFromSquareGrid() {
+ var nodesMap = {};
+ var edgesMap = {};
+
+ for (var x = 0; x <= this.numCells; x++) {
+ for (var y = 0; y <= this.numCells; y++) {
+ var node = new Node(x, y);
+ var nodesMapIndex = x + ' ' + y;
+
+ nodesMap[nodesMapIndex] = node;
+ }
+ }
+
+ for (var x = 0; x <= this.numCells; x++) {
+ for (var y = 0; y <= this.numCells; y++) {
+ var nodesMapIndex = x + ' ' + y;
+ var node = nodesMap[nodesMapIndex];
+ var adjCoors = [ [ x-1, y ], [ x+1, y ], [ x, y-1 ], [ x, y+1 ] ];
+
+ adjCoors.forEach(function(coors) {
+ var xAdj = coors[0];
+ var yAdj = coors[1];
+
+ if (xAdj >= 0 && xAdj <= this.numCells &&
+ yAdj >= 0 && yAdj <= this.numCells) {
+ var adjNodeMapIndex = xAdj + ' ' + yAdj;
+ var adjNode = nodesMap[adjNodeMapIndex];
+ var edgesMapIndex = nodesMapIndex + ' ' + adjNodeMapIndex;
+ var edgesMapOppositeIndex = adjNodeMapIndex + ' ' + nodesMapIndex;
+ var edgeInMap = edgesMap[edgesMapIndex] || edgesMap[edgesMapOppositeIndex];
+
+ if (!edgeInMap) {
+ var edge = new Edge(node, adjNode);
+
+ edgesMap[edgesMapIndex] = edge;
+ node.neighbors.push(adjNode);
+ adjNode.neighbors.push(node);
+ }
+ }
+ }, this);
+ }
+ }
+
+ for (var x = 0; x < this.numCells; x++) {
+ for (var y = 0; y < this.numCells; y++) {
+ var cell = new Cell();
+
+ var edgesMapIndexA = (x+1) + ' ' + y + ' ' + x + ' ' + y;
+ var edgesMapIndexB = x + ' ' + y + ' ' + x + ' ' + (y+1);
+ var edgesMapIndexC = x + ' ' + (y+1) + ' ' + (x+1) + ' ' + (y+1);
+ var edgesMapIndexD = (x+1) + ' ' + (y+1) + ' ' + (x+1) + ' ' + y;
+ var edgesMapIndices = [ edgesMapIndexA, edgesMapIndexB, edgesMapIndexC, edgesMapIndexD ];
+ var nodesMapIndices = [ (x+1) + ' ' + y, x + ' ' + y, x + ' ' + (y+1), (x+1) + ' ' + (y+1) ];
+
+ for (var i = 0; i < 4; i++) {
+ var edgesMapIndex = edgesMapIndices[i];
+ var nodesMapIndexA = nodesMapIndices[i];
+ var nodesMapIndexB = nodesMapIndices[(i+1) % 4];
+
+ var edge = edgesMap[edgesMapIndex];
+ var nodeA = nodesMap[nodesMapIndexA];
+ var nodeB = nodesMap[nodesMapIndexB];
+ var halfedge = new HalfEdge();
+
+ halfedge.cell = cell;
+ halfedge.edge = edge;
+ halfedge.nodeA = nodeA;
+ halfedge.nodeB = nodeB;
+ cell.halfedges.push(halfedge);
+ cell.corners.push(nodeA);
+ nodeA.cells.push(cell);
+ }
+
+ var numHalfEdges = cell.halfedges.length;
+
+ for (var i = 1; i <= numHalfEdges; i++) {
+ var halfedgeA = cell.halfedges[i-1];
+ var halfedgeB = cell.halfedges[i % numHalfEdges];
+
+ halfedgeA.next = halfedgeB;
+ }
+
+ cell.center.x = x+0.5;
+ cell.center.y = y+0.5;
+ cell.id = this.cells.length;
+
+ this.cells.push(cell);
+ }
+ }
+
+ this.cells.forEach(function(cell) {
+ var neighbors = {};
+
+ cell.corners.forEach(function(node) {
+ node.cells.forEach(function(nodeCell) {
+ var id = nodeCell.id;
+
+ if (id !== cell.id) {
+ neighbors[id] = nodeCell;
+ }
+ });
+ });
+
+ cell.neighbors = _.values(neighbors);
+ });
+
+ this.nodes = _.values(nodesMap);
+ this.edges = _.values(edgesMap);
+ }
+
+ _generateFromHexGrid() {
+ var nodesMap = {};
+ var edgesMap = {};
+
+ for (var x = 0; x < this.numCells; x++) {
+ for (var y = 0; y < this.numCells; y++) {
+ var hexCoors = [ [ 1.5, 0 ], [ 0.5, 0 ], [ 0, 1 ], [ 0.5, 2 ], [ 1.5, 2 ], [ 2, 1 ] ];
+
+ hexCoors.forEach(function(coor) {
+ var xCoor = coor[0] + (x * 2);
+ var yCoor = coor[1] + (y * 2);
+
+ if (x > 0) {
+ xCoor -= (0.5 * x);
+ }
+
+ if (x % 2) {
+ yCoor -= 1;
+ }
+
+ var nodesMapIndex = xCoor + ' ' + yCoor;
+
+ if (!nodesMap[nodesMapIndex]) {
+ var node = new Node(xCoor, yCoor);
+
+ nodesMap[nodesMapIndex] = node
+ }
+ });
+ }
+ }
+
+ for (var x = 0; x < this.numCells; x++) {
+ for (var y = 0; y < this.numCells; y++) {
+ var hexCoors = [ [ 1.5, 0 ], [ 0.5, 0 ], [ 0, 1 ], [ 0.5, 2 ], [ 1.5, 2 ], [ 2, 1 ] ];
+
+ hexCoors.forEach(function(coor) {
+ var xCoor = coor[0] + (x * 2);
+ var yCoor = coor[1] + (y * 2);
+
+ if (x > 0) {
+ xCoor -= (0.5 * x);
+ }
+
+ if (x % 2) {
+ yCoor -= 1;
+ }
+
+ var nodesMapIndex = xCoor + ' ' + yCoor;
+ var node = nodesMap[nodesMapIndex];
+ var adjCoors = [
+ [ xCoor-1, yCoor ],
+ [ xCoor+1, yCoor ],
+ [ xCoor+0.5, yCoor-1 ],
+ [ xCoor-0.5, yCoor-1 ],
+ [ xCoor+0.5, yCoor+1 ],
+ [ xCoor-0.5, yCoor+1 ]
+ ];
+
+ adjCoors.forEach(function(adjCoor) {
+ var xAdj = adjCoor[0];
+ var yAdj = adjCoor[1];
+
+ var adjNodeMapIndex = xAdj + ' ' + yAdj;
+ var adjNode = nodesMap[adjNodeMapIndex];
+
+ if (adjNode) {
+ var edgesMapIndex = nodesMapIndex + ' ' + adjNodeMapIndex;
+ var edgesMapOppositeIndex = adjNodeMapIndex + ' ' + nodesMapIndex;
+ var edgeInMap = edgesMap[edgesMapIndex] || edgesMap[edgesMapOppositeIndex];
+
+ if (!edgeInMap) {
+ var edge = new Edge(node, adjNode);
+
+ edgesMap[edgesMapIndex] = edge;
+ node.neighbors.push(adjNode);
+ adjNode.neighbors.push(node);
+ }
+ }
+ }, this);
+ });
+ }
+ }
+
+ for (var x = 0; x < this.numCells; x++) {
+ for (var y = 0; y < this.numCells; y++) {
+ var cell = new Cell();
+
+ var edgesMapIndexA = (1.5) + ' ' + (0) + ' ' + (0.5) + ' ' + (0);
+ var edgesMapIndexB = (0.5) + ' ' + (0) + ' ' + (0) + ' ' + (1);
+ var edgesMapIndexC = (0) + ' ' + (1) + ' ' + (0.5) + ' ' + (2);
+ var edgesMapIndexD = (0.5) + ' ' + (2) + ' ' + (1.5) + ' ' + (2);
+ var edgesMapIndexE = (1.5) + ' ' + (2) + ' ' + (2) + ' ' + (1);
+ var edgesMapIndexF = (2) + ' ' + (1) + ' ' + (1.5) + ' ' + (0);
+ var edgesMapIndices = [
+ edgesMapIndexA, edgesMapIndexB, edgesMapIndexC,
+ edgesMapIndexD, edgesMapIndexE, edgesMapIndexF
+ ];
+ var nodesMapIndices = [
+ (1.5) + ' ' + (0),
+ (0.5) + ' ' + (0),
+ (0) + ' ' + (1),
+ (0.5) + ' ' + (2),
+ (1.5) + ' ' + (2),
+ (2) + ' ' + (1)
+ ];
+
+ var xOffset = (x > 0) ? -0.5 * x : 0;
+ var yOffset = (x % 2) ? -1 : 0;
+
+ for (var i = 0; i < 6; i++) {
+ var edgesMapIndex = edgesMapIndices[i];
+ var nodesMapIndexA = nodesMapIndices[i];
+ var nodesMapIndexB = nodesMapIndices[(i+1) % 6];
+
+ var edgesMapIndexSplit = edgesMapIndex.split(' ');
+ var nodesMapIndexSplitA = nodesMapIndexA.split(' ');
+ var nodesMapIndexSplitB = nodesMapIndexB.split(' ');
+
+ edgesMapIndexSplit[0] = (Number(edgesMapIndexSplit[0]) + (x*2)) + xOffset;
+ edgesMapIndexSplit[1] = (Number(edgesMapIndexSplit[1]) + (y*2)) + yOffset;
+ edgesMapIndexSplit[2] = (Number(edgesMapIndexSplit[2]) + (x*2)) + xOffset;
+ edgesMapIndexSplit[3] = (Number(edgesMapIndexSplit[3]) + (y*2)) + yOffset;
+
+ nodesMapIndexSplitA[0] = (Number(nodesMapIndexSplitA[0]) + (x*2)) + xOffset;
+ nodesMapIndexSplitA[1] = (Number(nodesMapIndexSplitA[1]) + (y*2)) + yOffset;
+ nodesMapIndexSplitB[0] = (Number(nodesMapIndexSplitB[0]) + (x*2)) + xOffset;
+ nodesMapIndexSplitB[1] = (Number(nodesMapIndexSplitB[1]) + (y*2)) + yOffset;
+
+ edgesMapIndex = edgesMapIndexSplit.join(' ');
+ nodesMapIndexA = nodesMapIndexSplitA.join(' ');
+ nodesMapIndexB = nodesMapIndexSplitB.join(' ');
+
+ var edge = edgesMap[edgesMapIndex];
+ var nodeA = nodesMap[nodesMapIndexA];
+ var nodeB = nodesMap[nodesMapIndexB];
+ var halfedge = new HalfEdge();
+
+ halfedge.cell = cell;
+ halfedge.edge = edge;
+ halfedge.nodeA = nodeA;
+ halfedge.nodeB = nodeB;
+ cell.halfedges.push(halfedge);
+ cell.corners.push(nodeA);
+ nodeA.cells.push(cell);
+ }
+
+ var numHalfEdges = cell.halfedges.length;
+
+ for (var i = 1; i <= numHalfEdges; i++) {
+ var halfedgeA = cell.halfedges[i-1];
+ var halfedgeB = cell.halfedges[i % numHalfEdges];
+
+ halfedgeA.next = halfedgeB;
+ }
+
+ cell.center.x = (x*2)+1 + xOffset;
+ cell.center.y = (y*2)+1 + yOffset;
+ cell.id = this.cells.length;
+
+ this.cells.push(cell);
+ }
+ }
+
+ this.cells.forEach(function(cell) {
+ var neighbors = {};
+
+ cell.corners.forEach(function(node) {
+ node.cells.forEach(function(nodeCell) {
+ var id = nodeCell.id;
+
+ if (id !== cell.id) {
+ neighbors[id] = nodeCell;
+ }
+ });
+ });
+
+ cell.neighbors = _.values(neighbors);
+ });
+
+ this.nodes = _.values(nodesMap);
+ this.edges = _.values(edgesMap);
+ }
+
+ _generateFromVoronoiGrid() {
+ var voronoi = new VORONOI();
+ var rng1 = SEEDRANDOM(this.seedVoronoi);
+ var rng2 = SEEDRANDOM(this.seedVoronoi + 1.0);
+ var bbox = { xl: 0, xr: this.numCells, yt: 0, yb: this.numCells };
+ var sites = [];
+
+ for (var i = 0; i < this.numCells * 10; i++) {
+ var x = rng1() * this.numCells;
+ var y = rng2() * this.numCells;
+
+ sites.push({
+ x: x,
+ y: y
+ });
+
+ rng1 = SEEDRANDOM(rng1());
+ rng2 = SEEDRANDOM(rng2());
+ }
+
+ var diagram = voronoi.compute(sites, bbox);
+ var nodesMap = {};
+ var edgesMap = {};
+
+ diagram.edges.forEach(function(edge) {
+ var vA = edge.va;
+ var vB = edge.vb;
+
+ var nodesMapIndexA = vA.x + ' ' + vA.y;
+ var nodesMapIndexB = vB.x + ' ' + vB.y;
+
+ if (!nodesMap[nodesMapIndexA]) {
+ nodesMap[nodesMapIndexA] = new Node(vA.x, vA.y);
+ }
+
+ if (!nodesMap[nodesMapIndexB]) {
+ nodesMap[nodesMapIndexB] = new Node(vB.x, vB.y);
+ }
+
+ var nodeA = nodesMap[nodesMapIndexA];
+ var nodeB = nodesMap[nodesMapIndexB];
+
+ nodeA.neighbors.push(nodeB);
+ nodeB.neighbors.push(nodeA);
+
+ var edgeNew = new Edge(nodeA, nodeB);
+ var edgesMapIndex = nodesMapIndexA + ' ' + nodesMapIndexB;
+
+ edgesMap[edgesMapIndex] = edgeNew;
+ });
+
+ diagram.cells.forEach(function(cell) {
+ var cellNew = new Cell();
+ var halfedges = cell.halfedges;
+
+ halfedges.forEach(function(halfedge) {
+ var vA = halfedge.getStartpoint();
+ var vB = halfedge.getEndpoint();
+ var nodesMapIndexA = vA.x + ' ' + vA.y;
+ var nodesMapIndexB = vB.x + ' ' + vB.y;
+ var nodeA = nodesMap[nodesMapIndexA];
+ var nodeB = nodesMap[nodesMapIndexB];
+
+ var edge = halfedge.edge;
+ var edgesMapIndex = nodesMapIndexA + ' ' + nodesMapIndexB;
+ var edgeNew = edgesMap[edgesMapIndex];
+
+ var halfedgeNew = new HalfEdge();
+
+ halfedgeNew.cell = cellNew;
+ halfedgeNew.nodeA = nodeA;
+ halfedgeNew.nodeB = nodeB;
+ halfedgeNew.edge = edgeNew;
+
+ cellNew.halfedges.push(halfedgeNew);
+ cellNew.corners.push(nodeA);
+ nodeA.cells.push(cellNew);
+ });
+
+ cellNew.id = this.cells.length;
+
+ this.cells.push(cellNew);
+ }, this);
+
+ this.cells.forEach(function(cell) {
+ var neighbors = {};
+
+ cell.corners.forEach(function(node) {
+ node.cells.forEach(function(nodeCell) {
+ var id = nodeCell.id;
+
+ if (id !== cell.id) {
+ neighbors[id] = nodeCell;
+ }
+ });
+ });
+
+ cell.neighbors = _.values(neighbors);
+ });
+
+ this.nodes = _.values(nodesMap);
+ this.edges = _.values(edgesMap);
+ }
+}
\ No newline at end of file
diff --git a/src/graph-manager/halfedge.js b/src/graph-manager/halfedge.js
new file mode 100644
index 00000000..4fd71996
--- /dev/null
+++ b/src/graph-manager/halfedge.js
@@ -0,0 +1,5 @@
+export default class HalfEdge {
+ constructor() {
+
+ }
+}
\ No newline at end of file
diff --git a/src/graph-manager/node.js b/src/graph-manager/node.js
new file mode 100644
index 00000000..f3c4e865
--- /dev/null
+++ b/src/graph-manager/node.js
@@ -0,0 +1,26 @@
+const THREE = require('three');
+
+export default class Node {
+ constructor(x, y, id) {
+ this.id = id;
+ this.x = x;
+ this.y = y;
+ this.pos = new THREE.Vector3(x, y, 0);
+ this.neighbors = [];
+ this.cells = [];
+ }
+
+ getLowestNeighbor() {
+ var lowestNeighbor;
+ var lowestElevation = Infinity;
+
+ this.neighbors.forEach(function(neighbor) {
+ if (neighbor.elevation <= lowestElevation) {
+ lowestElevation = neighbor.elevation;
+ lowestNeighbor = neighbor;
+ }
+ });
+
+ return lowestNeighbor;
+ }
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100755
index 00000000..898b0926
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,151 @@
+const THREE = require('three');
+
+import Framework from './framework';
+import MapManager from './map';
+import GraphManager from './graph-manager/graph-manager';
+import GeographyManager from './geography-manager/geography-manager';
+import ViewManager from './view-manager/view-manager';
+
+function setup(options, scene) {
+
+ // Setup lighting
+ var ambientLight = new THREE.AmbientLight(0xffffff, options.lighting.ambientLight);
+ var directionalLight = new THREE.DirectionalLight(0xffffff, options.lighting.directionalLight);
+ var directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+
+ directionalLight.position.set(0, 0, 100);
+ directionalLight.target.position.set(50, 50, 100);
+
+ scene.add(ambientLight);
+ scene.add(directionalLight);
+ // scene.add(directionalLightHelper);
+
+ // Setup map
+ var map = new Map();
+
+ var graphManager = new GraphManager(options.graphManager, map);
+ var geographyManager = new GeographyManager(options.geographyManager, map);
+ var viewManager = new ViewManager(options.viewManager, map, scene);
+
+ map.graphManager = graphManager;
+ map.geographyManager = geographyManager;
+ map.viewManager = viewManager;
+
+ graphManager.generateGrid()
+ geographyManager.generateGeography();
+ viewManager.renderMap();
+}
+
+function teardown(scene) {
+ for (var i = scene.children.length - 1; i >= 0; i--) {
+ scene.remove(scene.children[i]);
+ }
+}
+
+function onLoad(framework) {
+ var { scene, camera, renderer, gui, stats } = framework;
+
+ var options = {
+ other: {
+ random: function() {
+
+ options.graphManager.seedVoronoi = Math.floor(Math.random() * 100);
+ options.geographyManager.seedElevation = Math.floor(Math.random() * 100);
+ options.geographyManager.seedMoisture = Math.floor(Math.random() * 100);
+ options.viewManager.seedPolygonVariation = Math.floor(Math.random() * 100);
+
+ teardown(scene);
+ setup(options, scene);
+ }
+ },
+ lighting: {
+ ambientLight: 0.5,
+ directionalLight: 0.5
+ },
+ graphManager: {
+ cellType: 'hex', // 'square', 'hex', 'voronoi'
+ numCells: 80,
+ seedVoronoi: 1.0,
+ },
+ geographyManager: {
+ seedElevation: 1.0,
+ seedMoisture: 1.0,
+ seedErosion: 1.0,
+ elevationNoisiness: 1.2,
+ moistureNoisiness: 2.0,
+ erosionSteps: 10,
+ rainDistribution: 'uniform', // 'moisture-map', 'uniform',
+ rainFrequency: 1.0
+ },
+ viewManager: {
+ renderGraph: false,
+ renderColors: 'biomes', // 'elevation', 'moisture', 'biomes'
+ render3D: 'polygon', // 'polygon', 'shader', 'none'
+ renderCoastline: false,
+ renderPolygonVariation: true,
+ seedPolygonVariation: 1.0,
+ renderDepth: true,
+ renderOceanDepth: false,
+ debugShowNodes: false,
+ debugShowCoastalNodes: false,
+ debugShowDropletNodes: false
+ }
+ };
+
+ setup(options, scene);
+
+ camera.position.set(0, 0, 100);
+ camera.lookAt(new THREE.Vector3(0,0,0));
+
+ var guiLighting = gui.addFolder('Lighting');
+ var guiGraphManager = gui.addFolder('Graph Manager');
+ var guiGeographyManager = gui.addFolder('Geography Manager');
+ var guiViewManager = gui.addFolder('View Manager');
+
+ gui.add(options.other, 'random').name('Generate new map');
+
+ guiLighting.add(options.lighting, 'ambientLight', 0, 1).name('Ambient light strength');
+ guiLighting.add(options.lighting, 'directionalLight', 0, 1).name('Directional light strength');
+
+ guiGraphManager.add(options.graphManager, 'cellType', ['square', 'hex', 'voronoi']).name('Cell type');
+ guiGraphManager.add(options.graphManager, 'numCells').name('Num cells');
+ guiGraphManager.add(options.graphManager, 'seedVoronoi', 0, 100).name('Voronoi seed').listen();
+
+ guiGeographyManager.add(options.geographyManager, 'seedElevation', 0, 100).name('Elevation seed').listen();
+ guiGeographyManager.add(options.geographyManager, 'seedMoisture', 0, 100).name('Moisture seed').listen();
+ guiGeographyManager.add(options.geographyManager, 'seedErosion', 0, 100).name('Erosion seed').listen();
+ guiGeographyManager.add(options.geographyManager, 'elevationNoisiness', 0, 10).name('Elevation noisiness');
+ guiGeographyManager.add(options.geographyManager, 'moistureNoisiness', 0, 10).name('Moisture noisiness');
+ guiGeographyManager.add(options.geographyManager, 'erosionSteps', 0, 100).name('Erosion steps');
+ guiGeographyManager.add(options.geographyManager, 'rainDistribution', ['moisture map', 'uniform']).name('Rain distribution');
+ guiGeographyManager.add(options.geographyManager, 'rainFrequency', 0, 5).name('Rain frequency');
+
+ guiViewManager.add(options.viewManager, 'renderGraph').name('Render graph');
+ guiViewManager.add(options.viewManager, 'renderColors', ['elevation', 'moisture', 'biomes']).name('Render map colors');
+ guiViewManager.add(options.viewManager, 'render3D', ['none', 'polygon', 'shader']).name('Render 3D');
+ guiViewManager.add(options.viewManager, 'renderCoastline').name('Render coastline');
+ guiViewManager.add(options.viewManager, 'renderPolygonVariation').name('Render polygon variation');
+ guiViewManager.add(options.viewManager, 'seedPolygonVariation', 0, 100).name('Polygon variation seed').listen();
+ guiViewManager.add(options.viewManager, 'renderDepth').name('Render depth');
+ guiViewManager.add(options.viewManager, 'renderOceanDepth').name('Render ocean depth');
+ guiViewManager.add(options.viewManager, 'debugShowNodes').name('Debug nodes');
+ guiViewManager.add(options.viewManager, 'debugShowCoastalNodes').name('Debug coastal nodes');
+ guiViewManager.add(options.viewManager, 'debugShowDropletNodes').name('Debug droplet nodes');
+
+ for (var i in gui.__folders) {
+ var folder = gui.__folders[i];
+
+ for (var j in folder.__controllers) {
+ folder.__controllers[j].onFinishChange(function() {
+ teardown(scene);
+ setup(options, scene);
+ });
+ }
+ }
+}
+
+function onUpdate(framework) {
+ var { scene, camera, renderer, gui, stats } = framework;
+}
+
+Framework.init(onLoad, onUpdate);
\ No newline at end of file
diff --git a/src/map.js b/src/map.js
new file mode 100644
index 00000000..e21df2a8
--- /dev/null
+++ b/src/map.js
@@ -0,0 +1,5 @@
+export default class Map {
+ constructor() {
+
+ }
+}
\ No newline at end of file
diff --git a/src/shaders/map-frag.glsl b/src/shaders/map-frag.glsl
new file mode 100755
index 00000000..5bad237b
--- /dev/null
+++ b/src/shaders/map-frag.glsl
@@ -0,0 +1,117 @@
+varying vec2 v_uv;
+varying vec3 v_normal;
+varying vec3 v_position;
+varying float v_elevation;
+varying float v_moisture;
+
+uniform int u_color;
+
+// Grabbed from: https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
+vec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
+vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
+float cnoise(vec2 P) {
+ vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
+ vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
+ Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
+ vec4 ix = Pi.xzxz;
+ vec4 iy = Pi.yyww;
+ vec4 fx = Pf.xzxz;
+ vec4 fy = Pf.yyww;
+ vec4 i = permute(permute(ix) + iy);
+ vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
+ vec4 gy = abs(gx) - 0.5;
+ vec4 tx = floor(gx + 0.5);
+ gx = gx - tx;
+ vec2 g00 = vec2(gx.x,gy.x);
+ vec2 g10 = vec2(gx.y,gy.y);
+ vec2 g01 = vec2(gx.z,gy.z);
+ vec2 g11 = vec2(gx.w,gy.w);
+ vec4 norm = 1.79284291400159 - 0.85373472095314 *
+ vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
+ g00 *= norm.x;
+ g01 *= norm.y;
+ g10 *= norm.z;
+ g11 *= norm.w;
+ float n00 = dot(g00, vec2(fx.x, fy.x));
+ float n10 = dot(g10, vec2(fx.y, fy.y));
+ float n01 = dot(g01, vec2(fx.z, fy.z));
+ float n11 = dot(g11, vec2(fx.w, fy.w));
+ vec2 fade_xy = fade(Pf.xy);
+ vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
+ float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
+ return 2.3 * n_xy;
+}
+
+vec3 getElevationColor(float elevation) {
+ vec3 lo = vec3(0.000, 0.541, 0.898);
+ vec3 hi = vec3(1.000, 1.000, 0.000);
+
+ return (hi - lo) * ((elevation + 1.0) / 2.0) / 1.0 + lo;
+}
+
+vec3 getMoistureColor(float moisture) {
+ vec3 lo = vec3(0.984, 0.635, 0.443);
+ vec3 hi = vec3(0.313, 0.439, 1.000);
+
+ return (hi - lo) * ((moisture + 1.0) / 2.0) / 1.0 + lo;
+}
+
+vec3 getBiomeColor(float elevation, float moisture) {
+ if (elevation <= 0.00) return vec3(0.211, 0.207, 0.384);
+
+ if (elevation <= 0.25) {
+ if (moisture <= -0.66) return vec3(0.325, 0.549, 0.435);
+ if (moisture <= -0.33) return vec3(0.325, 0.549, 0.435);
+ if (moisture <= 0.00) return vec3(0.431, 0.654, 0.372);
+ if (moisture <= 0.33) return vec3(0.431, 0.654, 0.372);
+ if (moisture <= 0.66) return vec3(0.596, 0.709, 0.427);
+ if (moisture <= 1.00) return vec3(0.913, 0.866, 0.776);
+ }
+
+ if (elevation <= 0.50) {
+ if (moisture <= -0.66) return vec3(0.376, 0.603, 0.435);
+ if (moisture <= -0.33) return vec3(0.701, 0.792, 0.658);
+ if (moisture <= 0.00) return vec3(0.701, 0.792, 0.658);
+ if (moisture <= 0.33) return vec3(0.596, 0.709, 0.427);
+ if (moisture <= 0.66) return vec3(0.596, 0.709, 0.427);
+ if (moisture <= 1.00) return vec3(0.803, 0.835, 0.650);
+ }
+
+ if (elevation <= 0.75) {
+ if (moisture <= -0.66) return vec3(0.800, 0.831, 0.729);
+ if (moisture <= -0.33) return vec3(0.800, 0.831, 0.729);
+ if (moisture <= 0.00) return vec3(0.768, 0.800, 0.729);
+ if (moisture <= 0.33) return vec3(0.768, 0.800, 0.729);
+ if (moisture <= 0.66) return vec3(0.803, 0.835, 0.650);
+ if (moisture <= 1.00) return vec3(0.803, 0.835, 0.650);
+ }
+
+ if (elevation <= 1.00) {
+ if (moisture <= -0.66) return vec3(0.972, 0.972, 0.972);
+ if (moisture <= -0.33) return vec3(0.972, 0.972, 0.972);
+ if (moisture <= 0.00) return vec3(0.972, 0.972, 0.972);
+ if (moisture <= 0.33) return vec3(0.866, 0.870, 0.725);
+ if (moisture <= 0.66) return vec3(0.733, 0.733, 0.733);
+ if (moisture <= 1.00) return vec3(0.835, 0.756, 0.600);
+ }
+}
+
+vec3 getSmoothedBiomeColor(float elevation, float moisture) {
+ float noise = (cnoise(v_position.xy + vec2( 0.0, 0.0)) + 1.0) / 2.0;
+ float noiseT = (cnoise(v_position.xy + vec2( 1.0, 0.0)) + 1.0) / 2.0;
+ float noiseB = (cnoise(v_position.xy + vec2(-1.0, 0.0)) + 1.0) / 2.0;
+ float noiseL = (cnoise(v_position.xy + vec2( 0.0, -1.0)) + 1.0) / 2.0;
+ float noiseR = (cnoise(v_position.xy + vec2( 0.0, 1.0)) + 1.0) / 2.0;
+
+ return getBiomeColor(elevation, moisture);
+
+}
+
+void main() {
+ vec3 c = (u_color == 0) ? getElevationColor(v_elevation) :
+ (u_color == 1) ? getMoistureColor(v_moisture) :
+ (u_color == 2) ? getSmoothedBiomeColor(v_elevation, v_moisture) :
+ vec3(0, 0, 0);
+
+ gl_FragColor = vec4(c, 1.0);
+}
\ No newline at end of file
diff --git a/src/shaders/map-vert.glsl b/src/shaders/map-vert.glsl
new file mode 100755
index 00000000..a6166154
--- /dev/null
+++ b/src/shaders/map-vert.glsl
@@ -0,0 +1,20 @@
+varying vec2 v_uv;
+varying vec3 v_normal;
+varying vec3 v_position;
+varying float v_elevation;
+varying float v_moisture;
+
+uniform int u_color;
+
+attribute float elevation;
+attribute float moisture;
+
+void main() {
+ v_uv = uv;
+ v_normal = normal;
+ v_position = position;
+ v_elevation = elevation;
+ v_moisture = moisture;
+
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+}
\ No newline at end of file
diff --git a/src/view-manager/view-manager.js b/src/view-manager/view-manager.js
new file mode 100644
index 00000000..5f4797e0
--- /dev/null
+++ b/src/view-manager/view-manager.js
@@ -0,0 +1,391 @@
+const THREE = require('three');
+const _ = require('lodash');
+const CHROMA = require('chroma-js');
+const SEEDRANDOM = require('seedrandom');
+
+export default class ViewManager {
+ constructor(options, map, scene) {
+ this.renderGraph = options.renderGraph;
+ this.renderColors = options.renderColors;
+ this.render3D = options.render3D;
+ this.renderCoastline = options.renderCoastline;
+ this.renderPolygonVariation = options.renderPolygonVariation;
+ this.renderDepth = options.renderDepth;
+ this.renderOceanDepth = options.renderOceanDepth;
+ this.seedPolygonVariation = options.seedPolygonVariation;
+ this.debugOcean = options.debugOcean;
+ this.debugShowNodes = options.debugShowNodes;
+ this.debugShowCoastalNodes = options.debugShowCoastalNodes;
+ this.debugShowDropletNodes = options.debugShowDropletNodes;
+ this.map = map;
+ this.scene = scene;
+
+ // Determines if the shader will use elevation, moisture, or biomes
+ var uColor = (this.renderColors === 'elevation') ? 0 :
+ (this.renderColors === 'moisture') ? 1 :
+ (this.renderColors === 'biomes') ? 2 :
+ 3;
+
+ this.shaderMaterial = new THREE.ShaderMaterial({
+ uniforms: {
+ u_color: { value: uColor }
+ },
+ vertexShader: require('../shaders/map-vert.glsl'),
+ fragmentShader: require('../shaders/map-frag.glsl'),
+ side: THREE.DoubleSide,
+ vertexColors: THREE.VertexColors
+ });
+ }
+
+ renderMap() {
+ if (this.render3D === 'polygon') {
+ this._render3DPolygon();
+ return;
+ }
+
+ if (this.render3D === 'shader') {
+ this._render3DShader();
+ return;
+ }
+
+ if (this.renderGraph) {
+ this._renderGraph();
+ }
+
+ this._renderCells();
+
+ if (this.renderCoastline) {
+ this._renderCoastline();
+ }
+
+ if (this.debugShowNodes) {
+ this._renderNodes();
+ }
+
+ if (this.debugShowCoastalNodes) {
+ this._renderCoastalNodes();
+ }
+
+ if (this.debugShowDropletNodes) {
+ this._renderDropletNodes();
+ }
+ }
+
+ _renderGraph() {
+ var edges = this.map.graphManager.edges;
+ var nodePairs = [];
+ var nodePairsColor = CHROMA('white');
+
+ edges.forEach(function(edge) {
+ nodePairs.push(edge.nodeA);
+ nodePairs.push(edge.nodeB);
+ }, this);
+
+ this._renderLineSegments(nodePairs, nodePairsColor);
+
+ if (this.debugShowNodes) {
+ var nodes = this.map.graphManager.nodes;
+ var nodesColor = CHROMA('white');
+
+ this._renderPoints(nodes, nodesColor);
+ }
+ }
+
+ _renderCells() {
+ var cells = this.map.graphManager.cells;
+
+ cells.forEach(function(cell) {
+ var color = (this.renderColors === 'elevation') ? cell.elevationColor :
+ (this.renderColors === 'moisture') ? cell.moistureColor :
+ (this.renderColors === 'biomes') ? cell.biomeColor :
+ CHROMA('black');
+
+ this._renderCell(cell, color);
+ }, this);
+ }
+
+ _render3DPolygon() {
+ var geometry = new THREE.Geometry();
+ var cells = this.map.graphManager.cells;
+ var positionsVisited = {};
+
+ var rng1 = SEEDRANDOM(this.seedPolygonVariation);
+ var rng2 = SEEDRANDOM(this.seedPolygonVariation + 1.0);
+
+ cells.forEach(function(cell) {
+ var corners = cell.corners;
+ var faceIndices = [];
+
+ corners.forEach(function(node, i) {
+ var pos = node.pos.clone().setComponent(2, node.elevation * 7);
+
+ if (!this.renderOceanDepth && node.elevation <= 0) {
+ pos.setComponent(2, 0);
+ }
+
+ if (!this.renderDepth) {
+ pos.setComponent(2, 0);
+ }
+
+ var posMapIndex = pos.x + ' ' + pos.y;
+ var id = positionsVisited[posMapIndex];
+
+ if (_.isUndefined(id)) {
+ id = geometry.vertices.length;
+ positionsVisited[posMapIndex] = id;
+ geometry.vertices.push(pos);
+ }
+
+ faceIndices.push(id);
+ }, this);
+
+ var color = (this.renderColors === 'elevation') ? cell.elevationColor :
+ (this.renderColors === 'moisture') ? cell.moistureColor :
+ (this.renderColors === 'biomes') ? cell.biomeColor :
+ CHROMA('black');
+ var normal = new THREE.Vector3(1, 1, 1);
+ var materialIndex = 0;
+
+ for (var i = 2; i < faceIndices.length; i++) {
+ var ia = faceIndices[0];
+ var ib = faceIndices[i - 1];
+ var ic = faceIndices[i];
+
+ var tempColor = (rng1() > 0.5) ? color.darken(rng2() / 10) : color.brighten(rng2() / 10);
+ var finalColor = this.renderPolygonVariation ? tempColor : color;
+
+ var face = new THREE.Face3(ia, ib, ic, normal, new THREE.Color(finalColor.hex()), materialIndex);
+
+ geometry.faces.push(face);
+
+ rng1 = SEEDRANDOM(rng1());
+ rng2 = SEEDRANDOM(rng2());
+ }
+ }, this);
+
+ geometry.computeFaceNormals();
+ geometry.computeVertexNormals();
+
+ var material = new THREE.MeshLambertMaterial({
+ color: 0xffffff,
+ side: THREE.DoubleSide,
+ vertexColors: THREE.FaceColors
+ });
+ var mesh = new THREE.Mesh(geometry, material);
+
+ this.scene.add(mesh);
+ }
+
+ _render3DShader() {
+ var nodes = this.map.graphManager.nodes;
+ var cells = this.map.graphManager.cells;
+ var numNodes = nodes.length;
+
+ var geometry = new THREE.BufferGeometry();
+ var indices = new Uint32Array((numNodes - 2) * 3 * 3);
+ var positions = new Float32Array(numNodes * 3 * 3);
+ var elevations = new Float32Array(numNodes * 3);
+ var moistures = new Float32Array(numNodes * 3);
+
+ var indicesOfPositions = {};
+ var i = 0;
+ var j = 0;
+
+ cells.forEach(function(cell) {
+ var corners = cell.corners;
+
+ corners.forEach(function(node) {
+ var elevation = node.elevation;
+ var moisture = node.moisture;
+ var pos = node.pos.clone();
+ var posMapIndex = pos.x + ' ' + pos.y;
+ var positionNotSeen = _.isUndefined(indicesOfPositions[posMapIndex]);
+
+ if (positionNotSeen) {
+ indicesOfPositions[posMapIndex] = i;
+
+ positions[(i * 3) ] = pos.x;
+ positions[(i * 3) + 1] = pos.y;
+ positions[(i * 3) + 2] = pos.z + (elevation * 7.0);
+
+ if (!this.renderOceanDepth && elevation <= 0) {
+ positions[(i * 3) + 2] = 0;
+ }
+
+ if (!this.renderDepth) {
+ positions[(i * 3) + 2] = 0;
+ }
+
+ elevations[i] = elevation;
+ moistures[i] = moisture;
+
+ i++;
+ }
+ }, this);
+ }, this);
+
+
+ cells.forEach(function(cell) {
+ var corners = cell.corners;
+
+ for (var k = 1; k < corners.length - 1; k++) {
+ var nA = corners[0];
+ var nB = corners[k];
+ var nC = corners[k + 1];
+
+ var posA = nA.pos.clone();
+ var posB = nB.pos.clone();
+ var posC = nC.pos.clone();
+
+ var posMapIndexA = posA.x + ' ' + posA.y;
+ var posMapIndexB = posB.x + ' ' + posB.y;
+ var posMapIndexC = posC.x + ' ' + posC.y;
+
+ var iA = indicesOfPositions[posMapIndexA];
+ var iB = indicesOfPositions[posMapIndexB];
+ var iC = indicesOfPositions[posMapIndexC];
+
+ indices[j ] = iA;
+ indices[j + 1] = iB;
+ indices[j + 2] = iC;
+
+ j += 3;
+ }
+ }, this);
+
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
+ geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
+ geometry.addAttribute('elevation', new THREE.BufferAttribute(elevations, 1));
+ geometry.addAttribute('moisture', new THREE.BufferAttribute(moistures, 1));
+ geometry.computeVertexNormals();
+
+ var material = this.shaderMaterial;
+ var mesh = new THREE.Mesh(geometry, material);
+
+ this.scene.add(mesh);
+ }
+
+ _renderCoastline() {
+ var nodes = this.map.graphManager.nodes;
+ var material = new THREE.LineBasicMaterial({
+ color: 0x000000,
+ linewidth: 2
+ });
+ var group = new THREE.Group();
+
+ nodes.forEach(function(node) {
+ if (node.isCoastal) {
+
+ var neighbors = node.neighbors;
+
+ neighbors.forEach(function(neighbor) {
+ if (neighbor.isCoastal) {
+ var geometry = new THREE.Geometry();
+
+ geometry.vertices.push(node.pos);
+ geometry.vertices.push(neighbor.pos);
+
+ var line = new THREE.Line(geometry, material);
+
+ group.add(line);
+ }
+ });
+ }
+ });
+
+ group.translateZ(0.2);
+
+ this.scene.add(group);
+ }
+
+ _renderPoints(nodes, color) {
+ var material = new THREE.PointsMaterial({ color: color });
+ var geometry = new THREE.Geometry();
+
+ _.forEach(nodes, function(node) {
+ geometry.vertices.push(node.pos);
+ })
+
+ var points = new THREE.Points(geometry, material);
+
+ points.translateZ(0.2);
+
+ this.scene.add(points);
+ }
+
+ _renderLine(nodes, color) {
+ var material = new THREE.LineBasicMaterial({ color: color });
+ var geometry = new THREE.Geometry();
+
+ nodes.forEach(function(node) {
+ geometry.vertices.push(node.pos);
+ });
+
+ var line = new THREE.Line(geometry, material);
+
+ line.translateZ(0.1);
+
+ this.scene.add(line);
+ }
+
+ _renderLineSegments(nodes, color) {
+ var material = new THREE.LineBasicMaterial({ color: color });
+ var geometry = new THREE.Geometry();
+
+ nodes.forEach(function(node) {
+ geometry.vertices.push(node.pos);
+ });
+
+ var line = new THREE.LineSegments(geometry, material);
+
+ line.translateZ(0.1);
+
+ this.scene.add(line);
+ }
+
+ _renderCell(cell, color) {
+ var halfedges = cell.halfedges;
+ var positions = [];
+
+ positions.push(halfedges[0].nodeA.pos);
+
+ _.forEach(cell.halfedges, function(halfedge) {
+ positions.push(halfedge.nodeB.pos);
+ });
+
+ var shape = new THREE.Shape(positions);
+ var geometry = new THREE.ShapeGeometry(shape);
+ var material = new THREE.MeshBasicMaterial({ color: color.hex() });
+ var mesh = new THREE.Mesh(geometry, material);
+
+ this.scene.add(mesh);
+ }
+
+ _renderCoastalNodes() {
+ var nodes = this.map.graphManager.nodes;
+ var coastalNodes = [];
+
+ nodes.forEach(function(node) {
+ if (node.isCoastal) coastalNodes.push(node);
+ });
+
+ this._renderPoints(coastalNodes, CHROMA('black').hex());
+ }
+
+ _renderDropletNodes() {
+ var nodes = this.map.graphManager.nodes;
+ var nodesWithDroplets = [];
+
+ nodes.forEach(function(node) {
+ if (node.spawnedDroplet) nodesWithDroplets.push(node);
+ });
+
+ this._renderPoints(nodesWithDroplets, CHROMA('blue').hex());
+ }
+
+ _renderNodes() {
+ var nodes = this.map.graphManager.nodes;
+
+ this._renderPoints(nodes, CHROMA('black').hex());
+ }
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100755
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
diff --git a/writeups/final_doc.md b/writeups/final_doc.md
new file mode 100644
index 00000000..39910173
--- /dev/null
+++ b/writeups/final_doc.md
@@ -0,0 +1,141 @@
+# Zany Maps: Procedurally Generated Maps
+
+Zack Elliott
+
+## Updated Design Doc:
+
+### Introduction:
+
+I read a lot of fantasy books when I was younger (Redwall, The 13½ Lives of Captain Bluebear, The Bartimaeus Trilogy, etc…) and always enjoyed flipping to the cover to look at the maps the author and illustrator had created. In an attempt to reignite my childhood passion for awesome fantasy maps, I thought it would be neat to build a web service that procedurally generates these maps.
+
+### Goal:
+
+Zany Maps is a service that procedurally generates realistic and interesting maps. Every aspect of these maps – including the geography, topography, and coloring – will be generated using procedural concepts I have learned in CIS 700.
+
+### Inspiration / Reference:
+
+Polygon Map Generation:
+ - Main demo: http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/demo.html
+ - Accompanying article: http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/
+
+
+
+Generating Fantasy Maps:
+ - Main demos and article: https://mewo2.com/notes/terrain/
+
+
+
+### Specification:
+
+Core features:
+ - Users can efficiently create and customize procedurally generated maps
+ - Users can select methods of generating the underlying graph
+ - Points grid structure (e.g. Voronoi diagram, square grid, hexagon grid, etc…)
+ - Other minor parameters (e.g. number of points, random noise seed, etc...)
+ - Users can add additional geographical features to the map, each of which can be customized by a number of parameters
+ - Elevation map
+ - Moisture map
+ - Biomes (e.g. elevation and moisture combinations map to different biomes)
+ - Rain distribution
+ - Erosion naturally transforms the map
+ - Users can select to view the map from a number of different perspectives
+ - Simplified 2D view allows the user to view the underlying elevation/moisture maps, graph structure, and certain types of nodes
+ - Tessellation process transforms the 2D map along with its elevation to a 3D representation
+ - Shaders are used to efficiently render the map and smooth between different biomes
+ - Users can customize many, many parameters in the map-generation process via a DAT.GUI display
+
+### Techniques:
+
+Below are a list of techniques and concepts I have learned in CIS 700. Below each concept are specific ways that use it in my project.
+ - Noise functions (e.g. simplex)
+ - Generate and fine tune elevation and moisture maps
+ - Voronoi diagrams
+ - Generate underlying grid structure
+ - Traverse with traditional graph algorithms
+ - Graph data structures and algorithms
+ - Store the underlying cell grid in an efficient manner (e.g. using a modified half-edge structure)
+ - Use traversal algorithms such as BFS/DFS to walk the graph structure
+ - Gradient maps
+ - Apply elevation and moisture maps to the grid that map to different biome patterns
+ - Enable different gradient maps depending on the map view (e.g. elevation, moisture, etc…)
+ - Shaders
+ - Color the map efficiently by assigning elevation and moisture on a per-node basis, and allowing shaders to blend biomes on a per-pixel basis
+ - 3D rendering
+ - Implement a polygonized 3D view of the generated map
+
+### Design:
+
+
+
+### Timeline:
+
+ - Week of 4/3
+ - Setup basic project framework (using HW base code) and user interface (DAT.GUI)
+ - Implement the Grid Manager (responsible for generating the base grid)
+ - Week of 4/10
+ - Implement the Continent Manager (responsible for defining the base coastlines and shape of the continent)
+ - Implement basic rendering
+ - Week of 4/17
+ - Implement components of the Geography Manager (elevation, biomes, rivers, lakes, etc…) and the View Manager in conjunction. This will be necessary as in order to debug aspects of the Geography Manager, I will need to be able to properly render them.
+ - Week of 4/24
+ - Continue with the week of 4/17 task
+ - Week of 5/1
+ - Fine tune and polish project
+ - If time, implement Erosion Manager (erodes basic continent & coastline)
+
+## Results:
+
+Demo link: http://zelliottm.com/Final-Project/
+
+
+
+3D rendering of a map with an underlying hex-cell grid, distributed biomes, and light erosion.
+
+
+
+The same map as above, this time in 2D, viewed from above. Additionally, instead of assigining biomes on a per-cell basis, I assign elevation and moisture to each node in the grid, and then a fragment shader assigns biomes on a per-pixel basis.
+
+
+
+This view of the map is useful for debugging. You can see the underlying grid structure (a square-cell grid is used) and the coastal nodes (points are in black).
+
+
+
+This is the same view as above, but now an underlying Voronoi grid structure is used.
+
+
+
+This view of the map shows how a 2D Simplex noise distribution is used to apply an elevation map to the grid.
+
+
+
+There are a ton of ways to modify parameters in the map-generation process, categorized by their specific parent manager classes.
+
+## Evaluation:
+
+Overall, I was most pleased with how my program structure remained clean and organized as I added additional features and reworked existing code. All of my code is divided into three distinct classes (GraphManager, GeographyManager, and ViewManager), which allows me to (1) easily debug hairy problems and (2) enable many, many aspects of the map generation to be customized by users. Additionally, I was happy I was able to store square-cell grids, hex-cell grids, and voronoi diagrams in a universal graph data structure... essentially a modified half-edge structure. Consequently, the other managers are agnostic as to the exact type of grid they are communicating with, which is very neat as it allows the underlying grid to be changed in real-time. Finally, I also implemented *almost* everything specified in my writeup (except lakes and rivers creation, as I couldn't figure out a nice natural way to generate them).
+
+Instead of hard-coding in any finely-tuned parameters, I allow essentially all parameters to be customized by the user. This is really useful for debugging, and gives the user more control over the map-generation process.
+
+Given I completed this product in stages, many of the WIP images can be reconstructed by simply viewing maps in different ways. For example, by rendering a map in 2D mode, colored by the elevation map, with the grid structure enabled, this represents how far I had gotten in milestone #1. In other words, I first implemented the basic structure of of each manager, to ensure that my overall project structure was sound. Then, I began implementing components of each manager (e.g. starting with the square/hex-cell grids in the GraphManager, the elevation/moisture maps in the GeographyManager, and basic rendering methods in the ViewManager). Thus, upon completion of the final project, I can reconstruct WIP images by essentially turning off features in each manager.
+
+## Future Work:
+
+ - More accurate erosion algorithm: The current erosion algorithm I am using is relatively basic. First, scatter rain droplets on nodes according to some distribution. Second, each droplet traverses the map's graph structure by finding the lowest adjacent neighbor and recursing. Third, on each recursion step, each droplet keeps track of its current speed and sediment capacity, and erodes or deposits sediment (i.e. increases/decreases elevation of nodes) based upon these properties and the slope of its current node. A more advanced erosion scheme would be more physically accurate, as well as take into consideration the moisture and biome at each node.
+ - Smooth between biome transitions: When biomes are assigned on a per-node basis and passed into the shader, the biome transitions are not smooth (i.e. are not dithered properly). I experimented a bit in my shader to try to fix this, but I believe I have to generate the elevation and moisture maps in the shader in order to do this.
+ - Rivers and lakes: One of the reasons I did not implement river/lake generation was because I couldn't come up with a realistic (yet easy) generation process that didn't use an already working rain/erosion scheme. That is, in a spirit to be physically-accurate, I would want to define a rainfall distribution on the map (akin to that used for erosion) that creates rivers and lakes over time.
+
+## Acknowledgements:
+
+Relevant NPM modules used:
+
+ - chroma-js: manages colors
+ - noisejs: generates simplex noise
+ - seedrandom: generates seeded random numbers
+ - voronoi: generates voronoi diagrams
+
+Online resources used:
+
+ - http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/
+ - http://ranmantaru.com/blog/2011/10/08/water-erosion-on-heightmap-terrain/
+ - http://mewo2.com/notes/terrain/
\ No newline at end of file
diff --git a/writeups/final_doc.pdf b/writeups/final_doc.pdf
new file mode 100644
index 00000000..1eecd80e
Binary files /dev/null and b/writeups/final_doc.pdf differ
diff --git a/writeups/images/image_1.PNG b/writeups/images/image_1.PNG
new file mode 100644
index 00000000..6ece0b53
Binary files /dev/null and b/writeups/images/image_1.PNG differ
diff --git a/writeups/images/image_2.PNG b/writeups/images/image_2.PNG
new file mode 100644
index 00000000..a0405800
Binary files /dev/null and b/writeups/images/image_2.PNG differ
diff --git a/writeups/images/image_3.PNG b/writeups/images/image_3.PNG
new file mode 100644
index 00000000..e5f1bf21
Binary files /dev/null and b/writeups/images/image_3.PNG differ
diff --git a/writeups/images/image_4.PNG b/writeups/images/image_4.PNG
new file mode 100644
index 00000000..6f5ee304
Binary files /dev/null and b/writeups/images/image_4.PNG differ
diff --git a/writeups/images/image_5.PNG b/writeups/images/image_5.PNG
new file mode 100644
index 00000000..c0141391
Binary files /dev/null and b/writeups/images/image_5.PNG differ
diff --git a/writeups/images/image_6.PNG b/writeups/images/image_6.PNG
new file mode 100644
index 00000000..4dd7bdc9
Binary files /dev/null and b/writeups/images/image_6.PNG differ
diff --git a/writeups/images/image_7.PNG b/writeups/images/image_7.PNG
new file mode 100644
index 00000000..20beec6a
Binary files /dev/null and b/writeups/images/image_7.PNG differ
diff --git a/writeups/images/image_8.PNG b/writeups/images/image_8.PNG
new file mode 100644
index 00000000..29b26e4d
Binary files /dev/null and b/writeups/images/image_8.PNG differ
diff --git a/writeups/images/image_9.PNG b/writeups/images/image_9.PNG
new file mode 100644
index 00000000..60eb5d04
Binary files /dev/null and b/writeups/images/image_9.PNG differ
diff --git a/writeups/images/milestone1_1.PNG b/writeups/images/milestone1_1.PNG
new file mode 100644
index 00000000..24192efa
Binary files /dev/null and b/writeups/images/milestone1_1.PNG differ
diff --git a/writeups/images/milestone1_2.png b/writeups/images/milestone1_2.png
new file mode 100644
index 00000000..b65d1e3e
Binary files /dev/null and b/writeups/images/milestone1_2.png differ
diff --git a/writeups/milestone1_doc.md b/writeups/milestone1_doc.md
new file mode 100644
index 00000000..53016581
--- /dev/null
+++ b/writeups/milestone1_doc.md
@@ -0,0 +1,30 @@
+# Milestone 1
+
+### Work completed
+
+I implemented a number of components in the Zany Maps pipeline for this milestone. First, I designed the general pipeline flow. I begin by instantiating a Map object, which acts as the overall encapsulating object for my project. Then, I instantiate various pipeline components: GraphManager, GeographyManager, and ViewManager. Each of these manager components are instantiated with (1) a reference to the parent Map object (so that they have access to one another's data) and (2) a number of options parameters users can tweak via DAT.gui. Finally, I call various methods on each of these managers in sequence in order to construct and render the map one step at a time.
+
+The GraphManager holds all grid and graph instantiation and access. I implemented the methods `generateFromSquareGrid()` and `generateFromHexGrid()`, which allows you to generate different types of grids. In addition, GraphManager stores the grids as graphs via lists of nodes, edges, and cells, allowing for easy traversal of the grid. There is a list of Cells, each of which has a center, a list of half edges, and corners. Corners are equivalent to Nodes, each of which has an id, position, and a list of neighbors. Half edges are currently unused, although they will be useful in traversing the map during more advanced rendering (e.g. triangulation) and coastline manipulation (e.g. erosion). Finally, there is also a list of Edges, each of which has start and end nodes. This storing method is fairly quick to set up, and allows for easy access and traversal of the graph.
+
+The GeographyManager currently manages only generating elevation maps, via the aptly named method `generateElevationMap()`. This method essentially applies Perlin noise on the scale from -1 to 1 to each corner of each cell (i.e. each node in the graph). This noise is set as that node's elevation. Furthermore, each cell's elevation is the average of the elevations of its corners.
+
+The ViewManager handles all map rendering. The rendering is split up into disjoint methods in order to allow for various portions of the map to rendered or not rendered separately.
+ - `renderGraph()` simply renders the the grid itself.
+ - `renderElevation()` renders a gradient map for each cell that maps to each cell's elevation.
+ - `renderCoastline()` renders a thicker black line that denotes the coastline of the map based upon elevation. Any cell/node under sea level (i.e. an elevation of 0), is treated as ocean (and vice versa for land)
+
+Below are various screenshots the project's current state:
+
+
+
+Grid, elevation, and coastline rendered for a hexagon-cell grid
+
+
+
+Grid, elevation, and coastline rendered for a square-cell grid
+
+### To demo:
+
+ 1. Simply run `npm install` and `npm run start`
+ 2. Open up `http://localhost:7000`
+ 3. Go to `src/main.js` and experiment with the map paramters in the `options` object declared on line 12.
\ No newline at end of file