Skip to content

Commit

Permalink
Nodes: Introduce custom shader nodes for Mitsuba BSDFs
Browse files Browse the repository at this point in the history
  • Loading branch information
ros-dorian committed Aug 26, 2022
1 parent eb2679c commit 3978972
Show file tree
Hide file tree
Showing 68 changed files with 3,369 additions and 1,184 deletions.
14 changes: 11 additions & 3 deletions mitsuba-blender/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import sys
import subprocess

from . import io, engine
from . import (
engine, nodes, properties, operators, ui
)

def get_addon_preferences(context):
return context.preferences.addons[__name__].preferences
Expand Down Expand Up @@ -69,8 +71,11 @@ def try_register_mitsuba(context):
prefs.is_mitsuba_initialized = could_init_mitsuba

if could_init_mitsuba:
io.register()
properties.register()
operators.register()
ui.register()
engine.register()
nodes.register()

return could_init_mitsuba

Expand All @@ -80,8 +85,11 @@ def try_unregister_mitsuba():
This may fail if Mitsuba wasn't found, hence the try catch guard
'''
try:
io.unregister()
engine.unregister()
nodes.unregister()
ui.unregister()
operators.unregister()
properties.unregister()
return True
except RuntimeError:
return False
Expand Down
8 changes: 7 additions & 1 deletion mitsuba-blender/engine/final.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
import tempfile
import os
import numpy as np
from ..io.exporter import SceneConverter
from ..exporter import SceneConverter

class MitsubaRenderEngine(bpy.types.RenderEngine):

bl_idname = "MITSUBA"
bl_label = "Mitsuba"
bl_use_preview = False
bl_use_texture_preview = False
# Hide Cycles shader nodes in the shading menu
bl_use_shading_nodes_custom = False
# FIXME: This is used to get a visual feedback of the shapes,
# it does not produce a correct result.
bl_use_eevee_viewport = True

# Init is called whenever a new render engine instance is created. Multiple
# instances may exist at the same time, for example for a viewport and final
Expand Down
42 changes: 28 additions & 14 deletions mitsuba-blender/engine/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ class MITSUBA_CAMERA_PT_sampler(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'render'
COMPAT_ENGINES = {'MITSUBA'}

@classmethod
def poll(cls, context):
return context.engine in cls.COMPAT_ENGINES

def draw(self, context):
layout = self.layout
if hasattr(context.scene.camera, 'data'):
Expand All @@ -410,14 +416,20 @@ class MITSUBA_CAMERA_PT_rfilter(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'render'
COMPAT_ENGINES = {'MITSUBA'}

@classmethod
def poll(cls, context):
return context.engine in cls.COMPAT_ENGINES

def draw(self, context):
layout = self.layout
if hasattr(context.scene.camera, 'data'):
cam_settings = context.scene.camera.data.mitsuba
layout.prop(cam_settings, "active_rfilter", text="Filter")
getattr(cam_settings.rfilters, cam_settings.active_rfilter).draw(layout)

def draw_device(self, context):
def mitsuba_render_draw(self, context):
scene = context.scene
layout = self.layout
layout.use_property_split = True
Expand All @@ -429,18 +441,20 @@ def draw_device(self, context):
col = layout.column()
col.prop(mts_settings, "variant")

def register():
bpy.types.RENDER_PT_context.append(draw_device)
bpy.utils.register_class(MitsubaRenderSettings)
bpy.utils.register_class(MitsubaCameraSettings)
bpy.utils.register_class(MITSUBA_RENDER_PT_integrator)
bpy.utils.register_class(MITSUBA_CAMERA_PT_sampler)
bpy.utils.register_class(MITSUBA_CAMERA_PT_rfilter)
classes = [
MitsubaRenderSettings,
MitsubaCameraSettings,
MITSUBA_RENDER_PT_integrator,
MITSUBA_CAMERA_PT_sampler,
MITSUBA_CAMERA_PT_rfilter,
]

def register():
bpy.types.RENDER_PT_context.append(mitsuba_render_draw)
for cls in classes:
bpy.utils.register_class(cls)

def unregister():
bpy.types.RENDER_PT_context.remove(draw_device)
bpy.utils.unregister_class(MitsubaRenderSettings)
bpy.utils.unregister_class(MitsubaCameraSettings)
bpy.utils.unregister_class(MITSUBA_RENDER_PT_integrator)
bpy.utils.unregister_class(MITSUBA_CAMERA_PT_sampler)
bpy.utils.unregister_class(MITSUBA_CAMERA_PT_rfilter)
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.RENDER_PT_context.remove(mitsuba_render_draw)
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import os

if "bpy" in locals():
import importlib
if "export_context" in locals():
importlib.reload(export_context)
if "materials" in locals():
importlib.reload(materials)
if "geometry" in locals():
importlib.reload(geometry)
if "lights" in locals():
importlib.reload(lights)
if "camera" in locals():
importlib.reload(camera)

import bpy

from . import export_context
Expand Down Expand Up @@ -53,7 +40,7 @@ def scene_to_dict(self, depsgraph, window_manager):

b_scene = depsgraph.scene #TODO: what if there are multiple scenes?
if b_scene.render.engine == 'MITSUBA':
integrator = getattr(b_scene.mitsuba.available_integrators,b_scene.mitsuba.active_integrator).to_dict()
integrator = getattr(b_scene.mitsuba.available_integrators, b_scene.mitsuba.active_integrator).to_dict()
else:
integrator = {
'type':'path',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
from mathutils import Matrix
import numpy as np
from math import degrees

def export_camera(camera_instance, b_scene, export_ctx):
#camera
b_camera = camera_instance.object#TODO: instances here too?
params = {}
params['type'] = 'perspective'

res_x = b_scene.render.resolution_x
res_y = b_scene.render.resolution_y

# Extract fov
sensor_fit = b_camera.data.sensor_fit
if sensor_fit == 'AUTO':
params['fov_axis'] = 'x' if res_x >= res_y else 'y'
params['fov'] = degrees(b_camera.data.angle_x)
elif sensor_fit == 'HORIZONTAL':
params['fov_axis'] = 'x'
params['fov'] = degrees(b_camera.data.angle_x)
elif sensor_fit == 'VERTICAL':
params['fov_axis'] = 'y'
params['fov'] = degrees(b_camera.data.angle_y)
else:
export_ctx.log(f'Unknown \'sensor_fit\' value when exporting camera: {sensor_fit}', 'ERROR')
#extract fov
params['fov_axis'] = 'x'
params['fov'] = b_camera.data.angle_x * 180 / np.pi#TODO: check cam.sensor_fit

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

scale = b_scene.render.resolution_percentage / 100
film['width'] = int(res_x * scale)
film['height'] = int(res_y * scale)
film['width'] = int(b_scene.render.resolution_x * scale)
film['height'] = int(b_scene.render.resolution_y * scale)


if b_scene.render.engine == 'MITSUBA':
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ def export_object(deg_instance, export_ctx, is_particle):
mat_nr)
if mts_mesh is not None and mts_mesh.face_count() > 0:
converted_parts.append((mat_nr, mts_mesh))
export_material(export_ctx, b_mesh.materials[mat_nr])
b_mat = b_mesh.materials[mat_nr]
export_material(export_ctx, b_mat)

if b_object.type != 'MESH':
b_object.to_mesh_clear()
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
from mathutils import Matrix
from ..utils.nodetree import get_active_output
from .export_context import Files

RoughnessMode = {'GGX': 'ggx', 'BECKMANN': 'beckmann', 'ASHIKHMIN_SHIRLEY':'beckmann', 'MULTI_GGX':'ggx'}
Expand Down Expand Up @@ -320,30 +321,32 @@ def cycles_material_to_dict(export_ctx, node):

return params

def get_dummy_material(export_ctx):
return {
'type': 'diffuse',
'reflectance': export_ctx.spectrum([1.0, 0.0, 0.3]),
}

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

mat_params = {}

if b_mat.use_nodes:
if original_mat.mitsuba.node_tree is not None:
output_node = get_active_output(original_mat.mitsuba.node_tree)
if output_node is not None:
mat_params = output_node.to_dict(export_ctx)
else:
export_ctx.log(f'Material {b_mat.name} does not have an output node.', 'ERROR')

elif b_mat.use_nodes:
try:
output_node_id = 'Material Output'
if output_node_id in b_mat.node_tree.nodes:
output_node = b_mat.node_tree.nodes[output_node_id]
surface_node = output_node.inputs["Surface"].links[0].from_node
mat_params = cycles_material_to_dict(export_ctx, surface_node)
else:
export_ctx.log(f'Export of material {b_mat.name} failed: Cannot find material output node. Exporting a dummy material instead.', 'WARN')
mat_params = get_dummy_material(export_ctx)
except NotImplementedError as e:
export_ctx.log(f'Export of material \'{b_mat.name}\' failed: {e.args[0]}. Exporting a dummy material instead.', 'WARN')
mat_params = get_dummy_material(export_ctx)
output_node = b_mat.node_tree.nodes["Material Output"]
surface_node = output_node.inputs["Surface"].links[0].from_node
mat_params = cycles_material_to_dict(export_ctx, surface_node)

except NotImplementedError as err:
export_ctx.log("Export of material %s failed : %s Exporting a dummy texture instead." % (b_mat.name, err.args[0]), 'WARN')
mat_params = {'type':'diffuse'}
mat_params['reflectance'] = export_ctx.spectrum([1.0,0.0,0.3])

else:
mat_params = {'type':'diffuse'}
mat_params['reflectance'] = export_ctx.spectrum(b_mat.diffuse_color)
Expand Down Expand Up @@ -407,16 +410,8 @@ def convert_world(export_ctx, world, ignore_background):

params = {}

if world is None:
export_ctx.log('No Blender world to export.', 'INFO')
return

if world.use_nodes and world.node_tree is not None:
output_node_id = 'World Output'
if output_node_id not in world.node_tree.nodes:
export_ctx.log('Failed to export world: Cannot find world output node.', 'WARN')
return
output_node = world.node_tree.nodes[output_node_id]
if world.use_nodes:
output_node = world.node_tree.nodes['World Output']
if not output_node.inputs["Surface"].is_linked:
return
surface_node = output_node.inputs["Surface"].links[0].from_node
Expand Down Expand Up @@ -488,14 +483,15 @@ def convert_world(export_ctx, world, ignore_background):
'type': 'constant',
'radiance': export_ctx.spectrum(radiance)
})

else:
raise NotImplementedError("Only Background and Emission nodes are supported as final nodes for World export, got '%s'" % surface_node.name)
else:
# Single color field for emission, no nodes
params.update({
'type': 'constant',
'radiance': export_ctx.spectrum(world.color)
})
params.update({
'type': 'constant',
'radiance': export_ctx.spectrum(world.color)
})

if export_ctx.export_ids:
export_ctx.data_add(params, "World")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def mi_shape_to_bl_node(mi_context, mi_props):
def mi_texture_to_bl_node(mi_context, mi_props):
# We only parse bitmap textures
if mi_props.plugin_name() != 'bitmap':
mi_context.log(f'Mitsuba texture "{mi_props.plugin_name()}" not supported.', 'ERROR')
return None

node = common.create_blender_node(common.BlenderNodeType.IMAGE, id=mi_props.id())
Expand Down Expand Up @@ -363,7 +364,7 @@ def instantiate_bl_data_node(mi_context, bl_node):
## Main loading ##
#########################

def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat):
def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat, with_mitsuba_nodes):
''' Load a Mitsuba scene from an XML file into a Blender scene.
Params
Expand All @@ -373,13 +374,14 @@ def load_mitsuba_scene(bl_context, bl_scene, bl_collection, filepath, global_mat
bl_collection: Blender collection
filepath: Path to the Mitsuba XML scene file
global_mat: Axis conversion matrix
with_mitsuba_nodes: Should create custom Mitsuba node tree
'''
start_time = time.time()
# Load the Mitsuba XML and extract the objects' properties
from mitsuba import xml_to_props
raw_props = xml_to_props(filepath)
mi_scene_props = common.MitsubaSceneProperties(raw_props)
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat)
mi_context = common.MitsubaSceneImportContext(bl_context, bl_scene, bl_collection, filepath, mi_scene_props, global_mat, with_mitsuba_nodes)

_, mi_props = mi_scene_props.get_first_of_class('Scene')
bl_scene_data_node = mi_props_to_bl_data_node(mi_context, 'Scene', mi_props)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def get_first_of_class(self, cls):

class MitsubaSceneImportContext:
''' Define a context for the Mitsuba scene importer '''
def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props, axis_matrix):
def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props, axis_matrix, with_mitsuba_nodes):
self.bl_context = bl_context
self.bl_scene = bl_scene
self.bl_collection = bl_collection
Expand All @@ -210,6 +210,7 @@ def __init__(self, bl_context, bl_scene, bl_collection, filepath, mi_scene_props
self.mi_scene_props = mi_scene_props
self.axis_matrix = axis_matrix
self.axis_matrix_inv = axis_matrix.inverted()
self.with_mitsuba_nodes = with_mitsuba_nodes
self.bl_material_cache = {}
self.bl_image_cache = {}

Expand Down
File renamed without changes.
Loading

0 comments on commit 3978972

Please sign in to comment.