模型削减的运用还是挺多的,它可以使目标物体实现逐步消失,如倒入或倒出杯中的动态水等
介绍
这篇实验我们以兔子模型作为实验对象,新建GameObject并命名为clip,并让clip与兔子模型产生关联,通过调整该clip的position与rotation使该模型产生切割效果,并在切割面的位置实现变换切割颜色(实际上改变了模型背面的颜色)来实现。
原理
如何使clip的position跟rotation与模型产生关联是实现这个效果的重点,也就是说当前clip对象在调整位置跟旋转时如何对模型进行有效告知?
旋转对模型的影响:我们采用法线与模型顶点的世界坐标的点积进行有效性计算,根据dot(N, W) = |N||W|cosθ(N指的法线向量,W指模型顶点的世界坐标)由于N是单位向量,所以dot(N, W) = |W|cosθ,如图:
由图中可知,|W|cosθ的值是W向量在N向量方向上的投影长度,根据该结论我们可以新建一个Quad物体,因为Quad物体的顶点数只有四个(模型顶点数如果过多不好进行有效分析)假设ABCD是该物体的四个顶点,O是世界空间的原点,可以简单做图如下:
假设ON向量=(0,1,0)根据结论可知,|ON1|是OB向量与OA向量在ON向量方向上的“有符号”投影长度,|ON2|是OD向量与OC向量在ON向量反方向上的“有符号”投影长度,而"有符号"的投影长度刚好等于各顶点世界坐标到原点的垂直“有符号”距离。这时我们会很自然的想通过当投影值大于0时进行片元丢弃的方式进行剔除。
if(p>0) {
// 丢弃片元的方式
discard;
}
而这种方式会发现当把clip对象放置于点N1位置时,Quad对象的上半部分也会被完全丢弃,并且任意调整clip对象也于事无补,这当然不是我们想要的。
平移对模型的影响:基于刚刚得到的结论,"有符号"的投影长度刚好等于各顶点世界坐标到原点的垂直“有符号”距离,我们很容易能想到,如果要让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属性,同时把兔子模型的材质也指定为该材质。运行后的效果:
当我们移动或旋转clip时,会对模型产生削减的效果,但是发现在切割处出现了镂空的错觉,这是因为我们在着色器处理上并没有打开双面渲染,此时需要在Pass内部添加Cull off表明当前pass需要进行双面渲染。开启后的效果如下:
开启双面渲染后,发现切割处的颜色是黑色的,那么如何更改背面颜色?此时就需要用到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;
}
最后结果:
关注我们吧: