Zhili's blog Zhili's blog
首页
关于
GitHub (opens new window)

Zhili

转型中的前端老兵
首页
关于
GitHub (opens new window)
  • webpack 区分环境使用CDN以及HtmlWebpackPlugin插件的编写
  • 使用 Mpvue 开发小程序总结
  • 前端知识汇总-持续更新
  • webGL知识汇总-持续更新
  • React思想要点
  • Mapbox 入门初试
  • 构建自己的 GLSL 绘图器 - 2d 版
    • webGL二维有向距离场(SDF)及布尔运算
    • webGL入门:绘制一个三角形
    • 使用 GLSL 绘图尝试:绘制正弦曲线
    • 简单解析虚拟 DOM
    • 自己动手开发一个 markdown 转微信文章工具
    • Vue组件扩展及权限管理的实现技巧
    • 使用 CSS 绘制三角形
    • Nginx 前端配置和使用
    • webpack按需加载配置和babel编译
    • 面试总结39题
    • 使用node反向代理接口改造旧项目
    • 可视化表单搭建系统的开发与思考
    • 前端
    zhili
    2019-07-30

    构建自己的 GLSL 绘图器 - 2d 版

    创建一个简版的绘图类,它需要能初始化我们的绘图区域、加载 shader 文件、在 fragment shader 中提供一些全局变量这些基本的功能。

    学习 GLSL 绘图有一段时间了,感觉现在才刚刚摸到门路,到现在能够绘制一些简单的图形了,比如圆和矩形。其他复杂的形状比较难以绘制,GLSL 绘图感觉难点在于数学上,如何将图形用数学公式表达出来,用向量的加减乘除表达出来,这是最难的了,入门还得好久。

    先撇开这些困难的 GLSL 绘图函数,先把基础搭建起来。要使用 GLSL 绘图,我们先创建一个简版的绘图类,它需要能初始化我们的绘图区域、加载 shader 文件、在 fragment shader 中提供一些全局变量这些基本的功能,那么,我们来一步一步的实现。

    # 1. 初始化绘图区域

    使用 GLSL 绘图,我们只要是在片元着色器中编写绘图方法,因此,我们只需要绘制一个矩形区域作为绘图面板即可。一般来说顶点着色器是不需要修改的,我们提供一个默认的顶点着色器:

    #ifdef GL_ES
    precision mediump float;
    #endif
    
    attribute vec4 aPosition;
    
    void main(){
      gl_Position=aPosition;
      gl_PointSize=1.;
    }`,
    fragmentShader:`#ifdef GL_ES
    precision mediump float;
    #endif
    void main(){
      gl_FragColor=vec4(0.,0.,0.,1.);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    以及一个默认的片元着色器:

    #ifdef GL_ES
    precision mediump float;
    #endif
    void main(){
      gl_FragColor=vec4(0.,0.,0.,1.);
    }
    
    1
    2
    3
    4
    5
    6

    在初始化绘图区域的时候,我们绘制 2 个三角形组成一个矩形绘图区即可,至于如何绘制矩形等等基础知识这里就不一一复述了。想了解的可以前往webgl-fundamentals (opens new window) 学习。

    # 2. 加载 shader 文件

    在 webGL 中,shader 文件是以字符串的形式存在的,并且在传递给 webgl 对象进行编译处理的。如果我们以字符串形式存储我们的片元着色器的话,不仅在编写上繁琐,而且编辑器无法提供高亮与格式化支持,在组织上也是十分不友好的。因此,我们以.glsl 文件的形式存储片元着色器。

    同时,在 webGL 中片元着色器是没有模块这一说法的,想要复用我们编写的着色器只能 Ctr+c,Ctr+v 了,但是,作为程序员,一段代码最好不要 ctr+v 超过 3 次。既然着色器代码是以字符串形式存在的,我们只要在传递给 webgl 之前处理成正确的格式就能实现代码复用了,而不用手动复制粘贴。

    # 2.1 实现加载器

    glsl 文件加载器十分简单,只需要使用 xhr 请求 glsl 文件获取文本即可:

    async loadGLSL(name) {
        if (name) {
          const errorMsg = 'load glsl file failed: file name is ' + name
          try {
            const res = await fetch(this.baseFragPath + name)
            if (res.ok) {
              return await res.text()
            } else {
              throw errorMsg
            }
          } catch (error) {
            console.error(errorMsg)
            throw error
          }
        } else {
          console.error('glsl file name is required')
        }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 2.2 实现 glsl 模块化

    由于 glsl 并没有制定模块化规则,我们可以定制自己的规则,比如,我们可以约定使用下面的语法作为我们加载 glsl 模块的方法:

    #include <name.glsl>
    
    1

    在 glsl 中以#开头为注释语法,不会影响到其他的语句,我们只需要匹配出 name.glsl ,并将这段语句替换成 name.glsl 文件的内容就可以实现模块化了。

    我们在将 shader 传递给 webgl 之前对其进行格式化处理,匹配出所有的 #include <>语句并进行替换,这里需要使用正则进行处理:

    格式化处理函数如下,使用递归进行处理:

    async _formatterCode(glslCode) {
        try {
          let code = glslCode
          // 判断是否包含 #include <*.glsl>
          const reg = /#include <(.*?.glsl)>/g
          if (reg.test(code)) {
            // 替换 include代码
            const includes = this._getIncludeGLSL(code)
            await Promise.all(includes.map(async item => {
              const subCode = await this.loadGLSL(item.target)
              const formatSubCode = await this._formatterCode(subCode)
              code = code.replace(item.reg, formatSubCode)
            }))
          }
          return code
        } catch (err) {
          const errorMsg = `load ${fileName} glsl file failed,check Is the include format correct`
          console.error(errorMsg)
          throw new Error(errorMsg)
        }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    正则匹配方法如下所示:

    _getIncludeGLSL(glsl) {
        try {
          const reg = /#include <(.*?.glsl)>/g
          const arr = [];
          let r = null
          while (r = reg.exec(glsl)) {
            arr.push({
              reg: r[0],
              target: r[1]
            })
          }
          return arr
        } catch (error) {
          const errorMSg = 'the include format is not correct'
          console.log(errorMSg, error)
          throw error
        }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    这样我们就可以放心的使用我们自定义的模块化语法,实现了着色器代码的复用

    # 3. 提供一些有用的全局变量

    在使用片元着色器绘图,我们常用到的变量是:绘图区域分辨率、时间、鼠标位置这三个变量。我们预定三个变量值为 uResolution、uTime、uMouse,判断在着色器中是否启用了这些值,如果有则将变量值传递,否则不需要进行赋值。 例如 uResolution:

    const uResolution = this.gl.getUniformLocation(this.program, 'uResolution')
    if (uResolution) {
      this.gl.uniform2f(uResolution, this.gl.canvas.width, this.gl.canvas.height)
    }
    
    1
    2
    3
    4

    对于 uTime,如果启用,我们需要使用 requestAnimationFrame 进行循环绘图,以将时间传递给着色器,为了节约性能,我们设置需要手动启用时间:

    const uTimeLocation = this.gl.getUniformLocation(this.program, 'uTime')
    
    const animateDraw = () => {
      const time = new Date().getTime() - this.clock
      this.gl.uniform1f(uTimeLocation, time / 1000)
      commonDraw()
      this._animateInterval = requestAnimationFrame(animateDraw)
    }
    
    const commonDraw = () => {
      this.gl.clearColor(0.0, 0.0, 0.0, 1.0)
      this.gl.clear(this.gl.COLOR_BUFFER_BIT)
      this.gl.drawElements(this.gl.TRIANGLES, indexes.length * 3, this.gl.UNSIGNED_BYTE, 0)
    }
    
    if (this._animateInterval) {
      cancelAnimationFrame(this._animateInterval)
      console.log('clear animation frame')
    }
    
    if (this.enableTime && uTimeLocation) {
      console.log('start animate draw')
      this.clock = new Date().getTime()
      animateDraw()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    到目前为止,我们的 GLSL 绘图器基本可用了,如下使用:

    const gRender = new GRender({
      canvas: document.getElementById('gl-canvas'),
      basePath: './fragments/'
    })
    
    async function runCode() {
      try {
        const fragVal = monacoIns.getValue()
        const enableTime = fragVal.indexOf('uniform float uTime;')
        gRender.enableTime = enableTime !== -1
        await gRender.renderByShader(fragVal)
      } catch (e) {
        console.log(e)
      }
    }
    
    gRender
      .loadGLSL('wall.glsl')
      .then(code => {
        runCode()
      })
      .catch(err => {
        console.log('加载wall.glsl失败', err)
      })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    我们可以专心于编写着色器代码,执行不同的着色器只需要修改gRender.loadGLSL()方法中的着色器名称。

    [注]:GRender详细代码可前往我的webGL-webGIS-Learning (opens new window) 代码仓库中查看

    编辑 (opens new window)
    #GLSL
    上次更新: 2022/03/14, 06:54:51
    Mapbox 入门初试
    webGL二维有向距离场(SDF)及布尔运算

    ← Mapbox 入门初试 webGL二维有向距离场(SDF)及布尔运算→

    最近更新
    01
    可视化表单搭建系统的开发与思考
    03-09
    02
    使用node反向代理接口改造旧项目
    11-17
    03
    面试总结39题
    10-11
    更多文章>
    Theme by Vdoing | Copyright © 2022-2023 zhili | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式