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. 当点击导航方块时,模型要随动转至对应方向。
概括下来就是随动功能。
友情提示:Autodesk对自己的导航方块样式单独申请了专利,所以如果你的商业产品中的导航方块与Autodesk的导航方块样式一致性较高(使用Forge的除外),建议修改规避,防止侵权。
开源示例
上手直接写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,如下图所示:
首先来看构造函数:
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;//初始化标志,默认为falsethis._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 functionvar 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 shadervar 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 programthis._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 programgl.useProgram(this._shaderprogram);};//#endregion
可以看到代码中提到了fragment shader (片元着色器) 与vertex shader (顶点着色器)。顶点着色器负责根据坐标与大小在Canvas中画点,而片元着色器负责给各个点进行上色或者一些特效处理:
着色器代码一般是用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编程指南》)
要把一张图片从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也转到这个位置。
*分割线*
初始化到这里就算结束了。
前面我们创建了这么多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 alphagl.uniform1f(this.u_alphaPointer, this._alpha);//highlightgl.uniform1f(this.u_highlightingPointer, this.highlighting);//cube direction selectiongl.uniform1f(this.u_selectionPointer, this._selection);//bind data buffers (之前已经开启了)//将缓冲区_vertexBuffer中的数据赋给a_vertexPointergl.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_idPointergl.vertexAttribPointer(this.a_idPointer, 1, gl.FLOAT, false, 0, 0);//#region 纹理处理gl.bindBuffer(gl.ARRAY_BUFFER, this._texCoordBuffer);//将缓冲区_texCoordBuffer中的数据赋给a_texCoordPointergl.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);//绘制cubegl.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的时候能像美剧《后翼弃兵》的女主一样,在脑海中一直要有三维坐标的概念。
这段代码非常重要,它揭示了这个导航方块是为何能被绘制出来!
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坐标都是负的,说明它是这样的:
源代码这里真是好奇怪的设定。
我们再来看看纹理坐标,首先看它的贴图:
这是一个512*512的图片,宽和高都是2的n次方,所以可以使用MipMap映射以减少锯齿,详见前面提到的loadimage()。
顶点坐标与纹理坐标需要一一对应的,不然衣服就穿歪了。所以可以得到这样的示意图:
说完点对应的事情,后面这个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的程序时候上方向一般指定(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
想观看课程的老铁可直接进入下面链接:
https://www.autodesk.com/autodesk-university/zh-hans/class/RevitkaifazaiBIMxiangmuzhongdeyanjinjiyurengongzhinengdejiehe-2020
QQ3群:1125105973(262/500,来加这里)
QQ2群:326126195(1k已满)
QQ1群:480950299(3k已满)
