Chapter 3. More Textures

Table of Contents

3.1. Eclipse of the Sun
3.2. Web Background
3.3. Filtering based on a source texture
3.4. Setting Suns
3.5. Fractal Fun
3.6. Where did my texture go?
3.7. Using Anonymous Classes
3.8. Map Texture Map
3.9. Saving High Resolution Images
3.10. Signals, Signals Everywhere
3.11. Springing Along

This section deals with making some more textures based on the principles illustrated in the previous chapters. Most, if not all of these textures are available in the distribution as built-in textures.

3.1. Eclipse of the Sun

Let's start with a fireball texture first. First, we'll create a texture which consists of a radial gradient which goes from the center of the texture towards the edges.

				
public class Fireball extends AbstractTexture {

    private Texture texture;
    

    public Fireball() {
        this(buildFireGradient(), 14, 6, 0.2);
    }

    protected static ColorGradient buildFireGradient() {
        ColorGradient gradient = ColorGradient.buildFire();
       
        gradient.add(RGBAColor.black());
        gradient.add(RGBAColor.black());
        gradient.add(RGBAColor.black());
        gradient.add(RGBAColor.black());

        return gradient;

    }

    public Fireball(double scale, int octaves, double size) {
        this(buildFireGradient(), scale, octaves, size);
    }

    public Fireball(ColorGradient gradient, double scale, int octaves, double size) {
        texture = new RadialGradient(gradient);    
    }

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

			

We added addition black colors on the end of the gradient since we don't want the fire ball to cover the entire texture, we want it positioned more in the center. This creates a fairly predictable texture.

Now we want to perturb the (u,v) points using a noise function so we create a flame effect. We do this using the UVTranslate texture which takes a source texture and uses it to generate the texture colors by calling it with (u,v) values that have been adjusted by noise. We make this change in the constructor where we decorate the radial gradient texture with a UV noise translation texture

				
    public Fireball(ColorGradient gradient, double scale, int octaves, double size) {
        texture = new RadialGradient(gradient);    
        texture = new UVNoiseTranslate(texture,14,5,0.2);
    }
			

This gives us a rather pleasing result with a nice glowing fireball in the middle of our texture

Now let's extend the example to make the fireball look like an eclipsed sun by inserting a big dark circle in the middle. We'll make this a composite texture and create the black circle as an anonymous class. We'll then overlay the black circle texture over the fireball and use transparency to show the fire around the planet.

				
public class SunEclipse extends AbstractTexture {

    Texture texture;

    public SunEclipse() {
        Texture planet = buildPlanet();
        Texture fireball = new Fireball();
        texture = new MergedTexture(fireball, planet);
    }

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

    private Texture buildPlanet() {

        //get the distance from the center of the texture, 
        //if < 10, it is opaque black
        //if > 10.5 it is transparent
        //else we interpolate the transparency between 10-10.5 to give
        //it a nice transition
        return new AbstractTexture() {

            public void getColor(double u, double v, RGBAColor value) {
                u = u - 0.5;
                v = v - 0.5;
                double dist = Math.sqrt((u * u) + (v * v));
                dist = dist * 50;
                if (dist < 10) {
                    value.setColor(0, 0, 0, 1);
                } else {
                    if (dist > 10.5) {
                        value.setColor(0, 0, 0, 0);
                    } else {
                        //in between                        
                        double fd = dist - (int) dist;
                        fd = fd * 2;

                        value.setColor(0, 0, 0, 1 - fd);
                    }
                }
            }
        };

    }
} 
			

3.2. Web Background

We can use color gradients with some transparent colors to generated faded background textures. We start with a marble texture and overlay it with a color gradient that has the same color across the gradient, but different alpha values.

				
  Texture marble = new ComplexMarble();
        
  ColorGradient gradient = new ColorGradient();
  gradient.add(new RGBAColor(16,32,64,0.3));
  gradient.add(new RGBAColor(16,32,64,1));        
  gradient.add(new RGBAColor(16,32,64,1));
        
  Texture overlay = new VerticalGradient(gradient);
                
  Texture texture  = new MergedTexture(marble, overlay);
        
  TextureViewer.show(texture);				

			

3.3. Filtering based on a source texture

We can use one texture to drive the channel signal for another texture. In this example, we use our image texture to set the alpha value for another texture which is a solid color. Because the alpha value of this texture is driven from the input signal, we get the shape of the image poking through on the final texture. This is overlaid onto a background image which shows through depending on the alpha value of the overlay.

				
    public static void main(String[] args) {
        
        //get out image texture
        Texture image = new ImageTexture("c://camelback.jpg");
        
        //build a channel signal to return the red component of the image
        ChannelSignal signal = new TextureSignal(image,Channel.RED);
        
        //create an alpha filter based on a black background and the channel signal
        Texture overlay = new AlphaSignal(SolidTexture.blackBackground(), new InvertSignal(signal));
        
        //select the background texture 
        Texture background = SolidTexture.whiteBackground();
        
        //merge the overlay and the background
        Texture merged = new MergedTexture( background,overlay);
        
        //show the texture
        TextureViewer.show( merged);
    }

			

We can alter the makeup of this texture by using different textures with the alpha filter and also for the background texture.

				
				
  // Image A
  Texture background = new Fireball();
  
  // Image B
  Texture overlay = new AlphaSignal(SolidTexture.whiteBackground(), new InvertSignal(signal));        
  Texture background = new ColorFilter(new ComplexMarble(), new RGBAColor(64,128,255));

  //Image C  
  Texture background = new UVTranslate(new Flame(Flame.greenFlame),1.2,0);    
		

			

3.4. Setting Suns

Let's use our noise functions to create some cloud textures and apply it to our sunset texture we created earlier. We'll use two different sets of clouds and let them grow smaller as they appear further in the distance.

We start with two gradients that are used to color the cloud textures. These two gradients are used to create two new HorizonalGradient textures which are then run through a NoisyTexture texture. The NoisyTexture texture takes an input and sets the alpha value to a the result of a noise function. We take our two cloud textures and merge them into a single clouds texture. This texture is then laid over the top of our SunsetTexture we created earlier.

				
public class CloudTexture extends AbstractTexture {

    ColorGradient redCloudGradient;
    ColorGradient lightCloudGradient;
    MergedTexture clouds;
    NoisyTexture redCloud;
    NoisyTexture lightCloud;
    MergedTexture mixer;

    public CloudTexture() {

        redCloudGradient = new ColorGradient()
            .add(new RGBAColor(255, 240, 230))
            .add(new RGBAColor(255, 140, 64));

        lightCloudGradient = new ColorGradient()
            .add(new RGBAColor(255, 250, 240))
            .add(new RGBAColor(255, 255, 245));

        redCloud = new NoisyTexture(
            new HorizontalGradient(redCloudGradient));
        lightCloud = new NoisyTexture(
            new HorizontalGradient(lightCloudGradient));

        //merge the clouds into one texture            
        clouds = new MergedTexture(lightCloud, redCloud);
        
        //lay the clouds over the sunset background
        mixer = new MergedTexture(new SunsetTexture(), clouds);
    }

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

			

If we look at the texture now, we get some flat clouds over the top of a distant sunset.

What we want to do is try and speed up the rate of the noise the 'further away' the cloud texture is. In this case, the clouds are further away the closer we get to the bottom of the texture (as v approaches 1). Also, the further away the clouds are, the more dense they appear. This is because when we look at clouds above us, we are looking through a thing sliver of cloud. Further away, our line of sight cuts through the cloud layer at an angle making it appear denser. To resolve this, we change the scale and size values of the noise functions the nearer to the bottom of the texture we get.

				
    public void getColor(double u, double v, RGBAColor value) {
        redCloud.setScale(2 + (v * 14));
        lightCloud.setScale(0.4 + (v * 18));

        redCloud.setSize(0.3 + (v * 1.2));
        lightCloud.setSize(0.3 + v);

        mixer.getColor(u, v, value);
    }

			

Not a thoroughly realistic sunset, but it might suffice for an image where you need some kind of background which will be out of focus in the background, especially if you are using a 3D renderer which will handle depth of field for you. This image was composed in a paint package and the background was manually blurred.

3.5. Fractal Fun

We have a number of textures which take a ChannelSignal and use it to interpolate a ColorGradient . Typically, these are noise based values, but we can also use other sources. In this example, we will use a Mandelbrot generator in a ChannelSignal to give us fractal values.

				
public class Mandelbrot extends AbstractTexture {

    private ColorGradient gradient;
    private MandelbrotSignal signal;
				
    public Mandelbrot(ColorGradient gradient, double startX, double startY, double endX, double endY) {
        signal = new MandelbrotSignal(startX,startY,endX,endY);
        this.gradient = gradient;
    }

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

As part of the constructor, the Mandelbrot texture will take start and end parameters for the range to generate. Unless you have a mandelbrot viewer, picking values can be hit and miss so here are some examples.

				
    Texture texture = new Mandelbrot(0.35, 0.35, 0.3507, 0.3507);
    Texture texture = new Mandelbrot(-1.487,-0.006950,-1.46944,0.007);
    Texture texture = new Mandelbrot(-0.86429,-0.2578125,-0.5671,-0.01406);
    Texture texture = new Mandelbrot(-0.75401,-0.23191,-0.68671,-0.1735);
    Texture texture = new Mandelbrot(-0.745725,-0.215732,-0.728179,-0.200524);				


			

This is another example of wrapping the texture logic in a Channel Signal that can be re-used in other textures. We can use the channel signal to drive the alpha value for merging two textures.

				
    public static void main(String[] args) {
        Texture texture = new AlphaSignal(new ComplexMarble(),new MandelbrotSignal(-0.745725,-0.215732,-0.728179,-0.200524));
        MergedTexture text = new MergedTexture(new CloudTexture(), texture);
        TextureViewer.show(text);
    }

			

3.6. Where did my texture go?

This texture involves using NoiseSignal textures to toggle between different textures to create a camouflage effect. Let's start with 3 simple colors to use in our texture and two SmoothThreshold textures switching between two solid color textures. The SmoothThreshold texture is the same as the Threshold texture except that when the texture switches, it does so smoothly instead of toggling instantly between the two.

				
  SolidTexture green = new SolidTexture(new RGBAColor(119,139,111));       
  SolidTexture brown = new SolidTexture(new RGBAColor(119,110,100));
  SolidTexture beige = new SolidTexture(new RGBAColor(166,150,116));        
        
  Texture cam1 = new SmoothThreshold(green,beige, new NoiseSignal(4,10,1,10),0.42);
  Texture cam2 = new SmoothThreshold(beige,brown, new NoiseSignal(4,10,1,10),0.25);
        
        
			

Now let's take these two textures cam1 and cam2 and toggle between the two. Before we do that let's apply a rotation and scale to the UV values before calculating the texture. This lets us scale and rotate the UV value per texture so we get skewed and rotated results.

				
        Texture rotatedCam1 = new UVRotate(new UVScale(cam1,1,4),20);
        Texture rotatedCam2 = new UVRotate(new UVScale(cam2,1,6),34);
        texture = new SmoothThreshold(rotatedCam1, rotatedCam2, new NoiseSignal(6,4,1));       
        
        texture = new Dirty(texture,RGBAColor.black(),0.32);
			

We take our rotated and scaled textures and create a new Threshold texture which will merge them based on another noise signal.

Finally we add a Dirty texture to the final texture. This creates colored noise (black in this case) and applies it to the source texture with a given strength. In this case, we are using it to just dirty up the final texture.

				
public class Camouflage extends AbstractTexture {

    private Texture texture;

    public Camouflage() {        
        SolidTexture green = new SolidTexture(new RGBAColor(119,139,111));       
        SolidTexture brown = new SolidTexture(new RGBAColor(119,110,100));
        SolidTexture beige = new SolidTexture(new RGBAColor(166,150,116));        
        
        Texture cam1 = new SmoothThreshold(green,beige, new NoiseSignal(4,10,1,10),0.42);
        Texture cam2 = new SmoothThreshold(beige,brown, new NoiseSignal(4,10,1,10),0.25);
        
        
        Texture rotatedCam1 = new UVRotate(new UVScale(cam1,1,4),20);
        Texture rotatedCam2 = new UVRotate(new UVScale(cam2,1,6),34);
        texture = new SmoothThreshold(rotatedCam1, rotatedCam2, new NoiseSignal(6,4,1));       
        
        texture = new Dirty(texture,RGBAColor.black(),0.32);

    }
    
    public void getColor(double u, double v, RGBAColor value) {
        texture. getColor(u*1.3, v*1.3,value);
    }
}

			

You could create a huge image of this texture and use it in 3D modelling applications to texture map tanks and so on.

3.7. Using Anonymous Classes

There are times when you might not need to create a brand new Signal or Texture class where you can simple use anonymous classes to implement the functionality. Doing so is easy with our single method interfaces.

				

  //create anonymous signal 
  ChannelSignal signal = new ChannelSignal() {

      public double getValue(double u, double v) {
         return Math.sin(u * v * 10);
      }
  };
        
  Texture texture = new GradientSignalTexture(signal,ColorGradient.buildFire());
        
  TextureViewer.show( texture);

			

3.8. Map Texture Map

Let's try making a map out of our noise function and shade the landscape according to the height and add in a sea level at which point we just shade the texture blue.

We'll start with a Map texture class that takes a gradient, a sea color, and the usual noise parameters for scale and octaves, and a value indicating where the sea level is. To calculate the map, we'll calculate a noise value based on the (u,v) values, and adjust it so the values fill the range 0..1 a little better. Since the value could go outside the 0..1 range, we will clamp it. Finally, we check the height value and if it is above sea level, we use a map color. If it is below sea level, we use the sea color. We don't want to fade into the sea color since the edge of the water on land is a fairly hard edge.

				
    double height = noise.fbmNoise2(u * scale, v * scale, octaves);
    height = height * 1.65;
    height = height - 0.25;
    height = GraphUtils.clamp(height);

    if (height < seaLevel) {
        value.setColor(seaColor);
    } else {
        value.setColor(gradient.interpolate(height));
    }
			

3.9. Saving High Resolution Images

If you want to generate a high resolution image outside of the viewer, and save it to disk for printing or using as a background, you can do so by going directly to the TextureImage class which is the foundation class for rendering textures that the TextureViewer class uses. We simply create an instance and pass a resolution into the constructor and call the render method. Once the image has rendered to its internal canvas, we can then save the image, or in the case of the viewer, render it in the window. Note that since the renderer is multi-threaded by default, the renderAndWait method must be used so it will not execute asynchonously. If we use the render method, then we will probably end up saving the image when it is only 10% of the way through.

				
public class SaveFileDemo {

    public static void main(String[] args) {
    
        TextureImage textureImage = new TextureImage(240, 240);
        Texture mand = new Mandelbrot(-0.86429,-0.2578125,-0.5671,-0.01406);
        
        //we want 4X4 anti-aliasing
        textureImage.setAntiAliased(true);
        
        textureImage.renderAndWait(mand);
        
        textureImage.saveAsPNG("c:/myImage.png");
    }
}
			

3.10. Signals, Signals Everywhere

It could be quite easy to go overboard with using signals in textures and use them for every single constant numerical value. We have a ConstantSignal signal class that just returns a single value that could be used for default constant values. For example, we could make the speed or octaves of a noise texture dependent on the u,v values. As an example, we will create a UVRotation texture where the angle of rotation is dependent on a ChannelSignal used.

							
public class UVSignalRotateTexture extends AbstractTexture {
        
    private ChannelSignal signal;
    private Texture source;

    public UVSignalRotateTexture(Texture source, ChannelSignal signal) {                
        this.signal = signal;
        this.source = source;
    }

    public void getColor(double u, double v, RGBAColor value) {
        //get the angle from the signal
        double radAngle = calculateSignalFromFilter(u, v, signal);
        //do the rotation
        double ru = Math.cos(radAngle) * u - Math.sin(radAngle) * v;
        double rv = Math.sin(radAngle) * u + Math.cos(radAngle) * v;
        //get the final color with the newly rotated u,v values
        calculateColorFromTexture(ru, rv, source,value);
    }
}
			

We get a value from the signal and then use this to drive the angle of rotation. Let's try it out using a USignal signal that returns the value 0 to 1 as it moves across the texture.

				
public static void main(String[] args) {

    Texture texture = new DirtyBrick();
    ChannelSignal signal = new USignal();
    texture = new UVSignalRotateTexture(texture, signal);
        
    TextureViewer.show(texture);        
}
			

Again, we gain the benefits of a pluggable system where we can plug on signal or texture into another and use that new value. We can plug this signal into a NoisySignal signal which will add random elements into it. We also changed it from a USignal to a VSignal .

				
public static void main(String[] args) {
  Texture texture = new DirtyBrick();
  ChannelSignal signal = new VSignal();
  signal = new NoisySignal(signal, 12, 8, 0.15);
  texture = new UVSignalRotateTexture(texture, signal);
  TextureViewer.show(texture);
}
			

We could make every single numerical parameter in a texture a signal parameter, but it would be over the top. However, it does indicate how useful the ChannelSignal architecture can be. We also have a class which lets you provide ChannelSignals to make up the red,green, blue, and alpha channels. You can pass in either 1 signal to be used for all channels, or one for each channel.

				
Listing A:
				
public static void main(String[] args) {

  ChannelSignal signal1 = new MandelbrotSignal(-1.487, -0.006950, -1.46944, 0.007);
  ChannelSignal signal2 = new MandelbrotSignal(0.35, 0.35, 0.3507, 0.3507);
  ChannelSignal signal3 = new MandelbrotSignal(-0.75401, -0.23191, -0.68671, -0.1735);

  ChannelSignal signal4 = new SineWave(100, 10, 2);// new ConstantSignal(1);

  Texture texture = new ChannelTexture(signal1, signal2, signal3, signal4);
  TextureViewer.show(new Background(texture, RGBAColor.black()));
}
			
				
Listing B:					

  public static void main(String[] args) {

    Texture image = new ImageTexture("c://camelback.jpg");            

    ChannelSignal signalRed = new AnimalStripe();        
    ChannelSignal signalGreen = new TextureSignal(image,ChannelSignal.Channel.GREEN);
    ChannelSignal signalBlue = new TextureSignal(image,ChannelSignal.Channel.BLUE);

    ChannelSignal signal4 = new ConstantSignal();
        
    signal4 = new InvertSignal(new RadialSignal(10));


    Texture texture = new ChannelTexture(signalRed, signalGreen, signalBlue, signal4);
    TextureViewer.show(new Background(texture, RGBAColor.black()));

    }
}
			
				
Listing C (variation on B):					

  public static void main(String[] args) {

    Texture image = new ImageTexture("c://camelback.jpg");            

    ChannelSignal stripe = new AnimalStripe();
    ChannelSignal imageRed = new TextureSignal(image,ChannelSignal.Channel.RED);
    ChannelSignal defaultValue = new ConstantSignal(1);
        
    ChannelSignal signalRed = new SignalThreshold(defaultValue,imageRed,stripe);        
                
    ChannelSignal signalGreen = new TextureSignal(image,ChannelSignal.Channel.GREEN);
    ChannelSignal signalBlue = new TextureSignal(image,ChannelSignal.Channel.BLUE);

    ChannelSignal signal4 = new ConstantSignal();
        
    signal4 = new InvertSignal(new RadialSignal(0.9));


    Texture texture = new ChannelTexture(signalRed, signalGreen, signalBlue, signal4);
    TextureViewer.show(new Background(texture, RGBAColor.black()));

  }
}
			

3.11. Springing Along

This section describes how to hook up textures and signals using Spring. This way, we could modify our textures without recompiling, and we can re-use some of the items much easier. This demonstration describes the content of the application.xml configuration part as well as the code required to fetch the final texture bean.

To start with, we'll create a fire gradient using a ColorGradient . Not only do we have to create a bean for the gradient, but also for the colors it contains.

				
    <bean name="fireGradient" class="org.texturemaker.geom.ColorGradient">
        <property name="colors">
            <list>
                <ref bean="blackColor"/>
                <ref bean="blackColor"/>
                <ref bean="redColor"/>
                <ref bean="orangeColor"/>
                <ref bean="yellowColor"/>
                <ref bean="whiteColor"/>
            </list>
        </property>
    </bean>
    
    <bean class="org.texturemaker.geom.RGBAColor" name="whiteColor">
        <property name="red" value="255"/>
        <property name="green" value="255"/>
        <property name="blue" value="255"/>
    </bean>
    
    <bean class="org.texturemaker.geom.RGBAColor" name="orangeColor">
        <property name="red" value="255"/>
        <property name="green" value="128"/>
        <property name="blue" value="0"/>
    </bean>
    
    <bean class="org.texturemaker.geom.RGBAColor" name="yellowColor">
        <property name="red" value="255"/>
        <property name="green" value="255"/>
        <property name="blue" value="0"/>
    </bean>
    
    <bean class="org.texturemaker.geom.RGBAColor" name="redColor">
        <property name="red" value="255"/>
        <property name="green" value="0"/>
        <property name="blue" value="0"/>
    </bean>
    
    <bean class="org.texturemaker.geom.RGBAColor" name="blackColor">
        <property name="red" value="0"/>
        <property name="green" value="0"/>
        <property name="blue" value="0"/>
    </bean>
    				



			

This creates the fire gradient that we need. We will construct our fire using a chain of textures to modify the basic horizontal fire gradient. We start with a NoisySignal that uses a VSignal signal as its source signal. This is used to disturb the (u,v) values used to calculate the fire color in the ColorGradient

				

    <bean name="vSignal" class="org.texturemaker.signals.VSignal"/>
    
    <bean name="noisySignal" class="org.texturemaker.signals.NoisySignal">
        <property name="source" ref="vSignal"/>
        <property name="size" value="0.3"/>
        <property name="octaves" value="15"/>
        <property name="scale" value="15"/>
    </bean>
    
    <bean name="verticalFire" class="org.texturemaker.textures.signal.GradientSignalTexture">
        <property name="gradient" ref="fireGradient"/>
        <property name="signal" ref="noisySignal"/>
    </bean>    
    

			

This looks ok, but we don't have much intensity (white parts) of the fire at the bottom, and it looks fairly flat with no flames leaping upwards. We can use a translation to move the fire upwards so we can see more of the bottom, but while we are at it, we could introduce some more noise to the fire. To do this, we add another NoisySignal that is fed into our original noisy signal. This time, we give it a negative size value so the fire shifts upwards as the noise is introduced.

				
    <bean name="noisySignal" class="org.texturemaker.signals.NoisySignal">
        <property name="source" ref="offsetNoisySignal"/>
        <property name="size" value="0.3"/>
        <property name="octaves" value="15"/>
        <property name="scale" value="15"/>
    </bean>
    
    <bean name="offsetNoisySignal" class="org.texturemaker.signals.NoisySignal">
        <property name="source" ref="vSignal"/>
        <property name="size" value="-0.3"/>
        <property name="octaves" value="5"/>
        <property name="scale" value="5"/>
        
    </bean>
    
			

Now we have some more of a white heat at the bottom, but it still looks rather flat. To remedy this, we can scale the image so the v value changes much faster.

				
<bean name="scaleUV" class="org.texturemaker.textures.uv.UVScale">
    <constructor-arg index="0" ref="verticalFire"/>
    <constructor-arg index="1" value="2.5"/>
    <constructor-arg index="2" value="1"/>        
</bean>
    
			

This time, we also used the constructor arguments to set the bean up. In order to view our texture, we need to initialize the Spring environment, pass it the application.xml file and request and instance of the texture we want to display.

				
public static void main(String[] args) {
    Resource res = new ClassPathResource("applicationContext.xml");
    XmlBeanFactory factory = new XmlBeanFactory(res);
    Texture bean = (Texture) factory.getBean("scaleUV");
    TextureViewer.show(bean);        
}
			

It is really that simple. Using Spring to set up the textures could be very useful for defining a library of often used textures, that can just be imported into your spring configuration. It can even let you define commonly re-used textures such as NoisySignal instances to re-use for putting a subtle amount of noise on a signal for example.