WebGL+Three.js—第七章 WebGL进阶操作

817 阅读10分钟

7.1 雾化

7.1.1 概念

        雾化,用来表示距离越远看的越模糊的现象。比如在大雾的天气里,距离我们越远的地方我们越看不清楚,如果距离足够远的话,我们只能看到白茫茫的一片,全是雾的颜色,看不见物体。

        WebGL使用雾化来实现这种逐渐模糊的效果。WebGL雾化实现是通过某点和视点之间的距离,距离越远雾化程度越高。这种雾化也称为线性雾化。某一点的雾化程度也成为了雾化因子。

image.png

        这个图比较容易理解雾化,在垂直于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表示的是齐次坐标。

        什么是齐次坐标呢?在数学的概念中,一个平面的两条平行直线永不相交,但在投影空间却不是这样的。比如下面的铁轨,它们本身是平行的,但在无穷远处,两条铁轨相交汇合为一点!          image.png

        我们可以得出一个概念,就是在投影空间里,当这个点变得无限远的时候,它们就有可能会相交,用数学的角度来表示的话,它们的坐标就是(∞,∞)。

        那么如何把一个坐标变成(∞,∞)呢?我们可以通过让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中,绘制一个点的形状是矩形。为了绘制一个圆点,我们需要将原先的方点“削”成圆形的。顶点着色器和片元着色器之间发生了光栅化过程,一个顶点被光栅化为了多个片元,每一个片元都会经过片元着色器的处理。如果直接进行绘制,画出的就是方形的点;而如果在片元着色器中稍微作改动,只绘制圆圈以内的片元,这样就可以绘制出圆形的点了。

image.png

7.2.1 片元着色器坐标

        在片元着色器中,它的每一个点也给它们设置了一个坐标系,坐标值得区间是从0.0到1.0,如下图所示:

image.png

        为了将矩形削成圆形,需要知道每个片元在光栅化过程中的坐标。在片元着色器里,它提供了一个内置的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);
    }
  }
`;

image.png

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);
  }
`;

image.png

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);

        备注:这里只写关键的代码,详细代码可以参考下面的实例代码。

image.png

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.ZERO0.00.00.0
gl.ONE1.01.01.0
gl.ONE_MINUS_SRC_COLOR1-Rs1-Gs1-Bs
gl.ONE_MINUS_DST_COLOR1-Rd1-Gd1-Bd
gl.ONE_MINUS_SRC_ALPHA1-As1-As1-As
gl.ONE_MINUS_DST_ALPHA1-Ad1-Ad1-Ad
gl.SRC_COLORRsGsBs
gl.DST_COLORRdGdBd
gl.SRC_ALPHAAsAsAs
gl.DST_ALPHAAdAdAd
gl.SRC_ALPHA_SATUREATEmin(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值来作为因子。

7.3.4 示例代码