Global Variable | Type | Represents |
---|---|---|
E | point | Camera position R |
P | point | Ray origin R |
I | vector | Ray direction R |
Ci | color | Ray color RW |
Oi | color | Ray opacity RW |
A volume shader is the last shader called by the renderer for a cast camera ray. In it, you
can change the output color and opacity of the last called surface shader, before they get
imprinted on the pixel. Take note that in order to see a volume shader, you must put a surface
in the background, and attach the volume shader to it via the RIB call
volume fog( float distance = 100, thickness = 1; color fogcolor = 1 ) { float findex = thickness * (1 - pow(3,-length(I) / distance)); Ci = mix( Ci, fogcolor, findex ); } |
A widely used method of volume shading is the so-called ray marcher, in which you move along the camera ray direction in small steps and collect atmospheric texture values along the way. Naturally, the smaller your step distance, the finer and slower your rendering will be.
volume litsmoke( float stepsize = 0.2, thickness = 0.02; color smokecolor = 1 ) { point pos; vector Inorm; float Ilen, opa, tracklen; Ilen = length(I); Inorm = I/Ilen; opa = 0; tracklen = stepsize; while( (opa < 1) && (tracklen < Ilen) ) { pos = E + tracklen*Inorm; illuminance( pos, vector (0,1,0), PI ) opa += comp(Cl,0) * thickness * stepsize; tracklen += stepsize; } Ci = mix( Ci, smokecolor, opa ); } |
The illuminance function samples the lights that shine on the position pos, 180° (PI radians) around the direction (0, 1, 0); we use it here to look around in every step for light rays.
Next, a slightly more complex use of this technique to create a smoky region:
![]() |
/* Note: This shader only works properly in BMRT. It should be applied to an inside-facing, single-sided sphere placed at the origin. You should also synchronize vradius with the sphere's radius */ volume volsmoke( float stepsize = 0.05, thickness = 0.7, texscale = 4, iteration = 7, vradius = 10; color cloudcolor = color (0.75, 0.8, 0.85) ) { point pos; vector Inorm; float Pang, inlen, outlen, Ilen, opa, smk, count, clips[2]; uniform float i, j; Ilen = length(I); if( length(vector E) > vradius ) /*camera outside sphere*/ { Pang = acos(normalize(vector P) . normalize(I)); /* sine rule */ inlen = vradius * sin(PI - 2*Pang) / sin(Pang); outlen = Ilen - inlen; } else /*camera inside sphere*/ { option( "Clipping", clips ); outlen = clips[0]; } opa = 0; Inorm = I/Ilen; count = 1; while( (opa < 1) && (outlen < Ilen) ) { pos = E + outlen*Inorm; smk = 0; for( i=1; i<iteration; i+=1 ) smk += (0.5 - abs( float noise((pos/texscale + vector (45.6, 100.3, 12.4)) * i) - 0.5 )) / (2 * i); smk = clamp( smk - 0.47, 0, 1 ); opa += smk; outlen += stepsize; count += 1; } opa = thickness * opa / count; Oi = opa; Ci = mix( Ci, cloudcolor, opa ); } |
As you might gather, ray-marching is slow. If the volume is the only thing you're rendering, you could tighten the rendering time by reducing PixelSamples to 1 (no antialiasing) and increasing the volume object's ShadingRate. Also, since the smoke is blurry, you might consider rendering the image in a smaller resolution, and then blowing it up later.
It's possible to extend the above shader to create self-shadowing. The idea is to trace a ray from the current sampling point to every light in the scene, and then ray-march along that ray to compute how much light penetrates the smoke to reach that point. You could also solidify the smoke into a virtual surface called a hypertexture. Both of these effects are very expensive to compute, and are somewhat advanced in flavour. So we'll leave them out for this class. If you're interested in these and other volume rendering techniques, you should check out the book Advanced Renderman Creating CGI for Motion Pictures by Anthony Apodaca and Larry Gritz (pub. Morgan Kaufmann Publishers, ISBN 1-55860-618-1).