Add sun path visualisation for solar analysis

This commit is contained in:
Dion Moult
2024-07-17 13:58:54 +10:00
parent 7a10edbd73
commit a2018a22a6
5 changed files with 136 additions and 70 deletions
@@ -25,6 +25,7 @@ classes = (
operator.ImportLatLong, operator.ImportLatLong,
operator.ImportTrueNorth, operator.ImportTrueNorth,
operator.RadianceRender, operator.RadianceRender,
operator.VisualiseShadows,
prop.BIMSolarProperties, prop.BIMSolarProperties,
prop.RadianceExporterProperties, prop.RadianceExporterProperties,
ui.BIM_PT_radiance_exporter, ui.BIM_PT_radiance_exporter,
@@ -28,11 +28,12 @@ import blenderbim.tool as tool
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
from blenderbim.bim.module.light.data import SolarData from blenderbim.bim.module.light.data import SolarData
from blenderbim.bim.module.light.decorator import SolarDecorator
class ExportOBJ(bpy.types.Operator): class ExportOBJ(bpy.types.Operator):
"""Exports the IFC File to OBJ""" """Exports the IFC File to OBJ"""
bl_idname = "export_scene.radiance" bl_idname = "export_scene.radiance"
bl_label = "Export" bl_label = "Export"
bl_description = "Export the IFC to OBJ" bl_description = "Export the IFC to OBJ"
@@ -48,16 +49,16 @@ class ExportOBJ(bpy.types.Operator):
aspect_ratio = resolution_x / resolution_y aspect_ratio = resolution_x / resolution_y
# Get the blend file path and create the "Radiance Rendering" directory # 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 blend_file_path = bpy.data.filepath
if not blend_file_path: if not blend_file_path:
self.report({'ERROR'}, "Please save the Blender file before exporting.") self.report({"ERROR"}, "Please save the Blender file before exporting.")
return {'CANCELLED'} return {"CANCELLED"}
blend_file_dir = os.path.dirname(blend_file_path) blend_file_dir = os.path.dirname(blend_file_path)
radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") 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): if not os.path.exists(radiance_dir):
os.makedirs(radiance_dir) os.makedirs(radiance_dir)
@@ -68,7 +69,6 @@ class ExportOBJ(bpy.types.Operator):
serializer_settings = ifcopenshell.geom.serializer_settings() serializer_settings = ifcopenshell.geom.serializer_settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS) settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
settings.set("apply-default-materials", True) settings.set("apply-default-materials", True)
serializer_settings.set("use-element-guids", True) serializer_settings.set("use-element-guids", True)
@@ -78,18 +78,16 @@ class ExportOBJ(bpy.types.Operator):
ifc_file_path = os.path.join(blend_file_dir, f"{ifc_file_name}.ifc") ifc_file_path = os.path.join(blend_file_dir, f"{ifc_file_name}.ifc")
if not os.path.exists(ifc_file_path): if not os.path.exists(ifc_file_path):
self.report({'ERROR'}, f"IFC file not found: {ifc_file_path}") self.report({"ERROR"}, f"IFC file not found: {ifc_file_path}")
return {'CANCELLED'} return {"CANCELLED"}
ifc_file = ifcopenshell.open(ifc_file_path) ifc_file = ifcopenshell.open(ifc_file_path)
for material in ifc_file.by_type("IfcMaterial"): 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") obj_file_path = os.path.join(radiance_dir, "model.obj")
mtl_file_path = os.path.join(radiance_dir, "model.mtl") 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, ifcopenshell.geom.serializer_settings())
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings) serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
serialiser.setFile(ifc_file) serialiser.setFile(ifc_file)
@@ -104,7 +102,7 @@ class ExportOBJ(bpy.types.Operator):
break break
serialiser.finalize() serialiser.finalize()
return {'FINISHED'} return {"FINISHED"}
def getResolution(self, context): def getResolution(self, context):
scene = context.scene scene = context.scene
@@ -116,14 +114,15 @@ class ExportOBJ(bpy.types.Operator):
class RadianceRender(bpy.types.Operator): class RadianceRender(bpy.types.Operator):
"""Radiance Rendering""" """Radiance Rendering"""
bl_idname = "render_scene.radiance" bl_idname = "render_scene.radiance"
bl_label = "Render" bl_label = "Render"
bl_description = "Renders the scene using Radiance" bl_description = "Renders the scene using Radiance"
def execute(self, context): def execute(self, context):
if pr is None: if pr is None:
self.report({'ERROR'}, "PyRadiance is not available. Cannot perform rendering.") self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.")
return {'CANCELLED'} return {"CANCELLED"}
# Get the resolution from the user input # Get the resolution from the user input
resolution_x, resolution_y = self.getResolution(context) resolution_x, resolution_y = self.getResolution(context)
@@ -134,13 +133,12 @@ class RadianceRender(bpy.types.Operator):
# Get the blend file path and create the "Radiance Rendering" directory # Get the blend file path and create the "Radiance Rendering" directory
blend_file_path = bpy.data.filepath blend_file_path = bpy.data.filepath
if not blend_file_path: if not blend_file_path:
self.report({'ERROR'}, "Please save the Blender file before rendering.") self.report({"ERROR"}, "Please save the Blender file before rendering.")
return {'CANCELLED'} return {"CANCELLED"}
blend_file_dir = os.path.dirname(blend_file_path) blend_file_dir = os.path.dirname(blend_file_path)
radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") radiance_dir = os.path.join(blend_file_dir, "RadianceRendering")
# Material processing # Material processing
style = [] style = []
obj_file_path = os.path.join(radiance_dir, "model.obj") obj_file_path = os.path.join(radiance_dir, "model.obj")
@@ -153,17 +151,16 @@ class RadianceRender(bpy.types.Operator):
# json_file_path = os.path.join(blend_file_dir, "material_mapping.json") # json_file_path = os.path.join(blend_file_dir, "material_mapping.json")
json_file_path = context.scene.radiance_exporter_properties.json_file_path 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: if json_file_path:
# self.report({'INFO'}, f"Selected JSON file: {json_file_name}") # self.report({'INFO'}, f"Selected JSON file: {json_file_name}")
json_dest_path = os.path.join(blend_file_dir, json_file_path.split("\\")[-1]) json_dest_path = os.path.join(blend_file_dir, json_file_path.split("\\")[-1])
shutil.copy(json_file_path, json_dest_path) 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: 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) data = json.load(file)
# Create materials.rad file # Create materials.rad file
@@ -180,18 +177,18 @@ class RadianceRender(bpy.types.Operator):
for i in set(style): for i in set(style):
file.write("inherit alias " + i + " " + data.get(i, "white") + "\n") 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 # Run obj2mesh
rtm_file_path = os.path.join(radiance_dir, "model.rtm") 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]) # 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") scene_file = os.path.join(radiance_dir, "scene.rad")
with open(scene_file, "w") as file: with open(scene_file, "w") as file:
file.write("void mesh model\n1 " + rtm_file_path + "\n0\n0\n") 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 # Py Radiance Rendering code
scene = pr.Scene("ascene") scene = pr.Scene("ascene")
@@ -207,15 +204,21 @@ class RadianceRender(bpy.types.Operator):
octpath = os.path.join(blend_file_dir, "ascene.oct") octpath = os.path.join(blend_file_dir, "ascene.oct")
print("Reached here") print("Reached here")
image = pr.render(scene, ambbounce=1, resolution=(resolution_x, resolution_y), image = pr.render(
quality=quality, detail=detail, variability=variability) scene,
ambbounce=1,
resolution=(resolution_x, resolution_y),
quality=quality,
detail=detail,
variability=variability,
)
raw_hdr_path = os.path.join(radiance_dir, "raw.hdr") raw_hdr_path = os.path.join(radiance_dir, "raw.hdr")
with open(raw_hdr_path, "wb") as wtr: with open(raw_hdr_path, "wb") as wtr:
wtr.write(image) wtr.write(image)
self.report({'INFO'}, "Radiance rendering completed. Output: {}".format(raw_hdr_path)) self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(raw_hdr_path))
return {'FINISHED'} return {"FINISHED"}
def getResolution(self, context): def getResolution(self, context):
scene = context.scene scene = context.scene
@@ -224,10 +227,11 @@ class RadianceRender(bpy.types.Operator):
resolution_y = props.radiance_resolution_y resolution_y = props.radiance_resolution_y
return resolution_x, resolution_y return resolution_x, resolution_y
def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs): def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs):
output_bytes = pr.obj2mesh(inp, **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) f.write(output_bytes)
return output_file return output_file
@@ -269,3 +273,21 @@ class ImportLatLong(bpy.types.Operator):
props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude) props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude)
props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude) props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude)
return {"FINISHED"} 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"}
@@ -20,8 +20,10 @@ import bpy
import pytz import pytz
import tzfpy import tzfpy
import datetime import datetime
from math import radians import sun_position.sun_calc
from bpy.props import IntProperty, StringProperty, EnumProperty, FloatProperty 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 bpy.types import PropertyGroup
from blenderbim.bim.module.light.data import SolarData from blenderbim.bim.module.light.data import SolarData
@@ -46,6 +48,9 @@ def update_hourminute(self, context):
def update_date(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() update_sun_path()
@@ -53,16 +58,48 @@ def update_true_north(self, context):
sun_props = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
# Preserve IFC sign convention # Preserve IFC sign convention
sun_props.north_offset = radians(self.true_north * -1) 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(): def update_sun_path():
props = bpy.context.scene.BIMSolarProperties props = bpy.context.scene.BIMSolarProperties
sun_props = bpy.context.scene.sun_pos_properties sun_props = bpy.context.scene.sun_pos_properties
timezone = pytz.timezone(tzfpy.get_tz(props.longitude, props.latitude)) 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) local_time = timezone.localize(dt, is_dst=None)
sun_props.use_daylight_savings = bool(local_time.dst()) sun_props.use_daylight_savings = bool(local_time.dst())
sun_props.UTC_zone = local_time.utcoffset().total_seconds() / 3600 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): class RadianceExporterProperties(PropertyGroup):
@@ -118,29 +155,10 @@ class BIMSolarProperties(PropertyGroup):
latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong) latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong)
longitude: FloatProperty(name="Longitude", min=-180, max=180, 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) true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north)
month: EnumProperty( month: IntProperty(name="Month", min=1, max=12, default=1, update=update_date)
name="Month", day: IntProperty(name="Date", min=1, max=31, default=1, update=update_date)
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)
hour: IntProperty(name="Hour", min=0, max=23, update=update_hourminute) hour: IntProperty(name="Hour", min=0, max=23, update=update_hourminute)
minute: IntProperty(name="Minute", min=0, max=59, 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)
@@ -105,8 +105,21 @@ class BIM_PT_solar(bpy.types.Panel):
row.operator("bim.import_true_north", icon="IMPORT", text="") row.operator("bim.import_true_north", icon="IMPORT", text="")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(props, "month", text="") row.prop(props, "month", text={
row.prop(props, "date") 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 = self.layout.row(align=True)
row.prop(props, "hour") row.prop(props, "hour")
@@ -134,5 +147,10 @@ class BIM_PT_solar(bpy.types.Panel):
row2.label(text=f"Sunset: {sunset}") row2.label(text=f"Sunset: {sunset}")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(sun_props, "sun_distance", text="Sun Path Size") row.prop(props, "sun_path_size")
row.prop(sun_props, "show_analemmas", icon="HIDE_ON" if sun_props.show_analemmas else "HIDE_OFF", text="")
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")
@@ -246,6 +246,13 @@ class Blender(blenderbim.core.tool.Blender):
if area.type == "VIEW_3D": if area.type == "VIEW_3D":
return area 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 @classmethod
def get_blender_prop_default_value(cls, props, prop_name: str) -> Any: def get_blender_prop_default_value(cls, props, prop_name: str) -> Any:
prop_bl_rna = props.bl_rna.properties[prop_name] prop_bl_rna = props.bl_rna.properties[prop_name]