import numpy as np from scipy.spatial import Voronoi from skimage.draw import polygon from PIL import Image from noise import snoise3 from skimage import exposure from scipy.interpolate import interp1d import cv2 from scipy.ndimage import gaussian_filter from scipy.ndimage import binary_dilation from argparse import ArgumentParser def save_height_map(height_map, file_name): #input height map should be float, raw output of noise map normalized_height_map = (((height_map - height_map.min()) / (height_map.max() - height_map.min()))*255).astype(np.uint8) cv2.imwrite(file_name, normalized_height_map) np.save(file_name[:-4] + '.npy', height_map) def get_boundary(vor_map, size, kernel=1): boundary_map = np.zeros_like(vor_map, dtype=bool) n, m = vor_map.shape clip = lambda x: max(0, min(size-1, x)) def check_for_mult(a): b = a[0] for i in range(len(a)-1): if a[i] != b: return 1 return 0 for i in range(n): for j in range(m): boundary_map[i, j] = check_for_mult(vor_map[ clip(i-kernel):clip(i+kernel+1), clip(j-kernel):clip(j+kernel+1), ].flatten()) return boundary_map def histeq(img, alpha=1): img_cdf, bin_centers = exposure.cumulative_distribution(img) img_eq = np.interp(img, bin_centers, img_cdf) img_eq = np.interp(img_eq, (0, 1), (-1, 1)) return alpha * img_eq + (1 - alpha) * img def voronoi(points, size): # Add points at edges to eliminate infinite ridges edge_points = size*np.array([[-1, -1], [-1, 2], [2, -1], [2, 2]]) new_points = np.vstack([points, edge_points]) # Calculate Voronoi tessellation vor = Voronoi(new_points) return vor def voronoi_map(vor, size): # Calculate Voronoi map vor_map = np.zeros((size, size), dtype=np.uint32) for i, region in enumerate(vor.regions): # Skip empty regions and infinte ridge regions if len(region) == 0 or -1 in region: continue # Get polygon vertices x, y = np.array([vor.vertices[i][::-1] for i in region]).T # Get pixels inside polygon rr, cc = polygon(x, y) # Remove pixels out of image bounds in_box = np.where((0 <= rr) & (rr < size) & (0 <= cc) & (cc < size)) rr, cc = rr[in_box], cc[in_box] # Paint image vor_map[rr, cc] = i return vor_map # Lloyd's relaxation def relax(points, size, k=10): new_points = points.copy() for _ in range(k): vor = voronoi(new_points, size) new_points = [] for i, region in enumerate(vor.regions): if len(region) == 0 or -1 in region: continue poly = np.array([vor.vertices[i] for i in region]) center = poly.mean(axis=0) new_points.append(center) new_points = np.array(new_points).clip(0, size) return new_points def noise_map(size, res, seed, octaves=1, persistence=0.5, lacunarity=2.0): scale = size/res return np.array([[ snoise3( (x+0.1)/scale, y/scale, seed, octaves=octaves, persistence=persistence, lacunarity=lacunarity ) for x in range(size)] for y in range(size) ]) def average_cells(vor, data): """Returns the average value of data inside every voronoi cell""" size = vor.shape[0] count = np.max(vor)+1 sum_ = np.zeros(count) count = np.zeros(count) for i in range(size): for j in range(size): p = vor[i, j] count[p] += 1 sum_[p] += data[i, j] average = sum_/ (count + 1e-3) average[count==0] = 0 return average def fill_cells(vor, data): size = vor.shape[0] image = np.zeros((size, size)) for i in range(size): for j in range(size): p = vor[i, j] image[i, j] = data[p] return image def color_cells(vor, data, dtype=int): size = vor.shape[0] image = np.zeros((size, size, 3)) for i in range(size): for j in range(size): p = vor[i, j] image[i, j] = data[p] return image.astype(dtype) def quantize(data, n): bins = np.linspace(-1, 1, n+1) return (np.digitize(data, bins) - 1).clip(0, n-1) def bezier(x1, y1, x2, y2, a): p1 = np.array([0, 0]) p2 = np.array([x1, y1]) p3 = np.array([x2, y2]) p4 = np.array([1, a]) return lambda t: ((1-t)**3 * p1 + 3*(1-t)**2*t * p2 + 3*(1-t)*t**2 * p3 + t**3 * p4) def bezier_lut(x1, y1, x2, y2, a): t = np.linspace(0, 1, 256) f = bezier(x1, y1, x2, y2, a) curve = np.array([f(t_) for t_ in t]) return interp1d(*curve.T) def filter_map(h_map, smooth_h_map, x1, y1, x2, y2, a, b): f = bezier_lut(x1, y1, x2, y2, a) output_map = b*h_map + (1-b)*smooth_h_map output_map = f(output_map.clip(0, 1)) return output_map def filter_inbox(pts, size): inidx = np.all(pts < size, axis=1) return pts[inidx] def generate_trees(n, size): trees = np.random.randint(0, size-1, (n, 2)) trees = relax(trees, size, k=10).astype(np.uint32) trees = filter_inbox(trees, size) return trees def place_trees(river_land_mask, adjusted_height_river_map, n, mask, size, a=0.5): trees= generate_trees(n, size) rr, cc = trees.T output_trees = np.zeros((size, size), dtype=bool) output_trees[rr, cc] = True output_trees = output_trees*(mask>a)*river_land_mask*(adjusted_height_river_map<0.5) output_trees = np.array(np.where(output_trees == 1))[::-1].T return output_trees def PCGGen(map_size, nbins = 256, seed = 3407): biome_names = [ # sand and rock "desert", # grass gravel rock stone "savanna", # mixed woodland and grassland # trees flower "tropical_woodland", # rainforest # dirt grass gravel rock stone "tundra", # no trees # trees flower "seasonal_forest", # trees "rainforest", # trees "temperate_forest", # trees "temperate_rainforest", # snow rock tree "boreal_forest" # taiga, snow forest ] biome_colors = [ [255, 255, 178], [184, 200, 98], [188, 161, 53], [190, 255, 242], [106, 144, 38], [33, 77, 41], [86, 179, 106], [34, 61, 53], [35, 114, 94] ] size = map_size n = nbins map_seed = seed # start generation points = np.random.randint(0, size, (514, 2)) points = relax(points, size, k=100) vor = voronoi(points, size) vor_map = voronoi_map(vor, size) boundary_displacement = 8 boundary_noise = np.dstack([noise_map(size, 32, 200 + map_seed, octaves=8), noise_map(size, 32, 250 + map_seed, octaves=8)]) boundary_noise = np.indices((size, size)).T + boundary_displacement*boundary_noise boundary_noise = boundary_noise.clip(0, size-1).astype(np.uint32) blurred_vor_map = np.zeros_like(vor_map) for x in range(size): for y in range(size): j, i = boundary_noise[x, y] blurred_vor_map[x, y] = vor_map[i, j] vor_map = blurred_vor_map temperature_map = noise_map(size, 2, 10 + map_seed) precipitation_map = noise_map(size, 2, 20 + map_seed) uniform_temperature_map = histeq(temperature_map, alpha=0.33) uniform_precipitation_map = histeq(precipitation_map, alpha=0.33) temperature_map = uniform_temperature_map precipitation_map = uniform_precipitation_map temperature_cells = average_cells(vor_map, temperature_map) precipitation_cells = average_cells(vor_map, precipitation_map) quantize_temperature_cells = quantize(temperature_cells, n) quantize_precipitation_cells = quantize(precipitation_cells, n) quantize_temperature_map = fill_cells(vor_map, quantize_temperature_cells) quantize_precipitation_map = fill_cells(vor_map, quantize_precipitation_cells) temperature_cells = quantize_temperature_cells precipitation_cells = quantize_precipitation_cells temperature_map = quantize_temperature_map precipitation_map = quantize_precipitation_map im = np.array(Image.open("./assets/biome_image.png"))[:, :, :3] im = cv2.resize(im, (256, 256)) biomes = np.zeros((256, 256)) for i, color in enumerate(biome_colors): indices = np.where(np.all(im == color, axis=-1)) biomes[indices] = i biomes = np.flip(biomes, axis=0).T n = len(temperature_cells) biome_cells = np.zeros(n, dtype=np.uint32) for i in range(n): temp, precip = temperature_cells[i], precipitation_cells[i] biome_cells[i] = biomes[temp, precip] biome_map = fill_cells(vor_map, biome_cells).astype(np.uint32) biome_color_map = color_cells(biome_map, biome_colors) height_map = noise_map(size, 4, 0 + map_seed, octaves=6, persistence=0.5, lacunarity=2) land_mask = height_map > 0 smooth_height_map = noise_map(size, 4, 0 + map_seed, octaves=1, persistence=0.5, lacunarity=2) biome_height_maps = [ # Desert filter_map(height_map, smooth_height_map, 0.75, 0.2, 0.95, 0.2, 0.2, 0.5), # Savanna filter_map(height_map, smooth_height_map, 0.5, 0.1, 0.95, 0.1, 0.1, 0.2), # Tropical Woodland filter_map(height_map, smooth_height_map, 0.33, 0.33, 0.95, 0.1, 0.1, 0.75), # Tundra filter_map(height_map, smooth_height_map, 0.5, 1, 0.25, 1, 1, 1), # Seasonal Forest filter_map(height_map, smooth_height_map, 0.75, 0.5, 0.4, 0.4, 0.33, 0.2), # Rainforest filter_map(height_map, smooth_height_map, 0.5, 0.25, 0.66, 1, 1, 0.5), # Temperate forest filter_map(height_map, smooth_height_map, 0.75, 0.5, 0.4, 0.4, 0.33, 0.33), # Temperate Rainforest filter_map(height_map, smooth_height_map, 0.75, 0.5, 0.4, 0.4, 0.33, 0.33), # Boreal filter_map(height_map, smooth_height_map, 0.8, 0.1, 0.9, 0.05, 0.05, 0.1) ] biome_count = len(biome_names) biome_masks = np.zeros((biome_count, size, size)) for i in range(biome_count): biome_masks[i, biome_map==i] = 1 biome_masks[i] = gaussian_filter(biome_masks[i], sigma=16) # Remove ocean from masks blurred_land_mask = land_mask blurred_land_mask = binary_dilation(land_mask, iterations=32).astype(np.float64) blurred_land_mask = gaussian_filter(blurred_land_mask, sigma=16) # biome mask - [9, size, size] biome_masks = biome_masks*blurred_land_mask adjusted_height_map = height_map.copy() for i in range(len(biome_height_maps)): adjusted_height_map = (1-biome_masks[i])*adjusted_height_map + biome_masks[i]*biome_height_maps[i] # add rivers biome_bound = get_boundary(biome_map, size, kernel=5) cell_bound = get_boundary(vor_map, size, kernel=2) river_mask = noise_map(size, 4, 4353 + map_seed, octaves=6, persistence=0.5, lacunarity=2) > 0 new_biome_bound = biome_bound*(adjusted_height_map<0.5)*land_mask new_cell_bound = cell_bound*(adjusted_height_map<0.05)*land_mask rivers = np.logical_or(new_biome_bound, new_cell_bound)*river_mask loose_river_mask = binary_dilation(rivers, iterations=8) rivers_height = gaussian_filter(rivers.astype(np.float64), sigma=2)*loose_river_mask adjusted_height_river_map = adjusted_height_map*(1-rivers_height) - 0.05*rivers sea_color = np.array([12, 14, 255]) river_land_mask = adjusted_height_river_map >= 0 land_mask_color = np.repeat(river_land_mask[:, :, np.newaxis], 3, axis=-1) rivers_biome_color_map = land_mask_color*biome_color_map + (1-land_mask_color)*sea_color rivers_biome_map = river_land_mask * biome_map + (1 - river_land_mask) * biome_count # use biome count=9 as water indicator semantic_map = rivers_biome_map semantic_map_color = rivers_biome_color_map height_map = adjusted_height_river_map tree_densities = [4000, 1500, 8000, 1000, 10000, 25000, 10000, 20000, 5000] trees = [np.array(place_trees(river_land_mask, adjusted_height_river_map, tree_densities[i], biome_masks[i], size)) for i in range(len(biome_names))] canvas = np.ones((size, size)) * 255 for k in range(len(biome_names)): canvas[trees[k][:, 1], trees[k][:, 0]] = k tree_map = canvas return height_map, semantic_map, tree_map, semantic_map_color if __name__ == '__main__': import os parser = ArgumentParser() parser.add_argument('--size', type=int, required=True) parser.add_argument('--nbins', type=int, default=256) parser.add_argument('--seed', type=int, default=3407) parser.add_argument('--outdir', type=str, required=True) args = parser.parse_args() outdir = args.outdir heightmap, semanticmap, treemap, colormap = PCGGen(args.size, args.nbins, args.seed) save_height_map(heightmap, os.path.join(outdir, 'heightmap.png')) cv2.imwrite(os.path.join(outdir, 'semanticmap.png'), semanticmap.astype(np.uint8)) cv2.imwrite(os.path.join(outdir, 'colormap.png'), colormap[..., [2, 1, 0]].astype(np.uint8)) cv2.imwrite(os.path.join(outdir, 'treemap.png'), treemap)