Update Feb 2016: Mike Mara and I published a paper at I3D'16 that fully-realizes the ideas originally sketched out in this blog post.
The method from the previous article could render surfaces that have varying reflectivity with respect to frequency of light ("color") and those that have partial coverage, such as the squares and transparent engine shown below. However, it could not handle transmission varying with frequency of light.
This new article describes a prototype of a simple extension to handle non-refractive colored transmission. For example, this can approximate the transmission through a green wine bottle, red blood, or a purple stained glass window.
The colored method still requires only one 3D pass over the transparent surfaces and one 2D compositing pass. It also has the same memory requirements: an RGB16F texture for accumulated color and an R8 texture for "revealage" (you can pack them into a single RGBA16F texture as well.)
The difference from the previous method is that the new one binds the final framebuffer's color texture as a third render target. It then modulates that color during the 3D transparent pass, since modulation is inherently independent of order. Doing so slightly increases the bandwidth cost of the transparent pass, but allows colored transmission.
This slight increase is undesirable if you know that your scene contains only monochrome transmission/coverage. Objects such as (colored) clouds, (colored) smoke, alpha-matted (colored) foliage, and colorless glass are cases that don't need the new method. I believe that the additional cost is easily acceptable for the quality increase if you need to render colored glass or fluids.
The new method is something that I just prototyped today, so I don't yet know its full strengths and failure cases. There's one theoretical concern: the implicit color normalization process by the average of averages is mathematically questionable. However, performing it would tint the highlights undesirably. When I've used the technique on diverse scenes and worked with it for a while, I'll write it up for formal publication.
3D Transparent Surface Pass
The render target setup for the transparent pass is as follows. The values written from the shader are based on the Reflected light, the coverage (a) and the Transmitted light. In practice, I use premultiplied-alpha for R, so I don't actually perform the explicit R.r * a, etc. computations.Render Target | Format | Clear | Src Blend | Dst Blend | Write ("Src") |
accum | RGBA16F | (0,0,0,0) | ONE | ONE | (R.r*a, R.g*a, R.b*a, a) * w |
revealage | R8 | (1,0,0,0) | ZERO | ONE_MINUS_SRC_COLOR | a |
color | RGB | - | ZERO | ONE_MINUS_SRC_COLOR | a * (1 - T.rgb) |
The "color" buffer is the final framebuffer's color buffer, containing the already-rendered opaque parts of the scene. It is not cleared before the transparent pass and can have any RGB format.
The 3D color shader differs by only the two lines (which handle the color buffer) from the monochrome algorithm:
The idea is to modulate the background image while accumulating the foreground color and coverage, and the apply averaged reflectance to the foreground.
#version330 layout(location = 0) out float4 _accum; layout(location = 1) out float _revealage; layout(location = 2) out float3 _modulate; /* NEW */ void writePixel(vec4 premultipliedReflect, vec3 transmit, float csZ) { /* NEW: Perform this operation before modifying the coverage to account for transmission. */ _modulate = premultipliedReflect.a * (vec3(1.0) - transmit); /* Modulate the net coverage for composition by the transmission. This does not affect the color channels of the transparent surface because the caller's BSDF model should have already taken into account if transmission modulates reflection. See McGuire and Enderton, Colored Stochastic Shadow Maps, ACM I3D, February 2011 http://graphics.cs.williams.edu/papers/CSSM/ for a full explanation and derivation.*/ premultipliedReflect.a *= 1.0 - (transmit.r + transmit.g + transmit.b) * (1.0 / 3.0); // Intermediate terms to be cubed float tmp = (premultipliedReflect.a * 8.0 + 0.01) * (-gl_FragCoord.z * 0.95 + 1.0); /* If a lot of the scene is close to the far plane, then gl_FragCoord.z does not provide enough discrimination. Add this term to compensate: tmp /= sqrt(abs(csZ)); */ float w = clamp(tmp * tmp * tmp * 1e3, 1e-2, 3e2); _accum = premultipliedReflect * w; _revealage = premultipliedReflect.a; }
The idea is to modulate the background image while accumulating the foreground color and coverage, and the apply averaged reflectance to the foreground.
2D Compositing Pass
Because the 3D pass already modulated the background, the compositing pass now needs only to add the weighted average of reflected color on top. The compositing shader run on the final framebuffer is run with the blending mode src = ONE_MINUS_SRC_ALPHA, dst = ONE:#version330 /* sum(rgb * a, a) */ uniform sampler2D accumTexture; /* prod(1 - a) */ uniform sampler2D revealageTexture; out float4 result; void main() { int2 C = int2(gl_FragCoord.xy); float revealage = texelFetch(revealageTexture, C, 0).r; if (revealage == 1.0) { // Save the blending and color texture fetch cost discard; } float4 accum = texelFetch(accumTexture, C, 0); // Suppress overflow if (isinf(maxComponent(abs(accum)))) { accum.rgb = float3(accum.a); } // dst' = (accum.rgb / accum.a) * (1 - revealage) + dst // [dst has already been modulated by the transmission colors and coverage and the blend mode // inverts revealage for us] result = float4(accum.rgb / max(accum.a, 0.00001), revealage); }
Example
Here's a simple example comparing the results of this new colored method to sorted transparency. Sorted transparency is significantly slower because it requires two draw calls per sorted primitive, one to modulate the background and one to add the reflected light.
Both images show two glass cages on a cobblestone ground (in the Uffizi Gallery, because this is computer graphics research). The red glass cage is closer to the viewer than the yellow one. The top image is the new fast, colored, and order-independent method. You can see the two buffers inset that it uses. The bottom image is a sorted transparency reference image. The order-independent method clearly captures most of the phenomena, including the intended tinting of the background where seen through the foreground. The primary difference is that the glossy highlights are dimmer (because they're averaged) in the new method. I don't show the old OIT method here...it would have both cages appear to be made from clear instead of colored glass.
Both images show two glass cages on a cobblestone ground (in the Uffizi Gallery, because this is computer graphics research). The red glass cage is closer to the viewer than the yellow one. The top image is the new fast, colored, and order-independent method. You can see the two buffers inset that it uses. The bottom image is a sorted transparency reference image. The order-independent method clearly captures most of the phenomena, including the intended tinting of the background where seen through the foreground. The primary difference is that the glossy highlights are dimmer (because they're averaged) in the new method. I don't show the old OIT method here...it would have both cages appear to be made from clear instead of colored glass.
New fast colored, order-independent transparency |
Slow, sorted transparency reference image |
Morgan McGuire (@morgan3d) is a professor of Computer Science at Williams College, a researcher at NVIDIA, and a professional game developer. His most recent games are Rocket Golfing and work on the Skylanders series. He is the author of the Graphics Codex, an essential reference for computer graphics now available in iOS and Web Editions.