Trying to create a script for creating a road/highway system
Last night, with the help of Google Gemini, I have a script created to help design highways by adding straight road objects, curved road objects, and intersections, and even have the ability to create road shoulders and even create slopes (for hills).
Adding a straight road section with shoulders is no problem. However, I am experiencing problems with creating a curved road section with shoulders, and the intersection should have curved shoulders on the T-intersection.
Here is the code I have so far, in Python script:
import bpy
import bmesh
import math
import random
from mathutils import Vector, Euler
def get_or_create_material(mat_name):
if mat_name not in bpy.data.materials:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
if mat_name == "RoadTexture":
bsdf.inputs['Base Color'].default_value = (0.1, 0.1, 0.1, 1)
elif mat_name == "GravelTexture":
bsdf.inputs['Base Color'].default_value = (0.3, 0.3, 0.3, 1)
return mat
return bpy.data.materials[mat_name]
def create_straight_road(name, start_point, length, width, shoulder_width, slope, lanes):
bm = bmesh.new()
lane_width = width / lanes
v_road_left_front = bm.verts.new((0, 0, 0))
v_road_right_front = bm.verts.new((0, width, 0))
v_road_left_back = bm.verts.new((length, 0, 0))
v_road_right_back = bm.verts.new((length, width, 0))
bm.faces.new((v_road_left_front, v_road_right_front, v_road_right_back, v_road_left_back))
me_road = bpy.data.meshes.new(name + "_road_mesh")
bm.to_mesh(me_road)
bm.free()
obj_road = bpy.data.objects.new(name + "_road", me_road)
bpy.context.collection.objects.link(obj_road)
bm_shoulders = bmesh.new()
v_sl_front = bm_shoulders.verts.new((0, -shoulder_width, 0))
v_sl_back = bm_shoulders.verts.new((length, -shoulder_width, 0))
v_sl_road_back = bm_shoulders.verts.new((length, 0, 0))
v_sl_road_front = bm_shoulders.verts.new((0, 0, 0))
bm_shoulders.faces.new((v_sl_front, v_sl_road_front, v_sl_road_back, v_sl_back))
v_sr_front = bm_shoulders.verts.new((0, width, 0))
v_sr_back = bm_shoulders.verts.new((length, width, 0))
v_sr_road_back = bm_shoulders.verts.new((length, width + shoulder_width, 0))
v_sr_road_front = bm_shoulders.verts.new((0, width + shoulder_width, 0))
bm_shoulders.faces.new((v_sr_front, v_sr_road_front, v_sr_road_back, v_sr_back))
me_shoulders = bpy.data.meshes.new(name + "_shoulders_mesh")
bm_shoulders.to_mesh(me_shoulders)
bm_shoulders.free()
obj_shoulders = bpy.data.objects.new(name + "_shoulders", me_shoulders)
bpy.context.collection.objects.link(obj_shoulders)
mat_road = get_or_create_material("RoadTexture")
mat_gravel = get_or_create_material("GravelTexture")
obj_road.data.materials.append(mat_road)
obj_shoulders.data.materials.append(mat_gravel)
obj_road.location = start_point
obj_shoulders.location = start_point
obj_road.rotation_euler = Euler((0, math.radians(slope), 0))
obj_shoulders.rotation_euler = Euler((0, math.radians(slope), 0))
return obj_road, obj_shoulders
def create_curved_road(name, start_point, radius, angle, width, shoulder_width, segments):
bm_road = bmesh.new()
bm_shoulders = bmesh.new()
center = (start_point[0], start_point[1] + radius, start_point[2])
start_angle_rad = 0
end_angle_rad = math.radians(angle)
angle_step = (end_angle_rad - start_angle_rad) / segments
prev_road_v_inner = None
prev_road_v_outer = None
prev_shoulder_v_inner = None
prev_shoulder_v_outer = None
for i in range(segments + 1):
current_angle = start_angle_rad + (i * angle_step)
# Calculate road vertices
x_road_inner = center[0] + radius * math.cos(current_angle)
y_road_inner = center[1] + radius * math.sin(current_angle)
x_road_outer = center[0] + (radius + width) * math.cos(current_angle)
y_road_outer = center[1] + (radius + width) * math.sin(current_angle)
road_v_inner = bm_road.verts.new((x_road_inner, y_road_inner, center[2]))
road_v_outer = bm_road.verts.new((x_road_outer, y_road_outer, center[2]))
# Calculate shoulder vertices
x_shoulder_inner = center[0] + (radius - shoulder_width) * math.cos(current_angle)
y_shoulder_inner = center[1] + (radius - shoulder_width) * math.sin(current_angle)
x_shoulder_outer = center[0] + (radius + width + shoulder_width) * math.cos(current_angle)
y_shoulder_outer = center[1] + (radius + width + shoulder_width) * math.sin(current_angle)
shoulder_v_inner = bm_shoulders.verts.new((x_shoulder_inner, y_shoulder_inner, center[2]))
shoulder_v_outer = bm_shoulders.verts.new((x_shoulder_outer, y_shoulder_outer, center[2]))
if i > 0:
# Create road face
bm_road.faces.new((road_v_inner, road_v_outer, prev_road_v_outer, prev_road_v_inner))
# **FIXED:** Create inner shoulder face using only shoulder vertices
bm_shoulders.faces.new((shoulder_v_inner, prev_shoulder_v_inner, prev_road_v_inner, road_v_inner))
# **FIXED:** Create outer shoulder face using only shoulder vertices
bm_shoulders.faces.new((road_v_outer, prev_road_v_outer, prev_shoulder_v_outer, shoulder_v_outer))
prev_road_v_inner = road_v_inner
prev_road_v_outer = road_v_outer
prev_shoulder_v_inner = shoulder_v_inner
prev_shoulder_v_outer = shoulder_v_outer
me_road = bpy.data.meshes.new(name + "_road_mesh")
bm_road.to_mesh(me_road)
bm_road.free()
obj_road = bpy.data.objects.new(name + "_road", me_road)
bpy.context.collection.objects.link(obj_road)
me_shoulders = bpy.data.meshes.new(name + "_shoulders_mesh")
bm_shoulders.to_mesh(me_shoulders)
bm_shoulders.free()
obj_shoulders = bpy.data.objects.new(name + "_shoulders", me_shoulders)
bpy.context.collection.objects.link(obj_shoulders)
mat_road = get_or_create_material("RoadTexture")
mat_gravel = get_or_create_material("GravelTexture")
obj_road.data.materials.append(mat_road)
obj_shoulders.data.materials.append(mat_gravel)
obj_road.location = start_point
obj_shoulders.location = start_point
return obj_road, obj_shoulders
def create_intersection(name, start_point, width, shoulder_width):
bm_road = bmesh.new()
size = (width + shoulder_width * 2) / 2
v1 = bm_road.verts.new((start_point[0] - size, start_point[1] - size, start_point[2]))
v2 = bm_road.verts.new((start_point[0] + size, start_point[1] - size, start_point[2]))
v3 = bm_road.verts.new((start_point[0] + size, start_point[1] + size, start_point[2]))
v4 = bm_road.verts.new((start_point[0] - size, start_point[1] + size, start_point[2]))
bm_road.faces.new((v1, v2, v3, v4))
me_road = bpy.data.meshes.new(name + "_mesh")
bm_road.to_mesh(me_road)
bm_road.free()
obj_road = bpy.data.objects.new(name, me_road)
bpy.context.collection.objects.link(obj_road)
mat_road = get_or_create_material("RoadTexture")
obj_road.data.materials.append(mat_road)
return obj_road
def get_last_road_end_point(obj):
if obj and "Road" in obj.name:
local_end = Vector((obj.dimensions.x, 0, 0))
return obj.matrix_world @ local_end
return None
class RoadProperties(bpy.types.PropertyGroup):
lanes: bpy.props.IntProperty(
name="Lanes",
description="Number of lanes",
default=2,
min=1,
max=4
)
lane_width: bpy.props.FloatProperty(
name="Lane Width",
description="Width of a single lane",
default=3.5,
min=2.0,
max=5.0
)
length: bpy.props.FloatProperty(
name="Length",
description="Length of the straight road",
default=10.0,
min=1.0,
max=50.0
)
radius: bpy.props.FloatProperty(
name="Radius",
description="Radius of the curved road",
default=10.0,
min=1.0,
max=50.0
)
angle: bpy.props.IntProperty(
name="Angle",
description="Bend angle of the curved road",
default=90,
min=0,
max=180
)
segments: bpy.props.IntProperty(
name="Segments",
description="Number of faces for the curved road",
default=20,
min=1,
max=100
)
slope: bpy.props.FloatProperty(
name="Slope",
description="Road incline/decline in degrees",
default=0.0,
min=-20.0,
max=20.0
)
shoulder_width: bpy.props.FloatProperty(
name="Shoulder Width",
description="Width of the road shoulders",
default=1.0,
min=0.1,
max=5.0
)
randomize: bpy.props.BoolProperty(
name="Randomize Parameters",
description="Randomly generate a road section",
default=False
)
class CreateStraightRoadOperator(bpy.types.Operator):
bl_idname = "object.create_straight_road"
bl_label = "Create Straight Road"
def execute(self, context):
props = context.scene.road_props
if props.randomize:
props.length = random.uniform(5, 30)
props.lanes = random.randint(1, 4)
props.lane_width = random.uniform(3, 4)
props.shoulder_width = random.uniform(0.5, 2.0)
props.slope = random.uniform(-10, 10)
road_width = props.lanes * props.lane_width
start_point = (0, 0, 0)
last_obj = bpy.context.view_layer.objects.active
if last_obj and "Road" in last_obj.name:
start_point = get_last_road_end_point(last_obj)
create_straight_road("StraightRoad", start_point, props.length, road_width, props.shoulder_width, props.slope, props.lanes)
return {'FINISHED'}
class CreateCurvedRoadOperator(bpy.types.Operator):
bl_idname = "object.create_curved_road"
bl_label = "Create Curved Road"
def execute(self, context):
props = context.scene.road_props
if props.randomize:
props.radius = random.uniform(5, 30)
props.angle = random.randint(10, 180)
props.lanes = random.randint(1, 4)
props.lane_width = random.uniform(3, 4)
props.shoulder_width = random.uniform(0.5, 2.0)
props.segments = random.randint(10, 50)
road_width = props.lanes * props.lane_width
start_point = (0, 0, 0)
last_obj = bpy.context.view_layer.objects.active
if last_obj and "Road" in last_obj.name:
start_point = get_last_road_end_point(last_obj)
create_curved_road("CurvedRoad", start_point, props.radius, props.angle, road_width, props.shoulder_width, props.segments)
return {'FINISHED'}
class CreateIntersectionOperator(bpy.types.Operator):
bl_idname = "object.create_intersection"
bl_label = "Create Intersection"
def execute(self, context):
props = context.scene.road_props
if props.randomize:
props.lanes = random.randint(1, 4)
props.lane_width = random.uniform(3, 4)
props.shoulder_width = random.uniform(0.5, 2.0)
road_width = props.lanes * props.lane_width
start_point = (0, 0, 0)
create_intersection("Intersection", start_point, road_width, props.shoulder_width)
return {'FINISHED'}
class RoadGeneratorPanel(bpy.types.Panel):
bl_label = "Road Generator"
bl_idname = "VIEW3D_PT_road_generator"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Create'
def draw(self, context):
layout = self.layout
props = context.scene.road_props
row = layout.row(align=True)
row.prop(props, "randomize", toggle=True)
box = layout.box()
box.label(text="Road Parameters:")
box.prop(props, "lanes")
box.prop(props, "lane_width")
box.prop(props, "length")
box = layout.box()
box.label(text="Curved Road Parameters:")
box.prop(props, "radius")
box.prop(props, "angle")
box.prop(props, "segments")
box = layout.box()
box.label(text="Advanced Parameters:")
box.prop(props, "shoulder_width")
box.prop(props, "slope")
layout.separator()
layout.label(text="Create Road Section:")
layout.operator("object.create_straight_road", text="Create Straight Road")
layout.operator("object.create_curved_road", text="Create Curved Road")
layout.operator("object.create_intersection", text="Create Intersection")
def register():
bpy.utils.register_class(RoadProperties)
bpy.utils.register_class(RoadGeneratorPanel)
bpy.utils.register_class(CreateStraightRoadOperator)
bpy.utils.register_class(CreateCurvedRoadOperator)
bpy.utils.register_class(CreateIntersectionOperator)
bpy.types.Scene.road_props = bpy.props.PointerProperty(type=RoadProperties)
def unregister():
bpy.utils.unregister_class(RoadProperties)
bpy.utils.unregister_class(RoadGeneratorPanel)
bpy.utils.unregister_class(CreateStraightRoadOperator)
bpy.utils.unregister_class(CreateCurvedRoadOperator)
bpy.utils.unregister_class(CreateIntersectionOperator)
del bpy.types.Scene.road_props
if __name__ == "__main__":
register()
What can be done with regards to editing the code in order to successfully create curved roads with shoulders, and T-intersections with curved shoulders at the junction?
