Chapter 2. Texture Basics

Table of Contents

2.1. Simple Gradients
2.2. Cum on feel the Noise()
2.2.1. Marbellous Textures
2.3. Bringing it all together
2.4. Filtering And Signals
2.4.1. Reusing Filtering
2.5. Transforming Inputs
2.6. Other transformations
2.7. Thread Safety

From here on, we focus mainly on the texture class itself, and assume that you are using the TextureView class or something similar to display the texture.

2.1. Simple Gradients

We'll start by looking at some simple gradient textures based on the u or v values, and then start looking at the ColorGradient class which provides us with some fundamental building blocks.

A gradient is simply an interpolation from one point to another, or in this case, one color to another. As a simple example, let's make the red component of the resulting texture color equal to the u value.

				
public class UGradient extends AbstractTexture {

    public void getColor(double u, double v, RGBAColor value) {
        value.setColor(u,0,0,1);
    }

}
			

Fairly simple, the red component of the resulting color increases as we go from left to right. This is because the U value increases in value from 0 to 1 as we go from left to right.

Let's extend this and set the green component to the v value of the input.

				
public class UVGradient extends AbstractTexture {

    public void getColor(double u, double v, RGBAColor value) {
        value.setColor(u,v,0,1);
    }

}
			

Nothing we wouldn't expect, but we need something a little more interesting. More complex gradients can be achieved by using the u or v value to interpolate through multiple points or colors. The ColorGradient class lets us easily perform this task.

				
public class SunsetTexture extends AbstractTexture {

    private ColorGradient gradient;

    public SunsetTexture() {
        gradient = new ColorGradient();
        gradient.add(new RGBAColor(16, 32, 64))
                .add(new RGBAColor(32, 64, 128))
                .add(new RGBAColor(128, 160, 255))                
                .add(new RGBAColor(192, 210, 255))                               
                .add(new RGBAColor(250, 240, 192));
    }

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

Our ColorGradient class takes a list of colors and when we are rendering our texture, we use those colors to calculate the resultant color based on the v value. This produces a sunset like effect in our final texture.

Gradients are fairly simple components to use and are often building blocks for more complex textures.

2.2. Cum on feel the Noise()

On key element of procedural textures is the ability to introduce controlled randomness to the texture. This is so don't have to worry about individually placing elements (clouds, dirt etc) and also so we can render larger textures without worrying about repeating elements. However, we don't want to just use a random number generator, we want some pre-determination that will allow us to calculate the same result for the same inputs.

Also, if we have two points, for which we can always calculate the same value, as we move between the two points, we want our random value move between the two points correspondingly. To achieve this, we look at Ken Perlin, a graphics guru and inventor of Perlin Noise in 1985. He has a page describing Perlin Noise at http://www.noisemachine.com/talk1/ .

I've included his implementation of ImprovedNoise.java which can be found on the web. I'll try and give a brief description of the technique of generating noise here, but Ken Perlin's page has a much more technical overview.

For any given point we can pass into the noise function we can construct a 'box' around the point where each corner of the box lies on the nearest integer values. If we consider one dimension, for the number 5.3, we would use the integer positions 5 and 6 as the nearest whole integer positions. In two dimensions, we would form a 2 dimensional rectangle on the integer boundaries and for 3 dimensions, it is a box with a corner on the surrounding integer points. The noise function takes the inputs and determines a value for each of those corners. Bear in mind that each time we call the function with the same values, we use the same corner values. We then interpolate the values across the faces of the box depending on the floating point part of the input values. However, straight interpolation can result in blocky looking noise, so the actual algorithm interpolates unit length gradients across the 'face' of the box. This results in much more rounded noise and also the levels tend towards zero at the lattice points.

Regardless, you don't need to fully understand how the noise functions work to see what they do and how you can use them in textures. Here is a simple noise texture with the greyscale value set to the noise result for the (u,v) values of the texture.

				
public class GreyNoise extends AbstractTexture {

    public void getColor(double u, double v, RGBAColor value) {
        double noiseValue = noise.noise2(u*10,v*10);
        value.setColor(noiseValue,noiseValue,noiseValue,1);
    }
}
			

A single set of noise values can be uninteresting, especially at lower frequencies. What we can do is sum the noise at different frequencies, scale them with different amplitudes, and add them to the result to create a richer noise function. To do this, we can use the PerlinNoise.fbmNoise() method which takes the input parameters and the number of octaves to use. It then calculates the noise at different frequencies and sums them together.

				Noise = noise(i) + 1/2noise(2i) + 1/4noise(4i) ...
			

We can demonstrate this using multiple images for different noise coefficients and then merging them to create a final complex noise texture.

2.2.1. Marbellous Textures

Another key basic texture is a marble type texture that is calculated by summing up noise functions at different frequencies and then passing the value through a sine function. In the noise class, there is a noiseSine() method that calculates this. The parameters for this are speed, which determines how fast the sine wave moves. Scale determines how much noise we apply to it, and octaves determine how many frequencies we use to sample the noise.

These images were generated using an anonymous texture class around a MarbleSignal signal to get the marble value. Noise can also be a great way to generate terrain landscapes, something I've been using the JTexGen framework for.

									
    public static void main(String[] args) {

        Texture texture = new AbstractTexture() {

            private ChannelSignal signal = new MarbleSignal(0,0,5,2,5,1);
            
            
            public void getColor(double u, double v, RGBAColor value) {
                double val = signal.getValue(u, v);
                value.setColor(val,val,val,1);
            }
        };
        TextureViewer.show(texture);

    }
				
				

2.3. Bringing it all together

Now we have a number of textures to create, we need a way to bring them all together, to take multiple textures and combine them to form one single texture. To do this, we have several composition textures that take one or more textures as input and produce an output based on the those textures and optionally based on the (u,v) input parameters. Usually, we need to consider the alpha channel as it is used when compositing one texture over another. We composite textures in such a way that the visibility of the background texture is dependent on the alpha value of the texture laid over it.

The simplest method of compositing is to use the MergedTexture texture. This texture takes two source textures, a background and an overlay texture and merges them. It does this by calculating the point for each texture at point (u,v) and returns the color based on the source inputs and the overlay's alpha value. If the overlay is anyway transparent, we will see the background texture through the overlay texture.

As an example of this, lets create a composite texture that takes a Marble texture and merges it with a white background. In this case, the Marble texture only produces the veins of the marble in a specific color with varying alpha transparency. It is supposed to be laid over another texture to provide a background which is what this texture does.

				
public class MergeTest extends AbstractTexture {

    private MergedTexture mixer;

    public MergeTest() {
        Marble marble = new Marble(RGBAColor.black());
        SolidTexture background = new SolidTexture(RGBAColor.white());
        mixer = new MergedTexture(background,marble);    
    }
    
    public void getColor(double u, double v, RGBAColor value) {
        mixer.getColor(u, v,value);
    }
}
			

You can see the results below. The marble texture always returns a solid color, but the alpha value varies between 0 and 1 depending on the value returned from the marbling noise method. When the alpha value is less than 1, then it is mixed proportionally with the background texture.

Note

When creating textures like the basic marble which is meant to be used in other textures, it is better to return a color with varying alpha rather than mixing the white and black in the marble texture. This is so that it can be placed over other textures and let the texture show through. This lets us create more re-usable textures.

Another alternative is to take one or more input sources and return the average of the mixed results. We do this by summing up the colors and then dividing by the number of colors mixed into the result. We can do this with the TextureMixer class. A simpler version of this class is the MixTexture class which takes two textures and and optional mix level (defaults to 0.5) and the result is based on :

				Result = (SourceA * level) + (SourceB * (1-level))
			

The MultiMergeTexture can be used to compose multiple texture elements onto a final texture which can then be composed onto a background. These textures are merged in the order that they are added. This next example takes a number of marble textures and puts them into a MultiMergeTexture and composites the multiple marble textures into one complex marble.

				
    public static void main(String[] args) {
        MultiMergeTexture mixer = new MultiMergeTexture();
        mixer.getTextures().add(new UVRotate(new Marble(new RGBAColor(255,255,255,0.7),20.7,20,0.2,19,12,1),35));
        mixer.getTextures().add(new UVRotate(new Marble(new RGBAColor(255,128,64,0.5),20.7,3,4,19,12,1),45));
        mixer.getTextures().add(new Marble(new RGBAColor(200,200,200,0.6),1.5,5,7,4,10,1));        
        mixer.getTextures().add(new Marble(new RGBAColor(0,0,0,0.4),20,20,4,5,13,1.4));        
        mixer.getTextures().add(new UVRotate(new Marble(new RGBAColor(255,176,80,0.5),20.7,3,4,19,12,1),145));        
        mixer.getTextures().add(new Marble(new RGBAColor(255,176,80,0.5),10.5,10,4,5,20,1));        
        TextureViewer.show(new Background(mixer,new RGBAColor(122,80,67)));    
    }
			

Probably the easiest way of putting a texture onto a background is to use the Background texture which takes a texture and overlays it on a solid white background. You can use other colors in the constructor, but white is the default.

				
  Marble marble = new Marble(RGBAColor.black());
  mixer = new Background(marble);
			

If you want to merge textures and layers using gradients, specifically by using gradients with the alpha channel, then the next section on filtering should help you create some more interesting effects.

2.4. Filtering And Signals

The previous section looked at merging two textures based on the calculated alpha value for one of them, however, we can also use the ChannelSignal interface to merge textures together (as well as much more). Channel signals are similar to textures in that they take (u,v) input values, and return a result which is consistently the same each time it is called for that (u,v) value. For signals however, we only return a single double value instead of a color. That value can then be processed by a texture that is able to use the value for a number of things.

The ChannelSignal interface is as simple as the Texture interface we saw earlier. This interface is implemented in an AbstractChannelSignal class.

							
public interface ChannelSignal {

    public enum Channel {

        RED, GREEN, BLUE, ALPHA;
    }

    double getValue(double u, double v);
}
			

Let's jump right in and create a simple channel signal. For our example, it will just return the u value that is passed in. That means that as we process the texture across the surface from left to right, the signal will change from 0 to 1.

				
public class ChannelTester extends AbstractChannelSignal {

    public double getValue(double u, double v) {
        return u;
    }

}
			

We will test this with our Threshold texture which takes two source textures and a ChannelSignal and returns one texture if the signal is below a certain threshold value, and returns another texture if the signal is above a certain threshold value. The default switch level is 0.5, which means that since our ChannelSignal just returns the u value, the output should switch from one texture to the other around the middle of the surface texture.

				
			
public class ChannelMergeTest extends AbstractTexture {

    private Texture texture;

    public ChannelMergeTest() {
        Texture marble = new ComplexMarble();        
        SolidTexture background = new SolidTexture(RGBAColor.blue());
        
        texture = new Threshold(marble, background, new ChannelTester());        
                
    }
    
    public void getColor(double u, double v, RGBAColor value) {
        texture.getColor(u, v,value);
    }
}
			

This isn't a very good example, but if we change the source signal in the ChannelTester class to one of the built-in ones, e.g. the NoiseSignal() signal which returns a noise value for the given (u,v) values, we can create a slightly more interesting texture.

Again, still not a very good texture, but it does offer us some possibilities. By using a Noisy signal with a threshold, we can let textures poke through other textures at random points.

2.4.1. Reusing Filtering

Using Channel Signals has actually expanded the ability to create re-usable components drastically. For example, the GradientSignalTexture texture takes a ColorGradient and a ChannelSignal . For each (u,v) we calculate the output of the signal which results in a double value. This value is then used to interpolate the gradient to obtain the final texture color. This texture has been used to rewrite a number of the existing textures such as the horizontal and vertical gradients (we use a USignal and VSignal to interpolate the gradient across the (u,v) values). We even used it to generate a Mandelbrot texture using a Mandelbrot ChannelSignal that returns the calculated fractal value for the point on the texture.

We refactored a MarbleSignal signal from the original marble texture and we now use it not only in the refactored Marble texture but in the Flame texture. The difference is that one takes a signal and assigns it to the resulting color's alpha channel while the other uses the value to interpolate a gradient giving a colored marble effect. We can easily re-use our signals for different types of textures and they will probably be used more in the future to create a more pluggable system.

2.5. Transforming Inputs

There are times when we want to modify the (u,v) inputs to reflect either scale, translation or rotation. Scale multiplies the (u,v) value by a scalar value. Rotation rotates the values around the point 0,0 and translation adds values to the (u,v) values. Each of these can useful effects on our textures. Scaling can allow us to zoom in or out of the texture, while rotation can give us a new angle on some textures which by default look more vertical or horizontal in nature. Translation can help us move the texture around on the surface which can be used to move more interesting parts of the texture into the center, or also reduce the visibility of patterns based on noise functions which are shared by two different textures. Also, we can apply noise to the (u,v) inputs to disturb the values slightly.

Most of these textures are passive in that they obtain the actual texture color value from a source texture we pass in to the transformation texture using the (u,v) values once they have been transformed.

We can demonstrate the use of these textures using a pattern based texture such as the DirtyBrick , Checker , or an image based texture.

The example below demonstrates the different types of transformations that can be performed.


				
// Rotation				
  Texture texture = new ImageTexture("c://camelback.jpg");
  TextureViewer.show(new UVRotate(texture, 45));
  

// Scale
  Texture texture = new ImageTexture("c://camelback.jpg");
  TextureViewer.show(new UVScale(texture, 2,3));
  

// Translation				
  Texture texture = new ImageTexture("c://camelback.jpg");
  TextureViewer.show(new UVTranslate(texture,0.2,0.5););


			

These transformations can be combined to produce more sophisticated transformations.For example, the texture is rotated around the (0,0) point, where you may want to transform around the center of the texture. To do this, we first translate the texture so the center is at (0,0) , then we rotate the texture and then translate the texture back to the center. Like all transformations, the ordering is important.

				
  Texture texture = new ImageTexture("c://camelback.jpg");
  
  texture = new UVTranslate(texture,-0.5,-0.5);        
  texture = new UVRotate(texture, 45);
  texture = new UVScale(texture,2,2);
  texture = new UVTranslate(texture,0.5,0.5);
        
  TextureViewer.show(texture);

			

In this example, we also applied a scale to the rotated texture so we can see the whole image rotated in the middle of the texture.

Caution

Note that transformations and rotations could lead to (u,v) parameters that are outside of the range 0 to 1. For this reason, you need to use the normalize(double value) function in the AbstractTexture class. This method converts the out of range value into an in-range value. For example, normalize(-5.3) returns 0.7 since -5.3 is 0.7 from the next lowest integer value. This method can resolve your (u,v) values for textures where the size of the fractional part is an issue (i.e. the Checker pattern).

In order to make things easier when using transformations, we have a UVTransformer texture class which lets you use scale,rotation and translation all in one texture. The transformations are applied in the order of scale,rotation and then translation. The transformations are automatically applied to the texture around the center as opposed to the top left corner where (0,0) is. This is similar to the multi-transformation example shown above. Below is an example of using this texture to create a series of rounded boxes with a semi-transparent gradient fill. Each box is moved to the right a little more and scaled up in size as we go from left to right.

				
  public static void main(String[] args) {
    //make the color a gradient
    Texture gradient = new HorizontalGradient(ColorGradient.buildFire());
    //Put an alpha filter texture over it
    gradient = new AlphaSignal(gradient, 0.75);

    //make a rounded corner that uses our gradient as the color
    RoundedCornerTexture box = new RoundedCornerTexture(gradient, 0.3);

    //create the final merging texture
    MultiMergeTexture mixer = new MultiMergeTexture();

    for (int i = 0; i < 20; i++) {
      //calcualte the scale and offset for this box
      double offset = ((double) i / 22) - 0.5;
      double scale = 14 - (i / 2);
      //create the new transformer texture wrapped around the box
      UVTransformer transformer = new UVTransformer(box, offset, 0, scale, scale, i * 3);
      //add it to the final merged texture
      mixer.getTextures().add(transformer);
    }
    //display it on a black background.
    TextureViewer.show(new Background(mixer, RGBAColor.black()));
}
			

We use a lot of textures here to create an interesting effect. We also see how we can use other simple textures to modify existing ones. For example, we apply an AlphaFilter to the gradient so we don't have to build a new gradient with a different alpha value. Also note that we re-use the rounded corner box instance that we created to re-display the box. We just apply a different transformation to each instance. We can re-use the box, and most textures, because there is no data contained in the texture instances. They in essence act like singletons or stateless beans.

2.6. Other transformations

We can transform the (u,v) points in other non-constant ways. One is to wrap the texture in a UVNoise texture which takes the (u,v) input and generates noise from them to create 2 new (u,v) values.

				
  Texture texture = new ImageTexture("c://camelback.jpg");
  
  texture = new UVNoise(texture,1,5);                
  TextureViewer.show(texture);        

			

Another way is to use the UVNoiseTranslate texture which takes the input (u,v) parameters and adds a little bit of noise to them so the resultng (u,v) values are somewhere near where they were originally and not some pseudo random values like the previous example.

				
// Less Noise
				
  Texture texture = new ImageTexture("c://camelback.jpg");
  texture = new UVNoiseTranslate(texture,4,4,0.3);        
  TextureViewer.show(texture);


// More Noise

  Texture texture = new ImageTexture("c://camelback.jpg");
  texture = new UVNoiseTranslate(texture,4,4,0.3);        
  TextureViewer.show(texture);


			

These two examples demonstrate the use of different quantities of noise to offset the (u,v) values. One gives a slighly perturbed image while the other is larger and looks like raindrops in a reflective puddle type of image.

2.7. Thread Safety

By default, when a texture is rendered, it is done so by spawning one or more threads. The number of threads is defaulted to the the number of processors available. This means that any signals or textures that you use to render a texture need to be thread safe because chances are that they are running in multiple threads on multiple processors. On the other hand, this will pretty much speed up the time taken to render the texture by the number of processors on the system.

As a general guide to creating thread safe textures and signals, make sure that they are immutable so that once you pass parameter values in to the signal/texture in the constructor, they cannot be changed by either the object itself or the calling code. The texture and signal objects should be stateless such that they carry no state from one call to the next. Any color objects passed to a texture are defensively copied so you can modify the object in calling code as it won't affect the texture holding a copy of it. All the textures and signals provided with the library to the best of my knowledge are thread safe with the caveat that they may use non-thread safe user defined textures or channels which are passed in to the signal or texture.