怎么做到的?3D数字孪生特效100行Shader代码搞定,万字长文讲清楚!

16 阅读1分钟

先上效果。这些画面已经很难在工程中使用了。 画面所代表的意义需要自己寻找,在理解背后的技术之后,读者朋友也可以自己去探索出自己觉得有意思的画面。 2505163632 2505163410 2505163857 2505163753

第一个火焰的作者 Xor代码实在是 golfy。我写了一个易读的版本, 可以访问 www.shadertoy.com/view/3XSXWV 查看。 在理解 Xor提出的高效湍流技术之后,我结合其他效果创作了其他 3 个视觉效果。

golfy 是一个shadertoy社区极客文化,他们以字母数量少为目标。比如上面的火焰只用了 340 个字母(没错,就是 340 个字母。)在不借助任何库的情况下就实现了上面的效果。 这个文化对新手不友好。 沉迷这种文化的人貌似大多都是个人 PC刚兴起的时候 20 岁,现在都 40,50 了。 也许是他们的浪漫吧

首先有三篇前置知识可以先了解

  1. 上篇的2D 火焰

  2. 上上篇的湍流技术

  3. 上上上篇的Tonmap技术

这里的实现有一些技巧是需要空间想象力,例如 raymarching技术。有一些不符合物理世界规律的,例如对光线进行扭曲。 作者本人水平实在有限,还在门口徘徊,尽力理解并讲解,如果读者对这个很有兴趣并还是不是很清楚。 欢迎加我微信(在公众号聊天可获取)讨论, 另外理解 Volumetric Rendering 与 IFS 对理解本篇中的技巧有帮助。 我在附录中也放了相关资料。

RayMarching光线步进

Raymarching 是一种在计算机图形学中用来渲染场景的技术,特别是在实时渲染和非实时渲染中的程序式图形和视觉特效的生成。与经典的光线投射(Raycasting)或光线追踪(Raytracing)相比,Raymarching 通过在场景中逐步“行进”来近似相交检测,并被广泛用于渲染体积材料(如烟雾、云)和复杂的几何形状(如分形)。

基本原理

2505161844 Raymarching 的基本原理是:对于场景中的每一个像素,发出一条从相机位置出发并穿过像素的光线。然后,沿着这条光线,分步前进一定的距离,并检查光线上的点是否击中了场景中的任何对象。

这个“前进步数”可以固定或者动态调整。动态调整步数的一个常见方法是使用所谓的“有符号距离场”(Signed Distance Fields, SDFs)。这是一种可以快速确定点到场景中最近表面距离的技术,因此光线可以安全地多走这段距离,不会错过任何物体。

现实世界中我们的视线实际上去接受世界上物体反射给我们的光,我们的眼睛是一个接收器,视觉中对于距离和物体形体的判断需要依靠大脑中非常复杂的机制。 而在代码世界中,我们没有大脑那么强的能力,但是却又拥有了一些奇妙的其他能力。 代码的Ray可以做任何变换,例如加入一个sin函数就能让你的视线在空间中旋转, 而物体形体的判断是通过距离函数来确定。当你的视线到了某个物体的表面,视线距离物体表面的距离接近于0.

Camera

最核心的是Ray和Distance这两个抽象的概念。不过我们先用 3D常见的 camera来理解他。 2505160129

在纯shader表示相机的方式与传统3D图形中的相机表示有所不同。相机通常是隐式定义的,最简单的camera就是cameraPosition(rayOrgin)和rayDirection两个变量

    vec3 rayOrgin       = vec3(0., 1., 0.);    vec3 rayDirection   = normalize(vec3(uv.x, uv.y, 1.));

这里可以思考一个有趣的问题,如果是按照传统的3D图形相机,通常有以下的结构

struct Camera {    vec3 position;  // 相机的位置    vec3 target;    // 相机指向的目标点(在世界空间中)    vec3 up;        // 相机的上方向    float fov;      // 相机的视野(Field of View)    float aspectRatio;  // 宽高比};

那么对应ray的配置落到camera的配置,应该值是什么呢?

position = (0, 1, 0)target =   (0, 0, 1)aspectRatio = iResolution.x / iResolution.yup = (0, 1, 0)fov = 90

碰撞检测

确定射线后,接下去的问题是如何确定这些射线于3d object是否相交。以下图从P0点不断的往P4方向行进,每次探一探距离,如果P点于边缘点小于某个小值,则说明已经相交了。 下探的距离使用经验值,即上一个点离表面的最短距离。 2505165854

#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函数应该是

#define MAX_STEPS 80#define MAX_DIST 100.#define SURFACE_DIST 0.001float 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世界。 明显画面没有体积感。在传统素描技法中需要明暗对比,透视,纹理来表现体积感。接下去就是增加光照。

2407070450

2407070450

基础光影

一个常用的简单光照模型是Phong光照模型,它由环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和高光(Specular Lighting)三个组成部分构成。

  • 环境光照 (Ambient Lighting) 环境光照是一个全局光照,假设光线从每个方向均匀的照射到物体上,它提供了一个没有方向性的基础亮度,以模拟间接光的效果,避免完全处于黑暗中。

    vec3 ambientColor = vec3(0.1, 0.1, 0.1); // 根据场景情况调整环境光亮度vec3 ambient = ambientColor * surfaceColor; // surfaceColor 是物体的颜色

  • 漫反射光照 (Diffuse Lighting) 漫反射光照根据表面法线和光源方向的角度差来计算,角度差越小(也就是这两个方向越贴近)漫反射光照越强,这表现了光线如何照射到表面并向各个方向均匀散射。

    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) 高光表现的是光线照射到表面后,沿着一个反射方向产生的亮点效果。高光大小取决于观察方向和反射方向的接近程度,它通常在镜面反射方向附近最亮。

    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… , 计算normal的代码为

#define NORMAL_DELTA 0.01vec3 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)    )); }

最后得到一个有体积感的空间物体啦

2505163028

2505163028

不过我们将在这里停止,不在继续讨论光照模型。 因为本篇侧重技巧不需要物理模型的光照,而是用一种聪明的,伪装的,看起来像的方法。

并非有光才有色

边缘权重增强

我们在这里换一种着色的方式. 不再使用光照。 我们做一种特殊的权重方式。 我们在 raymarching的过程中,如果离球体表面越近,那么我们就增加一些颜色, 最后求平均。 2505163109 核心代码如下所示

float ShaderSphere(vec3 ro, vec3 rd) {    float rayDepth = 0.;    float stepSize = 0.0;    float colorWeight = 0.0;	// 迭代 50 次    for (int i=0; i<50; i++){        vec3 p = ro + rayDepth*rd;        float distanceFromSurface = GetDist(p);		// 离表面越近,步子越小        stepSize = 0.01 + abs(distanceFromSurface)/7.;		// 步子越小,颜色贡献越大        colorWeight += 1. / stepSize;        rayDepth += stepSize;    }    return colorWeight;}

当射线与平面的法向量越接近,那么步子会走的更快。从而边缘能够获得更多的着色机会。下图为着色算法,当射线朝向球中心,和射线朝向球边缘的迭代图。 x轴为迭代次数,y轴为rayDepth。 可以看到在边缘处整体射线会停留更久,也就是会获得更多颜色。最后的变现就是边缘更亮

2505153404

2505153427

色盘

IQ在这篇文章中介绍了通过三角函数创造色盘的方法 iquilezles.org/articles/pa…

// cosine based palette, 4 vec3 paramsvec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ){    return a + b*cos( 6.28318*(c*t+d) );}

为了获取自己喜欢的颜色,也可以到 dev.thi.ng/gradients/ 这个网站去调配颜色, 这里我随便需选了一些颜色

vec3 palette( in float t ){    vec3 a = vec3(0.902,0.914,0.086);    vec3 b = vec3(0.529,0.494,0.808);    vec3 c = vec3(1.000, 1.000, 1.000);     vec3 d = vec3(0.000, 0.333, -2.362);    return a + b*cos( 6.28318*(c*t+d) );}

当我们讲这个颜色放到权重函数中去,并且用时间做一下 offset. 看看效果会如何

vec3 gCol = palette(rayDepth - iTime ) * 1.  ;color += gCol / stepSize;

2505165518

2505165518

火焰原作者使用了简化版本,这里但背后的原理是一样,其方法为

vec3 gCol = sin(rayDepth / 3.0 + vec3(7.0, 2.0, 3.0) - iTime) + 1.1;color += gCol / stepSize;

最后效果有 2505165814

三角函数 + FBM

Gyroid

**这里只做了解, 核心是要知道。 我们通过对坐标进行 三角函数操作, 可以得到一些一想不到的曲面... **。 如果想要详细了解关于Gyroid结构的艺术创作,一定要去看在文章尾部The art of code的讲解。 下图为 shadertoy另外一个大神FabriceNeyret2 创作的

2505160627

2505160712

Gyroid(双曲极小曲面) 是一种复杂的三维几何结构,属于数学中的 三重周期极小曲面(Triply Periodic Minimal Surfaces, TPMS)。它因其独特的拓扑结构和物理特性,在材料科学、生物学、3D打印、艺术设计等领域有广泛应用。其 ** 数学定义**

  • 极小曲面:曲面的平均曲率为零,即在任意一点上,曲面的两个主曲率之和为零。这种特性使极小曲面具有最小能量状态(如肥皂膜的形状)。

  • 三重周期性:Gyroid 在三个维度(x, y, z)上周期性重复,形成无限延展的蜂窝状结构。

  • 无自交:曲面在三维空间中不会与自身相交,保持连续性和光滑性。

数学表达式(近似形式):(实际精确表达式更复杂,涉及超几何函数或隐式方程)

\sin(x)\cos(y) + \sin(y)\cos(z) + \sin(z)\cos(x) = 0
\

Gyroid 结构在自然界中广泛存在,例如:

  • 细胞膜:某些细胞器(如线粒体)的膜系统呈现类似 Gyroid 的双层结构,用于高效物质交换。

  • 昆虫翅膀:甲虫翅鞘的纳米级结构具有 Gyroid 模式,增强机械强度和光学性能。

  • 病毒外壳:部分病毒的蛋白质外壳采用 Gyroid 几何排列,优化稳定性和组装效率。

FBM

当我们用这种曲面的手段进一步结合 FBM的模式。具体如下代码

p += cos(p.yzx * 1.0)  / 1.0;p += cos(p.yzx * 2.0)  / 2.0;p += cos(p.yzx * 4.0)  / 4.0;p += cos(p.yzx * 8.0)  / 8.0;

会发现出现了漂亮的曲面 2505162406

到这里最终的形状不大可解释了, 不过不妨碍他很美。 这里我们只看到颜色的运动, 如果想要形状发生改变,我们试着在 三角函数里面加上一些时间分量看看。

        vec3 offset = vec3(iTime, iTime * 2.0, 0.);                   p += cos((p.yzx + offset) * 1.0)  / 1.0;        p += cos((p.yzx + offset) * 2.0)  / 2.0;        p += cos((p.yzx + offset) * 4.0)  / 4.0;        p += cos((p.yzx + offset) * 8.0)  / 8.0;

最后得到了 2505162804 已经很美了,虽然不知道这是个什么玩意.... 至此所有核心的技巧已经讲解完毕

至此所有核心的技巧已经讲解完毕, 接下去就是发现美了

火焰

火焰代码如下。

// Fork of "3D Fire [340]" by Xor. https://shadertoy.com/view/3XXSWS// 2025-05-06 22:13:32void mainImage(out vec4 fragColor, vec2 fragCoord){    // Time for animation    float t = iTime;        // Raymarch loop iterator    float i = 0.0;        // Raymarched depth (rayDepth)    float rayDepth = 0.0;        // Raymarch step size and "Turbulence" frequency (stepSize)    float stepSize;    // Normalize screen coordinates    vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;    // Camera setup    vec3 rayOrigin = vec3(0.0, 0.0, 0.0);    vec3 camPos = vec3(0.0, 0.0, 1.0);    vec3 dir0 = normalize(-camPos);    vec3 up = vec3(0.0, 1.0, 0.0);    vec3 right = normalize(cross(dir0, up));    up = cross(right, dir0); // Recalculate up to ensure orthogonality    // Calculate ray direction based on screen UV    vec3 rd = normalize(        dir0         + up * (uv.y + 1.0 / iResolution.y)         + right * (uv.x + 1.0 / iResolution.x)    );    // Raymarching loop with 50 iterations    for (i = 0.0; i < 50.0; i++) {        // Compute raymarch sample point        vec3 hitPoint = rayOrigin + rayDepth * rd;        // Animation: Flame moves backward with slight wobble        hitPoint.z += 5.0 + cos(t);        // Distortion: Rotate x/z plane based on y-coordinate        float rotationAngle = hitPoint.y * 0.5;        mat2 rotationMatrix = mat2(            cos(rotationAngle), -sin(rotationAngle),            sin(rotationAngle),  cos(rotationAngle)        );        hitPoint.xz *= rotationMatrix;        // Flame shape: Expanding upward cone        hitPoint.xz /= max(hitPoint.y * 0.1 + 1.0, 0.1);        // Turbulence effect using fractal noise (fBm)        float turbulenceFrequency = 2.0;        for (int turbulenceIter = 0; turbulenceIter < 5; turbulenceIter++) {            vec3 turbulenceOffset = cos(                (hitPoint.yzx - vec3(t / 0.1, t, turbulenceFrequency))                 * turbulenceFrequency            );            hitPoint += turbulenceOffset / turbulenceFrequency;            turbulenceFrequency /= 0.6;        }        // Calculate distance to flame surface (hollow cone)        float coneRadius = length(hitPoint.xz);        float coneDistance = abs(coneRadius + hitPoint.y * 0.3 - 0.5);        // Compute step size for raymarching        stepSize = 0.01 + coneDistance / 7.0;        // Update ray depth        rayDepth += stepSize;        // Add color and glow effect based on depth        vec4 flameColor = sin(rayDepth / 3.0 + vec4(7.0, 2.0, 3.0, 0.0)) + 1.1;        fragColor += flameColor / stepSize;    }    // Tanh tonemapping    fragColor = tanh(fragColor / 2000.0);}

资料

  1. Interactive Graphics 25 - Volume Rendering www.youtube.com/watch?v=y4K…

  2. How Big Budget AAA Games Render Clouds www.youtube.com/watch?v=Qj\…

  3. RayMarching介绍 www.bilibili.com/video/BV1MV…

  4. Volume Rendering www.scratchapixel.com/lessons/3d-…

  5. 爆米花图案 iquilezles.org/articles/po…

  6. IFS 祛魅 neozhaoliang.github.io/ifs-demysti…

  7. Raymarching in Raymarching www.shadertoy.com/view/wlSGWy

  8. Gyroid结构 www.youtube.com/watch?v=-ad…