之前在知乎上看到有大佬模拟了云海效果,正好之前项目里要用,就仔细研究一下,发现确实挺有意思的。
主要原理就是视差映射ParallaxMapping,先主要介绍一下视差映射的原理。
说起视差映射,首先就要说起大家都不陌生的法线贴图技术。
法线贴图把法线储存在贴图的RGB通道中,在片元着色器里采样后,再计算光照,就可以在物体表面模拟凹凸的细节,让原本平滑、没什么细节的表面,可以模拟丰富细节的表面上的光照效果和反射效果等。
但是,在视线离物体很近的时候,法线贴图模拟出的凹凸效果往往就会不那么真实了。
如果配合上一张高度图,再加上视差映射技术,就可以让细节的真实感更进一步
这种方法是基于SPM的基础上的另一个优化版本
如图所示,POM只是对最后两次的采样结果进行简单的插值计算,没有像RPM一样进行二分搜索
nextHeight = H(T3)- currentLayerHeight
prevHeight = H(T2)-(currentLayerHeight - layerHeight)
weight = nextHeight/(nextHeight - prevHeight)
Tp = T(T2)weight + T(T3)(1.0 - weight)
POM会比RPM更容易漏掉一些小细节,在短距离内发生高度的大幅度变化的情况,使用POM也会得到错误的结果
三种优化方案中,这种方法效果适中,性能也较为优良
所以最后选中POM进行云海效果模拟的方法
POM代码
float3 ParallaxMapping(in float3 viewDir, in float2 texcoord, in float height)
{
viewDir.z = abs(viewDir.z) + 0.42;
const float numLayers = 10;
float layerHeight = height/numLayers;
float3 offsetStep = layerHeight * viewDir/viewDir.z;
offsetStep.z /= height;
//xy记录当前uv,z记录当前LayerHeight
float3 curTexcoord = float3(texcoord, 0);
float3 prevTexcoord = curTexcoord;
float curTexHeight = tex2D(_CloudTex, curTexcoord).a;
float prevTexHeight = curTexHeight;
//当前层高度高于高度图高度时停止循环
while(curTexHeight > curTexcoord.z)
{
prevTexcoord = curTexcoord;
curTexcoord += offsetStep;
prevTexHeight = curTexHeight;
curTexHeight = tex2Dlod(_CloudTex, float4(curTexcoord.xy,0,0)).a;
}
//当高度为0的时候,直接不会进入循环,导致分母为0
//所以要加一个极小值让分母不为0
float w = (curTexHeight - curTexcoord.z)/(abs((curTexHeight - curTexcoord.z) - (prevTexHeight - prevTexcoord.z))+1e-7f);
curTexcoord = curTexcoord * (1-w) + prevTexcoord * w;
curTexcoord.z = curTexHeight * (1-w) + prevTexHeight * w;
//输出一个float3类型的变量,xy为偏差后的uv,z为高度
return curTexcoord;
}
自阴影,即为模型自身的一部分阻挡住了光线射向另一部分,导致另一部分产生阴影的现象
而一个平面显然是不会有自阴影,但现在使用视差映射在平面表面模拟了凹凸,那么自阴影现象也是需要存在的
首先使用刚刚视差映射得到的最终uv和最终高度h,依次向光源方向步进
如果层高度小于采样点高度,就说明该点在表面之下,光线被阻挡,如果是计算硬阴影,直接设置为阴影;如果是计算软阴影,增加阴影系数,继续步进
如果层高度大于采样点高度,就说明该点在表面之上,光线没有被阻挡
软阴影需要计算从起始点到最终不阻挡光线的那个点,而阴影系数根据当前层深度和当前高度图深度之间的差异计算,计算软阴影系数的公式如下
代码如下
float ParallaxSoftShadow(in float3 lightDir, in float2 texcoord, in float height)
{
float shadowMultiplier = 1;
const float minLayers = 25;
const float maxLayers = 50;
lightDir.z = abs(lightDir.z) + 0.42;
if(dot(float3(0,0,1), lightDir) > 0)
{
float numSamplesUnderSurface = 0;
shadowMultiplier = 0;
//光线靠近垂直方向时,减少分层,光线偏离垂直方向时(更为倾斜),增加分层,在保证效果的前提下节约性能
float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0,0,1), lightDir)));
float layerHeight = height/numLayers;
half2 offsetStep = lightDir.xy/lightDir.z/numLayers;
float curLayerHeight = height - layerHeight;
float2 curTexcoord = texcoord + offsetStep;
float heightFromTexture = tex2D(_CloudTex, curTexcoord).a;
int stepIndex = 1;
while(curLayerHeight > 0)
{
if(heightFromTexture < curLayerHeight)
{
numSamplesUnderSurface +=1;
shadowMultiplier = max(shadowMultiplier, (curLayerHeight - heightFromTexture)*(1.0 - stepIndex/numLayers));
}
stepIndex += 1;
curLayerHeight -= layerHeight;
curTexcoord += offsetStep;
heightFromTexture = tex2Dlod(_CloudTex, float4(curTexcoord, 0,0)).a;
}
shadowMultiplier = numSamplesUnderSurface < 1 ? 1 : 1-shadowMultiplier;
}
return shadowMultiplier;
}
模拟云海效果,只用高度图即可,把高度图写入主图的a通道,使用视差映射的算法,得到偏差后的坐标,采样MainTex,最后再乘上阴影系数即可。
完整代码如下
Shader "Custom/Scene/Cloud"
{
Properties
{
_CloudTex ("Cloud Texture", 2D) = "white"{
}
_CloudColor ("Cloud Color", Color) = (1, 1, 1, 1)
_CloudSpeed ("Cloud Speed", Vector) = (2, 1, 0, 0)
_Height ("Height", Range(0, 1)) = 0.5
}
SubShader
{
Tags {
"Queue"="Transparent" "RenderType"="Opaque" }
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _CloudTex;
float4 _CloudTex_ST;
fixed4 _CloudColor;
float4 _CloudSpeed;
float _Height;
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 tanViewDir : TEXCOORD1;
float3 tanLightDir : TEXCOORD2;
};
v2f vert (appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _CloudTex) + frac(_Time.x * _CloudSpeed.xy);
TANGENT_SPACE_ROTATION;
o.tanViewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
o.tanLightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
return o;
}
float3 ParallaxMapping(in float3 viewDir, in float2 texcoord, in float height)
{
viewDir.z = abs(viewDir.z) + 0.42;
const float numLayers = 10;
float layerHeight = height/numLayers;
float3 offsetStep = layerHeight * viewDir/viewDir.z;
offsetStep.z /= height;
//xy记录当前uv,z记录当前LayerHeight
float3 curTexcoord = float3(texcoord, 0);
float3 prevTexcoord = curTexcoord;
float curTexHeight = tex2D(_CloudTex, curTexcoord).a;
float prevTexHeight = curTexHeight;
//当前层高度高于高度图高度时停止循环
while(curTexHeight > curTexcoord.z)
{
prevTexcoord = curTexcoord;
curTexcoord += offsetStep;
prevTexHeight = curTexHeight;
curTexHeight = tex2Dlod(_CloudTex, float4(curTexcoord.xy,0,0)).a;
}
float w = (curTexHeight - curTexcoord.z)/(abs((curTexHeight - curTexcoord.z) - (prevTexHeight - prevTexcoord.z))+1e-7f);
curTexcoord = curTexcoord * (1-w) + prevTexcoord * w;
curTexcoord.z = curTexHeight * (1-w) + prevTexHeight * w;
return curTexcoord;
}
float ParallaxSoftShadow(in float3 lightDir, in float2 texcoord, in float height)
{
float shadowMultiplier = 1;
const float minLayers = 25;
const float maxLayers = 50;
lightDir.z = abs(lightDir.z) + 0.42;
if(dot(float3(0,0,1), lightDir) > 0)
{
float numSamplesUnderSurface = 0;
shadowMultiplier = 0;
//光线靠近垂直方向时,减少分层,光线偏离垂直方向时(更为倾斜),增加分层,在保证效果的前提下节约性能
float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0,0,1), lightDir)));
float layerHeight = height/numLayers;
half2 offsetStep = lightDir.xy/lightDir.z/numLayers;
float curLayerHeight = height - layerHeight;
float2 curTexcoord = texcoord + offsetStep;
float heightFromTexture = tex2D(_CloudTex, curTexcoord).a;
int stepIndex = 1;
while(curLayerHeight > 0)
{
if(heightFromTexture < curLayerHeight)
{
numSamplesUnderSurface +=1;
shadowMultiplier = max(shadowMultiplier, (curLayerHeight - heightFromTexture)*(1.0 - stepIndex/numLayers));
}
stepIndex += 1;
curLayerHeight -= layerHeight;
curTexcoord += offsetStep;
heightFromTexture = tex2Dlod(_CloudTex, float4(curTexcoord, 0,0)).a;
}
shadowMultiplier = numSamplesUnderSurface < 1 ? 1 : 1-shadowMultiplier;
}
return shadowMultiplier;
}
fixed4 frag (v2f i) : SV_Target
{
float3 uv = ParallaxMapping(normalize(i.tanViewDir), i.uv, _Height);
float shadowMultiplier = ParallaxSoftShadow(normalize(i.tanLightDir), uv.xy, uv.z);
half4 c = tex2D(_CloudTex, uv.xy) * _CloudColor;
c.rgb *= _LightColor0.rgb * (shadowMultiplier);
return c;
}
ENDCG
}
}
}