最近看到一个效果还不错的体积云,了解了下其实现原理,将其实现思路做一个简单的整理和记录,并重写该Shader
其效果图:
该效果来自名为CloudPuff的Unity资源包中示例场景截图
实现的主要思路为:
使用MatCap对预先存储的光照进行采样
MatCap是使用存储了视空间下不同法线方向的球形光照纹理,在计算光照时直接对纹理进行采样,运算效率高,但也有其局限,只能适用于单一材质的模型,并且无法对光源位置和相机位置的变化做出响应。MatCap Shader的基本思路是,使用某特定材质球的贴图,作为当前材质的视图空间环境贴图(view-space environment map),来实现具有均匀表面着色的反射材质物体的显示。考虑到物体的所有法线的投影的范围在x(-1,1),y(-1,1),构成了一个圆形,所以MatCap 贴图中存储光照信息的区域是一个圆形。
关于MatCap的详细原理,可以参考这里和这里
使用粒子的序列图播放实现云层的变化
勾选粒子的 TextureSheetAnimation 的选项,Shader中传入云层的序列帧纹理并设置相关参数即可实现云层的序列帧动画
Shader部分
Shder主要分为两部分,一个是边缘光Pass,另一个是主体光照颜色计算Pass:
边缘光Pass部分:
边缘光Pass部分比较简单,主要是对 边缘光的纹理进行采样:
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
o.scrPos=ComputeScreenPos(o.pos);
o.color=v.color;
return o;
}
fixed4 frag (v2f i):SV_Target{
fixed4 col=tex2D(_MainTex,i.uv);
col.rgb*=0;
col.a*=i.color.a;
fixed4 edgeCol=tex2D(_EdgeLight,i.scrPos.xy/i.scrPos.w);
col.rgb=(edgeCol)*col.a*_EdgeStrength;
return col;
}
由于边缘光的纹理是通过RenderTexture对当前相机角度下的"场景"进行捕捉,采样时使用当前像素点对应的视口坐标,在计算颜色输出时,使用到了主纹理的Alpha通道值
主体光照计算部分
主体光照计算部分主要是对MatCap的球形纹理进行采样,由于纹理的坐标范围是[0,1],而视空间下的法线范围是在[-1,1],因此需要将视空间下的法线做一个范围映射:
即:
o.cap.xyz=worldNormal*0.5+0.5;
另外,在开启软粒子效果时,为了使粒子与场景中的有深度值的物体之间过渡自然,会有一个深度判断,并将判断结果影响其Alpha值,
雾效的叠加过程中,与边缘光的纹理处理类似,是通过RenderTexture对当前相机角度下的"场景"进行捕捉,采样使用当前像素点对应的视口坐标
着色器代码,附相关注释:
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
//法线变换,转置逆矩阵
fixed3 worldNormal=normalize(unity_WorldToObject[0].xyz*v.normal.x+unity_WorldToObject[1].xyz*v.normal.y+unity_WorldToObject[2].xyz*v.normal.z);
//转换法线到视空间
worldNormal=mul((fixed3x3)UNITY_MATRIX_V,worldNormal);
o.cap.xyz=worldNormal*0.5+0.5;
//计算顶点在屏幕空间的位置,未归一化
o.scrPos=ComputeScreenPos(o.pos);
//如果使用软粒子效果,计算视空间下的深度值,后续与场景深度值作比较
#ifdef SOFTPARTICLES_ON
COMPUTE_EYEDEPTH(o.scrPos.z);
#endif
UNITY_TRANSFER_FOG(o,o.pos);
o.color=v.color;
return o;
}
fixed4 frag(v2f i):SV_Target{
//如果使用软粒子效果,通过深度值比较,距离云层近的物体,云层透明度高
#ifdef SOFTPARTICLES_ON
//对相机深度纹理采样(输入的是未归一化的srcPos,方法内部做srcPos.xy/srcPos.w透视除法,得到视口坐标)
//通过LinearEyeDepth方法转换到视空间下的深度
fixed sceneZ=LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,UNITY_PROJ_COORD(i.scrPos)));
//这里的i.scrPos.z经过 COMPUTE_EYEDEPTH(o.scrPos.z) 已经存储的是视空间里的深度
fixed partZ=i.scrPos.z;
fixed fade=saturate(_ParticleFade*(sceneZ-partZ));
i.color.a*=fade;
#endif
//光照纹理采样
fixed4 mc=tex2D(_MatCapLight,i.cap);
mc.a=1;
//主纹理采样
fixed4 col=tex2D(_MainTex,i.uv);
col.rgb*=i.color*mc*3;
col.a*=i.color.a;
//雾效叠加
#if ADVFOG_ON
fixed4 advFog=tex2D(_AdvFog,i.scrPos.xy/i.scrPos.w);
col.rgb=col.rgb+(advFog*_FogStrength);
#endif
#if ADVFOG_ON
advFog.rgb*=0.75;
UNITY_APPLY_FOG_COLOR(i.fogCoord,col,advFog);
#endif
#if ADVFOG_OFF
UNITY_APPLY_FOG_COLOR(i.fogCoord,col,UNITY_LIGHTMODEL_AMBIENT);
#endif
return col;
}
场景设置部分
场景设置中,需要提前准备与SkyBox对应的光照纹理,边缘光纹理,环境纹理,即:
环境光纹理题
并将该纹理赋予三个双面球体,为什么要这么做呢?
前面提到过,MatCap的局限在于,使用MatCap的光照效果,由于纹理贴图是静态的,因此在场景中无法对光源和相机的位置变化做出光影反应,同理使用RenderTexture作为边缘光和雾效纹理也会有同样的问题
那如果这个纹理是动态的,会随着相机角度变化而变化呢?
因此,在上述 双面球体的中心分别使用三个相机,并与场景主相机保持同步旋转,这样采到的RendTexture就能随着主相机的角度变化而变化,由于光照纹理是使用MatCap,因此需要将相机改为正交模式,并将近裁剪面设置为 1,远裁剪面设置为-1,这样相机就能够得到一个球外视角,中间球形的动态纹理,
对于边缘光纹理和雾效纹理,相机保持透视模式,设置较小的远近裁剪面范围,只捕捉到双面球体内部的贴图就可以了
Shader的_MainTex为一个云朵的序列帧图:
新建一个粒子系统,并将该Shader生成的材质赋予该粒子系统,勾选 TextureSheetAnimation 选项,设置相关参数
单个粒子系统生成效果:
多个粒子系统生成效果: