【Unity Shader学习笔记】实现反射与折射模拟水面、使用grabPass与环境贴图
文章目录
- 写在前面
- 一个水波效果
- 大致组成部分与对应的实现方案
- 交界线与深度贴图
- 折射效果与GrabPass
- 使用Cubemap与法线信息来模拟反射
- 在正确的地点创建对应的cubemap
- 通过贴图获取法线信息
- 关于法线贴图
- 法线空间到世界空间变换
- 计算反射角
- 菲涅尔反射
- 混合所有部分的颜色
写在前面
出于个人写作习惯,还是喜欢在开始之前加上一小段技术无关的叙述,通过这个方式我会更加容易进入到写作状态,当然也可以理解为序章一样的东西,会参杂一些生活上的事,废话会比较多,如果不看的话请直接通过目录跳转。
在完成了这个效果之后,我继续参考了Linden Reid女士的博客希望为我的水面增加上一层折射效果,显然我走的更远了一些,把反射也加上了,期间也复习了使用环境纹理这个知识点,多少算是有一点点在进步吧,当初看书时一知半解的东西随着要解决实际问题,也变得更加清晰了。
今天因为个人身体原因需要会去一趟医院,因此就不对原理进行过深描述。而是转而直接介绍代码的实现,当然了过程中如果有所提及,还是会尽量解释的。
至于涉及的原理,以及过程中必然会涉及到的知识点,其内容肯定是多于我在代码中使用到的,而为了帮助我自己记忆和学习,我也会找时间写下来的,主要参考还是冯乐乐的书,当然官网的文档也会使用。(大概率明天就会写)
接下来就开始吧。
一个水波效果
大致组成部分与对应的实现方案
为了更加明确思路,而不是愣在那里想“我要写个水面”,然后毫无头绪,我们还是把水面的效果拆解开来,并一个一个针对实现吧。
交界线与深度贴图
首先是水面在与其他物体接触的时候会产生的交界线,会与周围有些许不同的效果,这个效果已经在这篇博客里头具体实现过了,当时的效果如下:
但是可以看到这个水面现在空有交界线和边界的起伏,非常简单。
折射效果与GrabPass
折射效果其实简单考虑起来,就是希望让水面下的画面能够进行一部分的扭动。
这里Unity提供了grabPass,通过简单的声明,能够获取当前摄像机渲染的屏幕画面纹理,而通过对这个纹理进行带变形的采样之后再显示出来,就能够实现折射效果。
为了达到这一点,需要对shader代码进行如下设置:
Shader "Custom/Water"
{Properties{//some properties here...}SubShader{Tags{"RenderType" = "Opaque" "Queue"="Transparent"}//将物体设置为不透明,渲染队列设置为透明队列GrabPass{"_GrabPass"}//紧接着就声明GrabPass,可以理解为就是一个Pass,将屏幕纹理输出到了指定名字的纹理中Pass{CGPROGRAM...sampler2D _GrabPass;;//在需要的地方声明变量,这里变量名要和GrabPass里的字符串一致float4 _GrabPass_TexelSize;//通过后缀获取该图像的纹素大小,若屏幕纹理大小为800*600,//那么该变量的xy即为1/800,1/600...ENDCG} }
}
这里解释一下渲染标签。实际上这里渲染的是不透明效果,我们要输出的颜色是grabpass获得的颜色与其他效果的叠加,并不会与背后的其他物体进行混合,因此选择RenderType为Opaque即可,当然也可以选择Transparent然后大概Blend选项,但是没有必要,因为我们已经能通过grabpass某种程度上获得屏幕上的像素了。
而渲染队列则必须设置为Transparent,因为这样保证我们当前绘制的物体是在Geometry中的物体之后绘制的,也就意味着在我们获取grabpass图像时,前提必须是其他不透明物体已经绘制完了,这样获得的grabpass才能够包含正确的我们需要的信息。
这部分所需代码与效果:
Shader "Custom/Water"
{Properties{//这部分会用到的变量,主要是后两个_Color("Color",Color) = (1,1,1,1)_DepthFactor("DepthFactor",range(0,1)) =0.5_Distortion("_Distortion",float) = 1.0_NoiseTex("noiseSample",2D) = "white"{}}SubShader{Tags{"RenderType" = "Opaque" "Queue"="Transparent"}GrabPass{"_GrabPass"}//使用GrabPassPass{CGPROGRAM// required to use ComputeScreenPos()#include "UnityCG.cginc"#pragma vertex vert#pragma fragment fragsampler2D _CameraDepthTexture;fixed4 _Color;float _DepthFactor;fixed _Distortion;sampler2D _NoiseTex;float4 _NoiseTex_ST;sampler2D _GrabPass;//通过声明获取需要的Grabpass图像float4 _GrabPass_TexelSize;struct vertexInput{float4 vertex : POSITION;float4 texcoord:TEXCOORD;float3 normal:NORMAL;float4 tangent:TANGENT;};struct vertexOutput{float4 pos : SV_POSITION;float4 grabScreenPos:texcoord2;float4 UV:TEXCOORD3;float4 originalPos:TEXCOORD4;};vertexOutput vert(vertexInput input){vertexOutput output;output.originalPos= UnityObjectToClipPos(input.vertex);output.UV.xy = TRANSFORM_TEX(input.texcoord.xy, _NoiseTex);//Animating//tex2D(_NoiseTex,input.texcoord)is not available in vertex shader //for there are no UV derivatives in Vertex Shaderfloat noise =tex2Dlod(_NoiseTex, float4(output.UV.xy, 0, 0));input.vertex.y += cos(_Time.y*10 * noise)*0.2*noise;// convert obj-space position to camera clip spaceoutput.pos = UnityObjectToClipPos(input.vertex);//使用了变换前的坐标来获取屏幕坐标,确保纹理会跟着定点动画波动output.grabScreenPos = ComputeGrabScreenPos(output.originalPos);return output;}float4 frag(vertexOutput input) : COLOR{//完成折射形变部分//获取对GrabPass的采样坐标,坐标包含形变,形变从噪声获取//使用噪声来进行偏移,包含了时间变量以让折射存在动感float noise = tex2D(_NoiseTex, input.UV.xy + float2(_Time.x,_Time.x));//bump参数乘以纹素大小以得到正确的UV偏移量float2 bump = float2(noise * 2 - 1,noise * 2 - 1)*_GrabPass_TexelSize;bump *= _Distortion;//使用屏幕坐标作为采样的UV,其中加入了噪声来进行偏移input.grabScreenPos.xy += bump;input.grabScreenPos.xy /= input.grabScreenPos.w;//手动进行齐次除法,当然也可以使用宏//对grabpass采样float4 ditortionColor = tex2D(_GrabPass, input.grabScreenPos.xy);//这里先不管与其他物体相交部分的代码直接输出折射颜色return float4(ditortionColor);}ENDCG} }
}
直接输出后效果如下:
使用Cubemap与法线信息来模拟反射
为了模拟反射,首先我们需要知道我们通过反射会看到哪里,也就是我们需要知道反射过后的视角方向是多少,从而知道我们将对环境中的哪里进行采样,得到反射效果中应该看到的颜色。
而环境则使用cubemap来进行模拟。
在正确的地点创建对应的cubemap
首先,利用脚本可以将从某地点出发所看到的周围景象渲染到一张Cubemap中,但是这要求该地点在正确的位置,如上图的原理示意图,如果想要看到正确的反射景象,就必须使该位置和摄影机关于反射面对称。
因此需要从摄影机和反射面的位置角度信息出发,计算出向量 a a a,通过简单的计算我们得到原理图中的公式。
下面看代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class RenderToCubemap : MonoBehaviour
{public Transform renderFromPosition;public Cubemap cubemap;public GameObject reflectSurface;Vector3 lastposition;int i = 0;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){i = i++ % 1000;if (i != 0) return;//计算出摄像机和水平面的差向量Vector3 direction = transform.position - reflectSurface.transform.position;//计算出水平面的法向量Vector3 normal = Vector3.Normalize(reflectSurface.transform.up);float distance = Vector3.Dot(direction, normal);//计算出摄影机和水平面的距离distance = distance > 0 ? distance : -distance;//计算出正确渲染cubemap的位置renderFromPosition.position = transform.position - 2 * distance * normal;//渲染cubemaprenderFromPosition.gameObject.AddComponent<Camera>();Camera temporaryCamera= renderFromPosition.gameObject.GetComponent<Camera>();temporaryCamera.RenderToCubemap(cubemap);DestroyImmediate(temporaryCamera);}
}
这样我们就能如愿以偿地得到我们所希望得到的cubemap并作为实现反射效果时的环境映射纹理。
接下来介绍求出正确的反射角,来实现反射。
通过贴图获取法线信息
获取法线的方式通常有两种,即使用shader提供的NORMAL语义来为结构体中的变量赋值,从而获取模型顶点本身自带的法线信息,和通过一张法线贴图来获取切线空间下被描述为一个单位向量的法线信息。
这里为了实现水面的波纹,我们使用一张简单的法线贴图
关于法线贴图
法线贴图存储的是切线空间下的发现,其xyz分量分别属于[-1,1],为了让这些值能够存储在一张图片中,每个分量都加上一再统一乘上0.5,使其映射到[0,1]的范围内。而一般来说,如果法线贴图对发现不做任何更改的话,法向量应该是[0,0,1],对应到颜色就是[0.5,0.5,1],因此法线贴图看起来大致是蓝色的。
法线贴图中存储的三个变量分别对应切线,副切线和法线。法线空间是右手系。
为了使用贴图中的数据,需要将贴图中的数据读出来并解码,一般来说将贴图类型设置为Normal,然后调用Unity自带的UnpackNormal函数即可。
//完成水面法线与反射部分//首先获取法线纹理中的数据,并且解包。然后通过法线方向来求反射方向。//法线数据获取float2 uvForRefl = input.UV.zw + noise +(1+ noise * _SwormFlowFactor/100)*_Time.x;float4 packednormal =tex2D(_NormalTex, uvForRefl);float3 tangentNormal =UnpackNormal(packednormal);//法线变换到世界坐标float3 worldNormal = mul(float3x3(input.T2W0.xyz, input.T2W1.xyz, input.T2W2.xyz), tangentNormal);
法线空间到世界空间变换
由于我们直接获取的法向量是法线空间下的,一般来说无法与视线和光照方向直接计算比较,因此需要将法向量转换到世界空间中。
为此我们需要计算出法线空间到世界空间的转换矩阵。
从空间A到空间B的坐标转换只需要知道A的坐标轴在B空间下的向量表示即可。换而言之我们需要知道法线空间的坐标轴在世界空间下的表示。
因此,我们可以获得顶点自带的法线与切线,利用Unity自带的函数将他们转换到世界空间下,通过叉乘可以求出副切线。然后将这几个行向量竖向按照xyz,即切线,副切线,法线的顺序排列即可得到法线空间向世界空间变换的变换矩阵。
vertexOutput vert(vertexInput input){vertexOutput output;output.originalPos= UnityObjectToClipPos(input.vertex);// convert obj-space position to camera clip spaceoutput.pos = UnityObjectToClipPos(input.vertex);// compute depth (screenPos is a float4)output.grabScreenPos = ComputeGrabScreenPos(output.originalPos);//compute tangent2world matrix,needs world space tangent coordinatesfixed3 worldPos = mul(unity_ObjectToWorld, input.vertex);fixed3 worldNormal = UnityObjectToWorldNormal(input.normal);fixed3 worldTangent = UnityObjectToWorldDir(input.tangent.xyz);fixed3 worldBitangent = cross(worldNormal, worldTangent)*input.tangent.w;//将上述几个切线空间坐标轴按下列方式摆放,相当于做了一个转置output.T2W0 = float4(worldTangent.x, worldBitangent.x, worldNormal.x, worldPos.x);output.T2W1 = float4(worldTangent.y, worldBitangent.y, worldNormal.y, worldPos.y);output.T2W2 = float4(worldTangent.z, worldBitangent.z, worldNormal.z, worldPos.z);return output;}
计算反射角
计算反射角需要两个向量,即入射角和法线。
法线通过上述步骤已经能获得。入射角需要用当前顶点的世界空间坐标减去摄影机坐标,得到视角向量。
然后通过reflect函数计算出反射角,并利用texCUBE函数对Cubemap进行采样,得到反射颜色。
//利用法线坐标求出反射角对cubemap采样,这里需要一个cubemap,麻烦出去Unity那边做一个哦float3 worldPos = float3(input.T2W0.w, input.T2W1.w, input.T2W2.w);float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));float3 worldReflDir = normalize(reflect(-worldViewDir, worldNormal));float4 reflColor = texCUBE(_CubeMap, worldReflDir);
菲涅尔反射
当然水面是符合菲涅尔反射的,即当入射角较大的时候反射较强,反之则折射较强。
具体数值为 F r e s n e l = p o w ( 1 − m a x ( n ⃗ ∙ v ⃗ ) , p o w e r F a c t o r ) Fresnel=pow(1-max(\vec{n}\bullet \vec{v}),powerFactor) Fresnel=pow(1−max(n ∙v ),powerFactor)该值越大,则反射占的百分比越高。
float fresnel = pow(1 - max(0, dot(worldViewDir, worldNormal)), _FresnelFactor)*smoothstep(0, _FresnelDistanceFactor, depth);
完成后反射效果应该如下所示。
混合所有部分的颜色
最终效果如下所示:
附上源代码:
Shader "Custom/Water"
{Properties{_Color("Color",Color) = (1,1,1,1)_DepthFactor("DepthFactor",range(0,1)) =0.5_Distortion("_Distortion",float) = 1.0_NoiseTex("noiseSample",2D) = "white"{}_RampTex("RampColor",2D) = "white"{}_WaveTex("_WaveTex",2D) = "white"{}_NormalTex("_NormalTex",2D)="white"{}_BumpFactor("_BumpFactor",range(0,1)) =1.0_FresnelFactor("_FresnelFactor",range(0,10)) = 4.0_FresnelDistanceFactor("_FresnelDistanceFactor",range(10,100)) = 30_SwormFlowFactor("_SwormFlowFactor",range(0,100)) = 4.0_CubeMap("_CubeMap",Cube) = "_Skybox"{}}SubShader{Tags{"RenderType" = "Opaque" "Queue"="Transparent"}GrabPass{"_GrabPass"}Pass{CGPROGRAM// required to use ComputeScreenPos()#include "UnityCG.cginc"#pragma vertex vert#pragma fragment fragsampler2D _CameraDepthTexture;fixed4 _Color;float _DepthFactor;fixed _Distortion;sampler2D _NoiseTex;sampler2D _RampTex;sampler2D _WaveTex;sampler2D _GrabPass;sampler2D _NormalTex;samplerCUBE _CubeMap;float4 _NoiseTex_ST;float4 _WaveTex_ST;float _BumpFactor;float _FresnelFactor;float _FresnelDistanceFactor;float _SwormFlowFactor;float4 _NormalTex_ST;float4 _GrabPass_TexelSize;struct vertexInput{float4 vertex : POSITION;float4 texcoord:TEXCOORD;float3 normal:NORMAL;float4 tangent:TANGENT;};struct vertexOutput{float4 pos : SV_POSITION;float4 grabScreenPos:texcoord2;float4 UV:TEXCOORD3;float4 originalPos:TEXCOORD4;float4 T2W0:TEXCOORD5;float4 T2W1:TEXCOORD6;float4 T2W2 : TEXCOORD7;};vertexOutput vert(vertexInput input){vertexOutput output;output.originalPos= UnityObjectToClipPos(input.vertex);output.UV.xy = TRANSFORM_TEX(input.texcoord.xy, _NoiseTex);output.UV.zw = TRANSFORM_TEX(input.texcoord.xy, _NormalTex);//Animating//tex2D(_NoiseTex,input.texcoord)is not available in vertex shader //for there are no UV derivatives in Vertex Shaderfloat noise =tex2Dlod(_NoiseTex, float4(output.UV.xy, 0, 0));input.vertex.y += cos(_Time.y*10 * noise)*0.2*noise;// convert obj-space position to camera clip spaceoutput.pos = UnityObjectToClipPos(input.vertex);// compute depth (screenPos is a float4)output.grabScreenPos = ComputeGrabScreenPos(output.originalPos);//compute tangent2world matrix,needs world space tangent coordinatesfixed3 worldPos = mul(unity_ObjectToWorld, input.vertex);fixed3 worldNormal = UnityObjectToWorldNormal(input.normal);fixed3 worldTangent = UnityObjectToWorldDir(input.tangent.xyz);fixed3 worldBitangent = cross(worldNormal, worldTangent)*input.tangent.w;output.T2W0 = float4(worldTangent.x, worldBitangent.x, worldNormal.x, worldPos.x);output.T2W1 = float4(worldTangent.y, worldBitangent.y, worldNormal.y, worldPos.y);output.T2W2 = float4(worldTangent.z, worldBitangent.z, worldNormal.z, worldPos.z);return output;}float4 frag(vertexOutput input) : COLOR{//完成折射形变部分//获取对GrabPass的采样坐标,坐标包含形变,形变从噪声获取float noise = tex2D(_NoiseTex, input.UV.xy + float2(_Time.x,_Time.x));float2 bump = float2(noise * 2 - 1,noise * 2 - 1)*_GrabPass_TexelSize;bump *= _Distortion;input.grabScreenPos.xy += bump;input.grabScreenPos.xy /= input.grabScreenPos.w;//手动进行齐次除法,当然也可以使用宏//对grabpass采样float4 ditortionColor = tex2D(_GrabPass, input.grabScreenPos.xy);//完成物体交互部分// sample camera depth texturefloat depthSample = tex2D(_CameraDepthTexture, input.grabScreenPos.xy);//采样得到非线性深度float depth = LinearEyeDepth(depthSample);//线性深度float foamLine = depth- input.grabScreenPos.w;//遮罩计算foamLine=(1-smoothstep(0,2,foamLine));//反转一下好用一点float4 foamRamp = float4(tex2D(_RampTex, smoothstep(0, 1+ _DepthFactor, float2(1 - foamLine, 0.5))).rgb, 1);//对材质进行采样float4 foamcolor = _Color*foamRamp;//输出颜色//水波float4 wave = tex2D(_WaveTex,TRANSFORM_TEX(float2(foamLine,input.grabScreenPos.x),_WaveTex))*foamLine;//完成水面法线与反射部分//首先获取法线纹理中的数据,并且解包。然后通过法线方向来求反射方向。//法线数据获取float2 uvForRefl = input.UV.zw + noise +(1+ noise * _SwormFlowFactor/100)*_Time.x;float4 packednormal =tex2D(_NormalTex, uvForRefl);float3 tangentNormal =UnpackNormal(packednormal);tangentNormal.xy *= _BumpFactor;tangentNormal.z = sqrt(1 - dot(tangentNormal.xy, tangentNormal.xy));//法线变换到世界坐标float3 worldNormal = mul(float3x3(input.T2W0.xyz, input.T2W1.xyz, input.T2W2.xyz), tangentNormal);//利用法线坐标求出反射角对cubemap采样,这里需要一个cubemap,麻烦出去Unity那边做一个哦float3 worldPos = float3(input.T2W0.w, input.T2W1.w, input.T2W2.w);float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));float3 worldReflDir = normalize(reflect(-worldViewDir, worldNormal));float4 reflColor = texCUBE(_CubeMap, worldReflDir);float fresnel = pow(1 - max(0, dot(worldViewDir, worldNormal)), _FresnelFactor)*smoothstep(0, _FresnelDistanceFactor, depth);float4 color = foamcolor * foamcolor.a + (1 - foamcolor.a)*ditortionColor;color = color * (1 - fresnel) + fresnel * reflColor+wave.rrrr*0.5;return float4(color);}ENDCG} }
}
【Unity Shader学习笔记】实现反射与折射模拟水面、使用grabPass与环境贴图相关推荐
- Unity Shader 学习笔记(33) 全局光照(GI)、反射探针、线性空间和伽马空间、高动态范围(HDR)
Unity Shader 学习笔记(33) 全局光照(GI).反射探针.线性空间和伽马空间.高动态范围(HDR) 参考书籍:<Unity Shader 入门精要> [<Real-Ti ...
- Unity Shader 学习笔记(27)渲染轮廓线(描边)方法、卡通风格渲染、素描风格渲染
Unity Shader 学习笔记(27)渲染轮廓线(描边)方法.卡通风格渲染.素描风格渲染 参考书籍:<Unity Shader 入门精要> 渲染轮廓线(描边) 五种方法: 基于观察角度 ...
- Unity Shader 学习笔记(3)URP渲染管线带阴影PBR-Shader模板(ASE优化版本)
此 Shader 已经不是最新版本,最新版本见本专栏的第四篇文章: Unity Shader 学习笔记(4) 材质面板截图: 功能实现(URP渲染管线下): PBR材质.投射和接收阴影. 代码展示: ...
- 【Unity】Unity Shader学习笔记(二)渲染管线
文章目录 渲染管线(Randering Pipeline) 渲染流程 可编程渲染管线 应用阶段 把数据加载到显存中 设置渲染状态 调用DrawCall 几何阶段.光栅化阶段 渲染管线(Randerin ...
- 【Unity Shader学习笔记】(五)使用鼠标绘制自由多边形(附完整工程源码)
前言 在前面的文章中,我们已经了解了怎样使用Unity Shader来绘制简单的点和线,本文将延续上次的话题,讲述一下如何在场景中使用Unity Shader绘制自由多边形. 本文所述的程序,支持在地 ...
- Unity Shader学习笔记(5)基于摄像机深度和法线的后处理描边效果
文章目标 : 主要参考书籍为<Unity Shader入门精要>,本文主要注重于整理,方便后续直接调用. 渲染效果图: 主要相关代码: 摄像机脚本文件: using System.Coll ...
- Unity Shader学习笔记/Urp/水墨风效果
实现简易的水墨风效果大致分为三部分: 1.将原始的rgb贴图转化为灰度图 float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN ...
- Unity Shader 入门精要——反射、折射
目录 反射 折射 菲涅尔反射 反射 使用了反射效果的物体通常看起来就像镀了层金属.想要模拟反射效果很简单,我们只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可. ...
- Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板
写在之前 Shader变体.Shader属性定义技巧.自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用 ...
最新文章
- java如何获取明天的时间_java获取各种格式的时间,获取昨天明天日期,获取一天的开始结束时间...
- spring YML属性提示
- Bug之PHPMailer附件名字不支持中文的
- PHP(一)——概述及服务器配置
- 循序渐进PYTHON3(十三) --8-- DJANGO之ADMIN
- 打印1-100内的素数
- JavaScript alert延迟弹出
- 区块链支付平台技术的应用
- 电脑亮度多少对眼睛好_黄江办公文员学费大概是多少,黄江附近哪个电脑学校比较好一点...
- 2021Java面试总结!平安银行java开发面试
- 【银行】2015农行软开 笔试+面试 小记
- 按键精灵注册大漠插件
- Windows10禁用Adobe Creative Cloud开机自启动
- qq邮箱sina邮箱服务器拒绝,为什么有的qq邮箱,新浪这边邮不过去呢?
- 杂谈——科比球鞋(Nike)全记录
- matlab的se是个什么东西
- 照片恢复软件哪个好用?5个好用的照片恢复软件推荐
- 快消供应链管理解决方案
- Internet与Web技术的基本概念
- innosetup 打包