Введение


Недавно писал о своем опыте очистки памяти в приложении, с использованием three.js. Напомню, что целью была перерисовка нескольких сцен с подгрузкой gltf моделей.

С тех пор я провел ряд экспериментов и считаю необходимым дополнить сказанное ранее этой небольшой статьей. Вот некоторые моменты, которые помогли мне улучшить производительность приложения.

Основная часть


Изучая различные примеры сборки мусора на three.js заинтересовал подход, предложенный на threejsfundamentals.org. Однако, реализовав предложенную конфигурацию и завернув в this.track() все материалы и геометрию, выяснилось, что при загрузке новых сцен нагрузка на GPU продолжает расти. Более того, предложенный пример некорректно работает с EffectComposer и другими классами для постобработки, поскольку в этих классах track() использовать нельзя.

Решение с добавлением ResourceTracker во все используемые классы не привлекает, по очевидным причинам, поэтому решил дополнить метод очистки упомянутого класса. Вот некоторые приемы, которые были использованы:

Прием 1. Грубый


Добавляем renderer.info после метода очистки. Поочередно убираем ресурсы из приложения, чтобы понять, какие из них составляют нагрузку и прячутся в текстурах или материалах. Это не способ решение проблем, а просто способ отладки о котором кто-то мог не знать.

Прием 2. Долгий


Открыв код используемого класса (например AfterimagePass, который можно найти на гитхабе three.js) смотрим, где создаются ресурсы, которые нам нужно очищать, чтобы поддерживать число геометрий и материалов в требуемых рамках.

this.textureComp = new WebGLRenderTarget( window.innerWidth, window.innerHeight, { ... }

То что надо. Согласно документации, WebGLRenderTarget имеет функцию dispose, на которой завязана очистка памяти. Получаем что-то вроде:

class Scene {
//...
    postprocessing_init(){ // В нашем классе
        this.afterimagePass = new AfterimagePass(0);
        this.composer.addPass(this.afterimagePass);
    }
//...
}
//...

class ResourceTracker {
//...
    dispose() {
    //...
    sceneObject.afterimagePass.WebGLRenderTarget.dispose();
    //...
    }
}

Прием 3


Работает, но код для очистки в таком случае раздувается. Попробуем использовать знакомый нам из предыдущей статьи подход. Напомню, в ней мы реализовали метод disposeNode(node), в котором ресурс перебирался на поиск того, что можно очистить. disposeNode() может выглядеть как-то так:

disposeNode(node) {
            node.parent = undefined;
            if (node.geometry) {
                node.geometry.dispose();
            }
            let material = node.material;
            if (material) {
                if (material.map) {
                    material.map.dispose();
                }
                if (material.lightMap) {
                    material.lightMap.dispose();
                }
                if (material.bumpMap) {
                    material.bumpMap.dispose();
                }
                if (material.normalMap) {
                    material.normalMap.dispose();
                }
                if (material.specularMap) {
                    material.specularMap.dispose();
                }
                if (material.envMap) {
                    material.envMap.dispose();
                }
                material.dispose();
            }
        } else if (node.constructor.name === "Object3D") {
            node.parent.remove(node);
            node.parent = undefined;
        }
    }

Отлично, теперь возьмем все дополнительные классы, которые мы применяли, и дополним наш ResourceTracker:

dispose() {
    for (let key in sceneObject.afterimagePass) {
        this.disposeNode(sceneObject.afterimagePass[key]);
    }
    for (let key in sceneObject.bloomPass) {
        this.disposeNode(sceneObject.bloomPass[key]);
    }
    for (let key in sceneObject.composer) {
        this.disposeNode(sceneObject.composer[key]);
    }
}

Итоги


В результате всех этих действий я значительно повысил ФПС и уменьшил нагрузку GPU в своем приложении. Возможно, я некорректно применял ResourceTracker, однако он в любом случае не помог бы в работе с дополнительными классами. Про то, что перебор EffectComposer через наш disposeNode(node) влияет на число текстур, оказывающихся в памяти я нигде не видел (однако так оно и есть). Этот вопрос следует рассмотреть отдельно.

Для сравнения предыдущая версия останется по старому адресу, а новую можно будет посмотреть отдельно.

Проект в некотором виде есть на гитхабе.

Буду рад услышать ваш опыт по работе с аналогичными проектами и обсудить детали!