文章目录
自学Raymarching汇总: Unity Shader - Ray Marching Study Summary - 学习汇总
搜索了一下资料:软阴影
也可以参考我之后翻译的一篇:penumbra shadows in raymarched SDFs - 光线步进中使用有向距离场实现软阴影
总结为下图
图中的:
- umbra 英 [ˈʌmbrə] 美 [ˈʌmbrə] ,本影 ==> 影子中光源完全照射不到的部分
- penumbra 英 [pəˈnʌmbrə] 美 [pəˈnʌmbrə] 半影 ==> 黑暗与光明之间的
这是比较理想、真实的软阴影的方式。
但是我们这里Raymarching的是另一种模拟方式:
如下图:
核心代码:
float getShadow(float3 p, float3 p2l, float distWithL) { // 获取当前碰撞点是否在阴影中,返回光衰减值
float3 ori = p; // 射线起点
float3 dir = p2l; // 射线方向
float3 pos; // 当前步进到的位置
float dist; // 当前步进到的最近距离
float d; // 当前最近距离
pos = p + dir * EPSILON; // 先将起点偏移一点点,防止本身在几何体表面距离太小而认为是在阴影
UNITY_LOOP
float minD = distWithL;
for (int it = 0; it < MAX_STEP_TIMES; it++) {
d = sceneDF(pos); // 获取场景中所有几何体当中最近距离的
dist += d; // 调整当前步进到的最近距离
pos = ori + dir * dist; // 调整当前的步进位置
minD = min(dist, minD);
if (d < EPSILON_SHADOW) { // 有碰撞,阴影的碰撞检测距离小一些,精度高一些
return dist < distWithL - EPSILON ? 0 : 1; // 碰撞点距离小于起点与灯管的距离
}
if (dist > distWithL) {
return 1;
}
}
return 1;
}
float getSoftShadow(float3 p, float3 p2l, float distWithL) { // 获取软阴影
float ret = 1.0;
float dist = EPSILON; // 累计步进距离
#if _SOFT_SHADOW_IMPROVE
float last_d = 1e10; // 初始的上次距离设置大一些,这样y的第一次的值就可以接近于0
#endif
for (int it = 0; it < MAX_STEP_TIMES; it++) {
float d = sceneDF(p + p2l * dist);
#if _SOFT_SHADOW_1
// ret = min(ret, 10.0 * d / dist); // 10.0 就是我们的灯光大小
ret = min(ret, LIGHT_SIZE(_LightSize) * d / dist);
#else // _SOFT_SHADOW_IMPROVE
float y = d * d / (2.0 * last_d);
float l = sqrt(d * d - y * y);
// ret = min(ret, 10.0 * l / max(0.0, dist - y));
ret = min(ret, LIGHT_SIZE(_LightSize) * l / max(0.0, dist - y));
last_d = d;
#endif
if (ret < EPSILON_SHADOW || dist > distWithL) {
break;
}
dist += d;
}
return saturate(ret);
}
fixed4 getColor(v2f i) {
float3 ori = i.ray; // 射线起点
float3 dir = normalize(i.ray); // 射线方向
float3 pos; // 当前步进到的位置
float dist; // 当前步进到的最近距离
float d; // 当前最近距离
float far = _ProjectionParams.z; // far
ori += _WorldSpaceCameraPos.xyz; // 偏移,加上相机位置
pos = ori; // 从起点出发
float3 lightPos = _LightInfo.xyz; // 灯光位置
float lightRange = _LightInfo.w; // 灯光范围
float rangeEnable = step(EPSILON, lightRange);
UNITY_LOOP
for (int it = 0; it < MAX_STEP_TIMES; it++) {
d = sceneDF(pos); // 获取当前几何体集合中最近的距离
dist += d; // 调整当前步进到的最近距离
pos = ori + dir * dist; // 调整当前的步进位置
if (d < EPSILON) { // 有碰撞
float3 p2l = lightPos - pos; // 碰撞点指向光源向量
float distWithL = length(p2l); // 与灯光距离
float atten = rangeEnable * (1 - clamp(distWithL * lightRange, 0, 1)); // 灯光衰减
float3 l = normalize(p2l); // 灯光方向
float3 n = getNormal(pos); // 法线方向
float NdotL = max(0, dot(n, l)); // 漫反射,diffuse
#if _HP2L
#if _HARD_SHADOW
float shadow = getShadow(pos, l, distWithL); // 获取从碰撞点到光源点的阴影光衰减值
#else
float shadow = getSoftShadow(pos, l, distWithL); // 软阴影
#endif
#else // _L2HP
// _L2HP ,Light to HitPos 只支持Hard,因为软阴影算法是从HitPos 到 Light的步进计算
// #if _HARD_SHADOW
float shadow = getShadow(lightPos, -l, distWithL); // 获取从光源点到碰撞点的阴影光衰减值
// #else
// float shadow = getSoftShadow(lightPos, -l, distWithL); // 软阴影
// #endif
#endif
return NdotL * atten * shadow;
}
if (dist > far) {
return 0;
}
}
return 0;
}
注意Soft只能用:HP2L(HitPos to LightPos)的方式,因为从表面碰撞点为起点,到光源位置,如果过程中里
Differences in Hard and Soft - 硬软阴影区别
Soft Shadow - 软阴影
根据 Sebastian Aaltonen 在2018 GDC 上发布过未改进的初版实现: 第 38
页
Aaltonen_Sebastian_GPU_Based_Clay.pdf 或是
百度网盘Aaltonen_Sebastian_GPU_Based_Clay.pdf 提取码: d1w8
ret = min(ret, 10.0 * d / dist); // 10.0 就是我们的灯光大小
可参考:http://www.iquilezles.org/www/articles/rmshadows/rmshadows.htm
软阴影的球体追踪
- 软半影的宽度阴影
- 沿着光射线步进的SDF,模拟圆锥体最大覆盖值
- 演示场景中,圆锥体覆盖值大概为1:
c = min(c, light_size * SDF(P) / time)
,其中c
就是coverage
简写,P
是当前步进到的位置点,再次SDF(P)
意思就是再次对当前点求场景最近距离场,time
是累计步进的距离,
从上面的公式可以看出来,如果light_size
如果也大,那么light_size * SDF(P) / time
就越容易到达1
值,那么c
的值也就越容易为1
,但这个效果往往与我们现实生活相反,不信你自己在房间开启你的光管,光管灯光大小是比较大的,所以阴影越是软,越是平滑,如果你把光管关了,用自己手机开启灯光,你会发现阴影的边界一本是比较锐利的,至少比光管的锐利多了。所以你可以看到本博文最后的代码中,用了一个宏:#define LIGHT_SIZE(v) (303-v) // 获取灯光大小
,就是反过来的值。
但这些都是模拟的实现,真实的光照中软阴影是影响参数有:
- Surface碰撞点指向Light Occluder碰撞点的最近距离
- Light Occluder碰撞点指向Light的距离
- Light 的大小
这些参数是我自己用手机灯光拉远拉近时观察的结果
下面我用Excel来采样一条射线步进结果:
上面LightSize=10
的shadow
值0.091324201
,可以看到h
没有一次是碰撞到表面的,但也有阴影值(不为1)
,这就是软阴影。
下面我们将LightSize=100
可以看到h
没有小于0.01
的,都是1
的shadow
值,意思就是不在阴影中,所以结果就是很锐利的。
Soft Shadow Improvements - 软阴影改进
根据 Sebastian Aaltonen 在2018 GDC 上发布的改进软阴影带条, 第 39
页
Aaltonen_Sebastian_GPU_Based_Clay.pdf 或是
百度网盘Aaltonen_Sebastian_GPU_Based_Clay.pdf 提取码: d1w8
float y = d * d / (2.0 * lastDist);
float l = sqrt(d * d - y * y);
// ret = min(ret, 10.0 * l / max(0.0, dist - y));
ret = min(ret, LIGHT_SIZE(_LightSize) * l / max(0.0, dist - y));
相比之前的:
ret = min(ret, LIGHT_SIZE(_LightSize) * d / dist); // 10.0 就是我们的灯光大小
多了,d
改成l
了,dist
改成 max(0.0, dist - y)
其中,l与y,可以查看这里的说明
运行效果 - 改进后的带条没那么严重了
下面的GIF因为上传后压缩后导致图像质量太低,出现了带条。
Add controlling Light Size - 添加用于控制的灯光大小
Shader
float _LightSize; // 点光源半径大小:lightSize
...
#define LIGHT_SIZE(v) (303-v) // 获取灯光大小
...
float getSoftShadow(float3 p, float3 p2l, float distWithL) { // 获取软阴影
float ret = 1.0;
float dist = EPSILON;
#if _SOFT_SHADOW_IMPROVE
float lastDist = 1e10; // 初始的上次距离设置大一些,这样y的第一次的值就可以接近于0
#endif
for (int it = 0; it < MAX_STEP_TIMES; it++) {
float d = sceneDF(p + p2l * dist);
#if _SOFT_SHADOW_1
// ret = min(ret, 10.0 * d / dist); // 10.0 就是我们的灯光大小
ret = min(ret, LIGHT_SIZE(_LightSize) * d / dist);
#else // _SOFT_SHADOW_IMPROVE
float y = d * d / (2.0 * lastDist);
float l = sqrt(d * d - y * y);
// ret = min(ret, 10.0 * l / max(0.0, dist - y));
ret = min(ret, LIGHT_SIZE(_LightSize) * l / max(0.0, dist - y));
lastDist = d;
#endif
if (ret < EPSILON_SHADOW || dist > distWithL) {
break;
}
dist += d;
}
return saturate(ret);
}
...
Project
- backup : T6_SoftShadow
GGB
Excel
总结
由于时间关系,先占坑,后面补充说明