Cycles: add distance-dependent transmission color in Principled BSDF #111806

Open
Weizhen Huang wants to merge 1 commits from weizhen/blender:principled-transmission-tint into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
Member

attenuate the ray color so that a white light becomes exactly the base color after traveling the distance specified in Transmission Depth. When set to zero, the tint is constant as before.

This is realized by adjusting the transmission and reflection color via sd->ray_length when the mesh is backfacing. The result is very similar as attaching an additional Volume Absorption node to the Material Output. However, the main point of the new parameter is to be able to control the exact color at certain depth. This is currently very difficult via the Volume Absorption node because it uses 1.0 - weight as the extinction coefficient (see fa34992def/intern/cycles/kernel/svm/closure.h (L832)). If change that to -log(weight), then Density is equivalent to 1.0 / transmission_depth:

Transmission Depth 0.1 Volume Absorption Density 10
13.52s 15.59 s

The highlight difference near the eyes is because Suzanne mesh is not closed.
Suzanne eye
This poses the question that the behaviour is not well-defined when the mesh is not closed or when there are intersecting regions (as in nested dielectrics). In these cases the volumetric approach is arguably better. However, the current approach is simpler to implement, and seems faster and less noisy than the volume approach. Maybe a more sophisticated logic can be applied than just checking if the surface is backfacing (camera ray? The number of transmission events?).

Another problem is that there is already a Transmission Depth in the Light Path Node, which means something different, so the naming of the new socket seems confusing, but I couldn't think of a better name yet.

attenuate the ray color so that a white light becomes exactly the base color after traveling the distance specified in Transmission Depth. When set to zero, the tint is constant as before. This is realized by adjusting the transmission and reflection color via `sd->ray_length` when the mesh is backfacing. The result is very similar as attaching an additional Volume Absorption node to the Material Output. However, the main point of the new parameter is to be able to control the exact color at certain depth. This is currently very difficult via the Volume Absorption node because it uses `1.0 - weight` as the extinction coefficient (see https://projects.blender.org/blender/blender/src/commit/fa34992def0a162bddbec5d903ef043d547fc4f2/intern/cycles/kernel/svm/closure.h#L832). If change that to `-log(weight)`, then `Density` is equivalent to `1.0 / transmission_depth`: | Transmission Depth 0.1 | Volume Absorption Density 10 | | -------- | -------- | | ![13.52s](https://projects.blender.org/attachments/6768c9b4-8afa-4c8c-82b2-ed82647c904e) | ![15.59 s](https://projects.blender.org/attachments/6c1d4b53-dde7-4b4c-a0a3-305008659f8f) | The highlight difference near the eyes is because Suzanne mesh is not closed. ![Suzanne eye](https://projects.blender.org/attachments/82db3975-c5d6-441f-9fb2-af93354e0ef2) This poses the question that the behaviour is not well-defined when the mesh is not closed or when there are intersecting regions (as in nested dielectrics). In these cases the volumetric approach is arguably better. However, the current approach is simpler to implement, and seems faster and less noisy than the volume approach. Maybe a more sophisticated logic can be applied than just checking if the surface is backfacing (camera ray? The number of transmission events?). Another problem is that there is already a Transmission Depth in the Light Path Node, which means something different, so the naming of the new socket seems confusing, but I couldn't think of a better name yet.
Weizhen Huang added 1 commit 2023-09-01 21:34:34 +02:00
Weizhen Huang added this to the Render & Cycles project 2023-09-01 22:11:34 +02:00
Member

Just for reference, a benefit of this approach is that it works with shadow caustics (Manifold Next Event Estimation) without further additions or modifications to the shadow caustics code. Adding a volumetric shader to the object does not work with shadow caustics.


This is probably a known issue or limitation of this technique. It seems objects inside transmissive objects render incorrectly (the transmission depth is the wrong length for objects inside transmissive objects).

There are two images below. "Actual result" and "Expected result". The "Actual result" is a transmissive cube with a perfect mirror in the middle. As you can see, there is a bright bit inside the cube. This comes from the transmissive depth being incorrect as it only takes into consideration the length from the mirror to the backface of the cube, not the combined length of cube to mirror + mirror to backface of cube. The "Expected result" is what I would expect in the real world, and this was achieved in Blender by removing the mirror inside the cube.

Actual result Expected Result
Incorrect absorption for objects inside glass.jpg Expected result for absorption for objects inside glass.jpg

.blend file: Transmission Depth Inside Object Example.blend

Just for reference, a benefit of this approach is that it works with shadow caustics (Manifold Next Event Estimation) without further additions or modifications to the shadow caustics code. Adding a volumetric shader to the object does not work with shadow caustics. --- This is probably a known issue or limitation of this technique. It seems objects inside transmissive objects render incorrectly (the transmission depth is the wrong length for objects inside transmissive objects). There are two images below. "Actual result" and "Expected result". The "Actual result" is a transmissive cube with a perfect mirror in the middle. As you can see, there is a bright bit inside the cube. This comes from the transmissive depth being incorrect as it only takes into consideration the length from the mirror to the backface of the cube, not the combined length of cube to mirror + mirror to backface of cube. The "Expected result" is what I would expect in the real world, and this was achieved in Blender by removing the mirror inside the cube. |Actual result|Expected Result| |-|-| |![Incorrect absorption for objects inside glass.jpg](/attachments/ddf65a2d-72c8-40a3-b0d3-45df120443ef)|![Expected result for absorption for objects inside glass.jpg](/attachments/a86e264c-ae2e-4127-b42e-51c914d33d57)| .blend file: [Transmission Depth Inside Object Example.blend](/attachments/dd1d5342-a3b8-483e-a3c0-46b792c3b971)
Member

Another problem is that there is already a Transmission Depth in the Light Path Node, which means something different, so the naming of the new socket seems confusing, but I couldn't think of a better name yet.

Arnold and Redshift calls this feature "Depth". I assume users understand that it's "Transmission Depth" due to context in the UI. Maybe once grouping and separators are added to nodes, the Principled BSDF could have a "Transmission" group with "Depth" as one of the options?

Renderman calls this feature "Transmittance Distance".

Mantra calls this feature "At Distance" in the "Absorption" section of their material. So it's probably "Absorption At Distance".

Here's some other names I thought of "Transmission Absorption Depth" or "Absorption Depth"?


While looking at what other render engines called this feature. I found that Mantra appears to do this feature differently.

Note: This is just speculation on how this feature works simply based on reading the documentation for Mantra.

Unlike this pull request where the absorption is applied based on the ray length from the previous interaction to the back face of the material. Mantra seems to enter a transmissive material and mark the ray as "inside transmissive material X". Each time the ray interacts with another surface, the absorption is applied based on the distance the ray traveled from the previous intersection. This keeps repeating until a light is hit, or the ray leaves the transmissive material by hitting a back face or by entering a higher priority nested dieletric. I assume this takes advantage of the medium tracking feature required for nested dieletrics to work, and hence probably isn't a viable feature to implement into Cycles until the ground work for nested dieletrics are implemented.

The approach I described above would solve some issues, and would support using this feature in conjunction with nested dielectrics, but there's still some weird behavior in cases like when the mesh is not closed.

> Another problem is that there is already a Transmission Depth in the Light Path Node, which means something different, so the naming of the new socket seems confusing, but I couldn't think of a better name yet. Arnold and Redshift calls this feature "Depth". I assume users understand that it's "Transmission Depth" due to context in the UI. Maybe once grouping and separators are added to nodes, the Principled BSDF could have a "Transmission" group with "Depth" as one of the options? Renderman calls this feature "Transmittance Distance". Mantra calls this feature "At Distance" in the "Absorption" section of their material. So it's probably "Absorption At Distance". Here's some other names I thought of "Transmission Absorption Depth" or "Absorption Depth"? --- While looking at what other render engines called this feature. I found that Mantra appears to do this feature differently. Note: This is just speculation on how this feature works simply based on reading the documentation for Mantra. Unlike this pull request where the absorption is applied based on the ray length from the previous interaction to the back face of the material. Mantra seems to enter a transmissive material and mark the ray as "inside transmissive material X". Each time the ray interacts with another surface, the absorption is applied based on the distance the ray traveled from the previous intersection. This keeps repeating until a light is hit, or the ray leaves the transmissive material by hitting a back face or by entering a higher priority nested dieletric. I assume this takes advantage of the medium tracking feature required for nested dieletrics to work, and hence probably isn't a viable feature to implement into Cycles until the ground work for nested dieletrics are implemented. The approach I described above would solve some issues, and would support using this feature in conjunction with nested dielectrics, but there's still some weird behavior in cases like when the mesh is not closed.
Author
Member

Maybe once grouping and separators are added to nodes, the Principled BSDF could have a "Transmission" group with "Depth" as one of the options?

I think this is a good idea. “At Distance” also sounds very intuitive to me.

I am aware of the limitations @Alaska mentioned, so I’m also not sure if this is the approach we want to take. But I think it works well as an initial implementation, for the cases it doesn’t work, people can still use the volume, so there is no loss.
However, the volume approach looks unusually slow and noisy to me for such a simple scene, maybe I can dig in there a bit and see if there’s any issue with the volume. Or, we can revisit this patch once we have a better idea of how to support nested dielectrics.

> Maybe once grouping and separators are added to nodes, the Principled BSDF could have a "Transmission" group with "Depth" as one of the options? I think this is a good idea. “At Distance” also sounds very intuitive to me. I am aware of the limitations @Alaska mentioned, so I’m also not sure if this is the approach we want to take. But I think it works well as an initial implementation, for the cases it doesn’t work, people can still use the volume, so there is no loss. However, the volume approach looks unusually slow and noisy to me for such a simple scene, maybe I can dig in there a bit and see if there’s any issue with the volume. Or, we can revisit this patch once we have a better idea of how to support nested dielectrics.
Author
Member

Turns out that the excessive noise is because I enabled path guiding. Without path guiding both are much less noisy.
The 12% overhead because of the extra volume closure seems expected though.

Turns out that the excessive noise is because I enabled path guiding. Without path guiding both are much less noisy. The 12% overhead because of the extra volume closure seems expected though.

I think we should use actual volume rendering here. In Standard Surface and upcoming OpenPBR, there are controls not only for absorption but also scattering. And dealing correctly with internal blocks and transparency I guess will be tricky even for the absorption only case. It will be slower but it seems acceptable.

The easiest way to implement this would be to modify the shader graph on the host side in PrincipledBsdfNode::expand. It would involve adding volume nodes linked to the Volume material output, using Add Shader nodes if something is already connected.

One issue with scattering and sharp refraction is that you end up with a volume caustic which is very noisy to render. The trick to sidestep this would be to either increase roughness on exit from a volume bounce, or to use a transparent BSDF instead of refraction. A transparent BSDF is probably most efficient.

I think we should use actual volume rendering here. In Standard Surface and upcoming OpenPBR, there are controls not only for absorption but also scattering. And dealing correctly with internal blocks and transparency I guess will be tricky even for the absorption only case. It will be slower but it seems acceptable. The easiest way to implement this would be to modify the shader graph on the host side in `PrincipledBsdfNode::expand`. It would involve adding volume nodes linked to the Volume material output, using Add Shader nodes if something is already connected. One issue with scattering and sharp refraction is that you end up with a volume caustic which is very noisy to render. The trick to sidestep this would be to either increase roughness on exit from a volume bounce, or to use a transparent BSDF instead of refraction. A transparent BSDF is probably most efficient.
Author
Member

I don't think scattering would be a problem (do we want anisotropy or not?) but internal blocks might be, so I want to figure out how to do nested dielectrics first. 12% seems a lot to me.

I don't think scattering would be a problem (do we want anisotropy or not?) but internal blocks might be, so I want to figure out how to do nested dielectrics first. 12% seems a lot to me.

Scattering would support anisotropy.
https://autodesk.github.io/standard-surface/#closures/speculartransmission

We can perhaps optimize the fixed color homogeneous volume absorption case somehow, without it being Principled BSDF specific. It could skip shade_volume for that case, and instead at the start of shade_surface loop over the volume stack and compute the absorption, regardless of which material it is shading. That way it could correctly deal with internal geometry.

Scattering would support anisotropy. https://autodesk.github.io/standard-surface/#closures/speculartransmission We can perhaps optimize the fixed color homogeneous volume absorption case somehow, without it being Principled BSDF specific. It could skip `shade_volume` for that case, and instead at the start of `shade_surface` loop over the volume stack and compute the absorption, regardless of which material it is shading. That way it could correctly deal with internal geometry.
This pull request has changes conflicting with the target branch.
  • intern/cycles/kernel/osl/shaders/node_principled_bsdf.osl
  • intern/cycles/kernel/svm/closure.h
  • intern/cycles/scene/shader_nodes.cpp
  • intern/cycles/scene/shader_nodes.h
  • source/blender/gpu/shaders/material/gpu_shader_material_principled.glsl
  • source/blender/nodes/shader/nodes/node_shader_bsdf_principled.cc

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u principled-transmission-tint:weizhen-principled-transmission-tint
git checkout weizhen-principled-transmission-tint
Sign in to join this conversation.
No reviewers
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No Assignees
3 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#111806
No description provided.