diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index ea4fb7e05c..04bd22a80c 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -39,6 +39,10 @@ classes = ( operator.ViewFromSun, operator.RefreshIFCMaterials, operator.UnmapMaterial, + operator.RADIANCE_OT_select_camera, + operator.RADIANCE_OT_export_material_mappings, + operator.RADIANCE_OT_import_material_mappings, + operator.RADIANCE_OT_open_spectraldb, prop.RadianceMaterial, prop.BIMSolarProperties, prop.RadianceExporterProperties, diff --git a/src/bonsai/bonsai/bim/module/light/list.py b/src/bonsai/bonsai/bim/module/light/list.py index c485cc4259..0536284e7a 100644 --- a/src/bonsai/bonsai/bim/module/light/list.py +++ b/src/bonsai/bonsai/bim/module/light/list.py @@ -1,3 +1,22 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai 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. +# +# Bonsai 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 Bonsai. If not, see . + + import bpy import json import os @@ -10,6 +29,15 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): if self.layout_type in {"DEFAULT", "COMPACT"}: row = layout.row(align=True) + + # Adding color preview or glass icon + if item.category == "Glass": + row.label(icon="SHADING_TEXTURE") + else: + color_rect = row.row() + color_rect.prop(item, "color", text="") + color_rect.scale_x = 0.3 + row.prop(item, "name", text="", emboss=False, icon_value=icon) if item.is_mapped: row.label(text=f"{item.category} - {item.subcategory}") diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 093cafedf8..de98b6d8b3 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -30,11 +30,17 @@ import bonsai.tool as tool from pathlib import Path from typing import Union, Optional, Sequence import json +import math +import time +import sun_position import ifcopenshell +import webbrowser import ifcopenshell.geom import multiprocessing from mathutils import Vector from bonsai.bim.module.light.data import SolarData +from bpy_extras.io_utils import ExportHelper +from bpy_extras.io_utils import ImportHelper ifc_materials = [] @@ -95,14 +101,10 @@ class ExportOBJ(bpy.types.Operator): while True: shape = iterator.get() materials = shape.geometry.materials - # print(shape.geometry) material_ids = shape.geometry.material_ids # material_names = shape.geometry.material_names - # print(material_ids) for material in materials: - # print(material, dir(material)) - # print(material.name) ifc_materials.append(material.name) serialiser.write(shape) @@ -129,10 +131,15 @@ class RadianceRender(bpy.types.Operator): self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.") return {"CANCELLED"} - # Get the resolution from the user input - + print("Starting Radiance rendering process...") props = context.scene.radiance_exporter_properties resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y + + context.scene.render.resolution_x = resolution_x + context.scene.render.resolution_y = resolution_y + + aspect_ratio = resolution_x / resolution_y + quality = props.radiance_quality.upper() detail = props.radiance_detail.upper() variability = props.radiance_variability.upper() @@ -141,6 +148,11 @@ class RadianceRender(bpy.types.Operator): output_file_format = props.output_file_format use_hdr = props.use_hdr choose_hdr_image = props.choose_hdr_image + + print(f"Resolution: {resolution_x}x{resolution_y}") + print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}") + print(f"Output directory: {output_dir}") + if use_hdr: hdr_image = "noon_grass_2k.hdr" hdr_mask = "noon_grass_2k_mask.hdr" @@ -148,30 +160,34 @@ class RadianceRender(bpy.types.Operator): hdr_image_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_image) hdr_mask_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_mask) sky_map_cal_path = os.path.join(os.path.dirname(__file__), "HDRs", sky_map_cal) - # os.chdir(output_dir) obj_file_path = os.path.join(output_dir, "model.obj") sun_props = context.scene.BIMSolarProperties + sun_pos_props = context.scene.sun_pos_properties sky_file_path = os.path.join(output_dir, "sky.rad") - latitude = sun_props.latitude - longitude = sun_props.longitude - timezone = sun_props.timezone - month = sun_props.month - day = sun_props.day - hour = sun_props.hour - minute = sun_props.minute + # latitude = sun_props.latitude + # longitude = sun_props.longitude + # month = sun_props.month + # day = sun_props.day + # hour = sun_props.hour + # minute = sun_props.minute - print("Sun Properties:") - print("Latitude: ", latitude) - print("Longitude: ", longitude) - print("Timezone: ", timezone) - print("Month: ", month) - print("Day: ", day) - print("Hour: ", hour) - print("Minute: ", minute) + # print("Sun Properties:") + # print("Latitude: ", latitude) + # print("Longitude: ", longitude) + # print("Timezone: ", timezone) + # print("Month: ", month) + # print("Day: ", day) + # print("Hour: ", hour) + # print("Minute: ", minute) + + print("Setting up camera...") + if props.use_active_camera: + camera = context.scene.camera + else: + camera = props.selected_camera - camera = self.get_active_camera(context) if camera is None: self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.") return {"CANCELLED"} @@ -179,19 +195,34 @@ class RadianceRender(bpy.types.Operator): # Get camera position and direction camera_position, camera_direction = self.get_camera_data(camera) - dt = datetime(2024, month, day, hour, minute) + print(f"Camera position: {camera_position}") + print(f"Camera direction: {camera_direction}") + + # azimuth, elevation = sun_position.sun_calc.get_sun_coordinates( + # sun_pos_props.time, + # sun_pos_props.latitude, + # sun_pos_props.longitude, + # -sun_pos_props.UTC_zone, + # sun_pos_props.month, + # sun_pos_props.day, + # sun_pos_props.year, + # ) + + dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute) sky_description = pr.gensky( dt=dt, - latitude=latitude, - longitude=longitude, - timezone=timezone, - year=2024, - sunny_with_sun=True, - sunny_without_sun=False, - cloudy=False, - ground_reflectance=0.2, - turbidity=3.0, + # azimuth=64.1, + # altitude=-26.6, + latitude=sun_props.latitude, + longitude=sun_props.longitude, + year=sun_pos_props.year, + timezone=-int(sun_props.UTC_zone), + # sunny_with_sun=False, + # sunny_without_sun=False, + # cloudy=False, + # ground_reflectance=0.2, + # turbidity=3.0, ) sky_description_str = sky_description.decode("utf-8") @@ -292,11 +323,7 @@ ground_glow source ground props = context.scene.radiance_exporter_properties - if props.use_json_file: - with open(props.json_file, "r") as file: - data = json.load(file) - else: - data = props.get_mappings_dict() + data = props.get_mappings_dict() materials_file = os.path.join(output_dir, "materials.rad") written_materials = set() @@ -318,9 +345,6 @@ ground_glow source ground file.write(material) written_materials.add(material.split()[2]) # Add material name to written set - print(data) - print(default_materials) - for style_id in all_materials: material = next((m for m in props.materials if m.style_id == style_id), None) if material and material.is_mapped: @@ -351,7 +375,7 @@ ground_glow source ground self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file)) - # Py Radiance Rendering code + print("Setting up Radiance scene...") scene = pr.Scene("ascene") material_path = os.path.join(output_dir, "materials.rad") @@ -360,10 +384,39 @@ ground_glow source ground scene.add_material(material_path) scene.add_surface(scene_path) scene.add_source(sky_file_path) + print("Setting up view...") + if camera.data.type == "PERSP": + # Perspective camera + camera_fov = camera.data.angle + # Calculate vertical FOV based on the desired aspect ratio + vertical_fov = 2 * math.atan(math.tan(camera_fov / 2) / aspect_ratio) - aview = pr.View(position=camera_position, direction=camera_direction) + aview = pr.View( + vtype="v", # Perspective view + position=camera_position, + direction=camera_direction, + vup=(0, 0, 1), # Assuming Z is up + horiz=math.degrees(camera_fov), + vert=math.degrees(vertical_fov), + ) + else: # 'ORTHO' + # Orthographic camera + # Calculate the view size based on the camera's orthographic scale + ortho_scale = camera.data.ortho_scale + view_width = ortho_scale + view_height = ortho_scale / aspect_ratio + + aview = pr.View( + vtype="l", # Parallel projection (orthographic) + position=camera_position, + direction=camera_direction, + vup=(0, 0, 1), # Assuming Z is up + horiz=view_width, + vert=view_height, + ) scene.add_view(aview) - + print("Starting render...") + start_time = time.time() image = pr.render( scene, ambbounce=1, @@ -371,32 +424,34 @@ ground_glow source ground quality=quality, detail=detail, variability=variability, + nproc=multiprocessing.cpu_count(), ) + end_time = time.time() + print(f"Render completed in {end_time - start_time:.2f} seconds") output_hdr_path = os.path.join(output_dir, f"{output_file_name}.{output_file_format.lower()}") - + print(f"Saving HDR output to: {output_hdr_path}") if output_file_format == "HDR": with open(output_hdr_path, "wb") as wtr: wtr.write(image) else: pass - + print("Applying tone mapping...") pcond_image = pr.pcond(hdr=output_hdr_path, human=True) tiff_path = os.path.join(output_dir, f"{output_file_name}.tiff") - + print(f"Saving TIFF output to: {tiff_path}") pr.ra_tiff(inp=pcond_image, out=tiff_path, lzw=True) - + print("Radiance rendering process completed successfully.") self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(tiff_path)) return {"FINISHED"} def get_active_camera(self, context): - if context.scene.camera: + props = context.scene.radiance_exporter_properties + if props.use_active_camera: return context.scene.camera - for obj in context.scene.objects: - if obj.type == "CAMERA": - return obj - return None + else: + return props.selected_camera def get_camera_data(self, camera): # Get camera position @@ -509,9 +564,29 @@ class RefreshIFCMaterials(bpy.types.Operator): for render_item in style.Styles: if render_item.is_a("IfcSurfaceStyleRendering"): style_id = f"IfcSurfaceStyleRendering-{render_item.id()}" - ifc_materials.append(style_id) style_name = style.Name or f"Unnamed Style {render_item.id()}" - props.add_material_mapping(style_id, style_name) + + # Extract color and transparency + color = (1.0, 1.0, 1.0) # Default white + transparency = 0.0 # Default opaque + if render_item.SurfaceColour: + color = ( + render_item.SurfaceColour.Red, + render_item.SurfaceColour.Green, + render_item.SurfaceColour.Blue, + ) + if hasattr(render_item, "Transparency") and render_item.Transparency is not None: + transparency = render_item.Transparency + + # Add material with color + material = props.add_material_mapping(style_id, style_name) + material.color = color + + # If transparency is high, consider it as glass + if transparency > 0.5: + material.category = "Glass" + material.subcategory = "Clear Glass" + material.is_mapped = True props.active_material_index = 0 if props.materials else -1 @@ -531,3 +606,70 @@ class UnmapMaterial(bpy.types.Operator): material = props.materials[self.material_index] props.unmap_material(material.name) return {"FINISHED"} + + +class RADIANCE_OT_select_camera(bpy.types.Operator): + bl_idname = "radiance.select_camera" + bl_label = "Select Camera" + bl_description = "Select a camera from the viewport" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object is not None and context.object.type == "CAMERA" + + def execute(self, context): + props = context.scene.radiance_exporter_properties + props.selected_camera = context.object + props.use_active_camera = False + return {"FINISHED"} + + +class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper): + bl_idname = "radiance.export_material_mappings" + bl_label = "Export Material Mappings" + bl_description = "Export material mappings to a JSON file" + + filename_ext = ".json" + + def execute(self, context): + props = context.scene.radiance_exporter_properties + mappings = {} + + for material in props.materials: + if material.is_mapped: + mappings[material.style_id] = { + "name": material.name, + "category": material.category, + "subcategory": material.subcategory, + } + + with open(self.filepath, "w") as f: + json.dump(mappings, f, indent=4) + + self.report({"INFO"}, f"Material mappings exported to {self.filepath}") + return {"FINISHED"} + + +class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper): + bl_idname = "radiance.import_material_mappings" + bl_label = "Import Material Mappings" + bl_description = "Import material mappings from a JSON file" + + filename_ext = ".json" + + def execute(self, context): + props = context.scene.radiance_exporter_properties + props.import_mappings(self.filepath) + self.report({"INFO"}, f"Material mappings imported from {self.filepath}") + return {"FINISHED"} + + +class RADIANCE_OT_open_spectraldb(bpy.types.Operator): + bl_idname = "radiance.open_spectraldb" + bl_label = "Open SpectralDB" + bl_description = "Open the SpectralDB website for reference" + + def execute(self, context): + webbrowser.open("https://spectraldb.com") + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index 519cf3583a..23dcf9228b 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -32,6 +32,7 @@ from bpy.props import ( FloatVectorProperty, BoolProperty, CollectionProperty, + PointerProperty, ) import os from bpy.types import PropertyGroup @@ -107,6 +108,11 @@ def update_display_sun_path(self, context): SolarDecorator.uninstall() +def update_resolution(self, context): + context.scene.render.resolution_x = self.radiance_resolution_x + context.scene.render.resolution_y = self.radiance_resolution_y + + def update_sun_path(): if not SolarData.is_loaded: SolarData.load() @@ -142,6 +148,10 @@ def update_sun_path(): rotation_euler = Euler((elevation - pi / 2, 0, -azimuth)) rotation_quaternion = rotation_euler.to_quaternion() + props.azimuth = azimuth + props.elevation = elevation + props.UTC_zone = zone + if sun_vector.z < 0: bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1)) else: @@ -162,14 +172,11 @@ class RadianceMaterial(PropertyGroup): category: StringProperty(name="Category") subcategory: StringProperty(name="Subcategory") is_mapped: BoolProperty(name="Is Mapped", default=False) + color: FloatVectorProperty(name="Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, size=3) class RadianceExporterProperties(PropertyGroup): - def update_json_file(self, context): - if self.json_file: - self.json_file = bpy.path.abspath(self.json_file) - def update_output_dir(self, context): if self.output_dir: self.output_dir = bpy.path.abspath(self.output_dir) @@ -184,8 +191,26 @@ class RadianceExporterProperties(PropertyGroup): item.style_id = style_id item.category = "" item.subcategory = "" + item.color = (1.0, 1.0, 1.0) # Default white return item + def import_mappings(self, filepath): + with open(filepath, "r") as f: + mappings = json.load(f) + + for style_id, mapping in mappings.items(): + material = self.get_material_mapping(mapping["name"]) + if material: + material.style_id = style_id + material.category = mapping["category"] + material.subcategory = mapping["subcategory"] + material.is_mapped = True + else: + new_material = self.add_material_mapping(style_id, mapping["name"]) + new_material.category = mapping["category"] + new_material.subcategory = mapping["subcategory"] + new_material.is_mapped = True + def get_material_mapping(self, style_name): return next((item for item in self.materials if item.name == style_name), None) @@ -213,12 +238,6 @@ class RadianceExporterProperties(PropertyGroup): item.subcategory = "" item.is_mapped = False - use_json_file: BoolProperty( - name="Upload JSON", - description="Toggle between uploading a JSON file and using in-UI material mapping", - default=False, - ) - is_exporting: bpy.props.BoolProperty( name="Is Exporting", description="Whether the OBJ export is in progress", default=False ) @@ -237,6 +256,7 @@ class RadianceExporterProperties(PropertyGroup): ("Plant", "Plant", ""), ("Exterior", "Exterior", ""), ("Color Swatch", "Color Swatch", ""), + ("Glass", "Glass", ""), ] def update_material_mapping(self, context): @@ -269,14 +289,13 @@ class RadianceExporterProperties(PropertyGroup): should_load_from_memory: BoolProperty( name="Load from Memory", default=False, - description="Use IFC file currently loaded in Bonsai", ) radiance_resolution_x: IntProperty( - name="X", description="Horizontal resolution of the output image", default=1920, min=1 + name="X", description="Horizontal resolution of the output image", default=1920, min=1, update=update_resolution ) radiance_resolution_y: IntProperty( - name="Y", description="Vertical resolution of the output image", default=1080, min=1 + name="Y", description="Vertical resolution of the output image", default=1080, min=1, update=update_resolution ) output_dir: StringProperty( name="Output Directory", @@ -292,13 +311,6 @@ class RadianceExporterProperties(PropertyGroup): subtype="FILE_PATH", update=lambda self, context: self.update_ifc_file(context), ) - json_file: StringProperty( - name="JSON File", - description="Path to the JSON file", - default="", - subtype="FILE_PATH", - update=lambda self, context: self.update_json_file(context), - ) radiance_quality: EnumProperty( name="Quality", @@ -350,6 +362,17 @@ class RadianceExporterProperties(PropertyGroup): default="Noon", ) + use_active_camera: BoolProperty( + name="Use Active Camera", description="Use the active camera in the scene", default=True + ) + + selected_camera: PointerProperty( + type=bpy.types.Object, + name="Camera", + description="Select a camera to use for rendering", + poll=lambda self, object: object.type == "CAMERA", + ) + class BIMSolarProperties(PropertyGroup): sites: EnumProperty(items=get_sites, name="Sites") @@ -364,6 +387,9 @@ class BIMSolarProperties(PropertyGroup): sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0)) sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0)) sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size) + azimuth: FloatProperty(name="Azimuth") + elevation: FloatProperty(name="Elevation") + UTC_zone: FloatProperty(name="UTC Zone") display_shadows: BoolProperty( name="Display Shadows", default=False, diff --git a/src/bonsai/bonsai/bim/module/light/spectraldb.json b/src/bonsai/bonsai/bim/module/light/spectraldb.json index 812cf88f7c..82ea07d0df 100644 --- a/src/bonsai/bonsai/bim/module/light/spectraldb.json +++ b/src/bonsai/bonsai/bim/module/light/spectraldb.json @@ -1,4 +1,7 @@ { + "Glass": { + "Clear Glass": "void BRTDfunc clear_glass\n10\n sr_clear_r sr_clear_g sr_clear_b\n st_clear_r st_clear_g st_clear_b\n 0 0 0\n glaze1.cal\n0\n19\n 0 0 0\n 0 0 0\n 0 0 0\n 1\n 0.074 0.077 0.079\n 0.074 0.077 0.079\n 0.862 0.890 0.886\n" + }, "Wall": { "White Painted Room Walls": "void plastic white_painted_room_walls \n0\n0\n5 0.8316 0.8116 0.7226 0.0036 0.2", "White Painted Corridor Walls": "void plastic white_painted_corridor_walls \n0\n0\n5 0.8143 0.7984 0.715 0.0039 0.2", diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index b13410c3d7..3a456badce 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -50,43 +50,52 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row.prop(props, "output_dir") row = layout.row() - layout.prop(props, "use_json_file") + layout.label(text="Info: Unmapped materials default to white") + row = layout.row() + row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index") + row.operator("radiance.open_spectraldb", text="", icon="WORLD") # Globe icon + if len(props.materials) > 0: + col = layout.column(align=True) + col.prop(props, "category") + if props.category: + col.prop(props, "subcategory") - if props.use_json_file: - row = layout.row() - row.prop(props, "json_file") + if props.active_material_index >= 0 and props.active_material_index < len(props.materials): + active_material = props.materials[props.active_material_index] + if active_material.category and active_material.subcategory: + layout.label( + text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}" + ) + else: + layout.label(text=f"Select category and subcategory for: {active_material.name}") - if not props.use_json_file: - row = layout.row() - layout.label(text="Info: Unmapped materials default to white") - row = layout.row() - row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index") - - if len(props.materials) > 0: - col = layout.column(align=True) - col.prop(props, "category") - if props.category: - col.prop(props, "subcategory") - - if props.active_material_index >= 0 and props.active_material_index < len(props.materials): - active_material = props.materials[props.active_material_index] - if active_material.category and active_material.subcategory: - layout.label( - text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}" - ) - else: - layout.label(text=f"Select category and subcategory for: {active_material.name}") - - row = layout.row() - row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials") + row = layout.row() + row.operator("radiance.import_material_mappings", text="Import Mappings", icon="IMPORT") + row.operator("radiance.export_material_mappings", text="Export Mappings", icon="EXPORT") + row = layout.row() + row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials") layout.separator() - row = layout.row() - row.label(text="Resolution") - row.prop(props, "radiance_resolution_x", text="X") row = layout.row() - row.label(text="") + layout.label(text="Step 1: Export geometry for simulation") + row = layout.row() + row.operator("export_scene.radiance", text="Export Geometry for Simulation") + + layout.separator() + + box = layout.box() + box.label(text="Camera Settings") + row = box.row() + row.prop(props, "use_active_camera") + if not props.use_active_camera: + row = box.row() + row.prop(props, "selected_camera") + row.operator("radiance.select_camera", text="", icon="EYEDROPPER") + + row = box.row(align=True) + row.label(text="Resolution") + row.prop(props, "radiance_resolution_x", text="X") row.prop(props, "radiance_resolution_y", text="Y") row = layout.row() @@ -115,8 +124,7 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row.prop(props, "choose_hdr_image") row = layout.row() - row.operator("export_scene.radiance", text="Export Geometry for Simulation") - + layout.label(text="Step 2: Run the simulation") row = layout.row() row.operator("render_scene.radiance", text="Radiance Render") row.enabled = not props.is_exporting