import type { Observer } from '@playcanvas/observer';
editor.once('plugins:load:asset-texture-lod', () => {
const app = editor.call('viewport:app');
if (!app) {
return;
}
let awaitingFolderCreation;
const slots = [
'aoMap',
'diffuseMap',
'emissiveMap',
'glossMap',
'clearCoatMap',
'clearCoatGlossMap',
'clearCoatNormalMap',
'lightMap',
'metalnessMap',
'opacityMap',
'specularMap',
'normalMap'
];
const convertFilter = function (current: Observer) {
if (!current) {
return false;
}
let items = [current];
if (editor.call('selector:type') === 'asset' && editor.call('selector:items').indexOf(current) !== -1) {
items = editor.call('selector:items');
}
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.get('type') !== 'texture' || item.get('source') === true) {
return false;
}
if (item.get('tags').indexOf('lod-low') !== -1 || item.get('tags').indexOf('lod-mid') !== -1) {
return false;
}
}
return true;
};
editor.call('assets:contextmenu:add', {
text: 'Texture LoD',
icon: 'E201',
items: [
{
text: 'Convert (x2, x4)',
icon: 'E201',
onSelect: function (current: Observer) {
let items = [current];
if (
editor.call('selector:type') === 'asset' &&
editor.call('selector:items').indexOf(current) !== -1
) {
items = editor.call('selector:items');
}
editor.call('plugin:texture-lod:convert', items, [
{
size: 2,
name: 'mid'
},
{
size: 4,
name: 'low'
}
]);
},
onIsEnabled: convertFilter
},
{
text: 'Convert (x4, x8)',
icon: 'E201',
onSelect: function (current: Observer) {
let items = [current];
if (
editor.call('selector:type') === 'asset' &&
editor.call('selector:items').indexOf(current) !== -1
) {
items = editor.call('selector:items');
}
editor.call('plugin:texture-lod:convert', items, [
{
size: 4,
name: 'mid'
},
{
size: 8,
name: 'low'
}
]);
},
onIsEnabled: convertFilter
},
{
text: 'Swap Textures',
items: [
{
text: 'Original',
value: 'texture-lod-original',
onSelect: function () {
editor.call('plugin:texture-lod:switch', 'original');
}
},
{
text: 'Medium',
onSelect: function () {
editor.call('plugin:texture-lod:switch', 'medium');
}
},
{
text: 'Low',
onSelect: function () {
editor.call('plugin:texture-lod:switch', 'low');
}
}
]
}
]
});
editor.method('plugin:texture-lod:switch', (quality) => {
if (quality !== 'original' && quality !== 'medium' && quality !== 'low') {
return;
}
const assets = editor.call('assets:list');
const index = {};
const indexLinks = {};
const assetsLow = [];
const assetsMid = [];
const assetsHi = [];
for (let i = 0; i < assets.length; i++) {
const asset = assets[i];
if (asset.get('type') !== 'texture' || asset.get('source')) {
continue;
}
const tags = asset.get('tags');
let list = null;
if (tags.indexOf('lod') !== -1) {
list = assetsHi;
} else if (tags.indexOf('lod-mid') !== -1) {
list = assetsMid;
} else if (tags.indexOf('lod-low') !== -1) {
list = assetsLow;
}
if (!list) {
continue;
}
list.push(asset);
index[asset.get('id')] = asset;
}
const assetsLod = assetsMid.concat(assetsLow);
for (let i = 0; i < assetsLod.length; i++) {
const assetLod = assetsLod[i];
const tags = assetLod.get('tags');
let level = '';
if (tags.indexOf('lod-mid') !== -1) {
level = 'medium';
} else if (tags.indexOf('lod-low') !== -1) {
level = 'low';
}
if (!level) {
continue;
}
for (let i = 0; i < tags.length; i++) {
const tag = tags[i];
if (!tag.startsWith('source-')) {
continue;
}
const sourceId = parseInt(tag.slice(7), 10);
if (!sourceId || !index[sourceId]) {
continue;
}
indexLinks[assetLod.get('id')] = index[sourceId];
indexLinks[sourceId + level] = assetLod;
}
}
const usedIndex = editor.call('assets:used:index');
if (quality === 'original') {
for (let i = 0; i < assetsLod.length; i++) {
const asset = assetsLod[i];
const used = usedIndex[asset.get('id')];
if (!used || !used.parent) {
continue;
}
for (const id in used.ref) {
if (used.ref[id].type !== 'asset') {
continue;
}
const assetRef = editor.call('assets:get', id);
if (!assetRef || assetRef.get('type') !== 'material') {
continue;
}
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (parseInt(assetRef.get(`data.${slot}`), 10) !== parseInt(asset.get('id'), 10)) {
continue;
}
const assetOriginal = indexLinks[asset.get('id')];
if (!assetOriginal) {
continue;
}
assetRef.set(`data.${slot}`, parseInt(assetOriginal.get('id'), 10));
}
}
}
} else {
let list;
if (quality === 'medium') {
list = assetsHi.concat(assetsLow);
} else {
list = assetsHi.concat(assetsMid);
}
for (let i = 0; i < list.length; i++) {
const asset = list[i];
const used = usedIndex[asset.get('id')];
if (!used || !used.parent) {
continue;
}
for (const id in used.ref) {
if (used.ref[id].type !== 'asset') {
continue;
}
const assetRef = editor.call('assets:get', id);
if (!assetRef || assetRef.get('type') !== 'material') {
continue;
}
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (parseInt(assetRef.get(`data.${slot}`), 10) !== parseInt(asset.get('id'), 10)) {
continue;
}
let assetOther;
if (indexLinks[asset.get('id')]) {
const original = indexLinks[asset.get('id')];
if (original) {
assetOther = indexLinks[original.get('id') + quality];
}
} else {
assetOther = indexLinks[asset.get('id') + quality];
}
if (!assetOther) {
continue;
}
assetRef.set(`data.${slot}`, parseInt(assetOther.get('id'), 10));
}
}
}
}
});
editor.method('plugin:texture-lod:convert', (items, sizes) => {
if (!items) {
if (editor.call('selector:type') !== 'asset') {
return;
}
items = editor.call('selector:items');
}
items.forEach((source) => {
if (source.get('status') === 'running' || source.get('type') !== 'texture') {
return;
}
if (source.get('tags').indexOf('lod-low') !== -1 || source.get('tags').indexOf('lod-mid') !== -1) {
return;
}
if (
!source.get('meta.width') ||
source.get('meta.width') < 128 ||
!source.get('meta.height') ||
source.get('meta.height') < 128
) {
return;
}
if (source.get('tags').indexOf('lod') === -1) {
source.insert('tags', 'lod');
}
source.set('preload', false);
let tasksDone = 0;
const path = source.get('path');
let name = source.get('name');
let ext = '';
const match = name.match(/\.[a-z]{1,4}$/);
if (match) {
ext = name.slice(match.index + 1);
name = name.slice(0, match.index);
}
if (!awaitingFolderCreation) {
awaitingFolderCreation = {};
}
if (!sizes) {
sizes = [
{
size: 2,
name: 'mid'
},
{
size: 4,
name: 'low'
}
];
}
const uiItem = editor.call('assets:panel:get', source.get('id'));
if (uiItem) {
uiItem.class.add('task', 'running');
}
sizes.forEach((options) => {
const folder = editor.call('assets:findOne', (asset) => {
return (
asset.get('type') === 'folder' &&
asset.get('name') === options.name &&
asset.get('path').equals(path)
);
});
const onFolderAvailable = function (folderId: number) {
let target = editor.call('assets:findOne', (asset) => {
const path = asset.get('path');
if (!path.length) {
return false;
}
return (
asset.get('name') === `${name}-${options.name}.${ext}` &&
parseInt(asset.get('source_asset_id'), 10) === parseInt(source.get('id'), 10) &&
path[path.length - 1] === folderId
);
});
if (target) {
target = target[1];
}
let evtTargetAvailable;
const onTargetAvailable = function (target: Observer) {
if (evtTargetAvailable) {
evtTargetAvailable.unbind();
evtTargetAvailable = null;
}
const task = {
source: parseInt(source.get('uniqueId'), 10),
target: parseInt(target.get('uniqueId'), 10),
filename: target.get('name'),
options: editor.call('assets:jobs:texture-convert-options', source.get('meta'))
};
task.options.size = {
width: Math.max(8, source.get('meta.width') / options.size),
height: Math.max(8, source.get('meta.height') / options.size)
};
editor.call('realtime:send', 'pipeline', {
name: 'convert',
data: task
});
editor.call('status:text', `texture-lod: converting '${name}-${options.name}.${ext}' asset`);
const onFileSet = function (value: unknown) {
if (!value) {
return;
}
target.off('file:set', onFileSet);
setTimeout(() => {
editor.call('assets:jobs:thumbnails', null, target);
if (++tasksDone === 2) {
editor.call('status:text', `texture-lod: finished '${name}.${ext}'`);
if (uiItem && source.get('task') === null) {
uiItem.class.remove('task', 'running');
}
}
}, 0);
};
target.on('file:set', onFileSet);
};
if (target) {
target.set('meta.compress', source.get('meta.compress'));
target.set('data', source.get('data'));
setTimeout(() => {
onTargetAvailable(target);
}, 500);
} else {
const assetNew = {
name: `${name}-${options.name}.${ext}`,
type: 'texture',
source: false,
source_asset_id: parseInt(source.get('id'), 10),
preload: false,
data: source.get('data'),
tags: [`source-${source.get('id').toString()}`, `lod-${options.name}`],
region: source.get('region'),
parent: folderId,
scope: source.get('scope'),
meta: source.get('meta')
};
editor.call(
'assets:create',
assetNew,
(err, id) => {
if (err) {
editor.call('status:error', `texture-lod: error creating asset (${err.message})`);
console.log(err.stack);
return;
}
target = editor.call('assets:get', id);
editor.call(
'status:text',
`texture-lod: created '${name}-${options.name}.${ext}' asset`
);
if (target) {
onTargetAvailable(target);
} else {
evtTargetAvailable = editor.once(`assets:add[${id}]`, onTargetAvailable);
}
},
true
);
}
};
if (folder) {
onFolderAvailable(parseInt(folder[1].get('id'), 10));
} else {
const parentFolder = path.length ? path[path.length - 1] : null;
const evtName = `${parentFolder}-${options.name}`;
if (awaitingFolderCreation[evtName]) {
editor.once(`plugin:texture-lod:folder:create:${evtName}`, (id) => {
onFolderAvailable(parseInt(id, 10));
});
} else {
awaitingFolderCreation[evtName] = true;
const folderNew = {
name: options.name,
type: 'folder',
source: true,
preload: false,
data: null,
parent: parentFolder,
scope: source.get('scope')
};
editor.call(
'assets:create',
folderNew,
(err, id) => {
if (err) {
editor.call('status:error', `texture-lod: error creating folder (${err.message})`);
console.log(err.stack);
return;
}
onFolderAvailable(parseInt(id, 10));
editor.emit(`plugin:texture-lod:folder:create:${evtName}`, id);
delete awaitingFolderCreation[evtName];
},
true
);
}
}
});
});
});
});