JavaScript OpenGL接口再设计

Web OpenGL封装

简介

将OpenGL提供的库函数接口进行一层封装。

上下文GLContext

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>

基类GLObject

由于大部分GL相关的封装类,都有着共同的方法,因此写一个基类。

<!--
    GLObject
      GL对象的基类
  -->
<script type="text/javascript">
    class GLObject {
        constructor() {
            this.gl = GLContext.getWebGL()
        }
        bind() {}
        unbind() {}
    }
</script>

着色器GLShader

OpenGL在创建一个“笔刷”的时候需要先创建vertex shaderfragment 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>

着色器程序GLProgram

真正的“笔刷(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>

着色器程序属性GLAttribute、GLUniform

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>

数据缓存GLBuffer

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>

GLTexture和GLFrameBuffer

已经准备好笔刷、颜料、画什么东西,最后还差画在哪里(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>

html标签

Canvas

画布,WebGL需要先创建一个Canvas,通过Canvas拿到WebGLContext

<div style="display: flex;">
    <canvas id="glCanvas" width="680" height="640">
        你的浏览器似乎不支持或者禁用了HTML5 <code>&lt;canvas&gt;</code> 元素.
    </canvas>
</div>

vertex shader

顶点着色器的代码。

<!-- 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>

fragment shader

片段着色器的代码。

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

iChannels

纹理图片。

<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"/>
-->

主程序ShaderToyGame

这只是一个调用以上封装好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>
上一篇:window mysql5.6


下一篇:webgl画一个和多个点