Unity Terrane和网格融合

大家好,OTUS将于6月再次启动Unity游戏开发人员课程预期课程的开始,我们准备了关于该主题的有趣材料的翻译。



今天,我们将讨论如何在Unity中混合地形网格物体(或其他网格物体)。该指南非常先进,但我尝试将其分解为单独的步骤。假定您具有一般的Unity技能和基本的C#知识。该指南是为放大而设计的,但我认为它也可以应用于着色器图。这是我的第一本指南,因此我希望它会足够清楚。如果您想添加一些内容,请告诉我。我在这里包括了一个入门工具包,其中包含该项目的一些其他着色器,以及用于入门的基本工具包。今天的手册中的所有项目文件都可以以5美元的价格提供给我的顾客,但在不久的将来,所有人都可以使用。

入门套件
整个项目

注意:考虑到本指南的篇幅,我想确保您了解此着色器的优缺点。今天我们将讨论着色器的两个版本。第一个允许您混合一个纹理,因此着色器适用于高分辨率纹理,但事实证明您仅限于一个纹理。第二个着色器允许您混合低分辨率或一种颜色的任意数量的Terranes纹理(或经过某些修改的Terranes网格纹理),并在统一地形和地形网格物体上工作。这些着色器没有正常的全功能混合,这在无样式的游戏中很有用,但是有一种方法可以自己实现。

所以,让我们开始吧。首先,我将提出问题并深入研究解决方案的基础理论。要在人脸和其他网格物体之间进行混合,您需要以某种方式告诉着色器与人脸相交的位置。说起来比做起来容易,但是有办法。它由什么组成?渲染纹理将帮助我们

什么是渲染纹理?


实际上,“ 渲染纹理”是摄影机的位置,该位置保存在资产文件中。游戏中的“ 渲染纹理”最常用于创建视频监控屏幕之类的事情。我们可以使用“ 渲染纹理 ”(Render Texture)将相机视图保存在编辑器中,以便可以将该纹理用作着色器。这很有用,因为我们可以将有关地形的信息(例如高度,法线和颜色)烘焙到纹理中,然后在运行时将其用于在网格和Terran之间进行混合。


用于在无标题鹅游戏中在电视上显示鹅图像的纹理渲染

客制化


首先,让我们创建一个新的场景和地形。设置一些适合工作的尺寸,例如200x200。现在,您可以根据需要排列地形。然后创建一个新层并将其命名为“地形”,并为该层分配地形。这是必需的,以便相机蒙版可以在“ 渲染纹理”中记录地形


我的杰作terrane

项目源文件有一个预制的名为“ BlendBakingCamera”的预制 -将其拖到舞台上。您将获得一个简单的正交摄影机。在相机上,您需要在新的地形层上放置一个消隐面罩将相机放置在地形中央,稍微高于地形最高点。然后调整远剪辑平面,以使摄像机看到地面。因此,场景应如下所示:


替换着色器


设置好摄像机之后,您需要找到一种记录地形数据的方法。为此,我们需要替换着色器。替换着色器似乎是个可疑的前景;我本人很长一段时间都不了解它是如何工作的。但是实际上,一切都很简单有效。本质上,使用“ 替换着色器”意味着用单个着色器渲染摄像机视场中的每个对象,而不管对象上叠加了哪个着色器。结果,所有对象将使用选定的Replacement Shader(实际上只是一个常规着色器)进行渲染。

我们需要混合的着色器是深度着色器。它渲染场景的深度,是创建我们的混合效果的关键组成部分,因为它将相机的深度值写入纹理,以便我们以后可以读取它们。总体上了解有关此着色器和Replacement Shader的更多信息,建议您阅读Unity中的使东西看起来不错的本手册


深度着色器示例

开始烘焙吧


让我们创建一个新类,并将其命名为“ TerrainBlendingBaker”让我们从为基本地形实现深度蒙版开始。稍后,我们将返回此脚本以添加颜色和法线。

定义几个变量。

//Shader that renders object based on distance to camera
public Shader depthShader;
//The render texture which will store the depth of our terrain
public RenderTexture depthTexture;
//The camera this script is attached to
private Camera cam;

现在,让我们创建一个新方法并将其命名为“ UpdateBakingCamera”在这种方法中,我们将确定着色器在全局变量中渲染混合可能需要的相机数据。

private void UpdateBakingCamera()
    {
        //if the camera hasn't been assigned then assign it
        if (cam == null)
        {
            cam = GetComponent<Camera>();
        }
 
        //the total width of the bounding box of our cameras view
        Shader.SetGlobalFloat("TB_SCALE", GetComponent<Camera>().orthographicSize * 2);
        //find the bottom corner of the texture in world scale by subtracting the size of the camera from its x and z position
        Shader.SetGlobalFloat("TB_OFFSET_X", cam.transform.position.x - cam.orthographicSize);
        Shader.SetGlobalFloat("TB_OFFSET_Z", cam.transform.position.z - cam.orthographicSize);
        //we'll also need the relative y position of the camera, lets get this by subtracting the far clip plane from the camera y position
        Shader.SetGlobalFloat("TB_OFFSET_Y", cam.transform.position.y - cam.farClipPlane);
        //we'll also need the far clip plane itself to know the range of y values in the depth texture
        Shader.SetGlobalFloat("TB_FARCLIP", cam.farClipPlane);
 
        //NOTE: some of the arithmatic here could be moved to the shader but keeping it here makes the shader cleaner so ¯\_(ツ)_/¯
    }

现在让我们将地面的深度烘烤到纹理中。

// The context menu tag allows us to run methods from the inspector (https://docs.unity3d.com/ScriptReference/ContextMenu.html)
[ContextMenu("Bake Depth Texture")]
public void BakeTerrainDepth()
{
    //call our update camera method 
    UpdateBakingCamera();
 
    //Make sure the shader and texture are assigned in the inspector
    if (depthShader != null && depthTexture != null)
    {
        //Set the camera replacment shader to the depth shader that we will assign in the inspector 
        cam.SetReplacementShader(depthShader, "RenderType");
        //set the target render texture of the camera to the depth texture 
        cam.targetTexture = depthTexture;
        //set the render texture we just created as a global shader texture variable
        Shader.SetGlobalTexture("TB_DEPTH", depthTexture);
    }
    else
    {
        Debug.Log("You need to assign the depth shader and depth texture in the inspector");
    }
}

当我们开始使用着色器时,这些值将变得更加清晰。这是一张小图片,可能会有所启发:


我们将把地形转换为深度纹理,以便稍后阅读并了解在哪里进行混合,

现在我们已经具备在着色器中创建基本混合效果所需的一切。此时,脚本看起来像这样:pastebin.com/xNusLJfh

好吧,现在有了脚本,我们可以将其添加到之前添加的烘焙相机中。初始资产有一个名为“ DepthShader”的着色器(Inresin / Shaders / DepthShader)和一个名为“ DepthTerrainRT”渲染纹理(Inresin / RenderTextures / DepthTextureRT),您需要将它们放在检查器中的相应字段中。

之后,只需在上下文菜单中运行该方法即可在“ 渲染纹理”中烘焙地形深度


着色器


最后,让我们创建一个用于混合的着色器。创建一个新的标准放大着色器并将其打开,将其命名为“ TerrainBlending”

现在我们需要为渲染纹理创建UV 。这将是正在渲染的点与相对于总面积缩放烘焙相机位置之间的差。这里的三个全局变量是我们刚刚在代码中声明的变量。我们还将worldY设置为局部变量,稍后我们将需要它。



让我们看一下深度纹理,将其指定为全局变量(对于此添加纹理样本节点,将其设置为全局并命名为“ TB_DEPTH”),如果将输出放在debug amplify shader'a field中,我们可以看到会发生什么。使用将要应用我们的新着色器的材质创建一个平面。


因此,在着色器中,我们具有有关深度的信息,现在我们需要在y中添加一个偏移量以进行混合。



该块缩放远剪贴平面蒙版的y位置,沿着渲染点的y轴从世界位置减去该值,然后最终将其移动到相机边界框的下侧(相机的y位置减去远剪贴平面)。

已经有东西了!我们看到了平面的边缘如何与地形融合。



好的,让我们更加控制混合。



现在我们可以控制混合地形的厚度和面积了。



我想我们甚至可以增加一点噪音。让我们使用世界位置从纹理生成噪声。



噪声纹理位于启动项目的纹理文件夹中,可以在检查器中指定它们,也可以在着色器本身中将其指定为常量。

最后是时候添加一些纹理了!首先,让我们使用一些简单的单色纹理,我向带有纹理资源的文件夹中添加了两个纹理。使“ SingleColorGrass”纹理成为地形纹理。然后,在着色器中,您需要创建一个Terrane纹理和一个对象纹理节点。我们将在刚创建的蒙版的红色通道上在它们之间切换。




这是完整的着色器,



添加自定义香椿照明或未照明的模型为该着色器提供最佳效果。我打开不亮赞助商可以使用完整软件包中的Terrane着色器和着色的着色器版本。


未照明的Terrane和网格物体

我还建议为该地形和其他网格物体添加一个三面体着色器。我可以在下一指南中考虑此问题。

好吧,我们几乎已经完成了本指南的主题。我添加了一些可以帮助着色器扩展的部分。

着色器扩展-正常


我在文件中添加了常规着色器,您可以使用它将法线写入到地面,也可以使用混合。对于我的游戏,我不需要常规的混合,所以我做了一些实验,看起来实验很成功,而且我的想法适用于高度没有高度变化的地形。我在目录中将普通映射着色器与起始集着色器一起包括在内。在这里,您可以看到我对法线贴图的基本实现:



代码与深度图几乎相同,但是这次我们将法线贴图用作替换着色器。我还添加了一组渲染纹理以记录法线(此处可能需要其他配置)。

为了使法线正常工作,可能需要进行一些与调整有关的工作,并且还可能存在与低级地形有关的限制(但我没有进行足够的测试来确认这一点)。

着色器扩展-所有颜色


我不会深入探讨该主题的细节,因为它超出了我的游戏所需的范围,但是我在编写本指南时就想到了这一点。要添加多种颜色的混合,我们可以选择不发光的地形颜色并将其另存为纹理。在这种情况下,我们受到相当低分辨率的限制,但是当使用单色地形纹理和低分辨率纹理或使用模糊出血时,此方法效果很好。进行较小的调整,它们也可以应用于地形网格物体。

这是多色选项的代码:

[Header("The following settings are only if using the multi-color terrain shader")]
//Shader that renders the unlit terraom of an object
public Shader unlitTerrainShader;
//The render texture which will store the normals of our terrain
public RenderTexture surfaceTexture;
//An unlit terrain material used to capture the texture of our terrain without any lighting
public Material unlitTerrainMaterial;
//The terrain you want to capture the textures of
public Terrain yourTerrain;
 
[ContextMenu("Bake Surface Texture")]
public void BakeTerrainSurface()
{
    UpdateBakingCamera();
 
    //return if there is no terrain assigned
    if (yourTerrain == null)
    {
        Debug.Log("You need to assign a terrain to capture surface texture");
        return;
    }
 
    StartCoroutine(BakeColors());
}
 
IEnumerator BakeColors()
{
    Material tempTerrainMaterial = yourTerrain.materialTemplate;
 
    yourTerrain.materialTemplate = unlitTerrainMaterial;
 
    yield return 0;
 
    cam.SetReplacementShader(unlitTerrainShader, "RenderType");
    cam.targetTexture = surfaceTexture;
    Shader.SetGlobalTexture("TB_SURFACE", surfaceTexture);
 
    yield return 0;
 
    cam.targetTexture = null;
    yourTerrain.materialTemplate = tempTerrainMaterial;
 
    yield return null;
 
}

着色器的唯一变化是,我们使用全局位置定义的地形表面纹理,而不是使用预定义的纹理,该纹理将相对位置用作UV。附加扩展使您可以更舒适地混合纹理。


混合多个纹理

这是带有普通混合着色器的完整多色图形:




结论


恭喜您已经阅读了这一点!正如我所说,这是我的第一本指南,因此,我非常感谢您提供反馈。如果您想支持我创建游戏和教程,请在此处查看或订阅我的Twitter



了解有关该课程的更多信息

All Articles