立方体添加平行光

优质
小牛编辑
129浏览
2023-12-01

该WebGL案例源码是通过给一个单色的立方体添加平行光进行渲染,通过这样一个简单的WebGL光照计算案例,来体会光照模型在物体渲染中的应用,在学习下面的代码之前确保你有逐顶点和颜色插值计算的概念,了解顶点位置数据、顶点颜色数据,本节课在这两种顶点数据的基础之上在引入一种新的顶点数据:定点法向量。

平行光照射在立方体上,与不同的平面夹角不同,自然反射的颜色RGB值强弱不同,实际绘图的时候你不可能手动计算去定义每一个像素的值,前面课程中讲解颜色插值计算的知识,应该对你有一定的启发,你只需要计算出每一个顶点在光照下的颜色, 然后利用插值计算就可以得到三个顶点之间区域的像素值,这时候顶点法向量数据就派上了用场,每一个顶点都有位置数据、颜色数据、法向量数据,法向量和颜色数据先进行计算得出新的顶点颜色数据,然后渲染管线对顶点进行装配光栅化的过程中,新的顶点颜色数据进行插值计算。 这时候提示一下大家,不要去从宏观思考法向量问题,要从逐顶点、插值计算的角度理解问题,下面的问题可能大家会有一个疑问,为什么立方体的一个顶点会有三个方向,从实际的物体来看,立方体的一个顶点就是一个顶点,但是从绘图的角度来看, 每一次是通过三个顶点绘制一个三角形面,只不过恰好三个三角形面共顶点,会有顶点位置重复而已,但是每一组的三个顶点是一个装配光栅化和插值计算的独立单元,两组的三个顶点位置有重复也不会影响他们各自的渲染得到的像素结果。

顶点着色器:声明变量

attribute vec4 apos;//attribute声明vec4类型变量apos
attribute vec4 a_color;// attribute声明顶点颜色变量
attribute vec4 a_normal;//顶点法向量变量
uniform vec3 u_lightColor;// uniform声明平行光颜色变量
uniform vec3 u_lightPosition;// uniform声明平行光颜色变量
varying vec4 v_color;//varying声明顶点颜色插值后变量

查看上面的代码大家可以发现一个新的关键字uniformuniform关键字和attribute关键字的都是为了javascript可以向WebGL着色器传递数据而出现。区别在于如果一个变量表示顶点相关的数据并且需要从javascript代码中获取顶点数据,需要使用attribute关键字声明该变量,比如上面代码attribute关键字声明的顶点位置变量apos、顶点颜色变量a_color、顶点法向量变量a_normal。如果一个变量是非顶点相关的数据并且需要javascript传递该变量相关的数据,需要使用uniform关键字声明该变量,比如上面代码通过uniform关键字声明的光源位置变量u_lightPosition、光源颜色变量u_lightColor。更多关于uniform关键字和attribute关键字的内容可以查看第二章着色器介绍2.8 三种变量类型attribute、uniform、varying

在上面代码中涉及WebGL着色器三维向量vec3和四维向量vec4两种数据类型,比如光线的方向需要xyz三个分量描述,可以使用关键字vec3声明,比如顶点位置的齐次坐标(x,y,z,w), 包含透明度的RGB颜色模型RGBA(r,g,b,a)都需要四个分量描述,可以使用关键字vec4声明。

attribute vec4 a_normal;中变量a_normal定义的是vec4类型,第四个参数默认是1.0主要是为了凑成齐次坐标用于矩阵计算,表示法向量方向的是前三个参数,所以执行a_normal.xyz就相当于访问法向量的xyz值,返回的结果是一个vec3数据,如果执行vec3.xy相当于返回一个vec2数据, 如果一个顶点的a_normal数据是(1.0,1.0,1.0,1.0),那么它的模长就不是1,而是3的平方根,这时候需要把前三个1全部除以3的平方根才可以把非单位向量转化为单位向量。

顶点着色器:光照计算

// 顶点法向量进行矩阵变换,然后归一化
vec3 normal = normalize((mx*my*a_normal).xyz);
// 计算点光源照射顶点的方向并归一化
vec3 lightDirection = normalize(vec3(gl_Position) - u_lightPosition);
// 计算平行光方向向量和顶点法向量的点积
float dot = max(dot(lightDirection, normal), 0.0);
// 计算反射后的颜色
vec3 reflectedLight = u_lightColor * a_color.rgb * dot;
//颜色插值计算
v_color = vec4(reflectedLight, a_color.a);

dot()是WebGL着色器语言的一个内置函数,可以直接使用,它的参数是两个向量,执行结果是两个向量的点积,如果光线方向向量和顶点法向量两个向量都是单位向量,求解的结果就是平行光线与物体表面法线夹角的余弦值。 内置函数normalize()dot()一样是WebGL着色器语言提供的用于数学计算的函数,normalize()的作用就是把向量归一化,具体点说就是如果向量的模长不是1,不改变向量方向,把模长变为1,也就是把向量转化为单位向量。

max(dot(lightDirection, normal), 0.0)在dot代码的外面嵌套了一个函数max()dot()的计算结果作为max()的第一个参数,dot的计算结果可能是[-1,1]之间,颜色不存在负值要舍去[-1,0),这时候就是内置函数max()派上用场的时候,max(dot(lightDirection, normal), 0.0)max()函数的第二个参数是0, dot()方法的计算结果会和0进行比较运算,返回一个较大的值,着色器语言内置提供了max()函数,自然也有对应的求较小值的函数min()。余弦值是负值的物理意义就是光线无法照的地方,临界点是余弦值0,入射角是90度,也就是说入射光线平行平面, 平行平面相当于光线没有照射到平面上,平面没有收到光自然无法反射光,你可以尝试改变立方体顶点的旋转矩阵可以看到一些面的颜色是黑色,就是因为没有光线照射,这一点是复合实际生活和物理规律的,不管是提本身是什么颜色,没有外界光源,那就表现为黑色。

u_lightColor * a_color.rgb * dot;是套用理想漫反射光照模型的一个公式进行计算,顶点颜色变量a_colorvec4类型包含透明度,计算式中光源颜色变量u_lightColor没有透明度分量A,所以使用a_color.rgb语句返回a_color变量的RGB三个分量,也就是返回一个vec3类型数据。

v_color = vec4(reflectedLight, a_color.a);通过把反射颜色reflectedLight赋值给varying声明的一个变量实现颜色的插值计算,关于插值计算1.7节讲过,不再多谈,这里说一下着色器语言数据类型的相关转化、构造、访问相关问题,vec4vec3floatint一样是数据类型的标识关键字,floatint在C语言中都是常见的类型, 着色器语言为了实现大规模的顶点运算增加了vec4vec3mat4等数据类型,vec4()vec3()这时候的表达相当于一个vec4vec3数据的构造函数,vec4(reflectedLight, a_color.a)中把一个vec3类型数据和一个vec4类型数据的一个分量a作为构造函数vec4的两个参数,来实现创建一个vec4类型数据。访问多元素数据的分量可使用点符号.,从面对象的角度来看, 一个vec4数据是一个对象,数据的一个元素就是数据的一个分量,比如 a_color.a表示vec4类型变量a_color的透明度分量a。关于vec4vec3等向量数据类型的介绍可以查看第二章WebGL着色器。

获取着色器变量

/**
 * 从program对象获取相关的变量
 * attribute变量声明的方法使用getAttribLocation()方法
 * uniform变量声明的方法使用getAttribLocation()方法
 **/
var aposLocation = gl.getAttribLocation(program,'apos');
var a_color = gl.getAttribLocation(program,'a_color');
var a_normal = gl.getAttribLocation(program,'a_normal');
var u_lightColor = gl.getUniformLocation(program,'u_lightColor');
var u_lightPosition = gl.getUniformLocation(program,'u_lightPosition');

要想给着色器程序中声明的变量传递数据,首先要获取数据的地址,然后通过指针地址传递给变量。要想获取变量地址,不可能像普通CPU变成一样,要考虑GPU的特殊性,首先要通过调用初始化着色器函数initShader(),把着色器程序处理后通过CPU与GPU的通信传递给GPU配置渲染管线, 执行初始化着色器函数initShader()的同时会返回一个program对象,通过对象program可以获取着色器程序中的变量索引地址,通过WebGL APIgl.getAttribLocation()用来获取attribute关键字声明的顶点变量地址,通过WebGL APIgl.getUniformLocation()获取uniform关键字声明的非顶点变量地址。

传递数据

对于简单的数据,不像顶点有大批量数据,不需要在显存上开辟缓冲区上传数据,可以直接使用WebGL APIgl.uniform3f()把数据传递给GPU,uniform3f(变量地址,x,y,z)表示传递三个浮点数x,y,z,接收这个数据的变量是vec3类型,uniform2f(变量地址,x,y)表示传递2个浮点数,接收数据的变量是vec2数据类型, 以此类推还有方法uniform4f()uniform1f(),命名的特点是数字表示传递的数据有多少分量,f是关键字float的缩写表示浮点数。

/**
 * 给平行光传入颜色和方向数据,RGB(1,1,1),单位向量(x,y,z)
 **/
gl.uniform3f(u_lightColor, 1.0, 1.0, 1.0);
//保证向量(x,y,-z)的长度为1,即单位向量
// 如果不是单位向量,也可以再来着色器代码中进行归一化
var x = 1/Math.sqrt(15), y = 2/Math.sqrt(15), z = 3/Math.sqrt(15);
gl.uniform3f(u_lightDirection,x,y,-z);

传递uniform变量的WebGL API对应的着色器uniform变量数据类型

WebGL API数据类型
uniform1f()float
uniform2f()vec2
uniform3f()vec3
uniform4f()vec4

顶点法向量

顶点法向量

/**
 *顶点法向量数组normalData
 **/
var normalData = new Float32Array([
    0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
    1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
    0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
    -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
    0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
    0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1//z轴负方向——面6
]);

对于立方体而言六个平面也就是有六个不同的平面法向量,但是这里要注意思考如何表达一个面的法向量,你不能说直接告诉GPU一个平面的法向量是(x,y,z),对于立方体而言而言这样比较简单,符合人的思维,但是如果是复杂的曲面这样并不合适,而且顶点着色器是逐顶点处理, 为了表示面的法向量,往往是通过顶点,具体点说是每个顶点对应一个法向量,三个顶点确定一个三角面,三角面的不同位置的法向量相当于通过他的三个顶点的法向量插值得出,或者换个说法就是三个顶点的法向量分别与各自颜色数据进行乘法运算,得到新的顶点颜色, 然后渲染管线利用新的顶点颜色进行插值计算,这就是通过顶点法向量表示面法向量的方法,通过这样的巧妙设计还可以实现法向量的插值计算,只不过插值是通过颜色插值完成的,对于平滑的曲面,过每一个顶点存在一个法平面,也就是有一个法向量,往往在不同三角面中同一位置的顶点法向量是相同的, 对于规则的长方体而言,每个顶点法向量往往在各自平面中有不同的值。

顶点法向量矩阵变换

执行代码gl_Position = mx*my*apos;意味着通过旋转矩阵mxmy对立方体顶点数据apos进行旋转变换,也就是说立方体在三维空间中进行了旋转,如果立方体进行了旋转,也就是说立方体表面的法线方向变化了,立方体表面的法线是通过顶点法向量表示的,所以说顶点法向量数据要和顶点位置数据一样执行矩阵变换mx*my*a_normal

//两个旋转矩阵、顶点齐次坐标连乘
gl_Position = mx*my*apos;
// 顶点法向量进行矩阵变换,然后归一化
vec3 normal = normalize((mx*my*a_normal).xyz);

视线

通过下面的代码测试,来体会WebGL投影的规律。

课程在《WebGL坐标系》中讲解过WebGL图形系统默认的投影方向,或者说人的眼睛观察几何体的方向,或者说照相机拍照的方向。 代码中的灯光方向的向量数据,第三个参数是负数,也就是说从z轴的角度看,平行光照射物体的方向,就是人眼睛看物体的方向,如果把灯光方向的向量数据z参数更改为正数,刷新浏览器看到的是一个漆黑的立方体投影, 这时候相当于黑暗的环境中,人站在物体的背光面,而不是向光面,对于WebGL图形系统,你可以形象的理解为把光源从屏幕前面放到了屏幕的后面,人的观察方向是沿着z轴负方向,从屏幕外向里观察,光线自然被立方体遮挡住了。

var x = 1/Math.sqrt(15), y = 2/Math.sqrt(15), z = 3/Math.sqrt(15);
gl.uniform3f(u_lightDirection,x,y,-z);

更改-z为z,立方体显示为黑色效果。

gl.uniform3f(u_lightDirection,x,y,z);