上一篇结尾,我们说过要进入3D的世界。但实际上我们还有一件事没有说这就是纹理贴图。某百科对纹理贴图的定义:
纹理贴图,又称 材质贴图,在计算机图形学中是把存储在内存里的位图包裹到3D渲染物体的表面。纹理贴图给物体提供了丰富的细节,用简单的方式模拟出了复杂的外观。
例如我们给一个物体贴上一个砖块的纹理他就看起来像一面墙:
添加纹理
需要注意的是无论物体是三维中如何复杂的物体,它都是由一个个几何平面组成的。
我们的问题贴图也是贴在这个几何平面之上。一张贴图的那个部分贴到空间中的哪一个平面上,这就涉及到纹理坐标和空间坐标的一个映射。
特别的纹理坐标系分别用U与V表示:
本章我们不会实现这么复杂的效果,目前我们能实现在一个正方形上贴图即可:
修改作色器
所以我们要在顶点作色器中再添加两个变量uvCoordinate与fragColor,uvCoordinate用来存放uv坐标,fragColor用来把uv坐标传递给片段作色器:
// 顶点作色器代码
attribute vec3 vertPosition;
attribute vec2 uvCoordinate;
varying vec2 fragColor;
void main() {
fragColor = uvCoordinate;
fragColor.y = 1.0 - fragColor.y;
gl_Position = vec4(vertPosition,1.0);
}
这里需要注意的是由于uv坐标是左下角是(0,0)点与图片的坐标是相反的所以在作色器中才有fragColor.y = 1.0 - fragColor.y;的语法
片段作色器中的fragColor就是由顶点作色器传递过来的,这里我们还声明了一个全局变量sampler用来存放纹理,webgl的WebGLRenderingContext.texImage2D()
方法指定了二维纹理图像。详情可以访问mdn文档查看。
// 片段作色器
precision mediump float;
varying vec2 fragColor;
uniform sampler2D sampler;
void main() {
gl_FragColor = texture2D(sampler, fragColor);
}
现在作色器准备完毕了,我们继续
创建buffer
我们需要在一个正方形上贴上一张贴图。所以我们需要空间定义两个三角形组成一个正方形。让后在告诉webgl每一个顶点所对应的uv坐标。根据前篇所述的方法我们创建这个buffer:
var positionData = [
-0.5, 0.5, .5,
0.5, 0.5, .5,
-0.5, -0.5, 0.75,
-0.5, -0.5, 0.75,
0.5, 0.5, .5,
0.5, -0.5, 0.75,
];
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positionData), gl.STATIC_DRAW);
var vertPosition = gl.getAttribLocation(program, 'vertPosition');
gl.vertexAttribPointer(
vertPosition,
3,
gl.FLOAT,
gl.FALSE,
0,
0
);
gl.enableVertexAttribArray(vertPosition);
var uvData = [
0.0, 1.0,
1.0, 1.0,
0.0, 0.0,
0.0, 0.0,
1.0, 1.0,
1.0, 0.0
];
var uvCoordinate = gl.getAttribLocation(program, 'uvCoordinate');
var uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvData), gl.STATIC_DRAW);
gl.vertexAttribPointer(
uvCoordinate,
2,
gl.FLOAT,
gl.FALSE,
0,
0
);
gl.enableVertexAttribArray(uvCoordinate);
只要定义了每个顶点的属性如uv,法线,颜色,那么对于顶点围城的图形中的任意一点。片段作色器都能通过线性插值的方法求出对应的uv,法线,颜色,实现平滑过度。
绑定纹理
var textureBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); //纹理坐标水平填充 s gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 纹理坐标垂直填充 t gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 纹理缩小滤波器 gl.LINEAR, gl.NEAREST, gl.NEAREST_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR (默认值), gl.LINEAR_MIPMAP_LINEAR.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 纹理放大滤波器 gl.LINEAR (默认值), gl.NEAREST.
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,
gl.UNSIGNED_BYTE, document.getElementById('crate-image')
);
与attribute不同,纹理buffer需要使用gl.createTexture方法创建。由于图片加载是操作异步的,我们将图片放在了一个img标签内,并在window.onload 中执行。
....
<body>
<img id="crate-image" src="./texture.jpg" width="0" height="0" />
<canvas id="canvas"></canvas>
<script>
window.onload = function () {
// ......
}
</script>
当然也可以用var imgNode = new Image()创建图片,并在imgNode.onload方法中执行buffer的绑定。
var imgNode = new Image();
imgNode.src = "example://site.com";
imgNode.onloade = function(){
// .....
}
这样最终得到如图结果:
开始你的表演
当然贴图的功能不仅如此,比如你可以切换通道来使贴图变色:
<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;
varying vec2 fragColor;
uniform sampler2D sampler;
void main() {
gl_FragColor = texture2D(sampler, fragColor).grba;
}
</script>
上面的片段作色器的红通道与绿通道互换了。
我们还能对图片进行更复杂的操作。
模糊
举个例子当我们要对一张图片进行模糊是,我们采用如下模糊算法,讲每个像素点表示为周围5个像素点的平均值。此时我们需要知道一个像素点有多大,我们可以在片段作色器中声明一个变量vec2 u_textureSize来表示图片的像素,那么它对应到uv坐标上的大小就是vec2(1.0)/u_textureSize。
这是GLSL的简化写法相当于 vec2(1.0/u_textureSize.x,1.0/u_textureSize.y);
在js代码中我们取出u_textureSize,并对其进行赋值:
var imgNode = document.getElementById('crate-image');
var u_textureSize = gl.getUniformLocation(program, "u_textureSize");
gl.uniform2fv(u_textureSize, new Float32Array([imgNode.width, imgNode.height]));
gl.uniform2fv的api可以参考mdn文档
// a_ 代表属性,值从缓冲中提供;
// u_ 代表全局变量,直接对着色器设置;
// v_ 代表可变量,是从顶点着色器的顶点中插值来出来的
卷积内核
现在任意一个像素显示的值,是由原始像素自己和周围的八个像素点决定了。那么每个像素贡献是多少?我们把他们存在一个矩阵之中这就是卷积内核,如上模糊操作,我们使用的矩阵就是
var edgeDetectKernel = [
1, 1, 1,
1, 1, 1,
1, 1, 1
];
将卷积内核传入作色器后
var u_kernel = gl.getUniformLocation(program, "u_kernel");
gl.uniform1fv(u_kernel,edgeDetectKernel);
修改作色器:
uniform float u_kernel[9];
void main() {
gl_FragColor = (
texture2D(sampler, fragColor + onePixel*vec2(-1.0,-1.0))*u_kernel[0] +
texture2D(sampler, fragColor + onePixel*vec2(.0,-1.0))*u_kernel[1] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,-1.0))*u_kernel[2] +
texture2D(sampler, fragColor + onePixel*vec2(-1.0,.0))*u_kernel[3] +
texture2D(sampler, fragColor + onePixel*vec2(.0,.0))*u_kernel[4] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,.0))*u_kernel[5] +
texture2D(sampler, fragColor + onePixel*vec2(-1.0,1.0))*u_kernel[6] +
texture2D(sampler, fragColor + onePixel*vec2(.0,1.0))*u_kernel[7] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,1.0))*u_kernel[8]);
}
但这样会造成颜色越来越亮,所以我们还需要定义u_kernelWeight
function computeKernelWeight(kernel) {
var weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}
var u_kernel = gl.getUniformLocation(program, "u_kernel");
var u_kernelWeight = gl.getUniformLocation(program, "u_kernelWeight");
gl.uniform1fv(u_kernel,edgeDetectKernel);
gl.uniform1f(u_kernelWeight,computeKernelWeight(edgeDetectKernel));
在作色器中除以u_kernelWeight:
vec4 colorSum =
texture2D(sampler, fragColor + onePixel*vec2(-1.0,-1.0))*u_kernel[0] +
texture2D(sampler, fragColor + onePixel*vec2(.0,-1.0))*u_kernel[1] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,-1.0))*u_kernel[2] +
texture2D(sampler, fragColor + onePixel*vec2(-1.0,.0))*u_kernel[3] +
texture2D(sampler, fragColor + onePixel*vec2(.0,.0))*u_kernel[4] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,.0))*u_kernel[5] +
texture2D(sampler, fragColor + onePixel*vec2(-1.0,1.0))*u_kernel[6] +
texture2D(sampler, fragColor + onePixel*vec2(.0,1.0))*u_kernel[7] +
texture2D(sampler, fragColor + onePixel*vec2(1.0,1.0))*u_kernel[8];
gl_FragColor = vec4((colorSum/u_kernelWeight).rgb,1.0);
当然使用不同的卷积内核我们可以得到人脑无法想象的效果,比如浮雕效果的卷积内核:
var edgeDetectKernel = [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
];
比如边界检测效果的卷积内核:
var edgeDetectKernel = [
0, 1, 0,
1, -4, 1,
0, 1, 0
];
下期预告
下期我们真的要进入3d世界了,讨论下3d世界中的三个变换。