US4L4——物体切割效果

本章代码关键字

1
2
3
VFACE                // 片元位于模型正面还是反面的语义,一般在片元着色器的浮点数参数中使用,Unity Shader会自动传入,1表示为正面,-1表示为背面
#pragma target 3.0 // 设置着色器模型版本,可以让VFACE​语义使用上的兼容性和稳定性更好
step() // 传入两个参数,若第二个参数大于等于第一个参数,则返回1,否则返回0

物体切割效果

在游戏开发中,物体切割效果就是,物体看似被切割、分割或隐藏一部分的视觉效果。
这种效果常用与游戏和动画中,比如角色攻击时的切割效果,场景中的墙壁切割效果等等。

imageimage

物体切割效果的基本原理

一句话描述它的基本原理:在片元着色器中判断片元的世界坐标是否满足切割条件,
如果满足则直接抛弃片元不渲染,判断片元在模型中的正反面,决定使用哪种纹理进行渲染

关键点:

  • 如何判断世界坐标

    在 Shader 中声明一个 Vector 坐标变量,通过 C# 代码把控制切割位置的物体世界空间位置传递给 Shader
    在 Shader 中用片元的世界坐标和传递进来的世界坐标做比较,在这里,我们可以分三种模式,分别比较 x、y、z 坐标,切割 x、y、z 方向的片元

    1
    material.SetVector("_CuttingPosition", cutObj.transform.position);

    image

  • 如何抛弃片元不渲染

    Unity Shader 中提供了一个内置函数 clip(x)​,它的作用就是在片元着色器中调用时来丢弃片元的,之前在透明度测试时使用过

    具体详见:US3S3L4——透明度测试

    传入的值 x​ 小于0,则会丢弃当前片元,被丢弃的片元不会被进一步处理也就不会被渲染了,
    也就是说,当我们判断片元位置需要被切割时,直接执行 clip​ 函数传入一个负数,这个片元就会被丢弃,不会被渲染了

  • 如何判断片元的正反面

    Unity Shader 中提供了一个语义 VFACE​,它只能在片元着色器中使用,它可以作为参数传递给片元着色器
    传入值 1​ 表示为正面,-1​ 表示为背面,我们可以利用它判断该片元是模型正面还是背面片元,决定使用哪种纹理或颜色进行渲染

    1
    2
    3
    4
    fixed4 frag(v2f i, fixed face: VFACE) : SV_Target
    {
    fixed4 col = face > 0 ? tex2D(_MainTex, i.uv) : tex2D(_BackTex, i.uv);
    }

    注意:在使用它时建议加上编译指令 #pragma target 3.0​ 或 4.0​、5.0
    表示设置着色器模型版本,可以让 VFACE​ 语义使用上的兼容性和稳定性更好

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Pass
    {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0
    //...
    ENDCG
    }

实现物体切割效果的 Shader 代码

  1. 新建 Shader ObjectCutting​,删除无用代码

  2. 属性声明 属性映射

    • 主纹理 _MainTex 2D
    • 背面纹理 _BackTex 2D
    • 切割方向(用来控制比较x、y、z哪个轴)_CuttingDir Float
    • 是否翻转切割 _Invert Float
    • 切割位置(从 C# 传递过来)_CuttingPos Vector
    1
    2
    3
    4
    5
    6
    7
    8
    Properties
    {
    _MainTex("Texture", 2D) = "white"{}
    _BackTex("BackTex", 2D) = "white"{} // 用于渲染模型背面像素的纹理
    _CuttingDir("CuttingDir", Float) = 0 // 切割方向,0代表x方向,1代表y方向,2代表z方向
    _Invert("Invert", Float) = 0 // 是否切割翻转,0代表不翻转,1代表翻转
    _CuttingPos("CuttingPos", Vector) = (0,0,0,0) // 切割的位置
    }
  3. 关闭剔除 因为要两面渲染

    1
    2
    3
    4
    5
    6
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    Cull Off // 正反都要渲染
    Pass { /*...*/ }
    }
  4. 编译指令 #pragma target 3.0​ 让 VFACE​ 兼容性更好

    1
    2
    3
    4
    5
    6
    7
    8
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0

    #include "UnityCG.cginc"
    // ...
    ENDCG
  5. 结构体

    UV、顶点、世界坐标

    1
    2
    3
    4
    5
    6
    struct v2f
    {
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 worldPos : TEXCOORD1; // 世界坐标位置
    };
  6. 顶点着色器函数

    坐标转换、纹理赋值、世界坐标转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    v2f vert (appdata_base v)
    {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

    return o;
    }
  7. 片元着色器函数

    1. 加入 VFACE​ 语义参数

    2. 根据正反面决定采样颜色

    3. 根据切割方向判断是否丢弃(0​ 代表丢弃,1​ 代表不丢弃)

      可以使用 step(edge, x)​ 函数

      • x >= edge​ 返回 1
      • x < edge​ 返回 0
    4. 利用是否翻转切割参数决定是否反转丢弃

    5. 利用 clip​ 函数丢弃片元

    6. 若不丢弃,直接返回颜色

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fixed4 frag (v2f i, fixed face : VFACE) : SV_Target
    {
    // 通过face来进行正反面判断,因为face的语义是VFACE,因此Unity Shader会自动传入对应的参数
    fixed4 color = face > 0 ? tex2D(_MainTex, i.uv) : tex2D(_BackTex, i.uv);

    // 计算丢弃中间值,其中_CuttingDir决定比较哪个方向
    fixed cutValue;
    if (_CuttingDir == 0)
    cutValue = step(_CuttingPos.x, i.worldPos.x); // 若片元的世界坐标x轴方向小于切割位置,说明需要切割
    else if (_CuttingDir == 1)
    cutValue = step(_CuttingPos.y, i.worldPos.y); // 若片元的世界坐标y轴方向小于切割位置,说明需要切割
    else if (_CuttingDir == 2)
    cutValue = step(_CuttingPos.z, i.worldPos.z); // 若片元的世界坐标z轴方向小于切割位置,说明需要切割

    // 如果是切割翻转的,则需要反转丢弃中间值,让原来被切割的变为不被切割的,原来未被切割的变为被切割的
    cutValue = _Invert ? 1 - cutValue : cutValue;

    // 如果丢弃中间值为0,则需要切割,此片元直接丢弃
    if (cutValue == 0)
    clip(-1);

    return color;
    }

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Shader "TeachShader/ObjectCutting"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
_BackTex("BackTex", 2D) = "white"{} // 用于渲染模型背面像素的纹理
_CuttingDir("CuttingDir", Float) = 0 // 切割方向,0代表x方向,1代表y方向,2代表z方向
_Invert("Invert", Float) = 0 // 是否切割翻转,0代表不翻转,1代表翻转
_CuttingPos("CuttingPos", Vector) = (0,0,0,0) // 切割的位置
}
SubShader
{
Tags { "RenderType"="Opaque" }
Cull Off // 正反都要渲染

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0

#include "UnityCG.cginc"

struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1; // 世界坐标位置
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BackTex;
float4 _BackTex_ST;
fixed _CuttingDir;
fixed _Invert;
float4 _CuttingPos;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

return o;
}

fixed4 frag (v2f i, fixed face : VFACE) : SV_Target
{
// 通过face来进行正反面判断,因为face的语义是VFACE,因此Unity Shader会自动传入对应的参数
fixed4 color = face > 0 ? tex2D(_MainTex, i.uv) : tex2D(_BackTex, i.uv);

// 计算丢弃中间值,其中_CuttingDir决定比较哪个方向
fixed cutValue;
if (_CuttingDir == 0)
cutValue = step(_CuttingPos.x, i.worldPos.x); // 若片元的世界坐标x轴方向小于切割位置,说明需要切割
else if (_CuttingDir == 1)
cutValue = step(_CuttingPos.y, i.worldPos.y); // 若片元的世界坐标y轴方向小于切割位置,说明需要切割
else if (_CuttingDir == 2)
cutValue = step(_CuttingPos.z, i.worldPos.z); // 若片元的世界坐标z轴方向小于切割位置,说明需要切割

// 如果是切割翻转的,则需要反转丢弃中间值,让原来被切割的变为不被切割的,原来未被切割的变为被切割的
cutValue = _Invert ? 1 - cutValue : cutValue;

// 如果丢弃中间值为0,则需要切割,此片元直接丢弃
if (cutValue == 0)
clip(-1);

return color;
}
ENDCG
}
}
}

实现物体切割效果的 C# 代码

  1. 新建 C# 脚本 和 Shader 名一样
  2. 加入 [ExecuteAlways]​ 特性,让编辑模式下也运行,可以看到效果
  3. 声明材质球和切割位置对象
  4. 材质球初始化
  5. Update​ 中不停的将切割物体位置传递给 Shader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;

[ExecuteAlways]
public class ObjectCutting : MonoBehaviour
{
private Material material;
public GameObject cutObj;

void Start()
{
material = this.GetComponent<Renderer>().sharedMaterial;
}

void Update()
{
if (material == null || cutObj == null)
return;

material.SetVector("_CuttingPos", cutObj.transform.position);
}
}

显示效果

在将被切割的物体上添加一个 CutObj​ 子对象,并将其关联到 ObjectCutting​ C# 脚本上

image

通过设置添加了 ObjectCutting​ Shader 的 Material 上的 CuttingDir​ 和 Invert​,并拖动 CutObj​ 的位置,即可修改切割效果

image

可见,物体呈现出了被切割的效果,显示出了其中的内部纹理