Raymarching SDFs
说明
功能实现
实现了有向距离场上的软阴影(Soft
Shadow) 、环境光遮蔽(AO) 、凹凸贴图(bump
mapping)
学习两个经典场景的写法
搭建了一个场景
项目来源
项目来源如下
作者使用两个三角形作为输入,不使用图片作为输入,仅使用 4K
资源就渲染出了如下的图片,属实让人震惊
使用有向距离场上的 ray marching 实现的
我也想学习一下有向距离场(SDF)在渲染中的作用,同时也想学习使用 SDF
构造一个自己的场景
概念
SDF
SDF
signed distance field
有向距离场
表示空间中的每一个点到物体表面的最小距离
Ray Marching
光线步进算法,用于和物体求交
对于某一个点,如果他到物体表面的距离(SDF 函数的值)为 \(R\) ,则光线前进 \(R\) 一定不会和物体相交
复现
原始作者给出了原始工程在 shadertoy
上的实现,首先我将其移植到 OpenGL 上,实现原始的效果
移植
shader toy
shader toy 上相当于一个简单的 fragment shader,有如下固定输入
1 2 3 4 5 6 7 8 9 10 uniform vec3 iResolution; uniform float iTime; uniform float iTimeDelta; uniform int iFrame; uniform float iChannelTime[4 ]; uniform vec3 iChannelResolution[4 ]; uniform vec4 iMouse; uniform samplerXX iChannel0..3 ; uniform vec4 iDate; uniform float iSampleRate;
1 2 3 void mainImage( out vec4 fragColor, in vec2 fragCoord ) { }
移植到 OpenGL
CPU 端程序
使用 shader 绘制两个三角形(也就是平面)
传入在 shadertoy 中使用的基本参数(被使用到的上面的参数)
vertex shader
1 2 3 4 5 6 #version 330 core layout (location = 0 ) in vec3 aPos;void main() { gl_Position = vec4 (aPos, 1.0 ); }
学习代码与重构
(1) 设置相机位置
在 fragment shader
中,我们获取到的是以以相机为观察中心的观察坐标系
通过矩阵(视点坐标系 \(\to\)
世界坐标系)将其变换回世界坐标系中,可以通过 camera
的三个坐标进行构造
是正交变换,而 OpenGL 恰好是列放置的
1 2 3 4 5 6 struct Camera { vec3 position; vec3 front; vec3 up; float right; };
(2) 生成光线
OpenGL 中 fragment shader 中的内置变量 gl_FragCoord
记录了当前像素的中心位置
偏移了 (0.5,0.5)
范围为 [0.5, w-0.5] * [0.5, h-0.5]
生成的光线用于 ray marching
算法,在函数
raycast()
中实现
生成两条另外的光线,x+1,y+1
两条光线,用于实现其他效果
(3) ray marching
raycast()
函数中,首先和整个场景的包围盒求交,计算出时间的上下界
[tmin, tmax]
map()
函数计算出 sdf
的值
主要步进算法 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 tmin = max (tb.x, tmin); tmax = min (tb.y, tmax); float t = tmin;for (int i = 0 ; i < 70 && t < tmax; i++) { vec2 h = map(ro + rd*t); if (abs (h.x) < (0.0001 *t)) { res = vec2 (t, h.y); break ; } t += h.x; }
map
中计算当前点的 sdf
的值
作者提供了很多原始的图元形式
具体的实现部分查看代码注释
返回一个二维向量 vec2 res
res.x
:sdf
的值
res.y
:一个随机数,用于生成物体的颜色
(4) 计算 normal
\[
n=\textbf{normalize}(\nabla{f(p)})
\]
\[
\dfrac{\mathrm{d}f(p)}{\mathrm{d}x}\approx\dfrac{f(p+(h,0,0))}{h}
\]
\[
\dfrac{\mathrm{d}f(p)}{\mathrm{d}x}\approx\dfrac{f(p+(h,0,0))-f(p-(h,0,0))}{2h}
\]
1 2 3 4 5 6 7 vec3 calcNormal( in vec3 p ) { const float eps = 0.0001 ; const vec2 h = vec2 (eps,0 ); return normalize ( vec3 (f(p+h.xyy) - f(p-h.xyy), f(p+h.yxy) - f(p-h.yxy), f(p+h.yyx) - f(p-h.yyx) ) ); }
1 2 3 4 5 6 7 8 vec3 calcNormal( in vec3 & p ) { const float h = 0.0001 ; const vec2 k = vec2 (1 ,-1 ); return normalize ( k.xyy*f( p + k.xyy*h ) + k.yyx*f( p + k.yyx*h ) + k.yxy*f( p + k.yxy*h ) + k.xxx*f( p + k.xxx*h ) ); }
推导
4 个点:\(k_i=\{1,-1,-1\},\{-1,-1,1\},\{-1,1,-1\},\{1,1,1\}\)
\(\sum_{i}k_i=\mathbf{0}\)
\(\ast\) 表示点对点乘法
\[
\begin{aligned}
m&=\sum_{i}k_i\ast f(p+k_ih)\\
&=\sum_{i}k_i\ast(f(p+k_ih)-f(p))\\
&=\sum_{i}k_i\ast(k_ih\ast\nabla{f(p))}\\
&=\sum_{i}4h(\nabla{f(p)})\\
\end{aligned}
\]
(5) 光照模型
材质:地面使用经过 filter
的棋盘格,其他使用不同的物体自带的随机数返回一种颜色
光照模型使用 Blinn-Phong
模型
天空盒、直接光源
(6) AO
思路
在着色点 p 的周围采样若干个点,计算它们的 sdf 值,和到 p
点的距离作比较
上面的粉红色 和黄色 的值相差越大,说明周围遮蔽越严重
\[
\begin{aligned}
ao
&=1-k\sum_{i=1}^{5}\dfrac{1}{2^i}(\textrm{pink}_i-\textrm{yellow}_i)\\
&=1-k\sum_{i=1}^{5}\dfrac{1}{2^i}(i\cdot\Delta-\mathrm{sdf}(p+n\cdot
i\cdot\Delta))\\
\end{aligned}
\]
(7) 软阴影
硬阴影
向着光源的方向步进,如果碰到物体则在阴影中,没有则不在阴影中
1 2 3 4 5 6 7 float t = mint;for (int i = ZERO; i < 24 ; i++) { float h = map( ro + rd*t ).x; if (h < 0.0001 ) return 0.0 ; t += h; } return 1.0 ;
软阴影
当向着光源的方向步进的时候没有碰到物体,但是离物体很近的时候,这时候对阴影也是有贡献的
两个观察,点 \(p\) 离物体很近
\(p\) 离着色点越近,则阴影越硬
\(p\) 离物体越近(sdf
值越小),则阴影越硬
1 2 3 4 5 6 7 8 9 10 11 const float k = 10.0 ; float res = 1.0 ;float t = mint;for (int i = ZERO; i < 24 ; i++) { float h = map( ro + rd*t ).x; if (h < 0.0001 ) return 0.0 ; res = min (res, k*h/t); t += h; } return res;
软阴影(改进)
我们在步进的时候可能错过产生最硬的阴影的点(例如图上的黄点 ),引入的问题就是会造成漏光现象 (light
leaking)
解决方案就是试图求出黄色的点对应的步进长度
红色圆的半径 \(r_1\) ,绿圆半径
\(r_2\) ,则实际上的到物体表面的距离(黄线的一半 )如下
\[
r_2\cos\theta=r_2\sqrt{1-\left(\dfrac{r_2/2}{r_1}\right)^2}
\]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 float k = 10.0 ; float res = 1.0 ;float t = mint;float ph = 1e20 ;for (int i = ZERO; i < 24 ; i++) { float h = map( ro + rd*t ).x; if (h < 0.0001 ) return 0.0 ; float y = h*h/(2.0 *ph); float d = sqrt (h*h - y*y); res = min (res, k*d/max (t - y, 0.0 )); ph = h; t += h; } return res;
其他问题,将阴影估计深了,例如下方黄线为物体包络线
问题不大,因为之后确实有这么深的阴影(第二个接触点 )
改进前后对比
其他问题:左下角的这些残影?
另一种实现
shadertoy 是这么写的
这种方式是没有残影的
就是没改进的软阴影,但是改进了 \(t,d\) 的函数
限制了步进的长度,从而减小漏光情况的发生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 float res = 1.0 ;float t = mint;for (int i = ZERO; i < 24 ; i++) { float h = map( ro + rd*t ).x; float s = clamp (8.0 *h/t, 0.0 , 1.0 ); res = min ( res, s ); t += h; if ( res < 0.037 || t > tmax ) break ; } res = res*res*(3.0 - 2.0 *res); return clamp ( res, 0.0 , 1.0 );
(8) 结果
monstor 场景实现
(1) 场景搭建
柱子
章鱼哥
(2) bump mapping
(3) 光源的雾状效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { vec3 v1 = lightPositionOffset + LIGHT_POSITION - camera.position; float lig_dist2 = dot (v1, v1); float dist = dot (v1 ,rd); float seg2 = lig_dist2 - dist*dist; #if 1 if ((dist > 0 && (dist < t || matID == 666 u))) { col += vec3 ( 1.0 , 0.95 , 0.90 )*0.75 *exp2 ( -64.0 *seg2 ); } #else #define R2 0.01 if ((dist > 0 && (dist < t || matID == 666 u)) && (seg2 < R2)) { return vec3 ( 1.0 , 0.95 , 0.90 ); } #endif }
(4) 结果展示
新场景搭建
参考资料
参考图
SDF 任务
https://weirdmachine.me/proj5-distancefields/
https://weirdmachine.me/proj5-distancefields/proposal/index.html
SDF 的一些视频教程