<十三>入门CocosShader之纹理映射

前言

前几篇的绘制都是基于基础图元的绘制,想要绘制的内容更真实,就需要更多顶点和更丰富的颜色。可以通过增加一张纹理来为物体添加更多的细节。
“纹理映射”是纹理应用中很重要的一项技术。
所谓的纹理映射,就是将一张图片映射到一个几何图形的表面上去。
举个例子:
如果将之前绘制的矩形填充纯白色的颜色值,它看起来就像一张白底画布,如果将一个图片贴到这个白底画布上,它就是一个有图像的画布,此时就可以称这个在矩形表面的图像为图像纹理或纹理。
注意图片和纹理不要搞混了,图片是图片,也有坐标,坐标遵循阅读模式,从左上角开始。纹理原点在右下角。在渲染时纹理坐标刚好上下翻转,这个在下面执行渲染指令时会有所体现。
image

纹理映射

纹理映射的作用是根据纹理图像为光栅化后的每个片元涂上适当的颜色。组成纹理图像的像素又称为“纹素”,每个纹素的颜色都可以用RGB或RGBA的格式编码。

纹理坐标

为了能把一张纹理映射到物体上,就需要指定物体的每个顶点要对应纹理上的哪个部分。
纹理使用上更多采用的是2D纹理。纹理坐标也有类似x,y的表示法,通常称之为uv坐标,如下图:
image
u对应水平方向,v对应垂直方向,原点为纹理的左下角。
如果是3D纹理,会有第三个方向w。
uvw对应的数值范围为0~1。

纹理采样

使用纹理坐标来获取纹理颜色的方法称为纹理采样。
在纹理映射中,物体的每个顶点会关联一个纹理坐标,表示从该纹理的哪个部分开始采样。
image

映射原理

映射的原理主要是将纹理图像的顶点映射到WebGL中的四个顶点,这里注意不是物体的顶点。

image

假如设置的映射范围超过了纹理坐标范围(0~1),WebGL会通过纹理的环绕方式来进行填充。

纹理环绕方式

  • Repeat
    image
    也是WebGL上默认的纹理环绕方式,会让超出的部分重复纹理图像的绘制

  • Mirrored_Repeat
    mirrored_repeat也会重复纹理图像的绘制,只不过重复的部分是镜像的
    image

  • Clamp_to_edge
    超出的部分会重复纹理坐标的边缘,有一种边缘被拉伸的效果
    image

  • 设置方法
    通过gl.texParameteril方式对坐标轴的不同方向进行设置
    image
    第二个参数gl.TEXTUREWRAP,2D纹理的S、T对应的就是uv;3D纹理S、T、R对应uvw。

需要注意的是:WebGL1的repeat模式对纹理的尺寸有要求,宽度和高度需要为2的整数次幂,否则运行时会出现警告,WebGL2则没有这种限制。

纹理过滤

纹理坐标不依赖分辨率,可以是任意的浮点值,所以WebGL知道如何将纹素映射到纹理坐标。但如果一个小的纹理需要映射到一个很大的物体上,就可能导致多像素都映射到同一个纹素上,纹理过滤就是为了解决不一致时纹理采样方式。
image

  • Nearest 临近过滤
    这种采样方式会直接选择中心点最接近纹理坐标的那个像素。效率最高。
  • Linear 线性过滤
    这种采样方式会选择离中心点最近的4个纹理像素加权计算,一个纹理像素距离中心点越近,该像素对最终的样本颜色影响最大。更平滑自然

image
左图部分区域可能有锯齿感,右图更加平滑。

在应用时,线性过滤可以产生更加真实的输出,而在做像素风格时则考虑Nearest。
放大图片时可采用Linear效果更自然平滑。缩小图片时可采用Nearest效率最高。

  • 设置接口
    image

纹理映射应用

尝试把纹理坐标关联给上一篇中实现的矩形
在这之前需要先准备好需要映射的图片,因为图片加载是异步的,所以需要等待加载完成再执行渲染指令。

  1. 准备图片
  2. await 加载图片
  3. 在顶点着色器中定义输入/输出纹理坐标
  4. 在片段着色器中定义一个2D的uniform纹理
  5. 结合uv,并添加uv信息(把原来的顶点位置信息插入uv信息)
  6. 创建图像缓存并绑定纹理对象
  7. 上传纹理图像
    image
    示例是加载到htmlElement资源,所以选择第三个接口
    示例接口都是gl接口,而2D纹理是一个uniform,为什么不用gl.uniform接口呢?因为在WebGL中会给纹理分配一个默认的纹理位置,称为“纹理单元”,默认会激活纹理单元0。这也是为什么没有手动给纹理分配纹理位置
    ,纹理贴图会自动默认绑定到激活的纹理单元上,当然也可以使用gl.uniform给片元着色器设置多个纹理,只需要激活对应的纹理单元即可。

通用设备支持8个纹理单元,中高端设备可能会支持更多。
一般情况下他们的编号是gl.TEXTURE0 ~ gl.TEXTURE8
通过这种编号方式可以在循环纹理单元时更方便。


//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}

function main(){
    let img = new Image();
    img.src = 'message.png';
    img.onload = function(){
        render(img);
    }
}


function render(img){
    //WebGL大多数元素都是可以通过状态来描述
    //状态是通过内置全局变量gl来调用切换
    //gl指向的是绘图上下文
    //绘图上下文需要通过canvas元素来获取
    //so,第一步,创建canvas元素
    const canvas = document.createElement('canvas');
    //这里解释一下为什么不直接在html中创建canvas元素,而是在js中动态创建
    //因为很多代码是在js中编写的,在这里定义的canvas当前脚本可以判断其类型,而这些属性在html中定义的canvas元素在js中获取是类型是any,无法使用其api代码提示

    //将创建的cānvas元素添加到body中
    document.getElementsByTagName('body')[0].appendChild(canvas)
    //定义画图尺寸
    canvas.width = 400;
    canvas.height = 300;
    //第二步,获取绘图上下文
    const gl = canvas.getContext("webgl");
    //判断当前浏览器是否支持webgl
    if(!gl){
        console.log('不支持webgl');
        return;
    }
    //第三步,编辑顶点着色器
    //定义着色器文本,因为在js中是无法直接使用GLSL语言,但是gl支持将文本转译成着色器,所以需要通过字符串的形式来编写
    const vertexSource = `
    attribute vec2 a_position;//接收一个顶点位置输入
    attribute vec4 a_color;//接收一个顶点颜色输入
    attribute vec2 a_uv;//接收一个纹理坐标输入

    varying vec4 v_color;//将顶点颜色传递给片段着色器
    varying vec2 v_uv;



    void main(){
        v_uv = a_uv;
        v_color = a_color;
        gl_Position = vec4(a_position,0.0,1.0);//gl_Position是一个Vec4,传入的顶点是一个vec2,所以需要转换成vec4,x,y取之a_position,没有深度所以z为0.0,w设置为1.0
    }
    `
    //创建顶点着色器
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    //定义顶点数据
    // const positions = [
    //     0,0,
    //     0.7,0,
    //     0,0.5,
    //     0.7,0.5
    // ]
    //顶点位置 && uv;这里直接顶满画布,uv坐标从0,0到1,1
    const vertexPosUv = [
        0,0,0,0,
        0.7,0,1,0,
        0,0.5,0,1,
        0.7,0.5,1,1
    ]
    //顶点颜色
    const colors = [
        255,0,0,255, 
        0,255,0,255,
        0,0,255,255,
        255,127,0,255,   
    ]
    //启用剔除背面,CocosCreator引擎默认开启了剔除背面
    gl.enable(gl.CULL_FACE);

    // //创建一个大的共享arryBuffer并指定它的长度,长度为positons的长度占用和color的长度占用之和
    // const arrayBuffer = new ArrayBuffer(positons.length * Float32Array.BYTES_PER_ELEMENT + colors.length);
    // //创建buffer
    // const positionsBuffer = new Float32Array(arrayBuffer);
    // const colorsBuffer = new Uint8Array(arrayBuffer);
    // //将数据填充到Buffer中
    // let offset = 0;//定义偏移量,表示数据在缓冲区的存储位置

    // for(let i = 0; i < positons.length; i += 2){
    //     positionsBuffer[offset] = positons[i];
    //     positionsBuffer[offset + 1] = positons[i+1];
    //     offset += 3;//偏移量每次增加3,因为顶点位置占用2个float,顶点颜色占用4个uint8 = 1个float
    // }
    // offset = 8;
    // for(let i = 0; i < colors.length; i += 4){
    //     colorsBuffer[offset] = colors[i];
    //     colorsBuffer[offset + 1] = colors[i+1];
    //     colorsBuffer[offset + 2] = colors[i+2];
    //     colorsBuffer[offset + 3] = colors[i+3];
    //     offset += 12;
    // }

    //创建缓冲区对象(顶点缓冲对象存储顶点数据)
    const vertexBuffer = gl.createBuffer();
    //在顶点着色器节点会在GPU上创建内存存储顶点数据,顶点缓冲对象就是为了管理这块内存
    //它会在GPU内存储大量的顶点,使用缓冲对象的好处就是可以一次性将大量顶点数据传输到GPU上,而不是每个顶点发送一次
    //顶点缓冲对象的类型是gl.ARRAY_BUFFER,WebGL允许同时绑定多个缓冲,只要类型不同,但同类型的Buffer会替换之前的Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    //将顶点数据传输到缓冲区对象中
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPosUv), gl.STATIC_DRAW);
    //gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
    //gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.STATIC_DRAW);
    // bufferData主要是初始化buffer对象的数据存储,第一个参数是缓冲区类型,第二个参数是设定数据存储区的大小(GPU内存通常不会很大,所以要合理分配内存),第三个参数是数据的使用方式
    // gl.STATIC_DRAW表示数据不会改变,这里因为传入的是一个固定的顶点数据,所以选择gl.STATIC_DRAW
    // gl.DYNAMIC_DRAW表示数据会频繁更新,常用于动态更新
    // gl.STREAM_DRAW表示数据每次渲染都会更新

    const colorsBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(colors), gl.STATIC_DRAW);


    //定义顶点索引
    const indices = [
        0,1,2,
        2,1,3
    ];
    //创建索引缓冲区对象
    const indexBuffer = gl.createBuffer();
    //将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
    //第四步,创建片段着色器
    //定义着色器文本
    const fragmentSource = `
    precision mediump float;
    //uniform vec4 u_color;
    varying vec4 v_color;
    varying vec2 v_uv;
    uniform sampler2D mainTexture;

    void main(){
        gl_FragColor = texture2D(mainTexture, v_uv) * v_color;//让图片结合uv生成纹理颜色,乘以顶点颜色,得到最终的像素颜色
        //gl_FragColor = vec4(1.0,0.0,0.5,1.0);//玫红色
        //gl_FragColor = u_color;
        gl_FragColor = v_color;
    }
    `;
    //创建片段着色器对象
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

    //第五步,创建着色程序
    const program = createProgram(gl, vertexShader, fragmentShader);

    //第六步,设置视口,方便将NDC坐标映射到画布上
    gl.viewport(0, 0, gl.canvas.width,gl.canvas.height);
    //清除画布颜色和颜色缓冲
    gl.clearColor(0, 0, 0, 1);//这里设置成黑色
    //清除画布颜色和颜色缓冲,由于最终呈现在屏幕上的颜色都要从颜色缓冲中读取,所以每次渲染都需要清除颜色缓冲,否则之前的渲染结果会残留导致花屏
    gl.clear(gl.COLOR_BUFFER_BIT);
    //使用着色程序对象
    gl.useProgram(program);
    //获取顶点位置属性变量
    const positonAttributeLocation = gl.getAttribLocation(program, 'a_position');
    //启用顶点位置属性变量
    gl.enableVertexAttribArray(positonAttributeLocation);

    const uvAttributeLocation = gl.getAttribLocation(program, 'a_uv');
    gl.enableVertexAttribArray(uvAttributeLocation);

    //获取顶点颜色属性变量
    const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');
    //启用顶点颜色属性变量
    gl.enableVertexAttribArray(colorAttributeLocation);


    // //和获取顶点坐标一样,这里获取u_color属性变量
    // const colorAttributeLocation = gl.getUniformLocation(program, 'u_color');
    // //设置颜色属性变量(这里采用4fv:4个float的数组形式,另外还有[1/2/3/4]+f/i/ui,matrix + [1/2/3/4]+fv等)
    // gl.uniform4fv(colorAttributeLocation, [Math.random(),Math.random(),Math.random(),1]);


    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
 
    //将缓冲区对象绑定到顶点位置属性变量上,并设置如何读取数据
    //gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 24, 0);
    gl.vertexAttribPointer(positonAttributeLocation, 2, gl.FLOAT, false, 16, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.vertexAttribPointer(uvAttributeLocation, 2, gl.FLOAT, false, 16, 8);

    gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
    //将缓冲区对象绑定到顶点颜色属性变量上,并设置如何读取数据
    gl.vertexAttribPointer(colorAttributeLocation, 4, gl.UNSIGNED_BYTE, true, 0, 0);

    //创建图像缓冲
    const texture = gl.createTexture();
    //绑定纹理对象到gl.TEXTURE_2D上,后续的纹理操作都会作用于这个纹理对象
    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.LINEAR);
    //纹理过滤参数,纹理过滤参数决定了当纹理像素大于渲染像素时如何采样纹理
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    //上传纹理图像
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

    //绘制三角形
    //gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE,0)
}

main();

保存运行一下:
image
体检图片加载跨源了。
这是因为这里的html是直接拖入浏览器的,开头的协议是file协议。

image
正常的网页应该是http或https开头的。
file协议更多的是将html解析为一个本地资源访问请求,是纯粹的本地文件,而图像标签src指向的是图像的url,浏览器出于安全考虑,会限制脚本内发起的跨源http请求,因此会出现跨源问题。

为了解决跨域问题,需要建立一个本地服务器。
这里采用npm的http-server
在当前项目文件终端运行:

npm install http-server -g 

全局安装。
安装完成后执行:http-server生成网址
image

在本地服务器上可以看到整个项目的目录结构
把图片的src改成带域名的链接

    img.src = 'http://127.0.0.1:8080/message.png';

重新运行:
image
不报错了,但纹理是反的。
这是因为图片的坐标和纹理坐标上上下相反的,通过WebGL中翻转Y轴的API将图像翻转过来。

绘制多张图像


//创建着色器
function createShader(gl,type,source){
    //创建顶点着色器对象 | 片段着色器
    const shader = gl.createShader(type);
    //将顶点着色器文本关联到顶点着色器对象 | xxx
    gl.shaderSource(shader, source);
    //编译顶点着色器 | xxx
    gl.compileShader(shader);
    //判断编译状态(编译成功才能使用)
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if(success){
        return shader;
    }
    //编译失败,打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    //删除顶点着色器 | xxx
    gl.deleteShader(shader);
}
//创建着色程序
function createProgram(gl, vertexShader, fragmentShader){
    //创建着色程序对象
    const program = gl.createProgram();
    //将顶点着色器关联到着色程序对象上
    gl.attachShader(program, vertexShader);
    //将片段着色器关联到着色程序对象上
    gl.attachShader(program, fragmentShader);
    //链接着色程序对象
    gl.linkProgram(program);
    //判断是否成功
    const success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(success){
        return program;
    }
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);

}

function main() {
    // 新增加一张纹理贴图
    const images = ["http://192.168.26.39:8080/logo.png", "http://192.168.26.39:8080/close-icon.png"];
    const dataList = [];
    let index = 0;
    for (let i = 0; i < 2; i++) {
        const image = new Image();
        image.src = images[i];
        dataList.push(image);
        image.onload = function () {
            index++;
            if (index >= images.length) {
                render(dataList);
            }
        };
    }
}

function render(dataList) {
    const canvas = document.createElement('canvas');
    document.getElementsByTagName('body')[0].appendChild(canvas);
    canvas.width = 400;
    canvas.height = 300;

    const gl = canvas.getContext("webgl");
    if (!gl) {
        return;
    }

    const vertexShaderSource = `
    attribute vec2 a_position;
    // 纹理贴图 uv 坐标
    attribute vec2 a_uv;
    attribute vec4 a_color;
    varying vec4 v_color;
    varying vec2 v_uv;
    // 着色器入口函数
    void main() {
        v_color = a_color;
        v_uv = a_uv;
        gl_Position = vec4(a_position, 0.0, 1.0);
    }`;

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    // 重新定义顶点位置
    const ratio = 0.5;
    const positions = [
        -ratio, -1,
        -ratio, 1,
        ratio, -1,
        ratio, 1
    ];
    const uvs = [
        0, 0, // 左下角
        0, 1, // 左上角
        1, 0, // 右下角
        1, 1 // 右上角
    ];

    // 在片元着色器文本处暂时屏蔽颜色带来的影响,但此处颜色值我们还是上传给顶点着色器
    const colors = [
        255, 0, 0, 255,
        0, 255, 0, 255,
       0, 0, 255, 255,
        255, 127, 0, 255
    ];

    const indices = [
        0, 1, 2,
        2, 1, 3 
    ];

    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    const attribOffset = (positions.length + uvs.length) * Float32Array.BYTES_PER_ELEMENT + colors.length;
    const arrayBuffer = new ArrayBuffer(attribOffset);
    const float32Buffer = new Float32Array(arrayBuffer);
    const colorBuffer = new Uint8Array(arrayBuffer);
    // 当前顶点属性结构方式是 pos + uv + color
    // 按 float 32 分布 pos(2)+ uv(2) + color(1)
    // 按子节分布 pos(2x4) + uv(2x4) + color(4)
    let offset = 0;
    let i = 0;
    for (i = 0; i < positions.length; i += 2) {
        float32Buffer[offset] = positions[i];
        float32Buffer[offset + 1] = positions[i + 1];
        offset += 5;
    }

    offset = 2;
    for (i = 0; i < uvs.length; i += 2) {
        float32Buffer[offset] = uvs[i];
        float32Buffer[offset + 1] = uvs[i + 1];
        offset += 5;
    }

    offset = 16;
    for (let j = 0; j < colors.length; j += 4) {
        // 2 个 position 的 float,加 4 个 unit8,2x4 + 4 = 12
        // stride + offset
        colorBuffer[offset] = colors[j];
        colorBuffer[offset + 1] = colors[j + 1];
        colorBuffer[offset + 2] = colors[j + 2];
        colorBuffer[offset + 3] = colors[j + 3];
        offset += 20;
    }

    gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.STATIC_DRAW);

    const indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW
    
    // 修改片元着色器文本
    const fragmentShaderSource = `
    precision mediump float;
    varying vec2 v_uv;
    varying vec4 v_color;
    // 新增一个纹理
    uniform sampler2D u_image0;
    uniform sampler2D u_image1;
    // 着色器入口函数
    void main() {
        vec4 tex1 = texture2D(u_image0, v_uv);
        vec4 tex2 = texture2D(u_image1, v_uv);
        // 将纹理色值相乘
        // rgb 和黑色相乘都为黑色(黑色 rgb 每分量都是 0),和白色相乘,都为原色(白色 rbg 每分量都是 1)
        gl_FragColor = tex1 * tex2;
    }`;
    
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
    const program = createProgram(gl, vertexShader, fragmentShader);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0, 0, 0, 255);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program);
    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(positionAttributeLocation);
    const uvAttributeLocation = gl.getAttribLocation(program, "a_uv");
    gl.enableVertexAttribArray(uvAttributeLocation);
    const colorAttributeLocation = gl.getAttribLocation(program, "a_color");
    gl.enableVertexAttribArray(colorAttributeLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 20, 0);
    // 新增顶点属性纹理坐标,这里大家应该都很清楚了,就不再多说了
    gl.vertexAttribPointer(uvAttributeLocation, 2, gl.FLOAT, false, 20, 8);
    gl.vertexAttribPointer(colorAttributeLocation, 4, gl.UNSIGNED_BYTE, true, 20, 16);
    
    // 判断有纹理才设置翻转
    if(dataList.length > 0){
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    }

    for (let j = 0; j < dataList.length; j++) {
        const data = dataList[j];
        const samplerName = `u_image${j}`;
        const u_image = gl.getUniformLocation(program, samplerName);
        // 设置每个纹理的位置值
        gl.uniform1i(u_image, j);
        const texture = gl.createTexture();
        gl.activeTexture(gl.TEXTURE0 + j);
        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.LINEAR);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
    } 
}

main();

补充

为什么在 GLSL 中变量的前缀都是 a_, u_ 或 v_ ?
这是一个命名约定,不是强制的,只是为了更清晰的知道值应该从哪里来,比如:a_ 就是指向顶点输入属性 attribute,代表数据是从顶点缓冲中来;u_ 就是全局变量 uniform,可以直接对着色器设置;v_ 代表可变量 varying,是从顶点着色器的顶点中插值而来。

posted @ 2025-02-20 15:28  EricShx  阅读(60)  评论(0)    收藏  举报