Cocos Shader 基础入门:纹理映射

纹理的应用就涉及到一项很重要的技术:纹理映射。所谓纹理映射,就是将一张图片映射到一个几何图形的表面上去,比如纹理映射到矩形物体上,这个矩形看上去就像是一张图片,这张图片又可以称为纹理图像或纹理

 

纹理映射

 

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

 

纹理坐标

 

为了能把一张纹理映射到物体上,我们需要指定物体的每个顶点各自对应纹理的哪个部分。纹理使用上更多采用的是 2D 纹理,纹理坐标在 x 和 y 轴上,范围在 0-1 之间。2D 的纹理坐标通常又称之为 uv 坐标,u 对应水平方向,也就是 x 轴,v 对应垂直方向,也就是 y 轴。如果是 3D 纹理,第三个则是 w,对应 z 轴。纹理坐标始于(0,0)点,也就是纹理左下角,终于(1,1),也就是纹理的右上角。使用纹理坐标来获取纹理颜色的方式称之为采样。每个顶点会关联着一个纹理坐标,用来表明该从纹理的哪部分采样。

 

 

纹理坐标看起来像是这样的:
const uvs = [
    0, 0, // 左下角
    0, 1, // 左上角
    1, 0, // 右下角
    1, 1 // 右上角
];

 

 

映射原理主要是将纹理图像的顶点映射到 WebGL 坐标系统的四个顶点。

 

 

纹理环绕方式

 

纹理坐标的范围通常是从 (0, 0) 到 (1, 1),如果超出这个范围该怎么办呢?OpenGL 默认行为是重复这个纹理图像,但是也提供了一些其它选择:

 

// 可以通过 gl.texParameter[fi] 对坐标不同轴向设置(2D 纹理 st 对应 uv,3D 纹理 str 对应 uvw )
// void gl.texParameterf(target, pname, param);
// 参数请参考:https://developer.mozilla.org/zh ... ontext/texParameter
// 由于应用条件较多,可以直接上链接了解一下,然后对应理解教程里涉及的部分即可
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);

 

当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。

 

 

纹理过滤

 

纹理坐标不依赖分辨率,可以是任意浮点值,所以 OpenGL 知道如何将纹素映射到纹理坐标。但是,如果此时有一个小的纹理需要映射到一个很大的物体上,就可能导致多个像素都映射到同一个纹素上,相反,单个像素可能会被映射到多个纹素。纹理过滤就是为了解决不一致时纹理的采样计算问题,其中最重要的就是如下两种:

 

  • NEAREST 临近过滤(下图左):选择中心点最接近纹理坐标的那个像素,也是最简单的纹理过滤方式,效率最高。
  • LINEAR 线性过滤(下图右):选择中心点周围最近的 4 个纹素加权计算出来,一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。

 

 

从图中可以看出,采用临近过滤的图片有更明显的锯齿感(比如眼眶那个地方),而右边图片则更加平滑。我这里选用的图片尺寸较大,尺寸小的会更加明显。线性过滤可以产生更加真实的输出,但是如果想开发像素风格的游戏,就可以用临近过滤选项。

 

当对图像进行放大和缩小的时候,我们可以选择不同的过滤选项。比如:在缩小的时候采用临近过滤,获取最高效率;放大时用线性过滤,获得较好表现。纹理过滤的使用方式跟纹理环绕类似:
// 当进行缩小时
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// 当进行放大时
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

 

 

将纹理应用到矩形上

 

接着,试着把纹理坐标关联给上一章的矩形。先将所有的顶点颜色还原成白色,下方列出了所有代码:
function createShader(gl, type, source) {
    // ...
}

function createProgram(gl, vertexShader, fragmentShader) {
    // ...
}

function main() {
  const image = new Image();
  // 如果是用 WebGL 中文文档上内置的运行环境编辑内容的,可以直接用网站内置的纹理图片。 

 
  // 由于我这里是自定义了本地的文件,因此创建了一个本地服务器来加载图片。使用本地文件的方式在文章末尾处。
  image.src = "http://192.168.55.63:8080/logo.png";
  image.onload = function() {
    render(image);
  };
}

function render() {
    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 = (image.width / image.height) / (canvas.width / canvas.height);
    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;
        float32Buffer[offset + 1] = positions[i + 1];
        offset += 5;
    }

    offset = 2;
    for (i = 0; i < uvs.length; i += 2) {
        float32Buffer[offset] = uvs;
        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;
    // GLSL 有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀
    // 比如此处使用的是 2D 纹理,类型就定义为 sampler2D
    uniform sampler2D u_image;
    // 着色器入口函数
    void main() {
        // 使用 GLSL 内建函数 texture2D 采样纹理,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标
        // 函数会使用之前设置的纹理参数对相应的颜色值进行采样,这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
        gl_FragColor = texture2D(u_image, v_uv);
    }`;
    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);
    const 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.LINEAR);
    // gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels);
    // 此接口主要为了指定二维纹理图像,图像的来源有多种,可以直接采用 HTMLCanvasElement、HTMLImageElement 或者 base64。此处选用最基础的 HTMLImageElement 讲解。
    // 关于参数的详细内容请参考:https://developer.mozilla.org/zh ... gContext/texImage2D
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}

 

最终,我们会在屏幕上看到这样的成像:

 

 

图片上下颠倒了,这是因为除了纹理坐标之外,图片自身也是有坐标系的,图片的坐标原点始于左上角,终于右下角,取值范围也是 0-1。把一张图片加载到纹理中,图片数据就会从图片坐标系到了纹理坐标系,此时图片就已经出现了上下倒置,所以我们需要一个 flipY 的操作,在渲染的时候将上下再进行一次倒置。
// 翻转图片
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

 

 

可能会有些同学有疑问,为什么 sampler2D 是个 uniform,但是却不用 gl.uniform 相关来赋值呢?因为在 OpenGL 中,会给纹理分配一个默认的纹理位置,称之为纹理单元。默认激活的纹理单元是 0,因此,我之前没有执行任何位置值分配,纹理贴图会自动绑定到默认纹理单元上。当然,我们也可以通过 gl.uniform 来给片段着色器设置多个纹理,只需要激活对应的纹理单元。通用设备支持 8 个纹理单元,现代中高端设备支持会更多,这个只能具体机型具体分析,一般限制在 8 个即可,它们的编号分别是 gl.TEXTURE0 - 8。通过这种编号方式,我们在循环纹理单元的时候会很方便,不过这个都是后话了。

 

接下来,我们尝试多加一个纹理。在原有代码上进行如下改造:
function main() {
    // 新增加一张纹理贴图
    const images = ["http://192.168.55.63:8080/logo.png", "http://192.168.55.63:8080/close-icon.png"];
    const dataList = [];
    let index = 0;
    for (let i = 0; i < 2; i++) {
        const image = new Image();
        image.src = images;
        dataList.push(image);
        image.onload = function () {
            index++;
            if (index >= images.length) {
                render(dataList);
            }
        };
    }
}

function render(dataList) {
    // ...
    // 重新定义顶点位置
    const ratio = 0.5;
    const positions = [
        -ratio, -1,
        -ratio, 1,
        ratio, -1,
        ratio, 1
    ];
    // ...
   
    // 修改片元着色器文本
    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;
    }`;
   
    // ...
   
    // 判断有纹理才设置翻转
    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);
    }
}

 

原图和渲染后的图片对比:

 

 

到这里为止,相信大家应该了解了纹理映射是怎么回事,接下来,我们再来看几个使用案例。

 

更多案例

 

这里展示的几个案例,还是按照一个纹理呈现。

 

不同顶点色应用到纹理

 

如果你跟着学到了这里,应该可以轻松实现了吧!
// 此处展示出部分应用代码

const colors = [
    255, 0, 0, 255,
    0, 255, 0, 255,
   0, 0, 255, 255,
    255, 127, 0, 255
];

const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_image;
void main() {
    vec4 tex1 = texture2D(u_image, v_uv);
    gl_FragColor = tex1 * v_color;
}`;

 

 

再增加一点细节,就有一种镭射卡的感觉了。

 

改变最终输出的 RGB 顺序

 

const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_image;
void main() {
    vec4 tex1 = texture2D(u_image, v_uv).bgra;
    gl_FragColor = tex1;
}`;

 

 

这个原理其实也就是将原来通道的颜色替换成另外一种颜色。

 

网上有很多纹理的应用实例,大家都可以尝试着去改造一下。

 

其他

 

为什么在 GLSL 中变量的前缀都是 a_, u_ 或 v_ ?

 

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

 

本地服务器搭建

 

由于本次教程的 WebGL 测试内容我都放在自定义文件夹里。因此,需要一个服务器去运行 HTML 文件。文件夹内容如下:

 

来源:

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Cocos ShaderCocos引擎中用于实现自定义渲染效果的功能。它基于OpenGL ES 2.0或OpenGL ES 3.0,并且可以在2D和3D场景中使用。 使用Cocos Shader,您可以创建各种独特的效果,如阴影、扭曲、发光等等。您可以通过编写GLSL(OpenGL Shading Language)代码来定义着色器程序,然后将其与Cocos引擎中的节点进行关联。 要使用Cocos Shader,首先需要创建一个自定义的Shader文件,然后在Cocos引擎中加载并应用它。您可以使用Cocos Creator编辑器或手动编写代码来实现这一点。 以下是一个简单的示例代码,展示了如何在Cocos Creator中使用Cocos Shader: ```javascript // 创建一个Sprite节点 var spriteNode = new cc.Node(); var sprite = spriteNode.addComponent(cc.Sprite); sprite.spriteFrame = new cc.SpriteFrame("path/to/your/image.png"); // 加载并应用Shader cc.loader.loadRes("path/to/your/shader", cc.RawAsset, function (err, shaderCode) { if (err) { cc.error(err.message || err); return; } // 创建自定义材质 var material = new cc.Material(); material.effectAsset = shaderCode; material.name = 'CustomShader'; // 将材质应用到Sprite节点上 sprite.setMaterial(0, material); }); ``` 在上述示例中,您需要将路径 "path/to/your/image.png" 替换为您的图像文件路径,将路径 "path/to/your/shader" 替换为您的Shader文件路径。 请注意,使用Cocos Shader需要对OpenGL ES和GLSL有一定的了解。如果您不熟悉这些概念,建议先学习相关知识。同时,Cocos官方提供了丰富的文档和示例代码,可供参考和学习。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值