Skip to content

Instantly share code, notes, and snippets.

@crsnbrt
Last active December 23, 2015 15:21
Show Gist options
  • Save crsnbrt/94e1ea268d07feecc146 to your computer and use it in GitHub Desktop.
Save crsnbrt/94e1ea268d07feecc146 to your computer and use it in GitHub Desktop.
ojVWdR
ul.controls
li: button#info Info
li: button#reset Generate
li: button#options Options
.info.module.hidden
p Change some options and regenerate the city!
.options.module.hidden
section
h3 Map
.row
.col.col-label: label(for="block_count") Grid Count
.col.col-input: input(type="number", name="block_count", id="block_count", min="1", max="50", value="10")
.row
.col.col-label: label(for="block_size") Grid Size
.col.col-input: input(type="number", name="block_size", id="block_size", min="10", max="300", value="100", step="10")
.row
.col.col-label: label(for="water_threshold") Water Threshold
.col.col-input: input(type="number", name="water_threshold", id="water_threshold", min="0.0", max="1.0", value="0.1", step="0.1")
.row
.col.col-label: label(for="tree_threshold") Park Threshold
.col.col-input: input(type="number", name="tree_threshold", id="tree_threshold", min="0.0", max="1.0", value="0.1", step="0.1")
.row
.col.col-label: label(for="noise_frequency") Noise Frequency
.col.col-input: input(type="number", name="noise_frequency", id="noise_frequency", min="0", max="50", value="8")
section
h3 Buildings
.row
.col.col-label: label(for="build_min_h") Min Height
.col.col-input: input(type="number", name="build_min_h", id="build_min_h", min="5", max="1000", value="20")
.row
.col.col-label: label(for="build_max_h") Max Height
.col.col-input: input(type="number", name="build_max_h", id="build_max_h", min="5", max="1000", value="300")
.row
.col.col-label: label(for="subdiv") Subdivisions
.col.col-input: input(type="number", name="subdiv", id="subdiv", min="0", max="4", value="2")
.row
.col.col-label: label(for="build_exp") Height Exponent
.col.col-input: input(type="number", name="build_exp", id="build_exp", min="1", max="10", value="6")
section
h3 Other
.row
.col.col-label: label(for="max_cars") Max Cars
.col.col-input: input(type="number", name="max_cars", id="max_cars", min="1", max="100", value="50")
.row
.col.col-label: label(for="preserve_seed") Preserve Seed
.col.col-input: input(type="checkbox", name="preserve_seed", id="preserve_seed")
.row
.col.col-label: label(for="debug") Debug
.col.col-input: input(type="checkbox", name="debug", id="debug")
section
h3 Direct Url
input#url(type="text", readonly)
.row.buttons
button#default Defaults
var scene, camera, renderer, width, height, controls,
light, shadowLight, backLight, composer, stats,
earth, bedrock, streets, building, heightmap,
carSpawner, trainSpawner, geom, mesh, trunk,
bridges, leaves, leaves2, tree, material, curb,
colors, city, _city, inputs, i, j, watermap, water;
//=========================================
// SETTINGS
//=========================================
var DEBUG = false;
colors = {
BUILDING: 0xE8E8E8,
//BUILDING: 0xD5E2E6,
//BUILDING: 0xE3E6D5,
GROUND: 0x81A377,
TREE: 0x216E41,
WHITE: 0xffffff,
BLACK: 0x000000,
DARK_BROWN: 0x545247,
LIGHT_BROWN: 0x736B5C,
GREY: 0x999999,
WATER: 0x4B95DE,
TRAIN: 0x444444,
CARS:[
0xCC4E4E,
//0xD6CA3E
]
};
//basic city options
city = {
//height of bedrock layer
base: 40,
//depth of the water and earth layers
water_height: 20,
//block size (w&l)
block: 100,
//num blocks x
blocks_x: 10,
//num blocks z
blocks_z: 10,
//road width
road_w: 16,
//curb height
curb_h: 2,
//block slices
subdiv: 2,
//sidewalk width
inner_block_margin: 5,
//max building height
build_max_h: 300,
//min building height
build_min_h: 20,
//deviation for height within block
block_h_dev: 10,
//exponent of height increase
build_exp: 6,
//chance of blocks being water
water_threshold: 0.1,
//chance of block containg trees
tree_threshold: 0.2,
//max trees per block
tree_max: 20,
//max bridges
bridge_max: 1,
//beight heaight
bridge_h: 25,
//max cars at one time
car_max: 10,
//train max
train_max: 1,
//maximum car speed
car_speed_min: 2,
//minimum car speed
car_speed_max: 3,
//train speed
train_speed: 4,
//noise factor, increase for smoother noise
noise_frequency: 8,
//seed for generating noise
//seed: Math.random()
seed: Math.random()
}
city.width = city.block * city.blocks_x;
city.length = city.block * city.blocks_z;
//store default options
_city = _.clone(city);
//map html input fields to city object values
inputs = [
{field: "block_count", setting: "blocks_x"},
{field: "block_count", setting: "blocks_z"},
{field: "block_size", setting: "block"},
{field: "subdiv", setting: "subdiv"},
{field: "build_max_h", setting: "build_max_h"},
{field: "build_min_h", setting: "build_min_h", },
{field: "water_threshold", setting: "water_threshold"},
{field: "tree_threshold", setting: "tree_threshold"},
{field: "noise_frequency", setting: "noise_frequency"},
{field: "max_cars", setting: "car_max"},
{field: "build_exp", setting: "build_exp"},
];
//=========================================
// SETUP
//=========================================
//create scene, renderer, and camera
function setupScene() {
width = window.innerWidth;
height = window.innerHeight;
var ratio = width / height;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({alpha: true,antialias: true, premultipliedAlpha: false });
renderer.setSize(width, height);
renderer.shadowMapEnabled = true;
renderer.shadowMapType = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera( 60, ratio, 1, 4000);
camera.position.set(500, 500, 500);
camera.lookAt(new THREE.Vector3(0, 0, 0));
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.maxPolarAngle = Math.PI/2
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
if(DEBUG) document.body.appendChild( stats.domElement );
window.addEventListener('resize', resize, false);
}
//window resize event
function resize() {
width = window.innerWidth;
height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
//create lights
function setupLights() {
light = new THREE.HemisphereLight(colors.WHITE, colors.WHITE, .5);
shadowLight = new THREE.DirectionalLight(colors.WHITE, .3);
shadowLight.position.set(city.width/2, 800, city.length/2);
//debug mesh at light location
if(DEBUG)(scene.add(getBoxMesh(colors.WHITE, 20, 20, 20, city.width/2, 800, city.length/2, false)));
shadowLight.castShadow = true;
shadowLight.shadowDarkness = .2;
backLight = new THREE.DirectionalLight(colors.WHITE, .1);
backLight.position.set(-100, 200, 50);
backLight.shadowDarkness = .1;
scene.add(backLight, shadowLight, light);
}
//generate normalized hightmap array from perlin noise
function setupHeightmap(){
noise.seed(city.seed);
//the heightmap and watermap are different to
//allow for different frequency levels for buildings and water
heightmap = [];
watermap = [];
//build the hightmap array from the perlin noise
for (i = 0; i < city.blocks_x; i++) {
for (j = 0; j < city.blocks_z; j++) {
heightmap.push(getNoiseValue(i, j));
watermap.push(getNoiseValue(i, j, 10));
}
}
// normalize and convert to 2d array
heightmap = normalizeArray(heightmap);
heightmap = _.chunk(heightmap, city.blocks_x);
watermap = normalizeArray(watermap);
watermap = _.map(watermap, function(n){ return n>=city.water_threshold ? 1 : 0; });
watermap = _.chunk(watermap, city.blocks_x);
}
//create ground meshes
function setupGround() {
var street_h = city.curb_h*2;
var earth_meshes = [];
var street_meshes = [];
//lowest ground Layer
bedrock = getBoxMesh(colors.LIGHT_BROWN, city.width, city.base, city.length);
bedrock.position.y = (-(city.base/2) - city.water_height - street_h);
bedrock.receiveShadow = false;
//water layer
water = getWaterMesh(city.width-2, city.water_height, city.length-2);
water.position.y = -(city.water_height/2) - street_h;
for(i=0;i<watermap.length;i++){
for(j=0;j<watermap[0].length;j++){
if(watermap[i][j]){
var x = ((city.block*i) + city.block/2) - city.width/2;
var z = ((city.block*j) + city.block/2) - city.length/2;
earth_meshes.push(getBoxMesh(colors.DARK_BROWN, city.block, city.water_height, city.block, x, (-(city.water_height/2) - street_h), z))
street_meshes.push(getBoxMesh(colors.GREY, city.block, street_h, city.block, x, -(street_h/2), z))
}
}
}
if(street_meshes.length){ scene.add(mergeMeshes(street_meshes)) }
if(earth_meshes.length){ scene.add(mergeMeshes(earth_meshes, false)) }
scene.add(bedrock, water);
}
//setup a single block
function setupBlocks(){
for (var i = 0; i < city.blocks_x; i++) {
for (var j = 0; j < city.blocks_z; j++) {
if(watermap[i][j]){
//var p = getPointVectorFromMap(i, j);
var x = ((city.block*i) + city.block/2) - city.width/2;
var z = ((city.block*j) + city.block/2) - city.length/2;
//get values from heightmap array
var hm = heightmap[i][j];
//get building height for block
var h = mapToRange(hm, city.build_min_h, city.build_max_h, city.build_exp);
//max possible distance from center of block
var w = city.block-city.road_w;
//with inner block margins
var inner = w-(city.inner_block_margin*2);
//create curb mesh
var curb_color = DEBUG ? getGreyscaleColor(hm) : colors.GROUND
curb = getBoxMesh(curb_color, w, city.curb_h, w);
curb.position.set(x, city.curb_h/2, z);
scene.add(curb);
//create buildings in debug mode the building color is mapped to the hightmap
if(hm > city.tree_threshold) {
var building_color = DEBUG ? getGreyscaleColor(hm) : colors.BUILDING;
setupBuildings(x, z, inner, inner, h, city.subdiv, building_color);
}
//create tree meshes
else{ setupPark(x, z, inner, inner); }
}
}
}
}
//create park
function setupPark(x, z, w, l){
var trees = [];
for(var i=0; i<getRandInt(0, city.tree_max);i++){
var tree_x = getRandInt(x-w/2, x+w/2);
var tree_z = getRandInt(z-l/2, z+l/2);
trees.push(new Tree(tree_x, tree_z).group);
}
//merge trees for this block into single mesh
if(trees.length) scene.add(mergeMeshes(trees));
}
//recursively create buildings return array of meshes
function setupBuildings(x, z, w, l, h, sub, color, buildings, cb){
//array of buildings for this block
buildings = buildings || [];
var depth = Math.pow(2, city.subdiv);
var tall = Math.round((h/city.build_max_h)*100) > 90
var slice_deviation = 15;
//really tall buildings take the whole block
if(sub<1 || tall){
building = new Building({
h: getRandInt(h-city.block_h_dev, h+city.block_h_dev),
w: w,
l: l,
x: x,
z: z,
tall:tall,
color: color
});
buildings.push(building.group);
//add all buildings in this block to scene as a single mesh
if(buildings.length >= depth || tall){
scene.add(mergeMeshes(buildings));
}
}
else{
//recursively slice the block until num of subdivisions met
//TODO: simplify this
var dir = (w==l) ? chance(50) : w>l;
if(dir){
var offset = Math.abs(getRandInt(0, slice_deviation));
var between = (city.inner_block_margin/2);
var half = w/2;
var x_prime = x + offset;
var w1 = Math.abs((x+half)-x_prime) - between;
var w2 = Math.abs((x-half)-x_prime) - between;
var x1 = x_prime + (w1/2) + between;
var x2 = x_prime - (w2/2) - between;
setupBuildings(x1, z, w1, l, h, sub-1, color, buildings);
setupBuildings(x2, z, w2, l, h, sub-1, color, buildings);
}
else{
var offset = Math.abs(getRandInt(0, slice_deviation));
var between = (city.inner_block_margin/2);
var half = l/2;
var z_prime = z + offset;
var l1 = Math.abs((z+half)-z_prime) - between;
var l2 = Math.abs((z-half)-z_prime) - between;
var z1 = z_prime + (l1/2) + between;
var z2 = z_prime - (l2/2) - between;
setupBuildings(x, z1, w, l1, h, sub-1, color, buildings);
setupBuildings(x, z2, w, l2, h, sub-1, color, buildings);
}
}
}
//create bridges
function setupBridges(){
bridges = _.shuffle(getEmptyRows()).splice(0, city.bridge_max);
var parts = [];
for(var i=0;i<bridges.length;i++){
var lx = getCoordinateFromIndex(bridges[i].index, city.width);
var lz = getCoordinateFromIndex(bridges[i].index, city.length);
parts.push(getBoxMeshOpts({
color: colors.BUILDING,
w: bridges[i].axis ? city.width : city.road_w,
l: bridges[i].axis ? city.road_w : city.length,
h: 4,
y: city.bridge_h+2,
x: bridges[i].axis ? 0 : lx,
z: bridges[i].axis ? lz : 0
}));
//columns
for(var j=0;j<(bridges[i].axis ? city.blocks_x : city.blocks_z);j++){
var h = city.bridge_h+(city.curb_h*2)+(city.water_height);
parts.push(getBoxMeshOpts({
color: colors.BUILDING,
w: 10,
l: 10,
h: h,
y: -((city.curb_h*2)+(city.water_height))+(h/2),
x: bridges[i].axis ? getCoordinateFromIndex(j, city.width) : lx,
z: bridges[i].axis ? lz : getCoordinateFromIndex(j, city.length)
}));
}
}
if(parts.length) scene.add(mergeMeshes(parts));
}
//=========================================
// OBJECTS
//=========================================
var Building = function(opts) {
this.parts = [];
//50% chance of building having a rim.
var rim = getRandInt(3,5);
var inset = getRandInt(2,4);
var rim_opts = {
color: opts.color,
h: rim,
y: opts.h + (rim/2) + city.curb_h,
shadow: false
}
//building core
this.parts.push(getBoxMeshOpts({
color: opts.color,
w: opts.w,
h: opts.h,
l: opts.l,
x: opts.x,
y: (opts.h/2)+city.curb_h,
z: opts.z,
shadow: true
}));
//draw rim on top of some buildings
if(chance(50)){
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: opts.w,
l: inset,
x: opts.x,
z: opts.z - (opts.l/2 - inset/2)
})));
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: opts.w,
l: inset,
x: opts.x,
z: opts.z + (opts.l/2 - inset/2)
})));
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: inset,
l: opts.l-(inset*2),
x: opts.x - (opts.w/2 - inset/2),
z: opts.z
})));
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: inset,
l: opts.l-(inset*2),
x: opts.x + (opts.w/2 - inset/2),
z: opts.z
})));
}
//additional details
if(chance(50)){
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: getRandInt(opts.w/4, opts.w/2),
l: getRandInt(opts.l/4, opts.l/2),
x: opts.x - (5*randDir()),
z: opts.z - (5*randDir())
})));
}
//antenna only on tall buildings
if(chance(25) && opts.tall){
this.parts.push(getBoxMeshOpts(_.assign(rim_opts, {
w: 3,
l: 3,
x: opts.x - (5*randDir()),
z: opts.z - (5*randDir()),
h: getRandInt(city.build_max_h/5, city.build_max_h/3)
})));
}
if(chance(25) && opts.tall){
var top = getBoxMeshOpts(_.assign(rim_opts, {
w: opts.w - (opts.w/3),
l: opts.w - (opts.w/3),
x: opts.x,
z: opts.z,
h: getRandInt(15, 30)
}))
top.castShadow = false;
this.parts.push(top);
}
//merged mesh
var merged = mergeMeshes(this.parts);
this.group = merged;
}
var Tree = function(x, z){
this.parts = [];
var h = getRandInt(2, 4);
trunk = getBoxMesh(colors.LIGHT_BROWN, 2, h, 2, x, h/2+city.curb_h, z);
leaves = getCylinderMesh(colors.TREE, 5, 10, 0, x, h+5+city.curb_h, z);
leaves2 = getCylinderMesh(colors.TREE, 5, 10, 0, x, leaves.position.y+5, z);
leaves.rotation.y = Math.random();
this.parts.push(leaves, leaves2, trunk);
this.group = mergeMeshes(this.parts);
};
var Intersection = function(x, z){
this.axis = Math.round(Math.random());
this.change = function(){
this.axis = this.axis ? 0 : 1;
}
}
var Car = function(x, z, dx, dz){
this.speed = Math.random() * (city.car_speed_min - city.car_speed_max) + city.car_speed_min;
this.color = colors.CARS[getRandInt(0, colors.CARS.length)];
this.mesh = getBoxMesh(this.color, 4, DEBUG ? 30 : 3, 9, x, 3, z, false);
this.arrived_threshold = 2;
this.collide_threshold = 2;
this.dir = randDir();
this.lane_offset = this.dir * city.road_w/4
this.axis = Math.round(Math.random());
if(this.axis){
this.mesh.rotation.y = Math.PI/2;
z = z - this.lane_offset;
x = -(this.dir * (city.width/2));
}
else{
x = x - this.lane_offset;
z = -(this.dir * (city.width/2));
}
this.mesh.position.set(x, DEBUG ? 20 : 3 , z);
this.drive = function(){
this.checkCollision();
if(this.axis){
this.mesh.position.x += (this.dir * this.speed);
}
else{
this.mesh.position.z += (this.dir * this.speed);
}
};
this.arrived = function(){
var out = outsideCity(this.mesh.position.x, this.mesh.position.z);
return out ||
((this.mesh.position.x < (dx + this.arrived_threshold)) &&
(this.mesh.position.x > (dx - this.arrived_threshold)) &&
(this.mesh.position.z < (dz + this.arrived_threshold)) &&
(this.mesh.position.z > (dz - this.arrived_threshold)));
};
this.checkCollision = function(){
};
};
var Train = function(bridge){
var parts = [];
this.dir = randDir();
this.carriages = 4;
this.carriage_length = 30;
this.carriage_offset = 5;
this.arrived_threshold = 2;
this.color = colors.TRAIN;
this.speed = city.train_speed;
var x = bridge.axis ? -(this.dir * (city.width/2)) : getCoordinateFromIndex(bridge.index, city.width);
var z = bridge.axis ? getCoordinateFromIndex(bridge.index, city.length) : -(this.dir * (city.length/2));
for(var i=0;i<this.carriages;i++){
var y = (this.carriage_length*i + this.carriage_offset*i);
parts.push(getBoxMesh(this.color, 6, 6, this.carriage_length, 0, 0, y, false));
}
this.mesh = mergeMeshes(parts);
this.mesh.rotation.y = (Math.PI/2)*bridge.axis;
this.mesh.position.set(x, city.bridge_h+8, z);
this.drive = function(){
this.mesh.position.x += bridge.axis ? (this.dir * this.speed) : 0;
this.mesh.position.z += bridge.axis ? 0 : (this.dir * this.speed);
};
this.arrived = function(){
return outsideCity(this.mesh.position.x, this.mesh.position.z);
};
};
var CarSpawner = function(){
this.cars = [];
this.locked = false;
this.max_add_rate = 300;
this.add = function(){
var self = this;
self.locked = true;
//get random x and z on a road
var rand_x = city.block * getRandInt((-city.blocks_x/2+1), city.blocks_x/2);
var rand_z = city.block * getRandInt((-city.blocks_z/2+1), city.blocks_z/2);
var car = new Car(rand_x, rand_z);
self.cars.push(car);
scene.add(car.mesh);
setTimeout(function(){ self.locked = false; }, self.max_add_rate);
};
this.update = function(){
var self = this;
//add cars
if((self.cars.length < city.car_max) && !self.locked){
self.add();
}
self.cars.forEach(function(car){
//remove from car array
if(car.arrived()){
_.pull(self.cars, car);
scene.remove(car.mesh);
}
//update car positions
else{
car.drive();
}
});
};
};
var TrainSpawner = function(){
this.trains = [];
this.locked = false;
this.add = function(){
var self = this;
var b = bridges[getRandInt(0, bridges.length)];
var train = new Train(b);
self.trains.push(train);
scene.add(train.mesh);
self.locked = false;
};
this.update = function(){
var self = this;
//add trains
if((self.trains.length < city.train_max) && !self.locked){
self.locked = true;
setTimeout(function(){ self.add(); }, getRandInt(1000, 3000));
}
self.trains.forEach(function(train){
//remove from train array
if(train.arrived()){
_.pull(self.trains, train);
scene.remove(train.mesh);
}
//update train positions
else{
train.drive();
}
});
};
};
//=========================================
// HELPER FUNCTIONS
//=========================================
//get a numerical value to represent a string
function encode(string) {
var number = "0x";
var length = string.length;
for (var i = 0; i < length; i++)
number += string.charCodeAt(i).toString(16);
return number;
}
//get a string value to represent a number
function decode(number) {
var string = "";
number = number.slice(2);
var length = number.length;
for (var i = 0; i < length;) {
var code = number.slice(i, i += 2);
string += String.fromCharCode(parseInt(code, 16));
}
return string;
}
//get query string values
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
//map val (0-1) to a range with optional weight
function mapToRange(val, min, max, exp){
exp = exp || 1;
var weighted = Math.pow(val, exp);
//make the highest little higher
if (val >= 0.9) weighted = val;
var num = Math.floor(weighted * (max - min)) + min;
return num;
}
//get a random in in range
function getRandInt(min, max, exp) {
return mapToRange(Math.random(), min, max, exp);
}
//map value from 0-1 to a luminosity
function getGreyscaleColor(val) {
return new THREE.Color().setHSL(0, 0, val);
}
//get values from noise map
function getNoiseValue(x, z, freq) {
freq = freq || city.noise_frequency
var value = Math.abs(noise.perlin2(x/freq, z/freq));
return value;
}
//is a point outside the city bounds
function outsideCity(x, z) {
return (Math.abs(x) > city.width/2) ||
(Math.abs(z) > city.length/2);
}
//get x,z from index
function getCoordinateFromIndex(index, offset) {
return (-(offset/2) + (index * city.block)) + (city.block/2);
}
//get rows or columns with no buildings
function getEmptyRows() {
var i, low, lri, lci, empty = [];
//loop through rows
for(i=0; i<heightmap.length;i++){
var row = heightmap[i];
// low = (low < _.sum(row)) ? low : _.sum(row);
//all values in row are under tree threshold
row = _.reject(row, function(n) {return n < city.tree_threshold})
if(!row.length){
empty.push({axis: 0, index: i});
}
}
//loop through columns
for(i=0; i<heightmap[0].length;i++){
var col = _.map(heightmap, function(row){ return row[i]; });
col = _.reject(col, function(n) {return n < city.tree_threshold})
if(!col.length){
empty.push({axis: 1, index: i});
}
}
return empty;
}
//normalize array values to 0-1
function normalizeArray(arr) {
var min = Math.min.apply(null, arr);
var max = Math.max.apply(null, arr);
return arr.map(function(num) {
return ((num-min)/(max-min));
});
}
//carete a box mesh with a geometry and material
function getBoxMesh(color, w, h, l, x, y, z, shadow) {
shadow = (typeof shadow === "undefined") ? true : shadow;
material = new THREE.MeshLambertMaterial({ color: color});
geom = new THREE.BoxGeometry(w, h, l);
mesh = new THREE.Mesh(geom, material);
mesh.position.set(x || 0, y || 0, z || 0);
mesh.receiveShadow = true;
if(shadow){
mesh.castShadow = true;
}
return mesh;
}
//carete a box mesh with a geometry and material
function getBoxMeshOpts(options) {
var o=options||{};
return getBoxMesh(o.color, o.w, o.h, o.l, o.x, o.y, o.z, o.shadow);
}
//water mesh
function getWaterMesh(w, h, l, x, y, z) {
material = new THREE.MeshPhongMaterial({color:colors.WATER, transparent: true, opacity: 0.6 } );
geom = new THREE.BoxGeometry(w, h, l);
mesh = new THREE.Mesh(geom, material);
mesh.position.set(x || 0, y || 0, z || 0);
mesh.receiveShadow = false;
mesh.castShadow = false;
return mesh;
}
//carete a cylinder mesh with a geometry and material
function getCylinderMesh(color, rb, h, rt, x, y, z) {
var material = new THREE.MeshLambertMaterial({ color: color});
var geom = new THREE.CylinderGeometry( rt, rb, h, 4, 1 );
var mesh = new THREE.Mesh(geom, material);
mesh.rotation.y = Math.PI/4;
mesh.position.set(x || 0, y || 0, z || 0);
mesh.receiveShadow = true;
mesh.castShadow = true;
return mesh;
}
//carete a cylinder mesh with a geometry and material
function getCylinderMeshOpts(options) {
var o = options || {}
return getCylinderMesh(o.color, o.rb, o.h, o.rt, o.x, o.y, o.z)
}
//returns true percent of the time
function chance(percent){
return (Math.random() < percent/100.0);
}
//return 1 or -1
function randDir(){
return Math.round(Math.random()) * 2 - 1;
}
//pythagorean theorem, return c
function pathag(a, b){
return Math.sqrt(Math.pow(a, 2)+Math.pow(b, 2));
}
//merge geometries of meshes
function mergeMeshes (meshes, shadows, material) {
shadow = (typeof shadow === "undefined") ? true : shadow;
material = material || meshes[0].material;
var combined = new THREE.Geometry();
for (var i = 0; i < meshes.length; i++) {
meshes[i].updateMatrix();
combined.merge(meshes[i].geometry, meshes[i].matrix);
}
var mesh = new THREE.Mesh(combined, material);
if(shadows){
mesh.castShadow = true;
mesh.receiveShadow = true;
}
return mesh;
}
//get the initial city settings
function getInitialSettings(){
var loaded = getParameterByName("city");
if(loaded){
city = JSON.parse(decodeURIComponent(loaded));
}
//set the initial values for dom input fields
for(var i=0; i<inputs.length; i++){
document.getElementById(inputs[i].field).value = city[inputs[i].setting];
}
};
//update city opject with values from input fields
function updateCityOptions(){
for(var i=0; i<inputs.length; i++){
var el = document.getElementById(inputs[i].field);
var type = el.getAttribute("type");
if(el.value){
city[inputs[i].setting] = (type=="number"||type=="range") ? Number(el.value) : el.value;
}
}
city.width = city.block * city.blocks_x;
city.length = city.block * city.blocks_z;
city.seed = document.getElementById("preserve_seed").checked ? city.seed : Math.random();
DEBUG = document.getElementById("debug").checked;
//city.seed = seed.value ? encode(seed.value) : Math.random();
//city.seed = Math.random();
//document.getElementById("current_seed").value = city.seed;
return city;
}
//reset the city options to the default values
function resetCityOptions(){
city = _.clone(_city);
for(var i=0; i<inputs.length; i++){
document.getElementById(inputs[i].field).value = city[inputs[i].setting];
}
return city;
}
//get a url for the city with encoded settings
function getCityUrl(){
var base = "http://codepen.io/pieceoftoast/pen/ojVWdR";
if(base.indexOf("?") > -1){
return base + "&city=" + encodeURIComponent(JSON.stringify(city));
}
else{
return base + "?city=" + encodeURIComponent(JSON.stringify(city));
}
}
//=========================================
// EVENT BINDING
//=========================================
//bind generate button click
document.getElementById('reset').addEventListener('click', function() {
city = updateCityOptions();
reset();
}, false);
//bind info button click
document.getElementById('info').addEventListener('click', function() {
document.querySelector('.options').classList.add('hidden');
document.querySelector('.info').classList.toggle('hidden');
}, false);
//bind options button click
document.getElementById('options').addEventListener('click', function() {
document.querySelector('.info').classList.add('hidden');
document.querySelector('.options').classList.toggle('hidden');
}, false);
//bind debug button click
// document.getElementById('debug').addEventListener('click', function() {
// this.innerHTML = this.innerHTML=="Debug Off" ? "Debug On " : "Debug Off";
// DEBUG = !DEBUG;
// reset();
// }, false);
//reset city options
document.getElementById('default').addEventListener('click', function() {
resetCityOptions();
}, false);
//=========================================
// MAIN
//=========================================
//main animation loop
var render = function() {
requestAnimationFrame(render);
renderer.render(scene, camera);
carSpawner.update();
if(bridges.length) trainSpawner.update();
controls.update();
stats.update();
};
//initial setup
function init(){
setupScene();
setupLights();
setupHeightmap();
setupGround();
setupBlocks();
setupBridges();
carSpawner = new CarSpawner();
if(bridges.length) trainSpawner = new TrainSpawner();
document.getElementById('url').value = getCityUrl();
}
//remove everything and reinitialize
function reset(){
scene = camera = renderer = width = heightmap = null;
backLight = composer = stats = floor = building = null;
height = controls = light = shadowLight = null;
var canvas = document.getElementsByTagName("CANVAS")[0];
document.body.removeChild(canvas);
init();
}
//run it
getInitialSettings();
init();
render();
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js"></script>
<script src="http://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="http://threejs.org/examples/js/libs/stats.min.js"></script>
<script src="https://cdn.rawgit.com/pieceoftoast/noisejs/master/perlin.js"></script>
@mixin linear-gradient($direction, $color-stops...) {
background: nth(nth($color-stops, 1), 1);
background: -webkit-linear-gradient(legacy-direction($direction), $color-stops);
background: linear-gradient($direction, $color-stops);
}
//lightblue
$base: #C1D7DE;
//darkblue
$base: #586A70;
$dark: darken($base, 20%);
$light: lighten($base, 10%);
*, *:before, *:after{
outline:none;
box-sizing: border-box;
}
body{
font-family: helvetica;
background: $dark;
overflow:hidden;
padding:0px;
margin:0px;
color:$dark;
}
.controls{
list-style:none;
position:absolute;
padding:0;
margin:0;
right:10px;
top:10px;
}
.controls li {
display:inline-block;
margin-left:3px;
}
button{
background-color:$light;
transition:all 0.2s;
border-radius:2px;
padding:7px 10px;
font-size:12px;
min-width: 80px;
cursor:pointer;
color:$dark;
border:0;
&:hover{background:lighten($light, 10%);}
&:active{
background:$base;
}
}
.hidden{
display:none;
}
.module{
background:$light;
position:absolute;
width:255px;
padding:15px;
top: 47px;
right: 10px;
max-height:80%;
border-radius:2px;
overflow:auto;
p{margin:0;}
button{
background:$dark;
color:$light;
&:active{ background:$base;}
}
section{
margin-bottom:15px;
}
h3{
font-size:14px;
margin-top:0;
margin-bottom:8px;
}
}
.options{
p{margin:0 0 20px 0;}
.row{
margin-bottom:5px;
}
.row.buttons{
margin:20px 0 0 0;
}
.col{
display:inline-block;
}
.col-label{
width:68%;
}
.col-input{
width:29%;
}
}
label{
font-size:12px;
}
input, textarea{
color:$light;
background:$dark;
padding:5px 8px;
border-radius:2px;
border:none;
width:100%;
}
input[readonly]{
background:$light;
color:$dark;
border:1px solid $dark;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment