Doris232 发表于 2021-11-28 06:00

自定义Unity Terrain材质来刷草-Part 2

书接上文,我们已经知道如何使用几何着色器来生成草的Mesh,那么现在该考虑怎么拿到地形的数据,并且在地形上面刷草。
如果对unity的地形机制有所了解的话,可以知道地形数据里实际是有一张height map还有一张control map,前一张图保存地形的高度信息,后一张图保存terrain layer的信息。在渲染过程中,要判断某一个片段显示什么,只需要根据当前的uv从control map中获得当前片段层的信息,然后取得层的颜色进行混合,最后获得当前片段的颜色。
TerrainLitPasses.hlsl中的代码片段如下:
float2 splatUV = (IN.uvMainAndLM.xy * (_Control_TexelSize.zw - 1.0f) + 0.5f) * _Control_TexelSize.xy;
half4 splatControl = SAMPLE_TEXTURE2D(_Control, sampler_Control, splatUV);

half weight;
half4 mixedDiffuse;
half4 defaultSmoothness;
SplatmapMix(IN.uvMainAndLM, IN.uvSplat01, IN.uvSplat23, splatControl, weight, mixedDiffuse, defaultSmoothness, normalTS);
half3 albedo = mixedDiffuse.rgb;上文中daniel ilett使用了一张Visibility map来在一个平面上刷草,我们如果能够拿到地形中control map的数据,知道哪里是属于草的layer,那么就在那里生成草的网格并且显示就好了。这样根本就不需要额外的map,额外的工具去存储草的位置,只要使用unity本身的terrain工具进行刷草就好了。这里唯一额外需要做的是修改地形的材质球,让它在刷完地形之后,再显示草。
以下面这个场景为例,这个地形有两个层,layer 0 为泥土,layer 1为草。


通过一个自定义的材质,我们可以看到这个场景的control map,其中红色通道表示layer 0的值,绿色通道表示layer 1的值


只要能在显示绿色的范围内显示草,就完成目标了。
代码完成后的刷草效果如下:





Grass Layer Index定义哪一层用来刷草,其他参数控制草的显示

下面解释一下实现细节:
URP 10中地形着色器文件为TerrainLit.shader,复制一下,并在中间插入一个新的Pass,名叫GrassPass。
我们有两个阶段可以获取control map的信息来判断要不要显示草。
第一个是细分曲面阶段,在patchConstantFunc拿到要细分三角形的顶点时,可以判断三个顶点在不在grass layer,这里有两个方案:
第一个是三个顶点全在grass层才进行细分,不过这样在边缘处效果不佳,能够很明显看出三角形的边缘。


第二个方案是只要有一个顶点在grass层,就细分。工程代码中采用第二个方案,效果是可以接受的。


细分曲面着色器部分代码如下
bool needTessellation(TessellationControlPoint vert)
{
        float2 splatUV = (vert.texcoord * (_Control_TexelSize.zw - 1.0f) + 0.5f) * _Control_TexelSize.xy;
        half4 splatControl = SAMPLE_TEXTURE2D_LOD(_Control, sampler_Control, splatUV, 0);

        return splatControl >= 0.1;
}

// Tessellation hull and domain shaders derived from Catlike Coding's tutorial:
// https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

// The patch constant function is where we create new control
// points on the patch. For the edges, increasing the tessellation
// factors adds new vertices on the edge. Increasing the inside
// will add more 'layers' inside the new triangle.
TessellationFactors patchConstantFunc(InputPatch<TessellationControlPoint, 3> patch)
{
        TessellationFactors f;

        if (needTessellation(patch) || needTessellation(patch) || needTessellation(patch))
        {
                f.edge = tessellationEdgeFactor(patch, patch);
                f.edge = tessellationEdgeFactor(patch, patch);
                f.edge = tessellationEdgeFactor(patch, patch);
                f.inside = (f.edge + f.edge + f.edge) / 3.0f;
        }else
        {
                f.edge = 1;
                f.edge = 1;
                f.edge = 1;
                f.inside = 1;
        }

        return f;
}

如果要学习如何写曲面细分着色器,可以参考CatLikeCoding的教程
另外曲面细分阶段还需要注意partitioning的值,CatLikeCoding教程中默认定义为integer,我们需要将这个值改为fractional_odd。
// The hull function is the first half of the tessellation shader.
// It operates on each patch (in our case, a patch is a triangle),
// and outputs new control points for the other tessellation stages.
//
// The patch constant function is where we create new control points
// (which are kind of like new vertices).





TessellationControlPoint hull(InputPatch<TessellationControlPoint, 3> patch, uint id : SV_OutputControlPointID)
{
        return patch;
}之所以这么改是因为unity地形引擎传过来的顶点是变化的,距离远时三角面大,距离近时三角面小。我们细分曲面的TessellationFactors是通过顶点距离除以草的间距计算的,如果partitioning值为integer,有可能导致同样范围区域在不同观察距离时细分的三角面数量不一致,导致摄像机移动时前方草在不断的变换位置。



同样一个小山坡,距离远时的Mesh



同样一个小山坡,距离近时的Mesh

第二个阶段是在几何着色器阶段,我们在这里获取grass layer信息判断要不要新生成顶点来显示草。这里阈值设置为0.1,只要草layer的值小于0.1就不生成草。
几何着色器代码如下:

void GrassGeom(point InterpolatorsVertex IN, inout TriangleStream<GeometryOutput> triStream)
{
// bool b = UnityWorldViewFrustumCull(IN.position + unity_ObjectToWorld._m03_m13_m23, IN.position + unity_ObjectToWorld._m03_m13_m23, IN.position + unity_ObjectToWorld._m03_m13_m23, 2.0);

float3 pos = IN.vertex; //world position

float distanceFromCamera = distance(pos, _WorldSpaceCameraPos);
if (distanceFromCamera > _MaxViewDistance)
{
    return;
}

float2 splatUV = (IN.uv * (_Control_TexelSize.zw - 1.0f) + 0.5f) * _Control_TexelSize.xy;
half4 splatControl = SAMPLE_TEXTURE2D_LOD(_Control, sampler_Control, splatUV, 0);

if (splatControl < 0.1)
{
    return;
}                               

float3 vNormal = IN.normal;
float4 vTangent = IN.tangent;
float3 vBinormal = cross(vNormal, vTangent.xyz) * vTangent.w;

float3x3 tangentToLocal = float3x3(
        vTangent.x, vBinormal.x, vNormal.x,
        vTangent.y, vBinormal.y, vNormal.y,
        vTangent.z, vBinormal.z, vNormal.z
        );

//We use the input position pos as the random seed for our rotation. This way, every blade will get a different rotation, but it will be consistent between frames.
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * TWO_PI, float3(0, 0, 1.0f));

//We use the position again as our random seed, this time swizzling it to create a unique seed.
float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * PI * 0.5, float3(-1.0f, 0, 0));


float2 windUV = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;
float2 windSample = (tex2Dlod(_WindDistortionMap, float4(windUV, 0, 0)).xy * 2 - 1) * _WindStrength;
// float2 windSample = float2(0.73, 0.73);
// float2 windSample = (float2(0.5, 1) * 2 - 1) * _WindStrength;

float3 windAxis = normalize(float3(windSample.x, windSample.y, 0));
float3x3 windRotation = AngleAxis3x3(PI * windSample, windAxis);

// Transform the grass blades to the correct tangent space.
float3x3 baseTransformationMatrix = mul(tangentToLocal, facingRotationMatrix);
float3x3 tipTransformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix);
                               
float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth;
float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight;               
float forward = rand(pos.yyz) * _BladeForward;

width = lerp(0, width, splatControl);
height = lerp(0, height, splatControl);

// Interactivity
float3 dis = distance(_PositionMoving, pos); // distance for radius
float3 radius = 1 - saturate(dis / _InteractorRadius); // in world radius based on objects interaction radius
float3 sphereDisp = pos - _PositionMoving; // position comparison
sphereDisp *= radius; // position multiplied by radius for falloff
                                                                          // increase strength
sphereDisp = clamp(sphereDisp.xyz * _InteractorStrength, -0.8, 0.8);

float3 faceNormal = float3(0, 1, 0);
faceNormal = mul(faceNormal, facingRotationMatrix);

for (int i = 0; i < BLADE_SEGMENTS; i++)
{
        float t = i / (float)BLADE_SEGMENTS;

        float segmentWidth = width * (1 - t);
        float segmentHeight = height * t;
        float segmentForward = pow(t, _BladeCurve) * forward;

        // the first (0) grass segment is thinner
        segmentWidth = i == 0 ? width * 0.3 : segmentWidth;
                                       
        float3 offset = float3(segmentWidth, segmentForward, segmentHeight);
                               
        float3x3 transformMatrix = i == 0 ? baseTransformationMatrix : tipTransformationMatrix;

        // first grass (0) segment does not get displaced by interactivity
        float3 newPos = i == 0 ? pos : pos + (float3(sphereDisp.x, sphereDisp.y, sphereDisp.z) * t);

        triStream.Append(GenerateGrassVertex(newPos, float3( offset.x, offset.y, offset.z), float2(0, t), transformMatrix, faceNormal));
        triStream.Append(GenerateGrassVertex(newPos, float3( -offset.x, offset.y, offset.z), float2(1, t), transformMatrix, faceNormal));
}

    triStream.Append(GenerateGrassVertex(pos + float3(sphereDisp.x * 1.5, sphereDisp.y, sphereDisp.z * 1.5), float3(0, forward, height), float2(0.5, 1), tipTransformationMatrix, faceNormal));

    // restart the strip to start another grass blade
    // triStream.RestartStrip();
}
最后一点要注意的是,要想让Unity地形引擎能够执行我们新加的pass,需要将Terrain Settings中的Draw Instanced勾选去掉。


这个应该是Unity本身的bug,但是目前并没有得到解决。详细可以看下下面这个thread。

最后附上工程源码,希望对你有所帮助。
页: [1]
查看完整版本: 自定义Unity Terrain材质来刷草-Part 2