接着 WebGL 基础概念,做一个绘制直线的简单示例。
主要参考以下两篇文章:
绘制一个点
绘制三角形
Origin
My GitHub
下面不会对每个使用的函数进行详细的解释,个人比较喜欢先对整体逻辑有个感觉,实际使用时再按需去查资料。
在基础概念中有提过是通过 Canvas 元素使用 WebGL :
<canvas id="demo" width="300" height="200"></canvas>
const canvasObj = document.querySelector("#demo"); const glContext = canvasObj.getContext("webgl"); if (!glContext) { alert("浏览器不支持 WebGL"); return; }
接着准备顶点数据。
在 WebGL 中所有实物都是在 3D 空间中,绘制一条线需要两个顶点,每个顶点都有一个 3D 坐标:
let vertices = [ -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ];
缓冲有多种类型,顶点缓冲对象的类型是 gl.ARRAY_BUFFER
。
/** * 设置缓冲 * @param {*} gl WebGL 上下文 * @param {*} vertexData 顶点数据 */ function setBuffers(gl, vertexData) { // 创建空白的缓冲对象 const buffer = gl.createBuffer(); // 绑定目标 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // WebGL 不支持直接使用 JavaScript 原始数组类型,需要转换 const dataFormat = new Float32Array(vertexData); // 初始化数据存储 gl.bufferData(gl.ARRAY_BUFFER, dataFormat, gl.STATIC_DRAW); },
bufferData 方法会把数据复制到当前绑定缓冲对象,该方法提供了管理给定数据的参数:
直线的数据不会改变,每次渲染都保持不变,所以这里使用的类型是 STATIC_DRAW
。现在已经把顶点数据储存在显卡的内存中,接着开始准备顶点着色器。
顶点着色器需要用 GLSL ES 语言编写,在前端书写形式有两种:
<script id="shader" type="x-shader/x-vertex"> attribute vec3 vertexPos; void main(void){ gl_Position = vec4(vertexPos, 1); } </script> <script> const shader = document.getElementById('shader').innerHTML, </script>
每个顶点都有一个 3D 坐标,创建了一个 vec3
类型输入变量 vertexPos
,vec3
表示三元组浮点数向量。
main
是入口函数,gl_Position
是着色器内置的变量,GLSL 中一个变量最多 4 个分量,最后一个分量是用在透视除法上。gl_Position
设置的值会成为该顶点着色器的输出。这里请回想一下基础概念中提到的状态机。
下面是纯字符形式:
/** * 创建顶点着色器 * @param {*} gl WebGL 上下文 */ function createVertexShader(gl) { // 顶点着色器 glsl 代码 const source = ` attribute vec3 vertexPos; void main(void){ gl_Position = vec4(vertexPos, 1); } `; // 创建着色器 const shader = gl.createShader(gl.VERTEX_SHADER); // 设置顶点着色器代码 gl.shaderSource(shader, source); // 编译 gl.compileShader(shader); // 判断是否编译成功 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("编译着色器报错: " + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; }
为了让 WebGL 使用该着色器,必须在运行时动态编译它的源代码。
gl.VERTEX_SHADER
的着色器对象;接着准备片段着色器。
Back to top片段着色器也是用 GLSL ES 语言编写。片段着色器所做的是计算像素最后的颜色输出,这里直接简化指定输出白色。gl_FragColor
是内置变量,表示颜色,4 个分量分别对应 R、G、B、A。
/** * 创建片段着色器 * @param {*} gl WebGL 上下文 */ function createFragmentShader(gl) { // 片段着色器 glsl 代码 const source = ` void main(void){ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } `; // 创建着色器 const shader = gl.createShader(gl.FRAGMENT_SHADER); // 设置片段着色器代码 gl.shaderSource(shader, source); // 编译 gl.compileShader(shader); // 判断是否编译成功 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("编译着色器报错: " + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; },
两个着色器都准备好后,需要进行链接合并才能使用。
着色器程序对象是多个着色器合并之后并最终链接完成的版本。当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,会得到一个连接错误。
当需要激活这个着色器的时候,把该对象作为参数调用 useProgram 函数。
/** * 初始化着色器程序 * @param {*} gl WebGL 上下文 * @param {*} vertexShader 顶点着色器 * @param {*} fragmentShader 片段着色器 */ function initShaderProgram(gl, vertexShader, fragmentShader) { // 创建着色器对象 const shaderProgram = gl.createProgram(); // 添加着色器 gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); // 多个着色器合并链接 gl.linkProgram(shaderProgram); // 创建是否成功检查 if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("无法初始化着色器程序: " + gl.getProgramInfoLog(shaderProgram)); return null; } return shaderProgram; }
目前为止,已经把输入顶点数据发送给了 GPU ,并指示了 GPU 如何在顶点和片段着色器中处理它。最后就剩下绘制了。
useProgram
函数激活着色器;gl.LINE_STRIP
。/** * 初始化着色器程序 * @param {*} gl WebGL 上下文 * @param {*} shaderProgram 着色器程序对象 */ function draw(gl, shaderProgram) { // 获取对应数据索引 const vertexPos = gl.getAttribLocation(shaderProgram, "vertexPos"); // 解析顶点数据 gl.vertexAttribPointer(vertexPos, 3, gl.FLOAT, false, 0, 0); // 启用顶点属性,顶点属性默认是禁用的。 gl.enableVertexAttribArray(vertexPos); // 激活着色器 gl.useProgram(shaderProgram); // 绘制 gl.drawArrays(gl.LINE_STRIP, 0, 2); }
这是示例,整体的逻辑大概是这样的:
const canvasObj = document.querySelector("#demo"); const glContext = canvasObj.getContext("webgl"); let vertices = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0]; // 顶点数据 setBuffers(glContext, vertices); // 缓冲数据 const vertexShader = createVertexShader(glContext); // 顶点着色器 const fragmentShader = createFragmentShader(glContext); // 片段着色器 const shaderProgram = initShaderProgram( glContext, vertexShader, fragmentShader ); // 着色器程序对象 draw(glContext, shaderProgram); // 绘制
这里面涉及很多的方法和变量,一开始的时候真的懵,多看几次亲自敲下代码后会慢慢习惯。
接下来会对期间产生的一些疑问进行总结,见 JavaScript WebGL 基础疑惑点。
Back to top