Skip to main content

webgl

Web Graphic Library,页面图形库;

借助canvas元素,使用JavaScript和OpenGL ES 2.0 的 API,进行2D或3D图形渲染。

WebGL 程序构成:

  • 控制代码(JavaScript)
  • 特效代码(在GPU中执行,shader code,着色代码)

GPU: Graphic Processing Unit,图形处理单元

基础概念

webgl在电脑的GPU中运行,因此需要能够在GPU上运行的代码;

这样的代码需要提供成对的方法;

每对方法中的一个叫顶点着色器,另一个叫片段着色器

并且使用一种c或c++类似的强类型语言GLSL(GL着色语言);

每一对组合起来称作一个Program(着色程序);

几乎整个webgl API都是关于如何设置这些成对方法的状态值以及运行它们;

顶点着色器

VertexShader,作用是,计算顶点的位置

根据计算出的一系列顶点位置,webgl可以对点,线和三角形在内的一些图元进行光栅化处理;

片段着色器

fragmentShader,对图元进行光栅化处理,需要使用片段着色器方法;

片段着色器的作用,计算出当前绘制图元中每个像素的颜色值

案例

  • 绘制三角形
  • 绘制彩色矩阵,并理解缓冲区(Buffer)的使用

三角形

理解顶点着色器和片段着色器的作用

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGL 三角形</title>
<style>
canvas {
display: block;
margin: 20px auto;
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script>
// 获取WebGL上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

// 顶点着色器源码
const vertexShaderSource = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition; // 设置顶点位置
}
`;

// 片元着色器源码
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置颜色为红色
}
`;

// 编译着色器
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
);

// 创建并链接程序
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// 定义三角形顶点数据
const vertices = new Float32Array([
0.0,
0.5, // 顶点1
-0.5,
-0.5, // 顶点2
0.5,
-0.5, // 顶点3
]);

// 创建缓冲区并绑定数据
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取顶点属性位置并启用
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);

// 清空画布并绘制
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置背景色为黑色
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制三角形

// 编译着色器的辅助函数
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("着色器编译失败:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
</script>
</body>
</html>

webgl上下文

  • canvas.getContext(webgl) 获取webgl上下文
  • webgl是基于状态机的API,所有操作都通过 gl对象完成

着色器shader

  • 顶点着色器:处理顶点数据,计算顶点在裁剪空间中的位置

    attribute vec4 aPosition; // 顶点属性
    void main() {
    gl_Position = aPosition; // 设置顶点位置
    }
  • 片段着色器:处理像素颜色

    void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置颜色为红色
    }

缓冲区Buffer

// 创建缓冲区并绑定数据
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  • gl.createBuffer创建
  • gl.bindBuffer绑定
  • gl.bufferData将顶点数据上传到GPU

顶点属性Attribute

// 获取顶点属性位置并启用
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
  • gl.getAttribLocation获取顶点属性的位置
  • gl.enableVertexAttribArray启用顶点属性
  • gl.vertexAttribPointer指定如何从缓冲区读取数据

绘制

// 清空画布并绘制
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置背景色为黑色
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制三角形
  • gl.clearColor设置背景色
  • gl.clear清空画布
  • gl.drawArrays绘制三角形

彩色矩阵

理解如何传递颜色数据

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL 彩色矩形</title>
<style>
canvas { display: block; margin: 20px auto; border: 1px solid #000; }
</style>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script>
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

// 顶点着色器源码
const vertexShaderSource = `
attribute vec4 aPosition;
attribute vec4 aColor;
varying vec4 vColor;
void main() {
gl_Position = aPosition;
vColor = aColor; // 传递颜色到片元着色器
}
`;

// 片元着色器源码
const fragmentShaderSource = `
precision mediump float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor; // 使用传递的颜色
}
`;

// 编译着色器
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

// 创建并链接程序
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// 定义矩形顶点数据(两个三角形)
const vertices = new Float32Array([
-0.5, 0.5, // 顶点1
-0.5, -0.5, // 顶点2
0.5, -0.5, // 顶点3
0.5, 0.5 // 顶点4
]);

// 定义颜色数据(RGBA)
const colors = new Float32Array([
1.0, 0.0, 0.0, 1.0, // 红色
0.0, 1.0, 0.0, 1.0, // 绿色
0.0, 0.0, 1.0, 1.0, // 蓝色
1.0, 1.0, 0.0, 1.0 // 黄色
]);

// 创建顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 创建颜色缓冲区
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);

// 获取顶点属性位置并启用
const aPosition = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(aPosition);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);

// 获取颜色属性位置并启用
const aColor = gl.getAttribLocation(program, "aColor");
gl.enableVertexAttribArray(aColor);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, 0, 0);

// 清空画布并绘制
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); // 绘制矩形

// 编译着色器的辅助函数
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("着色器编译失败:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
</script>
</body>
</html>

顶点数据与颜色数据分离

  • 使用两个缓冲区分别存储顶点数据和颜色数据
  • 通过gl.vertexAttribPointer指定如何读取数据

varying变量

  • 顶点着色器中定义varying变量,用于将数据传递到片段着色器
  • 片段着色器使用varying变量设置颜色

绘制模式

  • 使用gl.TRIANGLE_FAN绘制矩阵,减少顶点数据重复

着色器获取数据

着色器需要的任何数据,都需要发送到GPU;

有4种方式:

  • 属性(Attributes)和缓冲
  • 全局变量(Uniforms)
  • 纹理(Textures)
  • 可变量(Varyings)

属性和缓冲

缓冲

是发送到GPU的一些二进制数据序列,通常情况下缓冲数据包括位置,法向量,纹理坐标,顶点颜色值等,可以存储任何数据;

缓冲不是随意读取的,事实上顶点着色器运行的次数,是一个指定的确切数字

每一次运行属性,会从指定缓冲中,按照指定规则依次获取下一个值;

属性

用来指明怎么从缓冲中获取所需数据,并将它提供给顶点着色器;

例如你可能在缓冲中用3个32位浮点型数据存储一个位置

对于一个确切的属性,需要告诉它:从哪个缓冲中获取数据,什么类型的,起始偏移值,到下一个位置的字节数是多少;

全局变量

在着色程序运行前赋值,在运行过程中全局有效;

纹理

是一个数据序列,可以在着色程序运行中随意读取其中的数据;

大多数情况存放的是图像数据,但纹理仅仅是数据序列,也可以随意存放除了颜色数据以外的其他数据;

可变量

是一种顶点着色器给片段着色器传值的方式;

依照渲染的图元是点,线还是三角形,顶点着色器中设置的可变量,会在片段着色器运行中获取不同的插值

裁剪空间

webgl只关心两件事:裁剪空间中的坐标值和颜色值;

webgl只关心两件事:裁剪空间的坐标值颜色值

顶点着色器提供坐标值,片段着色器提供颜色值;

无论画布多大,,裁剪空间的坐标范围永远是-1到1; 例子:

从顶点着色器开始

// 一个属性值,将会从缓冲中获取数据
attribute vec4 a_position;

// 所有着色器都有一个main方法
void main() {

// gl_Position 是一个顶点着色器主要设置的变量
gl_Position = a_position;
}

如果用js代替glsl,当它运行的时候,做了类似以下的事情:

var positionBuffer = [
0, 0, 0, 0,
0, 0.5, 0, 0,
0.7, 0, 0, 0,
];
var attributes = {};
var gl_Position;

drawArrays(..., offset, count) {
var stride = 4;
var size = 4;
for (var i = 0; i < count; ++i) {
// 从positionBuffer复制接下来4个值给a_position属性
const start = offset + i * stride;
attributes.a_position = positionBuffer.slice(start, start + size);
runVertexShader();// 运行顶点着色器
...
doSomethingWith_gl_Position();
}

但实际情况没那么简单,因为positionBuffer将会被转换成二进制数据;

接着看片段着色器:

// 片段着色器没有默认精度,所以我们需要设置一个精度
// mediump是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;

void main() {
// gl_FragColor是一个片段着色器主要设置的变量
gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“红紫色”
}

案例

  1. 画布
  2. webgl渲染上下文
  3. 创建GLSL
  4. 创建着色器
  5. 着色程序

画布

<body onload="main()">
<canvas id="glcanvas" width="640" height="480">
你的浏览器似乎不支持或者禁用了 HTML5 <code>&lt;canvas&gt;</code> 元素。
</canvas>
</body>

webgl渲染上下文

<script>
// 从这里开始
function main() {
const canvas = document.querySelector("#glcanvas");
// 初始化 WebGL 上下文
const gl = canvas.getContext("webgl");

// 确认 WebGL 支持性
if (!gl) {
alert("无法初始化 WebGL,你的浏览器、操作系统或硬件等可能不支持 WebGL。");
return;
}
...
}

</script>

创建GLSL

  • 串联方式,ajax下载,用多行模板数据;
  • 放在非JavaScript类型的标签中;

事实上,大多数三维引擎在运行时利用模版,串联等方式创建GLSL;

<script id="vertex-shader-2d" type="notjs">

// 一个属性变量,将会从缓冲中获取数据
attribute vec4 a_position;

// 所有着色器都有一个main方法
void main() {

// gl_Position 是一个顶点着色器主要设置的变量
gl_Position = a_position;
}

</script>

<script id="fragment-shader-2d" type="notjs">

// 片段着色器没有默认精度,所以我们需要设置一个精度
// mediump是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;

void main() {
// gl_FragColor是一个片段着色器主要设置的变量
gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“瑞迪施紫色”
}

</script>

创建着色器

// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
function createShader(gl, type, source) {
var shader = gl.createShader(type); // 创建着色器对象
gl.shaderSource(shader, source); // 提供数据源
gl.compileShader(shader); // 编译 -> 生成着色器
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}

console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}

这个方法,只需要上传GLSL数据,然后编译成着色器;

用该方法创建两个着色器:

var vertexShaderSource = document.querySelector("#vertex-shader-2d").text;
var fragmentShaderSource = document.querySelector("#fragment-shader-2d").text;

var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

着色程序

需要将两个着色器link到一个program

function createProgram(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}

console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}

已经在cpu上创建了一个glsl着色程序,还需要给它提供数据;

webgl的主要任务是:设置好状态并为glsl着色程序提供数据

这个例子中glsl着色程序的唯一输入是一个属性值a_position

要做的第一件事是从刚刚创建的glsl着色程序中找到这个属性值所在的位置;

var positionAttributeLocation = gl.getAttribLocation(program, "a_position");

寻找属性值位置(和全局属性位置)应该在初始化时完成,而不是在渲染循环中;

属性值从缓冲中获取数据,所以创建一个缓冲:

var positionBuffer = gl.createBuffer();

webgl可以通过绑定点操控全局范围内的许多数据,可以把绑定点想象成一个webgl内部的全局变量;

首先绑定一个数据源到绑定点,然后可以引用绑定点指向该数据源;

比如下面的绑定点就是ARRAY_BUFFER;

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

通过绑定点向缓存中存放数据

// 三个二维点坐标
var positions = [
0, 0,
0, 0.5,
0.7, 0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

这里做了哪些操作:

  • js序列positions
  • 创建了32位浮点型数据序列,从positions中复制数据到序列中
  • gl.bufferData复制这些数据到GPU的positionBuffer
  • gl.STATIC_DRAW提示webgl我们不会经常改变这些数据,属于优化

最终传递到positionBuffer上是因为在前一步中将它绑定到了ARRAY_BUFFER(也就是绑定点)

在此之上的代码是初始化代码,在页面加载时只会运行一次;

接下来是渲染代码,而这些代码将在每次渲染或者绘制时执行;

渲染

绘制之前,应该调整画布尺寸匹配它的显示尺寸;

画布和图片类似,有两个尺寸:

  • 实际像素个数;
  • 显示的大小;

css决定画布显示的大小,应该尽可能用css设置所需画布大小,它比其他方式灵活的多