webgl学习之模板缓冲区

692 阅读5分钟

一、介绍

当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer),我们可以在渲染的时候更新它来获得一些很有意思的效果。 一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。

二、原理

模板缓冲的一个简单的例子如下:

image.png

模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。

模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以读取这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥,但大体的步骤如下:

  • 启用模板缓冲的写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲的写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

三、模板函数

和深度测试一样,我们对模板缓冲应该通过还是失败,以及它应该如何影响模板缓冲,也是有一定控制的。一共有两个函数能够用来配置模板测试:gl.stencilFuncgl.stencilOp

gl.stencilFunc(func,ref,mask)一共包含三个参数:

  • func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:gl.NEVERgl.LESSgl.LEQUALgl.GREATERgl.GEQUALgl.EQUALgl.NOTEQUALgl.ALWAYS。它们的语义和深度缓冲的函数类似。
  • ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。
  • mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。

在一开始的那个简单的模板例子中,函数被设置为:

gl.stencilFunc(gl.EQUAL, 1, 0xFF)

这会告诉OpenGL,只要一个片段的模板值等于(GL.EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃。

但是gl.stencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,而不是我们应该如何更新缓冲。这就需要gl.stencilOp这个函数了。

gl.stencilOp(sfail,dpfail,dppass) 一共包含三个选项,我们能够设定每个选项应该采取的行为:

  • sfail:模板测试失败时采取的行为。
  • dpfail:模板测试通过,但深度测试失败时采取的行为。
  • dppass:模板测试和深度测试都通过时采取的行为。

每个选项都可以选用以下的其中一种行为:

行为描述
gl.KEEP保持当前储存的模板值
gl.ZERO将模板值设置为0
gl.REPLACE将模板值设置为gl.stencilFunc函数设置的ref
gl.INCR如果模板值小于最大值则将模板值加1
gl.INCR_WRAPgl.INCR一样,但如果模板值超过了最大值则归零
gl.DECR如果模板值大于最小值则将模板值减1
gl.DECR_WRAPgl.DECR一样,但如果模板值小于0则将其设置为最大值
gl.INVERT按位翻转当前的模板缓冲值

默认情况下gl.stencilOp是设置为(gl.KEEP, gl.KEEP, gl.KEEP)的,所以不论任何测试的结果是如何,模板缓冲都会保留它的值。默认的行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你需要至少对其中一个选项设置不同的值。

所以,通过使用gl.stencilFuncgl.stencilOp,我们可以精确地指定更新模板缓冲的时机与行为了,我们也可以指定什么时候该让模板缓冲通过,即什么时候片段需要被丢弃。

四、webgl实践之绘制地球轮廓

使用模板缓冲区需要在获取webgl上下文时,首先设置模板缓冲区可用const gl = canvas.getContext('webgl', { stencil: true }),否则后续操作无效

步骤如下

  1. 清除模板缓冲区(
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
  1. 开启模板测试
gl.enable(gl.STENCIL_TEST)
  1. 模板测试选项,只通过模板测试保持原样,通过模板测试不通过深度测试保持原样,两个都通过则写入值
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
  1. 设置绘制物体的所有的片段都应该更新模板缓冲
gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
  1. 打开模板测试写入,即设置模板测试的写入值为0xFF
 gl.stencilMask(0xFF);
  1. 绘制地球,并切换着色器等准备下一个物体绘制
draw(gl, currentAngle, sphere, 1.0)
  1. 只绘制模板值不为1的部分
gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);
  1. 禁用模板写入和深度测试
// 禁止模板缓冲的写入
gl.stencilMask(0x00);
//禁用深度测试
gl.disable(gl.DEPTH_TEST);
  1. 绘制缩放的地球,并恢复深度,模板的测试与写入等
draw(gl, currentAngle, sphere, 1.01)
gl.stencilMask(0xFF);
gl.enable(gl.DEPTH_TEST);

效果如下

image.png

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
    content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>模板缓冲区</title>
  <style>
    body {
      margin: 0;
      text-align: center;
    }

    #canvas {
      margin: 0;
    }
  </style>
</head>

<body onload="main()">
  <canvas id="canvas" height="800" width="1200"></canvas>
</body>
<script src="/lib/webgl-utils.js"></script>
<script src="/lib/webgl-debug.js"></script>
<script src="/lib/cuon-utils.js"></script>
<script src="/lib/cuon-matrix.js"></script>
<script src="/lib/helper.js"></script>
<script>
  //顶点着色器
  var VSHADER_SOURCE = /*glsl*/`
        attribute vec2 a_Uv;
        attribute vec4 a_Position;
        uniform mat4 u_ModelViewMatrix; 
        varying vec2 v_Uv;
        void main(){ 
           gl_Position = u_ModelViewMatrix*a_Position;
            v_Uv=a_Uv;
        }`;

  //片元着色器
  var FSHADER_SOURCE = /*glsl*/`
        #ifdef GL_ES
        precision mediump float;
        #endif
        uniform sampler2D u_Texture;
        varying vec2 v_Uv;
        void main(){
        
           gl_FragColor = texture2D(u_Texture,vec2(v_Uv.x,v_Uv.y));
        // gl_FragColor=vec4(1.0);
        }`;

  var FSHADER_SOURCE1 =/*glsl*/`
      void main(){
          gl_FragColor=vec4(1.0,1.0,0.0,1.0);
        }`;

  //声明js需要的相关变量
  var canvas = document.getElementById("canvas");
  // var gl = getWebGLContext(canvas);
  const gl = canvas.getContext('webgl', { stencil: true });

  async function main() {
    if (!gl) {
      console.log("你的浏览器不支持WebGL");
      return;
    }

    const program = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE)

    if (!program) {
      console.error('创建着色器程序失败')
      return
    }


    const program1 = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE1)
    if (!program1) {
      console.error('创建着色器程序1失败')
      return
    }


    gl.program = program
    gl.useProgram(program)

    getVariableLocation()


    const sphere = drawSphere(gl)


    const texture = await initTexture(gl, './image/earth.jpg', 0)
    //设置底色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    var currentAngle = 0
    var tick = function () {
      //计算角度
      currentAngle -= 0.3
      //清除缓冲区
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
      //开启模板测试
      gl.enable(gl.STENCIL_TEST)
      //模板测试选项,只通过模板测试保持原样,通过模板测试不通过深度测试保持原样,两个都通过则写入值
      gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);

      //所有的片段都应该更新模板缓冲
      gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
      // 设置模板缓冲写入值,打开模板测试写入
      gl.stencilMask(0xFF);
      gl.enable(gl.DEPTH_TEST);
      gl.program = program
      gl.useProgram(program)

      draw(gl, currentAngle, sphere, 1.0)

      gl.program = program1
      gl.useProgram(program1)
      //只绘制模板值不为1的部分
      gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);
      // 禁止模板缓冲的写入
      gl.stencilMask(0x00);
      //禁用深度测试
      gl.disable(gl.DEPTH_TEST);


      draw(gl, currentAngle, sphere, 1.01)
      gl.stencilMask(0xFF);
      gl.enable(gl.DEPTH_TEST);
      requestAnimationFrame(tick)
    }
    tick()
    //进入场景初始化
    //draw(gl, n, u_ModelViewMatrix, u_ModelMatrix, u_NormalMatrix);
  }

  function draw(gl, currentAngle, sphere, scale) {
    //设置视角矩阵的相关信息(视点,视线,上方向)
    var viewMatrix = new Matrix4();
    viewMatrix.setLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);

    //设置模型矩阵的相关信息
    var modelMatrix = new Matrix4();
    modelMatrix.setRotate(-23.5, 0, 0, 1);
    modelMatrix.rotate(currentAngle, 0, 1, 0);
    modelMatrix.scale(scale, scale, scale)


    //设置透视投影矩阵
    var projMatrix = new Matrix4();
    projMatrix.setPerspective(30, canvas.width / canvas.height, 0.1, 100);

    //计算出模型视图矩阵 viewMatrix.multiply(modelMatrix)相当于在着色器里面u_ViewMatrix * u_ModelMatrix
    var modeViewMatrix = projMatrix.multiply(viewMatrix.multiply(modelMatrix));

    //设置视角矩阵的相关信息
    var u_ModelViewMatrix = gl.getUniformLocation(gl.program, "u_ModelViewMatrix");
    if (u_ModelViewMatrix < 0) {
      console.log("无法获取mvp矩阵变量的存储位置");
      return;
    }

    //将试图矩阵传给u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ModelViewMatrix, false, modeViewMatrix.elements);

    //开启隐藏面清除
    // gl.enable(gl.DEPTH_TEST);

    //清空颜色和深度缓冲区
    // gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    const length = sphere.length

    //绘制图形
    gl.drawElements(gl.TRIANGLES, length, gl.UNSIGNED_SHORT, 0);



  }




  function drawSphere(gl) {
    const maxLat = 30, maxLon = 30
    const radius = 1
    const position = []
    const uv = []
    //计算所有的索引值
    const indexArr = []
    for (let lat = 0; lat <= maxLat; lat++) {
      //     //计算占据的百分比并转为弧度(0->PI之间)
      // let lat=15
      const theta = lat / maxLat * Math.PI
      const y = radius * Math.cos(theta)
      for (let lon = 0; lon < maxLon; lon++) {
        //计算横向占据的百分比(0-2*PI之间)
        const phi = lon * 2 * Math.PI / (maxLon - 1)
        const x = radius * Math.sin(theta) * Math.cos(phi)
        const z = radius * Math.sin(theta) * Math.sin(phi)
        position.push(x, y, z)
        uv.push(1 - lon / (maxLon - 1), 1 - lat / maxLat)


        //计算当前索引个数
        if (lat < maxLat) {
          const current = lon + lat * maxLon
          if (lon === maxLon - 1) {
            continue;
          } else {
            indexArr.push(current, current + maxLon, current + 1)
            indexArr.push(current + 1, current + maxLon, current + maxLon + 1)

          }
        }


      }

    }
    const vertices = new Float32Array(position)
    const indices = new Uint16Array(indexArr)
    const uvs = new Float32Array(uv)
    //创建缓冲区对象
    const positionBuffer = initArrayBuffer(gl, vertices, 3, gl.FLOAT, gl.program.position);
    const uvBuffer = initArrayBuffer(gl, uvs, 2, gl.FLOAT, gl.program.uv);
    //将顶点索引数据写入缓冲区对象
    var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

    return {
      indexBuffer: indexBuffer,
      arrayBuffers: [positionBuffer, null, uvBuffer, null],
      length: indices.length
    };

  }


</script>

</html>