City Builder
In this tutorial we'll create a dense grid of buildings, then cut away from them to place roads with curves. We'll also make use of a generator to combine the buildings with the roads.
Setting Up
Create a Bezier Curve object. You can enter edit mode and delete the default curve it creates.
Then create a new script. Setting up an external editor is recommended.
Import Geometry Script, and create a basic tree builder function. We'll add a few arguments to configure the buildings.
from geometry_script import *
@tree("City Builder")
def city_builder(
geometry: Geometry,
seed: Int,
road_width: Float = 0.25,
size_x: Float = 5, size_y: Float = 5, density: Float = 10,
building_size_min: Vector = (0.1, 0.1, 0.2),
building_size_max: Vector = (0.3, 0.3, 1),
):
return geometry
Run the script to create the tree, then add a Geometry Nodes modifier to your curve object and select the City Builder node group.
Buildings
Let's start with the buildings. We'll distribute points on a grid with size_x
and size_y
.
def city_builder(...):
building_points = grid(size_x=size_x, size_y=size_y).distribute_points_on_faces(density=density, seed=seed).points
return building_points
Next, we'll instance cubes on these points to serve as our buildings. We move the cube object up half its height so the buildings sit flat on the grid, and scale them randomly between the min and max sizes.
def city_builder(...):
...
return building_points.instance_on_points(
instance=cube().transform(translation=(0, 0, 0.5)),
scale=random_value(data_type=RandomValue.DataType.FLOAT_VECTOR, min=building_size_min, max=building_size_max, seed=seed),
)
Roads
Using curve_to_mesh
, we can turn the input curve into a flat mesh. We'll use the yield
keyword to join the curve mesh and the building mesh automatically. Change the building_points.instance_on_points
line to use yield
for this to work.
def city_builder(...):
yield geometry.curve_to_mesh(profile_curve=curve_line(
start=combine_xyz(x=road_width * -0.5),
end=combine_xyz(x=road_width * 0.5)
))
...
yield building_points.instance_on_points(...)
But now the buildings are overlapping the road. We need to remove any point that falls within the road curve. We'll use geometry_proximity
and delete_geometry
to find and remove these invalid points.
def city_builder(...):
...
building_points = ...
road_points = geometry.curve_to_points(mode=CurveToPoints.Mode.EVALUATED).points
building_points = building_points.delete_geometry(
domain=DeleteGeometry.Domain.POINT,
selection=geometry_proximity(target_element=GeometryProximity.TargetElement.POINTS, target=road_points, source_position=position()).distance < road_width
)
...
Drawing Roads
Enter edit mode and select the Draw tool. Simply draw roads onto your city to see the buildings and meshes update.
Final Script
from geometry_script import *
@tree("City Builder")
def city_builder(
geometry: Geometry,
seed: Int,
road_width: Float = 0.25,
size_x: Float = 5, size_y: Float = 5, density: Float = 10,
building_size_min: Vector = (0.1, 0.1, 0.2),
building_size_max: Vector = (0.3, 0.3, 1),
):
# Road mesh
yield geometry.curve_to_mesh(profile_curve=curve_line(
start=combine_xyz(x=road_width * -0.5),
end=combine_xyz(x=road_width * 0.5)
))
# Building points
building_points = grid(size_x=size_x, size_y=size_y).distribute_points_on_faces(density=density, seed=seed).points
road_points = geometry.curve_to_points(mode=CurveToPoints.Mode.EVALUATED).points
# Delete points within the curve
building_points = building_points.delete_geometry(
domain=DeleteGeometry.Domain.POINT,
selection=geometry_proximity(target_element=GeometryProximity.TargetElement.POINTS, target=road_points, source_position=position()).distance < road_width
)
# Building instances
yield building_points.instance_on_points(
instance=cube().transform(translation=(0, 0, 0.5)),
scale=random_value(data_type=RandomValue.DataType.FLOAT_VECTOR, min=building_size_min, max=building_size_max, seed=seed),
)