Skip to content

Instantly share code, notes, and snippets.

@stilllisisi
Created March 10, 2020 04:18
Show Gist options
  • Select an option

  • Save stilllisisi/2f962e35e11a4c980c2229d41a232910 to your computer and use it in GitHub Desktop.

Select an option

Save stilllisisi/2f962e35e11a4c980c2229d41a232910 to your computer and use it in GitHub Desktop.
【游戏-渲染】移动端云渲染的实现
//我们使用的噪声其实是这个噪声:https://www.shadertoy.com/view/4sfGzS
//这是Iq大神弄的一个噪声,效率和表现都算很好。
float noise(in float3 x)
float3 p = floor(x);
float3 f = frac(x);
f = f * f*(3.0 - 2.0*f);
float2 uv2 = (p.xy + float2(37.0, 17.0)*p.z) + f.xy;
float2 rg = tex2Dlod(_NoiseTex, float4((uv2 + 0.5) / 256.0, 0, 0)).yx;
return lerp(rg.x, rg.y, f.z);
float4 map(in float3 p, in float t)
float3 pos = p;
//d就是当前坐标距离顶部的差值
pos.y += _cloudRange.z;
pos /= _cloudRange.z;
float d = -max(0.0, pos.y - _cloudRange.y / _cloudRange.z);
float3 q = pos - _Wind.xyz * _Time.y;
float f;
f = 0.5000*noise(q);
q = q * 2.02;
f += 0.2500*noise(q);
q = q * 2.03;
f += 0.1250*noise(q);
q = q * 2.01;
f += 0.0625*noise(q);
//算出的噪声就是我们想要的噪声,然后让d去和噪声相加,模拟当前云的颜色值。
d += _NoiseMultiplier * f;
d = saturate(d);
float4 res = (float4)d;
res.xyz = lerp(_Bright, _Dark, res.x*res.x);
return res;
//还要分析下基本的算法,看下地平线早期的云实现,主要思想还是根据raymarching得到云的外形,然后加上光照。因为手机上要考虑性能,所以会对完整的云进行大幅度阉割。
//计算出水平的fov的tan
//算出垂直fov的一半的弧度
float halfFov_vert_rad = Camera.main.fieldOfView * Mathf.Deg2Rad / 2.0f;
//根据tan可以算出当距离为1的时候摄像机的宽度,进行atan就可以得到水平的弧度,依然是一半
float halfFov_horiz_rad = Mathf.Atan(Mathf.Tan(halfFov_vert_rad) * Camera.main.aspect);
//同样,在shader里也是进行如此的计算
void computeCamera(in float2 screenPos, out float3 ro, out float3 rd)
//这是水平tan
float tanFovH = _TanFov;
//这是垂直tan
float tanFovV = _TanFov * _ScreenParams.y / _ScreenParams.x;
float3 forward = UNITY_MATRIX_V[2].xyz;
float3 right = UNITY_MATRIX_V[0].xyz;
float3 up = UNITY_MATRIX_V[1].xyz;
//出发点就是摄像机的位置
ro = _WorldSpaceCameraPos;
//方向根据屏幕的y坐标,往垂直方向偏移,根据x坐标,往水平方向偏移,看如果y坐标或者x坐标满值,刚好就是摄像机的视角边缘线
rd = normalize(forward + screenPos.y * tanFovV * up + screenPos.x * tanFovH * right);
float4 RayMarch(in float3 ro, in float3 rd, in float zbuf)
float4 sum = (float4)0;
float dt = 0.1;
float t = dt;
//这个是根据方向算出完全朝上的部分
float upStep = dot(rd, float3(0, 1, 0));
bool rayUp = upStep > 0;
float angleMultiplier = 1;
for (int i = 0; i < StepCount; i++)
//摄像机的深度图-0.1
float distToSurf = zbuf - t;
//从ro出发的y增加上rd的y,比例是t
float rayPosY = ro.y + t * rd.y;
/* Calculate the cutoff planes for the top and bottom.
Involves some hardcoding for our particular case. */
float topCutoff = (_CloudVerticalRange.y + _CloudGranularity * max(1., _ParallaxQuotient) + .06*t + max(0, ro.y)) - rayPosY;
float botCutoff = rayPosY - (_CloudVerticalRange.x - _CloudGranularity * max(1., _ParallaxQuotient) - t / .06 + min(0, ro.y));
if (distToSurf <= 0.001 || (rayUp && topCutoff < 0) || (!rayUp && botCutoff < 0)) break;
// Fade out the clouds near the max z distance
float wt;
if (zbuf < _ProjectionParams.z - 10)
wt = (distToSurf >= dt) ? 1. : distToSurf / dt;
else
wt = distToSurf / zbuf;
RaymarchStep(ro + t * rd, dt, wt, sum, t);
t += max(dt, _CloudStepMultiplier*t*0.0011);
return saturate(sum);
//其实我们可以直接从摄像机的顶部交点或者底部交点开始运算。如果位于摄像机里面,那么就是从摄像机位置开始运算。
float t = 0;
if (rd.y < 0)
t = (_cloudTop - ro.y) / rd.y;
t = max(0, t);
else
t = (_cloudBottom - ro.y) / rd.y;
t = max(0, t);
//由于正常情况下云层一般较高,移动摄像机位置引起的变化量容易过大,导致一旦开始移动,采样贴图的坐标也迅速移动,导致云层瞬间变化。
//然而摄像机那点移动对云来说不算什么,于是增加一个缩放系数,因为云层基本在几千米左右,就把这个系数定位1000,再根据和云层的距离做一个基本的非线性关系。
float times = 1000;
if (ro.y > _cloudTop)
times *= pow(max(0, (2 * _cloudTop - ro.y) / _cloudTop), 1);
else
times *= pow(max(0, _cloudBottom - ro.y) / _cloudBottom, 0.1);
times = max(times, 1);
pos = ro / times + rd * (t - abs(_cloudOffset / rd.y)) + offset;
//接下来是边缘过度问题,云层的边缘超过顶部和底部会直接不计算,那么会导致一个难看的切边,于是我要根据顶部和底部增加一个透明过度。
//另外,如果云层很厚,我们将整个噪点分布在全部云层,会导致云层和稀薄,于是需要一个循环处理,我通过一个简单的线性周期函数来实现这个东西。
float offy = p.y - _cloudBottom;
float topOff = _cloudTop - p.y;
topOff = clamp(topOff, 0, _cloudEdge) / _cloudEdge;
offy = clamp(offy, 0, _cloudEdge) / _cloudEdge;
float offy1 = _cloudPadding * abs(frac(offy * _cloudLayers) - 0.5) - 1.5;
float den = clamp((_cloudCut - offy1 + _smooth * f) * topOff * offy, 0, 1);
return den
//我们根据位置进行那么远的采样,必然会导致都是噪点。于是我增加了_cloudOffset这个变量,会根据摄像机的位置和云的位置动态变化,保证云的渲染效果一直以较近距离观察为主。
//最后,是光照。直接使用了iq大佬的基本思想,云的边缘会变量。通过往光线方向采样,得到新的位置的云的密度,如果密度变小,说明越靠近云的边缘,就更亮。
//具体实现的时候依然根据云的上层和下层做了区别,因为如果一个方向变量,那么另一个方向反而会变暗,所以需要反向一下y轴。
void addSum(float den, float3 pos, float t, inout float4 sum, float3 sunColor, float3 ro, float3 rd)
if (den > 0.01)
float a = 0.6;
if (ro.y > _cloudTop)
rd = -rd;
a = 0.45;
float dif = clamp((den - map(pos , ro, rd, t, 0.3 * _WorldSpaceLightPos0.xyz)) / a, 0.0, 1.0);
float3 lin = float3(0.65, 0.7, 0.75) * 1.4 + _LightColor0 * dif;
float4 col = float4(lerp(float3(1, 0.95, 0.8), float3(0.25, 0.3, 0.35), den), den);
col.xyz *= lin;
col.a *= 0.4;
col.rgb *= col.a;
sum = sum + col * (1 - sum.a);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment