Unity渲染編程(材質篇)【第一卷:Modify Mesh In PixelShader】
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
#include "UnityCG.cginc"
#include "Lighting.cginc"
#define LEN(X) length(_HolePosArray[X].xyz - worldpos)-_HoleRadArray[X]
half4 _HolePos;
half _HoleRad;
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;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _HolePosArray[64];
float _HoleRadArray[64];
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);
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));
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
}
}
}
腳本:
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:
todo...
Reference
【1】射線與平面相交
推薦閱讀: