作为前端的你需要学习WebGL啦 (保姆级教程二)
OpenGL ES
基本特点
-
大小写敏感
-
强制分号
-
着色器语言通过main函数作为入口,没有任何返回值。
-
注释同js注释相同。
-
强制类型语言
- float 单精度浮点类型
- bool
- int
-
变量命名不能以
gl_
、webgl_
、_webgl_
作为开头。 -
数据类型转换
float()
bool()
int()
// 可以将对应的变量转换为其他的类型。
-
可以使用分支和循环语句。
- break, continue可以跳出循环。
- discard 只能在片元着色器中使用,表示放弃当前片元直接处理下一个片元。
-
可以定义函数
// 返回值类型 函数名 (参数) { return 返回值}
void main() {
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
vTex = vec2(aTex.x, aTex.y);
}
矢量类型
参考我们前面定义的顶点坐标类型。
- vec2、vec3、vec4 具有 2,3,4 个浮点数元素的矢量。
- ivec2、ivec3、ivec4 具有 2,3,4 个整型元素的矢量。
- bvec2、bvec3、bvec4 具有 2,3,4 个布尔值元素的矢量。
我们可以通过vec4
构造函数为矢量类型赋值。
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`; // 片元着色器
访问矢量里的分量。
- x, y, z, w 访问顶点坐标的分量
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aTranslate;
void main() {
gl_Position = vec4(aPosition.x + aTranslate, aPosition.y, aPosition.z, 1.0);
gl_PointSize = 10.0;
}
`; // 顶点着色器
并且可以通过混合的方式获取多个值,获取到的是⼀个新的矢量内容。
- s, t, p, q 访问纹理坐标分量。
矩阵类型
mat2、mat3、mat4 分别为2 * 2, 3 * 3, 4 * 4 的浮点数元素矩阵。矩阵参数是列主序的。
// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
uniform mat4 mat; // 平移时需要平移三角形的所有顶点,所以使用uniform属性
void main() {
gl_Position = mat * aPosition;
gl_PointSize = 10.0;
}
`; // 顶点着色器
纹理取样器类型
他们都只能通过uniform
声明。
- sampler2D,2D取样器。
const FRAGMENT_SHADER_SOURCE = `
precision lowp float; // 定义精度
uniform sampler2D uSampler; // 定义纹理取样器
varying vec2 vTex;
void main() {
// 从图像中逐片元获取内容进行颜色填充
gl_FragColor = texture2D(uSampler, vTex);
}
`; // 片元着色器
- samplerCube,3D取样器
限定词
const
,声明一个常量,定义之后不能被改变。attribute
,只能出现在顶点着色器中,只能声明为全局变量,表示逐顶点信息。单个顶点的信息。uniform
, 可同时出现在 顶点着色器 和 片元着⾊器中。只读类型,强调一致性。用来存储的是影响所有顶点的数据。 如变换矩阵。varying
, 从顶点着色器向片元着色器传递数据。
// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
varying vec4 vColor; // 主要作用,顶点着色器向片元着色器中传递数据
void main() {
vColor = aPosition;
gl_Position = aPosition;
}
`; // 顶点着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float; // 片元着色器中需要声明精度
varying vec4 vColor; // 这里需要和顶点着色器声明一样的内容
void main() {
gl_FragColor = vColor;
}
`; // 片元着色器
精度限定
作用是提升运行效率,削减内存开⽀。
- 单独对于一个变量限制精度。
mediump float f;
。 - 通过
precision
关键字来修改着色器的默认精度。
const FRAGMENT_SHADER_SOURCE=`
// ⾼精度:highp, 低精度:lowp,
precision mediump float; // 必须设置精度
// 注意这里没有vec1类型,需要使用float代替
uniform float uColor; // 这里定义的vec和下面的赋值需要对应。
void main() {
gl_FragColor = vec4(uColor, 0.0, 0.0,1.0); // vec4
}
`; // 片元着色器
缓存区对象
缓冲区对象是WebGL系统中的一块内存区域,可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。
类型化数组
由于缓冲区对象存取的是大量相同类型数据,所以我们就可以使用类型化数组提供数据。
在 webgl 中,需要处理⼤量的相同类型数据,所以引入类型化数组,这样程序就可以预知到数组中的数据类型, 提⾼性能。
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])
创建一块内存区域
const buffer = gl.createBuffer();
将缓冲区绑定到gl上
const buffer = gl.createBuffer();
/**
* gl.bindBuffer(target, buffer)
* buffer: 已经创建好的缓冲区对象
* target:填入的是数据还是索引
* - gl.ARRAY_BUFFER: 表示缓冲区存储的是顶点的数据
* - gl.ELEMENT_ARRAY_BUFFER: 表示缓冲区存储的是顶点的索引值
*
* */
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
/**
* gl.bufferData(target, data, type)
* target:和bindBuffer的target一样的类型
* data:写⼊缓冲区的顶点数据。
* type: 如何写入数据,表示如何使⽤缓冲区对象中的数据
* - gl.STATIC_DRAW: 写⼊⼀次,多次绘制
* - gl.STREAM_DRAW: 写⼊⼀次,绘制若干次
* - gl.DYNAMIC_DRAW: 写⼊多次,绘制多次
*/
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
/***
* gl,vertexAttribPointer(location, size, type, normalized, stride, offset)
* 将缓冲区数据复制到顶点属性上。
* location:attribute变量位置
* size:指数量定每个顶点属性的组成
* type:指定数组中每个元素的数据类型
* - gl.FLOAT:浮点型
* - gl.UNSIGNED_BYTE:⽆符号字节
* - gl.SHORT:短整型
* - gl.UNSIGNED_SHORT:无符号短整型
* - gl.INT:整型
* - gl.UNSIGNED_INT:无符号整型
* normalized:当转换为浮点数时是否应该将整数数值归一化到特定的范围(是否转化为-1,1之间)
* stride:两个相邻顶点之间的字节数。可通过points.BYTES_PER_ELEMENT获取,这个在多个属性公用一个缓冲区时有用。
* offset:数据偏移量字节数。就是每个顶点在缓冲区取数据是否是连续的,不连续则需要指定偏移量
* */
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
// 激活attribute属性
gl.enableVertexAttribArray(aPosition)
// gl.vertexAttrib2f(aPosition, 0.0, 0.0)
/***
* 绘制
* 参数二:从哪个顶点开始绘制
* 参数三:绘制几个顶点
* */
gl.drawArrays(gl.POINTS, 0, 3);
缓冲区执行流程
多个属性共用一个缓冲区
通过gl,vertexAttribPointer(location, size, type, normalized, stride, offset)
中的tride
, offset
实现。
const ctx = document.getElementById('canvas')
const gl = ctx.getContext('webgl')
// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`; // 顶点着色器
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`; // 片元着色器
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aPointSize = gl.getAttribLocation(program, 'aPointSize');
const points = new Float32Array([
-0.3, -0.3, 10.0, // 10.0
0.3, -0.3, 30.0, // 30.0
0.3, 0.3, 50.0, // 50.0
])
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
// 获取1个数据字节数
const BYTES = points.BYTES_PER_ELEMENT;
// 每个顶点有三个数据组成,所以偏移三个BYTES,其实就是一个顶点多少个字节数
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0);
// 激活aPosition attribute属性
gl.enableVertexAttribArray(aPosition)
// 应为3个数据为一组,一组中最后一个数字为大小数据,所以数据偏移量为BYTES * 2
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, BYTES * 2);
// 激活aPointSize attribute属性
gl.enableVertexAttribArray(aPointSize)
gl.drawArrays(gl.POINTS, 0, 3);
绘制其他图形
使用drawArrays
来绘制图形。
- gl.POINTS 点 一系列点
- gl.LINES 线段 一系列单独的线段,如果顶点是奇数,最后一个会被忽略。
- gl.LINE_LOOP 闭合线 一系列连接的线段,结束时,会闭合终点和起点
- gl.LINE_STRIP 线条 一系列连接的线段,不会闭合终点和起点
- gl.TRIANGLES 三角形 一系列单独的三角形,如果想绘制多个三角形,我们必须传入的顶点数为3的倍数。
- gl.TRIANGLE_STRIP 三角带 一系列条带状的三角形
- gl.TRIANGLE_FAN 三角形 飘带状三角形
纹理绘制
const ctx = document.getElementById('canvas')
const gl = ctx.getContext('webgl')
// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
// 只传递顶点数据
attribute vec4 aPosition;
// 声明纹理坐标
attribute vec4 aTex;
// texture2D接收vec2类型,所以这里定义vec2类型
varying vec2 vTex;
void main() {
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
vTex = vec2(aTex.x, aTex.y);
}
`; // 顶点着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float; // 定义精度
uniform sampler2D uSampler; // 定义纹理取样器
varying vec2 vTex;
void main() {
/**
* 从图像中逐片元获取内容进行颜色填充
* vec4 texture2D(sampler2D sampler, vec2 coord)
*
* sampler 纹理单元编号
*
* coord 纹理坐标 (图片坐标)
*
* **/
gl_FragColor = texture2D(uSampler, vTex);
}
`; // 片元着色器
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aTex = gl.getAttribLocation(program, 'aTex');
const uSampler = gl.getUniformLocation(program, 'uSampler');
const points = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0,
])
const buffer = gl.createBuffer();
const BYTES = points.BYTES_PER_ELEMENT;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
// 每个顶点相差是个字节数据
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 4, 0);
gl.enableVertexAttribArray(aPosition)
// 顶点数据的偏移量,每组数据我们的前两个才是顶点需要的数据
gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, BYTES * 4, BYTES * 2);
gl.enableVertexAttribArray(aTex)
const img = new Image();
img.onload = function() {
// 创建纹理对象,存储纹理数据
const texture = gl.createTexture();
// 翻转 图片 Y轴 ,因为他和webgl的坐标方向不一样。
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
// 开启一个纹理单元
// Webgl 是通过纹理单元来管理纹理对象,每个纹理单元管理⼀张纹理图像。
gl.activeTexture(gl.TEXTURE0);
/**
* 绑定纹理对象
* gl.bindTexture(type, texture)
* - type 参数有以下两种:
* - gl.TEXTURE_2D: 二维纹理
* - gl.TEXTURE_CUBE_MAP: 立方体纹理
* - texture 纹理对象
* **/
gl.bindTexture(gl.TEXTURE_2D, texture);
/**
* 图像变化的时候获取纹理信息
* gl.texParamteri(type, pname, param)
*
* type 同上
*
* pname
* - gl.TEXTURE_MAG_FILTER 放⼤
* - gl.TEXTURE_MIN_FILTER 缩⼩
* - gl.TEXTURE_WRAP_S 横向(⽔平填充)
* - gl.TEXTURE_WRAP_T 纵向(垂直填充)
*
* 当pname为gl.TEXTURE_MAG_FILTER和 gl.TEXTURE_MIN_FILTER param 可以为
* - gl.NEAREST 使用像素颜色值
* - gl.LINEAR 使用四周的加权平均值
*
* 当pname为gl.TEXTURE_WRAP_S和 gl.TEXTURE_WRAP_T param 可以为
* - gl.REPEAT 平铺重复
* - gl.MIRRORED_REPEAT 镜像对称
* - gl.CLAMP_TO_EDGE 边缘延伸
* **/
// 处理放大缩小的逻辑
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
// 横向 纵向 平铺的方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
/**
* 配置纹理图像
* gl.texImage2D(type, level, internalformat, format,dataType, image)
*
* type 同上
*
* level 为0
*
* internalformat 图像的内部格式
* - gl.RGB
* - gl.RGBA
* - gl.ALPHA
* - gl.LUMINANCE 使用物体表面的 红绿蓝 分量的加权平均值来计算
* - gl.LUMINANCE_ALPHA
*
* format 纹理的内部格式,必须和 internalformat 相同
*
* dataType 纹理数据的数据类型
* - gl.UNSIGNED_BYTE
* - gl.UNSIGNED_SHORT_5_6_5
* - gl.UNSIGNED_SHORT_4_4_4_4
* - gl.UNSIGNED_SHORT_5_5_5_1
*
* image 图片对象
* **/
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
// 为uniform变量赋值,赋值纹理单元(编号)
gl.uniform1i(uSampler, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
img.src = '../assets/wenlitest.jpg'
3D基础
辅助函数
- 归一化函数, 归一化到 0-1 的区间内
// 归一化函数
function normalized(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i]
}
const middle = Math.sqrt(sum);
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] / middle;
}
}
- 叉积,求两个平面的法向量
// 叉积函数 获取法向量
function cross(a,b) {
return new Float32Array([
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
])
}
- 点积,求某点在x,y,z轴上的投影长度
// 点积函数 获取投影长度
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
- 向量差,获取视点到目标点之间的向量
// 向量差
function minus(a, b) {
return new Float32Array([
a[0] - b[0],
a[1] - b[1],
a[2] - b[2],
])
}
- 获取视图矩阵
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
// 视点
const eye = new Float32Array([eyex, eyey, eyez])
// 目标点
const lookAt = new Float32Array([lookAtx, lookAty, lookAtz])
// 上方向
const up = new Float32Array([upx, upy, upz])
// 确定z轴
const z = minus(eye, lookAt);
normalized(z);
normalized(up);
// 确定x轴
const x = cross(z, up);
normalized(x);
// 确定y轴
const y = cross(x, z);
return new Float32Array([
x[0], y[0], z[0], 0,
x[1], y[1], z[1], 0,
x[2], y[2], z[2], 0,
-dot(x,eye),-dot(y,eye),-dot(z,eye),1
])
}
正射投影
空间中的物体投影到平面上的尺寸是一样的。
获取正射投影矩阵
// 获取正射投影矩阵
function getOrtho(l, r, t, b, n, f) { // 左右上下近远
return new Float32Array([
2 / (r - l), 0, 0, 0,
0, 2/(t-b), 0, 0,
0, 0, -2/(f-n), 0,
-(r+l)/(r-l),-(t+b)/(t-b),-(f+n)/(f-n),1
])
}
透视投影
能够反映物体的空间形象。
// 获取透视投影矩阵
function getPerspective(fov, aspect, far, near) {
fov = fov * Math.PI / 180;
return new Float32Array([
1/(aspect*Math.tan(fov / 2)), 0, 0, 0,
0, 1/(Math.tan(fov/2)),0,0,
0,0,-(far+near)/(far-near),-(2*far*near)/(far-near),
0,0,-1,0,
])
}
往期文章
转载自:https://juejin.cn/post/7270416462677590068