var check = require('./util/check') var extend = require('./util/extend') var values = require('./util/values') var isTypedArray = require('./util/is-typed-array') var isNDArrayLike = require('./util/is-ndarray') var pool = require('./util/pool') var convertToHalfFloat = require('./util/to-half-float') var isArrayLike = require('./util/is-array-like') var flattenUtils = require('./util/flatten') var isPow2 = require('./util/is-pow2') var dtypes = require('./constants/arraytypes.json') var arrayTypes = require('./constants/arraytypes.json') var GL_COMPRESSED_TEXTURE_FORMATS = 0x86A3 var GL_TEXTURE_2D = 0x0DE1 var GL_TEXTURE_CUBE_MAP = 0x8513 var GL_TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515 var GL_RGBA = 0x1908 var GL_ALPHA = 0x1906 var GL_RGB = 0x1907 var GL_LUMINANCE = 0x1909 var GL_LUMINANCE_ALPHA = 0x190A var GL_RGBA4 = 0x8056 var GL_RGB5_A1 = 0x8057 var GL_RGB565 = 0x8D62 var GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033 var GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034 var GL_UNSIGNED_SHORT_5_6_5 = 0x8363 var GL_UNSIGNED_INT_24_8_WEBGL = 0x84FA var GL_DEPTH_COMPONENT = 0x1902 var GL_DEPTH_STENCIL = 0x84F9 var GL_SRGB_EXT = 0x8C40 var GL_SRGB_ALPHA_EXT = 0x8C42 var GL_HALF_FLOAT_OES = 0x8D61 var GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0 var GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1 var GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2 var GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3 var GL_COMPRESSED_RGB_ATC_WEBGL = 0x8C92 var GL_COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL = 0x8C93 var GL_COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL = 0x87EE var GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00 var GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01 var GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02 var GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03 var GL_COMPRESSED_RGB_ETC1_WEBGL = 0x8D64 var GL_UNSIGNED_BYTE = 0x1401 var GL_UNSIGNED_SHORT = 0x1403 var GL_UNSIGNED_INT = 0x1405 var GL_FLOAT = 0x1406 var GL_TEXTURE_WRAP_S = 0x2802 var GL_TEXTURE_WRAP_T = 0x2803 var GL_REPEAT = 0x2901 var GL_CLAMP_TO_EDGE = 0x812F var GL_MIRRORED_REPEAT = 0x8370 var GL_TEXTURE_MAG_FILTER = 0x2800 var GL_TEXTURE_MIN_FILTER = 0x2801 var GL_NEAREST = 0x2600 var GL_LINEAR = 0x2601 var GL_NEAREST_MIPMAP_NEAREST = 0x2700 var GL_LINEAR_MIPMAP_NEAREST = 0x2701 var GL_NEAREST_MIPMAP_LINEAR = 0x2702 var GL_LINEAR_MIPMAP_LINEAR = 0x2703 var GL_GENERATE_MIPMAP_HINT = 0x8192 var GL_DONT_CARE = 0x1100 var GL_FASTEST = 0x1101 var GL_NICEST = 0x1102 var GL_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE var GL_UNPACK_ALIGNMENT = 0x0CF5 var GL_UNPACK_FLIP_Y_WEBGL = 0x9240 var GL_UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241 var GL_UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243 var GL_BROWSER_DEFAULT_WEBGL = 0x9244 var GL_TEXTURE0 = 0x84C0 var MIPMAP_FILTERS = [ GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR ] var CHANNELS_FORMAT = [ 0, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA ] var FORMAT_CHANNELS = {} FORMAT_CHANNELS[GL_LUMINANCE] = FORMAT_CHANNELS[GL_ALPHA] = FORMAT_CHANNELS[GL_DEPTH_COMPONENT] = 1 FORMAT_CHANNELS[GL_DEPTH_STENCIL] = FORMAT_CHANNELS[GL_LUMINANCE_ALPHA] = 2 FORMAT_CHANNELS[GL_RGB] = FORMAT_CHANNELS[GL_SRGB_EXT] = 3 FORMAT_CHANNELS[GL_RGBA] = FORMAT_CHANNELS[GL_SRGB_ALPHA_EXT] = 4 var formatTypes = {} formatTypes[GL_RGBA4] = GL_UNSIGNED_SHORT_4_4_4_4 formatTypes[GL_RGB565] = GL_UNSIGNED_SHORT_5_6_5 formatTypes[GL_RGB5_A1] = GL_UNSIGNED_SHORT_5_5_5_1 formatTypes[GL_DEPTH_COMPONENT] = GL_UNSIGNED_INT formatTypes[GL_DEPTH_STENCIL] = GL_UNSIGNED_INT_24_8_WEBGL function objectName (str) { return '[object ' + str + ']' } var CANVAS_CLASS = objectName('HTMLCanvasElement') var OFFSCREENCANVAS_CLASS = objectName('OffscreenCanvas') var CONTEXT2D_CLASS = objectName('CanvasRenderingContext2D') var BITMAP_CLASS = objectName('ImageBitmap') var IMAGE_CLASS = objectName('HTMLImageElement') var VIDEO_CLASS = objectName('HTMLVideoElement') var PIXEL_CLASSES = Object.keys(dtypes).concat([ CANVAS_CLASS, OFFSCREENCANVAS_CLASS, CONTEXT2D_CLASS, BITMAP_CLASS, IMAGE_CLASS, VIDEO_CLASS ]) // for every texture type, store // the size in bytes. var TYPE_SIZES = [] TYPE_SIZES[GL_UNSIGNED_BYTE] = 1 TYPE_SIZES[GL_FLOAT] = 4 TYPE_SIZES[GL_HALF_FLOAT_OES] = 2 TYPE_SIZES[GL_UNSIGNED_SHORT] = 2 TYPE_SIZES[GL_UNSIGNED_INT] = 4 var FORMAT_SIZES_SPECIAL = [] FORMAT_SIZES_SPECIAL[GL_RGBA4] = 2 FORMAT_SIZES_SPECIAL[GL_RGB5_A1] = 2 FORMAT_SIZES_SPECIAL[GL_RGB565] = 2 FORMAT_SIZES_SPECIAL[GL_DEPTH_STENCIL] = 4 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGB_S3TC_DXT1_EXT] = 0.5 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_S3TC_DXT1_EXT] = 0.5 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_S3TC_DXT3_EXT] = 1 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_S3TC_DXT5_EXT] = 1 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGB_ATC_WEBGL] = 0.5 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL] = 1 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL] = 1 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG] = 0.5 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG] = 0.25 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG] = 0.5 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG] = 0.25 FORMAT_SIZES_SPECIAL[GL_COMPRESSED_RGB_ETC1_WEBGL] = 0.5 function isNumericArray (arr) { return ( Array.isArray(arr) && (arr.length === 0 || typeof arr[0] === 'number')) } function isRectArray (arr) { if (!Array.isArray(arr)) { return false } var width = arr.length if (width === 0 || !isArrayLike(arr[0])) { return false } return true } function classString (x) { return Object.prototype.toString.call(x) } function isCanvasElement (object) { return classString(object) === CANVAS_CLASS } function isOffscreenCanvas (object) { return classString(object) === OFFSCREENCANVAS_CLASS } function isContext2D (object) { return classString(object) === CONTEXT2D_CLASS } function isBitmap (object) { return classString(object) === BITMAP_CLASS } function isImageElement (object) { return classString(object) === IMAGE_CLASS } function isVideoElement (object) { return classString(object) === VIDEO_CLASS } function isPixelData (object) { if (!object) { return false } var className = classString(object) if (PIXEL_CLASSES.indexOf(className) >= 0) { return true } return ( isNumericArray(object) || isRectArray(object) || isNDArrayLike(object)) } function typedArrayCode (data) { return arrayTypes[Object.prototype.toString.call(data)] | 0 } function convertData (result, data) { var n = data.length switch (result.type) { case GL_UNSIGNED_BYTE: case GL_UNSIGNED_SHORT: case GL_UNSIGNED_INT: case GL_FLOAT: var converted = pool.allocType(result.type, n) converted.set(data) result.data = converted break case GL_HALF_FLOAT_OES: result.data = convertToHalfFloat(data) break default: check.raise('unsupported texture type, must specify a typed array') } } function preConvert (image, n) { return pool.allocType( image.type === GL_HALF_FLOAT_OES ? GL_FLOAT : image.type, n) } function postConvert (image, data) { if (image.type === GL_HALF_FLOAT_OES) { image.data = convertToHalfFloat(data) pool.freeType(data) } else { image.data = data } } function transposeData (image, array, strideX, strideY, strideC, offset) { var w = image.width var h = image.height var c = image.channels var n = w * h * c var data = preConvert(image, n) var p = 0 for (var i = 0; i < h; ++i) { for (var j = 0; j < w; ++j) { for (var k = 0; k < c; ++k) { data[p++] = array[strideX * j + strideY * i + strideC * k + offset] } } } postConvert(image, data) } function getTextureSize (format, type, width, height, isMipmap, isCube) { var s if (typeof FORMAT_SIZES_SPECIAL[format] !== 'undefined') { // we have a special array for dealing with weird color formats such as RGB5A1 s = FORMAT_SIZES_SPECIAL[format] } else { s = FORMAT_CHANNELS[format] * TYPE_SIZES[type] } if (isCube) { s *= 6 } if (isMipmap) { // compute the total size of all the mipmaps. var total = 0 var w = width while (w >= 1) { // we can only use mipmaps on a square image, // so we can simply use the width and ignore the height: total += s * w * w w /= 2 } return total } else { return s * width * height } } module.exports = function createTextureSet ( gl, extensions, limits, reglPoll, contextState, stats, config) { // ------------------------------------------------------- // Initialize constants and parameter tables here // ------------------------------------------------------- var mipmapHint = { "don't care": GL_DONT_CARE, 'dont care': GL_DONT_CARE, 'nice': GL_NICEST, 'fast': GL_FASTEST } var wrapModes = { 'repeat': GL_REPEAT, 'clamp': GL_CLAMP_TO_EDGE, 'mirror': GL_MIRRORED_REPEAT } var magFilters = { 'nearest': GL_NEAREST, 'linear': GL_LINEAR } var minFilters = extend({ 'mipmap': GL_LINEAR_MIPMAP_LINEAR, 'nearest mipmap nearest': GL_NEAREST_MIPMAP_NEAREST, 'linear mipmap nearest': GL_LINEAR_MIPMAP_NEAREST, 'nearest mipmap linear': GL_NEAREST_MIPMAP_LINEAR, 'linear mipmap linear': GL_LINEAR_MIPMAP_LINEAR }, magFilters) var colorSpace = { 'none': 0, 'browser': GL_BROWSER_DEFAULT_WEBGL } var textureTypes = { 'uint8': GL_UNSIGNED_BYTE, 'rgba4': GL_UNSIGNED_SHORT_4_4_4_4, 'rgb565': GL_UNSIGNED_SHORT_5_6_5, 'rgb5 a1': GL_UNSIGNED_SHORT_5_5_5_1 } var textureFormats = { 'alpha': GL_ALPHA, 'luminance': GL_LUMINANCE, 'luminance alpha': GL_LUMINANCE_ALPHA, 'rgb': GL_RGB, 'rgba': GL_RGBA, 'rgba4': GL_RGBA4, 'rgb5 a1': GL_RGB5_A1, 'rgb565': GL_RGB565 } var compressedTextureFormats = {} if (extensions.ext_srgb) { textureFormats.srgb = GL_SRGB_EXT textureFormats.srgba = GL_SRGB_ALPHA_EXT } if (extensions.oes_texture_float) { textureTypes.float32 = textureTypes.float = GL_FLOAT } if (extensions.oes_texture_half_float) { textureTypes['float16'] = textureTypes['half float'] = GL_HALF_FLOAT_OES } if (extensions.webgl_depth_texture) { extend(textureFormats, { 'depth': GL_DEPTH_COMPONENT, 'depth stencil': GL_DEPTH_STENCIL }) extend(textureTypes, { 'uint16': GL_UNSIGNED_SHORT, 'uint32': GL_UNSIGNED_INT, 'depth stencil': GL_UNSIGNED_INT_24_8_WEBGL }) } if (extensions.webgl_compressed_texture_s3tc) { extend(compressedTextureFormats, { 'rgb s3tc dxt1': GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 'rgba s3tc dxt1': GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, 'rgba s3tc dxt3': GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, 'rgba s3tc dxt5': GL_COMPRESSED_RGBA_S3TC_DXT5_EXT }) } if (extensions.webgl_compressed_texture_atc) { extend(compressedTextureFormats, { 'rgb atc': GL_COMPRESSED_RGB_ATC_WEBGL, 'rgba atc explicit alpha': GL_COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL, 'rgba atc interpolated alpha': GL_COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL }) } if (extensions.webgl_compressed_texture_pvrtc) { extend(compressedTextureFormats, { 'rgb pvrtc 4bppv1': GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, 'rgb pvrtc 2bppv1': GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG, 'rgba pvrtc 4bppv1': GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, 'rgba pvrtc 2bppv1': GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG }) } if (extensions.webgl_compressed_texture_etc1) { compressedTextureFormats['rgb etc1'] = GL_COMPRESSED_RGB_ETC1_WEBGL } // Copy over all texture formats var supportedCompressedFormats = Array.prototype.slice.call( gl.getParameter(GL_COMPRESSED_TEXTURE_FORMATS)) Object.keys(compressedTextureFormats).forEach(function (name) { var format = compressedTextureFormats[name] if (supportedCompressedFormats.indexOf(format) >= 0) { textureFormats[name] = format } }) var supportedFormats = Object.keys(textureFormats) limits.textureFormats = supportedFormats // associate with every format string its // corresponding GL-value. var textureFormatsInvert = [] Object.keys(textureFormats).forEach(function (key) { var val = textureFormats[key] textureFormatsInvert[val] = key }) // associate with every type string its // corresponding GL-value. var textureTypesInvert = [] Object.keys(textureTypes).forEach(function (key) { var val = textureTypes[key] textureTypesInvert[val] = key }) var magFiltersInvert = [] Object.keys(magFilters).forEach(function (key) { var val = magFilters[key] magFiltersInvert[val] = key }) var minFiltersInvert = [] Object.keys(minFilters).forEach(function (key) { var val = minFilters[key] minFiltersInvert[val] = key }) var wrapModesInvert = [] Object.keys(wrapModes).forEach(function (key) { var val = wrapModes[key] wrapModesInvert[val] = key }) // colorFormats[] gives the format (channels) associated to an // internalformat var colorFormats = supportedFormats.reduce(function (color, key) { var glenum = textureFormats[key] if (glenum === GL_LUMINANCE || glenum === GL_ALPHA || glenum === GL_LUMINANCE || glenum === GL_LUMINANCE_ALPHA || glenum === GL_DEPTH_COMPONENT || glenum === GL_DEPTH_STENCIL || (extensions.ext_srgb && (glenum === GL_SRGB_EXT || glenum === GL_SRGB_ALPHA_EXT))) { color[glenum] = glenum } else if (glenum === GL_RGB5_A1 || key.indexOf('rgba') >= 0) { color[glenum] = GL_RGBA } else { color[glenum] = GL_RGB } return color }, {}) function TexFlags () { // format info this.internalformat = GL_RGBA this.format = GL_RGBA this.type = GL_UNSIGNED_BYTE this.compressed = false // pixel storage this.premultiplyAlpha = false this.flipY = false this.unpackAlignment = 1 this.colorSpace = GL_BROWSER_DEFAULT_WEBGL // shape info this.width = 0 this.height = 0 this.channels = 0 } function copyFlags (result, other) { result.internalformat = other.internalformat result.format = other.format result.type = other.type result.compressed = other.compressed result.premultiplyAlpha = other.premultiplyAlpha result.flipY = other.flipY result.unpackAlignment = other.unpackAlignment result.colorSpace = other.colorSpace result.width = other.width result.height = other.height result.channels = other.channels } function parseFlags (flags, options) { if (typeof options !== 'object' || !options) { return } if ('premultiplyAlpha' in options) { check.type(options.premultiplyAlpha, 'boolean', 'invalid premultiplyAlpha') flags.premultiplyAlpha = options.premultiplyAlpha } if ('flipY' in options) { check.type(options.flipY, 'boolean', 'invalid texture flip') flags.flipY = options.flipY } if ('alignment' in options) { check.oneOf(options.alignment, [1, 2, 4, 8], 'invalid texture unpack alignment') flags.unpackAlignment = options.alignment } if ('colorSpace' in options) { check.parameter(options.colorSpace, colorSpace, 'invalid colorSpace') flags.colorSpace = colorSpace[options.colorSpace] } if ('type' in options) { var type = options.type check(extensions.oes_texture_float || !(type === 'float' || type === 'float32'), 'you must enable the OES_texture_float extension in order to use floating point textures.') check(extensions.oes_texture_half_float || !(type === 'half float' || type === 'float16'), 'you must enable the OES_texture_half_float extension in order to use 16-bit floating point textures.') check(extensions.webgl_depth_texture || !(type === 'uint16' || type === 'uint32' || type === 'depth stencil'), 'you must enable the WEBGL_depth_texture extension in order to use depth/stencil textures.') check.parameter(type, textureTypes, 'invalid texture type') flags.type = textureTypes[type] } var w = flags.width var h = flags.height var c = flags.channels var hasChannels = false if ('shape' in options) { check(Array.isArray(options.shape) && options.shape.length >= 2, 'shape must be an array') w = options.shape[0] h = options.shape[1] if (options.shape.length === 3) { c = options.shape[2] check(c > 0 && c <= 4, 'invalid number of channels') hasChannels = true } check(w >= 0 && w <= limits.maxTextureSize, 'invalid width') check(h >= 0 && h <= limits.maxTextureSize, 'invalid height') } else { if ('radius' in options) { w = h = options.radius check(w >= 0 && w <= limits.maxTextureSize, 'invalid radius') } if ('width' in options) { w = options.width check(w >= 0 && w <= limits.maxTextureSize, 'invalid width') } if ('height' in options) { h = options.height check(h >= 0 && h <= limits.maxTextureSize, 'invalid height') } if ('channels' in options) { c = options.channels check(c > 0 && c <= 4, 'invalid number of channels') hasChannels = true } } flags.width = w | 0 flags.height = h | 0 flags.channels = c | 0 var hasFormat = false if ('format' in options) { var formatStr = options.format check(extensions.webgl_depth_texture || !(formatStr === 'depth' || formatStr === 'depth stencil'), 'you must enable the WEBGL_depth_texture extension in order to use depth/stencil textures.') check.parameter(formatStr, textureFormats, 'invalid texture format') var internalformat = flags.internalformat = textureFormats[formatStr] flags.format = colorFormats[internalformat] if (formatStr in textureTypes) { if (!('type' in options)) { flags.type = textureTypes[formatStr] } } if (formatStr in compressedTextureFormats) { flags.compressed = true } hasFormat = true } // Reconcile channels and format if (!hasChannels && hasFormat) { flags.channels = FORMAT_CHANNELS[flags.format] } else if (hasChannels && !hasFormat) { if (flags.channels !== CHANNELS_FORMAT[flags.format]) { flags.format = flags.internalformat = CHANNELS_FORMAT[flags.channels] } } else if (hasFormat && hasChannels) { check( flags.channels === FORMAT_CHANNELS[flags.format], 'number of channels inconsistent with specified format') } } function setFlags (flags) { gl.pixelStorei(GL_UNPACK_FLIP_Y_WEBGL, flags.flipY) gl.pixelStorei(GL_UNPACK_PREMULTIPLY_ALPHA_WEBGL, flags.premultiplyAlpha) gl.pixelStorei(GL_UNPACK_COLORSPACE_CONVERSION_WEBGL, flags.colorSpace) gl.pixelStorei(GL_UNPACK_ALIGNMENT, flags.unpackAlignment) } // ------------------------------------------------------- // Tex image data // ------------------------------------------------------- function TexImage () { TexFlags.call(this) this.xOffset = 0 this.yOffset = 0 // data this.data = null this.needsFree = false // html element this.element = null // copyTexImage info this.needsCopy = false } function parseImage (image, options) { var data = null if (isPixelData(options)) { data = options } else if (options) { check.type(options, 'object', 'invalid pixel data type') parseFlags(image, options) if ('x' in options) { image.xOffset = options.x | 0 } if ('y' in options) { image.yOffset = options.y | 0 } if (isPixelData(options.data)) { data = options.data } } check( !image.compressed || data instanceof Uint8Array, 'compressed texture data must be stored in a uint8array') if (options.copy) { check(!data, 'can not specify copy and data field for the same texture') var viewW = contextState.viewportWidth var viewH = contextState.viewportHeight image.width = image.width || (viewW - image.xOffset) image.height = image.height || (viewH - image.yOffset) image.needsCopy = true check(image.xOffset >= 0 && image.xOffset < viewW && image.yOffset >= 0 && image.yOffset < viewH && image.width > 0 && image.width <= viewW && image.height > 0 && image.height <= viewH, 'copy texture read out of bounds') } else if (!data) { image.width = image.width || 1 image.height = image.height || 1 image.channels = image.channels || 4 } else if (isTypedArray(data)) { image.channels = image.channels || 4 image.data = data if (!('type' in options) && image.type === GL_UNSIGNED_BYTE) { image.type = typedArrayCode(data) } } else if (isNumericArray(data)) { image.channels = image.channels || 4 convertData(image, data) image.alignment = 1 image.needsFree = true } else if (isNDArrayLike(data)) { var array = data.data if (!Array.isArray(array) && image.type === GL_UNSIGNED_BYTE) { image.type = typedArrayCode(array) } var shape = data.shape var stride = data.stride var shapeX, shapeY, shapeC, strideX, strideY, strideC if (shape.length === 3) { shapeC = shape[2] strideC = stride[2] } else { check(shape.length === 2, 'invalid ndarray pixel data, must be 2 or 3D') shapeC = 1 strideC = 1 } shapeX = shape[0] shapeY = shape[1] strideX = stride[0] strideY = stride[1] image.alignment = 1 image.width = shapeX image.height = shapeY image.channels = shapeC image.format = image.internalformat = CHANNELS_FORMAT[shapeC] image.needsFree = true transposeData(image, array, strideX, strideY, strideC, data.offset) } else if (isCanvasElement(data) || isOffscreenCanvas(data) || isContext2D(data)) { if (isCanvasElement(data) || isOffscreenCanvas(data)) { image.element = data } else { image.element = data.canvas } image.width = image.element.width image.height = image.element.height image.channels = 4 } else if (isBitmap(data)) { image.element = data image.width = data.width image.height = data.height image.channels = 4 } else if (isImageElement(data)) { image.element = data image.width = data.naturalWidth image.height = data.naturalHeight image.channels = 4 } else if (isVideoElement(data)) { image.element = data image.width = data.videoWidth image.height = data.videoHeight image.channels = 4 } else if (isRectArray(data)) { var w = image.width || data[0].length var h = image.height || data.length var c = image.channels if (isArrayLike(data[0][0])) { c = c || data[0][0].length } else { c = c || 1 } var arrayShape = flattenUtils.shape(data) var n = 1 for (var dd = 0; dd < arrayShape.length; ++dd) { n *= arrayShape[dd] } var allocData = preConvert(image, n) flattenUtils.flatten(data, arrayShape, '', allocData) postConvert(image, allocData) image.alignment = 1 image.width = w image.height = h image.channels = c image.format = image.internalformat = CHANNELS_FORMAT[c] image.needsFree = true } if (image.type === GL_FLOAT) { check(limits.extensions.indexOf('oes_texture_float') >= 0, 'oes_texture_float extension not enabled') } else if (image.type === GL_HALF_FLOAT_OES) { check(limits.extensions.indexOf('oes_texture_half_float') >= 0, 'oes_texture_half_float extension not enabled') } // do compressed texture validation here. } function setImage (info, target, miplevel) { var element = info.element var data = info.data var internalformat = info.internalformat var format = info.format var type = info.type var width = info.width var height = info.height setFlags(info) if (element) { gl.texImage2D(target, miplevel, format, format, type, element) } else if (info.compressed) { gl.compressedTexImage2D(target, miplevel, internalformat, width, height, 0, data) } else if (info.needsCopy) { reglPoll() gl.copyTexImage2D( target, miplevel, format, info.xOffset, info.yOffset, width, height, 0) } else { gl.texImage2D(target, miplevel, format, width, height, 0, format, type, data || null) } } function setSubImage (info, target, x, y, miplevel) { var element = info.element var data = info.data var internalformat = info.internalformat var format = info.format var type = info.type var width = info.width var height = info.height setFlags(info) if (element) { gl.texSubImage2D( target, miplevel, x, y, format, type, element) } else if (info.compressed) { gl.compressedTexSubImage2D( target, miplevel, x, y, internalformat, width, height, data) } else if (info.needsCopy) { reglPoll() gl.copyTexSubImage2D( target, miplevel, x, y, info.xOffset, info.yOffset, width, height) } else { gl.texSubImage2D( target, miplevel, x, y, width, height, format, type, data) } } // texImage pool var imagePool = [] function allocImage () { return imagePool.pop() || new TexImage() } function freeImage (image) { if (image.needsFree) { pool.freeType(image.data) } TexImage.call(image) imagePool.push(image) } // ------------------------------------------------------- // Mip map // ------------------------------------------------------- function MipMap () { TexFlags.call(this) this.genMipmaps = false this.mipmapHint = GL_DONT_CARE this.mipmask = 0 this.images = Array(16) } function parseMipMapFromShape (mipmap, width, height) { var img = mipmap.images[0] = allocImage() mipmap.mipmask = 1 img.width = mipmap.width = width img.height = mipmap.height = height img.channels = mipmap.channels = 4 } function parseMipMapFromObject (mipmap, options) { var imgData = null if (isPixelData(options)) { imgData = mipmap.images[0] = allocImage() copyFlags(imgData, mipmap) parseImage(imgData, options) mipmap.mipmask = 1 } else { parseFlags(mipmap, options) if (Array.isArray(options.mipmap)) { var mipData = options.mipmap for (var i = 0; i < mipData.length; ++i) { imgData = mipmap.images[i] = allocImage() copyFlags(imgData, mipmap) imgData.width >>= i imgData.height >>= i parseImage(imgData, mipData[i]) mipmap.mipmask |= (1 << i) } } else { imgData = mipmap.images[0] = allocImage() copyFlags(imgData, mipmap) parseImage(imgData, options) mipmap.mipmask = 1 } } copyFlags(mipmap, mipmap.images[0]) // For textures of the compressed format WEBGL_compressed_texture_s3tc // we must have that // // "When level equals zero width and height must be a multiple of 4. // When level is greater than 0 width and height must be 0, 1, 2 or a multiple of 4. " // // but we do not yet support having multiple mipmap levels for compressed textures, // so we only test for level zero. if ( mipmap.compressed && ( mipmap.internalformat === GL_COMPRESSED_RGB_S3TC_DXT1_EXT || mipmap.internalformat === GL_COMPRESSED_RGBA_S3TC_DXT1_EXT || mipmap.internalformat === GL_COMPRESSED_RGBA_S3TC_DXT3_EXT || mipmap.internalformat === GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ) ) { check(mipmap.width % 4 === 0 && mipmap.height % 4 === 0, 'for compressed texture formats, mipmap level 0 must have width and height that are a multiple of 4') } } function setMipMap (mipmap, target) { var images = mipmap.images for (var i = 0; i < images.length; ++i) { if (!images[i]) { return } setImage(images[i], target, i) } } var mipPool = [] function allocMipMap () { var result = mipPool.pop() || new MipMap() TexFlags.call(result) result.mipmask = 0 for (var i = 0; i < 16; ++i) { result.images[i] = null } return result } function freeMipMap (mipmap) { var images = mipmap.images for (var i = 0; i < images.length; ++i) { if (images[i]) { freeImage(images[i]) } images[i] = null } mipPool.push(mipmap) } // ------------------------------------------------------- // Tex info // ------------------------------------------------------- function TexInfo () { this.minFilter = GL_NEAREST this.magFilter = GL_NEAREST this.wrapS = GL_CLAMP_TO_EDGE this.wrapT = GL_CLAMP_TO_EDGE this.anisotropic = 1 this.genMipmaps = false this.mipmapHint = GL_DONT_CARE } function parseTexInfo (info, options) { if ('min' in options) { var minFilter = options.min check.parameter(minFilter, minFilters) info.minFilter = minFilters[minFilter] if (MIPMAP_FILTERS.indexOf(info.minFilter) >= 0 && !('faces' in options)) { info.genMipmaps = true } } if ('mag' in options) { var magFilter = options.mag check.parameter(magFilter, magFilters) info.magFilter = magFilters[magFilter] } var wrapS = info.wrapS var wrapT = info.wrapT if ('wrap' in options) { var wrap = options.wrap if (typeof wrap === 'string') { check.parameter(wrap, wrapModes) wrapS = wrapT = wrapModes[wrap] } else if (Array.isArray(wrap)) { check.parameter(wrap[0], wrapModes) check.parameter(wrap[1], wrapModes) wrapS = wrapModes[wrap[0]] wrapT = wrapModes[wrap[1]] } } else { if ('wrapS' in options) { var optWrapS = options.wrapS check.parameter(optWrapS, wrapModes) wrapS = wrapModes[optWrapS] } if ('wrapT' in options) { var optWrapT = options.wrapT check.parameter(optWrapT, wrapModes) wrapT = wrapModes[optWrapT] } } info.wrapS = wrapS info.wrapT = wrapT if ('anisotropic' in options) { var anisotropic = options.anisotropic check(typeof anisotropic === 'number' && anisotropic >= 1 && anisotropic <= limits.maxAnisotropic, 'aniso samples must be between 1 and ') info.anisotropic = options.anisotropic } if ('mipmap' in options) { var hasMipMap = false switch (typeof options.mipmap) { case 'string': check.parameter(options.mipmap, mipmapHint, 'invalid mipmap hint') info.mipmapHint = mipmapHint[options.mipmap] info.genMipmaps = true hasMipMap = true break case 'boolean': hasMipMap = info.genMipmaps = options.mipmap break case 'object': check(Array.isArray(options.mipmap), 'invalid mipmap type') info.genMipmaps = false hasMipMap = true break default: check.raise('invalid mipmap type') } if (hasMipMap && !('min' in options)) { info.minFilter = GL_NEAREST_MIPMAP_NEAREST } } } function setTexInfo (info, target) { gl.texParameteri(target, GL_TEXTURE_MIN_FILTER, info.minFilter) gl.texParameteri(target, GL_TEXTURE_MAG_FILTER, info.magFilter) gl.texParameteri(target, GL_TEXTURE_WRAP_S, info.wrapS) gl.texParameteri(target, GL_TEXTURE_WRAP_T, info.wrapT) if (extensions.ext_texture_filter_anisotropic) { gl.texParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, info.anisotropic) } if (info.genMipmaps) { gl.hint(GL_GENERATE_MIPMAP_HINT, info.mipmapHint) gl.generateMipmap(target) } } // ------------------------------------------------------- // Full texture object // ------------------------------------------------------- var textureCount = 0 var textureSet = {} var numTexUnits = limits.maxTextureUnits var textureUnits = Array(numTexUnits).map(function () { return null }) function REGLTexture (target) { TexFlags.call(this) this.mipmask = 0 this.internalformat = GL_RGBA this.id = textureCount++ this.refCount = 1 this.target = target this.texture = gl.createTexture() this.unit = -1 this.bindCount = 0 this.texInfo = new TexInfo() if (config.profile) { this.stats = { size: 0 } } } function tempBind (texture) { gl.activeTexture(GL_TEXTURE0) gl.bindTexture(texture.target, texture.texture) } function tempRestore () { var prev = textureUnits[0] if (prev) { gl.bindTexture(prev.target, prev.texture) } else { gl.bindTexture(GL_TEXTURE_2D, null) } } function destroy (texture) { var handle = texture.texture check(handle, 'must not double destroy texture') var unit = texture.unit var target = texture.target if (unit >= 0) { gl.activeTexture(GL_TEXTURE0 + unit) gl.bindTexture(target, null) textureUnits[unit] = null } gl.deleteTexture(handle) texture.texture = null texture.params = null texture.pixels = null texture.refCount = 0 delete textureSet[texture.id] stats.textureCount-- } extend(REGLTexture.prototype, { bind: function () { var texture = this texture.bindCount += 1 var unit = texture.unit if (unit < 0) { for (var i = 0; i < numTexUnits; ++i) { var other = textureUnits[i] if (other) { if (other.bindCount > 0) { continue } other.unit = -1 } textureUnits[i] = texture unit = i break } if (unit >= numTexUnits) { check.raise('insufficient number of texture units') } if (config.profile && stats.maxTextureUnits < (unit + 1)) { stats.maxTextureUnits = unit + 1 // +1, since the units are zero-based } texture.unit = unit gl.activeTexture(GL_TEXTURE0 + unit) gl.bindTexture(texture.target, texture.texture) } return unit }, unbind: function () { this.bindCount -= 1 }, decRef: function () { if (--this.refCount <= 0) { destroy(this) } } }) function createTexture2D (a, b) { var texture = new REGLTexture(GL_TEXTURE_2D) textureSet[texture.id] = texture stats.textureCount++ function reglTexture2D (a, b) { var texInfo = texture.texInfo TexInfo.call(texInfo) var mipData = allocMipMap() if (typeof a === 'number') { if (typeof b === 'number') { parseMipMapFromShape(mipData, a | 0, b | 0) } else { parseMipMapFromShape(mipData, a | 0, a | 0) } } else if (a) { check.type(a, 'object', 'invalid arguments to regl.texture') parseTexInfo(texInfo, a) parseMipMapFromObject(mipData, a) } else { // empty textures get assigned a default shape of 1x1 parseMipMapFromShape(mipData, 1, 1) } if (texInfo.genMipmaps) { mipData.mipmask = (mipData.width << 1) - 1 } texture.mipmask = mipData.mipmask copyFlags(texture, mipData) check.texture2D(texInfo, mipData, limits) texture.internalformat = mipData.internalformat reglTexture2D.width = mipData.width reglTexture2D.height = mipData.height tempBind(texture) setMipMap(mipData, GL_TEXTURE_2D) setTexInfo(texInfo, GL_TEXTURE_2D) tempRestore() freeMipMap(mipData) if (config.profile) { texture.stats.size = getTextureSize( texture.internalformat, texture.type, mipData.width, mipData.height, texInfo.genMipmaps, false) } reglTexture2D.format = textureFormatsInvert[texture.internalformat] reglTexture2D.type = textureTypesInvert[texture.type] reglTexture2D.mag = magFiltersInvert[texInfo.magFilter] reglTexture2D.min = minFiltersInvert[texInfo.minFilter] reglTexture2D.wrapS = wrapModesInvert[texInfo.wrapS] reglTexture2D.wrapT = wrapModesInvert[texInfo.wrapT] return reglTexture2D } function subimage (image, x_, y_, level_) { check(!!image, 'must specify image data') var x = x_ | 0 var y = y_ | 0 var level = level_ | 0 var imageData = allocImage() copyFlags(imageData, texture) imageData.width = 0 imageData.height = 0 parseImage(imageData, image) imageData.width = imageData.width || ((texture.width >> level) - x) imageData.height = imageData.height || ((texture.height >> level) - y) check( texture.type === imageData.type && texture.format === imageData.format && texture.internalformat === imageData.internalformat, 'incompatible format for texture.subimage') check( x >= 0 && y >= 0 && x + imageData.width <= texture.width && y + imageData.height <= texture.height, 'texture.subimage write out of bounds') check( texture.mipmask & (1 << level), 'missing mipmap data') check( imageData.data || imageData.element || imageData.needsCopy, 'missing image data') tempBind(texture) setSubImage(imageData, GL_TEXTURE_2D, x, y, level) tempRestore() freeImage(imageData) return reglTexture2D } function resize (w_, h_) { var w = w_ | 0 var h = (h_ | 0) || w if (w === texture.width && h === texture.height) { return reglTexture2D } reglTexture2D.width = texture.width = w reglTexture2D.height = texture.height = h tempBind(texture) for (var i = 0; texture.mipmask >> i; ++i) { var _w = w >> i var _h = h >> i if (!_w || !_h) break gl.texImage2D( GL_TEXTURE_2D, i, texture.format, _w, _h, 0, texture.format, texture.type, null) } tempRestore() // also, recompute the texture size. if (config.profile) { texture.stats.size = getTextureSize( texture.internalformat, texture.type, w, h, false, false) } return reglTexture2D } reglTexture2D(a, b) reglTexture2D.subimage = subimage reglTexture2D.resize = resize reglTexture2D._reglType = 'texture2d' reglTexture2D._texture = texture if (config.profile) { reglTexture2D.stats = texture.stats } reglTexture2D.destroy = function () { texture.decRef() } return reglTexture2D } function createTextureCube (a0, a1, a2, a3, a4, a5) { var texture = new REGLTexture(GL_TEXTURE_CUBE_MAP) textureSet[texture.id] = texture stats.cubeCount++ var faces = new Array(6) function reglTextureCube (a0, a1, a2, a3, a4, a5) { var i var texInfo = texture.texInfo TexInfo.call(texInfo) for (i = 0; i < 6; ++i) { faces[i] = allocMipMap() } if (typeof a0 === 'number' || !a0) { var s = (a0 | 0) || 1 for (i = 0; i < 6; ++i) { parseMipMapFromShape(faces[i], s, s) } } else if (typeof a0 === 'object') { if (a1) { parseMipMapFromObject(faces[0], a0) parseMipMapFromObject(faces[1], a1) parseMipMapFromObject(faces[2], a2) parseMipMapFromObject(faces[3], a3) parseMipMapFromObject(faces[4], a4) parseMipMapFromObject(faces[5], a5) } else { parseTexInfo(texInfo, a0) parseFlags(texture, a0) if ('faces' in a0) { var faceInput = a0.faces check(Array.isArray(faceInput) && faceInput.length === 6, 'cube faces must be a length 6 array') for (i = 0; i < 6; ++i) { check(typeof faceInput[i] === 'object' && !!faceInput[i], 'invalid input for cube map face') copyFlags(faces[i], texture) parseMipMapFromObject(faces[i], faceInput[i]) } } else { for (i = 0; i < 6; ++i) { parseMipMapFromObject(faces[i], a0) } } } } else { check.raise('invalid arguments to cube map') } copyFlags(texture, faces[0]) check.optional(function () { if (!limits.npotTextureCube) { check(isPow2(texture.width) && isPow2(texture.height), 'your browser does not support non power or two texture dimensions') } }) if (texInfo.genMipmaps) { texture.mipmask = (faces[0].width << 1) - 1 } else { texture.mipmask = faces[0].mipmask } check.textureCube(texture, texInfo, faces, limits) texture.internalformat = faces[0].internalformat reglTextureCube.width = faces[0].width reglTextureCube.height = faces[0].height tempBind(texture) for (i = 0; i < 6; ++i) { setMipMap(faces[i], GL_TEXTURE_CUBE_MAP_POSITIVE_X + i) } setTexInfo(texInfo, GL_TEXTURE_CUBE_MAP) tempRestore() if (config.profile) { texture.stats.size = getTextureSize( texture.internalformat, texture.type, reglTextureCube.width, reglTextureCube.height, texInfo.genMipmaps, true) } reglTextureCube.format = textureFormatsInvert[texture.internalformat] reglTextureCube.type = textureTypesInvert[texture.type] reglTextureCube.mag = magFiltersInvert[texInfo.magFilter] reglTextureCube.min = minFiltersInvert[texInfo.minFilter] reglTextureCube.wrapS = wrapModesInvert[texInfo.wrapS] reglTextureCube.wrapT = wrapModesInvert[texInfo.wrapT] for (i = 0; i < 6; ++i) { freeMipMap(faces[i]) } return reglTextureCube } function subimage (face, image, x_, y_, level_) { check(!!image, 'must specify image data') check(typeof face === 'number' && face === (face | 0) && face >= 0 && face < 6, 'invalid face') var x = x_ | 0 var y = y_ | 0 var level = level_ | 0 var imageData = allocImage() copyFlags(imageData, texture) imageData.width = 0 imageData.height = 0 parseImage(imageData, image) imageData.width = imageData.width || ((texture.width >> level) - x) imageData.height = imageData.height || ((texture.height >> level) - y) check( texture.type === imageData.type && texture.format === imageData.format && texture.internalformat === imageData.internalformat, 'incompatible format for texture.subimage') check( x >= 0 && y >= 0 && x + imageData.width <= texture.width && y + imageData.height <= texture.height, 'texture.subimage write out of bounds') check( texture.mipmask & (1 << level), 'missing mipmap data') check( imageData.data || imageData.element || imageData.needsCopy, 'missing image data') tempBind(texture) setSubImage(imageData, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, x, y, level) tempRestore() freeImage(imageData) return reglTextureCube } function resize (radius_) { var radius = radius_ | 0 if (radius === texture.width) { return } reglTextureCube.width = texture.width = radius reglTextureCube.height = texture.height = radius tempBind(texture) for (var i = 0; i < 6; ++i) { for (var j = 0; texture.mipmask >> j; ++j) { gl.texImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, j, texture.format, radius >> j, radius >> j, 0, texture.format, texture.type, null) } } tempRestore() if (config.profile) { texture.stats.size = getTextureSize( texture.internalformat, texture.type, reglTextureCube.width, reglTextureCube.height, false, true) } return reglTextureCube } reglTextureCube(a0, a1, a2, a3, a4, a5) reglTextureCube.subimage = subimage reglTextureCube.resize = resize reglTextureCube._reglType = 'textureCube' reglTextureCube._texture = texture if (config.profile) { reglTextureCube.stats = texture.stats } reglTextureCube.destroy = function () { texture.decRef() } return reglTextureCube } // Called when regl is destroyed function destroyTextures () { for (var i = 0; i < numTexUnits; ++i) { gl.activeTexture(GL_TEXTURE0 + i) gl.bindTexture(GL_TEXTURE_2D, null) textureUnits[i] = null } values(textureSet).forEach(destroy) stats.cubeCount = 0 stats.textureCount = 0 } if (config.profile) { stats.getTotalTextureSize = function () { var total = 0 Object.keys(textureSet).forEach(function (key) { total += textureSet[key].stats.size }) return total } } function restoreTextures () { for (var i = 0; i < numTexUnits; ++i) { var tex = textureUnits[i] if (tex) { tex.bindCount = 0 tex.unit = -1 textureUnits[i] = null } } values(textureSet).forEach(function (texture) { texture.texture = gl.createTexture() gl.bindTexture(texture.target, texture.texture) for (var i = 0; i < 32; ++i) { if ((texture.mipmask & (1 << i)) === 0) { continue } if (texture.target === GL_TEXTURE_2D) { gl.texImage2D(GL_TEXTURE_2D, i, texture.internalformat, texture.width >> i, texture.height >> i, 0, texture.internalformat, texture.type, null) } else { for (var j = 0; j < 6; ++j) { gl.texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + j, i, texture.internalformat, texture.width >> i, texture.height >> i, 0, texture.internalformat, texture.type, null) } } } setTexInfo(texture.texInfo, texture.target) }) } function refreshTextures () { for (var i = 0; i < numTexUnits; ++i) { var tex = textureUnits[i] if (tex) { tex.bindCount = 0 tex.unit = -1 textureUnits[i] = null } gl.activeTexture(GL_TEXTURE0 + i) gl.bindTexture(GL_TEXTURE_2D, null) gl.bindTexture(GL_TEXTURE_CUBE_MAP, null) } } return { create2D: createTexture2D, createCube: createTextureCube, clear: destroyTextures, getTexture: function (wrapper) { return null }, restore: restoreTextures, refresh: refreshTextures } }