mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
Light(Radiance): Implemented IES File Mapping | Render False Color Images | Updated UI
This commit is contained in:
@@ -38,6 +38,8 @@ classes = (
|
|||||||
operator.ImportTrueNorth,
|
operator.ImportTrueNorth,
|
||||||
operator.MoveSunPathTo3DCursor,
|
operator.MoveSunPathTo3DCursor,
|
||||||
operator.RadianceRender,
|
operator.RadianceRender,
|
||||||
|
operator.FalseColorRadiance,
|
||||||
|
operator.ConvertHDRToFootCandles,
|
||||||
operator.ViewFromSun,
|
operator.ViewFromSun,
|
||||||
operator.LightPickCoordinates,
|
operator.LightPickCoordinates,
|
||||||
operator.LightSetTimeToNow,
|
operator.LightSetTimeToNow,
|
||||||
@@ -50,12 +52,17 @@ classes = (
|
|||||||
operator.EnumPropertySearch,
|
operator.EnumPropertySearch,
|
||||||
operator.PrepareRadianceScene,
|
operator.PrepareRadianceScene,
|
||||||
operator.SetEnumProperty,
|
operator.SetEnumProperty,
|
||||||
|
operator.AddIESLight,
|
||||||
|
operator.RemoveIESLight,
|
||||||
|
operator.SetIESLightObject,
|
||||||
prop.RadianceMaterial,
|
prop.RadianceMaterial,
|
||||||
|
prop.IESLight,
|
||||||
prop.BIMSolarProperties,
|
prop.BIMSolarProperties,
|
||||||
prop.RadianceExporterProperties,
|
prop.RadianceExporterProperties,
|
||||||
ui.BIM_PT_radiance_exporter,
|
ui.BIM_PT_radiance_exporter,
|
||||||
ui.BIM_PT_solar,
|
ui.BIM_PT_solar,
|
||||||
list.MATERIAL_UL_radiance_materials,
|
list.MATERIAL_UL_radiance_materials,
|
||||||
|
list.MATERIAL_UL_ies_lights,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import bpy
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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:
|
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||||
spectraldb = json.load(f)
|
spectraldb = json.load(f)
|
||||||
@@ -59,3 +60,42 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList):
|
|||||||
op.material_index = index
|
op.material_index = index
|
||||||
else:
|
else:
|
||||||
row.label(text="Not Mapped (White)")
|
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
|
||||||
|
|||||||
@@ -141,7 +141,32 @@ class PrepareRadianceScene(bpy.types.Operator):
|
|||||||
direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||||
direction.normalize()
|
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):
|
def execute(self, context):
|
||||||
print("Starting Radiance scene preparation...")
|
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)
|
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)
|
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")
|
sky_file_path = os.path.join(output_dir, "sky.rad")
|
||||||
|
|
||||||
print("Setting up camera...")
|
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.")
|
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
# Get camera position and direction
|
# Get camera position, direction, and up vector
|
||||||
camera_position, camera_direction = self.get_camera_data(camera)
|
camera_position, camera_direction, camera_up = self.get_camera_data(camera)
|
||||||
|
|
||||||
print(f"Camera position: {camera_position}")
|
print(f"Camera position: {camera_position}")
|
||||||
print(f"Camera direction: {camera_direction}")
|
print(f"Camera direction: {camera_direction}")
|
||||||
|
print(f"Camera up: {camera_up}")
|
||||||
|
|
||||||
# Build datetime for Radiance gensky
|
# Generate sky file based on use_sun setting
|
||||||
# Note: sun_pos_props and sun_props are synchronized by update_sun_path()
|
if props.use_sun:
|
||||||
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
|
# 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:")
|
# Build datetime for Radiance gensky
|
||||||
print(f" DateTime: {dt}")
|
# Note: sun_pos_props and sun_props are synchronized by update_sun_path()
|
||||||
print(f" Latitude: {sun_props.latitude}°")
|
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
|
||||||
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 = pr.gensky(
|
print(f"Sun position data for Radiance gensky:")
|
||||||
dt=dt,
|
print(f" DateTime: {dt}")
|
||||||
latitude=sun_props.latitude,
|
print(f" Latitude: {sun_props.latitude}°")
|
||||||
longitude=sun_props.longitude,
|
print(f" Longitude: {sun_props.longitude}°")
|
||||||
year=sun_pos_props.year,
|
print(f" UTC Zone: {sun_props.UTC_zone}")
|
||||||
timezone=-int(sun_props.UTC_zone),
|
print(f" Year: {sun_pos_props.year}")
|
||||||
sunny_with_sun=sunny_with_sun,
|
# Map sky condition enum to boolean parameters
|
||||||
sunny_without_sun=sunny_without_sun,
|
sky_condition = props.sky_condition
|
||||||
cloudy=cloudy,
|
sunny_with_sun = sky_condition == "SUNNY_WITH_SUN"
|
||||||
ground_reflectance=props.ground_reflectance,
|
sunny_without_sun = sky_condition == "SUNNY_WITHOUT_SUN"
|
||||||
turbidity=props.turbidity,
|
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:
|
# Only generate sky.rad file when use_sun is True
|
||||||
f.write(sky_description_str)
|
if props.use_sun:
|
||||||
f.write("\n")
|
if use_hdr and choose_hdr_image == "Noon":
|
||||||
|
with open(sky_file_path, "w") as f:
|
||||||
f.write(
|
f.write(sky_description_str)
|
||||||
'''void colorpict env_map
|
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 "'''
|
7 red green blue "'''
|
||||||
+ hdr_image_path
|
+ hdr_image_path
|
||||||
+ '''" "'''
|
+ '''" "'''
|
||||||
+ sky_map_cal_path
|
+ sky_map_cal_path
|
||||||
+ '''" map_u map_v
|
+ '''" map_u map_v
|
||||||
0
|
0
|
||||||
1 0.5
|
1 0.5
|
||||||
|
|
||||||
@@ -271,10 +308,10 @@ skyfunc colorfunc sky_colour
|
|||||||
|
|
||||||
void mixpict composite
|
void mixpict composite
|
||||||
7 env_colour sky_colour grey "'''
|
7 env_colour sky_colour grey "'''
|
||||||
+ hdr_mask_path
|
+ hdr_mask_path
|
||||||
+ '''" "'''
|
+ '''" "'''
|
||||||
+ sky_map_cal_path
|
+ sky_map_cal_path
|
||||||
+ """" map_u map_v
|
+ """" map_u map_v
|
||||||
0
|
0
|
||||||
2 0.5 1
|
2 0.5 1
|
||||||
|
|
||||||
@@ -297,16 +334,17 @@ ground_glow source ground
|
|||||||
0
|
0
|
||||||
0
|
0
|
||||||
4 0 0 -1 180"""
|
4 0 0 -1 180"""
|
||||||
)
|
)
|
||||||
|
|
||||||
elif not use_hdr:
|
elif not use_hdr:
|
||||||
with open(sky_file_path, "w") as f:
|
with open(sky_file_path, "w") as f:
|
||||||
f.write(sky_description_str)
|
f.write(sky_description_str)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
|
# When using sun, skyfunc is defined by gensky
|
||||||
f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
|
f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
|
||||||
f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
|
f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
|
||||||
f.write("ground_glow source ground\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()
|
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}")
|
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"OBJ file size: {os.path.getsize(obj_file_path)} bytes")
|
||||||
print(f"Materials file size: {os.path.getsize(materials_file)} bytes")
|
print(f"Materials file size: {os.path.getsize(materials_file)} bytes")
|
||||||
print(f"Converting OBJ to RTM format...")
|
print(f"Converting OBJ to RTM format...")
|
||||||
@@ -363,9 +422,42 @@ ground_glow source ground
|
|||||||
scene_file = os.path.join(output_dir, "scene.rad")
|
scene_file = os.path.join(output_dir, "scene.rad")
|
||||||
with open(scene_file, "w") as file:
|
with open(scene_file, "w") as file:
|
||||||
file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n')
|
file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n')
|
||||||
|
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))
|
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...")
|
print("Setting up Radiance scene...")
|
||||||
scene = pr.Scene("ascene")
|
scene = pr.Scene("ascene")
|
||||||
|
|
||||||
@@ -374,7 +466,14 @@ ground_glow source ground
|
|||||||
|
|
||||||
scene.add_material(material_path)
|
scene.add_material(material_path)
|
||||||
scene.add_surface(scene_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...")
|
print("Setting up view...")
|
||||||
assert isinstance(camera.data, bpy.types.Camera)
|
assert isinstance(camera.data, bpy.types.Camera)
|
||||||
if camera.data.type == "PERSP":
|
if camera.data.type == "PERSP":
|
||||||
@@ -387,7 +486,7 @@ ground_glow source ground
|
|||||||
aview.type = "v" # Perspective view
|
aview.type = "v" # Perspective view
|
||||||
aview.vp = camera_position
|
aview.vp = camera_position
|
||||||
aview.vdir = camera_direction
|
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.horiz = math.degrees(camera_fov)
|
||||||
aview.vert = math.degrees(vertical_fov)
|
aview.vert = math.degrees(vertical_fov)
|
||||||
else: # 'ORTHO'
|
else: # 'ORTHO'
|
||||||
@@ -401,7 +500,7 @@ ground_glow source ground
|
|||||||
aview.type = "l" # Parallel projection (orthographic)
|
aview.type = "l" # Parallel projection (orthographic)
|
||||||
aview.vp = camera_position
|
aview.vp = camera_position
|
||||||
aview.vdir = camera_direction
|
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.horiz = view_width
|
||||||
aview.vert = view_height
|
aview.vert = view_height
|
||||||
scene.add_view(aview)
|
scene.add_view(aview)
|
||||||
@@ -470,6 +569,224 @@ class RadianceRender(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
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):
|
def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs):
|
||||||
try:
|
try:
|
||||||
output_bytes = pr.obj2mesh(inp, **kwargs)
|
output_bytes = pr.obj2mesh(inp, **kwargs)
|
||||||
@@ -768,3 +1085,141 @@ class SetEnumProperty(bpy.types.Operator):
|
|||||||
data = context.space_data.context_pointer_get("data")
|
data = context.space_data.context_pointer_get("data")
|
||||||
setattr(data, self.prop_name, self.enum_value)
|
setattr(data, self.prop_name, self.enum_value)
|
||||||
return {"FINISHED"}
|
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"}
|
||||||
|
|||||||
@@ -227,6 +227,76 @@ class RadianceMaterial(PropertyGroup):
|
|||||||
color: tuple[float, float, float]
|
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):
|
class RadianceExporterProperties(PropertyGroup):
|
||||||
|
|
||||||
def update_output_dir(self, context) -> None:
|
def update_output_dir(self, context) -> None:
|
||||||
@@ -415,6 +485,12 @@ class RadianceExporterProperties(PropertyGroup):
|
|||||||
default="Noon",
|
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 generation parameters
|
||||||
sky_condition: EnumProperty(
|
sky_condition: EnumProperty(
|
||||||
name="Sky Condition",
|
name="Sky Condition",
|
||||||
@@ -454,6 +530,76 @@ class RadianceExporterProperties(PropertyGroup):
|
|||||||
poll=lambda self, object: object.type == "CAMERA",
|
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:
|
if TYPE_CHECKING:
|
||||||
is_exporting: bool
|
is_exporting: bool
|
||||||
category: str
|
category: str
|
||||||
@@ -472,11 +618,22 @@ class RadianceExporterProperties(PropertyGroup):
|
|||||||
output_file_format: Literal["HDR"]
|
output_file_format: Literal["HDR"]
|
||||||
use_hdr: bool
|
use_hdr: bool
|
||||||
choose_hdr_image: Literal["Noon"]
|
choose_hdr_image: Literal["Noon"]
|
||||||
|
use_sun: bool
|
||||||
sky_condition: Literal["SUNNY_WITH_SUN", "SUNNY_WITHOUT_SUN", "CLOUDY"]
|
sky_condition: Literal["SUNNY_WITH_SUN", "SUNNY_WITHOUT_SUN", "CLOUDY"]
|
||||||
ground_reflectance: float
|
ground_reflectance: float
|
||||||
turbidity: float
|
turbidity: float
|
||||||
use_active_camera: bool
|
use_active_camera: bool
|
||||||
selected_camera: Union[bpy.types.Object, None]
|
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):
|
class BIMSolarProperties(PropertyGroup):
|
||||||
|
|||||||
@@ -142,16 +142,63 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
|||||||
row = box.row()
|
row = box.row()
|
||||||
row.prop(props, "choose_hdr_image")
|
row.prop(props, "choose_hdr_image")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
# Sky Generation Settings
|
# Sky Generation Settings
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Sky Generation Settings")
|
|
||||||
box.prop(props, "sky_condition")
|
|
||||||
row = box.row()
|
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 = 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
|
# Step 1: Export geometry for simulation
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
@@ -176,6 +223,52 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
|||||||
row.operator("render_scene.radiance", text="Radiance Render")
|
row.operator("render_scene.radiance", text="Radiance Render")
|
||||||
row.enabled = not props.is_exporting
|
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):
|
class BIM_PT_solar(bpy.types.Panel):
|
||||||
"""Creates a Panel in the render properties window"""
|
"""Creates a Panel in the render properties window"""
|
||||||
|
|||||||
Reference in New Issue
Block a user