'use strict' var ndarray = require('ndarray') var ops = require('ndarray-ops') var pool = require('typedarray-pool') module.exports = createTexture2D var linearTypes = null var filterTypes = null var wrapTypes = null function lazyInitLinearTypes(gl) { linearTypes = [ gl.LINEAR, gl.NEAREST_MIPMAP_LINEAR, gl.LINEAR_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_NEAREST ] filterTypes = [ gl.NEAREST, gl.LINEAR, gl.NEAREST_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR, gl.LINEAR_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_LINEAR ] wrapTypes = [ gl.REPEAT, gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT ] } function acceptTextureDOM (obj) { return ( ('undefined' != typeof HTMLCanvasElement && obj instanceof HTMLCanvasElement) || ('undefined' != typeof HTMLImageElement && obj instanceof HTMLImageElement) || ('undefined' != typeof HTMLVideoElement && obj instanceof HTMLVideoElement) || ('undefined' != typeof ImageData && obj instanceof ImageData)) } var convertFloatToUint8 = function(out, inp) { ops.muls(out, inp, 255.0) } function reshapeTexture(tex, w, h) { var gl = tex.gl var maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) if(w < 0 || w > maxSize || h < 0 || h > maxSize) { throw new Error('gl-texture2d: Invalid texture size') } tex._shape = [w, h] tex.bind() gl.texImage2D(gl.TEXTURE_2D, 0, tex.format, w, h, 0, tex.format, tex.type, null) tex._mipLevels = [0] return tex } function Texture2D(gl, handle, width, height, format, type) { this.gl = gl this.handle = handle this.format = format this.type = type this._shape = [width, height] this._mipLevels = [0] this._magFilter = gl.NEAREST this._minFilter = gl.NEAREST this._wrapS = gl.CLAMP_TO_EDGE this._wrapT = gl.CLAMP_TO_EDGE this._anisoSamples = 1 var parent = this var wrapVector = [this._wrapS, this._wrapT] Object.defineProperties(wrapVector, [ { get: function() { return parent._wrapS }, set: function(v) { return parent.wrapS = v } }, { get: function() { return parent._wrapT }, set: function(v) { return parent.wrapT = v } } ]) this._wrapVector = wrapVector var shapeVector = [this._shape[0], this._shape[1]] Object.defineProperties(shapeVector, [ { get: function() { return parent._shape[0] }, set: function(v) { return parent.width = v } }, { get: function() { return parent._shape[1] }, set: function(v) { return parent.height = v } } ]) this._shapeVector = shapeVector } var proto = Texture2D.prototype Object.defineProperties(proto, { minFilter: { get: function() { return this._minFilter }, set: function(v) { this.bind() var gl = this.gl if(this.type === gl.FLOAT && linearTypes.indexOf(v) >= 0) { if(!gl.getExtension('OES_texture_float_linear')) { v = gl.NEAREST } } if(filterTypes.indexOf(v) < 0) { throw new Error('gl-texture2d: Unknown filter mode ' + v) } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, v) return this._minFilter = v } }, magFilter: { get: function() { return this._magFilter }, set: function(v) { this.bind() var gl = this.gl if(this.type === gl.FLOAT && linearTypes.indexOf(v) >= 0) { if(!gl.getExtension('OES_texture_float_linear')) { v = gl.NEAREST } } if(filterTypes.indexOf(v) < 0) { throw new Error('gl-texture2d: Unknown filter mode ' + v) } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, v) return this._magFilter = v } }, mipSamples: { get: function() { return this._anisoSamples }, set: function(i) { var psamples = this._anisoSamples this._anisoSamples = Math.max(i, 1)|0 if(psamples !== this._anisoSamples) { var ext = this.gl.getExtension('EXT_texture_filter_anisotropic') if(ext) { this.gl.texParameterf(this.gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, this._anisoSamples) } } return this._anisoSamples } }, wrapS: { get: function() { return this._wrapS }, set: function(v) { this.bind() if(wrapTypes.indexOf(v) < 0) { throw new Error('gl-texture2d: Unknown wrap mode ' + v) } this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, v) return this._wrapS = v } }, wrapT: { get: function() { return this._wrapT }, set: function(v) { this.bind() if(wrapTypes.indexOf(v) < 0) { throw new Error('gl-texture2d: Unknown wrap mode ' + v) } this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, v) return this._wrapT = v } }, wrap: { get: function() { return this._wrapVector }, set: function(v) { if(!Array.isArray(v)) { v = [v,v] } if(v.length !== 2) { throw new Error('gl-texture2d: Must specify wrap mode for rows and columns') } for(var i=0; i<2; ++i) { if(wrapTypes.indexOf(v[i]) < 0) { throw new Error('gl-texture2d: Unknown wrap mode ' + v) } } this._wrapS = v[0] this._wrapT = v[1] var gl = this.gl this.bind() gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._wrapS) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._wrapT) return v } }, shape: { get: function() { return this._shapeVector }, set: function(x) { if(!Array.isArray(x)) { x = [x|0,x|0] } else { if(x.length !== 2) { throw new Error('gl-texture2d: Invalid texture shape') } } reshapeTexture(this, x[0]|0, x[1]|0) return [x[0]|0, x[1]|0] } }, width: { get: function() { return this._shape[0] }, set: function(w) { w = w|0 reshapeTexture(this, w, this._shape[1]) return w } }, height: { get: function() { return this._shape[1] }, set: function(h) { h = h|0 reshapeTexture(this, this._shape[0], h) return h } } }) proto.bind = function(unit) { var gl = this.gl if(unit !== undefined) { gl.activeTexture(gl.TEXTURE0 + (unit|0)) } gl.bindTexture(gl.TEXTURE_2D, this.handle) if(unit !== undefined) { return (unit|0) } return gl.getParameter(gl.ACTIVE_TEXTURE) - gl.TEXTURE0 } proto.dispose = function() { this.gl.deleteTexture(this.handle) } proto.generateMipmap = function() { this.bind() this.gl.generateMipmap(this.gl.TEXTURE_2D) //Update mip levels var l = Math.min(this._shape[0], this._shape[1]) for(var i=0; l>0; ++i, l>>>=1) { if(this._mipLevels.indexOf(i) < 0) { this._mipLevels.push(i) } } } proto.setPixels = function(data, x_off, y_off, mip_level) { var gl = this.gl this.bind() if(Array.isArray(x_off)) { mip_level = y_off y_off = x_off[1]|0 x_off = x_off[0]|0 } else { x_off = x_off || 0 y_off = y_off || 0 } mip_level = mip_level || 0 var directData = acceptTextureDOM(data) ? data : data.raw if(directData) { var needsMip = this._mipLevels.indexOf(mip_level) < 0 if(needsMip) { gl.texImage2D(gl.TEXTURE_2D, 0, this.format, this.format, this.type, directData) this._mipLevels.push(mip_level) } else { gl.texSubImage2D(gl.TEXTURE_2D, mip_level, x_off, y_off, this.format, this.type, directData) } } else if(data.shape && data.stride && data.data) { if(data.shape.length < 2 || x_off + data.shape[1] > this._shape[1]>>>mip_level || y_off + data.shape[0] > this._shape[0]>>>mip_level || x_off < 0 || y_off < 0) { throw new Error('gl-texture2d: Texture dimensions are out of bounds') } texSubImageArray(gl, x_off, y_off, mip_level, this.format, this.type, this._mipLevels, data) } else { throw new Error('gl-texture2d: Unsupported data type') } } function isPacked(shape, stride) { if(shape.length === 3) { return (stride[2] === 1) && (stride[1] === shape[0]*shape[2]) && (stride[0] === shape[2]) } return (stride[0] === 1) && (stride[1] === shape[0]) } function texSubImageArray(gl, x_off, y_off, mip_level, cformat, ctype, mipLevels, array) { var dtype = array.dtype var shape = array.shape.slice() if(shape.length < 2 || shape.length > 3) { throw new Error('gl-texture2d: Invalid ndarray, must be 2d or 3d') } var type = 0, format = 0 var packed = isPacked(shape, array.stride.slice()) if(dtype === 'float32') { type = gl.FLOAT } else if(dtype === 'float64') { type = gl.FLOAT packed = false dtype = 'float32' } else if(dtype === 'uint8') { type = gl.UNSIGNED_BYTE } else { type = gl.UNSIGNED_BYTE packed = false dtype = 'uint8' } var channels = 1 if(shape.length === 2) { format = gl.LUMINANCE shape = [shape[0], shape[1], 1] array = ndarray(array.data, shape, [array.stride[0], array.stride[1], 1], array.offset) } else if(shape.length === 3) { if(shape[2] === 1) { format = gl.ALPHA } else if(shape[2] === 2) { format = gl.LUMINANCE_ALPHA } else if(shape[2] === 3) { format = gl.RGB } else if(shape[2] === 4) { format = gl.RGBA } else { throw new Error('gl-texture2d: Invalid shape for pixel coords') } channels = shape[2] } else { throw new Error('gl-texture2d: Invalid shape for texture') } //For 1-channel textures allow conversion between formats if((format === gl.LUMINANCE || format === gl.ALPHA) && (cformat === gl.LUMINANCE || cformat === gl.ALPHA)) { format = cformat } if(format !== cformat) { throw new Error('gl-texture2d: Incompatible texture format for setPixels') } var size = array.size var needsMip = mipLevels.indexOf(mip_level) < 0 if(needsMip) { mipLevels.push(mip_level) } if(type === ctype && packed) { //Array data types are compatible, can directly copy into texture if(array.offset === 0 && array.data.length === size) { if(needsMip) { gl.texImage2D(gl.TEXTURE_2D, mip_level, cformat, shape[0], shape[1], 0, cformat, ctype, array.data) } else { gl.texSubImage2D(gl.TEXTURE_2D, mip_level, x_off, y_off, shape[0], shape[1], cformat, ctype, array.data) } } else { if(needsMip) { gl.texImage2D(gl.TEXTURE_2D, mip_level, cformat, shape[0], shape[1], 0, cformat, ctype, array.data.subarray(array.offset, array.offset+size)) } else { gl.texSubImage2D(gl.TEXTURE_2D, mip_level, x_off, y_off, shape[0], shape[1], cformat, ctype, array.data.subarray(array.offset, array.offset+size)) } } } else { //Need to do type conversion to pack data into buffer var pack_buffer if(ctype === gl.FLOAT) { pack_buffer = pool.mallocFloat32(size) } else { pack_buffer = pool.mallocUint8(size) } var pack_view = ndarray(pack_buffer, shape, [shape[2], shape[2]*shape[0], 1]) if(type === gl.FLOAT && ctype === gl.UNSIGNED_BYTE) { convertFloatToUint8(pack_view, array) } else { ops.assign(pack_view, array) } if(needsMip) { gl.texImage2D(gl.TEXTURE_2D, mip_level, cformat, shape[0], shape[1], 0, cformat, ctype, pack_buffer.subarray(0, size)) } else { gl.texSubImage2D(gl.TEXTURE_2D, mip_level, x_off, y_off, shape[0], shape[1], cformat, ctype, pack_buffer.subarray(0, size)) } if(ctype === gl.FLOAT) { pool.freeFloat32(pack_buffer) } else { pool.freeUint8(pack_buffer) } } } function initTexture(gl) { var tex = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, tex) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) return tex } function createTextureShape(gl, width, height, format, type) { var maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) if(width < 0 || width > maxTextureSize || height < 0 || height > maxTextureSize) { throw new Error('gl-texture2d: Invalid texture shape') } if(type === gl.FLOAT && !gl.getExtension('OES_texture_float')) { throw new Error('gl-texture2d: Floating point textures not supported on this platform') } var tex = initTexture(gl) gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null) return new Texture2D(gl, tex, width, height, format, type) } function createTextureDOM(gl, directData, width, height, format, type) { var tex = initTexture(gl) gl.texImage2D(gl.TEXTURE_2D, 0, format, format, type, directData) return new Texture2D(gl, tex, width, height, format, type) } //Creates a texture from an ndarray function createTextureArray(gl, array) { var dtype = array.dtype var shape = array.shape.slice() var maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) if(shape[0] < 0 || shape[0] > maxSize || shape[1] < 0 || shape[1] > maxSize) { throw new Error('gl-texture2d: Invalid texture size') } var packed = isPacked(shape, array.stride.slice()) var type = 0 if(dtype === 'float32') { type = gl.FLOAT } else if(dtype === 'float64') { type = gl.FLOAT packed = false dtype = 'float32' } else if(dtype === 'uint8') { type = gl.UNSIGNED_BYTE } else { type = gl.UNSIGNED_BYTE packed = false dtype = 'uint8' } var format = 0 if(shape.length === 2) { format = gl.LUMINANCE shape = [shape[0], shape[1], 1] array = ndarray(array.data, shape, [array.stride[0], array.stride[1], 1], array.offset) } else if(shape.length === 3) { if(shape[2] === 1) { format = gl.ALPHA } else if(shape[2] === 2) { format = gl.LUMINANCE_ALPHA } else if(shape[2] === 3) { format = gl.RGB } else if(shape[2] === 4) { format = gl.RGBA } else { throw new Error('gl-texture2d: Invalid shape for pixel coords') } } else { throw new Error('gl-texture2d: Invalid shape for texture') } if(type === gl.FLOAT && !gl.getExtension('OES_texture_float')) { type = gl.UNSIGNED_BYTE packed = false } var buffer, buf_store var size = array.size if(!packed) { var stride = [shape[2], shape[2]*shape[0], 1] buf_store = pool.malloc(size, dtype) var buf_array = ndarray(buf_store, shape, stride, 0) if((dtype === 'float32' || dtype === 'float64') && type === gl.UNSIGNED_BYTE) { convertFloatToUint8(buf_array, array) } else { ops.assign(buf_array, array) } buffer = buf_store.subarray(0, size) } else if (array.offset === 0 && array.data.length === size) { buffer = array.data } else { buffer = array.data.subarray(array.offset, array.offset + size) } var tex = initTexture(gl) gl.texImage2D(gl.TEXTURE_2D, 0, format, shape[0], shape[1], 0, format, type, buffer) if(!packed) { pool.free(buf_store) } return new Texture2D(gl, tex, shape[0], shape[1], format, type) } function createTexture2D(gl) { if(arguments.length <= 1) { throw new Error('gl-texture2d: Missing arguments for texture2d constructor') } if(!linearTypes) { lazyInitLinearTypes(gl) } if(typeof arguments[1] === 'number') { return createTextureShape(gl, arguments[1], arguments[2], arguments[3]||gl.RGBA, arguments[4]||gl.UNSIGNED_BYTE) } if(Array.isArray(arguments[1])) { return createTextureShape(gl, arguments[1][0]|0, arguments[1][1]|0, arguments[2]||gl.RGBA, arguments[3]||gl.UNSIGNED_BYTE) } if(typeof arguments[1] === 'object') { var obj = arguments[1] var directData = acceptTextureDOM(obj) ? obj : obj.raw if (directData) { return createTextureDOM(gl, directData, obj.width|0, obj.height|0, arguments[2]||gl.RGBA, arguments[3]||gl.UNSIGNED_BYTE) } else if(obj.shape && obj.data && obj.stride) { return createTextureArray(gl, obj) } } throw new Error('gl-texture2d: Invalid arguments for texture2d constructor') }