Search code examples
javascriptthree.jsglslshaderfragment-shader

GLSL Fragment shader UV displacement based on world space


I'm trying to create an rgb offset effect for images on a website. I have the basic functionality but the problem is the channels are offset with the uv of the texture. So if images are different sizes the offset is not visually the same for each image.

This is my fragment shader.

uniform sampler2D texture;
varying vec2 vUv; // vertex uv


void main() {
    vec2 uv = vUv;

    float red = texture2D(texture, vec2(uv.x, uv.y - .1)).r;
    float green = texture2D(texture, uv).g;
    float blue = texture2D(texture, vec2(uv.x, uv.y + .1)).b;
    float alpha = texture2D(texture, uv).a;

    gl_FragColor = vec4(vec3(red, green, blue), alpha);

}

And how it looks rendered to the page.

example

How would I go about normalising the uv offset without having to pass in a uniform value?


Solution

  • It would be normal to pass in more info like the amount of offset

    uniform float offset1;
    uniform float offset2;
    uniform sampler2D texture;
    varying vec2 vUv; // vertex uv
    
    
    void main() {
        vec2 uv = vUv;
    
        float red = texture2D(texture, vec2(uv.x, uv.y + offset1)).r;
        float green = texture2D(texture, uv).g;
        float blue = texture2D(texture, vec2(uv.x, uv.y + offset2)).b;
        float alpha = texture2D(texture, uv).a;
    
        gl_FragColor = vec4(vec3(red, green, blue), alpha);
    
    }
    

    You can then adjust this in JavaScript. For example

      const uniforms = {
        offset1:  { value: 0 },
        offset2:  { value: 0 },
        ...
      };
    
      ...
    
      uniforms.offset1.value =  2 / textureHeight;
      uniforms.offset2.value = -2 / textureHeight;
    
    

    If it was me I might do it more like this

    uniform vec2 channelOffsets[4];
    uniform vec4 channelMult[4];
    uniform sampler2D texture;
    varying vec2 vUv; // vertex uv
    
    
    void main() {
        vec2 uv = vUv;
    
        vec4 channel0 = texture2D(texture, uv + channelOffset[0]);
        vec4 channel1 = texture2D(texture, uv + channelOffset[1]);
        vec4 channel2 = texture2D(texture, uv + channelOffset[2]);
        vec4 channel3 = texture2D(texture, uv + channelOffset[3]);
    
        gl_FragColor = 
            channelMult[0] * channel0 +
            channelMult[1] * channel1 +
            channelMult[2] * channel2 +
            channelMult[3] * channel3 ; 
    }
    

    And set them

      const uniforms = {
        channelOffsets:  { value: [
          new THREE.Vector2(),
          new THREE.Vector2(),
          new THREE.Vector2(),
          new THREE.Vector2(),
        ]},
        channelMults: { value: [
          new THREE.Vector4(1, 0, 0, 0),
          new THREE.Vector4(0, 1, 0, 0),
          new THREE.Vector4(0, 0, 1, 0),
          new THREE.Vector4(0, 0, 0, 1),
        ]},
        ....
      }
    
    ...
    
      uniforms.channelOffsets.value[0].y = -2 / textureHeight;
      uniforms.channelOffsets.value[2].y =  2 / textureHeight;
    
    

    For an example of something less hard coded. I might even use texture matrices instead of offsets which would allow rotating and scaling each channel and combine them with matrices which would allow swapping channels.