最近公司裁了一个技术美术,然而现在用人之际,才发现缺一个技术美术,便把我推到火坑上,美术不够前端来凑?
目前HDRP的shader资料非常少,国外有一些shader graph教程,无奈我不太喜欢这种通过编写节点和拉线来完成一个shader,哪怕只是简单几行的HLSL代码,也要拉N个节点来拼凑。好吧,手动撸一个水滴及地面的水波纹shader效果,需要结合特效:
雨
完整shader如下:
/*
*Author : SunnyDecember
*/
Shader "Weather/RainDrop"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_Brightness("Brightness", Range(0, 2)) = 1
_MainColor("MainColor", COLOR) = (1, 1, 1, 1)
_NormalTex("NormalTex", 2D) = "bump" {}
_CutoutTex("CutoutTex", 2D) = "white" {}
_Distortion("Distortion", Range(0.5, 5)) = 1
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
#include "Packages/com.unity.shadergraph/ShaderGraphLibrary/Functions.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
CBUFFER_START(RainDorp)
real _Brightness;
real4 _MainColor;
real4 _NormalTex_ST;
real4 _CutoutTex_ST;
real4 _MainTex_ST;
real _Distortion;
real4 _ColorPyramidTexture_TexelSize;
CBUFFER_END
TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);
TEXTURE2D(_NormalTex); SAMPLER(sampler_NormalTex);
TEXTURE2D(_CutoutTex); SAMPLER(sampler_CutoutTex);
SAMPLER(sampler_ColorPyramidTexture);
ENDHLSL
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent"}
Lighting Off
//HDRP不支持GrabPass ! ! ! 抠脚。
//GrabPass{ }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
struct VertexInput
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float4 color : COLOR;
};
struct VertexOutput
{
float4 clipPosition : SV_POSITION;
half2 uv : TEXCOORD0;
real4 grabUV : TEXCOORD1;
real2 normalUV : TEXCOORD2;
real2 cutoutUV : TEXCOORD3;
real4 color : TEXCOORD4;
};
VertexOutput vert(VertexInput v)
{
VertexOutput o;
float3 positionWS = TransformObjectToWorld(v.vertex);
o.clipPosition = TransformWorldToHClip(positionWS);
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.color = v.color;
//屏幕UV
#if UNITY_UV_STARTS_AT_TOP
float scale = -1;
#else
float scale = 1;
#endif
o.grabUV.xy = (float2(o.clipPosition.x, o.clipPosition.y * scale) + o.clipPosition.w) * 0.5;
o.grabUV.zw = o.clipPosition.zw;
//获取法线图UV
o.normalUV = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw;
o.cutoutUV = v.texcoord.xy * _CutoutTex_ST.xy + _CutoutTex_ST.zw;
return o;
}
float4 frag (VertexOutput IN) : SV_Target
{
real4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
real2 bump = UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, IN.normalUV)).rg;
real4 cutoutColor = SAMPLE_TEXTURE2D(_CutoutTex, sampler_CutoutTex, IN.cutoutUV);
//水折射
real2 offset = bump * _Distortion * IN.grabUV.z;
IN.grabUV.xy += offset;
real4 grabColor = SAMPLE_TEXTURE2D_X_LOD(_ColorPyramidTexture, sampler_ColorPyramidTexture, IN.grabUV.xy / IN.grabUV.w, 0);
albedo *= _Brightness * _MainColor * IN.color.r;
return float4(grabColor.rgb + albedo.rgb, cutoutColor.a * _MainColor.a);
}
ENDHLSL
}
}
FallBack Off
}
雨滴使用的图:
地面波纹使用的图:
1.声明Unity的属性
水滴和地面的水波纹都是使用同一个shader,只是使用的纹理不一样,主要实现了水的折射。定义shader的属性,和普通Unity版本一致的写法:
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_Brightness("Brightness", Range(0, 2)) = 1
_MainColor("MainColor", COLOR) = (1, 1, 1, 1)
_NormalTex("NormalTex", 2D) = "bump" {}
_CutoutTex("CutoutTex", 2D) = "white" {}
_Distortion("Distortion", Range(0.5, 5)) = 1
}
_Brightness 为控制水珠的亮度。_MainColor为水珠颜色。_NormalTex为法线图,非常重要,水的折射就靠她来模拟了。_CutoutTex用于控制水珠的透明度,这个shader其实是作用在面片上,我们希望面片周围的多余像素不显示,请观察她的纹理_____。_Distortion控制水扭曲的程度。
2.引入HLSL文件及声明变量
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
#include "Packages/com.unity.shadergraph/ShaderGraphLibrary/Functions.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
CBUFFER_START(RainDorp)
real _Brightness;
real4 _MainColor;
real4 _NormalTex_ST;
real4 _CutoutTex_ST;
real4 _MainTex_ST;
real _Distortion;
real4 _ColorPyramidTexture_TexelSize;
CBUFFER_END
TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);
TEXTURE2D(_NormalTex); SAMPLER(sampler_NormalTex);
TEXTURE2D(_CutoutTex); SAMPLER(sampler_CutoutTex);
SAMPLER(sampler_ColorPyramidTexture);
ENDHLSL
使用HLSLINCLUDE / ENDHLSL ,包裹住HLSL文件。把变量定义在CBUFFER_START / CBUFFER_END中,只是为了放在缓冲区,这里没什么值得深究的。在纹理名字后加上“_ST”,获取纹理的Tiling和offset值,在inspector面板有显示此值。这里的real类型,要么是half要么是float,在Common.hlsl文件中有声明。在这里贴出简化后的代码:
#if REAL_IS_HALF
#define real half
#define real2 half2
#define real3 half3
#define real4 half4
...
#else
#define real float
#define real2 float2
#define real3 float3
#define real4 float4
...
#endif
TEXTURE2D(名字) / SAMPLER(名字)声明纹理和她的采样器,对应于普通unity版本的sampler2D。注意,在SAMPLER中的声明,需要在纹理名字前面加上 “sampler”。_ColorPyramidTexture是unity声明的变量,所以在此我们只需要她的采样器就好了,她的作用是获取屏幕像素,普通版本的GrabPass在HDRP中无法使用了,用_ColorPyramidTexture代替之。
3.命令及结构体
Tags { "Queue"="Transparent" "RenderType"="Transparent"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
struct VertexInput
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float4 color : COLOR;
};
struct VertexOutput
{
float4 clipPosition : SV_POSITION;
half2 uv : TEXCOORD0;
real4 grabUV : TEXCOORD1;
real2 normalUV : TEXCOORD2;
real2 cutoutUV : TEXCOORD3;
real4 color : TEXCOORD4;
};
我们使用的几种渲染队列值分别为:
- Background
值为1000。使用此队列的物体,会率先渲染,通常用于渲染背景及天空盒。- Geometry
值为2000。此队列用得最多,用于渲染不透明物体。- AlphaTest
值为2450。- Transparent
值为3000。用于半透明物体的渲染。此时Background和Geometry已经渲染完毕,物体可以混合他们的颜色实现半透明效果。- Overlay
值为4000。最后的叠加效果。
在渲染物体时候,Unity会先对他们进行排序,规则是按照物体的网格中心点离摄像机的距离远近排列。Background,Geometry和AlphaTest均是从近到远排序,也就是先渲染距离近摄像机的物体。为什么需要排序呢?主要是为了避免同一个像素点的重复绘制。Transparent和Overlay是从远到近的规则排列后渲染,因为远的物体必须要先渲染,近的物体才能和他们做颜色混合,呈现半透明颜色。所以呢,这两个队列会有重复绘制,性能稍低一点。
我们希望不透明物体的颜色已经写入颜色缓冲后,才开始渲染水滴,此时水滴颜色可以混合颜色缓冲来达到半透明效果,可以解释的了能透过水波纹看到地面砖,因此用透明队列"Queue"="Transparent"。RenderType是给shader replacement使用的,标记的RenderType可用于写入摄像机深度纹理或者灯光的深度图。
每个片元会经过深度测试,深度测试通过了,把深度记录到深度缓冲中,并写入颜色到颜色缓冲。但是,通常半透明物体不会写入深度值的,ZWrite Off。原因1,半透明物体是从远到近渲染,远的物体不会遮挡近的物体。原因2,如果开启深度写入,会使得看不到半透明物体的背面,因为深度值被离摄像机更近的面覆盖了。
4.顶点函数
VertexOutput vert(VertexInput v)
{
VertexOutput o;
float3 positionWS = TransformObjectToWorld(v.vertex);
o.clipPosition = TransformWorldToHClip(positionWS);
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.color = v.color;
//屏幕UV
#if UNITY_UV_STARTS_AT_TOP
float scale = -1;
#else
float scale = 1;
#endif
o.grabUV.xy = (float2(o.clipPosition.x, o.clipPosition.y * scale) + o.clipPosition.w) * 0.5;
o.grabUV.zw = o.clipPosition.zw;
//获取法线图UV
o.normalUV = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw;
o.cutoutUV = v.texcoord.xy * _CutoutTex_ST.xy + _CutoutTex_ST.zw;
return o;
}
UNITY_UV_STARTS_AT_TOP可判断uv坐标开始位置在纹理的左上还是左下。openGL在左下,DX的在左上并且沿y方向往下逐渐减小。经过TransformWorldToHClip转换后的clipPosition坐标,此时在裁剪坐标系下,他的xyz范围都在(-w,w)之间。加上clipPosition.w在乘上0.5,此时的x和y范围被转换到(0,w)之间了,而zw不需要转换。这么做的原因,是为片元函数做准备,在片元函数需要把他的xy除以w来转换到(0,1)范围,也就是所谓的NDC空间吧。为什么不直接在顶点函数直接除以w?而且在片元函数才做这步呢?因为顶点函数之后会做插值,我们希望这个插值是线性的,否则就不准确了(这和gamma,linear是一样的道理)。待插值完毕后,我们在片元函数才除以w。
5.片元函数
float4 frag (VertexOutput IN) : SV_Target
{
real4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
real2 bump = UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, IN.normalUV)).rg;
real4 cutoutColor = SAMPLE_TEXTURE2D(_CutoutTex, sampler_CutoutTex, IN.cutoutUV);
//水折射
real2 offset = bump * _Distortion * IN.grabUV.z;
IN.grabUV.xy += offset;
real4 grabColor = SAMPLE_TEXTURE2D_X_LOD(_ColorPyramidTexture, sampler_ColorPyramidTexture, IN.grabUV.xy / IN.grabUV.w, 0);
albedo *= _Brightness * _MainColor * IN.color.r;
return float4(grabColor.rgb + albedo.rgb, cutoutColor.a * _MainColor.a);
}
分别对_MainTex,_NormalTex,_CutoutTex采样得到相应的颜色值。我们对法线的xy偏移值并叠加到grabUV上以模拟折射,让grabUV除以他的w值,此时grabUV的xy范围是(0,1)。我们算grabUV的xy的目的,其实是在计算水的每个片元映射到屏幕的哪个位置,通过对这个屏幕位置的偏移后,再去对屏幕颜色采样,以模仿水折射效果,当雨滴打在地砖时候,会观察到地砖颜色有偏移。而我们希望水的折射和深度有关,远近的折射不一样,在计算offset时候乘上了grabUV.z值。普通版本的Unity的grabpass在HDRP中不能使用了, 在HDRP中可使用_ColorPyramidTexture来获取屏幕颜色,对屏幕颜色采样后得到grabColor,加上反射率albedo可以达到我们想要的效果。为了不显示多余的颜色,我们在A通道乘上cutoutColor.a。_CutoutTex图的目的是剔除多余的颜色而已。
Author : SunnyDecember
Date : 2020.3.14
原文
网友评论