面向web前端的WebGL教程,网络上的教程均是假设有计算机图形学基础,对web开发者来说不是很友好, 故开辟此坑
最终效果
https://codepen.io/chendonmin...
鼠标点击 画一个点。
webGL如何展示一个点
首先得知道webGL如何展示出一个点?
webGL画任意物体 都需要一个顶点着色器
和片元着色器
,
顶点着色器:描述顶点的特性(位置、颜色等)的程序.
片元着色器: 进行着片元处理过程的程序。
也许你会很懵,一大堆官方理论又要望而却步了,所以我直接展示下最简单的展示一个点的代码,相信你会马上明白。
<canvas id="glcanvas" width="640" height="480">
你的浏览器似乎不支持或者禁用了HTML5 <code><canvas></code> 元素.
</canvas>
首先,我需要一些简单的封装函数:
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
/**
* Create the linked program object
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return created program object, or null if the creation has failed
*/
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}
// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// Link the program object
gl.linkProgram(program);
// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
/**
* Create a shader object
* @param gl GL context
* @param type the type of the shader object to be created
* @param source shader program (string)
* @return created shader object, or null if the creation has failed.
*/
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
// Set the shader program
gl.shaderSource(shader, source);
// Compile the shader
gl.compileShader(shader);
// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}
return shader;
}
这是一段初始化着色器的函数
初始化webgl:
const canvas = document.querySelector("#glcanvas");
// 初始化WebGL上下文
const gl = canvas.getContext("webgl");
// 确认WebGL支持性
if (!gl) {
alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
return;
}
// 使用完全不透明的黑色清除所有图像
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用上面指定的颜色清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
调用初始化着色器函数。
const VSHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0 ,0.0 ,0.0 , 1.0);
gl_PointSize = 10.0;
}
`;
const FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
}
`;
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
gl.drawArrays(gl.POINTS, 0, 1);
OK,目前为止,你应该能看到黑色canvas中间有个红色的点了。
解析
最关键的部分,其实就是VSHADER_SOURCE
和FSHADER_SOURCE
两个字符串,分别表示了点的坐标和点的颜色。
VSHADER_SOURCE
和FSHADER_SOURCE
是属于glsl
代码,
VSHADER_SOURCE
中的gl_Position
代表的就是点的位置,gl_Position
是glsl
的内置变量。
你会发现gl_Position
的值是个vec4类型,坐标居然有4个值?其实这个是齐次坐标.
对于vec4(x, y, z, w), 真实的世界坐标是 (x/w, y/w, z/w), 所以一般vec4第四个参数我们设置为1。
为什么需要齐次坐标呢,因为三维世界中,向量也是三个坐标表示的, 所以为了区分向量和真实位置引入了第四个参数,向量的第四个参数是0.
js和GLSL通信
上面代码确实画出一个点, 但是是写在一个字符串中的,这肯定不方便我们进行操作啊,所以操作glsl
中的变量就很有必要了。
const VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
如上图,我们对顶点着色器代码 加入了一个attribute, 然后attribute a_Position赋值给glsl内置变量gl_Position,这是否意味着我改动a_Position的值,gl_Position也会改变呢?
js获取并修改attribute
需要的API:
gl.getAttribLocation(gl.program, attribute);
gl.vertexAttrib3f(index, x, y, z);
getAttribLocation方法返回了给定WebGLProgram对象中某属性的下标指向位置
vertexAttrib3f可以为顶点attibute变量赋值
现在只需要在gl.drawArrays(gl.POINTS, 0, 1);
之前修改attribute即可
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
目前为止,完整代码如下:
const canvas = document.querySelector("#glcanvas");
// 初始化WebGL上下文
const gl = canvas.getContext("webgl");
// 确认WebGL支持性
if (!gl) {
alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
return;
}
// 使用完全不透明的黑色清除所有图像
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用上面指定的颜色清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
const VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
const FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
}
`;
//初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
//画点
gl.drawArrays(gl.POINTS, 0, 1);
画多个点
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
你会发现屏幕上存在了两个红点。
drawArrays
方法用于从向量数组中绘制图元,每执行一次就要通知GPU渲染图元。
现在两个点还好,如果是成千上万个点呢?我们需要一次性画多个点,这样才能保持性能。
类型化数组TypedArray
对于多个点,我们需要把点的位置存在变量中,我们选择了TypedArray,
它相比普通Array有几个好处:性能 性能 还tm是性能。
对于typedArray
介绍看如下代码:
// 下面代码是语法格式,不能直接运行,
// TypedArray 关键字需要替换为底部列出的构造函数。
new TypedArray(); // ES2017中新增
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);
// TypedArray 指的是以下的其中之一:
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
那怎么选择呢, 看如下列表:
类型 | 单个元素值的范围 | 大小(bytes) | 描述 | Web IDL 类型 | C 语言中的等价类型 |
---|---|---|---|---|---|
Int8Array | -128 to 127 | 1 | 8 位二进制有符号整数 | byte | int8_t |
Uint8Array | 0 to 255 | 1 | 8 位无符号整数(超出范围后从另一边界循环) | octet | uint8_t |
Uint8ClampedArray | 0 to 255 | 1 | 8 位无符号整数(超出范围后为边界值) | octet | uint8_t |
Int16Array | -32768 to 32767 | 2 | 16 位二进制有符号整数 | short | int16_t |
Uint16Array | 0 to 65535 | 2 | 16 位无符号整数 | unsigned short | uint16_t |
Int32Array | -2147483648 to 2147483647 | 4 | 32 位二进制有符号整数 | long | int32_t |
Uint32Array | 0 to 4294967295 | 4 | 32 位无符号整数 | unsigned long | uint32_t |
Float32Array | 1.2 ×10^-38 to 3.4 ×10^38 | 4 | 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567 ) | unrestricted float | float |
Float64Array | 5.0 ×10^-324 to 1.8 ×10^308 | 8 | 64 位 IEEE 浮点数(16 有效数字,如 1.123...15 ) | unrestricted double | double |
BigInt64Array | -2^63 to 2^63-1 | 8 | 64 位二进制有符号整数 | bigint | int64_t (signed long long) |
BigUint64Array | 0 to 2^64 - 1 | 8 | 64 位无符号整数 | bigint | uint64_t (unsigned long long) |
对于这个教程,因为等会我们会使用浮点数,又因为数据不大,所以选择Float32Array
.
尝试绘制两个点
将两个点的坐标储存到变量中
const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);
把数据挂到缓冲区某个内存位置,写入数据
const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // verties就是我们自己创建的Float32Array数据 gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
读取数据并修改attribute
const a_Position = gl.getAttribLocation(gl.program, "a_Position"); gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(a_Position);
理解vertexAttribPointer函数可以看我的这篇笔记
https://note.youdao.com/s/c5E...修改了attribute,下一步调用绘制命令
// 因为是绘制两个点,第三个参数输入2 gl.drawArrays(gl.POINTS, 0, 2);
完整代码
除去初始化webGL和工具函数initShaders
, 因为每次都写 没有变化...
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
const verties = new Float32Array([0.0, 0.5, -0.5, -0.5]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
gl.drawArrays(gl.POINTS, 0, 2);
鼠标监听坐标并写入
如果上面的都理解了话,第三步反而是最简单的了(对于web开发人员来说).
具体功能上面代码都是实现了,只需要:
- 点击的时候把屏幕坐标转成webGL坐标
- 把坐标存入Float32Array数据
- 修改attribute,渲染。
转成webGl坐标
const x = (e.offsetX - 320) / 320;
const y = -(e.offsetY - 240) / 240;
其中 320 = 640/2
240 = 480/2
320代表canvas元素的宽, 240代表canvas元素高
存入Float32Array数据
首先Float32Array是固定长度的,无法动态修改,所以需要新建一个Float32Array
const newArr = new Float32Array(length + 2)
for (let i = 0; i < arrayBuffer.length; i++) {
newArr[i] = arrayBuffer[i]
}
newArr[arrayBuffer.length] = x;
newArr[arrayBuffer.length + 1] = y;
最终代码
代码可以在codePen里查看, 如果无法打开的话, 我将代码展示出来:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
</head>
<body onload="main()">
<canvas id="glcanvas" width="640" height="480">
你的浏览器似乎不支持或者禁用了HTML5 <code><canvas></code> 元素.
</canvas>
</body>
<script src="utils/cuon-utils.js"></script>
<script>
let arrayBuffer = new Float32Array()
function main() {
const canvas = document.querySelector("#glcanvas");
// 初始化WebGL上下文
const gl = canvas.getContext("webgl");
// 确认WebGL支持性
if (!gl) {
alert("无法初始化WebGL,你的浏览器、操作系统或硬件等可能不支持WebGL。");
return;
}
// 使用完全不透明的黑色清除所有图像
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用上面指定的颜色清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
const VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
const FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
}
`;
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 监听点击事件
document.getElementById('glcanvas').addEventListener('mousedown', e => {
clear(gl);
// 左上角原点坐标
const x = (e.offsetX - 320) / 320;
const y = -(e.offsetY - 240) / 240;
let length = arrayBuffer.length;
const newArr = new Float32Array(length + 2)
for (let i = 0; i < arrayBuffer.length; i++) {
newArr[i] = arrayBuffer[i]
}
newArr[arrayBuffer.length] = x;
newArr[arrayBuffer.length + 1] = y;
const len = initVertexBuffer(gl, newArr);
gl.drawArrays(gl.POINTS, 0, len);
arrayBuffer = newArr;
})
}
function initVertexBuffer(gl, verties) {
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verties, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
const FSIZE = verties.BYTES_PER_ELEMENT
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0);
gl.enableVertexAttribArray(a_Position);
return verties.length / 2;
}
function clear(gl) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
</script>
</html>
其中cuon-utils.js
是封装的一个小工具函数
// cuon-utils.js (c) 2012 kanda and matsuda
/**
* Create a program object and make current
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return true, if the program object was created and successfully made current
*/
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
/**
* Create the linked program object
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return created program object, or null if the creation has failed
*/
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}
// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// Link the program object
gl.linkProgram(program);
// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
/**
* Create a shader object
* @param gl GL context
* @param type the type of the shader object to be created
* @param source shader program (string)
* @return created shader object, or null if the creation has failed.
*/
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
// Set the shader program
gl.shaderSource(shader, source);
// Compile the shader
gl.compileShader(shader);
// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Initialize and get the rendering for WebGL
* @param canvas <cavnas> element
* @param opt_debug flag to initialize the context for debugging
* @return the rendering context for WebGL
*/
function getWebGLContext(canvas, opt_debug) {
// Get the rendering context for WebGL
var gl = WebGLUtils.setupWebGL(canvas);
if (!gl) return null;
// if opt_debug is explicitly false, create the context for debugging
if (arguments.length < 2 || opt_debug) {
gl = WebGLDebugUtils.makeDebugContext(gl);
}
return gl;
}
happy
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。