? 最近开始研究起WebGL来,发现以前在图形学课上看javascript还真是不太理智的做法。
? 这一系列学习笔记是自己学习过程的总结,难免有错和不正确,希望发现问题的同学可以“惨无人道”的指出。
? WebGL简单说就是OpenGL在浏览器端的实现。那OpenGL又是什么?OpenGL就是一组提供了生成2d、3d图形的API。
? 其实,要想用WebGL来真正“画”出一些东西,首先要对图形学的一些基本概念有理解。
? 简明图形学
? 图形学,指利用计算机来生成图形(creation)、绘制或者叫渲染图形(render)、处理图形(manipulation)的学科。
? (一)
? 首先是生成的问题。我对图形生成的理解,就是怎么样来描述各种需要进行绘制的图形,尤其是那些复杂的人物啊、建筑啊等等。
? 在图形学中,我们从描述最最基本的点(vertex)开始,描述一个点,图形学中的点和几何意义上的点稍有不同,除了基本的位置外,可能还会需要颜色、顶点发向量(用于计算光照)、纹理坐标(用于贴图)等额外的信息。正如图像是由像素构成,图形学中的图形其实可以理解成由点构成。比如:我们按照一个人的形状,在三维空间中定义出这个人的轮廓,就已经大致可以描述这个人了。那有了点,我们就可以把它们连起来,可是这么多的点该怎样连?图形学里会将点连成三角形(polygon)。然后这许多的三角形就可以组成模型的骨架,线框网格(wire mesh)。
? 比如下面这个有点像人的恶心模型,?我们可以看到组成它的点连接起来,形成了模型的网格(mesh):
?这就是模型的最最基本的描述,如果再加上一些材质、打上灯光什么的,它看起来会是这样:
? (二)
? 大致说了图形描述的问题,那接着是绘制。也就是怎么样把三维的东西绘制到二维的屏幕。
? 这里,我们想到,要是有一个图形渲染机,我们把我们对模型的描述,也就上面说的一堆点的信息,统统倒入这个机器,机器一阵处理后就可以在屏幕上绘制出模型,那就好了。确实也有这样一个机器,它的输入可以理解成一堆点(用数组之类的表示),输出就是我们屏幕上闪闪动人的模型。其实,这个机器可以理解成显卡。那它是怎么做到的?
? 首先是我们怎么样把点的信息告诉显卡,OpenGL(或者WebGL)这时终于是派上用场了,它封装了底层的调用,提供给了我们和设备无关的函数来进行这些操作;显卡拿到了点的信息,就必须进行处理,因为最后屏幕上显示的是像素信息呀,这些处理简单说只有2个部分:点处理+像素处理(又叫光栅化),下面详细叙述;生成了像素数据后,这些像素数据会保存到一块叫帧缓存的内存中,然后,这些像素数据就最终在屏幕上显示了。
? 上面我们说,处理过程分为点处理和像素处理。点处理,主要是根据输入的点的信息再进行一些必要的计算,最终产生每个点实际在屏幕上的显示位置;像素处理,则是真正计算每个像素应该显示的颜色。
? 接着,我们需要想想更详细的东西。
? 我们通过点来描述模型,那必然会需要一个坐标系,否则,点的位置、法向量该如何描述。这个参照的坐标系一般称为模型坐标系(或者object/local space)。那现在我们有了很多个模型,就需要有一个放置它们的世界,或者叫场景吧,这时也需要一个坐标系来定位模型的位置,这个坐标系就叫做世界坐标系(或者world space)。世界如此之大,我们或许不可能把整个世界都显示出来,那有点贪心了,这时我们需要一台摄像机,或者称它观察者吧,透过它的眼睛来看我们的世界,它一般会形成一个观察体,在这个观察体里的才显示,不在的就当作隐形了,这个坐标系就叫做观察坐标系(或者view space)。处在观察者注视下的世界其实还是一个3d的世界,可是我们需要显示在2d平面上啊,这时就需要一个很重要的变换过程了,叫做投影(projection),有正交投影和透视投影二种,投影变换后3d的世界就会被投影到一个2d的投影平面上,并且同时基本保持了在3d空间的性质,如:近大远小等等。最后,我们再进行一个视口(viewport)变换,将投影窗口变换为屏幕上的一个矩形区域(其实就是我们显示图形的窗口)。这些变换后,我们应该就会开始计算视口中每个像素该显示的颜色,然后绘制出来,整个绘制过程就完成了。这一系列操作,可以称为渲染管线。就好象linux的管道一样,这个管道输出的信息作为下一个管道输入的信息,不断进行下去。
? 这个过程,其实简单说来,就是将我们输入的点从一个坐标系变换到另一个坐标系,那这种变换,矩阵运算就是最拿手的了。比如:在world space会对模型进行一些平移、旋转、缩放操作,都可以定义成一个变换矩阵;在view space,要将世界坐标中的点变换为观察坐标中的点,也是定义成一个变换矩阵;投影变换、视口变换也都是变换矩阵。这些细节,OpenGL的API其实都有函数能够直接使用,但是WebGL又稍微有点特殊,需要我们深入到渲染管线。
? (三)
? 总结下,渲染管线的流程大致是这样:
? 点信息(vertices data) -> 世界坐标系中的变换,如:平移、缩放、选择(world space transformation) -> 世界坐标系转为观察坐标系,需要定义摄像机(view space transformation) -> 投影变换(projection transformation) -> 裁剪(clipping,摄像机外的就不显示),背面剔除(backface culling,背对摄像机的那个面不显示) ->?齐次裁剪空间 -> 视口变换(viewport transformation) -> 光栅化(rasterize,可能会包括:贴图生成、光照生成、场景雾生成等) -> 最终像素颜色 -> 帧缓存(frame buffer) -> 显示到屏幕
? 在这个管线中,大部分都是显卡在做工作,但是,我们却有机会直接对显卡编程,来操作这些数据,可以编程的2个部分分别叫做:vertex shader和fragment shader,其实就是分别对应对点的操作和对像素的操作。其中,vertex shader是对输入的每个点依次执行,生成该点的最终位置;fragment shader对每个像素操作,生成该像素的显示颜色;这2个shader之间也可以传递数据,不过只能是vertex shader传递给fragment shader,因为总是先执行vertex shader(比如:vertex shader内先根据点的法向量计算一些光照参数,然后传给fragment shader生成最终有光照考虑的像素颜色;vertex shader直接传递贴图坐标给fragment shader,fragment shader根据贴图坐标计算加上了贴图考虑的像素颜色等)。使用WebGL恶心的地方就是,就算只是显示一个三角形,都需要自己写shader。
? 对应渲染管线的话,插入这2个可编程部件后,大致应该是这样:
??点信息(vertices data) ->
? vertex shader {?世界坐标系中的变换,如:平移、缩放、选择(world space transformation) -> 世界坐标系转为观察坐标系,需要定义摄像机(view space transformation) -> 投影变换(projection transformation) -> 其他一些计算?}?->?
?裁剪(clipping,摄像机外的就不显示),背面剔除(backface culling,背对摄像机的那个面不显示) ->?齐次裁剪空间 -> 视口变换(viewport transformation) ->
?fragment shader {?贴图生成、光照生成、场景雾生成、其他一些计算?}?->
?光栅化(rasterize,可能会包括:贴图生成、光照生成、场景雾生成等) -> 最终像素颜色 ->
?帧缓存(frame buffer) -> 显示到屏幕
? WebGL绘制三角形
? 简单的总结完图形学的基本概念后,我们可以动手写程序了。就好象第一个程序都是Hello World,个人觉得图形学里的Hello World应该就是画一个三角形。
? 我们可以先来看看OpenGL写的话,或许会是这样的(引用自:http://fly.cc.fer.hr/~unreal/theredbook/chapter01.html):
#include <whateverYouNeed.h> main() { OpenAWindowPlease(); glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 1.0, 1.0); glOrtho(-2.0, 2.0, -2.0, 2.0, -1.0, 1.0); glBegin(GL_TRIANGLES); glVertex2f(0.0, 1.0); glVertex2f(-1.0, 1.0); glVertex2f(1.0, -1.0); glEnd(); glFlush(); KeepTheWindowOnTheScreenForAWhile(); }
? glClearColor/glClear那里是清屏,为绘制做准备;glOrtho那句就是定义一个正交投影的“摄像机”;glBegin/glEnd那里就是通过三个点定义了一个三角形;glFlush就是将帧缓存画到屏幕。挺简洁呀~但是,WebGL没有像glBegin/glEnd这种东西,也不会很好心的自己帮你把点根据你定义的摄像机进行合适的变换,我们需要做更多的工作。
? 以下代码,引用自MDN的文档(https://developer.mozilla.org/en/WebGL),文档的demo代码真是太乱了,然后做了适当的调整和修改。为了偷懒,我就对自己学习时觉得不好理解的部分进行一下记录,全部代码可以在这里获取:https://github.com/KohPoll/webgl-learn
? (一)关于shader及program的创建
function getShader(gl, id) { var shaderScript = document.getElementById(id), theSource = '', shader = null; if (!shaderScript) return shader; theSource = text(shaderScript); if (shaderScript.type === 'x-shader/x-fragment') { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (shaderScript.type === 'x-shader/x-vertex') { shader = gl.createShader(gl.VERTEX_SHADER); } else { return shader; } gl.shaderSource(shader, theSource); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return null; return shader; } function initShaders() { var fragmentShader, vertexShader; fragmentShader = getShader(gl, 'shader-fs'); vertexShader = getShader(gl, 'shader-vs'); shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, fragmentShader); gl.attachShader(shaderProgram, vertexShader); gl.linkProgram(shaderProgram); if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { gl.useProgram(shaderProgram); } }
? shader的创建步骤:1.创建一个shader(gl.createShader);2.获取shader的源代码(这里是从dom节点中获取)并进行设置(gl.shaderSource);3.编译shader(gl.compileShader)。
? 将shader“注入”到可编程组件program的步骤:1.创建一个program(gl.createProgram);2.依附shader到program上(gl.attachShader);3.链接program(gl.linkProgram);4.使用该program(gl.useProgram)。
? (二)关于点信息的创建(buffer的使用)
? 我们上面说,可以将点的描述传送给显卡,这些信息其实是存放在内存里面的。
function initBuffers() { var vertices, colors; // vertex buffer vertices = [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ]; verticesBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // vertex color buffer colors = [ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ]; verticesColorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); }
? 大致步骤是这样的:1.我们将点信息存放在数组里;2.然后创建buffer(gl.createBuffer),并绑定它(gl.bindBuffer),以便可以对它进行操作;3.设置数据(gl.bufferData)。PS:那个gl.STATIC_DRAW的意思我也不是很理解,大概是这样的:STATIC_DRAW保存的数据内容只被程序定义一次,GL绘制命令可以使用多次;DYNAMIC_DRAW保存的数据内容将被程序重复定义,GL绘制命令可以使用多次。
? (三)关于渲染
function drawScene() { var projectMatrix, worldMatrix, viewMatrix, pUniform, wUniform, vUniform; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // {{ phase 1 // bind to a shader attribute so the shader code can access. vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition'); gl.enableVertexAttribArray(vertexPositionAttribute); gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0); vertexColorAttribute = gl.getAttribLocation(shaderProgram, 'aVertexColor'); gl.enableVertexAttribArray(vertexColorAttribute); gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer); gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0); // }} // {{ phase 2 //projectMatrix= makePerspective(75, canvas.width / canvas.height, 1.0, 100.0); projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0); //modelviewMatrix= Matrix.I(4); worldMatrix = Matrix.I(4); worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4()); viewMatrix = Matrix.I(4); viewMatrix = viewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4()); // generate and deliver to the shader. pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix'); gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten())); wUniform = gl.getUniformLocation(shaderProgram, 'uWMatrix'); gl.uniformMatrix4fv(wUniform, false, new Float32Array(worldMatrix.flatten())); vUniform = gl.getUniformLocation(shaderProgram, 'uVMatrix'); gl.uniformMatrix4fv(vUniform, false, new Float32Array(viewMatrix.flatten())); // }} gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw) }
? 这里有很多重要的东西。
? 首先是js怎么和shader交互的问题,就是怎么把相应的数据传递给shader使用。简单说明下shader的变量的“类型”,attribute只有vertex shader有,是通过程序(js)传递给它的变量。uniform两种shader都有,而且是不能改变的,可以理解成常量;varying是vertex shader向fragment shader传递数据,fragment shader接受数据的方式。
? 然后,我们看上面的phase 1部分的代码,这里就是将刚刚设置到buffer中的点信息作为attribute传递给shader使用的代码。
? 1.调用vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition')会返回一个“位置”,这个位置可以理解成shader中对名为aVertexPosition这个attribute的引用(指针);
? 2.使用gl.enableVertexAttribArray(vertexPositionAttribute)开启attribute的数组传递(大概是这样吧?);
? 3.绑定我们创建并填充了点信息的那块verticesBuffer,gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
? 4.让步骤1中的shader的attribute指向这块buffer,gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0),我们之前创建buffer时传递的数据都是一维的,那第2个参数3就用来说明每3个数组元素组成一个attribute(其实就是一个vector3,代表点的位置)。表示颜色的attribute的过程与此类似。
? 接着,我们看phase 2部分的代码,这里就是设置观察变换矩阵、投影变换矩阵,并作为uniform传递给shader使用的代码。
? 1.projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0),创建正交投影矩阵,用于投影变换;
? 2.worldMatrix = Matrix.I(4);worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4());创建worldMatrix,并绕z轴旋转,这里其实就是在进行世界坐标系中的变换(平移、选择、缩放);
? 3.viewMatrix= Matrix.I(4);modelviewMatrix = modelviewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4());创建viewMatrix,并进行平移,这里之所以要进行平移,是因为我们的点的z轴设置的都是0,而我们的摄像机的z轴范围是1到100,进行这个平移,以便摄像机能看到这些点,实际上就是从世界坐标系到观察坐标系的一个变换;
? 4.pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix'),与attribute类似,会返回一个“位置”,这个位置可以理解成shader中对名为uPMatrix这个uniform的引用(指针);
? 5.gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten()));设置这个uniform的数据,那个flatten是将2维的矩阵转成1维数组的方法。其它的uniform设置与此类似。
? 最后,我们调用gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw),告诉程序以三角形的模式绘制,使用3个点。关于模式的参数,可以参考这里:http://fly.cc.fer.hr/~unreal/theredbook/figures/fig2-6.gif
?(四)关于shader
? 一切看起来都挺好,但是,shader呢?没有shader来进行真正的处理,传递这些数据是一点用处也没有的啊。我们就来依次来看看2个shader。
? 首先是vertex shader:
<script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; attribute vec4 aVertexColor; //attribute vec2 aTextureCoord; uniform mat4 uVMatrix; uniform mat4 uWMatrix; uniform mat4 uPMatrix; varying lowp vec4 vColor; //varing lowp vec2 vTextureCoord; void main(void) { gl_Position = uPMatrix * uVMatrix * uWMatrix * vec4(aVertexPosition, 1.0); vColor = aVertexColor; //vTextureCoord = aTextureCoord; } </script>
? 可以看到,vertex shader我们定义了2个attribute,分别表示点的位置和点的颜色信息;3个uniform,分别表示世界变换矩阵、观察变换矩阵、投影变换矩阵,这些值通过程序传递给shader。我们还定义了一个varying,用于传递给fragmeng shader颜色信息(因为vertex shader实际上无法操作像素,所以把颜色信息传递下去比较合理)。
? gl_Position = uPMatrix * uVMatrix * uWMatrix * vec4(aVertexPosition, 1.0);就是对每一个点进行对应的变换,先是世界坐标中的变换(乘以uWMatrix);然后是观察坐标变换(乘以uVMatrix),最后投影变换(乘以uPMatrix)。然后赋值给shader内置的变量gl_Position,表示点的最终计算出的位置。
? vColor = aVertexColor;将传递进来的点的颜色信息,直接赋值给vColor,以便fragment shader使用。
? 然后,看看fragment shader:
<script id="shader-fs" type="x-shader/x-fragment"> //uniform sampler2D uSampler; varying lowp vec4 vColor; //varing lowp vec2 vTextureCoord; void main(void) { gl_FragColor = vColor; //gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord,t)); } </script>
? 可以看到,fragment shader定义了一个varying,用于接收vertex shader传递的颜色信息;然后将这个颜色信息赋值给shader的内置变量gl_FragColor,表示顶点颜色。光栅化时,实际上,会对顶点表示的这个图元(三角形)的像素颜色进行插值,然后确定出最终颜色,用来插值的就是顶点颜色。
? 所以,最后的效果就是这样: