3. Webgl绘制三角形

 

 
2023-02-01345阅读12分钟
 
专栏: 
WebGL
 

webgl 中的三维模型都是由三角面组成的。

一 webgl 的绘图方式

webgl是怎么画图的。

  1. 绘制多点

    image-20200922151301533

  2. 如果是线,就连点成线

    image-20200922153449307

  3. 如果是面,那就在图形内部,逐片元填色

    image-20200922153643189

那webgl这个绘图方式在程序中是如何实现的呢?

二 在webgl绘制多点

在webgl 里所有的图形都是由顶点连接而成的,先画三个可以构成三角形的点。

注意,现在要画的多点是可以被webgl 加工绘制成线、或者面的,这和上一篇单纯的想要绘制多个点是不一样的。

2-1 绘制多点的整体步骤

  1. 建立着色器源文件

     
    <script id="vertexShader" type="x-shader/x-vertex">
        attribute vec4 a_Position;
        void main(){
            gl_Position = a_Position;
            gl_PointSize = 20.0;
        }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
        void main(){
            gl_FragColor=vec4(1.0,1.0,0.0,1.0);
        }
    </script>
    
  2. 获取webgl 上下文

     
    const canvas = document.getElementById('canvas');
    canvas.width=window.innerWidth;
    canvas.height=window.innerHeight;
    const gl = canvas.getContext('webgl');
    
  3. 初始化着色器

     
    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;
    initShaders(gl, vsSource, fsSource);
    
  4. 设置顶点点位

     
    const vertices=new Float32Array([
        0.0,  0.1,
        -0.1,-0.1,
        0.1, -0.1
    ])
    const vertexBuffer=gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
    const a_Position=gl.getAttribLocation(gl.program,'a_Position');
    gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0);
    gl.enableVertexAttribArray(a_Position);
    
  5. 清理画布

     
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
  6. 绘图

     
    gl.drawArrays(gl.POINTS, 0, 3);
    

实际效果:

image.png

 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>webgl绘制三角形</title>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          gl_Position = a_Position;
          gl_PointSize = 20.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 type="module">
      import { initShaders } from './Utils.js'
      const canvas = document.querySelector('#canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      //三维画笔
      const gl = canvas.getContext('webgl')
      // 获取着色器文本
      const vsSource = document.querySelector('#vertexShader').innerText
      const fsSource = document.querySelector('#fragmentShader').innerText
      //初始化着色器
      initShaders(gl, vsSource, fsSource)
      
      //建立顶点数据,两个浮点数构成一个顶点,分别代表x、y 值
      const vertices = new Float32Array([0.0, 0.1, -0.1, -0.1, 0.1, -0.1])
      // 建立着色器和js 都能进入的缓冲区。
      const vertexBuffer = gl.createBuffer()
      //  缓冲区和着色器建立绑定关系
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
      // 往缓冲区对象中写入数据
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 将缓冲区对象分配给attribute 变量
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
      // 开启顶点数据的批处理功能
      gl.enableVertexAttribArray(a_Position)

      //底色
      gl.clearColor(0, 0, 0, 1)
      //刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘图
      gl.drawArrays(gl.POINTS, 0, 3)
    </script>
  </body>
</html>
 
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
}

export { initShaders }

2-2 绘制多点详解

在用js定点位的时候,肯定是要建立一份顶点数据的,这份顶点数据是给着色器的,因为着色器需要这份顶点数据绘图。

然而,在js中建立顶点数据,着色器肯定是拿不到的,这是语言不通导致的。

为了解决这个问题,webgl 系统建立了一个能翻译双方语言的缓冲区。js 可以用特定的方法把数据存在这个缓冲区中,着色器可以从缓冲区中拿到相应的数据。

1. 建立顶点数据,两个浮点数构成一个顶点,分别代表x、y 值。

 
const vertices=new Float32Array([
    //x    y
    0.0,  0.1, //顶点
    -0.1,-0.1, //顶点
    0.1, -0.1  //顶点
])

现在上面的这些顶点数据是存储在js 缓存里的,着色器拿不到,所以需要建立一个着色器和js 都能进入的缓冲区。

2. 建立着色器和js 都能进入的缓冲区。

 
const vertexBuffer=gl.createBuffer();

现在上面的这个缓冲区是独立存在的,它只是一个空着的仓库,和谁都没有关系。接下来让其和着色器建立连接。

image.png

3. 缓冲区和着色器建立绑定关系。

 
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);

gl.bindBuffer(target,buffer) 绑定缓冲区

  • target 要把缓冲区放在webgl 系统中的什么位置
  • buffer 缓冲区

着色器对象在执行initShaders() 初始化方法的时候,已经被写入webgl 上下文对象gl 中了。

4. 往缓冲区对象中写入数据

 
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);

bufferData(target, data, usage) 将数据写入缓冲区

  • target 要把缓冲区放在webgl 系统中的什么位置
  • data 数据
  • usage 向缓冲区写入数据的方式, 例如gl.STATIC_DRAW 方式,是向缓冲区中一次性写入数据,着色器会绘制多次。

现在着色器绑定了缓冲区,可以访问里面的数据了,但是还得让着色器知道这个仓库是给哪个变量的,比如这里用于控制点位的attribute 变量。这样做是为了提高绘图效率。

5. 将缓冲区对象分配给attribute 变量

 
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0);

gl.vertexAttribPointer(local,size,type,normalized,stride,offset) 将缓冲区对象分配给attribute 变量

  • local attribute变量
  • size 顶点分量的个数,比如上边例子的vertices 数组中,两个数据表示一个顶点,那就写2
  • type 数据类型,比如 gl.FLOAT 浮点型
  • normalized 是否将顶点数据归一
  • stride 相邻两个顶点间的字节数,例子里写的是0,那就是顶点之间是紧挨着的
  • offset 从缓冲区的什么位置开始存储变量,例子里写的是0,那就是从头开始存储变量

这样,着色器就知道缓冲区的数据是给谁的了。

因为上边例子中缓冲区里的顶点数据是数组,里面有多个顶点。所以我们得开启一个让着色器批量处理顶点数据的属性。默认着色器只会一个一个的接收顶点数据,然后一个一个的绘制顶点。

6. 开启顶点数据的批处理功能。

 
gl.enableVertexAttribArray(a_Position);
  • enableVertexAttribArray()参数 attribute变量

7. 绘图

 
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 3);

drawArrays(mode,first,count)

  • mode 绘图模式,比如 gl.POINTS 画点
  • first 从哪个顶点开始绘制
  • count 要画多少个顶点

1.gif

三 基于多点绘制图形

三个点可以确定一个唯一的三角面。

3-1 绘制三角面

在之前绘制多点的基础上做一下修改。

  1. 顶点着色器中的gl_PointSize = 20.0 去掉,因为这个属性是控制顶点大小的,已经不需要显示顶点了。
 
<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
        gl_Position = a_Position;
        //gl_PointSize = 20.0;
    }
</script>
  1. 在js 中修改绘图方式
 
// gl.drawArrays(gl.POINTS, 0, 3);
gl.drawArrays(gl.TRIANGLES, 0, 3);

上面的gl.TRIANGLES 就是绘制三角面的意思。

看一下效果:

image.png

webgl 可以画面了,同样可以在gl.drawArrays() 方法的第一个参数里进行设置画线。

3-2 绘制基本图形——gl.drawArrays(mode,first,count)

gl.drawArrays(mode,first,count) 方法可以绘制以下图形:

  • POINTS 可视的点
  • LINES 单独线段
  • LINE_STRIP 线条
  • LINE_LOOP 闭合线条
  • TRIANGLES 单独三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇

上面的POINTS ,指的是一个个可视的点。

线和面的绘制方式各有三种

3-2-1-点的绘制

POINTS 可视的点

image-20200927115202967

上面六个点的绘制顺序是:v0, v1, v2, v3, v4, v5

3-2-2-线的绘制

1. LINES 单独线段

image-20200927112630534

上面三条有向线段的绘制顺序是:

v0>v1 、 v2>v3 、 v4>v5

2. LINE_STRIP 线条

image-20200927113956696

上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5

3. LINE_LOOP 闭合线条

image-20200927114345495

上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5>v0

3-2-3 面的绘制

对于面的绘制,首先要知道一个原理:

  • 面有正反两面。
  • 面向我们的面,如果是正面,那它必然是逆时针绘制的;
  • 面向我们的面,如果是反面,那它必然是顺时针绘制的;

面的三种绘制方式:

1. TRIANGLES 单独三角形

image-20200927161356266

上面两个面的绘制顺序是: v0>v1>v2、 v3>v4>v5

2. TRIANGLE_STRIP 三角带

image-20200930230050526

上面四个面的绘制顺序是:

  • v0>v1>v2
  • 以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形
  • v2>v1>v3
  • 以上一个三角形的第三条边+下一个点为基础,以和第二条边相反的方向绘制三角形
  • v2>v3>v4
  • 以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形
  • v4>v3>v5

规律:

  • 第一个三角形:v0>v1>v2
  • 第偶数个三角形:以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形
  • 第奇数个三角形:以上一个三角形的第三条边+下一个点为基础,以和第二条边相反的方向绘制三角形
3. TRIANGLE_FAN 三角扇

image-20200927160758122

上面四个面的绘制顺序是:

  • v0>v1>v2
  • 以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形
  • v0>v2>v3
  • 以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形
  • v0>v3>v4
  • 以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形
  • v0>v4>v5

3-3-实例:绘制矩形面

首先,webgl 可以绘制的面只有三角面,所以要绘制矩形面的话,只能用两个三角形去拼。

3-3-1-三角形拼矩形的方法

可以用TRIANGLE_STRIP 三角带拼矩形。

下面的两个三角形分别是:

v0>v1>v2、v2>v1>v3

image-20200930220329539

代码实现——用TRIANGLE_STRIP 三角带拼矩形
  1. 建立顶点数据
 
const vertices=new Float32Array([
    -0.2, 0.2,
    -0.2,-0.2,
    0.2, 0.2,
    0.2,-0.2,
])

上面两个浮点代表一个顶点,依次是v0、v1、v2、v3,如上图所示。

  1. 绘图
 
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

上面参数的意思分别是:三角带、从第0个顶点开始画、画四个。

效果如下:

image.png

就可以简单的绘制出矩形。

接下来可以去尝试其它的图形。比如:把TRIANGLE_STRIP 三角带变成TRIANGLE_FAN 扇形

 
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

画出了一个三角带的样子:

image.png

其绘图顺序是:

v0>v1>v2 、v0>v2>v3

image-20200930230452532

四 异步绘制多点

在项目实战的时候,用户交互事件是异步的,所以必须要考虑异步绘图。

4-1 异步绘制线段

1.先画一个点

image-20210306161928219

2.一秒钟后,在左下角画一个点

image-20210306162145896

3.两秒钟后,我再画一条线段

image-20210306162351559

接下来看一下代码实现:

1.顶点着色器和片元着色器

 
<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          gl_Position=a_Position;
          gl_PointSize=20.0;
      }
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
      void main(){
          gl_FragColor=vec4(1,1,0,1);
      }
</script>

2.初始化着色器

 
import { initShaders } from "../Utils.js";

const canvas = document.querySelector("#canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 获取着色器文本
const vsSource = document.querySelector("#vertexShader").innerText;
const fsSource = document.querySelector("#fragmentShader").innerText;

//三维画笔
const gl = canvas.getContext("webgl");

//初始化着色器
initShaders(gl, vsSource, fsSource);

3.建立缓冲对象,并将其绑定到webgl 上下文对象上,然后向其中写入顶点数据。将缓冲对象交给attribute变量,并开启attribute 变量的批处理功能。

 
//顶点数据
let points=[0, 0.2]
//缓冲对象
const vertexBuffer = gl.createBuffer();
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//写入数据
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(points),gl.STATIC_DRAW)
//获取attribute 变量
const a_Position=gl.getAttribLocation(gl.program, 'a_Position')
//修改attribute 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
//赋能-批处理
gl.enableVertexAttribArray(a_Position)

4.刷底色并绘制顶点

 
//声明颜色 rgba
gl.clearColor(0, 0, 0, 1);
//刷底色
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1);

5.一秒钟后,向顶点数据中再添加的一个顶点,修改缓冲区数据,然后清理画布,绘制顶点

 
setTimeout(()=>{
  points.push(-0.2,-0.1)
  gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(points),gl.STATIC_DRAW)
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.POINTS, 0, 2);
},1000)

6.两秒钟后,清理画布,绘制顶点,绘制线条

 
setTimeout(()=>{
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.POINTS, 0, 2);
    gl.drawArrays(gl.LINE_STRIP, 0, 2);
},2000)
  • 当缓冲区被绑定在了webgl 上下文对象上后,我们在异步方法里直接对WebGLBuffer缓冲区数据进行修改即可,顶点着色器在绘图的时候会自动从其中调用数据。
  • WebGLBuffer缓冲区中的数据在异步方法里不会被重新置空。

4-1-1 异步绘制线段效果

1.gif

 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>webgl绘制三角形</title>
  </head>

  <body>
    <canvas id="canvas"></canvas>

    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          gl_Position = a_Position;
          gl_PointSize = 20.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 type="module">
      import { initShaders } from './Utils.js'
      const canvas = document.querySelector('#canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      //三维画笔
      const gl = canvas.getContext('webgl')
      // 获取着色器文本
      const vsSource = document.querySelector('#vertexShader').innerText
      const fsSource = document.querySelector('#fragmentShader').innerText
      //初始化着色器
      initShaders(gl, vsSource, fsSource)
      //顶点数据
      let points = [0, 0.2]
      //  建立着色器和js 都能进入的缓冲区。
      const vertexBuffer = gl.createBuffer()


      //  缓冲区和着色器建立绑定关系
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
      // 往缓冲区对象中写入数据
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW)
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 将缓冲区对象分配给attribute 变量
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
      // 开启顶点数据的批处理功能
      gl.enableVertexAttribArray(a_Position)

      //底色
      gl.clearColor(0, 0, 0, 1)
      //刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 绘图
      gl.drawArrays(gl.POINTS, 0, 1)
      // 一秒钟后,向顶点数据中再添加的一个顶点,修改缓冲区数据,然后清理画布,绘制顶点
      setTimeout(() => {
        points.push(-0.2, -0.1)
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW)
        gl.clear(gl.COLOR_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 2)
      }, 1000)
      // 两秒钟后,清理画布,绘制顶点,绘制线条
      setTimeout(() => {
        gl.clear(gl.COLOR_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 2)
        gl.drawArrays(gl.LINE_STRIP, 0, 2)
      }, 2000)
    </script>
  </body>
</html>

理解了异步绘图原理后,还可以对这种图形的绘制进行一个简单的封装。

4-2 封装多边形对象——对图形的绘制进行封装

封装一个Poly 对象,辅助理解

 
// poly.js
const defAttr = () => ({
  gl: null, // webgl上下文对象
  vertices: [], // 顶点数据集合,在被赋值的时候会做两件事
  // 1.更新count 顶点数量,数据运算尽量不放渲染方法里
  // 2.向缓冲区内写入顶点数据
  geoData: [], // 模型数据,对象数组,可解析出vertices 顶点数据
  size: 2, // 顶点分量的数目
  attrName: 'a_Position', // 代表顶点位置的attribute 变量名
  count: 0, // 顶点数量
  types: ['POINTS'] // 绘图方式,可以用多种方式绘图
})
export default class Poly {
  constructor(attr) {
    Object.assign(this, defAttr(), attr)
    this.init()
  }
  // init() 初始化方法,建立缓冲对象,并将其绑定到webgl 上下文对象上,
  // 然后向其中写入顶点数据。将缓冲对象交给attribute变量,并开启attribute 变量的批处理功能。
  init() {
    const { attrName, size, gl } = this
    if (!gl) {
      return
    }
    const vertexBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    this.updateBuffer()
    const a_Position = gl.getAttribLocation(gl.program, attrName)
    gl.vertexAttribPointer(a_Position, size, gl.FLOAT, false, 0, 0)
    gl.enableVertexAttribArray(a_Position)
  }
  // 添加顶点
  addVertice(...params) {
    this.vertices.push(...params)
    this.updateBuffer()
  }
  // 删除最后一个顶点
  popVertice() {
    const { vertices, size } = this
    const len = vertices.length
    vertices.splice(len - size, len)
    this.updateCount()
  }
  // 根据索引位置设置顶点
  setVertice(ind, ...params) {
    const { vertices, size } = this
    const i = ind * size
    params.forEach((param, paramInd) => {
      vertices[i + paramInd] = param
    })
  }
  // 更新缓冲区数据,同时更新顶点数量
  updateBuffer() {
    const { gl, vertices } = this
    this.updateCount()
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
  }
  // 更新顶点数量
  updateCount() {
    this.count = this.vertices.length / this.size
  }
  // 基于geoData 解析出vetices 数据
  updateVertices(params) {
    const { geoData } = this
    const vertices = []
    geoData.forEach((data) => {
      params.forEach((key) => {
        vertices.push(data[key])
      })
    })
    this.vertices = vertices
  }
  // 绘图方法
  draw(types = this.types) {
    const { gl, count } = this
    for (let type of types) {
      gl.drawArrays(gl[type], 0, count)
    }
  }
}

接下来用Poly 对象实现之前的案例。

 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>webgl绘制三角形</title>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          gl_Position = a_Position;
          gl_PointSize = 20.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 type="module">
      import { initShaders } from './Utils.js'
      import Poly from './poly.js'

      const canvas = document.querySelector('#canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      //三维画笔
      const gl = canvas.getContext('webgl')

      // 获取着色器文本
      const vsSource = document.querySelector('#vertexShader').innerText
      const fsSource = document.querySelector('#fragmentShader').innerText

      //初始化着色器
      initShaders(gl, vsSource, fsSource)
      //顶点数据
      let points = [0, 0.2]
      //  建立着色器和js 都能进入的缓冲区。
      const vertexBuffer = gl.createBuffer()
      console.log('webgl缓冲区', vertexBuffer)

      //  缓冲区和着色器建立绑定关系
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
      // 往缓冲区对象中写入数据
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW)
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 将缓冲区对象分配给attribute 变量
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
      // 开启顶点数据的批处理功能
      gl.enableVertexAttribArray(a_Position)

      //底色
      gl.clearColor(0, 0, 0, 1)
      //刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 使用封装的Poly对象绘图
      const poly = new Poly({
        gl,
        vertices: [0, 0.2]
      })
      poly.draw(['POINTS'])

      setTimeout(() => {
        poly.addVertice(-0.2, -0.1)
        gl.clear(gl.COLOR_BUFFER_BIT)
        poly.draw(['POINTS'])
      }, 1000)

      setTimeout(() => {
        gl.clear(gl.COLOR_BUFFER_BIT)
        poly.draw(['POINTS', 'LINE_STRIP'])
      }, 2000)
    </script>
  </body>
</html>

1.gif

异步绘图原理跑通了,也就可以用鼠标绘制线条了。

1.gif

 
//实例化多边形
const poly=new Poly({
    gl,
    types:['POINTS','LINE_STRIP']
})

// 鼠标点击事件
canvas.addEventListener("click", (event) => {
    const {x,y}=getMousePosInWebgl(event,canvas)
    poly.addVertice(x,y)
    gl.clear(gl.COLOR_BUFFER_BIT);
    poly.draw()
});
// 获取鼠标点击的webgl坐标
function getMousePosInWebgl(event, canvas) {
    const { clientX, clientY } = event
    const { left, top, width, height } = canvas.getBoundingClientRect()
    const [cssX, cssY] = [clientX - left, clientY - top]
    const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
    const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
    const yBaseCenterTop = -yBaseCenter
    return { x: xBaseCenter / halfWidth, y: yBaseCenterTop / halfHeight }
}

当前每次所能画的只有一条线条

4-3 绘制多条线

如何绘制多条线呢?比如要画一个狮子座。

image-20210309094648689

绘制多条线,需要有个容器来承载它们,这样方便管理。

4-3-1 建立容器对象

建立一个Sky 对象,作为承载多边形的容器。

 
// Sky.js
export default class Sky {
  constructor(gl) {
    this.gl = gl // webgl上下文对象
    this.children = [] // 子级,都是Poly实例
  }
  // 添加子对象
  add(obj) {
    obj.gl = this.gl
    this.children.push(obj)
  }
  // 更新子对象的顶点数据
  updateVertices(params) {
    this.children.forEach((ele) => {
      ele.updateVertices(params)
    })
  }
  // 遍历子对象绘图,每个子对象对应一个poly对象,
  // 所以在子对象绘图之前要先初始化
  draw() {
    this.children.forEach((ele) => {
      ele.init()
      ele.draw()
    })
  }
}

4-3-2 示例场景——绘制多边形路径

一个示例场景:鼠标点击画布,绘制多边形路径。鼠标右击,取消绘制。鼠标再次点击,绘制新的多边形。

1.gif

 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>webgl绘制</title>
  </head>

  <body>
    <canvas id="canvas"></canvas>

    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main(){
          gl_Position = a_Position;
          gl_PointSize = 20.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 type="module">
      import { initShaders } from './Utils.js'
      import Poly from './poly.js'
      import Sky from './Sky.js'

      const canvas = document.querySelector('#canvas')
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
      //三维画笔
      const gl = canvas.getContext('webgl')

      // 获取着色器文本
      const vsSource = document.querySelector('#vertexShader').innerText
      const fsSource = document.querySelector('#fragmentShader').innerText

      //初始化着色器
      initShaders(gl, vsSource, fsSource)
      //顶点数据
      let points = [0, 0.2]
      //  建立着色器和js 都能进入的缓冲区。
      const vertexBuffer = gl.createBuffer()
      console.log('webgl缓冲区', vertexBuffer)

      //  缓冲区和着色器建立绑定关系
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
      // 往缓冲区对象中写入数据
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW)
      const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
      // 将缓冲区对象分配给attribute 变量
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
      // 开启顶点数据的批处理功能
      gl.enableVertexAttribArray(a_Position)

      //底色
      gl.clearColor(0, 0, 0, 1)
      //刷底色
      gl.clear(gl.COLOR_BUFFER_BIT)
      // 使用封装的Poly对象绘图
      //实例化多边形

      function getMousePosInWebgl(event, canvas) {
        const { clientX, clientY } = event
        const { left, top, width, height } = canvas.getBoundingClientRect()
        const [cssX, cssY] = [clientX - left, clientY - top]
        const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
        const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
        const yBaseCenterTop = -yBaseCenter
        return { x: xBaseCenter / halfWidth, y: yBaseCenterTop / halfHeight }
      }
      //夜空
      const sky = new Sky(gl)
      //当前正在绘制的多边形
      let poly = null

      //取消右击提示
      canvas.oncontextmenu = function () {
        return false
      }
      // 鼠标点击事件
      canvas.addEventListener('mousedown', (event) => {
        if (event.button === 2) {
          // 点击鼠标右键取消当前线段绘制,poly变量重置为null
          popVertice()
        } else {
          const { x, y } = getMousePosInWebgl(event, canvas)
          if (poly) {
            poly.addVertice(x, y)
          } else {
            crtPoly(x, y)
          }
        }
        render()
      })
      //鼠标移动
      canvas.addEventListener('mousemove', (event) => {
        if (poly) {
          const { x, y } = getMousePosInWebgl(event, canvas)
          poly.setVertice(poly.count - 1, x, y)
          render()
        }
      })

      //删除最后一个顶点
      function popVertice() {
        poly.popVertice()
        poly = null
      }
      //创建多边形
      function crtPoly(x, y) {
        poly = new Poly({
          vertices: [x, y, x, y],
          types: ['POINTS', 'LINE_STRIP']
        })
        sky.add(poly)
      }
      // 渲染方法
      function render() {
        gl.clear(gl.COLOR_BUFFER_BIT)
        console.log(sky)
        sky.draw()
      }
    </script>
  </body>
</html>

image.png

五 扩展知识-图形转面

5-1 webgl三种面的适应场景

之前说过,webgl 可以绘制三种面:

  • TRIANGLES 单独三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇

在实际的引擎开发中,TRIANGLES 是用得最多的。

TRIANGLES 的优势是可以绘制任意模型,缺点是比较费点。

适合TRIANGLES 单独三角形的的模型:

image-20210312161835601

TRIANGLE_STRIP 和TRIANGLE_FAN 的优点是相邻的三角形可以共用一条边,比较省点,然而其缺点也明显,因为它们只适合绘制具备相应特点的模型。

适合TRIANGLE_STRIP三角带的模型:

image-20210312161129484

适合TRIANGLE_FAN三角扇的模型:

image-20210312161335598

three.js 使用的绘制面的方式就是TRIANGLES,可以在其WebGLRenderer 对象的源码的中找到:

image.png

5-2 图形转面

在three.js 里有一个图形几何体ShapeGeometry,可以把图形变成面。

image-20210311153236165

只要有数学支撑,也可以实现这种效果。

posted on 2025-09-09 16:33  漫思  阅读(10)  评论(0)    收藏  举报

导航