{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Custom Python plugin" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "In Mitsuba it easy to add custom code for BSDFs, integrators, emitters, sensors, and more. This tutorial will show you how to create custom plugins in Python and register them for use.\n", "\n", "To illustrate this, we are going to implement a new *tinted dielectric* BSDF, that behaves much like a regular [dielectric BSDF][1] but adds a colorful tint to the reflections at grazing angle. We will then register this new BSDF and use it to render a simple scene.\n", "\n", "
BSDF
][2]. This allows us to override the constructor method as well as [sample()
][3], [eval()
][4] and [pdf()
][5].\n",
"\n",
"The constructor takes a [Properties
][6] object as an argument, which can be used to read parameters defined in the XML scene description or passed in the [load_dict()
][7] dictionary. Here we read the index of refraction ratio `eta` as well as the tint color `tint` from `props`. In the constructor, we also properly set the BSDF members used in other methods like `m_flags` and `m_components`.\n",
"\n",
"Similarly to the regular dielectric BSDF, the `eval()` and `pdf()` methods of our custom BSDF should always return `0.0` and never be called as it is a degenerate BSDF described by a Dirac delta distribution.\n",
"\n",
"Regarding the `sample()` method, apart from the computation of the tinted reflection value `value_r`, the rest of code should be identical to the [C++ implementation][0] of `dielectric`.\n",
"\n",
"Note that it is also possible to override the [to_string()
][8] method which is called in any printing/logging routine.\n",
"\n",
"Finally, we override the implementation of the [traverse()
][9] and [parameters_changed()
][10] methods to expose the `tint` parameter via the *traverse* mechanism. This will allow us to edit this parameter after the BSDF is instanciated.\n",
"\n",
"[0]: https://github.com/mitsuba-renderer/mitsuba3/blob/master/src/bsdfs/dielectric.cpp \n",
"[1]: https://mitsuba.readthedocs.io/en/latest/src/generated/plugins_bsdfs.html#smooth-dielectric-material-dielectric\n",
"[2]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.BSDF\n",
"[3]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.BSDF\n",
"[4]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.BSDF\n",
"[5]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.BSDF\n",
"[6]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.Properties\n",
"[7]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.load_dict\n",
"[8]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.Object.to_string\n",
"[9]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.Object.traverse\n",
"[10]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.Object.parameters_changed"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"class MyBSDF(mi.BSDF):\n",
" def __init__(self, props):\n",
" mi.BSDF.__init__(self, props)\n",
"\n",
" # Read 'eta' and 'tint' properties from `props`\n",
" self.eta = 1.33\n",
" if props.has_property('eta'):\n",
" self.eta = props['eta']\n",
"\n",
" self.tint = props['tint']\n",
"\n",
" # Set the BSDF flags\n",
" reflection_flags = mi.BSDFFlags.DeltaReflection | mi.BSDFFlags.FrontSide | mi.BSDFFlags.BackSide\n",
" transmission_flags = mi.BSDFFlags.DeltaTransmission | mi.BSDFFlags.FrontSide | mi.BSDFFlags.BackSide\n",
" self.m_components = [reflection_flags, transmission_flags]\n",
" self.m_flags = reflection_flags | transmission_flags\n",
"\n",
" def sample(self, ctx, si, sample1, sample2, active):\n",
" # Compute Fresnel terms\n",
" cos_theta_i = mi.Frame3f.cos_theta(si.wi)\n",
" r_i, cos_theta_t, eta_it, eta_ti = mi.fresnel(cos_theta_i, self.eta)\n",
" t_i = dr.maximum(1.0 - r_i, 0.0)\n",
"\n",
" # Pick between reflection and transmission\n",
" selected_r = (sample1 <= r_i) & active\n",
"\n",
" # Fill up the BSDFSample struct\n",
" bs = mi.BSDFSample3f()\n",
" bs.pdf = dr.select(selected_r, r_i, t_i)\n",
" bs.sampled_component = dr.select(selected_r, mi.UInt32(0), mi.UInt32(1))\n",
" bs.sampled_type = dr.select(selected_r, mi.UInt32(+mi.BSDFFlags.DeltaReflection),\n",
" mi.UInt32(+mi.BSDFFlags.DeltaTransmission))\n",
" bs.wo = dr.select(selected_r,\n",
" mi.reflect(si.wi),\n",
" mi.refract(si.wi, cos_theta_t, eta_ti))\n",
" bs.eta = dr.select(selected_r, 1.0, eta_it)\n",
"\n",
" # For reflection, tint based on the incident angle (more tint at grazing angle)\n",
" value_r = dr.lerp(mi.Color3f(self.tint), mi.Color3f(1.0), dr.clamp(cos_theta_i, 0.0, 1.0))\n",
"\n",
" # For transmission, radiance must be scaled to account for the solid angle compression\n",
" value_t = mi.Color3f(1.0) * dr.sqr(eta_ti)\n",
"\n",
" value = dr.select(selected_r, value_r, value_t)\n",
" \n",
" return (bs, value)\n",
"\n",
" def eval(self, ctx, si, wo, active):\n",
" return 0.0\n",
"\n",
" def pdf(self, ctx, si, wo, active):\n",
" return 0.0\n",
"\n",
" def eval_pdf(self, ctx, si, wo, active):\n",
" return 0.0, 0.0\n",
"\n",
" def traverse(self, callback):\n",
" callback.put_parameter('tint', self.tint, mi.ParamFlags.Differentiable)\n",
"\n",
" def parameters_changed(self, keys):\n",
" print(\"🏝️ there is nothing to do here 🏝️\")\n",
"\n",
" def to_string(self):\n",
" return ('MyBSDF[\\n'\n",
" ' eta=%s,\\n'\n",
" ' tint=%s,\\n'\n",
" ']' % (self.eta, self.tint))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plugin registration\n",
"\n",
"There's only one more thing to do before we can use our custom BSDF in scenes. We need to register it in the system so it can be used. This can be done by calling the [register_bsdf()
][1] function and specifying the name to be used to instantiate this plugin. The function takes a *constructor lambda* function as the second parameter.\n",
"\n",
"\n",
"traverse
][1] mechanism.\n",
"\n",
"[1]: https://mitsuba.readthedocs.io/en/latest/src/api_reference.html#mitsuba.traverse"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"SceneParameters[\n",
" -------------------------------------------------------------------------------\n",
" Name Flags Type Parent\n",
" -------------------------------------------------------------------------------\n",
" light.radiance.value ∂ Float UniformSpectrum\n",
" sensor.near_clip float PerspectiveCamera\n",
" sensor.far_clip float PerspectiveCamera\n",
" sensor.shutter_open float PerspectiveCamera\n",
" sensor.shutter_open_time float PerspectiveCamera\n",
" sensor.film.size ScalarVector2u HDRFilm\n",
" sensor.film.crop_size ScalarVector2u HDRFilm\n",
" sensor.film.crop_offset ScalarPoint2u HDRFilm\n",
" sensor.x_fov Float PerspectiveCamera\n",
" sensor.to_world Transform4f PerspectiveCamera\n",
" sphere.to_world Transform4f Sphere\n",
" sphere.bsdf.tint ∂ Array3f64 BSDF\n",
"]"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"params = mi.traverse(scene)\n",
"params"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can then update the `tint` value:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"🏝️ there is nothing to do here 🏝️\n"
]
}
],
"source": [
"key = 'sphere.bsdf.tint'\n",
"params[key] = mi.ScalarColor3f(0.9, 0.2, 0.2)\n",
"params.update();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When re-rendering this scene, we now see that the new tint is indeed used!"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/html": [
"