/* * TSDGLDataBuffer.js * Keynote HTML Player * * Created by Tungwei Cheng * Copyright (c) 2018 Apple Inc. All rights reserved. */ var CHAR_MAX = 127; var UCHAR_MAX = 255; var SHRT_MAX = 32767; var USHRT_MAX = 65535; /* Boolean */ var GL_FALSE = 0; var GL_TRUE = 1; /* BeginMode */ var GL_POINTS = 0x0000; var GL_LINES = 0x0001; var GL_LINE_LOOP = 0x0002; var GL_LINE_STRIP = 0x0003; var GL_TRIANGLES = 0x0004; var GL_TRIANGLE_STRIP = 0x0005; var GL_TRIANGLE_FAN = 0x0006; /* DataType */ var GL_BYTE = 0x1400; var GL_UNSIGNED_BYTE = 0x1401; var GL_SHORT = 0x1402; var GL_UNSIGNED_SHORT = 0x1403; var GL_INT = 0x1404; var GL_UNSIGNED_INT = 0x1405; var GL_FLOAT = 0x1406; var GL_DOUBLE = 0x140A; var GL_STREAM_DRAW = 0x88E0; var GL_STATIC_DRAW = 0x88E4; var GL_DYNAMIC_DRAW = 0x88E8; var GL_FLOAT_VEC2 = 0x8B50; var GL_FLOAT_VEC3 = 0x8B51; var GL_FLOAT_VEC4 = 0x8B52; var GL_INT_VEC2 = 0x8B53; var GL_INT_VEC3 = 0x8B54; var GL_INT_VEC4 = 0x8B55; var GL_BOOL = 0x8B56; var GL_BOOL_VEC2 = 0x8B57; var GL_BOOL_VEC3 = 0x8B58; var GL_BOOL_VEC4 = 0x8B59; var GL_FLOAT_MAT2 = 0x8B5A; var GL_FLOAT_MAT3 = 0x8B5B; var GL_FLOAT_MAT4 = 0x8B5C; var GL_SAMPLER_1D = 0x8B5D; var GL_SAMPLER_2D = 0x8B5E; var GL_SAMPLER_3D = 0x8B5F; var GL_SAMPLER_CUBE = 0x8B60; var TSDGLDataBufferDataTypeUnknown = 0; var TSDGLDataBufferDataTypeByte = GL_BYTE; var TSDGLDataBufferDataTypeUnsignedByte = GL_UNSIGNED_BYTE; var TSDGLDataBufferDataTypeShort = GL_SHORT; var TSDGLDataBufferDataTypeUnsignedShort = GL_UNSIGNED_SHORT; var TSDGLDataBufferDataTypeFloat = GL_FLOAT; function TSDGLDataBufferDataTypeAsGLEnum(dataType) { var result = 0; switch (dataType) { case TSDGLDataBufferDataTypeByte: result = GL_BYTE; break; case TSDGLDataBufferDataTypeUnsignedByte: result = GL_UNSIGNED_BYTE; break; case TSDGLDataBufferDataTypeUnsignedShort: result = GL_UNSIGNED_SHORT; break; case TSDGLDataBufferDataTypeShort: result = GL_SHORT; break; case TSDGLDataBufferDataTypeFloat: result = GL_FLOAT; break; case TSDGLDataBufferDataTypeUnknown: console.log("Unknown TSDGLdataBufferDataType!"); break; } return result; } function TSDGLDataBufferDataTypeSize(dataType) { var result = 0; switch (dataType) { case GL_BYTE: result = 1; break; case GL_UNSIGNED_BYTE: result = 1; break; case GL_SHORT: result = 2; break; case GL_UNSIGNED_SHORT: result = 2; break; case GL_FLOAT: result = 4; break; default: break; } return result; } function TSDGLPoint2DByteFromPoint2D(aPoint, isNormalized) { var x = TSDGLbyteFromFloat(aPoint.x, isNormalized); var y = TSDGLbyteFromFloat(aPoint.y, isNormalized); var p = new Int8Array(2); p.set([x, y], 0); return p; } function TSDGLbyteFromFloat(aFloat, isNormalized) { if (isNormalized) { aFloat *= CHAR_MAX; } return aFloat; } function TSDGLPoint2DUnsignedByteFromPoint2D(aPoint, isNormalized) { var x = TSDGLubyteFromFloat(aPoint.x, isNormalized); var y = TSDGLubyteFromFloat(aPoint.y, isNormalized); var p = new Uint8Array(2); p.set([x, y], 0); return p; } function TSDGLubyteFromFloat(aFloat, isNormalized) { if (isNormalized) { aFloat *= UCHAR_MAX; } return aFloat; } function TSDGLPoint2DShortFromPoint2D(aPoint, isNormalized) { var x = TSDGLshortFromFloat(aPoint.x, isNormalized); var y = TSDGLshortFromFloat(aPoint.y, isNormalized); var p = new Int16Array(4); p.set([x, y], 0); return p; } function TSDGLshortFromFloat(aFloat, isNormalized) { if (isNormalized) { aFloat *= SHRT_MAX; } return aFloat; } function TSDGLPoint2DUnsignedShortFromPoint2D(aPoint, isNormalized) { var x = TSDGLushortFromFloat(aPoint.x, isNormalized); var y = TSDGLushortFromFloat(aPoint.y, isNormalized); var p = new Uint16Array(4); p.set([x, y], 0); return p; } function TSDGLushortFromFloat(aFloat, isNormalized) { if (isNormalized) { aFloat *= USHRT_MAX; } return aFloat; } function TSDGLDataBufferSetGLPoint2DWithDataType(mGLData, bufferOffset, dataType, isNormalized, aPoint2D) { switch (dataType) { case TSDGLDataBufferDataTypeByte: var value = TSDGLPoint2DByteFromPoint2D(aPoint2D, isNormalized); var typedArray = new Int8Array(mGLData); typedArray.set(value, bufferOffset); break; case TSDGLDataBufferDataTypeUnsignedByte: var value = TSDGLPoint2DUnsignedByteFromPoint2D(aPoint2D, isNormalized); var typedArray = new Uint8Array(mGLData); typedArray.set(value, bufferOffset); break; case TSDGLDataBufferDataTypeShort: var value = TSDGLPoint2DShortFromPoint2D(aPoint2D, isNormalized); var typedArray = new Int16Array(mGLData); typedArray.set(value, bufferOffset/2); break; case TSDGLDataBufferDataTypeUnsignedShort: var value = TSDGLPoint2DUnsignedShortFromPoint2D(aPoint2D, isNormalized); var typedArray = new Uint16Array(mGLData); typedArray.set(value, bufferOffset/2); break; case TSDGLDataBufferDataTypeFloat: var typedArray = new Float32Array(mGLData); typedArray.set([aPoint2D.x, aPoint2D.y], bufferOffset/4); break; case TSDGLDataBufferDataTypeUnknown: console.log("Unknown data type!"); break; } } var TSDGLDataBufferAttribute = Class.create({ initialize: function(name, bufferUsage, dataType, normalized, componentCount) { this.locationInShader = -1; this.bufferOffset = null; this.dataArrayBuffer = null; this.dataBuffer = null; this.initWithName(name, bufferUsage, dataType, normalized, componentCount); }, initWithName: function(attributeName, bufferUsage, dataType, isNormalized, componentCount) { this.name = attributeName; this.bufferUsage = bufferUsage; this.dataType = dataType; if (this.dataType === GL_SHORT) { this.dataType = GL_FLOAT; } this.componentCount = componentCount; this.isNormalized = isNormalized; this.locationInShader = -1; } }); var TSDGLDataArrayBuffer = Class.create({ initialize: function(gl) { this.gl = gl; this._vertexAttributes = null; this.mVertexCount = 0; // data type size in bytes this._dataTypeSizeInBytes = 0; // GL_STATIC_DRAW, GL_STREAM_DRAW, etc this._bufferUsage = 0; this.mNeedsUpdateFirstIndex = []; this.mNeedsUpdateLastIndex = []; this.mGLData = null; // GL vertex data buffer this.mGLDataBufferHasBeenSetup = false; this.mGLDataBuffers = []; this.mAttributeOffsetsDictionary = null; this.GLDataBufferEntrySize = 0; // for double-buffering this.bufferCount = 1; this.currentBufferIndex = 0; }, initWithVertexAttributes: function(attributes, vertexCount, bufferCount) { this._vertexAttributes = attributes.slice(); this.mVertexCount = vertexCount; this.mAttributeOffsetsDictionary = {}; // Sort the attributes into buffers by buffer usage type var bufferSizeIfFloats = 0; var bufferOffset = 0; for (var i = 0, length = this._vertexAttributes.length; i < length; i++) { var attribute = this._vertexAttributes[i]; // Assign data array buffer to attribute attribute.dataArrayBuffer = this; var dataTypeSizeInBytes = TSDGLDataBufferDataTypeSize(attribute.dataType); if (this._bufferUsage === 0) { this._bufferUsage = attribute.bufferUsage; } // Assign buffer offset attribute.bufferOffset = bufferOffset; var paddedSize = attribute.componentCount * dataTypeSizeInBytes; paddedSize = (paddedSize + 3) & ~3; bufferOffset += paddedSize; bufferSizeIfFloats += attribute.componentCount * 4; } // Create the buffer data (if necessary) this.GLDataBufferEntrySize = bufferOffset; // We need to give the arraybuffer a size if (this.GLDataBufferEntrySize > 0) { this.mGLData = new ArrayBuffer(this.mVertexCount * this.GLDataBufferEntrySize); } this.bufferCount = bufferCount; this.mNeedsUpdateFirstIndex = []; this.mNeedsUpdateLastIndex = []; for (var i = 0; i < bufferCount; i++) { this.mNeedsUpdateFirstIndex[i] = -1; this.mNeedsUpdateLastIndex[i] = -1; } }, p_setupGLDataBufferIfNecessary: function() { var gl = this.gl; // Sets up GL buffers if (this.mGLDataBufferHasBeenSetup) { return; } for (var i = 0; i < this.bufferCount; i++) { this.mGLDataBuffers[i] = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.mGLDataBuffers[i]); gl.bufferData(gl.ARRAY_BUFFER, this.mGLData, this._bufferUsage); this.mNeedsUpdateFirstIndex[i] = -1; this.mNeedsUpdateLastIndex[i] = -1; } this.mGLDataBufferHasBeenSetup = true; }, updateDataBufferIfNecessary: function() { this.p_setupGLDataBufferIfNecessary(); if (!this.hasUpdatedData()) { // Nothing needs to be updated! return; } if (this._bufferUsage == GL_STATIC_DRAW) { console.log("We're GL_STATIC_DRAW but trying (and FAILING) to update the array after initial setup!"); return; } var gl = this.gl; // Combine all buffer's updated ranges, in case they're not the same... var firstIndex = Number.MAX_SAFE_INTEGER; var lastIndex = -1; for (var i = 0; i < this.bufferCount; i++) { var thisFirstIndex = this.mNeedsUpdateFirstIndex[i]; if (thisFirstIndex !== -1) { firstIndex = Math.min(firstIndex, thisFirstIndex); } var thisLastIndex = this.mNeedsUpdateLastIndex[i]; if (thisLastIndex !== -1) { lastIndex = Math.max(lastIndex, this.mNeedsUpdateLastIndex[i]); } } var offset = firstIndex; var size = lastIndex + 1 - firstIndex; offset *= this.GLDataBufferEntrySize; size *= this.GLDataBufferEntrySize; gl.bindBuffer(gl.ARRAY_BUFFER, this.mGLDataBuffers[this.currentBufferIndex]); gl.bufferSubData(gl.ARRAY_BUFFER, offset, this.mGLData); this.mNeedsUpdateFirstIndex[this.currentBufferIndex] = -1; this.mNeedsUpdateLastIndex[this.currentBufferIndex] = -1; }, p_bufferOffsetOfAttribute: function(attribute, index, component) { var bufferOffset = index * this.GLDataBufferEntrySize; bufferOffset += attribute.bufferOffset; if (component !== 0) { bufferOffset += TSDGLDataBufferDataTypeSize(attribute.dataType) * component; } return bufferOffset; }, setGLPoint2D: function(aPoint2D, attribute, index) { var bufferOffset = this.p_bufferOffsetOfAttribute(attribute, index, 0); TSDGLDataBufferSetGLPoint2DWithDataType(this.mGLData, bufferOffset, attribute.dataType, attribute.isNormalized, aPoint2D); this.addIndexNeedsUpdate(index); }, enableVertexAttributeArrayBuffersWithShader: function(shader) { var gl = this.gl; this.updateDataBufferIfNecessary(); gl.bindBuffer(gl.ARRAY_BUFFER, this.mGLDataBuffers[this.currentBufferIndex]); for (var i = 0, length = this._vertexAttributes.length; i < length; i++) { var attribute = this._vertexAttributes[i]; var locationInShader = attribute.locationInShader; if (locationInShader === -1) { locationInShader = shader.locationForAttribute(attribute.name); if (locationInShader === -1) { console.log("Could not find attribute " + attribute.name + "in shader!"); } attribute.locationInShader = locationInShader; } var stride = 0; if (this._vertexAttributes.length > 1) { // we're not tight-packed, so need to specify how many elements to skip when iterating stride = this.GLDataBufferEntrySize; } var dataType = TSDGLDataBufferDataTypeAsGLEnum(attribute.dataType); gl.enableVertexAttribArray(locationInShader); gl.vertexAttribPointer(locationInShader, attribute.componentCount, dataType, attribute.isNormalized ? GL_TRUE : GL_FALSE, stride, attribute.bufferOffset); } }, disableVertexAttributeArrayBuffersWithShader: function(shader) { var gl = this.gl; for (var i = 0, length = this._vertexAttributes.length; i < length; i++) { var attribute = this._vertexAttributes[i]; gl.disableVertexAttribArray(attribute.locationInShader); } gl.bindBuffer(gl.ARRAY_BUFFER, null); }, hasUpdatedData: function() { for (var i = 0; i < this.bufferCount; i++) { if (this.mNeedsUpdateFirstIndex[i] !== -1) { return true; } } return false; }, addIndexNeedsUpdate: function(index) { var currentBufferIndex = this.currentBufferIndex; var mNeedsUpdateFirstIndex = this.mNeedsUpdateFirstIndex; var mNeedsUpdateLastIndex = this.mNeedsUpdateLastIndex; mNeedsUpdateFirstIndex[currentBufferIndex] = (mNeedsUpdateFirstIndex[currentBufferIndex] == -1) ? index : Math.min(mNeedsUpdateFirstIndex[currentBufferIndex], index); mNeedsUpdateLastIndex[currentBufferIndex] = (mNeedsUpdateLastIndex[currentBufferIndex] == -1) ? index : Math.max(mNeedsUpdateLastIndex[currentBufferIndex], index); } }); var TSDGLDataBuffer = Class.create({ initialize: function(gl) { this.gl = gl; this.mCurrentBufferIndex = 0; this.mArrayBuffers = []; this.mAttributeToArrayBuffersDictionary = {}; // Element array buffer this.mElementArrayCount = 0; this.mGLElementData = null; this.mGLElementDataBufferWasSetup = false; this.mGLElementDataBuffer = null; this.mGLElementMeshSize = { width: 0, height: 0 } this.mGLElementQuadParticleCount = 0; }, p_setupGLElementArrayBufferIfNecessary: function() { var gl = this.gl; if (this.mGLElementDataBufferWasSetup) { return; } if (!this.mGLElementData) { this.mGLElementDataBufferWasSetup = true; return; } var useIndexCounter = false; var indexCounter = 0; if (!CGSizeEqualToSize(this.mGLElementMeshSize, CGSizeZero)) { useIndexCounter = true; // set up grid-based element array data for (var y = 0; y < this.mGLElementMeshSize.height - 1; ++y) { for (var x = 0; x < this.mGLElementMeshSize.width; ++x) { this.setGLushort((y + 0) * this.mGLElementMeshSize.width + x, indexCounter++); this.setGLushort((y + 1) * this.mGLElementMeshSize.width + x, indexCounter++); } } } else if (this.mGLElementQuadParticleCount != 0) { useIndexCounter = true; this.drawMode = GL_TRIANGLES; // set up quad particle-based element array data for (var i = 0; i < this.mGLElementQuadParticleCount; ++i) { // First triangle this.setGLushort((4 * i + 0), indexCounter++); this.setGLushort((4 * i + 1), indexCounter++); this.setGLushort((4 * i + 2), indexCounter++); // Second triangle this.setGLushort((4 * i + 0), indexCounter++); this.setGLushort((4 * i + 2), indexCounter++); this.setGLushort((4 * i + 3), indexCounter++); } } this.mGLElementDataBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.mGLElementDataBuffer); this.mGLElementDataBufferWasSetup = true; }, newDataBufferWithVertexAttributes: function(attributes, meshSize, isDoubleBuffered) { var vertexCount = meshSize.width * meshSize.height; var indexCount = meshSize.width * 2 * (meshSize.height - 1); this.initWithVertexAttributesDesignated(attributes, vertexCount, indexCount, isDoubleBuffered); this.mGLElementMeshSize = meshSize; }, initWithVertexAttributes: function(attributes, meshSize) { var vertexCount = meshSize.width * meshSize.height; var indexCount = meshSize.width * 2 * (meshSize.height - 1); this.initWithVertexAttributesDesignated(attributes, vertexCount, indexCount, false); this.mGLElementMeshSize = meshSize; }, initWithVertexAttributesDesignated: function(attributes, vertexCount, indexElementCount, isDoubleBuffered) { this._doubleBuffered = isDoubleBuffered; this.drawMode = GL_TRIANGLE_STRIP; this._vertexAttributes = attributes; this._vertexCount = vertexCount; this.mArrayBuffers = []; this.mAttributeToArrayBuffersDictionary = {}; var attributesToArrange = attributes.slice(); while (attributesToArrange.length > 0) { var thisAttribute = attributesToArrange[0]; var currentAttributes = []; for (var i = 0, length = attributesToArrange.length; i < length; i++) { var attribute = attributesToArrange[i]; if (attribute.bufferUsage == thisAttribute.bufferUsage) { currentAttributes.push(attribute); } } var bufferCount = ((isDoubleBuffered && thisAttribute.bufferUsage !== GL_STATIC_DRAW) ? 2 : 1); var arrayBuffer = new TSDGLDataArrayBuffer(this.gl); arrayBuffer.initWithVertexAttributes(currentAttributes, vertexCount, bufferCount); for (var i = 0, length = currentAttributes.length; i < length; i++) { var attribute = currentAttributes[i]; // this will cause circular reference attribute.dataBuffer = this; this.mAttributeToArrayBuffersDictionary[attribute.name] = arrayBuffer; } this.mArrayBuffers.push(arrayBuffer); for (var i = 0, length = currentAttributes.length; i < length; i++) { var element = currentAttributes[i]; attributesToArrange.splice(attributesToArrange.indexOf(element), 1); } } if (indexElementCount > 0) { this.mElementArrayCount = indexElementCount; this.mGLElementData = new ArrayBuffer(this.mElementArrayCount * 2); } }, initWithVertexRect: function(vertexRect, textureRect, meshSize, isTextureFlipped, includeCenterAttribute) { var gl = this.gl; var shouldSetupTexCoords = !CGRectEqualToRect(textureRect, CGRectZero); var quadAttributes = []; var positionAttribute = new TSDGLDataBufferAttribute("Position", GL_STATIC_DRAW, GL_FLOAT, false, 2); quadAttributes.push(positionAttribute); var texCoordAttribute; if (shouldSetupTexCoords) { var dataType = GL_SHORT; if (CGRectEqualToRect(textureRect, CGRectMake(0, 0, 1, 1)) && CGSizeEqualToSize(meshSize, CGSizeMake(2, 2))) { // If we're just passing in the unit rectangle, we can use lower precision texcoords! dataType = GL_UNSIGNED_BYTE; } texCoordAttribute = new TSDGLDataBufferAttribute("TexCoord", GL_STATIC_DRAW, dataType, true, 2); quadAttributes.push(texCoordAttribute); } var centerAttribute; if (includeCenterAttribute) { centerAttribute = new TSDGLDataBufferAttribute("Center", GL_STATIC_DRAW, GL_FLOAT, false, 2); quadAttributes.push(centerAttribute); } this.initWithVertexAttributes(quadAttributes, meshSize); var index = 0; // This is TSDGLPoint2D in native which is a struct of float type var center = TSDCenterOfRect(vertexRect); var verticesWide = parseInt(meshSize.width - 1); var verticesHigh = parseInt(meshSize.height - 1); for (var row = 0; row <= verticesHigh; ++row) { for (var col = 0; col <= verticesWide; ++col) { var point = WebGraphics.makePoint(col / verticesWide, row / verticesHigh); // This is TSDGLPoint2D in native which is a struct of float type var vertex = TSDPointFromNormalizedRect(point, vertexRect); this.setGLPoint2D(vertex, positionAttribute, index); if (shouldSetupTexCoords) { var texCoord = TSDPointFromNormalizedRect(point, textureRect); if (isTextureFlipped) { texCoord = WebGraphics.makePoint(texCoord.x, 1.0 - texCoord.y); } this.setGLPoint2D(texCoord, texCoordAttribute, index); } if (includeCenterAttribute) { this.setGLPoint2D(center, centerAttribute, index); } index++; } } }, setGLPoint2D: function(aPoint2D, attribute, index) { attribute.dataArrayBuffer.setGLPoint2D(aPoint2D, attribute, index); }, setGLushort: function(aShort, index) { var bufferOffset = index; var typedArray = new Uint16Array(this.mGLElementData); typedArray.set([aShort], bufferOffset); }, enableElementArrayBuffer: function() { var gl = this.gl; this.p_setupGLElementArrayBufferIfNecessary(); if (this.mGLElementDataBufferWasSetup) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.mGLElementDataBuffer); } }, disableElementArrayBuffer: function() { var gl = this.gl; if (this.mGLElementDataBufferWasSetup) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); } }, enableDataBufferWithShader: function(shader) { // Vertex Array Object is an expension in WebGL and currently not implemented in Safari if (!shader.isActive) { shader.activate(); } for (var i = 0, length = this.mArrayBuffers.length; i < length; i++) { var buffer = this.mArrayBuffers[i]; buffer.enableVertexAttributeArrayBuffersWithShader(shader); } this.enableElementArrayBuffer(); this._enabledShader = shader; this._isEnabled = true; }, disableDataBufferWithShader: function(shader) { if (!this._isEnabled) { return; } this.disableElementArrayBuffer(); for (var i = 0, length = this.mArrayBuffers.length; i < length; i++) { var buffer = this.mArrayBuffers[i]; buffer.disableVertexAttributeArrayBuffersWithShader(shader); } this._enabledShader = null; this._isEnabled = false; }, drawWithShader: function(shader, shouldDeactivateShader) { var gl = this.gl; var range = { location: 0, length: this.mElementArrayCount > 0 ? this.mElementArrayCount : this._vertexCount }; this.enableDataBufferWithShader(shader); if (this.mGLElementDataBufferWasSetup && this.mElementArrayCount > 0) { // we need to send element data to element array buffer, e.g. [0, 2, 1, 3] gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.mGLElementData, gl.STATIC_DRAW); if (!CGSizeEqualToSize(this.mGLElementMeshSize, CGSizeZero)) { // Draw mesh by rows var width = this.mGLElementMeshSize.width; for (var y = 0; y < this.mGLElementMeshSize.height - 1; ++y) { // location is vertex location, so need to multiply by two to get index location gl.drawElements(this.drawMode, width * 2, gl.UNSIGNED_SHORT, 2 * y * width * 2); } } else { // just draw everything gl.drawElements(this.drawMode, range.length, gl.UNSIGNED_SHORT, 2 * range.location); } } else { // No element data; just pass vertices straight down gl.drawArrays(this.drawMode, range.location, range.length); } this.disableDataBufferWithShader(shader); // Swap buffers if (this.isDoubleBuffered) { this.mCurrentBufferIndex = (this.mCurrentBufferIndex + 1) % 2; for (var i = 0, length = this.mArrayBuffers.length; i < length; i++) { var buffer = this.mArrayBuffers[i]; if (buffer.bufferCount != 1) { buffer.currentBufferIndex = this.mCurrentBufferIndex; } } } if (shouldDeactivateShader) { shader.deactivate(); } }, vertexAttributeNamed: function(attributeName) { for (var attrib in this._vertexAttributes) { var attribute = this._vertexAttributes[attrib]; if (attribute.name === attributeName) { return attribute; } } return null; } });