diff --git a/.vscode/.browse.VC.db b/.vscode/.browse.VC.db new file mode 100644 index 00000000..20d9dcb4 Binary files /dev/null and b/.vscode/.browse.VC.db differ diff --git a/.vscode/.browse.VC.db-shm b/.vscode/.browse.VC.db-shm new file mode 100644 index 00000000..02663f85 Binary files /dev/null and b/.vscode/.browse.VC.db-shm differ diff --git a/.vscode/.browse.VC.db-wal b/.vscode/.browse.VC.db-wal new file mode 100644 index 00000000..092223dc Binary files /dev/null and b/.vscode/.browse.VC.db-wal differ diff --git a/README 2.md b/README 2.md new file mode 100644 index 00000000..3c439e07 --- /dev/null +++ b/README 2.md @@ -0,0 +1,105 @@ + +The objective of this assignment is to create an L System parser and generate interesting looking plants. Start by forking and then cloning this repository: [https://github.com/CIS700-Procedural-Graphics/Project3-LSystems](https://github.com/CIS700-Procedural-Graphics/Project3-LSystems) + +# Description + +**Linked List:** + +I began by implementing a basic doubly-linked list in `lsystem.js` using the new ES6 class methodology. I also added a file `tests.js` (and added an npm command `npm run tests`) where I tested out the linked list functionality. + +**LSystem Implementation:** + +Then, I added additional methods to my linked list implementation to transform it into more of an "LSystem" use case. For example, I wrote the method `replaceNode()` that replaces a character in the linked list with its replacement rule, and the method `doIterations()`, which runs multiple iterations of the LSystem replacement process. My LSystem can also handle multiple rules with a given probability distribution. + +**Turtle:** + +After finishing the linked list and LSystem, I started on the turtle implementation. I added a number of member variables to my turtle (including `rotY`, `rotZ`, `flowerColor`, etc...) to allow for the turtle's rendering to be customized by the user. I added four additional grammar rules, listed below: + +- `<`: Rotate in the Y direction X degrees +- `>`: Rotate in the Y direction -X degrees +- `O`: Draw a flower +- `L`: Draw a leaf + +I also added THREE.JS geometries for leaves and flowers (which are essentially just basic shapes). + +**GUI:** + +Almost every aspect of the turtle and the LSystem can be customized in the dat.gui sidebar. The LSystem iterations, initial axiom, and rules (probabilities and replacements) can all be tweaked. Additionally, the turtle's rotations (in both Y and Z), cylinder dimensions, and all colors can be customized. + +**Design technique:** + +To be honest, I essentially just added and tweaked rules here and there until I was able to generate a plant I thought looked cool. I experimented with different probabilities, and tried to position leaves and flowers in reasonable looking positions. + +**Screenshots:** + +![Screenshot A](https://raw.githubusercontent.com/zelliott/Project3-LSystems/master/images/plant_a.png) + +![Screenshot B](https://raw.githubusercontent.com/zelliott/Project3-LSystems/master/images/plant_b.png) + +![Screenshot C](https://raw.githubusercontent.com/zelliott/Project3-LSystems/master/images/plant_c.png) + + +# L-System Parser + +lsystem.js contains classes for L-system, Rule, and LinkedList. Here’s our suggested structure: + +**The Symbol Nodes/Linked List:** + +Rather than representing our symbols as a string like in many L-system implementations, we prefer to use a linked list. This allows us to store additional information about each symbol at time of parsing (e.g. what iteration was this symbol added in?) Since we’re adding and replacing symbols at each iteration, we also save on the overhead of creating and destroying strings, since linked lists of course make it easy to add and remove nodes. You should write a Linked List class with Nodes that contain at least the following information: + +- The next node in the linked list +- The previous node in the linked list +- The grammar symbol at theis point in the overal string + +We also recommend that you write the following functions to interact with your linked list: + +- A function to symmetrically link two nodes together (e.g. Node A’s next is Node B, and Node B’s prev is Node A) +- A function to expand one of the symbol nodes of the linked list by replacing it with several new nodes. This function should look at the list of rules associated with the symbol in the linked list’s grammar dictionary, then generate a uniform random number between 0 and 1 in order to determine which of the Rules should be used to expand the symbol node. You will refer to a Rule’s probability and compare it to your random number in order to determine which Rule should be chosen. + +**Rules:** + +These are containers for the preconditions, postconditions and probability of a single replacement operation. They should operate on a symbol node in your linked list. + +**L-system:** + +This is the parser, which will loop through your linked list of symbol nodes and apply rules at each iteration. + +Implement the following functions in L-System so that you can apply grammar rules to your axiom given some number of iterations. More details and implementation suggestions about functions can be found in the TODO comments + +- `stringToLinkedList(input_string)` +- `linkedListToString(linkedList)` +- `replaceNode(linkedList, node, replacementString)` +- `doIterations(num)` + +## Turtle + +`turtle.js` has a function called renderSymbol that takes in a single node of a linked list and performs an operation to change the turtle’s state based on the symbol contained in the node. Usually, the turtle’s change in state will result in some sort of rendering output, such as drawing a cylinder when the turtle moves forward. We have provided you with a few example functions to illustrate how to write your own functions to be called by renderSymbol; these functions are rotateTurtle, moveTurtle, moveForward, and makeCylinder. If you inspect the constructor of the Turtle class, you can see how to associate an operation with a grammar symbol. + +- Modify turtle.js to support operations associated with the symbols `[` and `]` + - When you parse `[` you need to store the current turtle state somewhere + - When you parse `]` you need to set your turtle’s state to the most recently stored state. Think of this a pushing and popping turtle states on and off a stack. For example, given `F[+F][-F]`, the turtle should draw a Y shape. Note that your program must be capable of storing many turtle states at once in a stack. + +- In addition to operations for `[` and `]`, you must invent operations for any three symbols of your choosing. + + +## Interactivity + +Using dat.GUI and the examples provided in the reference code, make some aspect of your demo an interactive variable. For example, you could modify: + +1. the axiom +2. Your input grammer rules and their probability +3. the angle of rotation of the turtle +4. the size or color or material of the cylinder the turtle draws, etc! + +## L-System Plants + +Design a grammar for a new procedural plant! As the preceding parts of this assignment are basic computer science tasks, this is where you should spend the bulk of your time on this assignment. Come up with new grammar rules and include screenshots of your plants in your README. For inspiration, take a look at Example 7: Fractal Plant in Wikipedia: https://en.wikipedia.org/wiki/L-system Your procedural plant must have the following features + +1. Grow in 3D. Take advantage of three.js! +2. Have flowers or leaves that are added as a part of the grammar +3. Variation. Different instances of your plant should look distinctly different! +4. A twist. Broccoli trees are cool and all, but we hope to see sometime a little more surprising in your grammars + +# Publishing Your code + +Running `npm run deploy` will automatically build your project and push it to gh-pages where it will be visible at `username.github.io/repo-name`. NOTE: You MUST commit AND push all changes to your MASTER branch before doing this or you may lose your work. The `git` command must also be available in your terminal or command prompt. If you're using Windows, it's a good idea to use Git Bash. \ No newline at end of file diff --git a/README.md b/README.md index fad423fa..cccb03b1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,51 @@ # Project 4: Shape Grammar +**General approach:** + +In this short summary, I'll briefly go over (1) how I designed my city layout, (2) how I created the shape grammar, and (3) any further implementation details along the way. + +**Code structure:** + +My codebase is divided up into a number of different sections and files: + + - `main.js`: This is the main runner, which sets up the framework and initializes the city and shape grammar. This file also creates shadows, lights, and sets up the camera. + - `city.js`: This file holds the `City` class, which in turn handles all aspects of rendering a city. This includes: rendering the base plane, rendering the rings and line divisions (streets), and rendering the cells and the buildings within them. + - `shapeGrammar.js`: This file is responsible for maintaining the shape grammar and advancing it any number of iterations. + - `shape_types/[shape_type].js`: Inside this folder are the various geometries that can be created in my shape grammar. These include chimneys, garages, houses, etc... + - There are other files, but the ones above are the main ones. + +**City layout:** + +My city is created and rendered in the following manner. First, I render the base plane of the city. Second, I render `n` concentric rings on the plane, of various radii. Third, I draw lines at various points between the rings to create divisions connecting rings with one another. Fourth, I render thicker geometries (streets) on these rings and divisions. Fifth, I divide up my plane into a number of cells (like a grid), and render a building on any cell that is far enough away from a street. At this point... the city has been rendered! This class has been designed in such a way that various properties of the city can be manipulated as member variables of the class. + +**Shape grammar:** + +Instead of using the given LSystem class, I created a new `shapeGrammar.js` file with a `ShapeGrammar` class because I wanted a more flexible implementation. This class begins by instantiating a starting axiom. Then the method `doIterations()` is run, which advances the intial axiom according to the shapes' successor rules. Finally, the class renders the final result of the shape grammar to the scene. There is also the option to apply per-shape-grammar-instance state data to each shape grammar, via the method `applyState(shapes)`. For example, location and density data can be passed into each instance of the shape grammar, thus allowing buildings in different locations to have completely different final results. + +**Rules:** + + - `Building`: Depending on the location of the building, its successors are either `Base`, `Mid`, and `Top` shapes, or `House` and `Garage` shapes. The former are for skyscraper-looking buildings, and the latter are for residential-looking buildings. The height of a `Building` is dependent on its location. + - `Base`: Successor is a `DoubleDoor`. + - `Mid`: Successors are a number of `Floor` shapes, depending on the height of the building. + - `Top`: Successor is a `Silo` with a random height. + - `Floor`: Successors are a number of `Window` shapes, with random colors. + - `Window`: Terminal + - `DoubleDoor`: Terminal + - `Silo`: Successor is an `Antenna`. + - `Antenna`: Terminal + - `House`: Successors are a `Door` and a `Roof`. The door is placed to the back of the house. + - `Door`: Terminal + - `Roof`: Successor is a `Chimney` in a random location. + - `Chimney`: Terminal + - `Garage`: Successor is a `GarageDoor` in a random location + - `GarageDoor`: Terminal + +--- + For this assignment you'll be building directly off of Project 3. To make things easier to keep track of, please fork and clone this repository [https://github.com/CIS700-Procedural-Graphics/Project4-Shape-Grammar](https://github.com/CIS700-Procedural-Graphics/Project4-Shape-Grammar) and copy your Project 3 code to start. -**Goal:** to model an urban environment using a shape grammar. +**Goal:** to model an urban environment using a shape grammar. **Note:** We’re well aware that a nice-looking procedural city is a lot of work for a single week. Focus on designing a nice building grammar. The city layout strategies outlined in class (the extended l-systems) are complex and not expected. We will be satisfied with something reasonably simple, just not a uniform grid! @@ -11,14 +53,14 @@ For this assignment you'll be building directly off of Project 3. To make things Modify your symbol node class to include attributes necessary for rendering, such as - Associated geometry instance - Position -- Scale +- Scale - Anything else you may need ## Grammar design (55 points) - Design at least five shape grammar rules for producing procedural buildings. Your buildings should vary in geometry and decorative features (beyond just differently-scaled cubes!). At least some of your rules should create child geometry that is in some way dependent on its parent’s state. (20 points) - Eg. A building may be subdivided along the x, y, or z axis into two smaller buildings - Some of your rules must be designed to use some property about its location. (10 points) - - Your grammar should have some element of variation so your buildings are non-deterministic. Eg. your buildings sometimes subdivide along the x axis, and sometimes the y. (10 points) + - Your grammar should have some element of variation so your buildings are non-deterministic. Eg. your buildings sometimes subdivide along the x axis, and sometimes the y. (10 points) - Write a renderer that will interpret the results of your shape grammar parser and adds the appropriate geometry to your scene for each symbol in your set. (10 points) ## Create a city (30 points) diff --git a/compiled/src/lsystem.js b/compiled/src/lsystem.js new file mode 100644 index 00000000..e7a75fc0 --- /dev/null +++ b/compiled/src/lsystem.js @@ -0,0 +1,290 @@ +// A class that represents a symbol replacement rule to +// be used when expanding an L-system grammar. +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +exports.stringToLinkedList = stringToLinkedList; +exports.linkedListToString = linkedListToString; +exports['default'] = Lsystem; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function Rule(prob, str) { + this.probability = prob; // The probability that this Rule will be used when replacing a character in the grammar string + this.successorString = str; // The string that will replace the char that maps to this Rule +} + +var ListNode = function ListNode(data) { + _classCallCheck(this, ListNode); + + this.prev = null; + this.next = null; + this.data = data; +}; + +exports.ListNode = ListNode; + +var LinkedList = (function () { + function LinkedList() { + _classCallCheck(this, LinkedList); + + this.head = null; + this.tail = null; + } + + // TODO: Turn the string into linked list + + _createClass(LinkedList, [{ + key: 'add', + value: function add(node) { + + if (!this.tail) { + this.head = node; + this.tail = node; + + return; + } + + node.prev = this.tail; + this.tail.next = node; + this.tail = node; + } + }, { + key: 'addFront', + value: function addFront(node) { + var n = this.head; + + this.head = node; + node.next = n; + n.prev = node; + } + }, { + key: 'addBack', + value: function addBack(node) { + var n = this.tail; + + this.tail = node; + n.next = node; + node.prev = n; + } + }, { + key: 'addAt', + value: function addAt(index, node) { + var n = this.head; + var i = 0; + var size = this.size(); + + if (index == 0) { + this.addFront(node); + return; + } + + if (index == size) { + this.addBack(node); + return; + } + + while (n) { + if (i == index) { + var prev = n.prev; + var next = n; + + if (prev) { + prev.next = node; + } + + node.prev = prev; + node.next = next; + + if (next) { + next.prev = node; + } + + return; + } + + n = n.next; + i++; + } + + throw new Error('Unable to add node at this index'); + } + }, { + key: 'removeFront', + value: function removeFront() { + var n = this.head; + + this.head = n.next; + this.head.prev = null; + } + }, { + key: 'removeBack', + value: function removeBack() { + var n = this.tail; + + this.tail = n.prev; + this.tail.next = null; + } + }, { + key: 'removeAt', + value: function removeAt(index) { + var n = this.head; + var i = 0; + var size = this.size(); + + if (index == 0) { + this.removeFront(); + return; + } + + if (index == size) { + this.removeBack(); + return; + } + + while (n) { + if (i == index) { + var prev = n.prev; + var next = n.next; + + if (prev) { + prev.next = next; + } + + if (next) { + next.prev = prev; + } + + return; + } + + n = n.next; + i++; + } + + throw new Error('Unable to remove node at this index'); + } + }, { + key: 'getAt', + value: function getAt(index) { + var n = this.head; + var i = 0; + + while (n) { + if (i == index) { + return n.data; + } + + n = n.next; + i++; + } + + throw new Error('Unable to get node at this index'); + } + }, { + key: 'clear', + value: function clear() { + this.head = null; + this.tail = null; + } + }, { + key: 'size', + value: function size() { + var n = this.head; + var i = 0; + + while (n) { + i++; + n = n.next; + } + + return i; + } + }, { + key: 'print', + value: function print() { + var n = this.head; + var ret = []; + + while (n) { + ret.push(n.data); + n = n.next; + } + + return ret; + } + }]); + + return LinkedList; +})(); + +exports.LinkedList = LinkedList; + +function stringToLinkedList(input_string) { + // ex. assuming input_string = "F+X" + // you should return a linked list where the head is + // at Node('F') and the tail is at Node('X') + var ll = new LinkedList(); + return ll; +} + +// TODO: Return a string form of the LinkedList + +function linkedListToString(linkedList) { + // ex. Node1("F")->Node2("X") should be "FX" + var result = ""; + return result; +} + +// TODO: Given the node to be replaced, +// insert a sub-linked-list that represents replacementString +function replaceNode(linkedList, node, replacementString) {} + +function Lsystem(axiom, grammar, iterations) { + // default LSystem + this.axiom = "FX"; + this.grammar = {}; + this.grammar['X'] = [new Rule(1.0, '[-FX][+FX]')]; + this.iterations = 0; + + // Set up the axiom string + if (typeof axiom !== "undefined") { + this.axiom = axiom; + } + + // Set up the grammar as a dictionary that + // maps a single character (symbol) to a Rule. + if (typeof grammar !== "undefined") { + this.grammar = Object.assign({}, grammar); + } + + // Set up iterations (the number of times you + // should expand the axiom in DoIterations) + if (typeof iterations !== "undefined") { + this.iterations = iterations; + } + + // A function to alter the axiom string stored + // in the L-system + this.updateAxiom = function (axiom) { + // Setup axiom + if (typeof axiom !== "undefined") { + this.axiom = axiom; + } + }; + + // TODO + // This function returns a linked list that is the result + // of expanding the L-system's axiom n times. + // The implementation we have provided you just returns a linked + // list of the axiom. + this.doIterations = function (n) { + var lSystemLL = StringToLinkedList(this.axiom); + return lSystemLL; + }; +} \ No newline at end of file diff --git a/compiled/src/tests.js b/compiled/src/tests.js new file mode 100644 index 00000000..1bd66143 --- /dev/null +++ b/compiled/src/tests.js @@ -0,0 +1,11 @@ +'use strict'; + +var _lsystemJs = require('./lsystem.js'); + +var list = new _lsystemJs.LinkedList(); +list.add(new _lsystemJs.ListNode(0)); +list.add(new _lsystemJs.ListNode(1)); +list.add(new _lsystemJs.ListNode(2)); + +console.log(list.getAt(2)); +console.log(list.size()); \ No newline at end of file diff --git a/deploy.js b/deploy.js new file mode 100644 index 00000000..9defe7c3 --- /dev/null +++ b/deploy.js @@ -0,0 +1,38 @@ +var colors = require('colors'); +var path = require('path'); +var git = require('simple-git')(__dirname); +var deploy = require('gh-pages-deploy'); +var packageJSON = require('require-module')('./package.json'); + +var success = 1; +git.fetch('origin', 'master', function(err) { + if (err) throw err; + git.status(function(err, status) { + if (err) throw err; + if (!status.isClean()) { + success = 0; + console.error('Error: You have uncommitted changes! Please commit them first'.red); + } + + if (status.current !== 'master') { + success = 0; + console.warn('Warning: Please deploy from the master branch!'.yellow) + } + + git.diffSummary(['origin/master'], function(err, diff) { + if (err) throw err; + + if (diff.files.length || diff.insertions || diff.deletions) { + success = 0; + console.error('Error: Current branch is different from origin/master! Please push all changes first'.red) + } + + if (success) { + var cfg = packageJSON['gh-pages-deploy'] || {}; + var buildCmd = deploy.getFullCmd(cfg); + deploy.displayCmds(deploy.getFullCmd(cfg)); + deploy.execBuild(buildCmd, cfg); + } + }) + }) +}) \ No newline at end of file diff --git a/images/plant_a.png b/images/plant_a.png new file mode 100644 index 00000000..5ada8241 Binary files /dev/null and b/images/plant_a.png differ diff --git a/images/plant_b.png b/images/plant_b.png new file mode 100644 index 00000000..6eaf07bd Binary files /dev/null and b/images/plant_b.png differ diff --git a/images/plant_c.png b/images/plant_c.png new file mode 100644 index 00000000..38cd8915 Binary files /dev/null and b/images/plant_c.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..e609adf4 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + HW2: LSystems + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 00000000..0d45d911 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "scripts": { + "start": "webpack-dev-server --hot --inline", + "build": "webpack", + "deploy": "node deploy.js", + "tests": "babel src/tests.js src/lsystem.js -d compiled && node compiled/src/tests.js" + }, + "gh-pages-deploy": { + "prep": [ + "build" + ], + "noprompt": true + }, + "dependencies": { + "dat-gui": "^0.5.0", + "gl-matrix": "^2.3.2", + "lodash": "^4.17.4", + "stats-js": "^1.0.0-alpha1", + "three": "^0.82.1", + "three-orbit-controls": "^82.1.0", + "three.meshline": "^1.0.3" + }, + "devDependencies": { + "babel-core": "^6.18.2", + "babel-loader": "^6.2.8", + "babel-preset-es2015": "^6.18.0", + "colors": "^1.1.2", + "gh-pages-deploy": "^0.4.2", + "simple-git": "^1.65.0", + "webpack": "^1.13.3", + "webpack-dev-server": "^1.16.2", + "webpack-glsl-loader": "^1.0.1" + } +} diff --git a/src/city.js b/src/city.js new file mode 100644 index 00000000..b4e4dfd9 --- /dev/null +++ b/src/city.js @@ -0,0 +1,308 @@ +const THREE = require('three'); +const ML = require('three.meshline'); +const _ = require('lodash'); + +export default class City { + constructor(scene, shapeGrammar) { + this.scene = scene; + + this.baseDim = 200; + this.baseDiagonal = Math.sqrt((2 * Math.pow(this.baseDim / 2, 2))); + this.base = null; + + this.numRings = 4; + this.ringPoints = 32; + this.ringWidth = 4; + + this.numDivisions = 12; + this.ringDivisions = []; + this.divisionWidth = this.ringWidth; + + this.riverPoints = 32; + + this.cellDim = 7; + this.cells = []; + + this.shapeGrammar = shapeGrammar; + + this.baseColor = 0x15938; + this.streetColor = 0x031923; + } + + renderBase() { + var geometry = new THREE.PlaneGeometry(this.baseDim, this.baseDim, 32); + var material = new THREE.MeshLambertMaterial({ color: this.baseColor, side: THREE.DoubleSide }); + var mesh = new THREE.Mesh(geometry, material); + + mesh.receiveShadow = true; + mesh.translateZ(-0.1); + + this.scene.add(mesh); + this.base = geometry; + } + + renderRings() { + var delta = (this.baseDim / 2) / this.numRings; + var radius = this.baseDim / 2; + + for (var i = 0; i < this.numRings; i++) { + var path = new THREE.Path(); + + path.absellipse(0, 0, radius, radius, 0, 2 * Math.PI); + + var points = _.map(path.getPoints(this.ringPoints), p => { + return new THREE.Vector3(p.x, p.y, 0); + }); + + this.renderLine(points); + this.renderRing(radius); + + this.ringDivisions.push({ + id: i, + radius: radius, + divisions: [], + segments: [], + points: points + }); + + radius -= delta; + } + } + + renderDivisions() { + var pos = new THREE.Vector3(0, this.baseDim / 2, 0); + var theta = (2 * Math.PI) / this.numDivisions; + var i = 0; + + for (var i = 0; i < this.numDivisions; i++) { + var rand = THREE.Math.randInt(0, this.numRings); + var z = new THREE.Vector3(0, 0, 1); + + for (var j = 0; j < rand; j++) { + var ringDivision = this.ringDivisions[j]; + var ratio = 0.75; + + var pA = pos.clone(); + var pB = pos.clone(); + + pA.multiplyScalar(1 - (j / this.numRings)); + pB.multiplyScalar(1 - ((j + 1) / this.numRings)); + + var points = [ pA, pB ]; + + this.renderLine(points); + this.renderDivision(pA, pB, theta * i); + + ringDivision.divisions.push({ + pA: pA, + pB: pB, + theta: theta * i, + }); + } + + pos.applyAxisAngle(z, theta); + } + + for (var i = 0; i < this.numRings; i++) { + var ringDivision = this.ringDivisions[i]; + var divisions = ringDivision.divisions; + + for (var j = 0; j < divisions.length; j++) { + var divisionA = divisions[j]; + var divisionB = divisions[(j + 1) % divisions.length]; + + var xs = [ divisionA.pA.x, divisionA.pB.x, divisionB.pA.x, divisionB.pB.x ]; + var ys = [ divisionA.pA.y, divisionA.pB.y, divisionB.pA.y, divisionB.pB.y ]; + + var minX = _.min(xs); + var maxX = _.max(xs); + var minY = _.min(ys); + var maxY = _.max(ys); + + var min = new THREE.Vector2(minX, minY); + var max = new THREE.Vector2(maxX, maxY); + + ringDivision.segments.push({ + bbox: new THREE.Box2(min, max) + }); + } + } + } + + renderCells() { + + var halfDim = this.baseDim / 2; + var epsilon = this.cellDim; + var threshold = 1; + + for (var x = -halfDim; x < halfDim; x += this.cellDim) { + for (var y = -halfDim; y < halfDim; y += this.cellDim) { + + var pos = new THREE.Vector3(); + pos.x = x; + pos.y = y; + pos.z = 0; + + var draw = true; + for (var i = 0; i < this.numRings; i++) { + var radius = this.ringDivisions[i].radius; + + if (Math.abs(pos.length() - radius) < epsilon) { + draw = false; + } + + var divisions = this.ringDivisions[i].divisions; + + for (var j = 0; j < divisions.length; j++) { + var division = divisions[j]; + var pA = division.pA; + var pB = division.pB; + + var distance = pA.distanceTo(pB); + var posDistance = pos.distanceTo(pA) + pos.distanceTo(pB); + + if (posDistance - distance < threshold) { + draw = false; + } + } + } + + if (halfDim - Math.abs(pos.x) < 5) { + draw = false; + } + + if (halfDim - Math.abs(pos.y) < 5) { + draw = false; + } + + if (draw) { + this.cells.push({ + pos: pos, + density: 1 - (pos.length() / this.baseDiagonal), + color: THREE.Math.randInt(0, 1) + }); + } + } + } + } + + renderRiver() { + var points = []; + var borderPoints = this.generateBorderPoints(2); + + points.push(borderPoints[0]); + + var x = this.getRandSpread(); + var y = this.getRandSpread(); + + points.push(new THREE.Vector3(x, y, 0)); + points.push(borderPoints[1]); + + var river = new THREE.CatmullRomCurve3(points); + + this.renderLine(river.getPoints(this.riverPoints)); + } + + renderBuildings() { + for (var i = 0; i < this.cells.length; i++) { + var cell = this.cells[i]; + + if (i % 2 == 0) { + continue; + } + + this.shapeGrammar.setState(cell); + this.shapeGrammar.render(); + } + } + + renderRing(radius) { + var halfWidth = (this.ringWidth / 2); + var geometry = new THREE.RingGeometry(radius - halfWidth, radius + halfWidth, this.ringPoints); + var material = new THREE.MeshBasicMaterial({ color: this.streetColor, side: THREE.DoubleSide }); + var mesh = new THREE.Mesh(geometry, material); + + this.scene.add(mesh); + } + + renderDivision(pointA, pointB, theta) { + var epsilon = 1; + var width = this.divisionWidth; + var height = (this.baseDim / (2 * 4)) + this.divisionWidth - epsilon; + + var geometry = new THREE.PlaneGeometry(width, height, 32); + var material = new THREE.MeshBasicMaterial({ color: this.streetColor, side: THREE.DoubleSide }); + var mesh = new THREE.Mesh(geometry, material); + + this.scene.add(mesh); + + var midPoint = pointA.clone(); + midPoint.add(pointB); + midPoint.multiplyScalar(0.5); + + mesh.translateX(midPoint.x); + mesh.translateY(midPoint.y); + mesh.rotateZ(theta); + } + + renderLine(points) { + var geometry = new THREE.Geometry(); + + geometry.vertices = points; + + var material = new THREE.LineBasicMaterial({ color: this.streetColor }); + var mesh = new THREE.Line(geometry, material); + + this.scene.add(mesh); + } + + generateBorderPoints(n) { + var points = []; + + for (var i = 0; i < n; i++) { + var p = new THREE.Vector3(); + var rand1 = THREE.Math.randInt(0, 3); + var rand2 = this.getRandSpread(); + + if (rand1 < 2) { + p.y = rand2; + } else { + p.x = rand2; + } + + switch (rand1) { + case 0: + p.x = this.baseDim / -2; + break; + case 1: + p.x = this.baseDim / 2; + break; + case 2: + p.y = this.baseDim / -2; + break; + case 3: + p.y = this.baseDim / 2; + break; + default: + break; + } + + points.push(p); + + } + + return points; + } + + getRandSpread() { + return THREE.Math.randFloatSpread(this.baseDim); + } + + clearBuildings() { + this.scene.traverseVisible(function(child) { + if (child.type !== 'Scene' && child.name === 'building') { + this.scene.remove(child); + } + }); + } +}; \ No newline at end of file diff --git a/src/color.js b/src/color.js new file mode 100644 index 00000000..7ffdc6e5 --- /dev/null +++ b/src/color.js @@ -0,0 +1,16 @@ +export default { + RED: 0xff0000, + GREEN: 0x00ff00, + BLUE: 0x0000ff, + CEMENT: 0x627c97, + WINDOW_OFF: 0x242c38, + WINDOW_ON: 0xf8e997, + DOOR: 0xffffff, + ROOF: 0xc7956c, + SILO: 0x95a9b2, + CHIMNEY: 0xd86345, + ANTENNA: 0xc7c8e1, + HOUSE: 0xccb4a0, + GARAGE: 0xb29e95, + GARAGEDOOR: 0xecffe2 +}; \ No newline at end of file diff --git a/src/framework.js b/src/framework.js new file mode 100644 index 00000000..acefaa8c --- /dev/null +++ b/src/framework.js @@ -0,0 +1,72 @@ + +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); + }, false); + + // 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 +} \ No newline at end of file diff --git a/src/lsystem.js b/src/lsystem.js new file mode 100644 index 00000000..fbbe0ac7 --- /dev/null +++ b/src/lsystem.js @@ -0,0 +1,358 @@ +// A class that represents a symbol replacement rule to +// be used when expanding an L-system grammar. +function Rule(prob, str) { + this.probability = prob; + this.successorString = str; +} + +export class ListNode { + constructor(data) { + this.prev = null; + this.next = null; + this.data = data; + } +} + +export class LinkedList { + constructor() { + this.head = null; + this.tail = null; + } + + add(node) { + + if (!this.tail) { + this.head = node; + this.tail = node; + + return; + } + + node.prev = this.tail; + this.tail.next = node; + this.tail = node; + } + + addFront(node) { + let n = this.head; + + this.head = node; + node.next = n; + n.prev = node; + } + + addBack(node) { + let n = this.tail; + + this.tail = node; + n.next = node; + node.prev = n; + } + + addAt(index, node) { + let n = this.head; + let i = 0; + let size = this.size(); + + if (index == 0) { + this.addFront(node); + return; + } + + if (index == size) { + this.addBack(node); + return; + } + + while (n) { + if (i == index) { + let prev = n.prev; + let next = n; + + if (prev) { + prev.next = node; + } + + node.prev = prev; + node.next = next; + + if (next) { + next.prev = node; + } + + return; + } + + n = n.next; + i++; + } + + throw new Error('Unable to add node at this index'); + } + + removeFront() { + let n = this.head; + + this.head = n.next; + this.head.prev = null; + } + + removeBack() { + let n = this.tail; + + this.tail = n.prev; + this.tail.next = null; + } + + removeAt(index) { + let n = this.head; + let i = 0; + let size = this.size(); + + if (index == 0) { + this.removeFront(); + return; + } + + if (index == size) { + this.removeBack(); + return; + } + + while (n) { + if (i == index) { + let prev = n.prev; + let next = n.next; + + if (prev) { + prev.next = next; + } + + if (next) { + next.prev = prev; + } + + return; + } + + n = n.next; + i++; + } + + throw new Error('Unable to remove node at this index'); + } + + getAt(index) { + let n = this.head; + let i = 0; + + while (n) { + if (i == index) { + return n.data; + } + + n = n.next; + i++; + } + + throw new Error('Unable to get node at this index'); + } + + clear() { + this.head = null; + this.tail = null; + } + + size() { + let n = this.head; + let i = 0; + + while (n) { + i++; + n = n.next; + } + + return i; + } + + print() { + let n = this.head; + let ret = []; + + while (n) { + ret.push(n.data); + n = n.next; + } + + return ret; + } + + fromString(input) { + let arr = input.split(''); + + for (let i = 0; i < arr.length; i++) { + let n = new ListNode(arr[i]); + this.add(n); + } + } + + toString() { + return this.print().join(''); + } +} + +// Turn the string into linked list +export function StringToLinkedList(input_string) { + var ll = new LinkedList(); + ll.fromString(input_string); + + return ll; +} + +// Return a string form of the LinkedList +export function LinkedListToString(linkedList) { + var result = linkedList.toString(); + + return result; +} + +// Replace node with the nodes generated from rules +function replaceNode(linkedList, node, index, rules) { + var ranges = []; + var start = 0; + + // Convert the probabilities to ranges + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + var prob = rule.probability; + + var range = { + lo: start, + hi: start + prob + }; + + ranges.push(range); + start += prob; + } + + var i = 0; + var rand = Math.random() * start; + + // Choose a random number and select the corresponding range + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + + if (rand >= range.lo && rand < range.hi) { + break; + } + } + + var replace = rules[i].successorString; + var arr = replace.split(''); + var next = node.next; + var start = node; + + // Based on our rule selection, make the replacement. + for (var i = 0; i < arr.length; i++) { + var a = arr[i]; + var n = new ListNode(a); + + node.next = n; + n.prev = node; + + node = n; + } + + if (next) { + node.next = next; + next.prev = node; + } + + linkedList.removeAt(index); + + return replace.length; +} + +export default function Lsystem(axiom, grammar, iterations) { + + // Default LSystem + this.axiom = 'FX'; + this.grammar = {}; + this.grammar['X'] = [ + new Rule(0.25, '[+F][>FF 17) ? 'skyscraper' : 'house'; + + if (type == 'skyscraper') { + + var base = new Shape(STBase, this); + var mid = new Shape(STMid, this); + var top = new Shape(STTop, this); + + return [ base, mid, top ]; + + } else { + + var house = new Shape(STHouse, this); + var garage = new Shape(STGarage, this); + + return [ house, garage ]; + + } + } +}; \ No newline at end of file diff --git a/src/shape_types/chimney.js b/src/shape_types/chimney.js new file mode 100644 index 00000000..c8c0fc55 --- /dev/null +++ b/src/shape_types/chimney.js @@ -0,0 +1,53 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +export default { + symbol: 'Chimney', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.CHIMNEY, + position: function () { + var position = this.parent.position(); + var scale = this.parent.scale(); + + position.x += THREE.Math.randInt(-0.5, 0.5); + position.y += THREE.Math.randInt(-2, -1); + position.z += (scale.z - 3); + + return position.clone(); + }, + scale: function () { + var scale = new THREE.Vector3(1, 1, 4); + + return scale.clone(); + }, + terminal: true, + successors: function() { + return []; + } +}; + +var STDoor = { + symbol: 'Door', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.DOOR, + position: function () { + var position = this.parent.position(); + var rand = THREE.Math.randInt(0, 1); + var y = rand ? -2.6 : 2.6; + + position.add(new THREE.Vector3(-1.6, y, 0)); + + return position; + }, + scale: function () { + var scale = new THREE.Vector3(0.5, 0.2, 1); + + return scale; + }, + successors: function () { + return []; + } +}; \ No newline at end of file diff --git a/src/shape_types/door.js b/src/shape_types/door.js new file mode 100644 index 00000000..eb1b3735 --- /dev/null +++ b/src/shape_types/door.js @@ -0,0 +1,28 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +export default { + symbol: 'Door', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.DOOR, + position: function () { + var position = this.parent.position(); + var rand = THREE.Math.randInt(0, 1); + var x = rand ? -1.6 : 1.6; + + position.add(new THREE.Vector3(x, 1.6, 0)); + + return position.clone(); + }, + scale: function () { + var scale = new THREE.Vector3(0.5, 0.1, 1); + + return scale.clone(); + }, + successors: function () { + return []; + } +}; \ No newline at end of file diff --git a/src/shape_types/doubledoor.js b/src/shape_types/doubledoor.js new file mode 100644 index 00000000..17b77887 --- /dev/null +++ b/src/shape_types/doubledoor.js @@ -0,0 +1,28 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +export default { + symbol: 'DoubleDoor', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.DOOR, + position: function () { + var position = this.parent.position(); + var rand = THREE.Math.randInt(0, 1); + var y = rand ? -2.6 : 2.6; + + position.add(new THREE.Vector3(-1.6, y, 0)); + + return position.clone(); + }, + scale: function () { + var scale = new THREE.Vector3(0.5, 0.1, 1); + + return scale.clone(); + }, + successors: function () { + return []; + } +}; \ No newline at end of file diff --git a/src/shape_types/floor.js b/src/shape_types/floor.js new file mode 100644 index 00000000..6b332c18 --- /dev/null +++ b/src/shape_types/floor.js @@ -0,0 +1,66 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STWindow from './window.js' + +export default { + symbol: 'Floor', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.CEMENT, + position: function () { + var position = this.parent.position(); + var side = this.side; + var id = this.id; + var floors = this.floors; + var midHeight = this.parent.scale().z; + var baseHeight = this.parent.position().z; + var floorHeight = midHeight / floors; + + var floorPosition = new THREE.Vector3(0, -2.6, floorHeight * id); + + floorPosition.applyAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI * (side / 2)); + + position.add(floorPosition); + + return position.clone(); + }, + scale: function () { + var id = this.id; + var floors = this.floors; + var side = this.side; + var midHeight = this.parent.scale().z; + var floorHeight = midHeight / floors; + + var x; + var y; + var z = floorHeight; + + if (side % 2 == 0) { + x = 5; + y = 0.1; + } else { + x = 0.1; + y = 5; + } + + var scale = new THREE.Vector3(x, y, z); + + return scale.clone(); + }, + successors: function () { + var successors = []; + + for (var i = 0; i < 5; i++) { + var window = new Shape(STWindow, this); + + window.set('id', i); + + successors.push(window); + } + + return successors; + } +}; \ No newline at end of file diff --git a/src/shape_types/garage.js b/src/shape_types/garage.js new file mode 100644 index 00000000..32e26779 --- /dev/null +++ b/src/shape_types/garage.js @@ -0,0 +1,43 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STGarageDoor from './garagedoor.js' + +export default { + symbol: 'Garage', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.GARAGE, + removable: false, + position: function () { + + if (this.computed.position) { + return this.computed.position.clone(); + } + + var position = this.parent.position(); + var rand = THREE.Math.randInt(0, 1); + var delta = rand ? 1 : -1; + + position.x += delta; + this.computed.position = position; + + return position.clone(); + }, + scale: function () { + var buildingHeight = this.parent.scale().z; + var garageHeight = THREE.Math.clamp(buildingHeight * 0.5, 2, 4); + var scale = new THREE.Vector3(3, 5, garageHeight); + + return scale.clone(); + }, + successors: function () { + var garageDoor = new Shape(STGarageDoor, this); + + garageDoor.terminal = true; + + return [ garageDoor ]; + } +}; \ No newline at end of file diff --git a/src/shape_types/garagedoor.js b/src/shape_types/garagedoor.js new file mode 100644 index 00000000..d8c40c2f --- /dev/null +++ b/src/shape_types/garagedoor.js @@ -0,0 +1,26 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +export default { + symbol: 'GarageDoor', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.GARAGEDOOR, + position: function () { + var position = this.parent.position(); + + position.add(new THREE.Vector3(0, -2.5, 0)); + + return position.clone(); + }, + scale: function () { + var scale = new THREE.Vector3(2, 0.1, 1.5); + + return scale.clone(); + }, + successors: function () { + return []; + } +}; \ No newline at end of file diff --git a/src/shape_types/house.js b/src/shape_types/house.js new file mode 100644 index 00000000..95ea269d --- /dev/null +++ b/src/shape_types/house.js @@ -0,0 +1,38 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STDoor from './door.js' +import STRoof from './roof.js' + +export default { + symbol: 'House', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.HOUSE, + removable: false, + position: function () { + var position = this.parent.position(); + var delta = 1 + + position.y += delta; + + return position.clone(); + }, + scale: function () { + var buildingHeight = this.parent.scale().z; + var houseHeight = buildingHeight * 0.8; + var scale = new THREE.Vector3(5, 3, houseHeight); + + return scale.clone(); + }, + successors: function () { + var door = new Shape(STDoor, this); + var roof = new Shape(STRoof, this); + + door.terminal = true; + + return [ door, roof ]; + } +}; \ No newline at end of file diff --git a/src/shape_types/mid.js b/src/shape_types/mid.js new file mode 100644 index 00000000..ad04821e --- /dev/null +++ b/src/shape_types/mid.js @@ -0,0 +1,54 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STFloor from './floor.js' + +export default { + symbol: 'Mid', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.CEMENT, + removable: false, + position: function () { + var buildingHeight = this.parent.scale().z; + var baseHeight = buildingHeight * 0.15; + var position = this.parent.position(); + + position.add(new THREE.Vector3(0, 0, baseHeight)); + + return position.clone(); + }, + scale: function () { + var buildingHeight = this.parent.scale().z; + var midHeight = buildingHeight * 0.7; + var scale = new THREE.Vector3(5, 5, midHeight); + + return scale.clone(); + }, + successors: function () { + var buildingHeight = this.parent.scale().z; + var midHeight = buildingHeight * 0.7; + var floors = 10; + var sides = 4; + var floorHeight = midHeight / floors; + var successors = []; + + if (buildingHeight > 17) { + for (var i = 0; i < floors; i++) { + for (var j = 0; j < sides; j++) { + var floor = new Shape(STFloor, this); + + floor.set('id', i); + floor.set('floors', floors); + floor.set('side', j); + + successors.push(floor); + } + } + } + + return successors; + } +}; \ No newline at end of file diff --git a/src/shape_types/roof.js b/src/shape_types/roof.js new file mode 100644 index 00000000..17433c16 --- /dev/null +++ b/src/shape_types/roof.js @@ -0,0 +1,56 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STChimney from './chimney.js' + +export default { + symbol: 'Roof', + geometry: function () { + var shape = new THREE.Shape(); + + var vertices = [ + new THREE.Vector2(-0.5, 0), new THREE.Vector2(0, 0.5), new THREE.Vector2(0.5, 0) + ]; + + shape.fromPoints(vertices); + + var geometry = new THREE.ExtrudeGeometry(shape, { + steps: 1, + amount: 1, + bevelEnabled: false + }); + + geometry.rotateX(Math.PI / 2); + + return geometry; + }, + color: COLOR.ROOF, + removable: false, + position: function () { + var position = this.parent.position(); + var scale = this.parent.scale(); + + position.y += 1.8; + position.z += scale.z; + position.z -= 3; + + return position.clone(); + }, + scale: function () { + var scale = new THREE.Vector3(1, 1, 1); + + scale.x = 7; + scale.y = 4; + scale.z = 5; + + return scale.clone(); + }, + successors: function () { + var chimney = new Shape(STChimney, this); + + return [ chimney ]; + } +}; \ No newline at end of file diff --git a/src/shape_types/silo.js b/src/shape_types/silo.js new file mode 100644 index 00000000..139f5be6 --- /dev/null +++ b/src/shape_types/silo.js @@ -0,0 +1,44 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STAntenna from './antenna.js' + +export default { + symbol: 'Silo', + geometry: function () { + var rand = THREE.Math.randInt(4, 8); + var geometry = new THREE.CylinderGeometry(1, 1, 1, rand); + + geometry.rotateX(Math.PI / 2); + + return geometry; + }, + color: COLOR.SILO, + removable: false, + position: function () { + var position = this.parent.position(); + + return position.clone(); + }, + scale: function () { + var scale = this.parent.scale(); + var rand = THREE.Math.randFloat(1, 3); + + scale.z = rand; + + return scale.multiply(new THREE.Vector3(0.5, 0.5, 1)).clone(); + }, + successors: function () { + var successors = []; + + if (THREE.Math.randFloat(0, 1) > 0.5) { + var antenna = new Shape(STAntenna, this); + successors.push(antenna); + } + + return successors; + } +}; \ No newline at end of file diff --git a/src/shape_types/top.js b/src/shape_types/top.js new file mode 100644 index 00000000..d1dfc52a --- /dev/null +++ b/src/shape_types/top.js @@ -0,0 +1,37 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +import STRoof from './roof.js' +import STSilo from './silo.js' + + +export default { + symbol: 'Top', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: COLOR.CEMENT, + removable: true, + position: function () { + var position = this.parent.position(); + var buildingHeight = this.parent.scale().z; + var baseMidHeight = buildingHeight * 0.85; + + position.add(new THREE.Vector3(0, 0, baseMidHeight)); + + return position.clone(); + }, + scale: function () { + var buildingHeight = this.parent.scale().z; + var topHeight = buildingHeight * 0.15; + var scale = new THREE.Vector3(5, 5, topHeight); + + return scale.clone(); + }, + successors: function () { + var silo = new Shape(STSilo, this); + + return [ silo ]; + } +}; \ No newline at end of file diff --git a/src/shape_types/window.js b/src/shape_types/window.js new file mode 100644 index 00000000..717573f5 --- /dev/null +++ b/src/shape_types/window.js @@ -0,0 +1,49 @@ +const THREE = require('three'); +const _ = require('lodash'); + +import COLOR from '../color.js' +import Shape from '../shape.js' + +export default { + symbol: 'Window', + geometry: new THREE.BoxGeometry(1, 1, 1), + color: function () { + return THREE.Math.randInt(0, 3) ? COLOR.WINDOW_OFF : COLOR.WINDOW_ON; + }, + position: function () { + var id = this.id; + var side = this.parent.side; + var position = this.parent.position(); + var floors = this.parent.floors; + var midHeight = this.parent.scale().z; + var floorHeight = midHeight / floors; + + var x = [ -2, -1, 0, 1, 2 ]; + var z = (floorHeight - 0.8) / -2; + var windowPosition = new THREE.Vector3(x[id], -0.1, z); + + windowPosition.applyAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI * (side / 2)); + + position.add(windowPosition); + + return position.clone(); + }, + scale: function () { + var scale = this.parent.scale(); + var side = this.parent.side; + + if (side % 2 == 0) { + scale.x = 0.8; + } else { + scale.y = 0.8; + } + + scale.z = 0.8; + + return scale.clone(); + }, + terminal: true, + successors: function () { + return []; + } +}; \ No newline at end of file diff --git a/src/tests.js b/src/tests.js new file mode 100644 index 00000000..a135177a --- /dev/null +++ b/src/tests.js @@ -0,0 +1,9 @@ +import { ListNode, LinkedList } from './lsystem.js' + +var list = new LinkedList(); +list.add(new ListNode(0)); +list.add(new ListNode(1)); +list.add(new ListNode(2)); + +console.log(list.getAt(2)); +console.log(list.size()); \ No newline at end of file diff --git a/src/turtle.js b/src/turtle.js new file mode 100644 index 00000000..04a1dd7c --- /dev/null +++ b/src/turtle.js @@ -0,0 +1,200 @@ +const THREE = require('three') +const _ = require('lodash'); + +// A class used to encapsulate the state of a turtle at a given moment. +// The Turtle class contains one TurtleState member variable. +// You are free to add features to this state class, +// such as color or whimiscality +var TurtleState = function(pos, dir) { + return { + pos: new THREE.Vector3(pos.x, pos.y, pos.z), + dir: new THREE.Vector3(dir.x, dir.y, dir.z) + } +} + +export default class Turtle { + + constructor(scene, grammar) { + this.state = new TurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(0,1,0)); + this.scene = scene; + this.stack = []; + this.rotZ = 45; + this.rotY = 45; + this.cylY = 2; + this.cylX = 0.1; + this.cylColor = 0x89c73c; + this.leafColor = 0xe1e241; + this.flowerColor = 0xff5b27; + + this.updateGrammar(); + } + + updateGrammar() { + this.renderGrammar = { + '+': this.rotateTurtle.bind(this, 0, 0, this.rotZ), + '-': this.rotateTurtle.bind(this, 0, 0, -1 * this.rotZ), + '<': this.rotateTurtle.bind(this, this.rotY, this.rotY, 0), + '>': this.rotateTurtle.bind(this, -1 * this.rotY, -1 * this.rotY, 0), + 'F': this.makeCylinder.bind(this, this.cylY, this.cylX), + '[': this.saveState.bind(this), + ']': this.returnToState.bind(this), + 'L': this.makeLeaf.bind(this), + 'O': this.makeFlower.bind(this) + }; + } + + clearScene() { + var obj; + for (var i = this.scene.children.length - 1; i > 0; i--) { + obj = this.scene.children[i]; + this.scene.remove(obj); + } + } + + // Resets the turtle's position to the origin + // and its orientation to the Y axis + clear() { + this.state = new TurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(0,1,0)); + } + + // A function to help you debug your turtle functions + // by printing out the turtle's current state. + printState() { + console.log(this.state.pos) + console.log(this.state.dir) + } + + // Rotate the turtle's _dir_ vector by each of the + // Euler angles indicated by the input. + rotateTurtle(x, y, z) { + var e = new THREE.Euler( + x * 3.14/180, + y * 3.14/180, + z * 3.14/180); + this.state.dir.applyEuler(e); + } + + // Translate the turtle along the input vector. + // Does NOT change the turtle's _dir_ vector + moveTurtle(x, y, z) { + var new_vec = THREE.Vector3(x, y, z); + this.state.pos.add(new_vec); + } + + // Translate the turtle along its _dir_ vector by the distance indicated + moveForward(dist) { + var newVec = this.state.dir.multiplyScalar(dist); + this.state.pos.add(newVec); + } + + // Make a cylinder of given length and width starting at turtle pos + // Moves turtle pos ahead to end of the new cylinder + makeCylinder(len, width) { + var geometry = new THREE.CylinderGeometry(width, width, len); + var material = new THREE.MeshBasicMaterial({ color: this.cylColor }); + var cylinder = new THREE.Mesh( geometry, material ); + this.scene.add(cylinder); + + // Orient the cylinder to the turtle's current direction + var quat = new THREE.Quaternion(); + quat.setFromUnitVectors(new THREE.Vector3(0,1,0), this.state.dir); + var mat4 = new THREE.Matrix4(); + mat4.makeRotationFromQuaternion(quat); + cylinder.applyMatrix(mat4); + + + // Move the cylinder so its base rests at the turtle's current position + var mat5 = new THREE.Matrix4(); + var trans = this.state.pos.add(this.state.dir.multiplyScalar(0.5 * len)); + mat5.makeTranslation(trans.x, trans.y, trans.z); + cylinder.applyMatrix(mat5); + + // Scoot the turtle forward by len units + this.moveForward(len / 2); + } + + makeLeaf() { + var shape = new THREE.Shape(); + var points = [ + new THREE.Vector2(0, 0), + new THREE.Vector2(0.5, 0), + new THREE.Vector2(0.5, 0.5) + ]; + + shape.fromPoints(points); + + var extrudeSettings = { + steps: 1, + amount: 0.1, + bevelEnabled: true, + bevelThickness: 0, + bevelSize: 0, + bevelSegments: 1 + }; + + var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings ); + var material = new THREE.MeshBasicMaterial({ color: this.leafColor }); + + var leaf = new THREE.Mesh(geometry, material); + this.scene.add(leaf); + + var mat5 = new THREE.Matrix4(); + var trans = this.state.pos; + mat5.makeTranslation(trans.x, trans.y, trans.z); + leaf.applyMatrix(mat5); + } + + makeFlower() { + var centers = [ + new THREE.Vector2(0.1, 0), + new THREE.Vector2(-0.2, -0.2), + new THREE.Vector2(-0.2, 0.2) + ]; + + for (var i = 0; i < centers.length; i++) { + var center = centers[i]; + var geometry = new THREE.CylinderGeometry(0.2, 0.2, this.cylX + 0.2, 32); + var material = new THREE.MeshBasicMaterial({ color: this.flowerColor }); + var petal = new THREE.Mesh( geometry, material ); + this.scene.add(petal); + + var quat = new THREE.Quaternion(); + var forward = new THREE.Vector3(0, 0, 1); + quat.setFromUnitVectors(new THREE.Vector3(0,1,0), forward); + var mat4 = new THREE.Matrix4(); + mat4.makeRotationFromQuaternion(quat); + petal.applyMatrix(mat4); + + var mat5 = new THREE.Matrix4(); + var trans = this.state.pos; + mat5.makeTranslation(trans.x + center.x + this.cylX, trans.y + center.y, trans.z); + petal.applyMatrix(mat5); + } + } + + saveState() { + this.stack.push(new TurtleState(this.state.pos, this.state.dir)); + } + + returnToState() { + this.state = this.stack.pop(); + } + + // Call the function to which the input symbol is bound. + // Look in the Turtle's constructor for examples of how to bind + // functions to grammar symbols. + renderSymbol(symbolNode) { + var func = this.renderGrammar[symbolNode.data]; + if (func) { + func(); + } + } + + // Invoke renderSymbol for every node in a linked list of grammar symbols. + renderSymbols(linkedList) { + var currentNode; + for(currentNode = linkedList.head; currentNode != null; currentNode = currentNode.next) { + this.renderSymbol(currentNode); + } + } +} \ 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