种种原因鸽了很久…已经学到第九章了第六章的笔记还没写…那就当对初级篇的一个总结吧,防止篇幅太长,分3部分来总结~
对于光照模型的讲解原书已经讲得非常详细了,这里就随便记一下咯…
感觉这章对于萌新来说最重要的是从框架开始跟着敲,熟悉一下流程和常用函数、变量、语义
没啥难度,但是基础很重要,这里熟悉了,后面代码感觉眼睛已经可以跳着看了emm
总结开始!
标准光照模型
在标准光照模型中,进入摄像机中的光有4种:自发光、环境光、漫反射、高光反射
1. 自发光Emissive
一般实时渲染中,自发光的物体只会给自己加上这个光照,而不会影响周边的物体(即:它不是光源)
直接使用该材质的自发光颜色emissive即可
2. 环境光Ambient
通常是一个全局变量
3. 漫反射Diffuse
漫反射的光线随机方向反射,所以默认它与观察方向无关,根据Lambert定律可得:
c d i f f u s e c_{diffuse} cdiffuse = ( c l i g h t ⋅ m d i f f u s e ) (c_{light} · m_{diffuse}) (clight⋅mdiffuse) max ( 0 , n n o r m a l ⋅ l l i g h t ) (0, n_{normal} · l_{light}) (0,nnormal⋅llight)
Half-Lambert
由于Lambert公式中,经过max函数与0取最大值后,会让原本小于0的结果(-1,0)全部等于0(失去了差别),导致暗部的颜色缺乏变化
Half-Lambert将对这一部分进行了改进:
c d i f f u s e c_{diffuse} cdiffuse = ( c l i g h t ⋅ m d i f f u s e ) (c_{light} · m_{diffuse}) (clight⋅mdiffuse) ( α ( n n o r m a l ⋅ l l i g h t ) + β ) (α(n_{normal} · l_{light}) + β) (α(nnormal⋅llight)+β)
绝大多数情况下,α和β会取0.5,将 n n o r m a l ⋅ l l i g h t n_{normal} · l_{light} nnormal⋅llight的值从[-1,1]映射到[-0.5,0.5],再加上0.5,总体值域变成了[0,1]这样就不会出现不同的点积映射到同一个值0上了
注:Half-Lambert没有任何物理依据
4. 高光反射Specular
只是一种经验模型
根据Phong模型,高光反射的计算方法为:
c s p e c u l a r c_{specular} cspecular = ( c l i g h t ⋅ m s p e c u l a r ) (c_{light} · m_{specular}) (clight⋅mspecular) max ( 0 , v v i e w ⋅ r r e f l e c t ) m g l o s s (0, v_{view} · r_{reflect})^{m_{gloss}} (0,vview⋅rreflect)mgloss
Blinn-Phong模型
使用 n n o r m a l ⋅ h h a l f n_{normal} · h_{half} nnormal⋅hhalf来替代 v v i e w ⋅ r r e f l e c t v_{view} · r_{reflect} vview⋅rreflect,其中 h h a l f h_{half} hhalf可以通过 v v i e w + l l i g h t v_{view} + l_{light} vview+llight得到
实际渲染中使用Blinn-Phong的情况较多
拼一个基础光照shader
平平无奇的基础光照shader
写一个最普通的shader,只要把上面提到的4中光加起来输出就可以了
写的时候可以从Frag的return开始,一步一步逆推补充,缺啥补啥,举例:
新建一个UnlitShader后,直接删个痛快
// 这里定义shader的名字,以及材质面板中显示的它的文件目录
Shader "Unlit/NewUnlitShader" {
// 这里定义那些需要暴露在Inspector中的变量(可以更方便的调整变量值)
Properties {
}
// 每个shader可以有多个SubShader,但最终只会执行一个
// Unity会按顺序根据设备的情况决定执行哪个SubShader
SubShader {
// SubShader的Tag对它的所有Pass生效,若想只对一个Pass生效,可以在Pass里写Tag
Tags {
"RenderType"="Opaque" }
// 每个SubShader可以有多个Pass,Pass们按顺序依次执行
// 每个Pass都是对输入的整个模型进行一次计算
Pass {
// CG代码开始的地方
CGPROGRAM
// 声明vertex和fragment着色器的名字是vert和frag(可自定义)
#pragma vertex vert
#pragma fragment frag
// 将要使用的库
#include "UnityCG.cginc"
// 顶点着色器的输入
struct a2v {
float4 vertex : POSITION;
};
// 顶点着色器的输出、片元着色器的输入
struct v2f {
float4 pos : SV_POSITION;
};
// 顶点着色器
v2f vert(a2v v) {
v2f o;
// 转换到裁剪空间,顶点着色器必做任务
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
// 片元着色器
fixed4 frag(v2f i) : SV_Target {
return col;
}
// CG代码结束
ENDCG
}
}
}
Properties变量补充
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
Pass内属性声明
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
Tag、include修改
Tags {
"LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
接着开始从片元着色器倒推
fixed4 frag(v2f i) : SV_Target {
// ambient, diffuse, specular是fixed3,补充一个1.0作为透明度
return fixed4(ambient + diffuse + specular, 1.0);
}
根据公式计算ambient,diffuse,specular
fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
缺啥补啥*1 —— 片元着色器(方向记得归一化)
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
注意:
这里的_WorldSpaceLightPos0
并不单纯是字面意义的“光源在世界空间的位置”,更准确的说它是光源在世界空间的位置或方向向量
。
当_WorldSpaceCameraPos.w值为0
时,代表该光源为平行光
,此时_WorldSpaceCameraPos.xyz为该光源的方向
;
当_WorldSpaceCameraPos.w值为1
时,代表该光源为点光源
或聚光灯
,此时_WorldSpaceCameraPos.xyz为该光源的位置
更多细节在第九章的笔记中
缺啥补啥*2 —— 片元着色器输入 / 顶点着色器输出结构体
struct v2f {
float4 pos : SV_POSITION;
// 补充内容
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
缺啥补啥*3 —— 顶点着色器
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 补充内容
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
缺啥补啥*4 —— 顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
最后补充上一个FallBack "Specular"就完成了
Shader "Unity Shaders Book/Chapter 6/Specular Pixel-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags {
"LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
以上是在片元着色器中计算光照的shader,着色单位是像素
要是想在顶点着色器中计算光照(着色单位是顶点),只需要在顶点着色器中计算好光照,片元着色器直接输出就可以了,因此倒推的时候从顶点着色器开始就好了
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = ambient + diffuse + specular;
return o;
}
easy!
使用Half - Lambert和Blinn - Phong的基础光照shader
Half - Lambert
c d i f f u s e c_{diffuse} cdiffuse = ( c l i g h t ⋅ m d i f f u s e ) (c_{light} · m_{diffuse}) (clight⋅mdiffuse) ( α ( n n o r m a l ⋅ l l i g h t ) + β ) (α(n_{normal} · l_{light}) + β) (α(nnormal⋅llight)+β)
修改diffuse的计算公式即可
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
这里的halfLambert可以理解为一个对uv的映射参数,乘以它就可以将uv从原来的[-1,1]映射到[0,1]
Blinn - Phong
使用 n n o r m a l ⋅ h h a l f n_{normal} · h_{half} nnormal⋅hhalf来替代 v v i e w ⋅ r r e f l e c t v_{view} · r_{reflect} vview⋅rreflect,其中 h h a l f h_{half} hhalf可以通过 v v i e w + l l i g h t v_{view} + l_{light} vview+llight得到
// 计算viewDir的方法二选一
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
以上两个模型的特点在前面已经提过了,就不再叨叨了
学习总结
总的来说这一章是在了解基本光照模型,以及习惯在shader lab中写shader(熟悉并理解一些变量的类型、unity shader的结构、unity常用内置变量和函数、unity一些简单内置函数的内部实现、顶点片元着色器的任务分配等)
从第一节开始跟着敲shader,学完最后一节再自己回过头重新敲一遍,一步一步慢慢来,主要是为了后面能够在大量代码里迅速看出这里是对哪种成分做了什么样的修改(比如将漫反射的颜色与纹理进行叠加相乘、将高光反射进行遮罩处理等)
本章(包括七、八章也是)前面的shader很多可以直接调用Unity内置函数的地方都没有调用它们,而是直接写出了它们的内部实现(比如矩阵变换、uv坐标的till和offset等),对于刚刚学习完第四章矩阵的萌新来说是一个的巩固矩阵知识的机会
目前养成的写shader的习惯有点像倒推法,写好properties和属性声明、tag等常规代码后,先在片元着色器写了输出的颜色成分,再向上分别写出各个成分的计算方法,遇到没有的变量,可以计算就直接计算,缺少数据就到顶点着色器里输出,然后补上a2v和v2f的结构体
所以其实就是,片元着色器缺啥就回顶点着色器补啥,顶点着色器随时等待补充新的输出…