Skip to content

Commit

Permalink
Mesh refactor and CI fixes(#63)
Browse files Browse the repository at this point in the history
## Description

This PR addresses compatibility issues caused by data layout changes in
Blender's mesh representation for the exporter add-on. With these
changes, the exporter should now support versions up to 3.6 of
Blender.

The mesh layout in blender is progressively moving from an array of
structs to a struct of arrays format (more detail
[here](https://projects.blender.org/blender/blender/issues/95965), so we
need to reflect those changes when interpreting blender data pointers in
the mesh exporter.

This PR fixes several issues related to exporting meshes with more recent versions of blender, and also finally allows the add-on to work again on Windows.

---------

Co-authored-by: Rami Tabbara <[email protected]>
  • Loading branch information
bathal1 and rtabbara authored Dec 14, 2023
1 parent 6783bab commit bb4b7b6
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 33 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ jobs:
environment:
- {
os: "ubuntu-latest",
mitsuba-version: "3.0.1"
mitsuba-version: "3.4.1"
}
- {
os: "windows-latest",
mitsuba-version: "3.0.1"
mitsuba-version: "3.4.1"
}
blender:
- {
version: "2.93"
version: "3.3"
}
- {
version: "3.3"
version: "3.6"
}

steps:
- name: Git checkout
uses: actions/checkout@v2
Expand Down
10 changes: 5 additions & 5 deletions mitsuba-blender/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from . import io, engine

DEPS_MITSUBA_VERSION = '3.0.1'
DEPS_MITSUBA_VERSION = '3.4.1'

def get_addon_preferences(context):
return context.preferences.addons[__name__].preferences
Expand Down Expand Up @@ -108,7 +108,7 @@ def ensure_pip():
def check_pip_dependencies(context):
prefs = get_addon_preferences(context)
result = subprocess.run([sys.executable, '-m', 'pip', 'list'], capture_output=True)

prefs.has_pip_dependencies = False
prefs.has_valid_dependencies_version = False

Expand Down Expand Up @@ -139,7 +139,7 @@ def update_additional_custom_paths(self, context):
self.additional_path = build_path
if self.additional_path not in os.environ['PATH']:
os.environ['PATH'] += os.pathsep + self.additional_path

# Add path to python libs to sys.path
self.additional_python_path = os.path.join(build_path, 'python')
if self.additional_python_path not in sys.path:
Expand All @@ -161,7 +161,7 @@ def execute(self, context):
result = subprocess.run([sys.executable, '-m', 'pip', 'install', f'mitsuba=={DEPS_MITSUBA_VERSION}', '--force-reinstall'], capture_output=False)
if result.returncode != 0:
self.report({'ERROR'}, f'Failed to install Mitsuba with return code {result.returncode}.')
return {'CANCELLED'}
return {'CANCELLED'}

check_pip_dependencies(context)

Expand Down Expand Up @@ -280,7 +280,7 @@ def draw(self, context):
box.prop(self, 'using_mitsuba_custom_path', text=f'Use custom Mitsuba path (Supported version is v{DEPS_MITSUBA_VERSION})')
if self.using_mitsuba_custom_path:
box.prop(self, 'mitsuba_custom_path')

classes = (
MITSUBA_OT_install_pip_dependencies,
MitsubaPreferences,
Expand Down
5 changes: 3 additions & 2 deletions mitsuba-blender/engine/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def draw(self, layout):

def to_dict(self):
'''
Function that converts the plugin into a dict that can be loaded or savec by mitsuba's API
Function that converts the plugin into a dict that can be loaded or saved by mitsuba's API
'''
plugin_params = {'type' : name}
if 'parameters' in self.args:
Expand All @@ -249,7 +249,8 @@ def to_dict(self):
list_type = param['values_type']
if list_type == 'integrator':
for integrator in self.integrators.collection:
plugin_params[integrator.name] = getattr(integrator.available_integrators, integrator.active_integrator).to_dict()
# Make sure we don't have any leading underscores for names - Mitsuba will otherwise complain!
plugin_params[integrator.name.lstrip('_')] = getattr(integrator.available_integrators, integrator.active_integrator).to_dict()
elif list_type == 'string':
selected_items = []
for choice in param['choices']:
Expand Down
2 changes: 1 addition & 1 deletion mitsuba-blender/io/exporter/export_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def data_add(self, mts_dict, name=''):
del mts_dict['id']

except KeyError:
name = '__elm__%i' % self.counter
name = 'elm__%i' % self.counter

self.scene_data.update([(name, mts_dict)])
self.counter += 1
Expand Down
50 changes: 43 additions & 7 deletions mitsuba-blender/io/exporter/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ def convert_mesh(export_ctx, b_mesh, matrix_world, name, mat_nr):
for logging/debug purposes.
mat_nr: The material ID to export.
'''
from mitsuba import load_dict
props = {'type': 'blender'}
from mitsuba import load_dict, Point3i
props = {
'type': 'blender',
'version': ".".join(map(str,bpy.app.version))
}
b_mesh.calc_normals()
# Compute the triangle tesselation
b_mesh.calc_loop_triangles()
Expand All @@ -38,23 +41,56 @@ def convert_mesh(export_ctx, b_mesh, matrix_world, name, mat_nr):
export_ctx.log(f"Mesh: '{name}' has multiple UV layers. Mitsuba only supports one. Exporting the one set active for render.", 'WARN')
for uv_layer in b_mesh.uv_layers:
if uv_layer.active_render: # If there is only 1 UV layer, it is always active
props['uvs'] = uv_layer.data[0].as_pointer()
if uv_layer.name in b_mesh.attributes:
props['uvs'] = b_mesh.attributes[uv_layer.name].data[0].as_pointer()
else:
props['uvs'] = uv_layer.data[0].as_pointer()
break

for color_layer in b_mesh.vertex_colors:
props['vertex_%s' % color_layer.name] = color_layer.data[0].as_pointer()
if color_layer.name in b_mesh.attributes:
props[f'vertex_{color_layer.name}'] = b_mesh.attributes[color_layer.name].data[0].as_pointer()
else:
props[f'vertex_{color_layer.name}'] = color_layer.data[0].as_pointer()

props['loop_tris'] = b_mesh.loop_triangles[0].as_pointer()
props['loops'] = b_mesh.loops[0].as_pointer()
props['polys'] = b_mesh.polygons[0].as_pointer()
props['verts'] = b_mesh.vertices[0].as_pointer()

if '.corner_vert' in b_mesh.attributes:
# Blender 3.6+ layout
props['loops'] = b_mesh.attributes['.corner_vert'].data[0].as_pointer()
else:
props['loops'] = b_mesh.loops[0].as_pointer()

if 'sharp_face' in b_mesh.attributes:
props['sharp_face'] = b_mesh.attributes['sharp_face'].data[0].as_pointer()

if bpy.app.version >= (3, 6, 0):
props['polys'] = b_mesh.loop_triangle_polygons[0].as_pointer()
else:
props['polys'] = b_mesh.polygons[0].as_pointer()

if 'position' in b_mesh.attributes:
# Blender 3.5+ layout
props['verts'] = b_mesh.attributes['position'].data[0].as_pointer()
else:
props['verts'] = b_mesh.vertices[0].as_pointer()

if bpy.app.version > (3, 0, 0):
props['normals'] = b_mesh.vertex_normals[0].as_pointer()

props['vert_count'] = len(b_mesh.vertices)
# Apply coordinate change
if matrix_world:
props['to_world'] = export_ctx.transform_matrix(matrix_world)

# material index to export, as only a single material per mesh is suported in mitsuba
props['mat_nr'] = mat_nr
if 'material_index' in b_mesh.attributes:
# Blender 3.4+ layout
props['mat_indices'] = b_mesh.attributes['material_index'].data[0].as_pointer()
else:
props['mat_indices'] = 0

# Return the mitsuba mesh
return load_dict(props)

Expand Down
9 changes: 2 additions & 7 deletions mitsuba-blender/io/exporter/materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def convert_float_texture_node(export_ctx, socket):
raise NotImplementedError( "Node type %s is not supported. Only texture nodes are supported for float inputs" % node.type)

else:
if socket.name == 'Roughness':#roughness values in blender are remapped with a square root
#roughness values in blender are remapped with a square root
if 'Roughness' in socket.name:
params = pow(socket.default_value, 2)
else:
params = socket.default_value
Expand Down Expand Up @@ -272,12 +273,6 @@ def convert_principled_materials_cycles(export_ctx, current_node):
clearcoat = convert_float_texture_node(export_ctx, current_node.inputs['Clearcoat'])
clearcoat_roughness = convert_float_texture_node(export_ctx, current_node.inputs['Clearcoat Roughness'])

# Undo default roughness transform done by the exporter
if type(roughness) is float:
roughness = np.sqrt(roughness)
if type(clearcoat_roughness) is float:
clearcoat_roughness = np.sqrt(clearcoat_roughness)

params.update({
'type': 'principled',
'base_color': base_color,
Expand Down
2 changes: 1 addition & 1 deletion mitsuba-blender/io/importer/mi_spectra_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def convert_mi_srgb_reflectance_spectrum(mi_obj, default):
#######################

def convert_mi_srgb_emitter_spectrum(mi_obj, default):
assert mi_obj.class_().name() == 'SRGBEmitterSpectrum'
assert mi_obj.class_().name() == 'SRGBReflectanceSpectrum'
obj_props = _get_mi_obj_properties(mi_obj)
radiance = list(obj_props.get('value', default))
return get_color_strength_from_radiance(radiance)
11 changes: 8 additions & 3 deletions mitsuba-blender/io/importer/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def apply_mi_independent_properties(mi_context, mi_props):
bl_independent_props.sample_count = mi_props.get('sample_count', 4)
bl_independent_props.seed = mi_props.get('seed', 0)
# Cycles properties
bl_renderer.sampling_pattern = 'SOBOL'
bl_renderer.sampling_pattern = 'SOBOL' if bpy.app.version < (3, 5, 0) else 'SOBOL_BURLEY'
bl_renderer.samples = mi_props.get('sample_count', 4)
bl_renderer.preview_samples = mi_props.get('sample_count', 4)
bl_renderer.seed = mi_props.get('seed', 0)
Expand All @@ -210,7 +210,7 @@ def apply_mi_stratified_properties(mi_context, mi_props):
bl_stratified_props.jitter = mi_props.get('jitter', True)
# Cycles properties
# NOTE: There isn't any equivalent sampler in Blender. We use the default Sobol pattern.
bl_renderer.sampling_pattern = 'SOBOL'
bl_renderer.sampling_pattern = 'SOBOL' if bpy.app.version < (3, 5, 0) else 'SOBOL_BURLEY'
bl_renderer.samples = mi_props.get('sample_count', 4)
bl_renderer.seed = mi_props.get('seed', 0)
return True
Expand All @@ -228,7 +228,12 @@ def apply_mi_multijitter_properties(mi_context, mi_props):
bl_multijitter_props.seed = mi_props.get('seed', 0)
bl_multijitter_props.jitter = mi_props.get('jitter', True)
# Cycles properties
bl_renderer.sampling_pattern = 'CORRELATED_MUTI_JITTER' if bpy.app.version < (3, 0, 0) else 'PROGRESSIVE_MULTI_JITTER'
if bpy.app.version < (3, 0, 0):
bl_renderer.sampling_pattern = 'CORRELATED_MUTI_JITTER'
elif bpy.app.version < (3, 5, 0):
bl_renderer.sampling_pattern = 'PROGRESSIVE_MULTI_JITTER'
else:
bl_renderer.sampling_pattern = 'TABULATED_SOBOL'
bl_renderer.samples = mi_props.get('sample_count', 4)
bl_renderer.seed = mi_props.get('seed', 0)
return True
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ def _bitmap_extract(self, bmp, require_variance=True):
b_root = b_root.convert(Bitmap.PixelFormat.XYZ, Struct.Type.Float32, False)
return np.array(b_root, copy=True), None
else:
img = np.array(split[1][1], copy=False)
img_m2 = np.array(split[2][1], copy=False)
# Check which split contains moments - it may not be the first one after root
m2_index = 1 if split[1][0].startswith('m2_') else 2
img = np.array(split[m2_index][1], copy=False)
img_m2 = np.array(split[m2_index][1], copy=False)
return img, img_m2 - img * img

def render_scene(self, scene_file, **kwargs):
Expand Down

0 comments on commit bb4b7b6

Please sign in to comment.