终于要开始做小场景啦,这次实现的主人翁是阿里云的吉祥物,名字叫做云小宝, 软软萌萌挺可爱。
通过之前学的知识我们将使用 400 行Shader代码实现以上场景. 完整的代码可以点击查看 www.shadertoy.com/view/XcsyRM 。所谓 shader就是一个程序,这个程序有能力知道任何一个屏幕上的像素应该显示什么颜色,就像有像素点问它,我现在是横着数第 346 号,竖着数是第 722 号像素,请问我应该穿什么颜色衣服, 然后 shader就告诉这个像素你要显示什么颜色
以下是系列文章清单
- 2D平面画出3D世界的Shader技术RayMarching的基本思路介绍
- Shader代码画出3D世界2: 柔软的阴影
- Shader代码画出3D世界3: 画面(透光,锯齿,阴影波纹)优化
- Shader 3d RayMarching4 相机与鼠标控制
- Shader 3d RayMarching5 3D SDF 原型
- Shader 3d RayMarching6 3D SDF造型
- Shader 3d RayMarching7 颜色与背景
- Old School光照的物理,数学与Shader代码
把云小宝画出来
如下图所示,云小宝使用以下 primitive组合而成,
- Ellipsoid
- 圆球
- 胶囊
- 黑球做眼睛
- 两个长方体做牙齿
- 弯曲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);
- 时间变量 t5 在 [0, 1) 范围内周期性变化。
- 距离 k 实际上就是 空间点到云小宝距离
- 波动方程中的变量 tt 结合了时间 t5 和位置距离 k。
- t5 * 15.0:将时间缩放到一个更大的范围内。
- PI * 2.0:调整相位,使其平衡或使波从原点生成。
- k * 3.0:根据距离增加相位,使波从中心向外扩展,并随着距离衰减。
- 波高 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) );
最后得到还不错的效果
天空云彩
云彩的实现原理其实是将天空的平面取到。这里不是很好理解。天空平面的函数为
vec2 uv = 1.8 * rd.xz/abs(rd.y + 0.2);
- 其中
rd.xz是射线方向在水平平面上的投影,形成一个二维向量。 - 将
rd.xz除以rd.y可以理解为将射线方向投影到一个水平面上,同时考虑射线的倾斜角度,使得云层效果随高度变化。当 rd.y 较大时,表示射线接近垂直向上,此时 uv 值较小,云层被压缩;当 rd.y 较小时,表示射线接近水平,此时 uv 值较大,云层被拉伸。这种方法通过简单的数学变换实现了天空云层的合理分布。 - 为 rd.y加上一个偏移量不是很好理解,通过以下代码获取图像感知
vec2 uv = 1.8 * rd.xz/abs(rd.y + sin(iTime));
col += sin(uv.x * uv.y);
rd.y + 0.2 得到以下图像, 可以看到黑线在远方越来越密集,符合近大远小的规律。
- 乘以 1.8 是一个经验值,用来调整纹理的范围和云层的大小。
float cl = sin(uv.x) + sin( uv.y );
col += 0.2 * smoothstep(-.1, .1, -0.7 - cl) ;
以上是一段生成小圆的纹理代码
上面的云彩太规律了,我想为它的边缘在增加一下波动,这个时候类似做一个傅里叶变换的动作。在增加一个波函数
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));
另外我们可以对 uv坐标做一些简单的旋转,让云的方向更真实一些
uv *= mat2(0.8,0.6,-0.6,0.8)*1.1;