记事本400行代码实现有山有云有小动物的虚拟世界

953 阅读9分钟

终于要开始做小场景啦,这次实现的主人翁是阿里云的吉祥物,名字叫做云小宝, 软软萌萌挺可爱。

通过之前学的知识我们将使用 400 行Shader代码实现以上场景. 完整的代码可以点击查看 www.shadertoy.com/view/XcsyRM 。所谓 shader就是一个程序,这个程序有能力知道任何一个屏幕上的像素应该显示什么颜色,就像有像素点问它,我现在是横着数第 346 号,竖着数是第 722 号像素,请问我应该穿什么颜色衣服, 然后 shader就告诉这个像素你要显示什么颜色

Shader(x,y)=color(red,green,blue)Shader(x, y) = color(red, green, blue)

以下是系列文章清单

  1. 2D平面画出3D世界的Shader技术RayMarching的基本思路介绍
  2. Shader代码画出3D世界2: 柔软的阴影
  3. Shader代码画出3D世界3: 画面(透光,锯齿,阴影波纹)优化
  4. Shader 3d RayMarching4 相机与鼠标控制
  5. Shader 3d RayMarching5 3D SDF 原型
  6. Shader 3d RayMarching6 3D SDF造型
  7. Shader 3d RayMarching7 颜色与背景
  8. Old School光照的物理,数学与Shader代码

把云小宝画出来

如下图所示,云小宝使用以下 primitive组合而成,

  1. Ellipsoid
  2. 圆球
  3. 胶囊
  4. 黑球做眼睛
  5. 两个长方体做牙齿
  6. 弯曲Ellipsoid 与身体做差当眼睛

跳动

运动主要是像一个弹力球一样往前跳动。 上下运动是一个抛物线

向前运动就是移动 z坐标, 同时别忘了把相机沿着 z坐标走

        // parabola jumping
        float y = 4.0 * t * (1.0 - t);  // from 0-1
        float z = time;
        vec3 center = vec3(0., y, z);

    	vec3 qos = pos - center;

于是我们的画面动起来了

身体duangduang

由于云小宝是软绵绵的,所以在跳动的时候希望它 duangduang的。在跌倒地上的时候能够扁一点。这里注意我们用到了很多Ellipsoid, Ellipsoid的 sdf如下

    float sdEllipsoid(vec3 p, vec3 r)
    {
        float k0 = length(p / r);
        float k1 = length(p / (r * r));
        return k0 * (k0 - 1.0) / k1;
    }

只要调整 r的值就可以获得不同长宽高的球. 这里我们首先对 抛物线运动函数进行求导

        float dy = 4.0 * (1.0 - 2.0 * t); // directive of y

导数越大变化越大,云小宝只有在到地上也就是导数最大的时候才会压扁。当到了地上的时候,Ellipsoid的 x,z变大,y变小

       float flattenedRatio = abs(dy) * abs(dy) * abs(dy);
        float sz = 0.002 * flattenedRatio ;
        float sx = sz;
        float sy = -2. * sz;

看起来有点傻,但是运动起来效果就非常不错啦

扭动

因为云小宝没有手和脚,没办法通过蹦腿完成往前移动,那么它应该是像小虫子一样扭着往前动的,所以我们为这个物体加上一个小小的旋转,看起来运动更真实。 先找到一个方波函数,信号每隔一秒变化一次正负

    float squareWare(float time) {
        return sign(fract(time * .5) - .5);
    }

扭动应该在最高处扭的最剧烈, 扭动实现通过做一次空间的旋转

        float wiggleAngle = 0.1 * PI * y * squareWare(time);
    	qos.xz = r2d(qos.xz, wiggleAngle);

这样云小宝就扭起来啦

地面 duangduang

由于我们运动的抛物线是完全不衰减力量的,这不符合常理。那只能找到一个原因是大地是有弹力的。那这个时候云小宝在地面跳的时候,会有类似于弹力床一样的波纹效果,亦或者石头调到水面形成的波纹。 整体代码如下

        float t5 = fract(time+0.05);
        float k = length(pos.xz-center.xz);
        float tt = t5*15.0- PI* 2.0 - k*3.0;
        groundHeight = 0.1*exp(-k*k)*sin(tt)*exp(-max(tt,0.0)/2.0)*smoothstep(0.0,0.01,t5);
        
  1. 时间变量 t5 在 [0, 1) 范围内周期性变化。
  2. 距离 k 实际上就是 空间点到云小宝距离
  3. 波动方程中的变量 tt 结合了时间 t5 和位置距离 k。
    • t5 * 15.0:将时间缩放到一个更大的范围内。
    • PI * 2.0:调整相位,使其平衡或使波从原点生成。
    • k * 3.0:根据距离增加相位,使波从中心向外扩展,并随着距离衰减。
  4. 波高 groundHeight
    • 0.1 * exp(-k * k):exp(-k * k) 是一个高斯衰减函数,表示波的振幅随距离的增加迅速减小。常数 0.1 是振幅系数,用于缩放波高。
    • sin(tt):sin 函数用于生成周期性的波动。
    • exp(-max(tt, 0.0) / 2.0):这个项用于动态衰减,为了使波在传播过程中逐渐减弱。max(tt, 0.0) 确保 tt 非负。/2.0 调节衰减速率。
    • smoothstep(0.0, 0.01, t5):用于平滑管理 t5 接近 0 的变化,使波高慢慢上升并变得平滑。 最后有以下效果

丰富的地形

制作地形的原理是做一堆Elliposiod, 然后和打的糅合在一起。 这里使用到经典的 fract用于创造重复的物体,同时引入一个 random函数让土包高度不一

        vec3 qos = vec3(
            mod(abs(pos.x), 4.0) - 2.,
            pos.y,
            mod(pos.z + 1.5, 3.0) - 1.5);
        vec2 id = vec2(
            floor(abs(pos.x / 4.0)),
            floor((pos.z + 1.5) / 3.0)
        );
        vec2 rr = srandom2(id);
        vec3 radius = vec3(1.0, 1.0 + rr.x + rr.y, .7);
        float tree = sdEllipsoid(qos, radius);
        

相机晃动

因为云小宝太重了, 所以掉在地上时候做一个相机晃动的感觉,会让画面更加有意思。其原理就是当云小宝快到地面时候就对镜头做一个轻微的 xz方向上的晃动

首先找到以上一个信号函数,将f(x)取绝对值便可以得到

尖尖的地方就是云小宝落在地面时候,这个使用使用通过 smoothstep做一下值的限制

        float ffx = abs(fract(time*0.5)-0.5)/0.5;
        float bounce = -1.0 + 2.0*ffx;
        rayOrigin += 0.06*sin(time*12.0+vec3(0.0,1.0,2.0) * 2.0)
    		*smoothstep( 0.85, 1.0, abs(bounce) );

最后得到还不错的效果 240721222

天空云彩

云彩的实现原理其实是将天空的平面取到。这里不是很好理解。天空平面的函数为

        vec2 uv =  1.8 * rd.xz/abs(rd.y + 0.2);
  1. 其中rd.xz 是射线方向在水平平面上的投影,形成一个二维向量。
  2. rd.xz 除以 rd.y 可以理解为将射线方向投影到一个水平面上,同时考虑射线的倾斜角度,使得云层效果随高度变化。当 rd.y 较大时,表示射线接近垂直向上,此时 uv 值较小,云层被压缩;当 rd.y 较小时,表示射线接近水平,此时 uv 值较大,云层被拉伸。这种方法通过简单的数学变换实现了天空云层的合理分布。
  3. 为 rd.y加上一个偏移量不是很好理解,通过以下代码获取图像感知
        vec2 uv =  1.8 * rd.xz/abs(rd.y + sin(iTime));
        col += sin(uv.x * uv.y);

rd.y + 0.2 得到以下图像, 可以看到黑线在远方越来越密集,符合近大远小的规律。

  1. 乘以 1.8 是一个经验值,用来调整纹理的范围和云层的大小。
        float cl =  sin(uv.x) + sin( uv.y );
        col += 0.2 * smoothstep(-.1, .1, -0.7 - cl)  ;

以上是一段生成小圆的纹理代码 2407213432

上面的云彩太规律了,我想为它的边缘在增加一下波动,这个时候类似做一个傅里叶变换的动作。在增加一个波函数

float cl =  1.0 * (sin(1.0 * uv.x) + sin(1.0 * uv.y)) + 
            0.3 * (sin(3.0 * uv.x) + sin(3.0 * uv.y));

2407213700

另外我们可以对 uv坐标做一些简单的旋转,让云的方向更真实一些

uv *= mat2(0.8,0.6,-0.6,0.8)*1.1;

2407213854