# BlenderBIM Add-on - OpenBIM Blender Add-on # Copyright (C) 2024 @Andrej730 # # This file is part of BlenderBIM Add-on. # # BlenderBIM Add-on is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # BlenderBIM Add-on is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . import os import bpy import csv import random import ifcopenshell import ifcopenshell.api import blenderbim.tool as tool from math import cos, sin, tan, pi from pathlib import Path from itertools import chain from ifcopenshell.util.shape_builder import ShapeBuilder, V from collections import namedtuple from mathutils import Vector, Matrix from random import uniform # When run from Blender BLEND_DIR = os.path.dirname(bpy.data.filepath) OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Landscape Library.ifc") SimpleTreeParams = namedtuple("SimpleTreeParams", "plant_height crown_diameter trunk_diameter") LowPolyTreeParams = namedtuple( "LowPolyTreeParams", "plant_height crown_diameter crown_max_loc crown_taper trunk_height trunk_diameter random_seed" ) PalmTreeParams = namedtuple("PalmTreeParams", "plant_height crown_diameter trunk_diameter") TreePresetData = namedtuple("TreePresetData", "preset_class generator") GeneratedGeometry = namedtuple("GeneratedGeometry", "items_2d items_3d") def generate_low_poly_tree( builder: ShapeBuilder, crown_taper=0.5, crown_max_loc=0.3, plant_height=12.0, trunk_height=2.0, crown_diameter=4, trunk_diameter=0.3, random_seed=10, ) -> GeneratedGeometry: unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file) random.seed(random_seed) res = 6 plant_height *= unit_conversion trunk_height *= unit_conversion crown_diameter *= unit_conversion trunk_diameter *= unit_conversion crown_height = plant_height - trunk_height def circle(center, r, res): points = [] for i in range(res): # fmt: off vert = ( cos(i * 2 * pi / res) * r + center[0], sin(i * 2 * pi / res) * r + center[1], 0.0 + center[2] ) # fmt: on points.append(vert) return points def merge_lists(lists): merged_list = [] for i in range(len(lists)): merged_list = merged_list + lists[i] return merged_list def XY_scale(vert, scale): vert_scaled = (vert[0] * scale, vert[1] * scale, vert[2]) return vert_scaled def rand_add(vert, amp): # fmt: off vert_new = ( vert[0] + uniform(-amp, amp), vert[1] + uniform(-amp, amp), vert[2] + uniform(-amp, amp) ) # fmt: on return vert_new height_segments_trunk = 2 height_segments_crown = 7 height_segments_total = height_segments_trunk + height_segments_crown verts = [] # Trunk for i in range(height_segments_trunk): points = circle((0.0, 0.0, i * trunk_height), trunk_diameter / 2, res) verts.append(points) # Crown h_crown_max_loc = int((height_segments_crown - 1) * crown_max_loc) crown_indices = [] for i in range(0, h_crown_max_loc): crown_indices.append(i) for i in range(0, height_segments_crown - h_crown_max_loc): crown_indices.append(h_crown_max_loc - i) my_min_val = min(crown_indices) my_max_val = max(crown_indices) n_crown_taper = crown_taper * 0.8 crown_diameters = [] for x in crown_indices: crown_diameters.append(((x - my_min_val) / (my_max_val - my_min_val)) * n_crown_taper + (1 - n_crown_taper)) randomize_amplitude = plant_height / 50 crown_segment_height = crown_height / height_segments_crown for i in range(0, height_segments_crown - 1): points = circle( (0.0, 0.0, trunk_height + (i + 1) * crown_segment_height), crown_diameter * crown_diameters[i] / 2, res, ) for i in range(0, len(points)): points[i] = rand_add(points[i], randomize_amplitude) verts.append(points) # Top verts.append(circle((0.0, 0.0, plant_height), 0.05, res)) edge_loops = [] for i in range(height_segments_total): edge_loops.append(range(i * res, (i + 1) * res)) verts = merge_lists(verts) faces = [] bottom = [*edge_loops[0]] faces.append(bottom) for i in range(height_segments_total - 1): for j in range(res): face = (edge_loops[i][j - 1], edge_loops[i][j], edge_loops[i + 1][j], edge_loops[i + 1][j - 1]) faces.append(face) top = [*edge_loops[-1]] faces.append(top) verts_3D = [verts] faces_3D = [faces] verts_2D = [ [ (-0.5865642428398132, 0.8647078275680542, 0.0), (-0.7583824992179871, 0.6572927236557007, 0.0), (-0.7234422564506531, 0.5865941047668457, 0.0), (-0.8714070916175842, 0.46631893515586853, 0.0), (-0.9203394055366516, 0.15792329609394073, 0.0), (-0.6487269997596741, 0.11522211134433746, 0.0), (-0.9273074269294739, 0.0903107076883316, 0.0), (-0.9606701731681824, -0.2852219343185425, 0.0), (-0.8060722351074219, -0.35055163502693176, 0.0), (-0.8271104693412781, -0.4677186608314514, 0.0), (-0.6899875998497009, -0.6695915460586548, 0.0), (-0.6215555667877197, -0.6767659187316895, 0.0), (-0.4315463900566101, -0.8824808597564697, 0.0), (-0.30316293239593506, -0.9023936986923218, 0.0), (-0.32218849658966064, -0.8037619590759277, 0.0), (-0.21792533993721008, -0.9530860185623169, 0.0), (0.36917445063591003, -0.877334475517273, 0.0), (0.30831706523895264, -0.685136079788208, 0.0), (0.4778200685977936, -0.8094909191131592, 0.0), (0.9163193106651306, -0.368840754032135, 0.0), (0.827597439289093, -0.29377281665802, 0.0), (0.9478662610054016, -0.245104119181633, 0.0), (0.9667968153953552, 0.08458180725574493, 0.0), (0.7529001832008362, 0.05212751030921936, 0.0), (0.9102436900138855, 0.19098728895187378, 0.0), (0.8279229998588562, 0.5522171258926392, 0.0), (0.6635335087776184, 0.6461355686187744, 0.0), (0.5753684639930725, 0.8170149326324463, 0.0), (0.3976244330406189, 0.8779193162918091, 0.0), (0.18733686208724976, 0.7513588666915894, 0.0), (0.338344544172287, 0.910933256149292, 0.0), (-0.013130240142345428, 0.9798095226287842, 0.0), (-0.18649885058403015, 0.9414206743240356, 0.0), (-0.3193224370479584, 0.8835403919219971, 0.0), ] ] edges_2D = [] faces_2D = [] verts_2D_new = [] for vert in verts_2D[0]: verts_2D_new.append(XY_scale(vert, crown_diameter / 2)) range_of_verts = range(0, len(verts_2D[0])) for i in range_of_verts: edges_2D.append([range_of_verts[i - 1], i]) faces_2D.append(i) verts_2D[0] = verts_2D_new edges_2D = [edges_2D] faces_2D = [[faces_2D]] verts_2D = [Vector(v).to_2d() for v in verts_2D[0]] shape_2d = builder.polyline(points=verts_2D, closed=True) shape_3d = builder.polygonal_face_set(points=verts_3D[0], faces=faces_3D[0]) return GeneratedGeometry((shape_2d,), (shape_3d,)) def generate_simple_tree( builder: ShapeBuilder, plant_height=2, crown_diameter=2, trunk_diameter=0.1 ) -> GeneratedGeometry: unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file) plant_height *= unit_conversion crown_diameter *= unit_conversion trunk_diameter *= unit_conversion crown_radius = crown_diameter / 2 # 3d shape trunk_base = builder.circle(radius=trunk_diameter / 2) trunk = builder.extrude(trunk_base, magnitude=plant_height - crown_diameter) crown_position = (0.0, 0.0, (plant_height - crown_radius)) crown = builder.sphere(radius=crown_radius, center=crown_position) # 2d shape shape_2D = builder.circle(radius=crown_radius) return GeneratedGeometry((shape_2D,), (trunk, crown)) def generate_palm_tree( builder: ShapeBuilder, plant_height=2, crown_diameter=5.8, trunk_diameter=0.1 ) -> GeneratedGeometry: unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file) palm_verts = [ Vector((-3.6415634155273438, -3.8275668621063232, -1.0948246717453003)), Vector((-0.3698049783706665, -1.805966854095459, 0.493910551071167)), Vector((-1.6397790908813477, -0.5056067705154419, 0.04353363811969757)), Vector((0.0, 0.0, 0.0)), Vector((5.023162364959717, -1.6531240940093994, -1.0948246717453003)), Vector((1.8090602159500122, 0.4589407444000244, 0.493910551071167)), Vector((1.1666079759597778, -1.2413609027862549, 0.04353363811969757)), Vector((1.6942416429519653, 5.038582801818848, -1.0948246717453003)), Vector((-0.43215513229370117, 1.833944320678711, 0.493910551071167)), Vector((1.2652605772018433, 1.1839056015014648, 0.04353363811969757)), Vector((-4.577561378479004, 2.689336061477661, -1.0948246717453003)), Vector((-1.8646581172943115, -0.0367276668548584, 0.493910551071167)), Vector((-0.8873085975646973, 1.4957730770111084, 0.04353363811969757)), ] palm_faces = [(2, 1, 0), (2, 3, 1), (6, 5, 4), (6, 3, 5), (9, 8, 7), (9, 3, 8), (12, 11, 10), (12, 3, 11)] # each segment starts with the last point from the previous palm_segments_2d_ifc = [(3, 11, 10, 12, 3), (3, 1, 0, 2, 3), (3, 5, 4, 6, 3), (3, 8, 7, 9, 3)] palm_segments_2d_ifc = [[i + 1 for i in segment] for segment in palm_segments_2d_ifc] # IFC indices start with 1 scale = Vector() scale.xyz = crown_diameter / 11.6 leaves_height = 0.493910551071167 * scale.z plant_height -= leaves_height for v in palm_verts: v *= scale v.z += plant_height v *= unit_conversion plant_height *= unit_conversion crown_diameter *= unit_conversion trunk_diameter *= unit_conversion # 3d leaves = builder.polygonal_face_set(palm_verts, palm_faces) trunk_base = builder.circle(radius=trunk_diameter / 2) trunk = builder.extrude(trunk_base, magnitude=plant_height) # 2d remove_z = Vector((1, 1, 0)) verts_2D = [v * remove_z for v in palm_verts] ifc_points = builder.file.createIfcCartesianPointList2D(verts_2D) ifc_segments = [] for segment in palm_segments_2d_ifc: ifc_segments.append(builder.file.createIfcLineIndex(segment)) shape_2d = builder.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments) return GeneratedGeometry((shape_2d,), (leaves, trunk)) tree_presets = { "simple": TreePresetData(SimpleTreeParams, generate_simple_tree), "low_poly": TreePresetData(LowPolyTreeParams, generate_low_poly_tree), "palm_tree": TreePresetData(PalmTreeParams, generate_palm_tree), } class LibraryGenerator: def generate(self, library_name, output_filename): ifcopenshell.api.pre_listeners = {} ifcopenshell.api.post_listeners = {} self.materials = {} self.file = ifcopenshell.api.run("project.create_file") self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name) self.library = ifcopenshell.api.run( "root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name ) ifcopenshell.api.run( "project.assign_declaration", self.file, definitions=[self.library], relating_context=self.project ) unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit]) model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan") self.representations = { "Model/Body/MODEL_VIEW": ifcopenshell.api.run( "context.add_context", self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model, ), "Plan/Body/PLAN_VIEW": ifcopenshell.api.run( "context.add_context", self.file, context_type="Plan", context_identifier="Body", target_view="PLAN_VIEW", parent=plan, ), "Model/Body/PLAN_VIEW": ifcopenshell.api.run( "context.add_context", self.file, context_type="Model", context_identifier="Body", target_view="PLAN_VIEW", parent=model, ), "Model/Body/SECTION_VIEW": ifcopenshell.api.run( "context.add_context", self.file, context_type="Model", context_identifier="Body", target_view="SECTION_VIEW", parent=model, ), } # Manually modeled trees for obj in bpy.data.objects: if not obj.type == "MESH": continue if "Plan/" in obj.name or "Model/" in obj.name: continue representations = {"Model/Body/MODEL_VIEW": obj.name} for rep_key in self.representations.keys(): rep_obj = bpy.data.objects.get(obj.name + " " + rep_key) if rep_obj: representations[rep_key] = rep_obj.name self.create_type("IfcGeographicElementType", obj.name, representations) # Auto generated generic trees self.builder = ShapeBuilder(self.file) builder = self.builder TreeData = namedtuple("TreeData", "tree_name preset_name preset_data") trees = [ TreeData("Conical Tree Small (5m)", "low_poly", (5.0, 1.13, 0.0, 1.0, 0.28, 0.16, 5)), TreeData("Conical Tree Medium (10m)", "low_poly", (10.0, 2.7, 0.0, 1.0, 0.32, 0.22, 4)), TreeData("Conical Tree Big (15m)", "low_poly", (15.0, 4.37, 0.0, 1.0, 0.74, 0.44, 3)), TreeData("Palm Tree (15m)", "palm_tree", (15.0, 16.64, 0.4)), TreeData("Shrub Small (0.4m)", "low_poly", (0.4, 0.4, 0.535, 0.524, 0.0, 0.0, 10)), TreeData("Shrub Big (0.8m)", "low_poly", (0.8, 0.8, 0.535, 0.524, 0.0, 0.0, 10)), TreeData("Lollipop Tree (4m)", "simple", (4.0, 2.5, 0.2)), TreeData("Lollipop Tree (6m)", "simple", (6.0, 3.5, 0.2)), TreeData("Lollipop Tree (8m)", "simple", (8.0, 4.5, 0.2)), TreeData("Lollipop Tree (10m)", "simple", (10.0, 5.5, 0.44)), TreeData("Lollipop Tree (14m)", "simple", (14.0, 7.5, 0.44)), TreeData("Lollipop Tree (18m)", "simple", (18.0, 9.5, 0.44)), TreeData("Lollipop Tree (25m)", "simple", (25.0, 13.5, 0.50)), TreeData("Tree Small (8m)", "low_poly", (8.0, 6.0, 0.0, 0.8, 1.0, 0.2, 10)), TreeData("Tree Medium (12m)", "low_poly", (12.0, 10.0, 0.0, 0.8, 2.0, 0.4, 20)), TreeData("Tree Big (20m)", "low_poly", (20.0, 15.0, 0.0, 0.8, 4.0, 0.6, 30)), ] for tree_data in trees: preset = tree_presets.get(tree_data.preset_name) preset_data = preset.preset_class(*tree_data.preset_data)._asdict() tree_geometry = preset.generator(builder, **preset_data) self.create_explicit_type( "IfcGeographicElementType", tree_data.tree_name, **self.get_representations(tree_geometry) ) # From tree species table with open(os.path.join(BLEND_DIR, "tree_species.csv"), "r") as csvfile: reader = csv.reader(csvfile) for i, row in enumerate(reader): if i == 0: continue preset = tree_presets.get(row[0]) data = [float(x) for x in row[2:]] preset_data = preset.preset_class(*data)._asdict() tree_geometry = preset.generator(builder, **preset_data) self.create_explicit_type("IfcGeographicElementType", row[1], **self.get_representations(tree_geometry)) self.file.write(output_filename) def get_representations(self, generated_geometry: GeneratedGeometry): representation_2d = self.builder.get_representation( self.representations["Plan/Body/PLAN_VIEW"], items=generated_geometry.items_2d ) representation_3d = self.builder.get_representation( self.representations["Model/Body/MODEL_VIEW"], items=generated_geometry.items_3d ) return { "representation_3d": representation_3d, "representation_2d": representation_2d, } def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d, **params): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) for param, value in params.items(): setattr(element, param, value) ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation_3d ) ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation_2d ) ifcopenshell.api.run( "project.assign_declaration", self.file, definitions=[element], relating_context=self.library ) return element def create_type(self, ifc_class, name, representations): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) for rep_name, obj_name in representations.items(): obj = bpy.data.objects.get(obj_name) representation = ifcopenshell.api.run( "geometry.add_representation", self.file, context=self.representations[rep_name], blender_object=obj, geometry=obj.data, total_items=max(1, len(obj.material_slots)), ) styles = [] for slot in obj.material_slots: style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name) ifcopenshell.api.run( "style.add_surface_style", self.file, style=style, ifc_class="IfcSurfaceStyleShading", attributes=tool.Style.get_surface_shading_attributes(slot.material), ) styles.append(style) if styles: ifcopenshell.api.run( "style.assign_representation_styles", self.file, shape_representation=representation, styles=styles ) ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) ifcopenshell.api.run( "project.assign_declaration", self.file, definitions=[element], relating_context=self.library ) if __name__ == "__main__": LibraryGenerator().generate("Landscape Assets Library", output_filename=OUT_PATH)