Raymarching(光线步进)

  • 概念
    • 和光追的不同
  • 光线步进
    • 屏幕射线插值方式重建
    • SDF(signed distance functions)
    • 步进过程
    • 深度
    • 阴影
    • AO
    • 反射
    • 结尾

前言:在做项目的时候遇到了要使用光线步进实现特定效果,就简单的学习了一下,如果有写的不对的地方请多指教。

参考:

深度相关以及屏幕射线插值重建:blog.csdn.net/puppet_master/article/details/77489948.

光线步进理解:
blog.csdn.net/qq_38275140/article/details/91049023
https://www.jianshu.com/p/46e161b911dd

SDF:
https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm

视频讲解光线步进:
https://www.youtube.com/watch?v=oPnft4z9iJs&list=PL3POsQzaCw53iK_EhOYR39h1J9Lvg-m-g

概念

光线步进和光线追踪类似,逆向追踪的思想,基于后处理,从屏幕发射射线,然后求射线和物体的交点。通过SDF判断是否到达物体表面以及得到每次前进步长。

和光追的不同

光线追踪:
1、视角方向发射射线
2、一次性算出射线与场景最近物体交点
3、获取交点材质颜色
4、若材质包含反射或折射,则改变射线方向
5、寻找下一个交点,即重复2(直到找不到交点或达到最大追踪次数)。

光线步进:
1、视角方向发射射线
2、一步步(SDF返回值控制前进距离级判定是否到达物体表面)的前进,不断的向交点趋近(受到最大步长以及最大步进次数限制)
3、计算交点的材质颜色
4、若材质包含反射或折射,则改变射线方向
5、步进寻找下一个交点,即重复2(直到达到光线限定最大距离或达到最大迭代次数)。

ray marching的独到之处:
为什么要有Raymarching
利用Ray Marching渲染软阴影、虚焦、漫反射、环境光遮蔽等比传统光线跟踪更为方便快捷,在实时渲染的情况下可以获得不错的图像质量。
目前,Ray Marching的应用主要局限于volume field的渲染上:例如在医学图像处理中,将CT机上获得的若干断层照片渲染成三维模型;或是渲染云彩、火焰、烟雾等效果。Ray Marching的机理使得我们可以对volume field做采样,以离散值的叠加逼近一团连续体积场的外观。

光线步进

屏幕射线插值方式重建

首先我们要得到,摄像机(这里使用的是scene摄像机)在每个像素的射线发出方向。

平面的图

上图中,A为相机位置,G为空间中我们要重建的一点,那么该点的世界坐标为A(worldPos) + 向量AG,我们要做的就是求得向量AG即可。根据三角形相似的原理,三角形AGH相似于三角形AFC,则得到AH / AC = AG /
AF。由于三角形相似就是比例关系,所以我们可以把AH /AC看做01区间的比值,那么AC就相当于远裁剪面距离,即为1,AH就是我们深度图采样后变换到01区间的深度值,即Linear01Depth的结果d。那么,AG= AF * d。所以下一步就是求AF,即求出相机到屏幕空间每个像素点对应的射线方向。看到上面的立体图,其实我们可以根据相机的各种参数,求得视锥体对应四个边界射线的值,这个操作在vertex阶段进行,由于我们的后处理实际上就是渲染了一个Quad,上下左右四个顶点,把这个射线传递给pixel阶段时,就会自动进行插值计算,也就是说在顶点阶段的方向值到pixel阶段就变成了逐像素的射线方向。

那么我们要求的其实就相当于AB这条向量的值,以上下平面为例,三维向量只比二维多一个维度,我们已知远裁剪面距离F,相机的三个方向(相机transform.forward,.right,.up),AB = AC+ CB,|BC| = tan(0.5fov) * |AC|,|AC|= Far,AC = transorm.forward * Far,CB = transform.up * tan(0.5fov) * Far。

因为只求方向就可,所以在下面的代码中令 far = 1;

 //返回一个矩阵,分别表示四个点的向量,在shader里插值后可以得到各像素点的方向
private Matrix4x4 CamFrustum(Camera cam)
{Matrix4x4 frustum = Matrix4x4.identity;float fov = Mathf.Tan((cam.fieldOfView*0.5f)*Mathf.Deg2Rad);Vector3 goUp = Vector3.up * fov;Vector3 goRight = Vector3.right * fov * cam.aspect;Vector3 TL = (-Vector3.forward - goRight + goUp);Vector3 TR = (-Vector3.forward + goRight + goUp);Vector3 BR = (-Vector3.forward + goRight - goUp);Vector3 BL = (-Vector3.forward - goRight - goUp);//顺序为左上,右上,右下,左下。frustum.SetRow(0, TL);frustum.SetRow(1, TR);frustum.SetRow(2, BR);frustum.SetRow(3, BL);return frustum;
}

传入shader,在顶点函数中计算好,4个顶点对应的向量,通过插值我们就可以在片元函数中得到每个像素的方向了。

使用GL把shader的输出渲染在一个Quad上面,这里就需要对齐(通过GL.Vertex3的z与视锥体行号对应来存储每个顶点对应的index值)

        RenderTexture.active = destination;_raymarchMaterial.SetTexture("_MainTex", source);// Graphics.Blit(source, destination, _raymarchMaterial);GL.PushMatrix();GL.LoadOrtho();_raymarchMaterial.SetPass(0);GL.Begin(GL.QUADS);//BLGL.MultiTexCoord2(0, 0.0f, 0.0f);GL.Vertex3(0.0f, 0.0f, 3.0f);//BRGL.MultiTexCoord2(0, 1.0f, 0.0f);GL.Vertex3(1.0f, 0.0f, 2.0f);//TRGL.MultiTexCoord2(0, 1.0f, 1.0f);GL.Vertex3(1.0f, 1.0f, 1.0f);//TLGL.MultiTexCoord2(0, 0.0f, 1.0f);GL.Vertex3(0.0f, 1.0f, 0.0f);GL.End();GL.PopMatrix();}

不使用GL也是可以的,将GL注释掉,在GL.PushMatrix()上面加上Graphics.Blit(source, destination, _raymarchMaterial);,
顶点着色器中也可以使用int index=(int)dot(v.uv,float2(1,2));
或用x、y来判断对应视锥体的哪个角。)

           v2f vert (appdata v){v2f o;half index = v.vertex.z;v.vertex.z = 0;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.uv;         o.ray = _CamFrustum[(int)index].xyz;/*int index =0;    //这样写也行if (v.vertex.x < 0.5 && v.vertex.y > 0.5)index = 0;else if (v.vertex.x > 0.5 && v.vertex.y > 0.5)index = 1;else if(v.vertex.x > 0.5 && v.vertex.y < 0.5)index = 2;else if(v.vertex.x < 0.5 && v.vertex.y < 0.5)index =3;v.vertex.z = 0;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.uv; o.ray = _CamFrustum[index].xyz;*/o.ray /= abs(o.ray.z);o.ray = mul(_CamToWorld,o.ray);return o;}

SDF(signed distance functions)

首先来介绍一下距离场(distance field):

距离场是三维空间中一个标量场,任意一点的数值代表离该点最近的模型表面的点的距离下界。通过距离场可以很方便地估计光线应当前进的幅度:只需让光线前进当前点距离场大小的数值即可,即所谓Sphere Tracing。

我们可以看个最简单的球例子:

传入的参数 p代表光线的“头”现在所在的位置,_sphere1.xyz代表球心的位置,w代表球的半径。

用向量的长度 – 球的半径得到 光线该点距离球表面的最近距离。

当距离场由多个物体构成,会返回union,取小值,依然是得到离模型最近的距离值。

SDF(signed distance functions)有很多写好的,也包括bool运算,平滑并交差集等,可以直接用,https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm

步进过程

搬运一张图:

在上图可以看到,每次前进的都是最新的点到距离场的最短距离,因为这个最短距离没有小于我们设定的判断阈值(判断光线到了物体表面),故一直走,走到最右边正方体表面停下。

fixed4 raymarching(float3 ro,float3 rd,float depth)  //ro 射线初始位置,rd射线前进方向,depth在深度图采样得到
{fixed4 result = fixed4(1,1,1,1);const int max_iteration = _MaxIterations;  //规定光线最多走几步float t = 0;        //光线走的长度for(int i = 0; i<max_iteration;i++){if(t>_maxDistance || t>= depth)  //最大边界 || 有物体遮挡{result = fixed4(rd,0);break;}float3 p =ro +rd*t;              //当前位置, rayOrigin + rayDirection*tfloat d = distanceField(p);if(d<_Accuracy) //当走到物体表面时,_Accuracy为阈值,越小越精确,一般0.1 - 0.001之间。{float3 nor = getNormal(p);   float s = Shading(p,nor);result = fixed4(_mainColor.rgb*s,1);break;}t+=d;//没走到表面,加上距离场返回的距离值,走下一步。}return result;
}

深度

在视空间,每个顶点的原始的Z值是视空间的深度,但是经过透视投影变换以及透视投影,转化到屏幕空间后,需要保证在屏幕空间的深度与1/z成正比才可以在屏幕空间逐像素地进行插值进而获得屏幕上任意一点像素的屏幕空间深度值,简单来说,这个转化的过程主要是为了从顶点数据获得屏幕空间任意一点的逐像素数据。而得到屏幕空间深度之后,我们要使用时,经过变换的这个屏幕空间的东西,又不是很直观,最直观的还是视空间的深度,所以我们要进行一步变换,把屏幕空间的深度再转换回原始的视空间深度。


这个深度是float,为什么还要乘i.ray向量的长度呢?

图中问号dist,为点到摄像机的欧式距离,是我们要求的,求出来后能在步进过程中 和射线所走长度比较来判断是否存在遮挡。

Near是近裁剪面的深度,这里为方便看做了1。
|TL|为摄像机到近裁剪面某一点的距离,即length(i.ray)。

阴影

硬阴影:朝光线方向再来一次光线步进,如果撞到了物体就说明光线被该物体挡住了,自身位于阴影中。

软阴影:考虑当阴影射线没有击中任何物体,但是非常接近的时候会发生什么,阴影下的半影。离击中物体的距离越近(h),让它的颜色越深;距离阴影的点越近(离阴影越近说明t,射线所走的距离越大),也就越暗。


注意result的初始值为1,代表没有阴影。

如图所示,t表示射线沿着光线方向(向上的)走的路程,而h是每走一步和物体的距离,简单地为前进过程中的每一个步长计算一个半影因子h/t,然后取所有半影中最暗的那个。k值是阴影的软化程度,在这里我们也可以知晓其实它就是加速h/t趋向于1,值越大,k*h/t就会越快的趋近1,阴影也就越锐利。

除以sinSun 是为了消除k=1时,整个地面都变暗的情况(远处虽然不受阴影影响,但min(k*h/t)为步进刚开始时的最小值,即光线与地面夹角的sin值。)

AO

特别的地方就在于是步进过程中是固定步长,不像之前每次加上到物体的最短距离。

float AmbientOcclusion(float3 p ,float3 n){float step = _AoStepsize;                    //每次前进的步长,这里使用固定步长float ao = 0.0;float dist;for(int i=1;i<=_AoIterations;i++){dist = step*i;                                                         //如果附近没有其他物体 dist<DistanceField 最终结果为负数并截为0 返回值=1 即无环境光遮蔽//如果附近有物体,那么法线步进就会靠近该物体 从而dist>DistanceField 结果大于0,返回值<1ao += max(0.0,(dist - distanceField(p + n*dist).w)/dist);}return(1.0 - ao*_AoIntensity);}

反射

对于场景中的物体:

场景物体的反射我们利用反射探针来完成,因为是imageEffect,所以内置的unity_SpecCube0无法正常配置,我们需要手动把光照探针的贴图传进去。

因为我的场景只有个天空盒,就直接传入的天空的cubemap。

对于距离场物体:

在开始计算距离场反射之前我们先让距离场物体能够有自己的颜色。

思路很简单,我们让距离场函数返回float4类型,xyz存储颜色,w存储距离

要改动的地方较多,首先所有调用DistanceField地方取值都要加上.w ,还有距离场的各个操作函数也要适应float4类型。

并对raymarching函数传入参数,返回值进行修改,使其内部不在调用shading,只是单纯的来判断有没有到表面。

bool raymarching(float3 ro,float3 rd,float depth,float maxDistance,int maxIterations,inout float3 p,inout fixed3 dColor)  //ro 射线初始位置,rd射线前进方向,depth在深度图采样得到{                                                                                                    //maxIterations规定光线最多走几步                                                  float t = 0;        //光线走的长度                                                               //inout 类似引用,保存碰撞点的位置、颜色信息 bool hit;for(int i = 0; i<maxIterations;i++){if(t>_maxDistance || t>= depth)  //最大边界 || 有物体遮挡{hit = false;break;}p =ro +rd*t;              //当前位置, rayOrigin + rayDirection*tfloat4 d = distanceField(p);if(d.w <_Accuracy)                  //当走到物体表面时,_Accuracy为阈值,越小越精确,一般0.1 - 0.001之间。{dColor = d.rgb;hit = true;break;}t += d.w;                           //没走到表面,加上距离场返回的距离值,走下一步。}return hit;}

反射也是根据反射方向进行光线步进。

结尾

主要是根据各位大佬的总结加入了一些个人的理解,确实挺有意思的。


工程放在了这里(跟着视频做的):
https://github.com/ZhanYang-Feng/Raymarching.git

Raymarching小尝试相关推荐

  1. SubstanceDesigner制作PBR材质制作并且同步到Unity小尝试

    SubstanceDesigner制作PBR材质制作并且同步到Unity小尝试 1.下载安装SubstanceDesigner,网址:https://zixue.3d66.com/softhtml/d ...

  2. Hack radio【小尝试总结】

    前言: 希望在这个网站慢慢记录自己学习过的东西,做过的东西,回看应该会明白更多. 我去年实习短暂接触了HackRF,做了这个小尝试,目标也很简单,就是实现一个对固定码的监听及发送功能.这篇帖子更多是从 ...

  3. 层 数据仓库_小尝试:基于指标体系的数据仓库搭建和数据可视化

    关于作者:小姬,某知名互联网公司产品专家,对数据采集.生产.加工有所了解,期望多和大家交流数据知识,以数据作为提出好问题的基础,挖掘商业价值. 0x00 前言 我将整理文章分享数据工作中的经验,因为业 ...

  4. 小尝试:基于指标体系的数据仓库搭建和数据可视化

    我将整理文章分享数据工作中的经验,因为业务内容上的差异,可能导致大家的理解不一致,无法体会到场景中的诸多特殊性,不过相信不断的沟通和交流,可以解决很多问题.前面我们分析了职场基本功.数据指标体系,今天 ...

  5. 提高外卖单量-整理思考小尝试(大花猫冯夏)

    (一).问别人 1.平台-付费 -- 2.微信个人-付费 - 3.微信公众号等平台-免费 - (二).自己学 一.现存问题分析 1.数据分析-现存问题和优化点 分析店铺存在的数据,通过商家后台数据显示 ...

  6. 揭秘手机空间不足的小尝试

    自从拿到了华为Mate 7的手机之后,大屏确实给我带来了很多不一样的使用体验,特色功能是双卡双待,确认让我省心不少,但是一直让我纠结的就是这手机存储空间的问题.本来存储空间是12G,到现在空间剩余总是 ...

  7. readelf小尝试

    代码如下: #include <stdio.h>/* run this program using the console pauser or add your own getch, sy ...

  8. Flutter 28: 图解 ListView/GridView 混用时滑动冲突小尝试

    小菜在学习过程中会在一个 Page 页面同时用到 GridView 和 ListView 或多个 ListView,此时就会遇到常见的滑动冲突问题.小菜尝试了两种解决滑动冲突的方案,仅记录一下基本的使 ...

  9. SDL小尝试,是男人就坚持20秒

    今天在电脑里掏出来自己以前试着使用SDL游戏引擎的时候做的一个简单版 是男人就坚持20秒的小游戏.. 玩家通过键盘WSAD操作人物躲避四面八方来的物体,看最终能坚持多长时间. 图片是随便在网上找的或者 ...

最新文章

  1. 数字图像处理实验(2):PROJECT 02-02, Reducing the Number of Gray Levels in an Image
  2. 用原生JavaScript实现淡入淡出轮播图
  3. selenium java 断言_Java+Selenium+Testng自动化测试学习(三)— 断言
  4. 西北大学计算机转专业,西北大学可以转专业吗,西北大学新生转专业政策
  5. 【PyTorch】PixelShuffle
  6. 长短期记忆网络_科研成果快报第181期:改进的长短期记忆网络用于长江上游干支流径流预测...
  7. amd cpu不能在cmd环境下运行java代码_Golang安装与环境搭建并在VSCode里面输出HelloWord...
  8. LM75 --温度采集(时序及代码)
  9. UE4 Text Render 中文字体制作
  10. Android 10.0 SystemUI状态栏屏蔽掉通知栏不显示通知
  11. ih5学习笔记_事件对象
  12. Java 移位、逻辑运算符详解(~史上最全|吹牛逼)
  13. 照片拼图制作怎么弄?这几个方法或许能帮到你
  14. 微服务架构设计总结实践篇,10 步搭建微服务
  15. 最简单的幻灯片制作,分分钟完成高逼格成片
  16. TLS协议分析 (九) 现代加密通信协议设计
  17. 凯利讯半导体滤波器的种类与作用
  18. HTML5的字体样式设置方法
  19. 宝元系统服务器不亮,我的服务器总是自动重启?
  20. 某信服EDR任意用户登陆漏洞分析

热门文章

  1. 排列的生成(二) —— 序数法
  2. 顺序表中将奇数排在偶数前面。
  3. 恒指怎么开户?恒指交易原则?
  4. [Mac OS]ASUS z97-K R2.0 + GTX960 + Clover v2.4k r4098 Install Sierra 10.12.5 安装过程中遇到的问题及解决方案
  5. 海思3516系列芯片SPI速率慢问题深入分析与优化(基于PL022 SPI 控制器)
  6. sedona-技术框架
  7. 计算机二级小蒋在教务处负责学生成绩,小蒋是一位中学教师,在教务处负责初一年级学生的成绩管理。由于学校地处偏远地区,缺乏必要的教学_考题宝...
  8. [ Office 365 开发系列 ] Graph Service
  9. PHP SDK for sinaweibo
  10. uni-app 使用uCharts图表插件