前言:
自己在学习UnityShader的时候从自己规划的学习路程,要进入体积渲染。就要去掌握Raymarching。但是想到视差贴图”本就是采用了光线步进的简化版算法。会简单一些,然后啊就去啃Rendering 20 ,发现他写的好复杂好头痛,看了它的源码,对于一个小白来说,一堆宏,一堆定义。但是它原文是讲的真的好,自己花了很长时间去拆解理解,(当然知乎已经有大佬写了文章了)终于做出来了,!(放鞭炮!!)可能也会有后面的人学习,所以就把自己学这些的心路历程,踩到的坑和详细方法给仔细写下来,并且只汇总在一个Shader,不添加cginc,和shaderGUI。将它作为自己第一次发文章的里程碑吧。(小白可能讲的不是很正确,但是我会尽力把我理解的写出来!欢迎各位大佬纠错!)
Rendering 20 (catlikecoding.com)
知乎同步:
介绍:
视差贴图是基于法线贴图的一个进阶版,在不增加模型表面顶点而用于更加明显的表达凹凸感的方法。可以用它做特效等。
视差凹陷
那么法线贴图和视差贴图的差别是什么呢?法线贴图改变的是光的行进,视差贴图依靠通过移动UV修改贴图在平面中制造了3D细节的假象.
那让我们开始吧!!!
1.视差贴图
1.1前期准备:
我们想把主贴图和法线贴图加上去
基础贴图
法线贴图
就先把URP模板添加后的放上主贴图和法向贴图。
Shader "Unlit/ParallaxMapping"
{
Properties
{
_MainTex ("Texture", 2D) = "white"
_NormalTex("NormalTex",2D) ="white"{}
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"//渲染管线设置为URP,告诉引擎是给通用渲染管线用的 就是在管线设置的情况下会有影响
"RenderType"="Opaque"
"Queue"="Geometry" //影响排序的顺序而已
}
LOD 100
//主纹理
Pass
{
//"LightMode"是指指定不同光照模式的关键字,而"UniversalForward"是指通用的前向渲染模式。
Tags{"LightMode" = "UniversalForward"}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
//pragrams
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3dll_9x
#pragma target 2.0
#pragma multi_compile_instancing
//这个库也一定要装
//......都是引用的内置的HLSL文件可以加一下然后了解
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 vertex : POSITION;
float3 normal:NORMAL;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float2 uv : TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float4 vertex : SV_POSITION;
};
//主贴图
sampler2D _MainTex;
//法向贴图
sampler2D _NormalTex;
float4 _MainTex_ST;
Varyings vert (Attributes v)
{
Varyings o;
o.vertex = TransformObjectToHClip(v.vertex);
o.worldNormal=TransformObjectToWorldNormal(v.normal);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv=v.uv;
return o;
}
float4 frag (Varyings i) : SV_Target
{
// sample the texture
float4 col = tex2D(_MainTex, i.uv);
float3 normalTex = UnpackNormal(tex2D(_NormalTex,i.uv));//法线贴图要结合光信息的,先不急
return col;
}
ENDHLSL
}
}
}
1.2视差贴图原理讲解
为了突出凹凸感觉,就需要改变我们是视觉的感受
蓝色为高度信息,白色为纹理,黄色为视角方向
如上图0代表纹理最底下,1为最高处,蓝色为高度信息。模型的顶点信息其实一直没有变是0,而蓝色是表示为高度图的信息。当视线向下看的时候,采样的原点是在b处,而实际我们看到的信息确实在a处。所以说视角采样的像素信息应该是a处的像素,而不是b的像素。采用异从而给我们一种立体的感觉。
知道原理过后让我们开始吧
1.3构建TBN矩阵获得切线空间并且计算偏移坐标
先材质面板添加一下参数。
[Header(ParallaxMap)]
_ParallaxMap("ParallaxMap",2D) = "black"{}
_ParallaxStrength("ParallaxStrength",Range(0,1)) = 0.3
_Parallaxskew("skew",Range(0,15))=1 //偏离值
因为视差是由相对于观察者的透视投影引起的。所以我们要把纹理坐标移到这个位置,根据视角方向移动坐标。然而纹理坐标存在于切线空间中,所以为了去调整这些坐标,我们就需要切线空间中的视图方向。所以说我们就需要矩阵乘法也就是TBN去转换。
现在构造器里面声明法线,切线:
struct Attributes
{
....
float4 tangent:TANGENT;
float3 normal:NORMAL;
.....
};
//声明一个TBN和TangentVS可以传参数
struct Varyings
{
......
//切线转职矩阵
float3x3 TBN:TEXCOORD3;
float3 TangentVS:TEXCOORD8;
.....
};
在顶点着色器构建TBN矩阵
Varyings vert (Attributes v)
{
.....
//1.4.1准备好TBN
float3x3 TBN = float3x3(
v.tangent.xyz,
cross(v.normal,v.tangent.xyz)* v.tangent.w, //副切线
v.normal
) ;
o.TBN=TBN;//传
.....
}
再在顶点着色器求出TangentVDir(切线空间视图方向)
o.TangentVS=mul(TBN,ObjSpaceViewDir(v.vertex)); //摄像机视角乘以TBN矩阵获得切线空间视角
o.TangentVS=normalize(o.TangentVS);
讲解:
1.在摄像机视角这里有一个细节。
它是运用的物体空间的摄像机位置,而不是世界空间的摄像机位置
我尝试过两种方法却有两种效果。效果却有略微差别
方法一:用世界空间转化为物体空间
half3 VdirWS=normalize(_WorldSpaceCameraPos-o.posWS); //用摄像机位置-物体世界空间的位置
half3 VdirOS=TransformWorldToObject(VdirWS); //再用URP的矩阵转换为物体空间
o.TangentVS=mul(TBN,VdirOS);
方法二:用untiy.CG的方法,这个需要拿出来
//方法一 摄像机的物体视角
inline float3 ObjSpaceViewDir( in float4 v )
{
float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
return objSpaceCameraPos - v.xyz;
}
方法二的效果:
而方法一挑动_Parallaxskew只是uv的移动而已。
但是最终效果方法方法二才是正确的,自己去查询一下和问了一下chatGPT,原因方法一是这种方法没有考虑物体自身的变换,如果物体有缩放、旋转或平移,计算出的视角方向可能不准确。
然后我们发现当视角下压至90度的时候,产生位移是最大的。而视角与法线(0,0,1)切空间中的视角方向等于表面法线(0,0,1),因此不会产生位移
所以视角越浅,投影越大,位移效应越大。
视角下压,发现扭曲会特别严重
1.4.基于高度的移动
用了高度图我们就可以让贴图看起来更高,我们使用视差贴图来缩放位移。采样地图,使用它的G通道作为高度,应用视差强度,并使用它来调节位移。
高度图
首先先采用贴图,我们可以创建一个函数获得高度图,方便我们后面的其他函数的调用。也方便来分类
//获得高度的方法
float GetParallaxHeight (float2 uv)
{
return tex2D(_ParallaxMap, uv.xy).g;
}
然后创建一个函数,设置视差偏转
//设置ParallaxOffset(视差补偿)--之前的方法
float2 ParallaxOffset (float2 uv, float2 viewDir)
{
float height = GetParallaxHeight(uv);
height -= 0.5;
height *= _ParallaxStrength;
return viewDir * height;
}
height -= 0.5; 为什么要减去0.5?
高度减去了0.5后,可以使低的区域也向下移动,而中间的区域保持在原来的位置,从而使效果更好。
但是这个_ParallaxStrength,还是只能在很低的强度,不然会炸。
效果,在强度0.036
在片段着色器调用方法
float2 uvOffset = ParallaxOffset(i.uv.xy,VdirTangentSpace);
1.5 正确地设置投影偏移值
目前使用的视差映射技术被称为带偏移限制的视差映射。我们只是使用视图方向的XY部分,它的最大长度是1。纹理偏移是有限的。效果可以给出不错的结果,但不能代表正确的透视投影。
我们必须缩放视图方向矢量,使它的Z分量变成1,我们通过除以它自己的Z分量来做。因为我们以后不需要用到Z,我们只需要用X和Y除以Z。因为高度差为1,根据相似三角形,xy/z得到正确的偏移
o.TangentVS.xy /= o.TangentVS.z;//1.5为了方便找到合适的偏移量
然后把__Parallaxskew,参数放入。
o.TangentVS.xy /= (o.TangentVS.z+_Parallaxskew);
1.6 Detail UV(用个纹理来明显我们的效果)和简化
细节贴图
标准着色器还简单地将UV偏移量添加到细节UV中,存储在UV插值器的ZW组件中。讲uv变成float4
float4 uv : TEXCOORD0;
然后存在zw分量
o.uv.zw = TRANSFORM_TEX(v.uv,_DetailTex);
再去片段着色器调用
float4 DetailTex=tex2D(_DetailTex,i.uv.zw);
设置5,5。设置uvoffset添加纹理细节
//1.7添加细节纹理
float2 uvOffset = ParallaxOffset(i.uv.xy,VdirTangentSpace);
i.uv.xy +=uvOffset;
i.uv.zw +=uvOffset*(_DetailTex_ST.xy / _MainTex_ST.xy);//使之细节贴图意义对应的。然后有了扭转效果
i.uv.zw +=uvOffset*(_DetailTex_ST.xy / _MainTex_ST.xy);
目的使之缩放应该相对于MainTex
效果:
加了纹理的效果
1.7细节的增加
因为法线贴图影响的光源也是要使用TBN矩阵,法线贴图目的是影响光源。所以我们做一个半兰伯特光照来凸显效果。
//添加法线贴图
float3 normalTex = UnpackNormal(tex2D(_NormalTex,i.uv));
half3x3 TBN =i.TBN;
half3 normalWS = normalize(mul(normalTex,TBN));
half3 L =normalize(_MainLightPosition);
half NdotL=normalize(dot(normalWS,L));
half3 halfdiffuse = max(0.3,NdotL*0.5+0.5);//确定阴影效果
//环境光
float3 ambient =UNITY_LIGHTMODEL_AMBIENT;
halfdiffuse +=ambient;
col.rgb *=halfdiffuse;
效果:
1.8阶段性代码
Shader "Parallax Mapping"
{
Properties
{ [Header(ParallaxMap)]
_ParallaxMap("ParallaxMap",2D) = "black"{}
_ParallaxStrength("ParallaxStrength",Range(0,1)) = 0.3
_Parallaxskew("skew",Range(0,15))=1
[Header(Detail grid texture.)]
_DetailTex("DetailGridTex",2D)="white"{}
_MainTex ("Texture", 2D) = "white" {}
[Header(NormalTex)]
_NormalTex("NormalTex",2D) ="white"{}
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"//渲染管线设置为URP,告诉引擎是给通用渲染管线用的 就是在管线设置的情况下会有影响
"RenderType"="Opaque"//表示渲染层级为不透明,实则没啥作用
"Queue"="Geometry" //影响排序的顺序而已
}
LOD 100
//主纹理
Pass
{
//最关键的Tag
//"LightMode"是指指定不同光照模式的关键字,而"UniversalForward"是指通用的前向渲染模式。
Tags{"LightMode" = "UniversalForward"}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
//pragrams
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3dll_9x
#pragma target 2.0
#pragma multi_compile_instancing
//这个库也一定要装
//......都是引用的内置的HLSL文件可以加一下然后了解
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct Varyings
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 posWS:TEXCOORD2;
//切线转职矩阵
float3x3 TBN:TEXCOORD3;
float3 TangentVS:TEXCOORD8;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
//视差贴图
sampler2D _ParallaxMap;
float _ParallaxRefer;
float _ParallaxStrength;
float _Parallaxskew;
sampler2D _DetailTex;
float4 _DetailTex_ST;
//方法一 摄像机的物体视角
inline float3 ObjSpaceViewDir( in float4 v )
{
float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
return objSpaceCameraPos - v.xyz;
}
//获得高度的方法
float GetParallaxHeight (float2 uv)
{
return tex2D(_ParallaxMap, uv.xy).g;
}
//设置ParallaxOffset(视差补偿)--之前的方法
float2 ParallaxOffset (float2 uv, float2 viewDir)
{
float height = GetParallaxHeight(uv);
height -= 0.5;
height *= _ParallaxStrength;
return viewDir * height;
}
Varyings vert (Attributes v)
{
Varyings o;
o.vertex = TransformObjectToHClip(v.vertex);
o.worldNormal=TransformObjectToWorldNormal(v.normal);
o.posWS=TransformObjectToWorld(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv,_DetailTex);
//增加uv
//其他TBN的写法
//转置计算 本地转换为世界
// o.worldTangent = TransformObjectToWorldDir(v.tangent);
// //又v.tangent.w决定我们副切线的方向
// float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
// //由叉积计算出副切线cross (a,b)叉积运算
// half3 worldBinormal = cross(o.worldNormal, o.worldTangent) * tangentSign;
// //写法1
// o.tSpace0 = float3(o.worldTangent.x,worldBinormal.x,o.worldNormal.x);
// o.tSpace1 = float3(o.worldTangent.y,worldBinormal.y,o.worldNormal.y);
// o.tSpace2 = float3(o.worldTangent.z,worldBinormal.z,o.worldNormal.z);
//
//1.4.1准备好TBN
float3x3 TBN = float3x3(
v.tangent.xyz,
cross(v.normal,v.tangent.xyz)* v.tangent.w,
v.normal
) ;
o.TBN=TBN;
//1.4.2.求出视线方向在切线空间下的一个坐标 ,并计算偏移坐标
//方法1
o.TangentVS=mul(TBN,ObjSpaceViewDir(v.vertex));
o.TangentVS=normalize(o.TangentVS);
o.TangentVS.xy /= (o.TangentVS.z+_Parallaxskew);//1.5为了方便找到合适的偏移量
// //方法2
// half3 VdirWS=normalize(_WorldSpaceCameraPos-o.posWS);
// half3 VdirOS=TransformWorldToObject(VdirWS);
// o.TangentVS=mul(TBN,VdirOS);
return o;
}
float4 frag (Varyings i) : SV_Target
{
// sample the texture
//1.4.2运用TangentVS
half3 VdirTangentSpace =i.TangentVS;
//half3 VdirTangentSpace = half3(dot(i.tSpace0,VdirOS),dot(i.tSpace1,VdirOS),dot(i.tSpace2,VdirOS));//取得到切线空间得值
//.变化uv得切线
VdirTangentSpace = normalize(VdirTangentSpace);
i.uv.xy += VdirTangentSpace.xy * _ParallaxStrength;
//1.5增加高度效果(后面写ParallaxOffset方法进行概括)
float2 uvOffset = ParallaxOffset(i.uv.xy,VdirTangentSpace); //将这个偏转 化一个参数
//1.7添加细节纹理
i.uv.xy +=uvOffset;
i.uv.zw +=uvOffset*(_DetailTex_ST.xy / _MainTex_ST.xy);//使之细节贴图意义对应的。然后有了扭转效果
float4 col = tex2D(_MainTex, i.uv);
float4 DetailTex=tex2D(_DetailTex,i.uv.zw);
col*=DetailTex*2;//增加一点亮度
//添加法线贴图
float3 normalTex = UnpackNormal(tex2D(_NormalTex,i.uv));
half3x3 TBN =i.TBN;
half3 normalWS = normalize(mul(normalTex,TBN));
half3 L =normalize(_MainLightPosition);
half NdotL=normalize(dot(normalWS,L));
half3 halfdiffuse = max(0.3,NdotL*0.5+0.5);//确定阴影效果
//环境光
float3 ambient =UNITY_LIGHTMODEL_AMBIENT;
halfdiffuse +=ambient;
col.rgb *=halfdiffuse;
return col;
}
ENDHLSL
}
}
}
2.Raymarching光线步进
2.1.原理分析
我们的视差效果的工作原理是,通过高度和体积发射一个视图光线,并确定它到达表面的位置。是通过在光线进入体的地方对高度图进行一次采样来实现。当采样的点和交点实际上具有相同的高度时,这才是正确的。如果我们能算出光线到达高度场的位置,那么我们就能总能找到真正的可见表面点。这不能用单个纹理样本完成。我们必须沿着视图光线小步移动,每次采样高度场,直到我们到达表面。这种技术被称为光线步进。
光线步进示意图
2.2视差函数 ParallaxRaymarching
这是差别与ParallaxOffset另一种函数,是pro版本。创建一个函数
float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
float2 uvOffset = 0; //将这个偏转 化一个参数
float stepSize =0.1; //步数是10布,1/10
float2 uvDelta =viewDir *(stepSize*_ParallaxStrength); //用视差强度,我们可以调整每一步采样的高度
float stepHeight =1;//归一化
float surfaceHeight =GetParallaxHeight(uv);
}
(1.) float2 uvDelta =viewDir *(stepSize*_ParallaxStrength);
用视差强度,我们可以调整每一步采样的高度。
(2.) float stepSize =0.1;
因为单位长度是1,结果=1/步数,步数越多最后呈现出来的效果更好,但是对性能的要求也会更高
编译步进循环
我们会想到两个循环,但是用While,编辑器警告告诉我们在循环中使用了梯度指令。这指的是我们循环中的纹理采样。GPU必须弄清楚要使用哪个mipmap级别,为此它需要比较相邻片段的使用过的UV坐标。只有当所有片段执行相同的代码时,它才能这样做。这对我们的循环来说是不可能的,因为它可以提前终止,每个片段可能不同。因此编译器将展开循环,这意味着它将始终执行所有九个步骤,而不管我们的逻辑是否建议我们可以提前停止
编译失败是因为编译器无法确定循环的最大迭代次数。它不知道这个最多等于9。让我们明确一点,把while循环变成for循环,这样就有了限制。
for (int i =1;i<10 && stepHeight>surfaceHeight;i++) //可能会导致性能不好但是是学习无所谓了
{
uvOffset -=uvDelta;
stepHeight-=stepSize;
surfaceHeight=GetParallaxHeight(uv+uvOffset);
}
然后用这个去替换,ApplyParallax
效果图
2.3使用更多步
这种基本的射线行进方法最适合陡视差映射。效果的质量由我们的样品分辨率决定。有些方法根据视角使用可变数量的步骤。更浅的角度需要更多的步骤,因为光线更长。
提高质量的明显方法是增加样本的数量
float stepSize =0.02; //步长是0.0.2 1/50
用50步去
for (int i =1;i<50 && stepHeight>surfaceHeight;i++) //可能会导致性能不好但是是学习无所谓了
{
uvOffset -=uvDelta;
stepHeight-=stepSize;
surfaceHeight=GetParallaxHeight(uv+uvOffset);
}
整个代码
//raymarching方法创建一个新函数--采用光线步进的方法
float2 ParallaxRaymarching (float2 uv,float2 viewDir)
{ float2 uvOffset = 0; //将这个偏转 化一个参数
float stepSize =0.02; //步长是0.1
float2 uvDelta =viewDir *(stepSize*_ParallaxStrength); //用视差强度,我们可以调整每一步采样的高度
float stepHeight =1;
float surfaceHeight =GetParallaxHeight(uv);
//循环使用梯度
for (int i =1;i<50 && stepHeight>surfaceHeight;i++) //可能会导致性能不好但是是学习无所谓了
{
uvOffset -=uvDelta;
stepHeight-=stepSize;
surfaceHeight=GetParallaxHeight(uv+uvOffset);
}
return uvOffset;
}
50步骤的效果
2.4图层间插值(提高质量)
提高质量的一种方法是对光线实际照射到表面的位置进行差值,上一步我们在水面上,下一步我们就在水面下了。在这两步之间的某个地方,射线一定击中了表面。
射线点和面点对定义了两条线段。因为射线和表面碰撞,这两条线相交。因此,如果我们跟踪前一步,我们可以在循环后执行直线相交。我们可以用这个信息来近似真实的交点。
原理图
在迭代过程中,我们必须跟踪之前的UV偏移量,步长高度和表面高度。最初,这些等于第一个样本在循环之前的值
float2 prevUVOffset = uvOffset;
float prevStepHeight = stepHeight;
float prevSurfaceHeight = surfaceHeight;
for ( ... )
{
prevUVOffset = uvOffset;
prevStepHeight = stepHeight;
prevSurfaceHeight = surfaceHeight;
…
}
循环之后,我们计算直线相交的位置。我们可以使用它来在前一个和最后一个UV偏移之间进行插值。
float prevDifference = prevStepHeight - prevSurfaceHeight;//之前的
float difference = surfaceHeight - stepHeight;//之后的
float t = prevDifference / (prevDifference + difference); //求出插值的因子
uvOffset = lerp(prevUVOffset, uvOffset, t);//插值计算
完整:
//跟踪之前的uv值
float2 prevUVOffset = uvOffset;
float prevStepHeight = stepHeight;
float prevSurfaceHeight = surfaceHeight;
//循环使用梯度
for (int i =1;i<50 && stepHeight>surfaceHeight;i++) //可能会导致性能不好但是是学习无所谓了
{
//记录之前的uv偏移
prevUVOffset = uvOffset;
prevStepHeight = stepHeight;
prevSurfaceHeight = surfaceHeight;
//记录之后的uv偏移
uvOffset -=uvDelta;
stepHeight-=stepSize;
surfaceHeight=GetParallaxHeight(uv+uvOffset);
}
//进行插值
float prevDifference = prevStepHeight - prevSurfaceHeight;//之前的
float difference = surfaceHeight - stepHeight;//之后的
float t = prevDifference / (prevDifference + difference); //求出插值的因子
uvOffset = lerp(prevUVOffset, uvOffset, t);//插值计算
效果对比。Step为10的时候。
插值之前 (10步)
插值之后(10步)
是不是很明显啦!!
做完了捏(放鞭炮!!)哗啦啦啦啦
2.5(可选).图层间搜索(更加积分思想)---处理不规则的高度场--优化的算法
通过在两个步骤之间进行线性插值,我们假设表面在它们之间是直线的。然而,情况往往并非如此。为了更好地处理不规则的高度场,我们必须搜索两个步骤之间的实际交点。或者至少离目标更近一点。
完成循环后,不要使用最后一个偏移量,而是将偏移量调整到最后两个步骤之间的中间位置。对该点的高度进行采样。如果我们最终在表面以下,移动偏移量的四分之一回到前一个点并再次采样。如果我们最终在地表以上,向前移动四分之一到最后一点并再次采样。它最适合浮雕映射方法。每走一步,距离就减半,直到到达目的地。
在我们的例子中,我们只是这样做固定的次数,达到一个期望的解决方案。在一步中,我们总是在最后两点的中间,即0.5处结束。通过两个步骤,我们最终得到0.25或0.75。通过三个步骤,它是0.125、0.375、0.625或0.875。等等
当然这种方法,我并没有去尝试,有兴趣的朋友可以去原文理解然后去试试。
3.总结
总之这就是自己的学习路程和具体的总结,实现了还不做的效果。当然!自己也是一个学习者,也是按照一个学习者的思维来写的文章。肯定其中也有考虑不完全的内容,第一次写文章肯定也有结构的疏忽而且可能有很多错别字。希望各位佬们可以理解。!谢谢喵!
总代码:
Shader "Parallax Mapping"
{
Properties
{ [Header(ParallaxMap)]
_ParallaxMap("ParallaxMap",2D) = "black"{}
_ParallaxStrength("ParallaxStrength",Range(0,1)) = 0.3
_Parallaxskew("skew",Range(0,15))=1
[Header(Detail grid texture.)]
_DetailTex("DetailGridTex",2D)="white"{}
_MainTex ("Texture", 2D) = "white" {}
[Header(NormalTex)]
_NormalTex("NormalTex",2D) ="white"{}
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"//渲染管线设置为URP,告诉引擎是给通用渲染管线用的 就是在管线设置的情况下会有影响
"RenderType"="Opaque"//表示渲染层级为不透明,实则没啥作用
"Queue"="Geometry" //影响排序的顺序而已
}
LOD 100
//主纹理
Pass
{
//最关键的Tag
//"LightMode"是指指定不同光照模式的关键字,而"UniversalForward"是指通用的前向渲染模式。
Tags{"LightMode" = "UniversalForward"}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
//pragrams
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3dll_9x
#pragma target 2.0
#pragma multi_compile_instancing
//这个库也一定要装
//......都是引用的内置的HLSL文件可以加一下然后了解
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct Varyings
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 posWS:TEXCOORD2;
//切线转职矩阵
float3x3 TBN:TEXCOORD3;
float3 TangentVS:TEXCOORD8;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
//视差贴图
sampler2D _ParallaxMap;
float _ParallaxRefer;
float _ParallaxStrength;
float _Parallaxskew;
sampler2D _DetailTex;
float4 _DetailTex_ST;
//方法一 摄像机的物体视角
inline float3 ObjSpaceViewDir( in float4 v )
{
float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
return objSpaceCameraPos - v.xyz;
}
//获得高度的方法
float GetParallaxHeight (float2 uv)
{
return tex2D(_ParallaxMap, uv.xy).g;
}
//设置ParallaxOffset(视差补偿)--之前的方法
float2 ParallaxOffset (float2 uv, float2 viewDir)
{
float height = GetParallaxHeight(uv);
height -= 0.5;
height *= _ParallaxStrength;
return viewDir * height;
}
//raymarching方法创建一个新函数--采用光线步进的方法
float2 ParallaxRaymarching (float2 uv,float2 viewDir)
{ float2 uvOffset = 0; //将这个偏转 化一个参数
float stepSize =0.02; //步长是0.1
float2 uvDelta =viewDir *(stepSize*_ParallaxStrength); //用视差强度,我们可以调整每一步采样的高度
float stepHeight =1;
float surfaceHeight =GetParallaxHeight(uv);
//跟踪之前的uv值
float2 prevUVOffset = uvOffset;
float prevStepHeight = stepHeight;
float prevSurfaceHeight = surfaceHeight;
//循环使用梯度
for (int i =1;i<50 && stepHeight>surfaceHeight;i++) //可能会导致性能不好但是是学习无所谓了
{
//记录之前的uv偏移
prevUVOffset = uvOffset;
prevStepHeight = stepHeight;
prevSurfaceHeight = surfaceHeight;
//记录之后的uv偏移
uvOffset -=uvDelta;
stepHeight-=stepSize;
surfaceHeight=GetParallaxHeight(uv+uvOffset);
}
//进行插值
float prevDifference = prevStepHeight - prevSurfaceHeight;//之前的
float difference = surfaceHeight - stepHeight;//之后的
float t = prevDifference / (prevDifference + difference); //求出插值的因子
uvOffset = lerp(prevUVOffset, uvOffset, t);//插值计算
return uvOffset;
}
Varyings vert (Attributes v)
{
Varyings o;
o.vertex = TransformObjectToHClip(v.vertex);
o.worldNormal=TransformObjectToWorldNormal(v.normal);
o.posWS=TransformObjectToWorld(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv,_DetailTex);
//增加uv
//其他TBN的写法
//转置计算 本地转换为世界
// o.worldTangent = TransformObjectToWorldDir(v.tangent);
// //又v.tangent.w决定我们副切线的方向
// float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
// //由叉积计算出副切线cross (a,b)叉积运算
// half3 worldBinormal = cross(o.worldNormal, o.worldTangent) * tangentSign;
// //写法1
// o.tSpace0 = float3(o.worldTangent.x,worldBinormal.x,o.worldNormal.x);
// o.tSpace1 = float3(o.worldTangent.y,worldBinormal.y,o.worldNormal.y);
// o.tSpace2 = float3(o.worldTangent.z,worldBinormal.z,o.worldNormal.z);
//
//1.4.1准备好TBN
float3x3 TBN = float3x3(
v.tangent.xyz,
cross(v.normal,v.tangent.xyz)* v.tangent.w,
v.normal
) ;
o.TBN=TBN;
//1.4.2.求出视线方向在切线空间下的一个坐标 ,并计算偏移坐标
//方法1
o.TangentVS=mul(TBN,ObjSpaceViewDir(v.vertex));
o.TangentVS=normalize(o.TangentVS);
o.TangentVS.xy /= (o.TangentVS.z+_Parallaxskew);//1.5为了方便找到合适的偏移量
// //方法2
// half3 VdirWS=normalize(_WorldSpaceCameraPos-o.posWS);
// half3 VdirOS=TransformWorldToObject(VdirWS);
// o.TangentVS=mul(TBN,VdirOS);
return o;
}
float4 frag (Varyings i) : SV_Target
{
// sample the texture
//1.4.2运用TangentVS
half3 VdirTangentSpace =i.TangentVS;
//half3 VdirTangentSpace = half3(dot(i.tSpace0,VdirOS),dot(i.tSpace1,VdirOS),dot(i.tSpace2,VdirOS));//取得到切线空间得值
//.变化uv得切线
VdirTangentSpace = normalize(VdirTangentSpace);
i.uv.xy += VdirTangentSpace.xy * _ParallaxStrength;
//1.5增加高度效果(后面写ParallaxOffset方法进行概括)
float2 uvOffset = ParallaxRaymarching(i.uv.xy,VdirTangentSpace); //将这个偏转 化一个参数
//1.7添加细节纹理
i.uv.xy +=uvOffset;
i.uv.zw +=uvOffset*(_DetailTex_ST.xy / _MainTex_ST.xy);//使之细节贴图意义对应的。然后有了扭转效果
float4 col = tex2D(_MainTex, i.uv);
float4 DetailTex=tex2D(_DetailTex,i.uv.zw);
col*=DetailTex*2;//增加一点亮度
//添加法线贴图
float3 normalTex = UnpackNormal(tex2D(_NormalTex,i.uv));
half3x3 TBN =i.TBN;
half3 normalWS = normalize(mul(normalTex,TBN));
half3 L =normalize(_MainLightPosition);
half NdotL=normalize(dot(normalWS,L));
half3 halfdiffuse = max(0.3,NdotL*0.5+0.5);//确定阴影效果
//环境光
float3 ambient =UNITY_LIGHTMODEL_AMBIENT;
halfdiffuse +=ambient;
col.rgb *=halfdiffuse;
return col;
}
ENDHLSL
}
}
}