Unity Shader - 水体交互
Posted 长生但酒狂
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader - 水体交互相关的知识,希望对你有一定的参考价值。
水体交互
水体交互效果在游戏中是一个很常见的需求,这里简单实现一个可交互的水体。
本篇文章主要是介绍水体交互的实现思路,水体的渲染这里就不再详细介绍,网上很多关于水体的渲染方法很多,可以自己百度、Google了解一下,这里不会过多提及。
效果图。
先放一张最终的GIF效果图!
实现思路
原理其实非常简单,就是通过粒子系统不断发射带有波纹法线贴图的面片,然后把这些法线渲染一张RenderTexture传输到Water Shader中,然后和Water Normal 叠加即可形成水波效果。
实现步骤可以简单分为:
- 简单的水体渲染
- 渲染水波法线RT
- 叠加法线
一、简单的水体渲染
这里的水体渲染采用简单的法线干扰实现,参考冯乐乐女神的《Unity Shader入门精要》里的水体渲染。
这里深水和潜水区的过渡是直接用场景深度值
和水面深度值
做差值
, 差值越接近0,就越接近浅滩区。
获取场景深度图需要开启 DepthModel: GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;
Shader代码:
//frag:
float2 screenPos = i.screenPos.xy/i.screenPos.w;
// 获取屏幕深度
half existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r;
half existingDepthLinear = LinearEyeDepth(existingDepth01);
half depthDifference = existingDepthLinear - i.screenPos.w;
// 深水和潜水颜色做插值
half waterDepthDifference01 = saturate(depthDifference / _DepthMaxDistance);
float4 waterColor = lerp(_ShallowColor, _DeepColor, waterDepthDifference01);
二、渲染水波法线的RenderTexture
1.首先在场景创建一个Camera,该Camera只渲染特定的Layer,即水波法线,Culling Mask 设置为WaterWave
,Clear Flags设置为Slid Color
,Background设置为黑色
,并且位置旋转都设置为和MainCamer一致。并且创建一个RenderTexture,拖拽到Target Texture上。
2.创建一个Shader,命名为WaterRing,该Shader用于渲染水波法线。
3.继续创建一个粒子系统,材质Shader设置为刚才创建的WaterRing Shader,并且该粒子的Layer需要修改为WaterWave
。
水波法线渲染:
水波法线的Shader可以非常简单,直接渲染一张环状的法线贴图即可。
但是这里为了可以更方便的调整水波法线的一些细节就采用动态计算的方式来渲染。
这里计算法线是通过ddx ddy的方式来计算,为了得到法线首先肯定需要知道高度差,那么我们可以先渲染一个环状的高度图,
这里就可以通过两个smoothstep相减得到一个较为平滑的环状高度。
Shader代码:
fixed doubleSmoothstep(float4 uv)
float dis = distance(uv, 0.5);
float halfWidth = _RingWidth * 0.5;
float range = _RingRange;
float smoothness = _RingSmoothness;
float threshold1 = range - halfWidth;
float threshold2 = range + halfWidth;
float value = smoothstep(threshold1, threshold1 + smoothness, dis);
float value2 = smoothstep(threshold2, threshold2 + smoothness, dis);
return value - value2;
fixed4 frag(v2f i) : SV_Target
fixed normalCenter = doubleSmoothstep(i.uv);
return fixed4(normalCenter,normalCenter,normalCenter,1);
有了高度差就可以计算出法线了:
Shader代码:
float normalCenter = doubleSmoothstep(i.uv);
// 波纹法线
float color0 = doubleSmoothstep(i.uv + half4(-1, 0, 0, 0) * 0.004);
float color1 = doubleSmoothstep(i.uv + half4(1, 0, 0, 0) * 0.004);
float color2 = doubleSmoothstep(i.uv + half4(0, -1, 0, 0) * 0.004);
float color3 = doubleSmoothstep(i.uv + half4(0, 1, 0, 0) * 0.004);
float2 ddxy = float2(color0 - color1, color2 - color3);
float3 normal = float3((ddxy * _BumpPower), 1.0);
normal = normalize(normal);
float4 finalColor = float4((normal * 0.5 + 0.5) * normalCenter * i.color.a, normalCenter * i.color.a);
return finalColor;
这里通过ddx ddy得到了法线后,需要把法线从[-1,1]映射到[0,1]范围(normal * 0.5 + 0.5)。
此时我们就可以通过参数动态调整该环状法线的宽度、强度、范围。
最后把该材质赋值给粒子,并且通过调整粒子参数使粒子随着生命周期逐渐变大、顶点色的A通道也跟随生命周期变化来控制透明度和强度。
此时如果不出意外的话可以看到RT是这样的。
三、叠加法线
最后一步就是把之前渲染得到的RT传递到Water Shader中,通过屏幕坐标采样得到水波法线,然后把值从[0,1]映射到[-1,1]范围(normal * 2-1),然后和水的法线叠加即可。
屏幕坐标可以由ComputeGrabScreenPos
计算得到。
Shader代码:
float4 ringColor = tex2D(_RingTex, screenPos);
float3 ringNormal = UnpackNormal(ringColor).rgb;
ringNormal = mul(float3x3(i.TtoW0.xyz,i.TtoW1.xyz,i.TtoW2.xyz),ringNormal);
ringNormal = normalize(ringNormal) * ringColor.a * _RingPower;
// float3 normal = BlendNormals(ringNormal,waterNormal);
float3 normal = normalize(waterNormal+ringNormal);
效果如下:
最后再结合粒子即可实现一个动态的水波扩散的效果。
完整工程源码: https://github.com/csdjk/LearnUnityShader
Unity Standard shader 里面 全局光照Global Illumination(GI)
Unity Standard shader 里面 全局光照Global Illumination(GI)
Standard 粗略的来看,其实分为两个部分,一个是真正的BRDF,第二部分是UnityGI。
全局光照是在局部光照的基础上,增加考虑物体与物体之间光线交互。所以说如果局部光照系统就是由光源+待渲染物体+视点组成的话,那么全局光照系统就是由光源+各待渲染物体之间的反射光+待渲染物体+视点组成。
另外如果没有全局光照技术,这些自发光的表面并不会真的着凉周围的物体,而是它本身看起来更亮了而已。
##Unity GI
half4 fragForwardBaseInternal (VertexOutputForwardBase i)
FRAGMENT_SETUP(s)
UNITY_SETUP_INSTANCE_ID(i);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
UnityLight mainLight = MainLight ();
UNITY_LIGHT_ATTENUATION(atten, i, s.posWorld);
half occlusion = Occlusion(i.tex.xy);
UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
c.rgb += Emission(i.tex.xy);
UNITY_APPLY_FOG(i.fogCoord, c.rgb);
return OutputForward (c, s.alpha);
在UnityStandardCore.cginc里面,简单的概括 color = FragmentGI + UNITY_BRDF_PBS +Emission;
GI主要在UnityGI,另外还有在UNITY_BRDF_PBS 引用到。
FragmentGI 函数 计算global illumination,返回 UnityGI
先看下 UnityGI,这个是个结构体。
在 UnityLightingCommon.cginc里面有定义
struct UnityGI
UnityLight light;
UnityIndirect indirect;
;
在UnityStandardCore.cginc 里面有四处定义了FragmentGI函数,最后还是下面的代码处理返回UnityGI
inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light, bool reflections)
UnityGIInput d;
d.light = light;
d.worldPos = s.posWorld;
d.worldViewDir = -s.eyeVec;
d.atten = atten;
#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
d.ambient = 0;
d.lightmapUV = i_ambientOrLightmapUV;
#else
d.ambient = i_ambientOrLightmapUV.rgb;
d.lightmapUV = 0;
#endif
d.probeHDR[0] = unity_SpecCube0_HDR;
d.probeHDR[1] = unity_SpecCube1_HDR;
#if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION)
d.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending
#endif
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
d.boxMax[0] = unity_SpecCube0_BoxMax;
d.probePosition[0] = unity_SpecCube0_ProbePosition;
d.boxMax[1] = unity_SpecCube1_BoxMax;
d.boxMin[1] = unity_SpecCube1_BoxMin;
d.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif
if(reflections)
Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.smoothness, -s.eyeVec, s.normalWorld, s.specColor);
// Replace the reflUVW if it has been compute in Vertex shader. Note: the compiler will optimize the calcul in UnityGlossyEnvironmentSetup itself
#if UNITY_STANDARD_SIMPLE
g.reflUVW = s.reflUVW;
#endif
return UnityGlobalIllumination (d, occlusion, s.normalWorld, g);
else
return UnityGlobalIllumination (d, occlusion, s.normalWorld);
FragmentGI函数处理根据UnityGIInput,其中UnityGIInput也是结构体。开始时候对UnityGIInput进行赋值。即是灯光+世界空间顶点坐标+观察方向(视线的反方向)+衰减直接赋值即可。随后是光照贴图,在启用了静态光照贴图或者动态光照贴图的情况下,环境光为0,然后获得光照贴图的UV。否则的话,ambient直接使用VertexGIForward计算的rgb值。
后面是对反射探针的计算。
对是否反射调用合适 UnityGlobalIllumination函数。
struct UnityGIInput
UnityLight light; // pixel light, sent from the engine
float3 worldPos;
half3 worldViewDir;
half atten;
half3 ambient;
// interpolated lightmap UVs are passed as full float precision data to fragment shaders
// so lightmapUV (which is used as a tmp inside of lightmap fragment shaders) should
// also be full float precision to avoid data loss before sampling a texture.
float4 lightmapUV; // .xy = static lightmap UV, .zw = dynamic lightmap UV
#if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION)
float4 boxMin[2];
#endif
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
float4 boxMax[2];
float4 probePosition[2];
#endif
// HDR cubemap properties, use to decompress HDR texture
float4 probeHDR[2];
;
UnityGlobalIllumination 真正计算GlobalIllumination的函数
在UnityGlobalIllumination.cginc文件里面有四个UnityGlobalIllumination函数,主要是下面的代码。
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
UnityGI o_gi = UnityGI_Base(data, occlusion, normalWorld);
o_gi.indirect.specular = UnityGI_IndirectSpecular(data, occlusion, glossIn);
return o_gi;
UnityGI_Base函数
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)
UnityGI o_gi;
ResetUnityGI(o_gi);
// Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
#endif
o_gi.light = data.light;
o_gi.light.color *= data.atten;
#if UNITY_SHOULD_SAMPLE_SH
o_gi.indirect.diffuse = ShadeSHPerPixel (normalWorld, data.ambient, data.worldPos);
#endif
#if defined(LIGHTMAP_ON)
// Baked lightmaps
half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
half3 bakedColor = DecodeLightmap(bakedColorTex);
#ifdef DIRLIGHTMAP_COMBINED
fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
o_gi.indirect.diffuse = DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld);
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap (o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#else // not directional lightmap
o_gi.indirect.diffuse = bakedColor;
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#endif
#endif
#ifdef DYNAMICLIGHTMAP_ON
// Dynamic lightmaps
fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex);
#ifdef DIRLIGHTMAP_COMBINED
half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld);
#else
o_gi.indirect.diffuse += realtimeColor;
#endif
#endif
o_gi.indirect.diffuse *= occlusion;
return o_gi;
ShadowMask阴影的衰减(烘焙阴影和实时阴影混合),SH的计算,烘焙的lightmap,平行光与非平行光lightmap,动态lightmap。最终返回UnityGI结构,该结构包含light,color,indirect.diffuse参数。
其中ShadowMask阴影遮罩是Unity5.6版本的新特性。
UnityGI_IndirectSpecular 函数 间接高光
inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, Unity_GlossyEnvironmentData glossIn)
half3 specular;
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
// we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice for probe0 and probe1), so keep original to pass into BoxProjectedCubemapDirection
half3 originalReflUVW = glossIn.reflUVW;
glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]);
#endif
#ifdef _GLOSSYREFLECTIONS_OFF
specular = unity_IndirectSpecColor.rgb;
#else
half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn);
#ifdef UNITY_SPECCUBE_BLENDING
const float kBlendFactor = 0.99999;
float blendLerp = data.boxMin[0].w;
UNITY_BRANCH
if (blendLerp < kBlendFactor)
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[1], data.boxMin[1], data.boxMax[1]);
#endif
half3 env1 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), data.probeHDR[1], glossIn);
specular = lerp(env1, env0, blendLerp);
else
specular = env0;
#else
specular = env0;
#endif
#endif
return specular * occlusion;
UnityGI_IndirectSpecular(UnityGLobalIllumination.cginc)计算间接高光,用probe相关的属性计算。
通过调用Unity_GlossyEnvironment采样Reflection Cube,计算HDR。
如果启用了Box Projection,则通过BoxProjectedCubemapDirection计算变换后的方向。
UNITY_BRDF_PBS 里面用到的GlobalIllumination
关于BRDF部分,来看下UnityStandardBRDF.cginc 里面的 BRDF1_Unity_PBS函数
half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
+ specularTerm * light.color * FresnelTerm (specColor, lh)
+ surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
light的颜色和 diffuse,以及specular 是GlobalIllumination传进来的。
Image Based Lighting (IBL)
其中在材质上反应出周围的环境也是PBS的重要组成部分。在光照模型中一般把周围的环境当作一个大的光源来对待,不过环境光不同于实时光,而是作为间接光(indirect light)通过IBL( Image Based Lighting)来实现。这里也是优化Standard shader的一个比较重要的原因。
IBL一般通过环境光贴图(environment map)来实现。Unity用reflection probe来保存环境光贴图,通过内置变量unity_SpecCube0,unity_SpecCube1访问。
IBL就是采样两次,用粗糙度做插值。这个地方可以做些优化。
以上是关于Unity Shader - 水体交互的主要内容,如果未能解决你的问题,请参考以下文章
Unity Standard shader 里面 全局光照Global Illumination(GI)