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.ImportTrueNorth,
operator.RadianceRender,
operator.VisualiseShadows,
prop.BIMSolarProperties,
prop.RadianceExporterProperties,
ui.BIM_PT_radiance_exporter,
@@ -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"}
@@ -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)
@@ -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")
@@ -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]