Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
zhouzhili opened this issue Jul 30, 2019 · 0 comments
Open

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

zhouzhili opened this issue Jul 30, 2019 · 0 comments

Comments

@zhouzhili
Copy link
Owner

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

学习 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.);
}

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

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

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

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')
    }
  }

2.2 实现 glsl 模块化

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

#include <name.glsl>

在 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)
    }
  }

正则匹配方法如下所示:

_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
    }
  }

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

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)
}

对于 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()
}

到目前为止,我们的 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)
  })

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant