webgl详解part4:实战!手把手教你在Canvas里画出第一个三角形
骚话鬼才又来分享WebGL干货啦!前面咱们把WebGL的API、底层逻辑都唠了个遍,今天直接上手实操,带你用WebGL在HTML的canvas里画出人生第一个三角形。别眨眼,跟着骚话王一步一步来,保证你也能在前端3D江湖里"亮剑出鞘"!
如果觉得有用,记得点赞收藏,骚话鬼才后续还有更多实战技巧等你来拿!
目标:用WebGL在Canvas中渲染一个三角形
咱们的终极目标很简单:
- 用HTML写一个canvas
- 用JavaScript调用WebGL API
- 创建着色器、程序对象、缓冲区
- 把三角形画到屏幕上
别怕,骚话王会把每一步都拆开讲,代码和原理全都安排得明明白白。
第一步:准备HTML和Canvas
先来一段最基础的HTML,画布就是咱们的"江湖擂台":
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGL三角形实战</title>
<style>
body { background: #222; color: #fff; }
canvas { display: block; margin: 40px auto; background: #333; }
</style>
</head>
<body>
<canvas id="webgl-canvas" width="400" height="400"></canvas>
<script src="triangle.js"></script>
</body>
</html>
你可以直接把这段HTML保存为
index.html,canvas的id和尺寸可以随意调整。
第二步:编写JavaScript,召唤WebGL上下文
新建一个triangle.js,先获取WebGL上下文:
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('你的浏览器不支持WebGL,换个浏览器试试吧!');
}
骚话王小贴士:
getContext('webgl')是WebGL的"入场券",拿不到就啥都别谈。- 有些老浏览器可能只支持
experimental-webgl,可以兼容性处理下。
第三步:写好着色器源码
WebGL的"魔法阵"——着色器,分为顶点着色器和片元着色器。直接用字符串写在JS里:
// 顶点着色器源码
const vertexShaderSource = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
gl_PointSize = 10.0; // 虽然这里没用到点,但演示下
}
`;
// 片元着色器源码
const fragmentShaderSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); // 橙色
}
`;
着色器源码用反引号包裹,多行写更清晰。
第四步:创建、编译着色器对象
封装一个创建着色器的函数,省得每次都写一大堆:
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error('着色器编译失败:' + info);
}
return shader;
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
骚话王小贴士:
- 编译失败一定要看
infoLog,GLSL语法错一个字母都不行。
第五步:创建程序对象,链接着色器
把顶点和片元着色器"组队"成一个完整的程序:
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const info = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error('程序对象链接失败:' + info);
}
return program;
}
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
第六步:准备三角形顶点数据,上传到GPU
三角形有3个顶点,每个顶点2个坐标(x, y),范围-1到1:
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);
你可以把缓冲区想象成"快递仓库",顶点数据都得先存进去。
第七步:配置attribute,把数据送进着色器
先查到attribute的位置,再告诉WebGL怎么读数据:
const positionLoc = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
2:每个顶点2个分量(x, y)gl.FLOAT:数据类型false:不归一化0:步长为0,紧密排列0:偏移为0,从头开始
骚话王小贴士:
- attribute名要和着色器里一模一样,大小写敏感。
- 步长和偏移单位是字节,不是元素个数。
第八步:清屏,绘制三角形!
一切准备就绪,点火开工:
gl.clearColor(0.1, 0.1, 0.1, 1.0); // 背景色
// 清除颜色缓冲
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
你可以把drawArrays想象成"发号施令",让GPU正式开工。
完整代码一览
把上面所有代码合起来,就是一个完整的三角形渲染Demo:
// 获取canvas元素,相当于找到"画布"
const canvas = document.getElementById('webgl-canvas');
// 获取WebGL上下文,后续所有WebGL操作都靠它
const gl = canvas.getContext('webgl');
if (!gl) {
alert('你的浏览器不支持WebGL,换个浏览器试试吧!');
}
// 顶点着色器源码,负责处理每个顶点的位置
const vertexShaderSource = `
attribute vec2 position; // 顶点输入变量,二维坐标
void main() {
gl_Position = vec4(position, 0.0, 1.0); // 组装成四维向量,送进裁剪空间
}
`;
// 片元着色器源码,负责决定每个像素的颜色
const fragmentShaderSource = `
precision mediump float; // 指定浮点精度
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); // 输出橙色
}
`;
// 封装创建着色器的函数,type可以是顶点或片元
function createShader(gl, type, source) {
const shader = gl.createShader(type); // 创建着色器对象
gl.shaderSource(shader, source); // 注入GLSL源码
gl.compileShader(shader); // 编译着色器
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { // 检查编译结果
const info = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error('着色器编译失败:' + info);
}
return shader; // 返回编译好的着色器对象
}
// 封装创建程序对象的函数,把顶点和片元着色器组队
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram(); // 创建程序对象
gl.attachShader(program, vertexShader); // 挂载顶点着色器
gl.attachShader(program, fragmentShader); // 挂载片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { // 检查链接结果
const info = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error('程序对象链接失败:' + info);
}
return program; // 返回可用的程序对象
}
// 创建并编译着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 顶点着色器
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 片元着色器
// 创建并链接程序对象
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program); // 激活程序对象,后续绘制都靠它
// 定义三角形的三个顶点,每个顶点2个分量(x, y),范围-1到1
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); // 绑定为当前操作的缓冲区
// 把顶点数据搬进GPU仓库
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 获取attribute变量在程序中的位置(编号)
const positionLoc = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionLoc); // 启用attribute变量
// 告诉WebGL如何从缓冲区读取数据送进position
// 参数依次为:位置、每组数据个数、类型、是否归一化、步长、偏移
// 这里每个顶点2个float,紧密排列,无偏移
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
// 设置清屏颜色(背景色)为深灰色
gl.clearColor(0.1, 0.1, 0.1, 1.0);
// 清除颜色缓冲区,准备开画
gl.clear(gl.COLOR_BUFFER_BIT);
// 发号施令,绘制三角形!参数:图元类型、起始索引、顶点数量
gl.drawArrays(gl.TRIANGLES, 0, 3);
- 着色器编译失败? 99%是GLSL语法写错,大小写、分号、变量名都要对。
- 画布没显示三角形? 检查canvas尺寸、attribute配置、drawArrays参数。
- 颜色不对? 检查
gl_FragColor赋值。 - 浏览器不支持? 换新版Chrome/Edge/Firefox,WebGL基本都支持。
- 调试建议:多用
console.log和gl.getError()排查问题。
补充:WebGL 1.0 和 WebGL 2.0 有啥区别?API升级全解析
骚话鬼才又来科普啦!很多小伙伴刚学会WebGL 1.0,结果一看网上还有WebGL 2.0,顿时一脸懵圈:这俩到底啥关系?API有啥不一样?是不是2.0更牛逼?今天就来给你掰扯掰扯,WebGL 1.0和2.0的区别,尤其是API上的升级!
1. WebGL 1.0:前端3D的"启蒙老师"
WebGL 1.0 基于 OpenGL ES 2.0,算是前端3D的"开山鼻祖"。它让你用JavaScript就能调动GPU,玩转3D世界。
- 优点:兼容性好,几乎所有主流浏览器都支持。
- 缺点:有些高级特性用不了,比如多重采样、3D纹理、实例化绘制等。
2. WebGL 2.0:前端3D的"进阶武器"
WebGL 2.0 则是基于 OpenGL ES 3.0,功能更强大,API更丰富,性能更高。
- 优点:支持更多现代图形特性,效率更高,画面更炫酷。
- 缺点:部分老设备/浏览器不支持(但新设备基本都OK)。
3. API上的主要区别
(1)着色器语言升级
- WebGL 1.0 只支持 GLSL ES 1.0
- WebGL 2.0 支持 GLSL ES 3.0,语法更强大,支持更多内置函数和数据类型
(2)顶点属性和缓冲区
- WebGL 2.0 支持 顶点数组对象(VAO),用
gl.createVertexArray()管理attribute配置,切换更高效 - 支持 实例化绘制,如
gl.drawArraysInstanced(),一行代码画一堆对象,粒子系统、草地森林so easy
(3)纹理和采样
- WebGL 2.0 支持 3D纹理、多重采样纹理、浮点纹理,画面更细腻
- 支持
gl.texImage3D()、gl.sampler等新API
(4)渲染管线增强
- 支持 多重渲染目标(MRT),一次渲染输出多个颜色缓冲,后处理、延迟渲染必备
- 支持
gl.drawBuffers()
(5)Uniform和UBO
- WebGL 2.0 支持 Uniform Buffer Object(UBO),批量传递uniform,效率飞起
- 支持
gl.getUniformBlockIndex()、gl.uniformBlockBinding()等
(6)更多内置常量和API
- WebGL 2.0 新增了很多常量,比如
gl.RGBA32F、gl.SAMPLER_3D等 - 新增API如
gl.fenceSync()、gl.invalidateFramebuffer()等
4. 代码举例:VAO的用法
WebGL 1.0 没有VAO,每次切换attribute都要重新配置:
// WebGL 1.0
// 每次都要enable/disable/vertexAttribPointer
WebGL 2.0 可以这样:
// WebGL 2.0
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// 配置attribute...
gl.bindVertexArray(null); // 切换只需bind
5. 如何判断当前环境支持WebGL 2.0?
const gl2 = canvas.getContext('webgl2');
if (gl2) {
console.log('支持WebGL 2.0,骚操作可以安排上!');
} else {
console.log('只支持WebGL 1.0,基础功能也够用!');
}
示例封装的简易shader类(可按照需求自行更改)
export interface WebGLShaderProgramOptions {
autoDeleteShader?: boolean // link成功后自动删除shader,默认true
}
export class WebGLShaderProgram {
private gl: WebGLRenderingContext | WebGL2RenderingContext
private vertexSource: string
private fragmentSource: string
private vertexShader: WebGLShader | null = null
private fragmentShader: WebGLShader | null = null
private program: WebGLProgram | null = null
private options: WebGLShaderProgramOptions
constructor(
gl: WebGLRenderingContext | WebGL2RenderingContext,
vertexSource: string,
fragmentSource: string,
options: WebGLShaderProgramOptions = {}
) {
this.gl = gl
this.vertexSource = vertexSource
this.fragmentSource = fragmentSource
this.options = { autoDeleteShader: true, ...options }
try {
this.init()
} catch (e) {
this.destroy()
throw e
}
}
/**
* 判断当前上下文是否为WebGL2
*/
isWebGL2(): boolean {
return typeof WebGL2RenderingContext !== 'undefined' && this.gl instanceof WebGL2RenderingContext
}
/**
* 编译shader源码,类型区分顶点/片元,失败时抛出详细错误并清理资源
*/
private createShader(type: number, source: string): WebGLShader {
const shader = this.gl.createShader(type)
if (!shader) throw new Error((type === this.gl.VERTEX_SHADER ? '顶点' : '片元') + 'Shader无法创建')
this.gl.shaderSource(shader, source)
this.gl.compileShader(shader)
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
const info = this.gl.getShaderInfoLog(shader)
this.gl.deleteShader(shader)
throw new Error((type === this.gl.VERTEX_SHADER ? '顶点' : '片元') + 'Shader编译失败: ' + info)
}
return shader
}
/**
* 初始化并链接program,任一步骤失败都自动清理资源
*/
private init() {
try {
this.vertexShader = this.createShader(this.gl.VERTEX_SHADER, this.vertexSource)
this.fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, this.fragmentSource)
this.program = this.gl.createProgram()
if (!this.program) throw new Error('Program无法创建')
this.gl.attachShader(this.program, this.vertexShader)
this.gl.attachShader(this.program, this.fragmentShader)
this.gl.linkProgram(this.program)
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
const info = this.gl.getProgramInfoLog(this.program)
throw new Error('Program链接失败: ' + info)
}
// link成功后自动删除shader
if (this.options.autoDeleteShader) {
if (this.vertexShader) {
this.gl.deleteShader(this.vertexShader)
this.vertexShader = null
}
if (this.fragmentShader) {
this.gl.deleteShader(this.fragmentShader)
this.fragmentShader = null
}
}
} catch (e) {
this.destroy()
throw e
}
}
/**
* 激活当前program
*/
use() {
if (this.program) {
this.gl.useProgram(this.program)
}
}
/**
* 获取底层WebGLProgram对象
*/
getProgram() {
return this.program
}
/**
* 释放所有WebGL资源
*/
destroy() {
if (this.vertexShader) {
this.gl.deleteShader(this.vertexShader)
this.vertexShader = null
}
if (this.fragmentShader) {
this.gl.deleteShader(this.fragmentShader)
this.fragmentShader = null
}
if (this.program) {
this.gl.deleteProgram(this.program)
this.program = null
}
}
}
如果觉得有用,记得点赞收藏,骚话王后续还会带来更多WebGL骚操作,咱们下篇再见!