This is a brief write-up of a shader project I used for teaching. Its intent is not to be a useful finished shader, but to show the kind of incremental development you usually do in SL, and to create something reasonably simple but still original and fun.
I am easily fascinated by Scottish tartan patterns, and their periodic nature and reasonably simple structure makes them suitable for a shader programming project. So, let's write a tartan shader!
First of all, we need to find a reference, either in the real world or in the form of a photo. It is easy to find tartan-like cloth in shops, or clothes made from such cloth, but traditional tartan weaving is also well documented in books and also on the Internet. There is even interactive on-line software to generate traditional tartan patterns.
Weaving is done in a loom, where a wide array of threads (the warp) is crossed with another thread (the weft) in an over-under pattern which both locks the threads in place relative to each other and provides the structure of the cloth. The basic structure of tartan cloth is a weaving pattern called "twill", where each thread passes first over two crossing threads, then under two crossing threads, and then that pattern repeats. This over-under sequence is offset one thread for the next row or column. A picture of the cloth will make this a lot more clear. A schematic diagram helps in determining the procedural functions we need to describe the pattern in SL.
To test our shader, we create a RIB scene with just a single quadrilateral:
Display "tartan.tif" "framebuffer" "rgb" Format 800 800 1 Projection "perspective" "fov" 30 ShadingRate 1 Translate -0.5 -0.5 1.9 WorldBegin LightSource "ambientlight" 1 "intensity" 1 Surface "tartan" AttributeBegin Polygon "P" [0 0 0 1 0 0 1 1 0 0 1 0] "st" [0 0 1 0 1 1 0 1 0] AttributeEnd WorldEnd
We are probably better off by keeping threads on an integer grid, but most primitives have default texture coordinates in the unit square, so let's scale the s and t texture coordinates to make them span a larger interval.
The surface is alternating between warp threads (let's call that the t direction) and weft threads (which is then the s direction). Our first task is to find a function that is 1 where a warp thread is on top, and 0 when a weft thread is on top. Without further explanation, this is a function with that property, wrapped in a very simple shader to test it. To figure out how it works, use a graph paper and spend some thought on it. It is not a trivial function unless you are very familiar with SL and this sort of periodic functions for procedural texturing.
surface tartan() { float threadscale = 50.0; float ss = threadscale*s; float tt = threadscale*t; float warp = mod(floor(0.5*floor(ss) + 0.5*floor(tt)), 2.0); Ci = color(warp); Oi = Os; }
A checkerboard pattern would have been similar in structure, but slightly simpler:
float check = mod(floor(ss) + floor(tt), 2.0)
To create some detail for each thread, we need a local coordinate system for each thread. Because the threads alternate in s and t directions, let's flip the local system so that one coordinate is always across the thread, and one is along the thread. When the thread is a warp thread, s is across and t is along, and when the thread is a weft thread, t is across and s is along. We also want the origin for the thread coordinates to be in the lower left corner of the thread region. This gives us:
float slocal = ss-floor(ss); float tlocal = tt-floor(tt); float across = mix(tlocal, slocal, warp); float along = mix(slocal, tlocal, warp);
This has a problem, however. The thread extends over two units in the "along" direction, so we need to add 1.0 if we are in the upper square of a warp thread or the rightmost square of a weft thread. By examining the pattern of squares where we need to add 1.0, we see that this is a checkerboard pattern (the red squares in the image below).
float check = mod(floor(ss) + floor(tt), 2.0) along = along + check;
The color for each thread can now be varied with the "across" coordinate to fade it to a darker color at the edges, thereby giving a simple impression of a cylinder:
surface tartan() { float threadscale = 50.0; float ss = threadscale*s; float tt = threadscale*t; float warp = mod(floor(0.5*floor(ss) + 0.5*floor(tt)), 2.0); float slocal = ss-floor(ss); float tlocal = tt-floor(tt); float across = mix(tlocal, slocal, warp); float along = mix(slocal, tlocal, warp); float check = mod(floor(ss) + floor(tt), 2.0) along = along + check; color black = color(0.0, 0.0, 0.0); color threadcolor = color(1.0, 0.0, 0.0); float threadfade = sin(PI*across); Ci = mix(black, threadcolor, threadfade); Oi = Os; }
We won't use the "along" coordinate for now, this looks good enough to continue.
A tartan is not all a single color. The very thing that characterizes it is that it has a complicated and carefully designed sequence of different colors for the threads. This sequence of colors is called the "sett" for the tartan. The sett is usually exactly the same for the warp and the weft, and the resulting pattern is a tiling of patterned squares with a period equal to the size of the sett. A sett is a one-dimensional structure, which is natural to express as an array in SL. We define an array of colors and make the color of a thread dependent on its global "across" position in the surface:
color red = color(1.0, 0.0, 0.0); color white = color(1.0, 1.0, 1.0); color sett[8] = {red, red, white, white, white, white, white, white}; float threadindex = floor(mix(tt, ss, warp)); // Integer "across" coordinate color threadcolor = sett[mod(threadindex, 8.0)]; // Wrap to the size of the array
Now, this simple pattern looks more like a tablecloth than a traditional tartan, but all that is needed to make it look a lot better is a better sett. By looking in our references, we find a sett for the classic "Royal Stuart" tartan, which is actually quite large and complicated with a period of 192 threads. Unfortunately, this is a task for which SL is not ideally suited. SL lacks dynamic arrays, so we need to hard-code some things which are dependent on the sett size. This is not pretty, and it shows that SL is not a general programming language, it is actually highly simplified and quite limited.
We could define each thread of the sett directly in a long array, but we choose to create it from a slightly more convenient description with thread counts, similar to how setts are recorded in the tartan-weaving industry. Our sett is the following:
Royal Stuart: R72 B8 K12 Y2 K2 W2 K2 G16 R8 K2 R4 W2 R4 K2 R8 G16 K2 W2 K2 Y2 K12 B8
The letters are colors, and the numbers are thread counts for that color. K is black, R is red, Y is yellow, G is green, B is blue and W is white. This is encoded as two arrays, one with the color and one with the corresponding thread counts, and the final sett array is filled by a simple for loop:
color W = color(1.0, 1.0, 1.0); color R = color(1.0, 0.0, 0.0); color G = color(0.0, 0.8, 0.1); color B = color(0.0, 0.0, 0.8); color Y = color(1.0, 1.0, 0.0); color K = color(0.2, 0.2, 0.2); color threadcolors[22] = {R, B, K, Y, K, W, K, G, R, K, R, W, R, K, R, G, K, W, K, Y, K, B}; float threadcounts[22] = {72, 8, 12, 2, 2, 2, 2, 16, 8, 2, 4, 2, 4, 2, 8, 16, 2, 2, 2, 2, 12, 8}; float settsize = 190; // Make sure this is the total thread count of the sett color sett[190]; // It would have been nicer if SL had supported dynamically sized arrays float i, j; float index = 0.0; for (i = 0; i < 22; i = i+1) { for (j = 0; j < threadcounts[i]; j = j+1) { sett[index] = threadcolors[i]; index = index+1; } }
The awkward constant "settsize" is a weak spot in this programming: if it is wrong the shader might index the array out of bounds and crash, or at least exhibit a warning. However, SL provides no easy way out of this, so we leave it like this.
Within its limitations, the shader is now finished, and the result looks a lot more convincing than what you might expect from the simple tablecloth pattern we had before. It is clear that the colors are what makes a tartan pattern special.
This is still just a fast hack in SL. If this were to be used in a production setting, several flaws would need to be addressed:
However, we leave all those details for later work, and the full shader so far is presented below.
surface tartan() { float threadscale = 200.0; // Lots of threads in this sett float ss = threadscale*s; float tt = threadscale*t; float warp = mod(floor(0.5*floor(ss) + 0.5*floor(tt)), 2.0); float slocal = ss-floor(ss); float tlocal = tt-floor(tt); float across = mix(tlocal, slocal, warp); float along = mix(slocal, tlocal, warp); float check = mod(floor(ss) + floor(tt), 2.0); along = along + check; color black = color(0.0, 0.0, 0.0); color W = color(1.0, 1.0, 1.0); color R = color(1.0, 0.0, 0.0); color G = color(0.0, 0.8, 0.1); color B = color(0.0, 0.0, 0.8); color Y = color(1.0, 1.0, 0.0); color K = color(0.2, 0.2, 0.2); color threadcolors[22] = {R, B, K, Y, K, W, K, G, R, K, R, W, R, K, R, G, K, W, K, Y, K, B}; float threadcounts[22] = {72, 8, 12, 2, 2, 2, 2, 16, 8, 2, 4, 2, 4, 2, 8, 16, 2, 2, 2, 2, 12, 8}; float settsize = 190; // Make sure this is the total thread count of the sett color sett[190]; // It would have been nicer if SL had supported dynamically sized arrays float i, j; float index = 0.0; for (i = 0; i < 22; i = i+1) { for (j = 0; j < threadcounts[i]; j = j+1) { sett[index] = threadcolors[i]; index = index+1; } } float threadindex = floor(mix(tt, ss, warp)); // Integer "across" coordinate color threadcolor = sett[mod(threadindex, settsize)]; // Wrap to the size of the array float threadfade = sin(PI*across); Ci = mix(black, threadcolor, threadfade); Oi = Os; }
Stefan Gustavson, 2008-11-04