当前位置:网站首页>了解Shader
了解Shader
2022-07-27 23:41:00 【我就是CC啊】
目录
这一篇文章将从头编写一个简单的Shader来帮助我们更好的了解Shader,Unity也有非常方便创建Shader的方法就是shader graph,但是我们用代码的形形式可以更好的了解它的底层原理。
创建Unity工程
首先我们创建一个URP模板的工程

然后在Assets目录下创建一个Unlit Shader ,同样就是一个无光照的shader

然后双击打开我们的Shader
编写代码
基本格式
打开shader把里面的代码全部删掉,我们要从头自己写一个简单的shader来更好的理解它的代码结构,我们在这里编写的其实并不是一个shader语言,而是Unity的ShaderLab。首先我们先Shader代码块,后面跟的字符串是这个shader的名字。然后我们再创建一个Subshader,一个Shader可能有一个或者多个SubShader,因为可能会需要根据GPU的设置、硬件或者渲染管线的不同做切换,我们这里就写一个SubShader就可以了,最后我们创建一个pass代码块,这里就是我们Shader真正工作的地方,pass可以有多个,可以理解多个pass就是对一个物体进行多次不同方式的渲染,我们这里先编辑一个pass
Shader "MyFirstShader"
{
SubShader
{
Pass
{
}
}
}
在pass代码块里,我们要开始用shader语言进行shader的编写了,先确定用什么语言,我们这里用到HLSL的PROGRAM
Pass
{
HLSLPROGRAM
ENDHLSL
}着色器函数
回忆我们上一个篇文章讲的渲染管线有两个非常主要的着色器一个是顶点着色器,一个是像素着色器,所以我们要先声明两个函数。顶点着色器是让我们的顶点变换到裁切空间,所以它的返回值是一个向量,HLSL的向量用float4来表示, 函数名我们就命名为vertex,这个函数其实就是我们的模型上的所有顶点被这个函数处理也就是遍历所有顶点,只不过处理它的是GPU而不是CPU,所以他的处理会很快。这个函数要输入值,也就是我们处理前顶点的位置,也是一个float4的一个参数,因为我们要我们之前CPU准备的数据正确的传给这个参数,所以我们要让他跟特殊的字符做关联,再后面加一个“:”,然后大写的POSITION,这代表我们顶点的位置数据,这个大写的POSITION叫语义(semantic),同样返回值也要关联SV_POSITION,SV代表系统中的值(System Value)。
float4 Vertex(float4 vertex : POSITION) ; SV_POSITION
{
}接下来还有一个函数,处理我们的像素的就叫Pixel有的地方可能叫(fragment)也就是片元,其实是一个意思。因为DX就叫它Pixel像素着色器,它的返回值是一个颜色,也算一个精度不需要很高的向量,我们给它一个half4,和顶点着色器一样
half4 Pixel():SV_TARGET
{
}创建完函数再和上面创建两个预编译指令
#pragma vertex Vertex
#pragma fargment Pixel如果我们直接不做处理返回顶点的位置,就会出现下面的情况

所以我们这里要用到一个MVP的矩阵变换,也就是model view projection,这里对矩阵变化有了解的话可以更好的理解,我们要让所有的模型都参考同一个坐标也就是我们的世界坐标(0,0,0)点,这样模型的顶点位置才能正常描述。因为模型有可能位移有可能旋转有可能缩放,所有就构成了一个模型矩阵(Model Matrix),如果了解矩阵变化的,就知道让顶点的位置与这个矩阵相乘,我们就会将位置变换到一个新的空间,这就是MVP的第一步M。第二部就是view,会加上一个摄像机,这一步就是以摄像机为参照物,把世界空间里的模型都变换到摄像机中。第三步是projection,是如何去构建这个矩阵,这部分CPU已近帮我们算好了,我们只要知道怎么用,它其实就是把模型投影到一个1*1的一个立方体里面,为了就是方便我们光栅化

说了这么多回到我们的代码里,我们应该怎么使用这些矩阵,其实Unity已经帮我们传进来了,我们需要include应该文件
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"获取到矩阵后,矩阵的运算就是mul()也是我们先做MVP里面的M,就是UNITY_MATRIX_M去乘我们传进来的vertex,我们的xyz怎么去代表是应该向量还是一个点呢,就是看他的齐次坐标,所以我们的float4的w应该是1.0,就是告诉他我们是一个点。接下来进入V阶段,也有一个矩阵UNITY_MATRIX_VP,看代码
float4 Vertex(float4 vertex:POSITION):SV_POSITION
{
float4 positionWS = mul(UNITY_MATRIX_M,float4(vertex.xyz,1.0));
float4 positionCS = mul(UNITY_MATRIX_VP,float4(positionWS.xyz,1.0));
return positionCS;
}这样我们就会出现效果了

当然我们还有更方便的办法就是Unity有一个内置的函数,直接完成这些变化用到这个方法TransformObjectToHClip(),效果一样。
float4 Vertex(float4 vertex:POSITION):SV_POSITION
{
//float4 positionWS = mul(UNITY_MATRIX_M,float4(vertex.xyz,1.0));
//float4 positionCS = mul(UNITY_MATRIX_VP,float4(positionWS.xyz,1.0));
return TransformObjectToHClip(vertex);
}添加参数
到了这一步,我们的shader颜色很单一,我们要为shader创建参数,再上面创建一个新的代码块,叫Properties代码块,然后往里面添加变量,具体看代码
Properties
{
_Color("Color",color) = (1,1,0,1)
}创建完color这个属性之后,我们还需要再下面shader的pass里面声明一些这个变量,颜色的话精度不用太高,就half4
half4 _Color;这样我们就有颜色可以选择了

接下来我们就要给它上贴图啦,为了加上贴图,我们的顶点数据就不止position了,我们还得知道它的uv也就是贴图坐标,继续添加一个参数,但是随着参数的增加,我们希望做成一个结构体。所以我们创建一个结构体,uv是一个float2,语义是TEXCOORD0,看代码
struct Attributes
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
};之后我们就可以把这个结构体交给顶点着色器函数处理,它处理后返回出来的值又是一个像素着色器需要的数据,所以我们为这个数据再创建一个结构体,看代码
struct Varyings
{
float4 positionCS:SV_POSITION;
float2 uv:TEXCOORD0;
};好了我们现在对我们的顶点着色器函数进行一个改写了,声明一个结构体,然后直接传入结构体
Varyings Vertex(Attributes IN)
{
Varyings OUT;
OUT.positionCS = TransformObjectToHClip(IN.vertex.xyz);
OUT.uv = IN.uv;
//float4 positionWS = mul(UNITY_MATRIX_M,float4(vertex.xyz,1.0));
//float4 positionCS = mul(UNITY_MATRIX_VP,float4(positionWS.xyz,1.0));
return OUT;
}接下来我们来改一下我们的像素着色器函数,我们就是用传来的uv信息来采样他的贴图,采样贴图是一个Tex2D(),的一个是需要我们的贴图名字,我们回到创建一个贴图_BaseMap并声明它
_BaseMap("Base Map",2D) = "white"{}和声明
sampler2D _BaseMap;然后是像素着色器函数
half4 Pixel(Varyings IN):SV_TARGET
{
half4 color;
color.rgb = tex2D(_BaseMap,IN.uv).rgb * _Color;
color.a = 1.0;
return color;
}这里的rgb跟xyz可以说是一样的,至于他的alpha是1,我们暂时不alpha就是1.0,然后乘上的_Color是我们之前的参数,然后我们回到Unity就可以看到多了一个参数,可以改变贴图啦

增加光照
因为我上面只是创建了一个最简单shader,接下来我们可以为他增加光照效果。不过我们不会上来就给它添加一个基于物理的光影效果,就是我们的PBR Shading。因为他有比较复杂的逻辑和计算,这里我们用一下基本的Lambert、Phong、Blinn-Phong光照模型,也叫经验模型,顾名思义就是考经验摸索出来的光照效果。
实现光影效果有两个元素,一个是我们模型上的法线(Normal),一个是我们的光照的方向(Light Direction),模型法线是CPU准备好的,我们再我们之前的结构体里添加一个float3代表我们的法线叫他normal,关联语义就是NORMAL,当然另外一个结构体也需要我们起名为normalWS
struct Attributes
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
float3 normal:NORMAL;
};
struct Varyings
{
float4 positionCS:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWS:NORMAL;
};然后我们的函数就要加上法线的转化,因为法线的转化不像顶点比较好了解,因为用矩阵转化法线会因为模型的缩放进行一个拉伸,这些有一些细节得处理,所以我们这里用到了Unity自带的一个函数也就是OUT.normal = TransformObjectToWorldNormal(IN.normal);进行转化。接下来就是去像素着色器里面进行计算,
Varyings Vertex(Attributes IN)
{
Varyings OUT;
OUT.positionCS = TransformObjectToHClip(IN.vertex.xyz);
OUT.normalWS = TransformObjectToWorldNormal(IN.normal);
OUT.uv = IN.uv;
//float4 positionWS = mul(UNITY_MATRIX_M,float4(vertex.xyz,1.0));
//float4 positionCS = mul(UNITY_MATRIX_VP,float4(positionWS.xyz,1.0));
return OUT;
}Lambert
Lambert光照模型比较简单,其实就是我们的法线跟灯光方向进行一个点乘,点乘的函数就是我们的dot(),点乘的时候我们的法线需要一个归一化,这里我们用到normalize(这个跟我们之前一篇文章人物移动输入的归一化差不多),其实TransformObjectToWorldNormal(IN.normal)中已经有normalize。所以我们这里//然后灯光就是我们Unity中传过来的_MainLightPosition.xyz,它其实就代表我们灯光的朝向,点成后他是一个-1到1的值,我们还得让他的最小值是0,然后就把它和我们之前的color相乘。当然还得乘上灯光的颜色_MainLightColor.rgb
half4 Pixel(Varyings IN):SV_TARGET
{
half4 color;
//float3 normalWS = normalize(IN.normalWS);
float NoL = max(0, dot(IN.normalWS,_MainLightPosition.xyz));
color.rgb = tex2D(_BaseMap,IN.uv).rgb * _Color * NoL * _MainLightColor.rgb;
color.a = 1.0;
return color;
}这样就有了光照效果Lambert

当然处理光照效果不止于这个太阳光,我们还有环境光,这里我们为它加上我们的环境光,我们使用球谐函数(Spherical Harmonics),要用这个函数要include一个新的文件#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl",然后到我们的像素着色器函数中添加用到SampleSH的一个方法,传进去一个法线IN.normalWS,SampleSH的返回值是一个half3,然后乘一个系数让它弱一点,看代码,这里两个光照之间用的是+
half4 Pixel(Varyings IN):SV_TARGET
{
half4 color;
//float3 normalWS = normalize(IN.normalWS);
float NoL = max(0, dot(IN.normalWS,_MainLightPosition.xyz));
half3 gi = SampleSH(IN.normalWS)*0.02;
color.rgb = tex2D(_BaseMap,IN.uv).rgb * _Color * NoL * _MainLightColor.rgb + gi;
color.a = 1.0;
return color;
}当然这里其实可以把灯光位置和颜色变成一个结构体Light,用一个函数获取它GetMainLight()
half4 Pixel(Varyings IN):SV_TARGET
{
half4 color;
Light light = GetMainLight();
//float3 normalWS = normalize(IN.normalWS);
float NoL = max(0, dot(IN.normalWS,light.direction));
half3 gi = SampleSH(IN.normalWS)*0.02;
color.rgb = tex2D(_BaseMap,IN.uv).rgb * _Color * NoL * light.color + gi;
color.a = 1.0;
return color;
} Phong
Phong模型比Lambert多了个高光,第一个要知道灯光的反射向量,用一个reflect()的函数来计算,然后参数就是我们的我们的灯光,法线。
float3 reflDir = reflect(light.direction,IN.normalWS);我们的观察方向。那我们的观察方向就是我们的摄像机了WorldSpaceCameraPosition.xyz减去时间空间的位置,就等于模型朝向我们的方向,可以看下图A-C=B,B是朝向我们的
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - IN.positionWS);
然后将他们点乘float spec = max(0,dot(reflDir,viewDir));然后我们用pow()来调节一下强度,我们来看Phong光照的最终代码
Shader "MyFirstShader"
{
Properties
{
_Color("Color",color) = (1,1,0,1)
_BaseMap("Base Map",2D) = "white"{}
_Shininess("Shininess",float) = 32;
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
half4 _Color;
sampler2D _BaseMap;
half _Shininess;
#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;
};
struct Varyings
{
float4 positionCS:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWS:NORMAL;
float3 position:TEXCOORD1
};
Varyings Vertex(Attributes IN)
{
Varyings OUT;
OUT.positionCS = TransformObjectToHClip(IN.vertex.xyz);
OUT.normalWS = TransformObjectToWorldNormal(IN.normal);
OUT.uv = IN.uv;
OUT.positionWS = mul(UNITY_MATRIX_M,float4(IN.vertex.xyz,1.0));
//float4 positionCS = mul(UNITY_MATRIX_VP,float4(positionWS.xyz,1.0));
return OUT;
}
half4 Pixel(Varyings IN):SV_TARGET
{
half4 color;
Light light = GetMainLight();
//float3 normalWS = normalize(IN.normalWS);
float NoL = max(0, dot(IN.normalWS,light.direction));
half3 gi = SampleSH(IN.normalWS)*0.02;
float3 viewDir = -normalize(_WorldSpaceCameraPos.xyz - IN.positionWS);
float3 reflDir = reflect(light.direction,normalize(IN.normalWS));
float spec = max(0,dot(reflDir,viewDir));
spec = pow(spec,_Shininess);
color.rgb = tex2D(_BaseMap,IN.uv).rgb * _Color * NoL * light.color + gi + spec;
color.a = 1.0;
return color;
}
ENDHLSL
}
}
}
Blinn-Phong
Blinn-Phong和Phong再高光上有一些细小的变化,它没有用到我们的反射函数reflect()。Blinn-Phong用我们的view跟我们的light的朝向进行一个相加,然后归一化变成一个半角向量float3 hVec = normalize(viewDir + light.direction),然后用hVec跟我们的normalWS来点乘float spec = max(0,dot(normalize(IN.normalWS),hVec));
总结
这个只是最简单的入门Shader,通过这个一章,可以理解基础的shader,同样的,这个也是我的一个学习结果,有什么不足希望大佬们提出来。
边栏推荐
- My rich second generation friend
- How to solve the pain points of 12000 small and medium-sized customers' component procurement? Say goodbye to overtime!
- Learn how Baidu PaddlePaddle easydl realizes automatic animal recognition in aquarium
- Gazebo control example
- Dart 代码注释和文档编写规范
- Opengauss active / standby architecture works with keeplive
- Support Wu xiongang! Arm Chinese management sent an open letter: shocked and angry at the unwarranted allegations!
- Spreadsheet export excel table
- Cross domain requests in nodejs
- qt 设置图标
猜你喜欢

Codeforces summer training weekly (7.14~7.20)

Summary of common shortcut keys in idea

BSP video tutorial issue 21: easy one key implementation of serial port DMA variable length transceiver, support bare metal and RTOS, including MDK and IAR, which is more convenient than stm32cubemx (

画刷和画笔

还在用WIFI你就OUT了:LI-FI更牛!!!

8000 word explanation of OBSA principle and application practice

“你“想当测试/开发程序员吗?努力发芽的我们......

How to calculate the profit and loss of spot Silver

Baidu PaddlePaddle easydl: when AI enters the factory, "small bearing" can also turn "big industry"

Qlib教程——基于源码(二)本地数据保存与加载
随机推荐
Flutter 通话界面UI
Knowledge of two-dimensional array
Oxygen temperature and humidity module
Leetcode 2347. the best poker hand
S-RPN: Sampling-balanced region proposal network for small crop pest detection
Three basic teaching
Dart 代码注释和文档编写规范
Token is used in nodejs
Lua快速上手
Count the number of given strings in a string
软件测试面试题:你们的性能测试需求哪里来?
字节月薪28K,分享一波我的自动化测试经验....
华为“天才少年”稚晖君又出新作,从零开始造“客制化”智能键盘
【样式集合1】tab 栏
Adding custom dynamic arts and Sciences to cesium
Matlab 44 animation gradient drawing programs
[C language] file operation
梳理 SQL 性能优化,收藏经典!
JUC concurrent programming learning
Centralized management of clusters