相较于上一个水波倒影,这个水波倒影要更为复杂,但也更为真实,大体思路如下
利用脚本抓取水面的镜像,并在Shader中进行处理

先上个Gif解解馋,代码后面整理好再上传,最近实在事情多,早上优化到晚上的工作

原理剖析:
首先镜面反射的原理: 如下图,入射光经过表面光滑的平面产生关于平面法线的反射光,人眼收集到反射光产生成像

使用上面原理,我们只需要收集 从物体发出的反射光即可,看下图
我们从镜面看到的倒影其实是从 图中物体反射光方向 发出的光,因此,我们可以在视角方向关于镜面对称的位置 A 设置一个相机,方向与物体反射光方向反向,然后将 A 位置渲染的图像映在镜面,即产生我们的倒影。

问题: 当出现如下图,C’为反射相机,C为视角方向,P为镜面。
此时C’的相机范围能够照射到AB两个区域的物体,但是实际上,B区域的物体不应该被渲染出来,因为被P镜面挡住了,此时我们应该更改C’反射相机的剪裁面,将近剪裁面改成P所在的平面。(关于远近剪裁面详见《Unity Shader入门精要 4.6.7》,第二张图是关于远近剪裁面的简易图)


以上就是在镜面中的两个核心问题。下面我们来看看如何将上面问题解决并用于代码中。
关于问题1,反射矩阵:
如下图(加粗为矢量),Q为被反射点,Q’为反射点,P为镜面,n为镜面的单位法线,d为平面到原点的距离
k为平面P到Q点的方向向量,M为k所在直线与平面P的交点,且kn平行
n M+d=0 ,(M是平面上任意一点的时候也满足上式)
∴Q’ = Q - 2kn
k= Qn - Mn
∴Q’ = Q - 2n(Qn -(-d))= Q-2n(Qn + d)

Q’x = (1-2nxnx)Qx-2nxnyQy-2nxnzQz-2nxd
Q’y = -2
nxnyQx+(1-2nyny)Qy-2nynzQz-2nyd
Q’z = -2nxnzQx-2nynzQy+(1-2nznz)Qz-2nz*d
上式可以写成如下

反射矩阵就是中间那个矩阵。

一篇同样关于反射矩阵的描写
关于问题2,替换剪裁平面:
剪裁矩阵的相关计算链接

C#的取镜面代码

using UnityEngine;using System.Collections;[ExecuteInEditMode]
public class Mirror : MonoBehaviour
{public bool m_DisablePixelLights = true;public int m_TextureSize = 256;public float m_ClipPlaneOffset = 0.07f;public bool m_IsFlatMirror = true;public Vector3 offset = new Vector3(0,0,0);public LayerMask m_ReflectLayers = -1;private Hashtable m_ReflectionCameras = new Hashtable(); private RenderTexture m_ReflectionTexture = null;private int m_OldReflectionTextureSize = 0;//是否已经在渲染private static bool s_InsideRendering = false;public void OnWillRenderObject(){if( !enabled || !GetComponent<Renderer>() || !GetComponent<Renderer>().sharedMaterial || !GetComponent<Renderer>().enabled )return;Camera cam = Camera.current;//玩家视角相机if( !cam )return;if( s_InsideRendering )return;s_InsideRendering = true;Camera reflectionCamera;//反射相机CreateMirrorObjects( cam, out reflectionCamera );Vector3 pos = transform.position + offset;Vector3 normal;if(m_IsFlatMirror){normal = transform.up;}else{ normal= transform.position - cam.transform.position ;normal.Normalize();}int oldPixelLightCount = QualitySettings.pixelLightCount;//限制像素光的数量if( m_DisablePixelLights )QualitySettings.pixelLightCount = 0;UpdateCameraModes( cam, reflectionCamera );float d = -Vector3.Dot (normal, pos) - m_ClipPlaneOffset;Vector4 reflectionPlane = new Vector4 (normal.x, normal.y, normal.z, d);Matrix4x4 reflection = Matrix4x4.zero;//反射矩阵CalculateReflectionMatrix (ref reflection, reflectionPlane);Vector3 oldpos = cam.transform.position;Vector3 newpos = reflection.MultiplyPoint( oldpos );//反射相机的位置?reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;//反射相机的worldToCameraMatrix矩阵进行反射// 计算剪切面(把原相机的位置及法线经上面计算的反射矩阵变化)Vector4 clipPlane = CameraSpacePlane( reflectionCamera, pos, normal, 1.0f );Matrix4x4 projection = cam.projectionMatrix; //投影矩阵CalculateObliqueMatrix (ref projection, clipPlane);//计算出剪切面相关的投影矩阵,剪切面以下内容不显示reflectionCamera.projectionMatrix = projection;reflectionCamera.cullingMask = ~(1<<4) & m_ReflectLayers.value; //设置可以反射的物体,已经忽略了本身reflectionCamera.targetTexture = m_ReflectionTexture;//注意当相机镜像后,其渲染的模型的顶点绕序也会镜像,需要将背面裁剪设置为正面裁剪,渲染结束后再修改回来GL.invertCulling = true;reflectionCamera.transform.position = newpos;Vector3 euler = cam.transform.eulerAngles;reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);reflectionCamera.Render();reflectionCamera.transform.position = oldpos;GL.invertCulling = false;Material[] materials = GetComponent<Renderer>().sharedMaterials;foreach( Material mat in materials ) {if( mat.HasProperty("_Ref") )mat.SetTexture( "_Ref", m_ReflectionTexture );}if( m_DisablePixelLights )QualitySettings.pixelLightCount = oldPixelLightCount;s_InsideRendering = false;}void OnDisable(){if( m_ReflectionTexture ) {DestroyImmediate( m_ReflectionTexture );m_ReflectionTexture = null;}foreach( DictionaryEntry kvp in m_ReflectionCameras )DestroyImmediate( ((Camera)kvp.Value).gameObject );m_ReflectionCameras.Clear();}/// <summary>/// 设置目标相机的参数,大部分从源相机中复制过去/// </summary>/// <param name="src"></param>/// <param name="dest"></param>private void UpdateCameraModes( Camera src, Camera dest ){if( dest == null )return;dest.clearFlags = src.clearFlags;dest.backgroundColor = src.backgroundColor;       if( src.clearFlags == CameraClearFlags.Skybox ){Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;if( !sky || !sky.material ){mysky.enabled = false;}else{mysky.enabled = true;mysky.material = sky.material;}}dest.farClipPlane = src.farClipPlane;dest.nearClipPlane = src.nearClipPlane;dest.orthographic = src.orthographic;dest.fieldOfView = src.fieldOfView;dest.aspect = src.aspect;dest.orthographicSize = src.orthographicSize;dest.renderingPath = src.renderingPath;}/// <summary>/// 创建反射相机及反射相机对应的照射纹理图/// </summary>/// <param name="currentCamera"></param>/// <param name="reflectionCamera"></param>private void CreateMirrorObjects( Camera currentCamera, out Camera reflectionCamera ){reflectionCamera = null;if( !m_ReflectionTexture || m_OldReflectionTextureSize != m_TextureSize ){if( m_ReflectionTexture )DestroyImmediate( m_ReflectionTexture );m_ReflectionTexture = new RenderTexture( m_TextureSize, m_TextureSize, 16 );m_ReflectionTexture.name = "__MirrorReflection" + GetInstanceID();m_ReflectionTexture.isPowerOfTwo = true;m_ReflectionTexture.hideFlags = HideFlags.DontSave;m_OldReflectionTextureSize = m_TextureSize;}reflectionCamera = m_ReflectionCameras[currentCamera] as Camera;if( !reflectionCamera ) {GameObject go = new GameObject( "Mirror Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox) );reflectionCamera = go.GetComponent<Camera>();reflectionCamera.enabled = false;reflectionCamera.transform.position = transform.position;reflectionCamera.transform.rotation = transform.rotation;go.hideFlags = HideFlags.HideAndDontSave;m_ReflectionCameras[currentCamera] = reflectionCamera;}       }/// <summary>/// 阶跃函数 -1,0,1/// </summary>/// <param name="a"></param>/// <returns></returns>private static float sgn(float a){if (a > 0.0f) return 1.0f;if (a < 0.0f) return -1.0f;return 0.0f;}/// <summary>/// 计算cam相机空间下的平面法线及位置/// </summary>/// <param name="cam">计算的相机</param>/// <param name="pos"></param>/// <param name="normal"></param>/// <param name="sideSign">1表示剪裁平面的法线为单位向量</param>/// <returns>Vector4( cam平面法线, -cpos的法线 )</returns>private Vector4 CameraSpacePlane (Camera cam, Vector3 pos, Vector3 normal, float sideSign){Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;Matrix4x4 m = cam.worldToCameraMatrix;Vector3 cpos = m.MultiplyPoint( offsetPos );Vector3 cnormal = m.MultiplyVector( normal ).normalized * sideSign;return new Vector4( cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos,cnormal) );}/// <summary>/// 将视角相机的远裁面替换成plane/// </summary>/// <param name="projection"></param>/// <param name="clipPlane">vector4(法线,平面位置)</param>private static void CalculateObliqueMatrix (ref Matrix4x4 projection, Vector4 clipPlane){//逆矩阵 * (sgn(clipPlane.x),sgn(clipPlane.y),1,1)Vector4 q = projection.inverse * new Vector4(sgn(clipPlane.x),sgn(clipPlane.y),1.0f,1.0f);//Vector4 c = clipPlane * (2.0F / (Vector4.Dot (clipPlane, q)));//矩阵第三列的值 = clipplane - 矩阵第四列的值projection[2]   = c.x - projection[3];projection[6]   = c.y - projection[7];projection[10]  = c.z - projection[11];projection[14]  = c.w - projection[15];}/// <summary>/// 计算反射矩阵/// </summary>/// <param name="reflectionMat"></param>/// <param name="plane"></param>private static void CalculateReflectionMatrix (ref Matrix4x4 reflectionMat, Vector4 plane){reflectionMat.m00 = (1F - 2F*plane[0]*plane[0]);reflectionMat.m01 = (   - 2F*plane[0]*plane[1]);reflectionMat.m02 = (   - 2F*plane[0]*plane[2]);reflectionMat.m03 = (   - 2F*plane[3]*plane[0]);reflectionMat.m10 = (   - 2F*plane[1]*plane[0]);reflectionMat.m11 = (1F - 2F*plane[1]*plane[1]);reflectionMat.m12 = (   - 2F*plane[1]*plane[2]);reflectionMat.m13 = (   - 2F*plane[3]*plane[1]);reflectionMat.m20 = (   - 2F*plane[2]*plane[0]);reflectionMat.m21 = (   - 2F*plane[2]*plane[1]);reflectionMat.m22 = (1F - 2F*plane[2]*plane[2]);reflectionMat.m23 = (   - 2F*plane[3]*plane[2]);reflectionMat.m30 = 0F;reflectionMat.m31 = 0F;reflectionMat.m32 = 0F;reflectionMat.m33 = 1F;}
}

Shader中的渲染代码

Shader "Mirrors/Bumped Specular"
{Properties {_Color ("Main Color", Color) = (1,1,1,1)_MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}[NoScaleOffset] _NoiseTex ("NoiseTex", 2D) = "white" {}             // 噪点图_BlendLevel("Main Material Blend Level",Range(0,1))=1_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)_Shininess ("Shininess", Range (0.03, 128)) = 0.078125_BumpMap ("Normalmap", 2D) = "bump" {}_Bumpness ("Bump Rate",Range(0,10))= 0.5_Ref ("Auto Generate!", 2D) = "white" {}_RefColor("Reflection Color",Color) = (1,1,1,1)_RefRate ("Reflective Rate", Range (0, 1)) = 1_Distortion ("Reflective Distortion", Range (0, 1)) = 0              // 镜面扭曲程度_NoiseScaleX ("NoiseScaleX", Range(0, 1)) = 0.1                     // 水平噪点放大系数_NoiseScaleY ("NoiseScaleY", Range(0, 1)) = 0.1                     // 垂直放大系数_NoiseSpeedX ("NoiseSpeedX", Range(0, 10)) = 1                      // 水平扰动速度_NoiseSpeedY ("NoiseSpeedY", Range(0, 10)) = 1                      // 垂直扰动速度_NoiseBrightOffset ("NoiseBrightOffset", Range(0, 0.9)) = 0.25      // 噪点图整体的数值偏移// _NoiseFalloff ("NoiseFalloff", Range(0, 1)) = 1                     // 扰动衰减// _MirrorRange ("MirrorRange", Range(0, 1)) = 1}SubShader { Pass{Tags {"LightMode" = "ForwardBase" "RenderType"="Opaque" }LOD 400CGPROGRAM// #pragma surface surf BlinnPhong#pragma multi_compile_fwdbase//顶点函数定义#pragma vertex vert  //片元函数定义#pragma fragment frag// #pragma target 3.0// #pragma debug//引入必要的Unity库 如下面的UnityObjectToClipPos 就是库中函数#include "UnityCG.cginc"//引入光照库 _LightColor0需要用#include "Lighting.cginc"sampler2D _MainTex;sampler2D _BumpMap;sampler2D _NoiseTex;sampler2D _Ref;float4 _MainTex_ST;//图片的(平铺和偏移系数)如果要使图片的Tilling和Offset生效就必须定义float4 _BumpMap_ST;float4 _NoiseTex_ST;fixed4 _Color;half _Shininess;half _RefRate;half _Bumpness;half _BlendLevel;half _Distortion;fixed4 _RefColor;fixed _NoiseScaleX, _NoiseScaleY;fixed _NoiseSpeedX, _NoiseSpeedY;fixed _NoiseBrightOffset;// fixed _NoiseFalloff;// float _MirrorRange;struct appdata {float4 vertex : POSITION;//每个顶点结构体必须有的float3 normal : NORMAL;//定义法线float4 tangent :TANGENT;//定义切线float4 maintex : TEXCOORD0;//存储主贴图纹理的坐标信息float4 noisetex : TEXCOORD1;//存储噪声纹理的坐标信息float4 bumptex : TEXCOORD2;//存储法线纹理的坐标信息float2 reftex : TEXCOORD3;//存储相机拍摄纹理的坐标信息};struct v2f{float4 pos : SV_POSITION;//每个片元结构体必须有的float3 lightDir : TEXCOORD0;float3 viewDir : TEXCOORD1;float4 uv1 : TEXCOORD2;//用于存储主贴图、噪声纹理信息float4 uv2 : TEXCOORD3;//用于存储法线、相机拍摄纹理信息float4 projPos :TEXCOORD4;};v2f vert (appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);//把顶点从模型空间转换到剪裁空间o.uv1.xy = TRANSFORM_TEX(v.maintex, _MainTex);o.uv1.zw = TRANSFORM_TEX(v.noisetex, _NoiseTex);o.uv2.xy = TRANSFORM_TEX(v.bumptex, _BumpMap);o.projPos = ComputeGrabScreenPos(o.pos);// o.uv2.zw = v.reftex;//这里是UnityCG.cginc 库里面定义函数,利用normal和tangent生成对应的rotation矩阵(模型空间转到切线空间的变换矩阵)//这也是在上面必须定义normal和tangent的原因TANGENT_SPACE_ROTATION;o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex));//将模型顶点的光照方向转到切线空间o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex));//将模型顶点的视角方向转到切线空间return o;} fixed4 frag (v2f i) : SV_Target//返回一个RGBA到模型上{fixed3 lightDir = normalize(i.lightDir);fixed3 viewDir = normalize(i.viewDir);//这里十分奇怪,要在片元着色器里面计算下面的才不会发生偏移,如果在顶点着色器中计算的话下面的会发生随视角靠近扭曲的现象i.uv2.zw = i.projPos.xy/i.projPos.w;fixed2 ouvxy = fixed2( // 噪点图采样,用于主纹理的UV偏移的tex2D(_NoiseTex, i.uv1.zw + fixed2(_Time.x * _NoiseSpeedX, 0)).r,tex2D(_NoiseTex, i.uv1.zw + fixed2(0, _Time.x * _NoiseSpeedY)).r);ouvxy -= _NoiseBrightOffset; // 0~1 to ==> -_NoiseBrightOffset~ 1 - _NoiseBrightOffsetouvxy *= fixed2(_NoiseScaleX, _NoiseScaleY);    // 扰动放大系数// float scale = i.projPos.x / _MirrorRange;          // 用距离来作为扰动衰减// scale = lerp(scale, 1, (1 - _NoiseFalloff));    // 距离越近扰动越是衰减(即:与镜面距离越近,基本是不扰动的,所以我们可以看到边缘与镜面的像素是吻合的)// ouvxy *= scale;fixed4 packedNormal = tex2D(_BumpMap,i.uv2.xy + ouvxy);//采样_BumpMap里面的法线信息//UnpackNormal英文含义就是 解压法线 将法线从颜色信息里面解压出来//这里涉及到一个知识点 为什么法线贴图是蓝色调的//法线贴图里面法线都是存储在切线空间里面的//详细请看文末提到的博客fixed3 tangentNormal = UnpackNormal(packedNormal);//切线空间的法线是单位长度为1的,所以只要求出其中两个就可以利用长度求出另一个值tangentNormal.xy *= _Bumpness;tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy,tangentNormal.xy)));fixed3 albedo = tex2D(_MainTex,i.uv1.xy).rgb * _Color * _BlendLevel;//采样主贴图的纹理颜色//半罗伯特反射fixed3 diffuse = _LightColor0.rgb * albedo * (1+dot(lightDir,tangentNormal))/2;//Blinn-Phong模型高光 fixed3 halfView = normalize(lightDir + viewDir);fixed3 specular = _LightColor0.rgb * _SpecColor * pow(saturate(dot(tangentNormal,halfView)),_Shininess);fixed3 reflect = tex2D(_Ref,i.uv2.zw + tangentNormal.xy*_Distortion + ouvxy).rgb * _RefRate ;return fixed4(diffuse + specular + reflect,1);}ENDCG}}FallBack "Specular"
}

下图为QualitySettings.pixelLightCount参数设置的含义

完整代码百度盘,提取码:oyy9

UnityShader_倒影,水波倒影(代码已更新)(2)相关推荐

  1. 微信快速开发框架(九)-- V3.0发布,代码已更新至Github 新增微店功能

    版本内容 1.修正了缺少对Event.View的支持 2.增加了用户UnionID 3.新增微信小店功能 4.多客服功能 5.单元测试 什么是UnionID 我们知道,每个用户针对一个微信公众账号都有 ...

  2. 微信快速开发框架V3.0--发布,代码已更新至Github 新增微店功能

    版本内容 1.修正了缺少对Event.View的支持 2.增加了用户UnionID 3.新增微信小店功能 4.多客服功能 5.单元测试 什么是UnionID 我们知道,每个用户针对一个微信公众账号都有 ...

  3. 微信快速开发框架(八)-- V2.3--增加语音识别及网页获取用户信息,代码已更新至Github...

    不知不觉,版本以每周更新一次的脚步进行着,接下来应该是重构我的代码及框架的结构,有朋友反应代码有点乱,确实如此,当时写的时候只是按照订阅号来写的,后来才慢慢增加到支持API接口.目前还在开发第三方微信 ...

  4. (代码已更新)QT 环境下 用opencv 进行骨架细化(骨架提取)得到图像中心线

    之前的任务是把如下的一个直钢管图像进行处理,提取出中心线,用到了骨架细化算法以及一些常用的opencv处理.思路就是: 预处理通过灰度得到二值图像--二值图形态学处理--骨架细化提取中心线--霍夫概率 ...

  5. ios查看线程数量_关于iOS多线程,你看我就够了(已更新)

    作者:@翁呀伟呀 授权本站转载. 在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项.当然也会给出几种多线程的案例,在实际使用中感受它们的区别.还有一点需要说明的是 ...

  6. 声称代码已开源却迟迟没更新,网友等了好几个月,最终一怒之下把作者挂网上...

    丰色 发自 凹非寺 量子位 报道 | 公众号 QbitAI "代码拖更"的经历,不知你遇到过没? 就是你看上了一篇论文或者项目,作者声称代码会开源或者已开源,但你左等右等,每天查查 ...

  7. 声称代码已开源却迟迟没更新,网友等了好几个月,最终一怒之下把作者挂网上

    丰色发自凹非寺 "代码拖更"的经历,不知你遇到过没? 就是你看上了一篇论文或者项目,作者声称代码会开源或者已开源,但你左等右等,每天查查 GitHub,代码就是一直没发布-- re ...

  8. (已更新)Ubuntu 14.04 Linux 3D桌面完全教程,显卡驱动安装方法,compiz特效介绍,常见问题解答

      内容   文章标题 : (已更新)Ubuntu 14.04 Linux 3D桌面完全教程,显卡驱动安装方法,compiz特效介绍,常见问题解答 发表于 : 2008-08-03 0:57    [ ...

  9. Android开发人员不得不收集的代码(持续更新中)(http://www.jianshu.com/p/72494773aace,原链接)

    Android开发人员不得不收集的代码(持续更新中) Blankj 关注 2016.07.31 04:22* 字数 370 阅读 102644评论 479喜欢 3033赞赏 14 utilcode D ...

最新文章

  1. 软件测试系统测试的定义,如何定义软件测试人员的测试范围
  2. Unity Log重新定向
  3. socket设置超时时间 SO_RCVTIMEO和SO_SNDTIMEO
  4. wpf 放大缩小界面_调整电脑屏幕文本文字显示大小,系统设置放大缩小DPI图文教程...
  5. Mavin build中隐藏的SAP UI5 JavaScript merge任务
  6. Nginx凭啥子并发数可以达到3w!
  7. 牛客网华为机试考试java_牛客网——华为机试(题17:坐标移动)(Java)
  8. 支付宝五福开奖!几个亿的项目你分到几块?
  9. .net Core 在 CentOS7下,报The type initializer for 'Gdip' threw an exception.异常
  10. 信息发展树标杆 智慧城市筑屏障
  11. 自己怎么制作地图,如何绘制电子版地图?
  12. dingo php,Laravel Dingo API
  13. 生死大PK:软路由是否会威胁到硬路由
  14. iOS 初中级工程师简历指南
  15. java fx 重绘_JavaFX 2.2:如何强制重绘/更新ListView
  16. ES8都有哪些新特性,你还在用ES6吗?
  17. 华尔街智商测试 交易员江平的答卷
  18. linux中解压rar文件
  19. MIKE 21 教程 2.2 Domain, Time, Module Selection设置教学
  20. 这不是TNT:py挂机脚本

热门文章

  1. Unity3D恶魔猎手移动方法
  2. 数据结构学习心得——顺序表
  3. 【聆听】汪国真诗集(一)
  4. Oracle中如何解决动态SQL语句过长的问题
  5. GitHub:社交,贡献,学习
  6. android7.12新功能,带来七大新功能 一加7T系列出厂即搭载Android 10
  7. linux oracle用户解锁
  8. 互联网时代,加强数字技能人才培养成刚需
  9. Android前端开发入门
  10. Openstack云平台脚本部署之Ceph存储集群配置(十四)