上车WebGL系列——用纹理画猫猫
因为要更好的学习 WebGl 就需要更多的上手敲代码,所以案例和演示代码就很显得重要了。为此,笔者打算将案例、源码都统一放到 github 仓库,并且搞了个在线文档供大家学习参考~大家可以给笔者点个 star!文章更新不迷路!
经过之前章节的学习,相信大家已经掌握了基本的二维几何图形绘制和着色器的基本工作原理,对 WebGL 已经有点熟悉了。那么这一节开始,我们开始学习 WegGL 中的纹理映射,let's go!
可能大家跟我一样,在还没学习到这一节的时候心里可能都会有个疑问,那就是虽然我们学 WebGL 的各种基础图形绘制、各种颜色控制,但如果我们要显示真实场景的图片,那我们应该怎样做呢?难道要自己把模型绘制出来,再上个色?比如说下面这张猫猫:
其实真的有心去画,慢慢绘制各种基本图形加线段,控制好每个像素点的颜色...emmm...好像也不是不行,只是工作量巨大,并且可能没有什么意义。那遇到这样的问题,我们应该如何解决呢?这个时候,纹理图像就登场了!
什么是纹理图像
纹理图像是通过纹理映射的技术,将一张图贴到我们绘制的几何图形的表面上,这样我们就能在 WebGL 中使用真实的图片了,而这样的图形就是纹理图像。
其实纹理映射就是将图像的每一个像素点的颜色映射到我们绘制好的图形上。回顾前两小节的内容,在顶点着色器执行完后,还有图形装配和光栅化的步骤,而光栅化后我们得到的是一个充满片元的图形,最后片元着色器再进行逐片元操作对每个像素点涂上颜色。而这里,无非就是给光栅化后的每个片元涂上对应照片中的颜色。
如上图所示,纹理映射的基本工作方式就是这样,根据图片将光栅化后的对应位置的每个片元涂上对应的颜色。 所以,组成纹理图像的基本单位就是一个个像素,这里称之为——纹素,纹素的颜色值使用 RGB
或者 RGBA
的格式。
纹理坐标
上文提到了纹理映射要将像素点的颜色涂到对应位置的纹素上,那我们就需要对"位置"有个明确的认识,这就需要用到——纹理坐标。没错,又双叒叕是坐标~我们想一下,我们既然要贴图,那是贴半张、还是一张呢?贴到目标载体的左上角还是右下角呢?
纹理坐标就是图像上的坐标,我们通过它可以拿到纹素的颜色,它的坐标系统如图所示:
这一看,还是我们比较熟悉的坐标系呢,左下角为原点。大家可能注意到了,笔者在图片的顶点都标了坐标(尽管图片是个长方形的),不管长宽,范围都是 0
到 1
。所以,这一点上我们的纹理坐标跟 WebGL 的坐标系统类似,范围就在 0-1
之间,并不依赖图片自身尺寸。(为了跟 x
、 y
坐标系统区分开,纹理坐标命名采用 s
和 t
来命名)
那有了纹理坐标后,我们就只需要将对应的纹理坐标贴到我们的 WebGL 系统的顶点坐标中就可以实现纹理图像了。我们可以通过下图来加深理解:
看到这里,相信你也知道了纹理映射的基本原理了,那接下来我们就进入实战,把文章开头的"猫猫"图贴到我们的 WebGL 图形的表面去!
第一张纹理图片
1. 着色器代码实现
首先看看顶点着色器的代码实现。通过上文的了解我们知道目前我们需要两种类型的坐标,其中一个是顶点坐标,另外一个就是纹理坐标,所以我们顶点着色器的代码实现如下:
const vertexCode = `
// 顶点坐标
attribute vec4 a_Position;
// 接收纹理坐标
attribute vec2 a_TexCoord;
// 向片元着色器传递纹理坐标
varying vec2 v_TexCoord;
void main () {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
`
上述代码其实我们都相对比较熟悉了,通过两个 attribute
变量分别接收 顶点、纹理 坐标,再通过 varying
变量 将纹理坐标传递给片元着色器(经过上一小节的学习,我们知道这里的纹理坐标其实是经过 WebGL 系统内插后的坐标值,它并不完全等于传入顶点着色器时候的纹理坐标)。
那么接下来,就该轮到片元着色器了。这里,它需要根据每个片元的的纹理坐标,到图像对应的纹素上提取颜色值,再绘制到当前片元中。
const fragmentCode = `
precision mediump float;
// 接收纹理坐标
varying vec2 v_TexCoord;
// 取样器,这里可将其理解为纹理对象
uniform sampler2D u_Sampler;
void main () {
// texture2D 用于抽取纹理图片中纹素的颜色
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`
在片元着色器的代码实现中,我们看到了两个相对比较陌生的东西,一个是 sampler2D
的变量类型,一个 texture2D
的内置函数。那么在这里,我们简单了解一下他们的作用:
sampler2D
。sampler 就是取样器(提取纹理图像的颜色),我们简单理解它就是纹素的颜色值texture2D
。内置函数,可抽取纹素颜色。传入单元编号(纹理对象)、纹理坐标两个参数使用。
2. WebGL 基础代码
这里,我们快速带过一下基础的 WebGL 长方形的绘制代码实现。(基本都是之前章节提过的,不会所有都深入)
因为我们的纹理图像是需要"贴"到一个长方形的模型上,所以我们还是需要跟之前章节一样绘制一个模型。回顾 WebGL绘制基本图形 中,我们可以通过两个三角形来实现一个长方形,坐标如下:
这里不再详细演示了,我们注意一下 buffer 数据即可,它跟之前的有一点不一样,我们这次存放的是顶点坐标和纹理坐标:
// 前两个是顶点坐标,后两个是纹理坐标(图像取的是整张图,所以是0-1)
const verticesTexCoords = new Float32Array([
-.5, .5, 0., 1.,
-.5, -.5, 0., 0.,
.5, .5, 1., 1.,
.5,-.5, 1., 0.
])
然后我们再注意下步进参数和偏移参数的设置即可:
// 设置顶点坐标
const a_Position = gl.getAttribLocation(program, 'a_Position')
// 注意步进参数设置
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
gl.enableVertexAttribArray(a_Position)
// 设置纹理坐标
const a_TexCoord = gl.getAttribLocation(program, 'a_TexCoord')
// 注意步进参数、偏移参数设置
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
gl.enableVertexAttribArray(a_TexCoord)
我们先来看一下模型的效果图(这里我暂时给图形涂上蓝色方便大家看):
我们要贴图的模型(长方形)已经造出来了,那接下来就让我们将图片贴到这个蓝色的长方形上面吧。
3. 实战纹理映射
首先第一步的准备工作,我们当然是要搞个图片了!这里我们直接通过 Image
对象来创建一个图片实例,并进行加载:
const img = new Image()
// 图片地址(注意不允许跨域)
img.src = '/public/images/third/4.1.jpeg'
img.onload = function () {
// 一系列实现纹理映射的相关代码
}
这里我们要注意一个小点就是 WebGL 中不可以使用跨域的图片,这一点跟我们平时对 <img />
标签、或者 Image
对象的理解会有些差异~如果我们在 WebGL 中使用了跨域的图片资源,浏览器将会出现如下的报错信息:
Uncaught DOMException Failed to execute 'texImage2D' on 'WebGLRenderingContext': The image element contains cross-origin data, and may not be loaded.
接下来,我们就分步骤具体看看图片加载完成后(onload
回调)我们具体需要怎么做:
1. 创建纹理对象
回顾缓冲区对象的使用,第一步其实跟这里是一样的,都是先要创建对象!这里我们通过 gl.createTexture 这个 api
来创建纹理对象:
const texture = gl.createTexture()
上述代码中,我们创建了一个纹理对象,用它来管理 WebGL 中的纹理图像。
2. 激活纹理单元
所谓纹理单元就是用来"管理"纹理图像的。我们每用一张图片,都要给他指定一个纹理单元。一般情况下,WebGL 中默认有 8
个纹理单元,从 gl.TEXTURE0
- gl.TEXTURE7
。
我们在使用纹理单元之前,首先要激活它,就是通过 gl.activeTexture 这个 api
:
// 参数就是待激活的纹理单元
gl.activeTexture(gl.TEXTURE0)
3. 绑定纹理对象
绑定...纹理对象?好像学缓冲区的时候也有绑定缓冲区对象这玩意...没错,就是这么相似,在使用纹理对象之前,我们也需要对其进行绑定。这一步我们依然可以跟缓冲区对象一样地理解:需要绑定纹理对象才能对其进行操作。
这一步,我们通过 gl.bindTexture 这个 api
来绑定纹理对象:
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.bindTexture
的第一个参数 target
,它可以传递好些值如:gl.TEXTURE_2D
、gl.TEXTURE_CUBE_MAP
、 gl.TEXTURE_3D
等等,这里我们只需要 gl.TEXTURE_2D
即可,因为我们的图像也是一张 2D 的猫猫照片而已。
到这一步,WebGL 系统中关于纹理对象的状态可以理解成如下图片:
4. 配置纹理对象
这一步比较关键,因为我们需要设置纹理图像以什么样的方式映射到我们的模型中,是放大还是缩小、是否要重复等等
首先我们配置一个 gl.TEXTURE_MIN_FILTER
纹理缩小:
// 纹理缩小,使用 gl.LINEAR 计算距离中心像素最近的四个像素的平均值
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
接着再配置 gl.TEXTURE_WRAP_*
纹理水平、垂直 填充 :
// 填充方式都是 gl.CLAMP_TO_EDGE 边缘切割取值
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.LINEAR
、gl.CLAMP_TO_EDGE
这些参数不太理解的也没关系,我们会在后面的学习中继续深入了解。这里我们仅需要知道我们通过配置这些参数就能实现我们的第一张纹理图像即可。
5. 分配纹理图像
配置好纹理对象的参数后,我们是不是该想到一件事!图片 onload
后我们好像从来都没有用到它呀!所以这一步,我们就需要将纹理图像给到我们的纹理对象去使用!
我们使用 gl.texImage2D 这个 api
来实现,然而...还是一如既往地一堆参数...
这里我们简单地过一下每个参数的简介,留个印象就好:
参数 | 描述 |
---|---|
target | 这个应该不用我介绍了,直接就 gl.TEXTURE_2D 它吧 |
level | 0 级是基本图像等级。这里我们传 0 |
internalformat | 图像的内部格式。如 RGB |
format | 纹理数据的格式。需要和 internalformat 的值一致 |
type | 纹理数据的类型。gl.UNSIGNED_BYTE :每个颜色分量占 1 字节 |
pixels | 纹理的像素源,本文是 Image 对象 |
// 本文用的图片是 jpeg 格式,所以格式参数均为 gl.RGB
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img)
那么到这一步,我们在 JavaScript 创建的 Image
图片对象就已经传入到 WebGL 系统的纹理对象中了。
6. 纹理单元传递给片元着色器
经过上一步的处理,现在我们的纹理图像已经存在 WebGL 的纹理对象中了。那还记得前文提到的片元着色器代码中的取样器变量 u_Sampler
吗?这里,我们就将纹理对象(纹理单元)传递给 u_Sampler
变量。(注意这里为什么是用 uniform
变量,首先是因为其需要在片元着色器中使用,其次它只是被分配一个纹理对象而已,不需要像 varying
一样有内插的过程)
这一步,我们通过 gl.uniform1i 这个 api
来实现:
// 获取 uniform 变量地址
const u_Sampler = gl.getUniformLocation(program, 'u_Sampler')
// 将 gl.TEXTURE0(0号纹理单元) 分配给 u_Sampler 变量
gl.uniform1i(u_Sampler, 0)
这个 api
的使用相对简单一点,我们只需要注意第二个参数,它是一个数字,传入 0
代表将 gl.TEXTURE0
绑定的纹理对象分配到 u_Sampler
上。
7. 片元着色器抽取纹素颜色
这一步,就是我们片元着色器中用到的内置函数 texture2D,它会根据片元的坐标 (x, y)
从纹理图像中抽取出对应位置的像素颜色,然后将颜色绘制到当前的片元中。
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
以上所有步骤,我们相关配置基本上就完成了,最后我们正常通过 gl.drawArrays
绘制我们的长方形,我们就能在 canvas
中看到我们的纹理图像了(也就是文章开头的示例程序)!直接看如下示例程序:
哈哈哈,图像居然是反的!这里我是故意的,上述介绍了纹理映射中的步骤中,其实我还隐藏了一步没有讲,那就是 Y
轴反转。当然,这一点放在最后讲也是为了加深大家的印象和理解。
首先来了解一下为什么我们需要 Y
轴反转这个操作,我们直接看下图即可:
由图可知,纹理坐标的原点是图片的左下角,而图片自身的坐标原点是左上角,所以我们不加入 Y
轴反转的时候,纹理坐标所取的纹素点其实跟原图在 Y
轴上是相反的!详情我们可以看看 百度百科-图像坐标系 的讲解~
所以!!我在上述示例程序中留了个交互功能,大家可以通过自行切换 Y
轴反转的状态来自行体验一下最终的纹理图像效果!
总结
- 创建纹理对象
gl.createTexture
- 设置
Y
轴反转gl.pixelStorei
- 激活纹理单元
gl.activeTexture
- 绑定纹理对象
gl.bindTexture
(绑定后才能操作纹理对象) - 配置纹理对象
gl.texParameteri
- 将纹理图像分配给纹理对象进行填充
gl.texImage2D
(纹理图像 -> 纹理对象) - 将纹理单元传递给片元着色器
gl.uniform1i
(纹理对象传递给u_Sampler
变量) - 片元着色器抽取纹素颜色 内置函数
texture2D
(通过取样器和片元坐标)
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhbkaefe
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13