diff --git a/docs/api-reference/loaders/glb-loader.md b/docs/api-reference/loaders/glb-loader.md deleted file mode 100644 index 26a845eb6..000000000 --- a/docs/api-reference/loaders/glb-loader.md +++ /dev/null @@ -1,35 +0,0 @@ -# GLBLoader - -Provides functions for parsing or generating the binary GLB containers used by glTF (and certain other formats). - -Takes a JavaScript data structure and encodes it as a JSON blob with binary data (e.g. typed arrays) extracted into a binary chunk. - - -## Usage - -``` -import {GLBLoader, loadFile} from 'loaders.gl'; - -loadFile(url, GLBLoader).then(data => { - // Application code here - ... -}); -``` - - -## `GLBParser` class - -The `GLBLoader` module exposes the `GLBParser` class with the following methods - -### constructor - -Creates a new `GLBParser` instance. - - -### parse(arrayBuffer : ArrayBuffer) : Object - -Parses an in-memory, GLB formatted `ArrayBuffer` into: - -* `arrayBuffer` - just returns the input array buffer -* `binaryByteOffset` - offset to the first byte in the binary chunk -* `json` - a JavaScript "JSON" data structure with inlined binary data fields. diff --git a/docs/api-reference/loaders/glb-writer.md b/docs/api-reference/loaders/glb-writer.md deleted file mode 100644 index 3f25a7080..000000000 --- a/docs/api-reference/loaders/glb-writer.md +++ /dev/null @@ -1,39 +0,0 @@ -# GLBWriter - -The `GLBWriter` supports encoding of GLB files. - - -## `GLBBuilder` class - -The `GLBWriter` module exposes the `GLBBuilder` class that allows applications to dynamically build up a hybrid JSON/binary GLB file. It exposes the following methods: - - -### constructor - -Creates a new `GLBBuilder` instance. - - -### addBuffer(typedArray : TypedArray, accessor : Object) : Number - -Adds one binary array intended to be loaded back as a WebGL buffer. - -* `typedArray` - -* `accessor` - {size, type, ...}. - -Type is autodeduced from the type of the typed array. - -The binary data will be added to the GLB BIN chunk, and glTF `bufferView` and `accessor` fields will be populated. - - -### addImage(typedArray: TypedArray) : Number - -Adds an image - -The binary image data will be added to the GLB BIN chunk, and glTF `bufferView` and `image` fields will be populated. - - -### encode(json: Object | Array, options : Object) : ArrayBuffer - -Writes JavaScript JSON data structure into an arrayBuffer that can be written atomically to file, extracting binary fields from the data and placing these in a compact binary chunk following the "stripped" JSON chunk. - -Note: Once all binary buffers have been added `encode()` can be called.. diff --git a/docs/api-reference/loaders/kml-loader.md b/docs/api-reference/loaders/kml-loader.md deleted file mode 100644 index 59fd65706..000000000 --- a/docs/api-reference/loaders/kml-loader.md +++ /dev/null @@ -1,45 +0,0 @@ -# KMLLoader - -KML (Keyhole Markup Language) is an XML-based file format used to display geographic data in an Earth browser such as Google Earth (originally named "Keyhole Earth Viewer"). It can be used with any 2D or 3D maps. - -References: - -* [Keyhole Markup Language - Wikipedia](https://en.wikipedia.org/wiki/Keyhole_Markup_Language) -* [KML Tutorial - Google](https://developers.google.com/kml/documentation/kml_tut) - - -## Structure of Data - -The parser will return a JavaScript object with a number of top-level array-valued fields: - -| Field | Description | -| --- | --- | -| `documents` | | -| `folders` | | -| `links` | | -| `points` | Points | -| `lines` | Lines | -| `polygons` | Polygons | -| `imageoverlays` | Urls and bounds of bitmap overlays | - - -## Parser Options - -> Work in progress - -| Option | Default | Description | -| --- | --- | --- | -| `useLngLatFormat` | `true` | KML longitudes and latitudes are specified as `[lat, lng]`. This option "normalizes" them to `[lng, lat]`. | -| `useColorArrays` | `true` | Convert color strings to arrays | - - -## Limitations - -* Currently XML parsing is only implemented in browsers, not in Node.js. Check `KMLLoader.supported` to check at run-time. - - -## License/Credits/Attributions - -License: MIT - -`XMLLoader` is an adaptation of Nick Blackwell's [`js-simplekml`](https://github.com/nickolanack/js-simplekml) module. diff --git a/docs/api-reference/loaders/las-loader.md b/docs/api-reference/loaders/las-loader.md deleted file mode 100644 index cda10e719..000000000 --- a/docs/api-reference/loaders/las-loader.md +++ /dev/null @@ -1,25 +0,0 @@ -# LASLoader - -The LASER (LAS) file format is a public format for the interchange of 3-dimensional point cloud data data, developed for LIDAR mapping purposes. - -* [LASER FILE FORMAT](https://www.asprs.org/divisions-committees/lidar-division/laser-las-file-format-exchange-activities) - -## Usage - -``` -import {OBJLoader, loadFile} from 'loaders.gl'; - -loadFile(url, OBJLoader, options).then(data => { - // Application code here - ... -}); -``` - -## Options - -TBA - - -## Data Loaded - -TBA diff --git a/docs/api-reference/loaders/obj-loader.md b/docs/api-reference/loaders/obj-loader.md deleted file mode 100644 index 2ef288666..000000000 --- a/docs/api-reference/loaders/obj-loader.md +++ /dev/null @@ -1,32 +0,0 @@ -# OBJLoader - -This loader handles the OBJ half of the classic Wavefront OBJ/MTL format. The OBJ format is a simple ASCII format that lists vertices, normals and faces on successive lines. - -References - -* [Wavefront OBJ file (Wikipedia)](https://en.wikipedia.org/wiki/Wavefront_.obj_file) - - -## Usage - -``` -import {OBJLoader, loadFile} from 'loaders.gl'; - -loadFile(url, OBJLoader).then(data => { - // Application code here - ... -}); -``` - - -## Loader Options - -N/A - - -## Data Loaded - -* `positions` - -* `normals` - -* `faces` - - diff --git a/docs/api-reference/loaders/pcd-loader.md b/docs/api-reference/loaders/pcd-loader.md deleted file mode 100644 index 0c51b97f6..000000000 --- a/docs/api-reference/loaders/pcd-loader.md +++ /dev/null @@ -1,33 +0,0 @@ -# PCDLoader - -A point cloud format defined by the Point Cloud Library - -Currently only `ascii` and `binary` subformats are supported. Compressed binary files are currently not supported. - -References - -* [Point Cloud Library](https://en.wikipedia.org/wiki/Point_Cloud_Library) -* [PointClouds.org](http://pointclouds.org/documentation/tutorials/pcd_file_format.php) - - -## Usage - -``` -import {PCFLoader, loadFile} from 'loaders.gl'; - -loadFile(url, PCFLoader) -.then(({header, attributes}) => { - // Application code here, e.g: - // return new Geometry(attributes) -}); -``` - -Loads `position`, `normal`, `color` attributes. - - -## Attribution/Credits - -This loader is a light adaption of the PCDLoader example in the THREE.js code base. The THREE.js source files contain the following attributions: - -* @author Filipe Caixeta / http://filipecaixeta.com.br -* @author Mugen87 / https://github.com/Mugen87 diff --git a/docs/api-reference/loaders/ply-loader.md b/docs/api-reference/loaders/ply-loader.md deleted file mode 100644 index da6a8d041..000000000 --- a/docs/api-reference/loaders/ply-loader.md +++ /dev/null @@ -1,22 +0,0 @@ -# PLYLoader - -PLY is a computer file format known as the Polygon File Format or the Stanford Triangle Format. It was principally designed to store three-dimensional data from 3D scanners. - - -References - -* [PLY format (Wikipedia)](https://en.wikipedia.org/wiki/PLY_(file_format)) - - -## Usage - -``` -import {PLYLoader, loadFile} from 'loaders.gl'; - -loadFile(url, PLYLoader).then(data => { - // Application code here - ... -}); -``` - - diff --git a/docs/api-reference/webgl/texture-2d.md b/docs/api-reference/webgl/texture-2d.md index 1d7b37937..45e074412 100644 --- a/docs/api-reference/webgl/texture-2d.md +++ b/docs/api-reference/webgl/texture-2d.md @@ -68,20 +68,32 @@ console.log( ## Methods -### Texture2D constructor +### constructor(gl : WebGLRenderingContext, props : Object | data : any) ``` -new Texture2D(gl, { - data=, - width=, - height=, - mipmaps=, - format=, - type=, - dataFormat=, - parameters=, - pixelStore= -}) +import {Texture2D} from '@luma.gl/core' +const texture1 = new Texture2D(gl, { + data: ..., + width: ..., + height: ..., + mipmaps: ..., + format: ..., + type: ..., + dataFormat: ..., + parameters: ... +}); +``` + +There is also a short form where the image data (or a promise resolving to the image data) can be the second argument of the constructor: + +``` +import {Texture2D} from '@luma.gl/core'; +import {loadImage} from '@loaders.gl/core'; + +const texture1 = new Texture2D(gl, loadImage(url)); +// equivalent to +const texture1 = new Texture2D(gl, {data: loadImage(url)}); + ``` * `gl` (WebGLRenderingContext) - gl context diff --git a/docs/whats-new.md b/docs/whats-new.md index af0f6cc07..63bbd49f4 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,21 @@ ## Version 7.0 +### "Asynchronous" Textures + +The `Texture` class now supports image data being initialized with a URL `string` or a `Promise`that resolves to any of the previously valid data types, e.g. an `Image` instance. This avoids the need to clutter your code with `promise.then()` and/or callback functions just to load textures. + +``` +new Texture2D(gl, 'path/to/my/image.png'); +// or +new Texture2D(gl, loadImage('path/to/my/image.png')); // loadImage returns a Promise +``` + +### loaders.gl - New Companion Framework for 3D Asset Loading + +[loaders.gl]() provides a rich suite of 3D file format loaders (including loaders for various popular `Mesh` and `PointCloud` formats) that parse files into objects that can be directly passed to luma.gl `Model` class. + + ### New Submodule with GPGPU Utilities * `@luma.gl/gpgpu` - an experimental module with a collection of GPU accelerated utility methods. @@ -23,6 +38,13 @@ model.setAttributes({ } ``` + + +## Version 6.4 + +Date: January 29, 2018 + + ## Version 6.3 Date: November 16, 2018 diff --git a/modules/core/src/io/README.md b/modules/core/src/io/README.md deleted file mode 100644 index 2ca88278b..000000000 --- a/modules/core/src/io/README.md +++ /dev/null @@ -1,11 +0,0 @@ -The IO submodule contains basic IO functions that can be implemented without -dependencies. - -More advanced IO support that handles e.g. - -* multi-format image loading under Node.js -* stream support in browsers - -is being developed as a separate module. - -Platform switching is handled by platform.js \ No newline at end of file diff --git a/modules/core/src/io/browser-request-file.js b/modules/core/src/io/browser-request-file.js deleted file mode 100644 index fe11dae84..000000000 --- a/modules/core/src/io/browser-request-file.js +++ /dev/null @@ -1,108 +0,0 @@ -// Supports loading (requesting) assets with XHR (XmlHttpRequest) -/* eslint-disable guard-for-in, complexity, no-try-catch */ - -/* global XMLHttpRequest */ -function noop() {} - -const XHR_STATES = { - UNINITIALIZED: 0, - LOADING: 1, - LOADED: 2, - INTERACTIVE: 3, - COMPLETED: 4 -}; - -class XHR { - constructor({ - url, - path = null, - method = 'GET', - asynchronous = true, - noCache = false, - // body = null, - sendAsBinary = false, - responseType = false, - onProgress = noop, - onError = noop, - onAbort = noop, - onComplete = noop - }) { - this.url = path ? path.join(path, url) : url; - this.method = method; - this.async = asynchronous; - this.noCache = noCache; - this.sendAsBinary = sendAsBinary; - this.responseType = responseType; - - this.req = new XMLHttpRequest(); - - this.req.onload = e => onComplete(e); - this.req.onerror = e => onError(e); - this.req.onabort = e => onAbort(e); - this.req.onprogress = e => { - if (e.lengthComputable) { - onProgress(e, Math.round((e.loaded / e.total) * 100)); - } else { - onProgress(e, -1); - } - }; - } - - setRequestHeader(header, value) { - this.req.setRequestHeader(header, value); - return this; - } - - // /* eslint-disable max-statements */ - sendAsync(body = this.body || null) { - return new Promise((resolve, reject) => { - try { - const {req, method, noCache, sendAsBinary, responseType} = this; - - const url = noCache - ? this.url + (this.url.indexOf('?') >= 0 ? '&' : '?') + Date.now() - : this.url; - - req.open(method, url, this.async); - - if (responseType) { - req.responseType = responseType; - } - - if (this.async) { - req.onreadystatechange = e => { - if (req.readyState === XHR_STATES.COMPLETED) { - if (req.status === 200) { - resolve(req.responseType ? req.response : req.responseText); - } else { - reject(new Error(`${req.status}: ${url}`)); - } - } - }; - } - - if (sendAsBinary) { - req.sendAsBinary(body); - } else { - req.send(body); - } - - if (!this.async) { - if (req.status === 200) { - resolve(req.responseType ? req.response : req.responseText); - } else { - reject(new Error(`${req.status}: ${url}`)); - } - } - } catch (error) { - reject(error); - } - }); - } - /* eslint-enable max-statements */ -} - -export function requestFile(opts) { - const xhr = new XHR(opts); - return xhr.sendAsync(); -} diff --git a/modules/core/src/io/load-files.js b/modules/core/src/io/load-files.js deleted file mode 100644 index 7bfca7788..000000000 --- a/modules/core/src/io/load-files.js +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable guard-for-in, complexity, no-try-catch */ -import assert from '../utils/assert'; -import {loadFile, loadImage} from './browser-load'; -import {Program, Texture2D} from '../webgl'; -import {Model} from '../core'; -import {Geometry} from '../geometry'; - -function noop() {} - -export function loadTexture(gl, url, opts = {}) { - assert(typeof url === 'string', 'loadTexture: url must be string'); - - return loadImage(url, opts).then(image => { - return new Texture2D(gl, Object.assign({id: url}, opts, {data: image})); - }); -} - -/* - * Loads (Requests) multiple files asynchronously - */ -export function loadFiles(opts = {}) { - const {urls, onProgress = noop} = opts; - assert(urls.every(url => typeof url === 'string'), 'loadImages: {urls} must be array of strings'); - let count = 0; - return Promise.all( - urls.map(url => { - const promise = loadFile(Object.assign({url}, opts)); - promise.then(file => - onProgress({ - progress: ++count / urls.length, - count, - total: urls.length, - url - }) - ); - return promise; - }) - ); -} - -/* - * Loads (requests) multiple images asynchronously - */ -export function loadImages(opts = {}) { - const {urls, onProgress = noop} = opts; - assert(urls.every(url => typeof url === 'string'), 'loadImages: {urls} must be array of strings'); - let count = 0; - return Promise.all( - urls.map(url => { - const promise = loadImage(url, opts); - promise.then(file => - onProgress({ - progress: ++count / urls.length, - count, - total: urls.length, - url - }) - ); - return promise; - }) - ); -} - -export function loadTextures(gl, opts = {}) { - const {urls, onProgress = noop} = opts; - assert( - urls.every(url => typeof url === 'string'), - 'loadTextures: {urls} must be array of strings' - ); - - return loadImages(Object.assign({urls, onProgress}, opts)).then(images => - images.map((img, i) => { - return new Texture2D(gl, Object.assign({id: urls[i]}, opts, {data: img})); - }) - ); -} - -export function loadProgram(gl, opts = {}) { - const {vs, fs, onProgress = noop} = opts; - return loadFiles(Object.assign({urls: [vs, fs], onProgress}, opts)).then( - ([vsText, fsText]) => new Program(gl, Object.assign({vs: vsText, fs: fsText}, opts)) - ); -} - -// Loads a simple JSON format -export function loadModel(gl, opts = {}) { - const {url, onProgress = noop} = opts; - return loadFiles(Object.assign({urls: [url], onProgress}, opts)).then(([file]) => - parseModel(gl, Object.assign({file}, opts)) - ); -} - -export function parseModel(gl, opts = {}) { - const {file, program = new Program(gl)} = opts; - const json = typeof file === 'string' ? parseJSON(file) : file; - // Remove any attributes so that we can create a geometry - // TODO - change format to put these in geometry sub object? - const attributes = {}; - const modelOptions = {}; - for (const key in json) { - const value = json[key]; - if (Array.isArray(value)) { - attributes[key] = key === 'indices' ? new Uint16Array(value) : new Float32Array(value); - } else { - modelOptions[key] = value; - } - } - - return new Model( - gl, - Object.assign({program, geometry: new Geometry({attributes})}, modelOptions, opts) - ); -} - -function parseJSON(file) { - try { - return JSON.parse(file); - } catch (error) { - throw new Error(`Failed to parse JSON: ${error}`); - } -} diff --git a/modules/core/src/io/node.js b/modules/core/src/io/node.js deleted file mode 100644 index 4a777361a..000000000 --- a/modules/core/src/io/node.js +++ /dev/null @@ -1,11 +0,0 @@ -export function loadFile(opts) { - throw new Error('loadFile not implemented under Node'); -} - -/* - * Loads images asynchronously - * returns a promise tracking the load - */ -export function loadImage(url) { - throw new Error('loadImage not implemented under Node'); -} diff --git a/modules/core/src/webgl/program.js b/modules/core/src/webgl/program.js index cbb6254b9..31d5854d5 100644 --- a/modules/core/src/webgl/program.js +++ b/modules/core/src/webgl/program.js @@ -16,32 +16,33 @@ import assert from '../utils/assert'; const LOG_PROGRAM_PERF_PRIORITY = 4; -// const GL_INTERLEAVED_ATTRIBS = 0x8C8C; const GL_SEPARATE_ATTRIBS = 0x8c8d; +const V6_DEPRECATED_METHODS = [ + 'setVertexArray', + 'setAttributes', + 'setBuffers', + 'unsetBuffers', + + 'use', + 'getUniformCount', + 'getUniformInfo', + 'getUniformLocation', + 'getUniformValue', + + 'getVarying', + 'getFragDataLocation', + 'getAttachedShaders', + 'getAttributeCount', + 'getAttributeLocation', + 'getAttributeInfo' +]; + export default class Program extends Resource { constructor(gl, opts = {}) { super(gl, opts); - this.stubRemovedMethods('Program', 'v6.0', [ - 'setVertexArray', - 'setAttributes', - 'setBuffers', - 'unsetBuffers', - - 'use', - 'getUniformCount', - 'getUniformInfo', - 'getUniformLocation', - 'getUniformValue', - - 'getVarying', - 'getFragDataLocation', - 'getAttachedShaders', - 'getAttributeCount', - 'getAttributeLocation', - 'getAttributeInfo' - ]); + this.stubRemovedMethods('Program', 'v6.0', V6_DEPRECATED_METHODS); // Experimental flag to avoid deleting Program object while it is cached this._isCached = false; @@ -135,15 +136,20 @@ export default class Program extends Resource { // TODO - move vertex array binding and transform feedback binding to withParameters? assert(vertexArray); + if (uniforms) { + // DEPRECATED: v7.0 (deprecated earlier but warning not properly implemented) + log.deprecated('Program.draw({uniforms})', 'Program.setUniforms(uniforms)')(); + this.setUniforms(uniforms, samplers); + } + + // Note: async textures set as uniforms might still be loading. + // Now that all uniforms have been updated, check if any texture + // in the uniforms is not yet initialized, then we don't draw + if (this._areTexturesLoading()) { + return this; + } + vertexArray.bindForDraw(vertexCount, instanceCount, () => { - if (uniforms) { - // DEPRECATED: v7.0 (deprecated earlier but warning not properly implemented) - log.deprecated('Program.draw({uniforms})', 'Program.setUniforms(uniforms)')(); - this.setUniforms(uniforms, samplers); - } - - this._bindTextures(); - if (framebuffer !== undefined) { parameters = Object.assign({}, parameters, {framebuffer}); } @@ -153,6 +159,8 @@ export default class Program extends Resource { transformFeedback.begin(primitiveMode); } + this._bindTextures(); + withParameters(this.gl, parameters, () => { // TODO - Use polyfilled WebGL2RenderingContext instead of ANGLE extension if (isIndexed && isInstanced) { @@ -207,6 +215,32 @@ export default class Program extends Resource { // PRIVATE METHODS + _areTexturesLoading() { + let texturesLoaded = true; + + for (const uniformName in this.uniforms) { + const uniformSetter = this._uniformSetters[uniformName]; + + if (uniformSetter && uniformSetter.textureIndex !== undefined) { + let uniform = this.uniforms[uniformName]; + + if (uniform instanceof Framebuffer) { + const framebuffer = uniform; + uniform = framebuffer.texture; + } + + if (uniform instanceof Texture) { + const texture = uniform; + // Check that texture is loaded + texturesLoaded = texturesLoaded && texture.loaded; + } + } + } + + return !texturesLoaded; + } + + // Binds textures (and checks that async textures have loaded) // This needs to be done before every draw call _bindTextures() { for (const uniformName in this.uniforms) { @@ -220,8 +254,9 @@ export default class Program extends Resource { uniform = uniform.texture; } if (uniform instanceof Texture) { + const texture = uniform; // Bind texture to index - uniform.bind(uniformSetter.textureIndex); + texture.bind(uniformSetter.textureIndex); } // Bind a sampler (if supplied) to index if (sampler) { diff --git a/modules/core/src/webgl/texture-2d.js b/modules/core/src/webgl/texture-2d.js index 720217ce2..76fed7090 100644 --- a/modules/core/src/webgl/texture-2d.js +++ b/modules/core/src/webgl/texture-2d.js @@ -1,5 +1,6 @@ import GL from '@luma.gl/constants'; import Texture from './texture'; +import {loadImage} from '../io'; import {assertWebGLContext} from '../webgl-utils'; export default class Texture2D extends Texture { @@ -7,24 +8,21 @@ export default class Texture2D extends Texture { return Texture.isSupported(gl, opts); } - /** - * @classdesc - * 2D WebGL Texture - * Note: Constructor will initialize your texture. - * - * @class - * @param {WebGLRenderingContext} gl - gl context - * @param {Image|ArrayBuffer|null} opts= - named options - * @param {Image|ArrayBuffer|null} opts.data= - buffer - * @param {GLint} width - width of texture - * @param {GLint} height - height of texture - */ - constructor(gl, opts = {}) { + constructor(gl, props = {}) { assertWebGLContext(gl); - super(gl, Object.assign({}, opts, {target: gl.TEXTURE_2D})); + // Signature: new Texture2D(gl, url | Promise) + if (props instanceof Promise || typeof props === 'string') { + props = {data: props}; + } + // Signature: new Texture2D(gl, {data: url}) + if (typeof props.data === 'string') { + props = Object.assign({}, props, {data: loadImage(props.data)}); + } - this.initialize(opts); + super(gl, Object.assign({}, props, {target: gl.TEXTURE_2D})); + + this.initialize(props); Object.seal(this); } diff --git a/modules/core/src/webgl/texture-cube.js b/modules/core/src/webgl/texture-cube.js index c57278e63..f63c10422 100644 --- a/modules/core/src/webgl/texture-cube.js +++ b/modules/core/src/webgl/texture-cube.js @@ -12,22 +12,22 @@ const FACES = [ ]; export default class TextureCube extends Texture { - constructor(gl, opts = {}) { - super(gl, Object.assign({}, opts, {target: GL.TEXTURE_CUBE_MAP})); - this.initialize(opts); + constructor(gl, props = {}) { + super(gl, Object.assign({}, props, {target: GL.TEXTURE_CUBE_MAP})); + this.initialize(props); Object.seal(this); } /* eslint-disable max-len, max-statements */ - initialize(opts = {}) { - const {format = GL.RGBA, mipmaps = true} = opts; + initialize(props = {}) { + const {format = GL.RGBA, mipmaps = true} = props; - let {width = 1, height = 1, type = GL.UNSIGNED_BYTE, dataFormat} = opts; + let {width = 1, height = 1, type = GL.UNSIGNED_BYTE, dataFormat} = props; // Deduce width and height based on one of the faces ({type, dataFormat} = this._deduceParameters({format, type, dataFormat})); ({width, height} = this._deduceImageSize({ - data: opts[GL.TEXTURE_CUBE_MAP_POSITIVE_X], + data: props[GL.TEXTURE_CUBE_MAP_POSITIVE_X], width, height })); @@ -36,26 +36,26 @@ export default class TextureCube extends Texture { assert(width === height); // Temporarily apply any pixel store paramaters and build textures - // withParameters(this.gl, opts, () => { + // withParameters(this.gl, props, () => { // for (const face of CUBE_MAP_FACES) { // this.setImageData({ // target: face, - // data: opts[face], + // data: props[face], // width, height, format, type, dataFormat, border, mipmaps // }); // } // }); - this.setCubeMapImageData(opts); + this.setCubeMapImageData(props); // Called here so that GL. // TODO - should genMipmap() be called on the cubemap or on the faces? if (mipmaps) { - this.generateMipmap(opts); + this.generateMipmap(props); } - // Store opts for accessors - this.opts = opts; + // Store props for accessors + this.opts = props; } subImage({face, data, x = 0, y = 0, mipmapLevel = 0}) { @@ -74,17 +74,66 @@ export default class TextureCube extends Texture { generateMipmap = false }) { const {gl} = this; - pixels = pixels || data; - this.bind(); - if (this.width || this.height) { - for (const face of FACES) { - gl.texImage2D(face, 0, format, width, height, border, format, type, pixels[face]); + const imageDataMap = pixels || data; + + // TODO - Make this a method of Texture, and call that + // A rare instance where a local function is the lesser evil? + const setImageData = (face, imageData) => { + if (this.width || this.height) { + gl.texImage2D(face, 0, format, width, height, border, format, type, imageData); + } else { + gl.texImage2D(face, 0, format, format, type, imageData); } - } else { - for (const face of FACES) { - gl.texImage2D(face, 0, format, format, type, pixels[face]); + }; + + this.bind(); + for (const face of FACES) { + const imageData = imageDataMap[face]; + + if (imageData instanceof Promise) { + imageData.then(resolvedImageData => setImageData(face, resolvedImageData)); + } else { + setImageData(face, imageData); } } + this.unbind(); + } + + setImageDataForFace(options) { + const { + face, + width, + height, + pixels, + data, + border = 0, + format = GL.RGBA, + type = GL.UNSIGNED_BYTE + // generateMipmap = false // TODO + } = options; + + const {gl} = this; + + const imageData = pixels || data; + + this.bind(); + if (imageData instanceof Promise) { + imageData.then(resolvedImageData => + this.setImageDataForFace( + Object.assign({}, options, { + face, + data: resolvedImageData, + pixels: resolvedImageData + }) + ) + ); + } else if (this.width || this.height) { + gl.texImage2D(face, 0, format, width, height, border, format, type, imageData); + } else { + gl.texImage2D(face, 0, format, format, type, imageData); + } + + return this; } bind({index} = {}) { diff --git a/modules/core/src/webgl/texture.js b/modules/core/src/webgl/texture.js index 56bda7d40..5446bec52 100644 --- a/modules/core/src/webgl/texture.js +++ b/modules/core/src/webgl/texture.js @@ -45,6 +45,11 @@ export default class Texture extends Resource { this.hasFloatTexture = gl.getExtension('OES_texture_float'); this.textureUnit = undefined; + // Program.draw() checks the loaded flag of all textures to avoid + // Textures that are still loading from promises + // Set to true as soon as texture has been initialized with valid data + this.loaded = false; + this.width = undefined; this.height = undefined; this.format = undefined; @@ -63,6 +68,18 @@ export default class Texture extends Resource { initialize(props = {}) { let data = props.data; + if (data instanceof Promise) { + data.then(resolvedImageData => + this.initialize( + Object.assign({}, props, { + pixels: resolvedImageData, + data: resolvedImageData + }) + ) + ); + return this; + } + const { pixels = null, format = GL.RGBA, @@ -290,6 +307,8 @@ export default class Texture extends Resource { } }); + this.loaded = true; + return this; } /* eslint-enable max-len, max-statements, complexity */ diff --git a/test/modules/core/webgl/texture-2d.spec.js b/test/modules/core/webgl/texture-2d.spec.js index c665a066f..91aadcdbb 100644 --- a/test/modules/core/webgl/texture-2d.spec.js +++ b/test/modules/core/webgl/texture-2d.spec.js @@ -26,6 +26,30 @@ test('WebGL#Texture2D construct/delete', t => { t.end(); }); +test('WebGL#Texture2D async constructor', t => { + const {gl} = fixture; + + let texture = new Texture2D(gl); + t.ok(texture instanceof Texture2D, 'Synchronous Texture2D construction successful'); + t.equal(texture.loaded, true, 'Sync Texture2D marked as loaded'); + texture.delete(); + + let loadCompleted; + const loadPromise = new Promise(resolve => { + loadCompleted = resolve; // eslint-disable-line + }); + texture = new Texture2D(gl, loadPromise); + t.ok(texture instanceof Texture2D, 'Asynchronous Texture2D construction successful'); + t.equal(texture.loaded, false, 'Async Texture2D initially marked as not loaded'); + + loadPromise.then(() => { + t.equal(texture.loaded, true, 'Async Texture2D marked as loaded on promise completion'); + t.end(); + }); + + loadCompleted(null); +}); + test('WebGL#Texture2D buffer update', t => { const {gl} = fixture;