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;
//初始化标志,默认为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 initshader
Navicube.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中画点,而片元着色器负责给各个点进行上色或者一些特效处理:
着色器代码一般是用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_vertexPointer
gl.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);
//方位ID
this._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_vertexPointer
gl.enableVertexAttribArray(this.a_idPointer);
当有很多点的时候,我们需要使用缓冲区进行渲染。在上述代码中有三个数据需要使用缓冲区:顶点位置,顶点索引,方位ID(就是上面提到方块各部位的枚举值)这三个数据集是一一对应的。
我们可以看到配置缓冲区有三个步骤:
a. createBuffer() //创建
b. bindBuffer() //将创建的Buffer进行绑定
c. bufferData() //将数据集塞到Buffer中
数据集是塞到Buffer中了,但没有和着色器中的变量产生什么关系,这里只是开启了变量:
this.a_vertexPointer = gl.getAttribLocation(this._shaderprogram, "aVertex"),
//开启变量a_vertexPointer
gl.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:vTexCoord
this.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 texture
this._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 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的时候能像美剧《后翼弃兵》的女主一样,在脑海中一直要有三维坐标的概念。
这段代码非常重要,它揭示了这个导航方块是为何能被绘制出来!
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 - far
mat4.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.根据顶点索引找到相应的点依次绘制三角形得到导航方块
//绘制cube
gl.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 face
1.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 face
this.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已满)