使用shader绘制线、线段、曲线
- 0.序
- 1. 绘制线
-
- 1.1 绘制水平线
-
- 1.1.1 根据判断条件着色绘制
- 1.1.2 使用step函数绘制
- 1.1.3 使用smoothstep函数绘制
- 1.2 绘制垂直线
- 1.3绘制斜线
-
- 1.3.1使用step函数绘制
- 1.3.2使用smoothstep函数绘制
- 1.4 绘制线段
-
- 1.4.1 通过限制x值范围绘制线段
- 1.4.2 通过起始点和结束点绘制线段
- 2. 绘制曲线
-
- 2. 1 绘制曲线sin函数曲线
- 2. 2 绘制曲线pow函数曲线
- 3. 示例代码
0.序
做三维web开发时,当发现引擎提供的基础几何体或者材质无法满足需求时,就需要用到强大的GLSL语言,也称shader,也是传说中最难过的一道坎,如果你能随心所欲的编写shader,那就是金字塔中顶层的那一波人员,话说不想当将军的士兵不是好士兵,不想学好shader的三维开发人员不是好开发人员,为此,我打算完成一个关于shader学习的系列文章,写博文的好处是可以加深理解、也相当于整理资料方便日后查阅,同时可以帮助需要的人,就让我们从最简单的线、线段、面的绘制开始慢慢成长
此时假设你对shader有一定的了解和基础,如果还没有请点击这里查阅shader入门
学好shader绝对让你受益匪浅,无论是WebGL开发还是通过三维引擎开发项目,让你无所不能,一句话概括就是没有什么需求是一个shader解决不了的,如果有那就来两个
1. 绘制线
1.1 绘制水平线
绘制的方法有很多主要列举以下三种:根据判断条件着色绘制、使用step函数绘制、使用smoothstep函数绘制
1.1.1 根据判断条件着色绘制
原理很简单,就是根据屏幕坐标限定一个范围,着色指定颜色,其余的地方着背景色,这样就绘制出一条线,下面的代码中限定的范围是0.0<y<0.02,显然会绘制出一条水平线
void main( void ) {
//窗口坐标调整为[-1,1],坐标原点在屏幕中心vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;vec3 line_color = vec3(1.0,1.0,0.0);vec3 color = vec3(0.6);//背景色if(st.y > 0.0&&st.y<0.02){
color = line_color;}gl_FragColor = vec4(color, 1);}
运行结果如下
1.1.2 使用step函数绘制
step函数接收两个参数一个是阀值,一个是想要检测的数据,如果检测的数据大于阀值返回1.0,小于阀值返回0.0,例如:res = step(0.5,x); 如果x大于0.5,res = 1.0,如果x小于0.5,res = 0.0;
若执行以下代码
float pct = step(0.0,st.y);
color = color+line_color*pct;
结果是这样
若执行以下代码
float pct = step(0.02,st.y);
color = color+line_color*pct;
得到结果应该是淡黄色部分,之所以显示成这样是为了和step(0.0,st.y)区分
接下来将它们相减
float pct = step(0.0,st.y)-step(0.02,st.y);
color = color+line_color*pct;
怎么结果和第一种方法绘制线的颜色不一样,是因为原本的背景色和线颜色叠加导致的,需要在叠加前从背景色中把线占据的部分给扣掉,然后再叠加
代码如下
float pct = step(0.0,st.y)-step(0.02,st.y);
color = color*(1.0-pct)+line_color*pct;
这样得到的结果和第一种方法绘制的结果就一模一样了
顺便说一下其实也可以直接使用mix函数混合,就可以不用从背景色中扣除了
代码如下
//使用step函数绘制
float pct = step(0.0,st.y)-step(0.02,st.y);
//color = color*(1.0-pct)+line_color*pct;
color = mix(color,line_color,pct);
结果都与第一种方式绘制的一模一样
1.1.3 使用smoothstep函数绘制
smoothstep与step功能类似也是来通过阀值检测数据返回1.0或0.0,它比step函数更强大,它需要三个参数,一个下限阀值,一个上限阀值,一个要检测的数据,如果检测的数据小于下限阀值返回0.0,如果检测的数据大于上限阀值返回1.0,如果介于下限阀值和上限阀值之间,则返回检测数据在两者之间的线性插值在[0,1]的值,例如:res = smoothstep(0.1,0.5,x); 如果x大于0.5,res = 1.0,如果x小于0.1,res = 0.0;如果检测的值为0.3在0.1到0.5之间,假设0.1到0.3为2个单位,0.1到0.5为4个单位,那么2个单位的长度插值在4个单位的总长度中应该是0.5
函数说清楚了直接上代码
//使用smoothstep函数绘制float pct = smoothstep(0.0,0.01,st.y) - smoothstep(0.01,0.02,st.y);color = mix(color,line_color,pct);
从结果中可以发现线的边缘与背景色之间看起来更加融合,没有step函数那样生硬,这是线性插值带来的好处,由于我们绘制的是水平直线,所以没有发现锯齿现象,如果绘制的是斜线,那么使用step绘制的线将会出现明显的锯齿,而使用smoothstep绘制时边缘可以柔和的过渡,其实就是模糊边缘,所以在shader中可以使用smoothstep函数消除锯齿
如果想让边缘锐利一些,使用pow函数加速收敛
float pct = pow(smoothstep(0.0,0.01,st.y),10.0) - pow(smoothstep(0.01,0.02,st.y),10.0);
color = mix(color,line_color,pct);
上述代码执行结果如下,线的边缘不在像没加pow之前那样模糊
1.2 绘制垂直线
绘制垂直线与绘制水平线是同样的思路与方法,只需要将原来代码中的st.y 替换为 st.x 即可
//绘制垂直线if(st.x > 0.0&&st.x < 0.02){
color = line_color;
}//使用step函数绘制
pct = step(0.1,st.x)-step(0.12,st.x);
color = mix(color,line_color,pct);//使用smoothstep函数绘制pct = smoothstep(0.2,0.21,st.x) - smoothstep(0.21,0.22,st.x);color = mix(color,line_color,pct);
结果如下图,分别在不同横坐标位置用之前的三种方法绘制了三条垂直的线
1.3绘制斜线
1.3.1使用step函数绘制
这次我们把绘制斜线的过程封装成一个函数,先来看看使用step函数如何封装
大家都知道使用斜率表示直线的函数是 y = kx + b 我们就利用这个公式封装一个绘制斜线的函数
这次step函数的参数调整为检测的阀值为通过屏幕坐标的x值计算的结果,而要检测的数据则是是屏幕坐标的y值
//斜线
float line_kx(vec2 st,float k,float t,float line_width) {
float y = k*st.x+t;return step(y,st.y) - step(y+line_width,st.y);
}
调用封装函数绘制斜线,我绘制了一条斜率为1过原点的直接和在此基础上向右平移0.3个单位的线
pct = line_kx(st,1.0,0.0,0.02);
color = mix(color,line_color,pct);pct = line_kx(st,1.0,-0.3,0.02);
color = mix(color,line_color,pct);
结果如下图,是我们想要的结果,不过正如上文提到的使用step函数绘制斜线出现了锯齿,所以尽量使用smoothstep函数吧
1.3.2使用smoothstep函数绘制
使用smoothstep函数封装斜线的思路与step函数封装一致,不同的是之前提到的阀值smoothstep变为两个阀值以及使用pow函数使斜线的边缘锐利一些
float line_kx_smooth(vec2 st,float k,float t,float line_width) {
float y = k*st.x+t;return pow(smoothstep(y-line_width,y,st.y) - smoothstep(y,y+line_width,st.y),25.0);
}
调用绘制函数
pct = line_kx_smooth(st,-0.5,0.0,0.04);
color = mix(color,line_color,pct);pct = line_kx_smooth(st,-0.5,0.3,0.04);
color = mix(color,line_color,pct);
执行结果
1.4 绘制线段
1.4.1 通过限制x值范围绘制线段
已经绘制了直接绘制线段不是顺理成章的事嘛,可以简单粗暴的限定x值的大小,具体如下
float line_segment_smooth(vec2 st,float k,float t,float line_width) {
float minX = 0.2;float maxX = 0.6;if(st.x > minX && st.x < maxX){
return line_kx_smooth(st,k,t,line_width);} else {
return 0.0;}
}
调用绘制线段函数
pct = line_segment_smooth(st,1.0,0.2,0.04);
color = mix(color,line_color,pct);
执行结果
线段时成功绘出,可是无法通过起始点来绘制线段,斜率也是不定的这样绘制线段时无法确定它的起始点,操作起来很不方便
1.4.2 通过起始点和结束点绘制线段
通过起始点和结束点绘制线段感觉非常简单,但是在图形学上却没有想象中的那么简单,先说说思路,分两部分一部分是限制绘制范围,另一部分是限制线宽
如下图假设a是线段的起始点,b是线段的结束点,c和c1是屏幕中的任意点,
利用向量点乘的性质 如果两个向量点乘的结果大于0小于1,则两个向量方向相同 ,如有不懂请参照文章 向量、向量加减法、向量的点积 中5.5.2 判断两个向量的方向部分
以穿过a点线与穿过b点的线将屏幕分为三部分,起始点a左侧的部分,a到b的部分(要绘制线段的区域),结束点右侧的部分,以结束点b为例,计算由点b指向点a的向量与向量c点乘,如果大于0,则点在a与b之间的区域内,否则在b点右侧的区域内如点c1,写代码的时候可以通过判断点乘结果是否大于0可以将结束点b右侧区域内的点过滤掉,同理可以过滤掉起始点a左侧的点
接下来说说,如何限制线段的宽度,我们可以利用点乘的性质计算出两个向量的夹角,计算公式如下
求出向量c与结束点指向起始点的向量的夹角θ后,就可以使用sin函数计算出点c到线段的距离h,将这个h限定在线宽的一半范围内就可绘制出相应的线段
原理说完了开始编码,封装绘制线段的函数
float line_segment_with_two_point (vec2 st,vec2 start,vec2 end,float line_width){
vec2 line_vector_from_end = normalize(vec2(start.x,start.y) - vec2(end.x,end.y));//结束点指向起始点的向量vec2 line_vector_from_start = -line_vector_from_end;//起始点指向结束点的向量vec2 st_vector_from_end = st - vec2(end.x,end.y); //结束点指向画布中任意点的向量vec2 st_vector_from_start = st - vec2(start.x,start.y);//起始点指向画布中任意点的向量float proj1 = dot(line_vector_from_end,st_vector_from_end);float proj2 = dot(line_vector_from_start,st_vector_from_start);if(proj1>0.0&&proj2>0.0){
//通过点乘结果>0判断是否同相,过滤掉线段两头超出部分//求结束点指向画布中任意点的向量与结束点指向起始点的向量的夹角float angle = acos(dot(line_vector_from_end,st_vector_from_end)/(length(st_vector_from_end)*length(line_vector_from_end)));//屏幕上任意点到直线的垂直距离float dist = sin(angle)*length(st_vector_from_end);return pow(1.0-smoothstep(0.0,line_width/2.0,dist),6.0);} else {
return 0.0;}
}
调用绘制函数
pct = line_segment_with_two_point(st,vec2(-0.1,0.2),vec2(0.6,0.7),0.04);
color = mix(color,line_color,pct);
绘制结果
2. 绘制曲线
绘制曲线与绘制直线思路基本相似,也是检测曲线函数结果是否超出线宽的一半,如果未超出绘制线的颜色,我们条两个比较常用的曲线函数进行绘制
2. 1 绘制曲线sin函数曲线
首先来看看正弦函数的表达式如下
正弦函数各常数值说明
A:振幅(即纵向拉伸压缩的倍数)
ω:角速度(控制波浪的周期)
φ:初相位
b:偏距,表示波形在Y轴的位置关系
同样将绘制sin曲线封装为函数
float sinLine(vec2 st,float line_width) {
// 振幅float amplitude = 0.08;// 角速度(控制波浪的周期)float angularVelocity = 12.0;// 偏距float b = 0.5;// 初相位float initialPhase = 0.5;//float frequency = 12.0;//initialPhase = frequency * u_time;float y = amplitude * sin((angularVelocity*st.x)+initialPhase)+b;return smoothstep(y-line_width/2.0,y,st.y)-smoothstep(y,y+line_width/2.0,st.y);
}
调用部分
pct = sinLine(st,0.04);
color = mix(color,line_color,pct);
执行结果如下
如果把封装的函数内部这两行代码放开,就会看到正弦曲线动起来,原理就是传入一个时间,随着时间的变换初相位不断变化
//float frequency = 12.0;
//initialPhase = frequency * u_time;
2. 2 绘制曲线pow函数曲线
这个函数就不讲解了,直接上代码
float powLine(vec2 st,float line_width) {
float y = pow(st.x,5.0);return smoothstep(y-line_width/2.0,y,st.y)-smoothstep(y,y+line_width/2.0,st.y);
}
调用
pct = powLine(st,0.04);
color = mix(color,line_color,pct);
结果是这样的
3. 示例代码
<body><div id="container"></div><script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script><script>var container;var camera, scene, renderer;var uniforms;var vertexShader = `void main() {gl_Position = vec4( position, 1.0 );} `var fragmentShader = `#ifdef GL_ESprecision mediump float;#endifuniform float u_time;uniform vec2 u_mouse;uniform vec2 u_resolution;//斜线float line_kx(vec2 st,float k,float t,float line_width) {float y = k*st.x+t;return step(y,st.y) - step(y+line_width,st.y);}float line_kx_smooth(vec2 st,float k,float t,float line_width) {float y = k*st.x+t;return pow(smoothstep(y-line_width,y,st.y) - smoothstep(y,y+line_width,st.y),25.0);}float line_segment_smooth(vec2 st,float k,float t,float line_width) {float minX = 0.2;float maxX = 0.6;if(st.x > minX && st.x < maxX){return line_kx_smooth(st,k,t,line_width);} else {return 0.0;}}float line_segment_with_two_point (vec2 st,vec2 start,vec2 end,float line_width){vec2 line_vector_from_end = normalize(vec2(start.x,start.y) - vec2(end.x,end.y));//结束点指向起始点的向量vec2 line_vector_from_start = -line_vector_from_end;//起始点指向结束点的向量vec2 st_vector_from_end = st - vec2(end.x,end.y); //结束点指向画布中任意点的向量vec2 st_vector_from_start = st - vec2(start.x,start.y);//起始点指向画布中任意点的向量float proj1 = dot(line_vector_from_end,st_vector_from_end);float proj2 = dot(line_vector_from_start,st_vector_from_start);if(proj1>0.0&&proj2>0.0){//通过点乘结果>0判断是否同相,过滤掉线段两头超出部分//求结束点指向画布中任意点的向量与结束点指向起始点的向量的夹角float angle = acos(dot(line_vector_from_end,st_vector_from_end)/(length(st_vector_from_end)*length(line_vector_from_end)));//屏幕上任意点到直线的垂直距离float dist = sin(angle)*length(st_vector_from_end);return pow(1.0-smoothstep(0.0,line_width/2.0,dist),6.0);} else {return 0.0;}}float sinLine(vec2 st,float line_width) {// 振幅float amplitude = 0.08;// 角速度(控制波浪的周期)float angularVelocity = 12.0;// 偏距float b = 0.5;// 初相位float initialPhase = 0.5;//float frequency = 12.0;//initialPhase = frequency * u_time;float y = amplitude * sin((angularVelocity*st.x)+initialPhase)+b;return smoothstep(y-line_width/2.0,y,st.y)-smoothstep(y,y+line_width/2.0,st.y);}float powLine(vec2 st,float line_width) {float y = pow(st.x,5.0);return smoothstep(y-line_width/2.0,y,st.y)-smoothstep(y,y+line_width/2.0,st.y);}float circleLine(vec2 st,vec2 center,float radius) { float pct = distance(st,center);float line_width = 0.02;return step(radius-line_width,pct) - step(radius,pct);}float circleLine_smooth(vec2 st,vec2 center,float radius) { float pct = distance(st,center);float line_width = 0.02;return (1.0-smoothstep(radius,radius+line_width,pct))-(1.0-smoothstep(radius-line_width,radius,pct));}void main( void ) {//窗口坐标调整为[-1,1],坐标原点在屏幕中心vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;vec3 line_color = vec3(1.0,1.0,0.0);vec3 color = vec3(0.6);//背景色float pct = 0.0;//绘制水平线// if(st.y > 0.0&&st.y < 0.02){// color = line_color;// }//使用step函数绘制// pct = step(0.0,st.y)-step(0.02,st.y);// //color = color*(1.0-pct)+line_color*pct;// color = mix(color,line_color,pct);//使用smoothstep函数绘制// pct = smoothstep(0.0,0.01,st.y) - smoothstep(0.01,0.02,st.y);// pct = pow(smoothstep(0.0,0.01,st.y),10.0) - pow(smoothstep(0.01,0.02,st.y),10.0);// color = mix(color,line_color,pct);//绘制垂直线if(st.x > 0.0&&st.x < 0.02){color = line_color;}//使用step函数绘制pct = step(0.1,st.x)-step(0.12,st.x);color = mix(color,line_color,pct);//使用smoothstep函数绘制pct = smoothstep(0.2,0.21,st.x) - smoothstep(0.21,0.22,st.x);color = mix(color,line_color,pct);pct = 0.0;//清除绘制的线条color = vec3(0.6);pct = line_kx(st,1.0,0.0,0.02);//color = mix(color,line_color,pct);pct = line_kx(st,1.0,-0.3,0.02);//color = mix(color,line_color,pct);pct = line_kx_smooth(st,-0.5,0.0,0.04);//color = mix(color,line_color,pct);pct = line_kx_smooth(st,-0.5,0.3,0.04);//color = mix(color,line_color,pct);pct = line_segment_smooth(st,1.0,0.2,0.04);//color = mix(color,line_color,pct);pct = line_segment_with_two_point(st,vec2(-0.1,0.2),vec2(0.6,0.7),0.04);//color = mix(color,line_color,pct);pct = sinLine(st,0.04);//color = mix(color,line_color,pct);pct = powLine(st,0.04);color = mix(color,line_color,pct);gl_FragColor = vec4(color, 1);}`init();animate();function init() {
container = document.getElementById('container');camera = new THREE.Camera();camera.position.z = 1;scene = new THREE.Scene();var geometry = new THREE.PlaneBufferGeometry(2, 2);uniforms = {
u_time: {
type: "f",value: 1.0},u_resolution: {
type: "v2",value: new THREE.Vector2()},u_mouse: {
type: "v2",value: new THREE.Vector2()}};var material = new THREE.ShaderMaterial({
uniforms: uniforms,vertexShader: vertexShader,fragmentShader: fragmentShader});var mesh = new THREE.Mesh(geometry, material);scene.add(mesh);renderer = new THREE.WebGLRenderer();//renderer.setPixelRatio(window.devicePixelRatio);container.appendChild(renderer.domElement);onWindowResize();window.addEventListener('resize', onWindowResize, false);document.onmousemove = function (e) {
uniforms.u_mouse.value.x = e.pageXuniforms.u_mouse.value.y = e.pageY}}function onWindowResize(event) {
renderer.setSize(800, 800);uniforms.u_resolution.value.x = renderer.domElement.width;uniforms.u_resolution.value.y = renderer.domElement.height;}function animate() {
requestAnimationFrame(animate);render();}function render() {
uniforms.u_time.value += 0.02;renderer.render(scene, camera);}</script>
</body>