三维模型平行光照射实现
1. demo效果
上图是平行光照射下效果
上图是无光照时的效果
2. 知识要点
2.1 平行光照射
平行光的光线相互平行,没有位置信息,只有方向,可以看做是无限远处的光源发出的光,例如太阳照射在地球上的光。
平行光照射在物体上会反射,不同方向的面反射光的方向也不同,平行光如何反射,取决于物体的表面法向量,如下图光线从左上方入射进来,在物体左表面和上表面的反射光完全不同
平行光如何反射决定于物体的表面法向量,如上图中物体左表面的法向量n呈水平方向,根据反射定律即可计算出反射光的方向
2.2 物体变换时表面法线处理
如果物体变换,那么相对光源表面法向量也会变,但是如果把物体的每个表面法向量计算一次太耗费性能,可以换一种思维,物体和物体表面法线不变换,但光源方向正好做相反的变换,这样的话,就可以通过模型变换矩阵求逆,得到用来计算光线变换的矩阵(反向思维时)
说到矩阵,必须介绍一个矩阵库cuon-matrix.js, 它提供了一系列WebGL需要用到的矩阵相关函数,该文件下载请点击这里
其中相关方法和属性如下
方法/属性名称 | 描述 |
---|---|
Matrix4.setIdentity() | 将Matrix4实例初始化为单位矩阵 |
Matrix4.setTranslate(x,y,z) | 将Matrix4实例设置为平移矩阵,参数x,y,z分别代表在X轴,Y轴,Z轴上平移的距离 |
Matrix4.setRotate(angle,x,y,z) | 将Matrix4实例设置为旋转矩阵,旋转的角度为angle,旋转轴为(x,y,z) |
Matrix4.setScale(x,y,z) | 将Matrix4实例设置为缩放矩阵,在三个轴上的缩放因子分别是x,y,z |
Matrix4.translate(x,y,z) | 将Matrix4实例乘以一个平移矩阵,该平移矩阵在X轴上平移x,Y轴上平移y,Z轴上平移z,相乘的结果还是储存在Matrix4中 |
Matrix4.rotate(angle,x,y,z) | 将Matrix4实例乘以一个旋转矩阵,该旋转矩阵旋转的角度为angle,旋转轴为(x,y,z),相乘的结果还是储存在Matrix4中 |
Matrix4.scale(x,y,z) | 将Matrix4实例乘以一个缩放矩阵,该缩放矩阵在三个轴上的缩放因子分别是x,y,z 。相乘的结果还是储存在Matrix4中 |
Matrix4.set(m) | 将Matrix4实例设置为m,m必须也是一个Matrix4实例 |
Matrix4.elements | 保存了Matrix4实例的矩阵元素的类型化数组,该类型化数组的种类为Float32Array |
Matrix4.setLookAt(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) | 根据视点(eyeX, eyeY, eyeZ)、观察点(atX, atY, atZ)、上方向(upX, upY, upZ)创建视图矩阵,视图矩阵的类型是Matrix4,观察点映射到的中心点 |
Matrix4.setOrtho(left, right, bottom, top, near, far) | 根据参数top、bottom、left、right、near、far计算正投影矩阵 |
Matrix4.setPerspective(fov, aspect, near, far) | 根据参数fov, aspect, near, far计算透视投影矩阵 |
Matrix4.setInverseOf(m) | 使自身称为矩阵m的逆矩阵 |
Matrix4.transpose() | 对自身进行转置操作,并将自身设置为转置后的结果 |
3. 实现要点
3.1 顶点着色器
在顶点着色器中首先声明接收顶点坐标、颜色、法线、模型视图投影矩阵、模型变换矩阵的逆矩阵、平行光方向、颜色的变量,接下来利用这些变量计算平行光照射
- 使用模型变换矩阵的逆矩阵和光线方向计算变换后的光线方向invLight
- 计算光照系数,使用光线和法线的点积,同时使用clamp函数将结果取值范围限定为0.1~1.0
- 确定最终的颜色,使用光线颜色乘以光照系数在乘以顶点颜色,然后传给存放颜色信息的varying变量
具体代码如下
//顶点着色器
var VSHADER_SOURCE = `
attribute vec3 position; //顶点位置信息
attribute vec4 color; //颜色
attribute vec3 normal; //法线
uniform mat4 uMvpMatrix; //模型视图投影矩阵
uniform mat4 uInvMatrix;//模型坐标变换矩阵的逆矩阵
uniform vec3 uLightDirection;//平行光方向
uniform vec3 u_LightColor;//光线颜色
varying vec4 vColor; //向片元着色器传值颜色信息
void main(){
vec3 invLight = normalize(uInvMatrix*vec4(uLightDirection,0.0)).xyz;
float diffuse = clamp(dot(normal,invLight),0.1,1.0);
vColor = vec4(vec3(u_LightColor),1.0)*color*vec4(vec3(diffuse),1.0);
gl_Position = uMvpMatrix*vec4(position,1.0); //将模型视图投影矩阵与顶点坐标相乘赋值给顶点着色器内置变量gl_Position
}
`
3.2 相关矩阵计算
向着色器一共传输了两个矩阵:模型视图投影矩阵uMvpMatrix和模型坐标变换矩阵的逆矩阵uInvMatrix
- 模型视图投影矩阵的计算过程
首先声明三个矩阵变量:模型视图投影矩阵 g_MvpMatrix、视图投影矩阵viewProjMatrix、模型矩阵modelMatrix ;然后使用setPerspective()和lookAt()计算视图投影矩阵
为了实现通过鼠标控制物体位置变化,需要用的模型矩阵,用鼠标获取的坐标信息设置模型矩阵modelMatrix
模型变换效果也要传给着色器,所以需要将g_MvpMatrix和modelMatrix相乘 - 模型坐标变换矩阵的逆矩阵,该矩阵只需要将模型矩阵modelMatrix取逆即可
具体实现过程参阅以下代码
//获取uniform变量模型视图投影矩阵、模型坐标变换矩阵的逆矩阵、平行光颜色、平行光方向
var uniformLocations = {
uMvpMatrix: gl.getUniformLocation(prg, 'uMvpMatrix'),
uInvMatrix: gl.getUniformLocation(prg, 'uInvMatrix'),
u_LightColor: gl.getUniformLocation(prg, 'u_LightColor'),
uLightDirection: gl.getUniformLocation(prg, 'uLightDirection'),
}
//给顶点着色器uniform变量u_LightColor- 光线颜色传值(1.0,1.0,1.0)
gl.uniform3f(uniformLocations.u_LightColor, 1.0, 1.0, 1.0)
var currentAngle = [0.0, 0.0]; //当前旋转的角度[x-axis, y-axis]
var g_MvpMatrix = new Matrix4(); //模型视图投影矩阵
var viewProjMatrix = new Matrix4(); //创建视图投影矩阵
var modelMatrix = new Matrix4(); //创建模型矩阵
var invMatrix = new Matrix4(); //创建模型矩阵
var lightDirection = [0, 30, 40]; //光照方向
viewProjMatrix.setPerspective(45.0, canvas.width / canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(0.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
...
(function tick() {
// gl初始化
gl.clearColor(0.0, 0.0, 0.0, 1.0); //指定调用 clear() 方法时使用的颜色值
gl.clearDepth(1.0); //设置深度清除值
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //清空颜色和深度缓冲区
//计算模型视图投影矩阵
g_MvpMatrix.set(viewProjMatrix); //设置视图投影矩阵
modelMatrix.setRotate(currentAngle[0], 1.0, 0.0, 0.0); //沿X轴旋转设置矩阵
modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); //沿Y轴旋转设置矩阵
g_MvpMatrix.multiply(modelMatrix) //相乘模型变换矩阵
//计算模型坐标变换矩阵的逆矩阵
invMatrix.setInverseOf(modelMatrix)
//向着色器传值模型视图投影矩阵uMvpMatrix、模型坐标变换矩阵的逆矩阵uInvMatrix、光线方向uLightDirection
gl.uniformMatrix4fv(uniformLocations.uMvpMatrix, false, g_MvpMatrix.elements);
gl.uniformMatrix4fv(uniformLocations.uInvMatrix, false, invMatrix.elements);
gl.uniform3fv(uniformLocations.uLightDirection, lightDirection)
//绘图
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
gl.flush();
requestAnimationFrame(tick)
})();
4. demo代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<!--通过canvas标签创建一个800px*800px大小的画布-->
<canvas id="webgl" width="800" height="800"></canvas>
<script type="text/javascript" src="./lib/cuon-matrix.js"></script>
<script>
//顶点着色器
var VSHADER_SOURCE = `
attribute vec3 position; //顶点位置信息
attribute vec4 color; //颜色
attribute vec3 normal; //法线
uniform mat4 uMvpMatrix; //模型视图投影矩阵
uniform mat4 uInvMatrix;//模型坐标变换矩阵的逆矩阵
uniform vec3 uLightDirection;//平行光方向
uniform vec3 u_LightColor;//光线颜色
varying vec4 vColor; //向片元着色器传值颜色信息
void main(){
vec3 invLight = normalize(uInvMatrix*vec4(uLightDirection,0.0)).xyz;
float diffuse = clamp(dot(normal,invLight),0.1,1.0);
vColor = vec4(vec3(u_LightColor),1.0)*color*vec4(vec3(diffuse),1.0);
gl_Position = uMvpMatrix*vec4(position,1.0); //将模型视图投影矩阵与顶点坐标相乘赋值给顶点着色器内置变量gl_Position
}
`
//片元着色器
var FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float; // 设置float类型为中精度
#endif
varying vec4 vColor; //接收顶点着色器传送的颜色信息
void main(){
gl_FragColor = vColor; //将接收的颜色信息赋值给内置变量gl_FragColor
}
`
onload = function () {
//通过getElementById()方法获取canvas画布
var canvas = document.getElementById('webgl');
//通过方法getContext()获取WebGL上下文
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
//创建程序对象
var prg = createProgram(VSHADER_SOURCE, FSHADER_SOURCE);
//获取顶点位置、法线、颜色的存储地址
var attLocations = {
position: gl.getAttribLocation(prg, 'position'),
normal: gl.getAttribLocation(prg, 'normal'),
color: gl.getAttribLocation(prg, 'color'),
}
//每个顶点属性的大小(分量数)
var attStrides = {
position: 3,
normal: 3,
color: 4,
}
// 生成绘制甜圈圈的信息
var torusData = torus(50, 50, 3.0, 8.0);
var position = torusData[0];
var normal = torusData[1];
var color = torusData[2];
var index = torusData[3];
// 创建存放顶点、法线、颜色的VBO
var vbos = {
position: create_vbo(position),
normal: create_vbo(normal),
color: create_vbo(color),
}
// 设置VBO
set_attribute(vbos, attLocations, attStrides);
// 创建IBO
var ibo = create_ibo(index);
// IBO绑定
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
//获取uniform变量模型视图投影矩阵、模型坐标变换矩阵的逆矩阵、平行光颜色、平行光方向
var uniformLocations = {
uMvpMatrix: gl.getUniformLocation(prg, 'uMvpMatrix'),
uInvMatrix: gl.getUniformLocation(prg, 'uInvMatrix'),
u_LightColor: gl.getUniformLocation(prg, 'u_LightColor'),
uLightDirection: gl.getUniformLocation(prg, 'uLightDirection'),
}
//给顶点着色器uniform变量u_LightColor- 光线颜色传值(1.0,1.0,1.0)
gl.uniform3f(uniformLocations.u_LightColor, 1.0, 1.0, 1.0)
var currentAngle = [0.0, 0.0]; //当前旋转的角度[x-axis, y-axis]
var g_MvpMatrix = new Matrix4(); //模型视图投影矩阵
var viewProjMatrix = new Matrix4(); //创建视图投影矩阵
var modelMatrix = new Matrix4(); //创建模型矩阵
var invMatrix = new Matrix4(); //创建模型矩阵
var lightDirection = [0, 30, 40]; //光照方向
viewProjMatrix.setPerspective(45.0, canvas.width / canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(0.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
gl.enable(gl.DEPTH_TEST); //开启隐藏面消除
gl.depthFunc(gl.LEQUAL); //如果传入值小于或等于深度缓冲区值,则通过
gl.enable(gl.CULL_FACE); //激活多边形正反面剔除
(function tick() {
// gl初始化
gl.clearColor(0.0, 0.0, 0.0, 1.0); //指定调用 clear() 方法时使用的颜色值
gl.clearDepth(1.0); //设置深度清除值
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //清空颜色和深度缓冲区
//计算模型视图投影矩阵
g_MvpMatrix.set(viewProjMatrix); //设置视图投影矩阵
modelMatrix.setRotate(currentAngle[0], 1.0, 0.0, 0.0); //沿X轴旋转设置矩阵
modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); //沿Y轴旋转设置矩阵
g_MvpMatrix.multiply(modelMatrix) //相乘模型变换矩阵
//计算模型坐标变换矩阵的逆矩阵
invMatrix.setInverseOf(modelMatrix)
//向着色器传值模型视图投影矩阵uMvpMatrix、模型坐标变换矩阵的逆矩阵uInvMatrix、光线方向uLightDirection
gl.uniformMatrix4fv(uniformLocations.uMvpMatrix, false, g_MvpMatrix.elements);
gl.uniformMatrix4fv(uniformLocations.uInvMatrix, false, invMatrix.elements);
gl.uniform3fv(uniformLocations.uLightDirection, lightDirection)
//绘图
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
gl.flush();
requestAnimationFrame(tick)
})();
initEventHandlers(canvas, currentAngle) //注册鼠标事件
//创建程序对象
function createProgram(vshader, fshader) {
//创建顶点着色器对象
var vertexShader = loadShader(gl.VERTEX_SHADER, vshader);
//创建片元着色器对象
var fragmentShader = loadShader(gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null
}
//创建程序对象program
var program = gl.createProgram();
if (!gl.createProgram()) {
return null
}
//分配顶点着色器和片元着色器到program
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
//链接program
gl.linkProgram(program);
//检查程序对象是否连接成功
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('程序对象连接失败: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null
}
//使用program
gl.useProgram(program);
gl.program = program;
//返回程序program对象
return program
}
function loadShader(type, source) {
// 创建顶点着色器对象
var shader = gl.createShader(type);
if (shader == null) {
console.log('创建着色器失败');
return null
}
// 引入着色器源代码
gl.shaderSource(shader, source);
// 编译着色器
gl.compileShader(shader);
// 检查顶是否编译成功
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('编译着色器失败: ' + error);
gl.deleteShader(shader);
return null
}
return shader
}
function initEventHandlers(canvas, currentAngle) {
var dragging = false; //默认鼠标拖动不旋转物体
var lastX = -1,
lastY = -1; //鼠标最后的位置
canvas.onmousedown = function (ev) { //注册鼠标按下事件
var x = ev.clientX,
y = ev.clientY;
//鼠标在物体上开始拖动
var rect = ev.target.getBoundingClientRect();
if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
lastX = x;
lastY = y;
dragging = true;
}
}
//鼠标松开拖动结束
canvas.onmouseup = function (ev) {
dragging = false;
}
canvas.onmousemove = function (ev) { //注册鼠标移动事件
var x = ev.clientX,
y = ev.clientY;
if (dragging) {
var factor = 100 / canvas.height; //旋转因子
var dx = factor * (x - lastX);
var dy = factor * (y - lastY);
//沿Y轴的旋转角度控制在-90到90度之间
currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0);
currentAngle[1] = currentAngle[1] + dx;
}
lastX = x;
lastY = y;
}
}
// 创建VBO
function create_vbo(data) {
//创建缓冲区对象
var vbo = gl.createBuffer();
//绑定缓冲区到ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
//将数据写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
//解绑缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vbo
}
// 向VBO写入数据
function set_attribute(vbo, attLocation, attStride) {
for (var key in vbo) {
//绑定缓冲区到ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, vbo[key]);
//分配缓存区到指定地址
gl.vertexAttribPointer(attLocation[key], attStride[key], gl.FLOAT, false, 0, 0);
//开启缓冲区
gl.enableVertexAttribArray(attLocation[key]);
}
}
// 创建IBO
function create_ibo(data) {
//创建缓冲区对象
var ibo = gl.createBuffer();
//绑定缓冲区到ELEMENT_ARRAY_BUFFER
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
//将数据写入缓冲区对象
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
//解绑缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return ibo;
}
//生成甜圈圈
//第一个参数表示管道截面圆分段数,第二个参数表示管道圆的分段数,
//第三个参数管道截面圆的半径。第四个参数表示从管道中心到管道截面圆中心的距离
function torus(row, column, irad, orad) {
var position = new Array(),
normal = new Array(),
color = new Array(),
index = new Array();
for (var i = 0; i <= row; i++) {
var r = Math.PI * 2 / row * i; //管道圆上每个分段的弧度
var rr = Math.cos(r);
var ry = Math.sin(r);
for (var ii = 0; ii <= column; ii++) {
var tr = Math.PI * 2 / column * ii;
//每个顶点位置的x、y、z分量
var tx = (rr * irad + orad) * Math.cos(tr);
var ty = ry * irad;
var tz = (rr * irad + orad) * Math.sin(tr);
var rx = rr * Math.cos(tr);
var rz = rr * Math.sin(tr);
position.push(tx, ty, tz);
normal.push(rx, ry, rz);
var tc = hsva(360 / column * ii, 1, 1, 1);
color.push(tc[0], tc[1], tc[2], tc[3]);
}
}
for (i = 0; i < row; i++) {
for (ii = 0; ii < column; ii++) {
r = (column + 1) * i + ii;
index.push(r, r + column + 1, r + 1);
index.push(r + column + 1, r + column + 2, r + 1);
}
}
return [position, normal, color, index];
}
//将HSV颜色转换为RGB颜色
function hsva(h, s, v, a) {
if (s > 1 || v > 1 || a > 1) {
return;
}
var th = h % 360;
var i = Math.floor(th / 60);
var f = th / 60 - i;
var m = v * (1 - s);
var n = v * (1 - s * f);
var k = v * (1 - s * (1 - f));
var color = new Array();
if (!s > 0 && !s < 0) {
color.push(v, v, v, a);
} else {
var r = new Array(v, n, m, m, k, v);
var g = new Array(k, v, v, n, m, m);
var b = new Array(m, m, k, v, v, n);
color.push(r[i], g[i], b[i], a);
}
return color;
}
}
</script>
</body>
</html>
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容