diff --git a/README.md b/README.md index 5ca56be..54f3658 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ + +# Stauffer Notes + +The l-system is updated/redrawn only when the iterations or axiom are changed. +Otherwise it was updating too frequently and I didn't get around to adding +the fix that was offered to delay updates. + +The 'Anim period' is period that controls cylinders' widths scaling based +on their color index value. The color index value serves as a proxy for iteration count, since +I couldn't find a quick way to get iteration info into the turtle mesh objects. + +I added these turtle rendering symbols: + +> X/x - Increment/Decrement subsequent rotation angles around X +> Y/y - Increment/Decrement subsequent rotation angles around Y +> Z/z - Increment/Decrement subsequent rotation angles around Z +> C/C - Increment/Decrement the state color value (a 1D value that indexes a ramp) + + +================================================================================ + + The objective of this assignment is to create an L System parser and generate interesting looking plants. Start by forking and then cloning this repository: [https://github.com/CIS700-Procedural-Graphics/Project3-LSystems](https://github.com/CIS700-Procedural-Graphics/Project3-LSystems) # L-System Parser @@ -64,4 +86,4 @@ Design a grammar for a new procedural plant! As the preceding parts of this assi # Publishing Your code -Running `npm run deploy` will automatically build your project and push it to gh-pages where it will be visible at `username.github.io/repo-name`. NOTE: You MUST commit AND push all changes to your MASTER branch before doing this or you may lose your work. The `git` command must also be available in your terminal or command prompt. If you're using Windows, it's a good idea to use Git Bash. \ No newline at end of file +Running `npm run deploy` will automatically build your project and push it to gh-pages where it will be visible at `username.github.io/repo-name`. NOTE: You MUST commit AND push all changes to your MASTER branch before doing this or you may lose your work. The `git` command must also be available in your terminal or command prompt. If you're using Windows, it's a good idea to use Git Bash. diff --git a/src/lsystem.js b/src/lsystem.js index e643b6d..5b0db13 100644 --- a/src/lsystem.js +++ b/src/lsystem.js @@ -1,3 +1,5 @@ +const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much + // A class that represents a symbol replacement rule to // be used when expanding an L-system grammar. function Rule(prob, str) { @@ -7,36 +9,138 @@ function Rule(prob, str) { // TODO: Implement a linked list class and its requisite functions // as described in the homework writeup +export class Node { + constructor(character, prev, next){ + this.character = character; + this.prev = prev; + this.next = next; + } +} +export class LinkedList { + constructor() { + this.head = null; + } + append( node ){ + if( this.head === null ){ + this.head = node; + return; + } + var n = this.head; + var done = false; + while( ! done ) { + if( n.next === null ){ + n.next = node; + return; + } + n = n.next; + } + } +} + +//Find the last node in a list of node, not necessarily +// a LinkedList +export function getLastNode( firstNode ) { + var lastNode = firstNode; + while( lastNode.next !== null ) + lastNode = lastNode.next; + return lastNode; +} + +//Append node B to node A +//Returns A +export function appendNode( A, B ){ + A.next = B; + B.prev = A; + return A; +} + +// Take a string and make linked nodes, +// but not a full LinkedList. +// Returns the first node +export function stringToNodes(input_string){ + var prev = null; + var first = null + for( var i=0; i < input_string.length; i++ ){ + var node = new Node( input_string.charAt(i), prev, null ); + if( first === null ) + //Begin of list + first = node; + else + //Point predecessor to this new node + prev.next = node; + //point the node to its predecessor + node.prev = prev; + //set up for next iteration + prev = node; + } + return first; +} // TODO: Turn the string into linked list -export function stringToLinkedList(input_string) { +export function StringToLinkedList(input_string) { // ex. assuming input_string = "F+X" // you should return a linked list where the head is // at Node('F') and the tail is at Node('X') var ll = new LinkedList(); + ll.head = stringToNodes(input_string); return ll; } // TODO: Return a string form of the LinkedList -export function linkedListToString(linkedList) { +export function LinkedListToString(linkedList) { // ex. Node1("F")->Node2("X") should be "FX" var result = ""; + var node = linkedList.head; + while( node !== null ){ + result += node.character; + node = node.next; + } return result; } // TODO: Given the node to be replaced, // insert a sub-linked-list that represents replacementString function replaceNode(linkedList, node, replacementString) { + var firstNewNode = stringToNodes( replacementString ); + firstNewNode.prev = node.prev; + var next = null; + if( linkedList.head === node ){ + next = node.next; + linkedList.head = firstNewNode; + } else { + node.prev.next = firstNewNode; + next = node.next; + } + //point to the old node's next node + var lastNewNode = getLastNode( firstNewNode ); + lastNewNode.next = next; + if( next !== null ) + next.prev = lastNewNode; } export default function Lsystem(axiom, grammar, iterations) { // default LSystem - this.axiom = "FX"; + this.axiom = "FA"; this.grammar = {}; - this.grammar['X'] = [ - new Rule(1.0, '[-FX][+FX]') + this.grammar['A'] = [ + new Rule(0.55, '[YY-CFA][YY+CFA]'), + new Rule(0.25, '[yyy--CFA][y++CFA]'), + new Rule(0.2, '[ZZccXA]' ) + ]; + this.grammar['B'] = [ + new Rule(0.0, ''), + new Rule(0.0, ''), + new Rule(0.0, '' ) + ]; + this.grammar['C'] = [ + new Rule(0.0, ''), + new Rule(0.0, ''), + new Rule(0.0, '' ) ]; - this.iterations = 0; + this.iterations = 10; + this.prevIterations = -1; + + this.startingRotations = [10,0,0]; // Set up the axiom string if (typeof axiom !== "undefined") { @@ -64,13 +168,103 @@ export default function Lsystem(axiom, grammar, iterations) { } } + this.getRandom = function(x,y){ + var vec = new THREE.Vector2(x,y); + return Math.abs( ( Math.sin( vec.dot( new THREE.Vector2(12.9898,78.233))) * 43758.5453 ) % 1 ); + } + + //Normalize rule probabilities + this.normalizeRuleProbabilities = function () { + for (var property in this.grammar) { + if (this.grammar.hasOwnProperty(property)) { + // do stuff + var ruleArr = this.grammar[property]; + var sum = 0; + for( var i = 0; i < ruleArr.length; i++ ) { + sum += ruleArr[i].probability; + }; + if( sum > 0) + for( var i = 0; i < ruleArr.length; i++ ) { + ruleArr[i].probability /= sum; + }; + } + } + + } + //For this node's character, check if there's an expansion for it + // and return the string. Otherwise just return ''. + this.getRuleExpansion = function( node, seed1, seed2 ){ + if( this.grammar[node.character] === undefined ){ + //console.log('getRuleExp: no expansion for ', node.character); + return ''; + } + //console.log('getRuleExp: expanding ', node.character); + + var date = new Date(); + seed1 += (date.getTime() % 7); + seed2 -= (date.getTime() % 11); + + //We've got a match. Choose rule variation based on probability. + //Assumes cumulative probability w/in a rule character is 1.0 + var rand = this.getRandom(seed1, seed2); // [0,1], hopefully nicely distributed + var cutoff = 0; + var result = ''; + var ruleArr = this.grammar[node.character]; + //NOTE: don't use array.ForEach here, cuz we can't break out of that loop + for( var i = 0; i < ruleArr.length; i++ ) { + var element = ruleArr[i]; + cutoff += element.probability; + if( rand <= cutoff ){ + //Make a list of linked nodes from the string + //console.log('getRuleExp - matched - element: ', element); + result = element.successorString; + //console.log('getRuleExp: chose rule ', result, ' rand, cutoff: ', rand, cutoff ); + break; + } + }; + + //should only get here when total probs for a rule set don't sum to 1.0 + if( result == '' && cutoff >= 1.0 ) + //alert('getRuleExpansion...oops!'); + console.log('WARNING: getRuleExpansion...oops! No result. cutoff, rand: ', cutoff, rand); + return result; + } + // TODO // This function returns a linked list that is the result // of expanding the L-system's axiom n times. // The implementation we have provided you just returns a linked // list of the axiom. - this.doIterations = function(n) { + this.doIterations = function(numIts) { + //Get the linked list representing the axom var lSystemLL = StringToLinkedList(this.axiom); + /*console.log('axiom: ', this.axiom); + console.log('lSystemLL ', lSystemLL ); + console.log('head ', lSystemLL.head ); + console.log('last ', getLastNode(lSystemLL.head) );*/ + + //Normalize the rule probabilities + this.normalizeRuleProbabilities(); + + //Run through N interations of expanding rules + //0 iterations is just the axiom, so for n==1 we just do one iteration + for( var iter = 1; iter <= numIts; iter++ ){ + for( var node = lSystemLL.head; node !== null; node=node.next ){ + //For this node's character, check if there's an expansion for it + // and return the new node list for it. Otherwise just return this node. + var string = this.getRuleExpansion( node, iter+1, numIts ); + //console.log('doIts: string: ', string); + if( string != '' ) + replaceNode(lSystemLL, node, string); + //debug + //var str = LinkedListToString(lSystemLL); + //console.log('iteration ', iter, ' of ', numIts); + //console.log('lsys ll to string: ', str ); + } + } + + var str = LinkedListToString(lSystemLL); + console.log('Final lsys ll to string: ', str ); return lSystemLL; } } \ No newline at end of file diff --git a/src/main.js b/src/main.js index f0c6600..fdfc2fc 100644 --- a/src/main.js +++ b/src/main.js @@ -22,26 +22,61 @@ function onLoad(framework) { scene.add(directionalLight); // set camera position - camera.position.set(1, 1, 2); - camera.lookAt(new THREE.Vector3(0,0,0)); + camera.position.set(20, 14, 4); + camera.lookAt(new THREE.Vector3(0,9,0)); // initialize LSystem and a Turtle to draw var lsys = new Lsystem(); - turtle = new Turtle(scene); - + turtle = new Turtle(scene, lsys.startingRotations); + framework.turtle = turtle; + + //period of color-based segment width cycling + framework.colorPeriod = 3000; + + ////// gui + gui.add(camera, 'fov', 0, 180).onChange(function(newVal) { camera.updateProjectionMatrix(); }); + gui.add(framework, 'colorPeriod', 100, 12000).name('Anim Period (msec)'); + gui.add(lsys, 'axiom').onChange(function(newVal) { - lsys.UpdateAxiom(newVal); + lsys.updateAxiom(newVal); + clearScene(turtle); + console.log('in gui onChange axiom'); doLsystem(lsys, lsys.iterations, turtle); }); gui.add(lsys, 'iterations', 0, 12).step(1).onChange(function(newVal) { + if( lsys.prevIterations == lsys.iterations ) + return; //avoid running this twice per gui change. don't know why it happens. clearScene(turtle); + //console.log('in gui onChange iterations'); doLsystem(lsys, newVal, turtle); + lsys.prevIterations = lsys.iterations; + }); + + gui.add(lsys.startingRotations, '0',0,60).name('Init Rot X'); + gui.add(lsys.startingRotations, '1',0,60).name('Init Rot Y'); + gui.add(lsys.startingRotations, '2',0,60).name('Init Rot Z'); + + var rules=['A','B','C']; + rules.forEach( function(el) { + var ruleArr = lsys.grammar[el]; + var f = gui.addFolder('Rule '+el); + for( var i=0; i < ruleArr.length; i++ ){ + var name = 'Sub '+i; + f.add(ruleArr[i], 'successorString').name(name); + f.add(ruleArr[i], 'probability', 0.0, 1.0).step(0.1).name(name+' prob'); + } }); + + + //generate! + clearScene(turtle); + doLsystem(lsys, lsys.iterations, turtle); + } // clears the scene by removing all geometries added by turtle.js @@ -54,14 +89,41 @@ function clearScene(turtle) { } function doLsystem(lsystem, iterations, turtle) { - var result = lsystem.DoIterations(iterations); + console.log('=========== doLsystem ============ '); + clearScene(turtle); + var result = lsystem.doIterations(iterations); turtle.clear(); - turtle = new Turtle(turtle.scene); + turtle = new Turtle(turtle.scene, lsystem.startingRotations); + //turtle.startingRotations = lsystem.startingRotations; //hack this in here turtle.renderSymbols(result); } // called on frame updates function onUpdate(framework) { + if( turtle === undefined ) + return; + + var date = new Date(); + var time = date.getTime(); + var period = framework.colorPeriod; + var phase = (time % period ) / period * 2 * 3.1415; + + //traverse nodes and change width + turtle.scene.traverse( function (node ) { + if ( node instanceof THREE.Mesh ) { + //node.stateColor is a hack for getting in some iteration state info in here + var thisPhase = phase + ( node.stateColor * 2 * 3.1415 ); + var scale = Math.sin( thisPhase ) + 1; + scale = scale / 6 + 0.2; + node.scale.set( scale, 1, scale ); + } + } ); +/* var x = Math.cos( phase ) * 4; + var z = Math.sin (phase ) * 4; + //console.log(time, phase, x, z); + framework.camera.position.set( x, 0, z ); + framework.camera.lookAt(new THREE.Vector3(0,0,0)); + framework.camera.updateProjectionMatrix();*/ } // when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate diff --git a/src/turtle.js b/src/turtle.js index 1db2723..7cf1061 100644 --- a/src/turtle.js +++ b/src/turtle.js @@ -4,36 +4,62 @@ const THREE = require('three') // 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) { +var TurtleState = function(pos, dir, color/* val [0,1]*/, rotArr ) { + var rotCopy = []; + rotCopy[0] = rotArr[0]; + rotCopy[1] = rotArr[1]; + rotCopy[2] = rotArr[2]; + return { pos: new THREE.Vector3(pos.x, pos.y, pos.z), - dir: new THREE.Vector3(dir.x, dir.y, dir.z) + dir: new THREE.Vector3(dir.x, dir.y, dir.z), + color: color, + rot: rotCopy } } export default class Turtle { - constructor(scene, grammar) { - this.state = new TurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(0,1,0)); + constructor(scene, startingRotations, grammar) { + //this.state = new TurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(0,1,0), 0, [30,0,0]); this.scene = scene; - + this.rotStep = 4; + // 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) + '+' : this.rotateTurtle.bind(this, 1), + '-' : this.rotateTurtle.bind(this, -1), + 'X' : this.rotateChange.bind(this, [this.rotStep,0,0]), + 'x' : this.rotateChange.bind(this, [-this.rotStep,0,0]), + 'Y' : this.rotateChange.bind(this, [0,this.rotStep,0]), + 'y' : this.rotateChange.bind(this, [0,-this.rotStep,0]), + 'Z' : this.rotateChange.bind(this, [0,0,this.rotStep]), + 'z' : this.rotateChange.bind(this, [0,0,-this.rotStep]), + 'F' : this.makeCylinder.bind(this, 2, 0.1), + '[' : this.pushState.bind(this), + ']' : this.popState.bind(this), + 'C' : this.changeColor.bind(this, 0.8), + 'c' : this.changeColor.bind(this, -0.8), }; } else { this.renderGrammar = grammar; } + + this.stateStack = []; + this.startingRotations = startingRotations; + //console.log('turtle ctor about to call clear'); + this.clear(); //sets new state } + // 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)); + //console.log('clear: startingRots: ', this.startingRotations ); + this.state = new TurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(0,1,0), 0, this.startingRotations ); + this.stateStack = []; } // A function to help you debug your turtle functions @@ -43,16 +69,40 @@ export default class Turtle { console.log(this.state.dir) } + // Copy a state to stack + pushState() { + //Note that TurtleState makes new Vector3 obj's from the input, as best I can tell + this.stateStack.push( new TurtleState( this.state.pos, this.state.dir, this.state.color, this.state.rot ) ); + + } + // Pop the state stack, return ref + popState() { + this.state = this.stateStack.pop(); + } + + //Change the state color value (single float) by amount + changeColor( amount ) { + this.state.color += amount; + this.state.color %= 1; + //console.log('changeColor: new: ', this.state.color); + } + // Rotate the turtle's _dir_ vector by each of the // Euler angles indicated by the input. - rotateTurtle(x, y, z) { + rotateTurtle(sign) { + //console.log('rotateTurtle: state.rot ', this.state.rot, ' sign ', sign, ' x ', x); var e = new THREE.Euler( - x * 3.14/180, - y * 3.14/180, - z * 3.14/180); + this.state.rot[0] * sign * 3.14/180, + this.state.rot[1] * sign * 3.14/180, + this.state.rot[2] * sign * 3.14/180); this.state.dir.applyEuler(e); } + rotateChange( change ) { + for( var i = 0; i < change.length; i++ ) + this.state.rot[i] += change[i]; + } + // Translate the turtle along the input vector. // Does NOT change the turtle's _dir_ vector moveTurtle(x, y, z) { @@ -66,12 +116,36 @@ export default class Turtle { this.state.pos.add(newVec); }; + // Get a color based on [0,1] range. + // Return a THREE.Color obj + getColor( colorValIn ) { + //Simply color ramp. + //Domain [0,0.5] goes from min to max, and [0.5,1]] goes max to min + // to allow it to cycle if you just keep incrementing colorVal + var colorVal = Math.abs(colorValIn) % 1; + if( colorVal <= 0.5 ) + colorVal *=2; + else + colorVal = 1 - colorVal; + + //simple color ramp + var r = Math.pow( colorVal, 1 ); + var g = Math.pow( colorVal, 3 ); + var b = 1 - Math.pow( colorVal, 1); + + return new THREE.Color( r, g, b ); + } + // 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 material = new THREE.MeshBasicMaterial( {color: 0x11cc11} ); + material.color = this.getColor( this.state.color); + var cylinder = new THREE.Mesh( geometry, material ); + cylinder.stateColor = this.state.color % 1; this.scene.add( cylinder ); //Orient the cylinder to the turtle's current direction