微信号:imwebteam

介绍:腾讯w3tech

前端新玩具——webGL简介

2015-11-18 08:48 devinran

在最初的六天,我创造了天与地

webGL是基于OpenGL的Web3D图形规范,是一套JavaScript的API。简单来说,可以把它看成是3D版的canvas。恩,你会这样引入canvas对吧:

canvas = document.getElementById("xxx");
ctx = canvas.getContext("2d");

SO,3D版本的就酱:

canvas = document.getElementById("xxx");
gl = canvas.getContext("experimental-webgl");

是的,webGL直接使用canvas元素,只是引入一个不同的上下文“experimental-webgl”,方便吧。

这里的上下文实际上应该是.getContext("webgl"),但由于现在webgl标准尚未完善,所以多数浏览器采用一个“试验性”的上下文

让我们先啰嗦一些玩意儿

3D坐标系

这个玩意儿大家都认识吧不多啰嗦了

这里y轴跟canvas是逆向的,这是一个右手坐标系

网格、多边形和顶点

网格(Mesh)是绘制3D图形的一种方法,它是由一个或多个多边形组成的物体,每个顶点的坐标(x,y,z)定义了多边形在3D空间中的位置,这里的多边形通常是三角形和四边形。网格用来描述物体形状。恩,大概......也许......差不多......长这样:

材质、纹理和光源

贴个骷髅头什么的最嗨森了。但仅仅这样是然并卵的,为什么?因为现在毛都看不见。诶不带丢鸡蛋的,诶卧槽你再丢!

为什么说看不见呢,因为视觉是光作用于视网膜细胞所产生的大脑认知,所以我们需要,还需要能反射光的表面。这样网格才能看得见

于是有:

  • 纹理映射(texture map) :物体表面对光的反射,颜色及光泽度等,常由位图来决定。
  • 光源(light) :顾名思义就是闪瞎你的那个东西。常用有环境光、点光源、平行光等,物体表面对光的反射还有环境反射、镜面反射和漫反射。
  • 材质(material) :网格表面的特性的统称。

变换和矩阵

网格的形状是由顶点决定的,而我们做的是动画,难道动画每一帧要重新定义所有网格的所有顶点?显然是不可取的,所以我们需要变换(transform)。变换是不需要遍历每个顶点就可以移动网格的操作,需要由矩阵(matrix)来操作。

类似介种:

相机、透视、视口和投影

我们生活在三维世界中,但是用眼睛只能看到二维的图像。同样的,三维的网格要能够看见,需要渲染成二维图像。

好多好多的概念:

  • 场景(scene) :容纳一切的容器
  • 相机(camera) :就是你在webGL世界里面的眼睛呐。
  • 视口(viewport) :想想浏览器的视口的概念,对,就是3D场景渲染的二维图像,也就是你从浏览器的canvas元素上看到的。
  • 视野(field of view) :相机可见范围左右边界的夹角。
  • 视锥体(view frustum) :物体可以被渲染到视口的空间,换句话说,只有处于视锥体空间内部的物体,才可以被看见。
  • 近裁剪面(near clipping plane) :视锥体靠近相机的一面,其实就是视口。
  • 远裁剪面(far clipping plane) :视锥体最远离相机的平面。

太君别开枪!我知道你最讨厌一大片的概念了,来看图:

这样清楚了吧~~~ ?(终于啰嗦完了......)

数学真难

概念讲完了是不是该开搞了呢?诸位看官别急,且听小生慢慢道来。

大家明白,模拟三维空间,需要非常多的计算,网格的坐标、大小、角度,网格的平移、旋转,相机观察网格的二维映射等等等等。

前方高能(学霸请无视这一行)

《线性代数》乱入:

前面说了,网格由N个多边形构成,实际上就是由多边形的顶点集合构成。顶点是一个向量,而向量可以用一个三维坐标(x, y, z)来表示。矢量之间存在加法、减法、点乘、叉乘运算。(作者抱着《线性代数》一顿狂翻......)

到这里有没有发现一个问题?就是向量和坐标的表示方法是一样的。于是这里引入齐次坐标(w)来区分,w=0,则表示向量,否则表示点。于是我们的向量就长这样:(x, y, z, w)。值得一提的是,齐次坐标表示方法不唯一,(x, y, z, w)跟(x/w, y/w, z/w, 1)表示同一个点,后者为齐次坐标的正常化处理。eg: (15, 12, 9, 3)跟(5, 4, 3, 1)并没有什么区别,都表示三维点(5, 4, 3)。(作者抱着《线性代数》又是一顿狂翻......)

所谓齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示——百度百科

http://baike.baidu.com/link?url=qKIV6kEiCXE-MEdwSFvI3Ew6XkDwoOywt_6oYG8Nu6tdThO1EF9heuS8MDQpujJbXyDErMPsDYBiGNj3FqL9l_

向量坐标说完了,各位看官还记得向量运算的好朋友——矩阵么?什么?不记得?请跟作者一起抱着《线性代数》一顿狂翻......

具体矩阵计算就不细说了,大致有这几个:加法减法乘法求逆转置

Waring:矩阵的乘法 不满足 乘法交换律,所以还分 左乘右乘

我知道各位看官要丢鸡蛋了,讲这么半天线性代数到底有什么卵用啊?恩且慢动手。接下来我们要说重要的东西了。

仿射变换

仿射变换:大概就是对原坐标做一些羞羞的事情然后获取他们新坐标的值。

下面图略丑请凑合看

平移

平移矩阵长这样,tx, ty, tz为位移向量,比如(tx, ty, tz)=(3,2,0),则平移变换效果如下图:

旋转

旋转三个矩阵,分别对应x、y、z轴,这个坐标轴遵循右手法则,右手法则就是:

那么比如我们绕z轴旋转,使用上面的第三个矩阵,旋转90度,效果这样:

缩放

这个运算看上去简单多了对吧嘿嘿嘿

注:上述仿射变换均是用对应的仿射矩阵 左乘 齐次坐标得到结果

好了,讲了半天这个那个矩阵的,《线性代数》已经被学渣作者翻烂,不知道各位看官是什么感觉(学霸:so easy!)

那么问题来了,难道玩图形学的人们天天搞矩阵?不!这不科学!一定不是这样的!程序员是一类神奇的生物,凡是遇到觉得很烦躁很麻烦的东西,都会创造另外一些东西让他们不烦躁不麻烦。这个“另外一些东西”就是:

正经开搞

好了我们要开始创造天与地了,不要担心,我们不会去算矩阵的,难道肚子饿了还要先插秧吗?webGL已经有那么些封装很好的引擎了,这些引擎能够帮助开发者规避矩阵计算等复杂的操作,让你能够专注于天地的创造。这里我们使用Three.js。

Three.js 是一个js编写的第三方库,运行在浏览器中,提供场景、相机、光照、材质等各种对象——http://threejs.org/

首先我们创建一个渲染器并添加到页面上

antialias是一个抗锯齿参数,我们设置了渲染器的宽高,简单吧。

渲染器有了我们就可以渲染场景了,然后往里面丢各种东西,想想还有点小激动呢。建场景就一行

现在世界已经从一片混沌变成天地初开了,我们需要一双发现真善美的眼睛——相机

Three.js最主要的相机一个是正投影相机(OrthographicCamera),这个相机是“上帝视角”,为啥说是上帝视角,因为东西是啥样他看着就是啥样儿。恩,我这样说我知道你肯定没听懂。没事儿我们继续看。

另一个就是我们这里用到的了,透视投影相机(PerspectiveCamera) (并不能把穿了衣服的看成没穿衣服的)。透视投影有一个基本点,远处的物体比近处的物体小。这就是与正投影的区别。还记得前面讲透视时候的那个图吗?

看上面的几个参数,CONST.FIELD_OF_VIEW——视野角度,第二个是宽高比,然后远近裁剪面,想起来了吧~~~

最后把它放在(0, 0, 3)的地方,偶吼吼,盘古大婶睁眼了。

不过现在睁眼然并卵啊,上帝说:要有光!

这里我们创造一组平行光,因为照亮世界的是太阳,物理学角度来说通常把太阳光看成是平行光。

除此之外还有环境光、区域光、点光源和聚光灯。

万事俱备,我们要开始开天辟地的辟地了。

我们先创造一个几何球体(当然同理还有CubeGeometry等等),三个参数,第一个是球体半径,后两个分别是球体在两个方向上的几何精度(其实就是每条线上用多少个顶点描述),这里的横向和纵向都设置为64个顶点。

接下来是定义材质,为了效果更逼真,我们使用着色器来定义材质,需要三张贴图,分别是:

  • 漫反射贴图 :即颜色贴图
  • 法线贴图 :描述材质的凹凸程度
  • 高光贴图 :描述材质的反光效果

这里我们拿到网上有一套非常清晰的地球的图(从左往右依次是法线贴图、高光贴图、漫反射贴图)

只要有了漫反射贴图,我们就可以通过 PixPlant 软件来生成其法线贴图和高光贴图,效果嘛,还行。

我们拿两张来试试,分别是木星和金星的漫反射贴图

经过PixPlant的处理后得到下面几张。是不是很爽?

好我们开始把贴图做成纹理

通过读取图片做成纹理映射,然后把纹理映射给到着色器材质

最后用几何体跟材质生成网格,并倾斜一个小角度方便我们瞅着它

把网格添加到场景中

这样“辟地”就弄好了

是不是感觉跟平常看到的不太一样?

对啊卧槽云呢?咱们的星球那么漂亮,要有云哇!

相同的步骤,我们再做一个网格。只不过这里我们不再需要着色器材质了,因为云层不需要高光法线这些东西。我们使用兰伯特(Lambert)材质,这个材质的特点是无论观察者角度如何变化,它的表面亮度都一样。这个性质用来做我们的云层最棒了。然后我们还要把云层网格设为透明,让它“罩”在地球上,转动比地球快一丢丢,更接近真实。

好了,最后我们使用requestAnimationFrame()函数来让它转起来!

requestAnimationFrame函数是专为脚本动画创建的,使用它可以让浏览器来自动控制动画的最佳帧频,提升性能、节省电能。在这一点上它比setTimeout和setInterval函数更好。

最后大功告成

完整代码

/* * build by rhj 2015/09/27 */
function World(id){
    //各部件Obj
    this.container = document.getElementById(id);
    this.renderer  = null;
    this.scene     = null;
    this.camera    = null;
    this.light     = null;
    this.world     = null;
    this.cloud     = null;

    //常量
    this.constObj  = {
        ANGLE_INCLINED      : Math.PI / 6,
        ROTATION_WORLD_RATE : 0.001,
        ROTATION_CLOUD_RATE : 0.0012,
        FIELD_OF_VIEW       : 45,
        NEAR_CLIPPING_PLANE : 1,
        FAR_CLIPPING_PLANE  : 10,
        WORLD_SHININESS     : 15,

        //云层高约10km,地球半径6371km,云层球体半径R=(6371+10)/6371≈1.0016
        WORLD_RADIUS        : 1,
        CLOUD_RADIUS        : 1.0016,
        GLOBE_RESOLUTION    : 64
    }
}

World.prototype.initRender = function(){
    var container = this.container;
    var renderer  = null;

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(container.offsetWidth, container.offsetHeight);

    this.renderer = renderer;
    this.container.appendChild( this.renderer.domElement );
}

World.prototype.initScene = function(){
    this.scene = new THREE.Scene();
}

World.prototype.initCamera = function(){
    var container = this.container;
    var CONST     = this.constObj;
    var camera    = null;

    camera = new THREE.PerspectiveCamera(
        CONST.FIELD_OF_VIEW, 
        container.offsetWidth/container.offsetHeight,
        CONST.NEAR_CLIPPING_PLANE, 
        CONST.FAR_CLIPPING_PLANE
    );
    //相机坐标
    camera.position.set(0, 0, 3);

    this.camera = camera;

    this.scene.add(this.camera);
}

World.prototype.initLight = function(){
    var light = null;

    light = new THREE.DirectionalLight(0xffffff, 1.5);
    //光源坐标
    light.position.set(0, 0, 1);

    this.light = light;

    this.scene.add(this.light);
}

World.prototype.initWorld = function(){
    var shader   = null;
    var uniforms = null;
    var material = null;
    var geometry = null;

    var CONST = this.constObj;

    var surfaceMap  = THREE.ImageUtils.loadTexture("images/earth_surface.jpg");
    var normalMap   = THREE.ImageUtils.loadTexture("images/earth_normal.jpg");
    var specularMap = THREE.ImageUtils.loadTexture("images/earth_specular.jpg");

    shader   = THREE.ShaderUtils.lib["normal"];
    uniforms = THREE.UniformsUtils.clone(shader.uniforms);

    //法线贴图、漫反射贴图、高光贴图
    uniforms["tNormal"].texture   = normalMap;
    uniforms["tDiffuse"].texture  = surfaceMap;
    uniforms["tSpecular"].texture = specularMap;

    uniforms["enableDiffuse"].value  = true;
    uniforms["enableSpecular"].value = true;

    //物体表面光滑度
    uniforms["uShininess"].value = CONST.WORLD_SHININESS;

    //着色器
    material = new THREE.ShaderMaterial({
        fragmentShader : shader.fragmentShader,
        vertexShader   : shader.vertexShader,
        uniforms       : uniforms,
        lights         : true
    });

    //球体网格(半径、纬线顶点数、经线顶点数)
    geometry = new THREE.SphereGeometry(CONST.WORLD_RADIUS, CONST.GLOBE_RESOLUTION, CONST.GLOBE_RESOLUTION);
    geometry.computeTangents();

    world = new THREE.Mesh(geometry, material);

    world.rotation.x = CONST.ANGLE_INCLINED;
    world.rotation.y = CONST.ANGLE_INCLINED;

    this.world = world;

    this.scene.add(this.world);
}

World.prototype.initCloud = function(){
    var cloudsMap      = null;
    var cloudsMaterial = null;
    var cloudsGeometry = null;

    var CONST = this.constObj;

    cloudsMap      = THREE.ImageUtils.loadTexture("images/earth_clouds.png");
    cloudsMaterial = new THREE.MeshLambertMaterial({color: 0xffffff, map: cloudsMap, transparent: true});

    //云层球体网格(半径、纬线顶点数、经线顶点数)
    cloudsGeometry = new THREE.SphereGeometry(CONST.CLOUD_RADIUS, CONST.GLOBE_RESOLUTION, CONST.GLOBE_RESOLUTION);
    cloud          = new THREE.Mesh(cloudsGeometry, cloudsMaterial);

    cloud.rotation.y = CONST.ANGLE_INCLINED;

    this.cloud = cloud;

    this.scene.add(this.cloud);
}

World.prototype.build = function(){
    this.initRender();
    this.initScene();
    this.initCamera();
    this.initLight();
    this.initWorld();
    this.initCloud();
}

World.prototype.rotate = function(self){
    var CONST = self.constObj;

    self.renderer.render(self.scene, self.camera);
    self.world.rotation.y += CONST.ROTATION_WORLD_RATE;
    self.cloud.rotation.y += CONST.ROTATION_CLOUD_RATE;
    requestAnimationFrame( function(){ self.rotate(self); } );
}

线上演示地址:http://rhj1122.github.io/webgl/

 
IMWEB前端社区 更多文章 前端如何呼风唤雨 【原译】javascript中的错误处理 腾讯SNG技术开放日莫卓颖分享回顾:如何构建高质量、高效率的前端体系 腾讯SNG技术开放日:关注前端分论坛,IMWeb讲师带你听 Qcon2016IMWeb团队fredwu精华回顾:互娱时代下Web音视频性能优化
猜您喜欢 PHP7 的抽象语法树(AST)带来的变化 flash和策略文件 是 WordPress 让 PHP 更流行了,而不是框架 【教学贴】程序员相亲指南~ 15 位健在的牛叉程序员,你知道哪几位?