WEBGL入门---绘制渐变三角形

绘制渐变三角形:深入理解缓冲区

上节带领大家学习了基本三角形图元的绘制过程,以及如何使用缓冲区向着色器传递多个数据,但上节只演示了往着色器传递坐标这一种数据,本节通过绘制渐变三角形,讲解一下如何通过缓冲区向着色器传递多种数据。

目标

本节通过一个鼠标每点击三次便会绘制一个渐变三角形的示例,带大家深入理解缓冲区的用法,最终效果如下图所示:

通过本节学习,你将会掌握如下内容:

  • 顶点数据在 buffer 中的排布方式。
  • 切换 buffer 时,bindBuffer 的重要性。
  • 使用多个 buffer 读取多种顶点数据。
  • 使用单个 buffer 读取多种顶点数据。
  • 如何实现渐变效果。

渐变三角形

上节我们实现的是单色三角形,通过在片元着色器中定义一个 uniform 变量,接收 JavaScript 传递过去的颜色值来实现。那渐变三角形的处理与单色三角形有何不同呢?

渐变三角形颜色不单一,在顶点与顶点之间进行颜色的渐变过渡,这就要求我们的顶点信息除了包含坐标,还要包含颜色。这样在顶点着色器之后,GPU 根据每个顶点的颜色对顶点与顶点之间的颜色进行插值,自动填补顶点之间像素的颜色,于是形成了渐变三角形。
那既然我们需要为每个顶点传递坐标信息和颜色信息,因此需要在顶点着色器中额外增加一个 attribute 变量a_Color,用来接收顶点的颜色,同时还需要在顶点着色器和片元着色器中定义一个 varying 类型的变量v_Color,用来传递顶点颜色信息。

着色器

  • 顶点着色器
<script type="shader-source" id="vertexShader">
        //浮点数设置为中等精度
		precision mediump float;
		//接收 JavaScript 传递过来的点的坐标(X, Y)
		attribute vec2 a_Position;
		// 接收canvas的尺寸。
		attribute vec2 a_Screen_Size;
		//接收 JavaScript 传递过来的点的颜色信息(R, G, B, A)
		attribute vec4 a_Color;
		//将顶点着色器插值后的颜色颜色信息(R, G, B, A)传递给片元着色器。
		varying vec4 v_Color;
		void main(){
			// 将 canvas 的坐标值 转换为 [-1.0, 1.0]的范围。
			vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
			// canvas的 Y 轴坐标方向和 设备坐标系的相反。
			position = position * vec2(1.0, -1.0);
			// 最终的顶点坐标。
			gl_Position = vec4(position, 0.0, 1.0);
                        // 设置颜色的信息
			v_Color = a_Color;
		}
    </script>
  • 片元着色器
    片元着色器新增一个 varying 变量 v_Color,用来接收插值后的颜色。
  <script type="shader-source" id="fragmentShader">
        //浮点数设置为中等精度
	precision mediump float;
	//用来接收顶点着色器传递过来的颜色。
	varying vec4 v_Color;

	void main(){
	   // 将颜色处理成 GLSL 允许的范围[0, 1]。
	   vec4 color = v_Color / vec4(255, 255, 255, 1);
	   // 点的最终颜色。
	   gl_FragColor = color;
	}
    </script>

按照正常思路,我们可以创建两个 buffer,其中一个 buffer 传递坐标,另外一个 buffer 传递颜色。创建两个 buffer,将 a_Position 和 positionBuffer 绑定,a_Color 和 colorBuffer 绑定,然后设置各自读取 buffer 的方式。

请谨记:程序中如果有多个 buffer 的时候,在切换 buffer 进行操作时,一定要通过调用 gl.bindBuffer 将要操作的 buffer 绑定到 gl.ARRAY_BUFFER 上,这样才能正> > 确地操作 buffer 。您可以将 bindBuffer 理解为一个状态机,bindBuffer 之后的对 buffer 的一些操作,都是基于最近一次绑定的 buffer 来进行的。

以下 buffer 的操作需要在绑定 buffer 之后进行:

  • gl.bufferData:传递数据。
  • gl.vertexAttribPointer:设置属性读取 buffer 的方式。

JavaScript 部分

<script>
  var random = Math.random;
  function randomColor() {
    return {
      r: random() * 255,
      g: random() * 255,
      b: random() * 255,
      a: random() * 1
    };
  }
  function $$(str){
      if(!str) throw Error('no element can select');
      if(str.startsWith('#')){
          return document.querySelector(str)
      }
      let result = document.querySelectorAll(atr);
      var temResult = result.length === 1? result[0]:result
      return temResult;
  }
  function getCanvas(id) {
    return $$(id)
  }
  function resizeCanvas(canvas, width, height){
      if(canvas.width !== width){
          canvas.width = width ? width:window.innerWidth
      }
      if(canvas.height !== height){
          canvas.height = height ? height:window.innerHeight
      }
  }
  function createSimpleProgramFromScript(gl, vertexScriptId, fragmentScriptId) {
    let vertexShader = createShaderFromScript(
      gl,
      gl.VERTEX_SHADER,
      vertexScriptId
    );
    let fragmentShader = createShaderFromScript(
      gl,
      gl.FRAGMENT_SHADER,
      fragmentScriptId
    );
    let program = createSimpleProgram(gl, vertexShader, fragmentShader);
    return program;
  }
  function createSimpleProgram(gl, vertexShader, fragmentShader) {
    if (!vertexShader || !fragmentShader) {
      console.warn('着色器不能为空');
      return;
    }
    let program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    let success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
      return program;
    }
    console.error(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
  }
  function getWebGLContext(canvas){
        return canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
    }
  
     /**
     * @param {*} gl,webgl绘图环境
     * @param {*} type,着色器类型
     * @param {*} scriptId,着色器源码scrpit标签id
     * @returns 返回着色器对象
     */
      function createShaderFromScript(gl, type, scriptId){
          let sourceScript = $$('#'+scriptId);
          if(!sourceScript){
              return null;
          }
          return createShader(gl, type, sourceScript.innerHTML);
      }
     /**
     * @param {*} gl,webgl绘图环境
     * @param {*} type,着色器类型
     * @param {*} source 着色器源码
     */

    function createShader(gl, type, source){
        let shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        //监测是否编译正常
        let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        if (success) {
          return shader;
        }
    }

    function createProgram(gl, vertexShader, fragmentShader){
      let program = gl.createProgram();
      vertexShader && gl.attachShader(program, vertexShader);
      fragmentShader && gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      //获取程序的状态参数
      let result = gl.getProgramParameter(program, gl.LINK_STATUS);
        if(result){
          console.log('program set up success');
          return program
        }
      }
    </script>
    <script>
        // 获取canvas
        let canvas = getCanvas('#canvas')
        // 设置canvas尺寸为满屏幕
        resizeCanvas(canvas);
        //获取绘图上下文
	let gl = getWebGLContext(canvas);
        //创建着色器程序
	let program = createSimpleProgramFromScript(gl, 'vertexShader', 'fragmentShader');
	//使用该着色器程序
	gl.useProgram(program);
        let a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');
	gl.vertexAttrib2f(a_Screen_Size, canvas.width, canvas.height);
	//顶点坐标数组
	let positions = [];
	//顶点颜色数组
	let colors = [];
        let a_Position = gl.getAttribLocation(program,'a_Position');
        let a_Color = gl.getAttribLocation(program, 'a_Color');
	// 使用enableVertexAttribArray()方法,来激活每一个属性以便使用,不被激活的属性是不会被使用的
	gl.enableVertexAttribArray(a_Position);
	// 使用enableVertexAttribArray()方法,来激活每一个属性以便使用,不被激活的属性是不会被使用的
	gl.enableVertexAttribArray(a_Color);
	// 创建 坐标信息 buffer
	var positionBuffer = gl.createBuffer();
	// 将当前 buffer 设置为 postionBuffer,接下来对 buffer 的操作都是针对 positionBuffer 了。
	gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
	// 设置 a_Position 变量读取 positionBuffer 缓冲区的方式。
	var size = 2;
	var type = gl.FLOAT;
	var normalize = false;
	var stride = 0;
	var offset = 0;
	gl.vertexAttribPointer(a_Position, size, type, normalize, stride, offset);
	// 创建 颜色信息 buffer
	var colorBuffer = gl.createBuffer();
	// 将当前 buffer 设置为 postionBuffer,接下来对 buffer 的操作都是针对 positionBuffer 了。
	gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
	// 设置 a_Position 变量读取 positionBuffer 缓冲区的方式。
	var size = 4;
	var type = gl.FLOAT;
	var normalize = false;
	var stride = 0;
	var offset = 0;

	gl.vertexAttribPointer(a_Color, size, type, normalize, stride, offset);
    </script>

假如我们顶点坐标数组中有四个顶点 8 个元素【30, 30, 30, 40, 40, 30, 20, 0】,顶点着色器中的 a_Position 属性在读取顶点坐标信息时,以 2 个元素为一组进行读取:

又假如我们顶点颜色数组中有两个顶点 8 个元素 【244, 230, 100, 1, 125, 30, 206, 1】,那么顶点着色器中的 a_Color 属性在读取顶点颜色信息时,以 4 个元素(r, g, b, a)为一组进行读取,如下图所示。


接下来我们为 canvas 添加点击事件:

  canvas.addEventListener('click', e => {
    var x = e.pageX;
    var y = e.pageY;
    positions.push(x, y);
    //随机一种颜色
    var color = randomColor();
    //将随机颜色的 rgba 值添加到顶点的颜色数组中。
    colors.push(color.r, color.g, color.b, color.a);
    //顶点的数量是 3 的整数倍时,执行绘制操作。
    if (positions.length % 6 == 0) {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.DYNAMIC_DRAW);
        render(gl);
    }
})
// 顺便绘制图像
function render(gl) {
      //用设置的清空画布颜色清空画布。
      gl.clear(gl.COLOR_BUFFER_BIT);
      if (positions.length <= 0) {
        return;
      }
      //绘制图元设置为三角形。
      var primitiveType = gl.TRIANGLES;
      //因为我们要绘制三个点,所以执行三次顶点绘制操作。
      gl.drawArrays(primitiveType, 0, positions.length / 2);
    }
posted @ 2022-02-10 10:27  自在一方  阅读(376)  评论(0编辑  收藏  举报