#Direct3D的初始化(下) ##学习目标编程
##4.3初始化Direct3D 对Direct3D进行初始化能够分为如下几个步骤数组
##4.3.1建立设备 要初始化Direct3D,必须先建立Direct3D12设备。Direct3D12设备至关于一个显示适配器,显示适配器通常都是一种3D图像硬件(如显卡),但也能够用软件显示适配器来模拟3D图形硬件功能,该设备能够检测系统环境对功能的支持状况,又能够用来建立全部其余的Direct3D接口对象(如资源,命令列表,视图(描述符)等等)。咱们能够经过一下函数建立Direct3D12设备:缓存
//@param:指定在建立设备的时候所用的显示适配器,若是把该指针设为空,则默认使用主显示适配器 //@param:应用程序须要硬件所支持的最低功能级别 //@parma:该ID3DDevice接口的COM ID //@parma:返回所建立的Direct3D12设备 HRESULT WINAPI mD3D12CreateDevice( IUnknown* pAdapter, D3D_FEATURE_LEVEL MinimumFeatureLevel, REFIID riid, void** ppDevice );
##4.3.2建立围栏并获取描述符的大小 建立好设备以后,咱们即可觉得CPU和GPU的同步建立围栏了。另外若是须要使用描述符进行工做,咱们还须要了解它们的大小。但描述符在不一样的GPU上大小是不一样的,因此须要咱们在建立围栏的时候顺便去查询相关的信息,而后将描述符的大小缓存起来,以便在须要的时候直接进行引用。框架
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence))); //渲染目标视图(描述符)大小 mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); //深度/模板视图(描述符)大小 mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV); //常量缓冲区/着色器资源/无序访问视图(描述符)大小 mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
##4.3.3检测对4X MSAA质量级别的支持 凡是支持Direct3D11的硬件,均可以支持多重采样技术的开启。因此咱们能够不用对此进行检测,可是,对质量级别的检测仍是必不可少的。咱们能够采用下面的方法进行检测:函数
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels; msQualityLevels.Format = mBackBufferFormat; msQualityLevels.SampleCount = 4; msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; msQualityLevels.NumQualityLevels = 0; ThrowIfFailed(md3dDevice->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &msQualityLevels, sizeof(msQualityLevels)));
##4.3.4建立命令队列和命令列表 在前面的章节可知,ID3D12CommandQueue接口表示命令队列,ID3D12CommandAllocator接口表示命令分配器,ID3D12CommandList接口表示命令列表,下面咱们将分别展现这几种对象的建立流程:布局
ComPtr<ID3D12CommandQueue> mCommandQueue; ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc; ComPtr<ID3D12CommandList> mCommandList; void D3DApp::CreateCommandObjects() { //建立命令队列对象 D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue))); //建立命令分配器 ThrowIfFailed(md3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf()))); //建立命令列表 ThrowIfFailed(md3dDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mDirectCmdListAlloc.Get(), nullptr, IID_PPV_ARGS(mCommandList.GetAddressOf()))); //首先要把命令列表关闭,由于第一次使用命令列表时咱们要把命令列表重置,重置以前必须确保命令列表 //已经关闭 mCommandList->Close(); }
##4.3.5描述并建立交换链 在建立交换链以前,咱们要先填写一份DXGI_SWAP_CHAIN_DESC结构体实例,用它来描述即将建立的交换链的特性。此结构体定义以下:性能
typedef struct DXGI_SWAP_CHAIN_DESC { DXGI_MODE_DESC BufferDesc; DXGI_SAMPLE_DESC SampleDesc; DXGI_USAGE BufferUsage; UINT BufferCount; HWND OutputWindow; BOOL Windowed; DXGI_SWAP_EFFECT SwapEffect; UINT Flags; }DXGI_SWAP_CHAIN_DESC;
其中DXGI_MODE_DESC则是另外一个结构体,该结构体定义以下:学习
typedef struct DXGI_MODE_DESC { UINT Width; UINT Height; DXGI_RATIONAL refreshRate; DXGI_FORMAT Format; DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; DXGI_MODE_SCALING scaling; }DXGI_MODE_DESC;
下面的代码将会展现如何在本书的演示框架下方便的建立交换链:优化
void D3DApp::CreateSwapChain() { //释放以前建立的交换链,而后进行重建 mSwapChain.Reset(); DXGI_SWAP_CHAIN_DESC sd; sd.BufferDesc.Width = mClientWidth; sd.BufferDesc.Height = mClientHeight; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.BufferDesc.Format = mBackBufferFormat; sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; sd.SampleDesc.Count = m4xMsaaState ? 4 : 1; sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = SwapChainBufferCount; sd.OutputWindow = mhMainWnd; sd.Windowed = true; sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; //注意,交换链须要经过命令队列才能刷新 ThrowIfFailed(mdxgiFactory->CreateSwapChain(mCommandQueue.Get(), &sd, mSwapChain.GetAddressOf())); }
##4.3.6建立描述符堆 在程序中,咱们须要建立描述符堆来存储程序中须要用到的描述符(视图),在Direct12中,ID3D12DescriptorHeap接口表示描述符堆,并用ID3D12Device::CreateDescriptorHeap方法来建立描述符堆,在下面的演示代码中,咱们将建立两个描述符堆,一个用来存储SwapChainBufferCount个渲染目标视图(Render Target View),还有一个用来存储1个深度/模板视图(Depth/Stencil View)。动画
ComPtr<ID3D12DescriptorHeap> mRtvHeap; ComPtr<ID3D12DescriptorHeap> mDsvHeap; void D3DApp::CreateRtvAndDsvDescriptorHeaps() { D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc; rtvHeapDesc.NumDescriptors = SwapChainBufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; rtvHeapDesc.NodeMask = 0; ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf()))); D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc; dsvHeapDesc.NumDescriptors = 1; dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; dsvHeapDesc.NodeMask = 0; ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf()))); }
##4.3.7建立渲染目标视图 因为资源不能直接和渲染流水线直接进行绑定,因此咱们须要先为资源建立视图(描述符),并将其绑定到渲染流水线中。为了向后台缓冲区建立一个渲染目标视图,咱们须要先得到交换链中的缓冲区资源。
咱们能够经过IDXGISwapChain::GetBuffer()方法后去交换链中的缓冲区资源,每次调用该方法以后,会增长相关后台缓冲区的引用次数,因此在每一次使用后都要释放,咱们能够经过Comptr自动实现这个功能。
接下来,咱们可使用ID3D12Device::CreateRenderTargetView()方法来为获取的后台缓冲区资源建立渲染目标视图。
如下实例将会经过调用这两个方法为交换链中的每个缓冲区都建立一个RTV:
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount]; CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle( mRtvHeap->GetCPUDescriptorHandleForHeapStart() ); for (UINT i = 0; i < SwapChainBufferCount; i++) { //获取交换链中第i个缓冲区 ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i]))); //为此缓冲区建立一个RTV md3dDevice->CreateDepthStencilView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle); //偏移到描述符的下一个缓冲区 rtvHeapHandle.Offset(1, mRtvDescriptorSize); }
##4.3.8建立深度/模板缓冲区及其视图(描述符) 深度缓冲区是一种纹理资源,它存储着离观察者最近的可视对象的深度信息(若是使用了模板,还有附有模板信息)。由于纹理是一种资源,因此咱们须要经过填写D3D12_RESOURCE_DESC结构体来描述纹理资源,在使用ID3D12DeveiceCreateCommittedResource方法来建立它。如下代码为D3D12_RESOURCE_DESC结构体的定义:
typedef struct D3D12_RESOURCE_DESC { D3D12_RESOURCE_DIMENSION Dimension; UINT64 Alignment; UINT64 Width; UINT Height; UINT16 DepthOrArraySize; DXGI_FORMAT Format; DXGI_SAMPLE_DESC SampleDesc; D3D12_TEXTURE_LAYOUT Layout; D3D12_RESOURCE_FLAGS Flags; }D3D12_RESOURCE_DESC;
GPU资源都存于堆(Heap)中,其本质是具备特定属性的GPU显存块,ID3D12Device::CreateCommittedResource将根据咱们所提供的属性,建立一个资源和一个堆,并把该资源提交到这个堆中。
HRESULT ID3D12Device::CreateCommittedResource( const D3D12_HEAP_PROPERTIES * pHeapProperties, D3D12_HEAP_FLAGS HeapFlags, const D3D12_RESOURCE_DESC * pDesc, D3D12_RESOURCE_STATES InitialResourceState, const D3D12_CLEAR_VALUE * pOptimizedClearValue, REFIID riidResource, void ** ppvResource ); typedef struct D3D12_HEAP_PROPERTIES { D3D12_HEAP_TYPE Type; D3D12_CPU_PAGE_PROPERTY CPUPageProperty; D3D12_MEMORY_POOL MemoryPoolPreference; UINT CreationNodeMask; UINT VisibleNodeMask; }D3D12_HEAP_PROPERTIES;
在使用深度/模板缓冲区以前,必定要先建立相关的深度/模板缓冲区视图(描述符),并将它绑定到渲染流水线中。下面的代码将会展现如何建立深度/模板纹理资源以及相对应的深度/模板缓冲区视图(描述符)
//建立深度/模板缓冲区视图 D3D12_RESOURCE_DESC depthStencilDesc; //资源的维度 depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; depthStencilDesc.Alignment = 0; //以纹素为单位来表示纹理宽度(若是是缓冲区资源,此项表示缓冲区占用的字节数) depthStencilDesc.Width = mClientWidth; //以纹素为单位来表示纹理高度 depthStencilDesc.Height = mClientHeight; //以纹素为单位来表示纹理深度 depthStencilDesc.DepthOrArraySize = 1; //mipmap层级的数量(后续讲纹理时会介绍mipmap) depthStencilDesc.MipLevels = 1; //DXGI_FORMAT枚举类型中的成员之一,用于指定纹素的格式 depthStencilDesc.Format = mDepthStencilFormat; //多重采样的质量级别以及对每个像素的采样次数 depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1; depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; //D3D12_TEXTURE_LAYOUT枚举类型的成员之一,用来指定纹理的布局 depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; //与资源有关的杂项标志 depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; //建立一个指向一个D3D12_CLEAR_VALUE对象的指针,该指针描述了一个用于清除资源的优化值, //选择适当的优化值能够提升清除操做的效率,若是不但愿指定优化值,也能够不建立。 D3D12_CLEAR_VALUE optClear; optClear.Format = mDepthStencilFormat; optClear.DepthStencil.Depth = 1.0f; optClear.DepthStencil.Stencil = 0; //建立深度/模板缓冲区 ThrowIfFailed(md3dDevice->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &depthStencilDesc, D3D12_RESOURCE_STATE_COMMON, &optClear, IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf()) )); //利用此资源的格式,为整个资源的第0层mip建立描述符 md3dDevice->CreateDepthStencilView( mDepthStencilBuffer.Get(), nullptr, DepthStencilView() ); //将资源从初始状态转换到深度缓冲区 mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(), D3D12_RESOURCE_STATE_COMMON,D3D12_RESOURCE_STATE_DEPTH_WRITE));
##4.3.9设置视口 视口:咱们一般会把3D场景绘制到整个屏幕中,或整个窗口工做区大小至关的后台缓冲区中,可是,有些时候咱们只但愿把3D场景绘制到后台缓冲区中的某一个矩形子区域中,而这个矩形子区域就称为视口
视口的结构体定义以下:
typedef struct D3D12_VIEWPORT { FLOAT TopLeftX; FLOAT TopLeftY; FLOAT Width; FLOAT Height; FLOAT MinDepth; FLOAT MaxDepth; };
填写好D3D12_VIEWPORT结构体以后,咱们即可以经过ID3D12GraphicsComandList::RSSetViewPort()方法来设置Direct3D中的视口了。下面将会展现经过建立和设置一个视口,把场景绘制到整个后台缓冲区中
D3D12_VIEWPORT vp; vp.TopLeftX = 0.0f; vp.TopLeftY = 0.0f; vp.Width = static_cast<float>(mClientWidth); vp.Height = static_cast<float>(, mClientHeight); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; //@param:绑定的视口数量 //@param:指向视口数组的指针 mCommandList->RSSetViewports(1, &vp);
4.3.10设置裁剪矩形 咱们能够在相对于后台缓冲区定义一个裁剪矩形,在这个矩形以外的像素都不会被光栅化到后台缓冲区(被剔除),这个方法能够优化程序的性能。好比咱们在游戏界面放置了一个UI,咱们能够经过设置裁剪矩形使程序没必要对3D空间中那些被它遮挡的像素进行处理了。设置裁剪矩形和设置视口同样,要先填写一个D3D12_RECT结构体,该结构体由类型为RECT的D3D12结构体定义而成:
typedef struct tagRECT { LONG left; LONG top; LONG right; LONG bottom; }RECT;
在Direct3D中,要用ID3D12GraphicsCommandList::RSSetScisorRects方法来设置裁剪矩形,下面的实例展示了如何建立并设置一个覆盖后台缓冲区左上角四分之一区域的裁剪矩形
mScissorRect = { 0,0,mClientWidth / 2,mClientHeight / 2 }; mCommandList->RSSetScissorRects(1, &mScissorRect);
##4.4计时与动画 为了制做出精准的动画效果就须要精确到计量时间,特别是要准确的度量出动画每帧画面之间的时间间隔,因此了解相关的知识是十分必要的
##4.4.1性能计时器 为了精确的计量时间,咱们将采用性能计时器(performance timer),性能计时器的单位是计数,能够经过QueryPerformanceCounter函数来获取性能计时器测量的当前时刻值(以计数为单位)
用QueryPerformanceFrequency函数获取性能计时器的频率(单位:计数/秒),经过单位能够看出,若是但愿把QueryPerformanceCounter函数获取的时刻值的单位转换为秒,能够经过**时刻值(计数)/性能计时器的频率(计数/秒)**获得
##4.4.2游戏计时类
class GameTimer { public: GameTimer(); float TotlaTime()const; //以秒为单位(总时间,不计暂停的时间) float DeltaTime()const; //以秒为单位(本帧与前一帧的时间差) void Reset(); //在开始消息循环以前调用 void Start(); //解除计时器暂停时调用 void Stop(); //暂停计时器时调用 void Tick(); //每一帧都要调用 private: double mSecondPerCount; double mDeltaTime; __int64 mBastTime; //应用程序开始运行的时间 __int64 mPauseTime; //全部暂停时间的总和 __int64 mPauseTime; //暂停的时刻 __int64 mPrevTime; //前一帧的时刻 __int64 mCurrTime; //当前的时刻 bool mStopped; };
下面是游戏计时类的构造函数的代码:
GameTimer::GameTimer() :mSecondsPerCount(0.0f), mPrevTime(0), mCurrTime(0), mStopped(false) { __int64 countPersec; QueryPerformanceFrequency((LARGE_INTEGER*)&countPersec); mSecondsPerCount = 1.0 / (double)countPersec; }
剩下的一些方法咱们会在下面几节介绍。
##4.4.3帧与帧之间的间隔 当渲染动画帧时,咱们须要知道帧与帧之间的间隔,以此来根据时间的流逝对游戏对象进行更新。因此咱们要计算出前一帧t1和后一帧t2的差值,即t2 - t1。计算差值的代码以下:
void GameTimer::Tick() { if (mStopped) { mDeltaTime = 0.0f; return; } //得到本帧开始显示的时间 __int64 currTime; QueryPerformanceCounter((LARGE_INTEGER*)currTime); mCurrTime = currTime; //本帧与前一帧的时间差 mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount; //准备计算下一帧的时间差 mPrevTime = mCurrTime; if (mCurrTime < 0) { mCurrTime = 0; } } float GameTimer::DeltaTime()const { return (float)mDeltaTime; }
Tick函数被调用于应用程序的消息循环之中
int D3DApp::Run() { MSG msg = { 0 }; mTimer.Reset(); while (msg.message != WM_QUIT) { //若是有窗口信息就进行处理 if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } //不然就执行游戏逻辑 else { mTimer.Tick(); if (mAppPaused) { CalculateFrameStats(); Update(mTimer); Draw(mTimer); } else { Sleep(100); } } } }
下面是GameTimer::Reset()方法的实现:
void GameTimer::Reset() { __int64 currTime; QueryPerformanceCounter((LARGE_INTEGER*)&currTime); mBaseTime = currTime; mPrevTime = currTime; mPausedTime = 0; mStopped = false; }
##总时间 总时间:总时间是一种自应用程序开始,不计中间暂停时间的时间总和。下面代码将会分别展现游戏计时类的Stop(),Start()和TotalTime()三个方法。
void GameTimer::Stop() { //若是已经处于中止状态,则直接退出函数 if (mStopped) { return; } __int64 currTime; QueryPerformanceCounter((LARGE_INTEGER*)currTime); mStopTime = currTime; mStopped = true; } void GameTimer::Start() { __int64 startTime; QueryPerformanceCounter((LARGE_INTEGER*)startTime); //若是处于暂停状态,则更新相关变量 if (mStopped) { mPausedTime += (startTime - mStopTime); mPrevTime = startTime; mStopTime = 0; mStopped = false; } } float GameTimer::TotalTime()const { //若是是暂停状态 if (mStopped) { return (float)((mStopTime - mPausedTime) - mBaseTime)*mSecondsPerCount; } else { return (float)((mCurrTime - mPausedTime) - mBaseTime)*mSecondsPerCount; } }
##4.5应用程序框架 略
##4.5.1D3DApp类 D3DApp类是一种基础的Direct3D应用程序类,它提供了建立应用程序主窗口,运行程序消息循环,处理窗口信息以及初始化Direct3D等多种功能的函数,并且它为应用程序例程定义了一组框架函数,咱们能够根据需求经过实例化一个继承自D3DApp的类,重写框架里的虚函数,即可以从D3DApp类中派生出自定义的用户代码。若是想要查看D3DApp类的定义,能够在GitHub上自行查找。
##4.5.2非框架方法 略
##4.5.3框架内容 略
##4.5.4帧的统计信息 游戏和图形应用程序每每都会测量每秒渲染的帧数(frame per second,FPS)做为一种画面流畅度的标杆,所以,咱们须要统计在特定时间t内所处理的帧数n,则时间t内的平均帧数(FPS)为n/t。在Direct3D中,提供了D3DApp::CalculateFrameStats方法来计算FPS的相关信息。D3DAPP::CalculateFrameStats()方法的定义以下
void D3DApp::CalculateFrameStats() { static int frameCnt = 0; static float timeElapsed = 0; //以1秒为统计周期来计算每一帧的平均帧数以及每一帧所花费的渲染时间 if ((mTimer.TotalTime() - timeElapsed) >= 1.0f) { float fps = (float)frameCnt; //计算渲染一帧所花费的时间(以毫秒为单位) float mspf = 1000.0f / fps; wstring fpsStr = to_wstring(fps); wstring mspfStr = to_wstring(mspf); wstring windowText = mMainWndCaption + L"fps:" + fpsStr + L"mspf:" + mspfStr; //为计算下一帧重置相关值 frameCnt = 0; timeElapsed += 1.0f; } }
##4.5.5消息处理函数 接下来咱们将了解一下如何处理一些咱们要亲自处理的消息,实际上应用程序代码都是在没有窗口信息能够处理时执行的,可是有一些重要的信息须要咱们亲自去处理
一、WM_ACTIVATE:当一个程序被激活或者进入非活动状态时会发送此消息
二、WM_SIZE:当用户调整窗口的大小的时候会发送此消息,主要目的是可让咱们每次随着用户的调整而对后台缓冲区和深度/模板缓冲区的大小和工做区矩形范围的大小保持一致(这样便不会出现图形拉伸的bug了)
三、WM_ENTERSIZEMOVE:当用户抓取调整栏时发送WM_ENTERSIZEMOVE消息。
四、WM_EXITSIZEMOVE:当用户释放调整栏时发送WM_EXITSIZEMOVE消息。
五、WM_DESTROY:当窗口被销毁时发送WM_DESTROY消息
六、WM_LBUTTONDOWN/WM_MBUTTONDOWN/WM_RBUTTONDOWN:按下鼠标左键/中键/右键时
七、WM_LBUTTONUP/WM_MBUTTONUP/WN_RBUTTONUP:松开鼠标左键/中键/右键时