diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5171c540 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/README.md b/README.md index fad423fa..23e6e6a3 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,56 @@ # Project 4: Shape Grammar -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. - -**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! - -## Symbol Node (5 points) -Modify your symbol node class to include attributes necessary for rendering, such as -- Associated geometry instance -- Position -- 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) -- 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) -- Add a ground plane or some other base terrain to your scene (0 points, come on now) -- Using any strategy you’d like, procedurally generate features that demarcate your city into different areas in an interesting and plausible way (Just a uniform grid is neither interesting nor plausible). (20 points) - - Suggestions: roads, rivers, lakes, parks, high-population density - - Note, these features don’t have to be directly visible, like high-population density, but they should somehow be visible in the appearance or arrangement of your buildings. Eg. High population density is more likely to generate taller buildings -- Generate buildings throughout your city, using information about your city’s features. Color your buildings with a method that uses some aspect of its state. Eg. Color buildings by height, by population density, by number of rules used to generate it. (5 points) -- Document your grammar rules and general approach in the readme. (5 points) -- ??? -- Profit. - -## Make it interesting (10) -Experiment! Make your city a work of art. - - -## Warnings: -You can very easily blow up three.js with this assignment. With a very simple grammar, our medium quality machine was able to handle 100 buildings with 6 generations each, but be careful if you’re doing this all CPU-side. - -## Suggestions for the overachievers: -Go for a very high level of decorative detail! -Place buildings with a strategy such that buildings have doors and windows that are always accessible. -Generate buildings with coherent interiors -If dividing your city into lots, generate odd-shaped lots and create building meshes that match their shape ie. rather than working with cubes, extrude upwards from the building footprints you find to generate a starting mesh to subdivide rather than starting with platonic geometry. +## Generating Buildings +All buildings start off as a single box geometry of sizes (small, medium, large) +with some minor variations. The height of the buildings are determined by a population map +that can be imported from an image file. + +The first iteration of the shape grammar determines whether the building will have +side panels. + +A=>C, A=>S + +In the next iteration, the side panels will terminate. The other symbols will randomly +select between uniform vertical division of the block, or the same division with +differentiation of the top and bottom levels. + +C=>U, C=>T/U/B, S=>X + +The next iteration adds details to the levels. The top levels have a ticker or a +scaled block. The bottom levels are scaled larger. The middle levels select between +alternating level sizes, billboards or signs. + +U=>{billboards, signs, alternating scaling} + +## Placing Buildings +The entire city is represented by a grid system like Sim City. Each time +a building is placed, the grid locations are marked as occupied to prevent overlapping buildings. + +Buildings are placed probabilistically based on the population map using +the pointalism algorithm we talked about in class. Random points are selected from +the grid and will be kept or discarded using the population map value as a threshold. + +I also implemented a HTML5 canvas based lsystem to generate the roads. This is +still a work in progress as it does not take into account any additional parameters. +My intention is to translate the canvas values into the city grid. This way, buildings +will not over lap with roads. + +## Fun Features +The ticker textures on the top of buildings are generated procedurally. Normally, +we load an image into Three.js to use as a texture, but its actually possible to use +an HTML5 canvas instead [[2]](http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/) ! Just draw on a canvas element and then pass it directly +into THREE.texture(canvas). + +## Demo +![image](https://i.imgur.com/HrDAwEA.jpg) +Demo: https://iambrian.github.io/Project4-Shape-Grammar/ + +# Resources +[1] Borrowed some of the ticker messages from Sim City: http://simcity.wikia.com/wiki/List_of_news_ticker_messages + +[2] Procedural Textures using canvas: http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/ + +[3] Subversion city generator for inspiration: https://www.youtube.com/watch?v=FR9xI0GgrBY + +[4] Also Mirror's Edge for inspiration: http://imgur.com/LsvEPJW 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/index.html b/index.html new file mode 100644 index 00000000..78231237 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + HW2: LSystems + + + + + + + diff --git a/maps/population.png b/maps/population.png new file mode 100644 index 00000000..93db95c7 Binary files /dev/null and b/maps/population.png differ diff --git a/package.json b/package.json new file mode 100644 index 00000000..be683fcb --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "scripts": { + "start": "webpack-dev-server --hot --inline", + "build": "webpack", + "deploy": "node deploy.js" + }, + "gh-pages-deploy": { + "prep": [ + "build" + ], + "noprompt": true + }, + "dependencies": { + "dat-gui": "^0.5.0", + "gl-matrix": "^2.3.2", + "stats-js": "^1.0.0-alpha1", + "three": "^0.82.1", + "three-orbit-controls": "^82.1.0" + }, + "devDependencies": { + "babel-core": "^6.18.2", + "babel-loader": "^6.2.8", + "babel-preset-es2015": "^6.18.0", + "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/OBJLoader.js b/src/OBJLoader.js new file mode 100644 index 00000000..3277449f --- /dev/null +++ b/src/OBJLoader.js @@ -0,0 +1,718 @@ + +/** + * @author mrdoob / http://mrdoob.com/ + */ + + const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much + + +THREE.OBJLoader = function(manager) { + + this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager; + + this.materials = null; + + this.regexp = { + // v float float float + vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // vn float float float + normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // vt float float + uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // f vertex vertex vertex + face_vertex: /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, + // f vertex/uv vertex/uv vertex/uv + face_vertex_uv: /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, + // f vertex/uv/normal vertex/uv/normal vertex/uv/normal + face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, + // f vertex//normal vertex//normal vertex//normal + face_vertex_normal: /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, + // o object_name | g group_name + object_pattern: /^[og]\s*(.+)?/, + // s boolean + smoothing_pattern: /^s\s+(\d+|on|off)/, + // mtllib file_reference + material_library_pattern: /^mtllib /, + // usemtl material_name + material_use_pattern: /^usemtl / + }; + +}; + +THREE.OBJLoader.prototype = { + + constructor: THREE.OBJLoader, + + load: function(url, onLoad, onProgress, onError) { + + var scope = this; + + var loader = new THREE.XHRLoader(scope.manager); + loader.setPath(this.path); + loader.load(url, function(text) { + + onLoad(scope.parse(text)); + + }, onProgress, onError); + + }, + + setPath: function(value) { + + this.path = value; + + }, + + setMaterials: function(materials) { + + this.materials = materials; + + }, + + _createParserState: function() { + + var state = { + objects: [], + object: {}, + + vertices: [], + normals: [], + uvs: [], + + materialLibraries: [], + + startObject: function(name, fromDeclaration) { + + // If the current object (initial from reset) is not from a g/o declaration in the parsed + // file. We need to use it for the first parsed g/o to keep things in sync. + if (this.object && this.object.fromDeclaration === false) { + + this.object.name = name; + this.object.fromDeclaration = (fromDeclaration !== false); + return; + + } + + if (this.object && typeof this.object._finalize === 'function') { + + this.object._finalize(); + + } + + var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined); + + this.object = { + name: name || '', + fromDeclaration: (fromDeclaration !== false), + + geometry: { + vertices: [], + normals: [], + uvs: [] + }, + materials: [], + smooth: true, + + startMaterial: function(name, libraries) { + + var previous = this._finalize(false); + + // New usemtl declaration overwrites an inherited material, except if faces were declared + // after the material, then it must be preserved for proper MultiMaterial continuation. + if (previous && (previous.inherited || previous.groupCount <= 0)) { + + this.materials.splice(previous.index, 1); + + } + + var material = { + index: this.materials.length, + name: name || '', + mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''), + smooth: (previous !== undefined ? previous.smooth : this.smooth), + groupStart: (previous !== undefined ? previous.groupEnd : 0), + groupEnd: -1, + groupCount: -1, + inherited: false, + + clone: function(index) { + return { + index: (typeof index === 'number' ? index : this.index), + name: this.name, + mtllib: this.mtllib, + smooth: this.smooth, + groupStart: this.groupEnd, + groupEnd: -1, + groupCount: -1, + inherited: false + }; + } + }; + + this.materials.push(material); + + return material; + + }, + + currentMaterial: function() { + + if (this.materials.length > 0) { + return this.materials[this.materials.length - 1]; + } + + return undefined; + + }, + + _finalize: function(end) { + + var lastMultiMaterial = this.currentMaterial(); + if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) { + + lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; + lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; + lastMultiMaterial.inherited = false; + + } + + // Guarantee at least one empty material, this makes the creation later more straight forward. + if (end !== false && this.materials.length === 0) { + this.materials.push({ + name: '', + smooth: this.smooth + }); + } + + return lastMultiMaterial; + + } + }; + + // Inherit previous objects material. + // Spec tells us that a declared material must be set to all objects until a new material is declared. + // If a usemtl declaration is encountered while this new object is being parsed, it will + // overwrite the inherited material. Exception being that there was already face declarations + // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + + if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function") { + + var declared = previousMaterial.clone(0); + declared.inherited = true; + this.object.materials.push(declared); + + } + + this.objects.push(this.object); + + }, + + finalize: function() { + + if (this.object && typeof this.object._finalize === 'function') { + + this.object._finalize(); + + } + + }, + + parseVertexIndex: function(value, len) { + + var index = parseInt(value, 10); + return (index >= 0 ? index - 1 : index + len / 3) * 3; + + }, + + parseNormalIndex: function(value, len) { + + var index = parseInt(value, 10); + return (index >= 0 ? index - 1 : index + len / 3) * 3; + + }, + + parseUVIndex: function(value, len) { + + var index = parseInt(value, 10); + return (index >= 0 ? index - 1 : index + len / 2) * 2; + + }, + + addVertex: function(a, b, c) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push(src[a + 0]); + dst.push(src[a + 1]); + dst.push(src[a + 2]); + dst.push(src[b + 0]); + dst.push(src[b + 1]); + dst.push(src[b + 2]); + dst.push(src[c + 0]); + dst.push(src[c + 1]); + dst.push(src[c + 2]); + + }, + + addVertexLine: function(a) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push(src[a + 0]); + dst.push(src[a + 1]); + dst.push(src[a + 2]); + + }, + + addNormal: function(a, b, c) { + + var src = this.normals; + var dst = this.object.geometry.normals; + + dst.push(src[a + 0]); + dst.push(src[a + 1]); + dst.push(src[a + 2]); + dst.push(src[b + 0]); + dst.push(src[b + 1]); + dst.push(src[b + 2]); + dst.push(src[c + 0]); + dst.push(src[c + 1]); + dst.push(src[c + 2]); + + }, + + addUV: function(a, b, c) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push(src[a + 0]); + dst.push(src[a + 1]); + dst.push(src[b + 0]); + dst.push(src[b + 1]); + dst.push(src[c + 0]); + dst.push(src[c + 1]); + + }, + + addUVLine: function(a) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push(src[a + 0]); + dst.push(src[a + 1]); + + }, + + addFace: function(a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) { + + var vLen = this.vertices.length; + + var ia = this.parseVertexIndex(a, vLen); + var ib = this.parseVertexIndex(b, vLen); + var ic = this.parseVertexIndex(c, vLen); + var id; + + if (d === undefined) { + + this.addVertex(ia, ib, ic); + + } else { + + id = this.parseVertexIndex(d, vLen); + + this.addVertex(ia, ib, id); + this.addVertex(ib, ic, id); + + } + + if (ua !== undefined) { + + var uvLen = this.uvs.length; + + ia = this.parseUVIndex(ua, uvLen); + ib = this.parseUVIndex(ub, uvLen); + ic = this.parseUVIndex(uc, uvLen); + + if (d === undefined) { + + this.addUV(ia, ib, ic); + + } else { + + id = this.parseUVIndex(ud, uvLen); + + this.addUV(ia, ib, id); + this.addUV(ib, ic, id); + + } + + } + + if (na !== undefined) { + + // Normals are many times the same. If so, skip function call and parseInt. + var nLen = this.normals.length; + ia = this.parseNormalIndex(na, nLen); + + ib = na === nb ? ia : this.parseNormalIndex(nb, nLen); + ic = na === nc ? ia : this.parseNormalIndex(nc, nLen); + + if (d === undefined) { + + this.addNormal(ia, ib, ic); + + } else { + + id = this.parseNormalIndex(nd, nLen); + + this.addNormal(ia, ib, id); + this.addNormal(ib, ic, id); + + } + + } + + }, + + addLineGeometry: function(vertices, uvs) { + + this.object.geometry.type = 'Line'; + + var vLen = this.vertices.length; + var uvLen = this.uvs.length; + + for (var vi = 0, l = vertices.length; vi < l; vi++) { + + this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen)); + + } + + for (var uvi = 0, l = uvs.length; uvi < l; uvi++) { + + this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen)); + + } + + } + + }; + + state.startObject('', false); + + return state; + + }, + + parse: function(text) { + + console.time('OBJLoader'); + + var state = this._createParserState(); + + if (text.indexOf('\r\n') !== -1) { + + // This is faster than String.split with regex that splits on both + text = text.replace('\r\n', '\n'); + + } + + var lines = text.split('\n'); + var line = '', + lineFirstChar = '', + lineSecondChar = ''; + var lineLength = 0; + var result = []; + + // Faster to just trim left side of the line. Use if available. + var trimLeft = (typeof ''.trimLeft === 'function'); + + for (var i = 0, l = lines.length; i < l; i++) { + + line = lines[i]; + + line = trimLeft ? line.trimLeft() : line.trim(); + + lineLength = line.length; + + if (lineLength === 0) continue; + + lineFirstChar = line.charAt(0); + + // @todo invoke passed in handler if any + if (lineFirstChar === '#') continue; + + if (lineFirstChar === 'v') { + + lineSecondChar = line.charAt(1); + + if (lineSecondChar === ' ' && (result = this.regexp.vertex_pattern.exec(line)) !== null) { + + // 0 1 2 3 + // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + + state.vertices.push( + parseFloat(result[1]), + parseFloat(result[2]), + parseFloat(result[3])); + + } else if (lineSecondChar === 'n' && (result = this.regexp.normal_pattern.exec(line)) !== null) { + + // 0 1 2 3 + // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + + state.normals.push( + parseFloat(result[1]), + parseFloat(result[2]), + parseFloat(result[3])); + + } else if (lineSecondChar === 't' && (result = this.regexp.uv_pattern.exec(line)) !== null) { + + // 0 1 2 + // ["vt 0.1 0.2", "0.1", "0.2"] + + state.uvs.push( + parseFloat(result[1]), + parseFloat(result[2])); + + } else { + + throw new Error("Unexpected vertex/normal/uv line: '" + line + "'"); + + } + + } else if (lineFirstChar === "f") { + + if ((result = this.regexp.face_vertex_uv_normal.exec(line)) !== null) { + + // f vertex/uv/normal vertex/uv/normal vertex/uv/normal + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] + + state.addFace( + result[1], result[4], result[7], result[10], + result[2], result[5], result[8], result[11], + result[3], result[6], result[9], result[12]); + + } else if ((result = this.regexp.face_vertex_uv.exec(line)) !== null) { + + // f vertex/uv vertex/uv vertex/uv + // 0 1 2 3 4 5 6 7 8 + // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] + + state.addFace( + result[1], result[3], result[5], result[7], + result[2], result[4], result[6], result[8]); + + } else if ((result = this.regexp.face_vertex_normal.exec(line)) !== null) { + + // f vertex//normal vertex//normal vertex//normal + // 0 1 2 3 4 5 6 7 8 + // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] + + state.addFace( + result[1], result[3], result[5], result[7], + undefined, undefined, undefined, undefined, + result[2], result[4], result[6], result[8]); + + } else if ((result = this.regexp.face_vertex.exec(line)) !== null) { + + // f vertex vertex vertex + // 0 1 2 3 4 + // ["f 1 2 3", "1", "2", "3", undefined] + + state.addFace( + result[1], result[2], result[3], result[4]); + + } else { + + throw new Error("Unexpected face line: '" + line + "'"); + + } + + } else if (lineFirstChar === "l") { + + var lineParts = line.substring(1).trim().split(" "); + var lineVertices = [], + lineUVs = []; + + if (line.indexOf("/") === -1) { + + lineVertices = lineParts; + + } else { + + for (var li = 0, llen = lineParts.length; li < llen; li++) { + + var parts = lineParts[li].split("/"); + + if (parts[0] !== "") lineVertices.push(parts[0]); + if (parts[1] !== "") lineUVs.push(parts[1]); + + } + + } + state.addLineGeometry(lineVertices, lineUVs); + + } else if ((result = this.regexp.object_pattern.exec(line)) !== null) { + + // o object_name + // or + // g group_name + + var name = result[0].substr(1).trim(); + state.startObject(name); + + } else if (this.regexp.material_use_pattern.test(line)) { + + // material + + state.object.startMaterial(line.substring(7).trim(), state.materialLibraries); + + } else if (this.regexp.material_library_pattern.test(line)) { + + // mtl file + + state.materialLibraries.push(line.substring(7).trim()); + + } else if ((result = this.regexp.smoothing_pattern.exec(line)) !== null) { + + // smooth shading + + // @todo Handle files that have varying smooth values for a set of faces inside one geometry, + // but does not define a usemtl for each face set. + // This should be detected and a dummy material created (later MultiMaterial and geometry groups). + // This requires some care to not create extra material on each smooth value for "normal" obj files. + // where explicit usemtl defines geometry groups. + // Example asset: examples/models/obj/cerberus/Cerberus.obj + + var value = result[1].trim().toLowerCase(); + state.object.smooth = (value === '1' || value === 'on'); + + var material = state.object.currentMaterial(); + if (material) { + + material.smooth = state.object.smooth; + + } + + } else { + + // Handle null terminated files without exception + if (line === '\0') continue; + + throw new Error("Unexpected line: '" + line + "'"); + + } + + } + + state.finalize(); + + var container = new THREE.Group(); + container.materialLibraries = [].concat(state.materialLibraries); + + for (var i = 0, l = state.objects.length; i < l; i++) { + + var object = state.objects[i]; + var geometry = object.geometry; + var materials = object.materials; + var isLine = (geometry.type === 'Line'); + + // Skip o/g line declarations that did not follow with any faces + if (geometry.vertices.length === 0) continue; + + var buffergeometry = new THREE.BufferGeometry(); + + buffergeometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(geometry.vertices), 3)); + + if (geometry.normals.length > 0) { + + buffergeometry.addAttribute('normal', new THREE.BufferAttribute(new Float32Array(geometry.normals), 3)); + + } else { + + buffergeometry.computeVertexNormals(); + + } + + if (geometry.uvs.length > 0) { + + buffergeometry.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(geometry.uvs), 2)); + + } + + // Create materials + + var createdMaterials = []; + + for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { + + var sourceMaterial = materials[mi]; + var material = undefined; + + if (this.materials !== null) { + + material = this.materials.create(sourceMaterial.name); + + // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. + if (isLine && material && !(material instanceof THREE.LineBasicMaterial)) { + + var materialLine = new THREE.LineBasicMaterial(); + materialLine.copy(material); + material = materialLine; + + } + + } + + if (!material) { + + material = (!isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial()); + material.name = sourceMaterial.name; + + } + + material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; + + createdMaterials.push(material); + + } + + // Create mesh + + var mesh; + + if (createdMaterials.length > 1) { + + for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { + + var sourceMaterial = materials[mi]; + buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi); + + } + + var multiMaterial = new THREE.MultiMaterial(createdMaterials); + mesh = (!isLine ? new THREE.Mesh(buffergeometry, multiMaterial) : new THREE.Line(buffergeometry, multiMaterial)); + + } else { + + mesh = (!isLine ? new THREE.Mesh(buffergeometry, createdMaterials[0]) : new THREE.Line(buffergeometry, createdMaterials[0])); + } + + mesh.name = object.name; + + container.add(mesh); + + } + + console.timeEnd('OBJLoader'); + + return container; + + } + +}; diff --git a/src/builder.js b/src/builder.js new file mode 100644 index 00000000..37d2b218 --- /dev/null +++ b/src/builder.js @@ -0,0 +1,564 @@ +const THREE = require('three') + +var SHAPES = { + BOX: 0, + ICOSAHEDRON: 1 +} + +var SYMBOLS = { + A: 0, + C: 1, + U: 2, // uniform middle levels + T: 3, // top levels + B: 4, // bottom levels + S: 5, // side appendages + X: 6 // terminal +} + +var Random = { + sign: function() { return Math.random() > 0.5 ? -1 : 1; } +} + +export default class Builder { + + constructor() { + this.materials = {}; + this.objects = {}; + } + + getRandomAd() { + if (Math.random() < 0.7) { + var c = Math.floor(Math.random() * 16777215); + return new THREE.MeshBasicMaterial({color: c}); + } else { + return this.materials.billboard[Math.floor(Math.random() * this.materials.billboard.length)]; + } + } + + getRandomTicker() { + var phrases = [ + 'NASDAQ', + '105 43 21', + 'Dow 30', + '20,619.77', + '-2.03 (-0.09%)', + 'S&P 500', + 'Pigeon Alert! Extreme Pigeon Danger!', + 'Lunar Eclipse Obscured By Clouds', + 'Rumor Of Kitty Kibble Shortage Causes Futures To Drop; Consumers Stockpile', + ]; + + var top_phrase = phrases[Math.floor(Math.random() * phrases.length)]; + var bot_phrase = phrases[Math.floor(Math.random() * phrases.length)]; + + var w = 1024; + var h = 256; + var canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + var context = canvas.getContext('2d'); + context.fillStyle = '#000000'; + context.fillRect(0,0,w,h); + context.font = "bold 100px Arial"; + context.fillStyle = '#ffc935' + context.fillText(top_phrase,128,128); + context.fillText(bot_phrase,800,200); + + context.fillStyle = '#000000'; + context.lineWidth = 3; + for (var i = 0; i < w; i+=10) { + context.beginPath(); + context.moveTo(i,0); + context.lineTo(i,h); + context.stroke(); + } + + for (var i = 0; i < h; i+=10) { + context.beginPath(); + context.moveTo(0,i); + context.lineTo(w,i); + context.stroke(); + } + var texture = new THREE.Texture(canvas); + texture.needsUpdate = true; + var material = new THREE.MeshLambertMaterial({ + map: texture, + side: THREE.DoubleSide + }); + + return material; + } + + getRandomSign() { + var c = Math.floor(Math.random() * 16777215); + return new THREE.MeshBasicMaterial({color: c}); + } + + loadResources(callback) { + var resources = []; + + var tex_loader = new THREE.TextureLoader(); + var load_texture = (function(set, key, path, resolve, reject) { + var texture = tex_loader.load(path); + if (set !== undefined) { + if (!this.materials[set]) { + this.materials[set] = []; + } + this.materials[set].push(new THREE.MeshLambertMaterial({map: texture, overdraw: 0.5})); + } else { + this.materials[key] = new THREE.MeshLambertMaterial({map: texture, overdraw: 0.5}); + } + if (texture) { + resolve("Successfully loaded texture"); + } else { + reject(Error("Could not load texture")); + } + }).bind(this); + + resources.push(new Promise(function(resolve, reject) { + load_texture('billboard', 'ad1', './textures/ad1.jpg', resolve, reject); + })); + + resources.push(new Promise(function(resolve, reject) { + load_texture('billboard', 'ad2', './textures/ad2.jpg', resolve, reject); + })); + + resources.push(new Promise(function(resolve, reject) { + load_texture('billboard', 'ad2', './textures/ad3.png', resolve, reject); + })); + + return Promise.all(resources); + } + + /*************************** Basic Operations *****************************/ + + scale(parent, sl, sw, sh) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child = (JSON.parse(JSON.stringify(parent))); + child.l = parent.l * sl; + child.w = parent.w * sw; + child.h = parent.h * sh; + return [child]; + } + + return [parent]; + } + + rotate(parent, rx, ry, rz) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child = (JSON.parse(JSON.stringify(parent))); + child.rx = parent.rx + rx; + child.ry = parent.ry + ry; + child.rz = parent.rz + rz; + return [child]; + } + + return []; + } + + billboard(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child = (JSON.parse(JSON.stringify(parent))); + var sign = Random.sign(); + if (Math.random() > 0.5) { + child.l = parent.l * 0.8; + child.w = parent.w * 0.1; + child.h = parent.h; + child.x = parent.x; + child.y = parent.y + 0.5 * parent.h - 0.1 * parent.h - 0.5 * child.h; + child.z = parent.z + sign * 0.5 * parent.w; + child.material = this.getRandomAd(); + child.isTerminal = true; + } else { + child.l = parent.l * 0.1; + child.w = parent.w * 0.8; + child.h = parent.h; + child.x = parent.x + sign * 0.5 * parent.l; + child.y = parent.y + 0.5 * parent.h - 0.1 * parent.h - 0.5 * child.h; + child.z = parent.z; + child.material = this.getRandomAd(); + child.isTerminal = true; + } + + parent.isTerminal = true; // makes sure the sign isnt floating + + return [parent, child]; + } + + return [parent]; + } + + sign(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child = (JSON.parse(JSON.stringify(parent))); + var sign1 = Random.sign(); // which part face + var sign2 = Random.sign(); // which face + + if (Math.random() > 0.5) { + child.l = parent.w * 0.05; // sign thickness + child.w = parent.y * 0.1; // sign wideness + child.h = parent.h * 0.5; // sign height + child.x = parent.x + sign1 * (0.5 * parent.l - child.l); + child.y = parent.y; + child.z = parent.z + sign2 * (0.5 * parent.w + 0.5 * child.w); + child.isTerminal = true; + } else { + child.l = parent.y * 0.1; // sign wideness + child.w = parent.w * 0.05; // sign thickness + child.h = parent.h * 0.5; // sign height + child.x = parent.x + sign2 * (0.5 * parent.l + 0.5 * child.l); + child.y = parent.y; + child.z = parent.z + sign1 * (0.5 * parent.w - child.w); + child.isTerminal = true; + } + child.material = this.getRandomSign(); + parent.isTerminal = true; // makes sure the sign isnt floating + return [parent, child]; + } + + return [parent]; + } + + ticker(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child = (JSON.parse(JSON.stringify(parent))); + child.l = parent.l * 1.1; + child.w = parent.w * 1.1; + child.h = parent.h * 0.9; + child.material = this.getRandomTicker(); + child.isTerminal = true; + + parent.isTerminal = true; // makes sure the sign isnt floating + return [parent, child]; + } + return [parent]; + } + + + hdivide(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child1 = (JSON.parse(JSON.stringify(parent))); + child1.l = parent.l / 2; + child1.x = parent.x + parent.l / 4; + + var child2 = (JSON.parse(JSON.stringify(parent))); + child2.l = parent.l / 2; + child2.x = parent.x - parent.l / 4; + + return [child1, child2]; + } + + return [parent]; + } + + hzdivide(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child1 = (JSON.parse(JSON.stringify(parent))); + child1.w = parent.w / 2; + child1.z = parent.z + parent.w / 4; + + var child2 = (JSON.parse(JSON.stringify(parent))); + child2.w = parent.w / 2; + child2.z = parent.z - parent.w / 4; + + return [child1, child2]; + } + + return [parent]; + } + + + vdivide(parent) { + if (parent.isTerminal) { + return [parent]; + } + + switch (parent.type) { + case SHAPES.BOX: + var child1 = (JSON.parse(JSON.stringify(parent))); + child1.h = parent.h / 2; + child1.y = parent.y + (parent.h / 4); + + var child2 = (JSON.parse(JSON.stringify(parent))); + child2.h = parent.h / 2; + child2.y = parent.y - (parent.h / 4); + + return [child1, child2]; + } + + return [parent]; + } + + subdivide(parent) { + if (parent.isTerminal) { + return [parent]; + } + + var children = []; + var v_parts = this.vdivide(parent); + var temp = []; + v_parts.forEach((function(s) { + temp.push(this.hdivide(s)); + }).bind(this)); + var h_parts = [].concat.apply([], temp); + var temp = []; + h_parts.forEach((function(s) { + temp.push(this.hzdivide(s)); + }).bind(this)); + children.push([].concat.apply([], temp)); + + return [].concat.apply([], children); + } + + swap(parent, type) { + if (parent.isTerminal) { + return [parent]; + } + + switch (type) { + case SHAPES.ICOSAHEDRON: + var child = (JSON.parse(JSON.stringify(parent))); + child.type = SHAPES.ICOSAHEDRON; + child.detail = 1; + child.radius = Math.min(child.l, child.w); + return [child]; + } + return [parent]; + } + + deleteShape(parent) { + if (parent.isTerminal) { + return [parent]; + } + return []; + } + + /************************** Composite Operations **************************/ + + uniform(parent, n) { + var children = [parent]; + for (var j = 0; j < n; j++) { + var levels = []; + for (var i = 0; i < children.length; i++) { + var c = this.vdivide(children[i]); + levels.push(c[0]); + levels.push(c[1]); + } + children = levels; + } + return children; + } + + huniform(parent, n) { + var children = [parent]; + for (var j = 0; j < n; j++) { + var levels = []; + for (var i = 0; i < children.length; i++) { + var c = this.hdivide(children[i]); + levels.push(c[0]); + levels.push(c[1]); + } + children = levels; + } + return children; + } + + + /************************** Assembly Operations ***************************/ + + iterate(shape) { + switch (shape.symbol) { + case SYMBOLS.A: + var r = Math.random(); + if (Math.random() < 0.7) { + shape.symbol = SYMBOLS.C; + return [shape]; + } else { + var levels = this.huniform(shape, 2); + for (var i = 0; i < levels.length; i++) { + levels[i].symbol = SYMBOLS.C; + } + levels[0].symbol = SYMBOLS.S; + levels[levels.length-1].symbol = SYMBOLS.S; + return levels; + } + case SYMBOLS.C: + if (Math.random() > 0.3) { // UNIFORM + var levels = this.uniform(shape, 3); + for (var i = 0; i < levels.length; i++) { + levels[i].symbol = SYMBOLS.U; + } + } else { // TOP-BOTTOM + var levels = this.uniform(shape, 3); + for (var i = 0; i < levels.length; i++) { + levels[i].symbol = SYMBOLS.U; + } + levels[0].symbol = SYMBOLS.T; + levels[levels.length-1].symbol = SYMBOLS.B; + } + return levels; + case SYMBOLS.U: + switch (this.uparam.type) { + case 0: + var level = this.billboard(shape); + level[0].symbol = SYMBOLS.X; + level[1].symbol = SYMBOLS.X; + break; + case 1: + var level = this.sign(shape); + level[0].symbol = SYMBOLS.X; + level[1].symbol = SYMBOLS.X; + break; + case 2: + var levels = this.vdivide(shape); + var ratio = this.uparam.r; + var l0 = this.scale(levels[0], 1, 1, 1 + ratio); + l0[0].symbol = SYMBOLS.X; + var l1 = this.scale(levels[1], 0.9, 0.9, 1 - ratio); + l1[0].symbol = SYMBOLS.X; + var level = [l0[0], l1[0]]; + break; + } + return level; + case SYMBOLS.T: + if (Math.random() < 0.5) { + var level = this.ticker(shape); + level[0].symbol = SYMBOLS.X; + level[1].symbol = SYMBOLS.X; + } else { + var sr = Math.random() / 2 + 0.5; + var level = this.scale(shape, sr, sr, 1); + level[0].symbol = SYMBOLS.X; + } + return level; + case SYMBOLS.B: + var level = this.scale(shape, 1.2, 1.2, 1); + level[0].symbol = SYMBOLS.X; + return level; + case SYMBOLS.S: + var level = this.scale(shape, 0.8, 0.8, 0.8); + level[0].symbol = SYMBOLS.X; + return level; + default: + return [shape]; + } + } + + evalShapes(scene, shapes, pos) { + for (var i = 0; i < shapes.length; i++) { + var shape = shapes[i]; + switch (shape.type) { + case SHAPES.BOX: + var geometry = new THREE.BoxGeometry(shape.l, shape.h, shape.w); + geometry.applyMatrix(new THREE.Matrix4().makeTranslation(shape.x, shape.y, shape.z)); + geometry.rotateX(shape.rx); + geometry.rotateY(shape.ry); + geometry.rotateZ(shape.rz); + if (shape.material) { + var material = shape.material; + } else { + var material = new THREE.MeshLambertMaterial({color: this.color}); + } + var mesh = new THREE.Mesh(geometry, material); + mesh.position.set(pos.x, 0, pos.z); + scene.add(mesh); + var geo = new THREE.EdgesGeometry(mesh.geometry); + var mat = new THREE.LineBasicMaterial( { color: 0xcccccc, linewidth: 1 } ); + var wireframe = new THREE.LineSegments(geo, mat); + mesh.add(wireframe); + + break; + case SHAPES.ICOSAHEDRON: + var geometry = new THREE.IcosahedronGeometry(shape.radius, shape.detail); + geometry.applyMatrix(new THREE.Matrix4().makeTranslation(shape.x, shape.y, shape.z)); + geometry.rotateX(shape.rx); + geometry.rotateY(shape.ry); + geometry.rotateZ(shape.rz); + var c = Math.floor(Math.random() * 16777215); + var material = new THREE.MeshBasicMaterial({color: c, wireframe: false}); + var mesh = new THREE.Mesh(geometry, material); + mesh.position.set(pos.x, 0, pos.z); + scene.add(mesh); + var geo = new THREE.EdgesGeometry(mesh.geometry); + var mat = new THREE.LineBasicMaterial( { color: 0xcccccc, linewidth: 1 } ); + var wireframe = new THREE.LineSegments(geo, mat); + mesh.add(wireframe); + + break; + } + } + } + + generateBuilding(scene, options) { + var box_length = options.length * 2; + var box_width = options.width * 2; + var box_height = options.height; + this.color = 0xA8A8A8; + this.uparam = { + type: Math.floor(Math.random() * 10) % 3, + r: Math.random() + }; + + var shape = { + symbol: SYMBOLS.A, + type: SHAPES.BOX, + l: options.length, + w: options.width, + h: options.height, + x: 0, + y: 0.5 * options.height, + z: 0, + rx: 0, + ry: 0, + rz: 0 + }; + + var building = [shape]; + for (var i = 0; i < 3; i++) { + var temp = []; + building.forEach((function(shape) { + temp.push(this.iterate(shape)); + }).bind(this)); + building = [].concat.apply([], temp); + } + + this.evalShapes(scene, building, {x: options.x, z: options.z}); + return; + } + +} diff --git a/src/cturtle.js b/src/cturtle.js new file mode 100644 index 00000000..90b389c5 --- /dev/null +++ b/src/cturtle.js @@ -0,0 +1,121 @@ +const THREE = require('three') + +var CTurtleState = 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 CTurtle { + + constructor(w,h) { + var startX = Math.floor(Math.random() * w); + var startY = Math.floor(Math.random() * h); + this.state = new CTurtleState(new THREE.Vector3(startX, startY, 0), new THREE.Vector3(1,0,0)); + this.stateStack = []; + this.canvas = document.createElement('canvas'); + this.canvas.width = w; + this.canvas.height = h; + this.context = this.canvas.getContext('2d'); + this.context.fillStyle = '#ffffff'; + this.context.fillRect( 0, 0, w, h ); + + // TODO: Start by adding rules for '[' and ']' then more! + // Make sure to implement the functions for the new rules inside Turtle + if (typeof grammar === "undefined") { + this.renderGrammar = { + '+' : this.rotateTurtle.bind(this, 37), + '-' : this.rotateTurtle.bind(this, -58), + '*' : this.rotateTurtle.bind(this, 100), + '/' : this.rotateTurtle.bind(this, -90), + 'M' : this.moveTurtle.bind(this, 20), + '[' : this.saveState.bind(this), + ']' : this.restoreState.bind(this), + }; + } else { + this.renderGrammar = grammar; + } + } + + saveState() { + this.stateStack.push(new CTurtleState(this.state.pos, this.state.dir)); + } + + restoreState() { + this.state = this.stateStack.pop(); + } + + // Resets the turtle's position to the origin + // and its orientation to the Y axis + clear() { + this.state = new CTurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(1,0,0)); + } + + resetState() { + var w = this.canvas.width; + var h = this.canvas.height; + var startX = Math.floor(Math.random() * w); + var startY = Math.floor(Math.random() * h); + this.state = new CTurtleState(new THREE.Vector3(startX, startY, 0), new THREE.Vector3(1,0,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(degrees) { + var e = new THREE.Euler( + 0, + 0, + degrees * 3.14/180); + this.state.dir.applyEuler(e); + } + + moveTurtle(len) { + var pos = this.state.pos; + var newpos = this.state.pos.clone(); + newpos.addScaledVector(this.state.dir, len); + + var px = pos.x; + var py = pos.y; + var npx = newpos.x; + var npy = newpos.y; + + if (npx < 0 || npx > this.canvas.width || npy < 0 || npy > this.canvas.height) { + this.resetState(); + return; + } + + this.context.beginPath(); + this.context.lineWidth = 1; + this.context.moveTo(px, py); + this.context.lineTo(npx,npy); + this.context.stroke(); + this.state.pos = newpos; + } + + // 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.character]; + 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); + } + return this.canvas; + } +} diff --git a/src/framework.js b/src/framework.js new file mode 100644 index 00000000..76f901a5 --- /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..ef5851f8 --- /dev/null +++ b/src/lsystem.js @@ -0,0 +1,182 @@ +// A class that represents a symbol replacement rule to +// be used when expanding an L-system grammar. +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 +} + +// TODO: Implement a linked list class and its requisite functions +// as described in the homework writeup +function LinkedList() { + this.head = undefined; + this.tail = undefined; +} + +function LinkedListNode() { + this.character = ''; + this.prev = undefined; + this.next = undefined; +} + +// TODO: Turn the string into linked list +export 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(); + + var prev = undefined; + for (var i = 0; i < input_string.length; i++) { + var node = new LinkedListNode(); + node.character = input_string.charAt(i); + node.prev = prev; + if (prev) { + prev.next = node; + } + prev = node; + + if (i == 0) { + ll.head = node; + } + + if (i == input_string.length - 1) { + ll.tail = node; + } + } + + return ll; +} + +// TODO: Return a string form of the LinkedList +export function LinkedListToString(linkedList) { + // ex. Node1("F")->Node2("X") should be "FX" + var temp = []; + for (var node = linkedList.head; node != linkedList.tail; node = node.next) { + temp.push(node.character); + } + var result = temp.join(""); + return result; +} + +// TODO: Given the node to be replaced, +// insert a sub-linked-list that represents replacementString +function replaceNode(linkedList, node, replacementString) { +} + +function pickRuleFromDistr(distribution) { + var rules = []; + var probs = []; + for (var i = 0; i < distribution.length; i++) { + rules.push(distribution[i].successorString); + probs.push(distribution[i].probability); + } + var filtered = probs.filter(function(val) { return val > 0; }); + var minProb = Math.min.apply(null, filtered); + var probs = probs.map(function(x) { return Math.round(x / minProb); }); + var cmlProbs = []; + probs.reduce(function(a,b,i) { return cmlProbs[i] = a+b; }, 0); + + var r = Math.random() * cmlProbs[cmlProbs.length-1]-0.0000001; // ensure always less than max + for (var i = 0; i < cmlProbs.length; i++) { + if (r < cmlProbs[i]) { + return rules[i]; + } + } + console.log("No Rules to pick from!"); +} + +export default function Lsystem(axiom, grammar, iterations) { + // default LSystem + this.axiom = "X"; + this.grammar = {}; + // this.grammar['X'] = [ + // new Rule(1.0, '[-FX][+FX]') + // ]; + this.iterations = 0; + this.expansions = {}; + + // 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; + this.expansions = {}; + } + } + + this.UpdateRules = function(rules) { + this.grammar = {}; + this.expansions = {}; + for (var i = 0; i < rules.length; i++) { + var entry = rules[i]; + var prob = entry.Prob; + var data = entry.Rule.replace(/\s/g, "").split("="); + if (data.length == 2) { + var symbol = data[0]; + var rule = data[1]; + var R = new Rule(prob, rule); + if (!this.grammar[symbol]) { + this.grammar[symbol] = []; + } + this.grammar[symbol].push(R); + } else { + console.log("Invalid Rule: " + i); + } + } + } + + this.DoExpansion = function(n) { + if (n < 0) { + throw 'Invalid number of expansions!'; + } + + if (n == 0) { + this.expansions[0] = this.axiom; + } else if (!this.expansions[n]) { + var prev = this.DoExpansion(n-1); + var expsn = []; + for (var i = 0; i < prev.length; i++) { + var c = prev.charAt(i); + if (this.grammar[c] && this.grammar[c].length > 0) { + var r = pickRuleFromDistr(this.grammar[c]); + expsn.push(r); + } else { + expsn.push(c); + } + } + this.expansions[n] = expsn.join(""); + } + return this.expansions[n]; + } + + // 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) { + if (!this.expansions[n]) { + this.DoExpansion(n); + } + var lSystemLL = StringToLinkedList(this.expansions[n]); + return lSystemLL; + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..eef6b62e --- /dev/null +++ b/src/main.js @@ -0,0 +1,452 @@ + +const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much +import Framework from './framework' +import Builder from './builder.js' +import Turtle from './turtle.js' +import CTurtle from './cturtle.js' +import Lsystem, {LinkedListToString} from './lsystem.js' + +var settings = { + seed: 1.0, + resetCamera: function() {}, + newSeed: function(newVal) { settings.seed = Math.random(); }, + size: 10.0, + resolution: 128, + split: 0.0, + numBuildings: 1000 +} + +var lsystem_settings = { + Axiom: "A", + Rules: [ + {Rule: "A=[[[RRRM]RRRM]RRRM]", Prob: 1.0}, + {Rule: "B=RMB", Prob: 1.0}, + {Rule: "C=MC", Prob: 5.0}, + {Rule: "C=M", Prob: 1.0}, + {Rule: "R=-MR", Prob: 1.0}, + {Rule: "R=+MR", Prob: 1.0}, + {Rule: "R=*MR", Prob: 1.0}, + {Rule: "R=/MR", Prob: 1.0}, + {Rule: "P=*MR", Prob: 1.0}, + {Rule: "P=/MR", Prob: 1.0}, + {Rule: "M=[RB]PMC", Prob: 1.0}, + ], + iterations: 4, + Render: function() {} +} + +var turtle; + +var ZONES = { + UNZONED : {value: 1, name: "Unzoned", color: 0xd1cfca}, + ROAD : {value: 2, name: "Road", color: 0x2c2a2d}, +}; + +var STATUS = { + VACANT : {value: 1, name: "Vacant", color: 0xd1cfca}, + OCCUPIED : {value: 2, name: "Occupied", color: 0x2c2a2d}, +}; + +var city = { + grid: [], + maps: {} +} + +// called after the scene loads +function onLoad(framework) { + var scene = framework.scene; + var camera = framework.camera; + var renderer = framework.renderer; + var gui = framework.gui; + var stats = framework.stats; + + // initialize a simple box and material + var directionalLight = new THREE.DirectionalLight( 0xffffff, 1 ); + directionalLight.color.setHSL(0.1, 1, 0.95); + directionalLight.position.set(1, 3, 2); + directionalLight.position.multiplyScalar(10); + scene.add(directionalLight); + + // set camera position + camera.position.set(0,10,10); + camera.lookAt(new THREE.Vector3(0,0,0)); + + gui.add(camera, 'fov', 0, 180).onChange(function(newVal) { + camera.updateProjectionMatrix(); + }); + gui.add(settings, 'resetCamera').onChange(function() { + camera.position.set(0,10,10); + camera.lookAt(new THREE.Vector3(0,0,0)); + }); + gui.add(settings, 'split', 0, 10); + + + // var roads = gui.addFolder('Roads'); + // roads.add(lsystem_settings, 'Axiom') + // for (var i = 0; i < lsystem_settings.Rules.length; i++) { + // roads.add(lsystem_settings.Rules[i], 'Rule'); + // roads.add(lsystem_settings.Rules[i], 'Prob'); + // } + // roads.add(lsystem_settings, 'Render').onChange(function() { + // regenerateCity(framework); + // }); + // roads.open(); + + regenerateCity(framework); +} + +function resizeCanvas(canvas) { + var canvas2 = document.createElement('canvas'); + canvas2.width = 512; + canvas2.height = 512; + var context = canvas2.getContext( '2d' ); + context.imageSmoothingEnabled = false; + context.webkitImageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height ); + return canvas2; +} + +function getMapValue(canvas, ni, nj) { + var i = canvas.width * ni; + var j = canvas.height * nj; + var context = canvas.getContext('2d'); + var pixel = context.getImageData(i, j, 1, 1); + var data = pixel.data; + return new THREE.Vector4(data[0], data[1], data[2], data[3]/255); +} + +function generateTexture() { + var size = settings.resolution; + var canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + var context = canvas.getContext('2d'); + context.fillStyle = '#ffffff'; + context.fillRect(0, 0, size, size); + + // generate grass texture + for (var i = 0; i < size; i++) { + for (var j = 0; j < size; j++) { + var r = Math.floor(50 * Math.random()); + var g = Math.floor(50 * Math.random()) + 150; + var b = Math.floor(50 * Math.random()); + context.fillStyle = 'rgb(' + [r, r, r].join( ',' ) + ')'; + context.fillRect( i, j, 1, 1 ); + } + } + + // generate roads + for (var i = 0; i < size; i++) { + for (var j = 0; j < size; j++) { + var norm_i = i / size; + var norm_j = j / size; + var rgba = getMapValue(city.maps['roads'], norm_i, norm_j); + if (rgba.x < 250) { + context.fillStyle = 'rgba(' + [200, 200, 200, (255 - rgba.x) / 255].join( ',' ) + ')'; + context.fillRect( i, j, 1, 1 ); + } + } + } + return resizeCanvas(canvas); +} + +function loadMapFromCanvas(framework, key, canvas) { + var scene = framework.scene; + var renderer = framework.renderer; + var texture = new THREE.Texture(canvas); + texture.anisotropy = renderer.getMaxAnisotropy(); + texture.needsUpdate = true; + var material = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide + }); + var geometry = new THREE.PlaneBufferGeometry( settings.size, settings.size, 1 ); + var plane = new THREE.Mesh( geometry, material ); + plane.rotateX(Math.PI / 2.0); + plane.name = key; + scene.add(plane); + city.maps[key] = canvas; + return canvas; +} + +function loadMapFromPath(framework, key, path) { + var promise = new Promise(function(resolve, reject) { + var img = new Image(); + img.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = 512; + canvas.height = 512; + var context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + if (loadMapFromCanvas(framework, key, canvas)) { + resolve("Stuff worked!"); + } else { + reject(Error("It broke")); + } + }; + img.src = path; + }); + return promise; +} + +function getZoneProperties(zoneValue) { + var myKeys = Object.keys(ZONES); + var matchingKeys = myKeys.filter(function(key){ + return ZONES[key].value == zoneValue; + }); + return ZONES[matchingKeys[0]]; +} + +function regenerateCity(framework) { + var scene = framework.scene; + var camera = framework.camera; + var renderer = framework.renderer; + var gui = framework.gui; + var stats = framework.stats; + + // clear scene + scene.children.forEach(function(object){ + scene.remove(object); + }); + + // reset grid + city.grid = []; + for (var i = 0; i < settings.resolution; i++) { + city.grid.push([]); + for (var j = 0; j < settings.resolution; j++) { + city.grid[i].push({ + zone: ZONES.UNZONED.value, + status: STATUS.VACANT.value + }); + } + } + + // generate roads + var lsys = new Lsystem(lsystem_settings.Axiom); + lsys.UpdateRules(lsystem_settings.Rules); + var result = lsys.DoIterations(lsystem_settings.iterations); + // turtle = new Turtle(city.grid); + // turtle.renderSymbols(result); + + var cturtle = new CTurtle(256,256); + var turtle_canvas = cturtle.renderSymbols(result); + loadMapFromCanvas(framework, 'roads', turtle_canvas); + // var debug = document.getElementById('debug'); + // debug.appendChild(turtle_canvas); + + loadMapFromPath(framework, 'population', './maps/population.png') + .then(function(response) { + try { + generateTerrain(framework); + generateBuildings(framework); + } catch (error) { + console.log("error " + error); + } + }, function(error) { + console.error("Failed!", error); + }); + + var light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 ); + scene.add(light); + var light = new THREE.PointLight( 0xffffff, 10, 100 ); + light.position.set( 50, 50, 50 ); + scene.add(light); +} + + +function generateTerrain(framework) { + var scene = framework.scene; + var renderer = framework.renderer; + + var geometry = new THREE.PlaneBufferGeometry( settings.size, settings.size, 1 ); + var texture = new THREE.Texture(generateTexture()); + texture.anisotropy = renderer.getMaxAnisotropy(); + texture.needsUpdate = true; + var material = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide + }); + var plane = new THREE.Mesh( geometry, material ); + + plane.rotateX(Math.PI / 2.0); + plane.name = "terrain"; + scene.add(plane); +} + +function generateRoads() { + // vertical streets + for (var i = 0; i < settings.resolution; i+=16) { + for (var j = 0; j < settings.resolution; j++) { + city.grid[i][j] = { + zone: ZONES.ROAD.value, + status: STATUS.OCCUPIED.value + }; + } + } + + // horizontal streets + for (var i = 0; i < settings.resolution; i+=8) { + for (var j = 0; j < settings.resolution; j++) { + city.grid[j][i] = { + zone: ZONES.ROAD.value, + status: STATUS.OCCUPIED.value + }; + } + } +} + + +function generateBuildings(framework) { + var scene = framework.scene; + var loader = new THREE.TextureLoader(); + var square_size = settings.size / settings.resolution; + var half_square_size = 0.5 * settings.size / settings.resolution; + var offset = settings.size / 2.0 - square_size / 2.0; + var builder = new Builder(); + + builder.loadResources() + .then(function(response) { + try { + + console.log("hey"); + + var building_sizes = { + small: [ + [2,2,2], + [3,1,2] + ], + medium: [ + [3,3,6], + [3,3,10], + ], + large: [ + [5,5,9], + [5,5,15], + [5,5,20] + ] + } + + var canvas = document.createElement('canvas'); + canvas.width = settings.resolution; + canvas.height = settings.resolution; + var context = canvas.getContext('2d'); + context.fillStyle = '#ffffff'; + context.fillRect(0,0,settings.resolution, settings.resolution); + context.fillStyle = '#000000'; + + // var debug = document.getElementById('debug'); + // debug.appendChild(canvas); + + for (var n = 0; n < settings.numBuildings; n++) { + // randomly pick points + var norm_i = Math.random(); + var norm_j = Math.random(); + + // keep point based on probability (relative to population) + var rgba = getMapValue(city.maps['population'], norm_i, norm_j); + + var keep = Math.random() * 255; + if (keep > rgba.x) { + + // pick a building size semi-randomly + if (keep < 100) { + var building_size = building_sizes.large[Math.floor(Math.random()*6)%3] + } else if (keep < 200) { + var building_size = building_sizes.medium[Math.floor(Math.random()*4)%2] + } else { + var building_size = building_sizes.small[Math.floor(Math.random()*3)%2] + } + + // check that the building fits in the location + var i = Math.floor(norm_i * settings.resolution); + var j = Math.floor(settings.resolution - norm_j * settings.resolution); + var bbox_x = [-Math.floor((building_size[0]+1)/2), Math.floor((building_size[0]+1)/2)]; + var bbox_z = [-Math.floor((building_size[1]+1)/2), Math.floor((building_size[1]+1)/2)]; + var isVacant = true; + for (var q = bbox_x[0]; q < bbox_x[1]; q++) { + for (var r = bbox_z[0]; r < bbox_z[1]; r++) { + + // bbox must not be out of grid and must not be occupied + if ((q+i).clamp(0,settings.resolution-1) != q+i || + (r+j).clamp(0,settings.resolution-1) != r+j || + city.grid[q+i][r+j].status != STATUS.VACANT.value) { + isVacant = false; + break; + } + } + } + + if (isVacant) { + // generate building + var scale_factor = settings.size / settings.resolution; // 0.5 bc box size is half size width + var density = Math.pow((255 - rgba.x) / 255, 2) * 20; + var density = 0.4; + var options = { + iterations: 5, + length: scale_factor * building_size[0], + width: scale_factor * building_size[1], + height: scale_factor * (building_size[2] + (Math.random() * building_size[2])), + x: i * square_size - offset, + z: j * square_size - offset + }; + var building = builder.generateBuilding(scene, options); + + // Update the grid + for (var q = bbox_x[0]; q < bbox_x[1]; q++) { + for (var r = bbox_z[0]; r < bbox_z[1]; r++) { + city.grid[q+i][r+j].status = STATUS.OCCUPIED.value; + } + } + } + } + } + + } catch (error) { + console.log("error " + error); + } + + }, function(error) { + console.error("Failed!", error); + }); +} + +function within(x, low, high) { + return x > low && x < high; +} + +// called on frame updates +function onUpdate(framework) { + var scene = framework.scene; + var camera = framework.camera; + var renderer = framework.renderer; + var gui = framework.gui; + var stats = framework.stats; + + var i = 1; + for (var key in city.maps) { + var plane = scene.getObjectByName(key); + plane.position.set(0, -i * settings.split, 0); + i++; + } +} + +/** + * Returns a number whose value is limited to the given range. + * + * Example: limit the output of this computation to between 0 and 255 + * (x * 255).clamp(0, 255) + * + * Source: http://strd6.com/2010/08/useful-javascript-game-extensions-clamp/ + * + * @param {Number} min The lower boundary of the output range + * @param {Number} max The upper boundary of the output range + * @returns A number in the range [min, max] + * @type Number + */ +Number.prototype.clamp = function(min, max) { + return Math.min(Math.max(this, min), max); +}; + +// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate +Framework.init(onLoad, onUpdate); diff --git a/src/turtle.js b/src/turtle.js new file mode 100644 index 00000000..0d09b9de --- /dev/null +++ b/src/turtle.js @@ -0,0 +1,195 @@ +const THREE = require('three') + +var DIR = { + N: 0, + NE: 1, + E: 2, + SE: 3, + S: 4, + SW: 5, + W: 6, + NW: 7 +} + + +var ZONES = { + UNZONED : {value: 1, name: "Unzoned", color: 0xd1cfca}, + ROAD : {value: 2, name: "Road", color: 0x2c2a2d}, + RESIDENTIAL: {value: 3, name: "Residential", color: 0x1fbc14}, + COMMERCIAL : {value: 4, name: "Commerical", color: 0x7c14bc}, + INDUSTRIAL : {value: 5, name: "Industrial", color: 0xddac30} +}; + +// 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.Vector2(pos.x, pos.y), + dir: dir + } +} + +export default class Turtle { + + constructor(grid, grammar) { + this.grid = grid; + console.log(this.grid); + // var startX = Math.floor(Math.random() * grid.length); + // var startY = Math.floor(Math.random() * grid[0].length); + var startX = 30 % this.grid.length; + var startY = 30 % this.grid[0].length; + this.state = new TurtleState(new THREE.Vector2(startX, startY), DIR.N); + this.stateStack = []; + + // TODO: Start by adding rules for '[' and ']' then more! + // Make sure to implement the functions for the new rules inside Turtle + if (typeof grammar === "undefined") { + this.renderGrammar = { + '+' : this.rotateTurtle.bind(this, 45), + '-' : this.rotateTurtle.bind(this, -45), + '*' : this.rotateTurtle.bind(this, 90), + '/' : this.rotateTurtle.bind(this, -90), + 'M' : this.moveTurtle.bind(this, 5), + '[' : this.saveState.bind(this), + ']' : this.restoreState.bind(this), + }; + } else { + this.renderGrammar = grammar; + } + } + + saveState() { + console.log("state stored!"); + console.log(this.state); + this.stateStack.push(new TurtleState(this.state.pos, this.state.dir, this.state.ortho)); + } + + restoreState() { + console.log("state restored!"); + this.state = this.stateStack.pop(); + console.log(this.state); + } + + // Resets the turtle's position to the origin + // and its orientation to the Y axis + clear() { + this.state = new TurtleState(new THREE.Vector2(0,0), DIR.N); + } + + // 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(degrees) { + if (degrees = 45) { + this.state.dir = (this.state.dir + 1) % 8; + } else if (degrees == -45) { + this.state.dir = (this.state.dir + 7) % 8; + } else if (degrees == 90) { + this.state.dir = (this.state.dir + 2) % 8; + } else if (degrees == -90) { + this.state.dir = (this.state.dir + 6) % 8; + } else { + console.log("unsupported rotation amount"); + } + } + + inBounds(pos) { + return pos.x < this.grid.length && + pos.x >= 0 && + pos.y < this.grid[0].length && + pos.y >= 0; + } + + moveTurtleDir(dir) { + var direction = new THREE.Vector2(0,0); + switch (dir) { + case DIR.N: + direction = new THREE.Vector2(0,1); + break; + case DIR.S: + direction = new THREE.Vector2(0,-1); + break; + case DIR.E: + direction = new THREE.Vector2(1,0); + break; + case DIR.W: + direction = new THREE.Vector2(-1,0); + break; + } + + this.grid[this.state.pos.x][this.state.pos.y] = { + zone: ZONES.ROAD.value + }; + var newpos = new THREE.Vector2(this.state.pos.x + direction.x, this.state.pos.y + direction.y) + if (this.inBounds(newpos)) { + this.state.pos = newpos; + } else { + var startX = Math.floor(Math.random() * this.grid.length); + var startY = Math.floor(Math.random() * this.grid[0].length); + this.state = new TurtleState(new THREE.Vector2(startX, startY), this.state.dir); + } + } + + moveTurtle(len) { + for (var i = 0; i < len; i++) { + switch (this.state.dir) { + case DIR.N: + this.moveTurtleDir(DIR.N); + break; + case DIR.S: + this.moveTurtleDir(DIR.S); + break; + case DIR.E: + this.moveTurtleDir(DIR.E); + break; + case DIR.W: + this.moveTurtleDir(DIR.W); + break; + case DIR.NE: + this.moveTurtleDir(DIR.N); + this.moveTurtleDir(DIR.E); + break; + case DIR.SE: + this.moveTurtleDir(DIR.S); + this.moveTurtleDir(DIR.E); + break; + case DIR.NW: + this.moveTurtleDir(DIR.N); + this.moveTurtleDir(DIR.W); + break; + case DIR.SW: + this.moveTurtleDir(DIR.S); + this.moveTurtleDir(DIR.W); + break; + } + } + } + + // 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.character]; + if (func) { + console.log(symbolNode.character); + 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); + } + console.log(this.grid); + } +} diff --git a/textures/.DS_Store b/textures/.DS_Store new file mode 100644 index 00000000..191cd4a7 Binary files /dev/null and b/textures/.DS_Store differ diff --git a/textures/ad1.jpg b/textures/ad1.jpg new file mode 100644 index 00000000..a86a92c4 Binary files /dev/null and b/textures/ad1.jpg differ diff --git a/textures/ad2.jpg b/textures/ad2.jpg new file mode 100644 index 00000000..4d37c354 Binary files /dev/null and b/textures/ad2.jpg differ diff --git a/textures/ad3.png b/textures/ad3.png new file mode 100644 index 00000000..05282404 Binary files /dev/null and b/textures/ad3.png differ 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