`
bcyy
  • 浏览: 1822849 次
文章分类
社区版块
存档分类
最新评论

【Visual C++】游戏开发笔记二十九 一步一步教你用优雅的Direct3D11代码画一个三角形

 
阅读更多

本系列文章由zhmxy555(毛星云)编写,转载请注明出处。

http://blog.csdn.net/zhmxy555/article/details/7774929

作者:毛星云 邮箱: happylifemxy@163.com

这个demo演示的效果是用Direct3D11在屏幕上渲染一个三角形,当然是通过这个demo进一步巩固和学习Direct3D11,而不是单单为了画一个三角形这么简单。正如之前所说,这个demo是建立在笔记二十八中讲解的D3D BlankWindows Demo之上的。

那么,我们就开门见山,直入正题吧。

有个别童鞋表示编译源代码时出现了“无法解析的外部符号”的一系列错误,这都是lib库文件没添加造成的,这属于我们的DirectX 11的开发环境没有配置好~
具体配置方法可以参考《Visual C++游戏开发笔记》系列第25篇文章:

【Visual C++】游戏开发笔记二十五 最简化的DirectX 11开发环境的配置

针对单个的程序的话,也可以在代码开头#include语句附近使用pragma语句来添加库文件的~ 针对这个DirectX11 demo的话,在代码开头添加以下几句就好了:

#pragma comment(lib,"dxerr.lib")
#pragma comment(lib,"d3d11.lib")
#pragma comment(lib,"d3dx11.lib")
#pragma comment(lib,"d3dcompiler.lib")

————————浅墨于 2012年 11月06日

一、 载入几何体

我们知道,为了渲染几何图形,我们需要一个顶点缓存,一个描述顶点布局的输入层,以及一系列的着色器,自DirectX10以来,着色器开始作为图形渲染的基础组成部分,在这个demo之中我们会指定顶点着色器与像素着色器,渲染一种简单的纯色表面。后面我们将延伸的讲解如何拓展使用这种效果来在表面映射图形纹理。

下面就开始进行这个demo的书写:

这个demo的核心内容当然是一个叫做TriangleDemo的类,我们为这个类定义几个成员变量,他们分别是ID3D11VertexShader类型的取名为solidColorVS_的变量,一个ID3D11PixelShader类型的唤作solidColorPS的变量。一个ID3D11InputLayout类型的唤作inputLayout_的变量,以及一个ID3D11Buffer类型的叫做vertexBuffer_的变量。

下面就是TriangleDemo.h头文件的源代码,简单的勾勒出了本文主角TriangleDemo类的轮廓:

代码段一 TriangleDemo.h头文件

#include"Dx11DemoBase.h"

class TriangleDemo : public Dx11DemoBase

{

public:

	TriangleDemo( );

	virtual ~TriangleDemo( );

	bool LoadContent( );

	void UnloadContent( );

	void Update( float dt );

	void Render( );

private:

	ID3D11VertexShader* solidColorVS_;

	ID3D11PixelShader* solidColorPS_;

	ID3D11InputLayout* inputLayout_;

	ID3D11Buffer* vertexBuffer_;

};

顶点我们采用一个简单的三分量式的浮点型结构体,在XNA Math library中一个叫做XMFLOAT3的结构体可以胜任这项殊荣。

接下来,开始丰富我们的TriangleDemo类,我们在代码段二中书写顶点结构体VertexPos和TriangleDemo的类的构造函数以及析构函数

代码段二 TriangleDemo 顶点结构体, 构造函数和析构函数.

#include"TriangleDemo.h"

#include<xnamath.h>

struct VertexPos

{

	XMFLOAT3 pos;

};

TriangleDemo::TriangleDemo( ) : solidColorVS_( 0 ), solidColorPS_( 0 ),

inputLayout_( 0 ), vertexBuffer_( 0 )

{

}

TriangleDemo::~TriangleDemo( )

{

}


下面继续丰富我们的TriangleDemo类,在代码段三中我们进行UnloadContent函数的书写,顾名思义,UnloadContent是进行unload content工作的,与后面将书写的LoadContent函数相对应。

代码段三 TriangleDemo类的UnloadContent函数的书写

void TriangleDemo::UnloadContent( )

{

	if( solidColorVS_ ) solidColorVS_->Release( );

	if( solidColorPS_ ) solidColorPS_->Release( );

	if( inputLayout_ ) inputLayout_->Release( );

	if( vertexBuffer_ ) vertexBuffer_->Release( );

	solidColorVS_ = 0;

	solidColorPS_ = 0;

	inputLayout_ = 0;

	vertexBuffer_ = 0;
	
}


顺理成章的,下一步便是LoadContent函数的书写。这个函数由顶点着色器载入,在文件SolidGreenColor.fx中可以查看。

一旦顶点着色器的源代码编译完成,着色器便创建一个CreateVertexShader函数的调用,我们接着创建顶点格式。由于顶点着色器与顶点格式相关联,所以我们还需要将顶点着色器加载到内存中。

创建完顶点着色器和输入格式后,下一步我们创建像素着色器。下面这段代码实现了LoadContent方法的一半的功能:

代码段四 LoadContent函数着色器载入代码

bool TriangleDemo::LoadContent( )

{

	ID3DBlob* vsBuffer = 0;

	bool compileResult = CompileD3DShader( "SolidGreenColor.fx",

	"VS_Main", "vs_4_0", &vsBuffer );

	if( compileResult == false )

	{

		MessageBox( 0, "载入顶点着色器错误!", "编译错误", MB_OK );

		return false;

	}

	HRESULT d3dResult;

	d3dResult = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer(
	
),

vsBuffer->GetBufferSize( ), 0, &solidColorVS_ );

if( FAILED( d3dResult ) )

{

	if( vsBuffer )

	vsBuffer->Release( );

	return false;

}

D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =

{

{ 	"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,

	0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }

};

unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,

totalLayoutElements, vsBuffer->GetBufferPointer( ),

vsBuffer->GetBufferSize( ), &inputLayout_ );

vsBuffer->Release( );

if( FAILED( d3dResult ) )

{

	return false;

}

ID3DBlob* psBuffer = 0;

compileResult = CompileD3DShader( "SolidGreenColor.fx",

"PS_Main", "ps_4_0", &psBuffer );

if( compileResult == false )

{

	MessageBox( 0, "载入像素着色器错误!", "编译错误", MB_OK );

	return false;

}

d3dResult = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),

psBuffer->GetBufferSize( ), 0, &solidColorPS_ );

psBuffer->Release( );

if( FAILED( d3dResult ) )

{

	return false;
	
}

...

//后接函数的下半段

}


CompileD3DShader相关的代码在代码段五中进行了演绎,这段代码巧妙地被分离于LoadContent之外,这样在加载多个不同的着色效果的时候便可以避免大段大段的冗余代码:

代码段五 CompileShader 函数的实现方法

bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char*

shaderModel, ID3DBlob** buffer )

{

	DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

	#if defined( DEBUG ) || defined( _DEBUG )

	shaderFlags |= D3DCOMPILE_DEBUG;

	#endif

	ID3DBlob* errorBuffer = 0;

	HRESULT result;

	result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,

	shaderFlags, 0, 0, buffer, &errorBuffer, 0 );

	if( FAILED( result ) )

	{

		if( errorBuffer != 0 )

			{

				OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );

				errorBuffer->Release( );

			}

	return false;

	}

	if( errorBuffer != 0 )

	errorBuffer->Release( );

	return true;

}


上面我们介绍了上半段LoadContent函数的构成,而下半段LoadContent函数主要实现了顶点缓存的创建。这段代码行文思路很明朗,首先定义一个简单的三角形,沿X轴与Y轴都是0.5f(半个单位的长度)。Z轴依然设为为0.5f,来使此三角形可见。因为若镜头隔表面太近或者太远,表面都不会成功的渲染。

顶点列表存储于一个叫做vertices的数组中,它提供了一个子资源数据,在CreateBuffer函数开始调用进行实际顶点缓存的创建的时候,这些数据可以派上用场。

下面就是上面这段叙述的代码实现,LoadContent函数的下半部分书写风格如下:

代码段六 LoadContent函数的几何图形载入代码

bool TriangleDemo::LoadContent( )

{

//前接函数的上半段

 

...

     VertexPos vertices[] =

	{

		XMFLOAT3( 0.5f, 0.5f, 0.5f ),

		XMFLOAT3( 0.5f, -0.5f, 0.5f ),

		XMFLOAT3( -0.5f, -0.5f, 0.5f )

	};

	D3D11_BUFFER_DESC vertexDesc;

	ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );
	
	vertexDesc.Usage = D3D11_USAGE_DEFAULT;

	vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

	vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;

	D3D11_SUBRESOURCE_DATA resourceData;

	ZeroMemory( &resourceData, sizeof( resourceData ) );

	resourceData.pSysMem = vertices;

	d3dResult = d3dDevice_->CreateBuffer( &vertexDesc,

	&resourceData, &vertexBuffer_ );

	if( FAILED( d3dResult ) )

	{

		return false;

	}
	
	return true;

}


二、渲染几何体

Direct11三角形Demo代码的最后两部分由实现几何渲染功能的代码和着色器本身构成。渲染几何图形的构成代码在TriangleDemo类中的Render函数中进行。函数中有

有一个条件语句,这样可以确保在Direct3D的上下文是有效的。

接下来,我们清除渲染目标,并设定输出程序集(input assembler)。而实际上,因为在这个demo之中的三角形是静态的,我们并不一定非要清除渲染目标,这里只是为了规范我们的代码书写,以免养成不良的开发习惯。在输出程序集阶段的设置由我们已经创建的输出结构(input layout)进行绑定,并提供顶点缓存,设置拓扑三角形的列表。

下面贴出Render函数的书写思路:

代码段七 TriangleDemo类的render函数书写

void TriangleDemo::Render( )

{

	if( d3dContext_ == 0 )

	return;

	float clearColor[4] = { 0.5, 0.5f, 0.5f, 1.0f };  //设定背景颜色

	d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

	unsigned int stride = sizeof( VertexPos );

	unsigned int offset = 0;

	d3dContext_->IASetInputLayout( inputLayout_ );

	d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

	d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_

	TRIANGLELIST );

	d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

	d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

	d3dContext_->Draw( 3, 0 );

	swapChain_->Present( 0, 0 );

}


最后一部分要介绍的代码是着色器。笼统的来说,顶点着色器基于它得到的内容。详细的来说,顶点着色器的作用是将内部得到的顶点位置传递到输出处,之后,我们须处理这些数据,正确绘制出我们的图形。但对于这个非常基础的demo,仅仅进行顶点位置内容的传递就够了。

如果没有几何图形着色器绑定到输出程序集之上,顶点着色器的输出的数据就是像素着色器的输入的数据。其中,像素着色器的输出就是写到输出缓存之中的颜色值。当交换链中的Present函数调用的时候,这个缓存就会最终显示给用户。

TriangleDemo的顶点着色器和像素着色器的书写方法如下代码段八:

代码段八 Triangledemo着色器的实现代码

float4 VS_Main( float4 pos : POSITION ) : SV_POSITION

{

	return pos;

}

float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET

{

	return float4( 0.0f, 1.0f, 0.0f, 1.0f );

}


这样,Triangledemo类就随着一步一步的勾勒,被我们书写完成了。

三、Dx11DemoBase类的书写

接下来,我们将之前讲解的BlankD3DWindows Demo模板中的Dx11DemoBase类进行丰富和修改,即可得到适用于本节demo的Dx11DemoBase类。

代码段九 Dx11DemoBase.h

#ifndef _DEMO_BASE_H_

#define _DEMO_BASE_H_

 

#include<d3d11.h>

#include<d3dx11.h>

#include<DxErr.h>

 

 

class Dx11DemoBase

{

    public:

        Dx11DemoBase();

        virtual ~Dx11DemoBase();

 

        bool Initialize( HINSTANCE hInstance, HWND hwnd );

        void Shutdown( );

 

        bool CompileD3DShader( char* filePath, char* entry,

                               char* shaderModel, ID3DBlob** buffer );

 

        virtual bool LoadContent( );

        virtual void UnloadContent( );

 

        virtual void Update( float dt ) = 0;

        virtual void Render( ) = 0;

 

    protected:

        HINSTANCE hInstance_;

        HWND hwnd_;

 

        D3D_DRIVER_TYPE driverType_;

        D3D_FEATURE_LEVEL featureLevel_;

 

        ID3D11Device* d3dDevice_;

        ID3D11DeviceContext* d3dContext_;

        IDXGISwapChain* swapChain_;

        ID3D11RenderTargetView* backBufferTarget_;

};

 

#endif

 


代码段十 Dx11DemoBase.cpp

#include"Dx11DemoBase.h"

#include<D3Dcompiler.h>

 

 

Dx11DemoBase::Dx11DemoBase( ) : driverType_( D3D_DRIVER_TYPE_NULL ), featureLevel_( D3D_FEATURE_LEVEL_11_0 ),

                                d3dDevice_( 0 ), d3dContext_( 0 ), swapChain_( 0 ), backBufferTarget_( 0 )

{

 

}

 

 

Dx11DemoBase::~Dx11DemoBase( )

{

    Shutdown( );

}

 

 

bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )

{

    hInstance_ = hInstance;

    hwnd_ = hwnd;

 

    RECT dimensions;

    GetClientRect( hwnd, &dimensions );

 

    unsigned int width = dimensions.right - dimensions.left;

    unsigned int height = dimensions.bottom - dimensions.top;

 

    D3D_DRIVER_TYPE driverTypes[] =

    {

        D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP,

        D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE

    };

 

    unsigned int totalDriverTypes = ARRAYSIZE( driverTypes );

 

    D3D_FEATURE_LEVEL featureLevels[] =

    {

        D3D_FEATURE_LEVEL_11_0,

        D3D_FEATURE_LEVEL_10_1,

        D3D_FEATURE_LEVEL_10_0

    };

 

    unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels );

 

    DXGI_SWAP_CHAIN_DESC swapChainDesc;

    ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) );

    swapChainDesc.BufferCount = 1;

    swapChainDesc.BufferDesc.Width = width;

    swapChainDesc.BufferDesc.Height = height;

    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

    swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;

    swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

    swapChainDesc.OutputWindow = hwnd;

    swapChainDesc.Windowed = true;

    swapChainDesc.SampleDesc.Count = 1;

    swapChainDesc.SampleDesc.Quality = 0;

 

    unsigned int creationFlags = 0;

 

#ifdef _DEBUG

    creationFlags |= D3D11_CREATE_DEVICE_DEBUG;

#endif

 

    HRESULT result;

    unsigned int driver = 0;

 

    for( driver = 0; driver < totalDriverTypes; ++driver )

    {

        result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags,

                                                featureLevels, totalFeatureLevels,

                                                D3D11_SDK_VERSION, &swapChainDesc, &swapChain_,

                                                &d3dDevice_, &featureLevel_, &d3dContext_ );

 

        if( SUCCEEDED( result ) )

        {

            driverType_ = driverTypes[driver];

            break;

        }

    }

 

    if( FAILED( result ) )

    {

        DXTRACE_MSG( "创建D3D设备失败!" );

        return false;

    }

 

    ID3D11Texture2D* backBufferTexture;

 

    result = swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture );

 

    if( FAILED( result ) )

    {

        DXTRACE_MSG( "获取交换链后台缓存失败!" );

        return false;

    }

 

    result = d3dDevice_->CreateRenderTargetView( backBufferTexture, 0, &backBufferTarget_ );

 

    if( backBufferTexture )

        backBufferTexture->Release( );

 

    if( FAILED( result ) )

    {

        DXTRACE_MSG( "创建渲染目标视图失败!" );

        return false;

    }

 

    d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0 );

 

    D3D11_VIEWPORT viewport;

    viewport.Width = static_cast<float>(width);

    viewport.Height = static_cast<float>(height);

    viewport.MinDepth = 0.0f;

    viewport.MaxDepth = 1.0f;

    viewport.TopLeftX = 0.0f;

    viewport.TopLeftY = 0.0f;

 

    d3dContext_->RSSetViewports( 1, &viewport );

 

    return LoadContent( );

}

 

 

bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer )

{

    DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

 

#if defined( DEBUG ) || defined( _DEBUG )

    shaderFlags |= D3DCOMPILE_DEBUG;

#endif

 

    ID3DBlob* errorBuffer = 0;

    HRESULT result;

 

    result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,

        shaderFlags, 0, 0, buffer, &errorBuffer, 0 );

 

    if( FAILED( result ) )

    {

        if( errorBuffer != 0 )

        {

            OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );

            errorBuffer->Release( );

        }

 

        return false;

    }

    

    if( errorBuffer != 0 )

        errorBuffer->Release( );

 

    return true;

}

 

 

bool Dx11DemoBase::LoadContent( )

{

    // 进行相关重载

    return true;

}

 

 

void Dx11DemoBase::UnloadContent( )

{

    // 进行相关重载

}

 

 

void Dx11DemoBase::Shutdown( )

{

    UnloadContent( );

 

    if( backBufferTarget_ ) backBufferTarget_->Release( );

    if( swapChain_ ) swapChain_->Release( );

    if( d3dContext_ ) d3dContext_->Release( );

    if( d3dDevice_ ) d3dDevice_->Release( );    

 

    backBufferTarget_ = 0;

    swapChain_ = 0;

    d3dContext_ = 0;

    d3dDevice_ = 0;

}


四、 赋予程序生命——wWinMain函数的书写

最后一步,依旧是主函数的书写,这个函数大家应该是最熟悉的,任何想顺利运行的C/C++程序中必不可少,而且这里的书写方式和前面demo中的也大同小异,浅墨在这里就不多赘言了,代码如下:

代码段十一 wWinMain函数的书写

#include<Windows.h>

#include<memory>

#include"TriangleDemo.h"

 

 

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

 

 

int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow )

{

    UNREFERENCED_PARAMETER( prevInstance );

    UNREFERENCED_PARAMETER( cmdLine );

 

    WNDCLASSEX wndClass = { 0 };

    wndClass.cbSize = sizeof( WNDCLASSEX ) ;

    wndClass.style = CS_HREDRAW | CS_VREDRAW;

    wndClass.lpfnWndProc = WndProc;

    wndClass.hInstance = hInstance;

    wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );

    wndClass.hbrBackground = ( HBRUSH )( COLOR_WINDOW + 1 );

    wndClass.lpszMenuName = NULL;

    wndClass.lpszClassName = "DX11BookWindowClass";

 

    if( !RegisterClassEx( &wndClass ) )

        return -1;

 

    RECT rc = { 0, 0, 640, 480 };

    AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );

 

    HWND hwnd = CreateWindowA( "DX11BookWindowClass", "Direct3D11三角形demo", WS_OVERLAPPEDWINDOW,

                                CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,

                                NULL, NULL, hInstance, NULL );

 

    if( !hwnd )

        return -1;

 

    ShowWindow( hwnd, cmdShow );

 

    TriangleDemo demo;

 

    // Demo 的初始化

    bool result = demo.Initialize( hInstance, hwnd );

 

    if( result == false )

        return -1;

 

    MSG msg = { 0 };

 

    while( msg.message != WM_QUIT )

    {

        if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )

        {

            TranslateMessage( &msg );

            DispatchMessage( &msg );

        }

 

        // 更新和绘制

        demo.Update( 0.0f );

        demo.Render( );

    }

 

    // Demo 关闭

    demo.Shutdown( );

 

    return static_cast<int>( msg.wParam );

}

 

 

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )

{

    PAINTSTRUCT paintStruct;

    HDC hDC;

 

    switch( message )

    {

        case WM_PAINT:

            hDC = BeginPaint( hwnd, &paintStruct );

            EndPaint( hwnd, &paintStruct );

            break;

 

        case WM_DESTROY:

            PostQuitMessage( 0 );

            break;

 

        default:

            return DefWindowProc( hwnd, message, wParam, lParam );

    }

 

    return 0;

}


用上面这些代码就可以在屏幕上输出一个绿色的三角形,源码以及工程文件在文章末尾有链接提供打包下载。

最后得到这个DirectX11三角形demo的效果图:


本篇文章配套的源代码请点击这里下载: 【Visual C++】Note_Code_29




文章最后和大家讲些题外话,这是浅墨回国度假后专栏的第一次更新,回国度假到8月25号。

因为欧洲与国内的五个小时时差问题,回家之后一直在倒时差,白天睡的香,晚上睡不着,刚开始还是比较难受的- -,目前基

本上生物钟正常了,可以早上起来多锻炼身体和看书写代码了。

关于后面接着几篇文章的更新,因为浅墨回国了,更新时间会有所更改,暂时定在每周周一的中午。

感谢一直支持【Visual C++】游戏开发笔记系列专栏的朋友们。

【Visual C++】游戏开发 系列文章才刚刚展开一点而已,因为游戏世界实在是太博大精深了~

但我们不能着急,得慢慢打好基础。做学问最忌好高骛远,不是吗?

浅墨希望看到大家的留言,希望与大家共同交流,希望得到睿智的评论(即使是批评)。

你们的支持是我写下去的动力~

精通游戏开发的路还很长很长,非常希望能和大家一起交流,共同学习,共同进步。

大家看过后觉得值得一看的话,可以顶一下这篇文章,你们的支持是我继续写下去的动力~

如果文章中有什么疏漏的地方,也请大家指正。也希望大家可以多留言来和我探讨相关的问题。

最后,谢谢你们一直的支持~~~

——————————浅墨于2012年7月23日


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics