Jump to content
  • Advertisement

Recommended Posts

I have a 9-slice shader working mostly nicely:

9-slice.png.231188fe8ce59a994fa693ec76675036.png

slice-scifi.png.461bcea84f60a33e3ac0fa1672f3b05a.png     slice-dashed.png.d215b7f9bf17f71a0c232b646f75c3b2.png

Here, both the sprites are separate images, so the shader code works well:

varying vec4 color;
varying vec2 texCoord;

uniform sampler2D tex;

uniform vec2 u_dimensions;
uniform vec2 u_border;

float map(float value, float originalMin, float originalMax, float newMin, float newMax) {
    return (value - originalMin) / (originalMax - originalMin) * (newMax - newMin) + newMin;
}

// Helper function, because WET code is bad code
// Takes in the coordinate on the current axis and the borders 
float processAxis(float coord, float textureBorder, float windowBorder) {
    if (coord < windowBorder)
        return map(coord, 0, windowBorder, 0, textureBorder) ;
    if (coord < 1 - windowBorder) 
        return map(coord,  windowBorder, 1 - windowBorder, textureBorder, 1 - textureBorder);
    return map(coord, 1 - windowBorder, 1, 1 - textureBorder, 1);
} 

void main(void) {
    vec2 newUV = vec2(
        processAxis(texCoord.x, u_border.x, u_dimensions.x),
        processAxis(texCoord.y, u_border.y, u_dimensions.y)
    );
    // Output the color
    gl_FragColor = texture2D(tex, newUV);
}

External from the shader, I upload vec2(slice/box.w, slice/box.h) into the u_dimensions variable, and vec2(slice/clip.w, slice/clip.h) into u_border. In this scenario, box represents the box dimensions, and clip represents dimensions of the 24x24 image to be 9-sliced, and slice is 8 (the size of each slice in pixels).

This is great and all, but it's very disagreeable if I decide I'm going to organize the various 9-slice images into a single image sprite sheet.

9-slice-fail.png.ecc4da7128ca4c27e238741d79062adb.png

Because OpenGL works between 0.0 and 1.0 instead of true pixel coordinates, and processes the full images rather than just the contents of the clipping rectangles, I'm kind of stumped about how to tell the shader to do what I need it to do. Anyone have pro advice on how to get it to be more sprite-sheet-friendly? Thank you! :)

Share this post


Link to post
Share on other sites
Advertisement

You just need to replace your hard-coded 0s and 1s with subspriteX/Y and subspriteW/H.

uniform vec4 subsprite; // [0,1] aka x,y are top-left, [2,3] aka z,w are bottom-right.

// all your code except main...

vec2 subspriteMap(vec2 inner) {
   return mix(subsprite.xy, subsprite.zw, inner.xy);
}

void main(void) {
  vec2 newUV = subspriteMap(vec2(processAxis(...), processAxis(...)));
  gl_FragColor = texture2D(tex, newUV);
}

Untested, quickly banged together. But should do the trick.

Share this post


Link to post
Share on other sites
Posted (edited)

Thanks! @Wyrframe Here is my current result:

9-slice-2.png.ea5de4b2683fdafcd9320f3887e6a1a4.png

It seems a little nearer to correct, but there's also some visible stretching on the left now, and the right border is still missing. I definitely think you're onto something with the use of subsprites, though! Also, what does the mix function do in this? Do you have any links to further reading on the use of subsprites and how mix plays in? Thanks! :)

Update:

With some additional fiddling around with the shader code, I was able to get this result:

9-slice-3.png.3684bfcba52781651d96dc0e509b51c0.png

slice-test.png.486702dca5bf56c2d4b8383598af70d2.png

Current shader code, for anyone in the future trying to do the same thing:

varying vec4 color;
varying vec2 texCoord;

uniform sampler2D tex;

uniform vec2 u_dimensions;
uniform vec2 u_border;

float map(float value, float originalMin, float originalMax, float newMin, float newMax) {
    return (value - originalMin) / (originalMax - originalMin) * (newMax - newMin) + newMin;
}

// Helper function, because WET code is bad code
// Takes in the coordinate on the current axis and the borders 
float processAxis(float coord, float textureBorder, float windowBorder) {
    if (coord < windowBorder)
        return map(coord, 0, windowBorder, 0, textureBorder) ;
    if (coord < 1 - windowBorder) 
        return map(coord,  windowBorder, 1 - windowBorder, textureBorder, 1 - textureBorder);
    return map(coord, 1 - windowBorder, 1, 1 - textureBorder, 1);
}

void main(void) {
    vec2 newUV = vec2(
        processAxis(texCoord.x, u_border.x, u_dimensions.x),
        processAxis(texCoord.y, u_border.y, u_dimensions.y)
    );
    newUV.x+=2.0; // which image to use: 0, 1, or 2
    newUV.x*=24.0/120; // clip.w / texture.w
    newUV.y*=24.0/48; // clip.h / texture.h
    gl_FragColor = texture2D(tex, newUV);
}

I still have to figure out how to go about using the 48x48 sprite instead of one of the three 24x24 ones, and update the code to omit the need for hard-coded math, but it looks like the most confusing part is finally out of the way! Fun stuff! :)

Edited by DelicateTreeFrog

Share this post


Link to post
Share on other sites
13 hours ago, DelicateTreeFrog said:

Also, what does the mix function do in this?

http://lmgtfy.com/?q=glsl+mix

tl:dr; mix(a,b,q), given arbitrary values A,B, and a value from 0..1 Q, returns the value Q% of the way from A to B. I use it there to re-map a 0,1 range (the requested coordinate within the subsprite, the output of your original code) to the range within the texture (the bounding coordinates of the subsprite). Were you setting subsprite to (to extract subsprite #0 from the 4-subsprite texture map shown in your latest update) the vec4 value [0,0, texWidth/5, texHeight/2]?

Quote

I still have to figure out how to go about using the 48x48 sprite instead of one of the three 24x24 ones, and update the code to omit the need for hard-coded math [...]

That's what passing the subsprite to use as a shading parameter (a uniform if you're drawing all boxes of one layer and type in one primitives call, or a varying if you want to draw a different box with each pair of triangles) is meant to do. You calculate the bounds once (or at most, once per draw call) in your program, and pass them into your shader as a uniform.

Share this post


Link to post
Share on other sites
Posted (edited)

Thanks for clarifying what mix does in your code! :) @Wyrframe

I never had any luck figuring out how to set it up that way, so I just did this:

void main(void) {
    vec2 newUV = vec2(
        processAxis(texCoord.x, u_border.x, u_dimensions.x),
        processAxis(texCoord.y, u_border.y, u_dimensions.y)
    );
    newUV.xy+=u_clip.xy/u_clip.wz;
    newUV.xy*=u_clip.zw/u_texsize.xy;
    gl_FragColor = texture2D(tex, newUV);
}

Now it works for all the possible clipping coordinates and sizes. I'll probably move the division from the shader into the CPU part of the program so it only has to be evaluated once per draw call instead of once per pixel, but yeah, very cool!

Edited by DelicateTreeFrog

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!