简约水面
场景准备:
水底和水面的示例物体
天空球
和天空球一样的Cubemap
组成部分
深度颜色
水下扭曲
泡沫
高光
反射
焦散
代码部分
git hub地址:
有注释,就不写了详细过程了
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace URP
{
public class WaterColor : MonoBehaviour
{
public Gradient Ramp;
public Texture2D RampTexture;
void OnValidate()
{
RampTexture = new Texture2D(256, 1);
RampTexture.wrapMode = TextureWrapMode.Clamp;
RampTexture.filterMode = FilterMode.Bilinear;
int n = RampTexture.width;
Color[] colors = new Color[n];
for (int i = 0; i < n; i++) colors[i] = Ramp.Evaluate((float)i / n);
RampTexture.SetPixels(colors);
RampTexture.Apply();
Material mtl = GetComponent<MeshRenderer>().sharedMaterial;
mtl.SetTexture("_RampTex", RampTexture);
}
}
}
Shader
Shader "MyURP/Water"
{
Properties
{
[Header(Main)]
_Depth("Depth", Range(0,0.1)) = 0
_Speed("Speed", Range(0, 1)) = 1
_Lightness("Lightness", float) = 1
[Header(Foam)]
_FoamTex("Foam Tex", 2D) = "white" {}
_FoamRange("Foam Range", Range(0, 15)) = 1
_FoamSize("Foam Size", Range(0, 3)) = 1
_FoamColor("Foam Color", color) = (1,1,1,1)
[Header(Distort)]
_Distort("Distort", Range(0,0.1)) = 0
_NormalTex("Normal Tex", 2D) = "white" {}
[Header(Specular)]
_SpecularColor("Specular Color", color) = (1,1,1,1)
_Specular("Specular", float) = 1
_Smoothness("Smoothness", float) = 1
[Header(Reflection)]
_ReflectionCube("Reflection Cube", cube) = "white" {}
_ReflectionPow("Reflection Pow", float) = 1
[Header(Caustic)]
_CausticTex("CausticTex", 2D) = "white" {}
_CausticInstensity("Caustic Instensity", float) = 1
}
SubShader
{
Tags
{
"Queue"="Transparent"
"RenderType" = "Transparent"
"IgnoreProjector" = "True"
"RenderPipeline" = "UniversalPipeline"
}
LOD 100
Pass
{
Name "Water"
//Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
// Required to compile gles 2.0 with standard srp library
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float4 uv : TEXCOORD0;
float4 normalUV : TEXCOORD1;
float fogCoord : TEXCOORD2;
float3 positionWS : TEXCOORD3;
float3 positionVS : TEXCOORD4;
};
CBUFFER_START(UnityPerMaterial)
half _Speed;
half _Depth;
half _Lightness;
half4 _FoamColor;
half _FoamRange;
half _FoamSize;
float4 _FoamTex_ST;
half _Distort;
float4 _NormalTex_ST;
half4 _SpecularColor;
half _Specular;
half _Smoothness;
half _ReflectionPow;
float4 _CausticTex_ST;
half _CausticInstensity;
CBUFFER_END
TEXTURE2D (_FoamTex);SAMPLER(sampler_FoamTex);
TEXTURE2D (_NormalTex);SAMPLER(sampler_NormalTex);
TEXTURECUBE (_ReflectionCube);SAMPLER(sampler_ReflectionCube);
TEXTURE2D (_CausticTex);SAMPLER(sampler_CausticTex);
TEXTURE2D (_RampTex);SAMPLER(sampler_RampTex);
TEXTURE2D (_CameraDepthTexture);SAMPLER(sampler_CameraDepthTexture);
TEXTURE2D (_CameraOpaqueTexture);SAMPLER(sampler_CameraOpaqueTexture);
Varyings vert(Attributes v)
{
Varyings o = (Varyings)0;
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
o.positionVS = TransformWorldToView(o.positionWS);
o.positionCS = TransformWViewToHClip(o.positionVS);
float speed = _Time.y * _Speed;
o.uv.xy = o.positionWS.xz * _FoamTex_ST.xy + speed;
o.uv.zw = v.uv;
o.normalUV.xy = TRANSFORM_TEX(v.uv, _NormalTex) + speed * float2(-1.07, 1.07);
o.normalUV.zw = TRANSFORM_TEX(v.uv, _NormalTex) + speed;
o.fogCoord = ComputeFogFactor(o.positionCS.z);
return o;
}
half4 frag(Varyings i) : SV_Target
{
half4 c = 0;
//水下的扭曲+深度过度颜色
//使用两个方向的法线贴图,做出波纹起伏的效果
//顶点着色器中之所以是1.07倍,是为了防止重叠时突然有一帧很亮
float3 normalUV01 = SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, i.normalUV.xy).xyz;
float3 normalUV02 = SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, i.normalUV.zw).xyz;
float3 normalUV = normalUV01 * normalUV02;
//屏幕坐标 = 该片段屏幕坐标(0~1) / 屏幕像素(1920*1080)
float2 screenUV = i.positionCS.xy / _ScreenParams.xy;
//偏移坐标就是在屏幕坐标之上稍微偏一点
float2 distortUV = screenUV + normalUV01.xy * _Distort;
half depthTex = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).x;
half depth = LinearEyeDepth(depthTex, _ZBufferParams);
half depthWater = depth + i.positionVS.z;
depthTex = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, distortUV).x;
depth = LinearEyeDepth(depthTex, _ZBufferParams);
half depthDistortWater = depth + i.positionVS.z;
//depth:深度图上该点到相机近裁剪面的距离。大于0
//i.positionVS.z:VS空间下的深度。小于0
//depthWater:未扭曲状态下的深度。 |depth|大一点,该位置更深,在水面之下。该值>0
//depthDistortWater:扭曲状态下的深度。 |片段深度|大一点,说明该像素看不到该片段。该值<0
float2 opaqueUV;
//true表示深度图采样到的depth加上该片段的高度小于0,即没有在水面之上
//用depthWater好点
if(depthWater < 0)
{
opaqueUV = screenUV;
}
else
{
opaqueUV = distortUV;
depthWater = depthDistortWater;
}
//根据扭曲后的UV采样抓屏纹理,水上的使用未扭曲的screenUV,水下的使用扭曲后的distortUV
half4 opaqueTex = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, opaqueUV);
//根据深度采样渐变纹理,水上的使用未扭曲的depthWater,水下的使用扭曲后的depthDistortWater
half4 waterColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, half2(depthWater * _Depth, 0));
//----------------泡沫----------------
//根据深度给予贴图一个范围,贴图颜色如果在范围内,就返回白色,最后乘上_FoamColor
half foamTex = SAMPLE_TEXTURE2D(_FoamTex, sampler_FoamTex, i.uv.xy).x;
foamTex = pow(abs(foamTex), _FoamSize);
half foamRange = depthWater * _FoamRange;
half foam = step(foamRange, foamTex);
half4 foamColor = _FoamColor * foam;
//----------------高光----------------
//Specular = SpecularColor * Ks * pow(NdotH, Smoothness)
half3 N = lerp(half3(0,1,0), normalUV, 0.8);
//H = L + V
half3 V = normalize(_WorldSpaceCameraPos.xyz - i.positionWS);
half3 H = normalize(GetMainLight().direction + V);
half NdotH = saturate(dot(N, H));
half4 specular = _SpecularColor * _Specular * pow(NdotH, _Smoothness);
//----------------反射----------------
//就是根据反射,出射角获取颜色
half3 reflectUV = reflect(-V, N);
half4 reflectTex = SAMPLE_TEXTURECUBE(_ReflectionCube, sampler_ReflectionCube, reflectUV);
half fresnel = 1 - saturate(dot(half3(0,1,0), V));
half4 reflect = reflectTex * pow(fresnel, _ReflectionPow);
//----------------焦散----------------
//原理详见深度贴花,
float4 depthVS = 1;
depthVS.xy = i.positionVS.xy * depth / -i.positionVS.z;
depthVS.z = depth;
float3 depthWS = mul(unity_CameraToWorld, depthVS).xyz;
//和水面法线类似,但水面是tex01 * tex02,而这里为了让焦散动起来,使用了min(tex01, tex02)
float2 causticUV01 = depthWS.xz * _CausticTex_ST.xy + depthWS.y * 0.05 + _Time.y * _Speed;
float2 causticUV02 = depthWS.xz * _CausticTex_ST.xy + depthWS.y * 0.08 + _Time.y * _Speed * float2(-1.07, 1.07);
half4 causticTex01 = SAMPLE_TEXTURE2D(_CausticTex, sampler_CausticTex, causticUV01);
half4 causticTex02 = SAMPLE_TEXTURE2D(_CausticTex, sampler_CausticTex, causticUV02);
half4 caustic = min(causticTex01, causticTex02) * _CausticInstensity;
c += waterColor * _Lightness;
c += specular * reflect;
c += foamColor;
c *= opaqueTex + caustic * _Lightness;
//Fog
c.rgb = MixFog(c.rgb, i.fogCoord);
c.a = 0.5;
return c;
}
ENDHLSL
}
}
}
卡通渲染
准备模型和贴图
(这是已经做了两个阶段的截图...)
Shader编写部分
1.描边Pass
做法很简单,就是将顶点往法线方向移动
描边Pass渲染时,会去渲染主Pass渲染过的地方,这样就造成了重复渲染
因此使用模板测试
主Pass部分:默认写入,并保持通过
描边Pass部分:不相等时才通过
问题一:近看很粗,远看很细
解决方法:将粗细乘上使用相机到顶点的距离
问题二:偏移方向不对
问题产生原因:可以从图中顶点的三个法线方向看出来,是有棱有角的,因此在偏移时,就是往比较硬的方向偏移的
解决办法一:将模型的法线属性变成“计算”并把光滑度拉满,这种做法就是在合并法线
但会产生新的问题,就是法线被修改了,我们后续无法再使用正确的法线方向了,因此光照部分会出问题
解决办法二:在模型制作时,将平均过的法线值储存到切线中,但我不会,幸好有代码侧的
解决办法三:使用C#代码,将平均过的法线值储存到切线中,直接选中模型跑Editor就行了
模型平均法线写入切线数据
public class PlugTangentTools
{
[MenuItem("Tools/模型平均法线写入切线数据")]
public static void WirteAverageNormalToTangentToos()
{
MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>();
foreach (var meshFilter in meshFilters)
{
Mesh mesh = meshFilter.sharedMesh;
WirteAverageNormalToTangent(mesh);
}
SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var skinMeshRender in skinMeshRenders)
{
Mesh mesh = skinMeshRender.sharedMesh;
WirteAverageNormalToTangent(mesh);
}
}
private static void WirteAverageNormalToTangent(Mesh mesh)
{
var averageNormalHash = new Dictionary<Vector3, Vector3>();
for (var j = 0; j < mesh.vertexCount; j++)
{
if (!averageNormalHash.ContainsKey(mesh.vertices[j]))
{
averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]);
}
else
{
averageNormalHash[mesh.vertices[j]] =
(averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized;
}
}
var averageNormals = new Vector3[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
averageNormals[j] = averageNormalHash[mesh.vertices[j]];
}
var tangents = new Vector4[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
tangents[j] = new Vector4(averageNormals[j].x, averageNormals[j].y, averageNormals[j].z, 0);
}
mesh.tangents = tangents;
}
}
问题四:模板测试使用唯一值的话,会让两个物体重叠部分产生不了描边
解决办法:使用C#脚本,在Start中定义_Ref
优化部分一:想自定义描边粗细和描边颜色(原神就有用到这个)
使用顶点色.rgb存储描边颜色,使用顶点色.a存储描边粗细
Shader代码部分,仅描边Pass
Outline
Pass
{
Name "Outline"
Stencil
{
Ref [_Ref]
Comp NotEqual
}
Tags { "LightMode"="Outline" }
Cull Front
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 tangentOS : TANGENT;
float4 color : COLOR;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float fogCoord : TEXCOORD0;
float4 color : TEXCOORD1;
};
CBUFFER_START(UnityPerMaterial)
half _Outline;
CBUFFER_END
Varyings vert(Attributes v)
{
Varyings o = (Varyings)0;
float3 positionWS = TransformObjectToWorld(v.positionOS);
float distance = length(_WorldSpaceCameraPos - positionWS);
float3 positionOS = v.positionOS.xyz;
//0.使用C#脚本,将平均后的法线值存到切线中
//1.根据_Outline * 0.01可以自定义描边粗细
//2.distance可以让描边在远近距离粗细一样
//3.顶点色的Alpha值可以用来存储想要的粗细
positionOS += normalize(v.tangentOS) * _Outline * 0.01 * distance * v.color.a;
o.color = v.color;
o.positionCS = TransformObjectToHClip(positionOS);
o.fogCoord = ComputeFogFactor(o.positionCS.z);
return o;
}
half4 frag(Varyings i) : SV_Target
{
//顶点色的RGB值可以用来存储描边颜色
return MixFog(i.color, i.fogCoord).x;
}
ENDHLSL
}
优化二:
由于模板测试的存在,模型的渲染顺序不同,可能会导致描边显示的不同:
有的时候是这样:描边Pass渲染时,底下的Body模型还没写入模板值,这样就可以渲染出来了
有的时候是这样:描边Pass渲染时,底下Body模型已经渲染了,且写入了模板值,这样就不会渲染了
解决办法:我们使用URP_Renderer自带的,规定Pass渲染顺序的功能
即,最底下的Add Renderer Feature,写上Pass的名字,并规定该Pass在什么时候渲
这样,我们就可以在所有主Pass渲染完之后,再去渲染描边Pass,Outline就是我们决定了渲染顺序的Pass。
可以看这篇文章,说得差不多【01】从零开始的卡通渲染-描边篇 - 技术专栏 - Unity官方开发者社区
阴影部分
使用Step,阶梯函数式阴影
但这样有个问题,_Step 0 时,就是全0
1时,就是全1
2时,一半是0.5,一半是1
3时,就是0.33-0.66-1
...
无法自定义,比如想要0.5-0.75-1就不行了
因此,使用采样渐变纹理,这样就能使用自定义软边,区域等
顺便把阴影接收也做了
高光部分
使用半角向量,并把值约束一下
菲涅尔外发光
和高光一样,把值约束一下
顶点部分
就是简单的一些赋值
Shader
除Shader之外,美术贴图方面更加重要
赛璐璐风格Shader
Shader "MyURP/Cartoon"
{
Properties
{
_BaseColor("Base Color",color) = (1,1,1,1)
_BaseMap("Base Map", 2D) = "white" {}
[Header(Outline)]
_Outline("Outline", Range(0,1)) = 1
_Ref("Ref", float) = 0
[Header(Color)]
_ShadowRampTex("Shadow Ramp Map", 2D) = "white" {}
[Header(Specular)]
_Specular("Instensity(x)Range(y)Smooth(z)", vector) = (0,0,0,0)
[Header(Fresnel)]
_Fresnel("Instensity(x)Range(y)Smooth(z)", vector) = (1,1,0,0)
_FresnelColor("Fresnel Color", color) = (1,1,1,1)
}
SubShader
{
Tags
{
"Queue"="Geometry"
"RenderType" = "Opaque"
"IgnoreProjector" = "True"
"RenderPipeline" = "UniversalPipeline"
}
LOD 100
Pass
{
Name "Cartoon"
Stencil
{
Ref [_Ref]
Comp Always
Pass Replace
}
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _SHADOWS_SOFT
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float fogCoord : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float3 viewWS : TEXCOORD3;
float3 positionWS : TEXCOORD4;
};
CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
float4 _BaseMap_ST;
float4 _ShadowRampTex_ST;
half4 _Specular;
half4 _Fresnel;
half4 _FresnelColor;
CBUFFER_END
TEXTURE2D (_BaseMap);SAMPLER(sampler_BaseMap);
TEXTURE2D (_ShadowRampTex);SAMPLER(sampler_ShadowRampTex);
Varyings vert(Attributes v)
{
Varyings o = (Varyings)0;
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
o.viewWS = normalize(_WorldSpaceCameraPos - o.positionWS);
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
o.fogCoord = ComputeFogFactor(o.positionCS.z);
return o;
}
half4 frag(Varyings i) : SV_Target
{
half4 c;
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
c = baseMap * _BaseColor;
//使用Lambert求出片段0~1的明暗
//然后再使用Step做出硬边的明暗
Light mainLight = GetMainLight(TransformWorldToShadowCoord(i.positionWS));
half3 L = mainLight.direction;
half3 N = normalize(i.normalWS);
half NdotL = dot(N, L) * 0.5 + 0.5;
half ramp = SAMPLE_TEXTURE2D(_ShadowRampTex, sampler_ShadowRampTex, half2(1-NdotL, 0));
ramp *= mainLight.shadowAttenuation * 0.5 + 0.5;
c = lerp(c * ramp, c, ramp);
//高光
half3 V = i.viewWS;
half3 H = normalize(L + V);
half NdotH = dot(N, H);
half specular = _Specular.x * pow(NdotH, _Specular.y);
specular = smoothstep(0.5, 0.5 + _Specular.z, specular);
c += specular;
//外发光
half NdotV = 1 - saturate(dot(N, V));
half fresnal = _Fresnel.x * pow(NdotV, _Fresnel.y);
fresnal = smoothstep(0.5, 0.5 + _Fresnel.z, fresnal);
c += _FresnelColor * fresnal;
c.rgb = MixFog(c.rgb, i.fogCoord);
return c;
}
ENDHLSL
}
Pass
{
Name "Outline"
Stencil
{
Ref [_Ref]
Comp NotEqual
}
Tags { "LightMode"="Outline" }
Cull Front
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 tangentOS : TANGENT;
float4 color : COLOR;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float fogCoord : TEXCOORD0;
float4 color : TEXCOORD1;
};
CBUFFER_START(UnityPerMaterial)
half _Outline;
CBUFFER_END
Varyings vert(Attributes v)
{
Varyings o = (Varyings)0;
float3 positionWS = TransformObjectToWorld(v.positionOS);
float distance = length(_WorldSpaceCameraPos - positionWS);
float3 positionOS = v.positionOS.xyz;
//0.使用C#脚本,将平均后的法线值存到切线中
//1.根据_Outline * 0.01可以自定义描边粗细
//2.distance可以让描边在远近距离粗细一样
//3.顶点色的Alpha值可以用来存储想要的粗细
positionOS += normalize(v.tangentOS) * _Outline * 0.01 * distance;
o.color = v.color;
o.positionCS = TransformObjectToHClip(positionOS);
o.fogCoord = ComputeFogFactor(o.positionCS.z);
return o;
}
half4 frag(Varyings i) : SV_Target
{
//顶点色的RGB值可以用来存储描边颜色
return MixFog(i.color, i.fogCoord).x;
}
ENDHLSL
}
Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
ZWrite On
ZTest LEqual
ColorMask 0
Cull[_Cull]
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
// -------------------------------------
// Material Keywords
#pragma shader_feature_local_fragment _ALPHATEST_ON
#pragma shader_feature_local_fragment _GLOSSINESS_FROM_BASE_ALPHA
//--------------------------------------
// GPU Instancing
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
// -------------------------------------
// Universal Pipeline keywords
// This is used during shadow map generation to differentiate between directional and punctual light shadows, as they use different formulas to apply Normal Bias
#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
#include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
ENDHLSL
}
}
}