Unable to apply shader to PGraphics in p5js

Hi. I’m trying to apply a shader to PGraphics. I was able to apply a shader to a canvas. But, when I tried to apply a shader to PGraphics as well, I could not.

Should I add something?

This is what I’m applying to the canvas

p5.setup = () => {
  p5.createCanvas(100, 100, p5.WEBGL)
  backbuffer0 = p5.createGraphics(p5.width, p5.height, p5.WEBGL)
  theShader = p5.createShader(vs, fs)
  backbuffer0.fill(0)
  backbuffer0.noStroke()
  backbuffer0.rect(-49, -49, 1, 1)
  p5.shader(theShader)
  theShader.setUniform('resolution', [p5.width, p5.height])
  theShader.setUniform('buffer', backbuffer0)
  p5.rect(-50, -50, 100, 100)
}

And this is what I’m applying to the PGraphics

p5.setup = () => {
  p5.createCanvas(100, 100, p5.WEBGL)
  backbuffer0 = p5.createGraphics(p5.width, p5.height, p5.WEBGL)
  backbuffer1 = p5.createGraphics(p5.width, p5.height, p5.WEBGL)
  theShader = p5.createShader(vs, fs)
  backbuffer0.fill(0)
  backbuffer0.noStroke()
  // backbuffer1.noStroke()
  backbuffer0.rect(-49, -49, 1, 1)
  backbuffer1.shader(theShader)
  theShader.setUniform('resolution', [p5.width, p5.height])
  theShader.setUniform('buffer', backbuffer0)
  backbuffer1.rect(-50, -50, 100, 100)
  p5.image(backbuffer1, -50, -50, p5.width, p5.height)
}

I am trying to achieve the life cycle with two PGraphics using this post as a reference. But, as a first step, I am not able to apply a shader to the PGraphics.

Thanks

Generally speaking buffer swapping is not necessary with WEBGL fragment shaders that sample values from the previous state of a texture. I presume this is because the fragment shader output is written to a new buffer each frame. However if you had a shader that needed to sample both the previous state and the state before that then it would be useful. However it is not clear from the code you have shared what the problem is.

Here is an example of a shader that is used to draw a texture to a p5.Graphics object while sampling from the previous frame for the same p5.Graphics object:

3 Likes

Thanks for the reply.

I am hoping to build my own GPU Particle System in the near future. It is my understanding that buffer swapping is required for this. So I wanted to make sure that p5js can do that.

I copied KumuPaul’s code to my local environment and tried to run it. But, for some reason, your life game did not show up in my local environment.

const vert = `
attribute vec3 aPosition;
// attribute vec2 aTexCoord;

varying highp vec2 vPos;

void main() {
  vPos = (gl_Position = vec4(aPosition, 1.0)).xy;
}`

const fragInit = `
precision highp float;

uniform int width;
uniform int height;
uniform int size;
uniform float seed;

varying highp vec2 vPos;

float PHI = 1.61803398874989484820459;

void main() {
  int scaledWidth = width / size;
  int scaledHeight = height / size;
  int x = int((vPos.x + 1.) / 2. * float(scaledWidth));
  int y = int(((vPos.y * -1.) + 1.) / 2. * float(scaledHeight));
    
  vec2 coord = vec2(float(x) / float(scaledWidth), float(y) / float(scaledHeight));
  float r = fract(tan(distance((coord + vec2(0.3, 0)) * PHI, (coord + vec2(0.1, 0.2))) * seed) * (coord + vec2(0.1, 0)).x * 2701.);
  if (r < 0.6) {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  } else {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
}
`
const fragGo = `
precision highp float;

uniform sampler2D state;
uniform int width;
uniform int height;
uniform int size;

varying highp vec2 vPos;


int get(ivec2 origin, int x, int y, int scaledWidth, int scaledHeight) {
  // We're drawing from bottom left: -1, -1 to top right: 1, 1
  // However texture2D/sampler2D work in a coordinate system from top left: 0, 0 to bottom right: 1, 1
  highp vec2 texPos = vec2(
    (float(origin.x + x) + 0.5) / float(scaledWidth),
    (float(origin.y + y) + 0.5) / float(scaledHeight)
  );
  
  return int(texture2D(state, texPos).r);
}

float signf(int val) {
  if (val < 0) {
    return -1.;
  } else if (val > 0) {
    return 1.;
  } else {
    return 0.;
  }
}

void main() {
  int scaledWidth = (width) / size;
  int scaledHeight = (height) / size;
  // Convert our floating point position from -1 to 1, to an integer position
  // from 0 to width/height
  int x = int((vPos.x + 1.) / 2. * float(scaledWidth));
  int y = int(((vPos.y * -1.) + 1.) / 2. * float(scaledHeight));
  
  ivec2 origin = ivec2(x, y);
  
  // test the basic texture sampling functionality
  /*
  float current = float(get(origin, 0, 0, scaledWidth, scaledHeight));
  gl_FragColor = vec4(current, current, current, 1.0);
  */
  
  int sum = get(origin, -1, -1, scaledWidth, scaledHeight) +
            get(origin, -1,  0, scaledWidth, scaledHeight) +
            get(origin, -1,  1, scaledWidth, scaledHeight) +
            get(origin,  0, -1, scaledWidth, scaledHeight) +
            get(origin,  0,  1, scaledWidth, scaledHeight) +
            get(origin,  1, -1, scaledWidth, scaledHeight) +
            get(origin,  1,  0, scaledWidth, scaledHeight) +
            get(origin,  1,  1, scaledWidth, scaledHeight);
  if (sum == 3) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  } else if (sum == 2) {
    float current = float(get(origin, 0, 0, scaledWidth, scaledHeight));
    gl_FragColor = vec4(current, current, current, 1.0);
  } else {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
}
`

export const Sketch = p5 => {
  let size = 2
  let golShader
  let initShader
  let init

  let lastUpdate = 0
  let generation = 0

  let golTexture

  p5.setup = function () {
    p5.createCanvas(p5.windowWidth, p5.windowHeight, p5.WEBGL)
    // p5.pixelDensity(1);
    p5.background(0)
    p5.noStroke()
    p5.fill(255)
    p5.textureMode(p5.NORMAL)

    golTexture = p5.createGraphics(p5.width, p5.height, p5.WEBGL)
    // golTexture.noStroke()

    initShader = p5.createShader(vert, fragInit)
    golShader = p5.createShader(vert, fragGo)

    reset()
  }

  function reset() {
    init = true
    golTexture.shader(initShader)
    initShader.setUniform('width', p5.width)
    initShader.setUniform('height', p5.height)
    initShader.setUniform('size', size)
    initShader.setUniform('seed', p5.random(0, 10000000))
    // frameRate(1);
    // p5.noLoop(); // for debuging

    generation = 0
    lastUpdate = p5.millis()
  }

  p5.draw = function () {
    let delta = p5.millis() - lastUpdate
    let didUpdate = false
    if (init || (maxFrameRate > 0 && delta > 1000 / maxFrameRate)) {
      golTexture.quad(-1, -1, 1, -1, 1, 1, -1, 1)
      lastUpdate = p5.millis()
      generation++
    }

    p5.orbitControl(2, 2, 0.01)
    p5.background('white')
    p5.texture(golTexture)
    p5.rect(-p5.width / 2, -p5.height / 2, p5.width, p5.height)
    //p5.torus(30, 15);

    if (init) {
      golTexture.shader(golShader)
      golShader.setUniform('state', golTexture)
      golShader.setUniform('width', p5.width)
      golShader.setUniform('height', p5.height)
      golShader.setUniform('size', size)
      init = false
    }
  }
  /*
	p5.mouseClicked = function() {
		p5.redraw();
	}*/

  let maxFrameRate = 60
}

The console log showed INVALID_OPERATION: drawElements: no valid shader program in use.

After commenting out golTexture.noStroke() in setup function, the INVALID_OPERATION in Webgl disappeared, but the canvas still does not display the lifegame. A dot is displayed in the center of the canvas.

It can be run in the openProcessing environment, so it is most likely a problem with my local environment.
Have you ever encountered a case where such an INVALID_OPERATION is output?

I don’t know if this will help, but my local environment is as follows
PC Chip: Apple M1 Max
p5js Version: 1.4.0
And I built p5js environment by using create-react-app

You need to call the createShader() function on the p5.Graphics on which you are using the shader (i.e. don’t call p5.createShader(), call golTexture.createShader())

2 Likes

Wow, It’s solved! It was my basic misunderstanding.
Thank you for taking the time to answer my question!

2 Likes