Webgl helper / easier utility for binding attributes

also a bit of improvement on the doc
This commit is contained in:
Olivier Guyot
2019-09-24 18:15:29 +02:00
parent 30f19f8317
commit 076b674d6f
2 changed files with 164 additions and 22 deletions

View File

@@ -14,7 +14,7 @@ import {
} from '../transform.js';
import {create, fromTransform} from '../vec/mat4.js';
import WebGLPostProcessingPass from './PostProcessingPass.js';
import {getContext, getSupportedExtensions} from '../webgl.js';
import {FLOAT, getContext, getSupportedExtensions, UNSIGNED_BYTE, UNSIGNED_INT, UNSIGNED_SHORT} from '../webgl.js';
import {includes} from '../array.js';
import {assert} from '../asserts.js';
@@ -59,6 +59,28 @@ export const DefaultAttrib = {
COLOR: 'a_color'
};
/**
* Attribute types, either `UNSIGNED_BYTE`, `UNSIGNED_SHORT`, `UNSIGNED_INT` or `FLOAT`
* Note: an attribute stored in a `Float32Array` should be of type `FLOAT`.
* @enum {number}
*/
export const AttributeType = {
UNSIGNED_BYTE: UNSIGNED_BYTE,
UNSIGNED_SHORT: UNSIGNED_SHORT,
UNSIGNED_INT: UNSIGNED_INT,
FLOAT: FLOAT
};
/**
* Description of an attribute in a buffer
* @typedef {Object} AttributeDescription
* @property {string} name Attribute name to use in shaders
* @property {number} size Number of components per attributes
* @property {AttributeType} [type] Attribute type, i.e. number of bytes used to store the value. This is
* determined by the class of typed array which the buffer uses (eg. `Float32Array` for a `FLOAT` attribute).
* Default is `FLOAT`.
*/
/**
* @typedef {number|Array<number>|HTMLCanvasElement|HTMLImageElement|ImageData|import("../transform").Transform} UniformLiteralValue
*/
@@ -122,14 +144,14 @@ export const DefaultAttrib = {
* // for subsequent rendering calls
* const vertexShader = new WebGLVertex(VERTEX_SHADER);
* const fragmentShader = new WebGLFragment(FRAGMENT_SHADER);
* this.program = this.context.getProgram(fragmentShader, vertexShader);
* this.context.useProgram(this.program);
* const program = this.context.getProgram(fragmentShader, vertexShader);
* helper.useProgram(this.program);
* ```
*
* Uniforms are defined using the `uniforms` option and can either be explicit values or callbacks taking the frame state as argument.
* You can also change their value along the way like so:
* ```js
* this.context.setUniformFloatValue('u_value', valueAsNumber);
* helper.setUniformFloatValue('u_value', valueAsNumber);
* ```
*
* ### Defining post processing passes
@@ -162,32 +184,42 @@ export const DefaultAttrib = {
* Examples below:
* ```js
* // at initialization phase
* this.verticesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
* this.indicesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
* const verticesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
* const indicesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
*
* // when array values have changed
* this.context.flushBufferData(ARRAY_BUFFER, this.verticesBuffer);
* this.context.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
* helper.flushBufferData(ARRAY_BUFFER, this.verticesBuffer);
* helper.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
*
* // at rendering phase
* this.context.bindBuffer(ARRAY_BUFFER, this.verticesBuffer);
* this.context.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
* helper.bindBuffer(ARRAY_BUFFER, this.verticesBuffer);
* helper.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
* ```
*
* ### Specifying attributes
*
* The GPU only receives the data as arrays of numbers. These numbers must be handled differently depending on what it describes (position, texture coordinate...).
* Attributes are used to specify these uses. Use {@link enableAttributeArray} and either
* Attributes are used to specify these uses. Use {@link enableAttributeArray_} and either
* the default attribute names in {@link module:ol/webgl/Helper.DefaultAttrib} or custom ones.
*
* Please note that you will have to specify the type and offset of the attributes in the data array. You can refer to the documentation of [WebGLRenderingContext.vertexAttribPointer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer) for more explanation.
* ```js
* // here we indicate that the data array has the following structure:
* // [posX, posY, offsetX, offsetY, texCoordU, texCoordV, posX, posY, ...]
* let bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
* this.context.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * 6, 0);
* this.context.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * 6, bytesPerFloat * 2);
* this.context.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * 6, bytesPerFloat * 4);
* helper.enableAttributes([
* {
* name: 'a_position',
* size: 2
* },
* {
* name: 'a_offset',
* size: 2
* },
* {
* name: 'a_texCoord',
* size: 2
* }
* ])
* ```
*
* ### Rendering primitives
@@ -195,13 +227,13 @@ export const DefaultAttrib = {
* Once all the steps above have been achieved, rendering primitives to the screen is done using {@link prepareDraw}, {@link drawElements} and {@link finalizeDraw}.
* ```js
* // frame preparation step
* this.context.prepareDraw(frameState);
* helper.prepareDraw(frameState);
*
* // call this for every data array that has to be rendered on screen
* this.context.drawElements(0, this.indicesBuffer.getArray().length);
* helper.drawElements(0, this.indicesBuffer.getArray().length);
*
* // finalize the rendering by applying post processes
* this.context.finalizeDraw(frameState);
* helper.finalizeDraw(frameState);
* ```
*
* For an example usage of this class, refer to {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
@@ -543,6 +575,7 @@ class WebGLHelper extends Disposable {
let textureSlot = 0;
this.uniforms_.forEach(function(uniform) {
value = typeof uniform.value === 'function' ? uniform.value(frameState) : uniform.value;
// apply value based on type
if (value instanceof HTMLCanvasElement || value instanceof HTMLImageElement || value instanceof ImageData) {
// create a texture & put data
@@ -734,15 +767,16 @@ class WebGLHelper extends Disposable {
}
/**
* Will set the currently bound buffer to an attribute of the shader program
* Will set the currently bound buffer to an attribute of the shader program. Used by `#enableAttributes`
* internally.
* @param {string} attribName Attribute name
* @param {number} size Number of components per attributes
* @param {number} type UNSIGNED_INT, UNSIGNED_BYTE, UNSIGNED_SHORT or FLOAT
* @param {number} stride Stride in bytes (0 means attribs are packed)
* @param {number} offset Offset in bytes
* @api
* @private
*/
enableAttributeArray(attribName, size, type, stride, offset) {
enableAttributeArray_(attribName, size, type, stride, offset) {
const location = this.getAttributeLocation(attribName);
// the attribute has not been found in the shaders; do not enable it
if (location < 0) {
@@ -753,6 +787,23 @@ class WebGLHelper extends Disposable {
false, stride, offset);
}
/**
* Will enable the following attributes to be read from the currently bound buffer,
* i.e. tell the GPU where to read the different attributes in the buffer. An error in the
* size/type/order of attributes will most likely break the rendering and throw a WebGL exception.
* @param {Array<AttributeDescription>} attributes Ordered list of attributes to read from the buffer
* @api
*/
enableAttributes(attributes) {
const stride = computeAttributesStride(attributes);
let offset = 0;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
this.enableAttributeArray_(attr.name, attr.size, attr.type || FLOAT, stride, offset);
offset += attr.size * getByteSizeFromType(attr.type);
}
}
/**
* WebGL context was lost
* @private
@@ -808,4 +859,34 @@ class WebGLHelper extends Disposable {
}
}
/**
* Compute a stride based on a list of attributes
* @param {Array<AttributeDescription>} attributes Ordered list of attributes
* @returns {number} Stride, ie amount of values for each vertex in the vertex buffer
* @api
*/
export function computeAttributesStride(attributes) {
let stride = 0;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
stride += attr.size * getByteSizeFromType(attr.type);
}
return stride;
}
/**
* Computes the size in byte of an attribute type.
* @param {AttributeType} type Attribute type
* @returns {number} The size in bytes
*/
function getByteSizeFromType(type) {
switch (type) {
case AttributeType.UNSIGNED_BYTE: return Uint8Array.BYTES_PER_ELEMENT;
case AttributeType.UNSIGNED_SHORT: return Uint16Array.BYTES_PER_ELEMENT;
case AttributeType.UNSIGNED_INT: return Uint32Array.BYTES_PER_ELEMENT;
case AttributeType.FLOAT:
default: return Float32Array.BYTES_PER_ELEMENT;
}
}
export default WebGLHelper;

View File

@@ -1,9 +1,10 @@
import WebGLHelper from '../../../../src/ol/webgl/Helper.js';
import WebGLHelper, {AttributeType, computeAttributesStride} from '../../../../src/ol/webgl/Helper.js';
import {
create as createTransform,
rotate as rotateTransform,
scale as scaleTransform, translate as translateTransform
} from '../../../../src/ol/transform.js';
import {FLOAT} from '../../../../src/ol/webgl.js';
const VERTEX_SHADER = `
@@ -290,4 +291,64 @@ describe('ol.webgl.WebGLHelper', function() {
});
});
});
describe('#enableAttributes', function() {
let baseAttrs, h;
beforeEach(function() {
h = new WebGLHelper();
baseAttrs = [
{
name: 'attr1',
size: 3
},
{
name: 'attr2',
size: 2
},
{
name: 'attr3',
size: 1
}
];
h.useProgram(h.getProgram(FRAGMENT_SHADER, `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix;
attribute vec3 attr1;
attribute vec2 attr2;
attribute float attr3;
uniform float u_test;
void main(void) {
gl_Position = vec4(u_test, a_test, 0.0, 1.0);
}`));
});
it('enables attributes based on the given array (FLOAT)', function() {
const spy = sinon.spy(h, 'enableAttributeArray_');
h.enableAttributes(baseAttrs);
const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
expect(spy.callCount).to.eql(3);
expect(spy.getCall(0).args[0]).to.eql('attr1');
expect(spy.getCall(0).args[1]).to.eql(3);
expect(spy.getCall(0).args[2]).to.eql(FLOAT);
expect(spy.getCall(0).args[3]).to.eql(6 * bytesPerFloat);
expect(spy.getCall(0).args[4]).to.eql(0);
expect(spy.getCall(1).args[0]).to.eql('attr2');
expect(spy.getCall(1).args[1]).to.eql(2);
expect(spy.getCall(1).args[2]).to.eql(FLOAT);
expect(spy.getCall(1).args[3]).to.eql(6 * bytesPerFloat);
expect(spy.getCall(1).args[4]).to.eql(3 * bytesPerFloat);
expect(spy.getCall(2).args[0]).to.eql('attr3');
expect(spy.getCall(2).args[1]).to.eql(1);
expect(spy.getCall(2).args[2]).to.eql(FLOAT);
expect(spy.getCall(2).args[3]).to.eql(6 * bytesPerFloat);
expect(spy.getCall(2).args[4]).to.eql(5 * bytesPerFloat);
});
});
});