Chapter 4. Textured Tiger Burning Bright

Table of Contents

4.1. Elements in the texture
4.2. Implementing Tiger Colors
4.3. Adding a fur texture

This chapter walks through the process of building a complex texture from start to finish. In this case, we will build a tigerskin texture. We start by thinking about the different components that go into the texture, and then the best way to build the texture such that we can re-use the different elements of it. We start by dividing the texture into different elements.

4.1. Elements in the texture

1) The main fur color, in this case a gold to white/grey gradient.
2) The actual stripes themselves, sort of noisy black lines running across the texture in one direction.
3) The fur texture itself which gives the impression that the color is painted on individual hairs. This will be tricky, and will mostly consist of a noise that is scaled across the fur in one direction.

4.2. Implementing Tiger Colors

We can split this into several different textures. The first will be called TigerColor and will consist of the main fur colors with the stripes on top of them. The stripes themselves will be generated from a ChannelSignal called AnimalStripes . Once we have our tiger colors, we can see how we like it and then pass that texture into our Fur texture which will apply a directional noise to the texture.

				
public class TigerColors extends AbstractTexture {

    private ColorGradient baseFurGradient = new ColorGradient();
    private Texture  stripe;
    private Texture mixer;

    public TigerColors() {
        
        baseFurGradient.add(new RGBAColor(201,203,202));
        baseFurGradient.add(new RGBAColor(171,118,40));
        baseFurGradient.add(new RGBAColor(108,53,0));
        baseFurGradient.add(new RGBAColor(173,103,43));
        baseFurGradient.add(new RGBAColor(200,205,198));        

        //based our stripe on a black texture with the alpha channel varied by our AnimalStripe signal.        
        stripe = new AlphaSignal(new RGBAColor(0,0,0,0.7),new NoisySignal(new AnimalStripe(),1,15,3));
        //add some noise in here so it's not so sharp on the edges.
        stripe = new UVNoiseTranslate(stripe,8,6,0.05);
        
        //make the background based on the horizonal gradient using our fur colors.
        Texture background = new HorizontalGradient(baseFurGradient);
        //again, apply a translation to the colors so it is not such a straight gradient 
        background = new UVNoiseTranslate(background,10,4,0.2);
        
        //create a mixer that applies our black stripes to our gradient
        mixer = new MergedTexture(background, stripe);       
    }

    public void getColor(double u, double v, RGBAColor value) {
        mixer.getColor(u, v,value);         
    }       
}

			

The only thing left here is to define our AnimalStripe signal. We calculate this by using multiple sine values, some of them rotated and then we scale, add and subtract one value from another. We also apply noise to the values to give us more random edges.

				
public class AnimalStripe extends AbstractChannelSignal {

    private static PerlinNoise noise = new PerlinNoise();
    private ChannelSignal sine1;
    private ChannelSignal sine2;
    private ChannelSignal sine3;
    
    private double centralDisplacementSize;

    public AnimalStripe(double centralDisplacementSize) {
        this.centralDisplacementSize = centralDisplacementSize;
        sine1 = new SineWave(10, 3, 1);
        sine2 = new SignalUVRotate(new SineWave(12, 4, 1), 30);
        sine3 = new SignalUVRotate(new SineWave(12, 4, 1), -30);

    }

    public double getValue(double u, double v) {

        //calculate the distance from the vertical center
        //this doesn't handle out of range u,v values
        double dist = Math.abs(0.5-v);
        u = u + (dist * centralDisplacementSize);

        //scale our u,v
        u = u * 3.5;
        v = v * 3.5;
        
        //calculate a couple of re-usable noise values
        double n1 = noise.fbmNoise2(u, v, 3) * 1;
        double n2 = noise.fbmNoise2(u + 37, v + 29, 3) * 1;

        //rescale u,v so we get an elongated set of stripes
        u = u * 1.5;
        v = v * 0.5;

        //calculate the sines and add/substract
        double s1 = sine1.getValue(u, v) * 0.5;
        double s2 = sine2.getValue(u + n2, v) * 1.4;
        double s3 = sine3.getValue(u + n2, v + n1);
        double value = s1 + s2 - s3;

        //clip the value so only 0.5 and above is a stripe
        return clip(value);

    }

    //if value > 0.55 return 1
    // if 0.55 > value > 0.5, return 0..1
    // else return 0 
    private double clip(double value) {
        if (value >= 0.5) {
            if (value > 0.55) {
                return 1;
            } else {
                return (value - 0.5) * 10;
            }
        } else {
            return 0;
        }
    }

    public double getCentralDisplacementSize() {
        return centralDisplacementSize;
    }

    public void setCentralDisplacementSize(double centralDisplacementSize) {
        this.centralDisplacementSize = centralDisplacementSize;
    }
}				
				
			

4.3. Adding a fur texture

Now we have our colors, we could technically just use this texture if we were interested in texture mapping a 3D model. However, to complex the exercise, we will apply the fur coloring to a Fur texture which will give it a fur-like grain.

Our fur texture simply takes the source texture (our fur color) , and disturbs it in the direction of the fur. It also applies a directional noise texture over the top of it. I also added a Channel Signal to rotate the (u,v) values to rotate the direction of the fur as it moved. I set the angle for each point based on a noise value based on the (u,v) values.

				
public class Fur extends AbstractTexture {

    private Texture furColor;
    private ChannelSignal noiseSignal;
    private SignalUVRotate rotationSignal;    

    public Fur(Texture furColor) {
        this.furColor = furColor;
        noiseSignal = new NoiseSignal(1,4,0.5);
        noiseSignal = new SignalUVScale(noiseSignal, 14,128);        
        rotationSignal = new SignalUVRotate(noiseSignal, 10);
        
    }

    public void getColor(double u, double v, RGBAColor value) {    
                
        double angle = noise.fbmNoise2(u*3, v*3, 3);
        rotationSignal.setAngle(angle*10);
        double val = calculateSignalFromFilter(u, v, rotationSignal);
        //make it fall off quickly
        val = val * val;
        
        //get the fur color
        calculateColorFromTexture(u, v,getFurColor(), value);
        //invert the coefficient value
        value.scale(1-val);
        //set the alpha to 1, this shouldn't be scaled.
        value.setAlpha(1);        
    }

    public Texture getFurColor() {
        return furColor;
    }

    public void setFurColor(Texture furColor) {
        this.furColor = furColor;
    }    
}
			

This demo can be seen by invoking TextureViewer.show(new Fur(new TigerColors())); in an application. If you are rendering this at a high resolution, you might want to add a UV Noise Translator to jiggle the U,V values slightly to reduce some of the smoothness. While the textures can render at different resolutions, they don't always look their best at all resolutions. You can see the effects with the noise translator by using TextureViewer.show(new UVNoiseTranslate(new Fur(new TigerColors()),100,3,0.01));