又两周没有写文章了,工作中各种被Push, 但又需要这份工来养家糊口。 什么时候可以存到500万然后在银行吃利息生活啊,不过玩了几个月的Shader, 终于要开始进入3D的世界啦,开心。照例看一些相关的Shader, 下面的happy jumping, Rainforest , temple都是使用IQ大神使用raymarching技术实时渲染出来的视觉效果,代码量都在600行左右,非常了不起吧。 不过我相信,我跟着学一个月,也能看懂并做一些些改动了。本节大部份内容都是对于最后学习资料的学习过程,部分图片使用学习资料的视频截图,这些图片会带上出处
RayMarching基本介绍
Raymarching 是一种在计算机图形学中用来渲染场景的技术,特别是在实时渲染和非实时渲染中的程序式图形和视觉特效的生成。与经典的光线投射(Raycasting)或光线追踪(Raytracing)相比,Raymarching 通过在场景中逐步"行进"来近似相交检测,并被广泛用于渲染体积材料(如烟雾、云)和复杂的几何形状(如分形)。
基本原理
Raymarching 的基本原理是:对于场景中的每一个像素,发出一条从相机位置出发并穿过像素的光线。然后,沿着这条光线,分步前进一定的距离,并检查光线上的点是否击中了场景中的任何对象。
这个"前进步数"可以固定或者动态调整。动态调整步数的一个常见方法是使用所谓的"有符号距离场"(Signed Distance Fields, SDFs)。这是一种可以快速确定点到场景中最近表面距离的技术,因此光线可以安全地多走这段距离,不会错过任何物体。
优缺点
优点
- 柔和的阴影和模糊效果:由于 Raymarching 是连续采样场景的,它可以很自然地产生柔和阴影和模糊效果(例如轻微地调整步进距离)。
- 复杂体积效果:能够以几何无关的方式渲染复杂的体积效果,因为它不需要传统网格来定义形状。
- 非欧几里得和程序化场景:Raymarching 特别适合处理复杂的分形几何结构和程序化材料,这些通常难以用传统模型表示。
缺点
- 性能开销:由于需要对每个像素的光线进行大量计算,Raymarching 可能非常消耗性能,尤其是在场景复杂或者光线需要行进很多步才能找到物体的时候。
- 优化困难:要让 Raymarching 运行地足够快以用于实时应用,可能需要进行大量的优化和技巧性写法。
应用领域
Raymarching 在视觉效果产业中被广泛使用,尤其是在那些需要高度定制渲染算法的场合。这包括实时图形(如视频游戏)、动态艺术、VR/AR 和电影特效。
技术要点
在实现 Raymarching 时,通常会遵循以下的一些步骤:
- 场景表示:将场景表示成距离场(通常是有符号距离场 SDF)。
- 光线步进:从相机位置出发,对于每个像素逐步沿光线方向前进。
- 距离评估:利用 SDF 评估当前点到物体表面的最小距离,以此来决定下一步进的大小。
- 渲染和着色:当光线最终靠近物体的时候(即距离小于预设阈值),计算着色和反照率。
Raymarching 通常在着色器(Shader)中实现,着色器编程使得开发者能够对 GPU 的并行计算能力进行利用,高效执行大量的光线步进和渲染计算。由于 GPU 的强大计算能力,Raymarching 已经可能在实时应用中达到令人印象深刻的视觉效果。
代码实现
在现实世界中我们的视线实际上去接受世界上物体反射给我们的光,我们的眼睛是一个接收器,视觉中对于距离和物体形体的判断需要依靠大脑中非常复杂的机制。 而在代码世界中,我们没有大脑那么强的能力,但是却又拥有了一些奇妙的其他能力。 代码的Ray可以做任何变换,例如加入一个sin函数就能让你的视线在空间中旋转, 而物体形体的判断是通过距离函数来确定。当你的视线到了某个物体的表面,视线距离物体表面的距离接近于0.
下图是一个非常完整代码实现思路,图中的照相机,屏幕和实际物体就是代码中需要处理的问题。最核心的是Ray和Distance这两个抽象的概念。
Camera
相机表示
在纯shader表示相机的方式与传统3D图形中的相机表示有所不同。相机通常是隐式定义的,最简单的camera就是cameraPosition(rayOrgin)和rayDirection两个变量
glsl
vec3 rayOrgin = vec3(0., 1., 0.);
vec3 rayDirection = normalize(vec3(uv.x, uv.y, 1.));
这里可以思考一个有趣的问题,如果是按照传统的3D图形相机,通常有以下的结构
glsl
struct Camera {
vec3 position; // 相机的位置
vec3 target; // 相机指向的目标点(在世界空间中)
vec3 up; // 相机的上方向
float fov; // 相机的视野(Field of View)
float aspectRatio; // 宽高比
};
那么对应ray的配置落到camera的配置,应该值是什么呢?
glsl
position = (0, 1, 0)
target = (0, 0, 1)
aspectRatio = iResolution.x / iResolution.y
up = (0, 1, 0)
fov = 90
碰撞检测
确定射线后,接下去的问题是如何确定这些射线于3d object是否相交。以下图从P0点不断的往P4方向行进,每次探一探距离,如果P点于边缘点小于某个小值,则说明已经相交了。 下探的距离使用经验值,即上一个点离表面的最短距离。 www.shadertoy.com/view/4dKyRz
glsl
#define MAX_STEPS 80;
#define MAX_DIST 100;
#define SURFACE_DIST 0.001;
float RayMarch(vec3 ro, vec3 rd) {
float distanceFromOrigin = 0.;
for (int i=0; i<MAX_STEPS; i++){
vec3 p = ro + distanceFromOrigin*rd;
float distanceFromSurface = GetDist(p);
distanceFromOrigin += distanceFromSurface;
if(distanceFromSurface<SURFACE_DIST || distanceFromOrigin>MAX_DIST) break;
}
return distanceFromOrigin;
}
以上图为例,这个世界有地平面和球两个物体,GetDist
函数应该是
ini
#define MAX_STEPS 80
#define MAX_DIST 100.
#define SURFACE_DIST 0.001
float GetDist(vec3 p) {
vec4 sphere = vec4(0., 1., 6., 1.);
float distanceSphere = length(p - sphere.xyz) - sphere.w;
float distancePlane = p.y;
return min(distanceSphere, distancePlane);
}
最后讲RayMarching获取的值,变成颜色变获得以下的3D世界。 明显画面没有体积感。在传统素描技法中需要明暗对比,透视,纹理来表现体积感。接下来就是光照
Lighting
基础光照模型
一个常用的简单光照模型是Phong光照模型,它由环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和高光(Specular Lighting)三个组成部分构成。
- 环境光照 (Ambient Lighting) 环境光照是一个全局光照,假设光线从每个方向均匀的照射到物体上,它提供了一个没有方向性的基础亮度,以模拟间接光的效果,避免完全处于黑暗中。
glsl
vec3 ambientColor = vec3(0.1, 0.1, 0.1); // 根据场景情况调整环境光亮度
vec3 ambient = ambientColor * surfaceColor; // surfaceColor 是物体的颜色
- 漫反射光照 (Diffuse Lighting) 漫反射光照根据表面法线和光源方向的角度差来计算,角度差越小(也就是这两个方向越贴近)漫反射光照越强,这表现了光线如何照射到表面并向各个方向均匀散射。
glsl
vec3 lightDir = normalize(lightPos - fragPos); // 计算光线方向
vec3 norm = normalize(normal); // 计算法线方向
float diff = max(dot(norm, lightDir), 0.0); // 计算法线和光线的点乘,取最大值以避免负数
vec3 diffuse = diff * lightColor * surfaceColor; // lightColor 是光源颜色
- 高光 (Specular Lighting) 高光表现的是光线照射到表面后,沿着一个反射方向产生的亮点效果。高光大小取决于观察方向和反射方向的接近程度,它通常在镜面反射方向附近最亮。
glsl
vec3 viewDir = normalize(viewPos - fragPos); // 观察方向
vec3 reflectDir = reflect(-lightDir, norm); // 反射方向,传入的光线方向需要取反
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); // shininess 是高光的反射程度
vec3 specular = spec * lightColor;
法向量计算
更详细的数值优化方法查看IQ的文章: iquilezles.org/articles/no...
Diffuse和Specular都需要法线支持。在本文的场景中,球面和地平面的发现都非常好计算,球面上某一点的法线是一个从球心指向该点的向量。由于球体在每个点上都是均匀的(完全对称),法线向量可以通过简单地从球心指向表面点的方向得到,并且该向量应当单位化(即变为单位向量)。而平面的法向量其实就是(0, 1, 0)
。 以上法向量的计算方法是分析方法,在SDF场景有更加通用的数值方法。回到法向量的定义, 在曲线中有
- 导数:在二维空间中,对于曲线 y = f(x),函数 f(x) 在某点 x 的导数 f'(x) 表示在该点位置曲线的斜率,也代表了曲线的瞬时变化率。
- 切线:曲线上某一点的切线是在该点与曲线相切的直线,其斜率等于函数在这一点的导数。切线与曲线在切点处拥有相同的方向。
- 法向量:对于平面曲线,法向量通常定义为指向曲线"外侧"的垂直于切线的向量。在二维中,如果切线的方向由向量 (dx, dy) 给出,法向量可以是 (dy, -dx) 或 (-dy, dx),取决于法向量的方向
而在曲面中
- 偏导数:在三维空间中,对于曲面 z = f(x, y),函数 f 的偏导数 ∂f/∂x 和 ∂f/∂y 描述了沿 x 和 y 方向的局部斜率。这些局部斜率可以用于确定在给定点的切平面。
- 切平面与切线:曲面上某一点的切平面是在该点接触曲面的平面,它包含所有可能经过该点的切线。对于曲面上的一条特定曲线,其切线就位于这个切平面上。
- 法向量:曲面上某一点的法向量是垂直于切平面的向量。如果知道曲面在该点的偏导数,法向量可以使用交叉积来计算。例如,如果曲面可以表示为参数形式 r(u, v) (u 和 v 是参数),那么法向量可以通过 r_u 与 r_v 的交叉积计算得出,其中 r_u 和 r_v 分别是曲面向量函数关于参数 u 和 v 的偏导数向量。
导数的含义其实就是微分趋于0的极限值,而用一个非常小的微分h
可以得到导数的近视值。假设我们的曲面函数为f(p)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( p ) = 0 n = n o r m a l i z e ( Δ f ( p ) ) Δ f ( p ) = { d f ( p ) d x , d f ( p ) d y , d f ( p ) d z } d f ( p ) d x ≈ f ( p + { h , 0 , 0 } ) − f ( p ) h \begin{aligned} f(p) = 0 \\ n = normalize(\Delta f(p)) \\ \Delta f(p) = \lbrace \frac{df(p)}{dx} , \frac{df(p)}{dy} ,\frac{df(p)}{dz} \rbrace \\ \frac{df(p)}{dx} \approx \frac{f(p+\lbrace h,0,0 \rbrace)-f(p)}{h} \end{aligned} </math>f(p)=0n=normalize(Δf(p))Δf(p)={dxdf(p),dydf(p),dzdf(p)}dxdf(p)≈hf(p+{h,0,0})−f(p)
当然上面的近视值只考虑了正数方向,如果增加负数方向可以表示为
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> d f ( p ) d x ≈ f ( p + { h , 0 , 0 } ) − f ( p − { h , 0 , 0 } ) 2 h \frac{df(p)}{dx} \approx \frac{f(p+\lbrace h,0,0 \rbrace)-f(p-\lbrace h,0,0 \rbrace)}{2h} </math>dxdf(p)≈2hf(p+{h,0,0})−f(p−{h,0,0})
所以整体的法向量近视值为
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> n ≈ n o r m a l i z e ( { f ( p + { h , 0 , 0 } ) − f ( p − { h , 0 , 0 } ) , f ( p + { 0 , h , 0 } ) − f ( p − { 0 , h , 0 } ) , f ( p + { 0 , 0 , h } ) − f ( p − { 0 , 0 , h } ) } ) n \approx normalize(\begin{aligned}\lbrace f(p+\lbrace h,0,0 \rbrace) &- f(p-\lbrace h,0,0 \rbrace), \\ f(p+\lbrace 0,h,0 \rbrace) &- f(p-\lbrace 0,h,0 \rbrace), \\ f(p+\lbrace 0,0,h \rbrace) &- f(p-\lbrace 0,0,h \rbrace) \rbrace \end{aligned} ) </math>n≈normalize({f(p+{h,0,0})f(p+{0,h,0})f(p+{0,0,h})−f(p−{h,0,0}),−f(p−{0,h,0}),−f(p−{0,0,h})})
于是有normal的代码为
glsl
#define NORMAL_DELTA 0.01
vec3 GetNormal(vec3 p) {
vec2 e = vec2(NORMAL_DELTA, 0.);
return normalize(vec3(
GetDist(p+e. xyy) - GetDist(p-e.xyy),
GetDist(p+e.yxy) - GetDist(p-e.yxy),
GetDist(p+e.yyx) - GetDist(p-e.yyx)
));
}
最后得到一个有体积感的空间物体啦
Study Materials
- ShaderToy教程bilibili中文: www.bilibili.com/video/BV18q...
- An introduction to Raymarching: www.youtube.com/watch?v=khb...
- The Art Of Code(youtube channel) www.youtube.com/@TheArtofCo...
- Ray Marching for Dummies!
- Ray Marching Simple Shapes
- Shader Coding: Ray Marching Tips & Tricks
- How to texture a procedural object
- Newton's Cradle 1,2,3
- Live Coding:Bending Light 1,2
- Inigo Quilez(shader master) www.youtube.com/@InigoQuile...
- LIVE Coding "Happy Jumping"
- Painting a Landscape with Maths
- Live Coding "Greek Temple"
- The SDF of a Line Segment
- Rounding Corners in SDFs
- The SDF of a Box