When working with object manipulation in PHP, it’s not just buildings that can benefit from creative scripting—any object can! This script takes a composite object, breaks it into separate components, and applies a random texture to each part. While simple in concept, it’s a fun way to experiment with procedural generation, whether for games, simulations, or visualizations.
The script assumes the object is pre-structured for separation (e.g., an array of parts). It then iterates through each segment, assigning a random texture from a predefined set—think wood, metal, or stone. The result? A visually diverse object with minimal effort. However, there’s a catch: it’s not optimized. For objects with a huge number of parts, the script can take forever to run, as it processes each component individually without performance tweaks like caching or batching.
Here’s the code:
import bpy
import secrets
import os
import random
import mathutils
from mathutils import Vector
uv_image_scale=4
num_images_to_use=40
image_name_3= '/home/gavin/Pictures/blender/seamless-texture-facade-building.jpg'
image_name_1= '/home/gavin/Pictures/blender/buildings-apartments-shopfronts-shutters/building_side2.png'
image_name_4= '/home/gavin/Pictures/blender/buildings-apartments-shopfronts-shutters/building_office13.png'
image_name_2= '/home/gavin/Pictures/blender/buildings-apartments-shopfronts-shutters/apartments6.png'
image_name_1='/home/gavin/Pictures/blender/sidewalk-displace.png'
texture_dir = '/home/gavin/Pictures/blender/buildings-apartments-shopfronts-shutters/'
roof_texture = '/home/gavin/Pictures/blender/sidewalk-displace.png'
def get_random_texture_from_directory(directory):
"""Get a random texture file from the directory."""
textures = [f for f in os.listdir(directory) if f.endswith(('png', 'jpg', 'jpeg'))]
if textures:
return os.path.join(directory, random.choice(textures))
else:
return None
def map_uvs_to_normal(part, collection):
"""Map UVs based on face normal, rotating face to align with Z-axis and then back."""
uv_layer = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
for face in part.data.polygons:
loop_indices = face.loop_indices
face_normal = face.normal.normalized()
# Step 1: Create a rotation matrix that aligns the face normal with the Z-axis
# Calculate the axis of rotation and the angle required to rotate face normal to align with Z-axis
x_axis = mathutils.Vector((1, 0, 0))
y_axis = mathutils.Vector((0, 1, 0))
z_axis = mathutils.Vector((0, 0, 1))
axis_of_rotation = face_normal.cross(z_axis)
angle_of_rotation = face_normal.angle(z_axis)
# If the angle is non-zero, apply a rotation matrix
if axis_of_rotation.length > 0.0:
rotation_matrix = mathutils.Matrix.Rotation(angle_of_rotation, 4, axis_of_rotation)
else:
rotation_matrix = mathutils.Matrix.Identity(4) # No rotation needed if already aligned
# Step 2: Apply the rotation matrix to the face vertices
uv_data = uv_layer.data
transformed_coords = []
for i in loop_indices:
vertex = part.data.vertices[part.data.loops[i].vertex_index]
transformed_pos = rotation_matrix @ vertex.co # Apply rotation to align face normal with Z-axis
transformed_coords.append(transformed_pos.xy) # Keep the 2D XY coordinates
# Step 3: Compute the bounding box in 2D (after rotation)
min_x, min_y = min(c[0] for c in transformed_coords), min(c[1] for c in transformed_coords)
max_x, max_y = max(c[0] for c in transformed_coords), max(c[1] for c in transformed_coords)
# Compute face width and height
face_width = max_x - min_x
face_height = max_y - min_y
# Compute aspect ratio scaling
if face_width > face_height:
scale_x = 1.0
scale_y = face_height / face_width # Shrink Y to maintain aspect ratio
else:
scale_x = face_width / face_height # Shrink X to maintain aspect ratio
scale_y = 1.0
# Step 4: Normalize UV coordinates while maintaining aspect ratio
for i, loop_index in enumerate(loop_indices):
uv_x = (transformed_coords[i][0] - min_x) / face_width if face_width > 0 else 0.5
uv_y = (transformed_coords[i][1] - min_y) / face_height if face_height > 0 else 0.5
# Apply aspect ratio scaling and center the texture
uv_x = (uv_x - 0.5) * scale_x + 0.5
uv_y = (uv_y - 0.5) * scale_y + 0.5
uv_data[loop_index].uv = (uv_x, uv_y)
# Link the object to the collection and update its data
collection.objects.link(part)
part.data.update()
def map_uvs_to_bbox(part,collection):
"""Map UVs to a 0.0 - 1.0 bounding box based on face orientation."""
uv_layer_flat = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
uv_layer_vertical = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
uv_layer_other = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
# Assign materials
roof_index = -1
for idx, mat in enumerate(obj.data.materials):
if mat.name == "Material_Roof":
roof_index = idx
break
obj_material=random.randint(4, num_images_to_use+3)
for face in part.data.polygons:
loop_indices = face.loop_indices
face_normal = face.normal.normalized()
if abs(face_normal.z) > 0.95:
uv_layer = uv_layer_flat
face.material_index = roof_index
elif (abs(face_normal.x) > 0.95) or (abs(face_normal.y) > 0.95):
uv_layer = uv_layer_vertical
face.material_index = obj_material
else:
uv_layer = uv_layer_other
face.material_index = obj_material
uv_data = uv_layer.data
if len(loop_indices) == 4: # Quads
uv_coords = [(0.0, 0.0), (uv_image_scale, 0.0), (uv_image_scale, uv_image_scale), (0.0, uv_image_scale)]
for i, loop_index in enumerate(loop_indices):
uv_data[loop_index].uv = uv_coords[i]
elif len(loop_indices) == 3: # Triangles
uv_coords = [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)]
for i, loop_index in enumerate(loop_indices):
uv_data[loop_index].uv = uv_coords[i]
else:
# Select UV projection based on dominant axis
if abs(face_normal.z) > 0.9: # Flat (XZ plane)
uv_layer = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
projection_axis = ('x', 'y') # Map to X-Y space
elif abs(face_normal.x) > 0.99 or abs(face_normal.y) > 0.99: # Vertical (YZ plane)
uv_layer = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
projection_axis = ('y', 'z') # Map to Y-Z space
else: # Other orientations
uv_layer = get_or_create_uv_layer(part, 'UVMap_NormalAligned')
projection_axis = ('x', 'z') # Map to X-Z space
uv_data = uv_layer.data
# Get face vertex positions in the selected 2D projection
coords = [(getattr(part.data.vertices[part.data.loops[i].vertex_index].co, projection_axis[0]),
getattr(part.data.vertices[part.data.loops[i].vertex_index].co, projection_axis[1]))
for i in loop_indices]
# Compute bounds
min_x, min_y = min(c[0] for c in coords), min(c[1] for c in coords)
max_x, max_y = max(c[0] for c in coords), max(c[1] for c in coords)
for i, loop_index in enumerate(loop_indices):
uv_x = (coords[i][0] - min_x) / (max_x - min_x) if max_x > min_x else 0.5
uv_y = (coords[i][1] - min_y) / (max_y - min_y) if max_y > min_y else 0.5
uv_data[loop_index].uv = (uv_x, uv_y)
collection.objects.link(part)
part.data.update()
def get_or_create_uv_layer(obj, name):
"""Check if UV layer exists; if not, create a new one."""
if name not in obj.data.uv_layers:
return obj.data.uv_layers.new(name=name)
return obj.data.uv_layers.get(name)
def get_or_create_material(name, image_name, uv_map_name, imagename):
"""Create or retrieve a material and assign a UV map to it."""
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
# Create the image
texImage = mat.node_tree.nodes.new('ShaderNodeTexImage')
texImage.image = bpy.data.images.load(imagename)
img = bpy.data.images.new(name=image_name, width=1024, height=1024)
img.generated_type = 'UV_GRID'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
uv_map_node = mat.node_tree.nodes.new('ShaderNodeUVMap')
uv_map_node.uv_map = uv_map_name
if bsdf:
tex_image = mat.node_tree.nodes.new('ShaderNodeTexImage')
tex_image.image = img
mat.node_tree.links.new(texImage.outputs['Color'], bsdf.inputs['Base Color'])
mat.node_tree.links.new(uv_map_node.outputs['UV'], texImage.inputs['Vector'])
return mat
# Ensure we're in OBJECT mode
bpy.ops.object.mode_set(mode='OBJECT')
# Get the active object
obj = bpy.context.object
if obj and obj.type == 'MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.mode_set(mode='OBJECT')
obj.data.materials.clear()
# Generate unique names for images
image_name1 = secrets.token_hex(5)
image_name2 = secrets.token_hex(5)
image_name3 = secrets.token_hex(5)
# Create materials
material_roof = get_or_create_material("Material_Roof", secrets.token_hex(5), "UVMap_NormalAligned", roof_texture)
material_flat = get_or_create_material("Material_1", image_name1, "UVMap_NormalAligned",image_name_1)
material_vertical = get_or_create_material("Material_2", image_name2, "UVMap_NormalAligned",image_name_2)
material_other = get_or_create_material("Material_3", image_name3, "UVMap_NormalAligned",image_name_3)
obj.data.materials.append(material_roof)
obj.data.materials.append(material_flat)
obj.data.materials.append(material_vertical)
obj.data.materials.append(material_other)
# Create 20 random materials with random textures
for i in range(num_images_to_use):
random_texture = get_random_texture_from_directory(texture_dir)
if random_texture:
material_name = f"Material_{i+1}"
image_name = secrets.token_hex(5)
material = get_or_create_material(material_name, image_name, "UVMap_NormalAligned", random_texture)
obj.data.materials.append(material)
print(f"Assigned materials to {len(obj.data.polygons)} faces.")
# Separate by loose parts
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')
separated_objects = [o for o in bpy.context.selected_objects if o != obj]
new_collection = bpy.data.collections.new("Separated_Meshes")
scene_collection = bpy.context.view_layer.layer_collection.collection
scene_collection.children.link(new_collection)
bpy.context.view_layer.update()
for part in separated_objects:
map_uvs_to_bbox(part,new_collection)
#original_obj = bpy.context.object
#copy_obj = original_obj.copy()
#copy_obj.data = original_obj.data.copy()
#new_collection.objects.link(copy_obj)
#map_uvs_to_bbox(obj,new_collection)
map_uvs_to_bbox(obj,new_collection)
print("UV mapping applied and meshes separated successfully.")
else:
print("No valid mesh object selected.")
This approach works for small-scale projects but needs refinement for efficiency. Still, it’s a great starting point for hobbyists looking to play with object customization in PHP!