Async Textures (#876)

This commit is contained in:
Ib Green 2019-02-01 16:17:45 -08:00 committed by GitHub
parent fc6da900d4
commit d9799de1d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 236 additions and 559 deletions

View File

@ -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.

View File

@ -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..

View File

@ -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.

View File

@ -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

View File

@ -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` -

View File

@ -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

View File

@ -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
...
});
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}

View File

@ -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}`);
}
}

View File

@ -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');
}

View File

@ -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) {

View File

@ -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);
}

View File

@ -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} = {}) {

View File

@ -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 */

View File

@ -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;