构建自己的 GLSL 绘图器 - 2d 版
zhouzhili opened this issue · 0 comments
构建自己的 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 代码仓库中查看