vlambda博客
学习文章列表

WebGL入坑系列:导航方块的制作

引言


目前市面上的Web端轻量化引擎遍地开花,如Autodesk Forge平台,广联达 BIMFace,秉匠BlackHole,大象云,酷家乐等等。


直接操作WebGL API对程序员的要求较高,所以出现了以WebGL为基础进行封装的框架,如为人熟知的threejs, GIS领域的Cesium,游戏框架Babylon.js,医学场景较多的SceneJs等等。


框架的好处是不用太了解底层,只需要知道如何使用封装好的套路即可,如果没有框架的存在,着色器(shader)的编写就可能劝退一大波人,更别提后面的API调用了;缺点也是显而易见的,如果框架出现bug或者某些参数需要调优,是很难定位问题的。一些特定化的需求也不仅仅是框架能满足的。所以,了解一些基础的WebGL API还是非常有必要。


本文是从0开始制作轻量化引擎的第一篇,我也和大多数朋友一样从头开始学习WebGL,我手头上的是《WebGL编程指南》:




这本书作为入门的基础还是比较适合的,我没有看全部的代码示例,只是在遇到问题的时候把这本书当字典翻翻。


导航方块


导航方块的作用顾名思义就是提供模型的转向,它有两个功能:

1. 当拽到场景中的模型时,导航方块也要随动转至对应的方向;

2. 当点击导航方块时,模型要随动转至对应方向。

概括下来就是随动功能。


WebGL入坑系列:导航方块的制作



友情提示:Autodesk对自己的导航方块样式单独申请了专利,所以如果你的商业产品中的导航方块与Autodesk的导航方块样式一致性较高(使用Forge的除外),建议修改规避,防止侵权。


WebGL入坑系列:导航方块的制作


开源示例


上手直接写WebGL我也没这么大本事,首先还是从Github上找找开源示例(非基于threejs的),搜索一圈发现有两个相关的库不错:

https://github.com/Ahmed-Abdelhak/WixBim
https://github.com/xBimTeam/XbimWebUI

第一个代码库WixBim是在第二个xbim的基础上进行一定的修改。主要是突出了模型浏览的功能,我在WixBim的基础上根据我的理解又对navigation cube的代码进行了大量中文注释和一定量的修改,并已上传至Github:

https://github.com/airforce094/Revit2Json

本文也以这个版本进行代码说明,希望在阅读这篇的时候您能下载相应的代码进行对照。


代码解析


WixBim这个代码库的前端主要包含两个部分viewer与navigation cube,如下图所示:

WebGL入坑系列:导航方块的制作

首先来看构造函数:

function Navicube(image) { this._image = image;
//6 faces //6面 this.TOP = 1600000; this.BOTTOM = 1600001; this.LEFT = 1600002; this.RIGHT = 1600003; this.FRONT = 1600004; this.BACK = 1600005;
//8 corners //8顶角 this.TOP_LEFT_FRONT = 1600006; this.TOP_RIGHT_FRONT = 1600007; this.TOP_LEFT_BACK = 1600008; this.TOP_RIGHT_BACK = 1600009; this.BOTTOM_LEFT_FRONT = 1600010; this.BOTTOM_RIGHT_FRONT = 1600011; this.BOTTOM_LEFT_BACK = 1600012; this.BOTTOM_RIGHT_BACK = 1600013;
//12 edges //12棱 this.TOP_LEFT = 1600014; this.TOP_RIGHT = 1600015; this.TOP_FRONT = 1600016; this.TOP_BACK = 1600017; this.BOTTOM_LEFT = 1600018; this.BOTTOM_RIGHT = 1600019; this.BOTTOM_FRONT = 1600020; this.BOTTOM_BACK = 1600021; this.FRONT_RIGHT = 1600022; this.FRONT_LEFT = 1600023; this.BACK_RIGHT = 1600024; this.BACK_LEFT = 1600025;
//初始化标志,默认为false this._initialized = false;
/** * 相对于整个viewer的比率,控制cube的大小 */ this.ratio = 0.1;
/**    * cube选中的高亮值 */ this.highlighting = 1.2;
    /**        * hover状态下的透明度,1为不透明 */ this.activeAlpha = 1.0;
    /**    * 非hover状态下的透明度,默认为0.3 */    this.passiveAlpha = 0.3;     this.position = this.TOP_RIGHT;}

构造函数首先定义了鼠标点击方块各部位的枚举值,让人很疑惑的是为什么是从160000开始的?这里先不急着解答,我们继续往下面看。。

接着又定义了方块的初始化标记,尺寸大小,高亮值,hover与非hover的透明值,以及方块初始的显示位置。


初始化函数较长,需要耐心理解。

1. 获取viewer的gl,然后方块的shader初始化

var self = this;this.viewer = viewer;this._alpha = this.passiveAlpha;this._selection = 0.0;var gl = this.viewer._gl;this._shaderprogram = null;//初始化Shader,调用useprogram后即可对navicube中的变量进行赋值操作this._initShader();

这里的_initShader() 是标准的7步骤:

//#region initshaderNavicube.prototype._initShader = function () {
//define compile function var gl = this.viewer._gl; var viewer = this.viewer; var compile = function (shader, code) { gl.shaderSource(shader, code); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { viewer._error(gl.getShaderInfoLog(shader)); return null; } }
/*1. create shader / 2. shader source / 3. compile shader / 4. create program / 5. attach shader / 6. link program / 7. use program */
//fragment shader var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); compile(fragmentShader, Shaders.cube_fshader);
//vertex shader (the more complicated one) var vertexShader = gl.createShader(gl.VERTEX_SHADER); compile(vertexShader, Shaders.cube_vshader);
//link program this._shaderprogram = gl.createProgram(); gl.attachShader(this._shaderprogram, vertexShader); gl.attachShader(this._shaderprogram, fragmentShader); gl.linkProgram(this._shaderprogram);
if (!gl.getProgramParameter(this._shaderprogram, gl.LINK_STATUS)) { viewer._error('Could not initialise shaders for a navicube plugin'); }
//use program gl.useProgram(this._shaderprogram);};//#endregion


可以看到代码中提到了fragment shader (片元着色器) 与vertex shader (顶点着色器)。顶点着色器负责根据坐标与大小在Canvas中画点,而片元着色器负责给各个点进行上色或者一些特效处理:

WebGL入坑系列:导航方块的制作

着色器代码一般是用C语言编写的,然后通过文本格式保存成js文件或者直接在js块中输入着色器代码。我们先看看两个着色器代码中有哪些变量,内部逻辑先放在后面说,直接上手容易劝退 :)

//片元着色器precision mediump float;
uniform float uAlpha;uniform sampler2D uSampler;uniform bool uColorCoding;uniform float uHighlighting;
varying vec2 vTexCoord;varying vec4 vIdColor;
//顶点着色器attribute highp vec3 aVertex;uniform mat3 uMvpMatrix;uniform mat4 uPMatrix;
attribute highp vec2 aTexCoord;varying vec2 vTexCoord;
attribute highp float aId;uniform bool uColorCoding;uniform float uSelection;varying vec4 vIdColor;


在上面两个着色器代码中可以看到有些变量的名称是一样的,但是变量类型却不一样。这里要说明下,attribute变量只有顶点着色器才使用;uniform顾名思义就是一致的,那么如果两个着色器都要使用的统一变量就可以用uniform;那么varying类型呢?varying是用来从顶点着色器传值给片元着色器使用的。一个是一致的数据,一个是传递的数据,理解一下。


//获取vshader相关变量this.u_mvpMatrixPointer = gl.getUniformLocation(this._shaderprogram, "uMvpMatrix");this.u_pMatrixPointer = gl.getUniformLocation(this._shaderprogram, "uPMatrix");this.u_colourCodingPointer = gl.getUniformLocation(this._shaderprogram, "uColorCoding");this.u_selectionPointer = gl.getUniformLocation(this._shaderprogram, "uSelection");
//配置缓存区及对应变量//顶点this._vertexBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.STATIC_DRAW);this.a_vertexPointer = gl.getAttribLocation(this._shaderprogram, "aVertex"),//开启变量a_vertexPointergl.enableVertexAttribArray(this.a_vertexPointer); 
//顶点索引this._indexBuffer = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW);
//方位IDthis._idBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, this._idBuffer);gl.bufferData(gl.ARRAY_BUFFER, this.ids(), gl.STATIC_DRAW);this.a_idPointer = gl.getAttribLocation(this._shaderprogram, "aId"),//开启变量a_vertexPointergl.enableVertexAttribArray(this.a_idPointer);


当有很多点的时候,我们需要使用缓冲区进行渲染。在上述代码中有三个数据需要使用缓冲区:顶点位置,顶点索引,方位ID(就是上面提到方块各部位的枚举值)这三个数据集是一一对应的。


我们可以看到配置缓冲区有三个步骤:

a. createBuffer()  //创建

b. bindBuffer()   //将创建的Buffer进行绑定

c. bufferData()   //将数据集塞到Buffer中


数据集是塞到Buffer中了,但没有和着色器中的变量产生什么关系,这里只是开启了变量:

this.a_vertexPointer = gl.getAttribLocation(this._shaderprogram, "aVertex"),//开启变量a_vertexPointergl.enableVertexAttribArray(this.a_vertexPointer);

那究竟什么时候把Buffer中的数据逐点扔给着色器变量呢?不急,接着往下面看 :)


3. 给导航方块穿件衣服,配置下纹理

穿衣服这件事有点讲究,需要进行一次坐标映射:

WebGL入坑系列:导航方块的制作

(图片来自《WebGL编程指南》)


要把一张图片从s-t坐标系映射到WebGL的x-y坐标上,还是需要有对应的点数据集,在本代码中,纹理坐标数据名为txtCoords,同样需要创建缓冲区:

//创建缓冲区,将纹理坐标写入缓冲区对象this._texCoordBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, this._texCoordBuffer);gl.bufferData(gl.ARRAY_BUFFER, this.txtCoords, gl.STATIC_DRAW);//获取aTexCoord遍量//vshader:aTexCoord -> vshader:vTexCoord -> fshader:vTexCoordthis.a_texCoordPointer = gl.getAttribLocation(this._shaderprogram, "aTexCoord"),//开启aTexCoord变量,链接该变量与_texCoordBuffer缓冲区gl.enableVertexAttribArray(this.a_texCoordPointer);

需要注意的是上述代码中缓冲区与对应的变量均没有产生关联,只是创建缓冲区和塞数据,以及开启变量。他们发生关联的地方在后面,请耐心!


除了纹理的坐标点需要与WebGL点进行映射之外,还有一个问题,就是这个图片作为纹理本身的属性该怎么设置:

/* load image texture into GPU* 如果我们使用了非2的n次方的图片(即图片的宽和高不是2的n次方),会有下面的一些限制:* 不能使用MipMap映射;*  在着色器中采样纹理贴图时:纹理过滤方式只能用最近点或线性, 不能使用重复模式。*  此函数并未分配纹理单元,将在draw()中完成该操作*/var loadimage = function () { //由于WebGL纹理坐标系统中的t轴的方向和PNG、BMP、JPG等格式图片的坐标系的Y轴方向相反。因此,只有将图像的Y轴进行反转,才能够正确地将图像映射到图形上。  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);  //绑定纹理对象 gl.bindTexture(gl.TEXTURE_2D, self._texture);  //配置纹理参数,均为默认  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);  //配置纹理图像  //如果纹理图片是JPG格式,该格式将每个像素用RGB三个分量表示,所以参数指定为gl.RGB。其他格式,例如PNG为gl.RGBA、BMP格式为gl.RGB。   gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, self._image);   //使用mipmap纹理减少锯齿   //优点:模型无论是远离还是离摄像机较近时,显示都会比较自然;渲染效率更高;   //缺点:内存使用会增大为单张图片的1/3;   gl.generateMipmap(gl.TEXTURE_2D);};


有了loadimage(),我们可以放心绑定纹理了:

//create texturethis._texture = gl.createTexture();//fshader-用于接收透明度值this.u_alphaPointer = gl.getUniformLocation(this._shaderprogram, "uAlpha");//fshader-用于接收高亮值this.u_highlightingPointer = gl.getUniformLocation(this._shaderprogram, "uHighlighting");//fshader-用于接收纹理图像this.u_SamplerPointer = gl.getUniformLocation(this._shaderprogram, "uSampler");

if (typeof (this._image) === "undefined") { var data = CubeTextures["en"];    var image = new Image(); self._image = image; //加载图像的过程是异步的,需要使用load监听事件 image.addEventListener("load", function () { loadimage();    }); image.src = data;} else {    loadimage();}


4.添加鼠标在viewer的事件监听

主要有mousemove,mousedown 与 mouseup事件,这些事件的主要作用是获取在模型转动时方块需要随动到的方向ID。简述就是,模型转到哪儿,得到这个转的方向ID,然后让方块根据这个ID也转到这个位置。

WebGL入坑系列:导航方块的制作

*分割线*

初始化到这里就算结束了。

前面我们创建了这么多Buffer,也开启了对应的变量,但Buffer和变量之间没产生关系啊,所以下面我们来看看draw()是如何让他们有关联的:

Navicube.prototype.draw = function () {    if (!this._initialized) return;    var gl = this.viewer._gl; //#region  //设置可视空间pMatrix,navicube不应该使用远小近大的透射投影perspective,而应该使用正射投影ortho  //(left - right - bottom - top - near - far) var pMatrix = mat4.create(); var height = 1.0 / this.ratio; var width = height * this.viewer._width / this.viewer._height;
//根据position的设置将cube的正交投影到相应位置 //若要保持照相机的横纵比例,(right-left)与(top-bottom)的比例为1:1。 switch (this.position) {
//左上 case this.TOP_LEFT: mat4.ortho(pMatrix, -1.0 * this.ratio * width, (1.0 - this.ratio) * width, (this.ratio - 1.0) * height, this.ratio * height, -1, 1); break;
//左下 case this.BOTTOM_LEFT: mat4.ortho(pMatrix, -1.0 * this.ratio * width, (1.0 - this.ratio) * width, this.ratio * -1.0 * height, (1.0 - this.ratio) * height, -1, 1); break; //右上 case this.TOP_RIGHT: mat4.ortho(pMatrix, (this.ratio - 1.0) * width, this.ratio * width, (this.ratio - 1.0) * height, this.ratio * height, -1, 1); break;
//右下 case this.BOTTOM_RIGHT: mat4.ortho(pMatrix, (this.ratio - 1.0) * width, this.ratio * width, this.ratio * -1.0 * height, (1.0 - this.ratio) * height, -1, 1); break; default: }
//正交投影矩阵 gl.uniformMatrix4fv(this.u_pMatrixPointer, false, pMatrix); //模型视图矩阵 即:视角(视图矩阵) * 平移/缩放/旋转(模型矩阵) var mvpMatrix = mat3.fromMat4(mat3.create(), this.viewer._mvMatrix); gl.uniformMatrix3fv(this.u_mvpMatrixPointer, false, mvpMatrix); //#endregion //cube alpha gl.uniform1f(this.u_alphaPointer, this._alpha); //highlight gl.uniform1f(this.u_highlightingPointer, this.highlighting); //cube direction selection gl.uniform1f(this.u_selectionPointer, this._selection); //bind data buffers (之前已经开启了) //将缓冲区_vertexBuffer中的数据赋给a_vertexPointer gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer); gl.vertexAttribPointer(this.a_vertexPointer, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this._idBuffer); //将缓冲区_idBuffer中的数据赋给a_idPointer gl.vertexAttribPointer(this.a_idPointer, 1, gl.FLOAT, false, 0, 0);
//#region 纹理处理 gl.bindBuffer(gl.ARRAY_BUFFER, this._texCoordBuffer); //将缓冲区_texCoordBuffer中的数据赋给a_texCoordPointer gl.vertexAttribPointer(this.a_texCoordPointer, 2, gl.FLOAT, false, 0, 0); //激活0号纹理单元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._texture); gl.uniform1i(this.u_SamplerPointer, 0); //#endregion //指定正面和/或背面多边形是否可以剔除(默认背面),此处应该开启剔除 var cfEnabled = gl.getParameter(gl.CULL_FACE); if (!cfEnabled) gl.enable(gl.CULL_FACE);
//绘制cube gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer); //对每个索引值依次操作,绘制三角面 gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_SHORT, 0);
//关闭剔除 if (!cfEnabled) gl.disable(gl.CULL_FACE);};


看了是不是要吐血,怎么前面还夹了这么多货WebGL入坑系列:导航方块的制作。好了,又要开始搞脑子了。希望大家在学习WebGL的时候能像美剧《后翼弃兵》的女主一样,在脑海中一直要有三维坐标的概念。


这段代码非常重要,它揭示了这个导航方块是为何能被绘制出来!


1.计算正交投影矩阵

在WebGL中有两类常用的可视空间:盒状空间与金字塔可视空间。前者由正交投影产生,后者有透视投影产生。在我们日常观看物体的时候会有近大远小的感觉,所以viewer中的构件可以使用透射投影;而导航方块只作为导航使用,如果做成近大远小反而觉得奇怪,所以其可以使用正交投影。


假设方块的高度:

var height = 1.0 / this.ratio;

那么根据宽高比可以得出方块的宽度:

var width = height * this.viewer._width / this.viewer._height;

方块的位置有四个可选项:左上、左下、右上、右下。

假设我们的方块在左上位置:

case this.TOP_LEFT:

那么它的正交矩阵为:

//left - right - bottom - top - near - farmat4.ortho(pMatrix,           -1.0 * this.ratio * width           (1.0 - this.ratio) * width,           (this.ratio - 1.0) * height,            this.ratio * height,            -1            1);

这是怎么得出的呢?

导航方块的远近大小一致,则(right-left)与(top-bottom)的比例为1:1。同时需要考虑到WebGL的坐标是向右为X轴坐标正方向,向上为Y轴坐标正方向,故得此正交投影矩阵。


2.对Shader中的变量进行赋值以及将缓冲区与变量进行关联

gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);gl.vertexAttribPointer(this.a_vertexPointer, 3, gl.FLOAT, false, 0, 0);

因为之前缓冲区已经塞入了数据,并且相对应的变量已经开启,但缓冲区中的数据还没有放入Shader中。所以使用gl.vertexAttribPointer()告诉显卡从当前绑定的缓冲区(bindBuffer()指定的缓冲区)中读取顶点数据。


3.根据顶点索引找到相应的点依次绘制三角形得到导航方块

//绘制cubegl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);//对每个索引值依次操作,绘制三角面gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_SHORT, 0);


*分割线*

说完绘制cube,再说下导航方块的最后一个功能:当方块接收到一个方向ID参数后该怎么旋转使这个方块转到该方向。


在说明这个函数之前,我们先来看看这个cube的顶点,顶点索引,纹理坐标,方向ID这四个参数是怎么一一初始化的。


我们以Front面为例:


顶点坐标:

// Front face/* 0 ____1* / /*  3/___/2*/-0.3, -0.5, -0.3,0.3, -0.5, -0.3,0.3, -0.5, 0.3,-0.3, -0.5, 0.3,

顶点索引:

 0, 1, 2, 0, 2, 3, // Front face

纹理坐标:

// Front face1.0 / 3.0 + 1.0 / 15.0, 0.0 / 3.0 + 1.0 / 15.0,2.0 / 3.0 - 1.0 / 15.0, 0.0 / 3.0 + 1.0 / 15.0,2.0 / 3.0 - 1.0 / 15.0, 1.0 / 3.0 - 1.0 / 15.0,1.0 / 3.0 + 1.0 / 15.0, 1.0 / 3.0 - 1.0 / 15.0,

方向ID

this.FRONT, // Front facethis.FRONT,this.FRONT,this.FRONT,

从顶点坐标来看,Front面的四个点的Y坐标都是负的,说明它是这样的:

WebGL入坑系列:导航方块的制作

源代码这里真是好奇怪的设定WebGL入坑系列:导航方块的制作

我们再来看看纹理坐标,首先看它的贴图:


WebGL入坑系列:导航方块的制作


这是一个512*512的图片,宽和高都是2的n次方,所以可以使用MipMap映射以减少锯齿,详见前面提到的loadimage()。

顶点坐标与纹理坐标需要一一对应的,不然衣服就穿歪了。所以可以得到这样的示意图:


WebGL入坑系列:导航方块的制作


说完点对应的事情,后面这个Pick()就好分析多了。

Navicube.prototype.onBeforePick = function (id) { if (id >= this.TOP && id <= this.BACK_LEFT) {
var dir = vec3.create(); var distance = this.viewer._distance; //如果非正面视角,让物体增加些距离感,故乘以一个系数 var diagonalRatio = 1.3;
switch (id) { case this.TOP: this.viewer.show('top'); return true; case this.BOTTOM: this.viewer.show('bottom'); return true; case this.LEFT: this.viewer.show('left'); return true; case this.RIGHT: this.viewer.show('right'); return true; case this.FRONT: this.viewer.show('front'); return true; case this.BACK: this.viewer.show('back'); return true;
case this.TOP_LEFT_FRONT: dir = vec3.fromValues(-1, -1, 1); distance *= diagonalRatio; break; case this.TOP_RIGHT_FRONT: dir = vec3.fromValues(1, -1, 1); distance *= diagonalRatio; break; case this.TOP_LEFT_BACK: dir = vec3.fromValues(-1, 1, 1); distance *= diagonalRatio; break; case this.TOP_RIGHT_BACK: dir = vec3.fromValues(1, 1, 1); distance *= diagonalRatio; break; case this.BOTTOM_LEFT_FRONT: dir = vec3.fromValues(-1, -1, -1); distance *= diagonalRatio; break; case this.BOTTOM_RIGHT_FRONT: dir = vec3.fromValues(1, -1, -1); distance *= diagonalRatio; break; case this.BOTTOM_LEFT_BACK: dir = vec3.fromValues(-1, 1, -1); distance *= diagonalRatio; break; case this.BOTTOM_RIGHT_BACK: dir = vec3.fromValues(1, 1, -1); distance *= diagonalRatio; break; case this.TOP_LEFT: dir = vec3.fromValues(-1, 0, 1); distance *= diagonalRatio; break; case this.TOP_RIGHT: dir = vec3.fromValues(1, 0, 1); distance *= diagonalRatio; break; case this.TOP_FRONT: dir = vec3.fromValues(0, -1, 1); distance *= diagonalRatio; break; case this.TOP_BACK: dir = vec3.fromValues(0, 1, 1); distance *= diagonalRatio; break; case this.BOTTOM_LEFT: dir = vec3.fromValues(-1, 0, -1); distance *= diagonalRatio; break; case this.BOTTOM_RIGHT: dir = vec3.fromValues(1, 0, -1); break; case this.BOTTOM_FRONT: dir = vec3.fromValues(0, -1, -1); distance *= diagonalRatio; break; case this.BOTTOM_BACK: dir = vec3.fromValues(0, 1, -1); distance *= diagonalRatio; break; case this.FRONT_RIGHT: dir = vec3.fromValues(1, -1, 0); distance *= diagonalRatio; break; case this.FRONT_LEFT: dir = vec3.fromValues(-1, -1, 0); distance *= diagonalRatio; break; case this.BACK_RIGHT: dir = vec3.fromValues(1, 1, 0); distance *= diagonalRatio; break; case this.BACK_LEFT: dir = vec3.fromValues(-1, 1, 0); distance *= diagonalRatio; break; default: break; }
var o = this.viewer._origin; var origin = vec3.fromValues(o[0], o[1], o[2]);
dir = vec3.normalize(vec3.create(), dir); var shift = vec3.scale(vec3.create(), dir, distance); var camera = vec3.add(vec3.create(), origin, shift);
//初始化的时候Front面在底部,所以上方向是躺着的,即(0,0,1) var heading = vec3.fromValues(0, 0, 1); mat4.lookAt(this.viewer._mvMatrix, camera, origin, heading); return true; } return false;};

根据不同的方向ID,计算出一个distance是为了让viewer中的构件与navicube更有一些距离感。


当我们观察一个物体的时候,除了相机(人眼),目标点外,还有一个重要的参数就是上方向。如果没有上方向,人的头部可以对着一个目标点左歪右歪,那得到的目标画面也是不同的,所以一定要确定上方向。

WebGL入坑系列:导航方块的制作

图片来自《WebGL编程指南》


值得注意的是一般我们在写WebGL的程序时候上方向一般指定(0,1,0)。但是在本案例中却是(0,0,1),这是因为之前提到的Front面其实是在底部,所以需要以躺着的视角去观察,这个时候Front面就面向我们了。


写在最后


终于写完了这篇文章,说实在话学习的时候还是有点懵的,但静下心来多画画可能就会发现,哦~,原来是这样的。其实本文中还有一些细节还没解释,如:shader是如何编写的?为什么方向ID是从160000开始的?navicube作为viewer的plugin该何时调用其函数?这些问题我们放到WebGL入坑系列的下一篇来讲。






去年参加的AU2020大师汇意外地获得了Top Speaker 荣誉,感谢大家对我的支持及对Autodesk Revit开发的热爱!

https://www.autodesk.com/autodesk-university/blog/Best-AU-2020-Speaker-Awards-2021?__mktvar002=3693102126%7CEML&utm_medium=email&utm_source=nur-newsletter&utm_campaign=3693102biemail-marketing&utm_id=3693102126


WebGL入坑系列:导航方块的制作


想观看课程的老铁可直接进入下面链接:

https://www.autodesk.com/autodesk-university/zh-hans/class/RevitkaifazaiBIMxiangmuzhongdeyanjinjiyurengongzhinengdejiehe-2020



QQ3群:1125105973(262/500,来加这里

QQ2群:326126195(1k已满)

QQ1群:480950299(3k已满)