Skip to content

Instantly share code, notes, and snippets.

@romanvolodin
Created October 8, 2018 16:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romanvolodin/3bfd349795443118a2298819349a0b3c to your computer and use it in GitHub Desktop.
Save romanvolodin/3bfd349795443118a2298819349a0b3c to your computer and use it in GitHub Desktop.
import json
from math import (
acos,
atan2,
cos,
radians,
sin,
)
import bpy
import bmesh
from bmesh.types import (
BMVert,
BMEdge,
BMFace,
)
from mathutils import (
Vector,
Matrix,
)
def add_empty(name):
obj = bpy.data.objects.new(name, None)
obj.empty_draw_type = 'ARROWS'
obj.empty_draw_size = 0.25
scene = bpy.context.scene
scene.objects.link(obj)
scene.update()
return obj
def calc_angle_signed_between_two_vectors(a, b):
a_norm = a.normalized()
b_norm = b.normalized()
n = get_positive_normal_from_two_vectors(a_norm, b_norm)
angle = atan2(
a_norm.cross(b_norm).dot(n),
a_norm.dot(b_norm)
)
return angle
def calc_rotation_matrix(x_local, y_local, z_local):
x_global = Vector((1, 0, 0))
y_global = Vector((0, 1, 0))
z_global = Vector((0, 0, 1))
global_to_local = Matrix(
(
(x_local.dot(x_global), x_local.dot(y_global), x_local.dot(z_global), 0),
(y_local.dot(x_global), y_local.dot(y_global), y_local.dot(z_global), 0),
(z_local.dot(x_global), z_local.dot(y_global), z_local.dot(z_global), 0),
(0, 0, 0, 1),
)
)
return global_to_local.transposed()
def clear_screen():
import os
os.system("clear")
def calc_angle_between_two_vectors(a, b):
angle = acos(
a.dot(b) / (a.length * b.length)
)
return angle
def get_normal_from_two_vectors(X, Z):
A = Matrix(
(
(X.y, Z.y),
(X.z, Z.z),
)
).determinant()
B = Matrix(
(
(X.x, Z.x),
(X.z, Z.z),
)
).determinant()
C = Matrix(
(
(X.x, Z.x),
(X.y, Z.y),
)
).determinant()
return Vector((A, B, C,))
def get_positive_normal_from_two_vectors(X, Z):
A = Matrix(
(
(X.y, Z.y),
(X.z, Z.z),
)
).determinant()
B = -Matrix(
(
(X.x, Z.x),
(X.z, Z.z),
)
).determinant()
C = Matrix(
(
(X.x, Z.x),
(X.y, Z.y),
)
).determinant()
if A < 0:
return Vector((-A, -B, -C))
if A == 0:
if B < 0:
return Vector((-A, -B, -C))
if B == 0:
if C < 0:
return Vector((-A, -B, -C))
return Vector((A, B, C))
def load_data(filepath):
try:
with open(filepath, 'r', encoding='utf-8') as json_file:
return json.load(json_file)
except json.decoder.JSONDecodeError:
return
def fill_wall_corners_positions(width, height):
return [
(0, 0, 0),
(width, 0, 0),
(width, 0, height),
(0, 0, height),
]
def fill_wall_corners_vertices(bm, width, height):
corner_positions = {
"wall_bottom_left": (0, 0, 0),
"wall_bottom_right": (width, 0, 0),
"wall_top_right": (width, 0, height),
"wall_top_left": (0, 0, height),
}
for layer_name, vertex_position in corner_positions.items():
vertex = bm.verts.new(vertex_position)
layer = bm.verts.layers.int.new(layer_name)
vertex[layer] = 1
return bm
def get_tallest_window(window_shapes):
return max(
window_shapes,
key=lambda shape: shape['height']
)
def sum_window_widths(window_shapes):
return sum(
[shape['width'] for shape in window_shapes]
)
def get_arc_points(start_angle=0.0,
sector=180.0,
count=8,
radius=1.0,
center=(0.0, 0.0, 0.0),
height=1.0):
step = sector / count
vertices = []
for i, v in enumerate(range(count + 1)):
rad = radians(step * i) + radians(start_angle)
x = radius * cos(rad) + center[0]
y = center[1]
z = radius * height * sin(rad) + center[2]
vertices.append((x, y, z))
return vertices
def get_window_bottom_verts(window_left_side,
window_right_side,
window_bottom_side):
return [
(window_left_side, 0, window_bottom_side),
(window_right_side, 0, window_bottom_side),
]
def get_window_top_verts(window_type,
window_left_side,
window_right_side,
window_top_side,
top_height):
if window_type == "RECTANGLE":
return [
(window_right_side, 0, window_top_side),
(window_left_side, 0, window_top_side)
]
if window_type == "ROUND":
radius = (window_right_side - window_left_side) / 2
center_x = window_left_side + radius
center_z = window_top_side - (radius * top_height)
center = (center_x, 0, center_z)
verts = get_arc_points(
radius=radius,
center=center,
height=top_height,
)
return verts
def fill_windows_vertex_positions(wall_width,
wall_height,
window_shapes,
margin_x,
margin_y,
space_between):
verts = []
window_count = len(window_shapes)
between_count = window_count - 1
common_window_widths = sum_window_widths(window_shapes)
block_width = common_window_widths + (space_between * between_count)
block_height = get_tallest_window(window_shapes)['height']
margin_vertical = (wall_height - block_height) * margin_y
margin_horiz = (wall_width - block_width) * margin_x
for shape in window_shapes:
window_vertices = []
window_type = shape['type']
window_width = shape['width']
window_height = shape['height']
top_height = shape['top_height']
window_bottom_side = margin_vertical + (block_height - window_height)
window_top_side = window_bottom_side + window_height
window_left_side = margin_horiz
window_right_side = window_left_side + window_width
window_vertices.extend(
get_window_bottom_verts(window_left_side,
window_right_side,
window_bottom_side)
)
window_vertices.extend(
get_window_top_verts(window_type,
window_left_side,
window_right_side,
window_top_side,
top_height)
)
margin_horiz += window_width + space_between
verts.append(window_vertices)
return verts
def fill_bm_verts(bm_object, positions):
for pos in positions:
bm_object.verts.new(pos)
return bm_object
def move_geo_to_center(bm_object, width, height):
for v in bm_object.verts:
v.co.x -= width / 2
v.co.z -= height / 2
return bm_object
def get_windows_edge_normals(windows_vertex_positions):
normals = []
bm = bmesh.new()
for window in windows_vertex_positions:
window_bm_verts = []
for v in window:
bm_vert = bm.verts.new(v)
window_bm_verts.append(bm_vert)
window_face = bm.faces.new(window_bm_verts)
extruded = bmesh.ops.extrude_face_region(bm, geom=[window_face])
bmesh.ops.translate(
bm,
vec=Vector((0, 1, 0)),
verts=[v for v in extruded["geom"] if isinstance(v, BMVert)]
)
bm.faces.remove(window_face)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.normal_update()
window_normals = []
for bm_vertex in window_bm_verts:
window_normals.append(
(bm_vertex.co, bm_vertex.normal)
)
normals.append(window_normals)
# bm = move_geo_to_center(bm, width, height)
bmesh_to_object(bm, 'windows', scene)
bm.free()
return normals
def add_object_profile_to_bmesh(bm_object, obj, scene):
old_verts = set(bm_object.verts)
old_edges = set(bm_object.edges)
old_faces = set(bm_object.faces)
bm_object.from_object(obj, scene)
updated_verts = set(bm_object.verts)
updated_edges = set(bm_object.edges)
updated_faces = set(bm_object.faces)
new_verts = updated_verts.difference(old_verts)
new_edges = updated_edges.difference(old_edges)
new_faces = updated_faces.difference(old_faces)
profile = {
'verts': list(new_verts),
'edges': list(new_edges),
'faces': list(new_faces),
}
return profile
def bmesh_to_object(bm_object, name, scene):
mesh = bpy.data.meshes.new(name)
bm_object.to_mesh(mesh)
obj = bpy.data.objects.new(name, mesh)
scene.objects.link(obj)
scene.objects.active = obj
obj.select = True
return obj
def add_bottom_polygon(bm, wall_bm_verts, windows_bm_verts):
bottom_polygon_verts = [
wall_bm_verts[0],
wall_bm_verts[1],
]
for wind in windows_bm_verts[::-1]:
bottom_polygon_verts.extend(
(wind[1], wind[0])
)
bm.faces.new(bottom_polygon_verts)
def add_top_polygon(bm, wall_bm_verts, windows_bm_verts):
top_polygon_verts = [
wall_bm_verts[1],
wall_bm_verts[2],
wall_bm_verts[3],
wall_bm_verts[0],
]
for windw in windows_bm_verts:
top_polygon_verts.append(windw[0])
for bm_v in windw[2::][::-1]:
top_polygon_verts.append(bm_v)
top_polygon_verts.append(windw[1])
bm.faces.new(top_polygon_verts)
def fill_wall_bm_verts(wall_vertex_positions):
wall_bm_verts = []
for v in wall_vertex_positions:
bv = bm.verts.new(v)
wall_bm_verts.append(bv)
return wall_bm_verts
def fill_windows_bm_verts(windows_vertex_positions):
windows_bm_verts = []
for win in windows_vertex_positions:
window_bm_verts = []
for v in win:
bv = bm.verts.new(v)
window_bm_verts.append(bv)
windows_bm_verts.append(window_bm_verts)
return windows_bm_verts
if __name__ == "__main__":
clear_screen()
print("~" * 40)
filepath = ("/project/tech/scripts/blender/bmesh/window_builder/5_win.json")
scene = bpy.context.scene
windows_data = load_data(filepath)
width = windows_data['width']
height = windows_data['height']
margin_x = windows_data['margin_x']
margin_y = windows_data['margin_y']
space_between = windows_data['space_between']
window_shapes = windows_data['window_shapes']
wall_vertex_positions = fill_wall_corners_positions(width, height)
windows_vertex_positions = fill_windows_vertex_positions(
width,
height,
window_shapes,
margin_x,
margin_y,
space_between,
)
bm = bmesh.new()
wall_bm_verts = fill_wall_bm_verts(wall_vertex_positions)
windows_bm_verts = fill_windows_bm_verts(windows_vertex_positions)
add_top_polygon(bm, wall_bm_verts, windows_bm_verts)
add_bottom_polygon(bm, wall_bm_verts, windows_bm_verts)
bm.normal_update()
# bm = move_geo_to_center(bm, width, height)
bmesh_to_object(bm, 'wall', scene)
bm.free()
###########################################################################
#
# D E C O R
#
###########################################################################
target_obj = bpy.data.objects["profile"]
windows_edge_normals = get_windows_edge_normals(windows_vertex_positions)
for i, window_normals in enumerate(windows_edge_normals, start=1):
print("before", i)
for num, normal in enumerate(window_normals):
vert_co, vert_normal = normal
print(
" co: {}\n"
"normal {}".format(vert_co, vert_normal)
)
for i, window_normals in enumerate(windows_edge_normals, start=1):
print("after", i)
bm = bmesh.new()
last_profile = None
for num, normal in enumerate(window_normals):
vert_co, vert_normal = normal
print(
" co: {}\n"
"normal {}".format(vert_co, vert_normal)
)
wall_normal = Vector((0, -1, 0))
kind_of_Y = get_normal_from_two_vectors(wall_normal, vert_normal)
rotation_matrix = calc_rotation_matrix(
wall_normal,
kind_of_Y,
vert_normal
)
new_profile = add_object_profile_to_bmesh(bm, target_obj, scene)
bmesh.ops.rotate(
bm,
verts=new_profile['verts'],
cent=(0.0, 0.0, 0.0),
matrix=rotation_matrix
)
bmesh.ops.translate(
bm,
verts=new_profile['verts'],
vec=vert_co
)
if last_profile is not None:
bmesh.ops.bridge_loops(
bm,
edges=last_profile['edges'] + new_profile['edges']
)
last_profile = new_profile
if num == 0:
first_profile = new_profile
bmesh.ops.bridge_loops(
bm,
edges=last_profile['edges'] + first_profile['edges']
)
bmesh_to_object(bm, 'decor', scene)
bm.free()
scene.update()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment