Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,53 @@ def _update_scene(self, depsgraph, context, changes, luxcore_scene):
)
props.Set(world_props)

# The attached sun of a Prague Sky world is exported as a sun3
# light whose parameters (atmosphere) depend on the world.
# Re-export it so it stays in sync when the world changes.
self._reexport_sky3_sun(depsgraph, luxcore_scene, props, context.scene)

return props

def _reexport_sky3_sun(self, depsgraph, luxcore_scene, scene_props, scene):
"""
Re-export the sun lamps of a Prague Sky world when the world changes
(e.g. visibility, altitude, groundalbedo are edited) so their sun3
lights pick up the new atmosphere parameters.
"""
world = scene.world
if (not world or world.luxcore.use_cycles_settings
or world.luxcore.light != "sky3"):
return

# Collect the sun lamps whose export depends on the world settings:
# the attached sun and all explicit Prague Sun (sun3) lamps
suns = set()
attached = world.luxcore.sun
if (attached and attached.type == "LIGHT" and attached.data
and attached.data.luxcore.light_type in ("sun", "sun3")):
suns.add(attached)
for obj in scene.objects:
if (obj.type == "LIGHT" and obj.data and obj.data.type == "SUN"
and obj.data.luxcore.light_type == "sun3"):
suns.add(obj)

for sun in suns:
obj_key = utils.make_key(sun)
if obj_key not in self.object_cache2.exported_objects:
# Not exported (e.g. hidden in this view layer), nothing to do
continue
if obj_key in self.object_cache2.reexported_light_keys:
# Already re-exported during this update pass, e.g. because
# the sun object itself was updated
continue

light_props, exported_light = light.convert_light(
self, sun, obj_key, depsgraph, luxcore_scene,
sun.matrix_world.copy(), True)
if exported_light:
self.object_cache2.exported_objects[obj_key] = exported_light
scene_props.Set(light_props)

def _init_stats(self, stats, config_props, scene):
render_engine = config_props.Get("renderengine.type").GetString()
stats.render_engine.value = utils_render.engine_to_str(render_engine)
Expand Down
2 changes: 1 addition & 1 deletion export/caches/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def diff(self, depsgraph):
world_updated = depsgraph.id_type_updated("WORLD") or self.world_name != world.name_full

# The sun influcences the world, e.g. through direction and turbidity if sky2 is used
if world.luxcore.light == "sky2" and depsgraph.id_type_updated("OBJECT"):
if world.luxcore.light in ("sky2", "sky3") and depsgraph.id_type_updated("OBJECT"):
for dg_update in depsgraph.updates:
if dg_update.id == world.luxcore.sun:
world_updated = True
Expand Down
27 changes: 27 additions & 0 deletions export/caches/object_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ def __init__(self):
self.exported_objects = {}
self.exported_meshes = {}
self.exported_hair = {}
# Keys of lights that were re-exported in the last update() pass.
# Used by the world update (see Exporter._reexport_sky3_sun) to
# avoid re-exporting the same light twice.
self.reexported_light_keys = set()

def first_run(
self,
Expand Down Expand Up @@ -767,6 +771,20 @@ def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):
# Always instance in viewport so we can move objects around
use_instancing = True

# Keys of light objects that were updated (moved or settings changed)
# in this depsgraph evaluation, and keys of lights that were already
# re-exported by the geometry update loop below.
# Lights are only re-exported if they actually changed, because
# re-exporting a light deletes and re-creates it in LuxCore, which can
# be expensive (e.g. the Prague Sky Model dataset is loaded again).
updated_light_ptrs = set()
reexported_light_keys = set()
self.reexported_light_keys = reexported_light_keys
for dg_update in depsgraph.updates:
if (isinstance(dg_update.id, bpy.types.Object)
and dg_update.id.type == "LIGHT"):
updated_light_ptrs.add(utils.make_key(dg_update.id))

# Geometry updates (mesh edit, modifier edit etc.)
if depsgraph.id_type_updated("OBJECT"):
for dg_update in depsgraph.updates:
Expand Down Expand Up @@ -863,6 +881,7 @@ def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):
if exported_stuff:
self.exported_objects[obj_key] = exported_stuff
scene_props.Set(props)
reexported_light_keys.add(obj_key)

# TODO maybe not loop over all instances, instead only loop over updated
# objects and check if they have a particle system that needs to be updated?
Expand Down Expand Up @@ -908,6 +927,14 @@ def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):

if updated:
scene_props.Set(exported_obj.get_props())
elif obj.type == "LIGHT" and not dg_obj_instance.is_instance and (
obj_key in reexported_light_keys
or (obj_key in self.exported_objects
and utils.make_key(dg_obj_instance.object) not in updated_light_ptrs)):
# Already exported light that was not updated, nothing to do.
# (Instanced lights are always re-exported because we can not
# tell if their emitter moved)
continue
else:
# Object is new and not in LuxCore yet, or it is a light, do a full export
self._convert_obj(
Expand Down
75 changes: 71 additions & 4 deletions export/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

WORLD_BACKGROUND_LIGHT_NAME = "__WORLD_BACKGROUND_LIGHT__"
MISSING_IMAGE_COLOR = [1, 0, 1]
TYPES_SUPPORTING_ENVLIGHTCACHE = {"sky2", "infinite", "constantinfinite"}
TYPES_SUPPORTING_ENVLIGHTCACHE = {"sky2", "sky3", "infinite", "constantinfinite"}

is_blender_5 = bpy.app.version[0] >= 5 # only test of Blender 5 for now

Expand Down Expand Up @@ -242,7 +242,15 @@ def _convert_luxcore_light(exporter, obj, depsgraph, luxcore_scene, transform, i

_define_brightness_and_color(light, definitions)

if light.luxcore.light_type == "sun":
# A sun attached to a Prague Sky world is exported as sun3,
# so sun and sky stay a physically consistent pair
is_sky3_sun = (light.luxcore.light_type == "sun"
and scene.world is not None
and scene.world.luxcore.light == "sky3"
and scene.world.luxcore.sun is not None
and scene.world.luxcore.sun.name == obj.name)

if light.luxcore.light_type == "sun" and not is_sky3_sun:
# sun
definitions["type"] = "sun"
definitions["dir"] = sun_dir
Expand All @@ -254,6 +262,19 @@ def _convert_luxcore_light(exporter, obj, depsgraph, luxcore_scene, transform, i
tint_color = light.luxcore.rgb_gain
for i in range(3):
definitions["gain"][i] *= tint_color[i]
elif light.luxcore.light_type == "sun3" or is_sky3_sun:
# sun3 (Prague Sky Model sun disc)
# Note: sun3 has no color property, the radiance comes from the model.
# RGB tint is applied via the gain.
definitions["type"] = "sun3"
definitions["dir"] = sun_dir
definitions["relsize"] = light.luxcore.relsize
_add_prague_sky_props(definitions, scene)

if light.luxcore.color_mode == "rgb":
tint_color = light.luxcore.rgb_gain
for i in range(3):
definitions["gain"][i] *= tint_color[i]
elif light.luxcore.light_type == "hemi":
# hemi
if light.luxcore.image:
Expand Down Expand Up @@ -541,6 +562,28 @@ def _convert_luxcore_world(exporter, scene, world, is_viewport_render):
for i in range(3):
definitions["gain"][i] *= tint_color[i]

elif light_type == "sky3":
# Prague Sky Model (Wilkie 2021), physically consistent only with a sun3 light
definitions["type"] = "sky3"
definitions["ground.enable"] = world.luxcore.ground_enable
definitions["ground.color"] = list(world.luxcore.ground_color)
_add_prague_sky_props(definitions, scene)

# The atmosphere parameters (visibility/altitude/groundalbedo) and the
# dataset path are additionally emitted as scene.pragueskymodel.*
# (see export/world.py) and per light (see _add_prague_sky_props)

if world.luxcore.sun and world.luxcore.sun.data:
# Use the sun direction so the user does not have to keep two values in sync
definitions["dir"] = _calc_sun_dir(world.luxcore.sun.matrix_world)
if world.luxcore.use_sun_gain_for_sky:
sun = world.luxcore.sun.data
gain, _, _ = _convert_common_props(exporter, scene, sun)
definitions["gain"] = apply_exposure(gain, sun.luxcore.exposure)

for i in range(3):
definitions["gain"][i] *= tint_color[i]

elif light_type == "infinite":
if world.luxcore.image:
transformation = Matrix.Rotation(world.luxcore.rotation, 4, "Z")
Expand Down Expand Up @@ -574,15 +617,39 @@ def _calc_sun_dir(transform):
return [matrix_inv[2][0], matrix_inv[2][1], matrix_inv[2][2]]


def _add_prague_sky_props(definitions, scene):
"""
Add the Prague Sky Model parameters per light.

The scene-level scene.pragueskymodel.* block is only part of the
initial scene export. During live viewport updates, a light is
re-parsed on its own with only its own properties, so without these
per-light fallbacks the atmosphere parameters revert to the LuxCore
defaults and the dataset could not be found at all (the light would
disappear until the viewport render is restarted).
"""
world = scene.world
if world and not world.luxcore.use_cycles_settings and world.luxcore.light == "sky3":
definitions["visibility"] = world.luxcore.sky3_visibility
definitions["altitude"] = world.luxcore.sky3_altitude
definitions["groundalbedo"] = world.luxcore.sky3_groundalbedo

# The dataset path is an addon preference
prefs = utils.get_addon_preferences(bpy.context)
dataset_path = bpy.path.abspath(prefs.skymodel_dataset_path)
if dataset_path:
definitions["dataset"] = dataset_path


def _convert_common_props(exporter, scene, light_or_world):
if isinstance(light_or_world, bpy.types.Light):
if light_or_world.type == "SUN" and light_or_world.luxcore.light_type == "sun":
if light_or_world.type == "SUN" and light_or_world.luxcore.light_type in ("sun", "sun3"):
raw_gain = light_or_world.luxcore.sun_sky_gain
else:
raw_gain = light_or_world.luxcore.gain
else:
# It's a bpy.types.World
if light_or_world.luxcore.light == "sky2":
if light_or_world.luxcore.light in ("sky2", "sky3"):
raw_gain = light_or_world.luxcore.sun_sky_gain
else:
raw_gain = light_or_world.luxcore.gain
Expand Down
52 changes: 52 additions & 0 deletions export/world.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import bpy
import os
import pyluxcore
from .. import utils
from ..utils.node import get_active_output
Expand All @@ -11,6 +13,11 @@ def convert(exporter, depsgraph, scene, is_viewport_render):
props = pyluxcore.Properties()
world = scene.world

# Prague Sky Model atmosphere parameters, shared by all sky3/sun3 lights of the scene
praguesky_props = _convert_pragueskymodel(scene)
if praguesky_props:
props.Set(praguesky_props)

if not world:
return props

Expand All @@ -33,3 +40,48 @@ def convert(exporter, depsgraph, scene, is_viewport_render):
LuxCoreErrorLog.add_warning(msg)

return props


def _needs_pragueskymodel(scene):
"""True if the scene contains sky3/sun3 lights that use the Prague Sky Model."""
world = scene.world
if world and not world.luxcore.use_cycles_settings and world.luxcore.light == "sky3":
return True

for obj in scene.objects:
if (obj.type == "LIGHT" and obj.data and obj.data.type == "SUN"
and obj.data.luxcore.light_type == "sun3"):
return True

return False


def _convert_pragueskymodel(scene):
"""
Export the scene.pragueskymodel.* block with the dataset path and the
atmosphere parameters shared by all sky3/sun3 lights of the scene.
"""
if not _needs_pragueskymodel(scene):
return None

definitions = {}

world = scene.world
if world and not world.luxcore.use_cycles_settings and world.luxcore.light == "sky3":
definitions["visibility"] = world.luxcore.sky3_visibility
definitions["altitude"] = world.luxcore.sky3_altitude
definitions["groundalbedo"] = world.luxcore.sky3_groundalbedo

# The dataset path is an addon preference
prefs = utils.get_addon_preferences(bpy.context)
dataset_path = bpy.path.abspath(prefs.skymodel_dataset_path)
if dataset_path:
if os.path.isfile(dataset_path):
definitions["dataset"] = dataset_path
else:
LuxCoreErrorLog.add_warning(
'Prague Sky Model dataset not found: "%s" '
"(set the correct path in the addon preferences)" % dataset_path)
# If no path is set, LuxCore resolves the default file name "SkyModelDataset.dat"

return utils.luxutils.create_props("scene.pragueskymodel.", definitions)
2 changes: 2 additions & 0 deletions properties/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def update_is_laser(self, context):
("sun", "Sun", "Physically correct sun that emits parallel light rays and changes color with elevation", 0),
("distant", "Distant", "Distant star without atmosphere simulation (emits parallel light)", 1),
("hemi", "Hemi", "180 degree constant light source", 2),
("sun3", "Prague Sun", "Sun disc based on the Prague Sky Model (Wilkie 2021), physically consistent "
"only in combination with the Prague Sky world light", 3),
]
light_type: EnumProperty(name="Sun Type", items=light_types, default="sun")

Expand Down
33 changes: 30 additions & 3 deletions properties/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,32 @@

GROUND_COLOR_DESC = GROUND_ENABLE_DESC

SKY3_VISIBILITY_DESC = (
"Horizontal meteorological range in km.\n"
"Replaces turbidity: 59.4 is moderately clear, "
"131.8 is very clear (closest to turbidity 2.2)"
)

SKY3_ALTITUDE_DESC = (
"Observer altitude in meters above sea level"
)

SKY3_GROUNDALBEDO_DESC = (
"Scalar ground albedo [0, 1] used in the sky simulation. "
"The Prague Sky Model only supports a gray ground albedo"
)


class LuxCoreWorldProps(bpy.types.PropertyGroup):
use_cycles_settings: BoolProperty(name="Use Cycles Settings", default=False)

lights = [
("sky2", "Sky", "Hosek and Wilkie sky model", 0),
("infinite", "HDRI", "High dynamic range image", 1),
("constantinfinite", "Flat Color", "Flat background color", 2),
("none", "None", "No background light", 3),
("sky3", "Prague Sky", "Wilkie 2021 sky model (requires the Prague Sky Model dataset, "
"use together with the Prague Sun)", 1),
("infinite", "HDRI", "High dynamic range image", 2),
("constantinfinite", "Flat Color", "Flat background color", 3),
("none", "None", "No background light", 4),
]
light: EnumProperty(name="Background", items=lights, default="sky2")

Expand Down Expand Up @@ -90,6 +107,16 @@ def poll_sun(self, obj):
min=0, max=1, subtype="COLOR",
description=GROUND_COLOR_DESC)

# sky3 settings (Prague Sky Model, Wilkie 2021)
# Note: the dataset path is an addon preference,
# the atmosphere parameters are exported as scene.pragueskymodel.*
sky3_visibility: FloatProperty(name="Visibility", default=59.4, min=20, max=131.8,
precision=1, description=SKY3_VISIBILITY_DESC)
sky3_altitude: FloatProperty(name="Altitude", default=0, min=0, max=15000,
subtype="NONE", precision=0, description=SKY3_ALTITUDE_DESC)
sky3_groundalbedo: FloatProperty(name="Ground Albedo", default=0.5, min=0, max=1,
description=SKY3_GROUNDALBEDO_DESC)

# infinite
def update_image(self, context):
self.image_user.update(self.image)
Expand Down
1 change: 1 addition & 0 deletions ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
view_layer_aovs.LUXCORE_RENDERLAYER_PT_aovs_render,
world.LUXCORE_PT_context_world,
world.LUXCORE_WORLD_PT_sky2,
world.LUXCORE_WORLD_PT_sky3,
world.LUXCORE_WORLD_PT_infinite,
world.LUXCORE_WORLD_PT_volume,
world.LUXCORE_WORLD_PT_performance,
Expand Down
Loading
Loading