DirectX12 3D 游戏开发与实战第十二章内容
12、几何着色器
如果不启用曲面细分,那么几何着色器这个可选阶段将会在位于顶点着色器和像素着色器之间。顶点着色器以顶点作为输入数据,而几何着色器以完整的图元为输入数据。与顶点着色器不同的是,顶点着色器不可以销毁或者创建图元,几何着色器可以销毁或者创建几何图形,我们可以借助这个特点将输入的图元扩展成一个或更多类型的图元,或者是不输出图元。
几何着色器所输出的图元由顶点列表定义而成,在退出几何着色器的时候,必须将顶点的位置变换到齐次裁剪空间。
学习目标
- 学习如何编写几何着色器
- 探究如何通过几何着色器来高效的实现公告牌技术(billboard)
- 了解自动生成图元ID及其相关的应用
- 研究如何创建和使用纹理数组,并认识到他们为何如此使用
- 理解如何运用alpha-to-coverage技术来辅助解决alpha裁剪失真的问题
12.1 、编写几何着色器
几何着色器的编写方式比较接近于顶点着色器和像素着色器,下面的代码展示了几何着色器的一般编写格式:
[maxvertexcount(N)]
void ShaderName(PrimitiveType InputVertexType InputName[NumElements],inout StreamOutputObject<OutputVertexType> OutputName)
{
// 几何着色器的具体实现
}
我们必须先指定几何着色器单词调用所输出的最大顶点数量(每个图元都会调用一次几何着色器,走一遍其中的处理流程),对此,我们可以使用下列属性语法来设置着色器定义之前的最大顶点数量。
[maxvertexcount(N)]
N是几何着色器单次调用所输出的顶点数量最大值,几何着色器每次输出的顶点数量可能各不相同,但是都不可以超过N。同时处于对性能的考虑,N值应该尽可能的小。
几何着色器有输入、输出两个参数,输入参数必须是一个定义有特定图元的顶点数组——点应该属于一个顶点,线段应该输入两个顶点,三角形需要输入三个顶点,线及其邻接图元为四个顶点,三角形及其邻接图元为六个顶点。几何着色器的输入顶点类型便是输出的顶点类型,输入参数一定要以图元类型为前缀,用以描述输入到几何着色器的具体图元类型。该前缀可以是以下几种类型之一:
名称 | 含义 |
---|---|
point | 输入的图元为点 |
line | 输入的图元为线段 |
triangle | 输入的图元为三角形列表或三角形带 |
lineadj | 输入的图元为线列表及其邻接图元或线条带及其邻接图元 |
triangleadj | 输入的为三角形列表及其邻接图元或三角形带及其邻接图元 |
输出参数一定要标有inout修饰符,而且一定要是一种流类型(stream type,即某种类型的流输出对象)。几何着色器可以通过内置方法Append向输出流列表添加单个顶点:
void StreamOutputObject<OutputVertexType>::Append(OutputVertexType v);
流类型本质是一种模板类型(template type),其模板参数用于指定输出顶点的具体类型,流类型有以下3种:
名称 | 含义 |
---|---|
PointStream | 一系列顶点所定义的店列表 |
LineStream | 一系列顶点所定义的线条带 |
TriangleStream | 一系列顶点所定义的三角形带 |
几何着色器输出的多个顶点会构成图元,图元的输出类型由流类型来指定,对于线条和三角形来说,几何着色器输出的对应图元一定是线条带和三角形带。而线条列表和三角形列表可以使用内置函数RestartStrip来实现:
void StrameOutputObject<OutputVertexType>::RestartStrip();
比如:如果希望输出三角形列表,则需要每次向输出流追加三个顶点之后调用RestartStrip。
以下是一些几何着色器签名的具体用例:
// 示例1:GS最多输出4个顶点,输入的图元一根是线条,输出的是一个三角形带
[maxvertexcount(4)
void GS(line VertexOut gin[2],inout TriangleStream<GeoOUt> triStream)
{
// 几何着色器的实现
}
// 示例二:GS最多输出32个顶点,输入的图元是一个三角形,输出的是一个三角形带
[maxvertexcount(32)]
void GS(triangle VertexOut gin[3],inout TriangleStream<GeoOut> triStream)
{
// 几何着色器的具体实现
}
// 示例三:GS最多输出4个顶点,输入的图元是一个店,输出的是一个三角形带
[maxvertexcount[4]]
void GS(point VertexOut gin[1],inout TriangleStream<GeoOut> triStream)
{
// 几何着色器的具体实现
}
下列几何着色器详细的展示了Append和RestartStrip方法的调用过程,此示例会将输入的三角形细分成四个小三角形输出:
struct VertexOut
{
float3 PosL : POSITION;
float3 Normal : NORMAL;
float2 Tex : TEXCOORD;
}
struct GeoOut
{
float4 PosH : SV_POSITION;
float4 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
float FogLerp : FOG;
}
void Subdivide(VertexOut inVerts[3],out VertexOut outVerts[6])
{
VertexOut m[3];
// 计算三角形三条边的中点
m[0].PosL = 0.5f*(inVerts[0].PosL + inVerts[1].PosL);
m[1].PosL = 0.5f*(inverts[1].PosL + inVerts[2].PosL);
m[2].PosL = 0.5f*(inverts[2].PosL + inVerts[0].PosL);
// 将顶点投影到单位球面上
m[0].PosL = normalize(m[0].PosL);
m[1].PosL = normalize(m[1].PosL);
m[2].PosL = normalize(m[2].PosL);
// 求出法线
m[0].NormalL = m[0].PosL;
m[1].NormalL = m[1].PosL;
m[2].NormalL = m[2].PosL;
// 对纹理坐标进行插值
m[0].Tex = 0.5f*(inVerts[0].Tex + inVerts[1].Tex);
m[1].Tex = 0.5f*(inVerts[1].Tex + inVerts[2].Tex);
m[2].Tex = 0.5f*(inVerts[2].Tex + inVerts[0].Tex);
outVerts[0] = inVerts[0];
outVerts[1] = m[0];
outVerts[2] = m[2];
outVerts[3] = m[1];
outVerts[4] = inVerts[2];
outVerts[5] = inVerts[1];
}
void OutputSubdivision(VertexOut v[6],inout TriangleStream<GeoOut> triStream)
{
GeoOut gout[6];
[unroll]
for(int i=0;i<6;++i)
{
// 将顶点变换到世界空间
gout[i].PosW = mul(float4(v[i].PosL,1.0f),gWorld).xyz;
gout[i].NormalW = mul(v[i].Normal,(float3x3)gWorldInvTranspose);
// 将顶点变换到齐次裁剪空间
gout[i].PosH = mul(float4(v[i].PosL,1.0f),gWorldViewProj);
gout[i].Tex = v[i].Tex;
}
// 我们可以将细分的小三角形绘制到两个三角形带中
// 三角形带1:底部的三个三角形
// 三角形带2:顶部的一个三角形
[unroll]
for(int i=0;i<5;++i)
{
triStream.Append(gout[j]);
}
triStream.RestartStrip();
triStream.Append(gout[1]);
triStream.Append(gout[5]);
triStream.Append(gout[3]);
}
[maxvertexcount(8)]
void GS(triangle VertexOut gin[3], inout TriangleStream<GeoOut>)
{
VertexOut v[6];
Subdivide(gin,v);
OutputSubdivision(v,triStream);
}
几何着色器的编译过程和顶点着色器、像素着色器一样,我们可以使用下列方法将几何着色器编译为字节码(TreeSprite.hlsl文件中有几何着色器)
mShaders["treeSpriteGS"] = d3dUtil::CompileShader(L"Shaders\\TreeSprite.hlsl",nullptr,"GS","gs_5_0");
然后我们要将几何着色器作为流水线状态对象的一部分,以此将它绑定到渲染流水线上
D3D12_GRAPHICS_PIPELINE_STATE_DESC treeSpritePsoDesc = opaquePsoDesc;
……
treeSpritePsoDesc.GS =
{
reinterpret_cast<BYTE*>(mShaders["treeSpriteGS"]->GetBufferPointer()),
mShaders["treeSpriteGS"]->GetBufferSize();
}
12.2、以公告牌技术实现森林效果
12.2.1、概述
当树与树之间的距离较远时,我们便可以使用公告牌技术来实现森林效果。即以绘制3D树木土拍你的四边形来替代对整棵3D树木的渲染,从远处看,公告牌技术可以以假乱真。同时,我们还会使公告牌总是面向摄像机,以避免露馅。
在世界空间中,如果给定一个公告牌的中心点的位置(即四边形的中心)为G(C1,C2,C3)。而摄像机的位置为E = (E1,E2,E3),那么我们便可以表示出该公告牌局部坐标与世界空间的相对关系。
\]
\]
\]
给出公告牌局部坐标系与世界空间的相对关系和公告牌在世界空间中的大小,我们便可以通过下列代码来获取公告牌四边形的4个顶点坐标:
v[0] = float4(gin[0].CenterW + halfWidth*right - haleHeight*up,1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + haleHeight*up,1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - haleHeight*up,1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + haleHeight*up,1.0f);
注意点:由于公告牌的局部坐标系都不相同,所以每一个公告牌的四边形都要分别计算。
对于这个演示程序而言,我们将会构造一系列距离陆地表面有着特定距离的点图元,这些点表示的是公告牌的中心点。在几何着色器中,我们会将这些点扩展成四边形,同时计算公告牌的世界矩阵。
构造点图元的方法:将PSO中的PrimitiveTopology成员指定为D3D12_PRIMITIVE_TOPOLOGY_POINT,并把ID3D12GraphicsCommandList::IASetPrimitiveTopology函数的参数指定为D3D_PRIMITIVE_TOPOLOGY_POINTLIST.
12.2.2、顶点结构体
我们用下列顶点结构体来描述公告牌:
struct TreeSpriteVertex
{
XMFLAOT3 Pos;
XMFLOAT2 Size;
}
mTreeSpriteInputLayout =
{
{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
{"SIZE",0,DXIG_FORMAT_R32G32_FLOAT,0,12,D3D12_INPUT_CALSSFICATION_PER_VERTEX_DATA,0},
}
顶点结构体中存储Pos变量是公告牌的中心位置,Size变量是公告牌的大小。通过Size变量,几何着色器可以知晓扩展后的公告牌会有多大,通过调整每一个顶点结构体中Size成员变量,我们就可以构造出不同尺寸的公告牌。
12.2.3、HLSL文件
这是第一次展示几何着色器示例,所以我们将会在此列出完整的HLSL代码,使我们可以更加便于了解几何着色器、顶点着色器和像素着色器这三者的结合使用方法。该HLSL文件也引入了一些之前没有介绍过的讨论过的新对象(SV_PrimitiveID和Texture2DArray),我们会在后续小节对此进行介绍。
12.2.4、SV_PrimitiveID语义
在HLSL文件中,几何着色器有一个使用SV_PrimitiveID语义的特殊无符号整数参数:
[maxvertexcount[4]]
void GS(point VertexOut gin[1],uint primID:SV_PrimitiveID,inout TriangleStream<GeoOut>)
若指定了该语义,则输入装配器阶段会自动为每个图元生成图元ID(第一个图元被标记为0,第二个图元被标记为1……),对于单词绘制调用来说,其中的图元ID都是唯一的。在公告牌示例中,几何着色器并不会使用到图元ID。
注意点1:像素着色器也可以获得图元ID,但是若存在几何着色器,则几何着色器首先获得图元ID,若不存在几何着色器,则像素着色器首先获得图元ID。即二者谁离图元装配器阶段近且被开启,则首先获得图元ID。
注意点2:输入装配器也可以生成顶点ID。不过我们需要在顶点着色器额外添加一个有语义SV_VertexID修饰的uint类型的参数:
VertexOut VS(VertexIn vin,uint vertID : SV_VertexID)
{
// 顶点着色器主体
}
12.3、纹理数组
12.3.1、概述
纹理数组即存放纹理的数组,和其他资源一样,在C++代码中,纹理数组也由ID3D12Resource接口来表示。创建ID3D12Resource对象时,我们可以通过设置DepthArraySize属性来指定纹理数组所存放的元素个数(对于3D纹理来说,此项设定则为深度值)。在HLSL文件(效果文件)中,纹理数组是通过Texture2DArray类型来表示的。
12.3.2、对纹理数组进行采样
在公告牌示例中,我们用以下代码对纹理数组进行采样:
float3 uvw = float3(pin.TexC,pin.PrimID%4);
float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap,uvw)*gDiffuseAlbedo;
使用纹理数组一共需要三个坐标值:前两个坐标为普通的2D纹理坐标,第三个坐标为纹理数组的索引。
在公告牌示例中,我们采用的是一个含有4个纹理资源的纹理数组,每个纹理元素为一种树木。但是,由于我们要绘制的树木多余四颗,所以最终的图元ID也会大于3。因此我们才会对图元ID进行模4运算。每次设置纹理或者绘制调用的时候都会产生相应的开销,使用纹理数组之后我们可以吧设置纹理与绘制调用的过程减少到一次。
12.3.3、加载纹理数组
在Common/DDSTextureLoader.h文件中,有加载存有纹理数组的DDS文件的方法。因此,我们只需要创建含有纹理数组的DDS文件即可。通过微软公司所提供的texassemble工具,然后通过下列语法便可以将dds图像合并成一个纹理数组。
比如:将t0.dds t1.dds t2.dds t3.dds合并成一个名为treeArray的纹理数组:
texassemble -array -o treeArtessray.dds t0.dds t1.dds t2.dds t3.dds
注意点:使用texassemble程序创建纹理数组时,每一个图像只允许有一种mipmap层级。但是我们可以用texconv工具对生成的纹理数组进行处理,以让其生成多种mipmap层级或者改变纹理的格式。
texconv -m 10 -f BC3_UNORM treeArray.dds
12.3.4、纹理子资源
下图展示了一个拥有四个纹理的纹理数组,其中的纹理都有各自的mipmap链。Direct3D中使用术语数组切片来表示纹理数组中某个纹理以及其mipmap链。又使用属于mip切片来表示纹理数组中特定层级的所有mipmap。子资源即是指纹理数组中某个纹理的单个mipmap层级。
这就是说,若给出纹理数组的索引以及mipmap层级,我们就可以访问纹理数组中相应的子资源。字子元也是由线性索引来标记的,而Direct3D所使用的线性索引规划如下图所示:
通过下面的工具函数,我们可以根据给出的mip切片索引、数组切片索引、平面切片索引、mipmap层级以及纹理数组的大小,直接计算出子资源的线性索引。
inline UINT D3D12CalcSubresource(UINT MipSlice, UINT ArraySlice,UINT PlaneSlice,UINT MipLevels,UINT ArraySize)
{
return MipSlice + ArraySlice + PlaneSlice + MipLevels + ArraySize;
}
12.4、alpha-to-coverage技术
在运行演示程序,如果以特定距离观察树木公告牌,我们会发现边缘部分呈锯齿状。这个问题出在clip函数上,我们用它来遮罩不属于树木纹理的像素,因此树木边缘的过度并不平滑,同时加上观察者与公告牌距离过近会引起纹理放大的情况发生,继而使块状失真更加明显。而且近距离也会导致低分辨率mipmap的启用。
解决这个问题的方法之一使使用透明混合代替alpha测试。通过线性纹理过滤,使边缘像素稍显模糊,从而使由不透明的像素到被遮罩的像素之间的过渡更为平滑。即透明混合可以让公告牌图像边缘的不透明像素到被遮罩像素之间实现平滑的渐变。但是使用透明缓和技术需要将场景中的物体按从后到前的顺序进行渲染,对于大量公告牌来说,在每一帧中进行排序是需要耗费大量性能的。所以这个方法 并不适合大量公告牌。
这个时候,我们可以采用MSAA(mutisampling antialiasing,多重采样抗锯齿技术)。使用MSAA可以解决块状失真的问题,但是也会出现一些问题。MSAA会给每一个像素执行一次像素着色器,使像素着色器在像素的中心采样,同时基于可视性和覆盖情况,将颜色信息共享给他的子像素。但关键是,覆盖情况使在多边形层级上确定下来的。因此MSAA并不会检测alpha通道所定义的树木公告牌的裁剪边缘,而是关注纹理所映射到的四边形的边缘。为了解决这个问题,我们就要使用alpha-to-coverage技术,使MSAA在计算覆盖情况的时候考虑alpha通道这个因素的影响。
问题:什么是可视性,什么是覆盖情况?
可视性:每个子像素所执行的深度/模板测试结构
覆盖情况:子像素的中心是位于多边形的内部还是外部
在开启MSAA和alpha-to-coverage(令成员D3D12_BLEND_DESC::AlphaToCoverageEnable = true)之后,硬件会检测像素着色器所返回的alpha值,用以确定覆盖情况。例如:在使用4x MSAA时,如果像素着色器返回的alpha值为0.5,那么即认为该像素里4个子像素中有2个是在四边形之外,并据此创建平缓的图像边缘。
一般来说,用alpha遮罩的方式来裁剪树叶和围栏这类纹理时,我们通常会使用alpha-to-coverage技术。
12.5、程序示例效果
12.5、小结
1、假设我们不使用曲面细分阶段,那么几何着色器这个可选阶段便会位于顶点着色器和像素着色器之间。几何着色器会对输入装配器传入的每一个图元进行处理,通过配置几何着色器,它可以不输出图元,输出一个图元,输出多个图元,而且输出的图元类型也可能和输入图元的类型不同。在输出的图元顶点离开几何着色器之前,我们应当将顶点变换到齐次裁剪空间中。几何着色器输出的图元,会进入渲染流水线的光栅化阶段。
2、公告牌技术采用的是附有图像的四边形对象,我们可以将它作为真实3D模型的替代品。对于远处的物体来说,公告牌可以以假乱真。公告牌技术的优点是可以节省GPU渲染整个3D对象的处理时间。为了使公告牌技术更加逼真,我们一定要使公告牌总是面向摄像机。同时,在几何着色器中实现公告牌技术往往使最合适的。
3、我们可以把一种由语义SV_PrimitiveID修饰的特殊uint类型参数加入到几何着色器的参数列表之中,其示例代码如下:
[maxvertexcount(4)]
void GS(point VertexOut gin[1],uint primID : SV_PrimitiveID,inout TriangleStream<GeoOut> triStream)
该语义会告知输入装配器环节自动为每一个图元生成一个图元ID,使第一个图元标记为0,第二个图元标记为1……。如果用户没有使用几何着色器,则可以将图元ID参数添加到像素着色器的参数列表中,如果使用了几何着色器,则一定要将图元ID参数添加到几何着色器中。图元ID可以给几何着色器使用,也可以传递到像素着色器中供像素着色器使用。
4、输入装配其可以为每一个图元生成图元ID,也可以为每一个顶点生成顶点ID。
5、纹理数组即存放纹理的数组,在c++中,纹理数组和其他的资源一样,用ID3DResource接口表示,创建ID3DResouce对象的时候,DepthOrArraySize就是用于指定纹理数组中元素个数的属性(如果是3D纹理,则该成员指定的是资源的深度值)。在HLSL中,纹理数组由TextureArray表示,使用纹理数组的时候,需要三个纹理坐标值,前两个坐标值为普通的2D纹理坐标,第三个值为纹理数组中的索引(从0开始)。使用纹理数组的优点之一:我们可以在单次绘制调用过程中,以多种不同的纹理渲染出一系列图元,而每一个图元都有一个指向纹理数组的索引,它指定了图元所应用的纹理。
6、在确定子像素的覆盖情况时,alpha-to-coverage可以让硬件检查像素着色器所返回的alpha数据,开启这项技术可以让树叶和围栏这样的使用alpha遮罩裁剪纹理具有平滑的边缘。
DirectX12 3D 游戏开发与实战第十二章内容的更多相关文章
- DirectX12 3D 游戏开发与实战第八章内容(下)
DirectX12 3D 游戏开发与实战第八章内容(下) 8.9.材质的实现 下面是材质结构体的部分代码: // 简单的结构体来表示我们所演示的材料 struct Material { // 材质唯一 ...
- DirectX12 3D 游戏开发与实战第五章内容
渲染流水线 学习目标: 了解用于在2D图像中表现出场景立体感和空间深度感等真实效果的关键因素 探索如何用Direct3D表示3D对象 学习如何建立虚拟摄像机 理解渲染流水线,根据给定的3D场景的几何描 ...
- DirectX12 3D 游戏开发与实战第八章内容(上)
8.光照 学习目标 对光照和材质的交互有基本的了解 了解局部光照和全局光照的区别 探究如何用数学来描述位于物体表面上某一点的"朝向",以此来确定入射光照射到表面的角度 学习如何正确 ...
- DirectX12 3D 游戏开发与实战第六章内容
利用Direct3D绘制几何体 学习目标 探索用于定义.存储和绘制几何体数据的Direct接口和方法 学习编写简单的顶点着色器和像素着色器 了解如何用渲染流水线状态对象来配置渲染流水线 理解怎样创建常 ...
- DirectX12 3D 游戏开发与实战第九章内容(上)
仅供个人学习使用,请勿转载. 9.纹理贴图 学习目标: 学习如何将局部纹理映射到网格三角形上 探究如何创建和启用纹理 学会如何通过纹理过滤来创建更加平滑的图像 探索如何使用寻址模式来进行多次纹理贴图 ...
- DirectX12 3D 游戏开发与实战第一章内容
DirectX12 3D 第一章内容 学习目标 1.学习向量在几何学和数学中的表示方法 2.了解向量的运算定义以及它在几何学中的应用 3.熟悉DirectXMath库中与向量有关的类和方法 1.1 向 ...
- DirectX12 3D 游戏开发与实战第四章内容(上)
Direct3D的初始化(上) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...
- DirectX12 3D 游戏开发与实战第四章内容(下)
Direct3D的初始化(下) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...
- DirectX12 3D 游戏开发与实战第三章内容
变换 学习目标 理解如何使用矩阵表示线性变换和仿射变换 学习对几何体进行缩放.旋转和平移的坐标变换 根据矩阵之间的乘法运算性质,将多个变换矩阵合并为一个单独的净变换矩阵 找寻不同坐标系之间的坐标转换方 ...
随机推荐
- 【c++ Prime 学习笔记】第10章 泛型算法
标准库未给容器添加大量功能,而是提供一组独立于容器的泛型算法 算法:它们实现了一些经典算法的公共接口 泛型:它们可用于不同类型的容器和不同类型的元素 利用这些算法可实现容器基本操作很难做到的事,例如查 ...
- Scrum Meeting 0607
零.说明 日期:2021-6-7 任务:简要汇报两日内已完成任务,计划后两日完成任务 一.进度情况 组员 负责 两日内已完成的任务 后两日计划完成的任务 困难 qsy PM&前端 重新设计产品 ...
- 【好好编程-技术博客】微信小程序开发中前后端的交互
微信小程序开发中前后端的交互 微信小程序的开发有点类似与普通网页的开发,但是也不尽然相同.小程序的主要开发语言是JavaScript,开发同普通的网页开发有很大的相似性,对于前端开发者而言,从网页开发 ...
- Spring Cloud Alibaba 使用Nacos作为服务注册中心
为什么需要注册中心? 在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用:服务管理,核心是有个服务注册表,心跳机制动态维护 : 服务注册 创建普通Spring ...
- 震惊,hzoi的考试竟然折磨简单,活到爆!
众所周知,hzoi的考试题非常"简单",那么究竟有多简单呢?最近,一位外国小哥开发出了hzoi的考试竟然折磨简单,活到爆!的方法,这究竟是怎么一回事呢?快和小编一起来看看吧- 满分 ...
- 【BZOJ-2199】奶牛议会
链接: BZOJ-2199 题意: 给出 \(n(1\leq n\leq 1000)\) 个点,\(m(1\leq m\leq 4000)\) 个形如:"点 \(a\) 取 \(ca\) 或 ...
- Python报错ModuleNotFoundError: No module named 'numpy'
转载:https://blog.csdn.net/qq_39779233/article/details/103224712 Python报错ModuleNotFoundError: No modul ...
- 确定两串乱序同构 牛客网 程序员面试金典 C++ Python
确定两串乱序同构 牛客网 程序员面试金典 C++ Python 题目描述 给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串.这里规定大小写为不同字符,且考虑字符串中 ...
- 远程设备管理opendx平台搭建-server,agent以及front实际搭建
本系列文章讲述的是一个系列的第二部分,最终可以搭建一整套设备远程管理平台,与stf不同的是,opendx搭建较为简单,而且由于底层是appium来支持的,所以,较容易支持ios,也容易支持更高版本的安 ...
- FAIL : Keyword 'BuiltIn.Log' expected 1 to 6 arguments, got 12(解决方法)
RF运行关键字:Run Keyword If ,log输出报错"FAIL : Keyword 'BuiltIn.Log' expected 1 to 6 arguments, got 12. ...