Skip to content

Commit

Permalink
Better rgb/spectrum tag convertion in scene loading
Browse files Browse the repository at this point in the history
This commit makes changes in the interpretation of <rgb value=".."> and <spectrum value=".."> properties
during scene loading for both RGB and spectral modes.

The main outcome of these changes:

1. In spectral mode, <spectrum ..> properties remain untouched (used to
be multiplied by D65 for emitters, and normalized according to CIE
curves)

2. In RGB mode, <spectrum ..> properties are pre-integrated and
normalized according to CIE curves. For reflectance properties this
pre-integration will take into account the D65 illuminant in order to
mimic the behavior of spectral mode.
  • Loading branch information
Speierers committed Sep 12, 2022
1 parent 95d8a9b commit f883834
Show file tree
Hide file tree
Showing 38 changed files with 1,070 additions and 447 deletions.
1 change: 0 additions & 1 deletion docs/generate_plugin_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
'irregular',
'srgb',
'd65',
'srgb_d65',
'blackbody'
]

Expand Down
6 changes: 3 additions & 3 deletions docs/src/plugin_reference/section_emitters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ the following snippet instantiates a point light emitter that illuminates a sphe
<!-- .. scene contents .. -->

<emitter type="point">
<spectrum name="intensity" value="1"/>
<rgb name="intensity" value="1"/>
<point name="position" x="0" y="0" z="-2"/>
</emitter>

Expand Down Expand Up @@ -60,7 +60,7 @@ These are specified as children of the corresponding ``<shape>`` element:

<shape type="sphere">
<emitter type="area">
<spectrum name="radiance" value="1"/>
<rgb name="radiance" value="1"/>
</emitter>
</shape>
</scene>
Expand All @@ -75,7 +75,7 @@ These are specified as children of the corresponding ``<shape>`` element:
'emitter': {
'type': 'area',
'radiance': {
'type': 'spectrum',
'type': 'rgb',
'value': 1.0,
}
}
Expand Down
34 changes: 28 additions & 6 deletions docs/src/plugin_reference/section_spectra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,39 @@ and all different color modes. Each plugin is briefly summarized below.
- Spectral mode
* - ``<spectrum name=".." value="0.5"/>``
- :ref:`uniform <spectrum-uniform>`
- :ref:`srgb <spectrum-srgb>`
- :ref:`uniform <spectrum-uniform>`
- :ref:`d65 <spectrum-d65>`
* - ``<spectrum name=".." value="400:0.1, 700:0.2"/>``
- :ref:`uniform <spectrum-uniform>`
- :ref:`srgb_d65 <spectrum-srgb_d65>`
- :ref:`srgb <spectrum-srgb>`
- :ref:`regular <spectrum-regular>`/:ref:`irregular <spectrum-irregular>`
* - ``<spectrum name=".." filename=".."/>``
- :ref:`uniform <spectrum-uniform>`
- :ref:`srgb_d65 <spectrum-srgb_d65>`
- :ref:`srgb <spectrum-srgb>`
- :ref:`regular <spectrum-regular>`/:ref:`irregular <spectrum-irregular>`
* - ``<rgb name=".." value="0.5, 0.2, 0.5"/>``
- :ref:`srgb_d65 <spectrum-srgb_d65>`
- :ref:`srgb_d65 <spectrum-srgb_d65>`
- :ref:`srgb_d65 <spectrum-srgb_d65>`
- :ref:`d65 <spectrum-d65>`
- :ref:`srgb <spectrum-srgb>`
- :ref:`d65 <spectrum-d65>`

A uniform spectrum does not produce a uniform RGB response in sRGB (which
has a D65 white point). Hence giving ``<spectrum name=".." value="1.0"/>``
as the radiance value of an emitter will result in a purple-ish color. On the
other hand, using such spectrum for a BSDF reflectance value will result in
an object appearing white. Both RGB and spectral modes of Mitsuba 3 will
exhibit this behavior consistently. The figure below illustrates this for
combinations of inputs for the emitter radiance (here using a :ref:`constant <emitter-constant>` emitter)
and the BSDF reflectance (here using a :ref:`diffuse <bsdf-diffuse>` BSDF).

.. image:: ../../resources/data/docs/images/misc/spectrum_rgb_table.png
:width: 60%
:align: center

.. warning::

While it is possible to define unbounded RGB properties (such as the ``eta``
value for a :ref:`conductor BSDF <bsdf-conductor>`) using ``<rgb name=".." value=".."/>``
tag, it is highly recommended to directly define a spectrum curve (or use a
material from :num:`conductor-ior-list>`) as the spectral uplifting algorithm
implemented in Mitsuba won't be able to guarantee that the produced spectrum
will behave consistently in both RGB and spectral modes.
42 changes: 40 additions & 2 deletions include/mitsuba/core/properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ class MI_EXPORT_LIB Properties {
ref<Object> object = find_object(name);
if (!object->class_()->derives_from(MI_CLASS(Texture)))
Throw("The property \"%s\" has the wrong type (expected "
" <spectrum> or <texture>).", name);
" <spectrum> or <texture>).", name);
mark_queried(name);
return (Texture *) object.get();
} else if (p_type == Properties::Type::Float) {
Expand All @@ -247,7 +247,7 @@ class MI_EXPORT_LIB Properties {
return (Texture *) PluginManager::instance()->create_object<Texture>(props).get();
} else {
Throw("The property \"%s\" has the wrong type (expected "
" <spectrum> or <texture>).", name);
" <spectrum> or <texture>).", name);
}
}

Expand All @@ -270,6 +270,44 @@ class MI_EXPORT_LIB Properties {
return texture<Texture>(name);
}

/// Retrieve a texture multiplied by D65 if necessary (if the property is a float, create a D65 texture instead)
template <typename Texture>
ref<Texture> texture_d65(const std::string &name) const {
if (!has_property(name))
Throw("Property \"%s\" has not been specified!", name);

auto p_type = type(name);
if (p_type == Properties::Type::Object) {
ref<Object> object = find_object(name);
if (!object->class_()->derives_from(MI_CLASS(Texture)))
Throw("The property \"%s\" has the wrong type (expected "
" <spectrum> or <texture>).", name);
mark_queried(name);
return (Texture *) Texture::D65((Texture *) object.get()).get();
} else if (p_type == Properties::Type::Float) {
return (Texture *) Texture::D65(get<Float>(name)).get();
} else {
Throw("The property \"%s\" has the wrong type (expected "
" <spectrum> or <texture>).", name);
}
}

/// Retrieve a texture multiplied by D65 if necessary (use the provided texture if no entry exists)
template <typename Texture>
ref<Texture> texture_d65(const std::string &name, ref<Texture> def_val) const {
if (!has_property(name))
return def_val;
return texture_d65<Texture>(name);
}

/// Retrieve a texture multiplied by D65 if necessary (or create D65 texture with default value)
template <typename Texture, typename FloatType>
ref<Texture> texture_d65(const std::string &name, FloatType def_val) const {
if (!has_property(name))
return (Texture *) Texture::D65(def_val).get();
return texture_d65<Texture>(name);
}

/// Retrieve a 3D texture
template <typename Volume>
ref<Volume> volume(const std::string &name) const {
Expand Down
94 changes: 83 additions & 11 deletions include/mitsuba/core/spectrum.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,31 @@ struct Spectrum<dr::detail::MaskedArray<Value_>, Size_>
a unit-valued spectrum integrates to a luminance of 1.0 */
#define MI_CIE_Y_NORMALIZATION (1.0 / 106.7502593994140625)

/**
* D65 illuminant data from CIE, expressed as relative spectral power
* distribution, normalized relative to the power at 560nm.
*/
const float d65_table[MI_CIE_SAMPLES] = {
46.6383f, 49.3637f, 52.0891f, 51.0323f, 49.9755f, 52.3118f, 54.6482f,
68.7015f, 82.7549f, 87.1204f, 91.486f, 92.4589f, 93.4318f, 90.057f,
86.6823f, 95.7736f, 104.865f, 110.936f, 117.008f, 117.41f, 117.812f,
116.336f, 114.861f, 115.392f, 115.923f, 112.367f, 108.811f, 109.082f,
109.354f, 108.578f, 107.802f, 106.296f, 104.79f, 106.239f, 107.689f,
106.047f, 104.405f, 104.225f, 104.046f, 102.023f, 100.0f, 98.1671f,
96.3342f, 96.0611f, 95.788f, 92.2368f, 88.6856f, 89.3459f, 90.0062f,
89.8026f, 89.5991f, 88.6489f, 87.6987f, 85.4936f, 83.2886f, 83.4939f,
83.6992f, 81.863f, 80.0268f, 80.1207f, 80.2146f, 81.2462f, 82.2778f,
80.281f, 78.2842f, 74.0027f, 69.7213f, 70.6652f, 71.6091f, 72.979f,
74.349f, 67.9765f, 61.604f, 65.7448f, 69.8856f, 72.4863f, 75.087f,
69.3398f, 63.5927f, 55.0054f, 46.4182f, 56.6118f, 66.8054f, 65.0941f,
63.3828f, 63.8434f, 64.304f, 61.8779f, 59.4519f, 55.7054f, 51.959f,
54.6998f, 57.4406f, 58.8765f, 60.3125f
};

/* Scaling the CIE D65 spectrum curve by the following constant ensures that
it integrates to a luminance of 1.0 */
#define MI_CIE_D65_NORMALIZATION (1.0 / 98.99741751876255)

/**
* \brief Struct carrying color space tables with fits for \ref cie1931_xyz and
* \ref cie1931_y as well as corresponding precomputed ITU-R Rec. BT.709 linear
Expand All @@ -152,6 +177,8 @@ template <typename Float> struct CIE1932Tables {
);

srgb = xyz_to_srgb(xyz);

d65 = dr::load<FloatStorage>(d65_table, MI_CIE_SAMPLES);
}

void release() {
Expand All @@ -160,12 +187,15 @@ template <typename Float> struct CIE1932Tables {
initialized = false;

xyz = srgb = Color<FloatStorage, 3>();
d65 = FloatStorage();
}

/// CIE 1931 XYZ color tables
Color<FloatStorage, 3> xyz;
/// ITU-R Rec. BT.709 linear RGB tables
Color<FloatStorage, 3> srgb;
/// CIE D65 illuminant spectrum table
FloatStorage d65;

private:
bool initialized = false;
Expand Down Expand Up @@ -264,6 +294,37 @@ Float cie1931_y(Float wavelength, dr::mask_t<Float> active = true) {
return dr::select(active, dr::fmadd(w0, v0, w1 * v1), 0.f);
}

/**
* \brief Evaluate the CIE D65 illuminant spectrum given a wavelength in
* nanometers, normalized to ensures that it integrates to a luminance of 1.0.
*/
template <typename Float>
Float cie_d65(Float wavelength, dr::mask_t<Float> active = true) {
using UInt32 = dr::uint32_array_t<Float>;
using Float32 = dr::float32_array_t<Float>;
using ScalarFloat = dr::scalar_t<Float>;

Float t = (wavelength - (ScalarFloat) MI_CIE_MIN) *
((MI_CIE_SAMPLES - 1) /
((ScalarFloat) MI_CIE_MAX - (ScalarFloat) MI_CIE_MIN));

active &= wavelength >= (ScalarFloat) MI_CIE_MIN &&
wavelength <= (ScalarFloat) MI_CIE_MAX;

UInt32 i0 = dr::clamp(UInt32(t), dr::zeros<UInt32>(), UInt32(MI_CIE_SAMPLES - 2)),
i1 = i0 + 1;

auto tables = detail::get_color_space_tables<Float32>();
Float v0 = (Float) dr::gather<Float32>(tables.d65, i0, active);
Float v1 = (Float) dr::gather<Float32>(tables.d65, i1, active);

Float w1 = t - Float(i0),
w0 = (ScalarFloat) 1.f - w1;

Float v = dr::fmadd(w0, v0, w1 * v1) * (ScalarFloat) MI_CIE_D65_NORMALIZATION;

return dr::select(active, v, Float(0.f));
}
/**
* \brief Evaluate the ITU-R Rec. BT.709 linear RGB color matching functions
* given a wavelength in nanometers
Expand Down Expand Up @@ -300,26 +361,34 @@ Result linear_rgb_rec(Float wavelength, dr::mask_t<Float> active = true) {
dr::fmadd(w0, v0_b, w1 * v1_b)) & dr::mask_t<Result>(active, active, active);
}

/// Spectral responses to XYZ.
/**
* \brief Spectral responses to XYZ normalized according to the CIE curves to
* ensure that a unit-valued spectrum integrates to a luminance of 1.0.
*/
template <typename Float, size_t Size>
Color<Float, 3> spectrum_to_xyz(const Spectrum<Float, Size> &value,
const Spectrum<Float, Size> &wavelengths,
dr::mask_t<Float> active = true) {
dr::Array<Spectrum<Float, Size>, 3> XYZ = cie1931_xyz(wavelengths, active);
return { dr::mean(XYZ.x() * value),
dr::mean(XYZ.y() * value),
dr::mean(XYZ.z() * value) };
Color<Float, 3> res = { dr::mean(XYZ.x() * value),
dr::mean(XYZ.y() * value),
dr::mean(XYZ.z() * value) };
return res * MI_CIE_Y_NORMALIZATION;
}

/// Spectral responses to sRGB.
/**
* \brief Spectral responses to sRGB normalized according to the CIE curves to
* ensure that a unit-valued spectrum integrates to a luminance of 1.0.
*/
template <typename Float, size_t Size>
Color<Float, 3> spectrum_to_srgb(const Spectrum<Float, Size> &value,
const Spectrum<Float, Size> &wavelengths,
dr::mask_t<Float> active = true) {
dr::Array<Spectrum<Float, Size>, 3> rgb = linear_rgb_rec(wavelengths, active);
return { dr::mean(rgb.x() * value),
dr::mean(rgb.y() * value),
dr::mean(rgb.z() * value) };
Color<Float, 3> res = { dr::mean(rgb.x() * value),
dr::mean(rgb.y() * value),
dr::mean(rgb.z() * value) };
return res * MI_CIE_Y_NORMALIZATION;
}

/// Convert ITU-R Rec. BT.709 linear RGB to XYZ tristimulus values
Expand Down Expand Up @@ -443,7 +512,7 @@ MI_EXPORT_LIB void spectrum_to_file(const fs::path &path,
const std::vector<Scalar> &values);

/**
* \brief Tranform a spectrum into a set of equivalent sRGB coefficients
* \brief Transform a spectrum into a set of equivalent sRGB coefficients
*
* When ``bounded`` is set, the resulting sRGB coefficients will be at most 1.0.
* In any case, sRGB coefficients will be clamped to 0 if they are negative.
Expand All @@ -453,12 +522,15 @@ MI_EXPORT_LIB void spectrum_to_file(const fs::path &path,
* \param values
* Array with the values at the previously specified wavelengths
* \param bounded
* Boolean that controls if clamping is required.
* Boolean that controls if clamping is required. (default: True)
* \param d65
* Should the D65 illuminant be included in the integration. (default: False)
*/
template <typename Scalar>
MI_EXPORT_LIB Color<Scalar, 3>
spectrum_list_to_srgb(const std::vector<Scalar> &wavelengths,
const std::vector<Scalar> &values,
bool bounded = true);
bool bounded = true,
bool d65 = false);

NAMESPACE_END(mitsuba)
3 changes: 1 addition & 2 deletions include/mitsuba/core/xml.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ extern MI_EXPORT_LIB ref<Object> create_texture_from_spectrum(
const std::string &variant,
bool within_emitter,
bool is_spectral_mode,
bool is_monochromatic_mode,
bool spectral_unbounded);
bool is_monochromatic_mode);

/// Expands a node (if it does not expand it is wrapped into a std::vector)
extern MI_EXPORT_LIB std::vector<ref<Object>> expand_node(
Expand Down
Loading

0 comments on commit f883834

Please sign in to comment.