1- import threading
21from time import time , sleep
3- from concurrent .futures import ThreadPoolExecutor
42from .. import export , utils
53from ..draw .final import FrameBufferFinal
64from ..utils import render as utils_render
@@ -25,37 +23,32 @@ def render(engine, depsgraph):
2523
2624 _check_halt_conditions (engine , scene )
2725
28- # Using ThreadPoolExecutor to manage threads efficiently
29- with ThreadPoolExecutor (max_workers = 4 ) as executor : # Limit the number of concurrent threads
30- futures = []
31- for layer_index , layer in enumerate (scene .view_layers ):
32- if layer .use :
33- futures .append (executor .submit (_render_layer_thread , engine , depsgraph , statistics , layer ))
26+ for layer_index , layer in enumerate (scene .view_layers ):
27+ print ('[Engine/Final] Rendering layer "%s"' % layer .name )
3428
35- for future in futures :
36- future .result () # Wait for all threads to finish
29+ dummy_result = engine .begin_result (0 , 0 , 1 , 1 , layer = layer .name )
3730
31+ # Check if the layer is disabled. Cycles does this the same way,
32+ # to be honest I have no idea why they don't just check layer.use
33+ if layer .name not in dummy_result .layers :
34+ # The layer is disabled
35+ engine .end_result (dummy_result , cancel = True , do_merge_results = False )
36+ continue
3837
39- def _render_layer_thread (engine , depsgraph , statistics , layer ):
40- """
41- A thread-specific function to render a single layer.
42- """
43- print (f'[Engine/Final] Rendering layer "{ layer .name } "' )
44-
45- dummy_result = engine .begin_result (0 , 0 , 1 , 1 , layer = layer .name )
46-
47- if layer .name not in dummy_result .layers :
4838 engine .end_result (dummy_result , cancel = True , do_merge_results = False )
49- return
5039
51- engine .end_result (dummy_result , cancel = True , do_merge_results = False )
40+ # This property is used during export, e.g. to check for layer visibility
41+ utils_view_layer .State .active_view_layer = layer .name
5242
53- # This property is used during export, e.g. to check for layer visibility
54- utils_view_layer . State . active_view_layer = layer . name
43+ _add_passes ( engine , layer , scene )
44+ _render_layer ( engine , depsgraph , statistics , layer )
5545
56- _add_passes (engine , layer , depsgraph .scene )
57- _render_layer (engine , depsgraph , statistics , layer )
46+ if _stop_requested (engine ):
47+ # Blender skips the rest of the render layers anyway
48+ return
5849
50+ print ('[Engine/Final] Finished rendering layer "%s"' % layer .name )
51+
5952
6053def _render_layer (engine , depsgraph , statistics , view_layer ):
6154 engine .reset ()
@@ -72,7 +65,7 @@ def _render_layer(engine, depsgraph, statistics, view_layer):
7265
7366 # Create session
7467 start = time ()
75- engine .session .Start () # Start session only once
68+ engine .session .Start ()
7669 session_init_time = time () - start
7770 print ("Session started in %.1f s" % session_init_time )
7871 statistics .session_init_time .value = session_init_time
@@ -125,6 +118,7 @@ def _render_layer(engine, depsgraph, statistics, view_layer):
125118 if engine .session .IsInPause ():
126119 engine .session .Resume ()
127120
121+ # Do session update (imagepipeline, lightgroups)
128122 changes = engine .exporter .get_changes (depsgraph )
129123 engine .exporter .update_session (changes , engine .session )
130124
@@ -133,42 +127,61 @@ def _render_layer(engine, depsgraph, statistics, view_layer):
133127 engine .framebuffer .draw (engine , engine .session , depsgraph .scene , render_stopped = False )
134128 else :
135129 if fast_refresh or update_stats or changes or manual_refresh_requested or (time_until_film_refresh <= 0 ):
130+ # We have to check the stats often to see if a halt condition is met
131+ # But film drawing is expensive, so we don't do it every time we check stats
136132 draw_film = fast_refresh or (time_until_film_refresh <= 0 )
133+
134+ # Refresh quickly when user changed something or requested a refresh via button
137135 draw_film |= changes or manual_refresh_requested
138136
139137 stats = utils_render .update_stats (engine .session )
140138 if draw_film :
141139 time_until_film_refresh = 0
142140 utils_render .update_status_msg (stats , engine , depsgraph .scene , config , time_until_film_refresh )
143141
142+ # Check if the user cancelled during the expensive stats update
144143 if _stop_requested (engine ) or engine .session .HasDone ():
145144 break
146145
147146 last_stat_refresh = now
148147 if draw_film :
148+ # Show updated film (this operation is expensive)
149149 engine .framebuffer .draw (engine , engine .session , depsgraph .scene , render_stopped = False )
150150 last_film_refresh = now
151151
152152 utils_render .update_status_msg (stats , engine , depsgraph .scene , config , time_until_film_refresh )
153153
154+ # Compute and print the optimal clamp value. Done only once after a warmup phase.
155+ # Only do this if clamping is disabled, otherwise the value is meaningless.
154156 samples = stats .Get ("stats.renderengine.pass" ).GetInt ()
155157 if not checked_optimal_clamp and samples > clamp_warmup_samples :
156158 clamp_value = utils_render .find_suggested_clamp_value (engine .session , depsgraph .scene )
157159 print ("Recommended clamp value:" , clamp_value )
158160 checked_optimal_clamp = True
159161
162+ # Check before we sleep
160163 if _stop_requested (engine ):
161164 break
162165
163- sleep (1 / 5 ) # Reduce refresh rate for better performance
166+ # Don't use up too much CPU time for this refresh loop, but stay responsive
167+ # Note: The engine Python code seems to be threaded by Blender,
168+ # so the interface would not even hang if we slept for minutes here
169+ sleep (1 / 5 )
164170
171+ # Check after we slept, before the next possible expensive operation
172+ if _stop_requested (engine ):
173+ break
174+
175+ # User wants to stop or halt condition is reached
176+ # Update stats to refresh film and draw the final result
165177 stats = utils_render .update_stats (engine .session )
166178 utils_render .update_status_msg (stats , engine , depsgraph .scene , config , time_until_film_refresh = 0 )
167179 engine .framebuffer .draw (engine , engine .session , depsgraph .scene , render_stopped = True )
168180 engine .update_stats ("Render" , "Stopping session..." )
169181 if engine .session .IsInPause ():
170182 engine .session .Resume ()
171183 engine .session .Stop ()
184+ # Clean up
172185 del engine .session
173186 engine .session = None
174187
@@ -197,9 +210,11 @@ def _check_halt_conditions(engine, scene):
197210 is_halt_enabled = True
198211
199212 if len (enabled_layers ) > 1 :
213+ # When we have multiple render layers, we need a halt condition for each one
200214 for layer in enabled_layers :
201215 layer_halt = layer .luxcore .halt
202216 if layer_halt .enable :
217+ # The layer overrides the global halt conditions
203218 has_halt_condition = layer_halt .is_enabled ()
204219 is_halt_enabled &= has_halt_condition
205220
@@ -208,6 +223,7 @@ def _check_halt_conditions(engine, scene):
208223 else :
209224 is_halt_enabled = False
210225
226+ # Global halt conditions
211227 if not is_halt_enabled :
212228 is_halt_enabled = scene .luxcore .halt .is_enabled ()
213229
@@ -216,19 +232,29 @@ def _check_halt_conditions(engine, scene):
216232
217233
218234def _add_passes (engine , layer , scene ):
235+ """
236+ Add our custom passes.
237+ Called by engine.final.render() before the render starts.
238+ layer is the current render layer.
239+ """
219240 aovs = layer .luxcore .aovs
241+
242+ # Denoiser
220243 if scene .luxcore .denoiser .enabled :
221244 transparent = scene .camera .data .luxcore .imagepipeline .transparent_film
222245 if transparent :
223246 engine .add_pass ("DENOISED" , 4 , "RGBA" , layer = layer .name )
224247 else :
225248 engine .add_pass ("DENOISED" , 3 , "RGB" , layer = layer .name )
249+
226250 if aovs .rgb :
227251 engine .add_pass ("RGB" , 3 , "RGB" , layer = layer .name )
228252 if aovs .rgba :
229253 engine .add_pass ("RGBA" , 4 , "RGBA" , layer = layer .name )
230254 if aovs .alpha :
231255 engine .add_pass ("ALPHA" , 1 , "A" , layer = layer .name )
256+ # Note: If the Depth pass is already added by Blender and we add it again, it won't be
257+ # displayed correctly in the "Depth" view mode of the "Combined" pass in the image editor.
232258 if aovs .depth and not layer .use_pass_z :
233259 engine .add_pass ("Depth" , 1 , "Z" , layer = layer .name )
234260 if aovs .albedo :
@@ -245,7 +271,66 @@ def _add_passes(engine, layer, scene):
245271 engine .add_pass ("CAUSTIC" , 3 , "RGB" , layer = layer .name )
246272 if aovs .direct_diffuse :
247273 engine .add_pass ("DIRECT_DIFFUSE" , 3 , "RGB" , layer = layer .name )
274+ if aovs .direct_diffuse_reflect :
275+ engine .add_pass ("DIRECT_DIFFUSE_REFLECT" , 3 , "RGB" , layer = layer .name )
276+ if aovs .direct_diffuse_transmit :
277+ engine .add_pass ("DIRECT_DIFFUSE_TRANSMIT" , 3 , "RGB" , layer = layer .name )
248278 if aovs .direct_glossy :
249279 engine .add_pass ("DIRECT_GLOSSY" , 3 , "RGB" , layer = layer .name )
280+ if aovs .direct_glossy_reflect :
281+ engine .add_pass ("DIRECT_GLOSSY_REFLECT" , 3 , "RGB" , layer = layer .name )
282+ if aovs .direct_glossy_transmit :
283+ engine .add_pass ("DIRECT_GLOSSY_TRANSMIT" , 3 , "RGB" , layer = layer .name )
250284 if aovs .indirect_diffuse :
251285 engine .add_pass ("INDIRECT_DIFFUSE" , 3 , "RGB" , layer = layer .name )
286+ if aovs .indirect_diffuse_reflect :
287+ engine .add_pass ("INDIRECT_DIFFUSE_REFLECT" , 3 , "RGB" , layer = layer .name )
288+ if aovs .indirect_diffuse_transmit :
289+ engine .add_pass ("INDIRECT_DIFFUSE_TRANSMIT" , 3 , "RGB" , layer = layer .name )
290+ if aovs .indirect_glossy :
291+ engine .add_pass ("INDIRECT_GLOSSY" , 3 , "RGB" , layer = layer .name )
292+ if aovs .indirect_glossy_reflect :
293+ engine .add_pass ("INDIRECT_GLOSSY_REFLECT" , 3 , "RGB" , layer = layer .name )
294+ if aovs .indirect_glossy_transmit :
295+ engine .add_pass ("INDIRECT_GLOSSY_TRANSMIT" , 3 , "RGB" , layer = layer .name )
296+ if aovs .indirect_specular :
297+ engine .add_pass ("INDIRECT_SPECULAR" , 3 , "RGB" , layer = layer .name )
298+ if aovs .indirect_specular_reflect :
299+ engine .add_pass ("INDIRECT_SPECULAR_REFLECT" , 3 , "RGB" , layer = layer .name )
300+ if aovs .indirect_specular_transmit :
301+ engine .add_pass ("INDIRECT_SPECULAR_TRANSMIT" , 3 , "RGB" , layer = layer .name )
302+ if aovs .position :
303+ engine .add_pass ("POSITION" , 3 , "XYZ" , layer = layer .name )
304+ if aovs .shading_normal :
305+ engine .add_pass ("SHADING_NORMAL" , 3 , "XYZ" , layer = layer .name )
306+ if aovs .avg_shading_normal :
307+ engine .add_pass ("AVG_SHADING_NORMAL" , 3 , "XYZ" , layer = layer .name )
308+ if aovs .geometry_normal :
309+ engine .add_pass ("GEOMETRY_NORMAL" , 3 , "XYZ" , layer = layer .name )
310+ if aovs .uv :
311+ # We need to pad the UV pass to 3 elements (Blender can't handle 2 elements)
312+ engine .add_pass ("UV" , 3 , "UVA" , layer = layer .name )
313+ if aovs .direct_shadow_mask :
314+ engine .add_pass ("DIRECT_SHADOW_MASK" , 1 , "X" , layer = layer .name )
315+ if aovs .indirect_shadow_mask :
316+ engine .add_pass ("INDIRECT_SHADOW_MASK" , 1 , "X" , layer = layer .name )
317+ if aovs .raycount :
318+ engine .add_pass ("RAYCOUNT" , 1 , "X" , layer = layer .name )
319+ if aovs .samplecount :
320+ engine .add_pass ("SAMPLECOUNT" , 1 , "X" , layer = layer .name )
321+ if aovs .convergence :
322+ engine .add_pass ("CONVERGENCE" , 1 , "X" , layer = layer .name )
323+ if aovs .noise :
324+ engine .add_pass ("NOISE" , 1 , "X" , layer = layer .name )
325+ if aovs .irradiance :
326+ engine .add_pass ("IRRADIANCE" , 3 , "RGB" , layer = layer .name )
327+
328+ # Light groups
329+ lightgroups = scene .luxcore .lightgroups
330+ lightgroup_pass_names = lightgroups .get_pass_names ()
331+ default_group_name = lightgroups .get_lightgroup_pass_name (is_default_group = True )
332+ # If only the default group is in the list, it doesn't make sense to show lightgroups
333+ # Note: this behaviour has to be the same as in the update_render_passes() method of the RenderEngine class
334+ if lightgroup_pass_names != [default_group_name ]:
335+ for name in lightgroup_pass_names :
336+ engine .add_pass (name , 3 , "RGB" , layer = layer .name )
0 commit comments