卡通风格着色器及描边效果实现(基于WebGL 2.0实现)
在开发过程中,我们并不总是以真实效果作为追求,最近在学习的过程中,看到了一些非真实质感的开发实现。本次带来一个球体的卡通效果及描边的demo。
先看一下效果图:
接下来,简单的阐述一下其中的原理吧。
1.卡通风格的开发,首先需要对物体进行普通的光照计算,得到物体各个区域的的结果光照强度,然后根据光照的强度对物体颜色进行赋值。
提示:我分了5个等级,光照强度我取的都是大于1的部分,demo中的光我采用的是散射光。
2.物体描边效果的实现,其开发要点是确定当前着色的片元是否处在边缘的位置,这里介绍一种比较简单的方法,就是计算视线向量与物体表面的夹角,若夹角大于一定的值则认为是边缘。
提示:计算的时候我们只需要求出视线向量(摄像机位置减去顶点位置)和法线向量,并对其进行单位化,然后两个向量做点积即可。
然后看下demo的核心代码,demo是基于WebGL 2.0开发,最重要的是顶点着色器和片元着色器的开发,着色器为version 300es,然后代码涉及的曲面构建还有将矩阵和坐标数组等送入渲染管线的部分就暂时不讲了,比较简单,涉及到底层的API,大家可以结合《WebGL编程指南》和khronos的 WebGL 2.0官方文档进行学习。
顶点着色器:
#version 300 es
uniform mat4 uMVPMatrix; //总变换矩阵
uniform mat4 uMMatrix; //变换矩阵
uniform vec3 uCamera; //摄像机位置
uniform vec3 uLightLocationSun; //光源位置
in vec3 aPosition; //顶点位置
in vec3 aNormal; //法向量
out float vDiffuse; //用于传递给片元着色器的变量
out float vStroke; //描边系数
//光照及描边系数计算的方法
void pointLight(
in vec3 normal, //法向量
out float diffuse, //方向光最终强度
out float stroke, //描边系数
in float lightDiffuse //光照强度
){
//计算变换后的法向量
vec3 normalTarget=aPosition+normal;
vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
//对法向量规格化
newNormal=normalize(newNormal);
//计算从表面点到摄像机的向量
vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
//计算描边系数
stroke=max(0.0,dot(newNormal,eye));
//计算从表面点到光源位置的向量vp
vec3 vp= normalize(uLightLocationSun-(uMMatrix*vec4(aPosition,1)).xyz);
//格式化vp
vp=normalize(vp);
//求法向量与vp的点积与0的最大值
float nDotViewPosition=max(0.0,dot(newNormal,vp));
//计算散射光的最终强度
diffuse=lightDiffuse*nDotViewPosition;
}
void main(){
//根据总变换矩阵计算此次绘制此顶点位置
gl_Position = uMVPMatrix * vec4(aPosition,1);
//计算光照及描边系数并传给片元着色器
pointLight(normalize(aNormal),vDiffuse,vStroke,1.0);
}
片元着色器:
#version 300 es
precision mediump float; //给出浮点默认精度
in float vDiffuse; //接收从顶点着色器过来散射光最终强度
out vec4 fragColor; //传递到渲染管线的片元颜色
in float vStroke; //描边系数
void main()
{
//默认颜色
vec3 color=vec3(0.0,0.0,1.0);
float diffuse=vDiffuse;
//根据最初光照强度来确认每部分的最终光照强度
if(diffuse>0.8){ diffuse=1.0; }
else if(diffuse>0.6){ diffuse=0.8; }
else if(diffuse>0.4){ diffuse=0.6; }
else if(diffuse>0.2){ diffuse=0.4; }
else { diffuse=0.25; }
//描边颜色
const vec4 strokeColor=vec4(0.0,0.0,0.0,1.0);
//判断是否进行描边
float finalStroke=step(0.25,vStroke);
//输出片元颜色
fragColor=finalStroke*vec4(color*diffuse,1.0)+(1.0-finalStroke)*strokeColor;
}
代码非常简单了,当然卡通及描边效果的开发还有很多种开发方法。本文中的描边效果并未使用阶梯性变化的纹理,不过实现的大致思路差不多;描边效果的实现还有跟多种,例如沿法线挤出(就是在物体的周围进行绘制,描边的坐标就是物体坐标沿法线加个值),还有在摄像机空间挤出(就是摄像机空间下进行沿法线挤出的操作)等。大家可以进行尝试开发。