Форум программистов, компьютерный форум, киберфорум
Наши страницы

Пишу игровой движок на C++. 025. База для моделей.

Войти
Регистрация
Восстановить пароль
Сайт движка - gost.imsoftworks.info
Исходные коды движка - https://github.com/532235/GoST
Документация
Примеры кода программ - https://github.com/532235/GoST/wiki

Другой хобби-проект в группе в вк https://vk.com/club154291467
Оценить эту запись

Пишу игровой движок на C++. 025. База для моделей.

Запись от 532235 размещена 29.12.2017 в 20:13

Как устроены модели (информация о 3D сетке).

Есть-интерфейс gtModel
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#pragma once
#ifndef __GT_MODEL_H__
#define __GT_MODEL_H__
 
/*
*/
 
namespace gost{
    
        //  software модель
    class gtModel : public gtRefObject{
    public:
 
            //  создаст возвратит суб модель для редактирования
        virtual gtSubModel* addSubModel( u32 v_count, u32 i_count, u32 stride ) = 0;
 
            // нужно учитывать количество суб моделей
        virtual gtSubModel* getSubModel( u32 id ) = 0;
 
            //  Вернёт количество мешбуфферов/субмоделей
        virtual u32     getSubmodelsCount( void ) = 0;
 
            //  Возврат количество байт на вершину
        virtual u32     getStride( void ) = 0;
 
 
            /* 
                При создании модели, зная, какие данные находятся в вершине, нужно создать
                массив gtVertexType, и указать последний елемент gtVertexType::end (чтобы не посылать ещё 1 аргумент для обозначения его размера)
            */
            //  Возврат на массив gtVertexType, который должен заканчиваться gtVertexType::end
        virtual gtVertexType*   getTypeArray( void ) = 0;
    };
 
}
 
#endif
Эта модель хранит в себе мешбуферы(в терминах GoST субмодели)
Когда плагин по импорту моделей будет создавать модель, он будет сначала вызывать
addSubModel, получит указатель на созданную суб модель, потом заполнит её
вершинами и т.д.

Метод getTypeArray нужен чтобы понять, какой формат у вершины

--------------------------------------
Сначала идёт создание не hardware модели.
По сути можно самому заполнить вершины информацией и уже потом создать модель для рендеринга.

Для теста, да и в будущем пригодится, сделал метод для создания плоскости, с указанием размеров.
Пусть все подобные методы находятся в отдельном классе.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#ifndef __GT_MODEL_SYSTEM_H__
#define __GT_MODEL_SYSTEM_H__
 
namespace gost{
 
 
    class gtModelSystem : public gtRefObject{
    public:
        // выделяет память
        virtual gtModel*    createEmpty( u32, gtVertexType* ) = 0;
 
        virtual gtModel*    createPlane( f32 x = 1.f, f32 y = 1.f ) = 0;
 
    };
 
}
 
#endif
Объект лежит в gtMainSystemCommon, так же есть метод для получения указателя.

Теперь реализация.
Класс gtModelSystemImpl реализуется в gost.dll
C++
1
2
3
4
5
6
7
8
gtModel*    gtModelSystemImpl::createEmpty( u32 stride, gtVertexType* vt ){
    gtPtr<gtModelImpl> m = gtPtrNew<gtModelImpl>(new gtModelImpl( stride, vt ) );
 
    if( m.data() )
        m->addRef();
 
    return m.data();
}
Пока один метод.

реализация модели
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#pragma once
#ifndef __GT_MODEL_IMPL_H__
#define __GT_MODEL_IMPL_H__
 
namespace gost{
 
    class gtModelImpl : public gtModel{
    
        /*хранит суб модели*/
        gtArray< gtSubModel* > m_submodels;
        
        /*тип вершины*/
        gtVertexType*   m_typeArray;
 
        /*количество байтов на вершину*/
        u32 m_stride;
 
    public:
 
        gtModelImpl( u32, gtVertexType* );
        ~gtModelImpl();
 
            //  возвратит суб модель, с типом, в зависимости от типа вершины
        gtSubModel* addSubModel( u32 v_count, u32 i_count, u32 stride );
 
            // нужно учитывать количество суб моделей
        gtSubModel* getSubModel( u32 id );
 
            //  Вернёт количество мешбуфферов/субмоделей
        u32     getSubmodelsCount( void );
 
            //  Возврат количество байт на вершину
        u32     getStride( void );
 
            //  Возврат на массив gtVertexType, который должен заканчиваться gtVertexType::end
        gtVertexType*   getTypeArray( void );
    };
 
}
 
#endif
.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
gtModelImpl::gtModelImpl( u32 s, gtVertexType* ta ):
    m_stride( s )
{
    u32 sz = 0u;
    gtVertexType * vt = &ta[ 0u ];
    while( *vt++ != gtVertexType::end ) sz++;
 
    m_typeArray = new gtVertexType[ sz + 1u ];
 
    memcpy( m_typeArray, ta, (sz + 1u) * sizeof(gtVertexType) );
 
}
 
gtModelImpl::~gtModelImpl(){
/*удаление субмоделей*/
    for each( auto* var in m_submodels ){
        delete var;
    }
 
    if( m_typeArray )/*тип вершины*/
        delete []m_typeArray;
}
 
    //  возвратит суб модель
    /*выделяет нужную память*/
gtSubModel* gtModelImpl::addSubModel( u32 v_count, u32 i_count, u32 s ){
    
    gtSubModel* subModel = new gtSubModel;
    
    if( !subModel ) return nullptr;
 
    subModel->m_iCount = i_count;
    subModel->m_vCount = v_count;
    
    subModel->allocate( s );
    
    if( !subModel->m_vertices ){
        delete subModel;
        return nullptr;
    }
 
    subModel->m_indices  = new u16[ i_count ];
    
    m_submodels.push_back( subModel );
 
    return subModel;
}
 
    // нужно учитывать количество суб моделей
gtSubModel* gtModelImpl::getSubModel( u32 id ){
    return m_submodels[id];
}
 
    //  Вернёт количество мешбуфферов/субмоделей
u32     gtModelImpl::getSubmodelsCount( void ){
    return m_submodels.size();
}
 
    //  Возврат количество байт на вершину
u32     gtModelImpl::getStride( void ){
    return m_stride;
}
 
    //  Возврат на массив gtVertexType, который должен заканчиваться gtVertexType::end
gtVertexType*   gtModelImpl::getTypeArray( void ){
    return &m_typeArray[0u];
}

Теперь как состоит субмодель
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#pragma once
#ifndef __GT_MESH_BUFFER_H__
#define __GT_MESH_BUFFER_H__
 
/*
*/
 
namespace gost{
    
        //  То, что идёт конкретно на рендеринг.
    struct gtSubModel{
 
        gtSubModel( void ):
            m_vertices( nullptr ),
            m_indices( nullptr ),
            m_vCount( 0u ),
            m_iCount( 0u )
        {}
 
        ~gtSubModel( void ){
            if( m_vertices )
                delete []m_vertices;
            if( m_indices )
                delete []m_indices;
        }
 
            //  вершины
        u8  *           m_vertices;
 
            //  индексы
        u16 *           m_indices;
 
            //  количество вершин
        u32 m_vCount;
 
            //  количество индексов
        u32 m_iCount;
 
            //  свой материал
        gtMaterial m_material;
 
        gtStringA m_name;
 
 
        void fillIndices( const u16* array ){
            for(u32 i = 0u; i < m_iCount; ++i){
                m_indices[ i ] = array[ i ];
            }
        }
 
        void    allocate( u32 stride ){
            if( !m_vertices )
                m_vertices = new u8[ stride * m_vCount ];
        }
    };
 
}
 
#endif
Отказался от gtVertex. Простой массив роднее для D3D11

Так же не уверен что тут должен быть материал.
По сути, материал должен быть у каждой субмодели.
Количество субмодели зависит от самой модели(очевидно).
Но что если нужно сделать несколько объектов с разными текстурами, шейдерами и т.д.
Пока что пусть всё остаётся так.

-------------------------------
Теперь создание плоскости
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
gtModel*    gtModelSystemImpl::createPlane( f32 x, f32 y ){
 
    gtVertexType vt[ 4 ] = {
        gtVertexType::position,
        gtVertexType::uv,
        gtVertexType::normal,
        gtVertexType::end
    };
 
    gtPtr<gtModel> model = gtPtrNew<gtModel>( createEmpty( gtStrideStandart, &vt[ 0u ] ) );
    if( !model.data() ){
        gtLogWriter::printWarning( u"Can not allocate memory for gtModel");
        return nullptr;
    }
    
    const u32 i_count = 6U;
    const u32 v_count = 4U;
    gtSubModel* subModel = model->addSubModel( v_count, i_count, gtStrideStandart );
 
    if( !subModel )
        return nullptr;
 
    u8 * v = &subModel->m_vertices[0];
        
        /*для удобного заполнения*/
    struct vert_t{
        v4f pos;
        v2f uv;
        v3f nor;
    }vert;
 
    vert.nor.set({0.f, 1.f, 0.f });
 
    vert.pos.set({-x, 0.f, -y, 1.f});
    vert.uv.set({0.f, 1.f});
    memcpy( v, &vert, gtStrideStandart );
 
    v += gtStrideStandart; /* + 36 байт*/
 
    vert.pos.set({x, 0.f, -y, 1.f});
    vert.uv.set({0.f, 0.f});
    memcpy( v, &vert, gtStrideStandart );
 
    v += gtStrideStandart;
 
    vert.pos.set({x, 0.f, y, 1.f});
    vert.uv.set({1.f, 0.f});
    memcpy( v, &vert, gtStrideStandart );
 
    v += gtStrideStandart;
 
    vert.pos.set({-x, 0.f, y, 1.f});
    vert.uv.set({1.f, 1.f});
    memcpy( v, &vert, gtStrideStandart );
            
    const u16 u[i_count] = {0U,1U,2U,0U,2U,3U};
 
    subModel->fillIndices( u );
 
    model->addRef();
    return model.data();
}
Из за того что нет камеры, и по умолчанию она как бы стоит с боку, то, так как плоскоть находится по горизонтали, она не будет видна.
Нужно просто изменить второе значение (значение Y - высота)

------------------------------
Модель для рисования.
У render плагинов должна быть своя модель, которую они будут рисовать.
Она должна хранить hardware буферы.

Создал абстрактный класс
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#ifndef __GT_RENDER_MODEL_H__
#define __GT_RENDER_MODEL_H__
 
/*
*/
 
namespace gost{
    
        //  hardware модель
        //  реализуется в плагинах.
        //  создаёт hardware буферы
    class gtRenderModel : public gtRefObject{
    public:
 
        virtual gtModel*    getModel( void ) = 0;
 
    };
 
}
 
#endif
реализация
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma once
#ifndef __GT_RENDER_MODEL_D3D11_H__
#define __GT_RENDER_MODEL_D3D11_H__
 
/*
*/
 
namespace gost{
    
        //  hardware модель
        //  реализуется в плагинах.
        //  создаёт hardware буферы
    class gtRenderModelD3D11 : public gtRenderModel{
 
        gtModel *   m_sModel; /*указатель на software модель*/
 
        gtDriverD3D11* m_driver;
 
    public:
 
        gtRenderModelD3D11( gtDriverD3D11* );
        ~gtRenderModelD3D11( void );
        
 
        bool    init( gtModel* );
 
 
        gtModel*    getModel( void );
        
        
        gtArray<ID3D11Buffer*> m_vBuffers;/*вершинные и индексные буферы*/
        gtArray<ID3D11Buffer*> m_iBuffers;
 
    };
 
}
 
#endif
реализация
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
gtRenderModelD3D11::gtRenderModelD3D11( gtDriverD3D11* d ):
    m_sModel( nullptr ),
    m_driver( d )
{}
 
gtRenderModelD3D11::~gtRenderModelD3D11( void ){
    for each( auto * var in m_vBuffers )
        var->Release();
    for each( auto * var in m_iBuffers )
        var->Release();
}
 
bool    gtRenderModelD3D11::init( gtModel* m ){
 
    m_sModel = m;
 
    D3D11_BUFFER_DESC   vbd, ibd;
    ZeroMemory( &vbd, sizeof( D3D11_BUFFER_DESC ) );
    ZeroMemory( &ibd, sizeof( D3D11_BUFFER_DESC ) );
 
    vbd.Usage       =   D3D11_USAGE_DEFAULT;
    vbd.BindFlags   =   D3D11_BIND_VERTEX_BUFFER;
 
 
    D3D11_SUBRESOURCE_DATA  vData, iData;
    ZeroMemory( &vData, sizeof( D3D11_SUBRESOURCE_DATA ) );
    ZeroMemory( &iData, sizeof( D3D11_SUBRESOURCE_DATA ) );
 
    ibd.Usage       =   D3D11_USAGE_DEFAULT;
    ibd.BindFlags   =   D3D11_BIND_INDEX_BUFFER;
 
 
    HRESULT hr;
 
    u32 smc = m_sModel->getSubmodelsCount();
 
    u32 stride = m_sModel->getStride();
 
    for( u32 i( 0u ); i < smc; ++i ){
 
        auto * sub = m_sModel->getSubModel( i );
 
        vbd.ByteWidth   =   stride * sub->m_vCount;
        
        vData.pSysMem = &sub->m_vertices[0];
 
        ID3D11Buffer* vBuffer = nullptr;
 
        hr = m_driver->getD3DDevice()->CreateBuffer( &vbd, &vData, &vBuffer );
        if( FAILED( hr ) ){
            gtLogWriter::printWarning( u"Can't create Direct3D 11 vertex buffer [%u]", hr );
            return false;
        }
        this->m_vBuffers.push_back( vBuffer );
 
        ibd.ByteWidth   =   sizeof( u16 ) * sub->m_iCount;
 
        iData.pSysMem   =   &sub->m_indices[ 0u ];
 
        ID3D11Buffer* iBuffer = nullptr;
        hr = m_driver->getD3DDevice()->CreateBuffer( &ibd, &iData, &iBuffer );
        if( FAILED( hr ) ){
            gtLogWriter::printWarning( u"Can't create Direct3D 11 index buffer [%u]", hr );
            return false;
        }
        this->m_iBuffers.push_back( iBuffer );
    }
 
 
    return true;
}
 
 
 
gtModel*    gtRenderModelD3D11::getModel( void ){
    return m_sModel;
}

-------------------------------
Рисование

Создал новый шейдер - 3d_basic.hlsl
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Texture2D tex2d_1;
SamplerState tex2D_sampler_1;
 
struct VSIn{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};
 
struct VSOut{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
};
 
VSOut VSMain(VSIn input)
{
    VSOut output;
    output.uv = input.uv;
    output.position = input.position;
    output.normal = input.normal;
    
    return output;
}
 
 
 
float4 PSMain(VSOut input) : SV_TARGET
{
    float4 diffuseColor = tex2d_1.Sample(tex2D_sampler_1, input.uv);
    
    float4 color = diffuseColor;
    
    return color;
}

В D3D11 плагине создал метод в котором будут создаваться стандартные шейдеры.
Убрал всё туда.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
bool    gtDriverD3D11::createShaders( void ){
    //  в будущем стандартные шейдеры нужно убрать внутрь плагина
    gtShaderModel shaderModel;
    shaderModel.pixelShaderModel = gtShaderModel::shaderModel::_5_0;
    shaderModel.vertexShaderModel = gtShaderModel::shaderModel::_5_0;
 
    gtVertexType vertexType2D[] = 
    {
        { gtVertexType::position },
        { gtVertexType::end }
    };
 
    m_shader2DStandart = getShader( 
        u"../shaders/2d_basic.hlsl",
        "VSMain",
        u"../shaders/2d_basic.hlsl",
        "PSMain",
        shaderModel,
        vertexType2D
        );
    if( m_shader2DStandart ){
        //  создание константного буффера.
        if( !m_shader2DStandart->createShaderObject( 96u ) ) return false;
        if( !m_shader2DStandart->createShaderObject( 16u ) ) return false;
    }
 
    gtVertexType vertexType3D[] = 
    {
        { gtVertexType::position },
        { gtVertexType::uv },
        { gtVertexType::normal },
        { gtVertexType::end }
    };
 
    m_shader3DStandart = getShader( 
        u"../shaders/3d_basic.hlsl",
        "VSMain",
        u"../shaders/3d_basic.hlsl",
        "PSMain",
        shaderModel,
        vertexType3D
        );
    if( m_shader3DStandart ){
        //  создание константного буффера.
        //if( !m_shader3DStandart->createShaderObject( 96u ) ) return false;
        //if( !m_shader3DStandart->createShaderObject( 16u ) ) return false;
    }
 
    return true;
}
Для рисования модели, в gtDriver добавил новый метод
C++
1
2
    //  нарисует gtRenderModel
virtual void drawModel( gtRenderModel* ) = 0;
Реализация на текущий момент
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    //  нарисует gtRenderModel
void gtDriverD3D11::drawModel( gtRenderModel* model ){
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    
    auto * soft = model->getModel();
 
    s32 smc = soft->getSubmodelsCount();
    
    u32 stride = soft->getStride();
 
    gtRenderModelD3D11* d3dm = (gtRenderModelD3D11*)model;
 
    u32 offset = 0u;
 
    for( u32 i( 0u ); i < smc; ++i ){
 
        auto * sub = soft->getSubModel( i );
 
        gtShader * shader = sub->m_material.shader;
        if( !shader ){
            shader = m_shader3DStandart;
        }
 
        gtMaterial& material = sub->m_material;
 
        for( u32 i = 0u; i < 16u; ++i ){
            if( !material.textureLayer[ i ].texture ) break;
 
            gtTextureD3D11* texture = (gtTextureD3D11*)material.textureLayer[ i ].texture;
 
            m_d3d11DevCon->PSSetShaderResources( i, 1, texture->getResourceView() );
            m_d3d11DevCon->PSSetSamplers( i, 1, texture->getSamplerState() );
        }
 
        m_d3d11DevCon->IASetInputLayout( ((gtShaderImpl*)shader)->m_vLayout );
        m_d3d11DevCon->VSSetShader( ((gtShaderImpl*)shader)->m_vShader, 0, 0 );
        m_d3d11DevCon->PSSetShader( ((gtShaderImpl*)shader)->m_pShader, 0, 0 );
 
        m_d3d11DevCon->IASetVertexBuffers( 0, 1, &d3dm->m_vBuffers[ i ], &stride, &offset );
        m_d3d11DevCon->IASetIndexBuffer( d3dm->m_iBuffers[ i ], DXGI_FORMAT_R16_UINT, 0);
        m_d3d11DevCon->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
        m_d3d11DevCon->DrawIndexed( sub->m_iCount, 0, 0 );
 
    }
}

Так же gtDriver должен создавать hardware модель
C++
1
2
    //  Создаёт модель для рисования
virtual gtRenderModel*  createModel( gtModel* ) = 0;
реализация
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    //  Создаёт модель для рисования
gtRenderModel*  gtDriverD3D11::createModel( gtModel* m ){
    GT_ASSERT2( m, "gtModel != nullptr" );
 
    gtPtr<gtRenderModelD3D11> model = gtPtrNew<gtRenderModelD3D11>( new gtRenderModelD3D11( this ) );
 
    if( !model->init( m ) ){
        gtLogWriter::printWarning( u"Can not init D3D11 model" );
        return nullptr;
    }
 
    model->addRef();
    return model.data();
}
идея с моделями подобна идеи с картинками.
есть software версия, есть hardware.


---------------------------------
пример

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*получение указателя на gtModelSystem*/
gtModelSystem* ms = my_system->getModelSystem();
 
/*создание плоскости*/
gtPtr<gtModel> model = gtPtrNew<gtModel>( ms->createPlane( 0.25f, 0.25f ) );
 
/*количество суб моделей*/
u32 smc = model->getSubmodelsCount();
 
/*получить суб модель*/
gtSubModel * sm = model->getSubModel( 0u );
 
/* если знаем из чего состоит вершина, то можно для удобства сделать структуру*/
struct vert_t{
    v4f pos;
    v2f uv;
    v3f nor;
};
 
/*получение вершин*/
vert_t * vert = reinterpret_cast<vert_t*>( &sm->m_vertices[0] );
 
/* если не знаем, то можно узнать так */
gtVertexType * varray = model->getTypeArray();
 
gtLogWriter::printInfo( u"\r\nModel vertex stride \t[%u]", model->getStride() );
 
gtLogWriter::printInfo( u"Model vertex type: ");
 
while( *varray != gtVertexType::end ){
    switch( *varray ){
    case gtVertexType::position: /*16байт*/
        gtLogWriter::printInfo( u"\t\t\tposition" );
        break;
    case gtVertexType::normal: /*12байт*/
        gtLogWriter::printInfo( u"\t\t\tnormal" );
        break;
    case gtVertexType::uv: /*8байт*/
        gtLogWriter::printInfo( u"\t\t\tuv" );
        break;
    }
    varray++;
}
 
/*копирование материала субмодели*/
model->getSubModel(0)->m_material = material1;
 
/*создание gtRenderModel*/
gtPtr<gtRenderModel>  rModel = gtPtrNew<gtRenderModel>( driver1->createModel( model.data() ) );
 
...
 
/*рисование*/
driver1->drawModel( rModel.data() );
Следующий пост о матрицах и камерах.
Уже потом загрузка OBJ
Размещено в Игровой движок
Просмотров 257 Комментарии 0
Всего комментариев 0

Комментарии

 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.