Repository navigation
Rewrite the mitsuba-blender addon - #156
Merged
Merged
Conversation
Arthur926564
marked this pull request as ready for review
September 4, 2026 11:30
Bump the supported dependency version to 3.9.0, only reload the mitsuba module when a custom path is in use (reloading the 3.9 alias package raises), and drop the dead MI_DEFAULT_VARIANT lookup in the render settings, which 3.9 no longer exposes and whose result was overwritten unconditionally anyway.
tests/run.py launches headless Blender (found via the BLENDER env var), runs pytest inside it through tests/_bootstrap.py, and propagates the exit code back to the host via a result file, so failures and crashes both fail the process. It prints the pytest report tail and a PASS/FAIL summary, keeping the full Blender output in a log file. Shared fixtures register the in-repo addon, reset the scene, render Mitsuba scene dicts and compare image statistics. Legacy tests directly under tests/ keep working with scripts/run_tests.py and are ignored by the new harness. tests/README.md documents usage and the slow/packaging markers.
The mean and second-moment images were both read from the m2 split of the moment integrator output, so the comparison test compared second moments instead of means and computed a bogus variance.
The render engine crashed before producing any output: RenderEngine subclasses must forward constructor arguments to the base class, the Mitsuba thread environment API (ScopedSetThreadEnvironment/Thread) no longer exists, the referenced b_scene.thread_env property was never registered, and scene_to_dict() was called without the newly required window_manager argument. Make window_manager optional (None disables progress updates) and drop the temporary directory block that only fed the removed file resolver call. Also write the root film image to Blender's built-in Combined pass (RGBA, padding alpha when Mitsuba produces RGB) instead of a custom 'Main' pass that never reached the saved render result.
The export context's 'directory' field was never set, so meshes and textures silently landed in subfolders of the process working directory. The export operator now derives it from the target file path and the render engine uses a temporary directory that lives until the scene dict has been loaded; dict_to_scene() resolves the relative file references against it via the Mitsuba file resolver. Drop the transform_relocate() call when writing XML: the exporter already places files in the proper subfolders, and relocating a file onto itself truncates it to zero bytes.
Four regressions from the parser API port in mitsuba-renderer#137: - write_mi_emitter_bsdf() left 'radiance'/'strength' unbound when the radiance was not stored as a Color; Float radiance is now converted through the same emissive-texture path and other types fall back to a warning plus the default. - The mask BSDF fast path called get_texture('reflectance') on a diffuse child without an explicit reflectance, which crashes in Mitsuba 3.9; the black-diffuse check now verifies that the property exists and is a Color first. - The same path inverted the opacity by writing back into the parser state, corrupting later conversions of the same material; the inverted value is now computed locally. - A shape with an emitter but no BSDF lost its emitter, and a failed material conversion appended None to the material list; emissive shapes without a BSDF now get an emission material on top of a null BSDF, and None materials are skipped.
Blender extension ids must be valid Python identifiers, so the package loses its hyphen ahead of the extension migration. All references in the test harnesses, release packaging and CI workflows follow suit. Note: scripts/run_tests.py already exits 1 at teardown on the base revision because its symlink cleanup races the new conftest fixture; the tests themselves pass (29 passed) and the next commit fixes the cleanup.
When the legacy runner collects the new-harness tests, both it and the session fixture in tests/conftest.py manage the same addon symlink, and whichever tears down second crashed on the missing link, failing the run after all tests passed. Guard both cleanups so either order works.
Add blender_manifest.toml (id mitsuba_blender, Blender 4.2+, BSD-3) and replace the legacy __init__.py with a minimal one: no bl_info, no pip subprocesses, no PATH surgery, no saved preference state machine. The DRJIT_NO_RTLD_DEEPBIND environment variable is set before the first mitsuba import. If mitsuba cannot be imported, only the preferences panel registers and reports the failure instead of raising. The one remaining preference is custom_mitsuba_path, a dev override that is prepended to sys.path before the import; changing it shows a 'restart Blender' note instead of attempting reload magic. Both test harnesses now install the addon by symlinking it into the user_default extension repository and enabling bl_ext.user_default.mitsuba_blender. The wheels list in the manifest is empty for local development (mitsuba comes from Blender's site-packages); CI fills it for release builds.
The 'if "bpy" in locals()' reload blocks existed to support Blender's legacy F8 script reloading, which extensions no longer use.
Marked @pytest.mark.packaging (opt-in): builds the zip with 'blender --command extension build' and installs it into a factory Blender with isolated user resources, asserting that the MITSUBA render engine registers.
The JSON plugin descriptions and the create_plugin_props metaprogramming are gone. properties.py now declares one PropertyGroup per supported integrator (path, direct, volpath, volpathmis, aov, ptracer), sampler (independent, stratified, multijitter, orthogonal, ldsampler) and reconstruction filter (box, tent, gaussian, mitchell, catmullrom, lanczos), and ui.py holds the panels, all with COMPAT_ENGINES handling. The nested moment/stokes UIs are dropped in favor of a custom integrator text field on the render settings: a Python dict literal that is parsed with ast.literal_eval and used verbatim. The importer stores integrators without a UI representation (such as moment) in this field, so they still survive an import/export round trip.
The minimum supported Blender is 4.2 LTS, so every remaining bpy.app.version branch was decidable: keep the modern path, delete the rest. compat.py is now the only module allowed to inspect bpy.app.version, enforced by a unit test, and provides the version string that the blender mesh exporter embeds.
Enabling, disabling and re-enabling the extension must leave no registered classes, panel COMPAT_ENGINES tags or RNA pointers behind, and must restore all of them on the second enable.
The exporter no longer passes as_pointer() addresses to the 'blender' plugin. It reads the triangulated geometry into numpy arrays via foreach_get, welds corners that share vertex, normal, UV and color data, and constructs mitsuba.Mesh objects directly. Per-corner normals from Blender cover smooth, flat and custom split normals uniformly, so the 'face_normals' flag is no longer needed. When rendering inside Blender, the meshes stay in memory and enter the scene dict as instantiated objects, with BSDFs and area emitters attached through the shape constructor; nothing is written to disk. The file-export path keeps writing binary PLY next to the XML. Materials assignments now come from the object's material slots instead of the mesh data, so per-object material overrides are respected.
Mesh data that is rendered more than once, whether through linked duplicates, instanced collections, vertex instancers or particle systems, is now converted a single time and referenced by a shapegroup with one Mitsuba instance per occurrence. Occurrences are identified by the evaluated data's session_uid together with the object's material slots, so per-object material overrides still export separately. The dependency graph is iterated twice (a counting pass and a conversion pass) because evaluated objects must not be kept alive across iteration steps; storing them crashed Blender for particle instances. This replaces both the bpy.data.particles scan and the name-based dedup of the old exporter, and prototype objects of vertex instancers are no longer rendered at their own location.
Mesh-backed shape plugins (ply, obj, serialized, and anything else Mitsuba can instantiate as a mesh) are now loaded by Mitsuba itself and their vertex buffers copied into a Blender mesh with foreach_set, including normals, UVs and per-vertex attributes such as vertex colors. This deletes the vendored PLY and OBJ importers (over 1000 lines) and automatically supports every file feature Mitsuba understands. Analytic shapes (sphere, rectangle, cube, disk) keep mapping to native Blender primitives.
A smooth-shaded Suzanne is exported through the render-mode pipeline and rendered at 64x64 with scalar_rgb, spp=16 and a fixed seed, then compared to a committed reference image by mean error and RMSE. The new --update-refs pytest option regenerates the references.
Material converters resolve their input sockets through a new _eval module: links are followed through reroutes and muted nodes, and subgraphs built entirely from value nodes (Math, Vector Math, Mix, RGB, Value, Invert, Gamma, Brightness/Contrast, Map Range, Clamp, Separate/Combine XYZ/Color, RGB to BW) are evaluated in Python. A socket resolves to a constant, a Mitsuba texture dict (via a registry that texture converters plug into), or an unsupported marker with a precise warning message. The RGB node value is read from its Color output socket rather than node.color, which is the header color (from PR mitsuba-renderer#153).
Shader node converters now self-register per node type with a @node_converter decorator; converter modules dropped into convert/export/materials are auto-discovered. convert_material wraps every conversion in an error boundary that logs a warning and substitutes a gray diffuse, so one broken material cannot abort an export, and a material whose Surface input is unlinked gets the same fallback instead of raising (from PR mitsuba-renderer#153). A diffuse converter serves as the reference implementation. The export entry point only routes materials with registered converters through the registry; everything else still takes the legacy path until the remaining converter families land.
Mitsuba BSDF plugins now map to Blender node builders through a registry keyed by plugin name, mirroring the export side. Converters receive a MaterialBuilder that creates and links nodes, converts nested BSDFs and textures through the same registries, and writes float/color properties with constant and texture handling. Failures never abort an import: they are logged and produce a distinctive pink error BSDF. A diffuse converter serves as the reference implementation; plugins without a registered converter still take the legacy import path.
Map the Principled node onto Mitsuba's principled BSDF through the converter registry: reflective materials export the 'specular' parameter and a twosided wrapper, transmissive ones stay one-sided and share Blender's IOR as 'eta'. Emission becomes a separate area emitter dict, an Alpha below one wraps the BSDF in a mask, and a tangent-space Normal Map input wraps it in a normalmap adapter. Unlike the legacy exporter, roughness is passed through unchanged (Blender and Mitsuba both square the perceptual value internally) and Coat Roughness maps to the inverse of clearcoat_gloss.
Build a Principled node from Mitsuba's principled BSDF via the import registry. The mutually exclusive eta/specular parameters fill both the IOR and Specular IOR Level inputs consistently (F0 = 0.08 * specular level), spec_tint maps back to a grayscale Specular Tint color and clearcoat_gloss to the inverse Coat Roughness, mirroring the export converter so that principled materials round-trip.
Glossy exports to a twosided (rough)conductor, including Cycles' anisotropy mapping to alpha_u/alpha_v. Glass and Refraction export to dielectric, thindielectric or roughdielectric depending on IOR and roughness; Mitsuba has no transmission-only BSDF, so Refraction reuses the dielectric path with a warning. Transparent exports to null, or to a mask over a black diffuse when the color is tinted. Translucent exports to principledthin with full diffuse transmission, the closest match to a diffuse transmitter. Constant roughness values are squared to match Mitsuba's alpha convention; textured roughness passes through unchanged. The registry test that expected Glass to be unhandled now uses the Sheen node.
conductor and roughconductor import as Glossy nodes; the reflection color comes from evaluating the BSDF head-on (smooth conductors are delta lobes, so they are evaluated through an equivalent rough one). Constant alpha_u/alpha_v pairs are inverted back into Cycles' roughness/anisotropy parametrization. dielectric, thindielectric and roughdielectric import as Glass nodes, with named IOR materials mapped to their numeric values. plastic and roughplastic import as a Principled BSDF approximation. The registry error-boundary test now restores the plastic converter it overrides instead of deleting the registry entry.
Mix Shader maps to blendbsdf (constant or textured factor), or to mask when one input is a Transparent BSDF; a textured factor with the transparent input second becomes a blend against a null BSDF since the factor cannot be inverted. Add Shader supports one Emission plus one BSDF, or two Emissions with summed radiance. The Emission node becomes an area emitter dict (zero radiance falls back to a black diffuse since Mitsuba rejects black emitters) and Holdout is approximated by null.
twosided with one child converts the child directly (Blender materials are two-sided by default); with two children it selects between them with a Mix Shader driven by Backfacing. blendbsdf becomes a Mix Shader with the weight on Fac, mask becomes a Transparent BSDF when it wraps a black diffuse with constant opacity and a Mix Shader with a Transparent input otherwise, and null becomes a Transparent BSDF. Nested BSDFs whose plugins have not moved to the registry yet are bridged through the legacy writers via a temporary reroute node, so scenes like twosided(principled) keep importing at full fidelity until the remaining converter groups land.
Registers converters for Image Texture, Checker and Color Attribute nodes, translates Mapping/Texture Coordinate chains into to_uv transforms (conjugated with the UV flip of the mesh exporter), and provides convert_normal_input for BSDF converters to wrap their result in normalmap/bumpmap plugins as well as convert_environment_texture for the world exporter. Unlike the legacy exporter, referencing an image never mutates the datablock: files that exist on disk are copied verbatim and everything else is saved through a temporary copy, so image.file_format and filepath are left untouched. In render mode pixel data is passed to Mitsuba as in-memory bitmaps without touching the disk at all.
Registers texture converters for bitmap (with wrap mode, filter type, raw color space and a Mapping/Texture Coordinate reconstruction of to_uv), checkerboard (folding the UV flip and cell-size conventions into the Scale input or a Mapping node), scale (a multiplying Mix node) and mesh_attribute (Color Attribute or Attribute node). The normalmap and bumpmap BSDF wrappers become Normal Map and Bump nodes feeding the Normal input of the wrapped BSDF, with chained perturbations rerouted through the Bump node. Loaded images are cached by path so shared textures reuse one datablock, and missing files degrade to the socket default with a warning instead of aborting the material.
The flat absolutify only handled top-level filenames, missing bitmaps nestied inside compound textures (math expressions, color ramps, etc.). Walk the params dict recursively so every filename is resolved.
Archinteriors scenes have duplicate datablocks where one has a broken Windows path and another has the actual data. When an image has no pixel data, search for a sibling with the same base name and use it instead. Also force lazy load on packed siblings.
A null bsdf should not be used for emitter in Mitsuba. Instead we can use a BSDF that reflect 0 light.
…ble emitters Object with visible_camera=False and no emitter are not exported at all since they contribute nothing visible. Objects with an emitter keep the shape for lighting but use a black diffuse BSDF and set visible=false on the emitter so the surface doesn't appear in renders.
Blender lights and emitter-only materials export with a black diffuse BSDF, since Mitsuba shapes always need one. The importer only treated a null BSDF as a placeholder, so those shapes came back as emissive meshes instead of lights.
…convert_combine_color
…p through Mitsuba's plugin registry
The float branch safely probes for the Steps input whereas the vector branch didn't.
A lot of images in Blender's scene have sibling (e.g. image.png and image.005.png), those sibling are usually equals. Those are useful to catch as it can happens that a texture image references one of those file which doesn't exist but the siblings exists. However this is not always the case it has happened that two siblings are not equal and not used for the same material. We therefore need to check the image itself first in order to try to not fall in the false sibling case.
Add a log to warn the use that this image exported here might not be the correct one. Also force lazy load for any image, not just packed.
Resolve filename the same way as does scalar_from_socket.
…led spec_int value
Since every emitter now carries an explicit diffuse BSDF with albedo 0.0, no converter returns a None BSDF anymore, so the branch in add_material_to_dict that fell back to a shared empty-emitter-bsdf entry was unreachable. An emitter only material stores its black diffuse under its own material id instead, which also preserves the material name across roundtrip. Update the tests and the docstrings that still described the shared placeholder
wjakob
force-pushed
the
io-experiments
branch
from
September 6, 2026 10:21
e39c745 to
ef90a64
Compare
Member
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Replace the addon with a new architecture that handles complex production scenes.
Context
When Fable came out in summer 2026, @wjakob used some of his unused token balance to do automated passes over the Mitsuba/Blender plugin to handle various material nodes encountered in production assets. This work can be found on the
mitsuba-renderer:exporter-experimentbranch. The resulting code ended up having over-engineered parts and missing features. The goal after this has been to clean-up, simplify over-engineered code and add missing features. This work can be found on the branchArthur926564/io-experiments.Exporter
mitsuba_blender/plugins.Two export modes (strict and relaxed) control how unsupported nodes are handled:
strictwill render the whole material of an unsupported nodes as a pink principled bsdf namedERROR_BSDF.relaxedmode will warn and use the socket default.Importer
The importer converts Mitsuba XML scenes back into Blender, reconstructing materials as Cycles shader node trees.
Mesh
The old exporter passed the memory addresses of Blender's in-memory mesh structures to a
blendershape plugin inside Mitsuba, which mirrored Blender's struct layout per version. When that layout changed, the result was silent memory corruption rather than an error. Commit (3eeb16c) updated this in order to export meshes throughmi.Meshby reading triangulated geometry into numpy arrays and welding shared face corners. It also builds Mitsuba mesh objects directly using the official Blender's API, without any raw pointers.Commit
b601d5aupdated mesh export to match what has been updated from Mitsuba PR#1933. The exporter now builds meshes with the two-phasemi.Mesh(props)andfrom_corners()API, which welds and splits face corners in C++. All meshes of a scene go into one sharedmeshes.serializedfile.Other
pytest.bpyat the top level, making the plugins importable outside Blender.