WebGL是一种基于OpenGL的浏览器内置3D渲染器,它可以让你在HTML5页面中直接显示3维内容。 在本教程中,我会介绍你使用此框架所需的所有基础内容。
介绍
开始学习之前,有几件事你是需要了解的。 WebGL是将3D内容渲染到HTML5的canvas元素上的一种JavaScript API。 它是利用"3D世界"中称为着色器的两种脚本来实现这一点的。 这两种着色器分别是:
- 顶点着色器
- 片元着色器
听到这些名词时也不要过于惊慌;它们只不过是"位置计算器"和"颜色选择器"的另一种说法罢了。 片元着色器容易理解;它只是告诉WebGL,模型上的指点定应该是什么颜色。 而顶点着色器解释起来就需要点技术了,不过基本上它起到将3维模型转变为2维坐标的作用。 因为所有的计算机显示器都是2维平面,当你在屏幕上看3维物体时,它们只不过是透视后的幻象。
如果你想完整地理解这个计算过程,你最好是问一个数学家,因为这个过程中用到了高级的4x4矩阵乘法,实在是有点超过我们这个"基础"教程的范围呀。 幸运的是,你并不需要知道它所有的工作原理,因为WebGL会处理背后大部分的细节。 那么,我们开始吧。
第一步:设置WebGL
WebGL有许多细微的设置,而且每次你要在屏幕画什么东西之前都要设置一遍。 为了节约时间,并使代码整洁一些,我们把所有"幕后"的代码包装成一个JavaScript对象,并存于一个独立的文件中。 现在我们要开始了,先创建一个新文件'WebGL.js',并写入如下代码:
function WebGL(CID, FSID, VSID){ var canvas = document.getElementById(CID); if(!canvas.getContext("webgl") && !canvas.getContext("experimental-webgl")) alert("Your Browser Doesn't Support WebGL"); else { this.GL = (canvas.getContext("webgl")) ? canvas.getContext("webgl") : canvas.getContext("experimental-webgl"); this.GL.clearColor(1.0, 1.0, 1.0, 1.0); // this is the color this.GL.enable(this.GL.DEPTH_TEST); //Enable Depth Testing this.GL.depthFunc(this.GL.LEQUAL); //Set Perspective View this.AspectRatio = canvas.width / canvas.height; //Load Shaders Here } }
这个构造函数的参数是canvas无形的ID,以及两个着色器对象。 首先,我们要获得canvas元素,并确保它是支持WebGL的。 如果支持WebGL,我们就将WebGL上下文赋值给一个局部变量,称为"GL"。 清除颜色(clearColor)其实就是设置背景颜色,值得一提的是,WebGL中大部分参数的取值范围都是0.0到1.0,所以我们需要让通常的rgb值除以255。 所以,我们的示例中,1.0,1.0,1.0,1.0表示背景为白色,且100%可见 (即无透明)。 接下来两行要求WebGL计算深度和透视,这样离你近的对象会挡住离你远的对象。 最后,我们设置宽高比,即canvas的宽度除以它的高度。
继续前行之前,我们要准备好两个着色器。 我把这些着色器写到HTML文件里去,这个HTML文件里还包含了我们的画布元素 (canvas)。 创建一个HTML文件,并将下面的两个script元素放在body标签之前。
<script id="VertexShader" type="x-shader/x-vertex"> attribute highp vec3 VertexPosition; attribute highp vec2 TextureCoord; uniform highp mat4 TransformationMatrix; uniform highp mat4 PerspectiveMatrix; varying highp vec2 vTextureCoord; void main(void) { gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0); vTextureCoord = TextureCoord; } </script> <script id="FragmentShader" type="x-shader/x-fragment"> varying highp vec2 vTextureCoord; uniform sampler2D uSampler; void main(void) { highp vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); gl_FragColor = texelColor; } </script>
先来看顶点着色器,我们定义了两个属性 (attributes):
- 顶点位置,它存储了当前顶点 (你的模型上的点) 的位置,包括x,y,z坐标。
- 纹理坐标,即赋给这个点的纹理在纹理图像中的位置
接下来,我们创建变换和透视矩阵等变量。 它们被用于将3D模型转化为2D图像。 下一行是创建一个与片元着色器共享的变量vTextureCoord,在主函数中,我们计算gl_Position (即最终的2D位置)。 然后,我们将'当前纹理坐标'赋给这个共享变量vTextureCoord。
在片元着色器中,我们取出定义在顶点着色器中的这个坐标,然后用这个坐标来对纹理进行'采样'。 基本上,通过这个过程,我们得到了我们几何体上的当前点处的纹理的颜色。
现在写完了着色器,我们可回过头去在JS文件中加载这些着色器。 将"//Load Shaders Here"换成如下代码:
var FShader = document.getElementById(FSID); var VShader = document.getElementById(VSID); if(!FShader || !VShader) alert("Error, Could Not Find Shaders"); else { //Load and Compile Fragment Shader var Code = LoadShader(FShader); FShader = this.GL.createShader(this.GL.FRAGMENT_SHADER); this.GL.shaderSource(FShader, Code); this.GL.compileShader(FShader); //Load and Compile Vertex Shader Code = LoadShader(VShader); VShader = this.GL.createShader(this.GL.VERTEX_SHADER); this.GL.shaderSource(VShader, Code); this.GL.compileShader(VShader); //Create The Shader Program this.ShaderProgram = this.GL.createProgram(); this.GL.attachShader(this.ShaderProgram, FShader); this.GL.attachShader(this.ShaderProgram, VShader); this.GL.linkProgram(this.ShaderProgram); this.GL.useProgram(this.ShaderProgram); //Link Vertex Position Attribute from Shader this.VertexPosition = this.GL.getAttribLocation(this.ShaderProgram, "VertexPosition"); this.GL.enableVertexAttribArray(this.VertexPosition); //Link Texture Coordinate Attribute from Shader this.VertexTexture = this.GL.getAttribLocation(this.ShaderProgram, "TextureCoord"); this.GL.enableVertexAttribArray(this.VertexTexture); }
你的纹理必须是偶数字节大小,否则会出错。。。比如2x2,4x4,16x16,32x32。。。
首先,我们要确保这些着色器是存在的,然后,我们逐一地加载它们。 这个过程基本上是:得到着色器源码,编译,附着到核心的着色程序上。 从HTML文件中提取着色器源码的代码,封装到了一个函数中,称为LoadShader;稍后会讲到。 我们使用这个'着色器程序'将两个着色器链接起来,通过它,我们可以访问到着色器中的变量。 我们将数据储存到定义在着色器中的属性;然后,我们就可以将几何体输入到着色器中了。
现在,让我们看一下LoadShader函数,你应该将它置于WebGL函数之外。
function LoadShader(Script){ var Code = ""; var CurrentChild = Script.firstChild; while(CurrentChild) { if(CurrentChild.nodeType == CurrentChild.TEXT_NODE) Code += CurrentChild.textContent; CurrentChild = CurrentChild.nextSibling; } return Code; }
基本上,这个函数通过遍历着色器来收集源码。
第二步:“简单”立方体
为了在WebGL中画出对象,你需要如下三个数组:
- 顶点 (vertices):构造你的对象的那些点
- 三角形 (triangles):告诉WebGL如何将顶点连接成面
- 纹理坐标 (texture coordinates):定义顶点如何被映射到纹理图像上
这个过程称为UV映射。 我们的例子是构造一个简单的立方体。 我将这个立方体分成4个顶点一组,每一组又连成两个三角形。 我们可以用一个变量来存储立方体的这些数组。
var Cube = { Vertices : [ // X, Y, Z Coordinates //Front 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, //Back 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, //Right 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, //Left -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, //Top 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, //Bottom 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0 ], Triangles : [ // Also in groups of threes to define the three points of each triangle //The numbers here are the index numbers in the vertex array //Front 0, 1, 2, 1, 2, 3, //Back 4, 5, 6, 5, 6, 7, //Right 8, 9, 10, 9, 10, 11, //Left 12, 13, 14, 13, 14, 15, //Top 16, 17, 18, 17, 18, 19, //Bottom 20, 21, 22, 21, 22, 23 ], Texture : [ //This array is in groups of two, the x and y coordinates (a.k.a U,V) in the texture //The numbers go from 0.0 to 1.0, One pair for each vertex //Front 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, //Back 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, //Right 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, //Left 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, //Top 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, //Bottom 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0 ] };
这样一个简单的立方体用到的数据似乎有点过多,不过,在我们教程的第二部分中,我们写一个导入3D模型的脚本,所以你现在不必计较这些。
你可能还在想,为什么需要24个顶点 (每一面4个) 呢,实际上只有8个呀? 我这样做是因为,你可以只用为每个顶点指定一个纹理坐标;而如果你用8个顶点,则整个立方体将看起来一样,因为它会将一个纹理值传播到顶点接触的所有面上。 通过我们的方式,每个面都有它独有的点,所以我们可以在每一面上指定不同的纹理区域。
现在,我们有了这样一个立方体变量 cube,然后,我们可以准备画它了。 我们还是回到WebGL方法中,并添加一个Draw函数。
第三步:Draw函数
WebGL中绘制对象的过程有许多步骤;所以最好是将每个步骤写成函数,来简化这个过程的代码。 基本的想法是将三个数组加载到WebGL的缓存中去。 然后,我们将这些缓存连接到着色器中定义的属性,以及变换和透视矩阵。 接下来,我们需要将纹理加载到内存中,并且最后调用draw命令。 那么,我们开始吧。
接下来的代码进入到WebGL函数中:
this.Draw = function(Object, Texture) { var VertexBuffer = this.GL.createBuffer(); //Create a New Buffer //Bind it as The Current Buffer this.GL.bindBuffer(this.GL.ARRAY_BUFFER, VertexBuffer); // Fill it With the Data this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Vertices), this.GL.STATIC_DRAW); //Connect Buffer To Shader's attribute this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0); //Repeat For The next Two var TextureBuffer = this.GL.createBuffer(); this.GL.bindBuffer(this.GL.ARRAY_BUFFER, TextureBuffer); this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Texture), this.GL.STATIC_DRAW); this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0); var TriangleBuffer = this.GL.createBuffer(); this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, TriangleBuffer); //Generate The Perspective Matrix var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 10000.0); var TransformMatrix = MakeTransform(Object); //Set slot 0 as the active Texture this.GL.activeTexture(this.GL.TEXTURE0); //Load in the Texture To Memory this.GL.bindTexture(this.GL.TEXTURE_2D, Texture); //Update The Texture Sampler in the fragment shader to use slot 0 this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0); //Set The Perspective and Transformation Matrices var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix)); var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix)); //Draw The Triangles this.GL.drawElements(this.GL.TRIANGLES, Object.Trinagles.length, this.GL.UNSIGNED_SHORT, 0); };
顶点着色器对你的对象进行放置,旋转和缩放时,依据的都是变换和透视矩阵。 在本教程第二部分中,我们会更深入地介绍变换。
我已经添加了两个函数:MakePerspective()和MakeTransform()。 它们只不过生成了WebGL所需的4x4矩阵。 MakePerspective()函数接受几个参数:视场竖直高度,宽高比,最近和最远点。 任何比1个单位近或比10000个单位远的对象都不会被显示,但是你可以调整这些值,以得到你所期望的效果。 现在,让我们看一看这两个函数:
function MakePerspective(FOV, AspectRatio, Closest, Farest){ var YLimit = Closest * Math.tan(FOV * Math.PI / 360); var A = -( Farest + Closest ) / ( Farest - Closest ); var B = -2 * Farest * Closest / ( Farest - Closest ); var C = (2 * Closest) / ( (YLimit * AspectRatio) * 2 ); var D = (2 * Closest) / ( YLimit * 2 ); return [ C, 0, 0, 0, 0, D, 0, 0, 0, 0, A, -1, 0, 0, B, 0 ]; } function MakeTransform(Object){ return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -6, 1 ]; }
这些矩阵都会影响到你的对象的最终视觉效果,但透视矩阵影响的是你的“3维世界”,比如视场和可见对象,而变换矩阵影响的是单个对象,比如它们的旋转和位置。 完成这些之后,我们几何可以开始画了,剩下的工作只是将一个图像转变为一个WebGL纹理。
第四步:加载纹理
加载一个纹理分两步。 首先,我们要用JavaScript的标准做法来加载一幅图像,然后,我们将其转化为一个WebGL纹理。 所以,我们先从第二步开始吧,毕竟我们正在讨论的是JS文件。 将下面的代码加到WebGL函数的底部,恰好在Draw命令之后。
this.LoadTexture = function(Img){ //Create a new Texture and Assign it as the active one var TempTex = this.GL.createTexture(); this.GL.bindTexture(this.GL.TEXTURE_2D, TempTex); //Flip Positive Y (Optional) this.GL.pixelStorei(this.GL.UNPACK_FLIP_Y_WEBGL, true); //Load in The Image this.GL.texImage2D(this.GL.TEXTURE_2D, 0, this.GL.RGBA, this.GL.RGBA, this.GL.UNSIGNED_BYTE, Img); //Setup Scaling properties this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MAG_FILTER, this.GL.LINEAR); this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MIN_FILTER, this.GL.LINEAR_MIPMAP_NEAREST); this.GL.generateMipmap(this.GL.TEXTURE_2D); //Unbind the texture and return it. this.GL.bindTexture(this.GL.TEXTURE_2D, null); return TempTex; };
值得一提的是,你的纹理大小必须是偶数字节,否则你会得到错误信息;比如它们可能的维度包括:2x2,4x4,16x16,32x32,等等。 我另加了一行来翻转Y坐标,只是因为我的3D应用的Y坐标是朝后的,但是否这样做完全取决于你。 这是因为一些程序取Y的零点为左上角,而其它则为左下角。 我设置的这些缩放性质只是告诉WebGL,图像应该如何向上采样和向下采样。 你可以使用其它的选项来得到不同的效果,不过我认为这个组合效果最佳。
现在,我们完成了JS文件,我们可以回到HTML文件,来完成最后一步了。
第五步:合起来
如前所述,WebGL是在canvas元素上画画。 因此,在body部分里,我们所需要的就只是一个canvas画布。 在添加canvas元素之后,你的html页面看起来像下面这样:
<html> <head> <!-- Include Our WebGL JS file --> <script src="WebGL.js" type="text/javascript"></script> <script> </script> </head> <body onload="Ready()"> <canvas id="GLCanvas" width="720" height="480"> Your Browser Doesn't Support HTML5's Canvas. </canvas> <!-- Your Vertex Shader --> <!-- Your Fragment Shader --> </body> </html>
这个页面相当简单。 在head区域,我链接了JS文件。 现在,让我们实现Ready函数,它在页面加载时调用。
//This will hold our WebGL variable var GL; //Our finished texture var Texture; //This will hold the textures image var TextureImage; function Ready(){ GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader"); TextureImage = new Image(); TextureImage.onload = function(){ Texture = GL.LoadTexture(TextureImage); GL.Draw(Cube, Texture); }; TextureImage.src = "Texture.png"; }
所以,我们创建一个新的WebGL对象,并将canvas和着色器的ID传递进去。 接下来,我们加载纹理图像。 一旦加载完成,我们对立方体Cube和纹理Texture调用Draw()方法。 如果你一路跟下来,你的屏幕上应该有一个覆盖有纹理的静止立方体。
虽然我说了下一次再讲变换,但我们不可能只丢给你一个静止矩形,这还不够三维。 让我们回过头去,再添加一个小小的旋转吧。 在HTML文件中,修改onload函数,使之如下面的代码:
TextureImage.onload = function(){ Texture = GL.LoadTexture(TextureImage); setInterval(Update, 33); };
这会使得每隔33毫秒调用一个称为Update()的函数,因而我们得到约30fps的帧率。 下面是这个更新函数:
function Update(){ GL.GL.clear(16384 | 256); GL.Draw(GL.Cube, Texture); }
这个函数相当简单;它只不过清除屏幕,然后绘制更新后的立方体。 现在,让我们进入JS文件,添加旋转代码。
第六步:添加一些旋转
我们不会完全实现变换的代码,因为我说了要等到下次现说,这次我们只是加一个绕Y轴的旋转。 要做的第一件事就是在Cube对象中加一个Rotation变量。 它会跟踪当前的角度,并让我们可以递增地保持旋转。 所以你的Cube变量的顶部代码应该如下面这样:
var Cube = { Rotation : 0, //The Other Three Arrays };
现在,让我们修改MakeTransform()函数,添加旋转功能:
function MakeTransform(Object){ var y = Object.Rotation * (Math.PI / 180.0); var A = Math.cos(y); var B = -1 * Math.sin(y); var C = Math.sin(y); var D = Math.cos(y); Object.Rotation += .3; return [ A, 0, B, 0, 0, 1, 0, 0, C, 0, D, 0, 0, 0, -6, 1 ]; }
更多精彩内容,请微信关注“前端达人”公众号!
原文链接:https://code.tutsplus.com/zh-hans/articles/webgl-essentials-part-i--net-25856
原文作者:Gabriel Manricks
本文暂时没有评论,来添加一个吧(●'◡'●)