Direct3D轮回:游戏场景之陆地

继天空效果之后,这一节简单阐述一点地形生成的基本原理和方法~ 如果哪里说的不对,还望园子的前辈们多多拍砖 ^ ^

首先,我们准备两张图:

Direct3D轮回:游戏场景之陆地 Direct3D轮回:游戏场景之陆地

grass.dds heightdata.raw

(注:dds为directx支持的特定格式;raw可由photoshop自动生成,在无文件头标准下,其内部仅包含各点颜色值,不再含有其他冗余数据。)

左边这张草地的图我们作基本的地表纹理只用;右边这张怪模怪样的灰度图,其实就是日常大家口中的“高度图”了,我们以其特定点的颜色数据为依据来形成相应顶点的高度,以产生地形的高低错落感~

以下,我们来编写这个基本的地形类——CBaseTerrain:
/-------------------------------------



代码清单:BaseTerrain.h

来自:
http://www.cnblogs.com/kenkao



-------------------------------------
/



#include
"D3DInit.h"



#pragmaonce



classCBaseTerrain

{

public:

CBaseTerrain(
void);

~CBaseTerrain(void);

public:

boolCreate(//构建地形

intiSizeX,//原始地形图宽

intiSizeY,//原始地形图高

intiRate,//放大倍率

charszHeightRaw,//原始地形图

char
szTexture//地形纹理

);

voidDraw();//绘制地形

voidRelease();//资源释放

floatGetExactHeightAt(floatxCoord,floatzCoord);//获得地形高度

private:

boolLoadHightData(intiSizeX,intiSizeY,charszHeightRaw);//加载地形图

floatGetHeightData(intX,intY){return(float)m_pHeightData[ m_SizeX
Y+X ];}//获得地形图数据

voidCreateTerrainVertices();//产生顶点缓冲

voidCreateTerrainIndices();//产生索引缓冲

voidGenerateNormals(VertexPosNorColorTexvertices,intindices);//计算全部法线数据

private:

VertexPosNorColorTex
m_pVertices;//顶点缓冲区

int
m_pIndices;//索引缓冲区

IDirect3DTexture9m_pTexture;//地形纹理指针

unsignedchar
m_pHeightData;//地形数据缓冲

intm_SizeX;//地形原始长

intm_SizeY;//地形原始宽

intm_Rate;//地形放大倍率

};Direct3D轮回:游戏场景之陆地Direct3D轮回:游戏场景之陆地BaseTerrain.cpp/-------------------------------------



代码清单:BaseTerrain.cpp

来自:
http://www.cnblogs.com/kenkao



-------------------------------------
/



#include
"StdAfx.h"

#include
"BaseTerrain.h"

#include
"Ken3DGame.h"

#include
"D3DCamera.h"

#include
<fstream>



externIDirect3DDevice9g_pD3DDevice;

externCD3DCamera
g_pD3DCamera;



CBaseTerrain::CBaseTerrain(
void) : m_pVertices(NULL),

m_pIndices(NULL),

m_pTexture(NULL),

m_pHeightData(NULL),

m_SizeX(
0),

m_SizeY(
0),

m_Rate(
1)

{

}



CBaseTerrain::
~CBaseTerrain(void)

{

}



voidCBaseTerrain::Release()

{

delete[] m_pHeightData;

ReleaseCOM(m_pTexture);

delete[] m_pVertices;

delete[] m_pIndices;

}



boolCBaseTerrain::Create(intiSizeX,intiSizeY,intiRate,charszHeightRaw,charszTexture)

{

m_Rate
=iRate;

//加载高度数据

if(!LoadHightData(iSizeX,iSizeY,szHeightRaw))

returnfalse;

//产生顶点缓冲

CreateTerrainVertices();

//产生索引缓冲

CreateTerrainIndices();

//计算发现数据

GenerateNormals(m_pVertices,m_pIndices);

//产生地形纹理

HRESULT hr=D3DXCreateTextureFromFile(g_pD3DDevice,szTexture,&m_pTexture);

if(FAILED(hr))

returnfalse;

returntrue;

}



voidCBaseTerrain::Draw()

{

//地形绘制

g_pD3DDevice->SetTexture(0,m_pTexture);

g_pD3DDevice
->SetFVF(VertexPosNorColorTex::FVF);

g_pD3DDevice
->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST,0, m_SizeXm_SizeY, (m_SizeX-1)(m_SizeY-1)2,&m_pIndices[0],

D3DFMT_INDEX32,
&m_pVertices[0],sizeof(VertexPosNorColorTex));

}



boolCBaseTerrain::LoadHightData(intiSizeX,intiSizeY,char
szHeightRaw)

{

m_SizeX
=iSizeX;

m_SizeY
=iSizeY;

m_pHeightData
=newunsignedchar[iSizeXiSizeY];

memset(m_pHeightData,
0, iSizeX
iSizeY);

//从.raw地形图读取高度数据

std::ifstream inFile(szHeightRaw, std::ios_base::binary);

if(!inFile)

{

delete[] m_pHeightData;

returnfalse;

}

inFile.read((
char)&m_pHeightData[0], iSizeXiSizeY);

inFile.close();

returntrue;

}



voidCBaseTerrain::CreateTerrainVertices()

{

//产生顶点缓冲

//高度图中有多少数据便产生多少顶点,或者你也可以仅仅使用其中的一部分数据,这个很好理解 :)

m_pVertices=newVertexPosNorColorTex[m_SizeXm_SizeY];

inti=0;

for(intz=0; z<m_SizeY; z++)

{

for(intx=0; x<m_SizeX; x++)

{

floaty=GetHeightData(x,z);

m_pVertices[i
++]=VertexPosNorColorTex(

x
m_Rate, y, zm_Rate,//注意x、z坐标m_Rate倍率的使用,相应的y值即为由地形图获得的高度数据

0,0,0,

D3DXCOLOR_WHITE,

(
float)x/30.0f, (float)z/30.0f);

}

}

}



voidCBaseTerrain::CreateTerrainIndices()

{

//产生全部的索引数据

/


------

| / | / |

------

| / | / |

------



索引缓冲包含的元素个数 = 三角形个数 x 3

N 行顶点形成 N-1 行“三角形【对】”,每行三角形对个数为列数 M -1,则总的索引缓冲数 = ( N - 1 ) * ( M - 1 ) * 2 * 3

--------- --------- --- ---

行数 - 1 列数 - 1 三角形对 每个三角形三个顶点

/

m_pIndices
=newint[(m_SizeX-1)(m_SizeY-1)6];

inti=0;

for(intz=0; z<m_SizeY-1; z++)

{

for(intx=0; x<m_SizeX-1; x++)

{

//分别为每【对】三角形的顶点索引赋值,注意一下不要被“背面剔除”就可以了 ^ ^

m_pIndices[i++]=zm_SizeX+x;

m_pIndices[i
++]=(z+1)
m_SizeX+x;

m_pIndices[i
++]=zm_SizeX+x+1;



m_pIndices[i
++]=(z+1)
m_SizeX+x;

m_pIndices[i
++]=(z+1)m_SizeX+x+1;

m_pIndices[i
++]=z
m_SizeX+x+1;

}

}

}



voidCBaseTerrain::GenerateNormals(VertexPosNorColorTexpVertices,intpIndices)

{

//遍历每个三角形

for(inti=0; i<(m_SizeX-1)(m_SizeY-1)2; i++)

{

//获得第一条向量

D3DXVECTOR3 firstVec=D3DXVECTOR3(

pVertices[pIndices[i
3+1]]._x-pVertices[pIndices[i3]]._x,

pVertices[pIndices[i
3+1]]._y-pVertices[pIndices[i3]]._y,

pVertices[pIndices[i
3+1]]._z-pVertices[pIndices[i3]]._z

);

//获得第二条向量

D3DXVECTOR3 secondVec=D3DXVECTOR3(

pVertices[pIndices[i
3+2]]._x-pVertices[pIndices[i3]]._x,

pVertices[pIndices[i
3+2]]._y-pVertices[pIndices[i3]]._y,

pVertices[pIndices[i
3+2]]._z-pVertices[pIndices[i3]]._z

);

//两向量叉乘的该三角形片面的发现数据

D3DXVECTOR3 normal;

D3DXVec3Cross(
&normal,&firstVec,&secondVec);

D3DXVec3Normalize(
&normal,&normal);



//所得法线数据单位化并累计到组成此片面的三个顶点的法线数据中

D3DXVECTOR3 nfirstVec=D3DXVECTOR3(

pVertices[pIndices[i
3]]._nx+=normal.x,

pVertices[pIndices[i
3]]._ny+=normal.y,

pVertices[pIndices[i
3]]._nz+=normal.z

);

D3DXVECTOR3 nsecondVec
=D3DXVECTOR3(

pVertices[pIndices[i
3+1]]._nx+=normal.x,

pVertices[pIndices[i
3+1]]._ny+=normal.y,

pVertices[pIndices[i
3+1]]._nz+=normal.z

);

D3DXVECTOR3 nthirdVec
=D3DXVECTOR3(

pVertices[pIndices[i
3+2]]._nx+=normal.x,

pVertices[pIndices[i
3+2]]._ny+=normal.y,

pVertices[pIndices[i
3+2]]._nz+=normal.z

);

}



//遍历全部顶点

for(inti=0;i<m_SizeX
m_SizeY;i++)

{

//单位化全部顶点的法线数据

D3DXVECTOR3 nVec;

D3DXVec3Normalize(
&nVec,

&D3DXVECTOR3(pVertices[i]._nx, pVertices[i]._ny, pVertices[i]._nz)

);

pVertices[i]._nx
=nVec.x;

pVertices[i]._ny
=nVec.y;

pVertices[i]._nz
=nVec.z;

}

//以上,基本原理即基于所有三角形片面计算全部顶点法线数据而后求平均 :)

}



floatCBaseTerrain::GetExactHeightAt(floatxCoord,floatzCoord)

{

//还原倍率

xCoord/=m_Rate;

zCoord
/=m_Rate;



//非法判定

boolinvalid=xCoord<0;

invalid
|=zCoord<0;

invalid
|=xCoord>m_SizeX-1;

invalid
|=zCoord>m_SizeY-1;

if(invalid)

return10;//默认高度10



//获得该点所在三角形【对】四点对应高度

intxLower=(int)xCoord;

intxHigher=xLower+1;

floatxRelative=(xCoord-xLower)/((float)xHigher-(float)xLower);

intzLower=(int)zCoord;

intzHigher=zLower+1;

floatzRelative=(zCoord-zLower)/((float)zHigher-(float)zLower);

floatheightLxLz=GetHeightData(xLower, zLower);

floatheightLxHz=GetHeightData(xLower, zHigher);

floatheightHxLz=GetHeightData(xHigher, zLower);

floatheightHxHz=GetHeightData(xHigher, zHigher);



//判断该点位于三角形对中的哪个,并根据该三角形三点高度进行双线性插值计算最终高度

boolpointAboveLowerTriangle=(xRelative+zRelative<1);

floatfinalHeight;

if(pointAboveLowerTriangle)

{

finalHeight
=heightLxLz;

finalHeight
+=zRelative(heightLxHz-heightLxLz);

finalHeight
+=xRelative
(heightHxLz-heightLxLz);

}

else

{

finalHeight
=heightHxHz;

finalHeight
+=(1.0f-zRelative)(heightHxLz-heightHxHz);

finalHeight
+=(1.0f-xRelative)
(heightLxHz-heightHxHz);

}

returnfinalHeight;

}

代码看起来挺长,不过其中并没有什么高深的机制 ^ ^

LoadHightData()用于加载高度图数据。

函数中使用的是最基本的C++读取文件二进制数据的方法,意在将raw存储的高度数据导入到内存的unsigned char型缓冲区中,已供生成顶点高度(也就是y坐标)之用~

CreateTerrainVertices()用于产生全部的顶点数据。

很明显,raw中有多少像素,我们这里便产生多少顶点即可。我在代码中提到的放大倍率,其实就是顶点与顶点间的间隔而已~

CreateTerrainIndices()用于产生全部的索引数据。

我在代码中画的那个简陋的图图不知道大家看懂没有,呵呵~ 简单而言就是索引数据永远是三角形个数的3倍,而三角形对数永远是 (行数-1)*(列数-1)。各索引值取得皆有公式,龙书中有详细的阐述,大家看不懂的话可以留言~

GenerateNormals()用于为所有顶点生成法线数据。

要实现地形的光照效果,顶点法线是必不可少的数据成分。单个顶点的法线其实就是其所在三角形面的法线,如果一个顶点为多个三角形共有,则累加之后单位化即可(相当于求平均)~

GetExactHeightAt()用于求取某点的高度。

高度图中仅存有各顶点的高度数据,而地形中并不是每个点都位于顶点的位置。如果某点位于顶点与顶点之间,则我们需要采取双线性差值的方法求得改点合适的高度,其中的原理跟高中学过的相似三角形类似,挺简单~

有关双线性差值的相关理论,可以参看老师为大家翻译的这篇文章:http://shiba.hpe.sh.cn/jiaoyanzu/WULI/showArticle.aspx?articleId=473&classId=4

之后是主体代码:
Direct3D轮回:游戏场景之陆地Direct3D轮回:游戏场景之陆地D3DGame.cpp/-------------------------------------



代码清单:D3DGame.cpp

来自:
http://www.cnblogs.com/kenkao



-------------------------------------
/



#include
"StdAfx.h"

#include
"D3DGame.h"

#include
"D3DCamera.h"

#include
"D3DEffect.h"

#include
"CoordCross.h"

#include
"SimpleXMesh.h"

#include
"Texture2D.h"

#include
"D3DSprite.h"

#include
"Skybox.h"

#include
"SpriteBatch.h"

#include
"BaseTerrain.h"

#include
<stdio.h>



//---通用全局变量



HINSTANCE g_hInst;

HWND g_hWnd;

D3DXMATRIX g_matProjection;



//---D3D全局变量



IDirect3D9
g_pD3D=NULL;

IDirect3DDevice9
g_pD3DDevice=NULL;

CMouseInput
g_pMouseInput=NULL;

CKeyboardInput
g_pKeyboardInput=NULL;

CD3DCamera
g_pD3DCamera=NULL;

CCoordCross
g_pCoordCross=NULL;

CSimpleXMesh
g_pSimpleXMesh=NULL;

CD3DEffect
g_pD3DEffect=NULL;

CD3DSprite
g_pD3DSprite=NULL;

CTexture2D
g_pTexture2D=NULL;

CSpriteBatch
g_SpriteBatch=NULL;

CTexture2D
g_pTexture2D2=NULL;

CD3DEffect
g_pD3DEffect2=NULL;

CSkybox
g_pSkybox=NULL;

CBaseTerrain
g_pBaseTerrain=NULL;





//---HLSL全局变量句柄



D3DXHANDLE g_CurrentTechHandle
=NULL;

D3DXHANDLE g_matWorldViewProj
=NULL;

D3DXHANDLE g_matWorld
=NULL;

D3DXHANDLE g_vecEye
=NULL;

D3DXHANDLE g_vecLightDir
=NULL;

D3DXHANDLE g_vDiffuseColor
=NULL;

D3DXHANDLE g_vSpecularColor
=NULL;

D3DXHANDLE g_vAmbient
=NULL;



D3DXHANDLE g_CurrentTechHandle2
=NULL;

D3DXHANDLE g_Scale
=NULL;



//HLSL特效参数设置

voidGetParameters();

voidSetParameters();





voidInitialize(HINSTANCE hInst, HWND hWnd)

{

g_hInst
=hInst;

g_hWnd
=hWnd;

InitD3D(
&g_pD3D,&g_pD3DDevice, g_matProjection, hWnd);

g_pMouseInput
=newCMouseInput;

g_pMouseInput
->Initialize(hInst,hWnd);

g_pKeyboardInput
=newCKeyboardInput;

g_pKeyboardInput
->Initialize(hInst,hWnd);

g_pD3DCamera
=newCD3DCamera;

}



voidLoadContent()

{

g_pCoordCross
=newCCoordCross;



g_pD3DCamera
->SetCameraPos(D3DXVECTOR3(0.0f,0.0f,0.0f));



g_pSimpleXMesh
=newCSimpleXMesh;

g_pSimpleXMesh
->LoadXMesh("teapot.X");



g_pD3DEffect
=newCD3DEffect;

g_pD3DEffect2
=newCD3DEffect;

g_pD3DEffect
->LoadEffect("Light.fx");

g_pD3DEffect2
->LoadEffect("Thunder.fx");



GetParameters();



g_pD3DSprite
=newCD3DSprite(g_pD3DDevice);

g_SpriteBatch
=newCSpriteBatch(g_pD3DDevice);



g_pTexture2D
=newCTexture2D;

g_pTexture2D
->LoadTexture("img.jpg");

g_pTexture2D2
=newCTexture2D;

g_pTexture2D2
->LoadTexture("img2.jpg");



g_pSkybox
=newCSkybox;

g_pSkybox
->Create("Skybox_0.JPG","Skybox_1.JPG","Skybox_2.JPG"

,
"Skybox_3.JPG","Skybox_4.JPG","Skybox_5.JPG");



g_pBaseTerrain
=newCBaseTerrain;

g_pBaseTerrain
->Create(128,128,10,"HeightData_128x128.raw","Grass.dds");

}



voidUpdate()

{

g_pMouseInput
->GetState();

g_pKeyboardInput
->GetState();

//更新摄影机高度

D3DXVECTOR3 CameraPos=g_pD3DCamera->GetCameraPos();

floatroleHeight=25.0f;

floatTy=g_pBaseTerrain->GetExactHeightAt(CameraPos.x,CameraPos.z)+roleHeight;

g_pD3DCamera
->SetCameraPos(D3DXVECTOR3(

CameraPos.x,

Ty,

CameraPos.z));

g_pD3DCamera
->Update();

//更新g_pD3DEffect中的视点位置

D3DXVECTOR4 vecEye=D3DXVECTOR4(CameraPos.x,Ty,CameraPos.z,0.0f);

g_pD3DEffect
->GetEffect()->SetVector(g_vecEye,&vecEye);

}



voidDraw()

{

//参数设定

SetParameters();

g_pD3DDevice
->SetTransform(D3DTS_VIEW,&g_pD3DCamera->GetViewMatrix());



g_pD3DDevice
->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_RGBA(100,149,237,255),1.0f,0);

if(SUCCEEDED(g_pD3DDevice->BeginScene()))

{

//天空处在无限远处,因此必须最先绘制天空盒

g_pSkybox->Draw();



UINT numPasses;

//开启特效

g_pD3DEffect->BeginEffect(numPasses);

for(UINT i=0;i<numPasses;i++)

{

//开启路径

g_pD3DEffect->GetEffect()->BeginPass(i);

//绘制地形

g_pBaseTerrain->Draw();

//路径结束

g_pD3DEffect->GetEffect()->EndPass();

}

//特效结束

g_pD3DEffect->EndEffect();



g_pD3DDevice
->EndScene();

}

g_pD3DDevice
->Present(NULL, NULL, NULL, NULL);

}



voidUnloadContent()

{

ReleaseCOM(g_pBaseTerrain);

ReleaseCOM(g_pSkybox);

ReleaseCOM(g_pTexture2D2);

ReleaseCOM(g_pTexture2D);

ReleaseCOM(g_SpriteBatch);

ReleaseCOM(g_pD3DSprite);

ReleaseCOM(g_pD3DEffect2);

ReleaseCOM(g_pD3DEffect);

ReleaseCOM(g_pSimpleXMesh);

ReleaseCOM(g_pCoordCross);

}



voidDispose()

{

ReleaseCOM(g_pD3DCamera);

ReleaseCOM(g_pKeyboardInput);

ReleaseCOM(g_pMouseInput);

ReleaseCOM(g_pD3DDevice);

ReleaseCOM(g_pD3D);

}



voidGetParameters()

{

//获得HLSL中各个全局变量句柄

g_CurrentTechHandle=g_pD3DEffect->GetEffect()->GetTechniqueByName("SpecularLight");

g_matWorldViewProj
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"matWorldViewProj");

g_matWorld
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"matWorld");

g_vecEye
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"vecEye");

g_vecLightDir
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"vecLightDir");

g_vDiffuseColor
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"vDiffuseColor");

g_vSpecularColor
=g_pD3DEffect->GetEffect()->GetParameterByName(0,"vSpecularColor");g_vAmbient=g_pD3DEffect->GetEffect()->GetParameterByName(0,"vAmbient");



g_CurrentTechHandle2
=g_pD3DEffect2->GetEffect()->GetTechniqueByName("Technique1");

g_Scale
=g_pD3DEffect2->GetEffect()->GetParameterByName(0,"Scale");

}



voidSetParameters()

{

//设定当前技术

g_pD3DEffect->GetEffect()->SetTechnique(g_CurrentTechHandle);

//设定HLSL中的各个参数

D3DXMATRIX worldMatrix;

D3DXMatrixTranslation(
&worldMatrix,0.0f,0.0f,0.0f);

g_pD3DEffect
->GetEffect()->SetMatrix(g_matWorldViewProj,&(worldMatrix
g_pD3DCamera->GetViewMatrix()*g_matProjection));

g_pD3DEffect
->GetEffect()->SetMatrix(g_matWorld,&worldMatrix);



D3DXVECTOR4 vLightDirection
=D3DXVECTOR4(-5.0f,-1.0f,2.0f,1.0f);

g_pD3DEffect
->GetEffect()->SetVector(g_vecLightDir,&vLightDirection);

D3DXVECTOR4 vColorDiffuse
=D3DXVECTOR4(0.0f,0.0f,0.0f,1.0f);

D3DXVECTOR4 vColorSpecular
=D3DXVECTOR4(1.0f,1.0f,1.0f,1.0f);

D3DXVECTOR4 vColorAmbient
=D3DXVECTOR4(0.0f,0.0f,0.0f,1.0f);

g_pD3DEffect
->GetEffect()->SetVector(g_vDiffuseColor,&vColorDiffuse);

// g_pD3DEffect
->GetEffect()->SetVector(g_vSpecularColor,&vColorSpecular); //暂时不加入镜面高光 ^ ^

g_pD3DEffect
->GetEffect()->SetVector(g_vAmbient,&vColorAmbient);



g_pD3DEffect2
->GetEffect()->SetTechnique(g_CurrentTechHandle2);

g_pD3DEffect2
->GetEffect()->SetFloat(g_Scale,0.8f);

}

我们使用light.fx为新生成的地形添加光照效果。这里我在其原有基础上作了改动。

首先添加一个纹理采样器:

sampler2D TextureSampler = sampler_state

{

magfilter=LINEAR;

minfilter=LINEAR;

mipfilter=LINEAR;

};

然后让像素着色器最终的输出结果累计地形纹理原有的颜色:

return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular + tex2D(TextureSampler, vtop.uv); //---最后的颜色 = 环境光 + 漫射光 + 镜面高光 + 本来颜色(纹理)

我们来看看效果:

Direct3D轮回:游戏场景之陆地 Direct3D轮回:游戏场景之陆地

山地跟沟壑~ 都是高度图的功劳,呵呵~

主体代码中我注释掉了镜面光的颜色赋值,因为按理说草地是不会反射阳光的。我们不妨换一张冷峻的岩石地表,然后再打开镜面光,试试看效果如何~

Direct3D轮回:游戏场景之陆地

怎么样,很耀眼吧?呵呵 ^ ^

到此,以上阐述了地形生成方法中的诸多基本原理,但如果要用于生成大型的地形或者无边界地形的话,仅仅靠这样简单的方法是不堪重负的。不过大家无需担心,地形生成的基本原理大致都是相同的,只不过复杂的大地形在此基础上加入了一些特殊的算法和机制,以动态数据的方法减轻了CPU和显卡的负担。如果后续有机会深度研究这些,我会尽我所能,为大家一一道来~

谢谢~ ^ ^
原文链接: https://www.cnblogs.com/kenkao/archive/2011/07/23/2114767.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/29227

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 上午6:38
下一篇 2023年2月8日 上午6:39

相关推荐