Light(Radiance): Implemented IES File Mapping | Render False Color Images | Updated UI

This commit is contained in:
Chirag Singh
2025-11-15 21:40:13 +05:30
parent f6e23c0462
commit a570d81fd7
5 changed files with 817 additions and 65 deletions
@@ -38,6 +38,8 @@ classes = (
operator.ImportTrueNorth,
operator.MoveSunPathTo3DCursor,
operator.RadianceRender,
operator.FalseColorRadiance,
operator.ConvertHDRToFootCandles,
operator.ViewFromSun,
operator.LightPickCoordinates,
operator.LightSetTimeToNow,
@@ -50,12 +52,17 @@ classes = (
operator.EnumPropertySearch,
operator.PrepareRadianceScene,
operator.SetEnumProperty,
operator.AddIESLight,
operator.RemoveIESLight,
operator.SetIESLightObject,
prop.RadianceMaterial,
prop.IESLight,
prop.BIMSolarProperties,
prop.RadianceExporterProperties,
ui.BIM_PT_radiance_exporter,
ui.BIM_PT_solar,
list.MATERIAL_UL_radiance_materials,
list.MATERIAL_UL_ies_lights,
)
+41 -1
View File
@@ -21,9 +21,10 @@ import bpy
import json
import os
from typing import TYPE_CHECKING
from pathlib import Path
if TYPE_CHECKING:
from bonsai.bim.module.light.prop import RadianceExporterProperties, RadianceMaterial
from bonsai.bim.module.light.prop import RadianceExporterProperties, RadianceMaterial, IESLight
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
spectraldb = json.load(f)
@@ -59,3 +60,42 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList):
op.material_index = index
else:
row.label(text="Not Mapped (White)")
class MATERIAL_UL_ies_lights(bpy.types.UIList):
"""UIList for displaying IES light fixtures."""
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: RadianceExporterProperties,
item: IESLight,
icon,
active_data,
active_propname,
index,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
# Enable/disable checkbox (visible toggle)
row.prop(item, "is_enabled", text="", emboss=True)
# IES file name
if item.ies_file_path:
filename = Path(item.ies_file_path).name
row.label(text=filename, icon="FILE")
else:
row.label(text="(No file selected)", icon="ERROR")
# Target object property (displays object name)
row.prop(item, "target_object", text="", emboss=False)
# Object eyedropper picker button (like in modifiers)
op = row.operator("radiance.set_ies_light_object", text="", icon="EYEDROPPER")
op.index = index
# Remove button (X icon - negative action)
op = row.operator("radiance.remove_ies_light", text="", icon="X")
op.index = index
+515 -60
View File
@@ -141,7 +141,32 @@ class PrepareRadianceScene(bpy.types.Operator):
direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
direction.normalize()
return (position.x, position.y, position.z), (direction.x, direction.y, direction.z)
# Get camera up vector
up = camera.matrix_world.to_quaternion() @ Vector((0, 1, 0))
up.normalize()
# Ensure up vector is perpendicular to view direction
# If they're parallel, use a different reference vector
if abs(direction.dot(up)) > 0.99: # Nearly parallel
# Use world Y axis as reference, or X axis if that's also parallel
if abs(direction.dot(Vector((0, 1, 0)))) > 0.99:
reference = Vector((1, 0, 0))
else:
reference = Vector((0, 1, 0))
# Compute perpendicular up vector using cross product
right = direction.cross(reference)
if right.length < 0.01: # Still parallel, try another reference
reference = Vector((0, 0, 1))
right = direction.cross(reference)
right.normalize()
up = right.cross(direction)
up.normalize()
return (
(position.x, position.y, position.z),
(direction.x, direction.y, direction.z),
(up.x, up.y, up.z),
)
def execute(self, context):
print("Starting Radiance scene preparation...")
@@ -187,9 +212,6 @@ class PrepareRadianceScene(bpy.types.Operator):
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)
sun_props = tool.Blender.get_solar_props()
sun_pos_props = tool.Blender.get_sun_props()
assert sun_pos_props
sky_file_path = os.path.join(output_dir, "sky.rad")
print("Setting up camera...")
@@ -202,56 +224,71 @@ class PrepareRadianceScene(bpy.types.Operator):
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
return {"CANCELLED"}
# Get camera position and direction
camera_position, camera_direction = self.get_camera_data(camera)
# Get camera position, direction, and up vector
camera_position, camera_direction, camera_up = self.get_camera_data(camera)
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
print(f"Camera up: {camera_up}")
# Build datetime for Radiance gensky
# Note: sun_pos_props and sun_props are synchronized by update_sun_path()
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
# Generate sky file based on use_sun setting
if props.use_sun:
# Use sun position data to generate sky
sun_props = tool.Blender.get_solar_props()
sun_pos_props = tool.Blender.get_sun_props()
if not sun_pos_props:
self.report({"ERROR"}, "Sun position addon not available. Enable 'Sun Position' addon or disable 'Use Sun'.")
return {"CANCELLED"}
print(f"Sun position data for Radiance gensky:")
print(f" DateTime: {dt}")
print(f" Latitude: {sun_props.latitude}°")
print(f" Longitude: {sun_props.longitude}°")
print(f" UTC Zone: {sun_props.UTC_zone}")
print(f" Year: {sun_pos_props.year}")
# Map sky condition enum to boolean parameters
sky_condition = props.sky_condition
sunny_with_sun = sky_condition == "SUNNY_WITH_SUN"
sunny_without_sun = sky_condition == "SUNNY_WITHOUT_SUN"
cloudy = sky_condition == "CLOUDY"
# Build datetime for Radiance gensky
# Note: sun_pos_props and sun_props are synchronized by update_sun_path()
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=sun_props.latitude,
longitude=sun_props.longitude,
year=sun_pos_props.year,
timezone=-int(sun_props.UTC_zone),
sunny_with_sun=sunny_with_sun,
sunny_without_sun=sunny_without_sun,
cloudy=cloudy,
ground_reflectance=props.ground_reflectance,
turbidity=props.turbidity,
)
print(f"Sun position data for Radiance gensky:")
print(f" DateTime: {dt}")
print(f" Latitude: {sun_props.latitude}°")
print(f" Longitude: {sun_props.longitude}°")
print(f" UTC Zone: {sun_props.UTC_zone}")
print(f" Year: {sun_pos_props.year}")
# Map sky condition enum to boolean parameters
sky_condition = props.sky_condition
sunny_with_sun = sky_condition == "SUNNY_WITH_SUN"
sunny_without_sun = sky_condition == "SUNNY_WITHOUT_SUN"
cloudy = sky_condition == "CLOUDY"
sky_description_str = sky_description.decode("utf-8")
sky_description = pr.gensky(
dt=dt,
latitude=sun_props.latitude,
longitude=sun_props.longitude,
year=sun_pos_props.year,
timezone=-int(sun_props.UTC_zone),
sunny_with_sun=sunny_with_sun,
sunny_without_sun=sunny_without_sun,
cloudy=cloudy,
ground_reflectance=props.ground_reflectance,
turbidity=props.turbidity,
)
if use_hdr and choose_hdr_image == "Noon":
sky_description_str = sky_description.decode("utf-8")
else:
# Skip sky generation when use_sun is False
print("Skipping sky generation (use_sun is False)...")
sky_description_str = None
with open(sky_file_path, "w") as f:
f.write(sky_description_str)
f.write("\n")
f.write(
'''void colorpict env_map
# Only generate sky.rad file when use_sun is True
if props.use_sun:
if use_hdr and choose_hdr_image == "Noon":
with open(sky_file_path, "w") as f:
f.write(sky_description_str)
f.write("\n")
# When using sun, skyfunc is defined by gensky, so we can use it
f.write(
'''void colorpict env_map
7 red green blue "'''
+ hdr_image_path
+ '''" "'''
+ sky_map_cal_path
+ '''" map_u map_v
+ hdr_image_path
+ '''" "'''
+ sky_map_cal_path
+ '''" map_u map_v
0
1 0.5
@@ -271,10 +308,10 @@ skyfunc colorfunc sky_colour
void mixpict composite
7 env_colour sky_colour grey "'''
+ hdr_mask_path
+ '''" "'''
+ sky_map_cal_path
+ """" map_u map_v
+ hdr_mask_path
+ '''" "'''
+ sky_map_cal_path
+ """" map_u map_v
0
2 0.5 1
@@ -297,16 +334,17 @@ ground_glow source ground
0
0
4 0 0 -1 180"""
)
)
elif not use_hdr:
with open(sky_file_path, "w") as f:
f.write(sky_description_str)
f.write("\n")
f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
elif not use_hdr:
with open(sky_file_path, "w") as f:
f.write(sky_description_str)
f.write("\n")
# When using sun, skyfunc is defined by gensky
f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
props = tool.Blender.get_radiance_exporter_props()
@@ -345,6 +383,27 @@ ground_glow source ground
self.report({"INFO"}, f"Exported Materials Rad file to: {materials_file}")
# Convert IES light files to Radiance format
print("Processing IES light files...")
converted_ies_lights = {} # {index: (rad_path, dat_path)}
for idx, ies_light in enumerate(props.ies_lights):
if not ies_light.is_enabled or not ies_light.ies_file_path or not ies_light.target_object:
continue
try:
rad_file, dat_file = convert_ies_to_radiance(
ies_light.ies_file_path,
output_dir,
lamp_type=ies_light.lamp_type,
lamp_color=ies_light.lamp_color,
multiply_factor=ies_light.multiply_factor,
radius=ies_light.radius,
)
converted_ies_lights[idx] = (rad_file, dat_file)
print(f" Converted IES light {idx}: {Path(ies_light.ies_file_path).name}")
except Exception as e:
print(f" ERROR converting IES light {idx}: {str(e)}")
self.report({"WARNING"}, f"Failed to convert IES light: {str(e)}")
print(f"OBJ file size: {os.path.getsize(obj_file_path)} bytes")
print(f"Materials file size: {os.path.getsize(materials_file)} bytes")
print(f"Converting OBJ to RTM format...")
@@ -363,9 +422,42 @@ ground_glow source ground
scene_file = os.path.join(output_dir, "scene.rad")
with open(scene_file, "w") as file:
file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n')
file.write("\n")
# Add IES light fixtures
if len(props.ies_lights) > 0:
file.write("\n# IES Light Fixtures\n")
for idx, ies_light in enumerate(props.ies_lights):
if ies_light.ies_file_path and ies_light.target_object:
obj = ies_light.target_object
pos = obj.location
z_rot = ies_light.rotation_z
if idx in converted_ies_lights:
# Light is enabled and was converted
rad_path = converted_ies_lights[idx][0]
rad_filename = Path(rad_path).name
xform_line = f"!xform -rz {z_rot} -t {pos.x} {pos.y} {pos.z} {rad_filename}\n"
file.write(xform_line)
else:
# Light is disabled or not converted - write as comment
rad_base = Path(ies_light.ies_file_path).stem
rad_filename = f"{rad_base}.rad"
xform_line = f"# !xform -rz {z_rot} -t {pos.x} {pos.y} {pos.z} {rad_filename}\n"
file.write(xform_line)
self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file))
# Validate that at least one light source will be available
has_sky = props.use_sun
has_ies_lights = len(converted_ies_lights) > 0
if not has_sky and not has_ies_lights:
error_msg = "ERROR: No light sources available. Please enable 'Use Sun' or add and map IES light fixtures."
self.report({"ERROR"}, error_msg)
print(error_msg)
return {"CANCELLED"}
print("Setting up Radiance scene...")
scene = pr.Scene("ascene")
@@ -374,7 +466,14 @@ ground_glow source ground
scene.add_material(material_path)
scene.add_surface(scene_path)
scene.add_source(sky_file_path)
# Add light source(s)
if props.use_sun:
scene.add_source(sky_file_path)
print(f"Added sky light source")
if has_ies_lights:
print(f"Added {len(converted_ies_lights)} IES light source(s)")
print("Setting up view...")
assert isinstance(camera.data, bpy.types.Camera)
if camera.data.type == "PERSP":
@@ -387,7 +486,7 @@ ground_glow source ground
aview.type = "v" # Perspective view
aview.vp = camera_position
aview.vdir = camera_direction
aview.vu = (0, 0, 1) # Assuming Z is up
aview.vu = camera_up # Use computed up vector perpendicular to view direction
aview.horiz = math.degrees(camera_fov)
aview.vert = math.degrees(vertical_fov)
else: # 'ORTHO'
@@ -401,7 +500,7 @@ ground_glow source ground
aview.type = "l" # Parallel projection (orthographic)
aview.vp = camera_position
aview.vdir = camera_direction
aview.vu = (0, 0, 1) # Assuming Z is up
aview.vu = camera_up # Use computed up vector perpendicular to view direction
aview.horiz = view_width
aview.vert = view_height
scene.add_view(aview)
@@ -470,6 +569,224 @@ class RadianceRender(bpy.types.Operator):
return {"FINISHED"}
class FalseColorRadiance(bpy.types.Operator):
"""Generate false color HDR image for illuminance analysis"""
bl_idname = "render_scene.false_color_radiance"
bl_label = "Generate False Color Image"
bl_description = "Generate a false color HDR image for illuminance analysis"
def execute(self, context):
props = tool.Blender.get_radiance_exporter_props()
output_dir = props.output_dir
output_file_name = props.output_file_name
# Check if the main render HDR exists
hdr_path = os.path.join(output_dir, f"{output_file_name}.hdr")
if not os.path.exists(hdr_path):
error_msg = f"HDR file not found at: {hdr_path}. Please run 'Radiance Render' first to generate the HDR output."
self.report({"ERROR"}, error_msg)
print(f"ERROR: {error_msg}")
return {"CANCELLED"}
print(f"Generating false color image from: {hdr_path}")
# Build false color parameters
fc_scale = str(int(props.false_color_scale)) if props.false_color_scale == int(props.false_color_scale) else str(props.false_color_scale)
# Determine contour mode
contour_mode = "l" if props.false_color_contour_lines else None
# Get multiplier based on label selection
if props.false_color_label == "fc":
multiplier = props.false_color_multiplier # Foot-candles default: 16.6295
elif props.false_color_label == "lux":
multiplier = props.false_color_multiplier if props.false_color_multiplier != 16.629505759940542 else 179.0
else: # cd/m2
multiplier = props.false_color_multiplier if props.false_color_multiplier != 16.629505759940542 else 179.0
print(f"False color parameters:")
print(f" Label: {props.false_color_label}")
print(f" Scale: {fc_scale}")
print(f" Contour lines: {props.false_color_contour_lines}")
print(f" Multiplier: {multiplier}")
try:
import subprocess
# Build command line arguments for falsecolor
# Format: falsecolor [options] -ip input.hdr > output.hdr
fc_output_name = props.false_color_output_name
fc_hdr_path = os.path.join(output_dir, f"{fc_output_name}.hdr")
# Get path to bundled falsecolor executable
light_module_dir = os.path.dirname(__file__)
falsecolor_exe = os.path.join(light_module_dir, "Radiance", "bin", "falsecolor.exe")
radiance_lib = os.path.join(light_module_dir, "Radiance", "lib")
falsecolor_bin = falsecolor_exe if os.path.exists(falsecolor_exe) else "falsecolor"
cmd = [falsecolor_bin]
# Add multiplier
cmd.extend(["-m", str(multiplier)])
# Add scale
cmd.extend(["-s", fc_scale])
# Add label
cmd.extend(["-l", props.false_color_label])
# Add contour lines if enabled
if props.false_color_contour_lines:
cmd.append("-cl")
# Add -ip flag (use same image for intensity and overlay)
cmd.append("-ip")
cmd.append(hdr_path)
print(f"Running falsecolor command: {' '.join(cmd)}")
print(f"Using falsecolor binary: {falsecolor_bin}")
# Setup environment for Radiance tools
env = os.environ.copy()
env["RAYPATH"] = "." + os.pathsep + radiance_lib
# Execute falsecolor and capture output
try:
result = subprocess.run(cmd, capture_output=True, check=True, env=env)
fc_image = result.stdout
print(f"Saving false color HDR to: {fc_hdr_path}")
with open(fc_hdr_path, "wb") as f:
f.write(fc_image)
print(f"False color HDR generated successfully: {fc_hdr_path}")
self.report({"INFO"}, f"False color image generated: {fc_hdr_path}")
except FileNotFoundError:
# falsecolor binary not found in PATH or bundled location
# Fallback: use pcond as an alternative visualization
print("WARNING: falsecolor binary not found")
print(f" Checked locations:")
print(f" 1. Bundled: {falsecolor_exe}")
print(f" 2. System PATH: falsecolor")
print("Using alternative method: Tone-mapped HDR for luminance visualization...")
print("Note: This is a fallback visualization without false color overlay")
fc_image = pr.pcond(hdr=hdr_path, human=True)
# Still save as HDR for consistency
with open(fc_hdr_path, "wb") as f:
f.write(fc_image)
print(f"Tone-mapped HDR saved to: {fc_hdr_path}")
self.report({"WARNING"},
"falsecolor binary not found. Generated tone-mapped HDR instead. "
"Ensure Radiance/bin folder is present in the light module directory."
)
except subprocess.CalledProcessError as e:
error_msg = f"falsecolor failed: {e.stderr.decode() if e.stderr else str(e)}"
print(f"ERROR: {error_msg}")
raise RuntimeError(error_msg)
# Generate TIFF version for convenience
try:
print("Generating TIFF version of false color image...")
pcond_fc_image = pr.pcond(hdr=fc_hdr_path, human=True)
fc_tiff_path = os.path.join(output_dir, f"{fc_output_name}.tiff")
print(f"Saving false color TIFF to: {fc_tiff_path}")
pr.ra_tiff(inp=pcond_fc_image, out=fc_tiff_path, lzw=True)
print(f"False color TIFF generated successfully: {fc_tiff_path}")
self.report({"INFO"}, f"False color TIFF also generated: {fc_tiff_path}")
except Exception as e:
print(f"Warning: Failed to generate false color TIFF: {str(e)}")
self.report({"WARNING"}, f"TIFF generation failed: {str(e)}")
return {"FINISHED"}
except Exception as e:
error_msg = f"Failed to generate false color image: {str(e)}"
self.report({"ERROR"}, error_msg)
print(f"ERROR: {error_msg}")
print(f"Traceback:")
import traceback
traceback.print_exc()
return {"CANCELLED"}
class ConvertHDRToFootCandles(bpy.types.Operator):
"""Convert HDR image to foot-candles unit"""
bl_idname = "render_scene.convert_hdr_to_fc"
bl_label = "Convert HDR to Foot-Candles"
bl_description = "Convert the main render HDR to foot-candles unit for illuminance verification"
def execute(self, context):
props = tool.Blender.get_radiance_exporter_props()
output_dir = props.output_dir
output_file_name = props.output_file_name
# Check if the main render HDR exists
hdr_path = os.path.join(output_dir, f"{output_file_name}.hdr")
if not os.path.exists(hdr_path):
error_msg = f"HDR file not found at: {hdr_path}. Please run 'Radiance Render' first."
self.report({"ERROR"}, error_msg)
print(f"ERROR: {error_msg}")
return {"CANCELLED"}
print(f"Converting HDR to foot-candles: {hdr_path}")
# Conversion factor from cd/m² to foot-candles
# 1 foot-candle = 10.764 lux = 10.764 cd/m²
# So to convert: multiply by 1/10.764 ≈ 0.0929
fc_scale_factor = 0.0929005
try:
# Use pyradiance Pcomb to convert
print(f"Applying pcomb with scale factor {fc_scale_factor} and original flag...")
# Create Pcomb command with original=True (-o flag) and scaler for unit conversion
# Note: fout=False ensures the output is a proper HDR file with headers, not raw data
pcomb_result = pr.Pcomb(fout=False).add(hdr_path, original=True, scaler=fc_scale_factor)()
# Save the converted HDR
fc_output_name = props.hdr_to_fc_output_name
fc_hdr_path = os.path.join(output_dir, f"{fc_output_name}.hdr")
print(f"Saving foot-candles HDR to: {fc_hdr_path}")
with open(fc_hdr_path, "wb") as f:
f.write(pcomb_result)
print(f"HDR converted to foot-candles successfully: {fc_hdr_path}")
self.report({"INFO"}, f"HDR converted to foot-candles: {fc_hdr_path}")
# Also generate TIFF version for convenient viewing
try:
print("Generating TIFF version of foot-candles image...")
pcond_fc_image = pr.pcond(hdr=fc_hdr_path, human=True)
fc_tiff_path = os.path.join(output_dir, f"{fc_output_name}.tiff")
print(f"Saving foot-candles TIFF to: {fc_tiff_path}")
pr.ra_tiff(inp=pcond_fc_image, out=fc_tiff_path, lzw=True)
print(f"TIFF version generated: {fc_tiff_path}")
self.report({"INFO"}, f"TIFF version also generated: {fc_tiff_path}")
except Exception as e:
print(f"Warning: Failed to generate foot-candles TIFF: {str(e)}")
self.report({"WARNING"}, f"TIFF generation failed: {str(e)}")
print("Foot-candles conversion completed successfully.")
return {"FINISHED"}
except Exception as e:
error_msg = f"Failed to convert HDR to foot-candles: {str(e)}"
self.report({"ERROR"}, error_msg)
print(f"ERROR: {error_msg}")
print(f"Traceback:")
import traceback
traceback.print_exc()
return {"CANCELLED"}
def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs):
try:
output_bytes = pr.obj2mesh(inp, **kwargs)
@@ -768,3 +1085,141 @@ class SetEnumProperty(bpy.types.Operator):
data = context.space_data.context_pointer_get("data")
setattr(data, self.prop_name, self.enum_value)
return {"FINISHED"}
def convert_ies_to_radiance(
ies_file_path: str,
output_dir: str,
lamp_type: str = "",
lamp_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
multiply_factor: float = 1.0,
radius: float = 0.0,
) -> tuple[str, str]:
"""
Convert IES file to Radiance format using pyradiance.
Args:
ies_file_path: Path to the .ies file
output_dir: Directory where .rad and .dat files will be saved
lamp_type: Type of lamp (e.g., 'LED', 'metal halide')
lamp_color: RGB color tuple (0.0-1.0 each)
multiply_factor: Brightness multiplier (0.1-10.0)
radius: Illum sphere radius (0 = use IES geometry)
Returns:
Tuple of (rad_file_path, dat_file_path)
"""
try:
import pyradiance as pr
ies_path = Path(ies_file_path)
base_name = ies_path.stem
# ies2rad creates .rad and .dat files
# We pass the output name root (without extension)
output_path = os.path.join(output_dir, base_name)
# Build ies2rad parameters
kwargs = {"outname": output_path}
if lamp_type:
kwargs["lamp_type"] = lamp_type
if lamp_color != (1.0, 1.0, 1.0):
kwargs["lamp_color"] = lamp_color
if multiply_factor != 1.0:
kwargs["multiply_factor"] = multiply_factor
if radius > 0.0:
kwargs["radius"] = radius
# Call ies2rad to convert the file
pr.ies2rad(ies_path, **kwargs)
rad_file = os.path.join(output_dir, f"{base_name}.rad")
dat_file = os.path.join(output_dir, f"{base_name}.dat")
return rad_file, dat_file
except Exception as e:
raise RuntimeError(f"Failed to convert IES file '{ies_file_path}': {str(e)}")
class AddIESLight(bpy.types.Operator, ImportHelper):
"""Upload and add an IES light fixture to the scene"""
bl_idname = "radiance.add_ies_light"
bl_label = "Add IES Light"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ies"
filter_glob: bpy.props.StringProperty(default="*.ies;*.IES", options={"HIDDEN"})
def execute(self, context):
props = tool.Blender.get_radiance_exporter_props()
# Create new IES light entry
ies_light = props.ies_lights.add()
ies_light.ies_file_path = self.filepath
ies_light.rotation_z = 0.0
ies_light.is_enabled = True
# Set as active
props.active_ies_light_index = len(props.ies_lights) - 1
self.report({"INFO"}, f"Added IES light: {Path(self.filepath).name}")
return {"FINISHED"}
class RemoveIESLight(bpy.types.Operator):
"""Remove an IES light fixture mapping"""
bl_idname = "radiance.remove_ies_light"
bl_label = "Remove IES Light"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Blender.get_radiance_exporter_props()
if 0 <= self.index < len(props.ies_lights):
props.ies_lights.remove(self.index)
# Adjust active index if needed
if props.active_ies_light_index >= len(props.ies_lights):
props.active_ies_light_index = len(props.ies_lights) - 1
self.report({"INFO"}, "IES light removed")
return {"FINISHED"}
self.report({"WARNING"}, "Invalid IES light index")
return {"CANCELLED"}
class SetIESLightObject(bpy.types.Operator):
"""Set the target Empty object for an IES light via eyedropper"""
bl_idname = "radiance.set_ies_light_object"
bl_label = "Set IES Light Object"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty(description="Index of IES light to set object for")
def execute(self, context):
if context.object is None or context.object.type != "EMPTY":
self.report({"WARNING"}, "Please select an Empty object")
return {"CANCELLED"}
props = tool.Blender.get_radiance_exporter_props()
if 0 <= self.index < len(props.ies_lights):
ies_light = props.ies_lights[self.index]
ies_light.target_object = context.object
props.active_ies_light_index = self.index
self.report({"INFO"}, f"Set target object to: {context.object.name}")
return {"FINISHED"}
self.report({"WARNING"}, "Invalid IES light index")
return {"CANCELLED"}
+157
View File
@@ -227,6 +227,76 @@ class RadianceMaterial(PropertyGroup):
color: tuple[float, float, float]
class IESLight(PropertyGroup):
"""Represents a mapping between an IES light file and a scene Empty object."""
ies_file_path: StringProperty(
name="IES File Path",
description="Path to the IES luminaire data file",
subtype="FILE_PATH",
default="",
)
target_object: PointerProperty(
type=bpy.types.Object,
name="Target Object",
description="Empty object where the light fixture will be placed",
poll=lambda self, obj: obj.type == "EMPTY",
)
rotation_z: FloatProperty(
name="Rotation Z",
description="Rotation around Z-axis in degrees (-180 to 180)",
min=-180.0,
max=180.0,
default=0.0,
subtype="ANGLE",
)
is_enabled: BoolProperty(
name="Enabled",
description="Include this light in the Radiance export",
default=True,
)
# Additional lamp properties for customization
lamp_type: StringProperty(
name="Lamp Type",
description="Type of lamp (e.g., 'LED', 'metal halide', 'fluorescent')",
default="",
)
lamp_color: FloatVectorProperty(
name="Lamp Color",
description="Lamp color (RGB) for custom color adjustments",
subtype="COLOR",
default=(1.0, 1.0, 1.0),
min=0.0,
max=1.0,
size=3,
)
multiply_factor: FloatProperty(
name="Brightness Factor",
description="Multiply all output quantities by this factor (0.1 to 10.0)",
min=0.1,
max=10.0,
default=1.0,
)
radius: FloatProperty(
name="Illum Sphere Radius",
description="Radius of illum sphere (ignores geometry from IES file). 0 = use IES geometry",
min=0.0,
max=10.0,
default=0.0,
)
if TYPE_CHECKING:
ies_file_path: str
target_object: Union[bpy.types.Object, None]
rotation_z: float
is_enabled: bool
lamp_type: str
lamp_color: tuple[float, float, float]
multiply_factor: float
radius: float
class RadianceExporterProperties(PropertyGroup):
def update_output_dir(self, context) -> None:
@@ -415,6 +485,12 @@ class RadianceExporterProperties(PropertyGroup):
default="Noon",
)
use_sun: BoolProperty(
name="Use Sun",
description="Use sun position data to generate sky. If disabled, generates a default sky without sun",
default=True,
)
# Sky generation parameters
sky_condition: EnumProperty(
name="Sky Condition",
@@ -454,6 +530,76 @@ class RadianceExporterProperties(PropertyGroup):
poll=lambda self, object: object.type == "CAMERA",
)
ies_lights: CollectionProperty(
type=IESLight,
name="IES Lights",
description="Collection of IES light fixtures mapped to scene objects",
)
active_ies_light_index: IntProperty(
name="Active IES Light Index",
description="Index of the active IES light in the collection",
default=-1,
)
# False color analysis properties
use_false_color: BoolProperty(
name="Generate False Color Image",
description="Generate a false color HDR image for illuminance analysis",
default=False,
)
false_color_label: EnumProperty(
name="Legend Label Unit",
description="Unit for the false color legend",
items=[
("fc", "Foot-Candles", "US lighting standard unit"),
("lux", "Lux", "International lighting standard unit"),
("cd/m2", "Candela per m²", "Luminance unit"),
],
default="fc",
)
false_color_scale: FloatProperty(
name="Scale Factor",
description="Maximum scale value for the false color legend",
min=0.1,
max=100.0,
default=3.0,
)
false_color_contour_lines: BoolProperty(
name="Contour Lines",
description="Add contour lines to the false color image",
default=True,
)
false_color_multiplier: FloatProperty(
name="Multiplier",
description="Conversion multiplier (179.0 for lux, 16.6295 for foot-candles)",
min=0.1,
max=1000.0,
default=16.629505759940542,
)
false_color_output_name: StringProperty(
name="False Color Output Name",
description="Name of the false color output file (without extension)",
default="false_color",
)
# HDR to foot-candles conversion properties
convert_hdr_to_fc: BoolProperty(
name="Convert to Foot-Candles",
description="Convert the main render HDR to foot-candles unit for illuminance verification",
default=False,
)
hdr_to_fc_output_name: StringProperty(
name="FC Output Name",
description="Name of the foot-candles converted HDR file (without extension)",
default="render_fc",
)
if TYPE_CHECKING:
is_exporting: bool
category: str
@@ -472,11 +618,22 @@ class RadianceExporterProperties(PropertyGroup):
output_file_format: Literal["HDR"]
use_hdr: bool
choose_hdr_image: Literal["Noon"]
use_sun: bool
sky_condition: Literal["SUNNY_WITH_SUN", "SUNNY_WITHOUT_SUN", "CLOUDY"]
ground_reflectance: float
turbidity: float
use_active_camera: bool
selected_camera: Union[bpy.types.Object, None]
ies_lights: bpy.types.bpy_prop_collection_idprop[IESLight]
active_ies_light_index: int
use_false_color: bool
false_color_label: Literal["fc", "lux", "cd/m2"]
false_color_scale: float
false_color_contour_lines: bool
false_color_multiplier: float
false_color_output_name: str
convert_hdr_to_fc: bool
hdr_to_fc_output_name: str
class BIMSolarProperties(PropertyGroup):
+97 -4
View File
@@ -142,16 +142,63 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row = box.row()
row.prop(props, "choose_hdr_image")
layout.separator()
# Sky Generation Settings
box = layout.box()
box.label(text="Sky Generation Settings")
box.prop(props, "sky_condition")
row = box.row()
row.prop(props, "ground_reflectance")
row.prop(props, "use_sun")
if props.use_sun:
box.label(text="Sky Generation Settings")
box.prop(props, "sky_condition")
row = box.row()
row.prop(props, "ground_reflectance")
row = box.row()
row.prop(props, "turbidity")
layout.separator()
# IES Light Fixtures
box = layout.box()
box.label(text="IES Light Fixtures")
row = box.row()
row.prop(props, "turbidity")
row.template_list("MATERIAL_UL_ies_lights", "", props, "ies_lights", props, "active_ies_light_index")
col = row.column(align=True)
col.operator("radiance.add_ies_light", text="", icon="ADD")
# Properties panel for selected IES light
if len(props.ies_lights) > 0 and props.active_ies_light_index >= 0 and props.active_ies_light_index < len(
props.ies_lights
):
active_light = props.ies_lights[props.active_ies_light_index]
box = layout.box()
box.label(text="Selected Light Properties")
# Rotation Z
row = box.row()
row.prop(active_light, "rotation_z")
# Lamp Type
row = box.row()
row.prop(active_light, "lamp_type")
# Lamp Color
row = box.row()
row.prop(active_light, "lamp_color")
# Brightness Factor
row = box.row()
row.prop(active_light, "multiply_factor")
# Illum Sphere Radius
row = box.row()
row.prop(active_light, "radius")
layout.separator()
# Step 1: Export geometry for simulation
box = layout.box()
@@ -176,6 +223,52 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row.operator("render_scene.radiance", text="Radiance Render")
row.enabled = not props.is_exporting
layout.separator()
# Step 4: Generate false color image
box = layout.box()
box.label(text="Step 4: Generate False Color Image")
row = box.row()
row.prop(props, "use_false_color")
if props.use_false_color:
row = box.row()
row.prop(props, "false_color_label")
row = box.row()
row.label(text="Scale Factor")
row.prop(props, "false_color_scale", text="")
row = box.row()
row.prop(props, "false_color_contour_lines")
row = box.row()
row.label(text="Multiplier")
row.prop(props, "false_color_multiplier", text="")
row = box.row()
row.label(text="Output Name")
row.prop(props, "false_color_output_name", text="")
row = box.row()
row.operator("render_scene.false_color_radiance", text="Generate False Color Image")
layout.separator()
# Step 5: Convert to foot-candles
box = layout.box()
box.label(text="Step 5: Convert HDR to Foot-Candles")
row = box.row()
row.prop(props, "convert_hdr_to_fc")
if props.convert_hdr_to_fc:
row = box.row()
row.label(text="Output Name")
row.prop(props, "hdr_to_fc_output_name", text="")
row = box.row()
row.operator("render_scene.convert_hdr_to_fc", text="Convert to Foot-Candles")
class BIM_PT_solar(bpy.types.Panel):
"""Creates a Panel in the render properties window"""