Search code examples
glslprocessingshaderp5.js

Why my texture coordinates are inverted each time I call my glsl shader in p5js?


I am trying to use a glsl shader with p5js to create a simulation like the game of life. To do that I want to create a shader which will take a texture as uniform and which will draw a new texture based on this previous texture. In a next iteration this new texture will be used as uniform and that should allow me create a simulation following the idea exposed here. I am experienced with p5.js but I'm completely new to shader programming so I'm probably missing something.

For now my code is as straightforward as possible:

  • In the preload() function, I create a texture using the createImage() function and setup some pixels to be white and the others to be black.
  • In the setup() function I use this texture to run the shader a first time to create a new texture. I also set a timer to run the shader at regular intervals and draw the result in a buffer.
  • In the draw() function I draw the buffer in the canvas.
  • To keep things simple I keep the canvas and the texture the same size.

My issue is that at some point the y coordinates in my code seems to get inverted and I don't understand why. My understanding is that my code should show a still image but each time I run the shader the image is inverted. Here is what I mean:

enter image description here

I am not sure if my issue comes from how I use glsl or how I use p5 or a mix of both. Can someone explain to me where this weird y inversion comes from?

Here is my minimal reproducible example (which is also in the p5 editor here):

The sketch file:

const sketch = (p5) => {
    const D = 100;
    let initialTexture;

    p5.preload = () => {
        // Create the initial image
        initialTexture = p5.createImage(D, D);
        initialTexture.loadPixels();
        for (let i = 0; i < initialTexture.width; i++) {
            for (let j = 0; j < initialTexture.height; j++) {
                const alive = i === j || i === 10 || j === 40;
                const color = p5.color(250, 250, 250, alive ? 250 : 0);
                initialTexture.set(i, j, color);
            }
        }
        initialTexture.updatePixels();

        // Initialize the shader
        shader = p5.loadShader('uniform.vert', 'test.frag');
    };

    p5.setup = () => {
        const canvas = p5.createCanvas(D, D, p5.WEBGL);
        canvas.parent('canvasDiv');

        // Create the buffer the shader will draw on
        graphics = p5.createGraphics(D, D, p5.WEBGL);
        graphics.shader(shader);

        /*
         * Initial step to setup the initial texture
         */
        // Used to normalize the frag coordinates
        shader.setUniform('u_resolution', [p5.width, p5.height]);
        // First state of the simulation
        shader.setUniform('u_texture', initialTexture);
        graphics.rect(0, 0, p5.width, p5.height);

        // Call the shader each time interval
        setInterval(updateSimulation, 1009);
    };
  
    const updateSimulation = () => {
        // Use the previous state as a texture
        shader.setUniform('u_texture', graphics);
        graphics.rect(0, 0, p5.width, p5.height);
    };

    p5.draw = () => {
        p5.background(0);
        // Use the buffer on the canvas
        p5.image(graphics, -p5.width / 2, -p5.height / 2);
    };
};

new p5(sketch);

The fragment shader which for now only takes the color of the texture and reuses it (I tried using st instead of uv to no avail):

precision highp float;

uniform vec2 u_resolution;
uniform sampler2D u_texture;

// grab texcoords from vert shader
varying vec2 vTexCoord;

void main() {
    // Normalize the position between 0 and 1
    vec2 st = gl_FragCoord.xy/u_resolution.xy; 
    // Get the texture coordinate from the vertex shader
    vec2 uv = vTexCoord;
    // Get the color at the texture coordinate
    vec4 c = texture2D(u_texture, uv);
    // Reuse the same color
    gl_FragColor = c;
}

And the vertex shader which I took from an example and does nothing excepted passing the coordinates:

/*
 * vert file and comments from adam ferriss https://github.com/aferriss/p5jsShaderExamples with additional comments from Louise Lessel
*/ 

precision highp float;

// This “vec3 aPosition” is a built in shader functionality. You must keep that naming.
// It automatically gets the position of every vertex on your canvas
attribute vec3 aPosition;
attribute vec2 aTexCoord;

varying vec2 vTexCoord;

// We always must do at least one thing in the vertex shader:
// tell the pixel where on the screen it lives:

void main() {
  // copy the texcoords
  vTexCoord = aTexCoord;

  // copy the position data into a vec4, using 1.0 as the w component
  vec4 positionVec4 = vec4(aPosition, 1.0);
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;

  // Send the vertex information on to the fragment shader
  // this is done automatically, as long as you put it into the built in shader function “gl_Position”
  gl_Position = positionVec4;
}

Solution

  • Long story short: the texture coordinates for a rectangle or a plane drawn with p5.js are (0, 0) in the bottom left, and (1, 1) in the top right, where as the coordinate system for sampling values from a texture are (0, 0) in the top left and (1, 1) in the bottom right. You can verify this by commenting out your color sampling code in your fragment shader and using the following:

    float val = (uv.x + uv.y) / 2.0;
    gl_FragColor = vec4(val, val, val, 1.0);
    

    As you can see by the resulting image:

    enter image description here

    The value (0 + 0) / 2 results in black in the lower left, and (1 + 1) / 2 results in white in the upper right.

    So, to sample the correct portion of the texture you just need to flip the y component of the uv vector:

    texture2D(u_texture, vec2(uv.x, 1.0 - uv.y));
    

    const sketch = (p5) => {
      const D = 200;
      let initialTexture;
    
      p5.preload = () => {
        // This doesn't actually need to go in preload
        // Create the initial image
        initialTexture = p5.createImage(D, D);
        initialTexture.loadPixels();
        for (let i = 0; i < initialTexture.width; i++) {
          for (let j = 0; j < initialTexture.height; j++) {
            // draw a big checkerboard
            const alive = (p5.round(i / 10) + p5.round(j / 10)) % 2 == 0;
    
            const color = alive ? p5.color('white') : p5.color(150, p5.map(j, 0, D, 50, 200), p5.map(i, 0, D, 50, 200));
            initialTexture.set(i, j, color);
          }
        }
        initialTexture.updatePixels();
      };
    
      p5.setup = () => {
        const canvas = p5.createCanvas(D, D, p5.WEBGL);
    
        // Create the buffer the shader will draw on
        graphics = p5.createGraphics(D, D, p5.WEBGL);
        // Initialize the shader
        shader = graphics.createShader(vert, frag);
    
        graphics.shader(shader);
    
        /*
         * Initial step to setup the initial texture
         */
        // Used to normalize the frag coordinates
        shader.setUniform('u_resolution', [p5.width, p5.height]);
        // First state of the simulation
        shader.setUniform('u_texture', initialTexture);
        graphics.rect(0, 0, p5.width, p5.height);
    
        // Call the shader each time interval
        setInterval(updateSimulation, 100);
      };
    
      const updateSimulation = () => {
        // Use the previous state as a texture
        shader.setUniform('u_texture', graphics);
        graphics.rect(0, 0, p5.width, p5.height);
      };
    
      p5.draw = () => {
        p5.background(0);
        // Use the buffer on the canvas
        p5.texture(graphics);
        p5.rect(-p5.width / 2, -p5.height / 2, p5.width, p5.height);
      };
    
      const frag = `
    precision highp float;
    
    uniform vec2 u_resolution;
    uniform sampler2D u_texture;
    
    // grab texcoords from vert shader
    varying vec2 vTexCoord;
    varying vec2 vPos;
    
    void main() {
        // Get the texture coordinate from the vertex shader
        vec2 uv = vTexCoord;
    
        gl_FragColor = texture2D(u_texture, vec2(uv.x, 1.0 - uv.y));
    
        //// For debugging uv coordinate orientation
        // float val = (uv.x + uv.y) / 2.0;
        // gl_FragColor = vec4(val, val, val, 1.0);
    }
    `;
    
      const vert = `
    /*
     * vert file and comments from adam ferriss https://github.com/aferriss/p5jsShaderExamples with additional comments from Louise Lessel
    */ 
    
    precision highp float;
    
    // This “vec3 aPosition” is a built in shader functionality. You must keep that naming.
    // It automatically gets the position of every vertex on your canvas
    attribute vec3 aPosition;
    attribute vec2 aTexCoord;
    
    varying vec2 vTexCoord;
    
    // We always must do at least one thing in the vertex shader:
    // tell the pixel where on the screen it lives:
    
    void main() {
      // copy the texcoords
      vTexCoord = aTexCoord;
    
      // copy the position data into a vec4, using 1.0 as the w component
      vec4 positionVec4 = vec4(aPosition, 1.0);
      // This maps positions 0..1 to -1..1
      positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
    
      // Send the vertex information on to the fragment shader
      // this is done automatically, as long as you put it into the built in shader function “gl_Position”
      gl_Position = positionVec4;
    }`;
    };
    
    new p5(sketch);
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.js"></script>