From b8bd2c66debab2b2f77117f72648c446bb5acaba Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Sun, 12 Feb 2017 23:26:05 -0500 Subject: [PATCH 01/21] + Initial commit --- .gitignore | 2 + README.md | 45 ---- deploy.js | 38 +++ index.html | 19 ++ package.json | 31 +++ src/framework.js | 72 ++++++ src/lsystem.js | 327 +++++++++++++++++++++++++ src/main.js | 137 +++++++++++ src/plants.js | 607 ++++++++++++++++++++++++++++++++++++++++++++++ src/turtle.js | 112 +++++++++ webpack.config.js | 28 +++ 11 files changed, 1373 insertions(+), 45 deletions(-) create mode 100644 .gitignore create mode 100644 deploy.js create mode 100644 index.html create mode 100644 package.json create mode 100644 src/framework.js create mode 100644 src/lsystem.js create mode 100644 src/main.js create mode 100644 src/plants.js create mode 100644 src/turtle.js create mode 100644 webpack.config.js 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..e69de29b 100644 --- a/README.md +++ b/README.md @@ -1,45 +0,0 @@ - -# 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. 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..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..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/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..b976d4a8 --- /dev/null +++ b/src/lsystem.js @@ -0,0 +1,327 @@ +var Random = require("random-js"); + +class LContext +{ + constructor() + { + this.branched = false; + } + + copy() + { + return new LContext(); + } +} + + +// An instruction is essentially a symbol with logic, context, stack and (TODO) parameters +class LInstruction +{ + symbol() { return "A"; } + evaluate(context, stack) { return context; } +} + +// Dummy instructions can be anything, they are used for replacement +// Generic instruction +class DummyInstruction extends LInstruction +{ + constructor(symbol) { super(); this.dummySymbol = symbol; } + + symbol() { return this.dummySymbol; } + + evaluate(context, stack) { + return null; + } +} + +// Generic instruction +class PushInstruction extends LInstruction +{ + symbol() { return "["; } + + evaluate(context, stack) { + stack.push(context); + return null; + } +} + +// Generic instruction +class PullInstruction extends LInstruction +{ + symbol() { return "]"; } + + evaluate(context, stack) { + var c = stack.pop(context); + c.branched = true; + return c; + } +} + +// A grammar chain is a doubly linked list of instructions +// that can be modified by given rules +class LInstructionChain +{ + constructor() + { + this.root = null; + this.last = null; + } + + push(value) + { + if(this.root == null) + { + this.root = { prev: null, next: null, value: value, new : false}; + this.last = this.root; + } + else if(this.last != null) + { + var node = { prev: this.last, next: null, value: value, new : true}; + this.last.next = node; + this.last = node; + } + + return this.last; + } + + // Evaluates a chain of instructions, both with a context and a stack + evaluate(initialState) + { + var contextStack = []; + var context = initialState; + var stateArray = [context.copy()]; + + this.evaluateInternal(function(node) { + var c = node.value.evaluate(context.copy(), contextStack); + + // Some instructions may not want to modify the context + if(c != null) + { + // Debug data :D + c.relatedInstruction = node.value; + stateArray.push(c); + + context = c; + } + }); + + return stateArray; + } + + evaluateInternal(evaluateFunc) + { + this.iterate(null, null, evaluateFunc); + } + + // General purpose iteration function + iterate(condition, returnFunc, evaluateFunc = null) + { + var node = this.root; + + while(node != null) { + + if(evaluateFunc != null) + evaluateFunc(node); + + if(returnFunc != null && condition != null && condition(node)) + return returnFunc(node); + + node = node.next; + } + + return null; + } + + toString() + { + var result = ""; + this.evaluateInternal(function(node) { result += node.value.symbol(); } ); + return result; + } + + findAll(value) + { + var nodes = []; + this.iterate(null, null, function(node) { if(node.value == value) nodes.push(node); }); + return nodes; + } + + find(value) + { + return this.iterate(function(node){return node.value == value;}, function(node) { return node } ); + } + + // Because we're expanding in-place, we must be careful not to + // expand recently added nodes that come from a previous replacement + // in the same expansion cycle. + expand(rules, random) + { + var node = this.root; + + while(node != null) + { + // Get next before replacement + var next = node.next; + + for(var pred in rules) + { + if (rules.hasOwnProperty(pred)) + { + var ruleArray = rules[pred]; + var replaced = false; + + var randomValue = random.real(0, 1, true); + + for(var r = 0; r < ruleArray.length && !replaced; r++) + { + if(node.value == ruleArray[r].predecessor) + { + if(ruleArray[r].probability >= 1.0 || ruleArray[r].probability > randomValue) + { + this.replace(node, ruleArray[r].successor); + replaced = true; + break; + } + else + { + randomValue -= ruleArray[r].probability; + } + } + } + } + } + + node = next; + } + } + + // Now it only replaces one symbol. TODO context aware rules + replaceSymbol(v, values) + { + this.replace(this.find(v), values); + } + + replace(node, values) + { + if(node == null) + return; + + var prevNode = node.prev; + this.last = prevNode; + + if(this.root == node) + this.root = this.last; + + for(var i = 0; i < values.length; i++) + this.push(values[i]); + + // Reconnect the chain, while ignoring the replaced node + if(this.last != null) + { + this.last.next = node.next; + + if(node.next != null) + node.next.prev = this.last; + + // Make sure we update the last node + while(this.last.next != null) + this.last = this.last.next; + } + } +} + +// Just an auxiliary container of strings +function LRule(predecessor, successor, probability) +{ + this.predecessor = predecessor; + this.successor = successor; + this.probability = probability; +} + +function LSystem(axiom, instructions, rules, iterations, random) +{ + this.registerInstruction = function(instruction) + { + this.instructionMap[instruction.symbol()] = instruction; + } + + this.getInstruction = function(symbol) + { + if(!(symbol in this.instructionMap)) + console.error("Symbol " + symbol + " not present in instruction map!"); + + return this.instructionMap[symbol]; + } + + this.parseAxiom = function(axiomSymbols) + { + this.chain = new LInstructionChain(); + + for(var i = 0; i < axiomSymbols.length; i++) + this.chain.push(this.getInstruction(axiomSymbols[i])); + } + + this.updateAxiom = function(axiom) + { + this.axiom = axiom; + this.parseAxiom(axiom); + } + + this.parseRule = function(predecessor, successorList, probability) + { + var predInstruction = this.getInstruction(predecessor); + var successorInstructions = []; + + for(var i = 0; i < successorList.length; i++) + successorInstructions.push(this.getInstruction(successorList[i])); + + if(!(predecessor in this.ruleMap)) + this.ruleMap[predecessor] = []; + + this.ruleMap[predecessor].push( { predecessor: predInstruction, successor: successorInstructions, probability: probability }); + } + + this.expand = function() + { + var t = performance.now(); + + // Reset the chain + this.updateAxiom(this.axiom); + + for(var i = 0; i < this.iterations; i++) + { + this.chain.expand(this.ruleMap, this.random); + } + + t = performance.now() - t; + + // console.log("Expansion took " + t.toFixed(1) + "ms"); + + return this.chain; + } + + this.evaluate = function(initialState) + { + return this.chain.evaluate(initialState); + } + + this.iterations = iterations; + this.instructionMap = {}; + this.ruleMap = {}; + this.chain = new LInstructionChain(); + this.random = random; + + // Register common instructions + this.registerInstruction(new PushInstruction()); + this.registerInstruction(new PullInstruction()); + + for(var i = 0; i < instructions.length; i++) + this.registerInstruction(instructions[i]); + + for(var r = 0; r < rules.length; r++) + this.parseRule(rules[r].predecessor, rules[r].successor, rules[r].probability); + + this.updateAxiom(axiom); +} + +export {LSystem, LContext, LRule, LInstruction, DummyInstruction} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..0b3aa9b1 --- /dev/null +++ b/src/main.js @@ -0,0 +1,137 @@ + +const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much +import Framework from './framework' +import LSystem from './lsystem.js' +import Turtle from './turtle.js' +import {PlantLSystem, MainCharacter, CactusCharacter, WillowCharacter} from './plants.js' + +var turtle; + +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(2, 3, 4); + camera.lookAt(new THREE.Vector3(0,2,0)); + + + var UserSettings = + { + iterations : 5, + willow : null, + main : null, + cactus : null, + rebuild : function() { RebuildTrees(scene, UserSettings) } + } + + // // initialize LSystem and a Turtle to draw + // var lsys = new Lsystem(); + // turtle = new Turtle(scene); + + gui.add(UserSettings, 'rebuild', 0, 180); + // gui.add(lsys, 'axiom').onChange(function(newVal) { + // lsys.UpdateAxiom(newVal); + // doLsystem(lsys, lsys.iterations, turtle); + // }); + + gui.add(UserSettings, 'iterations', 0, 8).step(1).onChange(function(newVal) { + // clearScene(turtle); + // doLsystem(lsys, newVal, turtle); + RebuildTrees(scene, UserSettings); + }); + + // var lSystem = new LSystem("FX", "", 10); + // lSystem.expand(); + + var lSystem = new MainCharacter(2234, 5); + var expandedChain = lSystem.expand(); + + var mesh = lSystem.generateMesh(); + mesh.scale.set(.3, .3, .3); + scene.add(mesh); + + var cactus = new CactusCharacter(6565, 6); + cactus.expand(); + var cactusMesh = cactus.generateMesh(); + cactusMesh.position.set(2, 0, 0); + cactusMesh.scale.set(.2, .2, .2); + scene.add(cactusMesh); + + var willow = new WillowCharacter(2135, 5); + willow.expand(); + var willowMesh = willow.generateMesh(); + willowMesh.position.set(-2, 0, 0); + willowMesh.scale.set(.2, .2, .2); + scene.add(willowMesh); + + UserSettings.willow = willowMesh; + UserSettings.main = mesh; + UserSettings.cactus = cactusMesh; +} + +function RebuildTrees(scene, UserSettings) +{ + scene.remove(UserSettings.willow); + scene.remove(UserSettings.main); + scene.remove(UserSettings.cactus); + + var lSystem = new MainCharacter(performance.now(), UserSettings.iterations); + lSystem.expand(); + var mesh = lSystem.generateMesh(); + mesh.scale.set(.3, .3, .3); + scene.add(mesh); + + var cactus = new CactusCharacter(performance.now(), UserSettings.iterations); + cactus.expand(); + var cactusMesh = cactus.generateMesh(); + cactusMesh.position.set(2, 0, 0); + cactusMesh.scale.set(.2, .2, .2); + scene.add(cactusMesh); + + var willow = new WillowCharacter(performance.now(), UserSettings.iterations); + willow.expand(); + var willowMesh = willow.generateMesh(); + willowMesh.position.set(-2, 0, 0); + willowMesh.scale.set(.2, .2, .2); + scene.add(willowMesh); + + + UserSettings.willow = willowMesh; + UserSettings.main = mesh; + UserSettings.cactus = cactusMesh; + +} + +// clears the scene by removing all geometries added by turtle.js +function clearScene(turtle) { + var obj; + for( var i = turtle.scene.children.length - 1; i > 3; i--) { + obj = turtle.scene.children[i]; + turtle.scene.remove(obj); + } +} + +function doLsystem(lsystem, iterations, turtle) { + var result = lsystem.DoIterations(iterations); + turtle.clear(); + turtle = new Turtle(turtle.scene); + turtle.renderSymbols(result); +} + +// called on frame updates +function onUpdate(framework) { +} + +// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate +Framework.init(onLoad, onUpdate); diff --git a/src/plants.js b/src/plants.js new file mode 100644 index 00000000..2eb66c20 --- /dev/null +++ b/src/plants.js @@ -0,0 +1,607 @@ +const THREE = require('three'); +var Random = require("random-js"); + +import { LSystem, LContext, LRule, LInstruction, DummyInstruction } from './lsystem.js' + +function toRadians(degrees) +{ + return degrees * Math.PI / 180.0; +} + +function randomTwistRotation(random, twist) +{ + var a = twist; + var euler = new THREE.Euler(0, a * random.real(.75, 1), 0); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + return quat; +} + +function randomQuaternion(random, amplitude) +{ + var a = amplitude * .5; + var euler = new THREE.Euler(a * random.real(-1,1, true), a * random.real(-1,1, true), a * random.real(-1,1, true)); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + return quat; +} + +class CrossSectionParameters +{ + constructor(a, b, m1, m2, n1, n2, n3) + { + this.a = a; + this.b = b; + this.m1 = m1; + this.m2 = m2; + this.n1 = n1; + this.n2 = n2; + this.n3 = n3; + } + + evaluate(t) + { + var term1 = Math.pow(Math.abs(Math.cos(this.m1 * t * .25) / this.a), this.n2); + var term2 = Math.pow(Math.abs(Math.sin(this.m2 * t * .25) / this.b), this.n3); + return Math.pow(term1 + term2, -1.0 / this.n1); + } + + copy() + { + return new CrossSectionParameters(this.a, this.b, this.m1, this.m2, this.n1, this.n2, this.n3); + } +} + +class PlantContext extends LContext +{ + constructor(position, rotation, branchLength, branchRadius, crossSection, random) + { + super(); + + this.position = position.clone(); + this.rotation = rotation.clone(); + this.branchLength = branchLength; + this.branchRadius = branchRadius; + this.random = random; + this.crossSection = crossSection; + this.renderable = false; + this.flower = false; + this.depth = 0; + } + + copy() + { + var c = new PlantContext(this.position, this.rotation, this.branchLength, this.branchRadius, this.crossSection.copy(), this.random); + c.depth = this.depth; + return c; + } +} + +class LInstructionOverride extends LInstruction +{ + constructor(symbolCharacter, instruction) + { + super(); + this.symbolCharacter = symbolCharacter; + this.instruction = instruction; + } + + symbol() + { + return this.symbolCharacter; + } + + evaluate(context, stack) + { + return this.instruction.evaluate(context, stack); + } +} + +// A more specific instruction that can modify branches +class BranchInstruction extends LInstruction +{ + constructor(sizeFactor, radiusFactor) + { + super(); + this.sizeFactor = sizeFactor; + this.radiusFactor = radiusFactor; + } + + symbol() { return "B"; } + + evaluate(context, stack) + { + var c = context; + c.branchLength *= this.sizeFactor; + c.branchRadius *= this.radiusFactor; + c.branched = true; + + // For now, when branching we lose all fine details + c.crossSection = new CrossSectionParameters(1,1,1,1,1,1,1); + return c; + } +} + +class RootInstruction extends LInstruction +{ + constructor(sizeFactor) + { + super(); + this.sizeFactor = sizeFactor; + } + + symbol() { return "R"; } + + evaluate(context, stack) + { + var c = context; + c.position.add(new THREE.Vector3(0, context.branchLength * .2, 0).applyQuaternion(c.rotation)); + c.renderable = true; + c.branchRadius *= this.sizeFactor; + return c; + } +} + +// Main branch +class ForwardInstruction extends LInstruction +{ + constructor(twistFactor) + { + super(); + this.twistFactor = twistFactor; + } + + symbol() { return "F"; } + + evaluate(context, stack) + { + var c = context; + c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); + c.branchRadius += c.random.real(-.2, .2, true) * c.branchRadius; + c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); + c.renderable = true; + c.depth++; + return c; + } +} + +class DetailInstruction extends LInstruction +{ + constructor(rotationFactor, minLength, maxLength, twistFactor) + { + super(); + this.rotationFactor = rotationFactor; + this.minLength = minLength; + this.maxLength = maxLength; + this.twistFactor = twistFactor; + } + + symbol() { return "Q"; } + + evaluate(context, stack) + { + var c = context; + c.position.add(new THREE.Vector3(0, c.random.real(this.minLength, this.maxLength) * c.branchLength, 0).applyQuaternion(c.rotation)); + c.renderable = true; + c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); + c.rotation.multiply(randomQuaternion(c.random, this.rotationFactor)); + c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; + c.depth++; + return c; + } +} + +class RotatePositiveInstruction extends LInstruction +{ + constructor(angle) + { + super(); + this.angle = angle; + } + + symbol() { return "+"; } + + evaluate(context, stack) { + var c = context; + + var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), this.angle * c.random.real(.5, 1)); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + + c.rotation.multiply(quat); + + // Jump to the boundary of the tree + c.position.add(new THREE.Vector3(0, context.branchRadius, 0).applyQuaternion(c.rotation)); + + return c; + } +} + +class RotateNegativeInstruction extends LInstruction +{ + symbol() { return "-"; } + + evaluate(context, stack) { + var c = context; + + var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), -1.25 * c.random.real(.5, 1)); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + + c.rotation.multiply(quat); + + // Jump to the boundary of the tree + c.position.add(new THREE.Vector3(0, context.branchRadius, 0).applyQuaternion(c.rotation)); + return c; + } +} + +class FlowerInstruction extends LInstruction +{ + symbol() { return "W"; } + + evaluate(context, stack) + { + context.flower = true; + return context; + } +} + +export default class PlantLSystem +{ + constructor() {} + + expand() + { + return this.system.expand(); + } + + evaluate() + { + // (a, b, m1, m2, n1, n2, n3) + var crossSection = new CrossSectionParameters(1,1,2,10,-1.5,1,1); + var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), 1.0, .95, crossSection, this.system.random); + return this.system.evaluate(state); + } + + generateCrossSectionVertices(geometry, state, subdivs, lastSectionOfBranch, nextState) + { + var centerPoint = state.position; + + for(var s = 0; s < subdivs; s++) + { + var theta = s * 2 * 3.1415 / subdivs; + var x = Math.cos(theta); + var y = Math.sin(theta); + + var r = state.crossSection.evaluate(theta) * state.branchRadius; + + if(lastSectionOfBranch) + r *= .5; + + var quat = state.rotation; + + if(state.branched && nextState != null) + quat = nextState.rotation; + + var point = centerPoint.clone().add(new THREE.Vector3(x * r, 0, y * r).applyQuaternion(quat)); + + geometry.vertices.push(point); + } + } + + generateMesh() + { + var plantContainer = new THREE.Group(); + + var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + material.side = THREE.DoubleSide; + + var stateArray = this.evaluate(); + + var t = performance.now(); + + var flowerGeometry = new THREE.SphereBufferGeometry(.1, 16, 16); + var geometry = new THREE.Geometry(); + var prevPosition = stateArray[0].position; + var subdivs = this.subdivisions; + var segments = 0; + var maxDepth = 0; + + // We need to find the max depth, so that we can normalize other stuff + for(var i = 0; i < stateArray.length; i++) + if(stateArray[i].depth > maxDepth) + maxDepth = stateArray[i].depth; + + // We always draw backwards, with consideration of branching and the first case + for(var i = 0; i < stateArray.length; i++) + { + var p = stateArray[i].position; + var offset = geometry.vertices.length - subdivs; + + var lastSectionOfBranch = (i == stateArray.length - 1) || stateArray[i+1].branched; + + // Note: if the grammar branched, we need to redraw the initial set of vertices + if(i == 0 || stateArray[i].renderable || stateArray[i].branched) + { + var nextState = i < stateArray.length - 1 ? stateArray[i+1] : null; + this.generateCrossSectionVertices(geometry, stateArray[i], subdivs, lastSectionOfBranch, nextState); + } + + if(stateArray[i].flower) + { + var sphere = new THREE.Mesh(flowerGeometry, material); + var scale = this.system.random.real(.15, 4); + sphere.position.copy(stateArray[i].position.add(new THREE.Vector3(0, -scale * .1, 0))); + + sphere.scale.set(scale, scale, scale); + plantContainer.add(sphere); + } + + if((prevPosition.distanceTo(p) > .01 && stateArray[i].renderable)) + { + // console.log(stateArray[i].position); + // console.log(stateArray[i-1].position); + // console.log('') + + if(offset >= 0) + { + for(var v = 0; v < subdivs; v++) + { + var v1 = offset + v; + var v2 = offset + ((v + 1) % subdivs); + var v3 = offset + subdivs + ((v + 1) % subdivs); + + var v4 = offset + v; + var v5 = offset + subdivs + ((v + 1) % subdivs); + var v6 = offset + subdivs + v; + + geometry.faces.push(new THREE.Face3(v3, v2, v1)); + geometry.faces.push(new THREE.Face3(v6, v5, v4)); + } + + segments++; + } + } + + prevPosition = p; + } + + // console.log(stateArray); + + geometry.mergeVertices(); + geometry.computeVertexNormals(); + + // console.log("Mesh generation took " + t.toFixed(1) + "ms (" + segments + " segments, " + subdivs + " subdivs, " + geometry.vertices.length + " vertices)"); + + var mesh = new THREE.Mesh(geometry, material); + + plantContainer.add(mesh); + return plantContainer; + } + + getLineDebugger() + { + var material = new THREE.LineBasicMaterial({ color: 0xffffff }); + material.transparent = true; + material.depthTest = false; + + var geometry = new THREE.Geometry(); + + var stateArray = this.evaluate(); + + var prevPosition = stateArray[0].position; + var subdivs = 8; + + for(var i = 0; i < stateArray.length; i++) + { + var p = stateArray[i].position; + + if(i == 0 || (prevPosition.distanceTo(p) > .01 && stateArray[i].renderable)) + { + this.generateCrossSectionVertices(geometry, stateArray[i], subdivs, null); + geometry.vertices.push(p); + } + + prevPosition = p; + } + + return new THREE.Line(geometry, material); + } +} + +export class MainCharacter extends PlantLSystem +{ + constructor(seed, iterations) + { + super(); + + var instructions = [new ForwardInstruction(toRadians(35)), + new DummyInstruction("X"), + new DummyInstruction("Y"), + new FlowerInstruction(), + new RotateNegativeInstruction(), + new RotatePositiveInstruction(toRadians(90)), + new BranchInstruction(.8, .6), + new DetailInstruction(toRadians(40), .3, .6, toRadians(5)), + new RootInstruction(.7), + new LInstructionOverride("E", new DetailInstruction(toRadians(10), .3, .2, toRadians(5)))]; + + var rules = []; + + // Two arms rule + rules.push(new LRule("X", "[B-QQQQQY][B+QQQQQY]", 1.0)); + + // Arms details (fingers?) + rules.push(new LRule("Y", "[B-QX][B+QX]QQ", .75)); + rules.push(new LRule("Y", "QW", .25)); + + // Detailing the main branch + rules.push(new LRule("F", "EEFXE", .85)); + rules.push(new LRule("F", "FXF", .15)); + + var random = new Random(Random.engines.mt19937().seed(seed)); + this.system = new LSystem("RRRRFXEEEEX", instructions, rules, iterations, random); + this.subdivisions = 32; + } +} + + +class CactusBranchInstruction extends LInstruction +{ + symbol() { return "C" }; + + evaluate(context, stack) + { + var c = context; + + var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), Math.PI * .5); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + + // Jump to the boundary of the tree + c.position.add(new THREE.Vector3(0, context.branchRadius , 0).applyQuaternion(quat)); + + c.branchRadius *= .5; + c.rotation.multiply(quat); + return c; + } +} + +class CactusForward extends LInstruction +{ + symbol() { return "F" }; + + evaluate(context, stack) + { + var c = context; + + c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); + c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; + // c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); + + var euler = new THREE.Euler(0, 0, -.35); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + + var dir = new THREE.Vector3(0,1,0).applyQuaternion(c.rotation) + + if(Math.abs(dir.y - 1.0) > .1) + c.rotation.multiply(quat); + + + c.renderable = true; + c.depth++; + return c; + } +} + +export class CactusCharacter extends PlantLSystem +{ + evaluate() + { + // (a, b, m1, m2, n1, n2, n3) + var crossSection = new CrossSectionParameters(1,1,18,18,2.225,1,10); + var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), .4, 1.25, crossSection, this.system.random); + return this.system.evaluate(state); + } + + constructor(seed, iterations) + { + super(); + + var instructions = [new CactusForward(), + new DummyInstruction("X"), + new DummyInstruction("Y"), + new CactusBranchInstruction(), + new FlowerInstruction(), + new RotateNegativeInstruction(), + new RotatePositiveInstruction(toRadians(90)), + new BranchInstruction(.8, .6), + new DetailInstruction(toRadians(0), .5, .5, toRadians(0)), + new RootInstruction(.8), + new LInstructionOverride("E", new CactusForward())]; + + var rules = []; + + rules.push(new LRule("F", "FF", .3)); // Grow Rule + // rules.push(new LRule("F", "E", .5)); + rules.push(new LRule("E", "EE", 1.0)); // Grow Rule + rules.push(new LRule("X", "[CE]", 1.0)); + + var random = new Random(Random.engines.mt19937().seed(seed)); + // this.system = new LSystem("RRFFRRR", instructions, rules, 5, random); + + this.system = new LSystem("RFXFXFXFFF", instructions, rules, iterations, random); + this.subdivisions = 128; + } +} + +class WillowInstruction extends LInstruction +{ + symbol() { return "T" }; + + + evaluate(context, stack) + { + var c = context; + + c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); + c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; + // c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); + + var euler = new THREE.Euler(0, 0, .35); + var quat = new THREE.Quaternion(); + quat.setFromEuler(euler); + + var dir = new THREE.Vector3(0,1,0).applyQuaternion(c.rotation) + + if(Math.abs(dir.y + 1.0) > .1) + c.rotation.multiply(quat); + + c.renderable = true; + c.depth++; + return c; + } +} + + + +export class WillowCharacter extends PlantLSystem +{ + evaluate() + { + // (a, b, m1, m2, n1, n2, n3) + var crossSection = new CrossSectionParameters(1,1,18,18,2.225,1,10); + var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), 1.0, 1.25, crossSection, this.system.random); + return this.system.evaluate(state); + } + + constructor(seed, iterations) + { + super(); + + var instructions = [new ForwardInstruction(toRadians(45)), + new DummyInstruction("X"), + new DummyInstruction("Y"), + new FlowerInstruction(), + new WillowInstruction(), + new RotateNegativeInstruction(), + new RotatePositiveInstruction(toRadians(90)), + new BranchInstruction(.8, .6), + new DetailInstruction(toRadians(15), .5, .5, toRadians(20)), + new RootInstruction(.8)]; + + var rules = []; + + rules.push(new LRule("Q", "QQ", 1.0)); // Grow Rule + rules.push(new LRule("T", "TT", 1.0)); // Grow Rule + + rules.push(new LRule("X", "[B+QT][B-QT]X", 1.0)); // Grow Rule + + var random = new Random(Random.engines.mt19937().seed(seed)); + + this.system = new LSystem("RRQX", instructions, rules, iterations, random); + this.subdivisions = 64; + } +} diff --git a/src/turtle.js b/src/turtle.js new file mode 100644 index 00000000..1db2723d --- /dev/null +++ b/src/turtle.js @@ -0,0 +1,112 @@ +const THREE = require('three') + +// 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; + + // 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, 30, 0, 0), + '-' : this.rotateTurtle.bind(this, -30, 0, 0), + 'F' : this.makeCylinder.bind(this, 2, 0.1) + }; + } else { + this.renderGrammar = grammar; + } + } + + // 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: 0x00cccc} ); + 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); + }; + + // 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); + } + } +} \ 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 From 118760203eea5dd4540c78cec5e9e19000921f8c Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Wed, 15 Feb 2017 04:09:27 -0500 Subject: [PATCH 02/21] + Mass modeling extrusion based on profile and lot boundary --- src/building.js | 151 ++++++++++++ src/lsystem.js | 327 -------------------------- src/main.js | 136 +++-------- src/plants.js | 607 ------------------------------------------------ src/turtle.js | 112 --------- 5 files changed, 184 insertions(+), 1149 deletions(-) create mode 100644 src/building.js delete mode 100644 src/lsystem.js delete mode 100644 src/plants.js delete mode 100644 src/turtle.js diff --git a/src/building.js b/src/building.js new file mode 100644 index 00000000..d7c31777 --- /dev/null +++ b/src/building.js @@ -0,0 +1,151 @@ +const THREE = require('three'); + +class Shape +{ + constructor(size) + { + this.children = []; + this.active = true; + this.size = size; + this.hasComponents = false; + } +} + +// The object that defines the boundaries of the mass shape +class BuildingLot +{ + constructor() + { + this.points = []; + this.normals = []; + } + + addPoint(x, y) + { + this.points.push(new THREE.Vector2(x, y)); + } + + buildNormals() + { + var l = this.points.length; + + for(var i = 0; i < l; i++) + { + var prev = ((i == 0) ? l - 1 : i - 1); + var next = (i+1) % l; + + var p = this.points[i]; + var prevP = this.points[prev]; + var nextP = this.points[next]; + + var t1 = prevP.clone().sub(p).normalize(); + var t2 = p.clone().sub(nextP).normalize(); + + var n1 = new THREE.Vector2(-t1.y, t1.x); + var n2 = new THREE.Vector2(-t2.y, t2.x); + + var n = n1.add(n2).multiplyScalar(.5); + this.normals[i] = n.normalize(); + } + } +} + +// The profile of an extrusion +class Profile +{ + constructor() + { + this.points = []; + } + + addPoint(x, y) + { + this.points.push(new THREE.Vector2(x, y)); + } +} + +class MassShape +{ + constructor() + { + } + + generateMesh(lot, profile) + { + lot.buildNormals(); + + var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + var geometry = new THREE.Geometry(); + + var boundaryVertexCount = lot.points.length; + var offset = 0; + + for(var i = 0; i < profile.points.length; i++) + { + var profilePoint = profile.points[i]; + var profileDiff = (i == 0) ? new THREE.Vector2(0,0) : (profilePoint.clone().sub(profile.points[i-1])); + + for(var j = 0; j < boundaryVertexCount; j++) + { + var boundaryPoint = lot.points[j]; + var boundaryNormal = lot.normals[j]; + + var vertex = new THREE.Vector3(boundaryPoint.x, profilePoint.y, boundaryPoint.y); + + var displ = boundaryNormal.clone().multiplyScalar(profilePoint.x - 1.0); + vertex.add(new THREE.Vector3(displ.x, 0.0, displ.y)); + + geometry.vertices.push(vertex); + } + + // Ignore first row of vertices + if(i > 0) + { + for(var v = 0; v < boundaryVertexCount; v++) + { + var v1 = offset + v; + var v2 = offset + ((v + 1) % boundaryVertexCount); + var v3 = offset + boundaryVertexCount + ((v + 1) % boundaryVertexCount); + + var v4 = offset + v; + var v5 = offset + boundaryVertexCount + ((v + 1) % boundaryVertexCount); + var v6 = offset + boundaryVertexCount + v; + + geometry.faces.push(new THREE.Face3(v3, v2, v1)); + geometry.faces.push(new THREE.Face3(v6, v5, v4)); + + } + + offset += boundaryVertexCount; + } + } + + // Implement cap here! + + geometry.mergeVertices(); + geometry.computeFlatVertexNormals(); + + var mesh = new THREE.Mesh(geometry, material); + return mesh; + } +} + +class Rule +{ + evaluate(shape) + { + + } +} + +class ShapeBuilder +{ + constructor() + { + this.shapes = []; + this.iterations = 5; + } + +} + +export {Shape, Rule, BuildingLot, Profile, MassShape} \ No newline at end of file diff --git a/src/lsystem.js b/src/lsystem.js deleted file mode 100644 index b976d4a8..00000000 --- a/src/lsystem.js +++ /dev/null @@ -1,327 +0,0 @@ -var Random = require("random-js"); - -class LContext -{ - constructor() - { - this.branched = false; - } - - copy() - { - return new LContext(); - } -} - - -// An instruction is essentially a symbol with logic, context, stack and (TODO) parameters -class LInstruction -{ - symbol() { return "A"; } - evaluate(context, stack) { return context; } -} - -// Dummy instructions can be anything, they are used for replacement -// Generic instruction -class DummyInstruction extends LInstruction -{ - constructor(symbol) { super(); this.dummySymbol = symbol; } - - symbol() { return this.dummySymbol; } - - evaluate(context, stack) { - return null; - } -} - -// Generic instruction -class PushInstruction extends LInstruction -{ - symbol() { return "["; } - - evaluate(context, stack) { - stack.push(context); - return null; - } -} - -// Generic instruction -class PullInstruction extends LInstruction -{ - symbol() { return "]"; } - - evaluate(context, stack) { - var c = stack.pop(context); - c.branched = true; - return c; - } -} - -// A grammar chain is a doubly linked list of instructions -// that can be modified by given rules -class LInstructionChain -{ - constructor() - { - this.root = null; - this.last = null; - } - - push(value) - { - if(this.root == null) - { - this.root = { prev: null, next: null, value: value, new : false}; - this.last = this.root; - } - else if(this.last != null) - { - var node = { prev: this.last, next: null, value: value, new : true}; - this.last.next = node; - this.last = node; - } - - return this.last; - } - - // Evaluates a chain of instructions, both with a context and a stack - evaluate(initialState) - { - var contextStack = []; - var context = initialState; - var stateArray = [context.copy()]; - - this.evaluateInternal(function(node) { - var c = node.value.evaluate(context.copy(), contextStack); - - // Some instructions may not want to modify the context - if(c != null) - { - // Debug data :D - c.relatedInstruction = node.value; - stateArray.push(c); - - context = c; - } - }); - - return stateArray; - } - - evaluateInternal(evaluateFunc) - { - this.iterate(null, null, evaluateFunc); - } - - // General purpose iteration function - iterate(condition, returnFunc, evaluateFunc = null) - { - var node = this.root; - - while(node != null) { - - if(evaluateFunc != null) - evaluateFunc(node); - - if(returnFunc != null && condition != null && condition(node)) - return returnFunc(node); - - node = node.next; - } - - return null; - } - - toString() - { - var result = ""; - this.evaluateInternal(function(node) { result += node.value.symbol(); } ); - return result; - } - - findAll(value) - { - var nodes = []; - this.iterate(null, null, function(node) { if(node.value == value) nodes.push(node); }); - return nodes; - } - - find(value) - { - return this.iterate(function(node){return node.value == value;}, function(node) { return node } ); - } - - // Because we're expanding in-place, we must be careful not to - // expand recently added nodes that come from a previous replacement - // in the same expansion cycle. - expand(rules, random) - { - var node = this.root; - - while(node != null) - { - // Get next before replacement - var next = node.next; - - for(var pred in rules) - { - if (rules.hasOwnProperty(pred)) - { - var ruleArray = rules[pred]; - var replaced = false; - - var randomValue = random.real(0, 1, true); - - for(var r = 0; r < ruleArray.length && !replaced; r++) - { - if(node.value == ruleArray[r].predecessor) - { - if(ruleArray[r].probability >= 1.0 || ruleArray[r].probability > randomValue) - { - this.replace(node, ruleArray[r].successor); - replaced = true; - break; - } - else - { - randomValue -= ruleArray[r].probability; - } - } - } - } - } - - node = next; - } - } - - // Now it only replaces one symbol. TODO context aware rules - replaceSymbol(v, values) - { - this.replace(this.find(v), values); - } - - replace(node, values) - { - if(node == null) - return; - - var prevNode = node.prev; - this.last = prevNode; - - if(this.root == node) - this.root = this.last; - - for(var i = 0; i < values.length; i++) - this.push(values[i]); - - // Reconnect the chain, while ignoring the replaced node - if(this.last != null) - { - this.last.next = node.next; - - if(node.next != null) - node.next.prev = this.last; - - // Make sure we update the last node - while(this.last.next != null) - this.last = this.last.next; - } - } -} - -// Just an auxiliary container of strings -function LRule(predecessor, successor, probability) -{ - this.predecessor = predecessor; - this.successor = successor; - this.probability = probability; -} - -function LSystem(axiom, instructions, rules, iterations, random) -{ - this.registerInstruction = function(instruction) - { - this.instructionMap[instruction.symbol()] = instruction; - } - - this.getInstruction = function(symbol) - { - if(!(symbol in this.instructionMap)) - console.error("Symbol " + symbol + " not present in instruction map!"); - - return this.instructionMap[symbol]; - } - - this.parseAxiom = function(axiomSymbols) - { - this.chain = new LInstructionChain(); - - for(var i = 0; i < axiomSymbols.length; i++) - this.chain.push(this.getInstruction(axiomSymbols[i])); - } - - this.updateAxiom = function(axiom) - { - this.axiom = axiom; - this.parseAxiom(axiom); - } - - this.parseRule = function(predecessor, successorList, probability) - { - var predInstruction = this.getInstruction(predecessor); - var successorInstructions = []; - - for(var i = 0; i < successorList.length; i++) - successorInstructions.push(this.getInstruction(successorList[i])); - - if(!(predecessor in this.ruleMap)) - this.ruleMap[predecessor] = []; - - this.ruleMap[predecessor].push( { predecessor: predInstruction, successor: successorInstructions, probability: probability }); - } - - this.expand = function() - { - var t = performance.now(); - - // Reset the chain - this.updateAxiom(this.axiom); - - for(var i = 0; i < this.iterations; i++) - { - this.chain.expand(this.ruleMap, this.random); - } - - t = performance.now() - t; - - // console.log("Expansion took " + t.toFixed(1) + "ms"); - - return this.chain; - } - - this.evaluate = function(initialState) - { - return this.chain.evaluate(initialState); - } - - this.iterations = iterations; - this.instructionMap = {}; - this.ruleMap = {}; - this.chain = new LInstructionChain(); - this.random = random; - - // Register common instructions - this.registerInstruction(new PushInstruction()); - this.registerInstruction(new PullInstruction()); - - for(var i = 0; i < instructions.length; i++) - this.registerInstruction(instructions[i]); - - for(var r = 0; r < rules.length; r++) - this.parseRule(rules[r].predecessor, rules[r].successor, rules[r].probability); - - this.updateAxiom(axiom); -} - -export {LSystem, LContext, LRule, LInstruction, DummyInstruction} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 0b3aa9b1..23be0b86 100644 --- a/src/main.js +++ b/src/main.js @@ -1,13 +1,15 @@ - -const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much +const THREE = require('three'); import Framework from './framework' -import LSystem from './lsystem.js' -import Turtle from './turtle.js' -import {PlantLSystem, MainCharacter, CactusCharacter, WillowCharacter} from './plants.js' -var turtle; +import * as Building from './building.js' + +var UserSettings = +{ + iterations : 5 +} -function onLoad(framework) { +function onLoad(framework) +{ var scene = framework.scene; var camera = framework.camera; var renderer = framework.renderer; @@ -25,108 +27,36 @@ function onLoad(framework) { camera.position.set(2, 3, 4); camera.lookAt(new THREE.Vector3(0,2,0)); + var profile = new Building.Profile(); + profile.addPoint(1.0, 0.0); + profile.addPoint(1.0, 1.0); - var UserSettings = - { - iterations : 5, - willow : null, - main : null, - cactus : null, - rebuild : function() { RebuildTrees(scene, UserSettings) } - } - - // // initialize LSystem and a Turtle to draw - // var lsys = new Lsystem(); - // turtle = new Turtle(scene); - - gui.add(UserSettings, 'rebuild', 0, 180); - // gui.add(lsys, 'axiom').onChange(function(newVal) { - // lsys.UpdateAxiom(newVal); - // doLsystem(lsys, lsys.iterations, turtle); - // }); - - gui.add(UserSettings, 'iterations', 0, 8).step(1).onChange(function(newVal) { - // clearScene(turtle); - // doLsystem(lsys, newVal, turtle); - RebuildTrees(scene, UserSettings); - }); - - // var lSystem = new LSystem("FX", "", 10); - // lSystem.expand(); - - var lSystem = new MainCharacter(2234, 5); - var expandedChain = lSystem.expand(); - - var mesh = lSystem.generateMesh(); - mesh.scale.set(.3, .3, .3); - scene.add(mesh); - - var cactus = new CactusCharacter(6565, 6); - cactus.expand(); - var cactusMesh = cactus.generateMesh(); - cactusMesh.position.set(2, 0, 0); - cactusMesh.scale.set(.2, .2, .2); - scene.add(cactusMesh); + profile.addPoint(.9, 1.0); + profile.addPoint(.9, 1.1); + profile.addPoint(.8, 1.1); + profile.addPoint(.8, 1.0); - var willow = new WillowCharacter(2135, 5); - willow.expand(); - var willowMesh = willow.generateMesh(); - willowMesh.position.set(-2, 0, 0); - willowMesh.scale.set(.2, .2, .2); - scene.add(willowMesh); + profile.addPoint(0.5, 1.0); + profile.addPoint(0.5, 2.0); + profile.addPoint(0.0, 2.0); - UserSettings.willow = willowMesh; - UserSettings.main = mesh; - UserSettings.cactus = cactusMesh; -} + var lot = new Building.BuildingLot(); + var subdivs = 15; + for(var i = 0; i < subdivs; i++) + { + var a = i * Math.PI * 2 / subdivs; + lot.addPoint(Math.cos(a), Math.sin(a)); + } + // lot.addPoint(1.0, 1.0); + // lot.addPoint(1.0, -1.0); + // lot.addPoint(-1.0, -1.0); + // lot.addPoint(-1.0, 1.0); + // lot.addPoint(.5, 1.5); -function RebuildTrees(scene, UserSettings) -{ - scene.remove(UserSettings.willow); - scene.remove(UserSettings.main); - scene.remove(UserSettings.cactus); + var shape = new Building.MassShape(); + var mesh = shape.generateMesh(lot, profile); - var lSystem = new MainCharacter(performance.now(), UserSettings.iterations); - lSystem.expand(); - var mesh = lSystem.generateMesh(); - mesh.scale.set(.3, .3, .3); scene.add(mesh); - - var cactus = new CactusCharacter(performance.now(), UserSettings.iterations); - cactus.expand(); - var cactusMesh = cactus.generateMesh(); - cactusMesh.position.set(2, 0, 0); - cactusMesh.scale.set(.2, .2, .2); - scene.add(cactusMesh); - - var willow = new WillowCharacter(performance.now(), UserSettings.iterations); - willow.expand(); - var willowMesh = willow.generateMesh(); - willowMesh.position.set(-2, 0, 0); - willowMesh.scale.set(.2, .2, .2); - scene.add(willowMesh); - - - UserSettings.willow = willowMesh; - UserSettings.main = mesh; - UserSettings.cactus = cactusMesh; - -} - -// clears the scene by removing all geometries added by turtle.js -function clearScene(turtle) { - var obj; - for( var i = turtle.scene.children.length - 1; i > 3; i--) { - obj = turtle.scene.children[i]; - turtle.scene.remove(obj); - } -} - -function doLsystem(lsystem, iterations, turtle) { - var result = lsystem.DoIterations(iterations); - turtle.clear(); - turtle = new Turtle(turtle.scene); - turtle.renderSymbols(result); } // called on frame updates diff --git a/src/plants.js b/src/plants.js deleted file mode 100644 index 2eb66c20..00000000 --- a/src/plants.js +++ /dev/null @@ -1,607 +0,0 @@ -const THREE = require('three'); -var Random = require("random-js"); - -import { LSystem, LContext, LRule, LInstruction, DummyInstruction } from './lsystem.js' - -function toRadians(degrees) -{ - return degrees * Math.PI / 180.0; -} - -function randomTwistRotation(random, twist) -{ - var a = twist; - var euler = new THREE.Euler(0, a * random.real(.75, 1), 0); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - return quat; -} - -function randomQuaternion(random, amplitude) -{ - var a = amplitude * .5; - var euler = new THREE.Euler(a * random.real(-1,1, true), a * random.real(-1,1, true), a * random.real(-1,1, true)); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - return quat; -} - -class CrossSectionParameters -{ - constructor(a, b, m1, m2, n1, n2, n3) - { - this.a = a; - this.b = b; - this.m1 = m1; - this.m2 = m2; - this.n1 = n1; - this.n2 = n2; - this.n3 = n3; - } - - evaluate(t) - { - var term1 = Math.pow(Math.abs(Math.cos(this.m1 * t * .25) / this.a), this.n2); - var term2 = Math.pow(Math.abs(Math.sin(this.m2 * t * .25) / this.b), this.n3); - return Math.pow(term1 + term2, -1.0 / this.n1); - } - - copy() - { - return new CrossSectionParameters(this.a, this.b, this.m1, this.m2, this.n1, this.n2, this.n3); - } -} - -class PlantContext extends LContext -{ - constructor(position, rotation, branchLength, branchRadius, crossSection, random) - { - super(); - - this.position = position.clone(); - this.rotation = rotation.clone(); - this.branchLength = branchLength; - this.branchRadius = branchRadius; - this.random = random; - this.crossSection = crossSection; - this.renderable = false; - this.flower = false; - this.depth = 0; - } - - copy() - { - var c = new PlantContext(this.position, this.rotation, this.branchLength, this.branchRadius, this.crossSection.copy(), this.random); - c.depth = this.depth; - return c; - } -} - -class LInstructionOverride extends LInstruction -{ - constructor(symbolCharacter, instruction) - { - super(); - this.symbolCharacter = symbolCharacter; - this.instruction = instruction; - } - - symbol() - { - return this.symbolCharacter; - } - - evaluate(context, stack) - { - return this.instruction.evaluate(context, stack); - } -} - -// A more specific instruction that can modify branches -class BranchInstruction extends LInstruction -{ - constructor(sizeFactor, radiusFactor) - { - super(); - this.sizeFactor = sizeFactor; - this.radiusFactor = radiusFactor; - } - - symbol() { return "B"; } - - evaluate(context, stack) - { - var c = context; - c.branchLength *= this.sizeFactor; - c.branchRadius *= this.radiusFactor; - c.branched = true; - - // For now, when branching we lose all fine details - c.crossSection = new CrossSectionParameters(1,1,1,1,1,1,1); - return c; - } -} - -class RootInstruction extends LInstruction -{ - constructor(sizeFactor) - { - super(); - this.sizeFactor = sizeFactor; - } - - symbol() { return "R"; } - - evaluate(context, stack) - { - var c = context; - c.position.add(new THREE.Vector3(0, context.branchLength * .2, 0).applyQuaternion(c.rotation)); - c.renderable = true; - c.branchRadius *= this.sizeFactor; - return c; - } -} - -// Main branch -class ForwardInstruction extends LInstruction -{ - constructor(twistFactor) - { - super(); - this.twistFactor = twistFactor; - } - - symbol() { return "F"; } - - evaluate(context, stack) - { - var c = context; - c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); - c.branchRadius += c.random.real(-.2, .2, true) * c.branchRadius; - c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); - c.renderable = true; - c.depth++; - return c; - } -} - -class DetailInstruction extends LInstruction -{ - constructor(rotationFactor, minLength, maxLength, twistFactor) - { - super(); - this.rotationFactor = rotationFactor; - this.minLength = minLength; - this.maxLength = maxLength; - this.twistFactor = twistFactor; - } - - symbol() { return "Q"; } - - evaluate(context, stack) - { - var c = context; - c.position.add(new THREE.Vector3(0, c.random.real(this.minLength, this.maxLength) * c.branchLength, 0).applyQuaternion(c.rotation)); - c.renderable = true; - c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); - c.rotation.multiply(randomQuaternion(c.random, this.rotationFactor)); - c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; - c.depth++; - return c; - } -} - -class RotatePositiveInstruction extends LInstruction -{ - constructor(angle) - { - super(); - this.angle = angle; - } - - symbol() { return "+"; } - - evaluate(context, stack) { - var c = context; - - var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), this.angle * c.random.real(.5, 1)); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - - c.rotation.multiply(quat); - - // Jump to the boundary of the tree - c.position.add(new THREE.Vector3(0, context.branchRadius, 0).applyQuaternion(c.rotation)); - - return c; - } -} - -class RotateNegativeInstruction extends LInstruction -{ - symbol() { return "-"; } - - evaluate(context, stack) { - var c = context; - - var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), -1.25 * c.random.real(.5, 1)); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - - c.rotation.multiply(quat); - - // Jump to the boundary of the tree - c.position.add(new THREE.Vector3(0, context.branchRadius, 0).applyQuaternion(c.rotation)); - return c; - } -} - -class FlowerInstruction extends LInstruction -{ - symbol() { return "W"; } - - evaluate(context, stack) - { - context.flower = true; - return context; - } -} - -export default class PlantLSystem -{ - constructor() {} - - expand() - { - return this.system.expand(); - } - - evaluate() - { - // (a, b, m1, m2, n1, n2, n3) - var crossSection = new CrossSectionParameters(1,1,2,10,-1.5,1,1); - var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), 1.0, .95, crossSection, this.system.random); - return this.system.evaluate(state); - } - - generateCrossSectionVertices(geometry, state, subdivs, lastSectionOfBranch, nextState) - { - var centerPoint = state.position; - - for(var s = 0; s < subdivs; s++) - { - var theta = s * 2 * 3.1415 / subdivs; - var x = Math.cos(theta); - var y = Math.sin(theta); - - var r = state.crossSection.evaluate(theta) * state.branchRadius; - - if(lastSectionOfBranch) - r *= .5; - - var quat = state.rotation; - - if(state.branched && nextState != null) - quat = nextState.rotation; - - var point = centerPoint.clone().add(new THREE.Vector3(x * r, 0, y * r).applyQuaternion(quat)); - - geometry.vertices.push(point); - } - } - - generateMesh() - { - var plantContainer = new THREE.Group(); - - var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); - material.side = THREE.DoubleSide; - - var stateArray = this.evaluate(); - - var t = performance.now(); - - var flowerGeometry = new THREE.SphereBufferGeometry(.1, 16, 16); - var geometry = new THREE.Geometry(); - var prevPosition = stateArray[0].position; - var subdivs = this.subdivisions; - var segments = 0; - var maxDepth = 0; - - // We need to find the max depth, so that we can normalize other stuff - for(var i = 0; i < stateArray.length; i++) - if(stateArray[i].depth > maxDepth) - maxDepth = stateArray[i].depth; - - // We always draw backwards, with consideration of branching and the first case - for(var i = 0; i < stateArray.length; i++) - { - var p = stateArray[i].position; - var offset = geometry.vertices.length - subdivs; - - var lastSectionOfBranch = (i == stateArray.length - 1) || stateArray[i+1].branched; - - // Note: if the grammar branched, we need to redraw the initial set of vertices - if(i == 0 || stateArray[i].renderable || stateArray[i].branched) - { - var nextState = i < stateArray.length - 1 ? stateArray[i+1] : null; - this.generateCrossSectionVertices(geometry, stateArray[i], subdivs, lastSectionOfBranch, nextState); - } - - if(stateArray[i].flower) - { - var sphere = new THREE.Mesh(flowerGeometry, material); - var scale = this.system.random.real(.15, 4); - sphere.position.copy(stateArray[i].position.add(new THREE.Vector3(0, -scale * .1, 0))); - - sphere.scale.set(scale, scale, scale); - plantContainer.add(sphere); - } - - if((prevPosition.distanceTo(p) > .01 && stateArray[i].renderable)) - { - // console.log(stateArray[i].position); - // console.log(stateArray[i-1].position); - // console.log('') - - if(offset >= 0) - { - for(var v = 0; v < subdivs; v++) - { - var v1 = offset + v; - var v2 = offset + ((v + 1) % subdivs); - var v3 = offset + subdivs + ((v + 1) % subdivs); - - var v4 = offset + v; - var v5 = offset + subdivs + ((v + 1) % subdivs); - var v6 = offset + subdivs + v; - - geometry.faces.push(new THREE.Face3(v3, v2, v1)); - geometry.faces.push(new THREE.Face3(v6, v5, v4)); - } - - segments++; - } - } - - prevPosition = p; - } - - // console.log(stateArray); - - geometry.mergeVertices(); - geometry.computeVertexNormals(); - - // console.log("Mesh generation took " + t.toFixed(1) + "ms (" + segments + " segments, " + subdivs + " subdivs, " + geometry.vertices.length + " vertices)"); - - var mesh = new THREE.Mesh(geometry, material); - - plantContainer.add(mesh); - return plantContainer; - } - - getLineDebugger() - { - var material = new THREE.LineBasicMaterial({ color: 0xffffff }); - material.transparent = true; - material.depthTest = false; - - var geometry = new THREE.Geometry(); - - var stateArray = this.evaluate(); - - var prevPosition = stateArray[0].position; - var subdivs = 8; - - for(var i = 0; i < stateArray.length; i++) - { - var p = stateArray[i].position; - - if(i == 0 || (prevPosition.distanceTo(p) > .01 && stateArray[i].renderable)) - { - this.generateCrossSectionVertices(geometry, stateArray[i], subdivs, null); - geometry.vertices.push(p); - } - - prevPosition = p; - } - - return new THREE.Line(geometry, material); - } -} - -export class MainCharacter extends PlantLSystem -{ - constructor(seed, iterations) - { - super(); - - var instructions = [new ForwardInstruction(toRadians(35)), - new DummyInstruction("X"), - new DummyInstruction("Y"), - new FlowerInstruction(), - new RotateNegativeInstruction(), - new RotatePositiveInstruction(toRadians(90)), - new BranchInstruction(.8, .6), - new DetailInstruction(toRadians(40), .3, .6, toRadians(5)), - new RootInstruction(.7), - new LInstructionOverride("E", new DetailInstruction(toRadians(10), .3, .2, toRadians(5)))]; - - var rules = []; - - // Two arms rule - rules.push(new LRule("X", "[B-QQQQQY][B+QQQQQY]", 1.0)); - - // Arms details (fingers?) - rules.push(new LRule("Y", "[B-QX][B+QX]QQ", .75)); - rules.push(new LRule("Y", "QW", .25)); - - // Detailing the main branch - rules.push(new LRule("F", "EEFXE", .85)); - rules.push(new LRule("F", "FXF", .15)); - - var random = new Random(Random.engines.mt19937().seed(seed)); - this.system = new LSystem("RRRRFXEEEEX", instructions, rules, iterations, random); - this.subdivisions = 32; - } -} - - -class CactusBranchInstruction extends LInstruction -{ - symbol() { return "C" }; - - evaluate(context, stack) - { - var c = context; - - var euler = new THREE.Euler(0, c.random.real(-Math.PI, Math.PI), Math.PI * .5); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - - // Jump to the boundary of the tree - c.position.add(new THREE.Vector3(0, context.branchRadius , 0).applyQuaternion(quat)); - - c.branchRadius *= .5; - c.rotation.multiply(quat); - return c; - } -} - -class CactusForward extends LInstruction -{ - symbol() { return "F" }; - - evaluate(context, stack) - { - var c = context; - - c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); - c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; - // c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); - - var euler = new THREE.Euler(0, 0, -.35); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - - var dir = new THREE.Vector3(0,1,0).applyQuaternion(c.rotation) - - if(Math.abs(dir.y - 1.0) > .1) - c.rotation.multiply(quat); - - - c.renderable = true; - c.depth++; - return c; - } -} - -export class CactusCharacter extends PlantLSystem -{ - evaluate() - { - // (a, b, m1, m2, n1, n2, n3) - var crossSection = new CrossSectionParameters(1,1,18,18,2.225,1,10); - var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), .4, 1.25, crossSection, this.system.random); - return this.system.evaluate(state); - } - - constructor(seed, iterations) - { - super(); - - var instructions = [new CactusForward(), - new DummyInstruction("X"), - new DummyInstruction("Y"), - new CactusBranchInstruction(), - new FlowerInstruction(), - new RotateNegativeInstruction(), - new RotatePositiveInstruction(toRadians(90)), - new BranchInstruction(.8, .6), - new DetailInstruction(toRadians(0), .5, .5, toRadians(0)), - new RootInstruction(.8), - new LInstructionOverride("E", new CactusForward())]; - - var rules = []; - - rules.push(new LRule("F", "FF", .3)); // Grow Rule - // rules.push(new LRule("F", "E", .5)); - rules.push(new LRule("E", "EE", 1.0)); // Grow Rule - rules.push(new LRule("X", "[CE]", 1.0)); - - var random = new Random(Random.engines.mt19937().seed(seed)); - // this.system = new LSystem("RRFFRRR", instructions, rules, 5, random); - - this.system = new LSystem("RFXFXFXFFF", instructions, rules, iterations, random); - this.subdivisions = 128; - } -} - -class WillowInstruction extends LInstruction -{ - symbol() { return "T" }; - - - evaluate(context, stack) - { - var c = context; - - c.position.add(new THREE.Vector3(0, context.branchLength, 0).applyQuaternion(c.rotation)); - c.branchRadius += c.random.real(-.1, .1, true) * c.branchRadius; - // c.rotation.multiply(randomTwistRotation(c.random, this.twistFactor)); - - var euler = new THREE.Euler(0, 0, .35); - var quat = new THREE.Quaternion(); - quat.setFromEuler(euler); - - var dir = new THREE.Vector3(0,1,0).applyQuaternion(c.rotation) - - if(Math.abs(dir.y + 1.0) > .1) - c.rotation.multiply(quat); - - c.renderable = true; - c.depth++; - return c; - } -} - - - -export class WillowCharacter extends PlantLSystem -{ - evaluate() - { - // (a, b, m1, m2, n1, n2, n3) - var crossSection = new CrossSectionParameters(1,1,18,18,2.225,1,10); - var state = new PlantContext(new THREE.Vector3(0,0,0), new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0)), 1.0, 1.25, crossSection, this.system.random); - return this.system.evaluate(state); - } - - constructor(seed, iterations) - { - super(); - - var instructions = [new ForwardInstruction(toRadians(45)), - new DummyInstruction("X"), - new DummyInstruction("Y"), - new FlowerInstruction(), - new WillowInstruction(), - new RotateNegativeInstruction(), - new RotatePositiveInstruction(toRadians(90)), - new BranchInstruction(.8, .6), - new DetailInstruction(toRadians(15), .5, .5, toRadians(20)), - new RootInstruction(.8)]; - - var rules = []; - - rules.push(new LRule("Q", "QQ", 1.0)); // Grow Rule - rules.push(new LRule("T", "TT", 1.0)); // Grow Rule - - rules.push(new LRule("X", "[B+QT][B-QT]X", 1.0)); // Grow Rule - - var random = new Random(Random.engines.mt19937().seed(seed)); - - this.system = new LSystem("RRQX", instructions, rules, iterations, random); - this.subdivisions = 64; - } -} diff --git a/src/turtle.js b/src/turtle.js deleted file mode 100644 index 1db2723d..00000000 --- a/src/turtle.js +++ /dev/null @@ -1,112 +0,0 @@ -const THREE = require('three') - -// 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; - - // 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, 30, 0, 0), - '-' : this.rotateTurtle.bind(this, -30, 0, 0), - 'F' : this.makeCylinder.bind(this, 2, 0.1) - }; - } else { - this.renderGrammar = grammar; - } - } - - // 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: 0x00cccc} ); - 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); - }; - - // 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); - } - } -} \ No newline at end of file From a496fce4b0bb3516210548d2d3dffba81c45325f Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Wed, 15 Feb 2017 18:35:55 -0500 Subject: [PATCH 03/21] + Rubik core logic --- src/main.js | 25 ++++++--- src/rubik.js | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 src/rubik.js diff --git a/src/main.js b/src/main.js index 23be0b86..eba8b09e 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ const THREE = require('three'); import Framework from './framework' import * as Building from './building.js' +import * as Rubik from './rubik.js' var UserSettings = { @@ -23,6 +24,14 @@ function onLoad(framework) directionalLight.position.multiplyScalar(10); scene.add(directionalLight); + + // initialize a simple box and material + var directionalLight2 = new THREE.DirectionalLight( 0xffffff, 1 ); + directionalLight2.color.setHSL(0.1, 1, 0.95); + directionalLight2.position.set(-1, -3, -2); + directionalLight2.position.multiplyScalar(10); + scene.add(directionalLight2); + // set camera position camera.position.set(2, 3, 4); camera.lookAt(new THREE.Vector3(0,2,0)); @@ -36,8 +45,8 @@ function onLoad(framework) profile.addPoint(.8, 1.1); profile.addPoint(.8, 1.0); - profile.addPoint(0.5, 1.0); - profile.addPoint(0.5, 2.0); + profile.addPoint(0.7, 1.0); + profile.addPoint(0.7, 2.0); profile.addPoint(0.0, 2.0); var lot = new Building.BuildingLot(); @@ -45,18 +54,18 @@ function onLoad(framework) for(var i = 0; i < subdivs; i++) { var a = i * Math.PI * 2 / subdivs; - lot.addPoint(Math.cos(a), Math.sin(a)); + var r = Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 + 1.0 ; + lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); } - // lot.addPoint(1.0, 1.0); - // lot.addPoint(1.0, -1.0); - // lot.addPoint(-1.0, -1.0); - // lot.addPoint(-1.0, 1.0); - // lot.addPoint(.5, 1.5); var shape = new Building.MassShape(); var mesh = shape.generateMesh(lot, profile); scene.add(mesh); + + var rubik = new Rubik.Rubik(); + // scene.add(rubik.build()); + } // called on frame updates diff --git a/src/rubik.js b/src/rubik.js new file mode 100644 index 00000000..db1e83ca --- /dev/null +++ b/src/rubik.js @@ -0,0 +1,154 @@ +const THREE = require('three'); + +function fequals(a, b) +{ + return Math.abs(a-b) < .0001; +} + +class Rubik +{ + constructor() + { + this.segments = []; + } + + rotateX(degrees, xPlane) + { + this.rotateInternal(degrees, xPlane, new THREE.Vector3( 1, 0, 0), function(x, y, plane) { + return new THREE.Vector3( plane, x, y ); + }); + } + + rotateY(degrees, yPlane) + { + this.rotateInternal(degrees, yPlane, new THREE.Vector3( 0, 1, 0), function(x, y, plane) { + return new THREE.Vector3( y, plane, x ); + }); + } + + rotateZ(degrees, zPlane) + { + this.rotateInternal(degrees, zPlane, new THREE.Vector3( 0, 0, 1), function(x, y, plane) { + return new THREE.Vector3( x, y, plane ); + }); + } + + rotateInternal(degrees, plane, axis, indexFunction) + { + var rad = THREE.Math.degToRad(THREE.Math.clamp(degrees, -90, 90)); + var tMatrix = new THREE.Matrix4(); + var sMatrix = new THREE.Matrix4(); + var rMatrix = new THREE.Matrix4(); + sMatrix.makeScale(.9, .9, .9); + + for(var i = 0; i < 3; i++) + { + for(var j = 0; j < 3; j++) + { + var p = indexFunction(i, j, plane); + var cube = this.segments[p.x][p.y][p.z]; + + var matrix = new THREE.Matrix4(); + cube.matrix.copy(matrix); + + rMatrix.makeRotationAxis(axis, rad); + tMatrix.makeTranslation(p.x - 1, p.y - 1, p.z - 1); + + matrix = rMatrix.clone(); + matrix.multiply(tMatrix); + matrix.multiply(sMatrix); + + cube.applyMatrix(matrix); + } + } + + // If the rotation is full, then we need to update the data + // structure + var tmpArray = []; + if(fequals(degrees, -90)) + { + for(var x = 0; x < 3; x++) + { + tmpArray.push(new Array()); + + for(var y = 0; y < 3; y++) + { + var p = indexFunction(2 - y, x, plane); + tmpArray[x][y] = this.segments[p.x][p.y][p.z]; + } + } + } + else if(fequals(degrees, 90)) + { + for(var x = 0; x < 3; x++) + { + tmpArray.push(new Array()); + + for(var y = 0; y < 3; y++) + { + var p = indexFunction(y, 2 - x, plane); + tmpArray[x][y] = this.segments[p.x][p.y][p.z]; + } + } + } + + // Now we effectively swap everything + if(tmpArray.length > 0) + { + for(var x = 0; x < 3; x++) + for(var y = 0; y < 3; y++) + { + var p = indexFunction(x, y, plane); + this.segments[p.x][p.y][p.z] = tmpArray[x][y]; + } + } + } + + build() + { + var container = new THREE.Object3D(); + var boxGeo = new THREE.BoxGeometry( 1, 1, 1 ); + var planeGeo = new THREE.PlaneGeometry(1, 1, 1, 1 ); + + var blackMaterial = new THREE.MeshLambertMaterial( {color: 0x777777} ); + + this.segments = [] + + for(var x = 0; x < 3; x++) + { + this.segments.push(new Array()); + + for(var y = 0; y < 3; y++) + { + this.segments[x].push(new Array()); + + for(var z = 0; z < 3; z++) + { + var mat = new THREE.MeshLambertMaterial( {color: 0x000000} ); + mat.color = new THREE.Color( (y + z) / 3, 0.0, 0.0 ); + + if(x == 1) + mat.color = new THREE.Color( 0.0, (y + z) / 3, 0.0 ); + else if(x == 2) + mat.color = new THREE.Color(0.0, 0.0, (y + z) / 3); + + var cube = new THREE.Mesh( boxGeo, mat ); + cube.position.copy(new THREE.Vector3( x - 1, y - 1, z - 1)); + cube.scale.copy(new THREE.Vector3( .9, .9, .9 )) + container.add(cube); + + // Index is x * 3 * 3 + y * 3 + z + this.segments[x][y].push(cube); + } + } + } + + + + return container; + } + + +} + +export {Rubik} \ No newline at end of file From 5a52b14c617ae9d5d5151ac60752757568358351 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Wed, 15 Feb 2017 19:08:41 -0500 Subject: [PATCH 04/21] + Rubik core animation --- src/main.js | 59 ++++++++++++++++++++++++++++++++++++++++++++------ src/rubik.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/main.js b/src/main.js index eba8b09e..e606a127 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ const THREE = require('three'); -import Framework from './framework' +const Random = require("random-js"); +import Framework from './framework' import * as Building from './building.js' import * as Rubik from './rubik.js' @@ -9,6 +10,22 @@ var UserSettings = iterations : 5 } +var Engine = +{ + initialized : false, + time : 0.0, + deltaTime : 0.0, + clock : null, + + music : null, + + loadingBlocker : null, + materials : [], + + rubik : null, + rubikTime : 0, +} + function onLoad(framework) { var scene = framework.scene; @@ -33,8 +50,8 @@ function onLoad(framework) scene.add(directionalLight2); // set camera position - camera.position.set(2, 3, 4); - camera.lookAt(new THREE.Vector3(0,2,0)); + camera.position.set(9, 7, 9); + camera.lookAt(new THREE.Vector3(0,0,0)); var profile = new Building.Profile(); profile.addPoint(1.0, 0.0); @@ -61,15 +78,43 @@ function onLoad(framework) var shape = new Building.MassShape(); var mesh = shape.generateMesh(lot, profile); - scene.add(mesh); + // scene.add(mesh); + + Engine.rubik = new Rubik.Rubik(); + scene.add(Engine.rubik.build()); - var rubik = new Rubik.Rubik(); - // scene.add(rubik.build()); + // Init Engine stuff + Engine.scene = scene; + Engine.renderer = renderer; + Engine.clock = new THREE.Clock(); + Engine.camera = camera; + Engine.initialized = true; + var random = new Random(Random.engines.mt19937().seed(2545)); + + var speed = .15; + + var callback = function() { + Engine.rubik.animate(random.integer(0, 2), random.integer(0, 2), speed, callback); + }; + + Engine.rubik.animate(0, 0, speed, callback); } // called on frame updates -function onUpdate(framework) { +function onUpdate(framework) +{ + if(Engine.initialized) + { + var screenSize = new THREE.Vector2( framework.renderer.getSize().width, framework.renderer.getSize().height ); + var deltaTime = Engine.clock.getDelta(); + + Engine.time += deltaTime; + Engine.cameraTime += deltaTime; + Engine.deltaTime = deltaTime; + + Engine.rubik.update(deltaTime); + } } // when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate diff --git a/src/rubik.js b/src/rubik.js index db1e83ca..d6213830 100644 --- a/src/rubik.js +++ b/src/rubik.js @@ -2,7 +2,7 @@ const THREE = require('three'); function fequals(a, b) { - return Math.abs(a-b) < .0001; + return Math.abs(a-b) < .001; } class Rubik @@ -10,25 +10,71 @@ class Rubik constructor() { this.segments = []; + this.animating = false; + this.currentAxis = 0; + this.currentPlane = 0; + this.currentLength = 1.0; + this.time = 0; + this.callback = null; + } + + animate(axis, plane, length, callback) + { + this.animating = true; + this.currentAxis = axis; + this.currentPlane = plane; + this.currentLength = length; + this.time = 0; + this.callback = callback; + } + + update(deltaTime) + { + if(!this.animating) + return; + + this.time += deltaTime; + + var t = THREE.Math.clamp(this.time / this.currentLength, 0.0, 1.0); + + t = THREE.Math.smoothstep(t, 0, 1); + var angle = t * 90; + var swapped = false; + + if(this.currentAxis == 0) + swapped = this.rotateX(angle, this.currentPlane); + else if(this.currentAxis == 1) + swapped = this.rotateY(angle, this.currentPlane); + else if(this.currentAxis == 2) + swapped = this.rotateZ(angle, this.currentPlane); + + if(swapped) + { + this.animating = false; + this.time = 0; + + if(this.callback != null) + this.callback(); + } } rotateX(degrees, xPlane) { - this.rotateInternal(degrees, xPlane, new THREE.Vector3( 1, 0, 0), function(x, y, plane) { + return this.rotateInternal(degrees, xPlane, new THREE.Vector3( 1, 0, 0), function(x, y, plane) { return new THREE.Vector3( plane, x, y ); }); } rotateY(degrees, yPlane) { - this.rotateInternal(degrees, yPlane, new THREE.Vector3( 0, 1, 0), function(x, y, plane) { + return this.rotateInternal(degrees, yPlane, new THREE.Vector3( 0, 1, 0), function(x, y, plane) { return new THREE.Vector3( y, plane, x ); }); } rotateZ(degrees, zPlane) { - this.rotateInternal(degrees, zPlane, new THREE.Vector3( 0, 0, 1), function(x, y, plane) { + return this.rotateInternal(degrees, zPlane, new THREE.Vector3( 0, 0, 1), function(x, y, plane) { return new THREE.Vector3( x, y, plane ); }); } @@ -101,7 +147,12 @@ class Rubik var p = indexFunction(x, y, plane); this.segments[p.x][p.y][p.z] = tmpArray[x][y]; } + + // We swapped + return true; } + + return false; } build() @@ -143,8 +194,6 @@ class Rubik } } - - return container; } From 16cb7683bcf116e150255dfd1379269eaf27a751 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 15:23:07 -0500 Subject: [PATCH 05/21] + Voronoi --- src/building.js | 91 +++++++++++++++++++++++++---- src/city.js | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 23 +++++--- src/rubik.js | 6 +- 4 files changed, 247 insertions(+), 22 deletions(-) create mode 100644 src/city.js diff --git a/src/building.js b/src/building.js index d7c31777..3cf0a143 100644 --- a/src/building.js +++ b/src/building.js @@ -1,5 +1,14 @@ const THREE = require('three'); +class Bounds +{ + constructor(min, max) + { + this.min = min; + this.max = max; + } +} + class Shape { constructor(size) @@ -66,29 +75,31 @@ class Profile class MassShape { - constructor() + constructor(lot, profile) { + this.lot = lot; + this.profile = profile; } - generateMesh(lot, profile) + generateMesh() { - lot.buildNormals(); + this.lot.buildNormals(); var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); var geometry = new THREE.Geometry(); - var boundaryVertexCount = lot.points.length; + var boundaryVertexCount = this.lot.points.length; var offset = 0; - for(var i = 0; i < profile.points.length; i++) + for(var i = 0; i < this.profile.points.length; i++) { - var profilePoint = profile.points[i]; - var profileDiff = (i == 0) ? new THREE.Vector2(0,0) : (profilePoint.clone().sub(profile.points[i-1])); + var profilePoint = this.profile.points[i]; + var profileDiff = (i == 0) ? new THREE.Vector2(0,0) : (profilePoint.clone().sub(this.profile.points[i-1])); for(var j = 0; j < boundaryVertexCount; j++) { - var boundaryPoint = lot.points[j]; - var boundaryNormal = lot.normals[j]; + var boundaryPoint = this.lot.points[j]; + var boundaryNormal = this.lot.normals[j]; var vertex = new THREE.Vector3(boundaryPoint.x, profilePoint.y, boundaryPoint.y); @@ -120,21 +131,77 @@ class MassShape } } - // Implement cap here! - geometry.mergeVertices(); geometry.computeFlatVertexNormals(); var mesh = new THREE.Mesh(geometry, material); + + this.geometry = geometry; + this.mesh = mesh; return mesh; } } class Rule { - evaluate(shape) + constructor() + { + this.componentWise = false; + } + + evaluateOverall(shape) { + } + + evaluateComponent(shape, scope) + { + } + evaluate(shape, scene) + { + if(this.componentWise) + { + var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + var geometry = new THREE.BoxGeometry( 1, 1, 1 ); + for(var f = 0; f < shape.geometry.faces.length; f += 2) + { + var face = shape.geometry.faces[f]; + + var v1 = shape.geometry.vertices[face.b]; + var v2 = shape.geometry.vertices[face.a]; + var v3 = shape.geometry.vertices[face.c]; + + var height = v2.clone().sub(v1).length(); + var width = v3.clone().sub(v1).length(); + + var u = v2.clone().sub(v1).normalize(); + var v = v3.clone().sub(v1).normalize(); + + var repetitionsU = 4; + var repetitionsV = 4; + + if(face.normal.y > .1 || height < .5) + continue; + + for(var r = 0; r < repetitionsU; r++) + { + for(var rV = 0; rV < repetitionsV; rV++) + { + var cube = new THREE.Mesh( geometry, material ); + var t = r / repetitionsU; + var tV = rV / repetitionsV; + cube.position.copy(u.clone().multiplyScalar(t * height).add(v.clone().multiplyScalar(tV * width).add(v1))); + cube.scale.set(.1 * width, .1 * height, .1); + cube.lookAt(cube.position.clone().add(face.normal)); + scene.add( cube ); + } + } + } + } + else + { + evaluateOverall(shape); + } } } diff --git a/src/city.js b/src/city.js new file mode 100644 index 00000000..85702498 --- /dev/null +++ b/src/city.js @@ -0,0 +1,149 @@ +const THREE = require('three'); +const Random = require("random-js"); + + +class Generator +{ + constructor() + { + } + + // Overall complexity of this Voronoi implementation: + // N + N^2 * 27 * 2 + build(scene) + { + var count = 20; + var random = new Random(Random.engines.mt19937().autoSeed()); + + var geometry = new THREE.Geometry(); + var pointsGeo = new THREE.Geometry(); + var points = []; + + // From center + var randomAmplitude = .5; + + // Distribute points + for(var x = 0; x < count; x++) + { + points.push(new Array()); + + for(var y = 0; y < count; y++) + { + var p = new THREE.Vector2( x + .5 + random.real(-randomAmplitude,randomAmplitude), y + .5 + random.real(-randomAmplitude,randomAmplitude)); + points[x].push(p); + pointsGeo.vertices.push(new THREE.Vector3( p.x, 0, p.y )); + } + } + + // Build half planes + var segments = []; + for(var x = 0; x < count; x++) + { + segments.push(new Array()); + + for(var y = 0; y < count; y++) + { + segments[x].push(new Array()); + + var p = points[x][y]; + + for(var i = -2; i < 3; i++) + { + for(var j = -2; j < 3; j++) + { + if(x+i >= 0 && y+j >= 0 && x+i < count && y+j < count) + { + var neighbor = points[x+i][y+j]; + + var normal = neighbor.clone().sub(p).normalize(); + var midpoint = neighbor.clone().add(p).multiplyScalar(.5); + var tangent = new THREE.Vector2( -normal.y, normal.x ); + + var segment = { valid : false, center : p, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } + segments[x][y].push(segment) + } + } + } + + // N^3 over amount of segments per cell (which is 27) + for(var i = 0; i < segments[x][y].length; i++) + { + for(var j = 0; j < segments[x][y].length; j++) + { + if(i == j) + continue; + + var s1 = segments[x][y][i]; + var s2 = segments[x][y][j]; + + // Parallel + if(Math.abs(s1.dir.dot(s2.normal)) < .001) + continue; + + var diff = s2.midpoint.clone().sub(s1.midpoint); + var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x; + + var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det; + var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det; + + var newPoint = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(u)); + var insideHull = true; + + for(var k = 0; k < segments[x][y].length; k++) + { + if(k != j && k != i) + { + var dP = newPoint.clone().sub(segments[x][y][k].midpoint); + + // Remember normals are inverted, this is why it is > 0 and not <= 0 + if(segments[x][y][k].normal.clone().dot(dP) > 0) + insideHull = false; + } + } + + if(!insideHull) + continue; + + s1.min = Math.min(s1.min, u); + s1.max = Math.max(s1.max, u); + s1.valid = true; + } + } + + // Save geo for display + for(var s = 0; s < segments[x][y].length; s++) + { + var segment = segments[x][y][s]; + + if(!segment.valid) + continue; + + var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); + var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + + geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) + geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) + } + } + } + + console.log(geometry.vertices.length); + + var material = new THREE.LineBasicMaterial( {color: 0xffffff} ); + + var line = new THREE.LineSegments(geometry, material); + scene.add(line); + + // material.wireframe = true; + + // var mesh = new THREE.Mesh(geometry, material); + // scene.add(mesh); + + var pointsMaterial = new THREE.PointsMaterial( { color: 0xffffff } ) + pointsMaterial.size = .1; + var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); + scene.add( pointsMesh ); + } +} + +export {Generator} \ No newline at end of file diff --git a/src/main.js b/src/main.js index e606a127..4a4a5c17 100644 --- a/src/main.js +++ b/src/main.js @@ -4,6 +4,7 @@ const Random = require("random-js"); import Framework from './framework' import * as Building from './building.js' import * as Rubik from './rubik.js' +import * as City from './city.js' var UserSettings = { @@ -63,25 +64,29 @@ function onLoad(framework) profile.addPoint(.8, 1.0); profile.addPoint(0.7, 1.0); - profile.addPoint(0.7, 2.0); + profile.addPoint(0.6, 2.0); profile.addPoint(0.0, 2.0); var lot = new Building.BuildingLot(); - var subdivs = 15; + var subdivs = 25; for(var i = 0; i < subdivs; i++) { var a = i * Math.PI * 2 / subdivs; - var r = Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 + 1.0 ; + var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 + 1.0 ; lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); } - var shape = new Building.MassShape(); - var mesh = shape.generateMesh(lot, profile); - + var shape = new Building.MassShape(lot, profile); + var mesh = shape.generateMesh(); // scene.add(mesh); + var rule = new Building.Rule(); + rule.componentWise = true; + // rule.evaluate(shape, scene); + Engine.rubik = new Rubik.Rubik(); - scene.add(Engine.rubik.build()); + var rubikMesh = Engine.rubik.build(); + // scene.add(rubikMesh); // Init Engine stuff Engine.scene = scene; @@ -94,6 +99,10 @@ function onLoad(framework) var speed = .15; + + var city = new City.Generator(); + city.build(scene); + var callback = function() { Engine.rubik.animate(random.integer(0, 2), random.integer(0, 2), speed, callback); }; diff --git a/src/rubik.js b/src/rubik.js index d6213830..fdb02e22 100644 --- a/src/rubik.js +++ b/src/rubik.js @@ -176,12 +176,12 @@ class Rubik for(var z = 0; z < 3; z++) { var mat = new THREE.MeshLambertMaterial( {color: 0x000000} ); - mat.color = new THREE.Color( (y + z) / 3, 0.0, 0.0 ); + mat.color = new THREE.Color( (y + z + .5) / 2, 0.0, 0.0 ); if(x == 1) - mat.color = new THREE.Color( 0.0, (y + z) / 3, 0.0 ); + mat.color = new THREE.Color( 0.0, (y + z + .5) / 2, 0.0 ); else if(x == 2) - mat.color = new THREE.Color(0.0, 0.0, (y + z) / 3); + mat.color = new THREE.Color(0.0, 0.0, (y + z + .5) / 2); var cube = new THREE.Mesh( boxGeo, mat ); cube.position.copy(new THREE.Vector3( x - 1, y - 1, z - 1)); From 1ea23b03bea3c8b1ac0aec1954e6c3a235e4a2e3 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 16:35:03 -0500 Subject: [PATCH 06/21] + Starting to fall down the rabbit hole... --- src/city.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/city.js b/src/city.js index 85702498..b03fd142 100644 --- a/src/city.js +++ b/src/city.js @@ -2,6 +2,16 @@ const THREE = require('three'); const Random = require("random-js"); + +class Voronoi +{ + constructor() + { + + } + +} + class Generator { constructor() @@ -12,7 +22,9 @@ class Generator // N + N^2 * 27 * 2 build(scene) { + // count * count final points var count = 20; + var scale = 2; var random = new Random(Random.engines.mt19937().autoSeed()); var geometry = new THREE.Geometry(); @@ -29,24 +41,34 @@ class Generator for(var y = 0; y < count; y++) { - var p = new THREE.Vector2( x + .5 + random.real(-randomAmplitude,randomAmplitude), y + .5 + random.real(-randomAmplitude,randomAmplitude)); + var r1 = random.real(-1, 1) * randomAmplitude * scale; + var r2 = random.real(-1, 1) * randomAmplitude * scale; + + var p = new THREE.Vector2( x * scale + .5 + r1, y * scale + .5 + r2); points[x].push(p); pointsGeo.vertices.push(new THREE.Vector3( p.x, 0, p.y )); } } - // Build half planes + // Build half planes, and finding their convex hulls var segments = []; + var hulls = []; + var hullPoints = []; + for(var x = 0; x < count; x++) { segments.push(new Array()); + hulls.push(new Array()); + hullPoints.push(new Array()); for(var y = 0; y < count; y++) { segments[x].push(new Array()); + hullPoints[x].push(new Array()); var p = points[x][y]; + // Find all planes for all close neighbors within 3 cells for(var i = -2; i < 3; i++) { for(var j = -2; j < 3; j++) @@ -104,6 +126,9 @@ class Generator if(!insideHull) continue; + hullPoints[x][y].push(newPoint); + pointsGeo.vertices.push(new THREE.Vector3( newPoint.x, 0, newPoint.y )); + s1.min = Math.min(s1.min, u); s1.max = Math.max(s1.max, u); s1.valid = true; @@ -123,10 +148,37 @@ class Generator geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) - } + } } } + // var mapCenter = new THREE.Vector3( count * .5, 0, count * .5); + + // for(var i = 0; i < geometry.vertices.length; i++) + // { + // // Deform it in an interesting way... + // var v = geometry.vertices[i]; + + // var toCenter = v.clone().sub(mapCenter); + // var dist = Math.pow(toCenter.length() / (count * count), 1.35) * count * count; + + // geometry.vertices[i] = mapCenter.clone().add(toCenter.multiplyScalar(dist)); + // } + + for(var x = 0; x < 10; x++) + { + var t = x / 9; + geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, 0 )); + geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, count * scale )); + } + + for(var y = 0; y < 10; y++) + { + var t = y / 9; + geometry.vertices.push(new THREE.Vector3(0,0, t * count * scale)); + geometry.vertices.push(new THREE.Vector3(count * scale, 0, t * count * scale)); + } + console.log(geometry.vertices.length); var material = new THREE.LineBasicMaterial( {color: 0xffffff} ); From e55771f10d9cc3b0e2bded6071c67aafbd2f3408 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 21:18:15 -0500 Subject: [PATCH 07/21] + We didnt start the fire --- images/hull_intersection.png | Bin 0 -> 110661 bytes src/building.js | 9 -- src/city.js | 272 +++++++++++++++++++++++++++++++---- src/common.js | 33 +++++ 4 files changed, 275 insertions(+), 39 deletions(-) create mode 100644 images/hull_intersection.png create mode 100644 src/common.js diff --git a/images/hull_intersection.png b/images/hull_intersection.png new file mode 100644 index 0000000000000000000000000000000000000000..754c7eba78aa38e903ece134eef439b23cd5426f GIT binary patch literal 110661 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�Fk7kAwo#XXxBGjJHEoqpLJ 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 Hu0mjfDp`h1 literal 0 HcmV?d00001 diff --git a/src/building.js b/src/building.js index 3cf0a143..96a2a7c2 100644 --- a/src/building.js +++ b/src/building.js @@ -1,14 +1,5 @@ const THREE = require('three'); -class Bounds -{ - constructor(min, max) - { - this.min = min; - this.max = max; - } -} - class Shape { constructor(size) diff --git a/src/city.js b/src/city.js index b03fd142..57484fa9 100644 --- a/src/city.js +++ b/src/city.js @@ -1,7 +1,7 @@ const THREE = require('three'); const Random = require("random-js"); - +import * as Common from './common.js' class Voronoi { @@ -9,7 +9,129 @@ class Voronoi { } +} + +// 2D only! +class ConvexHull +{ + constructor() + { + this.bounds = null; + this.points = []; + this.segments = []; + } + + addPoint(point) + { + if(this.bounds == null) + this.bounds = new Common.Bounds(point, point); + else + this.bounds.encapsulate(point); + + this.points.push(point); + } + getCenter() + { + var c = new THREE.Vector2(0,0); + + for(var p = 0; p < this.points.length; p++) + c.add(this.points[p]); + + c.multiplyScalar(1.0 / this.points.length); + + return c; + } + + addSegment(s) + { + // No bounds, this assumes each vertex was added... + this.segments.push(s); + } + + sort() + { + } + + split(axis, splitDistance) + { + var newHull = new ConvexHull(); + + var tangent = new THREE.Vector2( -axis.y, axis.x); + var offset = axis.clone().multiplyScalar(splitDistance); + + var oldPoints = this.points; + + var projectedMidpoint = this.getCenter(); + var distanceToOffset = projectedMidpoint.clone().sub(offset); + projectedMidpoint = projectedMidpoint.clone().sub(axis.clone().multiplyScalar(axis.dot(distanceToOffset))); + + // The new segment to add + var s1 = { valid : true, normal: axis, dir: tangent, midpoint: projectedMidpoint, min : -1000, max : 1000 } + + for(var s = 0; s < this.segments.length; s++) + { + var s2 = this.segments[s]; + + // Parallel + if(Math.abs(s1.dir.dot(s2.normal)) < .001) + continue; + + var diff = s2.midpoint.clone().sub(s1.midpoint); + var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x; + + var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det; + var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det; + + if(u < 0) + s1.min = -Math.min(-s1.min, -u); + else + s1.max = Math.min(s1.max, u); + } + + this.points = []; + this.bounds = null; + + for(var p = 0; p < oldPoints.length; p++) + { + var point = oldPoints[p]; + + var d = point.clone().sub(offset).dot(axis); + + if(d > 0) + this.addPoint(point); + else + newHull.addPoint(point); + } + + // If the other hull is empty, dont split + if(newHull.points.length == 0) + return null; + + // If this hull is empty, absorb the other hull + if(this.points.length == 0) + { + this.points = newHull.points; + this.bounds = newHull.bounds; + return null; + } + + // If no hull is empty, there was effectively a split + // Now lets add the new segment + this.addSegment(s1); + newHull.addSegment(s1); + + var from = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(s1.min)); + var to = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(s1.max)); + + this.addPoint(from); + this.addPoint(to); + + newHull.addPoint(from); + newHull.addPoint(to); + + return newHull; + } } class Generator @@ -30,6 +152,8 @@ class Generator var geometry = new THREE.Geometry(); var pointsGeo = new THREE.Geometry(); var points = []; + var xAxis = new THREE.Vector2( 1, 0 ); + var yAxis = new THREE.Vector2( 0, 1 ); // From center var randomAmplitude = .5; @@ -46,27 +170,24 @@ class Generator var p = new THREE.Vector2( x * scale + .5 + r1, y * scale + .5 + r2); points[x].push(p); - pointsGeo.vertices.push(new THREE.Vector3( p.x, 0, p.y )); + // pointsGeo.vertices.push(new THREE.Vector3( p.x, 0, p.y )); } } // Build half planes, and finding their convex hulls var segments = []; var hulls = []; - var hullPoints = []; for(var x = 0; x < count; x++) { segments.push(new Array()); - hulls.push(new Array()); - hullPoints.push(new Array()); for(var y = 0; y < count; y++) { segments[x].push(new Array()); - hullPoints[x].push(new Array()); var p = points[x][y]; + var hull = new ConvexHull(); // Find all planes for all close neighbors within 3 cells for(var i = -2; i < 3; i++) @@ -81,12 +202,44 @@ class Generator var midpoint = neighbor.clone().add(p).multiplyScalar(.5); var tangent = new THREE.Vector2( -normal.y, normal.x ); - var segment = { valid : false, center : p, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } + var segment = { valid : false, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } segments[x][y].push(segment) } } } + // For reference: failed attempt at constraining cell half planes + // { + // var fX = Math.floor(10 * (x / count)) * scale * count / 10; + // var fP = new THREE.Vector2( fX, 0 ); + + // var segment = { valid : false, normal: xAxis.clone().negate(), dir: yAxis, midpoint: fP, min : 1000, max : -1000 } + // segments[x][y].push(segment); + // } + // { + // var fX = (Math.floor(10 * (x / count))+1) * scale * count / 10; + // var fP = new THREE.Vector2( fX, 0 ); + + // var segment = { valid : false, normal: xAxis.clone(), dir: yAxis, midpoint: fP, min : 1000, max : -1000 } + // segments[x][y].push(segment); + // } + + // { + // var fY = Math.floor(10 * (y / count)) * scale * count / 10; + // var fP = new THREE.Vector2( 0, fY ); + + // var segment = { valid : false, normal: yAxis.clone().negate(), dir: new THREE.Vector2( -yAxis.y, yAxis.x ), midpoint: fP, min : 1000, max : -1000 } + // segments[x][y].push(segment); + // } + + // { + // var fY = (Math.floor(10 * (y / count)) + 1) * scale * count / 10; + // var fP = new THREE.Vector2( 0, fY ); + + // var segment = { valid : false, normal: yAxis.clone(), dir: new THREE.Vector2( -yAxis.y, yAxis.x ), midpoint: fP, min : 1000, max : -1000 } + // segments[x][y].push(segment); + // } + // N^3 over amount of segments per cell (which is 27) for(var i = 0; i < segments[x][y].length; i++) { @@ -126,8 +279,9 @@ class Generator if(!insideHull) continue; - hullPoints[x][y].push(newPoint); - pointsGeo.vertices.push(new THREE.Vector3( newPoint.x, 0, newPoint.y )); + hull.addPoint(newPoint) + hull.addSegment(s1); + // pointsGeo.vertices.push(new THREE.Vector3( newPoint.x, 0, newPoint.y )); s1.min = Math.min(s1.min, u); s1.max = Math.max(s1.max, u); @@ -135,20 +289,25 @@ class Generator } } - // Save geo for display - for(var s = 0; s < segments[x][y].length; s++) - { - var segment = segments[x][y][s]; + // // Save geo for display + // for(var s = 0; s < segments[x][y].length; s++) + // { + // var segment = segments[x][y][s]; - if(!segment.valid) - continue; + // if(!segment.valid) + // continue; - var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); - var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + // var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); + // var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); - geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) - geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) - } + // geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) + // geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) + // } + + hulls.push(hull); + + // var center = hull.getCenter(); + // pointsGeo.vertices.push(new THREE.Vector3( center.x, 0, center.y)); } } @@ -165,20 +324,73 @@ class Generator // geometry.vertices[i] = mapCenter.clone().add(toCenter.multiplyScalar(dist)); // } - for(var x = 0; x < 10; x++) - { - var t = x / 9; - geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, 0 )); - geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, count * scale )); - } - for(var y = 0; y < 10; y++) + var newHulls = []; + + // for(var x = 0; x < 10; x++) + // { + // var t = x / 9; + // geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, 0 )); + // geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, count * scale )); + // } + + // var sliceX = t * count * scale; + + // for(var h = 0; h < hulls.length; h++) + // { + // var hull = hulls[h]; + + // if(hull.bounds.intersectsX(sliceX)) + // { + // var newHull = hull.split(xAxis, sliceX); + + // if(newHull != null) + // newHulls.push(newHull); + // } + // } + // } + + // for(var h = 0; h < newHulls.length; h++) + // hulls.push(newHulls[h]); + + // for(var y = 0; y < 10; y++) + // { + // var t = y / 9; + // geometry.vertices.push(new THREE.Vector3(0,0, t * count * scale)); + // geometry.vertices.push(new THREE.Vector3(count * scale, 0, t * count * scale)); + // } + + + + // Save geo for display + + + for(var h = 0; h < hulls.length; h++) { - var t = y / 9; - geometry.vertices.push(new THREE.Vector3(0,0, t * count * scale)); - geometry.vertices.push(new THREE.Vector3(count * scale, 0, t * count * scale)); + for(var s = 0; s < hulls[h].segments.length; s++) + { + var segment = hulls[h].segments[s]; + + if(!segment.valid) + continue; + + var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); + var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + + geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) + geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) + + + pointsGeo.vertices.push(new THREE.Vector3( from.x, 0, from.y)); + pointsGeo.vertices.push(new THREE.Vector3( to.x, 0, to.y)); + } + + // var center = hulls[h].getCenter(); + // pointsGeo.vertices.push(new THREE.Vector3( center.x, 0, center.y)); } + console.log(newHulls.length); + console.log(hulls.length); console.log(geometry.vertices.length); var material = new THREE.LineBasicMaterial( {color: 0xffffff} ); diff --git a/src/common.js b/src/common.js new file mode 100644 index 00000000..26160775 --- /dev/null +++ b/src/common.js @@ -0,0 +1,33 @@ +const THREE = require('three'); + +class Bounds +{ + constructor(min, max) + { + this.min = min.clone(); + this.max = max.clone(); + } + + encapsulate(p) + { + this.min.min(p); + this.max.max(p); + } + + intersectsX(x) + { + return this.min.x < x && this.max.x > x; + } + + intersectsY(y) + { + return this.min.y < y && this.max.y > y; + } + + intersectsZ(Z) + { + return this.min.z < z && this.max.z > z; + } +} + +export {Bounds} \ No newline at end of file From e2b87f4be2f996bba7c03353afaa4b9c72aa7e1d Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 22:19:40 -0500 Subject: [PATCH 08/21] + It was always burning since the world's been turning --- src/city.js | 398 ++++++++++++++++++++-------------------------------- 1 file changed, 152 insertions(+), 246 deletions(-) diff --git a/src/city.js b/src/city.js index 57484fa9..54fcca54 100644 --- a/src/city.js +++ b/src/city.js @@ -17,20 +17,9 @@ class ConvexHull constructor() { this.bounds = null; - this.points = []; this.segments = []; } - addPoint(point) - { - if(this.bounds == null) - this.bounds = new Common.Bounds(point, point); - else - this.bounds.encapsulate(point); - - this.points.push(point); - } - getCenter() { var c = new THREE.Vector2(0,0); @@ -42,95 +31,130 @@ class ConvexHull return c; } - - addSegment(s) + + addSegment(normal, direction, midpoint, min, max) { - // No bounds, this assumes each vertex was added... - this.segments.push(s); + this.segments.push({ valid : false, normal: normal, dir: direction, midpoint: midpoint, min : min, max : max }); } sort() - { + { } - split(axis, splitDistance) - { - var newHull = new ConvexHull(); - - var tangent = new THREE.Vector2( -axis.y, axis.x); - var offset = axis.clone().multiplyScalar(splitDistance); - - var oldPoints = this.points; + updateBounds() + { + this.bounds = null; - var projectedMidpoint = this.getCenter(); - var distanceToOffset = projectedMidpoint.clone().sub(offset); - projectedMidpoint = projectedMidpoint.clone().sub(axis.clone().multiplyScalar(axis.dot(distanceToOffset))); + for(var h = 0; h < this.segments.length; h++) + { + if(this.segments[h].valid) + { + var segment = this.segments[h]; + var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); + var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + + if(this.bounds == null) + this.bounds = new Common.Bounds(from, from); - // The new segment to add - var s1 = { valid : true, normal: axis, dir: tangent, midpoint: projectedMidpoint, min : -1000, max : 1000 } + this.bounds.encapsulate(from); + this.bounds.encapsulate(to); + } + } + } - for(var s = 0; s < this.segments.length; s++) + calculateSegments() + { + for(var i = 0; i < this.segments.length; i++) { - var s2 = this.segments[s]; + for(var j = 0; j < this.segments.length; j++) + { + if(i == j) + continue; - // Parallel - if(Math.abs(s1.dir.dot(s2.normal)) < .001) - continue; + var s1 = this.segments[i]; + var s2 = this.segments[j]; - var diff = s2.midpoint.clone().sub(s1.midpoint); - var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x; + // Parallel + if(Math.abs(s1.dir.dot(s2.normal)) < .001) + continue; - var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det; - var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det; + var diff = s2.midpoint.clone().sub(s1.midpoint); + var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x; - if(u < 0) - s1.min = -Math.min(-s1.min, -u); - else - s1.max = Math.min(s1.max, u); - } + var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det; + var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det; - this.points = []; - this.bounds = null; + var newPoint = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(u)); + var insideHull = true; - for(var p = 0; p < oldPoints.length; p++) - { - var point = oldPoints[p]; + // This is the naive, N^3 intersection test method + for(var k = 0; k < this.segments.length; k++) + { + if(k != j && k != i) + { + var dP = newPoint.clone().sub(this.segments[k].midpoint); + + // Remember normals are inverted, this is why it is > 0 and not <= 0 + if(this.segments[k].normal.clone().dot(dP) > 0) + insideHull = false; + } + } - var d = point.clone().sub(offset).dot(axis); + if(!insideHull) + continue; - if(d > 0) - this.addPoint(point); - else - newHull.addPoint(point); + s1.min = Math.min(s1.min, u); + s1.max = Math.max(s1.max, u); + s1.valid = true; + } } - // If the other hull is empty, dont split - if(newHull.points.length == 0) - return null; + this.updateBounds(); + } + + isValid() + { + var valid = 0; - // If this hull is empty, absorb the other hull - if(this.points.length == 0) + for(var h = 0; h < this.segments.length; h++) { - this.points = newHull.points; - this.bounds = newHull.bounds; - return null; + if(this.segments[h].valid) + valid++; } + + return valid > 2; + } - // If no hull is empty, there was effectively a split - // Now lets add the new segment - this.addSegment(s1); - newHull.addSegment(s1); + // Returns two copies + splitComplex(axis, splitDistance) + { + var h1 = new ConvexHull(); + var h2 = new ConvexHull(); + + var tangent = new THREE.Vector2( -axis.y, axis.x); + var offset = axis.clone().multiplyScalar(splitDistance); + + var oldPoints = this.points; - var from = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(s1.min)); - var to = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(s1.max)); + h1.addSegment(axis, tangent, offset, 1000, -1000); + h2.addSegment(axis.clone().negate(), tangent.clone().negate(), offset, 1000, -1000); - this.addPoint(from); - this.addPoint(to); + for(var h = 0; h < this.segments.length; h++) + { + var segment = this.segments[h]; - newHull.addPoint(from); - newHull.addPoint(to); + // Some pruning + if(segment.valid) + { + h1.addSegment(segment.normal, segment.dir, segment.midpoint, segment.min, segment.max); + h2.addSegment(segment.normal, segment.dir, segment.midpoint, segment.min, segment.max); + } + } - return newHull; + h1.calculateSegments(); + h2.calculateSegments(); + + return [h1, h2]; } } @@ -140,12 +164,54 @@ class Generator { } - // Overall complexity of this Voronoi implementation: - // N + N^2 * 27 * 2 + sliceHullSet(hulls, axis, subdivisions, scale, intersectionFunction) + { + for(var h = 0; h < hulls.length; h++) + hulls[h].added = false; + + var newHulls = []; + for(var x = 0; x < subdivisions + 1; x++) + { + var t = x / subdivisions; + var sliceOffset = t * scale; + + for(var h = 0; h < hulls.length; h++) + { + var hull = hulls[h]; + + if(intersectionFunction(hull, sliceOffset)) + { + var split = hull.splitComplex(axis, sliceOffset); + + if(split[0].isValid()) + newHulls.push(split[0]); + + if(split[1].isValid()) + newHulls.push(split[1]); + } + else + { + if(!hull.added) + { + hull.added = true; + newHulls.push(hull); + } + } + } + } + + return newHulls; + } + + // Algorithm overview: + // Build voronoi with half plane intersection tests, based on a jittered grid + // (super important each point is inside each cell) + // Generate convex hulls + // Subdivide convex hulls in X and Y, 9 times build(scene) { // count * count final points - var count = 20; + var count = 40; var scale = 2; var random = new Random(Random.engines.mt19937().autoSeed()); @@ -156,7 +222,7 @@ class Generator var yAxis = new THREE.Vector2( 0, 1 ); // From center - var randomAmplitude = .5; + var randomAmplitude = .499; // Distribute points for(var x = 0; x < count; x++) @@ -170,22 +236,16 @@ class Generator var p = new THREE.Vector2( x * scale + .5 + r1, y * scale + .5 + r2); points[x].push(p); - // pointsGeo.vertices.push(new THREE.Vector3( p.x, 0, p.y )); } } // Build half planes, and finding their convex hulls - var segments = []; var hulls = []; for(var x = 0; x < count; x++) { - segments.push(new Array()); - for(var y = 0; y < count; y++) { - segments[x].push(new Array()); - var p = points[x][y]; var hull = new ConvexHull(); @@ -202,169 +262,24 @@ class Generator var midpoint = neighbor.clone().add(p).multiplyScalar(.5); var tangent = new THREE.Vector2( -normal.y, normal.x ); - var segment = { valid : false, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } - segments[x][y].push(segment) + // var segment = { valid : false, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } + + hull.addSegment(normal, tangent, midpoint, 1000, -1000); } } } - // For reference: failed attempt at constraining cell half planes - // { - // var fX = Math.floor(10 * (x / count)) * scale * count / 10; - // var fP = new THREE.Vector2( fX, 0 ); - - // var segment = { valid : false, normal: xAxis.clone().negate(), dir: yAxis, midpoint: fP, min : 1000, max : -1000 } - // segments[x][y].push(segment); - // } - // { - // var fX = (Math.floor(10 * (x / count))+1) * scale * count / 10; - // var fP = new THREE.Vector2( fX, 0 ); - - // var segment = { valid : false, normal: xAxis.clone(), dir: yAxis, midpoint: fP, min : 1000, max : -1000 } - // segments[x][y].push(segment); - // } - - // { - // var fY = Math.floor(10 * (y / count)) * scale * count / 10; - // var fP = new THREE.Vector2( 0, fY ); - - // var segment = { valid : false, normal: yAxis.clone().negate(), dir: new THREE.Vector2( -yAxis.y, yAxis.x ), midpoint: fP, min : 1000, max : -1000 } - // segments[x][y].push(segment); - // } - - // { - // var fY = (Math.floor(10 * (y / count)) + 1) * scale * count / 10; - // var fP = new THREE.Vector2( 0, fY ); - - // var segment = { valid : false, normal: yAxis.clone(), dir: new THREE.Vector2( -yAxis.y, yAxis.x ), midpoint: fP, min : 1000, max : -1000 } - // segments[x][y].push(segment); - // } - - // N^3 over amount of segments per cell (which is 27) - for(var i = 0; i < segments[x][y].length; i++) - { - for(var j = 0; j < segments[x][y].length; j++) - { - if(i == j) - continue; - - var s1 = segments[x][y][i]; - var s2 = segments[x][y][j]; - - // Parallel - if(Math.abs(s1.dir.dot(s2.normal)) < .001) - continue; - - var diff = s2.midpoint.clone().sub(s1.midpoint); - var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x; - - var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det; - var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det; - - var newPoint = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(u)); - var insideHull = true; - - for(var k = 0; k < segments[x][y].length; k++) - { - if(k != j && k != i) - { - var dP = newPoint.clone().sub(segments[x][y][k].midpoint); - - // Remember normals are inverted, this is why it is > 0 and not <= 0 - if(segments[x][y][k].normal.clone().dot(dP) > 0) - insideHull = false; - } - } - - if(!insideHull) - continue; - - hull.addPoint(newPoint) - hull.addSegment(s1); - // pointsGeo.vertices.push(new THREE.Vector3( newPoint.x, 0, newPoint.y )); - - s1.min = Math.min(s1.min, u); - s1.max = Math.max(s1.max, u); - s1.valid = true; - } - } - - // // Save geo for display - // for(var s = 0; s < segments[x][y].length; s++) - // { - // var segment = segments[x][y][s]; - - // if(!segment.valid) - // continue; - - // var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); - // var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); - - // geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) - // geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) - // } - + hull.calculateSegments(); hulls.push(hull); - - // var center = hull.getCenter(); - // pointsGeo.vertices.push(new THREE.Vector3( center.x, 0, center.y)); } } - // var mapCenter = new THREE.Vector3( count * .5, 0, count * .5); - - // for(var i = 0; i < geometry.vertices.length; i++) - // { - // // Deform it in an interesting way... - // var v = geometry.vertices[i]; - - // var toCenter = v.clone().sub(mapCenter); - // var dist = Math.pow(toCenter.length() / (count * count), 1.35) * count * count; - - // geometry.vertices[i] = mapCenter.clone().add(toCenter.multiplyScalar(dist)); - // } - - - var newHulls = []; - - // for(var x = 0; x < 10; x++) - // { - // var t = x / 9; - // geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, 0 )); - // geometry.vertices.push(new THREE.Vector3(t * count * scale, 0, count * scale )); - // } - - // var sliceX = t * count * scale; - - // for(var h = 0; h < hulls.length; h++) - // { - // var hull = hulls[h]; - - // if(hull.bounds.intersectsX(sliceX)) - // { - // var newHull = hull.split(xAxis, sliceX); - - // if(newHull != null) - // newHulls.push(newHull); - // } - // } - // } - - // for(var h = 0; h < newHulls.length; h++) - // hulls.push(newHulls[h]); - - // for(var y = 0; y < 10; y++) - // { - // var t = y / 9; - // geometry.vertices.push(new THREE.Vector3(0,0, t * count * scale)); - // geometry.vertices.push(new THREE.Vector3(count * scale, 0, t * count * scale)); - // } - - + // Subdivide everything 9 times in X and Y + hulls = this.sliceHullSet(hulls, xAxis, 9, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsX(sliceOffset); }); + hulls = this.sliceHullSet(hulls, yAxis, 9, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsY(sliceOffset); }); + console.log("Hulls: " + hulls.length); // Save geo for display - - for(var h = 0; h < hulls.length; h++) { for(var s = 0; s < hulls[h].segments.length; s++) @@ -380,7 +295,6 @@ class Generator geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) - pointsGeo.vertices.push(new THREE.Vector3( from.x, 0, from.y)); pointsGeo.vertices.push(new THREE.Vector3( to.x, 0, to.y)); } @@ -389,20 +303,12 @@ class Generator // pointsGeo.vertices.push(new THREE.Vector3( center.x, 0, center.y)); } - console.log(newHulls.length); - console.log(hulls.length); console.log(geometry.vertices.length); - var material = new THREE.LineBasicMaterial( {color: 0xffffff} ); - - var line = new THREE.LineSegments(geometry, material); + var lineMaterial = new THREE.LineBasicMaterial( {color: 0xffffff} ); + var line = new THREE.LineSegments(geometry, lineMaterial); scene.add(line); - // material.wireframe = true; - - // var mesh = new THREE.Mesh(geometry, material); - // scene.add(mesh); - var pointsMaterial = new THREE.PointsMaterial( { color: 0xffffff } ) pointsMaterial.size = .1; var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); From df3652e0daa37d9dbc9bd16003b938bf54fa8370 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 23:19:47 -0500 Subject: [PATCH 09/21] + No we didn't light it --- src/city.js | 108 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/src/city.js b/src/city.js index 54fcca54..79e578e9 100644 --- a/src/city.js +++ b/src/city.js @@ -18,6 +18,8 @@ class ConvexHull { this.bounds = null; this.segments = []; + this.vertices = []; + this.midpoint = new THREE.Vector2(0,0); } getCenter() @@ -34,13 +36,46 @@ class ConvexHull addSegment(normal, direction, midpoint, min, max) { - this.segments.push({ valid : false, normal: normal, dir: direction, midpoint: midpoint, min : min, max : max }); + this.segments.push({ valid : false, normal: normal.clone(), dir: direction.clone(), midpoint: midpoint.clone(), min : 1000, max : -1000}); } sort() { } + calculateVertices() + { + this.vertices = []; + this.midpoint = new THREE.Vector2(0,0); + + function addVertex(v, vertices, midpoint) + { + for(var i = 0; i < vertices.length; i++) + if(v.distanceTo(vertices[i]) < .01) + return; + + vertices.push(v); + midpoint.add(v); + } + + for(var s = 0; s < this.segments.length; s++) + { + var segment = this.segments[s]; + + if(!segment.valid) + continue; + + var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); + var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + + addVertex(from, this.vertices, this.midpoint); + addVertex(to, this.vertices, this.midpoint); + } + + if(this.vertices.length > 0) + this.midpoint.multiplyScalar(1.0 / this.vertices.length); + } + updateBounds() { this.bounds = null; @@ -121,7 +156,7 @@ class ConvexHull if(this.segments[h].valid) valid++; } - + return valid > 2; } @@ -136,8 +171,8 @@ class ConvexHull var oldPoints = this.points; - h1.addSegment(axis, tangent, offset, 1000, -1000); - h2.addSegment(axis.clone().negate(), tangent.clone().negate(), offset, 1000, -1000); + h1.addSegment(axis, tangent, offset); + h2.addSegment(axis.clone().negate(), tangent.clone().negate(), offset); for(var h = 0; h < this.segments.length; h++) { @@ -146,8 +181,8 @@ class ConvexHull // Some pruning if(segment.valid) { - h1.addSegment(segment.normal, segment.dir, segment.midpoint, segment.min, segment.max); - h2.addSegment(segment.normal, segment.dir, segment.midpoint, segment.min, segment.max); + h1.addSegment(segment.normal, segment.dir, segment.midpoint); + h2.addSegment(segment.normal, segment.dir, segment.midpoint); } } @@ -166,18 +201,17 @@ class Generator sliceHullSet(hulls, axis, subdivisions, scale, intersectionFunction) { - for(var h = 0; h < hulls.length; h++) - hulls[h].added = false; - var newHulls = []; - for(var x = 0; x < subdivisions + 1; x++) + + for(var h = 0; h < hulls.length; h++) { - var t = x / subdivisions; - var sliceOffset = t * scale; + var hull = hulls[h]; + var intersected = false; - for(var h = 0; h < hulls.length; h++) + for(var x = 0; x < subdivisions + 1 && !intersected; x++) { - var hull = hulls[h]; + var t = x / subdivisions; + var sliceOffset = t * scale; if(intersectionFunction(hull, sliceOffset)) { @@ -188,16 +222,13 @@ class Generator if(split[1].isValid()) newHulls.push(split[1]); - } - else - { - if(!hull.added) - { - hull.added = true; - newHulls.push(hull); - } + + intersected = true; } } + + if(!intersected) + newHulls.push(hull); } return newHulls; @@ -211,7 +242,7 @@ class Generator build(scene) { // count * count final points - var count = 40; + var count = 30; var scale = 2; var random = new Random(Random.engines.mt19937().autoSeed()); @@ -261,10 +292,7 @@ class Generator var normal = neighbor.clone().sub(p).normalize(); var midpoint = neighbor.clone().add(p).multiplyScalar(.5); var tangent = new THREE.Vector2( -normal.y, normal.x ); - - // var segment = { valid : false, normal: normal, dir: tangent, midpoint: midpoint, min : 1000, max : -1000 } - - hull.addSegment(normal, tangent, midpoint, 1000, -1000); + hull.addSegment(normal, tangent, midpoint); } } } @@ -274,14 +302,23 @@ class Generator } } - // Subdivide everything 9 times in X and Y - hulls = this.sliceHullSet(hulls, xAxis, 9, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsX(sliceOffset); }); - hulls = this.sliceHullSet(hulls, yAxis, 9, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsY(sliceOffset); }); + // Subdivide everything 11 times in X and Y + // Then we take the inner 9x9 grids to build a cube + hulls = this.sliceHullSet(hulls, xAxis, 11, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsX(sliceOffset); }); + hulls = this.sliceHullSet(hulls, yAxis, 11, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsY(sliceOffset); }); console.log("Hulls: " + hulls.length); // Save geo for display for(var h = 0; h < hulls.length; h++) { + if(!hulls[h].isValid()) + continue; + + var height = 0;// h * .1; + + hulls[h].calculateVertices(); + pointsGeo.vertices.push(new THREE.Vector3( hulls[h].midpoint.x, height, hulls[h].midpoint.y)); + for(var s = 0; s < hulls[h].segments.length; s++) { var segment = hulls[h].segments[s]; @@ -292,15 +329,12 @@ class Generator var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); - geometry.vertices.push(new THREE.Vector3( from.x, 0, from.y )) - geometry.vertices.push(new THREE.Vector3( to.x, 0, to.y )) + geometry.vertices.push(new THREE.Vector3( from.x, height, from.y )) + geometry.vertices.push(new THREE.Vector3( to.x, height, to.y )) - pointsGeo.vertices.push(new THREE.Vector3( from.x, 0, from.y)); - pointsGeo.vertices.push(new THREE.Vector3( to.x, 0, to.y)); + pointsGeo.vertices.push(new THREE.Vector3( from.x, height, from.y)); + pointsGeo.vertices.push(new THREE.Vector3( to.x, height, to.y)); } - - // var center = hulls[h].getCenter(); - // pointsGeo.vertices.push(new THREE.Vector3( center.x, 0, center.y)); } console.log(geometry.vertices.length); From 8716c7f219681f155082ddbe04b06c44ed614e4e Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Thu, 16 Feb 2017 23:31:13 -0500 Subject: [PATCH 10/21] But we tried to fight it --- src/city.js | 29 +++++++++++++++++++++++++++-- src/common.js | 5 +++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/city.js b/src/city.js index 79e578e9..b38ba942 100644 --- a/src/city.js +++ b/src/city.js @@ -308,15 +308,40 @@ class Generator hulls = this.sliceHullSet(hulls, yAxis, 11, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsY(sliceOffset); }); console.log("Hulls: " + hulls.length); + var cellScale = count * scale / 11; + var cellBounds = []; + + // Top cell + cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 1 * cellScale ), new THREE.Vector2( 7 * cellScale, 4 * cellScale ))); + cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 7 * cellScale ), new THREE.Vector2( 7 * cellScale, 10 * cellScale ))); + + // Horizontal cells + cellBounds.push(new Common.Bounds(new THREE.Vector2( 1 * cellScale, 4 * cellScale ), new THREE.Vector2( 4 * cellScale, 7 * cellScale ))); + cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 4 * cellScale ), new THREE.Vector2( 7 * cellScale, 7 * cellScale ))); + cellBounds.push(new Common.Bounds(new THREE.Vector2( 7 * cellScale, 4 * cellScale ), new THREE.Vector2( 10 * cellScale, 7 * cellScale ))); + + // Last cell + cellBounds.push(new Common.Bounds(new THREE.Vector2( 7 * cellScale, 7 * cellScale ), new THREE.Vector2( 10 * cellScale, 10 * cellScale ))); + // Save geo for display for(var h = 0; h < hulls.length; h++) { if(!hulls[h].isValid()) continue; - var height = 0;// h * .1; - hulls[h].calculateVertices(); + + var bounded = false; + for(var b = 0; b < cellBounds.length; b++) + { + if(cellBounds[b].contains(hulls[h].midpoint)) + bounded = true; + } + + if(!bounded) + continue; + + var height = 0; pointsGeo.vertices.push(new THREE.Vector3( hulls[h].midpoint.x, height, hulls[h].midpoint.y)); for(var s = 0; s < hulls[h].segments.length; s++) diff --git a/src/common.js b/src/common.js index 26160775..f5da52ab 100644 --- a/src/common.js +++ b/src/common.js @@ -28,6 +28,11 @@ class Bounds { return this.min.z < z && this.max.z > z; } + + contains(p) + { + return this.min.x < p.x && this.min.y < p.y && this.max.x > p.x && this.max.y > p.y; + } } export {Bounds} \ No newline at end of file From 2f4dfa9c4ad8a73310b6dbd4270544adfa2f4c9b Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 00:47:13 -0500 Subject: [PATCH 11/21] + We didn't start the fire (x2) --- src/city.js | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/city.js b/src/city.js index b38ba942..99a909fc 100644 --- a/src/city.js +++ b/src/city.js @@ -39,8 +39,47 @@ class ConvexHull this.segments.push({ valid : false, normal: normal.clone(), dir: direction.clone(), midpoint: midpoint.clone(), min : 1000, max : -1000}); } - sort() + // Sorts the vertices with the gift wrapping algorithm + sortVertices() { + var unorderedVertices = this.vertices; + this.vertices = []; + + if(unorderedVertices.length == 0) + return; + + var leftmostPoint = unorderedVertices[0]; + + for(var i = 1; i < unorderedVertices.length; i++) + if(unorderedVertices[i].x < leftmostPoint.x) + leftmostPoint = unorderedVertices[i]; + + function isLeft(a, b, c) + { + return ((b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x)) > 0; + } + + var currentPoint = leftmostPoint; + var endpoint = unorderedVertices[0]; + var i = 0; + do + { + this.vertices.push(currentPoint); + endpoint = unorderedVertices[0]; + + for(var j = 1; j < unorderedVertices.length; j++) + { + if((endpoint === currentPoint) || isLeft(currentPoint, endpoint, unorderedVertices[j])) + endpoint = unorderedVertices[j]; + } + + currentPoint = endpoint; + i++; + } + while(i < unorderedVertices.length * 2 && !(endpoint === this.vertices[0])); + + if(unorderedVertices.length != this.vertices.length) + console.log("Convex hull vertex sort did not work: " + unorderedVertices.length + " original points, result: " + this.vertices.length) } calculateVertices() @@ -157,7 +196,7 @@ class ConvexHull valid++; } - return valid > 2; + return valid > 2 && this.segments.length > 2; } // Returns two copies @@ -340,6 +379,8 @@ class Generator if(!bounded) continue; + + hulls[h].sortVertices(); var height = 0; pointsGeo.vertices.push(new THREE.Vector3( hulls[h].midpoint.x, height, hulls[h].midpoint.y)); From 65a0bf02f0935e717e95221dcef849f69785231a Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 02:32:56 -0500 Subject: [PATCH 12/21] + It was always burning --- src/building.js | 29 +++++++++++ src/city.js | 127 ++++++++++++++++++++++++++++++++++++++++-------- src/main.js | 8 +-- 3 files changed, 140 insertions(+), 24 deletions(-) diff --git a/src/building.js b/src/building.js index 96a2a7c2..66a2ac21 100644 --- a/src/building.js +++ b/src/building.js @@ -18,6 +18,8 @@ class BuildingLot { this.points = []; this.normals = []; + this.hasCap = false; + this.center = THREE.Vector2(0,0); } addPoint(x, y) @@ -28,6 +30,7 @@ class BuildingLot buildNormals() { var l = this.points.length; + this.center = new THREE.Vector2(0,0); for(var i = 0; i < l; i++) { @@ -46,7 +49,11 @@ class BuildingLot var n = n1.add(n2).multiplyScalar(.5); this.normals[i] = n.normalize(); + + this.center.add(p); } + + this.center.multiplyScalar(1.0 / l); } } @@ -122,9 +129,31 @@ class MassShape } } + // End the + if(this.lot.hasCap) + { + var center = this.lot.center; + var height = this.profile.points[this.profile.points.length - 1].y; + var vertex = new THREE.Vector3(center.x, height, center.y); + geometry.vertices.push(vertex); + + + for(var v = 0; v < boundaryVertexCount; v++) + { + + var v1 = offset + v; + var v2 = offset + ((v + 1) % boundaryVertexCount); + var v3 = geometry.vertices.length - 1; + + geometry.faces.push(new THREE.Face3(v1, v2, v3)); + } + } + geometry.mergeVertices(); geometry.computeFlatVertexNormals(); + material.side = THREE.DoubleSide; + var mesh = new THREE.Mesh(geometry, material); this.geometry = geometry; diff --git a/src/city.js b/src/city.js index 99a909fc..ff3012a8 100644 --- a/src/city.js +++ b/src/city.js @@ -2,6 +2,7 @@ const THREE = require('three'); const Random = require("random-js"); import * as Common from './common.js' +import * as Building from './building.js' class Voronoi { @@ -20,6 +21,7 @@ class ConvexHull this.segments = []; this.vertices = []; this.midpoint = new THREE.Vector2(0,0); + this.area = 0; } getCenter() @@ -39,6 +41,22 @@ class ConvexHull this.segments.push({ valid : false, normal: normal.clone(), dir: direction.clone(), midpoint: midpoint.clone(), min : 1000, max : -1000}); } + calculateArea() + { + var area = 0; + for(var i = 0; i < this.vertices.length; i++) + { + var nextIndex = (i+1) % this.vertices.length; + + var v = this.vertices[i]; + var next = this.vertices[nextIndex]; + + area += v.x * next.y - v.y * next.x; + } + + this.area = Math.abs(area) * .5; + } + // Sorts the vertices with the gift wrapping algorithm sortVertices() { @@ -80,6 +98,8 @@ class ConvexHull if(unorderedVertices.length != this.vertices.length) console.log("Convex hull vertex sort did not work: " + unorderedVertices.length + " original points, result: " + this.vertices.length) + + this.calculateArea(); } calculateVertices() @@ -278,12 +298,13 @@ class Generator // (super important each point is inside each cell) // Generate convex hulls // Subdivide convex hulls in X and Y, 9 times - build(scene) + // Extract sections from this subdivision + // Ignore border sections, as I have no time to deal with edge cases (no pun intended) + buildHulls(scene, random) { // count * count final points var count = 30; var scale = 2; - var random = new Random(Random.engines.mt19937().autoSeed()); var geometry = new THREE.Geometry(); var pointsGeo = new THREE.Geometry(); @@ -362,48 +383,54 @@ class Generator // Last cell cellBounds.push(new Common.Bounds(new THREE.Vector2( 7 * cellScale, 7 * cellScale ), new THREE.Vector2( 10 * cellScale, 10 * cellScale ))); + var boundedHulls = new Array(); + + for(var i = 0; i < 6; i++) + boundedHulls.push(new Array()); + // Save geo for display for(var h = 0; h < hulls.length; h++) { - if(!hulls[h].isValid()) + var hull = hulls[h]; + + if(!hull.isValid()) continue; - hulls[h].calculateVertices(); + hull.calculateVertices(); var bounded = false; for(var b = 0; b < cellBounds.length; b++) { - if(cellBounds[b].contains(hulls[h].midpoint)) + if(cellBounds[b].contains(hull.midpoint)) + { bounded = true; + boundedHulls[b].push(hull); + break; + } } if(!bounded) continue; - hulls[h].sortVertices(); + hull.sortVertices(); var height = 0; - pointsGeo.vertices.push(new THREE.Vector3( hulls[h].midpoint.x, height, hulls[h].midpoint.y)); + pointsGeo.vertices.push(new THREE.Vector3( hull.midpoint.x, height, hull.midpoint.y)); - for(var s = 0; s < hulls[h].segments.length; s++) + for(var i = 0; i < hull.vertices.length; i++) { - var segment = hulls[h].segments[s]; + var i1 = (i+1) % hull.vertices.length; + var v = hull.vertices[i]; + var v1 = hull.vertices[i1]; - if(!segment.valid) - continue; - - var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); - var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); - - geometry.vertices.push(new THREE.Vector3( from.x, height, from.y )) - geometry.vertices.push(new THREE.Vector3( to.x, height, to.y )) + geometry.vertices.push(new THREE.Vector3( v.x, height, v.y )); + geometry.vertices.push(new THREE.Vector3( v1.x, height, v1.y )); - pointsGeo.vertices.push(new THREE.Vector3( from.x, height, from.y)); - pointsGeo.vertices.push(new THREE.Vector3( to.x, height, to.y)); + pointsGeo.vertices.push(new THREE.Vector3( v.x, height, v.y )); } } - console.log(geometry.vertices.length); + // console.log(geometry.vertices.length); var lineMaterial = new THREE.LineBasicMaterial( {color: 0xffffff} ); var line = new THREE.LineSegments(geometry, lineMaterial); @@ -413,6 +440,66 @@ class Generator pointsMaterial.size = .1; var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); scene.add( pointsMesh ); + + + return boundedHulls; + } + + buildLots(hullContainer, random) + { + var lotContainer = []; + + for(var i = 0; i < hullContainer.length; i++) + { + lotContainer.push(new Array()); + var hulls = hullContainer[i]; + + for(var h = 0; h < hulls.length; h++) + { + var hull = hulls[h]; + + // If it is too small, no lot + // If it is medium sized, it can be ignored with a probability + if(hull.area > .5 && (random.real(0,1) > .1 || hull.area > 1.5)) + { + var lot = new Building.BuildingLot(); + + // Yes, directly reuse them ;) + lot.points = hull.vertices; + lot.buildNormals(); + lot.hasCap = true; + + lotContainer[i].push(lot); + } + } + } + + return lotContainer; + } + + build(scene) + { + var random = new Random(Random.engines.mt19937().autoSeed()); + var hulls = this.buildHulls(scene, random); + var lots = this.buildLots(hulls, random); + + var baseLotProfile = new Building.Profile(); + baseLotProfile.addPoint(1.1, 0.0); + baseLotProfile.addPoint(1.1, .025); + baseLotProfile.addPoint(1.2, .025); + baseLotProfile.addPoint(1.2, .075); + baseLotProfile.addPoint(1.3, .075); + + for(var i = 0; i < lots.length; i++) + { + for(var j = 0; j < lots[i].length; j++) + { + // The blocks profiles are also mass shapes + var shape = new Building.MassShape(lots[i][j], baseLotProfile); + var mesh = shape.generateMesh(); + scene.add(mesh); + } + } } } diff --git a/src/main.js b/src/main.js index 4a4a5c17..72c54bd6 100644 --- a/src/main.js +++ b/src/main.js @@ -76,12 +76,12 @@ function onLoad(framework) lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); } - var shape = new Building.MassShape(lot, profile); - var mesh = shape.generateMesh(); + // var shape = new Building.MassShape(lot, profile); + // var mesh = shape.generateMesh(); // scene.add(mesh); - var rule = new Building.Rule(); - rule.componentWise = true; + // var rule = new Building.Rule(); + // rule.componentWise = true; // rule.evaluate(shape, scene); Engine.rubik = new Rubik.Rubik(); From 267c3c41cb51a042e696ae2f8d6bce9fe14b9840 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 03:25:07 -0500 Subject: [PATCH 13/21] + Since the world's been turning --- src/city.js | 112 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/city.js b/src/city.js index ff3012a8..485c08a4 100644 --- a/src/city.js +++ b/src/city.js @@ -126,6 +126,10 @@ class ConvexHull var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min)); var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max)); + + // Cache these points for future reference... + segment.from = from; + segment.to = to; addVertex(from, this.vertices, this.midpoint); addVertex(to, this.vertices, this.midpoint); @@ -414,32 +418,32 @@ class Generator hull.sortVertices(); - var height = 0; - pointsGeo.vertices.push(new THREE.Vector3( hull.midpoint.x, height, hull.midpoint.y)); + // var height = 0; + // pointsGeo.vertices.push(new THREE.Vector3( hull.midpoint.x, height, hull.midpoint.y)); - for(var i = 0; i < hull.vertices.length; i++) - { - var i1 = (i+1) % hull.vertices.length; - var v = hull.vertices[i]; - var v1 = hull.vertices[i1]; + // for(var i = 0; i < hull.vertices.length; i++) + // { + // var i1 = (i+1) % hull.vertices.length; + // var v = hull.vertices[i]; + // var v1 = hull.vertices[i1]; - geometry.vertices.push(new THREE.Vector3( v.x, height, v.y )); - geometry.vertices.push(new THREE.Vector3( v1.x, height, v1.y )); + // geometry.vertices.push(new THREE.Vector3( v.x, height, v.y )); + // geometry.vertices.push(new THREE.Vector3( v1.x, height, v1.y )); - pointsGeo.vertices.push(new THREE.Vector3( v.x, height, v.y )); - } + // pointsGeo.vertices.push(new THREE.Vector3( v.x, height, v.y )); + // } } // console.log(geometry.vertices.length); - var lineMaterial = new THREE.LineBasicMaterial( {color: 0xffffff} ); - var line = new THREE.LineSegments(geometry, lineMaterial); - scene.add(line); + // var lineMaterial = new THREE.LineBasicMaterial( {color: 0xffffff} ); + // var line = new THREE.LineSegments(geometry, lineMaterial); + // scene.add(line); - var pointsMaterial = new THREE.PointsMaterial( { color: 0xffffff } ) - pointsMaterial.size = .1; - var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); - scene.add( pointsMesh ); + // var pointsMaterial = new THREE.PointsMaterial( { color: 0xffffff } ) + // pointsMaterial.size = .1; + // var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); + // scene.add( pointsMesh ); return boundedHulls; @@ -460,7 +464,7 @@ class Generator // If it is too small, no lot // If it is medium sized, it can be ignored with a probability - if(hull.area > .5 && (random.real(0,1) > .1 || hull.area > 1.5)) + if(hull.area > .75 && (random.real(0,1) > .1 || hull.area > 2)) { var lot = new Building.BuildingLot(); @@ -468,6 +472,7 @@ class Generator lot.points = hull.vertices; lot.buildNormals(); lot.hasCap = true; + lot.hull = hull; lotContainer[i].push(lot); } @@ -477,6 +482,59 @@ class Generator return lotContainer; } + generateMassShapesForLot(lot, random) + { + var hull = lot.hull; + var shapes = []; + + var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + var geometry = new THREE.BoxGeometry( 1, 1, 1 ); + + for(var s = 0; s < hull.segments.length; s++) + { + var segment = hull.segments[s]; + + if(!segment.valid) + continue; + + var from = segment.from; + var to = segment.to; + var segmentLength = from.distanceTo(to); + + if(segmentLength < .5) + continue; + + var count = random.integer(1, 3); + + for(var i = 0; i < count; i++) + { + // We dont want to overlap with other segments + var t = (i / (count + 1)); + var p = from.clone().lerp(to, t); + p.add(segment.normal.clone().multiplyScalar(-.4)); + p.add(segment.dir.clone().multiplyScalar(.5*segmentLength/count)); + + var normal = new THREE.Vector3( segment.normal.x, 0, segment.normal.y ); + + // Facing street + var faceLength = random.real(.7, .99) * .5 * segmentLength / count; + var depth = random.real(.2, .5); + var height = random.real(.2, 10.0 * depth * faceLength); // Height is dependent on depth+length + + p.add(segment.normal.clone().multiplyScalar(depth*-.5)); + + var cube = new THREE.Mesh( geometry, material ); + cube.scale.set(faceLength, height, depth); + cube.position.copy(new THREE.Vector3( p.x, cube.scale.y * .5, p.y )); + cube.lookAt(cube.position.clone().add(normal)); + + shapes.push(cube); + } + } + + return shapes; + } + build(scene) { var random = new Random(Random.engines.mt19937().autoSeed()); @@ -484,11 +542,11 @@ class Generator var lots = this.buildLots(hulls, random); var baseLotProfile = new Building.Profile(); - baseLotProfile.addPoint(1.1, 0.0); - baseLotProfile.addPoint(1.1, .025); - baseLotProfile.addPoint(1.2, .025); - baseLotProfile.addPoint(1.2, .075); - baseLotProfile.addPoint(1.3, .075); + baseLotProfile.addPoint(1.15, 0.0); + baseLotProfile.addPoint(1.15, .025); + baseLotProfile.addPoint(1.25, .025); + baseLotProfile.addPoint(1.25, .05); + baseLotProfile.addPoint(1.35, .05); for(var i = 0; i < lots.length; i++) { @@ -498,7 +556,13 @@ class Generator var shape = new Building.MassShape(lots[i][j], baseLotProfile); var mesh = shape.generateMesh(); scene.add(mesh); + + var massShapes = this.generateMassShapesForLot(lots[i][j], random); + + for(var s = 0; s < massShapes.length; s++) + scene.add(massShapes[s]); } + } } } From 3821ebf706b98e3ef0387e760333df2f928c1c8e Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 03:38:09 -0500 Subject: [PATCH 14/21] * Tweaked distribution of mass shapes so that they fall inside the lot boundaries. --- src/city.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/city.js b/src/city.js index 485c08a4..9e7407b2 100644 --- a/src/city.js +++ b/src/city.js @@ -504,22 +504,22 @@ class Generator if(segmentLength < .5) continue; - var count = random.integer(1, 3); + var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 4) + 1; for(var i = 0; i < count; i++) { // We dont want to overlap with other segments var t = (i / (count + 1)); - var p = from.clone().lerp(to, t); - p.add(segment.normal.clone().multiplyScalar(-.4)); + var p = from.clone().lerp(to, t * .8 + .1); + p.add(segment.normal.clone().multiplyScalar(-.25)); p.add(segment.dir.clone().multiplyScalar(.5*segmentLength/count)); var normal = new THREE.Vector3( segment.normal.x, 0, segment.normal.y ); // Facing street var faceLength = random.real(.7, .99) * .5 * segmentLength / count; - var depth = random.real(.2, .5); - var height = random.real(.2, 10.0 * depth * faceLength); // Height is dependent on depth+length + var depth = random.real(.2, .65); + var height = random.real(random.real(.2, 1), 6.0 * depth * faceLength); // Height is dependent on depth+length p.add(segment.normal.clone().multiplyScalar(depth*-.5)); @@ -528,7 +528,18 @@ class Generator cube.position.copy(new THREE.Vector3( p.x, cube.scale.y * .5, p.y )); cube.lookAt(cube.position.clone().add(normal)); - shapes.push(cube); + var intersects = false; + for(var j = 0; j < shapes.length; j++) + { + if(shapes[j].position.distanceTo(cube.position) < faceLength) + { + intersects = true; + break; + } + } + + if(!intersects) + shapes.push(cube); } } From 10e9e40b68a088d1be64e708dfbacd2a3f006444 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 06:19:27 -0500 Subject: [PATCH 15/21] + This was a triumph --- src/city.js | 146 +++++++++++++++++++++++++++++++++++++++++---------- src/main.js | 9 ++-- src/rubik.js | 123 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 237 insertions(+), 41 deletions(-) diff --git a/src/city.js b/src/city.js index 9e7407b2..8591ed90 100644 --- a/src/city.js +++ b/src/city.js @@ -373,23 +373,10 @@ class Generator console.log("Hulls: " + hulls.length); var cellScale = count * scale / 11; - var cellBounds = []; - - // Top cell - cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 1 * cellScale ), new THREE.Vector2( 7 * cellScale, 4 * cellScale ))); - cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 7 * cellScale ), new THREE.Vector2( 7 * cellScale, 10 * cellScale ))); - - // Horizontal cells - cellBounds.push(new Common.Bounds(new THREE.Vector2( 1 * cellScale, 4 * cellScale ), new THREE.Vector2( 4 * cellScale, 7 * cellScale ))); - cellBounds.push(new Common.Bounds(new THREE.Vector2( 4 * cellScale, 4 * cellScale ), new THREE.Vector2( 7 * cellScale, 7 * cellScale ))); - cellBounds.push(new Common.Bounds(new THREE.Vector2( 7 * cellScale, 4 * cellScale ), new THREE.Vector2( 10 * cellScale, 7 * cellScale ))); - - // Last cell - cellBounds.push(new Common.Bounds(new THREE.Vector2( 7 * cellScale, 7 * cellScale ), new THREE.Vector2( 10 * cellScale, 10 * cellScale ))); - + var cellBounds = this.getSubcellRemapping(cellScale); var boundedHulls = new Array(); - for(var i = 0; i < 6; i++) + for(var i = 0; i < cellBounds.length; i++) boundedHulls.push(new Array()); // Save geo for display @@ -408,6 +395,7 @@ class Generator if(cellBounds[b].contains(hull.midpoint)) { bounded = true; + hull.cellIndex = b; boundedHulls[b].push(hull); break; } @@ -445,8 +433,50 @@ class Generator // var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial ); // scene.add( pointsMesh ); + return { hulls: boundedHulls, cells : cellBounds }; + } + + getSubcellRemapping(cellScale) + { + var cellBounds = []; + + function addSubcells() + { + for(var x = 0; x < 3; x++) + { + for(var y = 0; y < 3; y++) + { + var from = new THREE.Vector2( (offset.x + x) * cellScale, (offset.y + y) * cellScale ); + var to = from.clone().add(new THREE.Vector2(cellScale, cellScale)); + + cellBounds.push(new Common.Bounds(from, to)); + } + } + } + + // Top cell + var offset = new THREE.Vector2( 4, 1 ); + addSubcells(); + + // Down cell + offset = new THREE.Vector2( 4, 7 ); + addSubcells(); + + // // Horizontal cells + offset = new THREE.Vector2( 1, 4 ); + addSubcells(); + + offset = new THREE.Vector2( 4, 4 ); + addSubcells(); - return boundedHulls; + offset = new THREE.Vector2( 7, 4 ); + addSubcells(); + + // // Last cell + offset = new THREE.Vector2( 7, 7 ); + addSubcells(); + + return cellBounds; } buildLots(hullContainer, random) @@ -482,6 +512,39 @@ class Generator return lotContainer; } + getMassShapeLot(position, faceLength, depth, height) + { + var lot = new Building.BuildingLot(); + var subdivs = 8; + + for(var i = 0; i < subdivs; i++) + { + var a = i * Math.PI * 2 / subdivs; + var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5; + lot.addPoint(Math.cos(a) * r * .5, Math.sin(a) * r * .5); + } + + lot.hasCap = true; + + return lot; + } + + getMassShapeProfile(position, faceLength, depth, height) + { + var profile = new Building.Profile(); + profile.addPoint(1.0, 0.0); + profile.addPoint(1.0, 1.0); + + profile.addPoint(.9, 1.0); + profile.addPoint(.9, 1.1); + profile.addPoint(.8, 1.1); + profile.addPoint(.8, 1.0); + + profile.addPoint(0.7, 1.0); + + return profile; + } + generateMassShapesForLot(lot, random) { var hull = lot.hull; @@ -504,7 +567,7 @@ class Generator if(segmentLength < .5) continue; - var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 4) + 1; + var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 4 * segmentLength) + 1; for(var i = 0; i < count; i++) { @@ -523,15 +586,22 @@ class Generator p.add(segment.normal.clone().multiplyScalar(depth*-.5)); - var cube = new THREE.Mesh( geometry, material ); - cube.scale.set(faceLength, height, depth); - cube.position.copy(new THREE.Vector3( p.x, cube.scale.y * .5, p.y )); - cube.lookAt(cube.position.clone().add(normal)); + var position = new THREE.Vector3( p.x, 0, p.y ); + var massLot = this.getMassShapeLot(position, faceLength, depth, height); + var massProfile = this.getMassShapeProfile(position, faceLength, depth, height); + + var shape = new Building.MassShape(massLot, massProfile) + var shapeMesh = shape.generateMesh(); + + // var cube = new THREE.Mesh( geometry, material ); + shapeMesh.scale.set(faceLength, height, depth); + shapeMesh.position.copy(position); + shapeMesh.lookAt(shapeMesh.position.clone().add(normal)); var intersects = false; for(var j = 0; j < shapes.length; j++) { - if(shapes[j].position.distanceTo(cube.position) < faceLength) + if(shapes[j].position.distanceTo(shapeMesh.position) < .5 * faceLength) { intersects = true; break; @@ -539,7 +609,7 @@ class Generator } if(!intersects) - shapes.push(cube); + shapes.push(shapeMesh); } } @@ -549,32 +619,50 @@ class Generator build(scene) { var random = new Random(Random.engines.mt19937().autoSeed()); - var hulls = this.buildHulls(scene, random); + + var hullData = this.buildHulls(scene, random); + var hulls = hullData.hulls; + var cellBounds = hullData.cells; var lots = this.buildLots(hulls, random); var baseLotProfile = new Building.Profile(); baseLotProfile.addPoint(1.15, 0.0); baseLotProfile.addPoint(1.15, .025); - baseLotProfile.addPoint(1.25, .025); - baseLotProfile.addPoint(1.25, .05); - baseLotProfile.addPoint(1.35, .05); + baseLotProfile.addPoint(1.2, .025); + baseLotProfile.addPoint(1.2, .035); + baseLotProfile.addPoint(1.35, .035); + + var cityBlocks = []; for(var i = 0; i < lots.length; i++) { + var group = new THREE.Group(); + for(var j = 0; j < lots[i].length; j++) { // The blocks profiles are also mass shapes var shape = new Building.MassShape(lots[i][j], baseLotProfile); var mesh = shape.generateMesh(); - scene.add(mesh); + group.add(mesh); var massShapes = this.generateMassShapesForLot(lots[i][j], random); for(var s = 0; s < massShapes.length; s++) - scene.add(massShapes[s]); + group.add(massShapes[s]); } + var center = cellBounds[i].min.clone().add(cellBounds[i].max).multiplyScalar(.5); + group.position.set(-center.x, 0, -center.y); + + var g2 = new THREE.Group(); + g2.add(group); + g2.userData = { index: i, offset: center } + cityBlocks.push(g2); + + // scene.add(g2); } + + return cityBlocks; } } diff --git a/src/main.js b/src/main.js index 72c54bd6..5eea0dcb 100644 --- a/src/main.js +++ b/src/main.js @@ -86,7 +86,7 @@ function onLoad(framework) Engine.rubik = new Rubik.Rubik(); var rubikMesh = Engine.rubik.build(); - // scene.add(rubikMesh); + scene.add(rubikMesh); // Init Engine stuff Engine.scene = scene; @@ -97,14 +97,17 @@ function onLoad(framework) var random = new Random(Random.engines.mt19937().seed(2545)); - var speed = .15; + var speed =1.15; var city = new City.Generator(); - city.build(scene); + var cityBlocks = city.build(scene); + + Engine.rubik.attachShapesToFace(cityBlocks); var callback = function() { Engine.rubik.animate(random.integer(0, 2), random.integer(0, 2), speed, callback); + // Engine.rubik.animate(1,0, speed, callback); }; Engine.rubik.animate(0, 0, speed, callback); diff --git a/src/rubik.js b/src/rubik.js index fdb02e22..413484e4 100644 --- a/src/rubik.js +++ b/src/rubik.js @@ -85,7 +85,7 @@ class Rubik var tMatrix = new THREE.Matrix4(); var sMatrix = new THREE.Matrix4(); var rMatrix = new THREE.Matrix4(); - sMatrix.makeScale(.9, .9, .9); + // sMatrix.makeScale(.9, .9, .9); for(var i = 0; i < 3; i++) { @@ -102,7 +102,7 @@ class Rubik matrix = rMatrix.clone(); matrix.multiply(tMatrix); - matrix.multiply(sMatrix); + matrix.multiply(cube.userData.rotationAccum); cube.applyMatrix(matrix); } @@ -111,11 +111,13 @@ class Rubik // If the rotation is full, then we need to update the data // structure var tmpArray = []; + var shapeArray = []; if(fequals(degrees, -90)) { for(var x = 0; x < 3; x++) { tmpArray.push(new Array()); + shapeArray.push(new Array()); for(var y = 0; y < 3; y++) { @@ -129,6 +131,7 @@ class Rubik for(var x = 0; x < 3; x++) { tmpArray.push(new Array()); + shapeArray.push(new Array()); for(var y = 0; y < 3; y++) { @@ -145,7 +148,10 @@ class Rubik for(var y = 0; y < 3; y++) { var p = indexFunction(x, y, plane); - this.segments[p.x][p.y][p.z] = tmpArray[x][y]; + + var segment = tmpArray[x][y]; + segment.userData.rotationAccum.premultiply(rMatrix); + this.segments[p.x][p.y][p.z] = segment; } // We swapped @@ -155,6 +161,103 @@ class Rubik return false; } + // Assumes userData = { index, offset} + attachShapesToFace(shapes) + { + for(var i = 0; i < shapes.length; i++) + { + var shape = shapes[i]; + var index = shape.userData.index; + var offset = shape.userData.offset; + + var overallScale = .55 / 3; + + // Front face + if(index >= 27 && index < 36) + { + var x = (index - 27) % 3; + var y = Math.floor((index - 27) / 3); + var z = 0; + + shape.rotateX(Math.PI * -.5); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + + // Top face + if(index >= 0 && index < 9) + { + var x = index % 3; + var y = 2; + var z = Math.floor(index / 3); + + // shape.rotateX(Math.PI * -.5); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + + // Down face + if(index >= 9 && index < 18) + { + var x = (index - 9) % 3; + var y = 0; + var z = Math.floor((index - 9) / 3); + + shape.rotateX(Math.PI * -1); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + + // Left face + if(index >= 18 && index < 27) + { + var x = 2; + var y = (index - 18) % 3; + var z = Math.floor((index - 18) / 3); + + shape.rotateZ(Math.PI * -.5); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + + // Right face + if(index >= 36 && index < 45) + { + var x = 0; + var y = (index - 36) % 3; + var z = Math.floor((index - 36) / 3); + + shape.rotateZ(Math.PI * .5); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + + // Last face + if(index >= 45 && index < 54) + { + var x = (index - 45) % 3; + var y = Math.floor((index - 45) / 3); + var z = 2; + + shape.rotateX(Math.PI * .5); + shape.translateY(.5); + shape.scale.set(overallScale, overallScale, overallScale); + this.segments[x][y][z].add(shape); + this.segments[x][y][z].userData.shape = shape; + } + } + } + build() { var container = new THREE.Object3D(); @@ -175,21 +278,23 @@ class Rubik for(var z = 0; z < 3; z++) { + var colorDebugging = z; var mat = new THREE.MeshLambertMaterial( {color: 0x000000} ); - mat.color = new THREE.Color( (y + z + .5) / 2, 0.0, 0.0 ); + mat.color = new THREE.Color(1, 0.0, 0.0 ); - if(x == 1) - mat.color = new THREE.Color( 0.0, (y + z + .5) / 2, 0.0 ); - else if(x == 2) - mat.color = new THREE.Color(0.0, 0.0, (y + z + .5) / 2); + if(colorDebugging == 1) + mat.color = new THREE.Color( 0.0, 1, 0.0 ); + else if(colorDebugging == 2) + mat.color = new THREE.Color(0.0, 0.0, 1); var cube = new THREE.Mesh( boxGeo, mat ); cube.position.copy(new THREE.Vector3( x - 1, y - 1, z - 1)); - cube.scale.copy(new THREE.Vector3( .9, .9, .9 )) + // cube.scale.copy(new THREE.Vector3( .9, .9, .9 )) container.add(cube); // Index is x * 3 * 3 + y * 3 + z this.segments[x][y].push(cube); + cube.userData = { rotationAccum : new THREE.Matrix4() }; } } } From 3c5cee0d35f2c20aab67ccc584d7b6205a62add1 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 07:08:33 -0500 Subject: [PATCH 16/21] + I'm making a note here: --- src/city.js | 27 ++++++++++++++++++--------- src/main.js | 19 ++++++++++--------- src/rubik.js | 18 +++++++++--------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/city.js b/src/city.js index 8591ed90..213e097c 100644 --- a/src/city.js +++ b/src/city.js @@ -494,7 +494,7 @@ class Generator // If it is too small, no lot // If it is medium sized, it can be ignored with a probability - if(hull.area > .75 && (random.real(0,1) > .1 || hull.area > 2)) + // if(hull.area > .5 && (random.real(0,1) > .1 || hull.area > 2)) { var lot = new Building.BuildingLot(); @@ -503,6 +503,7 @@ class Generator lot.buildNormals(); lot.hasCap = true; lot.hull = hull; + lot.park = (hull.area < .5 || (random.real(0,1) < .1 && hull.area < 2)); lotContainer[i].push(lot); } @@ -547,6 +548,9 @@ class Generator generateMassShapesForLot(lot, random) { + if(lot.park) + return []; + var hull = lot.hull; var shapes = []; @@ -567,7 +571,7 @@ class Generator if(segmentLength < .5) continue; - var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 4 * segmentLength) + 1; + var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 3 * segmentLength) + 1; for(var i = 0; i < count; i++) { @@ -580,9 +584,9 @@ class Generator var normal = new THREE.Vector3( segment.normal.x, 0, segment.normal.y ); // Facing street - var faceLength = random.real(.7, .99) * .5 * segmentLength / count; - var depth = random.real(.2, .65); - var height = random.real(random.real(.2, 1), 6.0 * depth * faceLength); // Height is dependent on depth+length + var faceLength = random.real(.7, .99) * .6 * segmentLength / count; + var depth = random.real(.4, .7); + var height = random.real(random.real(.1, .2), THREE.Math.clamp(2.0 * depth * faceLength, .4, 4)); // Height is dependent on depth+length p.add(segment.normal.clone().multiplyScalar(depth*-.5)); @@ -638,19 +642,26 @@ class Generator { var group = new THREE.Group(); + var geometryBatch = new THREE.Geometry(); + for(var j = 0; j < lots[i].length; j++) { // The blocks profiles are also mass shapes var shape = new Building.MassShape(lots[i][j], baseLotProfile); var mesh = shape.generateMesh(); - group.add(mesh); + geometryBatch.mergeMesh(mesh) var massShapes = this.generateMassShapesForLot(lots[i][j], random); for(var s = 0; s < massShapes.length; s++) - group.add(massShapes[s]); + geometryBatch.mergeMesh(massShapes[s]) } + var batchMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + batchMaterial.side = THREE.DoubleSide; + var batchedMesh = new THREE.Mesh(geometryBatch, batchMaterial); + group.add(batchedMesh); + var center = cellBounds[i].min.clone().add(cellBounds[i].max).multiplyScalar(.5); group.position.set(-center.x, 0, -center.y); @@ -658,8 +669,6 @@ class Generator g2.add(group); g2.userData = { index: i, offset: center } cityBlocks.push(g2); - - // scene.add(g2); } return cityBlocks; diff --git a/src/main.js b/src/main.js index 5eea0dcb..323a22b0 100644 --- a/src/main.js +++ b/src/main.js @@ -35,24 +35,26 @@ function onLoad(framework) var gui = framework.gui; var stats = framework.stats; + renderer.setClearColor(new THREE.Color(.4, .75, .95), 1); + // 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); + directionalLight.color = new THREE.Color(.9, .9, 1 ); + directionalLight.position.set(-10, 10, 10); scene.add(directionalLight); - // initialize a simple box and material var directionalLight2 = new THREE.DirectionalLight( 0xffffff, 1 ); - directionalLight2.color.setHSL(0.1, 1, 0.95); - directionalLight2.position.set(-1, -3, -2); + directionalLight2.color = new THREE.Color(.4, .4, .7); + directionalLight2.position.set(-1, -3, 2); directionalLight2.position.multiplyScalar(10); scene.add(directionalLight2); // set camera position - camera.position.set(9, 7, 9); + camera.position.set(40, 40, 40); camera.lookAt(new THREE.Vector3(0,0,0)); + camera.fov = 5; + camera.updateProjectionMatrix(); var profile = new Building.Profile(); profile.addPoint(1.0, 0.0); @@ -97,8 +99,7 @@ function onLoad(framework) var random = new Random(Random.engines.mt19937().seed(2545)); - var speed =1.15; - + var speed = .45; var city = new City.Generator(); var cityBlocks = city.build(scene); diff --git a/src/rubik.js b/src/rubik.js index 413484e4..81f55e83 100644 --- a/src/rubik.js +++ b/src/rubik.js @@ -264,8 +264,6 @@ class Rubik var boxGeo = new THREE.BoxGeometry( 1, 1, 1 ); var planeGeo = new THREE.PlaneGeometry(1, 1, 1, 1 ); - var blackMaterial = new THREE.MeshLambertMaterial( {color: 0x777777} ); - this.segments = [] for(var x = 0; x < 3; x++) @@ -279,13 +277,15 @@ class Rubik for(var z = 0; z < 3; z++) { var colorDebugging = z; - var mat = new THREE.MeshLambertMaterial( {color: 0x000000} ); - mat.color = new THREE.Color(1, 0.0, 0.0 ); - - if(colorDebugging == 1) - mat.color = new THREE.Color( 0.0, 1, 0.0 ); - else if(colorDebugging == 2) - mat.color = new THREE.Color(0.0, 0.0, 1); + var mat = new THREE.MeshPhongMaterial( {color: 0x888888} ); + mat.shininess = 5; + mat.specular = new THREE.Color(.2,.2,.3); + // mat.color = new THREE.Color(1, 0.0, 0.0 ); + + // if(colorDebugging == 1) + // mat.color = new THREE.Color( 0.0, 1, 0.0 ); + // else if(colorDebugging == 2) + // mat.color = new THREE.Color(0.0, 0.0, 1); var cube = new THREE.Mesh( boxGeo, mat ); cube.position.copy(new THREE.Vector3( x - 1, y - 1, z - 1)); From 6ff0c22324b20d9e381275c533026babe28bad20 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 07:56:38 -0500 Subject: [PATCH 17/21] + Huge success? --- src/building.js | 131 ++++++++++++++++++++++++++++++++++++++++++++++-- src/city.js | 10 ++-- src/main.js | 2 +- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/src/building.js b/src/building.js index 66a2ac21..31a8ce0a 100644 --- a/src/building.js +++ b/src/building.js @@ -11,6 +11,129 @@ class Shape } } +class BuildingFactory +{ + constructor() + { + this.lots = []; + + this.build(); + } + + build() + { + this.lots.push(this.buildHLot()); + this.lots.push(this.buildTLot()); + this.lots.push(this.buildLLot()); + this.lots.push(this.buildCLot()); + this.lots.push(this.buildSquareLot()); + + for(var i = 0; i < this.lots.length; i++) + this.lots[i].buildNormals(); + } + + getLotForShape(random, faceLength, depth, height) + { + if(random.real(0,1) < .2 || (height > 2 && random.real(0,1) < .6)) + { + // Circular + var subdivs = random.integer(4, 15); + var displ = random.real(0, 1); + + var lot = new BuildingLot(); + + for(var i = 0; i < subdivs; i++) + { + var a = i * Math.PI * 2 / subdivs; + var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 * displ; + lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); + } + + return lot; + } + + return this.lots[random.integer(0, this.lots.length - 1)]; + } + + buildSquareLot() + { + var lot = new BuildingLot(); + + lot.addPoint(-1, -1); + lot.addPoint(1, -1); + lot.addPoint(1, 1); + lot.addPoint(-1, 1); + + return lot; + } + + buildCLot() + { + var lot = new BuildingLot(); + + lot.addPoint(-1, -1); + lot.addPoint(1, -1); + lot.addPoint(1, -.5); + lot.addPoint(0, -.5); + lot.addPoint(0, .5); + lot.addPoint(1, .5); + lot.addPoint(1, 1); + lot.addPoint(-1, 1); + + return lot; + } + + buildLLot() + { + var lot = new BuildingLot(); + + lot.addPoint(-1, -1); + lot.addPoint(0, -1); + lot.addPoint(0, 0); + lot.addPoint(1, 0); + lot.addPoint(1, 1); + lot.addPoint(-1, 1); + + return lot; + } + + buildTLot() + { + var lot = new BuildingLot(); + + lot.addPoint(-1, -1); + lot.addPoint(0, -1); + lot.addPoint(0, -.5); + lot.addPoint(1, -.5); + lot.addPoint(1, .5); + lot.addPoint(0, .5); + lot.addPoint(0, 1); + lot.addPoint(-1, 1); + + return lot; + } + + buildHLot() + { + var lot = new BuildingLot(); + + lot.addPoint(-1, -1); + lot.addPoint(-.5, -1); + lot.addPoint(-.5, -.5); + lot.addPoint(.5, -.5); + lot.addPoint(.5, -1); + lot.addPoint(1, -1); + + lot.addPoint(1, 1); + lot.addPoint(.5, 1); + lot.addPoint(.5, .5); + lot.addPoint(-.5, .5); + lot.addPoint(-.5, 1); + + return lot; + } +} + // The object that defines the boundaries of the mass shape class BuildingLot { @@ -18,7 +141,7 @@ class BuildingLot { this.points = []; this.normals = []; - this.hasCap = false; + this.hasCap = true; this.center = THREE.Vector2(0,0); } @@ -63,6 +186,7 @@ class Profile constructor() { this.points = []; + this.scale = 1.0; // Specifically, height } addPoint(x, y) @@ -129,7 +253,7 @@ class MassShape } } - // End the + // End the last row of vertices into a cap if(this.lot.hasCap) { var center = this.lot.center; @@ -137,7 +261,6 @@ class MassShape var vertex = new THREE.Vector3(center.x, height, center.y); geometry.vertices.push(vertex); - for(var v = 0; v < boundaryVertexCount; v++) { @@ -235,4 +358,4 @@ class ShapeBuilder } -export {Shape, Rule, BuildingLot, Profile, MassShape} \ No newline at end of file +export {Shape, Rule, BuildingLot, Profile, MassShape, BuildingFactory} \ No newline at end of file diff --git a/src/city.js b/src/city.js index 213e097c..72d2e1ed 100644 --- a/src/city.js +++ b/src/city.js @@ -546,7 +546,7 @@ class Generator return profile; } - generateMassShapesForLot(lot, random) + generateMassShapesForLot(lot, random, factory) { if(lot.park) return []; @@ -591,14 +591,14 @@ class Generator p.add(segment.normal.clone().multiplyScalar(depth*-.5)); var position = new THREE.Vector3( p.x, 0, p.y ); - var massLot = this.getMassShapeLot(position, faceLength, depth, height); + var massLot = factory.getLotForShape(random, faceLength, depth, height);// this.getMassShapeLot(position, faceLength, depth, height); var massProfile = this.getMassShapeProfile(position, faceLength, depth, height); var shape = new Building.MassShape(massLot, massProfile) var shapeMesh = shape.generateMesh(); // var cube = new THREE.Mesh( geometry, material ); - shapeMesh.scale.set(faceLength, height, depth); + shapeMesh.scale.set(faceLength * .5, height, depth * .5); shapeMesh.position.copy(position); shapeMesh.lookAt(shapeMesh.position.clone().add(normal)); @@ -624,6 +624,8 @@ class Generator { var random = new Random(Random.engines.mt19937().autoSeed()); + + var factory = new Building.BuildingFactory(); var hullData = this.buildHulls(scene, random); var hulls = hullData.hulls; var cellBounds = hullData.cells; @@ -651,7 +653,7 @@ class Generator var mesh = shape.generateMesh(); geometryBatch.mergeMesh(mesh) - var massShapes = this.generateMassShapesForLot(lots[i][j], random); + var massShapes = this.generateMassShapesForLot(lots[i][j], random, factory); for(var s = 0; s < massShapes.length; s++) geometryBatch.mergeMesh(massShapes[s]) diff --git a/src/main.js b/src/main.js index 323a22b0..421c6946 100644 --- a/src/main.js +++ b/src/main.js @@ -111,7 +111,7 @@ function onLoad(framework) // Engine.rubik.animate(1,0, speed, callback); }; - Engine.rubik.animate(0, 0, speed, callback); + // Engine.rubik.animate(0, 0, speed, callback); } // called on frame updates From b9f1e76f1ab61e502183d397699201ed27bfd5da Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 08:10:04 -0500 Subject: [PATCH 18/21] [...] You just keep on trying --- src/building.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++++- src/city.js | 20 ++---------- src/main.js | 2 +- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/building.js b/src/building.js index 31a8ce0a..a566c9af 100644 --- a/src/building.js +++ b/src/building.js @@ -16,6 +16,7 @@ class BuildingFactory constructor() { this.lots = []; + this.profiles = []; this.build(); } @@ -30,6 +31,88 @@ class BuildingFactory for(var i = 0; i < this.lots.length; i++) this.lots[i].buildNormals(); + + this.profiles.push(this.buildSimpleProfile()); + this.profiles.push(this.buildExtremeProfile()); + } + + getProfileForShape(random, faceLength, depth, height) + { + if(height > .75 || random.real(0,1) < .05) + { + var floors = random.integer(1, 7 * height); + var profile = new Profile(); + + profile.addPoint(1.05, 0.0); + profile.addPoint(1.05, 0.05); + profile.addPoint(1.0, 0.05); + + for(var i = 0; i < floors; i++) + { + var height = (i / floors); + + // Floor separator + profile.addPoint(1.0, height - .025); + profile.addPoint(1.1, height - .025); + profile.addPoint(1.1, height + .025); + profile.addPoint(1.0, height + .025); + } + + profile.addPoint(1.0, 1.0); + + profile.addPoint(.9, 1.0); + profile.addPoint(.9, 1.1); + profile.addPoint(.8, 1.1); + profile.addPoint(.8, 1.0); + + profile.addPoint(0.7, 1.0); + + return profile; + } + + // Almost always simple case + if(random.real(0,1) > .15) + return this.profiles[0]; + + // Extremee + return this.profiles[1]; + } + + buildSimpleProfile() + { + var profile = new Profile(); + + profile.addPoint(1.05, 0.0); + profile.addPoint(1.05, 0.05); + profile.addPoint(1.0, 0.05); + profile.addPoint(1.0, 1.0); + + profile.addPoint(.9, 1.0); + profile.addPoint(.9, 1.1); + profile.addPoint(.8, 1.1); + profile.addPoint(.8, 1.0); + + profile.addPoint(0.7, 1.0); + + return profile; + } + + buildExtremeProfile() + { + var profile = new Profile(); + profile.addPoint(.5, 0.0); + profile.addPoint(.5, .2); + profile.addPoint(.2, .2); + profile.addPoint(.2, 1.0); + + profile.addPoint(.9, 1.0); + profile.addPoint(.9, 1.1); + profile.addPoint(.8, 1.1); + profile.addPoint(.8, 1.0); + + profile.addPoint(0.7, 1.0); + + return profile; } getLotForShape(random, faceLength, depth, height) @@ -45,7 +128,7 @@ class BuildingFactory for(var i = 0; i < subdivs; i++) { var a = i * Math.PI * 2 / subdivs; - var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 * displ; + var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * displ; lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); } diff --git a/src/city.js b/src/city.js index 72d2e1ed..21432479 100644 --- a/src/city.js +++ b/src/city.js @@ -503,7 +503,7 @@ class Generator lot.buildNormals(); lot.hasCap = true; lot.hull = hull; - lot.park = (hull.area < .5 || (random.real(0,1) < .1 && hull.area < 2)); + lot.park = (hull.area < .55 || (random.real(0,1) < .1 && hull.area < 2)); lotContainer[i].push(lot); } @@ -530,22 +530,6 @@ class Generator return lot; } - getMassShapeProfile(position, faceLength, depth, height) - { - var profile = new Building.Profile(); - profile.addPoint(1.0, 0.0); - profile.addPoint(1.0, 1.0); - - profile.addPoint(.9, 1.0); - profile.addPoint(.9, 1.1); - profile.addPoint(.8, 1.1); - profile.addPoint(.8, 1.0); - - profile.addPoint(0.7, 1.0); - - return profile; - } - generateMassShapesForLot(lot, random, factory) { if(lot.park) @@ -592,7 +576,7 @@ class Generator var position = new THREE.Vector3( p.x, 0, p.y ); var massLot = factory.getLotForShape(random, faceLength, depth, height);// this.getMassShapeLot(position, faceLength, depth, height); - var massProfile = this.getMassShapeProfile(position, faceLength, depth, height); + var massProfile = factory.getProfileForShape(random, faceLength, depth, height); var shape = new Building.MassShape(massLot, massProfile) var shapeMesh = shape.generateMesh(); diff --git a/src/main.js b/src/main.js index 421c6946..323a22b0 100644 --- a/src/main.js +++ b/src/main.js @@ -111,7 +111,7 @@ function onLoad(framework) // Engine.rubik.animate(1,0, speed, callback); }; - // Engine.rubik.animate(0, 0, speed, callback); + Engine.rubik.animate(0, 0, speed, callback); } // called on frame updates From 1a4dc1003608997a5a0f43dd23521fba1fd17a79 Mon Sep 17 00:00:00 2001 From: Mariano Merchante Date: Fri, 17 Feb 2017 08:11:08 -0500 Subject: [PATCH 19/21] * Fix --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index e609adf4..c8e72fdc 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ - HW2: LSystems + Shape Grammars