UV变换
基于上一篇文章我们画好的长方形,我们开始进行UV变换相关的实践。
注意:UV变换的相关代码要写在SDF函数调用之前
平移
总之先尝试给uv
的x
和y
加上一些值:
uv.x += .5;
uv.y += .5;
为什么加上值会往左下角移动呢?
我们把uv坐标分布的情况用fragColor画出来就知道了:
之前位于中间的原点值是(0,0)
,现在则变成了(0.5,0.5)
,那么上一个(0,0)
则是跑到了左下角的如图位置,而SDF
函数输入的坐标值的原点值是(0,0)
,正好对应左下方的那个点,因此图形才会整体往左下方移动。
简单来说:要确认SDF
图形位置的变化,要看目前(0,0)
这个点的位置变化。
相反,如果要往右上方平移的话,就用减法就行了,顺带一提可以简化为:
uv -= vec2(.5);
缩放
给UV的x和y坐标做乘运算以及除运算,可以实现缩放效果:
与上面的加减法差不多的,原理一致,不赘述了,贴一张图在这里:
翻转
要实现翻转我们需要找一个三角形的SDF
函数,sdEquilateralTriangle
float sdEquilateralTriangle( in vec2 p, in float r )
{
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r/k;
if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0;
p.x -= clamp( p.x, -2.0*r, 0.0 );
return -length(p)*sign(p.y);
}
...
float d = sdEquilateralTriangle(uv, .5);
float c = smoothstep(0., .02, d);
fragColor = vec4(vec3(c), 1.);
...
我们得到的是一个三角形,要实现翻转,我们只需将uv的y乘上-1就行了。
简单的逻辑原理图是这样的:
之前的虚线三角形的上顶点是(0.,0.5),乘上-1 后就变成了(0.,-0.5),也就变成了实际三角形的下顶点。
旋转
这个效果有点复杂,但是好在,有已经封装好的库,2D相关的旋转函数我们直接拿来用:
mat2 rotation2d(float angle){
float s=sin(angle);
float c=cos(angle);
return mat2(
c,-s,
s,c
);
}
vec2 rotate(vec2 v,float angle){
return rotation2d(angle)*v;
}
我们可以简单地讲解一下这个函数的意义,有一点需要提前了解:在数学和计算机图形学中,二维平面上的旋转通常是默认逆时针方向的。这个约定基于右手坐标系的定义,在这种坐标系中,正角度表示逆时针旋转。
这其实相当于简单的复习了一下学生时代的知识。所以上面的旋转函数的实现就相当于是:
通过二维矩阵的方式对uv进行乘法,得到的就是旋转之后的新的坐标。
另外我们在进行旋转操作时经常会用到PI这个常量,代表 180 度。
const float PI = 3.14159265359;
uv = rotate(uv, PI / 2.);
最后会变成:
在之前的文章中我们提到了iTime
的变量,表示Shader
从开始到现在执行所经过的时间。
我们可以以此来做一个简单的旋转的动画效果:
uv = rotate(uv, iTime)
重复
我们来认识一个新的内置内涵fract
,它的作用很简单,就是获取一个数的小数部分。
比如我们fract(1145.14)
,那我们得到的值就是.14
。
来自2024年7月1日的修正:
因此,fract函数对于负数返回的是其小数部分作为正数,这是由floor函数的特性决定的。
这个方法有什么用呢?我们来试试
用上图的代码运行出来的结果也在右边显示出来了。为什么会这样?
我稍微做一下标记:
将坐标的值乘以2,我图中给出了几个象征意义的关键点,大于1的部分的点的整数部分都被去掉了,剩下的就是他们的小数点部分,这样其实就可以达成重复的效果。
那我们重新回到三角形
来咯,四个小三角。怎么实现的,不用再赘述了吧。注意一点:我是把uv先乘了2.之后,再进行的居中和适应画布。这样就可以得到四个均等的区域,并且大家都是居中对齐的啦。
镜像
关于uv的变换,我们最后再来认识一个函数吧,大家高中有可能听说过但是大学经常会用到的:abs
。它的作用是获取一个数的绝对值。
函数图像长这样:
由此可以看出,两个函数都是基于对应的变量轴对称,由此可以得出结论:这个函数具有“对称性”
那我们来举个例子:
这是镜像之前的:
这个是镜像之后的:
非常明显的差距,原本UV坐标的左下角的部分是负数,现在全部镜像翻转为了正数,大小还和x轴对称的值一样。
那我们获得一个菱形也很简单:三角形翻转一下不就完事儿了
SDF图形变换
除了UV
之外,SDF
本身的形状也是可以改变的,变形函数现阶段咱们可以直接从网上搜罗到,搜不到满足不了需求的,也可以使用AI嘛不是。
咱们还是从之前的图形学大佬iq的博客里拿下来一些函数来用,比如:
float onRound (in float d, in float r) {
return d -r;
}
float opOnion (in float d, in float r) {
return abs(d) - r;
}
onRound
是“圆角”操作,能让图形的角变成圆角
opOnion
是“镂空”操作,能挖空图形中间的部分
SDF布尔运算
SDF
函数本身能绘制出很多的形状,但是也是有限的,我们想画一个梯形,可以用sdTrapezoid
,但是如果我们想画一个类似于人的骨头关节的那种呢?没有对应的SDF
函数是不是就不能画了?NoNoNo,我们可以采用大家都会的
并,交,差运算
同样的,如果自身数学基础不好,没法自己推导的话,可以从Iq大佬的博客copy一份,或者寻求AI的帮助。新时代了,要充分结合AI创造出自己的优势区间
float opUnion(float d1,float d2)
{
return min(d1,d2);
}
float opIntersection(float d1,float d2)
{
return max(d1,d2);
}
float opSubtraction(float d1,float d2)
{
return max(-d1,d2);
}
虽然是copy过来的,但是还是简单的解释一下吧:
opUnion:
实现两个形状的并集,也就是结合两个图形,形成一个新的图形。
原理:对于每个点,取两个图形中,距离最近的那个。- 如果一个点在形状A内部或形状B内部,它就属于新形状。
- 取较小的距离 min(d1, d2) 就实现了这个效果。
opIntersection::实现两个形状的交集,即只保留两个形状共同的部分。
原理:对于每个点,取两个形状中距离最大的那一个。- 如果一个点同时在形状A内部和形状B内部,它才属于新形状。
- 取较大的距离 max(d1, d2) 就实现了这个效果。
opSubtraction::实现两个形状的差集,即从第二个形状中减去第一个形状。
原理:对于每个点,取第一个形状的负距离和第二个形状的距离中较大的那个。- 如果一个点在形状A内部但不在形状B内部,它就属于 新形状。
- 取 max(-d1, d2) 就实现了这个效果。
- 没什么好说的,你就可以理解成把d1的内容取个反,比如一个正方形区域,由一个圆形和其余部分组成,一开始d1是那个圆,但是取负处理之后,就成为了剩余的部分,然后这个差集就是把剩余的部分和第二个参数取了一个交集,就是上面我们说到的那个max,应该豁然开朗了吧?
这是并集运算示例:
这是交集运算示例:
这是差集运算示例:
平滑效果
布尔运算还有一种另外的版本:(smooth)
,能够产生一种更加有机的结果,也即是“粘稠”的感觉。我们可以直接从iq大佬的博客上拿一些示例下来,之后了解的更深了,我们也可以自己写一些SDF函数:
float opSmoothUnion( float d1, float d2, float k )
{
float h = clamp( 0.5+0.5*(d2-d1)/k, 0.0, 1.0 );
return mix( d2, d1, h ) - k*h*(1.0-h);
}
float opSmoothSubtraction( float d1, float d2, float k )
{
float h = clamp( 0.5-0.5*(d2+d1)/k, 0.0, 1.0 );
return mix( d2, -d1, h ) + k*h*(1.0-h);
}
float opSmoothIntersection( float d1, float d2, float k )
{
float h = clamp( 0.5-0.5*(d2-d1)/k, 0.0, 1.0 );
return mix( d2, d1, h ) + k*h*(1.0-h);
}
可以看看这些函数实现的效果:
- 并集:
- 交集:
- 差集: