作为gis专业的前端开发人员,工作中少不了与三维场景打交道,像cesium、threejs、mapbox-gl等前端渲染库底层都是封装了WebGL,本文以从一个基础的三角形开始入门WebGL的学习
WebGL 是一组基于 JavaScript 语言的图形规范,浏览器厂商按照这组规范进行实现,为 Web 开发者提供一套3D图形相关的 API。通过JavaScript和GLSL语言对电脑的显卡(GPU)进行操作,从而在屏幕上绘制出各种各样的3D应用。
不同与传统的前端开发,WebGL程序虽然也使用JavaScrip进行数据的传递与开发,但是却需要GLSL语言实现与GPU的通信(GPU是不认JavaScript的),为了将三维数据显示在浏览器的屏幕上,WebGL有一套固定的流程,这个流程称为渲染管线。
渲染管线大致可以分为以下几个步骤:
由JavaScript处理着色器需要的顶点坐标、颜色、纹理等信息,并负责为着色器提供这些数据。
数据的获取由JavaScript代码提供,这些步骤由CPU完成,然后通过webgl的相关方法将这些数据传递给着色器程序,由GPU运行着色器代码
Webgl实际是控制GPU的渲染,GPU上运行的代码是一对着色器:顶点着色器和片元着色器,着色器在运行时会依次调用顶点着色器和片元着色器。
顶点着色器接收 JavaScript 传递过来的与顶点相关的信息,将顶点绘制到对应坐标,顶点着色器的赋值一般是通过以下这种形式,其中xyz是由webgl借助自己的方法从JavaScript取到的:
gl_Position = vec4 (x, y, z, 1);
gl_Position 接收一个 4 维向量表示的坐标,即(X, Y ,Z ,W),W 不等于 0,这个坐标是在裁剪坐标系中,我们称它为裁剪坐标,裁剪坐标W一般情况赋1就可以。他的原点在屏幕中心,X轴正方向向右,Y轴正方向向上,范围都是[-1,1]。
顶点着色器中既可以接受JavaScript提供的信息,又可以将与顶点相关的信息进行一定变换后传递给片元着色器。顶点着色器在数据的传递可以通过以下几个关键字:
关键字 | 描述 |
---|---|
attributes | 获取缓冲区内的数据信息,只能由外部传递给顶点着色器 |
uniform | 一般用来传递全局信息 |
varyings | 将顶点信息传递给片元 |
textures | 获取纹理信息 |
图元装配指的是将设置的点、纹理、颜色组装为可渲染多边形的过程。比如将三个顶点装配成三角形图元,顶点着色器执行了三次,形成了一个三角形图元。
图元有可能是一个点,一个线段,三角形或者其他多边形,,具体图元类型取决于在调用绘制函数时选择的图形类型。
光栅化阶段指将装配好的图元进行像素填充,同时在这个过程中可能还会将一些已经装配好但是不在屏幕显示范围内(遮挡、放大溢出屏幕、背面)的部分裁剪掉以提高WebGL的运行效率。
,光栅化阶段后的每个被填充过的像素都称为片元,在片元着色器阶段,GPU为片元内部的像素z逐一填充颜色信息。其运行的次数由图形的片元数决定 。
步骤3 4对于开发者来说是无法直接控制的,WebGL会自动帮助完成这些工作
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webgltitle>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
padding: 0;
}
canvas {
width: 100%;
height: 100%;
}
style>
head>
<body>
<canvas id="canvas">canvas>
body>
html>
WebGL程序是基于Canvas绘制的,因此需要先添加Canvas对象,使用vsCode编辑器的话,在html中加入/** @type {HTMLCanvasElement} */会出现canvas代码的智能提示。
<script>
/** @type {HTMLCanvasElement} */
//------------------------------------------------------创建画布
// 获取canvas元素对象
let canvas = document.getElementById('canvas');
// 设置canvas宽高
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 获取webgl绘图上下文
const gl = canvas.getContext('webgl');
if (!gl) {
throw new Error('WebGL not supported');
}
// 第一次创建webgl绘图上下文时,需要设置窗口大小
gl.viewport(0, 0, canvas.width, canvas.height)
GLSL与语言与C语言有些类似,必须放在main函数中,attribute在GLSL中是一个关键字,它只能用于顶点着色器中,用来声明在GLSL中的全局变量,用来存储定点信息。
在下列着色器中未经定义直接使用gl_Position、gl_FragColor两个变量,他们均为webgl中的内置变量
- gl_Position:顶点的裁剪坐标,最终经过一系列坐标转换成为屏幕坐标。
- gl_FragColor:片元颜色(r, g, b, a) ,webgl中每一项的取值都是【0.0,1.0】,GPU 会为片元进行上色。
//------------------------------------------------------创建着色器
// 创建顶点着色器
let vertexShader = gl.createShader(gl.VERTEX_SHADER)
// 创建顶点着色器的代码GLSL
gl.shaderSource(vertexShader, `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
`)
// 编译顶点着色器
gl.compileShader(vertexShader)
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(vertexShader));
throw new Error('could not compile vertexShader');
}
// 创建片元着色器
let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
// 创建片元着色器的代码GLSL
gl.shaderSource(fragmentShader, `
void main(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`)
// 编译片元着色器
gl.compileShader(fragmentShader)
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(fragmentShader));
throw new Error('could not compile fragmentShader');
}
通过以上代码,完成了对顶点着色器和片元着色器的创建
创建主程序的代码是很固定的,就是按照这个流程来
//------------------------------------------------------创建主程序
// 创建程序
let program = gl.createProgram();
console.log(program)
// 程序关联顶点着色器和片元着色器
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 链接程序
gl.linkProgram(program);
// 使用程序进行渲染
gl.useProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log(gl.getProgramInfoLog(program));
throw new Error('could not link shaders');
}
在这里通过创建缓冲区的方式绘制三角形,流程如下
//------------------------------------------------------传递数据并绘制
// 创建顶点缓冲区对象
let vertexBuffer = gl.createBuffer()
// 绑定顶点缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向顶点缓冲区对象写入数据
let vertices = new Float32Array([
0.0, 0.5, // 顶点着色器补全为(0.0,0.5,0.0,1.0)
-0.5, -0.5,
0.5, -0.5
])
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 获取顶点着色器中a_Position变量的位置
let a_Position = gl.getAttribLocation(program, 'a_Position');
// 顶点缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 启用顶点着色器的a_Position变量
gl.enableVertexAttribArray(a_Position);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
<script>
这里第一次写的话的确会觉得很绕,主要有两点:首先,GLSL不能直接把数据写到缓冲区里去,所以,我们必须以gl.ARRAY_BUFFER作为中转,把它绑定至缓冲区,然后对这个中转传递数据;第二点:gl.vertexAttribPointer()方法的第一个参数同样不能直接指给attribute变量,需要指给a_Position对应的attribute,然后将缓冲区分配给a_Position,以此传给attribute变量。
可以看到,如果完全按照WebGL的流程来,即使是绘制一个最简单的元素也需要写很多代码,因此,很多方法完全可以封装起来,在《WebGL编程指南》中就直接提供了工具函数,学习WebGL的话这本书是一定要买的(非广告哈~)