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..936da3fa 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,8 @@ - -# 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. +# Shape Grammars + +Disclaimer: this is not finished as expected, because of the unexpected complexities of convex hull operations. +However it does have a procedural city with different rules for mass volumes (lot shape and profile), as described in the original paper. + +It does not (for now) use any per-component rule (such as windows); although it is implemented, more time is needed to build the obj library. + + diff --git a/deploy.js b/deploy.js new file mode 100644 index 00000000..9defe7c3 --- /dev/null +++ b/deploy.js @@ -0,0 +1,38 @@ +var colors = require('colors'); +var path = require('path'); +var git = require('simple-git')(__dirname); +var deploy = require('gh-pages-deploy'); +var packageJSON = require('require-module')('./package.json'); + +var success = 1; +git.fetch('origin', 'master', function(err) { + if (err) throw err; + git.status(function(err, status) { + if (err) throw err; + if (!status.isClean()) { + success = 0; + console.error('Error: You have uncommitted changes! Please commit them first'.red); + } + + if (status.current !== 'master') { + success = 0; + console.warn('Warning: Please deploy from the master branch!'.yellow) + } + + git.diffSummary(['origin/master'], function(err, diff) { + if (err) throw err; + + if (diff.files.length || diff.insertions || diff.deletions) { + success = 0; + console.error('Error: Current branch is different from origin/master! Please push all changes first'.red) + } + + if (success) { + var cfg = packageJSON['gh-pages-deploy'] || {}; + var buildCmd = deploy.getFullCmd(cfg); + deploy.displayCmds(deploy.getFullCmd(cfg)); + deploy.execBuild(buildCmd, cfg); + } + }) + }) +}) \ No newline at end of file diff --git a/images/hull_intersection.png b/images/hull_intersection.png new file mode 100644 index 00000000..754c7eba Binary files /dev/null and b/images/hull_intersection.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..c8e72fdc --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + Shape Grammars + + + + + + 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/building.js b/src/building.js new file mode 100644 index 00000000..a566c9af --- /dev/null +++ b/src/building.js @@ -0,0 +1,444 @@ +const THREE = require('three'); + +class Shape +{ + constructor(size) + { + this.children = []; + this.active = true; + this.size = size; + this.hasComponents = false; + } +} + +class BuildingFactory +{ + constructor() + { + this.lots = []; + this.profiles = []; + + 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(); + + 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) + { + 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) * 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 +{ + constructor() + { + this.points = []; + this.normals = []; + this.hasCap = true; + this.center = THREE.Vector2(0,0); + } + + addPoint(x, y) + { + this.points.push(new THREE.Vector2(x, y)); + } + + buildNormals() + { + var l = this.points.length; + this.center = new THREE.Vector2(0,0); + + 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(); + + this.center.add(p); + } + + this.center.multiplyScalar(1.0 / l); + } +} + +// The profile of an extrusion +class Profile +{ + constructor() + { + this.points = []; + this.scale = 1.0; // Specifically, height + } + + addPoint(x, y) + { + this.points.push(new THREE.Vector2(x, y)); + } +} + +class MassShape +{ + constructor(lot, profile) + { + this.lot = lot; + this.profile = profile; + } + + generateMesh() + { + this.lot.buildNormals(); + + var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 }); + var geometry = new THREE.Geometry(); + + var boundaryVertexCount = this.lot.points.length; + var offset = 0; + + for(var i = 0; i < this.profile.points.length; i++) + { + 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 = this.lot.points[j]; + var boundaryNormal = this.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; + } + } + + // End the last row of vertices into a cap + 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; + this.mesh = mesh; + return mesh; + } +} + +class Rule +{ + 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); + } + } +} + +class ShapeBuilder +{ + constructor() + { + this.shapes = []; + this.iterations = 5; + } + +} + +export {Shape, Rule, BuildingLot, Profile, MassShape, BuildingFactory} \ No newline at end of file diff --git a/src/city.js b/src/city.js new file mode 100644 index 00000000..21432479 --- /dev/null +++ b/src/city.js @@ -0,0 +1,664 @@ +const THREE = require('three'); +const Random = require("random-js"); + +import * as Common from './common.js' +import * as Building from './building.js' + +class Voronoi +{ + constructor() + { + + } +} + +// 2D only! +class ConvexHull +{ + constructor() + { + this.bounds = null; + this.segments = []; + this.vertices = []; + this.midpoint = new THREE.Vector2(0,0); + this.area = 0; + } + + 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(normal, direction, midpoint, min, max) + { + 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() + { + 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) + + this.calculateArea(); + } + + 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)); + + // Cache these points for future reference... + segment.from = from; + segment.to = to; + + 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; + + 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); + + this.bounds.encapsulate(from); + this.bounds.encapsulate(to); + } + } + } + + calculateSegments() + { + for(var i = 0; i < this.segments.length; i++) + { + for(var j = 0; j < this.segments.length; j++) + { + if(i == j) + continue; + + var s1 = this.segments[i]; + var s2 = this.segments[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; + + // 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; + } + } + + if(!insideHull) + continue; + + s1.min = Math.min(s1.min, u); + s1.max = Math.max(s1.max, u); + s1.valid = true; + } + } + + this.updateBounds(); + } + + isValid() + { + var valid = 0; + + for(var h = 0; h < this.segments.length; h++) + { + if(this.segments[h].valid) + valid++; + } + + return valid > 2 && this.segments.length > 2; + } + + // 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; + + h1.addSegment(axis, tangent, offset); + h2.addSegment(axis.clone().negate(), tangent.clone().negate(), offset); + + for(var h = 0; h < this.segments.length; h++) + { + var segment = this.segments[h]; + + // Some pruning + if(segment.valid) + { + h1.addSegment(segment.normal, segment.dir, segment.midpoint); + h2.addSegment(segment.normal, segment.dir, segment.midpoint); + } + } + + h1.calculateSegments(); + h2.calculateSegments(); + + return [h1, h2]; + } +} + +class Generator +{ + constructor() + { + } + + sliceHullSet(hulls, axis, subdivisions, scale, intersectionFunction) + { + var newHulls = []; + + for(var h = 0; h < hulls.length; h++) + { + var hull = hulls[h]; + var intersected = false; + + for(var x = 0; x < subdivisions + 1 && !intersected; x++) + { + var t = x / subdivisions; + var sliceOffset = t * scale; + + 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]); + + intersected = true; + } + } + + if(!intersected) + 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 + // 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 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 = .499; + + // Distribute points + for(var x = 0; x < count; x++) + { + points.push(new Array()); + + for(var y = 0; y < count; y++) + { + 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); + } + } + + // Build half planes, and finding their convex hulls + var hulls = []; + + for(var x = 0; x < count; x++) + { + for(var y = 0; y < count; y++) + { + 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++) + { + 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 ); + hull.addSegment(normal, tangent, midpoint); + } + } + } + + hull.calculateSegments(); + hulls.push(hull); + } + } + + // 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); + + var cellScale = count * scale / 11; + var cellBounds = this.getSubcellRemapping(cellScale); + var boundedHulls = new Array(); + + for(var i = 0; i < cellBounds.length; i++) + boundedHulls.push(new Array()); + + // Save geo for display + for(var h = 0; h < hulls.length; h++) + { + var hull = hulls[h]; + + if(!hull.isValid()) + continue; + + hull.calculateVertices(); + + var bounded = false; + for(var b = 0; b < cellBounds.length; b++) + { + if(cellBounds[b].contains(hull.midpoint)) + { + bounded = true; + hull.cellIndex = b; + boundedHulls[b].push(hull); + break; + } + } + + if(!bounded) + continue; + + hull.sortVertices(); + + // 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]; + + // 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 )); + // } + } + + // console.log(geometry.vertices.length); + + // 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 ); + + 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(); + + offset = new THREE.Vector2( 7, 4 ); + addSubcells(); + + // // Last cell + offset = new THREE.Vector2( 7, 7 ); + addSubcells(); + + return cellBounds; + } + + 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 > 2)) + { + var lot = new Building.BuildingLot(); + + // Yes, directly reuse them ;) + lot.points = hull.vertices; + lot.buildNormals(); + lot.hasCap = true; + lot.hull = hull; + lot.park = (hull.area < .55 || (random.real(0,1) < .1 && hull.area < 2)); + + lotContainer[i].push(lot); + } + } + } + + 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; + } + + generateMassShapesForLot(lot, random, factory) + { + if(lot.park) + return []; + + 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 = Math.floor(Math.pow(random.real(0, 1), 2.0) * 3 * segmentLength) + 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 * .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) * .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)); + + 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 = factory.getProfileForShape(random, 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 * .5, height, depth * .5); + 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(shapeMesh.position) < .5 * faceLength) + { + intersects = true; + break; + } + } + + if(!intersects) + shapes.push(shapeMesh); + } + } + + return shapes; + } + + build(scene) + { + 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; + 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.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(); + + 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(); + geometryBatch.mergeMesh(mesh) + + var massShapes = this.generateMassShapesForLot(lots[i][j], random, factory); + + for(var s = 0; s < massShapes.length; 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); + + var g2 = new THREE.Group(); + g2.add(group); + g2.userData = { index: i, offset: center } + cityBlocks.push(g2); + } + + return cityBlocks; + } +} + +export {Generator} \ No newline at end of file diff --git a/src/common.js b/src/common.js new file mode 100644 index 00000000..f5da52ab --- /dev/null +++ b/src/common.js @@ -0,0 +1,38 @@ +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; + } + + 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 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/main.js b/src/main.js new file mode 100644 index 00000000..323a22b0 --- /dev/null +++ b/src/main.js @@ -0,0 +1,134 @@ +const THREE = require('three'); +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 = +{ + 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; + var camera = framework.camera; + var renderer = framework.renderer; + 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 = 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 = 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(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); + 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); + profile.addPoint(0.6, 2.0); + profile.addPoint(0.0, 2.0); + + var lot = new Building.BuildingLot(); + var subdivs = 25; + 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 + 1.0 ; + lot.addPoint(Math.cos(a) * r, Math.sin(a) * r); + } + + // 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(); + var rubikMesh = Engine.rubik.build(); + scene.add(rubikMesh); + + // 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 = .45; + + var city = new City.Generator(); + 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); +} + +// called on frame updates +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 +Framework.init(onLoad, onUpdate); diff --git a/src/rubik.js b/src/rubik.js new file mode 100644 index 00000000..81f55e83 --- /dev/null +++ b/src/rubik.js @@ -0,0 +1,308 @@ +const THREE = require('three'); + +function fequals(a, b) +{ + return Math.abs(a-b) < .001; +} + +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) + { + 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) + { + 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) + { + return 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(cube.userData.rotationAccum); + + cube.applyMatrix(matrix); + } + } + + // 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++) + { + 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()); + shapeArray.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); + + var segment = tmpArray[x][y]; + segment.userData.rotationAccum.premultiply(rMatrix); + this.segments[p.x][p.y][p.z] = segment; + } + + // We swapped + return true; + } + + 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(); + var boxGeo = new THREE.BoxGeometry( 1, 1, 1 ); + var planeGeo = new THREE.PlaneGeometry(1, 1, 1, 1 ); + + 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 colorDebugging = z; + 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)); + // 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() }; + } + } + } + + return container; + } + + +} + +export {Rubik} \ 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