Shader实现模型削减

Number of views 439

模型削减的运用还是挺多的,它可以使目标物体实现逐步消失,如倒入或倒出杯中的动态水等

image.png

介绍

这篇实验我们以兔子模型作为实验对象,新建GameObject并命名为clip,并让clip与兔子模型产生关联,通过调整该clip的position与rotation使该模型产生切割效果,并在切割面的位置实现变换切割颜色(实际上改变了模型背面的颜色)来实现。

原理

如何使clip的position跟rotation与模型产生关联是实现这个效果的重点,也就是说当前clip对象在调整位置跟旋转时如何对模型进行有效告知?
旋转对模型的影响:我们采用法线与模型顶点的世界坐标的点积进行有效性计算,根据dot(N, W) = |N||W|cosθ(N指的法线向量,W指模型顶点的世界坐标)由于N是单位向量,所以dot(N, W) = |W|cosθ,如图:

image.png

由图中可知,|W|cosθ的值是W向量在N向量方向上的投影长度,根据该结论我们可以新建一个Quad物体,因为Quad物体的顶点数只有四个(模型顶点数如果过多不好进行有效分析)假设ABCD是该物体的四个顶点,O是世界空间的原点,可以简单做图如下:

image.png

假设ON向量=(0,1,0)根据结论可知,|ON1|OB向量OA向量ON向量方向上的“有符号”投影长度,|ON2|是OD向量与OC向量在ON向量反方向上的“有符号”投影长度,而"有符号"的投影长度刚好等于各顶点世界坐标到原点的垂直“有符号”距离。这时我们会很自然的想通过当投影值大于0时进行片元丢弃的方式进行剔除。

if(p>0) {   
    // 丢弃片元的方式   
    discard; 
}

而这种方式会发现当把clip对象放置于点N1位置时,Quad对象的上半部分也会被完全丢弃,并且任意调整clip对象也于事无补,这当然不是我们想要的。

image.png

平移对模型的影响:基于刚刚得到的结论,"有符号"的投影长度刚好等于各顶点世界坐标到原点的垂直“有符号”距离,我们很容易能想到,如果要让clip物体移动时对目标模型产生影响,就需要计算clip物体到原点的垂直距离。如:当clip对象在N1位置(0, 0.5, 0)时,“有符号”投影长度为0.5,而clip的垂直“有符号”距离就需要为-0.5,才能在clip对象在N1位置时对模型不产生切割。当clip的位置在(0, 0.4, 0)时,此时clip的垂直“有符号”距离为-0.4,而在N1位置时的有符号投影长度为0.5,此时0.5+(-0.4) = 0.1,所以会对大于0的值进行片元丢弃。同理当clip对象的位置为(0, -0.3, 0)时,它的垂直有符号距离就为0.3。

那么既要去求取法线,又要去求取clip的垂直有符号距离,似乎很麻烦。幸好unity给我们提供了Plane对象,通过Plane对象的normal属性,我们可以获取法线向量,通过Plane对象的distance属性,我们可以获取到原点的垂直“有符号”距离。(Unity Plane介绍

实验

1.新建C#脚本,命名为ClipModel,该脚本用于传递法线与距离参数。

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine;  
public class ClipModel : MonoBehaviour {     
    // 材质对象,由于接收clip的normal与distance信息     
    public Material mat;      
    // Update is called once per frame     
    void Update() {         
        // Plane对象,传递两个参数,法线向量与该Plane对象经过的位置         
        Plane plane = new Plane(transform.up, transform.position);         
        // 放置法线与distance参数于Vector4中         
        Vector4 dealData = new Vector4(plane.normal.x, plane.normal.y, plane.normal.z, plane.distance);         
        // 传递法线与距离至shader中         
        this.mat.SetVector("_Clip", dealData);     
    } 
}

2.新建Unlit Shader文件,命名为clip,我们会为该着色器添加兰伯特光照模型,便于查看效果。

Shader "Custom/clip" {     
    Properties {         
        _MainColor ("MainColor", Color) = (1,1,1,1)     
    }     
    SubShader {         
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}         
        Pass {             
            CGPROGRAM             
            #pragma vertex vert             
            #pragma fragment frag             
            // make fog work             
            #pragma multi_compile_fog              
            #include "UnityCG.cginc"              
            struct appdata {                 
                 float4 vertex : POSITION;                 
                 float3 normal : NORMAL;                 
                 float2 uv : TEXCOORD0;             
            };              
            struct v2f {                 
                 float2 uv : TEXCOORD0;                 
                 UNITY_FOG_COORDS(1)                 
                 float4 vertex : SV_POSITION;                 
                 float4 w_pos : TEXCOORD1;                 
                 float3 w_normal : TEXCOORD2;             
            };              
            fixed4 _MainColor;             
            // 由C#脚本进行传递,前三个值为法线的值,第四个值为distance             
            uniform half4 _Clip;             
            v2f vert (appdata v) {                 
                 v2f o;                 
                 o.vertex = UnityObjectToClipPos(v.vertex);                 
                 // 顶点坐标转为世界坐标                 
                 o.w_pos = mul(unity_ObjectToWorld, v.vertex);                 
                 // 模型自身的法线向量,转到世界空间中,用于光照计算                 
                 o.w_normal = normalize(UnityObjectToWorldNormal(v.normal));                 
                 UNITY_TRANSFER_FOG(o,o.vertex);                 
                 return o;             
            }              
            fixed4 frag (v2f i) : SV_Target {                 
                 // 通过光照向量与模型法线点乘求取漫反射值                 
                 fixed diffuse = saturate(dot(normalize(UnityWorldSpaceLightDir(i.w_pos.xyz)), i.w_normal));                 
                 // 通过顶点世界坐标与Plane的法线点乘来求取clip的旋转对模型的影响程度                 
                 // 再通过与Plane中的distance相加来求取clip的移动对模型的影响程度  
                 half distance = dot(i.w_pos, _Clip.xyz)+ _Clip.w;
                 // sample the texture                 
                 fixed4 col = _MainColor * diffuse;                 
                 // apply fog                 
                 UNITY_APPLY_FOG(i.fogCoord, col);                 
                 // 相当于前面的discard判断,因为clip函数会对值为负值的片元进行剔除
                 // 所以这里对distance取负
                 clip(-distance);
                 return col;
             }
             ENDCG
         }
     }
}

编写完着色器后,可以把C#组件添加至clip对象上,并新建材质,指定该材质的着色器为Custom/clip,并把该材质赋予ClipModel组件中的mat属性,同时把兔子模型的材质也指定为该材质。运行后的效果:

image.png

当我们移动或旋转clip时,会对模型产生削减的效果,但是发现在切割处出现了镂空的错觉,这是因为我们在着色器处理上并没有打开双面渲染,此时需要在Pass内部添加Cull off表明当前pass需要进行双面渲染。开启后的效果如下:

image.png

开启双面渲染后,发现切割处的颜色是黑色的,那么如何更改背面颜色?此时就需要用到frag函数中的第二个参数,该参数有两个值,当facing为背面时等于-1,当facing为正面时等于1, 因为颜色处理后面需要用到lerp函数,而lerp函数是0~1的有效取值,所以需要把-1转换为0(转换方式为0.5*facing+0.5),这种转换方式是不是很熟悉?半兰伯特光照算法也是通过这种方式。在此之前需要在Properties中新增ClipColor属性,用于背面与正面的颜色区分:

Properties:

Properties {
    _MainColor ("MainColor", Color) = (1,1,1,1)
    _ClipColor ("ClipColor", Color) = (1,1,0,1)
}

frag函数:

fixed4 _ClipColor; 
fixed4 frag (v2f i, fixed facing : VFACE) : SV_Target {
    // 通过光照向量与模型法线点乘求取漫反射值
    fixed diffuse = saturate(dot(normalize(UnityWorldSpaceLightDir(i.w_pos.xyz)), i.w_normal));
    // 通过顶点世界坐标与Plane的法线点乘来求取clip的旋转对模型的影响程度
    // 再通过与Plane中的distance相加来求取clip的移动对模型的影响程度
    half distance = dot(i.w_pos, _Clip.xyz)+ _Clip.w;
    // sample the texture
    fixed4 col = _MainColor * diffuse;
    // 把背面的-1转为0,正面保持为1
    facing = 0.5 * facing + 0.5;
    // 通过lerp函数,当为背面时就让背面的颜色置为_ClipColor
    col = lerp(_ClipColor, col, facing);
    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    // 相当于前面的discard判断,因为clip函数会对值为负值的片元进行剔除
    // 所以这里对distance取负
    clip(-distance);
    return col;
}

最后结果:

image.png

关注我们吧:

0 Answers