首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 网站开发 > Web前端 >

WebGL学习札记【一】概述及三角形

2012-11-03 
WebGL学习笔记【一】概述及三角形转自:http://www.cnblogs.com/kohpoll/WebGL学习笔记【一】概述及三角形?这就

WebGL学习笔记【一】概述及三角形

转自:http://www.cnblogs.com/kohpoll/


WebGL学习笔记【一】概述及三角形

?这就是模型的最最基本的描述,如果再加上一些材质、打上灯光什么的,它看起来会是这样:

WebGL学习札记【一】概述及三角形

? (二)

? 大致说了图形描述的问题,那接着是绘制。也就是怎么样把三维的东西绘制到二维的屏幕。

? 这里,我们想到,要是有一个图形渲染机,我们把我们对模型的描述,也就上面说的一堆点的信息,统统倒入这个机器,机器一阵处理后就可以在屏幕上绘制出模型,那就好了。确实也有这样一个机器,它的输入可以理解成一堆点(用数组之类的表示),输出就是我们屏幕上闪闪动人的模型。其实,这个机器可以理解成显卡。那它是怎么做到的?

? 首先是我们怎么样把点的信息告诉显卡,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,表示顶点颜色。光栅化时,实际上,会对顶点表示的这个图元(三角形)的像素颜色进行插值,然后确定出最终颜色,用来插值的就是顶点颜色。

? 所以,最后的效果就是这样:

WebGL学习札记【一】概述及三角形

热点排行