WebGL编程指南笔记三 第五章 颜色和纹理

参考
【《WebGL编程指南》读书笔记-颜色与纹理】

一、创建多个缓冲区将非坐标数据传入顶点着色器

为了探讨这一功能,本节示例程序为MultiAttributeSize,分别绘制了三个不同尺寸(分别是10.0,20.0,30.0)的点,效果如下:


image.png

MultiAttributeSize.js的完整代码如下:

// MultiAttributeSize.js
// 顶点着色器程序
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute float a_PointSize;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = a_PointSize;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main(){\n' + ' gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n' + '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点信息
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}

function initVertexBuffers(gl) {
  // 数据准备
  let vertices = new Float32Array([
    0.0,
    0.5,
    -0.5,
    -0.5,
    0.5,
    -0.5, // 点的位置
  ])
  let n = 3 // 点的数量
  let sizes = new Float32Array([
    10.0,
    20.0,
    30.0, // 点的尺寸
  ])
  // 创建缓冲区
  let vertexBuffer = gl.createBuffer()
  let sizeBuffer = gl.createBuffer()
  if (!vertexBuffer || !sizeBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }

  // 将顶点坐标写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) // 绑定缓冲区
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) // 向缓冲区存入数据
  // 将缓冲区分配给attribute变量
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_Position)

  // 将顶点尺寸写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW)
  let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize')
    return -1
  }
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(a_PointSize)

  return n
}

代码中都是以往应用的功能,此处我们在原绘制多个点的示例基础上,做了如下改变:

新增了attribute变量a_PointSize,该变量为Float类型,负责接收JavaScript程序传入的顶点尺寸数据并被赋值给gl_PointSize变量,
在initVertexBuffers()函数中也增加了a_PointSize相关内容。
下图展示了示例程序刚运行完毕未清空颜色缓冲区时,WebGL系统内部的状态:


image.png

小结:多个缓冲区的操作只是将一个缓冲区的操作再复制一遍,没有新的函数和思路需要掌握,但其中揭示了WebGL处理缓冲区对象的一种方式,这种方式在初次使用缓冲区对象的时候有所介绍,此处表现得更加具体:WebGL系统每次只能处理一个缓冲区对象。书中没有进行说明,笔者补充于此:

  • 示例中除了创建缓冲区对象之外,其他包括绑定缓冲区、向缓冲区传输数据、将缓冲区分配给attribute变量,都是一个缓冲区一个缓冲区进行的;
  • gl.bufferData()是通过绑定的target识别缓冲区并向其传输数据;
  • gl.vertexAttribPointer()不需要缓冲区相关参数,只需要被分配的attribute即可。

以上一些信息,说明WebGL系统(最起码在已经了解的部分)只能够一个缓冲区对象一个缓冲区对象地逐个处理,主要以target来定位缓冲区对象的位置,target只会保留一个缓冲区对象的地址(指针),当位置分配给attribute变量之后,才可以通过attribute变量来处理缓冲区对象的内容。

二、一个缓冲区对象传输多类型信息——步进和偏移参数与交错组织

使用多个缓冲区对象向着色器传递多种数据,比较适合数据量不大的情况。当程序中的复杂三维图形具有成千上万个顶点时,维护所有的顶点数据是很困难的。实际上,3D建模工具会自动生成数据,所以没有必要手动输入数据,或者显式地检查数据的完整性。我们将在第10章讨论如何使用建模工具生成模型。

WebGL允许我们把顶点的坐标和尺寸数据打包到同一个缓冲区对象中(交错组织interleaving),并通过某种机制有差别地访问缓冲区对象中不同种类的数据,即使用gl.vertexAttribPointer()函数的第5个参数stride和第6个参数offset。

下面的例子中,会把顶点坐标和顶点尺寸打包在一起:

// MultiAttributeSize_Interleaved.js
// 顶点着色器程序
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute float a_PointSize;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = a_PointSize;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main(){\n' + ' gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n' + '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点信息
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}

function initVertexBuffers(gl) {
  // 数据准备
  let verticesSizes = new Float32Array([
    // 顶点坐标和点的尺寸
    0.0,    0.5,    10.0, // 第一个点
    -0.5,    -0.5,    20.0, // 第二个点
    0.5,    -0.5,    30.0, // 第三个点
  ])
  let n = 3 // 点的数量
  // 创建缓冲区
  let vertexSizeBuffer = gl.createBuffer()
  if (!vertexSizeBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer)
  // 向缓冲区存入数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW)

  // 获取单个数据的大小
  let FSIZE = verticesSizes.BYTES_PER_ELEMENT

  // 获取a_Position的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  // 将缓冲区分配给a_Position
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_Position)

  // 获取a_PointSize的存储位置
  let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize')
    return -1
  }
  // 将缓冲区分配给a_PointSize
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_PointSize)

  return n
}

这段代码主要用到了gl.vertexAttribPointer()函数来对前一个示例进行改造。通过示例代码,我们多少知道了stride参数和offset参数的用法,如下图所示:(每个顶点有3个数据项,所以stride为FSIZE*3;offset表示需要的数据项距离首个元素的距离,a_Position为0,a_PointSize为FSIZE*2)

image.png

TypedArray.BYTES_PER_ELEMENT属性代表了强类型数组中每个元素所占的字节数。在关于某个顶点的三个值中,前两个是顶点坐标,后一个是顶点尺寸。因此顶点的offset是0,尺寸的offset是2。

image.png
三、修改颜色(初识varying变量)——为顶点单独设置颜色

本节示例为MultiAttributeColor,顾名思义,本节示例希望在缓冲区对象中同时填充顶点坐标和颜色数据,然后分配给attribute变量用以处理颜色,最终呈现结果如下:


image.png

在此之前,我们只静态设置或统一设置(uniform变量)过顶点的颜色,许多操作都放在了顶点着色器中,还没有真正操作过片元着色器。而作为每个顶点单独的属性,attribute变量又只支持在顶点着色器中使用,所以,我们需要知道顶点着色器和片元着色器是如何交流的,这样才能使顶点着色器的数据进入片元着色器(也能明白为什么attribute变量只支持在顶点着色器中使用)。此处需要使用到一个新的变量:varying变量(varying variable),从顶点着色器向片元着色器传输数据。

// MultiAttributeColor.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  ' v_Color = a_Color;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_FragColor = v_Color;\n' +
  '}\n'
// 主程序
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 获取顶点位置
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}
// 顶点位置相关函数
function initVertexBuffers(gl) {
  // 数据准备
  let verticesColors = new Float32Array([
    // 顶点坐标和颜色
    0.0, 0.5, 1.0, 0.0, 0.0, 
    -0.5, -0.5, 0.0, 1.0, 0.0, 
    0.5, -0.5, 0.0, 0.0, 1.0,
  ])
  let n = 3
  // 创建缓冲区对象
  let vertexColorBuffer = gl.createBuffer()
  if (!vertexColorBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 向缓冲区存储数据
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)

  let FSIZE = verticesColors.BYTES_PER_ELEMENT
  // 获取a_Position的存储位置,分配缓冲区并开启
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
  gl.enableVertexAttribArray(a_Position) // Enable buffer assignment
  // 获取a_Color的存储位置,分配缓冲区并开启
  let a_Color = gl.getAttribLocation(gl.program, 'a_Color')
  if (a_Color < 0) {
    console.log('Failed to get the storage location of a_Color')
    return -1
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
  gl.enableVertexAttribArray(a_Color) // 开启缓冲区分配

  return n
}

顶点着色器中声明了attribute变量a_Color用以接收颜色数据,同时在两个着色器中都声明了varying变量v_Color变量,该变量将值从顶点着色器传递到片元着色器,最终赋值给gl_FragColor(在WebGL中,如果顶点着色器与片元着色器中又类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值会自动传入片元着色器)。注意:varying变量只能是float(以及相关的vec2,vec3,vec4,mat2,mat3,mat4)类型。

image.png

这里来对比一下Unity Shader,参考UnityShader精要笔记一 基础示例

在实践中,我们往往希望从顶点着色器输出的一些数据(模型的法线、纹理坐标等)传递给片元着色器,这就涉及到顶点着色器和片元着色器之间的通信。因此,我们需要再定义一个新的结构体,来储存顶点着色器的输出数据,并作为片元着色器的输入参数。修改后的代码如下:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter5-SimpleShader"
{
    SubShader
    {
        Pass{
            CGPROGRAM

            //vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
            //哪个函数包含了片元着色器的代码
            #pragma vertex vert
            #pragma fragment frag

            //使用一个结构体来定义顶点着色器的输入
            struct a2v {
                //POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
                float4 vertex : POSITION;
                //NORMAL 语义告诉Unity,用模型空间的发现方向填充normal变量
                float3 normal : NORMAL;
                //TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
                float4 texcoord : TEXCOORD0;
            };

            //使用一个结构体来定义顶点着色器的输出
            struct v2f {
                //SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
                float4 pos : SV_POSITION;
                //COLOR0语义可以用于存储颜色信息
                fixed3 color : COLOR0;
            };

            //float4 vert(a2v v) : SV_POSITION{
                //将顶点坐标从模型空间转换到剪裁空间中
                //使用v.vertex来访问模型空间的顶点坐标
                //return UnityObjectToClipPos(v.vertex);
            //}

            //fixed4 frag():SV_Target{
                //fixed4 低精度RGBA颜色
                //片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色
                //return fixed4(1.0,1.0,1.0,1.0);
            //}

            v2f vert(a2v v):SV_POSITION {
                //声明输出结构
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                //v.normal 包含了顶点的法线方向,其分量范围在[-1.0,1.0]
                //下面的代码把分量范围映射到了[0.0,1.0]
                //并存储到o.color中传递给片元着色器
                o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target{
                //将插值后的i.color 显示在屏幕上
                return fixed4(i.color,1.0);
            }

            ENDCG
        }
    }
}

之前返回的是一个float4类型的变量,即顶点在裁剪空间中的位置。现在返回的是v2f,v2f必须包含一个变量,它的语义是SV_POSITION。否则渲染器就无法得到剪裁空间中的顶点坐标,也就无法把顶点渲染到屏幕上。

COLOR0语义中的数据由用户自行定义,但一般都是存储颜色,例如漫反射颜色或逐顶点的高光反射颜色。

至此,我们完成了顶点着色器和片元着色器之间的通信。需要注意的是,顶点着色器是逐顶点调用的,而片元着色器是逐片元调用的。片元着色器中的输入实际是把顶点着色器的输出进行插值后得到的结果。(注:作者的这段话,可以根据闫令琪的图形学入门知识来理解。)

四、彩色三角形

在上一节示例中,如果我们把gl.drawArrays()参数修改一下,就得到了ColoredTriangles.js,如下所示:

  // 绘制
  gl.drawArrays(gl.TRIANGLES, 0, n)

示例程序在三角形表面产生了颜色平滑过渡的效果:

image.png

原书用大量的篇幅来解释从顶点着色器到片元着色器经历的插值过程,可以参考UnityShader精要笔记四 渲染流水线

在ColoredTriangel.js中,我们指定了三个顶点的颜色,最后却得到了一个颜色渐变的三角形,这是因为varying变量在光栅化的过程中经过了内插操作,如下图所示:


image.png

image.png

内插之后,每个片元都根据三个顶点的v_Color变量得到了自己的v_Color变量,该变量和坐标信息(可能还有其他信息)一起保存在片元中。所以,片元着色器中的v_Color变量,和顶点着色器中的v_Color变量实际上并不是一回事,这就是varying变量为什么被称作“varying(变化的)”变量的原因。

五、在矩形表面贴上像素

理论知识可以参考图形学笔记七 纹理和贴图 AO

示例程序TexturedQuad.js:

// 顶点着色器
let VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'
// 片元着色器
let FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n' +
  '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点坐标
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)

  // 配置纹理信息
  if (!initTextures(gl, n)) {
    console.log('Failed to configure the texture')
    return
  }
}
// 顶点坐标
function initVertexBuffers(gl) {
  // 数据准备
  let verticesTexCoords = new Float32Array([
    // 顶点坐标,纹理坐标
    -0.5, 0.5, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, -0.5,
    1.0, 0.0,
  ])
  let n = 4
  // 创建缓冲区
  let vertexTexCoordBuffer = gl.createBuffer()
  if (!vertexTexCoordBuffer) {
    console.log('Failed to create vertexTexCoordBuffer')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer)
  // 缓冲区存储数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW)

  // 顶点坐标相关配置
  let FSIZE = verticesTexCoords.BYTES_PER_ELEMENT
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
  gl.enableVertexAttribArray(a_Position)

  // 纹理坐标相关配置
  let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord')
    return -1
  }
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
  gl.enableVertexAttribArray(a_TexCoord)

  // 一切顺利,返回顶点数量
  return n
}
// 初始化纹理
function initTextures(gl, n) {
  // 创建纹理对象
  let texture = gl.createTexture()
  if (!texture) {
    console.log('Failed to create texture')
    return
  }
  // 获取u_Sampler(纹理图像)的存储位置
  let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
  if (!u_Sampler) {
    console.log('Failed to get the storage loaction of u_Sampler')
    return
  }
  // 创建一个image对象
  let image = new Image()
  // 注册图像加载事件的响应函数
  image.onload = function () {
    loadTexture(gl, n, texture, u_Sampler, image)
  }
  // 浏览器加载图像
  image.src = '../image/sky.jpg'

  return true
}

function loadTexture(gl, n, texture, u_Sampler, image) {
  // 对纹理图像进行Y轴反转
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
  // 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)
  // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
}
image.png

示例代码越来越长,不过有很多是之前学习过的内容(包括顶点坐标赋值等),新加入的纹理相关操作流程可分为如下五个部分:

  • 顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器(GLSL ES)。
  • 片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元(GLSL ES)。
  • 设置顶点的纹理坐标(initVertexBuffers())
  • 准备待加载的纹理图像,令浏览器读取它(initTextures())。
  • 监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理(loadTexture())。

后三部分在JavaScript代码中操作放置在main()函数中随dom加载执行,前两部分书写在着色器语言中并在图像加载完成之后执行。我们就纹理相关流程进行分解。

1.设置顶点的纹理坐标
  let verticesTexCoords = new Float32Array([
    // 顶点坐标,纹理坐标
    -0.5, 0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 0.0, 
    0.5, 0.5, 1.0, 1.0, 
    0.5, -0.5, 1.0, 0.0,
  ])
image.png

然后,将顶点坐标和纹理坐标写入缓冲区对象

gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
2.配置和加载纹理
function loadTexture(gl, n, texture, u_Sampler, image) {
  // 对纹理图像进行Y轴反转(此时图像是空的)
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
  // 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)
  // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
}
3.图像Y轴旋转(gl.pixelStorei())

WebGL中纹理坐标系统中t轴方向和PNG、BMP、JPG等格式图片的坐标系统Y轴方向是相反的,所以要先将图像Y轴进行反转才能正确映射(这一操作也可以放在着色器中手动进行)。


image.png

gl.pixelStorei(pname, param):
使用pname和param指定的方式处理加载得到的图像。
参数:

  • pname: 可以是以下二者之一
    • gl.UNPACK_FLIP_Y_WEBGL 对图像进行Y轴反转,默认值为false
    • gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL 将图像RGB颜色值得每一个分量乘以A。默认值为false。
  • param 指定非0(true)或0(false)。必须为整数。
  • 返回值: 无
  • 错误:
    INVALID_ENUM: pname不是合法的值。
4.激活纹理单元(gl.activeTexture())
// 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)

WebGL通过一种称作纹理单元(texture unit)的机制同时使用多个纹理(单个纹理也遵循此规则),每个纹理单元有一个单元编号来管理一张纹理图像。

系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,但在默认情况下,WebGL至少支持8个纹理单元,一些其他的系统支持的个数更多。如下图所示,内置变量gl.TEXTURE0、gl.TEXTURE1······gl.TEXTURE7各表示一个纹理单元。


image.png

激活纹理单元后,WebGL系统内如下图所示:


image.png
5.绑定纹理对象(gl.bindTexture())
 // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

与缓冲区对象类似,纹理对象在使用前也需要绑定到target,以确定类型,同时之后的配置操作也以target为对象(指针?)进行操作。

gl.bindTexture(target, texture):
开启texture指定的纹理对象,并将其绑定到target(目标)上。此外,如果已经通过gl.activeTexture()激活了某个纹理单元,则纹理对象也会绑定到这个纹理单元上。
参数:

  • target: gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP

  • texture: 表示绑定的纹理单元。

  • 返回值: 无

  • 错误:
    INVALID_ENUM: target不是合法的值。

与缓冲区对象不同的是,此处完成了两个任务:开启纹理单元和绑定纹理对象。因为0号纹理单元已经激活,所以纹理对象也绑定到了纹理单元上。

在该方法执行完毕后,WebGL系统如下图所示:


image.png
6.配置纹理对象的参数(gl.texParameteri())
  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

配置纹理对象,就是设置纹理图像映射到图形上的具体方式:如何根据纹理坐标获取纹素颜色、按哪种方式重复填充纹理。

gl.texParameteri(target, pname, param):
将param的值赋给绑定到目标的纹理对象的panme参数上。

  • 参数:
    • target: gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP
    • pname: 纹理参数。
    • param: 纹理参数的值。
  • 返回值: 无
  • 错误:
    INVALID_ENUM: target不是合法的值。
    INVALID_OPERATION: 当前目标上没有绑定纹理对象

纹理参数及其默认值如下表所示:

纹理参数 描述 默认值
gl.TEXTURE_MAG_FILTER 纹理放大 gl.LINEAR
gl.TEXTURE_MIN_FILTER 纹理缩小 gl.NEARST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S 纹理水平填充 gl.REPEAT
gl.TEXTURE_WRAP_T 纹理垂直填充 gl.REPEAT

四种纹理参数的效果如下图所示:


image.png

示例程序进行到这一步后,WebGL系统如下图所示:


image.png
7.将纹理图像分配给纹理对象(gl.texImage2D())
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

这一函数的参数相对较多,其函数规范如下:

gl.texImage2D(target, level, internalformat, format, type, image):
将param的值赋给绑定到目标的纹理对象的panme参数上。
参数:

  • target: gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP
  • level: 传入0(是为金字塔纹理准备的,本书不涉及)。
  • internalformat: 图像的内部格式。
  • format: 纹理数据的格式,必须使用与internalformat相同的值。
  • type: 纹理数据的类型。
  • image: 包含纹理图像的Image对象。
  • 返回值: 无
  • 错误:
    INVALID_ENUM: target不是合法的值。
    INVALID_OPERATION: 当前目标上没有绑定纹理对象

internalformat和format分别表示图像的内部格式和纹素的格式,一般情况下二者取值相同,如下表:

格式 描述
gl.RGB 红、绿、蓝
gl.RGBA 红、绿、蓝、透明度
gl.ALPHA (0.0,0.0,0.0,透明度)
gl.LUMINANCE L、L、L、1L:流明
gl.LUMINANCE_ALPHA L、L、L,透明度

补充一:流明(luminance)表示我们感知到的物体表面的亮度。通常使用物体表面红、绿、蓝颜色分量值的加权平均来计算流明。
补充二:常见的JPG格式图片将每个像素用RGB三个分量来表示,属于gl.RGB格式;PNG格式图片属于gl.RGBA格式;BMP图片属于gl.RGB格式;gl.LUMINANCE和gl.LUMINANCE_ALPHA通常用于灰度图像。

type参数表示纹理数据的类型,即经过处理后,纹理将被保存为该类型的纹理数据。该参数取值如下:

格式 描述
gl.UNSIGNED_BYTE 无符号整型,每个颜色分量占据1字节
gl.UNSIGNED_SHORT_5_6_5 RGB:每个分量分别占据5、6、5比特
gl.UNSIGNED_SHORT_4_4_4_4 RGBA:每个分量分别占据4、4、4、4比特
gl.UNSIGNED_SHORT_5_5_5_1 RGBA:RGB每个分量各占据5比特,A分量占据1比特

补充一:通常我们选用gl.UNSIGNED_BYTE数据类型,其它数据类型通常用于压缩数据以减少浏览器加载图像的时间。

经过这一过程后,WebGL系统如下所示:


image.png
8.传输纹理单元给着色器(gl.uniform1i())

通过以上方式,我们已经在WebGL系统中配置好了纹理,现在需要将纹理传输给着色器。

在着色器中,我们已经定义了u_Sampler变量(采样器)来存储纹理。

'uniform sampler2D u_Sampler;\n'

在WebGL中需要定义变量的类型,既然用于存储纹理数据,那么该变量的数据类型就与纹理的target有关。示例中使用的采样器的数据类型为sampler2D,对应于绑定到gl.TEXTURE_2D上的纹理数据类型。专用于纹理的数据类型如下:

类型 描述
sampler2D 绑定到gl.TEXTURE_2D上的纹理数据类型
samplerCube 绑定到gl.TEXTURE_CUBE_MAP上的纹理数据类型

所以,我们需要将之前配置的纹理传输给该采样器。

传输纹理使用纹理编号进行定位,采样器使用之前获得的采样器地址进行定位:

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

该函数的作用是:分配正确的纹理单元给对应采样器,参数u_Sampler就是采样器地址,'0’表示0号纹理单元gl.TEXTURE0。

之后,片元着色器就可以访问纹理图像了:


image.png
9.着色器的行为
  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)

之后我们使用gl.drawArrays()方法调用着色器绘制图形。因为涉及异步请求,绘制函数也需要放在图像加载的响应函数中。

根据着色器语言,纹理相关的着色器行为如下:

从顶点着色器向片元着色器传输纹理坐标
在片元着色器中获取纹理像素颜色(texture2D())

10.从顶点着色器向片元着色器传输纹理坐标
// 顶点着色器
let VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'

此处我们通过attribute变量a_TexCoord接收顶点的纹理坐标,将其赋值给varying变量v_TexCoord。

光栅化过程中varying变量会进行插值,在生成的片元中,每一个都包含片元坐标和v_TexCoord信息。

11.从片元着色器中获取纹理像素颜色(texture2D())
' gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n'

该步骤使用GLSL ES内置函数texture2D()来抽取纹素颜色,该函数规范如下:

gl.texture2D(sampler2D sampler, vec2 coord):
从sampler指定的纹理上获取coord指定的纹理坐标处的像素颜色。
参数:

  • sampler: 指定纹理单元编号
  • coord: 指定纹理坐标
  • 返回值:
    纹理坐标处像素的颜色值,其格式由gl.texImage2D()的internalformat参数决定。如果由于某些原因导致纹理图像不可使用,就返回(0.0, 0.0, 0.0, 1.0)。

上述函数的返回值如下:

internalformat 返回值
gl.RGB (R, G, B, 1.0)
gl.RGBA (R, G, B, A)
gl.ALPHA (0.0, 0.0, 0.0, A)
gl.LUMINANCE (L, L, L, 1.0) L 指定亮度(indicates luminance)
gl.LUMINANCE_ALPHA (L, L, L, A)

纹理配置的相关参数决定WebGL系统如何插出片元。最后gl_FragColor变量接收了texture2D()的返回值,进行绘制。

12.扩展:非2的n次幂图像的纹理

非书中内容,可以参考
【《WebGL编程指南》读书笔记-颜色与纹理】

六、使用多幅纹理

在本章之前说过,WebGL可以同时处理多幅纹理,纹理单元就是为了这个目的而设计的。之前的示例程序都只用到了一幅纹理,也只用到了一个纹理单元。这一节的示例程序在矩形上重叠粘贴两幅纹理图像。

image.png

为了说明WebGL具有处理不同纹理图像格式的能力,本例故意使用了两种不同格式的图像。最关键的是,你需要对每幅纹理分别进行前一节所述的将纹理图像映射到图形表面的操作,以此来将多张纹理图像同时贴到图形上去。


image.png

具体代码和前一部分类似,参见原书。关键代码如下:

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler0;\n' +
  'uniform sampler2D u_Sampler1;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
  ' vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
  ' gl_FragColor = color0 * color1;\n' +
  '}\n'

片元着色器访问两个纹理,计算颜色。此处两个纹理对象采用一样的坐标,所以在顶点着色器中没有区别。

在片元着色器中,color0和color1分别从u_Sampler0和u_Sampler1中根据v_TexCoord提供的坐标提取颜色,最后计算颜色的方式为二者相乘。vec4格式数据乘法如下:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容