Pull to refresh

OpenSceneGraph: Инкапсуляция машины состояний OpenGL

Reading time14 min
Views2.8K
image

Введение


Как правило, работая с параметрами рендеринга, OpenGL действует как конечный автомат. Состояние рендеринга — это совокупность атрибутов состояния, таких как источники света, материалы, текстуры и режимы отображения, включаемые и выключаемые функциями glEnable() и glDisable(). При установке определенного состояния оно действует до тех пор, пока какая-либо другая функция не изменит его. Конвейер OpenGL поддерживает стек состояний для сохранения и восстановления состояний в любой момент времени. Машина состояний предоставляет разработчику полный контроль над текущими и сохраненными в стеке состояниями рендеринга.

Однако такой подход неудобен при работе с OSG. По этой причине машина состояний OpenGL инкапсулируется классом osg::StateSet, который берет на себя операции по работе со стеком состояний и их установке в процессе обхода графа сцены.

Экземпляр класса osg::StateSet содержит подмножество различных состояний рендеринга и может применять их к узлам сцены osg::Node и геометрическим объектам osg::Drawable с помощью метода setStateSet()

osg::StateSet *stateset = new osg::StateSet;
node->setStateSet(stateset);

Более безопасным способом будет использование метода getOrCreateStateSet(), который гарантирует возврат корректного состояния и присоединение его к узлу или drawable объекту

osg::StateSet *stateset = node->getOrCreateStateSet();

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

1. Атрибуты и режимы


В OSG определен класс osg::StateAttribute для хранения атрибутов рендеринга. Это виртуальный базовый класс, который наследуется различными атрибутами рендеринга, такими как свет, материал и туман.

Режимы рендеринга работаю как переключатели, которые можно включать и выключать. Кроме того, с ними связаны перечислители, которые используют для указания типа режима OpenGL. Иногда режим рендеринга ассоциируется с атрибутом, например режим GL_LIGHTING включает переменные для источников света, отправляемые в конвейер OpenGL когда включен, и выключает освещение в противном случае.

Класс osg::StateSet делит атрибуты и режимы на две групы: текстурные и не текстурные. Он имеет несколько публичных методов для добавления не текстурных атрибутов и режимов в набор состояний:

  1. setAttribute() — добавляет объект типа osg::StateAttribute к набору состояний. Атрибуты одного типа не могут сосуществовать в одном наборе состояний. Предыдущее заданное значение будет перезаписано новым.
  2. setMode() — прикрепляет перечислитель режима к набору состояний и устанавливает его значение в osg::StateAttribute::ON или в osg::StateAttribute::OFF, что обозначает включение или отключение режима.
  3. setAttributeAndModes() — прикрепляет атрибут рендеринга и ассоциированный с ним режим и задает значение переключателя (по-умолчанию ON). Следует иметь в виду, что не каждый атрибут имеет соответствующий режим, но вы можете использовать этот метод в любом случае.

Для установки атрибута и ассоциированного с ним режима можно использовать такой код

stateset->setAttributeAndModes(attr, osg::StateAttribute::ON);

Для установки текстурных атрибутов необходимо передавать дополнительный параметр, чтобы указать текстуру, к которой он должен быть применен. Для этого osg::StateSet предоставляет несколько других публичных методов, таких как setTextureAttribute(), setTextureMode() и setTextureAttributeAndModes()

stateset->setTextureAttributeAndModes(0, textattr, osg::StateAttribute::ON);

применяет атрибут textattr к текстуре с идентификатором 0.

2. Задание режима отображения полигонов для узлов сцены


Проиллюстрируем вышеописанную теорию практическим примером — изменением режима растеризации полигонов OpenGL, используя класс osg::PolygonMode, наследующий от osg::StateAttribute. Этот класс инкапсулирует функцию glPolygonMode() и предоставляет интерфейс для установки режима отображения полигонов для конкретного узла сцены.

Пример polygonmode
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/PolygonMode>
#include    <osg/MatrixTransform>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

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

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");

    osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
    transform1->setMatrix(osg::Matrix::translate(-25.0f, 0.0f, 0.0f));
    transform1->addChild(model.get());

    osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform;
    transform2->setMatrix(osg::Matrix::translate(25.0f, 0.0f, 0.0f));
    transform2->addChild(model.get());

    osg::ref_ptr<osg::PolygonMode> pm = new osg::PolygonMode;
    pm->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE);
    transform1->getOrCreateStateSet()->setAttribute(pm.get());

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(transform1.get());
    root->addChild(transform2.get());

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


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

osg::ref_ptr<osg::PolygonMode> pm = new osg::PolygonMode;
pm->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE);
transform1->getOrCreateStateSet()->setAttribute(pm.get());



Если обратится к спецификации OpenGL, то можно легко представить себе какие параметры отображения полигонов будут доступны нам при использовании setMode() в данном конкретном случае. Первый параметр может принимать значения osg::PolygonMode::FRONT, BACK и FRONT_AND_BACK, соответствующие перечислителям OpenGL GL_FRONT, GL_BACK, GL_FRONT_AND_BACK. Второй параметр может принимать значения osg::PolygonMode::POINT, LINE и FILL, которые соответствуют GL_POINT, GL_LINE и GL_FILL. Никаких других трюков, как это часто бывает при разработке на чистом OpenGL тут применять не нужно — OSG берет на себя большую часть работы. Режим отображения полигонов не имеет связанного режима и не требует вызова пары glEnable()/glDisable(). Метод setAttributeAndModes() будет прекрасно работать и в данном случае, но значение его третьего параметра будет при этом бесполезным.

3. Наследование состояний рендеринга. Применение атрибутов и режимов


Набор состояний узла влияет на текущий узел и все его дочерние элементы. Например, атрибут osg::PolygonMode, установленный для узла transform1 из предыдущего примера будет применен ко всем дочерним элементам этого узла. Однако, дочерний узел может переопределять родительские атрибуты, то есть состояние рендеринга будет наследоваться от родительского узла, если дочерний узел не изменит поведение.

Иногда требуется переопределить поведение узла в части применения атрибутов. Например, в большинстве 3D-редакторов пользователь может загрузить несколько моделей и менять режим их отображения для всех загруженных моделей одновременно, независимо от того, каким образом они отображались ранее. Другими словами, все модели в редакторе должны наследовать единый атрибут независимо от того, как его задавали раньше для каждой из моделей. В OSG это может быть реализовано с помощью флага osg::StateAttribute::OVERRIDE, например

stateset->StateAttribute(attr, osg::StateAttribute::OVERRIDE);

При установке режимов и режимов с атрибутами используются оператор побитового ИЛИ

stateset->StateAttributeAndModes(attr, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);

Кроме того, возможна и защита атрибута от переопределения — для этого он должен быть помечен флагом osg::StateAttribute::PROTECTED.

Имеется и третий флаг, osg::StateAttribute::INHERIT, который используется для того, чтобы отметить, что данный атрибут должен наследоваться из набора состояний родительского узла.

Приведем короткий пример использование флагов OVERRIDE и PROTECTED. Корневой узел будет установлен в OVERRIDE, чтобы заставить все дочерние узлы наследовать его атрибуты и режимы. При этом дочерние узлы будут пытаться изменить свое состояние с помощью или без помощи флага PROTECTED, что будет приводить к различным результатам.

Текст примера inherit
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/PolygonMode>
#include    <osg/MatrixTransform>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

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

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/glider.osg");

    osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
    transform1->setMatrix(osg::Matrix::translate(-0.5f, 0.0f, 0.0f));
    transform1->addChild(model.get());

    osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform;
    transform2->setMatrix(osg::Matrix::translate(0.5f, 0.0f, 0.0f));
    transform2->addChild(model.get());

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(transform1.get());
    root->addChild(transform2.get());

    transform1->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    transform2->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
    root->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}




Чтобы понять, что вообще происходит, необходимо посмотреть как выглядит нормально освещенный дельтаплан, загрузив его штатный просмотрщик OSG osgviewer

$ osgviewer glider.osg

В примере мы пытаемся поменять режим освещения для узлов transform1 и transform2, отключив напрочь освещение.

transform1->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
transform2->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);

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

transform2->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);

В итоге, несмотря на то, что мы выключаем освещение у узла transform1 левый дельтаплан по-прежнему освещен, так как настройки корня сцены перекрыли нашу попытку выключить освещение для него. Правый дельтаплан отображается без освещения (он выглядит ярче только потому, что залит простым цветом без просчета освещенности), так как transform2 защищен от наследования атрибутов корневого узла.

4. Перечень атрибутов OpenGL, поддерживаемых в OpenSceneGraph


OSG поддерживает почти все атрибуты и режимы рендеринга, поддерживаемые OpenGL, через классы, производные от osg::StateAttribute. В таблице представлены все параметры машины состояния OpenGL, доступные из движка.
ID типа атрибута Имя класса Ассоциированный режим Эквивалентная функция OpenGL
ALPHEFUNC osg::AlphaFunc GL_ALPHA_TEST glAlphaFunc()
BLENDFUNC osg::BlendFunc GL_BLEND glBlendFunc() и glBlendFuncSeparate()
CLIPPLANE osg::ClipPlane GL_CLIP_PLANEi (i от 1 до 5) glClipPlane()
COLORMASK osg::ColorMask glColorMask()
CULLFACE osg::CullFace GL_CULLFACE glCullFace()
DEPTH osg::Depth GL_DEPTH_TEST glDepthFunc(), glDepthRange() и glDepthMask()
FOG osg::Fog GL_FOG glFog()
FRONTFACE osg::FrontFace glFrontFace()
LIGHT osg::Light GL_LIGHTi (i от 1 до 7) glLight()
LIGHTMODEL osg::LightModel glLightModel()
LINESTRIPPLE osg::LineStripple GL_LINE_STRIPPLE glLineStripple()
LINEWIDTH osg::LineWidth glLineWidht()
LOGICOP osg::LogicOp GL_COLOR_LOGIC_OP glLogicOp()
MATERIAL osg::Material glMaterial() и glColorMaterial()
POINT osg::Point GL_POINT_SMOOTH glPointParameter()
POINTSPRITE osg::PointSprite GL_POINT_SPRITE_ARB Функции для работы со спрайтами OpenGL
POLYGONMODE osg::PolygonMode glPolygonMode()
POLYGONOFFSET osg::PolygonOffset GL_POLYGON_OFFSET_POINT glPolygonOffset()
POLYGONSTRIPPLE osg::PolygonStripple GL_POLYGON_STRIPPLE glPolygonStripple()
SCISSOR osg::Scissor GL_SCISSOR_TEST glScissor()
SHADEMODEL osg::ShadeModel glShadeModel()
STENCIL osg::Stencil GL_STENCIL_TEST glStencilFunc(), glStencilOp() и glStencilMask()
TEXENV osg::TexEnv glTexEnv()
TEXGEN osg::TexGen GL_TEXTURE_GEN_S glTexGen()

Колонка ID типа атрибута указывает на специфический идентификатор OSG, которым обозначается данный атрибут в перечислителях класса osg::StateAttribute. Он может быть использован в методе getAttribute, для получения значения конкретного атрибута

osg::PolygonMode *pm = dynamic_cast<osg::PolygonMode *>(stateset->getAttribute(osg::StateAttribute::POLYGONMODE));

Валидный указатель указывает на то, что атрибут был установлен ранее. В противном случае метод вренет NULL. Получить значение текущего режима можно также, использовав вызов

osg::StateAttribute::GLModeValue value = stateset->getMode(GL_LIGHTING);

Здесь перечислитель GL_LIGHTING используется для включения/отключения освещения на всей сцене.

5. Применение тумана к модели в сцене


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

Текст примера fog
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Fog>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif


main.cpp


#include	"main.h"

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

    osg::ref_ptr<osg::Fog> fog = new osg::Fog;
    fog->setMode(osg::Fog::LINEAR);
    fog->setStart(500.0f);
    fog->setEnd(2500.0f);
    fog->setColor(osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f));

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/lz.osg");
    model->getOrCreateStateSet()->setAttributeAndModes(fog.get());
    
    osgViewer::Viewer viewer;
    viewer.setSceneData(model.get());

    return viewer.run();
}


Первым делом создаем атрибут тумана. Используем линейную модель, настраиваем диапазон отображения тумана по дальности до модели


osg::ref_ptr<osg::Fog> fog = new osg::Fog;
fog->setMode(osg::Fog::LINEAR);
fog->setStart(500.0f);
fog->setEnd(2500.0f);
fog->setColor(osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f));

Загружаем образец ландшафта lz.osg и применяем к нему данный атрибут

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/lz.osg");
model->getOrCreateStateSet()->setAttributeAndModes(fog.get());

В окне вьювера видим затуманенный ландшафт, и можем посмотреть как изменяется густота тумана в зависимости от расстояния до модели







6. Работа с источниками света и оcвещением


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

Класс osg::Light предоставляет методы для управления источниками света, включающие: setLightNum() и getLightNum() — для работы с количеством источников; setAmbient() и getAmbient() для управления окружающей компонентой; setDiffuse() и getDiffuse() — для работы с рассеянной компонентой и т.д.

Кроме того, OSG описывает класс osg::LightSource для добавления источников света в сцену. Он предоставляет метод setLight() и является листовым узлом графа сцены с единственным атрибутом. Все остальные узлы графа сцены испытывают на себе влияние источника света если установлен соответствующие режим для GL_LIGHTi. Например:

// Источник света 1
osg::ref_ptr<osg::Light> light = new osg::Light;
light->setLightNum( 1 ); 
...
// Создаем источник света как узле сцены
osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource;
lightSource->setLight( light.get() ); 
...
// Добавляем созданный узел в корневой узел сцены и устанавливаем режим для него
root->addChild( lightSource.get() );
root->getOrCreateStateSet()->setMode( GL_LIGHT1,
osg::StateAttribute::ON );


Другим, более удобным решением является метод setStateSetModes(), с помощью которого источник света с нужным номером автоматически присоединяется к корневому узлу

root->addChild( lightSource.get() );
lightSource->setStateSetModes( root->getOrCreateStateSet(), osg::StateAttribute::ON );

Можно добавить дочерние узлы к источнику света, но это совершенно не означает, вы будете освещать связанный с ним подграф как-то по особому. Он будет обрабатываться как геометрия, представленная физической формой источника света.

Узел osg::LightSource может быть прикреплен к узлу трансформации, и, например точечный источник света можно будет перемещать в пространстве. Это можно отключить, установив для источника света абсолютную систему координат

lightSource->setReferenceFrame( osg::LightSource::ABSOLUTE_RF );

7. Создание источников света в сцене


По-умолчанию OSG автоматически настраивает источник света с номером 0, излучающий на сцену равномерный направленный свет. Однако, в любой момент можно добавить несколько дополнительных источников света, да ещё и управлять ими, применяя узлы трансформации координат. Перемещению можно подвергать только позиционные источники (точечные). Направленный свет имеет только направление (поток параллельных лучей идущих из бесконечности) и не имеет привязки к конкретному положению на сцене. OpenGL и OSG импользуют четвертый компонент параметра положения чтобы задать тип источника света. Если он равен 0, то свет рассматривается как направленный; при значении равном 1 — позиционный.

Рассмотрим небольшой пример работы с освещением.

Заголовок спойлера
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/MatrixTransform>
#include    <osg/LightSource>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp


#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
osg::Node *createLightSource(int num,
                             const osg::Vec3 &trans,
                             const osg::Vec4 &color)
{
    osg::ref_ptr<osg::Light> light = new osg::Light;
    light->setLightNum(num);
    light->setDiffuse(color);
    light->setPosition(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));

    osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource;
    lightSource->setLight(light);

    osg::ref_ptr<osg::MatrixTransform> sourceTrans = new osg::MatrixTransform;
    sourceTrans->setMatrix(osg::Matrix::translate(trans));
    sourceTrans->addChild(lightSource.get());

    return sourceTrans.release();
}

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

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(model.get());

    osg::Node *light0 = createLightSource(0, osg::Vec3(-20.0f, 0.0f, 0.0f),
                                          osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f));

    osg::Node *light1 = createLightSource(1, osg::Vec3(0.0f, -20.0f, 0.0f),
                                          osg::Vec4(0.0f, 1.0f, 1.0f, 1.0f));

    root->getOrCreateStateSet()->setMode(GL_LIGHT0, osg::StateAttribute::ON);
    root->getOrCreateStateSet()->setMode(GL_LIGHT1, osg::StateAttribute::ON);

    root->addChild(light0);
    root->addChild(light1);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


Для создания источника света заведем отдельную функцию

osg::Node *createLightSource(int num,
                             const osg::Vec3 &trans,
                             const osg::Vec4 &color)
{
    osg::ref_ptr<osg::Light> light = new osg::Light;
    light->setLightNum(num);
    light->setDiffuse(color);
    light->setPosition(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));

    osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource;
    lightSource->setLight(light);

    osg::ref_ptr<osg::MatrixTransform> sourceTrans = new osg::MatrixTransform;
    sourceTrans->setMatrix(osg::Matrix::translate(trans));
    sourceTrans->addChild(lightSource.get());

    return sourceTrans.release();
}

В этой функции мы сперва определяем параметры освещения, даваемые источником, создавая тем самым атрибут GL_LIGHTi

osg::ref_ptr<osg::Light> light = new osg::Light;
// Номер источника света
light->setLightNum(num);
// Цвет источника
light->setDiffuse(color);
// Положение источника. Последний компонент указывает на то, что источник точечный
light->setPosition(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));

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

osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource;
lightSource->setLight(light);

Создаем и настраиваем узел трансформации, передавая ему наш источник света в качестве дочернего узла

osg::ref_ptr<osg::MatrixTransform> sourceTrans = new osg::MatrixTransform;
sourceTrans->setMatrix(osg::Matrix::translate(trans));
sourceTrans->addChild(lightSource.get());

Возвращаем указатель на узел трансформации

return sourceTrans.release();

В теле основной программы мы загружаем трехмерную модель (снова наша любимая цессна)

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");

osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(model.get());

Создаем два источника света с номерами 0 и 1. Первый будет светить желтым светом, второй — сине-зеленым

osg::Node *light0 = createLightSource(0, osg::Vec3(-20.0f, 0.0f, 0.0f),
                                          osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f));

osg::Node *light1 = createLightSource(1, osg::Vec3(0.0f, -20.0f, 0.0f),
                                      osg::Vec4(0.0f, 1.0f, 1.0f, 1.0f));

Сообщаем машине состояний OpenGL что необходимо включить 0 и 1 источники света и добавяем созданные нами источники в сцену

root->getOrCreateStateSet()->setMode(GL_LIGHT0, osg::StateAttribute::ON);
root->getOrCreateStateSet()->setMode(GL_LIGHT1, osg::StateAttribute::ON);

root->addChild(light0);
root->addChild(light1);

После инициализации и запуска вьювера получаем картинку



Заключение


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

Сегодня я постарался рассмотреть, опять таки, довольно базовые вещи движка OSG. Не уверен, что вышло круто. Но пока я излагаю именно примитивные вещи, в том ключе, в котором понял их сам. Все примеры проверил лично, мой репозиторий доступен здесь. Спасибо вам, дорогие коллеги, постараюсь чтобы у этой истории было продолжение
Tags:
Hubs:
+13
Comments1

Articles

Change theme settings