滤镜算法以及WebGL实现

https://github.com/newbieYoung/NewbieWebArticles/blob/master/colorful-shadow.html(2017-09-22T13:15:47Z)

© Young 2017-08-22 10:41
Welcome to My GitHub

前言

如果你想仔细了解文中WebGL实现的滤镜算法,首先你得具备一些原生WebGL知识,但是如果你只是想大概了解滤镜算法的作用和规律,不具备原生WebGL知识也没关系,建议跳过具体实现,聚焦文字说明、函数、图形、矩阵等;另外下述部分关于滤镜的WebGL实现都是参考于WebGLImageFilter,因此你也可以认为这篇文章有部分内容是对WebGLImageFilter这个类库源代码的简单解析。

引子

几天前设计跟我说来实现稍微不一样的阴影,如下:

简单来说就是某个元素的阴影需要和其内容相关,这就可能导致阴影颜色是渐变的,而且渐变还没有明显规律,显然使用CSS3的box-shadow暂时是没办法实现的了。

稍微思考了一下,其实可以采用重叠方式来模拟,首先复制该元素,然后使用滤镜过滤,最后把过滤之后的元素和原始元素重叠在一起,原始元素在上,过滤元素在下,从而实现和内容相关阴影。

由于CSS3、SVG、Canvas等都直接支持滤镜,因此实现方式也有很多种;

一、CSS3实现

例子一
例子二

二、Canvas实现

例子三
例子四

需要注意的Canvas的filter属性兼容性很不好,因此下边的工具都是使用CSS3 filter来实现的。

另外因为阴影会降低页面的渲染效率,而且页面中的元素不一定是固定不变的,因此静态方式不能完全满足实际需要,所以我封装了一个简单的工具类库colorful-shadow用于动态给某个元素增加内容阴影,

Github上有源代码,在这个项目里边也有简单的测试用例

可以调用init方法来实现自定义的重叠内容阴影;
也可以直接调用bottomContentShadow来实现底部内容投影阴影;
或者调用bottomGradientShadow来实现底部渐变阴影。

另外由于是涉及重叠元素的动态定位,这就要求原始元素必须具有定位属性了,比如:relative、absolute、fixed等。

滤镜不光频繁出现在各种Web技术中,而且在各种图像处理软件中也有很多类似的概念;

由于滤镜实在是太普遍了,作为一个有追求的程序员(出于装逼的需要),仅仅知道怎么用,或者就算知道一些特殊的用法,显然也是远远不够的!

那接下来就聊聊滤镜算法以及实现吧……

blur高斯模糊

详细请看阮一峰 高斯模糊的算法,通俗易懂。

高斯模糊 WebGL实现

主要算法如图:

图中区域A很明显是高斯函数权值,区域B则是模糊范围,但是有个地方需要注意一下,上文中高斯模糊的范围是当前像素点的四周,但是区域B只是对角线区域,为什么会这样呢?

从上述代码可以看到执行高斯模糊算法片元着色器时是分两次执行的,先设置blur.x为0处理纵轴,然后设置blur.y为0处理横轴,初一看起来这样做也只是处理了纵轴和横轴附近的点和处理四周的点依然有差距,其实不然因为是分开处理,最后处理横轴是基于上一次处理纵轴之后的数据,和一次性处理所有四周的效果是一样的。

维基百科上是这么描述的:

高斯模糊也可以在二维图像上对两个独立的一维空间分别进行计算,这叫作线性可分;这也就是说,使用二维矩阵变换得到的效果也可以通过在水平方向进行一维高斯矩阵变换加上竖直方向的一维高斯矩阵变换得到。

contrast对比度

contrast对比度 WebGL实现

主要算法如图:

需要注意一下上述算法中alpha通道没有变化且alpha通道也不会影响其它色值;

变换矩阵如图:

从片元着色器算法和变换矩阵可知contrast对比度算法中变换后色值和变换前色值的函数关系如下:

由此可知随着contrast值的不断增大,大于128的色值会越来越快速的增加到255,小于128的色值会越来越快速的减少到0,简单来说这种算法会让图像中的0-255的中间色值越来越少

稍微了解blur高斯模糊和contrast对比度后,就能理解在下边的这个例子中blur和contrast结合的效果了。



图中1位置即使在blur滤镜的基础上加了contrast滤镜之后依然没有变化是因为contrast滤镜算法中alpha值不会变化也不会影响其它色值,但是2区域确出现了红色,则是因为blackrgb(0,0,0)yellowrgb(255,255,0),那么中间的混合颜色区域肯定存在一部分R大于128G小于128的区域,这段区域在经过contrast对比度算法处理后R大于128的会快速增加为255G小于128的则会快速减少为0,最终呈现为rgb(255,0,0)为红色,contrast值越大红色和黄色的过渡颜色越少。

brightness亮度

brightness亮度 WebGL实现

由于brightness亮度算法也不涉及alpha通道的变化,因此可以和contrast对比度算法共用相同的片元着色器。

变换矩阵如图:

从片元着色器算法和变换矩阵可知brightness亮度算法中变换后色值和变换前色值的函数关系如下:

由此可知随着brightness值的不断增大,所有色值都会越来越快速的增加到255,简单来说这种算法会让小于255的色值越来越少

另外有个地方需要注意下,刚开始我以为brightness值超级大的时候,图像所有色值都会变成255,那图像最终会变成一片空白,然而实际却不是这样,不管brightness值设置成多大,图像中依然有些地方有颜色。

其实仔细看那个函数图就会发现,不管斜率如何增大,色值变化直线始终都会过原点,因此brightness值超级大时,图像中依然存在非空白颜色的地方就是那些一开始色值中就有某些通道为0的地方

其实仔细看上边那个brightness亮度滤镜算法存在一个问题的,也就是这个算法只能增加亮度不能降低亮度,而CSS原生的filter设置brightness值为小数时会降低亮度,主要原因是该算法会在设置的brightness值之后再加上1,导致函数斜率始终大于1。
另外增加亮度就是让所有色值向255(WebGL中是1)靠拢,那么也就意味着亮度滤镜算法并不是固定的,你可以实现其它多种算法。

grayscale灰度

grayscale灰度 WebGL实现

灰度照片只有256种颜色,一般的处理方法是将图像颜色值的RGB三个通道设为一样,这样图像的显示效果就会是灰色;灰度处理有很多中方法,常用的是加权平均值法,即新的颜色值R=G=B=(R * Wr+G*Wg+B*Wb)

一般由于人眼对不同颜色的敏感度不一样,所以三种颜色值的权重不一样,一般来说绿色最高,红色其次,蓝色最低,最合理的取值分别为Wr = 30%,Wg = 59%,Wb = 11%。

HSL、HSV

上边简单介绍了四种滤镜算法,我们大概知道了这些算法是怎么改变RGBA色值,但是我们并不清楚为什么要这么变化。比如brightness亮度滤镜为什么要让小于255的色值越来越少呢?grayscale灰度为什么要让RGB三个通道变为一样呢?

从RGBA颜色表示方法我们很难找到原因,因为RGBA颜色表示方法并不直观和人类感觉颜色的逻辑不太符合,因此诞生了HSL和HSV两种更直观的颜色表示方法。

HSL简单来说就是什么颜色饱和度如何亮度如何
HSV简单来说就是什么颜色深浅如何明暗如何

更多详情请去维基百科查询HSL和HSV色彩空间

之所以要提及这两种颜色表示方法是因为滤镜更多是人们从自身的颜色视角出发对图像进行一些处理的算法,单纯看RGBA的变化会对这种变化感觉比较困惑。

invert反转

invert反转 WebGL实现

invert反转同样没有涉及alpha通道的变化,因此还是和上边的几种滤镜共用相同的片元着色器。

变换矩阵如图:

在我理解来说invert反转滤镜属于色相变化滤镜,hue-rotate色相旋转滤镜(后续会介绍)也属于色相变化滤镜,只不过区别在于变化方式不一样,hue-rotate色相旋转滤镜应该是所有色相绕成一个圆然后旋转变化而invert反转滤镜则是直接翻转变化,从变换矩阵也可以看出。

可以看到从grayscale灰度滤镜开始我就没有再画函数图,并不是因为变懒了,而是因为以前画图是期望从图中找到这种滤镜算法在RGBA中的变化规律从而更好理解的该滤镜算法,但实际上从RGBA的变化规律理解反而会更迷惑;因此上边介绍了两种新的颜色表示方法,正确理解这些滤镜算法的路径应该是把RGBA色值的变化规律转换成HSL或者HSV的变化规律,转换规律可以在维基百科中查询到HSL和HSV色彩空间

未完待续

在MAC操作系统中安装Mxnet并运行其中的梵高绘画风格模拟例子

https://github.com/newbieYoung/NewbieWebArticles/blob/master/install-incubator-mxnet-and-run-testcase.html(2017-08-23T12:43:00Z)

© Young 2017-08-23 17:41
Welcome to My GitHub

概述

前些天偶然间看到大家在讨论某些活动页面风格不一致的情况,然后想到了很久以前看过的一篇文章关于深度学习框架Mxnet的,里边有一个例子能把任意图片转换成梵高的绘画风格,结合上述大家讨论的情况,开了个脑洞;能不能通过这个例子做一个判断页面风格是否一致的工具呢?

  • 首先确定某个页面为标准风格;
  • 然后使用Mxnet框架中的例子把活动页面转换为标准风格;
  • 然后比较该活动页面标准风格和原始风格的相似程度来判断该活动页面的原始风格是否符合既定的标准风格。

折腾了一下午证实了这个脑洞是个失败的脑洞,主要原因是Mxnet框架里边的这个例子中的算法不具备通用性,只能把任意图片转换成“特征“很明显的图片风格。

而不同网页的风格相比不同的绘画风格,其实是差异很小的。

虽然脑洞失败了,但是毕竟折腾了一下午,在安装Mxnet以及运行例子程序时遇到了很多问题,鉴于网上的教程大多不靠谱,还是记录一下,万一后边对深度学习之类有兴趣了呢!

安装

其实安装流程参照官网流程即可,比网上的教程靠谱很多,一般不会出现啥问题。

运行

Github上有很多信息,包括梵高绘画风格模拟例子的怎么运行的指引

但是在执行python nstyle.py时却报错ImportError: No module named skimage

根据错误信息可知,Python中某个叫skimage的模块导入失败,有可能是该模块不存在或者版本不对,此时需要安装或者更新该模块;

执行sudo pip install scikit-image --upgrade

此时还是有问题:

Found existing installation: six 1.4.1
DEPRECATION: Uninstalling a distutils installed project (six) has been deprecated and will be removed in a future version...

大致原因是因为安装skimage时本地已经安装的six库版本过低需要更新,结果更新时没有权限就报错了;

因此需要忽略six安装skimage;

sudo pip install scikit-image --upgrade --ignore-installed six

结果还是报错:

OSError:[Errno 1] Operation not permitted: '/System/*/Python.framework/Version/2.7/share'

新版的MAC操作系统有一个叫SIP的机制禁止任何用户直接修改System目录的文件,解决办法是取消这种机制,具体做法是:

  • 重启电脑,按住Command+R(直到出现苹果标志)进入恢复模式;

  • 左上角菜单中打开终端;

  • 执行csrutil disable

  • 重启电脑即可;

如果想要启用SIP机制,按上述流程,执行csrutil enable即可。

到此应该不存在其它环境问题了,但是运行例子程序时还是报错:

Operator _zeros is not implemented for GPU.

原因在于该例子程序默认是在GPU上执行的,但是我们安装的时候是安装的CPU版本,因此需要在运行指定在CPU上执行;

python nstyle.py --gpu -1

结语

虽说脑洞失败,但这个例子其实还是有那么一点意思的,比如生成一张梵高绘画风格的图片……

WebGL ThreeJS学习总结一、二

https://github.com/newbieYoung/NewbieWebArticles/blob/master/webgl-threejs-1-2.html(2017-08-31T12:35:50Z)

© Young 2016-10-18 07:57
Welcome to My GitHub

概述

在写这篇总结之前,这已经是我第二次尝试学习WebGL了,第一次是在两年前,那时候正在从事Java开发相关工作,觉得没什么意思,然后平时工作中有接触到一些前端相关的东西,刚好那时HTML5很火,就稍微了解了一下Canvas,然后自然而然的知道了WebGL,刚开始学的时候感觉很吃力,然后就买了本书《WebGL入门指南》,虽然跟着书上的例子也能照葫芦画瓢弄个正方体出来,但是总感觉实现了某个例子,依然啥也不会,没多久就放弃了。

现在已经从事前端开发工作接近两年,在前不久看到一篇博客后再次燃起了学习WebGL的热情,然后采购了一本书《WebGL编程指南》,花了一周时间看了100多页之后,差点再次放弃,思考了一下午决定改进下学习方法。

结合这两次的从准备入门到放弃的经验来看,单独学习ThreeJS容易给人造成不知其然的感觉,而单独学习WebGL又太枯燥,所以我决定把二者结合起来,先根据《WebGL编程指南》学习WebGL感觉枯燥之后就参照ThreeJS官方文档以及官方源代码中的例子学习ThreeJS并找些相对容易实现的例子实现,在这过程中写一些学习总结。

总结一主要是了解一些WebGL的基本概念,然后通过示例程序学习WebGL程序的基本结构、GLSL语言以及编写一些简单的WebGL程序。

概念

一、基本概念

1、WebGL起源

在个人计算机上使用最广泛的两种三维图形渲染技术是Direct3D和OpenGL,其中WebGL就是从OpenGL的一个特殊版本OpenGL ES中派生出来的,后者专用于嵌入式计算机、智能手机、家用游戏机等设备。

2、WebGL程序结构

在HTML中动态网页包括HTML和JavaScript两种语言,在引入WebGL后,还需要加入着色器语言GLSL ES。

3、WebGL坐标系

暂时认为WebGL坐标系为右手坐标系,即当你面朝计算机屏幕时,X轴是水平的(正方向为右),Y轴是垂直的(正方向为上),Z轴垂直于屏幕(正方向为外)。

4、WebGL颜色分量取值范围

在平时前端开发中我们一般使用的颜色分量取值范围是从0到255,但是由于WebGL是继承自OpenGL,所以它遵循传统OpenGL颜色分量的取值范围,即从0.0到1.0。

5、WebGL坐标取值范围

坐标取值范围与颜色分量的取值范围一样也是从0.0到1.0。

个人理解:取值范围从0.0到1.0其实可以看成从0%到100%,之所以取值范围是这样,可能是为了适应计算机硬件的发展吧,比如颜色分量取值范围如果固定为某个具体数值,随着硬件的发展能识别更多颜色之后就会有兼容性的问题。

6、齐次坐标

齐次坐标(x,y,z,w)等价于三维坐标(x/w,y/w,z/w),暂时理解为齐次坐标能提高三维数据的运算效率,在GLSL ES中使用vec4类型表示。

7、着色器

WebGL需要两种着色器;

顶点着色器,顶点着色器是用来描述顶点特性(如位置、颜色等)的程序;

片元着色器,进行逐片元处理过程(如光照)的程序。

8、片元

暂时理解成像素即可。

9、网格

绘制3D图形的方法有很多,最常用的一种方法就是使用网格(Mesh),这也是为什么下边的程序示例是绘制三角形的原因。

10、矩阵

虽然上大学时没好好学,但是网上资料挺多,就不一一拷贝了。

理解矩阵乘法

11、顶点着色器和片元着色器之间图形装配和光栅化的过程

  • 执行顶点着色器,传入缓冲区对象中的第一个顶点坐标,一旦赋值成功,该数据就进入了图形装配区域,并暂时存储在那里;

  • 重复执行顶点着色器直到所有顶点数据赋值完成;

  • 开始装配图形(按照一定的规则把所有顶点连接起来);

  • 将图形转化为片元,这个过程被称为光栅化,光栅化是三维图形学的关键技术之一,它负责将矢量的几何图形转变为栅格化的片元

补充解释下光栅化,其实就是把矢量图形转化成像素点的过程,因为最常用的一种绘制3D图形的方法就是使用网格(Mesh),也就是点线面属于矢量图形,而当前屏幕是像素渲染的,那么从矢量图形转化成用户所看到的像素图像必然需要光栅化这一步骤。

  • 光栅化结束之后,程序就开始逐片元调用片元着色器,每调用一次就处理一个片元,片元着色器会计算出该片元的颜色,并写入颜色缓冲区;

  • 当最后一个片元处理完成,浏览器就会显示最终结果。

另外这个过程如果我们使用当前流行的ThreeJS框架可以理解如下:

其中黄色区域是ThreeJS框架中使用JS实现的,绿色部分是THreeJS框架中使用GLSL ES实现的。

12、缓冲区对象

对于那些由多个顶点组成的图形,比如三角形、矩形等,你需要一次性地将图形的顶点全部传入顶点着色器,然后才能把图形画出来,WebGL提供了一种很方便的机制,即缓冲区对象(是WebGL系统中的一块内存区域),它可以一次性地向着色器传入多个顶点数据。

使用缓冲区对象时需要遵循以下五个步骤:

创建缓冲区对象(gl.createBuffer());
绑定缓冲区对象(gl.bindBuffer());
将数据写入缓冲区对象(gl.bufferData());
将缓冲区对象分配给着色器变量(gl.vertexAttriPointer());
启动着色器变量(gl.enableVertexAttriArray())。

使用多个缓冲区对象向着色器传递多种数据,比较适合数据量不大的情况,当数据量很大时这种方式很难维护,所以WebGL允许我们把不同种类的数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区对象中不同种类的数据。

13、纹理坐标

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色,WebGL系统中的纹理坐标系统是二维的,为了将纹理坐标和广泛使用的x坐标和y坐标区分开来,使用s和t命名,称之为st坐标系统。

14、纹理映射

纹理映射的一般步骤:

准备好映射到几何图形上的纹理图像;
为几何图形配置纹理映射方式;
加载纹理图像,对其进行一些配置,以在WebGL中使用它;
在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋予片元。

在配置纹理映射的时候需要注意一点,图片坐标系统和WebGL纹理坐标系统的Y轴方向是相反的,这时候你需要对纹理图像进行Y轴反转;

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);// Flip the image’s y-axis

15、纹理单元

WebGL通过一种纹理单元的机制来同时使用多个纹理,每个纹理单元有一个单元编号来管理一张纹理图像,系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,默认情况下至少支持8个纹理单元。

使用纹理单元的一般步骤:

激活纹理单元(gl.activeTexture());
绑定纹理对象(gl.bindTexture());
配置纹理对象的参数(gl.texParameteri());
将纹理图像分配给纹理对象(gl.texImage2D());
将纹理单元编号传递给取样器。

16、FrameBuffer

有点类似于Canvas中的离屏渲染,一般我们使用gl.drawArrays或者gl.drawElements都是将对象绘制在了默认的窗口中,但是当我们指定一个FrameBuffer时,再用这个两个方法去绘制,则会将对象会绘制于当前指定的FrameBuffer。

一般用于纹理的多重处理,比如对于一个纹理我们可以先灰度过滤,然后再模糊过滤,但是灰度过滤后还不能直接渲染到屏幕上,则可以使用FrameBuffer过渡,直到处理完成之后再渲染到屏幕上;
使用范例可以去WebGL简单实现高斯模糊这个例子中查看。

二、GLSL

1、attribute变量和uniform变量

都是GLSL ES中的变量类型,使用这两种变量可以把数据从JavaScript程序中传给着色器程序;

attribute和uniform类似都是存储限定符,主要区别如下:attribute只能用于顶点着色器,用来表示逐顶点数据;uniform可以用于顶点着色器也可以用于片元着色器,用来表示不变的数据。

uniform表示不变的数据的意思是在GLSL程序内部不能再被改变,但是可以再次由JavaScript程序赋予新的值。

2、按行主序和按列主序

在编程时我们通常使用数组存储矩阵,但是矩阵是二维的,数组是一维的,所以如果在数组中按行存储矩阵元素就被称为按行主序,在数组中按列存储矩阵元素就被称为按列主序;

WebGL和OpenGL一样,矩阵元素是按列主序存储在数组中的。

3、varying变量以及颜色值内插

暂时理解为varying变量的作用是从顶点着色器向片元着色器传输数据;

颜色值内插暂时就理解为颜色自动渐变(如果想要深入了解内插过程,可以参考《计算机图形学》)。

示例程序

移动端网页绝对自适应方案总结

https://github.com/newbieYoung/NewbieWebArticles/blob/master/auto-suit-website.html(2017-09-13T09:58:32Z)

© Young 2017-07-31 20:53
Welcome to My GitHub

概述

网页自适应是个涵盖很广的话题,这里我只简单总结下移动端网页绝对自适应,简单来说如果设计师提供给你的设计稿是750px宽,此时里边有一张图片宽高均为375px;那么在IPhone6 Plus手机里边屏幕宽为414px时该图片的渲染宽度应该为207px,这种保证元素尺寸、字体大小在屏幕中所占比例大小始终不变的方式我称之为移动端网页绝对自适应

目前来说我总结了大概有如下五种方式来实现移动端网页绝对自适应

一、百分比

在CSS中很多属性都可以使用百分比值,正确使用百分比有两个关键点:

1、找到当前元素的参照元素;

某个元素的参照元素不能简单的理解成父元素,还和其定位有关系,比如:

静态定位元素相对定位元素的参照元素一般是其父元素;
绝对定位元素参照元素一般是离它最近的绝对定位、相对定位或者固定定位的父元素,如果不存在则为视口;
固定定位元素的参照元素一般是视口。

2、找到当前属性的参照属性;

在CSS中很多属性都可以被设置为百分比数值,但是其意义确会有很多不同;总结如下:

padding百分比是相对于参照元素的宽度而言;
margin百分比是相对于参照元素的宽度而言;
width百分比是相对于参照元素的宽度而言;
leftright百分比是相对于参照元素的宽度而言;
height百分比是相对于参照元素的高度而言;
topbottom百分比是相对于参照元素的高度而言;
line-height百分比是相当于元素自身文字大小而言;
background-size百分比是相当于元素自身的宽高而言;
border-radius百分比是相当于元素自身的宽高而言;
transform百分比是相当于元素自身的宽高而言。

示例代码

以上只总结了较为常见的属性,实际还存在很多其它属性可以被设置为百分比,并可能存在你意想不到的意义。

vw和vh应该就是百分比的一种变种,只不过所有元素的参照元素都是视口,参照属性都是视口的宽高而已。

小结

百分比方式在处理文字大小的自适应上会比较麻烦,另外在处理图片的自适应也有问题,只能使用img标签的方式来实现,这样就会导致不能把图片合成精灵图(就目前来说把小图片合成精灵图还是有必要的);

另外设计稿一般都是固定数值的,所以重构页面时你得自己计算出百分比数值,虽然计算量不大,其实也是有那么一点点麻烦的。

二、媒体查询

媒体查询方式的问题在于只能处理特定状态,而“绝对自适应”其实可以理解成有无限多的状态。

三、REM

REM方式我一般会在页面里边加入如下脚本;

(function (doc, win) {
    var $body = document.querySelector('body');
    var docEl = doc.documentElement,
            resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
            recalc = function () {
                var clientWidth = docEl.clientWidth;
                if (!clientWidth) return;
                clientWidth = clientWidth>750?750:clientWidth;//防止被放大保证宽屏效果
                //以宽750px为例子,并扩大100倍,尽可能的保证精度
                docEl.style.fontSize = 100 * (clientWidth / 750) + 'px';
                //缩放之后再显示页面,防止缩放过程被观察到,影响体验
                $body.style.visibility = 'visible';
            };
    if (!doc.addEventListener) return;
    win.addEventListener(resizeEvt, recalc, false);
    doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);

需要注意的是脚本中750表示的是设计稿的宽度,后述脚本中的640等也是相同的意义。

其中关于扩大100倍的主要原因是如果这里没有扩大100倍,那么假设屏幕宽度是400px,那么html标签的font-size是0.5333...px,在某些浏览器只保留两位小数,此时实际有效值就变成了0.53px,如果CSS样式里边的值为46rem,最终结果就是0.53*46=24.38px

但是如果扩大100倍,屏幕宽度还是400px,那么html标签的font-size是53.33...px,还是在这个保留两位小数的浏览器里边,此时为了保证整体大小不变,CSS样式里边的值缩小100倍为0.46rem,此时最终结果就是53.33*0.46=24.53px

显然24.53px会比24.38px更精确一些。

REM方式理论上来说已经能解决问题了,但是实际上还是会有一些问题,最核心的问题在于数值的精度问题(不同浏览器处理浮点数时所保留的精度不一样),比如:

其一:较小数值可能会因为精度问题导致实际渲染大小为0,比如边框等;
其二:在Android系统里边有可能出现即使设置了height=line-height时文字依然不能完全垂直居中的情况,如果使用REM,font-size、height、line-height均是小数,精度问题会导致问题更明显。

四、meta inital-scale

上述方式归根到底都属于一种间接缩放的方法,需要注意的WebView早就提供一个直接缩放整个页面的方法,那就是meta标签中inital-scale属性,也就说可以控制这个属性来缩放页面,来达到绝对自适应;

控制脚本和REM控制脚本类似:

(function (doc, win) {
    var docEl = doc.documentElement,
            resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
            recalc = function () {
                var clientWidth = docEl.clientWidth;
                if (!clientWidth) return;
                clientWidth = clientWidth>750?750:clientWidth;//防止被放大保证宽屏效果
                document.querySelector('meta[name=viewport]').setAttribute('content','width=750, initial-scale='+clientWidth/750+', maximum-scale=1.0, user-scalable=0');
                document.querySelector('body').setAttribute('style','visibility:visible;');
            };
    if (!doc.addEventListener) return;
    doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);

这种方式异常的简单方便(谁用谁知道),如果REM方式是无脑自适应的话,那么这种方式可以被称为脑残自适应;这种方式基本不存在REM的精度问题,移动端细边框等问题完全不需要考虑;唯一需要你做的就是认认真真的把设计稿里边的值复制到页面中,然后再加上上述脚本;

起初我以为找到了完美的解决方案,但是后来实践的时候发现这个方式的缺点也很明显;

其一:存在极少数的设备不支持meta标签的initial-scale属性(少到可以忽略不计的那种);
其二:脚本中所涉及meta标签中的那些属性不能再被其它脚本所处理(貌似普通网页也不会随便操作mata标签);
其三:由于这个方式是缩放整个页面的方式来实现自适应,那么页面里边所有的样式都得遵循这种方式(就实际情况来看局限性很大),另外渲染性能也偏低(如果页面存在大量动画,不建议采用这种方式)。

五、transform scale

受上一种方式启发,缩放除了meta标签的initial-scale方式,CSS3的tranform也可以做到,而且meta标签的initial-scale是缩放整个页面,导致会和其它样式冲突,那可不可以专门定义一块区域缩放,其它区域不缩放呢?

(function (doc, win) {
    var docEl = doc.documentElement,
            resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
            recalc = function () {
                var clientWidth = docEl.clientWidth;
                if (!clientWidth) return;
                var $bodyContent = document.querySelector('.body-content');
                var $body = document.querySelector('body');
                var percent = clientWidth/750;
                percent = percent>1?1:percent;//防止被放大保证宽屏效果
                var othersHeight = $body.clientHeight-$bodyContent.clientHeight;//计算其它元素的高度
                $bodyContent.setAttribute('style','width:750px;transform:scale('+percent+');transform-origin:0 0;-webkit-transform:scale('+percent+');-webkit-transform-origin:0 0;margin:auto;');
                var lastStyle = $bodyContent.getAttribute('style');
                var articleHeight = $bodyContent.clientHeight*percent;
                $bodyContent.style.height = articleHeight+'px';
                $body.setAttribute('style','visibility:visible;height:'+(articleHeight+othersHeight)+'px;');
            };
    if (!doc.addEventListener) return;
    doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);

上述脚本就实现了上述的设想,解决了meta标签的initial-scale方式的第一个和第二个缺陷;

需要注意的是脚本中还额外处理了页面的高度,因为CSS3的tranform方式只影响了元素视觉效果,不会影响元素的占位尺寸,比如如果元素本身1000px高,缩放一半后看起来只有500px高,但是高度占位还是1000px,导致如果页面出现滚动条,缩放之后滚动高度还是不变,此时需要设置页面高度为缩放之后的高度。

除了高度问题,这种方式还需要注意一些问题:

其一:tansform会导致其内部元素的fixed定位失效,变为absolute定位

示例代码

2017-08-24 更新

fixed定位失效的真正原因在于transform影响了fixed定位元素的堆叠上下文,简单来说fixed定位的元素,如果其祖先元素存在非none的transform值,那么该元素将相对于设定了transform的祖先元素定位,而不是原本相对于视口。

所谓堆叠上下文这个概念可以看看这篇文章层叠顺序(stacking level)与堆叠上下文(stacking context)知多少?

其二:缩放区域得清除浮动,否则会导致高度计算异常;
其三:待发现。

总结

上述这些方式仅仅我在坚持开发所谓的绝对自适应网页过程中慢慢得到的一些经验总结,就目前看来没有一种十全十美的方式,具体情况具体分析咯,毕竟方法是死的,人是活的。

关于贝塞尔曲线的一些事

https://github.com/newbieYoung/NewbieWebArticles/blob/master/draw-bezier-curve.html(2017-08-17T06:37:02Z)

© Young 2017-07-21 20:22
Welcome to My GitHub

概述

前段时间用SVG实现了一个简单的路径过渡动画,如下:

由于以前很少接触SVG导致遇到了一些困难,解决之后得到了一些总结:

其一:路径过渡动画只适用于指令类似的路径;

上述图中分别是动画前路径和动画后路径,由于这两个路径是设计同学分开设计的,两个路径之间指令完全不一样,比如动画前路径中全是贝塞尔曲线,但是动画后路径中却存在直线;因此这两个路径之间是不能在没有任何处理的情况下通过Animation实现过渡动画效果的。

其二:Animation在IPhone中存在兼容性问题;

在IPhone中一个路径存在多个Animation时多次触发会有bug,某些Animation不能显式的表现出来;

问题例子

解决方案

实现之后喜闻乐见加需求了,产品要求图形张开之后里边的文案是可配置的,也就是说图形张开之后水平区域的宽度是可控制的;面对这种情况我唯一能想到的就只有通过JS动态更改路径中的指令参数了,而这就要求我得完全明白路径中指令和参数代表的是什么意思了!

SVG缩写

不幸的是软件导出的SVG文件出于文件大小的考虑会采用缩写的形式,导致可读性较差,比如:

<path d="M640.5,300.5c-36,0-221.2-0.1-235.5-0.1c-18.5,0-29.6-15.9-29.5-30.7c0-14,12.3-29.9,29.5-29.9c13.9,0,198.2,0,235.4,0L640.5,300.5z"/>

不仅没有空格分隔符,还有很多不明意义的横杠(后来才知道其实就是负号的意思);

通过查询资料发现PATH属性存在一些缩写规则,比如:

字母和数字之间可以省略分隔符;
负数和数字之间可以省略分隔符;
数字和字母之间也可以省略分隔符;
逗号就是普通的分隔符和空格类似,之所以使用逗号代替空格应该是因为空格在不同操作系统或者不同编码格式下会有差异;

因此上述软件导出的PATH其实和下边格式化之后的PATH表现一致。

<path d="M640.5, 300.5 c -36, 0 -221.2 -0.1 -235.5 -0.1 c -18.5, 0 -29.6 -15.9 -29.5 -30.7 c 0 -14, 12.3 -29.9, 29.5 -29.9 c 13.9, 0, 198.2, 0, 235.4, 0 L640.5,300.5z"/>

相对坐标

格式化之后可读性好了很多,但是还有一个地方比较困惑,SVG中大写C和小写c都表示三次贝塞尔曲线,但是大写C中的坐标是绝对坐标,小写c中的坐标是相对坐标;三次贝塞尔曲线存在一个起始点、一个结束点和两个控制点,那么相对坐标中是都是相对起始点而言还是相对上一个点而言呢?

稍作验证即可得出相对坐标都是相对起始点而言的,起始点也就是上一个指令的结束点;

示例代码

其实大家可以稍微思考下为什么软件导出的会默认是相对坐标?以及为什么相对坐标都是相对上一个指令的结束点而言?
猜想:相对坐标会减少文件字符从而减少文件大小,另外相对上一个指令的结束点和相对当前指令的上一个点的区别在于前一种方式一个指令中的数据是不存在联系的,而后一种方式一个指令中的数据是存在的联系的,也许这种联系会带来逻辑上的复杂性(比如会影响绘制效率等)。
当然上述猜想也可能是我想多了……

贝塞尔曲线

贝塞尔曲线的数学基础是早在1912年就广为人知的伯恩斯坦多项式。但直到1959年,当时就职于雪铁龙的法国数学家Paul de Casteljau才开始对它进行图形化应用的尝试,并提出了一种数值稳定的de Casteljau 算法。然而贝塞尔曲线的得名,却是由于1962年另一位就职于雷诺的法国工程师Pierre Bézier 的广泛宣传,他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。

正是因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域和计算机图形学领域得到了广泛的应用,贝塞尔曲线根据控制点的多少分为,一次贝塞尔曲线、二次贝塞尔曲线、三次贝塞尔曲线等。

手绘二次贝塞尔曲线

在坐标系中选取三个不共线的点,分别是P0P1P2
在线段P0P1上任选一点Q0,并计算线段P0Q0P0P1上所占的比例t
根据上一步得到的比例,在线段P1P2上找到Q1,使得P1Q1P1P2上所占的比例等于t
连接Q0Q1
在新线段上再次找到B,使得Q0BQ0Q1上所占比例等于t
B即为该二次贝塞尔曲线上的一点;
以此类推。

根据上述规律可以对二次贝塞尔曲线公式进行推导;

完成产品要求

了解了这么多没什么卵用的东西,是时候解决产品的要求了;

具体代码

最终结果如图:

ThreeJS的DeviceOrientationControls源代码解析

https://github.com/newbieYoung/NewbieWebArticles/blob/master/webgl-threejs-DeviceOrientationControls.html(2017-06-04T08:54:28Z)

© Young 2017-06-02 11:04
Welcome to My GitHub

概述

在学习ThreeJS和WebGL的过程中想着实现一个基于手机陀螺仪的3D环绕视角的例子,遇到了很多困难,一直都没有完成;后来发现ThreeJS中已经有了类似的例子,在这个例子里边作者封装了一个叫作DeviceOrientationControls的控制器,专门处理基于手机陀螺仪的相机的方位变化,所以我就想仔细了解一下这个控制器的工作原理。

分析

使用方法

该控制器使用方法很简单;

  • 首先引入代码;

  • 然后通过相机构建;

  • 最后在渲染方法里边不断的调用该控制器的update方法即可;

核心代码

这个控制器代码量不多,只有短短的100行,逻辑看起来也很简单,只需要监听手机陀螺仪事件然后不断改变相机方位即可,核心代码如下:

42行和第50行初始化了一个Euler对象;

    var euler = new THREE.Euler();
    euler.set(beta,alpha,-gamma,'YXZ');//'ZXY' for the device, but 'YXZ' for us

Euler就是欧拉角,简单来说欧拉角的基本思想就是任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成,任意三个轴和任意顺序都可以,但最有意义的是使用笛卡尔坐标系(旋转物体自身的坐标系,而不是世界坐标系)并按一定顺序所组成的旋转序列;

手机陀螺仪的deviceorientation事件返回的数据,就是一个欧拉角位移数据;

alpha表示设备绕Z轴旋转的角度,范围为0~360;
beta表示设备绕X轴旋转的角度,范围为-180~180;
gamma表示设备绕Y轴旋转的角度,范围为-90~90;

这里存在几个问题:

其一:为什么每个角度的范围不一致?

首先得明确一点为什么要有范围,主要是因为如果没有范围,比如alpha为10和370其实是等价的,一个角位移存在多个描述会导致一些麻烦(可以思考下会有什么麻烦?);再来就是为什么不同方向的角位移限制不一致,其实原因还是基于上述理由,如果都限制为0~360,还是有些角位移有多种表示,导致连一些基本的问题,比如两组欧拉角代表的角位移是否相同都很难回答。
至于为什么范围分别是0~360、-180~180、-90~90,这里就不过多展开了,同时也压根不准备证明欧拉角定理(欧拉定理是以瑞士数学家莱昂哈德·欧拉命名,于1775年,欧拉使用简单的几何论述证明了这定理)。

其二:怎么判断手机陀螺仪的顺序?

很简单,范围越大表示次序越靠前,因为最外层的运动范围更大一些,也就是说手机陀螺仪欧拉角的顺序是ZXY

在解释42行和50行的代码之前,我们先用已经了解的知识制作一个例子来演示手机陀螺仪,

请使用支持陀螺仪的移动设备浏览。

在这个例子里边我创建了一个和手机外型类似的长方体,然后让其方位跟随手机设备方位的变化而变化,核心代码如下:

在这个方法里边用了三种方式来实现旋转;

第一种方式就是分别调用物体的rotateZrotateXrotateY方法(因为任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成);

第二种方式是使用矩阵,同样是基于上述原理,唯一要注意的是角度要取负值(简单理解为物体实际没有动,变换的是坐标系);

第三种方式就是使用ThreeJS框架实现的欧拉角;

    /**
     *  注意下边不要想当然为啥set方法的前三个参数和后边的顺序不是一一对应;
     *  比如如果顺序为ZXY,那么前边参数顺序应该为alpha、beta、gamma;
     *  ThreeJS代码实现就是前边参数的顺序固定为XYZ,和后边的顺序是没有关系的;
     *  因为这个想当然的问题困扰我很久。
     */
    var euler = new THREE.Euler();
    euler.set( beta,gamma,alpha,  'ZXY' );

接下来回头再看控制器里边的代码,就会有几个问题了:

其一:为什么欧拉角的顺序变成了YXZ而不是ZXY?

其实控制器代码里边作者是写了注释的,ZXY for the device, but YXZ for us,简单来说摄像机坐标系和我们人眼的坐标系是不一样的,比如摄像机还存在一个正方向的问题,你在设置顺序为ZXY时就会发现在旋转到某个角度时整个页面的图像会发生颠倒,至于为啥设置顺序为YXZ就不会发生这种情况,我也不是很清楚。

其二:为什么gamma和alpha的位置发生了交换?

其三:为什么gamma值为负数?

第二和第三个问题会在第46行和第54行中得到解释。

46行和第54行把相机绕X轴旋转了负90度

    var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
    quaternion.multiply( q1 ); // camera looks out the back of the device, not the top

Quaternion就是四元数,在了解四元数所代表的几何意义之前,我们简单了解下为什么会有四元数;

主要是因为欧拉角存在一些问题,比如万向节死锁

简单来说就是在旋转过程中有两个轴重合导致,失去一个维度,导致此后的角位移没办法用欧拉角表示,也就是说欧拉角失效了,在欧拉角进行插值操作时大多数情况下会导致抖动、路径错误,物体会突然飘起来。
优酷里边有个视频教程讲的挺清楚的欧拉旋转
为什么用三个数来表达3D方位一定会导致如万向锁这样的问题?这是有数学原因的,它涉及到一些非常高级的数学概念,如“簇”;而四元数通过使用四个数来表达方位,从而避免了这些问题。

另外还涉及到复数,复数为实数的延伸,它使任一方程式都有根;

任意复数都可以表示为x+y*i,i是复数当中的虚数单位,它是-1的平方根。

数学家们慢慢接受复数之后,发现复数集存在于一个2D平面上,该平面有两个轴,实轴和虚轴;这样就能将复数(x+y*i)解释为2D向量,也能用来旋转2D中的向量;然后很自然的数学家们就想找到一种方法将复数从2D扩展到3D,刚开始他们认为这种新的复数应该有一个实部和两个虚部,然而这种方案一直没有进展,直到后来意识到应该有三个虚部和一个实部,就这样四元数诞生了。

老实说这是一个比较复杂的概念(至少当我写这篇博客的时候感觉如此),一个四元数包含一个标量分量和一个3D向量分量,经常记标量分量为w,记向量分量为单一的v或者分开的x,y,z。

    [w,v]
    [w,(x,y,z)]

四元数能被解释为角位移的绕某个轴n旋转a角度,公式如下:

    q = [cos(a/2),sin(a/2)*n]

因此可以知道第46行就是一个表示绕X轴旋转负90度的四元数了,那么现在的问题就是为什么要把摄像机绕X轴旋转负90度?

主要是因为手机陀螺仪的初始状态是在手机平放于水平面的时候,但是我们处理3D环绕视角场景时是希望在手机竖直时的状态为初始状态,因此需要把坐标系绕X轴旋转负90度,旋转负90度之后,现在Z轴相当于以前的负Y轴,现在的Y轴相当于以前的Z轴,因此也就不难理解为什么gamma和alpha的位置发生了交换?为什么gamma值为负数?这两个问题了。

40行和第56行处理了手机横屏和竖屏的问题

    var zee = new THREE.Vector3( 0, 0, 1 );
    quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation

结语

由于数学基础薄弱,上述内容存在很多一笔带过甚至是强行解释的现象,欢迎大家纠正;

主要参考资料为《3D数学基础:图形与游戏开发》。

WebGL ThreeJS学习总结四

https://github.com/newbieYoung/NewbieWebArticles/blob/master/webgl-threejs-4.html(2017-09-12T09:27:41Z)

© Young 2017-05-13 19:07
Welcome to My GitHub

概述

通过前段时间的学习,现在已经能使用ThreeJS框架制作一些简单3D效果,对原生WebGL也有了简单的了解;在学习过程中察觉到自己在数学方面欠缺的太多,所以我决定先缓一缓技术方面的学习,补一补数学基础。

另外需要的注意的是以下内容大部分来自于《3D数学基础:图形与游戏开发》。

概念

一、数

自然数、整数、有理数、实数、复数等。

1、复数

复数为实数的延伸,它使任一多项方程式都有根。

任意复数都可以表达为x+y*i,i是复数当中的虚数单位,它是-1的平方根,后边的四元数会涉及到复数。

二、坐标系

笛卡尔坐标系、多坐标系、世界坐标系、物体坐标系、摄像机坐标系、惯性坐标系、嵌套式坐标系、描述坐标系、坐标系转换等。

1、惯性坐标系

惯性坐标系的原点和物体坐标系原点重合,但惯性坐标系的轴平行于世界坐标系的轴;引入惯性坐标系的好处在于从物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移。

上边列了这么多坐标系的名称或者概念,很容易让人感到疑惑,为什么需要这么多坐标系?所有问题都放到世界坐标系中处理不就好了吗?

我的理解是有很多问题放到世界坐标系中处理反而会更复杂,比如我要计算我和吴小胖(一只猫)的距离,根据我的纬度和经度以及吴小胖的纬度和经度算,反而没有以我为原点拿标尺去量更简单;
文艺一点的说法,正如乔治.奥维尔在《动物庄园》所解释的那样,所有坐标系都是平等的,但某些可能比其它的更合适

三、向量

零向量、负向量、向量的大小、标量与向量相乘、标准化向量、向量的加法与减法、向量点乘、向量投影、向量叉乘等。

1、向量点乘

两个向量点乘结果越大,那么两个向量的夹角越小,两向量越靠近。

    a = [x1,y1];
    b = [x2,y2];
    //a、b向量夹角为c
    a*b = [x1,y1]*[x2,y2] = x1*x2+y1*y2 = |a|*|b|*cos(c);
    //因此可得两个向量夹角的公式
    c = arccos(a*b/(|a|*|b|));

2、向量叉乘

叉乘仅适用于3D向量,两个向量叉乘得到的向量垂直于原来的两个向量,另外叉乘的优先级比点乘高。

    a = [x1,y1,z1];
    b = [x2,y2,z2];
    //a、b向量夹角为c
    a*b = [x1,y1,z1]*[x2,y2,z2] = [y1*z2-z1*y2,z1*x2-x1*z2,x1*y2-y1*x2];
    |a*b| = |a|*|b|*sin(c);
    //因此可得两个向量夹角的公式
    c = arcsin(|a*b|/(|a|*|b|));

到这基本上可以去稍微过一眼ThreeJS框架中Vect2、Vect3、Vect4等向量类的源代码了,另外如果不习惯英文版,也可以去看国内大神翻译的中文版源代码
另外ThreeJS在设计时赋予Vect3、Vect2、Vect4等向量类多种意义,比如Vect3即可以表示一个3D坐标系中的点,也可以表示一个3D坐标系中向量等;这是一种有争议的设计方法,好处在于可以简化某些功能函数的设计,比如旋转函数,点可以旋转,向量也可以旋转,如果点和向量分开,那么旋转函数必须得定义接收多种参数的版本;反之则只需要定义接收一种参数的版本。坏处在于在逻辑上会带来混淆的情况。

四、矩阵

方阵、对角矩阵、单位矩阵、矩阵转置、矩阵乘法、行向量、列向量、行列式等。

1、方阵

方阵能描述任意线性变换,简单来说线性变换会保留直线,也就是线性变换的图形不可能发生弯曲和卷折。

2、矩阵的几何意义

矩阵并不神秘,它只是用一种紧凑的方式来表达坐标转换所需的数学运算。

    //3D坐标系中的基向量乘以任意矩阵M的情况
    M = [m11,m12,m13,
        m21,m22,m23,
        m31,m32,m33];


    x = [1,0,0];
    y = [0,1,0];
    z = [0,0,1];


    x*M = [m11,m12,m13];


    y*M = [m21,m22,m23];


    z*M = [m31,m32,m33];
    //由此可见矩阵的每一行都可以解释为转换后的基向量

给出一个期望的变换,我们要做的一切就是计算基向量的变换,然后将变换后的基向量填入矩阵,这样就得到此次变换的矩阵形式。

五、矩阵与线性变换

旋转、缩放、正交投影、镜像、切变、变换组合、变换分类、行列式、余子式、矩阵的逆、标准伴随矩阵、正交矩阵、齐次矩阵等。

在讨论变换之前有一点很重要,我们必须清楚变换的主体是啥。也许你会说这还用说吗?当然是变换物体咯,其实不然,在某些情况下变换坐标系反而更方便。

1、3D空间绕任意轴旋转矩阵推导

    /**
     * 看了半天才看懂,摘录下来加深印象;
     * 首先假设该任意轴过原点,用向量n表示(向量n为单位向量),然后某个向量v绕n旋转得到新向量m;
     * 此次旋转的矩阵表示为R(n,c),也就是绕n旋转c角度;
     * 向量乘法有点乘和叉乘的区别,所以下边表达式中.表示点乘,*表示叉乘,普通乘法省略操作符。
     */
    m = v.R(n,c);


    /**
     * 将向量分解为垂直于n向量的v2和平行为n向量的v1;
     * 这么做的好处在于将复杂的3D问题转换为简单的2D问题;
     * 那么绕n旋转v就可以分为绕n旋转v1和绕n旋转v2;
     * v1平行于n,旋转之后依然为v1;
     * v2垂直于n,旋转之后为m2。
     */
    v = v1+v2;
    m = v1+m2;


    //v1是v在n上的投影
    v1 = |v1|n;
       = |v|cos(c1)n;
       = |v||n|cos(c1)/|n|n;
       = v.n/|n|n;//n为单位向量长度为1
       = (v.n)n;


    //v1+v2 = v
    v2 = v - v1;
       = v - (v.n)n;


    //向量w垂直于向量n和向量v2
    w = n*v2;//叉乘
      = n*(v-v1);
      = n*v - n*v1;//两方向相同的向量叉乘结果为向量0
      = n*v - 0;
      = n*v;


    m2 = w1+v3;


    w1 = |cos(c-90)|w;
       = |cos(c)cos(90)+sin(c)sin(90)|w;
       = sin(c)w;//180>c>90


    v3 = -|sin(c-90)|v2;
       = -|sin(c)cos(90)-cos(c)sin(90)|v2;
       = -|-cos(c)|v2;//180>c>90
       = cos(c)v2;


    m2 = sin(c)w+cos(c)v2;
       = sin(c)(n*v)+cos(c)(v - (v.n)n);


    m = v1+m2;
      = (v.n)n+sin(c)(n*v)+cos(c)(v - (v.n)n);


    /**
     * 现在已经得到了原向量v、旋转角度c、过原点旋转轴向量n以及旋转后向量m的表达式;
     * 那么如果已经旋转角度、过原点旋转轴、原向量v就可以得到旋转后向量m了;
     * 那么要得到期望变换的矩阵,只需要计算基向量的变换,然后把变换后的基向量整合起来就可以了。
     */


    //在3D坐标中,基向量就是x、y、z轴的正方向向量
    x = [1,0,0];
    n = [nx,ny,nz];
    x1 = (x.n)n+sin(c)(n*x)+cos(c)(x - (x.n)n);
    //...
    x1 = [(nx)(nx)(1-cos(c))+cos(c),(nx)(ny)(1-cos(c))+(nz)sin(c),(nx)(nz)(1-cos(c))-(ny)sin(c)];
    //后续依次代入y轴和z轴即可

2、3D空间或者2D平面沿任意方向缩放

    /**
     * 设n为平行于缩放方向的单位向量,k为缩放因子,向量v为原始向量,m为缩放后的向量;
     * 我们需要推导出一个表达式,可以通过n、v、k来计算m;
     * 向量乘法有点乘和叉乘的区别,所以下边表达式中.表示点乘,*表示叉乘,普通乘法省略操作符。
     */


     v = v1+v2;
     v1 = (v.n)n;


     m2 = v2;
        = v-v1;
        = v-(v.n)n;
     m1 = kv1;
        = k(v.n)n;


     m = m1+m2;
       = v-(v.n)n+k(v.n)n;
       = v+(v.n)n(k-1);


    //2D平面
    x = [1,0];
    n = [nx,ny];


    //3D空间
    x = [1,0,0];
    n = [nx,ny,nz];


    //具体求解矩阵略...

3、正交投影

正交投影的原理就是让某个方向的缩放因子为0

    /**
     * S表示缩放矩阵,S(n,k)表示在向量n方向上缩放,缩放因子为k。
     */


    //2D平面,向x轴投影,也就是y轴方向上缩放,缩放因子为0
    Px = S([0,1],0) = [1,0,
                       0,0];


    //3D空间,向xy平面投影,也就是z轴方向上缩放,缩放因子为0
    Pxy = S([0,0,1],0) = [1,0,0,
                          0,1,0,
                          0,0,0];


    //任意方向的投影矩阵略,可以根据沿任意方向的缩放矩阵推导。

4、镜像

镜像也叫反射,其作用是将物体沿直线(2D)或者平面(3D)翻折,根据缩放公式使缩放因子为-1,可以得到镜像矩阵。

5、切变

切变是一种坐标系扭曲的变换,切变的时候角度会发生变化,但是面积或者体积却保持不变,基本思想是将某一坐标的乘积加到另一个上。

    //2D平面中,将y乘以某个系数s,然后加到x上 x1 = sy+x
    Hx(s) = [1,0,
             s,1];


    //3D空间中,将z乘以不同的系数s、t,然后分别加到x、y上 x1 = sz+x y1 = tz+y
    Hxy(s,t) = [1,0,0,
                0,1,0,
                s,t,1];

6、变换组合

    /**
     * 设想世界坐标系中有一个任意方向,任意位置的物体,我们要把它渲染到任意方向,任意位置的摄像机中去,为了做到这一点必须将物体上所有的顶点从物体坐标系变换到世界坐标系,然后再由世界坐标系变换到摄像机坐标系。
     */
    P世界 = P物体*M(物体->世界);
    P摄像机 = P世界*M(世界->摄像机);
           = P物体*M(物体->世界)*M(世界->摄像机);
           = P物体*(M(物体->世界)*M(世界->摄像机));//使用组合变换矩阵效率更高

7、变换分类

线性变换、仿射变换、可逆变换、等角变换、正交变换、刚体变换等。

8、余子式与代数余子式

假设矩阵M有r行、c列,从中移除第i行和第j列后剩下的矩阵称为M的余子式,而余子式矩阵有符号的行列式则称为M的代数余子式。

9、行列式

任意方阵都存在一个标量,称为该标量的行列式,行列式有很多有趣的几何解释,在2D中矩阵的行列式等于以基向量为边的平行四边形的有符号的面积;在3D中矩阵的行列式等于以基向量为边的平行六面体的有符号体积;

    /**
     * 从矩阵中任意选择一行或者一列,对该行或者该列中的每个元素都乘以各自的代数余子式,
     * 这些乘积的和就是该矩阵的行列式。
     */


    //2*2矩阵行列式
    [m11,m12,
     m21,m22] = m11*m12-m12*m21;


    //3*3矩阵行列式
    [m11,m12,m13,
     m21,m22,m23,
     m31,m32,m33] = m11*|[m22,m23,m32,m33]|-m12*|[m21,m23,m31,m33]|+m13|[m21,m22,m31,m32]|


    //以此类推

10、标准伴随矩阵

某个矩阵的代数余子式矩阵的转置矩阵被称为该矩阵的标准伴随矩阵。

11、可逆矩阵和非可逆矩阵

并非所有矩阵都是有逆的,如果一个矩阵有逆矩阵,那么称它为可逆矩阵或者非奇异矩阵;如果一个矩阵没有逆矩阵,那么称它为非可逆矩阵或者奇异矩阵;奇异矩阵的行列式为0,非奇异矩阵的行列式不为0。

可逆矩阵的逆矩阵为其标准伴随矩阵除以其行列式,这种方式适合低阶可逆矩阵求逆矩阵;其实还存在其他方法比如高斯消元法,适用于高阶可逆矩阵求逆矩阵;
矩阵转置的逆等于矩阵逆的转置;
矩阵乘积的逆等于矩阵逆的相反顺序的乘积,扩展到多个矩阵也适用;
矩阵的逆在几何上非常有用,因为它可以使得我们可以计算变换的反向或者相反变换,从而撤销原变换。

11、正交矩阵

若方阵M是正交,当且仅当方阵和其转置矩阵的乘积等于单位矩阵。

这是一条非常有用的性质,因为在实际应用中经常需要计算矩阵的逆,而3D图形计算中正交矩阵出现得又是如此频繁,例如旋转和镜像矩阵,如果知道某个矩阵是正交的,就可以避免计算逆矩阵了,大大减少计算量。
计算逆矩阵时,仅在预先知道矩阵是正交的情况下才能利用正交矩阵的优点,如果预先不知道,检查正交性通常是浪费时间。
有时候可能得到略微得到违反正交性的矩阵,例如外部坏数据或者浮点数运算等,这种情况下需要做矩阵正交化,得到一个正交矩阵,这个矩阵要尽可能的和原矩阵相同(至少希望是这样);
构造一组正交基向量的标准算法是施密特正交化,它的基本思想是,对每一行,从中减去它平行于已处理过行的部分,最后得到垂直向量。

12、齐次矩阵

4*4齐次矩阵只是3D运算的一种方便记法而已,并不代表四维空间。

六、3D中的方位与角位移

1、欧拉角

简单来说欧拉角的基本思想就是任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成,任意三个轴和任意顺序都可以,但最有意义的是使用笛卡尔坐标系(旋转物体自身的坐标系,而不是世界坐标系)并按一定顺序所组成的旋转序列;

作为前端工程师我们得知道手机里边陀螺仪设备所返回的数据,就是一个欧拉角位移数据;

alpha表示设备绕Z轴旋转的角度,范围为0~360;
beta表示设备绕X轴旋转的角度,范围为-180~180;
gamma表示设备绕Y轴旋转的角度,范围为-90~90;

这里存在几个问题:

其一:为什么每个角度的范围不一致?

首先得明确一点为什么要有范围,主要是因为如果没有范围,比如alpha为10和370其实是等价的,一个角位移存在多个描述会导致一些麻烦(可以思考下会有什么麻烦?);再来就是为什么不同方向的角位移限制不一致,其实原因还是基于上述理由,如果都限制为0~360,还是有些角位移有多种表示,导致连一些基本的问题,比如两组欧拉角代表的角位移是否相同都很难回答。
至于为什么范围分别是0~360、-180~180、-90~90,这里就不过多展开了,同时也压根不准备证明欧拉角定理(欧拉定理是以瑞士数学家莱昂哈德·欧拉命名,于1775年,欧拉使用简单的几何论述证明了这定理)。

其二:怎么判断手机陀螺仪的顺序?

很简单,范围越大表示次序越靠前,因为最外层的运动范围更大一些,也就是说手机陀螺仪欧拉角的顺序是ZXY

说到欧拉角还有一个经典的问题得说下:

万向节死锁

简单来说就是在旋转过程中有两个轴重合导致,失去一个维度,导致此后的角位移没办法用欧拉角表示,也就是说欧拉角失效了,在欧拉角进行插值操作时大多数情况下会导致抖动、路径错误,物体会突然飘起来。
优酷里边有个视频教程讲的挺清楚的欧拉旋转
为什么用三个数来表达3D方位一定会导致如万向锁这样的问题?这是有数学原因的,它涉及到一些非常高级的数学概念,如“簇”;下边的四元数通过使用四个数来表达方位,从而避免了这些问题。

尽管矩阵有一些缺点,比如占用了更多的内存,不直观,可能会出现病态矩阵的情况,但是当和图形API交流时,最终必须用矩阵来描述所需的转换,因此我们得了解如何将欧拉角转换为矩阵。

另外需要注意欧拉角可以转换为矩阵形式,旋转矩阵也可以转换为欧拉角的形式,这里不准备过多阐述。

2、四元数

老实说这是一个比较复杂的概念(至少当我写这篇博客的时候感觉如此),一个四元数包含一个标量分量和一个3D向量分量,经常记标量分量为w,记向量分量为单一的v或者分开的x,y,z。

    [w,v]
    [w,(x,y,z)]

数学家们慢慢接受复数之后,发现复数集存在于一个2D平面上,该平面有两个轴,实轴和虚轴;这样就能将复数(x+y*i)解释为2D向量,也能用来旋转2D中的向量;然后很自然的数学家们就想找到一种方法将复数从2D扩展到3D,刚开始他们认为这种新的复数应该有一个实部和两个虚部,然而这种方案一直没有进展,直到后来意识到应该有三个虚部和一个实部,就这样四元数诞生了。

四元数能被解释为角位移的绕某个轴n旋转a角度,公式如下:

    q = [cos(a/2),sin(a/2)*n]

另外四元数有slerpsquad插值方式,可以有效避免欧拉角插值过程中的问题。

我为什么要在2017年初换新工作

https://github.com/newbieYoung/NewbieWebArticles/blob/master/why-leave-futu.html(2017-03-18T14:33:57Z)

© Young 2017-03-18 16:37
Welcome to My GitHub

我又换工作了,为什么要说又呢?因为工作三年半,下一家公司应该是我加入的第五家公司了(由于某些原因刚毕业一年半我换过三家公司,然后在上家公司呆了两年,那么很明显下一家就是第五家了)……

在加入上家公司之前,虽然当时我自己没什么感觉,不过我现在回头看来,当时的情况其实已经很糟糕了,不过还好可能面试的时候喜欢折腾的劲头打动了当时的面试官。

加入的第一年我都有种感觉可能会永远呆下去的感觉了,首先在知识结构上给了我很多时间和机会去弥补从Java开发工程师转为前端开发工程所欠缺的基础知识的问题,另外整个团队的氛围第一次让我摆脱了刚毕业小心翼翼的行为方式,体验了专利申请奖金,组内鼓励技术分享让我充分感受到了分享知识所带来的成就感等等;

不过最大的收获还是在于给我单薄的工作经历添加了浓墨重彩一笔,让我有了可以聊工作收获和缺陷的机会和内容。

至于我为什么又换工作了,是因为随着时间的流逝和年龄的增长,作为大环境中微不足道的一员,很多目标会越来越清晰的呈现在你眼前。

如果说两年前我的目标是改变职业发展状况异常糟糕的现状,那么现在目标就是:

  • 三年之后我希望自己首先能成为一个小leader,虽然我在学校和刚毕业的时候对编程就是青春饭,一直都嗤之以鼻,认为是无稽之谈,但是现在却越来越感觉这基本上就是一个不管你承不承认都无力改变的趋势;
  • 另外在程序员这个群体里边具备一定的知名度,最好是在Web视觉化编程领域有比较系统和深入的了解而且其它领域也要在平均水平以上;
  • 有一定的财富积累,对投资等财富游戏有一定的了解,最好有一定的收获。

就第一个目标来说,其实在任何公司都会有这样的机会,但是我自己感觉自己是一个在管理这方面不太擅长的人,从平时的自我管理就能看出来,因此我觉得我需要公司对我进行系统的培训,然后我再依靠自己的学习能力去弥补,而上一家公司虽然也在倡导学习型组织不过由于资源的原因,我觉得很难达到我的要求。

另外我觉得在上一家的工作过程中给管理层留下了一些对我进行管理方面方面发展不好的印象,因此如果我要想在上一家公司完成我的第一个目标,我会比我的竞争者多出很多工作量。

有逃避问题的嫌疑哈,面试的时候不要说,你懂的……

再有就是上一家虽然发展势头良好,但是目前人员规模也只有接近300人的样子,如果我要实现我的第一个目标,按照理想模型,那么至少上一家公司的人员规模得在三年之后达到1000人,对此我持怀疑的态度。

关于第二个目标,其实很简单,如果在一个已经很知名的团队里边,自然对提升自身知名度也会有很多帮助;但是上一家公司的团队对外输出比较少。

另外自己本身可能也要更多的参与论坛社区等,前不久加入掘金翻译计划和掘金专栏作者以及和同学合伙运营一个微信公众号应该是一个良好的开始。

在Web视觉化编程领域有比较系统和深入的了解,对我自己而言难点可能在于要去弥补下计算机图形学等基础内容。

另外第二个目标其实有相互矛盾的地方,一个人的精力是有限的,很难面面俱到;因此在其它领域也要在平均水平以上的话,就不能去专研一些很细节的东西,对于现在前端领域新框架新技术频出的现状而言,对这些东西最好优先理解原理、优缺点和适用范围,在辅以简单例子来学习了;对于那些网络协议等重要基础反而应该详细了解。

最后一个目标其实就是福利待遇的问题,另外对投资等财富游戏有一定的了解反而和工作关系不大,这里就不作过多说明了。

因此要实现我的第一个目标,我的下一家公司得是一家具有职业进阶通道和培训的大型互联网公司,不过可能会在适应螺丝钉角色和处理人际关系上边遇到一定的挑战。

某个程序员眼中的用户体验设计

https://github.com/newbieYoung/NewbieWebArticles/blob/master/understanding-experience.html(2017-03-08T12:23:26Z)

© Young 2017-02-28 18:20
Welcome to My GitHub

概述

作为一名半路出家的前端工程师,大概开发移动端网页有两年时间了吧,虽然交互设计不是我的工作内容,但也或多或少的踩了一些坑,然后根据这些坑记录了一些小总结,比如:

  • 移动网页中输入项最好在页面顶部,关联搜索等复杂交互最好是单独新开页面,不应该和其它交互杂糅在一起;
  • 移动网页中需要交互触发的元素不应该设计的太小,设计的太小对于手指操作来说很困难;
  • 移动网页中某个交互不应该对页面元素以及布局产生突然变化,容易让用户很困惑;如果某个交互导致页面变化很大则应该用新开的页面的方式展现;
  • 移动网页的交互不仅仅包括其内部本身的一些交互,还包括和任何可能出现该网页的APP软件的交互;简单来说,设计和实现一个移动网页其实还要考虑该网页在当前流行APP中分享的文案、图标等;
  • 异常反馈很重要,不仅仅指产品设计时的异常情况,还包括程序运行时的各种异常(这里需要开发尽可能的考虑清楚并反馈给产品),尽可能的给用户清晰的反馈;
  • 最好对用户在页面的操作有一定的反馈,比如点击按钮等。

都是些零零散散的东西,直到前不久在公司的图书角找到了一本漂亮,纸质很好的书《匠心体验-智能手机与平板电脑的用户体验设计》:

前言

首先得声明一下这只是某个程序员粗略浏览《匠心体验-智能手机与平板电脑的用户体验设计》之后为方便和加深自己理解而记录的读后感,主要内容基本可以概括为:从一个适合自己理解的角度来简要复述上述书籍的内容。

因此你不可能在后续的内容中发现细致深入的知识点,甚至看到最后你还可能还会感觉这个角度完全不适合你。

详解

第一章作者从视觉、触觉、听觉、感知等方面,阐述了智能手机和平板电脑在用户体验设计中应该遵循的规则和注意事项。

看完之后…

作者从哪几个方面阐述来着…

知其然不知其所以然就会出现这样的情况了,如果不能系统的了解并转化成自己的知识,就只能停留在死记硬背的程度。

工作四年了,在这四年的屡次面试中,不管是JAVA开发工程师还是前端开发工程师,大部分情况下都能遇到一道面试题,请尽可能详细的阐述从浏览器中输入url地址到浏览器渲染出页面的过程

这里既不讨论这道题的答案也不讨论这道题作为面试题的意义,我只是感觉这个问题本身描述了一种方法,从头到尾系统了解某个事物的方法。

那么对比某个用户使用智能手机或者平板电脑浏览网页或者APP的行为,是不是可以从头到尾可以分成以下几个步骤来理解呢?

有一天在某个无聊的会议上你准备掏出手机

移动终端的使用情景比较特殊,用户往往在时间紧张的情况下使用,即在移动过程中或者在需要分散注意力的时候使用。

比如公交上、地铁上、某些会议期间、餐厅里边等(不排除存在特例,但是至少大部分移动终端的使用情景是这样的)。

既然这样我们是不是得让我们的网页或者APP做到适应分散注意力的环境,在书中作者基于聚焦点原理列出了以下几点建议:

  • 屏幕上突出显示聚焦点;
  • 通过动画创造临时聚焦点;

  • 标明内容的主次关系;

    前两点没什么好说的,但是在标明内容的主次关系中作者抨击了下扁平化设计,认为这种设计会导致页面中没有任何重点,所有元素都处于同一背景层,用户需要消耗更多精力来发现重要元素;
    所谓扁平化设计维基百科给出的定义是一种交互界面的设计理念,强调对于三维效果图像元素使用的最小化(其元素也包括阴影、渐变和纹理),亦专注于使用去除了冗余信息的图像元素,排版及纯色效果
    虽然扁平化设计确实可能会存在重点不突出的问题,但是其实也有自身的优势,比如:画面看起来更为流线型、更能适应不同尺寸的屏幕、元素简单导致性能更高等;
    暂且认为各有优劣吧,具体情况具体分析。

  • 预防用户分心

    大致意思就是设计时得考虑用户分心的情况,比如某些提示信息在决定采用短暂显示的形式来保证页面更加简洁时,应当慎重一点,除非是一些不太重要的信息,即使用户没有看到也无关紧要。

之后你打开富途牛牛准备看下自己的收益情况

当用户看到移动端的屏幕时,需要使用选择性注意的能力,区分有用和无用的内容;屏幕上无用的信息越多,大脑选择耗费的认知成本就越大。

因此我们设计时就得考虑怎样让用户更方便的获取信息,在书中作者也列出了以下几点建议:

  • 清理屏幕,给内容让位;

    这里引申出交互时刻的概念,即用户看到的屏幕内容、刚刚进行的操作、最终的目标等不同因素,决定了用户的需求也不一样。
    比如以前总结的关联搜索等复杂交互最好是单独新开页面就是基于上述原理。

  • 让文字信息利于阅读。

    专业的事还是交给专业的人来做,文字排版其实也涉及很多东西,这里暂且只列下书中作者提及的几点:1、把阅读内容分成小段;2、控制好背景和文字的对比度;3、优化文字大小;4、注意行间距与页边距等。

意外发现自己大赚了一笔,这时候你决定卖出股票落袋为安

用户通过触控与智能手机和平板电脑交流、下达指示、在屏幕上运行各种功能、享受服务……所以需要考虑这种操作模式的内在制约。

用户偏好使用左手还是右手、持握电话的习惯姿势、移动终端的尺寸大小等因素都存在差异,因此用户触及屏幕区域的难易程度也不尽相同,如下图所示:

同样不过多展开,但是有一些很基础的点;

  • 足够大的活动区域

    如果屏幕上元素过小,用户就难以避免的出现操作失误,另外这里的说的元素大小,并不一定是指元素显示出来的大小,而是包括了元素周围可以响应触摸的区域。
    还要注意的是可触控区域并非越大越好,一旦超过了一定限度,反而会起相反的效果。

  • 在可触控区域之间拉开距离

    可触控区域之间距离越近,用户同样就越难以避免的出现操作失误,但是距离越远就会浪费屏幕空间,所以这里也需要合理规划。

  • 功能可供性

    功能可供性是事物自身特点所显示出的操作可能性,简单来说,凸起矩形一般就是可点击按钮,按钮上边文字是买入,那么点击这个按钮就能买入股票等。
    功能可供性还有一点要注意的是,在设计界面的各种功能可供性元素时,切记不要出现错误操作指示,即形式上貌似可以操作,但实际上不可操作的元素。

你点击卖出股票然后被弹框告知“交易成功”

用户不可能百分百知道哪些元素可操作,哪些元素不可操作,既然如此,在用户操作后立即提供反馈显得非常重要。通过反馈,让用户知道系统已感知到他的行为,而且如果某个操作处理时间相对较长,提供反馈就更显得尤为重要,这样用户才不会对操作是否正确产生疑虑,从而避免进行没有意义的操作。

其它

上述小故事中仅仅涉及了《匠心体验-智能手机与平板电脑的用户体验设计》书中的一小部分基础知识点,还有大量关于操作手势体验传感器体验声音体验等待体验情感体验移动应用导航设计移动应用效率问题用户教学设计等高端知识点,如果有兴趣可以查找相关资料详细了解。

ThreeJS四步制作一个简易魔方

https://github.com/newbieYoung/NewbieWebArticles/blob/master/threejs-webgl-cube.html(2017-05-13T09:06:44Z)

© Young 2017-02-28 16:38
Welcome to My GitHub

概述

去年之所以再次兴起了学习WebGL的念头,主要是有两个原因;第一个是想制作一个魔方玩,另外一个是想用Web技术还原一些经典电影的经典镜头,比如《Cast Away》又译《荒岛余生》中电影快结束时主人公站在十字路口的场景。

现在看来我想第一个目的已经达成了,有点可惜的是在此之前已经有很多人做过同样的事了,比如:

站在前人的肩膀上整个事情简单了很多,但是解决问题所带来的成就感也相对减少了很多,这也是没有办法的事情了。

前言

首先我假设你是一名前端工程师而且已经初步了解WebGL和ThreeJS的基础知识,比如坐标系、相机、光线、矩阵、弧度等;

如若不清楚可以浏览以前几篇文章快速入门:

编码

第一步:搭架子

从我短暂的ThreeJS编程经验来看,有个通用的的架构能处理大部分情况,如下:

第一步完整代码如下:

https://github.com/newbieYoung/Threejs_rubik/blob/master/step1.html

第二步:画外型

魔方的外型很简单,就是由一些小正方体组成的一个大正方体而已。

用一个方法封装起来:

基本都是些ThreeJS对象的简单运用,比如盒子对象BoxGeometry、纹理Texture、材质MeshLambertMaterial等,纹理主要是用来描述物体表面静态属性的对象,材质主要是用来描述物体表面动态属性的对象,比如处理光照等,不知道这么理解有没有问题。

其中faces方法主要是生成一块黑色边框的大正方形其内部是某种颜色填充的圆角小正方形的canvas画布,用来充当纹理渲染魔方中小正方体的某个面。

如果把这个canvas画布渲染出来,大致是下边这样的:

另外基于魔方中心在坐标系原点从而推算出所有小正方体中心点坐标可以画图理解如下:

最后需要把生成的魔方加入到场景中才会被渲染出来:

第二步完整代码如下:

https://github.com/newbieYoung/Threejs_rubik/blob/master/step2.html

此时在浏览器中运行第二步完整代码应该是下边这个样子的:

此时相比于第一步一片空白的页面而言,此时页面中多了一个类似九宫格的正方形,有人可能会说大兄弟要画这么个玩意用得着ThreeJS吗,DIV+CSS分分钟搞定……

其实之所以会这样是因为我们设置的相机的位置是在坐标系的Z轴,魔方的中心在坐标系原点,它们刚好处于同一条直线上,导致显示出来的是魔方的正视图。

第三步:操控魔方视角

第二步完成之后有个很严重的问题,我们只能看到魔方的正面,为了解决这个问题我们需要让相机随着鼠标或者触摸点的移动而移动;

在ThreeJS中作者提供了很多种视角控制类库,比如:

  • 轨迹球控件TrackballControls(最常用的控件,用鼠标控制相机移动和转动);

  • 飞行控件FlyControls(飞行模拟器控件,用键盘和鼠标控制相机移动和旋转);

  • 翻滚控件RollControls(翻滚控件是飞行控件的简化版,控制相机绕Z轴旋转);

  • 第一人称控件FirstPersonControls(类似于第一人称视角的相机控件);

  • 轨道空间OrbitControls(类似于轨道中的卫星,控制鼠标和键盘在场景中游走);

  • 路径控件PathControls(控制相机在预定义的轨道上移动和旋转);

在这里我使用OrbitControls控制器,具体用法很简单如下:

首先引入代码:

然后根据相机以及画布初始化即可:

第三步完整代码如下:

https://github.com/newbieYoung/Threejs_rubik/blob/master/step3.html

此时在浏览器中运行第三步完整代码应该是下边这个样子的:

第四步:转动魔方

经过前三步在视觉方面简易魔方已经完成了差不多了,但是依然欠缺很重要的东西,没办法转动连最基本的可玩性都没有;

要想转动魔方需要解决以下几个问题:

  • 首先得确定触摸点

也就是说必须得在代码里边判断出魔方的哪个部位被触摸了,Canvas编程是没办法像DOM编程那样有完备的事件机制支持的;所以这个问题需要其它解决办法,比如在2D Canvas我们可以根据坐标来判断当前鼠标或者触摸点在哪个元素上,从而假定该元素获得了焦点;但是在WebGL中存在一个平面2D坐标映射为3D坐标的问题,万幸ThreeJS也提供了对应的解决方案Raycaster

简单来说就是模拟一道光从屏幕点击或者触摸的位置上开始,以相机朝向为方向,然后检测光线与物体的碰撞,可以得知距离、碰撞点以及哪些物体先碰撞哪些物体慢碰撞。

首先得知道在页面的2D坐标,这里可以通过监听鼠标事件或者触摸事件来完成;

Raycaster的调用也很简单,但是需要注意的是当魔方获得焦点时需要屏蔽视角的转动,给控制器的enabled属性置为false即可;另外如果魔方正在转动时下一次转动应该等这次转动完成才可以,这里用isRotating变量控制;开始一次转动时置为true,转动结束之后才还原为false。

  • 然后得确定转动方向

转动魔方时应该是存在有六个方向的,分别是X轴正方向、X轴负方向、Y轴正方向、Y轴负方向、Z轴正方向、Z轴负方向;

先根据滑动时的两点确定转动向量,然后判断转动向量和这六个方向向量夹角最小的方向即为转动方向;

但是光知道方向其实还是不能够转动魔方的,比如下图中从点A滑动到点B和从点C滑动到点D,滑动方向都是X轴的正方向,而且还有其它情况滑动方向是X轴正方向的;对魔方来说这完全是两种不同的情形,所以我们还需要知道是在哪个平面滑动的。

判断是在哪个平面,我们可以通过该平面的法向量和哪个坐标轴平行来判断,比如如果滑动平面的法向量平行于坐标系的Y轴且等于Y轴正方向的单位向量,那么该滑动平面肯定是魔方的上平面,以此类推;上边那个判断转动方向的方法可以优化为如下这个样子:

那么接下来的问题就是怎么获得滑动平面的法向量了,所幸ThreeJS的光线碰撞检测机制除了能得到碰撞物体、碰撞点,还能得到碰撞平面;已知平面那么就可以获得平面法向量了。

但是ThreeJS中有个问题需要我们注意,在ThreeJS中存在物体自身坐标系和世界坐标系的区分,在初始化时物体的坐标和世界坐标系一致,但是当物体发生变化之后它自身的坐标系也是会发生变化的;比如说刚开始某个物体上平面的法向量就是其自身坐标系Y轴正方向的单位向量,同时也是世界坐标系Y轴正方向的单位向量,如果该物体旋转180度之后,其上平面的法向量还是其自身坐标系Y轴正方向的单位向量,但是却是世界坐标系Y轴负方向的单位向量了,如图:

所以不能使用魔方中小正方体的碰撞平面,因为小正方体的坐标系是会随着小正方体的变化而变化的,此时需要再加入一个和魔方整体大小一样的透明正方体,然后根据该透明正方体的碰撞平面的法向量来判断。

  • 再然后我们得根据转动方向、触发点获取转动物体

比如上图中从点A滑动到点B,转动物体是魔方上平面的所有小正方体;

至于怎么判断,有两种方法,第一种可以根据小正方体的中心点来判断,比如如果转动的是魔方上平面的正方体,那么已知触发点所在正方体的中心点,根据其Z轴大小就可以确定其它小正方体了;

还有一种办法则是根据小正方体初始化时的编号规律来判断,转动之后更新编号,保证其规律不发生变化,后续判断依旧即可,从下边的简图很容易就能看出其编号规律。

第二种方法有个好处在于,我们可以把转动之后更新的编号和初始化时的编号进行比较,来判断魔方是否回到初始化状态,也就是被还原正常了。

这种方法不知道有问题没有,由于本人不会玩魔方所以一直没有测试。

  • 最后是制作转动动画

制作转动动画的过程中使用requestAnimationFrame,这没什么要说的;唯一要注意的地方还是关于物体自身坐标系和世界坐标系的问题,举例来说,绕世界坐标系Y轴旋转的方法应该是如下图所示:

第四步完整代码如下:

https://github.com/newbieYoung/Threejs_rubik/blob/master/step4.html

可以通过点击下边的链接来查看在线例子

https://yii-server.lione.me/harp-server/simple-cube

拓展

至此一个没什么卵用的魔方已经完成了,是时候开下脑洞了;

  • 玩具店应该只有最简单的三阶魔方买,但是对这个例子稍加拓展,你甚至可以玩100阶魔方。

  • 这个例子稍加扩展应该是能做出一些计时、计步的魔方游戏的。

  • 这个例子稍加扩展结合摄像头和自动还原算法,应该是可以做到扫描现实中的魔方,然后根据自动还原算法还原,得到一步步还原魔方的动画演示例子的。