Thoughts on making Cycles into a spectral renderer

I think all of the domain specific terminology is throwing me off a bit here, and I don’t yet see how to access the PathState from within bsdf_eval since it isn’t an argument.

You need to pass the PathState or the wavelength info as an argument to the functions that need it then, all the way to bsdf_eval and similar.

Most BSDFs do not need to be aware of wavelengths, just a few would need it I think. For most it’s a matter of converting the returned RGB value.

Thanks for that.

I’m having trouble understanding this. Is the returned RGB value of a closure just the value of the colour input on the node, or the resulting colour after simulating that node? Sorry I’m not quite sure how to say what I’m thinking.

The wavelengths which have hijacked the place of RGB in the sampling side of things would have to be taken into account for any calculation which has a colour involved, I think…

Apologies for all the questions, I’m sure you have other things to be doing.

A closure has an associated RGB weight. Additionally the BSDF can also return a color from evaluation or sampling (though most of them return a grayscale value). These are multiplied together to get the final resulting RGB color from evaluating or sampling the closure.

Some the result is often the color input of the node multiplied by some scalar, but not always.

Okay that’s what I needed to know. If that’s the case, each BSDF will need to know the wavelengths of the three channels so that it can determine its reflectivity for each one based on the spectral lookup from it’s associated RGB value.

What is that ‘associated RGB weight’ represented by in the diffuse BSDF? I still can’t quite figure that bit out.

It is sc->weight, as used in _shader_bsdf_multi_eval_branched and _shader_bsdf_multi_eval.

I’m having no progress with modifying the BSDFs unfortunately.

I’m not sure if I’ve misunderstood what the sc->weight means, since it doesn’t seem to represent the RGB triplet in the BSDF node. All I need to do is replace the RGB coming into the BSDF nodes with 3 new values based on the wavelengths being calculated, but I can’t see where I would do that other than in the shader OSL such as kernel/shaders/node_diffuse_bsdf.osl in which I don’t seem to be able to get the wavelengths.

bsdf_diffuse_sample doesn’t seem to utilise sc->weight at all, in fact.

Except you almost never want this, as you are nearly always dealing with a reference additive RGB gamut. Further, XYZ and RGB are virtually synonymous the moment you define the colour space. So essentially any decent upsampling approach is already dealing with XYZ.

Remember, the goal here is an upsampled spectral, which will always be coming from an input colourspace. It would be quite important to keep it based on the primaries of said space or else one risks wonky gamut hulls for a particular input.

The shape of the spectral is also up for debate, as I believe the need for a 100% reflectance is a naive one[1]. A parametric approach is important as one is never aware of the spectral composition goal, and as such, there isn’t always an optimal composition.

[1] I believe it is too easy to conflate a reflected value and an emissive need. Basing on primaries, the upsample would be based on emissive properties, and I believe the important facet is that which is important to any well designed RGB; a sum of Y to 1.0. From there, subtractive values should be able to be generated with little concern for the peak reflectivity, given no physically plausible materials have albedos of 1.0 etc. Of course we can make them, but I believe that particular facet is a confusion of protocol.

It is multiplied with the result from BSDF evaluation later, in _shader_bsdf_multi_eval_branched and _shader_bsdf_multi_eval. It could be moved into the specific BSDF functions if that makes the code cleaner, having it outside just saves a few operations in some cases.

I need to confirm, is sc->value the ‘colour’ of the closure? If I am to get a Diffuse BSDF node and colour it with 1, 0, 0, will the sc value match that?

sc->weight typically matches the color input of the BSDF node. It also includes any weights from mixing the BSDF with other nodes. Also, for example the principled hair BSDF stores the base color elsewhere and leaves sc->weight only for mixing weights.

The important thing is that it’s the product of sc->weight and the return value from bsdf_eval that needs to be converted to spectral.

I’ve updated that part of code and I’m getting colour in diffuse closures but glossy and emission aren’t reacting to the change. The diffuse component of principled is working too. Any idea why that might be the case?

EDIT: Looks like glossy doesn’t take the colour for the camera rays when roughness is 0 (the metal looks grey) but bounced light does take the colour, as does the metal when roughness increases. Very strange behaviour.

It is as though diffuse is the only type that actually works like this but I don’t know where to look to confirm those suspicions.


As you can see, the principled cubes look right, but the emission is supposed to be pure green and the glossy ball should be pink. It seems like some closures are calculated in a different way, but I’m not entirely sure how.

3 Likes

Have you looked at the paper I linked? That approach works on (nearly) all of XYZ uniformly, and it gets really good results. It defines a region of XYZ values with plausible smooth spectra, so not literally every XYZ value gets a reflectance spectrum (as is to be expected), but the coverage is really good. And as far as I understand, it’s one transform to rule them all. No need to specialize based on your chosen color space.
Because that’s already taken care of by restricting the color selector to the corresponding space. Out of Gamut colors wouldn’t be accessible in the first place.

That paper is also very specifically about reflectance spectra, not about emission.

Just for reference it’d be cool if you could provide side-by-sides of the equivalent in Cycles master.
That glossy behavior is indeed very odd.
Great start though!

Make sure the conversion from RGB to spectral also happens for the RGB value returned from bsdf_sample(). With MIS, sharp reflection is mostly handled by sampling the BSDF, while diffuse reflection is mostly handled by sampling the light and then evaluating the BSDF with bsdf_eval().

kernel_shader.h in shader_bsdf_sample()

745 float3 eval;
...
748 label = bsdf_sample(kg, sd, sc, randu, randv, &eval, omega_in, domega_in, pdf);

Is it the value of eval which needs converting, or the sc->weight in this situation? One of them, it seems, is grey while the other somehow contains the colour, but it seems to differ from location to location as to which one it is. it seems to return an int, and I’m not quite sure why… What does a ‘label’ represent?

The only things that aren’t working now are principled when metallic, and emission, as well as all volume effects.

Lights seem to take on the correct colour when denoising is on, but regular materials lose their colour… I think I’ve gotten confused somewhere.