Advanced_Renderman_Book[torrents.ru]
.pdf
Pattern Generation
10.1 Proceduralism versus Stored Textures
One advantage of having a full programming language to specify the behavior of lights and surfaces is that you can perform arbitrary computations to determine appearances. Not only can you compute new texture coordinates, as we've already seen, but you can even compute entire patterns without any texture file access at all. This is known as procedural texturing.
There are many potential advantages of procedural textures over stored image textures:
■Stored image textures need to come from somewhere-either by meticulously painting or being scanned from a photograph or a real object. Painting obviously requires a lot of work by a skilled artist. Scanning is not always convenient either. Real objects with the desired pattern may not be flat enough to be easily scanned, or it may be hard to acquire a large enough area of material. Furthermore, the scanning process itself will capture the illumination on the object at the time of the scan, which is generally not what is desired. Procedural textures do not suffer from these problems.
■Any stored texture has limited resolution. If you zoom in too closely, you will see a lack of detail or even signs of the original pixels in the texture. But procedural
244 10 Pattern Generation
patterns can have detail at all scales (for example, by introducing new highfrequency detail as you zoom in).
■Similarly, if you zoom out on a stored texture, you will eventually have problems related to its covering only a finite area. What happens if your object is larger than your scanned sample? You will have to tile the texture, which can result in seams or noticeable repetition. On the other hand, a procedural texture can be written to cover arbitrarily large areas without seams or repetition.
■Painted textures created for a specific object are usually one-of-a-kind works that cannot be practically used in other situations or for other objects (unless you truly want an exact duplicate). In contrast, a good procedural texture can often be used for many different objects of related material types or can make different instances of an object each appear slightly different with no more difficulty than adjusting parameters.
■Stored image textures can take lots of disk space, whereas procedural textures only require the space of the compiled shaders themselves, which are much smaller than texture files.
But, of course, there are also many reasons why scanned or painted textures can be much more convenient than proceduralism:
■You have to write a program to generate a procedural texture. This is not always easy. It can often take longer for a programmer to write a procedural shader than it would to have an artist paint a texture and simply use paintedplastic. Some patterns are difficult for even the most experienced shader writer to reproduce procedurally.
■What happens if your carefully crafted procedural texture isn't quite what is desired? Imagine the art director pointing at the screen and saying, "Change the color of that plank, and add another big gouge here." Particularly with stochastic patterns, it can be difficult or impossible to fine-tune the exact appearance. With a painting, it is trivial to add, subtract, or edit particular features at the whim of your art director.
■Procedural textures are notoriously hard to antialias (see Chapter 11, which only touches on the basics). You can easily spend more time trying to antialias your shader than it took you to generate the pattern in the first place. On the other hand, anything you can shove into a stored texture map will be automatically antialiased by the texture system in the renderer (assuming, of course, that the texture was created in the first place without any aliasing).
Ultimately, the proceduralism versus stored texture debate is a false dichotomy. At Pixar, we used to have a macho attitude about proceduralism, thinking that only artwork (like a product label) should be painted or scanned but everything else should be done procedurally if at all possible. This position has fallen out of favor, yielding a more pragmatic attitude.
Obviously, what would be artwork in the real world (product labels, paintings, signs, etc.) are painted and used as texture maps. The textures for "hero objects"
245
10.2 Regular Patterns
(character faces or other objects that are important for plot points) are almost always painted for maximum flexibility, editability, and potential to please the art director. Almost none of the shaders that we write for production are either simple RGB texture maps or purely procedural textures. Rather, most shaders combine the two techniques in various ways. For example, a painted texture may be used to compute the underlying color of an object, but a procedural noise function may supply the lumpy displacement.
Stored texture is often used to control proceduralism. Consider the task of creating a rusty metal object. It would be too much trouble for an artist to paint the individual rust grains all over the object. Making a procedural rust pattern may look convincing but it would be hard to direct exactly where on the object had more or less rust (something that may be very important for aesthetic or plot reasons). A hybrid approach may be for the artist to paint a simple grey map that specifies which areas of the object are rusty (black representing no rust, white full rust). The shader may read the texture and use the value to modulate between a procedural rust grain pattern and the unrusted surface.
Patterns that must cover large areas seamlessly or must be detailed over large ranges of scale, and would therefore be impractical or exhaustive to paint, are done procedurally. An example of this is the ground plane shader on Ant Island in the film A Bug's Life, created over the course of several months by Chris Perry. In some shots you can see hundreds of square meters of the island at once; in other shots a few square millimeters fill an entire theater movie screen. No artist could ever paint so much texture at so fine a detail (at least, not in a single lifetime and not without going insane) or get it to follow all the nooks and crannies of the island geometry. As we have described, this shader did use texture maps under the control of procedures and, in turn, to control the proceduralism.
10.2 Regular Patterns
This section will describe some more built-in Shading Language functions that are useful for building regular patterns.
10.2.1 Thresholding
float step (float edge, x)
The built-in step( ) function returns 0 if x < edge and 1 if x ≥ edge.
float smoothstep (float edge0, edgel, x)
The smoothstep( ) function returns 0 if x ≤ edge0, returns 1 if x ≥ edge1, and performs a smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1. This is useful in cases where you would want a thresholding function with a smooth transition.
246 10 Pattern Generation
Figure 10.1 A comparison of the built-in step( ) and smoothstep( ) functions and a pulse function made from the difference of two steps.
Figure 10.2 Using mod( ) to construct periodic functions.
Figure 10.1 shows graphs of the built-in step( ) and smoothstep( ) functions,
as well as a pulse( ) function that can be constructed from the difference of two steps.
10.2.2 Constructing Periodic Functions
Recall the mod(x, period) function, which returns the remainder of x/period. An interesting use of mod( ) is to make periodic functions. Suppose you have an arbitrary function f(x) defined on [0, p]. Then
f (mod(x,p))
will give a periodic version of f (x). Naturally, you must beware of any discontinuity at p. (See Figure 10.2.)
10.2.3 Arbitrary Spline Interpolation
A handy feature of Shading Language is a built-in spline interpolation function:
type spline (float x; type val1, ..., valn)
247
10.3 Irregular Patterns: noise( )
As x varies from 0 to 1, spline returns the value of a cubic interpolation of val1...valn. Knots are assumed to be evenly spaced. The type may be any of float, color, point, or vector. By default, the interpolating spline uses a CatmullRom basis (that's why you want val1=val2 and val(n-1)=valn). However, you can specify other interpolation bases by name with an optional first parameter:
type spline (string basis; float x; type val1, va12, ... valn)
Valid bases include "catmull-rom" (the default), "bspline", and "linear", indicating a piecewise-linear interpolation. In the case of linear interpolation, val1 and valn are effectively ignored. In other words, when x = 0, the linear spline returns val2; and when x = 1, the spline returns val(n-1). This is for the sake of symmetry to the cubic case. In addition, you have the option of supplying an array instead of a list of individual values. For example,
type spline (float x ; type vals [] )
10.3Irregular Patterns: noise( )
It is also very useful to build irregular patterns, and Shading Language provides a number of built-in functions to facilitate this. Most of these are based on variations of the noise( ) function.
10.3.1 The noise( ) Function
Shading Language provides a primitive function called noise ( ) that can be used as a basis for irregular patterns. (In this book, noise( ) refers to the variety described in the RenderMan spec.) Noise is a variant of a function described by Perlin (1985). The noise( ) function looks a little like a sine wave but with the bumps not all the same height. Figure 10.3 shows a plot of 1D noise.
Noise has the following useful properties:
■noise( ) is repeatable, in the sense that multiple calls to noise with the same
arguments will always return the same values. In other words, it is not a random number generator, which would return a different value every time it is called.
■ noise( ) comes in varieties that are defined on 1D, 2D, 3D, and 4D domains:
noise (float) |
|
|
noise (float, float) |
/* also vector or normal */ |
|
noise |
(point) |
|
noise |
(point, float) |
|
■The return value of noise( ) ranges from 0-1, with an overall average of 0.5. The return value of noise is exactly 0.5 whenever its input coordinates are exactly integer values. While noise( ) is guaranteed not to stray outside the [0,1] range, in fact it tends to spend the vast majority of its time within [0.3, 0.7].
248 10 Pattern Generation
Figure 10.3 Plot of 1D noise.
■noise( ) is
continuous everywhere, meaning that its first derivative is continuous.
Most places it is also
(i.e., the derivative changes smoothly) but not everywhere-in particular, the second derivative is discontinuous at the integer lattice points. (You may occasionally see artifacts related to this.)
■noise( ) is bandlimited, with its peak frequencies in the range of 0.5-1.
■noise( ) is approximately isotropic (has no preferred direction) and is effectively
nonperiodic. (This is not quite true. The implementations of noise( ) that we know of all have a surprisingly short period. Nonetheless, very few situations crop up in which this is a problem.)
Noise that varies over three dimensions is often called solid noise because when used on a surface it makes the object look like it was carved out of a solid block of the material, as opposed to simply having a 2D decal applied to it. Figure 10.4 shows the difference between 2D noise and 3D noise when applied to a teapot. More recent renderers also support a 4D noise: noise(point, float). The intent of this function is to allow a solid noise to vary with time.
Different varieties of noise also exist that have different return types. We have thus far discussed noise( ) as if it always returned a float; for example,
float f = float noise (P);
But noise can also return a point, vector, or color, regardless of the input domain, as in the following examples:
color C = color noise (s,t);
vector offset = vector noise (P, time);
The return type is denoted by type casting, just as we did with texture( ). Also as with texture( ), if you don't explicitly cast the return type for noise( ), it does not default to float-rather, the compiler tries to deduce what you want from the context. This is often not what you intended, as the context is often ambiguous. It is therefore good practice to always explicitly cast noise( ).
249
10.3 Irregular Patterns: noise( )
Figure 10.4 Teapots with 2D noise on (s, t) (left) and 3D noise on P (right). The teapot with 3D noise seems "solid," while the 2D noise appears to vary considerably depending on how the s, t space is stretched on each piece of geometry.
10.3.2cellnoise ( ) -- A Discrete Pseudorandom Value
Sometimes, rather than a continuous noise value, you may want a repeatable pseudorandom discrete value. Example uses include coloring each flower petal differently, each brick, each wood plank, and so on. The standard noise( ) call is undesirable because it's expensive (partly due to its interpolation, which is unnecessary in this application) and its values hang out too close to 0.5.
For this purpose, there is a built-in function called cellnoise( ) that has the following desirable properties:
■cellnoise( ) is pseudorandom but repeatable (same inputs yield same outputs).
Although most implementations are periodic, its period is long enough to not be noticeable.
■ The return value of cellnoise( ) is constant between integer input values (i.e., within each "cell") but discontinuous just before integer values. In other words,
cellnoise(x) ≡ cellnoise(floor(x)).
■The range of cellnoise(x) is uniformly distributed on (0,1). It is not biased to have a tendency to stay close to 0.5, as noise( ) does.
■Like noise( ), cellnoise( ) has varieties with 1, 2, 3, and 4D domains, as well as
float, color, or point, or vector return values:
type cellnoise (float x) type cellnoise (float x, y) type cellnoise (point p)
type cellnoise (point p, float t)
where type maybe any of float, color, or point, or vector.
The cellnoise(x) function is much cheaper to compute than regular noise( ).
25010 Pattern Generation
10.3.3Periodic Noise
Sometimes you want noise that is periodic-that repeats at a particular interval. An example would be if you wanted to wrap a cylindrical projection, like a banana. Such a function is provided by pnoise( ) . The domain of pnoise( ) is like noise, 1-- 4 dimensions, and its range is also any of float, color, point, vector:
type pnoise (float f; type pnoise (float s, type pnoise (point P; type pnoise (point P;
uniform float period)
t; uniform float speriod, tperiod) uniform point Pperiod)
float t; point Pperiod; uniform float tperiod)
10.3.4 Ideas for Using Noise
How can noise( ) be manipulated and used? Here are some ideas for experimentation.
• |
amplitude modulation: amp * noise (x) |
• |
frequency modulation: noise (freq * x) |
• |
using an offset to "push" noise( ) around: noise(x + offset) |
• |
remapping noise( ) to the range (-1,1), also called signed noise: |
|
#define snoise(x) (2*noise(x)-1) |
|
#define vsnoise(x) (2*(vector noise(x))-1) |
|
This is useful because it gives you a function whose average value is zero, rather |
|
than 0.5. |
• |
making anything lumpy and irregular: |
|
foo += noise(P); |
• |
using noise( ) to perturb otherwise regular patterns by changing |
|
foo = func(x); |
|
into |
|
foo = func(x + amp*snoise(freq*x)); |
|
or |
|
foo = amp * noise(freq * func(x)); |
|
or even into |
|
foo = func(x); |
|
foo += amp * snoise(freq*foo)); |
|
In particular, this can be used to perturb the lookup coordinates of a texture access, |
|
giving a warped appearance: |
251
10.4 Fractional Brownian Motion and Turbulence
float ss = s + amp * noise(freq*s,freq*t);
float tt = t + amp * noise(freq*s-12.2,freq*t+100.63); color C = color texture (texturename, ss, tt);
Notice that this example offsets the texture lookup by separate noise( ) values in each of s and t. We add large offsets to the second noise( ) lookup in order for s and t to appear uncorrelated.
■ adding detail to surfaces:
Ct = mix (basecolor, dirtcolor, smoothstep(0.2,0.8,noise(freq*P)))
■ adding bumps to keep a surface from looking totally smooth:
P += snoise(freq*P) * amp * normalize(N);
N = calculatenormal(P);
■making each discrete part of a pattern look different:
woodcolor *= cellnoise(plankx, planky)
■ thresholding noise():
step(thresh, noise(x))
which gives an irregularly spaced kind of pulse. (Note: after you read Chapter 11, you will know that you should be using filterstep( ) rather than step.)
■ taking an iso-value set of noise( ):
n = noise(x);
f = step(iso-0.05, n) - step(iso+0.05, n)
We hope this will give you some taste for how noise might be used. We're not providing explicit examples at this point, though, because noise has the tendency to alias, and we don't want to encourage you even a little bit to write shaders that alias. The following chapter will cover the basics of shader antialiasing. With those new tools in our arsenal, we will later return to the task of writing beautiful procedural shaders.
10.4Fractional Brownian Motion and Turbulence
What happens when we add together several copies of noise at different frequencies, weighted so that lower frequencies are stronger? As you can see in Figure 10.5, the big wiggles of the low frequency add with the smaller, closer wiggles of the higher frequencies, until we get a net effect of a rough curve reminiscent of the profile of a mountain range. Such patterns turn up frequently (no pun intended) in nature, and not just in the way that mountains are shaped. We will formalize this summation of noise octaves as shown in the fBm( ) function in Listing 10.1. (Note that this version will tend to alias at high frequencies. See Section 11.4 for an antialiased fBm.)
252 10 Pattern Generation
Figure 10.5 Summing octaves of fractional Brownian motion.
You can see that fBm adds several copies of noise() together, and each copy has a different frequency and amplitude. The frequencies and amplitudes of successive additions of noise( ) are related by factors of lacunarity and gain, respectively. Notice that it is "self-similar"-in other words, it is summing different copies of itself at different scales. This function is called "fractional Brownian motion." (Not fractal Brownian motion! That is a common mispronunciation, though this function does happen to be a fractal as long as you sum more than two octaves.) Fractional Brownian motion just happens to have a nice, complex, natural-looking pattern that mimics many things in nature. Typical parameters are lacunarity=2 and gain=0.5. Any time that gain =1/lacunarity, the function is what is known as "1/f noise," indicating that the amplitude of successive additions of noise is inversely proportional to its frequency. Figure 10.6 explores some of the parameter space of the fBm function, by varying one of the parameters at a time.
A relative of fBm is a function that Perlin called turbulence. The only difference between turbulence and fBm is the presence of the absolute value call. This roughly doubles the effective frequency and makes everything positive. The result is a more "billowy" appearance, like clouds. (Warning: the sharp corner produced by the abs( ) also introduces a small amount of arbitrarily high frequencies, which
