0
\$\begingroup\$

I am trying to draw a rounded border / outline around a texture. Basically I have two textures doing a transition and the border should look like being part of the textures itself.

I tried different approaches, but the best I could get is a border that is cut off on the top and only the top right corner being rounded.

The result looks like this:

texture not in the center and only one corner is rounded.

I have the feeling I am pretty close, but it seems that I am missing something.

Here is my fragment shader code:

precision highp float;

uniform vec2 uImageActiveSize;
uniform vec2 uImageNextSize;
uniform vec2 uPlaneSize;
uniform vec2 uDirection;
uniform float uTransitionProgress;
uniform float uMaskProgress;

uniform float uBorderThickness;
uniform float uBorderRadius;
uniform vec4 uBorderColor;

uniform sampler2D tMapActive;
uniform sampler2D tMapNext;

varying vec2 vUv;
varying vec3 vPosition;

vec4 getFromColor(vec2 uv){return texture2D(tMapActive, uv);}
vec4 getToColor(vec2 uv){return texture2D(tMapNext, uv);}

const float smoothness = 0.5;
const vec2 center = vec2(0.5, 0.5);

vec4 transition (vec2 uv) {
  vec2 v = normalize(uDirection);
  v /= abs(v.x) + abs(v.y);
  float d = v.x * center.x + v.y * center.y;
  float m = 1.0 - smoothstep(-smoothness, 0.0, v.x * uv.x + v.y * uv.y - (d - 0.5 - uTransitionProgress * (1.0 + smoothness)));
  return mix(getFromColor((uv - 0.5) * (1.0 - m) + 0.5), getToColor((uv - 0.5) * m + 0.5), m);
}


float calcDistance(vec2 uv) {
    vec2 extend = vec2((uPlaneSize.x), (uPlaneSize.y));
    vec2 coords = abs(uv) * (extend + uBorderRadius);
    vec2 delta = max(coords - extend, 0.);
    return length(delta);
}

void main() {

  vec2 ratio = vec2(
    min((uPlaneSize.x / uPlaneSize.y) / (uImageActiveSize.x / uImageActiveSize.y), 1.0),
    min((uPlaneSize.y / uPlaneSize.x) / (uImageActiveSize.y / uImageActiveSize.x), 1.0)
  );

  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );

  vec2 ratio2 = vec2(
    min((uPlaneSize.x / uPlaneSize.y) / (uImageNextSize.x / uImageNextSize.y), 1.0),
    min((uPlaneSize.y / uPlaneSize.x) / (uImageNextSize.y / uImageNextSize.x), 1.0)
  );

  vec2 uv2 = vec2(
    vUv.x * ratio2.x + (1.0 - ratio2.x) * 0.5,
    vUv.y * ratio2.y + (1.0 - ratio2.y) * 0.5
  );

  vec2 uvMix = mix(uv, uv2, -uTransitionProgress);

  float dist = calcDistance(vUv);
  if (dist > uBorderRadius) discard;

  float alpha = ( vPosition.y + uMaskProgress ) > 0.5 ? 0.0 : 1.0;


  float aspect = uPlaneSize.y / uPlaneSize.x;
  float maxX = 1.0 - uBorderThickness;
  float minX = uBorderThickness;
  float maxY = maxX / aspect;
  float minY = minX / aspect;

  if (vUv.x < maxX && vUv.x > minX &&
      vUv.y < maxY && vUv.y > minY) {
    gl_FragColor = transition(uvMix);
  } else {
    gl_FragColor = vec4(1.,0.,0.,1.);
  }

  gl_FragColor.a *= alpha; 
}
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

You have:

float calcDistance(vec2 uv) {
    vec2 extend = vec2((uPlaneSize.x), (uPlaneSize.y));
    vec2 coords = abs(uv) * (extend + uBorderRadius);
    vec2 delta = max(coords - extend, 0.);
    return length(delta);
}

This would do what you want if texture UV coordinates had a range of -1 to 1 — then all four corners would have coordinates of ±1 and get rounded. But they actually have a range of 0 to 1, so the only corner that is rounded is the one that does have coordinates of (1, 1), the top right.

You can fix this by rescaling the coordinates so they are symmetric that way:

float calcDistance(vec2 uv) {
    // Rescale UV to be -1 to 1
    vec2 positionInQuadrant = abs(uv * 2.0 - 1.0);

    vec2 extend = uPlaneSize;
    vec2 coords = positionInQuadrant * (extend + uBorderRadius);
    vec2 delta = max(coords - extend, 0.);
    return length(delta);
}

Note that because this stretches the coordinates to double the range, you will have to compensate for that by changing uPlaneSize too.

float calcDistance(vec2 uv) {
    // Rescale UV to be -1 to 1
    vec2 positionInQuadrant = abs(uv * 2.0 - 1.0);  // key change
    // Rescale uPlaneSize to compensate
    vec2 extend = uPlaneSize / 2.0;

    vec2 coords = positionInQuadrant * (extend + uBorderRadius);
    vec2 delta = max(coords - extend, 0.);
    return length(delta);
}

I tested this out by running the following shader in Shadertoy:

const float uBorderRadius = 20.0;

float calcDistance(vec2 uv) {
    vec2 positionInQuadrant = abs(uv * 2.0 - 1.0);
    vec2 extend = vec2(iResolution) / 2.0;
    vec2 coords = positionInQuadrant * (extend + uBorderRadius);
    vec2 delta = max(coords - extend, 0.);
    return length(delta);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord/iResolution.xy;
    
    float dist = calcDistance(uv);
    if (dist > uBorderRadius) {
        fragColor = vec4(0.0, 0.0, 0.0, 1.0);
    } else if (dist > 0.0) {
        fragColor = vec4(1.0, 0.0, 0.0, 1.0);
    } else {
        fragColor = vec4(0.0, 0.5, 1.0 ,1.0);
    }
}

Screenshot

\$\endgroup\$
1
  • \$\begingroup\$ Thanks, that is a great explanation and it does work perfectly now. Really appreciate the detailed clarification. \$\endgroup\$
    – hank_sp
    Commented Apr 30, 2023 at 18:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .