MY BLOG DIRECTORY:
todo...
INTRODUCTION:
在做很多效果的时候,我们会有对模型进行形状改变的需求,特别是做各种溶解,穿透等特效的时候,本篇就这个需求,详细探索下对模型形状改变的方法及做法。
MAIN CONTENT:
【穿透】
很多穿透特效中,需要有击穿模型的需求,如果对穿透要求不那么高的话直接给个OpacityMask把那部分模型clip掉就可以了。做法也非常简单,直接从脚本层穿个点到shader里,然后根据距离把范围内的像素全部clip掉即可
但是如果对质量要求更高点,比如就是想在模型表面挖个洞。经过上面的步骤我们已经得到一个洞,但是这洞只有表皮部分,洞的内壁根本就没有渲染出来。那么如何才能渲染出内壁呢,目前大致有两种做法,一种就是去改模型的VB和IB真实改模型的渲染数据,另一种就是在Pixelshader里做文章,本文选择后者。
既然是想在PS里做文章,那么肯定需要先有Pixle才行,续著上面的步骤,洞后面的像素都已经没有了,所以想要渲染厚度,就需要把洞壁部分的像素先找出来。洞口的像素已经被我们clip掉了,所以我们需要从其他地方找像素出来,所以我重新渲染了一个pass,用cull off模式把那部分像素拿到。
但是可以知道的是,这部分像素的数据除了位置数据是我们想要的意外其它数据全部对我们渲染洞壁一点帮助都没有。首先就是法线,我们需要重新构建洞壁部分像素的法线数据,这部分数据怎么构建呢,因为我们知道这是个洞,又知道洞壁像素的位置,所以洞中线位置和洞壁像素的位置的差的normalize结果就是我们要的法线。
half3 N = normalize(HolePos - worldpos);
最后可以得到如下效果:
但是一个还不够,我想做很多个洞,这时我们可以从脚本层向GPU传一个数组,这个数组大小是固定的,如果我们有一个洞那么就初始化数组的第一个元素,其余数组元素数值为0,这样其他洞就不会渲染,如果有两个洞就初始化两个元素,其余元素数值为0,这样其他洞就不会渲染,以此类推。
完整代码如下:
shader
Shader "Effects/HoleMaster" { Properties { [HideInInspector] _HolePos("HolePos", Vector) = (0, 0, 0, 0) [HideInInspector] _HoleRad("HoleRad", Float) = 0.2 } SubShader { Tags { "RenderType"="Opaque" } LOD 100
Pass { Cull off CGPROGRAM #pragma fragment MainPS #pragma vertex MainVS // make fog work #pragma multi_compile_fog
#include "UnityCG.cginc" #include "Lighting.cginc"
#define LEN(X) (length(_HolePosArray[X].xyz - worldpos)-_HoleRadArray[X]) #define COP(X, Y) (LEN(X)<LEN(Y)?X:Y)
half4 _HolePos; half _HoleRad;
float4 _HolePosArray[64]; float _HoleRadArray[64];
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 normal : NORMAL; float4 tangent : TANGENT; };
struct FVertToPixle { float2 uv : TEXCOORD0; float3 normal : NORMAL; float3 worldpos : TEXCOORD1; float3 viewpos : TEXCOORD2; float3 tangent : TEXCOORD3; float3 binormal : TEXCOORD4; UNITY_FOG_COORDS(5) float3 objectpos : TEXCOORD6; float4 vertex : SV_POSITION; };
FVertToPixle MainVS(appdata VertexData) { FVertToPixle Output;
Output.vertex = UnityObjectToClipPos(VertexData.vertex); Output.uv = VertexData.uv;
Output.normal = UnityObjectToWorldNormal(VertexData.normal).xyz; Output.tangent = UnityObjectToWorldDir(VertexData.tangent).xyz; Output.binormal = cross(VertexData.tangent, VertexData.normal).xyz;
Output.worldpos = mul(UNITY_MATRIX_M, VertexData.vertex).xyz; Output.viewpos = UnityObjectToViewPos(VertexData.vertex).xyz; Output.objectpos = mul(UNITY_MATRIX_M, float4(0, 0, 0, 1)).xyz; UNITY_TRANSFER_FOG(Output, Output.vertex);
return Output; }
fixed4 MainPS(FVertToPixle i) : SV_Target { // sample the texture half4 output = half4(1,1,1,1); float3 worldpos = i.worldpos; float3 objHpos = half3(i.objectpos.x, 0, i.objectpos.z); float3 worldHpos = half3(worldpos.x, 0, worldpos.z);
float3 HolePos = _HolePosArray[COP(COP(COP(COP(COP(0, 1), 2), 3), 4), 5)];
half3 N = normalize(HolePos - worldpos); float3 V = normalize(UnityWorldSpaceViewDir(i.worldpos)); half3 L = -normalize(_WorldSpaceLightPos0.xyz); half3 H = normalize(L + V); half3 R = -reflect(V, N); half3 NR = -reflect(V, N); half RoL = saturate(dot(R, L)); half NoL = saturate(dot(N, L)); half NoV = saturate(dot(N, V)); half NoH = saturate(dot(N, H)); half VoH = saturate(dot(V, H)); half VoL = saturate(dot(V, L));
output.rgb = NoL; output.a = 0; // apply fog UNITY_APPLY_FOG(i.fogCoord, output);
float len = min(min(min(min(min(LEN(0), LEN(1)), LEN(2)), LEN(3)), LEN(4)), LEN(5)); clip(len); return output; } ENDCG } Pass {
CGPROGRAM #pragma fragment MainPS #pragma vertex MainVS // make fog work #pragma multi_compile_fog
#define LEN(X) length(_HolePosArray[X].xyz - worldpos)-_HoleRadArray[X]
sampler2D _MainTex; float4 _MainTex_ST;
FVertToPixle MainVS (appdata VertexData) { FVertToPixle Output;
fixed4 MainPS (FVertToPixle i) : SV_Target { // sample the texture half4 output = half4(1,1,1,1); half3 N = i.normal; float3 worldpos = i.worldpos; float3 V = normalize(UnityWorldSpaceViewDir(i.worldpos)); half3 L = -normalize(_WorldSpaceLightPos0.xyz); half3 H = normalize(L + V); half3 R = -reflect(V, N); half3 NR = -reflect(V, N); half RoL = saturate(dot(R, L)); half NoL = saturate(dot(N, L)); half NoV = saturate(dot(N, V)); half NoH = saturate(dot(N, H)); half VoH = saturate(dot(V, H)); half VoL = saturate(dot(V, L));
float len = min(min(min(min(min(LEN(0), LEN(1)), LEN(2)), LEN(3)), LEN(4)), LEN(5)); clip(len); return output; } ENDCG } } }
脚本:
using System.Collections; using System.Collections.Generic; using UnityEngine;
[ExecuteInEditMode] public class C_HoleMasterPointPosControler : MonoBehaviour { [System.Serializable] public class LocationHelperData { public Transform LocationHelper; [Range(0, 1)] public float LocationHelperRadius; } public LocationHelperData[] LocationHelpers;
public float Randius = 0.25f; public Material HoleMasterMaterial; // Use this for initialization void Start () {
}
// Update is called once per frame void Update () { if (HoleMasterMaterial != null && LocationHelpers.Length > 0) {
int ArrayNum = LocationHelpers.Length > 64 ? 64 : LocationHelpers.Length; Vector4[] LocationPosArray = new Vector4[ArrayNum]; float[] LocationRadArray = new float[ArrayNum];
for (int i = 0; i < ArrayNum; i++) { if(LocationHelpers[i].LocationHelper != null) { LocationPosArray[i] = LocationHelpers[i].LocationHelper.position; LocationRadArray[i] = LocationHelpers[i].LocationHelperRadius; } else { LocationPosArray[i] = new Vector4(0, 0, 0, 0); LocationRadArray[i] = 0; } } HoleMasterMaterial.SetVectorArray("_HolePosArray", LocationPosArray); HoleMasterMaterial.SetFloatArray("_HoleRadArray", LocationRadArray); } } }
除了球形以外,我们还能有其它形状,这个形状的构造方法可以使用SDF公式。
YivanLee:Begin ray marching in unreal engine 4【第三卷:更多图形更复杂的光照】
【压平】
除了挖洞,还有压平,倒角等操作。其实原理和上面的挖洞一样,只是构建SDF的方法不同。还是两个pass。首先也是渲染出被挖掉的上界面,直接强行把上截面的法线掰成half3(0,1,0)然后正常渲染下半部分的模型即可。
修改第一个pass
修改第二个pass
这个方法可以用来做瓶子中的液体效果。
clip(i.objectpos.y - worldpos.y + _FillPersent);
但是我们上面的效果只是重建了法线,下面我们需要重建world position
我们在截面处假设一个平面,然后沿著V的方向反向追踪求交点,即可得到对应的像素新的位置。
关于射线和平面的求交方法可以去看Reference【1】
得到像素新的位置坐标后,直接拿像素的位置坐标作为UV来采图。
float t = 0; float3 ro = worldpos; float3 rd = V; float3 p0 = float3(0, i.objectpos.y + _FillPercent, 0); float N = float3(0, 1, 0); t = max(dot((p0 - ro), -N) / dot(rd, -N), 0); worldpos.xyz = ro + rd * t - i.objectpos.xyz; float2 UV = worldpos.xz;
至此我们就完成了对截面的法线,位置,UV的重建。
然后就能在切面做一些效果了
其实这算是二分之一体渲染吧。从脚本传个速度进去就可以做液面摇晃了。
float3 PixleDir = worldpos.xyz; float heightoffset = dot(PixleDir, clamp(_MeshWaterVelocity.xyz, MinVec, MaxVec)); clip(i.objectpos.y - i.worldpos.y + (HeightColor + heightoffset) * _FillPercent);
SUMMARY AND OUTLOOK:
以上就是在像素著色器中对模型进行修改的操作,但是这种做法缺陷显著。在截面处的很多数据全是没法用的,要对这部分数据进行大量重计算,而且水面的深度信息无法复原。
NEXT:
Reference
【1】射线与平面相交