Moonlight Pixels

Novice After-Hours Game Development.

LibGDX: Apply Gradient Effect to Center Patch of Ninepatch

2018-05-04 Jay

The basic building block of UIs in JRPG Engine is the panel. They are used to break up content on menus, display dialog between characters and render messages to the player.

A Dialog Panel from Final Fantasy VI

A Dialog Panel from Final Fantasy VI

A Panel in JRPG Engine is a Scene2D Container with a themed background Drawable. The easiest type of Drawable to for Panels is LibGDX’s NinePatch Drawable. A NinePatch is an image that is divided into a 3x3 grid of patches that can be stretched such that the center patch stretches to fill most of the Drawable area.

In the game I’m building I use the following 9x9 texture to build a NinePatch for my Panels, dividing it evenly into 3x3 pixel patches.

Panel Texture with Simple Gradient Center

Panel Texture with Simple Gradient Center

We can create a NinePatchDrawable and make it our default Panel style with the following bit of code during Game Bootstrapping.

  JRPGEngine.run(jrpgConfiguration -> {
      // Other bootstrapping ommitted
      jrpgConfiguration.uiStyle(uiStyle -> {
          uiStyle.set(UiStyle.DEFAULT_STYLE, new Panel.PanelStyle(
              new NinePatchDrawable(
                  new NinePatch(new Texture("assets/jrpg/panel/gradient_panel.png"), 3, 3, 3, 3)
              )
          ));
      });
  });

Unfortunately when we render a Panel with this style, we get this.

Blocky

What we want to do is set a filter to blend pixels in the center patch. We can manipulate rendering of the center patch if we divide the panel texture into 9 TextureRegions ourselve and use a different NinePatch constructor. Since this alteration will likely be useful and reusable, lets make a utility for this.

package com.moonlightpixels.jrpg.ui.util;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.NinePatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_RIGHT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_RIGHT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_RIGHT;

/**
 * Utility class for producing special cases of Ninepatches.
 */
public final class NinePatchUtil {
    private static final int NINEPATCH_PATCHCOUNT = 9;

    private NinePatchUtil() { }

    /**
     * Creates a Ninepatch with a gradient blending technique applied to the center patch.
     *
     * @param texture Texture representing teh complete Ninepatch graphic.
     * @param left Pixels from left edge.
     * @param right Pixels from right edge.
     * @param top Pixels from top edge.
     * @param bottom Pixels from bottom edge.
     *
     * @return Ninepatch with a gradient blending technique applied to the center patch
     */
    public static NinePatch createGradientNinePatch(final Texture texture, final int left, final int right,
                                                    final int top, final int bottom) {
        final TextureRegion[] patches = new TextureRegion[NINEPATCH_PATCHCOUNT];
        final int height = texture.getHeight();
        final int width = texture.getWidth();
        final int centerHeight = height - (top + bottom);
        final int centerWidth = width - (left + right);

        patches[TOP_LEFT] = new TextureRegion(texture, 0, 0, left, top);
        patches[TOP_CENTER] = new TextureRegion(texture, left, 0, centerWidth, top);
        patches[TOP_RIGHT] = new TextureRegion(texture, width - right, 0, right, top);
        patches[MIDDLE_LEFT] = new TextureRegion(texture, 0, bottom, left, centerHeight);
        patches[MIDDLE_CENTER] = new TextureRegion(texture, left, top, centerWidth, centerHeight);
        patches[MIDDLE_RIGHT] = new TextureRegion(texture, width - right, right, left, centerHeight);
        patches[BOTTOM_LEFT] = new TextureRegion(texture, 0, height - top, left, bottom);
        patches[BOTTOM_CENTER] = new TextureRegion(texture, left, height - top, centerWidth, bottom);
        patches[BOTTOM_RIGHT] = new TextureRegion(texture, width - right, height - top, right, bottom);

        patches[MIDDLE_CENTER].getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);

        return new NinePatch(patches);
    }
}

The following line is the magic this utility will be doing for us.

patches[MIDDLE_CENTER].getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);

The Linear filter samples a 2 x 2 texel grid around the target pixel on the screen and chooses a color that is the average of the sampled texels. (A Texel is the term for a pixel from the original Texture).

Our rendered panel now looks like this:

Better

This looks much better!

But, wait. There is still some room for improvement. If you look closely you’ll notice uneven brightness and shadows around the corners/borders. This is because near the edges of our center patch, that 2 x 2 texel grid being samples is pulling texels from the borders of our NinePatch. We can prvent this by sampling a smaller portion of the texture when creating our center patch. If we pull the bounds of the center TextureRegion in by a half-texel on all sides, we will prevent the borders from being samples by our filter.

Here’s the finished version of our utility class that does just this:

package com.moonlightpixels.jrpg.ui.util;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.NinePatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.BOTTOM_RIGHT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.MIDDLE_RIGHT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_CENTER;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_LEFT;
import static com.badlogic.gdx.graphics.g2d.NinePatch.TOP_RIGHT;

/**
 * Utility class for producing special cases of Ninepatches.
 */
public final class NinePatchUtil {
    private static final int NINEPATCH_PATCHCOUNT = 9;

    private NinePatchUtil() { }

    /**
     * Creates a Ninepatch with a gradient blending technique applied to the center patch.
     *
     * @param texture Texture representing teh complete Ninepatch graphic.
     * @param left Pixels from left edge.
     * @param right Pixels from right edge.
     * @param top Pixels from top edge.
     * @param bottom Pixels from bottom edge.
     *
     * @return Ninepatch with a gradient blending technique applied to the center patch
     */
    public static NinePatch createGradientNinePatch(final Texture texture, final int left, final int right,
                                                    final int top, final int bottom) {
        final TextureRegion[] patches = getPatches(texture, left, right, top, bottom);
        final TextureRegion centerPatch = patches[MIDDLE_CENTER];
        final float halfTexelHeight = 1 / (float) (texture.getHeight() * 2);
        final float halfTexelWidth = 1 / (float) (texture.getWidth() * 2);

        centerPatch.getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
        centerPatch.setU(centerPatch.getU() + halfTexelWidth);
        centerPatch.setU2(centerPatch.getU2() - halfTexelWidth);
        centerPatch.setV(centerPatch.getV() + halfTexelHeight);
        centerPatch.setV2(centerPatch.getV2() - halfTexelHeight);

        return new NinePatch(patches);
    }

    private static TextureRegion[] getPatches(final Texture texture, final int left, final int right,
                                              final int top, final int bottom) {
        final TextureRegion[] patches = new TextureRegion[NINEPATCH_PATCHCOUNT];
        final int height = texture.getHeight();
        final int width = texture.getWidth();
        final int centerHeight = height - (top + bottom);
        final int centerWidth = width - (left + right);

        patches[TOP_LEFT] = new TextureRegion(texture, 0, 0, left, top);
        patches[TOP_CENTER] = new TextureRegion(texture, left, 0, centerWidth, top);
        patches[TOP_RIGHT] = new TextureRegion(texture, width - right, 0, right, top);
        patches[MIDDLE_LEFT] = new TextureRegion(texture, 0, bottom, left, centerHeight);
        patches[MIDDLE_CENTER] = new TextureRegion(texture, left, top, centerWidth, centerHeight);
        patches[MIDDLE_RIGHT] = new TextureRegion(texture, width - right, right, left, centerHeight);
        patches[BOTTOM_LEFT] = new TextureRegion(texture, 0, height - top, left, bottom);
        patches[BOTTOM_CENTER] = new TextureRegion(texture, left, height - top, centerWidth, bottom);
        patches[BOTTOM_RIGHT] = new TextureRegion(texture, width - right, height - top, right, bottom);

        return patches;
    }
}

And now our panel has a smooth gradient texture in the center.

Perfect

comments powered by Disqus