WebGL第二十六课:贴图代码实战| 8月更文挑战

681 阅读7分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

本文标题:WebGL第二十六课:贴图代码实战| 8月更文挑战

友情提示

这篇文章是WebGL课程专栏的第26篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

本课代码直接跳转获取:二十六课代码

引子

我们需要预备的概念:

  • 我们传入WebGL的是一个一个的顶点信息, 包含(坐标, 颜色, UV,其他等等)
  • 对于画三角形模式来说,WebGL每画三个点之后,就在这三个点内部,进行插值,计算出三个点内部的坐标,颜色,UV
  • 然后根据计算出来的插值,对三个点内部进行填充颜色,或者根据UV进行图片采样

上面的内容是在上一次课进行讲解的,不清楚的小伙伴先去上一次课了解一下更好。

创建一个目录

我们本次要搞贴图,所以我们创建一个目录

images

然后把你需要的图片放到这个目录里面,我的图片名字叫 funny-cat.jpeg, 是一张猫的图片:

images/funny-cat.jpeg

funny-cat.jpeg

前端js拉取图片

这里不难,主要就注意一点,拉取图片是一个异步过程

我们一定要在拉取图片完成之后,再把数据传入到WebGL,否则就出不来图片,并且会报错。

代码如下:

var image = new Image();
image.src = "images/funny-cat.jpeg"; // 这里改成你自己的图片
image.addEventListener('load', function () {
    // 加载完成之后,这个函数会被调用
});

将图片数据传入WebGL

这里没什么可以讲的,基本过程:

    1. 在 WebGL 里创建一个 贴图存储区。
    1. 将 WebGL 这个状态机切换到这个贴图存储区。
    1. 将刚才拉取的图片数据,传给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

不管是展示什么东西,我们必须有顶点,为了画三角形,我们至少准备三个顶点。

顶点的信息,这里有两个:

    1. 坐标
    1. 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。

看看效果

注意,我们需要拖动一下网页上的滑竿,才能出来图像,是因为拖动的时候会调用画图的代码。

image.png

首先,图片倒了,这个不是什么大问题。由于图片本身的坐标系,和UV所采用的坐标系不一样。

改法有很多种,这里我们直接改顶点的UV:

var data = [-1, -1, 0, 1,
            1, -1, 1, 1,
            0, 1, 0.5, 0
        ];

那么效果如下(还是需要先拖动一下滑竿):

image.png

哎呀,小猫咪显示的不完整啊。

通过更改顶点的UV,来显示完整的图片

由于三个顶点的UV就代表了,这三个点锚定在图片的那个点上,中间的填充,是用插值自动来做的。

所以,要让小猫咪显示完整的话,我们把C点(最上面的点)变成-1试试:

var data = [-1, -1, 0, 1,
    1, -1, 1, 1,
    0, 1, 0.5, -1
];

image.png

仔细分析一下:

image.png

根据三个点的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
        ];

结果如下:

image.png

大家根据插值,和三个点的UV,来计算一下这个方框的大概UV,看看是不是,差不多就是一个图形的四个角。

好了,这就是为什么,这个框框里面就能完整的显示一个小猫咪了。

UV的y轴反了,非常不爽

由于这一点非常不爽,我们在外面构造UV信息的时候,经常不好想象。

所以我们不在外面构造的时候,进行这一步修正。

我们在vertex_shader里修正:

          uv = a_PointUV;
          uv.y = 1.0 - uv.y; // 就这一句。。。。。。

看效果:

image.png

小猫咪又倒过来了,是因为我们数据的UV刚才修正过一遍,我们用将一开始的数据找回来:

var data = [-1, -1, 0, 0,
    1, -1, 1, 0,
    0, 1, 0.5, 1
];

效果:

image.png

又显示不完整了,这里我就不给出怎么显示完整的代码了,小伙伴们自己搞一搞。




  正文结束,下面是答疑

小瓜瓜说:我如果乱填UV信息,会发生什么呢?

  • 答:不会发生什么,无非就是三个点的UV信息是乱的,然后中间插值也是乱的,显示的图片也是乱的:
var data = [-1, -1, Math.random(), Math.random(),
          1, -1, Math.random(), Math.random(),
          0, 1, Math.random(), Math.random()
      ];

你可以试试这样写UV。。。