7.1 雾化
7.1.1 概念
雾化,用来表示距离越远看的越模糊的现象。比如在大雾的天气里,距离我们越远的地方我们越看不清楚,如果距离足够远的话,我们只能看到白茫茫的一片,全是雾的颜色,看不见物体。
WebGL使用雾化来实现这种逐渐模糊的效果。WebGL雾化实现是通过某点和视点之间的距离,距离越远雾化程度越高。这种雾化也称为线性雾化。
某一点的雾化程度也成为了雾化因子。
这个图比较容易理解雾化,在垂直于X轴的虚线,它表示我们看见物体没有一丝模糊的最大距离,也就是雾化起点,超过了就开始出现模糊的效果,也就是雾化。到了图上的雾化终点就会完全看不见。在雾化起点到雾化终点就会形成一个线性比例,雾化的程度根据由距离而定。
雾化因子计算:雾化因子 = (终点 - 当前点) / (终点 - 起点)
物体颜色计算:颜色 = 物体颜色 * 雾化因子 + 雾化颜色 * (1 - 雾化因子)
7.1.2 给场景添加雾化效果
因为雾化这个效果最终影响的是我们所看到的物体颜色,因此这个计算过程是在片元着色器中进行,而不是顶点着色器。
1、计算雾化因子
根据雾化因子的公式我们知道它需要3个参数:起点、终点、当前点。起点和终点通过外部传参进来,而当前点的位置可以通过顶点着色器传过来,因为顶点着色器专门负责渲染物体的位置。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
varying float vDist;
uniform mat4 mat;
void main() {
......
// 顶点的世界坐标
vec4 vertexPosition = mat * aPosition;
gl_Position = vertexPosition;
vDist = gl_Position.w;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
varying float vDist;
// 起点到终点的距离[x, y] x代表起点 y代表终点
uniform vec2 uFogDist;
void main() {
// 计算雾化因子
float fogFactor = (uFogDist.y - vDist) / (uFogDist.y - uFogDist.x);
}
`;
const start = 0;
const end = 200;
const fogDist = new Float32Array([start, end]);
const uFogDist = gl.getUniformLocation(program, 'uFogDist');
注意:这里的当前点位置,取的不是点的坐标,而是它的齐次坐标gl_Position.w。
一个点的坐标是(x, y, z, w),xyz分别对应3个轴的坐标,而w表示的是齐次坐标。
什么是齐次坐标呢?在数学的概念中,一个平面的两条平行直线永不相交,但在投影空间却不是这样的。比如下面的铁轨,它们本身是平行的,但在无穷远处,两条铁轨相交汇合为一点!
我们可以得出一个概念,就是在投影空间里,当这个点变得无限远的时候,它们就有可能会相交,用数学的角度来表示的话,它们的坐标就是(∞,∞)。
那么如何把一个坐标变成(∞,∞)呢?我们可以通过让x和y分别除以一个值来实现,假设这个值为w,如果w为1的情况下,那么(x/1, y/1)是没有变化的,但如果w为0.000001,那么(x/0.000001, y/0.000001),此时这个坐标就会变得很大,以此类推,如果w为无限小的时候,坐标就会变成(∞,∞)。
简单说来,齐次坐标就是在原有的坐标维度上再添加一个维度,(x, y) => (x, y, 1)。解释完这个齐次坐标的概念之后,回归到雾化的公式上,它读取点的当前位置读的是齐次坐标。
2、计算雾化后的物体颜色
雾化后的物体颜色 = 物体颜色 * 雾化因子 + 雾化颜色 * (1 - 雾化因子)
目前我们已经把雾化因子计算出来了,物体本身的颜色在顶点着色器中使用光照计算好了,而雾化颜色通过外部传参进来,这样3者具备就可以计算雾化后的物体颜色。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute vec4 aNormal;
varying vec4 vColor;
varying float vDist;
uniform mat4 mat;
void main() {
// 定义点光源的颜色
vec3 uPointLightColor = vec3(1.0,1.0,0.0);
// 点光源的位置
vec3 uPointLightPosition = vec3(-5.2,5.6,5.0);
// 环境光
vec3 uAmbientLightColor = vec3(0.2,0.2,0.2);
// 物体表面的颜色
vec4 aColor = vec4(1.0,0.0,0.0,1.0);
// 顶点的世界坐标
vec4 vertexPosition = mat * aPosition;
// 点光源的方向
vec3 lightDirection = normalize(uPointLightPosition - vec3(vertexPosition));
// 环境反射
vec3 ambient = uAmbientLightColor * vec3(aColor);
// 计算入射角 光线方向和法线方向的点积
float dotDeg = dot(lightDirection, vec3(aNormal));
// 漫反射光的颜色
vec3 diffuseColor = uPointLightColor * vec3(aColor) * dotDeg;
gl_Position = vertexPosition;
vColor = vec4(ambient + diffuseColor, aColor.a);
vDist = gl_Position.w;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
varying vec4 vColor;
varying float vDist;
// 雾化颜色
uniform vec3 uFogColor;
// 起点到终点的距离[x, y] x代表起点 y代表终点
uniform vec2 uFogDist;
void main() {
// 计算雾化因子
float fogFactor = (uFogDist.y - vDist) / (uFogDist.y - uFogDist.x);
// 雾化后的物体颜色 = 物体颜色*雾化因子+雾化颜色*(1-雾化因子)
// mix 线性混合计算 mix(x,y,a) => { x * (1-a) + y * a }
vec3 color = mix(uFogColor, vec3(vColor), fogFactor);
gl_FragColor = vec4(color, vColor.a);
}
`;
const start = 0;
const end = 200;
const fogColor = new Float32Array([0.0,0.0,0.0]);
const fogDist = new Float32Array([start, end]);
const uFogColor = gl.getUniformLocation(program, 'uFogColor');
const uFogDist = gl.getUniformLocation(program, 'uFogDist');
gl.uniform3fv(uFogColor, fogColor);
注意:这里引入了一个新的概念:mix线性混合计算。
mix函数用于混合两个颜色得到新的颜色。有3个参数分别是颜色1,颜色2,以及混合比例。它的计算公式为:mix(x, y, a) = x * (1-a) + y * a。
当a为0的时候,mix(x, y, 0.0) = x。
当a为1的时候,mix(x, y, 1.0) = y。
因此我们得出一个结论,这个雾化后的物体颜色的公式,实际上也就是webgl定义好的mix函数而已。
3、添加动画效果
我们可以模拟一个场景,从近到远的距离来观察物体颜色的变化。因为越接近雾化终点就越模糊,那我们可以通过改变雾化终点的距离来观察颜色的变化。
const start = 0;
const end = 200;
const fogDist = new Float32Array([start, end]);
function draw() {
fogDist[1] -= 1;
if (fogDist[1] < start) {
fogDist[1] = end;
}
gl.uniform2fv(uFogDist, fogDist);
// 设置背景色为黑色,跟雾化颜色一致
gl.clearColor(0.0,0.0,0.0,1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.drawElements(gl.TRIANGLES, indeces.length, gl.UNSIGNED_BYTE, 0);
requestAnimationFrame(draw)
}
draw();
7.1.3 代码演示
7.2 绘制圆形的点
在Webgl中,绘制一个点的形状是矩形。为了绘制一个圆点,我们需要将原先的方点“削”成圆形的。顶点着色器和片元着色器之间发生了光栅化过程,一个顶点被光栅化为了多个片元,每一个片元都会经过片元着色器的处理。如果直接进行绘制,画出的就是方形的点;而如果在片元着色器中稍微作改动,只绘制圆圈以内的片元,这样就可以绘制出圆形的点了。
7.2.1 片元着色器坐标
在片元着色器中,它的每一个点也给它们设置了一个坐标系,坐标值得区间是从0.0到1.0,如下图所示:
为了将矩形削成圆形,需要知道每个片元在光栅化过程中的坐标。在片元着色器里,它提供了一个内置的gl_PointCoord变量表示当前片元所属的点内的坐标。有了gl_PointCoord变量之后,求出它与点的中心(0.5,0.5)距离,将超过0.5距离的片元剔除掉即可。
7.2.2 求片元与点中心的距离
求两点的距离,无论是二维还是三维,用勾股定理即可求得。
二维空间中,两点(x1, y1)和(x2, y2)之间的距离计算公式:d = √((x2 - x1)^2 + (y2 - y1)^2)
三维空间中,两点(x1, y1, z1)和(x2, y2, z2)之间的距离计算公式:d = √((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)
备注:√表示平方根。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0);
gl_PointSize = 100.0;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
float distanceSelf(vec2 a, vec2 b) {
float x = a.x - b.x;
float y = a.y - b.y;
float v = x * x + y * y;
return sqrt(v);
}
void main() {
// 计算距离
float dis = distanceSelf(gl_PointCoord, vec2(0.5,0.5));
if (dis <= 0.5) {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
}
`;
7.2.3 distance方法
在WebGL中,它提供了一个计算两点距离的内置方法distance,只要把两个点的坐标传进去即可返回距离值,不用自己手动计算。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0);
gl_PointSize = 100.0;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
void main() {
// 计算距离
float dis = distance(gl_PointCoord, vec2(0.5,0.5));
if (dis > 0.5) {
discard;
}
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
注意:这个discard是片元着色器内置的语句,意思是放弃当前片元,有点类似于js中的break或者continue。
7.2.4 制作圆环
我们有了绘制圆形的原理,绘制圆环实际上道理是一样的,因为圆环就是多个圆组成的,还是可以通过片元坐标与点中心的距离来进行过滤渲染。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0);
gl_PointSize = 100.0;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
void main() {
// 计算距离
float dis = distance(gl_PointCoord, vec2(0.5,0.5));
if (dis > 0.5 || (dis < 0.4 && dis > 0.3) || dis < 0.2) {
discard;
}
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
7.2.5 示例代码
7.3 绘制半透明物体
半透明物体的意义在于它可以在不影响视线的情况下,让光线穿过物体,使得我们能够观察到物体背后或者内部的部分。这种特性在许多应用中都非常有用,比如建筑物中的玻璃窗、医疗设备中的透明塑料、以及电子设备中的显示屏。此外,半透明物体还具有一定的艺术效果,可以让物体呈现出半透明的质感和神秘感,增强视觉体验。
7.3.1 流程说明
1、在片元着色器中定义透明度属性,并使用gl_FragColor来设置颜色和透明度值。
2、使用gl.enable(gl.BLEND)启用混合模式,这样可以将透明物体正确地混合到场景中。
3、使用gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)设置混合函数,指定源颜色因子和目标颜色因子,以便正确地混合透明物体。
4、在渲染透明物体时,确保先渲染不透明的物体,再渲染半透明的物体。这样可以避免混合时受到前面物体的影响而导致不正确的结果。
7.3.2 使用步骤
在WebGL中创建半透明物体需要遵循以下步骤:
1、开启混合功能:gl.enable(gl.BLEND)
2、指定混合函数:gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute vec4 aNormal;
varying vec4 vColor;
uniform mat4 mat;
void main() {
// 环境光
vec3 uAmbientLightColor = vec3(0.2,0.2,0.2);
// 物体表面的颜色
vec4 aColor = vec4(1.0,0.0,0.0,1.0);
// 环境反射光颜色
vec3 ambient = uAmbientLightColor * vec3(aColor);
// 定义点光源的颜色
vec3 uPointLightColor = vec3(1.0,1.0,0.0);
// 点光源的位置
vec3 uPointLightPosition = vec3(-5.0,6.0,10.0);
// 顶点的世界坐标
vec4 vertexPosition = mat * aPosition;
// 点光源的方向
vec3 lightDirection = normalize(uPointLightPosition - vec3(vertexPosition));
// 计算入射角 光线方向和法线方向的点积
float dotDeg = dot(lightDirection, vec3(aNormal));
// 漫反射光的颜色
vec3 diffuseColor = uPointLightColor * vec3(aColor) * dotDeg;
gl_Position = vertexPosition;
vColor = vec4(ambient + diffuseColor, 0.5);
}
`;
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
备注:这里只写关键的代码,详细代码可以参考下面的实例代码。
7.3.3 gl.blendFunc(src_factor, dst_factor)混合函数说明
src_factor: 指定源颜色在混合颜色的权重因子
dst_factor: 指定目标颜色在混合后颜色的权重因子
权重因子列表
(Rs,Gs,Bs,As) 表示源颜色各分量, (Rd,Gd,Bd,Ad) 表示目标颜色的各分量
常量 | R分量的系数 | G分量的系数 | B分量的系数 |
---|---|---|---|
gl.ZERO | 0.0 | 0.0 | 0.0 |
gl.ONE | 1.0 | 1.0 | 1.0 |
gl.ONE_MINUS_SRC_COLOR | 1-Rs | 1-Gs | 1-Bs |
gl.ONE_MINUS_DST_COLOR | 1-Rd | 1-Gd | 1-Bd |
gl.ONE_MINUS_SRC_ALPHA | 1-As | 1-As | 1-As |
gl.ONE_MINUS_DST_ALPHA | 1-Ad | 1-Ad | 1-Ad |
gl.SRC_COLOR | Rs | Gs | Bs |
gl.DST_COLOR | Rd | Gd | Bd |
gl.SRC_ALPHA | As | As | As |
gl.DST_ALPHA | Ad | Ad | Ad |
gl.SRC_ALPHA_SATUREATE | min(As,Ad) | min(As,Ad) | min(As,Ad) |
gl.ZERO:表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算。
gl.ONE:表示使用1.0作为因子,实际上相当于完全的使用了这种颜色参与混合运算。
gl.SRC_ALPHA:表示使用源颜色的alpha值来作为因子。
gl.DST_ALPHA:表示使用目标颜色的alpha值来作为因子。
gl.ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来作为因子。
gl.ONE_MINUS_DST_ALPHA:表示用1.0减去目标颜色的alpha值来作为因子。