Pull to refresh

OpenSceneGraph: Граф сцены и умные указатели

Reading time10 min
Views4.4K
image

Введение


В прошлой статье мы рассмотрели сборку OpenSceneGraph из исходников и написали элементарный пример, в котором в пустом фиолетовом мире висит серый самолет. Согласен, не слишком впечатляет. Однако, как я говорил раньше, в этом маленьком примере присутствуют главные концепции, на которых основан данный графический движок. Рассмотрим их подробнее. В ниже приведенном материале использованы иллюстрации из блога Александра Бобкова об OSG (жаль, что автор забросил писать об OSG...). Статья базируется так же на материале и примерах из книги OpenSceneGraph 3.0. Beginner’s Guide

Надо сказать, что предыдущая публикация была подвергнута некоторой критике, с которой я частично соглашусь — материал вышел недосказанным и вырванным из контекста. Постараюсь исправить это упущение под катом.

1. Коротко о графе сцены и его узлах


Центральным понятием движка является так называемый граф сцены (не случайно он затесался в само название фреймворка) — древовидная иерархическая структура, позволяющая организовать логическое и пространственное представление трехмерной сцены. Граф сцены содержит корневой узел и связанные с ним промежуточные и оконечные узлы или ноды.

Например



данный граф изображает сцену, состоящую из дома и находящегося в нем стола. Дом обладает неким геометрическим представлением и определенным образом располагается в пространстве относительно некой базовой системы координат, связанной с корневым узлом (root). Стол тоже описывается некоторой геометрией, расположенной некоторым образом относительно дома, а вместе с домом — относительно корневой ноды. Все ноды, обладая общностью свойств, ибо наследуются от одного класса osg::Node, разделяются на типы по своему функциональному назначению

  1. Групповые ноды (osg::Group) — являются базовым классом для всех промежуточных узлов и предназначены для объединения других узлов в группы
  2. Ноды трансформации (osg::Transform и его наследники) — предназначены для описания трансформации координат объектов
  3. Геометрические ноды (osg::Geode) — оконечные (листовые) узлы графа сцены, содержащие в себе информацию об одном или нескольких геометрических объектах.

Геометрия объектов сцены в OSG описывается в собственной локальной системе координат объекта. Узлы трансформации, расположенные между данным объектом и корневым узлом реализуют матричные преобразования координат для получения положения объекта в базовой системе координат.

Узлы выполняют множество важных функций, в частности хранят состояние отображения объектов, причем данной состояние оказывает воздействие только на подграф, связанный с данным узлом. С узлами графа сцены могут связываться несколько обратных вызовов, обработчики событий, позволяющие изменять состояние узла и связанного с ним подграфа.

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

В рассмотренном в прошлый раз примере наша сцена состояла из единственного объекта — модели самолета, загруженной из файла. Забегая очень далеко вперед, скажу, что эта модель является листовым узлом графа сцены. Она намертво приварена к глобальной базовой системе координат движка.

2. Управление памятью в OSG


Поскольку узлы графа сцены хранят массу данных об объектах сцены и операциях над ними, то для хранения этих данных необходимо выделять память, в том числе и динамически. В этом случае, при манипуляциях с графом сцены, и, например, удалении некоторых его узлов нужно внимательно следить, чтобы удаленные узлы графа более не обрабатывались. Этот процесс всегда сопровождается ошибками, трудоемкой отладкой, так как разработчику довольно трудно отследить какие указатели на объекты ссылаются на существующие данные, а какие должны быть удалены. Без эффективного управления памятью велика вероятность возникновения ошибок сегментации и утечек памяти.

Управление памятью является критически важной задачей в OSG и его концепция базируется на двух тезисах:

  1. Выделение памяти: обеспечение выделения нужного для хранения объекта объема памяти.
  2. Освобождение памяти: Возврат выделенной памяти системе, в тот момент когда в ней нет необходимости.

Многие современные языки программирования, такие как C#, Java, Visual Basic .Net и им подобные, используют так называемый сборщик мусора для освобождения выделенной памяти. Концепция языка C++ не предусматривает подобного подхода, однако мы можем имитировать её путем использования так называемых "умных" указателей.

Это сегодня C++ имеет в своем арсенале умные указатели, что называется «из коробки» (а стандарт C++17 уже успел избавить язык от некоторых устаревших типов умных указателей), но так было не всегда. Самая ранняя из официальных версий OSG за номером 0.9 появилась на свет в 2002 году, и до первого официального релиза было ещё три года. В то время стандарт C++ ещё не предусматривал умных указателей, да и если верить одному историческому экскурсу, сам язык переживал не лучшие времена. Так что появление велосипеда в виде собственных умных указателей, кои и реализованы в OSG совершенно не удивительно. Этот механизм глубоко интегрирован в структуру движка, так что понимать его работу совершенно необходимо с самого начала.

3. Классы osg::ref_ptr<> и osg::Referenced


OSG обеспечивается собственный механизм умных указателей на основе шаблонного класса osg::ref_ptr<>, для реализации автоматической сборки мусора. Для его правильной работы OSG предоставляет ещё один класс osg::Referenced для управления блоками памяти, для которых осуществляется подсчет ссылок на них.

Класс osg::ref_ptr<> предоставляет несколько операторов и методов.

  • get() — публичный метод возвращающий "сырой" указатель, например, при использовании в качестве аргумента шаблона osg::Node данный метод вернет osg::Node*.
  • operator*() — фактически оператор разыменования.
  • operator->() и operator=() — позволяют использовать osg::ref_ptr<> как классический указатель при доступе к методам и свойствам объектов, описываемых данным указателем.
  • operator==(), operator!=() и operator!() — позволяют выполнять над умными указателями операции сравнения.
  • valid() — публичный метод, возвращающий истину, если управляемый указатель имеет корректное значение (не NULL). Выражение some_ptr.valid() эквивалентно выражению some_ptr != NULL, если some_ptr — умный указатель.
  • release() — публичный метод, полезен, когда требуется возвратить управляемый адрес из функции. Про него будет подробнее рассказано позже.

Класс osg::Referenced является базовым классом для всех элементов графа сцены, таких как ноды, геометрия, состояния рендеринга и другие объекты, размещаемые на сцене. Таким образом, создавая корневой узел сцены, мы косвенно наследуем весь функционал, предоставляемый классом osg::Referenced. Поэтому в нашей программе присутствует объявление

osg::ref_ptr<osg::Node> root;

Класс osg::Referenced содержит целочисленный счетчик ссылок на выделенный блок памяти. Этот счетчик инициализируется нулем в конструкторе класса. Он увеличивается на единицу, когда создается объект osg::ref_ptr<>. Этот счетчик уменьшается, как только удаляется какая-либо ссылка на объект, описываемый данным указателем. Объект автоматически уничтожается, когда на него перестают ссылаться какие-либо умные указатели.

Класс osg::Referenced имеет три публичных метода:

  • ref() — публичный метод, увеличивающий на 1 счетчик ссылок.
  • unref() — публичный метода, уменьшающий на 1 счетчик ссылок.
  • referenceCount() — публичный метод, возвращающий текущее значение счетчика ссылок, что бывает полезно при отладке кода.

Эти методы доступны во всех классах, производных от osg::Referenced. Однако, следует помнить о том, что ручное управление счетчиком ссылок может привести к непредсказуемым последствиям, и пользуясь этим следует четко представлять себе что вы делаете.

4. Как в OSG выполняется сборка мусора и зачем она нужна


Существуют несколько причин, по которым следует использовать умные указатели и сборку мусора:

  • Минимизация критических ошибок: использование умных указателей позволяет автоматизировать выделение и освобождение памяти. Отсутствуют опасные "сырые" указатели.
  • Эффективное управление памятью: память, выделенная под объект освобождается сразу, как только объект становится не нужен, что ведет к экономному использованию ресурсов системы.
  • Облегчение отладки приложения: имея возможность четко отслеживать число ссылок на объект, мы имеем возможности для разного рода оптимизаций и экспериментов.

Допустим, что граф сцены состоит из корневой ноды и нескольких уровней дочерних узлов. Если корневая нода и все дочерние ноды управляются с использованием класса osg::ref_ptr<>, то приложение может отслеживать только указатель на корневую ноду. Удаление этой ноды приведет к последовательному, автоматическому удалению всех дочерних узлов.



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

Умные указатели настоятельно рекомендуются разработчиками OSG к использованию в проектах, однако есть несколько принципиальных моментов, на которые следует обратить внимание:

  • Экземпляры osg::Referenced и его производных могут быть созданы исключительно на куче. Они не могут быть созданы на стеке как локальные переменные, так как деструкторы этих классов объявлены как proteced. Например

osg::ref_ptr<osg::Node> node = new osg::Node; // правильно
osg::Node node; // неправильно

  • Можно создавать временные узлы сцены используя и обычные указатели C++, однако такой подход будет небезопасным. Лучше применять умные указатели, гарантирующие корректное управление графом сцены

osg::Node *tmpNode = new osg::Node; // в принципе, будет работать...
osg::ref_ptr<osg::Node> node = tmpNode; // но лучше завершить работу с временным указателем таким образом!

  • Ни в коем случае не стоит использовать в дереве сцены циклических ссылок, когда узел ссылается сам на себя непосредственно или косвенно через несколько уровней



В приведенном примере графа сцены нода Child 1.1 ссылается сама на себя, а так же нода Child 2.2 ссылается на ноду Child 1.2. Такого рода ссылки могут привести к неверному расчету количества ссылок и неопределенному поведению программы.

5. Отслеживание управляемых объектов


Для иллюстрации работы механизма умных указателей в OSG напишем следующий синтетический пример

main.h

#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/ref_ptr>
#include    <osg/Referenced>
#include    <iostream>

#endif // MAIN_H

main.cpp

#include    "main.h"

class MonitoringTarget : public osg::Referenced
{
public:

    MonitoringTarget(int id) : _id(id)
    {
        std::cout << "Constructing target " << _id << std::endl;
    }

protected:

    virtual ~MonitoringTarget()
    {
        std::cout << "Dsetroying target " << _id << std::endl;
    }

    int _id;
};

int main(int argc, char *argv[])
{
    (void) argc;
    (void) argv;

    osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0);
    std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl;
    osg::ref_ptr<MonitoringTarget> anotherTarget = target;
    std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl;

    return 0;
}

Создаем класс-наследник osg::Referenced, не делающий ничего, кроме как в конструкторе и деструкторе сообщающий о том, что создан его экземпляр и выводящий идентификатор, определяемый при создании экземпляра. Создаем экземпляр класса с использованием механизма умных указателей

osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0);

Далее выводим счетчик ссылок на объект target

std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl;

После этого создаем новый умный указатель, присваивая ему значение предыдущего указателя

osg::ref_ptr<MonitoringTarget> anotherTarget = target;

и снова выводим счетчик ссылок

std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl;

Посмотрим, что у нас получилось, проанализировав вывод программы

15:42:39: Отладка запущена
Constructing target 0
Referenced count before referring: 1
Referenced count after referring: 2
Dsetroying target 0
15:42:42: Отладка завершена

При запуске конструктора класса выводится соответствующее сообщение, говорящее нам о том, что память под объект выделена и конструктор нормально отработал. Далее, после создания умного указателя, мы видим, что счетчик ссылок на созданный объект увеличился на единицу. Создание нового указателя, с присвоением ему значения старого указателя — по сути создание новой ссылки на тот же самый объект, поэтому счетчик ссылок увеличивается ещё на единицу. При выходе из программы вызывается деструктор класса MonitoringTarget.



Проведем ещё один эксперимент, дописав в конец функции main() такой код

for (int i = 1; i < 5; i++)
{
	osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i);
}

приводящий к такому "выхлопу" программы

16:04:30: Отладка запущена
Constructing target 0
Referenced count before referring: 1
Referenced count after referring: 2
Constructing target 1
Dsetroying target 1
Constructing target 2
Dsetroying target 2
Constructing target 3
Dsetroying target 3
Constructing target 4
Dsetroying target 4
Dsetroying target 0
16:04:32: Отладка завершена

Мы создаем несколько объектов в теле цикла, используя при этом умный указатель. Так как область видимости указателя распространяется в данном случае только на тело цикла, при выходе из него происходит автоматический вызов деструктора. Этого бы не происходило, совершенно очевидно, используй мы обычные указатели.

С автоматическим освобождением памяти связана другая важная особенность работы с умными указателями. Так как деструктор классов производных от osg::Referenced выполнен защищенным, мы не можем явно вызвать оператор delete для удаления объекта. Единственный способ удалить объект — обнулить количество ссылок на него. Но тогда наш код становится небезопасным при многопоточной обработке данных — мы можем обращаться к уже удаленному объекту из другого потока.

К счастью OSG Обеспечивает решение этой проблемы средствами своего планировщика удаления объектов. Этот планировщик основан на использовании класса osg::DeleteHandler. Он работает так, что не выполняет операцию удаления объекта сразу, а выполняет её через некоторое время. Все объекты, подлежащие удалению, временно запоминаются, пока не наступит момент для из безопасного удаления, и тогда они все разом удаляются. Планировщик удаления osg::DeleteHandler управляется бэкэндом рендера OSG.

6. Возврат из функции


Добавим в код нашего примера следующую функцию

MonitoringTarget *createMonitoringTarget(int id)
{
    osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id);

    return target.release();
}

и заменим вызов оператора new в цикле на вызов этой функции

for (int i = 1; i < 5; i++)
{
    osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i);
}

Вызов release() уменьшит количество ссылок на объект до нуля, но вместо удаления памяти возвращает напрямую фактический указатель на выделенную память. Если этот указатель присваивается другому умному указателю, то утечек памяти не будет.

Выводы


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

  • Предполагается длительное хранение объекта
  • Один объект хранит в себе ссылку на другой объект
  • Необходимо вернуть указатель из функции

Пример кода, приведенный в статье доступен здесь.

Продолжение следует...
Tags:
Hubs:
Total votes 6: ↑6 and ↓0+6
Comments0

Articles