'use strict';
const WebGLContext = require('./WebGLContext');
const MODES = {
POINTS: true,
LINES: true,
LINE_STRIP: true,
LINE_LOOP: true,
TRIANGLES: true,
TRIANGLE_STRIP: true,
TRIANGLE_FAN: true
};
const TYPES = {
BYTE: true,
UNSIGNED_BYTE: true,
SHORT: true,
UNSIGNED_SHORT: true,
FIXED: true,
FLOAT: true
};
const BYTES_PER_TYPE = {
BYTE: 1,
UNSIGNED_BYTE: 1,
SHORT: 2,
UNSIGNED_SHORT: 2,
FIXED: 4,
FLOAT: 4
};
const SIZES = {
1: true,
2: true,
3: true,
4: true
};
/**
* The default attribute point byte offset.
* @private
* @constant {number}
*/
const DEFAULT_BYTE_OFFSET = 0;
/**
* The default render mode (primitive type).
* @private
* @constant {string}
*/
const DEFAULT_MODE = 'TRIANGLES';
/**
* The default index offset to render from.
* @private
* @constant {number}
*/
const DEFAULT_INDEX_OFFSET = 0;
/**
* The default count of indices to render.
* @private
* @constant {number}
*/
const DEFAULT_COUNT = 0;
/**
* Parse the attribute pointers and determine the byte stride of the buffer.
*
* @private
*
* @param {Map} attributePointers - The attribute pointer map.
*
* @returns {number} The byte stride of the buffer.
*/
function getStride(attributePointers) {
// if there is only one attribute pointer assigned to this buffer,
// there is no need for stride, set to default of 0
if (attributePointers.size === 1) {
return 0;
}
let maxByteOffset = 0;
let byteSizeSum = 0;
let byteStride = 0;
attributePointers.forEach(pointer => {
const byteOffset = pointer.byteOffset;
const size = pointer.size;
const type = pointer.type;
// track the sum of each attribute size
byteSizeSum += size * BYTES_PER_TYPE[type];
// track the largest offset to determine the byte stride of the buffer
if (byteOffset > maxByteOffset) {
maxByteOffset = byteOffset;
byteStride = byteOffset + (size * BYTES_PER_TYPE[type]);
}
});
// check if the max byte offset is greater than or equal to the the sum of
// the sizes. If so this buffer is not interleaved and does not need a
// stride.
if (maxByteOffset >= byteSizeSum) {
// TODO: test what stride === 0 does for an interleaved buffer of
// length === 1.
return 0;
}
return byteStride;
}
/**
* Parse the attribute pointers to ensure they are valid.
*
* @private
*
* @param {Object} attributePointers - The attribute pointer map.
*
* @returns {Object} The validated attribute pointer map.
*/
function getAttributePointers(attributePointers) {
// parse pointers to ensure they are valid
const pointers = new Map();
Object.keys(attributePointers).forEach(key => {
const index = parseInt(key, 10);
// check that key is an valid integer
if (isNaN(index)) {
throw `Attribute index \`${key}\` does not represent an integer`;
}
const pointer = attributePointers[key];
const size = pointer.size;
const type = pointer.type;
const byteOffset = pointer.byteOffset;
// check size
if (!SIZES[size]) {
throw 'Attribute pointer `size` parameter is invalid, must be one of ' +
JSON.stringify(Object.keys(SIZES));
}
// check type
if (!TYPES[type]) {
throw 'Attribute pointer `type` parameter is invalid, must be one of ' +
JSON.stringify(Object.keys(TYPES));
}
pointers.set(index, {
size: size,
type: type,
byteOffset: (byteOffset !== undefined) ? byteOffset : DEFAULT_BYTE_OFFSET
});
});
return pointers;
}
/**
* A vertex buffer object.
*/
class VertexBuffer {
/**
* Instantiates an VertexBuffer object.
*
* @param {WebGLBuffer|VertexPackage|ArrayBuffer|Array|Number} arg - The buffer or length of the buffer.
* @param {Object} attributePointers - The array pointer map, or in the case of a vertex package arg, the options.
* @param {Object} options - The rendering options.
* @param {string} options.mode - The draw mode / primitive type.
* @param {string} options.indexOffset - The index offset into the drawn buffer.
* @param {string} options.count - The number of indices to draw.
*/
constructor(arg, attributePointers = {}, options = {}) {
this.gl = WebGLContext.get();
this.buffer = null;
this.mode = MODES[options.mode] ? options.mode : DEFAULT_MODE;
this.count = (options.count !== undefined) ? options.count : DEFAULT_COUNT;
this.indexOffset = (options.indexOffset !== undefined) ? options.indexOffset : DEFAULT_INDEX_OFFSET;
// first, set the attribute pointers
this.pointers = getAttributePointers(attributePointers);
// set the byte stride
this.byteStride = getStride(this.pointers);
// then buffer the data
if (arg) {
if (arg instanceof WebGLBuffer) {
// WebGLBuffer argument
this.buffer = arg;
} else {
// Array or ArrayBuffer or number argument
this.bufferData(arg);
}
}
}
/**
* Upload vertex data to the GPU.
*
* @param {Array|ArrayBuffer|ArrayBufferView|number} arg - The array of data to buffer, or size of the buffer in bytes.
*
* @returns {VertexBuffer} The vertex buffer object, for chaining.
*/
bufferData(arg) {
const gl = this.gl;
// ensure argument is valid
if (Array.isArray(arg)) {
// cast array into Float32Array
arg = new Float32Array(arg);
} else if (
!(arg instanceof ArrayBuffer) &&
!(ArrayBuffer.isView(arg)) &&
!(Number.isInteger(arg))
) {
// if not arraybuffer or a numeric size
throw 'Argument must be of type `Array`, `ArrayBuffer`, `ArrayBufferView`, or `Number`';
}
// create buffer if it doesn't exist already
if (!this.buffer) {
this.buffer = gl.createBuffer();
}
// buffer the data
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, arg, gl.STATIC_DRAW);
}
/**
* Upload partial vertex data to the GPU.
*
* @param {Array|ArrayBuffer} array - The array of data to buffer.
* @param {number} byteOffset - The byte offset at which to buffer.
*
* @returns {VertexBuffer} The vertex buffer object, for chaining.
*/
bufferSubData(array, byteOffset = DEFAULT_BYTE_OFFSET) {
const gl = this.gl;
// ensure the buffer exists
if (!this.buffer) {
throw 'Buffer has not yet been allocated, allocate with `bufferData`';
}
// ensure argument is valid
if (Array.isArray(array)) {
array = new Float32Array(array);
} else if (
!(array instanceof ArrayBuffer) &&
!ArrayBuffer.isView(array)
) {
throw 'Argument must be of type `Array`, `ArrayBuffer`, or `ArrayBufferView`';
}
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, byteOffset, array);
return this;
}
/**
* Binds the vertex buffer object.
*
* @returns {VertexBuffer} - Returns the vertex buffer object for chaining.
*/
bind() {
const gl = this.gl;
// bind buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
// for each attribute pointer
this.pointers.forEach((pointer, index) => {
// set attribute pointer
gl.vertexAttribPointer(
index,
pointer.size,
gl[pointer.type],
false,
this.byteStride,
pointer.byteOffset);
// enable attribute index
gl.enableVertexAttribArray(index);
});
return this;
}
/**
* Unbinds the vertex buffer object.
*
* @returns {VertexBuffer} The vertex buffer object, for chaining.
*/
unbind() {
const gl = this.gl;
// unbind buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
this.pointers.forEach((pointer, index) => {
// disable attribute index
gl.disableVertexAttribArray(index);
});
return this;
}
/**
* Execute the draw command for the bound buffer.
*
* @param {Object} options - The options to pass to 'drawArrays'. Optional.
* @param {string} options.mode - The draw mode / primitive type.
* @param {string} options.indexOffset - The index offset into the drawn buffer.
* @param {string} options.count - The number of indices to draw.
*
* @returns {VertexBuffer} The vertex buffer object, for chaining.
*/
draw(options = {}) {
const gl = this.gl;
const mode = gl[options.mode || this.mode];
const indexOffset = (options.indexOffset !== undefined) ? options.indexOffset : this.indexOffset;
const count = (options.count !== undefined) ? options.count : this.count;
if (count === 0) {
throw 'Attempting to draw with a count of 0';
}
// draw elements
gl.drawArrays(mode, indexOffset, count);
return this;
}
}
module.exports = VertexBuffer;