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):