Shared posts

29 Apr 05:15

Generating Towns for Townscaper in Processing

by Dan Malec

I’ve been playing Oskar Stålberg’s Townscaper recently and enjoying seeing how the algorithms react to changes. While looking for information on how garden paths are calculated, I stumbled across Chris Love’s article on the Townscaper file format. Using that as a starting point, I decided to try building up town maps in processing using Perlin noise to determine height, color, and distance above waterline.

My first attempt iterated the map while incrementing a 1D vector in Perlin noise space to determine these attributes. This gave a nice wavy pattern; but, wasn’t quite what I was trying for.

The next attempt involved mapping the coordinates of the map to a 2D point in Perlin noise space; but, I didn’t scale things, so there’s a fairly low level of variation.

Increasing the variation in building height and distance between the base of each building node and the waterline yielded a fairly nice result.

I think my favorite towns involve a middle of the road approach which yields town maps that have some variation in height; but, not too much.

I’m sharing the code I used – hopefully continuing in the footsteps of Chris and Patrick (whose shoulders I stand on for map generation) with the idea that someone may take this and run somewhere new with it. If you do, I’m curious to see where you go! Even if you don’t, I strongly recommend checking out Townscaper – what Oskar Stålberg has created is absolutely wonderful.

// --------------------------------------------------------------------
// Load a save file from Townscaper as a template, replace the voxel
// data, then save the file out with a timestamp.
// --------------------------------------------------------------------

import java.lang.Float;
import java.text.SimpleDateFormat;
import java.util.Date;

// Offsets in noise space for attributes.
final static float BUILDING_HEIGHT_NOISE_OFFSET = 0.0;
final static float BUILDING_COLOR_NOISE_OFFSET = 25.0;
final static float STRUT_HEIGHT_NOISE_OFFSET = 50.0;

// Noise distribution (larger values -> more variation).
final static float BUILDING_HEIGHT_NOISE_RANGE = 20.0;
final static float BUILDING_COLOR_NOISE_RANGE = 1.0;
final static float STRUT_HEIGHT_NOISE_RANGE = 20.0;

// Value ranges.
final static float MAX_BUILDING_HEIGHT = 4.0;
final static float MAX_BUILDING_COLOR = 15;
final static float MAX_STRUT_HEIGHT = 10.0;


XML xml;
XML[] corners;
XML voxelHolder;
float minX, maxX, minY, maxY;
float xRange, yRange;
  
void loadTownFile() {
  // The code expects a file called "Town0.scape" to be located in the project's "data" folder.
  xml = loadXML("Town0.scape");
  
  // Load the corner array.
  XML cornerHolder = xml.getChild("corners");
  corners = cornerHolder.getChildren("C");

  // Traverse the corner array to find the range for X and Y coordinates.
  minX = minY = Float.MAX_VALUE;
  maxX = maxY = Float.MIN_VALUE;
  
  for (int i=0; i<corners.length; i++) {
    float x = corners[i].getChild("x").getFloatContent();
    float y = corners[i].getChild("y").getFloatContent();
    
    minX = min(minX, x);
    maxX = max(maxX, x);
    
    minY = min(minY, y);
    maxY = max(maxY, y);
  }
  
  println("X Range: " + minX + " - " + maxX);
  println("Y Range: " + minY + " - " + maxY);

  xRange = (maxX - minX);
  yRange = (maxY - minY);

  // Load the voxel array.
  voxelHolder = xml.getChild("voxels");
  XML[] voxels = voxelHolder.getChildren("V");

  // Remove all existing voxels.
  for (int i=0; i<voxels.length; i++) {
    voxelHolder.removeChild(voxels[i]);
  }
}

void generateTownMap() {
  for (int c=0; c<corners.length; c++) {
    // Get the X and Y of the corner, normalized so 0 is the minimum.
    float normalizedX = minX + corners[c].getChild("x").getFloatContent();
    float normalizedY = minY + corners[c].getChild("y").getFloatContent();
    
    // Calculate the building height.
    int buildingHeight = 1 + (int)(noise(BUILDING_HEIGHT_NOISE_OFFSET + normalizedX / xRange * BUILDING_HEIGHT_NOISE_RANGE, BUILDING_HEIGHT_NOISE_OFFSET + normalizedY / yRange * BUILDING_HEIGHT_NOISE_RANGE) * MAX_BUILDING_HEIGHT);
    corners[c].getChild("count").setContent(str(buildingHeight));
    
    // Calculate strut height
    int strutHeight = (int)(noise(STRUT_HEIGHT_NOISE_OFFSET + normalizedX / xRange * STRUT_HEIGHT_NOISE_RANGE, STRUT_HEIGHT_NOISE_OFFSET + normalizedY / yRange * STRUT_HEIGHT_NOISE_RANGE) * MAX_STRUT_HEIGHT);
        
    for (int v=0; v<buildingHeight; v++) {
      // Calculate color
      int voxColor = (int)(noise(BUILDING_COLOR_NOISE_OFFSET + normalizedX / xRange * BUILDING_COLOR_NOISE_RANGE, BUILDING_COLOR_NOISE_OFFSET + normalizedY / yRange * BUILDING_COLOR_NOISE_RANGE) * MAX_BUILDING_COLOR);

      // Add the voxel to the array and set the color and height attributes.
      XML genVoxel = voxelHolder.addChild("V");
      XML colorEl = genVoxel.addChild("t");
      colorEl.setContent(str(voxColor));
      XML heightEl = genVoxel.addChild("h");
      heightEl.setContent(str(strutHeight + v));
    }
  }
}

void saveTownFile() {
  // Build a timestamped filename.
  final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
  final String timeStamp = format.format(new Date());
  final String generatedXmlFileName = "Town" + timeStamp + ".scape";
  
  // Save the XML out.
  saveXML(xml, generatedXmlFileName);
}

void setup() {
  loadTownFile();
  generateTownMap();
  saveTownFile();
}
15 Aug 02:21

COVID Risk Comfort Zone

I'm like a vampire, except I'm not crossing that threshold even if you invite me.