Unity中实现平面截断的效果

有些时候我们希望让三维物体在一个面某方向上可以看到,另一方向不能看到
就像这样

这时候需要写一个shader来实现这个功能

<0x01> 原理

既然是某个面一侧可以显示,一侧不能显示
我们可以联想到判断面正反的量——法向

所谓法向是一个垂直于平面的向量,向量正方向表示平面正面
而我们知道,通过向量乘法,我们可以判断俩向量夹角是否小于90°
通过俩向量得出的值即可判断渲染的部分在面的正侧还是背侧

<0x02> 实现

这里通过Unity Shader Graph实现
通过Shader Graph可以很方便地继承URP自带的那几个Shader的效果
比如默认的Lit Shader

首先先新建一个Shader Graph
选择Lit Shader Graph

在Shader Graph中创建三个参数

在Graph Inspector的Graph Settings中打开Alpha Clipping,然后这样连接

这里暴露3个参数
PlanePosition表示平面的位置
PlaneNormal表示平面法向方向
BaseMap表示物体的材质

平面的参数需要通过脚本传入,所以要新建一个脚本,代码如下

using UnityEngine;

// 让脚本可以在编辑模式中运行
[ExecuteInEditMode]
public class PlaneCutShaderSetter : MonoBehaviour
{
    public Transform plane;
    
    private static readonly int PlanePositionID = Shader.PropertyToID("_PlanePosition");
    private static readonly int PlaneNormalID = Shader.PropertyToID("_PlaneNormal");

    private Renderer _renderer;
    private Material _material;

	// 通过Awake,使得添加脚本时自动更改材质
    private void Awake()
    {
        _renderer = GetComponent<Renderer>();
        _renderer.material = new Material(Shader.Find("Shader Graphs/PlaneCutShader"));
        _material = _renderer.sharedMaterial;
    }

    private void Update()
    {
        if (!plane) return;
        _material.SetVector(PlanePositionID, plane.position);
        _material.SetVector(PlaneNormalID, plane.up);
    }

	// 通过OnDestroy,使得销毁组件时更改材质为默认材质
    private void OnDestroy()
    {
        _renderer.material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
    }
}

<0x03> 测试

在场景中添加一个物体和一个平面
然后在物体上添加上面写的脚本组件
在组件中指定平面
效果如下

这就实现了简单的平面截断效果

<0x04> 优化

不指定平面情况

目前的实现是要求给一个平面,如果不给的话显示会有问题
如果需要不指定平面就正常显示的话有两个思路

首先是给PlaneNormal和PlanePosition设定默认值
比如说PlanePosition设定默认值给一个很低的值,然后给PlaneNormal设定(0,1,0)

也可以修改Shader Graph,添加新的参数控制是否进行截断处理

然后修改脚本代码,添加不指定平面的情况

using UnityEngine;

[ExecuteInEditMode]
public class PlaneCutShaderSetter : MonoBehaviour
{
    public Transform plane;

    private static readonly int CutEnableID = Shader.PropertyToID("_CutEnable");
    private static readonly int PlanePositionID = Shader.PropertyToID("_PlanePosition");
    private static readonly int PlaneNormalID = Shader.PropertyToID("_PlaneNormal");

    private Renderer _renderer;
    private Material _material;

    private void Awake()
    {
        _renderer = GetComponent<Renderer>();
        _renderer.material = new Material(Shader.Find("Shader Graphs/PlaneCutShader"));
        _material = _renderer.sharedMaterial;
    }

    private void Update()
    {
        if (plane)
        {
            _material.SetFloat(CutEnableID, 1);
            _material.SetVector(PlanePositionID, plane.position);
            _material.SetVector(PlaneNormalID, plane.up);
        }
        else
        {
            _material.SetFloat(CutEnableID, 0);
        }
    }

    private void OnDestroy()
    {
        _renderer.material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
    }
}

需要指定多个平面的情况

没啥特别好的办法,就是在Shader Graph中多复制几个平面判断
然后在脚本中传参
(因为我没找到办法实现数组类型传参)

Licensed under CC BY-NC-SA 4.0