标签:
注:如果你还不了解ShaderToy,请看开篇。
作为ShaderToy系列的第一篇,我们先来点简单的。下面是效果:
(CSDN目前不能传gif文件了,暂时空缺,可以看下面的原shader效果,是一样的)
原Shader地址:https://www.shadertoy.com/view/XsfGRn
我们使用了之前的开篇中的基础模板。这里仅仅给出main函数的代码:
vec4 main(vec2 fragCoord) { vec2 p = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.y,iResolution.x); p.y -= 0.25; // background color vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p)); // animate float tt = mod(iGlobalTime,1.5)/1.5; float ss = pow(tt,.2)*0.5 + 0.5; ss = 1.0 + ss*0.5*sin(tt*6.2831*3.0 + p.y*0.5)*exp(-tt*4.0); p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5); // shape float a = atan(p.x,p.y)/3.141593; float r = length(p); float h = abs(a); float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h); // color float s = 1.0-0.5*clamp(r/d,0.0,1.0); s = 0.75 + 0.75*p.x; s *= 1.0-0.25*r; s = 0.5 + 0.6*s; s *= 0.5+0.5*pow( 1.0-clamp(r/d, 0.0, 1.0 ), 0.1 ); vec3 hcol = vec3(1.0,0.5*r,0.3)*s; vec3 col = mix( bcol, hcol, smoothstep( -0.01, 0.01, d-r) ); return vec4(col,1.0); }
这部分比较简单。这个背景实际上是一个某点为中心,中心最亮、向边缘逐渐变暗的背景,比较常见。但通常我们都是用美术给的纹理直接用,现在我们用纯数学计算来看看如何实现它!
首先,我们在看一个重要变量的计算——p。
vec2 p = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.y,iResolution.x); p.y -= 0.25;
在p.y减去0.25之前,中心点即是屏幕中心,代码先计算了每个像素点到中心的方向(如左图),然后再除以屏幕的高度(或宽度)(如右图)。图例如下:
这样的计算结果可以保存每个像素点距离中心的方向、远近等信息。
在这个基础上,我们还可以移动中心点的位置来控制渐变。代码中是将y减去了0.25,即将中心点向上移动了屏幕高度的0.25/2=0.125个单位。
我们可以通过更改代码来看到这样的效果:
vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-length(p));
return vec4(bcol,1.0);
以上步骤即可得到屏幕上每一个点到中心点的方向、相对距离等信息。接下来,我们就可以根据这些信息计算背景颜色了:
// background color vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p));
心形的计算代码很简单。这里画心的原理是,判断该像素点是否在心的内部,如果在就是用心的颜色绘制,否则使用背景颜色绘制:
vec3 col = mix( bcol, hcol, smoothstep( -0.01, 0.01, d-r) );
smoothstep控制模糊效果的原理在于,在心形的边界部分,d-r的值在正负0左右波动,我们可以通过添加一个[-0.01, 0.01]范围内的平缓过渡,来平缓d-r的值在正负交界处的突变。当然,我们可以更改0.01的值,来控制模糊范围。例如,如果我们改成0.05,那么效果如下:
重要:mix+smoothstep的组合,是实现这种模糊效果的很常见的搭配!
下面,我们来解释,为什么d-r表示该像素是否在心形内。相关代码如下:
// shape float a = atan(p.x,p.y)/3.141593; float r = length(p); float h = abs(a); float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
这样,我们就可以通过判断该像素对应方向的长度r和a的差来判断该像素是否在心形内了。如果我们以a-r为依据而非d-r,我们会得到下面的结果:
可以看出,心形只有一侧,而且看起来比最终效果要胖了许多。这就是h和d的意义。h对a取绝对值,使得a的负值区域同样可以得到半个心形。而d对h进行修正,使用的函数表达式可以根据代码看出来,至于为什么是这个函数,大家都膜拜数学的魅力好啦。。。
心的颜色这里有点难理解。我们把它拆成两部分:
vec3 hcol = vec3(1.0,0.5*r,0.3)*s;
这个渐变色没有考虑心形的约束。而s的计算则相对复杂。它用于修正上面的渐变颜色,使得在心形内外的颜色有所区别。vec3 hcol = vec3(1.0,0.5,0.3)*s;的结果如下:
s的计算如下:
// color float s = 1.0-0.5*clamp(r/d,0.0,1.0); s = 0.75 + 0.75*p.x; s *= 1.0-0.25*r; s = 0.5 + 0.6*s; s *= 0.5+0.5*pow( 1.0-clamp(r/d, 0.0, 1.0 ), 0.1 );
从图片我们可以直观的看出,第一行根据p的x方向来得到一个在x方向上的渐变,第二行在此基础上添加了根据p的距离来产生的渐变,虽然不明显但可以看出新的渐变有了弧形,第三行则是使用类似半兰伯特的方法,增亮了左侧暗部区域,而最后一行则关键的分出了心形内外的区域颜色。
重要:最后一步中,使用pow+clamp分割区域的方法也是我们值得借鉴的地方。
动画的部分肯定离不开数学函数图像的帮助。我们来看这里用到的计算:
// animate float tt = mod(iGlobalTime,1.5)/1.5; float ss = pow(tt,.2)*0.5 + 0.5; ss = 1.0 + ss*0.5*sin(tt*6.2831*3.0 + p.y*0.5)*exp(-tt*4.0); p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5);
下面ss的计算就是利用函数图像来模拟动画效果。它的函数图像如图所示(这里去掉了p.y的影响部分):
如图,这个函数是一个在y轴上、数值1附近跳动的函数。
最后一行,p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5);,我们对x和y方向使用了的参数,是因为我们想要心是在水平方向上收缩、竖直方向上拉伸的动画效果,当然我们也可以修改它。
我将一些参数设为shader的属性,以便在面板中调节。这些属性有:背景颜色,心的颜色,背景的离散洗漱,心的边缘模糊洗漱,以及跳动的时间周期。
Shader "shadertoy/Heart" { Properties { _BackgroundColor ("Background Color", Color) = (1.0, 0.8, 0.7, 1.0) _HeartColor ("Heart Color", Color) = (1.0, 0.5, 0.3, 1.0) _Eccentricity ("Eccentricity", Range(0, 0.5)) = 0.25 _Blur ("Edge Blur", Range(0, 0.3)) = 0.01 _Duration ("Duration", Range(0.5, 10.0)) = 1.5 } CGINCLUDE #include "UnityCG.cginc" #pragma target 3.0 #define vec2 float2 #define vec3 float3 #define vec4 float4 #define mat2 float2x2 #define iGlobalTime _Time.y #define mod fmod #define mix lerp #define atan atan2 #define fract frac #define texture2D tex2D // 屏幕的尺寸 #define iResolution _ScreenParams // 屏幕中的坐标,以pixel为单位 #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) vec4 _BackgroundColor; vec4 _HeartColor; float _Eccentricity; float _Blur; float _Duration; struct vertOut { float4 pos : SV_POSITION; float4 srcPos : TEXCOORD0; }; vertOut vert(appdata_base v) { vertOut o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); o.srcPos = ComputeScreenPos(o.pos); return o; } vec4 main(vec2 fragCoord); fixed4 frag(vertOut _iParam) : COLOR0 { vec2 fragCoord = gl_FragCoord; return main(fragCoord); } vec4 main(vec2 fragCoord) { vec2 p = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.y,iResolution.x); p.y -= 0.25; // background color vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p)); bcol = _BackgroundColor.xyz * (1.0-_Eccentricity*length(p)); // animate float tt = mod(iGlobalTime,1.5)/1.5; tt = mod(iGlobalTime,_Duration)/_Duration; float ss = pow(tt,.2)*0.5 + 0.5; ss = 1.0 + ss*0.5*sin(tt*6.2831*3.0 + p.y*0.5)*exp(-tt*4.0); p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5); // shape float a = atan(p.x,p.y)/3.141593; float r = length(p); float h = abs(a); float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h); // color float s = 1.0-0.5*clamp(r/d,0.0,1.0); s = 0.75 + 0.75*p.x; s *= 1.0-0.25*r; s = 0.5 + 0.6*s; s *= 0.5+0.5*pow( 1.0-clamp(r/d, 0.0, 1.0 ), 0.1 ); vec3 hcol = vec3(1.0,0.5*r,0.3)*s; hcol = _HeartColor.xyz *s; vec3 col = mix( bcol, hcol, smoothstep( -0.01, 0.01, d - r) ); col = mix( bcol, hcol, smoothstep( -_Blur, _Blur, d - r) ); return vec4(col, 1.0); } ENDCG SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest ENDCG } } FallBack "Diffuse" }
当我们把动画速度放得很慢后,动画效果像一个装了水的心在弹动一样,也很有趣。
ShaderToy里面大部分shader是不可以直接拿来用的,因为它的很多效果完全靠数学计算,消耗很大。当然,这也正跟它的名字一样,玩具嘛,数学+shader=牛人的玩具~
从这篇还有之前的几篇可以看出数学在shader中的重要性。几乎所有的动画都是依靠数学公式来完成的。数学提供给我们很多变化,像这里心的跳动,我们也完全可以使用其他跳动函数来模拟。我们还可以看出来,shader中很多也是经验公式,例如里面的很多参数,我们也是都可以自己修改的。
数学不好真的是硬伤,哎。还是那句话,多看看长长见识总没坏处。
标签:
原文地址:http://blog.csdn.net/candycat1992/article/details/44040273