31 WebGL平行光下的漫反射光的计算

2018-03-04 03:04:02 人阅读

案例查看有问题?
全屏试试!

案例解析

 通过上一节的介绍,我们可以知道,平行光下的漫反射光的颜色可以由一下式子得出:

<漫反射光颜色>=<入射光颜色>x<表面基底色>x cos θ

所以,我们要计算出来漫反射的光的颜色需要三项数据:

(1)平行入射光的颜色

(2)表面的基底色

(3)入射光与表面形成的入射角  θ

但是,我们没有办法直接就确定 θ值是多少,我们必须根据入射光的方向和物体表面的朝向(即法线方向)来计算出入射角。这需要我们先确定每个表面的朝向,在指定光源的时候,再确定光的方向,来计算出入射角,并确定 θ的值。

幸运的是,我们可以通过计算两个矢量的点积,来计算着两个矢量的夹角余弦值cos  θ。点积运算的使用非常频繁,GLSL ES内置了点积运算函数(详见GLSL ES中的第11节,11 WebGL 着色器编程语言GLSL ES的内置函数)。在公式中,我们使用点符号 · 来表示点积运算。这样,cos θ就可以通过下式计算出来:

cos θ = <光线方向> · <法线方向>

所以,上面计算平行光下的漫反射的颜色的式子就可以修改成:

<漫反射光颜色> = <入射光颜色> x <表面基底色> x (<光线方向> · <法线方向>)

这里有两点需要注意:

(1)光线方向矢量和表面法线矢量的长度必须为1,否则反射光的颜色就会过暗或过亮。将一个矢量的长度调整为1,同时保持方向不变的过程称之为归一化。GLSL ES提供了内置的归一化函数,你可以直接使用。

(2)这里所谓的“光线方向”,实际上是入射方向的反方向,即从入射点指向光源方向(因为这样,该方向与法线方向的夹角才是入射角),如图。

法线:表面的朝向

物体表面的朝向,即垂直于表面的方向,又称法线或法向量。法向量有三个分量,向量(nx,ny,nz)表示从原点(0,0,0)指向点(nx,ny,nz)的方向。比如说,向量(1,0,0)表示x轴正方向,向量(0,0,1)表示z轴正方向。涉及到表面和法向量的问题时,必须考虑以下两点:

(1)一个表面具有两个法向量

每个表面都有两个面,“正面”和“背面”。两个面各自具有一个法向量。比如,垂直于z轴的x-y平面,其正面的法向量为z正半轴,即(0,0,1),背面的法向量为z负半轴,即(0,0,-1)。

在三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序。当你按照v0,v1,v2,v3的顶点顺序绘制了一个平面,那么当你从正面观察这个表面时,这4个顶点是顺时针的,而你从背面观察该表面,这四个顶点就是逆时针的。

(2)平面的法向量唯一

由于法向量表示的是方向,与位置无关,所以一个平面只有一个法向量。换句话说,平面的任意一点都具有相同的法向量。

进一步说,即使有两个不同的平面,只要朝向相同(也就是两个平面平行),法向量也相同。

一旦计算好每个平面的法向量,接下来的任务就是将数据传给着色器程序。以前的程序把颜色作为“逐顶点数据”存储在缓冲区中,并传给着色器。对法向量数据也可以这样做。上图所示,每个顶点对应3个法向量,就像之前每个顶点都对应3个颜色值一样。我们明白了这一点以后,就可以通过法向量获取平行光照后的颜色值了。

在顶点着色器中,我们实现了漫反射光颜色的计算,使用的是:

<漫反射光颜色> = <入射光颜色> x <表面基底色> x (<光线方向> · <法线方向>)

计算漫反射光颜色需要:(1)入射光颜色 (2)表面基底色 (3)入射光方向 (4)表面法线方向。其中后两者都必须是归一化的(即长度为1.0)。

在上面的案例代码中:

a_Color 变量表示表面基底色

a_Normal变量表示表面法线方向

u_LightColor变量表示入射光颜色

u_LightDirection变量表示入射光方向(是在世界坐标系下的,在javascript中归一化了,可以避免着色器每次执行都对它进行归一化)

相关的变量已经获取,下面是进行处理:

着色器部分处理:

首先对a_Normal法线变量进行归一化,虽然写的值是已经归一化过的,但是顶点着色器可不知道传入的矢量是否经过了归一化,而且这里没有节省开销的理由(法向量是逐顶点的),由于使用x,y和z三个分量就可以表示法线方向,我们需要将这三个分量提取出来,使用到了vec3()方法,然后使用内置函数normalize()函数进行归一化。

接下来,我们计算点积<光线方向> · <法线方向>。法线方向已经被归一化,光线方向在传入进来之前已经被归一化。直接使用GLSL ES提供的内置函数dot()进行点积运算。然后又使用max()函数时因为如果计算出来的数大于90度的话,就已经在背面了。我们这是计算的不透明的,所以在背面就是0。

计算出来点积后,就可以继续套用公式进行漫反射光的颜色,计算出漫反射光的rgb颜色。

最后再把rgba的a给补上,最后就把颜色给计算了出来,再把颜色通过varying变量v_Color传到片元着色器。

js部分处理:

首先就给u_LightColor赋值了光线的颜色。

然后给u_LightDirection赋值光线方向,再使用cuon-matrix.js里面封装好的函数,直接在对象上调用一下,使对象的值归一化,最后把值赋值给了u_LightDirection。

最后在颜色下面声明了一个存储法向量的Float32Array将值存储,和顶点和颜色一起赋值。

案例源代码


                        
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Title</title>
    <style>
        body {
            margin: 0;
            text-align: center;
        }
 
        #canvas {
            margin: 0;
        }
    </style>
</head>
<body onload="main()">
<canvas id="canvas" height="800" width="1200"></canvas>
</body>
<script src="/lib/webgl-utils.js"></script>
<script src="/lib/webgl-debug.js"></script>
<script src="/lib/cuon-utils.js"></script>
<script src="/lib/cuon-matrix.js"></script>
<script>
    //顶点着色器
    var VSHADER_SOURCE = "" +
        "attribute vec4 a_Position;\n" +
        "attribute vec4 a_Color;\n" +
        "attribute vec4 a_Normal;\n" +//法向量
        "uniform mat4 u_MvpMatrix;\n" +
        "uniform vec3 u_LightColor;\n" +//光线颜色
        "uniform vec3 u_LightDirection;\n" +//入射光方向,归一化的世界坐标
        "varying vec4 v_Color;\n" +
        "void main(){\n" +
        "   gl_Position = u_MvpMatrix * a_Position;\n" +
        //对法向量进行归一化
        "   vec3 normal = normalize(vec3(a_Normal));\n" +
        //计算光线方向和法向量的点积
        "   float nDotL = max(dot(u_LightDirection,normal),0.0);\n" +
        //计算漫反射光的颜色
        "   vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n" +
        "   v_Color = vec4(diffuse,a_Color.a);\n" +
        "}";
 
    //片元着色器
    var FSHADER_SOURCE = "" +
        "#ifdef GL_ES\n" +
        "precision mediump float;\n" +
        "#endif\n" +
        "varying vec4 v_Color;\n" +
        "void main(){\n" +
        "   gl_FragColor = v_Color;\n" +
        "}";
 
    //主函数,页面加载完成触发
    function main() {
        //获取canvas对象
        var canvas = document.getElementById("canvas");
 
        //获取WebGL上下文
        var gl = getWebGLContext(canvas);
        if (!gl) {
            console("您的浏览器不支持WebGL");
            return;
        }
 
        //初始化着色器
        if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
            console.log("初始化着色器失败");
            return;
        }
 
        //设置顶点的坐标、颜色和法向量
        var n = initVertexBuffers(gl);
        if (n < 0) {
            console.log("无法获取到顶点个数,设置顶点坐标、颜色和法向量失败");
            return;
        }
 
        //初始化背景色和前后关系功能开启
        gl.clearColor(0, 0, 0, 1);
        gl.enable(gl.DEPTH_TEST);
 
        //获取模型视图投影矩阵、光线颜色变量和归一化世界坐标uniform变量的存储位置
        var u_MvpMatrix = gl.getUniformLocation(gl.program, "u_MvpMatrix");
        var u_LightColor = gl.getUniformLocation(gl.program, "u_LightColor");
        var u_LightDirection = gl.getUniformLocation(gl.program, "u_LightDirection");
        if (!u_MvpMatrix || !u_LightColor || !u_LightDirection) {
            console.log("无法获取相关的存储位置,或者未定义");
            return;
        }
 
        //设置光线颜色(白色)
        gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
        //设置光线方向(世界坐标系下的)
        var lightDirection = new Vector3([0.5, 3.0, 4.0]);
        lightDirection.normalize(); //归一化
        gl.uniform3fv(u_LightDirection, lightDirection.elements);
 
        //计算模型视图投影矩阵
        var mvpMatrix = new Matrix4(); //声明一个矩阵模型
        mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);//设置透视矩阵
        mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
 
        //将模型视图投影矩阵传给u_MvpMatrix变量
        gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
 
        //清除底色和深度缓冲区
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 
        gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0); //绘制图形
    }
 
    function initVertexBuffers(gl) {
        // 绘制一个立方体
        //    v6----- v5
        //   /|      /|
        //  v1------v0|
        //  | |     | |
        //  | |v7---|-|v4
        //  |/      |/
        //  v2------v3
        var vertices = new Float32Array([   // 顶点坐标
            1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 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, // v0-v3-v4-v5 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, // v0-v5-v6-v1 up
            -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 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, // v7-v4-v3-v2 down
            1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0  // v4-v7-v6-v5 back
        ]);
 
 
        var colors = new Float32Array([    // 顶点颜色
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,     // v0-v1-v2-v3 front
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,     // v0-v3-v4-v5 right
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,     // v0-v5-v6-v1 up
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,     // v1-v6-v7-v2 left
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,     // v7-v4-v3-v2 down
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0     // v4-v7-v6-v5 back
        ]);
 
 
        var normals = new Float32Array([    // 法向量
            0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,  // v0-v1-v2-v3 front
            1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,  // v0-v3-v4-v5 right
            0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,  // v0-v5-v6-v1 up
            -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,  // v1-v6-v7-v2 left
            0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,  // v7-v4-v3-v2 down
            0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0   // v4-v7-v6-v5 back
        ]);
 
 
        // 绘制点的顺序下标
        var indices = new Uint8Array([
            0, 1, 2, 0, 2, 3,    // front
            4, 5, 6, 4, 6, 7,    // right
            8, 9, 10, 8, 10, 11,    // up
            12, 13, 14, 12, 14, 15,    // left
            16, 17, 18, 16, 18, 19,    // down
            20, 21, 22, 20, 22, 23     // back
        ]);
 
 
        // 通过initArrayBuffer方法将顶点数据保存到缓冲区
        if (!initArrayBuffer(gl, 'a_Position', vertices, 3, gl.FLOAT)) return -1;
        if (!initArrayBuffer(gl, 'a_Color', colors, 3, gl.FLOAT)) return -1;
        if (!initArrayBuffer(gl, 'a_Normal', normals, 3, gl.FLOAT)) return -1;
 
        // 创建顶点索引缓冲区对象
        var indexBuffer = gl.createBuffer();
        if (!indexBuffer) {
            console.log('无法创建顶点索引的缓冲区对象');
            return -1;
        }
 
        //将顶点索引数据存入缓冲区
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
 
        return indices.length;
    }
 
    function initArrayBuffer(gl, attribute, data, num, type) {
        //创建缓冲区对象
        var buffer = gl.createBuffer();
        if (!buffer) {
            console.log("无法创建缓冲区对象");
            return false;
        }
        //绑定缓冲区,并将数据存入缓冲区
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
 
        //获取相关变量存储位置,赋值并开启缓冲区
        var a_attribute = gl.getAttribLocation(gl.program, attribute);
        if (a_attribute < 0) {
            console.log("无法获取" + attribute + "变量的相关位置");
            return false;
        }
 
        //向缓冲区赋值
        gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0)
 
        //开启数据,并解绑缓冲区
        gl.enableVertexAttribArray(a_attribute);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
 
        return true;
    }
</script>
</html>
展开内容

联系我们

一个人的力量不如两个人的,两个人的力量不如一群人的。欢迎加入大家庭一起共同学习,共同进步。

查看更多