1、Unity光源模型
1.1 两种渲染方式
前向渲染:每个物体都有可能被重复渲染,即基于物体的光源渲染;超出灯光设置数量被当作顶点光(6*6)
延迟渲染:基于光源的物体渲染;以灯光为单位,先计算灯光所需东西,如RT0为漫反射,RT1为金属度,RT2为法线图;对于多光源友好(6+6);
区别:延迟渲染需要MRT技术(multi render target)和空间容量大
1.2 前向渲染
Unity两种前向渲染管线
bulid in:跟上文提到的概念一样; 个pass;base仅一个方向光。
urp:一个pass,最多八盏灯;
1.3 Phong光源模型
高光反射:pow(RdotV,smoothness)×lightcolor;
漫反射:NdotL×lightcolor
若附加纹理,相乘即可。
1.4 法线贴图
unity中的切线a分量是为了兼容各平台差异
法线贴图需要解码,解码后调整隆起大小再转至TBN空间
反射为什么灯光要负方向?
因为unity内置的计算灯光方向是从模型向光源走
forwardbase的实现:
Shader "Unlit/phong2"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_smoothness("Smoothness",Range(0.01,100))=1.0
_specuIntensity("SpecuIntensity",Range(0.00,100))=1.0
_NormalTex ("NormalTex", 2D) = "white" {}
_normalIntensity("NormalIntensity",Range(0.00,5))=1.0
}
SubShader
{
LOD 100
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "AutoLight.cginc"
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 nor:NORMAL;
float4 tangent:TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 posW:TEXCOORD1;
float3 norW:TEXCOORD2;
float3 tangentW:TEXCOORD3;
float3 biNormal:TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _LightColor0;
float _smoothness;
float _specuIntensity;
float _normalIntensity;
sampler2D _NormalTex;
float4 _NormalTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.posW=mul(unity_ObjectToWorld,v.vertex);
o.norW=normalize(mul(float4(v.nor,0.0),unity_ObjectToWorld).xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.tangentW=normalize(mul(unity_ObjectToWorld,float4(v.tangent.xyz,0.0)).xyz)*v.tangent.w;
o.biNormal=normalize(cross(o.tangentW,o.norW));
return o;
}
half4 frag (v2f i) : SV_Target
{
half3 final;
//normal map
float4 normalmMap=tex2D(_NormalTex,i.uv);
float3 normal=UnpackNormal(normalmMap).xyz;
normal.xy=normal.xy*_normalIntensity;
float3 tangentW=normalize(i.tangentW);
float3 biNormal=normalize(i.biNormal);
float3 norW=normalize(i.norW);
float3x3 TBN=float3x3(tangentW,biNormal,norW);
normal=normalize(mul(normal,TBN)) ;
// ambient
fixed4 baseColor = tex2D(_MainTex, i.uv);
half3 ambient=baseColor.rgb*UNITY_LIGHTMODEL_AMBIENT.rgb;
//DIFFUSE
//float3 normalW=normalize(i.norW);
float3 L=normalize(UnityWorldSpaceLightDir(i.posW));
float NdotL=saturate(dot(L,normal));//
half3 diffuse=NdotL*baseColor.rgb*_LightColor0.xyz;
//SPECULAR
float3 R=reflect(-L,normal);//
float3 viewDirW=normalize(UnityWorldSpaceViewDir(i.posW));
float3 specu=pow(saturate(dot(R,viewDirW)),_smoothness)*_LightColor0.xyz*_specuIntensity;
final=diffuse.rgb+ambient.rgb+specu.rgb;
return half4(final,1.0);
}
ENDCG
}
}
}
forwardadd的实现:
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "AutoLight.cginc"
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 nor:NORMAL;
float4 tangent:TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 posW:TEXCOORD1;
float3 norW:TEXCOORD2;
float3 tangentW:TEXCOORD3;
float3 biNormal:TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _LightColor0;
float _smoothness;
float _specuIntensity;
float _normalIntensity;
sampler2D _NormalTex;
float4 _NormalTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.posW=mul(unity_ObjectToWorld,v.vertex);
o.norW=normalize(mul(float4(v.nor,0.0),unity_ObjectToWorld).xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.tangentW=normalize(mul(unity_ObjectToWorld,float4(v.tangent.xyz,0.0)).xyz)*v.tangent.w;
o.biNormal=normalize(cross(o.tangentW,o.norW));
return o;
}
half4 frag (v2f i) : SV_Target
{
half3 final;
//光源衰减
#if defined(DIRECTIONAL)
float3 L=normalize(UnityWorldSpaceLightDir(i.posW));
float attenuation=1.0;
#elif defined(POINT)
float3 L=normalize(_WorldSpaceLightPos0.xyz-i.posW);
float distance=length(_WorldSpaceLightPos0.xyz-i.posW);
float Range=1.0/unity_WorldToLight[0][0];
float attenuation=saturate(Range-distance/Range);
#endif
//光源衰减
//normal map
float4 normalmMap=tex2D(_NormalTex,i.uv);
float3 normal=UnpackNormal(normalmMap).xyz;
normal.xy=normal.xy*_normalIntensity;
float3 tangentW=normalize(i.tangentW);
float3 biNormal=normalize(i.biNormal);
float3 norW=normalize(i.norW);
float3x3 TBN=float3x3(tangentW,biNormal,norW);
normal=normalize(mul(normal,TBN)) ;
// ambient
fixed4 baseColor = tex2D(_MainTex, i.uv);
half3 ambient=baseColor.rgb*UNITY_LIGHTMODEL_AMBIENT.rgb;
//DIFFUSE
//float3 normalW=normalize(i.norW);
float NdotL=saturate(dot(L,normal));//
half3 diffuse=NdotL*baseColor.rgb*_LightColor0.xyz;
//SPECULAR
float3 R=reflect(-L,normal);//
float3 viewDirW=normalize(UnityWorldSpaceViewDir(i.posW));
float3 specu=pow(saturate(dot(R,viewDirW)),_smoothness)*_LightColor0.xyz*_specuIntensity;
final=(diffuse.rgb+specu.rgb)*attenuation;
return half4(final,1.0);
}
点光源衰减范围:r-d/r
bliphong,在phong模型基础上优化高光反射,减少消耗,(半角向量)h=l+v,NdotH
// phong
// float3 R=reflect(-L,normal);
// float RdotN=saturate(dot(R,norW));
// float3 specu=pow(RdotN,_smoothness)*_LightColor0.xyz*_specuIntensity;
//bliphong
float3 viewDirW=normalize(UnityWorldSpaceViewDir(i.posW));
float3 h=max(0.0,L+viewDirW);
float HdotN=saturate(dot(h,norW));
float3 specu=pow(HdotN,_smoothness)*_LightColor0.xyz*_specuIntensity;
BliPhong Phong
2、ACESFilm tone-mapping:
显示器显示范围为0-1,为LDR,而计算机渲染可以是任意数值。HDR→LDR过程造成失真,丢失许多细节。(HDR:高动态范围)
解决这个问题:HDR→LDR(tone-mapping)
tone-mapping(个人经验),pow(basecolor(diffuse2d),2.2)转换至线性空间,pow(tonecolor,1.0/2.2)转换至伽马空间。
ACESFilm not
全局环境光开启泛光?解决:globalAmbient×baseColor;
3、视差贴图(原理还是不熟悉):
置换贴图,模型表面曲面细分,高密度顶点,读取高度图,顶点偏移。深度图,其实是高度图的反相,白色代表凹陷
for (int i = 0; i < 10; i++)
{
half height = tex2D(_ParallaxMap, uv_parallax);
uv_parallax = uv_parallax+ (1.0 - height) * view_tangentspace.xy * _Parallax * 0.01f;
}
开启 未开启
4、shadowmap
- shadowmap
- screen space shadowmap
- 联级阴影(csm) cascaded shadowmapping:shadowmap基础上,视锥体分级
shadowmap就是一张深度图,
shadowmap实现:光源相机渲染深度信息,物体转换至光源相机空间,跟shadowmap里的深度值作比较,若片元深度值>深度值(shadowmap),光面。反之阴影面。
联级阴影以及屏幕空间下的阴影还需深入学习!
注意:光位置的w分量可以判断是否为平行光,0为平行光。1为点光。
5、间接光照
5.1基本概念
环境光≈间接光
三种技术实现:
- LightMap(预先烘焙好)
- 反射探针:捕抓环境信息→环境贴图,模拟漫反射
- 光照探针:预先收集环境和光源的漫反射信息,模拟漫反射;
实现原理:
- 环境贴图
- IBL基于图像的照明
- SH球谐光照
CubeMap采样全景图实现:
反射探针和CubeMap实现:
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float3 viewDir=UnityWorldSpaceViewDir(i.posW);
float3 normalW=normalize(i.normalW);
float3 ref=reflect(-viewDir,normalW);
//反射探针
float4 environColor=UNITY_SAMPLE_TEXCUBE(unity_SpecCube0,ref);
//为了移动端也可以获取到HDR的信息
float3 hdrColor=DecodeHDR(environColor,unity_SpecCube0_HDR);
//fixed4 col = texCUBE(_CubeMap, ref);//CubeMap
return float4(hdrColor,1.0);
}
6、IBL 基于图像的照明
ibl ,一个设置一行代码。
为什么是mipmap?
pow对比度,直接相乘亮度,lerp整体粗糙度,线性粗糙度
ibl镜面反射和漫反射
使用IBL要三线性过滤
IBL实现一般不需要大尺寸(压缩压缩再压缩,,,,)
IBL间接镜面反射光:
fixed4 frag (v2f i) : SV_Target
{
// 取rougness,算出mipmap的level
float roughness=tex2D(_RoughtMap,i.uv).r;
roughness=pow(roughness,_Contrastion)*_Brighteness;
roughness=lerp(_RoughnessMin,_RoughnessMax,roughness);
roughness = roughness * (1.7 - 0.7 * roughness);
float level=roughness*6.0;
//取法线,算反射
float3 normaldata= UnpackNormal(tex2D(_NormalMap,i.uv));
float3 normalW=normalize(i.normalW);
float3 tangent=normalize(i.tangent);
float3 binormal=normalize(i.binormal);
float3x3 TBN=float3x3(tangent,binormal,normalW);
float3 normal=normalize(mul(normaldata,TBN));
float3 viewDir=UnityWorldSpaceViewDir(i.posW);
float3 reflectDir=reflect(-viewDir,normal);
//ao
float aoMap=tex2D(_AOMap,i.uv).r;
float ao= lerp(1.0,aoMap,_AOAdjust);
//cubemap的ibl实现
float4 cubeMap=texCUBElod(_CubeMap,float4(reflectDir,level));
float3 envir=DecodeHDR(cubeMap,_CubeMap_HDR).rgb;
//cubemap的probe实现
//half4 color_cubemap = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, level);
//half3 envir = DecodeHDR(color_cubemap, unity_SpecCube0_HDR);//确保在移动端能拿到HDR信息
//最终颜色
float3 col=envir*_Tint.rgb*_Tint.rgb*ao* _Expose;
//tonemapping
col=pow(col,2.2);
col=ACES_Tonemapping(col);
col=pow(col,1/2.2);
return float4(col,1.0);
}
IBL间接漫反射光:
float4 uv_ibl = float4(normal_dir, mip_level);
half4 color_cubemap = texCUBElod(_CubeMap, uv_ibl);
half3 env_color = DecodeHDR(color_cubemap, _CubeMap_HDR);//确保在移动端能拿到HDR信息
7、球谐光照(SH)
sh球谐光照内置实现(light probe的probe)
在forwardbase实现;
造轮子实现:
half4 frag (v2f i) : SV_Target
{
half3 normal_dir = normalize(i.normal_world);
half3 normaldata = UnpackNormal(tex2D(_NormalMap,i.uv));
normaldata.xy = normaldata.xy* _NormalIntensity;
half3 tangent_dir = normalize(i.tangent_world);
half3 binormal_dir = normalize(i.binormal_world);
normal_dir = normalize(tangent_dir * normaldata.x
+ binormal_dir * normaldata.y + normal_dir * normaldata.z);
half ao = tex2D(_AOMap, i.uv).r;
ao = lerp(1.0,ao, _AOAdjust);
float4 normalForSH = float4(normal_dir, 1.0);
//SHEvalLinearL0L1
half3 x;
x.r = dot(custom_SHAr, normalForSH);
x.g = dot(custom_SHAg, normalForSH);
x.b = dot(custom_SHAb, normalForSH);
//SHEvalLinearL2
half3 x1, x2;
// 4 of the quadratic (L2) polynomials
half4 vB = normalForSH.xyzz * normalForSH.yzzx;
x1.r = dot(custom_SHBr, vB);
x1.g = dot(custom_SHBg, vB);
x1.b = dot(custom_SHBb, vB);
// Final (5th) quadratic (L2) polynomial
half vC = normalForSH.x*normalForSH.x - normalForSH.y*normalForSH.y;
x2 = custom_SHC.rgb * vC;
float3 sh = max(float3(0.0, 0.0, 0.0), (x + x1 + x2));
sh = pow(sh, 1.0 / 2.2);
half3 env_color = sh;
half3 final_color = env_color * ao * _Tint.rgb * _Expose;
return float4(final_color,1.0);
}
unity内置实现:
half4 frag (v2f i) : SV_Target
{
half3 normal_dir = normalize(i.normal_world);
half3 normaldata = UnpackNormal(tex2D(_NormalMap,i.uv));
normaldata.xy = normaldata.xy* _NormalIntensity;
half3 tangent_dir = normalize(i.tangent_world);
half3 binormal_dir = normalize(i.binormal_world);
normal_dir = normalize(tangent_dir * normaldata.x
+ binormal_dir * normaldata.y + normal_dir * normaldata.z);
half ao = tex2D(_AOMap, i.uv).r;
ao = lerp(1.0,ao, _AOAdjust);
half3 env_color = ShadeSH9(float4(normal_dir,1.0));
half3 final_color = env_color * ao * _Tint.rgb * _Expose;
return float4(final_color,1.0);
}
unity全局间接光:
- lighting-environment lighting 漫反射
- lighting-environment refection 镜面反射
lighting-mixed lighting-baked global illumination 烘焙多光源
8、玉石模拟
玉石光线(穿透感)+光滑质感(环境反射)
组成:漫反射+透射+环境光;
漫反射=phong漫反射+addcolor+竖直方向AO;
透射计算:VdotB(B为L扭曲而成),结合对比度、亮度,乘以模型厚度、basecolor(光源颜色影响世界,所以独立一个光源颜色)和平行光颜色;
环境光:cubemap结合菲涅尔;
forbase:
float4 frag(v2f i) : COLOR
{
//info
float3 diffuse_color = _DiffuseColor;
float3 normalDir = normalize(i.normalDir);
float3 viewDir = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
//diffuse
float diff_term = max(0.0, dot(normalDir, lightDir));
float3 diffuselight_color = diff_term * diffuse_color * _LightColor0.rgb;
//竖直方向的AO,上亮下暗
float sky_sphere = (dot(normalDir,float3(0,1,0)) + 1.0) * 0.5;
float3 sky_light = sky_sphere * diffuse_color;
float3 final_diffuse = diffuselight_color + sky_light * _Opacity + _AddColor.xyz;
//trans light
float3 back_dir = -normalize(lightDir + normalDir * _BasePassDistortion);
float VdotB = max(0.0, dot(viewDir, back_dir));
float backlight_term = max(0.0,pow(VdotB, _BasePassPower)) * _BasePassScale;
float thickness = 1.0 - tex2D(_ThicknessMap, i.uv).r;
float3 backlight = backlight_term * thickness *
_LightColor0.xyz * _BasePassColor.xyz;
//ENV
float3 reflectDir = reflect(-viewDir,normalDir);
half theta = _EnvRotate * UNITY_PI / 180.0f;
float2x2 m_rot = float2x2(cos(theta), -sin(theta), sin(theta),cos(theta));
float2 v_rot = mul(m_rot, reflectDir.xz);
reflectDir = half3(v_rot.x, reflectDir.y, v_rot.y);
float4 cubemap_color = texCUBE(_EnvMap,reflectDir);
half3 env_color = DecodeHDR(cubemap_color, _EnvMap_HDR);
float fresnel = 1.0 - saturate(dot(normalDir, viewDir));
fresnel = smoothstep(_FresnelMin, _FresnelMax, fresnel);
float3 final_env = env_color * _EnvIntensity * fresnel;
//combine
float3 combined_color = final_diffuse + final_env + backlight;
float3 final_color = combined_color;
return float4(final_color,1.0);
}
forwardadd:
float4 frag(v2f i) : COLOR
{
float3 diffuse_color = _DiffuseColor * _DiffuseColor;
float3 normalDir = normalize(i.normalDir);
float3 viewDir = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
float NdotV = saturate(dot(normalDir,viewDir));
//light info
float3 lightDir = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz,_WorldSpaceLightPos0.w));
float attenuation = LIGHT_ATTENUATION(i);
//trans light
float3 back_dir = -normalize(lightDir + normalDir * _AddPassDistortion);
float VdotB = max(0.0,dot(viewDir, back_dir));
float backlight_term = max(0.0, pow(VdotB, _AddPassPower)) * _AddPassScale;
float thickness = 1.0 - tex2D(_ThicknessMap, i.uv).r;
float3 backlight = backlight_term * thickness *
_LightColor0.xyz * _AddPassColor.xyz;
//combine
float3 final_color = backlight;
final_color = sqrt(final_color);
return float4(final_color,1.0);
}
9、移动端角色渲染
分析与问题:
- ue和unity的单位区别(米、厘米);
- x轴向的区别;
- 模型和骨骼动画的大量到处;
- Mc纹理,rg通道粗糙度、金属度;
- 角色脚部抖动,曲线优化和压缩问题;
细节:
- 皮肤区域高光弱,金属区域高光强,albedo贴图的使用原理其一
- 皮肤区域漫反射强,金属区域漫反射弱,albedo贴图的使用原理其二
- 粗糙度做文章,越粗糙,光滑度越低
- 阴影不在间接光中计算,仅在直接光中,因为间接光的作用在于提亮暗部
- 皮肤正常sh,其他乘以0.5sh
疑问:
- 间接光的漫反射为什么要限定为0.5到1的范围内?
解答:使用了半兰伯特模型,提亮暗部
2. 153/154的操作不是很能理解,间接光都要乘以这个?
解答:shininess x smoothness ≈ a(1-a)x+a²,a为smoothness,但是还是线性关系
3. 皮肤柔光的插值操作
公式总结:
- 直接光漫反射:diffterm x basecolor x atten x lightcolor
- 直接光镜面反射:specterm x speccolor x atten x lightcolor
- 间接光漫反射:sh x basecolor x halflambert
- 间接光镜面反射:ibl x expose x speccolor x halflambert
头发渲染:kk的各向异性高光渲染
技术原理:normal实现的高光仅仅是一点,我们需要在头发的横截面都有高光
公式:T、L、V三个成关系,LV成半角关系H,求sin(T,H);
细节:
- T根据normal进行偏移,高光即可实现偏移;
- 噪声图扰动各向异性方向,范围由0到1缩放到-1到1;
- N x noise
- half_lambert/NdotL ,防止背光区域也有高光
疑问:
1. 为什么各向异性高光只亮一处?
因为sin(T,H)的夹角为90°时才为1
2. 为什么成光盘一样的扇形?
因为顺着边缘变化,中间的偏移更大,两边渐变,越远渐变范围越大;而中心范围小是因为没有偏移
细节语法:
1. 宏定义:检查器隐藏
2. 宏开关:多重编译和shader特性,增加一个,代码量两倍