🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

GLSL: 9-slicing

Started by
5 comments, last by valencio 4 years, 4 months ago

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! :)

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.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.

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! :)

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.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.

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!

@delicatetreefrog Is it possible to explain your end result as for the vertex and the fragment shader? I understand that this might be a topic that is way back for you, but I tried to implement this, but I still get skewing of the texture and I am so confused and don't know what to do.

This topic is closed to new replies.

Advertisement