elation-engine
Version:
WebGL/WebVR engine written in Javascript
1,402 lines (1,317 loc) • 85.9 kB
JavaScript
elation.require(['utils.workerpool', 'engine.external.three.three', 'engine.external.libgif', 'engine.external.textdecoder-polyfill', 'engine.external.three.three-loaders', 'engine.external.three.three-vrm', 'engine.external.three.three-icosa'], function() {
THREE.Cache.enabled = true;
// TODO - submit pull request to add these to three.js
THREE.SBSTexture = function ( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
THREE.Texture.call( this, image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
this.repeat.x = 0.5;
}
THREE.SBSTexture.prototype = Object.create( THREE.Texture.prototype );
THREE.SBSTexture.prototype.constructor = THREE.SBSTexture;
THREE.SBSTexture.prototype.setEye = function(eye) {
if (eye == 'left') {
this.offset.x = (this.reverse ? 0.5 : 0);
} else {
this.offset.x = (this.reverse ? 0 : 0.5);
}
this.eye = eye;
}
THREE.SBSTexture.prototype.swap = function() {
if (this.eye == 'right') {
this.setEye('left');
} else {
this.setEye('right');
}
}
THREE.SBSVideoTexture = function ( video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
THREE.VideoTexture.call( this, video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
this.repeat.x = 0.5;
this.reverse = false;
}
THREE.SBSVideoTexture.prototype = Object.create( THREE.SBSTexture.prototype );
THREE.SBSVideoTexture.prototype.constructor = THREE.SBSVideoTexture;
THREE.SBSVideoTexture.prototype = Object.assign( Object.create( THREE.VideoTexture.prototype ), {
constructor: THREE.SBSVideoTexture,
isVideoTexture: true,
update: function () {
var video = this.image;
if ( video.readyState >= video.HAVE_CURRENT_DATA ) {
this.needsUpdate = true;
}
},
setEye: function(eye) {
if (eye == 'left') {
this.offset.x = (this.reverse ? 0.5 : 0);
} else {
this.offset.x = (this.reverse ? 0 : 0.5);
}
this.eye = eye;
}
} );
elation.extend('engine.assets', {
assets: {},
types: {},
corsproxy: '',
dracopath: false,
placeholders: {},
scriptOverrides: {},
init: function(dummy) {
var corsproxy = elation.config.get('engine.assets.corsproxy', '');
//THREE.Loader.Handlers.add(/.*/i, corsproxy);
//THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
if (corsproxy != '') {
this.setCORSProxy(corsproxy, dummy);
}
},
initTextureLoaders: function(rendersystem, libpath) {
this.rendersystem = rendersystem;
let renderer = rendersystem.renderer;
/*
let basisloader = new THREE.BasisTextureLoader();
basisloader.setTranscoderPath(libpath);
basisloader.detectSupport(renderer);
this.basisloader = basisloader;
*/
this.ktx2loader = new THREE.KTX2Loader();
this.ktx2loader.setTranscoderPath(libpath);
this.ktx2loader.detectSupport(renderer);
let pmremGenerator = new THREE.PMREMGenerator( renderer );
pmremGenerator.compileEquirectangularShader();
this.pmremGenerator = pmremGenerator;
},
loadAssetPack: function(url, baseurl) {
this.assetroot = new elation.engine.assets.pack({name: url, src: url, baseurl: baseurl});
return this.assetroot;
},
loadJSON: function(json, baseurl) {
var assetpack = new elation.engine.assets.pack({name: "asdf", baseurl: baseurl, json: json});
return assetpack;
},
get: function(asset) {
if (!ENV_IS_BROWSER) return;
var type = asset.assettype || 'base';
var assetclass = elation.engine.assets[type] || elation.engine.assets.unknown;
var assetobj = new assetclass(asset);
return assetobj;
},
find: function(type, name, raw) {
if (!ENV_IS_BROWSER) return;
var asset;
if (elation.engine.assets.types[type]) {
asset = elation.engine.assets.types[type][name];
}
//console.log(asset, type, name, elation.engine.assets.types[type]);
if (raw) {
return asset;
}
if (asset) {
return asset.getInstance();
} else {
asset = elation.engine.assets.get({assettype: type, name: name});
return asset.getInstance();
}
return undefined;
},
setCORSProxy: function(proxy, dummy) {
elation.engine.assets.corsproxy = proxy;
var loader = new elation.engine.assets.corsproxyloader(proxy, undefined, dummy);
elation.engine.assetdownloader.setCORSProxy(proxy);
THREE.DefaultLoadingManager.addHandler(/.*/i, loader);
if (!elation.env.isWorker && elation.engine.assets.loaderpool) {
elation.engine.assets.loaderpool.sendMessage('setcorsproxy', proxy);
}
},
setPlaceholder: function(type, name) {
this.placeholders[type] = this.find(type, name);
},
setDracoPath: function(dracopath) {
this.dracopath = dracopath;
},
getOrigin(baseurl=null) {
if (baseurl) {
let m = baseurl.match(/(https?:\/\/[^\/]+)/i);
if (m) {
return m[1];
}
}
return self.location.origin;
},
isURLRelative: function(src) {
if (src && src.match(/^([\S]+:)?\/\//) || src[0] == '/') {
return false;
}
return true;
},
isURLAbsolute: function(src) {
return (src[0] == '/' && src[1] != '/');
},
isURLLocal: function(src) {
if (this.isURLBlob(src) || this.isURLData(src)) {
return true;
}
if (src.match(/^(https?:)?\/\//i)) {
return (src.indexOf(self.location.origin) == 0);
}
return (
(src[0] == '/' && src[1] != '/') ||
(src[0] != '/')
);
},
isURLData: function(url) {
if (!url) return false;
return url.indexOf('data:') == 0;
},
isURLBlob: function(url) {
if (!url) return false;
return url.indexOf('blob:') == 0;
},
isURLProxied: function(url) {
if (!url || !elation.engine.assets.corsproxy) return false;
return url.indexOf(elation.engine.assets.corsproxy) == 0;
},
getFullURL: function(url, baseurl=null) {
if (!url) url = '';
if (!baseurl) baseurl = '';
var fullurl = url;
if (!this.isURLBlob(fullurl) && !this.isURLData(fullurl)) {
if (this.isURLRelative(fullurl) && fullurl.substr(0, baseurl.length) != baseurl) {
fullurl = baseurl + fullurl;
} else if (this.isURLProxied(fullurl)) {
fullurl = fullurl.replace(elation.engine.assets.corsproxy, '');
} else if (this.isURLAbsolute(fullurl)) {
fullurl = this.getOrigin(baseurl) + fullurl;
}
}
return fullurl;
},
loaderpool: false
});
elation.extend('engine.assetdownloader', new function() {
this.corsproxy = '';
this.queue = {};
this.setCORSProxy = function(proxy) {
this.corsproxy = proxy;
}
this.isUrlInQueue = function(url) {
var fullurl = url;
if (this.corsproxy && fullurl.indexOf(this.corsproxy) != 0) fullurl = this.corsproxy + fullurl;
return fullurl in this.queue;
}
this.fetchURLs = function(urls, progress, responsetype) {
var promises = [],
queue = this.queue;
for (var i = 0; i < urls.length; i++) {
let subpromise = this.fetchURL(urls[i], progress, responsetype);
promises.push(subpromise);
}
return Promise.all(promises);
}
this.fetchURL = function(url, progress, responsetype) {
var corsproxy = this.corsproxy;
let agent = this.getAgentForURL(url);
return agent.fetch(url, progress, responsetype);
}
this.getAgentForURL = function(url) {
let urlparts = elation.utils.parseURL(url);
let agentname = urlparts.scheme;
// Check if we have a handler for this URL protocol, if we don't then fall back to the default 'xhr' agent
if (!elation.engine.assetloader.agent[agentname]) {
agentname = 'xhr';
}
return elation.engine.assetloader.agent[agentname];
}
});
elation.extend('engine.assetloader.agent.xhr', new function() {
this.getFullURL = function(url) {
}
this.fetch = function(url, progress, responsetype='arraybuffer') {
return new Promise(function(resolve, reject) {
if (!this.queue) this.queue = {};
var fullurl = url;
let corsproxy = elation.engine.assetdownloader.corsproxy;
if (corsproxy &&
fullurl.indexOf(corsproxy) != 0 &&
fullurl.indexOf('blob:') != 0 &&
fullurl.indexOf('data:') != 0 &&
fullurl.indexOf(self.location.origin) != 0)
{
fullurl = corsproxy + fullurl;
}
if (!this.queue[fullurl]) {
var xhr = this.queue[fullurl] = elation.net.get(fullurl, null, {
responseType: responsetype,
onload: (ev) => {
delete this.queue[fullurl];
var status = ev.target.status;
if (status == 200) {
resolve(ev);
} else {
reject();
}
},
onerror: () => { delete this.queue[fullurl]; reject(); },
onprogress: progress,
headers: {
'X-Requested-With': 'Elation Engine asset loader'
}
});
} else {
var xhr = this.queue[fullurl];
if (xhr.readyState == 4) {
setTimeout(function() { resolve({target: xhr}); }, 0);
} else {
elation.events.add(xhr, 'load', resolve);
elation.events.add(xhr, 'error', reject);
elation.events.add(xhr, 'progress', progress);
}
}
});
}
});
elation.extend('engine.assetloader.agent.dat', new function() {
this.fetch = function(url) {
return new Promise((resolve, reject) => {
if (typeof DatArchive == 'undefined') {
console.warn('DatArchive not supported in this browser');
reject();
return;
}
let urlparts = elation.utils.parseURL(url);
if (urlparts.host) {
this.getArchive(urlparts.host).then(archive => {
let path = urlparts.path;
if (path[path.length-1] == '/') {
path += 'index.html';
}
archive.readFile(path, 'binary').then(file => {
// FIXME - we're emulating an XHR object here, because the asset loader was initially written for XHR and uses some of the convenience functions
resolve({
target: {
responseURL: url,
data: file,
response: file,
getResponseHeader(header) {
if (header.toLowerCase() == 'content-type') {
// FIXME - We should implement mime type detection at a lower level, and avoid the need to access the XHR object in calling code
return 'application/octet-stream';
}
}
},
});
});
});
}
});
}
this.getArchive = function(daturl) {
return new Promise((resolve, reject) => {
if (!this.archives) this.archives = {};
if (this.archives[daturl]) {
resolve(this.archives[daturl]);
} else {
DatArchive.load(daturl).then(archive => {
this.archives[daturl] = archive;
resolve(this.archives[daturl]);
});
}
});
}
});
elation.extend('engine.assetcache', new function() {
this.queued = [];
this.open = function(name) {
this.cachename = name;
caches.open(name).then(elation.bind(this, this.setCache));
}
this.setCache = function(cache) {
this.cache = cache;
// If we queued any cache lookups before the cache opened, resolve them
return Promises.all(this.queued);
}
this.get = function(key) {
if (this.cache) {
return new Promise(elation.bind(function(resolve, reject) {
var req = (key instanceof Request ? key : new Request(key));
this.cache.get(req).then(resolve);
}));
} else {
// TODO - queue it!
console.log('AssetCache warning: cache not open yet, cant get', key, this.cachename);
}
}
this.set = function(key, value) {
if (this.cache) {
return new Promise(elation.bind(function(resolve, reject) {
var req = (key instanceof Request ? key : new Request(key));
this.cache.get(req).then(resolve);
}));
} else {
// TODO - queue it!
console.log('AssetCache warning: cache not open yet, cant set', key, value, this.cachename);
}
}
});
elation.define('engine.assets.corsproxyloader', {
_construct: function(corsproxy, manager, dummy) {
this.corsproxy = corsproxy || '';
this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
this.uuidmap = {};
this.dummy = dummy;
},
load: function ( url, onLoad, onProgress, onError ) {
var fullurl = url;
if (this.corsproxy != '' &&
url.indexOf(this.corsproxy) != 0 &&
url.indexOf('blob:') != 0 &&
url.indexOf('data:') != 0 &&
url.indexOf(self.location.origin) != 0) {
fullurl = this.corsproxy + url;
}
if (!this.dummy) {
return THREE.TextureLoader.prototype.load.call(this, fullurl, onLoad, onProgress, onError);
}
return this.getDummyTexture(fullurl, onLoad);
},
getDummyTexture: function(url, onLoad) {
var texture = new THREE.Texture();
var uuid = this.uuidmap[url];
if (!uuid) {
uuid = this.uuidmap[url] = THREE.MathUtils.generateUUID();
}
var img = { uuid: uuid, src: url, toDataURL: function() { return url; } };
texture.image = img;
if (onLoad) {
setTimeout(onLoad.bind(img, texture), 0);
}
return texture;
}
}, THREE.TextureLoader);
elation.define('engine.assets.base', {
assettype: 'base',
name: '',
description: '',
license: 'unknown',
author: 'unknown',
sourceurl: false,
size: false,
loaded: false,
preview: false,
baseurl: '',
src: false,
proxy: true,
preload: false,
instances: [],
assetpack: null,
_construct: function(args) {
elation.class.call(this, args);
this.init();
},
init: function() {
this.instances = [];
if (this.preload && this.preload !== 'false') {
this.load();
}
},
load: function() {
console.log('engine.assets.base load() should not be called directly', this);
},
isURLRelative: function(src) {
return elation.engine.assets.isURLRelative(src);
},
isURLAbsolute: function(src) {
return elation.engine.assets.isURLAbsolute(src);
},
isURLLocal: function(src) {
return elation.engine.assets.isURLLocal(src);
},
isURLData: function(url) {
return elation.engine.assets.isURLData(url);
},
isURLBlob: function(url) {
return elation.engine.assets.isURLBlob(url);
},
isURLProxied: function(url) {
return elation.engine.assets.isURLProxied(url);
},
getFullURL: function(url, baseurl) {
if (!url) url = this.src;
if (!url) url = '';
if (!baseurl) baseurl = this.baseurl;
var fullurl = url;
if (!this.isURLBlob(fullurl) && !this.isURLData(fullurl)) {
if (this.isURLRelative(fullurl) && fullurl.substr(0, baseurl.length) != baseurl) {
fullurl = baseurl + fullurl;
} else if (this.isURLProxied(fullurl)) {
fullurl = fullurl.replace(elation.engine.assets.corsproxy, '');
} else if (this.isURLAbsolute(fullurl)) {
fullurl = this.getOrigin() + fullurl;
}
}
return fullurl;
},
getOrigin() {
return elation.engine.assets.getOrigin(this.baseurl);
},
getProxiedURL: function(url, baseurl) {
var proxiedurl = this.getFullURL(url, baseurl);
if (this.proxy && this.proxy != 'false' && proxiedurl && elation.engine.assets.corsproxy && !this.isURLLocal(proxiedurl) && proxiedurl.indexOf(elation.engine.assets.corsproxy) == -1) {
var re = /:\/\/([^\/\@]+@)/;
var m = proxiedurl.match(re);
// Check it asset has authentication info, and pass it through if it does
if (m) {
proxiedurl = elation.engine.assets.corsproxy.replace(':\/\/', ':\/\/' + m[1]) + proxiedurl.replace(m[1], '');
} else {
proxiedurl = elation.engine.assets.corsproxy + proxiedurl;
}
}
return proxiedurl;
},
getBaseURL: function(url) {
var url = url || this.getFullURL();
if (url.indexOf('/') == -1) url = document.location.href;
var parts = url.split('/');
parts.pop();
return parts.join('/') + '/';
},
getInstance: function(args) {
return undefined;
},
executeWhenLoaded: function(callback) {
if (this.loaded) {
// We've already loaded the asset, so execute the callback asynchronously
setTimeout(callback, 0);
} else {
// Asset isn't loaded yet, set up a local callback that can self-remove, so our callback only executes once
let cb = (ev) => {
elation.events.remove(this, 'asset_load', cb);
callback();
};
elation.events.add(this, 'asset_load', cb);
}
},
update: function(args) {
if (args) {
for (let k in args) {
this[k] = args[k];
}
}
},
dispose() {
console.log('dispose of basic asset', this);
if (this.assetpack) this.assetpack = null;
}
}, elation.class);
elation.define('engine.assets.unknown', {
assettype: 'unknown',
load: function() {
},
_construct: function(args) {
console.log('Unknown asset type: ', args.assettype, args);
elation.engine.assets.base.call(this, args);
}
}, elation.engine.assets.base);
elation.define('engine.assets.image', {
assettype: 'image',
src: false,
canvas: false,
sbs3d: false,
ou3d: false,
reverse3d: false,
texture: false,
frames: false,
flipy: true,
invert: false,
imagetype: '',
tex_linear: true,
srgb: false,
equi: false,
hasalpha: null,
rawimage: null,
preload: true,
maxsize: null,
load: function() {
if (this.texture) {
this._texture = this.texture;
} else if (this.src) {
var fullurl = this.getFullURL(this.src);
var texture;
if (this.sbs3d) {
texture = this._texture = new THREE.SBSTexture();
texture.reverse = this.reverse3d;
} else {
texture = this._texture = new THREE.Texture();
}
texture.image = this.canvas = this.getCanvas();
texture.image.originalSrc = this.src;
texture.sourceFile = this.src;
//texture.needsUpdate = true;
texture.flipY = (this.flipy === true || this.flipy === 'true');
texture.encoding = (this.srgb ? THREE.sRGBEncoding : THREE.LinearEncoding);
if (this.equi) {
texture.mapping = THREE.EquirectangularReflectionMapping;
}
if (this.isURLData(fullurl)) {
this.loadImageByURL();
} else {
elation.engine.assetdownloader.fetchURLs([fullurl], elation.bind(this, this.handleProgress), 'blob').then(
elation.bind(this, function(events) {
var xhr = events[0].target;
// FIXME - we're looking at both mime type and file extension here, we should really just use one or the other
var type = this.contenttype = xhr.getResponseHeader('content-type')
let imagetype = this.detectImageType();
if (imagetype == 'basis') {
let loader = elation.engine.assets.basisloader;
// FIXME - we switched loader to request Blob responses, make sure Basis textures still load
let blob = events[0].target.response;
blob.arrayBuffer()
.then(buffer => loader._createTexture([buffer]))
.then(texture => this.handleLoadBasis(texture))
} else if (imagetype == 'hdr') {
let loader = new THREE.RGBELoader();
loader.load(fullurl, texture => {
this._texture = texture;
//const envMap = elation.engine.assets.pmremGenerator.fromEquirectangular( texture );
//this._texture = envMap.texture;
this.loaded = true;
this.uploaded = false;
this.sendLoadEvents();
});
} else if (imagetype == 'exr') {
// TODO - this should probably done off-thread if possible, it currently locks rendering for a noticable amount of time
let loader = new THREE.EXRLoader();
if (loader) {
loader.setDataType( THREE.UnsignedByteType );
loader.load(fullurl, (exrtexture) => {
let exrCubeRenderTarget = elation.engine.assets.pmremGenerator.fromEquirectangular( exrtexture );
let exrBackground = exrCubeRenderTarget.texture;
exrtexture.dispose();
this._texture = exrBackground;
this.loaded = true;
this.uploaded = false;
this.sendLoadEvents();
});
}
} else if (imagetype == 'dds') {
let loader = new THREE.DDSLoader();
loader.load(this.getProxiedURL(this.src), data => {
data.encoding = THREE.sRGBEncoding;
this._texture = data;
this.loaded = true;
this.uploaded = false;
this.sendLoadEvents();
});
} else {
let blob = xhr.response;
if (typeof createImageBitmap == 'function' && type != 'image/gif') {
createImageBitmap(blob).then(elation.bind(this, this.handleLoad), elation.bind(this, this.handleBitmapError));
} else {
let imgurl = URL.createObjectURL(blob);
this.loadImageByURL(imgurl);
}
}
this.state = 'processing';
elation.events.fire({element: this, type: 'asset_load_processing'});
}),
elation.bind(this, function(error) {
this.state = 'error';
elation.events.fire({element: this, type: 'asset_error'});
})
);
elation.events.fire({element: this, type: 'asset_load_queued'});
}
} else if (this.canvas) {
var texture = this._texture = new THREE.Texture();
texture.image = this.canvas;
texture.image.originalSrc = '';
texture.sourceFile = '';
texture.needsUpdate = true;
texture.flipY = this.flipy;
elation.events.add(this.canvas, 'asset_update', () => { texture.needsUpdate = true; });
this.loaded = true;
setTimeout(() => this.sendLoadEvents(), 0);
}
},
loadImageByURL: function(src) {
if (!src) {
src = this.getProxiedURL(this.src);
}
var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );
elation.events.add(image, 'load', elation.bind(this, this.handleLoad, image));
elation.events.add(image, 'error', elation.bind(this, this.handleError));
image.crossOrigin = 'anonymous';
image.src = src;
return image;
},
getCanvas: function() {
if (!this.canvas) {
var canvas = document.createElement('canvas');
var size = 32,
gridcount = 4,
gridsize = size / gridcount;
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#cccccc';
ctx.fillRect(0,0,size,size);
ctx.fillStyle = '#666666';
for (var i = 0; i < gridcount*gridcount; i += 1) {
var x = i % gridcount;
var y = Math.floor(i / gridcount);
if ((x + y) % 2 == 0) {
ctx.fillRect(x * gridsize, y * gridsize, gridsize, gridsize);
}
}
this.canvas = canvas;
}
return this.canvas;
},
handleLoad: function(image) {
//console.log('loaded image', this, image);
this.rawimage = image;
var texture = this._texture;
texture.image = this.processImage(image);
texture.needsUpdate = true;
texture.wrapS = texture.wrapT = (this.tex_linear ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping);
texture.anisotropy = elation.config.get('engine.assets.image.anisotropy', 4);
this.loaded = true;
this.uploaded = false;
this.sendLoadEvents();
},
handleLoadBasis: function(texture) {
//console.log('loaded Basis texture', this, texture);
this._texture = texture;
this._texture.generateMipmaps = false;
texture.needsUpdate = true;
texture.wrapS = texture.wrapT = (this.tex_linear ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping);
texture.anisotropy = elation.config.get('engine.assets.image.anisotropy', 4);
this.loaded = true;
this.uploaded = false;
this.sendLoadEvents();
},
sendLoadEvents: function() {
elation.events.fire({type: 'asset_load', element: this._texture});
elation.events.fire({type: 'asset_load', element: this});
elation.events.fire({element: this, type: 'asset_load_complete'});
},
handleProgress: function(ev) {
var progress = {
src: ev.target.responseURL,
loaded: ev.loaded,
total: ev.total
};
this.size = ev.total;
//console.log('image progress', progress);
elation.events.fire({element: this, type: 'asset_load_progress', data: progress});
},
handleBitmapError: function(src, ev) {
console.log('Error loading image via createImageBitmap, fall back on normal image', this.src);
this.loadImageByURL();
},
handleError: function(ev) {
console.log('image error!', this, this._texture.image, ev);
var canvas = this.getCanvas();
var size = 16;
canvas.width = canvas.height = size;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#f0f';
ctx.fillRect(0,0,size,size);
this._texture.image = canvas;
this._texture.needsUpdate = true;
this._texture.generateMipmaps = false;
elation.events.fire({type: 'asset_error', element: this._texture});
},
getInstance: function(args) {
if (!this._texture) {
this.load();
}
return this._texture;
},
processImage: function(image) {
this.imagetype = this.detectImageType();
if (this.imagetype == 'gif') {
this.hasalpha = true; // FIXME - if we're cracking the gif open already, we should be able to tell if it has alpha or not
return this.convertGif(image);
} else { //if (!elation.engine.materials.isPowerOfTwo(image.width) || !elation.engine.materials.isPowerOfTwo(image.height)) {
// Scale up the texture to the next highest power of two dimensions.
var canvas = this.canvas;
canvas.src = this.src;
canvas.originalSrc = this.src;
var imagemax = elation.utils.any(this.maxsize, elation.config.get('engine.assets.image.maxsize', Infinity));
canvas.width = Math.min(image.width, imagemax); //(this.tex_linear ? Math.min(imagemax, this.nextHighestPowerOfTwo(image.width)) : image.width);
canvas.height = Math.min(image.height, imagemax); //(this.tex_linear ? Math.min(imagemax, this.nextHighestPowerOfTwo(image.height)) : image.height);
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
if (this.hasalpha === null) {
if (!this.src.match(/\.jpg$/i)) {
this.hasalpha = this.canvasHasAlpha(canvas);
} else {
this.hasalpha = false;
}
}
this._texture.generateMipmaps = elation.config.get('engine.assets.image.mipmaps', true);
if (this.invert) {
this.invertImage(canvas);
}
return canvas;
//} else {
// return image;
}
},
convertGif: function(image) {
var gif = new SuperGif({gif: image, draw_while_loading: true, loop_mode: false, auto_play: false});
// Decode gif frames into a series of canvases, then swap between canvases to animate the texture
// This could be made more efficient by uploading each frame to the GPU as a separate texture, and
// swapping the texture handle each frame instead of re-uploading the frame. This is hard to do
// with the way Three.js handles Texture objects, but it might be possible to fiddle with
// renderer.properties[texture].__webglTexture directly
// It could also be made more efficient by moving the gif decoding into a worker, and just passing
// back messages with decoded frame data.
var getCanvas = () => {
var newcanvas = document.createElement('canvas');
newcanvas.width = this.nextHighestPowerOfTwo(image.width);
newcanvas.height = this.nextHighestPowerOfTwo(image.height);
return newcanvas;
}
var newcanvas = getCanvas();
var mainctx = newcanvas.getContext('2d');
var texture = this._texture;
texture.minFilter = THREE.NearestFilter;
texture.magFilter = THREE.NearestFilter;
//texture.generateMipmaps = false;
var frames = [];
var frametextures = this.frames = [];
var framedelays = [];
var framenum = -1;
var lastframe = texture;
//console.log('load gif?', image);
gif.load(function() {
var canvas = gif.get_canvas();
/*
console.log('gif loaded!', canvas);
document.body.appendChild(newcanvas);
newcanvas.style.position = 'absolute';
newcanvas.style.zIndex = 1000;
newcanvas.style.top = 0;
newcanvas.style.left = 0;
*/
var doGIFFrame = function(isstatic) {
framenum = (framenum + 1) % gif.get_length();
var frame = frames[framenum];
if (!frame) {
gif.move_to(framenum);
var gifframe = gif.get_frame(framenum);
if (gifframe) {
frame = frames[framenum] = { framenum: framenum, delay: gifframe.delay, image: getCanvas() };
ctx = frame.image.getContext('2d');
newcanvas.width = canvas.width;
newcanvas.height = canvas.height;
//mainctx.putImageData(gifframe.data, 0, 0, 0, 0, canvas.width, canvas.height);
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, frame.image.width, frame.image.height);
texture.minFilter = texture.magFilter = THREE.NearestFilter; // FIXME - should this be hardcoded for all gifs?
frametextures[framenum] = new THREE.Texture(frame.image);
frametextures[framenum].minFilter = frametextures[framenum].magFilter = THREE.NearestFilter; // FIXME - should this be hardcoded for all gifs?
frametextures[framenum].wrapS = frametextures[framenum].wrapT = THREE.RepeatWrapping;
frametextures[framenum].needsUpdate = true;
}
}
if (frame && frame.image) {
//console.log('gifframe', frame);
/*
texture.image = frame.image;
texture.needsUpdate = true;
elation.events.fire({type: 'update', element: texture});
*/
var frametex = frametextures[framenum] || lastframe;
if (frametex !== lastframe) {
lastframe = frametex;
}
elation.events.fire({element: texture, type: 'asset_update', data: frametex});
elation.events.fire({element: texture, type: 'update', data: frametex});
elation.events.fire({element: this, type: 'asset_update', data: frametex});
}
if (!isstatic) {
var delay = (frame && frame.delay > 0 ? frame.delay : 10);
setTimeout(doGIFFrame, delay * 10);
}
}
doGIFFrame(gif.get_length() == 1);
});
return newcanvas;
},
detectImageType: function() {
// FIXME - really we should be cracking open the file and looking at magic number to determine this
// We might also be able to get hints from the XHR loader about the image's MIME type
var type = 'jpg';
var map = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
};
if (this.contenttype && map[this.contenttype]) {
type = map[this.contenttype];
} else if (this.src && this.src.match(/\.(.*?)$/)) {
var parts = this.src.split('.');
type = parts.pop();
} else if (this.canvas) {
type = 'png';
}
return type;
},
canvasHasAlpha: function(canvas) {
if (!(this.imagetype == 'gif' || this.imagetype == 'png')) {
return false;
}
// This could be made more efficient by doing the work on the gpu. We could make a scene with the
// texture and an orthographic camera, and a shader which returns alpha=0 for any alpha value < 1
// We could then perform a series of downsamples until the texture is (1,1) in size, and read back
// that pixel value with readPixels(). If there was any alpha in the original image, this final
// pixel should also be transparent.
/*
var width = Math.min(64, canvas.width),
height = Math.min(64, canvas.height);
if (!this._scratchcanvas) {
this._scratchcanvas = document.createElement('canvas');
this._scratchcanvasctx = this._scratchcanvas.getContext('2d');
}
var checkcanvas = this._scratchcanvas,
ctx = this._scratchcanvasctx;
checkcanvas.width = width;
checkcanvas.height = height;
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
var pixeldata = ctx.getImageData(0, 0, width, height);
var hasalpha = false;
for (var i = 0; i < pixeldata.data.length; i+=4) {
if (pixeldata.data[i+3] != 255) {
return true;
}
}
return false;
*/
return elation.engine.assets.rendersystem.textureHasAlpha(this._texture);
},
invertImage: function(canvas) {
var ctx = canvas.getContext('2d');
var pixeldata = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var i = 0; i < pixeldata.data.length; i+=4) {
pixeldata.data[i] = 255 - pixeldata.data[i];
pixeldata.data[i+1] = 255 - pixeldata.data[i+1];
pixeldata.data[i+2] = 255 - pixeldata.data[i+2];
}
ctx.putImageData(pixeldata, 0, 0);
},
nextHighestPowerOfTwo: function(num) {
return Math.pow(2, Math.ceil(Math.log(num) / Math.log(2)));
},
dispose() {
if (this.assetpack) this.assetpack = null;
console.log('dispose of image', this);
delete this.rawimage;
delete this.canvas;
if (this._texture) {
this._texture.dispose();
}
this.loaded = false;
},
toCubemap(texture) {
let pmremGenerator = new THREE.PMREMGenerator( elation.engine.assets.rendersystem.renderer );
pmremGenerator.compileEquirectangularShader();
if (!texture) texture = this._texture;
let cubeRenderTarget = pmremGenerator.fromEquirectangular( texture );
let cubemap = cubeRenderTarget.texture;
//texture.dispose();
this._texture = cubemap;
this.loaded = true;
this.uploaded = false;
}
}, elation.engine.assets.base);
elation.define('engine.assets.video', {
assettype: 'video',
src: false,
video: false,
sbs3d: false,
ou3d: false,
eac360: false,
vr180: false,
hasalpha: false,
reverse3d: false,
auto_play: false,
loop: false,
texture: false,
srgb: true,
tex_linear: true,
preload: false,
extratracks: false,
hls: null,
type: THREE.UnsignedByteType,
format: THREE.RGBAFormat,
load: function() {
var video = this.video || this._video;
if (!video && this.src) {
var url = this.getProxiedURL(this.src);
var video = document.createElement('video');
video.muted = false;
video.preload = this.preload;
video.src = url;
video.crossOrigin = 'anonymous';
video.loop = this.loop;
if (url.match(/\.webm$/)) {
this.hasalpha = true;
}
if ('requestVideoFrameCallback' in video) {
video.requestVideoFrameCallback((time, metadata) => this.updateVideoFrame(time, metadata));
}
if (this.extratracks) {
this.extratracks.forEach(t => {
let track = document.createElement('track');
for (let k in t) {
track[k] = t[k];
}
video.appendChild(track);
console.log('- add track to video', track);
});
}
}
this._video = video;
let textureFormat = this.format;
if (false && this.sbs3d) {
this._texture = new THREE.SBSVideoTexture(video, THREE.UVMapping, THREE.ClampToEdgeWrapping, THREE.ClampToEdgeWrapping, null, null, textureFormat, this.type);
this._texture.reverse = this.reverse3d;
} else {
this._texture = new THREE.VideoTexture(video, THREE.UVMapping, THREE.ClampToEdgeWrapping, THREE.ClampToEdgeWrapping, null, null, textureFormat, this.type);
}
//this._texture.minFilter = THREE.LinearFilter;
//this._texture.magFilter = THREE.LinearFilter;
this._texture.encoding = (this.srgb ? THREE.sRGBEncoding : THREE.LinearEncoding);
elation.events.add(video, 'loadeddata', elation.bind(this, this.handleLoad));
elation.events.add(video, 'error', elation.bind(this, this.handleError));
if (this.auto_play) {
// FIXME - binding for easy event removal later. This should happen at a lower level
this.handleAutoplayStart = elation.bind(this, this.handleAutoplayStart);
// Bind on next tick to avoid time-ou firing prematurely due to load-time lag
setTimeout(elation.bind(this, function() {
elation.events.add(video, 'playing', this.handleAutoplayStart);
this._autoplaytimeout = setTimeout(elation.bind(this, this.handleAutoplayTimeout), 1000);
}), 0);
if (this.hls === true) {
this.initHLS();
} else {
this.play();
}
}
},
play: function() {
let video = this._video;
var promise = video.play();
if (promise) {
promise.then(elation.bind(this, function() {
this.handleAutoplayStart();
})).catch(elation.bind(this, function(err) {
// If autoplay failed, retry with muted video
var strerr = err.toString();
if (strerr.indexOf('NotAllowedError') == 0) {
//video.muted = true;
//video.play().catch((e) => console.log('huh what', e));
} else if (strerr.indexOf('NotSupportedError') == 0 && this.hls !== false) {
this.initHLS();
}
}));
}
},
handleLoad: function() {
this.loaded = true;
elation.events.fire({element: this, type: 'asset_load'});
elation.events.fire({element: this, type: 'asset_load_complete'});
},
handleProgress: function(ev) {
//console.log('image progress!', ev);
var progress = {
src: ev.target.responseURL,
loaded: ev.loaded,
total: ev.total
};
this.size = ev.total;
elation.events.fire({element: this, type: 'asset_load_progress', data: progress});
},
handleError: function(ev) {
console.log('video error!', ev);
//this._texture = false;
//console.log('Video failed to load, try HLS', this._video.error, ev);
/*
// Disabled Feb 2021 - this caused users with intermediate connection issues to constantly degrade HLS stream quality
let hls = this.hls;
if (hls) {
this.hlsDropHighestLevel();
}
*/
},
handleAutoplayStart: function(ev) {
if (this._autoplaytimeout) {
clearTimeout(this._autoplaytimeout);
}
elation.events.remove(this._video, 'playing', this.handleAutoplayStart);
elation.events.fire({element: this._texture, type: 'autoplaystart'});
},
handleAutoplayTimeout: function(ev) {
elation.events.fire({element: this._texture, type: 'autoplaytimeout'});
},
handleAutoplayFail: function(ev) {
elation.events.fire({element: this._texture, type: 'autoplayfail'});
},
updateVideoFrame: function(time, metadata) {
//elation.events.fire({element: this, type: 'videoframe'});
elation.events.fire({element: this._texture, type: 'videoframe'});
this._video.requestVideoFrameCallback((time, metadata) => this.updateVideoFrame(time, metadata));
},
getInstance: function(args) {
if (!this._texture) {
this.load();
}
return this._texture;
},
initHLS: function() {
if (typeof Hls != 'function') {
elation.file.get('js', 'https://cdn.jsdelivr.net/npm/hls.js@latest', elation.bind(this, this.initHLS));
//elation.file.get('js', 'https://baicoianu.com/~bai/janusweb/test/hls-modified.js', elation.bind(this, this.initHLS));
return;
}
let hlsConfig = {
debug: false,
//maxBufferLength: 10,
maxBufferLength: 30,
//capLevelOnFPSDrop: true,
//fpsDroppedMonitoringThreshold: .05,
maxBufferSize: 500 * 1024 * 1024,
/*
xhrSetup: function (xhr,url) {
//xhr.withCredentials = true; // do send cookie
xhr.setRequestHeader("Access-Control-Allow-Headers","Content-Type, Accept, X-Requested-With");
xhr.setRequestHeader("Access-Control-Allow-Origin",document.location.origin);
xhr.setRequestHeader("Access-Control-Allow-Credentials","true");
},
*/
xhrSetup: undefined,
progressive: false,
fetchSetup: function(context, initParams) {
//initParams.credentials = 'include';
return new Request(context.url, initParams);
},
};
console.log('set up hls', hlsConfig);
var hls = new Hls(hlsConfig);
let mediaErrorCount = 0,
lastErrorTime = null,
errorTimer = setInterval(() => {
if (lastErrorTime) {
let timeSinceError = Date.now() - lastErrorTime;
if (mediaErrorCount > 0 && timeSinceError >= 10000) {
mediaErrorCount--;
}
}
}, 10000);
hls.on(Hls.Events.ERROR, (event, data) => {
console.log('HLS.Events.ERROR: ', event, data, mediaErrorCount);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
// try to recover network error
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
hls.recoverMediaError();
break;
default:
// cannot recover
hls.destroy();
break;
}
//} else if (data.details === 'internalException' && data.type === 'otherError' && isMobile()) {
// this.hlsDropHighestLevel();
} else if (data.type == Hls.ErrorTypes.MEDIA_ERROR) {
//hls.recoverMediaError();
lastErrorTime = Date.now();
mediaErrorCount++;
if (mediaErrorCount > 4 && hls.currentLevel > 0) {
// after 4 media errors, try stepping down to the next lowest level
let newlevel = hls.currentLevel - 1;
console.warn(`Stepping HLS level down from ${hls.currentLevel} to ${newlevel}`, hls);
hls.currentLevel = newlevel;
mediaErrorCount = 0;
}
}
});
hls.loadSource(this.getProxiedURL());
hls.attachMedia(this._video);
this.hls = hls;
if (this.auto_play) {
var video = this._video;
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
}
},
hlsDropHighestLevel() {
if (this._video && this._video.src == '') {
console.log('video stopped, do nothing');
this.hls.destroy();
} else if (this.hls) {
const levels = this.hls.levels;
if (levels && levels.length > 0) {
const level = levels[levels.length - 1];
if (level) {
this.hls.removeLevel(level.level, level.urlId);
}
console.log('HLS load failed, try removing highest res and trying again', level, this.hls, this.hls.levels, this._video, this._video.src);
this.hls.recoverMediaError();
return true;
} else {
console.log('HLS load exhausted all possible sources, giving up', this.hls, this);
return false;
}
}
},
update: function(args) {
if (args) {
for (let k in args) {
this[k] = args[k];
}
if (this._texture && this._texture.image !== this.video) {
this._texture.image = this.video;
}
}
}
}, elation.engine.assets.base);
elation.define('engine.assets.material', {
assettype: 'material',
color: null,
map: null,
normalMap: null,
specularMap: null,
load: function() {
var matargs = {};
if (this.color) matargs.color = new THREE.Color(this.color);
if (this.map) matargs.map = elation.engine.assets.find('image', this.map);
if (this.normalMap) matargs.normalMap = elation.engine.assets.find('image', this.normalMap);
if (this.specularMap) matargs.specularMap = elation.engine.assets.find('image', this.normalMap);
this._material = new THREE.MeshPhongMaterial(matargs);
console.log('new material!', this._material);
},
getInstance: function(args) {
if (!this._material) {
this.load();
}
return this._material;
},
handleLoad: function(data) {
console.log('loaded image', data);
this._texture = data;
},
handleProgress: function(ev) {
},
handleError: function(ev) {
console.log('image uh oh!', ev);
this._texture = false;
}
}, elation.engine.assets.base);
elation.define('engine.assets.sound', {
assettype: 'sound',
src: false,
load: function() {
if (this.src) {
//this._sound = new THREE.Audio(this.src);
}
},
handleLoad: function(data) {
console.log('loaded sound', data);
this._sound = data;
this.loaded = true;
},
handleProgress: function(ev) {
console.log('sound progress!', ev);
this.size = ev.total;
},
handleError: function(ev) {
console.log('sound uh oh!', ev);
this._sound = false;
},
getInstance: function(args) {
return this;
/*
if (!this._sound) {
this.load();
}
return this._sound;
*/
}
}, elation.engine.assets.base);
elation.define('engine.assets.model', {
assettype: 'model',
src: false,
mtl: false,
tex: false,
tex0: false,
tex1: false,
tex2: false,
tex3: false,
tex_linear: true,
color: false,
modeltype: '',
compression: 'none',
object: false,
assetpack: null,
animations: false,
loadworkers: [
],
getInstance: function(args) {
var group = new THREE.Group();
if (!this._model) {
if (!this.loading) {
this.load();
}
var mesh;
if (elation.engine.assets.placeholders.model) {
mesh = elation.engine.assets.placeholders.model.clone();
} else {
mesh = this.createPlaceholder();
}
group.add(mesh);
} else {
//group.add(this._model.clone());
this.fillGroup(group, this._model);
//group = this._model.clone();
this.assignTextures(group, args);
setTimeout(function() {
elation.events.fire({type: 'asset_load', element: group});
elation.events.fire({type: 'asset_load', element: this});
}, 0);
}
this.instances.push(group);
return group;
},
fillGroup: function(group, source, clone=true) {
if (!source) source = this._model;
if (source) {
var newguy = (clone ? THREE.SkeletonUtils.cloneWithAnimations(source, this.animations) : source);
group.add(newguy);
newguy.updateMatrix(true);
newguy.updateMatrixWorld(true);
}
return group;
},
copyMaterial: function(oldmat) {
var m = new THREE.MeshPhongMaterial();
m.anisotropy = elation.config.get('engine.assets.image.anisotropy', 4);
m.name = oldmat.name;
m.map = oldmat.map;
m.normalMap = oldmat.normalMap;
m.lightMap = oldmat.lightMap;
m.color.copy(oldmat.color);
m.transparent = oldmat.transparent;
m.alphaTest = oldmat.alphaTest;
return m;
},
assignTextures: function(group, args) {
return;
var minFilter = (this.tex_linear && this.tex_linear != 'false' ? THREE.LinearMipMapLinearFilter : THREE.NearestFilter);
var magFilter = (this.tex_linear && this.tex_linear != 'false' ? THREE.LinearFilter : THREE.NearestFilter);
if (this.tex) this.tex0 = this.tex;
if (this.tex0) {
var tex0 = elation.engine.assets.find('image', this.te