将OpenGL提供的库函数接口进行一层封装。
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。
<!-- GLContext单例 GL上下文对象 --> <script type="text/javascript"> class GLContext { static width = 0 static height = 0 constructor() { this.gl = null } static getWebGL() { if (!this.gl) { // 获取html中的glCanvas let canvas = document.getElementById('glCanvas') //.transferControlToOffscreen() GLContext.width = canvas.width GLContext.height = canvas.height // 获取glCanvas中的gl上下文 this.gl = canvas.getContext('webgl2') } return this.gl } } </script>
由于大部分GL相关的封装类,都有着共同的方法,因此写一个基类。
<!-- GLObject GL对象的基类 --> <script type="text/javascript"> class GLObject { constructor() { this.gl = GLContext.getWebGL() } bind() {} unbind() {} } </script>
OpenGL在创建一个“笔刷”的时候需要先创建vertex shader
和fragment shader
。
<!-- 用于创建:顶点着色器 or 片元着色器 GLShader GL着色器 --> <script type="text/javascript"> class GLShader extends GLObject { /** * @param sourceCode 元素id、shaderCode字符串、元素对象 * @param type shader的类型 */ constructor(sourceCode, type) { super() if (sourceCode == undefined || type.constructor != Number) { // sourceCode为空 或者 type着色器类型不是数字 throw "sourceCode is undefined or type not number. \n"; } else if (sourceCode.constructor == String) { // sourceCode本身是一个字符串, 要么是shader源码, 要么是标签的id if (sourceCode[0] == '#') { // sourceCode是一个id sourceCode = $(sourceCode)[0].innerHTML } } else if (sourceCode.constructor == HTMLScriptElement) { // sourceCode是一个元素对象 sourceCode = sourceCode.innerHTML } else { // 其他情况直接错误 throw "Create Shader Error. \n"; } // 创建对应type的着色器 this.id = GLShader.createShader(this.gl, sourceCode, type) } /** * @param gl gl上下文 * @param sourceCode shaderCode字符串 * @param type shader的类型 */ static createShader(gl, sourceCode, type) { // 创建对应type的shader句柄 let shader = gl.createShader(type); // 绑定sourceCode和type gl.shaderSource(shader, sourceCode); // 编译shader gl.compileShader(shader); // 检查错误 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { // 获取shader编译信息 let info = gl.getShaderInfoLog(shader); // 释放shader gl.deleteShader(shader) // 抛出错误 throw "Could not compile WebGL program. \n" + (type == gl.VERTEX_SHADER ? "vertex" : "fragment") + " shader error. \n" + info; } // 创建完成 return shader; } delete() { // 释放shader this.gl.deleteShader(this.id) } } </script>
真正的“笔刷(Program)”,需要传入vertex shader和fragment shader,才能成为一个program
。
<!-- 用于创建:笔刷(着色器程序) GLProgram 着色器程序 --> <script type="text/javascript"> class GLProgram extends GLObject { /** * @param vsCode vs元素id、vsCode字符串、元素对象(顶点着色器) * @param fsCode fs元素id、fsCode字符串、元素对象(片元着色器) */ constructor(vsCode, fsCode) { super() if (vsCode == undefined || fsCode == undefined) { // vsCode为空 或者 fsCode为空 throw "vsCode is undefined or fsCode is undefined. \n"; } // gl句柄 let gl = this.gl // 创建vs和fs let vertexShader = new GLShader(vsCode, gl.VERTEX_SHADER) let fragmentShader = new GLShader(fsCode, gl.FRAGMENT_SHADER) // 创建shader program let program = gl.createProgram() // 将vs、fs与program绑定 gl.attachShader(program, vertexShader.id) gl.attachShader(program, fragmentShader.id) // 链接shader program gl.linkProgram(program) // 检查错误 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { // 获取program链接信息 let info = gl.getProgramInfoLog(program) // 释放program this.delete() // 抛出错误 throw 'WebGL program compile error. \n' + info } // 保存vs、fs、program的句柄 this.vertexShader = vertexShader this.fragmentShader = fragmentShader this.id = program } /** * 使用program脚本 */ use() { this.gl.useProgram(this.id) } /** * 删除program脚本,释放内存 */ delete() { this.vertexShader.delete() this.fragmentShader.delete() this.gl.deleteProgram(this.id) } } </script>
在use program
后,program还可能需要颜料(attribute/uniform)
,为此包装对应的attribute和uniform
。
<!-- GLAtribute GLUniform --> <script type="text/javascript"> class GLAttribute extends GLObject { /** * @param program 着色器对象、着色器id * @param name 属性名 */ constructor(program, name) { super() if (program == undefined || name == undefined) { // program为空 或者 name为空 throw "program is undefined or name is undefined. \n"; } else if (program.constructor != GLProgram) { // program不是GLProgram类型的 throw "typeof(" + program + ") != GLProgram. \n"; } // program id program = program.id // gl句柄 let gl = this.gl // 获取program中name的id let id = gl.getAttribLocation(program, name) if (id < 0) { // 在gl中, 获取到的id不可能小于0 (小于0说明name错误) throw "attribute " + name + " id < 0. \n"; } // 保存id this.id = id } /** * 绑定Attribute */ bind() { this.gl.enableVertexAttribArray(this.id) } /** * 解绑定Attribute */ unbind() { this.gl.disableVertexAttribArray(this.id) } /** * 设置Attribute属性 */ vertexAttribPointer(size, type, normalized, stride, offset) { this.gl.vertexAttribPointer(this.id, size, type, normalized, stride, offset) } } class GLUniform extends GLObject { /** * @param program 着色器对象、着色器id * @param name 属性名 */ constructor(program, name) { super() if (program == undefined || name == undefined) { // program为空 或者 name为空 throw "program is undefined or name is undefined \n"; } else if (program.constructor != GLProgram) { // program不是GLProgram类型的 throw "typeof(" + program + ") != GLProgram. \n"; } // program id program = program.id // gl句柄 let gl = this.gl // 获取program中name的id let id = gl.getUniformLocation(program, name) if (id < 0) { // 在gl中, 获取到的id不可能小于0 (小于0说明name错误) throw "uniform " + name + " id < 0 \n"; } // 保存id this.id = id } uniform1f(v0) { this.gl.uniform1f(this.id, v0) } uniform1fv(value) { this.gl.uniform1fv(this.id, value) } uniform1i(v0) { this.gl.uniform1i(this.id, v0) } uniform1iv(value) { this.gl.uniform1iv(this.id, v0) } uniform2f(v0, v1) { this.gl.uniform2f(this.id, v0, v1) } uniform2fv(value) { this.gl.uniform2fv(this.id, value) } uniform2i(v0, v1) { this.gl.uniform2i(this.id, v0, v1) } uniform2iv(value) { this.gl.uniform2iv(this.id, value) } uniform3f(v0, v1, v2) { this.gl.uniform3f(this.id, v0, v1, v2) } uniform3fv(value) { this.gl.uniform3fv(this.id, value) } uniform3i(v0, v1, v2) { this.gl.uniform3i(this.id, v0, v1, v2) } uniform3iv(value) { this.gl.uniform3iv(this.id, value) } uniform4f(v0, v1, v2, v3) { this.gl.uniform4f(this.id, v0, v1, v2, v3) } uniform4fv(value) { this.gl.uniform4fv(this.id, value) } uniform4i(v0, v1, v2, v3) { this.gl.uniform4i(this.id, v0, v1, v2, v3) } uniform4iv(value) { this.gl.uniform4iv(this.id, value) } uniformMatrix2fv(location, transpose, value) { this.gl.uniformMatrix2fv(this.id, transpose, value) } uniformMatrix3fv(location, transpose, value) { this.gl.uniformMatrix2fv(this.id, transpose, value) } uniformMatrix4fv(location, transpose, value) { this.gl.uniformMatrix2fv(this.id, transpose, value) } } </script>
OpenGL在绘制时需要知道画在**哪里(vertex/uv)
**也就是buffer
。
<!-- GLBuffer --> <script type="text/javascript"> class GLBuffer extends GLObject { /** * @param buffer 数据信息 */ constructor(buffer) { super() if (buffer == undefined) { // buffer为空 throw "buffer is undefined \n"; } // gl句柄 let gl = this.gl // vbo let vbo = gl.createBuffer() // 绑定vbo gl.bindBuffer(gl.ARRAY_BUFFER, vbo) // vbo数据设置 gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW) // 保存vbo id this.id = vbo } /** * 绑定vbo */ bind() { this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.id) } /** * 释放vbo */ delete() { this.gl.deleteBuffer(this.id) } } </script>
已经准备好笔刷、颜料、画什么东西,最后还差画在哪里(fbo)
。当然我们还可以直接拿一张图贴到fbo上,也就是直接拿一张**贴纸(texture)
**画在fbo
上。
<!-- GLTexture GLFramebuffer --> <script type="text/javascript"> class GLTexture extends GLObject { /** * @param imageUrl 图片 */ constructor(imageUrl) { super() if (imageUrl == undefined) { // 路径为空 throw "imageUrl is undefined. \n"; } else if (imageUrl.constructor == String) { // 路径不为空 if (imageUrl[0] == '#') { // 获取src imageUrl = $($(imageUrl)[0]).attr("src") } } // gl句柄 let gl = this.gl // this句柄 let that = this // 创建image let image = new Image() // 服务器不支持跨域访问的话就会被拦截 image.crossOrigin = "Anonymous" // image加载成功后的回调 (ps: 这里可能会出现加载的时序问题) image.onload = function() { // 创建texture id let textureId = gl.createTexture() // 绑定texture gl.bindTexture(gl.TEXTURE_2D, textureId) // 设置texture属性 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) // 设置像素存储模式 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) // 设置texture格式 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) if (textureId <= 0) { // 创建成功的texture id必然大于0, 小于说明失败, 因此释放texture gl.deleteTexture(textureId) // 抛出错误 throw "texture create error. \n"; } // 保存id that.id = textureId // 解绑定 (ps: 最好做, 为了保持一致, 这个原因是因为后面可能会有绘制也会用到gl.bindTexture(gl.TEXTURE_2D, textureId)) gl.bindTexture(gl.TEXTURE_2D, null) } // image加载失败后的回调 image.onerror = function() { alert("get image error") } // 设置image的src, 设置后将加载 image.src = imageUrl } /** * 绑定texture */ bind() { this.gl.bindTexture(this.gl.TEXTURE_2D, this.id) } /** * 释放texture */ delete() { this.gl.deleteTexture(this.id) } } class GLFramebuffer extends GLObject { /** * 一般来说, FBO是framebuffer和texture绑定 * @param texture 纹理 */ constructor(texture) { // 创建fbo let framebuffer = gl.createFramebuffer() // 绑定fbo gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) // 保存texture对象 if (texture == undefined) { this.texture = undefined } else if (texture.constructor == GLTexture) { this.texture = texture } else if (texture.constructor == String) { this.texture = new GLTexture(texture) } // 将fbo和texture绑定 if (texture != undefined) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture.id, 0) } // 保存fbo id this.id = framebuffer } /** * 绑定fbo */ bind() { this.gl.bindFramebuffer(this.FRAMEBUFFER, this.id) } /** * 释放fbo */ delete() { this.texture.delete() this.deleteFramebuffer(this.id) } } </script>
画布,WebGL需要先创建一个Canvas,通过Canvas拿到WebGLContext。
<div style="display: flex;"> <canvas id="glCanvas" width="680" height="640"> 你的浏览器似乎不支持或者禁用了HTML5 <code><canvas></code> 元素. </canvas> </div>
顶点着色器的代码。
<!-- shader脚本 --> <script id="shader-vs" type="x-shader/x-vertex"> attribute vec4 iPosition; attribute vec2 iTexcoord; uniform float iTime; varying highp vec2 vTexcoord; void main(void) { gl_Position = iPosition; vTexcoord = iTexcoord; } </script>
片段着色器的代码。
<script id="shader-fs" type="x-shader/x-fragment"> uniform sampler2D iChannel0; uniform highp float iTime; varying highp vec2 vTexcoord; void main(void) { gl_FragColor = texture2D(iChannel0, vTexcoord); } </script>
纹理图片。
<img id="iChannel0" src="images/src0.jpg" class="hidden"/> <img id="iChannel1" src="images/src1.jpg" class="hidden"/> <!-- .................. <img id="iChannel7" src="images/src7.jpg" class="hidden"/> -->
这只是一个调用以上封装好GL接口的实例
。
<script type="text/javascript"> class ShaderToyGame { constructor() { let gl = GLContext.getWebGL() // 确认WebGL支持性 if (!gl) { alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。") return } // 使用完全不透明的黑色清除所有图像 gl.clearColor(0.0, 1.0, 0.0, 1.0) // 用上面指定的颜色清除缓冲区 gl.clear(gl.COLOR_BUFFER_BIT) // 设置视口 gl.viewport(0, 0, GLContext.width, GLContext.height) } run(isRun) { if (this.isRun == isRun) { // 状态一样, 直接返回 return; } // 是否运行, false会停止运行 this.isRun = isRun; if (isRun) { // 开启request requestAnimationFrame(renderGL) } } renderGL(now) { // gl上下文 let gl = GLContext.getWebGL() // 使用program this.program.use() // 绑定和输入顶点数据 this.verteBuffer.bind() this.vertexPosition.bind() this.vertexPosition.vertexAttribPointer(3, gl.FLOAT, false, 0, 0) // 绑定和输入纹理数据 this.texcoordBuffer.bind() this.texcoordPosition.bind() this.texcoordPosition.vertexAttribPointer(2, gl.FLOAT, false, 0, 0) // 输入时间数据 this.timeUniform.uniform1f(now * 0.001) // 输入纹理数据 for(let i = 0; i < this.maxSize; i++) { if(this.textures[i]) { gl.activeTexture(gl.TEXTURE0+i) this.textures[i].bind() this.iChannels[i].uniform1i(i) } } // 绘制 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) // 递归调用 if(this.isRun) requestAnimationFrame(renderGL) } render() { let gl = GLContext.getWebGL() // 顶点坐标 let vertices = [ 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0 ] // 纹理坐标 let texcoord = [ 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0 ] // 顶点buffer this.verteBuffer = new GLBuffer(new Float32Array(vertices)) // 纹理buffer this.texcoordBuffer = new GLBuffer(new Float32Array(texcoord)) // 着色器program let program = new GLProgram("#shader-vs", "#shader-fs") this.program = program // 顶点属性 let vertexPosition = new GLAttribute(program, "iPosition") // 纹理属性 let texcoordPosition = new GLAttribute(program, "iTexcoord") // 时间属性 let timeUniform = new GLUniform(program, "iTime") // 纹理数组 let iChannels = [] this.textures = [] // texture最大数量 this.maxSize = 8 // 创建iChannels属性数组和texture数组 for(let i = 0; i < this.maxSize; i++) { let iChannel = new GLUniform(program, 'iChannel'+i) let texture = undefined if(iChannel.id) { texture = new GLTexture('#iChannel'+i) } iChannels.push(iChannel) this.textures.push(texture) } // 开始渲染 run(true); } destory() { this.isRun = false this.program.delete() this.verteBuffer.delete() this.texcoordBuffer.delete() for(let i = 0; i < this.maxSize; i++) { if(this.textures[i]) { this.textures[i].delete() } } } } </script>
<script type="text/javascript"> // 主函数 function main() { let stg = new ShaderToyGame() stg.render() } main() </script>