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 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>
着色器程序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><canvas></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>