标签:
写前面一篇的时候,发现还是不够基础。因此打算增加几篇基础篇,从点线面开始,希望可以更好理解。
其实用Pixel Shader的过程很像在纸上绘画的过程。屏幕上的每一个像素对应了纸上的一个方格,如果你愿意,你甚至可以一个个判断像素的位置,从而画出任何你想画的图像,也的确有爱好者这么做过。但往往,我们需要的是一个动态的效果,这个效果往往依赖于数学公式的约束。我们可以说是,用数学去绘画。我们用数学去约束,哪些点应该用什么颜色去绘制。
这篇,我们从基本的点和线开始,看一下如何在Pixel Shader里面随心画出点和线。
在开始之前,有一个最基本的问题我们要计算清楚,就是如何知道当前计算的fragment的像素位置。在之前的开篇中,我们给出了模板。其中v2f结构里,有一个很重要的变量srcPos,它的计算如下:
v2f vert(appdata_base v) {
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.srcPos = ComputeScreenPos(o.pos);
o.w = o.pos.w;
return o;
} fixed4 frag(v2f _iParam) : COLOR0 {
vec2 fragCoord = gl_FragCoord;
return main(gl_FragCoord);
} // 屏幕的尺寸 #define iResolution _ScreenParams // 屏幕中的坐标,以pixel为单位 #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy)
根据不同的需求,我们会在shader中对位置有不同的需求:有时我们想要得到如上的像素位置,有时我们想得到相对于屏幕中心的uv坐标等等。以下有五种常见的位置需求:
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)
vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)
vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)
vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)
return vec4(1);
}在Shader中,一个点其实可以当成一个圆形。那么问题就变成了如何绘制一个圆:给定圆心在屏幕的位置(圆心像素值占屏幕的百分比),圆的半径(像素为单位),以及圆的颜色,如何绘制一个圆。
为此,我们先需要在Properties中声明两个参数:_Parameters和_Color:
Shader "shadertoy/Simple Circle" {
Properties{
_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 0) // Center: (x, y), Radius: z
_Color ("Circle Color", Color) = (1, 1, 1, 1)
}圆在数学里面的表达式相对简单,即到圆心距离小于半径的点就在圆内。那么事情就变得简单了:只要在圆内的点就是用设置的颜色绘制,否则用背景色绘制(黑色)。
vec4 circle(vec2 pos, vec2 center, float radius, float4 color) {
if (length(pos - center) < radius) {
// In the circle
return vec4(1, 1, 1, 1) * color;
} else {
return vec4(0, 0, 0, 1);
}
} vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
// vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)
// vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)
// vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)
// vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)
return circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _Color);
}
上面得到的圆在边缘处有一些小锯齿,这当然是我们无法忍受的拉!Shader中抗锯齿的原理大概是这样:由于原来非A即B的计算会使得A和B的交界处产生锯齿(例如上面圆的边界),因此我们只需要在A和B的边界平缓过渡即可。这往往需要透明度的配合,即使用透明度来混合颜色。
在shader中,一种常见的抗锯齿(平滑)操作是使用smoothstep函数(当然有人诟病这种方法不直观,但我觉得挺好用的。。。whatever~)。smoothstep函数在CG文档里面是这样的:
Interpolates smoothly from 0 to 1 based on x compared to a and b. 1) Returns 0 if x < a < b or x > a > b 1) Returns 1 if x < b < a or x > b > a 3) Returns a value in the range [0,1] for the domain [a,b].
这样,我们就可以改写原来的circle函数:
vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
float d = length(pos - center) - radius;
float t = smoothstep(0, antialias, d);
return vec4(color, 1.0 - t);
}antialias就是平滑过渡的边界范围。为了方便调试,我们可以在shader中利用_Parameters的z分量作为抗锯齿因子,当然在实际工程中可以设为定值。接下来就是和背景颜色进行混合,我们使用的是lerp函数(在ShaderToy中对应的是mix函数):
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);
return mix(layer1, layer2, layer2.a);
}Shader "shadertoy/Simple Circle" {
Properties{
_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 1) // Center: (x, y), Radius: z
_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
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)
#define PI2 6.28318530718
#define pi 3.14159265358979
#define halfpi (pi * 0.5)
#define oneoverpi (1.0 / pi)
float4 _Parameters;
float4 _CircleColor;
float4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
float4 srcPos : TEXCOORD0;
};
// precision highp float;
v2f vert(appdata_base v) {
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.srcPos = ComputeScreenPos(o.pos);
return o;
}
vec4 main(vec2 fragCoord);
fixed4 frag(v2f _iParam) : COLOR0 {
vec2 fragCoord = gl_FragCoord;
return main(gl_FragCoord);
}
vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
float d = length(pos - center) - radius;
float t = smoothstep(0, antialias, d);
return vec4(color, 1.0 - t);
}
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);
return mix(layer1, layer2, layer2.a);
}
ENDCG
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
FallBack Off
}
我们现在来看看如何画出更多的点。之前的circle函数已经可以画出任何一个大小、圆心的圆了,现在的问题仅仅是如何将这些元素都添加到画布上。一种基本的思想就是图层叠加,这很像我们在Photoshop中做的事情:背景在最后一层,我们只需要增加新的图层,并确保它们按层级顺序一层层向上排列即可。所以,我们可以这样做:
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
vec2 point1 = vec2(0.3, 0.8);
vec2 point2 = vec2(0.8, 0.2);
vec4 layer2 = circle(pos, point1 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);
vec4 layer3 = circle(pos, point2 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);
vec4 fragColor = mix(layer1, layer2, layer2.a);
fragColor = mix(fragColor, layer3, layer3.a);
return fragColor;
}
这种思想可以延伸到任意层数的元素叠加~
现在,我们已经知道了直线上的两个点,那么我们来看看如何画出过这两个点的直线。首先,我们可以声明一个空的画直线的函数,并且添加一个新的图层给它,后续再填充这个函数:
vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
return vec4(0);
}
vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
float d = length(pos - center) - radius;
float t = smoothstep(0, antialias, d);
return vec4(color, 1.0 - t);
}
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec2 point1 = vec2(0.3, 0.8) * iResolution.xy;
vec2 point2 = vec2(0.8, 0.2) * iResolution.xy;
vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);
vec4 layer3 = circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);
vec4 layer4 = circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);
vec4 fragColor = mix(layer1, layer2, layer2.a);
fragColor = mix(fragColor, layer3, layer3.a);
fragColor = mix(fragColor, layer4, layer4.a);
return fragColor;
}为了方便,上面新定义了几个参数。后面会给出完整代码,但我相信这里也很好懂。对于图层的顺序我也进行了调增,即第二层为直线,后面是点图层,这是因为我们希望点的颜色可以覆盖直线颜色。保存后返回查看结果,是没有任何变化的,因为此时直线图层返回的颜色的透明度为0。
注意了!!!现在又到了数学魅力展现的时刻了!!!绘制直线的思想其实和圆很类似,我们只需要判断像素位置是否在直线内(因为这里的直线是有宽度的)就可以了:判断像素点到直线的距离是否小于直线宽度的一半。那么,我们只需要回想起当年的点到直线距离公式。你是不是忘记了!!!这里直接给出答案了,公式也很简单:
vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
float k = (point1.y - point2.y)/(point1.x - point2.x);
float b = point1.y - k * point1.x;
float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);
float t = smoothstep(width/2.0, width/2.0 + antialias, d);
return vec4(color, 1.0 - t);
}很简单有木有!!!效果如下:
完整的代码如下:
Shader "shadertoy/Simple Line" {
Properties{
_CircleRadius ("Circle Radius", float) = 5
_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)
_LineWidth ("Line Width", float) = 5
_LineColor ("Line Color", Color) = (1, 1, 1, 1)
_Antialias ("Antialias Factor", float) = 3
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
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)
#define PI2 6.28318530718
#define pi 3.14159265358979
#define halfpi (pi * 0.5)
#define oneoverpi (1.0 / pi)
float _CircleRadius;
float4 _CircleColor;
float _LineWidth;
float4 _LineColor;
float _Antialias;
float4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
float4 srcPos : TEXCOORD0;
};
// precision highp float;
v2f vert(appdata_base v) {
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.srcPos = ComputeScreenPos(o.pos);
return o;
}
vec4 main(vec2 fragCoord);
fixed4 frag(v2f _iParam) : COLOR0 {
vec2 fragCoord = gl_FragCoord;
return main(gl_FragCoord);
}
vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
float k = (point1.y - point2.y)/(point1.x - point2.x);
float b = point1.y - k * point1.x;
float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);
float t = smoothstep(width/2.0, width/2.0 + antialias, d);
return vec4(color, 1.0 - t);
}
vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
float d = length(pos - center) - radius;
float t = smoothstep(0, antialias, d);
return vec4(color, 1.0 - t);
}
vec4 main(vec2 fragCoord) {
vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
vec2 point1 = vec2(0.4, 0.1) * iResolution.xy;
vec2 point2 = vec2(0.7, 0.8) * iResolution.xy;
vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);
vec4 layer3 = circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);
vec4 layer4 = circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);
vec4 fragColor = mix(layer1, layer2, layer2.a);
fragColor = mix(fragColor, layer3, layer3.a);
fragColor = mix(fragColor, layer4, layer4.a);
return fragColor;
}
ENDCG
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
FallBack Off
}
这篇写的很基础,但有些知识是值得真实项目里借鉴的。有些人说ShaderToy里的只是玩具,没有价值,我个人是不这么觉得啦~游戏里很多细腻的动画效果是无法仅仅靠贴图来完成的,了解些基本或者稍微复杂一点的shader计算还是很有好处滴~
最后,关于ShaderToy的用处,还有一点就是我们可以自己改进成非Pixel Shader的版本,例如利用模型的uv坐标去代替屏幕坐标等等。更多的用处等待自己去发现啦!
标签:
原文地址:http://blog.csdn.net/candycat1992/article/details/44244549