From bb4b7b61f974d1223200627028bf1c7133561729 Mon Sep 17 00:00:00 2001 From: Baptiste Nicolet <40777524+bathal1@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:12:19 +0100 Subject: [PATCH] Mesh refactor and CI fixes(#63) ## 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 --- .github/workflows/test.yml | 10 ++-- mitsuba-blender/__init__.py | 10 ++-- mitsuba-blender/engine/properties.py | 5 +- mitsuba-blender/io/exporter/export_context.py | 2 +- mitsuba-blender/io/exporter/geometry.py | 50 ++++++++++++++++--- mitsuba-blender/io/exporter/materials.py | 9 +--- .../io/importer/mi_spectra_utils.py | 2 +- mitsuba-blender/io/importer/renderer.py | 11 ++-- tests/fixtures/__init__.py | 6 ++- 9 files changed, 72 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 745c9e2..f4d6555 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/mitsuba-blender/__init__.py b/mitsuba-blender/__init__.py index 5bc91da..913c7b3 100644 --- a/mitsuba-blender/__init__.py +++ b/mitsuba-blender/__init__.py @@ -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 @@ -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 @@ -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: @@ -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) @@ -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, diff --git a/mitsuba-blender/engine/properties.py b/mitsuba-blender/engine/properties.py index 4eb6194..d956c66 100644 --- a/mitsuba-blender/engine/properties.py +++ b/mitsuba-blender/engine/properties.py @@ -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: @@ -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']: diff --git a/mitsuba-blender/io/exporter/export_context.py b/mitsuba-blender/io/exporter/export_context.py index 6863750..602c21f 100644 --- a/mitsuba-blender/io/exporter/export_context.py +++ b/mitsuba-blender/io/exporter/export_context.py @@ -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 diff --git a/mitsuba-blender/io/exporter/geometry.py b/mitsuba-blender/io/exporter/geometry.py index 07a1336..6b341e6 100644 --- a/mitsuba-blender/io/exporter/geometry.py +++ b/mitsuba-blender/io/exporter/geometry.py @@ -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() @@ -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) diff --git a/mitsuba-blender/io/exporter/materials.py b/mitsuba-blender/io/exporter/materials.py index b3e5a15..bf589b0 100644 --- a/mitsuba-blender/io/exporter/materials.py +++ b/mitsuba-blender/io/exporter/materials.py @@ -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 @@ -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, diff --git a/mitsuba-blender/io/importer/mi_spectra_utils.py b/mitsuba-blender/io/importer/mi_spectra_utils.py index 615d2f3..6224913 100644 --- a/mitsuba-blender/io/importer/mi_spectra_utils.py +++ b/mitsuba-blender/io/importer/mi_spectra_utils.py @@ -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) \ No newline at end of file diff --git a/mitsuba-blender/io/importer/renderer.py b/mitsuba-blender/io/importer/renderer.py index faa1909..ffa0c87 100644 --- a/mitsuba-blender/io/importer/renderer.py +++ b/mitsuba-blender/io/importer/renderer.py @@ -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) @@ -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 @@ -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 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 0ebdeff..e1dc2a1 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -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):