Skip to content

Commit 7edc090

Browse files
committed
Nodes: Introduce custom shader nodes for Mitsuba BSDFs
1 parent 424f4d2 commit 7edc090

File tree

118 files changed

+4491
-2002
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+4491
-2002
lines changed

.github/workflows/test.yml

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515

1616
# Workflow steps
1717
jobs:
18-
build:
18+
test:
1919
name: "${{ matrix.environment.os }} | Blender ${{ matrix.blender.version }}"
2020
runs-on: ${{ matrix.environment.os }}
2121
strategy:
@@ -81,4 +81,11 @@ jobs:
8181
run: |
8282
BLENDER_EXECUTABLE=$(find blender/ -maxdepth 1 -regextype posix-extended -regex '.*blender(.exe)?' -print -quit)
8383
echo "Blender Executable is $BLENDER_EXECUTABLE"
84-
./$BLENDER_EXECUTABLE -b -noaudio --factory-startup --python scripts/run_tests.py -- -v --cov=mitsuba-blender
84+
./$BLENDER_EXECUTABLE -b -noaudio --factory-startup --python scripts/run_tests.py -- -v --cov=mitsuba-blender --local-tmp
85+
86+
- name: Recover crash logs
87+
if: ${{ failure() }}
88+
uses: actions/upload-artifact@v3
89+
with:
90+
name: "crash-logs-${{ matrix.environment.os }}-blender-${{ matrix.blender.version }}"
91+
path: tmp/*crash*.txt

mitsuba-blender/__init__.py

+144-131
Large diffs are not rendered by default.

mitsuba-blender/engine/final.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
import tempfile
33
import os
44
import numpy as np
5-
from ..io.exporter import SceneConverter
5+
from ..exporter import SceneConverter
66

77
class MitsubaRenderEngine(bpy.types.RenderEngine):
88

99
bl_idname = "MITSUBA"
1010
bl_label = "Mitsuba"
1111
bl_use_preview = False
12+
bl_use_texture_preview = False
13+
# Hide Cycles shader nodes in the shading menu
14+
bl_use_shading_nodes_custom = False
15+
# FIXME: This is used to get a visual feedback of the shapes,
16+
# it does not produce a correct result.
17+
bl_use_eevee_viewport = True
1218

1319
# Init is called whenever a new render engine instance is created. Multiple
1420
# instances may exist at the same time, for example for a viewport and final

mitsuba-blender/engine/properties.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@ class MITSUBA_CAMERA_PT_sampler(bpy.types.Panel):
397397
bl_space_type = 'PROPERTIES'
398398
bl_region_type = 'WINDOW'
399399
bl_context = 'render'
400+
COMPAT_ENGINES = {'MITSUBA'}
401+
402+
@classmethod
403+
def poll(cls, context):
404+
return context.engine in cls.COMPAT_ENGINES
405+
400406
def draw(self, context):
401407
layout = self.layout
402408
if hasattr(context.scene.camera, 'data'):
@@ -410,14 +416,20 @@ class MITSUBA_CAMERA_PT_rfilter(bpy.types.Panel):
410416
bl_space_type = 'PROPERTIES'
411417
bl_region_type = 'WINDOW'
412418
bl_context = 'render'
419+
COMPAT_ENGINES = {'MITSUBA'}
420+
421+
@classmethod
422+
def poll(cls, context):
423+
return context.engine in cls.COMPAT_ENGINES
424+
413425
def draw(self, context):
414426
layout = self.layout
415427
if hasattr(context.scene.camera, 'data'):
416428
cam_settings = context.scene.camera.data.mitsuba
417429
layout.prop(cam_settings, "active_rfilter", text="Filter")
418430
getattr(cam_settings.rfilters, cam_settings.active_rfilter).draw(layout)
419431

420-
def draw_device(self, context):
432+
def mitsuba_render_draw(self, context):
421433
scene = context.scene
422434
layout = self.layout
423435
layout.use_property_split = True
@@ -429,18 +441,20 @@ def draw_device(self, context):
429441
col = layout.column()
430442
col.prop(mts_settings, "variant")
431443

432-
def register():
433-
bpy.types.RENDER_PT_context.append(draw_device)
434-
bpy.utils.register_class(MitsubaRenderSettings)
435-
bpy.utils.register_class(MitsubaCameraSettings)
436-
bpy.utils.register_class(MITSUBA_RENDER_PT_integrator)
437-
bpy.utils.register_class(MITSUBA_CAMERA_PT_sampler)
438-
bpy.utils.register_class(MITSUBA_CAMERA_PT_rfilter)
444+
classes = [
445+
MitsubaRenderSettings,
446+
MitsubaCameraSettings,
447+
MITSUBA_RENDER_PT_integrator,
448+
MITSUBA_CAMERA_PT_sampler,
449+
MITSUBA_CAMERA_PT_rfilter,
450+
]
439451

452+
def register():
453+
bpy.types.RENDER_PT_context.append(mitsuba_render_draw)
454+
for cls in classes:
455+
bpy.utils.register_class(cls)
456+
440457
def unregister():
441-
bpy.types.RENDER_PT_context.remove(draw_device)
442-
bpy.utils.unregister_class(MitsubaRenderSettings)
443-
bpy.utils.unregister_class(MitsubaCameraSettings)
444-
bpy.utils.unregister_class(MITSUBA_RENDER_PT_integrator)
445-
bpy.utils.unregister_class(MITSUBA_CAMERA_PT_sampler)
446-
bpy.utils.unregister_class(MITSUBA_CAMERA_PT_rfilter)
458+
for cls in classes:
459+
bpy.utils.unregister_class(cls)
460+
bpy.types.RENDER_PT_context.remove(mitsuba_render_draw)

mitsuba-blender/io/exporter/__init__.py renamed to mitsuba-blender/exporter/__init__.py

+1-14
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
import os
22

3-
if "bpy" in locals():
4-
import importlib
5-
if "export_context" in locals():
6-
importlib.reload(export_context)
7-
if "materials" in locals():
8-
importlib.reload(materials)
9-
if "geometry" in locals():
10-
importlib.reload(geometry)
11-
if "lights" in locals():
12-
importlib.reload(lights)
13-
if "camera" in locals():
14-
importlib.reload(camera)
15-
163
import bpy
174

185
from . import export_context
@@ -53,7 +40,7 @@ def scene_to_dict(self, depsgraph, window_manager):
5340

5441
b_scene = depsgraph.scene #TODO: what if there are multiple scenes?
5542
if b_scene.render.engine == 'MITSUBA':
56-
integrator = getattr(b_scene.mitsuba.available_integrators,b_scene.mitsuba.active_integrator).to_dict()
43+
integrator = getattr(b_scene.mitsuba.available_integrators, b_scene.mitsuba.active_integrator).to_dict()
5744
else:
5845
integrator = {
5946
'type':'path',

mitsuba-blender/io/exporter/geometry.py renamed to mitsuba-blender/exporter/geometry.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ def export_object(deg_instance, export_ctx, is_particle):
101101
mat_nr)
102102
if mts_mesh is not None and mts_mesh.face_count() > 0:
103103
converted_parts.append((mat_nr, mts_mesh))
104-
export_material(export_ctx, b_mesh.materials[mat_nr])
104+
b_mat = b_mesh.materials[mat_nr]
105+
export_material(export_ctx, b_mat)
105106

106107
if b_object.type != 'MESH':
107108
b_object.to_mesh_clear()

mitsuba-blender/io/exporter/materials.py renamed to mitsuba-blender/exporter/materials.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
from mathutils import Matrix
3+
from ..utils.nodetree import get_active_output
34
from .export_context import Files
45

56
RoughnessMode = {'GGX': 'ggx', 'BECKMANN': 'beckmann', 'ASHIKHMIN_SHIRLEY':'beckmann', 'MULTI_GGX':'ggx'}
@@ -340,22 +341,37 @@ def get_dummy_material(export_ctx):
340341

341342
def b_material_to_dict(export_ctx, b_mat):
342343
''' Converting one material from Blender / Cycles to Mitsuba'''
344+
# NOTE: The evaluated material does not keep references to Mitsuba node trees.
345+
# We need to use the original material instead.
346+
original_mat = b_mat.original
343347

344348
mat_params = {}
345349

346-
if b_mat.use_nodes:
350+
if original_mat.mitsuba.node_tree is not None:
351+
output_node = get_active_output(original_mat.mitsuba.node_tree)
352+
if output_node is not None:
353+
mat_params = output_node.to_dict(export_ctx)
354+
else:
355+
export_ctx.log(f'Material {b_mat.name} does not have an output node.', 'ERROR')
356+
357+
elif b_mat.use_nodes:
347358
try:
348359
output_node_id = 'Material Output'
349360
if output_node_id in b_mat.node_tree.nodes:
350361
output_node = b_mat.node_tree.nodes[output_node_id]
351-
surface_node = output_node.inputs["Surface"].links[0].from_node
352-
mat_params = cycles_material_to_dict(export_ctx, surface_node)
362+
if len(output_node.inputs['Surface'].links) > 0:
363+
surface_node = output_node.inputs["Surface"].links[0].from_node
364+
mat_params = cycles_material_to_dict(export_ctx, surface_node)
365+
else:
366+
export_ctx.log(f'Export of material {b_mat.name} failed: Output node is not connected. Exporting a dummy material instead.', 'WARN')
367+
mat_params = get_dummy_material(export_ctx)
353368
else:
354369
export_ctx.log(f'Export of material {b_mat.name} failed: Cannot find material output node. Exporting a dummy material instead.', 'WARN')
355370
mat_params = get_dummy_material(export_ctx)
356371
except NotImplementedError as e:
357372
export_ctx.log(f'Export of material \'{b_mat.name}\' failed: {e.args[0]}. Exporting a dummy material instead.', 'WARN')
358373
mat_params = get_dummy_material(export_ctx)
374+
359375
else:
360376
mat_params = {'type':'diffuse'}
361377
mat_params['reflectance'] = export_ctx.spectrum(b_mat.diffuse_color)
@@ -500,6 +516,7 @@ def convert_world(export_ctx, world, ignore_background):
500516
'type': 'constant',
501517
'radiance': export_ctx.spectrum(radiance)
502518
})
519+
503520
else:
504521
raise NotImplementedError("Only Background and Emission nodes are supported as final nodes for World export, got '%s'" % surface_node.name)
505522
else:

mitsuba-blender/io/importer/__init__.py renamed to mitsuba-blender/importer/__init__.py

+21-35
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
import time
22

3-
if "bpy" in locals():
4-
import importlib
5-
if "common" in locals():
6-
importlib.reload(common)
7-
if "materials" in locals():
8-
importlib.reload(materials)
9-
if "shapes" in locals():
10-
importlib.reload(shapes)
11-
if "cameras" in locals():
12-
importlib.reload(sensors)
13-
if "emitters" in locals():
14-
importlib.reload(emitters)
15-
if "world" in locals():
16-
importlib.reload(world)
17-
if "textures" in locals():
18-
importlib.reload(textures)
19-
if "renderer" in locals():
20-
importlib.reload(renderer)
21-
if "mi_props_utils" in locals():
22-
importlib.reload(mi_props_utils)
23-
243
import bpy
254

265
from . import common
@@ -30,7 +9,10 @@
309
from . import sensors
3110
from . import world
3211
from . import textures
33-
from . import renderer
12+
from . import integrators
13+
from . import rfilters
14+
from . import samplers
15+
from . import films
3416
from . import mi_props_utils
3517

3618
########################
@@ -50,6 +32,15 @@ def _convert_named_references(mi_context, mi_props, parent_node, type_filter=[])
5032
if child_node is not None:
5133
parent_node.add_child(child_node)
5234

35+
def _init_mitsuba_renderer(mi_context):
36+
mi_context.bl_scene.render.engine = 'MITSUBA'
37+
mi_renderer = mi_context.bl_scene.mitsuba
38+
if 'scalar_rgb' not in mi_renderer.variants():
39+
mi_context.log('Mitsuba variant "scalar_rgb" not available.', 'ERROR')
40+
return False
41+
mi_renderer.variant = 'scalar_rgb'
42+
return True
43+
5344
########################
5445
## Scene convertion ##
5546
########################
@@ -174,10 +165,6 @@ def mi_shape_to_bl_node(mi_context, mi_props):
174165
return node
175166

176167
def mi_texture_to_bl_node(mi_context, mi_props):
177-
# We only parse bitmap textures
178-
if mi_props.plugin_name() != 'bitmap':
179-
return None
180-
181168
node = common.create_blender_node(common.BlenderNodeType.IMAGE, id=mi_props.id())
182169
# Convert dependencies if any
183170
_convert_named_references(mi_context, mi_props, node)
@@ -288,7 +275,7 @@ def instantiate_bl_object_node(mi_context, bl_node):
288275
return True
289276

290277
def instantiate_film_properties_node(mi_context, bl_node):
291-
if not renderer.apply_mi_film_properties(mi_context, bl_node.mi_props):
278+
if not films.apply_mi_film_properties(mi_context, bl_node.mi_props):
292279
return False
293280

294281
# Instantiate child rfilter if present.
@@ -299,13 +286,13 @@ def instantiate_film_properties_node(mi_context, bl_node):
299286
return True
300287

301288
def instantiate_integrator_properties_node(mi_context, bl_node):
302-
return renderer.apply_mi_integrator_properties(mi_context, bl_node.mi_props)
289+
return integrators.apply_mi_integrator_properties(mi_context, bl_node.mi_props)
303290

304291
def instantiate_rfilter_properties_node(mi_context, bl_node):
305-
return renderer.apply_mi_rfilter_properties(mi_context, bl_node.mi_props)
292+
return rfilters.apply_mi_rfilter_properties(mi_context, bl_node.mi_props)
306293

307294
def instantiate_sampler_properties_node(mi_context, bl_node):
308-
return renderer.apply_mi_sampler_properties(mi_context, bl_node.mi_props)
295+
return samplers.apply_mi_sampler_properties(mi_context, bl_node.mi_props)
309296

310297
_bl_properties_node_instantiators = {
311298
common.BlenderPropertiesNodeType.FILM: instantiate_film_properties_node,
@@ -363,7 +350,7 @@ def instantiate_bl_data_node(mi_context, bl_node):
363350
## Main loading ##
364351
#########################
365352

366-
def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat):
353+
def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat, with_cycles_nodes):
367354
''' Load a Mitsuba scene from an XML file into a Blender scene.
368355
369356
Params
@@ -373,13 +360,14 @@ def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat
373360
bl_collection: Blender collection
374361
filepath: Path to the Mitsuba XML scene file
375362
global_mat: Axis conversion matrix
363+
with_cycles_nodes: Should create Cycles node tree
376364
'''
377365
start_time = time.time()
378366
# Load the Mitsuba XML and extract the objects' properties
379367
from mitsuba import xml_to_props
380368
raw_props = xml_to_props(filepath)
381369
mi_scene_props = common.MitsubaSceneProperties(raw_props)
382-
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat)
370+
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat, with_cycles_nodes)
383371

384372
_, mi_props = mi_scene_props.get_first_of_class('Scene')
385373
bl_scene_data_node = mi_props_to_bl_data_node(mi_context, 'Scene', mi_props)
@@ -388,7 +376,7 @@ def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat
388376
return
389377

390378
# Initialize the Mitsuba renderer inside of Blender
391-
renderer.init_mitsuba_renderer(mi_context)
379+
_init_mitsuba_renderer(mi_context)
392380

393381
if not instantiate_bl_data_node(mi_context, bl_scene_data_node):
394382
mi_context.log('Failed to instantiate Blender scene', 'ERROR')
@@ -404,5 +392,3 @@ def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat
404392

405393
end_time = time.time()
406394
mi_context.log(f'Finished loading Mitsuba scene. Took {end_time-start_time:.2f}s.', 'INFO')
407-
408-
return

mitsuba-blender/io/importer/common.py renamed to mitsuba-blender/importer/common.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def get_first_of_class(self, cls):
201201

202202
class MitsubaSceneImportContext:
203203
''' Define a context for the Mitsuba scene importer '''
204-
def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props, axis_matrix):
204+
def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props, axis_matrix, with_cycles_nodes):
205205
self.bl_context = bl_context
206206
self.bl_scene = bl_scene
207207
self.bl_collection = bl_collection
@@ -210,6 +210,7 @@ def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props
210210
self.mi_scene_props = mi_scene_props
211211
self.axis_matrix = axis_matrix
212212
self.axis_matrix_inv = axis_matrix.inverted()
213+
self.with_cycles_nodes = with_cycles_nodes
213214
self.bl_material_cache = {}
214215
self.bl_image_cache = {}
215216

0 commit comments

Comments
 (0)