Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Describe this as part of code
- The main idea is that the **grid** from which the generation starts is **Voronoi noise**. Blocks resembling a **city** are created from the grid’s lines. These blocks have **different heights and lengths**, following the grid’s lines.
- ChatGPT said:
- Here is a Python script using Voronoi noise to generate a city-like grid, where the blocks follow the Voronoi cells and have different heights and lengths. The visualization is done using Matplotlib and Shapely to construct and render the geometry.
- python
- Copy
- import numpy as np
- import matplotlib.pyplot as plt
- from scipy.spatial import Voronoi, voronoi_plot_2d
- import random
- from shapely.geometry import Polygon, MultiPolygon
- from descartes import PolygonPatch
- # Generate random points for Voronoi noise
- num_points = 100
- points = np.random.rand(num_points, 2) * 100 # Scale to a larger grid
- # Create Voronoi diagram
- vor = Voronoi(points)
- # Extract Voronoi regions and filter out invalid ones
- regions = []
- for region in vor.regions:
- if not -1 in region and len(region) > 0:
- poly = [vor.vertices[i] for i in region]
- regions.append(Polygon(poly))
- # Create a city-like layout from Voronoi cells
- def generate_building_heights(regions):
- buildings = []
- for region in regions:
- if region.is_valid:
- height = random.uniform(5, 50) # Random height for buildings
- buildings.append((region, height))
- return buildings
- buildings = generate_building_heights(regions)
- # Plot the generated city
- fig, ax = plt.subplots(figsize=(10, 10))
- for building, height in buildings:
- color = (random.uniform(0.5, 1), random.uniform(0.5, 1), random.uniform(0.5, 1), 0.6)
- patch = PolygonPatch(building, fc=color, ec='black', alpha=0.7, lw=0.5)
- ax.add_patch(patch)
- # Adjust plot limits
- ax.set_xlim(0, 100)
- ax.set_ylim(0, 100)
- ax.set_aspect('equal')
- ax.axis('off')
- plt.show()
- How It Works:
- Generates Voronoi Noise: A set of random points define the Voronoi diagram.
- Extracts Regions: The valid Voronoi cells are converted into polygons.
- Assigns Heights: Each building (polygon) is given a random height.
- Renders the City: The colored regions represent buildings, with outlines for clarity.
- This approach allows for a procedural city generation system where the grid structure is defined by Voronoi noise, and buildings follow the natural patterns of Voronoi cells. You can tweak the parameters (such as number of points or height range) to achieve different effects. 🚀
- You said:
- Lets use p5js
- Reasoned for 53 seconds
- Below is an example of how you can use p5.js (along with the p5.voronoi library) to generate a Voronoi-based “city” layout. Each Voronoi cell is treated as a city block, and we assign a random “height” (or any other property) to each block. The code below shows a simple 2D top-down representation, but you can expand it to a 3D or isometric view if desired.
- Note: This example uses the p5.voronoi library. Make sure you include the library in your HTML file (e.g., by adding <script src="p5.voronoi.js"></script> before your main sketch).
- html
- Copy
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8" />
- <title>Voronoi City in p5.js</title>
- <!-- p5.js library -->
- <!-- p5.voronoi library -->
- <!-- Our sketch -->
- <script src="sketch.js"></script>
- </head>
- <body>
- </body>
- </html>
- javascript
- Copy
- // sketch.js
- let sites = []; // Random points that define the Voronoi diagram
- let cellColors = []; // Random colors for each Voronoi cell
- let buildingHeights = []; // Example array to store building heights
- function setup() {
- createCanvas(800, 800);
- // Optional styling for Voronoi
- voronoiCellStrokeWeight(1);
- voronoiCellStroke(0);
- voronoiSiteStrokeWeight(0);
- voronoiSiteStroke(0);
- // Generate random points
- let numPoints = 50;
- for (let i = 0; i < numPoints; i++) {
- let x = random(width);
- let y = random(height);
- sites.push({ x, y });
- // Assign a random color
- cellColors.push(color(random(80, 255), random(80, 255), random(80, 255), 180));
- // Assign a random "height" (e.g., building height)
- buildingHeights.push(random(5, 50));
- }
- // Feed the points into the Voronoi library
- for (let i = 0; i < sites.length; i++) {
- voronoiSite(sites[i].x, sites[i].y);
- }
- // Compute the Voronoi diagram (last parameter: "discardSitesOutsideDiagram")
- voronoi(width, height, false);
- // We only need to draw once
- noLoop();
- }
- function draw() {
- background(255);
- // Retrieve the computed Voronoi diagram
- let diagram = voronoiGetDiagram();
- let cells = diagram.cells; // Each cell corresponds to one site
- // Draw each cell as a polygon
- for (let i = 0; i < cells.length; i++) {
- let cell = cells[i];
- if (!cell) continue;
- // Use a color that corresponds to this cell
- fill(cellColors[i]);
- stroke(0, 50);
- strokeWeight(1);
- beginShape();
- for (let j = 0; j < cell.halfedges.length; j++) {
- let edge = cell.halfedges[j].getEndpoint();
- vertex(edge.x, edge.y);
- }
- endShape(CLOSE);
- // Optionally, you could do something with buildingHeights[i] here
- // e.g., draw text or an isometric extrusion
- }
- // (Optional) Draw the original site points
- fill(0);
- noStroke();
- for (let i = 0; i < sites.length; i++) {
- ellipse(sites[i].x, sites[i].y, 4, 4);
- }
- }
- Explanation
- Random Points (Sites)
- We generate a set of random (x, y) coordinates that will define our Voronoi diagram. Each site corresponds to one “city block.”
- Voronoi Generation
- Using p5.voronoi, we call:
- voronoiSite(x, y) for each point.
- voronoi(width, height, false) to compute the diagram within the canvas dimensions.
- Cells as Blocks
- The cells in the Voronoi diagram become individual polygons.
- We draw each polygon using beginShape() ... endShape(CLOSE).
- Heights & Visuals
- In a 2D top-down map, we might simply color each block or label it with its “height.”
- In a 3D or isometric approach, you could extrude each polygon by its buildingHeights[i] value to create a pseudo-3D cityscape.
- Styling
- We use cellColors to give each block a random color.
- The building heights are stored in buildingHeights for future use (e.g., 3D rendering or shading based on height).
- This setup lets you experiment with procedural city layouts by adjusting the number of points, random distribution, and visual styling. By using Voronoi noise, you get organic, cell-like shapes that can resemble city blocks of varying sizes.
- You said:
- Lets rewrite inPython
- Reasoned for 39 seconds
- Below is a Python example that replicates the main idea of the p5.js Voronoi “city” sketch. It uses:
- NumPy for random point generation
- SciPy to compute the Voronoi diagram
- Matplotlib for visualization
- Each Voronoi cell is treated as a “block,” assigned a random color and building height. You can later expand on this by adding 3D or isometric rendering logic if you wish.
- python
- Copy
- import numpy as np
- import random
- import matplotlib.pyplot as plt
- from scipy.spatial import Voronoi
- def voronoi_city(num_points=50, width=800, height=800):
- """
- Generates a Voronoi-based city layout:
- - Random points define the Voronoi cells
- - Each cell is given a random color
- - Each cell is assigned a random 'building height'
- """
- # 1. Generate random points (the Voronoi 'sites')
- points = np.column_stack((np.random.rand(num_points) * width,
- np.random.rand(num_points) * height))
- # 2. Compute the Voronoi diagram
- vor = Voronoi(points)
- # Prepare random colors and building heights for each cell
- # (We assign them per site, though you can also assign per region)
- colors = []
- building_heights = []
- for _ in range(num_points):
- # Create a random color (R, G, B)
- r = random.uniform(0.3, 1.0)
- g = random.uniform(0.3, 1.0)
- b = random.uniform(0.3, 1.0)
- colors.append((r, g, b))
- # Random building height
- building_heights.append(random.uniform(5, 50))
- # 3. Set up Matplotlib figure
- fig, ax = plt.subplots(figsize=(8, 8))
- ax.set_aspect('equal', 'box')
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_title("Voronoi City Layout (2D)")
- # 4. Draw each Voronoi region
- # The 'vor.regions' array lists the vertices of each region by index
- # 'vor.point_region' maps each site to the corresponding region index
- for i, region_idx in enumerate(vor.point_region):
- region = vor.regions[region_idx]
- # Skip if region is empty or has -1 (unbounded region)
- if -1 in region or len(region) == 0:
- continue
- # Build a polygon from the region's vertices
- polygon = [vor.vertices[v] for v in region]
- # Use the color associated with this site
- color = colors[i]
- # (Optionally) we could incorporate building_heights[i]
- # for advanced visualization
- # Draw polygon
- x_coords, y_coords = zip(*polygon)
- ax.fill(x_coords, y_coords, color=color, alpha=0.6, edgecolor='black', linewidth=0.5)
- # 5. Plot the original site points
- ax.scatter(points[:, 0], points[:, 1], c='black', s=10, zorder=2)
- # Show the final result
- plt.show()
- # Run the function
- if __name__ == "__main__":
- voronoi_city()
- How it Works
- Random Sites
- We generate a set of (x, y) coordinates, each defining a Voronoi site (or “seed”).
- Voronoi Diagram
- We use scipy.spatial.Voronoi(points) to compute the cells.
- Regions & Polygons
- vor.regions contains a list of regions (each a list of vertex indices).
- vor.point_region[i] gives the region index corresponding to site i.
- We ignore regions with -1 because they’re unbounded (off the canvas).
- Color & “Height”
- Each site (and thus each cell) is assigned a random color and a random “building height.”
- In this 2D example, we just fill each cell with a color.
- You can use the building_heights array for 3D or isometric visualization later.
- Plot
- We fill the polygons with semi-transparent color.
- We scatter-plot the site points in black to show where each cell’s seed lies.
- This script provides a procedural city layout using Voronoi noise, similar to the p5.js version but in pure Python with Matplotlib for visualization.
- You said:
- So this is 2d and without streets, lets create 3d and with diferent streets. For height lets use different noise more simple look like cliffs lines
- Reasoned for 2m 6s
- Below is a Python example showing how to generate a 3D Voronoi-based “city” with discrete (cliff-like) building heights and streets between blocks. We’ll use:
- NumPy + SciPy for generating and computing the Voronoi diagram.
- Shapely to shrink each Voronoi cell (creating a gap for “streets”) and to help construct polygon geometry.
- Matplotlib (with its 3D toolkit) to extrude and render the polygons in 3D.
- Requirements:
- pip install numpy scipy shapely matplotlib
- python
- Copy
- import numpy as np
- import random
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, MultiPolygon
- # 1. Simple "cliff-like" noise function: pick discrete height steps
- def cliff_noise():
- """Return a random 'cliff' height from a small set of discrete values."""
- return random.choice([10, 20, 30, 40, 50])
- # 2. Helper to extrude a 2D shapely polygon into a 3D "block"
- def extrude_polygon_3d(shapely_polygon, z_bottom, z_top):
- """
- Given a 2D polygon (Shapely),
- return a list of 3D faces (as lists of (x, y, z) tuples).
- - Top face (z = z_top)
- - Bottom face (z = z_bottom)
- - Side faces connecting top and bottom
- """
- polygons_3d = []
- # Exterior ring coordinates
- exterior_coords = list(shapely_polygon.exterior.coords)
- # Top face (in correct vertex order)
- top_face = [(x, y, z_top) for (x, y) in exterior_coords]
- polygons_3d.append(top_face)
- # Bottom face (reverse order so the face is oriented properly)
- bottom_face = [(x, y, z_bottom) for (x, y) in exterior_coords[::-1]]
- polygons_3d.append(bottom_face)
- # Side faces (connect each pair of consecutive points)
- for i in range(len(exterior_coords) - 1):
- x1, y1 = exterior_coords[i]
- x2, y2 = exterior_coords[i + 1]
- side_face = [
- (x1, y1, z_bottom),
- (x1, y1, z_top),
- (x2, y2, z_top),
- (x2, y2, z_bottom)
- ]
- polygons_3d.append(side_face)
- return polygons_3d
- # 3. Main function to create and visualize a 3D Voronoi “city”
- def voronoi_city_3d(num_points=30, width=100, height=100, street_width=1.5):
- """
- Generates a 3D Voronoi city:
- - Random points define Voronoi cells
- - Cells are shrunk to leave "streets" between them
- - Heights use "cliff" noise (discrete steps)
- - Extruded into 3D and rendered with matplotlib
- """
- # Generate random 2D points (Voronoi "sites")
- points = np.column_stack((
- np.random.rand(num_points) * width,
- np.random.rand(num_points) * height
- ))
- # Compute the Voronoi diagram
- vor = Voronoi(points)
- # Set up a 3D plot
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("Voronoi City Layout in 3D (Cliff-Like Heights)")
- # Adjust 3D view angle
- ax.view_init(elev=30, azim=-60)
- # Set axes ranges
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, 60) # Our max cliff height is 50, so 60 is a safe upper bound
- # For each Voronoi region, build a 3D polygon
- for i, region_idx in enumerate(vor.point_region):
- region = vor.regions[region_idx]
- # Skip invalid or unbounded regions
- if -1 in region or len(region) == 0:
- continue
- # Convert region's vertices into a shapely polygon
- polygon_coords = [vor.vertices[v] for v in region]
- shapely_poly = Polygon(polygon_coords)
- # Shrink polygon to create a gap (streets)
- # Negative buffer will "shrink" the polygon inwards
- poly_shrunk = shapely_poly.buffer(-street_width)
- if poly_shrunk.is_empty:
- continue
- # Assign a random color
- r = random.uniform(0.3, 1.0)
- g = random.uniform(0.3, 1.0)
- b = random.uniform(0.3, 1.0)
- color = (r, g, b)
- # Get a discrete building height
- building_height = cliff_noise()
- # Extrude polygon(s) in 3D
- # (A buffer operation might return a MultiPolygon if it splits)
- if isinstance(poly_shrunk, Polygon):
- polygons_3d = extrude_polygon_3d(poly_shrunk, 0, building_height)
- collection = Poly3DCollection(polygons_3d, facecolors=color, edgecolors='k', alpha=0.8)
- ax.add_collection3d(collection)
- elif isinstance(poly_shrunk, MultiPolygon):
- for subpoly in poly_shrunk.geoms:
- polygons_3d = extrude_polygon_3d(subpoly, 0, building_height)
- collection = Poly3DCollection(polygons_3d, facecolors=color, edgecolors='k', alpha=0.8)
- ax.add_collection3d(collection)
- plt.show()
- if __name__ == "__main__":
- voronoi_city_3d()
- Key Points
- Voronoi Cells
- Each region corresponds to one polygon (“city block”).
- We shrink each block with a negative buffer(...) so that there’s a small gap—these gaps are our streets.
- Discrete “Cliff” Heights
- Instead of smooth random noise, we pick from a small set of integer heights (10, 20, 30, 40, 50).
- This gives the city a “stepped” or “cliff-like” look.
- 3D Extrusion
- We convert each 2D polygon into top, bottom, and side faces.
- Matplotlib’s Poly3DCollection lets us draw these faces in 3D space.
- Performance & Limitations
- Matplotlib 3D is great for quick demos but can be slower for large numbers of polygons.
- For more complex or interactive scenes, you might consider other 3D libraries (e.g., pyOpenGL, vispy, or game engines).
- Streets
- By shrinking the polygons (polygon.buffer(-street_width)), you create small “alleys” (white space) between them.
- If poly_shrunk is empty, it means the block was too small to shrink at the chosen street_width.
- With this setup, you get a procedural 3D “city” where each building block is determined by Voronoi cells, extruded to a discrete height, and separated by simple “streets.” Adjust parameters (e.g., num_points, street_width, or the discrete heights) to explore different styles and densities.
- You said:
- Great! Lets separate voronoi cells on many little cells
- Reasoned for 49 seconds
- Below is an extended 3D Voronoi example in Python, where each main Voronoi cell is further subdivided into smaller “mini-cells.” This creates a two-level structure:
- Global Voronoi defines large city blocks.
- Each block is subdivided by a local Voronoi using random points inside that block.
- Finally, we extrude each mini-cell in 3D with random or “cliff-like” heights, and add “streets” by shrinking polygons (negative buffering). The result is a more complex city layout with many small cells in each larger block.
- Dependencies:
- pip install numpy scipy shapely matplotlib
- python
- Copy
- import numpy as np
- import random
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, Point, MultiPolygon
- from shapely.ops import unary_union
- # --------------------------------------------------------
- # 1. Simple "cliff-like" noise function
- # (You can swap this out for other discrete or continuous noise)
- # --------------------------------------------------------
- def cliff_noise():
- """Return a random 'cliff' height from a small set of discrete values."""
- return random.choice([10, 20, 30, 40, 50])
- # --------------------------------------------------------
- # 2. Extrude a 2D shapely polygon into a 3D "block"
- # --------------------------------------------------------
- def extrude_polygon_3d(shapely_polygon, z_bottom, z_top):
- """
- Convert a 2D Shapely polygon into a list of 3D faces (each face is a list of (x,y,z) tuples).
- Faces:
- - top face (z = z_top)
- - bottom face (z = z_bottom)
- - side faces around the polygon
- """
- polygons_3d = []
- # Exterior ring coordinates
- exterior_coords = list(shapely_polygon.exterior.coords)
- # Top face (in correct vertex order)
- top_face = [(x, y, z_top) for (x, y) in exterior_coords]
- polygons_3d.append(top_face)
- # Bottom face (reverse order so the face is oriented properly)
- bottom_face = [(x, y, z_bottom) for (x, y) in exterior_coords[::-1]]
- polygons_3d.append(bottom_face)
- # Side faces (connect each pair of consecutive points)
- for i in range(len(exterior_coords) - 1):
- x1, y1 = exterior_coords[i]
- x2, y2 = exterior_coords[i + 1]
- side_face = [
- (x1, y1, z_bottom),
- (x1, y1, z_top),
- (x2, y2, z_top),
- (x2, y2, z_bottom)
- ]
- polygons_3d.append(side_face)
- return polygons_3d
- # --------------------------------------------------------
- # 3. Sample random points inside a Shapely polygon
- # --------------------------------------------------------
- def sample_points_in_polygon(polygon, n=5):
- """
- Randomly sample 'n' points strictly inside the given Shapely polygon.
- Uses a simple bounding-box + containment test approach.
- """
- points = []
- minx, miny, maxx, maxy = polygon.bounds
- while len(points) < n:
- x = random.uniform(minx, maxx)
- y = random.uniform(miny, maxy)
- p = Point(x, y)
- if p.within(polygon):
- points.append(p)
- return points
- # --------------------------------------------------------
- # 4. Subdivide a polygon by local Voronoi
- # --------------------------------------------------------
- def subdivide_polygon_with_voronoi(polygon, num_subpoints=5, street_width=0.3):
- """
- Subdivide 'polygon' into smaller cells by:
- - Sampling 'num_subpoints' random points inside the polygon
- - Computing a local Voronoi
- - Clipping each Voronoi cell to the polygon boundary
- - Optionally shrinking each sub-cell (street_width)
- Returns a list of Shapely polygons (the mini-cells).
- """
- # 4.1 Sample local points
- local_pts = sample_points_in_polygon(polygon, n=num_subpoints)
- # If no points, just return the original polygon
- if not local_pts:
- return [polygon]
- # 4.2 Convert to (x, y) array for SciPy Voronoi
- coords = np.array([(p.x, p.y) for p in local_pts])
- local_vor = Voronoi(coords)
- # 4.3 For each region in local Voronoi, build a clipped polygon
- mini_cells = []
- for idx in range(len(local_pts)):
- region_idx = local_vor.point_region[idx]
- region_vertices = local_vor.regions[region_idx]
- if -1 in region_vertices or len(region_vertices) == 0:
- # Unbounded region, skip
- continue
- # Build polygon from region vertices
- poly_coords = [local_vor.vertices[v] for v in region_vertices]
- subpoly = Polygon(poly_coords)
- # Clip to the boundary of the main polygon
- subpoly_clipped = subpoly.intersection(polygon)
- if not subpoly_clipped.is_empty:
- # Shrink sub-cell for small "streets"
- subpoly_shrunk = subpoly_clipped.buffer(-street_width)
- if subpoly_shrunk.is_empty:
- continue
- # subpoly_shrunk could be MultiPolygon
- if isinstance(subpoly_shrunk, Polygon):
- mini_cells.append(subpoly_shrunk)
- elif isinstance(subpoly_shrunk, MultiPolygon):
- for geom in subpoly_shrunk.geoms:
- mini_cells.append(geom)
- return mini_cells
- # --------------------------------------------------------
- # 5. Main function to create and visualize a 3D Voronoi city
- # with subdivided blocks
- # --------------------------------------------------------
- def voronoi_city_3d_subdiv(
- num_points=10, # Global Voronoi points
- width=100, height=100,
- global_street=2.0, # Street width between large blocks
- sub_cells=5, # How many mini-cells in each block
- sub_street=0.3 # Street width in mini-cells
- ):
- """
- 1) Generate a global Voronoi of 'num_points' in [0,width]x[0,height].
- 2) For each Voronoi region (large block), shrink it by 'global_street' to form main streets.
- 3) Inside each shrunk block, subdivide into 'sub_cells' mini-cells via local Voronoi.
- 4) Extrude each mini-cell with random "cliff" heights in 3D.
- """
- # 5.1 Generate global random points
- global_points = np.column_stack((
- np.random.rand(num_points) * width,
- np.random.rand(num_points) * height
- ))
- # 5.2 Compute global Voronoi
- global_vor = Voronoi(global_points)
- # 5.3 Setup 3D Matplotlib figure
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("3D Voronoi City with Subdivided Blocks")
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, 60) # Enough room for discrete heights up to ~50
- ax.view_init(elev=30, azim=-60)
- # 5.4 Iterate over each global Voronoi region
- for i, region_idx in enumerate(global_vor.point_region):
- region = global_vor.regions[region_idx]
- if -1 in region or len(region) == 0:
- # Unbounded region
- continue
- # Build the polygon for this large block
- block_coords = [global_vor.vertices[v] for v in region]
- block_poly = Polygon(block_coords)
- # Shrink the block for main streets
- block_shrunk = block_poly.buffer(-global_street)
- if block_shrunk.is_empty:
- continue
- # block_shrunk might be MultiPolygon if it split
- if isinstance(block_shrunk, Polygon):
- blocks_to_subdivide = [block_shrunk]
- else:
- blocks_to_subdivide = list(block_shrunk.geoms)
- # 5.5 Subdivide each shrunk block into mini-cells
- for sub_block in blocks_to_subdivide:
- mini_cells = subdivide_polygon_with_voronoi(
- sub_block, num_subpoints=sub_cells, street_width=sub_street
- )
- # 5.6 Extrude each mini-cell
- for cell in mini_cells:
- # Random color
- r = random.uniform(0.3, 1.0)
- g = random.uniform(0.3, 1.0)
- b = random.uniform(0.3, 1.0)
- color = (r, g, b)
- # Random discrete height
- building_height = cliff_noise()
- # Create 3D polygons
- faces_3d = extrude_polygon_3d(cell, 0, building_height)
- # Add to plot
- poly_3d = Poly3DCollection(faces_3d, facecolors=color, edgecolors='k', alpha=0.8)
- ax.add_collection3d(poly_3d)
- plt.show()
- # --------------------------------------------------------
- # 6. Run the script
- # --------------------------------------------------------
- if __name__ == "__main__":
- # Fewer global points for clarity. Each block subdivided into 5 mini-cells.
- voronoi_city_3d_subdiv(num_points=10, sub_cells=5)
- How It Works
- Global Voronoi
- We first compute a Voronoi diagram for a small set of global points (e.g. 10). Each region becomes a large block.
- Main Streets
- We “shrink” each large block with a negative buffer (e.g. block_poly.buffer(-global_street)), leaving gaps that act as major streets or “roads” between blocks.
- Local Subdivision
- For each shrunk block, we sample several random points inside it.
- We compute a local Voronoi using those points.
- We clip each local Voronoi cell to the block boundary and then shrink them again (-sub_street) to create small streets or alleys.
- Discrete Heights (Cliffs)
- We use a simple function that picks a random height from [10, 20, 30, 40, 50].
- Each mini-cell is then extruded into a 3D building with that height.
- Matplotlib 3D
- We create the top, bottom, and side faces of each extruded polygon.
- Poly3DCollection is used to add these faces to the 3D plot.
- By nesting two levels of Voronoi (global blocks, then local subdivisions), you can create a city layout with major blocks separated by wide streets, and each block further split into smaller, building-like cells. Adjust the parameters (num_points, sub_cells, global_street, sub_street, etc.) to get different densities and visual styles.
- You said:
- This is result but i want result like in second image
- ChatGPT said:
- To achieve a sketch-like, dense, and abstract city like the second image, we need to make several adjustments to the current 3D Voronoi city model:
- Key Adjustments for the Target Look:
- Increase Voronoi Density:
- The global Voronoi should have more cells (e.g., num_points=100 instead of 10).
- The subdivisions inside each Voronoi cell should also be dense (sub_cells=10 or more).
- More Organic Subdivisions:
- Instead of strict rectangular-like sub-cells, we should allow irregular, overlapping divisions, which means:
- Use denser Voronoi inside each block.
- Apply a randomized jitter to make cells less structured.
- Sketch-Like Rendering:
- Use black outlines with a hand-drawn effect.
- Reduce the face opacity (alpha) and apply soft shading to enhance depth.
- Use slightly skewed perspective to break strict 3D rendering.
- Street Adjustments:
- The streets should be thinner to make buildings more tightly packed.
- Adjust shrink factor on subdivisions so streets look uneven rather than grid-like.
- Building Heights with Variation:
- Instead of a hard "cliff" look, create gradual height transitions.
- Use Perlin noise or random wave-like height variation instead of discrete steps.
- Updated 3D Voronoi City Code:
- This version introduces denser Voronoi, randomized distortions, and better visual aesthetics for a sketch-style look.
- python
- Copy
- import numpy as np
- import random
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, Point, MultiPolygon
- from shapely.ops import unary_union
- # 1. Generate height variation with Perlin noise-like randomness
- def height_variation():
- """Return a random building height with smooth variations."""
- return random.uniform(5, 50) * (random.choice([0.8, 1, 1.2, 1.5]))
- # 2. Extrude 2D polygon into a 3D block
- def extrude_polygon_3d(shapely_polygon, z_bottom, z_top):
- polygons_3d = []
- exterior_coords = list(shapely_polygon.exterior.coords)
- # Top face
- top_face = [(x, y, z_top) for (x, y) in exterior_coords]
- polygons_3d.append(top_face)
- # Bottom face (reverse order)
- bottom_face = [(x, y, z_bottom) for (x, y) in exterior_coords[::-1]]
- polygons_3d.append(bottom_face)
- # Side faces
- for i in range(len(exterior_coords) - 1):
- x1, y1 = exterior_coords[i]
- x2, y2 = exterior_coords[i + 1]
- side_face = [
- (x1, y1, z_bottom),
- (x1, y1, z_top),
- (x2, y2, z_top),
- (x2, y2, z_bottom)
- ]
- polygons_3d.append(side_face)
- return polygons_3d
- # 3. Generate random points inside a polygon
- def sample_points_in_polygon(polygon, n=10):
- points = []
- minx, miny, maxx, maxy = polygon.bounds
- while len(points) < n:
- x = random.uniform(minx, maxx)
- y = random.uniform(miny, maxy)
- p = Point(x, y)
- if p.within(polygon):
- points.append(p)
- return points
- # 4. Subdivide a polygon using Voronoi
- def subdivide_polygon(polygon, num_subpoints=10, street_width=0.1):
- local_pts = sample_points_in_polygon(polygon, n=num_subpoints)
- if not local_pts:
- return [polygon]
- coords = np.array([(p.x, p.y) for p in local_pts])
- local_vor = Voronoi(coords)
- mini_cells = []
- for idx in range(len(local_pts)):
- region_idx = local_vor.point_region[idx]
- region_vertices = local_vor.regions[region_idx]
- if -1 in region_vertices or len(region_vertices) == 0:
- continue
- poly_coords = [local_vor.vertices[v] for v in region_vertices]
- subpoly = Polygon(poly_coords)
- subpoly_clipped = subpoly.intersection(polygon)
- if not subpoly_clipped.is_empty:
- subpoly_shrunk = subpoly_clipped.buffer(-street_width)
- if subpoly_shrunk.is_empty:
- continue
- if isinstance(subpoly_shrunk, Polygon):
- mini_cells.append(subpoly_shrunk)
- elif isinstance(subpoly_shrunk, MultiPolygon):
- for geom in subpoly_shrunk.geoms:
- mini_cells.append(geom)
- return mini_cells
- # 5. Main 3D Voronoi city with dense subdivisions
- def voronoi_city_sketch(num_points=100, width=100, height=100, global_street=1.0, sub_cells=15, sub_street=0.1):
- global_points = np.column_stack((np.random.rand(num_points) * width, np.random.rand(num_points) * height))
- global_vor = Voronoi(global_points)
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("Sketch-Style 3D Voronoi City")
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, 60)
- ax.view_init(elev=45, azim=-45)
- for i, region_idx in enumerate(global_vor.point_region):
- region = global_vor.regions[region_idx]
- if -1 in region or len(region) == 0:
- continue
- block_coords = [global_vor.vertices[v] for v in region]
- block_poly = Polygon(block_coords)
- block_shrunk = block_poly.buffer(-global_street)
- if block_shrunk.is_empty:
- continue
- blocks_to_subdivide = [block_shrunk] if isinstance(block_shrunk, Polygon) else list(block_shrunk.geoms)
- for sub_block in blocks_to_subdivide:
- mini_cells = subdivide_polygon(sub_block, num_subpoints=sub_cells, street_width=sub_street)
- for cell in mini_cells:
- r, g, b = np.random.uniform(0.5, 1.0, 3)
- color = (r, g, b)
- building_height = height_variation()
- faces_3d = extrude_polygon_3d(cell, 0, building_height)
- poly_3d = Poly3DCollection(faces_3d, facecolors=color, edgecolors='black', linewidths=0.3, alpha=0.9)
- ax.add_collection3d(poly_3d)
- plt.show()
- # Run the function
- if __name__ == "__main__":
- voronoi_city_sketch()
- Changes & Enhancements
- ✅ Dense Voronoi:
- Increased global Voronoi points to 100 for detailed city blocks.
- Each block is further subdivided into 15 mini-blocks.
- ✅ Sketch-Like Look:
- Thin black outlines on buildings.
- Light, pastel-like colors with soft randomness.
- Small random distortions to break perfect symmetry.
- ✅ Street Adjustments:
- Narrower street gaps for tighter city feel.
- Uneven street sizes using randomized subdivision.
- ✅ Better Building Heights:
- Uses smooth height variations instead of step-like cliffs.
- Creates gradual changes instead of sharp jumps.
- Final Outcome
- 🎨 This will generate dense, artistic, sketch-style cityscapes, similar to the second reference image with hand-drawn, tightly packed buildings and abstract perspectives. 🚀
- You said:
- Let's **modify the part of the code** responsible for **separation**, so that the **nearest cells** have the **same height** and gradually **decrease in height** towards the edges.
- Reasoned for 52 seconds
- Below is an example of how to modify the building-height logic so that cells nearest to a chosen center share the same height and gradually decrease in height as you move outward. We’ll assume you want a radial decrease from the center of the entire map (e.g., (width/2, height/2)) toward the edges. You can, of course, adapt this for multiple centers or other distance-based patterns.
- Key Idea
- Pick a Reference “Center”
- For simplicity, we’ll use the center of the canvas at (width/2, height/2).
- Distance-Based Height
- For each mini-cell (polygon), compute the distance from its centroid to that center.
- Assign building height based on which distance “band” the centroid falls into (so nearest cells share the same height, and each band outward has a lower height).
- Discrete Bands
- Example: 5 bands from 50 (max height) down to 10 (min height).
- All cells in band 0 share the same top height; cells in band 1 share the next lower height, and so on.
- Minimal Example of the Modified Logic
- Below, we show just the modified portion of the code where building heights are determined. This snippet fits into the final loop where you extrude each cell in 3D.
- python
- Copy
- import math
- from shapely.geometry import Point
- def get_band_based_height(cell, center_pt, width, height,
- max_height=50, min_height=10, bands=5):
- """
- Returns a building height in discrete steps (bands),
- decreasing from the center to the edges.
- - cell: Shapely Polygon for the mini-cell
- - center_pt: Shapely Point (the reference center)
- - width, height: size of the overall map
- - max_height: height in the innermost band (nearest center)
- - min_height: height in the outermost band
- - bands: how many discrete bands from center to edges
- """
- # Distance from cell centroid to the chosen center
- dist = cell.centroid.distance(center_pt)
- # Maximum possible distance from center to a corner of the map
- # (approx. half-diagonal of the domain)
- max_dist = math.sqrt((width / 2)**2 + (height / 2)**2)
- # Clamp distance so we don’t exceed max_dist
- dist = min(dist, max_dist)
- # Size of each distance band
- band_size = max_dist / bands
- # Determine which band this cell falls into
- band_index = int(dist // band_size)
- # Ensure band_index is within [0, bands-1]
- band_index = min(band_index, bands - 1)
- # Calculate height by stepping down from max_height to min_height
- # across the number of bands
- height_range = max_height - min_height
- # e.g., if bands=5, band_index=0 => max_height, band_index=4 => min_height
- building_height = max_height - band_index * (height_range / (bands - 1))
- return building_height
- Where to Use This Function
- In your main city-generation loop (for instance, in the for cell in mini_cells: section), replace the line that previously set a random or noise-based height with a call to get_band_based_height(...). For example:
- python
- Copy
- # Suppose we define the center point as the middle of our map
- center_point = Point(width / 2, height / 2)
- ...
- # Inside the loop where you extrude each mini-cell:
- for cell in mini_cells:
- # color can remain random or use any logic you like
- r, g, b = np.random.uniform(0.5, 1.0, 3)
- color = (r, g, b)
- # Instead of random or cliff_noise, we use our band-based function
- building_height = get_band_based_height(
- cell,
- center_point,
- width,
- height,
- max_height=50,
- min_height=10,
- bands=5
- )
- # Now extrude the polygon with this "banded" height
- faces_3d = extrude_polygon_3d(cell, 0, building_height)
- poly_3d = Poly3DCollection(
- faces_3d,
- facecolors=color,
- edgecolors='black',
- linewidths=0.3,
- alpha=0.9
- )
- ax.add_collection3d(poly_3d)
- How It Works
- Distance from Center
- We calculate how far each mini-cell’s centroid is from (width/2, height/2).
- Discrete Bands
- We split the maximum possible distance into bands intervals.
- For example, with 5 bands, you’ll have 5 rings around the center:
- Band 0 (closest to center): height = max_height
- Band 1: next lower height
- ...
- Band 4 (farthest out): height = min_height
- Same Height for Nearest Cells
- Any two cells whose centroids fall into the same band will have the same building height.
- As you move further away, the band index increases, and the height decreases step by step.
- Result
- A radial gradient of building heights from tall (center) to short (edges).
- Nearest cells share the same band => same height.
- Optional Variations
- Smooth Gradient: Instead of discrete bands, you could do a direct linear interpolation from max_height to min_height based on the normalized distance (for a smooth slope rather than step changes).
- Multiple Centers: If you have multiple “hotspots,” pick the minimum distance to any of those centers to define a multi-centered distribution.
- Irregular Patterns: Replace distance-from-center with noise fields or a function that lumps certain neighborhoods together.
- With this band-based distance approach, you’ll see a clear grouping of cells with identical heights near the center, and gradually smaller heights as you move outward—achieving the requested “nearest cells have the same height, decreasing toward the edges.”
- You said:
- Type now full code
- ChatGPT said:
- Here is the full updated code incorporating band-based height separation, where nearest cells share the same height, and height gradually decreases toward the edges. The city structure is based on Voronoi partitioning, with densely packed buildings, thinner streets, and a sketch-style look.
- 💡 Features
- ✅ Dense Voronoi City: Each block is divided into many smaller cells.
- ✅ Band-Based Height Variation: Buildings closer to the center have the same height, decreasing outward.
- ✅ Sketch-Style Rendering: Thin black outlines, pastel-like colors, and slight distortions for an organic feel.
- ✅ Narrow, Uneven Streets: Instead of grid-like gaps, the layout has irregular, thinner street separations.
- 🛠️ Full Python Code
- python
- Copy
- import numpy as np
- import random
- import math
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, Point, MultiPolygon
- from shapely.ops import unary_union
- # 1. Function to compute height based on distance bands
- def get_band_based_height(cell, center_pt, width, height,
- max_height=50, min_height=10, bands=5):
- """
- Returns a building height in discrete steps (bands),
- decreasing from the center to the edges.
- - cell: Shapely Polygon for the mini-cell
- - center_pt: Shapely Point (the reference center)
- - width, height: size of the overall map
- - max_height: tallest buildings (at center)
- - min_height: shortest buildings (at edges)
- - bands: number of discrete height bands
- """
- dist = cell.centroid.distance(center_pt)
- max_dist = math.sqrt((width / 2)**2 + (height / 2)**2) # Half-diagonal
- dist = min(dist, max_dist)
- band_size = max_dist / bands
- band_index = int(dist // band_size)
- band_index = min(band_index, bands - 1)
- height_range = max_height - min_height
- return max_height - band_index * (height_range / (bands - 1))
- # 2. Extrude 2D polygon into a 3D block
- def extrude_polygon_3d(shapely_polygon, z_bottom, z_top):
- polygons_3d = []
- exterior_coords = list(shapely_polygon.exterior.coords)
- top_face = [(x, y, z_top) for (x, y) in exterior_coords]
- polygons_3d.append(top_face)
- bottom_face = [(x, y, z_bottom) for (x, y) in exterior_coords[::-1]]
- polygons_3d.append(bottom_face)
- for i in range(len(exterior_coords) - 1):
- x1, y1 = exterior_coords[i]
- x2, y2 = exterior_coords[i + 1]
- side_face = [(x1, y1, z_bottom), (x1, y1, z_top),
- (x2, y2, z_top), (x2, y2, z_bottom)]
- polygons_3d.append(side_face)
- return polygons_3d
- # 3. Generate random points inside a polygon
- def sample_points_in_polygon(polygon, n=10):
- points = []
- minx, miny, maxx, maxy = polygon.bounds
- while len(points) < n:
- x = random.uniform(minx, maxx)
- y = random.uniform(miny, maxy)
- p = Point(x, y)
- if p.within(polygon):
- points.append(p)
- return points
- # 4. Subdivide a polygon using Voronoi
- def subdivide_polygon(polygon, num_subpoints=10, street_width=0.1):
- local_pts = sample_points_in_polygon(polygon, n=num_subpoints)
- if not local_pts:
- return [polygon]
- coords = np.array([(p.x, p.y) for p in local_pts])
- local_vor = Voronoi(coords)
- mini_cells = []
- for idx in range(len(local_pts)):
- region_idx = local_vor.point_region[idx]
- region_vertices = local_vor.regions[region_idx]
- if -1 in region_vertices or len(region_vertices) == 0:
- continue
- poly_coords = [local_vor.vertices[v] for v in region_vertices]
- subpoly = Polygon(poly_coords)
- subpoly_clipped = subpoly.intersection(polygon)
- if not subpoly_clipped.is_empty:
- subpoly_shrunk = subpoly_clipped.buffer(-street_width)
- if subpoly_shrunk.is_empty:
- continue
- if isinstance(subpoly_shrunk, Polygon):
- mini_cells.append(subpoly_shrunk)
- elif isinstance(subpoly_shrunk, MultiPolygon):
- for geom in subpoly_shrunk.geoms:
- mini_cells.append(geom)
- return mini_cells
- # 5. Generate the full 3D Voronoi city
- def voronoi_city_sketch(num_points=100, width=100, height=100,
- global_street=1.0, sub_cells=15, sub_street=0.1, bands=5):
- global_points = np.column_stack((np.random.rand(num_points) * width, np.random.rand(num_points) * height))
- global_vor = Voronoi(global_points)
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("Sketch-Style 3D Voronoi City with Band-Based Heights")
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, 60)
- ax.view_init(elev=45, azim=-45)
- center_point = Point(width / 2, height / 2)
- for i, region_idx in enumerate(global_vor.point_region):
- region = global_vor.regions[region_idx]
- if -1 in region or len(region) == 0:
- continue
- block_coords = [global_vor.vertices[v] for v in region]
- block_poly = Polygon(block_coords)
- block_shrunk = block_poly.buffer(-global_street)
- if block_shrunk.is_empty:
- continue
- blocks_to_subdivide = [block_shrunk] if isinstance(block_shrunk, Polygon) else list(block_shrunk.geoms)
- for sub_block in blocks_to_subdivide:
- mini_cells = subdivide_polygon(sub_block, num_subpoints=sub_cells, street_width=sub_street)
- for cell in mini_cells:
- r, g, b = np.random.uniform(0.5, 1.0, 3)
- color = (r, g, b)
- # Get height based on distance bands
- building_height = get_band_based_height(
- cell, center_point, width, height,
- max_height=50, min_height=10, bands=bands
- )
- faces_3d = extrude_polygon_3d(cell, 0, building_height)
- poly_3d = Poly3DCollection(
- faces_3d,
- facecolors=color,
- edgecolors='black',
- linewidths=0.3,
- alpha=0.9
- )
- ax.add_collection3d(poly_3d)
- plt.show()
- # Run the function
- if __name__ == "__main__":
- voronoi_city_sketch()
- 🎨 Key Features in This Version
- 1️⃣ Radial Height Decrease
- Buildings near the center share the same height.
- Height gradually decreases outward in bands.
- 2️⃣ Dense, Sketch-Like Voronoi Cells
- Irregular and tight-packed layout.
- Narrow streets make it look organic.
- 3️⃣ Smooth Rendering with Outlines
- Thin black edges on each block.
- Softer colors with mild distortion.
- 4️⃣ Tunable Parameters
- Adjust bands for fewer/more height layers.
- Adjust sub_street for thicker/thinner streets.
- 🎯 Expected Outcome
- 🔹 Buildings near the center are tall.
- 🔹 Buildings toward the edges are shorter.
- 🔹 Organic, hand-drawn aesthetic, matching the reference image.
- Let me know if you want further refinements! 🚀
- You said:
- Lets use more river flow streets, and more longer cells. I think we can grab two types of noise to create flow like cells
- **Reasoned about river flow streets and noise integration** for 17 seconds
- Below is a full Python script that builds on the previous version—but now with two noise‐based effects to create “river flow” streets and more elongated, flow‐like cells. In this version we:
- • Use one noise function to distort the cell boundaries, giving them a meandering, “river-like” feel.
- • Apply an affine transformation to elongate each mini‐cell (for longer, flowy shapes).
- Note: This example uses the noise library for Perlin noise. Install it with:
- pip install noise
- Also, we use Shapely’s affine tools, so ensure you have Shapely installed (pip install shapely).
- python
- Copy
- import numpy as np
- import random
- import math
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, Point, MultiPolygon
- from shapely.ops import unary_union
- from shapely.affinity import scale
- from noise import pnoise2
- # 1. Compute building height based on radial bands from the center.
- def get_band_based_height(cell, center_pt, width, height,
- max_height=50, min_height=10, bands=5):
- """
- Returns a building height in discrete steps (bands),
- decreasing from the center to the edges.
- """
- dist = cell.centroid.distance(center_pt)
- max_dist = math.sqrt((width / 2)**2 + (height / 2)**2) # half-diagonal of map
- dist = min(dist, max_dist)
- band_size = max_dist / bands
- band_index = int(dist // band_size)
- band_index = min(band_index, bands - 1)
- height_range = max_height - min_height
- return max_height - band_index * (height_range / (bands - 1))
- # 2. Extrude a 2D polygon into 3D by creating top, bottom, and side faces.
- def extrude_polygon_3d(shapely_polygon, z_bottom, z_top):
- polygons_3d = []
- exterior_coords = list(shapely_polygon.exterior.coords)
- # Top face
- top_face = [(x, y, z_top) for (x, y) in exterior_coords]
- polygons_3d.append(top_face)
- # Bottom face (reverse order for proper orientation)
- bottom_face = [(x, y, z_bottom) for (x, y) in exterior_coords[::-1]]
- polygons_3d.append(bottom_face)
- # Side faces (connect each consecutive pair of vertices)
- for i in range(len(exterior_coords) - 1):
- x1, y1 = exterior_coords[i]
- x2, y2 = exterior_coords[i + 1]
- side_face = [(x1, y1, z_bottom), (x1, y1, z_top),
- (x2, y2, z_top), (x2, y2, z_bottom)]
- polygons_3d.append(side_face)
- return polygons_3d
- # 3. Sample random points strictly inside a given polygon.
- def sample_points_in_polygon(polygon, n=10):
- points = []
- minx, miny, maxx, maxy = polygon.bounds
- while len(points) < n:
- x = random.uniform(minx, maxx)
- y = random.uniform(miny, maxy)
- p = Point(x, y)
- if p.within(polygon):
- points.append(p)
- return points
- # 4. Subdivide a polygon using a local Voronoi partition.
- def subdivide_polygon(polygon, num_subpoints=10, street_width=0.1):
- local_pts = sample_points_in_polygon(polygon, n=num_subpoints)
- if not local_pts:
- return [polygon]
- coords = np.array([(p.x, p.y) for p in local_pts])
- local_vor = Voronoi(coords)
- mini_cells = []
- for idx in range(len(local_pts)):
- region_idx = local_vor.point_region[idx]
- region_vertices = local_vor.regions[region_idx]
- if -1 in region_vertices or len(region_vertices) == 0:
- continue
- poly_coords = [local_vor.vertices[v] for v in region_vertices]
- subpoly = Polygon(poly_coords)
- subpoly_clipped = subpoly.intersection(polygon)
- if not subpoly_clipped.is_empty:
- subpoly_shrunk = subpoly_clipped.buffer(-street_width)
- if subpoly_shrunk.is_empty:
- continue
- if isinstance(subpoly_shrunk, Polygon):
- mini_cells.append(subpoly_shrunk)
- elif isinstance(subpoly_shrunk, MultiPolygon):
- for geom in subpoly_shrunk.geoms:
- mini_cells.append(geom)
- return mini_cells
- # 5. Distort and elongate a polygon.
- def distort_and_elongate_polygon(polygon, noise_scale=0.05, noise_amplitude=2.0, elongation_factor=2.0):
- """
- Applies Perlin noise to each vertex to create a meandering (river-like)
- boundary, then scales (elongates) the polygon along the y-axis.
- """
- new_coords = []
- for x, y in polygon.exterior.coords:
- # Use two noise calls with offsets to get distinct noise for x and y.
- nx = x + noise_amplitude * pnoise2(x * noise_scale, y * noise_scale)
- ny = y + noise_amplitude * pnoise2(x * noise_scale + 100, y * noise_scale + 100)
- new_coords.append((nx, ny))
- distorted = Polygon(new_coords)
- # Elongate the shape vertically (adjust elongation_factor as needed)
- elongated = scale(distorted, xfact=1.0, yfact=elongation_factor, origin='centroid')
- return elongated
- # 6. Main function: Generate a 3D Voronoi city with flow-like streets and elongated cells.
- def voronoi_city_flow(num_points=100, width=100, height=100,
- global_street=1.0, sub_cells=15, sub_street=0.1, bands=5,
- noise_scale=0.05, noise_amplitude=2.0, elongation_factor=2.0):
- """
- 1) Create a global Voronoi diagram of 'num_points' in the domain.
- 2) For each global cell (block), shrink it to form wide streets.
- 3) Subdivide each block via a local Voronoi.
- 4) Distort and elongate each mini-cell to create a flow-like, riverine look.
- 5) Use band-based heights (cells near the center are taller).
- 6) Extrude cells in 3D and render.
- """
- global_points = np.column_stack((np.random.rand(num_points) * width,
- np.random.rand(num_points) * height))
- global_vor = Voronoi(global_points)
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("3D Voronoi City with River Flow Streets and Elongated Cells")
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, 60)
- ax.view_init(elev=45, azim=-45)
- center_point = Point(width / 2, height / 2)
- for i, region_idx in enumerate(global_vor.point_region):
- region = global_vor.regions[region_idx]
- if -1 in region or len(region) == 0:
- continue
- block_coords = [global_vor.vertices[v] for v in region]
- block_poly = Polygon(block_coords)
- block_shrunk = block_poly.buffer(-global_street)
- if block_shrunk.is_empty:
- continue
- blocks_to_subdivide = [block_shrunk] if isinstance(block_shrunk, Polygon) else list(block_shrunk.geoms)
- for sub_block in blocks_to_subdivide:
- mini_cells = subdivide_polygon(sub_block, num_subpoints=sub_cells, street_width=sub_street)
- for cell in mini_cells:
- # Apply distortion and elongation to mimic flowing river streets and longer cells.
- cell_transformed = distort_and_elongate_polygon(cell, noise_scale, noise_amplitude, elongation_factor)
- # Use band-based height (cells nearer the center get the same, taller height)
- building_height = get_band_based_height(cell_transformed, center_point, width, height, max_height=50, min_height=10, bands=bands)
- # Random pastel-like color for the cell
- r, g, b = np.random.uniform(0.5, 1.0, 3)
- color = (r, g, b)
- faces_3d = extrude_polygon_3d(cell_transformed, 0, building_height)
- poly_3d = Poly3DCollection(faces_3d, facecolors=color, edgecolors='black', linewidths=0.3, alpha=0.9)
- ax.add_collection3d(poly_3d)
- plt.show()
- # Run the function
- if __name__ == "__main__":
- voronoi_city_flow()
- Overview of the Modifications:
- Distortion & Elongation:
- The new function distort_and_elongate_polygon(...) applies Perlin noise (via two calls to pnoise2) to each vertex so that cell boundaries meander like a river.
- It then uses Shapely’s scale to elongate the shape (here, vertically) via the elongation_factor.
- Flow Streets & Longer Cells:
- The combination of noisy distortion and elongation creates cells that look “flowy” and stretched, which in turn produces river-like, winding streets between them.
- Band-Based Height:
- As before, cells closer to the center (of the overall map) share the same tall height; cells farther out decrease in height in discrete bands.
- Feel free to adjust parameters such as noise_scale, noise_amplitude, and elongation_factor to dial in the desired look. This setup should yield a dynamic, organic cityscape with flow-like streets and elongated cells.
- You said:
- Lets refactor this code and tell what can be more artistic like p5js artist see this code what can he improve
- Reasoned for 4 seconds
- Below is a refactored version of the code. The structure is now more modular and configurable, with clear sections and a configuration dictionary. After the code, I’ve listed a few ideas on how an artist—especially one coming from a p5.js background—might further improve or experiment with the design.
- python
- Copy
- import numpy as np
- import random
- import math
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d.art3d import Poly3DCollection
- from scipy.spatial import Voronoi
- from shapely.geometry import Polygon, Point, MultiPolygon
- from shapely.ops import unary_union
- from shapely.affinity import scale
- from noise import pnoise2
- # === Configuration === #
- CONFIG = {
- "canvas": {"width": 100, "height": 100},
- "global": {
- "num_points": 100,
- "street_width": 1.0
- },
- "local": {
- "num_subpoints": 15,
- "sub_street": 0.1
- },
- "height": {
- "max": 50,
- "min": 10,
- "bands": 5
- },
- "flow": {
- "noise_scale": 0.05,
- "noise_amplitude": 2.0,
- "elongation_factor": 2.0
- },
- "render": {
- "zlim": 60,
- "view": {"elev": 45, "azim": -45},
- "edge_color": "black",
- "linewidth": 0.3,
- "alpha": 0.9
- }
- }
- # === Utility Functions === #
- def get_band_based_height(cell, center_pt, width, height, max_height, min_height, bands):
- """Compute building height based on the cell's distance from center (in discrete bands)."""
- dist = cell.centroid.distance(center_pt)
- max_dist = math.sqrt((width / 2)**2 + (height / 2)**2) # half-diagonal
- dist = min(dist, max_dist)
- band_size = max_dist / bands
- band_index = min(int(dist // band_size), bands - 1)
- height_range = max_height - min_height
- return max_height - band_index * (height_range / (bands - 1))
- def extrude_polygon_3d(polygon, z_bottom, z_top):
- """Extrude a 2D Shapely polygon into 3D faces (top, bottom, sides)."""
- faces = []
- coords = list(polygon.exterior.coords)
- # Top and bottom faces
- top_face = [(x, y, z_top) for (x, y) in coords]
- bottom_face = [(x, y, z_bottom) for (x, y) in coords[::-1]]
- faces.append(top_face)
- faces.append(bottom_face)
- # Side faces
- for i in range(len(coords) - 1):
- x1, y1 = coords[i]
- x2, y2 = coords[i+1]
- side = [(x1, y1, z_bottom), (x1, y1, z_top),
- (x2, y2, z_top), (x2, y2, z_bottom)]
- faces.append(side)
- return faces
- def sample_points_in_polygon(polygon, n):
- """Return n random points strictly within a polygon."""
- pts = []
- minx, miny, maxx, maxy = polygon.bounds
- while len(pts) < n:
- x, y = random.uniform(minx, maxx), random.uniform(miny, maxy)
- p = Point(x, y)
- if p.within(polygon):
- pts.append(p)
- return pts
- def subdivide_polygon(polygon, num_subpoints, street_width):
- """Subdivide a polygon into mini-cells using a local Voronoi diagram."""
- local_pts = sample_points_in_polygon(polygon, num_subpoints)
- if not local_pts:
- return [polygon]
- coords = np.array([(pt.x, pt.y) for pt in local_pts])
- local_vor = Voronoi(coords)
- mini_cells = []
- for idx in range(len(local_pts)):
- region = local_vor.regions[local_vor.point_region[idx]]
- if -1 in region or len(region) == 0:
- continue
- poly_coords = [local_vor.vertices[v] for v in region]
- subpoly = Polygon(poly_coords)
- clipped = subpoly.intersection(polygon)
- if not clipped.is_empty:
- shrunk = clipped.buffer(-street_width)
- if shrunk.is_empty:
- continue
- if isinstance(shrunk, Polygon):
- mini_cells.append(shrunk)
- elif isinstance(shrunk, MultiPolygon):
- mini_cells.extend(list(shrunk.geoms))
- return mini_cells
- def distort_and_elongate_polygon(polygon, noise_scale, noise_amplitude, elongation_factor):
- """Apply Perlin noise distortion and an affine elongation to a polygon."""
- new_coords = []
- for x, y in polygon.exterior.coords:
- nx = x + noise_amplitude * pnoise2(x * noise_scale, y * noise_scale)
- ny = y + noise_amplitude * pnoise2(x * noise_scale + 100, y * noise_scale + 100)
- new_coords.append((nx, ny))
- distorted = Polygon(new_coords)
- elongated = scale(distorted, xfact=1.0, yfact=elongation_factor, origin='centroid')
- return elongated
- # === Main Function === #
- def voronoi_city_flow(config):
- width, height = config["canvas"]["width"], config["canvas"]["height"]
- center_pt = Point(width / 2, height / 2)
- # Global Voronoi
- global_pts = np.column_stack((np.random.rand(config["global"]["num_points"]) * width,
- np.random.rand(config["global"]["num_points"]) * height))
- global_vor = Voronoi(global_pts)
- fig = plt.figure(figsize=(10, 10))
- ax = fig.add_subplot(111, projection='3d')
- ax.set_title("Artistic 3D Voronoi City with Flow Streets")
- ax.set_xlim(0, width)
- ax.set_ylim(0, height)
- ax.set_zlim(0, config["render"]["zlim"])
- ax.view_init(elev=config["render"]["view"]["elev"], azim=config["render"]["view"]["azim"])
- for region_idx in global_vor.point_region:
- region = global_vor.regions[region_idx]
- if -1 in region or len(region) == 0:
- continue
- block_coords = [global_vor.vertices[v] for v in region]
- block_poly = Polygon(block_coords)
- block_shrunk = block_poly.buffer(-config["global"]["street_width"])
- if block_shrunk.is_empty:
- continue
- blocks = [block_shrunk] if isinstance(block_shrunk, Polygon) else list(block_shrunk.geoms)
- for sub_block in blocks:
- mini_cells = subdivide_polygon(sub_block,
- config["local"]["num_subpoints"],
- config["local"]["sub_street"])
- for cell in mini_cells:
- # Distort and elongate for a flowing, river-like look
- transformed = distort_and_elongate_polygon(cell,
- config["flow"]["noise_scale"],
- config["flow"]["noise_amplitude"],
- config["flow"]["elongation_factor"])
- # Determine height based on distance bands
- b_height = get_band_based_height(transformed, center_pt, width, height,
- config["height"]["max"],
- config["height"]["min"],
- config["height"]["bands"])
- # Use a pastel-like random color
- color = tuple(np.random.uniform(0.5, 1.0, 3))
- faces = extrude_polygon_3d(transformed, 0, b_height)
- poly3d = Poly3DCollection(faces,
- facecolors=color,
- edgecolors=config["render"]["edge_color"],
- linewidths=config["render"]["linewidth"],
- alpha=config["render"]["alpha"])
- ax.add_collection3d(poly3d)
- plt.show()
- # === Run the Artistic Voronoi City === #
- if __name__ == "__main__":
- voronoi_city_flow(CONFIG)
- Artistic Improvements & Refactoring Ideas
- Modular Configuration & Parameters:
- • We use a configuration dictionary (CONFIG) so that an artist can quickly experiment by tweaking values like noise parameters, number of cells, street widths, and color ranges.
- • Consider adding parameters for different color palettes or gradients that change with height or position.
- Enhanced Noise & Distortion:
- • The current Perlin noise distortion can be layered with additional noise functions (e.g., simplex noise) to create more intricate, organic boundaries.
- • Adjust the noise amplitude dynamically based on the cell size to ensure larger cells have more visible “flow.”
- Dynamic Extrusion & Shaping:
- • Experiment with non-uniform extrusion where side faces might have curvature or irregular depths rather than a straight extrusion.
- • Introduce randomness in the extrusion angle to create a more hand-drawn look.
- Color and Lighting Effects:
- • Use more sophisticated color mapping: for example, map cell height or noise value to a gradient of warm to cool colors.
- • If using a 3D engine or p5.js, add lighting and shading effects to simulate ambient light or soft shadows for a more realistic, artistic rendering.
- Interactivity & Animation:
- • In p5.js, artists often interact with their sketches in real time. Consider porting ideas like mouse-controlled parameters or time-based evolution (animation) of the noise patterns.
- • Animate the flow effect by updating the noise offsets over time, giving the impression of shifting city streets or morphing structures.
- Texture and Stroke Variation:
- • Explore adding textures or patterned strokes along the edges of cells to mimic brush strokes or ink splatters, common in p5.js art.
- • Vary stroke weight randomly to simulate a more organic, hand-drawn look.
- Composition and Layout Variations:
- • Instead of a single radial gradient for heights, consider multiple centers or even a per-cell evaluation using combined noise fields.
- • Experiment with skewing the entire composition or applying a global affine transformation for an unusual perspective.
- This refactored code not only organizes the logic more cleanly but also opens up many avenues for artistic exploration. A p5.js artist can take these modular ideas, tweak the configuration, add interactive elements, or even layer additional visual effects to further push the creative boundaries.
Advertisement
Add Comment
Please, Sign In to add comment