好久没写博客了,紧接着前面的博客,不过这次读取纹理的借口换为了DXUT框架里面的接口。

程序结构

如果没有学会ShadowMap,PCF软阴影原理的建议回去回顾下面几个教程:

Directx11教程三十一之ShadowMap(阴影贴图)之聚光灯光源成影

Directx11教程三十三之Soft Shadows—PCF(Percent closer Filter)

Directx11教程三十一之ShadowMap(阴影贴图)之平行光成影

程序的框架如下所示:

这里有关CascadeShadowMap的实现代码基本封装在CascasdeShadowMapManagerClass里。

CascadeShadowMap(层级阴影贴图)的原理思想

我们都知道,在实时渲染中,存在这样的一个渲染优化手段,也就是“LOD”,level of details,也就是细节等级的意思,总体来说也就是根据我们观察相机距离被观察物体的距离来决定优化被观察物体的渲染细节,比如说一个物体距离观察相机很远的时候,渲染Shader可以只采用最基本的DiffuseMap,并进行简单的光照计算,毕竟人眼睛会一定程序忽略远处物体的细节。而当该物体距离相机很近时,渲染Shader可以采用DiffuseMap,NormalMap,ParallaxMap等,并计算复杂的光照计算,以显示更多的细节。

CascadeShadowMap其实就是来源于LOD的思想,总体的原理: 将我们观察相机的视截体根据与相机空间的原点的远近距离来划分为多个部分(也就是所谓的层级Cascade),并将这几个部分的物体渲染到相应的ShadowMap。

因为CascasdeShadowMap主要是用于主光源的阴影投射的,也就是平行光,下面重点讲解平行光下的CascasdeShadowMap算法。

CascadeShadowMap(层级阴影贴图)的计算过程

视截体划根据远近距离分为多个层级:

先来看看我们的视截体:

我们视截体取横截面进行观察:

然后我们根据在相机空间下坐标点距离原点的远近,划分视截体为三个层级,其中箭头为平行光方向如下所示:

因为平行光的投影变换矩阵是正交投影矩阵(OrthoProjMatrix),从上面给出的几个教程我们知道:

平行光渲染ShadowMap的变换公式: objectPos*WorldMatrix*LightViewMatrix*OrthoProjMatrix,

这里毫无疑问 objectPos(物体的顶点局部空间位置),WorldMatrix(世界变换矩阵),LightViewMatrix(光源相机空间的变换矩阵)都是已经知道的。

那么下面我们要求的就是观察相机的视截体的Near,Middle,Far三个部分在平行光源的相机空间形成的OrthoProjMatrix(注意理解平行光的相机空间,我在前面的教程将形成阴影的光照比作投影相机),先看看普通情况的平行光x相机空间的OrthoProjMatrix,如下所示:

上面的箭头也就是平行光的方向,我选取平行光源的XZ平面来说明,上图中ABCD也就是平行光的相机空间代表正交投影矩阵的长方体在XZ平面的截面。这里代表正交投影矩阵的长方体其实是个AABB(轴对齐包围盒)

那么我们相机的视截体的Near,Middle,Far三个部分是如何在平行光的相机空间计算AABB或者说计算正交投影矩阵的长方体,请看下图:

其实就是计算观察相机的视截体三个层级(Near,Middle,Far)在平行光源空间形成的AABB包围盒,这样自然而然得到相应的正交投影变换矩阵了。那么如何计算Near,Middle,Far三个部分形成的AABB包围盒呢?这里分为三步:

(1)首先计算观察相机的三个层级(3D梯形)的顶点的位置,注意一个3D梯形有八个顶点,因为是3D梯形,因为我这里是二维平面截图,你看到是4个顶点,别以为只是4个顶点。计算如下图所示:

计算公式:

DX11计算一个层级八个顶点的代码:

//求出相机的一个层级的八个顶点
void CascadedShadowsManager::CreateFrustumPointsFromCascadeInterval(FLOAT fCascadeIntervalBegin, FLOAT fCascadeIntervalEnd, CXMMATRIX vProjection, XMVECTOR* pvCornerPointsWorld)
{BoundingFrustum vViewFrust(vProjection);vViewFrust.Near = fCascadeIntervalBegin;vViewFrust.Far = fCascadeIntervalEnd;static const XMVECTORU32 vGrabY = { 0x00000000,0xFFFFFFFF,0x00000000,0x00000000 };static const XMVECTORU32 vGrabX = { 0xFFFFFFFF,0x00000000,0x00000000,0x00000000 };XMVECTORF32 vRightTop = { vViewFrust.RightSlope,vViewFrust.TopSlope,1.0f,1.0f };XMVECTORF32 vLeftBottom = { vViewFrust.LeftSlope,vViewFrust.BottomSlope,1.0f,1.0f };XMVECTORF32 vNear = { vViewFrust.Near,vViewFrust.Near ,vViewFrust.Near,1.0f };XMVECTORF32 vFar = { vViewFrust.Far,vViewFrust.Far,vViewFrust.Far,1.0f };XMVECTOR vRightTopNear = XMVectorMultiply(vRightTop, vNear);XMVECTOR vRightTopFar = XMVectorMultiply(vRightTop, vFar);XMVECTOR vLeftBottomNear = XMVectorMultiply(vLeftBottom, vNear);XMVECTOR vLeftBottomFar = XMVectorMultiply(vLeftBottom, vFar);//nearpvCornerPointsWorld[0] = vRightTopNear;  //近平面右上角pvCornerPointsWorld[1] = XMVectorSelect(vRightTopNear, vLeftBottomNear, vGrabX); //近平面左上角pvCornerPointsWorld[2] = vLeftBottomNear;  //近平面左下角pvCornerPointsWorld[3] = XMVectorSelect(vRightTopNear, vLeftBottomNear, vGrabY); //近平面右下角//farpvCornerPointsWorld[4] = vRightTopFar;  //远平面右上角pvCornerPointsWorld[5] = XMVectorSelect(vRightTopFar, vLeftBottomFar, vGrabX); //远平面左上角pvCornerPointsWorld[6] = vLeftBottomFar;  //远平面左下角pvCornerPointsWorld[7] = XMVectorSelect(vRightTopFar, vLeftBottomFar, vGrabY); //远平面右下角}

(2)进行了上面的一步,得到相应层级在观察相机的相机空间(ViewSpace)的八个顶点位置,然后将这八个顶点乘以CameraViewMatrix的逆矩阵 变换到到世界空间

WorldSpacePos = CameraViewPos*Inverse(CameraViewMatrix)

然后乘以平行光源的相机变换矩阵变换到平行光源的ViewSpace,也就是

LightViewSpacePos= WorldSpacePos *LightViewMatrix

(3)求相应层级在LightViewSpace 空间(可称为LightSpace)的八个顶点形成的AABB包围盒,也就是这八个顶点的每个分量最大值形成的顶点和分量最小值形成的顶点围成的长方体。计算如下:

        lightCameraFrustumOrthoMin = g_XMFltMax;lightCameraFrustumOrthoMax = g_XMFltMin;XMVECTOR vTempTranslateCornerPoints;//将视截体的八个点从相机空间变换到光照空间,并求其在光照空间的AABBfor (int index = 0; index < 8; ++index){//将视截体的点从相机空间变换到世界空间frustumPoints[index] = XMVector3Transform(frustumPoints[index], cameraInverseViewMatrix);//将视截体的点从世界空间变换到光照空间vTempTranslateCornerPoints = XMVector3Transform(frustumPoints[index], lightViewMatrix);//求出AABB体的位置最大点和最小点lightCameraFrustumOrthoMin = XMVectorMin(lightCameraFrustumOrthoMin, vTempTranslateCornerPoints);lightCameraFrustumOrthoMax = XMVectorMax(lightCameraFrustumOrthoMax, vTempTranslateCornerPoints);}lightCameraFrustumOrthoMin = g_XMFltMax;lightCameraFrustumOrthoMax = g_XMFltMin;XMVECTOR vTempTranslateCornerPoints;//将视截体的八个点从相机空间变换到光照空间,并求其在光照空间的AABBfor (int index = 0; index < 8; ++index){//将视截体的点从相机空间变换到世界空间frustumPoints[index] = XMVector3Transform(frustumPoints[index], cameraInverseViewMatrix);//将视截体的点从世界空间变换到光照空间vTempTranslateCornerPoints = XMVector3Transform(frustumPoints[index], lightViewMatrix);//求出AABB体的位置最大点和最小点lightCameraFrustumOrthoMin = XMVectorMin(lightCameraFrustumOrthoMin, vTempTranslateCornerPoints);lightCameraFrustumOrthoMax = XMVectorMax(lightCameraFrustumOrthoMax, vTempTranslateCornerPoints);}
 

那么这个层级的OrthoProjMatrix就可以计算出来,

 mShadowProj[iCascadeIndex] = XMMatrixOrthographicOffCenterLH(XMVectorGetX(lightCameraFrustumOrthoMin),XMVectorGetX(lightCameraFrustumOrthoMax),XMVectorGetY(lightCameraFrustumOrthoMin),XMVectorGetY(lightCameraFrustumOrthoMax), XMVectorGetZ(lightCameraFrustumOrthoMin), XMVectorGetZ(lightCameraFrustumOrthoMax));

Shader实现代码:

#ifndef CASCADE_COUNT_FLAG
#define CASCADE_COUNT_FLAG 3
#endif//常量缓存数据
cbuffer CB_ALL_SHADOW_DATA:register(b0)
{matrix mWorldViewProj;matrix mWorldView;matrix mWorld;matrix mShadowView;float4 mCascadeOffset[8];float4 mCascadeScale[8];int mCascadeNum;int mVisualizeCascade;int mPCFBlurForLoopStart;int mPCFBlurForLoopEnd;float mMinBorderPadding;float mMaxBorderPadding;float mShadowBias; //阴影bias,解决阴影粉刺float mShadowPartitionSize;float mTexelSize;  //ShadowMap纹理像素大小float mNativeTexelSizeInX; // Texel size in native map ( textures are packed )float mCascadeBlendArea;float pad;float4 mCascadeFrustumViewSpaceDepth[2];float4 mCascadeFrustumViewSpaceDepthFloat4[8];float4 mLightDir;
};//纹理
Texture2D gDiffuse:register(t0);  //diffuseMap
Texture2D gShadow:register(t5); //shadowMap//采样器
SamplerState samLinear:register(s0);
SamplerState samPoint:register(s1);
SamplerComparisonState samShadow:register(s5);struct VertexIn
{float3 Pos:POSITION;float3 Normal:NORMAL;float2 Tex:TEXCOORD0;  };struct VertexOut
{float4 Pos:SV_POSITION;float3 WorldNormal:NORMAL;  //世界空间的法线float2 DiffuseTex:TEXCOORD0;float4 ShadowTex:TEXCOORD1;float4 InterPos:TEXCOORD2;float Depth: TEXCOORD3;
};//每个层级的可视化颜色
static const float4 vCascadeColorsMultiplier[8] =
{float4 (1.5f, 0.0f, 0.0f, 1.0f),float4 (0.0f, 1.5f, 0.0f, 1.0f),float4 (0.0f, 0.0f, 5.5f, 1.0f),float4 (1.5f, 0.0f, 5.5f, 1.0f),float4 (1.5f, 1.5f, 0.0f, 1.0f),float4 (1.0f, 1.0f, 1.0f, 1.0f),float4 (0.0f, 1.0f, 5.5f, 1.0f),float4 (0.5f, 3.5f, 0.75f, 1.0f)
};void ComputeCoordinatesTransform(in int iCascadeIndex,in out float4 vShadowTexCoord)
{vShadowTexCoord.x *= mShadowPartitionSize;     //1.0f / (float)mCascadeConfig.m_nCascadeLevels; 因为多张DepthRT合成了一张DepthRTvShadowTexCoord.x += (mShadowPartitionSize * (float)iCascadeIndex);}void CalculatePCFPercentLit(in float4 vShadowTexCoord, in float fBlurRowSize, out float fPercentLit)
{fPercentLit = 0.0f;for (int x = mPCFBlurForLoopStart; x < mPCFBlurForLoopEnd; ++x){for (int y = mPCFBlurForLoopStart; y < mPCFBlurForLoopEnd; ++y){float depthcompare = vShadowTexCoord.z;depthcompare -= mShadowBias;/*float2 texUV = float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX, vShadowTexCoord.y + ((float)y) * mTexelSize);float depth = gShadow.Sample(samPoint, texUV).r;fPercentLit += (depth >= depthcompare);*/fPercentLit += gShadow.SampleCmpLevelZero(samShadow,float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX,vShadowTexCoord.y + ((float)y) * mTexelSize),depthcompare);}}fPercentLit /= (float)fBlurRowSize;
}VertexOut VS(VertexIn vin)
{VertexOut vout;vout.Pos = mul(float4(vin.Pos, 1.0), mWorldViewProj);vout.WorldNormal = mul(vin.Normal, (float3x3)mWorld);vout.DiffuseTex = vin.Tex;vout.InterPos = float4(vin.Pos, 1.0);vout.Depth = mul(float4(vin.Pos,1.0), mWorldView).z;vout.ShadowTex = mul(float4(vin.Pos, 1.0), mShadowView);return vout;
}float4 PS(VertexOut pin) : SV_Target
{float4 diffuseColor = gDiffuse.Sample(samLinear, pin.DiffuseTex);float4 shadowMapTextureCoord = 0.0f;float4 visualCascadeColor = float4(0.0f, 0.0f, 0.0f, 1.0f);float fPercentLit = 0.0f;//模糊范围大小int iBlurRowSize = mPCFBlurForLoopEnd - mPCFBlurForLoopStart;iBlurRowSize *= iBlurRowSize;float fBlurRowSize = (float)iBlurRowSize;int iCascadeFound = 0;int iCurrentCascadeIndex = 0;float4 shadowMapTextureViewSpaceCoord = pin.ShadowTex;//将shadowMap的采样坐标从ViewSpace变换到纹理投影空间for (int iCascadeIndex = 0; iCascadeIndex < CASCADE_COUNT_FLAG&&iCascadeFound == 0; ++iCascadeIndex){shadowMapTextureCoord = shadowMapTextureViewSpaceCoord * mCascadeScale[iCascadeIndex];shadowMapTextureCoord += mCascadeOffset[iCascadeIndex];if (min(shadowMapTextureCoord.x, shadowMapTextureCoord.y) > mMinBorderPadding&&max(shadowMapTextureCoord.x, shadowMapTextureCoord.y) < mMaxBorderPadding&&shadowMapTextureCoord.z>0.0f&&mMaxBorderPadding&&shadowMapTextureCoord.z<1.0f){iCurrentCascadeIndex = iCascadeIndex;iCascadeFound = 1;}}ComputeCoordinatesTransform(iCurrentCascadeIndex,shadowMapTextureCoord);//层级标记颜色visualCascadeColor = vCascadeColorsMultiplier[iCurrentCascadeIndex];//shadowMap的深度和像素的深度进行比较CalculatePCFPercentLit(shadowMapTextureCoord,fBlurRowSize, fPercentLit);if (!mVisualizeCascade)visualCascadeColor = float4(1.0f, 1.0f, 1.0f, 1.0f);float3 vLightDir1 = float3(-1.0f, 1.0f, -1.0f);float3 vLightDir2 = float3(1.0f, 1.0f, -1.0f);float3 vLightDir3 = float3(0.0f, -1.0f, 0.0f);float3 vLightDir4 = float3(1.0f, 1.0f, 1.0f);//模拟ambient-Lightfloat fLighting =saturate(dot(vLightDir1, pin.WorldNormal))*0.05f +saturate(dot(vLightDir2, pin.WorldNormal))*0.05f +saturate(dot(vLightDir3, pin.WorldNormal))*0.05f +saturate(dot(vLightDir4, pin.WorldNormal))*0.05f;float4 vShadowLighting = fLighting*0.5f;float3 lightDir = -normalize(mLightDir).xyz;fLighting += saturate(dot(pin.WorldNormal, lightDir));fLighting = lerp(vShadowLighting, fLighting, fPercentLit);float4 finalColor = fLighting*visualCascadeColor*diffuseColor;float4 color = pow(finalColor, 1.0f / 2.2f);return color;}

Shader解惑:

(1)注意这里有三个层级,理论上有对应的三张ShadowMap,但是合成了一张ShadowMap,也就是说如果ShadowMap的分辨率是1024X1024,则合成的ShadowMap分辨率为3072X1024,因此可以看到计算ShadowMap采样坐标的代码为:

void ComputeCoordinatesTransform(in int iCascadeIndex,in out float4 vShadowTexCoord)
{vShadowTexCoord.x *= mShadowPartitionSize;     //1.0f / (float)mCascadeConfig.m_nCascadeLevels; 因为多张DepthRT合成了一张DepthRTvShadowTexCoord.x += (mShadowPartitionSize * (float)iCascadeIndex);}

其中 mShadowPartitionSize= 1.0/mCascadeNum, mCascadeNum为总共划分视截体的层级数。

PCF的代码为:

void CalculatePCFPercentLit(in float4 vShadowTexCoord, in float fBlurRowSize, out float fPercentLit)
{fPercentLit = 0.0f;for (int x = mPCFBlurForLoopStart; x < mPCFBlurForLoopEnd; ++x){for (int y = mPCFBlurForLoopStart; y < mPCFBlurForLoopEnd; ++y){float depthcompare = vShadowTexCoord.z;depthcompare -= mShadowBias;/*float2 texUV = float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX, vShadowTexCoord.y + ((float)y) * mTexelSize);float depth = gShadow.Sample(samPoint, texUV).r;fPercentLit += (depth >= depthcompare);*/fPercentLit += gShadow.SampleCmpLevelZero(samShadow,float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX,vShadowTexCoord.y + ((float)y) * mTexelSize),depthcompare);}}fPercentLit /= (float)fBlurRowSize;
}

这里假设ShadowMap的分辨率的Height为X,则width为3X,所以PCF的ShadowMap采样坐标的横向平移单位和纵向平移单位是不一样的,

这里 mTexelSize = 1.0 / X                                mNativeTexelInX = mTexelSize / mCascadeNum

渲染效果:

未可视化Cascade:

可视化Cascade:

参考资料:

[1]. http://ogldev.atspace.co.uk/www/tutorial49/tutorial49.html

[2].https://msdn.microsoft.com/en-us/library/windows/desktop/ee416307?ranMID=24542&ranEAID=TnL5HPStwNw&ranSiteID=TnL5HPStwNw-6XqNnhs2CFZrh4f6WF_b9w&tduid=(6936a70c84b18e2114dc180de68c9033)(256380)(2459594)(TnL5HPStwNw-6XqNnhs2CFZrh4f6WF_b9w)()

[3].DirectxSampleSDK的CascadeShaowMap例子

[4].http://blog.csdn.net/qq_29523119/article/details/72862517

源码链接:

集成了CSM算法的3D渲染引擎源码:

https://github.com/2047241149/SDEngine

Directx11的版本:

http://download.csdn.net/download/qq_29523119/10244932

模型资源在 directx-sdk-samples 的Media\powerplant 目录

OpenGL的版本:

http://download.csdn.net/download/qq_29523119/10153078

CSM算法实现余留的问题

(1)阴影不稳定,或者部分阴影无法显示出来的问题,看下面:

毫无疑问,部门阴影丢掉了,不见了,这在我分享的Directx11或者OpenGL实现CSM算法都会出现这个问题,

http://ogldev.atspace.co.uk/www/tutorial49/tutorial49.html分享的CSM算法是粗糙的,并没有说明这种情况。

(2)相机移动过程中的阴影抖动(jitter)问题

想知道如何解决CSM的阴影丢失问题和相机移动时的阴影抖动(jitter)问题,请看下一个教程:

Directx11进阶教程之CascadeShadowMap(层级阴影)(上)相关推荐

  1. Directx11进阶教程之CascadeShadowMap(层级阴影)(下)---解决阴影丢失和阴影抖动问题

    承接上一篇博客,我们探讨下阴影丢失和阴影抖动的原因和提出相应的解决办法. CSM算法的阴影丢失或者显示错乱问题: 再次看看试验场景的阴影丢失或者阴影显示错乱的现象: 错误的显示:(上个教程不完善的CS ...

  2. Directx11进阶教程之Cluster Based Deffered Shading

    前言 很多游戏中存在大量的点光源(PointLight),环境艺术家为了让游戏模拟现实的氛围,一个场景下放下上千个点光源(PointLight)毫不奇怪. 在上一章中  Directx11进阶教程之T ...

  3. Directx11进阶教程之Tiled Based Deffered Shading

    前言 很多游戏中存在大量的点光源(PointLight),环境艺术家为了让游戏模拟现实的氛围,一个场景下放下上千个点光源(PointLight)毫不奇怪. 下面介绍下传统的渲染管线大量点光源的表现. ...

  4. 根据用户查进程_【磨叽教程】Android进阶教程之在Android系统下各进程之间的优先级关系...

    导读:本文大约2000字,预计阅读时间3分钟.本文纯属技术文,无推广. 正文     首先应用进程的生命周期并不由应用本身直接控制,而是由系统综合多种因素来确定的.Android系统有自己的一套标准, ...

  5. Vue 进阶教程之:详解 v-model

    分享 Vue 官网教程上关于 v-model 的讲解不是十分的详细,写这篇文章的目的就是详细的剖析一下, 并介绍 Vue 2.2 v-model 改进的地方,然后穿插的再说点 Vue 的小知识. 在 ...

  6. C++高级进阶教程之STL 教程

    STL 教程 在前面的章节中,我们已经学习了 C++ 模板的概念.C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和 ...

  7. rom diy进阶教程之apk反编译基础(插桩解析)

    [Android] [SMALI]smali文件内容具体介绍 大家都应该知道APK文件其实就是一个MIME为ZIP的压缩包,我们修改ZIP后缀名方式可以看到内部的文件结构,例如修改后缀后用RAR打开鳄 ...

  8. linux使用安卓厨房,【图片】【教程】进阶教程之“使用安卓厨房制作/修改ROM包”【联想a798t吧】_百度贴吧...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 4.然后是美化 不喜欢或者不懂美化的同学可以绕过这一步了.这一步骤是毕竟比较难的,整个制作过程中涉及到手动反编译的就在这里. 美化主要是弄以下方面,修改s ...

  9. Python新手进阶教程之1、海龟作图——用Python绘图(1)

    1.1海龟的作用 使用海龟作图,我们不仅能够只用几行代码就创建出令人影响深刻的视觉效果,而且还可以跟海龟看看几行代买如何影响到它的移动.者能够帮助我们理解代码的逻辑. 1.2第一个海龟程序 让我们使用 ...

最新文章

  1. 腾讯宣布员工最高可申请免息借款90万!网友:应届当码农就能一线城市买房了!...
  2. Android 系统自动重启Bug(高通平台)
  3. 用C语言实现三子棋游戏
  4. java10个基础错误_我们处理了10亿个Java记录的错误-这是导致97%的错误的原因
  5. 准程序员必看!该怎么规划自己的职业人生,看看前辈们给的建议!
  6. Redis内存缓存系统入门
  7. python操作数据库慢_MySQL数据库之python 拉取mysql 慢日志
  8. C#中对 API函数的调用
  9. 拓端tecdat|R语言中进行Spearman等级相关分析
  10. 每天一个linux命令
  11. Atitit.常用语言的常用内部api 以及API兼容性对源码级别可移植的重要性 总结
  12. matlab 平滑曲线连接_曲线拟合的一些想法
  13. 中国大学MOOC C语言程序设计(大连理工大学) 课后编程题 第十周题解(个人向仅供参考)
  14. java 语音聊天室
  15. 苹果系统这么没有关闭订阅服务器,iPhone 上没有取消订阅的选项怎么办?
  16. c语言中优先级劣后级,基金优先级与劣后级的区别是什么 看完你就明白了
  17. 90后在校大学生开旅游公司创业
  18. 你不会还在机械重复的输入格式化信息吧?snippet配置来帮你一键生成
  19. Prezi安装中文字体找不到“com.prezi.PreziDesktop”文件夹的解决办法
  20. miui升级系统无服务器,细数MIUI 11的BUG,还没升级的朋友,先来了解一下

热门文章

  1. MSP430定时器、中断
  2. Adobe Audition基本的使用
  3. 奈氏准则、香农公式、信噪比、信道容量
  4. 华为荣耀X1刷机包 B003版精简 官方原汁原味卡刷rom
  5. 华为荣耀v8计算机没了,华为荣耀V8失败变砖开不了机了怎么办_华为荣耀V8救砖方法...
  6. 动态生成word文档的靠谱方式
  7. Greenplum 6.0 版本官方最强解读
  8. 中本聪系数会如何影响区块链的健壮性?
  9. 手机IMEI查询,手机串号查询
  10. dock模拟macos教程_如何设置macOS应用程序以最小化其Dock图标