mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 10:33:20 +00:00
Light: Optimized radiance rendering workflow
This commit is contained in:
@@ -52,6 +52,7 @@ classes = (
|
||||
operator.AddIESLight,
|
||||
operator.RemoveIESLight,
|
||||
operator.SetIESLightObject,
|
||||
operator.CleanupRadianceFiles,
|
||||
prop.RadianceMaterial,
|
||||
prop.IESLight,
|
||||
prop.BIMSolarProperties,
|
||||
@@ -64,7 +65,7 @@ classes = (
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
|
||||
bpy.types.Scene.BIMRadianceExporterProperties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
|
||||
bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties)
|
||||
|
||||
if pyradiance:
|
||||
@@ -77,5 +78,5 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMRadianceExporeterProperies
|
||||
del bpy.types.Scene.BIMRadianceExporterProperties
|
||||
del bpy.types.Scene.BIMSolarProperties
|
||||
|
||||
@@ -18,17 +18,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import bpy
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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)
|
||||
|
||||
|
||||
class MATERIAL_UL_radiance_materials(bpy.types.UIList):
|
||||
def draw_item(
|
||||
@@ -89,12 +85,13 @@ class MATERIAL_UL_ies_lights(bpy.types.UIList):
|
||||
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
|
||||
# Target: collection or single object
|
||||
if item.use_collection:
|
||||
row.prop(item, "target_collection", text="", icon="OUTLINER_COLLECTION", emboss=False)
|
||||
else:
|
||||
row.prop(item, "target_object", text="", emboss=False)
|
||||
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")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
@@ -43,7 +44,6 @@ from bonsai.bim.module.light.data import SolarData
|
||||
from bonsai.bim.module.light.decorator import SolarDecorator
|
||||
|
||||
sun_position = tool.Blender.get_addon("sun_position")
|
||||
now = datetime.datetime.now()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
spectraldb: dict[str, dict[str, str]] = json.load(f)
|
||||
@@ -72,8 +72,8 @@ def update_coordinates(self: "BIMSolarProperties", context: bpy.types.Context) -
|
||||
def update_latlong(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
sun_props.longitude = sun_props.longitude
|
||||
sun_props.latitude = sun_props.latitude
|
||||
sun_props.longitude = self.longitude
|
||||
sun_props.latitude = self.latitude
|
||||
self["coordinates"] = sun_props.coordinates
|
||||
update_sun_path(self)
|
||||
|
||||
@@ -135,6 +135,14 @@ def update_resolution(self: "RadianceExporterProperties", context: bpy.types.Con
|
||||
context.scene.render.resolution_y = self.radiance_resolution_y
|
||||
|
||||
|
||||
def update_day(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
|
||||
"""Clamp the day to the valid range for the current month/year."""
|
||||
max_day = calendar.monthrange(self.year, self.month)[1]
|
||||
if self.day > max_day:
|
||||
self["day"] = max_day
|
||||
update_sun_path(self)
|
||||
|
||||
|
||||
def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context, None] = None) -> None:
|
||||
if not SolarData.is_loaded:
|
||||
SolarData.load()
|
||||
@@ -152,9 +160,13 @@ def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context
|
||||
sun_props.sun_distance = self.sun_path_size
|
||||
sun_props.latitude = self.latitude
|
||||
sun_props.longitude = self.longitude
|
||||
# Clamp day to valid range for the current month/year
|
||||
max_day = calendar.monthrange(self.year, self.month)[1]
|
||||
day = min(self.day, max_day)
|
||||
|
||||
sun_props.year = self.year
|
||||
sun_props.month = self.month
|
||||
sun_props.day = self.day
|
||||
sun_props.day = day
|
||||
sun_props.time = self.hour + (self.minute / 60)
|
||||
# Preserve IFC sign convention
|
||||
sun_props.north_offset = self.true_north * -1
|
||||
@@ -230,7 +242,12 @@ class RadianceMaterial(PropertyGroup):
|
||||
|
||||
|
||||
class IESLight(PropertyGroup):
|
||||
"""Represents a mapping between an IES light file and a scene Empty object."""
|
||||
"""Represents a mapping between an IES light file and scene Empty objects.
|
||||
|
||||
Supports two targeting modes:
|
||||
- Object mode: target a single Empty object
|
||||
- Collection mode: target all Empty objects in a collection
|
||||
"""
|
||||
|
||||
ies_file_path: StringProperty(
|
||||
name="IES File Path",
|
||||
@@ -238,12 +255,22 @@ class IESLight(PropertyGroup):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
)
|
||||
use_collection: BoolProperty(
|
||||
name="Use Collection",
|
||||
description="Apply this light to all Empty objects in a collection instead of a single object",
|
||||
default=False,
|
||||
)
|
||||
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",
|
||||
)
|
||||
target_collection: PointerProperty(
|
||||
type=bpy.types.Collection,
|
||||
name="Target Collection",
|
||||
description="Collection of Empty objects where the light fixture will be placed",
|
||||
)
|
||||
rotation_z: FloatProperty(
|
||||
name="Rotation Z",
|
||||
description="Rotation around Z-axis in degrees (-180 to 180)",
|
||||
@@ -288,9 +315,24 @@ class IESLight(PropertyGroup):
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
def get_target_empties(self) -> list[bpy.types.Object]:
|
||||
"""Return all Empty objects this light targets."""
|
||||
if self.use_collection and self.target_collection is not None:
|
||||
return [obj for obj in self.target_collection.all_objects if obj.type == "EMPTY" and not obj.hide_get()]
|
||||
elif not self.use_collection and self.target_object is not None:
|
||||
try:
|
||||
_ = self.target_object.name
|
||||
if not self.target_object.hide_get():
|
||||
return [self.target_object]
|
||||
except ReferenceError:
|
||||
pass
|
||||
return []
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ies_file_path: str
|
||||
use_collection: bool
|
||||
target_object: Union[bpy.types.Object, None]
|
||||
target_collection: Union[bpy.types.Collection, None]
|
||||
rotation_z: float
|
||||
is_enabled: bool
|
||||
lamp_type: str
|
||||
@@ -326,7 +368,10 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
mappings = json.load(f)
|
||||
|
||||
for style_id, mapping in mappings.items():
|
||||
material = self.get_material_mapping(mapping["name"])
|
||||
# Try matching by style_id first, fall back to name
|
||||
material = self.get_material_mapping_by_id(style_id)
|
||||
if material is None:
|
||||
material = self.get_material_mapping(mapping["name"])
|
||||
if material:
|
||||
material.style_id = style_id
|
||||
material.category = mapping["category"]
|
||||
@@ -338,11 +383,14 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
new_material.subcategory = mapping["subcategory"]
|
||||
new_material.is_mapped = True
|
||||
|
||||
def get_material_mapping_by_id(self, style_id: str) -> Union[RadianceMaterial, None]:
|
||||
return next((item for item in self.materials if item.style_id == style_id), None)
|
||||
|
||||
def get_material_mapping(self, style_name: str) -> Union[RadianceMaterial, None]:
|
||||
return next((item for item in self.materials if item.name == style_name), None)
|
||||
|
||||
def set_material_mapping(self, style_id: str, style_name: str, category: str, subcategory: str) -> None:
|
||||
item = self.get_material_mapping(style_name)
|
||||
item = self.get_material_mapping_by_id(style_id) or self.get_material_mapping(style_name)
|
||||
if item:
|
||||
item.category = category
|
||||
item.subcategory = subcategory
|
||||
@@ -358,8 +406,12 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
if item.category and item.subcategory
|
||||
}
|
||||
|
||||
def unmap_material(self, style_name: str) -> None:
|
||||
item = self.get_material_mapping(style_name)
|
||||
def unmap_material(self, style_name: str, style_id: str = "") -> None:
|
||||
item = None
|
||||
if style_id:
|
||||
item = self.get_material_mapping_by_id(style_id)
|
||||
if item is None:
|
||||
item = self.get_material_mapping(style_name)
|
||||
if item:
|
||||
item.category = ""
|
||||
item.subcategory = ""
|
||||
@@ -369,6 +421,14 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
name="Is Exporting", description="Whether the OBJ export is in progress", default=False
|
||||
)
|
||||
|
||||
is_preparing: bpy.props.BoolProperty(
|
||||
name="Is Preparing", description="Whether scene preparation is in progress", default=False
|
||||
)
|
||||
|
||||
is_rendering: bpy.props.BoolProperty(
|
||||
name="Is Rendering", description="Whether a Radiance render is in progress", default=False
|
||||
)
|
||||
|
||||
categories = [
|
||||
("Wall", "Wall", ""),
|
||||
("Floor", "Floor", ""),
|
||||
@@ -436,6 +496,15 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
update=lambda self, context: self.update_ifc_file(context),
|
||||
)
|
||||
|
||||
ambient_bounces: IntProperty(
|
||||
name="Ambient Bounces",
|
||||
description="Number of indirect light bounces. Higher = more light fills dark areas but slower. "
|
||||
"1 is minimal, 2-3 recommended for IES-only scenes, 4+ for complex interiors",
|
||||
min=0,
|
||||
max=8,
|
||||
default=2,
|
||||
)
|
||||
|
||||
radiance_quality: EnumProperty(
|
||||
name="Quality",
|
||||
description="Radiance rendering quality",
|
||||
@@ -569,6 +638,15 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
default=3.0,
|
||||
)
|
||||
|
||||
false_color_steps: IntProperty(
|
||||
name="Legend Steps",
|
||||
description="Number of divisions on the legend. Contour increment = Scale / Steps. "
|
||||
"E.g. Scale=20, Steps=10 → contours at 2, 4, 6, 8...",
|
||||
min=2,
|
||||
max=50,
|
||||
default=10,
|
||||
)
|
||||
|
||||
false_color_contour_lines: BoolProperty(
|
||||
name="Enable Contour Lines",
|
||||
description="Add contour lines to the false color image",
|
||||
@@ -605,6 +683,8 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_exporting: bool
|
||||
is_preparing: bool
|
||||
is_rendering: bool
|
||||
category: str
|
||||
subcategory: str
|
||||
materials: bpy.types.bpy_prop_collection_idprop[RadianceMaterial]
|
||||
@@ -614,6 +694,7 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
radiance_resolution_y: int
|
||||
output_dir: str
|
||||
ifc_file: str
|
||||
ambient_bounces: int
|
||||
radiance_quality: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
radiance_detail: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
radiance_variability: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
@@ -632,6 +713,7 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
use_false_color: bool
|
||||
false_color_label: Literal["fc", "lux", "cd/m2"]
|
||||
false_color_scale: float
|
||||
false_color_steps: int
|
||||
false_color_contour_lines: bool
|
||||
false_color_contour_mode: Literal["WITH_BG", "WITHOUT_BG"]
|
||||
false_color_multiplier: float
|
||||
@@ -662,11 +744,12 @@ class BIMSolarProperties(PropertyGroup):
|
||||
)
|
||||
timezone: StringProperty(name="Timezone", default="Etc/GMT")
|
||||
true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_sun_path)
|
||||
year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_sun_path)
|
||||
month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_sun_path)
|
||||
day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_sun_path)
|
||||
hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_sun_path)
|
||||
minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_sun_path)
|
||||
# Defaults are static; use the "Now" button (LightSetTimeToNow) to set current time.
|
||||
year: IntProperty(name="Year", min=1, max=9999, default=2025, update=update_day)
|
||||
month: IntProperty(name="Month", min=1, max=12, default=1, update=update_day)
|
||||
day: IntProperty(name="Date", min=1, max=31, default=1, update=update_day)
|
||||
hour: IntProperty(name="Hour", min=0, max=23, default=12, update=update_sun_path)
|
||||
minute: IntProperty(name="Minute", min=0, max=59, default=0, update=update_sun_path)
|
||||
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)
|
||||
|
||||
@@ -130,6 +130,9 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
row = box.row()
|
||||
row.prop(props, "radiance_variability")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "ambient_bounces")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "output_file_name")
|
||||
|
||||
@@ -140,10 +143,6 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
row = box.row()
|
||||
row.prop(props, "use_hdr")
|
||||
|
||||
if props.use_hdr:
|
||||
row = box.row()
|
||||
row.prop(props, "choose_hdr_image")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Sky Generation Settings
|
||||
@@ -179,6 +178,21 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
box = layout.box()
|
||||
box.label(text="Selected Light Properties")
|
||||
|
||||
# Target mode toggle
|
||||
row = box.row()
|
||||
row.prop(active_light, "use_collection", text="Target Collection", icon="OUTLINER_COLLECTION")
|
||||
|
||||
# Show the appropriate target picker
|
||||
row = box.row()
|
||||
if active_light.use_collection:
|
||||
row.prop(active_light, "target_collection", text="Collection")
|
||||
if active_light.target_collection:
|
||||
empties = [o for o in active_light.target_collection.all_objects if o.type == "EMPTY"]
|
||||
row = box.row()
|
||||
row.label(text=f"{len(empties)} empty object(s) in collection", icon="INFO")
|
||||
else:
|
||||
row.prop(active_light, "target_object", text="Object")
|
||||
|
||||
# Rotation Z
|
||||
row = box.row()
|
||||
row.prop(active_light, "rotation_z")
|
||||
@@ -213,7 +227,10 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
box = layout.box()
|
||||
box.label(text="Step 2: Prepare Radiance scene")
|
||||
row = box.row()
|
||||
row.operator("scene.prepare_radiance", text="Prepare Scene")
|
||||
if props.is_preparing:
|
||||
row.label(text="Preparing scene...", icon="SORTTIME")
|
||||
else:
|
||||
row.operator("scene.prepare_radiance", text="Prepare Scene")
|
||||
|
||||
layout.separator()
|
||||
|
||||
@@ -221,8 +238,11 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
box = layout.box()
|
||||
box.label(text="Step 3: Run the simulation")
|
||||
row = box.row()
|
||||
row.operator("render_scene.radiance", text="Radiance Render")
|
||||
row.enabled = not props.is_exporting
|
||||
if props.is_rendering:
|
||||
row.label(text="Rendering in progress...", icon="RENDER_STILL")
|
||||
else:
|
||||
row.operator("render_scene.radiance", text="Radiance Render")
|
||||
row.enabled = not props.is_exporting
|
||||
|
||||
layout.separator()
|
||||
|
||||
@@ -240,6 +260,9 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
row.label(text="Scale Factor")
|
||||
row.prop(props, "false_color_scale", text="")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "false_color_steps")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "false_color_contour_lines")
|
||||
|
||||
@@ -259,6 +282,14 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
row = box.row()
|
||||
row.operator("render_scene.false_color_radiance", text="Generate False Color Image")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Cleanup
|
||||
box = layout.box()
|
||||
box.label(text="Cleanup")
|
||||
row = box.row()
|
||||
row.operator("radiance.cleanup_files", text="Cleanup Generated Files", icon="TRASH")
|
||||
|
||||
|
||||
class BIM_PT_solar(bpy.types.Panel):
|
||||
"""Creates a Panel in the render properties window"""
|
||||
@@ -375,7 +406,10 @@ class BIM_PT_solar(bpy.types.Panel):
|
||||
row = self.layout.row()
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity")
|
||||
if sun_props.sun_object is not None and sun_props.sun_object.data is not None:
|
||||
row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity")
|
||||
else:
|
||||
row.label(text="Sun object not found. Toggle shadow mode to recreate.", icon="ERROR")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.view_from_sun", icon="LIGHT_HEMI")
|
||||
|
||||
@@ -1845,7 +1845,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
@classmethod
|
||||
def get_radiance_exporter_props(cls) -> RadianceExporterProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMRadianceExporeterProperies # pyright: ignore[reportAttributeAccessIssue]
|
||||
return scene.BIMRadianceExporterProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_fm_props(cls) -> BIMFMProperties:
|
||||
|
||||
Reference in New Issue
Block a user