这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战
本文标题:WebGL第二十六课:贴图代码实战| 8月更文挑战
友情提示
这篇文章是WebGL课程专栏的第26篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
本课代码直接跳转获取:二十六课代码
引子
我们需要预备的概念:
- 我们传入WebGL的是一个一个的顶点信息, 包含(坐标, 颜色, UV,其他等等)
- 对于
画三角形模式
来说,WebGL每画三个点之后,就在这三个点内部,进行插值,计算出三个点内部的坐标,颜色,UV - 然后根据计算出来的插值,对三个点内部进行填充颜色,或者根据UV进行图片采样
上面的内容是在上一次课进行讲解的,不清楚的小伙伴先去上一次课了解一下更好。
创建一个目录
我们本次要搞贴图,所以我们创建一个目录
images
然后把你需要的图片放到这个目录里面,我的图片名字叫 funny-cat.jpeg
, 是一张猫的图片:
images/funny-cat.jpeg
前端js拉取图片
这里不难,主要就注意一点,拉取图片是一个异步过程
。
我们一定要在拉取图片完成之后,再把数据传入到WebGL
,否则就出不来图片,并且会报错。
代码如下:
var image = new Image();
image.src = "images/funny-cat.jpeg"; // 这里改成你自己的图片
image.addEventListener('load', function () {
// 加载完成之后,这个函数会被调用
});
将图片数据传入WebGL
这里没什么可以讲的,基本过程:
-
- 在 WebGL 里创建一个 贴图存储区。
-
- 将 WebGL 这个状态机切换到这个贴图存储区。
-
- 将刚才拉取的图片数据,传给WebGL。
代码如下:
// 在 WebGL 里创建一个 texture
let texture = gl.createTexture();
// 切换状态机到当前 texture
gl.bindTexture(gl.TEXTURE_2D, texture);
//
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
综合上面两个过程
由于拉取图片成功,是用异步回调函数的形式给出的,所以代码如下:
var images_loaing_progress = 0; // 搞一个变量,用来记录图片是否已经加载完成
function CreateTextureAndLoadImage(gl) {
// 在 WebGL 里创建一个 texture
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 异步加载一张图片,存进刚刚创建好的 texture 里
var image = new Image();
image.src = "images/funny-cat.jpeg";
image.addEventListener('load', function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
images_loaing_progress = 100;
});
return texture;
}
如果这样写的话, 很有可能你到时候,图片不出来,还会报错。
为什么呢?
因为WebGL
对图片的 width
height
有要求,那就是,最好都是 2
的整数次方。比如说2, 4, 8, 16,等等。
为了让所有的图片都可以好用,我们最终代码如下:
var images_loaing_progress = 0;
// 判断是否是 2 的 整数次方
function isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
function CreateTextureAndLoadImage(gl) {
// 在 WebGL 里创建一个 texture
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 异步加载一张图片,存进刚刚创建好的 texture 里
var image = new Image();
image.src = "images/funny-cat.jpeg";
image.addEventListener('load', function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
gl.generateMipmap(gl.TEXTURE_2D);
} else {
console.log("非2的整数次方");
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
images_loaing_progress = 100;
});
return texture;
}
好的,也许你发现了
gl.generateMipmap(gl.TEXTURE_2D);
这一句,是用来生成 Mipmap
的,什么叫 Mipmap
,后面用一篇文章接着说。这里这么写就行。
生成顶点坐标和顶点UV
不管是展示什么东西,我们必须有顶点,为了画三角形,我们至少准备三个顶点。
顶点的信息,这里有两个:
-
- 坐标
-
- UV
我们随意取三个点:
-
A
坐标:
(-1, -1)UV:
(0, 0) 图片左下角 -
B
坐标:
(1, -1)UV:
(1, 0) 图片右下角 -
C
坐标:
(0, 1)UV:
(0.5, 1) 图片最上面中间
我们将上面的数据,用一个平坦的数组表示出来:
var data = [-1, -1, 0, 0,
1, -1, 1, 0,
0, 1, 0.5, 1
];
var dataArr = new Float32Array(data);
var pointCount = 3;
注意,三个点的坐标,一定要是逆时针。
vertex_shader 接收UV信息
我们知道,vertex_shader 可以用来接收 buffer 中的信息。
顶点的坐标:
attribute vec2 a_PointVertex; // 顶点坐标
UV信息同样:
attribute vec2 a_PointUV; // 顶点UV
极其简单。
由于最终的颜色,和图片采用啥的,都是在fragment_shader
中进行。所以UV信息需要传到 fragment_shader
。
所以必须声明一个 varying 变量:
varying vec2 uv;
最终的 vertex_shader
:
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
uniform mat3 u_all; // 拉伸 旋转 位移
attribute vec2 a_PointVertex; // 顶点坐标
attribute vec2 a_PointUV; // 顶点UV
varying vec2 uv;
void main() {
vec3 coord = u_all * vec3(a_PointVertex, 1.0);
gl_Position = vec4(coord.x, coord.y, 0.0, 1.0);
uv = a_PointUV;
}
</script>
由于我们用的一个buffer来存储 坐标和UV 两个信息,所以,我们必须告知 vertex_shader
如何使用这个buffer:
var a_PointVertex = gl.getAttribLocation(program, 'a_PointVertex');
var a_PointUV = gl.getAttribLocation(program, 'a_PointUV');
gl.vertexAttribPointer(a_PointVertex, 2, gl.FLOAT, false, 16, 0); // 每个顶点 16 个字节, 坐标从第0个字节开始
gl.enableVertexAttribArray(a_PointVertex);
gl.vertexAttribPointer(a_PointUV, 2, gl.FLOAT, false, 16, 8); // 每个顶点 16 个字节, UV从第8个字节开始
gl.enableVertexAttribArray(a_PointUV);
fragment_shader 利用UV信息,对图片进行采样
我们知道 buffer
信息,在vertex_shader
可以用 attribute
的形式接收。
那么Image信息,在fragment_shader
中,怎么来取呢?
答案就是采样!
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
uniform sampler2D u_funny_cat; // 有趣的猫的图片
varying vec2 uv;
void main() {
vec4 sample_color = texture2D(u_funny_cat, uv);
gl_FragColor = vec4(sample_color, 1.0);
}
</script>
还有一步
var u_FunnyCatLocation = gl.getUniformLocation(program, "u_funny_cat");
gl.uniform1i(u_FunnyCatLocation, 0);
上面两句是干嘛的呢,我们看看刚才的fragment_shader
,是不是有一个uniform
变量u_funny_cat
。
这个变量代表的是我们存入WebGL中的图片的下标,我们只存了一个图,所以这个图的下标自然就是0。
看看效果
注意,我们需要拖动一下网页上的滑竿,才能出来图像,是因为拖动的时候会调用画图的代码。
首先,图片倒了,这个不是什么大问题。由于图片本身的坐标系,和UV所采用的坐标系不一样。
改法有很多种,这里我们直接改顶点的UV:
var data = [-1, -1, 0, 1,
1, -1, 1, 1,
0, 1, 0.5, 0
];
那么效果如下(还是需要先拖动一下滑竿):
哎呀,小猫咪显示的不完整啊。
通过更改顶点的UV,来显示完整的图片
由于三个顶点的UV就代表了,这三个点锚定在图片的那个点上,中间的填充,是用插值自动来做的。
所以,要让小猫咪显示完整的话,我们把C点(最上面的点)变成-1试试:
var data = [-1, -1, 0, 1,
1, -1, 1, 1,
0, 1, 0.5, -1
];
仔细分析一下:
根据三个点的UV,我们图中的红点处的UV根据插值,大概差不多就是 (0.5, 0)
而 (0.5, 0) 这个点,刚刚好就是原来的图片的 最上方中间位置。(注意,原图的Y是倒过来的, y = 0,就是最上方)。
那么其他的点,也是根据这个逻辑,来跟原图对应的,这就是所谓插值。
我们把
- 左边的点UV往左拉一拉
- 右边的点UV往右拉一拉
代码:
var data = [-1, -1, -0.5, 1,
1, -1, 1.5, 1,
0, 1, 0.5, -1
];
结果如下:
大家根据插值,和三个点的UV,来计算一下这个方框的大概UV,看看是不是,差不多就是一个图形的四个角。
好了,这就是为什么,这个框框里面就能完整的显示一个小猫咪了。
UV的y轴反了,非常不爽
由于这一点非常不爽,我们在外面构造UV信息的时候,经常不好想象。
所以我们不在外面构造的时候,进行这一步修正。
我们在vertex_shader
里修正:
uv = a_PointUV;
uv.y = 1.0 - uv.y; // 就这一句。。。。。。
看效果:
小猫咪又倒过来了,是因为我们数据的UV刚才修正过一遍,我们用将一开始的数据找回来:
var data = [-1, -1, 0, 0,
1, -1, 1, 0,
0, 1, 0.5, 1
];
效果:
又显示不完整了,这里我就不给出怎么显示完整的代码了,小伙伴们自己搞一搞。
正文结束,下面是答疑
小瓜瓜说:我如果乱填UV信息,会发生什么呢?
- 答:不会发生什么,无非就是三个点的UV信息是乱的,然后中间插值也是乱的,显示的图片也是乱的:
var data = [-1, -1, Math.random(), Math.random(),
1, -1, Math.random(), Math.random(),
0, 1, Math.random(), Math.random()
];
你可以试试这样写UV。。。