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
4 changes: 3 additions & 1 deletion engine/final.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def render(engine, depsgraph):

_check_halt_conditions(engine, scene)

layer = depsgraph.view_layer_eval
# Use original view_layer instead of evaluated (evaluated doesn't work correctly with indirect_only_get)
view_layer_eval = depsgraph.view_layer_eval
layer = scene.original.view_layers.get(view_layer_eval.name)

print('[Engine/Final] Rendering layer "%s"' % layer.name)

Expand Down
8 changes: 7 additions & 1 deletion engine/viewport.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,14 @@ def view_update(engine, context, depsgraph, changes=None):
print("=" * 50)
print("[Engine/Viewport] New session")
engine.exporter = export.Exporter()

# Get original view_layer for indirect_only and holdout support
view_layer_eval = depsgraph.view_layer_eval
scene_orig = depsgraph.scene_eval.original
view_layer = scene_orig.view_layers.get(view_layer_eval.name)

engine.session = engine.exporter.create_session(
depsgraph, context, engine=engine
depsgraph, context, engine=engine, view_layer=view_layer
)
# Start in separate thread to avoid blocking the UI
engine.starting_session = True
Expand Down
6 changes: 5 additions & 1 deletion export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,12 @@ def _update_scene(self, depsgraph, context, changes, luxcore_scene):
props.Set(self.camera_cache.props)

if changes & Change.OBJECT:
# Get original view_layer from evaluated (evaluated doesn't work correctly with indirect_only_get)
view_layer_eval = depsgraph.view_layer_eval
scene_orig = depsgraph.scene_eval.original
view_layer = scene_orig.view_layers.get(view_layer_eval.name)
self.object_cache2.update(
self, depsgraph, luxcore_scene, props, context
self, depsgraph, luxcore_scene, props, context, view_layer
)

if changes & Change.MATERIAL:
Expand Down
101 changes: 83 additions & 18 deletions export/caches/object_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def get_material(obj, material_index, depsgraph):


def export_material(
obj, material_index, exporter, depsgraph, is_viewport_render
obj, material_index, exporter, depsgraph, is_viewport_render, force_holdout=False
):
mat = get_material(obj, material_index, depsgraph)

Expand All @@ -220,7 +220,7 @@ def export_material(
mat = mat.original

lux_mat_name, mat_props = material.convert(
exporter, depsgraph, mat, is_viewport_render, obj.name
exporter, depsgraph, mat, is_viewport_render, obj.name, force_holdout
)
node_tree = mat.luxcore.node_tree
return lux_mat_name, mat_props, node_tree
Expand Down Expand Up @@ -320,6 +320,24 @@ def first_run(
is_viewport_render = bool(context)
instances = {}

# Hybrid batching: Conditional + Smart
# Check once if scene uses indirect_only/holdout to avoid per-instance overhead
scene_uses_indirect_or_holdout = False
if view_layer:
def check_layer_coll(lc):
if lc.indirect_only or lc.holdout:
return True
for child in lc.children:
if check_layer_coll(child):
return True
return False
scene_uses_indirect_or_holdout = check_layer_coll(view_layer.layer_collection)

if scene_uses_indirect_or_holdout:
print(f"[Export] Hybrid batching: Scene uses indirect_only/holdout - full smart batching")
else:
print(f"[Export] Hybrid batching: Scene clean - fast batching")

if engine:
obj_count_estimate = max(1, get_obj_count_estimate(depsgraph))
else:
Expand All @@ -340,6 +358,7 @@ def first_run(
)
)
and obj.type in MESH_OBJECTS
# Smart batching: always allow batching, group by (mesh, visibility) later
):
# This code is optimized for large amounts of duplis. Drawback is that objects generated from this
# code can't be transformed later in a viewport render session (due to BlendLuxCore implementation
Expand All @@ -351,10 +370,31 @@ def first_run(
engine, obj.name, " (dupli)", index, obj_count_estimate
)

# Hybrid batching: Conditional + Smart
if scene_uses_indirect_or_holdout:
# Full smart batching: Calculate visibility and holdout per instance
instance_visible = utils.visible_to_camera(dg_obj_instance, is_viewport_render, view_layer)

# Holdout overrides indirect_only
check_obj = dg_obj_instance.parent if dg_obj_instance.is_instance else obj
is_holdout = utils.is_holdout_object(check_obj.original, view_layer)

if is_holdout:
instance_visible = True # Holdout needs to be visible to camera to cut hole

# Key: (mesh_pointer, camerainvisible, is_holdout)
# Must distinguish holdout vs normal visible - they need different materials!
camerainvisible = not instance_visible
batch_key = (obj.original.as_pointer(), camerainvisible, is_holdout)
else:
# Fast batching: Simple key without per-instance overhead
# All instances assumed visible, no holdout/indirect_only checks
batch_key = (obj.original.as_pointer(), False, False) # (mesh, camerainvisible=False, is_holdout=False)

try:
# The code in this try block is performance-critical, as it is
# executed most often when exporting millions of instances.
duplis = instances[obj.original.as_pointer()]
duplis = instances[batch_key]
# If duplis is None, then a non-exportable object like a curve with zero faces is being duplicated
if duplis:
obj_id = dg_obj_instance.object.original.luxcore.id
Expand Down Expand Up @@ -393,12 +433,13 @@ def first_run(
if exported_obj:
# Note, the transformation matrix and object ID of this first instance is not added
# to the duplication list, since it already exists in the scene
instances[obj.original.as_pointer()] = Duplis(
# Smart batching: Store by (mesh, visibility) key
instances[batch_key] = Duplis(
exported_obj
)
else:
# Could not export the object, happens e.g. with curve objects with zero faces
instances[obj.original.as_pointer()] = None
instances[batch_key] = None
else:
# This code is for singular objects and for duplis that should be movable later in a viewport render
if not utils.is_instance_visible(
Expand Down Expand Up @@ -542,8 +583,10 @@ def _convert_obj(

self.exported_hair[obj_key] = lux_shape
if lux_shape:
# Check if object is in holdout layer collection
force_holdout = utils.is_holdout_object(obj.original, view_layer)
lux_mat, mat_props, node_tree = export_material(
obj, 0, exporter, depsgraph, is_viewport_render
obj, 0, exporter, depsgraph, is_viewport_render, force_holdout
)
scene_props.Set(mat_props)
set_hair_props(
Expand Down Expand Up @@ -645,8 +688,10 @@ def _convert_obj(
self.exported_hair[psys_key] = lux_shape

if lux_shape:
# Check if object is in holdout layer collection
force_holdout = utils.is_holdout_object(obj.original, view_layer)
lux_mat, mat_props, node_tree = export_material(
obj, mat_index, exporter, depsgraph, is_viewport_render
obj, mat_index, exporter, depsgraph, is_viewport_render, force_holdout
)
scene_props.Set(mat_props)
set_hair_props(
Expand Down Expand Up @@ -720,13 +765,18 @@ def _convert_mesh_obj(
loaded_from_cache = False

if exported_mesh:
# Check if object is in holdout layer collection (like Cycles)
# For instances, check the parent object (similar to visible_to_camera logic)
check_obj = dg_obj_instance.parent if dg_obj_instance.is_instance else obj
force_holdout = utils.is_holdout_object(check_obj.original, view_layer)

mat_names = []
for idx, (shape_name, mat_index) in enumerate(
exported_mesh.mesh_definitions
):
shape = shape_name
lux_mat_name, mat_props, node_tree = export_material(
obj, mat_index, exporter, depsgraph, is_viewport_render
obj, mat_index, exporter, depsgraph, is_viewport_render, force_holdout
)
scene_props.Set(mat_props)
mat_names.append(lux_mat_name)
Expand All @@ -744,14 +794,21 @@ def _convert_mesh_obj(
obj_transform = transform.copy() if use_instancing else None
obj_id = utils.make_object_id(dg_obj_instance)

visible = utils.visible_to_camera(
dg_obj_instance, is_viewport_render, view_layer
)

# Holdout overrides indirect_only - holdout needs object to be visible to camera
# to "cut a hole" in the film. In reflections/GI it will still be visible normally.
if force_holdout:
visible = True

return ExportedObject(
obj_key,
exported_mesh.mesh_definitions,
mat_names,
obj_transform,
utils.visible_to_camera(
dg_obj_instance, is_viewport_render, view_layer
),
visible,
obj_id,
)

Expand All @@ -761,7 +818,9 @@ def diff(self, depsgraph):
)
return depsgraph.id_type_updated("OBJECT") and not only_scene

def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):
def update(self, exporter, depsgraph, luxcore_scene, scene_props, context, view_layer=None):
if view_layer is None:
view_layer = depsgraph.view_layer_eval
is_viewport_render = bool(context)
redefine_objs_with_these_mesh_keys = []
# Always instance in viewport so we can move objects around
Expand Down Expand Up @@ -898,12 +957,17 @@ def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):
exported_obj.obj_id = obj_id
updated = True

if exported_obj.visible_to_camera != utils.visible_to_camera(
dg_obj_instance, is_viewport_render
):
exported_obj.visible_to_camera = utils.visible_to_camera(
dg_obj_instance, is_viewport_render
)
visible = utils.visible_to_camera(
dg_obj_instance, is_viewport_render, view_layer
)

# Holdout overrides indirect_only
check_obj = dg_obj_instance.parent if dg_obj_instance.is_instance else dg_obj_instance.object
if utils.is_holdout_object(check_obj.original, view_layer):
visible = True

if exported_obj.visible_to_camera != visible:
exported_obj.visible_to_camera = visible
updated = True

if updated:
Expand All @@ -918,6 +982,7 @@ def update(self, exporter, depsgraph, luxcore_scene, scene_props, context):
luxcore_scene,
scene_props,
is_viewport_render,
view_layer,
)

# self._debug_info()
36 changes: 27 additions & 9 deletions export/cycles_node_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,48 @@
}


def convert(material, props, luxcore_name, obj_name=""):
def convert(material, props, luxcore_name, obj_name="", force_holdout=False):
# print("Converting Cycles node tree of material", material.name_full)
# Note: luxcore_name already has "_holdout" suffix if force_holdout=True (added in material.py)

output = material.node_tree.get_output_node("CYCLES")
if output is None:
return black(luxcore_name)
return black(luxcore_name, force_holdout)

link = utils_node.get_link(output.inputs["Surface"])
if link is None:
return black(luxcore_name)
return black(luxcore_name, force_holdout)

result = _node(link.from_node, link.from_socket, props, material, luxcore_name, obj_name)
if result == ERROR_VALUE:
return black(luxcore_name)
return black(luxcore_name, force_holdout)

assert result == luxcore_name

# Override: If force_holdout, set holdout.enable flag
if force_holdout:
prefix = "scene.materials." + luxcore_name + "."
import pyluxcore
props.Set(pyluxcore.Property(prefix + "holdout.enable", True))

return luxcore_name, props


def black(luxcore_name="__BLACK__"):
def black(luxcore_name="__BLACK__", force_holdout=False):
props = pyluxcore.Properties()
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0
""".format(mat_name=luxcore_name))

if force_holdout:
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0
scene.materials.{mat_name}.holdout.enable = true
""".format(mat_name=luxcore_name))
else:
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0
""".format(mat_name=luxcore_name))

return luxcore_name, props


Expand Down
46 changes: 36 additions & 10 deletions export/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@

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

def convert(exporter, depsgraph, material, is_viewport_render, obj_name=""):
def convert(exporter, depsgraph, material, is_viewport_render, obj_name="", force_holdout=False):
try:
if material is None:
return fallback()

props = pyluxcore.Properties()
luxcore_name = utils.get_luxcore_name(material, is_viewport_render)

# If force_holdout, append suffix to create variant material
if force_holdout:
luxcore_name += "_holdout"
node_tree = material.luxcore.node_tree

# Try to use Cycles nodes on assets without LuxCore nodes, so the user doesn't have to
Expand All @@ -32,17 +36,17 @@ def convert(exporter, depsgraph, material, is_viewport_render, obj_name=""):
matusenodes = material.use_nodes

if matusenodes and (material.luxcore.use_cycles_nodes or is_asset_without_lux_mat):
return cycles_node_reader.convert(material, props, luxcore_name, obj_name)
return cycles_node_reader.convert(material, props, luxcore_name, obj_name, force_holdout)

if node_tree is None:
LuxCoreErrorLog.add_warning(f'Material "{material.name}": Missing node tree', obj_name=obj_name)
return fallback(luxcore_name)
return fallback(luxcore_name, force_holdout)

active_output = get_active_output(node_tree)

if active_output is None:
LuxCoreErrorLog.add_warning(f'Node tree "{node_tree.name}": Missing active output node', obj_name=obj_name)
return fallback(luxcore_name)
return fallback(luxcore_name, force_holdout)

if _has_volumes_and_transparency(node_tree, active_output):
msg = f'Material "{material.name}": Combining volumes and materials with opacity < 1 can lead to artifacts!'
Expand All @@ -51,6 +55,17 @@ def convert(exporter, depsgraph, material, is_viewport_render, obj_name=""):
# Now export the material node tree, starting at the output node
active_output.export(exporter, depsgraph, props, luxcore_name)

# Override: If force_holdout, set holdout.enable flag
# Similar to how Cycles respects LayerCollection.holdout
if force_holdout:
import sys
sys.stderr.write(f"[MAT HOLDOUT] Setting holdout for {luxcore_name}\n")
sys.stderr.flush()
prefix = "scene.materials." + luxcore_name + "."
props.Set(pyluxcore.Property(prefix + "holdout.enable", True))
sys.stderr.write(f"[MAT HOLDOUT] Props after Set: {props.GetSize()}\n")
sys.stderr.flush()

return luxcore_name, props
except Exception as error:
msg = f'Material "{material.name}": {error}'
Expand All @@ -60,13 +75,24 @@ def convert(exporter, depsgraph, material, is_viewport_render, obj_name=""):
return fallback()


def fallback(luxcore_name=GLOBAL_FALLBACK_MAT):
def fallback(luxcore_name=GLOBAL_FALLBACK_MAT, force_holdout=False):
props = pyluxcore.Properties()
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0.5
""".format(mat_name=luxcore_name))
return luxcore_name, props

if force_holdout:
# If holdout is forced, create holdout material instead of matte
luxcore_name_with_suffix = luxcore_name if luxcore_name == GLOBAL_FALLBACK_MAT else luxcore_name + "_holdout"
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0.5
scene.materials.{mat_name}.holdout.enable = true
""".format(mat_name=luxcore_name_with_suffix))
return luxcore_name_with_suffix, props
else:
props.SetFromString("""
scene.materials.{mat_name}.type = matte
scene.materials.{mat_name}.kd = 0.5
""".format(mat_name=luxcore_name))
return luxcore_name, props


def _has_volumes_and_transparency(node_tree, active_output):
Expand Down
Loading