Created
March 10, 2020 04:18
-
-
Save stilllisisi/2f962e35e11a4c980c2229d41a232910 to your computer and use it in GitHub Desktop.
【游戏-渲染】移动端云渲染的实现
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //我们使用的噪声其实是这个噪声: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