Why does my vertex shader ignore Uint8Array data but not Float32Array data?

356 views Asked by At

My vertex shader has the following attribute:

attribute float a_color;

I have an array buffer (WebAssembly's memory) which consists only of 0s and 1s and I create a view of it by creating a Uint8Array. However my vertex shader ignores it (no errors, but seems to treat everything as 0). I am using twgl. Here's my code:

Vertex shader:

attribute vec2 a_position;
attribute float a_color;
uniform vec2 u_resolution;
uniform float u_point_width;
varying vec4 v_color;

vec2 normalize_coords(vec2 position) {
   float x = position[0];
   float y = position[1];
   float resx = u_resolution[0];
   float resy = u_resolution[1];
   return vec2(2.0 * x / resx - 1.0, 2.0 * y / resy - 1.0);
}

void main() {
   gl_PointSize = u_point_width;
   vec2 coords = normalize_coords(a_position);
   gl_Position = vec4(coords * vec2(1, -1), 0, 1);
   v_color = vec4(0, a_color ,0,1);
}

Fragment shader:

precision highp float;

varying vec4 v_color;
void main() {
  gl_FragColor = v_color;
}

Javascript:

  const canvas = document.getElementById("c");

  const CELL_SIZE = 10;

  const gl = canvas.getContext("webgl");
  const programInfo = twgl.createProgramInfo(gl, [
    "vertex-shader",
    "fragment-shader"
  ]);

  twgl.setDefaults({ attribPrefix: "a_" });

  twgl.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  const universe = Universe.new(
    Math.ceil(gl.canvas.width / CELL_SIZE / 2),
    Math.ceil(gl.canvas.height / CELL_SIZE / 2)
  );
  const width = universe.width();
  const height = universe.height();

  let points = [];

  for (let i = 0; i < width; i++) {
    for (let j = 0; j < height; j++) {
      points.push([i * CELL_SIZE * 2, j * CELL_SIZE * 2]);
    }
  }

  const cellsPtr = universe.cells();
  // Webassembly memory (only 0's and 1's)
  const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

  const arrays = {
    position: {
      data: new Float32Array(points.flat()),
      size: 2
    },
    color: {
      data: cells,
      size: 1,
      type: gl.UNSIGNED_BYTE
    }
  };

  const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);

  const uniforms = {
    u_resolution: [gl.canvas.width, gl.canvas.height],
    u_point_width: CELL_SIZE
  };

  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(programInfo.program);

  twgl.setUniforms(programInfo, uniforms);

  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  twgl.drawBufferInfo(gl, bufferInfo, gl.POINTS);

  function renderLoop(time) {
    twgl.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    universe.tick();
    const cellsPtr = universe.cells();
    const cells = new Uint8Array(
      memory.buffer,
      cellsPtr,
      width * height
    );

    const uniforms = {
      u_resolution: [gl.canvas.width, gl.canvas.height],
      u_point_width: CELL_SIZE
    };

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, uniforms);

    twgl.setAttribInfoBufferFromArray(gl, bufferInfo.attribs.a_color, {
      data: cells,
      size: 1,
      type: gl.UNSIGNED_BYTE
    }); // Dynamically update buffer with new colors

    twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

    twgl.drawBufferInfo(gl, bufferInfo, gl.POINTS);
    requestAnimationFrame(renderLoop);
  }

  requestAnimationFrame(renderLoop);
};

I have no problems if I manually convert cells to Float32Array. Am I doing something wrong?

Here's a live simplified version of the code above (there should be green points on the screen but there aren't):

https://codesandbox.io/s/silly-jackson-ut3wm

2

There are 2 answers

0
gman On BEST ANSWER

If you actually are using twgl then, in the same way that it guesses size = 3 for position, 4 for colors, 2 for texcoords, the issue that twgl is guessing to use normalize: true and you need to tell it normalize: false

From the docs

normalize boolean --- normalize for vertexAttribPointer. Default is true if type is Int8Array or Uint8Array otherwise false.

So,

  const arrays = {
    position: {
      data: new Float32Array(points.flat()),
      size: 2
    },
    color: {
      data: cells,
      size: 1,
      type: gl.UNSIGNED_BYTE,
      normalize: false,       // added <------------------------
    }
  };

BTW: if cells is a Uint8Array then you don't need type: gl.UNSIGNED_BYTE. twgl will set type to gl.UNSIGNED_BYTE based on the fact that the data is Uint8Array.

1
Blindman67 On

The library you are using is not consistently defaulting undefined properties. Setting normalized to true along some path for Uint8Array and Int8Array array types.

Normalizing the attribute buffer data

The normalize argument if true in WebGLRenderingContext.vertexAttribPointer will normalize integer within a unit range. For signed -1 to 1 and for unsigned 0 to 1.

This results in the color attribute is set to 1 / 255 when you use an array of type Uint8Array and dont define the value ofnormalized`

You can scale the value back to 1 in the vertex shader.

Example vertex shader

attribute float color;

//... code

varying vec4 colorOut;
void main() {

    // ... code

    colorOut = vec4(0, color * 255.0, 0, 1);  // scale the color by 255
}

Example

Showing how normalized changes attribute data. The red is is not normalized and the green is. Note that this does not use any library.

const GL_OPTIONS = {alpha: false, depth: false, premultpliedAlpha: false, preserveDrawingBufer: true};
const CELL_SIZE = 10, GRID_SIZE = 32, GRID_PX_WIDTH = GRID_SIZE * CELL_SIZE, CELLS = GRID_SIZE ** 2;
const GL_SETUP = {
    get context() { return  canvas.getContext("webgl", GL_OPTIONS) },
    get locations() { return ["A_pos", "A_green", "A_red", "U_res"] },  
    get vertex() { return `
attribute vec2 pos;
attribute float green;
attribute float red;
uniform vec2 res;
varying vec4 colorV;
void main() {
    gl_PointSize = ${(CELL_SIZE - 2).toFixed(1)};
    gl_Position = vec4(pos / res, 0, 1);
    colorV = vec4(red, green * 255.0, 0, 1);
}`;
    },
    get fragment() { return `
precision highp float;
varying vec4 colorV;
void main() {
    gl_FragColor = colorV;
}`;
    },
    get buffers() {
        return {
            pos: { buffer: GL_SETUP.grid },
            red: { size: 1, type: gl.UNSIGNED_BYTE, normalize: false, buffer: GL_SETUP.colorsRed },
            green: { size: 1, type: gl.UNSIGNED_BYTE, normalize: true, buffer: GL_SETUP.colorsGreen }
        };
    },
    get grid() {
        var i = CELLS, buf = new Float32Array(i * 2), idx;
        while (i--) {
            idx = i << 1;
            buf[idx++]  = (i % GRID_SIZE) * CELL_SIZE - GRID_PX_WIDTH / 2;
            buf[idx] = (i / GRID_SIZE | 0) * CELL_SIZE - GRID_PX_WIDTH / 2;
        }
        return buf;
    },
    get colorsRed() {
        var i = CELLS, buf = new Uint8Array(i);
        while(i--) { buf[i] = Math.random() < 0.5 && i < CELLS / 2 ? 1 : 0 }
        return buf; 
    },    
    get colorsGreen() {
        var i = CELLS, buf = new Uint8Array(i);
        while(i--) { buf[i] = Math.random() < 0.5 && i >= CELLS / 2 ? 1 : 0 }
        return buf; 
    },    
};

const gl = GL_SETUP.context;
const shader = initShader(gl, GL_SETUP);
const W = canvas.width = innerWidth, H = canvas.height = innerHeight;
gl.viewport(0, 0, W, H);
gl.uniform2fv(shader.locations.res, new Float32Array([W / 2 , -H / 2]));
gl.drawArrays(gl.POINTS, 0, CELLS);

function compileAndAttach(gl, program, src, type = gl.VERTEX_SHADER) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, src);
    gl.compileShader(shader);   
    gl.attachShader(program, shader);
}
function createShader(gl, settings) {
    const locations = {}, program = gl.createProgram();
    compileAndAttach(gl, program, settings.vertex);
    compileAndAttach(gl, program, settings.fragment, gl.FRAGMENT_SHADER);
    gl.linkProgram(program);
    gl.useProgram(program);
    for(const desc of settings.locations) {
        const [type, name] = desc.split("_");
        locations[name] = gl[`get${type==="A" ? "Attrib" : "Uniform"}Location`](program, name);
    }
    return {program, locations, gl};
}
function initShader(gl, settings) {
    const shader = createShader(gl, settings);
    for (const [name, data] of Object.entries(settings.buffers)) {
        const {use = gl.STATIC_DRAW, type = gl.FLOAT, buffer, size = 2, normalize = false} = data;
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
        gl.bufferData(gl.ARRAY_BUFFER, buffer, use);
        gl.enableVertexAttribArray(shader.locations[name]);
        gl.vertexAttribPointer(shader.locations[name], size, type, normalize, 0, 0);
    }
    return shader;
}
body { padding: 0px }
canvas {
  position: absolute;
  top: 0px;
  left: 0px;
}
<canvas id="canvas"></canvas>