Skip to content

Commit 3978972

Browse files
committed
Nodes: Introduce custom shader nodes for Mitsuba BSDFs
1 parent eb2679c commit 3978972

Some content is hidden

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

68 files changed

+3369
-1184
lines changed

mitsuba-blender/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import sys
2121
import subprocess
2222

23-
from . import io, engine
23+
from . import (
24+
engine, nodes, properties, operators, ui
25+
)
2426

2527
def get_addon_preferences(context):
2628
return context.preferences.addons[__name__].preferences
@@ -69,8 +71,11 @@ def try_register_mitsuba(context):
6971
prefs.is_mitsuba_initialized = could_init_mitsuba
7072

7173
if could_init_mitsuba:
72-
io.register()
74+
properties.register()
75+
operators.register()
76+
ui.register()
7377
engine.register()
78+
nodes.register()
7479

7580
return could_init_mitsuba
7681

@@ -80,8 +85,11 @@ def try_unregister_mitsuba():
8085
This may fail if Mitsuba wasn't found, hence the try catch guard
8186
'''
8287
try:
83-
io.unregister()
8488
engine.unregister()
89+
nodes.unregister()
90+
ui.unregister()
91+
operators.unregister()
92+
properties.unregister()
8593
return True
8694
except RuntimeError:
8795
return False

mitsuba-blender/engine/final.py

Lines changed: 7 additions & 1 deletion
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

Lines changed: 28 additions & 14 deletions
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

Lines changed: 1 addition & 14 deletions
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/camera.py renamed to mitsuba-blender/exporter/camera.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
11
from mathutils import Matrix
22
import numpy as np
3-
from math import degrees
43

54
def export_camera(camera_instance, b_scene, export_ctx):
65
#camera
76
b_camera = camera_instance.object#TODO: instances here too?
87
params = {}
98
params['type'] = 'perspective'
10-
11-
res_x = b_scene.render.resolution_x
12-
res_y = b_scene.render.resolution_y
13-
14-
# Extract fov
15-
sensor_fit = b_camera.data.sensor_fit
16-
if sensor_fit == 'AUTO':
17-
params['fov_axis'] = 'x' if res_x >= res_y else 'y'
18-
params['fov'] = degrees(b_camera.data.angle_x)
19-
elif sensor_fit == 'HORIZONTAL':
20-
params['fov_axis'] = 'x'
21-
params['fov'] = degrees(b_camera.data.angle_x)
22-
elif sensor_fit == 'VERTICAL':
23-
params['fov_axis'] = 'y'
24-
params['fov'] = degrees(b_camera.data.angle_y)
25-
else:
26-
export_ctx.log(f'Unknown \'sensor_fit\' value when exporting camera: {sensor_fit}', 'ERROR')
9+
#extract fov
10+
params['fov_axis'] = 'x'
11+
params['fov'] = b_camera.data.angle_x * 180 / np.pi#TODO: check cam.sensor_fit
2712

2813
#TODO: test other parameters relevance (camera.lens, orthographic_scale, dof...)
2914
params['near_clip'] = b_camera.data.clip_start
@@ -46,8 +31,8 @@ def export_camera(camera_instance, b_scene, export_ctx):
4631
film['type'] = 'hdrfilm'
4732

4833
scale = b_scene.render.resolution_percentage / 100
49-
film['width'] = int(res_x * scale)
50-
film['height'] = int(res_y * scale)
34+
film['width'] = int(b_scene.render.resolution_x * scale)
35+
film['height'] = int(b_scene.render.resolution_y * scale)
5136

5237

5338
if b_scene.render.engine == 'MITSUBA':

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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 28 additions & 32 deletions
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'}
@@ -320,30 +321,32 @@ def cycles_material_to_dict(export_ctx, node):
320321

321322
return params
322323

323-
def get_dummy_material(export_ctx):
324-
return {
325-
'type': 'diffuse',
326-
'reflectance': export_ctx.spectrum([1.0, 0.0, 0.3]),
327-
}
328-
329324
def b_material_to_dict(export_ctx, b_mat):
330325
''' Converting one material from Blender / Cycles to Mitsuba'''
326+
# NOTE: The evaluated material does not keep references to Mitsuba node trees.
327+
# We need to use the original material instead.
328+
original_mat = b_mat.original
331329

332330
mat_params = {}
333331

334-
if b_mat.use_nodes:
332+
if original_mat.mitsuba.node_tree is not None:
333+
output_node = get_active_output(original_mat.mitsuba.node_tree)
334+
if output_node is not None:
335+
mat_params = output_node.to_dict(export_ctx)
336+
else:
337+
export_ctx.log(f'Material {b_mat.name} does not have an output node.', 'ERROR')
338+
339+
elif b_mat.use_nodes:
335340
try:
336-
output_node_id = 'Material Output'
337-
if output_node_id in b_mat.node_tree.nodes:
338-
output_node = b_mat.node_tree.nodes[output_node_id]
339-
surface_node = output_node.inputs["Surface"].links[0].from_node
340-
mat_params = cycles_material_to_dict(export_ctx, surface_node)
341-
else:
342-
export_ctx.log(f'Export of material {b_mat.name} failed: Cannot find material output node. Exporting a dummy material instead.', 'WARN')
343-
mat_params = get_dummy_material(export_ctx)
344-
except NotImplementedError as e:
345-
export_ctx.log(f'Export of material \'{b_mat.name}\' failed: {e.args[0]}. Exporting a dummy material instead.', 'WARN')
346-
mat_params = get_dummy_material(export_ctx)
341+
output_node = b_mat.node_tree.nodes["Material Output"]
342+
surface_node = output_node.inputs["Surface"].links[0].from_node
343+
mat_params = cycles_material_to_dict(export_ctx, surface_node)
344+
345+
except NotImplementedError as err:
346+
export_ctx.log("Export of material %s failed : %s Exporting a dummy texture instead." % (b_mat.name, err.args[0]), 'WARN')
347+
mat_params = {'type':'diffuse'}
348+
mat_params['reflectance'] = export_ctx.spectrum([1.0,0.0,0.3])
349+
347350
else:
348351
mat_params = {'type':'diffuse'}
349352
mat_params['reflectance'] = export_ctx.spectrum(b_mat.diffuse_color)
@@ -407,16 +410,8 @@ def convert_world(export_ctx, world, ignore_background):
407410

408411
params = {}
409412

410-
if world is None:
411-
export_ctx.log('No Blender world to export.', 'INFO')
412-
return
413-
414-
if world.use_nodes and world.node_tree is not None:
415-
output_node_id = 'World Output'
416-
if output_node_id not in world.node_tree.nodes:
417-
export_ctx.log('Failed to export world: Cannot find world output node.', 'WARN')
418-
return
419-
output_node = world.node_tree.nodes[output_node_id]
413+
if world.use_nodes:
414+
output_node = world.node_tree.nodes['World Output']
420415
if not output_node.inputs["Surface"].is_linked:
421416
return
422417
surface_node = output_node.inputs["Surface"].links[0].from_node
@@ -488,14 +483,15 @@ def convert_world(export_ctx, world, ignore_background):
488483
'type': 'constant',
489484
'radiance': export_ctx.spectrum(radiance)
490485
})
486+
491487
else:
492488
raise NotImplementedError("Only Background and Emission nodes are supported as final nodes for World export, got '%s'" % surface_node.name)
493489
else:
494490
# Single color field for emission, no nodes
495-
params.update({
496-
'type': 'constant',
497-
'radiance': export_ctx.spectrum(world.color)
498-
})
491+
params.update({
492+
'type': 'constant',
493+
'radiance': export_ctx.spectrum(world.color)
494+
})
499495

500496
if export_ctx.export_ids:
501497
export_ctx.data_add(params, "World")

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def mi_shape_to_bl_node(mi_context, mi_props):
176176
def mi_texture_to_bl_node(mi_context, mi_props):
177177
# We only parse bitmap textures
178178
if mi_props.plugin_name() != 'bitmap':
179+
mi_context.log(f'Mitsuba texture "{mi_props.plugin_name()}" not supported.', 'ERROR')
179180
return None
180181

181182
node = common.create_blender_node(common.BlenderNodeType.IMAGE, id=mi_props.id())
@@ -363,7 +364,7 @@ def instantiate_bl_data_node(mi_context, bl_node):
363364
## Main loading ##
364365
#########################
365366

366-
def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat):
367+
def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat, with_mitsuba_nodes):
367368
''' Load a Mitsuba scene from an XML file into a Blender scene.
368369
369370
Params
@@ -373,13 +374,14 @@ def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat
373374
bl_collection: Blender collection
374375
filepath: Path to the Mitsuba XML scene file
375376
global_mat: Axis conversion matrix
377+
with_mitsuba_nodes: Should create custom Mitsuba node tree
376378
'''
377379
start_time = time.time()
378380
# Load the Mitsuba XML and extract the objects' properties
379381
from mitsuba import xml_to_props
380382
raw_props = xml_to_props(filepath)
381383
mi_scene_props = common.MitsubaSceneProperties(raw_props)
382-
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat)
384+
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat, with_mitsuba_nodes)
383385

384386
_, mi_props = mi_scene_props.get_first_of_class('Scene')
385387
bl_scene_data_node = mi_props_to_bl_data_node(mi_context, 'Scene', mi_props)

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

Lines changed: 2 additions & 1 deletion
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_mitsuba_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_mitsuba_nodes = with_mitsuba_nodes
213214
self.bl_material_cache = {}
214215
self.bl_image_cache = {}
215216

0 commit comments

Comments
 (0)