有号距离场(SDF)是指建立一个空间场,这个空间场中每个像素或体素记录自己与周围像素或体素的距离,如果在物体内,则距离位负,如果是在外面则记录相差的距离,在边上刚好位0
在2D上的演示如下
原图:
SDF
简而言之他是一个记录空间距离的信息。
SDF能做什么? 1.高清文字(TMP) 2.形变动画 3.序列帧动画柔和过度 4.碰撞检测 5.软阴影 6.环境遮蔽(AO) TMP:用过TMP的人可能会有印象,他是一个友好距离文本,他本身的图片信息记录了他的距离场。
GPU是通过插值的方式来确定中间点的信息
对于位图来说,每个像素点是RGB颜色,插值周边的值没有实际意义。(数学上面的混合)。
但对于SDF来说,每个像素点是边界的有向距离,插值后得到新增的点,SDF就是利用了插值器的特性实现了光滑放大效果。
这一步只需要记录好有号距离场的图片,然后在shader中做插值就可以了。
举个例子:
fixed4 sdf = tex2D(_SDFTextTexture, i.uv);
float distance = sdf.a;
col.a = smoothstep(_DistanceMark - _SmoothDelta, _DistanceMark + _SmoothDelta, distance);
col.rgb = lerp(fixed3(0,0,0), fixed3(1,1,1), col.a);
形变动画:
这是由两个有号距离场数据组成的
然后在两个距离场纹理的采样(记得线性空间,不要进行纹理压缩)中进行简单的Lerp操作。
序列帧动画柔和过度:他的实现原理是在有号距离场的序列帧中,多采样一帧,然后在两帧之间进行时间上的过度。
这个的实现过程可以详细说一下:
首先建立有号距离场:for (int i = -half; i < half; i++)
{
for (int j = -half; j < half; j++)
{
for (int k = -half; k < half; k++)
{
bool isContinue = false;
for (int q = 0; q < SDFCollider.Length; q++)
{
Vector3 origin = new Vector3(i, j, k);
Vector3Int vector = new Vector3Int(Mathf.RoundToInt(i * floatToIntNum), Mathf.RoundToInt(j * floatToIntNum), Mathf.RoundToInt(k * floatToIntNum));
Vector3 dir = (SDFCollider[q].transform.position - origin).normalized;
Ray ray = new Ray(origin, dir);
if (Physics.Raycast(ray, 1f))
{
SubSDF(new Vector3(i, j, k), SDFCollider);
isContinue = true;
continue;
}
AddSDFData(vector, ray);
}
if (isContinue)
{
continue;
}
}
}
}
这里的做法是在-half到half之间,根据SDFCollider所有collider的中心为方向,发射射线检测是否有撞到对象表面,如果没有则记录距离。(注意这里ijk是用int来执行增加的,也就相当于说我这里是用1为一个体素来步进的)。
如果有撞到表面,则表示这里有碰撞体,需要细化碰撞体,我这里是在SubSDF中继续检测更细的距离场。
private void SubSDF(Vector3 startVector, Transform[] trans)
{
float interval = 0.1f;
for (float i = startVector.x-1; i -0.5)
{
float3 pos = ro + t * rd;//摄像机方向步进
float3 nor = calcNormal(pos);
// material
float3 mate = float3(0.3,0.3,0.3);
// key light
//float3 lig = normalize(float3(-0.1, 0.3, 0.6));
float3 hal = normalize(_Light - rd);
//漫反射以及他的阴影,阴影用步进得到光线方向上的下一个投影到的点就是阴影点,然后用3次方的衰减让他成为软阴影
float dif = clamp(dot(nor, _Light), 0.0, 1.0) *
calcSoftshadow(pos, _Light, 0.01, 3.0, tech);//摄像机方向步进的点然后看光照方向的阴影
//高光
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0)*
dif *
(0.04 + 0.96*pow(clamp(1.0 + dot(hal, rd), 0.0, 1.0), 5.0));
//颜色为材质本身颜色跟漫反射以及软阴影结合
col = mate * 4.0*dif*float3(1.00, 0.70, 0.5);
//再加上高光
col += 12.0*spe*float3(1.00, 0.70, 0.5);
// ambient light
float occ = calcAO(pos, nor);
float amb = clamp(0.5 + 0.5*nor.y, 0.0, 1.0);
col += mate * amb*occ*float3(0.0, 0.08, 0.1);
// fog
col *= exp(-0.0005*t*t*t);
}
return col;
}
float3x3 setCamera(in float3 ro, in float3 ta, float cr)
{
float3 cw = normalize(ta - ro);//摄像机外一点与摄像机得到的方向,这个就是其中一个轴
float3 cp = float3(sin(cr), cos(cr), 0.0);//摄像机旋转角度
float3 cu = normalize(cross(cw, cp));//叉乘后得到摄像机一个轴
float3 cv = normalize(cross(cu, cw));//通过另一个叉乘得到另一个轴。
return float3x3(cu, cv, cw);//摄像机的坐标(xyz轴)
}
void mainImage(out float4 fragColor, in float2 fragCoord, in float2 iResolution)
{
// camera
float iTime = _Time.y;
float an = 12.0 - sin(0.1*iTime);
float3 ro = float3(3.0*cos(0.1*an), 1, -3.0*sin(0.1*an));//摄像机位置
float3 ta = float3(0.0, 2, 0.0);//外面的一点,决定摄像机的坐标系
// camera-to-world transformation
float3x3 ca = setCamera(ro, ta, 0.0);
int tech = (frac(iTime / 2.0) > 0.5) ? 1 : 0;
float3 tot = float3(0,0,0);
#if AA>1
for (int m = 0; m < AA; m++)
for (int n = 0; n < AA; n++)
{
// pixel coordinates
float2 o = float2(float(m), float(n)) / float(AA) - 0.5;
float2 p = (-iResolution.xy + 2.0*(fragCoord + o)) / iResolution.y;
#else
float2 p = (-iResolution.xy + 2.0*fragCoord) / iResolution.y;//(-iResolution.xy + 2.0*fragCoord)是把相机设置到-1到1,然后除iResolution.y相当于乘以一个屏幕大小
#endif
// ray direction
float3 rd = mul(ca, normalize(float3(p.xy, 2.0)));//屏幕空间的坐标转到摄像机空间
// render
float3 col = render(ro, rd, tech);
// gamma
col = pow(col, 0.4545);//pow(col, float3(0.4545));
tot += col;
#if AA>1
}
tot /= float(AA*AA);
#endif
fragColor = float4(tot, 1.0);
}
环境遮蔽(AO)
什么是环境遮蔽(AO)
左边的效果是没有ao的,可以看到他的立体感是比较差的,一些应该光照难以照到的位置都是亮的。而右边戴上AO后,他的间接光效果就比较立体了。AO也是全局光照中很重要的一个组成模块。市面上有很多ao的做法,比如贴图ao,ssao,hbao,gtao,dfao等。
而其中dfao就是集成在UE中的基于有号距离场的ao效果。
dfao的优点是:Dfao通过距离场产生比一般实时屏幕空间ao更加精确的ao效果,
能提供环境光镜面反射的遮挡效果,能提高画面实时效果又不产生太大消耗。
缺点:
Dfao不适合太过精细的物体投影,因为构建dfao大多数时候是物体的一个拟合;
遮蔽效果有时候会覆盖物的光泽;不适合pc以外的主机;会与其他透明效果产生
冲突等等。
UE用暴力建场的方法,就是直接迭代32*32*32个点,然后在每个点上向周围发射一大堆光线,然后保存交到的最小距离,作为距离场在这个点上的值。
在三维空间中,他是一个根据xyz三个轴来分割所有物体的一个树状结构,他的优势是可以快速找到近邻的物体,他的搜索方式是首先找到树种临近的分叉中的物体,然后再以要找的对象和到这个物体的位置为半径搜索这个半径内的物体是否有更近的。这是因为最近的物体有可能并不在自己这个树的这边,而是在另一边。所以需要这个半径范围再找一遍。
以上就是了解到和自己也实践的一些关于有号距离场的知识,如有问题欢迎指出。