几何着色器的一般形式如下
[maxvertexcount(N)]
void ShaderName (
PrimitiveType InputVertexType InputName [NumElements],
inout StreamOutputObject<OutputVertexType> OutputName)
{
// Geometry shader body…
}
第一排方括号内的是最大输出的顶点数量,第一个参数是顶点着色器输出,也就是几何着色器的输入,输入类型有以下五种:
第二个参数要用inout标注,输出的是一个StreamOutputObject,这是一个模板类,取值有以下几种
可以用StreamOutputObject
方法来输出顶点,输出的一定是strip,如果希望输出list,可以用StreamOutputObject
来让strip重新开始。
有的时候可以吧几张图片放在同一张纹理上,形成一个纹理数组,hlsl中输入参数长这样
Texture2DArray gTreeMapArray;
当然也可以用以下方法代替:
Texture2D TexArray[4];
…
float4 PS(GeoOut pin) : SV_Target
{
float4 c = TexArray[pin.PrimID%4].Sample(samLinear,pin.Tex);
新的DX12支持后面这种,但是旧的版本不一定对这种方法支持的很好,所以有的时候我们需要用Texture Array来达到同样效果。
在Texture Array上采样可以通过下面这种方法
float3 uvw = float3(pin.Tex, pin.PrimID%4);
float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap, uvw) * gDiffuseAlbedo;
uvw的前两维就是uv,第三维w是数组下标,从0开始。
然后primitiveID可以在几何着色器的输入种通过SV_PrimitiveID获得,如下:
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID,
inout TriangleStream<GeoOut> triStream)
{
···
gout.PrimID = primID;
···
}
如果有几何着色器的话,primitive id只能在几何着色器的输入签名中出现,如果没有几何着色器的话,则可以在像素着色器的输入参数中获取:
float4 PS(VertexOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
// Pixel shader body...
}
载入Texture Array的时候,绑定srv的操作不一样,应该注意,以下会给出代码。
接下来还有几个概念可以了解一下
横着的是texture数组,一竖条称为一个array slice,竖着的是mipmap列,一横条称作一个mip slice,其中一张图叫做一个subresource,subresource的默认标号方法如下
要获取一个subresource的标号的话,可以用如下方法
inline UINT D3D12CalcSubresource( UINT MipSlice, UINTArraySlice, UINT PlaneSlice, UINT MipLevels, UINT ArraySize )
{
return MipSlice + ArraySlice * MipLevels + PlaneSlice * MipLevels * ArraySize;
}
之前在blend一章也提到过这是个对叶子、头发等渲染非常有用的一个设置,首先这个问题来源于,计算msaa的时候,以4x msaa为例,着色器只会在采样中心运行一次,然后在4个小方块里检查coverage,然后把颜色取平均,但是问题来了,这个检查coverage的时候是只看多边形有没有cover这个小方块,是不考虑alpha通道的,所以这样的话效果会不好,因此,我们希望把alpha低的地方在取平均的时候加权更小,比如有一个方块虽然被多边形cover了但他的alpha只有0.5,那么颜色取平均的时候这个方块的颜色还要再乘个0.5,这样的话,出来的效果就是alpha越低的地方颜色越淡,过渡也就会均匀很多,按理说我们希望所有有透明度的地方都打开alpha-to-coverage检测,这个设置在D3D12_BLEND_DESC::AlphaToCoverageEnable = true
,叶子、头发之类的都可以用这个加强效果,开销主要来源于msaa,ff的头发就是用了这种。
接下来实现一个非常简单的billboard demo,即用一系列点经过几何着色器变成一个面片(两个三角面),然后在这个三角面上渲染树的图案,同时,树的y轴永远朝上,xz轴永远朝向摄像机。
整个demo基于之前的blend demo,下面只给出修改了的关键部分代码
首先创建srv的时候,树的贴图是个Texture Array,创建的时候ViewDimension是D3D12_SRV_DIMENSION_TEXTURE2DARRAY,此外还有些别的设置不同,可以注意一下。
void TreeBillboardsApp::BuildDescriptorHeaps()
{
//
// Create the SRV heap.
//
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = 4;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&mSrvDescriptorHeap)));
//
// Fill out the heap with actual descriptors.
//
CD3DX12_CPU_DESCRIPTOR_HANDLE hDescriptor(mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
auto grassTex = mTextures["grassTex"]->Resource;
auto waterTex = mTextures["waterTex"]->Resource;
auto fenceTex = mTextures["fenceTex"]->Resource;
auto treeArrayTex = mTextures["treeArrayTex"]->Resource;
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = grassTex->GetDesc().Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
md3dDevice->CreateShaderResourceView(grassTex.Get(), &srvDesc, hDescriptor);
// next descriptor
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = waterTex->GetDesc().Format;
md3dDevice->CreateShaderResourceView(waterTex.Get(), &srvDesc, hDescriptor);
// next descriptor
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = fenceTex->GetDesc().Format;
md3dDevice->CreateShaderResourceView(fenceTex.Get(), &srvDesc, hDescriptor);
// next descriptor
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
auto desc = treeArrayTex->GetDesc();
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY;
srvDesc.Format = treeArrayTex->GetDesc().Format;
srvDesc.Texture2DArray.MostDetailedMip = 0;
srvDesc.Texture2DArray.MipLevels = -1;
srvDesc.Texture2DArray.FirstArraySlice = 0;
srvDesc.Texture2DArray.ArraySize = treeArrayTex->GetDesc().DepthOrArraySize;
md3dDevice->CreateShaderResourceView(treeArrayTex.Get(), &srvDesc, hDescriptor);
}
然后树的billboard单独要一个shader,输入只要pos和size就行了,size是二维的,存halfWidth和halfHeight
void TreeBillboardsApp::BuildShadersAndInputLayouts()
{
const D3D_SHADER_MACRO defines[] =
{
"FOG", "1",
NULL, NULL
};
const D3D_SHADER_MACRO alphaTestDefines[] =
{
"FOG", "1",
"ALPHA_TEST", "1",
NULL, NULL
};
mShaders["standardVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "VS", "vs_5_0");
mShaders["opaquePS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", defines, "PS", "ps_5_0");
mShaders["alphaTestedPS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", alphaTestDefines, "PS", "ps_5_0");
mShaders["treeSpriteVS"] = d3dUtil::CompileShader(L"Shaders\\TreeSprite.hlsl", nullptr, "VS", "vs_5_0");
mShaders["treeSpriteGS"] = d3dUtil::CompileShader(L"Shaders\\TreeSprite.hlsl", nullptr, "GS", "gs_5_0");
mShaders["treeSpritePS"] = d3dUtil::CompileShader(L"Shaders\\TreeSprite.hlsl", alphaTestDefines, "PS", "ps_5_0");
mStdInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
mTreeSpriteInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "SIZE", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
}
然后创建树的顶点的时候只要创建单独的一些顶点就行,我们让几何着色器来把顶点变成面,这样开销比较小。创建顶点的代码不再列出。
然后在创建Render Item的时候,primitive类型选PointList。
auto treeSpritesRitem = std::make_unique<RenderItem>();
treeSpritesRitem->World = MathHelper::Identity4x4();
treeSpritesRitem->ObjCBIndex = 3;
treeSpritesRitem->Mat = mMaterials["treeSprites"].get();
treeSpritesRitem->Geo = mGeometries["treeSpritesGeo"].get();
treeSpritesRitem->PrimitiveType = D3D11_PRIMITIVE_TOPOLOGY_POINTLIST;
treeSpritesRitem->IndexCount = treeSpritesRitem->Geo->DrawArgs["points"].IndexCount;
treeSpritesRitem->StartIndexLocation = treeSpritesRitem->Geo->DrawArgs["points"].StartIndexLocation;
treeSpritesRitem->BaseVertexLocation = treeSpritesRitem->Geo->DrawArgs["points"].BaseVertexLocation;
mRitemLayer[(int)RenderLayer::AlphaTestedTreeSprites].push_back(treeSpritesRitem.get());
然后创建PSO的时候,拓扑类型也要是point。
//
// PSO for tree sprites
//
D3D12_GRAPHICS_PIPELINE_STATE_DESC treeSpritePsoDesc = opaquePsoDesc;
treeSpritePsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["treeSpriteVS"]->GetBufferPointer()),
mShaders["treeSpriteVS"]->GetBufferSize()
};
treeSpritePsoDesc.GS =
{
reinterpret_cast<BYTE*>(mShaders["treeSpriteGS"]->GetBufferPointer()),
mShaders["treeSpriteGS"]->GetBufferSize()
};
treeSpritePsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["treeSpritePS"]->GetBufferPointer()),
mShaders["treeSpritePS"]->GetBufferSize()
};
treeSpritePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT;
treeSpritePsoDesc.InputLayout = { mTreeSpriteInputLayout.data(), (UINT)mTreeSpriteInputLayout.size() };
treeSpritePsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&treeSpritePsoDesc, IID_PPV_ARGS(&mPSOs["treeSprites"])));
最后Draw的时候,先渲染opaque和alpha tested,然后是树,最后是transparent
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
mCommandList->SetPipelineState(mPSOs["alphaTested"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::AlphaTested]);
mCommandList->SetPipelineState(mPSOs["treeSprites"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::AlphaTestedTreeSprites]);
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
完整的shader如下,可以看下里面几何着色器干了什么
// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif
// Include structures and functions for lighting.
#include "LightingUtil.hlsl"
Texture2DArray gTreeMapArray : register(t0);
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
// Constant data that varies per frame.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
};
// Constant data that varies per material.
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight;
float4 gFogColor;
float gFogStart;
float gFogRange;
float2 cbPerObjectPad2;
// Indices [0, NUM_DIR_LIGHTS) are directional lights;
// indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
// indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
// are spot lights for a maximum of MaxLights per object.
Light gLights[MaxLights];
};
cbuffer cbMaterial : register(b2)
{
float4 gDiffuseAlbedo;
float3 gFresnelR0;
float gRoughness;
float4x4 gMatTransform;
};
struct VertexIn
{
float3 PosW : POSITION;
float2 SizeW : SIZE;
};
struct VertexOut
{
float3 CenterW : POSITION;
float2 SizeW : SIZE;
};
struct GeoOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD;
uint PrimID : SV_PrimitiveID;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Just pass data over to geometry shader.
vout.CenterW = vin.PosW;
vout.SizeW = vin.SizeW;
return vout;
}
// We expand each point into a quad (4 vertices), so the maximum number of vertices
// we output per geometry shader invocation is 4.
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID,
inout TriangleStream<GeoOut> triStream)
{
//
// Compute the local coordinate system of the sprite relative to the world
// space such that the billboard is aligned with the y-axis and faces the eye.
//
float3 up = float3(0.0f, 1.0f, 0.0f);
float3 look = gEyePosW - gin[0].CenterW;
look.y = 0.0f; // y-axis aligned, so project to xz-plane
look = normalize(look);
float3 right = cross(up, look);
//
// Compute triangle strip vertices (quad) in world space.
//
float halfWidth = 0.5f*gin[0].SizeW.x;
float halfHeight = 0.5f*gin[0].SizeW.y;
float4 v[4];
v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);
//
// Transform quad vertices to world space and output
// them as a triangle strip.
//
float2 texC[4] =
{
float2(0.0f, 1.0f),
float2(0.0f, 0.0f),
float2(1.0f, 1.0f),
float2(1.0f, 0.0f)
};
GeoOut gout;
[unroll]
for(int i = 0; i < 4; ++i)
{
gout.PosH = mul(v[i], gViewProj);
gout.PosW = v[i].xyz;
gout.NormalW = look;
gout.TexC = texC[i];
gout.PrimID = primID;
triStream.Append(gout);
}
}
float4 PS(GeoOut pin) : SV_Target
{
float3 uvw = float3(pin.TexC, pin.PrimID%3);
float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap, uvw) * gDiffuseAlbedo;
#ifdef ALPHA_TEST
// Discard pixel if texture alpha < 0.1. We do this test as soon
// as possible in the shader so that we can potentially exit the
// shader early, thereby skipping the rest of the shader code.
clip(diffuseAlbedo.a - 0.1f);
#endif
// Interpolating normal can unnormalize it, so renormalize it.
pin.NormalW = normalize(pin.NormalW);
// Vector from point being lit to eye.
float3 toEyeW = gEyePosW - pin.PosW;
float distToEye = length(toEyeW);
toEyeW /= distToEye; // normalize
// Light terms.
float4 ambient = gAmbientLight*diffuseAlbedo;
const float shininess = 1.0f - gRoughness;
Material mat = { diffuseAlbedo, gFresnelR0, shininess };
float3 shadowFactor = 1.0f;
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
pin.NormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
#ifdef FOG
float fogAmount = saturate((distToEye - gFogStart) / gFogRange);
litColor = lerp(litColor, gFogColor, fogAmount);
#endif
// Common convention to take alpha from diffuse albedo.
litColor.a = diffuseAlbedo.a;
return litColor;
}
渲染出来的效果如图所示