polygonjs-engine
Version:
node-based webgl 3D engine https://polygonjs.com
421 lines (389 loc) • 13.5 kB
text/typescript
import {VideoTexture} from 'three/src/textures/VideoTexture';
import {TextureLoader} from 'three/src/loaders/TextureLoader';
import {Texture} from 'three/src/textures/Texture';
import {UnsignedByteType} from 'three/src/constants';
import {CoreWalker} from '../Walker';
import {BaseNodeType} from '../../engine/nodes/_Base';
import {BaseParamType} from '../../engine/params/_Base';
import {BaseCopNodeClass} from '../../engine/nodes/cop/_Base';
import {TextureContainer} from '../../engine/containers/Texture';
import {Poly} from '../../engine/Poly';
import {ModuleName} from '../../engine/poly/registers/modules/_BaseRegister';
import {CoreUserAgent} from '../UserAgent';
interface VideoSourceTypeByExt {
ogg: string;
ogv: string;
mp4: string;
}
// interface ImageScriptUrlByExt {
// exr: string;
// basis: string;
// }
interface ThreeLoaderByExt {
exr: string;
basis: string;
hdr: string;
}
enum Extension {
EXR = 'exr',
BASIS = 'basis',
HDR = 'hdr',
}
export class CoreTextureLoader {
static PARAM_DEFAULT = '/examples/textures/uv.jpg';
static PARAM_ENV_DEFAULT = '/examples/textures/piz_compressed.exr';
static VIDEO_EXTENSIONS = ['mp4', 'webm', 'ogv'];
static VIDEO_SOURCE_TYPE_BY_EXT: VideoSourceTypeByExt = {
ogg: 'video/ogg; codecs="theora, vorbis"',
ogv: 'video/ogg; codecs="theora, vorbis"',
mp4: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
};
constructor(private _node: BaseNodeType, private _param: BaseParamType) {}
async load_texture_from_url_or_op(url: string): Promise<Texture | VideoTexture | null> {
let texture: Texture | null = null;
let found_node;
if (url.substring(0, 3) == 'op:') {
const node_path = url.substring(3);
found_node = CoreWalker.find_node(this._node, node_path);
if (found_node) {
if (found_node instanceof BaseCopNodeClass) {
const container: TextureContainer = await found_node.requestContainer();
texture = container.texture();
} else {
this._node.states.error.set(`found node is not a texture node`);
}
// this._assign_texture(attrib, texture)
} else {
this._node.states.error.set(`no node found in path '${node_path}'`);
}
} else {
texture = await this.load_url(url);
if (texture) {
// param.mark_as_referencing_asset(url)
if (this._param.options.texture_as_env()) {
// texture = await CoreTextureLoader.set_texture_for_env(texture, this._node);
} else {
texture = CoreTextureLoader.set_texture_for_mapping(texture);
}
} else {
this._node.states.error.set(`could not load texture ${url}`);
}
}
// NOTE: if this._param gets its value from an expression like `ch('/CONTROL/photo_url')`
// then found_node will be null, so the graph should not be changed
if (found_node && this._param.graphPredecessors()[0] != found_node) {
this._param.graphDisconnectPredecessors();
this._param.addGraphInput(found_node);
}
// this._assign_texture(attrib, texture)
return texture;
}
async load_url(url: string): Promise<Texture> {
return new Promise(async (resolve, reject) => {
// url = this._resolve_url(url)
const ext = CoreTextureLoader.get_extension(url);
if (url[0] != 'h') {
const assets_root = this._node.scene().assets.root();
if (assets_root) {
url = `${assets_root}${url}`;
}
}
if (CoreTextureLoader.VIDEO_EXTENSIONS.includes(ext)) {
const texture: VideoTexture = await this._load_as_video(url);
resolve(texture);
} else {
this.loader_for_ext(ext).then(async (loader) => {
if (loader) {
CoreTextureLoader.increment_in_progress_loads_count();
await CoreTextureLoader.wait_for_max_concurrent_loads_queue_freed();
loader.load(
url,
(texture: Texture) => {
CoreTextureLoader.decrement_in_progress_loads_count();
resolve(texture);
},
undefined,
(error: any) => {
CoreTextureLoader.decrement_in_progress_loads_count();
Poly.warn('error', error);
reject();
}
);
} else {
reject();
}
});
}
});
}
static module_names(ext: string): ModuleName[] | void {
switch (ext) {
case Extension.EXR:
return [ModuleName.EXRLoader];
case Extension.HDR:
return [ModuleName.RGBELoader];
case Extension.BASIS:
return [ModuleName.BasisTextureLoader];
}
}
async loader_for_ext(ext: string) {
const ext_lowercase = ext.toLowerCase() as keyof ThreeLoaderByExt;
switch (ext_lowercase) {
case Extension.EXR: {
return await this._exr_loader();
}
case Extension.HDR: {
return await this._hdr_loader();
}
case Extension.BASIS: {
return await this._basis_loader();
}
}
return new TextureLoader();
}
private async _exr_loader() {
const module = await Poly.modulesRegister.module(ModuleName.EXRLoader);
if (module) {
return new module.EXRLoader();
}
}
private async _hdr_loader() {
const module = await Poly.modulesRegister.module(ModuleName.RGBELoader);
if (module) {
const loader = new module.RGBELoader();
loader.setDataType(UnsignedByteType);
return loader;
}
}
private async _basis_loader() {
const module = await Poly.modulesRegister.module(ModuleName.BasisTextureLoader);
if (module) {
const loader = new module.BasisTextureLoader();
loader.setTranscoderPath('/three/js/libs/basis/');
const renderer = await Poly.renderersController.waitForRenderer();
if (renderer) {
loader.detectSupport(renderer);
} else {
Poly.warn('texture loader found no renderer for basis texture loader');
}
return loader;
}
}
_load_as_video(url: string): Promise<VideoTexture> {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
// document.body.appendChild(video)
// video.id = 'video'
video.setAttribute('crossOrigin', 'anonymous');
video.setAttribute('autoplay', `${true}`); // to ensure it loads
video.setAttribute('loop', `${true}`);
// wait for onloadedmetadata to ensure that we have a duration
video.onloadedmetadata = function () {
video.pause();
const texture = new VideoTexture(video);
resolve(texture);
};
// video.setAttribute('controls', true)
// video.style="display:none"
// add source as is
const original_source = document.createElement('source');
const original_ext = CoreTextureLoader.get_extension(url) as keyof VideoSourceTypeByExt;
let type: string = CoreTextureLoader.VIDEO_SOURCE_TYPE_BY_EXT[original_ext];
type = type || CoreTextureLoader._default_video_source_type(url);
original_source.setAttribute('type', type);
original_source.setAttribute('src', url);
video.appendChild(original_source);
// add secondary source, either mp4 or ogv depending on the first url
let secondary_url = url;
if (original_ext == 'mp4') {
// add ogv
secondary_url = CoreTextureLoader.replace_extension(url, 'ogv');
} else {
// add mp4
secondary_url = CoreTextureLoader.replace_extension(url, 'mp4');
}
const secondary_source = document.createElement('source');
const secondary_ext = CoreTextureLoader.get_extension(secondary_url) as keyof VideoSourceTypeByExt;
type = CoreTextureLoader.VIDEO_SOURCE_TYPE_BY_EXT[secondary_ext];
type = type || CoreTextureLoader._default_video_source_type(url);
secondary_source.setAttribute('type', type);
secondary_source.setAttribute('src', url);
video.appendChild(secondary_source);
});
}
static _default_video_source_type(url: string) {
const ext = this.get_extension(url);
return `video/${ext}`;
}
static pixel_data(texture: Texture) {
const img = texture.image;
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
if (context) {
context.drawImage(img, 0, 0, img.width, img.height);
return context.getImageData(0, 0, img.width, img.height);
}
}
// TODO: typescript: check what type the pixel_data is
// static pixel_data_to_attribute(pixel_data: Pixel, geometry: BufferGeometry, attrib_name_with_component:string, convert_method: (x:number, y:number, z:number, w:number)=>number) {
// const {data} = pixel_data;
// const core_geometry = new CoreGeometry(geometry);
// // TODO: add error if no uvs
// const values = [];
// const points = core_geometry.points();
// for (let point of points) {
// const uv = point.attribValue('uv');
// const x = Math.floor((pixel_data.width - 1) * uv.x);
// const y = Math.floor((pixel_data.height - 1) * (1 - uv.y));
// const i = y * pixel_data.width + x;
// // const val = data[4*i] / 255.0;
// if (convert_method) {
// const val = convert_method(data[4 * i + 0], data[4 * i + 1], data[4 * i + 2], data[4 * i + 3]);
// values.push(val);
// } else {
// values.push([data[4 * i + 0], data[4 * i + 1], data[4 * i + 2]]);
// }
// }
// const attrib_name_elements = attrib_name_with_component.split('.');
// let attrib_name = attrib_name_elements[0];
// let component_offset = null;
// if (attrib_name_elements.length > 1) {
// const component = attrib_name_elements[1] as keyof Vector4Like
// component_offset = {x: 0, y: 1, z: 2, w: 3}[component];
// }
// let attrib = geometry.attributes[attrib_name];
// if (attrib) {
// const array = attrib.array;
// let index = 0;
// let is_array = null;
// for (let value of values) {
// if (is_array || CoreType.isArray(value)) {
// is_array = true;
// let component_index = 0;
// for (let value_c of value) {
// array[attrib.itemSize * index + component_index] = value_c;
// component_index++;
// }
// } else {
// array[attrib.itemSize * index + component_offset] = value;
// }
// index++;
// }
// } else {
// attrib = geometry.setAttribute(attrib_name, new Float32BufferAttribute(values, 1));
// }
// attrib.needsUpdate = true;
// }
static get_extension(url: string) {
const elements = url.split('.');
return elements[elements.length - 1].toLowerCase();
}
static replace_extension(url: string, new_extension: string) {
const elements = url.split('?');
const url_without_params = elements[0];
const url_elements = url_without_params.split('.');
url_elements.pop();
url_elements.push(new_extension);
return [url_elements.join('.'), elements[1]].join('?');
}
// static private _resolve_url(url: string):string{
// if(url[0] == '/'){
// const root_url = POLY.env_is_production() ? 'https://polygonjs.com' : 'http://localhost:5000'
// url = `${root_url}${url}`
// }
// return url
// }
static set_texture_for_mapping(texture: Texture) {
// let val = texture['wrapS']
// Object.defineProperty(texture, 'wrapS', {
// get () {
// return val // Simply return the cached value
// },
// set (newVal) {
// val = newVal // Save the newVal
// }
// })
// texture.wrapS = RepeatWrapping
// texture.wrapT = RepeatWrapping
return texture;
}
// static async set_texture_for_env(texture: Texture, registerer: BaseNode) {
// if (registerer._registered_env_map) {
// POLY.renderers_controller.deregister_env_map(registerer._registered_env_map);
// }
// registerer._registered_env_map = await POLY.renderers_controller.register_env_map(texture);
// return registerer._registered_env_map;
// }
//
//
// CONCURRENT LOADS
//
//
private static MAX_CONCURRENT_LOADS_COUNT: number = CoreTextureLoader._init_max_concurrent_loads_count();
private static CONCURRENT_LOADS_DELAY: number = CoreTextureLoader._init_concurrent_loads_delay();
private static in_progress_loads_count: number = 0;
private static _queue: Array<() => void> = [];
private static _init_max_concurrent_loads_count(): number {
return CoreUserAgent.is_chrome() ? 10 : 4;
// const parser = new UAParser();
// const name = parser.getBrowser().name;
// // limit to 4 for non chrome,
// // as firefox was seen hanging trying to load multiple glb files
// // limit to 1 for safari,
// if (name) {
// const loads_count_by_browser: PolyDictionary<number> = {
// Chrome: 10,
// Firefox: 4,
// };
// const loads_count = loads_count_by_browser[name];
// if (loads_count != null) {
// return loads_count;
// }
// }
// return 1;
}
private static _init_concurrent_loads_delay(): number {
return CoreUserAgent.is_chrome() ? 0 : 10;
// const parser = new UAParser();
// const name = parser.getBrowser().name;
// // add a delay for browsers other than Chrome and Firefox
// if (name) {
// const delay_by_browser: PolyDictionary<number> = {
// Chrome: 0,
// Firefox: 10,
// };
// const delay = delay_by_browser[name];
// if (delay != null) {
// return delay;
// }
// }
// return 100;
}
public static override_max_concurrent_loads_count(count: number) {
this.MAX_CONCURRENT_LOADS_COUNT = count;
}
private static increment_in_progress_loads_count() {
this.in_progress_loads_count++;
}
private static decrement_in_progress_loads_count() {
this.in_progress_loads_count--;
const queued_resolve = this._queue.pop();
if (queued_resolve) {
const delay = this.CONCURRENT_LOADS_DELAY;
setTimeout(() => {
queued_resolve();
}, delay);
}
}
private static async wait_for_max_concurrent_loads_queue_freed() {
if (this.in_progress_loads_count <= this.MAX_CONCURRENT_LOADS_COUNT) {
return;
} else {
return new Promise((resolve) => {
this._queue.push(resolve);
});
}
}
}