找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3322|回复: 1

【着色器】在shadertoy上随手写了个玻璃折射方块的渲染

[复制链接]
发表于 2018-7-8 21:17:38 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
www.shadertoy.com是一个非常不错的用于练习着色器编写的网站。它用webGL来进行着色器的编译和渲染,并且给你提供4个浮点格式的帧缓冲,你只要写Fragment Shader就可以了。

因为只有Fragment Shader的部分,如果你要渲染一些几何体的话,最常见的办法就是使用射线相交检测了(通常我们用“raycast”这个词来装X,其中ray是射线的意思,然而微软的D
X
SDK叫它intersect,即“相交”)。原理就是把每个像素看作从眼睛射出的视线,然后判断视线是否与特定位置的几何体的表面发生相交,是的话就按照这个几何体的材质和光照来判断它的颜色,最后输出。

事实上玻璃折射是很简单的实践(另一个更简单的是镜面反射),因为它不涉及到漫反射的情况,不需要处理光线散射的情况。你只需要计算每一根射线,然后根据它在玻璃里面经过的距离算出光线衰减量,最后当它从玻璃里出去后根据它的方向来决定颜色就行了。

totalrefr.png

如图所示,灰色是“视线”,绿色是折射进入玻璃的视线,红色是由于折射角度的关系而被全反射后的视线,蓝色则是折射出玻璃的视线。
在特定角度下,视线可能会在方块里面发生大量次数的折射。

refr.png

由于完全纯净无杂质的玻璃不存在,而且玻璃本身也不是完全透明、可以完全无损传导光线的。所以我们需要计算光线的衰减量,根据光线在玻璃内经过的总距离长度来判断衰减量的大小。我们给玻璃定义一个本体颜色,用于处理这种情况。光线在玻璃内经过的总距离越远,这根光线所代表的像素的颜色就越接近玻璃的本体颜色。

折射的计算很简单,在GLSL里面可以用reflect()来计算。但reflect()在遇到不得不全反射的情况下,并不会给你计算出反射向量,而是返回genType(0.0);。所以在这种情况下我们要自己去计算反射。

反射的计算也很简单,在GLSL里面用reflect()即可,而且没有全反射的情况。

稍微有点麻烦的是如何做射线到立方体的相交的计算,以及在立方体内部的射线源如何与立方体的内面进行相交的计算。

我以前用的办法是把立方体的六个面做成六个几何平面(ax + by + cz + d = 0的那种),然后用射线(或直线)到平面之间的交点距离来判断是否相交,以及求出相交距离。一个立方体有六个面,它通常有三个面是面向你的射线源的,而另外三个面则背向射线源。在“正面”的三个交点里找最远的,然后在“背面”的三个交点里找最近的,最后判断最近的是不是比最远的近,是的话,那就不相交,否则“正面最远交点”的距离就是这根射线与立方体表面的交点的距离了(然后直接用射线的源坐标和向量乘以距离值,就能直接推导出交点的具体坐标了)。



然而在shadertoy网站上被大佬说,你这个代码很复杂并且效率不行。后来我看了这个大佬给的实例,我发现他的做法更巧妙一些。那就是直接把摄像头移动到相对于立方体之间的坐标和方向,这样的话立方体的六个面就是绝对的面对x、y、z坐标轴方向或者反方向了。这样直接进行比例计算就可以判断是否相交,以及“正面最远”“背面最近”的两个交点了。

这个大佬并没有处理射线源在立方体内部与立方体内面进行相交的计算,于是我在他的代码上添油加醋,实现了我要的功能。
最终我写出来的样子是这样的:
  1. struct box_t
  2. {
  3.     vec3 p, d; // Position, Dimension
  4.     mat3 r; // Rotation
  5. };

  6. bool box_raycast(box_t box, vec3 start, vec3 n_ray, out vec3 castpoint, out vec3 normal, out vec2 uv, out float castdist, inout bool isfrominside)
  7. {
  8.         mat4 box_mat = mat4
  9.         (
  10.                 vec4(box.r[0], 0.),
  11.                 vec4(box.r[1], 0.),
  12.                 vec4(box.r[2], 0.),
  13.                 vec4(box.p, 1.)
  14.         );
  15.         bool inside = false;
  16.        
  17.         mat4 box_mat_inv = inverse(box_mat);
  18.        
  19.         vec3 start_local = (box_mat_inv * vec4(start, 1.)).xyz;
  20.         vec3 ray_local = (box_mat_inv * vec4(n_ray, 0.)).xyz;
  21.        
  22.         vec3 sv = step(abs(start_local), box.d);
  23.         if(sv.x > .5 && sv.y > .5 && sv.z > .5) inside = true;
  24.         if(inside || isfrominside) start_local = -start_local;
  25.        
  26.         vec3 rat = 1.0 / ray_local;
  27.         vec3 trp = rat * start_local;
  28.         vec3 dim = box.d * abs(rat);
  29.        
  30.         vec3 t1 = -trp - dim;
  31.         vec3 t2 = -trp + dim;

  32.         float tN = max( max( t1.x, t1.y ), t1.z );
  33.         float tF = min( min( t2.x, t2.y ), t2.z );
  34.        
  35.         if( !isfrominside && (tN > tF || tF < 0.0) ) return false;
  36.        
  37.         vec3 nor = -sign(ray_local) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);
  38.        
  39.         castpoint = start + n_ray * tN;
  40.         castdist = abs(tN);
  41.         normal = (box_mat * vec4(nor,0.)).xyz;
  42.         isfrominside = inside;
  43.        
  44.         vec3 cp_local = (box_mat_inv * vec4(castpoint, 1.)).xyz;
  45.        
  46.         uv = (nor.x * cp_local.yz + nor.y * cp_local.zx + nor.z * cp_local.xy) *.5 + .5;
  47.        
  48.         return true;
  49. }
复制代码
其中isfrominside这个参数如果你给了true的话,这个函数就判断你的射线源就在立方体内部,于是进行内面相交计算了。

现在我们已经能计算光线(或者说是视线)的折射和反射方向了,并且已经能计算射线到立方体之间的相交了,接下来我想做一个简单的场景出来。

最简单的办法就是直接使用Cubemap,它是一种特殊的纹理,你只需要给出一个向量,它就能根据你提供的方向来采样。
所以直接把折射出来的向量拿来从Cubemap采样,然后把它当作环境光就行了。这样一个简单的场景就有了。

前文说过光线在进入立方体后,有可能会发生很多次的全反射过程。这意味着,你不能只进行一次两次的射线相交运算就可以求出完整的颜色(或者取得完整的信息),你需要进行迭代运算。
然而在着色器里面你不能无限次数迭代,而且也不建议无限次数迭代。不然运气不好的时候,你的观察角正好能让一根视线被困住,你的显卡连续死循环2秒后,显卡驱动就“凉了”(XP则是直接蓝屏给你看)。

我的解决办法是直接用一个固定次数的循环来限制迭代次数即可。
  1. uint iter;

  2. for(iter = 0u; iter < 64u; iter ++)
  3. {
  4.         // 在这里迭代计算折射和反射等
  5. }
复制代码
但是,当我实际测试的时候,我遇到了一个奇怪的问题:在iPhone X上无法编译通过,有两个原因:
1、它不认识uint这种变量类型。这倒是可以理解的。
2、for循环必须在初始化部分定义一个变量,而不能用以前定义好的……这我就很纳闷了

也就是说,你必须这样改才行:
  1. for(float iter = 0.; iter < 64.; iter ++)
  2. {
  3.         // 在这里迭代计算折射和反射等
  4. }
复制代码
在超过迭代次数后,还没有迭代完的话,怎么办呢?我的办法是直接把迭代结果计算出来的颜色钦定为“玻璃本色”就行了。

玻璃的折射解决了,但玻璃并不会只有折射的颜色,它还会反射,而且还不是完全地反射(不然就成镜子了,并且也没有光线能射入它并且被折射了)。那到底多少比例的光会折射进来、多少比例的光会反射出去呢?

这是个问题。但你要是仔细观察过玻璃,或者湖面的话,你会发现当你低着头、正面看着一盆很平静的水的时候,你并不能照出自己的脸,只能看到盆底。但是当你以几乎平行于水面的角度去看盆里的水的话,它却几乎能反射全部的光线。

嗯并且可以配一副偏光眼镜,在钓鱼的时候,偏光眼镜的偏振片能挡住很多来自于水面的反射光。这是因为从水面反射的光线主要都是横向振动的光波——这些概念大概和现在的主题没什么卵关系。

所以总之用dot计算法向量与光线(视线)的向量的点乘的值拿来降低折射和反射的亮度,并且叠加起来就可以了。
回复

使用道具 举报

 楼主| 发表于 2018-7-8 21:19:04 | 显示全部楼层
最终效果大概是这样的:(如果你的显卡可以编译通过的话)
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-22 09:50 , Processed in 0.036444 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表