这是一篇研究计算机图形学的文章。而webGL(几乎没用他的特性)只是一种工具。用canvas2D也能实现相同的功能。
本文记录了研究的体会,难免有错。欢迎有识之士一起讨论研究,不吝赐教。
先上效果图:
(一)向量篇
向量是计算机图形学中最基本的元素。一般由三个坐标组成vector(x, y, z)。他的基本运算有:加法(add),减法(subtract),数乘(multiply),数除(divide),点乘(dot),叉乘(cross),归一化(normalize),length(模运算)。
下面给出vector运算的javascript代码
(function() { Vector = function(x, y, z) { this.x = x; this.y = y; this.z = z; } Vector.prototype = { add: function(v) { return Vector.create(this.x + v.x, this.y + v.y, this.z + v.z); }, substract: function(v) { return Vector.create(this.x - v.x, this.y - v.y, this.z - v.z); }, multiply: function(n) { return Vector.create(this.x * n, this.y * n, this.z * n); }, divide: function(n) { return Vector.create(this.x / n, this.y / n, this.z / n); }, dot: function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; }, cross: function(v) { return Vector.create( this.y * v.z - this.z * v.y, this.z * v.x - this.x * v.z, this.x * v.y - this.y * v.x ); }, length: function() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }, normalize: function() { var len = this.length(); return Vector.create( this.x / len, this.y / len, this.z / len ); }, toString: function() { return "x:" + this.x + "\ny:" + this.y + "\nz:" + this.z; } }; Vector.create = function(x, y, z) { return new Vector(x, y, z); }; })();
(二)几何篇
1. 球和线的交点
要渲染图形,最简单的莫过于球了。
球在数学中的定义:
|| Surface - Center || = radius
而我们看到的球其实是以我们眼睛为起点产生的射线组成的视椎体和球表面的交点集合。
直线方程在解析几何中如下定义:
F(x) = origin + distance * direction (1)
其中origin是直线经过的点,distance是距离,是个数字,direction是单位向量,仅仅表示距离
上面的直线方程可以理解为经过origin,方向为direction的点集。
球方程两边平方,得
pow(surface - center, 2) = radius * radius (2)
经(1)(2)式联立:
pow(origin -center + distance * direction, 2) = radius * radius
设v = origin-center
pow(v, 2) + 2 * distance * direction * v + pow(distance * direction, 2) = pow(radius, 2)
direction是单位向量,平方=1
上式等于
pow(v, 2) + 2 * distance * direction * v + pow(direction, 2) = pow(radius, 2)
经二次曲线万能求根公式
a = 1
b = 2 * distance * direction * v
c = pow(v, 2) - pow(radius, 2)
d = pow(b, 2) - 4 * a * c
这里有几个限制。
1. dot(direction, v)必须小于0。因为方向是不一样的。如果>=0代表摄像机反向的镜像。
2. d >=0
3. 由于求的是距离,所以distance必须要>0,否则就是direction有问题。
4. 由于求的是最短距离,所以只需要求一个根
最后的求距离的式子整理如下:
distance = - direction * v - sqrt(pow(direction * v, 2) - pow(v, 2) + pow(radius, 2))
相关实现的GLSL代码:
bool intersectSphere(vec3 center, vec3 lStart, vec3 lDir, out float dist) { vec3 c = lStart - center; float b = dot(c, lDir); /** * 当球心和CameraPos的向量和Ray向量(方向相反)成钝角的时候,才显示到屏幕上 */ if (b > 0.0) { dist = 10000.0; return false; } /* radius:0.1 */ float d = b * b - dot(c, c) + 0.01; if (d < 0.0) { dist = 10000.0; return false; } // 求最短距离,所以sqrt前的符号为- dist = -b - sqrt(d); if (dist < 0.0) { dist = 10000.0; return false; } return true; }
2.平面和线的交点
定义平面上的交点为p(x, y, z)
平面上的任意一点为p1(x1, y1, z1)
平面的法线为VP
直线方程为F(P) = origin + d * direction
对于平面上任意向量(p - p1)有 (p - p1) * VP = 0
将p用直线方程联立有
(origin - p1 + d * direction) * VP = 0
(origin - p1) * VP + d * direction * VP = 0
(p1 - origin) * VP / direction * VP = d
如果d=0,代表没有交点(平行)
如果d<0,说明direction有问题。
判断平面的代码如下:
bool intersectPlane(vec3 lStart, vec3 lDir, out float dist) { vec3 normal = normalize(vec3(0.0, 1.0, 0.0)); float a = dot(lDir, normal); if (a > 0.0) { dist = 10000.0; return false; } float b = dot(normal, lStart - vec3(0.0, 0.0, 0.0)); dist = -b / a; return true; }
(三)场景篇
大家拿着照相机随拍的时候,是不是远处的东西小,近处的东西大?数码照相机屏幕上的画面就是远处那个物体在照相机镜头上的投影。也就是说,从镜头开始,展开了一个视锥体。视锥体的横截面就是照相机的屏幕。要模拟创建这个视锥体。首先定义相机镜头的起始位置
var cameraFrom = Vector.create(0, 0.4, 1);
相机镜头的目标位置
var cameraTo = Vector.create(0, 0, 0);
得到光线向量:cameraDirection = cameraFrom.substract(cameraTo);
利用光线向量创建视椎体
首先利用相机方向和y轴无限向量创建视椎体左边的向量
cameraLeft = cameraDirection.cross(vec3(0, 1, 0)).normalize();
再利用cameraDirection和cameraLeft的叉乘求出视椎体向上的向量,并乘以高宽比
cameraTop = cameraDirection.cross(camerLeft).normalize().multiply(canvas's height / canvas's width);
那么左上角的向量cameraLeftTop = cameraDirection.substract(cameraLeft).add(cameraTop).normalize();
右上角cameraRightTop = cameraDirection.add(cameraLeft).add(cameraTop).normalize();
左下角cameraLeftBottom = cameraDirection.substract(cameraLeft). substract(cameraTop).normalize();
右下角cameraRightBottom = cameraDirection.add(cameraLeft). substract(cameraTop).normalize();
最后补充下webGL的gl.TRIANGLE_STRIP的绘画顺序。按照左上,右上,左下,右下的顺序画出一个四方形(两三角形)
然后,由于webGL的Y轴是和浏览器相反的。
本来绘画的顺序是左上,右上,左下,右下。现在经沿X轴翻转后,顺序变为右上,左上,右下,左下
实例代码如下:
// radio = height / width var radio = canvas.height / canvas.width; var cameraPersp = 2; var cameraDir = cameraTo.substract(cameraFrom).normalize(); // camera line var cameraCenter = cameraFrom.add(cameraDir.multiply(cameraPersp)); var cameraLeft = cameraDir.cross(up).normalize(); var cameraTop = cameraLeft.cross(cameraDir).normalize().multiply(radio); var cameraLeftTop = cameraCenter.add(cameraLeft).add(cameraTop).normalize(); var cameraRightTop = cameraCenter.substract(cameraLeft).add(cameraTop).normalize(); var cameraLeftBottom = cameraCenter.add(cameraLeft).substract(cameraTop).normalize(); var cameraRightBottom = cameraCenter.substract(cameraLeft).substract(cameraTop).normalize(); var corner = []; // Z字按照X轴翻转后的顺序,因为渲染时候的Y和浏览器的Y刚好相反 pushVector(cameraRightTop, corner); pushVector(cameraLeftTop, corner); pushVector(cameraRightBottom, corner); pushVector(cameraLeftBottom, corner); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(corner), gl.STATIC_DRAW); gl.uniform3f(cameraPos, cameraFrom.x, cameraFrom.y, cameraFrom.z); gl.uniform3f(sphere1Center, x1, y1, z1); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimFrame(drawScene);
(四)材质篇
这里涉及到冯氏光照模型的原理:
大致意思为光照=环境光+漫反射+镜面反射。
这部分内容在以后的文章中探讨,敬请期待。
实例的GLSL代码:
// ************* // diffuse light // ************* float diffuseWighting = max(dot(normalize(lightDir), normal), 0.0); // ************* // specular light // ************* vec3 eyeDirection = normalize(-vPosition.xyz); vec3 reflectionDirection = reflect(-lightDir, normal); // factor: 32 float specularLightWeighting = pow(max(dot(reflectionDirection, eyeDirection), 0.0), 32.0); color = ambientColor + diffuseWighting * diffuseColor + specularLightWeighting * specularColor;
(五)关于示例代码启动
win7下任何支持webGL的浏览器都可以直接运行本示例。
XP下请http://www.hiwebgl.com/,点击右边的大按钮,<<我为什么无法运行webGL>>。或者直接点击http://www.hiwebgl.com/?p=628#WebGL-2