前言
本文将介绍用webgl写一个球体的核心逻辑,以及光照在webgl的具体应用,平行光、点光、聚光等原理,dat.gui如何快速配置,让你快速理解三维世界的光照
绘制球体
前置工作
一般而已,我们会封装webgl-utils的工具库、封装通用的矩阵算法、封装创建几何体的方法
<script src="resources/webgl-utils.js"></script> //webgl的通用封装
<script src="resources/m4.js"></script> // 矩阵通用方法
<script src="resources/primitives.js"></script> // 几何体封装方法
TWGL
为了更好的方便我们开发,我们可以使用twgl.js轻量库github.com/greggman/tw… 来简化繁琐的api调用。
绘制球体的核心逻辑
function createSphere (gl, radius, divideByYAxis, divideByCircle) {
let yUnitAngle = Math.PI / divideByYAxis;
let circleUnitAngle = (Math.PI * 2) / divideByCircle;
let positions = [];
let normals = [];
for (let i = 0; i <= divideByYAxis; i++) {
let unitY = Math.cos(yUnitAngle * i);
let yValue = radius * unitY;
for (let j = 0; j <= divideByCircle; j++) {
let u = i / divideByYAxis
let v = j / divideByCircle
let unitX = Math.sin(yUnitAngle * i) * Math.cos(circleUnitAngle * j);
let unitZ = Math.sin(yUnitAngle * i) * Math.sin(circleUnitAngle * j);
let xValue = radius * unitX;
let zValue = radius * unitZ;
positions.push(xValue, yValue, zValue);
normals.push(unitX, unitY, unitZ);
texCoords.push(1 - u, v);
}
}
球体的每个面的点计算还是挺复杂的,我们先从Y轴来拆分层级,注意我们通过cos得到的是y轴的坐标,而不是x坐标。然后计算每层的x轴坐标,unitX = Math.cos(circleUnitAngle * j)
这样得到的结果你会发现是平面的图形,但是我们球面需要根据unitY对应的x坐标,来乘积得到符合曲线规律的x坐标,同理在这个平面的z轴也可以得出。
法向量和纹理uv
法向量一般表示图形面的朝向的单位向量,我们根据之前绘制的xyz的坐标就可以获得,注意是单位向量。同时纹理uv,其实是展开的二维贴图的坐标,我们根据需要,如果每一面要自定义纹理,就是对应的份数差。注意这里纹理坐标是相反的,所以1 - u
平行光原理
就是将光源向量和法向量做向量的点积, 最后在片元着色器的gl_FragColor的rgb相乘即可
function dot (a, b) {
return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
}
vec3 normal = normalize(v_normal);
float light = dot(normal, u_reverseLightDirection);
点光源原理
点光源是四周扩展的效果,点光源明显的问题就是根据角度的不同,光照强度会不同,离点的角度越小,那么光照强度会越低。
核心就是先得到黑色线段的向量,然后将此向量和法向量做点积
v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
vec3 surfaceToLightDirection = normalize(v_surfaceToLight); //转为法向量
light = dot(normal, surfaceToLightDirection);
点积公式
a * b = |a| * |b| * cosθ
由于a和b都是单位向量,实数是相等的,角度越大cos值越小,所以一旦法向量和反射的向量一致,点积为1,相反为-1
高光原理
如果光线直接在物体反射后,如果光线的方向正好是人眼,那么物体表面会有高光现象,但是越粗糙的物体,漫反射会越强,同时镜面反射会弱。所以金属、玻璃材质的会有更强的高光感受。normal和 halfVector的点积代表高光的强度。
- vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
- vec3 surfaceToViewDirection = normalize(v_surfaceToView);
- vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
-
- float specular = dot(normal, halfVector);
聚光灯原理
聚光灯其实就是限制了范围而已,我们通过limit变量控制范围,值是0到1的范围,那么对应的角度通过三角函数在cos的x值为0.94,那么对应的角度可以得出。
有一个 GLSL 函数叫做 step
,它获取两个值,如果第二个值大于或等于第一个值就返回 1.0, 否则返回 0, 只要是1,就显示光照
float dotFromDirection = dot(surfaceToLightDirection, -u_lightDirection);
// 如果光线在聚光灯范围内 inLight 就为 1,否则为 0
float inLight = step(u_limit, dotFromDirection);
float light = inLight * dot(normal, surfaceToLightDirection);
float specular = inLight * pow(dot(normal, halfVector), u_shininess);
通过dat.gui快速配置
dat.gui能快速根据配置项生成控制面板,方便我们开发和调试。
const options = {
lightX: 43,
lightY: 13,
lightZ: 82,
shininess: 8,
lightColor_r: .4,
lightColor_g: 1,
lightColor_b: 1,
sphereColor_r: .7,
sphereColor_g: .5,
sphereColor_b: 1,
innerLimit: 20,
outerLimit: 200,
}
function createGUI () {
const gui = new dat.GUI();
gui.add(options, 'lightX', 0, 100)
gui.add(options, 'lightY', 0, 100)
gui.add(options, 'lightZ', 0, 100)
gui.add(options, 'shininess', 0, 1000)
gui.add(options, 'lightColor_r', 0, 1)
gui.add(options, 'lightColor_g', 0, 1)
gui.add(options, 'lightColor_b', 0, 1)
gui.add(options, 'sphereColor_r', 0, 1)
gui.add(options, 'sphereColor_g', 0, 1)
gui.add(options, 'sphereColor_b', 0, 1)
gui.add(options, 'innerLimit', 1, 600)
gui.add(options, 'outerLimit', 1, 600)
}
总结
webgl学得越深入,发现里面的坑无穷无尽🤕,想要图形化领域学得好,数学还真得下功夫。尤其是webgl的调试困难,api繁琐老旧,很多时候没有数学的支撑或者原理的理解,是很难找到问题所在的。
本文只是简单的概述了光照在gl的应用,实际渲染场景里,灯光是很重要的一环,一般常见场景的有太阳光、点光、聚光灯、线性灯等,太阳光一般我们用平行光模拟,当然你也可以用点光源。同时光源的计算强度有不同的单位控制。聚光灯为了更好的融合场景,会存在衰减的深度范围,包括一些材质可以实现自发光的效果,三维纹理的应用等等.....