playcanvas
Version:
PlayCanvas WebGL game engine
333 lines (330 loc) • 14 kB
JavaScript
import { ADDRESS_CLAMP_TO_EDGE, PIXELFORMAT_RGBA8, PIXELFORMAT_RGB8, TEXTURETYPE_RGBM, TEXTURETYPE_DEFAULT } from '../../platform/graphics/constants.js';
import { Texture } from '../../platform/graphics/texture.js';
import { Asset } from '../asset/asset.js';
import { ResourceHandler } from './handler.js';
/**
* @import { AppBase } from '../app-base.js'
*/ /**
* Resource handler used for loading cubemap {@link Texture} resources.
*
* @category Graphics
*/ class CubemapHandler extends ResourceHandler {
load(url, callback, asset) {
this.loadAssets(asset, callback);
}
open(url, data, asset) {
// caller will set our return value to asset.resources[0]. We've already set resources[0],
// but we must return it again here so it doesn't get overwritten.
return asset ? asset.resource : null;
}
patch(asset, registry) {
this.loadAssets(asset, (err, result)=>{
if (err) {
// fire error event if patch failed
registry.fire('error', asset);
registry.fire("error:" + asset.id, err, asset);
asset.fire('error', asset);
}
// nothing to do since asset:change would have been raised if
// resources were changed.
});
}
// get the list of dependent asset ids for the cubemap
getAssetIds(cubemapAsset) {
var result = [];
// prefiltered cubemap is stored at index 0
result[0] = cubemapAsset.file;
// faces are stored at index 1..6
if ((cubemapAsset.loadFaces || !cubemapAsset.file) && cubemapAsset.data && cubemapAsset.data.textures) {
for(var i = 0; i < 6; ++i){
result[i + 1] = cubemapAsset.data.textures[i];
}
} else {
result[1] = result[2] = result[3] = result[4] = result[5] = result[6] = null;
}
return result;
}
// test whether two assets ids are the same
compareAssetIds(assetIdA, assetIdB) {
if (assetIdA && assetIdB) {
if (parseInt(assetIdA, 10) === assetIdA || typeof assetIdA === 'string') {
return assetIdA === assetIdB; // id or url
}
// else {
return assetIdA.url === assetIdB.url; // file/url structure with url and filename
}
// else {
return assetIdA !== null === (assetIdB !== null);
}
// update the cubemap resources given a newly loaded set of assets with their corresponding ids
update(cubemapAsset, assetIds, assets) {
var assetData = cubemapAsset.data || {};
var oldAssets = cubemapAsset._handlerState.assets;
var oldResources = cubemapAsset._resources;
var tex, mip, i;
// faces, prelit cubemap 128, 64, 32, 16, 8, 4
var resources = [
null,
null,
null,
null,
null,
null,
null
];
// texture type used for faces and prelit cubemaps are both taken from
// cubemap.data.rgbm
var getType = function getType() {
if (assetData.hasOwnProperty('type')) {
return assetData.type;
}
if (assetData.hasOwnProperty('rgbm')) {
return assetData.rgbm ? TEXTURETYPE_RGBM : TEXTURETYPE_DEFAULT;
}
return null;
};
// handle the prelit data
if (!cubemapAsset.loaded || assets[0] !== oldAssets[0]) {
// prelit asset changed
if (assets[0]) {
tex = assets[0].resource;
if (tex.cubemap) {
for(i = 0; i < 6; ++i){
resources[i + 1] = new Texture(this._device, {
name: cubemapAsset.name + "_prelitCubemap" + (tex.width >> i),
cubemap: true,
// assume prefiltered data has same encoding as the faces asset
type: getType() || tex.type,
width: tex.width >> i,
height: tex.height >> i,
format: tex.format,
levels: [
tex._levels[i]
],
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE,
// generate cubemaps on the top level only
mipmaps: i === 0
});
}
} else {
// prefiltered data is an env atlas
resources[1] = tex;
}
}
} else {
// prelit asset didn't change so keep the existing cubemap resources
resources[1] = oldResources[1] || null;
resources[2] = oldResources[2] || null;
resources[3] = oldResources[3] || null;
resources[4] = oldResources[4] || null;
resources[5] = oldResources[5] || null;
resources[6] = oldResources[6] || null;
}
var faceAssets = assets.slice(1);
if (!cubemapAsset.loaded || !this.cmpArrays(faceAssets, oldAssets.slice(1))) {
// face assets have changed
if (faceAssets.indexOf(null) === -1) {
// extract cubemap level data from face textures
var faceTextures = faceAssets.map((asset)=>{
return asset.resource;
});
var faceLevels = [];
for(mip = 0; mip < faceTextures[0]._levels.length; ++mip){
faceLevels.push(faceTextures.map((faceTexture)=>{
return faceTexture._levels[mip];
}));
}
// Force RGBA8 if we are loading a RGB8 texture due to a bug on M1 Macs Monterey and Chrome not
// rendering the face on right of the cubemap (`faceAssets[0]` and `resources[1]`).
// Using a RGBA8 texture works around the issue https://github.com/playcanvas/engine/issues/4091
var format = faceTextures[0].format;
var _assetData_mipmaps;
var faces = new Texture(this._device, {
name: "" + cubemapAsset.name + "_faces",
cubemap: true,
type: getType() || faceTextures[0].type,
width: faceTextures[0].width,
height: faceTextures[0].height,
format: format === PIXELFORMAT_RGB8 ? PIXELFORMAT_RGBA8 : format,
mipmaps: (_assetData_mipmaps = assetData.mipmaps) != null ? _assetData_mipmaps : true,
levels: faceLevels,
minFilter: assetData.hasOwnProperty('minFilter') ? assetData.minFilter : faceTextures[0].minFilter,
magFilter: assetData.hasOwnProperty('magFilter') ? assetData.magFilter : faceTextures[0].magFilter,
anisotropy: assetData.hasOwnProperty('anisotropy') ? assetData.anisotropy : 1,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
resources[0] = faces;
}
} else {
// no faces changed so keep existing faces cubemap
resources[0] = oldResources[0] || null;
}
// check if any resource changed
if (!this.cmpArrays(resources, oldResources)) {
// set the new resources, change events will fire
cubemapAsset.resources = resources;
cubemapAsset._handlerState.assetIds = assetIds;
cubemapAsset._handlerState.assets = assets;
// destroy the old cubemap resources that are not longer needed
for(i = 0; i < oldResources.length; ++i){
if (oldResources[i] !== null && resources.indexOf(oldResources[i]) === -1) {
oldResources[i].destroy();
}
}
}
// destroy old assets which have been replaced
for(i = 0; i < oldAssets.length; ++i){
if (oldAssets[i] !== null && assets.indexOf(oldAssets[i]) === -1) {
oldAssets[i].unload();
}
}
}
cmpArrays(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
for(var i = 0; i < arr1.length; ++i){
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
// convert string id to int
resolveId(value) {
var valueInt = parseInt(value, 10);
return valueInt === value || valueInt.toString() === value ? valueInt : value;
}
loadAssets(cubemapAsset, callback) {
// initialize asset structures for tracking load requests
if (!cubemapAsset.hasOwnProperty('_handlerState')) {
cubemapAsset._handlerState = {
// the list of requested asset ids in order of [prelit cubemap, 6 faces]
assetIds: [
null,
null,
null,
null,
null,
null,
null
],
// the dependent (loaded, active) texture assets
assets: [
null,
null,
null,
null,
null,
null,
null
]
};
}
var self = this;
var assetIds = self.getAssetIds(cubemapAsset);
var assets = [
null,
null,
null,
null,
null,
null,
null
];
var loadedAssetIds = cubemapAsset._handlerState.assetIds;
var loadedAssets = cubemapAsset._handlerState.assets;
var registry = self._registry;
// one of the dependent assets has finished loading
var awaiting = 7;
var onLoad = function onLoad(index, asset) {
assets[index] = asset;
awaiting--;
if (awaiting === 0) {
// all dependent assets are finished loading, set them as the active resources
self.update(cubemapAsset, assetIds, assets);
callback(null, cubemapAsset.resources);
}
};
// handle an asset load failure
var onError = function onError(index, err, asset) {
callback(err);
};
// process the texture asset
var processTexAsset = function processTexAsset(index, texAsset) {
if (texAsset.loaded) {
// asset already exists
onLoad(index, texAsset);
} else {
// asset is not loaded, register for load and error events
registry.once("load:" + texAsset.id, onLoad.bind(self, index));
registry.once("error:" + texAsset.id, onError.bind(self, index));
if (!texAsset.loading) {
// kick off load if it's not already
registry.load(texAsset);
}
}
};
var texAsset;
for(var i = 0; i < 7; ++i){
var assetId = this.resolveId(assetIds[i]);
if (!assetId) {
// no asset
onLoad(i, null);
} else if (self.compareAssetIds(assetId, loadedAssetIds[i])) {
// asset id hasn't changed from what is currently set
processTexAsset(i, loadedAssets[i]);
} else if (parseInt(assetId, 10) === assetId) {
// assetId is an asset id
texAsset = registry.get(assetId);
if (texAsset) {
processTexAsset(i, texAsset);
} else {
// if we are unable to find the dependent asset, then we introduce here an
// asynchronous step. this gives the caller (for example the scene loader)
// a chance to add the dependent scene texture to registry before we attempt
// to get the asset again.
setTimeout(((index, assetId_)=>{
var texAsset = registry.get(assetId_);
if (texAsset) {
processTexAsset(index, texAsset);
} else {
onError(index, "failed to find dependent cubemap asset=" + assetId_);
}
}).bind(null, i, assetId));
}
} else {
// assetId is a url or file object and we're responsible for creating it
var file = typeof assetId === 'string' ? {
url: assetId,
filename: assetId
} : assetId;
// if the referenced prefiltered texture is not a dds file, then we're loading an
// envAtlas. In this case we must specify the correct texture state.
var data = file.url.search('.dds') === -1 ? {
type: 'rgbp',
addressu: 'clamp',
addressv: 'clamp',
mipmaps: false
} : null;
texAsset = new Asset(cubemapAsset.name + "_part_" + i, 'texture', file, data);
registry.add(texAsset);
processTexAsset(i, texAsset);
}
}
}
/**
* Create a new CubemapHandler instance.
*
* @param {AppBase} app - The running {@link AppBase}.
* @ignore
*/ constructor(app){
super(app, 'cubemap');
this._device = app.graphicsDevice;
this._registry = app.assets;
this._loader = app.loader;
}
}
export { CubemapHandler };