From a2018a22a6ee6a1138fe2cdb632a466589298bae Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 17 Jul 2024 13:58:54 +1000 Subject: [PATCH] Add sun path visualisation for solar analysis --- .../blenderbim/bim/module/light/__init__.py | 1 + .../blenderbim/bim/module/light/operator.py | 100 +++++++++++------- .../blenderbim/bim/module/light/prop.py | 72 ++++++++----- .../blenderbim/bim/module/light/ui.py | 26 ++++- src/blenderbim/blenderbim/tool/blender.py | 7 ++ 5 files changed, 136 insertions(+), 70 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/light/__init__.py b/src/blenderbim/blenderbim/bim/module/light/__init__.py index 4434460f3b..c24cc4d89a 100644 --- a/src/blenderbim/blenderbim/bim/module/light/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/light/__init__.py @@ -25,6 +25,7 @@ classes = ( operator.ImportLatLong, operator.ImportTrueNorth, operator.RadianceRender, + operator.VisualiseShadows, prop.BIMSolarProperties, prop.RadianceExporterProperties, ui.BIM_PT_radiance_exporter, diff --git a/src/blenderbim/blenderbim/bim/module/light/operator.py b/src/blenderbim/blenderbim/bim/module/light/operator.py index b555925928..6613f02da5 100644 --- a/src/blenderbim/blenderbim/bim/module/light/operator.py +++ b/src/blenderbim/blenderbim/bim/module/light/operator.py @@ -28,11 +28,12 @@ import blenderbim.tool as tool from pathlib import Path from typing import Union from blenderbim.bim.module.light.data import SolarData - +from blenderbim.bim.module.light.decorator import SolarDecorator class ExportOBJ(bpy.types.Operator): """Exports the IFC File to OBJ""" + bl_idname = "export_scene.radiance" bl_label = "Export" bl_description = "Export the IFC to OBJ" @@ -43,21 +44,21 @@ class ExportOBJ(bpy.types.Operator): quality = context.scene.radiance_exporter_properties.radiance_quality.upper() detail = context.scene.radiance_exporter_properties.radiance_detail.upper() variability = context.scene.radiance_exporter_properties.radiance_variability.upper() - + # Calculate the aspect ratio aspect_ratio = resolution_x / resolution_y # Get the blend file path and create the "Radiance Rendering" directory - self.report({'INFO'}, "Exporting Radiance files...") + self.report({"INFO"}, "Exporting Radiance files...") blend_file_path = bpy.data.filepath if not blend_file_path: - self.report({'ERROR'}, "Please save the Blender file before exporting.") - return {'CANCELLED'} + self.report({"ERROR"}, "Please save the Blender file before exporting.") + return {"CANCELLED"} blend_file_dir = os.path.dirname(blend_file_path) radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") - - self.report({'INFO'}, "Radiance directory: {}".format(radiance_dir)) + + self.report({"INFO"}, "Radiance directory: {}".format(radiance_dir)) if not os.path.exists(radiance_dir): os.makedirs(radiance_dir) @@ -67,7 +68,6 @@ class ExportOBJ(bpy.types.Operator): # Settings for obj serializer_settings = ifcopenshell.geom.serializer_settings() - settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS) settings.set("apply-default-materials", True) @@ -76,19 +76,17 @@ class ExportOBJ(bpy.types.Operator): ifc_file_name = context.scene.radiance_exporter_properties.ifc_file_name ifc_file_path = os.path.join(blend_file_dir, f"{ifc_file_name}.ifc") - + if not os.path.exists(ifc_file_path): - self.report({'ERROR'}, f"IFC file not found: {ifc_file_path}") - return {'CANCELLED'} - + self.report({"ERROR"}, f"IFC file not found: {ifc_file_path}") + return {"CANCELLED"} ifc_file = ifcopenshell.open(ifc_file_path) for material in ifc_file.by_type("IfcMaterial"): - self.report({'INFO'}, f"Material: {material.Name}, ID: {material.id()}") + self.report({"INFO"}, f"Material: {material.Name}, ID: {material.id()}") obj_file_path = os.path.join(radiance_dir, "model.obj") mtl_file_path = os.path.join(radiance_dir, "model.mtl") - # serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, ifcopenshell.geom.serializer_settings()) serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings) @@ -104,8 +102,8 @@ class ExportOBJ(bpy.types.Operator): break serialiser.finalize() - return {'FINISHED'} - + return {"FINISHED"} + def getResolution(self, context): scene = context.scene props = scene.radiance_exporter_properties @@ -116,31 +114,31 @@ class ExportOBJ(bpy.types.Operator): class RadianceRender(bpy.types.Operator): """Radiance Rendering""" + bl_idname = "render_scene.radiance" bl_label = "Render" bl_description = "Renders the scene using Radiance" def execute(self, context): if pr is None: - self.report({'ERROR'}, "PyRadiance is not available. Cannot perform rendering.") - return {'CANCELLED'} - + self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.") + return {"CANCELLED"} + # Get the resolution from the user input resolution_x, resolution_y = self.getResolution(context) quality = context.scene.radiance_exporter_properties.radiance_quality.upper() detail = context.scene.radiance_exporter_properties.radiance_detail.upper() variability = context.scene.radiance_exporter_properties.radiance_variability.upper() - + # Get the blend file path and create the "Radiance Rendering" directory blend_file_path = bpy.data.filepath if not blend_file_path: - self.report({'ERROR'}, "Please save the Blender file before rendering.") - return {'CANCELLED'} + self.report({"ERROR"}, "Please save the Blender file before rendering.") + return {"CANCELLED"} blend_file_dir = os.path.dirname(blend_file_path) radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") - # Material processing style = [] obj_file_path = os.path.join(radiance_dir, "model.obj") @@ -153,19 +151,18 @@ class RadianceRender(bpy.types.Operator): # json_file_path = os.path.join(blend_file_dir, "material_mapping.json") json_file_path = context.scene.radiance_exporter_properties.json_file_path - self.report({'INFO'}, f"Selected JSON file: {json_file_path}") + self.report({"INFO"}, f"Selected JSON file: {json_file_path}") if json_file_path: # self.report({'INFO'}, f"Selected JSON file: {json_file_name}") json_dest_path = os.path.join(blend_file_dir, json_file_path.split("\\")[-1]) shutil.copy(json_file_path, json_dest_path) - self.report({'INFO'}, f"JSON file saved to: {json_dest_path}") + self.report({"INFO"}, f"JSON file saved to: {json_dest_path}") else: - self.report({'WARNING'}, "No JSON file selected") + self.report({"WARNING"}, "No JSON file selected") - - with open(json_file_path, 'r') as file: + with open(json_file_path, "r") as file: data = json.load(file) - + # Create materials.rad file materials_file = os.path.join(radiance_dir, "materials.rad") with open(materials_file, "w") as file: @@ -180,18 +177,18 @@ class RadianceRender(bpy.types.Operator): for i in set(style): file.write("inherit alias " + i + " " + data.get(i, "white") + "\n") - self.report({'INFO'}, "Exported Materials Rad file to: {}".format(materials_file)) + self.report({"INFO"}, "Exported Materials Rad file to: {}".format(materials_file)) # Run obj2mesh rtm_file_path = os.path.join(radiance_dir, "model.rtm") - mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) + mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) # subprocess.run(["obj2mesh", "-a", materials_file, obj_file_path, rtm_file_path]) - self.report({'INFO'}, "obj2mesh output: {}".format(mesh_file_path)) + self.report({"INFO"}, "obj2mesh output: {}".format(mesh_file_path)) scene_file = os.path.join(radiance_dir, "scene.rad") with open(scene_file, "w") as file: file.write("void mesh model\n1 " + rtm_file_path + "\n0\n0\n") - self.report({'INFO'}, "Exported Scene file to: {}".format(scene_file)) + self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file)) # Py Radiance Rendering code scene = pr.Scene("ascene") @@ -207,15 +204,21 @@ class RadianceRender(bpy.types.Operator): octpath = os.path.join(blend_file_dir, "ascene.oct") print("Reached here") - image = pr.render(scene, ambbounce=1, resolution=(resolution_x, resolution_y), - quality=quality, detail=detail, variability=variability) - + image = pr.render( + scene, + ambbounce=1, + resolution=(resolution_x, resolution_y), + quality=quality, + detail=detail, + variability=variability, + ) + raw_hdr_path = os.path.join(radiance_dir, "raw.hdr") with open(raw_hdr_path, "wb") as wtr: wtr.write(image) - self.report({'INFO'}, "Radiance rendering completed. Output: {}".format(raw_hdr_path)) - return {'FINISHED'} + self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(raw_hdr_path)) + return {"FINISHED"} def getResolution(self, context): scene = context.scene @@ -224,10 +227,11 @@ class RadianceRender(bpy.types.Operator): resolution_y = props.radiance_resolution_y return resolution_x, resolution_y + def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs): output_bytes = pr.obj2mesh(inp, **kwargs) - with open(output_file, 'wb') as f: + with open(output_file, "wb") as f: f.write(output_bytes) return output_file @@ -269,3 +273,21 @@ class ImportLatLong(bpy.types.Operator): props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude) props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude) return {"FINISHED"} + + +class VisualiseShadows(bpy.types.Operator): + bl_idname = "bim.visualise_shadows" + bl_label = "Visualise Shadows" + bl_description = "Enables a visual style to display shadows easily" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.render.engine = "BLENDER_WORKBENCH" + context.scene.display.shading.light = "FLAT" + context.scene.display.shading.show_shadows = True + context.scene.display.shadow_focus = 1.0 + context.scene.view_settings.view_transform = "Standard" # Preserve shading colours + space = tool.Blender.get_view3d_space() + space.shading.type = "RENDERED" + SolarDecorator.install(bpy.context) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/light/prop.py b/src/blenderbim/blenderbim/bim/module/light/prop.py index 64b582b583..f70dfa87fc 100644 --- a/src/blenderbim/blenderbim/bim/module/light/prop.py +++ b/src/blenderbim/blenderbim/bim/module/light/prop.py @@ -20,8 +20,10 @@ import bpy import pytz import tzfpy import datetime -from math import radians -from bpy.props import IntProperty, StringProperty, EnumProperty, FloatProperty +import sun_position.sun_calc +from math import radians, pi +from mathutils import Euler, Vector, Matrix +from bpy.props import IntProperty, StringProperty, EnumProperty, FloatProperty, FloatVectorProperty from bpy.types import PropertyGroup from blenderbim.bim.module.light.data import SolarData @@ -46,6 +48,9 @@ def update_hourminute(self, context): def update_date(self, context): + sun_props = context.scene.sun_pos_properties + sun_props.month = self.month + sun_props.day = self.day update_sun_path() @@ -53,16 +58,48 @@ def update_true_north(self, context): sun_props = context.scene.sun_pos_properties # Preserve IFC sign convention sun_props.north_offset = radians(self.true_north * -1) + update_sun_path() + + +def update_sun_path_size(self, context): + sun_props = context.scene.sun_pos_properties + sun_props.sun_distance = self.sun_path_size + update_sun_path() def update_sun_path(): props = bpy.context.scene.BIMSolarProperties sun_props = bpy.context.scene.sun_pos_properties timezone = pytz.timezone(tzfpy.get_tz(props.longitude, props.latitude)) - dt = datetime.datetime(datetime.datetime.now(datetime.UTC).year, int(props.month), props.date, props.hour, props.minute) + dt = datetime.datetime(sun_props.year, sun_props.month, sun_props.day, props.hour, props.minute) local_time = timezone.localize(dt, is_dst=None) sun_props.use_daylight_savings = bool(local_time.dst()) sun_props.UTC_zone = local_time.utcoffset().total_seconds() / 3600 + zone = -sun_props.UTC_zone + if sun_props.use_daylight_savings: + zone -= 1 + azimuth, elevation = sun_position.sun_calc.get_sun_coordinates( + sun_props.time, + sun_props.latitude, + sun_props.longitude, + zone, + sun_props.month, + sun_props.day, + sun_props.year, + ) + sun_vector = sun_position.sun_calc.get_sun_vector(azimuth, elevation) * sun_props.sun_distance + props.sun_position = sun_vector + # sun_vector.z = max(0, sun_vector.z) + # Light direction is a bit weird? + mat = Matrix(((-1.0, 0.0, 0.0, 0.0), (0.0, 0, 1.0, 0.0), (-0.0, -1.0, 0, 0.0), (0.0, 0.0, 0.0, 1.0))).inverted() + if sun_vector.z < 0: + bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1)) + else: + rotation_euler = Euler((elevation - pi / 2, 0, -azimuth)) + bpy.context.scene.display.light_direction = mat @ ( + rotation_euler.to_quaternion() @ Vector((0, 0, -1)) + ) + SolarData.data["sun"] = sun_vector class RadianceExporterProperties(PropertyGroup): @@ -118,29 +155,10 @@ class BIMSolarProperties(PropertyGroup): latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong) longitude: FloatProperty(name="Longitude", min=-180, max=180, update=update_latlong) true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north) - month: EnumProperty( - name="Month", - items=[ - (str(i + 1), m, "") - for i, m in enumerate( - ( - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ) - ) - ], - update=update_date - ) - date: IntProperty(name="Date", min=1, max=31, default=1, update=update_date) + month: IntProperty(name="Month", min=1, max=12, default=1, update=update_date) + day: IntProperty(name="Date", min=1, max=31, default=1, update=update_date) hour: IntProperty(name="Hour", min=0, max=23, update=update_hourminute) minute: IntProperty(name="Minute", min=0, max=59, update=update_hourminute) + sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0)) + sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(10, 0, 0)) + sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size) diff --git a/src/blenderbim/blenderbim/bim/module/light/ui.py b/src/blenderbim/blenderbim/bim/module/light/ui.py index 2c9bf0fcda..169ebfa3d7 100644 --- a/src/blenderbim/blenderbim/bim/module/light/ui.py +++ b/src/blenderbim/blenderbim/bim/module/light/ui.py @@ -105,8 +105,21 @@ class BIM_PT_solar(bpy.types.Panel): row.operator("bim.import_true_north", icon="IMPORT", text="") row = self.layout.row(align=True) - row.prop(props, "month", text="") - row.prop(props, "date") + row.prop(props, "month", text={ + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }[props.month]) + row.prop(props, "day") row = self.layout.row(align=True) row.prop(props, "hour") @@ -134,5 +147,10 @@ class BIM_PT_solar(bpy.types.Panel): row2.label(text=f"Sunset: {sunset}") row = self.layout.row(align=True) - row.prop(sun_props, "sun_distance", text="Sun Path Size") - row.prop(sun_props, "show_analemmas", icon="HIDE_ON" if sun_props.show_analemmas else "HIDE_OFF", text="") + row.prop(props, "sun_path_size") + + row = self.layout.row() + row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity") + + row = self.layout.row() + row.operator("bim.visualise_shadows", text="Visualise Shadows") diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 3a10a91437..8e0714d95e 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -246,6 +246,13 @@ class Blender(blenderbim.core.tool.Blender): if area.type == "VIEW_3D": return area + @classmethod + def get_view3d_space(cls): + if area := cls.get_view3d_area(): + for space in area.spaces: + if space.type == "VIEW_3D": + return space + @classmethod def get_blender_prop_default_value(cls, props, prop_name: str) -> Any: prop_bl_rna = props.bl_rna.properties[prop_name]