撰写了文章 更新于 2019-02-27 17:16:26
Ray Marching a Sphere
这篇文章记录了如何使用 Ray Marching 方法渲染一个球体, 让我们开始吧!
整体过程
- 为了方便观察,我们需要先设置摄像机
- 然后每个像素沿着相机方向发出射线,检测物体
- 最后设置一个光源,给球体打光,并添加阴影
开始渲染一个地面和球体
想象在一个3D空间,我们有一个摄像机,摄像机前面有个球体。还记得小孔成像原理吧,照相机相当于一个小孔,我们把摄像机范围内的大场景,照进一个小小的平面里面,这个平面相当于我们的显示器。不同的是,小孔成像是在异侧成像,而我们是在同侧成像(这样计算很方便),并且没有反转。
一个非常简单的相机设置
vec3 ro = vec3(0., 1., 0.); //摄像机原点 vec3 rd = normalize(vec3(uv.x, uv.y, 1.)); //光线方向,z正轴是往屏幕里面
步进过程
输入:原点和方向
输出:到物体的距离
#define MAX_STEPS 100 #define MAX_DIST 100. //到表面的阈值 #define SURF_DIST .01 float RayMarching(vec3 ro, vec3 rd){ float step = 0.; for(int i=0; i <MAX_STEPS; ++i){ vec3 p = ro + rd*step; //当前步进的位置 float r = GetDist(p); //与场景中最近物体的距离 step+= r; //进行步进 if(step>MAX_DIST || r < SURF_DIST)break; } return step; }
Ray Marching 有时也叫 Sphere Tracing, 因为每次都是检测球形内的物体。
获取距离
输入:空间中的某一点
输出:此点与空间中最近物体的距离
float GetDist(vec3 p){ vec4 sphere = vec4(0, 1, 6, 1); //球体的数据,xyz是位置,w是半径 float sphereDist = length(p-sphere.xyz) - sphere.w;//当前步进位置到球表面的距离 float planeDist = p.y; //相机到平面的距离 return min(sphereDist, planeDist); //获取最小的距离(也就是最近的物体) }
main代码
void mainImage(out vec4 fragColor, in vec2 fragCoord){ vec2 uv = (fragCoord -.5*iResolution.xy) / iResolution.y; vec3 col = vec3(0.); //设置摄像机 vec3 ro = vec3(0., 1., 0.); vec3 rd = normalize(vec3(uv.x, uv.y, 1.)); float d = RayMarching(ro, rd); d/=6.; //由于数值大过 1 的时屏幕显示白色,所以要缩小到0~1之间 col = vec3(d); fragColor = vec4(col, 1.0); }
到此为止,我们已经完成了一个非常简单的Ray Marching。以上代码可以直接使用shasdertoy运行,也可以稍加改动运行在OpenGL或Unity3d中。
灵魂画手上线
以下为原理图,底下的黑线是地平面, 红点表示在此处获取最近物体距离(也就是RayMarching里面的一次循环), 黄色圈表示圈外已经有物体(所以圈内没有物体), 绿色表示黄色圈的半径(也就是离最近物体的距离)。
步进过程
效果图
添加灯光和阴影
光源 添加一个光源,我们需要知道光源的位置,物体表面的法向量
输入:表面顶点
输出:经过光照的值
float GetLight(vec3 p){ vec3 lightPos = vec3(0, 5, 6); //定义光源位置 vec3 l = normalize(lightPos - p); //光到表面的标准向量 vec3 n = GetNormal(p); //获取表面法向量 float dif = clamp(dot(n, l),0,1); //法向量与光向量点乘,背面的阴影会变成负数,要限制 return dif; }
获取法线
输入:表面顶点
输出:一个模拟的法向量分布
vec3 GetNormal(vec3 p){ float d = GetDist(p); vec2 e = vec2(.01, 0); //我们自己算一个法向量,结果如下图 vec3 n = d - vec3( GetDist(p - e.xyy), GetDist(p - e.yxy), GetDist(p - e.yyx) ); return normalize(n); }
阴影
阴影一定是投射在物体上面的,我们有了物体的表面位置,只要和光源进行Ray Marching计算,所以阴影也出奇地简单。
此段代码放在GetLight()的合适位置
float d = RayMarching(p+n*SURF_DIST, l); //这里要注意之前的p点已经是Ray Marching的断点,所以要加上一个向量值
if(d < length(lightPos - p))dif *= .1;
更改后的主程序
void mainImage(out vec4 fragColor, in vec2 fragCoord){
vec2 uv = (fragCoord -.5*iResolution.xy) / iResolution.y;
vec3 col = vec3(0.);
//设置摄像机
vec3 ro = vec3(0., 1., 0.);
vec3 rd = normalize(vec3(uv.x, uv.y, 1.));
float d = RayMarching(ro, rd);
vec3 p = ro + rd * d; //获取表面顶点
float dif = GetLight(p);
col = vec3(dif);
fragColor = vec4(col, 1.0);
}
效果图就是标题图了。
下篇预告
下一篇文章可能是关于云朵渲染吧,这东西要花点时间。