CS184 Final Project - Cutting Planes

by Mikel Bancroft, Michael Fong, and Oren Jacob


Overview

In computer graphics, one of the most difficult things to reproduce realistically are shadows. Most of the shadow algorithms today either
  1. produce overly sharp and unrealistic shadows or
  2. require very high overhead (ie. radiosity).
What follows is a method of generating artificial shadows which can be soft and gradiated. The algorithm is not a true soft shadow generator, but the ease with which the shadows can be placed, and manipulated makes it a worthwhile venture.

Why Did We Do This?

On film sets, many lights are used to illuiminate a set. To create shadows (i.e. the director may wish to darken half of the scene), large cutting cards are raised up on ropes to cover the appopriate portions of light. This idea would be extremely useful if it was implemented in a graphics renderer.

Concept

The main concept is rather simple. Generate selectively transparent half-plane objects which can be placed and manipulated within a scene. The gradiation and orientation should be alterable parameters.

Our implementation is actually a big improvement over the real life version described above (mostly due to questionable details in the code, which we have deemed to be FEATURES!). Our cutting planes are completely invisible during the rendering process except to the lights we assign to see them. This means that unlike physical cutting cards, we can place the half-planes right in the middle of the scene without them showing up. It is also possible to place cutting planes through objects, which can allow for some pretty cool effects (we will leave that to your imagination... no pics of that).

Cutting Cards in Action

As a base for presenting our work, we use two scenes both illuminated by a barn light. A barnlight, is essentially the same as a spot light, but it is square. It is located at some point in space, and points in a given direction. It's area of effect is defined by two rectangles, which increase in size as distance from the source of the light increases. All parts of a given scene which fall within the inner rectangle of the light, are fully illuminated by the light. All parts of the scene falling outside the outer rectangle are not at all illuminated by the barnlight. Between the two rectangles, the light intensity falls off gradually toward zero.

At the scene creation level, the cutting cards exist as a pair of parallel lines, which represent a sort of half plane. The intensity of light falling beyond the outer edge of one line falls to 0. If light falls beyond the outer border of the other line, it's intensity remains the same. Between the two lines, the intensity of light falls off between the lights intensity at that point, and zero intensity.

For detailed information on the pictures contained in this report:

Nuts and Bolts

What follows are the important bits of the actual code used to render the tea kettle scenes. The code was developed in Pixar's Modelling Language which is very similar in syntax to C++.



    include "std.m";
    include "defaults.m";
    include "cameras.m";
    include "colors.m";
    include "lib/math.m" when (!math_m);
    include "std_avars.m";
    include "shear.m";
    include "ldetail.m";
    include "metam.m" when (!metam_m);
    include "/usr/anim/com/global/mdl/trace.m";

    /* set up the coordinate system */
    coordsys("worldspace");

    include "mdl/Counter/counter.m";
    include "mdl/Wall/wall.m";
    include "mdl/Cabinets/cabinets.m";
    include "mdl/Outlet/Outlet.m";
    include "mdl/Kettle/kettle.m";
    include "mdl/Floor/floor.m";

    /* include the code for cutlights and cutcards */
    include "cutlight.m" when (!cutlight_m);
    include "cutter.m"   when (!cutter_m);

    /* set up the main camera */
    apr_cam();

    /* the cutlight associated with the cutting cards */
    cutlight("oreolight", GLOBALLIGHT, APR, (1,1,1), SPOT, FULL, LIGHTFALLOFF,
	     SHADOWENABLE, "Loreolight_sm", (0,0,0), "", "",
	     shaderargs("cutter1", "trial1", "cutter2", "trial2"));

    /* instantiate 2 cutting cards */
    cutter("trial1");
    cutter("trial2");

    /* assorted objects*/
    do_Wall();
    do_Counter();
    do_Cabinets("_upper",1);
    do_Cabinets("_lower",-1);
    do_Outlet();
    do_Kettle();
    do_Floor();


This section of code sets up the scene found in Model Universe. Cutlight() instantiates a barnlight with links to specified cutting cards, called cutters. The other parameters of Cutlight are generic values that all lightsources at Pixar take. This is done so it can be used transparently with older versions of lighting code.

This macro is responsible for drawing the green outline of the barnlight that you see in Model Universe , which is the visual representation of the lightsource in the animation system, Menv. In calling the macro, the shadowmaps, illumination model, and shadow/light colors are prescribed.

Immediately following that, the two cutter() lines invoke 2 cutting cards. There can be an arbitrarily large number of cutting cards, but at this point, any given light can only look at a maximum of 3. This number can also be extended arbitrarily high, but was left at 3 for simplicity. You will notice that in the cutlight() macro the names of the cutting cards to use are listed explicitly. This is necessary because the light has to have complete knowledge of which cutting cards it is going to use.




    const cutter_m = 1;

    /*
     * This is the global macro that is used for the interactive placement
     * cutting cards.  Avars stand for "animation variables" or "articulated
     * variables".  They can be interactively changed in the Pixar animation
     * system.
     */

    global macro cutter(name)
    begin
	object cat("cutter_",name) {

	    avar  tx = 0, ty = 0, tz = 0;
	    avar  rx = 0, ry = 0, rz = 0, rspin = 0;
	    avar  rpivx = 0, rpivy = 0, rpivz = 0;  
	    avar  cutx = 0,
		  width = 5       {min = 0.001};
	    avar  showlength = 10 {min = 0.001};

	    cutterNailed(name,
			 cutx, width,
			 tx, ty, tz,
			 rpivx, rpivy, rpivz,
			 rx, ry, rz, rspin,
			 showlength);
	}
    end

    /*
     * The following is the macro that is either called with hard-wired numbers
     * (for attaching a cutting plane to some geometry in the scene) or called
     * via the cutter() macro above for interactive placement.
     */

    global macro cutterNailed(name,cutx,width, tx, ty, tz, rpivx, rpivy, rpivz,
				rx, ry, rz, rspin, showlength)
    begin
    {
	translate(tx, ty, tz);
	translate(rpivx, rpivy, rpivz);
	object "rotpivot" {
	    color(0, 1, 0.191);
	    local sz = 5;
	    ldetail(LD_guides, LD_lighting);
	    line((-(sz, 0, 0), (sz, 0, 0)));
	    line((-(0, sz, 0), (0, sz, 0)));
	    line((-(0, 0, sz), (0, 0, sz)));
	    chars(name);
	}
	rotate(rz, z);
	rotate(ry, y);
	rotate(rx, x);
	rotate(rspin, z);
	translate(-rpivx, -rpivy, -rpivz);
	translate(cutx, 0, 0);
	scale(max(width, 0.001), 1, 1);    

	/*
	 * this is the heart of the whole thing... this stores the
	 * transformation matrix into "cutter space" for each individual
	 * piece of geometry to be transformed to, allowing a very simple
	 * hit test against the XY plane for the determination of the
	 * amount of light intensity reduction.
	 */

        /* save coordinate system of cutting plane */
	coordsys(name);

	{
	    ldetail(LD_guides,LD_lighting);
	    {
		color(1, 0, 0.191);
		chars(name);
		line(((0, -showlength, 0), (0, showlength, 0)));
	    }
	    {
		translate(1, 0, 0);
		color(0, 0.191, 1);
		chars(name);
		line(((0, -showlength, 0), (0, showlength, 0)));
	    }
	}
    }
    end



The macro cutter calls cutterNailed with animation variables being passed in, as opposed to hard coding numbers in for stationary cutting planes. The macro cutterNailed positions the cutting plane based on the input parameters through translates, rotates, and scales. Once that is done, a coordsys() call is used to store the transformation matrix into "cutter space". The coordinate is saved to avoid having to do hit tests of rays leaving the lightsource. Instead points on the objects can be transformed back into "cutter space" and a simple test against the XY plane can be used to determine hit or miss. The calls to line() and chars() are used to indicate visually to the animator where the cutting card is located. You will notice the red and blue lines in the Model Universe image. They represent where the cutting card has full and no effect. These lines move interactively as the values of translate, rotate, and scale are changed.




light cutlight( /* various parameters */)
{

    /* various variable declerations */

	/* Cutter variables */
        point  apexcut;           /* pyramid apex in cutter space  */
        point  Pcut;              /* Surface point in cutter space */
        point  Lcut;              /* Light vector in cutter space  */
        float  apexcutz, apexcutx;
        float  Pcutz, Lcutz;
        float  tcut;              /* zplane intersection param     */
        float  intcutx;

    /* test for inside/outside the barndoors of the green display */

	/*
	 * This is the first of the 3 cutplanes that the light accesses.
	 * They can be easily macro-ized into a prepackaged set of
	 * calls that could be used to modulate intensity (like in this
	 * case), modulate shadow intensity, shadow blur, or any other
	 * parameter in this light shader.
	 */
        if (cutter1 != "") {
            apexcut  = transform(cutter1, nfrom);
            Pcut     = transform(cutter1, Ps);
            apexcutz = zcomp(apexcut);
            Pcutz    = zcomp(Pcut);
            if ((apexcutz * Pcutz) < 0) {     /* opposite sides of zplane 0 */
                /* the above is dangerous if cutters actually penetrate     */
                /* illuminated surfaces.  Likely?                           */
                Lcut  = Pcut - apexcut;
                Lcutz = zcomp(Lcut);
                tcut = -(apexcutz / Lcutz);   /* could test tcut instead    */
                intcutx = xcomp(apexcut + tcut * Lcut);
                           /* x coord of intersection with zplane 0
			      using parametric definition of a line */
                atten *= (smoothstep(0, 1, intcutx) * (1 - cutint1) + cutint1);
		/* attenuation variable is modified by the cutting plane
		   calculation */
            }
        }


    /* test to see if we're inside the barndoors, and if yes, do the
       test to see if we're in shadow */

    /* then do the slide projection, if there is one */

    /* if we're in the barndoors, calculate the intensity of light hitting
       the geometry */

    illuminate( from, to, angle ) {
	/* calculate the intensity falloff */
    }

    /* finally, set the output color of light to be the multiplication of
       the basic color and the various modulating variables */

    Cl = Clight * atten * intensity * falloffintensity;

    }




Cutlight is a shader which implements the cutting card algorithm. Shaders are pieces of code that get linked into RenderMan at image computing time. They are used to define surface properties, atmospheric properties, and, in our case, the behavior of a lightsource. The renderer sends a point to Cutlight which then does a quick hit or miss test to verify if thepoint is within its assigned cutting planes. If the point is within a half-plane, the light intensity is modified.

Conclusion

This is really how this whole thing works. Within every light that the user wants to respond to cutting planes, there is a set of code that transforms the geometric points into "cutter space" and then tests the point against the cutting plane. The test for opposite side of the plane is there to basically allow the front of the cabinets to be fully illuminated while the back wall has its light modulated. (See Kitchen scene with cutting cards) This way, cutters can be stacked up along the viewing vector of the lightsource and effect only objects behind behind them. You will also notice that the test of whether we're in the cutter's cutting area or not is a simple smoothstep() of the x component of the intersection of the ray between the lightsource and the geometry with the z=0 plane in "cutter space". The simplicity of this test, and its computational efficiency are the reasons for doing the calculation this way. With the result of the smoothstep() call, the attentuation variable is modified and execution passes farther down the shader, going through the shadow code and finally out to the setting of the output color of light that the surface receives.

It is a simple matter to increase the number of possible cutting planes from 3 to n, and would allow for far greater precision in the shaping of light sources. Imagine trying to create a light source shaped as some convex polygon. With cutting planes it can be done lots simpler. It also isn't a difficult matter to create cutting planes of different shapes. If you create circular cutting planes, even cooler shaping can be accomplished. Another interesting aspect of this technique is that other variables like shadow intensity, shadow blur, and even things like reflection intensity can be easily modulated with other cutting planes.

This... is just the tip of the iceberg!