Light/radiance (#5252)

* Cam fix, Sun Fix, import-export as json

* Ran black formatting
This commit is contained in:
Chirag Singh
2024-08-30 04:02:45 +05:30
committed by GitHub
parent afd5a5b23e
commit 9633d92c17
6 changed files with 319 additions and 108 deletions
@@ -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,
@@ -1,3 +1,22 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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}")
+197 -55
View File
@@ -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"}
+46 -20
View File
@@ -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,
@@ -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",
+41 -33
View File
@@ -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