一张纹理做天空盒

前言

  前段时间做了一件很有意思的事,使用一张普通纹理图片做成了一个天空盒(SkyBox),其实是半个,因为最终形成的天空盒是上下对称的,看起来有天空之境的感觉。

前言

  前段时间做了一件很有意思的事,使用一张普通纹理图片做成了一个天空盒(SkyBox),其实是半个,因为最终形成的天空盒是上下对称的,看起来有天空之境的感觉。

天空盒篇

  常见的天空盒一般采用立方体包围盒做 geometry,再使用 CubeMap 将 6 张纹理图贴到立方体包围盒上,但是如果只用一张纹理图片,要映射到立方体包围盒上,无论怎么展 UV,都会造成部分棱和顶点 UV 聚集或突变,从而在这样的地方造成贴图扭曲或错位的不协调现象。

  既然立方体包围盒不可行,就只有采用另一种 geometry —— 球 了,但是直接用球作为 geometry 会造成两种问题:1、球的极点处会有扭曲现象,因为球极点处的 UV 是聚集的,同一个位置,V 都为 1 或 0,而 U 则为 [0, 1] 都有,即在该位置处一个 V 对应多个 U,自然会造成扭曲;2、由于球是一个圆周,所以 U 为 0 的位置和 U 为 1 的位置是同一个,对于一张普通纹理图,这就会造成错位,能看到非常明显的接缝线。所以需要重新展开球面的 UV,而常见的展 UV 方法主要有以下几种:

  1. 对于平面,uv 坐标自然通过线性归一化解决,取左下角坐标和右上角坐标,得到平面的宽度和长度,再用当前坐标减去左下角坐标,最后除以平面宽度和长度得到 uv 坐标。
  2. 对于球面,自然是取水平方位角 \(θ, \theta \in [-\pi, \pi]\) 和 垂直仰角 \(φ, φ\in [0, \pi]\) 作为 uv 坐标。对于 threejs 而言,方位角在 XZ 平面上,可计算 \(\theta = Math.atan2(z, x)\),仰角 \(φ = Math.acos(-y)\),其中 \(x,y,z\) 为当前坐标归一化后的值,则 \(u = (θ +\pi)/(2*\pi)\)\(v=φ/\pi\)
  3. 对于一般的曲面,可能会依据当前顶点的法线计算 uv 坐标。对法线向量 x,y,z 三个分量的值进行排序,取最小的两个作为 uv,或者将最小的两个除以最大的一个作为 uv(CubeMap 中通常用这种进行 6 个面的纹理映射,最大的那个决定映射 6 个面中的哪一面),或者取固定的两个分量作为 uv ,或者得到最小的两个分量索引取对应顶点坐标的分量值作为 uv 等等,若 uv 取值为 [-1,1],则还需要做 \(uv = uv *0.5+0.5\) 。根据法线计算 uv 一般需要根据不同的使用情况选择不同的 uv 计算策略。

  Shaun 很快的解决了第二个问题,想要将一张普通纹理图片贴在整个球面上而不留下接缝线是不现实的,而铺在半个球面上,另一半球面做对称,这个是可行的,而且看起来的效果也不错,计算 uv 就很简单了,原始的 v 不需要改变,只需要将 u 从 [0, 0.5] 映射为 [0, 1],从 [0.5, 1] 映射为 [1, 0],即 \[ f(u)=\begin{cases} 2*u, 0 \le u \le 0.5 \\ 2*(1-u), 0.5 \le u \le 1 \end{cases} \] ,接缝线问题使用对称性巧妙的解决了,但第一个问题,极点扭曲现象,还是没法解决,接下来才是真正的难点 ╯︿╰。

  为了解决极点扭曲问题,首先需要知道极点扭曲的根本原因是同一个位置对应了多个 uv,所以要么将这些 uv 给散开,要么抛弃一部分 uv,散开 uv 的方式 Shaun 没想出来,抛弃一部分 uv 倒是有一种简单的方式,不过抛弃 uv 也意味着会损失一部分纹理,就这个天空盒而言,抛弃一部分 uv 是能接受的,不然平面到球面必然有形变。具体抛弃方法为,将球面铺平,变为一个大圆面,即忽略球面顶点坐标的 z 值,求出球的 AABB,将 AABB 铺平,即为大圆的外接正方形,用计算平面 uv 的方式重新球上各顶点的 uv 坐标,这种方式的确能解决极点扭曲的问题,但又带来了一个更为严重的问题。

  这个问题就是,球面上半部分显示很正常,但是越靠近底部大圆的部分,纹理拉伸的越厉害,造成天空盒四周都出现很严重的纹理拉伸现象。出现纹理拉伸现象的原因也很好理解,那就是越靠近底部大圆的顶点,uv 坐标之间的间隔越小,即同样的 uv 间隔,顶点之间的距离变大了,在进行纹理插值时,自然会导致纹理拉伸现象。知道问题出现的原因了,那怎么解决了?这又是一个新问题 😔。

  导致纹理拉伸现象的原因是线性映射,那能不能对计算好的 uv 进行非线性映射,从而抵消 OpenGL 线性纹理插值的影响,非线性映射最重要的是找到合适的非线性函数,常见非线性函数一般有幂函数(伽马变换就是一种幂函数变换,幂 < 1 拉伸小值,幂 > 1 拉伸大值),对数函数,指数等,对于 Shaun 这里的情况,常规的非线性函数肯定是不行的,只能自己想一个函数。

  注意到,这是在球面上,要取一个非线性映射函数,自然需要从圆的弧长入手,先计算圆的弧长。计算弧长的本质是勾股定理 \(ds=\sqrt{(dx)^2 + (dy)^2} = \sqrt{1+(dy/dx)^2} * dx\),对于圆 \(x^2 + y^2 = r^2, y \ge 0\),即 \(y=\sqrt{r^2-x^2}\),有 \(dy/dx = -x/ \sqrt{r^2-x^2}\),则对于圆的弧长 \(L=\int \sqrt{1+x^2/(r^2-x^2)}dx = r \int 1/ \sqrt{1-(x/r)^2}d(x/r) = r*arcsin(x/r)|\),即对于圆在第一象限的弧长可以为 \(L=r*arcsin(x/r), 0\le x \le r\)

  由于 v 是均匀的,所以只需要对 u 进行拉伸即可,离圆心越近的点,则越接近 0.5, 离圆心越远的点则越偏离 0.5,非线性拉伸公式为 $ u = α * (u-0.5) + 0.5$,其中 \(α = L / (\pi*r/2), L中的x为点到圆心的距离\),使用这种拉伸后,天空盒四周的纹理拉伸现象确实不见了,但是又引入了新的问题,天空盒顶部出现了局部拉伸现象,出现微弱的纹理模糊,虽然区域不大,依靠一些手段可以让用户看不到这块区域,但是不能自欺欺人,这一块存在总让 Shaun 觉得很不舒服,于是,就有了下面的终极解决方案。

  Shaun 最终想出解决方案是:既然单纯的拉伸不能完美解决问题,那还是只能从问题根源入手,完全重新计算 uv 坐标,这次计算 uv 坐标,还是需要借助上文的 \(α\)。具体计算 uv 坐标的方式如下:

  1. 先计算球面上顶点相对大圆圆心的角度,即 \(angle = Math.atan2(y - center.y, x - center.x)\),其中 center 即为大圆圆心(0, 0)。
  2. 根据顶点到圆心的距离得到 \(\alpha\),计算 \(α_x = α * cos(angle), α_y=α * sin(angle)\)
  3. 计算 uv:\(u = α_x * 0.5 + 0.5, v=α_y*0.5+0.5\)

  使用这个方式计算 uv,天空盒的全部问题都解决了,天空盒没有任何拉伸扭曲等令人看起来不协调的地方,至于对称也说的过去,Shaun 个人感觉挺漂亮的 ( ̄▽ ̄)"。自此天空盒的事就算是告一段落了。

后记

  做这个天空盒,确实花费了 Shaun 了不少力气,在做的那两天,满脑子都是为什么会拉伸扭曲,以及如何解决拉伸扭曲,最终想出了这套方案,简单优雅,最后的效果也是完美达到了 Shaun 的预期。不过一般人应该也用不到 Shaun 这套方案,这只是 Shaun 自己想做做而已,搞不出来没关系,搞出来当然是好的。

Windows Terminal 尝鲜小记

前言

  Windows Terminal 正式版在两个月之前终于发布了,正好最近找了点时间尝尝鲜,感觉确实可以,Cmder 可以退休了。

前言

  Windows Terminal 正式版在两个月之前终于发布了,正好最近找了点时间尝尝鲜,感觉确实可以,Cmder 可以退休了。

尝鲜篇

  直接在 Microsoft Store 安装,顺便安装好 Powerline,执行以下三个命令:

1
2
3
Install-Module posh-git -Scope CurrentUser
Install-Module oh-my-posh -Scope CurrentUser
Install-Module -Name PSReadLine -Scope CurrentUser -Force -SkipPublisherCheck // 使用 powershell core 则必选

然后执行 notepad $PROFILE ,在弹出的记事本中添加:

1
2
3
Import-Module posh-git
Import-Module oh-my-posh
Set-Theme Paradox

重启 terminal,若出现 “无法加载文件 ***.ps1, 因为此系统上禁止运行脚本”,则需要执行 set-executionpolicy RemoteSigned,使powershell 能顺利执行该脚本。

  由于目前 Windows Terminal 不会自动注册右键快捷菜单,所以需要手动修改注册表,执行 mkdir "%USERPROFILE%\AppData\Local\terminal" 后,在网上找一个终端图标,命名为 wt_32.ico,将该图标复制到 %USERPROFILE%\AppData\Local\terminal 目录中, 新建 wt.reg 文件后直接双击执行,该注册表文件的内容如下:

1
2
3
4
5
6
7
8
9
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Directory\Background\shell\wt]
@="Windows terminal here"
"Icon"="%USERPROFILE%\\AppData\\Local\\terminal\\wt_32.ico"

[HKEY_CLASSES_ROOT\Directory\Background\shell\wt\command]
@="C:\\Users\\[user_name]\\AppData\\Local\\Microsoft\\WindowsApps\\wt.exe"

其中 [user_name] 是使用者电脑的用户名,wt_32.ico 可以是随便找的一张缩略图,也可以直接用 icons - yanglr 中的 wt_32.ico。

  为了简单美化一下 Windows Terminal 界面,需要安装 Cascadia Code GitHub releases page 中 Cascadia Code PL 或 Cascadia Mono PL 字体,Shaun 因为只是尝鲜所以就简单配置了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"theme": "dark",
"profiles":
{
"defaults":
{
// Put settings here that you want to apply to all profiles.
},
"list":
[
{
// Make changes here to the powershell.exe profile.
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"name": "Windows PowerShell",
"commandline": "powershell.exe",
"hidden": false,
"startingDirectory" : ".",
"acrylicOpacity" : 0.00000001,
"colorScheme" : "Campbell",
"cursorColor" : "#00CCFF",
"fontFace" : "Cascadia Mono PL",
"useAcrylic" : true
},
{
// Make changes here to the cmd.exe profile.
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"name": "命令提示符",
"commandline": "cmd.exe",
"hidden": false
},
{
"guid": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}",
"hidden": false,
"name": "Azure Cloud Shell",
"source": "Windows.Terminal.Azure"
}
]
},

  为了在 VSCode 中使用 Windows Terminal ,需要简单设置一下默认终端,首先将设置默认终端 "terminal.integrated.shell.windows" 注释掉或者直接不设置,添加设置:

1
2
"terminal.external.windowsExec": "C:\\Users\\[user_name]\\AppData\\Local\\Microsoft\\WindowsApps\\wt.exe",
"terminal.integrated.fontFamily": "Cascadia Mono PL",

如此可在 VSCode 中集成 Windows Terminal,并将其作为默认终端。

后记

  在这两三天的使用过程中,发现 Windows Terminal 和 Cmder 之间还是存在差距的(如在输入命令过快的时候,tab 键补全跟不上等问题),暂时就两者先并行使用一段时间吧,等后续更新巨硬修复这些问题,相信 Windows Terminal 是能代替 Cmder 成为 Windows 首选终端的。

参考资料

[1] 【避坑】PowerShell:因为在此系统上禁止运行脚本 附原因和解决办法

[2] 新发布的Windows Terminal如何添加到右键菜单?

[3] Setting Windows Terminal as Default External Terminal in Visual Studio Code

快速判断三角形与长方体相交

前言

  一种快速判断空间中三角形与 box 相交的方法,出自论文:Tomas Akenine-Moller. Fast 3D triangle-box overlap testing. A. K. Peters, Ltd. 2002.

前言

  一种快速判断空间中三角形与 box 相交的方法,出自论文:Tomas Akenine-Moller. Fast 3D triangle-box overlap testing. A. K. Peters, Ltd. 2002.

预备篇

  该论文的理论基础来自分离轴理论(separating axis theorem, AST),AST 常用于检测两凸多边形是否相交。一句话描述 AST 即为:若两多边形能用一条直线分隔开,则两多边形不相交。如何判断该直线存在即为 AST 的关键。常用的判断方法为找出两多边形所有边向量(多边形相邻两点构成的向量,顺时针或逆时针都行)的法向量,使用向量点积分别计算两多边形在各法向量上的投影(一般以多边形上的点和原点构成一个向量与法向量做点积),从而得到两个投影集合,判断两集合是否相交(找出两个集合的最大值和最小值,若最小值大于最大值,则不相交),若不相交,则 AST 中的直线存在,即两多边形不相交,若相交,则继续判断在其它法向量上的投影,若所有法向量上的投影都相交,则两凸多边形相交。AST 常用于二维下判断两凸多边形的相交情况,三维下的情况比较复杂。

正文篇

  该论文给定 13 个向量,若 box 的边和三角形的边在这 13 个向量中的投影均相交,则认为 box 与三角形相交。为简化运算,box 直接假定为轴向包围盒(axis-aligned bounding box, AABB),坐标轴原点为 box 中心,由于可以将普通 box 通过旋转平移等一系列变换,变成以原点为中心的 AABB, 所以该假定是有效的。设三角形的顶点为 \(v_0, v_1, v_2\) ,box 的一半长宽高为 \(h_x, h_y, h_z\) ,则这 13 个向量分别为 \(e_0(1, 0, 0), e_1(0, 1, 0), e_2(0, 0, 1)\) ,三角形的法向量 \(n\) (通过三角形两边向量叉乘得到),剩下九个向量分别为 \(a_{ij} = e_i \times f_j , i,j \in \{0, 1, 2\}\) ,其中 \(f\) 为三角形的边向量 \(f_0 = v_1 - v_0, f_1 = v_2 - v_1, f_2 = v_0 - v_2\)\(\times\) 代表向量叉乘。若直接这样一个个的计算投影是否相交,虽然能达到目的,但快速就无法体现了,所以作者根据向量计算方法和一些策略将其中一些需要计算投影的地方极大的简化了,所以加快的计算速度。具体简化过程为:

  1. 首先来看最后九个向量,\(a_{00} = e_0 \times f_0 = (0, -f_{0z}, f_{0y})\) ,三角形三个顶点在在该向量上的投影分别为:

    \(p_0 = a_{00} \cdot v_0 = (0, -f_{0z}, f_{0y}) \cdot v_0 = v_{0z}v_{1y} - v_{0y}v_{1z}\)

    \(p_1 = a_{00} \cdot v_1 = (0, -f_{0z}, f_{0y}) \cdot v_1 = v_{0z}v_{1y} - v_{0y}v_{1z} = p_0\)

    $p_2 = a_{00} v_2 = (0, -f_{0z}, f_{0y}) v_2 = (v_{1y} - v_{0y})v_{2z} - (v_{1z} - v_{0z})v_{2y} $

    由于 \(p_0 == p_1\), 所以在求最大最小值时只需要做一次比较,接着求 box 在该向量上的投影,box 中心在原点,所以投影半径 \(r\) 可以以一种简单的方式求出:

    \(r = h_x|a_{00x}| + h_y|a_{00y}| + h_z|a_{00z}| = h_y|a_{00y}| + h_z|a_{00z}|\)

    计算投影是否重合也很简单:

    \(if(min(p_0, p_2) > r \ || \ max(p_0, p_2) < -r) \quad return \ false\) 否则两者投影相交,继续计算其它向量。

  2. 三个轴向单位向量 e 中的投影是否重合就更好判断了,完全不需要计算投影,只需要计算三角形的最小 AABB,判断两个 AABB 是否相交即可(取两个 AABB 最小的顶点和最大的顶点,从三维上判断最小是否的大于最大的即可,若任意一个维度上最小的比最大的大,则两者不相交),

  3. 至于判断最后一个向量——三角形的法向量上的投影是否重合,相当于判断三角形所在平面是否与 box 相交。判断 box 与平面相交有一种简单快速的方式,即通过公式 \(|d| <= a_1 |n \cdot A^1| + a_2|n \cdot A^2| + a_3 |n \cdot A^3|\) ,其中 \(d\) 为 box 中心到平面的距离(中心点到平面上一点构成的向量与平面法向量做点积),\(n\) 为平面法向量,\(A^1\) 为 box 侧面法向量,对于 AABB 可为 \((1, 0, 0)\)\(a_1\) 为 box 中心到侧面的距离,对于 AABB 可为 \(h_x\),同理 \(A^2\) 为 box 顶面法向量,对于 AABB 可为 \((0, 1, 0)\)\(a_2\) 为 box 中心到顶面距离,对于 AABB 可为 \(h_y\)\(A^3\) 为 box 正面法向量,对于 AABB 可为 \((0, 0, 1)\)\(a_3\) 为 box 中心到正面距离,对于 AABB 可为 \(h_z\),即在 AABB 中,该公式可简化为 \(|d| <= h_x|n_x| + h_y|n_y| + h_z|n_z|\) ,满足该公式,即可判定平面与 AABB 相交。

  如此 13 个向量全部判断完毕,如全都相交,则可认定三角形与长方体相交,若其中一个不相交,则三角形与长方体不相交。三维的都能判断,二维的三角形与矩形相交判断就更简单了,分成两类法向量后,利用向量运算先简化运算量,再计算投影是否相交即可。

附录

  还有一种根据距离判断两个 AABB 是否相交的办法,即先取两个 AABB 的中心 \((x_1, y_1, z_1)\)\((x_2, y_2, z_2)\),然后计算两个中心点之间的三个维度的距离,将 x 维度的距离与两个AABB 的 \(h_x\) 之和比较,若中心点 x 维度的距离较大,则不相交。即:\(if (|x_1 - x_2| > h_{x1} + h_{x2}) \quad return \ false\) ,否则比较 y 维度, z 维度,若所有都小,则两 AABB 相交。这种方式是 Shaun 在一次面试中被问到没答出后在网上找到的答案,其实感觉和比较最小最大顶点也差不多,都是从不相交出发,因为直接判断相交基本不可能,而不相交很容易判断,把所有的不相交情况判断完,那就只剩相交了,可惜面试官只想要这种方案 ╮(╯▽╰)╭。

参考资料

[1] Code by Tomas Akenine-Möller

[2] Simple Intersection Tests For Games

网页菜单纯 css 实现

前言

  最近搞了些前端的工作,本来做菜单栏的时候想直接用 bootstrap 的,但是感觉 bootstrap 太大了,而且依赖有点多,在 webpack 中也不是很好打包(虽然可以绕过去),所以就索性自己在网上找了一些实现方式,改改感觉也还可以。这次主要实现了两种菜单栏,具体如下。

前言

  最近搞了些前端的工作,本来做菜单栏的时候想直接用 bootstrap 的,但是感觉 bootstrap 太大了,而且依赖有点多,在 webpack 中也不是很好打包(虽然可以绕过去),所以就索性自己在网上找了一些实现方式,改改感觉也还可以。这次主要实现了两种菜单栏,具体如下。

鼠标悬停下拉菜单

  鼠标悬停下拉菜单应该是最常见的一种菜单栏了,当鼠标悬停在菜单栏上时,子菜单缓缓下拉,看起来就很舒服,用 flex 布局结合列表也很好实现(不用像以前那种 float 了,舒服)。具体实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<style>
.flex-body {
display: flex;
flex-direction: column;
}

header {
z-index: 1;
}

ul,
ul li {
list-style: none;
margin: 0;
padding: 0;
}

.menu {
display: flex;
justify-content: start;
}

ul li {
width: 100px;
height: 50px;
line-height: 50px;
}

.menu li .submenu {
/* display: none; */
background-color: aqua;
}

.submenu li {
height: 0;
line-height: 0;
opacity: 0;
visibility: hidden;
}

.menu li:hover .submenu li {
/* display: block; */
height: 50px;
line-height: 50px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="flex-body">
<header>
<ul class="menu">
<li>
<a>menu1</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu2</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu3</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
</ul>
</header>
<main>mainmainmainmainmainmainmainmainmainmain</main>
</div>

  ※注: 实现该菜单栏有两个需要注意的点:1、不能用 display: noneblock 来使子菜单消失或出现,因为这样会造成缓缓下拉的动画失效,transition 并不支持 display,所以只能用 visibilityheight 来共同实现,以达到下拉动画效果;2、因为使用了 flex 布局,所以 z-index 只对同级 flex-item 有效,所以为防止菜单栏下面的内容出现在子菜单之上,即将子菜单栏位于最上层,需要将整个页面的布局都设置为 flex,并使 headerz-index 最大,如此才能保证子菜单的菜单覆盖 main 中的内容,不然就会有重叠干扰现象。

鼠标点击手风琴菜单

  手风琴特效也算是非常常见的了,一般的手风琴是鼠标悬停展开,这种比较好实现,难的是如何保持这种展开状态,focus 可以短暂保持展开状态,但是不能点击其他地方,局限性太大。所以需要引入其它的东西来记录这种展开状态,可以用 checkboxradiochecked 来记录这种状态,从而只用 css 即可实现该菜单,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<style>
input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 500px;
height: 50px;
line-height: 50px;
border: 1px solid black;
}

.menu-content {
width: 500px;
max-height: 100px;
overflow-y: auto;
}

.menu-content>li {
height: 0;
line-height: 20px;
overflow: auto;
opacity: 0;
visibility: hidden;
transition: all 1s;
}

input[data-prop="menu-recorder"]:checked+.menu-content>li {
height: 20px;
line-height: 20px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="accordion-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  这里需要注意一点的就是 height 从 0 到 100% 并不会触发 transition 渐变动画,而是需要确切的高度值变化才能触发,所以上文这里添加了个 <li> 标签,直接在该标签上添加动画,还有一种就是在 .menu-content 上设定确定的 max-height,也能触发动画,但是有个缺点就是前后 max-height 的差距太大时,动画效果就很不理想了,这时可能只能依靠 js 了。上文中 checkbox 也可用 radio 替换,效果略有差异,一个是能全部展开或收起,而另一个则是能且仅能展开一个。

Tab 标签页切换菜单

  这个菜单和上面那个菜单的实现非常相似,先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<style>
.tab-menu {
display: flex;
position: relative;
width: 500px;
height: 300px;
}

input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 100px;
line-height: 50px;
text-align: center;
border: 1px solid black;
border-right: 0;
box-sizing: border-box;
transition: all 1s;
}

.tab-menu .menu-item:last-child .menu-title {
border-right: 1px solid black;
}

.menu-content {
position: absolute;
left: 0;
top: 51px;
height: calc(100% - 50px);
overflow-y: auto;
width: 100%;
border: 1px solid #000;
box-sizing: border-box;
font-size: 24px;
text-align: center;
opacity: 0;
visibility: hidden;
transition: all 1s;

}

input[data-prop="menu-recorder"]:checked+.menu-content {
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="tab-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  从代码上看关键点就是 .menu-content 的定位方式了,采用了绝对定位,并将整个菜单栏设置为相对定位,以保证所有 tab 标签页内容位置和大小保持一致。当然 tab 标签页肯定是唯一的,所以只能用 radio 记录显示标签页了。其中为了保证 .menu-title.menu-content 的边框不重叠,所以在 .menu-title 中只设置 line-height,而 .menu-contenttop 比其多一个像素。

后记

  这三种菜单应该是最常见也是用的最多的了,纯 css 实现的方式也比较类似,无非就是 flex 布局以及借助 css3 强大的选择器功能(父类选择器不知要到猴年马月了,比较遗憾 😥),就能相对简单的实现了,当然借助一些 js 库或框架可能会更简单一些 ,但能用 css 为何不用呢 😄。

参考资料

[1] 利用flex实现的二级导航栏

[2] CSS3动画下拉菜单(当transition遇到display的坑)

[3] CSS3手风琴下拉菜单

[4] 教你两招用纯CSS写Tab切换

hexo-theme-chi主题更新小记

前言

  Chi 主题的大体结构功能算是写完了,但是还有一些个性化的东西需要添加,所以以后有关于 Chi 主题更新的部分就都写在这里吧。但是由于是 Shaun 个性化定制的一些东西,所以如果不是大 Bug 或大优化的更新,一般就不进 Chi 主题仓库 中了。

前言

  Chi 主题的大体结构功能算是写完了,但是还有一些个性化的东西需要添加,所以以后有关于 Chi 主题更新的部分就都写在这里吧。但是由于是 Shaun 个性化定制的一些东西,所以如果不是大 Bug 或大优化的更新,一般就不进 Chi 主题仓库 中了。

功能篇

1. 脚注提示功能

功能描述: 鼠标悬停在脚注上即可显示对应脚注内容。 Shaun 的脚注由于是采用 pandoc 渲染的,所以也是属于个性化定制,就不将这个功能放进 Chi 主题仓库中了。

解决方案: 还是利用 Bootstrap 的 tooltip 提示插件,具体实现代码如下:

1
2
3
4
5
6
7
$('a.footnote-ref').each(function (index, elem) {
let post_id = $(this).parents('article').attr('id');
let fn_href = $(this).attr('href');
elem.setAttribute('data-toggle', 'tooltip');
elem.setAttribute('data-html', 'true');
elem.setAttribute('title', $("#" + post_id + " " + fn_href).html());
});

遍历脚注,先获取文章 id,再获取对应文章下的对应脚注内容,使用 tooltip 提示。

Bug 篇

1. 图片没居中

问题描述: 上次那篇翻译的文章有几张图片,在放置的时候发现图片没有居中,查看代码后发现居中样式没写,但由于其图片标签 <img> 是放在一个 <figure> 标签中,由于不确定是不是 pandoc 渲染的问题,所以就没将这个修正放进 Chi 主题仓库中了。

解决办法:style.styl 文件中添加样式:

1
2
3
figure {
text-align: center;
}

即可让图片居中。

动画篇

1. 鼠标跟随动画

  其实一直都想把『奥日与黑暗森林』中的鼠标轨迹特效移植过来,但是苦于水平有限,一直没法做到,恰好 19 年 StackOverflow 的愚人节彩蛋中有个鼠标跟随动画很有意思,有好事者还专门将该彩蛋做了个脚本:Will there be an option to permanently keep this year's April Fools design active? 。查看代码,知道实现原理后,发现用 jQuery 和 CSS3 实现一个类似的效果也不算很难,于是 Shaun 就尝试做了一下,并简单的美化了一下,感觉效果还行,就加到自己的个性化主题上了。至于在 Chrome 中的小尾巴和 Firefox 中的卡顿现象,是 Shaun 故意的,因为 Shaun 觉得这个小尾巴很有意思,从这个动画看,Chrome 确实比 Firefox 要流畅一点。最终实现代码如下:

css 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.cursor-trail--item {
display: inline-block;
line-height: 1px;
position: fixed;
pointer-events: none;
touch-action: none;
z-index: 9999999;
will-change: transform;
font-size: 10px;
color: rgba(186, 227, 240, 0.1);
text-shadow: 0 0 2px #6CC2F8;
-webkit-animation: cursorTrail 0.9s ease;
animation: cursorTrail 0.9s ease;
}

@keyframes cursorTrail {
0% {
opacity: 1;
}

20% {
opacity: 0.5;
transform: scale(5);
}

100% {
opacity: 0;
transform: translate3D(0, -20px, 0) scale(1) rotate(90deg);
}
}

js 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$(document).on('mousemove', function (e) {
e.preventDefault();
if (this.time && (Date.now() - this.time) < 16) return;
this.time = Date.now();
let trail_character = '•';
let mouse_x = e.originalEvent.x || e.originalEvent.layerX || 0;
let mouse_y = e.originalEvent.y || e.originalEvent.layerY || 0;
mouse_x = mouse_x + 20;
mouse_y = mouse_y + 26;


$('#cursor-trail').append(
'<span class="cursor-trail--item" style="left:'
+ mouse_x
+ 'px;top:'
+ mouse_y
+ 'px;">'
+ trail_character
+ '</span>'
);

$('.cursor-trail--item').each(function () {
let item = $(this);
setTimeout(function () {
$(item).remove();
}, 900);
});
});

若不要小尾巴,则只需要将移除元素的时间改快一点就行了,除开上面的代码,还需要在 html 页面中添加一个元素 <span id="cursor-trail"></span>

2. 点击波纹

  一次偶然的机会,看到一篇这样的文章:还原一个 Windows 10 Metro 布局 。感觉其中的点击波纹动画很有意思,Shaun 决定把这个动画也放进自己的个性化主题中。至于代码就不贴了,毕竟就是上面文章提供的代码,只是用这个动画的时候发现了 Chrome 的一个问题,就是在进行模糊动画的时候会出现正方形的右边框和下边框,单个模糊不进行动画,不会有边框,进行动画但没模糊也不会有边框,两个同时一起就会出现问题,Firefox 没有这个问题,但 Firefox 存在另一个问题,就是鼠标快速连点的时候,鼠标跟随动画可能会出现问题。浏览器的问题 Shaun 暂时没法解决了,就先这样吧 ╮(╯▽╰)╭。虽然对 Chrome 为啥会出现这样的问题有些猜想,但还是不说出来丢人了,万一不是 Shaun 想的这样就尴尬了 ,,ԾㅂԾ,, 。

后记

有更新再继续更新吧。

[译]为什么深度学习没有取代传统的计算机视觉

前言

  这是一篇译文,译自:Why Deep Learning Has Not Superseded Traditional Computer Vision,原作者为:Zbigatron 。Shaun 水平有限,仅供参考学习,更多内容还请自行查看原文。

至于为什么要翻译这篇文章,算是回答别人的一个问题吧。

前言

  这是一篇译文,译自:Why Deep Learning Has Not Superseded Traditional Computer Vision,原作者为:Zbigatron 。Shaun 水平有限,仅供参考学习,更多内容还请自行查看原文。

至于为什么要翻译这篇文章,算是回答别人的一个问题吧。

为什么深度学习还没有取代传统的计算机视觉?

scale-cv-dl

  编写这篇文章的原因是在论坛中经常有人问:深度学习是否取代了传统的计算机视觉?或者类似的问题:深度学习效果这么好,还有继续研究传统计算机视觉的必要?

  这是个好问题,深度学习(DL)确实彻底改变了计算机视觉(CV)和人工智能,许多曾经看起来不可能解决的问题都解决了,甚至达到了 机器的结果比人类更好 的程度,比如图像分类。正如 我之前讨论的深度学习确实为计算机视觉做了很大的贡献

  但是深度学习只是计算机视觉的一个工具,并不是解决所有问题的万能药,所以,在这篇文章中,我想详细说明一下为什么传统的计算机视觉仍然非常有用,并且应该继续学习。

这篇文章主要有一下几个观点:

  • 深度学习需要大量数据;
  • 深度学习有时大材小用了;
  • 传统的计算机视觉可以辅助深度学习。

  但是在我开始讨论这些观点之前,需要先解释一下什么是传统的计算机视觉,什么是深度学习,以及深度学习为何这么 dio。

背景知识

  在深度学习之前,如果要实现图像分类这样的功能,需要首先进行 特征提取。特征即为图像中的信息块,能代表图像的部分信息,可以通过 边缘检测角点检测物体检测 等技术来提取特征。

  在进行特征提取和图像分类之类的工作时,一个想法是从一类对象(例如椅子,马等)的图像中提取尽可能多的特征,并将这些特征视为对象的一种定义(比如 词袋模型),然后在其他图像中查找这些定义,若另一个图像中的特征和定义的特征很相似,则该图像可能包含该特定的对象(如椅子,马等)。

  在图像分类中,这种特征提取的难点在于每个图像都必须选择哪些特征进行查找,当需要分类的类别开始增加时,比如 10 或 20 种类别,这种方式将变得很麻烦以至于不可能实现。当然也可以考虑角点、边缘和纹理信息,使用不同的特征可以更好的描述不同类别的对象,但是如果选择使用很多特征,则必须处理大量参数,所有这些参数都必须微调。

  深度学习中有一个端到端学习的概念,其含义为告诉机器要针对每个特定类别对象学习需要查找的特征,它为每个对象设计了最具描述性和显著性的特征,换言之,告诉神经网络要发现图像中每个类别的基本模式

  因此,通过端到端学习,不需要再决定使用哪种传统的计算机视觉技术来描述特征,机器自动选择好了,Wired magazine 说过:

如果想教一个神经网络识别一只猫,不需要让它寻找胡须、耳朵、皮毛和眼睛,只需要给它大量猫的图像,它就会自动识别猫,若它将狐狸错认成猫,不需要重写代码,只需要继续训练即可。

下图表示了特征提取(使用传统计算机视觉)和端到端学习之间的差异:

traditional-cv-and-dl

  以上就是需要的背景知识,下面开始深入探讨为什么传统的计算机视觉仍然有存在的必要。

深度学习需要大量数据

  首先,深度学习需要大量的数据,那些著名的图像分类模型就是在海量数据集上训练的,训练数据集中最大的三个是:

  一般的图像分类任务不需要这么多图片,但仍然需要很多,如果没办法获得这么多图片怎么办?我们还是得训练我们所有的数据(有些方法可以增多我们的训练数据,但这些都是人工方法)。但是,没有足够的数据支持,训练出来的模型可能会在训练集之外表现不好,因为机器没有洞察能力,无法对没有的数据进行分类。而且无法直观查看训练好的模型并手动调整里面的数据,因为深度学习模型里面有数百万个参数,并且这些参数在训练时会自动微调,某种程度上,深度学习模型就是一个黑盒子。

  传统的计算机视觉完全透明,可以很清楚的判断自己的解决方案在训练数据之外是否可行,而且可以深入了解算法中存在的问题。如果有没法解决的问题,也可以更容易的找出原因并调整。

深度学习有时大材小用了

  这可能是我支持传统计算机视觉技术研究的最佳理由。

  训练深度神经网络需要很长时间,而且需要专门的硬件(高性能 GPU),如果想在普通的笔记本上训练最先进的图像分类模型,可以去外面玩一个星期,回来之后应该还没训练完 :) 。而且,如果训练好的模型表现不好怎么办?必须调整训练参数并重新开始训练,这个过程有时会重复数百次。

  但有时候使用深度学习是完全没有必要的,因为有时传统的计算机视觉技术可以比深度学习更有效的解决问题并且代码更少。比如,我曾经做过一个项目来检测传送带上每个罐头是否都有红色的勺子,解决这个问题可以训练深度神经网络来检测勺子,也可以针对红色编写一个简单的阈值分割算法(红色的某个范围内的像素点为白色,其它像素点为黑色),然后计算有多少个白色像素点,后者明显简单的多,一个小时就完成了。

  了解传统的计算机视觉有时会节省大量时间和避免不必要的麻烦

传统计算机视觉提高改进深度学习技能

  了解传统的计算机视觉可以帮助我们更好地进行深度学习。

  例如,计算机视觉中使用的最常见的神经网络是卷积神经网络。但什么是卷积?它实际上是一种广泛使用的图像处理技术(例如 Sobel边缘检测)。了解这一点可以帮助我们了解神经网络正在做什么,并因此可以更好的设计和调整神经网络来解决问题。

  深度学习中还可以对图像进行预处理,所谓的预处理是指对训练的数据进行一定的处理(Shaun 注:比如图像增强,图像去噪等),这些预处理操作一般由传统的计算机视觉技术完成,比如:当没有足够的训练数据时,可以使用一种叫数据增强的技术,使用数据增强让图像进行旋转,平移,裁剪等操作,从而增加“新”图像,通过执行这些操作,可以成倍的增加训练数据集。

总结

  在这篇文章中,我解释了为什么深度学习还没有取代传统的计算机视觉技术,因此还需要研究后者。首先我发现了深度学习要想表现的足够好需要大量数据的问题,有时候没法获得大量数据,这时只能用传统计算机视觉技术代替;其次,对于特定的任务,使用深度学习可能大材小用了,传统计算机视觉有时比深度学习更有效且代码量也更少;最后了解传统的计算机视觉可以更好的学习深度学习,因为这可以使我们更好的了解深度学习的内部机制,并且可以使用某些预处理操作来改善深度学习结果。

  简而言之,深度学习只是计算机视觉的一个工具,不是万能药,不要只是因为它现在很流行所以使用它,传统的计算机视觉技术仍然十分有用,了解它可以节省时间和避免许多麻烦。

后记

  翻译这篇文章的原因在于,因为某些原因,Shaun 不得不回答一个 “为什么不用深度学习?” 的问题,虽然这里面有极大的因素是客观原因(设备不够 ╮(╯▽╰)╭),但 Shaun 不能明说,正好在网上看到这篇文章,加上也看到过一个问题(类似于 “既然已经能用深度学习做高层次的工作,为何还要用深度学习做底层的工作?”,比如深度学习能做全景分割(可以说是基本完成了图像理解),为何还要做目标检测,图像分割),预计以后有很大的概率也会被问到,所以就需要借用文中的一些观点来进行回答。至于为啥还要用深度学习做底层的工作,可以这样认为,如果用深度学习做底层工作效果不错的话,应该可以对上层工作进行一些辅助,并且可以为上层工作的做法提供一些新的思路。