目录

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 使用多个纹理

此文上接WebGL 图像处理, 如果没读建议从那开始

现在可能是一个合适的时机去回答“如何使用 2 个或多个纹理?”。

非常简单,回到几节之前绘制一个图像的着色器, 将它升级到使用两个纹理。

首先改变代码加载两个图像,这其实不是 WebGL 的事情,是 HTML5 和 JavaScript 的事情,但是我也会涉及到。图像加载是异步的,如果你没有开始进行网络编程,可能需要适应一下。

基本上有两种方式来处理图像的加载,一种是重构代码, 让它在没有纹理的时候运行,当图像加载后, 再更新程序。我们会在以后的文章中用到这个方法。

这个例子中就等两个图像都加载完成后再开始绘制。

首先修改加载单个图像的方法,非常简单, 先创建一个新的 Image 对象,设置加载的 url,然后设置回调函数在图像加载完成后调用。

function loadImage(url, callback) {
    var image = new Image()
    image.src = url
    image.onload = callback
    return image
}

现在来创建一个方法加载一个 URL 序列,并且创建一个图像序列。 首先设置 imagesToLoad 为加载图像的个数,然后为每个图像调用 loadImage, 当 imagesToLoad 递减到 0 的时候说明所有图像加载完成,调用回调函数。

function loadImages(urls, callback) {
    var images = []
    var imagesToLoad = urls.length

    // 每个图像加载完成后调用一次
    var onImageLoad = function () {
        --imagesToLoad
        // 如果所有图像都加载完成就调用回调函数
        if (imagesToLoad === 0) {
            callback(images)
        }
    }

    for (var ii = 0; ii < imagesToLoad; ++ii) {
        var image = loadImage(urls[ii], onImageLoad)
        images.push(image)
    }
}

然后就可以像这样调用 loadImages

function main() {
    loadImages(['resources/leaves.jpg', 'resources/star.jpg'], render)
}

接下来修改着色器使用两个纹理,在这个例子中我们将两个纹理相乘。

#version 300 es
precision highp float;

// 纹理
*uniform sampler2D u_image0;
*uniform sampler2D u_image1;

// 从顶点着色器传入的 texCoords
in vec2 v_texCoord;

// 定义一个传递到片段着色器的变量
out vec2 outColor;

void main() {
*   vec4 color0 = texture2D(u_image0, v_texCoord);
*   vec4 color1 = texture2D(u_image1, v_texCoord);
*   outColor = color0 * color1;
}

需要创建两个 WebGL 纹理对象。

// 创建两个纹理
var textures = []
for (var ii = 0; ii < 2; ++ii) {
    var texture = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, texture)

    // 设置参数,不需要贴图
    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.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)

    // 上传图像到纹理
    var mipLevel = 0 // 最大的贴图
    var internalFormat = gl.RGBA // 纹理格式
    var srcFormat = gl.RGBA // 提供的数据格式
    var srcType = gl.UNSIGNED_BYTE // 提供的数据类型
    gl.texImage2D(
        gl.TEXTURE_2D,
        mipLevel,
        internalFormat,
        srcFormat,
        srcType,
        images[ii]
    )

    // 添加纹理到纹理数组
    textures.push(texture)
}

WebGL 有一个叫做"texture units"的对象,你可以把它看成是一个纹理引用的序列, 你需要告诉着色器每个 sampler(取样器) 使用哪一个 texture unit(纹理单元)。

  // 寻找取样器的位置
  var u_image0Location = gl.getUniformLocation(program, "u_image0");
  var u_image1Location = gl.getUniformLocation(program, "u_image1");

  ...

  // 设置使用的纹理单元
  gl.uniform1i(u_image0Location, 0);  // 纹理单元 0
  gl.uniform1i(u_image1Location, 1);  // 纹理单元 1

然后将每个纹理单元绑定纹理。

// 设置每个纹理单元对应一个纹理
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, textures[0])
gl.activeTexture(gl.TEXTURE1)
gl.bindTexture(gl.TEXTURE_2D, textures[1])

使用的两个图像像这样

这就是使用 WebGL 将它们相乘的结果。

有些需要回顾的部分。

理解纹理单元的简单方式是:所有的纹理方法可以在“激活的纹理单元”上使用,“激活的纹理单元”就是一个全局变量指向你想使用的所有纹理单元, 在 WebGL2 上每个纹理单元有 4 个目标对象, TEXTURE_2D 目标, TEXTURE_3D 目标, TEXTURE_2D_ARRAY 目标, 和 TEXTURE_CUBE_MAP 目标。 每个纹理方法针对激活纹理单元上的一个目标,如果用 JavaScript 表示 WebGL 方法可能像这样

var getContext = function () {
    var textureUnits = [
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        },
        {
            TEXTURE_2D: null,
            TEXTURE_3D: null,
            TEXTURE_2D_ARRAY: null,
            TEXTURE_CUBE_MAP: null
        }
    ]
    var activeTextureUnit = 0

    var activeTexture = function (unit) {
        // 将纹理单元枚举转换成索引
        var index = unit - gl.TEXTURE0
        // 设置激活纹理单元
        activeTextureUnit = index
    }

    var bindTexture = function (target, texture) {
        // 设置激活纹理单元的目标对象纹理
        textureUnits[activeTextureUnit][target] = texture
    }

    var texImage2D = function (target, ...args) {
        // 在当前纹理调用 texImage2D 激活纹理单元的目标对象纹理
        var texture = textureUnits[activeTextureUnit][target]
        texture.image2D(...args)
    }

    var texImage3D = function (target, ...args) {
        // 在当前纹理调用 texImage3D 激活纹理单元的目标对象纹理
        var texture = textureUnits[activeTextureUnit][target]
        texture.image3D(...args)
    }

    // 返回 WebGL API
    return {
        activeTexture: activeTexture,
        bindTexture: bindTexture,
        texImage2D: texImage2D,
        texImage3D: texImage3D
    }
}

着色器获得纹理单元

gl.uniform1i(u_image0Location, 0) // 纹理单元 0
gl.uniform1i(u_image1Location, 1) // 纹理单元 1

需要注意的是,设置全局变量的时候使用索引代替纹理单元,但是调用 gl.activeTexture 的时候你需要传递特殊的常量 gl.TEXTURE0, gl.TEXTURE1 之类。 幸运的是这些常量是连续的,所以这些代码

gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, textures[0])
gl.activeTexture(gl.TEXTURE1)
gl.bindTexture(gl.TEXTURE_2D, textures[1])

可以写成这样

gl.activeTexture(gl.TEXTURE0 + 0)
gl.bindTexture(gl.TEXTURE_2D, textures[0])
gl.activeTexture(gl.TEXTURE0 + 1)
gl.bindTexture(gl.TEXTURE_2D, textures[1])

或这样

for (var ii = 0; ii < 2; ++ii) {
    gl.activeTexture(gl.TEXTURE0 + ii)
    gl.bindTexture(gl.TEXTURE_2D, textures[ii])
}

希望这样能够帮助你理解 WebGL 单次绘制中如何使用多个纹理。

有意见或建议? 在GitHub上提issue.
comments powered by Disqus