WebGL笔记(三)纹理

前两篇整理了《WebGL编程指南》的一到四章,笔记的重要性体现出来了,130页的内容,整理到两篇文章中,能非常快的进行回顾或复习或重新学习(长时间不用,大部分知识就忘了,这也是整理笔记的主要原因);

本篇开始进入第5章“颜色与纹理”;

发生在顶点着色器和片元着色器之间的从图形到片元的转化,又称图元光栅化;

一、varying 变量

作用:从顶点着色器往片元着色器传值;

看下面的顶点着色器和片元着色器代码:

attribute vec4 a_Position;
attribute float a_PointSize;
attribute vec4 a_Color;
varying vec4 v_Color;

void main(){
  gl_Position = a_Position;
  gl_PointSize = a_PointSize;
  v_Color = a_Color;
}
precision mediump float;
varying vec4 v_Color;
void main(){
  gl_FragColor = v_Color;
}

两段代码中,都有一个”v_Color”变量的声明;顶点着色器中,对”v_Color”进行赋值,值是外部传进来的a_Color的值;

varying变量的好处是不用再单独处理不在顶点着色器中的属性(颜色等),在操作顶点着色器的时候就能把属性传进去;js相关操作如下:

gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, byteLen, fsize * 3);
gl.enableVertexAttribArray(a_Color);

没什么特别操作,就是创建缓冲区那一套逻辑,给a_Color变量分配值;

这里有个相关的demo,该demo中,有三个不同颜色的点,绘制成了一个三色渐变三角形;

从顶点着色器到片元着色器,有两个步骤:

  • 图形装配过程:将孤立的顶点坐标装配成几何图形,几何图形的类别是由 drawArrays() 的第一个参数决定;
  • 光栅化过程:将装配好的几何图形转化为片元;

光栅化过程中有一个内插过程,内插是指内部计算未指定的颜色,上面的例子中,只指定了三个顶点的颜色,将三角形构建完成后,内部其他像素的颜色都会先计算出来,计算完成后,才会传给片元着色器中的 v_Color,片元着色器就拿这些信息进行绘制了;所以,两个着色器中的 v_Color 不是一回事;

gl_FragCoord 内置变量

vec4 类型,第1、2个分量表示片元在canvas坐标系统中的坐标值;

可以用这个变量来绘制渐变,片元着色器代码:

precision mediump float;
void main(){
  gl_FragColor = vec4(gl_FragCoord.x/u_Width, gl_FragCoord.y/u_Height, gl_FragCoord.z/u_Height, 1.0);
}

二、纹理基本概念和原理

纹理映射:将一张图片映射到一个几何图形的表面上去;

这张图片又可以称为纹理图像或纹理;

纹理映射就是将光栅化后的每个片元(像素)涂上合适的颜色,组成纹理图像的像素被称为纹素(texels,texture elements)– 我觉得翻译成纹元更好一些吧;

WebGL中的纹理映射需要四步:

  • 准备好图像;
  • 为几何图形配置纹理映射方式;
  • 加载纹理图像,对其进行一些配置,以在WebGL中使用;
  • 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋值给片元;

三、纹理的具体操作

源代码在这儿,下面是一些关键的代码摘要;

顶点着色器代码:

attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;

void main(){
  gl_Position = a_Position;
  v_TexCoord = a_TexCoord;
}

片元着色器代码:

precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;

void main(){
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}

纹理数据类型:sampler2D / samplerCube

texture2D(sampler2D sampler, vec2 coord),返回值为纹素颜色;
u_Sampler: 纹理单元编号;coord: 指定纹理坐标

js 中纹理相关的代码:

// 创建纹理对象
var texture = gl.createTexture();    
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');

var img = new Image();
img.src = './resource/texture.jpg';
img.onload = function () {

  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 对纹理图像进行y轴反转
  gl.activeTexture(gl.TEXTURE0);      // 激活纹理单元,开启0号纹理单元
  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, img);
  // 将0号纹理传递给着色器中的取样器变量
  gl.uniform1i(u_Sampler, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
};

0、纹理坐标

纹理坐标系和WebGL坐标系不一致,需要将顶点进行对应,下面的图片中,将纹理的四个角映射到WebGL坐标系中一半(0.5)的区域内;

1、创建纹理对象

var texture = gl.createTexture(); 返回值为新创建的纹理对象,如果创建失败,为null;

删除纹理对象:gl.deleteTexture(texture)

2、对纹理图像进行y轴反转

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 图片坐标系和纹理坐标系,y轴相反,所以要反转;

第一个参数默认值为false,其他可选值为

  • gl.UNPACK_FLIP_Y_WEBGL:对图像进行y轴反转;
  • gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:将图像RGB颜色值的每一个分量乘以透明度A;

第二个参数为整数,非0或0,不知道具体是做什么的

3、激活纹理单元,开启纹理单元 gl.activeTexture(gl.TEXTURE0)

WebGL通过纹理单元的机制来管理多个纹理,每个纹理单元有一个单元编号来管理一张纹理图像;

硬件和WebGL实现决定系统支持的纹理单元个数,默认情况下,WebGL至少支持8个纹理单元,gl.TEXTURE0 到 gl.TEXTURE7;

使用纹理单元之前,要先激活;

纹理单元可跳跃使用,想用哪个用哪个,但要和后边第7步中的 uniform1i 参数统一;

4、绑定纹理对象 bindTexture(target, texture)

与绑定缓冲区对象类似,第二个参数 texture 是第一步中创建的纹理对象,第一个参数 target 取值如下:

  • gl.TEXTURE_2D:二维纹理;
  • gl.TEXTURE_CUBE_MAP:立方体纹理;

5、配置纹理展示规则 texParameteri(target, pname, param)

target:和上一步的 target 一样;

pname:取值如下,表示要修改的属性:

  • gl.TEXTURE_MIN_FILTER:纹理放大,指定WebGL在图片放大时,如何处理间隙
  • gl.TEXTURE_MAG_FILTER:纹理缩小,指定WebGL在图片缩小时,如何移除像素
  • gl.TEXTURE_WRAP_S:水平填充,如何对纹理图像的左侧或右侧空白区域进行填充
  • gl.TEXTURE_WRAP_T:垂直填充,如何对纹理图像的上侧或下侧空白区域进行填充

param:为第二个参数属性对应的值,即 pname 为属性,param为值;

gl.TEXTURE_MIN_FILTER / gl.TEXTURE_MAG_FILTER 取值范围如下:

  • gl.NEAREST:使用原纹理上距离映射后像素(新像素)中心最近的那个像素值的颜色值,作为新像素的值(使用曼哈顿距离);
  • gl.LINEAR:使用距离新像素中心最近的四个像素的颜色值的加权平均,作为新像素的值(效果更好,开销更大);

TEXTURE_WRAP_S / TEXTURE_WRAP_T 取值范围如下:

  • gl.REPEAT:平铺式重复纹理;
  • gl.MIRRORED_REPEAT:镜像对称式重复纹理;
  • gl.CLAMP_TO_EDGE:使用纹理图像边缘值填充剩余的纹理区域,重复边上的一像素;

点击这里查看展示规则相关demo,该demo中,水平方向是边缘模式(边缘的一像素拉伸),竖直是镜像模式;

6、将纹理图像分配给纹理对象 gl.texImage2D(target, level ,internalformat, format)

target 参数同第四步的 target;

level:为金字塔纹理准备的,暂不涉及,设为0即可;

internalformat:图像的内部格式,取值如下

  • gl.RGB
  • gl.RGBA
  • gl.ALPHA:(0.0,0.0,0.0,透明度)
  • gl.LUMINANCE:L、L、L、1L: 流明
  • gl.LUMINANCE_ALPHA:L、L、L, 透明度

format:纹理的数据格式,和 internalformat 保持一致;

type:纹理的数据类型,取值如下

  • gl.UNSIGNED_BYTE 无符号整型,每个颜色分量占一个字节
  • gl.UNSIGNED_SHORT_5_6_5 每个分量分别占5、6、5比特
  • gl.UNSIGNED_4_4_4_4 每个分量分别占4、4、4、4比特
  • gl.UNSIGNED_5_5_5_1 rgb各占5个比特,A占1个比特

img:图片对象

7、将纹理传递给着色器中的取样器变量

gl.uniform1i(u_Sampler, 1),第二个参数表示使用第几个纹理,和第3步中的纹理编号保持一致;

需要注意的点

如果图片尺寸不是2的n次幂,第5步需要设置水平、竖直为边缘模式,否则会Warning;

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

四、使用多个纹理

片元着色器代码

precision mediump float;
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
varying vec2 v_TexCoord;

void main(){
  vec4 color0 = texture2D(u_Sampler0, v_TexCoord);
  vec4 color1 = texture2D(u_Sampler1, v_TexCoord);
  gl_FragColor = color0 * color1;
}

使用多个纹理时,gl_FragColor 的值是多个纹素颜色矢量相乘;

如果纹理数量不定,可以在片元着色器中提前声明多个纹理变量,未被赋值的变量不会有影响,实际用到几个就往片元着色器中传几个;详细demo请点击查看


TODO

整个纹理操作过程中,还是有一些问题,这本书没有解释清楚的,比如

第2步的反转的时候,第一个参数,除了反转y轴,另一个是什么效果,第二个参数参数0和非0分别表示什么含义

第4步的立方体纹理;

第6步的金字塔纹理;

等等

这些暂时没有用到,可以先不深究,等完成一遍学习回来再看;

如果这篇文章对你有用,可以点击下面的按钮告诉我

0

发表回复