一、烘培系统
关于最基础的渲染算法和光照理论在前面的两个系列博客中讲过:光栅化实时渲染Opengl实时渲染器(一)架构搭建 – PULUO 光线追踪离线渲染Optix光线追踪渲染器(一)硬件光追原理与Optix入门 – PULUO,所以光栅化管线和光追算法的知识这里就先不写了,只补一些Unity特有的系统和概念。
首先一般的实时渲染引擎里都具备两大类光:实时光和烘培光,
实时光:动态计算每一盏光对于场景中每一个顶点和片元的着色影响。优点:场景的物体可动态;缺点:开销太大,容易引起掉帧;同时只能计算一次弹射。
烘培光:把光线经过多次反射(光追)的效果计算在一张场景光照贴图(LightMapping)上,这张帖图作用于场景中所有物体的表面。优点:效果更好、没有实时计算的开销;缺点:对于实时运动的光源和物体不可用。
Unity一共提供了Enlighten、Progressive Lightmap两种全局光照GI系统(Enlighten已被废弃,就不提了)。这两种系统的内容比较相像,基本都提供了两种GI方案,一个是离线的烘培光,一个是实时的预计算光。Progressive Lightmap的界面如下所示:
大部分选项都顾名思义,勾选stitch seams可以让烘培结果更平滑;放大Lightmap Resolution和Padding可以让烘培精度更高
烘培示例:
这里可以看到烘培出来的LightMap的样子:
(面试题里经常会考uv是什么,这里就补一嘴。将一个三维模型沿着表面剪开,平展成一个二维平面图,此时三维模型上的每一个点都能在该平面图上找到一个二维坐标,即uv坐标。)
Post-Processing后处理组件可以在烘培的基础上创建,添加bloom、SSR、SSAO等组件:
刚才提到了,除了烘培系统以外,还有一种预计算实时GI(Precomputed Real-time GI)的算法,奈何我的Unity版本太低用不了这个,所以暂时搁置。。。
二、光照探针
烘培系统有一个最大的问题是:要求目标物体完全不能动。如果光照固定,但是目标物体会活动的话 就不能烘培了。此时就需要使用光照探针作为替代方案。
光照探针是一种使用球谐来存储周围环境光照信息的一种载体,当场景中的光源非常复杂的时候,可以拼排大量的光照探针对空间中每一个小区域的光照情况进行记载。当物体在探针组中穿梭时,其受光信息可以通过身边探针的光照信息插值得到。
摆放技巧:探针应该放在颜色变化较剧烈 或 明暗变化较剧烈的位置。
如果不在变化剧烈的位置增加探针的话,如下图,两个探针之间插值就会出问题,中间部分的黑暗部分会插不出来(因为中间缺少探针)
正确的做法是在中间较黑的区域补充一组探针。
三、反射探针
因为Unity的渲染流水线中 光线基本只弹射一次,因此所有的镜面反射都是无法实现的。所以可以沿用光照探针的思路,在场景中放置一个反射探针,向四周拍摄一张Cubemap出来 作为反射参考图。设置界面如下:
其中反射探针也分为烘培和实时计算的,烘培的只能捕捉静态的场景,实时的可以捕捉动态的场景但是开销巨大,几乎每帧需要重新拍摄一张Cubemap全景图。
在海平面的上空放置一个探针,这样下面的海洋就可以根据探针捕捉到上方的天空场景是什么样子的,就可以完成反射了:
当然,屏幕后处理插件Post-Processing有一个SSR的功能,也可以实现反射,而且不需要创建额外的Cubemap。不过SSR是基于屏幕空间的,也就是说屏幕内没有出现的物体是无法被反射出来的,因此和反射探针各有利弊。
四、前向渲染
前向渲染基本就是前面介绍的渲染管线的基本流程,特别强调在片元着色器中 直接对视锥内的所有片元进行逐光源计算,但是很多片元之间都是相互遮挡的,需要剔除,这就导致前向渲染出现大量的片元计算浪费,后续会有与之对应的延迟渲染管线来解决这个问题,这里先不提。
前向渲染的流程图如下:
光照的着色方法分为3大类:逐像素着色、逐顶点着色、球谐光照着色。其效果按顺序递减、开销也按顺序递减。
假设有ABCDEFGH这么多盏灯,考虑到前向渲染的多光源着色开销太大,Unity会做出对应的取舍。首先,对于每个像素 会对其中最亮的若干盏(或是设置成Important的光源,总数为Pixel Light Count)光照ABCD使用逐像素着色,然后次亮的几盏(最多4)光源EFG使用逐顶点着色,其余琐碎的小光源H使用球谐着色。遵从的理念就是:对影响最大的使用高开销高质量的着色算法,对影响小的用低开销低质量的着色算法。
事实上Unity将前向渲染分成了两部分依次进行渲染(就是拆分成2个Pass),对于第一个Pass叫做ForwardBase,在这个Pass中,只对一盏最最亮的平行光A进行逐像素着色,然后对所有逐顶点着色的灯EFG、球谐着色的灯H进行依次着色;后续的Pass叫做ForwardAdd,在这一系列Pass中,对剩余需要逐像素着色的灯BCD进行逐像素着色。
(注意:这里的ForwardAdd是渲染路径的概念,和所谓的Forward+渲染算法不是一个东西!!!Forward+是通过剔除光源来优化性能的一种前向渲染思维,在后续会提到)
五、Shader入门
基本可以从这篇blog入门:A gentle introduction to shaders in Unity - Shader tutorial (alanzucconi.com)
一个初级Shader的结构是这样的:先定义一个Shader名字(头),然后定义Properties区域和SubShader区域。Properties区域编写所有可以公开到面板上的参数,SubShader区域编写包含了一次流程中所有Pass的着色器定义。SubShader可以写多个,用于应付各种兼容性不同的显卡。
Properties中定义参数的方法是:_参数名("参数面板名称",参数类型) = 参数初值
SubShader中首先要定义渲染队列形式和渲染模式(是否半透),如果是半透 需要定义混合模式。然后将Properties的参数传递进来(即定义声明),定义应用阶段从CPU端传来的顶点数据结构体(以及从几何阶段传输给光栅化阶段的数据结构体),最后编写surface或vertex+fragment着色器。
Shader "Unlit/3_InitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_MyNormalMap("My normal map", 2D) = "bump" {} // Grey
_MyInt("My integer", Int) = 2
_MyFloat("My float", Float) = 1.5
_MyRange("My range", Range(0.0, 1.0)) = 0.5
_MyColor("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A)
_MyVector("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)
}
SubShader
{
Tags {
"Queue" = "Geometry"
"RenderType"="Opaque"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
其中Queue是渲染队列,Background (1000): 用于天空盒和背景,这部分应该最先绘制;Geometry (2000): 用于渲染不透明的几何体;Transparent (3000): 用于渲染半透明物体;Overlay (4000): 用于渲染后处理效果、UI或文字。
六、传统光照模型Shader
一些传统(非PBR)光照模型的shader实现可见博客BRDF光照模型与渲染方程 – PULUO,这里简单写一下各个模型的构成。先说一下通用形式,反射光的亮度应该等于自发光的亮度+反射光的亮度,其中反射光的亮度等于入射光的亮度乘BRDF乘cos<入射光,法线>;而任何光照模型的BRDF都可以写成漫反射与镜面反射的组合:
$$L(x→out)=L_e(x→out)+ f_r * L(in→x) * cos(in,N)$$
$$f_r=g()k_s+k_d$$
$$k_d=\frac{\rho_d}{\pi}$$
各个光照模型的区别主要体现在g()。
1、Lambert模型:没有高光项,所以g()=0。
float3 normalDirection = normalize(mul(float4(v.normal,0.0),unity_WorldToObject).xyz);//将模型空间的法线转到世界空间
float3 lightDirection;
lightDirection = normalize(_WorldSpaceLightPos0.xyz);//灯光方向
float3 diffuseReflection = _Lightness * _LightColor0.xyz * max(0.0, dot(normalDirection,lightDirection));//计算兰伯特漫反射
float3 lightFinal = diffuseReflection + UNITY_LIGHTMODEL_AMBIENT.xyz;//与环境光结合
o.col = float4(lightFinal * _Color.rgb,1.0);
o.pos = UnityObjectToClipPos(v.vertex);
return o;
2、Phong模型:
先放一张示意图:
$$\pmb R=2(\pmb N · \pmb I)\pmb N -\pmb I$$
$$g()=\frac{(\pmb R·\pmb V)^n}{\pmb N·\pmb {in}}$$
float3 L = normalize(_WorldSpaceLightPos0.xyz);
float3 N = normalize(mul(float4(i.normal, 0.0), unity_WorldToObject).xyz);
float3 viewDir = normalize(ObjSpaceViewDir(i.objPos));//计算出视线
float lightness = saturate(dot(L, N));
float3 reflection = normalize(2*dot(N,L)*N-L);
float spec = pow(max(0, dot(reflection, viewDir)), _Specular);
spec /= lightness;
float f = spec * _ks + _kd;
float3 col = f * lightness * _LightColor0.xyz + UNITY_LIGHTMODEL_AMBIENT.xyz;
3、Blinn-Phong模型:
$$g()=\frac{(\pmb N·\pmb H)^n}{\pmb N·\pmb {in}}$$
float3 L = normalize(_WorldSpaceLightPos0.xyz);
float3 N = normalize(mul(float4(i.normal, 0.0), unity_WorldToObject).xyz);
float3 viewDir = normalize(ObjSpaceViewDir(i.objPos));
float3 H = normalize(L+viewDir);
float lightness = saturate(dot(L, N));
float3 reflection = normalize(2*dot(N,L)*N-L);
float spec = pow(max(0, dot(N, H)), _Specular);
spec /= lightness;
float f = spec * _ks + _kd;
float3 col = f * lightness * _LightColor0.xyz + UNITY_LIGHTMODEL_AMBIENT.xyz;
4、微表面模型(pbr)
微表面模型理论和pbr模型已经在Opengl实时渲染器(五)PBR与IBL – PULUO有详述了,不多赘述。这里直接给公式:
$$g() = \frac{D*F*G}{4(\omega_o · n)(\omega_i · n)}$$
其中D是法线分布函数,G是几何遮蔽项,F是菲涅尔项:
$$D = \frac{\alpha^2}{\pi((n · h)^2(\alpha^2 - 1) + 1)^2}$$
$$G_{自阴影} = \frac{n · l}{(n · l)(1 - k) + k} * \frac{n · v}{(n · v)(1 - k) + k}$$
$$F = F_0 + (1 - F_0)(1 - (h · v))^5$$
Comments | NOTHING