webgl最简单的图形-画一个点

279 阅读5分钟

1、绘图步骤:

canvas 2d 的绘图逻辑是这样的,首先要有canvas 画布,然后通过canvas 画布的getContext('2d') 方法获取一支二维画笔,然后直接在画布上画画。就像这样:

  //canvas画布 
  const canvas=document.getElementById('canvas'); 
  //二维画笔 
  const ctx=canvas.getContext('2d'); 
  //设置画笔的颜色 
  ctx.fillStyle='red'; 
  //用画笔画一个矩形 
  ctx.fillRect(20,20,300,200);

webgl的绘图逻辑:

webgl更像电脑绘画,其绘画的步骤里还多了一层介质。 这层介质就是手绘板,就像这样:

  1. 找一台电脑。
  2. 找一块手绘板。
  3. 找一支触控笔。
  4. 开始画画。

2、canvas 2d和webgl绘图的差异

实际上,webgl 的绘图逻辑和canvas 2d 的绘图逻辑还有一个本质的差别。 大家在学习html 的时候应该知道,浏览器有三大线程: js 引擎线程、GUI 渲染线程、浏览器事件触发线程。

其中GUI 渲染线程就是用于渲图的,在这个渲染线程里,有负责不同渲染工作的工人。比如有负责渲染HTML+css的工人,有负责渲染二维图形的工人,有负责渲染三维图形的工人。

渲染二维图形的工人和渲染三维图形的工人不是一个国家的,他们说的语言不一样。

渲染二维图形的工人说的是js语言。

渲染三维图形的工人说的是GLSL ES 语言。

而我们在做web项目时,业务逻辑、交互操作都是用js 写的。

我们在用js 绘制canvas 2d 图形的时候,渲染二维图形的工人认识js 语言,所以它可以正常渲图。

但我们在用js 绘制webgl图形时,渲染三维图形的工人就不认识这个js 语言了,因为它只认识GLSL ES 语言。

因此,这个时候我们就需要找人翻译翻译。

这个做翻译的人是谁呢,它就是我们之前提到过的手绘板,它在webgl 里叫“程序对象”。

接下来咱们从手绘板的绘图步骤中捋一下webgl 的绘图思路。

3、webgl绘图思路

  1. 找一台电脑 - 浏览器里内置的webgl 渲染引擎,负责渲染webgl 图形,只认GLSL ES语言。
  2. 找一块手绘板 - 程序对象,承载GLSL ES语言,翻译GLSL ES语言和js语言,使两者可以相互通信。
  3. 找一支触控笔 - 通过canvas 获取的webgl 类型的上下文对象,可以向手绘板传递绘图命令,并接收手绘板的状态信息。
  4. 开始画画 - 通过webgl 类型的上下文对象,用js 画画。

在上面的思路中,大家对其中的一些名词可能还没有太深的概念,比如程序对象。接下来咱们就详细说一下webgl 实际的绘图步骤。

完整代码:

<style>
  body {
    margin: 0;
    overflow: hidden;
  }
</style>
<body>
  <!-- 1:在html中建立canvas画布 -->
  <canvas id="canvas"></canvas>
</body>
<!-- 4: 在script中建立顶点着色器和片元着色器,glsl es语言 -->
<!-- 顶点着色器 -->
<!-- 注意:1、参数值要为浮点数;2、结束必须加; -->
<script id="vertexShader" type="x-shader/x-vertex">
  void main() {
    //点位
    gl_Position = vec4(0.5, 0.0, 0.0, 1.0);
    //尺寸
    gl_PointSize = 50.0;
  }
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
  void main() {
    gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
  }
</script>
<script>
  // 2:在js中获取canvas画布
  const canvas = document.getElementById('canvas');
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight

  // 3:使用canvas获取webgl绘图上下文对象
  const gl = canvas.getContext('webgl');

  // 5:在js中获取着色器的文本(之后需要基于这个文本对着色器进行初始化)
  const vsSourse = document.getElementById('vertexShader').innerText;
  const fsSource = document.getElementById('fragmentShader').innerText;

  // 6:初始化着色器
  // 功能:解析着色器文本,整合到程序对象中,关联webgl上下文对象,实现两种语言的相互通信
  initShaders(gl, vsSourse, fsSource)
 
  // 7: 指定清空绘图区的颜色
  gl.clearColor(0, 0, 0, 1)

  // 8:刷底色
  gl.clear(gl.COLOR_BUFFER_BIT)

  // 9:绘制顶点
  gl.drawArrays(gl.POINTS, 0, 1)


  function initShaders(gl,vsSource,fsSource){
    //创建程序对象
    const program = gl.createProgram();
    //建立着色对象
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
    //把顶点着色对象装进程序对象中
    gl.attachShader(program, vertexShader);
    //把片元着色对象装进程序对象中
    gl.attachShader(program, fragmentShader);
    //连接webgl上下文对象和程序对象
    gl.linkProgram(program);
    //启动程序对象
    gl.useProgram(program);
    //将程序对象挂到上下文对象上
    gl.program = program;
    return true;
  }

  function loadShader(gl, type, source) {
    //根据着色类型,建立着色器对象
    const shader = gl.createShader(type);
    //将着色器源文件传入着色器对象中
    gl.shaderSource(shader, source);
    //编译着色器对象
    gl.compileShader(shader);
    //返回着色器对象
    return shader;
  }
</script>

4、着色器

4.1 着色器的概念

webgl 绘图需要两种着色器:

  1. 顶点着色器(Vertex shader):描述顶点的特征,如位置、颜色等。
  2. 片元着色器(Fragment shader):进行逐片元处理,如光照。

看了这两个名词的解释,我想很多初学者会是懵的。

我给大家翻译翻译:

补间动画大家知道不?顶点着色器里的顶点就是补间动画里的关键帧,片元着色器里的片元就是关键帧之间以某种算法算出的插值。当然,咱们webgl里的片元是像素的意思。

再给大家举一个更简单、更贴切的例子:

两点决定一条直线大家知道不?顶点着色器里的顶点就是决定这一条直线的两个点,片元着色器里的片元就是把直线画到画布上后,这两个点之间构成直线的每个像素。

4.2着色器语言

webgl 的着色器语言是GLSL ES语言

  • 顶点着色程序,要写在type=“x-shader/x-vertex” 的script中。
<script id="vertexShader" type="x-shader/x-vertex">
    void main() {
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
        gl_PointSize = 100.0;
    }
</script>
  • 片元着色程序,要写在type=“x-shader/x-fragment” 的script中。
<script id="fragmentShader" type="x-shader/x-fragment">
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
</script>

void main() {…… } 是主体函数。

在顶点着色器中,gl_Position 是顶点的位置,gl_PointSize 是顶点的尺寸,这种名称都是固定的,不能写成别的。

在片元着色器中,gl_FragColor 是片元的颜色。

vec4() 是一个4维矢量对象。

将vec4() 赋值给顶点点位gl_Position 的时候,其中的前三个参数是x、y、z,第4个参数默认1.0,其含义我们后面会详解;

将vec4() 赋值给片元颜色gl_FragColor 的时候,其中的参数是r,g,b,a。

4.3着色器初始化

function initShaders(gl, vsSource, fsSource) {
  //创建程序对象(目前这只是一个手绘板的外壳)
  const program = gl.createProgram();
  //建立着色对象(这是手绘板里用于接收触控笔信号的零部件,二者可以分工合作,把触控笔的压感(js信号)解析为计算机语言(GLSL ES),然后让计算机(浏览器的webgl 渲染引擎)识别显示。)
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  //把顶点着色对象装进程序对象中(这就完成的手绘板的拼装 )
  gl.attachShader(program, vertexShader);
  //把片元着色对象装进程序对象中(这就完成的手绘板的拼装 )
  gl.attachShader(program, fragmentShader);
  //连接webgl上下文对象和程序对象(就像连接触控笔和手绘板一样(触控笔里有传感器,可以向手绘板发送信号)。)
  gl.linkProgram(program);
  //启动程序对象(就像按下了手绘板的启动按钮,使其开始工作)
  gl.useProgram(program);
  //将程序对象挂到上下文对象上
  gl.program = program;
  return true;
}

function loadShader(gl, type, source) {
  //根据着色类型,建立着色器对象
  const shader = gl.createShader(type);
  //将着色器源文件传入着色器对象中,这里的着色器源文件就是我们之前在script 里用GLSL ES写的着色程序。
  gl.shaderSource(shader, source);
  //编译着色器对象
  gl.compileShader(shader);
  //返回着色器对象
  return shader;
}

在以后的学习里,initShaders 会经常用到,所以我们可以将其模块化。

后面需要的时候,import 引入即可。

import {initShaders} from '../jsm/Utils.js';