blog cover

🚿 [WebGL编程指南] 2. 入门

WebGL

着色器是什么

要使用WbGL进行绘图就必须使用着色器。

  • 1.顶点着色器:顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,比如二维或三维图形的端点或交点。
  • 2.片元着色器:片元着色器(Fragment shader):进行逐片元处理过程如光照的程序。片元(fragment)是一个WebGL术语,你可以将其理解为像素(图像的单元)。
  • image

    WebGL坐标系

    由于WbGL处理的是三维图形,所以它使用三维坐标系统(笛卡尔坐标系),具有 X轴、Y轴和Z轴。三维坐标系统很容易理解,因为我们的世界也是三维的:具有宽度、高度和长度。在任何坐标系统中,轴的方向都非常重要。通常,在WbGL中,当你面向计算机屏幕时,X轴是水平的(正方向为右),Y轴是垂直的(正方向为下),而Z轴垂直于屏幕(正方向为外)

    image

    WebGL的坐标系和<canvas>绘图区的坐标系不同,需要将前者映射到后者。默认情况下,如图2.18所示,WebGL坐标与<canvas>坐标的对应关系如下。

  • <canvas>:的中心点:(0.0,0.0,0.0)
  • <canvas>的上边缘和下边缘:(-1.0,0.0,0.0)和(1.0,0.0,0.0)
  • <canvas.>的左边缘和右边缘:(0.0,-1.0,0.0)和(0.0,1.0,0.0)
  • image

    绘制一个点

    htmlCopy
    <html charset="utf-8" lang="zh">
      <head>
        <title>WebGLDemo</title>
      </head>
      <body>
        <canvas id="webgl" height="300" width="300"></canvas>
        <br />
        <button id="draw-point-button">绘制一个点</button>
    
        <script src="./src/drawPoint.js"></script>
        <script src="./utils/webGL.utils.js"></script>
        <script>
          const drawPonitButton = document.querySelector("#draw-point-button");
          if (drawPonitButton) {
            drawPonitButton.addEventListener("click", () => {
              drawPoint();
            });
          }
        </script>
      </body>
    </html>
    

  • 顶点着色器指定了点的位置和尺寸(gl_Positiongl_PointSize)。本例中,点的位置是(0.0,0.0,0.0),尺寸是 10.0像素。
  • 片元着色器指定了点的颜色(gl_FragColor)。本例中,点的颜色是红色(1.0,0.0,0.0,1.0)。
  • 着色器使用类似于C的OpenGL ES着色器语言(GLSL ES)来编写。和C语言程序一样,必须包含一个main()函数。 main()前面的关键字void表示这个函数不会有返回值。还有,你不能为main()指定参数

    注意,g1_Position变量必须被赋值,否则着色器就无法正常工作。相反,g1_Pointsize不是必须的,如果你不赋值,着色器就会为其取默认值1.0。

    注意,赋给g1_Position的矢量中,我们添加了1.0作为第4个分量。由4个分量组成的矢量被称为齐次坐标因为它能够提高处理三维数据的效率

    齐次坐标使用如下的符号描述:(x,y,z,w)。齐次坐标(x,y,z,w)等价于三维坐标(x/w,y/w,z/w)所以如果齐次坐标的第4个分量是1.0,你就可以将它当做三维坐标来使用。w的值必须是大于等于0的。如果w趋近于0,那么它所表示的点将趋近无穷远,所以在齐次坐标系中可以有无穷概念。齐次坐标的存在,使得用矩阵乘法来描述顶点变换成为可能,三维图形统在计算过程中,通常使用齐次坐标来表示顶点的三维坐标。

    gl.drawArrays()是一个强大的函数,它可以用来绘制各种图形,该函数的规范如下表所示。

    image
    javascriptCopy
    // 入口函数
    function drawPoint() {
      // 顶点着色器
      const VSHADER_SOURCE =
        "void main() {\n" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" + // 设置坐标
        "  gl_PointSize = 10.0;\n" + // 设置大小
        "}\n";
      // 片元着色器
      const FSHADER_SOURCE =
        "void main() {\n" +
        "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" + // 设置颜色
        "}\n";
      var canvas = document.getElementById("webgl");
      var gl = canvas.getContext("webgl");
    
      // 初始化着色器
      if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log("Failed to intialize shaders.");
        return;
      }
    
      // Specify the color for clearing <canvas>
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
    
      // 清除缓冲区
      gl.clear(gl.COLOR_BUFFER_BIT);
    
      // 绘制
      gl.drawArrays(gl.POINTS, 0, 1);
    }
    

    image

    initShader

    javascriptCopy
    function initShaders(gl, vshader, fshader) {
    	const vertextShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
    	const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, vshader)
    	// 创建程序对象
    	const program = gl.createProgram()
    	
    	// 为程序分配着色器对象
    	gl.attachShader(program, vertextShader)
    	gl.attachShader(program, fragmentShader)
    	
    	// 连接着色器
    	gl.linkProgram(program)
    	
    	// 检查连接
    	const linked = gl.getProgramParameter(program, gl.LINK_STATUS)
    	// ...
    	
    	gl.useProgram(program)
    	gl.program = program
    
    }
    
    function loadShader(gl, type, source) {
    	const shader = gl.createShader(type)
    	
    	// 设置着色器源代码
    	gl.shaderSource(shader, source)
    	// 编译着色器
    	gl.compileSource(shader)
    	
    	// 检查着色器编译状态
    	const compiled = gl.getShaderParameter(sharder, gl.COMPILE_STATUS)
    	// ...
    	return shader 
    }

  • 1.创建着色器对象(gl.createShader)
  • image
  • 1.向着色器对象中填充着色器程序的源代码gl.shaderSource
  • image
  • 1.编译着色器对象gl.compileShader
  • image

    GLSL ES语言需要编译成二进制可执行格式,WebGL使用这种可执行格式。

    编译错误可以通过gl.getShaderParameter(sharder, gl.COMPILE_STATUS) 来判断。错误信息可以通过gl.getShaderInfoLog获取。

  • 1.创建程序对象gl.createProgram
  • image
  • 1.为程序对象分配着色器gl.attachShader
  • image
  • 1.连接程序对象 gl.linkProgram
  • image
  • 1.使用程序对象gl.useProgram
  • image
  • 着色器对象:管理一个顶点着色器或片元着色器。
  • 程序对象:程序对象是管理着色器对象的容器。一个程序必须包含宇哥顶点着色器和一个片元着色器。
  • 传递变量

    我们的目标是,将位置信息从JavaScript程序中传给顶点着色器。有两种方式可以做到这点:attribute变量和uniform变量。使用哪一个变量取决于需传输的数据本身,attribute变量传输的是那些与顶点相关的数据,而uniform变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据。本例将使用attribute变量来传输顶点坐标,显然不同的顶点通常具有不同的坐标。

    javascriptCopy
    // 入口函数
    function drawPoint() {
      // 顶点着色器
      const VSHADER_SOURCE =
    	  'attribute vec4 a_Position;\n' + // 使用attribute 变量
    	  'void main() {\n' +
    	  '  gl_Position = a_Position;\n' +
    	  '  gl_PointSize = 10.0;\n' +
    	  '}\n'; 
    
    	// ...
    
      // 初始化着色器
      if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log("Failed to intialize shaders.");
        return;
      }
      // 获取 a_Position 储存位置
      var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      // 传递变量
      gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
      
      
      // ...
    }
    

  • attribute vec4 a_Position;。关键词attribute被称为存储限定符(storage qualifier),它表示接下来的变量(在这个例子中是a_Position)是一个attribute变量。attribute变量必须声明成全局变量,数据将从着色器外部传给该变量。变量的声明必须按照以下的格式:<存储限定符><类型><变量名>
  • gl.getAttribLocation(gl.program, 'a_Position');方法的第一个参数是一个程序对象(program object),它包括了顶点着色器和片元着色器,
  • 通过鼠标点击绘制

    javascriptCopy
    function drawPoint() {
    	// ...
    	
      // 获取 a_Position 储存位置
      var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    	
    	canvas.onmounsedown = (ev) => {
    		click(ev, gl, canvas, a_Position)
    	}
    	
    	
    	// ...
    	
      // 清除缓冲区
      gl.clear(gl.COLOR_BUFFER_BIT);
    
    }
    
    let points = []
    
    function click(ev, gl, canvas, a_Position) {
    	let x = ev.clientX // 鼠标点击x
    	let y = ev.clientY // 鼠标点击y
    	let rect = ev.target.getBoundingClientRect();
    	
    	x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2)
    	y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
    	
    	points.push(x)
    	points.push(y)
    	
      // 清除缓冲区
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      for(let i = 0; i < points.length; i+=2 ) {
    	  // 传递变量
    	  gl.vertexAttrib3f(a_Position, points[i], points[i+1], 0.0);
    	  // 绘制
    	  gl.drawArrays(gl.POINTS, 0, 1);
      }
    
    }

  • 鼠标点击事件坐标,是client area中的坐标
  • image
  • canvas和WebGL的坐标不一样,Y轴正方向也不一致
  • image
  • let rect = ev.target.getBoundingClientRect(); 可以获取canvasclient area 中的坐标
  • (x - rect.left)(y - rect.top) 可以将鼠标点击的x,y坐标,转化到canvas坐标系坐标。
  • 通过canvas.width / 2(y - rect.top) 可以获取到canvas中心坐标
  • 再通过(x - rect.left) - canvas.width / 2canvas.height / 2 - (y - rect.top) 将canvas原点平移到中心
  • webGL坐标取值范围为-1.0到1.0。需要将x,y分别除以canvas.width / 2 canvas.height / 2 ,来将x、y映射到webGL坐标系。
  • webGL绘制结束会将缓冲区的内容,显示在屏幕上,然后清空颜色缓冲区。所以需要记录每次点击的x,y坐标。
  • 改变颜色

    javascriptCopy
    // 入口函数
    function drawPoint() {
      // 片元着色器
      const FSHADER_SOURCE =
    	  "uniform vec4 u_FragColor; \n+"
        "void main() {\n" +
        "  gl_FragColor = u_FragColor;\n" + // 设置颜色
        "}\n";
        
      // 获取u_FragColor变量储存位置
    	const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor'); 
    	
    	
    	// 设置RGBA颜色
    	gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 0.0) 
    	// ...  
    }

  • 注意:只有顶点着色器才能使用attribute变量。片元着色器需要使用uniform变量,或者varying变量。

  • 2024/10/13 09:22