体积光的光源可以是平行光、探照灯、点光源等,我们今天先来看看平行光如何制作体积光。
体积光的原理网上已经有很多了,这里就不赘述了。着重快速实现:
Shader "Unlit/VolumetricLightingShader" { Properties { _MainTex ("Texture", 2D) = "white" {} _Intensity("Intensity",float) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #define MAIN_LIGHT_CALCULATE_SHADOWS #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl" #define STEP_TIME 64 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 worldPos:TEXCOORD1; float4 screenPos :TEXCOORD2; }; TEXTURE2D_X_FLOAT(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); TEXTURE2D(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float _Intensity; v2f vert (appdata v) { v2f o; o.vertex = TransformObjectToHClip(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.screenPos = ComputeScreenPos(o.vertex); return o; } half4 frag (v2f i) : SV_Target { half2 screenPos = i.screenPos.xy / i.screenPos.w; //rebuild world position according to depth float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture,sampler_CameraDepthTexture, screenPos).r; depth = Linear01Depth(depth, _ZBufferParams); float2 positionNDC = screenPos * 2 - 1; float3 farPosNDC = float3(positionNDC.xy,1)*_ProjectionParams.z; float4 viewPos = mul(unity_CameraInvProjection,farPosNDC.xyzz); viewPos.xyz *= depth; float4 worldPos = mul(UNITY_MATRIX_I_V,viewPos); float noise = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, screenPos*3).r; float3 startPos = i.worldPos; float3 dir = normalize(worldPos - startPos); startPos += dir * noise; worldPos.xyz += dir * noise; float len = length(worldPos - startPos); float3 stepLen = dir * len / STEP_TIME; half3 color = 0; half3 sceneColor = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, screenPos).rgb; UNITY_LOOP for (int i = 0; i < STEP_TIME; i++) { startPos += stepLen; float4 shadowPos = TransformWorldToShadowCoord(startPos); float intensity = MainLightRealtimeShadow(shadowPos)*_Intensity; color += intensity*_MainLightColor.rgb; } color /= STEP_TIME; color += sceneColor; return half4(color.xyz,1); } ENDHLSL } } }
代码如上,可以看到体积光的基础实现非常简单,在片元着色器步进采样shadowmap,看当前步进的位置是否处于阴影区域,如果不处于阴影区域,就叠加强度,最后形成散射效果。
此shader直接挂在一个quad片上,能把相机遮住就OK,这种方式适合快速实现各种屏幕空间的效果,等效果觉得OK了,然后再慢慢的转移到URP的RenderFeature中。
用quad片直接放在相机前面制作上有以下好处:
1.不需要写任何c#代码,一个shader文件全搞定
2.不需要计算屏幕空间近裁切面的世界坐标的位置,直接取片的position就可以,非常的方便
3.快,主要就是快,这里指的快是写代码敲得快,哈哈哈~
Shader有两个参数,第一个参数是传入一张纹理,这里需要传入噪声纹理,最好是blue noise,这个纹理是用来dither步进距离特别大的时候产生的分层效果,使得分层感不那么明显。通过调整“#define STEP_TIME 64”中的数值来控制步进次数。这里可以看一下没有dither(上图)和有dither(下图)的效果对比:
可以看到分层感的改善还是非常明显的。需要注意的是这里的实现只是简单的步进叠加强度,没有进行任何的散射算法实现,常用的散射算法有米氏散射和瑞利散射,各位童鞋可自行查看,套用公式即可,这里给出这个框架,公式随便套~
如果代码看不懂,这里给出几个关键词可供百度,百度完自然就能看懂了:
1.Raymarching
2.基于深度还原世界坐标
3.ShadowMap的实现原理
如果看完还有问题,欢迎博客讨论区留言~ 或者加qq群:104794354