1. varying变量
假设我们现在需要绘制四个点,这四个点在不同的位置而且有不同的颜色,我们需要怎么做?
使用uniform变量?
一次就只能给uniform赋一个值,所以要根据位置赋值不同的颜色是不可能的,一次执行就只能对应一个颜色。
简单的来说,就是顶点着色器用来传递变量给片段着色器,所以他是可变的。
// 顶点着色器
attribute vec4 pos;
// 顶点变量_color
attribute vec4 _color;
// 要传递给片段着色器的变量color
varying vec4 color;
void main(){
gl_Position = pos;
gl_PointSize = 30.0;
// 把外部传入的color值赋值给varying变量
color = _color;
}
// 片段着色器
precision mediump float;
// 声明一样的varying color
varying vec4 color;
void main(){
// 复制给颜色变量
gl_FragColor = color;
}
// 主程序
import vertexCode from './vertex.vert'
import fragmentCode from './fragment.frag'
const canvas = document.getElementById('webgl') as HTMLCanvasElement
const gl = canvas.getContext('webgl')
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(vertexShader, vertexCode)
gl.shaderSource(fragmentShader, fragmentCode)
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
const program = gl.createProgram();
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program);
gl.useProgram(program);
const pos = gl.getAttribLocation(program, 'pos')
const _color = gl.getAttribLocation(program, '_color')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
/**
准备了缓冲数据,包含四个点坐标和颜色
*/
const data = new Float32Array([
/** 坐标 */ -0.5, 0.5, /**颜色 */ 1, 1, 1, 1,
/** 坐标 */ 0.5, 0.5, /**颜色 */1, 0, 0, 1,
/** 坐标 */ 0.5, -0.5,/**颜色 */ 0, 1, 0, 1,
/** 坐标 */ -0.5, -0.5, /**颜色 */0, 0, 1, 1
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
// 赋值给几个点
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, eleSize * 6, 0)
gl.vertexAttribPointer(_color, 4, gl.FLOAT, false, eleSize * 6, eleSize * 2)
gl.enableVertexAttribArray(pos)
gl.enableVertexAttribArray(_color)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 4)
效果,左上角开始,顺时针4个点赋予了不同颜色。
2. 颜色
我们改动一下上面的代码
-gl.drawArrays(gl.POINTS, 0, 4)
+gl.drawArrays(gl.TRIANGLES, 0, 4)
效果
我们使用三个点,画了一个三角形,但是颜色却变成了这种渐变,而且正好是三个点的颜色混合。
2.1 颜色是怎么来的
解释为什么之前,我们再来回顾一下webgl是怎么工作的
从上到下,
webgl对于每一个点的处理:
- 前三步,从缓冲区读取数据,通过
顶点着色器处理,放入正确位置 - 第四步,进行
图形装配。这里的装配就是上面的gl.drawArrays根据传的参数不同,进行不同的装配,比如gl.POINTS就只是画点,gl.TRIANGLES就是画三角形。注意此时没有颜色,只有一个框架。 - 第五步,
光栅化就是填充像素点,然后片元着色器依次赋予颜色。
2.2 实验
现在我们试验一下,片段着色器是否是一个一个片元着色的。
// 顶点着色器
attribute vec4 pos;
void main(){
gl_Position = pos;
gl_PointSize = 30.0;
}
// 片段着色器
precision mediump float;
void main(){
precision mediump float;
void main(){
/**
* gl_FragCoord是内置变量,注意:表示的是片元在canvas坐标系中的位置
*
* canvas坐标原点在左上角,webgl原点在canvas中心
* 这里除以canvas的宽高,是为了归一化。从而实现从左到右,红色越来越亮
*/
gl_FragColor = vec4(1.0, 0.0, 0.0, gl_FragCoord.x / 1200.0);
}
// 主程序
// ....跳过初始化
const pos = gl.getAttribLocation(program, 'pos')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
const data = new Float32Array([
-0.5, 0,
0, 0.5,
0.5, 0,
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, eleSize * 2, 0)
gl.enableVertexAttribArray(pos)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
// 画个三角形
gl.drawArrays(gl.TRIANGLES, 0, 3)
这里可以看出,片元着色器确实是一个片元一个片元着色的。
2.3 varying内插
之前我们知道。
varying变量是顶点着色器传递给片元着色器的一个通道,但是在传递之间,还有一步操作,称之为内插。这也是为什么varying相对于uniform为可变的变量
光栅化中会自动插入额外的颜色,来填充两个颜色之间的差异。具体原理可以去看看以下参考:
事实上,顶点着色器传递的所有数据,都会被插值。目的就是使变化平滑,比如对坐标进行插值就可以实现抗锯齿。
3. 纹理
3.1 纹理映射
简单点就是讲一张图片(纹理),像素对像素的复制到图像上。
在webgl里面进行纹理映射的步骤大概如下:
- 准备好纹理图像
- 为几何图形配置纹理映射方式(比如位置,大小)
- 加载纹理图像并配置
- 将纹素赋给片元
3.2 纹理坐标
纹理也有自己的坐标系,单独分出来目的是为了方便操作
纹理坐标是原点在左下角的坐标系,这里为了区分webgl的xy坐标,使用t、s来表示坐标轴。这里要注意一下,纹理坐标是二维的,webgl是三维的。
对应关系在上图有,实际使用的时候就是按照这样的对应关系进行映射。
4 纹理实践
先上代码,由于步骤有点多,后面在分段解释。
// 顶点着色器
attribute vec4 geometryPos;
// 注意这里是二维变量,因为纹理坐标是二维的
attribute vec2 texturePos;
varying vec2 fTexturePos;
void main(){
gl_Position = geometryPos;
gl_PointSize = 30.0;
fTexturePos = texturePos;
}
// 片元着色器
precision mediump float;
uniform sampler2D sampler;
varying vec2 fTexturePos;
void main(){
gl_FragColor = texture2D(sampler, fTexturePos);
}
// 主程序
import vertexCode from './vertex.vert'
import fragmentCode from './fragment.frag'
import textureImag from './texture.jpg'
// ....创建program程序
/*************************** 设置顶点坐标、纹理坐标 *****************************/
const geometryPos = gl.getAttribLocation(program, 'geometryPos')
const texturePos = gl.getAttribLocation(program, 'texturePos')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
const data = 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 eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(geometryPos, 2, gl.FLOAT, false, eleSize * 4, 0)
gl.vertexAttribPointer(texturePos, 2, gl.FLOAT, false, eleSize * 4, eleSize * 2)
gl.enableVertexAttribArray(geometryPos)
gl.enableVertexAttribArray(texturePos)
/*************************** 设置纹理 *****************************/
// 创建纹理对象
const texture = gl.createTexture()
// 获取采样器变量
const sampler = gl.getUniformLocation(program, 'sampler')
// 创建image对象来出来图片
const image = new Image()
// 在事件中处理图像
image.onload = () => {
// 这一步是必须的,因为原始图像的坐标轴原点在左上角,和纹理坐标是反的
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
// 激活一个纹理单元来处理纹理,至少有8个
gl.activeTexture(gl.TEXTURE0)
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture)
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
// 配置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)
// 将纹理处理单元传递给采样器
gl.uniform1i(sampler, 0)
// 绘制图像
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
// 配置图片地址
image.src = textureImag
效果
4.1 (第一部分)设置纹理坐标
我们在着色器代码里面定义了一个varying变量fTexturePos用来设定纹理坐标,设定方法和顶点一样,但是注意:
- 纹理坐标是二维的
attribute vec2 texturePos;
varying vec2 fTexturePos;
- 注意坐标的对应关系
const data = 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 ])
这里可以看到,webgl中的(-0.5, 0.5)对应的(0.0,1.0),以此类推(可以自己画两个坐标图对应一下)。
我们可以改变一下纹理坐标看看效果,假如现在我想要显示图片的上半截
const data = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.5, // 往上移动了下面两个坐标点
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.5 // 往上移动了下面两个坐标点
])
图像有点拉伸,这是正确的,因为片元着色器需要将图元都对应上半截图片。
4.2 (第二部分)加载纹理
- 创建纹理对象
gl.createTexture()
纹理对象就是存储纹理图片的一个对象。
- 设置怎么解析图片
/**
参数1:
gl.UNPACK_FLIP_Y_WEBGL:y轴翻转
gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:RGB的每一个分量乘以一个ALPHA值
参数2:参数1的值true=1,false=0
*/
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
- 激活纹理单元
gl.activeTexture(gl.TEXTURE0)
webgl至少有8个纹理单元,一个纹理单元就是管理一张纹理图片的单元。
- 绑定纹理对象
/**
参数1:纹理对象类型
参数2:绑定的纹理对象
**/
gl.bindTexture(gl.TEXTURE_2D, texture)
将纹理单元、纹理对象绑定在一起,并指定了纹理对象类型为gl.TEXTURE_2D
- 配置纹理参数
/**
如何获取纹理像素颜色,如何重复像素
参数1:纹理对象类型
参数2:纹理参数(将纹理覆盖到图形的处理方法)
参数3:纹理参数的值
**/
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
纹理参数有如下:
gl.TEXTURE_MAG_FILTER:放大方法,假如纹理图像比绘制图形小,告诉webgl怎么填充空出来的像素
gl.TEXTURE_MIN_FILTER: 缩小方法,加入纹理图像比绘制图形大,告诉webgl怎么删减纹理的像素
gl.TEXTURE_WRAP_S: 水平填充,如何对左右侧的像素填充
gl.TEXTURE_WRAP_T: 垂直填充,如何对上下的像素填充
对应的参数值有如下:
gl.TEXTURE_MAG_FILTER和gl.TEXTURE_MIN_FILTER:
gl.NEAREST:使用原纹理上距离映射后像素 (新像素) 中心最近的那个像素的颜色值,作为新像素的值 (使用曼哈顿距离,或者叫棋盘距离、直角距离)
gl.LINEAR:使用距离新像素中心最近的四个像素的颜色值的加权平均,作为新像素的值 (与gl.NEAREST相比,该方法图像质量更好,但是会有较大的开销。
上面的值不包含金字塔纹理类型(mipmap、多级渐远纹理),什么是mipmap类型不赘述,大概就是一系列不同分辨率的纹理图像,而且可以随着分辨率等级改变映射方式
TEXTURE_WRAP_S和TEXTURE_WRAP_T:
- gl.REPEAT:平铺式的重复纹理
- gl.MIRRORED_REPEAT:镜像对称式的重复纹理
- gl.CLAMP_TO_EDGE:使用纹理图像边缘值
下面是各种参数的效果:
6. 配置纹理图像
/**
传递纹理图像给纹理对象,同时还能配置一些特性
target: 纹理对象类型
level: 为 mipmap准备的参数,映射等级
internalformat: 内部格式,webgl内部用什么格式处理图像
format: 外部格式,就是纹理图片本身的格式。webgl中必须是和内部格式一样
type: 内存中纹理数据类型
source: TexImageSource
**/
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)
下面是常用的内外部格式:
| 格式 | 描述 |
|---|---|
gl.RGB | 红、绿、蓝 |
gl.RGBA | 红、绿、蓝、透明度 |
gl.ALPHA | (0.0,0.0,0.0,透明度) |
gl.LUMINANCE | L、L、L、1L:流明 |
gl.LUMINANCE_ALPHA | L、L、L、透明度 |
内存中纹理数据常见类型:
| 格式 | 描述 |
|---|---|
gl.UNSIGNED_BYTE | 无符号整型,每个颜色分量占据1字节 |
gl.UNSIGNED_SHORT_5_6_5 | RGB:每个分量分别占据5、6、5比特 |
gl.UNSIGNED_SHORT_4_4_4_4 | RGBA:每个分量分别占据4、4、4、4比特 |
gl.UNSIGNED_SHORT_5_5_5_1 | RGBA:RGB每个分量各占据5比特,A分量占据1比特 |
- 将纹理处理单元传递给采样器
/***
给uniform变量赋值,指定为0号纹理单元,也就是将0号纹理对象传给了sampler
***/
gl.uniform1i(sampler, 0)
// 片元着色器
precision mediump float;
/**
纹理专用的类型
gl.TEXTURE_2D -> sampler2D
gl.TEXTURE_CUBE_MAP -> samplerCube
**/
uniform sampler2D sampler;
varying vec2 fTexturePos;
void main(){
/**
参数1:纹理处理单元编号
参数2:纹理坐标
*/
gl_FragColor = texture2D(sampler, fTexturePos);
}
整个绑定过程:
5.多重纹理
就是将多张纹理绑定在同一个形状上,先看看程序。
// 顶点着色器
attribute vec4 geometryPos;
attribute vec2 texturePos;
varying vec2 fTexturePos;
void main(){
gl_Position = geometryPos;
gl_PointSize = 30.0;
fTexturePos = texturePos;
}
// 片元着色器
precision mediump float;
// 由于使用多张纹理,定义了两个采样器
uniform sampler2D sampler0;
uniform sampler2D sampler1;
varying vec2 fTexturePos;
void main(){
vec4 color0 = texture2D(sampler0, fTexturePos);
vec4 color1 = texture2D(sampler1, fTexturePos);
// 这里直接乘了两个向量,他们的分量分别相乘
// 你也可以用其他运算,比如加法减法
gl_FragColor = color0 * color1;
}
// 省略初始化代码
/*************************** 设置顶点坐标、纹理坐标 *****************************/
const geometryPos = gl.getAttribLocation(program, 'geometryPos')
const texturePos = gl.getAttribLocation(program, 'texturePos')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
const data = 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 eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(geometryPos, 2, gl.FLOAT, false, eleSize * 4, 0)
gl.vertexAttribPointer(texturePos, 2, gl.FLOAT, false, eleSize * 4, eleSize * 2)
gl.enableVertexAttribArray(geometryPos)
gl.enableVertexAttribArray(texturePos)
/*************************** 设置纹理 *****************************/
// 这里多了一个image标签
const image0 = new Image()
const image1 = new Image()
image0.src = textureImg0
image1.src = textureImg1
// 纹理单元激活标志
const activeFlags = [false, false]
// 简单封装一个初始纹理函数
// 设置主要过程和单个纹理一样
const initTexture = (samplerIndex: number, image: HTMLImageElement) => {
const texture = gl.createTexture()
const sampler = gl.getUniformLocation(program, `sampler${samplerIndex}`)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
// @ts-ignore
gl.activeTexture(gl[`TEXTURE${samplerIndex}`])
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
gl.uniform1i(sampler, samplerIndex)
activeFlags[samplerIndex] = true
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
// 两张纹理都准备好了才绘制图形
if (activeFlags.every(Boolean)) {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
}
image0.decode().then(() => {
initTexture(0, image0)
})
image1.decode().then(() => {
initTexture(1, image1)
})
效果:
我使用了两张纹理,一张是背景的火山,一张是一个显示屏。
这里注意一点,这张显示屏的图片不是透明背景,而是纯白色背景,归一化后就是rgb(1, 1, 1),这样乘以背景火山的对应分量时,结果就是火山本身。