WebGL学习(四)颜色与纹理

232 阅读10分钟

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个点赋予了不同颜色。

图片.png

2. 颜色

我们改动一下上面的代码

-gl.drawArrays(gl.POINTS, 0, 4)
+gl.drawArrays(gl.TRIANGLES, 0, 4)

效果

图片.png 我们使用三个点,画了一个三角形,但是颜色却变成了这种渐变,而且正好是三个点的颜色混合。

2.1 颜色是怎么来的

解释为什么之前,我们再来回顾一下webgl是怎么工作的

图片.png 从上到下,webgl对于每一个点的处理:

  1. 前三步,从缓冲区读取数据,通过顶点着色器处理,放入正确位置
  2. 第四步,进行图形装配。这里的装配就是上面的gl.drawArrays根据传的参数不同,进行不同的装配,比如gl.POINTS就只是画点,gl.TRIANGLES就是画三角形。注意此时没有颜色,只有一个框架。
  3. 第五步,光栅化就是填充像素点,然后片元着色器依次赋予颜色。

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)

图片.png 这里可以看出,片元着色器确实是一个片元一个片元着色的。

2.3 varying内插

图片.png 之前我们知道。varying变量是顶点着色器传递给片元着色器的一个通道,但是在传递之间,还有一步操作,称之为内插这也是为什么varying相对于uniform为可变的变量

图片.png 光栅化中会自动插入额外的颜色,来填充两个颜色之间的差异。具体原理可以去看看以下参考:

参考1,回答中提到了很多延伸知识

参考2,提到了重心插值

事实上,顶点着色器传递的所有数据,都会被插值。目的就是使变化平滑,比如对坐标进行插值就可以实现抗锯齿。

3. 纹理

3.1 纹理映射

简单点就是讲一张图片(纹理),像素对像素的复制到图像上。

webgl里面进行纹理映射的步骤大概如下:

  1. 准备好纹理图像
  2. 为几何图形配置纹理映射方式(比如位置,大小)
  3. 加载纹理图像并配置
  4. 将纹素赋给片元

3.2 纹理坐标

纹理也有自己的坐标系,单独分出来目的是为了方便操作

图片.png

纹理坐标是原点在左下角的坐标系,这里为了区分webglxy坐标,使用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

效果 image.png

4.1 (第一部分)设置纹理坐标

我们在着色器代码里面定义了一个varying变量fTexturePos用来设定纹理坐标,设定方法和顶点一样,但是注意:

  1. 纹理坐标是二维的
attribute vec2 texturePos; 
varying vec2 fTexturePos;
  1. 注意坐标的对应关系
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 // 往上移动了下面两个坐标点
])

image.png 图像有点拉伸,这是正确的,因为片元着色器需要将图元都对应上半截图片。

4.2 (第二部分)加载纹理

  1. 创建纹理对象
gl.createTexture()

纹理对象就是存储纹理图片的一个对象。

  1. 设置怎么解析图片
/**
 参数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)
  1. 激活纹理单元
gl.activeTexture(gl.TEXTURE0)

webgl至少有8个纹理单元,一个纹理单元就是管理一张纹理图片的单元。

  1. 绑定纹理对象
/**
参数1:纹理对象类型
参数2:绑定的纹理对象
**/
gl.bindTexture(gl.TEXTURE_2D, texture)

纹理单元纹理对象绑定在一起,并指定了纹理对象类型为gl.TEXTURE_2D image.png

  1. 配置纹理参数
/**
如何获取纹理像素颜色,如何重复像素
参数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:使用纹理图像边缘值

下面是各种参数的效果:

20200212141540166.png

2020021214173842.png 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.LUMINANCEL、L、L、1L:流明
gl.LUMINANCE_ALPHAL、L、L、透明度

内存中纹理数据常见类型:

格式描述
gl.UNSIGNED_BYTE无符号整型,每个颜色分量占据1字节
gl.UNSIGNED_SHORT_5_6_5RGB:每个分量分别占据5、6、5比特
gl.UNSIGNED_SHORT_4_4_4_4RGBA:每个分量分别占据4、4、4、4比特
gl.UNSIGNED_SHORT_5_5_5_1RGBA:RGB每个分量各占据5比特,A分量占据1比特
  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);
}

整个绑定过程: image.png

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;
}

image.png

// 省略初始化代码
/***************************  设置顶点坐标、纹理坐标 *****************************/
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)
})

效果: image.png 我使用了两张纹理,一张是背景的火山,一张是一个显示屏。

这里注意一点,这张显示屏的图片不是透明背景,而是纯白色背景,归一化后就是rgb(1, 1, 1),这样乘以背景火山的对应分量时,结果就是火山本身。