netlib.narod.ru< Назад | Оглавление | Далее >

Библиотека классов 3dPlus

В предыдущих главах уже говорилось, что библиотека классов 3dPlus не претендует на роль ведущего средства для работы с функциями Direct3D. Я спроектировал ее лишь для того, чтобы исследовать концепции трехмерной графики более удобным и привычным способом, чем с использованием COM-интерфейсов. Для разработки трехмерных приложений эта библиотека не нужна, однако ее использование в качестве учебного средства или основы для настоящей библиотеки может ускорить вашу работу.

Для проверки указателей и различных условий в библиотеке применяются директивы ASSERT. Во многих случаях ошибки в ваших программах приведут к тому, что отладчик Visual C++ остановится на директиве ASSERT вместо того, чтобы заглохнуть где-нибудь в ядре библиотеки трехмерной графики.

Многие классы 3dPlus представляют собой простейшие оболочки для интерфейсов Direct3D. Отдельные классы предоставляют более высокий уровень функциональности, чем интерфейс. В любом случае я старался сделать так, чтобы вы могли максимально просто обойти класс и напрямую обратиться к базовому интерфейсу. Для этого в большинстве классов библиотеки присутствует функция GetInterface, которая возвращает указатель на базовый интерфейс. Обратите внимание на то, что перед возвращением указателя она не вызывает функцию AddRef, так что в этом случае вам не следует вызывать функцию Release для указателя — относитесь к нему, как к обычному указателю на объект класса C++.

На рис. 3.3 изображена иерархия классов библиотеки 3dPlus. Я не стал включать в нее классы, относящиеся непосредственно к программному слою DirectDraw. Все классы на рис. 3.3 относятся к абстрактному режиму Direct3D.


Рис. 3.3. Иерархия классов библиотеки 3dPlus

Рис. 3.3. Иерархия классов библиотеки 3dPlus


Классы библиотеки делятся на три группы: производные непосредственно от C3dObject, производные от C3dVisual и производные от C3dFrame. Если вы посмотрите на иерархию Direct3D, изображенную на рис. 3.1, то увидите, что эти две иерархии во многом схожи. Основное отличие между ними заключается в том, что я сделал некоторые классы производными от C3dFrame, чтобы объекты этих классов могли иметь собственное положение и направление и при этом выступать в роли визуальных элементов. Следовательно, по отношению к интерфейсам это означает, что классы, производные от C3dFrame, используют оба интерфейса — IDirect3DRMFrame и IDirect3DRMVisual. Давайте кратко познакомимся с классами 3dPlus, узнаем их назначение и в отдельных случаях посмотрим, как ими пользоваться.

C3dEngine

Класс C3dEngine объединяет несколько глобальных функций механизма визуализации. Библиотека классов 3dPlus содержит всего один глобальный объект этого класса с именем the3dEngine. Функции данного класса чаще всего используются для создания других объектов, относящихся к механизму визуализации, и возвращают указатель на интерфейс. Обычно вам не придется непосредственно пользоваться этим классом в своих приложениях, однако при создании объектов других классов нередко применяется код C3dEngine. В приведенном ниже примере показано, как работают с объектом the3dEngine:

    BOOL C3dFrame::Create(C3dFrame* pParent)
    {
        if (m_pIFrame) {
            m_pIFrame->Release();
            m_pIFrame = NULL;
        }
        if (!the3dEngine.CreateFrame(_GetRef(pParent),
                                          &m_pIFrame)) {
            TRACE("Frame create failed\n");
            m_pIFrame = NULL;
            return FALSE;
        }
        ASSERT(m_pIFrame);
        m_pIFrame->SetAppData((ULONG)this);
        return TRUE;
    } 

Именно функция CreateFrame объекта the3dEngine фактически создает интерфейс фрейма и присваивает указатель на него переменной фрейма m_pIFrame.

C3dMatrix

В механизме визуализации предусмотрена собственная матрица 4 x 4 для преобразований координат, однако я предпочитаю пользоваться классами C++, поскольку они сокращают объем программного кода. Например, найдите в приведенном ниже описании класса C3dFrame функцию C3dPosCtrl::OnUpdate, и вы увидите, как матрица используется для вращения двух векторов. Программа выглядит до смешного простой, невзирая на сложный математический базис вычислений. Классы C++ позволяют чрезвычайно гибко работать с матрицами, не загромождая программу.

Разумеется, вы не обязаны пользоваться классами C3dMatrix и C3dVector. Однако при работе с другими классами библиотеки 3dPlus вы увидите, что наличие классов для матриц и векторов упрощает вашу работу. Матрицы подробнее рассмотрены в главе 5.

C3dDevice

Класс C3dDevice представляет собой простую оболочку для интерфейса IDirect3DRMDevice. Класс C3dStage пользуется C3dDevice для создания окружения, в котором отображаются трехмерные объекты. Вероятно, вам не придется обращаться к объектам этого класса, если только вы не надумаете полностью пересмотреть концепцию сцены. Тем не менее вам может пригодиться функция SetQuality, устанавливающая качество визуализации в вашем приложении. Работу с классом устройства рассмотрим на примере функции для создания сцены:

    BOOL C3dStage::Create(CDirect3D* pD3D)
    {
        .
        .
        .
        // Создать новое устройство по поверхностям Direct3D
        if (!m_Device.Create(pD3D)) return FALSE;
        // Установить текущее качество визуализации
        m_Device.SetQuality(m_Quality);
        .
        .
        .
    } 

C3dViewport

Класс C3dViewport представляет собой простую оболочку для интерфейса IDirect3DRMViewport. Маловероятно, что вам придется непосредственно работать с этим классом, поскольку объект C3dStage берет управление портом просмотра на себя. Ниже приводится функция класса сцены, которая воспроизводит на экране текущее состояние макета:

    void C3dStage::Render()
    {
        ASSERT(m_pIFrame); 

        // Очистить ракурс
        m_Viewport.Clear(); 

        if (m_pScene) { 

            // Воспроизвести макет
            m_Viewport.Render(m_pScene);
        } 

        // Обновить изображение
        m_Device.Update();
} 

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

C3dWrap

Класс C3dWrap (определяемый в 3dImage.cpp) также в основном используется как оболочка интерфейса IDirect3DRMWrap, однако он обладает и самостоятельной ценностью. Функция Apply реализована в двух версиях. Ниже приведен первый, более простой вариант, при котором покрытие накладывается на весь объект:

    BOOL C3dWrap::Apply(C3dShape* pShape)
    {
        ASSERT(pShape);
        ASSERT(m_pIWrap);
        HRESULT hr;
        hr = m_pIWrap->Apply(pShape->GetVisual());
        return SUCCEEDED(hr);
    } 

Второй вариант, приведенный в предыдущем разделе, накладывает покрытие лишь на одну грань.

Наличие двух разных вариантов функции Apply упрощает код и одновременно сохраняет гибкость в реализации. Покрытия, в том числе и хромовые, подробно рассмотрены в главе 8, посвященной текстурам.

C3dVisual

Базовый класс для всех классов, объекты которых используются в качестве визуальных элементов макета. C3dVisual содержит переменную, в которой хранится имя объекта. Для задания и получения этого имени служат функции SetName и GetName. Имена помогают при выделении объектов с помощью мыши — отображение имени объекта позволяет проверить выбор.

C3dImage

Единственный класс, который не пользуется никакими интерфейсами, входящими в механизм визуализации. Он предназначен для загрузки изображений из файлов на диске или ресурсов приложения и их последующего использования в качестве текстур или декалов. Механизм визуализации определяет для таких изображений специальную структуру с именем D3DRMIMAGE. Класс C3dImage пользуется ей для хранения данных загруженного изображения. В приведенном ниже примере класс C3dImage применяется для загрузки из файла на диске изображения, которое будет использовано в качестве фонового изображения макета:

    C3dImage* pImg = new C3dImage;
    if (!pImg->Load()) {
        delete pImg;
        return;
    } 

    ASSERT(m_pScene);
    m_pScene->m_ImgList.Append(pImg);
    m_pScene->SetBackground(pImg); 

Функция C3dImage::Load вызвана без аргументов, поэтому на экране появляется окно диалога. Здесь пользователь может выбрать растровый (bitmap) файл Windows, который будет служить фоновым изображением. Кроме того, загружаемый растр можно выбрать и другим способом — передавая функции Load имя файла или идентификатор растрового ресурса. В приведенном ниже примере мы загружаем растровый ресурс и затем используем его для создания текстуры:

    // Загрузить изображение земного шара
    C3dImage* pImg1 = new C3dImage;
    if (!pImg1->Load(IDB_WORLD)) {
        AfxMessageBox("Failed to load world1.bmp");
        delete pImg1;
        return;
    }
    m_pScene->m_ImgList.Append(pImg1); 

    // Создать текстуру по изображению
    C3dTexture tex1;
    tex1.Create(pImg1);

C3dTexture

Класс C3dTexture (определяемый в 3dImage.cpp) является оболочкой интерфейса IDirect3DRMTexture. Как видно из приведенного выше примера, текстуры создаются на основе графических изображений. Размеры сторон у таких изображений должны быть равны степеням двойки. Так, параметры 32 x 64, 128 x 128 и 4 x 8 подходят для создания текстур; а величины 32 x 45 и 11 x 16 являются недопустимыми. Если изображение имеет неправильный размер, функция C3dTexture::Create завершится неудачей:

    BOOL C3dTexture::Create()
    {
        if (m_pITexture) {
            m_pITexture->Release();
            m_pITexture = NULL;
        } 

        // Убедиться, что размеры изображения равны степеням 2
        for (int i = 0; (1 << i) < GetWidth(); i++);
            for (int j = 0; (1 << j) < GetHeight(); j++);
                if (GetWidth() != (1 << i) || GetHeight() != (1 << j)) {
                    TRACE("This image can't be used as a texture."\
                          " Its sides are not exact powers of 2\n");
                } 

        if (!the3dEngine.CreateTexture(GetObject(),
                                       &m_pITexture)) {
            TRACE("Texture create failed\n");
            m_pITexture = NULL;
            return FALSE;
        }
        ASSERT(m_pITexture);
        return TRUE;
    } 

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

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

    C3dImage* pImg1 = new C3dImage;
    pImg1->Load(IDB_LEAVES);

    C3dTexture tex1;
    tex1.Create(pImg1); 

    C3dWrap wrap;
    wrap.Create(D3DRMWRAP_CYLINDER,
                NULL,
                0, 0, 0, // Начало координат
                0, 0, 1, // Направление
                0, 1, 0, // Вверх
                0, 0,    // Начало текстуры
                1, 1);   // Масштаб текстуры
    pTree->SetTexture(&tex1);
    wrap.Apply(pTree); 

C3dFrame

Класс C3dFrame является оболочкой интерфейса IDirect3DRMFrame и включает несколько дополнительных функций, облегчающих работу с ним. Фреймы содержат ряд атрибутов, в число которых входит положение фрейма и его ориентация в трехмерном пространстве. Положение фрейма устанавливается функцией SetPosition, а ориентация (то есть направление, в котором обращен фрейм) — функцией SetDirection. Для точного определения ориентации необходимо указать два вектора. Первый вектор описывает переднее направление, а второй — верхнее. Рассмотрим ситуацию на примере летящего самолета. Передний вектор (или вектор направления) — это курс, по которому летит самолет, то есть направление, в котором ориентирован его нос. Верхний вектор показывает, куда обращено хвостовое перо самолета — вверх, вниз, влево и т.д. Для некоторых объектов верхний вектор оказывается несущественным. Например, в вашем макете может присутствовать конус, указывающий на некоторый объект. Ориентация конуса совпадает с направлением, куда смотрит его вершина. Верхний вектор не имеет никакого значения, поскольку при вращении конуса вокруг продольной оси его внешний вид не меняется. Чтобы упростить вашу работу, функция SetDirection позволяет задать только передний вектор и определяет верхний вектор за вас. Вот как это делается:

    void C3dFrame::SetDirection(double dx, double dy, double dz,
                                C3dFrame* pRef)
    {
        ASSERT(m_pIFrame); 

        // Создать передний вектор
        C3dVector d(dx, dy, dz);

        // Сгенерировать верхний вектор
        C3dVector u = d.GenerateUp(); 

        SetDirection(d.x, d.y, d.z, u.x, u.y, u.z, pRef);
    } 

Класс C3dVector содержит ряд функций для генерации верхних векторов, благодаря которым работа с классом упрощается до предела. Мне это нравится.

Во всех функциях для определения положения и ориентации присутствует обязательный аргумент — эталонный фрейм (pRef в приведенном выше примере). Он чрезвычайно важен, поскольку ваш фрейм может находиться в произвольном месте иерархии фреймов, а его положение определяется его собственным преобразованием вместе с преобразованиями всех родительских фреймов. Это напоминает бег по кухне; если перенести ваш дом из Вашингтона в Колорадо, вы все равно сможете бегать по кухне, но ваше положение на планете при этом изменится. Другими словами, любые перемещения происходят по отношению к некоторому эталонному фрейму. Для удобства можно передать вместо эталонного фрейма NULL, и тогда за эталон будет принят фрейм-родитель. Примером использования эталонного фрейма служит функция из файла 3dInCtlr.cpp, которая позиционирует объекты в макете по мере того, как пользователь перемещает их с помощью клавиатуры, мыши или джойстика:

    void C3dPosCtlr::OnUpdate(_3DINPUTSTATE& st,
                              C3dFrame* pFrame)
    {
        // Получить указатель на сцену, которая будет 
        // использоваться
        // в качестве эталона при определении положений 
        // фреймов и т.д.
        ASSERT(m_pWnd);
        C3dStage* pStage = m_pWnd->GetStage();
        ASSERT(pStage); 

        double x, y, z;
        pFrame->GetPosition(x, y, z, pStage);
        x += st.dX * 0.1;
        y += st.dY * 0.1;
        z += st.dZ * 0.1;
        pFrame->SetPosition(x, y, z, pStage); 

        C3dVector d, u;
        pFrame->GetDirection(d, u, pStage);
        // Повернуть вектор направления и верхний вектор
        double a = 3.0;
        C3dMatrix r;
        r.Rotate(-st.dR * a, -st.dU * a, -st.dV * a);
        d = r * d;
        u = r * u;
        pFrame->SetDirection(d, u, pStage);
    } 

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

C3dScene

Класс C3dScene содержит всю информацию, необходимую для описания макета: источники света, список фигур, текущее фоновое изображение и настройку камеры. В любой момент времени к сцене может быть присоединен только один объект-макет C3dScene. Макет содержит встроенный источник рассеянного света, параметры которого задаются функцией SetAmbientLight. Вы можете включить в макет и другие источники света, вызывая функцию AddLight. Макет возглавляет всю иерархию фреймов, поэтому трехмерные фигуры (которые также являются фреймами) присоединяются к ней функцией AddChild. Вы можете задать цвет фона макета функцией SetBackground(r, g, b) или же вместо этого указать фоновое изображение, для чего используется функция SetBackground(pImage). Функция Move обновляет положение всех движущихся объектов макета и воспроизводит текущий макет в окне приложения. Вызов функции Move приводит к каким-то результатам лишь в том случае, если макет присоединен к сцене. В объекте C3dScene также хранятся векторы, определяющие положение и направление камеры. Значения этих векторов задаются функциями SetCameraPosition и SetCameraDirection. Приведенный ниже фрагмент программы создает новый макет и задает исходные параметры источников света:

    // Создать исходный макет
    m_pScene = new C3dScene;
    if (!m_pScene->Create()) return FALSE; 

    // Установить источники света
    C3dDirLight d1;
    d1.Create(0.8, 0.8, 0.8);
    m_pScene->AddChild(&d1);
    d1.SetPosition(-2, 2, -5);
    d1.SetDirection(1, -1, 1);
    m_pScene->SetAmbientLight(0.4, 0.4, 0.4); 

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

Объект C3dScene содержит два объекта-списка, которые облегчают работу с макетами. Список m_ShapeList помогает определить перечень объектов C3dShape, а список m_ImageList — объектов C3dImage, удаляемых при уничтожении макета. Новые объекты заносятся в эти списки только при вызове функции Append объекта-списка. Вы не можете свободно манипулировать содержимым этих списков.

C3dSprite

Класс C3dSprite поддерживает работу с плоскими объектами в объемном мире. В документации по DirectX 2 SDK такие спрайты именуются декалами. В главе 9 показано, как можно пользоваться спрайтами в играх, где производительность важнее подлинной объемности изображения. Класс C3dSprite является производным от C3dFrame, поэтому спрайты обладают теми же возможностями позиционирования, что и трехмерные объекты.

C3dCamera

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

C3dShape

Класс C3dShape сочетает функциональность интерфейсов фрейма и визуального элемента, что позволяет создавать трехмерные фигуры, для которых можно задать непосредственное расположение в макете. Одна фигура легко присоединяется к другой в качестве потомка, так что вы можете строить сложные фигуры. В класс входит несколько функций, предназначенных для создания простых геометрических фигур (сфер, цилиндров и конусов), а также функция Load, которая позволяет создать объект по данным из файла .X. Вы можете задать цвет и текстуру всей фигуры или ее отдельных граней. При наложении текстур на отдельные грани имеются ограничения, о которых рассказано в главе 8. В приведенном ниже примере мы создаем простую геометрическую фигуру и присоединяем ее к текущему макету:

    C3dShape sh1;
    sh1.CreateCube(2);
    m_pScene->AddChild(&sh1); 

Обратите внимание на то, что объект C3dShape всего лишь является контейнером для интерфейсов фрейма и визуального элемента. После того как фигура будет присоединена к макету или другому фрейму, объект-контейнер оказывается ненужным, потому что для включения фигуры в макет использовались интерфейсы, а не объект C++. Если визуальный элемент, соответствующий одному объекту, будет использован во втором объекте, то второй объект вызывает AddRef для интерфейсного указателя визуального элемента. Таким образом, даже после уничтожения исходного контейнера и освобождения указателя на интерфейс визуальный элемент все равно продолжит свое существование.

Хотя я и сказал, что контейнер не обязательно сохранять после завершения его работы, он все же может принести определенную пользу. Дело в том, что пользователь может выбрать объект, с которым желает произвести какие-то манипуляции в макете. Хотелось бы управлять им посредством объекта-контейнера C++. Непонятно? Мы рассмотрим эту тему в главе 6, когда будем изучать манипуляции с объектами, входящими в макет.

C3dStage

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

    BOOL C3dWnd::SetScene(C3dScene* pScene)
    {
        if (!m_pStage) return FALSE;
        m_pScene = pScene;
        m_pStage->SetScene(m_pScene);
        if (m_pScene) {
            // Разрешить воспроизведение
            // во время пассивной работы приложения
            m_bEnableUpdates = TRUE;
        } else {
            m_bEnableUpdates = FALSE;
        } 

        return TRUE;
    } 

C3dLight

Базовый класс для производных классов, объекты которых представляют источники света. Его функция Create вызывается производными классами для конструирования соответствующего интерфейса:

    BOOL C3dLight::Create(D3DRMLIGHTTYPE type,
                   double r, double g, double b)
    {
        // Создать фрейм для источника света
        if (!C3dFrame::Create(NULL)) return FALSE; 

        // Создать объект-источник света
        ASSERT(m_pILight == NULL);
        if (!the3dEngine.CreateLight(type, r, g, b, &m_pILight))
        {
            return FALSE;
        }
        ASSERT(m_pILight); 

        // Присоединить источник света к его фрейму
        ASSERT(m_pIFrame);
        m_hr = m_pIFrame->AddLight(m_pILight);
        if (FAILED(m_hr)) return FALSE; 

        return TRUE;
    } 

C3dAmbLight

Класс C3dAmbLight реализует рассеянный источник света, встроенный в объект C3dScene. Вместо того чтобы вызывать функции этого класса, можно работать с источником рассеянного света в макете посредством C3dScene::SetAmbientLight.

C3dDirLight

Класс реализует направленный источник света, который может размещаться в произвольной точке макета для получения бликов на его объектах. Ниже приводится пример размещения направленного источника света в левом верхнем углу макета:

    C3dDirLight d1;
    d1.Create(0.8, 0.8, 0.8);
    m_pScene->AddChild(&d1);
    d1.SetPosition(-2, 2, -5);
    d1.SetDirection(1, -1, 1); 

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

Класс C3dLight имеет еще несколько производных классов. Все различные типы источников света рассмотрены в главе 10.

C3dShapeList, C3dImageList, C3dFrameList

Эти классы предназначены для хранения списка объектов C3dShape (C3dShapeList), C3dImage (C3dImageList), C3dFrame (C3dFrameList). Каждый из этих классов является производным от CObList, входящего в библиотеку MFC. Подробное описание работы CObList можно найти в документации по MFC. Все эти списки содержатся в объекте C3dScene и помогают найти фигуры, изображения и фреймы, которые необходимо удалить при уничтожении макета.

C3dWnd

Класс C3dWnd содержит все необходимое для создания всплывающего или дочернего окна, содержащего трехмерный макет. В частности, в этот класс включается объект-сцена. Класс поддерживает работу с манипуляторами, которые используются для перемещения объектов (клавиатура, мышь и джойстик), а также выбор объектов мышью. Он позволяет чрезвычайно легко создавать дочерние трехмерные окна, как показывает приведенный ниже фрагмент программы:

    // Создать трехмерное окно
    if (!m_wnd3d.Create(this, IDC_3DWND)) {
        return -1;
    } 

Трехмерное окно создается как дочернее, с заданным родительским окном и идентификатором дочернего окна. После построения окна вам остается лишь создать макет и присоединить его к сцене трехмерного окна, чтобы он появился на экране:

    m_pScene = new C3dScene;
    if (!m_pScene->Create()) return FALSE; 

    // Установить источники света
    C3dDirLight d1;
    d1.Create(0.8, 0.8, 0.8);
    m_pScene->AddChild(&d1);
    d1.SetPosition(-2, 2, -5);
    d1.SetDirection(1, -1, 1);
    m_pScene->SetAmbientLight(0.4, 0.4, 0.4);
    m_wnd3d.SetScene(m_pScene); 

C3dVector

В механизме визуализации определен трехмерный вектор D3DVECTOR, который представляет собой обычную структуру, однако я предпочитаю работать с векторными объектами C++. При таком подходе я могу иметь несколько конструкторов, упрощающих создание вектора, и, конечно же, реализовать операторы (например, сложения и умножения) для упрощения кода. Класс C3dVector является производным от структуры D3DVECTOR, так что во всех случаях, где допускается присутствие структуры D3DVECTOR, вы можете пользоваться объектом C3dVector. В качестве примера использования класса C3dVector приведу фрагмент программы, который обновляет положение трехмерного объекта, перемещаемого пользователем по экрану:

    C3dVector d, u;
    pFrame->GetDirection(d, u, pStage);
    // Повернуть вектор направления и верхний вектор
    double a = 3.0;
    C3dMatrix r;
    r.Rotate(-st.dR * a, -st.dU * a, -st.dV * a);
    d = r * d;
    u = r * u;
    pFrame->SetDirection(d, u, pStage); 

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


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz